From 03ca8170ba1973b4aa2872dbf45eebcb151070c6 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Thu, 6 Aug 2026 15:37:13 +0300 Subject: [PATCH 01/50] port current osrs colosseum and inferno onto upstream 5c replaces the stale mid-july PR 613 payload with the post-recut tree: item-table inventory obs, fused entity encoders in env .cu files, macos openmp and arm simd build fixes, and no python package. --- build.sh | 28 +- config/osrs_colosseum.ini | 246 +++ config/osrs_inferno.ini | 337 +++ .../colosseum/encounter_colosseum_boss.inc | 95 +- .../colosseum/encounter_colosseum_combat.inc | 189 +- .../encounter_colosseum_forecast.inc | 1065 +-------- .../colosseum/encounter_colosseum_helpers.inc | 37 +- .../colosseum/encounter_colosseum_lab.inc | 6 +- .../encounter_colosseum_mask_render.inc | 12 +- .../colosseum/encounter_colosseum_model.inc | 259 +-- .../encounter_colosseum_modifiers.inc | 415 +++- .../encounter_colosseum_movement.inc | 46 +- .../encounter_colosseum_obs_mask.inc | 1027 ++++----- .../encounter_colosseum_player_actions.inc | 527 ++--- .../encounter_colosseum_render_snapshot.inc | 72 +- .../encounter_colosseum_reset_spawn.inc | 59 +- .../encounter_colosseum_reward_step.inc | 373 ++-- ocean/osrs/encounters/encounter_colosseum.h | 2 + ocean/osrs/encounters/encounter_inferno.h | 3 + ocean/osrs/encounters/encounter_nh_pvp.h | 10 +- ocean/osrs/encounters/encounter_zulrah.h | 1017 +++++---- .../inferno/encounter_inferno_combat.inc | 66 +- .../inferno/encounter_inferno_forecast.inc | 73 +- .../inferno/encounter_inferno_helpers.inc | 39 +- .../inferno/encounter_inferno_model.inc | 48 +- .../inferno/encounter_inferno_obs_mask.inc | 177 +- .../encounter_inferno_player_actions.inc | 429 ++-- .../encounter_inferno_render_snapshot.inc | 263 +-- .../inferno/encounter_inferno_reset_spawn.inc | 80 +- .../osrs/osrs_colosseum_item_obs_generated.h | 24 + ocean/osrs/osrs_colosseum_item_obs_table.inc | 203 ++ ocean/osrs/osrs_combat.h | 5 - ocean/osrs/osrs_consumables.h | 6 +- ocean/osrs/osrs_encounter.h | 529 ++++- ocean/osrs/osrs_encounter_player.h | 70 +- ocean/osrs/osrs_env_profile.h | 57 +- ocean/osrs/osrs_interaction.h | 4 - ocean/osrs/osrs_inventory.h | 2 + ocean/osrs/osrs_inventory_actions.h | 288 +++ ocean/osrs/osrs_inventory_clicks.h | 347 ++- ocean/osrs/osrs_pathfinding.h | 4 +- ocean/osrs/osrs_player_consumables.h | 32 +- ocean/osrs/osrs_pvp_actions.h | 8 +- ocean/osrs/osrs_pvp_combat.h | 8 +- ocean/osrs/osrs_pvp_movement.h | 9 +- ocean/osrs/osrs_render.h | 21 + ocean/osrs/osrs_types.h | 39 +- ocean/osrs/osrs_visual.c | 428 +++- ocean/osrs/osrs_visual_net.h | 310 ++- ocean/osrs/scripts/check-builds.sh | 49 + ocean/osrs/tests/bench_colo_step.c | 275 +++ ocean/osrs/tests/probe_colo_sim_invariant.c | 224 ++ .../osrs/tests/probe_env_thread_determinism.c | 160 ++ ocean/osrs/tests/test_colo_hit_delay.c | 159 ++ ocean/osrs/tests/test_colo_prayer_wiring.c | 1 - .../tests/test_colosseum_forecast_exact.c | 256 ++- ocean/osrs/tests/test_colosseum_golden.c | 30 +- ocean/osrs/tests/test_colosseum_modifiers.c | 1957 ++++++++++++----- ocean/osrs/tests/test_inferno_attack_styles.c | 643 +++--- ocean/osrs/tests/test_inferno_golden.c | 36 +- ocean/osrs/tests/test_osrs_eat_attack_delay.c | 89 + .../osrs/tests/test_osrs_inventory_actions.c | 152 ++ ocean/osrs/tests/test_osrs_player_step.c | 185 ++ ocean/osrs/tests/test_zulrah_golden.c | 195 ++ ocean/osrs/tests/test_zulrah_hit_delay.c | 348 +++ .../osrs/tools/gen_colosseum_item_obs_table.c | 165 ++ ocean/osrs_colosseum/colosseum_profile.h | 33 +- ocean/osrs_colosseum/osrs_colosseum.cu | 96 +- ocean/osrs_colosseum/osrs_colosseum.h | 318 ++- ocean/osrs_inferno/osrs_inferno.cu | 210 ++ ocean/osrs_inferno/osrs_inferno.h | 37 +- src/ocean.cu | 5 + 72 files changed, 10092 insertions(+), 4925 deletions(-) create mode 100644 config/osrs_colosseum.ini create mode 100644 config/osrs_inferno.ini create mode 100644 ocean/osrs/osrs_colosseum_item_obs_generated.h create mode 100644 ocean/osrs/osrs_colosseum_item_obs_table.inc create mode 100644 ocean/osrs/osrs_inventory_actions.h create mode 100755 ocean/osrs/scripts/check-builds.sh create mode 100644 ocean/osrs/tests/bench_colo_step.c create mode 100644 ocean/osrs/tests/probe_colo_sim_invariant.c create mode 100644 ocean/osrs/tests/probe_env_thread_determinism.c create mode 100644 ocean/osrs/tests/test_colo_hit_delay.c create mode 100644 ocean/osrs/tests/test_osrs_eat_attack_delay.c create mode 100644 ocean/osrs/tests/test_osrs_inventory_actions.c create mode 100644 ocean/osrs/tests/test_osrs_player_step.c create mode 100644 ocean/osrs/tests/test_zulrah_golden.c create mode 100644 ocean/osrs/tests/test_zulrah_hit_delay.c create mode 100644 ocean/osrs/tools/gen_colosseum_item_obs_table.c create mode 100644 ocean/osrs_inferno/osrs_inferno.cu diff --git a/build.sh b/build.sh index 2208b11789..0683199616 100755 --- a/build.sh +++ b/build.sh @@ -67,6 +67,22 @@ else STANDALONE_LDFLAGS=(-framework Cocoa -framework IOKit -framework CoreVideo -framework OpenGL) fi +# OpenMP for the standalone (--local/--fast/--cpu) clang paths. Apple clang rejects +# -fopenmp; it needs -Xclang -fopenmp plus Homebrew libomp on the include/lib path. +# Linux clang/gcc take -fopenmp directly, so those defaults are preserved there. +OMP_CFLAGS=(-fopenmp) +OMP_LDFLAGS=(-fopenmp) +if [ "$PLATFORM" != "Linux" ]; then + OMP_PREFIX="$(brew --prefix libomp 2>/dev/null || true)" + if [ -n "$OMP_PREFIX" ]; then + OMP_CFLAGS=(-Xclang -fopenmp -I"$OMP_PREFIX/include") + OMP_LDFLAGS=(-L"$OMP_PREFIX/lib" -lomp) + else + OMP_CFLAGS=(-Xclang -fopenmp) + OMP_LDFLAGS=(-lomp) + fi +fi + CLANG_WARN=( -Wall -Wno-narrowing @@ -168,8 +184,12 @@ SRC_FILE=${SRC_FILE:-$SRC_DIR/$ENV.c} # Standalone environment build # -mavx2 enables AVX2 intrinsics (__m256, _mm256_*) which drive.h and -# src/pufferenv.h use directly. x86_64 only — strip if porting to ARM/Apple Silicon. -SIMD_FLAGS=(-mavx2 -mfma) +# src/pufferenv.h use directly. x86_64 only — omitted on ARM/Apple Silicon. +if [ "$(uname -m)" = "x86_64" ]; then + SIMD_FLAGS=(-mavx2 -mfma) +else + SIMD_FLAGS=() +fi if [ -n "$DEBUG" ] || [ "$MODE" = "local" ]; then CLANG_OPT=(-g -O0 "${CLANG_WARN[@]}" "${SANITIZE_FLAGS[@]}" "${SIMD_FLAGS[@]}") NVCC_OPT="-O0 -g" @@ -187,7 +207,7 @@ if [ "$MODE" = "local" ] || [ "$MODE" = "fast" ]; then "${LINK_ARCHIVES[@]}" "${EXTRA_LDFLAGS[@]}" "${STANDALONE_LDFLAGS[@]}" - -lm -lpthread -fopenmp + -lm -lpthread "${OMP_CFLAGS[@]}" "${OMP_LDFLAGS[@]}" -DPLATFORM_DESKTOP "${EXTRA_CFLAGS[@]}" ) @@ -232,7 +252,7 @@ elif [ "$MODE" = "cpu" ]; then "${LINK_ARCHIVES[@]}" \ "${EXTRA_LDFLAGS[@]}" \ "${STANDALONE_LDFLAGS[@]}" \ - -lm -lpthread -fopenmp \ + -lm -lpthread "${OMP_CFLAGS[@]}" "${OMP_LDFLAGS[@]}" \ -o build_cpu echo "Built: ./build_cpu" exit 0 diff --git a/config/osrs_colosseum.ini b/config/osrs_colosseum.ini new file mode 100644 index 0000000000..71dea264c8 --- /dev/null +++ b/config/osrs_colosseum.ini @@ -0,0 +1,246 @@ +# OSRS Fortis Colosseum encounter. + +[base] +env_name = osrs_colosseum +async = 0 + +[env] +start_wave = 1 +damage_reward_coeff = 0.00404769694 +wave_clear_bonus = 0.10361582 +win_bonus = 0.556438088 +death_penalty_coeff = 0.751913667 +timeout_penalty = 0.195512325 +farm_safe_damage_cap = 1 +farm_cap_waves = 4 +boss_damage_reward_coeff = 0.0 +boss_phase_bonus = 0.0865581334 +loadout_profile_mode = 2 +beginner_loadout_fraction = 0.5 +mask_inventory_heads = 0 +late_start_state_mode = 2 +late_start_supply_fraction_per_wave = 0.07 +bis_gear_oracle_mode = 0 +episode_max_ticks_override = 0 +prayer_switch_fail_prob = 0.0 +player_damage_received_scale = 1.0 +damage_scale_anneal_start = 1.0 +damage_scale_anneal_ticks = 0 +classic_curriculum_mode = 1 +curriculum_num_tiers = 4 +curriculum_wave_1 = 11 +curriculum_frac_1 = 0.0574102551 +curriculum_wave_2 = 5 +curriculum_frac_2 = 0.0 +curriculum_wave_3 = 12 +curriculum_frac_3 = 0.0668167621 +curriculum_wave_4 = 6 +curriculum_frac_4 = 0.332286835 +curriculum_wave_5 = 6 +curriculum_frac_5 = 0.0 + +[vec] +total_agents = 8192 +num_buffers = 8 +num_threads = 32 +action_mask_size = 452 +gpu_env = 0 + +[policy] +hidden_size = 256 +num_layers = 2 +expansion_factor = 1 + +[train] +total_timesteps = 360785504 +horizon = 32 +min_lr_ratio = 0.0598145202 +learning_rate = 0.000741419091 +momentum = 0.984893203 +ent_coef = 2.93842027e-06 +gamma = 0.999876142 +gae_lambda = 0.979838431 +vtrace_rho_clip = 1.65516651 +vtrace_c_clip = 0.573781013 +prio_alpha = 0.571603894 +prio_beta0 = 0.00201839209 +clip_coef = 0.0961709544 +vf_coef = 8 +vf_clip_coef = 0.0481410585 +max_grad_norm = 6.59531498 +replay_ratio = 3.97566438 +minibatch_size = 8192 + +[sweep] +max_suggestion_cost = 3600 +max_runs = 400 +gpus = 1 +early_stop_quantile = 0.25 + +[sweep.train.total_timesteps] +distribution = log_normal +min = 10_000_000 +max = 2_000_000_000 +scale = time + +[sweep.env.damage_reward_coeff] +distribution = log_normal +min = 0.00001 +max = 0.010 +scale = auto + +[sweep.env.wave_clear_bonus] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.win_bonus] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.boss_phase_bonus] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.death_penalty_coeff] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.timeout_penalty] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +# Ceiling was 8.0 and the top 20 of the 20260803 sweep ran 7.3 to 8.0, median exactly 8.0. +# A dim pinned on its own boundary is not an answer, it is a range that ran out. +[sweep.train.vf_coef] +distribution = uniform +min = 0.1 +max = 24.0 +scale = auto + +[sweep.train.vf_clip_coef] +distribution = uniform +min = 0.000002 +max = 1.0 +scale = auto + +# 1024 and 2048 put nothing in the top 20 across 210 trials, so the head of the range was +# only ever buying wall clock. The floor drops instead: obs 1258 -> 922 bought the most where +# the env dominates the step, which is the small-hidden end, so the knee may have moved down. +[sweep.policy.hidden_size] +distribution = uniform_pow2 +min = 128 +max = 1024 +scale = auto + +[sweep.train.horizon] +distribution = uniform_pow2 +min = 8 +max = 128 +scale = auto + +[sweep.train.learning_rate] +distribution = log_normal +min = 0.00003 +max = 0.004 +scale = 0.5 + +[sweep.train.ent_coef] +distribution = log_normal +min = 0.00000001 +max = 0.002 +scale = auto + +[sweep.train.gamma] +distribution = logit_normal +min = 0.995 +max = 0.99997 +scale = auto + +[sweep.train.gae_lambda] +distribution = logit_normal +min = 0.05 +max = 0.999 +scale = auto + +[sweep.train.min_lr_ratio] +distribution = uniform +min = 0.0 +max = 0.9 +scale = auto + +[sweep.train.clip_coef] +distribution = uniform +min = 0.001 +max = 0.12 +scale = auto + +[sweep.train.max_grad_norm] +distribution = uniform +min = 0.3 +max = 8.0 +scale = auto + +[sweep.train.replay_ratio] +distribution = uniform +min = 1.5 +max = 6.0 +scale = auto + +[sweep.env.curriculum_frac_1] +distribution = uniform +min = 0.0 +max = 0.30 +scale = auto + +[sweep.env.curriculum_frac_2] +distribution = uniform +min = 0.0 +max = 0.30 +scale = auto + +[sweep.env.curriculum_frac_3] +distribution = uniform +min = 0.0 +max = 0.40 +scale = auto + +[sweep.env.curriculum_frac_4] +distribution = uniform +min = 0.0 +max = 0.40 +scale = auto + +[sweep.env.curriculum_wave_1] +distribution = int_uniform +min = 1 +max = 12 +scale = auto + +[sweep.env.curriculum_wave_2] +distribution = int_uniform +min = 1 +max = 12 +scale = auto + +[sweep.env.curriculum_wave_3] +distribution = int_uniform +min = 1 +max = 12 +scale = auto + +[sweep.env.curriculum_wave_4] +distribution = int_uniform +min = 1 +max = 12 +scale = auto + diff --git a/config/osrs_inferno.ini b/config/osrs_inferno.ini new file mode 100644 index 0000000000..c63bf6114e --- /dev/null +++ b/config/osrs_inferno.ini @@ -0,0 +1,337 @@ +# OSRS Inferno encounter. + +[base] +env_name = osrs_inferno +async = 0 + +[env] +start_wave = 1 +damage_reward_coeff = 0.00369666866 +offensive_prayer_reward_coeff = 0 +shield_penalty_coeff = 0.000809495454 +tag_reward_coeff = 0.669779062 +shield_tag_reward_coeff = 0.0147339059 +late_start_supply_profile_scale = 1.0 +curriculum_supply_jitter_mode = 1 +curriculum_supply_shared_jitter = 0 +curriculum_supply_brew_jitter = 0.0847514719 +curriculum_supply_restore_jitter = 0.0291104093 +curriculum_no_brew_mode = 0 +curriculum_no_brew_frac = 0.0794238672 +loadout_profile_mode = 0 +budget_loadout_fraction = 0.0 +classic_curriculum_mode = 1 +reward_profile = 2 +supply_milestone_brew_reward_coeff = 0.0 +supply_milestone_restore_reward_coeff = 0.0 +death_penalty_coeff = 0.0 +terminal_penalty_enabled = 0 +step_out_forecast_obs_mode = 2 +phase_900_bonus = 0.0 +phase_600_bonus = 0.0 +phase_300_bonus = 0.0 +shield_penalty_episode_cap = 0.0 +curriculum_wave_1 = 67 +curriculum_frac_1 = 0.106778406 +curriculum_wave_2 = 69 +curriculum_frac_2 = 0.174882144 +curriculum_wave_3 = 70 +curriculum_frac_3 = 0.00210916414 +curriculum_wave_4 = 71 +curriculum_frac_4 = 0.15076603 +curriculum_wave_5 = 55 +curriculum_frac_5 = 0.241018683 +curriculum_wave_6 = 61 +curriculum_frac_6 = 0.0 +curriculum_frac_7 = 0.0 +curriculum_frac_8 = 0.0 + +jad_damage_reward_coeff = 0 +zuk_healer_damage_reward_coeff = 0.006 +set_damage_reward_coeff = 0.00418951968 +jad_kill_bonus = 0.713236809 +zuk_healer_kill_bonus = 0.194986016 +set_kill_bonus = 0.148986891 +post_healer_zuk_damage_coeff = 0.0 +post_healer_set_damage_reward_coeff = 0.0 +post_healer_set_kill_bonus = 0.0 +post_healer_set_alive_tick_penalty_coeff = 0.0 +post_healer_set_alive_penalty_cap = 0.0 +zuk_healer_phase_hp_delta_coeff = 0.0 +zuk_untagged_healer_tick_penalty_coeff = 0.0 +zuk_untagged_healer_target_bonus_coeff = 0.0 +zuk_safe_untagged_healer_target_bonus_coeff = 0.0 +zuk_untagged_healer_nonmagic_attack_bonus_coeff = 0.137061894 +zuk_healer_mage_attack_penalty_coeff = 0.144513637 +zuk_safe_untagged_healer_target_mask = 0 +zuk_force_safe_untagged_healer_target_mask = 0 +zuk_healer_reward_mode = 0 +joseph_reward_mode = 1 +post_jad_zuk_multiplier = 1.78125858 +jad_alive_zuk_multiplier = 0.1573513 + +[vec] +total_agents = 4096 +num_buffers = 2 +num_threads = 16 +action_mask_size = 457 +gpu_env = 0 + +[policy] +hidden_size = 1024 +num_layers = 2 +expansion_factor = 1 + +[train] +total_timesteps = 168232608 +horizon = 16 +min_lr_ratio = 0 +learning_rate = 0.00142262119 +momentum = 0.9641862728826048 +ent_coef = 0.0002 +gamma = 0.998270094 +gae_lambda = 0.25 +vtrace_rho_clip = 1.86103201 +vtrace_c_clip = 1.0 +prio_alpha = 0.0387323499 +prio_beta0 = 0.315968394 +clip_coef = 0.00100000005 +vf_coef = 1.97704399 +vf_clip_coef = 0.03 +max_grad_norm = 3.15836239 +replay_ratio = 4.71614552 +minibatch_size = 8192 + +[sweep] +max_suggestion_cost = 4800 +max_runs = 400 +gpus = 1 +early_stop_quantile = 0.25 + +[sweep.train.total_timesteps] +distribution = log_normal +min = 50_000_000 +max = 1_000_000_000 +scale = time + +[sweep.policy.hidden_size] +distribution = uniform_pow2 +min = 256 +max = 2048 +scale = auto + +[sweep.train.horizon] +distribution = uniform_pow2 +min = 16 +max = 64 +scale = auto + +[sweep.train.minibatch_size] +distribution = uniform_pow2 +min = 4096 +max = 16384 +scale = auto + +[sweep.train.learning_rate] +distribution = log_normal +min = 0.0005 +max = 0.004 +scale = 0.5 + +[sweep.train.ent_coef] +distribution = log_normal +min = 0.0000000001 +max = 0.002 +scale = auto + +[sweep.train.gamma] +distribution = logit_normal +min = 0.9975 +max = 0.99997 +scale = auto + +[sweep.train.gae_lambda] +distribution = logit_normal +min = 0.25 +max = 0.999 +scale = auto + +[sweep.train.min_lr_ratio] +distribution = uniform +min = 0.0 +max = 0.1 +scale = auto + +[sweep.train.vtrace_rho_clip] +distribution = uniform +min = 1.0 +max = 2.0 +scale = auto + +[sweep.train.vtrace_c_clip] +distribution = uniform +min = 0.3 +max = 1.0 +scale = auto + +[sweep.train.prio_alpha] +distribution = logit_normal +min = 0.0 +max = 0.25 +scale = auto + +[sweep.train.prio_beta0] +distribution = logit_normal +min = 0.05 +max = 0.4 +scale = auto + +[sweep.train.clip_coef] +distribution = uniform +min = 0.001 +max = 0.12 +scale = auto + +[sweep.train.vf_coef] +distribution = log_normal +min = 0.02 +max = 4.0 +scale = auto + +[sweep.train.vf_clip_coef] +distribution = uniform +min = 0.03 +max = 0.25 +scale = auto + +[sweep.train.max_grad_norm] +distribution = uniform +min = 0.3 +max = 8.0 +scale = auto + +[sweep.train.replay_ratio] +distribution = uniform +min = 3.0 +max = 6.0 +scale = auto + +[sweep.env.curriculum_frac_1] +distribution = uniform +min = 0.0 +max = 0.18 +scale = auto + +[sweep.env.curriculum_frac_2] +distribution = uniform +min = 0.0 +max = 0.24 +scale = auto + +[sweep.env.curriculum_frac_3] +distribution = uniform +min = 0.0 +max = 0.05 +scale = auto + +[sweep.env.curriculum_frac_4] +distribution = uniform +min = 0.0 +max = 0.18 +scale = auto + +[sweep.env.curriculum_wave_5] +distribution = int_uniform +min = 52 +max = 59 +scale = auto + +[sweep.env.curriculum_frac_5] +distribution = uniform +min = 0.03 +max = 0.25 +scale = auto + +[sweep.env.curriculum_supply_jitter_mode] +distribution = int_uniform +min = 0 +max = 3 +scale = auto + +[sweep.env.curriculum_supply_shared_jitter] +distribution = uniform +min = 0.0 +max = 0.30 +scale = auto + +[sweep.env.curriculum_supply_brew_jitter] +distribution = uniform +min = 0.0 +max = 0.25 +scale = auto + +[sweep.env.curriculum_supply_restore_jitter] +distribution = uniform +min = 0.0 +max = 0.25 +scale = auto + +[sweep.env.curriculum_no_brew_mode] +distribution = int_uniform +min = 0 +max = 3 +scale = auto + +[sweep.env.curriculum_no_brew_frac] +distribution = uniform +min = 0.0 +max = 0.35 +scale = auto + +[sweep.env.damage_reward_coeff] +distribution = log_normal +min = 0.0015 +max = 0.006 +scale = auto + +[sweep.env.offensive_prayer_reward_coeff] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.shield_penalty_coeff] +distribution = uniform +min = 0.0 +max = 0.003 +scale = auto + +[sweep.env.tag_reward_coeff] +distribution = log_normal +min = 0.05 +max = 1.25 +scale = auto + +[sweep.env.zuk_untagged_healer_nonmagic_attack_bonus_coeff] +distribution = uniform +min = 0.0 +max = 0.70 +scale = auto + +[sweep.env.zuk_healer_mage_attack_penalty_coeff] +distribution = uniform +min = 0.0 +max = 0.30 +scale = auto + +[sweep.env.death_penalty_coeff] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.step_out_forecast_obs_mode] +distribution = int_uniform +min = 1 +max = 3 +scale = auto diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc index 2512da5983..58ab110814 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc @@ -249,6 +249,23 @@ static int col_sol_aoe_tile_is_hazard(const SolHereditState* sol, int x, int y) return 0; } +static ColoSolDamageSource col_sol_aoe_damage_source(ColoSolAoeAttack attack) { + switch (attack) { + case COLO_SOL_AOE_SPEAR1: + return COLO_SOL_DAMAGE_SPEAR_1; + case COLO_SOL_AOE_SPEAR2: + return COLO_SOL_DAMAGE_SPEAR_2; + case COLO_SOL_AOE_SHIELD1: + return COLO_SOL_DAMAGE_SHIELD_1; + case COLO_SOL_AOE_SHIELD2: + return COLO_SOL_DAMAGE_SHIELD_2; + case COLO_SOL_AOE_NONE: + break; + } + fprintf(stderr, "invalid Sol AOE damage source %d\n", (int)attack); + abort(); +} + static void col_sol_cast_aoe(ColosseumState* s, int idx, int kind, int variant) { SolHereditState* sol = &s->sol; const ColoNPC* boss = &s->npcs[idx]; @@ -276,9 +293,14 @@ static void col_sol_tick_aoe(ColosseumState* s) { col_sol_aoe_tile_is_hazard(sol, s->player.x, s->player.y)) { int dmg = COLO_SOL_SLAM_MIN_HIT + encounter_rand_int(&s->rng_state, COLO_SOL_SLAM_RAND); - col_damage_player_from(s, dmg, COLO_SOL_HEREDIT, COLO_DMG_UNPRAYABLE); - s->last_hit_by_type = COLO_SOL_HEREDIT; - col_mod_on_player_damaged(s, dmg); + ColoSolDamageSource source = + col_sol_aoe_damage_source(sol->aoe_attack); + int applied = col_damage_player_from_sol(s, dmg, source); + if (applied > 0) { + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged( + s, applied, col_damage_source_from_sol(source)); + } } if (sol->aoe_age >= COLO_SOL_AOE_LIFETIME) sol->aoe_attack = COLO_SOL_AOE_NONE; } @@ -313,10 +335,14 @@ static void col_sol_crystal_patrol( } } -static int col_sol_crystal_in_line(const ColosseumState* s, const ColoSolCrystal* c) { +static int col_sol_crystal_hits_tile(const ColoSolCrystal* c, int x, int y) { if (c->edge == COLO_SOL_EDGE_NORTH || c->edge == COLO_SOL_EDGE_SOUTH) - return s->player.x == c->x; - return s->player.y == c->y; + return x == c->x; + return y == c->y; +} + +static int col_sol_crystal_in_line(const ColosseumState* s, const ColoSolCrystal* c) { + return col_sol_crystal_hits_tile(c, s->player.x, s->player.y); } static void col_sol_spawn_crystal(ColosseumState* s) { @@ -374,9 +400,13 @@ static void col_sol_tick_crystals(ColosseumState* s) { col_sol_crystal_in_line(s, c)) { int dmg = COLO_SOL_LASER_MIN_HIT + encounter_rand_int(&s->rng_state, COLO_SOL_LASER_RAND); - col_damage_player_from(s, dmg, COLO_SOL_HEREDIT, COLO_DMG_UNPRAYABLE); - s->last_hit_by_type = COLO_SOL_HEREDIT; - col_mod_on_player_damaged(s, dmg); + int applied = col_damage_player_from_sol( + s, dmg, COLO_SOL_DAMAGE_CRYSTAL_LASER); + if (applied > 0) { + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_SOL_CRYSTAL_LASER); + } } c->move_timer--; @@ -384,11 +414,16 @@ static void col_sol_tick_crystals(ColosseumState* s) { } } +static int col_sol_molten_sand_at(const SolHereditState* sol, int x, int y) { + for (int i = 0; i < sol->hazard_tile_count; i++) + if (sol->hazard_tile_x[i] == x && sol->hazard_tile_y[i] == y) return 1; + return 0; +} + static void col_sol_add_pool(ColosseumState* s, int x, int y) { if (col_static_blocked(x, y)) return; SolHereditState* sol = &s->sol; - for (int i = 0; i < sol->hazard_tile_count; i++) - if (sol->hazard_tile_x[i] == x && sol->hazard_tile_y[i] == y) return; + if (col_sol_molten_sand_at(sol, x, y)) return; int slot = sol->hazard_tile_count < COLO_SOL_HAZARD_TILES_MAX ? sol->hazard_tile_count++ : encounter_rand_int(&s->rng_state, COLO_SOL_HAZARD_TILES_MAX); @@ -475,15 +510,16 @@ static void col_sol_tick_molten(ColosseumState* s) { sol->beams[b].active = 0; } - for (int i = 0; i < sol->hazard_tile_count; i++) { - if (s->player.x != sol->hazard_tile_x[i] || s->player.y != sol->hazard_tile_y[i]) - continue; + if (col_sol_molten_sand_at(sol, s->player.x, s->player.y)) { int dmg = COLO_MOLTEN_SAND_MIN_HIT + encounter_rand_int(&s->rng_state, COLO_MOLTEN_SAND_RAND); - col_damage_player_from(s, dmg, COLO_SOL_HEREDIT, COLO_DMG_UNPRAYABLE); - s->last_hit_by_type = COLO_SOL_HEREDIT; - col_mod_on_player_damaged(s, dmg); - break; + int applied = + col_damage_player_from_sol(s, dmg, COLO_SOL_DAMAGE_MOLTEN_SAND); + if (applied > 0) { + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_SOL_MOLTEN_SAND); + } } } @@ -532,9 +568,13 @@ static void col_sol_tick_triple_parry(ColosseumState* s) { s->tick_scratch.prayer_correct++; } else { s->tick_scratch.offpray_damage_this_tick += (float)sol->parry_damage[h]; - col_damage_player_from(s, sol->parry_damage[h], COLO_SOL_HEREDIT, COLO_DMG_OFFPRAY); - s->last_hit_by_type = COLO_SOL_HEREDIT; - col_mod_on_player_damaged(s, sol->parry_damage[h]); + int applied = col_damage_player_from_sol( + s, sol->parry_damage[h], COLO_SOL_DAMAGE_TRIPLE_PARRY); + if (applied > 0) { + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_SOL_TRIPLE_PARRY); + } } col_log_prayer_event(s, COLO_SOL_HEREDIT, blocked, blocked ? 0 : sol->parry_damage[h]); s->player.prayer = PRAYER_NONE; @@ -575,9 +615,13 @@ static void col_sol_tick_grapple(ColosseumState* s) { if (sol->grapple_timer == 0 && !sol->grapple_resolved) { int dmg = COLO_SOL_SLAM_MIN_HIT + encounter_rand_int(&s->rng_state, COLO_SOL_SLAM_RAND); - col_damage_player_from(s, dmg, COLO_SOL_HEREDIT, COLO_DMG_UNPRAYABLE); - s->last_hit_by_type = COLO_SOL_HEREDIT; - col_mod_on_player_damaged(s, dmg); + int applied = + col_damage_player_from_sol(s, dmg, COLO_SOL_DAMAGE_GRAPPLE); + if (applied > 0) { + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_SOL_GRAPPLE); + } sol->grapple_active = 0; sol->grapple_resolved = 1; sol->grapple_outcome = COLO_GRAPPLE_OUTCOME_FAILED; @@ -603,8 +647,7 @@ static void col_sol_enter_phase(ColosseumState* s, int new_phase) { if (new_phase > s->log.boss_phase_reached) s->log.boss_phase_reached = new_phase; } -static void col_sol_boss_tick(ColosseumState* s, ColosseumContext* ctx) { - (void)ctx; +static void col_sol_boss_tick(ColosseumState* s) { SolHereditState* sol = &s->sol; if (s->wave != COLO_WAVE_BOSS || !sol->started) return; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc index 1cfc44ecba..1695d3202b 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc @@ -151,25 +151,68 @@ static void col_log_prayer_event(ColosseumState* s, int npc_type, int prayed, in else s->log.offpray_damage_by_type[npc_type] += (float)dmg; } -static inline void col_damage_player_from( +static inline int col_damage_player_from( ColosseumState* s, int dmg, int npc_type, ColoDamageChannel channel ) { dmg = col_scale_incoming_damage(s, dmg); - float before = s->tick_scratch.damage_received; - encounter_damage_player(&s->player, dmg, &s->tick_scratch.damage_received); - float applied = s->tick_scratch.damage_received - before; + int hitpoints_before = s->player.current_hitpoints; + encounter_damage_player(&s->player, dmg, NULL); + int applied = hitpoints_before - s->player.current_hitpoints; + s->tick_scratch.damage_received += (float)applied; switch (channel) { case COLO_DMG_OFFPRAY: - s->tick_scratch.landed_offpray_damage += applied; + s->tick_scratch.landed_offpray_damage += (float)applied; break; case COLO_DMG_UNPRAYABLE: - s->tick_scratch.landed_unprayable_damage += applied; - if (applied > 0.0f && npc_type >= 0 && npc_type < COLO_NUM_NPC_TYPES) - s->log.typeless_damage_by_type[npc_type] += applied; + s->tick_scratch.landed_unprayable_damage += (float)applied; + if (applied > 0 && npc_type >= 0 && npc_type < COLO_NUM_NPC_TYPES) + s->log.typeless_damage_by_type[npc_type] += (float)applied; break; } - if (dmg > 0 && npc_type >= 0 && npc_type < COLO_NUM_NPC_TYPES) - s->log.total_damage_by_type[npc_type] += (float)dmg; + if (applied > 0 && npc_type >= 0 && npc_type < COLO_NUM_NPC_TYPES) + s->log.total_damage_by_type[npc_type] += (float)applied; + return applied; +} + +static inline int col_damage_player_from_sol( + ColosseumState* s, int dmg, ColoSolDamageSource source +) { + assert(source >= 0 && source < COLO_NUM_SOL_DAMAGE_SOURCES); + ColoDamageChannel channel = source == COLO_SOL_DAMAGE_TRIPLE_PARRY + ? COLO_DMG_OFFPRAY + : COLO_DMG_UNPRAYABLE; + int hitpoints_before = s->player.current_hitpoints; + int applied = + col_damage_player_from(s, dmg, COLO_SOL_HEREDIT, channel); + s->log.sol_damage_by_source[source] += + (float)(hitpoints_before - s->player.current_hitpoints); + return applied; +} + +static ColoDamageSource col_damage_source_from_sol( + ColoSolDamageSource source +) { + switch (source) { + case COLO_SOL_DAMAGE_SPEAR_1: + return COLO_DAMAGE_SOL_SPEAR_1; + case COLO_SOL_DAMAGE_SPEAR_2: + return COLO_DAMAGE_SOL_SPEAR_2; + case COLO_SOL_DAMAGE_SHIELD_1: + return COLO_DAMAGE_SOL_SHIELD_1; + case COLO_SOL_DAMAGE_SHIELD_2: + return COLO_DAMAGE_SOL_SHIELD_2; + case COLO_SOL_DAMAGE_TRIPLE_PARRY: + return COLO_DAMAGE_SOL_TRIPLE_PARRY; + case COLO_SOL_DAMAGE_GRAPPLE: + return COLO_DAMAGE_SOL_GRAPPLE; + case COLO_SOL_DAMAGE_CRYSTAL_LASER: + return COLO_DAMAGE_SOL_CRYSTAL_LASER; + case COLO_SOL_DAMAGE_MOLTEN_SAND: + return COLO_DAMAGE_SOL_MOLTEN_SAND; + case COLO_NUM_SOL_DAMAGE_SOURCES: + break; + } + abort(); } typedef struct { @@ -178,23 +221,39 @@ typedef struct { } ColPendingHitObserverContext; static void col_pending_hit_prayer_observer( - void* user, const EncounterPendingHit* hit, int dmg, int prayed, int checked) { + void* user, const EncounterPendingHit* hit, int dmg, int applied, + int prayed, int checked +) { ColPendingHitObserverContext* observer_ctx = (ColPendingHitObserverContext*)user; ColosseumState* s = observer_ctx->state; ColosseumContext* ctx = observer_ctx->context; col_render_hit_events_add( &ctx->player_render_hit_count, ctx->player_render_hit_damage, dmg); - if (dmg > 0 && hit->source_npc_type >= 0 && hit->source_npc_type < COLO_NUM_NPC_TYPES) { - s->log.total_damage_by_type[hit->source_npc_type] += (float)dmg; + if (applied > 0 && + hit->source_npc_type >= 0 && + hit->source_npc_type < COLO_NUM_NPC_TYPES) { + s->log.total_damage_by_type[hit->source_npc_type] += (float)applied; s->last_hit_by_type = hit->source_npc_type; + if (hit->source_npc_type == COLO_JAVELIN_COLOSSUS) { + s->log.javelin_damage_by_source[COLO_JAVELIN_DAMAGE_BASIC_RANGED] += + (float)applied; + } + ColoDamageSource source = + hit->source_npc_type == COLO_JAVELIN_COLOSSUS + ? COLO_DAMAGE_JAVELIN_BASIC_RANGED + : COLO_DAMAGE_NPC_ATTACK; + s->last_damage_source = source; + col_mod_add_doom_stack(s, source); } col_apply_echo_boots_recoil(s, ctx, hit->source_npc_slot, dmg); - if (dmg > 0) { - if (checked && prayed) s->tick_scratch.landed_prayed_damage += (float)dmg; - else s->tick_scratch.landed_offpray_damage += (float)dmg; + if (applied > 0) { + if (checked && prayed) + s->tick_scratch.landed_prayed_damage += (float)applied; + else + s->tick_scratch.landed_offpray_damage += (float)applied; } if (!checked && hit->source_npc_type == COLO_MANTICORE) { if (hit->hit_success) col_mod_manticore_apply_venom(s, hit->hit_success); @@ -208,26 +267,25 @@ static void col_pending_hit_prayer_observer( static void col_resolve_player_pending_hits_ctx(ColosseumState* s, ColosseumContext* ctx) { int prayer_correct = 0; - int off_prayer = 0; - float dmg_received = 0.0f; + float damage_received = 0.0f; ColPendingHitObserverContext observer_ctx = { .state = s, .context = ctx, }; encounter_resolve_player_pending_hits_observed( &s->player_pending_hits, &s->player, s->player.prayer, - &dmg_received, &prayer_correct, &off_prayer, + &damage_received, &prayer_correct, NULL, col_pending_hit_prayer_observer, &observer_ctx); - s->tick_scratch.damage_received += dmg_received; + int applied = (int)damage_received; + s->tick_scratch.damage_received += damage_received; s->tick_scratch.prayer_correct += prayer_correct; int blas = col_mod_tier(s, COLO_MOD_BLASPHEMY); - if (blas > 0 && dmg_received > 0.0f && s->player.current_prayer > 0) { - int drain = ((int)dmg_received * COLO_BLASPHEMY_DRAIN_PCT[blas]) / 100; + if (blas > 0 && applied > 0 && s->player.current_prayer > 0) { + int drain = (applied * COLO_BLASPHEMY_DRAIN_PCT[blas]) / 100; s->player.current_prayer -= drain; if (s->player.current_prayer < 0) s->player.current_prayer = 0; } - if (col_mod_active(s, COLO_MOD_DOOM)) s->doom_stacks += off_prayer; } static void col_resolve_player_pending_hits(ColosseumState* s) { @@ -325,14 +383,14 @@ static void col_apply_instant_melee_hit( if (!prayed) s->tick_scratch.offpray_damage_this_tick += (float)dmg; col_log_prayer_event(s, npc_type, prayed, dmg); - col_apply_echo_boots_recoil(s, ctx, attacker_slot, dmg); - col_damage_player_from(s, dmg, npc_type, COLO_DMG_OFFPRAY); - - int applied = col_scale_incoming_damage(s, dmg); + int applied = + col_damage_player_from(s, dmg, npc_type, COLO_DMG_OFFPRAY); if (applied > 0) { - col_mod_on_player_damaged(s, applied); + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_NPC_ATTACK); s->last_hit_by_type = npc_type; } + col_apply_echo_boots_recoil(s, ctx, attacker_slot, dmg); } static void col_npc_melee_attack( @@ -417,10 +475,15 @@ static void col_npc_resolve_javelin_skyfall( jv->skyfall_pending = 0; int on_tile = (s->player.x == jv->skyfall_tile_x && s->player.y == jv->skyfall_tile_y); int dmg = on_tile ? jv->skyfall_damage : 0; - col_damage_player_from(s, dmg, npc->type, COLO_DMG_UNPRAYABLE); - if (dmg > 0) { + int hitpoints_before = s->player.current_hitpoints; + int applied = + col_damage_player_from(s, dmg, npc->type, COLO_DMG_UNPRAYABLE); + s->log.javelin_damage_by_source[COLO_JAVELIN_DAMAGE_SKYFALL] += + (float)(hitpoints_before - s->player.current_hitpoints); + if (applied > 0) { s->last_hit_by_type = npc->type; - col_mod_on_player_damaged(s, dmg); + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_JAVELIN_SKYFALL); } col_mod_reentry_on_skyfall(s, jv->skyfall_tile_x, jv->skyfall_tile_y); } @@ -448,7 +511,8 @@ static int col_manticore_orb_max_hit(AttackStyle style) { static void col_npc_manticore_fire_orb(ColosseumState* s, int idx, const ColoNpcStats* stats) { #ifdef COLO_PROFILE_ENABLED - int col_prof_enabled = COLO_PROFILE_ENABLED(); + int col_prof_enabled = + COLO_PROFILE_ENABLED() && !s->profiling_suppressed; double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; #endif ColoNPC* npc = &s->npcs[idx]; @@ -592,7 +656,8 @@ static int col_warband_melee_adjacent(const ColosseumState* s, const ColoNPC* np static void col_warband_attack_phase(ColosseumState* s, ColosseumContext* ctx) { #ifdef COLO_PROFILE_ENABLED - int col_prof_enabled = COLO_PROFILE_ENABLED(); + int col_prof_enabled = + COLO_PROFILE_ENABLED() && !s->profiling_suppressed; double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; #endif if (s->warband_cycle_anchor < 0) { @@ -633,6 +698,7 @@ static void col_live_threat_style_counts( for (int n = 0; n < COLO_MAX_NPCS; n++) { const ColoNPC* npc = &s->npcs[n]; if (!col_npc_is_live_enemy(npc)) continue; + if (npc->type == COLO_SOL_HEREDIT) continue; const ColoNpcStats* ns = &COLO_NPC_STATS[npc->type]; if (ns->attack_range < 1) continue; int dist = col_npc_dist_to_player(s, npc); @@ -650,65 +716,14 @@ static void col_live_threat_style_counts( } } -static AttackStyle col_oracle_incoming_style(const ColosseumState* s, const ColoNPC* npc) { - if (!col_npc_is_live_enemy(npc)) return ATTACK_STYLE_NONE; - if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return ATTACK_STYLE_NONE; - const ColoNpcStats* st = &COLO_NPC_STATS[npc->type]; - - if (col_type_is_warbander(npc->type)) { - if (s->warband_cycle_anchor < 0) return ATTACK_STYLE_NONE; - int phase = (s->tick - s->warband_cycle_anchor + COLO_WARBAND_CYCLE_TICKS) - % COLO_WARBAND_CYCLE_TICKS; - if (phase != col_warband_window_offset(npc->type)) return ATTACK_STYLE_NONE; - if (!col_warband_melee_adjacent(s, npc)) return ATTACK_STYLE_NONE; - return st->default_style; - } - - if (npc->type == COLO_MANTICORE) { - const ColoManticoreState* mc = &npc->type_state.manticore; - if (mc->cycle_step > 0) return mc->orb_style[mc->cycle_step]; - if (npc->attack_timer > 1) return ATTACK_STYLE_NONE; - int d = col_npc_dist_to_player(s, npc); - if (d < 1 || d > st->attack_range) return ATTACK_STYLE_NONE; - if (!col_npc_has_los_to_player(s, npc)) return ATTACK_STYLE_NONE; - return mc->fixed_orb_style[0]; - } - - if (npc->type == COLO_MINOTAUR) { - if (npc->attack_timer > 1) return ATTACK_STYLE_NONE; - return (col_npc_dist_to_player(s, npc) == 1) ? ATTACK_STYLE_MELEE : ATTACK_STYLE_NONE; - } - - if (npc->attack_timer > 1) return ATTACK_STYLE_NONE; - if (st->attack_range > 1) { - int d = col_npc_dist_to_player(s, npc); - if (d < 1 || d > st->attack_range) return ATTACK_STYLE_NONE; - if (!col_npc_has_los_to_player(s, npc)) return ATTACK_STYLE_NONE; - return st->default_style; - } - return col_npc_melee_adjacent_to_player(s, npc) ? ATTACK_STYLE_MELEE : ATTACK_STYLE_NONE; -} - -static void col_apply_prayer_oracle(ColosseumState* s) { - int magic = 0, melee = 0, ranged = 0; - for (int i = 0; i < COLO_MAX_NPCS; i++) { - AttackStyle style = col_oracle_incoming_style(s, &s->npcs[i]); - if (style == ATTACK_STYLE_MAGIC) magic = 1; - else if (style == ATTACK_STYLE_MELEE) melee = 1; - else if (style == ATTACK_STYLE_RANGED) ranged = 1; - } - if (magic) s->player.prayer = PRAYER_PROTECT_MAGIC; - else if (melee) s->player.prayer = PRAYER_PROTECT_MELEE; - else if (ranged) s->player.prayer = PRAYER_PROTECT_RANGED; -} - static void col_tick_npcs_ctx(ColosseumState* s, ColosseumContext* ctx) { #ifdef COLO_PROFILE_ENABLED - int col_prof_enabled = COLO_PROFILE_ENABLED(); + int col_prof_enabled = + COLO_PROFILE_ENABLED() && !s->profiling_suppressed; double col_prof_total_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; double col_prof_t0 = col_prof_total_t0; #endif - col_sol_boss_tick(s, ctx); + col_sol_boss_tick(s); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_STEP_SOL_BOSS); #endif diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc index bd89f9d163..9920d4e8d5 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc @@ -4,24 +4,6 @@ #define COLO_STYLE_MASK_RANGED 2 #define COLO_STYLE_MASK_MAGIC 4 -typedef struct { - const ColosseumState* state; - uint8_t (*npc_flags)[COLO_ARENA_HEIGHT]; - int player_x; - int player_y; - int sol_clamp_active; -} ColoForecastMoveCtx; - -typedef struct { - int held; - int dist; - int los; - int npc_x; - int npc_y; - int land_x; - int land_y; -} ColoForecastMoveResult; - typedef struct { int valid; int land_x; @@ -29,110 +11,69 @@ typedef struct { } ColoForecastLanding; typedef struct { - int first_attack_tick; - int first_style_mask; - int max_hit; - int ranged_magic_same_tick; -} ColoForecastObsSummary; - -static int col_forecast_style_mask(AttackStyle style) { - if (style == ATTACK_STYLE_MELEE) return COLO_STYLE_MASK_MELEE; - if (style == ATTACK_STYLE_RANGED) return COLO_STYLE_MASK_RANGED; - if (style == ATTACK_STYLE_MAGIC) return COLO_STYLE_MASK_MAGIC; - return 0; -} - -static int col_forecast_style_max_hit(ColoNpcType type, AttackStyle style) { - if (type == COLO_MANTICORE) return col_manticore_orb_max_hit(style); - return COLO_NPC_STATS[type].max_hit; -} - -static void col_step_out_forecast_record_style_mask( - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary, - int tick_idx, - ColoNpcType type, - int style_mask, - int max_hit -) { - if (tick_idx < 0 || tick_idx >= COLO_STEP_OUT_FORECAST_HORIZON) return; - ColoStepOutForecastTick* tick = &action->ticks[tick_idx]; - int styles = 0; - if (style_mask & COLO_STYLE_MASK_MELEE) { - tick->melee_count++; - if (max_hit > tick->max_hit) tick->max_hit = max_hit; - styles++; - } - if (style_mask & COLO_STYLE_MASK_RANGED) { - tick->ranged_count++; - if (max_hit > tick->max_hit) tick->max_hit = max_hit; - styles++; - } - if (style_mask & COLO_STYLE_MASK_MAGIC) { - tick->magic_count++; - if (max_hit > tick->max_hit) tick->max_hit = max_hit; - styles++; - } - if (styles >= 2 || ((tick->melee_count > 0) + - (tick->ranged_count > 0) + (tick->magic_count > 0)) >= 2) { - action->same_tick_mixed_style_conflict = 1; - } - if ((style_mask & COLO_STYLE_MASK_MELEE) && type == COLO_MANTICORE) - action->melee_fallback_exposure = 1; - if (!summary) return; - int tick_style_mask = 0; - if (tick->melee_count > 0) tick_style_mask |= COLO_STYLE_MASK_MELEE; - if (tick->ranged_count > 0) tick_style_mask |= COLO_STYLE_MASK_RANGED; - if (tick->magic_count > 0) tick_style_mask |= COLO_STYLE_MASK_MAGIC; - if (summary->first_attack_tick == 0) { - summary->first_attack_tick = tick_idx + 1; - summary->first_style_mask = tick_style_mask; - } else if (summary->first_attack_tick == tick_idx + 1) { - summary->first_style_mask = tick_style_mask; - } - if (tick->max_hit > summary->max_hit) summary->max_hit = tick->max_hit; - if (tick->ranged_count > 0 && tick->magic_count > 0) - summary->ranged_magic_same_tick = 1; -} - -static void col_step_out_forecast_record_style( - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary, - int tick_idx, - ColoNpcType type, - AttackStyle style -) { - int style_mask = col_forecast_style_mask(style); - if (style_mask == 0) return; - col_step_out_forecast_record_style_mask( - action, summary, tick_idx, type, style_mask, - col_forecast_style_max_hit(type, style)); -} - -static int col_step_out_forecast_tick_style_mask(const ColoStepOutForecastTick* tick) { - int mask = 0; - if (tick->melee_count > 0) mask |= COLO_STYLE_MASK_MELEE; - if (tick->ranged_count > 0) mask |= COLO_STYLE_MASK_RANGED; - if (tick->magic_count > 0) mask |= COLO_STYLE_MASK_MAGIC; - return mask; -} - -static int col_step_out_forecast_tick_has_event(const ColoStepOutForecastTick* tick) { - return tick->melee_count > 0 || - tick->ranged_count > 0 || - tick->magic_count > 0; + int found; + int npc_slot; + int x; + int y; + int size; + int ticks; + int distance; +} ColoVolatilityBlastPreview; + +static inline ColoVolatilityBlastPreview col_volatility_blast_preview_none(void) { + return (ColoVolatilityBlastPreview){ + .found = 0, + .npc_slot = -1, + .x = 0, + .y = 0, + .size = 0, + .ticks = 0, + .distance = 1 << 30, + }; } -static void col_step_out_forecast_finalize_action( - ColoStepOutForecastAction* action, int horizon) { - for (int t = 0; t < horizon - 1; t++) { - int ranged_then_magic = action->ticks[t].ranged_count > 0 && - action->ticks[t + 1].magic_count > 0; - int magic_then_ranged = action->ticks[t].magic_count > 0 && - action->ticks[t + 1].ranged_count > 0; - if (ranged_then_magic || magic_then_ranged) - action->ranged_magic_offtick_opportunity = 1; - } +static ColoVolatilityBlastPreview col_soonest_queued_volatility_blast( + const ColosseumState* s +) { + ColoVolatilityBlastPreview best = col_volatility_blast_preview_none(); + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (!col_npc_is_live_enemy(npc)) continue; + for (int h = 0; h < npc->pending_hits.count; h++) { + const EncounterPendingHit* hit = &npc->pending_hits.hits[h]; + if (!hit->active) continue; + int ticks = hit->ticks_remaining < 0 ? 0 : hit->ticks_remaining; + int damage = 0; + for (int j = 0; j < npc->pending_hits.count; j++) { + const EncounterPendingHit* other = &npc->pending_hits.hits[j]; + if (!other->active) continue; + int other_ticks = + other->ticks_remaining < 0 ? 0 : other->ticks_remaining; + if (other_ticks <= ticks) damage += other->damage; + } + if (damage < npc->hp) continue; + int size = col_npc_effective_size(npc); + int center_x = npc->x + size / 2; + int center_y = npc->y + size / 2; + int dx = abs(center_x - s->player.x); + int dy = abs(center_y - s->player.y); + int distance = dx > dy ? dx : dy; + if (best.found && + (ticks > best.ticks || + (ticks == best.ticks && distance >= best.distance))) + continue; + best = (ColoVolatilityBlastPreview){ + .found = 1, + .npc_slot = n, + .x = npc->x, + .y = npc->y, + .size = size, + .ticks = ticks, + .distance = distance, + }; + } + } + return best; } static int col_step_out_forecast_action_valid(const ColosseumState* s, int action_idx) { @@ -194,880 +135,24 @@ static ColoForecastLanding col_step_out_forecast_action_landing_ctx( return landing; } -static ColoForecastLanding col_step_out_forecast_set_action_landing_ctx( - const ColosseumState* s, - int action_idx, - ColoStepOutForecastAction* action -) { - ColoForecastLanding landing = - col_step_out_forecast_action_landing_ctx(s, action_idx); - action->valid = landing.valid; - action->land_x = landing.land_x; - action->land_y = landing.land_y; - return landing; -} - -static int col_step_out_forecast_duplicate_landing( - const ColoStepOutForecast* forecast, - int action_idx -) { - const ColoStepOutForecastAction* action = &forecast->actions[action_idx]; - for (int prior_idx = 0; prior_idx < action_idx; prior_idx++) { - const ColoStepOutForecastAction* prior = &forecast->actions[prior_idx]; - if (prior->valid && - prior->land_x == action->land_x && - prior->land_y == action->land_y) { - return prior_idx; - } - } - return -1; -} - -static int col_step_out_forecast_prepare_unique_action_ctx( - const ColosseumState* s, - ColoStepOutForecast* out, - ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS], - int action_idx, - int forecast_slot_count -) { - ColoStepOutForecastAction* action = &out->actions[action_idx]; - ColoForecastLanding landing = col_step_out_forecast_set_action_landing_ctx( - s, action_idx, action); - if (!action->valid || forecast_slot_count == 0) return 0; - - int duplicate_idx = col_step_out_forecast_duplicate_landing(out, action_idx); - if (duplicate_idx >= 0) { - int valid = action->valid; - *action = out->actions[duplicate_idx]; - action->valid = valid; - action->land_x = landing.land_x; - action->land_y = landing.land_y; - if (summaries) - summaries[action_idx] = summaries[duplicate_idx]; - return 0; - } - return 1; -} - -static int col_collect_step_out_forecast_slots( - const ColosseumState* s, - int* out -) { - int n = 0; - for (int i = 0; i < COLO_MAX_NPCS; i++) { - const ColoNPC* npc = &s->npcs[i]; - if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) continue; - if (col_type_is_hazard_entity(npc->type)) continue; - out[n++] = i; - } - return n; -} - -static void col_forecast_local_copy_npc_slots( - const ColosseumState* s, - ColoForecastNpcLocal out[COLO_MAX_NPCS], - const int slots[COLO_MAX_NPCS], - int slot_count -) { - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - const ColoNPC* src = &s->npcs[i]; - const ColoNpcStats* stats = &COLO_NPC_STATS[src->type]; - ColoForecastNpcLocal* dst = &out[i]; - *dst = (ColoForecastNpcLocal){0}; - dst->active = src->active; - dst->type = src->type; - dst->forecast_behavior = COLO_FORECAST_BEHAVIOR_STANDARD; - if (col_type_is_warbander(src->type)) - dst->forecast_behavior = COLO_FORECAST_BEHAVIOR_WARBAND; - else if (src->type == COLO_MANTICORE) - dst->forecast_behavior = COLO_FORECAST_BEHAVIOR_MANTICORE; - else if (src->type == COLO_SOL_HEREDIT) - dst->forecast_behavior = COLO_FORECAST_BEHAVIOR_SOL; - dst->x = src->x; - dst->y = src->y; - dst->size = src->size; - dst->hp = src->hp; - dst->attack_range = stats->attack_range; - dst->attack_speed = stats->attack_speed; - dst->can_move = stats->can_move; - dst->default_style = stats->default_style; - dst->attack_timer = src->attack_timer; - dst->stun_timer = src->stun_timer; - dst->frozen_ticks = src->frozen_ticks; - dst->death_ticks = src->death_ticks; - if (!src->active) continue; - if (dst->forecast_behavior == COLO_FORECAST_BEHAVIOR_MANTICORE) { - dst->cycle_step = src->type_state.manticore.cycle_step; - for (int orb = 0; orb < 3; orb++) { - dst->orb_style[orb] = src->type_state.manticore.orb_style[orb]; - dst->fixed_orb_style[orb] = - src->type_state.manticore.fixed_orb_style[orb]; - } - } else if (dst->forecast_behavior == COLO_FORECAST_BEHAVIOR_WARBAND) { - dst->formation_dir = src->type_state.warband.formation_dir; - } else if (src->type == COLO_JAVELIN_COLOSSUS) { - dst->javelin_attack_count = src->type_state.javelin.attack_count; - } else if (dst->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL) { - dst->sol_attack_delay = s->sol.attack_delay; - dst->sol_immobile_ticks = s->sol.immobile_ticks; - } - } -} - -static void col_forecast_local_stamp_npc( - uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT], - const ColoForecastNpcLocal* npc, - uint8_t value -) { - if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return; - if (col_type_is_hazard_entity(npc->type)) return; - for (int dx = 0; dx < npc->size; dx++) { - for (int dy = 0; dy < npc->size; dy++) { - int gx, gy; - if (col_grid_index(npc->x + dx, npc->y + dy, &gx, &gy)) - npc_flags[gx][gy] = value; - } - } -} - -static void col_forecast_local_rebuild_npc_flags( - ColoForecastNpcLocal npcs[COLO_MAX_NPCS], - uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT], - const int slots[COLO_MAX_NPCS], - int slot_count -) { - memset(npc_flags, 0, sizeof(uint8_t) * COLO_ARENA_WIDTH * COLO_ARENA_HEIGHT); - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - col_forecast_local_stamp_npc(npc_flags, &npcs[i], 1); - } -} - -static int col_forecast_local_npc_blocked_ignore_player(void* user, int x, int y, int size) { - ColoForecastMoveCtx* ctx = (ColoForecastMoveCtx*)user; - const ColosseumState* s = ctx->state; - if (col_static_footprint_blocked_lookup(x, y, size)) return 1; - for (int dx = 0; dx < size; dx++) { - for (int dy = 0; dy < size; dy++) { - int tx = x + dx; - int ty = y + dy; - if (ctx->sol_clamp_active && !col_in_boss_arena(s, tx, ty)) return 1; - int gx, gy; - if (!col_grid_index(tx, ty, &gx, &gy)) return 1; - if (ctx->npc_flags[gx][gy]) return 1; - } - } - return 0; -} - -static int col_forecast_local_npc_blocked(void* user, int x, int y, int size) { - ColoForecastMoveCtx* ctx = (ColoForecastMoveCtx*)user; - if (col_forecast_local_npc_blocked_ignore_player(user, x, y, size)) return 1; - return ctx->player_x >= x && ctx->player_x < x + size && - ctx->player_y >= y && ctx->player_y < y + size; -} - -static int col_forecast_local_has_los_to_player( - const ColosseumState* s, - const ColoForecastNpcLocal* npc, - int player_x, - int player_y -) { - int sx = player_x; - int sy = player_y; - if (sx < npc->x) sx = npc->x; - if (sx > npc->x + npc->size - 1) sx = npc->x + npc->size - 1; - if (sy < npc->y) sy = npc->y; - if (sy > npc->y + npc->size - 1) sy = npc->y + npc->size - 1; - return col_tiles_have_los(s, sx, sy, player_x, player_y); -} - -static int col_forecast_local_dist_to_player( - const ColoForecastNpcLocal* npc, - int player_x, - int player_y -) { - return encounter_dist_to_npc(player_x, player_y, npc->x, npc->y, npc->size); -} - -static int col_forecast_local_melee_adjacent( - const ColoForecastNpcLocal* npc, - int player_x, - int player_y -) { - return entity_has_line_of_sight( - NULL, 0, npc->x, npc->y, npc->size, player_x, player_y, 1, 1); -} - -static int col_forecast_npc_threat_distance_impossible( - const ColoForecastNpcLocal* npc, - int player_x, - int player_y, - int horizon -) { - int attack_reach = npc->attack_range; - int steps_per_tick = npc->can_move ? 1 : 0; - if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_WARBAND) { - attack_reach = 1; - steps_per_tick = COLO_WARBAND_TILES_PER_TICK; - } else if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL) { - attack_reach = 15; - steps_per_tick = npc->can_move ? 1 : 0; - } - int dist = col_forecast_local_dist_to_player(npc, player_x, player_y); - return dist > attack_reach + steps_per_tick * horizon; -} - -static int col_forecast_action_rollout_writes_no_threats( - const ColoForecastNpcLocal base_npcs[COLO_MAX_NPCS], - const int slots[COLO_MAX_NPCS], - int slot_count, - const ColoStepOutForecastAction* action, - int horizon -) { - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - if (!col_forecast_npc_threat_distance_impossible( - &base_npcs[i], action->land_x, action->land_y, horizon)) { - return 0; - } - } - return 1; -} - -static int col_forecast_warband_adjacent( - const ColoForecastNpcLocal* npc, - int player_x, - int player_y -) { - int dx = npc->x > player_x ? npc->x - player_x : player_x - npc->x; - int dy = npc->y > player_y ? npc->y - player_y : player_y - npc->y; - return dx + dy == 1; -} - -static int col_forecast_move_result_matches( - const ColoForecastMoveResult* move, - const ColoForecastNpcLocal* npc, - const ColoStepOutForecastAction* action -) { - return move && move->held && - move->npc_x == npc->x && move->npc_y == npc->y && - move->land_x == action->land_x && move->land_y == action->land_y; -} - -static ColoForecastMoveResult col_forecast_local_move_npc( - const ColosseumState* s, - ColoForecastNpcLocal npcs[COLO_MAX_NPCS], - uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT], - int idx, - ColoStepOutForecastAction* action, - int sol_clamp_active -) { - ColoForecastMoveResult result = {0}; - ColoForecastNpcLocal* npc = &npcs[idx]; - if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return result; - if (!npc->can_move) return result; - if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL && - npc->sol_immobile_ticks > 0) - return result; - - int ranged_hold = npc->attack_range > 1 && - npc->forecast_behavior != COLO_FORECAST_BEHAVIOR_SOL; - if (ranged_hold) { - int dist = col_forecast_local_dist_to_player(npc, action->land_x, action->land_y); - int los = 0; - if (dist >= 1 && dist <= npc->attack_range && - (los = col_forecast_local_has_los_to_player( - s, npc, action->land_x, action->land_y))) { - result.held = 1; - result.dist = dist; - result.los = los; - result.npc_x = npc->x; - result.npc_y = npc->y; - result.land_x = action->land_x; - result.land_y = action->land_y; - return result; - } - } - if (npc->attack_timer > npc->attack_speed) return result; - - ColoForecastMoveCtx move_ctx; - move_ctx.state = s; - move_ctx.npc_flags = npc_flags; - move_ctx.player_x = action->land_x; - move_ctx.player_y = action->land_y; - move_ctx.sol_clamp_active = sol_clamp_active; - - col_forecast_local_stamp_npc(npc_flags, npc, 0); - int tx = action->land_x; - int ty = action->land_y; - EncounterNpcStepPolicy step_policy = npc->attack_range > 1 - ? ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET - : ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE; - encounter_npc_blocked_fn blocked = col_forecast_local_npc_blocked_ignore_player; - int steps = 1; - uint32_t rng = s->rng_state; - if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_WARBAND) { - tx = action->land_x + COLO_WARBAND_FORM_OFFSET[npc->formation_dir][0]; - ty = action->land_y + COLO_WARBAND_FORM_OFFSET[npc->formation_dir][1]; - step_policy = ENCOUNTER_NPC_STEP_TRAVEL_TARGET; - blocked = col_forecast_local_npc_blocked; - steps = COLO_WARBAND_TILES_PER_TICK; - } - if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL) { - step_policy = ENCOUNTER_NPC_STEP_STOP_AT_MELEE; - blocked = col_forecast_local_npc_blocked; - steps = 1; - } - - for (int step = 0; step < steps; step++) { - if (!encounter_npc_step_toward_policy( - &npc->x, &npc->y, tx, ty, npc->size, 1, - step_policy, blocked, &move_ctx, NULL, &rng)) { - break; - } - } - - col_forecast_local_stamp_npc(npc_flags, npc, 1); - return result; -} - -static int col_forecast_warband_phase(const ColosseumState* s, int tick_idx) { - if (s->warband_cycle_anchor < 0) return -1; - int phase = (s->tick + 1 + tick_idx - s->warband_cycle_anchor) % - COLO_WARBAND_CYCLE_TICKS; - if (phase < 0) phase += COLO_WARBAND_CYCLE_TICKS; - return phase; -} - -typedef struct { - int warband_phase[COLO_STEP_OUT_FORECAST_HORIZON]; - int multi_manticore_possible; - int sol_clamp_active; -} ColoForecastPrecomp; - -static ColoForecastPrecomp col_forecast_precompute( - const ColosseumState* s, - ColoForecastNpcLocal base_npcs[COLO_MAX_NPCS], - const int slots[COLO_MAX_NPCS], - int slot_count -) { - ColoForecastPrecomp pre; - for (int tick_idx = 0; tick_idx < COLO_STEP_OUT_FORECAST_HORIZON; tick_idx++) - pre.warband_phase[tick_idx] = col_forecast_warband_phase(s, tick_idx); - pre.sol_clamp_active = col_sol_clamp_active(s); - int manticore_count = 0; - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - const ColoForecastNpcLocal* npc = &base_npcs[i]; - if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_MANTICORE) - manticore_count++; - } - pre.multi_manticore_possible = manticore_count >= 2 ? 1 : 0; - return pre; -} - -static void col_forecast_manticore_delay_ready_peers( - const ColosseumState* s, - ColoForecastNpcLocal npcs[COLO_MAX_NPCS], - const int slots[COLO_MAX_NPCS], - int slot_count, - int idx, - const ColoStepOutForecastAction* action -) { - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - if (i == idx) continue; - ColoForecastNpcLocal* other = &npcs[i]; - if (other->forecast_behavior != COLO_FORECAST_BEHAVIOR_MANTICORE) - continue; - if (other->attack_timer > 1) continue; - if (other->cycle_step > 0) continue; - if (other->stun_timer > 0 || other->frozen_ticks > 0) continue; - int dist = col_forecast_local_dist_to_player( - other, action->land_x, action->land_y); - if (dist < 1 || dist > other->attack_range) continue; - if (!col_forecast_local_has_los_to_player( - s, other, action->land_x, action->land_y)) continue; - other->attack_timer = COLO_MANTICORE_STAGGER_TICKS + 1; - } -} - -static void col_forecast_local_fire_manticore_orb( - ColoForecastNpcLocal* npc, - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary, - int tick_idx -) { - if (npc->cycle_step < 0 || npc->cycle_step >= 3) return; - AttackStyle style = npc->orb_style[npc->cycle_step]; - col_step_out_forecast_record_style(action, summary, tick_idx, npc->type, style); - npc->cycle_step++; - if (npc->cycle_step >= 3) { - npc->cycle_step = -1; - npc->orb_style[0] = ATTACK_STYLE_NONE; - npc->orb_style[1] = ATTACK_STYLE_NONE; - npc->orb_style[2] = ATTACK_STYLE_NONE; - npc->attack_timer = npc->attack_speed - 2; - } -} - -static void col_forecast_local_attack_manticore( - const ColosseumState* s, - ColoForecastNpcLocal npcs[COLO_MAX_NPCS], - const int slots[COLO_MAX_NPCS], - int slot_count, - int idx, - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary, - int tick_idx, - const ColoForecastMoveResult* move_result, - int multi_manticore_possible -) { - ColoForecastNpcLocal* npc = &npcs[idx]; - if (npc->cycle_step > 0) { - if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; - col_forecast_local_fire_manticore_orb(npc, action, summary, tick_idx); - return; - } - - if (npc->attack_timer > 0) npc->attack_timer--; - if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; - - int move_result_matches = col_forecast_move_result_matches(move_result, npc, action); - int dist = move_result_matches ? move_result->dist : - col_forecast_local_dist_to_player(npc, action->land_x, action->land_y); - if (dist < 1 || dist > npc->attack_range) return; - int los = move_result_matches ? move_result->los : - col_forecast_local_has_los_to_player(s, npc, action->land_x, action->land_y); - if (!los) return; - if (npc->orb_style[0] == ATTACK_STYLE_NONE) { - for (int orb = 0; orb < 3; orb++) npc->orb_style[orb] = npc->fixed_orb_style[orb]; - npc->cycle_step = 0; - } - if (npc->attack_timer > 0) return; - col_forecast_local_fire_manticore_orb(npc, action, summary, tick_idx); - if (multi_manticore_possible) - col_forecast_manticore_delay_ready_peers( - s, npcs, slots, slot_count, idx, action); -} - -static void col_forecast_local_attack_warband( - ColoForecastNpcLocal* npc, - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary, - int tick_idx, - int phase -) { - if (phase < 0) return; - if (phase != col_warband_window_offset(npc->type)) return; - if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; - if (!col_forecast_warband_adjacent(npc, action->land_x, action->land_y)) return; - col_step_out_forecast_record_style( - action, summary, tick_idx, npc->type, npc->default_style); -} - -static void col_forecast_local_attack_sol( - const ColosseumState* s, - ColoForecastNpcLocal* npc, - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary, - int tick_idx -) { - if (npc->sol_immobile_ticks > 0 || npc->sol_attack_delay > 0) return; - int dist = col_forecast_local_dist_to_player(npc, action->land_x, action->land_y); - if (dist == 1) { - col_step_out_forecast_record_style( - action, summary, tick_idx, npc->type, ATTACK_STYLE_MELEE); - npc->sol_immobile_ticks = COLO_SOL_SPEAR_FREEZE; - npc->sol_attack_delay = COLO_SOL_SPEAR_DELAY; - return; - } - if (dist > 1 && dist <= 15 && - col_forecast_local_has_los_to_player(s, npc, action->land_x, action->land_y)) { - col_step_out_forecast_record_style( - action, summary, tick_idx, npc->type, ATTACK_STYLE_RANGED); - npc->sol_immobile_ticks = COLO_SOL_SPEAR_FREEZE; - npc->sol_attack_delay = COLO_SOL_SPEAR_DELAY; - } -} - -static void col_forecast_local_attack_standard( - const ColosseumState* s, - ColoForecastNpcLocal* npc, - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary, - int tick_idx, - const ColoForecastMoveResult* move_result -) { - if (npc->attack_timer > 0) npc->attack_timer--; - if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; - if (npc->attack_timer > 0) return; - - int move_result_matches = col_forecast_move_result_matches(move_result, npc, action); - int dist = move_result_matches ? move_result->dist : - col_forecast_local_dist_to_player(npc, action->land_x, action->land_y); - if (npc->type == COLO_MINOTAUR) { - if (dist == 1) { - col_step_out_forecast_record_style( - action, summary, tick_idx, npc->type, ATTACK_STYLE_MELEE); - } - npc->attack_timer = npc->attack_speed; - return; - } - if (npc->type == COLO_JAGUAR_WARRIOR) { - if (col_forecast_local_melee_adjacent(npc, action->land_x, action->land_y)) { - col_step_out_forecast_record_style( - action, summary, tick_idx, npc->type, ATTACK_STYLE_MELEE); - npc->attack_timer = npc->attack_speed; - } - return; - } - - if (npc->attack_range > 1) { - if (dist < 1 || dist > npc->attack_range) return; - int los = move_result_matches ? move_result->los : - col_forecast_local_has_los_to_player(s, npc, action->land_x, action->land_y); - if (!los) - return; - } else if (!col_forecast_local_melee_adjacent(npc, action->land_x, action->land_y)) { - return; - } - - if (npc->type == COLO_JAVELIN_COLOSSUS) { - npc->javelin_attack_count++; - if (npc->javelin_attack_count % 5 != 0) { - col_step_out_forecast_record_style( - action, summary, tick_idx, npc->type, ATTACK_STYLE_RANGED); - } - npc->attack_timer = npc->attack_speed; - return; - } - - if (npc->type == COLO_SERPENT_SHAMAN || - npc->type == COLO_SHOCKWAVE_COLOSSUS) { - col_step_out_forecast_record_style( - action, summary, tick_idx, npc->type, npc->default_style); - npc->attack_timer = npc->attack_speed; - } -} - -static void col_forecast_local_attack_npc( - const ColosseumState* s, - ColoForecastNpcLocal npcs[COLO_MAX_NPCS], - const int slots[COLO_MAX_NPCS], - int slot_count, - int idx, - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary, - int tick_idx, - const ColoForecastMoveResult* move_result, - const ColoForecastPrecomp* pre -) { - ColoForecastNpcLocal* npc = &npcs[idx]; - switch (npc->forecast_behavior) { - case COLO_FORECAST_BEHAVIOR_WARBAND: - col_forecast_local_attack_warband( - npc, action, summary, tick_idx, pre->warband_phase[tick_idx]); - return; - case COLO_FORECAST_BEHAVIOR_MANTICORE: - col_forecast_local_attack_manticore( - s, npcs, slots, slot_count, idx, action, summary, tick_idx, - move_result, pre->multi_manticore_possible); - return; - case COLO_FORECAST_BEHAVIOR_SOL: - col_forecast_local_attack_sol(s, npc, action, summary, tick_idx); - return; - case COLO_FORECAST_BEHAVIOR_STANDARD: - break; - } - col_forecast_local_attack_standard(s, npc, action, summary, tick_idx, move_result); -} - -static void col_step_out_forecast_readonly_tick( - const ColosseumState* s, - ColoForecastNpcLocal npcs[COLO_MAX_NPCS], - uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT], - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary, - int tick_idx, - const int slots[COLO_MAX_NPCS], - int slot_count, - const ColoForecastPrecomp* pre -) { - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - ColoForecastNpcLocal* npc = &npcs[i]; - if (npc->stun_timer > 0) npc->stun_timer--; - if (npc->frozen_ticks > 0) npc->frozen_ticks--; - if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL) { - if (npc->sol_immobile_ticks > 0) npc->sol_immobile_ticks--; - if (npc->sol_attack_delay > 0) npc->sol_attack_delay--; - } - ColoForecastMoveResult move_result = - col_forecast_local_move_npc( - s, npcs, npc_flags, i, action, pre->sol_clamp_active); - col_forecast_local_attack_npc( - s, npcs, slots, slot_count, i, action, summary, tick_idx, - &move_result, pre); - } -} - -static void col_forecast_local_copy_slot_set( - ColoForecastNpcLocal dst[COLO_MAX_NPCS], - const ColoForecastNpcLocal src[COLO_MAX_NPCS], - const int slots[COLO_MAX_NPCS], - int slot_count -) { - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - dst[i] = src[i]; - } -} - -static int col_forecast_action_is_run_tile(int action_idx) { - return action_idx >= 9 && action_idx < ENCOUNTER_MOVE_ACTIONS; -} - -static void col_forecast_static_threat_npc( - const ColosseumState* s, - const ColoForecastNpcLocal* npc, - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary -) { - if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return; - if (col_type_is_hazard_entity(npc->type)) return; - if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; - int lx = action->land_x; - int ly = action->land_y; - int dist = col_forecast_local_dist_to_player(npc, lx, ly); - - if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_WARBAND) { - if (col_forecast_warband_adjacent(npc, lx, ly)) - col_step_out_forecast_record_style( - action, summary, 0, npc->type, npc->default_style); - return; - } - if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL) { - if (npc->sol_immobile_ticks > 0 || npc->sol_attack_delay > 0) return; - if (dist == 1) { - col_step_out_forecast_record_style( - action, summary, 0, npc->type, ATTACK_STYLE_MELEE); - } else if (dist > 1 && dist <= 15 && - col_forecast_local_has_los_to_player(s, npc, lx, ly)) { - col_step_out_forecast_record_style( - action, summary, 0, npc->type, ATTACK_STYLE_RANGED); - } - return; - } - if (npc->type == COLO_MINOTAUR || npc->type == COLO_JAGUAR_WARRIOR) { - if (dist == 1) - col_step_out_forecast_record_style( - action, summary, 0, npc->type, ATTACK_STYLE_MELEE); - return; - } - if (npc->attack_range > 1) { - if (dist < 1 || dist > npc->attack_range) return; - if (!col_forecast_local_has_los_to_player(s, npc, lx, ly)) return; - AttackStyle style = npc->default_style; - if (npc->type == COLO_MANTICORE) { - int orb = npc->cycle_step > 0 ? npc->cycle_step : 0; - AttackStyle cur = npc->cycle_step > 0 ? npc->orb_style[orb] - : npc->fixed_orb_style[0]; - if (cur != ATTACK_STYLE_NONE) style = cur; - } - col_step_out_forecast_record_style(action, summary, 0, npc->type, style); - return; - } - if (dist == 1) - col_step_out_forecast_record_style( - action, summary, 0, npc->type, npc->default_style); -} - -static void col_forecast_static_threat_action( - const ColosseumState* s, - const ColoForecastNpcLocal base_npcs[COLO_MAX_NPCS], - const int slots[COLO_MAX_NPCS], - int slot_count, - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary -) { - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - col_forecast_static_threat_npc(s, &base_npcs[i], action, summary); - } -} - -typedef struct { - int pillar; - int tier; - ColoSolarflareOrb orb; -} ColoForecastSolarflare; - -static ColoForecastSolarflare col_forecast_solarflare_precompute(const ColosseumState* s) { - ColoForecastSolarflare pre = { .pillar = -1, .tier = 0, .orb = {0} }; - if (!s->solarflare.active) return pre; - int tier = col_mod_tier(s, COLO_MOD_SOLARFLARE); - if (tier == 0) return pre; - int best_pillar = -1; - int best_distance = 1 << 30; - for (int p = 0; p < COLO_NUM_PILLARS; p++) { - int x, y; - col_solarflare_tile(s, p, s->solarflare.step, &x, &y); - int dx = abs(x - s->player.x); - int dy = abs(y - s->player.y); - int distance = dx > dy ? dx : dy; - if (distance < best_distance) { - best_distance = distance; - best_pillar = p; - } - } - pre.pillar = best_pillar; - pre.tier = tier; - pre.orb = s->solarflare; - return pre; -} - -static int col_forecast_solarflare_contact_tick( - const ColosseumState* s, - const ColoForecastSolarflare* pre, - int land_x, - int land_y, - int horizon -) { - if (pre->pillar < 0) return -1; - ColoSolarflareOrb orb = pre->orb; - for (int tick_idx = 0; tick_idx < horizon; tick_idx++) { - col_solarflare_advance_orb(&orb, pre->tier); - int x, y; - col_solarflare_tile(s, pre->pillar, orb.step, &x, &y); - if (x == land_x && y == land_y) return tick_idx; - } - return -1; -} - -static void col_build_step_out_forecast_horizon_mode_summary( +static int col_primary_action_valid( const ColosseumState* s, - ColoStepOutForecast* out, - ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS], - int horizon, - int run_tile_mode + int primary_action ) { - if (horizon < 1) horizon = 1; - if (horizon > COLO_STEP_OUT_FORECAST_HORIZON) - horizon = COLO_STEP_OUT_FORECAST_HORIZON; - memset(out, 0, sizeof(*out)); - if (summaries) - memset(summaries, 0, sizeof(ColoForecastObsSummary) * ENCOUNTER_MOVE_ACTIONS); -#ifdef COLO_PROFILE_ENABLED - int col_fc_prof = COLO_PROFILE_ENABLED(); - double col_fc_t = col_fc_prof ? COLO_PROFILE_NOW_MS() : 0.0; -#endif - int forecast_slots[COLO_MAX_NPCS]; - int forecast_slot_count = col_collect_step_out_forecast_slots(s, forecast_slots); - - ColoForecastNpcLocal base_npcs[COLO_MAX_NPCS]; - uint8_t base_npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; - ColoForecastPrecomp pre = {0}; - if (forecast_slot_count > 0) { - col_forecast_local_copy_npc_slots( - s, base_npcs, forecast_slots, forecast_slot_count); - col_forecast_local_rebuild_npc_flags( - base_npcs, base_npc_flags, forecast_slots, forecast_slot_count); - pre = col_forecast_precompute( - s, base_npcs, forecast_slots, forecast_slot_count); - } -#ifdef COLO_PROFILE_ENABLED - if (col_fc_prof) { double n = COLO_PROFILE_NOW_MS(); COLO_PROFILE_ADD(COLO_PROF_FC_SETUP, n - col_fc_t); col_fc_t = n; } -#endif - - for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - if (!col_step_out_forecast_prepare_unique_action_ctx( - s, out, summaries, action_idx, forecast_slot_count)) { - continue; - } - ColoStepOutForecastAction* action = &out->actions[action_idx]; - ColoForecastObsSummary* summary = summaries ? &summaries[action_idx] : NULL; - if (col_forecast_action_rollout_writes_no_threats( - base_npcs, forecast_slots, forecast_slot_count, action, horizon)) { - continue; - } - if (run_tile_mode == COLO_FORECAST_RUN_TILE_STATIC_THREAT && - col_forecast_action_is_run_tile(action_idx)) { - col_forecast_static_threat_action( - s, base_npcs, forecast_slots, forecast_slot_count, action, summary); - col_step_out_forecast_finalize_action(action, horizon); - continue; - } - ColoForecastNpcLocal npcs[COLO_MAX_NPCS]; - uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; - col_forecast_local_copy_slot_set( - npcs, base_npcs, forecast_slots, forecast_slot_count); - memcpy(npc_flags, base_npc_flags, sizeof(base_npc_flags)); - for (int tick_idx = 0; tick_idx < horizon; tick_idx++) { - col_step_out_forecast_readonly_tick( - s, npcs, npc_flags, action, summary, tick_idx, - forecast_slots, forecast_slot_count, &pre); - } - col_step_out_forecast_finalize_action(action, horizon); + if (primary_action < 0 || primary_action >= COLO_PRIMARY_DIM) { + fprintf(stderr, "colosseum primary action out of range: %d\n", primary_action); + abort(); } -#ifdef COLO_PROFILE_ENABLED - if (col_fc_prof) { double n = COLO_PROFILE_NOW_MS(); COLO_PROFILE_ADD(COLO_PROF_FC_ROLLOUT, n - col_fc_t); col_fc_t = n; } -#endif - - ColoForecastSolarflare solarflare = col_forecast_solarflare_precompute(s); - for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - ColoStepOutForecastAction* action = &out->actions[action_idx]; - action->solarflare_contact_tick = action->valid - ? col_forecast_solarflare_contact_tick( - s, &solarflare, action->land_x, action->land_y, horizon) - : -1; + if (primary_action == 0) return 1; + if (s->modifiers.draft_pending && !s->modifiers.draft_free_movement) return 0; + if (primary_action < ENCOUNTER_MOVE_ACTIONS) { + int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[primary_action]; + int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[primary_action]; + return col_player_walkable((void*)s, nx, ny); } -#ifdef COLO_PROFILE_ENABLED - if (col_fc_prof) COLO_PROFILE_ADD(COLO_PROF_FC_SOLARFLARE, COLO_PROFILE_NOW_MS() - col_fc_t); -#endif -} - -static void col_build_step_out_forecast_horizon_mode( - const ColosseumState* s, - ColoStepOutForecast* out, - int horizon, - int run_tile_mode -) { - col_build_step_out_forecast_horizon_mode_summary( - s, out, NULL, horizon, run_tile_mode); -} -static int col_write_step_out_forecast_obs_summary( - const ColoStepOutForecast* forecast, - const ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS], - int horizon, - float* obs, - int i -) { - for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - const ColoStepOutForecastAction* action = &forecast->actions[action_idx]; - const ColoForecastObsSummary* summary = &summaries[action_idx]; - obs[i++] = action->valid ? 1.0f : 0.0f; - obs[i++] = (float)summary->first_attack_tick / (float)horizon; - obs[i++] = (float)summary->first_style_mask / 7.0f; - obs[i++] = (float)summary->max_hit / 150.0f; - obs[i++] = action->same_tick_mixed_style_conflict ? 1.0f : 0.0f; - obs[i++] = summary->ranged_magic_same_tick ? 1.0f : 0.0f; - obs[i++] = action->ranged_magic_offtick_opportunity ? 1.0f : 0.0f; - obs[i++] = action->melee_fallback_exposure ? 1.0f : 0.0f; - obs[i++] = action->solarflare_contact_tick >= 0 - ? (float)(horizon - action->solarflare_contact_tick) / (float)horizon - : 0.0f; - } - return i; + int obs_slot = col_primary_obs_slot_from_action(primary_action); + int npc_slot = col_obs_slot_to_npc(s, obs_slot); + return npc_slot >= 0 && col_npc_is_live_target(&s->npcs[npc_slot]); } -static void col_build_step_out_forecast_ctx( - const ColosseumState* s, - ColoStepOutForecast* out -) { - col_build_step_out_forecast_horizon_mode( - s, out, COLO_STEP_OUT_FORECAST_HORIZON, COLO_FORECAST_RUN_TILE_FULL); -} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc index 785106a2d1..ad869c7cdc 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc @@ -1,6 +1,6 @@ #line 76 "encounter_colosseum.h" -static float col_compute_reward_ctx(ColosseumState* s, const ColosseumContext* ctx); +static void col_accumulate_tick_stats(ColosseumState* s, const ColosseumContext* ctx); static void col_spawn_wave(ColosseumState* s); static void col_tick_npcs_ctx(ColosseumState* s, ColosseumContext* ctx); static void col_tick_player_ctx( @@ -13,7 +13,7 @@ static void col_resolve_player_pending_hits(ColosseumState* s); static void col_rebuild_player_collision_flags(ColosseumState* s); static void col_refresh_current_obs_slots_ctx(ColosseumState* s, const ColosseumContext* ctx); -static void col_sol_boss_tick(ColosseumState* s, ColosseumContext* ctx); +static void col_sol_boss_tick(ColosseumState* s); static void col_sol_begin_boss_arena(ColosseumState* s); static int col_sol_clamp_active(const ColosseumState* s); static int col_in_boss_arena(const ColosseumState* s, int x, int y); @@ -239,12 +239,12 @@ static void col_queue_npc_pending_hit( ColoNPC* npc = &s->npcs[npc_slot]; EncounterPendingHit hit = { .active = 1, - .damage = damage, - .ticks_remaining = ticks, - .attack_style = style, + .ticks_remaining = (int8_t)ticks, + .attack_style = (int8_t)style, .check_prayer = 0, - .spell_type = spell_type, + .spell_type = (int8_t)spell_type, .source_npc_slot = -1, + .damage = (int16_t)damage, }; encounter_pending_hit_queue_push( &npc->pending_hits, hit, "colosseum-npc", s->tick, npc_slot, npc->type); @@ -259,21 +259,12 @@ static inline int col_scale_incoming_damage(const ColosseumState* s, int dmg) { } static void col_push_player_pending_hit(ColosseumState* s, EncounterPendingHit hit) { - hit.damage = col_scale_incoming_damage(s, hit.damage); + hit.damage = (int16_t)col_scale_incoming_damage(s, hit.damage); encounter_pending_hit_queue_push( &s->player_pending_hits, hit, "colosseum-player", s->tick, hit.source_npc_slot, hit.source_npc_type); } -static int col_default_action_debug_log(void) { - const char* value = getenv("OSRS_COLOSSEUM_ACTION_LOG"); - if (!value || value[0] == '\0') return 0; - if (strcmp(value, "1") == 0) return 1; - if (strcmp(value, "0") == 0) return 0; - fprintf(stderr, "OSRS_COLOSSEUM_ACTION_LOG must be 0 or 1, got %s\n", value); - abort(); -} - static ColConfig col_default_config(void) { return (ColConfig){ .start_wave = 0, @@ -282,24 +273,12 @@ static ColConfig col_default_config(void) { .wave_clear_bonus = 1.0f, .boss_phase_bonus = 0.0f, .win_bonus = 0.0f, - .prayer_correct_reward = 0.0f, - .offpray_damage_penalty_coeff = 0.0f, - .multistyle_exposure_penalty_coeff = 0.0f, - .argmax_gear_reward_coeff = 0.0f, - .offensive_boost_reward_coeff = 0.0f, - .stall_penalty_coeff = 0.0f, .farm_safe_damage_cap = 0, .farm_cap_waves = COLO_FARM_CAP_WAVES, - .avoided_damage_coeff = 0.0f, .death_penalty_coeff = 0.0f, .timeout_penalty = 0.0f, .curriculum_agent = 0, - .step_out_forecast_obs_enabled = 1, - .threat_field_obs_enabled = 1, - .forecast_horizon = COLO_STEP_OUT_FORECAST_HORIZON, - .forecast_run_tile_mode = COLO_FORECAST_RUN_TILE_FULL, .mask_inventory_heads = 0, - .action_debug_log = col_default_action_debug_log(), .late_start_state_mode = 2, .late_start_supply_fraction_per_wave = 0.07f, .prayer_switch_fail_prob = 0.0f, @@ -324,11 +303,9 @@ static void col_destroy_context(EncounterContext* context) { static ColosseumContext* col_legacy_context(void) { static ColosseumContext ctx; - static Log log; static int initialized = 0; if (!initialized) { col_init_context_typed(&ctx); - ctx.log = &log; initialized = 1; } return &ctx; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc index 21cbdf241d..6bd8d8702b 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc @@ -248,14 +248,12 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* col_lab_abort("cannot move inactive npc slot %d", move->slot); if (!col_lab_in_arena(move->x, move->y)) col_lab_abort("npc tile out of arena: (%d,%d)", move->x, move->y); - int size = col_npc_effective_size(&s->npcs[move->slot]); - col_stamp_npc_collision_footprint(s, s->npcs[move->slot].x, - s->npcs[move->slot].y, size, 0); + col_stamp_npc_collision(s, &s->npcs[move->slot], 0); s->npcs[move->slot].x = move->x; s->npcs[move->slot].y = move->y; s->npcs[move->slot].target_x = move->x; s->npcs[move->slot].target_y = move->y; - col_stamp_npc_collision_footprint(s, move->x, move->y, size, 1); + col_stamp_npc_collision(s, &s->npcs[move->slot], 1); col_lab_clear_transient(s); col_lab_refresh_geometry(s); return; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc index c3d8ab1897..4cd0c9b225 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc @@ -7,16 +7,12 @@ static void col_write_mask_ctx(EncounterState* state, EncounterContext* context, int offset = 0; int frozen = s->modifiers.draft_pending && !s->modifiers.draft_free_movement; - mask[offset++] = 1.0f; - for (int d = 1; d < ENCOUNTER_MOVE_ACTIONS; d++) { - int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[d]; - int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[d]; - mask[offset++] = (!frozen && col_player_walkable(s, nx, ny)) ? 1.0f : 0.0f; + for (int primary_action = 0; + primary_action < COLO_PRIMARY_DIM; + primary_action++) { + mask[offset++] = col_primary_action_valid(s, primary_action) ? 1.0f : 0.0f; } - for (int n = 0; n < COLO_OBS_NPCS; n++) - mask[offset++] = (!frozen && col_obs_slot_is_targetable(s, n)) ? 1.0f : 0.0f; - mask[offset++] = 1.0f; mask[offset++] = s->player.prayer != PRAYER_NONE ? 1.0f : 0.0f; mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index 68a2b08bee..3766c5c051 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -34,33 +34,6 @@ static inline int col_wave_entry_delay_ticks(int target_wave) { #define COLO_MAX_NPCS 24 #define COLO_OBS_NPCS COLO_MAX_NPCS -#define COLO_STEP_OUT_FORECAST_HORIZON 4 -#define COLO_STEP_OUT_FORECAST_ACTION_FEATURES 9 -#define COLO_STEP_OUT_FORECAST_OBS_SIZE \ - (ENCOUNTER_MOVE_ACTIONS * COLO_STEP_OUT_FORECAST_ACTION_FEATURES) - -typedef struct { - int melee_count; - int ranged_count; - int magic_count; - int max_hit; -} ColoStepOutForecastTick; - -typedef struct { - int valid; - int land_x; - int land_y; - ColoStepOutForecastTick ticks[COLO_STEP_OUT_FORECAST_HORIZON]; - int same_tick_mixed_style_conflict; - int ranged_magic_offtick_opportunity; - int melee_fallback_exposure; - int solarflare_contact_tick; -} ColoStepOutForecastAction; - -typedef struct { - ColoStepOutForecastAction actions[ENCOUNTER_MOVE_ACTIONS]; -} ColoStepOutForecast; - typedef enum { COLO_FORECAST_BEHAVIOR_STANDARD = 0, COLO_FORECAST_BEHAVIOR_WARBAND, @@ -157,10 +130,18 @@ static uint64_t COLO_STATIC_LOS_BITS[COLO_STATIC_LOS_TABLE_WORDS]; static uint8_t COLO_STATIC_FOOTPRINT_BLOCKED[COLO_STATIC_FOOTPRINT_MAX_SIZE] [COLO_STATIC_FOOTPRINT_WIDTH][COLO_STATIC_FOOTPRINT_HEIGHT]; static int COLO_STATIC_LOOKUP_TABLES_BUILT = 0; +static int COLO_STATIC_ARENA_BUILT = 0; +static int COLO_NPC_STATS_BUILT = 0; static void col_build_static_lookup_tables_once(void); +/* Derived purely from compile-time tables, so build it once. Rebuilding per reset + zeroed a grid that every other env thread paths against, which handed them an + arena with no walls for the width of the rebuild. puf_init runs serially, so the + single build lands before any worker thread exists. */ static void col_build_static_arena(void) { + if (COLO_STATIC_ARENA_BUILT) return; + COLO_STATIC_ARENA_BUILT = 1; memset(COLO_STATIC_BLOCKED, 0, sizeof(COLO_STATIC_BLOCKED)); for (int y = 0; y < COLO_ARENA_HEIGHT; y++) { for (int sp = 0; sp < COLO_WALL_SPANS_PER_ROW; sp++) { @@ -262,53 +243,6 @@ static void col_build_static_lookup_tables_once(void) { COLO_STATIC_LOOKUP_TABLES_BUILT = 1; } -static void col_static_los_table_selftest(void) { - col_build_static_arena(); - for (int x0 = COLO_ARENA_MIN_X; x0 <= COLO_ARENA_MAX_X; x0++) { - for (int y0 = COLO_ARENA_MIN_Y; y0 <= COLO_ARENA_MAX_Y; y0++) { - for (int x1 = COLO_ARENA_MIN_X; x1 <= COLO_ARENA_MAX_X; x1++) { - for (int y1 = COLO_ARENA_MIN_Y; y1 <= COLO_ARENA_MAX_Y; y1++) { - int table = col_static_los_get(x0, y0, x1, y1); - int slow = col_tiles_have_los_reference_slowpath(x0, y0, x1, y1); - if (table != slow) { - fprintf(stderr, - "colosseum LoS table mismatch (%d,%d)->(%d,%d): table=%d slow=%d\n", - x0, y0, x1, y1, table, slow); - abort(); - } - } - } - } - } - printf("colosseum LoS table selftest PASS: %u pairs\n", - (unsigned)COLO_STATIC_LOS_TABLE_BITS); -} - -static void col_static_footprint_table_selftest(void) { - col_build_static_arena(); - int checks = 0; - int min_x = COLO_STATIC_FOOTPRINT_MIN_X - COLO_STATIC_FOOTPRINT_MAX_SIZE; - int min_y = COLO_STATIC_FOOTPRINT_MIN_Y - COLO_STATIC_FOOTPRINT_MAX_SIZE; - int max_x = COLO_STATIC_FOOTPRINT_MAX_X + COLO_STATIC_FOOTPRINT_MAX_SIZE; - int max_y = COLO_STATIC_FOOTPRINT_MAX_Y + COLO_STATIC_FOOTPRINT_MAX_SIZE; - for (int size = 1; size <= COLO_STATIC_FOOTPRINT_MAX_SIZE; size++) { - for (int x = min_x; x <= max_x; x++) { - for (int y = min_y; y <= max_y; y++) { - int table = col_static_footprint_blocked_lookup(x, y, size); - int slow = col_static_footprint_blocked_reference_slowpath(x, y, size); - if (table != slow) { - fprintf(stderr, - "colosseum footprint table mismatch (%d,%d) size=%d: table=%d slow=%d\n", - x, y, size, table, slow); - abort(); - } - checks++; - } - } - } - printf("colosseum footprint table selftest PASS: %d checks\n", checks); -} - #define COLO_NUM_SPAWN_ANCHORS 13 #define COLO_SPAWN_ZONE_SIZE 3 static const int COLO_SPAWN_ANCHORS[COLO_NUM_SPAWN_ANCHORS][2] = { @@ -524,6 +458,8 @@ static ColoNpcStats COLO_NPC_STATS[COLO_NUM_NPC_TYPES]; static void col_build_npc_stats(void) { col_build_static_arena(); + if (COLO_NPC_STATS_BUILT) return; + COLO_NPC_STATS_BUILT = 1; for (int i = 0; i < COLO_NUM_NPC_TYPES; i++) { const MonsterStats* m = &COLO_NPC_BASE[i]; const ColoNpcOverlay* o = &COLO_NPC_OVERLAY[i]; @@ -616,7 +552,7 @@ static const ColoWaveDef COLO_WAVES[COLO_NUM_WAVES] = { #define COLO_NUM_MODIFIERS 16 #define COLO_MODIFIER_DRAFT_OPTIONS 3 -#define COLO_RFDD_LAST_DRAFT_WAVE 6 +#define COLO_RFDD_FIRST_DRAFT_WAVE 6 #define COLO_DRAFT_UPGRADE_WEIGHT 2 @@ -677,14 +613,20 @@ typedef struct { #define COLO_MAX_TOTEMS COLO_MAX_NPCS #define COLO_TOTEM_HEAL_PCT 30 #define COLO_TOTEM_HEAL_INTERVAL 7 +#define COLO_TOTEM_SPAWN_HEAL_DELAY 5 +#define COLO_TOTEM_PROJECTILE_TICKS 2 #define COLO_TOTEM_RESPAWN_TICKS 200 #define COLO_TOTEM_SOL_HEAL 75 +#define COLO_TOTEM_SOL_EXTRA_INTERVAL 200 +#define COLO_TOTEM_OBS_COUNT_NORM 4 typedef struct { ColoHazardPhase phase; + int owner_slot; int npc_slot; int respawn_timer; int heal_timer; + int projectile_timer; } ColoTotem; #define COLO_SOLARFLARE_RING_STEPS 16 @@ -708,8 +650,9 @@ static const int COLO_DOOM_CAP[4] = { 0, 15, 10, 5 }; #define COLO_VOLATILITY_MAX_HIT 25 typedef enum { - COLO_POOL_VOLATILITY = 0, - COLO_POOL_REENTRY, + COLO_POOL_VOLATILITY = 1, + COLO_POOL_REENTRY = 2, + COLO_POOL_REENTRY_VOLATILITY = 3, } ColoMoltenPoolKind; typedef enum { @@ -742,6 +685,50 @@ typedef enum { COLO_SOL_AOE_SHIELD2, } ColoSolAoeAttack; +typedef enum { + COLO_SOL_DAMAGE_SPEAR_1 = 0, + COLO_SOL_DAMAGE_SPEAR_2, + COLO_SOL_DAMAGE_SHIELD_1, + COLO_SOL_DAMAGE_SHIELD_2, + COLO_SOL_DAMAGE_TRIPLE_PARRY, + COLO_SOL_DAMAGE_GRAPPLE, + COLO_SOL_DAMAGE_CRYSTAL_LASER, + COLO_SOL_DAMAGE_MOLTEN_SAND, + COLO_NUM_SOL_DAMAGE_SOURCES, +} ColoSolDamageSource; + +typedef enum { + COLO_JAVELIN_DAMAGE_BASIC_RANGED = 0, + COLO_JAVELIN_DAMAGE_SKYFALL, + COLO_JAVELIN_DAMAGE_REENTRY_POOL, + COLO_JAVELIN_DAMAGE_REENTRY_VOLATILITY_POOL, + COLO_NUM_JAVELIN_DAMAGE_SOURCES, +} ColoJavelinDamageSource; + +typedef enum { + COLO_DAMAGE_NPC_ATTACK = 0, + COLO_DAMAGE_JAVELIN_BASIC_RANGED, + COLO_DAMAGE_MANTICORE_VENOM, + COLO_DAMAGE_BEE_POISON, + COLO_DAMAGE_BEE_CONTACT, + COLO_DAMAGE_JAVELIN_SKYFALL, + COLO_DAMAGE_REENTRY_POOL, + COLO_DAMAGE_VOLATILITY_EXPLOSION, + COLO_DAMAGE_VOLATILITY_POOL, + COLO_DAMAGE_REENTRY_VOLATILITY_POOL, + COLO_DAMAGE_SOLARFLARE, + COLO_DAMAGE_SELF, + COLO_DAMAGE_SOL_SPEAR_1, + COLO_DAMAGE_SOL_SPEAR_2, + COLO_DAMAGE_SOL_SHIELD_1, + COLO_DAMAGE_SOL_SHIELD_2, + COLO_DAMAGE_SOL_TRIPLE_PARRY, + COLO_DAMAGE_SOL_GRAPPLE, + COLO_DAMAGE_SOL_CRYSTAL_LASER, + COLO_DAMAGE_SOL_MOLTEN_SAND, + COLO_NUM_DAMAGE_SOURCES, +} ColoDamageSource; + typedef enum { COLO_SOL_ATTACK_NONE = 0, COLO_SOL_ATTACK_SPEAR, @@ -808,7 +795,7 @@ typedef struct { int last_attack_kind; int last_variant; - int aoe_attack; + ColoSolAoeAttack aoe_attack; int aoe_age; int aoe_x, aoe_y; int aoe_dir_x, aoe_dir_y; @@ -1025,34 +1012,31 @@ typedef struct { #define COLO_BEST_GEAR_CACHE_WAYS 12 -#define COLO_WEAPON_CHOICE_OBS_CACHE_WAYS 8 -#define COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS (2 * OSRS_INVENTORY_SIZE + 2) +/* Feeds encounter_init_maxed_player_combat_stats and, through it, every prayer-restore + * feature the item observation table bakes. Never rewritten during an episode. */ +#define COLO_PLAYER_PRAYER_LEVEL 99 + +/* Replaces the 11 per-slot item stat rows. Those were a second encoding of combat maths + * the player block already carries: encounter_compute_loadout_stats sums the equipped set + * into EncounterLoadoutStats and the prefix emits attack_bonus, strength_bonus, max_hit, + * attack_speed, attack_range, all five defence bonuses, eff_level and spell_base_damage. + * Equipment EFFECTS were the one part with no aggregate in the observation. */ +#define COLO_EQUIPPED_AGG_OBS_FEATURES 10 #define COLO_INVENTORY_OBS_CACHE_WAYS 8 #define COLO_INVENTORY_OBS_CACHE_FLOATS \ - (OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES + \ - NUM_GEAR_SLOTS * OSRS_EQUIPPED_SELF_OBS_FEATURES) - -#define COLO_THREAT_FIELD_OBS_CACHE_WAYS 4 -#define COLO_THREAT_FIELD_OBS_CACHE_FLOATS (2 * 17 * 17) + (OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES_CODED + \ + COLO_EQUIPPED_AGG_OBS_FEATURES) typedef struct { ColoBestGear best_gear[COLO_BEST_GEAR_CACHE_WAYS][COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; uint64_t best_gear_sig[COLO_BEST_GEAR_CACHE_WAYS]; int best_gear_valid[COLO_BEST_GEAR_CACHE_WAYS]; int best_gear_next; - float weapon_choice[COLO_WEAPON_CHOICE_OBS_CACHE_WAYS][COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS]; - uint64_t weapon_choice_sig[COLO_WEAPON_CHOICE_OBS_CACHE_WAYS]; - int weapon_choice_valid[COLO_WEAPON_CHOICE_OBS_CACHE_WAYS]; - int weapon_choice_next; float inventory[COLO_INVENTORY_OBS_CACHE_WAYS][COLO_INVENTORY_OBS_CACHE_FLOATS]; uint64_t inventory_sig[COLO_INVENTORY_OBS_CACHE_WAYS]; int inventory_valid[COLO_INVENTORY_OBS_CACHE_WAYS]; int inventory_next; - float threat_field[COLO_THREAT_FIELD_OBS_CACHE_WAYS][COLO_THREAT_FIELD_OBS_CACHE_FLOATS]; - uint64_t threat_field_sig[COLO_THREAT_FIELD_OBS_CACHE_WAYS]; - int threat_field_valid[COLO_THREAT_FIELD_OBS_CACHE_WAYS]; - int threat_field_next; } ColoObsMemoCaches; typedef enum { @@ -1406,47 +1390,26 @@ typedef struct { float wave_clear_bonus; float boss_phase_bonus; float win_bonus; - float prayer_correct_reward; - float offpray_damage_penalty_coeff; - float multistyle_exposure_penalty_coeff; - float argmax_gear_reward_coeff; - - float offensive_boost_reward_coeff; - - float stall_penalty_coeff; int farm_safe_damage_cap; int farm_cap_waves; - - float avoided_damage_coeff; float death_penalty_coeff; float timeout_penalty; int curriculum_agent; int loadout_profile_mode; float beginner_loadout_fraction; - int step_out_forecast_obs_enabled; - int threat_field_obs_enabled; - int forecast_horizon; - int forecast_run_tile_mode; int mask_inventory_heads; - int action_debug_log; int late_start_state_mode; float late_start_supply_fraction_per_wave; - int prayer_oracle_mode; - int bis_gear_oracle_mode; - int invuln_mode; - int episode_max_ticks_override; - int remove_brews; - float prayer_switch_fail_prob; float player_damage_received_scale; @@ -1457,11 +1420,6 @@ typedef struct { } ColConfig; -typedef enum { - COLO_FORECAST_RUN_TILE_FULL = 0, - COLO_FORECAST_RUN_TILE_STATIC_THREAT = 1, -} ColoForecastRunTileMode; - typedef enum { COLO_NPC_RENDER_ATTACK_NORMAL = 0, COLO_NPC_RENDER_ATTACK_JAVELIN_SKYFALL, @@ -1472,7 +1430,6 @@ typedef struct { const CollisionMap* collision_map; int world_offset_x; int world_offset_y; - Log* log; const HumanCommand* human_commands; int human_command_count; int human_command_mode; @@ -1489,13 +1446,23 @@ typedef struct { int player_venator_chain_slots[OSRS_VENATOR_MAX_CHAIN_HITS]; } ColosseumContext; +/* Clears only what was written. col_render_hit_events_add aborts past + * ENCOUNTER_RENDER_HITS_MAX and fills strictly below count, and a row whose count is zero + * is already zero from the previous clear, so the old blanket memset of + * COLO_MAX_NPCS x ENCOUNTER_RENDER_HITS_MAX ints re-zeroed memory that was already zero. + * That was 3488 B per tick per env and 15% of the whole env step. */ static inline void col_context_clear_render_events(ColosseumContext* ctx) { memset(ctx->npc_render_attack_anim, 0, sizeof(ctx->npc_render_attack_anim)); memset(ctx->manticore_charge_started, 0, sizeof(ctx->manticore_charge_started)); + for (int h = 0; h < ctx->player_render_hit_count; h++) + ctx->player_render_hit_damage[h] = 0; ctx->player_render_hit_count = 0; - memset(ctx->player_render_hit_damage, 0, sizeof(ctx->player_render_hit_damage)); - memset(ctx->npc_render_hit_count, 0, sizeof(ctx->npc_render_hit_count)); - memset(ctx->npc_render_hit_damage, 0, sizeof(ctx->npc_render_hit_damage)); + for (int i = 0; i < COLO_MAX_NPCS; i++) { + int n = ctx->npc_render_hit_count[i]; + if (n <= 0) continue; + for (int h = 0; h < n; h++) ctx->npc_render_hit_damage[i][h] = 0; + ctx->npc_render_hit_count[i] = 0; + } ctx->thrall_attacked_this_tick = 0; ctx->player_spec_fired_kind = 0; ctx->player_venator_chain_count = 0; @@ -1548,6 +1515,7 @@ typedef struct { int waves_cleared; int win; int died; + int timed_out; float total_damage_dealt; float total_damage_received; float total_boss_damage; @@ -1564,6 +1532,7 @@ typedef struct { float total_damage_by_type[COLO_NUM_NPC_TYPES]; float death_by_type[COLO_NUM_NPC_TYPES]; + float npc_attack_death_by_type[COLO_NUM_NPC_TYPES]; float death_fatal_damage; float offpray_damage_conflict; @@ -1576,7 +1545,33 @@ typedef struct { float death_dmg_self; float death_heal_remaining; float farm_damage; + float reward_steps; + float reward_clamped_steps; + float reward_clamp_loss; + float reward_raw_peak; + float clamp_loss_wave_clear; + float clamp_loss_win; + float rew_damage; + float rew_boss_phase; + float rew_wave_clear; + float rew_win; + float rew_death; + float rew_timeout; + float avoid_total; + float avoid_achieved; + float avoid_missed; + float avoid_impossible; + float dmg_unprayable; + float inv_memo_hits; + float inv_memo_misses; + float npc_blocked_calls; + float npc_blocked_tiles; + float npc_stamp_tiles; float typeless_damage_by_type[COLO_NUM_NPC_TYPES]; + float sol_damage_by_source[COLO_NUM_SOL_DAMAGE_SOURCES]; + float javelin_damage_by_source[COLO_NUM_JAVELIN_DAMAGE_SOURCES]; + float death_by_source[COLO_NUM_DAMAGE_SOURCES]; + float doom_death_by_source[COLO_NUM_DAMAGE_SOURCES]; float outcome_score; float max_wave_depth; } ColosseumLog; @@ -1617,6 +1612,7 @@ typedef struct { int doom_stacks; ColoBeeSwarm bees[COLO_MAX_BEE_SWARMS]; ColoTotem totems[COLO_MAX_TOTEMS]; + int sol_totem_spawn_timer; ColoSolarflareOrb solarflare; int player_venom; int player_venom_timer; @@ -1633,8 +1629,9 @@ typedef struct { float reward; float episode_return; int last_hit_by_type; + int last_damage_source; + int last_doom_damage_source; int rewarded_boss_phase; - float avoided_damage_already_rewarded; int min_sol_hp_seen; OsrsInteraction interaction; @@ -1674,15 +1671,21 @@ typedef struct { int player_dest_x, player_dest_y; uint8_t npc_collision_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; - uint8_t player_collision_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; - - ColosseumLog log; + /* The player occupies one tile, so a full arena grid recorded one cell and was + * memset in full twice per tick. Cache the cell instead. -1 means off-grid. */ + int player_grid_x, player_grid_y; int start_wave; uint32_t rng_state; float active_player_damage_received_scale; + int profiling_suppressed; + /* Last two members, and both are skipped by probe_colo_sim_invariant. The probe hashes + * this struct's raw bytes, so a member that grows mid-struct repads everything after it + * and moves all twelve digests with no behaviour change -- which is exactly what adding + * reward telemetry to the log did. At the tail, neither can shift a hashed byte. */ + ColosseumLog log; ColoObsMemoCaches obs_memos; } ColosseumState; @@ -1691,7 +1694,7 @@ typedef enum { COLO_DMG_UNPRAYABLE = 1, } ColoDamageChannel; -static inline void col_damage_player_from( +static inline int col_damage_player_from( ColosseumState* s, int dmg, int npc_type, ColoDamageChannel channel); static void col_apply_bis_gear_oracle(ColosseumState* s); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc index d4c00c1408..b87a7027ab 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc @@ -10,11 +10,17 @@ static inline int col_mod_tier(const ColosseumState* s, ColoModifier mod) { return col_mod_active(s, mod) ? s->modifiers.tier[mod] : 0; } +static const ColoModifier COLO_FIRST_DRAFT_OPTIONS[COLO_MODIFIER_DRAFT_OPTIONS] = { + COLO_MOD_RELENTLESS, + COLO_MOD_BLASPHEMY, + COLO_MOD_FRAILTY, +}; + static int col_modifier_eligible(const ColosseumState* s, ColoModifier mod, int next_wave) { if (s->modifiers.tier[mod] >= COLO_MODIFIER_MAX_TIER[mod]) return 0; if (COLO_MODIFIER_PRE_BOSS_ONLY[mod] && next_wave >= COLO_WAVE_BOSS) return 0; if ((mod == COLO_MOD_RED_FLAG || mod == COLO_MOD_DYNAMIC_DUO) && - next_wave > COLO_RFDD_LAST_DRAFT_WAVE) return 0; + next_wave < COLO_RFDD_FIRST_DRAFT_WAVE) return 0; return 1; } @@ -25,9 +31,27 @@ static int col_modifier_draft_weight(const ColosseumState* s, ColoModifier mod) return 1; } +static void col_modifier_open_first_draft(ColosseumState* s) { + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + s->modifiers.draft_options[o] = (int)COLO_FIRST_DRAFT_OPTIONS[o]; + for (int o = COLO_MODIFIER_DRAFT_OPTIONS - 1; o > 0; o--) { + int j = encounter_rand_int(&s->rng_state, o + 1); + int tmp = s->modifiers.draft_options[o]; + s->modifiers.draft_options[o] = s->modifiers.draft_options[j]; + s->modifiers.draft_options[j] = tmp; + } + s->modifiers.draft_pending = 1; + s->modifiers.draft_gates_spawn = 1; +} + static void col_modifier_open_draft(ColosseumState* s, int next_wave) { for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) s->modifiers.draft_options[o] = -1; + if (next_wave == 0) { + col_modifier_open_first_draft(s); + return; + } + int pool[COLO_NUM_REAL_MODIFIERS]; int weight[COLO_NUM_REAL_MODIFIERS]; int pool_n = 0; @@ -183,22 +207,19 @@ typedef struct { ColoWaveEntrySample samples[COLO_WAVE_ENTRY_RESERVOIR_CAP]; } ColoWaveEntryReservoir; -static ColoWaveEntryReservoir g_colo_wave_entry_res[COLO_NUM_WAVES]; -static char g_colo_wave_entry_lock; -static uint32_t g_colo_wave_entry_rng = 0x9e3779b9u; - -static void col_wave_entry_lock(void) { - while (__atomic_test_and_set(&g_colo_wave_entry_lock, __ATOMIC_ACQUIRE)) {} -} - -static void col_wave_entry_unlock(void) { - __atomic_clear(&g_colo_wave_entry_lock, __ATOMIC_RELEASE); -} +/* Per worker thread, not global. A shared reservoir is order dependent by + construction: res->seen and the slot draw both advance in whichever order + threads happen to clear waves, and locking fixes tearing but not order. Envs + are partitioned schedule(static) (src/pufferl.cu:1040,1334), so each thread + sees a fixed env subset in a fixed order and its reservoir is reproducible. + Donors are spread across threads because start_wave is drawn per env, so a + thread's ~256 envs still hold ~80 wave-1 donors to sample from. */ +static OSRS_THREAD_LOCAL ColoWaveEntryReservoir g_colo_wave_entry_res[COLO_NUM_WAVES]; +static OSRS_THREAD_LOCAL uint32_t g_colo_wave_entry_rng = 0x9e3779b9u; static void col_record_wave_entry(const ColosseumState* s, int entered_wave) { if (s->start_wave != 0) return; if (entered_wave <= 0 || entered_wave >= COLO_NUM_WAVES) return; - col_wave_entry_lock(); ColoWaveEntryReservoir* res = &g_colo_wave_entry_res[entered_wave]; res->seen++; int slot = -1; @@ -218,21 +239,18 @@ static void col_record_wave_entry(const ColosseumState* s, int entered_wave) { out->prayer_points = (int16_t)s->player.current_prayer; out->special_energy = (uint8_t)s->player.special_energy; } - col_wave_entry_unlock(); } static int col_apply_wave_entry_sample(ColosseumState* s, int start_wave) { if (start_wave <= 0 || start_wave >= COLO_NUM_WAVES) return 0; ColoWaveEntrySample sample; int have = 0; - col_wave_entry_lock(); ColoWaveEntryReservoir* res = &g_colo_wave_entry_res[start_wave]; if (res->stored > 0) { uint32_t r = xorshift32(&g_colo_wave_entry_rng); sample = res->samples[r % (uint32_t)res->stored]; have = 1; } - col_wave_entry_unlock(); if (!have) return 0; memcpy(s->inventory_cells, sample.cells, sizeof(sample.cells)); @@ -285,8 +303,27 @@ static int col_mod_myopia_range(const ColosseumState* s, int base_range) { return r < 1 ? 1 : r; } -static void col_mod_on_player_damaged(ColosseumState* s, int damage) { +static void col_mod_add_doom_stack( + ColosseumState* s, + ColoDamageSource source +) { + assert(source >= 0 && source < COLO_NUM_DAMAGE_SOURCES); + int tier = col_mod_tier(s, COLO_MOD_DOOM); + if (tier <= 0) return; + int cap = COLO_DOOM_CAP[tier]; + if (s->doom_stacks < cap && s->doom_stacks + 1 >= cap) + s->last_doom_damage_source = source; + s->doom_stacks++; +} + +static void col_mod_on_player_damaged( + ColosseumState* s, + int damage, + ColoDamageSource source +) { if (damage <= 0) return; + assert(source >= 0 && source < COLO_NUM_DAMAGE_SOURCES); + s->last_damage_source = source; int blas = col_mod_tier(s, COLO_MOD_BLASPHEMY); if (blas > 0 && s->player.current_prayer > 0) { @@ -295,7 +332,7 @@ static void col_mod_on_player_damaged(ColosseumState* s, int damage) { if (s->player.current_prayer < 0) s->player.current_prayer = 0; } - if (col_mod_active(s, COLO_MOD_DOOM)) s->doom_stacks++; + col_mod_add_doom_stack(s, source); } static int col_mod_doom_lethal(const ColosseumState* s) { @@ -325,9 +362,13 @@ static void col_mod_tick_venom(ColosseumState* s) { if (s->player_venom_timer > 0) s->player_venom_timer--; if (s->player_venom_timer > 0) return; int dmg = s->player_venom; - col_damage_player_from(s, dmg, COLO_MANTICORE, COLO_DMG_UNPRAYABLE); - s->last_hit_by_type = COLO_MANTICORE; - col_mod_on_player_damaged(s, dmg); + int applied = + col_damage_player_from(s, dmg, COLO_MANTICORE, COLO_DMG_UNPRAYABLE); + if (applied > 0) { + s->last_hit_by_type = COLO_MANTICORE; + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_MANTICORE_VENOM); + } s->player_venom += COLO_VENOM_STEP; if (s->player_venom > COLO_VENOM_CAP) s->player_venom = COLO_VENOM_CAP; s->player_venom_timer = COLO_VENOM_INTERVAL; @@ -347,10 +388,12 @@ static void col_mod_tick_poison(ColosseumState* s) { if (s->player_poison_timer > 0) s->player_poison_timer--; if (s->player_poison_timer > 0) return; int dmg = (s->player_poison + 4) / 5; - col_damage_player_from(s, dmg, COLO_BEE_SWARM, COLO_DMG_UNPRAYABLE); - if (dmg > 0) { + int applied = + col_damage_player_from(s, dmg, COLO_BEE_SWARM, COLO_DMG_UNPRAYABLE); + if (applied > 0) { s->last_hit_by_type = COLO_BEE_SWARM; - col_mod_on_player_damaged(s, dmg); + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_BEE_POISON); } s->player_poison--; if (s->player_poison <= 0) { @@ -361,24 +404,29 @@ static void col_mod_tick_poison(ColosseumState* s) { } } +static int col_hazard_tile_free(const ColosseumState* s, int x, int y) { + if (col_static_blocked(x, y)) return 0; + if (col_sol_clamp_active(s) && !col_in_boss_arena(s, x, y)) return 0; + return 1; +} + static int col_spawn_hazard_npc_beside( ColosseumState* s, ColoNpcType type, const ColoNPC* owner ) { + if (col_find_free_npc(s) < 0) return -1; int size = col_npc_effective_size(owner); + if (col_hazard_tile_free(s, owner->x - 1, owner->y - 1)) + return col_spawn_npc_at(s, type, owner->x - 1, owner->y - 1); + for (int ring = 0; ring < 2; ring++) { int lo_x = owner->x - 1 - ring, hi_x = owner->x + size + ring; int lo_y = owner->y - 1 - ring, hi_y = owner->y + size + ring; - for (int pass = 0; pass < 2; pass++) { - for (int y = lo_y; y <= hi_y; y++) { - for (int x = lo_x; x <= hi_x; x++) { - int on_shell = (x == lo_x || x == hi_x || y == lo_y || y == hi_y); - if (!on_shell) continue; - int is_east = (x == hi_x); - if ((pass == 0) != is_east) continue; - if (col_static_blocked(x, y)) continue; - if (col_sol_clamp_active(s) && !col_in_boss_arena(s, x, y)) continue; - return col_spawn_npc_at(s, type, x, y); - } + for (int y = lo_y; y <= hi_y; y++) { + for (int x = lo_x; x <= hi_x; x++) { + int on_shell = (x == lo_x || x == hi_x || y == lo_y || y == hi_y); + if (!on_shell) continue; + if (!col_hazard_tile_free(s, x, y)) continue; + return col_spawn_npc_at(s, type, x, y); } } } @@ -387,8 +435,28 @@ static int col_spawn_hazard_npc_beside( } static ColoTotem* col_totem_for_owner(ColosseumState* s, int owner_slot) { - assert(owner_slot >= 0 && owner_slot < COLO_MAX_TOTEMS); - return &s->totems[owner_slot]; + for (int t = 0; t < COLO_MAX_TOTEMS; t++) { + ColoTotem* tot = &s->totems[t]; + if (tot->phase != COLO_HAZARD_NONE && tot->owner_slot == owner_slot) return tot; + } + return NULL; +} + +static ColoTotem* col_totem_alloc(ColosseumState* s, int owner_slot) { + for (int t = 0; t < COLO_MAX_TOTEMS; t++) { + ColoTotem* tot = &s->totems[t]; + if (tot->phase != COLO_HAZARD_NONE) continue; + *tot = (ColoTotem){ .phase = COLO_HAZARD_NONE, .owner_slot = owner_slot, .npc_slot = -1 }; + return tot; + } + return NULL; +} + +static int col_find_sol_slot(const ColosseumState* s) { + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s->npcs[i].active && s->npcs[i].hp > 0 && s->npcs[i].type == COLO_SOL_HEREDIT) + return i; + return -1; } static void col_hazard_on_npc_death(ColosseumState* s, int npc_slot) { @@ -400,6 +468,7 @@ static void col_hazard_on_npc_death(ColosseumState* s, int npc_slot) { tot->phase = COLO_HAZARD_RESPAWNING; tot->npc_slot = -1; tot->respawn_timer = COLO_TOTEM_RESPAWN_TICKS; + tot->projectile_timer = 0; return; } return; @@ -417,8 +486,7 @@ static void col_hazard_on_npc_death(ColosseumState* s, int npc_slot) { } } -static void col_mod_on_owner_death(ColosseumState* s, int owner_slot) { - ColoTotem* tot = col_totem_for_owner(s, owner_slot); +static void col_totem_release(ColosseumState* s, ColoTotem* tot) { if (tot->phase == COLO_HAZARD_ALIVE && tot->npc_slot >= 0 && s->npcs[tot->npc_slot].active && s->npcs[tot->npc_slot].type == COLO_HEALING_TOTEM) @@ -426,19 +494,61 @@ static void col_mod_on_owner_death(ColosseumState* s, int owner_slot) { *tot = (ColoTotem){ .phase = COLO_HAZARD_NONE }; } -static void col_totem_spawn_for_owner(ColosseumState* s, int owner_slot) { - ColoTotem* tot = col_totem_for_owner(s, owner_slot); - int slot = col_spawn_hazard_npc_beside(s, COLO_HEALING_TOTEM, &s->npcs[owner_slot]); - if (slot < 0) { +static void col_mod_on_owner_death(ColosseumState* s, int owner_slot) { + for (int t = 0; t < COLO_MAX_TOTEMS; t++) { + ColoTotem* tot = &s->totems[t]; + if (tot->phase == COLO_HAZARD_NONE || tot->owner_slot != owner_slot) continue; + col_totem_release(s, tot); + } +} +static void col_totem_place(ColosseumState* s, ColoTotem* tot) { + int slot = col_spawn_hazard_npc_beside( + s, COLO_HEALING_TOTEM, &s->npcs[tot->owner_slot]); + if (slot < 0) { tot->phase = COLO_HAZARD_RESPAWNING; tot->npc_slot = -1; tot->respawn_timer = 1; + tot->projectile_timer = 0; return; } tot->phase = COLO_HAZARD_ALIVE; tot->npc_slot = slot; - tot->heal_timer = COLO_TOTEM_HEAL_INTERVAL; + tot->heal_timer = COLO_TOTEM_SPAWN_HEAL_DELAY; + tot->projectile_timer = 0; +} + +static int col_totem_spawn_for_owner(ColosseumState* s, int owner_slot) { + ColoTotem* tot = col_totem_alloc(s, owner_slot); + if (!tot) return 0; + col_totem_place(s, tot); + return 1; +} + +static int col_totem_heal_amount(const ColoNPC* owner) { + return owner->type == COLO_SOL_HEREDIT + ? COLO_TOTEM_SOL_HEAL + : (owner->max_hp * COLO_TOTEM_HEAL_PCT) / 100; +} + +static void col_totem_apply_heal(ColoNPC* owner) { + owner->hp += col_totem_heal_amount(owner); + if (owner->hp > owner->max_hp) owner->hp = owner->max_hp; +} + +static const ColoTotem* col_totem_for_npc_slot(const ColosseumState* s, int npc_slot) { + for (int t = 0; t < COLO_MAX_TOTEMS; t++) { + const ColoTotem* tot = &s->totems[t]; + if (tot->phase == COLO_HAZARD_ALIVE && tot->npc_slot == npc_slot) return tot; + } + return NULL; +} + +static int col_live_totem_count(const ColosseumState* s) { + int n = 0; + for (int t = 0; t < COLO_MAX_TOTEMS; t++) + if (s->totems[t].phase == COLO_HAZARD_ALIVE) n++; + return n; } static void col_mod_on_npc_hp_changed(ColosseumState* s, int npc_slot) { @@ -447,25 +557,42 @@ static void col_mod_on_npc_hp_changed(ColosseumState* s, int npc_slot) { if (!npc->active || npc->hp <= 0 || npc->max_hp <= 0) return; if (col_type_is_hazard_entity(npc->type)) return; if (npc->hp * 2 > npc->max_hp) return; - if (col_totem_for_owner(s, npc_slot)->phase != COLO_HAZARD_NONE) return; - col_totem_spawn_for_owner(s, npc_slot); + if (col_totem_for_owner(s, npc_slot)) return; + if (!col_totem_spawn_for_owner(s, npc_slot)) return; + if (npc->type == COLO_SOL_HEREDIT) + s->sol_totem_spawn_timer = COLO_TOTEM_SOL_EXTRA_INTERVAL; +} + +static void col_mod_tick_sol_extra_totems(ColosseumState* s) { + if (s->sol_totem_spawn_timer <= 0) return; + int sol_slot = col_find_sol_slot(s); + if (sol_slot < 0) { + s->sol_totem_spawn_timer = 0; + return; + } + s->sol_totem_spawn_timer--; + if (s->sol_totem_spawn_timer > 0) return; + s->sol_totem_spawn_timer = COLO_TOTEM_SOL_EXTRA_INTERVAL; + col_totem_spawn_for_owner(s, sol_slot); } static void col_mod_tick_totems(ColosseumState* s) { - for (int o = 0; o < COLO_MAX_TOTEMS; o++) { - ColoTotem* tot = &s->totems[o]; + col_mod_tick_sol_extra_totems(s); + + for (int t = 0; t < COLO_MAX_TOTEMS; t++) { + ColoTotem* tot = &s->totems[t]; if (tot->phase == COLO_HAZARD_NONE) continue; - ColoNPC* owner = &s->npcs[o]; + ColoNPC* owner = &s->npcs[tot->owner_slot]; if (!owner->active || owner->hp <= 0 || col_type_is_hazard_entity(owner->type)) { - col_mod_on_owner_death(s, o); + col_totem_release(s, tot); continue; } if (tot->phase == COLO_HAZARD_RESPAWNING) { if (tot->respawn_timer > 0) tot->respawn_timer--; - if (tot->respawn_timer == 0) col_totem_spawn_for_owner(s, o); + if (tot->respawn_timer == 0) col_totem_place(s, tot); continue; } @@ -475,16 +602,21 @@ static void col_mod_tick_totems(ColosseumState* s) { continue; } + if (tot->projectile_timer > 0) { + tot->projectile_timer--; + if (tot->projectile_timer == 0) { + col_totem_apply_heal(owner); + tot->heal_timer = + COLO_TOTEM_HEAL_INTERVAL - COLO_TOTEM_PROJECTILE_TICKS; + } + continue; + } + + if (owner->hp >= owner->max_hp) continue; + if (tot->heal_timer > 0) tot->heal_timer--; if (tot->heal_timer > 0) continue; - tot->heal_timer = COLO_TOTEM_HEAL_INTERVAL; - if (owner->type == COLO_SOL_HEREDIT) { - owner->hp += COLO_TOTEM_SOL_HEAL; - } else { - if (owner->hp * 2 > owner->max_hp) continue; - owner->hp += (owner->max_hp * COLO_TOTEM_HEAL_PCT) / 100; - } - if (owner->hp > owner->max_hp) owner->hp = owner->max_hp; + tot->projectile_timer = COLO_TOTEM_PROJECTILE_TICKS; } } @@ -539,7 +671,7 @@ static void col_mod_add_molten_pool( if (col_static_blocked(x, y)) return; for (int i = 0; i < s->molten_count; i++) { if (s->molten_x[i] != x || s->molten_y[i] != y) continue; - s->molten_kind[i] = kind; + s->molten_kind[i] |= kind; if (lifetime == COLO_POOL_PERMANENT) s->molten_lifetime[i] = lifetime; s->molten_burn_timer[i] = 0; return; @@ -570,28 +702,56 @@ static void col_mod_clear_wave_end_pools(ColosseumState* s) { s->molten_count = out; } -static int col_mod_molten_pool_damage(ColosseumState* s, ColoMoltenPoolKind kind) { - if (kind == COLO_POOL_REENTRY) - return 1 + encounter_rand_int(&s->rng_state, COLO_REENTRY_MOLTEN_MAX_HIT); - return COLO_MOLTEN_SAND_MIN_HIT - + encounter_rand_int(&s->rng_state, COLO_MOLTEN_SAND_RAND); +static int col_mod_molten_pool_damage(ColosseumState* s) { + return 1 + encounter_rand_int( + &s->rng_state, COLO_REENTRY_MOLTEN_MAX_HIT); } static void col_mod_tick_molten_pools(ColosseumState* s) { int burned = 0; for (int i = 0; i < s->molten_count; i++) { - int reentry = s->molten_kind[i] == COLO_POOL_REENTRY; + int reentry = s->molten_kind[i] & COLO_POOL_REENTRY; + int volatility = s->molten_kind[i] & COLO_POOL_VOLATILITY; + assert(reentry || volatility); int player_on_pool = s->player.x == s->molten_x[i] && s->player.y == s->molten_y[i]; - if (!burned && player_on_pool && (!reentry || s->molten_burn_timer[i] == 0)) { - int dmg = col_mod_molten_pool_damage(s, (ColoMoltenPoolKind)s->molten_kind[i]); - if (reentry) s->molten_burn_timer[i] = COLO_REENTRY_MOLTEN_BURN_INTERVAL - 1; - col_damage_player_from(s, dmg, COLO_JAVELIN_COLOSSUS, COLO_DMG_UNPRAYABLE); - s->last_hit_by_type = s->last_hit_by_type < 0 ? COLO_JAVELIN_COLOSSUS : s->last_hit_by_type; - col_mod_on_player_damaged(s, dmg); + if (!burned && player_on_pool && s->molten_burn_timer[i] == 0) { + int dmg = col_mod_molten_pool_damage(s); + s->molten_burn_timer[i] = + COLO_REENTRY_MOLTEN_BURN_INTERVAL - 1; + int hitpoints_before = s->player.current_hitpoints; + int applied = col_damage_player_from( + s, + dmg, + reentry && !volatility ? COLO_JAVELIN_COLOSSUS : -1, + COLO_DMG_UNPRAYABLE); + if (reentry && volatility) { + s->log.javelin_damage_by_source[ + COLO_JAVELIN_DAMAGE_REENTRY_VOLATILITY_POOL] += + (float)(hitpoints_before - s->player.current_hitpoints); + if (applied > 0) { + s->last_hit_by_type = -1; + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_REENTRY_VOLATILITY_POOL); + } + } else if (reentry) { + s->log.javelin_damage_by_source[COLO_JAVELIN_DAMAGE_REENTRY_POOL] += + (float)(hitpoints_before - s->player.current_hitpoints); + if (applied > 0) { + s->last_hit_by_type = COLO_JAVELIN_COLOSSUS; + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_REENTRY_POOL); + } + } else { + if (applied > 0) { + s->last_hit_by_type = -1; + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_VOLATILITY_POOL); + } + } burned = 1; continue; } - if (reentry && s->molten_burn_timer[i] > 0) + if (s->molten_burn_timer[i] > 0) s->molten_burn_timer[i]--; } } @@ -618,33 +778,50 @@ static int col_volatility_reach_for_tier(int tier) { return tier >= 2 ? 2 : 1; } -static int col_volatility_player_in_blast( - const ColosseumState* s, +static int col_volatility_tile_in_blast( int x, int y, int size, - int tier + int tier, + int player_x, + int player_y ) { int reach = col_volatility_reach_for_tier(tier); if (reach <= 0) return 0; int span = size + 2 * reach; return encounter_entity_footprints_overlap( - x - reach, y - reach, span, s->player.x, s->player.y, 1); + x - reach, y - reach, span, player_x, player_y, 1); +} + +static int col_volatility_player_in_blast( + const ColosseumState* s, + int x, + int y, + int size, + int tier +) { + return col_volatility_tile_in_blast( + x, y, size, tier, s->player.x, s->player.y); } static void col_mod_volatility_on_death(ColosseumState* s, int x, int y, int size) { int tier = col_mod_tier(s, COLO_MOD_VOLATILITY); if (tier == 0) return; if (col_volatility_player_in_blast(s, x, y, size, tier)) { - int vol_dmg = col_scale_incoming_damage(s, COLO_VOLATILITY_MAX_HIT); - encounter_damage_player(&s->player, vol_dmg, &s->tick_scratch.damage_received); - - s->tick_scratch.landed_unprayable_damage += (float)vol_dmg; - col_mod_on_player_damaged(s, vol_dmg); + int applied = col_damage_player_from( + s, COLO_VOLATILITY_MAX_HIT, -1, COLO_DMG_UNPRAYABLE); + if (applied > 0) { + s->last_hit_by_type = -1; + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_VOLATILITY_EXPLOSION); + } } - if (tier >= 3) + if (tier >= 3) { + ColoMoltenPoolLifetime lifetime = col_mod_tier(s, COLO_MOD_REENTRY) >= 2 + ? COLO_POOL_PERMANENT : COLO_POOL_TEMPORARY; col_mod_add_molten_pool( - s, x + size / 2, y + size / 2, COLO_POOL_VOLATILITY, COLO_POOL_TEMPORARY); + s, x + size / 2, y + size / 2, COLO_POOL_VOLATILITY, lifetime); + } } static void col_clamp_bee_swarm_to_arena(ColoNPC* npc) { @@ -694,22 +871,22 @@ static int col_bee_swarm_contacts_player(const ColosseumState* s, const ColoNPC* return encounter_entity_footprints_overlap(npc->x, npc->y, size, s->player.x, s->player.y, 1); } -static void col_bee_swarm_next_tile_toward_player( - const ColosseumState* s, - const ColoNPC* npc, +static void col_bee_swarm_next_tile_toward( + int player_x, + int player_y, + int x, + int y, + int size, int* out_x, int* out_y ) { - if (!s || !npc || !out_x || !out_y) abort(); - int size = col_npc_effective_size(npc); - int x = npc->x; - int y = npc->y; + if (!out_x || !out_y || size <= 0) abort(); int hi_x = x + size - 1; int hi_y = y + size - 1; - if (s->player.x < x) x--; - else if (s->player.x > hi_x) x++; - if (s->player.y < y) y--; - else if (s->player.y > hi_y) y++; + if (player_x < x) x--; + else if (player_x > hi_x) x++; + if (player_y < y) y--; + else if (player_y > hi_y) y++; int max_x = COLO_ARENA_MAX_X - size + 1; int max_y = COLO_ARENA_MAX_Y - size + 1; if (x < COLO_ARENA_MIN_X) x = COLO_ARENA_MIN_X; @@ -721,15 +898,23 @@ static void col_bee_swarm_next_tile_toward_player( } static int col_bee_swarm_contact_damage(ColosseumState* s) { - if (osrs_effect_profile_has(col_live_effects(s), OSRS_ITEM_EFFECT_VENOM_IMMUNE)) - return 0; int span = COLO_BEE_MAX_DAMAGE - COLO_BEE_MIN_DAMAGE + 1; - return COLO_BEE_MIN_DAMAGE + encounter_rand_int(&s->rng_state, span); + int dmg = COLO_BEE_MIN_DAMAGE + encounter_rand_int(&s->rng_state, span); + if (osrs_effect_profile_has(col_live_effects(s), OSRS_ITEM_EFFECT_VENOM_IMMUNE)) + dmg /= 2; + return dmg; } static void col_bee_swarm_step_toward_player(ColosseumState* s, ColoNPC* npc) { int next_x, next_y; - col_bee_swarm_next_tile_toward_player(s, npc, &next_x, &next_y); + col_bee_swarm_next_tile_toward( + s->player.x, + s->player.y, + npc->x, + npc->y, + col_npc_effective_size(npc), + &next_x, + &next_y); npc->x = next_x; npc->y = next_y; npc->target_x = s->player.x; @@ -762,10 +947,12 @@ static void col_mod_tick_bees(ColosseumState* s) { if (col_bee_swarm_contacts_player(s, npc)) { col_mod_apply_bee_poison(s); int dmg = col_bee_swarm_contact_damage(s); - col_damage_player_from(s, dmg, COLO_BEE_SWARM, COLO_DMG_UNPRAYABLE); - if (dmg > 0) { + int applied = col_damage_player_from( + s, dmg, COLO_BEE_SWARM, COLO_DMG_UNPRAYABLE); + if (applied > 0) { s->last_hit_by_type = COLO_BEE_SWARM; - col_mod_on_player_damaged(s, dmg); + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_BEE_CONTACT); } } } @@ -830,12 +1017,14 @@ static void col_mod_apply_solarflare_contact(ColosseumState* s, int tier) { int x, y; col_solarflare_tile(s, p, s->solarflare.step, &x, &y); if (x != s->player.x || y != s->player.y) continue; - int dmg = encounter_rand_int(&s->rng_state, col_mod_solarflare_max_hit(tier) + 1); - dmg = col_scale_incoming_damage(s, dmg); - encounter_damage_player(&s->player, dmg, &s->tick_scratch.damage_received); - - s->tick_scratch.landed_unprayable_damage += (float)dmg; - if (dmg > 0) col_mod_on_player_damaged(s, dmg); + int dmg = + encounter_rand_int(&s->rng_state, col_mod_solarflare_max_hit(tier) + 1); + int applied = + col_damage_player_from(s, dmg, -1, COLO_DMG_UNPRAYABLE); + if (applied > 0) { + s->last_hit_by_type = -1; + col_mod_on_player_damaged(s, applied, COLO_DAMAGE_SOLARFLARE); + } if (tier >= 3) s->player.prayer = PRAYER_NONE; } } @@ -872,7 +1061,8 @@ static void col_mod_tick_solarflare(ColosseumState* s) { static void col_modifiers_tick(ColosseumState* s) { #ifdef COLO_PROFILE_ENABLED - int col_prof_enabled = COLO_PROFILE_ENABLED(); + int col_prof_enabled = + COLO_PROFILE_ENABLED() && !s->profiling_suppressed; double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; #endif col_mod_tick_bees(s); @@ -886,8 +1076,15 @@ static void col_modifiers_tick(ColosseumState* s) { #endif } +static void col_modifiers_on_wave_cleared(ColosseumState* s) { + s->player_venom = 0; + s->player_venom_timer = 0; +} + static void col_modifiers_on_wave_spawn(ColosseumState* s) { s->doom_stacks = 0; + s->last_doom_damage_source = -1; + s->sol_totem_spawn_timer = 0; col_mod_clear_wave_end_pools(s); col_mod_apply_frailty_hp(s); col_mod_sync_bees(s); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc index 2cfc0ae147..3275affa4a 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc @@ -37,6 +37,8 @@ static int col_npc_has_los_to_player(const ColosseumState* s, const ColoNPC* npc static int col_npc_blocked_ignore_player(void* ctx, int x, int y, int size) { ColosseumState* s = (ColosseumState*)ctx; + s->log.npc_blocked_calls += 1.0f; + s->log.npc_blocked_tiles += (float)(size * size); if (col_static_footprint_blocked_lookup(x, y, size)) return 1; int sol_clamp_active = col_sol_clamp_active(s); for (int dx = 0; dx < size; dx++) { @@ -57,7 +59,7 @@ static int col_npc_blocked(void* ctx, int x, int y, int size) { for (int dy = 0; dy < size; dy++) { int gx, gy; if (!col_grid_index(x + dx, y + dy, &gx, &gy)) return 1; - if (s->player_collision_flags[gx][gy]) return 1; + if (gx == s->player_grid_x && gy == s->player_grid_y) return 1; } } return 0; @@ -141,18 +143,6 @@ static int col_npc_path_blocked(void* ctx, int x, int y) { static OSRS_THREAD_LOCAL uint64_t col_warband_bfs_memo_key[COL_WARBAND_BFS_MEMO_SIZE]; static OSRS_THREAD_LOCAL uint8_t col_warband_bfs_memo_result[COL_WARBAND_BFS_MEMO_SIZE]; -#ifdef COLO_BFS_INSTR -static long col_warband_bfs_memo_instr_hits; -static long col_warband_bfs_memo_instr_misses; -static int col_warband_bfs_memo_instr_registered; -static void col_warband_bfs_memo_instr_report(void) { - long total = col_warband_bfs_memo_instr_hits + col_warband_bfs_memo_instr_misses; - fprintf(stderr, "COLO_BFS_INSTR: memo hits=%ld misses=%ld (%.1f%% hit)\n", - col_warband_bfs_memo_instr_hits, col_warband_bfs_memo_instr_misses, - total ? 100.0 * col_warband_bfs_memo_instr_hits / total : 0.0); -} -#endif - static uint64_t col_warband_bfs_memo_pack( const ColosseumState* s, int x, int y, int tx, int ty) { uint64_t key = 1; @@ -211,18 +201,9 @@ static int col_npc_path_step_with_ctx(ColoNpcPathCtx* pc, int* x, int* y, int tx uint64_t memo_key = 0; uint32_t memo_slot = 0; if (memoable) { -#ifdef COLO_BFS_INSTR - if (!col_warband_bfs_memo_instr_registered) { - col_warband_bfs_memo_instr_registered = 1; - atexit(col_warband_bfs_memo_instr_report); - } -#endif memo_key = col_warband_bfs_memo_pack(pc->s, *x, *y, tx, ty); memo_slot = col_warband_bfs_memo_slot(memo_key); if (col_warband_bfs_memo_key[memo_slot] == memo_key) { -#ifdef COLO_BFS_INSTR - col_warband_bfs_memo_instr_hits++; -#endif uint8_t r = col_warband_bfs_memo_result[memo_slot]; int found = r & 1; int next_dx = (int)((r >> 1) & 3) - 1; @@ -232,12 +213,10 @@ static int col_npc_path_step_with_ctx(ColoNpcPathCtx* pc, int* x, int* y, int tx *y += next_dy; return 1; } -#ifdef COLO_BFS_INSTR - col_warband_bfs_memo_instr_misses++; -#endif } #ifdef COLO_PROFILE_ENABLED - int col_prof_enabled = COLO_PROFILE_ENABLED(); + int col_prof_enabled = + COLO_PROFILE_ENABLED() && !pc->s->profiling_suppressed; double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; #endif PathResult pr = pathfind_step_arena(NULL, 0, *x, *y, tx, ty, @@ -322,24 +301,27 @@ static void col_npc_move_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) npc->target_x = tx; npc->target_y = ty; - col_stamp_npc_collision_footprint(s, npc->x, npc->y, size, 0); - ColoNpcMoveCtx move_ctx = { s, idx }; - + /* Both hold paths decide from footprint geometry and the static LOS table, neither of + * which reads npc_collision_flags, so they are checked before the clear. An NPC that was + * never going to move now touches the grid zero times instead of erasing and redrawing + * its own footprint. Measured before this: 27.9 stamp writes per tick against 7.0 blocked + * probes, with most NPCs holding most ticks behind the attack_timer gate. */ int is_ranged = st->attack_range > 1; if (is_ranged) { int dist = encounter_dist_to_npc(s->player.x, s->player.y, npc->x, npc->y, size); if (dist >= 1 && dist <= st->attack_range && col_npc_has_los_to_player(s, npc)) { - col_stamp_npc_collision_footprint(s, npc->x, npc->y, size, 1); npc->moved_this_tick = 0; return; } } if (npc->attack_timer > st->attack_speed) { - col_stamp_npc_collision_footprint(s, npc->x, npc->y, size, 1); npc->moved_this_tick = 0; return; } + col_stamp_npc_collision(s, npc, 0); + ColoNpcMoveCtx move_ctx = { s, idx }; + int moved; if (npc->type == COLO_MINOTAUR && col_mod_active(s, COLO_MOD_RED_FLAG)) { @@ -355,6 +337,6 @@ static void col_npc_move_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) col_npc_move_blocked_ignore_player, &move_ctx, col_npc_move_overlap_hold, &s->rng_state); } - col_stamp_npc_collision_footprint(s, npc->x, npc->y, size, 1); + col_stamp_npc_collision(s, npc, 1); npc->moved_this_tick = moved ? 1 : 0; } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc index 950e59b68c..f85ea2ad9c 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -1,25 +1,72 @@ #line 76 "encounter_colosseum.h" #define COLO_PLAYER_OBS_SIZE 36 -#define COLO_PILLAR_OBS_SIZE (COLO_NUM_PILLARS * 3) -#define COLO_INVENTORY_CELL_OBS_FEATURES OSRS_INVENTORY_CELL_OBS_FEATURES +/* Each cell ships an item code plus the two features the item does not determine; the entity + * encoder rebuilds the other twelve from COLO_ITEM_OBS_TABLE, exactly as it rebuilds the NPC + * type one-hot from a code. Keeping the EXPANDED record identical to the old compact one + * means the inventory branch weights keep their meaning across this recut. */ +#define COLO_INVENTORY_CELL_OBS_FEATURES OSRS_INVENTORY_CELL_OBS_FEATURES_CODED +#define COLO_INVENTORY_CELL_ENCODER_FEATURES OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT #define COLO_INVENTORY_OBS_SIZE \ (COLO_INVENTORY_DISPLAY_SLOTS * COLO_INVENTORY_CELL_OBS_FEATURES) -#define COLO_EQUIPPED_SELF_OBS_SIZE \ - (NUM_GEAR_SLOTS * OSRS_EQUIPPED_SELF_OBS_FEATURES) +#define COLO_EQUIPPED_SELF_OBS_SIZE COLO_EQUIPPED_AGG_OBS_FEATURES static_assert(COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE == COLO_INVENTORY_OBS_CACHE_FLOATS, "inventory obs cache float count must match the inventory+equipped block size"); -#define COLO_FEATURES_PER_NPC 37 +static_assert(COLO_ITEM_OBS_TABLE_ROWS == OSRS_INVENTORY_CELL_OBS_CODE_COUNT, + "regenerate osrs_colosseum_item_obs_table.inc: the cell code space changed"); +static_assert(COLO_ITEM_OBS_TABLE_COLS == COLO_INVENTORY_CELL_ENCODER_FEATURES, + "regenerate osrs_colosseum_item_obs_table.inc: the expanded record width changed"); +static_assert(COLO_ITEM_OBS_TABLE_BASE_PRAYER == COLO_PLAYER_PRAYER_LEVEL, + "regenerate osrs_colosseum_item_obs_table.inc: it bakes prayer_restore at this level"); +static_assert(COLO_ITEM_OBS_TABLE_BASE_LEVEL == MAXED_BASE_ATTACK, + "regenerate osrs_colosseum_item_obs_table.inc: it bakes offensive_boost at this level"); +static_assert(COLO_ITEM_OBS_OVERLAY_EQUIPPED == OSRS_INVENTORY_CELL_COMPACT_EQUIPPED && + COLO_ITEM_OBS_OVERLAY_HP_HEAL == OSRS_INVENTORY_CELL_COMPACT_HP_HEAL, + "the gather adds the observed floats at these expanded slots; the table holds zero there"); +static_assert(COLO_ITEM_OBS_CODE_SCALE == OSRS_INVENTORY_CELL_OBS_CODE_SCALE, + "the gather multiplies the observed code back by this scale before indexing a row"); +#define COLO_NPC_CORE_FEATURES 11 +#define COLO_NPC_TELLS_FEATURES 10 +#define COLO_NPC_VENATOR_FEATURES 1 +#define COLO_NPC_TYPE_CODE_FEATURES 1 +#define COLO_NPC_TELLS_OFFSET (COLO_NPC_TYPE_CODE_FEATURES + COLO_NPC_CORE_FEATURES) +#define COLO_FEATURES_PER_NPC \ + (COLO_NPC_TELLS_OFFSET + COLO_NPC_TELLS_FEATURES + COLO_NPC_VENATOR_FEATURES) #define COLO_NPC_OBS_SIZE (COLO_OBS_NPCS * COLO_FEATURES_PER_NPC) #define COLO_EXPECTED_DPT_NORM 15.0f -#define COLO_MODIFIER_FLAGS_OBS_SIZE (2 * COLO_NUM_MODIFIERS + 1 + COLO_MODIFIER_DRAFT_OPTIONS) -#define COLO_MODIFIER_HAZARD_OBS_SIZE 38 +#define COLO_MODIFIER_FLAGS_OBS_SIZE (COLO_NUM_REAL_MODIFIERS + 1 + COLO_MODIFIER_DRAFT_OPTIONS) +#define COLO_MODIFIER_HAZARD_OBS_SIZE 42 #define COLO_MODIFIER_OBS_SIZE (COLO_MODIFIER_FLAGS_OBS_SIZE + COLO_MODIFIER_HAZARD_OBS_SIZE) #define COLO_WAVE_OBS_SIZE 4 -#define COLO_BOSS_OBS_SIZE 51 + +typedef enum { + COLO_SOL_PARRY_CUE_INACTIVE = 0, + COLO_SOL_PARRY_CUE_LEAD_TIME, + COLO_SOL_PARRY_CUE_OVERHEAD_OFF, + COLO_SOL_PARRY_CUE_FLICK_MELEE, + COLO_SOL_PARRY_CUE_POISONED, + COLO_SOL_PARRY_CUE_COUNT, +} ColoSolParryCue; + +typedef enum { + COLO_SOL_HAZARD_AOE = 0, + COLO_SOL_HAZARD_CRYSTAL_LASER, + COLO_SOL_HAZARD_MOLTEN_SAND, + COLO_SOL_HAZARD_SOURCE_COUNT, +} ColoSolHazardSource; + +#define COLO_SOL_PARRY_LAND_TICKS_MAX 4 +#define COLO_BOSS_OBS_PARRY_CUE_OFFSET 19 +#define COLO_BOSS_OBS_PARRY_TICKS_OFFSET \ + (COLO_BOSS_OBS_PARRY_CUE_OFFSET + COLO_SOL_PARRY_CUE_COUNT) +#define COLO_BOSS_OBS_PARRY_DAMAGE_OFFSET \ + (COLO_BOSS_OBS_PARRY_TICKS_OFFSET + COLO_SOL_PARRY_LAND_TICKS_MAX) +#define COLO_BOSS_OBS_PREMOVE_HAZARD_OFFSET 37 +#define COLO_BOSS_OBS_SIZE \ + (COLO_BOSS_OBS_PREMOVE_HAZARD_OFFSET + COLO_SOL_HAZARD_SOURCE_COUNT) #define COLO_OBS_PENDING_HITS 16 #define COLO_FEATURES_PER_HIT 7 @@ -29,55 +76,33 @@ static_assert(COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE == #define COLO_THRALL_DC_OBS_SIZE 6 -#define COLO_CELL_WEAPON_DPT_OBS_SIZE OSRS_INVENTORY_SIZE -#define COLO_CELL_SPEC_OBS_SIZE OSRS_INVENTORY_SIZE -#define COLO_WIELDED_DPT_OBS_SIZE 2 -#define COLO_WEAPON_CHOICE_OBS_SIZE (COLO_CELL_WEAPON_DPT_OBS_SIZE + \ - COLO_CELL_SPEC_OBS_SIZE + COLO_WIELDED_DPT_OBS_SIZE) -static_assert(COLO_WEAPON_CHOICE_OBS_SIZE == COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS, - "weapon-choice obs cache float count must match the block size"); - #define COLO_SPAWN_PHASE_OBS_SIZE 2 #define COLO_ANCHOR_BLOCKED_OBS_SIZE COLO_NUM_SPAWN_ANCHORS #define COLO_SPAWN_OBS_SIZE (COLO_SPAWN_PHASE_OBS_SIZE + COLO_ANCHOR_BLOCKED_OBS_SIZE) -#define COLO_THREAT_FIELD_RADIUS 8 -#define COLO_THREAT_FIELD_DIM (2 * COLO_THREAT_FIELD_RADIUS + 1) -#define COLO_THREAT_FIELD_TILES (COLO_THREAT_FIELD_DIM * COLO_THREAT_FIELD_DIM) -#define COLO_THREAT_FIELD_OBS_SIZE (2 * COLO_THREAT_FIELD_TILES) -static_assert(COLO_THREAT_FIELD_OBS_SIZE == COLO_THREAT_FIELD_OBS_CACHE_FLOATS, - "threat-field obs cache float count must match the block size"); - -#define COLO_NUM_OBS (COLO_PLAYER_OBS_SIZE + COLO_PILLAR_OBS_SIZE + \ +#define COLO_NUM_OBS (COLO_PLAYER_OBS_SIZE + \ COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE + COLO_NPC_OBS_SIZE + \ COLO_MODIFIER_OBS_SIZE + COLO_WAVE_OBS_SIZE + COLO_BOSS_OBS_SIZE + \ - COLO_PENDING_HIT_OBS_SIZE + COLO_STEP_OUT_FORECAST_OBS_SIZE + \ + COLO_PENDING_HIT_OBS_SIZE + \ COLO_THREAT_LOS_OBS_SIZE + COLO_THRALL_DC_OBS_SIZE + \ - COLO_WEAPON_CHOICE_OBS_SIZE + COLO_SPAWN_OBS_SIZE + \ - COLO_THREAT_FIELD_OBS_SIZE) + COLO_SPAWN_OBS_SIZE) #define COLO_OBS_AFTER_PLAYER COLO_PLAYER_OBS_SIZE -#define COLO_OBS_AFTER_PILLARS (COLO_OBS_AFTER_PLAYER + COLO_PILLAR_OBS_SIZE) -#define COLO_OBS_AFTER_INVENTORY (COLO_OBS_AFTER_PILLARS + COLO_INVENTORY_OBS_SIZE) +#define COLO_OBS_AFTER_INVENTORY (COLO_OBS_AFTER_PLAYER + COLO_INVENTORY_OBS_SIZE) #define COLO_OBS_AFTER_EQUIPPED_SELF \ (COLO_OBS_AFTER_INVENTORY + COLO_EQUIPPED_SELF_OBS_SIZE) #define COLO_OBS_AFTER_NPCS (COLO_OBS_AFTER_EQUIPPED_SELF + COLO_NPC_OBS_SIZE) #define COLO_OBS_AFTER_MODS (COLO_OBS_AFTER_NPCS + COLO_MODIFIER_OBS_SIZE) #define COLO_OBS_AFTER_WAVE (COLO_OBS_AFTER_MODS + COLO_WAVE_OBS_SIZE) #define COLO_OBS_AFTER_BOSS (COLO_OBS_AFTER_WAVE + COLO_BOSS_OBS_SIZE) -#define COLO_OBS_AFTER_PENDING_HITS (COLO_OBS_AFTER_BOSS + COLO_PENDING_HIT_OBS_SIZE) -#define COLO_OBS_AFTER_STEP_OUT_FORECAST \ - (COLO_OBS_AFTER_PENDING_HITS + COLO_STEP_OUT_FORECAST_OBS_SIZE) +#define COLO_OBS_AFTER_PENDING_HITS \ + (COLO_OBS_AFTER_BOSS + COLO_PENDING_HIT_OBS_SIZE) #define COLO_OBS_AFTER_THREAT_LOS \ - (COLO_OBS_AFTER_STEP_OUT_FORECAST + COLO_THREAT_LOS_OBS_SIZE) + (COLO_OBS_AFTER_PENDING_HITS + COLO_THREAT_LOS_OBS_SIZE) #define COLO_OBS_AFTER_THRALL_DC \ (COLO_OBS_AFTER_THREAT_LOS + COLO_THRALL_DC_OBS_SIZE) -#define COLO_OBS_AFTER_WEAPON_CHOICE \ - (COLO_OBS_AFTER_THRALL_DC + COLO_WEAPON_CHOICE_OBS_SIZE) #define COLO_OBS_AFTER_SPAWN \ - (COLO_OBS_AFTER_WEAPON_CHOICE + COLO_SPAWN_OBS_SIZE) -#define COLO_OBS_AFTER_THREAT_FIELD \ - (COLO_OBS_AFTER_SPAWN + COLO_THREAT_FIELD_OBS_SIZE) + (COLO_OBS_AFTER_THRALL_DC + COLO_SPAWN_OBS_SIZE) static inline float col_clamp01(float v) { if (v < 0.0f) return 0.0f; @@ -212,29 +237,6 @@ static inline void col_write_nearest_hazard_xy_value( s, nearest->found, nearest->x, nearest->y, value, obs, idx); } -static inline void col_write_nearest_hazard_xy_values( - const ColosseumState* s, - const ColoNearestHazardTile* nearest, - float value0, - float value1, - float* obs, - int* idx -) { - int i = *idx; - if (nearest->found) { - obs[i++] = col_obs_rel_x(nearest->x, s->player.x); - obs[i++] = col_obs_rel_y(nearest->y, s->player.y); - obs[i++] = value0; - obs[i++] = value1; - } else { - obs[i++] = 0.0f; - obs[i++] = 0.0f; - obs[i++] = 0.0f; - obs[i++] = 0.0f; - } - *idx = i; -} - static void col_refresh_current_obs_slots_ctx(ColosseumState* s, const ColosseumContext* ctx) { (void)ctx; for (int i = 0; i < COLO_OBS_NPCS; i++) s->current_obs_slots[i] = -1; @@ -285,31 +287,6 @@ static void col_write_style_onehot_melee_ranged_magic(AttackStyle style, float* *idx = i; } -static int col_pending_hit_prayer_ticks(const EncounterPendingHit* hit) { - return hit->prayer_check_delay > 0 ? hit->prayer_check_delay : hit->ticks_remaining; -} - -static ColoNpcNextPrayerObs col_source_matched_pending_prayer( - const ColosseumState* s, - int npc_slot -) { - ColoNpcNextPrayerObs best = col_npc_next_prayer_none(); - for (int h = 0; h < s->player_pending_hits.count && h < ENCOUNTER_MAX_PENDING_HITS; h++) { - const EncounterPendingHit* hit = &s->player_pending_hits.hits[h]; - if (!hit->active) continue; - if (!hit->check_prayer) continue; - if (hit->source_npc_slot != npc_slot) continue; - if (!col_prayer_attack_style((AttackStyle)hit->attack_style)) continue; - int ticks = col_pending_hit_prayer_ticks(hit); - if (!best.active || ticks < best.ticks) { - best.active = 1; - best.style = (AttackStyle)hit->attack_style; - best.ticks = ticks; - } - } - return best; -} - static int col_manticore_next_orb_ticks(const ColoNPC* npc) { const ColoManticoreState* mc = &npc->type_state.manticore; if (mc->cycle_step > 0) return 1; @@ -390,8 +367,7 @@ static ColoNpcNextPrayerObs col_npc_next_prayer_obs( const ColoNPC* npc, int npc_slot ) { - ColoNpcNextPrayerObs pending = col_source_matched_pending_prayer(s, npc_slot); - if (pending.active) return pending; + (void)npc_slot; ColoNpcNextPrayerObs manticore = col_visible_manticore_next_prayer(npc); if (manticore.active) return manticore; return col_deterministic_npc_next_prayer(s, npc, &COLO_NPC_STATS[npc->type]); @@ -449,7 +425,14 @@ static void col_write_npc_tells(const ColosseumState* s, const ColoNPC* npc, flo const ColoBeeSwarm* bee = col_obs_bee_state_for_npc_slot(s, npc_slot); int next_x, next_y; int size = col_npc_effective_size(npc); - col_bee_swarm_next_tile_toward_player(s, npc, &next_x, &next_y); + col_bee_swarm_next_tile_toward( + s->player.x, + s->player.y, + npc->x, + npc->y, + size, + &next_x, + &next_y); tells[0] = bee ? col_clamp01((float)bee->move_timer / (float)COLO_BEE_MOVE_INTERVAL) : 0.0f; @@ -466,13 +449,32 @@ static void col_write_npc_tells(const ColosseumState* s, const ColoNPC* npc, flo tells[0] = (dist > 1) ? 1.0f : 0.0f; break; } + case COLO_HEALING_TOTEM: { + const ColoTotem* tot = col_totem_for_npc_slot(s, npc_slot); + if (tot) { + const ColoNPC* owner = &s->npcs[tot->owner_slot]; + tells[0] = tot->projectile_timer > 0 + ? 0.0f + : col_clamp01( + (float)tot->heal_timer / (float)COLO_TOTEM_HEAL_INTERVAL); + tells[1] = col_clamp01( + (float)tot->projectile_timer / (float)COLO_TOTEM_PROJECTILE_TICKS); + tells[2] = owner->max_hp > 0 + ? col_clamp01( + (float)col_totem_heal_amount(owner) / (float)owner->max_hp) + : 0.0f; + tells[3] = col_obs_rel_x(owner->x, s->player.x); + tells[4] = col_obs_rel_y(owner->y, s->player.y); + } + break; + } case COLO_JAGUAR_WARRIOR: tells[0] = 1.0f; break; default: break; } - for (int t = 0; t < 10; t++) obs[i++] = tells[t]; + for (int t = 0; t < COLO_NPC_TELLS_FEATURES; t++) obs[i++] = tells[t]; *idx = i; } @@ -485,22 +487,23 @@ static void col_write_npc_slot( int* idx ) { int i = *idx; + int slot_base = i; int px = s->player.x, py = s->player.y; const ColoNpcStats* ns = &COLO_NPC_STATS[npc->type]; int size = col_npc_effective_size(npc); - for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) - obs[i++] = (npc->type == t) ? 1.0f : 0.0f; + /* type+1, so 0 stays "empty slot" from the memset. The entity encoder expands this + * back to the same one-hot on the GPU, so the network sees exactly what it did before + * while the observation carries 1 float per slot instead of COLO_NUM_NPC_TYPES. */ + obs[i++] = (float)(npc->type + 1); obs[i++] = col_obs_rel_x(npc->x, px); obs[i++] = col_obs_rel_y(npc->y, py); obs[i++] = npc->max_hp > 0 ? col_clamp01((float)npc->hp / (float)npc->max_hp) : 0.0f; - *idx = i; - col_write_style_onehot_melee_ranged_magic(npc->attack_style, obs, idx); - i = *idx; - obs[i++] = col_clamp01((float)npc->attack_timer / 8.0f); + obs[i++] = npc->type == COLO_SOL_HEREDIT + ? 0.0f : col_clamp01((float)npc->attack_timer / 8.0f); int dist = encounter_dist_to_npc(px, py, npc->x, npc->y, size); - int range = ns->attack_range > 0 ? ns->attack_range : 1; - obs[i++] = (dist >= 1 && dist <= range) ? 1.0f : 0.0f; + obs[i++] = (ns->attack_range > 0 && dist >= 1 && dist <= ns->attack_range) + ? 1.0f : 0.0f; ColoNpcNextPrayerObs next_prayer = col_npc_next_prayer_obs(s, npc, npc_slot); obs[i++] = next_prayer.active ? 1.0f : 0.0f; @@ -510,78 +513,144 @@ static void col_write_npc_slot( i = *idx; obs[i++] = next_prayer.active ? col_clamp01((float)next_prayer.ticks / 10.0f) : 0.0f; obs[i++] = col_npc_has_los_to_player(s, npc) ? 1.0f : 0.0f; + assert(i - slot_base == COLO_NPC_TELLS_OFFSET); *idx = i; col_write_npc_tells(s, npc, obs, idx); i = *idx; int extra_bounces = col_venator_extra_bounce_if_shot(s, venator_targets, npc_slot); obs[i++] = (float)extra_bounces / (float)(OSRS_VENATOR_MAX_CHAIN_HITS - 1); + assert(i - slot_base == COLO_FEATURES_PER_NPC); *idx = i; } -static void col_write_nearest_armed_line(const ColosseumState* s, float* obs, int* idx) { - ColoNearestHazardTile nearest = col_nearest_hazard_tile_none(); +static int col_sol_premove_aoe_hit(const ColosseumState* s) { + return s->sol.aoe_attack != COLO_SOL_AOE_NONE && + s->sol.aoe_age + 1 == COLO_SOL_AOE_DAMAGE_AGE && + col_sol_aoe_tile_is_hazard(&s->sol, s->player.x, s->player.y); +} + +static int col_sol_premove_laser_hit(const ColosseumState* s) { for (int k = 0; k < s->sol.crystal_count; k++) { - const ColoSolCrystal* c = &s->sol.crystals[k]; - if (!c->active || c->firing_freeze < COLO_SOL_LASER_DAMAGE_FREEZE) continue; - if (!col_sol_crystal_in_line(s, c)) continue; - col_nearest_hazard_tile_consider( - &nearest, k, c->x, c->y, s->player.x, s->player.y); + const ColoSolCrystal* crystal = &s->sol.crystals[k]; + if (crystal->active && + crystal->firing_freeze == COLO_SOL_LASER_DAMAGE_FREEZE && + col_sol_crystal_hits_tile(crystal, s->player.x, s->player.y)) + return 1; } - float ticks_frac = nearest.found - ? col_clamp01((float)(s->sol.crystals[nearest.index].firing_freeze - - COLO_SOL_LASER_DAMAGE_FREEZE) / - (float)(COLO_SOL_LASER_FREEZE - COLO_SOL_LASER_DAMAGE_FREEZE)) - : 0.0f; - col_write_nearest_hazard_xy_values(s, &nearest, ticks_frac, 1.0f, obs, idx); + return 0; } -static void col_write_nearest_molten(const ColosseumState* s, float* obs, int* idx) { - ColoNearestHazardTile nearest = col_nearest_hazard_tile_none(); - for (int k = 0; k < s->sol.hazard_tile_count; k++) { - col_nearest_hazard_tile_consider( - &nearest, k, s->sol.hazard_tile_x[k], s->sol.hazard_tile_y[k], - s->player.x, s->player.y); +static int col_sol_premove_molten_hit(const ColosseumState* s) { + if (col_sol_molten_sand_at(&s->sol, s->player.x, s->player.y)) return 1; + for (int k = 0; k < COLO_SOL_BEAM_MAX; k++) { + const ColoSolBeam* beam = &s->sol.beams[k]; + if (beam->active && beam->ticks_to_pool <= 1 && + beam->x == s->player.x && beam->y == s->player.y) + return 1; } - col_write_nearest_hazard_xy_value(s, &nearest, 1.0f, obs, idx); + return 0; } -static int col_player_in_aoe_danger(const ColosseumState* s) { - const SolHereditState* sol = &s->sol; - if (sol->aoe_attack == COLO_SOL_AOE_NONE) return 0; - return col_sol_aoe_tile_is_hazard(sol, s->player.x, s->player.y); +static int col_premove_molten_forecast_needed( + const ColosseumState* s +) { + if (s->molten_count > 0) return 1; + if (col_mod_tier(s, COLO_MOD_REENTRY) == 0) return 0; + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (!col_npc_is_live_target(npc) || + npc->type != COLO_JAVELIN_COLOSSUS) { + continue; + } + const ColoJavelinState* jv = &npc->type_state.javelin; + if (jv->skyfall_pending && jv->skyfall_timer <= 1) return 1; + } + return 0; } -static void col_write_nearest_crystal(const ColosseumState* s, float* obs, int* idx) { - ColoNearestHazardTile nearest = col_nearest_hazard_tile_none(); - for (int k = 0; k < s->sol.crystal_count; k++) { - const ColoSolCrystal* ck = &s->sol.crystals[k]; - if (!ck->active) continue; - col_nearest_hazard_tile_consider( - &nearest, k, ck->x, ck->y, s->player.x, s->player.y); +static int col_build_post_premove_molten_forecast( + const ColosseumState* s, + ColosseumState* forecast +) { + *forecast = *s; + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (!col_npc_is_live_target(npc) || + npc->type != COLO_JAVELIN_COLOSSUS) { + continue; + } + const ColoJavelinState* jv = &npc->type_state.javelin; + if (!jv->skyfall_pending || jv->skyfall_timer > 1) continue; + col_mod_reentry_on_skyfall( + forecast, jv->skyfall_tile_x, jv->skyfall_tile_y); + } + + int burned = 0; + for (int pool = 0; pool < forecast->molten_count; pool++) { + int player_on_pool = + forecast->molten_x[pool] == forecast->player.x && + forecast->molten_y[pool] == forecast->player.y; + if (!burned && + player_on_pool && + forecast->molten_burn_timer[pool] == 0) { + forecast->molten_burn_timer[pool] = + COLO_REENTRY_MOLTEN_BURN_INTERVAL - 1; + burned = 1; + continue; + } + if (forecast->molten_burn_timer[pool] > 0) + forecast->molten_burn_timer[pool]--; } - const ColoSolCrystal* c = nearest.found ? &s->sol.crystals[nearest.index] : NULL; - col_write_nearest_hazard_xy_values( - s, - &nearest, - c && c->firing_freeze > 0 ? 1.0f : 0.0f, - c ? col_clamp01((float)c->firing_freeze / (float)COLO_SOL_LASER_FREEZE) : 0.0f, - obs, - idx); + return burned; } -static void col_write_nearest_beam(const ColosseumState* s, float* obs, int* idx) { - ColoNearestHazardTile nearest = col_nearest_hazard_tile_none(); - for (int k = 0; k < COLO_SOL_BEAM_MAX; k++) { - if (!s->sol.beams[k].active) continue; - col_nearest_hazard_tile_consider( - &nearest, k, s->sol.beams[k].x, s->sol.beams[k].y, - s->player.x, s->player.y); +typedef struct { + ColoSolParryCue cue; + int ticks_to_land; + int damage; +} ColoSolParryTell; + +static ColoSolParryTell col_sol_parry_tell(const ColosseumState* s) { + const SolHereditState* sol = &s->sol; + int hit = -1; + for (int h = 0; h < COLO_SOL_PARRY_HITS; h++) { + if (sol->parry_land_in[h] <= 0) continue; + if (hit < 0 || sol->parry_land_in[h] < sol->parry_land_in[hit]) hit = h; + } + if (hit < 0) { + return (ColoSolParryTell){ + .cue = COLO_SOL_PARRY_CUE_INACTIVE, + .ticks_to_land = 0, + .damage = 0, + }; + } + + int ticks_to_land = sol->parry_land_in[hit]; + int landing_tick = s->tick + ticks_to_land; + int lookback = COLO_SOL_PARRY_LOOKBACK[hit]; + int poisoned = 0; + for (int k = 1; k <= lookback; k++) { + int history_tick = landing_tick - k; + if (history_tick < 0 || history_tick > s->tick) continue; + if (sol->overhead_history[history_tick % COLO_SOL_PRAYER_HISTORY]) { + poisoned = 1; + break; + } } - float ticks_frac = nearest.found - ? col_clamp01((float)s->sol.beams[nearest.index].ticks_to_pool / - (float)COLO_SOL_BEAM_TO_POOL_TICKS) - : 0.0f; - col_write_nearest_hazard_xy_values(s, &nearest, ticks_frac, 1.0f, obs, idx); + + ColoSolParryCue cue = COLO_SOL_PARRY_CUE_LEAD_TIME; + if (poisoned) + cue = COLO_SOL_PARRY_CUE_POISONED; + else if (ticks_to_land == 1) + cue = COLO_SOL_PARRY_CUE_FLICK_MELEE; + else if (ticks_to_land <= lookback + 1) + cue = COLO_SOL_PARRY_CUE_OVERHEAD_OFF; + + return (ColoSolParryTell){ + .cue = cue, + .ticks_to_land = ticks_to_land, + .damage = sol->parry_damage[hit], + }; } static void col_write_boss_obs(const ColosseumState* s, float* obs, int* idx) { @@ -599,7 +668,6 @@ static void col_write_boss_obs(const ColosseumState* s, float* obs, int* idx) { obs[i++] = (sol->aoe_attack == COLO_SOL_AOE_SHIELD1) ? 1.0f : 0.0f; obs[i++] = (sol->aoe_attack == COLO_SOL_AOE_SHIELD2) ? 1.0f : 0.0f; obs[i++] = col_clamp01((float)sol->aoe_age / (float)COLO_SOL_AOE_LIFETIME); - obs[i++] = col_player_in_aoe_danger(s) ? 1.0f : 0.0f; if (sol->boss_idx >= 0) { const ColoNPC* boss = &s->npcs[sol->boss_idx]; int bsz = boss->size > 0 ? boss->size : 5; @@ -609,49 +677,33 @@ static void col_write_boss_obs(const ColosseumState* s, float* obs, int* idx) { obs[i++] = 0.0f; obs[i++] = 0.0f; } - if (sol->aoe_attack == COLO_SOL_AOE_NONE) { - obs[i++] = 0.0f; - obs[i++] = 0.0f; - } else { - int adx, ady; - col_sol_aoe_effective_dir(sol, px, py, &adx, &ady); - obs[i++] = (float)adx; - obs[i++] = (float)ady; - } - obs[i++] = col_clamp01((float)sol->attack_delay / 12.0f); obs[i++] = col_clamp01((float)sol->immobile_ticks / 10.0f); obs[i++] = sol->special_cooldown <= 0 ? 1.0f : 0.0f; obs[i++] = sol->force_spear ? 1.0f : 0.0f; - int next_land = 0, next_dmg = 0; - for (int h = 0; h < COLO_SOL_PARRY_HITS; h++) { - if (sol->parry_land_in[h] <= 0) continue; - if (next_land == 0 || sol->parry_land_in[h] < next_land) { - next_land = sol->parry_land_in[h]; - next_dmg = sol->parry_damage[h]; - } - } - obs[i++] = sol->parry_hits_left > 0 ? 1.0f : 0.0f; - obs[i++] = col_clamp01((float)next_land / 10.0f); - obs[i++] = col_clamp01((float)next_dmg / 45.0f); - obs[i++] = col_clamp01((float)sol->parry_hits_left / (float)COLO_SOL_PARRY_HITS); - obs[i++] = sol->phase >= 3 ? 1.0f : 0.0f; + ColoSolParryTell parry = col_sol_parry_tell(s); + assert(parry.ticks_to_land >= 0 && + parry.ticks_to_land <= COLO_SOL_PARRY_LAND_TICKS_MAX); + for (int cue = 0; cue < COLO_SOL_PARRY_CUE_COUNT; cue++) + obs[i++] = (int)parry.cue == cue ? 1.0f : 0.0f; + for (int tick = 1; tick <= COLO_SOL_PARRY_LAND_TICKS_MAX; tick++) + obs[i++] = parry.ticks_to_land == tick ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)parry.damage / 45.0f); obs[i++] = sol->grapple_active ? 1.0f : 0.0f; for (int g = 0; g < COLO_NUM_GRAPPLE_SLOTS; g++) obs[i++] = (sol->grapple_active && sol->grapple_body_slot == g) ? 1.0f : 0.0f; obs[i++] = col_clamp01((float)sol->grapple_timer / (float)COLO_SOL_GRAPPLE_WINDOW); obs[i++] = sol->next_attack_guaranteed_max ? 1.0f : 0.0f; - - obs[i++] = col_clamp01((float)sol->crystal_count / (float)COLO_SOL_MAX_CRYSTALS); + if (col_sol_clamp_active(s)) { + obs[i++] = col_sol_premove_aoe_hit(s) ? 1.0f : 0.0f; + obs[i++] = col_sol_premove_laser_hit(s) ? 1.0f : 0.0f; + obs[i++] = col_sol_premove_molten_hit(s) ? 1.0f : 0.0f; + } else { + i += COLO_SOL_HAZARD_SOURCE_COUNT; + } *idx = i; - col_write_nearest_crystal(s, obs, idx); - col_write_nearest_armed_line(s, obs, idx); - - col_write_nearest_molten(s, obs, idx); - - col_write_nearest_beam(s, obs, idx); } static inline int col_pending_hit_ignores_prayer(const EncounterPendingHit* ph) { @@ -679,28 +731,6 @@ static int col_collect_obs_pending_hits(const ColosseumState* s, int* out) { #define COLO_MODIFIER_MOLTEN_OBS_COUNT 4 -typedef struct { - int found; - int npc_slot; - int x; - int y; - int size; - int ticks; - int distance; -} ColoVolatilityBlastPreview; - -static inline ColoVolatilityBlastPreview col_volatility_blast_preview_none(void) { - return (ColoVolatilityBlastPreview){ - .found = 0, - .npc_slot = -1, - .x = 0, - .y = 0, - .size = 0, - .ticks = 0, - .distance = 1 << 30, - }; -} - static int col_pending_hit_will_damage_pre_move( const EncounterPendingHit* hit, OverheadPrayer prayer @@ -780,49 +810,6 @@ static void col_write_nearest_modifier_solarflare( *idx = i; } -static ColoVolatilityBlastPreview col_soonest_queued_volatility_blast( - const ColosseumState* s -) { - ColoVolatilityBlastPreview best = col_volatility_blast_preview_none(); - for (int n = 0; n < COLO_MAX_NPCS; n++) { - const ColoNPC* npc = &s->npcs[n]; - if (!col_npc_is_live_enemy(npc)) continue; - for (int h = 0; h < npc->pending_hits.count; h++) { - const EncounterPendingHit* hit = &npc->pending_hits.hits[h]; - if (!hit->active) continue; - int ticks = hit->ticks_remaining < 0 ? 0 : hit->ticks_remaining; - int damage = 0; - for (int j = 0; j < npc->pending_hits.count; j++) { - const EncounterPendingHit* other = &npc->pending_hits.hits[j]; - if (!other->active) continue; - int other_ticks = other->ticks_remaining < 0 ? 0 : other->ticks_remaining; - if (other_ticks <= ticks) damage += other->damage; - } - if (damage < npc->hp) continue; - int size = col_npc_effective_size(npc); - int center_x = npc->x + size / 2; - int center_y = npc->y + size / 2; - int dx = abs(center_x - s->player.x); - int dy = abs(center_y - s->player.y); - int distance = dx > dy ? dx : dy; - if (best.found && - (ticks > best.ticks || - (ticks == best.ticks && distance >= best.distance))) - continue; - best = (ColoVolatilityBlastPreview){ - .found = 1, - .npc_slot = n, - .x = npc->x, - .y = npc->y, - .size = size, - .ticks = ticks, - .distance = distance, - }; - } - } - return best; -} - static void col_write_volatility_blast_previews( const ColosseumState* s, float* obs, @@ -862,12 +849,11 @@ static void col_write_volatility_blast_previews( *idx = i; } -static int col_count_pre_move_doom_instances(const ColosseumState* s) { +static int col_count_pre_move_unavoidable_damage_instances( + const ColosseumState* s, + int premove_molten_burn +) { int count = 0; - for (int h = 0; h < s->player_pending_hits.count; h++) { - const EncounterPendingHit* hit = &s->player_pending_hits.hits[h]; - if (col_pending_hit_will_damage_pre_move(hit, s->player.prayer)) count++; - } for (int n = 0; n < COLO_MAX_NPCS; n++) { const ColoNPC* npc = &s->npcs[n]; if (!col_npc_is_live_target(npc) || npc->type != COLO_JAVELIN_COLOSSUS) @@ -887,8 +873,15 @@ static int col_count_pre_move_doom_instances(const ColosseumState* s) { int contact = 0; if (bee->move_timer <= 1) { int next_x, next_y; - col_bee_swarm_next_tile_toward_player(s, npc, &next_x, &next_y); int size = col_npc_effective_size(npc); + col_bee_swarm_next_tile_toward( + s->player.x, + s->player.y, + npc->x, + npc->y, + size, + &next_x, + &next_y); contact = encounter_entity_footprints_overlap( next_x, next_y, size, s->player.x, s->player.y, 1); } else { @@ -909,13 +902,9 @@ static int col_count_pre_move_doom_instances(const ColosseumState* s) { if (x == s->player.x && y == s->player.y) count++; } } - for (int m = 0; m < s->molten_count; m++) { - if (s->molten_x[m] == s->player.x && s->molten_y[m] == s->player.y) { - count++; - break; - } - } + count += premove_molten_burn; if (s->player_venom > 0 && s->player_venom_timer <= 1) count++; + if (s->player_poison > 0 && s->player_poison_timer <= 1) count++; int volatility_tier = col_mod_tier(s, COLO_MOD_VOLATILITY); if (volatility_tier > 0) { ColoVolatilityBlastPreview queued = col_soonest_queued_volatility_blast(s); @@ -924,24 +913,68 @@ static int col_count_pre_move_doom_instances(const ColosseumState* s) { s, queued.x, queued.y, queued.size, volatility_tier)) count++; } + if (col_sol_clamp_active(s)) { + count += col_sol_premove_aoe_hit(s); + count += col_sol_premove_laser_hit(s); + count += col_sol_premove_molten_hit(s); + if (s->sol.grapple_active && + !s->sol.grapple_resolved && + s->sol.grapple_timer <= 1) { + count++; + } + } return count; } -static void col_write_modifier_hazards(const ColosseumState* s, float* obs, int* idx) { +static int col_count_pre_move_doom_instances( + const ColosseumState* s, + int premove_molten_burn +) { + int count = col_count_pre_move_unavoidable_damage_instances( + s, premove_molten_burn); + for (int h = 0; h < s->player_pending_hits.count; h++) { + const EncounterPendingHit* hit = &s->player_pending_hits.hits[h]; + if (col_pending_hit_will_damage_pre_move(hit, s->player.prayer)) count++; + } + for (int hit = 0; hit < COLO_SOL_PARRY_HITS; hit++) { + if (s->sol.parry_land_in[hit] != 1 || + s->sol.parry_damage[hit] <= 0) { + continue; + } + int early = + col_sol_overhead_on_in_window(s, COLO_SOL_PARRY_LOOKBACK[hit]); + int blocked = !early && s->player.prayer == PRAYER_PROTECT_MELEE; + if (!blocked) count++; + } + return count; +} + +static void col_write_modifier_hazards( + const ColosseumState* s, + int premove_molten_burn, + float* obs, + int* idx +) { int i = *idx; int doom_tier = col_mod_tier(s, COLO_MOD_DOOM); int doom_cap = doom_tier > 0 ? COLO_DOOM_CAP[doom_tier] : 0; - int doom_pending = doom_cap > 0 ? col_count_pre_move_doom_instances(s) : 0; + int doom_pending = doom_cap > 0 + ? col_count_pre_move_doom_instances(s, premove_molten_burn) + : 0; obs[i++] = doom_tier > 0 ? 1.0f : 0.0f; obs[i++] = doom_cap > 0 ? col_clamp01((float)s->doom_stacks / (float)doom_cap) : 0.0f; obs[i++] = doom_cap > 0 && s->doom_stacks + 1 >= doom_cap ? 1.0f : 0.0f; obs[i++] = doom_cap > 0 ? col_clamp01((float)doom_pending / (float)doom_cap) : 0.0f; - obs[i++] = s->player_venom > 0 ? 1.0f : 0.0f; obs[i++] = col_clamp01((float)s->player_venom / (float)COLO_VENOM_CAP); obs[i++] = s->player_venom > 0 ? col_clamp01((float)s->player_venom_timer / (float)COLO_VENOM_INTERVAL) : 0.0f; + obs[i++] = col_clamp01( + (float)s->player_poison / (float)COLO_POISON_BEE_CONTACT_SEVERITY); + obs[i++] = s->player_poison > 0 + ? col_clamp01((float)s->player_poison_timer / (float)COLO_POISON_INTERVAL) + : 0.0f; ColoNearestHazardTile nearest_bee = col_nearest_hazard_tile_none(); for (int b = 0; b < COLO_MAX_BEE_SWARMS; b++) { @@ -959,6 +992,16 @@ static void col_write_modifier_hazards(const ColosseumState* s, float* obs, int* col_write_nearest_modifier_solarflare(s, obs, idx); col_write_top_modifier_molten(s, obs, idx); col_write_volatility_blast_previews(s, obs, idx); + i = *idx; + obs[i++] = + col_count_pre_move_unavoidable_damage_instances( + s, premove_molten_burn) > 0 ? 1.0f : 0.0f; + + obs[i++] = col_clamp01( + (float)s->sol_totem_spawn_timer / (float)COLO_TOTEM_SOL_EXTRA_INTERVAL); + obs[i++] = col_clamp01( + (float)col_live_totem_count(s) / (float)COLO_TOTEM_OBS_COUNT_NORM); + *idx = i; } static void col_write_threat_los_counts(const ColosseumState* s, float* obs, int* idx) { @@ -971,110 +1014,6 @@ static void col_write_threat_los_counts(const ColosseumState* s, float* obs, int *idx = i; } -static uint64_t col_threat_field_obs_signature(const ColosseumState* s) { - uint64_t h = 1469598103934665603ULL; - h = (h ^ (uint64_t)(uint32_t)s->player.x) * 1099511628211ULL; - h = (h ^ (uint64_t)(uint32_t)s->player.y) * 1099511628211ULL; - for (int n = 0; n < COLO_MAX_NPCS; n++) { - const ColoNPC* npc = &s->npcs[n]; - if (!col_npc_is_live_enemy(npc)) continue; - int range = COLO_NPC_STATS[npc->type].attack_range; - if (range <= 1) continue; - uint64_t shooter = (uint64_t)(uint32_t)npc->x - | ((uint64_t)(uint32_t)npc->y << 8) - | ((uint64_t)(uint32_t)col_npc_effective_size(npc) << 16) - | ((uint64_t)(uint32_t)range << 24); - h = (h ^ shooter) * 1099511628211ULL; - } - const uint8_t* grid = &s->npc_collision_flags[0][0]; - const size_t grid_bytes = sizeof(s->npc_collision_flags); - size_t b = 0; - for (; b + 8 <= grid_bytes; b += 8) { - uint64_t word; - memcpy(&word, grid + b, 8); - h = (h ^ word) * 1099511628211ULL; - } - for (; b < grid_bytes; b++) - h = (h ^ (uint64_t)grid[b]) * 1099511628211ULL; - return h; -} - -static void col_write_threat_field_obs(ColosseumState* s, float* obs, int* idx) { - int i = *idx; - if (!COLO_STATIC_LOOKUP_TABLES_BUILT) col_build_static_arena(); - - ColoObsMemoCaches* memos = &s->obs_memos; - uint64_t sig = col_threat_field_obs_signature(s); - for (int w = 0; w < COLO_THREAT_FIELD_OBS_CACHE_WAYS; w++) { - if (memos->threat_field_valid[w] && memos->threat_field_sig[w] == sig) { - memcpy(&obs[i], memos->threat_field[w], - sizeof(float) * COLO_THREAT_FIELD_OBS_CACHE_FLOATS); - *idx = i + COLO_THREAT_FIELD_OBS_CACHE_FLOATS; - return; - } - } - int block_start = i; - - int shooter_x0[COLO_MAX_NPCS], shooter_y0[COLO_MAX_NPCS]; - int shooter_x1[COLO_MAX_NPCS], shooter_y1[COLO_MAX_NPCS]; - int shooter_ranges[COLO_MAX_NPCS]; - int n_shooters = 0; - for (int n = 0; n < COLO_MAX_NPCS; n++) { - const ColoNPC* npc = &s->npcs[n]; - if (!col_npc_is_live_enemy(npc)) continue; - int range = COLO_NPC_STATS[npc->type].attack_range; - if (range <= 1) continue; - int size = col_npc_effective_size(npc); - shooter_x0[n_shooters] = npc->x; - shooter_y0[n_shooters] = npc->y; - shooter_x1[n_shooters] = npc->x + size - 1; - shooter_y1[n_shooters] = npc->y + size - 1; - shooter_ranges[n_shooters] = range; - n_shooters++; - } - for (int dy = -COLO_THREAT_FIELD_RADIUS; dy <= COLO_THREAT_FIELD_RADIUS; dy++) { - for (int dx = -COLO_THREAT_FIELD_RADIUS; dx <= COLO_THREAT_FIELD_RADIUS; dx++) { - int tx = s->player.x + dx, ty = s->player.y + dy; - int gx, gy; - int count = 0; - if (col_grid_index(tx, ty, &gx, &gy)) { - for (int k = 0; k < n_shooters; k++) { - int sx = tx < shooter_x0[k] ? shooter_x0[k] - : (tx > shooter_x1[k] ? shooter_x1[k] : tx); - int sy = ty < shooter_y0[k] ? shooter_y0[k] - : (ty > shooter_y1[k] ? shooter_y1[k] : ty); - int adx = tx - sx; if (adx < 0) adx = -adx; - int ady = ty - sy; if (ady < 0) ady = -ady; - int dist = adx > ady ? adx : ady; - if (dist < 1 || dist > shooter_ranges[k]) continue; - if (!col_static_los_get(sx, sy, tx, ty)) continue; - count++; - } - } - obs[i++] = col_clamp01((float)count / 4.0f); - } - } - for (int dy = -COLO_THREAT_FIELD_RADIUS; dy <= COLO_THREAT_FIELD_RADIUS; dy++) { - for (int dx = -COLO_THREAT_FIELD_RADIUS; dx <= COLO_THREAT_FIELD_RADIUS; dx++) { - int tx = s->player.x + dx, ty = s->player.y + dy; - int gx, gy; - int blocked = 1; - if (col_grid_index(tx, ty, &gx, &gy)) - blocked = col_static_blocked(tx, ty) || s->npc_collision_flags[gx][gy]; - obs[i++] = blocked ? 1.0f : 0.0f; - } - } - - int slot = memos->threat_field_next; - memcpy(memos->threat_field[slot], &obs[block_start], - sizeof(float) * COLO_THREAT_FIELD_OBS_CACHE_FLOATS); - memos->threat_field_sig[slot] = sig; - memos->threat_field_valid[slot] = 1; - memos->threat_field_next = (slot + 1) % COLO_THREAT_FIELD_OBS_CACHE_WAYS; - - *idx = i; -} - static void col_write_thrall_dc(const ColosseumState* s, float* obs, int* idx) { int i = *idx; int lifetime_full = COLO_THRALL_LIFETIME_TICKS[s->active_loadout_profile]; @@ -1088,66 +1027,7 @@ static void col_write_thrall_dc(const ColosseumState* s, float* obs, int* idx) { *idx = i; } -static void col_write_pillar_obs(const ColosseumState* s, float* obs, int* idx) { - int i = *idx; - for (int p = 0; p < COLO_NUM_PILLARS; p++) { - obs[i++] = col_obs_rel_x(COLO_PILLARS[p][0], s->player.x); - obs[i++] = col_obs_rel_y(COLO_PILLARS[p][1], s->player.y); - obs[i++] = col_clamp01((float)COLO_PILLAR_SIZE / 7.0f); - } - *idx = i; -} -static void col_equipment_bonus_projection( - const uint8_t equipment[NUM_GEAR_SLOTS], - float out[6] -) { - int slash = 0; - int melee_str = 0; - int ranged_att = 0; - int ranged_str = 0; - int magic = 0; - int defence = 0; - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - uint8_t item_idx = equipment[slot]; - if (item_idx == ITEM_NONE) continue; - if (item_idx >= NUM_ITEMS) { - fprintf(stderr, "colosseum obs: invalid equipped item %u\n", item_idx); - abort(); - } - const Item* item = &ITEM_DATABASE[item_idx]; - slash += item->attack_slash; - melee_str += item->melee_strength; - ranged_att += item->attack_ranged; - ranged_str += item->ranged_strength; - magic += item->attack_magic + item->magic_damage; - defence += item->defence_stab + item->defence_slash + item->defence_crush + - item->defence_magic + item->defence_ranged; - } - out[0] = (float)slash / STAT_NORM_ATTACK; - out[1] = (float)melee_str / STAT_NORM_STRENGTH; - out[2] = (float)ranged_att / STAT_NORM_ATTACK; - out[3] = (float)ranged_str / STAT_NORM_STRENGTH; - out[4] = (float)magic / (STAT_NORM_ATTACK + STAT_NORM_MAGIC_DMG); - out[5] = (float)defence / (5.0f * STAT_NORM_DEFENCE); -} - -static void col_hypothetical_equipment_after_cell( - const ColosseumState* s, - int cell_idx, - uint8_t out[NUM_GEAR_SLOTS] -) { - memcpy(out, s->player.equipped, NUM_GEAR_SLOTS); - const ColoInvCell* cell = &s->inventory_cells[cell_idx]; - if (cell->item_idx == ITEM_NONE) return; - int gear_slot = osrs_item_gear_slot(cell->item_idx); - if (gear_slot < 0) return; - if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(cell->item_idx)) - out[GEAR_SLOT_SHIELD] = ITEM_NONE; - if (gear_slot == GEAR_SLOT_SHIELD && item_is_two_handed(out[GEAR_SLOT_WEAPON])) - out[GEAR_SLOT_WEAPON] = ITEM_NONE; - out[gear_slot] = cell->item_idx; -} static float col_expected_dpt_for_equipment_vs_npc( ColosseumState* s, @@ -1315,8 +1195,8 @@ static void col_memo_loadout_stats( } } - static EncounterLoadoutStats overflow_stats; - static OsrsEquipmentEffectProfile overflow_effects; + static OSRS_THREAD_LOCAL EncounterLoadoutStats overflow_stats; + static OSRS_THREAD_LOCAL OsrsEquipmentEffectProfile overflow_effects; encounter_compute_loadout_stats( equipment, style, prayer, current_att_level, fight_style, spell_base_damage, &overflow_stats); @@ -1448,30 +1328,59 @@ static void col_build_best_gear_table( } } -static uint64_t col_best_gear_signature(const ColosseumState* s) { +static uint64_t col_best_gear_cache_signature(const ColosseumState* s) { + ColoGearCandidates candidates; + col_build_gear_candidates(s, &candidates); uint64_t h = 1469598103934665603ULL; - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) - h = (h ^ (uint64_t)s->player.equipped[slot]) * 1099511628211ULL; - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) - h = (h ^ (uint64_t)s->inventory_cells[cell].item_idx) * 1099511628211ULL; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + int count = candidates.count[slot]; + h = (h ^ (uint64_t)(uint32_t)count) * 1099511628211ULL; + uint8_t sorted[COLO_BEST_GEAR_MAX_CAND]; + memcpy(sorted, candidates.items[slot], (size_t)count); + for (int i = 1; i < count; i++) { + uint8_t item = sorted[i]; + int j = i; + while (j > 0 && sorted[j - 1] > item) { + sorted[j] = sorted[j - 1]; + j--; + } + sorted[j] = item; + } + for (int i = 0; i < count; i++) + h = (h ^ (uint64_t)sorted[i]) * 1099511628211ULL; + } const int scalars[5] = { s->player.current_attack, s->player.current_strength, s->player.current_ranged, s->player.current_magic, (int)s->active_loadout_profile, }; - for (int k = 0; k < 5; k++) - h = (h ^ (uint64_t)(uint32_t)scalars[k]) * 1099511628211ULL; + for (int i = 0; i < 5; i++) + h = (h ^ (uint64_t)(uint32_t)scalars[i]) * 1099511628211ULL; return h; } static const ColoBestGear (*col_get_best_gear_table(ColosseumState* s))[COLO_NUM_NPC_TYPES] { if (!s) abort(); +#ifdef COLO_PROFILE_ENABLED + if (COLO_PROFILE_ENABLED()) + COLO_PROFILE_ADD(COLO_PROF_BEST_GEAR_REQUESTS, 1.0); +#endif ColoObsMemoCaches* memos = &s->obs_memos; - uint64_t sig = col_best_gear_signature(s); + uint64_t sig = col_best_gear_cache_signature(s); for (int w = 0; w < COLO_BEST_GEAR_CACHE_WAYS; w++) { - if (memos->best_gear_valid[w] && memos->best_gear_sig[w] == sig) + if (memos->best_gear_valid[w] && memos->best_gear_sig[w] == sig) { +#ifdef COLO_PROFILE_ENABLED + if (COLO_PROFILE_ENABLED()) + COLO_PROFILE_ADD(COLO_PROF_BEST_GEAR_HITS, 1.0); +#endif return memos->best_gear[w]; + } + } +#ifdef COLO_PROFILE_ENABLED + if (COLO_PROFILE_ENABLED()) { + COLO_PROFILE_ADD(COLO_PROF_BEST_GEAR_BUILDS, 1.0); } +#endif int slot = memos->best_gear_next; col_build_best_gear_table(s, memos->best_gear[slot]); memos->best_gear_sig[slot] = sig; @@ -1505,105 +1414,6 @@ static void col_apply_bis_gear_oracle(ColosseumState* s) { col_apply_weapon_set(s, best_set); } -static ColoWeaponSet col_weapon_set_for_item(uint8_t item) { - int item_style = get_item_attack_style(item); - if (item_style == 2) return COLO_GEAR_RANGED; - if (item_style == 3) return COLO_GEAR_MAGIC; - return COLO_GEAR_MELEE; -} - -static uint64_t col_weapon_choice_obs_signature(ColosseumState* s) { - uint64_t h = col_best_gear_signature(s); - int has_target = 0, type = 0, def_drained = 0, size = 0; - int target = col_player_target_slot(s); - if (target >= 0 && target < COLO_MAX_NPCS) { - const ColoNPC* npc = &s->npcs[target]; - if (col_npc_is_live_target(npc) && !col_type_is_hazard_entity(npc->type)) { - has_target = 1; - type = (int)npc->type; - def_drained = npc->def_drained; - size = npc->size; - } - } - const int target_fields[4] = { has_target, type, def_drained, size }; - for (int k = 0; k < 4; k++) - h = (h ^ (uint64_t)(uint32_t)target_fields[k]) * 1099511628211ULL; - return h; -} - -static void col_write_weapon_choice_obs(ColosseumState* s, float* obs, int* io) { - int i = *io; - - ColoObsMemoCaches* memos = &s->obs_memos; - uint64_t sig = col_weapon_choice_obs_signature(s); - for (int w = 0; w < COLO_WEAPON_CHOICE_OBS_CACHE_WAYS; w++) { - if (memos->weapon_choice_valid[w] && memos->weapon_choice_sig[w] == sig) { - memcpy(&obs[i], memos->weapon_choice[w], - sizeof(float) * COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS); - *io = i + COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS; - return; - } - } - int block_start = i; - - const ColoNPC* npc = NULL; - int target = col_player_target_slot(s); - if (target >= 0 && target < COLO_MAX_NPCS) { - const ColoNPC* cand = &s->npcs[target]; - if (col_npc_is_live_target(cand) && !col_type_is_hazard_entity(cand->type)) - npc = cand; - } - const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = NULL; - float best_dpt = 0.0f; - if (npc) { - best = col_get_best_gear_table(s); - for (int st = 0; st < COLO_NUM_WEAPON_SETS; st++) - if (best[st][npc->type].dpt > best_dpt) - best_dpt = best[st][npc->type].dpt; - } - - for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - float v = 0.0f; - uint8_t item = s->inventory_cells[c].item_idx; - if (npc && item != ITEM_NONE && - osrs_item_gear_slot(item) == GEAR_SLOT_WEAPON) { - ColoWeaponSet set = col_weapon_set_for_item(item); - const ColoBestGear* bs = &best[set][npc->type]; - if (bs->dpt >= 0.0f) { - uint8_t setup[NUM_GEAR_SLOTS]; - memcpy(setup, bs->setup, NUM_GEAR_SLOTS); - setup[GEAR_SLOT_WEAPON] = item; - if (item_is_two_handed(item)) - setup[GEAR_SLOT_SHIELD] = ITEM_NONE; - float dpt = col_expected_dpt_for_equipment_vs_npc( - s, setup, npc, 1); - v = col_clamp01(dpt / COLO_EXPECTED_DPT_NORM); - } - } - obs[i++] = v; - } - - for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - uint8_t item = s->inventory_cells[c].item_idx; - obs[i++] = (item != ITEM_NONE && osrs_spec_cost(item) > 0) ? 1.0f : 0.0f; - } - - float cur = npc - ? col_expected_dpt_for_equipment_vs_npc(s, s->player.equipped, npc, 1) - : 0.0f; - obs[i++] = col_clamp01(cur / COLO_EXPECTED_DPT_NORM); - obs[i++] = (npc && best_dpt > 0.0f) ? col_clamp01(cur / best_dpt) : 0.0f; - - int slot = memos->weapon_choice_next; - memcpy(memos->weapon_choice[slot], &obs[block_start], - sizeof(float) * COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS); - memos->weapon_choice_sig[slot] = sig; - memos->weapon_choice_valid[slot] = 1; - memos->weapon_choice_next = (slot + 1) % COLO_WEAPON_CHOICE_OBS_CACHE_WAYS; - - *io = i; -} - static void col_write_spawn_obs(ColosseumState* s, float* obs, int* io) { int i = *io; float until_spawn = (float)s->wave_spawn_delay; @@ -1638,7 +1448,10 @@ static int col_weapon_set_is_argmax_dpt_for_npc( return 1; } +/* Sampled on attack ticks only. Reading it on every tick that merely has a target fired + * the best-gear oracle ~4x more often than the reward that motivated the metric. */ static int col_current_set_is_argmax_dpt_for_target(ColosseumState* s) { + if (!s->tick_scratch.player_attacked) return -1; int target = col_player_target_slot(s); if (target < 0 || target >= COLO_MAX_NPCS) return -1; const ColoNPC* npc = &s->npcs[target]; @@ -1659,26 +1472,11 @@ static int col_attacked_with_argmax_set(ColosseumState* s) { return col_weapon_set_is_argmax_dpt_for_npc(s, npc_slot, s->weapon_set, best); } -static void col_inventory_cell_post_use_deltas( - const ColosseumState* s, - int cell_idx, - const float before[6], - float out[6] -) { - for (int i = 0; i < 6; i++) out[i] = 0.0f; - if (s->inventory_cells[cell_idx].item_idx == ITEM_NONE) return; - - float after[6]; - uint8_t hypothetical[NUM_GEAR_SLOTS]; - col_hypothetical_equipment_after_cell(s, cell_idx, hypothetical); - col_equipment_bonus_projection(hypothetical, after); - for (int i = 0; i < 6; i++) out[i] = after[i] - before[i]; -} - static uint64_t col_inventory_obs_signature(const ColosseumState* s) { + /* Equipped gear is deliberately NOT in the key. It was only ever an input through the + * counterfactual deltas, and hashing it made every gear swap evict the block -- measured + * at a 34.9% hit rate. What remains depends on cell contents and base stats only. */ uint64_t h = 1469598103934665603ULL; - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) - h = (h ^ (uint64_t)s->player.equipped[slot]) * 1099511628211ULL; for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { const ColoInvCell* c = &s->inventory_cells[cell]; uint64_t packed = (uint64_t)c->item_idx @@ -1694,12 +1492,49 @@ static uint64_t col_inventory_obs_signature(const ColosseumState* s) { return h; } +/* Gear-dependent, so it is written fresh on every path. The memo key covers cell contents + * only -- putting equipped gear back in it would send the hit rate back to the 34.9% that + * made this block expensive in the first place. */ +/* Non-const because col_live_effects memoises internally. C accepts the implicit cast and + * g++/nvcc reject it, so this only shows up in check-builds.sh, never in the C test gate. */ +static void col_write_equipped_effect_aggregate( + ColosseumState* s, float* obs, int* pi +) { + int i = *pi; + const OsrsEquipmentEffectProfile* fx = col_live_effects(s); + float agg_eff4[4]; + osrs_item_effect_class4(fx->effect_mask, agg_eff4); + obs[i++] = agg_eff4[0]; + obs[i++] = agg_eff4[1]; + obs[i++] = agg_eff4[2]; + obs[i++] = agg_eff4[3]; + obs[i++] = col_clamp01((float)fx->virtus_piece_count / 3.0f); + obs[i++] = col_clamp01((float)fx->dharok_piece_count / 4.0f); + obs[i++] = col_clamp01((float)fx->crystal_armour_points / 6.0f); + obs[i++] = fx->recoil_source != OSRS_RECOIL_SOURCE_NONE ? 1.0f : 0.0f; + obs[i++] = fx->spec_regen_mode == OSRS_SPEC_REGEN_MODE_LIGHTBEARER ? 1.0f : 0.0f; + obs[i++] = fx->shield_item != ITEM_NONE ? 1.0f : 0.0f; + *pi = i; +} + +/* Likewise gear-dependent: it asks whether the item in this cell is the one currently worn + * in its slot. Stamped after a cache hit so a gear swap shows up on the tick it happens. */ +static void col_stamp_equipped_flags(const ColosseumState* s, float* obs, int block_start) { + for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) { + obs[block_start + c * COLO_INVENTORY_CELL_OBS_FEATURES + + OSRS_INVENTORY_CELL_OBS_EQUIPPED] = + osrs_inventory_cell_holds_equipped_item(&s->player, s->inventory_cells, c) + ? 1.0f : 0.0f; + } +} + static void col_write_inventory_and_equipped_obs( ColosseumState* s, float* obs, int* idx ) { int i = *idx; + int block_start = i; ColoObsMemoCaches* memos = &s->obs_memos; uint64_t sig = col_inventory_obs_signature(s); @@ -1707,37 +1542,45 @@ static void col_write_inventory_and_equipped_obs( if (memos->inventory_valid[w] && memos->inventory_sig[w] == sig) { memcpy(&obs[i], memos->inventory[w], sizeof(float) * COLO_INVENTORY_OBS_CACHE_FLOATS); - *idx = i + COLO_INVENTORY_OBS_CACHE_FLOATS; + col_stamp_equipped_flags(s, obs, block_start); + int j = block_start + COLO_INVENTORY_OBS_SIZE; + col_write_equipped_effect_aggregate(s, obs, &j); + s->log.inv_memo_hits += 1.0f; + *idx = block_start + COLO_INVENTORY_OBS_CACHE_FLOATS; return; } } - int block_start = i; + s->log.inv_memo_misses += 1.0f; + + /* prayer_restore and offensive_boost are baked into COLO_ITEM_OBS_TABLE at these levels. + * base_hitpoints is deliberately absent from that list: Frailty rewrites it, which is why + * hp_heal still rides the observation. Anything that starts moving the other two the same + * way has to ship them too, and must not discover that during a training run. */ + if (s->player.base_prayer != COLO_ITEM_OBS_TABLE_BASE_PRAYER || + s->player.base_attack != COLO_ITEM_OBS_TABLE_BASE_LEVEL) { + fprintf(stderr, + "colosseum inventory obs: base prayer %d attack %d, item table baked %d/%d\n", + s->player.base_prayer, s->player.base_attack, + COLO_ITEM_OBS_TABLE_BASE_PRAYER, COLO_ITEM_OBS_TABLE_BASE_LEVEL); + abort(); + } - float current_equipment_projection[6]; - col_equipment_bonus_projection(s->player.equipped, current_equipment_projection); for (int cell_idx = 0; cell_idx < COLO_INVENTORY_DISPLAY_SLOTS; cell_idx++) { - float deltas[6]; const ColoInvCell* cell = &s->inventory_cells[cell_idx]; - col_inventory_cell_post_use_deltas( - s, cell_idx, current_equipment_projection, deltas); - osrs_write_inventory_cell_affordance_features( + osrs_write_inventory_cell_obs_code_features( &obs[i], cell->item_idx, cell->raw_osrs_id, cell->dose, - col_inventory_cell_holds_equipped_item(s, cell_idx), - deltas, + osrs_inventory_cell_holds_equipped_item(&s->player, s->inventory_cells, cell_idx), s->player.base_hitpoints, s->player.base_prayer, s->player.base_attack); - i += OSRS_INVENTORY_CELL_OBS_FEATURES; + i += COLO_INVENTORY_CELL_OBS_FEATURES; } assert(i == COLO_OBS_AFTER_INVENTORY); - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - osrs_write_equipped_self_features(&obs[i], s->player.equipped[slot]); - i += OSRS_EQUIPPED_SELF_OBS_FEATURES; - } + col_write_equipped_effect_aggregate(s, obs, &i); assert(i == COLO_OBS_AFTER_EQUIPPED_SELF); int slot = memos->inventory_next; @@ -1758,6 +1601,13 @@ static void col_write_obs_ctx(EncounterState* state, EncounterContext* context, double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; #endif col_refresh_current_obs_slots_ctx(s, ctx); + ColosseumState post_premove_molten; + int premove_molten_burn = 0; + if (col_premove_molten_forecast_needed(s)) { + premove_molten_burn = + col_build_post_premove_molten_forecast( + s, &post_premove_molten); + } #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_REFRESH_SLOTS); #endif @@ -1808,8 +1658,6 @@ static void col_write_obs_ctx(EncounterState* state, EncounterContext* context, COLO_PROFILE_MARK(COLO_PROF_OBS_PREFIX); #endif - col_write_pillar_obs(s, obs, &i); - assert(i == COLO_OBS_AFTER_PILLARS); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_PILLARS); #endif @@ -1839,17 +1687,16 @@ static void col_write_obs_ctx(EncounterState* state, EncounterContext* context, COLO_PROFILE_MARK(COLO_PROF_OBS_NPC_SLOTS); #endif - for (int m = 0; m < COLO_NUM_MODIFIERS; m++) { - obs[i++] = (s->modifiers.active_mask & (1u << m)) ? 1.0f : 0.0f; + for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) obs[i++] = col_clamp01((float)s->modifiers.tier[m] / 3.0f); - } obs[i++] = s->modifiers.draft_pending ? 1.0f : 0.0f; for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { int opt = s->modifiers.draft_options[o]; obs[i++] = (s->modifiers.draft_pending && opt >= 0) ? (float)(opt + 1) / (float)COLO_NUM_MODIFIERS : 0.0f; } - col_write_modifier_hazards(s, obs, &i); + col_write_modifier_hazards( + s, premove_molten_burn, obs, &i); assert(i == COLO_OBS_AFTER_MODS); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_MODIFIERS); @@ -1892,24 +1739,6 @@ static void col_write_obs_ctx(EncounterState* state, EncounterContext* context, COLO_PROFILE_MARK(COLO_PROF_OBS_PENDING_HITS); #endif - if (ctx->config.step_out_forecast_obs_enabled) { - int horizon = ctx->config.forecast_horizon; - if (horizon < 1) horizon = 1; - if (horizon > COLO_STEP_OUT_FORECAST_HORIZON) - horizon = COLO_STEP_OUT_FORECAST_HORIZON; - ColoStepOutForecast forecast; - ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS]; - col_build_step_out_forecast_horizon_mode_summary( - s, &forecast, summaries, horizon, ctx->config.forecast_run_tile_mode); - i = col_write_step_out_forecast_obs_summary(&forecast, summaries, horizon, obs, i); - } else { - i += COLO_STEP_OUT_FORECAST_OBS_SIZE; - } - assert(i == COLO_OBS_AFTER_STEP_OUT_FORECAST); -#ifdef COLO_PROFILE_ENABLED - COLO_PROFILE_MARK(COLO_PROF_OBS_FORECAST); -#endif - col_write_threat_los_counts(s, obs, &i); assert(i == COLO_OBS_AFTER_THREAT_LOS); #ifdef COLO_PROFILE_ENABLED @@ -1922,28 +1751,12 @@ static void col_write_obs_ctx(EncounterState* state, EncounterContext* context, COLO_PROFILE_MARK(COLO_PROF_OBS_THRALL_DC); #endif - col_write_weapon_choice_obs(s, obs, &i); - assert(i == COLO_OBS_AFTER_WEAPON_CHOICE); -#ifdef COLO_PROFILE_ENABLED - COLO_PROFILE_MARK(COLO_PROF_OBS_WEAPON_CHOICE); -#endif - col_write_spawn_obs(s, obs, &i); assert(i == COLO_OBS_AFTER_SPAWN); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_SPAWN); #endif - if (ctx->config.threat_field_obs_enabled) { - col_write_threat_field_obs(s, obs, &i); - } else { - i += COLO_THREAT_FIELD_OBS_SIZE; - } - assert(i == COLO_OBS_AFTER_THREAT_FIELD); -#ifdef COLO_PROFILE_ENABLED - COLO_PROFILE_MARK(COLO_PROF_OBS_THREAT_FIELD); -#endif - if (i != COLO_NUM_OBS) { fprintf(stderr, "colosseum obs index mismatch: wrote %d, expected %d\n", i, COLO_NUM_OBS); abort(); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc index 90f14007d1..2663bb6b7d 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -133,14 +133,28 @@ static void col_tick_live_stat_drift_and_divines(ColosseumState* s) { } static void col_apply_divine_combat_potion_effect(ColosseumState* s) { - encounter_damage_player(&s->player, 10, &s->tick_scratch.damage_received); - s->tick_scratch.landed_self_damage += 10.0f; + int hitpoints_before = s->player.current_hitpoints; + encounter_damage_player(&s->player, 10, NULL); + int applied = hitpoints_before - s->player.current_hitpoints; + s->tick_scratch.damage_received += (float)applied; + s->tick_scratch.landed_self_damage += (float)applied; + if (applied > 0) { + s->last_damage_source = COLO_DAMAGE_SELF; + col_mod_add_doom_stack(s, COLO_DAMAGE_SELF); + } s->divine_combat_timer = ENCOUNTER_DIVINE_POTION_TICKS; } static void col_apply_divine_ranged_potion_effect(ColosseumState* s) { - encounter_damage_player(&s->player, 10, &s->tick_scratch.damage_received); - s->tick_scratch.landed_self_damage += 10.0f; + int hitpoints_before = s->player.current_hitpoints; + encounter_damage_player(&s->player, 10, NULL); + int applied = hitpoints_before - s->player.current_hitpoints; + s->tick_scratch.damage_received += (float)applied; + s->tick_scratch.landed_self_damage += (float)applied; + if (applied > 0) { + s->last_damage_source = COLO_DAMAGE_SELF; + col_mod_add_doom_stack(s, COLO_DAMAGE_SELF); + } s->divine_ranged_timer = ENCOUNTER_DIVINE_POTION_TICKS; } @@ -152,7 +166,6 @@ static int col_overhead_switch_slips(uint32_t* rng_state, float fail_prob) { static void col_player_pretick(ColosseumState* s, ColosseumContext* ctx, const int* actions) { OverheadPrayer prev_prayer = s->player.prayer; OffensivePrayer prev_offensive = s->player.offensive_prayer; - int prayer_before = s->player.current_prayer; int overhead_activated = encounter_apply_overhead_action(&s->player.prayer, actions[COLO_HEAD_PRAYER]); if (s->player.prayer != prev_prayer && @@ -169,16 +182,6 @@ static void col_player_pretick(ColosseumState* s, ColosseumContext* ctx, const i encounter_drain_all_prayers(&s->player, encounter_player_prayer_bonus(&s->player)); if (s->player.offensive_prayer != prev_offensive) col_mark_live_loadout_dirty(s); - if (ctx->config.action_debug_log) { - fprintf(stderr, - "colo_action tick=%d pretick primary=%d prayer_action=%d " - "offensive_action=%d spell=%d prayer=%d->%d offensive=%d->%d pp=%d->%d\n", - s->tick, actions[COLO_HEAD_PRIMARY], actions[COLO_HEAD_PRAYER], - actions[COLO_HEAD_OFFENSIVE], - actions[COLO_HEAD_SPELL], prev_prayer, s->player.prayer, - prev_offensive, s->player.offensive_prayer, prayer_before, - s->player.current_prayer); - } } static void col_apply_weapon_set(ColosseumState* s, ColoWeaponSet set) { @@ -204,76 +207,12 @@ static void col_sync_weapon_set_from_equipped_weapon(ColosseumState* s) { s->player.visible_gear = s->player.current_gear; } -static int col_first_empty_inventory_cell(const ColosseumState* s, int except_slot) { - for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - if (i == except_slot) continue; - if (osrs_inventory_cell_is_empty(&s->inventory_cells[i])) return i; - } - return -1; -} - -static int col_inventory_cell_holds_equipped_item(const ColosseumState* s, int cell_idx) { - const ColoInvCell* cell = &s->inventory_cells[cell_idx]; - if (cell->item_idx == ITEM_NONE) return 0; - int slot = osrs_item_gear_slot(cell->item_idx); - if (slot < 0) return 0; - return s->player.equipped[slot] == cell->item_idx; -} - static int col_can_equip_from_cell(const ColosseumState* s, int cell_idx) { - const ColoInvCell* cell = &s->inventory_cells[cell_idx]; - if (cell->item_idx == ITEM_NONE) return 0; - if (col_inventory_cell_holds_equipped_item(s, cell_idx)) return 0; - - uint8_t item_idx = cell->item_idx; - int gear_slot = osrs_item_gear_slot(item_idx); - if (gear_slot < 0) return 0; - if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(item_idx) && - s->player.equipped[GEAR_SLOT_SHIELD] != ITEM_NONE && - s->player.equipped[GEAR_SLOT_WEAPON] != ITEM_NONE && - col_first_empty_inventory_cell(s, cell_idx) < 0) return 0; - if (gear_slot == GEAR_SLOT_SHIELD && - item_is_two_handed(s->player.equipped[GEAR_SLOT_WEAPON]) && - col_first_empty_inventory_cell(s, cell_idx) < 0) return 0; - return 1; + return osrs_can_equip_from_cell(&s->player, s->inventory_cells, cell_idx); } static void col_equip_from_cell(ColosseumState* s, int cell_idx) { - if (!col_can_equip_from_cell(s, cell_idx)) return; - - ColoInvCell* cell = &s->inventory_cells[cell_idx]; - uint8_t item_idx = cell->item_idx; - int gear_slot = osrs_item_gear_slot(item_idx); - if (gear_slot < 0) { - fprintf(stderr, "colosseum equip: item %u has no gear slot\n", item_idx); - abort(); - } - uint8_t displaced = s->player.equipped[gear_slot]; - - if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(item_idx)) { - uint8_t shield = s->player.equipped[GEAR_SLOT_SHIELD]; - if (shield != ITEM_NONE) { - if (displaced == ITEM_NONE) { - displaced = shield; - } else { - int empty = col_first_empty_inventory_cell(s, cell_idx); - if (empty < 0) return; - s->inventory_cells[empty] = osrs_inventory_cell_from_item(shield); - } - s->player.equipped[GEAR_SLOT_SHIELD] = ITEM_NONE; - } - } else if (gear_slot == GEAR_SLOT_SHIELD && - item_is_two_handed(s->player.equipped[GEAR_SLOT_WEAPON])) { - displaced = s->player.equipped[GEAR_SLOT_WEAPON]; - s->player.equipped[GEAR_SLOT_WEAPON] = ITEM_NONE; - - s->player.spec_armed = 0; - } - - s->player.equipped[gear_slot] = item_idx; - *cell = osrs_inventory_cell_from_item(displaced); - if (gear_slot == GEAR_SLOT_WEAPON) s->player.spec_armed = 0; - osrs_refresh_player_equipment(&s->player); + if (osrs_equip_from_cell(&s->player, s->inventory_cells, cell_idx) < 0) return; col_sync_weapon_set_from_equipped_weapon(s); col_mark_live_loadout_dirty(s); } @@ -282,28 +221,7 @@ static int col_can_eat_consumable_kind( const ColosseumState* s, OsrsConsumableKind kind ) { - switch (kind) { - case OSRS_CONSUMABLE_SHARK_FOOD: - return s->player.food_timer == 0 && - s->player.current_hitpoints < s->player.base_hitpoints; - case OSRS_CONSUMABLE_KARAMBWAN: - return s->player.karambwan_timer == 0 && - s->player.current_hitpoints < s->player.base_hitpoints; - case OSRS_CONSUMABLE_BREW: - case OSRS_CONSUMABLE_SUPER_RESTORE: - case OSRS_CONSUMABLE_SANFEW: - case OSRS_CONSUMABLE_SUPER_COMBAT: - case OSRS_CONSUMABLE_DIVINE_COMBAT: - case OSRS_CONSUMABLE_RANGING: - case OSRS_CONSUMABLE_DIVINE_RANGING: - case OSRS_CONSUMABLE_SURGE: - case OSRS_CONSUMABLE_GUTHIX_REST: - case OSRS_CONSUMABLE_SATURATED_HEART: - case OSRS_CONSUMABLE_ANTIVENOM_PLUS: - case OSRS_CONSUMABLE_NONE: - return 0; - } - abort(); + return osrs_can_eat_consumable_kind(&s->player, kind); } #define COLO_BOOST_REDRINK_LEVEL 105 @@ -338,6 +256,8 @@ static int col_drink_has_effect(const ColosseumState* s, OsrsConsumableKind kind return p->current_magic < COLO_BOOST_REDRINK_LEVEL; case OSRS_CONSUMABLE_SUPER_RESTORE: return col_restore_worth_drinking(s); + case OSRS_CONSUMABLE_PRAYER_RESTORE: + return p->current_prayer < p->base_prayer; case OSRS_CONSUMABLE_SANFEW: return afflicted || col_restore_worth_drinking(s); default: @@ -368,7 +288,7 @@ static void col_heal_player_capped(ColosseumState* s, int amount) { s->player.current_hitpoints = s->player.base_hitpoints; } -static void col_apply_food_cell(ColosseumState* s, OsrsConsumableKind kind) { +static int col_apply_food_cell(ColosseumState* s, OsrsConsumableKind kind) { FoodType type; if (kind == OSRS_CONSUMABLE_SHARK_FOOD) type = FOOD_SHARK; else if (kind == OSRS_CONSUMABLE_KARAMBWAN) type = FOOD_KARAMBWAN; @@ -376,34 +296,23 @@ static void col_apply_food_cell(ColosseumState* s, OsrsConsumableKind kind) { fprintf(stderr, "colosseum: consumable kind %d is not food\n", (int)kind); abort(); } - - EatResult r = osrs_eat_food(type, s->player.current_hitpoints, - s->player.base_hitpoints, osrs_player_food_timer(&s->player, type)); - if (!r.consumed) return; - if (type == FOOD_KARAMBWAN) { - s->player.karambwan_timer = 2; - s->player.food_timer = 3; - s->player.potion_timer = 3; - s->player.ate_karambwan_this_tick = 1; - s->player.last_karambwan_heal = r.hp_healed; - } else { - s->player.food_timer = 3; - s->player.ate_food_this_tick = 1; - s->player.last_food_heal = r.hp_healed; - } - col_heal_player_capped(s, r.hp_healed); + return osrs_player_eat_food_effects(&s->player, type).consumed; } static void col_apply_drink_one_dose_effect(void* ctx, OsrsConsumableKind kind) { ColosseumState* s = (ColosseumState*)ctx; switch (kind) { - case OSRS_CONSUMABLE_BREW: - encounter_apply_brew_heal( - &s->player, osrs_brew_heal_amount(s->player.base_hitpoints)); + case OSRS_CONSUMABLE_BREW: { + int brew_heal = osrs_brew_heal_amount(s->player.base_hitpoints); + int cap = col_mod_active(s, COLO_MOD_FRAILTY) + ? s->player.base_hitpoints + : s->player.base_hitpoints + brew_heal; + encounter_apply_brew_heal_capped(&s->player, brew_heal, cap); encounter_apply_consumable_stat_effect( &s->player, ENCOUNTER_CONSUMABLE_STAT_EFFECT_BREW_DRAIN); col_finish_stat_moving_consumable(s); return; + } case OSRS_CONSUMABLE_SUPER_RESTORE: encounter_add_prayer_restore( &s->player, osrs_super_restore_amount(s->player.base_prayer)); @@ -412,6 +321,11 @@ static void col_apply_drink_one_dose_effect(void* ctx, OsrsConsumableKind kind) encounter_cap_prayer_restore(&s->player); col_finish_stat_moving_consumable(s); return; + case OSRS_CONSUMABLE_PRAYER_RESTORE: + encounter_add_prayer_restore( + &s->player, osrs_prayer_potion_restore_amount(s->player.base_prayer)); + encounter_cap_prayer_restore(&s->player); + return; case OSRS_CONSUMABLE_SANFEW: encounter_add_prayer_restore( &s->player, osrs_sanfew_restore_amount(s->player.base_prayer)); @@ -473,6 +387,10 @@ static void col_apply_drink_one_dose_effect(void* ctx, OsrsConsumableKind kind) case OSRS_CONSUMABLE_KARAMBWAN: fprintf(stderr, "colosseum: consumable kind %d is not drinkable\n", (int)kind); abort(); + case OSRS_CONSUMABLE_BASTION: + case OSRS_CONSUMABLE_STAMINA: + fprintf(stderr, "colosseum: consumable kind %d not in colosseum kits\n", (int)kind); + abort(); } abort(); } @@ -497,154 +415,40 @@ static int col_apply_drink_cell( return 1; } -typedef struct { - int equip_cell_by_slot[NUM_GEAR_SLOTS]; - int equip_order_by_slot[NUM_GEAR_SLOTS]; - int eat_cell; - int eat_order; - OsrsInventoryClickResolution eat_resolution; - int drink_cell; - int drink_order; - OsrsInventoryClickResolution drink_resolution; -} ColoInventoryTickIntent; - -static ColoInventoryTickIntent col_inventory_tick_intent_empty(void) { - ColoInventoryTickIntent intent; - memset(&intent, 0, sizeof(intent)); - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - intent.equip_cell_by_slot[slot] = -1; - intent.equip_order_by_slot[slot] = -1; - } - intent.eat_cell = -1; - intent.eat_order = -1; - intent.drink_cell = -1; - intent.drink_order = -1; - return intent; -} - -static ColoInventoryTickIntent col_resolve_inventory_tick_intent( +static OsrsInventoryTickIntent col_resolve_inventory_tick_intent( const ColosseumState* s, const int* actions ) { - ColoInventoryTickIntent intent = col_inventory_tick_intent_empty(); - - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - int action = actions[COLO_HEAD_EQUIP_SLOT(slot)]; - if (action <= 0 || action > COLO_INVENTORY_DISPLAY_SLOTS) continue; - int cell_idx = action - 1; - if (!col_can_equip_from_cell(s, cell_idx)) continue; - int gear_slot = osrs_item_gear_slot(s->inventory_cells[cell_idx].item_idx); - if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) { - fprintf(stderr, "colosseum equip intent: invalid gear slot %d\n", gear_slot); - abort(); - } - if (gear_slot != slot) continue; - intent.equip_cell_by_slot[slot] = cell_idx; - intent.equip_order_by_slot[slot] = slot; - } - - int eat_action = actions[COLO_HEAD_EAT]; - if (eat_action > 0 && eat_action <= COLO_INVENTORY_DISPLAY_SLOTS) { - int cell_idx = eat_action - 1; - OsrsInventoryClickResolution resolution = - osrs_inventory_cell_click_interpret( - &s->inventory_cells[cell_idx], OSRS_CLICK_TICK_FIRST); - if (resolution.click_action == OSRS_CLICK_EAT && - col_can_eat_consumable_kind(s, resolution.consumable_kind)) { - intent.eat_cell = cell_idx; - intent.eat_order = NUM_GEAR_SLOTS; - intent.eat_resolution = resolution; - } - } - - int drink_action = actions[COLO_HEAD_DRINK]; - if (drink_action > 0 && drink_action <= COLO_INVENTORY_DISPLAY_SLOTS) { - int cell_idx = drink_action - 1; - OsrsInventoryClickResolution resolution = - osrs_inventory_cell_click_interpret( - &s->inventory_cells[cell_idx], OSRS_CLICK_TICK_FIRST); - if (resolution.click_action == OSRS_CLICK_DRINK && - s->inventory_cells[cell_idx].dose > 0 && - s->player.potion_timer == 0) { - intent.drink_cell = cell_idx; - intent.drink_order = NUM_GEAR_SLOTS + 1; - intent.drink_resolution = resolution; - } - } - - return intent; -} - -static int col_inventory_tick_intent_has_effect(const ColoInventoryTickIntent* intent) { - if (intent->eat_cell >= 0 || intent->drink_cell >= 0) return 1; + OsrsInventoryClickActions clicks; for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) - if (intent->equip_cell_by_slot[slot] >= 0) return 1; - return 0; -} - -static int col_next_inventory_apply_order(const ColoInventoryTickIntent* intent) { - int sentinel = NUM_GEAR_SLOTS + 2; - int best = sentinel; - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - int order = intent->equip_order_by_slot[slot]; - if (order >= 0 && order < best) best = order; - } - if (intent->eat_order >= 0 && intent->eat_order < best) best = intent->eat_order; - if (intent->drink_order >= 0 && intent->drink_order < best) best = intent->drink_order; - return best == sentinel ? -1 : best; + clicks.equip_by_slot[slot] = actions[COLO_HEAD_EQUIP_SLOT(slot)]; + clicks.eat = actions[COLO_HEAD_EAT]; + clicks.drink = actions[COLO_HEAD_DRINK]; + return osrs_resolve_inventory_tick_intent(&s->player, s->inventory_cells, &clicks); } static void col_apply_inventory_tick_intent( ColosseumState* s, - ColoInventoryTickIntent* intent + OsrsInventoryTickIntent* intent ) { - if (col_inventory_tick_intent_has_effect(intent)) + if (osrs_inventory_tick_intent_has_effect(intent)) osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EQUIP); - int order; - while ((order = col_next_inventory_apply_order(intent)) >= 0) { - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - if (intent->equip_order_by_slot[slot] != order) continue; - col_equip_from_cell(s, intent->equip_cell_by_slot[slot]); - intent->equip_order_by_slot[slot] = -1; - intent->equip_cell_by_slot[slot] = -1; - } - if (intent->eat_order == order) { - col_apply_food_cell(s, intent->eat_resolution.consumable_kind); - col_empty_inventory_cell(s, intent->eat_cell); - intent->eat_order = -1; - intent->eat_cell = -1; + OsrsInventoryApplyStep step; + while (osrs_inventory_intent_next(intent, &step)) { + switch (step.kind) { + case OSRS_INVENTORY_APPLY_EQUIP: + col_equip_from_cell(s, step.cell_idx); + break; + case OSRS_INVENTORY_APPLY_EAT: + if (col_apply_food_cell(s, step.resolution.consumable_kind)) + col_empty_inventory_cell(s, step.cell_idx); + break; + case OSRS_INVENTORY_APPLY_DRINK: + (void)col_apply_drink_cell(s, step.cell_idx, step.resolution); + break; } - if (intent->drink_order == order) { - (void)col_apply_drink_cell( - s, intent->drink_cell, intent->drink_resolution); - intent->drink_order = -1; - intent->drink_cell = -1; - } - } -} - -static void col_log_inventory_tick_intent( - const ColosseumState* s, - const ColoInventoryTickIntent* intent, - const int* actions, - int interrupted -) { - fprintf(stderr, "colo_action tick=%d equip_heads=", s->tick); - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) - fprintf(stderr, "%s%d", slot == 0 ? "" : ",", - actions[COLO_HEAD_EQUIP_SLOT(slot)]); - fprintf(stderr, " eat_head=%d drink_head=%d equips=", - actions[COLO_HEAD_EAT], actions[COLO_HEAD_DRINK]); - int wrote = 0; - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - if (intent->equip_cell_by_slot[slot] < 0) continue; - fprintf(stderr, "%sslot%d:cell%d", wrote ? "," : "", - slot, intent->equip_cell_by_slot[slot]); - wrote = 1; } - fprintf(stderr, " eat_cell=%d drink_cell=%d interrupted=%d\n", - intent->eat_cell, intent->drink_cell, interrupted); } static int col_npc_is_thrall_target(const ColosseumState* s, int slot) { @@ -1121,9 +925,7 @@ static void col_player_attack_target_ctx( abort(); } - int ticks = ls->style == ATTACK_STYLE_MELEE - ? hit_timing.damage_delay_ticks - : (ls->attack_range > 1 ? 3 : 1); + int ticks = hit_timing.damage_delay_ticks; int damage = 0; int bf_heal = 0; for (int splat = 0; splat < resolved_count; splat++) { @@ -1185,12 +987,95 @@ static void col_player_attack_target(ColosseumState* s, int npc_slot) { col_player_attack_target_ctx(s, col_legacy_context(), npc_slot); } +typedef struct { + OsrsPlayerStepInput input; + int move_action; +} ColoPrimaryStepPlan; + +static ColoPrimaryStepPlan col_primary_step_plan( + ColosseumState* s, + ColosseumContext* ctx, + Player* player, + OsrsInteraction* interaction, + int primary_action, + int* player_dest_x, + int* player_dest_y, + ColoWalkCtx* walk_ctx, + OsrsLosQuery* los_query +) { + if (primary_action < 0 || primary_action >= COLO_PRIMARY_DIM) { + fprintf(stderr, "colosseum primary action out of range: %d\n", primary_action); + abort(); + } + + int has_new_target = 0; + int new_target_slot = -1; + int move_action = 0; + if (primary_action > 0 && primary_action < ENCOUNTER_MOVE_ACTIONS) { + move_action = primary_action; + } else if (primary_action >= COLO_PRIMARY_ATTACK_BASE) { + int obs_slot = col_primary_obs_slot_from_action(primary_action); + int npc_slot = col_obs_slot_to_npc(s, obs_slot); + if (npc_slot >= 0 && s->npcs[npc_slot].active) { + has_new_target = 1; + new_target_slot = npc_slot; + } + } + + OsrsPlayerCommand command = { .kind = OSRS_PLAYER_CMD_NONE }; + if (has_new_target) { + command.kind = OSRS_PLAYER_CMD_TARGET; + command.target_slot = new_target_slot; + } else if (*player_dest_x >= 0) { + command.kind = OSRS_PLAYER_CMD_MOVE; + command.move_kind = OSRS_PLAYER_MOVE_DESTINATION; + } else if (move_action > 0 && move_action < ENCOUNTER_MOVE_ACTIONS) { + command.kind = OSRS_PLAYER_CMD_MOVE; + command.move_kind = OSRS_PLAYER_MOVE_ACTION; + command.move_action = move_action; + } + + *walk_ctx = (ColoWalkCtx){s, ctx}; + *los_query = col_player_los_query(s); + return (ColoPrimaryStepPlan){ + .input = { + .player = player, + .interaction = interaction, + .target_lookup = col_lookup_player_attack_target, + .target_ctx = s, + .command = command, + .dest_x = player_dest_x, + .dest_y = player_dest_y, + .arena = { + .collision_map = ctx->collision_map, + .world_offset_x = ctx->world_offset_x, + .world_offset_y = ctx->world_offset_y, + .is_walkable = col_tile_walkable, + .walkable_ctx = walk_ctx, + .extra_blocked = col_pathfind_blocked, + .blocked_ctx = walk_ctx, + .los_query = los_query, + .arena_base_x = COLO_ARENA_MIN_X, + .arena_base_y = COLO_ARENA_MIN_Y, + .arena_w = COLO_ARENA_WIDTH, + .arena_h = COLO_ARENA_HEIGHT, + }, + }, + .move_action = move_action, + }; +} + static void col_tick_player_ctx( ColosseumState* s, ColosseumContext* ctx, const int* actions, int can_attack ) { +#ifdef COLO_PROFILE_ENABLED + int col_prof_enabled = + COLO_PROFILE_ENABLED() && !s->profiling_suppressed; + double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; +#endif if (s->player_last_interaction_age == 0) s->player_last_interaction_age = 1; @@ -1202,13 +1087,17 @@ static void col_tick_player_ctx( if (encounter_tick_saturated_heart(&s->player)) col_mark_live_loadout_dirty(s); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PLAYER_TIMERS); +#endif + if (!ctx->config.mask_inventory_heads) { - ColoInventoryTickIntent intent = col_resolve_inventory_tick_intent(s, actions); - if (ctx->config.action_debug_log) - col_log_inventory_tick_intent( - s, &intent, actions, col_inventory_tick_intent_has_effect(&intent)); + OsrsInventoryTickIntent intent = col_resolve_inventory_tick_intent(s, actions); col_apply_inventory_tick_intent(s, &intent); } +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PLAYER_INVENTORY); +#endif int spec_act = actions[COLO_HEAD_SPEC]; uint8_t equipped_weapon = s->player.equipped[GEAR_SLOT_WEAPON]; @@ -1238,98 +1127,27 @@ static void col_tick_player_ctx( if (grapple_act > 0 && grapple_act <= COLO_NUM_GRAPPLE_SLOTS) col_sol_grapple_parry_attempt(s, grapple_act - 1); - int primary_act = actions[COLO_HEAD_PRIMARY]; - if (primary_act < 0 || primary_act >= COLO_PRIMARY_DIM) { - fprintf(stderr, "colosseum primary action out of range: %d\n", primary_act); - abort(); - } +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PLAYER_INTENT); +#endif - int has_new_target = 0; - int new_target_slot = -1; - int move_act = 0; - if (primary_act > 0 && primary_act < ENCOUNTER_MOVE_ACTIONS) { - move_act = primary_act; - } else if (primary_act >= COLO_PRIMARY_ATTACK_BASE) { - int obs_slot = col_primary_obs_slot_from_action(primary_act); - int npc_slot = col_obs_slot_to_npc(s, obs_slot); - if (npc_slot >= 0 && s->npcs[npc_slot].active) { - has_new_target = 1; - new_target_slot = npc_slot; - } - } - - int has_explicit_move = (move_act > 0 || s->player_dest_x >= 0); - OsrsPlayerMoveKind move_kind = OSRS_PLAYER_MOVE_NONE; - if (has_explicit_move) { - if (s->player_dest_x >= 0) move_kind = OSRS_PLAYER_MOVE_DESTINATION; - else if (move_act > 0 && move_act < ENCOUNTER_MOVE_ACTIONS) - move_kind = OSRS_PLAYER_MOVE_ACTION; - } - int player_x_before_step = s->player.x; - int player_y_before_step = s->player.y; - int npc_flag_before_step = - col_npc_collision_flag_at(s, player_x_before_step, player_y_before_step); - int interaction_before_step = osrs_interaction_active(&s->interaction) - ? s->interaction.target_slot : -1; - ColoWalkCtx wc = { s, ctx }; - OsrsLosQuery los_query = col_player_los_query(s); - OsrsPlayerStepInput step_input = { - .player = &s->player, - .interaction = &s->interaction, - .target_lookup = col_lookup_player_attack_target, - .target_ctx = s, - .has_new_target = has_new_target, - .new_target_slot = new_target_slot, - .move_kind = move_kind, - .move_action = move_act, - .dest_x = &s->player_dest_x, - .dest_y = &s->player_dest_y, - .arena = { - .collision_map = ctx->collision_map, - .world_offset_x = ctx->world_offset_x, - .world_offset_y = ctx->world_offset_y, - .is_walkable = col_tile_walkable, - .walkable_ctx = &wc, - .extra_blocked = col_pathfind_blocked, - .blocked_ctx = &wc, - .los_query = &los_query, - .arena_base_x = COLO_ARENA_MIN_X, - .arena_base_y = COLO_ARENA_MIN_Y, - .arena_w = COLO_ARENA_WIDTH, - .arena_h = COLO_ARENA_HEIGHT, - }, - }; - OsrsPlayerStepResult step_result = osrs_encounter_player_step(&step_input); - if (has_new_target && step_result.target_slot == new_target_slot) { - s->player_last_interaction_target_slot = new_target_slot; + int primary_act = actions[COLO_HEAD_PRIMARY]; + ColoWalkCtx walk_ctx; + OsrsLosQuery los_query; + ColoPrimaryStepPlan primary_plan = col_primary_step_plan( + s, ctx, &s->player, &s->interaction, primary_act, + &s->player_dest_x, &s->player_dest_y, &walk_ctx, &los_query); + OsrsPlayerStepResult step_result = + osrs_encounter_player_step(&primary_plan.input); + if (primary_plan.input.command.kind == OSRS_PLAYER_CMD_TARGET && + step_result.target_slot == primary_plan.input.command.target_slot) { + s->player_last_interaction_target_slot = primary_plan.input.command.target_slot; s->player_last_interaction_age = 0; } - if (ctx->config.action_debug_log) { - fprintf(stderr, - "colo_action tick=%d movement move_kind=%d move_action=%d " - "dest=%d,%d has_new_target=%d new_target=%d interaction=%d->%d " - "pos=%d,%d->%d,%d moved=%d explicit=%d chased=%d can_attack=%d\n", - s->tick, move_kind, move_act, s->player_dest_x, s->player_dest_y, - has_new_target, new_target_slot, interaction_before_step, - step_result.target_slot, player_x_before_step, player_y_before_step, - s->player.x, s->player.y, step_result.moved, - step_result.explicit_moved, step_result.chased_target, - step_result.can_attack); - fprintf(stderr, - "colo_action tick=%d movement_walkability npc_flag=%d->%d " - "walkable=%d->%d path_extra=%d->%d\n", - s->tick, npc_flag_before_step, - col_npc_collision_flag_at(s, s->player.x, s->player.y), - col_player_walkable(s, player_x_before_step, player_y_before_step), - col_player_walkable(s, s->player.x, s->player.y), - col_pathfind_blocked(&wc, - player_x_before_step + ctx->world_offset_x, - player_y_before_step + ctx->world_offset_y), - col_pathfind_blocked(&wc, - s->player.x + ctx->world_offset_x, - s->player.y + ctx->world_offset_y)); - } col_rebuild_player_collision_flags(s); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PLAYER_MOVE); +#endif if (ctx->config.bis_gear_oracle_mode) col_apply_bis_gear_oracle(s); @@ -1354,4 +1172,7 @@ static void col_tick_player_ctx( } if (can_attack) col_tick_thrall_and_death_charge(s, ctx); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PLAYER_ATTACK); +#endif } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index e4a62a11b2..02cb7beaa0 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -25,12 +25,6 @@ static void col_put_int_ctx( else if (strcmp(key, "curriculum_agent") == 0) ctx->config.curriculum_agent = encounter_require_binary_config("colosseum", key, value); - else if (strcmp(key, "step_out_forecast_obs_enabled") == 0) - ctx->config.step_out_forecast_obs_enabled = - encounter_require_binary_config("colosseum", key, value); - else if (strcmp(key, "threat_field_obs_enabled") == 0) - ctx->config.threat_field_obs_enabled = - encounter_require_binary_config("colosseum", key, value); else if (strcmp(key, "mask_inventory_heads") == 0) ctx->config.mask_inventory_heads = encounter_require_binary_config("colosseum", key, value); @@ -45,12 +39,6 @@ static void col_put_int_ctx( } ctx->config.farm_cap_waves = value; } - else if (strcmp(key, "action_debug_log") == 0) - ctx->config.action_debug_log = - encounter_require_binary_config("colosseum", key, value); - else if (strcmp(key, "prayer_oracle_mode") == 0) - ctx->config.prayer_oracle_mode = - encounter_require_binary_config("colosseum", key, value); else if (strcmp(key, "late_start_state_mode") == 0) { if (value < 0 || value > 2) { fprintf(stderr, "colosseum: late_start_state_mode must be 0..2, got %d\n", @@ -62,9 +50,6 @@ static void col_put_int_ctx( else if (strcmp(key, "bis_gear_oracle_mode") == 0) ctx->config.bis_gear_oracle_mode = encounter_require_binary_config("colosseum", key, value); - else if (strcmp(key, "invuln_mode") == 0) - ctx->config.invuln_mode = - encounter_require_binary_config("colosseum", key, value); else if (strcmp(key, "episode_max_ticks_override") == 0) { if (value < 0) { fprintf(stderr, "colosseum: episode_max_ticks_override must be >= 0, got %d\n", @@ -73,9 +58,6 @@ static void col_put_int_ctx( } ctx->config.episode_max_ticks_override = value; } - else if (strcmp(key, "remove_brews") == 0) - ctx->config.remove_brews = - encounter_require_binary_config("colosseum", key, value); else if (strcmp(key, "damage_scale_anneal_ticks") == 0) { if (value < 0) { fprintf(stderr, "colosseum: damage_scale_anneal_ticks must be >= 0, got %d\n", @@ -84,22 +66,6 @@ static void col_put_int_ctx( } ctx->config.damage_scale_anneal_ticks = value; } - else if (strcmp(key, "forecast_horizon") == 0) { - if (value < 1 || value > COLO_STEP_OUT_FORECAST_HORIZON) { - fprintf(stderr, "colosseum: forecast_horizon must be 1..%d, got %d\n", - COLO_STEP_OUT_FORECAST_HORIZON, value); - abort(); - } - ctx->config.forecast_horizon = value; - } - else if (strcmp(key, "forecast_run_tile_mode") == 0) { - if (value < 0 || value > COLO_FORECAST_RUN_TILE_STATIC_THREAT) { - fprintf(stderr, "colosseum: forecast_run_tile_mode must be 0..%d, got %d\n", - COLO_FORECAST_RUN_TILE_STATIC_THREAT, value); - abort(); - } - ctx->config.forecast_run_tile_mode = value; - } else if (strcmp(key, "loadout_profile_mode") == 0) { if (value < 0 || value > COLO_LOADOUT_PROFILE_MODE_MIXED) { fprintf(stderr, "colosseum: loadout_profile_mode must be 0..2, got %d\n", value); @@ -133,20 +99,6 @@ static void col_put_float_ctx( else if (strcmp(key, "wave_clear_bonus") == 0) ctx->config.wave_clear_bonus = value; else if (strcmp(key, "boss_phase_bonus") == 0) ctx->config.boss_phase_bonus = value; else if (strcmp(key, "win_bonus") == 0) ctx->config.win_bonus = value; - else if (strcmp(key, "prayer_correct_reward") == 0) - ctx->config.prayer_correct_reward = value; - else if (strcmp(key, "offpray_damage_penalty_coeff") == 0) - ctx->config.offpray_damage_penalty_coeff = value; - else if (strcmp(key, "multistyle_exposure_penalty_coeff") == 0) - ctx->config.multistyle_exposure_penalty_coeff = value; - else if (strcmp(key, "argmax_gear_reward_coeff") == 0) - ctx->config.argmax_gear_reward_coeff = value; - else if (strcmp(key, "offensive_boost_reward_coeff") == 0) - ctx->config.offensive_boost_reward_coeff = value; - else if (strcmp(key, "stall_penalty_coeff") == 0) - ctx->config.stall_penalty_coeff = value; - else if (strcmp(key, "avoided_damage_coeff") == 0) - ctx->config.avoided_damage_coeff = value; else if (strcmp(key, "death_penalty_coeff") == 0) ctx->config.death_penalty_coeff = value; else if (strcmp(key, "timeout_penalty") == 0) ctx->config.timeout_penalty = value; else if (strcmp(key, "beginner_loadout_fraction") == 0) { @@ -215,7 +167,11 @@ static void* col_get_log_ctx(EncounterState* state, EncounterContext* context) { s->log.episode_length = s->tick; s->log.wave_reached = s->wave; s->log.win = (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_WON) ? 1 : 0; - s->log.died = (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_DIED) ? 1 : 0; + /* the tick cap reports PLAYER_DIED so training keeps timeout-as-terminal, but a + truncation has no killer and no damage source, so it must not count as a death. */ + s->log.timed_out = (s->episode_over && s->time_limit_truncated) ? 1 : 0; + s->log.died = (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_DIED + && !s->time_limit_truncated) ? 1 : 0; s->log.boss_phase_reached = s->sol.phase; s->log.active_tier_sum = col_modifier_active_tier_sum(s); return &s->log; @@ -1033,17 +989,9 @@ static void col_translate_human_commands_ctx( case HUMAN_COMMAND_SPEC_TOGGLE: actions[COLO_HEAD_SPEC] = 1; break; - case HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK: { - int action_head = - col_queue_inventory_click_action(s, actions, cmd->inventory_slot); - if (ctx->config.action_debug_log) { - fprintf(stderr, - "colo_action tick=%d human_inventory_click slot=%d head=%d action=%d\n", - s->tick, cmd->inventory_slot, action_head, - action_head >= 0 ? actions[action_head] : 0); - } + case HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK: + col_queue_inventory_click_action(s, actions, cmd->inventory_slot); break; - } case HUMAN_COMMAND_ITEM_ON_ITEM: osrs_inventory_swap_cells( s->inventory_cells, cmd->inventory_slot, cmd->target_inventory_slot); @@ -1083,7 +1031,7 @@ static void col_step_human_commands_ctx( } #define COLO_SNAPSHOT_MAGIC 0xC010C001u -#define COLO_SNAPSHOT_VERSION 20u +#define COLO_SNAPSHOT_VERSION 22u typedef struct { uint32_t magic; @@ -1174,10 +1122,6 @@ static const EncounterDef ENCOUNTER_COLOSSEUM = { .snapshot = col_snapshot_ctx, .restore = col_restore_ctx, - .cell_key_size = NULL, - .write_cell_key = NULL, - .progress_score = NULL, - .write_obs = col_write_obs_ctx, .write_mask = col_write_mask_ctx, .get_reward = col_get_reward_ctx, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc index 3c2446398d..e0190237d6 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -11,6 +11,7 @@ static int col_npc_effective_size(const ColoNPC* npc) { } static void col_stamp_npc_collision_footprint(ColosseumState* s, int x, int y, int size, uint8_t v) { + s->log.npc_stamp_tiles += (float)(size * size); for (int dx = 0; dx < size; dx++) { for (int dy = 0; dy < size; dy++) { int gx, gy; @@ -20,11 +21,21 @@ static void col_stamp_npc_collision_footprint(ColosseumState* s, int x, int y, i } } +static int col_npc_blocks_other_npcs(const ColosseumState* s, ColoNpcType type) { + return !(type == COLO_MINOTAUR && col_mod_active(s, COLO_MOD_RED_FLAG)); +} + +static void col_stamp_npc_collision(ColosseumState* s, const ColoNPC* npc, uint8_t v) { + if (!col_npc_blocks_other_npcs(s, npc->type)) return; + col_stamp_npc_collision_footprint( + s, npc->x, npc->y, col_npc_effective_size(npc), v); +} + static void col_rebuild_player_collision_flags(ColosseumState* s) { - memset(s->player_collision_flags, 0, sizeof(s->player_collision_flags)); - int gx, gy; - if (col_grid_index(s->player.x, s->player.y, &gx, &gy)) - s->player_collision_flags[gx][gy] = 1; + if (!col_grid_index(s->player.x, s->player.y, &s->player_grid_x, &s->player_grid_y)) { + s->player_grid_x = -1; + s->player_grid_y = -1; + } } static int col_find_free_npc(ColosseumState* s) { @@ -50,11 +61,18 @@ static void col_record_score_spawn(ColosseumState* s, ColoNpcType type) { static void col_deactivate_npc(ColosseumState* s, int idx) { ColoNPC* npc = &s->npcs[idx]; - if (!col_type_is_hazard_entity(npc->type) && !col_type_is_warbander(npc->type)) { - int size = col_npc_effective_size(npc); - col_stamp_npc_collision_footprint(s, npc->x, npc->y, size, 0); - } + if (!col_type_is_hazard_entity(npc->type) && !col_type_is_warbander(npc->type)) + col_stamp_npc_collision(s, npc, 0); npc->active = 0; + /* The per-tick reset skips inactive slots, so a slot leaving play clears its own tick + * flags once here instead of every tick forever. */ + npc->attacked_this_tick = 0; + npc->attack_style_this_tick = ATTACK_STYLE_NONE; + npc->moved_this_tick = 0; + npc->moved_last_tick = 0; + npc->hit_landed_this_tick = 0; + npc->hit_damage = 0; + npc->hit_was_successful_this_tick = 0; } static void col_roll_manticore_orb_cycle(ColosseumState* s, AttackStyle out[3]) { @@ -115,7 +133,7 @@ static void col_init_npc(ColosseumState* s, int idx, ColoNpcType type, int x, in if (!col_type_is_hazard_entity(type) && !col_type_is_warbander(type) && col_npc_effective_size(npc) > 0) - col_stamp_npc_collision_footprint(s, x, y, col_npc_effective_size(npc), 1); + col_stamp_npc_collision(s, npc, 1); } static int col_spawn_npc_at(ColosseumState* s, ColoNpcType type, int x, int y) { @@ -420,6 +438,7 @@ static void col_sync_consumable_counters_from_inventory(ColosseumState* s) { break; case OSRS_CONSUMABLE_SUPER_RESTORE: case OSRS_CONSUMABLE_SANFEW: + case OSRS_CONSUMABLE_PRAYER_RESTORE: restore_doses += cell->dose; break; case OSRS_CONSUMABLE_SUPER_COMBAT: @@ -442,6 +461,11 @@ static void col_sync_consumable_counters_from_inventory(ColosseumState* s) { case OSRS_CONSUMABLE_SHARK_FOOD: case OSRS_CONSUMABLE_KARAMBWAN: break; + case OSRS_CONSUMABLE_BASTION: + case OSRS_CONSUMABLE_STAMINA: + fprintf(stderr, "colosseum: consumable kind %d not in colosseum kits\n", + (int)resolution.consumable_kind); + abort(); } } s->player.brew_doses = brew_doses; @@ -452,17 +476,6 @@ static void col_sync_consumable_counters_from_inventory(ColosseumState* s) { s->surge_doses = surge_doses; } -static void col_strip_brews_from_inventory(ColosseumState* s) { - for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - ColoInvCell* cell = &s->inventory_cells[i]; - if (cell->dose == 0) continue; - OsrsInventoryClickResolution resolution = - osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); - if (resolution.consumable_kind == OSRS_CONSUMABLE_BREW) - *cell = osrs_inventory_cell_empty(); - } -} - static void col_reset_ctx(EncounterState* state, EncounterContext* context, uint32_t seed) { col_build_npc_stats(); ColosseumState* s = (ColosseumState*)state; @@ -480,13 +493,16 @@ static void col_reset_ctx(EncounterState* state, EncounterContext* context, uint s->player_dest_x = -1; s->player_dest_y = -1; s->last_hit_by_type = -1; + s->last_damage_source = -1; + s->last_doom_damage_source = -1; + s->sol.boss_idx = -1; s->min_sol_hp_seen = COLO_SOL_HP_MAX; s->active_player_damage_received_scale = ctx->config.player_damage_received_scale; for (int i = 0; i < COLO_OBS_NPCS; i++) s->current_obs_slots[i] = -1; s->player.entity_type = ENTITY_PLAYER; - encounter_init_maxed_player_combat_stats(&s->player, 99); + encounter_init_maxed_player_combat_stats(&s->player, COLO_PLAYER_PRAYER_LEVEL); osrs_item_effect_state_init(&s->player.item_effect_state); s->weapon_set = COLO_GEAR_MELEE; @@ -513,7 +529,6 @@ static void col_reset_ctx(EncounterState* state, EncounterContext* context, uint memset(s->player.inventory, ITEM_NONE, sizeof(s->player.inventory)); memset(s->player.num_items_in_slot, 0, sizeof(s->player.num_items_in_slot)); col_seed_inventory_cells_from_gameplay_loadout(s); - if (ctx->config.remove_brews) col_strip_brews_from_inventory(s); const ColoSupplyLoadout* supplies = &COLO_SUPPLY_LOADOUTS[s->active_loadout_profile]; s->full_supplies = *supplies; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc index b3d8a1d39a..4cbb3ccbe7 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc @@ -5,11 +5,32 @@ static float col_expected_dpt_for_equipment_vs_npc( ColosseumState* s, const uint8_t equipment[NUM_GEAR_SLOTS], const ColoNPC* npc, int use_style_matched_prayer); -static float col_terminal_death_reward(const ColosseumContext* ctx) { +/* The trainer hard-clamps every per-step reward to [-1,1] before PPO, so any tick whose + * terms sum past 1 delivers a signal the agent never sees. Measured here rather than + * bounded per-coefficient, because the terms stack: a wave-12 clear carries the killing + * blow's damage reward, wave_clear_bonus and win_bonus on one tick. */ +static void col_record_reward_clamp_headroom(ColosseumState* s) { + float mag = fabsf(s->reward); + s->log.reward_steps += 1.0f; + if (mag > s->log.reward_raw_peak) s->log.reward_raw_peak = mag; + if (mag <= 1.0f) return; + float over = mag - 1.0f; + s->log.reward_clamped_steps += 1.0f; + s->log.reward_clamp_loss += over; + if (s->tick_scratch.wave_completed) s->log.clamp_loss_wave_clear += over; + /* episode_over guards the winner read: COLO_OUTCOME_PLAYER_WON is 0, which is also + * what winner holds on every non-terminal tick. */ + if (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_WON) + s->log.clamp_loss_win += over; +} + +static float col_terminal_death_reward(ColosseumState* s, const ColosseumContext* ctx) { + s->log.rew_death -= ctx->config.death_penalty_coeff; return -ctx->config.death_penalty_coeff; } -static float col_terminal_timeout_reward(const ColosseumContext* ctx) { +static float col_terminal_timeout_reward(ColosseumState* s, const ColosseumContext* ctx) { + s->log.rew_timeout -= ctx->config.timeout_penalty; return -ctx->config.timeout_penalty; } @@ -61,6 +82,11 @@ static float col_current_wave_score_progress(const ColosseumState* s) { return progress; } +/* A loss caps here, a win scores 1.0. At the old 0.99 a policy that always reached + Sol's last phase and never won scored ~0.973, so winning was worth under 3% and the + sweep optimised depth instead. */ +#define COLO_SCORE_LOSS_CEILING 0.5f + static float col_episode_outcome_score(const ColosseumState* s) { if (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_WON) return 1.0f; int waves_cleared = s->log.waves_cleared; @@ -69,7 +95,7 @@ static float col_episode_outcome_score(const ColosseumState* s) { col_current_wave_score_progress(s) + col_sol_score_progress(s); float ratio = depth / (float)COLO_NUM_WAVES; - return 0.99f * ratio * ratio; + return COLO_SCORE_LOSS_CEILING * ratio * ratio; } static void col_record_terminal_outcome_score(ColosseumState* s) { @@ -86,68 +112,32 @@ static float col_episode_max_wave_depth(const ColosseumState* s) { col_sol_score_progress(s); } -static int col_distinct_live_threat_styles(const ColosseumState* s) { - int magic, ranged, melee; - col_live_threat_style_counts(s, &magic, &ranged, &melee); - return (magic > 0) + (ranged > 0) + (melee > 0); -} - -static float col_rewardable_avoided_damage(ColosseumState* s) { - ColoTickScratch* t = &s->tick_scratch; - if (__builtin_popcount((unsigned)t->styles_faced_mask) != 1) return 0.0f; - int style = __builtin_ctz((unsigned)t->styles_faced_mask); - float raw_avoided = t->avoidable_threat_by_style[style] - t->offpray_damage_this_tick; - if (raw_avoided <= 0.0f) return 0.0f; - float progress_budget = s->log.total_damage_dealt - s->avoided_damage_already_rewarded; - if (progress_budget <= 0.0f) return 0.0f; - return raw_avoided < progress_budget ? raw_avoided : progress_budget; -} - -static float col_attacked_gear_quality_ratio(ColosseumState* s) { - if (!s->tick_scratch.player_attacked) return -1.0f; - int npc_slot = s->player_attack_npc_idx; - if (npc_slot < 0 || npc_slot >= COLO_MAX_NPCS) return -1.0f; - const ColoNPC* npc = &s->npcs[npc_slot]; - if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) - return -1.0f; - const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = col_get_best_gear_table(s); - float best_dpt = 0.0f; - for (int set = 0; set < COLO_NUM_WEAPON_SETS; set++) { - float d = best[set][npc->type].dpt; - if (d > best_dpt) best_dpt = d; - } - if (best_dpt <= 0.0f) return -1.0f; - - float actual = col_expected_dpt_for_equipment_vs_npc(s, s->player.equipped, npc, 1); - float ratio = actual / best_dpt; - if (ratio < 0.0f) ratio = 0.0f; - if (ratio > 1.0f) ratio = 1.0f; - return ratio; -} - -static int col_attacked_with_offensive_boost(ColosseumState* s) { - if (!s->tick_scratch.player_attacked) return -1; - int npc_slot = s->player_attack_npc_idx; - if (npc_slot < 0 || npc_slot >= COLO_MAX_NPCS) return -1; - const ColoNPC* npc = &s->npcs[npc_slot]; - if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) - return -1; - AttackStyle style = (AttackStyle)s->player_attack_style_id; - int cur_att = 0, cur_str = 0; - col_current_style_levels(&s->player, style, &cur_att, &cur_str); - int base_att, base_str; - if (style == ATTACK_STYLE_RANGED) { - base_att = base_str = s->player.base_ranged; - } else if (style == ATTACK_STYLE_MAGIC) { - base_att = base_str = s->player.base_magic; - } else { - base_att = s->player.base_attack; - base_str = s->player.base_strength; +/* Damage the player was thrown, could have prayed, and did pray away this tick. Only one + * overhead can be up, so `best` is the most any policy could have stopped and the remainder + * is structurally unrecoverable -- crediting it would pay for luck, not play. */ +typedef struct { + float total; + float best; + float achieved; +} ColoTickAvoidance; + +static ColoTickAvoidance col_tick_avoidance(const ColoTickScratch* t) { + ColoTickAvoidance a = {0.0f, 0.0f, 0.0f}; + for (int i = 0; i < 4; i++) { + float v = t->avoidable_threat_by_style[i]; + a.total += v; + if (v > a.best) a.best = v; } - return (cur_att > base_att || cur_str > base_str) ? 1 : 0; + a.achieved = a.total - t->offpray_damage_this_tick; + if (a.achieved < 0.0f) a.achieved = 0.0f; + if (a.achieved > a.best) a.achieved = a.best; + return a; } -static float col_compute_reward_ctx(ColosseumState* s, const ColosseumContext* ctx) { +/* Split from the reward so the three terminal paths, which compute a shaped reward and then + * throw it away in favour of a terminal penalty, still record their tick statistics without + * crediting reward the agent never receives. */ +static void col_accumulate_tick_stats(ColosseumState* s, const ColosseumContext* ctx) { const ColConfig* cfg = &ctx->config; ColoTickScratch* t = &s->tick_scratch; @@ -163,41 +153,55 @@ static float col_compute_reward_ctx(ColosseumState* s, const ColosseumContext* c s->log.total_npc_attacks += t->attacks_fired; col_update_min_sol_hp_seen(s); if (s->sol.phase > s->log.boss_phase_reached) s->log.boss_phase_reached = s->sol.phase; + if (s->wave < cfg->farm_cap_waves) + s->log.farm_damage += t->fresh_damage_reinforcement; + + /* Headroom split for a defensive reward. Only ONE overhead can be up, so on a tick where + * two styles land only the larger can ever be prayed: the remainder is structurally + * unavoidable and no reward shape can recover it. Separating that from what the policy + * simply misplayed is the number that decides whether a defensive term is worth building. */ + ColoTickAvoidance av = col_tick_avoidance(t); + s->log.avoid_total += av.total; + s->log.avoid_achieved += av.achieved; + s->log.avoid_missed += av.best - av.achieved; + s->log.avoid_impossible += av.total - av.best; + s->log.dmg_unprayable += t->landed_unprayable_damage; +} + +/* Returned by value rather than logged in place: a tick that ends in death, timeout or a win + * overwrites the shaped reward with a terminal one, and the per-term log must record what was + * actually paid. col_commit_reward_terms is the single place that credits the log. */ +typedef struct { + float damage; + float boss_phase; +} ColoShapedReward; + +static ColoShapedReward col_shaped_reward(ColosseumState* s, const ColosseumContext* ctx) { + const ColConfig* cfg = &ctx->config; + ColoTickScratch* t = &s->tick_scratch; + ColoShapedReward out = {0.0f, 0.0f}; - float reward = 0.0f; float rewarded_fresh = t->fresh_damage_dealt; - if (s->wave < cfg->farm_cap_waves) { - s->log.farm_damage += t->fresh_damage_reinforcement; - if (cfg->farm_safe_damage_cap) - rewarded_fresh -= t->fresh_damage_reinforcement; - } - reward += cfg->damage_reward_coeff * rewarded_fresh; - reward += cfg->boss_damage_reward_coeff * t->fresh_damage_boss; - reward += cfg->prayer_correct_reward * (float)t->prayer_correct; - reward -= cfg->offpray_damage_penalty_coeff * t->offpray_damage_this_tick; - if (cfg->multistyle_exposure_penalty_coeff != 0.0f) { - int excess_styles = col_distinct_live_threat_styles(s) - 1; - if (excess_styles > 0) - reward -= cfg->multistyle_exposure_penalty_coeff * (float)excess_styles; - } - if (cfg->argmax_gear_reward_coeff != 0.0f) { - float gear_quality = col_attacked_gear_quality_ratio(s); - if (gear_quality >= 0.0f) - reward += cfg->argmax_gear_reward_coeff * gear_quality; - } - if (cfg->offensive_boost_reward_coeff != 0.0f) { - if (col_attacked_with_offensive_boost(s) > 0) - reward += cfg->offensive_boost_reward_coeff; - } - float rewardable_avoided = col_rewardable_avoided_damage(s); - reward += cfg->avoided_damage_coeff * rewardable_avoided; - s->avoided_damage_already_rewarded += rewardable_avoided; + if (s->wave < cfg->farm_cap_waves && cfg->farm_safe_damage_cap) + rewarded_fresh -= t->fresh_damage_reinforcement; + out.damage = cfg->damage_reward_coeff * rewarded_fresh + + cfg->boss_damage_reward_coeff * t->fresh_damage_boss; if (s->sol.started && s->sol.phase > s->rewarded_boss_phase) { - reward += cfg->boss_phase_bonus * (float)(s->sol.phase - s->rewarded_boss_phase); + out.boss_phase = + cfg->boss_phase_bonus * (float)(s->sol.phase - s->rewarded_boss_phase); s->rewarded_boss_phase = s->sol.phase; } - return reward; + return out; +} + +static float col_shaped_total(ColoShapedReward r) { + return r.damage + r.boss_phase; +} + +static void col_commit_reward_terms(ColosseumState* s, ColoShapedReward r) { + s->log.rew_damage += r.damage; + s->log.rew_boss_phase += r.boss_phase; } static float col_inventory_heal_remaining(const ColosseumState* s) { @@ -219,6 +223,18 @@ static float col_inventory_heal_remaining(const ColosseumState* s) { static void col_record_death_attribution(ColosseumState* s) { int kt = s->last_hit_by_type; if (kt >= 0 && kt < COLO_NUM_NPC_TYPES) s->log.death_by_type[kt] += 1.0f; + if (s->last_damage_source == COLO_DAMAGE_NPC_ATTACK && + kt >= 0 && + kt < COLO_NUM_NPC_TYPES) { + s->log.npc_attack_death_by_type[kt] += 1.0f; + } + if (s->last_damage_source >= 0 && + s->last_damage_source < COLO_NUM_DAMAGE_SOURCES) + s->log.death_by_source[s->last_damage_source] += 1.0f; + if (col_mod_doom_lethal(s) && + s->last_doom_damage_source >= 0 && + s->last_doom_damage_source < COLO_NUM_DAMAGE_SOURCES) + s->log.doom_death_by_source[s->last_doom_damage_source] += 1.0f; s->log.death_fatal_damage += s->tick_scratch.damage_received; if (__builtin_popcount(s->tick_scratch.styles_faced_mask) >= 2) s->log.death_on_conflict_tick += 1.0f; @@ -238,121 +254,196 @@ static void col_classify_tick_offpray(ColosseumState* s) { s->log.offpray_damage_solo += d; } -static void col_step_ctx(EncounterState* state, EncounterContext* context, const int* actions) { - ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); - if (s->episode_over) return; - +typedef struct { + int player_phase_reached; + int spawn_wave_now; + int in_wave_gap; + int in_ready_gap; +} ColoPrePlayerPhase; + +static ColoPrePlayerPhase col_advance_pre_player_phase_ctx( + ColosseumState* s, + ColosseumContext* ctx, + const int* actions +) { + ColoPrePlayerPhase phase = {0}; +#ifdef COLO_PROFILE_ENABLED + int col_prof_enabled = + COLO_PROFILE_ENABLED() && !s->profiling_suppressed; + double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; +#endif s->reward = 0.0f; s->tick_scratch = (ColoTickScratch){0}; s->active_player_damage_received_scale = ctx->config.player_damage_received_scale; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PRE_RESET_SCRATCH); +#endif col_context_clear_render_events(ctx); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PRE_RESET_RENDER); +#endif s->player_attack_timing = (EncounterProjectileTiming){0}; encounter_clear_tick_flags(&s->player); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PRE_RESET_PLAYER); +#endif + /* Mean live NPCs is 4.4 of 24, and the tick-flag fields sit in their own cache line + * per NPC, so clearing all 24 touched 24 lines to reset ~4. col_deactivate_npc clears + * a slot on the way out, so skipping inactive slots here is equivalent. */ for (int i = 0; i < COLO_MAX_NPCS; i++) { - s->npcs[i].attacked_this_tick = 0; - s->npcs[i].attack_style_this_tick = ATTACK_STYLE_NONE; - s->npcs[i].moved_last_tick = s->npcs[i].moved_this_tick; - s->npcs[i].moved_this_tick = 0; - s->npcs[i].hit_landed_this_tick = 0; - s->npcs[i].hit_damage = 0; - s->npcs[i].hit_was_successful_this_tick = 0; + ColoNPC* npc = &s->npcs[i]; + if (!npc->active) continue; + npc->attacked_this_tick = 0; + npc->attack_style_this_tick = ATTACK_STYLE_NONE; + npc->moved_last_tick = npc->moved_this_tick; + npc->moved_this_tick = 0; + npc->hit_landed_this_tick = 0; + npc->hit_damage = 0; + npc->hit_was_successful_this_tick = 0; } s->tick++; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PRE_RESET_NPCS); +#endif if (s->tick >= s->episode_max_ticks) { - (void)col_compute_reward_ctx(s, ctx); + col_accumulate_tick_stats(s, ctx); s->episode_over = 1; s->time_limit_truncated = 1; s->winner = COLO_OUTCOME_PLAYER_DIED; - s->reward = col_terminal_timeout_reward(ctx); + s->reward = col_terminal_timeout_reward(s, ctx); col_record_terminal_outcome_score(s); s->log.max_wave_depth = col_episode_max_wave_depth(s); + col_record_reward_clamp_headroom(s); s->episode_return += s->reward; - return; + return phase; } col_player_pretick(s, ctx, actions); - if (ctx->config.prayer_oracle_mode) col_apply_prayer_oracle(s); s->player.prayer_display = s->player.prayer; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PRE_PLAYER_PRETICK); +#endif if (s->wave_ready_delay > 0) s->wave_ready_delay--; if (s->wave_attack_delay > 0) s->wave_attack_delay--; - int spawn_wave_now = 0; if (s->wave_spawn_delay > 0) { s->wave_spawn_delay--; - if (s->wave_spawn_delay == 0) spawn_wave_now = 1; + if (s->wave_spawn_delay == 0) phase.spawn_wave_now = 1; } - if (spawn_wave_now) { + if (phase.spawn_wave_now) { s->wave = s->wave_spawn_target; col_spawn_wave(s); } - int in_wave_gap = (s->wave_spawn_delay > 0 || spawn_wave_now || - s->modifiers.draft_pending); - int in_ready_gap = s->wave_ready_delay > 0; + phase.in_wave_gap = + s->wave_spawn_delay > 0 || + phase.spawn_wave_now || + s->modifiers.draft_pending; + phase.in_ready_gap = s->wave_ready_delay > 0; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PRE_SPAWN); +#endif col_resolve_player_projectiles_on_npcs_ctx(s, ctx); col_resolve_player_pending_hits_ctx(s, ctx); col_tick_npc_death_lingers(s); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PRE_RESOLVE_HITS); +#endif - if (!in_wave_gap && !in_ready_gap) { + if (!phase.in_wave_gap && !phase.in_ready_gap) { if (s->wave_attack_delay == 0 && s->warband_cycle_anchor < 0) s->warband_cycle_anchor = s->tick - 1; col_tick_live_stat_drift_and_divines(s); col_rebuild_player_collision_flags(s); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PRE_COLLISION); +#endif col_tick_npcs_ctx(s, ctx); + if (s->wave_attack_delay == 0) col_warband_attack_phase(s, ctx); col_modifiers_tick(s); if (s->surge_cooldown > 0) s->surge_cooldown--; } +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PRE_NPC_PHASE); +#endif col_classify_tick_offpray(s); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_PRE_OFFPRAY); +#endif - if (ctx->config.invuln_mode) { - s->player.current_hitpoints = s->player.base_hitpoints; - } else if (s->player.current_hitpoints <= 0 || col_mod_doom_lethal(s)) { - (void)col_compute_reward_ctx(s, ctx); + if (s->player.current_hitpoints <= 0 || col_mod_doom_lethal(s)) { + col_accumulate_tick_stats(s, ctx); col_record_death_attribution(s); s->episode_over = 1; s->winner = COLO_OUTCOME_PLAYER_DIED; - s->reward = col_terminal_death_reward(ctx); + s->reward = col_terminal_death_reward(s, ctx); col_record_terminal_outcome_score(s); s->log.max_wave_depth = col_episode_max_wave_depth(s); + col_record_reward_clamp_headroom(s); s->episode_return += s->reward; - return; + return phase; } + phase.player_phase_reached = 1; + return phase; +} - int can_player_attack = !in_wave_gap; +static void col_step_ctx(EncounterState* state, EncounterContext* context, const int* actions) { + ColosseumState* s = (ColosseumState*)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + if (s->episode_over) return; +#ifdef COLO_PROFILE_ENABLED + int col_prof_enabled = + COLO_PROFILE_ENABLED() && !s->profiling_suppressed; + double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; +#endif + + ColoPrePlayerPhase phase = + col_advance_pre_player_phase_ctx(s, ctx, actions); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_PRE_PLAYER); +#endif + if (!phase.player_phase_reached) return; + + int can_player_attack = !phase.in_wave_gap; int px = s->player.x, py = s->player.y; col_tick_player_ctx(s, ctx, actions, can_player_attack); s->tick_scratch.player_moved = (s->player.x != px || s->player.y != py) ? 1 : 0; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_PLAYER); +#endif + + col_accumulate_tick_stats(s, ctx); + ColoShapedReward shaped = col_shaped_reward(s, ctx); + s->reward = col_shaped_total(shaped); + int shaped_paid = 1; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_REWARD); +#endif - if (!in_wave_gap && !in_ready_gap && s->wave_attack_delay == 0) - col_warband_attack_phase(s, ctx); - - s->reward = col_compute_reward_ctx(s, ctx); - - if (can_player_attack && ctx->config.stall_penalty_coeff != 0.0f) - s->reward -= ctx->config.stall_penalty_coeff; int all_dead = 1; int wave_cleared = 0; - if (ctx->config.invuln_mode) { - s->player.current_hitpoints = s->player.base_hitpoints; - } else if (s->player.current_hitpoints <= 0 || col_mod_doom_lethal(s)) { + if (s->player.current_hitpoints <= 0 || col_mod_doom_lethal(s)) { col_record_death_attribution(s); s->episode_over = 1; s->winner = COLO_OUTCOME_PLAYER_DIED; - s->reward = col_terminal_death_reward(ctx); + s->reward = col_terminal_death_reward(s, ctx); + shaped_paid = 0; goto finish_step; } - if (spawn_wave_now || s->wave_spawn_delay > 0 || s->modifiers.draft_pending) + if (phase.spawn_wave_now || + s->wave_spawn_delay > 0 || + s->modifiers.draft_pending) { goto finish_step; + } for (int i = 0; i < COLO_MAX_NPCS; i++) { if (col_npc_is_live_enemy(&s->npcs[i])) { @@ -371,13 +462,20 @@ static void col_step_ctx(EncounterState* state, EncounterContext* context, const if (wave_cleared) { s->tick_scratch.wave_completed = 1; + col_modifiers_on_wave_cleared(s); s->log.waves_cleared++; - s->reward += ctx->config.wave_clear_bonus; if (s->wave + 1 >= COLO_NUM_WAVES) { s->episode_over = 1; s->winner = COLO_OUTCOME_PLAYER_WON; - s->reward += ctx->config.win_bonus; + /* Terminal, so paid as an assignment exactly like death and timeout. Stacking + * the win onto the same tick's shaped reward AND wave clear pushed the tick past + * the trainer's [-1,1] clamp and destroyed 59% of the win bonus (measured). */ + s->reward = ctx->config.win_bonus; + s->log.rew_win += ctx->config.win_bonus; + shaped_paid = 0; } else { + s->reward += ctx->config.wave_clear_bonus; + s->log.rew_wave_clear += ctx->config.wave_clear_bonus; s->wave_spawn_target = s->wave + 1; col_record_wave_entry(s, s->wave + 1); @@ -389,11 +487,16 @@ static void col_step_ctx(EncounterState* state, EncounterContext* context, const } finish_step: + if (shaped_paid) col_commit_reward_terms(s, shaped); if (s->episode_over) { col_record_terminal_outcome_score(s); s->log.max_wave_depth = col_episode_max_wave_depth(s); } + col_record_reward_clamp_headroom(s); s->episode_return += s->reward; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_WAVE_LOGIC); +#endif } static void col_step(EncounterState* state, const int* actions) { diff --git a/ocean/osrs/encounters/encounter_colosseum.h b/ocean/osrs/encounters/encounter_colosseum.h index d4d872275a..98229afdd3 100644 --- a/ocean/osrs/encounters/encounter_colosseum.h +++ b/ocean/osrs/encounters/encounter_colosseum.h @@ -14,6 +14,8 @@ #include "../osrs_encounter_visual_events.h" #include "../osrs_player_consumables.h" #include "../osrs_inventory_clicks.h" +#include "../osrs_colosseum_item_obs_generated.h" +#include "../osrs_inventory_actions.h" #include "../osrs_interaction.h" #include "../data/npc_models.h" #include diff --git a/ocean/osrs/encounters/encounter_inferno.h b/ocean/osrs/encounters/encounter_inferno.h index 385e8f4983..8b14f4b0d8 100644 --- a/ocean/osrs/encounters/encounter_inferno.h +++ b/ocean/osrs/encounters/encounter_inferno.h @@ -12,6 +12,9 @@ #include "../osrs_encounter.h" #include "../osrs_encounter_player.h" #include "../osrs_encounter_visual_events.h" +#include "../osrs_player_consumables.h" +#include "../osrs_inventory_clicks.h" +#include "../osrs_inventory_actions.h" #include "../osrs_interaction.h" #include "../data/npc_models.h" #include diff --git a/ocean/osrs/encounters/encounter_nh_pvp.h b/ocean/osrs/encounters/encounter_nh_pvp.h index c4e6edc2ea..a3d8cc55fa 100644 --- a/ocean/osrs/encounters/encounter_nh_pvp.h +++ b/ocean/osrs/encounters/encounter_nh_pvp.h @@ -295,15 +295,15 @@ static const EncounterDef ENCOUNTER_NH_PVP = { .put_float = nh_pvp_put_float, .put_ptr = nh_pvp_put_ptr, - .render_post_tick = NULL, - .get_log = nh_pvp_get_log, - .get_tick = nh_pvp_get_tick, - .get_winner = nh_pvp_get_winner, - .translate_human_input = NULL, .head_move = -1, .head_prayer = -1, .head_target = -1, + + .render_post_tick = NULL, + .get_log = nh_pvp_get_log, + .get_tick = nh_pvp_get_tick, + .get_winner = nh_pvp_get_winner, }; __attribute__((constructor)) diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h index 80e432438a..42852db6a1 100644 --- a/ocean/osrs/encounters/encounter_zulrah.h +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -5,6 +5,7 @@ #include "../osrs_encounter_player.h" #include "../osrs_encounter_visual_events.h" #include "../osrs_interaction.h" +#include "../osrs_inventory_actions.h" #include "../osrs_types.h" #include "../osrs_items.h" #include "../osrs_combat.h" @@ -102,32 +103,36 @@ static const int ZUL_POSITIONS[ZUL_NUM_POSITIONS][2] = { #define ZUL_PLAYER_RESTORE_DOSES 8 #define ZUL_MAX_TICKS 600 -#define ZUL_NUM_OBS 123 -#define ZUL_NUM_ACTION_HEADS 7 +#define ZUL_NUM_SCALAR_OBS 118 +#define ZUL_NUM_OBS (ZUL_NUM_SCALAR_OBS + \ + OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES + \ + NUM_GEAR_SLOTS * OSRS_EQUIPPED_SELF_OBS_FEATURES) +#define ZUL_NUM_ACTION_HEADS (2 + NUM_GEAR_SLOTS + 2 + 2) + +#define ZUL_OBS_NPC_SLOTS (1 + ZUL_MAX_SNAKELINGS) #define ZUL_MOVE_DIM ENCOUNTER_MOVE_ACTIONS -#define ZUL_ATTACK_DIM 3 +#define ZUL_PRIMARY_DIM (ZUL_MOVE_DIM + ZUL_OBS_NPC_SLOTS) #define ZUL_PRAYER_DIM ENCOUNTER_OVERHEAD_DIM_PVE #define ZUL_OFFENSIVE_DIM ENCOUNTER_OFFENSIVE_DIM -#define ZUL_FOOD_DIM 3 -#define ZUL_POTION_DIM 3 -#define ZUL_SPEC_DIM 2 - -#define ZUL_ACTION_MASK_SIZE (ZUL_MOVE_DIM + ZUL_ATTACK_DIM + ZUL_PRAYER_DIM + \ - ZUL_FOOD_DIM + ZUL_POTION_DIM + ZUL_SPEC_DIM + ZUL_OFFENSIVE_DIM) - -#define ZUL_HEAD_MOVE 0 -#define ZUL_HEAD_ATTACK 1 -#define ZUL_HEAD_PRAYER 2 -#define ZUL_HEAD_FOOD 3 -#define ZUL_HEAD_POTION 4 -#define ZUL_HEAD_SPEC 5 -#define ZUL_HEAD_OFFENSIVE 6 - +#define ZUL_INV_CLICK_HEADS (NUM_GEAR_SLOTS + 2) +#define ZUL_INV_CLICK_DIM (OSRS_INVENTORY_SIZE + 1) +#define ZUL_SPEC_DIM 3 + +#define ZUL_ACTION_MASK_SIZE (ZUL_PRIMARY_DIM + ZUL_PRAYER_DIM + \ + ZUL_INV_CLICK_HEADS * ZUL_INV_CLICK_DIM + ZUL_SPEC_DIM + ZUL_OFFENSIVE_DIM) + +#define ZUL_HEAD_PRIMARY 0 +#define ZUL_HEAD_PRAYER 1 +#define ZUL_HEAD_EQUIP_BASE 2 +#define ZUL_HEAD_EQUIP_SLOT(slot) (ZUL_HEAD_EQUIP_BASE + (slot)) +#define ZUL_HEAD_EAT (ZUL_HEAD_EQUIP_BASE + NUM_GEAR_SLOTS) +#define ZUL_HEAD_DRINK (ZUL_HEAD_EAT + 1) +#define ZUL_HEAD_SPEC (ZUL_HEAD_DRINK + 1) +#define ZUL_HEAD_OFFENSIVE (ZUL_HEAD_SPEC + 1) + +#define ZUL_PRIMARY_ATTACK_BASE ZUL_MOVE_DIM #define ZUL_MOVE_STAY 0 -#define ZUL_ATK_NONE 0 -#define ZUL_ATK_MAGE 1 -#define ZUL_ATK_RANGE 2 typedef enum { ZUL_FORM_GREEN = 0, @@ -141,11 +146,6 @@ static const int ZUL_FORM_MONSTER_IDX[] = { [ZUL_FORM_BLUE] = MON_ZULRAH_BLUE, }; -typedef enum { - ZUL_GEAR_MAGE = 0, - ZUL_GEAR_RANGE, -} ZulrahGearStyle; - typedef enum { ZA_END = 0, ZA_RANGED, @@ -287,10 +287,20 @@ static const int ZUL_ROT_LENGTHS[ZUL_NUM_ROTATIONS] = { 11, 11, 12, 13 }; #undef ZA #undef ZE -static const int ZUL_ACTION_HEAD_DIMS[ZUL_NUM_ACTION_HEADS] = { - ZUL_MOVE_DIM, ZUL_ATTACK_DIM, ZUL_PRAYER_DIM, - ZUL_FOOD_DIM, ZUL_POTION_DIM, ZUL_SPEC_DIM, ZUL_OFFENSIVE_DIM, -}; +#define ZUL_ACTION_DIMS_INIT { \ + ZUL_PRIMARY_DIM, \ + ZUL_PRAYER_DIM, \ + ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, \ + ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, \ + ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, \ + ZUL_INV_CLICK_DIM, \ + ZUL_INV_CLICK_DIM, \ + ZUL_SPEC_DIM, \ + ZUL_OFFENSIVE_DIM \ +} +static const int ZUL_ACTION_HEAD_DIMS[ZUL_NUM_ACTION_HEADS] = ZUL_ACTION_DIMS_INIT; +static_assert(ZUL_HEAD_OFFENSIVE == ZUL_NUM_ACTION_HEADS - 1, + "OFFENSIVE must be the last zulrah action head"); #define ZUL_NUM_GEAR_TIERS 3 #define ZUL_GEAR_TIER_FIXED 0 @@ -414,6 +424,10 @@ typedef struct { int melee_target_x, melee_target_y; int melee_pending; int melee_stare_timer; + OverheadPrayer melee_prayer_at_calc; + + EncounterPendingHitQueue player_pending_hits; + EncounterPendingHitQueue zulrah_pending_hits; int phase_timer; int surface_timer; @@ -428,7 +442,7 @@ typedef struct { ZulrahPendingCloud pending_clouds[ZUL_MAX_PENDING_CLOUDS]; ZulrahSnakeling snakelings[ZUL_MAX_SNAKELINGS]; - ZulrahGearStyle player_gear; + OsrsInventoryCell inventory_cells[OSRS_INVENTORY_SIZE]; OsrsInteraction interaction; int player_dest_x, player_dest_y; int player_dest_explicit; @@ -446,10 +460,9 @@ typedef struct { float gear_tier_weights[ZUL_NUM_GEAR_TIERS]; ZulrahRewardConfig reward_config; - EncounterLoadoutStats mage_stats; - EncounterLoadoutStats range_stats; + EncounterLoadoutStats live_stats; + int live_stats_dirty; int human_command_mode; - EncounterLoadoutStats human_loadout_stats; const HumanCommand* human_commands; int human_command_count; @@ -598,7 +611,7 @@ static void zul_update_npc_anim_lifetime(ZulrahState* s) { s->zulrah_anim_event_tick = -1; } -static const EncounterLoadoutStats* zul_current_loadout_stats(ZulrahState* s, int is_mage); +static const EncounterLoadoutStats* zul_live_stats(ZulrahState* s); static int zul_lookup_player_attack_target( void* ctx, @@ -606,15 +619,27 @@ static int zul_lookup_player_attack_target( OsrsAttackTarget* out ) { ZulrahState* s = (ZulrahState*)ctx; - if (target_slot != 0 || !s->zulrah_visible || s->is_diving) - return 0; - const EncounterLoadoutStats* ls = zul_current_loadout_stats( - s, s->player_gear == ZUL_GEAR_MAGE); + const EncounterLoadoutStats* ls = zul_live_stats(s); + if (target_slot == 0) { + if (!s->zulrah_visible || s->is_diving) return 0; + *out = (OsrsAttackTarget){ + .slot = 0, + .x = s->zulrah.x, + .y = s->zulrah.y, + .size = ZUL_NPC_SIZE, + .attack_range = ls->attack_range, + }; + return 1; + } + int snakeling_idx = target_slot - 1; + if (snakeling_idx < 0 || snakeling_idx >= ZUL_MAX_SNAKELINGS) return 0; + const ZulrahSnakeling* sn = &s->snakelings[snakeling_idx]; + if (!sn->active) return 0; *out = (OsrsAttackTarget){ - .slot = 0, - .x = s->zulrah.x, - .y = s->zulrah.y, - .size = ZUL_NPC_SIZE, + .slot = target_slot, + .x = sn->entity.x, + .y = sn->entity.y, + .size = 1, .attack_range = ls->attack_range, }; return 1; @@ -860,6 +885,31 @@ static inline int zul_cap_damage(ZulrahState* s, int damage) { return damage; } +static void zul_apply_recoil(ZulrahState* s, int damage, AttackStyle style, + Player* attacker) { + osrs_ensure_player_equipment(&s->player); + DamageResult damage_result = osrs_apply_passive_damage_pipeline( + damage, + style, + s->player.prayer, + 0, + 0, + 0, + &s->player.equipment_effect_profile, + &s->player.item_effect_state, + &s->rng_state + ); + if (damage_result.recoil_damage > 0) { + int recoil = damage_result.recoil_damage; + if (s->player.equipment_effect_profile.recoil_source == OSRS_RECOIL_SOURCE_RING_OF_RECOIL && + recoil > s->player.item_effect_state.recoil_charges) { + recoil = s->player.item_effect_state.recoil_charges; + } + encounter_damage_player(attacker, recoil, NULL); + osrs_consume_recoil_charges(&s->player, recoil); + } +} + static void zul_apply_player_damage(ZulrahState* s, int damage, AttackStyle style, Player* attacker) { if (damage <= 0) return; @@ -867,27 +917,61 @@ static void zul_apply_player_damage(ZulrahState* s, int damage, AttackStyle styl s->total_damage_received += damage; s->player.hit_style = style; - if (attacker) { - osrs_ensure_player_equipment(&s->player); - DamageResult damage_result = osrs_apply_passive_damage_pipeline( - damage, - style, - s->player.prayer, - 0, - 0, - 0, - &s->player.equipment_effect_profile, - &s->player.item_effect_state, - &s->rng_state - ); - if (damage_result.recoil_damage > 0) { - int recoil = damage_result.recoil_damage; - if (s->player.equipment_effect_profile.recoil_source == OSRS_RECOIL_SOURCE_RING_OF_RECOIL && - recoil > s->player.item_effect_state.recoil_charges) { - recoil = s->player.item_effect_state.recoil_charges; - } - encounter_damage_player(attacker, recoil, NULL); - osrs_consume_recoil_charges(&s->player, recoil); + if (attacker) zul_apply_recoil(s, damage, style, attacker); +} + +/** Landing-side accounting for one player->Zulrah hit; returns 1 when it landed + this tick. Shared by the instant (delay 0) and queued paths. */ +static int zul_land_zulrah_hit(ZulrahState* s, EncounterPendingHit* ph) { + int landed = 0; + int hit_damage = 0; + float dealt = 0.0f; + if (!encounter_resolve_npc_pending_hit( + ph, &s->zulrah.current_hitpoints, &landed, &hit_damage, + NULL, NULL, &dealt)) + return 0; + s->damage_dealt_this_tick += dealt; + s->total_damage_dealt += dealt; + s->zulrah.hit_landed_this_tick = 1; + s->zulrah.hit_damage = hit_damage; + s->zulrah.hit_was_successful = hit_damage > 0; + return 1; +} + +static void zul_queue_zulrah_hit(ZulrahState* s, int damage, AttackStyle style, + int is_special) { + int distance = encounter_projectile_distance( + s->player.x, s->player.y, 1, + s->zulrah.x, s->zulrah.y, ZUL_NPC_SIZE, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + int delay = zul_player_projectile_timing( + style, s->player.equipped[GEAR_SLOT_WEAPON], is_special, distance) + .damage_delay_ticks; + + EncounterPendingHit hit = { + .active = 1, + .ticks_remaining = (int8_t)delay, + .attack_style = (int8_t)style, + .check_prayer = 0, + .spell_type = ENCOUNTER_SPELL_NONE, + .source_npc_slot = -1, + .damage = (int16_t)damage, + }; + if (delay <= 0) { + zul_land_zulrah_hit(s, &hit); + return; + } + encounter_pending_hit_queue_push( + &s->zulrah_pending_hits, hit, "zulrah-npc", s->tick, -1, 0); +} + +static void zul_resolve_zulrah_pending_hits(ZulrahState* s) { + for (int i = 0; i < s->zulrah_pending_hits.count; i++) { + EncounterPendingHit* ph = &s->zulrah_pending_hits.hits[i]; + zul_land_zulrah_hit(s, ph); + if (!ph->active) { + encounter_pending_hit_queue_remove(&s->zulrah_pending_hits, i, "zulrah-npc"); + i--; } } } @@ -901,8 +985,7 @@ static void zul_try_envenom(ZulrahState* s) { } static int zul_player_def_roll(ZulrahState* s, int attack_style) { - const EncounterLoadoutStats* ls = zul_current_loadout_stats( - s, s->player_gear == ZUL_GEAR_MAGE); + const EncounterLoadoutStats* ls = zul_live_stats(s); int roll = encounter_player_def_roll_from_loadout( 99, 99, ls->def_stab, ls->def_slash, ls->def_crush, ls->def_magic, ls->def_ranged, @@ -928,33 +1011,69 @@ static void zul_record_attack(ZulrahState* s, int src_x, int src_y, s->attack_events[i].damage = damage; } +/** Queues one Zulrah->player hit. Protect-prayer and damage freeze on THIS tick + (the calculation tick); only the application waits out the hit delay. */ +static int zul_queue_player_hit(ZulrahState* s, int raw_damage, AttackStyle style, + int accuracy_hit) { + int distance = encounter_projectile_distance( + s->zulrah.x, s->zulrah.y, ZUL_NPC_SIZE, + s->player.x, s->player.y, 1, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + int delay = encounter_projectile_base_hit_delay( + distance, 0, encounter_projectile_delay_kind_for_style(style)); + + int prayed = 0; + EncounterPendingHit hit = encounter_pending_hit_resolved_at_throw( + raw_damage, delay, style, s->player.prayer, + s->zulrah.npc_def_id, -1, accuracy_hit, &prayed); + encounter_pending_hit_queue_push( + &s->player_pending_hits, hit, "zulrah-player", s->tick, -1, style); + return prayed ? 0 : raw_damage; +} + +static void zul_player_hit_landed( + void* user, const EncounterPendingHit* hit, int damage_after_prayer, + int damage_applied, int prayer_was_correct, int prayer_was_checked +) { + (void)damage_applied; + (void)prayer_was_correct; + (void)prayer_was_checked; + ZulrahState* s = (ZulrahState*)user; + if (damage_after_prayer <= 0) return; + s->total_damage_received += damage_after_prayer; + s->player.hit_style = (AttackStyle)hit->attack_style; + zul_apply_recoil(s, damage_after_prayer, (AttackStyle)hit->attack_style, + &s->zulrah); +} + +static void zul_resolve_player_pending_hits(ZulrahState* s) { + encounter_resolve_player_pending_hits_observed( + &s->player_pending_hits, &s->player, s->player.prayer, + &s->damage_received_this_tick, NULL, NULL, + zul_player_hit_landed, s); +} + static void zul_attack_ranged(ZulrahState* s) { const MonsterStats* m = &MONSTER_DATABASE[MON_ZULRAH_GREEN]; int npc_att_roll = osrs_npc_attack_roll(m->range_level, m->range_att_bonus); - int dmg = 0; + int def_roll = zul_player_def_roll(s, ATTACK_STYLE_RANGED); int did_hit = 0; - if (encounter_prayer_correct_for_style(s->player.prayer, ATTACK_STYLE_RANGED)) { - int def_roll = zul_player_def_roll(s, ATTACK_STYLE_RANGED); - did_hit = encounter_roll_hit_chance(&s->rng_state, npc_att_roll, def_roll); - } else { - int def_roll = zul_player_def_roll(s, ATTACK_STYLE_RANGED); - if (encounter_roll_hit_chance(&s->rng_state, npc_att_roll, def_roll)) { - did_hit = 1; - dmg = encounter_rand_int(&s->rng_state, m->max_hit + 1); - zul_apply_player_damage(s, dmg, ATTACK_STYLE_RANGED, &s->zulrah); - } - } + int raw = encounter_npc_roll_attack_ex( + npc_att_roll, def_roll, m->max_hit, 0, &s->rng_state, &did_hit); + int dmg = zul_queue_player_hit(s, raw, ATTACK_STYLE_RANGED, did_hit); if (did_hit) zul_try_envenom(s); zul_record_attack(s, s->zulrah.x, s->zulrah.y, s->player.x, s->player.y, 0, dmg); } static void zul_attack_magic(ZulrahState* s) { - int dmg = 0; - if (!encounter_prayer_correct_for_style(s->player.prayer, ATTACK_STYLE_MAGIC)) { - dmg = encounter_rand_int(&s->rng_state, MONSTER_DATABASE[MON_ZULRAH_BLUE].max_hit + 1); - zul_apply_player_damage(s, dmg, ATTACK_STYLE_MAGIC, &s->zulrah); - } + const MonsterStats* m = &MONSTER_DATABASE[MON_ZULRAH_BLUE]; + int npc_att_roll = osrs_npc_attack_roll(m->magic_level, m->magic_att_bonus); + int def_roll = zul_player_def_roll(s, ATTACK_STYLE_MAGIC); + int did_hit = 0; + int raw = encounter_npc_roll_attack_ex( + npc_att_roll, def_roll, m->max_hit, 0, &s->rng_state, &did_hit); + int dmg = zul_queue_player_hit(s, raw, ATTACK_STYLE_MAGIC, did_hit); zul_try_envenom(s); zul_record_attack(s, s->zulrah.x, s->zulrah.y, s->player.x, s->player.y, 1, dmg); @@ -978,6 +1097,7 @@ static void zul_melee_start(ZulrahState* s) { s->melee_target_y = s->player.y; s->melee_pending = 1; s->melee_stare_timer = ZUL_MELEE_STARE_TICKS; + s->melee_prayer_at_calc = s->player.prayer; } static void zul_melee_hit(ZulrahState* s) { @@ -985,7 +1105,7 @@ static void zul_melee_hit(ZulrahState* s) { int dmg = 0; if (s->player.x == s->melee_target_x && s->player.y == s->melee_target_y && !zul_on_pillar_safespot(s->player.x, s->player.y)) { - if (!encounter_prayer_correct_for_style(s->player.prayer, ATTACK_STYLE_MELEE)) { + if (!encounter_prayer_correct_for_style(s->melee_prayer_at_calc, ATTACK_STYLE_MELEE)) { dmg = 20 + encounter_rand_int(&s->rng_state, 11); zul_apply_player_damage(s, dmg, ATTACK_STYLE_MELEE, &s->zulrah); s->player_stunned_ticks = ZUL_MELEE_STUN_TICKS; @@ -1020,37 +1140,24 @@ static AttackStyle zul_player_equipped_attack_style(const ZulrahState* s) { return ATTACK_STYLE_RANGED; } -static void zul_refresh_human_loadout_stats(ZulrahState* s) { - AttackStyle style = zul_player_equipped_attack_style(s); - FightStyle fight_style = s->player.fight_style; - int spell_base_damage = (style == ATTACK_STYLE_MAGIC) ? 30 : 0; - encounter_compute_player_equipped_stats( - &s->player, style, fight_style, spell_base_damage, - &s->human_loadout_stats); +static void zul_mark_live_stats_dirty(ZulrahState* s) { + s->live_stats_dirty = 1; } -static void zul_refresh_cached_loadout_stats(ZulrahState* s) { - int prayer_active = s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE; - if (s->mage_stats.style == ATTACK_STYLE_MAGIC) { - encounter_update_loadout_level(&s->mage_stats, - prayer_active ? OFFENSIVE_PRAYER_AUGURY : OFFENSIVE_PRAYER_NONE, - s->player.current_magic, s->player.current_magic); - } - if (s->range_stats.style == ATTACK_STYLE_RANGED) { - encounter_update_loadout_level(&s->range_stats, - prayer_active ? OFFENSIVE_PRAYER_RIGOUR : OFFENSIVE_PRAYER_NONE, - s->player.current_ranged, s->player.current_ranged); - } - if (s->human_command_mode) - zul_refresh_human_loadout_stats(s); -} -static const EncounterLoadoutStats* zul_current_loadout_stats(ZulrahState* s, int is_mage) { - if (s->human_command_mode) { - zul_refresh_human_loadout_stats(s); - return &s->human_loadout_stats; + +static const EncounterLoadoutStats* zul_live_stats(ZulrahState* s) { + if (s->live_stats_dirty) { + AttackStyle style = zul_player_equipped_attack_style(s); + FightStyle fight_style = s->human_command_mode + ? s->player.fight_style + : (style == ATTACK_STYLE_RANGED ? FIGHT_STYLE_RAPID : FIGHT_STYLE_ACCURATE); + int spell_base_damage = (style == ATTACK_STYLE_MAGIC) ? 30 : 0; + encounter_compute_player_equipped_stats( + &s->player, style, fight_style, spell_base_damage, &s->live_stats); + s->live_stats_dirty = 0; } - return is_mage ? &s->mage_stats : &s->range_stats; + return &s->live_stats; } static int zul_player_can_attack_zulrah( @@ -1087,26 +1194,23 @@ static int zul_player_attack_hits( : encounter_roll_hit_chance(&s->rng_state, att_roll, def_roll); } -static void zul_player_attack(ZulrahState* s, int is_mage) { +static void zul_player_attack(ZulrahState* s) { if (!s->zulrah_visible || s->is_diving) return; if (s->player.attack_timer > 0) return; if (s->player_stunned_ticks > 0) return; - const EncounterLoadoutStats* ls = zul_current_loadout_stats(s, is_mage); + const EncounterLoadoutStats* ls = zul_live_stats(s); if (!zul_player_can_attack_zulrah(s, ls)) return; - int gear_ok = s->human_command_mode - ? ((is_mage && ls->style == ATTACK_STYLE_MAGIC) || - (!is_mage && ls->style == ATTACK_STYLE_RANGED)) - : ((is_mage && s->player_gear == ZUL_GEAR_MAGE) || - (!is_mage && s->player_gear == ZUL_GEAR_RANGE)); + AttackStyle style = zul_player_equipped_attack_style(s); + int is_mage = (style == ATTACK_STYLE_MAGIC); const MonsterStats* monster = &MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[s->current_form]]; OsrsMagicAttackKind magic_kind = is_mage ? OSRS_MAGIC_ATTACK_POWERED_STAFF : OSRS_MAGIC_ATTACK_NONE; OsrsPreparedAttackEffects attack_effects = osrs_prepare_attack_effects( &s->player.equipment_effect_profile, &s->player.item_effect_state, s->player.equipped[GEAR_SLOT_WEAPON], - is_mage ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED, + style, magic_kind, (OsrsTargetRef){ .kind = OSRS_TARGET_NPC, .id = 0 }, 1, @@ -1119,22 +1223,20 @@ static void zul_player_attack(ZulrahState* s, int is_mage) { s->player.base_hitpoints ); s->player.attack_timer = ls->attack_speed; - if (!gear_ok) return; int dmg = 0; int hit = zul_player_attack_hits(s, is_mage, &attack_effects); if (hit) { dmg = encounter_rand_int(&s->rng_state, attack_effects.max_hit + 1); dmg = zul_cap_damage(s, dmg); - encounter_damage_player(&s->zulrah, dmg, &s->damage_dealt_this_tick); - s->total_damage_dealt += dmg; } + zul_queue_zulrah_hit(s, dmg, style, 0); { OsrsPostAttackEffects post_effects = osrs_finalize_attack_effects( &s->player.equipment_effect_profile, &s->player.item_effect_state, s->player.equipped[GEAR_SLOT_WEAPON], - is_mage ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED, + style, magic_kind, (OsrsTargetRef){ .kind = OSRS_TARGET_NPC, .id = 0 }, 1, @@ -1150,14 +1252,10 @@ static void zul_player_attack(ZulrahState* s, int is_mage) { } } s->player.just_attacked = 1; - s->player.last_attack_style = is_mage ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED; - s->player.attack_style_this_tick = is_mage ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED; + s->player.last_attack_style = style; + s->player.attack_style_this_tick = style; zul_record_player_attack_visual( s, s->player.attack_style_this_tick, dmg, 0); - - s->zulrah.hit_landed_this_tick = 1; - s->zulrah.hit_damage = dmg; - s->zulrah.hit_was_successful = (dmg > 0); } static void zul_player_spec(ZulrahState* s) { @@ -1165,15 +1263,12 @@ static void zul_player_spec(ZulrahState* s) { if (s->player.attack_timer > 0) return; if (s->player_stunned_ticks > 0) return; - int is_mage = (s->player_gear == ZUL_GEAR_MAGE); - const EncounterLoadoutStats* ls = zul_current_loadout_stats(s, is_mage); + AttackStyle style = zul_player_equipped_attack_style(s); + int is_mage = (style == ATTACK_STYLE_MAGIC); + const EncounterLoadoutStats* ls = zul_live_stats(s); if (!zul_player_can_attack_zulrah(s, ls)) return; - int weapon = s->human_command_mode - ? s->player.equipped[GEAR_SLOT_WEAPON] - : (is_mage - ? ZUL_MAGE_LOADOUT[s->gear_tier][GEAR_SLOT_WEAPON] - : ZUL_RANGE_LOADOUT[s->gear_tier][GEAR_SLOT_WEAPON]); + int weapon = s->player.equipped[GEAR_SLOT_WEAPON]; int cost = osrs_spec_cost(weapon); if (cost == 0) return; @@ -1189,7 +1284,7 @@ static void zul_player_spec(ZulrahState* s) { s->player.special_energy -= sr.spec_cost; s->player.just_attacked = 1; s->player.used_special_this_tick = 1; - s->player.last_attack_style = is_mage ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED; + s->player.last_attack_style = style; s->player.attack_style_this_tick = s->player.last_attack_style; s->player.attack_timer = sr.attack_speed_override ? sr.attack_speed_override : ls->attack_speed; zul_record_player_attack_visual(s, s->player.attack_style_this_tick, 0, 1); @@ -1197,7 +1292,7 @@ static void zul_player_spec(ZulrahState* s) { int total_dmg = 0; for (int i = 0; i < sr.num_hits; i++) { int dmg = zul_cap_damage(s, sr.damage[i]); - encounter_damage_player(&s->zulrah, dmg, NULL); + zul_queue_zulrah_hit(s, dmg, style, 1); total_dmg += dmg; } @@ -1209,12 +1304,7 @@ static void zul_player_spec(ZulrahState* s) { s->magic_def_drain += sr.magic_def_drain; - s->damage_dealt_this_tick += total_dmg; - s->total_damage_dealt += total_dmg; s->player_attack_dmg = total_dmg; - s->zulrah.hit_landed_this_tick = 1; - s->zulrah.hit_damage = total_dmg; - s->zulrah.hit_was_successful = (total_dmg > 0); } static void zul_pick_snakeling_pos(ZulrahState* s, int* ox, int* oy) { @@ -1649,80 +1739,110 @@ static void zul_phase_tick(ZulrahState* s) { } } -static void zul_process_prayer(ZulrahState* s, int overhead_action, int offensive_action) { - if (encounter_apply_overhead_action(&s->player.prayer, overhead_action)) { - s->player.prayer_just_activated = 1; - } - OffensivePrayer prev_offensive = s->player.offensive_prayer; - if (encounter_apply_offensive_action(&s->player.offensive_prayer, offensive_action)) { - s->player.offensive_prayer_just_activated = 1; +static void zul_apply_eat_cell(ZulrahState* s, int cell_idx, OsrsConsumableKind kind) { + FoodType type; + switch (kind) { + case OSRS_CONSUMABLE_SHARK_FOOD: type = FOOD_SHARK; break; + case OSRS_CONSUMABLE_KARAMBWAN: type = FOOD_KARAMBWAN; break; + default: + fprintf(stderr, "zulrah eat: unsupported consumable kind %d\n", (int)kind); + abort(); } - if (s->player.offensive_prayer != prev_offensive) - zul_refresh_cached_loadout_stats(s); + OsrsPlayerEatResult r = osrs_player_eat_food_effects(&s->player, type); + if (r.consumed) osrs_inventory_cell_consume_eat(&s->inventory_cells[cell_idx]); } -static void zul_process_food(ZulrahState* s, int a) { - if (a == 0) return; - FoodType type; - switch (a) { - case 1: - type = FOOD_SHARK; - break; - case 2: - type = FOOD_KARAMBWAN; +static void zul_apply_drink_one_dose_effect(void* ctx, OsrsConsumableKind kind) { + ZulrahState* s = (ZulrahState*)ctx; + switch (kind) { + case OSRS_CONSUMABLE_PRAYER_RESTORE: + encounter_add_prayer_restore( + &s->player, osrs_prayer_potion_restore_amount(s->player.base_prayer)); + encounter_cap_prayer_restore(&s->player); + return; + case OSRS_CONSUMABLE_ANTIVENOM_PLUS: + s->venom_counter = 0; + s->venom_timer = 0; + s->antivenom_timer = ZUL_ANTIVENOM_DURATION; + return; + default: break; + } + fprintf(stderr, "zulrah drink: unsupported consumable kind %d\n", (int)kind); + abort(); +} + +static int zul_drink_has_effect(const ZulrahState* s, OsrsConsumableKind kind) { + switch (kind) { + case OSRS_CONSUMABLE_PRAYER_RESTORE: + return s->player.current_prayer < s->player.base_prayer; + case OSRS_CONSUMABLE_ANTIVENOM_PLUS: + return s->antivenom_timer == 0; default: - fprintf(stderr, "unsupported Zulrah food action: %d\n", a); - abort(); + break; } + fprintf(stderr, "zulrah drink mask: unsupported consumable kind %d\n", (int)kind); + abort(); +} - osrs_player_eat_food_type(&s->player, type); +static void zul_apply_drink_cell( + ZulrahState* s, int cell_idx, OsrsInventoryClickResolution resolution +) { + (void)osrs_inventory_cell_consume_drink_one_dose( + &s->inventory_cells[cell_idx], resolution, &s->player.potion_timer, + zul_apply_drink_one_dose_effect, s); } -static void zul_process_potion(ZulrahState* s, int a) { - if (a == 0) return; - if (a == 1) { - if (s->player.prayer_pot_doses <= 0) return; - DrinkResult r = osrs_drink_potion(POTION_PRAYER_RESTORE, s->player.current_prayer, - s->player.base_prayer, s->player.potion_timer); - if (!r.consumed) return; - s->player.prayer_pot_doses--; - s->player.potion_timer = 3; - s->player.current_prayer += r.prayer_restored; - if (s->player.current_prayer > s->player.base_prayer) - s->player.current_prayer = s->player.base_prayer; - } else if (a == 2) { - if (s->player.antivenom_doses <= 0) return; - DrinkResult r = osrs_drink_potion(POTION_ANTIVENOM_PLUS, 0, 0, s->player.potion_timer); - if (!r.consumed) return; - s->player.antivenom_doses--; - s->player.potion_timer = 3; - s->venom_counter = 0; - s->venom_timer = 0; - s->antivenom_timer = r.antivenom_ticks; - } +static void zul_player_attack_snakeling(ZulrahState* s, int snakeling_idx) { + if (snakeling_idx < 0 || snakeling_idx >= ZUL_MAX_SNAKELINGS) return; + ZulrahSnakeling* sn = &s->snakelings[snakeling_idx]; + if (!sn->active) return; + + const EncounterLoadoutStats* ls = zul_live_stats(s); + if (!encounter_player_can_attack( + s->player.x, s->player.y, + sn->entity.x, sn->entity.y, 1, + ls->attack_range, osrs_los_open_query())) return; + + AttackStyle style = zul_player_equipped_attack_style(s); + s->player.attack_timer = ls->attack_speed; + s->player.just_attacked = 1; + s->player.last_attack_style = style; + + const MonsterStats* m = &MONSTER_DATABASE[sn->is_magic + ? MON_ZULRAH_SNAKELING_MAGIC : MON_ZULRAH_SNAKELING_MELEE]; + int def_bonus = style == ATTACK_STYLE_MAGIC ? m->magic_def : m->ranged_def; + int def_roll = (m->def_level + 8) * (def_bonus + 64); + if (def_roll < 0) def_roll = 0; + int att_roll = osrs_player_att_roll(ls->eff_level, ls->attack_bonus); + if (!encounter_roll_hit_chance(&s->rng_state, att_roll, def_roll)) return; + + int dmg = encounter_rand_int(&s->rng_state, ls->max_hit + 1); + encounter_damage_player(&sn->entity, dmg, &s->damage_dealt_this_tick); + if (sn->entity.current_hitpoints <= 0) sn->active = 0; } -static void zul_process_gear(ZulrahState* s, int atk) { - if (atk == ZUL_ATK_MAGE && s->player_gear != ZUL_GEAR_MAGE) { - s->player_gear = ZUL_GEAR_MAGE; - encounter_apply_loadout(&s->player, ZUL_MAGE_LOADOUT[s->gear_tier], GEAR_MAGE); - } else if (atk == ZUL_ATK_RANGE && s->player_gear != ZUL_GEAR_RANGE) { - s->player_gear = ZUL_GEAR_RANGE; - encounter_apply_loadout(&s->player, ZUL_RANGE_LOADOUT[s->gear_tier], GEAR_RANGED); +static void zul_process_prayer(ZulrahState* s, int overhead_action, int offensive_action) { + if (encounter_apply_overhead_action(&s->player.prayer, overhead_action)) { + s->player.prayer_just_activated = 1; } + OffensivePrayer prev_offensive = s->player.offensive_prayer; + if (encounter_apply_offensive_action(&s->player.offensive_prayer, offensive_action)) { + s->player.offensive_prayer_just_activated = 1; + } + if (s->player.offensive_prayer != prev_offensive) + zul_mark_live_stats_dirty(s); } + + + static FightStyle zul_default_fight_style_for_style(AttackStyle style) { if (style == ATTACK_STYLE_MAGIC) return FIGHT_STYLE_ACCURATE; if (style == ATTACK_STYLE_RANGED) return FIGHT_STYLE_RAPID; return FIGHT_STYLE_ACCURATE; } -static void zul_sync_human_gear_style(ZulrahState* s) { - AttackStyle style = zul_player_equipped_attack_style(s); - s->player_gear = (style == ATTACK_STYLE_MAGIC) ? ZUL_GEAR_MAGE : ZUL_GEAR_RANGE; -} static void zul_apply_human_player_commands(ZulrahState* s) { int did_change_stats = 0; @@ -1737,7 +1857,6 @@ static void zul_apply_human_player_commands(ZulrahState* s) { if (cmd->gear_slot == GEAR_SLOT_WEAPON) { AttackStyle style = zul_player_equipped_attack_style(s); s->player.fight_style = zul_default_fight_style_for_style(style); - zul_sync_human_gear_style(s); } } } @@ -1750,7 +1869,7 @@ static void zul_apply_human_player_commands(ZulrahState* s) { } } if (did_change_stats) - zul_refresh_human_loadout_stats(s); + zul_mark_live_stats_dirty(s); } static void zul_write_obs(EncounterState* state, EncounterContext* context, float* obs) { @@ -1764,13 +1883,8 @@ static void zul_write_obs(EncounterState* state, EncounterContext* context, floa obs[i++] = (float)s->player.x / ZUL_ARENA_SIZE; obs[i++] = (float)s->player.y / ZUL_ARENA_SIZE; obs[i++] = (float)s->player.attack_timer / 5.0f; - obs[i++] = (float)s->player.food_count / ZUL_PLAYER_FOOD; - obs[i++] = (float)s->player.karambwan_count / ZUL_PLAYER_KARAMBWAN; - obs[i++] = (float)s->player.prayer_pot_doses / ZUL_PLAYER_RESTORE_DOSES; obs[i++] = (float)s->player.food_timer / 3.0f; obs[i++] = (float)s->player.potion_timer / 3.0f; - obs[i++] = (s->player_gear == ZUL_GEAR_MAGE) ? 1.0f : 0.0f; - obs[i++] = (s->player_gear == ZUL_GEAR_RANGE) ? 1.0f : 0.0f; obs[i++] = (s->player.prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; obs[i++] = (s->player.prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; obs[i++] = (s->player.prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; @@ -1864,6 +1978,25 @@ static void zul_write_obs(EncounterState* state, EncounterContext* context, floa for (int m = 0; m < ZUL_MOVE_DIM; m++) obs[i++] = zul_move_action_cloud_unsafe(s, m) ? 1.0f : 0.0f; + static const float ZUL_ZERO_POST_USE_DELTAS[6] = {0}; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + osrs_write_inventory_cell_affordance_features( + &obs[i], + s->inventory_cells[cell].item_idx, + s->inventory_cells[cell].raw_osrs_id, + s->inventory_cells[cell].dose, + osrs_inventory_cell_holds_equipped_item(&s->player, s->inventory_cells, cell), + ZUL_ZERO_POST_USE_DELTAS, + s->player.base_hitpoints, + s->player.base_prayer, + s->player.base_attack); + i += OSRS_INVENTORY_CELL_OBS_FEATURES; + } + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + osrs_write_equipped_self_features(&obs[i], s->player.equipped[slot]); + i += OSRS_EQUIPPED_SELF_OBS_FEATURES; + } + if (i != ZUL_NUM_OBS) { fprintf(stderr, "zulrah obs size mismatch: wrote %d expected %d\n", i, ZUL_NUM_OBS); abort(); @@ -1887,14 +2020,10 @@ static void zul_write_mask(EncounterState* state, EncounterContext* context, flo } off++; } - for (int a = 0; a < ZUL_ATTACK_DIM; a++) { - const EncounterLoadoutStats* attack_stats = - a == ZUL_ATK_MAGE ? &s->mage_stats : - (a == ZUL_ATK_RANGE ? &s->range_stats : NULL); - if (a > 0 && (!s->zulrah_visible || s->is_diving || - s->player.attack_timer > 0 || s->player_stunned_ticks > 0 || - !zul_player_can_attack_zulrah(s, attack_stats))) - mask[off] = 0.0f; + mask[off] = s->zulrah_visible && !s->is_diving ? 1.0f : 0.0f; + off++; + for (int n = 0; n < ZUL_MAX_SNAKELINGS; n++) { + mask[off] = s->snakelings[n].active ? 1.0f : 0.0f; off++; } for (int p = 0; p < ZUL_PRAYER_DIM; p++) { @@ -1904,32 +2033,60 @@ static void zul_write_mask(EncounterState* state, EncounterContext* context, flo mask[off] = 0.0f; off++; } + int cell_equip_slot[OSRS_INVENTORY_SIZE]; + int cell_can_eat[OSRS_INVENTORY_SIZE]; + int cell_can_drink[OSRS_INVENTORY_SIZE]; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + cell_equip_slot[cell] = -1; + cell_can_eat[cell] = 0; + cell_can_drink[cell] = 0; + OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( + &s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); + if (r.click_action == OSRS_CLICK_EQUIP) { + if (osrs_can_equip_from_cell(&s->player, s->inventory_cells, cell)) + cell_equip_slot[cell] = + osrs_item_gear_slot(s->inventory_cells[cell].item_idx); + } else if (r.click_action == OSRS_CLICK_EAT) { + cell_can_eat[cell] = + osrs_can_eat_consumable_kind(&s->player, r.consumable_kind); + } else if (r.click_action == OSRS_CLICK_DRINK) { + cell_can_drink[cell] = s->inventory_cells[cell].dose > 0 && + s->player.potion_timer == 0 && + zul_drink_has_effect(s, r.consumable_kind); + } + } + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + mask[off] = 1.0f; + off++; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + mask[off] = cell_equip_slot[cell] == slot ? 1.0f : 0.0f; + off++; + } + } + mask[off] = 1.0f; off++; - if (!osrs_player_can_eat_food_type(&s->player, FOOD_SHARK) || - osrs_player_food_wasted_hp(&s->player, FOOD_SHARK) > 0) - mask[off] = 0.0f; - off++; - if (!osrs_player_can_eat_food_type(&s->player, FOOD_KARAMBWAN) || - osrs_player_food_wasted_hp(&s->player, FOOD_KARAMBWAN) > 0) - mask[off] = 0.0f; - off++; - off++; - if (s->player.prayer_pot_doses <= 0 || s->player.potion_timer > 0 || - s->player.current_prayer >= s->player.base_prayer) - mask[off] = 0.0f; - off++; - if (s->player.antivenom_doses <= 0 || s->player.potion_timer > 0 || - s->antivenom_timer > 0) - mask[off] = 0.0f; - off++; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + mask[off] = cell_can_eat[cell] ? 1.0f : 0.0f; + off++; + } + mask[off] = 1.0f; off++; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + mask[off] = cell_can_drink[cell] ? 1.0f : 0.0f; + off++; + } { int weapon = s->player.equipped[GEAR_SLOT_WEAPON]; - int spec_cost = osrs_spec_cost(weapon); - if (spec_cost <= 0) - mask[off] = 0.0f; + int weapon_spec_cost = osrs_spec_cost(weapon); + mask[off] = 1.0f; + off++; + mask[off] = weapon_spec_cost > 0 && + (s->player.special_energy >= weapon_spec_cost || s->player.spec_armed) + ? 1.0f : 0.0f; + off++; + mask[off] = s->player.spec_armed ? 1.0f : 0.0f; + off++; } - off++; for (int o = 0; o < ZUL_OFFENSIVE_DIM; o++) { if (o == ENCOUNTER_OFFENSIVE_OFF && s->player.offensive_prayer == OFFENSIVE_PRAYER_NONE) @@ -1938,6 +2095,11 @@ static void zul_write_mask(EncounterState* state, EncounterContext* context, flo mask[off] = 0.0f; off++; } + if (off != ZUL_ACTION_MASK_SIZE) { + fprintf(stderr, "zulrah mask size mismatch: wrote %d expected %d\n", + off, ZUL_ACTION_MASK_SIZE); + abort(); + } } static float zul_compute_reward(ZulrahState* s) { @@ -2004,6 +2166,30 @@ static float zul_current_kill_progress(const ZulrahState* s) { return 1.0f - (float)s->zulrah.current_hitpoints / (float)max_hp; } +typedef struct { + float win; + float score; +} ZulEpisodeOutcome; + +static ZulEpisodeOutcome zul_episode_outcome(const ZulrahState* s) { + float kills = (float)s->kills_this_episode; + float win = (s->episode_mode == ZUL_EPISODE_TRIP) + ? kills + : ((s->winner == ZUL_OUTCOME_PLAYER_WON) ? 1.0f : 0.0f); + float partial = (s->episode_mode == ZUL_EPISODE_TRIP) + ? zul_current_kill_progress(s) + : 0.0f; + float speed_bonus = (s->episode_mode == ZUL_EPISODE_TRIP) + ? s->score_speed_bonus_sum + : (win > 0.0f + ? zul_score_speed_bonus_for_duration(s->tick) + : 0.0f); + return (ZulEpisodeOutcome){ + .win = win, + .score = win + partial + speed_bonus, + }; +} + static void zul_clear_active_kill(ZulrahState* s) { memset(s->clouds, 0, sizeof(s->clouds)); memset(s->pending_clouds, 0, sizeof(s->pending_clouds)); @@ -2012,6 +2198,8 @@ static void zul_clear_active_kill(ZulrahState* s) { s->cloud_event_count = 0; s->melee_pending = 0; s->melee_stare_timer = 0; + encounter_pending_hit_queue_clear(&s->player_pending_hits); + encounter_pending_hit_queue_clear(&s->zulrah_pending_hits); s->phase_timer = 0; s->surface_timer = 0; s->is_diving = 0; @@ -2094,6 +2282,33 @@ static void zul_record_episode_timeout(ZulrahState* s) { } } +static void zul_seed_inventory_cells(ZulrahState* s) { + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) + s->inventory_cells[i] = osrs_inventory_cell_empty(); + int cell = 0; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + uint8_t item = ZUL_RANGE_LOADOUT[s->gear_tier][slot]; + if (item == ITEM_NONE || item == s->player.equipped[slot]) continue; + s->inventory_cells[cell++] = osrs_inventory_cell_from_item(item); + } + static const struct { uint16_t raw_osrs_id; int count; } ZUL_TRIP_CONSUMABLES[] = { + {385, ZUL_PLAYER_FOOD}, + {3144, ZUL_PLAYER_KARAMBWAN}, + {2434, ZUL_PLAYER_RESTORE_DOSES / 4}, + {12913, ZUL_ANTIVENOM_DOSES / 4}, + }; + for (size_t k = 0; k < sizeof(ZUL_TRIP_CONSUMABLES) / sizeof(*ZUL_TRIP_CONSUMABLES); k++) { + for (int n = 0; n < ZUL_TRIP_CONSUMABLES[k].count; n++) { + if (cell >= OSRS_INVENTORY_SIZE) { + fprintf(stderr, "zulrah trip inventory overflows %d cells\n", OSRS_INVENTORY_SIZE); + abort(); + } + s->inventory_cells[cell++] = osrs_inventory_cell_from_raw_osrs_id( + ZUL_TRIP_CONSUMABLES[k].raw_osrs_id); + } + } +} + static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t seed) { (void)context; ZulrahState* s = (ZulrahState*)state; @@ -2128,11 +2343,7 @@ static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t encounter_init_maxed_player_combat_stats(&s->player, ZUL_PLAYER_PRAYER); s->player.x = ZUL_PLAYER_START_X; s->player.y = ZUL_PLAYER_START_Y; - s->player.food_count = ZUL_PLAYER_FOOD; - s->player.karambwan_count = ZUL_PLAYER_KARAMBWAN; - s->player.prayer_pot_doses = ZUL_PLAYER_RESTORE_DOSES; s->player.special_energy = 100; - s->player.antivenom_doses = ZUL_ANTIVENOM_DOSES; osrs_item_effect_state_init(&s->player.item_effect_state); if (s->gear_tier == 2) { s->player.saturated_heart_count = 1; @@ -2145,24 +2356,16 @@ static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t } osrs_interaction_init(&s->interaction); s->player.spec_armed = 0; - s->player_gear = ZUL_GEAR_MAGE; encounter_apply_loadout(&s->player, ZUL_MAGE_LOADOUT[s->gear_tier], GEAR_MAGE); zul_populate_player_inventory(&s->player, s->gear_tier); - OffensivePrayer mage_prayer = (s->gear_tier >= 1) ? OFFENSIVE_PRAYER_AUGURY : OFFENSIVE_PRAYER_NONE; - OffensivePrayer range_prayer = (s->gear_tier >= 1) ? OFFENSIVE_PRAYER_RIGOUR : OFFENSIVE_PRAYER_NONE; - s->player.offensive_prayer = mage_prayer; - encounter_compute_loadout_stats(ZUL_MAGE_LOADOUT[s->gear_tier], ATTACK_STYLE_MAGIC, - mage_prayer, s->player.current_magic, FIGHT_STYLE_ACCURATE, 30, &s->mage_stats); - encounter_compute_loadout_stats(ZUL_RANGE_LOADOUT[s->gear_tier], ATTACK_STYLE_RANGED, - range_prayer, s->player.current_ranged, FIGHT_STYLE_RAPID, 0, &s->range_stats); + zul_seed_inventory_cells(s); + s->player.offensive_prayer = + (s->gear_tier >= 1) ? OFFENSIVE_PRAYER_AUGURY : OFFENSIVE_PRAYER_NONE; + zul_mark_live_stats_dirty(s); zul_start_active_kill(s); } -static void zul_step(EncounterState* state, EncounterContext* context, const int* actions) { - (void)context; - ZulrahState* s = (ZulrahState*)state; - if (s->episode_over) return; - +static void zul_step_tick(ZulrahState* s, const int* actions) { s->reward = 0.0f; s->damage_dealt_this_tick = 0.0f; s->damage_received_this_tick = 0.0f; @@ -2193,7 +2396,7 @@ static void zul_step(EncounterState* state, EncounterContext* context, const int s->zulrah.npc_visible = 0; zul_finish_boss_death(s); } - goto finish; + return; } zul_update_npc_anim_lifetime(s); @@ -2207,68 +2410,102 @@ static void zul_step(EncounterState* state, EncounterContext* context, const int if (s->tick > 0 && s->tick % 60 == 0) stats_changed |= encounter_decay_player_combat_stats_toward_base(&s->player); if (stats_changed) - zul_refresh_cached_loadout_stats(s); + zul_mark_live_stats_dirty(s); + + zul_resolve_zulrah_pending_hits(s); + zul_resolve_player_pending_hits(s); if (s->melee_pending) { s->melee_stare_timer--; if (s->melee_stare_timer <= 0) zul_melee_hit(s); } + if (s->zulrah_visible && s->zulrah.current_hitpoints <= 0) { + zul_record_boss_kill(s); + return; + } + if (s->player.current_hitpoints <= 0) { + zul_record_player_loss(s); + return; + } + zul_process_prayer(s, actions[ZUL_HEAD_PRAYER], actions[ZUL_HEAD_OFFENSIVE]); if (s->human_command_mode) zul_apply_human_player_commands(s); - if (actions[ZUL_HEAD_SPEC] == 1) { - osrs_spec_toggle(&s->player.spec_armed); + int spec_act = actions[ZUL_HEAD_SPEC]; + int spec_cost = osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]); + if (spec_act == 1 && spec_cost > 0 && s->player.special_energy >= spec_cost) { + s->player.spec_armed = 1; + } else if (spec_act == 2) { + s->player.spec_armed = 0; } - if (actions[ZUL_HEAD_FOOD] > 0) - osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EAT); - if (actions[ZUL_HEAD_POTION] > 0) - osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_DRINK); - zul_process_food(s, actions[ZUL_HEAD_FOOD]); - zul_process_potion(s, actions[ZUL_HEAD_POTION]); - - int atk_action = actions[ZUL_HEAD_ATTACK]; - if (!s->human_command_mode) { - if ((atk_action == ZUL_ATK_MAGE && s->player_gear != ZUL_GEAR_MAGE) || - (atk_action == ZUL_ATK_RANGE && s->player_gear != ZUL_GEAR_RANGE)) { + { + OsrsInventoryClickActions clicks; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + clicks.equip_by_slot[slot] = actions[ZUL_HEAD_EQUIP_SLOT(slot)]; + clicks.eat = actions[ZUL_HEAD_EAT]; + clicks.drink = actions[ZUL_HEAD_DRINK]; + OsrsInventoryTickIntent intent = + osrs_resolve_inventory_tick_intent(&s->player, s->inventory_cells, &clicks); + if (osrs_inventory_tick_intent_has_effect(&intent)) osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EQUIP); + OsrsInventoryApplyStep click_step; + while (osrs_inventory_intent_next(&intent, &click_step)) { + switch (click_step.kind) { + case OSRS_INVENTORY_APPLY_EQUIP: + if (osrs_equip_from_cell( + &s->player, s->inventory_cells, click_step.cell_idx) >= 0) + zul_mark_live_stats_dirty(s); + break; + case OSRS_INVENTORY_APPLY_EAT: + zul_apply_eat_cell( + s, click_step.cell_idx, click_step.resolution.consumable_kind); + break; + case OSRS_INVENTORY_APPLY_DRINK: + zul_apply_drink_cell(s, click_step.cell_idx, click_step.resolution); + break; + } } - zul_process_gear(s, atk_action); } + int primary = actions[ZUL_HEAD_PRIMARY]; int has_new_target = 0; - if (atk_action == ZUL_ATK_MAGE || atk_action == ZUL_ATK_RANGE) { + int new_target_slot = 0; + if (primary >= ZUL_PRIMARY_ATTACK_BASE && primary < ZUL_PRIMARY_DIM) { has_new_target = 1; + new_target_slot = primary - ZUL_PRIMARY_ATTACK_BASE; } - OsrsPlayerMoveKind move_kind = OSRS_PLAYER_MOVE_NONE; - if (s->player_dest_explicit) { + OsrsPlayerCommand command = { .kind = OSRS_PLAYER_CMD_NONE }; + if (has_new_target) { + command.kind = OSRS_PLAYER_CMD_TARGET; + command.target_slot = new_target_slot; s->player_dest_explicit = 0; - move_kind = OSRS_PLAYER_MOVE_DESTINATION; + s->player_dest_x = -1; + s->player_dest_y = -1; + } else if (s->player_dest_explicit) { + s->player_dest_explicit = 0; + command.kind = OSRS_PLAYER_CMD_MOVE; + command.move_kind = OSRS_PLAYER_MOVE_DESTINATION; + } else if (primary > 0 && primary < ZUL_MOVE_DIM) { + s->player_dest_x = s->player.x + ENCOUNTER_MOVE_TARGET_DX[primary]; + s->player_dest_y = s->player.y + ENCOUNTER_MOVE_TARGET_DY[primary]; + command.kind = OSRS_PLAYER_CMD_MOVE; + command.move_kind = OSRS_PLAYER_MOVE_DESTINATION; } else { - int m = actions[ZUL_HEAD_MOVE]; - if (m > 0 && m < ZUL_MOVE_DIM) { - s->player_dest_x = s->player.x + ENCOUNTER_MOVE_TARGET_DX[m]; - s->player_dest_y = s->player.y + ENCOUNTER_MOVE_TARGET_DY[m]; - move_kind = OSRS_PLAYER_MOVE_DESTINATION; - } else { - s->player_dest_x = -1; - s->player_dest_y = -1; - } + s->player_dest_x = -1; + s->player_dest_y = -1; } - OsrsPlayerStepResult step_result = osrs_encounter_player_step(&(OsrsPlayerStepInput){ + OsrsPlayerStepInput step_input = { .player = &s->player, .interaction = &s->interaction, .target_lookup = zul_lookup_player_attack_target, .target_ctx = s, - .has_new_target = has_new_target, - .new_target_slot = 0, - .move_kind = move_kind, - .target_move_policy = OSRS_PLAYER_TARGET_MOVE_EXPLICIT_FIRST, + .command = command, .dest_x = &s->player_dest_x, .dest_y = &s->player_dest_y, .blocked_ticks = s->player_stunned_ticks, @@ -2284,7 +2521,8 @@ static void zul_step(EncounterState* state, EncounterContext* context, const int .arena_w = ZUL_ARENA_SIZE, .arena_h = ZUL_ARENA_SIZE, }, - }); + }; + OsrsPlayerStepResult step_result = osrs_encounter_player_step(&step_input); s->player_moved_this_tick = step_result.moved; s->player_chased_target_this_tick = step_result.chased_target; @@ -2295,28 +2533,33 @@ static void zul_step(EncounterState* state, EncounterContext* context, const int } if (osrs_interaction_active(&s->interaction) && - s->player.attack_timer == 0 && s->zulrah_visible && !s->is_diving && - s->player_stunned_ticks == 0) { - - if (s->player.spec_armed && s->player.special_energy >= osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON])) { - zul_player_spec(s); - osrs_spec_disarm(&s->player.spec_armed); + s->player.attack_timer == 0 && s->player_stunned_ticks == 0) { + int target = s->interaction.target_slot; + if (target == 0) { + if (s->zulrah_visible && !s->is_diving) { + if (s->player.spec_armed && s->player.special_energy >= + osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON])) { + zul_player_spec(s); + osrs_spec_disarm(&s->player.spec_armed); + } else { + zul_player_attack(s); + } + } } else { - if (s->player_gear == ZUL_GEAR_MAGE) zul_player_attack(s, 1); - else zul_player_attack(s, 0); + zul_player_attack_snakeling(s, target - 1); } } if (s->zulrah_visible && s->zulrah.current_hitpoints <= 0) { zul_record_boss_kill(s); - goto finish; + return; } zul_pending_cloud_tick(s); zul_cloud_tick(s); if (s->player.current_hitpoints <= 0) { zul_record_player_loss(s); - goto finish; + return; } zul_phase_tick(s); @@ -2331,21 +2574,55 @@ static void zul_step(EncounterState* state, EncounterContext* context, const int encounter_drain_all_prayers( &s->player, encounter_player_prayer_bonus(&s->player)); if (s->player.offensive_prayer != prev_off_drain) - zul_refresh_cached_loadout_stats(s); + zul_mark_live_stats_dirty(s); if (s->player.current_hitpoints <= 0) { zul_record_player_loss(s); - goto finish; + return; } if (s->tick >= ZUL_MAX_TICKS) { zul_record_episode_timeout(s); - goto finish; + return; } -finish: +} + +static void zul_step(EncounterState* state, EncounterContext* context, const int* actions) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + if (s->episode_over) return; + zul_step_tick(s, actions); s->reward = zul_compute_reward(s); s->episode_return += s->reward; } +static int zul_first_cell_with_kind( + const ZulrahState* s, OsrsClickAction click, OsrsConsumableKind kind +) { + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( + &s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); + if (r.click_action == click && r.consumable_kind == kind) return cell; + } + return -1; +} + +static int zul_cell_with_item(const ZulrahState* s, uint8_t item_idx) { + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) + if (s->inventory_cells[cell].item_idx == item_idx) return cell; + return -1; +} + +static void zul_heuristic_gear_swap( + const ZulrahState* s, int* actions, const uint8_t* loadout +) { + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + uint8_t want = loadout[slot]; + if (want == ITEM_NONE || s->player.equipped[slot] == want) continue; + int cell = zul_cell_with_item(s, want); + if (cell >= 0) actions[ZUL_HEAD_EQUIP_SLOT(slot)] = cell + 1; + } +} + static void zul_heuristic_actions(ZulrahState* s, int* actions) { for (int i = 0; i < ZUL_NUM_ACTION_HEADS; i++) actions[i] = 0; @@ -2380,24 +2657,33 @@ static void zul_heuristic_actions(ZulrahState* s, int* actions) { } } - if (s->player.potion_timer <= 0 && s->player.antivenom_doses > 0 && - s->antivenom_timer <= 5) { - actions[ZUL_HEAD_POTION] = 2; - return; + if (s->player.potion_timer <= 0 && s->antivenom_timer <= 5) { + int cell = zul_first_cell_with_kind( + s, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS); + if (cell >= 0) { + actions[ZUL_HEAD_DRINK] = cell + 1; + return; + } } - if (hp < 60 && s->player.food_timer <= 0 && s->player.food_count > 0 && + if (hp < 60 && s->player.food_timer <= 0 && hp <= s->player.base_hitpoints - osrs_food_heal_amount(FOOD_SHARK)) { - actions[ZUL_HEAD_FOOD] = 1; + int cell = zul_first_cell_with_kind( + s, OSRS_CLICK_EAT, OSRS_CONSUMABLE_SHARK_FOOD); + if (cell >= 0) actions[ZUL_HEAD_EAT] = cell + 1; } - else if (hp < 40 && s->player.food_timer <= 0 && s->player.karambwan_count > 0 && + else if (hp < 40 && s->player.karambwan_timer <= 0 && hp <= s->player.base_hitpoints - osrs_food_heal_amount(FOOD_KARAMBWAN)) { - actions[ZUL_HEAD_FOOD] = 2; + int cell = zul_first_cell_with_kind( + s, OSRS_CLICK_EAT, OSRS_CONSUMABLE_KARAMBWAN); + if (cell >= 0) actions[ZUL_HEAD_EAT] = cell + 1; } if (s->player.current_prayer < 30 && s->player.potion_timer <= 0 && - s->player.prayer_pot_doses > 0 && s->player.current_prayer < s->player.base_prayer) { - actions[ZUL_HEAD_POTION] = 1; + s->player.current_prayer < s->player.base_prayer) { + int cell = zul_first_cell_with_kind( + s, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE); + if (cell >= 0) actions[ZUL_HEAD_DRINK] = cell + 1; } { @@ -2415,14 +2701,20 @@ static void zul_heuristic_actions(ZulrahState* s, int* actions) { } if (s->zulrah_visible && !s->is_diving) { - if (s->current_form == ZUL_FORM_BLUE) { - actions[ZUL_HEAD_ATTACK] = ZUL_ATK_RANGE; - int spec_cost = osrs_spec_cost(ZUL_RANGE_LOADOUT[s->gear_tier][GEAR_SLOT_WEAPON]); - if (spec_cost > 0 && s->player.special_energy >= spec_cost && !s->player.spec_armed) { - actions[ZUL_HEAD_SPEC] = 1; - } + AttackStyle want_style = s->current_form == ZUL_FORM_BLUE + ? ATTACK_STYLE_RANGED : ATTACK_STYLE_MAGIC; + const uint8_t* want_loadout = s->current_form == ZUL_FORM_BLUE + ? ZUL_RANGE_LOADOUT[s->gear_tier] : ZUL_MAGE_LOADOUT[s->gear_tier]; + if (zul_player_equipped_attack_style(s) != want_style) { + zul_heuristic_gear_swap(s, actions, want_loadout); } else { - actions[ZUL_HEAD_ATTACK] = ZUL_ATK_MAGE; + actions[ZUL_HEAD_PRIMARY] = ZUL_PRIMARY_ATTACK_BASE; + if (want_style == ATTACK_STYLE_RANGED) { + int spec_cost = osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]); + if (spec_cost > 0 && s->player.special_energy >= spec_cost && + !s->player.spec_armed) + actions[ZUL_HEAD_SPEC] = 1; + } } } } @@ -2605,18 +2897,8 @@ static void* zul_get_log(EncounterState* state, EncounterContext* context) { s->log.episode_return += s->episode_return; s->log.episode_length += (float)s->tick; float kills = (float)s->kills_this_episode; - float win = (s->episode_mode == ZUL_EPISODE_TRIP) - ? kills - : ((s->winner == ZUL_OUTCOME_PLAYER_WON) ? 1.0f : 0.0f); - float partial = (s->episode_mode == ZUL_EPISODE_TRIP) - ? zul_current_kill_progress(s) - : 0.0f; - float speed_bonus = (s->episode_mode == ZUL_EPISODE_TRIP) - ? s->score_speed_bonus_sum - : (win > 0.0f - ? zul_score_speed_bonus_for_duration(s->tick) - : 0.0f); - s->log.wins += win; + ZulEpisodeOutcome outcome = zul_episode_outcome(s); + s->log.wins += outcome.win; s->log.zulrah_kills += kills; s->log.damage_dealt += s->total_damage_dealt; s->log.damage_received += s->total_damage_received; @@ -2633,8 +2915,8 @@ static void* zul_get_log(EncounterState* state, EncounterContext* context) { abort(); } s->log.zulrah_tier_n[tier] += 1.0f; - s->log.zulrah_tier_wins[tier] += win; - s->log.zulrah_tier_score_sum[tier] += win + partial + speed_bonus; + s->log.zulrah_tier_wins[tier] += outcome.win; + s->log.zulrah_tier_score_sum[tier] += outcome.score; s->log.zulrah_tier_damage_received[tier] += s->total_damage_received; s->log.zulrah_tier_episode_length[tier] += (float)s->tick; s->log.zulrah_tier_cloud_occupancy_ticks[tier] += @@ -2674,7 +2956,7 @@ static void zul_emit_player_projectile_profile( int p_start_delay = s->player_attack_timing.visual_start_delay_ticks * 30; int visual_damage = sequence_index == sequence_count - 1 ? damage : 0; - (void)osrs_emit_combat_projectile_profile_player_to_npc(ov, profile, &(OsrsCombatProjectileEmitSpec){ + OsrsCombatProjectileEmitSpec emit_spec = { .src_x = s->player.x, .src_y = s->player.y, .dst_x = s->zulrah.x, @@ -2690,7 +2972,8 @@ static void zul_emit_player_projectile_profile( .fallback_end_h = fallback_end_h, .curve = 16, .splash_gfx_id = GFX_SPLASH, - }); + }; + (void)osrs_emit_combat_projectile_profile_player_to_npc(ov, profile, &emit_spec); } static void zul_emit_player_attack_projectiles(ZulrahState* s, EncounterOverlay* ov) { @@ -2768,7 +3051,7 @@ static void zul_render_post_tick(EncounterState* state, EncounterContext* contex ov->projectile_count = 0; for (int i = 0; i < s->attack_event_count; i++) { if (s->attack_events[i].style == 4) { - int pi = osrs_emit_projectile_with_spec(ov, &(OsrsProjectileEventSpec){ + OsrsProjectileEventSpec spawn_spec = { .src_x = s->attack_events[i].src_x, .src_y = s->attack_events[i].src_y, .dst_x = s->attack_events[i].dst_x, @@ -2784,7 +3067,8 @@ static void zul_render_post_tick(EncounterState* state, EncounterContext* contex .dst_size = 1, .model_id = GFX_SNAKELING_SPAWN_MODEL, .anim_id = GFX_SNAKELING_SPAWN_ANIM, - }, 0); + }; + int pi = osrs_emit_projectile_with_spec(ov, &spawn_spec, 0); encounter_set_projectile_source_npc_slot(ov, pi, 0); } else if (s->attack_events[i].style == 2) { continue; @@ -2793,7 +3077,7 @@ static void zul_render_post_tick(EncounterState* state, EncounterContext* contex ? GFX_RANGED_PROJ_MODEL : GFX_MAGIC_PROJ_MODEL; int zul_proj_anim = (s->attack_events[i].style == 0) ? GFX_RANGED_PROJ_ANIM : GFX_MAGIC_PROJ_ANIM; - (void)osrs_emit_projectile_npc_to_player(ov, &(OsrsProjectileEventSpec){ + OsrsProjectileEventSpec attack_spec = { .src_x = s->attack_events[i].src_x, .src_y = s->attack_events[i].src_y, .dst_x = s->attack_events[i].dst_x, @@ -2809,11 +3093,12 @@ static void zul_render_post_tick(EncounterState* state, EncounterContext* contex .dst_size = 1, .model_id = zul_proj_model, .anim_id = zul_proj_anim, - }, 0); + }; + (void)osrs_emit_projectile_npc_to_player(ov, &attack_spec, 0); } } for (int i = 0; i < s->cloud_event_count; i++) { - int pi = osrs_emit_projectile_with_spec(ov, &(OsrsProjectileEventSpec){ + OsrsProjectileEventSpec cloud_spec = { .src_x = s->cloud_events[i].src_x, .src_y = s->cloud_events[i].src_y, .dst_x = s->cloud_events[i].dst_x, @@ -2829,7 +3114,8 @@ static void zul_render_post_tick(EncounterState* state, EncounterContext* contex .dst_size = 1, .model_id = GFX_CLOUD_PROJ_MODEL, .anim_id = GFX_CLOUD_PROJ_ANIM, - }, 0); + }; + int pi = osrs_emit_projectile_with_spec(ov, &cloud_spec, 0); encounter_set_projectile_source_npc_slot(ov, pi, 0); } zul_emit_player_attack_projectiles(s, ov); @@ -2839,43 +3125,25 @@ static int zul_get_winner(EncounterState* state, EncounterContext* context) { return ((ZulrahState*)state)->winner; } -static int zul_attack_action_for_weapon(uint8_t weapon) { - AttackStyle style = (AttackStyle)get_item_attack_style(weapon); - return (style == ATTACK_STYLE_MAGIC) ? ZUL_ATK_MAGE : ZUL_ATK_RANGE; -} - static void zul_translate_human_commands(HumanInput* hi, int* actions, ZulrahState* s) { for (int h = 0; h < ZUL_NUM_ACTION_HEADS; h++) actions[h] = 0; - uint8_t queued_weapon = s->player.equipped[GEAR_SLOT_WEAPON]; int path_command_seen = 0; for (int i = 0; i < hi->commands.count; i++) { const HumanCommand* cmd = &hi->commands.items[i]; - if (cmd->kind == HUMAN_COMMAND_EQUIP_INVENTORY_ITEM && - cmd->gear_slot == GEAR_SLOT_WEAPON && - cmd->item_db_idx >= 0 && cmd->item_db_idx < NUM_ITEMS) { - queued_weapon = (uint8_t)cmd->item_db_idx; - } - switch (cmd->kind) { case HUMAN_COMMAND_WALK: path_command_seen = 1; s->player_dest_x = cmd->world_x; s->player_dest_y = cmd->world_y; s->player_dest_explicit = 1; - actions[ZUL_HEAD_ATTACK] = ZUL_ATK_NONE; + actions[ZUL_HEAD_PRIMARY] = 0; osrs_interaction_clear(&s->interaction); break; case HUMAN_COMMAND_ATTACK_NPC: - path_command_seen = 1; - actions[ZUL_HEAD_ATTACK] = zul_attack_action_for_weapon(queued_weapon); - s->player_dest_x = -1; - s->player_dest_y = -1; - s->player_dest_explicit = 0; - break; case HUMAN_COMMAND_SPELL_TARGET: path_command_seen = 1; - actions[ZUL_HEAD_ATTACK] = ZUL_ATK_MAGE; + actions[ZUL_HEAD_PRIMARY] = ZUL_PRIMARY_ATTACK_BASE; s->player_dest_x = -1; s->player_dest_y = -1; s->player_dest_explicit = 0; @@ -2886,17 +3154,26 @@ static void zul_translate_human_commands(HumanInput* hi, int* actions, ZulrahSta case HUMAN_COMMAND_OFFENSIVE_PRAYER: actions[ZUL_HEAD_OFFENSIVE] = cmd->offensive_prayer; break; - case HUMAN_COMMAND_EAT: - actions[ZUL_HEAD_FOOD] = cmd->food == 1 ? 2 : 1; + case HUMAN_COMMAND_EAT: { + int cell = zul_first_cell_with_kind(s, OSRS_CLICK_EAT, + cmd->food == 1 ? OSRS_CONSUMABLE_KARAMBWAN : OSRS_CONSUMABLE_SHARK_FOOD); + if (cell >= 0) actions[ZUL_HEAD_EAT] = cell + 1; break; - case HUMAN_COMMAND_DRINK: - if (cmd->potion == POTION_BREW) actions[ZUL_HEAD_FOOD] = 1; - else if (cmd->potion == POTION_RESTORE || - cmd->potion == POTION_PRAYER_POT) actions[ZUL_HEAD_POTION] = 1; - else if (cmd->potion == POTION_ANTIVENOM) actions[ZUL_HEAD_POTION] = 2; + } + case HUMAN_COMMAND_DRINK: { + OsrsConsumableKind kind = OSRS_CONSUMABLE_NONE; + if (cmd->potion == POTION_RESTORE || cmd->potion == POTION_PRAYER_POT) + kind = OSRS_CONSUMABLE_PRAYER_RESTORE; + else if (cmd->potion == POTION_ANTIVENOM) + kind = OSRS_CONSUMABLE_ANTIVENOM_PLUS; + if (kind != OSRS_CONSUMABLE_NONE) { + int cell = zul_first_cell_with_kind(s, OSRS_CLICK_DRINK, kind); + if (cell >= 0) actions[ZUL_HEAD_DRINK] = cell + 1; + } break; + } case HUMAN_COMMAND_SPEC_TOGGLE: - actions[ZUL_HEAD_SPEC] = 1; + actions[ZUL_HEAD_SPEC] = s->player.spec_armed ? 2 : 1; break; case HUMAN_COMMAND_EQUIP_INVENTORY_ITEM: case HUMAN_COMMAND_FIGHT_STYLE: @@ -2914,7 +3191,7 @@ static void zul_translate_human_commands(HumanInput* hi, int* actions, ZulrahSta s->player_dest_x = hi->pending_move_x; s->player_dest_y = hi->pending_move_y; s->player_dest_explicit = 1; - actions[ZUL_HEAD_ATTACK] = ZUL_ATK_NONE; + actions[ZUL_HEAD_PRIMARY] = 0; osrs_interaction_clear(&s->interaction); } } @@ -2925,8 +3202,7 @@ static void zul_step_human_commands(EncounterState* state, EncounterContext* con s->human_command_mode = 1; s->human_commands = hi->commands.items; s->human_command_count = hi->commands.count; - zul_sync_human_gear_style(s); - zul_refresh_human_loadout_stats(s); + zul_mark_live_stats_dirty(s); zul_translate_human_commands(hi, actions, s); zul_step(state, context, actions); s->human_commands = NULL; @@ -2949,6 +3225,7 @@ static void zul_destroy_context(EncounterContext* context) { static void zul_init_state_ctx(EncounterState* state, EncounterContext* context) { (void)context; memset(state, 0, sizeof(ZulrahState)); + ((ZulrahState*)state)->reward_config = zul_default_reward_config(); } static const EncounterDef ENCOUNTER_ZULRAH = { @@ -2981,7 +3258,7 @@ static const EncounterDef ENCOUNTER_ZULRAH = { .arena_base_y = 0, .arena_width = ZUL_ARENA_SIZE, .arena_height = ZUL_ARENA_SIZE, - .head_move = ZUL_HEAD_MOVE, + .head_move = ZUL_HEAD_PRIMARY, .head_prayer = ZUL_HEAD_PRAYER, .head_target = -1, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc index 7c35fd2fe5..c6ebedf61e 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc @@ -16,12 +16,28 @@ static AttackStyle inf_player_equipped_attack_style(const InfernoState* s) { static void inf_refresh_human_loadout_stats(InfernoState* s) { AttackStyle style = inf_player_equipped_attack_style(s); - int spell_base_damage = (style == ATTACK_STYLE_MAGIC) ? 30 : 0; + int spell_base_damage = (style == ATTACK_STYLE_MAGIC) + ? inf_player_magic_base_damage(&s->player) : 0; encounter_compute_player_equipped_stats( &s->player, style, s->player.fight_style, spell_base_damage, &s->human_loadout_stats); } +static FightStyle inf_live_fight_style_for(AttackStyle style) { + if (style == ATTACK_STYLE_MAGIC) return FIGHT_STYLE_AUTOCAST; + if (style == ATTACK_STYLE_RANGED) return FIGHT_STYLE_RAPID; + return FIGHT_STYLE_ACCURATE; +} + +static void inf_refresh_live_stats(InfernoState* s) { + AttackStyle style = inf_player_equipped_attack_style(s); + int spell_base_damage = (style == ATTACK_STYLE_MAGIC) + ? inf_player_magic_base_damage(&s->player) : 0; + encounter_compute_player_equipped_stats( + &s->player, style, inf_live_fight_style_for(style), spell_base_damage, + &s->live_stats); +} + static const EncounterLoadoutStats* inf_current_loadout_stats( InfernoState* s, const InfernoContext* ctx @@ -30,7 +46,7 @@ static const EncounterLoadoutStats* inf_current_loadout_stats( inf_refresh_human_loadout_stats(s); return &s->human_loadout_stats; } - return &s->loadout_stats[s->weapon_set]; + return &s->live_stats; } static int inf_player_weapon_is(const InfernoState* s, uint8_t item) { @@ -46,10 +62,6 @@ typedef struct { uint8_t weapon; } InfPlayerAttack; -static int inf_is_barrage_spell(int spell) { - return spell == ENCOUNTER_SPELL_BLOOD || spell == ENCOUNTER_SPELL_ICE; -} - static int inf_spell_magic_level_requirement(int spell) { if (spell == ENCOUNTER_SPELL_ICE) return ICE_BARRAGE_LEVEL; if (spell == ENCOUNTER_SPELL_BLOOD) return BLOOD_BARRAGE_LEVEL; @@ -63,20 +75,6 @@ static int inf_spell_action_to_spell(int action_spell) { return ENCOUNTER_SPELL_NONE; } -static int inf_spell_base_damage(int spell) { - if (!inf_is_barrage_spell(spell)) { - fprintf(stderr, "BUG: invalid Inferno spell %d\n", spell); - abort(); - } - return 30; -} - -static int inf_player_autocast_spell(const Player* p) { - return p->autocast_spell == ENCOUNTER_SPELL_ICE - ? ENCOUNTER_SPELL_ICE - : ENCOUNTER_SPELL_BLOOD; -} - static FightStyle inf_player_spell_fight_style(const Player* p) { return (p->autocast_defensive || p->fight_style == FIGHT_STYLE_DEFENSIVE_AUTOCAST) ? FIGHT_STYLE_DEFENSIVE_AUTOCAST @@ -99,8 +97,7 @@ static void inf_compute_manual_spell_stats( inf_player_spell_fight_style(&s->player), inf_spell_base_damage(spell), out); - out->attack_speed = 5; - out->attack_range = 10; + inf_apply_spell_cast_stats(out); } static int inf_resolve_player_attack_ctx( @@ -137,8 +134,7 @@ static int inf_resolve_player_attack_ctx( out->spell = autocast_spell; out->is_barrage = 1; - out->stats.attack_speed = 5; - out->stats.attack_range = 10; + inf_apply_spell_cast_stats(&out->stats); return 1; } @@ -221,7 +217,7 @@ static int inf_player_can_attack_npc_from_current_tile( const InfNPC* npc = &s->npcs[npc_idx]; if (!npc->active || npc->death_ticks != 0 || npc->hp <= 0) return 0; - const EncounterLoadoutStats* ls = &s->loadout_stats[s->weapon_set]; + const EncounterLoadoutStats* ls = &s->live_stats; OsrsLosQuery los_query = inf_player_los_query(s); return encounter_player_can_attack( s->player.x, @@ -707,16 +703,18 @@ static void inf_npc_attack_ctx(InfernoState* s, InfernoContext* ctx, int idx) { } else { int max_hit = osrs_npc_magic_max_hit(stats->magic_base_dmg, stats->magic_dmg_pct); int dmg = encounter_rand_int(&s->rng_state, max_hit + 1); + EncounterProjectileTiming zuk_timing = + inf_npc_projectile_timing(npc->type, ATTACK_STYLE_MAGIC, dist); inf_queue_player_pending_hit(s, idx, npc->type, (EncounterPendingHit){ - .damage = dmg, - .ticks_remaining = 3, + .ticks_remaining = (int8_t)zuk_timing.damage_delay_ticks, .attack_style = ATTACK_STYLE_NONE, .check_prayer = 0, .prayer_check_delay = 0, .spell_type = 0, - .source_npc_type = npc->type, + .source_npc_type = (int8_t)npc->type, .hit_success = 1, .elysian_reduced = 0, + .damage = (int16_t)dmg, }); s->last_hit_by_type = INF_NPC_ZUK; npc->attacked_this_tick = 1; @@ -835,19 +833,19 @@ static void inf_npc_attack_ctx(InfernoState* s, InfernoContext* ctx, int idx) { EncounterPendingHit hit; if (is_jad) { hit = (EncounterPendingHit){ - .damage = dmg, - .ticks_remaining = hit_delay, - .attack_style = actual_style, + .ticks_remaining = (int8_t)hit_delay, + .attack_style = (int8_t)actual_style, .check_prayer = 1, .prayer_check_delay = INF_JAD_PROJECTILE_DELAY + 1, .spell_type = ENCOUNTER_SPELL_NONE, - .source_npc_type = npc->type, - .hit_success = accuracy_hit, + .source_npc_type = (int8_t)npc->type, + .hit_success = (int8_t)accuracy_hit, .elysian_reduced = 0, + .damage = (int16_t)dmg, }; } else { hit = encounter_pending_hit_resolved_at_throw( - dmg, hit_delay - 1, actual_style, s->player.prayer, + dmg, hit_delay, actual_style, s->player.prayer, npc->type, 0 , accuracy_hit, NULL); } inf_queue_player_pending_hit(s, idx, npc->type, hit); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc index 9760f0b2a1..56c8b95db1 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc @@ -1,7 +1,9 @@ #line 5216 "encounter_inferno.h" #define INF_PLAYER_OBS_SIZE 75 #define INF_PILLAR_OBS_SIZE (INF_NUM_PILLARS * 5) -#define INF_BASE_NPC_OBS_SIZE 896 +#define INF_NPC_TYPE_ONEHOT 14 +#define INF_NPC_SLOT_FEATURES (INF_NPC_TYPE_ONEHOT + 34) +#define INF_BASE_NPC_OBS_SIZE (INF_OBS_NPCS * INF_NPC_SLOT_FEATURES) #define INF_TOTAL_NPC_OBS_SIZE INF_BASE_NPC_OBS_SIZE #define INF_STEP_OUT_FORECAST_HORIZON 4 #define INF_STEP_OUT_FORECAST_ACTION_FEATURES 8 @@ -11,7 +13,19 @@ #define INF_FEATURES_PER_SPARK 7 #define INF_PENDING_HIT_OBS_SIZE (INF_FEATURES_PER_HIT * ENCOUNTER_MAX_PENDING_HITS) #define INF_PENDING_SPARK_OBS_SIZE (INF_FEATURES_PER_SPARK * INF_SPARK_OBS_SLOTS) -#define INF_NUM_OBS (INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + INF_TOTAL_NPC_OBS_SIZE + INF_STEP_OUT_FORECAST_OBS_SIZE + INF_PENDING_HIT_OBS_SIZE + INF_PENDING_SPARK_OBS_SIZE) +#define INF_INVENTORY_OBS_SIZE (OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES) +#define INF_EQUIPPED_OBS_SIZE (NUM_GEAR_SLOTS * OSRS_EQUIPPED_SELF_OBS_FEATURES) +#define INF_OBS_AFTER_PLAYER INF_PLAYER_OBS_SIZE +#define INF_OBS_AFTER_PILLARS (INF_OBS_AFTER_PLAYER + INF_PILLAR_OBS_SIZE) +#define INF_OBS_AFTER_NPCS (INF_OBS_AFTER_PILLARS + INF_TOTAL_NPC_OBS_SIZE) +#define INF_OBS_AFTER_FORECAST (INF_OBS_AFTER_NPCS + INF_STEP_OUT_FORECAST_OBS_SIZE) +#define INF_OBS_AFTER_PENDING_HITS (INF_OBS_AFTER_FORECAST + INF_PENDING_HIT_OBS_SIZE) +#define INF_OBS_AFTER_SPARKS (INF_OBS_AFTER_PENDING_HITS + INF_PENDING_SPARK_OBS_SIZE) +#define INF_OBS_AFTER_INVENTORY (INF_OBS_AFTER_SPARKS + INF_INVENTORY_OBS_SIZE) +#define INF_NUM_OBS (INF_OBS_AFTER_INVENTORY + INF_EQUIPPED_OBS_SIZE) + +static_assert(INF_NPC_TYPE_ONEHOT == INF_NUM_NPC_TYPES, + "the per-NPC obs one-hot is the NPC type one-hot; a new NPC type must widen it"); typedef struct { int melee_count; @@ -422,49 +436,6 @@ static void inf_restore_step_out_forecast_action_state( sim, prev_player_x, prev_player_y); } -#ifdef INF_POISON_FORECAST -static void inf_poison_step_out_forecast_restore_source( - InfernoState* restore_source, - const InfernoState* s, - const int slots[INF_MAX_NPCS], - int slot_count -) { - memset(restore_source, 0xAA, sizeof(*restore_source)); - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - restore_source->npcs[i] = s->npcs[i]; - } - restore_source->rng_state = s->rng_state; - memcpy( - restore_source->npc_collision_flags, - s->npc_collision_flags, - sizeof(restore_source->npc_collision_flags)); -} - -static void inf_poison_step_out_forecast_check_player_flags( - const InfernoState* sim, - const InfernoState* s, - const Player* moved -) { - InfernoState oracle = *s; - memset( - oracle.player_collision_flags, - 0xAA, - sizeof(oracle.player_collision_flags)); - oracle.player.x = moved->x; - oracle.player.y = moved->y; - oracle.player.is_running = moved->is_running; - inf_rebuild_player_collision_flags(&oracle); - if (memcmp( - sim->player_collision_flags, - oracle.player_collision_flags, - sizeof(sim->player_collision_flags)) != 0) { - fprintf(stderr, "inferno forecast player collision restore mismatch\n"); - abort(); - } -} -#endif - static void inf_step_out_forecast_finalize_action( InfStepOutForecastAction* action ) { @@ -1169,13 +1140,6 @@ static void inf_build_step_out_forecast_exact_ctx( INF_PROF_FORECAST_STATE_COPY, INF_PROFILE_NOW_MS() - base_copy_t0); } -#endif - const InfernoState* restore_source = s; -#ifdef INF_POISON_FORECAST - InfernoState poison_restore_source; - inf_poison_step_out_forecast_restore_source( - &poison_restore_source, s, forecast_slots, forecast_slot_count); - restore_source = &poison_restore_source; #endif for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { InfStepOutForecastAction* action = &out->actions[action_idx]; @@ -1214,10 +1178,7 @@ static void inf_build_step_out_forecast_exact_ctx( double state_copy_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; #endif inf_restore_step_out_forecast_action_state( - &sim, restore_source, forecast_slots, forecast_slot_count, &moved); -#ifdef INF_POISON_FORECAST - inf_poison_step_out_forecast_check_player_flags(&sim, s, &moved); -#endif + &sim, s, forecast_slots, forecast_slot_count, &moved); #ifdef INF_PROFILE_ENABLED if (inf_prof_enabled) { INF_PROFILE_ADD( diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc index 5c87721040..2ccf110939 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc @@ -1,4 +1,30 @@ #line 862 "encounter_inferno.h" +static inline int inf_is_barrage_spell(int spell) { + return spell == ENCOUNTER_SPELL_BLOOD || spell == ENCOUNTER_SPELL_ICE; +} + +static inline int inf_spell_base_damage(int spell) { + if (spell == ENCOUNTER_SPELL_ICE) return ICE_BARRAGE_MAX_HIT; + if (spell == ENCOUNTER_SPELL_BLOOD) return BLOOD_BARRAGE_MAX_HIT; + fprintf(stderr, "BUG: invalid Inferno spell %d\n", spell); + abort(); +} + +static inline int inf_player_autocast_spell(const Player* p) { + return p->autocast_spell == ENCOUNTER_SPELL_ICE + ? ENCOUNTER_SPELL_ICE + : ENCOUNTER_SPELL_BLOOD; +} + +static inline int inf_player_magic_base_damage(const Player* p) { + return inf_spell_base_damage(inf_player_autocast_spell(p)); +} + +static inline void inf_apply_spell_cast_stats(EncounterLoadoutStats* out) { + out->attack_speed = COMBAT_SPELL_CAST_SPEED; + out->attack_range = COMBAT_SPELL_ATTACK_RANGE; +} + static EncounterPendingHit* inf_queue_npc_pending_hit( InfernoState* s, int npc_idx, @@ -24,13 +50,13 @@ static EncounterPendingHit* inf_queue_npc_pending_hit( } EncounterPendingHit hit = { - .damage = damage, - .ticks_remaining = ticks_remaining, - .attack_style = attack_style, + .ticks_remaining = (int8_t)ticks_remaining, + .attack_style = (int8_t)attack_style, .check_prayer = 0, - .spell_type = spell_type, - .hit_success = hit_success, + .spell_type = (int8_t)spell_type, + .hit_success = (int8_t)hit_success, .elysian_reduced = 0, + .damage = (int16_t)damage, }; return encounter_pending_hit_queue_push( &npc->pending_hits, hit, "inferno npc", s->tick, npc_idx, npc->type); @@ -42,7 +68,7 @@ static EncounterPendingHit* inf_queue_player_pending_hit( int npc_type, EncounterPendingHit hit ) { - hit.source_npc_slot = npc_idx; + hit.source_npc_slot = (int8_t)npc_idx; return encounter_pending_hit_queue_push( &s->player_pending_hits, hit, "inferno player", s->tick, npc_idx, npc_type); } @@ -746,7 +772,6 @@ static InfConfig inf_default_config(void) { .zuk_healer_reward_mode = INF_ZUK_HEALER_REWARD_MODE_BASELINE, .post_jad_zuk_multiplier = 1.0f, .jad_alive_zuk_multiplier = 1.0f, - .oracle_mode = 0, .shield_tag_reward_coeff = 0.0f, .joseph_reward_mode = INF_JOSEPH_REWARD_MODE_OFF, }; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc index 58e54f8477..0453c0916c 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc @@ -39,7 +39,7 @@ static const int INF_SPAWN_POS[INF_NUM_SPAWN_POS][2] = { #define INF_WAVE_ZUK (INF_NUM_WAVES - 1) #define INF_START_WAVE_ZUK_JAD (INF_WAVE_ZUK + 1) #define INF_MAX_PUBLIC_START_WAVE (INF_NUM_WAVES + 2) -#define INF_NUM_ACTION_HEADS 9 +#define INF_NUM_ACTION_HEADS 19 typedef enum { INF_NPC_NIBBLER = 0, @@ -130,6 +130,11 @@ typedef struct { int can_move; } InfNPCStats; +/** attack_decision_ticks: ticks between inf_npc_attack decisions, when that is + not MONSTER_DATABASE attack_speed. 0 means use the database. Jal-Ak is the + only deviation: the wiki gives it attack speed 6 and a protection-prayer + scan 3 ticks before each attack, and inferno spends one decision on the scan + and one on the attack, so its decision cadence is half its attack speed. */ typedef struct { int attack_range; AttackStyle default_style; @@ -140,6 +145,7 @@ typedef struct { int max_hit_cap; int stun_on_spawn; int can_move; + int attack_decision_ticks; } InfNPCOverlay; static const MonsterIndex INF_NPC_TO_MON[INF_NUM_NPC_TYPES] = { @@ -160,20 +166,20 @@ static const MonsterIndex INF_NPC_TO_MON[INF_NUM_NPC_TYPES] = { }; static const InfNPCOverlay INF_NPC_OVERLAY[INF_NUM_NPC_TYPES] = { - [INF_NPC_NIBBLER] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_CRUSH, 0, 0, 0, 0, 1, 1 }, - [INF_NPC_BAT] = { 4, ATTACK_STYLE_RANGED, MELEE_STYLE_STAB, 0, 0, 0, 0, 0, 1 }, - [INF_NPC_BLOB] = { 15, ATTACK_STYLE_MAGIC, MELEE_STYLE_CRUSH, 1, 29, 100, 0, 0, 1 }, - [INF_NPC_BLOB_MELEE] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_CRUSH, 0, 0, 0, 0, 0, 1 }, - [INF_NPC_BLOB_RANGE] = { 15, ATTACK_STYLE_RANGED, MELEE_STYLE_STAB, 0, 0, 0, 0, 0, 1 }, - [INF_NPC_BLOB_MAGE] = { 15, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 0, 18, 100, 0, 0, 1 }, - [INF_NPC_MELEER] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_SLASH, 0, 0, 0, 0, 0, 1 }, - [INF_NPC_RANGER] = { 15, ATTACK_STYLE_RANGED, MELEE_STYLE_CRUSH, 1, 0, 0, 0, 0, 1 }, - [INF_NPC_MAGER] = { 15, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 1, 70, 100, 0, 0, 1 }, - [INF_NPC_JAD] = { 50, ATTACK_STYLE_RANGED, MELEE_STYLE_STAB, 1, 113, 100, 113, 0, 1 }, - [INF_NPC_ZUK] = { 99, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 0, 148, 100, 0, 8, 0 }, - [INF_NPC_HEALER_JAD] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_CRUSH, 0, 0, 0, 0, 1, 1 }, - [INF_NPC_HEALER_ZUK] = { 99, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 0, 10, 100, 0, 1, 0 }, - [INF_NPC_ZUK_SHIELD] = { 0, ATTACK_STYLE_NONE, MELEE_STYLE_STAB, 0, 0, 0, 0, 1, 0 }, + [INF_NPC_NIBBLER] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_CRUSH, 0, 0, 0, 0, 1, 1, 0 }, + [INF_NPC_BAT] = { 4, ATTACK_STYLE_RANGED, MELEE_STYLE_STAB, 0, 0, 0, 0, 0, 1, 0 }, + [INF_NPC_BLOB] = { 15, ATTACK_STYLE_MAGIC, MELEE_STYLE_CRUSH, 1, 29, 100, 0, 0, 1, 3 }, + [INF_NPC_BLOB_MELEE] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_CRUSH, 0, 0, 0, 0, 0, 1, 0 }, + [INF_NPC_BLOB_RANGE] = { 15, ATTACK_STYLE_RANGED, MELEE_STYLE_STAB, 0, 0, 0, 0, 0, 1, 0 }, + [INF_NPC_BLOB_MAGE] = { 15, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 0, 18, 100, 0, 0, 1, 0 }, + [INF_NPC_MELEER] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_SLASH, 0, 0, 0, 0, 0, 1, 0 }, + [INF_NPC_RANGER] = { 15, ATTACK_STYLE_RANGED, MELEE_STYLE_CRUSH, 1, 0, 0, 0, 0, 1, 0 }, + [INF_NPC_MAGER] = { 15, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 1, 70, 100, 0, 0, 1, 0 }, + [INF_NPC_JAD] = { 50, ATTACK_STYLE_RANGED, MELEE_STYLE_STAB, 1, 113, 100, 113, 0, 1, 0 }, + [INF_NPC_ZUK] = { 99, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 0, 148, 100, 0, 8, 0, 0 }, + [INF_NPC_HEALER_JAD] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_CRUSH, 0, 0, 0, 0, 1, 1, 0 }, + [INF_NPC_HEALER_ZUK] = { 99, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 0, 10, 100, 0, 1, 0, 0 }, + [INF_NPC_ZUK_SHIELD] = { 0, ATTACK_STYLE_NONE, MELEE_STYLE_STAB, 0, 0, 0, 0, 1, 0, 0 }, }; static InfNPCStats INF_NPC_STATS[INF_NUM_NPC_TYPES]; @@ -185,8 +191,9 @@ static void inf_build_npc_stats(void) { InfNPCStats* s = &INF_NPC_STATS[i]; s->hp = m->hp; - s->attack_speed = m->attack_speed; - if (i == INF_NPC_BLOB) s->attack_speed = 3; + s->attack_speed = o->attack_decision_ticks > 0 + ? o->attack_decision_ticks + : m->attack_speed; s->size = m->size; s->att_level = m->att_level; s->str_level = m->str_level; @@ -693,7 +700,6 @@ typedef struct { int zuk_healer_reward_mode; float post_jad_zuk_multiplier; float jad_alive_zuk_multiplier; - int oracle_mode; float shield_tag_reward_coeff; int joseph_reward_mode; } InfConfig; @@ -855,6 +861,8 @@ typedef struct { InfWeaponSet weapon_set; InfLoadoutProfile active_loadout_profile; EncounterLoadoutStats loadout_stats[INF_NUM_WEAPON_SETS]; + OsrsInventoryCell inventory_cells[OSRS_INVENTORY_SIZE]; + EncounterLoadoutStats live_stats; EncounterLoadoutStats human_loadout_stats; int stamina_active_ticks; @@ -901,8 +909,8 @@ typedef struct { int total_zuk_healer_out_of_range_ticks; int total_zuk_healer_attackable_ticks; int total_action_mask_checks; - int zero_valid_action_head_count[9]; - int min_valid_action_count_by_head[9]; + int zero_valid_action_head_count[INF_NUM_ACTION_HEADS]; + int min_valid_action_count_by_head[INF_NUM_ACTION_HEADS]; int target_head_valid_healer_count; int target_head_valid_zuk_count; int target_head_valid_set_count; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc index e1fb75b01f..4c108feb47 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -349,42 +349,34 @@ static void inf_write_obs_ctx( int has_timer = (type != INF_NPC_NIBBLER && type != INF_NPC_HEALER_JAD && type != INF_NPC_ZUK_SHIELD); int has_meleer_dig = (type == INF_NPC_MELEER); - int num_features = 12; - if (has_timer) num_features += 1; - if (has_style) num_features += 3; - if (has_los) num_features += 1; - if (has_scan) num_features += 3; - if (has_target_category) num_features += 4; - num_features += 1; - num_features += 6; - if (has_meleer_dig) num_features += 3; - if (n >= 0) { InfNPC* npc = &s->npcs[n]; + for (int ty = 0; ty < INF_NPC_TYPE_ONEHOT; ty++) + obs[i++] = (ty == type) ? 1.0f : 0.0f; obs[i++] = (float)npc->hp / (float)npc->max_hp; obs[i++] = (float)(npc->x - px) / (float)INF_ARENA_WIDTH; obs[i++] = (float)(npc->y - py) / (float)INF_ARENA_HEIGHT; obs[i++] = inf_obs_norm_npc_size(npc->size); - if (has_timer) obs[i++] = (float)npc->attack_timer / 10.0f; + obs[i++] = has_timer ? (float)npc->attack_timer / 10.0f : 0.0f; if (has_style) { int style = (npc->type == INF_NPC_JAD) ? inf_npc_jad(npc)->attack_style : npc->attack_style; obs[i++] = (style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; obs[i++] = (style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; obs[i++] = (style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + } else { + obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; } - if (has_los) obs[i++] = inf_npc_has_los(s, n) ? 1.0f : 0.0f; + obs[i++] = (has_los && inf_npc_has_los(s, n)) ? 1.0f : 0.0f; - if (has_scan) { - if (npc->blob_scanned_prayer >= 0) { - OverheadPrayer scanned = (OverheadPrayer)npc->blob_scanned_prayer; - obs[i++] = (scanned == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; - obs[i++] = (scanned == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; - obs[i++] = (scanned != PRAYER_PROTECT_MAGIC && scanned != PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; - } else { - obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; - } + if (has_scan && npc->blob_scanned_prayer >= 0) { + OverheadPrayer scanned = (OverheadPrayer)npc->blob_scanned_prayer; + obs[i++] = (scanned == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; + obs[i++] = (scanned == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; + obs[i++] = (scanned != PRAYER_PROTECT_MAGIC && scanned != PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; + } else { + obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; } obs[i++] = (float)inf_barrage_total_target_count(s, n) / @@ -418,6 +410,8 @@ static void inf_write_obs_ctx( obs[i++] = (category == INF_TARGET_CATEGORY_ZUK) ? 1.0f : 0.0f; obs[i++] = (category == INF_TARGET_CATEGORY_SHIELD) ? 1.0f : 0.0f; obs[i++] = (category == INF_TARGET_CATEGORY_OTHER_NPC) ? 1.0f : 0.0f; + } else { + obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; } obs[i++] = (osrs_interaction_active(&s->interaction) && s->interaction.target_slot == n) ? 1.0f : 0.0f; obs[i++] = inf_npc_is_phantom_barrage_targetable_now(s, ctx, n) ? 1.0f : 0.0f; @@ -427,9 +421,11 @@ static void inf_write_obs_ctx( obs[i++] = no_los; obs[i++] = (float)npc->dig_freeze_timer / 6.0f; obs[i++] = (float)npc->dig_attack_delay / 6.0f; + } else { + obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; } } else { - for (int j = 0; j < num_features; j++) obs[i++] = 0.0f; + for (int j = 0; j < INF_NPC_SLOT_FEATURES; j++) obs[i++] = 0.0f; } } #ifdef INF_PROFILE_ENABLED @@ -437,11 +433,9 @@ static void inf_write_obs_ctx( #endif { - int expected_npc_end = INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + - INF_TOTAL_NPC_OBS_SIZE; - if (i != expected_npc_end) { + if (i != INF_OBS_AFTER_NPCS) { fprintf(stderr, "FATAL: obs misaligned after NPC section: i=%d expected=%d\n", - i, expected_npc_end); + i, INF_OBS_AFTER_NPCS); abort(); } } @@ -486,11 +480,9 @@ static void inf_write_obs_ctx( #endif { - int expected_forecast_end = INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + - INF_TOTAL_NPC_OBS_SIZE + INF_STEP_OUT_FORECAST_OBS_SIZE; - if (i != expected_forecast_end) { + if (i != INF_OBS_AFTER_FORECAST) { fprintf(stderr, "FATAL: obs misaligned after step-out forecast: i=%d expected=%d\n", - i, expected_forecast_end); + i, INF_OBS_AFTER_FORECAST); abort(); } } @@ -511,6 +503,12 @@ static void inf_write_obs_ctx( INF_PROFILE_MARK(INF_PROF_OBS_PENDING_HITS); #endif + if (i != INF_OBS_AFTER_PENDING_HITS) { + fprintf(stderr, "FATAL: obs misaligned after pending hits: i=%d expected=%d\n", + i, INF_OBS_AFTER_PENDING_HITS); + abort(); + } + int used_sparks[INF_MAX_PENDING_SPARKS] = {0}; for (int slot = 0; slot < INF_SPARK_OBS_SLOTS; slot++) { int best = -1; @@ -542,6 +540,31 @@ static void inf_write_obs_ctx( INF_PROFILE_MARK(INF_PROF_OBS_SPARKS); #endif + if (i != INF_OBS_AFTER_SPARKS) { + fprintf(stderr, "FATAL: obs misaligned before inventory block: i=%d expected=%d\n", + i, INF_OBS_AFTER_SPARKS); + abort(); + } + + { + static const float inf_zero_post_use_deltas[6] = {0}; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { + const OsrsInventoryCell* cell = &s->inventory_cells[c]; + int is_equipped = osrs_inventory_cell_holds_equipped_item( + &s->player, s->inventory_cells, c); + osrs_write_inventory_cell_affordance_features( + &obs[i], cell->item_idx, cell->raw_osrs_id, cell->dose, + is_equipped, inf_zero_post_use_deltas, + s->player.base_hitpoints, s->player.base_prayer, + s->player.base_ranged); + i += OSRS_INVENTORY_CELL_OBS_FEATURES; + } + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + osrs_write_equipped_self_features(&obs[i], s->player.equipped[slot]); + i += OSRS_EQUIPPED_SELF_OBS_FEATURES; + } + } + if (i != INF_NUM_OBS) { fprintf(stderr, "BUG: inf_write_obs wrote %d features, expected %d\n", i, INF_NUM_OBS); abort(); @@ -590,6 +613,27 @@ static int inf_is_human_targetable_npc_slot(EncounterState* state, int npc_slot) state, (EncounterContext*)inf_legacy_context(), npc_slot); } +static int inf_drink_has_effect(const InfernoState* s, OsrsConsumableKind kind) { + const Player* p = &s->player; + switch (kind) { + case OSRS_CONSUMABLE_BREW: + return p->current_hitpoints < p->base_hitpoints; + case OSRS_CONSUMABLE_SUPER_RESTORE: { + int pray_missing = p->base_prayer - p->current_prayer; + int stats_drained = p->current_attack < 99 || p->current_strength < 99 || + p->current_defence < 99 || p->current_ranged < 99 || + p->current_magic < 99; + return stats_drained || pray_missing >= (INF_RESTORE_AMOUNT + 1) / 2; + } + case OSRS_CONSUMABLE_BASTION: + return p->current_ranged >= 99 && p->current_ranged <= 105; + case OSRS_CONSUMABLE_STAMINA: + return s->stamina_active_ticks == 0; + default: + return 0; + } +} + static void inf_write_mask_ctx( EncounterState* state, EncounterContext* context, @@ -600,7 +644,7 @@ static void inf_write_mask_ctx( inf_refresh_current_obs_slots_ctx(s, ctx); int offset = 0; if (s->total_action_mask_checks == 0) { - for (int h = 0; h < 9; h++) { + for (int h = 0; h < INF_NUM_ACTION_HEADS; h++) { if (s->min_valid_action_count_by_head[h] == 0) s->min_valid_action_count_by_head[h] = 1000000; } @@ -642,45 +686,58 @@ static void inf_write_mask_ctx( mask[offset++] = is_targetable ? 1.0f : 0.0f; } - mask[offset++] = 1.0f; - mask[offset++] = (s->weapon_set != INF_GEAR_MAGE) ? 1.0f : 0.0f; - mask[offset++] = (s->weapon_set != INF_GEAR_LONG_RANGE) ? 1.0f : 0.0f; - mask[offset++] = (s->weapon_set != INF_GEAR_BP) ? 1.0f : 0.0f; - - mask[offset++] = 1.0f; - mask[offset++] = (s->player.brew_doses > 0 && - s->player.potion_timer == 0 && - s->player.current_hitpoints < s->player.base_hitpoints) - ? 1.0f : 0.0f; - - mask[offset++] = 1.0f; { - int pray_missing = s->player.base_prayer - s->player.current_prayer; - int stats_drained = s->player.current_attack < 99 || s->player.current_strength < 99 || - s->player.current_defence < 99 || s->player.current_ranged < 99 || - s->player.current_magic < 99; - int pray_worth = pray_missing >= (INF_RESTORE_AMOUNT + 1) / 2; - mask[offset++] = (s->player.restore_doses > 0 && - s->player.potion_timer == 0 && - (stats_drained || pray_worth)) - ? 1.0f : 0.0f; + int cell_equip_slot[OSRS_INVENTORY_SIZE]; + int cell_can_eat[OSRS_INVENTORY_SIZE]; + int cell_can_drink[OSRS_INVENTORY_SIZE]; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { + cell_equip_slot[c] = -1; + cell_can_eat[c] = 0; + cell_can_drink[c] = 0; + const OsrsInventoryCell* cell = &s->inventory_cells[c]; + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); + switch (resolution.click_action) { + case OSRS_CLICK_EQUIP: + if (osrs_can_equip_from_cell(&s->player, s->inventory_cells, c)) + cell_equip_slot[c] = osrs_item_gear_slot(cell->item_idx); + break; + case OSRS_CLICK_EAT: + cell_can_eat[c] = osrs_can_eat_consumable_kind( + &s->player, resolution.consumable_kind); + break; + case OSRS_CLICK_DRINK: + cell_can_drink[c] = cell->dose > 0 && + s->player.potion_timer == 0 && + inf_drink_has_effect(s, resolution.consumable_kind); + break; + case OSRS_CLICK_NONE: + break; + } + } + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + mask[offset++] = 1.0f; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) + mask[offset++] = cell_equip_slot[c] == slot ? 1.0f : 0.0f; + } + mask[offset++] = 1.0f; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) + mask[offset++] = cell_can_eat[c] ? 1.0f : 0.0f; + mask[offset++] = 1.0f; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) + mask[offset++] = cell_can_drink[c] ? 1.0f : 0.0f; } - mask[offset++] = (s->player.bastion_doses > 0 && s->player.potion_timer == 0 && - s->player.current_ranged >= 99 && s->player.current_ranged <= 105) - ? 1.0f : 0.0f; - mask[offset++] = (s->player.stamina_doses > 0 && - s->player.potion_timer == 0 && - s->stamina_active_ticks == 0) - ? 1.0f : 0.0f; mask[offset++] = 1.0f; mask[offset++] = s->player.current_magic >= BLOOD_BARRAGE_LEVEL ? 1.0f : 0.0f; mask[offset++] = s->player.current_magic >= ICE_BARRAGE_LEVEL ? 1.0f : 0.0f; mask[offset++] = 1.0f; - mask[offset++] = (s->weapon_set == INF_GEAR_BP && + mask[offset++] = (!s->player.spec_armed && + s->player.equipped[GEAR_SLOT_WEAPON] == ITEM_TOXIC_BLOWPIPE && s->player.special_energy >= BLOWPIPE_SPEC_COST) ? 1.0f : 0.0f; + mask[offset++] = s->player.spec_armed ? 1.0f : 0.0f; mask[offset++] = 1.0f; mask[offset++] = s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE ? 1.0f : 0.0f; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc index 9cce365979..7261ae1cfa 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -1,26 +1,32 @@ #line 3605 "encounter_inferno.h" -#define INF_HEAD_MOVE 0 -#define INF_HEAD_PRAYER 1 -#define INF_HEAD_TARGET 2 -#define INF_HEAD_GEAR 3 -#define INF_HEAD_EAT 4 -#define INF_HEAD_POTION 5 -#define INF_HEAD_SPELL 6 -#define INF_HEAD_SPEC 7 -#define INF_HEAD_OFFENSIVE 8 +#define INF_HEAD_MOVE 0 +#define INF_HEAD_PRAYER 1 +#define INF_HEAD_TARGET 2 +#define INF_HEAD_EQUIP_BASE 3 +#define INF_HEAD_EQUIP_SLOT(slot) (INF_HEAD_EQUIP_BASE + (slot)) +#define INF_HEAD_EAT (INF_HEAD_EQUIP_BASE + NUM_GEAR_SLOTS) +#define INF_HEAD_DRINK (INF_HEAD_EAT + 1) +#define INF_HEAD_SPELL (INF_HEAD_DRINK + 1) +#define INF_HEAD_SPEC (INF_HEAD_SPELL + 1) +#define INF_HEAD_OFFENSIVE (INF_HEAD_SPEC + 1) +#define INF_INV_CLICK_HEADS (NUM_GEAR_SLOTS + 2) +#define INF_INV_CLICK_DIM (OSRS_INVENTORY_SIZE + 1) +#define INF_SPEC_DIM 3 #define INF_ACTION_DIMS_INIT { \ ENCOUNTER_MOVE_ACTIONS, \ ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION, \ INF_OBS_NPCS+1, \ - 4, \ - 2, \ - 4, \ + INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, \ + INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, \ + INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, \ + INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, \ + INF_INV_CLICK_DIM, \ 3, \ - 2, \ + INF_SPEC_DIM, \ ENCOUNTER_OFFENSIVE_DIM \ } static const int INF_ACTION_DIMS[INF_NUM_ACTION_HEADS] = INF_ACTION_DIMS_INIT; -#define INF_ACTION_MASK_SIZE (ENCOUNTER_MOVE_ACTIONS + ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION + INF_OBS_NPCS+1 + 4 + 2 + 4 + 3 + 2 + ENCOUNTER_OFFENSIVE_DIM) +#define INF_ACTION_MASK_SIZE (ENCOUNTER_MOVE_ACTIONS + ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION + INF_OBS_NPCS+1 + INF_INV_CLICK_HEADS*INF_INV_CLICK_DIM + 3 + INF_SPEC_DIM + ENCOUNTER_OFFENSIVE_DIM) #define INF_OVERHEAD_SET_REFRESH_REDEMPTION ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE static int inf_action_head_mask_offset(int head) { @@ -187,182 +193,16 @@ static void inf_apply_npc_death(InfernoState* s, int npc_idx) { static void inf_apply_blood_barrage_heal_damage(InfernoState* s, int damage) { if (damage <= 0) return; int hp_before = s->player.current_hitpoints; - s->player.current_hitpoints += damage / 4; + s->player.current_hitpoints += damage * BLOOD_BARRAGE_HEAL_PERCENT / 100; if (s->player.current_hitpoints > s->player.base_hitpoints) s->player.current_hitpoints = s->player.base_hitpoints; s->tick_scratch.blood_heal += s->player.current_hitpoints - hp_before; } -typedef struct { - int target_slot; - int gear; - int overhead_style; - int offensive; -} InfOraclePick; - -static int inf_oracle_trigger_active(const InfernoState* s, int mode) { - int zuk_idx = inf_find_live_zuk_idx(s); - if (zuk_idx < 0) return 0; - int zuk_hp = s->npcs[zuk_idx].hp; - if (mode == 1 || mode == 2 || mode == 8) return zuk_hp <= 300; - if (mode == 3) return zuk_hp <= 240; - if (mode >= 4 && mode <= 7) return s->zuk.jad_spawned || zuk_hp <= 600; - if (mode == 9 || mode == 10 || mode == 11) - return s->zuk.healer_spawned && s->total_zuk_healer_tags < 4; - return 0; -} - -static InfOraclePick inf_oracle_pick_full(const InfernoState* s, int mode) { - InfOraclePick pick = { -1, -1, ATTACK_STYLE_NONE, OFFENSIVE_PRAYER_NONE }; - if (mode <= 0 || !inf_oracle_trigger_active(s, mode)) return pick; - - if (mode == 9) { - for (int o = 33; o < 37; o++) { - int n = s->current_obs_slots[o]; - if (n < 0 || n >= INF_MAX_NPCS) continue; - const InfNPC* npc = &s->npcs[n]; - if (!npc->active || npc->death_ticks != 0 || - npc->type != INF_NPC_HEALER_ZUK) - continue; - if (npc->aggro_target >= 0 && npc->aggro_target < INF_MAX_NPCS && - s->npcs[npc->aggro_target].active && - s->npcs[npc->aggro_target].type == INF_NPC_ZUK) { - pick.target_slot = o; - return pick; - } - } - return pick; - } - - if (mode == 10 || mode == 11) { - if (!inf_player_behind_zuk_shield_now(s)) return pick; - if (mode == 11 && s->player.attack_timer != 0) return pick; - - for (int o = 33; o < 37; o++) { - int n = s->current_obs_slots[o]; - if (!inf_is_untagged_live_zuk_healer_slot(s, n)) - continue; - if (inf_player_can_attack_npc_from_current_tile(s, n)) { - pick.target_slot = o; - return pick; - } - } - return pick; - } - - for (int o = 22; o < 25; o++) { - int n = s->current_obs_slots[o]; - if (n >= 0 && n < INF_MAX_NPCS && - s->npcs[n].active && s->npcs[n].death_ticks == 0 && - s->npcs[n].type == INF_NPC_JAD) { - pick.target_slot = o; - pick.gear = INF_GEAR_LONG_RANGE; - pick.offensive = OFFENSIVE_PRAYER_RIGOUR; - AttackStyle jas = inf_npc_jad_const(&s->npcs[n])->attack_style; - if (jas == ATTACK_STYLE_RANGED) pick.overhead_style = ATTACK_STYLE_RANGED; - else if (jas == ATTACK_STYLE_MAGIC) pick.overhead_style = ATTACK_STYLE_MAGIC; - return pick; - } - } - if (mode == 1) return pick; - - for (int o = 33; o < 37; o++) { - int n = s->current_obs_slots[o]; - if (n >= 0 && n < INF_MAX_NPCS && - s->npcs[n].active && s->npcs[n].death_ticks == 0 && - s->npcs[n].type == INF_NPC_HEALER_ZUK) { - pick.target_slot = o; - pick.gear = INF_GEAR_BP; - pick.offensive = OFFENSIVE_PRAYER_RIGOUR; - pick.overhead_style = ATTACK_STYLE_MAGIC; - return pick; - } - } - - for (int o = 0; o < 22; o++) { - int n = s->current_obs_slots[o]; - if (n < 0 || n >= INF_MAX_NPCS) continue; - if (!s->npcs[n].active || s->npcs[n].death_ticks != 0) continue; - int t = s->npcs[n].type; - if (t == INF_NPC_ZUK || t == INF_NPC_ZUK_SHIELD || - t == INF_NPC_JAD || - t == INF_NPC_HEALER_ZUK || t == INF_NPC_HEALER_JAD) - continue; - pick.target_slot = o; - pick.offensive = OFFENSIVE_PRAYER_RIGOUR; - if (t == INF_NPC_MAGER) { - pick.gear = INF_GEAR_LONG_RANGE; - pick.overhead_style = ATTACK_STYLE_MAGIC; - } else if (t == INF_NPC_RANGER) { - pick.gear = INF_GEAR_LONG_RANGE; - pick.overhead_style = ATTACK_STYLE_RANGED; - } else if (t == INF_NPC_MELEER) { - pick.gear = INF_GEAR_BP; - pick.overhead_style = ATTACK_STYLE_MELEE; - } else { - pick.gear = INF_GEAR_BP; - } - return pick; - } - - return pick; -} - -static int inf_oracle_overrides_target(int mode) { - return mode >= 1 && mode <= 11; -} - -static int inf_oracle_overrides_gear_offensive(int mode) { - return mode == 6 || mode == 7 || mode == 8; -} - -static int inf_oracle_overrides_overhead(int mode) { - return mode == 5 || mode == 7 || mode == 8; -} - -static int inf_oracle_overhead_action_for(int wanted_style) { - if (wanted_style == ATTACK_STYLE_RANGED) return ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; - if (wanted_style == ATTACK_STYLE_MAGIC) return ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC; - if (wanted_style == ATTACK_STYLE_MELEE) return ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; - return ENCOUNTER_OVERHEAD_NO_CHANGE; -} - -static int inf_oracle_offensive_action_for(int wanted_offensive) { - if (wanted_offensive == OFFENSIVE_PRAYER_RIGOUR) { - return ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; - } - if (wanted_offensive == OFFENSIVE_PRAYER_PIETY) { - return ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY; - } - if (wanted_offensive == OFFENSIVE_PRAYER_AUGURY) { - return ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY; - } - return ENCOUNTER_OFFENSIVE_NO_CHANGE; -} - -static int inf_oracle_gear_action_for(const InfernoState* s, int wanted_gear) { - if (wanted_gear < 0) return 0; - if ((int)s->weapon_set == wanted_gear) return 0; - return wanted_gear + 1; -} - static void inf_player_pretick(InfernoState* s, InfernoContext* ctx, const int* actions) { OffensivePrayer prev_offensive = s->player.offensive_prayer; int prayer_act = inf_canonical_overhead_action(actions[INF_HEAD_PRAYER]); int offensive_act = actions[INF_HEAD_OFFENSIVE]; - if (ctx->config.oracle_mode > 0) { - InfOraclePick pick = inf_oracle_pick_full(s, ctx->config.oracle_mode); - if (pick.target_slot >= 0) { - if (inf_oracle_overrides_overhead(ctx->config.oracle_mode) && - pick.overhead_style != ATTACK_STYLE_NONE) { - prayer_act = inf_oracle_overhead_action_for(pick.overhead_style); - } - if (inf_oracle_overrides_gear_offensive(ctx->config.oracle_mode) && - pick.offensive != OFFENSIVE_PRAYER_NONE) { - offensive_act = inf_oracle_offensive_action_for(pick.offensive); - } - } - } if (encounter_apply_overhead_action(&s->player.prayer, prayer_act)) { s->player.prayer_just_activated = 1; } @@ -378,12 +218,6 @@ static void inf_player_pretick(InfernoState* s, InfernoContext* ctx, const int* } } -static FightStyle inf_default_fight_style_for_style(AttackStyle style) { - if (style == ATTACK_STYLE_MAGIC) return FIGHT_STYLE_AUTOCAST; - if (style == ATTACK_STYLE_RANGED) return FIGHT_STYLE_RAPID; - return FIGHT_STYLE_ACCURATE; -} - static void inf_note_human_weapon_set(InfernoState* s) { uint8_t weapon = s->player.equipped[GEAR_SLOT_WEAPON]; const uint8_t* const* loadouts = @@ -396,6 +230,69 @@ static void inf_note_human_weapon_set(InfernoState* s) { } } +static void inf_sync_consumable_counters_from_cells(InfernoState* s) { + Player* p = &s->player; + p->brew_doses = 0; + p->restore_doses = 0; + p->bastion_doses = 0; + p->stamina_doses = 0; + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) { + const OsrsInventoryCell* cell = &s->inventory_cells[i]; + if (cell->raw_osrs_id == 0) continue; + OsrsConsumableClick click = + osrs_consumable_click_lookup_raw_osrs_id(cell->raw_osrs_id); + switch (click.consumable_kind) { + case OSRS_CONSUMABLE_BREW: p->brew_doses += cell->dose; break; + case OSRS_CONSUMABLE_SUPER_RESTORE: p->restore_doses += cell->dose; break; + case OSRS_CONSUMABLE_BASTION: p->bastion_doses += cell->dose; break; + case OSRS_CONSUMABLE_STAMINA: p->stamina_doses += cell->dose; break; + default: break; + } + } +} + +static void inf_apply_drink_one_dose_effect(void* ctx, OsrsConsumableKind kind) { + InfernoState* s = (InfernoState*)ctx; + switch (kind) { + case OSRS_CONSUMABLE_BREW: + encounter_apply_brew_heal(&s->player, INF_BREW_HEAL); + s->tick_scratch.brewed = 1; + encounter_apply_consumable_stat_effect( + &s->player, ENCOUNTER_CONSUMABLE_STAT_EFFECT_BREW_DRAIN); + break; + case OSRS_CONSUMABLE_SUPER_RESTORE: + encounter_add_prayer_restore(&s->player, INF_RESTORE_AMOUNT); + encounter_apply_consumable_stat_effect( + &s->player, ENCOUNTER_CONSUMABLE_STAT_EFFECT_RESTORE); + encounter_cap_prayer_restore(&s->player); + break; + case OSRS_CONSUMABLE_BASTION: + encounter_bastion_boost(&s->player); + break; + case OSRS_CONSUMABLE_STAMINA: + s->stamina_active_ticks = 200; + break; + default: + fprintf(stderr, "inferno: consumable kind %d not in inferno kit\n", (int)kind); + abort(); + } + encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, &s->player); + inf_refresh_live_stats(s); +} + +static void inf_apply_eat_cell(InfernoState* s, int cell_idx, OsrsConsumableKind kind) { + FoodType food; + switch (kind) { + case OSRS_CONSUMABLE_SHARK_FOOD: food = FOOD_SHARK; break; + case OSRS_CONSUMABLE_KARAMBWAN: food = FOOD_KARAMBWAN; break; + default: + fprintf(stderr, "inferno: consumable kind %d is not food\n", (int)kind); + abort(); + } + osrs_player_eat_food_effects(&s->player, food); + osrs_inventory_cell_consume_eat(&s->inventory_cells[cell_idx]); +} + static void inf_apply_human_player_commands_ctx(InfernoState* s, InfernoContext* ctx) { int did_change_equipment = 0; for (int i = 0; i < ctx->human_command_count; i++) { @@ -414,7 +311,7 @@ static void inf_apply_human_player_commands_ctx(InfernoState* s, InfernoContext* ? FIGHT_STYLE_DEFENSIVE_AUTOCAST : FIGHT_STYLE_AUTOCAST; } else { - s->player.fight_style = inf_default_fight_style_for_style(style); + s->player.fight_style = inf_live_fight_style_for(style); } inf_note_human_weapon_set(s); } @@ -466,38 +363,6 @@ static void inf_tick_player_ctx( if (ctx->human_command_mode) { inf_apply_human_player_commands_ctx(s, ctx); - } else { - int gear_act = actions[INF_HEAD_GEAR]; - if (inf_oracle_overrides_gear_offensive(ctx->config.oracle_mode)) { - InfOraclePick pick = inf_oracle_pick_full(s, ctx->config.oracle_mode); - if (pick.target_slot >= 0 && pick.gear >= 0) { - gear_act = inf_oracle_gear_action_for(s, pick.gear); - } - } - if (gear_act >= 1) s->total_gear_switches++; - if (gear_act >= 1 && gear_act <= 3) { - InfWeaponSet new_set = (InfWeaponSet)(gear_act - 1); - s->weapon_set = new_set; - GearSet gs = (new_set == INF_GEAR_MAGE) ? GEAR_MAGE : GEAR_RANGED; - const uint8_t* const* loadouts = - inf_loadouts_for_profile(s->active_loadout_profile); - encounter_apply_loadout(&s->player, loadouts[new_set], gs); - } - { - const uint8_t* const* loadouts = - inf_loadouts_for_profile(s->active_loadout_profile); - uint8_t current_weapon = s->player.equipped[GEAR_SLOT_WEAPON]; - if (current_weapon != loadouts[s->weapon_set][GEAR_SLOT_WEAPON]) { - for (int g = 0; g < INF_NUM_WEAPON_SETS; g++) { - if (loadouts[g][GEAR_SLOT_WEAPON] == current_weapon) { - s->weapon_set = (InfWeaponSet)g; - GearSet gs = (g == INF_GEAR_MAGE) ? GEAR_MAGE : GEAR_RANGED; - encounter_apply_loadout(&s->player, loadouts[g], gs); - break; - } - } - } - } } int manual_spell = inf_spell_action_to_spell(actions[INF_HEAD_SPELL]); @@ -505,7 +370,9 @@ static void inf_tick_player_ctx( osrs_tick_special_regen(&s->player); if (actions[INF_HEAD_SPEC] == 1) - osrs_spec_toggle(&s->player.spec_armed); + s->player.spec_armed = 1; + else if (actions[INF_HEAD_SPEC] == 2) + s->player.spec_armed = 0; if (s->tick > 0 && s->tick % 60 == 0) { int* stats[] = { &s->player.current_ranged, &s->player.current_magic, @@ -516,6 +383,7 @@ static void inf_tick_player_ctx( else if (*stats[si] < 99) (*stats[si])++; } encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, &s->player); + inf_refresh_live_stats(s); if (ctx->human_command_mode) inf_refresh_human_loadout_stats(s); } @@ -523,52 +391,57 @@ static void inf_tick_player_ctx( if (s->player.potion_timer > 0) s->player.potion_timer--; if (s->stamina_active_ticks > 0) s->stamina_active_ticks--; - int eat_act = actions[INF_HEAD_EAT]; - if (eat_act == 1 && s->player.brew_doses > 0 && s->player.potion_timer == 0 - && s->player.current_hitpoints < s->player.base_hitpoints) { - encounter_apply_brew_heal_and_timer(&s->player, INF_BREW_HEAL); - s->tick_scratch.brewed = 1; - encounter_apply_consumable_stat_effect( - &s->player, ENCOUNTER_CONSUMABLE_STAT_EFFECT_BREW_DRAIN); - encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, &s->player); - if (ctx->human_command_mode) - inf_refresh_human_loadout_stats(s); - } - - int pot_act = actions[INF_HEAD_POTION]; - if (pot_act == 1 && s->player.restore_doses > 0 && s->player.potion_timer == 0) { - encounter_add_prayer_restore(&s->player, INF_RESTORE_AMOUNT); - encounter_cap_prayer_restore(&s->player); - encounter_apply_consumable_stat_effect( - &s->player, ENCOUNTER_CONSUMABLE_STAT_EFFECT_RESTORE); - encounter_finish_potion_dose(&s->player, &s->player.restore_doses); - encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, &s->player); - if (ctx->human_command_mode) - inf_refresh_human_loadout_stats(s); - } else if (pot_act == 2 && s->player.bastion_doses > 0 && s->player.potion_timer == 0) { - encounter_bastion_boost(&s->player); - encounter_finish_potion_dose(&s->player, &s->player.bastion_doses); - encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, &s->player); - if (ctx->human_command_mode) - inf_refresh_human_loadout_stats(s); - } else if (pot_act == 3 && s->player.stamina_doses > 0 && s->player.potion_timer == 0) { - s->stamina_active_ticks = 200; - s->player.stamina_doses--; - s->player.potion_timer = 3; + if (!ctx->human_command_mode) { + OsrsInventoryClickActions clicks; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + clicks.equip_by_slot[slot] = actions[INF_HEAD_EQUIP_SLOT(slot)]; + clicks.eat = actions[INF_HEAD_EAT]; + clicks.drink = actions[INF_HEAD_DRINK]; + OsrsInventoryTickIntent intent = + osrs_resolve_inventory_tick_intent(&s->player, s->inventory_cells, &clicks); + if (intent.eat_cell >= 0) + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EAT); + if (intent.drink_cell >= 0) + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_DRINK); + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + if (intent.equip_cell_by_slot[slot] >= 0) { + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EQUIP); + break; + } + } + OsrsInventoryApplyStep click_step; + while (osrs_inventory_intent_next(&intent, &click_step)) { + switch (click_step.kind) { + case OSRS_INVENTORY_APPLY_EQUIP: + if (osrs_equip_from_cell( + &s->player, s->inventory_cells, click_step.cell_idx) >= 0) { + s->total_gear_switches++; + inf_note_human_weapon_set(s); + inf_refresh_live_stats(s); + } + break; + case OSRS_INVENTORY_APPLY_EAT: + inf_apply_eat_cell( + s, click_step.cell_idx, click_step.resolution.consumable_kind); + break; + case OSRS_INVENTORY_APPLY_DRINK: { + OsrsInventoryDrinkConsumeResult drink = + osrs_inventory_cell_consume_drink_one_dose( + &s->inventory_cells[click_step.cell_idx], + click_step.resolution, + &s->player.potion_timer, + inf_apply_drink_one_dose_effect, s); + if (drink.consumed) + inf_sync_consumable_counters_from_cells(s); + break; + } + } + } } - if (eat_act > 0) - osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EAT); - if (pot_act > 0) - osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_DRINK); - inf_refresh_current_obs_slots_ctx(s, ctx); int target = actions[INF_HEAD_TARGET]; - if (inf_oracle_overrides_target(ctx->config.oracle_mode)) { - InfOraclePick pick = inf_oracle_pick_full(s, ctx->config.oracle_mode); - if (pick.target_slot >= 0) target = pick.target_slot + 1; - } int has_new_target = 0; if (target > 0 && target <= INF_OBS_NPCS) { int obs_idx = target - 1; @@ -613,15 +486,6 @@ static void inf_tick_player_ctx( has_new_target = 1; } } - if ((ctx->config.oracle_mode == 10 || ctx->config.oracle_mode == 11) && - osrs_interaction_active(&s->interaction) && - inf_is_untagged_live_zuk_healer_slot(s, s->interaction.target_slot) && - (!inf_player_behind_zuk_shield_now(s) || - !inf_player_can_attack_npc_from_current_tile( - s, s->interaction.target_slot) || - (ctx->config.oracle_mode == 11 && s->player.attack_timer != 0))) { - osrs_interaction_clear(&s->interaction); - } if (ctx->config.zuk_safe_untagged_healer_target_mask && osrs_interaction_active(&s->interaction) && !inf_untagged_zuk_healer_target_is_safe_now( @@ -638,8 +502,6 @@ static void inf_tick_player_ctx( osrs_interaction_clear(&s->interaction); } int has_explicit_move = (actions[INF_HEAD_MOVE] > 0 || s->player_dest_x >= 0); - if (!has_new_target && has_explicit_move) - osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_MOVE); if (osrs_interaction_active(&s->interaction)) { int target_slot = s->interaction.target_slot; int target_can_manual_phantom = @@ -659,13 +521,19 @@ static void inf_tick_player_ctx( s->tick_at_first_zuk_healer_target = s->tick; } - OsrsPlayerMoveKind move_kind = OSRS_PLAYER_MOVE_NONE; + int claimed_target_standing = + has_new_target && osrs_interaction_active(&s->interaction); + OsrsPlayerCommand command = { .kind = OSRS_PLAYER_CMD_NONE }; int move_act = actions[INF_HEAD_MOVE]; - if (has_explicit_move && !osrs_interaction_active(&s->interaction)) { - if (s->player_dest_x >= 0) - move_kind = OSRS_PLAYER_MOVE_DESTINATION; - else if (move_act > 0 && move_act < ENCOUNTER_MOVE_ACTIONS) - move_kind = OSRS_PLAYER_MOVE_ACTION; + if (!claimed_target_standing && has_explicit_move) { + if (s->player_dest_x >= 0) { + command.kind = OSRS_PLAYER_CMD_MOVE; + command.move_kind = OSRS_PLAYER_MOVE_DESTINATION; + } else if (move_act > 0 && move_act < ENCOUNTER_MOVE_ACTIONS) { + command.kind = OSRS_PLAYER_CMD_MOVE; + command.move_kind = OSRS_PLAYER_MOVE_ACTION; + command.move_action = move_act; + } } InfWalkCtx wc = { s, ctx }; InfPlayerStepTargetCtx target_ctx = { @@ -679,8 +547,7 @@ static void inf_tick_player_ctx( .interaction = &s->interaction, .target_lookup = inf_lookup_player_attack_target, .target_ctx = &target_ctx, - .move_kind = move_kind, - .move_action = move_act, + .command = command, .dest_x = &s->player_dest_x, .dest_y = &s->player_dest_y, .arena = { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index 114cf18997..f19e81b80b 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -60,13 +60,6 @@ static void inf_put_int_ctx( inf_require_valid_loadout_profile_mode(value); ctx->config.loadout_profile_mode = (InfLoadoutProfileMode)value; } - else if (strcmp(key, "oracle_mode") == 0) { - if (value < 0 || value > 11) { - fprintf(stderr, "inferno: oracle_mode must be in [0,11], got %d\n", value); - abort(); - } - ctx->config.oracle_mode = value; - } else if (strcmp(key, "zuk_healer_reward_mode") == 0) { if (value < INF_ZUK_HEALER_REWARD_MODE_BASELINE || value > INF_ZUK_HEALER_REWARD_MODE_TAGS_FIRST) { @@ -605,6 +598,40 @@ static void* inf_get_player_for_input(void* state, int idx) { return (idx == 0) ? (void*)&s->player : NULL; } +static int inf_first_cell_with_kind(const InfernoState* s, OsrsConsumableKind kind) { + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { + const OsrsInventoryCell* cell = &s->inventory_cells[c]; + if (cell->raw_osrs_id == 0) continue; + OsrsConsumableClick click = + osrs_consumable_click_lookup_raw_osrs_id(cell->raw_osrs_id); + if (click.consumable_kind != kind) continue; + if (click.click_action == OSRS_CLICK_DRINK && cell->dose == 0) continue; + return c; + } + return -1; +} + +static void inf_translate_drink(const InfernoState* s, int* actions, OsrsConsumableKind kind) { + int cell = inf_first_cell_with_kind(s, kind); + if (cell >= 0) actions[INF_HEAD_DRINK] = cell + 1; +} + +static void inf_translate_gear_swap(const InfernoState* s, int* actions, InfWeaponSet want) { + const uint8_t* const* loadouts = + inf_loadouts_for_profile(s->active_loadout_profile); + const uint8_t* want_loadout = loadouts[want]; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + uint8_t item = want_loadout[slot]; + if (item == ITEM_NONE || s->player.equipped[slot] == item) continue; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { + if (s->inventory_cells[c].item_idx == item) { + actions[INF_HEAD_EQUIP_SLOT(slot)] = c + 1; + break; + } + } + } +} + static void inf_translate_human_input_ctx( HumanInput* hi, int* actions, @@ -631,19 +658,28 @@ static void inf_translate_human_input_ctx( actions[INF_HEAD_TARGET] = 0; } - if (hi->pending_gear > 0) actions[INF_HEAD_GEAR] = hi->pending_gear; + if (hi->pending_gear >= 1 && hi->pending_gear <= INF_NUM_WEAPON_SETS) + inf_translate_gear_swap(s, actions, (InfWeaponSet)(hi->pending_gear - 1)); - if (hi->pending_food || hi->pending_potion == POTION_BREW) - actions[INF_HEAD_EAT] = 1; + if (hi->pending_food) { + int cell = inf_first_cell_with_kind(s, OSRS_CONSUMABLE_SHARK_FOOD); + if (cell < 0) cell = inf_first_cell_with_kind(s, OSRS_CONSUMABLE_KARAMBWAN); + if (cell >= 0) actions[INF_HEAD_EAT] = cell + 1; + } - if (hi->pending_potion == POTION_RESTORE) actions[INF_HEAD_POTION] = 1; - else if (hi->pending_potion == POTION_BASTION) actions[INF_HEAD_POTION] = 2; - else if (hi->pending_potion == POTION_STAMINA) actions[INF_HEAD_POTION] = 3; + if (hi->pending_potion == POTION_BREW) + inf_translate_drink(s, actions, OSRS_CONSUMABLE_BREW); + else if (hi->pending_potion == POTION_RESTORE) + inf_translate_drink(s, actions, OSRS_CONSUMABLE_SUPER_RESTORE); + else if (hi->pending_potion == POTION_BASTION) + inf_translate_drink(s, actions, OSRS_CONSUMABLE_BASTION); + else if (hi->pending_potion == POTION_STAMINA) + inf_translate_drink(s, actions, OSRS_CONSUMABLE_STAMINA); if (hi->pending_spell == ATTACK_BLOOD) actions[INF_HEAD_SPELL] = 1; else if (hi->pending_spell == ATTACK_ICE) actions[INF_HEAD_SPELL] = 2; - if (hi->pending_spec) actions[INF_HEAD_SPEC] = 1; + if (hi->pending_spec) actions[INF_HEAD_SPEC] = s->player.spec_armed ? 2 : 1; } static void inf_translate_human_input(HumanInput* hi, int* actions, EncounterState* state) { @@ -693,17 +729,25 @@ static void inf_translate_human_commands_ctx( case HUMAN_COMMAND_OFFENSIVE_PRAYER: actions[INF_HEAD_OFFENSIVE] = cmd->offensive_prayer; break; - case HUMAN_COMMAND_EAT: - actions[INF_HEAD_EAT] = 1; + case HUMAN_COMMAND_EAT: { + int cell = inf_first_cell_with_kind(s, OSRS_CONSUMABLE_SHARK_FOOD); + if (cell < 0) + cell = inf_first_cell_with_kind(s, OSRS_CONSUMABLE_KARAMBWAN); + if (cell >= 0) actions[INF_HEAD_EAT] = cell + 1; break; + } case HUMAN_COMMAND_DRINK: - if (cmd->potion == POTION_BREW) actions[INF_HEAD_EAT] = 1; - else if (cmd->potion == POTION_RESTORE) actions[INF_HEAD_POTION] = 1; - else if (cmd->potion == POTION_BASTION) actions[INF_HEAD_POTION] = 2; - else if (cmd->potion == POTION_STAMINA) actions[INF_HEAD_POTION] = 3; + if (cmd->potion == POTION_BREW) + inf_translate_drink(s, actions, OSRS_CONSUMABLE_BREW); + else if (cmd->potion == POTION_RESTORE) + inf_translate_drink(s, actions, OSRS_CONSUMABLE_SUPER_RESTORE); + else if (cmd->potion == POTION_BASTION) + inf_translate_drink(s, actions, OSRS_CONSUMABLE_BASTION); + else if (cmd->potion == POTION_STAMINA) + inf_translate_drink(s, actions, OSRS_CONSUMABLE_STAMINA); break; case HUMAN_COMMAND_SPEC_TOGGLE: - actions[INF_HEAD_SPEC] = 1; + actions[INF_HEAD_SPEC] = s->player.spec_armed ? 2 : 1; break; case HUMAN_COMMAND_EQUIP_INVENTORY_ITEM: case HUMAN_COMMAND_FIGHT_STYLE: @@ -746,175 +790,6 @@ static void inf_step_human_commands(EncounterState* state, HumanInput* hi) { state, (EncounterContext*)inf_legacy_context(), hi); } -typedef struct { - uint8_t wave; - uint8_t weapon_set; - uint8_t player_hp_bin; - uint8_t player_prayer_bin; - uint8_t brew_doses; - uint8_t restore_doses; - uint8_t overhead_prayer; - uint8_t offensive_prayer_attack_timer; - uint8_t player_x_quant; - uint8_t player_y_quant; - uint8_t zuk_hp_bin; - uint8_t zuk_phase_flags; - uint8_t active_jad_count; - uint8_t active_zuk_healer_count; - uint8_t active_set_count; - uint8_t jad_hp_bin; -} InfCellKey; - -typedef struct { - int live_jad_count; - int live_zuk_healer_count; - int live_set_count; - int live_jad_hp; - int live_jad_max_hp; -} InfLateAddCounts; - -static InfLateAddCounts inf_late_add_counts(const InfernoState* s) { - InfLateAddCounts counts; - memset(&counts, 0, sizeof(counts)); - - for (int i = 0; i < INF_MAX_NPCS; i++) { - const InfNPC* npc = &s->npcs[i]; - if (!npc->active || npc->hp <= 0) continue; - switch (npc->type) { - case INF_NPC_JAD: - counts.live_jad_count++; - counts.live_jad_hp += npc->hp; - counts.live_jad_max_hp += npc->max_hp > 0 ? npc->max_hp : npc->hp; - break; - case INF_NPC_HEALER_ZUK: - counts.live_zuk_healer_count++; - break; - case INF_NPC_MAGER: - case INF_NPC_MELEER: - case INF_NPC_RANGER: - counts.live_set_count++; - break; - default: - break; - } - } - - return counts; -} - -static size_t inf_cell_key_size_ctx(EncounterState* state, EncounterContext* context) { - (void)state; - (void)context; - return sizeof(InfCellKey); -} - -static size_t inf_cell_key_size(EncounterState* state) { - return inf_cell_key_size_ctx(state, (EncounterContext*)inf_legacy_context()); -} - -static uint8_t inf_cell_attack_timer_bucket(const InfernoState* s) { - int timer = s->player.attack_timer; - if (timer < 0) timer = 0; - if (timer > 15) timer = 15; - return (uint8_t)timer; -} - -static void inf_write_cell_key_ctx( - EncounterState* state, - EncounterContext* context, - void* out -) { - (void)context; - const InfernoState* s = (const InfernoState*)state; - InfCellKey* k = (InfCellKey*)out; - memset(k, 0, sizeof(InfCellKey)); - - k->wave = (uint8_t)(s->wave & 0xff); - k->weapon_set = (uint8_t)s->weapon_set; - - k->player_hp_bin = (uint8_t)(s->player.current_hitpoints / 10); - k->player_prayer_bin = (uint8_t)(s->player.current_prayer / 10); - k->brew_doses = (uint8_t)s->player.brew_doses; - k->restore_doses = (uint8_t)s->player.restore_doses; - k->overhead_prayer = (uint8_t)s->player.prayer; - k->offensive_prayer_attack_timer = (uint8_t)( - ((uint8_t)s->player.offensive_prayer & 0x0fu) | - (inf_cell_attack_timer_bucket(s) << 4)); - - int dx = s->player.x - INF_ARENA_MIN_X; - int dy = s->player.y - INF_ARENA_MIN_Y; - if (dx < 0) dx = 0; - if (dy < 0) dy = 0; - k->player_x_quant = (uint8_t)((dx / 2) & 0xff); - k->player_y_quant = (uint8_t)((dy / 2) & 0xff); - - int zuk_idx = inf_find_live_zuk_idx(s); - int zuk_hp = (zuk_idx >= 0) ? s->npcs[zuk_idx].hp : 0; - k->zuk_hp_bin = (uint8_t)((zuk_hp / 50) & 0xff); - - k->zuk_phase_flags = (uint8_t)( - (s->zuk.healer_spawned ? 0x01u : 0u) | - (s->zuk.jad_spawned ? 0x02u : 0u) | - (s->zuk.enraged ? 0x04u : 0u) | - (s->zuk.timer_paused ? 0x08u : 0u) - ); - - InfLateAddCounts counts = inf_late_add_counts(s); - k->active_jad_count = (uint8_t)( - counts.live_jad_count > 255 ? 255 : counts.live_jad_count); - k->active_zuk_healer_count = (uint8_t)( - counts.live_zuk_healer_count > 255 ? 255 : counts.live_zuk_healer_count); - k->active_set_count = (uint8_t)( - counts.live_set_count > 255 ? 255 : counts.live_set_count); - int jad_hp_bin = counts.live_jad_hp / 50; - k->jad_hp_bin = (uint8_t)(jad_hp_bin > 255 ? 255 : jad_hp_bin); -} - -static void inf_write_cell_key(EncounterState* state, void* out) { - inf_write_cell_key_ctx(state, (EncounterContext*)inf_legacy_context(), out); -} - -static float inf_progress_score_ctx(EncounterState* state, EncounterContext* context) { - (void)context; - const InfernoState* s = (const InfernoState*)state; - if (s->episode_over && s->winner == INF_OUTCOME_PLAYER_WON) return 2.0f; - - float min_zhp = (s->min_zuk_hp_seen > 0.0f) ? s->min_zuk_hp_seen : 1200.0f; - if (min_zhp < 0.0f) min_zhp = 0.0f; - if (min_zhp > 1200.0f) min_zhp = 1200.0f; - - float q = (1200.0f - min_zhp) / 1200.0f; - InfLateAddCounts counts = inf_late_add_counts(s); - - if (s->zuk.jad_spawned && counts.live_jad_count == 0) { - q += 0.10f; - if (min_zhp < 600.0f) { - q += 0.06f * ((600.0f - min_zhp) / 600.0f); - } - } else if (s->zuk.jad_spawned && counts.live_jad_max_hp > 0) { - float jad_damage_frac = - (float)(counts.live_jad_max_hp - counts.live_jad_hp) / - (float)counts.live_jad_max_hp; - if (jad_damage_frac < 0.0f) jad_damage_frac = 0.0f; - if (jad_damage_frac > 1.0f) jad_damage_frac = 1.0f; - q += 0.09f * jad_damage_frac; - } - - if (s->zuk.healer_spawned && counts.live_zuk_healer_count == 0) { - q += 0.08f; - } - - if (min_zhp <= 900.0f && counts.live_set_count == 0) { - q += 0.04f; - } - - return q; -} - -static float inf_progress_score(EncounterState* state) { - return inf_progress_score_ctx(state, (EncounterContext*)inf_legacy_context()); -} - #define INF_SNAPSHOT_MAGIC 0x1FE00001u #define INF_SNAPSHOT_VERSION 18u @@ -986,7 +861,6 @@ static uint64_t inf_config_fingerprint(const InfConfig* config) { INF_HASH_CONFIG_FIELD(config, &h, zuk_healer_reward_mode); INF_HASH_CONFIG_FIELD(config, &h, post_jad_zuk_multiplier); INF_HASH_CONFIG_FIELD(config, &h, jad_alive_zuk_multiplier); - INF_HASH_CONFIG_FIELD(config, &h, oracle_mode); INF_HASH_CONFIG_FIELD(config, &h, shield_tag_reward_coeff); INF_HASH_CONFIG_FIELD(config, &h, joseph_reward_mode); return h; @@ -1000,7 +874,8 @@ static void inf_refresh_after_state_load(InfernoState* s, InfernoContext* ctx) { inf_loadouts_for_profile(s->active_loadout_profile); encounter_compute_loadout_stats(loadouts[INF_GEAR_MAGE], ATTACK_STYLE_MAGIC, s->player.offensive_prayer, s->player.current_magic, - inf_player_spell_fight_style(&s->player), 30, + inf_player_spell_fight_style(&s->player), + inf_player_magic_base_damage(&s->player), &s->loadout_stats[INF_GEAR_MAGE]); encounter_compute_loadout_stats(loadouts[INF_GEAR_LONG_RANGE], ATTACK_STYLE_RANGED, s->player.offensive_prayer, s->player.current_ranged, @@ -1110,10 +985,6 @@ static const EncounterDef ENCOUNTER_INFERNO = { .snapshot = inf_snapshot_ctx, .restore = inf_restore_ctx, - .cell_key_size = inf_cell_key_size_ctx, - .write_cell_key = inf_write_cell_key_ctx, - .progress_score = inf_progress_score_ctx, - .write_obs = inf_write_obs_ctx, .write_mask = inf_write_mask_ctx, .get_reward = inf_get_reward_ctx, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc index 733110472c..1f00fbff75 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -1,13 +1,83 @@ #line 1436 "encounter_inferno.h" +static void inf_sync_consumable_counters_from_cells(InfernoState* s); +static void inf_refresh_live_stats(InfernoState* s); + static InfSupplyDoses inf_full_starting_supplies(void) { return (InfSupplyDoses){ .brew_doses = 24, - .restore_doses = 40, + .restore_doses = 36, .bastion_doses = 16, .stamina_doses = 4, }; } +static uint16_t inf_potion_raw_id_for_dose(OsrsConsumableKind kind, int dose) { + int n = (int)(sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / + sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0])); + for (int i = 0; i < n; i++) { + const OsrsConsumableClick* row = &OSRS_CONSUMABLE_CLICK_REGISTRY[i]; + if (row->consumable_kind == kind && row->dose_count == dose) + return row->raw_osrs_id; + } + fprintf(stderr, "inferno: no registry row for kind %d dose %d\n", (int)kind, dose); + abort(); +} + +static void inf_seed_potion_cells( + InfernoState* s, + int* next_cell, + OsrsConsumableKind kind, + int doses +) { + while (doses > 0) { + int dose = doses >= 4 ? 4 : doses; + if (*next_cell >= OSRS_INVENTORY_SIZE) { + fprintf(stderr, "inferno: inventory kit overflows %d cells\n", + OSRS_INVENTORY_SIZE); + abort(); + } + s->inventory_cells[(*next_cell)++] = osrs_inventory_cell_from_raw_osrs_id( + inf_potion_raw_id_for_dose(kind, dose)); + doses -= dose; + } +} + +static void inf_seed_inventory_cells(InfernoState* s) { + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) + s->inventory_cells[i] = osrs_inventory_cell_empty(); + + int cell = 0; + const uint8_t* const* loadouts = + inf_loadouts_for_profile(s->active_loadout_profile); + for (int g = 0; g < INF_NUM_WEAPON_SETS; g++) { + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + uint8_t item = loadouts[g][slot]; + if (item == ITEM_NONE) continue; + if (s->player.equipped[slot] == item) continue; + int duplicate = 0; + for (int c = 0; c < cell; c++) { + if (s->inventory_cells[c].item_idx == item) { + duplicate = 1; + break; + } + } + if (duplicate) continue; + if (cell >= OSRS_INVENTORY_SIZE) { + fprintf(stderr, "inferno: gear kit overflows %d cells\n", + OSRS_INVENTORY_SIZE); + abort(); + } + s->inventory_cells[cell++] = osrs_inventory_cell_from_item(item); + } + } + + inf_seed_potion_cells(s, &cell, OSRS_CONSUMABLE_BREW, s->player.brew_doses); + inf_seed_potion_cells(s, &cell, OSRS_CONSUMABLE_SUPER_RESTORE, + s->player.restore_doses); + inf_seed_potion_cells(s, &cell, OSRS_CONSUMABLE_BASTION, s->player.bastion_doses); + inf_seed_potion_cells(s, &cell, OSRS_CONSUMABLE_STAMINA, s->player.stamina_doses); +} + static const InfSupplyProfileAnchor INF_SUPPLY_PROFILE_ANCHORS[] = { { 1, { 1.0000f, 1.0000f, 1.0000f, 1.0000f } }, { 20, { 1.0000f, 0.9500f, 1.0000f, 1.0000f } }, @@ -305,7 +375,7 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint s->total_zuk_healer_out_of_range_ticks = 0; s->total_zuk_healer_attackable_ticks = 0; s->total_action_mask_checks = 0; - for (int h = 0; h < 9; h++) { + for (int h = 0; h < INF_NUM_ACTION_HEADS; h++) { s->zero_valid_action_head_count[h] = 0; s->min_valid_action_count_by_head[h] = 1000000; } @@ -337,6 +407,8 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint s->player.bastion_doses = start_supplies.bastion_doses; s->player.stamina_doses = start_supplies.stamina_doses; inf_apply_curriculum_supply_variation(s, ctx, full_supplies); + inf_seed_inventory_cells(s); + inf_sync_consumable_counters_from_cells(s); s->stamina_active_ticks = 0; s->player.prayer = PRAYER_NONE; s->player.autocast_enabled = 1; @@ -349,11 +421,13 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint s->last_hit_by_type = -1; encounter_compute_loadout_stats(loadouts[INF_GEAR_MAGE], ATTACK_STYLE_MAGIC, - OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, &s->loadout_stats[INF_GEAR_MAGE]); + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, + inf_player_magic_base_damage(&s->player), &s->loadout_stats[INF_GEAR_MAGE]); encounter_compute_loadout_stats(loadouts[INF_GEAR_LONG_RANGE], ATTACK_STYLE_RANGED, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, &s->loadout_stats[INF_GEAR_LONG_RANGE]); encounter_compute_loadout_stats(loadouts[INF_GEAR_BP], ATTACK_STYLE_RANGED, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, &s->loadout_stats[INF_GEAR_BP]); + inf_refresh_live_stats(s); int effective_start = inf_runtime_wave_from_start_wave(s->start_wave); int is_zuk_wave = (effective_start >= INF_WAVE_ZUK); diff --git a/ocean/osrs/osrs_colosseum_item_obs_generated.h b/ocean/osrs/osrs_colosseum_item_obs_generated.h new file mode 100644 index 0000000000..680e75ce1e --- /dev/null +++ b/ocean/osrs/osrs_colosseum_item_obs_generated.h @@ -0,0 +1,24 @@ +/* Generated by ocean/osrs/tools/gen_colosseum_item_obs_table.c. Do not edit. + * Dependency-free on purpose: src/ocean.cu includes it and no OSRS header. */ +#ifndef OSRS_COLOSSEUM_ITEM_OBS_GENERATED_H +#define OSRS_COLOSSEUM_ITEM_OBS_GENERATED_H + +#define COLO_ITEM_OBS_TABLE_ROWS 201 +#define COLO_ITEM_OBS_TABLE_COLS 15 +#define COLO_ITEM_OBS_TABLE_BASE_PRAYER 99 +#define COLO_ITEM_OBS_TABLE_BASE_LEVEL 99 + +/* The observation carries code/SCALE; multiply back to index a row. A power of two + * keeps the round trip exact in bf16 and keeps the code out of the global linear + * layer's dynamic range. */ +#define COLO_ITEM_OBS_CODE_SCALE 256 + +/* Slots the observation adds in; the table holds zero at both. */ +#define COLO_ITEM_OBS_OVERLAY_EQUIPPED 1 +#define COLO_ITEM_OBS_OVERLAY_HP_HEAL 10 + +/* Rows live in osrs_colosseum_item_obs_table.inc so each consumer names its own + * storage: src/ocean.cu wants a __device__ copy, the viewer wants a host one, and + * a single guarded definition would hand whichever included first to both. */ + +#endif diff --git a/ocean/osrs/osrs_colosseum_item_obs_table.inc b/ocean/osrs/osrs_colosseum_item_obs_table.inc new file mode 100644 index 0000000000..b58881c382 --- /dev/null +++ b/ocean/osrs/osrs_colosseum_item_obs_table.inc @@ -0,0 +1,203 @@ +/* Generated by ocean/osrs/tools/gen_colosseum_item_obs_table.c. Do not edit. + * Included as the initialiser body of a [ROWS][COLS] float array. */ +{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.666666687f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.25f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.666666687f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.666666687f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.550000012f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.5f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.75f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.5f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.0666666701f, 0.5f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.899999976f, 0.666666687f, 0.550000012f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.666666687f, 0.649999976f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.25f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.5f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.349999994f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.333333343f, 0.5f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.400000006f, 0.5f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.600000024f, 0.666666687f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.333333343f, 0.5f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.5f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.13333334f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.400000006f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, diff --git a/ocean/osrs/osrs_combat.h b/ocean/osrs/osrs_combat.h index d2f16e48b3..9e16832427 100644 --- a/ocean/osrs/osrs_combat.h +++ b/ocean/osrs/osrs_combat.h @@ -560,11 +560,6 @@ static inline int osrs_player_ranged_max_hit(int eff_range_level, int ranged_str return (eff_range_level * (ranged_str_bonus + 64) + 320) / 640; } -/** magic_dmg_pct is the total gear bonus in percent (30 = +30%). */ -static inline int osrs_player_magic_max_hit(int spell_base_dmg, int magic_dmg_pct) { - return spell_base_dmg * (100 + magic_dmg_pct) / 100; -} - /** Correct overhead prayer blocks 100% of damage in PvE but only 40% in PvP. */ static inline int osrs_prayer_reduce_damage(int damage, int prayer, int attack_style, int is_pvp) { if (damage <= 0) return 0; diff --git a/ocean/osrs/osrs_consumables.h b/ocean/osrs/osrs_consumables.h index a72cde433c..165d42b807 100644 --- a/ocean/osrs/osrs_consumables.h +++ b/ocean/osrs/osrs_consumables.h @@ -58,6 +58,10 @@ static inline int osrs_saturated_heart_magic_boost(int base_magic) { return 4 + base_magic / 10; } +static inline int osrs_prayer_potion_restore_amount(int level) { + return 7 + level / 4; +} + static inline int osrs_super_restore_amount(int level) { return 8 + level / 4; } @@ -112,7 +116,7 @@ static inline DrinkResult osrs_drink_potion(PotionType type, int current_prayer, case POTION_PRAYER_RESTORE: if (current_prayer >= prayer_level) return r; r.consumed = 1; - r.prayer_restored = 7 + prayer_level / 4; + r.prayer_restored = osrs_prayer_potion_restore_amount(prayer_level); break; case POTION_SUPER_RESTORE: if (current_prayer >= prayer_level) return r; diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index b6da764efe..7178202648 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -26,11 +26,6 @@ typedef struct EncounterContext EncounterContext; #define ENCOUNTER_RENDER_HITS_MAX 32 -typedef struct { - EncounterState* state; - EncounterContext* context; -} EncounterRuntime; - static inline void encounter_abort_unknown_config( const char* encounter_name, const char* config_type, const char* key ) { @@ -64,20 +59,25 @@ static inline int encounter_require_int_range_config( #define ENCOUNTER_MAX_PENDING_HITS 32 typedef struct { - int active; - int damage; - int ticks_remaining; - int attack_style; - int check_prayer; - int prayer_check_delay; - - int spell_type; - int source_npc_type; - int source_npc_slot; - int hit_success; - int elysian_reduced; + int8_t active; + int8_t ticks_remaining; + int8_t attack_style; + int8_t check_prayer; + int8_t prayer_check_delay; + + int8_t spell_type; + int8_t source_npc_type; + int8_t source_npc_slot; + int8_t hit_success; + int8_t elysian_reduced; + int16_t damage; } EncounterPendingHit; +/* Replicated per NPC per env and streamed every step, so the width is load-bearing for + * throughput, not tidiness. damage trails the int8 run so the record packs to 12 with no + * padding; C++ also requires designated initializers in this order, so keep it last. */ +static_assert(sizeof(EncounterPendingHit) == 12, "pending hit record must stay 12 bytes"); + typedef struct { EncounterPendingHit hits[ENCOUNTER_MAX_PENDING_HITS]; int count; @@ -92,16 +92,16 @@ static inline EncounterPendingHit encounter_pending_hit_resolved_at_throw( if (out_prayed) *out_prayed = pr.prayed; return (EncounterPendingHit){ .active = 1, - .damage = pr.frozen_damage, - .ticks_remaining = ticks_remaining, - .attack_style = attack_style, + .ticks_remaining = (int8_t)ticks_remaining, + .attack_style = (int8_t)attack_style, .check_prayer = 0, .prayer_check_delay = 0, .spell_type = ENCOUNTER_SPELL_NONE, - .source_npc_type = source_npc_type, - .source_npc_slot = source_npc_slot, - .hit_success = accuracy_hit && !pr.prayed, + .source_npc_type = (int8_t)source_npc_type, + .source_npc_slot = (int8_t)source_npc_slot, + .hit_success = (int8_t)(accuracy_hit && !pr.prayed), .elysian_reduced = 0, + .damage = (int16_t)pr.frozen_damage, }; } @@ -1149,6 +1149,435 @@ static inline int encounter_attack_seek_has_exact_tile( return 0; } +typedef struct { + const CollisionMap* collision_map; + encounter_walkable_fn is_walkable; + void* walkable_ctx; + pathfind_blocked_fn extra_blocked; + void* blocked_ctx; + int world_offset_x; + int world_offset_y; + int arena_base_x; + int arena_base_y; + int arena_w; + int arena_h; + int source_x; + int source_y; + int min_explored_x; + int min_explored_y; + int max_explored_x; + int max_explored_y; + uint16_t visit_order[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + uint16_t depth[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + int8_t via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + int visited_count; +} EncounterArenaAttackRouteField; + +typedef struct { + int land_x; + int land_y; +} EncounterAttackRouteLanding; + +static inline int encounter_attack_route_field_extra_blocked( + const EncounterArenaAttackRouteField* field, + int x, + int y +) { + return field->extra_blocked && + field->extra_blocked( + field->blocked_ctx, + x + field->world_offset_x, + y + field->world_offset_y); +} + +static inline void encounter_build_arena_attack_route_field( + EncounterArenaAttackRouteField* field, + const CollisionMap* cmap, + int world_offset_x, + int world_offset_y, + int source_x, + int source_y, + encounter_walkable_fn is_walkable, + void* walkable_ctx, + pathfind_blocked_fn extra_blocked, + void* blocked_ctx, + int arena_base_x, + int arena_base_y, + int arena_w, + int arena_h +) { + if (!field || !is_walkable) { + fprintf(stderr, "attack route field is missing required input\n"); + abort(); + } + if (arena_w <= 0 || arena_w > PATHFIND_ARENA_MAX || + arena_h <= 0 || arena_h > PATHFIND_ARENA_MAX) { + fprintf(stderr, "attack route arena dimensions out of bounds: %dx%d\n", + arena_w, arena_h); + abort(); + } + + int local_source_x = source_x - arena_base_x; + int local_source_y = source_y - arena_base_y; + if (local_source_x < 0 || local_source_x >= arena_w || + local_source_y < 0 || local_source_y >= arena_h) { + fprintf(stderr, + "attack route source out of arena: source=(%d,%d) arena=(%d,%d,%d,%d)\n", + source_x, source_y, arena_base_x, arena_base_y, arena_w, arena_h); + abort(); + } + + memset(field, 0, sizeof(*field)); + field->collision_map = cmap; + field->is_walkable = is_walkable; + field->walkable_ctx = walkable_ctx; + field->extra_blocked = extra_blocked; + field->blocked_ctx = blocked_ctx; + field->world_offset_x = world_offset_x; + field->world_offset_y = world_offset_y; + field->arena_base_x = arena_base_x; + field->arena_base_y = arena_base_y; + field->arena_w = arena_w; + field->arena_h = arena_h; + field->source_x = source_x; + field->source_y = source_y; + field->min_explored_x = local_source_x; + field->min_explored_y = local_source_y; + field->max_explored_x = local_source_x; + field->max_explored_y = local_source_y; + + int queue_x[PATHFIND_MAX_QUEUE_ARENA]; + int queue_y[PATHFIND_MAX_QUEUE_ARENA]; + int head = 0; + int tail = 0; + field->visited_count = 1; + field->visit_order[local_source_x][local_source_y] = 1; + field->via[local_source_x][local_source_y] = VIA_START; + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, + local_source_x, local_source_y); + + static const int route_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int route_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; + static const int route_via[8] = { + VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE + }; + + while (head < tail) { + int cur_x = queue_x[head]; + int cur_y = queue_y[head]; + head++; + + int tile_x = arena_base_x + cur_x; + int tile_y = arena_base_y + cur_y; + int abs_x = tile_x + world_offset_x; + int abs_y = tile_y + world_offset_y; + uint16_t next_depth = field->depth[cur_x][cur_y] + 1; + + for (int i = 0; i < 8; i++) { + int dx = route_dx[i]; + int dy = route_dy[i]; + int next_x = cur_x + dx; + int next_y = cur_y + dy; + if (next_x < 0 || next_x >= arena_w || + next_y < 0 || next_y >= arena_h) { + continue; + } + if (field->visit_order[next_x][next_y] != 0) continue; + if (!collision_traversable_step(cmap, 0, abs_x, abs_y, dx, dy)) + continue; + + int next_tile_x = tile_x + dx; + int next_tile_y = tile_y + dy; + if (!is_walkable(walkable_ctx, next_tile_x, next_tile_y)) continue; + if (extra_blocked && + extra_blocked( + blocked_ctx, + next_tile_x + world_offset_x, + next_tile_y + world_offset_y)) { + continue; + } + if (dx != 0 && dy != 0) { + if (extra_blocked && + (extra_blocked( + blocked_ctx, + tile_x + dx + world_offset_x, + tile_y + world_offset_y) || + extra_blocked( + blocked_ctx, + tile_x + world_offset_x, + tile_y + dy + world_offset_y))) { + continue; + } + if (!is_walkable(walkable_ctx, tile_x + dx, tile_y) || + !is_walkable(walkable_ctx, tile_x, tile_y + dy)) { + continue; + } + } + + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, + next_x, next_y); + field->visited_count++; + field->visit_order[next_x][next_y] = + (uint16_t)field->visited_count; + field->depth[next_x][next_y] = next_depth; + field->via[next_x][next_y] = (int8_t)route_via[i]; + if (next_x < field->min_explored_x) field->min_explored_x = next_x; + if (next_y < field->min_explored_y) field->min_explored_y = next_y; + if (next_x > field->max_explored_x) field->max_explored_x = next_x; + if (next_y > field->max_explored_y) field->max_explored_y = next_y; + } + } +} + +static inline EncounterAttackRouteLanding +encounter_attack_route_overlap_landing( + const EncounterArenaAttackRouteField* field, + int target_x, + int target_y, + int target_size +) { + Player player = { + .x = field->source_x, + .y = field->source_y, + }; + int max_r = (target_size + 1) / 2 + 1; + int best_dsq = 9999; + int best_x = -1; + int best_y = -1; + for (int dy = -max_r; dy <= max_r; dy++) { + for (int dx = -max_r; dx <= max_r; dx++) { + if (dx == 0 && dy == 0) continue; + int x = player.x + dx; + int y = player.y + dy; + if (!field->is_walkable(field->walkable_ctx, x, y)) continue; + if (encounter_entity_footprints_overlap( + x, y, 1, target_x, target_y, target_size)) { + continue; + } + int dsq = dx * dx + dy * dy; + if (dsq < best_dsq) { + best_dsq = dsq; + best_x = x; + best_y = y; + } + } + } + if (best_x >= 0) { + encounter_walk_toward( + &player, best_x, best_y, + field->collision_map, + field->world_offset_x, + field->world_offset_y, + field->is_walkable, + field->walkable_ctx, + field->extra_blocked, + field->blocked_ctx, + field->arena_base_x, + field->arena_base_y, + field->arena_w, + field->arena_h); + } + return (EncounterAttackRouteLanding){ + .land_x = player.x, + .land_y = player.y, + }; +} + +static inline EncounterAttackRouteLanding encounter_arena_attack_route_landing( + const EncounterArenaAttackRouteField* field, + int target_x, + int target_y, + int target_size, + int attack_range, + const OsrsLosQuery* los_query +) { + if (!field || !field->is_walkable || + field->arena_w <= 0 || field->arena_w > PATHFIND_ARENA_MAX || + field->arena_h <= 0 || field->arena_h > PATHFIND_ARENA_MAX || + field->visited_count <= 0 || + target_size <= 0 || attack_range <= 0) { + fprintf(stderr, "invalid attack route landing query\n"); + abort(); + } + + EncounterAttackRouteLanding landing = { + .land_x = field->source_x, + .land_y = field->source_y, + }; + int dist = encounter_rect_distance( + field->source_x, field->source_y, 1, + target_x, target_y, target_size); + if (dist == 0) { + return encounter_attack_route_overlap_landing( + field, target_x, target_y, target_size); + } + if (encounter_player_can_attack( + field->source_x, field->source_y, + target_x, target_y, target_size, attack_range, los_query)) { + return landing; + } + + EncounterAttackSeekTile seek_tiles[ENCOUNTER_ATTACK_SEEK_MAX_TILES]; + int seek_count = 0; + for (int xx = 0; xx < target_size; xx++) { + int x = target_x + xx; + encounter_attack_seek_add_tile( + seek_tiles, &seek_count, x, target_y - 1, + field->world_offset_x, field->world_offset_y, + field->extra_blocked, field->blocked_ctx); + encounter_attack_seek_add_tile( + seek_tiles, &seek_count, x, target_y + target_size, + field->world_offset_x, field->world_offset_y, + field->extra_blocked, field->blocked_ctx); + } + for (int yy = 0; yy < target_size; yy++) { + int y = target_y + yy; + encounter_attack_seek_add_tile( + seek_tiles, &seek_count, target_x - 1, y, + field->world_offset_x, field->world_offset_y, + field->extra_blocked, field->blocked_ctx); + encounter_attack_seek_add_tile( + seek_tiles, &seek_count, target_x + target_size, y, + field->world_offset_x, field->world_offset_y, + field->extra_blocked, field->blocked_ctx); + } + + int selected_x = -1; + int selected_y = -1; + uint16_t selected_order = UINT16_MAX; + for (int x = 0; x < field->arena_w; x++) { + for (int y = 0; y < field->arena_h; y++) { + uint16_t order = field->visit_order[x][y]; + if (order == 0 || order >= selected_order) continue; + int tile_x = field->arena_base_x + x; + int tile_y = field->arena_base_y + y; + if (!field->is_walkable(field->walkable_ctx, tile_x, tile_y)) + continue; + if (encounter_attack_route_field_extra_blocked( + field, tile_x, tile_y)) { + continue; + } + int exact = seek_count > 0 + ? encounter_attack_seek_has_exact_tile( + tile_x, tile_y, seek_tiles, seek_count) + : encounter_player_can_attack( + tile_x, tile_y, + target_x, target_y, target_size, + attack_range, los_query); + if (!exact) continue; + selected_x = x; + selected_y = y; + selected_order = order; + } + } + + if (selected_x < 0 && seek_count > 0) { + int first_local_x = seek_tiles[0].x - field->arena_base_x; + int first_local_y = seek_tiles[0].y - field->arena_base_y; + int scan_min_x = + field->min_explored_x > first_local_x - PATHFIND_MAX_FALLBACK_RADIUS + ? field->min_explored_x + : first_local_x - PATHFIND_MAX_FALLBACK_RADIUS; + int scan_min_y = + field->min_explored_y > first_local_y - PATHFIND_MAX_FALLBACK_RADIUS + ? field->min_explored_y + : first_local_y - PATHFIND_MAX_FALLBACK_RADIUS; + int scan_max_x = + field->max_explored_x > first_local_x + PATHFIND_MAX_FALLBACK_RADIUS + ? field->max_explored_x + : first_local_x + PATHFIND_MAX_FALLBACK_RADIUS; + int scan_max_y = + field->max_explored_y > first_local_y + PATHFIND_MAX_FALLBACK_RADIUS + ? field->max_explored_y + : first_local_y + PATHFIND_MAX_FALLBACK_RADIUS; + if (scan_min_x < 0) scan_min_x = 0; + if (scan_min_y < 0) scan_min_y = 0; + if (scan_max_x >= field->arena_w) scan_max_x = field->arena_w - 1; + if (scan_max_y >= field->arena_h) scan_max_y = field->arena_h - 1; + + int best_dsq = 0x3fffffff; + int best_depth = 100; + for (int x = scan_min_x; x <= scan_max_x; x++) { + for (int y = scan_min_y; y <= scan_max_y; y++) { + if (field->visit_order[x][y] == 0) continue; + int tile_x = field->arena_base_x + x; + int tile_y = field->arena_base_y + y; + if (!field->is_walkable(field->walkable_ctx, tile_x, tile_y)) + continue; + int depth = field->depth[x][y]; + if (depth >= 100) continue; + int dsq = encounter_attack_seek_nearest_dsq( + tile_x, tile_y, seek_tiles, seek_count); + if (dsq < best_dsq || + (dsq == best_dsq && depth < best_depth)) { + selected_x = x; + selected_y = y; + best_dsq = dsq; + best_depth = depth; + } + } + } + } + if (selected_x < 0) return landing; + + int source_local_x = field->source_x - field->arena_base_x; + int source_local_y = field->source_y - field->arena_base_y; + int first_x = source_local_x; + int first_y = source_local_y; + int second_x = source_local_x; + int second_y = source_local_y; + int cur_x = selected_x; + int cur_y = selected_y; + uint16_t selected_depth = field->depth[selected_x][selected_y]; + while (field->depth[cur_x][cur_y] > 0) { + uint16_t depth = field->depth[cur_x][cur_y]; + if (depth == 1) { + first_x = cur_x; + first_y = cur_y; + } else if (depth == 2) { + second_x = cur_x; + second_y = cur_y; + } + + int via = field->via[cur_x][cur_y]; + if (via == VIA_NONE || via == VIA_START) { + fprintf(stderr, + "broken attack route parent at local tile (%d,%d)\n", + cur_x, cur_y); + abort(); + } + if (via & VIA_W) cur_x++; + else if (via & VIA_E) cur_x--; + if (via & VIA_S) cur_y++; + else if (via & VIA_N) cur_y--; + if (cur_x < 0 || cur_x >= field->arena_w || + cur_y < 0 || cur_y >= field->arena_h) { + fprintf(stderr, "attack route parent left arena\n"); + abort(); + } + } + if (cur_x != source_local_x || cur_y != source_local_y) { + fprintf(stderr, "attack route did not terminate at source\n"); + abort(); + } + if (selected_depth == 0) return landing; + + landing.land_x = field->arena_base_x + first_x; + landing.land_y = field->arena_base_y + first_y; + if (encounter_player_can_attack( + landing.land_x, landing.land_y, + target_x, target_y, target_size, attack_range, los_query)) { + return landing; + } + if (selected_depth >= 2) { + landing.land_x = field->arena_base_x + second_x; + landing.land_y = field->arena_base_y + second_y; + } + return landing; +} + static inline PathResult encounter_pathfind_arena_attack_approach( const CollisionMap* cmap, int world_offset_x, int world_offset_y, int src_x, int src_y, @@ -1687,7 +2116,7 @@ static inline int encounter_resolve_npc_pending_hit( typedef void (*EncounterPendingHitObserver)( void* user, const EncounterPendingHit* hit, int damage_after_prayer, - int prayer_was_correct, int prayer_was_checked); + int damage_applied, int prayer_was_correct, int prayer_was_checked); static inline void encounter_resolve_player_pending_hits_observed( EncounterPendingHitQueue* queue, @@ -1728,9 +2157,13 @@ static inline void encounter_resolve_player_pending_hits_observed( if (off_prayer_hit_count) (*off_prayer_hit_count)++; } + int hitpoints_before = player->current_hitpoints; + encounter_damage_player(player, dmg, NULL); + int applied = hitpoints_before - player->current_hitpoints; + if (damage_received_acc) + *damage_received_acc += (float)applied; if (observer && hit->attack_style != ATTACK_STYLE_NONE) - observer(observer_user, hit, dmg, prayed, checked); - encounter_damage_player(player, dmg, damage_received_acc); + observer(observer_user, hit, dmg, applied, prayed, checked); encounter_pending_hit_queue_remove(queue, i, "player"); i--; } @@ -2221,13 +2654,18 @@ typedef enum { ENCOUNTER_CONSUMABLE_STAT_EFFECT_SANFEW, } EncounterConsumableStatEffect; -static inline void encounter_apply_brew_heal(Player* p, int brew_heal) { +static inline void encounter_apply_brew_heal_capped( + Player* p, int brew_heal, int hitpoints_cap +) { p->current_hitpoints += brew_heal; - if (p->current_hitpoints > p->base_hitpoints + brew_heal) - p->current_hitpoints = p->base_hitpoints + brew_heal; + if (p->current_hitpoints > hitpoints_cap) p->current_hitpoints = hitpoints_cap; p->ate_food_this_tick = 1; } +static inline void encounter_apply_brew_heal(Player* p, int brew_heal) { + encounter_apply_brew_heal_capped(p, brew_heal, p->base_hitpoints + brew_heal); +} + static inline void encounter_apply_brew_heal_and_timer(Player* p, int brew_heal) { encounter_apply_brew_heal(p, brew_heal); p->brew_doses--; @@ -2475,10 +2913,6 @@ typedef struct { void (*restore)( EncounterState* state, EncounterContext* context, const void* data, size_t n); - size_t (*cell_key_size)(EncounterState* state, EncounterContext* context); - void (*write_cell_key)(EncounterState* state, EncounterContext* context, void* out); - float (*progress_score)(EncounterState* state, EncounterContext* context); - void (*write_obs)(EncounterState* state, EncounterContext* context, float* obs_out); void (*write_mask)(EncounterState* state, EncounterContext* context, float* mask_out); float (*get_reward)(EncounterState* state, EncounterContext* context); @@ -2525,35 +2959,6 @@ typedef struct { int (*get_winner)(EncounterState* state, EncounterContext* context); } EncounterDef; -static inline EncounterRuntime encounter_runtime_create(const EncounterDef* def) { - EncounterRuntime runtime = {0}; - if (!def || def->state_size == 0 || def->context_size == 0) { - fprintf(stderr, "encounter_runtime_create: %s has no typed runtime\n", - def ? def->name : "(null)"); - abort(); - } - runtime.state = (EncounterState*)calloc(1, def->state_size); - runtime.context = (EncounterContext*)calloc(1, def->context_size); - if (!runtime.state || !runtime.context) { - fprintf(stderr, "encounter_runtime_create: out of memory for %s\n", def->name); - abort(); - } - if (def->init_context) def->init_context(runtime.context); - if (def->init_state) def->init_state(runtime.state, runtime.context); - return runtime; -} - -static inline void encounter_runtime_destroy(const EncounterDef* def, EncounterRuntime* runtime) { - if (!runtime) return; - if (def && def->destroy_context && runtime->context) { - def->destroy_context(runtime->context); - } - free(runtime->state); - free(runtime->context); - runtime->state = NULL; - runtime->context = NULL; -} - #define MAX_ENCOUNTERS 32 typedef struct { diff --git a/ocean/osrs/osrs_encounter_player.h b/ocean/osrs/osrs_encounter_player.h index eebef8b928..4dc2aa018f 100644 --- a/ocean/osrs/osrs_encounter_player.h +++ b/ocean/osrs/osrs_encounter_player.h @@ -10,10 +10,27 @@ typedef enum { OSRS_PLAYER_MOVE_DESTINATION, } OsrsPlayerMoveKind; +/** + * One click per tick. The engine cannot both start an attack and walk somewhere + * else on the same tick: a ground click cancels the entity interaction, an entity + * click cancels the walk. TARGET and MOVE are therefore mutually exclusive by + * construction rather than by caller discipline. + * + * A walk already in flight is state, not a command. It continues under CMD_NONE + * and is cancelled by either new command. + */ typedef enum { - OSRS_PLAYER_TARGET_MOVE_CHASE = 0, - OSRS_PLAYER_TARGET_MOVE_EXPLICIT_FIRST, -} OsrsPlayerTargetMovePolicy; + OSRS_PLAYER_CMD_NONE = 0, + OSRS_PLAYER_CMD_TARGET, + OSRS_PLAYER_CMD_MOVE, +} OsrsPlayerCommandKind; + +typedef struct { + OsrsPlayerCommandKind kind; + int target_slot; + OsrsPlayerMoveKind move_kind; + int move_action; +} OsrsPlayerCommand; typedef struct { const CollisionMap* collision_map; @@ -48,11 +65,7 @@ typedef struct { OsrsInteraction* interaction; OsrsAttackTargetLookupFn target_lookup; void* target_ctx; - int has_new_target; - int new_target_slot; - OsrsPlayerMoveKind move_kind; - OsrsPlayerTargetMovePolicy target_move_policy; - int move_action; + OsrsPlayerCommand command; int* dest_x; int* dest_y; int blocked_ticks; @@ -73,8 +86,9 @@ static inline void osrs_player_step_require_input(const OsrsPlayerStepInput* inp fprintf(stderr, "osrs player step input is missing required fields\n"); abort(); } - if ((input->move_kind == OSRS_PLAYER_MOVE_DESTINATION || - input->move_kind == OSRS_PLAYER_MOVE_ACTION) && + if (input->command.kind == OSRS_PLAYER_CMD_MOVE && + (input->command.move_kind == OSRS_PLAYER_MOVE_DESTINATION || + input->command.move_kind == OSRS_PLAYER_MOVE_ACTION) && (!input->dest_x || !input->dest_y)) { fprintf(stderr, "osrs player step move input is missing destination storage\n"); abort(); @@ -105,21 +119,23 @@ static inline int osrs_player_step_can_attack_target( } static inline int osrs_player_step_apply_explicit_move( - const OsrsPlayerStepInput* input + const OsrsPlayerStepInput* input, + OsrsPlayerMoveKind move_kind ) { Player* player = input->player; - if (input->move_kind == OSRS_PLAYER_MOVE_ACTION) { - if (input->move_action <= 0 || input->move_action >= ENCOUNTER_MOVE_ACTIONS) + if (move_kind == OSRS_PLAYER_MOVE_ACTION) { + int move_action = input->command.move_action; + if (move_action <= 0 || move_action >= ENCOUNTER_MOVE_ACTIONS) return 0; return encounter_move_to_target( player, - ENCOUNTER_MOVE_TARGET_DX[input->move_action], - ENCOUNTER_MOVE_TARGET_DY[input->move_action], + ENCOUNTER_MOVE_TARGET_DX[move_action], + ENCOUNTER_MOVE_TARGET_DY[move_action], input->arena.is_walkable, input->arena.walkable_ctx); } - if (input->move_kind != OSRS_PLAYER_MOVE_DESTINATION) + if (move_kind != OSRS_PLAYER_MOVE_DESTINATION) return 0; return encounter_move_toward_dest( player, @@ -179,14 +195,17 @@ static inline OsrsPlayerStepResult osrs_encounter_player_step( return result; } - if (input->has_new_target) { + if (input->command.kind == OSRS_PLAYER_CMD_TARGET) { OsrsAttackTarget target; - if (osrs_player_step_lookup_target(input, input->new_target_slot, &target)) { - osrs_interaction_set(interaction, input->new_target_slot); + if (osrs_player_step_lookup_target(input, input->command.target_slot, &target)) { + osrs_interaction_set(interaction, input->command.target_slot); } else { osrs_interaction_clear(interaction); } - } else if (input->move_kind != OSRS_PLAYER_MOVE_NONE) { + /* an entity click cancels any walk already in flight */ + if (input->dest_x) *input->dest_x = -1; + if (input->dest_y) *input->dest_y = -1; + } else if (input->command.kind == OSRS_PLAYER_CMD_MOVE) { osrs_interaction_check_interrupt(interaction, OSRS_IACT_MOVE); } @@ -200,10 +219,13 @@ static inline OsrsPlayerStepResult osrs_encounter_player_step( } } - if (input->move_kind != OSRS_PLAYER_MOVE_NONE && - (!osrs_interaction_active(interaction) || - input->target_move_policy == OSRS_PLAYER_TARGET_MOVE_EXPLICIT_FIRST)) { - result.moved = osrs_player_step_apply_explicit_move(input) > 0; + /* an active interaction always chases. the only way to walk elsewhere is a + MOVE command, and that cleared the interaction above, so the two are + mutually exclusive by construction rather than by a tie-break policy. */ + if (input->command.kind == OSRS_PLAYER_CMD_MOVE && + !osrs_interaction_active(interaction)) { + result.moved = + osrs_player_step_apply_explicit_move(input, input->command.move_kind) > 0; result.explicit_moved = result.moved; } else if (osrs_interaction_active(interaction) && has_target) { result.moved = osrs_player_step_chase_target(input, &target) > 0; diff --git a/ocean/osrs/osrs_env_profile.h b/ocean/osrs/osrs_env_profile.h index e74da5f9d2..19fec16bed 100644 --- a/ocean/osrs/osrs_env_profile.h +++ b/ocean/osrs/osrs_env_profile.h @@ -1,6 +1,16 @@ #include #include +/* Defined the same way in osrs_pathfinding.h, but this header is a standalone template that + * env wrappers include on their own, so it cannot depend on that one being pulled in first. */ +#ifndef OSRS_ENV_PROFILE_THREAD_LOCAL +#if defined(__cplusplus) +#define OSRS_ENV_PROFILE_THREAD_LOCAL thread_local +#else +#define OSRS_ENV_PROFILE_THREAD_LOCAL _Thread_local +#endif +#endif + #define OSRS_ENV_PROFILE_CAT_(a, b) a##b #define OSRS_ENV_PROFILE_CAT(a, b) OSRS_ENV_PROFILE_CAT_(a, b) #define OSRS_ENV_PROFILE_FN(suffix) OSRS_ENV_PROFILE_CAT(OSRS_ENV_PROFILE_PREFIX, suffix) @@ -15,7 +25,35 @@ typedef enum { } OSRS_ENV_PROFILE_SLOT_TYPE; static int OSRS_ENV_PROFILE_G(_profile_enabled) = -1; -static double OSRS_ENV_PROFILE_G(_profile_ms)[OSRS_ENV_PROFILE_COUNT]; + +/* Per-thread accumulators, summed only at report time. + * + * This was a single shared array updated under `#pragma omp atomic`, hit ~40 times per env + * step by every worker thread. All slots fit in a handful of cache lines, so each mark became + * a contended read-modify-write and the profiler spent most of its time measuring itself: + * reported cost was ~5.7x the wall-clock truth, and inflated worst for the slots that were + * marked most often -- which distorts the shares, not just the total. + * + * Each thread gets its own cache-line-aligned row, so marks are now plain local adds. */ +#define OSRS_ENV_PROFILE_MAX_THREADS 128 +#define OSRS_ENV_PROFILE_ROW_DOUBLES \ + (((OSRS_ENV_PROFILE_COUNT + 7) / 8) * 8) + +static double OSRS_ENV_PROFILE_G(_profile_ms) + [OSRS_ENV_PROFILE_MAX_THREADS][OSRS_ENV_PROFILE_ROW_DOUBLES]; +static int OSRS_ENV_PROFILE_G(_profile_next_tid); + +static int OSRS_ENV_PROFILE_FN(_profile_tid)(void) { + static OSRS_ENV_PROFILE_THREAD_LOCAL int tid = -1; + if (tid < 0) { + int slot; + #pragma omp atomic capture + slot = OSRS_ENV_PROFILE_G(_profile_next_tid)++; + if (slot >= OSRS_ENV_PROFILE_MAX_THREADS) abort(); + tid = slot; + } + return tid; +} static const char* OSRS_ENV_PROFILE_G(_profile_names)[OSRS_ENV_PROFILE_COUNT] = { #define OSRS_ENV_PROFILE_X(slot, label) label, @@ -40,8 +78,7 @@ static double OSRS_ENV_PROFILE_FN(_profile_now_ms)(void) { static void OSRS_ENV_PROFILE_FN(_profile_add)(int slot, double ms) { if (slot < 0 || slot >= OSRS_ENV_PROFILE_COUNT) abort(); - #pragma omp atomic update - OSRS_ENV_PROFILE_G(_profile_ms)[slot] += ms; + OSRS_ENV_PROFILE_G(_profile_ms)[OSRS_ENV_PROFILE_FN(_profile_tid)()][slot] += ms; } static void OSRS_ENV_PROFILE_FN(_profile_mark)(int enabled, double* last_ms, int slot) { @@ -60,13 +97,17 @@ OSRS_ENV_PROFILE_EXPORT const char* OSRS_ENV_PROFILE_FN(_env_profile_name)(int s return OSRS_ENV_PROFILE_G(_profile_names)[slot]; } +/* Called from the reporting thread between epochs, so a plain sum over the per-thread rows is + * enough. Workers may still be marking; a torn double only skews one report. */ OSRS_ENV_PROFILE_EXPORT double OSRS_ENV_PROFILE_FN(_env_profile_read_reset_ms)(int slot) { if (slot < 0 || slot >= OSRS_ENV_PROFILE_COUNT) abort(); - double value; - #pragma omp atomic read - value = OSRS_ENV_PROFILE_G(_profile_ms)[slot]; - #pragma omp atomic write - OSRS_ENV_PROFILE_G(_profile_ms)[slot] = 0.0; + double value = 0.0; + int threads = OSRS_ENV_PROFILE_G(_profile_next_tid); + if (threads > OSRS_ENV_PROFILE_MAX_THREADS) threads = OSRS_ENV_PROFILE_MAX_THREADS; + for (int t = 0; t < threads; t++) { + value += OSRS_ENV_PROFILE_G(_profile_ms)[t][slot]; + OSRS_ENV_PROFILE_G(_profile_ms)[t][slot] = 0.0; + } return value; } diff --git a/ocean/osrs/osrs_interaction.h b/ocean/osrs/osrs_interaction.h index 7c0799dd47..b5bba0231b 100644 --- a/ocean/osrs/osrs_interaction.h +++ b/ocean/osrs/osrs_interaction.h @@ -47,10 +47,6 @@ static inline int osrs_interaction_check_interrupt(OsrsInteraction* ix, int acti } } -static inline void osrs_spec_toggle(int* spec_armed) { - *spec_armed = !(*spec_armed); -} - static inline void osrs_spec_disarm(int* spec_armed) { *spec_armed = 0; } diff --git a/ocean/osrs/osrs_inventory.h b/ocean/osrs/osrs_inventory.h index 9ee4cd4eb1..f31aaefc05 100644 --- a/ocean/osrs/osrs_inventory.h +++ b/ocean/osrs/osrs_inventory.h @@ -12,8 +12,10 @@ typedef struct { uint8_t item_idx; + uint8_t binary_padding0; uint16_t raw_osrs_id; uint8_t dose; + uint8_t binary_padding1; } OsrsInventoryCell; typedef struct { diff --git a/ocean/osrs/osrs_inventory_actions.h b/ocean/osrs/osrs_inventory_actions.h new file mode 100644 index 0000000000..cdf3c0b01d --- /dev/null +++ b/ocean/osrs/osrs_inventory_actions.h @@ -0,0 +1,288 @@ +#ifndef OSRS_INVENTORY_ACTIONS_H +#define OSRS_INVENTORY_ACTIONS_H + +#include "osrs_types.h" +#include "osrs_items.h" +#include "osrs_item_effects.h" +#include "osrs_inventory_clicks.h" + +static inline int osrs_first_empty_inventory_cell( + const OsrsInventoryCell* cells, + int except_cell +) { + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) { + if (i == except_cell) continue; + if (osrs_inventory_cell_is_empty(&cells[i])) return i; + } + return -1; +} + +static inline int osrs_inventory_cell_holds_equipped_item( + const Player* p, + const OsrsInventoryCell* cells, + int cell_idx +) { + const OsrsInventoryCell* cell = &cells[cell_idx]; + if (cell->item_idx == ITEM_NONE) return 0; + int slot = osrs_item_gear_slot(cell->item_idx); + if (slot < 0) return 0; + return p->equipped[slot] == cell->item_idx; +} + +static inline int osrs_can_equip_from_cell( + const Player* p, + const OsrsInventoryCell* cells, + int cell_idx +) { + const OsrsInventoryCell* cell = &cells[cell_idx]; + if (cell->item_idx == ITEM_NONE) return 0; + if (osrs_inventory_cell_holds_equipped_item(p, cells, cell_idx)) return 0; + + uint8_t item_idx = cell->item_idx; + int gear_slot = osrs_item_gear_slot(item_idx); + if (gear_slot < 0) return 0; + if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(item_idx) && + p->equipped[GEAR_SLOT_SHIELD] != ITEM_NONE && + p->equipped[GEAR_SLOT_WEAPON] != ITEM_NONE && + osrs_first_empty_inventory_cell(cells, cell_idx) < 0) return 0; + if (gear_slot == GEAR_SLOT_SHIELD && + item_is_two_handed(p->equipped[GEAR_SLOT_WEAPON]) && + osrs_first_empty_inventory_cell(cells, cell_idx) < 0) return 0; + return 1; +} + +/** Equips the cell's item, displacing worn gear (2h weapon/shield rules + * included) back into the inventory. Returns the gear slot equipped into, + * or -1 if the click was not equippable. */ +static inline int osrs_equip_from_cell( + Player* p, + OsrsInventoryCell* cells, + int cell_idx +) { + if (!osrs_can_equip_from_cell(p, cells, cell_idx)) return -1; + + OsrsInventoryCell* cell = &cells[cell_idx]; + uint8_t item_idx = cell->item_idx; + int gear_slot = osrs_item_gear_slot(item_idx); + if (gear_slot < 0) { + fprintf(stderr, "inventory equip: item %u has no gear slot\n", item_idx); + abort(); + } + uint8_t displaced = p->equipped[gear_slot]; + + if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(item_idx)) { + uint8_t shield = p->equipped[GEAR_SLOT_SHIELD]; + if (shield != ITEM_NONE) { + if (displaced == ITEM_NONE) { + displaced = shield; + } else { + int empty = osrs_first_empty_inventory_cell(cells, cell_idx); + if (empty < 0) return -1; + cells[empty] = osrs_inventory_cell_from_item(shield); + } + p->equipped[GEAR_SLOT_SHIELD] = ITEM_NONE; + } + } else if (gear_slot == GEAR_SLOT_SHIELD && + item_is_two_handed(p->equipped[GEAR_SLOT_WEAPON])) { + displaced = p->equipped[GEAR_SLOT_WEAPON]; + p->equipped[GEAR_SLOT_WEAPON] = ITEM_NONE; + p->spec_armed = 0; + } + + p->equipped[gear_slot] = item_idx; + *cell = osrs_inventory_cell_from_item(displaced); + if (gear_slot == GEAR_SLOT_WEAPON) p->spec_armed = 0; + osrs_refresh_player_equipment(p); + return gear_slot; +} + +static inline int osrs_can_eat_consumable_kind( + const Player* p, + OsrsConsumableKind kind +) { + switch (kind) { + case OSRS_CONSUMABLE_SHARK_FOOD: + return p->food_timer == 0 && + p->current_hitpoints < p->base_hitpoints; + case OSRS_CONSUMABLE_KARAMBWAN: + return p->karambwan_timer == 0 && + p->current_hitpoints < p->base_hitpoints; + case OSRS_CONSUMABLE_BREW: + case OSRS_CONSUMABLE_SUPER_RESTORE: + case OSRS_CONSUMABLE_SANFEW: + case OSRS_CONSUMABLE_SUPER_COMBAT: + case OSRS_CONSUMABLE_DIVINE_COMBAT: + case OSRS_CONSUMABLE_RANGING: + case OSRS_CONSUMABLE_DIVINE_RANGING: + case OSRS_CONSUMABLE_SURGE: + case OSRS_CONSUMABLE_GUTHIX_REST: + case OSRS_CONSUMABLE_SATURATED_HEART: + case OSRS_CONSUMABLE_ANTIVENOM_PLUS: + case OSRS_CONSUMABLE_PRAYER_RESTORE: + case OSRS_CONSUMABLE_BASTION: + case OSRS_CONSUMABLE_STAMINA: + case OSRS_CONSUMABLE_NONE: + return 0; + } + abort(); +} + +/** One tick's worth of inventory-click actions: per-gear-slot equip heads + * plus eat and drink heads, each 0 = no-op or 1..28 = inventory cell + 1. */ +typedef struct { + int equip_by_slot[NUM_GEAR_SLOTS]; + int eat; + int drink; +} OsrsInventoryClickActions; + +typedef struct { + int equip_cell_by_slot[NUM_GEAR_SLOTS]; + int equip_order_by_slot[NUM_GEAR_SLOTS]; + int eat_cell; + int eat_order; + OsrsInventoryClickResolution eat_resolution; + int drink_cell; + int drink_order; + OsrsInventoryClickResolution drink_resolution; +} OsrsInventoryTickIntent; + +static inline OsrsInventoryTickIntent osrs_inventory_tick_intent_empty(void) { + OsrsInventoryTickIntent intent; + memset(&intent, 0, sizeof(intent)); + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + intent.equip_cell_by_slot[slot] = -1; + intent.equip_order_by_slot[slot] = -1; + } + intent.eat_cell = -1; + intent.eat_order = -1; + intent.drink_cell = -1; + intent.drink_order = -1; + return intent; +} + +static inline OsrsInventoryTickIntent osrs_resolve_inventory_tick_intent( + const Player* p, + const OsrsInventoryCell* cells, + const OsrsInventoryClickActions* clicks +) { + OsrsInventoryTickIntent intent = osrs_inventory_tick_intent_empty(); + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + int action = clicks->equip_by_slot[slot]; + if (action <= 0 || action > OSRS_INVENTORY_SIZE) continue; + int cell_idx = action - 1; + if (!osrs_can_equip_from_cell(p, cells, cell_idx)) continue; + int gear_slot = osrs_item_gear_slot(cells[cell_idx].item_idx); + if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) { + fprintf(stderr, "inventory equip intent: invalid gear slot %d\n", gear_slot); + abort(); + } + if (gear_slot != slot) continue; + intent.equip_cell_by_slot[slot] = cell_idx; + intent.equip_order_by_slot[slot] = slot; + } + + if (clicks->eat > 0 && clicks->eat <= OSRS_INVENTORY_SIZE) { + int cell_idx = clicks->eat - 1; + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret(&cells[cell_idx], OSRS_CLICK_TICK_FIRST); + if (resolution.click_action == OSRS_CLICK_EAT && + osrs_can_eat_consumable_kind(p, resolution.consumable_kind)) { + intent.eat_cell = cell_idx; + intent.eat_order = NUM_GEAR_SLOTS; + intent.eat_resolution = resolution; + } + } + + if (clicks->drink > 0 && clicks->drink <= OSRS_INVENTORY_SIZE) { + int cell_idx = clicks->drink - 1; + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret(&cells[cell_idx], OSRS_CLICK_TICK_FIRST); + if (resolution.click_action == OSRS_CLICK_DRINK && + cells[cell_idx].dose > 0 && + p->potion_timer == 0) { + intent.drink_cell = cell_idx; + intent.drink_order = NUM_GEAR_SLOTS + 1; + intent.drink_resolution = resolution; + } + } + + return intent; +} + +static inline int osrs_inventory_tick_intent_has_effect( + const OsrsInventoryTickIntent* intent +) { + if (intent->eat_cell >= 0 || intent->drink_cell >= 0) return 1; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + if (intent->equip_cell_by_slot[slot] >= 0) return 1; + return 0; +} + +static inline int osrs_next_inventory_apply_order( + const OsrsInventoryTickIntent* intent +) { + int sentinel = NUM_GEAR_SLOTS + 2; + int best = sentinel; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + int order = intent->equip_order_by_slot[slot]; + if (order >= 0 && order < best) best = order; + } + if (intent->eat_order >= 0 && intent->eat_order < best) best = intent->eat_order; + if (intent->drink_order >= 0 && intent->drink_order < best) best = intent->drink_order; + return best == sentinel ? -1 : best; +} + +typedef enum { + OSRS_INVENTORY_APPLY_EQUIP = 0, + OSRS_INVENTORY_APPLY_EAT = 1, + OSRS_INVENTORY_APPLY_DRINK = 2, +} OsrsInventoryApplyKind; + +typedef struct { + OsrsInventoryApplyKind kind; + int gear_slot; + int cell_idx; + OsrsInventoryClickResolution resolution; +} OsrsInventoryApplyStep; + +/** Pops the intent's next click in OSRS application order (equip slots + * ascending, then eat, then drink). Returns 0 when the intent is drained. */ +static inline int osrs_inventory_intent_next( + OsrsInventoryTickIntent* intent, + OsrsInventoryApplyStep* out +) { + int order = osrs_next_inventory_apply_order(intent); + if (order < 0) return 0; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + if (intent->equip_order_by_slot[slot] != order) continue; + out->kind = OSRS_INVENTORY_APPLY_EQUIP; + out->gear_slot = slot; + out->cell_idx = intent->equip_cell_by_slot[slot]; + memset(&out->resolution, 0, sizeof(out->resolution)); + intent->equip_order_by_slot[slot] = -1; + intent->equip_cell_by_slot[slot] = -1; + return 1; + } + if (intent->eat_order == order) { + out->kind = OSRS_INVENTORY_APPLY_EAT; + out->gear_slot = -1; + out->cell_idx = intent->eat_cell; + out->resolution = intent->eat_resolution; + intent->eat_order = -1; + intent->eat_cell = -1; + return 1; + } + if (intent->drink_order == order) { + out->kind = OSRS_INVENTORY_APPLY_DRINK; + out->gear_slot = -1; + out->cell_idx = intent->drink_cell; + out->resolution = intent->drink_resolution; + intent->drink_order = -1; + intent->drink_cell = -1; + return 1; + } + abort(); +} + +#endif diff --git a/ocean/osrs/osrs_inventory_clicks.h b/ocean/osrs/osrs_inventory_clicks.h index 0fc81d9744..d22b40b15a 100644 --- a/ocean/osrs/osrs_inventory_clicks.h +++ b/ocean/osrs/osrs_inventory_clicks.h @@ -4,6 +4,7 @@ #include "osrs_inventory.h" #include "osrs_items.h" #include "osrs_consumables.h" +#include "osrs_special_attacks.h" typedef enum { OSRS_CLICK_NONE = 0, @@ -27,6 +28,9 @@ typedef enum { OSRS_CONSUMABLE_ANTIVENOM_PLUS = 11, OSRS_CONSUMABLE_SHARK_FOOD = 12, OSRS_CONSUMABLE_KARAMBWAN = 13, + OSRS_CONSUMABLE_PRAYER_RESTORE = 14, + OSRS_CONSUMABLE_BASTION = 15, + OSRS_CONSUMABLE_STAMINA = 16, } OsrsConsumableKind; typedef enum { @@ -63,6 +67,28 @@ typedef void (*OsrsInventoryDrinkOneDoseEffectFn)( ); #define OSRS_INVENTORY_CELL_OBS_FEATURES 28 +#define OSRS_INVENTORY_CELL_OBS_SHARED 5 +#define OSRS_INVENTORY_CELL_OBS_KIND_UNION 10 +#define OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT \ + (OSRS_INVENTORY_CELL_OBS_SHARED + OSRS_INVENTORY_CELL_OBS_KIND_UNION) + +/* Slot names for the compact record. The coded observation ships three of these and an + item-table gather rebuilds the rest, so both sides must agree on where they sit. */ +#define OSRS_INVENTORY_CELL_COMPACT_PRESENT 0 +#define OSRS_INVENTORY_CELL_COMPACT_EQUIPPED 1 +#define OSRS_INVENTORY_CELL_COMPACT_DOSE 2 +#define OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR 3 +#define OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON 4 +/* Union slot: effect_class4[2] on a gear cell, hp_heal on a consumable cell. */ +#define OSRS_INVENTORY_CELL_COMPACT_HP_HEAL (OSRS_INVENTORY_CELL_OBS_SHARED + 5) + +/* The coded observation: everything a cell carries that the item alone does not determine. + hp_heal is here because it divides by base_hitpoints, which the Frailty modifier rewrites + mid-episode; dose and the other ten features are pure item facts and live in the table. */ +#define OSRS_INVENTORY_CELL_OBS_CODE 0 +#define OSRS_INVENTORY_CELL_OBS_EQUIPPED 1 +#define OSRS_INVENTORY_CELL_OBS_HP_HEAL 2 +#define OSRS_INVENTORY_CELL_OBS_FEATURES_CODED 3 #define OSRS_EQUIPPED_SELF_OBS_FEATURES 18 static const OsrsConsumableClick OSRS_CONSUMABLE_CLICK_REGISTRY[] = { @@ -107,6 +133,18 @@ static const OsrsConsumableClick OSRS_CONSUMABLE_CLICK_REGISTRY[] = { {12915, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 3}, {12917, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 2}, {12919, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 1}, + {2434, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 4}, + {139, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 3}, + {141, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 2}, + {143, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 1}, + {22461, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 4}, + {22464, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 3}, + {22467, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 2}, + {22470, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 1}, + {12625, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 4}, + {12627, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 3}, + {12629, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 2}, + {12631, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 1}, {385, OSRS_CLICK_EAT, OSRS_CONSUMABLE_SHARK_FOOD, 0}, {3144, OSRS_CLICK_EAT, OSRS_CONSUMABLE_KARAMBWAN, 0}, }; @@ -143,15 +181,18 @@ static inline OsrsConsumableKind6 col_consumable_kind6(OsrsConsumableKind k) { switch (k) { case OSRS_CONSUMABLE_BREW: return COL_CKIND6_BREW; case OSRS_CONSUMABLE_SUPER_RESTORE: - case OSRS_CONSUMABLE_SANFEW: return COL_CKIND6_RESTORE; + case OSRS_CONSUMABLE_SANFEW: + case OSRS_CONSUMABLE_PRAYER_RESTORE: return COL_CKIND6_RESTORE; case OSRS_CONSUMABLE_SUPER_COMBAT: case OSRS_CONSUMABLE_DIVINE_COMBAT: return COL_CKIND6_COMBAT_BOOST; case OSRS_CONSUMABLE_RANGING: - case OSRS_CONSUMABLE_DIVINE_RANGING: return COL_CKIND6_RANGED_BOOST; + case OSRS_CONSUMABLE_DIVINE_RANGING: + case OSRS_CONSUMABLE_BASTION: return COL_CKIND6_RANGED_BOOST; case OSRS_CONSUMABLE_SURGE: case OSRS_CONSUMABLE_GUTHIX_REST: case OSRS_CONSUMABLE_SATURATED_HEART: - case OSRS_CONSUMABLE_ANTIVENOM_PLUS: return COL_CKIND6_SPECIAL; + case OSRS_CONSUMABLE_ANTIVENOM_PLUS: + case OSRS_CONSUMABLE_STAMINA: return COL_CKIND6_SPECIAL; case OSRS_CONSUMABLE_SHARK_FOOD: case OSRS_CONSUMABLE_KARAMBWAN: return COL_CKIND6_FOOD; case OSRS_CONSUMABLE_NONE: return COL_CKIND6_NONE; @@ -172,6 +213,7 @@ static inline int osrs_consumable_hp_heal_amount(OsrsConsumableKind k, int base_ static inline int osrs_consumable_prayer_restore_amount(OsrsConsumableKind k, int base_prayer) { switch (k) { case OSRS_CONSUMABLE_SUPER_RESTORE: return osrs_super_restore_amount(base_prayer); + case OSRS_CONSUMABLE_PRAYER_RESTORE: return osrs_prayer_potion_restore_amount(base_prayer); case OSRS_CONSUMABLE_SANFEW: return osrs_sanfew_restore_amount(base_prayer); default: return 0; } @@ -182,7 +224,8 @@ static inline int osrs_consumable_offensive_boost_amount(OsrsConsumableKind k, i case OSRS_CONSUMABLE_SUPER_COMBAT: case OSRS_CONSUMABLE_DIVINE_COMBAT: return osrs_super_combat_boost_amount(base_level); case OSRS_CONSUMABLE_RANGING: - case OSRS_CONSUMABLE_DIVINE_RANGING: return osrs_ranging_boost_amount(base_level); + case OSRS_CONSUMABLE_DIVINE_RANGING: + case OSRS_CONSUMABLE_BASTION: return osrs_ranging_boost_amount(base_level); case OSRS_CONSUMABLE_SATURATED_HEART: return osrs_saturated_heart_magic_boost(base_level); default: return 0; } @@ -211,8 +254,25 @@ static inline uint8_t osrs_item_index_for_raw_osrs_id(uint16_t raw_osrs_id) { return ITEM_NONE; } -static inline void osrs_write_inventory_cell_affordance_features( - float* out, +typedef struct { + float present; + float is_equipped; + float dose; + float is_armor; + float is_weapon; + float style3[3]; + float post_use_deltas[6]; + float effect_class4[4]; + float attack_speed; + float attack_range; + float spec_cost; + float kind5[5]; + float hp_heal; + float prayer_restore; + float offensive_boost; +} OsrsInventoryCellAffordance; + +static inline OsrsInventoryCellAffordance osrs_inventory_cell_affordance( uint8_t item_idx, uint16_t raw_osrs_id, uint8_t dose, @@ -227,42 +287,121 @@ static inline void osrs_write_inventory_cell_affordance_features( int present = raw_osrs_id != 0 || item_idx != ITEM_NONE; int is_gear = item_idx != ITEM_NONE; int style = is_gear ? get_item_attack_style(item_idx) : 0; - uint32_t effect_mask = is_gear ? ITEM_DATABASE[item_idx].effect_mask : OSRS_ITEM_EFFECT_NONE; - - out[0] = present ? 1.0f : 0.0f; - out[1] = is_equipped ? 1.0f : 0.0f; - out[2] = dose > 0 ? osrs_clamp_unit((float)dose / 4.0f) : 0.0f; - out[3] = style == 1 ? 1.0f : 0.0f; - out[4] = style == 2 ? 1.0f : 0.0f; - out[5] = style == 3 ? 1.0f : 0.0f; - for (int i = 0; i < 6; i++) out[6 + i] = osrs_clamp_unit(post_use_deltas[i]); - + uint32_t effect_mask = + is_gear ? ITEM_DATABASE[item_idx].effect_mask : OSRS_ITEM_EFFECT_NONE; int is_weapon = is_gear && ITEM_DATABASE[item_idx].slot == SLOT_WEAPON; - out[12] = (is_gear && !is_weapon) ? 1.0f : 0.0f; - out[13] = is_weapon ? 1.0f : 0.0f; OsrsConsumableKind6 k6 = col_consumable_kind6(consumable.consumable_kind); - out[14] = k6 == COL_CKIND6_BREW ? 1.0f : 0.0f; - out[15] = k6 == COL_CKIND6_RESTORE ? 1.0f : 0.0f; - out[16] = k6 == COL_CKIND6_COMBAT_BOOST ? 1.0f : 0.0f; - out[17] = k6 == COL_CKIND6_RANGED_BOOST ? 1.0f : 0.0f; - out[18] = k6 == COL_CKIND6_SPECIAL ? 1.0f : 0.0f; OsrsConsumableKind ck = consumable.consumable_kind; int hp_heal = osrs_consumable_hp_heal_amount(ck, base_hitpoints); int pray_restore = osrs_consumable_prayer_restore_amount(ck, base_prayer); int off_boost = osrs_consumable_offensive_boost_amount(ck, base_level); - out[19] = base_hitpoints > 0 ? osrs_clamp_unit((float)hp_heal / (float)base_hitpoints) : 0.0f; - out[20] = base_prayer > 0 ? osrs_clamp_unit((float)pray_restore / (float)base_prayer) : 0.0f; - out[21] = osrs_clamp_unit((float)off_boost / STAT_NORM_STRENGTH); - float eff4[4]; - osrs_item_effect_class4(effect_mask, eff4); - out[22] = eff4[0]; - out[23] = eff4[1]; - out[24] = eff4[2]; - out[25] = eff4[3]; - out[26] = is_weapon - ? osrs_clamp_unit((float)ITEM_DATABASE[item_idx].attack_speed / STAT_NORM_SPEED) : 0.0f; - out[27] = is_weapon - ? osrs_clamp_unit((float)ITEM_DATABASE[item_idx].attack_range / STAT_NORM_RANGE) : 0.0f; + + OsrsInventoryCellAffordance a; + a.present = present ? 1.0f : 0.0f; + a.is_equipped = is_equipped ? 1.0f : 0.0f; + a.dose = dose > 0 ? osrs_clamp_unit((float)dose / 4.0f) : 0.0f; + a.is_armor = (is_gear && !is_weapon) ? 1.0f : 0.0f; + a.is_weapon = is_weapon ? 1.0f : 0.0f; + a.style3[0] = style == 1 ? 1.0f : 0.0f; + a.style3[1] = style == 2 ? 1.0f : 0.0f; + a.style3[2] = style == 3 ? 1.0f : 0.0f; + for (int i = 0; i < 6; i++) + a.post_use_deltas[i] = + post_use_deltas ? osrs_clamp_unit(post_use_deltas[i]) : 0.0f; + /* Normalised energy cost, 0 when the weapon has no special. Doubles as the "this item + * has a special attack" flag while also telling the agent whether it can afford one. */ + a.spec_cost = is_weapon + ? osrs_clamp_unit((float)osrs_spec_cost(item_idx) / 100.0f) + : 0.0f; + osrs_item_effect_class4(effect_mask, a.effect_class4); + a.attack_speed = is_weapon + ? osrs_clamp_unit((float)ITEM_DATABASE[item_idx].attack_speed / STAT_NORM_SPEED) + : 0.0f; + a.attack_range = is_weapon + ? osrs_clamp_unit((float)ITEM_DATABASE[item_idx].attack_range / STAT_NORM_RANGE) + : 0.0f; + a.kind5[0] = k6 == COL_CKIND6_BREW ? 1.0f : 0.0f; + a.kind5[1] = k6 == COL_CKIND6_RESTORE ? 1.0f : 0.0f; + a.kind5[2] = k6 == COL_CKIND6_COMBAT_BOOST ? 1.0f : 0.0f; + a.kind5[3] = k6 == COL_CKIND6_RANGED_BOOST ? 1.0f : 0.0f; + a.kind5[4] = k6 == COL_CKIND6_SPECIAL ? 1.0f : 0.0f; + a.hp_heal = base_hitpoints > 0 + ? osrs_clamp_unit((float)hp_heal / (float)base_hitpoints) : 0.0f; + a.prayer_restore = base_prayer > 0 + ? osrs_clamp_unit((float)pray_restore / (float)base_prayer) : 0.0f; + a.offensive_boost = osrs_clamp_unit((float)off_boost / STAT_NORM_STRENGTH); + return a; +} + +static inline void osrs_write_inventory_cell_affordance_features( + float* out, + uint8_t item_idx, + uint16_t raw_osrs_id, + uint8_t dose, + int is_equipped, + const float post_use_deltas[6], + int base_hitpoints, + int base_prayer, + int base_level +) { + OsrsInventoryCellAffordance a = osrs_inventory_cell_affordance( + item_idx, raw_osrs_id, dose, is_equipped, post_use_deltas, + base_hitpoints, base_prayer, base_level); + + out[0] = a.present; + out[1] = a.is_equipped; + out[2] = a.dose; + out[3] = a.style3[0]; + out[4] = a.style3[1]; + out[5] = a.style3[2]; + for (int i = 0; i < 6; i++) out[6 + i] = a.post_use_deltas[i]; + out[12] = a.is_armor; + out[13] = a.is_weapon; + for (int i = 0; i < 5; i++) out[14 + i] = a.kind5[i]; + out[19] = a.hp_heal; + out[20] = a.prayer_restore; + out[21] = a.offensive_boost; + for (int i = 0; i < 4; i++) out[22 + i] = a.effect_class4[i]; + out[26] = a.attack_speed; + out[27] = a.attack_range; +} + +static inline void osrs_write_inventory_cell_affordance_features_compact( + float* out, + uint8_t item_idx, + uint16_t raw_osrs_id, + uint8_t dose, + int is_equipped, + int base_hitpoints, + int base_prayer, + int base_level +) { + OsrsInventoryCellAffordance a = osrs_inventory_cell_affordance( + item_idx, raw_osrs_id, dose, is_equipped, NULL, + base_hitpoints, base_prayer, base_level); + + out[OSRS_INVENTORY_CELL_COMPACT_PRESENT] = a.present; + out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] = a.is_equipped; + out[OSRS_INVENTORY_CELL_COMPACT_DOSE] = a.dose; + out[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] = a.is_armor; + out[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] = a.is_weapon; + + float* u = out + OSRS_INVENTORY_CELL_OBS_SHARED; + for (int i = 0; i < OSRS_INVENTORY_CELL_OBS_KIND_UNION; i++) u[i] = 0.0f; + if (a.is_armor != 0.0f || a.is_weapon != 0.0f) { + u[0] = a.style3[0]; + u[1] = a.style3[1]; + u[2] = a.style3[2]; + for (int i = 0; i < 4; i++) u[3 + i] = a.effect_class4[i]; + u[7] = a.attack_speed; + u[8] = a.attack_range; + u[9] = a.spec_cost; + } else { + for (int i = 0; i < 5; i++) u[i] = a.kind5[i]; + u[5] = a.hp_heal; + u[6] = a.prayer_restore; + u[7] = a.offensive_boost; + } } static inline void osrs_write_equipped_self_features(float* out, uint8_t item_idx) { @@ -551,4 +690,140 @@ static inline void osrs_inventory_cell_consume_eat(OsrsInventoryCell* cell) { *cell = osrs_inventory_cell_empty(); } +/* One dense code per distinguishable cell content. Gear keys on the item index; consumables + key on their click-registry entry, because a potion's raw id is what carries its kind and + its dose; every remaining present-but-inert raw id collapses onto a single code, since no + derived feature can tell two of them apart. + + The code rides the observation divided by OSRS_INVENTORY_CELL_OBS_CODE_SCALE. The encoder's + global linear layer reads the WHOLE observation, so 28 raw codes near 200 sitting beside + 894 features in [0,1] outweigh everything else by four orders of magnitude in the input + variance -- measured, and it cost episode_return 7.8 -> 0.6 with entropy collapsing 7.3 -> + 1.9 over 30M steps. Scaling by a power of two only shifts the exponent, so a code that is + exact in bf16 stays exact and the gather's multiply-back is lossless; that is also why the + count must stay at or under the scale. */ +#define OSRS_CONSUMABLE_CLICK_REGISTRY_COUNT \ + ((int)(sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / \ + sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0]))) +#define OSRS_INVENTORY_CELL_OBS_CODE_EMPTY 0 +#define OSRS_INVENTORY_CELL_OBS_CODE_GEAR_BASE 1 +#define OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE \ + (OSRS_INVENTORY_CELL_OBS_CODE_GEAR_BASE + NUM_ITEMS) +#define OSRS_INVENTORY_CELL_OBS_CODE_INERT \ + (OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE + OSRS_CONSUMABLE_CLICK_REGISTRY_COUNT) +#define OSRS_INVENTORY_CELL_OBS_CODE_COUNT (OSRS_INVENTORY_CELL_OBS_CODE_INERT + 1) +#define OSRS_INVENTORY_CELL_OBS_CODE_SCALE 256 + +static inline float osrs_inventory_cell_obs_code_encode(int code) { + return (float)code / (float)OSRS_INVENTORY_CELL_OBS_CODE_SCALE; +} + +static inline int osrs_inventory_cell_obs_code_decode(float observed) { + return (int)lrintf(observed * (float)OSRS_INVENTORY_CELL_OBS_CODE_SCALE); +} + +/* Stands in for every present raw id that is neither gear nor a registered consumable. */ +#define OSRS_INVENTORY_CELL_INERT_RAW_OSRS_ID 0xFFFFu + +static inline int osrs_inventory_cell_obs_code(uint8_t item_idx, uint16_t raw_osrs_id) { + if (item_idx != ITEM_NONE) { + if (item_idx >= NUM_ITEMS) osrs_inventory_clicks_trap(); + return OSRS_INVENTORY_CELL_OBS_CODE_GEAR_BASE + item_idx; + } + if (raw_osrs_id == 0) return OSRS_INVENTORY_CELL_OBS_CODE_EMPTY; + for (int i = 0; i < OSRS_CONSUMABLE_CLICK_REGISTRY_COUNT; i++) { + if (OSRS_CONSUMABLE_CLICK_REGISTRY[i].raw_osrs_id == raw_osrs_id) { + return OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE + i; + } + } + return OSRS_INVENTORY_CELL_OBS_CODE_INERT; +} + +static inline OsrsInventoryCell osrs_inventory_cell_for_obs_code(int code) { + if (code < 0 || code >= OSRS_INVENTORY_CELL_OBS_CODE_COUNT) { + osrs_inventory_clicks_trap(); + } + if (code == OSRS_INVENTORY_CELL_OBS_CODE_EMPTY) return osrs_inventory_cell_empty(); + if (code < OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE) { + return osrs_inventory_cell_from_item( + (uint8_t)(code - OSRS_INVENTORY_CELL_OBS_CODE_GEAR_BASE)); + } + if (code < OSRS_INVENTORY_CELL_OBS_CODE_INERT) { + return osrs_inventory_cell_from_raw_osrs_id( + OSRS_CONSUMABLE_CLICK_REGISTRY[ + code - OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE].raw_osrs_id); + } + return (OsrsInventoryCell){ + .item_idx = ITEM_NONE, + .raw_osrs_id = OSRS_INVENTORY_CELL_INERT_RAW_OSRS_ID, + .dose = 0, + }; +} + +/* The item-table row: the compact record with the two slots the observation supplies zeroed, + so a gather can add the observed values in without branching on the cell's kind. On a gear + cell the union slot holds effect_class4[2], which is an item fact and stays. */ +static inline void osrs_write_inventory_cell_obs_table_row( + float* out, + int code, + int base_hitpoints, + int base_prayer, + int base_level +) { + OsrsInventoryCell cell = osrs_inventory_cell_for_obs_code(code); + osrs_write_inventory_cell_affordance_features_compact( + out, cell.item_idx, cell.raw_osrs_id, cell.dose, 0, + base_hitpoints, base_prayer, base_level); + + out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] = 0.0f; + if (out[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] == 0.0f && + out[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] == 0.0f) { + out[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL] = 0.0f; + } +} + +/* The other half of the same record. Projecting both sides off one compact write is what + makes them complementary: a change to the affordance reaches the table and the + observation together instead of drifting between them. */ +static inline void osrs_write_inventory_cell_obs_code_features( + float* out, + uint8_t item_idx, + uint16_t raw_osrs_id, + uint8_t dose, + int is_equipped, + int base_hitpoints, + int base_prayer, + int base_level +) { + float compact[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; + osrs_write_inventory_cell_affordance_features_compact( + compact, item_idx, raw_osrs_id, dose, is_equipped, + base_hitpoints, base_prayer, base_level); + + int is_gear = compact[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] != 0.0f || + compact[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] != 0.0f; + + out[OSRS_INVENTORY_CELL_OBS_CODE] = osrs_inventory_cell_obs_code_encode( + osrs_inventory_cell_obs_code(item_idx, raw_osrs_id)); + out[OSRS_INVENTORY_CELL_OBS_EQUIPPED] = + compact[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED]; + out[OSRS_INVENTORY_CELL_OBS_HP_HEAL] = + is_gear ? 0.0f : compact[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL]; +} + +/* Inverse of the split above, and the exact arithmetic colo_ent_gather_inv performs. */ +static inline void osrs_expand_inventory_cell_obs_code_features( + float* out, + const float* coded, + const float* table_row +) { + for (int f = 0; f < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; f++) { + out[f] = table_row[f]; + } + out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] += + coded[OSRS_INVENTORY_CELL_OBS_EQUIPPED]; + out[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL] += + coded[OSRS_INVENTORY_CELL_OBS_HP_HEAL]; +} + #endif diff --git a/ocean/osrs/osrs_pathfinding.h b/ocean/osrs/osrs_pathfinding.h index b41729038e..c581d27630 100644 --- a/ocean/osrs/osrs_pathfinding.h +++ b/ocean/osrs/osrs_pathfinding.h @@ -350,7 +350,9 @@ static inline PathResult pathfind_step_arena( } while (1) { + if (!BFS_VISITED(cur_x, cur_y)) break; int v = BFS_VIA(cur_x, cur_y); + if (v == VIA_NONE || v == VIA_START) break; int prev_x = cur_x, prev_y = cur_y; if (v & VIA_W) prev_x++; else if (v & VIA_E) prev_x--; if (v & VIA_S) prev_y++; else if (v & VIA_N) prev_y--; @@ -360,8 +362,8 @@ static inline PathResult pathfind_step_arena( result.next_dy = cur_y - local_src_y; return result; } + if (prev_x < 0 || prev_x >= arena_w || prev_y < 0 || prev_y >= arena_h) break; cur_x = prev_x; cur_y = prev_y; - if (BFS_VIA(cur_x, cur_y) == VIA_NONE || BFS_VIA(cur_x, cur_y) == VIA_START) break; } return result; diff --git a/ocean/osrs/osrs_player_consumables.h b/ocean/osrs/osrs_player_consumables.h index 8068ce361b..c1a779fb8f 100644 --- a/ocean/osrs/osrs_player_consumables.h +++ b/ocean/osrs/osrs_player_consumables.h @@ -45,18 +45,9 @@ static inline int osrs_player_can_eat_food_type(const Player* p, FoodType type) return r.consumed; } -static inline int osrs_player_food_wasted_hp(const Player* p, FoodType type) { - osrs_require_player_food_action(type); - EatResult r = osrs_eat_food(type, p->current_hitpoints, - p->base_hitpoints, osrs_player_food_timer(p, type)); - if (!r.consumed) return 0; - return osrs_food_heal_amount(type) - r.hp_healed; -} - -static inline OsrsPlayerEatResult osrs_player_eat_food_type(Player* p, FoodType type) { +static inline OsrsPlayerEatResult osrs_player_eat_food_effects(Player* p, FoodType type) { osrs_require_player_food_action(type); OsrsPlayerEatResult out = {0, 0, 0, 0}; - if (osrs_player_food_count(p, type) <= 0) return out; EatResult r = osrs_eat_food(type, p->current_hitpoints, p->base_hitpoints, osrs_player_food_timer(p, type)); @@ -68,7 +59,6 @@ static inline OsrsPlayerEatResult osrs_player_eat_food_type(Player* p, FoodType out.hp_wasted = heal_amount - r.hp_healed; if (type == FOOD_KARAMBWAN) { - p->karambwan_count--; p->karambwan_timer = 2; p->food_timer = 3; p->potion_timer = 3; @@ -77,7 +67,6 @@ static inline OsrsPlayerEatResult osrs_player_eat_food_type(Player* p, FoodType p->last_karambwan_waste = out.hp_wasted; out.attack_delay_ticks = 2; } else { - p->food_count--; p->food_timer = 3; p->ate_food_this_tick = 1; p->last_food_heal = out.hp_healed; @@ -89,15 +78,24 @@ static inline OsrsPlayerEatResult osrs_player_eat_food_type(Player* p, FoodType if (type != FOOD_ANGLERFISH && p->current_hitpoints > p->base_hitpoints) p->current_hitpoints = p->base_hitpoints; - int combat_ticks = 0; - if (p->has_attack_timer) { - combat_ticks = p->attack_timer > 0 ? p->attack_timer : 0; + if (p->attack_timer > 0) { + int combat_ticks = p->attack_timer; + p->attack_timer = combat_ticks + out.attack_delay_ticks; + p->attack_timer_uncapped = combat_ticks + out.attack_delay_ticks; } - p->attack_timer = combat_ticks + out.attack_delay_ticks; - p->attack_timer_uncapped = combat_ticks + out.attack_delay_ticks; p->has_attack_timer = 1; return out; } +static inline OsrsPlayerEatResult osrs_player_eat_food_type(Player* p, FoodType type) { + if (osrs_player_food_count(p, type) <= 0) return (OsrsPlayerEatResult){0, 0, 0, 0}; + OsrsPlayerEatResult out = osrs_player_eat_food_effects(p, type); + if (out.consumed) { + if (type == FOOD_KARAMBWAN) p->karambwan_count--; + else p->food_count--; + } + return out; +} + #endif diff --git a/ocean/osrs/osrs_pvp_actions.h b/ocean/osrs/osrs_pvp_actions.h index e371ce066e..cbf865bc16 100644 --- a/ocean/osrs/osrs_pvp_actions.h +++ b/ocean/osrs/osrs_pvp_actions.h @@ -296,8 +296,14 @@ static void execute_switches(OsrsEnv* env, int agent_idx, int* actions) { loadout_action == LOADOUT_SPEC_MAGIC || loadout_action == LOADOUT_GMAUL); + int attack_claims_tick = is_attack_action(combat_action); + if (attack_claims_tick) { + env->pvp_runtime.walk_dest_x[agent_idx] = -1; + env->pvp_runtime.walk_dest_y[agent_idx] = -1; + } + int command_issued = 0; - if (!is_spec_loadout && head_move > 0 && head_move < MOVE_DIM) { + if (!is_spec_loadout && !attack_claims_tick && head_move > 0 && head_move < MOVE_DIM) { pvp_set_walk_dest_from_head_move(env, agent_idx, head_move); command_issued = 1; } else if (!is_spec_loadout && is_move_action(combat_action)) { diff --git a/ocean/osrs/osrs_pvp_combat.h b/ocean/osrs/osrs_pvp_combat.h index 58a1344ce2..d4341c5d8c 100644 --- a/ocean/osrs/osrs_pvp_combat.h +++ b/ocean/osrs/osrs_pvp_combat.h @@ -340,10 +340,10 @@ static inline int get_blood_base_hit(int current_magic) { } static inline int get_blood_heal_percent(int current_magic) { - if (current_magic >= BLOOD_BARRAGE_LEVEL) return 25; - if (current_magic >= BLOOD_BLITZ_LEVEL) return 20; - if (current_magic >= BLOOD_BURST_LEVEL) return 15; - return 10; + if (current_magic >= BLOOD_BARRAGE_LEVEL) return BLOOD_BARRAGE_HEAL_PERCENT; + if (current_magic >= BLOOD_BLITZ_LEVEL) return BLOOD_BLITZ_HEAL_PERCENT; + if (current_magic >= BLOOD_BURST_LEVEL) return BLOOD_BURST_HEAL_PERCENT; + return BLOOD_RUSH_HEAL_PERCENT; } /* pvp hit delays historically excluded the +1 attacker-is-player tick, so every diff --git a/ocean/osrs/osrs_pvp_movement.h b/ocean/osrs/osrs_pvp_movement.h index 858f1a5717..355a43fbe6 100644 --- a/ocean/osrs/osrs_pvp_movement.h +++ b/ocean/osrs/osrs_pvp_movement.h @@ -310,11 +310,10 @@ static inline OsrsPlayerStepResult pvp_step_player_movement(OsrsEnv* env, int ag .interaction = &p->interaction, .target_lookup = pvp_lookup_attack_target, .target_ctx = env, - .has_new_target = 0, - .new_target_slot = -1, - .move_kind = OSRS_PLAYER_MOVE_DESTINATION, - .target_move_policy = OSRS_PLAYER_TARGET_MOVE_EXPLICIT_FIRST, - .move_action = 0, + .command = { + .kind = OSRS_PLAYER_CMD_MOVE, + .move_kind = OSRS_PLAYER_MOVE_DESTINATION, + }, .dest_x = dest_x, .dest_y = dest_y, .blocked_ticks = p->frozen_ticks, diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h index f6755e046d..1e65b105a7 100644 --- a/ocean/osrs/osrs_render.h +++ b/ocean/osrs/osrs_render.h @@ -775,6 +775,13 @@ static ColosseumState* render_colosseum_state_from_env(OsrsEnv* env) { return (ColosseumState*)env->encounter_state; } +static ZulrahState* render_zulrah_state_from_env(OsrsEnv* env) { + if (!env || !env->encounter_def || !env->encounter_state) return NULL; + const EncounterDef* def = (const EncounterDef*)env->encounter_def; + if (strcmp(def->name, "zulrah") != 0) return NULL; + return (ZulrahState*)env->encounter_state; +} + static Color render_inferno_lab_forecast_color( const InfStepOutForecastAction* action ) { @@ -6288,6 +6295,20 @@ void pvp_render(OsrsEnv* env) { rc->gui.display_inventory_osrs_ids[i] = live_kit[i]; rc->gui.display_inventory_count = COLO_INVENTORY_DISPLAY_SLOTS; } + ZulrahState* zul_inv = render_zulrah_state_from_env(env); + if (zul_inv) { + for (int i = 0; i < OSRS_INVENTORY_SIZE && i < INV_GRID_SLOTS; i++) + rc->gui.display_inventory_osrs_ids[i] = + zul_inv->inventory_cells[i].raw_osrs_id; + rc->gui.display_inventory_count = OSRS_INVENTORY_SIZE; + } + InfernoState* inf_inv = render_inferno_state_from_env(env); + if (inf_inv) { + for (int i = 0; i < OSRS_INVENTORY_SIZE && i < INV_GRID_SLOTS; i++) + rc->gui.display_inventory_osrs_ids[i] = + inf_inv->inventory_cells[i].raw_osrs_id; + rc->gui.display_inventory_count = OSRS_INVENTORY_SIZE; + } } if (gui_player) { BeginMode2D(render_chrome_camera( diff --git a/ocean/osrs/osrs_types.h b/ocean/osrs/osrs_types.h index 3252d3f455..2497454c08 100644 --- a/ocean/osrs/osrs_types.h +++ b/ocean/osrs/osrs_types.h @@ -47,6 +47,16 @@ #define BLOOD_BLITZ_MAX_HIT 25 #define BLOOD_BARRAGE_MAX_HIT 29 +/** Percent of damage dealt that a blood spell heals the caster. */ +#define BLOOD_RUSH_HEAL_PERCENT 10 +#define BLOOD_BURST_HEAL_PERCENT 15 +#define BLOOD_BLITZ_HEAL_PERCENT 20 +#define BLOOD_BARRAGE_HEAL_PERCENT 25 + +/** Ticks between casts, and tile range, for a combat spell cast from a staff. */ +#define COMBAT_SPELL_CAST_SPEED 5 +#define COMBAT_SPELL_ATTACK_RANGE 10 + #define NUM_GEAR_SLOTS 11 #define NUM_ACTION_HEADS 9 @@ -704,32 +714,9 @@ typedef struct Log { float post_healer_set_pressure_normal_sum; float action_mask_checks_normal_sum; - float hist_score_bank[8]; - float hist_n_bank[8]; - - float colo_pray_faced_by_type[12]; - float colo_pray_correct_by_type[12]; - float colo_offpray_damage_by_type[12]; - float colo_total_damage_by_type[12]; - - float colo_death_by_type[12]; - float colo_death_fatal_damage; - - float colo_offpray_damage_conflict; - float colo_offpray_damage_solo; - float colo_death_on_conflict_tick; - - float colo_death_dmg_unprayable; - float colo_death_dmg_offpray; - float colo_death_dmg_prayed; - float colo_death_dmg_self; - float colo_death_heal_remaining; - float colo_farm_damage; - float colo_typeless_damage_by_type[12]; - float colo_outcome_score; - float colo_min_sol_hp; - - float colo_max_depth_reached; + float inf_death_wave_hist[6]; + float inf_death_brew_doses_hist[6]; + float inf_death_restore_doses_hist[6]; } Log; typedef struct { diff --git a/ocean/osrs/osrs_visual.c b/ocean/osrs/osrs_visual.c index 86b7b97512..76704cb821 100644 --- a/ocean/osrs/osrs_visual.c +++ b/ocean/osrs/osrs_visual.c @@ -26,6 +26,42 @@ #include "puffercpu.h" #include "osrs_visual_net.h" +_Static_assert(COLO_ENT_INF_OBS_FEATS == COLO_FEATURES_PER_NPC, + "entity encoder per-NPC OBSERVATION width must track the colosseum layout"); +_Static_assert( + COLO_ENT_INF_FEATS == + COLO_ENT_INF_TYPE_ONEHOT + (COLO_FEATURES_PER_NPC - COLO_NPC_TYPE_CODE_FEATURES), + "encoder record width is the obs record with the type code expanded to a one-hot"); +_Static_assert(COLO_ENT_INF_NUM_NPCS == COLO_OBS_NPCS, + "entity encoder NPC count must track the colosseum observation layout"); +_Static_assert(COLO_ENT_INF_NPC_START == COLO_OBS_AFTER_EQUIPPED_SELF, + "entity encoder NPC block offset must track the colosseum observation layout"); +_Static_assert(COLO_ENT_INF_TYPE_ONEHOT == COLO_NUM_NPC_TYPES, + "entity pool presence mask is the NPC type one-hot; a new NPC type must widen it"); +_Static_assert(COLO_ENT_INF_INV_START == COLO_OBS_AFTER_PLAYER, + "entity encoder inventory block offset must track the colosseum observation layout"); +_Static_assert(COLO_ENT_INF_INV_NUM_CELLS == COLO_INVENTORY_DISPLAY_SLOTS, + "entity encoder inventory cell count must track the colosseum observation layout"); +_Static_assert(COLO_ENT_INF_INV_OBS_FEATS == COLO_INVENTORY_CELL_OBS_FEATURES, + "entity encoder inventory cell OBSERVATION width must track the colosseum layout"); +_Static_assert(COLO_ENT_INF_INV_FEATS == COLO_INVENTORY_CELL_ENCODER_FEATURES, + "encoder inventory record is one item table row with the observed floats added in"); + +_Static_assert(INF_ENT_FEATS == INF_NPC_SLOT_FEATURES, + "entity encoder per-NPC width must track the inferno observation layout"); +_Static_assert(INF_ENT_NUM_NPCS == INF_OBS_NPCS, + "entity encoder NPC count must track the inferno observation layout"); +_Static_assert(INF_ENT_NPC_START == INF_OBS_AFTER_PILLARS, + "entity encoder NPC block offset must track the inferno observation layout"); +_Static_assert(INF_ENT_TYPE_ONEHOT == INF_NUM_NPC_TYPES, + "entity pool presence mask is the NPC type one-hot; a new NPC type must widen it"); +_Static_assert(INF_ENT_INV_START == INF_OBS_AFTER_SPARKS, + "entity encoder inventory block offset must track the inferno observation layout"); +_Static_assert(INF_ENT_INV_NUM_CELLS == OSRS_INVENTORY_SIZE, + "entity encoder inventory cell count must track the inferno observation layout"); +_Static_assert(INF_ENT_INV_FEATS == OSRS_INVENTORY_CELL_OBS_FEATURES, + "entity encoder inventory cell width must track the inferno observation layout"); + static void visual_require_gui_item_sprite(int raw_osrs_id, void* ctx) { gui_require_sprite_by_osrs_id((GuiState*)ctx, raw_osrs_id); } @@ -146,19 +182,34 @@ static double osrs_profile_now_seconds(void) { return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0; } +static uint64_t osrs_profile_hash_bytes(uint64_t hash, const void* data, size_t size) { + const uint8_t* bytes = (const uint8_t*)data; + for (size_t i = 0; i < size; i++) + hash = (hash ^ bytes[i]) * 1099511628211ULL; + return hash; +} + #ifdef COLO_PROFILE_ENABLED -static void osrs_print_colosseum_profile_results(void) { +static int osrs_colosseum_profile_slot_is_counter(int slot) { + return slot == COLO_PROF_BEST_GEAR_REQUESTS || + slot == COLO_PROF_BEST_GEAR_HITS || + slot == COLO_PROF_BEST_GEAR_BUILDS; +} + +static void osrs_print_colosseum_profile_results(int total_steps) { int count = colosseum_env_profile_count(); if (count <= 0) return; double values[COLO_PROF_COUNT]; int order[COLO_PROF_COUNT]; + int order_count = 0; for (int i = 0; i < count; i++) { values[i] = colosseum_env_profile_read_reset_ms(i); - order[i] = i; + if (!osrs_colosseum_profile_slot_is_counter(i)) + order[order_count++] = i; } - for (int i = 0; i < count; i++) { + for (int i = 0; i < order_count; i++) { int best = i; - for (int j = i + 1; j < count; j++) { + for (int j = i + 1; j < order_count; j++) { if (values[order[j]] > values[order[best]]) best = j; } int tmp = order[i]; @@ -167,12 +218,25 @@ static void osrs_print_colosseum_profile_results(void) { } double total = values[COLO_PROF_C_STEP_TOTAL]; printf("Colosseum profile buckets:\n"); - for (int r = 0; r < count; r++) { + for (int r = 0; r < order_count; r++) { int slot = order[r]; double pct = total > 0.0 ? 100.0 * values[slot] / total : 0.0; printf(" %-28s %.3f ms %.2f%%\n", colosseum_env_profile_name(slot), values[slot], pct); } + double steps = total_steps > 0 ? (double)total_steps : 1.0; + double best_gear_requests = values[COLO_PROF_BEST_GEAR_REQUESTS]; + double best_gear_hits = values[COLO_PROF_BEST_GEAR_HITS]; + printf("Colosseum cache counters:\n"); + printf(" %-28s %.0f total %.6f per step %.2f%% hit\n", + "best_gear", + best_gear_requests, + best_gear_requests / steps, + best_gear_requests > 0.0 ? 100.0 * best_gear_hits / best_gear_requests : 0.0); + printf(" %-28s %.0f total %.6f per step\n", + colosseum_env_profile_name(COLO_PROF_BEST_GEAR_BUILDS), + values[COLO_PROF_BEST_GEAR_BUILDS], + values[COLO_PROF_BEST_GEAR_BUILDS] / steps); } #endif @@ -480,7 +544,7 @@ static void run_profile( printf(" Steps/sec: %.0f\n", total_steps / elapsed); #ifdef COLO_PROFILE_ENABLED if (encounter_name && strcmp(encounter_name, "colosseum") == 0) - osrs_print_colosseum_profile_results(); + osrs_print_colosseum_profile_results(total_steps); #endif #ifdef INF_PROFILE_ENABLED if (encounter_name && strcmp(encounter_name, "inferno") == 0) @@ -640,10 +704,37 @@ static int visual_policy_is_continuous( return 1; } -#define VISUAL_POLICY_ENTITY_FEATS 37 -#define VISUAL_POLICY_ENTITY_BOTTLENECK 16 -#define VISUAL_POLICY_INV_FEATS 28 -#define VISUAL_POLICY_INV_BOTTLENECK 16 +typedef enum { + VISUAL_ENTITY_ARCH_NONE = 0, + VISUAL_ENTITY_ARCH_COLOSSEUM = 1, + VISUAL_ENTITY_ARCH_INFERNO = 2, +} VisualEntityArch; + +static VisualEntityArch visual_policy_entity_arch(const EncounterDef* edef) { + if (strcmp(edef->name, "colosseum") == 0) return VISUAL_ENTITY_ARCH_COLOSSEUM; + if (strcmp(edef->name, "inferno") == 0) return VISUAL_ENTITY_ARCH_INFERNO; + return VISUAL_ENTITY_ARCH_NONE; +} + +static int visual_policy_entity_feats(VisualEntityArch entity_arch) { + switch (entity_arch) { + case VISUAL_ENTITY_ARCH_COLOSSEUM: return COLO_ENT_INF_FEATS; + case VISUAL_ENTITY_ARCH_INFERNO: return INF_ENT_FEATS; + case VISUAL_ENTITY_ARCH_NONE: break; + } + fprintf(stderr, "policy: entity feats requested without an entity arch\n"); + abort(); +} + +static int visual_policy_inv_feats(VisualEntityArch entity_arch) { + switch (entity_arch) { + case VISUAL_ENTITY_ARCH_COLOSSEUM: return COLO_ENT_INF_INV_FEATS; + case VISUAL_ENTITY_ARCH_INFERNO: return INF_ENT_INV_FEATS; + case VISUAL_ENTITY_ARCH_NONE: break; + } + fprintf(stderr, "policy: inv feats requested without an entity arch\n"); + abort(); +} static int64_t visual_policy_expected_weight_count( int input_size, @@ -652,7 +743,8 @@ static int64_t visual_policy_expected_weight_count( const int* action_dims, int num_action_heads, int decoder_value_heads, - int entity_encoder + int entity_encoder, + VisualEntityArch entity_arch ) { int action_sum = 0; for (int h = 0; h < num_action_heads; h++) { @@ -662,12 +754,12 @@ static int64_t visual_policy_expected_weight_count( int64_t total = 0; total += (int64_t)hidden_size * input_size; if (entity_encoder >= 1) { - total += (int64_t)VISUAL_POLICY_ENTITY_BOTTLENECK * VISUAL_POLICY_ENTITY_FEATS; - total += (int64_t)hidden_size * VISUAL_POLICY_ENTITY_BOTTLENECK; + total += (int64_t)COLO_ENT_INF_BOTTLENECK * visual_policy_entity_feats(entity_arch); + total += (int64_t)hidden_size * COLO_ENT_INF_BOTTLENECK; } if (entity_encoder >= 2) { - total += (int64_t)VISUAL_POLICY_INV_BOTTLENECK * VISUAL_POLICY_INV_FEATS; - total += (int64_t)hidden_size * VISUAL_POLICY_INV_BOTTLENECK; + total += (int64_t)COLO_ENT_INF_INV_BOTTLENECK * visual_policy_inv_feats(entity_arch); + total += (int64_t)hidden_size * COLO_ENT_INF_INV_BOTTLENECK; } total += (int64_t)(action_sum + decoder_value_heads) * hidden_size; if (visual_policy_is_continuous(action_dims, num_action_heads)) { @@ -692,6 +784,8 @@ static VisualPolicyModelShape visual_policy_select_model_shape( int obs_input_size = policy->obs_size; int full_input_size = policy->obs_size + policy->mask_size; int64_t file_weights = visual_policy_file_weight_count(policy->weights); + VisualEntityArch entity_arch = visual_policy_entity_arch(edef); + int enc_max = entity_arch == VISUAL_ENTITY_ARCH_NONE ? 0 : 2; VisualPolicyModelShape match = {0}; int matches = 0; @@ -700,14 +794,14 @@ static VisualPolicyModelShape visual_policy_select_model_shape( if (cli_hidden_size > 0 && hs != cli_hidden_size) continue; for (int layers = 1; layers <= 8; layers++) { if (cli_num_layers > 0 && layers != cli_num_layers) continue; - for (int enc = 0; enc <= 2; enc++) { + for (int enc = 0; enc <= enc_max; enc++) { if (cli_entity_encoder > 0 && enc != cli_entity_encoder) continue; for (int value_heads = 0; value_heads <= 1; value_heads++) { for (int variant = 0; variant <= 1; variant++) { int input_size = variant ? full_input_size : obs_input_size; int64_t expected = visual_policy_expected_weight_count( input_size, hs, layers, policy->action_dims, - policy->num_action_heads, value_heads, enc); + policy->num_action_heads, value_heads, enc, entity_arch); if (expected != file_weights) continue; match = (VisualPolicyModelShape){ input_size, value_heads, enc, hs, layers}; @@ -727,8 +821,9 @@ static VisualPolicyModelShape visual_policy_select_model_shape( if (matches == 0) { fprintf(stderr, "policy: %s model shape mismatch: file=%lld floats matches no architecture" - " (obs=%d mask=%d, scanned hs 128..4096, layers 1..8, entity 0..2%s)\n", + " (obs=%d mask=%d, scanned hs 128..4096, layers 1..8, entity 0..%d%s)\n", edef->name, (long long)file_weights, policy->obs_size, policy->mask_size, + enc_max, (cli_hidden_size > 0 || cli_num_layers > 0 || cli_entity_encoder > 0) ? " within the given CLI constraints" : ""); abort(); @@ -769,7 +864,8 @@ static VisualNet* visual_policy_make_puffernet( int action_dims[], int num_action_heads, int decoder_value_heads, - int entity_encoder + int entity_encoder, + VisualEntityArch entity_arch ) { VisualNet* net = (VisualNet*)calloc(1, sizeof(VisualNet)); if (!net) { @@ -802,12 +898,12 @@ static VisualNet* visual_policy_make_puffernet( if (entity_encoder) { off = visual_policy_layout_tensor("enc.global_w", off, (int64_t)hidden_dim * input_dim); off = visual_policy_layout_tensor("enc.entity_l1_w", off, - (int64_t)COLO_ENT_INF_BOTTLENECK * COLO_ENT_INF_FEATS); + (int64_t)COLO_ENT_INF_BOTTLENECK * visual_policy_entity_feats(entity_arch)); off = visual_policy_layout_tensor("enc.entity_l2_w", off, (int64_t)hidden_dim * COLO_ENT_INF_BOTTLENECK); if (entity_encoder >= 2) { off = visual_policy_layout_tensor("enc.inv_l1_w", off, - (int64_t)COLO_ENT_INF_INV_BOTTLENECK * COLO_ENT_INF_INV_FEATS); + (int64_t)COLO_ENT_INF_INV_BOTTLENECK * visual_policy_inv_feats(entity_arch)); off = visual_policy_layout_tensor("enc.inv_l2_w", off, (int64_t)hidden_dim * COLO_ENT_INF_INV_BOTTLENECK); } @@ -830,8 +926,19 @@ static VisualNet* visual_policy_make_puffernet( int64_t off_total = off; if (entity_encoder) { - net->entity_encoder = make_colosseum_entity_encoder( - weights, 1, input_dim, hidden_dim, entity_encoder); + switch (entity_arch) { + case VISUAL_ENTITY_ARCH_COLOSSEUM: + net->entity_encoder = make_colosseum_entity_encoder( + weights, 1, input_dim, hidden_dim, entity_encoder); + break; + case VISUAL_ENTITY_ARCH_INFERNO: + net->entity_encoder = make_inferno_entity_encoder( + weights, 1, input_dim, hidden_dim, entity_encoder); + break; + case VISUAL_ENTITY_ARCH_NONE: + fprintf(stderr, "policy: entity encoder resolved without an entity arch\n"); + abort(); + } } else { net->encoder = make_linear(weights, 1, input_dim, hidden_dim); } @@ -874,7 +981,6 @@ static float g_cli_camera_dist = -1.0f; static float g_cli_camera_yaw = -1000.0f; static float g_cli_camera_pitch = -1000.0f; static int g_cli_visual_loadout_mode = -1; -static int g_cli_prayer_oracle = 0; static void visual_policy_init( VisualPolicy* policy, const EncounterDef* edef, @@ -930,7 +1036,8 @@ static void visual_policy_init( policy->action_dims, policy->num_action_heads, model_shape.decoder_value_heads, - model_shape.entity_encoder); + model_shape.entity_encoder, + visual_policy_entity_arch(edef)); int64_t file_weights = visual_policy_file_weight_count(policy->weights); if (policy->weights->idx != file_weights) { fprintf(stderr, @@ -1007,19 +1114,11 @@ static int visual_policy_sample_masked( return best_action; } -static void visual_policy_actions( - VisualPolicy* policy, - const EncounterDef* edef, - EncounterState* state, - EncounterContext* context, - int* actions -) { +static void visual_policy_actions_from_obs(VisualPolicy* policy, int* actions) { if (!policy || !policy->enabled) return; - edef->write_obs(state, context, policy->obs); - edef->write_mask(state, context, policy->obs + policy->obs_size); float* encoded; if (policy->net->entity_encoder) { - colosseum_entity_encoder(policy->net->entity_encoder, policy->obs); + entity_encoder_forward(policy->net->entity_encoder, policy->obs); encoded = policy->net->entity_encoder->output; } else { linear(policy->net->encoder, policy->obs); @@ -1048,6 +1147,152 @@ static void visual_policy_actions( } } +static void visual_policy_actions( + VisualPolicy* policy, + const EncounterDef* edef, + EncounterState* state, + EncounterContext* context, + int* actions +) { + if (!policy || !policy->enabled) return; + edef->write_obs(state, context, policy->obs); + edef->write_mask(state, context, policy->obs + policy->obs_size); + visual_policy_actions_from_obs(policy, actions); +} + +static void run_policy_profile( + OsrsEnv* env, + const char* encounter_name, + int start_wave, + int profile_steps, + const char* model_path, + VisualPolicyMode policy_mode, + uint32_t policy_seed, + int loadout_mode +) { + if (!encounter_name || strcmp(encounter_name, "colosseum") != 0) abort(); + if (profile_steps <= 0) { + fprintf(stderr, "policy profile requires --profile-steps > 0\n"); + abort(); + } + const EncounterDef* edef = visual_open_encounter(env, encounter_name); + if (!edef) abort(); + visual_load_encounter_collision_map(edef, env, encounter_name); + if (start_wave >= 0) + edef->put_int(env->encounter_state, env->encounter_context, + "start_wave", start_wave); + edef->put_int(env->encounter_state, env->encounter_context, + "loadout_profile_mode", loadout_mode); + edef->reset(env->encounter_state, env->encounter_context, policy_seed); + + VisualPolicy policy; + visual_policy_init(&policy, edef, model_path, policy_mode, policy_seed, + g_cli_hidden_size, g_cli_num_layers, g_cli_entity_encoder); + if (!policy.enabled) abort(); + +#ifdef COLO_PROFILE_ENABLED + int profile_count = colosseum_env_profile_count(); + for (int i = 0; i < profile_count; i++) + (void)colosseum_env_profile_read_reset_ms(i); +#endif + + int actions[VISUAL_POLICY_MAX_ACTION_HEADS] = {0}; + int total_steps = 0; + int reset_count = 0; + double environment_ms = 0.0; + uint64_t trace_hash = 1469598103934665603ULL; + double wall_start = osrs_profile_now_seconds(); + while (total_steps < profile_steps) { + double environment_step_ms = 0.0; + double start_ms = osrs_profile_now_seconds() * 1000.0; + edef->write_obs(env->encounter_state, env->encounter_context, policy.obs); + double end_ms = osrs_profile_now_seconds() * 1000.0; + environment_step_ms += end_ms - start_ms; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_ADD(COLO_PROF_C_WRITE_OBS, end_ms - start_ms); +#endif + + start_ms = end_ms; + edef->write_mask(env->encounter_state, env->encounter_context, + policy.obs + policy.obs_size); + end_ms = osrs_profile_now_seconds() * 1000.0; + environment_step_ms += end_ms - start_ms; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_ADD(COLO_PROF_C_WRITE_MASK, end_ms - start_ms); +#endif + trace_hash = osrs_profile_hash_bytes( + trace_hash, + policy.obs, + (size_t)(policy.obs_size + policy.mask_size) * sizeof(float)); + + visual_policy_actions_from_obs(&policy, actions); + trace_hash = osrs_profile_hash_bytes( + trace_hash, + actions, + (size_t)policy.num_action_heads * sizeof(int)); + + start_ms = osrs_profile_now_seconds() * 1000.0; + edef->step(env->encounter_state, env->encounter_context, actions); + end_ms = osrs_profile_now_seconds() * 1000.0; + environment_step_ms += end_ms - start_ms; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_ADD(COLO_PROF_C_ENCOUNTER_STEP, end_ms - start_ms); +#endif + + start_ms = end_ms; + float reward = edef->get_reward(env->encounter_state, env->encounter_context); + int terminal = edef->is_terminal( + env->encounter_state, env->encounter_context); + end_ms = osrs_profile_now_seconds() * 1000.0; + environment_step_ms += end_ms - start_ms; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_ADD(COLO_PROF_C_REWARD_TERMINAL, end_ms - start_ms); +#endif + trace_hash = osrs_profile_hash_bytes(trace_hash, &reward, sizeof(reward)); + trace_hash = osrs_profile_hash_bytes(trace_hash, &terminal, sizeof(terminal)); + + if (terminal) { + start_ms = end_ms; + reset_count++; + edef->reset(env->encounter_state, env->encounter_context, + policy_seed + (uint32_t)reset_count); + visual_policy_reset_recurrent(&policy); + end_ms = osrs_profile_now_seconds() * 1000.0; + environment_step_ms += end_ms - start_ms; +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_ADD(COLO_PROF_C_RESET, end_ms - start_ms); +#endif + } + +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_ADD(COLO_PROF_C_STEP_TOTAL, environment_step_ms); +#endif + environment_ms += environment_step_ms; + total_steps++; + } + double wall_elapsed = osrs_profile_now_seconds() - wall_start; + + printf("Policy profile results:\n"); + printf(" Total steps: %d\n", total_steps); + printf(" Resets: %d\n", reset_count); + printf(" Wall time: %.3f seconds\n", wall_elapsed); + printf(" Environment time: %.3f seconds\n", environment_ms / 1000.0); + printf(" Environment steps/sec: %.0f\n", + environment_ms > 0.0 ? 1000.0 * total_steps / environment_ms : 0.0); + printf(" Wall steps/sec: %.0f\n", + wall_elapsed > 0.0 ? total_steps / wall_elapsed : 0.0); + printf(" Trace hash: %016llx\n", (unsigned long long)trace_hash); +#ifdef COLO_PROFILE_ENABLED + osrs_print_colosseum_profile_results(total_steps); +#endif + + visual_policy_destroy(&policy); + edef->destroy(env->encounter_state); + env->encounter_state = NULL; + visual_destroy_encounter_context( + edef, (EncounterContext**)&env->encounter_context); +} + typedef struct { VisualPolicy* policy; const EncounterDef* edef; @@ -1374,6 +1619,10 @@ static void run_metrics( static uint64_t npc_eff_n[COLO_NUM_NPC_TYPES]; static uint64_t wave_ticks[12], wave_visits[12], wave_reinforced[12]; static uint64_t wave_attacks_post_reinforce[12]; + double sol_damage_by_source[COLO_NUM_SOL_DAMAGE_SOURCES] = {0}; + double javelin_damage_by_source[COLO_NUM_JAVELIN_DAMAGE_SOURCES] = {0}; + double death_by_source[COLO_NUM_DAMAGE_SOURCES] = {0}; + double doom_death_by_source[COLO_NUM_DAMAGE_SOURCES] = {0}; memset(wpn_npc, 0, sizeof(wpn_npc)); memset(wpn_total, 0, sizeof(wpn_total)); memset(wpn_spec, 0, sizeof(wpn_spec)); @@ -1388,6 +1637,7 @@ static void run_metrics( uint64_t total_attacks = 0, argmax_set_attacks = 0, argmax_evals = 0; long total_ticks = 0; int episodes = 0; + int sol_episodes = 0; int prev_wave = 0, prev_reinf_timer = 0, wave_seen = 0; int enc_actions[64] = {0}; @@ -1464,6 +1714,21 @@ static void run_metrics( (EncounterContext*)env->encounter_context)) { ep_scores[episodes] = cs->log.outcome_score; ep_winners[episodes] = cs->winner; + if (cs->sol.started) sol_episodes++; + for (int source = 0; source < COLO_NUM_SOL_DAMAGE_SOURCES; source++) + sol_damage_by_source[source] += + (double)cs->log.sol_damage_by_source[source]; + for (int source = 0; + source < COLO_NUM_JAVELIN_DAMAGE_SOURCES; + source++) + javelin_damage_by_source[source] += + (double)cs->log.javelin_damage_by_source[source]; + for (int source = 0; source < COLO_NUM_DAMAGE_SOURCES; source++) { + death_by_source[source] += + (double)cs->log.death_by_source[source]; + doom_death_by_source[source] += + (double)cs->log.doom_death_by_source[source]; + } episodes++; edef->reset(env->encounter_state, (EncounterContext*)env->encounter_context, @@ -1478,6 +1743,7 @@ static void run_metrics( printf("# episodes=%d ticks=%ld total_attacks=%llu mode=%s bis_oracle=%d\n", num_episodes, total_ticks, (unsigned long long)total_attacks, policy_mode == VISUAL_POLICY_ARGMAX ? "argmax" : "sample", bis_oracle); + printf("# sol_episodes=%d\n", sol_episodes); { double score_sum = 0.0; int wins = 0; @@ -1522,6 +1788,84 @@ static void run_metrics( 100.0 * (double)wave_reinforced[wv] / (double)wave_visits[wv], (unsigned long long)wave_attacks_post_reinforce[wv]); } + static const char* const sol_damage_source_names[COLO_NUM_SOL_DAMAGE_SOURCES] = { + "spear_1", + "spear_2", + "shield_1", + "shield_2", + "triple_parry", + "grapple", + "crystal_laser", + "molten_sand", + }; + double sol_damage_total = 0.0; + for (int source = 0; source < COLO_NUM_SOL_DAMAGE_SOURCES; source++) + sol_damage_total += sol_damage_by_source[source]; + printf("\nsol_damage_source,total,per_episode,per_sol_episode,pct\n"); + for (int source = 0; source < COLO_NUM_SOL_DAMAGE_SOURCES; source++) { + double damage = sol_damage_by_source[source]; + printf("%s,%.0f,%.3f,%.3f,%.1f%%\n", + sol_damage_source_names[source], + damage, + damage / (double)num_episodes, + sol_episodes ? damage / (double)sol_episodes : 0.0, + sol_damage_total > 0.0 ? 100.0 * damage / sol_damage_total : 0.0); + } + static const char* const javelin_damage_source_names[ + COLO_NUM_JAVELIN_DAMAGE_SOURCES + ] = { + "basic_ranged", + "skyfall", + "reentry_pool", + "reentry_volatility_pool", + }; + double javelin_damage_total = 0.0; + for (int source = 0; + source < COLO_NUM_JAVELIN_DAMAGE_SOURCES; + source++) + javelin_damage_total += javelin_damage_by_source[source]; + printf("\njavelin_damage_source,total,per_episode,pct\n"); + for (int source = 0; + source < COLO_NUM_JAVELIN_DAMAGE_SOURCES; + source++) { + double damage = javelin_damage_by_source[source]; + printf("%s,%.0f,%.3f,%.1f%%\n", + javelin_damage_source_names[source], + damage, + damage / (double)num_episodes, + javelin_damage_total > 0.0 + ? 100.0 * damage / javelin_damage_total + : 0.0); + } + static const char* const damage_source_names[COLO_NUM_DAMAGE_SOURCES] = { + "npc_attack", + "javelin_basic_ranged", + "manticore_venom", + "bee_poison", + "bee_contact", + "javelin_skyfall", + "reentry_pool", + "volatility_explosion", + "volatility_pool", + "reentry_volatility_pool", + "solarflare", + "self", + "sol_spear_1", + "sol_spear_2", + "sol_shield_1", + "sol_shield_2", + "sol_triple_parry", + "sol_grapple", + "sol_crystal_laser", + "sol_molten_sand", + }; + printf("\ndamage_source,deaths,doom_deaths\n"); + for (int source = 0; source < COLO_NUM_DAMAGE_SOURCES; source++) { + printf("%s,%.0f,%.0f\n", + damage_source_names[source], + death_by_source[source], + doom_death_by_source[source]); + } printf("\nweapon\\npc"); for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) printf(",%s", npc_names[t]); printf("\n"); @@ -1569,11 +1913,6 @@ static void run_visual( edef->put_int(env->encounter_state, env->encounter_context, "loadout_profile_mode", g_cli_visual_loadout_mode); } - if (strcmp(encounter_name, "colosseum") == 0 && edef->put_int && - g_cli_prayer_oracle) { - edef->put_int(env->encounter_state, env->encounter_context, - "prayer_oracle_mode", 1); - } VisualCollisionLoad cload = visual_load_encounter_collision_map(edef, env, encounter_name); if (cload.cmap) { @@ -1943,8 +2282,6 @@ int main(int argc, char** argv) { loadout_mode = atoi(argv[++i]); g_cli_visual_loadout_mode = loadout_mode; } - else if (strcmp(argv[i], "--prayer-oracle") == 0) - g_cli_prayer_oracle = 1; } #ifdef __EMSCRIPTEN__ @@ -1977,7 +2314,12 @@ int main(int argc, char** argv) { if (use_profile) { visual_alloc_env_buffers(&env); - run_profile(&env, encounter_name, start_wave, profile_steps); + if (model_path && model_path[0]) { + run_policy_profile(&env, encounter_name, start_wave, profile_steps, + model_path, policy_mode, policy_seed, loadout_mode); + } else { + run_profile(&env, encounter_name, start_wave, profile_steps); + } visual_free_env_buffers(&env); pvp_close(&env); diff --git a/ocean/osrs/osrs_visual_net.h b/ocean/osrs/osrs_visual_net.h index 3e6487bce6..ee863105e5 100644 --- a/ocean/osrs/osrs_visual_net.h +++ b/ocean/osrs/osrs_visual_net.h @@ -1,68 +1,191 @@ #pragma once -#define COLO_ENT_INF_NPC_START 1030 +#include "osrs_colosseum_item_obs_generated.h" + +/* 5c puffercpu.h dropped _gelu. Keep a local twin so the viewer encoder stays + bit-compatible with the CUDA GELU without carrying a core patch. */ +static inline void osrs_visual_gelu(float* input, float* output, int size) { + for (int i = 0; i < size; i++) { + float x = input[i]; + output[i] = 0.5f * x * (1.0f + tanhf(0.7978845608028654f * (x + 0.044715f * x * x * x))); + } +} + +#define COLO_ENT_INF_NPC_START 130 #define COLO_ENT_INF_NUM_NPCS 24 -#define COLO_ENT_INF_FEATS 37 +#define COLO_ENT_INF_FEATS 34 +/* obs carries a type CODE per slot; the encoder expands it back to the one-hot */ +#define COLO_ENT_INF_OBS_FEATS 23 #define COLO_ENT_INF_TYPE_ONEHOT 12 #define COLO_ENT_INF_BOTTLENECK 16 -#define COLO_ENT_INF_INV_START 48 +#define COLO_ENT_INF_INV_START 36 #define COLO_ENT_INF_INV_NUM_CELLS 28 -#define COLO_ENT_INF_INV_FEATS 28 -#define COLO_ENT_INF_INV_PRESENT 0 +#define COLO_ENT_INF_INV_FEATS 15 +/* obs carries an item CODE plus is_equipped and hp_heal; the encoder rebuilds the rest */ +#define COLO_ENT_INF_INV_OBS_FEATS 3 #define COLO_ENT_INF_INV_BOTTLENECK 16 -typedef struct ColosseumEntityEncoder ColosseumEntityEncoder; -struct ColosseumEntityEncoder { - float* output; - float* global_w; - float* entity_l1_w; - float* entity_l2_w; +/* Host twin of COLO_ITEM_OBS_TABLE_DEV in src/ocean.cu. Separate names on purpose: both + land in one translation unit when the viewer is built, and a single guarded definition + would give the CUDA kernel a host array. */ +static const float COLO_ITEM_OBS_TABLE + [COLO_ITEM_OBS_TABLE_ROWS][COLO_ITEM_OBS_TABLE_COLS] = { +#include "osrs_colosseum_item_obs_table.inc" +}; + +#define INF_ENT_NPC_START 90 +#define INF_ENT_NUM_NPCS 37 +#define INF_ENT_FEATS 48 +#define INF_ENT_TYPE_ONEHOT 14 +#define INF_ENT_INV_START 2450 +#define INF_ENT_INV_NUM_CELLS 28 +#define INF_ENT_INV_FEATS 28 + +/* How the observation record becomes the encoder record. Every branch materialises the + expanded record first and then runs one dense dot, which is what the CUDA gathers do. */ +typedef enum { + ENTITY_RECORD_VERBATIM = 0, + ENTITY_RECORD_TYPE_ONEHOT, + ENTITY_RECORD_ITEM_TABLE, +} EntityRecordExpansion; + +#define ENTITY_RECORD_MAX_FEATS 64 + +typedef struct EntityPoolBranch EntityPoolBranch; +struct EntityPoolBranch { + int start; + int num_recs; + int feats; + int obs_feats; + int type_onehot; + int bottleneck; + int mask_prefix; + EntityRecordExpansion expansion; + float* l1_w; + float* l2_w; float* z1; float* h1; - float* entity_e; + float* e; +}; + +typedef struct EntityEncoder EntityEncoder; +struct EntityEncoder { + float* output; + float* global_w; int batch_size; int input_dim; int hidden_dim; - int mode; - float* inv_l1_w; - float* inv_l2_w; - float* inv_z1; - float* inv_h1; - float* inv_e; + int num_branches; + EntityPoolBranch branches[2]; }; -ColosseumEntityEncoder* make_colosseum_entity_encoder( - Weights* weights, int batch_size, int input_dim, int hidden_dim, int mode) { +static void entity_pool_branch_init( + EntityPoolBranch* branch, Weights* weights, int hidden_dim, + int start, int num_recs, int feats, int obs_feats, int type_onehot, + int bottleneck, int mask_prefix, EntityRecordExpansion expansion) { + if (feats > ENTITY_RECORD_MAX_FEATS) { + fprintf(stderr, "entity pool branch: %d features exceeds the expansion scratch\n", + feats); + abort(); + } + branch->start = start; + branch->num_recs = num_recs; + branch->feats = feats; + branch->obs_feats = obs_feats; + branch->type_onehot = type_onehot; + branch->bottleneck = bottleneck; + branch->mask_prefix = mask_prefix; + branch->expansion = expansion; + branch->l1_w = get_weights_aligned(weights, bottleneck * feats); + branch->l2_w = get_weights_aligned(weights, hidden_dim * bottleneck); + branch->z1 = (float*)calloc((size_t)num_recs * bottleneck, sizeof(float)); + branch->h1 = (float*)calloc((size_t)num_recs * bottleneck, sizeof(float)); + branch->e = (float*)calloc((size_t)num_recs * hidden_dim, sizeof(float)); +} + +/* Weight reads are sequenced statements in reg_params order (src/ocean.cu): + global_w, then entity_l1_w/entity_l2_w, then inv_l1_w/inv_l2_w. + get_weights_aligned advances a shared cursor, so call order IS the .bin layout. */ +static EntityEncoder* make_entity_encoder_global( + Weights* weights, int batch_size, int input_dim, int hidden_dim) { size_t out_size = (size_t)batch_size * hidden_dim * sizeof(float); - ColosseumEntityEncoder* layer = - (ColosseumEntityEncoder*)calloc(1, sizeof(ColosseumEntityEncoder) + out_size); - *layer = (ColosseumEntityEncoder){ - .output = (float*)(layer + 1), - .global_w = get_weights_aligned(weights, hidden_dim * input_dim), - .entity_l1_w = get_weights_aligned(weights, COLO_ENT_INF_BOTTLENECK * COLO_ENT_INF_FEATS), - .entity_l2_w = get_weights_aligned(weights, hidden_dim * COLO_ENT_INF_BOTTLENECK), - .z1 = (float*)calloc((size_t)COLO_ENT_INF_NUM_NPCS * COLO_ENT_INF_BOTTLENECK, sizeof(float)), - .h1 = (float*)calloc((size_t)COLO_ENT_INF_NUM_NPCS * COLO_ENT_INF_BOTTLENECK, sizeof(float)), - .entity_e = (float*)calloc((size_t)COLO_ENT_INF_NUM_NPCS * hidden_dim, sizeof(float)), - .batch_size = batch_size, - .input_dim = input_dim, - .hidden_dim = hidden_dim, - .mode = mode, - }; - // mode 2: the inventory-pool weights follow entity_l2 in the .bin (reg_params order). - // Read them as sequenced statements AFTER the initializer above (C does not specify the - // initializer's internal evaluation order, so the global/entity reads must complete first). + EntityEncoder* layer = (EntityEncoder*)calloc(1, sizeof(EntityEncoder) + out_size); + layer->output = (float*)(layer + 1); + layer->global_w = get_weights_aligned(weights, hidden_dim * input_dim); + layer->batch_size = batch_size; + layer->input_dim = input_dim; + layer->hidden_dim = hidden_dim; + return layer; +} + +EntityEncoder* make_colosseum_entity_encoder( + Weights* weights, int batch_size, int input_dim, int hidden_dim, int mode) { + EntityEncoder* layer = make_entity_encoder_global( + weights, batch_size, input_dim, hidden_dim); + entity_pool_branch_init(&layer->branches[0], weights, hidden_dim, + COLO_ENT_INF_NPC_START, COLO_ENT_INF_NUM_NPCS, + COLO_ENT_INF_FEATS, COLO_ENT_INF_OBS_FEATS, COLO_ENT_INF_TYPE_ONEHOT, + COLO_ENT_INF_BOTTLENECK, COLO_ENT_INF_TYPE_ONEHOT, + ENTITY_RECORD_TYPE_ONEHOT); + layer->num_branches = 1; if (mode >= 2) { - layer->inv_l1_w = get_weights_aligned(weights, COLO_ENT_INF_INV_BOTTLENECK * COLO_ENT_INF_INV_FEATS); - layer->inv_l2_w = get_weights_aligned(weights, hidden_dim * COLO_ENT_INF_INV_BOTTLENECK); - layer->inv_z1 = (float*)calloc((size_t)COLO_ENT_INF_INV_NUM_CELLS * COLO_ENT_INF_INV_BOTTLENECK, sizeof(float)); - layer->inv_h1 = (float*)calloc((size_t)COLO_ENT_INF_INV_NUM_CELLS * COLO_ENT_INF_INV_BOTTLENECK, sizeof(float)); - layer->inv_e = (float*)calloc((size_t)COLO_ENT_INF_INV_NUM_CELLS * hidden_dim, sizeof(float)); + entity_pool_branch_init(&layer->branches[1], weights, hidden_dim, + COLO_ENT_INF_INV_START, COLO_ENT_INF_INV_NUM_CELLS, + COLO_ENT_INF_INV_FEATS, COLO_ENT_INF_INV_OBS_FEATS, 0, + COLO_ENT_INF_INV_BOTTLENECK, 1, ENTITY_RECORD_ITEM_TABLE); + layer->num_branches = 2; } return layer; } -void colosseum_entity_encoder(ColosseumEntityEncoder* layer, float* observations) { +EntityEncoder* make_inferno_entity_encoder( + Weights* weights, int batch_size, int input_dim, int hidden_dim, int mode) { + EntityEncoder* layer = make_entity_encoder_global( + weights, batch_size, input_dim, hidden_dim); + entity_pool_branch_init(&layer->branches[0], weights, hidden_dim, + INF_ENT_NPC_START, INF_ENT_NUM_NPCS, + INF_ENT_FEATS, INF_ENT_FEATS, 0, + COLO_ENT_INF_BOTTLENECK, INF_ENT_TYPE_ONEHOT, ENTITY_RECORD_VERBATIM); + layer->num_branches = 1; + if (mode >= 2) { + entity_pool_branch_init(&layer->branches[1], weights, hidden_dim, + INF_ENT_INV_START, INF_ENT_INV_NUM_CELLS, + INF_ENT_INV_FEATS, INF_ENT_INV_FEATS, 0, + COLO_ENT_INF_INV_BOTTLENECK, 1, ENTITY_RECORD_VERBATIM); + layer->num_branches = 2; + } + return layer; +} + +/* Mirrors colo_ent_gather_npcs and colo_ent_gather_inv in src/ocean.cu. */ +static void entity_expand_record(const EntityPoolBranch* p, const float* rec, float* out) { + switch (p->expansion) { + case ENTITY_RECORD_VERBATIM: + for (int i = 0; i < p->feats; i++) out[i] = rec[i]; + return; + case ENTITY_RECORD_TYPE_ONEHOT: { + int code = (int)lrintf(rec[0]); + for (int i = 0; i < p->type_onehot; i++) + out[i] = (code == i + 1) ? 1.0f : 0.0f; + for (int i = 0; i < p->feats - p->type_onehot; i++) + out[p->type_onehot + i] = rec[1 + i]; + return; + } + case ENTITY_RECORD_ITEM_TABLE: { + int code = (int)lrintf(rec[0] * (float)COLO_ITEM_OBS_CODE_SCALE); + if (code < 0 || code >= COLO_ITEM_OBS_TABLE_ROWS) { + fprintf(stderr, "entity pool branch: item code %d out of table\n", code); + abort(); + } + for (int i = 0; i < p->feats; i++) out[i] = COLO_ITEM_OBS_TABLE[code][i]; + out[COLO_ITEM_OBS_OVERLAY_EQUIPPED] += rec[1]; + out[COLO_ITEM_OBS_OVERLAY_HP_HEAL] += rec[2]; + return; + } + } +} + +void entity_encoder_forward(EntityEncoder* layer, float* observations) { int H = layer->hidden_dim; int IN = layer->input_dim; for (int b = 0; b < layer->batch_size; b++) { @@ -75,73 +198,45 @@ void colosseum_entity_encoder(ColosseumEntityEncoder* layer, float* observations out[o] = sum; } - float* npcs = obs + COLO_ENT_INF_NPC_START; - for (int n = 0; n < COLO_ENT_INF_NUM_NPCS; n++) { - float* rec = npcs + n * COLO_ENT_INF_FEATS; - float* z1n = layer->z1 + n * COLO_ENT_INF_BOTTLENECK; - for (int k = 0; k < COLO_ENT_INF_BOTTLENECK; k++) { - float sum = 0.0f; - for (int i = 0; i < COLO_ENT_INF_FEATS; i++) - sum += rec[i] * layer->entity_l1_w[k * COLO_ENT_INF_FEATS + i]; - z1n[k] = sum; - } - } - _gelu(layer->z1, layer->h1, COLO_ENT_INF_NUM_NPCS * COLO_ENT_INF_BOTTLENECK); - for (int n = 0; n < COLO_ENT_INF_NUM_NPCS; n++) { - float* h1n = layer->h1 + n * COLO_ENT_INF_BOTTLENECK; - float* en = layer->entity_e + (size_t)n * H; - for (int o = 0; o < H; o++) { - float sum = 0.0f; - for (int k = 0; k < COLO_ENT_INF_BOTTLENECK; k++) - sum += h1n[k] * layer->entity_l2_w[o * COLO_ENT_INF_BOTTLENECK + k]; - en[o] = sum; - } - } - - for (int o = 0; o < H; o++) { - float best = -INFINITY; - int best_n = -1; - for (int n = 0; n < COLO_ENT_INF_NUM_NPCS; n++) { - float* rec = npcs + n * COLO_ENT_INF_FEATS; - float type_sum = 0.0f; - for (int t = 0; t < COLO_ENT_INF_TYPE_ONEHOT; t++) type_sum += rec[t]; - if (type_sum <= 0.0f) continue; - float v = layer->entity_e[(size_t)n * H + o]; - if (v > best) { best = v; best_n = n; } - } - out[o] += (best_n < 0) ? 0.0f : best; - } - - if (layer->mode >= 2) { - float* cells = obs + COLO_ENT_INF_INV_START; - for (int n = 0; n < COLO_ENT_INF_INV_NUM_CELLS; n++) { - float* rec = cells + n * COLO_ENT_INF_INV_FEATS; - float* z1n = layer->inv_z1 + n * COLO_ENT_INF_INV_BOTTLENECK; - for (int k = 0; k < COLO_ENT_INF_INV_BOTTLENECK; k++) { + for (int br = 0; br < layer->num_branches; br++) { + EntityPoolBranch* p = &layer->branches[br]; + float* recs = obs + p->start; + float expanded[ENTITY_RECORD_MAX_FEATS]; + for (int n = 0; n < p->num_recs; n++) { + float* rec = recs + n * p->obs_feats; + float* z1n = p->z1 + n * p->bottleneck; + entity_expand_record(p, rec, expanded); + for (int k = 0; k < p->bottleneck; k++) { + const float* w = p->l1_w + k * p->feats; float sum = 0.0f; - for (int i = 0; i < COLO_ENT_INF_INV_FEATS; i++) - sum += rec[i] * layer->inv_l1_w[k * COLO_ENT_INF_INV_FEATS + i]; + for (int i = 0; i < p->feats; i++) sum += expanded[i] * w[i]; z1n[k] = sum; } } - _gelu(layer->inv_z1, layer->inv_h1, COLO_ENT_INF_INV_NUM_CELLS * COLO_ENT_INF_INV_BOTTLENECK); - for (int n = 0; n < COLO_ENT_INF_INV_NUM_CELLS; n++) { - float* h1n = layer->inv_h1 + n * COLO_ENT_INF_INV_BOTTLENECK; - float* en = layer->inv_e + (size_t)n * H; + osrs_visual_gelu(p->z1, p->h1, p->num_recs * p->bottleneck); + for (int n = 0; n < p->num_recs; n++) { + float* h1n = p->h1 + n * p->bottleneck; + float* en = p->e + (size_t)n * H; for (int o = 0; o < H; o++) { float sum = 0.0f; - for (int k = 0; k < COLO_ENT_INF_INV_BOTTLENECK; k++) - sum += h1n[k] * layer->inv_l2_w[o * COLO_ENT_INF_INV_BOTTLENECK + k]; + for (int k = 0; k < p->bottleneck; k++) + sum += h1n[k] * p->l2_w[o * p->bottleneck + k]; en[o] = sum; } } for (int o = 0; o < H; o++) { float best = -INFINITY; int best_n = -1; - for (int n = 0; n < COLO_ENT_INF_INV_NUM_CELLS; n++) { - float* rec = cells + n * COLO_ENT_INF_INV_FEATS; - if (rec[COLO_ENT_INF_INV_PRESENT] <= 0.0f) continue; - float v = layer->inv_e[(size_t)n * H + o]; + for (int n = 0; n < p->num_recs; n++) { + float* rec = recs + n * p->obs_feats; + /* A coded record is live when its code is nonzero; a verbatim one + when its presence prefix is. Both mirror the fused pool's mask. */ + float mask_sum = 0.0f; + if (p->expansion != ENTITY_RECORD_VERBATIM) + mask_sum = rec[0] > 0.0f ? 1.0f : 0.0f; + else for (int t = 0; t < p->mask_prefix; t++) mask_sum += rec[t]; + if (mask_sum <= 0.0f) continue; + float v = p->e[(size_t)n * H + o]; if (v > best) { best = v; best_n = n; } } out[o] += (best_n < 0) ? 0.0f : best; @@ -150,13 +245,12 @@ void colosseum_entity_encoder(ColosseumEntityEncoder* layer, float* observations } } -void free_colosseum_entity_encoder(ColosseumEntityEncoder* layer) { - free(layer->z1); - free(layer->h1); - free(layer->entity_e); - free(layer->inv_z1); - free(layer->inv_h1); - free(layer->inv_e); +void free_entity_encoder(EntityEncoder* layer) { + for (int br = 0; br < layer->num_branches; br++) { + free(layer->branches[br].z1); + free(layer->branches[br].h1); + free(layer->branches[br].e); + } free(layer); } @@ -165,7 +259,7 @@ struct VisualNet { int num_agents; float* obs; Linear* encoder; - ColosseumEntityEncoder* entity_encoder; + EntityEncoder* entity_encoder; MinGRU* mingru; Linear* decoder; float* log_std; @@ -177,7 +271,7 @@ struct VisualNet { void visual_net_free(VisualNet* net) { free(net->obs); if (net->encoder) free(net->encoder); - if (net->entity_encoder) free_colosseum_entity_encoder(net->entity_encoder); + if (net->entity_encoder) free_entity_encoder(net->entity_encoder); free(net->decoder); free_mingru(net->mingru); if (net->multidiscrete) free(net->multidiscrete); diff --git a/ocean/osrs/scripts/check-builds.sh b/ocean/osrs/scripts/check-builds.sh new file mode 100755 index 0000000000..48481feee6 --- /dev/null +++ b/ocean/osrs/scripts/check-builds.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Neither frontend alone reproduces nvcc: g++ takes _Static_assert as an extension, +# clang++ takes out-of-order designated initializers. Run both. +# --cuda runs the real nvcc build for every osrs env, and only works on the box. +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +cd "${ROOT}" + +if [ "${1:-}" = "--cuda" ]; then + status=0 + for dir in ocean/osrs_*/; do + env="$(basename "${dir}")" + [ -f "${dir}/${env}.h" ] || { echo "SKIP ${env} (no ${env}.h wrapper)"; continue; } + log="$(mktemp)" + if ./build.sh "${env}" >"${log}" 2>&1; then + echo "OK ${env}" + else + echo "FAIL ${env}" + grep -m5 -E 'error:|error #' "${log}" || tail -5 "${log}" + status=1 + fi + rm -f "${log}" + done + exit ${status} +fi + +GXX="${GXX:-g++-16}" +CXX_CLANG="${CXX_CLANG:-clang++}" +TU="$(mktemp -d)" +trap 'rm -rf "${TU}"' EXIT +status=0 + +for enc in colosseum inferno zulrah nh_pvp; do + src="${TU}/${enc}.cc" + echo "#include \"ocean/osrs/encounters/encounter_${enc}.h\"" >"${src}" + out="$("${GXX}" -fsyntax-only -std=c++17 -I. "${src}" 2>&1)"; rc=$? + out="${out}$("${CXX_CLANG}" -fsyntax-only -std=c++17 -Werror=c11-extensions -ferror-limit=0 -I. "${src}" 2>&1)" + rc=$((rc + $?)) + if [ ${rc} -eq 0 ]; then + echo "OK encounter_${enc}.h" + else + echo "FAIL encounter_${enc}.h" + printf '%s\n' "${out}" + status=1 + fi +done + +exit ${status} diff --git a/ocean/osrs/tests/bench_colo_step.c b/ocean/osrs/tests/bench_colo_step.c new file mode 100644 index 0000000000..7824b54efc --- /dev/null +++ b/ocean/osrs/tests/bench_colo_step.c @@ -0,0 +1,275 @@ +/* Wall-clock cost of one colosseum env step, with no profiler marks compiled in. + * + * Exists to reconcile two numbers that disagree by 5x: the in-env profiler reports ~24400 + * ns/env-step, an earlier campaign recorded 4570. Both cannot be right, and every optimisation + * decision downstream is priced against whichever one is. + * + * Covers exactly what puf_step covers: encounter step, observation write, action mask write. + * + * Equip heads are PINNED. The env memoises on a loadout signature, and uniform random actions + * churn every equip head each tick, which is a genuine signature change any correct memo must + * miss -- that artifact once produced a bogus "87% of env time is gear/DPT" conclusion. + * + * Build with -std=gnu11: plain c11 hides clock_gettime on glibc and _POSIX_C_SOURCE hides + * snprintf on macOS. */ + +#include +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +static double now_ns(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1e9 + (double)ts.tv_nsec; +} + +static inline uint64_t splitmix64(uint64_t* s) { + uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +static void fill_actions(const ColosseumState* s, uint64_t* rng, + int actions[COLO_NUM_ACTION_HEADS]) { + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) { + int equip = h >= COLO_HEAD_EQUIP_BASE && h < COLO_HEAD_EQUIP_BASE + NUM_GEAR_SLOTS; + actions[h] = equip ? 0 : (int)(splitmix64(rng) % (uint64_t)COLO_ACTION_DIMS[h]); + } + if (s->modifiers.draft_pending) { + actions[COLO_HEAD_PRIMARY] = 0; + actions[COLO_HEAD_MODIFIER_SELECT] = + 1 + (int)(splitmix64(rng) % COLO_MODIFIER_DRAFT_OPTIONS); + } +} + +typedef struct { double step, obs, mask, total; long n; } Bench; + +/* STREAM-triad-style scan: the machine's practical DRAM ceiling as a function of thread count. + * If the env's parallel efficiency curve has the same shape, the env is bandwidth-bound and + * not, say, lock-bound or oversubscribed. */ +static void bandwidth_scan(void) { + const size_t N = 24u << 20; /* 192 MB per array, far past the 36 MiB L3 */ + double* a = (double*)malloc(N * sizeof(double)); + double* b = (double*)malloc(N * sizeof(double)); + double* c = (double*)malloc(N * sizeof(double)); + if (!a || !b || !c) { printf(" (allocation failed)\n"); return; } + for (size_t i = 0; i < N; i++) { a[i] = 1.0; b[i] = 2.0; c[i] = 0.0; } + + printf("memory bandwidth ceiling (triad, 3 x %zu MB)\n", N * sizeof(double) / (1u << 20)); + printf(" %8s %14s %12s\n", "threads", "GB/s", "vs 1 thread"); + double one = 0.0; + int tc[] = {1, 2, 4, 8, 16, 24, 32}; + for (int i = 0; i < (int)(sizeof(tc)/sizeof(tc[0])); i++) { + int T = tc[i]; + double t0 = now_ns(); + for (int rep = 0; rep < 4; rep++) { +#ifdef _OPENMP + #pragma omp parallel for num_threads(T) schedule(static) +#endif + for (size_t j = 0; j < N; j++) c[j] = a[j] + 3.0 * b[j]; + } + double sec = (now_ns() - t0) / 1e9; + double gbs = 4.0 * 3.0 * N * sizeof(double) / sec / 1e9; + if (i == 0) one = gbs; + printf(" %8d %14.1f %11.2fx\n", T, gbs, gbs / one); + } + printf("\n"); + free(a); free(b); free(c); +} + +/* Same as run_multi but skips the observation and mask writes, removing ~7 KB of stores per + * step while leaving the simulation identical. If the env is bandwidth-bound, dropping those + * bytes should visibly improve parallel efficiency, not just lower the single-thread cost. */ +static Bench run_multi_sim_only(int num_envs, int steps_per_env) { + ColosseumContext* ctxs = (ColosseumContext*)calloc(num_envs, sizeof(ColosseumContext)); + ColosseumState* ss = (ColosseumState*)calloc(num_envs, sizeof(ColosseumState)); + int actions[COLO_NUM_ACTION_HEADS]; + Bench b = {0}; + uint64_t rng = 0xABCDEF01ULL; + for (int e = 0; e < num_envs; e++) { + col_init_context_typed(&ctxs[e]); + col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], (uint32_t)(e + 1)); + } + double t0 = now_ns(); + for (int rep = 0; rep < steps_per_env; rep++) { + for (int e = 0; e < num_envs; e++) { + if (ss[e].episode_over) + col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], + (uint32_t)(e * 7919 + rep + 1)); + fill_actions(&ss[e], &rng, actions); + col_step_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], actions); + b.n++; + } + } + b.total = now_ns() - t0; + free(ctxs); free(ss); + return b; +} + +/* Steps N independent envs round-robin, which is what the trainer does. One env fits in L2; + * N of them do not, and the gap between those two numbers is the working-set cost that a + * single-env benchmark cannot see. */ +static Bench run_multi(int num_envs, int steps_per_env) { + ColosseumContext* ctxs = (ColosseumContext*)calloc(num_envs, sizeof(ColosseumContext)); + ColosseumState* ss = (ColosseumState*)calloc(num_envs, sizeof(ColosseumState)); + static float obs[COLO_NUM_OBS]; + static float mask[COLO_ACTION_MASK_SIZE]; + int actions[COLO_NUM_ACTION_HEADS]; + Bench b = {0}; + uint64_t rng = 0xABCDEF01ULL; + + for (int e = 0; e < num_envs; e++) { + col_init_context_typed(&ctxs[e]); + col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], (uint32_t)(e + 1)); + } + for (int rep = 0; rep < 40; rep++) { + for (int e = 0; e < num_envs; e++) { + fill_actions(&ss[e], &rng, actions); + col_step_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], actions); + } + } + + double t0 = now_ns(); + for (int rep = 0; rep < steps_per_env; rep++) { + for (int e = 0; e < num_envs; e++) { + if (ss[e].episode_over) + col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], + (uint32_t)(e * 7919 + rep + 1)); + fill_actions(&ss[e], &rng, actions); + col_step_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], actions); + col_write_obs_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], obs); + col_write_mask_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], mask); + b.n++; + } + } + b.total = now_ns() - t0; + free(ctxs); free(ss); + return b; +} + +static Bench run(int warmup_steps, int measured_steps) { + ColosseumContext ctx; + ColosseumState s; + static float obs[COLO_NUM_OBS]; + static float mask[COLO_ACTION_MASK_SIZE]; + int actions[COLO_NUM_ACTION_HEADS]; + Bench b = {0}; + + col_init_context_typed(&ctx); + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 12345u); + uint64_t rng = 0xBEEF1234ULL; + + for (int i = 0; i < warmup_steps + measured_steps; i++) { + int measuring = i >= warmup_steps; + if (s.episode_over) { + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, (uint32_t)(1000 + i)); + } + fill_actions(&s, &rng, actions); + + double t0 = now_ns(); + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + double t1 = now_ns(); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + double t2 = now_ns(); + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + double t3 = now_ns(); + + if (measuring) { + b.step += t1 - t0; + b.obs += t2 - t1; + b.mask += t3 - t2; + b.total += t3 - t0; + b.n++; + } + } + return b; +} + +int main(void) { + /* One untimed pass so the static arena/LOS tables are built before anything is measured. */ + run(200, 1); + +#ifdef _OPENMP + /* The trainer runs 8192 envs across 32 threads and its profiler reports ~7x the + * single-threaded cost per step. Neither cache pressure nor profiler overhead accounts + * for that, so scan thread count directly with the env count held fixed. */ + bandwidth_scan(); + printf("thread scan: 8192 envs, %d steps each\n", 40); + printf(" %8s %12s %12s %12s\n", "threads", "ns/step", "speedup", "efficiency"); + double t1 = 0.0; + int tcounts[] = {1, 2, 4, 8, 16, 24, 32}; + for (int i = 0; i < (int)(sizeof(tcounts)/sizeof(tcounts[0])); i++) { + int T = tcounts[i]; + int per_thread = 8192 / T; + double t0 = now_ns(); + long total = 0; + #pragma omp parallel num_threads(T) reduction(+:total) + { + Bench m = run_multi(per_thread, 40); + total += m.n; + } + double wall = now_ns() - t0; + double ns = wall * T / (double)total; + double wall_ns = wall / (double)total; + if (i == 0) t1 = wall_ns; + printf(" %8d %12.1f %11.2fx %11.0f%%\n", T, ns, t1 / wall_ns, + 100.0 * (t1 / wall_ns) / T); + } + printf("\n sim only, no obs/mask writes (~7 KB/step less traffic)\n"); + printf(" %8s %12s %12s %12s\n", "threads", "ns/step", "speedup", "efficiency"); + double s1 = 0.0; + for (int i = 0; i < (int)(sizeof(tcounts)/sizeof(tcounts[0])); i++) { + int T = tcounts[i]; + int per_thread = 8192 / T; + double t0 = now_ns(); + long total = 0; + #pragma omp parallel num_threads(T) reduction(+:total) + { + Bench m = run_multi_sim_only(per_thread, 40); + total += m.n; + } + double wall_ns = (now_ns() - t0) / (double)total; + if (i == 0) s1 = wall_ns; + printf(" %8d %12.1f %11.2fx %11.0f%%\n", T, wall_ns * T, s1 / wall_ns, + 100.0 * (s1 / wall_ns) / T); + } + printf("\n"); +#endif + + printf("working-set scan: one thread, N envs round-robin\n"); + printf(" %6s %12s %14s %12s\n", "envs", "ns/step", "state MB", "vs 1 env"); + double base = 0.0; + int counts[] = {1, 8, 64, 256, 1024, 4096, 8192}; + for (int i = 0; i < (int)(sizeof(counts)/sizeof(counts[0])); i++) { + int n = counts[i]; + int per = n <= 64 ? 4000 : (n <= 1024 ? 400 : 60); + Bench m = run_multi(n, per); + double ns = m.total / m.n; + if (i == 0) base = ns; + printf(" %6d %12.1f %14.1f %11.2fx\n", n, ns, + (double)n * (sizeof(ColosseumState) + sizeof(ColosseumContext)) / 1e6, ns / base); + } + printf("\n sizeof(ColosseumState)=%zu sizeof(ColosseumContext)=%zu\n\n", + sizeof(ColosseumState), sizeof(ColosseumContext)); + + Bench b = run(2000, 200000); + printf("colosseum step benchmark, no profiler marks, equip heads pinned\n"); + printf(" measured steps %ld\n", b.n); + printf(" col_step_ctx %8.1f ns/step\n", b.step / b.n); + printf(" col_write_obs_ctx %8.1f ns/step\n", b.obs / b.n); + printf(" col_write_mask_ctx %8.1f ns/step\n", b.mask / b.n); + printf(" TOTAL %8.1f ns/step\n", b.total / b.n); + printf("\n obs is %d floats, mask is %d floats\n", + COLO_NUM_OBS, COLO_ACTION_MASK_SIZE); + return 0; +} diff --git a/ocean/osrs/tests/probe_colo_sim_invariant.c b/ocean/osrs/tests/probe_colo_sim_invariant.c new file mode 100644 index 0000000000..91372365ef --- /dev/null +++ b/ocean/osrs/tests/probe_colo_sim_invariant.c @@ -0,0 +1,224 @@ +/* Hashes SIMULATION state only, never observation floats. + * + * test_colosseum_golden folds COLO_NUM_OBS obs floats and the sim scalars into one + * hash, so re-seeding it after an observation change also re-seeds the simulation + * half and a sim regression landed in the same commit is invisible. This probe is + * the half that must not move when the observation is recut. + * + * Covers the whole ColosseumState plus the full 452-float action mask. Storage whose + * SIZE is not part of the simulation contract -- the obs memo caches, and the pending-hit + * queues' spare capacity -- is hashed by content rather than by raw bytes, so resizing or + * renarrowing it does not move a digest. + * + * Re-seeding rule: prove the digests first, never re-print to make an edit pass. The + * pending-hit narrowing was carried across by computing baselines on the old record with + * this same hash and confirming the new one reproduced all 12. + * + * The player-collision re-seed could NOT use that method: replacing a 1156-byte array with + * two ints moved the struct's padding (size fell 1152, not the 1148 the fields imply), so + * the hashed byte SEQUENCE differs even though the content does not. That re-seed rests on + * the golden tests, which hash observation floats every tick and would move if any NPC were + * blocked differently, plus test_colosseum_modifiers at 10491/10491. + * + * ColosseumLog is skipped as of the reward-clamp telemetry: it is an observation OF the + * simulation, not part of it, so adding a counter to it must not read as a sim regression. + * That re-seed WAS proved by the differential method: digests printed with the log skipped + * on the tree without the telemetry reproduced all 12 with it. Sim quantities the log + * derives from still reach the goldens through the observation. */ + +#include +#include +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +#define FNV_OFFSET 1469598103934665603ULL +#define FNV_PRIME 1099511628211ULL + +static inline uint64_t fnv_bytes(uint64_t h, const void* p, size_t n) { + const uint8_t* b = (const uint8_t*)p; + for (size_t i = 0; i < n; i++) { h ^= b[i]; h *= FNV_PRIME; } + return h; +} + +static inline uint64_t fnv_f32(uint64_t h, float v) { + uint32_t bits; memcpy(&bits, &v, sizeof(bits)); + return fnv_bytes(h, &bits, sizeof(bits)); +} + +static inline uint64_t fnv_i32(uint64_t h, int v) { + int32_t w = (int32_t)v; + return fnv_bytes(h, &w, sizeof(w)); +} + +static inline uint64_t splitmix64(uint64_t* s) { + uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +typedef struct { + const char* name; + int public_start_wave; + uint32_t env_seed; + uint64_t action_seed; + uint64_t baseline; +} SimConfig; + +/* Baselines are generated on the tree that contains the pathfinding and reservoir + * determinism fixes. Regenerate with --print only when a SIMULATION change is + * intended, never to make an observation edit pass. */ +static SimConfig CONFIGS[] = { + {"w01", 1, 1001ULL, 0xC0FFEE01ULL, 0x9c4cbcde5d159413ULL}, + {"w02", 2, 1002ULL, 0xC0FFEE02ULL, 0x926e334416bc21f0ULL}, + {"w03", 3, 1003ULL, 0xC0FFEE03ULL, 0xcc8a421a35859befULL}, + {"w04", 4, 1004ULL, 0xC0FFEE04ULL, 0x14b1e80d759d1cb2ULL}, + {"w05", 5, 1005ULL, 0xC0FFEE05ULL, 0x22f415d9f4d6dd02ULL}, + {"w06", 6, 1006ULL, 0xC0FFEE06ULL, 0xb82309c9c86c363bULL}, + {"w07", 7, 1007ULL, 0xC0FFEE07ULL, 0x4fd42a575b3fd127ULL}, + {"w08", 8, 1008ULL, 0xC0FFEE08ULL, 0x48548d73bd727fd2ULL}, + {"w09", 9, 1009ULL, 0xC0FFEE09ULL, 0xb3f7b27247b54a07ULL}, + {"w10", 10, 1010ULL, 0xC0FFEE10ULL, 0x2b0b3f61efb8b43fULL}, + {"w11", 11, 1011ULL, 0xC0FFEE11ULL, 0x1e8ff8d2425f33a7ULL}, + {"w12", 12, 1012ULL, 0xC0FFEE12ULL, 0x210c0b6ce8537df3ULL}, +}; + +static void fill_actions( + const ColosseumState* s, uint64_t* rng, int actions[COLO_NUM_ACTION_HEADS] +) { + for (int head = 0; head < COLO_NUM_ACTION_HEADS; head++) + actions[head] = (int)(splitmix64(rng) % (uint64_t)COLO_ACTION_DIMS[head]); + if (s->modifiers.draft_pending) { + actions[COLO_HEAD_PRIMARY] = 0; + actions[COLO_HEAD_MODIFIER_SELECT] = + 1 + (int)(splitmix64(rng) % COLO_MODIFIER_DRAFT_OPTIONS); + } +} + +/* Queue contents widened back to int32, so narrowing a field's storage type does not + * move the digest. Only the live prefix is hashed: readers all loop to count, so bytes + * past it are not simulation state. */ +static uint64_t fnv_queue(uint64_t h, const EncounterPendingHitQueue* q) { + h = fnv_i32(h, q->count); + for (int i = 0; i < q->count; i++) { + const EncounterPendingHit* p = &q->hits[i]; + h = fnv_i32(h, p->active); + h = fnv_i32(h, p->damage); + h = fnv_i32(h, p->ticks_remaining); + h = fnv_i32(h, p->attack_style); + h = fnv_i32(h, p->check_prayer); + h = fnv_i32(h, p->prayer_check_delay); + h = fnv_i32(h, p->spell_type); + h = fnv_i32(h, p->source_npc_type); + h = fnv_i32(h, p->source_npc_slot); + h = fnv_i32(h, p->hit_success); + h = fnv_i32(h, p->elysian_reduced); + } + return h; +} + +static uint64_t fnv_npc(uint64_t h, const ColoNPC* n) { + const uint8_t* b = (const uint8_t*)n; + size_t off = offsetof(ColoNPC, pending_hits); + size_t end = off + sizeof(n->pending_hits); + h = fnv_bytes(h, b, off); + h = fnv_bytes(h, b + end, sizeof(*n) - end); + return fnv_queue(h, &n->pending_hits); +} + +typedef struct { size_t off, len; } SkipRegion; + +static int skip_cmp(const void* a, const void* b) { + size_t x = ((const SkipRegion*)a)->off, y = ((const SkipRegion*)b)->off; + return x < y ? -1 : (x > y ? 1 : 0); +} + +static uint64_t hash_sim(uint64_t h, const ColosseumState* s, const float* mask) { + /* Hash AROUND storage whose SIZE is not part of the simulation contract, then hash + * its contents semantically. FNV-1a multiplies once per byte, so leaving a memo + * cache or a queue's spare capacity in the byte range makes resizing it move every + * digest -- a false alarm on exactly the edits this probe exists to clear. */ + /* log and obs_memos are the last two members, skipped as ONE region running to the end + * of the struct. Skipping them individually left the alignment padding between them in + * the hash, and that padding is 4 bytes or 0 depending on sizeof(ColosseumLog) mod 8 -- + * so adding three floats to the log moved all twelve digests with no behaviour change. + * Taking the whole tail removes the padding from the hash entirely. */ + SkipRegion skip[] = { + {offsetof(ColosseumState, npcs), sizeof(s->npcs)}, + {offsetof(ColosseumState, player_pending_hits), sizeof(s->player_pending_hits)}, + {offsetof(ColosseumState, log), sizeof(*s) - offsetof(ColosseumState, log)}, + }; + int nskip = (int)(sizeof(skip) / sizeof(skip[0])); + qsort(skip, (size_t)nskip, sizeof(skip[0]), skip_cmp); + + const uint8_t* base = (const uint8_t*)s; + size_t cur = 0; + for (int i = 0; i < nskip; i++) { + h = fnv_bytes(h, base + cur, skip[i].off - cur); + cur = skip[i].off + skip[i].len; + } + h = fnv_bytes(h, base + cur, sizeof(*s) - cur); + + for (int i = 0; i < COLO_MAX_NPCS; i++) h = fnv_npc(h, &s->npcs[i]); + h = fnv_queue(h, &s->player_pending_hits); + + for (int i = 0; i < COLO_ACTION_MASK_SIZE; i++) h = fnv_f32(h, mask[i]); + return h; +} + +static uint64_t run_episode(const SimConfig* cfg, int max_ticks) { + ColosseumContext ctx; + ColosseumState s; + static float mask[COLO_ACTION_MASK_SIZE]; + int actions[COLO_NUM_ACTION_HEADS]; + + col_init_context_typed(&ctx); + ctx.config.start_wave = cfg->public_start_wave - 1; + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, cfg->env_seed); + + uint64_t rng = cfg->action_seed; + uint64_t h = FNV_OFFSET; + + for (int t = 0; t < max_ticks; t++) { + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + fill_actions(&s, &rng, actions); + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + h = hash_sim(h, &s, mask); + if (s.episode_over) break; + } + return h; +} + +int main(int argc, char** argv) { + int print = (argc > 1 && strcmp(argv[1], "--print") == 0); + int n = (int)(sizeof(CONFIGS) / sizeof(CONFIGS[0])); + int failed = 0; + + for (int i = 0; i < n; i++) { + uint64_t h = run_episode(&CONFIGS[i], 4000); + if (print) { + printf(" {\"%s\", %2d, %luULL, 0x%08lXULL, 0x%016llxULL},\n", + CONFIGS[i].name, CONFIGS[i].public_start_wave, + (unsigned long)CONFIGS[i].env_seed, + (unsigned long)CONFIGS[i].action_seed, + (unsigned long long)h); + continue; + } + int ok = (h == CONFIGS[i].baseline); + printf(" %-6s 0x%016llx %s\n", CONFIGS[i].name, + (unsigned long long)h, ok ? "PASS" : "FAIL"); + if (!ok) { + printf(" expected 0x%016llx\n", + (unsigned long long)CONFIGS[i].baseline); + failed++; + } + } + if (print) return 0; + printf("\n%d/%d sim invariants match baseline\n", n - failed, n); + return failed ? 1 : 0; +} diff --git a/ocean/osrs/tests/probe_env_thread_determinism.c b/ocean/osrs/tests/probe_env_thread_determinism.c new file mode 100644 index 0000000000..e92ecd823f --- /dev/null +++ b/ocean/osrs/tests/probe_env_thread_determinism.c @@ -0,0 +1,160 @@ +/** + * Steps a fixed set of colosseum envs through a fixed action sequence, once with + * one OpenMP worker and once with many, then compares every observation float. + * + * This isolates the environment from the trainer. Training diverges run to run + * only when vec.num_threads > 1, and the trainer's env loop is + * `#pragma omp parallel for schedule(static)` over independent Env structs, so + * either the env carries thread-shared state or the cause is outside the env. + * Nothing here is reimplemented: it drives col_step_ctx and col_write_obs_ctx. + */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +#define NUM_ENVS 256 +#define NUM_TICKS 300 + +typedef struct { + ColosseumState state; + ColosseumContext ctx; +} ProbeEnv; + +static uint64_t sm(uint64_t* s) { + uint64_t z = (*s += 0x9E3779B97F4A7C15ull); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBull; + return z ^ (z >> 31); +} + +static ProbeEnv* g_envs; +static float* g_obs; +static uint64_t g_state_hash[NUM_ENVS]; + +/* Observations are a lossy view of the state, so hash the whole struct too: + divergence in rewards, timers or RNG can be invisible in obs yet still steer + training. States are memset before reset, so padding hashes deterministically. */ +static uint64_t fnv1a(const void* p, size_t n) { + const unsigned char* b = (const unsigned char*)p; + uint64_t h = 1469598103934665603ull; + for (size_t i = 0; i < n; i++) { + h ^= b[i]; + h *= 1099511628211ull; + } + return h; +} + +static void run_pass(int workers, int late_start_mode, const int* actions) { + for (int i = 0; i < NUM_ENVS; i++) { + col_init_context_typed(&g_envs[i].ctx); + g_envs[i].ctx.config.start_wave = i % 12; + g_envs[i].ctx.config.late_start_state_mode = late_start_mode; + memset(&g_envs[i].state, 0, sizeof(ColosseumState)); + col_reset_ctx((EncounterState*)&g_envs[i].state, + (EncounterContext*)&g_envs[i].ctx, 900u + (unsigned)i * 7u); + } + /* The trainer resets inside the parallel loop when an episode ends, which is + the path col_apply_late_start_entry_state and the wave-entry reservoir sit + on. Stepping without it leaves finished envs frozen and never exercises it. */ + for (int t = 0; t < NUM_TICKS; t++) { + #pragma omp parallel for schedule(static) num_threads(workers) + for (int i = 0; i < NUM_ENVS; i++) { + const int* act = &actions[(size_t)(t * NUM_ENVS + i) * COLO_NUM_ACTION_HEADS]; + col_step_ctx((EncounterState*)&g_envs[i].state, + (EncounterContext*)&g_envs[i].ctx, act); + if (g_envs[i].state.episode_over) { + unsigned seed = 900u + (unsigned)i * 7u + (unsigned)(t + 1) * 65537u; + memset(&g_envs[i].state, 0, sizeof(ColosseumState)); + col_reset_ctx((EncounterState*)&g_envs[i].state, + (EncounterContext*)&g_envs[i].ctx, seed); + } + } + } + #pragma omp parallel for schedule(static) num_threads(workers) + for (int i = 0; i < NUM_ENVS; i++) { + col_write_obs_ctx((EncounterState*)&g_envs[i].state, + (EncounterContext*)&g_envs[i].ctx, &g_obs[(size_t)i * COLO_NUM_OBS]); + g_state_hash[i] = fnv1a(&g_envs[i].state, sizeof(ColosseumState)); + } +} + +static int compare(const float* ref_obs, const uint64_t* ref_hash, const char* label) { + long obs_diffs = 0, state_diffs = 0; + int first_env = -1, first_idx = -1, first_state_env = -1; + for (int i = 0; i < NUM_ENVS; i++) { + if (g_state_hash[i] != ref_hash[i]) { + if (first_state_env < 0) first_state_env = i; + state_diffs++; + } + for (int k = 0; k < COLO_NUM_OBS; k++) { + size_t o = (size_t)i * COLO_NUM_OBS + k; + if (ref_obs[o] == g_obs[o]) continue; + if (first_env < 0) { first_env = i; first_idx = k; } + obs_diffs++; + } + } + if (obs_diffs == 0 && state_diffs == 0) { + printf(" %-26s IDENTICAL (%d envs, %d obs floats, full state hash)\n", + label, NUM_ENVS, COLO_NUM_OBS); + return 0; + } + printf(" %-26s DIFFERS obs %ld/%lld floats, state %ld/%d envs", + label, obs_diffs, (long long)NUM_ENVS * COLO_NUM_OBS, state_diffs, NUM_ENVS); + if (obs_diffs == 0 && state_diffs > 0) + printf(" <-- state only, obs did not show it (first env %d)", first_state_env); + else if (first_env >= 0) + printf(" (first env %d idx %d)", first_env, first_idx); + printf("\n"); + return 1; +} + +int main(int argc, char** argv) { + int workers = argc > 1 ? atoi(argv[1]) : 32; + + size_t nact = (size_t)NUM_TICKS * NUM_ENVS * COLO_NUM_ACTION_HEADS; + int* actions = (int*)malloc(nact * sizeof(int)); + uint64_t rng = 20260730u; + for (size_t i = 0; i < nact; i++) + actions[i] = (int)(sm(&rng) % (uint64_t)COLO_ACTION_DIMS[i % COLO_NUM_ACTION_HEADS]); + + g_envs = (ProbeEnv*)malloc(sizeof(ProbeEnv) * NUM_ENVS); + g_obs = (float*)malloc(sizeof(float) * NUM_ENVS * COLO_NUM_OBS); + float* ref = (float*)malloc(sizeof(float) * NUM_ENVS * COLO_NUM_OBS); + uint64_t ref_hash[NUM_ENVS]; + size_t obs_bytes = sizeof(float) * NUM_ENVS * COLO_NUM_OBS; + + int failures = 0; + for (int mode = 0; mode <= 2; mode += 2) { + printf("late_start_state_mode = %d\n", mode); + char label[64]; + + run_pass(1, mode, actions); + memcpy(ref, g_obs, obs_bytes); + memcpy(ref_hash, g_state_hash, sizeof(ref_hash)); + run_pass(1, mode, actions); + failures += compare(ref, ref_hash, "1 worker vs 1 worker"); + + run_pass(1, mode, actions); + memcpy(ref, g_obs, obs_bytes); + memcpy(ref_hash, g_state_hash, sizeof(ref_hash)); + run_pass(workers, mode, actions); + snprintf(label, sizeof(label), "1 worker vs %d workers", workers); + failures += compare(ref, ref_hash, label); + + run_pass(workers, mode, actions); + memcpy(ref, g_obs, obs_bytes); + memcpy(ref_hash, g_state_hash, sizeof(ref_hash)); + run_pass(workers, mode, actions); + snprintf(label, sizeof(label), "%d workers, twice", workers); + failures += compare(ref, ref_hash, label); + printf("\n"); + } + + printf("%s\n", failures ? "ENV IS THREAD-SENSITIVE" : "env is thread-invariant"); + return failures ? 1 : 0; +} diff --git a/ocean/osrs/tests/test_colo_hit_delay.c b/ocean/osrs/tests/test_colo_hit_delay.c new file mode 100644 index 0000000000..3f94e08a73 --- /dev/null +++ b/ocean/osrs/tests/test_colo_hit_delay.c @@ -0,0 +1,159 @@ +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" +#include "ocean/osrs/tests/osrs_test_check.h" + +/** Section 8 of osrs-engine-quirks, transcribed independently of the sim. + Player-on-NPC projectiles land one tick later than the base table. */ +static int section8_player_hit_delay(AttackStyle style, int distance) { + switch (style) { + case ATTACK_STYLE_MELEE: + return 0; + case ATTACK_STYLE_RANGED: + return 1 + (3 + distance) / 6 + 1; + case ATTACK_STYLE_MAGIC: + return 1 + (1 + distance) / 3 + 1; + default: + break; + } + abort(); +} + +static void clear_npcs(ColosseumState* s) { + memset(s->npcs, 0, sizeof(s->npcs)); + memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); + memset(s->totems, 0, sizeof(s->totems)); + memset(s->bees, 0, sizeof(s->bees)); + col_rebuild_player_collision_flags(s); +} + +static void init_state(ColosseumState* s, ColosseumContext* ctx, int px, int py) { + col_init_context_typed(ctx); + ctx->config.late_start_state_mode = 0; + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, 4141u); + clear_npcs(s); + s->modifiers.draft_pending = 0; + s->player.x = px; + s->player.y = py; + col_rebuild_player_collision_flags(s); +} + +static ColoWeaponSet weapon_set_for_style(AttackStyle style) { + switch (style) { + case ATTACK_STYLE_MELEE: return COLO_GEAR_MELEE; + case ATTACK_STYLE_RANGED: return COLO_GEAR_RANGED; + case ATTACK_STYLE_MAGIC: return COLO_GEAR_MAGIC; + default: break; + } + abort(); +} + +typedef struct { + int distance; + int queued_ticks; + int check_prayer; + int hit_count; +} QueuedSwing; + +/** Fires one real player swing at an NPC placed `offset` tiles east of the + player and reports what the sim actually queued on that NPC. */ +static QueuedSwing swing_at_offset(AttackStyle style, int offset) { + ColosseumState s; + ColosseumContext ctx; + int px = 16; + int py = 16; + init_state(&s, &ctx, px, py); + col_apply_weapon_set(&s, weapon_set_for_style(style)); + col_init_npc(&s, 0, COLO_FREMENNIK_ARCHER, px + offset, py); + + ColoNPC* npc = &s.npcs[0]; + int distance = encounter_projectile_distance( + s.player.x, s.player.y, 1, + npc->x, npc->y, col_npc_effective_size(npc), + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + + col_player_attack_target(&s, 0); + + QueuedSwing out = { + .distance = distance, + .queued_ticks = npc->pending_hits.count > 0 + ? npc->pending_hits.hits[0].ticks_remaining + : -1, + .check_prayer = npc->pending_hits.count > 0 + ? npc->pending_hits.hits[0].check_prayer + : -1, + .hit_count = npc->pending_hits.count, + }; + return out; +} + +static void test_timing_helper_matches_section8(void) { + const AttackStyle styles[3] = { + ATTACK_STYLE_MELEE, ATTACK_STYLE_RANGED, ATTACK_STYLE_MAGIC + }; + const uint8_t weapons[3] = { + ITEM_SCYTHE_OF_VITUR, ITEM_TWISTED_BOW, ITEM_TUMEKENS_SHADOW + }; + const char* labels[3] = { "melee", "ranged", "magic" }; + + for (int i = 0; i < 3; i++) { + for (int d = 0; d <= 12; d++) { + char label[96]; + snprintf(label, sizeof(label), + "col_player_projectile_timing %s d=%d matches section 8", + labels[i], d); + EncounterProjectileTiming timing = + col_player_projectile_timing(styles[i], weapons[i], d); + ASSERT_INT_EQ(label, timing.damage_delay_ticks, + section8_player_hit_delay(styles[i], d)); + } + } +} + +static void test_queued_delay_matches_section8(void) { + const AttackStyle styles[3] = { + ATTACK_STYLE_MELEE, ATTACK_STYLE_RANGED, ATTACK_STYLE_MAGIC + }; + const char* labels[3] = { "melee", "ranged", "magic" }; + const int melee_offsets[1] = { 1 }; + const int ranged_offsets[6] = { 1, 2, 3, 6, 9, 10 }; + + for (int i = 0; i < 3; i++) { + const int* offsets = styles[i] == ATTACK_STYLE_MELEE + ? melee_offsets : ranged_offsets; + int n = styles[i] == ATTACK_STYLE_MELEE ? 1 : 6; + for (int k = 0; k < n; k++) { + QueuedSwing swing = swing_at_offset(styles[i], offsets[k]); + char label[128]; + snprintf(label, sizeof(label), + "queued %s hit at d=%d lands on the section 8 tick", + labels[i], swing.distance); + ASSERT_INT_EQ(label, swing.queued_ticks, + section8_player_hit_delay(styles[i], swing.distance)); + + snprintf(label, sizeof(label), + "queued %s hit at d=%d resolves prayer at throw, not on landing", + labels[i], swing.distance); + ASSERT_INT_EQ(label, swing.check_prayer, 0); + } + } +} + +static void test_magic_delay_grows_with_distance(void) { + QueuedSwing near = swing_at_offset(ATTACK_STYLE_MAGIC, 1); + QueuedSwing far = swing_at_offset(ATTACK_STYLE_MAGIC, 9); + CHECK("magic hit delay grows with distance rather than staying flat", + far.queued_ticks > near.queued_ticks); + CHECK("magic swings still queue exactly one splat", + near.hit_count == 1 && far.hit_count == 1); +} + +int main(void) { + printf("colosseum player hit delay\n"); + test_timing_helper_matches_section8(); + test_queued_delay_matches_section8(); + test_magic_delay_grows_with_distance(); + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/test_colo_prayer_wiring.c b/ocean/osrs/tests/test_colo_prayer_wiring.c index 0f069c9db7..217dc9b7f7 100644 --- a/ocean/osrs/tests/test_colo_prayer_wiring.c +++ b/ocean/osrs/tests/test_colo_prayer_wiring.c @@ -48,7 +48,6 @@ static void run_test(const char* label, PrayerPolicy policy, int start_wave, int ctx.config.start_wave = start_wave; ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; ctx.config.beginner_loadout_fraction = 0.5f; - ctx.config.step_out_forecast_obs_enabled = 1; static float obs[COLO_NUM_OBS]; ColosseumState s; diff --git a/ocean/osrs/tests/test_colosseum_forecast_exact.c b/ocean/osrs/tests/test_colosseum_forecast_exact.c index 9a2f66085b..b859e58a4d 100644 --- a/ocean/osrs/tests/test_colosseum_forecast_exact.c +++ b/ocean/osrs/tests/test_colosseum_forecast_exact.c @@ -70,24 +70,68 @@ static void col_step_out_forecast_landing_selftest(void) { ENCOUNTER_MOVE_ACTIONS); } +static void col_static_los_table_selftest(void) { + col_build_static_arena(); + for (int x0 = COLO_ARENA_MIN_X; x0 <= COLO_ARENA_MAX_X; x0++) { + for (int y0 = COLO_ARENA_MIN_Y; y0 <= COLO_ARENA_MAX_Y; y0++) { + for (int x1 = COLO_ARENA_MIN_X; x1 <= COLO_ARENA_MAX_X; x1++) { + for (int y1 = COLO_ARENA_MIN_Y; y1 <= COLO_ARENA_MAX_Y; y1++) { + int table = col_static_los_get(x0, y0, x1, y1); + int slow = col_tiles_have_los_reference_slowpath(x0, y0, x1, y1); + if (table != slow) { + fprintf(stderr, + "colosseum LoS table mismatch (%d,%d)->(%d,%d): table=%d slow=%d\n", + x0, y0, x1, y1, table, slow); + abort(); + } + } + } + } + } + printf("colosseum LoS table selftest PASS: %u pairs\n", + (unsigned)COLO_STATIC_LOS_TABLE_BITS); +} + +static void col_static_footprint_table_selftest(void) { + col_build_static_arena(); + int checks = 0; + int min_x = COLO_STATIC_FOOTPRINT_MIN_X - COLO_STATIC_FOOTPRINT_MAX_SIZE; + int min_y = COLO_STATIC_FOOTPRINT_MIN_Y - COLO_STATIC_FOOTPRINT_MAX_SIZE; + int max_x = COLO_STATIC_FOOTPRINT_MAX_X + COLO_STATIC_FOOTPRINT_MAX_SIZE; + int max_y = COLO_STATIC_FOOTPRINT_MAX_Y + COLO_STATIC_FOOTPRINT_MAX_SIZE; + for (int size = 1; size <= COLO_STATIC_FOOTPRINT_MAX_SIZE; size++) { + for (int x = min_x; x <= max_x; x++) { + for (int y = min_y; y <= max_y; y++) { + int table = col_static_footprint_blocked_lookup(x, y, size); + int slow = col_static_footprint_blocked_reference_slowpath(x, y, size); + if (table != slow) { + fprintf(stderr, + "colosseum footprint table mismatch (%d,%d) size=%d: table=%d slow=%d\n", + x, y, size, table, slow); + abort(); + } + checks++; + } + } + } + printf("colosseum footprint table selftest PASS: %d checks\n", checks); +} + #define col_init_context_typed(ctx_ptr) do { \ col_init_context_typed(ctx_ptr); \ (ctx_ptr)->config.late_start_state_mode = 0; \ } while (0) #define EXACT_MAGIC "COLOEXACTv1" -#define EXACT_VERSION 2u +#define EXACT_VERSION 5u #define EXACT_CHUNK_BYTES 65536 typedef struct { char magic[16]; uint32_t version; uint32_t state_size; - uint32_t forecast_size; - uint32_t forecast_obs_size; uint32_t obs_size; uint32_t action_mask_size; - uint32_t action_features; uint32_t record_count; } ColoExactFileHeader; @@ -101,10 +145,7 @@ typedef struct { uint32_t state_size; uint32_t obs_size; uint32_t action_mask_size; - uint32_t forecast_size; uint64_t state_hash; - uint64_t forecast_hash; - uint64_t forecast_obs_hash; uint64_t obs_hash; uint64_t action_mask_hash; float reward; @@ -167,11 +208,8 @@ static void exact_writer_open(ColoExactWriter* writer, const char* path) { memcpy(header.magic, EXACT_MAGIC, sizeof(EXACT_MAGIC)); header.version = EXACT_VERSION; header.state_size = (uint32_t)sizeof(ColosseumState); - header.forecast_size = (uint32_t)sizeof(ColoStepOutForecast); - header.forecast_obs_size = COLO_STEP_OUT_FORECAST_OBS_SIZE; header.obs_size = COLO_NUM_OBS; header.action_mask_size = COLO_ACTION_MASK_SIZE; - header.action_features = COLO_STEP_OUT_FORECAST_ACTION_FEATURES; exact_write_all(writer->file, &header, sizeof(header)); } @@ -180,11 +218,8 @@ static void exact_writer_close(ColoExactWriter* writer) { memcpy(header.magic, EXACT_MAGIC, sizeof(EXACT_MAGIC)); header.version = EXACT_VERSION; header.state_size = (uint32_t)sizeof(ColosseumState); - header.forecast_size = (uint32_t)sizeof(ColoStepOutForecast); - header.forecast_obs_size = COLO_STEP_OUT_FORECAST_OBS_SIZE; header.obs_size = COLO_NUM_OBS; header.action_mask_size = COLO_ACTION_MASK_SIZE; - header.action_features = COLO_STEP_OUT_FORECAST_ACTION_FEATURES; header.record_count = writer->record_count; if (fseek(writer->file, 0, SEEK_SET) != 0) { perror("seek colosseum exact fixture"); @@ -205,18 +240,9 @@ static void exact_capture( ColosseumState* s, ColosseumContext* ctx ) { - ColoStepOutForecast forecast; - float forecast_obs[COLO_STEP_OUT_FORECAST_OBS_SIZE]; float obs[COLO_NUM_OBS]; float action_mask[COLO_ACTION_MASK_SIZE]; - ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS]; - col_build_step_out_forecast_horizon_mode_summary( - s, &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, - ctx->config.forecast_run_tile_mode); - int fi = col_write_step_out_forecast_obs_summary( - &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, forecast_obs, 0); - assert(fi == COLO_STEP_OUT_FORECAST_OBS_SIZE); col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); col_write_mask_ctx((EncounterState*)s, (EncounterContext*)ctx, action_mask); @@ -231,17 +257,12 @@ static void exact_capture( record.state_size = (uint32_t)sizeof(*s); record.obs_size = COLO_NUM_OBS; record.action_mask_size = COLO_ACTION_MASK_SIZE; - record.forecast_size = (uint32_t)sizeof(forecast); record.state_hash = exact_hash_bytes(s, sizeof(*s)); - record.forecast_hash = exact_hash_bytes(&forecast, sizeof(forecast)); - record.forecast_obs_hash = exact_hash_bytes(forecast_obs, sizeof(forecast_obs)); record.obs_hash = exact_hash_bytes(obs, sizeof(obs)); record.action_mask_hash = exact_hash_bytes(action_mask, sizeof(action_mask)); record.reward = col_get_reward_ctx((EncounterState*)s, (EncounterContext*)ctx); exact_write_all(writer->file, &record, sizeof(record)); - exact_write_all(writer->file, &forecast, sizeof(forecast)); - exact_write_all(writer->file, forecast_obs, sizeof(forecast_obs)); exact_write_all(writer->file, obs, sizeof(obs)); exact_write_all(writer->file, action_mask, sizeof(action_mask)); exact_write_all(writer->file, s, sizeof(*s)); @@ -282,7 +303,6 @@ static void exact_init_state( ) { col_init_context_typed(ctx); ctx->config.start_wave = start_wave; - ctx->config.step_out_forecast_obs_enabled = 1; memset(s, 0, sizeof(*s)); col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); } @@ -320,6 +340,178 @@ static void exact_prepare_custom( exact_refresh_geometry(s, ctx); } +typedef struct { + ColoWalkCtx walk; + int blocker_count; + int blocker_x[16]; + int blocker_y[16]; +} ExactAttackRouteContext; + +static int exact_attack_route_walkable(void* ctx, int x, int y) { + ExactAttackRouteContext* route_ctx = (ExactAttackRouteContext*)ctx; + return col_tile_walkable(&route_ctx->walk, x, y); +} + +static int exact_attack_route_blocked(void* ctx, int abs_x, int abs_y) { + ExactAttackRouteContext* route_ctx = (ExactAttackRouteContext*)ctx; + if (col_pathfind_blocked(&route_ctx->walk, abs_x, abs_y)) return 1; + int x = abs_x - route_ctx->walk.ctx->world_offset_x; + int y = abs_y - route_ctx->walk.ctx->world_offset_y; + for (int i = 0; i < route_ctx->blocker_count; i++) { + if (route_ctx->blocker_x[i] == x && + route_ctx->blocker_y[i] == y) { + return 1; + } + } + return 0; +} + +static EncounterAttackRouteLanding exact_attack_route_runtime_landing( + const ColosseumState* s, + const ColosseumContext* ctx, + ExactAttackRouteContext* route_ctx, + int target_x, + int target_y, + int target_size, + int attack_range, + const OsrsLosQuery* los_query +) { + Player player = s->player; + encounter_chase_attack_target( + &player, + target_x, + target_y, + target_size, + attack_range, + ctx->collision_map, + ctx->world_offset_x, + ctx->world_offset_y, + exact_attack_route_walkable, + route_ctx, + exact_attack_route_blocked, + route_ctx, + los_query, + COLO_ARENA_MIN_X, + COLO_ARENA_MIN_Y, + COLO_ARENA_WIDTH, + COLO_ARENA_HEIGHT); + return (EncounterAttackRouteLanding){ + .land_x = player.x, + .land_y = player.y, + }; +} + +static int exact_attack_route_property_scenario( + const char* scenario, + ColosseumState* s, + ColosseumContext* ctx, + ExactAttackRouteContext* route_ctx +) { + EncounterArenaAttackRouteField field; + encounter_build_arena_attack_route_field( + &field, + ctx->collision_map, + ctx->world_offset_x, + ctx->world_offset_y, + s->player.x, + s->player.y, + exact_attack_route_walkable, + route_ctx, + exact_attack_route_blocked, + route_ctx, + COLO_ARENA_MIN_X, + COLO_ARENA_MIN_Y, + COLO_ARENA_WIDTH, + COLO_ARENA_HEIGHT); + OsrsLosQuery los_query = col_player_los_query(s); + static const int target_sizes[] = {1, 2, 5}; + static const int attack_ranges[] = {1, 3, 6, 10}; + int checks = 0; + + for (int target_y = COLO_ARENA_MIN_Y - 1; + target_y <= COLO_ARENA_MAX_Y + 1; + target_y += 5) { + for (int target_x = COLO_ARENA_MIN_X - 1; + target_x <= COLO_ARENA_MAX_X + 1; + target_x += 4) { + for (size_t size_idx = 0; + size_idx < sizeof(target_sizes) / sizeof(target_sizes[0]); + size_idx++) { + for (size_t range_idx = 0; + range_idx < sizeof(attack_ranges) / sizeof(attack_ranges[0]); + range_idx++) { + int target_size = target_sizes[size_idx]; + int attack_range = attack_ranges[range_idx]; + EncounterAttackRouteLanding expected = + exact_attack_route_runtime_landing( + s, ctx, route_ctx, + target_x, target_y, target_size, attack_range, + &los_query); + EncounterAttackRouteLanding actual = + encounter_arena_attack_route_landing( + &field, + target_x, target_y, target_size, attack_range, + &los_query); + if (actual.land_x != expected.land_x || + actual.land_y != expected.land_y) { + fprintf(stderr, + "attack route mismatch scenario=%s player=(%d,%d) target=(%d,%d,%d) range=%d field=(%d,%d) runtime=(%d,%d)\n", + scenario, + s->player.x, + s->player.y, + target_x, + target_y, + target_size, + attack_range, + actual.land_x, + actual.land_y, + expected.land_x, + expected.land_y); + abort(); + } + checks++; + } + } + } + } + return checks; +} + +static void exact_attack_route_property_selftest(void) { + ColosseumState s; + ColosseumContext ctx; + exact_prepare_custom(&s, &ctx, 0xA771u, 16, 16); + ExactAttackRouteContext route_ctx = { + .walk = {&s, &ctx}, + }; + int checks = exact_attack_route_property_scenario( + "open-arena", &s, &ctx, &route_ctx); + + route_ctx.blocker_count = 8; + int blocker_x[] = {14, 15, 16, 17, 18, 17, 16, 15}; + int blocker_y[] = {16, 15, 14, 15, 16, 17, 18, 17}; + memcpy(route_ctx.blocker_x, blocker_x, sizeof(blocker_x)); + memcpy(route_ctx.blocker_y, blocker_y, sizeof(blocker_y)); + checks += exact_attack_route_property_scenario( + "dynamic-blockers", &s, &ctx, &route_ctx); + + route_ctx.blocker_count = 0; + s.wave = COLO_WAVE_BOSS; + s.sol.started = 1; + s.sol.boss_arena_min_x = COLO_BOSS_ARENA_MIN_X; + s.sol.boss_arena_min_y = COLO_BOSS_ARENA_MIN_Y; + s.sol.boss_arena_max_x = COLO_BOSS_ARENA_MAX_X; + s.sol.boss_arena_max_y = COLO_BOSS_ARENA_MAX_Y; + s.player.x = COLO_BOSS_ARENA_MIN_X + 1; + s.player.y = COLO_BOSS_ARENA_MIN_Y + 1; + exact_refresh_geometry(&s, &ctx); + checks += exact_attack_route_property_scenario( + "sol-clamp", &s, &ctx, &route_ctx); + printf( + "colosseum attack route property selftest PASS: %d target queries across 3 fields\n", + checks); +} + static void exact_run_steps( ColoExactWriter* writer, uint32_t scenario_id, @@ -548,17 +740,23 @@ static int exact_compare_files(const char* expected_path, const char* actual_pat } int main(int argc, char** argv) { + if (argc == 2 && strcmp(argv[1], "--attack-route-selftest") == 0) { + exact_attack_route_property_selftest(); + return 0; + } if (argc != 3 || (strcmp(argv[1], "--write-golden") != 0 && strcmp(argv[1], "--compare") != 0)) { fprintf(stderr, - "usage: %s --write-golden DIR | --compare DIR\n", argv[0]); + "usage: %s --attack-route-selftest | --write-golden DIR | --compare DIR\n", + argv[0]); return 2; } col_static_los_table_selftest(); col_static_footprint_table_selftest(); col_step_out_forecast_landing_selftest(); + exact_attack_route_property_selftest(); char fixture_path[1024]; char current_path[1024]; diff --git a/ocean/osrs/tests/test_colosseum_golden.c b/ocean/osrs/tests/test_colosseum_golden.c index 8ef21ba404..0e848c1af4 100644 --- a/ocean/osrs/tests/test_colosseum_golden.c +++ b/ocean/osrs/tests/test_colosseum_golden.c @@ -66,8 +66,6 @@ static uint64_t run_episode(const GoldenConfig* cfg, int max_ticks) { col_init_context_typed(&ctx); ctx.config.start_wave = cfg->public_start_wave - 1; - ctx.config.step_out_forecast_obs_enabled = 1; - ctx.config.forecast_horizon = 4; memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, cfg->env_seed); @@ -183,19 +181,23 @@ static const GoldenConfig CONFIGS[] = { #define NUM_CONFIGS ((int)(sizeof(CONFIGS) / sizeof(CONFIGS[0]))) #define EPISODE_TICKS 4000 +/* Re-seeded for the inventory observation recut: 15 floats per cell became an item code plus + * is_equipped and hp_heal, obs 1258 -> 922. All twelve moved because the hash folds obs + * floats and every episode carries 28 inventory cells from tick zero. probe_colo_sim_invariant + * stayed 12/12 across the change, which is the proof no simulated tick moved. */ static const uint64_t BASELINE[NUM_CONFIGS] = { - 0x6b648dbd26450b82ULL, - 0xd25dd5f73aea2df6ULL, - 0x137d8011443b61f8ULL, - 0xeb82de91da34947dULL, - 0xf10a60721a18d0ecULL, - 0xef3182c4eebbaeb5ULL, - 0x63f92c48e77deeecULL, - 0x4e15c78ede861b01ULL, - 0x4b420b3e18d846ebULL, - 0x054f146530962087ULL, - 0x4b5835b98fc7d4b0ULL, - 0xae22f0ac054585aaULL, + 0xd2c17aa73e2a0b43ULL, + 0x872cf27641f8df3cULL, + 0x50498ba64b2aa646ULL, + 0xbb24e8e6cb69976dULL, + 0xaa9e6568f552204eULL, + 0xa526657dbf0b7a2fULL, + 0xd8369f4857156246ULL, + 0x2bf1b1a8ae9ec50bULL, + 0x57ae207dc177c45bULL, + 0x44c0ae98c7ab041eULL, + 0x2c7389fc883943a0ULL, + 0x4f5a9f22f5ecfd4dULL, }; int main(int argc, char** argv) { diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index cfe0404587..a3e4323f5a 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -13,13 +13,17 @@ #include "ocean/osrs/tests/osrs_test_check.h" -#define TEST_NPC_TELLS_OFFSET 26 #define TEST_MOD_HAZARD_BASE (COLO_OBS_AFTER_NPCS + COLO_MODIFIER_FLAGS_OBS_SIZE) #define TEST_MOD_OBS_DOOM_LETHAL (TEST_MOD_HAZARD_BASE + 2) -#define TEST_MOD_OBS_VENOM_TIMER (TEST_MOD_HAZARD_BASE + 6) -#define TEST_MOD_OBS_SOLARFLARE (TEST_MOD_HAZARD_BASE + 10) -#define TEST_MOD_OBS_MOLTEN (TEST_MOD_HAZARD_BASE + 18) -#define TEST_MOD_OBS_VOLATILITY (TEST_MOD_HAZARD_BASE + 30) +#define TEST_MOD_OBS_DOOM_PENDING (TEST_MOD_HAZARD_BASE + 3) +#define TEST_MOD_OBS_VENOM_SEVERITY (TEST_MOD_HAZARD_BASE + 4) +#define TEST_MOD_OBS_VENOM_TIMER (TEST_MOD_HAZARD_BASE + 5) +#define TEST_MOD_OBS_POISON_SEVERITY (TEST_MOD_HAZARD_BASE + 6) +#define TEST_MOD_OBS_POISON_TIMER (TEST_MOD_HAZARD_BASE + 7) +#define TEST_MOD_OBS_SOLARFLARE (TEST_MOD_HAZARD_BASE + 11) +#define TEST_MOD_OBS_MOLTEN (TEST_MOD_HAZARD_BASE + 19) +#define TEST_MOD_OBS_VOLATILITY (TEST_MOD_HAZARD_BASE + 31) +#define TEST_MOD_OBS_PREMOVE_UNAVOIDABLE (TEST_MOD_HAZARD_BASE + 39) static EncounterLoadoutStats test_col_live_stats_for_set( const ColosseumState* s, @@ -107,7 +111,7 @@ static void kill_first_live_score_enemy(ColosseumState* s) { static float score_for_depth(float depth) { float ratio = depth / (float)COLO_NUM_WAVES; - return 0.99f * ratio * ratio; + return COLO_SCORE_LOSS_CEILING * ratio * ratio; } static void land_pending_player_hits(ColosseumState* s) { @@ -147,10 +151,10 @@ static int forecast_move_action_for_delta(int dx, int dy) { return 0; } -static int forecast_action_has_event(const ColoStepOutForecastAction* action) { - for (int tick = 0; tick < COLO_STEP_OUT_FORECAST_HORIZON; tick++) - if (col_step_out_forecast_tick_has_event(&action->ticks[tick])) return 1; - return 0; +static int test_obs_slot_for_npc(const ColosseumState* s, int npc_idx) { + for (int slot = 0; slot < COLO_OBS_NPCS; slot++) + if (s->current_obs_slots[slot] == npc_idx) return slot; + return -1; } static int test_find_inventory_cell_with_item(const ColosseumState* s, uint8_t item_idx) { @@ -242,6 +246,7 @@ static int test_aggregate_doses_for_kind( return s->player.brew_doses; case OSRS_CONSUMABLE_SUPER_RESTORE: case OSRS_CONSUMABLE_SANFEW: + case OSRS_CONSUMABLE_PRAYER_RESTORE: return s->player.restore_doses; case OSRS_CONSUMABLE_SUPER_COMBAT: case OSRS_CONSUMABLE_DIVINE_COMBAT: @@ -258,6 +263,8 @@ static int test_aggregate_doses_for_kind( case OSRS_CONSUMABLE_NONE: case OSRS_CONSUMABLE_SHARK_FOOD: case OSRS_CONSUMABLE_KARAMBWAN: + case OSRS_CONSUMABLE_BASTION: + case OSRS_CONSUMABLE_STAMINA: return -1; } abort(); @@ -282,18 +289,45 @@ static void test_prepare_for_drink_kind( } } +/* The committed table src/ocean.cu and the viewer both read. Kept here as a third, + independent copy so test_item_obs_table_is_current can diff it against a fresh build. */ +static const float TEST_ITEM_OBS_TABLE + [COLO_ITEM_OBS_TABLE_ROWS][COLO_ITEM_OBS_TABLE_COLS] = { +#include "ocean/osrs/osrs_colosseum_item_obs_table.inc" +}; + +/* Exactly what colo_ent_gather_inv does: the observation names a table row and supplies the + two features the item does not fix. Every per-cell assertion below reads the result, so it + asserts against what the encoder actually sees rather than against the raw observation. */ +static void test_expand_inventory_cell( + const float obs[COLO_NUM_OBS], + int cell_idx, + float out[COLO_INVENTORY_CELL_ENCODER_FEATURES] +) { + const float* coded = &obs[COLO_OBS_AFTER_PLAYER + + cell_idx * COLO_INVENTORY_CELL_OBS_FEATURES]; + int code = osrs_inventory_cell_obs_code_decode(coded[OSRS_INVENTORY_CELL_OBS_CODE]); + if (code < 0 || code >= COLO_ITEM_OBS_TABLE_ROWS) { + fprintf(stderr, "inventory cell %d observed item code %d\n", cell_idx, code); + abort(); + } + osrs_expand_inventory_cell_obs_code_features(out, coded, TEST_ITEM_OBS_TABLE[code]); +} + +/* Offsets into the EXPANDED record above, not into the observation. */ typedef enum { - TEST_INV_OBS_ROLE_ARMOR = 12, - TEST_INV_OBS_ROLE_WEAPON = 13, - TEST_INV_OBS_KIND_BREW = 14, - TEST_INV_OBS_KIND_RESTORE = 15, - TEST_INV_OBS_KIND_COMBAT_BOOST = 16, - TEST_INV_OBS_KIND_RANGED_BOOST = 17, - TEST_INV_OBS_KIND_SPECIAL = 18, - TEST_INV_OBS_EFFECT_LIFESTEAL = 22, - TEST_INV_OBS_EFFECT_DAMAGE_AMP = 23, - TEST_INV_OBS_EFFECT_DEFENSIVE = 24, - TEST_INV_OBS_EFFECT_UTIL = 25, + TEST_INV_OBS_ROLE_ARMOR = 3, + TEST_INV_OBS_ROLE_WEAPON = 4, + TEST_INV_OBS_KIND_BREW = OSRS_INVENTORY_CELL_OBS_SHARED + 0, + TEST_INV_OBS_KIND_RESTORE = OSRS_INVENTORY_CELL_OBS_SHARED + 1, + TEST_INV_OBS_KIND_COMBAT_BOOST = OSRS_INVENTORY_CELL_OBS_SHARED + 2, + TEST_INV_OBS_KIND_RANGED_BOOST = OSRS_INVENTORY_CELL_OBS_SHARED + 3, + TEST_INV_OBS_KIND_SPECIAL = OSRS_INVENTORY_CELL_OBS_SHARED + 4, + TEST_INV_OBS_EFFECT_LIFESTEAL = OSRS_INVENTORY_CELL_OBS_SHARED + 3, + TEST_INV_OBS_EFFECT_DAMAGE_AMP = OSRS_INVENTORY_CELL_OBS_SHARED + 4, + TEST_INV_OBS_EFFECT_DEFENSIVE = OSRS_INVENTORY_CELL_OBS_SHARED + 5, + TEST_INV_OBS_EFFECT_UTIL = OSRS_INVENTORY_CELL_OBS_SHARED + 6, + TEST_INV_OBS_SPEC_COST = OSRS_INVENTORY_CELL_OBS_SHARED + 9, } TestInventoryObsFeature; typedef struct { @@ -350,20 +384,20 @@ static TestDroppedInventoryFields test_reconstructed_dropped_inventory_fields( const float mask[COLO_ACTION_MASK_SIZE], int cell_idx ) { - int base = COLO_OBS_AFTER_PILLARS + - cell_idx * COLO_INVENTORY_CELL_OBS_FEATURES; - const float* cell_obs = &obs[base]; - float kind = test_any_inventory_kind_bit(cell_obs); - float effect = test_binary_float( + float expanded[COLO_INVENTORY_CELL_ENCODER_FEATURES]; + test_expand_inventory_cell(obs, cell_idx, expanded); + const float* cell_obs = expanded; + int is_gear_cell = cell_obs[TEST_INV_OBS_ROLE_ARMOR] != 0.0f || + cell_obs[TEST_INV_OBS_ROLE_WEAPON] != 0.0f; + float kind = is_gear_cell ? 0.0f : test_any_inventory_kind_bit(cell_obs); + float effect = is_gear_cell ? test_binary_float( cell_obs[TEST_INV_OBS_EFFECT_LIFESTEAL] != 0.0f || cell_obs[TEST_INV_OBS_EFFECT_DAMAGE_AMP] != 0.0f || cell_obs[TEST_INV_OBS_EFFECT_DEFENSIVE] != 0.0f || - cell_obs[TEST_INV_OBS_EFFECT_UTIL] != 0.0f); + cell_obs[TEST_INV_OBS_EFFECT_UTIL] != 0.0f) : 0.0f; return (TestDroppedInventoryFields){ - .is_gear = test_binary_float( - cell_obs[TEST_INV_OBS_ROLE_ARMOR] != 0.0f || - cell_obs[TEST_INV_OBS_ROLE_WEAPON] != 0.0f), + .is_gear = test_binary_float(is_gear_cell), .is_consumable = kind, .can_use = test_click_mask_for_cell_s(s, mask, cell_idx), .has_effect = effect, @@ -728,18 +762,17 @@ static void test_draft_offer_and_select(void) { col_spawn_wave(&s); CHECK("modifier persists across waves", col_mod_active(&s, (ColoModifier)chosen)); - int rfdd_late = 0, rfdd_window = 0, boss_excluded_seen = 0; + int rfdd_before_window = 0, rfdd_in_window = 0, boss_excluded_seen = 0; for (int rep = 0; rep < 400; rep++) { - int late_wave = 7 + rep % 4; - col_modifier_open_draft(&s, late_wave); + col_modifier_open_draft(&s, rep % COLO_RFDD_FIRST_DRAFT_WAVE); for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { int m = s.modifiers.draft_options[o]; - if (m == COLO_MOD_RED_FLAG || m == COLO_MOD_DYNAMIC_DUO) rfdd_late = 1; + if (m == COLO_MOD_RED_FLAG || m == COLO_MOD_DYNAMIC_DUO) rfdd_before_window = 1; } - col_modifier_open_draft(&s, 2 + rep % 5); + col_modifier_open_draft(&s, COLO_RFDD_FIRST_DRAFT_WAVE + rep % 5); for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { int m = s.modifiers.draft_options[o]; - if (m == COLO_MOD_RED_FLAG || m == COLO_MOD_DYNAMIC_DUO) rfdd_window = 1; + if (m == COLO_MOD_RED_FLAG || m == COLO_MOD_DYNAMIC_DUO) rfdd_in_window = 1; } col_modifier_open_draft(&s, COLO_WAVE_BOSS); for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { @@ -748,11 +781,51 @@ static void test_draft_offer_and_select(void) { } } s.modifiers.draft_pending = 0; - CHECK("Red Flag / Dynamic Duo never offered into wave 8+", rfdd_late == 0); - CHECK("Red Flag / Dynamic Duo do appear in drafts before wave 7", rfdd_window == 1); + CHECK("Red Flag / Dynamic Duo are not offered before the wave-7 draft", + rfdd_before_window == 0); + CHECK("Red Flag / Dynamic Duo appear from the wave-7 draft through wave 11", + rfdd_in_window == 1); CHECK("the wave-12 draft excludes RF/DD/Mantimayhem/Reentry", boss_excluded_seen == 0); } +static void test_first_draft_is_fixed_trio(void) { + printf("test_first_draft_is_fixed_trio\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + + int all_trios_exact = 1; + int order_varied = 0; + int first_order[COLO_MODIFIER_DRAFT_OPTIONS] = { -1, -1, -1 }; + for (int rep = 0; rep < 200; rep++) { + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 1234u + (uint32_t)rep); + + int seen_relentless = 0, seen_blasphemy = 0, seen_frailty = 0; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + int m = s.modifiers.draft_options[o]; + if (m == COLO_MOD_RELENTLESS) seen_relentless = 1; + else if (m == COLO_MOD_BLASPHEMY) seen_blasphemy = 1; + else if (m == COLO_MOD_FRAILTY) seen_frailty = 1; + else all_trios_exact = 0; + } + if (!(seen_relentless && seen_blasphemy && seen_frailty)) all_trios_exact = 0; + + if (rep == 0) { + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + first_order[o] = s.modifiers.draft_options[o]; + } else { + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + if (s.modifiers.draft_options[o] != first_order[o]) order_varied = 1; + } + } + + CHECK("the wave-1 draft is always exactly Relentless/Blasphemy/Frailty", + all_trios_exact == 1); + CHECK("the wave-1 draft is pending on reset", s.modifiers.draft_pending == 1); + CHECK("the wave-1 draft slot order is not a fixed constant", order_varied == 1); +} + static void test_draft_upgrade_bias(void) { printf("test_draft_upgrade_bias\n"); ColosseumContext ctx; @@ -1006,6 +1079,13 @@ static void test_bees_hazard(void) { draft_is_open(&sc) && sc.wave_spawn_target == 1); } +static int count_live_totems(const ColosseumState* s) { + int n = 0; + for (int t = 0; t < COLO_MAX_TOTEMS; t++) + if (s->totems[t].phase != COLO_HAZARD_NONE) n++; + return n; +} + static void test_totem_lifecycle(void) { printf("test_totem_lifecycle\n"); ColosseumContext ctx; @@ -1034,15 +1114,38 @@ static void test_totem_lifecycle(void) { col_mod_on_npc_hp_changed(&s, 0); CHECK("no duplicate totem for the same owner", s.totems[0].phase == COLO_HAZARD_ALIVE); - for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL - 1; t++) col_mod_tick_totems(&s); - CHECK("no heal before the 7th tick", s.npcs[0].hp == 60); + CHECK("the totem spawns on the owner's south-west tile", + s.npcs[tslot].x == s.npcs[0].x - 1 && s.npcs[tslot].y == s.npcs[0].y - 1); + + int heal = s.npcs[0].max_hp * COLO_TOTEM_HEAL_PCT / 100; + for (int t = 0; t < COLO_TOTEM_SPAWN_HEAL_DELAY + COLO_TOTEM_PROJECTILE_TICKS - 1; t++) + col_mod_tick_totems(&s); + CHECK("no heal lands before the spawn delay plus projectile flight", + s.npcs[0].hp == 60); col_mod_tick_totems(&s); - CHECK("the 7th tick heals 30% of the owner's max HP", s.npcs[0].hp == 60 + 37); + CHECK("the first heal restores 30% of the owner's max HP", + s.npcs[0].hp == 60 + heal); for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); - CHECK("the pulse is gated while the owner is above 50%", s.npcs[0].hp == 97); + CHECK("heals land every 7 ticks and continue above 50%", + s.npcs[0].hp == 60 + 2 * heal || s.npcs[0].hp == s.npcs[0].max_hp); + for (int t = 0; t < 4 * COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + CHECK("the totem heals the owner back to full and then stops", + s.npcs[0].hp == s.npcs[0].max_hp); + s.npcs[0].hp = 50; + for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL - COLO_TOTEM_PROJECTILE_TICKS; t++) + col_mod_tick_totems(&s); + CHECK("a heal projectile is in flight before it lands", + s.totems[0].projectile_timer > 0 && s.npcs[0].hp == 50); + col_player_attack_target(&s, tslot); + land_pending_player_hits(&s); for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); - CHECK("the pulse resumes once the owner re-crosses 50%", s.npcs[0].hp == 87); + CHECK("destroying the totem cancels the in-flight heal", + s.npcs[0].hp == 50 && s.totems[0].phase == COLO_HAZARD_RESPAWNING); + s.totems[0].phase = COLO_HAZARD_ALIVE; + s.totems[0].npc_slot = tslot; + s.npcs[tslot].active = 1; + s.npcs[tslot].hp = 1; col_player_attack_target(&s, tslot); land_pending_player_hits(&s); @@ -1094,18 +1197,24 @@ static void test_totemic_sol_wave12(void) { s.npcs[sol].hp = COLO_SOL_HP_MAX * 60 / 100; col_mod_on_npc_hp_changed(&s, sol); - CHECK("no totem while Sol is above 50%", s.totems[sol].phase == COLO_HAZARD_NONE); + CHECK("no totem while Sol is above 50%", col_totem_for_owner(&s, sol) == NULL); s.npcs[sol].hp = COLO_SOL_HP_MAX / 2; col_mod_on_npc_hp_changed(&s, sol); - CHECK("Sol at 50% spawns a totem", s.totems[sol].phase == COLO_HAZARD_ALIVE); - int tslot = s.totems[sol].npc_slot; + ColoTotem* sol_totem = col_totem_for_owner(&s, sol); + CHECK("Sol at 50% spawns a totem", + sol_totem && sol_totem->phase == COLO_HAZARD_ALIVE); + int tslot = sol_totem->npc_slot; CHECK("Sol's totem is an attackable 1-HP NPC inside the boss arena", s.npcs[tslot].type == COLO_HEALING_TOTEM && s.npcs[tslot].hp == 1 && col_in_boss_arena(&s, s.npcs[tslot].x, s.npcs[tslot].y)); + CHECK("the first Sol totem arms the two-minute extra-totem timer", + s.sol_totem_spawn_timer == COLO_TOTEM_SOL_EXTRA_INTERVAL); int hp0 = s.npcs[sol].hp; - for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL - 1; t++) col_mod_tick_totems(&s); - CHECK("no Sol heal before the 7th tick", s.npcs[sol].hp == hp0); + for (int t = 0; t < COLO_TOTEM_SPAWN_HEAL_DELAY + COLO_TOTEM_PROJECTILE_TICKS - 1; t++) + col_mod_tick_totems(&s); + CHECK("no Sol heal before the spawn delay plus projectile flight", + s.npcs[sol].hp == hp0); col_mod_tick_totems(&s); CHECK("the pulse heals Sol exactly 75", s.npcs[sol].hp == hp0 + COLO_TOTEM_SOL_HEAL); for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); @@ -1116,9 +1225,208 @@ static void test_totemic_sol_wave12(void) { land_pending_player_hits(&s); int hp1 = s.npcs[sol].hp; for (int t = 0; t < 3 * COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + sol_totem = col_totem_for_owner(&s, sol); CHECK("a destroyed totem stops the Sol heal (until the 200t respawn)", !s.npcs[tslot].active && s.npcs[sol].hp == hp1 && - s.totems[sol].phase == COLO_HAZARD_RESPAWNING); + sol_totem && sol_totem->phase == COLO_HAZARD_RESPAWNING); +} + +static void test_obs_signal_defects(void) { + printf("test_obs_signal_defects\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 1; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 5150); + + CHECK("reset leaves no boss index, so pre-boss waves cannot read one", + s.sol.boss_idx == -1); + + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.modifiers.active_mask |= (1u << COLO_MOD_TOTEMIC) | (1u << COLO_MOD_BEES); + s.modifiers.tier[COLO_MOD_TOTEMIC] = 1; + s.modifiers.tier[COLO_MOD_BEES] = 1; + s.player.x = 12; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + + static float obs[COLO_NUM_OBS]; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + int boss_base = COLO_OBS_AFTER_MODS + COLO_WAVE_OBS_SIZE; + int boss_rel_x = boss_base + COLO_SOL_NUM_PHASES + 7; + CHECK("pre-boss waves emit no stray boss-relative position", + obs[boss_rel_x] == 0.0f && obs[boss_rel_x + 1] == 0.0f); + + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 16); + s.npcs[0].hp = 60; + col_mod_on_npc_hp_changed(&s, 0); + int tslot = col_totem_for_owner(&s, 0)->npc_slot; + s.npcs[tslot].x = s.player.x + 1; + s.npcs[tslot].y = s.player.y; + + col_mod_sync_bees(&s); + int bslot = s.bees[0].npc_slot; + s.npcs[bslot].x = s.player.x; + s.npcs[bslot].y = s.player.y + 1; + + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + int in_range_off = COLO_NPC_TYPE_CODE_FEATURES + 3 + 1; + int totem_slot = test_obs_slot_for_npc(&s, tslot); + int bee_slot = test_obs_slot_for_npc(&s, bslot); + int shaman_slot = test_obs_slot_for_npc(&s, 0); + CHECK("an adjacent totem does not claim to threaten the player", + obs[COLO_OBS_AFTER_EQUIPPED_SELF + + totem_slot * COLO_FEATURES_PER_NPC + in_range_off] == 0.0f); + CHECK("a bee adjacent to the player does not claim an attack range", + obs[COLO_OBS_AFTER_EQUIPPED_SELF + + bee_slot * COLO_FEATURES_PER_NPC + in_range_off] == 0.0f); + CHECK("a real attacker in range still reports it", + obs[COLO_OBS_AFTER_EQUIPPED_SELF + + shaman_slot * COLO_FEATURES_PER_NPC + in_range_off] == 1.0f); +} + +static void test_totem_heal_timing_obs(void) { + printf("test_totem_heal_timing_obs\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 1; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.modifiers.active_mask |= (1u << COLO_MOD_TOTEMIC); + s.modifiers.tier[COLO_MOD_TOTEMIC] = 1; + s.player.x = 25; s.player.y = 18; + col_rebuild_player_collision_flags(&s); + + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 12, 16); + s.npcs[0].hp = 60; + col_mod_on_npc_hp_changed(&s, 0); + int tslot = s.totems[0].npc_slot; + CHECK("fixture: totem spawned", tslot >= 0); + + static float obs[COLO_NUM_OBS]; + int totem_base, owner_base; + + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + totem_base = COLO_OBS_AFTER_EQUIPPED_SELF + + test_obs_slot_for_npc(&s, tslot) * COLO_FEATURES_PER_NPC; + owner_base = COLO_OBS_AFTER_EQUIPPED_SELF + + test_obs_slot_for_npc(&s, 0) * COLO_FEATURES_PER_NPC; + int launch_idx = totem_base + COLO_NPC_TELLS_OFFSET + 0; + int flight_idx = totem_base + COLO_NPC_TELLS_OFFSET + 1; + int heal_idx = totem_base + COLO_NPC_TELLS_OFFSET + 2; + int owner_dx_idx = totem_base + COLO_NPC_TELLS_OFFSET + 3; + int owner_dy_idx = totem_base + COLO_NPC_TELLS_OFFSET + 4; + + CHECK("a fresh totem shows a pending launch and no projectile in flight", + obs[launch_idx] > 0.0f && obs[flight_idx] == 0.0f); + CHECK("the pending heal is 30% of a non-Sol owner's max HP", + fabsf(obs[heal_idx] - 0.30f) < 0.02f); + CHECK("the totem carries its owner's relative position", + obs[owner_dx_idx] != 0.0f || obs[owner_dy_idx] != 0.0f); + CHECK("a serpent shaman writes no totem tells", + obs[owner_base + COLO_NPC_TELLS_OFFSET + 0] == 0.0f && + obs[owner_base + COLO_NPC_TELLS_OFFSET + 1] == 0.0f && + obs[owner_base + COLO_NPC_TELLS_OFFSET + 2] == 0.0f); + CHECK("the totem features cost no extra record width", + COLO_FEATURES_PER_NPC == 23); + + for (int t = 0; t < COLO_TOTEM_SPAWN_HEAL_DELAY; t++) col_mod_tick_totems(&s); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("once launched the projectile is visible and the launch cue clears", + s.totems[0].projectile_timer > 0 && + obs[flight_idx] > 0.0f && obs[launch_idx] == 0.0f); + + float flight_before = obs[flight_idx]; + col_mod_tick_totems(&s); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("the in-flight cue counts down toward the landing tick", + obs[flight_idx] < flight_before && obs[flight_idx] > 0.0f); + + int hazard_base = COLO_OBS_AFTER_NPCS + COLO_MODIFIER_FLAGS_OBS_SIZE; + int sol_timer_idx = hazard_base + COLO_MODIFIER_HAZARD_OBS_SIZE - 2; + int count_idx = hazard_base + COLO_MODIFIER_HAZARD_OBS_SIZE - 1; + CHECK("one live totem is reported in the hazard block", + fabsf(obs[count_idx] - 1.0f / (float)COLO_TOTEM_OBS_COUNT_NORM) < 1e-6f); + CHECK("no Sol extra-totem timer outside wave 12", obs[sol_timer_idx] == 0.0f); +} + +static void test_totem_sol_obs_reports_stacking(void) { + printf("test_totem_sol_obs_reports_stacking\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 11; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4243); + advance_to_wave_spawn(&s, &ctx); + s.modifiers.active_mask |= (1u << COLO_MOD_TOTEMIC); + s.modifiers.tier[COLO_MOD_TOTEMIC] = 1; + int sol = col_sol_find_idx(&s); + s.npcs[sol].hp = COLO_SOL_HP_MAX / 2; + col_mod_on_npc_hp_changed(&s, sol); + + static float obs[COLO_NUM_OBS]; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + int hazard_base = COLO_OBS_AFTER_NPCS + COLO_MODIFIER_FLAGS_OBS_SIZE; + int sol_timer_idx = hazard_base + COLO_MODIFIER_HAZARD_OBS_SIZE - 2; + int count_idx = hazard_base + COLO_MODIFIER_HAZARD_OBS_SIZE - 1; + + CHECK("the Sol extra-totem countdown is observable once armed", + obs[sol_timer_idx] > 0.0f); + float armed = obs[sol_timer_idx]; + + int tslot = col_totem_for_owner(&s, sol)->npc_slot; + int tbase = COLO_OBS_AFTER_EQUIPPED_SELF + + test_obs_slot_for_npc(&s, tslot) * COLO_FEATURES_PER_NPC; + CHECK("a Sol totem's pending heal reads as 75 of Sol's max HP", + fabsf(obs[tbase + COLO_NPC_TELLS_OFFSET + 2] - + (float)COLO_TOTEM_SOL_HEAL / (float)COLO_SOL_HP_MAX) < 1e-4f); + + for (int t = 0; t < COLO_TOTEM_SOL_EXTRA_INTERVAL; t++) col_mod_tick_totems(&s); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("a second stacked totem raises the observed live count", + count_live_totems(&s) == 2 && + fabsf(obs[count_idx] - 2.0f / (float)COLO_TOTEM_OBS_COUNT_NORM) < 1e-6f); + CHECK("the countdown rearms after the extra totem spawns", + obs[sol_timer_idx] > 0.0f && fabsf(obs[sol_timer_idx] - armed) < 0.05f); +} + +static void test_totemic_sol_extra_totems_every_two_minutes(void) { + printf("test_totemic_sol_extra_totems_every_two_minutes\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 11; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 227); + advance_to_wave_spawn(&s, &ctx); + s.modifiers.active_mask |= (1u << COLO_MOD_TOTEMIC); + s.modifiers.tier[COLO_MOD_TOTEMIC] = 1; + int sol = col_sol_find_idx(&s); + CHECK("Sol is live", sol >= 0); + + s.npcs[sol].hp = COLO_SOL_HP_MAX / 2; + col_mod_on_npc_hp_changed(&s, sol); + + int totems_after_first = count_live_totems(&s); + CHECK("exactly one totem at the 50% trigger", totems_after_first == 1); + + for (int t = 0; t < COLO_TOTEM_SOL_EXTRA_INTERVAL - 1; t++) col_mod_tick_totems(&s); + CHECK("no extra totem one tick early", count_live_totems(&s) == 1); + col_mod_tick_totems(&s); + CHECK("a second totem spawns two minutes after the first", + count_live_totems(&s) == 2); + + for (int t = 0; t < COLO_TOTEM_SOL_EXTRA_INTERVAL; t++) col_mod_tick_totems(&s); + CHECK("a third totem spawns two minutes later", count_live_totems(&s) == 3); + + s.npcs[sol].hp = 0; + col_apply_npc_death(&s, sol); + CHECK("Sol's death despawns every totem it owned", count_live_totems(&s) == 0); } static void test_reentry_sand_tiles(void) { @@ -1191,14 +1499,69 @@ static void test_reentry_sand_tiles(void) { CHECK("Reentry T3 pools are PERMANENT (survive wave end)", s.molten_count == 3); s.molten_count = 0; + s.modifiers.tier[COLO_MOD_REENTRY] = 1; s.modifiers.active_mask |= (1u << COLO_MOD_VOLATILITY); s.modifiers.tier[COLO_MOD_VOLATILITY] = 3; s.player.x = 5; s.player.y = 18; col_mod_volatility_on_death(&s, 20, 16, 1); - CHECK("Volatility T3 leaves a temporary Volatility pool at the centre", + CHECK("Volatility T3 leaves a Volatility pool at the centre", s.molten_count == 1 && s.molten_kind[0] == COLO_POOL_VOLATILITY); + s.player.x = 20; + s.player.y = 16; + s.player.current_hitpoints = 99; + float javelin_typeless_before = + s.log.typeless_damage_by_type[COLO_JAVELIN_COLOSSUS]; + col_mod_tick_molten_pools(&s); + CHECK("Volatility pool damage is not attributed to Javelin", + s.player.current_hitpoints < 99 && + s.log.typeless_damage_by_type[COLO_JAVELIN_COLOSSUS] == + javelin_typeless_before); col_modifiers_on_wave_spawn(&s); - CHECK("the Volatility (temporary) pool clears at wave end", s.molten_count == 0); + CHECK("under Reentry I the Volatility pool is temporary and clears at wave end", + s.molten_count == 0); + + s.modifiers.tier[COLO_MOD_REENTRY] = 2; + col_mod_volatility_on_death(&s, 20, 16, 1); + CHECK("Reentry II makes the Volatility III pool permanent", + s.molten_count == 1 && s.molten_lifetime[0] == COLO_POOL_PERMANENT); + col_modifiers_on_wave_spawn(&s); + CHECK("that permanent Volatility pool survives wave end with its kind intact", + s.molten_count == 1 && s.molten_kind[0] == COLO_POOL_VOLATILITY); + + s.molten_count = 0; + s.modifiers.tier[COLO_MOD_REENTRY] = 3; + col_mod_volatility_on_death(&s, 20, 16, 1); + CHECK("Reentry III also makes the Volatility III pool permanent", + s.molten_count == 1 && s.molten_lifetime[0] == COLO_POOL_PERMANENT); +} + +static void test_mantimayhem_venom_cured_at_wave_end(void) { + printf("test_mantimayhem_venom_cured_at_wave_end\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 41); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave = 0; + s.wave_spawn_delay = 0; + s.wave_ready_delay = 0; + s.reinforcement_timer = COLO_REINFORCEMENT_TICKS; + s.player.x = 12; + s.player.y = 16; + s.modifiers.active_mask |= (1u << COLO_MOD_MANTIMAYHEM); + s.modifiers.tier[COLO_MOD_MANTIMAYHEM] = 2; + + s.player_venom = COLO_VENOM_START; + s.player_venom_timer = COLO_VENOM_INTERVAL; + CHECK("rig sanity: the player is venomed mid-wave", s.player_venom > 0); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + step_and_observe(&s, &ctx, idle); + CHECK("rig sanity: the empty wave clears", s.tick_scratch.wave_completed == 1); + CHECK("Mantimayhem venom is cured at the end of each wave", + s.player_venom == 0 && s.player_venom_timer == 0); } static void test_venom_escalation(void) { @@ -1625,7 +1988,7 @@ static void test_modifier_hazard_obs_fixes(void) { jv->skyfall_tile_y = 14; jv->skyfall_timer = 2; col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - int tells = COLO_OBS_AFTER_EQUIPPED_SELF + TEST_NPC_TELLS_OFFSET; + int tells = COLO_OBS_AFTER_EQUIPPED_SELF + COLO_NPC_TELLS_OFFSET; CHECK("javelin skyfall tells expose landing dx while pending", fabsf(obs[tells + 2] - col_obs_rel_x(19, s.player.x)) < 0.000001f); CHECK("javelin skyfall tells expose landing dy while pending", @@ -1644,7 +2007,7 @@ static void test_modifier_hazard_obs_fixes(void) { .move_timer = 1, }; col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - tells = COLO_OBS_AFTER_EQUIPPED_SELF + TEST_NPC_TELLS_OFFSET; + tells = COLO_OBS_AFTER_EQUIPPED_SELF + COLO_NPC_TELLS_OFFSET; CHECK("bee tells expose a nonzero move timer", obs[tells] > 0.0f && obs[tells] <= 1.0f); CHECK("bee tells expose next-step contact", @@ -1653,6 +2016,22 @@ static void test_modifier_hazard_obs_fixes(void) { fabsf(obs[tells + 3] - col_obs_rel_y(10, s.player.y)) < 0.000001f && obs[tells + 4] == 1.0f); + s.player_venom = 8; + s.player_venom_timer = 17; + s.player_poison = 4; + s.player_poison_timer = 23; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("venom observation is a compact severity and timer pair", + fabsf(obs[TEST_MOD_OBS_VENOM_SEVERITY] - + 8.0f / (float)COLO_VENOM_CAP) < 0.000001f && + fabsf(obs[TEST_MOD_OBS_VENOM_TIMER] - + 17.0f / (float)COLO_VENOM_INTERVAL) < 0.000001f); + CHECK("bee poison observation exposes remaining hits and time to tick", + fabsf(obs[TEST_MOD_OBS_POISON_SEVERITY] - + 4.0f / (float)COLO_POISON_BEE_CONTACT_SEVERITY) < 0.000001f && + fabsf(obs[TEST_MOD_OBS_POISON_TIMER] - + 23.0f / (float)COLO_POISON_INTERVAL) < 0.000001f); + init_forecast_test_state(&s, &ctx, 303, 10, 10); s.molten_count = 6; s.molten_x[0] = 20; s.molten_y[0] = 20; @@ -1661,6 +2040,10 @@ static void test_modifier_hazard_obs_fixes(void) { s.molten_x[3] = 10; s.molten_y[3] = 13; s.molten_x[4] = 8; s.molten_y[4] = 8; s.molten_x[5] = 10; s.molten_y[5] = 14; + for (int pool = 0; pool < s.molten_count; pool++) { + s.molten_kind[pool] = COLO_POOL_VOLATILITY; + s.molten_lifetime[pool] = COLO_POOL_TEMPORARY; + } col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); int molten = TEST_MOD_OBS_MOLTEN; CHECK("modifier molten obs lists the nearest pool first", @@ -1706,6 +2089,15 @@ static void test_modifier_hazard_obs_fixes(void) { col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); CHECK("Doom lethality obs flips at cap minus one", obs[TEST_MOD_OBS_DOOM_LETHAL] == 1.0f); + s.molten_count = 1; + s.molten_x[0] = s.player.x; + s.molten_y[0] = s.player.y; + s.molten_kind[0] = COLO_POOL_REENTRY; + s.molten_lifetime[0] = COLO_POOL_PERMANENT; + s.molten_burn_timer[0] = 1; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("Doom pending ignores a Reentry pool on burn cooldown", + obs[TEST_MOD_OBS_DOOM_PENDING] == 0.0f); s.player_venom = COLO_VENOM_START; s.player_venom_timer = COLO_VENOM_INTERVAL / 2; @@ -2218,7 +2610,8 @@ static void test_player_walks_through_npc_footprint(void) { col_rebuild_player_collision_flags(&s); int gx, gy; int npc_flag = col_grid_index(17, 16, &gx, &gy) && s.npc_collision_flags[gx][gy]; - int player_flag = col_grid_index(17, 16, &gx, &gy) && s.player_collision_flags[gx][gy]; + int player_flag = col_grid_index(17, 16, &gx, &gy) && + gx == s.player_grid_x && gy == s.player_grid_y; ColoWalkCtx wc = { .s = &s, .ctx = &ctx }; CHECK("NPC footprint remains stamped for NPC systems", npc_flag != 0); CHECK("NPC footprint is not stamped as player collision", player_flag == 0); @@ -2330,20 +2723,85 @@ static void test_warband_move_skip(void) { while ((s.wave_ready_delay > 0 || s.wave_attack_delay > 1) && !s.episode_over) step_and_observe(&s, &ctx, idle); - int attacks = 0; + int attacks_while_moving = 0; + int attacks_once_chasing = 0; int moved_every_tick = 1; for (int t = 0; t < 14 && !s.episode_over; t++) { s.player.current_hitpoints = 9999; step_and_observe(&s, &ctx, walk_south); if (!s.tick_scratch.player_moved) moved_every_tick = 0; - for (int sp = 0; sp < COLO_MAX_NPCS; sp++) - if (s.npcs[sp].active && col_type_is_warbander(s.npcs[sp].type) && - s.npcs[sp].attacked_this_tick) attacks++; + for (int sp = 0; sp < COLO_MAX_NPCS; sp++) { + const ColoNPC* npc = &s.npcs[sp]; + if (!npc->active || !col_type_is_warbander(npc->type)) continue; + if (!npc->attacked_this_tick) continue; + if (npc->moved_this_tick || npc->moved_last_tick) attacks_while_moving++; + if (t > 0) attacks_once_chasing++; + } } CHECK("the scripted stutter-step actually moved every tick", moved_every_tick); - CHECK("warband fired zero attacks across the stutter-step run", attacks == 0); - CHECK("zero warband damage across the stutter-step run", - s.log.total_damage_received == 0.0f); + CHECK("no warbander attacked on a tick it moved on or moved the tick before", + attacks_while_moving == 0); + /* t == 0 is excluded: the berserker starts stationary and adjacent, and NPCs + process before players, so its attack is calculated before the first step. */ + CHECK("once the warband is chasing the stutter-step suppresses every attack", + attacks_once_chasing == 0); +} + +/** Regression: warband melee is calculated before the player tick, so a same-tick + step-out cannot dodge it. */ +static void test_warband_melee_not_dodged_by_same_tick_step_out(void) { + printf("test_warband_melee_not_dodged_by_same_tick_step_out\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + + int attacked_standing = 0; + int attacked_stepping = 0; + for (int step_out = 0; step_out <= 1; step_out++) { + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 53); + advance_to_wave_spawn(&s, &ctx); + complete_open_draft(&s, &ctx, 1); + wb_isolate_warband(&s); + int berserker = wb_find_npc(&s, COLO_FREMENNIK_BERSERKER); + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s.npcs[i].active && i != berserker) col_deactivate_npc(&s, i); + wb_move_npc(&s, berserker, s.player.x, s.player.y + 1); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int step_east[COLO_NUM_ACTION_HEADS] = {0}; + step_east[COLO_HEAD_PRIMARY] = 7; + + while ((s.wave_ready_delay > 0 || s.wave_attack_delay > 1) && !s.episode_over) + step_and_observe(&s, &ctx, idle); + + int px = s.player.x, py = s.player.y; + for (int t = 0; t < 12 && !s.episode_over; t++) { + const ColoNPC* npc = &s.npcs[berserker]; + int anchor = s.warband_cycle_anchor; + int next_phase = anchor < 0 ? -1 + : (s.tick + 1 - anchor) % COLO_WARBAND_CYCLE_TICKS; + int fires_now = + next_phase == col_warband_window_offset(COLO_FREMENNIK_BERSERKER) && + !npc->moved_this_tick && !npc->moved_last_tick && + col_warband_melee_adjacent(&s, npc); + + s.player.current_hitpoints = 9999; + step_and_observe(&s, &ctx, fires_now && step_out ? step_east : idle); + + if (fires_now) { + if (step_out) attacked_stepping = npc->attacked_this_tick; + else attacked_standing = npc->attacked_this_tick; + break; + } + s.player.x = px; + s.player.y = py; + } + } + + CHECK("stationary player takes the warband melee", attacked_standing == 1); + CHECK("same-tick step-out does not dodge the warband melee", attacked_stepping == 1); } static uint64_t wb_trajectory_hash(ColosseumState* s, ColosseumContext* ctx, @@ -2788,66 +3246,6 @@ static void test_manticore_telegraph_during_windup(void) { mc->fixed_orb_style[0] != ATTACK_STYLE_NONE); } -static void test_prayer_oracle_manticore_orbs(void) { - printf("test_prayer_oracle_manticore_orbs\n"); - ColosseumContext ctx; - col_init_context_typed(&ctx); - ColosseumState s; - memset(&s, 0, sizeof(s)); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 79); - advance_to_wave_spawn(&s, &ctx); - geo_clear_npcs(&s); - s.wave_ready_delay = 0; - s.wave_attack_delay = 0; - s.player.x = 17; s.player.y = 16; - col_rebuild_player_collision_flags(&s); - col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); - ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); - - s.npcs[0].attack_timer = 1; - mc->cycle_step = -1; - mc->fixed_orb_style[0] = ATTACK_STYLE_MAGIC; - mc->fixed_orb_style[1] = ATTACK_STYLE_RANGED; - mc->fixed_orb_style[2] = ATTACK_STYLE_MELEE; - s.player.prayer = PRAYER_NONE; - col_apply_prayer_oracle(&s); - CHECK("oracle prays orb 0's style on the charge-complete tick (magic, not default ranged)", - s.player.prayer == PRAYER_PROTECT_MAGIC); - - for (int o = 0; o < 3; o++) mc->orb_style[o] = mc->fixed_orb_style[o]; - mc->cycle_step = 1; - s.player.prayer = PRAYER_NONE; - col_apply_prayer_oracle(&s); - CHECK("oracle prays the in-flight orb 1 style (ranged)", - s.player.prayer == PRAYER_PROTECT_RANGED); - mc->cycle_step = 2; - s.player.prayer = PRAYER_NONE; - col_apply_prayer_oracle(&s); - CHECK("oracle prays the in-flight orb 2 style (melee, not default ranged)", - s.player.prayer == PRAYER_PROTECT_MELEE); - - mc->cycle_step = -1; - for (int o = 0; o < 3; o++) mc->orb_style[o] = ATTACK_STYLE_NONE; - s.npcs[0].attack_timer = 5; - s.player.prayer = PRAYER_PROTECT_MELEE; - col_apply_prayer_oracle(&s); - CHECK("oracle leaves prayer untouched with no thrower this tick", - s.player.prayer == PRAYER_PROTECT_MELEE); - - s.npcs[0].attack_timer = 2; - for (int t = 0; t < 36; t++) { - s.player.current_hitpoints = 99; - col_apply_prayer_oracle(&s); - col_npc_attack_ctx(&s, &ctx, 0); - } - float faced = s.log.pray_faced_by_type[COLO_MANTICORE]; - float correct = s.log.pray_correct_by_type[COLO_MANTICORE]; - CHECK("solo-manticore barrages under the oracle: every orb faced", - faced >= 9.0f); - CHECK("solo-manticore barrages under the oracle: every orb prayed", - correct == faced); -} - static int late_start_total_doses(const ColosseumState* s) { int total = 0; for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) @@ -3307,6 +3705,109 @@ static void test_javelin_skyfall_no_defence_gate(void) { s.player.current_hitpoints == 99 && jv->skyfall_pending == 0); } +#define SKYFALL_DODGE_FORCED_DAMAGE 38 + +static void skyfall_dodge_init_state( + ColosseumState* s, + ColosseumContext* ctx, + uint32_t seed +) { + col_init_context_typed(ctx); + ctx->config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY; + ctx->config.beginner_loadout_fraction = 0.0f; + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); + geo_clear_npcs(s); + s->wave_spawn_delay = 0; + s->wave_ready_delay = 0; + s->modifiers.draft_pending = 0; + s->warband_cycle_anchor = s->tick; + s->player.x = 17; + s->player.y = 16; + s->player.current_hitpoints = 99; + s->player.attack_timer = 99; + s->player_dest_x = -1; + s->player_dest_y = -1; + col_apply_weapon_set(s, COLO_GEAR_RANGED); + encounter_pending_hit_queue_clear(&s->player_pending_hits); + col_rebuild_player_collision_flags(s); + col_init_npc(s, 0, COLO_JAVELIN_COLOSSUS, 20, 16); + s->npcs[0].stun_timer = 0; + s->npcs[0].frozen_ticks = 0; +} + +static int skyfall_dodge_move_action_off_tile( + const ColosseumState* s, + int marked_x, + int marked_y +) { + for (int action = 1; action < ENCOUNTER_MOVE_ACTIONS; action++) { + ColosseumState tmp = *s; + int moved = encounter_move_to_target( + &tmp.player, + ENCOUNTER_MOVE_TARGET_DX[action], + ENCOUNTER_MOVE_TARGET_DY[action], + col_player_walkable, + &tmp); + if (moved > 0 && (tmp.player.x != marked_x || tmp.player.y != marked_y)) + return action; + } + assert(0 && "no move action leaves the marked tile"); + return 0; +} + +static int skyfall_dodge_damage_after_wait(int wait_visible_ticks) { + ColosseumContext ctx; + ColosseumState s; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + skyfall_dodge_init_state(&s, &ctx, 0x5100u + (uint32_t)wait_visible_ticks); + + ColoJavelinState* jv = colo_npc_javelin(&s.npcs[0]); + jv->attack_count = 4; + s.npcs[0].attack_timer = 0; + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + assert(jv->skyfall_pending == 1 && + jv->skyfall_timer == COLO_JAVELIN_SKYFALL_DELAY); + jv->skyfall_damage = SKYFALL_DODGE_FORCED_DAMAGE; + + int move_action = skyfall_dodge_move_action_off_tile( + &s, jv->skyfall_tile_x, jv->skyfall_tile_y); + int hp_before = s.player.current_hitpoints; + + for (int i = 0; i < wait_visible_ticks; i++) { + memset(actions, 0, sizeof(actions)); + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + } + + if (colo_npc_javelin(&s.npcs[0])->skyfall_pending) { + memset(actions, 0, sizeof(actions)); + actions[COLO_HEAD_PRIMARY] = move_action; + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + } + + while (colo_npc_javelin(&s.npcs[0])->skyfall_pending) { + memset(actions, 0, sizeof(actions)); + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + } + return hp_before - s.player.current_hitpoints; +} + +static void test_javelin_skyfall_dodge_lead_window(void) { + printf("test_javelin_skyfall_dodge_lead_window\n"); + int dodged_all = 1; + for (int wait = 0; wait < COLO_JAVELIN_SKYFALL_DELAY - 1; wait++) { + int visible_timer = COLO_JAVELIN_SKYFALL_DELAY - wait; + if (skyfall_dodge_damage_after_wait(wait) != 0) { + printf(" skyfall not dodged at visible timer %d\n", visible_timer); + dodged_all = 0; + } + } + CHECK("moving with skyfall timer >= 2 dodges the marked tile", dodged_all); + CHECK("moving with skyfall timer == 1 is too late", + skyfall_dodge_damage_after_wait(COLO_JAVELIN_SKYFALL_DELAY - 1) == + SKYFALL_DODGE_FORCED_DAMAGE); +} + static int sol_setup(ColosseumState* s, ColosseumContext* ctx, uint32_t seed) { col_init_context_typed(ctx); ctx->config.start_wave = 11; @@ -3375,6 +3876,44 @@ static void sol_pin(ColosseumState* s, int idx, int x, int y) { s->sol.immobile_ticks = 30000; } +static void test_sol_generic_observation_signals_are_neutral(void) { + printf("test_sol_generic_observation_signals_are_neutral\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 98, 16, 16); + col_init_npc(&s, 0, COLO_SOL_HEREDIT, 17, 16); + s.sol.boss_idx = 0; + s.sol.attack_delay = 0; + s.sol.immobile_ticks = 0; + s.npcs[0].attack_timer = 5; + + CHECK("fixture: Sol retains the generic melee metadata internally", + s.npcs[0].attack_style == ATTACK_STYLE_MELEE && + s.npcs[0].attack_timer == 5); + + static float obs[COLO_NUM_OBS]; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + int obs_slot = test_obs_slot_for_npc(&s, 0); + CHECK("Sol receives an NPC observation slot", obs_slot >= 0); + int npc_base = COLO_OBS_AFTER_EQUIPPED_SELF + + obs_slot * COLO_FEATURES_PER_NPC; + int timer_idx = npc_base + COLO_NUM_NPC_TYPES + 3; + int next_prayer_active_idx = timer_idx + 2; + CHECK("Sol's stale generic attack timer is hidden", + obs[timer_idx] == 0.0f); + CHECK("Sol has no generic next-prayer cue", + obs[next_prayer_active_idx] == 0.0f); + + int magic, ranged, melee; + col_live_threat_style_counts(&s, &magic, &ranged, &melee); + CHECK("adjacent Sol contributes no prayable live threat style", + magic == 0 && ranged == 0 && melee == 0); + CHECK("the global style-count observation stays neutral for Sol", + obs[COLO_OBS_AFTER_PENDING_HITS] == 0.0f && + obs[COLO_OBS_AFTER_PENDING_HITS + 1] == 0.0f && + obs[COLO_OBS_AFTER_PENDING_HITS + 2] == 0.0f); +} + static void test_sol_adjacency_gate_and_kiting(void) { printf("test_sol_adjacency_gate_and_kiting\n"); ColosseumContext ctx; @@ -3508,6 +4047,23 @@ static void test_sol_parry_schedule_and_damage(void) { ColosseumState s; int idle[COLO_NUM_ACTION_HEADS] = {0}; + CHECK("751 HP remains above the delayed-third-hit phase", + col_sol_phase_for_hp(751) == 2); + CHECK("750 HP enters the delayed-third-hit phase", + col_sol_phase_for_hp(750) == 3); + + int boundary_idx = sol_setup(&s, &ctx, 108); + s.npcs[boundary_idx].hp = 751; + s.sol.phase = col_sol_phase_for_hp(s.npcs[boundary_idx].hp); + col_sol_start_triple_parry(&s, boundary_idx); + CHECK("751 HP schedules the third hit at +9", + s.sol.parry_land_in[2] == 9); + s.npcs[boundary_idx].hp = 750; + s.sol.phase = col_sol_phase_for_hp(s.npcs[boundary_idx].hp); + col_sol_start_triple_parry(&s, boundary_idx); + CHECK("750 HP schedules the third hit at +10", + s.sol.parry_land_in[2] == 10); + for (int low = 0; low <= 1; low++) { int idx = sol_setup(&s, &ctx, 109 + (uint32_t)low); s.npcs[idx].hp = low ? (COLO_SOL_HP_MAX * 40) / 100 : (COLO_SOL_HP_MAX * 80) / 100; @@ -3516,6 +4072,8 @@ static void test_sol_parry_schedule_and_damage(void) { sol_move_player(&s, 12, 12); col_sol_start_triple_parry(&s, idx); s.player.current_hitpoints = 99; + float parry_damage_before = + s.log.sol_damage_by_source[COLO_SOL_DAMAGE_TRIPLE_PARRY]; int dmg_at[13] = {0}; int hp_prev = 99; @@ -3535,6 +4093,9 @@ static void test_sol_parry_schedule_and_damage(void) { if (t != 3 && t != 6 && t != h3 && dmg_at[t] != 0) clean = 0; CHECK("no parry damage lands off-schedule", clean); CHECK("the combo retires after the third hit", s.sol.parry_hits_left == 0); + CHECK("triple-parry damage has its own source total", + s.log.sol_damage_by_source[COLO_SOL_DAMAGE_TRIPLE_PARRY] == + parry_damage_before + 15.0f + (float)d2 + (float)d3); } } @@ -3548,33 +4109,143 @@ static void test_sol_parry_prayer_punish(void) { s.sol.attack_delay = 1000; sol_move_player(&s, 12, 12); - col_sol_start_triple_parry(&s, idx); - s.player.current_hitpoints = 99; - int actions[COLO_NUM_ACTION_HEADS] = {0}; - int deactivated_after_each = 1; - int prayer_correct_before = s.log.total_prayer_correct; - for (int t = 1; t <= 9; t++) { - actions[COLO_HEAD_PRAYER] = (t == 3 || t == 6 || t == 9) - ? COLO_OVERHEAD_MELEE : COLO_OVERHEAD_NO_CHANGE; - step_and_observe(&s, &ctx, actions); - if ((t == 3 || t == 6 || t == 9) && s.player.prayer != PRAYER_NONE) - deactivated_after_each = 0; + col_sol_start_triple_parry(&s, idx); + s.player.current_hitpoints = 99; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + int deactivated_after_each = 1; + int prayer_correct_before = s.log.total_prayer_correct; + for (int t = 1; t <= 9; t++) { + actions[COLO_HEAD_PRAYER] = (t == 3 || t == 6 || t == 9) + ? COLO_OVERHEAD_MELEE : COLO_OVERHEAD_NO_CHANGE; + step_and_observe(&s, &ctx, actions); + if ((t == 3 || t == 6 || t == 9) && s.player.prayer != PRAYER_NONE) + deactivated_after_each = 0; + } + CHECK("flicking Protect from Melee exactly at land blocks all three hits", + s.player.current_hitpoints == 99); + CHECK("every parry hit force-deactivates the overhead prayers", + deactivated_after_each); + CHECK("blocked parry hits count prayer_correct", + s.log.total_prayer_correct >= prayer_correct_before + 3); + + col_sol_start_triple_parry(&s, idx); + s.player.current_hitpoints = 99; + for (int t = 1; t <= 9; t++) { + actions[COLO_HEAD_PRAYER] = COLO_OVERHEAD_MELEE; + step_and_observe(&s, &ctx, actions); + } + CHECK("camping the prayer early makes every hit unblockable (75 total)", + s.player.current_hitpoints == 99 - (15 + 25 + 35)); +} + +static ColoSolParryCue test_sol_parry_cue_from_obs( + ColosseumState* s, + ColosseumContext* ctx, + int* ticks_to_land, + float* damage +) { + static float obs[COLO_NUM_OBS]; + col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); + int cue_base = COLO_OBS_AFTER_WAVE + COLO_BOSS_OBS_PARRY_CUE_OFFSET; + int ticks_base = COLO_OBS_AFTER_WAVE + COLO_BOSS_OBS_PARRY_TICKS_OFFSET; + int damage_idx = COLO_OBS_AFTER_WAVE + COLO_BOSS_OBS_PARRY_DAMAGE_OFFSET; + ColoSolParryCue cue = COLO_SOL_PARRY_CUE_INACTIVE; + int cue_count = 0; + *ticks_to_land = 0; + for (int c = 0; c < COLO_SOL_PARRY_CUE_COUNT; c++) { + if (obs[cue_base + c] != 1.0f) continue; + cue = (ColoSolParryCue)c; + cue_count++; + } + for (int tick = 1; tick <= COLO_SOL_PARRY_LAND_TICKS_MAX; tick++) { + if (obs[ticks_base + tick - 1] == 1.0f) *ticks_to_land = tick; + } + *damage = obs[damage_idx] * 45.0f; + CHECK("Sol parry cue is one-hot", cue_count == 1); + return cue; +} + +static int test_sol_parry_prayer_action( + const ColosseumState* s, + ColoSolParryCue cue +) { + if (cue == COLO_SOL_PARRY_CUE_FLICK_MELEE) return COLO_OVERHEAD_MELEE; + if ((cue == COLO_SOL_PARRY_CUE_OVERHEAD_OFF || + cue == COLO_SOL_PARRY_CUE_POISONED) && + s->player.prayer != PRAYER_NONE) + return COLO_OVERHEAD_OFF; + return COLO_OVERHEAD_NO_CHANGE; +} + +static void test_sol_parry_observation_contract(void) { + printf("test_sol_parry_observation_contract\n"); + ColosseumContext ctx; + ColosseumState s; + + for (int low = 0; low <= 1; low++) { + int idx = sol_setup(&s, &ctx, 117 + (uint32_t)low); + s.npcs[idx].hp = low ? 750 : 751; + s.sol.phase = col_sol_phase_for_hp(s.npcs[idx].hp); + s.sol.attack_delay = 1000; + sol_move_player(&s, 12, 12); + col_sol_start_triple_parry(&s, idx); + s.player.current_hitpoints = 99; + + int saw_lead_time = 0; + int saw_four_tick_countdown = 0; + int saw_third_hit_damage = 0; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + int total_ticks = low ? 10 : 9; + for (int t = 0; t < total_ticks; t++) { + int ticks_to_land = 0; + float damage = 0.0f; + ColoSolParryCue cue = + test_sol_parry_cue_from_obs(&s, &ctx, &ticks_to_land, &damage); + saw_lead_time |= cue == COLO_SOL_PARRY_CUE_LEAD_TIME; + saw_four_tick_countdown |= ticks_to_land == 4; + saw_third_hit_damage |= damage == (low ? 45.0f : 35.0f); + actions[COLO_HEAD_PRAYER] = test_sol_parry_prayer_action(&s, cue); + step_and_observe(&s, &ctx, actions); + } + CHECK("the observation-only parry policy blocks all three hits", + s.player.current_hitpoints == 99); + CHECK(low ? "the delayed third hit exposes lead time" + : "the normal third hit has no extra lead time", + saw_lead_time == low); + CHECK(low ? "the delayed third hit exposes a 4-tick one-hot" + : "the normal third hit never exposes a 4-tick one-hot", + saw_four_tick_countdown == low); + CHECK("the observation exposes the phase-correct third-hit damage", + saw_third_hit_damage); } - CHECK("flicking Protect from Melee exactly at land blocks all three hits", - s.player.current_hitpoints == 99); - CHECK("every parry hit force-deactivates the overhead prayers", - deactivated_after_each); - CHECK("blocked parry hits count prayer_correct", - s.log.total_prayer_correct >= prayer_correct_before + 3); + int idx = sol_setup(&s, &ctx, 119); + s.npcs[idx].hp = 751; + s.sol.phase = col_sol_phase_for_hp(s.npcs[idx].hp); + s.sol.attack_delay = 1000; + sol_move_player(&s, 12, 12); col_sol_start_triple_parry(&s, idx); + s.sol.overhead_history[s.tick % COLO_SOL_PRAYER_HISTORY] = 1; s.player.current_hitpoints = 99; - for (int t = 1; t <= 9; t++) { - actions[COLO_HEAD_PRAYER] = COLO_OVERHEAD_MELEE; + + int saw_poisoned = 0; + int saw_poisoned_damage = 0; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + for (int t = 0; t < 9; t++) { + int ticks_to_land = 0; + float damage = 0.0f; + ColoSolParryCue cue = + test_sol_parry_cue_from_obs(&s, &ctx, &ticks_to_land, &damage); + saw_poisoned |= cue == COLO_SOL_PARRY_CUE_POISONED; + saw_poisoned_damage |= + cue == COLO_SOL_PARRY_CUE_POISONED && damage == 15.0f; + actions[COLO_HEAD_PRAYER] = test_sol_parry_prayer_action(&s, cue); step_and_observe(&s, &ctx, actions); } - CHECK("camping the prayer early makes every hit unblockable (75 total)", - s.player.current_hitpoints == 99 - (15 + 25 + 35)); + CHECK("the observation exposes an already-poisoned next hit", saw_poisoned); + CHECK("the poisoned cue preserves next-hit damage", saw_poisoned_damage); + CHECK("a poisoned first hit does not hide recovery for hits two and three", + s.player.current_hitpoints == 99 - 15); } static void test_sol_grapple_perfect_parry(void) { @@ -3591,9 +4262,14 @@ static void test_sol_grapple_perfect_parry(void) { CHECK("the called slot is inside the 5-slot A12 domain", s.sol.grapple_body_slot >= 0 && s.sol.grapple_body_slot < COLO_NUM_GRAPPLE_SLOTS); s.player.current_hitpoints = 99; + float grapple_damage_before = + s.log.sol_damage_by_source[COLO_SOL_DAMAGE_GRAPPLE]; for (int t = 0; t < COLO_SOL_GRAPPLE_WINDOW; t++) step_and_observe(&s, &ctx, idle); int fail_dmg = 99 - s.player.current_hitpoints; CHECK("an unanswered grapple lands 20-44", fail_dmg >= 20 && fail_dmg <= 44); + CHECK("failed grapple damage has its own source total", + s.log.sol_damage_by_source[COLO_SOL_DAMAGE_GRAPPLE] == + grapple_damage_before + (float)fail_dmg); col_sol_start_grapple(&s); s.player.current_hitpoints = 99; @@ -3913,6 +4589,9 @@ static void test_sol_aoe_reaction_window(void) { cast_seen && s.sol.aoe_attack == COLO_SOL_AOE_SPEAR1 && col_sol_aoe_tile_is_hazard(&s.sol, s.player.x, s.player.y)); + ColoSolDamageSource opener_source = + col_sol_aoe_damage_source(s.sol.aoe_attack); + float opener_damage_before = s.log.sol_damage_by_source[opener_source]; s.player.current_hitpoints = 99; step_and_observe(&s, &ctx, idle); CHECK("no AoE damage on the telegraph tick (cast + 1)", @@ -3920,6 +4599,9 @@ static void test_sol_aoe_reaction_window(void) { step_and_observe(&s, &ctx, idle); CHECK("a stationary player is hit exactly 2 ticks after the cast", s.player.current_hitpoints < 99); + CHECK("the opener AOE lands in its shape-specific source total", + s.log.sol_damage_by_source[opener_source] == + opener_damage_before + (float)(99 - s.player.current_hitpoints)); sol_move_player(&s, 16, 16); cast_seen = 0; @@ -3966,9 +4648,14 @@ static void test_sol_laser_react_window(void) { for (int t = 0; t < 3; t++) step_and_observe(&s, &ctx, idle); CHECK("the 3 reaction ticks pass without damage", s.player.current_hitpoints == 99); + float laser_damage_before = + s.log.sol_damage_by_source[COLO_SOL_DAMAGE_CRYSTAL_LASER]; step_and_observe(&s, &ctx, idle); CHECK("the aligned hit lands on the tick after the reaction window", s.player.current_hitpoints < 99); + CHECK("crystal-laser damage has its own source total", + s.log.sol_damage_by_source[COLO_SOL_DAMAGE_CRYSTAL_LASER] == + laser_damage_before + (float)(99 - s.player.current_hitpoints)); } static void test_sol_phase_transition_sand_guarantees(void) { @@ -4041,14 +4728,21 @@ static void test_sol_beams_become_pools(void) { sol_move_player(&s, s.sol.hazard_tile_x[0], s.sol.hazard_tile_y[0]); int burns_ok = 1; + int burn_damage_total = 0; + float molten_damage_before = + s.log.sol_damage_by_source[COLO_SOL_DAMAGE_MOLTEN_SAND]; for (int t = 0; t < 30; t++) { s.player.current_hitpoints = 99; step_and_observe(&s, &ctx, idle); int dmg = 99 - s.player.current_hitpoints; + burn_damage_total += dmg; if (dmg < COLO_MOLTEN_SAND_MIN_HIT || dmg > COLO_MOLTEN_SAND_MIN_HIT + COLO_MOLTEN_SAND_RAND - 1) burns_ok = 0; } CHECK("standing on a pool burns 5-9 every tick", burns_ok); + CHECK("molten-sand damage has its own source total", + s.log.sol_damage_by_source[COLO_SOL_DAMAGE_MOLTEN_SAND] == + molten_damage_before + (float)burn_damage_total); CHECK("pools persist for the rest of the fight", s.sol.hazard_tile_count == 6); } @@ -4534,8 +5228,8 @@ static void test_loadout_divine_potions_and_stat_drift(void) { s.divine_ranged_timer = 234; ColoSnapshot snap; col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); - CHECK("snapshot version is v20 for Solarflare shared cadence", - snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 20u); + CHECK("snapshot version is v22 for exact NPC death telemetry", + snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 22u); ColosseumState restored; memset(&restored, 0, sizeof(restored)); col_restore_ctx((EncounterState*)&restored, (EncounterContext*)&ctx, &snap, sizeof(snap)); @@ -5046,6 +5740,195 @@ static void test_total_damage_by_type_captures_typeless(void) { s.log.typeless_damage_by_type[COLO_JAVELIN_COLOSSUS] == typeless_before + 17.0f); } +static void test_javelin_and_doom_damage_source_contract(void) { + printf("test_javelin_and_doom_damage_source_contract\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 2325, 17, 16); + + EncounterPendingHit basic = { + .active = 1, + .damage = 11, + .ticks_remaining = 1, + .attack_style = ATTACK_STYLE_RANGED, + .source_npc_type = COLO_JAVELIN_COLOSSUS, + .source_npc_slot = -1, + }; + encounter_pending_hit_queue_push( + &s.player_pending_hits, basic, "test-javelin", s.tick, -1, + COLO_JAVELIN_COLOSSUS); + col_resolve_player_pending_hits_ctx(&s, &ctx); + CHECK("basic ranged damage has its own Javelin source counter", + s.log.javelin_damage_by_source[COLO_JAVELIN_DAMAGE_BASIC_RANGED] == + 11.0f); + CHECK("basic ranged damage has its own fatal-source identity", + s.last_damage_source == COLO_DAMAGE_JAVELIN_BASIC_RANGED); + + col_init_npc(&s, 0, COLO_JAVELIN_COLOSSUS, 20, 16); + ColoJavelinState* jv = colo_npc_javelin(&s.npcs[0]); + jv->skyfall_pending = 1; + jv->skyfall_tile_x = s.player.x; + jv->skyfall_tile_y = s.player.y; + jv->skyfall_timer = 1; + jv->skyfall_damage = 13; + col_npc_resolve_javelin_skyfall(&s, &ctx, 0); + CHECK("skyfall damage has its own Javelin source counter", + s.log.javelin_damage_by_source[COLO_JAVELIN_DAMAGE_SKYFALL] == + 13.0f); + + s.molten_count = 1; + s.molten_x[0] = s.player.x; + s.molten_y[0] = s.player.y; + s.molten_kind[0] = COLO_POOL_REENTRY; + s.molten_lifetime[0] = COLO_POOL_PERMANENT; + s.molten_burn_timer[0] = 0; + col_mod_tick_molten_pools(&s); + CHECK("Reentry damage has its own positive Javelin source counter", + s.log.javelin_damage_by_source[COLO_JAVELIN_DAMAGE_REENTRY_POOL] > + 0.0f); + + s.modifiers.active_mask |= (1u << COLO_MOD_DOOM); + s.modifiers.tier[COLO_MOD_DOOM] = 3; + s.doom_stacks = COLO_DOOM_CAP[3] - 1; + s.molten_kind[0] = COLO_POOL_VOLATILITY; + s.molten_burn_timer[0] = 0; + col_mod_tick_molten_pools(&s); + CHECK("the lethal Doom stack records the Volatility-pool mechanism", + col_mod_doom_lethal(&s) && + s.last_damage_source == COLO_DAMAGE_VOLATILITY_POOL && + s.last_doom_damage_source == COLO_DAMAGE_VOLATILITY_POOL); + col_mod_on_player_damaged(&s, 1, COLO_DAMAGE_SOLARFLARE); + CHECK("Doom source latches on the transition to lethal", + s.last_damage_source == COLO_DAMAGE_SOLARFLARE && + s.last_doom_damage_source == COLO_DAMAGE_VOLATILITY_POOL); + col_record_death_attribution(&s); + CHECK("death and Doom death metrics preserve their distinct mechanisms", + s.log.death_by_source[COLO_DAMAGE_SOLARFLARE] == 1.0f && + s.log.doom_death_by_source[COLO_DAMAGE_VOLATILITY_POOL] == 1.0f); +} + +static void test_applied_damage_and_merged_pool_attribution(void) { + printf("test_applied_damage_and_merged_pool_attribution\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 2326, 17, 16); + s.modifiers.active_mask |= (1u << COLO_MOD_DOOM); + s.modifiers.tier[COLO_MOD_DOOM] = 3; + s.active_player_damage_received_scale = 0.0f; + s.player_venom = COLO_VENOM_START; + s.player_venom_timer = 0; + int doom_before = s.doom_stacks; + col_mod_tick_venom(&s); + CHECK("damage rounded to zero does not add Doom or a fatal source", + s.doom_stacks == doom_before && + s.last_damage_source == -1 && + s.tick_scratch.damage_received == 0.0f); + + s.active_player_damage_received_scale = 1.0f; + s.player.current_hitpoints = 5; + int applied = col_damage_player_from( + &s, 20, COLO_JAVELIN_COLOSSUS, COLO_DMG_UNPRAYABLE); + col_mod_on_player_damaged( + &s, applied, COLO_DAMAGE_JAVELIN_SKYFALL); + CHECK("overkill records only the actual five hitpoints lost", + applied == 5 && + s.tick_scratch.damage_received == 5.0f && + s.log.total_damage_by_type[COLO_JAVELIN_COLOSSUS] == 5.0f); + int source_after_fatal = s.last_damage_source; + CHECK("damage after zero hitpoints cannot overwrite the fatal source", + col_damage_player_from( + &s, 15, COLO_JAVELIN_COLOSSUS, COLO_DMG_UNPRAYABLE) == 0 && + s.last_damage_source == source_after_fatal); + + init_forecast_test_state(&s, &ctx, 2327, 17, 16); + s.modifiers.active_mask |= + (1u << COLO_MOD_DOOM) | + (1u << COLO_MOD_BLASPHEMY); + s.modifiers.tier[COLO_MOD_DOOM] = 3; + s.modifiers.tier[COLO_MOD_BLASPHEMY] = 3; + s.player.current_hitpoints = 5; + s.player.current_prayer = 77; + col_apply_divine_combat_potion_effect(&s); + CHECK("divine self-damage has an explicit source and adds one Doom stack", + s.player.current_hitpoints == 0 && + s.tick_scratch.damage_received == 5.0f && + s.tick_scratch.landed_self_damage == 5.0f && + s.last_damage_source == COLO_DAMAGE_SELF && + s.doom_stacks == 1); + CHECK("Blasphemy ignores divine self-damage", + s.player.current_prayer == 77); + + init_forecast_test_state(&s, &ctx, 2328, 17, 16); + col_mod_add_molten_pool( + &s, + s.player.x, + s.player.y, + COLO_POOL_REENTRY, + COLO_POOL_PERMANENT); + col_mod_add_molten_pool( + &s, + s.player.x, + s.player.y, + COLO_POOL_VOLATILITY, + COLO_POOL_TEMPORARY); + CHECK("same-tile Reentry and Volatility preserve both provenances", + s.molten_count == 1 && + s.molten_kind[0] == COLO_POOL_REENTRY_VOLATILITY && + s.molten_lifetime[0] == COLO_POOL_PERMANENT); + int hp_before = s.player.current_hitpoints; + col_mod_tick_molten_pools(&s); + int first_burn = hp_before - s.player.current_hitpoints; + CHECK("combined pool damage has a distinct source and Javelin counter", + first_burn >= 1 && + first_burn <= COLO_REENTRY_MOLTEN_MAX_HIT && + s.last_damage_source == COLO_DAMAGE_REENTRY_VOLATILITY_POOL && + s.log.javelin_damage_by_source[ + COLO_JAVELIN_DAMAGE_REENTRY_VOLATILITY_POOL] == (float)first_burn); + hp_before = s.player.current_hitpoints; + col_mod_tick_molten_pools(&s); + CHECK("combined modifier pool respects the every-other-tick cadence", + s.player.current_hitpoints == hp_before && + s.molten_burn_timer[0] == 0); + col_mod_tick_molten_pools(&s); + CHECK("combined modifier pool burns again after one clear tick", + s.player.current_hitpoints < hp_before); + col_mod_clear_wave_end_pools(&s); + CHECK("a permanent merged tile survives wave end with both provenances", + s.molten_count == 1 && + s.molten_kind[0] == COLO_POOL_REENTRY_VOLATILITY && + s.molten_lifetime[0] == COLO_POOL_PERMANENT); +} + +static void test_sol_damage_source_contract(void) { + printf("test_sol_damage_source_contract\n"); + CHECK("every AOE shape maps to its distinct damage source", + col_sol_aoe_damage_source(COLO_SOL_AOE_SPEAR1) == COLO_SOL_DAMAGE_SPEAR_1 && + col_sol_aoe_damage_source(COLO_SOL_AOE_SPEAR2) == COLO_SOL_DAMAGE_SPEAR_2 && + col_sol_aoe_damage_source(COLO_SOL_AOE_SHIELD1) == COLO_SOL_DAMAGE_SHIELD_1 && + col_sol_aoe_damage_source(COLO_SOL_AOE_SHIELD2) == COLO_SOL_DAMAGE_SHIELD_2); + + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 2324); + + s.player.current_hitpoints = 5; + col_damage_player_from_sol(&s, 20, COLO_SOL_DAMAGE_MOLTEN_SAND); + CHECK("source totals count applied damage rather than overkill", + s.log.sol_damage_by_source[COLO_SOL_DAMAGE_MOLTEN_SAND] == 5.0f); + CHECK("the NPC-type total also counts applied damage", + s.log.total_damage_by_type[COLO_SOL_HEREDIT] == 5.0f); + + s.player.current_hitpoints = 99; + float typeless_before = s.log.typeless_damage_by_type[COLO_SOL_HEREDIT]; + col_damage_player_from_sol(&s, 15, COLO_SOL_DAMAGE_TRIPLE_PARRY); + CHECK("triple-parry source damage uses the off-prayer channel", + s.log.sol_damage_by_source[COLO_SOL_DAMAGE_TRIPLE_PARRY] == 15.0f && + s.tick_scratch.landed_offpray_damage == 15.0f && + s.log.typeless_damage_by_type[COLO_SOL_HEREDIT] == typeless_before); +} + static void test_npc_magic_defence_rolls_off_magic_level(void) { printf("test_npc_magic_defence_rolls_off_magic_level\n"); col_build_npc_stats(); @@ -5158,15 +6041,24 @@ static void test_combat_fidelity_contract_sizes(void) { CHECK("prayer head uses shared PVE overhead dim", COLO_ACTION_DIMS[COLO_HEAD_PRAYER] == ENCOUNTER_OVERHEAD_DIM_PVE); CHECK("spell head dim is 3 (none/summon-thrall/death-charge)", COLO_SPELL_DIM == 3); - CHECK("obs width is 3044", COLO_NUM_OBS == 3044); - CHECK("weapon-choice tail has 58 features (28 cell DPT + 28 spec + 2 wielded)", - COLO_WEAPON_CHOICE_OBS_SIZE == 58); - CHECK("inventory block has 784 features", COLO_INVENTORY_OBS_SIZE == 784); - CHECK("equipped-self block has 198 features", COLO_EQUIPPED_SELF_OBS_SIZE == 198); - CHECK("modifier hazard tail has 38 features", COLO_MODIFIER_HAZARD_OBS_SIZE == 38); - CHECK("modifier block has 74 features", COLO_MODIFIER_OBS_SIZE == 74); - CHECK("NPC slots have 37 features (DPT obs removed, B0 neutral)", COLO_FEATURES_PER_NPC == 37); - CHECK("snapshot version is v20", COLO_SNAPSHOT_VERSION == 20u); + CHECK("obs width is 922", COLO_NUM_OBS == 922); + CHECK("inventory block has 84 features (28 cells x code, equipped, hp_heal)", + COLO_INVENTORY_OBS_SIZE == 84); + CHECK("the encoder still sees the 15-feature record, rebuilt from the item table", + COLO_INVENTORY_CELL_ENCODER_FEATURES == 15 && + COLO_ITEM_OBS_TABLE_COLS == 15); + /* spec_cost is 0 for everything without a special, so it doubles as the has-spec flag + * while also telling the agent whether it can afford one. */ + CHECK("spec cost is exposed and normalised for a spec weapon", + osrs_clamp_unit((float)osrs_spec_cost(ITEM_DRAGON_CLAWS) / 100.0f) == 0.5f); + CHECK("a non-spec weapon reads zero spec cost", osrs_spec_cost(ITEM_NONE) == 0); + CHECK("equipped block is the 10-feature effect aggregate, not 11 item stat rows", + COLO_EQUIPPED_SELF_OBS_SIZE == 10); + CHECK("modifier hazard tail has 42 features", COLO_MODIFIER_HAZARD_OBS_SIZE == 42); + CHECK("modifier block has 60 features", COLO_MODIFIER_OBS_SIZE == 60); + CHECK("NPC slots carry a type code, not a type one-hot", + COLO_FEATURES_PER_NPC == 23); + CHECK("snapshot version is v22", COLO_SNAPSHOT_VERSION == 22u); CHECK("every active NPC gets an obs slot (no busy-wave drop)", COLO_OBS_NPCS == 24 && COLO_OBS_NPCS == COLO_MAX_NPCS); CHECK("PRIMARY head covers noop, movement, and NPC obs slots", @@ -5179,13 +6071,12 @@ static void test_combat_fidelity_contract_sizes(void) { CHECK("mask size equals the summed action-head dims", COLO_ACTION_MASK_SIZE == mask_sum && COLO_ACTION_MASK_SIZE == 452); - int obs_sum = COLO_PLAYER_OBS_SIZE + COLO_PILLAR_OBS_SIZE + + int obs_sum = COLO_PLAYER_OBS_SIZE + COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE + COLO_NPC_OBS_SIZE + COLO_MODIFIER_OBS_SIZE + COLO_WAVE_OBS_SIZE + COLO_BOSS_OBS_SIZE + - COLO_PENDING_HIT_OBS_SIZE + COLO_STEP_OUT_FORECAST_OBS_SIZE + + COLO_PENDING_HIT_OBS_SIZE + COLO_THREAT_LOS_OBS_SIZE + COLO_THRALL_DC_OBS_SIZE + - COLO_WEAPON_CHOICE_OBS_SIZE + COLO_SPAWN_OBS_SIZE + - COLO_THREAT_FIELD_OBS_SIZE; + COLO_SPAWN_OBS_SIZE; CHECK("obs width equals the summed section sizes", COLO_NUM_OBS == obs_sum); float opa, ops; @@ -5424,6 +6315,68 @@ static void test_venator_bow_bounce_colosseum_integration(void) { ov.projectiles[2].start_delay > ov.projectiles[1].start_delay); } +static void test_frailty_disables_brew_overheal(void) { + printf("test_frailty_disables_brew_overheal\n"); + ColosseumContext ctx; + ColosseumState s; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 909); + int base = s.player.base_hitpoints; + int brew_heal = osrs_brew_heal_amount(base); + s.player.current_hitpoints = base; + col_apply_drink_one_dose_effect(&s, OSRS_CONSUMABLE_BREW); + CHECK("without Frailty a brew overheals above base hitpoints", + s.player.current_hitpoints == base + brew_heal); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 909); + s.modifiers.active_mask |= (1u << COLO_MOD_FRAILTY); + s.modifiers.tier[COLO_MOD_FRAILTY] = 1; + col_mod_apply_frailty_hp(&s); + int frail_base = s.player.base_hitpoints; + CHECK("Frailty I cuts base hitpoints by 10%", + frail_base == 99 - (99 * COLO_FRAILTY_HP_CUT_PCT[1]) / 100); + + s.player.current_hitpoints = frail_base - 1; + col_apply_drink_one_dose_effect(&s, OSRS_CONSUMABLE_BREW); + CHECK("under Frailty a brew heals up to the reduced base but never past it", + s.player.current_hitpoints == frail_base); +} + +static void test_red_flag_minotaur_not_solid_to_other_npcs(void) { + printf("test_red_flag_minotaur_not_solid_to_other_npcs\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 77); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + col_init_npc(&s, 0, COLO_MINOTAUR, 14, 16); + int size = col_npc_effective_size(&s.npcs[0]); + CHECK("without Red Flag the minotaur blocks other NPCs", + col_npc_blocked_ignore_player(&s, 14, 16, size)); + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 77); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.modifiers.active_mask |= (1u << COLO_MOD_RED_FLAG); + s.modifiers.tier[COLO_MOD_RED_FLAG] = 1; + col_init_npc(&s, 0, COLO_MINOTAUR, 14, 16); + CHECK("with Red Flag other NPCs can move into the minotaur's tiles", + !col_npc_blocked_ignore_player(&s, 14, 16, size)); + + col_init_npc(&s, 1, COLO_SERPENT_SHAMAN, 20, 16); + int shaman_size = col_npc_effective_size(&s.npcs[1]); + CHECK("Red Flag does not make other NPC types passable", + col_npc_blocked_ignore_player(&s, 20, 16, shaman_size)); + + col_deactivate_npc(&s, 1); + CHECK("deactivating a non-solid minotaur leaves no stale collision flags", + !col_npc_blocked_ignore_player(&s, 14, 16, size)); +} + static void test_bee_contact_damage_band(void) { printf("test_bee_contact_damage_band\n"); ColosseumContext ctx; @@ -5454,12 +6407,23 @@ static void test_bee_contact_damage_band(void) { col_apply_weapon_set(&s, COLO_GEAR_MELEE); CHECK("rig sanity: the melee set is venom-immune", osrs_effect_profile_has(col_live_effects(&s), OSRS_ITEM_EFFECT_VENOM_IMMUNE)); - bee->x = s.player.x; - bee->y = s.player.y; - s.player.current_hitpoints = 99; - col_mod_tick_bees(&s); - CHECK("serpentine-helm immunity zeroes bee contact damage", - s.player.current_hitpoints == 99); + + s.player_poison = 0; + s.player_poison_timer = 0; + int halved_in_band = 1, halved_any_zero = 0; + for (int t = 0; t < 200; t++) { + bee->x = s.player.x; + bee->y = s.player.y; + s.player.current_hitpoints = 99; + col_mod_tick_bees(&s); + int dmg = 99 - s.player.current_hitpoints; + if (dmg < COLO_BEE_MIN_DAMAGE / 2 || dmg > COLO_BEE_MAX_DAMAGE / 2) + halved_in_band = 0; + if (dmg == 0) halved_any_zero = 1; + } + CHECK("antipoison halves bee contact damage rather than nulling it", + halved_in_band && !halved_any_zero); + CHECK("antipoison still blocks the bee poison stack", s.player_poison == 0); } static void test_divine_state_obs_presence(void) { @@ -5762,7 +6726,7 @@ static void test_combat_fidelity_snapshot_roundtrip(void) { ColoSnapshot snap; col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); - CHECK("snapshot frame is v20", snap.version == 20u); + CHECK("snapshot frame is v22", snap.version == 22u); ColosseumState restored; memset(&restored, 0, sizeof(restored)); @@ -5779,137 +6743,6 @@ static void test_combat_fidelity_snapshot_roundtrip(void) { restored.death_charge_window_left == 44 && restored.death_charge_cd == 0); } -static void test_step_out_forecast_manticore_armed_pattern(void) { - printf("test_step_out_forecast_manticore_armed_pattern\n"); - ColosseumContext ctx; - ColosseumState s; - init_forecast_test_state(&s, &ctx, 401, 17, 16); - col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); - s.npcs[0].attack_timer = 1; - ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); - mc->cycle_step = 0; - mc->orb_style[0] = ATTACK_STYLE_MAGIC; - mc->orb_style[1] = ATTACK_STYLE_RANGED; - mc->orb_style[2] = ATTACK_STYLE_MELEE; - - ColoStepOutForecast forecast; - col_build_step_out_forecast_ctx(&s, &forecast); - const ColoStepOutForecastAction* idle = &forecast.actions[0]; - CHECK("armed manticore idle forecast is valid", idle->valid == 1); - CHECK("armed manticore orb 0 records magic on tick 1", - idle->ticks[0].magic_count == 1 && idle->ticks[0].max_hit == COLO_MANTICORE_MAX_HIT_MAGIC); - CHECK("armed manticore orb 1 records ranged on tick 2", - idle->ticks[1].ranged_count == 1 && idle->ticks[1].max_hit == COLO_MANTICORE_MAX_HIT_RANGED); - CHECK("armed manticore orb 2 records melee on tick 3", - idle->ticks[2].melee_count == 1 && idle->melee_fallback_exposure == 1); -} - -static void test_step_out_forecast_manticore_pair_stagger(void) { - printf("test_step_out_forecast_manticore_pair_stagger\n"); - ColosseumContext ctx; - ColosseumState s; - init_forecast_test_state(&s, &ctx, 406, 17, 16); - col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); - col_init_npc(&s, 1, COLO_MANTICORE, 12, 12); - s.npcs[0].attack_timer = 1; - s.npcs[1].attack_timer = 1; - ColoManticoreState* amc = colo_npc_manticore(&s.npcs[0]); - ColoManticoreState* bmc = colo_npc_manticore(&s.npcs[1]); - amc->cycle_step = 0; - amc->orb_style[0] = ATTACK_STYLE_MAGIC; - amc->orb_style[1] = ATTACK_STYLE_RANGED; - amc->orb_style[2] = ATTACK_STYLE_MELEE; - bmc->cycle_step = 0; - bmc->orb_style[0] = ATTACK_STYLE_MAGIC; - bmc->orb_style[1] = ATTACK_STYLE_RANGED; - bmc->orb_style[2] = ATTACK_STYLE_MELEE; - - ColoStepOutForecast forecast; - col_build_step_out_forecast_ctx(&s, &forecast); - const ColoStepOutForecastAction* idle = &forecast.actions[0]; - CHECK("synced-pair forecast predicts ONE orb per tick, not two", - idle->ticks[0].magic_count == 1 && - idle->ticks[1].ranged_count == 1 && - idle->ticks[2].melee_count == 1); - - s.npcs[1].attack_timer = 3; - bmc->orb_style[0] = ATTACK_STYLE_RANGED; - col_build_step_out_forecast_ctx(&s, &forecast); - idle = &forecast.actions[0]; - CHECK("still-charging peer forecast overlaps mid-barrage", - idle->ticks[2].melee_count == 1 && idle->ticks[2].ranged_count == 1); -} - -static void test_step_out_forecast_warband_window_and_break(void) { - printf("test_step_out_forecast_warband_window_and_break\n"); - ColosseumContext ctx; - ColosseumState s; - init_forecast_test_state(&s, &ctx, 402, 7, 18); - s.tick = 100; - s.warband_cycle_anchor = 100; - col_init_npc(&s, 0, COLO_FREMENNIK_BERSERKER, 8, 18); - - ColoStepOutForecast forecast; - col_build_step_out_forecast_ctx(&s, &forecast); - int run_west = forecast_move_action_for_delta(-2, 0); - CHECK("adjacent berserker records melee on its next window", - forecast.actions[0].ticks[0].melee_count == 1); - CHECK("running west breaks the berserker forecast adjacency", - !forecast_action_has_event(&forecast.actions[run_west])); -} - -static void test_step_out_forecast_ranged_los_candidate_tiles(void) { - printf("test_step_out_forecast_ranged_los_candidate_tiles\n"); - ColosseumContext ctx; - ColosseumState s; - init_forecast_test_state(&s, &ctx, 403, 7, 9); - col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 12, 12); - s.npcs[0].attack_timer = 1; - - ColoStepOutForecast forecast; - col_build_step_out_forecast_ctx(&s, &forecast); - int run_north = forecast_move_action_for_delta(0, 2); - CHECK("pillar-blocked idle tile records no shaman forecast", - !forecast_action_has_event(&forecast.actions[0])); - CHECK("clear run-north tile records the shaman magic forecast", - forecast.actions[run_north].ticks[0].magic_count == 1); -} - -static void test_step_out_forecast_valid_flags(void) { - printf("test_step_out_forecast_valid_flags\n"); - ColosseumContext ctx; - ColosseumState s; - init_forecast_test_state(&s, &ctx, 404, 7, 9); - - ColoStepOutForecast forecast; - col_build_step_out_forecast_ctx(&s, &forecast); - int walk_east = forecast_move_action_for_delta(1, 0); - int walk_west = forecast_move_action_for_delta(-1, 0); - CHECK("pillar move has invalid step-out forecast flag", - forecast.actions[walk_east].valid == 0); - CHECK("clear move has valid step-out forecast flag", - forecast.actions[walk_west].valid == 1); -} - -static void test_step_out_forecast_same_tick_mixed_styles(void) { - printf("test_step_out_forecast_same_tick_mixed_styles\n"); - ColosseumContext ctx; - ColosseumState s; - init_forecast_test_state(&s, &ctx, 405, 17, 16); - col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 16); - col_init_npc(&s, 1, COLO_JAVELIN_COLOSSUS, 20, 15); - s.npcs[0].attack_timer = 1; - s.npcs[1].attack_timer = 1; - - ColoStepOutForecast forecast; - col_build_step_out_forecast_ctx(&s, &forecast); - const ColoStepOutForecastAction* idle = &forecast.actions[0]; - CHECK("same tick magic and ranged forecast conflict is flagged", - idle->same_tick_mixed_style_conflict == 1); - CHECK("same tick magic and ranged counts are both recorded", - idle->ticks[0].magic_count == 1 && idle->ticks[0].ranged_count == 1); -} - static void test_render_bridge_combat_visuals_and_loadout(void) { printf("test_render_bridge_combat_visuals_and_loadout\n"); ColosseumContext ctx; @@ -6623,6 +7456,142 @@ static void test_echo_boots_recoil_reflects_to_attacker(void) { sol.player.item_effect_state.echo_boot_charges == OSRS_ECHO_BOOTS_MAX_CHARGES); } +static void test_pending_hit_recoil_volatility_damage_accounting(void) { + printf("test_pending_hit_recoil_volatility_damage_accounting\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 1.0f, 92); + geo_clear_npcs(&s); + s.player.x = 18; + s.player.y = 16; + s.player.current_hitpoints = 99; + s.player.prayer = PRAYER_NONE; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 16, 16); + s.npcs[0].hp = 1; + s.npcs[0].min_hp_seen = 1; + s.modifiers.active_mask |= 1u << COLO_MOD_VOLATILITY; + s.modifiers.tier[COLO_MOD_VOLATILITY] = 1; + + EncounterPendingHit hit = encounter_pending_hit_resolved_at_throw( + 10, + 1, + ATTACK_STYLE_RANGED, + PRAYER_NONE, + COLO_JAGUAR_WARRIOR, + 0, + 1, + NULL); + col_push_player_pending_hit(&s, hit); + col_resolve_player_pending_hits_ctx(&s, &ctx); + + CHECK("pending hit lands before its echo-boots recoil death effect", + s.player.current_hitpoints == 64 && + s.npcs[0].hp == 0); + CHECK("nested Volatility and pending-hit damage remain separately exact", + s.tick_scratch.damage_received == 35.0f && + s.tick_scratch.landed_offpray_damage == 10.0f && + s.tick_scratch.landed_unprayable_damage == 25.0f); + col_accumulate_tick_stats(&s, &ctx); +} + +/* The win bonus used to be `+=` onto the same tick's shaped reward and wave-clear bonus, + * which pushed the tick past the trainer's [-1,1] clamp and measurably destroyed 59% of it. + * Terminal outcomes are now assignments, like death and timeout. */ + +/* The inventory memo key covers cell contents but deliberately NOT equipped gear, so that a + * gear swap does not evict the block. Two fields in the cached range are still gear-dependent + * -- the per-cell is_equipped flag and the trailing effect aggregate -- and both must be + * rewritten on a cache hit. Swapping gear WITHOUT touching any cell leaves the key unchanged, + * which is exactly the case a stale cache would get wrong, and the goldens do not cover it. */ +static void test_colosseum_inventory_memo_tracks_gear_swaps(void) { + printf("test_colosseum_inventory_memo_tracks_gear_swaps\n"); + ColosseumState s; + ColosseumContext ctx; + static float obs_a[COLO_NUM_OBS]; + static float obs_b[COLO_NUM_OBS]; + + col_init_context_typed(&ctx); + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242u); + + int cell = -1, slot = -1; + for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) { + uint8_t it = s.inventory_cells[c].item_idx; + if (it == ITEM_NONE) continue; + int gs = osrs_item_gear_slot(it); + if (gs < 0) continue; + if (s.player.equipped[gs] == it) continue; + cell = c; slot = gs; break; + } + CHECK("a swappable gear cell exists at reset", cell >= 0); + if (cell < 0) return; + + uint64_t sig_before = col_inventory_obs_signature(&s); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_a); + + /* Equip it directly. No cell changes, so the memo key is untouched. */ + s.player.equipped[slot] = s.inventory_cells[cell].item_idx; + CHECK("a gear swap alone does not move the memo key", + col_inventory_obs_signature(&s) == sig_before); + + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_b); + + int flag = COLO_OBS_AFTER_PLAYER + cell * COLO_INVENTORY_CELL_OBS_FEATURES + 1; + CHECK("the cell reads not-equipped before the swap", obs_a[flag] == 0.0f); + CHECK("a cache hit still reports the freshly equipped item", + obs_b[flag] == 1.0f); +} + +static void test_colosseum_win_tick_is_not_stacked(void) { + printf("test_colosseum_win_tick_is_not_stacked\n"); + ColosseumState s; + ColosseumContext ctx; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + + col_init_context_typed(&ctx); + ctx.config.start_wave = COLO_WAVE_BOSS; + ctx.config.win_bonus = 0.7f; + ctx.config.wave_clear_bonus = 0.9f; + ctx.config.damage_reward_coeff = 0.01f; + ctx.config.boss_phase_bonus = 1.0f; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242u); + + for (int t = 0; t < 60 && !s.episode_over; t++) { + if (s.modifiers.draft_pending) { + int draft[COLO_NUM_ACTION_HEADS] = {0}; + draft[COLO_HEAD_MODIFIER_SELECT] = 1; + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, draft); + continue; + } + if (s.sol.started && col_sol_find_idx(&s) >= 0) break; + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, idle); + } + CHECK("reached the boss wave with Sol alive", + !s.episode_over && s.wave == COLO_WAVE_BOSS && col_sol_find_idx(&s) >= 0); + + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (s.npcs[i].active) col_deactivate_npc(&s, i); + } + s.tick_scratch.fresh_damage_dealt = 90.0f; + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, idle); + + CHECK("clearing the final wave ends the episode as a win", + s.episode_over == 1 && s.winner == COLO_OUTCOME_PLAYER_WON); + CHECK("the winning tick pays exactly win_bonus, with nothing stacked on it", + fabsf(s.reward - 0.7f) < 1e-5f); + CHECK("the winning tick stays inside the trainer clamp", + fabsf(s.reward) <= 1.0f); + CHECK("the win tick pays no wave_clear_bonus", + fabsf(s.log.rew_wave_clear) < 1e-6f); + CHECK("the per-term log credits only what was paid", + fabsf(s.log.rew_win - 0.7f) < 1e-5f && + fabsf(s.log.rew_damage) < 1e-6f); + CHECK("the clamp telemetry counted the winning tick", + s.log.reward_steps > 0.0f && fabsf(s.log.reward_clamp_loss) < 1e-6f); +} + static void test_colosseum_live_inventory_display(void) { printf("test_colosseum_live_inventory_display\n"); ColosseumContext ctx; @@ -7183,7 +8152,7 @@ static void test_stage3_t6_obs_mask_fuzz_contract(void) { } step_and_observe(&s, &ctx, actions); } - CHECK("T6 obs running-index assert reached COLO_NUM_OBS", COLO_NUM_OBS == 3044); + CHECK("T6 obs running-index assert reached COLO_NUM_OBS", COLO_NUM_OBS == 922); CHECK("T6 mask running-index assert reached 452", COLO_ACTION_MASK_SIZE == 452); } @@ -7198,20 +8167,24 @@ static void test_death_attribution_credits_actual_source(void) { manticore.source_npc_type = COLO_MANTICORE; manticore.source_npc_slot = -1; manticore.attack_style = ATTACK_STYLE_MAGIC; - col_pending_hit_prayer_observer(&obs, &manticore, 10, 0, 0); + col_pending_hit_prayer_observer(&obs, &manticore, 10, 10, 0, 0); CHECK("manticore landing credits the manticore", s.last_hit_by_type == COLO_MANTICORE); EncounterPendingHit shockwave = {0}; shockwave.source_npc_type = COLO_SHOCKWAVE_COLOSSUS; shockwave.source_npc_slot = -1; shockwave.attack_style = ATTACK_STYLE_RANGED; - col_pending_hit_prayer_observer(&obs, &shockwave, 12, 0, 0); + col_pending_hit_prayer_observer(&obs, &shockwave, 12, 12, 0, 0); CHECK("a non-manticore landing re-credits the actual source", s.last_hit_by_type == COLO_SHOCKWAVE_COLOSSUS); - col_pending_hit_prayer_observer(&obs, &manticore, 0, 1, 0); + col_pending_hit_prayer_observer(&obs, &manticore, 0, 0, 1, 0); CHECK("a 0-damage splash does not change attribution", s.last_hit_by_type == COLO_SHOCKWAVE_COLOSSUS); + + col_record_death_attribution(&s); + CHECK("generic NPC deaths retain their exact attacker type", + s.log.npc_attack_death_by_type[COLO_SHOCKWAVE_COLOSSUS] == 1.0f); } static int test_walkable_block_corner(void* ctx, int x, int y) { @@ -7298,51 +8271,6 @@ static void test_modifier_draft_forces_pick(void) { CHECK("no-op valid again once no draft is pending", mask[base] == 1.0f); } -static void test_gear_and_boost_reward_signals(void) { - printf("test_gear_and_boost_reward_signals\n"); - ColosseumContext ctx; - ColosseumState s; - loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); - advance_to_wave_spawn(&s, &ctx); - int slot = -1; - for (int i = 0; i < COLO_MAX_NPCS; i++) - if (col_npc_is_live_target(&s.npcs[i]) && !col_type_is_hazard_entity(s.npcs[i].type)) { - slot = i; - break; - } - CHECK("a live target exists at reset", slot >= 0); - - s.tick_scratch.player_attacked = 1; - s.player_attack_npc_idx = slot; - - float q_attack = col_attacked_gear_quality_ratio(&s); - CHECK("gear-quality signal fires in [0,1] when attacking", q_attack >= 0.0f && q_attack <= 1.0f); - - const ColoNPC* tnpc = &s.npcs[slot]; - const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = col_get_best_gear_table(&s); - int argmax_set = 0; - float argmax_dpt = -1.0f; - for (int set = 0; set < COLO_NUM_WEAPON_SETS; set++) - if (best[set][tnpc->type].dpt > argmax_dpt) { - argmax_dpt = best[set][tnpc->type].dpt; - argmax_set = set; - } - memcpy(s.player.equipped, best[argmax_set][tnpc->type].setup, sizeof(s.player.equipped)); - CHECK("oracle's argmax-best kit yields ~max gear quality", - col_attacked_gear_quality_ratio(&s) > 0.99f); - - s.player_attack_style_id = ATTACK_STYLE_RANGED; - s.player.current_ranged = s.player.base_ranged; - CHECK("no boost reward when ranged is at base", - col_attacked_with_offensive_boost(&s) == 0); - s.player.current_ranged = s.player.base_ranged + 5; - CHECK("boost reward when ranged is above base", - col_attacked_with_offensive_boost(&s) == 1); - - s.tick_scratch.player_attacked = 0; - CHECK("gear-quality is the no-attack sentinel", col_attacked_gear_quality_ratio(&s) < 0.0f); - CHECK("boost signal is the no-attack sentinel", col_attacked_with_offensive_boost(&s) < 0); -} static int colo_test_cell_of_named_item(const ColosseumState* s, const char* name) { for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { @@ -7354,79 +8282,95 @@ static int colo_test_cell_of_named_item(const ColosseumState* s, const char* nam return -1; } -static void test_threat_field_obs(void) { - printf("test_threat_field_obs\n"); +/* Round-to-nearest-even bf16, which is what the trainer stores observations in by default. */ +static float test_through_bf16(float v) { + uint32_t bits; + memcpy(&bits, &v, sizeof(bits)); + uint32_t rounded = (bits + 0x7FFFu + ((bits >> 16) & 1u)) & 0xFFFF0000u; + float out; + memcpy(&out, &rounded, sizeof(out)); + return out; +} + +/* The whole scheme rests on the code surviving the observation buffer. Nothing else in the + C gate runs at training precision, so pin it here. */ +static void test_item_obs_code_survives_bf16(void) { + printf("test_item_obs_code_survives_bf16\n"); + int worst = -1; + for (int code = 0; code < OSRS_INVENTORY_CELL_OBS_CODE_COUNT; code++) { + float observed = test_through_bf16(osrs_inventory_cell_obs_code_encode(code)); + if (osrs_inventory_cell_obs_code_decode(observed) != code) worst = code; + } + CHECK("every item code round-trips through bf16", worst < 0); + CHECK("the code stays inside the unit range the rest of the observation lives in", + osrs_inventory_cell_obs_code_encode(OSRS_INVENTORY_CELL_OBS_CODE_COUNT - 1) <= 1.0f); +} + +/* The table is generated and committed, so a checkout can hold a stale one and train on + silently wrong observations. Rebuild every row and diff it, float for float. */ +static void test_item_obs_table_is_current(void) { + printf("test_item_obs_table_is_current\n"); + for (int code = 0; code < COLO_ITEM_OBS_TABLE_ROWS; code++) { + float row[COLO_ITEM_OBS_TABLE_COLS]; + osrs_write_inventory_cell_obs_table_row( + row, code, MAXED_BASE_HITPOINTS, + COLO_ITEM_OBS_TABLE_BASE_PRAYER, COLO_ITEM_OBS_TABLE_BASE_LEVEL); + for (int f = 0; f < COLO_ITEM_OBS_TABLE_COLS; f++) { + char label[160]; + snprintf(label, sizeof(label), + "item table row %d feature %d matches a fresh build " + "(rerun ocean/osrs/tools/gen_colosseum_item_obs_table.c)", code, f); + CHECK(label, row[f] == TEST_ITEM_OBS_TABLE[code][f]); + } + } +} + +/* The recut is only legitimate if the encoder still sees the record it saw before. Drive a + live episode and compare the expanded record against the writer the observation used to + call, cell by cell and tick by tick, across a Frailty draft so base_hitpoints moves. */ +static void test_inventory_obs_expansion_is_lossless(void) { + printf("test_inventory_obs_expansion_is_lossless\n"); ColosseumContext ctx; ColosseumState s; loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); - s.player.x = 5; s.player.y = 9; - col_rebuild_player_collision_flags(&s); - int manti = col_spawn_npc_at(&s, COLO_MANTICORE, 11, 8); - CHECK("fixture: shooter spawned", manti >= 0); - CHECK("fixture: the pillar blocks LoS to the player's tile", - !col_npc_has_los_to_player(&s, &s.npcs[manti])); - static float obs[COLO_NUM_OBS]; - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - - const int f0 = COLO_OBS_AFTER_SPAWN; - const int f1 = f0 + COLO_THREAT_FIELD_TILES; -#define FIELD_CELL(dx, dy) \ - (((dy) + COLO_THREAT_FIELD_RADIUS) * COLO_THREAT_FIELD_DIM + \ - ((dx) + COLO_THREAT_FIELD_RADIUS)) - - CHECK("pillar shadow: the player's tile reads zero shooters", - obs[f0 + FIELD_CELL(0, 0)] == 0.0f); - CHECK("exposed tile east of the pillar reads the shooter", - obs[f0 + FIELD_CELL(7, 3)] > 0.0f); - CHECK("pillar tile is unstandable", obs[f1 + FIELD_CELL(3, -1)] == 1.0f); - CHECK("NPC body tile is unstandable", obs[f1 + FIELD_CELL(6, -1)] == 1.0f); - CHECK("the player's own tile is standable", obs[f1 + FIELD_CELL(0, 0)] == 0.0f); - CHECK("out-of-arena tile is unstandable", obs[f1 + FIELD_CELL(-8, 0)] == 1.0f); - - s.player.x = 12; s.player.y = 12; - col_rebuild_player_collision_flags(&s); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("center cell reads one shooter after stepping into LoS", - obs[f0 + FIELD_CELL(0, 0)] == 0.25f); - - int jag = col_spawn_npc_at(&s, COLO_JAGUAR_WARRIOR, 16, 12); - CHECK("fixture: melee NPC spawned", jag >= 0); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("jaguar body tile is unstandable", obs[f1 + FIELD_CELL(4, 0)] == 1.0f); - int jag_size = col_npc_effective_size(&s.npcs[jag]); - col_stamp_npc_collision_footprint(&s, s.npcs[jag].x, s.npcs[jag].y, jag_size, 0); - s.npcs[jag].x += 1; - col_stamp_npc_collision_footprint(&s, s.npcs[jag].x, s.npcs[jag].y, jag_size, 1); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("vacated tile frees after the melee body moves (not a stale memo)", - obs[f1 + FIELD_CELL(4, 0)] == 0.0f); - CHECK("the melee body's new tile is unstandable", - obs[f1 + FIELD_CELL(6, 0)] == 1.0f); - - col_deactivate_npc(&s, manti); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("center cell reads zero shooters after the manticore dies", - obs[f0 + FIELD_CELL(0, 0)] == 0.0f); - CHECK("dead manticore's body tile frees in channel 1", - obs[f1 + FIELD_CELL(-1, -4)] == 0.0f); - - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - float served_field[COLO_THREAT_FIELD_OBS_CACHE_FLOATS]; - memcpy(served_field, &obs[f0], sizeof(served_field)); - memset(&s.obs_memos, 0, sizeof(s.obs_memos)); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("memo-served threat field == fresh recompute", - memcmp(served_field, &obs[f0], sizeof(served_field)) == 0); + int mismatches = 0; + int hp_heal_seen = 0; + + for (int tier = 0; tier < 4; tier++) { + s.modifiers.active_mask |= (1u << COLO_MOD_FRAILTY); + s.modifiers.tier[COLO_MOD_FRAILTY] = tier; + col_mod_apply_frailty_hp(&s); + memset(&s.obs_memos, 0, sizeof(s.obs_memos)); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { + const ColoInvCell* c = &s.inventory_cells[cell]; + float want[COLO_INVENTORY_CELL_ENCODER_FEATURES]; + osrs_write_inventory_cell_affordance_features_compact( + want, c->item_idx, c->raw_osrs_id, c->dose, + osrs_inventory_cell_holds_equipped_item(&s.player, s.inventory_cells, cell), + s.player.base_hitpoints, s.player.base_prayer, s.player.base_attack); + + float got[COLO_INVENTORY_CELL_ENCODER_FEATURES]; + test_expand_inventory_cell(obs, cell, got); + + if (want[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL] != 0.0f && + want[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] == 0.0f && + want[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] == 0.0f) { + hp_heal_seen++; + } + for (int f = 0; f < COLO_INVENTORY_CELL_ENCODER_FEATURES; f++) { + if (want[f] != got[f]) mismatches++; + } + } + } - ctx.config.threat_field_obs_enabled = 0; - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - int all_zero = 1; - for (int k = 0; k < COLO_THREAT_FIELD_OBS_SIZE; k++) - if (obs[f0 + k] != 0.0f) all_zero = 0; - CHECK("disabled threat field leaves the block zeroed", all_zero); -#undef FIELD_CELL + CHECK("expanded record equals the pre-recut compact record on every cell and tier", + mismatches == 0); + CHECK("the sweep actually exercised hp_heal, the one feature the table cannot hold", + hp_heal_seen > 0); } static void test_inventory_obs_memo(void) { @@ -7443,104 +8387,90 @@ static void test_inventory_obs_memo(void) { s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6685); CHECK("4-dose brew seeded", s.inventory_cells[cell_brew].dose == 4); - const int cell_base = COLO_OBS_AFTER_PILLARS + - cell_brew * COLO_INVENTORY_CELL_OBS_FEATURES; + /* Dose rides the item code now: a potion's raw id names its dose, so the sip lands a + different code and the expanded record picks the dose up from the table row. */ + float expanded[COLO_INVENTORY_CELL_ENCODER_FEATURES]; col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("4-dose brew renders the full dose feature", obs[cell_base + 2] == 1.0f); + test_expand_inventory_cell(obs, cell_brew, expanded); + CHECK("4-dose brew renders the full dose feature", + expanded[OSRS_INVENTORY_CELL_COMPACT_DOSE] == 1.0f); s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6687); CHECK("sip took a dose", s.inventory_cells[cell_brew].dose == 3); col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + test_expand_inventory_cell(obs, cell_brew, expanded); CHECK("sip moves the dose feature (dose is in the memo key, not a stale block)", - obs[cell_base + 2] == 0.75f); + expanded[OSRS_INVENTORY_CELL_COMPACT_DOSE] == 0.75f); col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); float served_block[COLO_INVENTORY_OBS_CACHE_FLOATS]; - memcpy(served_block, &obs[COLO_OBS_AFTER_PILLARS], sizeof(served_block)); + memcpy(served_block, &obs[COLO_OBS_AFTER_PLAYER], sizeof(served_block)); memset(&s.obs_memos, 0, sizeof(s.obs_memos)); col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); CHECK("memo-served inventory block == fresh recompute after the sip", - memcmp(served_block, &obs[COLO_OBS_AFTER_PILLARS], sizeof(served_block)) == 0); + memcmp(served_block, &obs[COLO_OBS_AFTER_PLAYER], sizeof(served_block)) == 0); } -static void test_weapon_choice_obs_rank_and_farm_cap(void) { - printf("test_weapon_choice_obs_rank_and_farm_cap\n"); +static void test_best_gear_cache_signatures(void) { + printf("test_best_gear_cache_signatures\n"); ColosseumContext ctx; ColosseumState s; loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); - static float obs[COLO_NUM_OBS]; - const int dpt_base = COLO_OBS_AFTER_THRALL_DC; - const int spec_base = dpt_base + COLO_CELL_WEAPON_DPT_OBS_SIZE; - const int wielded_base = spec_base + COLO_CELL_SPEC_OBS_SIZE; - - int cell_tentacle = colo_test_cell_of_named_item(&s, "Abyssal tentacle"); - int cell_claws = colo_test_cell_of_named_item(&s, "Dragon claws"); - int cell_tbow = colo_test_cell_of_named_item(&s, "Twisted bow"); - CHECK("speedrun kit carries tentacle+claws+tbow in cells", - cell_tentacle >= 0 && cell_claws >= 0 && cell_tbow >= 0); - - int manti = col_spawn_npc_at(&s, COLO_MANTICORE, 10, 10); - osrs_interaction_set(&s.interaction, manti); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("3x3: wielded scythe DPT outranks the tentacle cell", - obs[wielded_base] > obs[dpt_base + cell_tentacle]); - CHECK("3x3: claws cell ranks below tentacle cell", - obs[dpt_base + cell_claws] < obs[dpt_base + cell_tentacle]); - CHECK("3x3: wielded scythe is ~the best achievable (ratio ~1)", - obs[wielded_base + 1] > 0.95f); - CHECK("claws cell carries the spec bit", obs[spec_base + cell_claws] == 1.0f); - CHECK("tentacle cell carries no spec bit", obs[spec_base + cell_tentacle] == 0.0f); - - int serpent = col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 20, 10); - osrs_interaction_set(&s.interaction, serpent); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("1x1: tbow cell outranks the wielded scythe", - obs[dpt_base + cell_tbow] > obs[wielded_base]); - CHECK("1x1: wielded-vs-best ratio exposes the scythe gap", - obs[wielded_base + 1] < 0.85f); - - osrs_interaction_clear(&s.interaction); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("no target: cell DPT + wielded floats are zero", - obs[dpt_base + cell_tentacle] == 0.0f && obs[wielded_base] == 0.0f && - obs[wielded_base + 1] == 0.0f); - CHECK("no target: spec bits stay up (target-independent)", - obs[spec_base + cell_claws] == 1.0f); - - osrs_interaction_set(&s.interaction, manti); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - float pre_drain_tentacle = obs[dpt_base + cell_tentacle]; - float pre_drain_tbow = obs[dpt_base + cell_tbow]; - float pre_drain_wielded = obs[wielded_base]; - s.npcs[manti].def_drained = COLO_NPC_STATS[COLO_MANTICORE].def_level; - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("def drain raises the melee tentacle cell DPT (not a stale block)", - obs[dpt_base + cell_tentacle] > pre_drain_tentacle); - CHECK("def drain raises the ranged tbow cell DPT (not a stale block)", - obs[dpt_base + cell_tbow] > pre_drain_tbow); - CHECK("def drain raises the wielded DPT (not a stale block)", - obs[wielded_base] > pre_drain_wielded); - - int cell_brew = -1; - for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) - if (s.inventory_cells[c].item_idx == ITEM_NONE) { cell_brew = c; break; } - CHECK("a gear-free cell exists to hold the brew", cell_brew >= 0); - s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6685); - CHECK("4-dose brew seeded", s.inventory_cells[cell_brew].dose == 4); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6687); - CHECK("sip took a dose", s.inventory_cells[cell_brew].dose == 3); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - float served_block[COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS]; - memcpy(served_block, &obs[dpt_base], - sizeof(float) * COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS); - memset(&s.obs_memos, 0, sizeof(s.obs_memos)); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("memo-served weapon-choice block == fresh recompute after the sip", - memcmp(served_block, &obs[dpt_base], - sizeof(float) * COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS) == 0); - s.inventory_cells[cell_brew] = osrs_inventory_cell_empty(); + int non_gear_cell = -1; + int weapon_cells[2] = {-1, -1}; + int weapon_count = 0; + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { + uint8_t item = s.inventory_cells[cell].item_idx; + if (item == ITEM_NONE) { + if (s.inventory_cells[cell].raw_osrs_id != 0 && non_gear_cell < 0) + non_gear_cell = cell; + continue; + } + int gear_slot = osrs_item_gear_slot(item); + if (gear_slot < 0 && osrs_spec_cost(item) == 0 && non_gear_cell < 0) + non_gear_cell = cell; + if (gear_slot == GEAR_SLOT_WEAPON && weapon_count < 2) + weapon_cells[weapon_count++] = cell; + } + CHECK("speedrun kit has a non-gear cell", non_gear_cell >= 0); + CHECK("speedrun kit has two weapon cells", weapon_count == 2); + + uint64_t best_gear_signature = col_best_gear_cache_signature(&s); + const ColoBestGear (*best_gear)[COLO_NUM_NPC_TYPES] = col_get_best_gear_table(&s); + int best_gear_next = s.obs_memos.best_gear_next; + + ColoInvCell non_gear = s.inventory_cells[non_gear_cell]; + s.inventory_cells[non_gear_cell] = osrs_inventory_cell_empty(); + CHECK("best-gear key ignores non-gear cells", + col_best_gear_cache_signature(&s) == best_gear_signature); + CHECK("non-gear change reuses best-gear table", + col_get_best_gear_table(&s) == best_gear && + s.obs_memos.best_gear_next == best_gear_next); + + s.inventory_cells[non_gear_cell] = non_gear; + ColoInvCell tmp = s.inventory_cells[weapon_cells[0]]; + s.inventory_cells[weapon_cells[0]] = s.inventory_cells[weapon_cells[1]]; + s.inventory_cells[weapon_cells[1]] = tmp; + CHECK("best-gear key canonicalizes weapon candidate order", + col_best_gear_cache_signature(&s) == best_gear_signature); + CHECK("weapon reorder reuses best-gear table", + col_get_best_gear_table(&s) == best_gear && + s.obs_memos.best_gear_next == best_gear_next); + + tmp = s.inventory_cells[weapon_cells[0]]; + s.inventory_cells[weapon_cells[0]] = s.inventory_cells[weapon_cells[1]]; + s.inventory_cells[weapon_cells[1]] = tmp; + s.player.current_ranged++; + CHECK("best-gear key includes current combat levels", + col_best_gear_cache_signature(&s) != best_gear_signature); +} + +static void test_farm_safe_damage_cap(void) { + printf("test_farm_safe_damage_cap\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); col_spawn_reinforcements(&s); int jaguar = -1; @@ -7555,7 +8485,8 @@ static void test_weapon_choice_obs_rank_and_farm_cap(void) { s.tick_scratch = (ColoTickScratch){0}; s.tick_scratch.fresh_damage_dealt = 100.0f; s.tick_scratch.fresh_damage_reinforcement = 30.0f; - float r_capped = col_compute_reward_ctx(&s, &ctx); + col_accumulate_tick_stats(&s, &ctx); + float r_capped = col_shaped_total(col_shaped_reward(&s, &ctx)); CHECK("cap on, wave 1: reinforcement damage pays nothing", fabsf(r_capped - 70.0f) < 1e-3f); CHECK("farm damage is logged", fabsf(s.log.farm_damage - 30.0f) < 1e-3f); @@ -7563,21 +8494,24 @@ static void test_weapon_choice_obs_rank_and_farm_cap(void) { ctx.config.farm_safe_damage_cap = 0; s.tick_scratch.fresh_damage_dealt = 100.0f; s.tick_scratch.fresh_damage_reinforcement = 30.0f; - float r_uncapped = col_compute_reward_ctx(&s, &ctx); + col_accumulate_tick_stats(&s, &ctx); + float r_uncapped = col_shaped_total(col_shaped_reward(&s, &ctx)); CHECK("cap off: full damage pays", fabsf(r_uncapped - 100.0f) < 1e-3f); ctx.config.farm_safe_damage_cap = 1; s.wave = COLO_FARM_CAP_WAVES; s.tick_scratch.fresh_damage_dealt = 100.0f; s.tick_scratch.fresh_damage_reinforcement = 30.0f; - float r_late = col_compute_reward_ctx(&s, &ctx); + col_accumulate_tick_stats(&s, &ctx); + float r_late = col_shaped_total(col_shaped_reward(&s, &ctx)); CHECK("cap on, wave 5+: reinforcements stay full-value at the default window", fabsf(r_late - 100.0f) < 1e-3f); ctx.config.farm_cap_waves = COLO_FARM_CAP_WAVES + 1; s.tick_scratch.fresh_damage_dealt = 100.0f; s.tick_scratch.fresh_damage_reinforcement = 30.0f; - float r_widened = col_compute_reward_ctx(&s, &ctx); + col_accumulate_tick_stats(&s, &ctx); + float r_widened = col_shaped_total(col_shaped_reward(&s, &ctx)); CHECK("widened farm_cap_waves caps the same wave-5 reinforcement damage", fabsf(r_widened - 70.0f) < 1e-3f); ctx.config.farm_cap_waves = COLO_FARM_CAP_WAVES; @@ -7612,6 +8546,7 @@ int main(void) { test_modifier_hazard_obs_fixes(); test_death_linger_wave_clear_and_render(); test_draft_offer_and_select(); + test_first_draft_is_fixed_trio(); test_draft_upgrade_bias(); test_mantimayhem_stress(); test_frailty_hp(); @@ -7621,7 +8556,13 @@ int main(void) { test_bees_hazard(); test_totem_lifecycle(); test_totemic_sol_wave12(); + test_totemic_sol_extra_totems_every_two_minutes(); + test_obs_signal_defects(); + test_totem_heal_timing_obs(); + test_totem_sol_obs_reports_stacking(); test_reentry_sand_tiles(); + test_mantimayhem_venom_cured_at_wave_end(); + test_frailty_disables_brew_overheal(); test_venom_escalation(); test_bee_poison_status(); test_mantimayhem_t3_shuffle(); @@ -7639,6 +8580,7 @@ int main(void) { test_player_walks_through_npc_footprint(); test_warband_cycle_offsets(); test_warband_move_skip(); + test_warband_melee_not_dodged_by_same_tick_step_out(); test_warband_bfs_memo_bit_identity(); test_warband_melee_distance_gate(); test_warband_two_tick_stationary_gate(); @@ -7649,20 +8591,23 @@ int main(void) { test_minotaur_heal_semantics(); test_manticore_barrage_period(); test_manticore_telegraph_during_windup(); - test_prayer_oracle_manticore_orbs(); test_late_start_entry_state(); test_manticore_orb_same_tick_flick(); test_projectile_prayer_locks_at_throw(); test_npc_melee_instant_unprayable(); test_player_melee_lands_at_delay_zero(); test_echo_boots_recoil_reflects_to_attacker(); + test_pending_hit_recoil_volatility_damage_accounting(); test_manticore_shared_wave_cycle(); test_manticore_stagger_overlap_fidelity(); test_javelin_skyfall_no_defence_gate(); + test_javelin_skyfall_dodge_lead_window(); + test_sol_generic_observation_signals_are_neutral(); test_sol_adjacency_gate_and_kiting(); test_sol_attack_selection_invariants(); test_sol_parry_schedule_and_damage(); test_sol_parry_prayer_punish(); + test_sol_parry_observation_contract(); test_sol_grapple_perfect_parry(); test_sol_perfect_parry_forces_spec_attack(); test_sol_shield_safe_rings(); @@ -7682,37 +8627,39 @@ int main(void) { test_consumable_overdrink_mask(); test_loadout_surge_potion(); test_loadout_spec_weapons(); + test_colosseum_inventory_memo_tracks_gear_swaps(); + test_colosseum_win_tick_is_not_stacked(); test_colosseum_live_inventory_display(); test_loadout_item_effects(); test_loadout_offensive_prayers(); test_npc_magic_defence_rolls_off_magic_level(); test_total_damage_by_type_captures_typeless(); + test_javelin_and_doom_damage_source_contract(); + test_applied_damage_and_merged_pool_attribution(); + test_sol_damage_source_contract(); test_matchup_dpt_obs_ranking(); test_primary_head_resolution(); test_combat_fidelity_contract_sizes(); test_scythe_multihit_per_size(); test_venator_bow_bounce_colosseum_integration(); + test_red_flag_minotaur_not_solid_to_other_npcs(); test_bee_contact_damage_band(); test_divine_state_obs_presence(); test_magic_set_max_hit_math(); test_thrall_regression(); test_death_charge_regression(); test_combat_fidelity_snapshot_roundtrip(); - test_step_out_forecast_manticore_armed_pattern(); - test_step_out_forecast_manticore_pair_stagger(); - test_step_out_forecast_warband_window_and_break(); - test_step_out_forecast_ranged_los_candidate_tiles(); - test_step_out_forecast_valid_flags(); - test_step_out_forecast_same_tick_mixed_styles(); test_render_bridge_combat_visuals_and_loadout(); test_render_bridge_npc_debug_and_warband_motion(); test_melee_reach_cardinal_vs_diagonal(); test_death_attribution_credits_actual_source(); test_move_action_no_corner_cut(); test_modifier_draft_forces_pick(); - test_gear_and_boost_reward_signals(); - test_weapon_choice_obs_rank_and_farm_cap(); - test_threat_field_obs(); + test_best_gear_cache_signatures(); + test_farm_safe_damage_cap(); + test_item_obs_code_survives_bf16(); + test_item_obs_table_is_current(); + test_inventory_obs_expansion_is_lossless(); test_inventory_obs_memo(); return osrs_test_summary(); diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index 7d8d12da4d..552c2f0bda 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -230,17 +230,6 @@ static int inferno_source_bundle_contains(const char* needle) { } \ } while (0) -#define ASSERT_FLOAT_GT(label, actual, threshold) do { \ - tests_run++; \ - if ((actual) > (threshold)) { \ - tests_passed++; \ - } else { \ - tests_failed++; \ - printf(" FAIL: %s - got %.6f, expected > %.6f\n", \ - (label), (float)(actual), (float)(threshold)); \ - } \ -} while (0) - #define ASSERT_STR_EQ(label, actual, expected) do { \ tests_run++; \ if (strcmp((actual), (expected)) == 0) { \ @@ -346,6 +335,7 @@ static void init_spell_cast_test_state(InfernoState* state, InfNPCType target_ty state->player_dest_y = -1; osrs_interaction_init(&state->interaction); encounter_apply_loadout(&state->player, INF_MAX_MAGE_LOADOUT, GEAR_MAGE); + inf_refresh_live_stats(state); encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, &state->loadout_stats[INF_GEAR_MAGE]); @@ -585,6 +575,41 @@ static void assert_supply_doses(const char* label, ASSERT_INT_EQ(buf, player->stamina_doses, expected.stamina_doses); } +static int test_occupied_inventory_cells(const InfernoState* s) { + int occupied = 0; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { + if (!osrs_inventory_cell_is_empty(&s->inventory_cells[c])) + occupied++; + } + return occupied; +} + +static int test_cell_holding_item(const InfernoState* s, uint8_t item) { + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { + if (s->inventory_cells[c].item_idx == item) + return c; + } + return -1; +} + +static int test_cell_doses_of_kind(const InfernoState* s, OsrsConsumableKind kind) { + int doses = 0; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { + const OsrsInventoryCell* cell = &s->inventory_cells[c]; + if (cell->raw_osrs_id == 0) continue; + if (osrs_consumable_click_lookup_raw_osrs_id( + cell->raw_osrs_id).consumable_kind == kind) + doses += cell->dose; + } + return doses; +} + +static OsrsConsumableKind test_drink_click_kind(const InfernoState* s, int action) { + if (action <= 0) return OSRS_CONSUMABLE_NONE; + return osrs_consumable_click_lookup_raw_osrs_id( + s->inventory_cells[action - 1].raw_osrs_id).consumable_kind; +} + static void test_final_wave_reward_applies_healer_tags_and_heal_cost(void) { printf("--- final-wave reward applies healer tags and heal cost ---\n"); @@ -1362,6 +1387,7 @@ static void init_ranged_offensive_prayer_test_state(InfernoState* state) { state->npcs[0].x = 13; state->npcs[0].y = 10; encounter_apply_loadout(&state->player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(state); encounter_compute_loadout_stats(INF_MAX_RANGE_FAST_LOADOUT, ATTACK_STYLE_RANGED, state->player.offensive_prayer, 99, FIGHT_STYLE_RAPID, 0, &state->loadout_stats[INF_GEAR_BP]); @@ -1500,7 +1526,8 @@ static void test_idle_diagnostics_count_missed_attack_opportunities(void) { InfernoState state = make_test_state(10, 10); state.weapon_set = INF_GEAR_BP; - state.loadout_stats[INF_GEAR_BP].attack_range = 7; + encounter_apply_loadout(&state.player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(&state); state.npcs[0] = make_test_npc( INF_NPC_RANGER, 14, 10, INF_NPC_STATS[INF_NPC_RANGER].size); state.npcs[0].active = 1; @@ -1710,6 +1737,18 @@ static void test_inferno_reset_supplies_match_current_inventory(void) { reset_inferno_at_public_wave(raw_state, 1, 1.0f); assert_supply_doses("wave 1", &state->player, full); + ASSERT_INT_EQ("brew cells match brew counter", + test_cell_doses_of_kind(state, OSRS_CONSUMABLE_BREW), + state->player.brew_doses); + ASSERT_INT_EQ("restore cells match restore counter", + test_cell_doses_of_kind(state, OSRS_CONSUMABLE_SUPER_RESTORE), + state->player.restore_doses); + ASSERT_INT_EQ("bastion cells match bastion counter", + test_cell_doses_of_kind(state, OSRS_CONSUMABLE_BASTION), + state->player.bastion_doses); + ASSERT_INT_EQ("stamina cells match stamina counter", + test_cell_doses_of_kind(state, OSRS_CONSUMABLE_STAMINA), + state->player.stamina_doses); inf_destroy(raw_state); } @@ -1718,39 +1757,6 @@ static int test_inventory_potion_vials(int doses) { return (doses + 3) / 4; } -static int test_player_inventory_occupied_slots(Player* p) { - int occupied = 0; - - for (int s = 0; s < NUM_GEAR_SLOTS; s++) { - for (int i = 0; i < p->num_items_in_slot[s]; i++) { - uint8_t item = p->inventory[s][i]; - if (item == ITEM_NONE) continue; - - int is_equipped = 0; - for (int e = 0; e < NUM_GEAR_SLOTS; e++) { - if (p->equipped[e] == item) { - is_equipped = 1; - break; - } - } - if (!is_equipped) - occupied++; - } - } - - occupied += p->food_count; - occupied += p->karambwan_count; - occupied += test_inventory_potion_vials(p->brew_doses); - occupied += test_inventory_potion_vials(p->restore_doses); - occupied += test_inventory_potion_vials(p->combat_potion_doses); - occupied += test_inventory_potion_vials(p->ranged_potion_doses); - occupied += test_inventory_potion_vials(p->bastion_doses); - occupied += test_inventory_potion_vials(p->stamina_doses); - occupied += test_inventory_potion_vials(p->antivenom_doses); - occupied += test_inventory_potion_vials(p->prayer_pot_doses); - return occupied; -} - static int test_player_slot_inventory_contains( const Player* p, int gear_slot, @@ -1774,8 +1780,8 @@ static void test_inferno_reset_inventory_leaves_one_empty_slot(void) { ASSERT_INT_EQ("full kit bastion doses", state->player.bastion_doses, 16); ASSERT_INT_EQ("full kit bastion vials", test_inventory_potion_vials(state->player.bastion_doses), 4); - ASSERT_INT_EQ("full kit occupied inventory slots", - test_player_inventory_occupied_slots(&state->player), 27); + ASSERT_INT_EQ("full kit occupied inventory cells", + test_occupied_inventory_cells(state), 27); inf_destroy(raw_state); } @@ -1851,8 +1857,8 @@ static void test_inferno_budget_profile_reset_uses_budget_gear(void) { ASSERT_INT_EQ("budget range legs available", test_player_slot_inventory_contains( &state->player, GEAR_SLOT_LEGS, ITEM_CRYSTAL_LEGS), 1); - ASSERT_INT_EQ("budget inventory leaves one empty slot", - test_player_inventory_occupied_slots(&state->player), 27); + ASSERT_INT_EQ("budget inventory leaves one empty cell", + test_occupied_inventory_cells(state), 27); inf_destroy(raw_state); } @@ -1890,8 +1896,8 @@ static void test_inferno_mixed_profile_sampling_respects_fraction(void) { inf_destroy(raw_c); } -static void test_inferno_gear_actions_map_to_active_profile_slots(void) { - printf("--- inferno gear actions map to active profile slots ---\n"); +static void test_inferno_equip_actions_move_cells_and_sync_weapon_set(void) { + printf("--- inferno equip actions move cells and sync weapon set ---\n"); EncounterState* raw_state = inf_create(); InfernoState* state = (InfernoState*)raw_state; @@ -1901,25 +1907,44 @@ static void test_inferno_gear_actions_map_to_active_profile_slots(void) { inf_reset(raw_state, 789u); memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_GEAR] = 2; + actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = + test_cell_holding_item(state, ITEM_BOW_OF_FAERDHINEN) + 1; + actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_BODY)] = + test_cell_holding_item(state, ITEM_CRYSTAL_BODY) + 1; inf_tick_player(state, actions, 1); - ASSERT_INT_EQ("gear action 2 selects long range", + ASSERT_INT_EQ("equipping bowfa syncs long-range weapon set", state->weapon_set, INF_GEAR_LONG_RANGE); ASSERT_INT_EQ("long range budget weapon", state->player.equipped[GEAR_SLOT_WEAPON], ITEM_BOW_OF_FAERDHINEN); + ASSERT_INT_EQ("two-handed bow displaces budget shield", + state->player.equipped[GEAR_SLOT_SHIELD], ITEM_NONE); + ASSERT_INT_EQ("body head equips crystal body in same tick", + state->player.equipped[GEAR_SLOT_BODY], ITEM_CRYSTAL_BODY); + ASSERT_INT_EQ("equipped bowfa left its cell", + test_cell_holding_item(state, ITEM_BOW_OF_FAERDHINEN), -1); + ASSERT_INT_EQ("displaced wand returns to a cell", + test_cell_holding_item(state, ITEM_DRAGON_HUNTER_WAND) >= 0, 1); + ASSERT_INT_EQ("displaced shield returns to a cell", + test_cell_holding_item(state, ITEM_CRYSTAL_SHIELD) >= 0, 1); + ASSERT_INT_EQ("displaced robe top returns to a cell", + test_cell_holding_item(state, ITEM_AHRIMS_ROBETOP) >= 0, 1); memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_GEAR] = 3; + actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = + test_cell_holding_item(state, ITEM_TOXIC_BLOWPIPE) + 1; inf_tick_player(state, actions, 1); - ASSERT_INT_EQ("gear action 3 selects fast range", + ASSERT_INT_EQ("equipping blowpipe syncs fast-range weapon set", state->weapon_set, INF_GEAR_BP); ASSERT_INT_EQ("fast range budget weapon", state->player.equipped[GEAR_SLOT_WEAPON], ITEM_TOXIC_BLOWPIPE); + ASSERT_INT_EQ("displaced bowfa returns to a cell", + test_cell_holding_item(state, ITEM_BOW_OF_FAERDHINEN) >= 0, 1); memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_GEAR] = 1; + actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = + test_cell_holding_item(state, ITEM_DRAGON_HUNTER_WAND) + 1; inf_tick_player(state, actions, 1); - ASSERT_INT_EQ("gear action 1 selects mage", + ASSERT_INT_EQ("equipping wand syncs mage weapon set", state->weapon_set, INF_GEAR_MAGE); ASSERT_INT_EQ("mage budget weapon", state->player.equipped[GEAR_SLOT_WEAPON], ITEM_DRAGON_HUNTER_WAND); @@ -1927,6 +1952,47 @@ static void test_inferno_gear_actions_map_to_active_profile_slots(void) { inf_destroy(raw_state); } +/* the random-action golden battery cannot see this: it drives INF_HEAD_MOVE > 0 on + 24/25 ticks, and that move interrupt already clears the interaction. */ +static void test_inferno_gear_switch_cancels_entity_interaction(void) { + printf("--- inferno gear switch cancels entity interaction ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + int actions[INF_NUM_ACTION_HEADS]; + + inf_put_int(raw_state, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + inf_reset(raw_state, 789u); + + int npc_slot = -1; + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (state->npcs[i].active) { npc_slot = i; break; } + } + ASSERT_INT_EQ("reset spawns an npc to target", npc_slot >= 0, 1); + + int body_cell = test_cell_holding_item(state, ITEM_CRYSTAL_BODY); + ASSERT_INT_EQ("budget reset carries a crystal body", body_cell >= 0, 1); + + osrs_interaction_set(&state->interaction, npc_slot); + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = body_cell + 1; + inf_tick_player(state, actions, 1); + ASSERT_INT_EQ("a click that equips nothing keeps interaction", + osrs_interaction_active(&state->interaction), 1); + + osrs_interaction_set(&state->interaction, npc_slot); + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = + test_cell_holding_item(state, ITEM_BOW_OF_FAERDHINEN) + 1; + inf_tick_player(state, actions, 1); + ASSERT_INT_EQ("gear switch equips the clicked weapon", + state->player.equipped[GEAR_SLOT_WEAPON], ITEM_BOW_OF_FAERDHINEN); + ASSERT_INT_EQ("gear switch clears interaction", + osrs_interaction_active(&state->interaction), 0); + + inf_destroy(raw_state); +} + static void test_inferno_reset_preserves_reward_config(void) { printf("--- inferno reset preserves reward config ---\n"); @@ -1998,7 +2064,7 @@ static void test_supply_milestone_reward_defaults_off(void) { InfernoState state = make_test_state(24, 24); test_config()->late_start_supply_profile_scale = 1.0f; state.player.brew_doses = 24; - state.player.restore_doses = 40; + state.player.restore_doses = 36; ASSERT_FLOAT_NEAR("default supply milestone reward", test_supply_milestone_surplus_reward(&state, 64), 0.0f, 0.0001f); @@ -2014,10 +2080,10 @@ static void test_supply_milestone_reward_pays_surplus_at_anchor_once(void) { test_config()->supply_milestone_brew_reward_coeff = 0.24f; test_config()->supply_milestone_restore_reward_coeff = 0.20f; state.player.brew_doses = 18; - state.player.restore_doses = 24; + state.player.restore_doses = 27; ASSERT_FLOAT_NEAR("wave 64 supply surplus reward", - test_supply_milestone_surplus_reward(&state, 64), 0.06f, 0.0001f); + test_supply_milestone_surplus_reward(&state, 64), 0.09f, 0.0001f); ASSERT_FLOAT_NEAR("wave 64 supply surplus pays once", test_supply_milestone_surplus_reward(&state, 64), 0.0f, 0.0001f); } @@ -2037,7 +2103,7 @@ static void test_supply_milestone_reward_never_penalizes_shortage(void) { test_config()->late_start_supply_profile_scale = 1.0f; test_config()->supply_milestone_brew_reward_coeff = 0.24f; non_anchor.player.brew_doses = 24; - non_anchor.player.restore_doses = 40; + non_anchor.player.restore_doses = 36; ASSERT_FLOAT_NEAR("non-anchor reward is zero", test_supply_milestone_surplus_reward(&non_anchor, 63), 0.0f, 0.0001f); } @@ -2139,7 +2205,7 @@ static void test_curriculum_supply_no_brew_is_curriculum_only(void) { ASSERT_INT_EQ("curriculum start applies no-brew", state->player.brew_doses, 0); ASSERT_INT_EQ("curriculum no-brew leaves restores alone", - state->player.restore_doses, 6); + state->player.restore_doses, 5); inf_put_int(raw_state, "curriculum_agent", 0); inf_put_int(raw_state, "curriculum_no_brew_mode", @@ -2188,7 +2254,7 @@ static void test_curriculum_supply_jitter_clamps_to_inventory_bounds(void) { ASSERT_INT_EQ("jitter keeps restore nonnegative", state->player.restore_doses >= 0, 1); ASSERT_INT_EQ("jitter keeps restore within full supplies", - state->player.restore_doses <= 40, 1); + state->player.restore_doses <= 36, 1); inf_put_int(raw_state, "curriculum_agent", 0); inf_put_int(raw_state, "curriculum_supply_jitter_mode", @@ -3299,18 +3365,25 @@ static void test_triple_jad_pending_threats_fit_obs_layout(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); - ASSERT_INT_EQ("inferno obs shape includes exact spark slots", INF_NUM_OBS, 1570); + ASSERT_INT_EQ("inferno obs shape includes exact spark slots", INF_NUM_OBS, 3432); } static void test_inferno_obs_shape_includes_step_out_forecast_features(void) { printf("--- inferno obs shape includes step-out forecast features ---\n"); - ASSERT_INT_EQ("gear action head removed tank slot", - INF_ACTION_DIMS[INF_HEAD_GEAR], 4); + ASSERT_INT_EQ("equip heads span every gear slot", + INF_HEAD_EAT - INF_HEAD_EQUIP_BASE, NUM_GEAR_SLOTS); + ASSERT_INT_EQ("equip head clicks cover every cell", + INF_ACTION_DIMS[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)], + OSRS_INVENTORY_SIZE + 1); + ASSERT_INT_EQ("eat head clicks cover every cell", + INF_ACTION_DIMS[INF_HEAD_EAT], OSRS_INVENTORY_SIZE + 1); + ASSERT_INT_EQ("drink head clicks cover every cell", + INF_ACTION_DIMS[INF_HEAD_DRINK], OSRS_INVENTORY_SIZE + 1); ASSERT_INT_EQ("prayer action head includes redemption", INF_ACTION_DIMS[INF_HEAD_PRAYER], ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION); - ASSERT_INT_EQ("action mask includes redemption", - INF_ACTION_MASK_SIZE, 89); + ASSERT_INT_EQ("action mask spans all inventory click heads", + INF_ACTION_MASK_SIZE, 457); ASSERT_SOURCE_BLOCK_CONTAINS("native binding reuses inferno action dims", "ocean/osrs_inferno/osrs_inferno.h", "#define OBS_SIZE INF_NUM_OBS", @@ -3321,13 +3394,17 @@ static void test_inferno_obs_shape_includes_step_out_forecast_features(void) { ASSERT_INT_EQ("pillar obs includes footprint size", INF_PILLAR_OBS_SIZE, 15); ASSERT_INT_EQ("npc obs includes loadout reachability signals", - INF_TOTAL_NPC_OBS_SIZE, 896); + INF_TOTAL_NPC_OBS_SIZE, 1776); ASSERT_INT_EQ("step-out forecast covers every movement action", INF_STEP_OUT_FORECAST_OBS_SIZE, 200); ASSERT_INT_EQ("inferno obs shape includes exact spark landings", INF_PENDING_SPARK_OBS_SIZE, 224); - ASSERT_INT_EQ("inferno obs shape includes reachability pass", - INF_NUM_OBS, 1570); + ASSERT_INT_EQ("inventory obs covers every cell", + INF_INVENTORY_OBS_SIZE, OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES); + ASSERT_INT_EQ("equipped obs covers every gear slot", + INF_EQUIPPED_OBS_SIZE, NUM_GEAR_SLOTS * OSRS_EQUIPPED_SELF_OBS_FEATURES); + ASSERT_INT_EQ("inferno obs shape includes inventory cells", + INF_NUM_OBS, 3432); ASSERT_INFERNO_SOURCE_NOT_CONTAINS("armor_tank state is removed", "armor_tank"); ASSERT_INFERNO_SOURCE_NOT_CONTAINS("extra npc obs scaffold is removed", @@ -3601,47 +3678,9 @@ static void test_jad_melee_stays_instant_and_untelegraphed(void) { ASSERT_INT_EQ("jad can still choose melee instantly at fire time", saw_melee, 1); } -static int inferno_obs_slot_type(int slot_idx) { - if (slot_idx >= 0 && slot_idx < 2) return INF_NPC_MAGER; - if (slot_idx >= 2 && slot_idx < 4) return INF_NPC_RANGER; - if (slot_idx >= 4 && slot_idx < 6) return INF_NPC_MELEER; - if (slot_idx >= 6 && slot_idx < 8) return INF_NPC_BLOB; - if (slot_idx >= 8 && slot_idx < 10) return INF_NPC_BAT; - if (slot_idx >= 10 && slot_idx < 12) return INF_NPC_BLOB_MAGE; - if (slot_idx >= 12 && slot_idx < 14) return INF_NPC_BLOB_RANGE; - if (slot_idx >= 14 && slot_idx < 16) return INF_NPC_BLOB_MELEE; - if (slot_idx >= 16 && slot_idx < 22) return INF_NPC_NIBBLER; - if (slot_idx >= 22 && slot_idx < 25) return INF_NPC_JAD; - if (slot_idx == 25) return INF_NPC_ZUK; - if (slot_idx == 26) return INF_NPC_ZUK_SHIELD; - if (slot_idx >= 27 && slot_idx < 33) return INF_NPC_HEALER_JAD; - if (slot_idx >= 33 && slot_idx < 37) return INF_NPC_HEALER_ZUK; - return -1; -} - -static int inferno_obs_slot_feature_count(int slot_idx) { - int type = inferno_obs_slot_type(slot_idx); - int has_style = (type == INF_NPC_BLOB || type == INF_NPC_JAD); - int has_scan = (type == INF_NPC_BLOB); - int has_los = (type != INF_NPC_NIBBLER && type != INF_NPC_MELEER && - type != INF_NPC_HEALER_JAD && type != INF_NPC_ZUK_SHIELD); - int has_target_category = (type != INF_NPC_NIBBLER && - type != INF_NPC_ZUK_SHIELD); - int has_timer = (type != INF_NPC_NIBBLER && type != INF_NPC_HEALER_JAD && - type != INF_NPC_ZUK_SHIELD); - int has_targeted = 1; - int has_meleer_dig = (type == INF_NPC_MELEER); - - return 11 + has_timer + 3 * has_style + has_los + 3 * has_scan + - 4 * has_target_category + has_targeted + 1 + 6 + 3 * has_meleer_dig; -} - static int inferno_obs_slot_start(int slot_idx) { - int start = INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE; - for (int i = 0; i < slot_idx; i++) { - start += inferno_obs_slot_feature_count(i); - } - return start; + return INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + + slot_idx * INF_NPC_SLOT_FEATURES; } static int inferno_pillar_obs_start(int pillar_idx) { @@ -3669,98 +3708,84 @@ static int inferno_spark_obs_start(void) { INF_FEATURES_PER_HIT * ENCOUNTER_MAX_PENDING_HITS; } -static int inferno_obs_slot_barrage_count_index(int slot_idx) { - int type = inferno_obs_slot_type(slot_idx); - int has_style = (type == INF_NPC_BLOB || type == INF_NPC_JAD); - int has_scan = (type == INF_NPC_BLOB); - int has_los = (type != INF_NPC_NIBBLER && type != INF_NPC_MELEER && - type != INF_NPC_HEALER_JAD && type != INF_NPC_ZUK_SHIELD); - int has_timer = (type != INF_NPC_NIBBLER && type != INF_NPC_HEALER_JAD && - type != INF_NPC_ZUK_SHIELD); - - return inferno_obs_slot_start(slot_idx) + 4 + has_timer + - 3 * has_style + has_los + 3 * has_scan; +static int inferno_obs_slot_hp_index(int slot_idx) { + return inferno_obs_slot_start(slot_idx) + 14; } static int inferno_obs_slot_size_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 3; + return inferno_obs_slot_start(slot_idx) + 17; } static int inferno_obs_slot_npc_los_index(int slot_idx) { - int type = inferno_obs_slot_type(slot_idx); - int has_timer = (type != INF_NPC_NIBBLER && type != INF_NPC_HEALER_JAD && - type != INF_NPC_ZUK_SHIELD); - int has_style = (type == INF_NPC_BLOB || type == INF_NPC_JAD); - return inferno_obs_slot_start(slot_idx) + 4 + has_timer + 3 * has_style; + return inferno_obs_slot_start(slot_idx) + 22; +} + +static int inferno_obs_slot_barrage_count_index(int slot_idx) { + return inferno_obs_slot_start(slot_idx) + 26; } static int inferno_obs_slot_edge_distance_index(int slot_idx) { - return inferno_obs_slot_barrage_count_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 27; } static int inferno_obs_slot_npc_can_attack_if_ready_index(int slot_idx) { - return inferno_obs_slot_edge_distance_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 28; } static int inferno_obs_slot_npc_can_attack_this_tick_index(int slot_idx) { - return inferno_obs_slot_npc_can_attack_if_ready_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 29; } static int inferno_obs_slot_frozen_index(int slot_idx) { - return inferno_obs_slot_npc_can_attack_this_tick_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 30; } static int inferno_obs_slot_player_can_attack_index(int slot_idx) { - return inferno_obs_slot_frozen_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 31; } static int inferno_obs_slot_player_has_los_index(int slot_idx) { - return inferno_obs_slot_player_can_attack_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 32; } static int inferno_obs_slot_player_can_mage_index(int slot_idx) { - return inferno_obs_slot_player_has_los_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 33; } static int inferno_obs_slot_player_can_long_range_index(int slot_idx) { - return inferno_obs_slot_player_can_mage_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 34; } static int inferno_obs_slot_player_can_blowpipe_index(int slot_idx) { - return inferno_obs_slot_player_can_long_range_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 35; } static int inferno_obs_slot_mage_range_deficit_index(int slot_idx) { - return inferno_obs_slot_player_can_blowpipe_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 36; } static int inferno_obs_slot_long_range_deficit_index(int slot_idx) { - return inferno_obs_slot_mage_range_deficit_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 37; } static int inferno_obs_slot_blowpipe_range_deficit_index(int slot_idx) { - return inferno_obs_slot_long_range_deficit_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 38; } static int inferno_obs_slot_target_category_start(int slot_idx) { - return inferno_obs_slot_barrage_count_index(slot_idx) + 13; + return inferno_obs_slot_start(slot_idx) + 39; } static int inferno_obs_slot_targeted_index(int slot_idx) { - int type = inferno_obs_slot_type(slot_idx); - int has_target_category = (type != INF_NPC_NIBBLER && - type != INF_NPC_ZUK_SHIELD); - - return inferno_obs_slot_target_category_start(slot_idx) + - 4 * has_target_category; + return inferno_obs_slot_start(slot_idx) + 43; } static int inferno_obs_slot_phantom_index(int slot_idx) { - return inferno_obs_slot_targeted_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 44; } static int inferno_obs_slot_dig_index(int slot_idx) { - return inferno_obs_slot_phantom_index(slot_idx) + 1; + return inferno_obs_slot_start(slot_idx) + 45; } static void init_threat_obs_state(InfernoState* state, int player_x, int player_y) { @@ -4945,6 +4970,8 @@ static void init_zuk_timing_state(InfernoState* state) { state->tick_at_first_zuk_healer_attack = -1; state->weapon_set = INF_GEAR_LONG_RANGE; osrs_interaction_init(&state->interaction); + encounter_apply_loadout(&state->player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(state); encounter_compute_loadout_stats(INF_MAX_RANGE_LONG_LOADOUT, ATTACK_STYLE_RANGED, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, &state->loadout_stats[INF_GEAR_LONG_RANGE]); @@ -4971,6 +4998,7 @@ static void init_zuk_timing_state(InfernoState* state) { static void equip_zuk_timing_state_blowpipe(InfernoState* state) { state->weapon_set = INF_GEAR_BP; encounter_apply_loadout(&state->player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(state); encounter_compute_loadout_stats(INF_MAX_RANGE_FAST_LOADOUT, ATTACK_STYLE_RANGED, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, &state->loadout_stats[INF_GEAR_BP]); @@ -5674,6 +5702,7 @@ static void test_zuk_healer_target_action_tags_on_landed_hit(void) { state.player.x = 20; state.player.y = 46; encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(&state); state.npcs[2] = make_test_npc( INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); @@ -5742,6 +5771,7 @@ static void test_zuk_healer_mage_attack_counts_penalty_event(void) { state.player.autocast_enabled = 1; state.player.autocast_spell = ENCOUNTER_SPELL_ICE; encounter_apply_loadout(&state.player, INF_MAX_MAGE_LOADOUT, GEAR_MAGE); + inf_refresh_live_stats(&state); encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, &state.loadout_stats[INF_GEAR_MAGE]); @@ -5775,6 +5805,7 @@ static void test_zuk_safe_healer_target_mask_requires_fire_window(void) { init_zuk_timing_state(&state); test_config()->zuk_safe_untagged_healer_target_mask = 1; encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(&state); state.npcs[2] = make_test_npc( INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); @@ -5823,6 +5854,7 @@ static void test_zuk_safe_healer_target_mask_clears_unsafe_target(void) { state.player.y = 46; state.player.attack_timer = 3; encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(&state); state.npcs[2] = make_test_npc( INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); @@ -5848,6 +5880,7 @@ static void test_zuk_force_safe_healer_target_mask_blocks_idle_when_safe(void) { init_zuk_timing_state(&state); test_config()->zuk_force_safe_untagged_healer_target_mask = 1; encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(&state); state.player.x = 24; state.player.y = 46; state.player.attack_timer = 0; @@ -5895,6 +5928,7 @@ static void test_zuk_force_safe_healer_target_mask_clears_stale_target(void) { init_zuk_timing_state(&state); test_config()->zuk_force_safe_untagged_healer_target_mask = 1; encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(&state); state.player.x = 24; state.player.y = 46; state.player.attack_timer = 0; @@ -6095,8 +6129,84 @@ static void test_npc_player_projectile_delays_use_reference_options(void) { ASSERT_INT_EQ("ranger queued one pending hit", state.player_pending_hits.count, 1); - ASSERT_INT_EQ("ranger pending hit lands one tick before the raw projectile delay", - state.player_pending_hits.hits[0].ticks_remaining, timing.damage_delay_ticks - 1); + /* Was `- 1`. The queue is resolved before NPCs throw and lands on + --ticks_remaining <= 0, so a hit queued with the raw delay D lands exactly D + ticks after the throw tick, which is what the section-8 table states. The old + expectation pinned every inferno NPC->player hit one tick early. */ + ASSERT_INT_EQ("ranger pending hit carries the raw projectile delay", + state.player_pending_hits.hits[0].ticks_remaining, timing.damage_delay_ticks); +} + +/** Regression: an NPC hit thrown on tick N must land on tick N + hit_delay. */ +static void test_npc_hit_lands_on_the_reference_tick(void) { + printf("--- npc hit lands on the reference tick ---\n"); + + const struct { const char* label; InfNPCType type; AttackStyle style; int x; int y; } + cases[] = { + { "ranger d=5", INF_NPC_RANGER, ATTACK_STYLE_RANGED, 29, 24 }, + { "ranger d=8", INF_NPC_RANGER, ATTACK_STYLE_RANGED, 32, 24 }, + { "mager d=5", INF_NPC_MAGER, ATTACK_STYLE_MAGIC, 29, 24 }, + { "mager d=9", INF_NPC_MAGER, ATTACK_STYLE_MAGIC, 33, 24 }, + }; + + for (size_t c = 0; c < sizeof(cases) / sizeof(cases[0]); c++) { + InfernoState* s = (InfernoState*)inf_create(); + inf_reset((EncounterState*)s, 20260728u); + inf_lab_apply_command(s, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_CLEAR_NPCS }); + inf_lab_apply_command(s, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_PLAYER, + .as.tile = { .x = 24, .y = 24 }, + }); + inf_lab_apply_command(s, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SPAWN_NPC, + .as.spawn_npc = { + .slot = 0, .type = cases[c].type, .x = cases[c].x, .y = cases[c].y, + .hp = { .kind = INF_LAB_OPTIONAL_INT_UNSET }, + .timer = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 0 }, + }, + }); + + InfNPC* npc = &s->npcs[0]; + npc->attack_style = cases[c].style; + npc->aggro_target = -1; + npc->had_los_last_tick = 1; + s->player.prayer = PRAYER_NONE; + s->wave_spawn_delay = 0; + s->wave_ready_delay = 0; + + int dist = encounter_projectile_distance( + npc->x, npc->y, npc->size, s->player.x, s->player.y, 1, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + int expected_delay = + inf_npc_projectile_timing(cases[c].type, cases[c].style, dist) + .damage_delay_ticks; + + int actions[INF_NUM_ACTION_HEADS] = {0}; + int throw_tick = -1; + int land_tick = -1; + + for (int t = 1; t <= 20 && land_tick < 0; t++) { + int hp_before = s->player.current_hitpoints; + npc->x = cases[c].x; + npc->y = cases[c].y; + inf_step((EncounterState*)s, actions); + + if (throw_tick < 0 && npc->attacked_this_tick) { + throw_tick = t; + npc->attack_timer = 10000; + for (int i = 0; i < s->player_pending_hits.count; i++) { + if (!s->player_pending_hits.hits[i].active) continue; + s->player_pending_hits.hits[i].damage = 7; + s->player_pending_hits.hits[i].hit_success = 1; + } + } else if (throw_tick >= 0 && s->player.current_hitpoints != hp_before) { + land_tick = t; + } + } + + ASSERT_INT_EQ(cases[c].label, land_tick - throw_tick, expected_delay); + inf_destroy((EncounterState*)s); + } } static void test_player_projectile_timing_uses_reference_options(void) { @@ -6148,6 +6258,7 @@ static void init_phantom_barrage_test_state( state->player.autocast_enabled = 1; state->player.autocast_spell = ENCOUNTER_SPELL_ICE; encounter_apply_loadout(&state->player, INF_MAX_MAGE_LOADOUT, GEAR_MAGE); + inf_refresh_live_stats(state); encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, &state->loadout_stats[INF_GEAR_MAGE]); @@ -6224,6 +6335,7 @@ static void test_ranged_attack_cannot_fire_on_dying_target(void) { init_spell_cast_test_state(&state, INF_NPC_NIBBLER); state.weapon_set = INF_GEAR_LONG_RANGE; encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(&state); state.player.autocast_enabled = 0; state.player.attack_timer = 0; state.npcs[0].hp = 0; @@ -6395,6 +6507,7 @@ static void init_confliction_barrage_test_state( state->player_dest_y = -1; osrs_interaction_init(&state->interaction); encounter_apply_loadout(&state->player, INF_MAX_MAGE_LOADOUT, GEAR_MAGE); + inf_refresh_live_stats(state); encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, &state->loadout_stats[INF_GEAR_MAGE]); @@ -6804,6 +6917,7 @@ static void test_manual_spell_in_range_gear_uses_range_gear_magic_stats(void) { init_spell_cast_test_state(&state, INF_NPC_RANGER); state.weapon_set = INF_GEAR_BP; encounter_apply_loadout(&state.player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(&state); state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; InfPlayerAttack attack; @@ -6831,6 +6945,7 @@ static void test_phantom_barrage_allows_explicit_spell_from_range_gear(void) { init_phantom_barrage_test_state(&state, 1, 1); state.weapon_set = INF_GEAR_BP; encounter_apply_loadout(&state.player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(&state); state.player.autocast_enabled = 1; state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; inf_refresh_current_obs_slots(&state); @@ -6900,12 +7015,12 @@ static void test_zuk_obs_tracks_shield_and_mager_aggro(void) { int mager_slot = 0; int shield_slot = 26; - int shield_start = inferno_obs_slot_start(shield_slot); + int shield_hp = inferno_obs_slot_hp_index(shield_slot); int mager_target_category = inferno_obs_slot_target_category_start(mager_slot); ASSERT_INT_EQ("first mager occupies mager slot 0", state.current_obs_slots[mager_slot], 0); ASSERT_INT_EQ("shield occupies dedicated shield slot", state.current_obs_slots[shield_slot], 2); - ASSERT_FLOAT_NEAR("shield hp ratio visible in shield slot", obs[shield_start], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("shield hp ratio visible in shield slot", obs[shield_hp], 0.5f, 1e-6f); ASSERT_FLOAT_NEAR("mager target_player off while on shield", obs[mager_target_category], 0.0f, 1e-6f); ASSERT_FLOAT_NEAR("mager target_shield on while on shield", @@ -6925,7 +7040,7 @@ static void test_zuk_obs_tracks_shield_and_mager_aggro(void) { inf_write_mask((EncounterState*)&state, mask); ASSERT_INT_EQ("dead shield drops out of shield slot", state.current_obs_slots[shield_slot], -1); - ASSERT_FLOAT_NEAR("dead shield slot hp zeros out", obs[shield_start], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("dead shield slot hp zeros out", obs[shield_hp], 0.0f, 1e-6f); ASSERT_FLOAT_NEAR("dead shield zeroes stale direction", obs[INF_OBS_ZUK_SHIELD_DIR], 0.0f, 1e-6f); ASSERT_FLOAT_NEAR("dead shield zeroes stale freeze", obs[INF_OBS_ZUK_SHIELD_FREEZE], 0.0f, 1e-6f); ASSERT_FLOAT_NEAR("mager target_player flips on", @@ -7243,32 +7358,46 @@ static void test_human_target_and_potion_translation(void) { ASSERT_INT_EQ("shield click is rejected", actions[INF_HEAD_TARGET], 0); + state.player.brew_doses = 8; + state.player.restore_doses = 8; + state.player.bastion_doses = 4; + state.player.stamina_doses = 4; + inf_seed_inventory_cells(&state); + hi = make_human_input(); hi.pending_potion = POTION_BREW; inf_translate_human_input(&hi, actions, (EncounterState*)&state); - ASSERT_INT_EQ("brew still maps to eat head", actions[INF_HEAD_EAT], 1); - ASSERT_INT_EQ("brew does not touch potion head", actions[INF_HEAD_POTION], 0); + ASSERT_INT_EQ("brew maps to a brew drink click", + test_drink_click_kind(&state, actions[INF_HEAD_DRINK]), + OSRS_CONSUMABLE_BREW); + ASSERT_INT_EQ("brew does not touch eat head", actions[INF_HEAD_EAT], 0); hi = make_human_input(); hi.pending_potion = POTION_RESTORE; inf_translate_human_input(&hi, actions, (EncounterState*)&state); - ASSERT_INT_EQ("restore maps to potion 1", actions[INF_HEAD_POTION], 1); + ASSERT_INT_EQ("restore maps to a restore drink click", + test_drink_click_kind(&state, actions[INF_HEAD_DRINK]), + OSRS_CONSUMABLE_SUPER_RESTORE); hi = make_human_input(); hi.pending_potion = POTION_BASTION; inf_translate_human_input(&hi, actions, (EncounterState*)&state); - ASSERT_INT_EQ("bastion maps to potion 2", actions[INF_HEAD_POTION], 2); + ASSERT_INT_EQ("bastion maps to a bastion drink click", + test_drink_click_kind(&state, actions[INF_HEAD_DRINK]), + OSRS_CONSUMABLE_BASTION); hi = make_human_input(); hi.pending_potion = POTION_STAMINA; inf_translate_human_input(&hi, actions, (EncounterState*)&state); - ASSERT_INT_EQ("stamina maps to potion 3", actions[INF_HEAD_POTION], 3); + ASSERT_INT_EQ("stamina maps to a stamina drink click", + test_drink_click_kind(&state, actions[INF_HEAD_DRINK]), + OSRS_CONSUMABLE_STAMINA); hi = make_human_input(); hi.pending_potion = POTION_PRAYER_POT; inf_translate_human_input(&hi, actions, (EncounterState*)&state); ASSERT_INT_EQ("prayer pot no longer aliases to restore", - actions[INF_HEAD_POTION], 0); + actions[INF_HEAD_DRINK], 0); } } @@ -7397,6 +7526,7 @@ static void test_autocast_is_inactive_with_non_autocast_weapon(void) { init_spell_cast_test_state(&state, INF_NPC_RANGER); state.weapon_set = INF_GEAR_BP; encounter_apply_loadout(&state.player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + inf_refresh_live_stats(&state); state.player.autocast_enabled = 1; state.player.autocast_spell = ENCOUNTER_SPELL_ICE; @@ -7779,7 +7909,8 @@ static void test_inferno_state_assignment_copy_replays_trajectory(void) { int prefix[INF_NUM_ACTION_HEADS] = {0}; prefix[INF_HEAD_MOVE] = 1; prefix[INF_HEAD_TARGET] = 1; - prefix[INF_HEAD_GEAR] = INF_GEAR_LONG_RANGE; + prefix[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = + test_cell_holding_item(&state_a, ITEM_TWISTED_BOW) + 1; for (int i = 0; i < 9; i++) inf_step_ctx((EncounterState*)&state_a, (EncounterContext*)&ctx_a, prefix); @@ -7838,188 +7969,6 @@ static void test_inferno_refresh_after_state_load_rebuilds_derived_state(void) { state.npc_los_cache[0], -1); } -static void test_inferno_cell_key_is_deterministic_and_16_bytes(void) { - printf("--- inferno cell key is deterministic and 16 bytes ---\n"); - - EncounterState* raw = inf_create(); - inf_reset(raw, 99u); - - ASSERT_INT_EQ("sizeof(InfCellKey) == 16", (int)sizeof(InfCellKey), 16); - ASSERT_INT_EQ("inf_cell_key_size returns sizeof(InfCellKey)", - (int)inf_cell_key_size(raw), (int)sizeof(InfCellKey)); - - ASSERT_INT_EQ("progress_score == 0 at reset", - (int)(inf_progress_score(raw) * 1000.0f), 0); - - int actions[INF_NUM_ACTION_HEADS] = {0}; - actions[INF_HEAD_MOVE] = 3; - actions[INF_HEAD_TARGET] = 1; - actions[INF_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; - actions[INF_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY; - - inf_reset(raw, 99u); - for (int i = 0; i < 20; i++) inf_step(raw, actions); - InfCellKey key_a; - inf_write_cell_key(raw, &key_a); - - inf_reset(raw, 99u); - for (int i = 0; i < 20; i++) inf_step(raw, actions); - InfCellKey key_b; - inf_write_cell_key(raw, &key_b); - - int diff = memcmp(&key_a, &key_b, sizeof(InfCellKey)); - ASSERT_INT_EQ("memcmp(key_a, key_b) == 0 for same seed+actions", diff, 0); - - inf_reset(raw, 99u); - InfCellKey key_at_reset; - inf_write_cell_key(raw, &key_at_reset); - int diff_post = memcmp(&key_at_reset, &key_a, sizeof(InfCellKey)); - ASSERT_INT_EQ("key at reset differs from key after stepping", (diff_post != 0) ? 1 : 0, 1); - - inf_destroy(raw); -} - -static void test_inferno_cell_key_quantization_groups_neighbors(void) { - printf("--- inferno cell key quantization groups neighbor states ---\n"); - - EncounterState* raw_a = inf_create(); - EncounterState* raw_b = inf_create(); - InfernoState* state_a = (InfernoState*)raw_a; - InfernoState* state_b = (InfernoState*)raw_b; - - inf_reset(raw_a, 1u); - inf_reset(raw_b, 1u); - - state_b->player.x = state_a->player.x + 1; - - InfCellKey key_a, key_b; - inf_write_cell_key(raw_a, &key_a); - inf_write_cell_key(raw_b, &key_b); - int diff_within_bin = memcmp(&key_a, &key_b, sizeof(InfCellKey)); - ASSERT_INT_EQ("1-tile player shift stays in same cell", diff_within_bin, 0); - - state_b->player.x = state_a->player.x + 2; - inf_write_cell_key(raw_b, &key_b); - int diff_cross_bin = memcmp(&key_a, &key_b, sizeof(InfCellKey)); - ASSERT_INT_EQ("2-tile player shift changes cell", (diff_cross_bin != 0) ? 1 : 0, 1); - - inf_destroy(raw_a); - inf_destroy(raw_b); -} - -static void test_inferno_cell_key_tracks_set_magers_and_jad_hp_bucket(void) { - printf("--- inferno cell key tracks set magers and jad hp bucket ---\n"); - - InfernoState state; - init_zuk_timing_state(&state); - - state.npcs[2] = make_test_npc( - INF_NPC_MAGER, 20, 36, INF_NPC_STATS[INF_NPC_MAGER].size); - state.npcs[2].active = 1; - state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; - - state.npcs[3] = make_test_npc( - INF_NPC_RANGER, 26, 36, INF_NPC_STATS[INF_NPC_RANGER].size); - state.npcs[3].active = 1; - state.npcs[3].hp = state.npcs[3].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; - - state.npcs[4] = make_test_npc( - INF_NPC_MELEER, 24, 36, INF_NPC_STATS[INF_NPC_MELEER].size); - state.npcs[4].active = 1; - state.npcs[4].hp = state.npcs[4].max_hp = INF_NPC_STATS[INF_NPC_MELEER].hp; - - state.npcs[5] = make_test_npc( - INF_NPC_JAD, 24, 32, INF_NPC_STATS[INF_NPC_JAD].size); - state.npcs[5].active = 1; - state.npcs[5].hp = 253; - state.npcs[5].max_hp = INF_NPC_STATS[INF_NPC_JAD].hp; - state.player.attack_timer = 6; - - InfCellKey key; - inf_write_cell_key((EncounterState*)&state, &key); - - ASSERT_INT_EQ("active_set_count includes mager ranger meleer", - key.active_set_count, 3); - ASSERT_INT_EQ("jad_hp_bin uses live Jad HP / 50", - key.jad_hp_bin, 5); - ASSERT_INT_EQ("cell key stores player attack timer bucket", - key.offensive_prayer_attack_timer >> 4, 6); -} - -static void test_inferno_progress_score_rewards_late_add_transitions(void) { - printf("--- inferno progress score rewards late add transitions ---\n"); - - InfernoState jad_alive; - init_zuk_timing_state(&jad_alive); - jad_alive.zuk.jad_spawned = 1; - jad_alive.min_zuk_hp_seen = 300.0f; - jad_alive.npcs[0].hp = 300; - jad_alive.npcs[2] = make_test_npc( - INF_NPC_JAD, 24, 32, INF_NPC_STATS[INF_NPC_JAD].size); - jad_alive.npcs[2].active = 1; - jad_alive.npcs[2].hp = jad_alive.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_JAD].hp; - - InfernoState jad_dead = jad_alive; - jad_dead.npcs[2].active = 0; - jad_dead.npcs[2].hp = 0; - - float q_jad_alive = inf_progress_score((EncounterState*)&jad_alive); - float q_jad_dead = inf_progress_score((EncounterState*)&jad_dead); - ASSERT_FLOAT_GT("same Zuk HP with Jad dead scores higher", - q_jad_dead, q_jad_alive); - - InfernoState jad_damaged = jad_alive; - jad_damaged.npcs[2].hp = jad_damaged.npcs[2].max_hp / 2; - float q_jad_damaged = inf_progress_score((EncounterState*)&jad_damaged); - ASSERT_FLOAT_GT("same Zuk HP with Jad damaged scores higher", - q_jad_damaged, q_jad_alive); - ASSERT_FLOAT_GT("Jad dead scores above partial Jad damage", - q_jad_dead, q_jad_damaged); - - InfernoState healer_alive = jad_dead; - healer_alive.zuk.healer_spawned = 1; - healer_alive.min_zuk_hp_seen = 180.0f; - healer_alive.npcs[0].hp = 180; - healer_alive.npcs[2] = make_test_npc( - INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); - healer_alive.npcs[2].active = 1; - healer_alive.npcs[2].hp = healer_alive.npcs[2].max_hp = - INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; - - InfernoState healer_dead = healer_alive; - healer_dead.npcs[2].active = 0; - healer_dead.npcs[2].hp = 0; - - float q_healer_alive = inf_progress_score((EncounterState*)&healer_alive); - float q_healer_dead = inf_progress_score((EncounterState*)&healer_dead); - ASSERT_FLOAT_GT("same Zuk HP with healers cleared scores higher", - q_healer_dead, q_healer_alive); - - InfernoState set_alive; - init_zuk_timing_state(&set_alive); - set_alive.min_zuk_hp_seen = 850.0f; - set_alive.npcs[0].hp = 850; - set_alive.npcs[2] = make_test_npc( - INF_NPC_MAGER, 20, 36, INF_NPC_STATS[INF_NPC_MAGER].size); - set_alive.npcs[2].active = 1; - set_alive.npcs[2].hp = set_alive.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; - - InfernoState set_dead = set_alive; - set_dead.npcs[2].active = 0; - set_dead.npcs[2].hp = 0; - - float q_set_alive = inf_progress_score((EncounterState*)&set_alive); - float q_set_dead = inf_progress_score((EncounterState*)&set_dead); - ASSERT_FLOAT_GT("same Zuk HP with set cleared scores higher", - q_set_dead, q_set_alive); - - InfernoState win = healer_alive; - win.episode_over = 1; - win.winner = INF_OUTCOME_PLAYER_WON; - float q_win = inf_progress_score((EncounterState*)&win); - ASSERT_FLOAT_GT("win scores above partial state", q_win, q_healer_dead); -} - static void test_inferno_healer_transition_stats_track_episode_progress(void) { printf("--- inferno healer transition stats track episode progress ---\n"); @@ -9403,7 +9352,8 @@ int main(void) { test_inferno_max_profile_reset_uses_existing_gear(); test_inferno_budget_profile_reset_uses_budget_gear(); test_inferno_mixed_profile_sampling_respects_fraction(); - test_inferno_gear_actions_map_to_active_profile_slots(); + test_inferno_equip_actions_move_cells_and_sync_weapon_set(); + test_inferno_gear_switch_cancels_entity_interaction(); test_inferno_reset_preserves_reward_config(); test_supply_milestone_reward_defaults_off(); test_supply_milestone_reward_pays_surplus_at_anchor_once(); @@ -9475,6 +9425,7 @@ int main(void) { test_set_attack_to_shield_is_projectile_delayed(); test_npc_target_projectile_delays_match_reference(); test_npc_player_projectile_delays_use_reference_options(); + test_npc_hit_lands_on_the_reference_tick(); test_player_projectile_timing_uses_reference_options(); test_phantom_barrage_target_is_masked_until_cast_window(); test_phantom_barrage_hits_aoe_on_first_cast_window(); @@ -9552,10 +9503,6 @@ int main(void) { test_inferno_snapshot_preserves_external_pointers(); test_inferno_state_assignment_copy_replays_trajectory(); test_inferno_refresh_after_state_load_rebuilds_derived_state(); - test_inferno_cell_key_is_deterministic_and_16_bytes(); - test_inferno_cell_key_quantization_groups_neighbors(); - test_inferno_cell_key_tracks_set_magers_and_jad_hp_bucket(); - test_inferno_progress_score_rewards_late_add_transitions(); test_inferno_healer_transition_stats_track_episode_progress(); test_inferno_human_equip_does_not_snap_loadout(); test_jad_render_uses_style_specific_attack_animation(); diff --git a/ocean/osrs/tests/test_inferno_golden.c b/ocean/osrs/tests/test_inferno_golden.c index 2a017a4550..45ab2fb1e3 100644 --- a/ocean/osrs/tests/test_inferno_golden.c +++ b/ocean/osrs/tests/test_inferno_golden.c @@ -42,6 +42,7 @@ static uint64_t run_episode(int start_wave, uint32_t seed, int max_ticks) { InfernoState* s = (InfernoState*)state; static float obs[INF_NUM_OBS]; + static float mask[INF_ACTION_MASK_SIZE]; int actions[INF_NUM_ACTION_HEADS]; uint64_t arng = ((uint64_t)seed << 20) ^ (uint64_t)(start_wave + 1) ^ 0xD1B54A32D192ED03ULL; @@ -56,10 +57,14 @@ static uint64_t run_episode(int start_wave, uint32_t seed, int max_ticks) { inf_step(state, actions); inf_write_obs(state, obs); + inf_write_mask(state, mask); for (int i = 0; i < INF_NUM_OBS; i++) { h = fnv_f32(h, obs[i]); } + for (int i = 0; i < INF_ACTION_MASK_SIZE; i++) { + h = fnv_f32(h, mask[i]); + } h = fnv_f32(h, s->reward); h = fnv_i32(h, s->wave); h = fnv_i32(h, s->tick); @@ -101,22 +106,21 @@ static const GoldenConfig CONFIGS[] = { #define EPISODE_TICKS 2000 static const uint64_t BASELINE[NUM_CONFIGS] = { - - 0x9d8970300cea947aULL, - 0xefeefc062898de1bULL, - 0x300b40b9b6c32f47ULL, - 0xf600c7a9f79479faULL, - 0x267ab0fac9ad5b27ULL, - 0x999a41e1a0916ab9ULL, - 0x2ddb91b645db1e75ULL, - 0xd2a1416c4b53157fULL, - 0x84a2ab3540f37ac8ULL, - 0xb98928c437e48005ULL, - 0x3e5a885e173d4674ULL, - 0xd6577872951242fdULL, - 0xc5af52a73611f3f6ULL, - 0x7a84c874f2b7c5ceULL, - 0x28c4ccc92a588192ULL, + 0x2d6c652157e87d57ULL, /* wave1_a */ + 0x84a62101fb408366ULL, /* wave1_b */ + 0x12c1265c673b0418ULL, /* wave1_c */ + 0x045770f82fdf144fULL, /* meleer_a */ + 0x33cdc2618bb61ef1ULL, /* meleer_b */ + 0xd853cb4e587c1a74ULL, /* ranger_a */ + 0xf8048ba7439ccafcULL, /* ranger_b */ + 0x462c210eda8590ecULL, /* mager_a */ + 0xa569619024c1f9fdULL, /* mager_b */ + 0x2a78f20fa4e0e699ULL, /* jad_a */ + 0x5aa060baae4b5f73ULL, /* jad_b */ + 0x107f3a8080b123b5ULL, /* jad_c */ + 0xadcc53c5a3d6444fULL, /* zuk_a */ + 0xdba6ec9381f73491ULL, /* zuk_b */ + 0xb97088c0a1fd42a3ULL, /* zuk_c */ }; int main(int argc, char** argv) { diff --git a/ocean/osrs/tests/test_osrs_eat_attack_delay.c b/ocean/osrs/tests/test_osrs_eat_attack_delay.c new file mode 100644 index 0000000000..2f220033b9 --- /dev/null +++ b/ocean/osrs/tests/test_osrs_eat_attack_delay.c @@ -0,0 +1,89 @@ +/** + * Engine contract (osrs-engine-quirks section 12): food adds its attack delay + * ONLY to an existing cooldown. From a ready weapon, eating adds nothing. + * Karambwan adds 2, standard food 3, and they stack onto a live cooldown. + */ +#include +#include +#include "ocean/osrs/encounters/encounter_colosseum.h" +#include "ocean/osrs/tests/osrs_test_check.h" + +static Player ready_player(void) { + Player p; + memset(&p, 0, sizeof(p)); + p.base_hitpoints = 99; + p.current_hitpoints = 50; + p.attack_timer = 0; + p.attack_timer_uncapped = 0; + p.has_attack_timer = 0; + return p; +} + +static void test_engine_contract(void) { + Player p = ready_player(); + osrs_player_eat_food_effects(&p, FOOD_SHARK); + CHECK("shark from ready adds no attack delay", p.attack_timer == 0); + + p = ready_player(); + osrs_player_eat_food_effects(&p, FOOD_KARAMBWAN); + CHECK("karambwan from ready adds no attack delay", p.attack_timer == 0); + + p = ready_player(); + p.attack_timer = 4; + osrs_player_eat_food_effects(&p, FOOD_SHARK); + CHECK("shark adds 3 to an existing cooldown", p.attack_timer == 7); + + p = ready_player(); + p.attack_timer = 4; + osrs_player_eat_food_effects(&p, FOOD_KARAMBWAN); + CHECK("karambwan adds 2 to an existing cooldown", p.attack_timer == 6); + + /* the bug: has_attack_timer unset must not erase a live cooldown */ + p = ready_player(); + p.attack_timer = 5; + p.has_attack_timer = 0; + osrs_player_eat_food_effects(&p, FOOD_SHARK); + CHECK("a live cooldown survives the first eat when has_attack_timer is unset", p.attack_timer == 8); + + /* food then karambwan on a live cooldown stacks 3 + 2 */ + p = ready_player(); + p.attack_timer = 4; + osrs_player_eat_food_effects(&p, FOOD_SHARK); + osrs_player_eat_food_effects(&p, FOOD_KARAMBWAN); + CHECK("shark then karambwan stack onto a live cooldown", p.attack_timer == 9); + + p = ready_player(); + p.current_hitpoints = 50; + osrs_player_eat_food_effects(&p, FOOD_SHARK); + CHECK("eating still heals from ready", p.current_hitpoints > 50); +} + +static void test_colosseum_charges_the_cooldown(void) { + ColosseumContext ctx; + static ColosseumState s; + col_init_context_typed(&ctx); + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242u); + + s.player.current_hitpoints = 20; + s.player.attack_timer = 4; + s.player.food_timer = 0; + s.player.karambwan_timer = 0; + int before = s.player.current_hitpoints; + + int consumed = col_apply_food_cell(&s, OSRS_CONSUMABLE_SHARK_FOOD); + CHECK("colosseum shark is consumed when the food timer is clear", consumed == 1); + CHECK("colosseum shark heals", s.player.current_hitpoints > before); + CHECK("colosseum eating charges the attack cooldown instead of being free", s.player.attack_timer == 7); + + /* refused eat must report not-consumed so the caller keeps the food */ + s.player.food_timer = 3; + int refused = col_apply_food_cell(&s, OSRS_CONSUMABLE_SHARK_FOOD); + CHECK("colosseum eat under an active food timer is refused", refused == 0); +} + +int main(void) { + test_engine_contract(); + test_colosseum_charges_the_cooldown(); + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/test_osrs_inventory_actions.c b/ocean/osrs/tests/test_osrs_inventory_actions.c new file mode 100644 index 0000000000..e9ec45c1ff --- /dev/null +++ b/ocean/osrs/tests/test_osrs_inventory_actions.c @@ -0,0 +1,152 @@ +#include +#include + +#include "../osrs_inventory_actions.h" +#include "osrs_test_check.h" + +static uint32_t test_rng = 0x1234567u; + +static uint32_t next_rand(void) { + test_rng ^= test_rng << 13; + test_rng ^= test_rng >> 17; + test_rng ^= test_rng << 5; + return test_rng; +} + +static int rand_below(int n) { + return (int)(next_rand() % (uint32_t)n); +} + +static const uint8_t TEST_ITEM_POOL[] = { + ITEM_MYSTIC_HAT, ITEM_GOD_CAPE, ITEM_GLORY, ITEM_AMETHYST_ARROW, + ITEM_TRIDENT_OF_SWAMP, ITEM_BOOK_OF_DARKNESS, ITEM_MYSTIC_TOP, + ITEM_MYSTIC_BOTTOM, ITEM_BARROWS_GLOVES, ITEM_MYSTIC_BOOTS, + ITEM_RING_OF_RECOIL, ITEM_TWISTED_BOW, ITEM_BOW_OF_FAERDHINEN, + ITEM_MASORI_MASK_F, ITEM_MASORI_BODY_F, ITEM_ELIDINIS_WARD_F, +}; +#define TEST_ITEM_POOL_SIZE ((int)(sizeof(TEST_ITEM_POOL) / sizeof(*TEST_ITEM_POOL))) + +static void count_items( + const Player* p, const OsrsInventoryCell* cells, int counts[NUM_ITEMS] +) { + memset(counts, 0, NUM_ITEMS * sizeof(int)); + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + if (p->equipped[slot] != ITEM_NONE) counts[p->equipped[slot]]++; + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) + if (cells[i].item_idx != ITEM_NONE) counts[cells[i].item_idx]++; +} + +static void run_equip_storm(int trial) { + Player p; + memset(&p, 0, sizeof(p)); + memset(p.equipped, ITEM_NONE, NUM_GEAR_SLOTS); + p.base_hitpoints = 99; + p.current_hitpoints = 50; + p.base_prayer = 99; + + OsrsInventoryCell cells[OSRS_INVENTORY_SIZE]; + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) + cells[i] = osrs_inventory_cell_empty(); + + int num_seeded = 4 + rand_below(OSRS_INVENTORY_SIZE - 6); + for (int i = 0; i < num_seeded; i++) + cells[i] = osrs_inventory_cell_from_item( + TEST_ITEM_POOL[rand_below(TEST_ITEM_POOL_SIZE)]); + osrs_refresh_player_equipment(&p); + + int before[NUM_ITEMS]; + int after[NUM_ITEMS]; + + for (int step = 0; step < 200; step++) { + count_items(&p, cells, before); + + OsrsInventoryClickActions clicks; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + clicks.equip_by_slot[slot] = rand_below(OSRS_INVENTORY_SIZE + 1); + clicks.eat = rand_below(OSRS_INVENTORY_SIZE + 1); + clicks.drink = rand_below(OSRS_INVENTORY_SIZE + 1); + + OsrsInventoryTickIntent intent = + osrs_resolve_inventory_tick_intent(&p, cells, &clicks); + OsrsInventoryApplyStep apply_step; + while (osrs_inventory_intent_next(&intent, &apply_step)) { + CHECK("gear-only storm must only yield equip steps", + apply_step.kind == OSRS_INVENTORY_APPLY_EQUIP); + int slot = osrs_equip_from_cell(&p, cells, apply_step.cell_idx); + if (slot >= 0) { + CHECK("equip must leave an item in the slot", + p.equipped[slot] != ITEM_NONE); + CHECK("equipped item must match its gear slot", + osrs_item_gear_slot(p.equipped[slot]) == slot); + } + } + + count_items(&p, cells, after); + for (int item = 0; item < NUM_ITEMS; item++) { + if (before[item] == after[item]) continue; + fprintf(stderr, "trial %d step %d: item %d count %d -> %d\n", + trial, step, item, before[item], after[item]); + CHECK("equip storm must conserve the item multiset", 0); + return; + } + + uint8_t weapon = p.equipped[GEAR_SLOT_WEAPON]; + if (weapon != ITEM_NONE && item_is_two_handed(weapon)) { + CHECK("two-handed weapon must never coexist with a shield", + p.equipped[GEAR_SLOT_SHIELD] == ITEM_NONE); + } + } +} + +static void run_drink_dose_check(void) { + Player p; + memset(&p, 0, sizeof(p)); + memset(p.equipped, ITEM_NONE, NUM_GEAR_SLOTS); + p.base_hitpoints = 99; + p.current_hitpoints = 99; + + OsrsInventoryCell cells[OSRS_INVENTORY_SIZE]; + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) + cells[i] = osrs_inventory_cell_empty(); + cells[0] = osrs_inventory_cell_from_raw_osrs_id(2434); + ASSERT_INT_EQ("prayer potion seeds with 4 doses", cells[0].dose, 4); + + for (int expected_dose = 4; expected_dose >= 1; expected_dose--) { + p.potion_timer = 0; + OsrsInventoryClickActions clicks; + memset(&clicks, 0, sizeof(clicks)); + clicks.drink = 1; + OsrsInventoryTickIntent intent = + osrs_resolve_inventory_tick_intent(&p, cells, &clicks); + OsrsInventoryApplyStep apply_step; + int steps = 0; + while (osrs_inventory_intent_next(&intent, &apply_step)) { + ASSERT_INT_EQ("drink click must yield a drink step", + apply_step.kind, OSRS_INVENTORY_APPLY_DRINK); + ASSERT_INT_EQ("resolution must carry the current dose count", + apply_step.resolution.dose_count, expected_dose); + osrs_inventory_cell_decrement_drink(&cells[0], apply_step.resolution); + steps++; + } + ASSERT_INT_EQ("one drink step per tick", steps, 1); + ASSERT_INT_EQ("dose decrements by one", cells[0].dose, expected_dose - 1); + } + CHECK("cell empties after the last dose", + osrs_inventory_cell_is_empty(&cells[0])); + + p.potion_timer = 3; + OsrsInventoryClickActions clicks; + memset(&clicks, 0, sizeof(clicks)); + clicks.drink = 1; + OsrsInventoryTickIntent intent = + osrs_resolve_inventory_tick_intent(&p, cells, &clicks); + CHECK("empty cell resolves to no drink intent", + !osrs_inventory_tick_intent_has_effect(&intent)); +} + +int main(void) { + for (int trial = 0; trial < 500; trial++) + run_equip_storm(trial); + run_drink_dose_check(); + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c new file mode 100644 index 0000000000..611b19da0b --- /dev/null +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -0,0 +1,185 @@ +/** + * @file test_osrs_player_step.c + * @brief Shared player-step command contract: one click per tick. + * + * The engine cannot both start an attack and walk to a chosen tile on the same + * tick. A ground click cancels the entity interaction; an entity click cancels + * the walk. These pin that contract at the shared layer, where every encounter + * now routes through OsrsPlayerCommand. + */ + +#include +#include + +#include "ocean/osrs/osrs_encounter_player.h" + +static int tests_run = 0; +static int tests_failed = 0; + +#define CHECK(label, cond) do { \ + tests_run++; \ + if (!(cond)) { \ + tests_failed++; \ + printf(" FAIL: %s\n", (label)); \ + } \ +} while (0) + +#define STEP_GRID 24 + +typedef struct { + OsrsAttackTarget target; + int target_valid; +} StepTargetCtx; + +static int step_tile_walkable(void* ctx, int x, int y) { + (void)ctx; + return x >= 0 && x < STEP_GRID && y >= 0 && y < STEP_GRID; +} + +static int step_lookup_target(void* ctx, int target_slot, OsrsAttackTarget* out) { + StepTargetCtx* c = (StepTargetCtx*)ctx; + if (!c->target_valid || target_slot != c->target.slot) return 0; + *out = c->target; + return 1; +} + +static OsrsEncounterArena step_arena(void) { + OsrsEncounterArena arena; + memset(&arena, 0, sizeof(arena)); + arena.is_walkable = step_tile_walkable; + arena.los_query = osrs_los_open_query(); + arena.arena_w = STEP_GRID; + arena.arena_h = STEP_GRID; + return arena; +} + +/* an entity click cancels a walk already in flight: the player closes on the + target instead of continuing to the clicked tile. this is the nh_pvp/zulrah + regression — EXPLICIT_FIRST used to honour both, giving free damage while + repositioning. */ +static void test_target_command_cancels_walk_in_flight(void) { + printf("--- entity click cancels a walk in flight ---\n"); + + Player player; + memset(&player, 0, sizeof(player)); + player.x = 5; player.y = 5; + player.run_energy = 10000; + + OsrsInteraction interaction; + osrs_interaction_init(&interaction); + + StepTargetCtx tctx = { + .target = { .slot = 3, .x = 5, .y = 12, .size = 1, .attack_range = 1 }, + .target_valid = 1, + }; + + int dest_x = 5, dest_y = 1; /* walking south, target is north */ + OsrsEncounterArena arena = step_arena(); + + OsrsPlayerStepInput input; + memset(&input, 0, sizeof(input)); + input.player = &player; + input.interaction = &interaction; + input.target_lookup = step_lookup_target; + input.target_ctx = &tctx; + input.command.kind = OSRS_PLAYER_CMD_TARGET; + input.command.target_slot = 3; + input.dest_x = &dest_x; + input.dest_y = &dest_y; + input.arena = arena; + + OsrsPlayerStepResult r = osrs_encounter_player_step(&input); + + CHECK("interaction is set to the clicked entity", interaction.target_slot == 3); + CHECK("walk destination is cancelled", dest_x == -1 && dest_y == -1); + CHECK("no explicit move ran", r.explicit_moved == 0); + CHECK("player closed on the target, not the clicked tile", player.y > 5); +} + +/* a ground click cancels the interaction and walks. */ +static void test_move_command_cancels_interaction(void) { + printf("--- ground click cancels the interaction ---\n"); + + Player player; + memset(&player, 0, sizeof(player)); + player.x = 5; player.y = 5; + player.run_energy = 10000; + + OsrsInteraction interaction; + osrs_interaction_init(&interaction); + osrs_interaction_set(&interaction, 3); + + StepTargetCtx tctx = { + .target = { .slot = 3, .x = 5, .y = 12, .size = 1, .attack_range = 1 }, + .target_valid = 1, + }; + + int dest_x = 5, dest_y = 1; + OsrsEncounterArena arena = step_arena(); + + OsrsPlayerStepInput input; + memset(&input, 0, sizeof(input)); + input.player = &player; + input.interaction = &interaction; + input.target_lookup = step_lookup_target; + input.target_ctx = &tctx; + input.command.kind = OSRS_PLAYER_CMD_MOVE; + input.command.move_kind = OSRS_PLAYER_MOVE_DESTINATION; + input.dest_x = &dest_x; + input.dest_y = &dest_y; + input.arena = arena; + + OsrsPlayerStepResult r = osrs_encounter_player_step(&input); + + CHECK("interaction is cancelled", !osrs_interaction_active(&interaction)); + CHECK("explicit move ran", r.explicit_moved == 1); + CHECK("player walked toward the clicked tile", player.y < 5); +} + +/* no click: an active interaction keeps auto-chasing. */ +static void test_none_command_chases_active_interaction(void) { + printf("--- idle tick auto-chases the standing interaction ---\n"); + + Player player; + memset(&player, 0, sizeof(player)); + player.x = 5; player.y = 5; + player.run_energy = 10000; + + OsrsInteraction interaction; + osrs_interaction_init(&interaction); + osrs_interaction_set(&interaction, 3); + + StepTargetCtx tctx = { + .target = { .slot = 3, .x = 5, .y = 12, .size = 1, .attack_range = 1 }, + .target_valid = 1, + }; + + int dest_x = -1, dest_y = -1; + OsrsEncounterArena arena = step_arena(); + + OsrsPlayerStepInput input; + memset(&input, 0, sizeof(input)); + input.player = &player; + input.interaction = &interaction; + input.target_lookup = step_lookup_target; + input.target_ctx = &tctx; + input.command.kind = OSRS_PLAYER_CMD_NONE; + input.dest_x = &dest_x; + input.dest_y = &dest_y; + input.arena = arena; + + OsrsPlayerStepResult r = osrs_encounter_player_step(&input); + + CHECK("interaction survives an idle tick", osrs_interaction_active(&interaction)); + CHECK("chase ran", r.chased_target == 1); + CHECK("no explicit move ran", r.explicit_moved == 0); +} + +int main(void) { + test_target_command_cancels_walk_in_flight(); + test_move_command_cancels_interaction(); + test_none_command_chases_active_interaction(); + + printf("\n%d/%d tests passed\n", tests_run - tests_failed, tests_run); + return tests_failed == 0 ? 0 : 1; +} diff --git a/ocean/osrs/tests/test_zulrah_golden.c b/ocean/osrs/tests/test_zulrah_golden.c new file mode 100644 index 0000000000..b239054667 --- /dev/null +++ b/ocean/osrs/tests/test_zulrah_golden.c @@ -0,0 +1,195 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_zulrah.h" + +#define FNV_OFFSET 1469598103934665603ULL +#define FNV_PRIME 1099511628211ULL + +static inline uint64_t fnv_bytes(uint64_t h, const void* p, size_t n) { + const uint8_t* b = (const uint8_t*)p; + for (size_t i = 0; i < n; i++) { + h ^= b[i]; + h *= FNV_PRIME; + } + return h; +} + +static inline uint64_t fnv_f32(uint64_t h, float v) { + uint32_t bits; + memcpy(&bits, &v, sizeof(bits)); + return fnv_bytes(h, &bits, sizeof(bits)); +} + +static inline uint64_t fnv_i32(uint64_t h, int v) { + int32_t w = (int32_t)v; + return fnv_bytes(h, &w, sizeof(w)); +} + +typedef struct { + const char* name; + const char* description; + int gear_tier; + int episode_mode; + uint32_t env_seed; +} GoldenConfig; + +typedef struct { + int ticks; + int kills; + int winner; +} EpisodeStats; + +static void apply_env_config( + EncounterState* state, EncounterContext* context, const GoldenConfig* cfg +) { + ENCOUNTER_ZULRAH.put_int(state, context, "gear_tier", cfg->gear_tier); + ENCOUNTER_ZULRAH.put_int(state, context, "gear_tier_mode", ZUL_GEAR_TIER_FIXED); + ENCOUNTER_ZULRAH.put_int(state, context, "episode_mode", cfg->episode_mode); + + ENCOUNTER_ZULRAH.put_float(state, context, "gear_tier_weight_0", 0.5f); + ENCOUNTER_ZULRAH.put_float(state, context, "gear_tier_weight_1", 0.3f); + ENCOUNTER_ZULRAH.put_float(state, context, "gear_tier_weight_2", 0.2f); + ENCOUNTER_ZULRAH.put_float(state, context, "reward_win", 0.4510606370109398f); + ENCOUNTER_ZULRAH.put_float(state, context, "reward_loss_penalty", 0.0f); + ENCOUNTER_ZULRAH.put_float(state, context, "reward_damage_dealt", 0.25796024594409434f); + ENCOUNTER_ZULRAH.put_float(state, context, "reward_correct_style", 0.2157127789623018f); + ENCOUNTER_ZULRAH.put_float(state, context, "reward_damage_received_penalty", 0.0f); + ENCOUNTER_ZULRAH.put_float(state, context, "reward_cloud_occupancy_penalty", 0.0f); +} + +static uint64_t run_episode(const GoldenConfig* cfg, int max_ticks, EpisodeStats* stats) { + ZulrahContext ctx; + ZulrahState s; + static float obs[ZUL_NUM_OBS]; + static float mask[ZUL_ACTION_MASK_SIZE]; + int actions[ZUL_NUM_ACTION_HEADS]; + + EncounterState* state = (EncounterState*)&s; + EncounterContext* context = (EncounterContext*)&ctx; + + ENCOUNTER_ZULRAH.init_context(context); + ENCOUNTER_ZULRAH.init_state(state, context); + apply_env_config(state, context, cfg); + ENCOUNTER_ZULRAH.reset(state, context, cfg->env_seed); + + uint64_t h = FNV_OFFSET; + + for (int t = 0; t < max_ticks; t++) { + zul_heuristic_actions(&s, actions); + ENCOUNTER_ZULRAH.step(state, context, actions); + ENCOUNTER_ZULRAH.write_obs(state, context, obs); + ENCOUNTER_ZULRAH.write_mask(state, context, mask); + + for (int i = 0; i < ZUL_NUM_OBS; i++) h = fnv_f32(h, obs[i]); + for (int i = 0; i < ZUL_ACTION_MASK_SIZE; i++) h = fnv_f32(h, mask[i]); + h = fnv_f32(h, s.reward); + h = fnv_i32(h, s.kills_this_episode); + h = fnv_i32(h, s.tick); + h = fnv_i32(h, s.episode_over); + h = fnv_i32(h, s.winner); + + if (s.episode_over) break; + } + + stats->ticks = s.tick; + stats->kills = s.kills_this_episode; + stats->winner = s.winner; + return h; +} + +static const GoldenConfig CONFIGS[] = { + { + "t0_single", + "gear tier 0 fixed, single-kill episode ends on first kill or death", + 0, ZUL_EPISODE_SINGLE_KILL, + 0x000A0001u, + }, + { + "t1_single", + "gear tier 1 fixed (thrall + augury), single-kill episode", + 1, ZUL_EPISODE_SINGLE_KILL, + 0x000A0003u, + }, + { + "t2_single", + "gear tier 2 fixed (saturated heart + thrall), single-kill episode", + 2, ZUL_EPISODE_SINGLE_KILL, + 0x000A0005u, + }, + { + "t0_trip", + "gear tier 0 fixed, trip episode chains kills until the tick cap", + 0, ZUL_EPISODE_TRIP, + 0x000B0007u, + }, + { + "t1_trip", + "gear tier 1 fixed, trip episode chains kills until the tick cap", + 1, ZUL_EPISODE_TRIP, + 0x000B000Bu, + }, + { + "t2_trip", + "gear tier 2 fixed, trip episode chains kills until the tick cap", + 2, ZUL_EPISODE_TRIP, + 0x000B000Du, + }, +}; + +#define NUM_CONFIGS ((int)(sizeof(CONFIGS) / sizeof(CONFIGS[0]))) +#define EPISODE_TICKS 700 + +static_assert(EPISODE_TICKS > ZUL_MAX_TICKS, + "episode budget must cover the in-sim tick cap"); + +static const uint64_t BASELINE[NUM_CONFIGS] = { + 0xecf8fefec11da90fULL, /* t0_single */ + 0xfaa0144d43a61972ULL, /* t1_single */ + 0x801a8acf962b8ff2ULL, /* t2_single */ + 0xdd701c2aac0eaa2fULL, /* t0_trip */ + 0xa9033115654c7a8aULL, /* t1_trip */ + 0x22e191b4b0fd500eULL, /* t2_trip */ +}; + +int main(int argc, char** argv) { + int print_mode = (argc > 1 && strcmp(argv[1], "--print") == 0); + + printf("zulrah golden-master (%d configs, <=%d ticks each)\n\n", + NUM_CONFIGS, EPISODE_TICKS); + + EpisodeStats stats[NUM_CONFIGS]; + + int failed = 0; + for (int c = 0; c < NUM_CONFIGS; c++) { + uint64_t h = run_episode(&CONFIGS[c], EPISODE_TICKS, &stats[c]); + if (print_mode) { + printf(" 0x%016llxULL, /* %s */\n", + (unsigned long long)h, CONFIGS[c].name); + } else { + int ok = h == BASELINE[c]; + printf(" %-12s 0x%016llx %s\n", + CONFIGS[c].name, (unsigned long long)h, ok ? "PASS" : "FAIL"); + if (!ok) { + printf(" expected 0x%016llx\n", + (unsigned long long)BASELINE[c]); + failed++; + } + } + } + + if (print_mode) { + printf("\nconfig coverage:\n"); + for (int c = 0; c < NUM_CONFIGS; c++) { + printf(" %-12s ticks=%-4d kills=%-2d winner=%d %s\n", + CONFIGS[c].name, stats[c].ticks, stats[c].kills, + stats[c].winner, CONFIGS[c].description); + } + printf("\npaste the array above into BASELINE[].\n"); + return 0; + } + + printf("\n%d/%d configs match baseline\n", NUM_CONFIGS - failed, NUM_CONFIGS); + return failed > 0 ? 1 : 0; +} diff --git a/ocean/osrs/tests/test_zulrah_hit_delay.c b/ocean/osrs/tests/test_zulrah_hit_delay.c new file mode 100644 index 0000000000..a9d469519d --- /dev/null +++ b/ocean/osrs/tests/test_zulrah_hit_delay.c @@ -0,0 +1,348 @@ +#include +#include + +#include "ocean/osrs/encounters/encounter_zulrah.h" +#include "ocean/osrs/tests/osrs_test_check.h" + +/** osrs-engine-quirks section 8, transcribed from the spec table rather than + from the sim. is_player adds the NPC-processes-first tick. */ +static int spec8_ranged_delay(int distance, int is_player) { + return 1 + (3 + distance) / 6 + (is_player ? 1 : 0); +} + +static int spec8_thrown_delay(int distance, int is_player) { + return 1 + distance / 6 + (is_player ? 1 : 0); +} + +static int spec8_magic_delay(int distance, int is_player) { + return 1 + (1 + distance) / 3 + (is_player ? 1 : 0); +} + +static ZulrahContext g_ctx; +static ZulrahState g_state; + +static ZulrahState* fresh_state(uint32_t seed) { + EncounterState* state = (EncounterState*)&g_state; + EncounterContext* context = (EncounterContext*)&g_ctx; + ENCOUNTER_ZULRAH.init_context(context); + ENCOUNTER_ZULRAH.init_state(state, context); + ENCOUNTER_ZULRAH.put_int(state, context, "gear_tier", 0); + ENCOUNTER_ZULRAH.put_int(state, context, "gear_tier_mode", ZUL_GEAR_TIER_FIXED); + ENCOUNTER_ZULRAH.put_int(state, context, "episode_mode", ZUL_EPISODE_SINGLE_KILL); + ENCOUNTER_ZULRAH.reset(state, context, seed); + return &g_state; +} + +/** Places the player `distance` tiles (edge to edge) due south of Zulrah. */ +static void place_at_distance(ZulrahState* s, int distance) { + s->zulrah.x = 10; + s->zulrah.y = 20; + s->player.x = 10; + s->player.y = s->zulrah.y - distance; + int actual = encounter_projectile_distance( + s->zulrah.x, s->zulrah.y, ZUL_NPC_SIZE, + s->player.x, s->player.y, 1, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + if (actual != distance) { + printf(" SETUP ERROR: wanted distance %d, got %d\n", distance, actual); + tests_failed++; + } +} + +/** Fires one Zulrah attack and returns the resolve pass on which the player's + hitpoints actually drop. 0 means it never landed within the budget. */ +static int ticks_until_player_hit(ZulrahState* s, AttackStyle style) { + s->player.prayer = PRAYER_NONE; + s->player.current_hitpoints = s->player.base_hitpoints; + + if (style == ATTACK_STYLE_RANGED) zul_attack_ranged(s); + else zul_attack_magic(s); + + if (encounter_pending_hit_queue_damage_sum(&s->player_pending_hits) <= 0) + return -1; + + for (int t = 1; t <= 16; t++) { + int before = s->player.current_hitpoints; + zul_resolve_player_pending_hits(s); + if (s->player.current_hitpoints < before) return t; + } + return 0; +} + +static int ticks_until_zulrah_hit(ZulrahState* s, AttackStyle style, int damage) { + s->zulrah.current_hitpoints = 500; + zul_queue_zulrah_hit(s, damage, style, 0); + + for (int t = 1; t <= 16; t++) { + int before = s->zulrah.current_hitpoints; + zul_resolve_zulrah_pending_hits(s); + if (s->zulrah.current_hitpoints < before) return t; + } + return 0; +} + +static void test_npc_hit_delay_by_distance(void) { + printf("--- Zulrah -> player hit delay matches section 8 ---\n"); + + static const int DISTANCES[] = { 1, 2, 3, 5, 6, 8, 9, 10 }; + const int n = (int)(sizeof(DISTANCES) / sizeof(DISTANCES[0])); + + for (int i = 0; i < n; i++) { + int d = DISTANCES[i]; + int expected = spec8_ranged_delay(d, 0); + + int landed = 0; + for (int attempt = 0; attempt < 64 && !landed; attempt++) { + ZulrahState* s = fresh_state(0x5EED0000u + (uint32_t)(d * 64 + attempt)); + place_at_distance(s, d); + int got = ticks_until_player_hit(s, ATTACK_STYLE_RANGED); + if (got < 0) continue; + landed = 1; + char label[96]; + snprintf(label, sizeof(label), "ranged delay at distance %d", d); + ASSERT_INT_EQ(label, got, expected); + } + CHECK("ranged attack eventually connects for the delay probe", landed); + } + + for (int i = 0; i < n; i++) { + int d = DISTANCES[i]; + int expected = spec8_magic_delay(d, 0); + + int landed = 0; + for (int attempt = 0; attempt < 64 && !landed; attempt++) { + ZulrahState* s = fresh_state(0x71D30000u + (uint32_t)(d * 64 + attempt)); + place_at_distance(s, d); + int got = ticks_until_player_hit(s, ATTACK_STYLE_MAGIC); + if (got < 0) continue; + landed = 1; + char label[96]; + snprintf(label, sizeof(label), "magic delay at distance %d", d); + ASSERT_INT_EQ(label, got, expected); + } + CHECK("magic attack eventually connects for the delay probe", landed); + } +} + +static void test_player_hit_delay_by_distance(void) { + printf("\n--- player -> Zulrah hit delay carries the +1 ---\n"); + + static const int DISTANCES[] = { 1, 2, 3, 5, 6, 8, 9, 10 }; + const int n = (int)(sizeof(DISTANCES) / sizeof(DISTANCES[0])); + + for (int i = 0; i < n; i++) { + int d = DISTANCES[i]; + ZulrahState* s = fresh_state(0x11110000u + (uint32_t)d); + place_at_distance(s, d); + + uint8_t weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + int is_thrown = (weapon == ITEM_TOXIC_BLOWPIPE); + + int expected_ranged = is_thrown + ? spec8_thrown_delay(d, 1) + : spec8_ranged_delay(d, 1); + char label[96]; + snprintf(label, sizeof(label), "player ranged delay at distance %d", d); + ASSERT_INT_EQ(label, + ticks_until_zulrah_hit(s, ATTACK_STYLE_RANGED, 7), expected_ranged); + } + + for (int i = 0; i < n; i++) { + int d = DISTANCES[i]; + ZulrahState* s = fresh_state(0x22220000u + (uint32_t)d); + place_at_distance(s, d); + s->player.equipped[GEAR_SLOT_WEAPON] = ITEM_SANGUINESTI_STAFF; + + char label[96]; + snprintf(label, sizeof(label), "player magic delay at distance %d", d); + ASSERT_INT_EQ(label, + ticks_until_zulrah_hit(s, ATTACK_STYLE_MAGIC, 7), + spec8_magic_delay(d, 1)); + } +} + +/** The canonical contract draws damage FIRST. Under the old inverted order the + accuracy roll consumed draw 1, so the queued damage could not match. */ +static void test_damage_drawn_before_accuracy(void) { + printf("\n--- damage roll precedes the accuracy roll ---\n"); + + int hits_checked = 0; + for (uint32_t seed = 0; seed < 400 && hits_checked < 40; seed++) { + ZulrahState* s = fresh_state(0x3C3C0000u + seed); + place_at_distance(s, 5); + s->player.prayer = PRAYER_NONE; + encounter_pending_hit_queue_clear(&s->player_pending_hits); + + uint32_t probe = s->rng_state; + int first_draw = encounter_rand_int( + &probe, MONSTER_DATABASE[MON_ZULRAH_GREEN].max_hit + 1); + + zul_attack_ranged(s); + + int queued = encounter_pending_hit_queue_damage_sum(&s->player_pending_hits); + if (queued <= 0) continue; + hits_checked++; + ASSERT_INT_EQ("ranged queued damage equals the first RNG draw", + queued, first_draw); + } + CHECK("collected ranged hits for the draw-order probe", hits_checked > 0); + + int magic_checked = 0; + for (uint32_t seed = 0; seed < 400 && magic_checked < 40; seed++) { + ZulrahState* s = fresh_state(0x4D4D0000u + seed); + place_at_distance(s, 5); + s->player.prayer = PRAYER_NONE; + encounter_pending_hit_queue_clear(&s->player_pending_hits); + + uint32_t probe = s->rng_state; + int first_draw = encounter_rand_int( + &probe, MONSTER_DATABASE[MON_ZULRAH_BLUE].max_hit + 1); + + zul_attack_magic(s); + + int queued = encounter_pending_hit_queue_damage_sum(&s->player_pending_hits); + if (queued <= 0) continue; + magic_checked++; + ASSERT_INT_EQ("magic queued damage equals the first RNG draw", + queued, first_draw); + } + CHECK("collected magic hits for the draw-order probe", magic_checked > 0); +} + +/** Prayer decides whether damage is frozen to zero, never how many draws the + attack consumes. This is what the skipped-roll defect broke. */ +static void test_prayer_does_not_perturb_the_rng_stream(void) { + printf("\n--- prayer state leaves the RNG stream untouched ---\n"); + + for (uint32_t seed = 0; seed < 32; seed++) { + ZulrahState* a = fresh_state(0x5A5A0000u + seed); + place_at_distance(a, 5); + a->player.prayer = PRAYER_NONE; + uint32_t rng_before = a->rng_state; + zul_attack_ranged(a); + uint32_t unprayed_rng = a->rng_state; + + ZulrahState* b = fresh_state(0x5A5A0000u + seed); + place_at_distance(b, 5); + b->player.prayer = PRAYER_PROTECT_RANGED; + b->rng_state = rng_before; + zul_attack_ranged(b); + + ASSERT_INT_EQ("ranged: prayer does not change RNG advancement", + (int)b->rng_state, (int)unprayed_rng); + ASSERT_INT_EQ("ranged: correct prayer freezes damage to zero", + encounter_pending_hit_queue_damage_sum(&b->player_pending_hits), 0); + } + + for (uint32_t seed = 0; seed < 32; seed++) { + ZulrahState* a = fresh_state(0x6B6B0000u + seed); + place_at_distance(a, 5); + a->player.prayer = PRAYER_NONE; + uint32_t rng_before = a->rng_state; + zul_attack_magic(a); + uint32_t unprayed_rng = a->rng_state; + + ZulrahState* b = fresh_state(0x6B6B0000u + seed); + place_at_distance(b, 5); + b->player.prayer = PRAYER_PROTECT_MAGIC; + b->rng_state = rng_before; + zul_attack_magic(b); + + ASSERT_INT_EQ("magic: prayer does not change RNG advancement", + (int)b->rng_state, (int)unprayed_rng); + ASSERT_INT_EQ("magic: correct prayer freezes damage to zero", + encounter_pending_hit_queue_damage_sum(&b->player_pending_hits), 0); + } +} + +/** Section 11: the protect check belongs to the tick the attack is calculated. + Flicking on after the throw must not save the player, and flicking off after + the throw must not doom one already blocked. */ +static void test_prayer_resolves_at_throw_not_landing(void) { + printf("\n--- protect prayer resolves at the throw tick ---\n"); + + int late_on_checked = 0; + for (uint32_t seed = 0; seed < 200 && late_on_checked < 20; seed++) { + ZulrahState* s = fresh_state(0x7E7E0000u + seed); + place_at_distance(s, 5); + s->player.prayer = PRAYER_NONE; + s->player.current_hitpoints = s->player.base_hitpoints; + encounter_pending_hit_queue_clear(&s->player_pending_hits); + + zul_attack_ranged(s); + if (encounter_pending_hit_queue_damage_sum(&s->player_pending_hits) <= 0) + continue; + late_on_checked++; + + s->player.prayer = PRAYER_PROTECT_RANGED; + for (int t = 0; t < 8; t++) zul_resolve_player_pending_hits(s); + + CHECK("praying after the throw does not cancel the hit", + s->player.current_hitpoints < s->player.base_hitpoints); + } + CHECK("collected throws for the late-prayer probe", late_on_checked > 0); + + for (uint32_t seed = 0; seed < 32; seed++) { + ZulrahState* s = fresh_state(0x8F8F0000u + seed); + place_at_distance(s, 5); + s->player.prayer = PRAYER_PROTECT_RANGED; + s->player.current_hitpoints = s->player.base_hitpoints; + encounter_pending_hit_queue_clear(&s->player_pending_hits); + + zul_attack_ranged(s); + s->player.prayer = PRAYER_NONE; + for (int t = 0; t < 8; t++) zul_resolve_player_pending_hits(s); + + ASSERT_INT_EQ("dropping prayer after the throw does not revive the hit", + s->player.current_hitpoints, s->player.base_hitpoints); + } +} + +/** Defect 3 mirror image: the melee stare froze its protect check at landing, + three ticks after Zulrah committed to the attack. */ +static void test_melee_stare_reads_prayer_at_calculation(void) { + printf("\n--- melee stare reads prayer at the calculation tick ---\n"); + + { + ZulrahState* s = fresh_state(0x9A9A0001u); + s->zulrah.x = 10; s->zulrah.y = 20; + s->player.x = 10; s->player.y = 18; + s->player.current_hitpoints = s->player.base_hitpoints; + s->player.prayer = PRAYER_NONE; + + zul_melee_start(s); + s->player.prayer = PRAYER_PROTECT_MELEE; + zul_melee_hit(s); + + CHECK("praying melee after the stare began still takes damage", + s->player.current_hitpoints < s->player.base_hitpoints); + } + + { + ZulrahState* s = fresh_state(0x9A9A0002u); + s->zulrah.x = 10; s->zulrah.y = 20; + s->player.x = 10; s->player.y = 18; + s->player.current_hitpoints = s->player.base_hitpoints; + s->player.prayer = PRAYER_PROTECT_MELEE; + + zul_melee_start(s); + s->player.prayer = PRAYER_NONE; + zul_melee_hit(s); + + ASSERT_INT_EQ("dropping melee prayer after the stare began stays blocked", + s->player.current_hitpoints, s->player.base_hitpoints); + } +} + +int main(void) { + printf("zulrah hit-delay and roll-order regressions\n\n"); + + test_npc_hit_delay_by_distance(); + test_player_hit_delay_by_distance(); + test_damage_drawn_before_accuracy(); + test_prayer_does_not_perturb_the_rng_stream(); + test_prayer_resolves_at_throw_not_landing(); + test_melee_stare_reads_prayer_at_calculation(); + + return osrs_test_summary(); +} diff --git a/ocean/osrs/tools/gen_colosseum_item_obs_table.c b/ocean/osrs/tools/gen_colosseum_item_obs_table.c new file mode 100644 index 0000000000..98e78c84c1 --- /dev/null +++ b/ocean/osrs/tools/gen_colosseum_item_obs_table.c @@ -0,0 +1,165 @@ +/* Emits the colosseum item observation table that colo_ent_gather_inv reads. + * + * src/ocean.cu deliberately sees no OSRS header, so the twelve per-cell features that are a + * pure function of the item cannot be computed there. They are baked here instead, by calling + * the same osrs_write_inventory_cell_affordance_features_compact the observation used to call, + * one row per cell code. + * + * cc -std=c11 -O2 -I. -o /tmp/gen_colo_item_obs \ + * ocean/osrs/tools/gen_colosseum_item_obs_table.c -lm + * /tmp/gen_colo_item_obs ocean/osrs/osrs_colosseum_item_obs_generated.h \ + * ocean/osrs/osrs_colosseum_item_obs_table.inc + * + * test_colosseum_modifiers rebuilds every row in memory and diffs it against the committed + * table, so a stale checkout fails the battery instead of training on wrong observations. + */ + +#include +#include +#include + +#include "ocean/osrs/osrs_inventory_clicks.h" + +/* encounter_init_maxed_player_combat_stats(&s->player, COLO_PLAYER_PRAYER_LEVEL) at reset. + * encounter_colosseum_obs_mask.inc static_asserts both against the encounter's own values. */ +#define GEN_BASE_PRAYER 99 +#define GEN_BASE_LEVEL MAXED_BASE_ATTACK + +/* Frailty rewrites base_hitpoints mid-episode, so hp_heal cannot be baked. It is the reason + * the observation still carries one float per cell. Prove the table is free of it by building + * every row at two hitpoint levels and rejecting any row that moves. */ +#define GEN_PROBE_HITPOINTS_A MAXED_BASE_HITPOINTS +#define GEN_PROBE_HITPOINTS_B 60 + +static void emit_float(FILE* out, float v) { + char buf[64]; + snprintf(buf, sizeof(buf), "%.9g", (double)v); + if (!strpbrk(buf, ".eEnN")) strncat(buf, ".0", sizeof(buf) - strlen(buf) - 1); + fprintf(out, "%sf", buf); +} + +static void build_row(float* row, int code, int base_hitpoints) { + osrs_write_inventory_cell_obs_table_row( + row, code, base_hitpoints, GEN_BASE_PRAYER, GEN_BASE_LEVEL); +} + +static void require_code_round_trip(int code) { + OsrsInventoryCell cell = osrs_inventory_cell_for_obs_code(code); + int back = osrs_inventory_cell_obs_code(cell.item_idx, cell.raw_osrs_id); + if (back == code) return; + fprintf(stderr, + "colosseum item obs table: code %d resolves to item %u raw %u, which codes back as " + "%d. Two codes share one cell content and the table is not a function of the code.\n", + code, cell.item_idx, cell.raw_osrs_id, back); + exit(1); +} + +static void require_hitpoints_free(int code) { + float a[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; + float b[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; + build_row(a, code, GEN_PROBE_HITPOINTS_A); + build_row(b, code, GEN_PROBE_HITPOINTS_B); + + for (int f = 0; f < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; f++) { + if (a[f] == b[f]) continue; + fprintf(stderr, + "colosseum item obs table: code %d feature %d depends on base_hitpoints " + "(%g at %d, %g at %d). Ship it in the observation or key the table on it.\n", + code, f, (double)a[f], GEN_PROBE_HITPOINTS_A, + (double)b[f], GEN_PROBE_HITPOINTS_B); + exit(1); + } +} + +static FILE* open_or_die(const char* path) { + FILE* f = fopen(path, "w"); + if (!f) { + fprintf(stderr, "colosseum item obs table: cannot write %s\n", path); + exit(1); + } + return f; +} + +static void write_header(const char* path) { + FILE* out = open_or_die(path); + fprintf(out, + "/* Generated by ocean/osrs/tools/gen_colosseum_item_obs_table.c. Do not edit.\n" + " * Dependency-free on purpose: src/ocean.cu includes it and no OSRS header. */\n" + "#ifndef OSRS_COLOSSEUM_ITEM_OBS_GENERATED_H\n" + "#define OSRS_COLOSSEUM_ITEM_OBS_GENERATED_H\n" + "\n" + "#define COLO_ITEM_OBS_TABLE_ROWS %d\n" + "#define COLO_ITEM_OBS_TABLE_COLS %d\n" + "#define COLO_ITEM_OBS_TABLE_BASE_PRAYER %d\n" + "#define COLO_ITEM_OBS_TABLE_BASE_LEVEL %d\n" + "\n" + "/* The observation carries code/SCALE; multiply back to index a row. A power of two\n" + " * keeps the round trip exact in bf16 and keeps the code out of the global linear\n" + " * layer's dynamic range. */\n" + "#define COLO_ITEM_OBS_CODE_SCALE %d\n" + "\n" + "/* Slots the observation adds in; the table holds zero at both. */\n" + "#define COLO_ITEM_OBS_OVERLAY_EQUIPPED %d\n" + "#define COLO_ITEM_OBS_OVERLAY_HP_HEAL %d\n" + "\n" + "/* Rows live in osrs_colosseum_item_obs_table.inc so each consumer names its own\n" + " * storage: src/ocean.cu wants a __device__ copy, the viewer wants a host one, and\n" + " * a single guarded definition would hand whichever included first to both. */\n" + "\n" + "#endif\n", + OSRS_INVENTORY_CELL_OBS_CODE_COUNT, + OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT, + GEN_BASE_PRAYER, + GEN_BASE_LEVEL, + OSRS_INVENTORY_CELL_OBS_CODE_SCALE, + OSRS_INVENTORY_CELL_COMPACT_EQUIPPED, + OSRS_INVENTORY_CELL_COMPACT_HP_HEAL); + fclose(out); +} + +static void write_table(const char* path) { + FILE* out = open_or_die(path); + fprintf(out, + "/* Generated by ocean/osrs/tools/gen_colosseum_item_obs_table.c. Do not edit.\n" + " * Included as the initialiser body of a [ROWS][COLS] float array. */\n"); + + for (int code = 0; code < OSRS_INVENTORY_CELL_OBS_CODE_COUNT; code++) { + require_code_round_trip(code); + require_hitpoints_free(code); + + float row[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; + build_row(row, code, GEN_PROBE_HITPOINTS_A); + + fprintf(out, "{"); + for (int f = 0; f < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; f++) { + if (f) fprintf(out, ", "); + emit_float(out, row[f]); + } + fprintf(out, "},\n"); + } + fclose(out); +} + +int main(int argc, char** argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + if (OSRS_INVENTORY_CELL_OBS_CODE_COUNT > OSRS_INVENTORY_CELL_OBS_CODE_SCALE) { + fprintf(stderr, + "colosseum item obs table: %d codes exceeds the scale %d; the code/scale round " + "trip is only exact in bf16 while a code fits in the scale's significand\n", + OSRS_INVENTORY_CELL_OBS_CODE_COUNT, OSRS_INVENTORY_CELL_OBS_CODE_SCALE); + return 1; + } + for (int code = 0; code < OSRS_INVENTORY_CELL_OBS_CODE_COUNT; code++) { + int back = osrs_inventory_cell_obs_code_decode( + osrs_inventory_cell_obs_code_encode(code)); + if (back == code) continue; + fprintf(stderr, "colosseum item obs table: code %d encodes back as %d\n", code, back); + return 1; + } + write_header(argv[1]); + write_table(argv[2]); + return 0; +} diff --git a/ocean/osrs_colosseum/colosseum_profile.h b/ocean/osrs_colosseum/colosseum_profile.h index 02dfa34cb2..cfdb59b2b9 100644 --- a/ocean/osrs_colosseum/colosseum_profile.h +++ b/ocean/osrs_colosseum/colosseum_profile.h @@ -14,25 +14,38 @@ X(COLO_PROF_C_REWARD_TERMINAL, "c_reward_terminal") \ X(COLO_PROF_C_TERMINAL_LOG, "c_terminal_log") \ X(COLO_PROF_C_RESET, "c_reset") \ + X(COLO_PROF_C_LOG_DPT, "c_log_dpt") \ X(COLO_PROF_OBS_REFRESH_SLOTS, "obs_refresh_slots") \ X(COLO_PROF_OBS_PREFIX, "obs_prefix") \ X(COLO_PROF_OBS_PILLARS, "obs_pillars") \ - X(COLO_PROF_OBS_BESTGEAR, "obs_bestgear_build") \ X(COLO_PROF_OBS_INVENTORY, "obs_inventory") \ X(COLO_PROF_OBS_VENATOR, "obs_venator") \ X(COLO_PROF_OBS_NPC_SLOTS, "obs_npc_slots") \ X(COLO_PROF_OBS_MODIFIERS, "obs_modifiers") \ X(COLO_PROF_OBS_BOSS, "obs_boss") \ X(COLO_PROF_OBS_PENDING_HITS, "obs_pending_hits") \ - X(COLO_PROF_OBS_FORECAST, "obs_forecast") \ X(COLO_PROF_OBS_THREAT_LOS, "obs_threat_los") \ X(COLO_PROF_OBS_THRALL_DC, "obs_thrall_dc") \ - X(COLO_PROF_OBS_WEAPON_CHOICE, "obs_weapon_choice") \ X(COLO_PROF_OBS_SPAWN, "obs_spawn") \ - X(COLO_PROF_OBS_THREAT_FIELD, "obs_threat_field") \ - X(COLO_PROF_FC_SETUP, "fc_setup") \ - X(COLO_PROF_FC_ROLLOUT, "fc_rollout") \ - X(COLO_PROF_FC_SOLARFLARE, "fc_solarflare") \ + X(COLO_PROF_STEP_PRE_PLAYER, "step_pre_player") \ + X(COLO_PROF_PRE_RESET_SCRATCH, "pre_reset_scratch") \ + X(COLO_PROF_PRE_RESET_RENDER, "pre_reset_render") \ + X(COLO_PROF_PRE_RESET_PLAYER, "pre_reset_player") \ + X(COLO_PROF_PRE_RESET_NPCS, "pre_reset_npcs") \ + X(COLO_PROF_PRE_PLAYER_PRETICK, "pre_player_pretick") \ + X(COLO_PROF_PRE_SPAWN, "pre_spawn") \ + X(COLO_PROF_PRE_RESOLVE_HITS, "pre_resolve_hits") \ + X(COLO_PROF_PRE_COLLISION, "pre_collision") \ + X(COLO_PROF_PRE_NPC_PHASE, "pre_npc_phase") \ + X(COLO_PROF_PRE_OFFPRAY, "pre_offpray") \ + X(COLO_PROF_STEP_PLAYER, "step_player") \ + X(COLO_PROF_PLAYER_TIMERS, "player_timers") \ + X(COLO_PROF_PLAYER_INVENTORY, "player_inventory") \ + X(COLO_PROF_PLAYER_INTENT, "player_intent") \ + X(COLO_PROF_PLAYER_MOVE, "player_move") \ + X(COLO_PROF_PLAYER_ATTACK, "player_attack") \ + X(COLO_PROF_STEP_REWARD, "step_reward") \ + X(COLO_PROF_STEP_WAVE_LOGIC, "step_wave_logic") \ X(COLO_PROF_STEP_NPC_TOTAL, "step_npc_total") \ X(COLO_PROF_STEP_SOL_BOSS, "step_sol_boss") \ X(COLO_PROF_STEP_JAVELIN_SKYFALL, "step_javelin_skyfall") \ @@ -41,7 +54,11 @@ X(COLO_PROF_STEP_NPC_ATTACK, "step_npc_attack") \ X(COLO_PROF_STEP_MANTICORE_BARRAGE, "step_manticore_barrage") \ X(COLO_PROF_STEP_WARBAND_ATTACK, "step_warband_attack") \ - X(COLO_PROF_STEP_MODIFIERS_HAZARDS, "step_modifiers_hazards") + X(COLO_PROF_STEP_MODIFIERS_HAZARDS, "step_modifiers_hazards") \ + X(COLO_PROF_ENV_STEPS, "env_steps") \ + X(COLO_PROF_BEST_GEAR_REQUESTS, "best_gear_requests") \ + X(COLO_PROF_BEST_GEAR_HITS, "best_gear_hits") \ + X(COLO_PROF_BEST_GEAR_BUILDS, "best_gear_builds") #define OSRS_ENV_PROFILE_PREFIX colosseum #define OSRS_ENV_PROFILE_COUNT COLO_PROF_COUNT diff --git a/ocean/osrs_colosseum/osrs_colosseum.cu b/ocean/osrs_colosseum/osrs_colosseum.cu index 45569bde92..d4703c7e12 100644 --- a/ocean/osrs_colosseum/osrs_colosseum.cu +++ b/ocean/osrs_colosseum/osrs_colosseum.cu @@ -1,19 +1,51 @@ // OSRS Colosseum CUDA entity encoder. // Included by src/ocean.cu — requires precision_t, Prec, Allocator, puf_mm, etc. + // ---- Colosseum entity encoder ---- -static constexpr int COLO_ENT_NPC_START = 1030; +// Mirrors the observation layout in encounter_colosseum_obs_mask.inc. No shared +// header reaches this TU, so drift is caught only by the _Static_asserts in +// osrs_visual.c; update both when the layout changes. +// +// The one exception is the item table, which is generated precisely so it can cross this +// boundary with no OSRS dependency. ENV_HEADER is included before this file and also pulls +// the guarded header, so the rows live in a separate .inc and each side names its own +// storage -- a single guarded definition would hand the host copy to the kernel. +#include "../ocean/osrs/osrs_colosseum_item_obs_generated.h" +__device__ static const float COLO_ITEM_OBS_TABLE_DEV + [COLO_ITEM_OBS_TABLE_ROWS][COLO_ITEM_OBS_TABLE_COLS] = { +#include "../ocean/osrs/osrs_colosseum_item_obs_table.inc" +}; + +static constexpr int COLO_ENT_NPC_START = 130; static constexpr int COLO_ENT_NUM_NPCS = 24; -static constexpr int COLO_ENT_FEATS = 37; +static constexpr int COLO_ENT_FEATS = 34; static constexpr int COLO_ENT_TYPE_ONEHOT = 12; static constexpr int COLO_ENT_BOTTLENECK = 16; static constexpr int COLO_ENT_NPC_BLOCK = COLO_ENT_NUM_NPCS * COLO_ENT_FEATS; -static constexpr int COLO_ENT_INV_START = 48; +// The observation carries a type CODE per slot (0 empty, type+1 otherwise) where the +// encoder wants a one-hot. colo_ent_gather_npcs expands it, so every kernel downstream -- +// including the deterministic backward -- is unchanged. +static constexpr int COLO_ENT_OBS_FEATS = + 1 + (COLO_ENT_FEATS - COLO_ENT_TYPE_ONEHOT); +static constexpr int COLO_ENT_NPC_OBS_BLOCK = COLO_ENT_NUM_NPCS * COLO_ENT_OBS_FEATS; +static constexpr int COLO_ENT_INV_START = 36; static constexpr int COLO_ENT_INV_NUM_CELLS = 28; -static constexpr int COLO_ENT_INV_FEATS = 28; +static constexpr int COLO_ENT_INV_FEATS = 15; static constexpr int COLO_ENT_INV_PRESENT = 0; static constexpr int COLO_ENT_INV_BOTTLENECK = 16; static constexpr int COLO_ENT_INV_BLOCK = COLO_ENT_INV_NUM_CELLS * COLO_ENT_INV_FEATS; +// Each cell observes an item CODE plus the two features the item does not fix: whether it is +// the piece currently worn in its slot, and how much it heals -- the latter divides by +// base_hitpoints, which the Frailty modifier rewrites mid-episode. colo_ent_gather_inv +// rebuilds the other twelve from the table and adds these two in, so the encoder record +// keeps the width and meaning it had before the recut. +static constexpr int COLO_ENT_INV_OBS_FEATS = 3; +static constexpr int COLO_ENT_INV_OBS_CODE = 0; +static constexpr int COLO_ENT_INV_OBS_EQUIPPED = 1; +static constexpr int COLO_ENT_INV_OBS_HP_HEAL = 2; +static_assert(COLO_ENT_INV_FEATS == COLO_ITEM_OBS_TABLE_COLS, + "encoder record width is one item table row"); struct ColosseumEntityEncoderWeights { Prec global_w; @@ -52,7 +84,16 @@ __global__ void colo_ent_gather_npcs( if (idx >= total) return; int b = idx / COLO_ENT_NPC_BLOCK; int off = idx % COLO_ENT_NPC_BLOCK; - npc_flat[idx] = obs[(int64_t)b * obs_size + COLO_ENT_NPC_START + off]; + int rec = off / COLO_ENT_FEATS; + int f = off - rec * COLO_ENT_FEATS; + const precision_t* src = obs + (int64_t)b * obs_size + COLO_ENT_NPC_START + + rec * COLO_ENT_OBS_FEATS; + if (f < COLO_ENT_TYPE_ONEHOT) { + int code = (int)lrintf(to_float(src[0])); + npc_flat[idx] = from_float(code == f + 1 ? 1.0f : 0.0f); + } else { + npc_flat[idx] = src[1 + (f - COLO_ENT_TYPE_ONEHOT)]; + } } __device__ __forceinline__ float colo_ent_gelu_fwd(float x) { @@ -76,7 +117,19 @@ __global__ void colo_ent_gather_inv( if (idx >= total) return; int b = idx / COLO_ENT_INV_BLOCK; int off = idx % COLO_ENT_INV_BLOCK; - inv_flat[idx] = obs[(int64_t)b * obs_size + COLO_ENT_INV_START + off]; + int cell = off / COLO_ENT_INV_FEATS; + int f = off - cell * COLO_ENT_INV_FEATS; + const precision_t* src = obs + (int64_t)b * obs_size + COLO_ENT_INV_START + + cell * COLO_ENT_INV_OBS_FEATS; + int code = (int)lrintf( + to_float(src[COLO_ENT_INV_OBS_CODE]) * (float)COLO_ITEM_OBS_CODE_SCALE); + assert(code >= 0 && code < COLO_ITEM_OBS_TABLE_ROWS); + float v = COLO_ITEM_OBS_TABLE_DEV[code][f]; + if (f == COLO_ITEM_OBS_OVERLAY_EQUIPPED) + v += to_float(src[COLO_ENT_INV_OBS_EQUIPPED]); + if (f == COLO_ITEM_OBS_OVERLAY_HP_HEAL) + v += to_float(src[COLO_ENT_INV_OBS_HP_HEAL]); + inv_flat[idx] = from_float(v); } // ---- fused pool kernels ---- @@ -224,15 +277,21 @@ __global__ void colo_ent_fused_grad_z1( } __syncthreads(); + // Gathered, not scattered: one thread owns each (record, k) accumulator and + // walks j in order, so the summation order is fixed. An atomicAdd scatter + // here let warp scheduling pick the order, and non-associative float + // addition then made training irreproducible run to run. int tile = H - base; if (tile > (int)blockDim.x) tile = blockDim.x; - for (int idx = threadIdx.x; idx < tile * COLO_ENT_BOTTLENECK; idx += blockDim.x) { - int j = idx / COLO_ENT_BOTTLENECK; - int k = idx - j * COLO_ENT_BOTTLENECK; - int n = arg_s[j]; - if (n < 0) continue; - float g = grad_s[j] * to_float(l2_w[(int64_t)(base + j) * COLO_ENT_BOTTLENECK + k]); - atomicAdd(&accum[n * COLO_ENT_BOTTLENECK + k], g); + for (int idx = threadIdx.x; idx < num_rec * COLO_ENT_BOTTLENECK; idx += blockDim.x) { + int n = idx / COLO_ENT_BOTTLENECK; + int k = idx - n * COLO_ENT_BOTTLENECK; + float sum = 0.0f; + for (int j = 0; j < tile; j++) { + if (arg_s[j] != n) continue; + sum += grad_s[j] * to_float(l2_w[(int64_t)(base + j) * COLO_ENT_BOTTLENECK + k]); + } + accum[idx] += sum; } __syncthreads(); } @@ -299,7 +358,8 @@ static Prec colo_entity_encoder_forward(void* w, void* activations, Prec input, Prec inv2d = {.data = a->inv_flat.data, .shape = {IB, COLO_ENT_INV_FEATS}}; puf_mm(&inv2d, &ew->inv_l1_w, &a->inv_z1, stream); static_assert(COLO_ENT_INV_PRESENT == 0, - "fused pool mask reads a prefix; present flag must be cell-local offset 0"); + "fused pool masks the EXPANDED record, so present must be cell-local offset 0 there; " + "the observation carries the item code at that offset instead"); colo_ent_launch_fused_fwd( a->out.data, a->inv_pool_argmax.data, a->inv_h1.data, a->inv_z1.data, a->inv_flat.data, @@ -337,7 +397,7 @@ static void colo_entity_encoder_init_weights(void* w, uint64_t* seed, cudaStream ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; auto init2d = [&](Prec& t, int rows, int cols) { Prec wt = {.data = t.data, .shape = {rows, cols}}; - puf_kaiming_init(&wt, std::sqrt(2.0f), (*seed)++, stream); + puf_kaiming_init(&wt, sqrtf(2.0f), (*seed)++, stream); }; init2d(ew->global_w, ew->hidden, ew->obs_size); init2d(ew->entity_l1_w, COLO_ENT_BOTTLENECK, COLO_ENT_FEATS); @@ -447,6 +507,12 @@ static void* colo_entity_encoder_create_weights(void* self) { return ew; } +// ---- Inferno entity encoder ---- +// Mirrors the observation layout in encounter_inferno_obs_mask.inc, whose block +// offsets are the INF_OBS_AFTER_* macros in encounter_inferno_forecast.inc. No +// shared header reaches this TU, so drift is caught only by the _Static_asserts +// in osrs_visual.c; update both when the layout changes. + static void create_osrs_colosseum_encoder(Encoder* enc) { *enc = Encoder{ .forward = colo_entity_encoder_forward, diff --git a/ocean/osrs_colosseum/osrs_colosseum.h b/ocean/osrs_colosseum/osrs_colosseum.h index 55aafb2c23..10d176f227 100644 --- a/ocean/osrs_colosseum/osrs_colosseum.h +++ b/ocean/osrs_colosseum/osrs_colosseum.h @@ -7,6 +7,11 @@ #include "pufferenv.h" +/* Before the encounter header, so the COLO_PROFILE_MARK sites inside it compile in. + * Every mark is a branch on a static int and does no work until + * PUFFER_COLOSSEUM_PROFILE is set, so the training path pays a predictable branch. */ +#include "colosseum_profile.h" + #define Log OsrsSharedLog #include "../osrs/encounters/encounter_colosseum.h" #undef Log @@ -19,6 +24,7 @@ typedef float obs_t; #define COLO_ENV_STATE(env) ((EncounterState*)&(env)->state) #define COLO_ENV_CONTEXT(env) ((EncounterContext*)&(env)->context) #define COLO_MAX_CURRICULUM_TIERS 8 +#define COLO_DPT_SAMPLE_INTERVAL 64 struct Log { float episode_return; @@ -26,6 +32,8 @@ struct Log { float damage_dealt; float damage_received; float wins; + float deaths; + float timeouts; float wave; float npc_kills; float prayer_correct; @@ -42,7 +50,12 @@ struct Log { float offpray_damage_by_type[COLO_NUM_NPC_TYPES]; float total_damage_by_type[COLO_NUM_NPC_TYPES]; float death_by_type[COLO_NUM_NPC_TYPES]; + float npc_attack_death_by_type[COLO_NUM_NPC_TYPES]; float typeless_damage_by_type[COLO_NUM_NPC_TYPES]; + float sol_damage_by_source[COLO_NUM_SOL_DAMAGE_SOURCES]; + float javelin_damage_by_source[COLO_NUM_JAVELIN_DAMAGE_SOURCES]; + float death_by_source[COLO_NUM_DAMAGE_SOURCES]; + float doom_death_by_source[COLO_NUM_DAMAGE_SOURCES]; float death_fatal_damage; float offpray_damage_conflict; float offpray_damage_solo; @@ -53,6 +66,28 @@ struct Log { float death_dmg_self; float death_heal_remaining; float farm_damage; + float reward_steps; + float reward_clamped_steps; + float reward_clamp_loss; + float reward_raw_peak; + float clamp_loss_wave_clear; + float clamp_loss_win; + float rew_damage; + float rew_boss_phase; + float rew_wave_clear; + float rew_win; + float rew_death; + float rew_timeout; + float avoid_total; + float avoid_achieved; + float avoid_missed; + float avoid_impossible; + float dmg_unprayable; + float inv_memo_hits; + float inv_memo_misses; + float npc_blocked_calls; + float npc_blocked_tiles; + float npc_stamp_tiles; float n; }; @@ -69,6 +104,7 @@ struct Env { int config_start_wave; int acts_staging[COLO_NUM_ACTION_HEADS]; uint64_t damage_scale_anneal_step; + uint64_t dpt_sample_step; float max_episode_depth_seen; }; @@ -129,8 +165,16 @@ static void col_assign_curriculum_wave(Env* env, Dict* kwargs) { float fracs[COLO_MAX_CURRICULUM_TIERS]; int num_tiers = 0; for (int i = 0; i < num_tiers_config; i++) { - int wave = (int)dict_get(kwargs, wave_keys[i]); - float frac = (float)dict_get(kwargs, frac_keys[i]); + DictItem* wave_item = dict_find(kwargs, wave_keys[i]); + DictItem* frac_item = dict_find(kwargs, frac_keys[i]); + if (!wave_item || !frac_item) { + fprintf(stderr, + "colosseum: curriculum_num_tiers=%d requires %s and %s, which are not set\n", + num_tiers_config, wave_keys[i], frac_keys[i]); + abort(); + } + int wave = (int)wave_item->value; + float frac = (float)frac_item->value; if (frac > 0.0f) { waves[num_tiers] = wave; fracs[num_tiers] = frac; @@ -141,6 +185,16 @@ static void col_assign_curriculum_wave(Env* env, Dict* kwargs) { if (classic_curriculum_mode == 0 || num_tiers == 0) { return; } + float total_frac = 0.0f; + for (int t = 0; t < num_tiers; t++) { + total_frac += fracs[t]; + } + if (total_frac > 0.9f) { + float rescale = 0.9f / total_frac; + for (int t = 0; t < num_tiers; t++) { + fracs[t] *= rescale; + } + } float draw = col_curriculum_uniform(env->rng); float cumulative = 0.0f; for (int t = 0; t < num_tiers; t++) { @@ -187,8 +241,6 @@ void puf_init(Env* env, Dict* kwargs) { "timeout_penalty", "boss_damage_reward_coeff", "boss_phase_bonus", - "argmax_gear_reward_coeff", - "offensive_boost_reward_coeff", "beginner_loadout_fraction", "late_start_supply_fraction_per_wave", "prayer_switch_fail_prob", @@ -205,16 +257,10 @@ void puf_init(Env* env, Dict* kwargs) { "farm_safe_damage_cap", "farm_cap_waves", "loadout_profile_mode", - "step_out_forecast_obs_enabled", - "threat_field_obs_enabled", - "forecast_horizon", "mask_inventory_heads", - "prayer_oracle_mode", "late_start_state_mode", "bis_gear_oracle_mode", - "invuln_mode", "episode_max_ticks_override", - "remove_brews", "damage_scale_anneal_ticks", }; for (size_t k = 0; k < sizeof(int_keys) / sizeof(*int_keys); k++) { @@ -235,6 +281,11 @@ void puf_reset(Env* env) { } void puf_step(Env* env) { +#ifdef COLO_PROFILE_ENABLED + int col_prof_enabled = COLO_PROFILE_ENABLED(); + double col_prof_step_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; + double col_prof_t0 = col_prof_step_t0; +#endif Agent* agent = &env->agents[0]; for (int i = 0; i < NUM_ATNS; i++) { env->acts_staging[i] = (int)agent->actions[i]; @@ -250,17 +301,37 @@ void puf_step(Env* env) { anneal_start + (1.0f - anneal_start) * frac; } +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_ACTIONS); +#endif + ENCOUNTER_COLOSSEUM.step(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), env->acts_staging); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_ENCOUNTER_STEP); +#endif float* obs = (float*)agent->observations; ENCOUNTER_COLOSSEUM.write_obs(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), obs); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_WRITE_OBS); +#endif col_write_action_mask_bytes(env, agent->action_mask); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_WRITE_MASK); +#endif agent->rewards[0] = ENCOUNTER_COLOSSEUM.get_reward(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env)); int is_terminal = ENCOUNTER_COLOSSEUM.is_terminal(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env)); agent->terminals[0] = (float)is_terminal; - - if (env->state.start_wave == env->config_start_wave) { +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_REWARD_TERMINAL); +#endif + + /* Both readings run the best-gear oracle, which is the single most expensive thing + * left in the env step. They are means over millions of steps, so a 1-in-N sample is + * just as precise and costs N times less. */ + if (env->state.start_wave == env->config_start_wave && + (env->dpt_sample_step++ % COLO_DPT_SAMPLE_INTERVAL) == 0) { col_log_dpt_sample( &env->log.current_set_argmax_dpt_hit, &env->log.current_set_argmax_dpt_n, @@ -269,6 +340,9 @@ void puf_step(Env* env) { &env->log.attacked_argmax_set_hit, &env->log.attacked_argmax_set_n, col_attacked_with_argmax_set(&env->state)); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_LOG_DPT); +#endif } if (is_terminal) { @@ -284,6 +358,8 @@ void puf_step(Env* env) { env->log.episode_return += clog->episode_return; env->log.episode_length += (float)clog->episode_length; env->log.wins += (float)clog->win; + env->log.deaths += (float)clog->died; + env->log.timeouts += (float)clog->timed_out; env->log.wave += (float)clog->wave_reached; env->log.damage_dealt += clog->total_damage_dealt; env->log.damage_received += clog->total_damage_received; @@ -298,8 +374,24 @@ void puf_step(Env* env) { env->log.offpray_damage_by_type[t] += clog->offpray_damage_by_type[t]; env->log.total_damage_by_type[t] += clog->total_damage_by_type[t]; env->log.death_by_type[t] += clog->death_by_type[t]; + env->log.npc_attack_death_by_type[t] += + clog->npc_attack_death_by_type[t]; env->log.typeless_damage_by_type[t] += clog->typeless_damage_by_type[t]; } + for (int source = 0; source < COLO_NUM_SOL_DAMAGE_SOURCES; source++) + env->log.sol_damage_by_source[source] += + clog->sol_damage_by_source[source]; + for (int source = 0; + source < COLO_NUM_JAVELIN_DAMAGE_SOURCES; + source++) + env->log.javelin_damage_by_source[source] += + clog->javelin_damage_by_source[source]; + for (int source = 0; source < COLO_NUM_DAMAGE_SOURCES; source++) { + env->log.death_by_source[source] += + clog->death_by_source[source]; + env->log.doom_death_by_source[source] += + clog->doom_death_by_source[source]; + } env->log.death_fatal_damage += clog->death_fatal_damage; env->log.offpray_damage_conflict += clog->offpray_damage_conflict; env->log.offpray_damage_solo += clog->offpray_damage_solo; @@ -310,13 +402,86 @@ void puf_step(Env* env) { env->log.death_dmg_self += clog->death_dmg_self; env->log.death_heal_remaining += clog->death_heal_remaining; env->log.farm_damage += clog->farm_damage; + env->log.reward_steps += clog->reward_steps; + env->log.reward_clamped_steps += clog->reward_clamped_steps; + env->log.reward_clamp_loss += clog->reward_clamp_loss; + env->log.clamp_loss_wave_clear += clog->clamp_loss_wave_clear; + env->log.clamp_loss_win += clog->clamp_loss_win; + env->log.reward_raw_peak += clog->reward_raw_peak; + env->log.rew_damage += clog->rew_damage; + env->log.rew_boss_phase += clog->rew_boss_phase; + env->log.rew_wave_clear += clog->rew_wave_clear; + env->log.rew_win += clog->rew_win; + env->log.rew_death += clog->rew_death; + env->log.rew_timeout += clog->rew_timeout; + env->log.avoid_total += clog->avoid_total; + env->log.avoid_achieved += clog->avoid_achieved; + env->log.avoid_missed += clog->avoid_missed; + env->log.avoid_impossible += clog->avoid_impossible; + env->log.dmg_unprayable += clog->dmg_unprayable; + env->log.inv_memo_hits += clog->inv_memo_hits; + env->log.inv_memo_misses += clog->inv_memo_misses; + env->log.npc_blocked_calls += clog->npc_blocked_calls; + env->log.npc_blocked_tiles += clog->npc_blocked_tiles; + env->log.npc_stamp_tiles += clog->npc_stamp_tiles; } +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_TERMINAL_LOG); +#endif ENCOUNTER_COLOSSEUM.reset(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), 0); ENCOUNTER_COLOSSEUM.write_obs(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), obs); col_write_action_mask_bytes(env, agent->action_mask); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_RESET); +#endif } +#ifdef COLO_PROFILE_ENABLED + if (col_prof_enabled) + COLO_PROFILE_ADD(COLO_PROF_C_STEP_TOTAL, COLO_PROFILE_NOW_MS() - col_prof_step_t0); + COLO_PROFILE_ADD(COLO_PROF_ENV_STEPS, 1.0); +#endif } +#ifdef COLO_PROFILE_ENABLED +/* best_gear_* accumulate event counts, not milliseconds, so a %-of-step column on them + * is meaningless. */ +static int col_profile_slot_is_counter(int slot) { + return slot == COLO_PROF_ENV_STEPS || + slot == COLO_PROF_BEST_GEAR_REQUESTS || + slot == COLO_PROF_BEST_GEAR_HITS || + slot == COLO_PROF_BEST_GEAR_BUILDS; +} + +#define PUF_ENV_PROFILE_REPORT puf_env_profile_report +void puf_env_profile_report(void) { + int n = colosseum_env_profile_count(); + if (n <= 0) return; + + double v[COLO_PROF_COUNT]; + for (int i = 0; i < n; i++) v[i] = colosseum_env_profile_read_reset_ms(i); + double total = v[COLO_PROF_C_STEP_TOTAL]; + if (total <= 0.0) return; + + /* Absolute ns per env step is the number worth judging: a share only says how a slot + * compares to its siblings, never whether the work itself is justified. */ + double steps = v[COLO_PROF_ENV_STEPS]; + if (steps <= 0.0) return; + printf("\nenv profile: %.0f env-steps, %.0f ns/env-step total\n", + steps, total * 1e6 / steps); + printf(" %-24s %12s %8s\n", "", "ns/env-step", "share"); + for (int i = 0; i < n; i++) { + if (v[i] <= 0.0 || col_profile_slot_is_counter(i)) continue; + printf(" %-24s %12.1f %7.1f%%\n", + colosseum_env_profile_name(i), v[i] * 1e6 / steps, 100.0 * v[i] / total); + } + for (int i = 0; i < n; i++) { + if (v[i] <= 0.0 || !col_profile_slot_is_counter(i)) continue; + printf(" %-24s %10.0f (count)\n", colosseum_env_profile_name(i), v[i]); + } + fflush(stdout); +} +#endif + void puf_render(Env* env) { (void)env; } @@ -331,6 +496,8 @@ void puf_log(Log* log, Dict* out) { dict_set(out, "damage_dealt", log->damage_dealt); dict_set(out, "damage_received", log->damage_received); dict_set(out, "wins", log->wins); + dict_set(out, "deaths", log->deaths); + dict_set(out, "timeouts", log->timeouts); dict_set(out, "wave", log->wave); dict_set(out, "npc_kills", log->npc_kills); @@ -341,6 +508,32 @@ void puf_log(Log* log, Dict* out) { dict_set(out, "score", log->score); dict_set(out, "sol_min_hp", log->sol_min_hp); dict_set(out, "max_depth_reached", log->max_depth_reached); + dict_set(out, "avoid_total", log->avoid_total); + dict_set(out, "avoid_achieved", log->avoid_achieved); + dict_set(out, "avoid_missed", log->avoid_missed); + dict_set(out, "avoid_impossible", log->avoid_impossible); + float inv_lookups = log->inv_memo_hits + log->inv_memo_misses; + dict_set(out, "inv_memo_hit_rate", inv_lookups > 0.0f + ? log->inv_memo_hits / inv_lookups : 0.0f); + dict_set(out, "inv_memo_misses", log->inv_memo_misses); + dict_set(out, "npc_blocked_tiles", log->npc_blocked_tiles); + dict_set(out, "npc_stamp_tiles", log->npc_stamp_tiles); + + /* Ahead of the per-type breakdowns on purpose: the dashboard renders only the first + * PUF_DASH_MAX_USER_ROWS*2 keys, and how much reward the [-1,1] clamp discards is a + * health check on the whole reward design, not a per-NPC detail. */ + dict_set(out, "reward_clamp_frac", log->reward_steps > 0.0f + ? log->reward_clamped_steps / log->reward_steps : 0.0f); + dict_set(out, "reward_clamp_loss", log->reward_clamp_loss); + dict_set(out, "reward_raw_peak", log->reward_raw_peak); + dict_set(out, "clamp_loss_wave_clear", log->clamp_loss_wave_clear); + dict_set(out, "clamp_loss_win", log->clamp_loss_win); + dict_set(out, "rew_damage", log->rew_damage); + dict_set(out, "rew_boss_phase", log->rew_boss_phase); + dict_set(out, "rew_wave_clear", log->rew_wave_clear); + dict_set(out, "rew_win", log->rew_win); + dict_set(out, "rew_death", log->rew_death); + dict_set(out, "rew_timeout", log->rew_timeout); dict_set(out, "current_set_is_argmax_dpt_for_target", log->current_set_argmax_dpt_n > 0.0f ? log->current_set_argmax_dpt_hit / log->current_set_argmax_dpt_n : 0.0f); @@ -380,6 +573,26 @@ void puf_log(Log* log, Dict* out) { for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) { dict_set(out, DEATH_BY_KEYS[t], log->death_by_type[t]); } + static const char* const NPC_ATTACK_DEATH_BY_KEYS[COLO_NUM_NPC_TYPES] = { + "npc_attack_death_by_berserker", + "npc_attack_death_by_archer", + "npc_attack_death_by_seer", + "npc_attack_death_by_serpent", + "npc_attack_death_by_jaguar", + "npc_attack_death_by_javelin", + "npc_attack_death_by_shockwave", + "npc_attack_death_by_minotaur", + "npc_attack_death_by_manticore", + "npc_attack_death_by_sol", + "npc_attack_death_by_totem", + "npc_attack_death_by_bee", + }; + for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) { + dict_set( + out, + NPC_ATTACK_DEATH_BY_KEYS[t], + log->npc_attack_death_by_type[t]); + } dict_set(out, "death_fatal_damage", log->death_fatal_damage); dict_set(out, "offpray_dmg_conflict", log->offpray_damage_conflict); dict_set(out, "offpray_dmg_solo", log->offpray_damage_solo); @@ -393,6 +606,87 @@ void puf_log(Log* log, Dict* out) { for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) { dict_set(out, TYPELESS_DMG_KEYS[t], log->typeless_damage_by_type[t]); } + static const char* const SOL_DAMAGE_SOURCE_KEYS[COLO_NUM_SOL_DAMAGE_SOURCES] = { + "sol_dmg_spear_1", + "sol_dmg_spear_2", + "sol_dmg_shield_1", + "sol_dmg_shield_2", + "sol_dmg_triple_parry", + "sol_dmg_grapple", + "sol_dmg_crystal_laser", + "sol_dmg_molten_sand", + }; + for (int source = 0; source < COLO_NUM_SOL_DAMAGE_SOURCES; source++) + dict_set( + out, + SOL_DAMAGE_SOURCE_KEYS[source], + log->sol_damage_by_source[source]); + static const char* const JAVELIN_DAMAGE_SOURCE_KEYS[ + COLO_NUM_JAVELIN_DAMAGE_SOURCES + ] = { + "javelin_dmg_basic_ranged", + "javelin_dmg_skyfall", + "javelin_dmg_reentry_pool", + "javelin_dmg_reentry_volatility_pool", + }; + for (int source = 0; + source < COLO_NUM_JAVELIN_DAMAGE_SOURCES; + source++) + dict_set( + out, + JAVELIN_DAMAGE_SOURCE_KEYS[source], + log->javelin_damage_by_source[source]); + static const char* const DEATH_SOURCE_KEYS[COLO_NUM_DAMAGE_SOURCES] = { + "death_source_npc_attack", + "death_source_javelin_basic_ranged", + "death_source_manticore_venom", + "death_source_bee_poison", + "death_source_bee_contact", + "death_source_javelin_skyfall", + "death_source_reentry_pool", + "death_source_volatility_explosion", + "death_source_volatility_pool", + "death_source_reentry_volatility_pool", + "death_source_solarflare", + "death_source_self", + "death_source_sol_spear_1", + "death_source_sol_spear_2", + "death_source_sol_shield_1", + "death_source_sol_shield_2", + "death_source_sol_triple_parry", + "death_source_sol_grapple", + "death_source_sol_crystal_laser", + "death_source_sol_molten_sand", + }; + static const char* const DOOM_DEATH_SOURCE_KEYS[COLO_NUM_DAMAGE_SOURCES] = { + "doom_death_source_npc_attack", + "doom_death_source_javelin_basic_ranged", + "doom_death_source_manticore_venom", + "doom_death_source_bee_poison", + "doom_death_source_bee_contact", + "doom_death_source_javelin_skyfall", + "doom_death_source_reentry_pool", + "doom_death_source_volatility_explosion", + "doom_death_source_volatility_pool", + "doom_death_source_reentry_volatility_pool", + "doom_death_source_solarflare", + "doom_death_source_self", + "doom_death_source_sol_spear_1", + "doom_death_source_sol_spear_2", + "doom_death_source_sol_shield_1", + "doom_death_source_sol_shield_2", + "doom_death_source_sol_triple_parry", + "doom_death_source_sol_grapple", + "doom_death_source_sol_crystal_laser", + "doom_death_source_sol_molten_sand", + }; + for (int source = 0; source < COLO_NUM_DAMAGE_SOURCES; source++) { + dict_set(out, DEATH_SOURCE_KEYS[source], log->death_by_source[source]); + dict_set( + out, + DOOM_DEATH_SOURCE_KEYS[source], + log->doom_death_by_source[source]); + } dict_set(out, "death_dmg_unprayable", log->death_dmg_unprayable); dict_set(out, "death_dmg_offpray", log->death_dmg_offpray); dict_set(out, "death_dmg_prayed", log->death_dmg_prayed); diff --git a/ocean/osrs_inferno/osrs_inferno.cu b/ocean/osrs_inferno/osrs_inferno.cu new file mode 100644 index 0000000000..e366d2d9f4 --- /dev/null +++ b/ocean/osrs_inferno/osrs_inferno.cu @@ -0,0 +1,210 @@ +// OSRS Inferno CUDA entity encoder. +// Included by src/ocean.cu — requires precision_t, Prec, Allocator, puf_mm, etc. +// Shares ColosseumEntityEncoderWeights/Activations layout (same weight slots). + +static constexpr int INF_ENT_NPC_START = 90; +static constexpr int INF_ENT_NUM_NPCS = 37; +static constexpr int INF_ENT_FEATS = 48; +static constexpr int INF_ENT_TYPE_ONEHOT = 14; +static constexpr int INF_ENT_NPC_BLOCK = INF_ENT_NUM_NPCS * INF_ENT_FEATS; +static constexpr int INF_ENT_INV_START = 2450; +static constexpr int INF_ENT_INV_NUM_CELLS = 28; +static constexpr int INF_ENT_INV_FEATS = 28; +static constexpr int INF_ENT_INV_BLOCK = INF_ENT_INV_NUM_CELLS * INF_ENT_INV_FEATS; +static constexpr int INF_ENT_OBS_SIZE = 3432; + +__global__ void inf_ent_gather( + precision_t* __restrict__ rec_flat, const precision_t* __restrict__ obs, + int B, int obs_size, int start, int block) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + int total = B * block; + if (idx >= total) return; + int b = idx / block; + int off = idx % block; + rec_flat[idx] = obs[(int64_t)b * obs_size + start + off]; +} + +static Prec inf_entity_encoder_forward(void* w, void* activations, Prec input, cudaStream_t stream) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; + int B = input.shape[0]; + int H = ew->hidden; + int NB = B * INF_ENT_NUM_NPCS; + + if (a->saved_obs.data) puf_copy(&a->saved_obs, &input, stream); + + puf_mm(&input, &ew->global_w, &a->out, stream); + + inf_ent_gather<<>>( + a->npc_flat.data, input.data, B, ew->obs_size, INF_ENT_NPC_START, INF_ENT_NPC_BLOCK); + Prec npc2d = {.data = a->npc_flat.data, .shape = {NB, INF_ENT_FEATS}}; + puf_mm(&npc2d, &ew->entity_l1_w, &a->entity_z1, stream); + colo_ent_launch_fused_fwd( + a->out.data, a->pool_argmax.data, a->entity_h1.data, + a->entity_z1.data, a->npc_flat.data, + ew->entity_l2_w.data, B, H, INF_ENT_NUM_NPCS, INF_ENT_FEATS, + INF_ENT_TYPE_ONEHOT, stream); + + int IB = B * INF_ENT_INV_NUM_CELLS; + inf_ent_gather<<>>( + a->inv_flat.data, input.data, B, ew->obs_size, INF_ENT_INV_START, INF_ENT_INV_BLOCK); + Prec inv2d = {.data = a->inv_flat.data, .shape = {IB, INF_ENT_INV_FEATS}}; + puf_mm(&inv2d, &ew->inv_l1_w, &a->inv_z1, stream); + colo_ent_launch_fused_fwd( + a->out.data, a->inv_pool_argmax.data, a->inv_h1.data, + a->inv_z1.data, a->inv_flat.data, + ew->inv_l2_w.data, B, H, INF_ENT_INV_NUM_CELLS, INF_ENT_INV_FEATS, + 1, stream); + return a->out; +} + +static void inf_entity_encoder_backward(void* w, void* activations, Prec grad, cudaStream_t stream) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; + int B = grad.shape[0]; + int H = ew->hidden; + int NB = B * INF_ENT_NUM_NPCS; + + puf_mm_tn(&grad, &a->saved_obs, &a->global_wgrad, stream); + + colo_ent_launch_fused_bwd( + a->entity_l2_wgrad.data, a->grad_z1.data, grad.data, + ew->entity_l2_w.data, a->entity_z1.data, a->entity_h1.data, + a->pool_argmax.data, B, H, INF_ENT_NUM_NPCS, stream); + Prec npc2d = {.data = a->npc_flat.data, .shape = {NB, INF_ENT_FEATS}}; + puf_mm_tn(&a->grad_z1, &npc2d, &a->entity_l1_wgrad, stream); + + int IB = B * INF_ENT_INV_NUM_CELLS; + colo_ent_launch_fused_bwd( + a->inv_l2_wgrad.data, a->inv_grad_z1.data, grad.data, + ew->inv_l2_w.data, a->inv_z1.data, a->inv_h1.data, + a->inv_pool_argmax.data, B, H, INF_ENT_INV_NUM_CELLS, stream); + Prec inv2d = {.data = a->inv_flat.data, .shape = {IB, INF_ENT_INV_FEATS}}; + puf_mm_tn(&a->inv_grad_z1, &inv2d, &a->inv_l1_wgrad, stream); +} + +static void inf_entity_encoder_init_weights(void* w, uint64_t* seed, cudaStream_t stream) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + auto init2d = [&](Prec& t, int rows, int cols) { + Prec wt = {.data = t.data, .shape = {rows, cols}}; + puf_kaiming_init(&wt, sqrtf(2.0f), (*seed)++, stream); + }; + init2d(ew->global_w, ew->hidden, ew->obs_size); + init2d(ew->entity_l1_w, COLO_ENT_BOTTLENECK, INF_ENT_FEATS); + init2d(ew->entity_l2_w, ew->hidden, COLO_ENT_BOTTLENECK); + init2d(ew->inv_l1_w, COLO_ENT_INV_BOTTLENECK, INF_ENT_INV_FEATS); + init2d(ew->inv_l2_w, ew->hidden, COLO_ENT_INV_BOTTLENECK); +} + +static void inf_entity_encoder_reg_params(void* w, Allocator* alloc) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + ew->global_w = {.shape = {ew->hidden, ew->obs_size}}; + ew->entity_l1_w = {.shape = {COLO_ENT_BOTTLENECK, INF_ENT_FEATS}}; + ew->entity_l2_w = {.shape = {ew->hidden, COLO_ENT_BOTTLENECK}}; + colo_entity_assert_aligned(numel(ew->global_w.shape), "global_w"); + colo_entity_assert_aligned(numel(ew->entity_l1_w.shape), "entity_l1_w"); + colo_entity_assert_aligned(numel(ew->entity_l2_w.shape), "entity_l2_w"); + alloc_register(alloc, &ew->global_w); + alloc_register(alloc, &ew->entity_l1_w); + alloc_register(alloc, &ew->entity_l2_w); + ew->inv_l1_w = {.shape = {COLO_ENT_INV_BOTTLENECK, INF_ENT_INV_FEATS}}; + ew->inv_l2_w = {.shape = {ew->hidden, COLO_ENT_INV_BOTTLENECK}}; + colo_entity_assert_aligned(numel(ew->inv_l1_w.shape), "inv_l1_w"); + colo_entity_assert_aligned(numel(ew->inv_l2_w.shape), "inv_l2_w"); + alloc_register(alloc, &ew->inv_l1_w); + alloc_register(alloc, &ew->inv_l2_w); +} + +static void inf_entity_encoder_reg_train(void* w, void* activations, Allocator* acts, Allocator* grads, int B_TT) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; + int H = ew->hidden; + int NB = B_TT * INF_ENT_NUM_NPCS; + *a = {}; + a->out = {.shape = {B_TT, H}}; + a->saved_obs = {.shape = {B_TT, ew->obs_size}}; + a->npc_flat = {.shape = {NB, INF_ENT_FEATS}}; + a->entity_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; + a->entity_h1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; + a->grad_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; + a->pool_argmax = {.shape = {B_TT, H}}; + alloc_register(acts, &a->out); + alloc_register(acts, &a->saved_obs); + alloc_register(acts, &a->npc_flat); + alloc_register(acts, &a->entity_z1); + alloc_register(acts, &a->entity_h1); + alloc_register(acts, &a->grad_z1); + alloc_register(acts, &a->pool_argmax); + int IB = B_TT * INF_ENT_INV_NUM_CELLS; + a->inv_flat = {.shape = {IB, INF_ENT_INV_FEATS}}; + a->inv_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; + a->inv_h1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; + a->inv_grad_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; + a->inv_pool_argmax = {.shape = {B_TT, H}}; + alloc_register(acts, &a->inv_flat); + alloc_register(acts, &a->inv_z1); + alloc_register(acts, &a->inv_h1); + alloc_register(acts, &a->inv_grad_z1); + alloc_register(acts, &a->inv_pool_argmax); + a->global_wgrad = {.shape = {H, ew->obs_size}}; + a->entity_l1_wgrad = {.shape = {COLO_ENT_BOTTLENECK, INF_ENT_FEATS}}; + a->entity_l2_wgrad = {.shape = {H, COLO_ENT_BOTTLENECK}}; + alloc_register(grads, &a->global_wgrad); + alloc_register(grads, &a->entity_l1_wgrad); + alloc_register(grads, &a->entity_l2_wgrad); + a->inv_l1_wgrad = {.shape = {COLO_ENT_INV_BOTTLENECK, INF_ENT_INV_FEATS}}; + a->inv_l2_wgrad = {.shape = {H, COLO_ENT_INV_BOTTLENECK}}; + alloc_register(grads, &a->inv_l1_wgrad); + alloc_register(grads, &a->inv_l2_wgrad); +} + +static void inf_entity_encoder_reg_rollout(void* w, void* activations, Allocator* alloc, int B) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; + int H = ew->hidden; + int NB = B * INF_ENT_NUM_NPCS; + a->out = {.shape = {B, H}}; + a->npc_flat = {.shape = {NB, INF_ENT_FEATS}}; + a->entity_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; + a->pool_argmax = {.shape = {B, H}}; + alloc_register(alloc, &a->out); + alloc_register(alloc, &a->npc_flat); + alloc_register(alloc, &a->entity_z1); + alloc_register(alloc, &a->pool_argmax); + int IB = B * INF_ENT_INV_NUM_CELLS; + a->inv_flat = {.shape = {IB, INF_ENT_INV_FEATS}}; + a->inv_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; + a->inv_pool_argmax = {.shape = {B, H}}; + alloc_register(alloc, &a->inv_flat); + alloc_register(alloc, &a->inv_z1); + alloc_register(alloc, &a->inv_pool_argmax); +} + +static void* inf_entity_encoder_create_weights(void* self) { + Encoder* e = (Encoder*)self; + if (e->in_dim != INF_ENT_OBS_SIZE) { + fprintf(stderr, "inferno entity encoder: env obs size %d != encoder slice " + "expectation %d; INF_ENT_* constants are stale\n", + e->in_dim, INF_ENT_OBS_SIZE); + abort(); + } + ColosseumEntityEncoderWeights* ew = + (ColosseumEntityEncoderWeights*)calloc(1, sizeof(ColosseumEntityEncoderWeights)); + ew->obs_size = e->in_dim; + ew->hidden = e->out_dim; + return ew; +} + +static void create_osrs_inferno_encoder(Encoder* enc) { + *enc = Encoder{ + .forward = inf_entity_encoder_forward, + .backward = inf_entity_encoder_backward, + .init_weights = inf_entity_encoder_init_weights, + .reg_params = inf_entity_encoder_reg_params, + .reg_train = inf_entity_encoder_reg_train, + .reg_rollout = inf_entity_encoder_reg_rollout, + .create_weights = inf_entity_encoder_create_weights, + .in_dim = enc->in_dim, .out_dim = enc->out_dim, + .activation_size = sizeof(ColosseumEntityEncoderActivations), + }; +} diff --git a/ocean/osrs_inferno/osrs_inferno.h b/ocean/osrs_inferno/osrs_inferno.h index fce0fa1d9a..63c13c90a5 100644 --- a/ocean/osrs_inferno/osrs_inferno.h +++ b/ocean/osrs_inferno/osrs_inferno.h @@ -134,6 +134,14 @@ static void inferno_apply_curriculum(Env* env, Dict* kwargs) { if (num_tiers == 0) return; + if (total_frac > 0.9f) { + float rescale = 0.9f / total_frac; + for (int t = 0; t < num_tiers; t++) { + fracs[t] *= rescale; + } + total_frac = 0.9f; + } + float u = (float)inf_lowbias32(inf_lowbias32((uint32_t)env->rng) ^ 0x9e3779b9U) / 4294967296.0f; float cursor = 1.0f - total_frac; @@ -207,7 +215,6 @@ void puf_init(Env* env, Dict* kwargs) { inferno_env_put_float(env, "late_start_supply_profile_scale", (float)dict_get(kwargs, "late_start_supply_profile_scale")); - inferno_env_put_int(env, "oracle_mode", (int)dict_get(kwargs, "oracle_mode")); inferno_env_put_int(env, "terminal_penalty_enabled", (int)dict_get(kwargs, "terminal_penalty_enabled")); @@ -300,6 +307,18 @@ void puf_step(Env* env) { env->log.hp_restored += s->total_hp_restored; env->log.wins += (s->winner == INF_OUTCOME_PLAYER_WON) ? 1.0f : 0.0f; env->log.wave += (float)s->wave; + if (s->winner != INF_OUTCOME_PLAYER_WON) { + int death_bucket = (s->wave < 62) ? 0 + : (s->wave == 62) ? 1 + : (s->wave <= 65) ? 2 + : (s->wave == 66) ? 3 + : (s->wave == 67) ? 4 : 5; + env->log.inf_death_wave_hist[death_bucket] += 1.0f; + env->log.inf_death_brew_doses_hist[death_bucket] += + (float)s->player.brew_doses; + env->log.inf_death_restore_doses_hist[death_bucket] += + (float)s->player.restore_doses; + } env->log.prayer_correct += (float)s->total_prayer_correct; env->log.prayer_total += (float)s->total_npc_attacks; env->log.offensive_prayer_attacks += @@ -460,6 +479,22 @@ void puf_log(Log* log, Dict* out) { dict_set(out, "damage_per_100_ticks", damage_per_tick * 100.0f); dict_set(out, "wins", log->wins); dict_set(out, "wave", log->wave); + { + static const char* death_wave_keys[6] = { + "death_wave_pre63", "death_wave_63", "death_wave_64_66", + "death_wave_67_jad", "death_wave_68_triplejad", "death_wave_69_zuk"}; + static const char* death_brew_keys[6] = { + "death_brews_pre63", "death_brews_63", "death_brews_64_66", + "death_brews_67_jad", "death_brews_68_triplejad", "death_brews_69_zuk"}; + static const char* death_restore_keys[6] = { + "death_restores_pre63", "death_restores_63", "death_restores_64_66", + "death_restores_67_jad", "death_restores_68_triplejad", "death_restores_69_zuk"}; + for (int i = 0; i < 6; i++) { + dict_set(out, death_wave_keys[i], log->inf_death_wave_hist[i]); + dict_set(out, death_brew_keys[i], log->inf_death_brew_doses_hist[i]); + dict_set(out, death_restore_keys[i], log->inf_death_restore_doses_hist[i]); + } + } dict_set(out, "idle_ticks", log->idle_ticks); inferno_log_idle_metric( out, diff --git a/src/ocean.cu b/src/ocean.cu index d7050fc9ea..b8d0a803b7 100644 --- a/src/ocean.cu +++ b/src/ocean.cu @@ -20,6 +20,7 @@ void puf_normal_init(Prec* dst, float std, ulong seed, cudaStream_t stream) { #include "../ocean/nmmo3/nmmo3.cu" #include "../ocean/minimal/minimal.cu" #include "../ocean/osrs_colosseum/osrs_colosseum.cu" +#include "../ocean/osrs_inferno/osrs_inferno.cu" #ifdef PUFFER_NETHACK #include "../ocean/nethack/nethack.cu" #endif @@ -44,6 +45,10 @@ static void create_custom_encoder(const char* env_name, Encoder* enc) { create_osrs_colosseum_encoder(enc); return; } + if (strcmp(env_name, "osrs_inferno") == 0) { + create_osrs_inferno_encoder(enc); + return; + } } static void create_custom_decoder(const char* env_name, Decoder* dec) { From 59f90b7f42c2ac81b0c14d6b1410031b613fc4fd Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Thu, 6 Aug 2026 16:42:56 +0300 Subject: [PATCH 02/50] port protein sweep observation resume onto 5c persist denormalized sweep_observations.csv, replay via resume_from, and skip the random phase so a warm surrogate does not re-draw Sobol. --- config/default.ini | 1 + src/protein.cu | 8 +++ src/pufferl.cu | 135 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+) diff --git a/config/default.ini b/config/default.ini index 62d82194e1..68cf1d84b2 100644 --- a/config/default.ini +++ b/config/default.ini @@ -118,6 +118,7 @@ downsample = 5 use_gpu = True prune_pareto = True early_stop_quantile = 0.3 +resume_from = '' [sweep.train.total_timesteps] distribution = log_normal diff --git a/src/protein.cu b/src/protein.cu index e52bf51869..205b64ac8c 100644 --- a/src/protein.cu +++ b/src/protein.cu @@ -658,6 +658,14 @@ void protein_sweep_observe(ProteinSweep *sw, const float *norm_params, } } +// Random phase is gated on suggestions made this process, not observations +// held. Without this, a resumed sweep re-draws Sobol samples it already has. +void protein_sweep_skip_random(ProteinSweep *sw) { + if (sw->suggestion_idx < sw->num_random_samples) { + sw->suggestion_idx = sw->num_random_samples; + } +} + int protein_sweep_should_stop(const ProteinSweep *sw, float score, float cost) { float threshold = -FLT_MAX; if (sw->cm_fitted) { diff --git a/src/pufferl.cu b/src/pufferl.cu index 51628446cc..32d18c047a 100644 --- a/src/pufferl.cu +++ b/src/pufferl.cu @@ -2619,6 +2619,125 @@ typedef struct { TrainResult result; } SweepJob; +// Raw (denormalized) values, so a resume can re-normalize against a space whose +// ranges have since moved. Normalized samples would silently change meaning. +static void sweep_obs_append(const char* path, SweepParam* params, SweepSpace* space, + const float* sample, int run, int point, float score, float cost) { + if (!path || !path[0]) { + return; + } + int fresh = access(path, F_OK) != 0; + FILE* f = fopen(path, "a"); + if (!f) { + fprintf(stderr, "sweep: cannot append observations to %s: %s\n", + path, strerror(errno)); + return; + } + if (fresh) { + fprintf(f, "run,point,score,cost"); + for (int i = 0; i < space->num; i++) { + fprintf(f, ",%s.%s", params[i].section, params[i].key); + } + fprintf(f, "\n"); + } + fprintf(f, "%d,%d,%.9g,%.9g", run, point, score, cost); + for (int i = 0; i < space->num; i++) { + fprintf(f, ",%.9g", space_unnormalize(&space->spaces[i], sample[i])); + } + fprintf(f, "\n"); + fclose(f); +} + +// Replays (score, cost) from a prior sweep's observation log so the surrogate +// starts warm. Rows whose values fall outside the current ranges are counted and +// reported, never dropped quietly. +static int sweep_obs_resume(const char* path, ProteinSweep* protein, + SweepParam* params, SweepSpace* space) { + FILE* f = fopen(path, "r"); + if (!f) { + fprintf(stderr, "sweep.resume_from: cannot read %s: %s\n", + path, strerror(errno)); + exit(1); + } + char line[16384]; + if (!fgets(line, sizeof(line), f)) { + fprintf(stderr, "sweep.resume_from: %s is empty\n", path); + exit(1); + } + int* col_of = (int*)calloc((size_t)space->num, sizeof(int)); + for (int i = 0; i < space->num; i++) { + col_of[i] = -1; + } + int ncols = 0; + char* save = NULL; + for (char* tok = strtok_r(line, ",\r\n", &save); tok; + tok = strtok_r(NULL, ",\r\n", &save)) { + for (int i = 0; i < space->num; i++) { + char name[160]; + snprintf(name, sizeof(name), "%s.%s", params[i].section, params[i].key); + if (strcmp(tok, name) == 0) { + col_of[i] = ncols; + } + } + ncols++; + } + for (int i = 0; i < space->num; i++) { + if (col_of[i] < 0) { + fprintf(stderr, "sweep.resume_from: %s has no column for [%s] %s\n", + path, params[i].section, params[i].key); + exit(1); + } + } + + float* sample = (float*)calloc((size_t)space->num, sizeof(float)); + float* vals = (float*)calloc((size_t)ncols, sizeof(float)); + int replayed = 0; + int skipped = 0; + while (fgets(line, sizeof(line), f)) { + int n = 0; + save = NULL; + for (char* tok = strtok_r(line, ",\r\n", &save); tok && n < ncols; + tok = strtok_r(NULL, ",\r\n", &save)) { + vals[n++] = strtof(tok, NULL); + } + if (n < ncols) { + skipped++; + continue; + } + // A value written at a range endpoint round-trips through float32 a hair + // outside it, so admit that epsilon and snap it back. A genuinely + // narrowed range lands far outside and is still rejected. + int in_range = 1; + for (int i = 0; i < space->num; i++) { + float norm = space_normalize(&space->spaces[i], vals[col_of[i]]); + if (!isfinite(norm) || norm < -1.001f || norm > 1.001f) { + in_range = 0; + break; + } + sample[i] = fmaxf(-1.0f, fminf(1.0f, norm)); + } + float score = vals[2]; + float cost = vals[3]; + if (!in_range || !isfinite(score) || !isfinite(cost)) { + skipped++; + continue; + } + protein_sweep_observe(protein, sample, score, cost, 0); + replayed++; + } + free(vals); + free(sample); + free(col_of); + fclose(f); + if (replayed > 0) { + protein_sweep_skip_random(protein); + } + printf("sweep resume: replayed %d observations from %s (%d skipped as " + "out of range or malformed)\n", replayed, path, skipped); + fflush(stdout); + return replayed; +} + void run_sweep(Ini* ini, const char* exe_path) { // Build SweepSpace + param map from [sweep.
.] sections. const char* goal = puf_ini_get_str(ini, "sweep", "goal"); @@ -2738,6 +2857,20 @@ void run_sweep(Ini* ini, const char* exe_path) { .rng_seed = 73ULL, }); + char obs_path[2048]; + { + char dir[1024]; + snprintf(dir, sizeof(dir), "%s/%s", + puf_ini_get_str(ini, "base", "log_dir"), + puf_ini_get_str(ini, "base", "env_name")); + mkdir_p(dir); + snprintf(obs_path, sizeof(obs_path), "%s/sweep_observations.csv", dir); + } + const char* resume_from = puf_ini_get_str(ini, "sweep", "resume_from"); + if (resume_from && resume_from[0]) { + sweep_obs_resume(resume_from, protein, params, space); + } + int parallel = sweep_gpus / train_gpus; // Row 0 = staging for suggest; rows 1..parallel = per-slot copies for observe. float* samples = (float*)calloc((parallel + 1) * space->num, sizeof(float)); @@ -2882,6 +3015,8 @@ void run_sweep(Ini* ini, const char* exe_path) { for (int pi = 0; pi < job->result.points; pi++) { protein_sweep_observe(protein, job->sample, job->result.scores[pi], job->result.costs[pi], 0); + sweep_obs_append(obs_path, params, space, job->sample, job->run, pi, + job->result.scores[pi], job->result.costs[pi]); } printf("sweep run=%d score=%.4f cost=%.2f steps=%.0f random=%d gp_obs=%d pareto=%d\n", job->run, job->result.score, job->result.cost, job->result.steps, From 0be691ae8b7606d34a4ecd1bd5c7caf385455c24 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Tue, 11 Aug 2026 09:49:16 +0300 Subject: [PATCH 03/50] compact Inferno policy observations --- config/osrs_inferno.ini | 9 +- .../colosseum/encounter_colosseum_model.inc | 3 +- .../encounter_colosseum_obs_mask.inc | 71 +- ocean/osrs/encounters/encounter_colosseum.h | 2 +- .../inferno/encounter_inferno_forecast.inc | 49 - .../inferno/encounter_inferno_helpers.inc | 80 -- .../inferno/encounter_inferno_model.inc | 18 +- .../inferno/encounter_inferno_obs_mask.inc | 626 ++++------ .../encounter_inferno_render_snapshot.inc | 30 - .../inferno/encounter_inferno_reward_step.inc | 16 - .../osrs/osrs_colosseum_item_obs_generated.h | 24 - ocean/osrs/osrs_inventory_clicks.h | 41 +- ocean/osrs/osrs_item_effects.h | 31 + ocean/osrs/osrs_item_obs_generated.h | 17 + ..._obs_table.inc => osrs_item_obs_table.inc} | 14 +- ocean/osrs/osrs_types.h | 3 - ocean/osrs/osrs_visual.c | 18 +- ocean/osrs/osrs_visual_net.h | 138 ++- ocean/osrs/tests/bench_inferno_forecast.c | 21 - ocean/osrs/tests/test_colosseum_modifiers.c | 124 +- ocean/osrs/tests/test_inferno_attack_styles.c | 1091 ++++++++--------- .../osrs/tests/test_inferno_forecast_exact.c | 22 +- ocean/osrs/tests/test_inferno_golden.c | 230 +++- .../osrs/tools/gen_colosseum_item_obs_table.c | 165 --- ocean/osrs/tools/gen_osrs_item_obs_table.c | 127 ++ ocean/osrs_colosseum/osrs_colosseum.cu | 40 +- ocean/osrs_inferno/inferno_profile.h | 1 - ocean/osrs_inferno/osrs_inferno.cu | 80 +- ocean/osrs_inferno/osrs_inferno.h | 16 - src/ocean.cu | 6 + tests/test_osrs_entity_encoders.py | 329 +++++ tests/test_osrs_entity_encoders_cuda.cu | 120 ++ 32 files changed, 1881 insertions(+), 1681 deletions(-) delete mode 100644 ocean/osrs/osrs_colosseum_item_obs_generated.h create mode 100644 ocean/osrs/osrs_item_obs_generated.h rename ocean/osrs/{osrs_colosseum_item_obs_table.inc => osrs_item_obs_table.inc} (97%) delete mode 100644 ocean/osrs/tools/gen_colosseum_item_obs_table.c create mode 100644 ocean/osrs/tools/gen_osrs_item_obs_table.c create mode 100644 tests/test_osrs_entity_encoders.py create mode 100644 tests/test_osrs_entity_encoders_cuda.cu diff --git a/config/osrs_inferno.ini b/config/osrs_inferno.ini index c63bf6114e..daf2309530 100644 --- a/config/osrs_inferno.ini +++ b/config/osrs_inferno.ini @@ -26,7 +26,6 @@ supply_milestone_brew_reward_coeff = 0.0 supply_milestone_restore_reward_coeff = 0.0 death_penalty_coeff = 0.0 terminal_penalty_enabled = 0 -step_out_forecast_obs_mode = 2 phase_900_bonus = 0.0 phase_600_bonus = 0.0 phase_300_bonus = 0.0 @@ -74,7 +73,7 @@ jad_alive_zuk_multiplier = 0.1573513 total_agents = 4096 num_buffers = 2 num_threads = 16 -action_mask_size = 457 +action_mask_size = 434 gpu_env = 0 [policy] @@ -329,9 +328,3 @@ distribution = uniform min = 0.0 max = 1.0 scale = auto - -[sweep.env.step_out_forecast_obs_mode] -distribution = int_uniform -min = 1 -max = 3 -scale = auto diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index 3766c5c051..4579e225e0 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -1021,12 +1021,11 @@ typedef struct { * into EncounterLoadoutStats and the prefix emits attack_bonus, strength_bonus, max_hit, * attack_speed, attack_range, all five defence bonuses, eff_level and spell_base_damage. * Equipment EFFECTS were the one part with no aggregate in the observation. */ -#define COLO_EQUIPPED_AGG_OBS_FEATURES 10 #define COLO_INVENTORY_OBS_CACHE_WAYS 8 #define COLO_INVENTORY_OBS_CACHE_FLOATS \ (OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES_CODED + \ - COLO_EQUIPPED_AGG_OBS_FEATURES) + OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES) typedef struct { ColoBestGear best_gear[COLO_BEST_GEAR_CACHE_WAYS][COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc index f85ea2ad9c..fca835b464 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -1,30 +1,30 @@ #line 76 "encounter_colosseum.h" #define COLO_PLAYER_OBS_SIZE 36 -/* Each cell ships an item code plus the two features the item does not determine; the entity - * encoder rebuilds the other twelve from COLO_ITEM_OBS_TABLE, exactly as it rebuilds the NPC - * type one-hot from a code. Keeping the EXPANDED record identical to the old compact one - * means the inventory branch weights keep their meaning across this recut. */ +/* Each cell ships an item code plus dynamic equipped and HP-heal fields. The entity encoder + * rebuilds the full compact record from OSRS_ITEM_OBS_TABLE, then overwrites those fields. */ #define COLO_INVENTORY_CELL_OBS_FEATURES OSRS_INVENTORY_CELL_OBS_FEATURES_CODED #define COLO_INVENTORY_CELL_ENCODER_FEATURES OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT #define COLO_INVENTORY_OBS_SIZE \ (COLO_INVENTORY_DISPLAY_SLOTS * COLO_INVENTORY_CELL_OBS_FEATURES) -#define COLO_EQUIPPED_SELF_OBS_SIZE COLO_EQUIPPED_AGG_OBS_FEATURES +#define COLO_EQUIPPED_SELF_OBS_SIZE OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES static_assert(COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE == COLO_INVENTORY_OBS_CACHE_FLOATS, "inventory obs cache float count must match the inventory+equipped block size"); -static_assert(COLO_ITEM_OBS_TABLE_ROWS == OSRS_INVENTORY_CELL_OBS_CODE_COUNT, - "regenerate osrs_colosseum_item_obs_table.inc: the cell code space changed"); -static_assert(COLO_ITEM_OBS_TABLE_COLS == COLO_INVENTORY_CELL_ENCODER_FEATURES, - "regenerate osrs_colosseum_item_obs_table.inc: the expanded record width changed"); -static_assert(COLO_ITEM_OBS_TABLE_BASE_PRAYER == COLO_PLAYER_PRAYER_LEVEL, - "regenerate osrs_colosseum_item_obs_table.inc: it bakes prayer_restore at this level"); -static_assert(COLO_ITEM_OBS_TABLE_BASE_LEVEL == MAXED_BASE_ATTACK, - "regenerate osrs_colosseum_item_obs_table.inc: it bakes offensive_boost at this level"); -static_assert(COLO_ITEM_OBS_OVERLAY_EQUIPPED == OSRS_INVENTORY_CELL_COMPACT_EQUIPPED && - COLO_ITEM_OBS_OVERLAY_HP_HEAL == OSRS_INVENTORY_CELL_COMPACT_HP_HEAL, - "the gather adds the observed floats at these expanded slots; the table holds zero there"); -static_assert(COLO_ITEM_OBS_CODE_SCALE == OSRS_INVENTORY_CELL_OBS_CODE_SCALE, +static_assert(OSRS_ITEM_OBS_TABLE_ROWS == OSRS_INVENTORY_CELL_OBS_CODE_COUNT, + "regenerate osrs_item_obs_table.inc: the cell code space changed"); +static_assert(OSRS_ITEM_OBS_TABLE_COLS == COLO_INVENTORY_CELL_ENCODER_FEATURES, + "regenerate osrs_item_obs_table.inc: the expanded record width changed"); +static_assert(OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS == MAXED_BASE_HITPOINTS, + "regenerate osrs_item_obs_table.inc: it bakes hp_heal at this level"); +static_assert(OSRS_ITEM_OBS_TABLE_BASE_PRAYER == COLO_PLAYER_PRAYER_LEVEL, + "regenerate osrs_item_obs_table.inc: it bakes prayer_restore at this level"); +static_assert(OSRS_ITEM_OBS_TABLE_BASE_RANGED == MAXED_BASE_RANGED, + "regenerate osrs_item_obs_table.inc: it bakes offensive_boost at this level"); +static_assert(OSRS_ITEM_OBS_OVERLAY_EQUIPPED == OSRS_INVENTORY_CELL_COMPACT_EQUIPPED && + OSRS_ITEM_OBS_OVERLAY_HP_HEAL == OSRS_INVENTORY_CELL_COMPACT_HP_HEAL, + "the gather overwrites the dynamic fields at these expanded slots"); +static_assert(OSRS_ITEM_OBS_CODE_SCALE == OSRS_INVENTORY_CELL_OBS_CODE_SCALE, "the gather multiplies the observed code back by this scale before indexing a row"); #define COLO_NPC_CORE_FEATURES 11 #define COLO_NPC_TELLS_FEATURES 10 @@ -1485,7 +1485,7 @@ static uint64_t col_inventory_obs_signature(const ColosseumState* s) { h = (h ^ packed) * 1099511628211ULL; } const int scalars[3] = { - s->player.base_hitpoints, s->player.base_prayer, s->player.base_attack, + s->player.base_hitpoints, s->player.base_prayer, s->player.base_ranged, }; for (int k = 0; k < 3; k++) h = (h ^ (uint64_t)(uint32_t)scalars[k]) * 1099511628211ULL; @@ -1500,21 +1500,8 @@ static uint64_t col_inventory_obs_signature(const ColosseumState* s) { static void col_write_equipped_effect_aggregate( ColosseumState* s, float* obs, int* pi ) { - int i = *pi; - const OsrsEquipmentEffectProfile* fx = col_live_effects(s); - float agg_eff4[4]; - osrs_item_effect_class4(fx->effect_mask, agg_eff4); - obs[i++] = agg_eff4[0]; - obs[i++] = agg_eff4[1]; - obs[i++] = agg_eff4[2]; - obs[i++] = agg_eff4[3]; - obs[i++] = col_clamp01((float)fx->virtus_piece_count / 3.0f); - obs[i++] = col_clamp01((float)fx->dharok_piece_count / 4.0f); - obs[i++] = col_clamp01((float)fx->crystal_armour_points / 6.0f); - obs[i++] = fx->recoil_source != OSRS_RECOIL_SOURCE_NONE ? 1.0f : 0.0f; - obs[i++] = fx->spec_regen_mode == OSRS_SPEC_REGEN_MODE_LIGHTBEARER ? 1.0f : 0.0f; - obs[i++] = fx->shield_item != ITEM_NONE ? 1.0f : 0.0f; - *pi = i; + osrs_write_equipment_effect_aggregate(&obs[*pi], col_live_effects(s)); + *pi += OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES; } /* Likewise gear-dependent: it asks whether the item in this cell is the one currently worn @@ -1552,16 +1539,14 @@ static void col_write_inventory_and_equipped_obs( } s->log.inv_memo_misses += 1.0f; - /* prayer_restore and offensive_boost are baked into COLO_ITEM_OBS_TABLE at these levels. - * base_hitpoints is deliberately absent from that list: Frailty rewrites it, which is why - * hp_heal still rides the observation. Anything that starts moving the other two the same - * way has to ship them too, and must not discover that during a training run. */ - if (s->player.base_prayer != COLO_ITEM_OBS_TABLE_BASE_PRAYER || - s->player.base_attack != COLO_ITEM_OBS_TABLE_BASE_LEVEL) { + /* The generated table bakes base-99 item semantics. Colosseum overwrites HP heal because + * Frailty rewrites base hitpoints, while prayer and ranged stay fixed. */ + if (s->player.base_prayer != OSRS_ITEM_OBS_TABLE_BASE_PRAYER || + s->player.base_ranged != OSRS_ITEM_OBS_TABLE_BASE_RANGED) { fprintf(stderr, - "colosseum inventory obs: base prayer %d attack %d, item table baked %d/%d\n", - s->player.base_prayer, s->player.base_attack, - COLO_ITEM_OBS_TABLE_BASE_PRAYER, COLO_ITEM_OBS_TABLE_BASE_LEVEL); + "colosseum inventory obs: base prayer %d ranged %d, item table baked %d/%d\n", + s->player.base_prayer, s->player.base_ranged, + OSRS_ITEM_OBS_TABLE_BASE_PRAYER, OSRS_ITEM_OBS_TABLE_BASE_RANGED); abort(); } @@ -1575,7 +1560,7 @@ static void col_write_inventory_and_equipped_obs( osrs_inventory_cell_holds_equipped_item(&s->player, s->inventory_cells, cell_idx), s->player.base_hitpoints, s->player.base_prayer, - s->player.base_attack); + s->player.base_ranged); i += COLO_INVENTORY_CELL_OBS_FEATURES; } assert(i == COLO_OBS_AFTER_INVENTORY); diff --git a/ocean/osrs/encounters/encounter_colosseum.h b/ocean/osrs/encounters/encounter_colosseum.h index 98229afdd3..288a98c404 100644 --- a/ocean/osrs/encounters/encounter_colosseum.h +++ b/ocean/osrs/encounters/encounter_colosseum.h @@ -14,7 +14,7 @@ #include "../osrs_encounter_visual_events.h" #include "../osrs_player_consumables.h" #include "../osrs_inventory_clicks.h" -#include "../osrs_colosseum_item_obs_generated.h" +#include "../osrs_item_obs_generated.h" #include "../osrs_inventory_actions.h" #include "../osrs_interaction.h" #include "../data/npc_models.h" diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc index 56c8b95db1..906b43ab0e 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc @@ -1,31 +1,5 @@ #line 5216 "encounter_inferno.h" -#define INF_PLAYER_OBS_SIZE 75 -#define INF_PILLAR_OBS_SIZE (INF_NUM_PILLARS * 5) -#define INF_NPC_TYPE_ONEHOT 14 -#define INF_NPC_SLOT_FEATURES (INF_NPC_TYPE_ONEHOT + 34) -#define INF_BASE_NPC_OBS_SIZE (INF_OBS_NPCS * INF_NPC_SLOT_FEATURES) -#define INF_TOTAL_NPC_OBS_SIZE INF_BASE_NPC_OBS_SIZE #define INF_STEP_OUT_FORECAST_HORIZON 4 -#define INF_STEP_OUT_FORECAST_ACTION_FEATURES 8 -#define INF_STEP_OUT_FORECAST_OBS_SIZE (ENCOUNTER_MOVE_ACTIONS * INF_STEP_OUT_FORECAST_ACTION_FEATURES) -#define INF_FEATURES_PER_HIT 5 -#define INF_SPARK_OBS_SLOTS INF_MAX_PENDING_SPARKS -#define INF_FEATURES_PER_SPARK 7 -#define INF_PENDING_HIT_OBS_SIZE (INF_FEATURES_PER_HIT * ENCOUNTER_MAX_PENDING_HITS) -#define INF_PENDING_SPARK_OBS_SIZE (INF_FEATURES_PER_SPARK * INF_SPARK_OBS_SLOTS) -#define INF_INVENTORY_OBS_SIZE (OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES) -#define INF_EQUIPPED_OBS_SIZE (NUM_GEAR_SLOTS * OSRS_EQUIPPED_SELF_OBS_FEATURES) -#define INF_OBS_AFTER_PLAYER INF_PLAYER_OBS_SIZE -#define INF_OBS_AFTER_PILLARS (INF_OBS_AFTER_PLAYER + INF_PILLAR_OBS_SIZE) -#define INF_OBS_AFTER_NPCS (INF_OBS_AFTER_PILLARS + INF_TOTAL_NPC_OBS_SIZE) -#define INF_OBS_AFTER_FORECAST (INF_OBS_AFTER_NPCS + INF_STEP_OUT_FORECAST_OBS_SIZE) -#define INF_OBS_AFTER_PENDING_HITS (INF_OBS_AFTER_FORECAST + INF_PENDING_HIT_OBS_SIZE) -#define INF_OBS_AFTER_SPARKS (INF_OBS_AFTER_PENDING_HITS + INF_PENDING_SPARK_OBS_SIZE) -#define INF_OBS_AFTER_INVENTORY (INF_OBS_AFTER_SPARKS + INF_INVENTORY_OBS_SIZE) -#define INF_NUM_OBS (INF_OBS_AFTER_INVENTORY + INF_EQUIPPED_OBS_SIZE) - -static_assert(INF_NPC_TYPE_ONEHOT == INF_NUM_NPC_TYPES, - "the per-NPC obs one-hot is the NPC type one-hot; a new NPC type must widen it"); typedef struct { int melee_count; @@ -1220,29 +1194,6 @@ static void inf_build_step_out_forecast_ctx( inf_build_step_out_forecast_exact_ctx(s, ctx, out); } -static void inf_build_step_out_forecast_mode_ctx( - const InfernoState* s, - const InfernoContext* ctx, - InfStepOutForecast* out -) { - switch (ctx->config.step_out_forecast_obs_mode) { - case INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT: - inf_build_step_out_forecast_exact_ctx(s, ctx, out); - return; - case INF_STEP_OUT_FORECAST_MODE_FAST_STATIC_TILE: - inf_build_step_out_forecast_fast_static_ctx(s, ctx, out); - return; - case INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE: - inf_build_step_out_forecast_fast_readonly_ctx(s, ctx, out); - return; - default: - fprintf(stderr, - "inferno: step_out_forecast_obs_mode must be 1, 2, or 3 for forecast build, got %d\n", - ctx->config.step_out_forecast_obs_mode); - abort(); - } -} - static int inf_step_out_forecast_action_dangerous( const InfStepOutForecastAction* action ) { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc index 2ccf110939..aef959e384 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc @@ -445,84 +445,6 @@ static inline int inf_npc_is_live_player_target( npc->type != INF_NPC_ZUK_SHIELD; } -typedef struct { - int if_ready_total; - int if_ready_melee; - int if_ready_ranged; - int if_ready_magic; - int if_ready_max_hit; - int if_ready_sum_max_hit; - int this_tick_total; - int this_tick_melee; - int this_tick_ranged; - int this_tick_magic; - int this_tick_max_hit; - int this_tick_sum_max_hit; - int timer_lte_1; - int timer_lte_2; - int timer_lte_4; - int if_ready_style_count; - int this_tick_style_count; - int nearest_timer; -} InfNpcPressureSummary; - -static inline int inf_npc_pressure_timer(const InfNPC* npc) { - return npc->attack_timer > 0 ? npc->attack_timer : 1; -} - -static inline int inf_npc_attack_unblocked_for_pressure(const InfNPC* npc) { - return npc->stun_timer == 0 && - npc->dig_freeze_timer == 0 && - npc->dig_attack_delay == 0; -} - -static inline InfNpcPressureSummary inf_npc_pressure_summary( - const InfernoState* s -) { - InfNpcPressureSummary out = { .nearest_timer = 999 }; - int if_ready_style_mask = 0; - int this_tick_style_mask = 0; - - for (int i = 0; i < INF_MAX_NPCS; i++) { - const InfNPC* npc = &s->npcs[i]; - InfNpcPlayerThreat threat = inf_npc_player_threat(s, npc); - if (!threat.can_attack_if_ready || !threat.style_mask) - continue; - - const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; - int max_hit = inf_npc_style_mask_max_hit(stats, threat.style_mask); - out.if_ready_total++; - if (threat.style_mask & INF_STYLE_MASK_MELEE) out.if_ready_melee++; - if (threat.style_mask & INF_STYLE_MASK_RANGED) out.if_ready_ranged++; - if (threat.style_mask & INF_STYLE_MASK_MAGIC) out.if_ready_magic++; - if (max_hit > out.if_ready_max_hit) out.if_ready_max_hit = max_hit; - out.if_ready_sum_max_hit += max_hit; - if_ready_style_mask |= threat.style_mask; - - if (inf_npc_attack_unblocked_for_pressure(npc)) { - int timer = inf_npc_pressure_timer(npc); - if (timer < out.nearest_timer) out.nearest_timer = timer; - if (timer <= 1) out.timer_lte_1++; - if (timer <= 2) out.timer_lte_2++; - if (timer <= 4) out.timer_lte_4++; - } - - if (!threat.can_attack_this_tick) - continue; - - out.this_tick_total++; - if (threat.style_mask & INF_STYLE_MASK_MELEE) out.this_tick_melee++; - if (threat.style_mask & INF_STYLE_MASK_RANGED) out.this_tick_ranged++; - if (threat.style_mask & INF_STYLE_MASK_MAGIC) out.this_tick_magic++; - if (max_hit > out.this_tick_max_hit) out.this_tick_max_hit = max_hit; - out.this_tick_sum_max_hit += max_hit; - this_tick_style_mask |= threat.style_mask; - } - - out.if_ready_style_count = inf_style_count_from_mask(if_ready_style_mask); - out.this_tick_style_count = inf_style_count_from_mask(this_tick_style_mask); - return out; -} static inline int inf_barrage_total_target_count( const InfernoState* s, @@ -744,8 +666,6 @@ static InfConfig inf_default_config(void) { .supply_milestone_restore_reward_coeff = 0.0f, .death_penalty_coeff = 0.0f, .terminal_penalty_enabled = 0, - .step_out_forecast_obs_enabled = 1, - .step_out_forecast_obs_mode = INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT, .phase_900_bonus = 0.0f, .phase_600_bonus = 0.0f, .phase_300_bonus = 0.0f, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc index 0453c0916c..a120b9ed19 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc @@ -323,8 +323,8 @@ static const InfWaveDef INF_WAVES[INF_NUM_WAVES] = { #undef W }; -#define INF_MAX_NPCS 32 -#define INF_OBS_NPCS 37 +#define INF_MAX_NPCS 37 +#define INF_OBS_NPCS 14 #define INF_START_READY_TICKS 6 #define INF_MAX_DEAD_MOBS 16 @@ -609,13 +609,6 @@ enum { INF_JOSEPH_REWARD_MODE_ON = 1, }; -enum { - INF_STEP_OUT_FORECAST_MODE_OFF = 0, - INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT = 1, - INF_STEP_OUT_FORECAST_MODE_FAST_STATIC_TILE = 2, - INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE = 3, -}; - enum { INF_CURRICULUM_SUPPLY_MODE_OFF = 0, INF_CURRICULUM_SUPPLY_MODE_ALL = 1, @@ -671,8 +664,6 @@ typedef struct { float supply_milestone_restore_reward_coeff; float death_penalty_coeff; int terminal_penalty_enabled; - int step_out_forecast_obs_enabled; - int step_out_forecast_obs_mode; float phase_900_bonus; float phase_600_bonus; float phase_300_bonus; @@ -835,11 +826,6 @@ typedef struct { int total_target_available_no_attack_ticks; int total_safe_attack_opportunity_missed_ticks; int total_progressless_ticks; - float total_npc_pressure_if_ready_count; - float total_npc_pressure_this_tick_count; - float total_npc_pressure_if_ready_max_hit; - float total_npc_pressure_this_tick_max_hit; - float max_npc_pressure_incoming_hit; int attack_ready_no_attack_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; int target_available_no_attack_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; int safe_attack_opportunity_missed_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc index 4c108feb47..6e6f71853d 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -1,4 +1,52 @@ #line 6591 "encounter_inferno.h" +#define INF_PLAYER_OBS_SIZE 45 +#define INF_PILLAR_FEATURES 3 +#define INF_PILLAR_OBS_SIZE (INF_NUM_PILLARS * INF_PILLAR_FEATURES) +#define INF_NPC_SLOT_FEATURES 13 +#define INF_TOTAL_NPC_OBS_SIZE (INF_OBS_NPCS * INF_NPC_SLOT_FEATURES) +#define INF_FEATURES_PER_HIT 3 +#define INF_SPARK_OBS_SLOTS INF_MAX_PENDING_SPARKS +#define INF_FEATURES_PER_SPARK 4 +#define INF_PENDING_HIT_OBS_SIZE \ + (ENCOUNTER_MAX_PENDING_HITS * INF_FEATURES_PER_HIT) +#define INF_PENDING_SPARK_OBS_SIZE \ + (INF_SPARK_OBS_SLOTS * INF_FEATURES_PER_SPARK) +#define INF_INVENTORY_OBS_SIZE OSRS_INVENTORY_SIZE +#define INF_EQUIPPED_OBS_SIZE OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES + +#define INF_OBS_AFTER_PLAYER INF_PLAYER_OBS_SIZE +#define INF_OBS_AFTER_PILLARS (INF_OBS_AFTER_PLAYER + INF_PILLAR_OBS_SIZE) +#define INF_OBS_AFTER_NPCS (INF_OBS_AFTER_PILLARS + INF_TOTAL_NPC_OBS_SIZE) +#define INF_OBS_AFTER_PENDING_HITS \ + (INF_OBS_AFTER_NPCS + INF_PENDING_HIT_OBS_SIZE) +#define INF_OBS_AFTER_SPARKS \ + (INF_OBS_AFTER_PENDING_HITS + INF_PENDING_SPARK_OBS_SIZE) +#define INF_OBS_AFTER_INVENTORY \ + (INF_OBS_AFTER_SPARKS + INF_INVENTORY_OBS_SIZE) +#define INF_OBS_AFTER_EQUIPMENT \ + (INF_OBS_AFTER_INVENTORY + INF_EQUIPPED_OBS_SIZE) +#define INF_NUM_OBS INF_OBS_AFTER_EQUIPMENT + +static_assert(INF_OBS_AFTER_PLAYER == 45, "Inferno player observation boundary"); +static_assert(INF_OBS_AFTER_PILLARS == 54, "Inferno pillar observation boundary"); +static_assert(INF_OBS_AFTER_NPCS == 236, "Inferno NPC observation boundary"); +static_assert(INF_OBS_AFTER_PENDING_HITS == 332, + "Inferno pending-hit observation boundary"); +static_assert(INF_OBS_AFTER_SPARKS == 460, "Inferno spark observation boundary"); +static_assert(INF_OBS_AFTER_INVENTORY == 488, + "Inferno inventory observation boundary"); +static_assert(INF_OBS_AFTER_EQUIPMENT == 498, + "Inferno equipment observation boundary"); +static_assert(INF_NUM_OBS == 498, "Inferno observation width"); + +#define INF_PROTECTION_PRAYER_CODE_SCALE 8.0f +#define INF_OFFENSIVE_PRAYER_CODE_SCALE 4.0f +#define INF_WAVE_PHASE_CODE_SCALE 8.0f +#define INF_WEAPON_SET_CODE_SCALE 4.0f +#define INF_NPC_TYPE_CODE_SCALE 16.0f +#define INF_ATTACK_STYLE_CODE_SCALE 4.0f +#define INF_TARGET_CATEGORY_CODE_SCALE 8.0f + typedef enum { INF_TARGET_CATEGORY_NONE = 0, INF_TARGET_CATEGORY_PLAYER, @@ -32,16 +80,6 @@ static int inf_wave_phase_index(int wave_idx) { return 5; } -static int inf_npc_is_phantom_barrage_targetable_now( - InfernoState* s, - const InfernoContext* ctx, - int npc_idx -) { - if (s->player.attack_timer != 0) return 0; - if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; - return s->npcs[npc_idx].death_ticks == 1 && - inf_player_can_phantom_barrage_npc(s, ctx, npc_idx); -} static int inf_pending_spark_obs_less( const InfernoState* s, @@ -64,27 +102,6 @@ static int inf_pending_spark_obs_less( return ai < bi; } -static inline float inf_obs_norm_count(int count) { - return (float)count / (float)INF_OBS_NPCS; -} - -static inline float inf_obs_norm_damage_sum(int damage) { - float value = (float)damage / 300.0f; - return value > 1.0f ? 1.0f : value; -} - -static inline float inf_obs_norm_range_deficit(int deficit) { - float value = (float)deficit / 15.0f; - return value > 1.0f ? 1.0f : value; -} - -static inline float inf_obs_norm_npc_size(int size) { - return (float)size / 7.0f; -} - -static inline float inf_obs_norm_pillar_size(const InfPillar* pillar) { - return pillar->active ? (float)INF_PILLAR_SIZE / 7.0f : 0.0f; -} static const struct { InfNPCType type; int count; } INF_OBS_SLOT_LAYOUT[INF_NUM_NPC_TYPES] = { @@ -99,45 +116,40 @@ INF_OBS_SLOT_LAYOUT[INF_NUM_NPC_TYPES] = { static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx) { int obs_slots[INF_OBS_NPCS]; - for (int j = 0; j < INF_OBS_NPCS; j++) obs_slots[j] = -1; + for (int slot = 0; slot < INF_OBS_NPCS; slot++) obs_slots[slot] = -1; int slot_counts[INF_NUM_NPC_TYPES] = {0}; - int slot_offsets[INF_NUM_NPC_TYPES]; - int slot_max[INF_NUM_NPC_TYPES]; - - int offset = 0; - for (int t = 0; t < INF_NUM_NPC_TYPES; t++) { - slot_offsets[INF_OBS_SLOT_LAYOUT[t].type] = offset; - slot_max[INF_OBS_SLOT_LAYOUT[t].type] = INF_OBS_SLOT_LAYOUT[t].count; - offset += INF_OBS_SLOT_LAYOUT[t].count; - } - - for (int n = 0; n < INF_MAX_NPCS; n++) { - InfNPC* npc = &s->npcs[n]; - if (npc->active && npc->death_ticks == 0) { - int t = npc->type; - if (slot_counts[t] < slot_max[t]) { - obs_slots[slot_offsets[t] + slot_counts[t]] = n; - slot_counts[t]++; - } - } - } - - for (int n = 0; n < INF_MAX_NPCS; n++) { - InfNPC* npc = &s->npcs[n]; - if (npc->active && npc->death_ticks > 0 && - inf_npc_is_phantom_barrage_obs_candidate(s, ctx, n)) { - int t = npc->type; - if (slot_counts[t] < slot_max[t]) { - obs_slots[slot_offsets[t] + slot_counts[t]] = n; - slot_counts[t]++; + int next_slot = 0; + + for (int pass = 0; pass < 2; pass++) { + for (int layout_idx = 0; layout_idx < INF_NUM_NPC_TYPES; layout_idx++) { + InfNPCType type = INF_OBS_SLOT_LAYOUT[layout_idx].type; + int type_cap = INF_OBS_SLOT_LAYOUT[layout_idx].count; + + for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { + InfNPC* npc = &s->npcs[npc_idx]; + if (!npc->active || npc->type != type) continue; + + int eligible = pass == 0 + ? npc->death_ticks == 0 + : npc->death_ticks > 0 && + inf_npc_is_phantom_barrage_obs_candidate(s, ctx, npc_idx); + if (!eligible || slot_counts[type] >= type_cap) continue; + + if (next_slot >= INF_OBS_NPCS) { + fprintf(stderr, + "inferno: observation target capacity exceeded at %d slots\n", + INF_OBS_NPCS); + abort(); + } + obs_slots[next_slot++] = npc_idx; + slot_counts[type]++; } } } - for (int j = 0; j < INF_OBS_NPCS; j++) { - s->current_obs_slots[j] = obs_slots[j]; - } + for (int slot = 0; slot < INF_OBS_NPCS; slot++) + s->current_obs_slots[slot] = obs_slots[slot]; } static void inf_refresh_current_obs_slots(InfernoState* s) { @@ -157,418 +169,202 @@ static void inf_write_obs_ctx( #endif memset(obs, 0, INF_NUM_OBS * sizeof(float)); int i = 0; - int px = s->player.x, py = s->player.y; + int px = s->player.x; + int py = s->player.y; const EncounterLoadoutStats* ls = inf_current_loadout_stats(s, ctx); InfSupplyDoses full_supplies = inf_full_starting_supplies(); + int offensive_prayer_code = 0; + if (s->player.offensive_prayer == OFFENSIVE_PRAYER_PIETY) + offensive_prayer_code = 1; + else if (s->player.offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) + offensive_prayer_code = 2; + else if (s->player.offensive_prayer == OFFENSIVE_PRAYER_AUGURY) + offensive_prayer_code = 3; + obs[i++] = (float)s->player.current_hitpoints / 99.0f; obs[i++] = (float)(px - INF_ARENA_MIN_X) / (float)INF_ARENA_WIDTH; - obs[i++] = (float)(INF_ARENA_MAX_X - px) / (float)INF_ARENA_WIDTH; obs[i++] = (float)(py - INF_ARENA_MIN_Y) / (float)INF_ARENA_HEIGHT; - obs[i++] = (float)(INF_ARENA_MAX_Y - py) / (float)INF_ARENA_HEIGHT; - obs[i++] = (s->player.prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; - obs[i++] = (s->player.prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; - obs[i++] = (s->player.prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; - obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_PIETY) ? 1.0f : 0.0f; - obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) ? 1.0f : 0.0f; - obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; + obs[i++] = (float)s->player.prayer / INF_PROTECTION_PRAYER_CODE_SCALE; + obs[i++] = (float)offensive_prayer_code / INF_OFFENSIVE_PRAYER_CODE_SCALE; obs[i++] = (float)s->player.brew_doses / (float)full_supplies.brew_doses; obs[i++] = (float)s->player.restore_doses / (float)full_supplies.restore_doses; obs[i++] = (float)s->player.current_prayer / 99.0f; - obs[i++] = (float)s->wave / (float)INF_NUM_WAVES; - { - int phase = inf_wave_phase_index(s->wave); - for (int p = 0; p < 6; p++) - obs[i++] = (p == phase) ? 1.0f : 0.0f; - } - obs[i++] = inf_zuk_attack_timer_obs(s); - obs[i++] = (s->weapon_set == INF_GEAR_MAGE) ? 1.0f : 0.0f; - obs[i++] = (s->weapon_set == INF_GEAR_LONG_RANGE) ? 1.0f : 0.0f; - obs[i++] = (s->weapon_set == INF_GEAR_BP) ? 1.0f : 0.0f; obs[i++] = (float)s->player.bastion_doses / (float)full_supplies.bastion_doses; obs[i++] = (float)s->player.stamina_doses / (float)full_supplies.stamina_doses; - obs[i++] = (s->stamina_active_ticks > 0) ? 1.0f : 0.0f; + obs[i++] = (float)s->wave / (float)INF_NUM_WAVES; + obs[i++] = (float)(inf_wave_phase_index(s->wave) + 1) / + INF_WAVE_PHASE_CODE_SCALE; + obs[i++] = inf_zuk_attack_timer_obs(s); + obs[i++] = (float)(s->weapon_set + 1) / INF_WEAPON_SET_CODE_SCALE; + obs[i++] = s->stamina_active_ticks > 0 ? 1.0f : 0.0f; obs[i++] = (float)s->player.potion_timer / 3.0f; obs[i++] = (float)s->player.attack_timer / 8.0f; + obs[i++] = osrs_interaction_active(&s->interaction) ? 1.0f : 0.0f; + obs[i++] = (float)s->dead_mob_count / (float)INF_MAX_DEAD_MOBS; + obs[i++] = (float)s->player.special_energy / 100.0f; obs[i++] = (float)s->player.current_defence / 99.0f; obs[i++] = (float)s->player.current_ranged / 99.0f; obs[i++] = (float)s->player.current_magic / 99.0f; - obs[i++] = osrs_interaction_active(&s->interaction) ? 1.0f : 0.0f; obs[i++] = (float)ls->attack_range / 15.0f; - obs[i++] = (float)s->dead_mob_count / (float)INF_MAX_DEAD_MOBS; obs[i++] = (float)ls->max_hit / 80.0f; obs[i++] = (float)ls->attack_speed / 6.0f; obs[i++] = (float)ls->def_stab / 300.0f; + obs[i++] = (float)ls->def_slash / 300.0f; + obs[i++] = (float)ls->def_crush / 300.0f; obs[i++] = (float)ls->def_magic / 300.0f; obs[i++] = (float)ls->def_ranged / 300.0f; - obs[i++] = (float)s->player.special_energy / 100.0f; - - { - int min_timer = 999; - int min_style = 0; - int has_melee_2 = 0, has_ranged_2 = 0, has_magic_2 = 0; - - for (int h = 0; h < s->player_pending_hits.count; h++) { - EncounterPendingHit* ph = &s->player_pending_hits.hits[h]; - if (ph->check_prayer) { - int t = inf_pending_hit_obs_timer(ph); - if (t < min_timer) { - min_timer = t; - min_style = ph->attack_style; - } - if (t <= 2) { - if (ph->attack_style == ATTACK_STYLE_MELEE) has_melee_2 = 1; - if (ph->attack_style == ATTACK_STYLE_RANGED) has_ranged_2 = 1; - if (ph->attack_style == ATTACK_STYLE_MAGIC) has_magic_2 = 1; - } - } + obs[i++] = (float)ls->eff_level / 165.0f; + obs[i++] = (float)ls->attack_bonus / 200.0f; + obs[i++] = (float)ls->strength_bonus / 160.0f; + obs[i++] = (float)osrs_spec_cost( + s->player.equipped[GEAR_SLOT_WEAPON]) / 100.0f; + obs[i++] = (float)ls->spell_base_damage / 40.0f; + + if (s->wave == INF_WAVE_ZUK) { + int shield_idx = s->zuk.shield_idx; + int shield_active = shield_idx >= 0 && + shield_idx < INF_MAX_NPCS && s->npcs[shield_idx].active; + obs[i++] = shield_active + ? (s->zuk.shield_freeze > 0 ? 0.0f : (float)s->zuk.shield_dir) + : 0.0f; + obs[i++] = shield_active ? (float)s->zuk.shield_freeze / 5.0f : 0.0f; + int behind_shield = 0; + float shield_offset = 0.0f; + if (shield_active) { + int shield_x = s->npcs[shield_idx].x; + int shield_size = INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size; + int shield_center = shield_x + shield_size / 2; + shield_offset = (float)(px - shield_center) / 15.0f; + behind_shield = + px >= shield_x && px < shield_x + shield_size && py >= 41; } - - for (int n = 0; n < INF_MAX_NPCS; n++) { - InfNPC* npc = &s->npcs[n]; - if (!npc->active || npc->death_ticks > 0) continue; - InfNpcPlayerThreat threat = inf_npc_player_threat(s, npc); - int style_mask = threat.telegraph_style_mask; - if (!style_mask) continue; - if (npc->stun_timer > 0 || - npc->dig_freeze_timer > 0 || - npc->dig_attack_delay > 0) - continue; - int preview_style = inf_attack_style_obs_preview(style_mask); - - int t = npc->attack_timer; - if (t == 0) t = 1; - - if (t < min_timer) { - min_timer = t; - min_style = preview_style; - } - if (t <= 2) { - if (style_mask & INF_STYLE_MASK_MELEE) has_melee_2 = 1; - if (style_mask & INF_STYLE_MASK_RANGED) has_ranged_2 = 1; - if (style_mask & INF_STYLE_MASK_MAGIC) has_magic_2 = 1; - } - } - - int conflict_count = has_melee_2 + has_ranged_2 + has_magic_2; - obs[i++] = (min_timer < 999) ? (float)min_timer / 10.0f : 1.0f; - obs[i++] = (min_style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; - obs[i++] = (min_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; - obs[i++] = (min_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; - obs[i++] = (float)conflict_count / 3.0f; + obs[i++] = behind_shield ? 1.0f : 0.0f; + obs[i++] = shield_offset; + obs[i++] = s->zuk.enraged ? 1.0f : 0.0f; + obs[i++] = (float)s->zuk.set_timer / 350.0f; + obs[i++] = s->zuk.timer_paused ? 1.0f : 0.0f; + obs[i++] = s->zuk.jad_spawned ? 1.0f : 0.0f; + obs[i++] = s->zuk.healer_spawned ? 1.0f : 0.0f; + } else { + i += 9; } + assert(i == INF_OBS_AFTER_PLAYER); - { - InfNpcPressureSummary pressure = inf_npc_pressure_summary(s); - obs[i++] = inf_obs_norm_count(pressure.if_ready_total); - obs[i++] = inf_obs_norm_count(pressure.if_ready_melee); - obs[i++] = inf_obs_norm_count(pressure.if_ready_ranged); - obs[i++] = inf_obs_norm_count(pressure.if_ready_magic); - obs[i++] = (float)pressure.if_ready_max_hit / 150.0f; - obs[i++] = inf_obs_norm_damage_sum(pressure.if_ready_sum_max_hit); - obs[i++] = inf_obs_norm_count(pressure.this_tick_total); - obs[i++] = inf_obs_norm_count(pressure.this_tick_melee); - obs[i++] = inf_obs_norm_count(pressure.this_tick_ranged); - obs[i++] = inf_obs_norm_count(pressure.this_tick_magic); - obs[i++] = (float)pressure.this_tick_max_hit / 150.0f; - obs[i++] = inf_obs_norm_damage_sum(pressure.this_tick_sum_max_hit); - obs[i++] = inf_obs_norm_count(pressure.timer_lte_1); - obs[i++] = inf_obs_norm_count(pressure.timer_lte_2); - obs[i++] = inf_obs_norm_count(pressure.timer_lte_4); - obs[i++] = (float)pressure.if_ready_style_count / 3.0f; - obs[i++] = (float)pressure.this_tick_style_count / 3.0f; - obs[i++] = (pressure.nearest_timer < 999) - ? (float)pressure.nearest_timer / 10.0f - : 1.0f; + for (int pillar_idx = 0; pillar_idx < INF_NUM_PILLARS; pillar_idx++) { + const InfPillar* pillar = &s->pillars[pillar_idx]; + obs[i++] = (float)pillar->hp / (float)INF_PILLAR_HP; + obs[i++] = (float)(pillar->x - px) / (float)INF_ARENA_WIDTH; + obs[i++] = (float)(pillar->y - py) / (float)INF_ARENA_HEIGHT; } - - { - int is_zuk = (s->wave == 68); - obs[i++] = is_zuk ? 1.0f : 0.0f; - - if (is_zuk) { - int si = s->zuk.shield_idx; - int shield_active = (si >= 0 && s->npcs[si].active); - - obs[i++] = shield_active - ? ((s->zuk.shield_freeze > 0) ? 0.0f : (float)s->zuk.shield_dir) - : 0.0f; - obs[i++] = shield_active ? (float)s->zuk.shield_freeze / 5.0f : 0.0f; - int behind = 0; - float shield_offset = 0.0f; - if (shield_active) { - int sx = s->npcs[si].x; - int sz = INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size; - int shield_center = sx + sz / 2; - shield_offset = (float)(px - shield_center) / 15.0f; - behind = (px >= sx && px < sx + sz && py >= 41); - } - obs[i++] = behind ? 1.0f : 0.0f; - obs[i++] = shield_offset; - obs[i++] = s->zuk.enraged ? 1.0f : 0.0f; - obs[i++] = (float)s->zuk.set_timer / 350.0f; - obs[i++] = s->zuk.timer_paused ? 1.0f : 0.0f; - obs[i++] = s->zuk.jad_spawned ? 1.0f : 0.0f; - obs[i++] = s->zuk.healer_spawned ? 1.0f : 0.0f; - } else { - for (int z = 0; z < 9; z++) obs[i++] = 0.0f; - } - } - - for (int p = 0; p < INF_NUM_PILLARS; p++) { - obs[i++] = s->pillars[p].active ? 1.0f : 0.0f; - obs[i++] = (float)s->pillars[p].hp / (float)INF_PILLAR_HP; - obs[i++] = (float)(s->pillars[p].x - px) / (float)INF_ARENA_WIDTH; - obs[i++] = (float)(s->pillars[p].y - py) / (float)INF_ARENA_HEIGHT; - obs[i++] = inf_obs_norm_pillar_size(&s->pillars[p]); - } - + assert(i == INF_OBS_AFTER_PILLARS); #ifdef INF_PROFILE_ENABLED INF_PROFILE_MARK(INF_PROF_OBS_PREFIX); #endif + inf_refresh_current_obs_slots_ctx(s, ctx); #ifdef INF_PROFILE_ENABLED INF_PROFILE_MARK(INF_PROF_OBS_REFRESH_SLOTS); #endif - int slot_types[INF_OBS_NPCS]; - int st_idx = 0; - for (int t = 0; t < INF_NUM_NPC_TYPES; t++) { - for (int j = 0; j < INF_OBS_SLOT_LAYOUT[t].count; j++) - slot_types[st_idx++] = INF_OBS_SLOT_LAYOUT[t].type; - } - - for (int k = 0; k < INF_OBS_NPCS; k++) { - int n = s->current_obs_slots[k]; - int type = slot_types[k]; - - int has_style = (type == INF_NPC_BLOB || type == INF_NPC_JAD); - int has_scan = (type == INF_NPC_BLOB); - int has_los = (type != INF_NPC_NIBBLER && type != INF_NPC_MELEER && type != INF_NPC_HEALER_JAD && type != INF_NPC_ZUK_SHIELD); - int has_target_category = (type != INF_NPC_NIBBLER && type != INF_NPC_ZUK_SHIELD); - int has_timer = (type != INF_NPC_NIBBLER && type != INF_NPC_HEALER_JAD && type != INF_NPC_ZUK_SHIELD); - int has_meleer_dig = (type == INF_NPC_MELEER); - - if (n >= 0) { - InfNPC* npc = &s->npcs[n]; - for (int ty = 0; ty < INF_NPC_TYPE_ONEHOT; ty++) - obs[i++] = (ty == type) ? 1.0f : 0.0f; - obs[i++] = (float)npc->hp / (float)npc->max_hp; - obs[i++] = (float)(npc->x - px) / (float)INF_ARENA_WIDTH; - obs[i++] = (float)(npc->y - py) / (float)INF_ARENA_HEIGHT; - obs[i++] = inf_obs_norm_npc_size(npc->size); - obs[i++] = has_timer ? (float)npc->attack_timer / 10.0f : 0.0f; - - if (has_style) { - int style = (npc->type == INF_NPC_JAD) ? inf_npc_jad(npc)->attack_style : npc->attack_style; - obs[i++] = (style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; - obs[i++] = (style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; - obs[i++] = (style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; - } else { - obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; - } - - obs[i++] = (has_los && inf_npc_has_los(s, n)) ? 1.0f : 0.0f; - - if (has_scan && npc->blob_scanned_prayer >= 0) { - OverheadPrayer scanned = (OverheadPrayer)npc->blob_scanned_prayer; - obs[i++] = (scanned == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; - obs[i++] = (scanned == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; - obs[i++] = (scanned != PRAYER_PROTECT_MAGIC && scanned != PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; - } else { - obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; - } - - obs[i++] = (float)inf_barrage_total_target_count(s, n) / - (float)BARRAGE_MAX_HITS; - - { - InfNpcPlayerThreat threat = inf_npc_player_threat(s, npc); - obs[i++] = (float)threat.edge_distance / (float)INF_ARENA_HEIGHT; - obs[i++] = threat.can_attack_if_ready ? 1.0f : 0.0f; - obs[i++] = threat.can_attack_this_tick ? 1.0f : 0.0f; - obs[i++] = (float)npc->frozen_ticks / (float)BARRAGE_FREEZE_TICKS; - obs[i++] = inf_player_can_attack_npc_from_current_tile(s, n) ? 1.0f : 0.0f; - obs[i++] = inf_player_has_los_to_npc_from_current_tile(s, n) ? 1.0f : 0.0f; - InfPlayerNpcLoadoutReach mage = - inf_player_loadout_reach_to_npc(s, n, INF_GEAR_MAGE); - InfPlayerNpcLoadoutReach long_range = - inf_player_loadout_reach_to_npc(s, n, INF_GEAR_LONG_RANGE); - InfPlayerNpcLoadoutReach blowpipe = - inf_player_loadout_reach_to_npc(s, n, INF_GEAR_BP); - obs[i++] = mage.can_attack_now ? 1.0f : 0.0f; - obs[i++] = long_range.can_attack_now ? 1.0f : 0.0f; - obs[i++] = blowpipe.can_attack_now ? 1.0f : 0.0f; - obs[i++] = inf_obs_norm_range_deficit(mage.range_deficit); - obs[i++] = inf_obs_norm_range_deficit(long_range.range_deficit); - obs[i++] = inf_obs_norm_range_deficit(blowpipe.range_deficit); - } - - if (has_target_category) { - InfTargetCategory category = inf_npc_target_category(s, npc); - obs[i++] = (category == INF_TARGET_CATEGORY_PLAYER) ? 1.0f : 0.0f; - obs[i++] = (category == INF_TARGET_CATEGORY_ZUK) ? 1.0f : 0.0f; - obs[i++] = (category == INF_TARGET_CATEGORY_SHIELD) ? 1.0f : 0.0f; - obs[i++] = (category == INF_TARGET_CATEGORY_OTHER_NPC) ? 1.0f : 0.0f; - } else { - obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; - } - obs[i++] = (osrs_interaction_active(&s->interaction) && s->interaction.target_slot == n) ? 1.0f : 0.0f; - obs[i++] = inf_npc_is_phantom_barrage_targetable_now(s, ctx, n) ? 1.0f : 0.0f; - if (has_meleer_dig) { - float no_los = (float)npc->no_los_ticks / 50.0f; - if (no_los > 1.0f) no_los = 1.0f; - obs[i++] = no_los; - obs[i++] = (float)npc->dig_freeze_timer / 6.0f; - obs[i++] = (float)npc->dig_attack_delay / 6.0f; - } else { - obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; - } - } else { - for (int j = 0; j < INF_NPC_SLOT_FEATURES; j++) obs[i++] = 0.0f; + for (int slot_idx = 0; slot_idx < INF_OBS_NPCS; slot_idx++) { + int npc_idx = s->current_obs_slots[slot_idx]; + if (npc_idx < 0) { + i += INF_NPC_SLOT_FEATURES; + continue; } - } -#ifdef INF_PROFILE_ENABLED - INF_PROFILE_MARK(INF_PROF_OBS_NPC_SLOTS); -#endif - { - if (i != INF_OBS_AFTER_NPCS) { - fprintf(stderr, "FATAL: obs misaligned after NPC section: i=%d expected=%d\n", - i, INF_OBS_AFTER_NPCS); - abort(); + InfNPC* npc = &s->npcs[npc_idx]; + AttackStyle style = npc->type == INF_NPC_JAD + ? inf_npc_jad(npc)->attack_style + : (AttackStyle)npc->attack_style; + InfTargetCategory category = inf_npc_target_category(s, npc); + float no_los_ticks = 0.0f; + float dig_freeze_timer = 0.0f; + float dig_attack_delay = 0.0f; + if (npc->type == INF_NPC_MELEER) { + no_los_ticks = (float)npc->no_los_ticks / 50.0f; + if (no_los_ticks > 1.0f) no_los_ticks = 1.0f; + dig_freeze_timer = (float)npc->dig_freeze_timer / 6.0f; + dig_attack_delay = (float)npc->dig_attack_delay / 6.0f; } - } - { - if (ctx->config.step_out_forecast_obs_mode != INF_STEP_OUT_FORECAST_MODE_OFF) { - InfStepOutForecast forecast; - inf_build_step_out_forecast_mode_ctx(s, ctx, &forecast); - for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - const InfStepOutForecastAction* action = &forecast.actions[action_idx]; - int first_attack_tick = 0; - int first_style_mask = 0; - int max_hit = 0; - int ranger_mager_same_tick = 0; - for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { - const InfStepOutForecastTick* tick = &action->ticks[tick_idx]; - int style_mask = inf_step_out_forecast_tick_style_mask(tick); - if (first_attack_tick == 0 && - inf_step_out_forecast_tick_has_event(tick)) { - first_attack_tick = tick_idx + 1; - first_style_mask = style_mask; - } - if (tick->max_hit > max_hit) max_hit = tick->max_hit; - if (tick->ranger_count > 0 && tick->mager_count > 0) - ranger_mager_same_tick = 1; - } - obs[i++] = action->valid ? 1.0f : 0.0f; - obs[i++] = (float)first_attack_tick / (float)INF_STEP_OUT_FORECAST_HORIZON; - obs[i++] = (float)first_style_mask / 7.0f; - obs[i++] = (float)max_hit / 150.0f; - obs[i++] = action->same_tick_mixed_style_conflict ? 1.0f : 0.0f; - obs[i++] = ranger_mager_same_tick ? 1.0f : 0.0f; - obs[i++] = action->ranger_mager_offtick_opportunity ? 1.0f : 0.0f; - obs[i++] = action->melee_fallback_exposure ? 1.0f : 0.0f; - } - } else { - i += INF_STEP_OUT_FORECAST_OBS_SIZE; - } + obs[i++] = (float)(npc->type + 1) / INF_NPC_TYPE_CODE_SCALE; + obs[i++] = npc->max_hp > 0 + ? (float)npc->hp / (float)npc->max_hp + : 0.0f; + obs[i++] = (float)(npc->x - px) / (float)INF_ARENA_WIDTH; + obs[i++] = (float)(npc->y - py) / (float)INF_ARENA_HEIGHT; + obs[i++] = (float)npc->attack_timer / 10.0f; + obs[i++] = (float)style / INF_ATTACK_STYLE_CODE_SCALE; + obs[i++] = inf_npc_has_los(s, npc_idx) ? 1.0f : 0.0f; + obs[i++] = (float)npc->frozen_ticks / (float)BARRAGE_FREEZE_TICKS; + obs[i++] = (float)category / INF_TARGET_CATEGORY_CODE_SCALE; + obs[i++] = osrs_interaction_active(&s->interaction) && + s->interaction.target_slot == npc_idx ? 1.0f : 0.0f; + obs[i++] = no_los_ticks; + obs[i++] = dig_freeze_timer; + obs[i++] = dig_attack_delay; } + assert(i == INF_OBS_AFTER_NPCS); #ifdef INF_PROFILE_ENABLED - INF_PROFILE_MARK(INF_PROF_OBS_FORECAST); + INF_PROFILE_MARK(INF_PROF_OBS_NPC_SLOTS); #endif - { - if (i != INF_OBS_AFTER_FORECAST) { - fprintf(stderr, "FATAL: obs misaligned after step-out forecast: i=%d expected=%d\n", - i, INF_OBS_AFTER_FORECAST); - abort(); - } - } - - for (int h = 0; h < ENCOUNTER_MAX_PENDING_HITS; h++) { - if (h < s->player_pending_hits.count) { - EncounterPendingHit* ph = &s->player_pending_hits.hits[h]; - obs[i++] = 1.0f; - obs[i++] = (ph->attack_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; - obs[i++] = (ph->attack_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; - obs[i++] = (float)inf_pending_hit_obs_timer(ph) / 10.0f; - obs[i++] = (float)ph->damage / 150.0f; + for (int hit_idx = 0; hit_idx < ENCOUNTER_MAX_PENDING_HITS; hit_idx++) { + if (hit_idx < s->player_pending_hits.count) { + EncounterPendingHit* hit = &s->player_pending_hits.hits[hit_idx]; + obs[i++] = (float)hit->attack_style / INF_ATTACK_STYLE_CODE_SCALE; + obs[i++] = (float)inf_pending_hit_obs_timer(hit) / 10.0f; + obs[i++] = (float)hit->damage / 150.0f; } else { - for (int j = 0; j < INF_FEATURES_PER_HIT; j++) obs[i++] = 0.0f; + i += INF_FEATURES_PER_HIT; } } + assert(i == INF_OBS_AFTER_PENDING_HITS); #ifdef INF_PROFILE_ENABLED INF_PROFILE_MARK(INF_PROF_OBS_PENDING_HITS); #endif - if (i != INF_OBS_AFTER_PENDING_HITS) { - fprintf(stderr, "FATAL: obs misaligned after pending hits: i=%d expected=%d\n", - i, INF_OBS_AFTER_PENDING_HITS); - abort(); - } - int used_sparks[INF_MAX_PENDING_SPARKS] = {0}; - for (int slot = 0; slot < INF_SPARK_OBS_SLOTS; slot++) { + for (int slot_idx = 0; slot_idx < INF_SPARK_OBS_SLOTS; slot_idx++) { int best = -1; - for (int sp = 0; sp < INF_MAX_PENDING_SPARKS; sp++) { - if (used_sparks[sp] || !s->pending_sparks[sp].active) continue; - if (best < 0 || - inf_pending_spark_obs_less( - s, &s->pending_sparks[sp], sp, - &s->pending_sparks[best], best)) { - best = sp; - } + for (int spark_idx = 0; spark_idx < INF_MAX_PENDING_SPARKS; spark_idx++) { + if (used_sparks[spark_idx] || !s->pending_sparks[spark_idx].active) + continue; + if (best < 0 || inf_pending_spark_obs_less( + s, &s->pending_sparks[spark_idx], spark_idx, + &s->pending_sparks[best], best)) + best = spark_idx; } - if (best >= 0) { - const InfPendingSpark* spark = &s->pending_sparks[best]; - used_sparks[best] = 1; - obs[i++] = 1.0f; - obs[i++] = (float)(spark->x - px) / (float)INF_ARENA_WIDTH; - obs[i++] = (float)(spark->y - py) / (float)INF_ARENA_HEIGHT; - obs[i++] = (float)(spark->src_x - px) / (float)INF_ARENA_WIDTH; - obs[i++] = (float)(spark->src_y - py) / (float)INF_ARENA_HEIGHT; - obs[i++] = (float)spark->ticks_remaining / 10.0f; - obs[i++] = (float)spark->damage / 10.0f; - } else { - for (int j = 0; j < INF_FEATURES_PER_SPARK; j++) obs[i++] = 0.0f; + if (best < 0) { + i += INF_FEATURES_PER_SPARK; + continue; } + const InfPendingSpark* spark = &s->pending_sparks[best]; + used_sparks[best] = 1; + obs[i++] = (float)(spark->x - px) / (float)INF_ARENA_WIDTH; + obs[i++] = (float)(spark->y - py) / (float)INF_ARENA_HEIGHT; + obs[i++] = (float)spark->ticks_remaining / 10.0f; + obs[i++] = (float)spark->damage / 10.0f; } + assert(i == INF_OBS_AFTER_SPARKS); #ifdef INF_PROFILE_ENABLED INF_PROFILE_MARK(INF_PROF_OBS_SPARKS); #endif - if (i != INF_OBS_AFTER_SPARKS) { - fprintf(stderr, "FATAL: obs misaligned before inventory block: i=%d expected=%d\n", - i, INF_OBS_AFTER_SPARKS); - abort(); + for (int cell_idx = 0; cell_idx < OSRS_INVENTORY_SIZE; cell_idx++) { + const OsrsInventoryCell* cell = &s->inventory_cells[cell_idx]; + obs[i++] = osrs_inventory_cell_obs_code_encode( + osrs_inventory_cell_obs_code(cell->item_idx, cell->raw_osrs_id)); } + assert(i == INF_OBS_AFTER_INVENTORY); - { - static const float inf_zero_post_use_deltas[6] = {0}; - for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - const OsrsInventoryCell* cell = &s->inventory_cells[c]; - int is_equipped = osrs_inventory_cell_holds_equipped_item( - &s->player, s->inventory_cells, c); - osrs_write_inventory_cell_affordance_features( - &obs[i], cell->item_idx, cell->raw_osrs_id, cell->dose, - is_equipped, inf_zero_post_use_deltas, - s->player.base_hitpoints, s->player.base_prayer, - s->player.base_ranged); - i += OSRS_INVENTORY_CELL_OBS_FEATURES; - } - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - osrs_write_equipped_self_features(&obs[i], s->player.equipped[slot]); - i += OSRS_EQUIPPED_SELF_OBS_FEATURES; - } - } - - if (i != INF_NUM_OBS) { - fprintf(stderr, "BUG: inf_write_obs wrote %d features, expected %d\n", i, INF_NUM_OBS); - abort(); - } + osrs_write_equipment_effect_aggregate( + &obs[i], &s->player.equipment_effect_profile); + i += OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES; + assert(i == INF_OBS_AFTER_EQUIPMENT); } static int inf_find_target_obs_slot(const InfernoState* s, int npc_slot) { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index f19e81b80b..c5c780d030 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -1,14 +1,4 @@ #line 7460 "encounter_inferno.h" -static void inf_require_valid_step_out_forecast_obs_mode(int value) { - if (value < INF_STEP_OUT_FORECAST_MODE_OFF || - value > INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE) { - fprintf(stderr, - "inferno: step_out_forecast_obs_mode must be in [0,3], got %d\n", - value); - abort(); - } -} - static void inf_put_int_ctx( EncounterState* state, EncounterContext* context, @@ -43,19 +33,6 @@ static void inf_put_int_ctx( inf_require_valid_curriculum_supply_mode(key, value); ctx->config.curriculum_no_brew_mode = value; } - else if (strcmp(key, "step_out_forecast_obs_enabled") == 0) { - int enabled = encounter_require_binary_config("inferno", key, value); - ctx->config.step_out_forecast_obs_enabled = enabled; - ctx->config.step_out_forecast_obs_mode = enabled ? - INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT : - INF_STEP_OUT_FORECAST_MODE_OFF; - } - else if (strcmp(key, "step_out_forecast_obs_mode") == 0) { - inf_require_valid_step_out_forecast_obs_mode(value); - ctx->config.step_out_forecast_obs_mode = value; - ctx->config.step_out_forecast_obs_enabled = - value != INF_STEP_OUT_FORECAST_MODE_OFF; - } else if (strcmp(key, "loadout_profile_mode") == 0) { inf_require_valid_loadout_profile_mode(value); ctx->config.loadout_profile_mode = (InfLoadoutProfileMode)value; @@ -248,12 +225,6 @@ static void* inf_get_log_ctx(EncounterState* state, EncounterContext* context) { ctx->log->safe_attack_opportunity_missed_ticks += (float)s->total_safe_attack_opportunity_missed_ticks; ctx->log->progressless_ticks += (float)s->total_progressless_ticks; - ctx->log->npc_pressure_if_ready_count += - s->total_npc_pressure_if_ready_count; - ctx->log->npc_pressure_this_tick_count += - s->total_npc_pressure_this_tick_count; - ctx->log->npc_pressure_max_incoming_hit += - s->max_npc_pressure_incoming_hit; for (int i = 0; i < OSRS_INFERNO_IDLE_PHASE_COUNT; i++) { ctx->log->attack_ready_no_attack_ticks_by_phase[i] += (float)s->attack_ready_no_attack_ticks_by_phase[i]; @@ -834,7 +805,6 @@ static uint64_t inf_config_fingerprint(const InfConfig* config) { INF_HASH_CONFIG_FIELD(config, &h, supply_milestone_restore_reward_coeff); INF_HASH_CONFIG_FIELD(config, &h, death_penalty_coeff); INF_HASH_CONFIG_FIELD(config, &h, terminal_penalty_enabled); - INF_HASH_CONFIG_FIELD(config, &h, step_out_forecast_obs_mode); INF_HASH_CONFIG_FIELD(config, &h, phase_900_bonus); INF_HASH_CONFIG_FIELD(config, &h, phase_600_bonus); INF_HASH_CONFIG_FIELD(config, &h, phase_300_bonus); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc index 250b99fd56..5c44c699df 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc @@ -604,22 +604,6 @@ static void inf_step_ctx(EncounterState* state, EncounterContext* context, const if (s->ticks_without_action > 0) s->total_idle_ticks++; s->total_brews_used += s->tick_scratch.brewed; s->total_blood_healed += s->tick_scratch.blood_heal; - { - InfNpcPressureSummary pressure = inf_npc_pressure_summary(s); - s->total_npc_pressure_if_ready_count += - (float)pressure.if_ready_total; - s->total_npc_pressure_this_tick_count += - (float)pressure.this_tick_total; - s->total_npc_pressure_if_ready_max_hit += - (float)pressure.if_ready_max_hit; - s->total_npc_pressure_this_tick_max_hit += - (float)pressure.this_tick_max_hit; - int max_incoming_hit = pressure.if_ready_max_hit > - pressure.this_tick_max_hit ? pressure.if_ready_max_hit : - pressure.this_tick_max_hit; - if ((float)max_incoming_hit > s->max_npc_pressure_incoming_hit) - s->max_npc_pressure_incoming_hit = (float)max_incoming_hit; - } if (can_player_attack && s->wave == 68) { s->total_zuk_ticks++; diff --git a/ocean/osrs/osrs_colosseum_item_obs_generated.h b/ocean/osrs/osrs_colosseum_item_obs_generated.h deleted file mode 100644 index 680e75ce1e..0000000000 --- a/ocean/osrs/osrs_colosseum_item_obs_generated.h +++ /dev/null @@ -1,24 +0,0 @@ -/* Generated by ocean/osrs/tools/gen_colosseum_item_obs_table.c. Do not edit. - * Dependency-free on purpose: src/ocean.cu includes it and no OSRS header. */ -#ifndef OSRS_COLOSSEUM_ITEM_OBS_GENERATED_H -#define OSRS_COLOSSEUM_ITEM_OBS_GENERATED_H - -#define COLO_ITEM_OBS_TABLE_ROWS 201 -#define COLO_ITEM_OBS_TABLE_COLS 15 -#define COLO_ITEM_OBS_TABLE_BASE_PRAYER 99 -#define COLO_ITEM_OBS_TABLE_BASE_LEVEL 99 - -/* The observation carries code/SCALE; multiply back to index a row. A power of two - * keeps the round trip exact in bf16 and keeps the code out of the global linear - * layer's dynamic range. */ -#define COLO_ITEM_OBS_CODE_SCALE 256 - -/* Slots the observation adds in; the table holds zero at both. */ -#define COLO_ITEM_OBS_OVERLAY_EQUIPPED 1 -#define COLO_ITEM_OBS_OVERLAY_HP_HEAL 10 - -/* Rows live in osrs_colosseum_item_obs_table.inc so each consumer names its own - * storage: src/ocean.cu wants a __device__ copy, the viewer wants a host one, and - * a single guarded definition would hand whichever included first to both. */ - -#endif diff --git a/ocean/osrs/osrs_inventory_clicks.h b/ocean/osrs/osrs_inventory_clicks.h index d22b40b15a..919457f3a1 100644 --- a/ocean/osrs/osrs_inventory_clicks.h +++ b/ocean/osrs/osrs_inventory_clicks.h @@ -231,22 +231,6 @@ static inline int osrs_consumable_offensive_boost_amount(OsrsConsumableKind k, i } } -static inline void osrs_item_effect_class4(uint32_t effect_mask, float out[4]) { - uint32_t lifesteal = OSRS_ITEM_EFFECT_BLOOD_FURY | OSRS_ITEM_EFFECT_SANG_HEAL; - uint32_t damage_amp = OSRS_ITEM_EFFECT_TWISTED_BOW | OSRS_ITEM_EFFECT_FANG | - OSRS_ITEM_EFFECT_TUMEKENS_SHADOW | OSRS_ITEM_EFFECT_DHAROK_PIECE | - OSRS_ITEM_EFFECT_DRAGON_HUNTER_WAND | OSRS_ITEM_EFFECT_VENATOR_BOUNCE; - uint32_t defensive = OSRS_ITEM_EFFECT_ELYSIAN | OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR | - OSRS_ITEM_EFFECT_RECOIL_RING | OSRS_ITEM_EFFECT_VENOM_IMMUNE | - OSRS_ITEM_EFFECT_ECHO_BOOTS | OSRS_ITEM_EFFECT_CONFLICTION | - OSRS_ITEM_EFFECT_VIRTUS_PIECE; - uint32_t util = OSRS_ITEM_EFFECT_LIGHTBEARER; - out[0] = (effect_mask & lifesteal) ? 1.0f : 0.0f; - out[1] = (effect_mask & damage_amp) ? 1.0f : 0.0f; - out[2] = (effect_mask & defensive) ? 1.0f : 0.0f; - out[3] = (effect_mask & util) ? 1.0f : 0.0f; -} - static inline uint8_t osrs_item_index_for_raw_osrs_id(uint16_t raw_osrs_id) { for (int i = 0; i < NUM_ITEMS; i++) { if (ITEM_DATABASE[i].item_id == raw_osrs_id) return (uint8_t)i; @@ -760,26 +744,21 @@ static inline OsrsInventoryCell osrs_inventory_cell_for_obs_code(int code) { }; } -/* The item-table row: the compact record with the two slots the observation supplies zeroed, - so a gather can add the observed values in without branching on the cell's kind. On a gear - cell the union slot holds effect_class4[2], which is an item fact and stays. */ +/* The generated item-table row: the full compact record at the table's fixed base stats. + Equipped is supplied by each observation and is the only zeroed slot. */ static inline void osrs_write_inventory_cell_obs_table_row( float* out, int code, int base_hitpoints, int base_prayer, - int base_level + int base_ranged ) { OsrsInventoryCell cell = osrs_inventory_cell_for_obs_code(code); osrs_write_inventory_cell_affordance_features_compact( out, cell.item_idx, cell.raw_osrs_id, cell.dose, 0, - base_hitpoints, base_prayer, base_level); + base_hitpoints, base_prayer, base_ranged); out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] = 0.0f; - if (out[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] == 0.0f && - out[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] == 0.0f) { - out[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL] = 0.0f; - } } /* The other half of the same record. Projecting both sides off one compact write is what @@ -811,7 +790,7 @@ static inline void osrs_write_inventory_cell_obs_code_features( is_gear ? 0.0f : compact[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL]; } -/* Inverse of the split above, and the exact arithmetic colo_ent_gather_inv performs. */ +/* Rebuilds the compact record, overwrites equipped, and overwrites HP-heal only for non-gear. */ static inline void osrs_expand_inventory_cell_obs_code_features( float* out, const float* coded, @@ -820,10 +799,14 @@ static inline void osrs_expand_inventory_cell_obs_code_features( for (int f = 0; f < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; f++) { out[f] = table_row[f]; } - out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] += + int is_gear = table_row[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] != 0.0f || + table_row[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] != 0.0f; + out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] = coded[OSRS_INVENTORY_CELL_OBS_EQUIPPED]; - out[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL] += - coded[OSRS_INVENTORY_CELL_OBS_HP_HEAL]; + if (!is_gear) { + out[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL] = + coded[OSRS_INVENTORY_CELL_OBS_HP_HEAL]; + } } #endif diff --git a/ocean/osrs/osrs_item_effects.h b/ocean/osrs/osrs_item_effects.h index d2c46e7209..97349debd2 100644 --- a/ocean/osrs/osrs_item_effects.h +++ b/ocean/osrs/osrs_item_effects.h @@ -64,6 +64,37 @@ static inline int osrs_effect_profile_has( return (profile->effect_mask & effect_mask) != 0; } +#define OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES 10 + +static inline void osrs_item_effect_class4(uint32_t effect_mask, float out[4]) { + uint32_t lifesteal = OSRS_ITEM_EFFECT_BLOOD_FURY | OSRS_ITEM_EFFECT_SANG_HEAL; + uint32_t damage_amp = OSRS_ITEM_EFFECT_TWISTED_BOW | OSRS_ITEM_EFFECT_FANG | + OSRS_ITEM_EFFECT_TUMEKENS_SHADOW | OSRS_ITEM_EFFECT_DHAROK_PIECE | + OSRS_ITEM_EFFECT_DRAGON_HUNTER_WAND | OSRS_ITEM_EFFECT_VENATOR_BOUNCE; + uint32_t defensive = OSRS_ITEM_EFFECT_ELYSIAN | OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR | + OSRS_ITEM_EFFECT_RECOIL_RING | OSRS_ITEM_EFFECT_VENOM_IMMUNE | + OSRS_ITEM_EFFECT_ECHO_BOOTS | OSRS_ITEM_EFFECT_CONFLICTION | + OSRS_ITEM_EFFECT_VIRTUS_PIECE; + uint32_t util = OSRS_ITEM_EFFECT_LIGHTBEARER; + out[0] = (effect_mask & lifesteal) ? 1.0f : 0.0f; + out[1] = (effect_mask & damage_amp) ? 1.0f : 0.0f; + out[2] = (effect_mask & defensive) ? 1.0f : 0.0f; + out[3] = (effect_mask & util) ? 1.0f : 0.0f; +} + +static inline void osrs_write_equipment_effect_aggregate( + float* out, + const OsrsEquipmentEffectProfile* profile +) { + osrs_item_effect_class4(profile->effect_mask, out); + out[4] = (float)profile->virtus_piece_count / 3.0f; + out[5] = (float)profile->dharok_piece_count / 4.0f; + out[6] = (float)profile->crystal_armour_points / 6.0f; + out[7] = profile->recoil_source != OSRS_RECOIL_SOURCE_NONE ? 1.0f : 0.0f; + out[8] = profile->spec_regen_mode == OSRS_SPEC_REGEN_MODE_LIGHTBEARER ? 1.0f : 0.0f; + out[9] = profile->shield_item != ITEM_NONE ? 1.0f : 0.0f; +} + static inline OsrsRecoilSource osrs_recoil_source_from_ring(uint8_t ring_item) { if (ring_item == ITEM_RING_OF_RECOIL) { return OSRS_RECOIL_SOURCE_RING_OF_RECOIL; diff --git a/ocean/osrs/osrs_item_obs_generated.h b/ocean/osrs/osrs_item_obs_generated.h new file mode 100644 index 0000000000..b8178857f4 --- /dev/null +++ b/ocean/osrs/osrs_item_obs_generated.h @@ -0,0 +1,17 @@ +/* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit. + * Dependency-free on purpose: src/ocean.cu includes it and no OSRS header. */ +#ifndef OSRS_ITEM_OBS_GENERATED_H +#define OSRS_ITEM_OBS_GENERATED_H + +#define OSRS_ITEM_OBS_TABLE_ROWS 201 +#define OSRS_ITEM_OBS_TABLE_COLS 15 +#define OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS 99 +#define OSRS_ITEM_OBS_TABLE_BASE_PRAYER 99 +#define OSRS_ITEM_OBS_TABLE_BASE_RANGED 99 + +#define OSRS_ITEM_OBS_CODE_SCALE 256 + +#define OSRS_ITEM_OBS_OVERLAY_EQUIPPED 1 +#define OSRS_ITEM_OBS_OVERLAY_HP_HEAL 10 + +#endif diff --git a/ocean/osrs/osrs_colosseum_item_obs_table.inc b/ocean/osrs/osrs_item_obs_table.inc similarity index 97% rename from ocean/osrs/osrs_colosseum_item_obs_table.inc rename to ocean/osrs/osrs_item_obs_table.inc index b58881c382..b7099d4635 100644 --- a/ocean/osrs/osrs_colosseum_item_obs_table.inc +++ b/ocean/osrs/osrs_item_obs_table.inc @@ -1,4 +1,4 @@ -/* Generated by ocean/osrs/tools/gen_colosseum_item_obs_table.c. Do not edit. +/* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit. * Included as the initialiser body of a [ROWS][COLS] float array. */ {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, @@ -145,10 +145,10 @@ {1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.400000006f, 0.0f}, {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, @@ -198,6 +198,6 @@ {1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.202020198f, 0.0f, 0.0f, 0.0f, 0.0f}, +{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.181818187f, 0.0f, 0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, diff --git a/ocean/osrs/osrs_types.h b/ocean/osrs/osrs_types.h index 2497454c08..6002f6afa0 100644 --- a/ocean/osrs/osrs_types.h +++ b/ocean/osrs/osrs_types.h @@ -650,9 +650,6 @@ typedef struct Log { float target_available_no_attack_ticks; float safe_attack_opportunity_missed_ticks; float progressless_ticks; - float npc_pressure_if_ready_count; - float npc_pressure_this_tick_count; - float npc_pressure_max_incoming_hit; float attack_ready_no_attack_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; float target_available_no_attack_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; float safe_attack_opportunity_missed_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; diff --git a/ocean/osrs/osrs_visual.c b/ocean/osrs/osrs_visual.c index 76704cb821..c59fcfccb3 100644 --- a/ocean/osrs/osrs_visual.c +++ b/ocean/osrs/osrs_visual.c @@ -47,20 +47,30 @@ _Static_assert(COLO_ENT_INF_INV_OBS_FEATS == COLO_INVENTORY_CELL_OBS_FEATURES, _Static_assert(COLO_ENT_INF_INV_FEATS == COLO_INVENTORY_CELL_ENCODER_FEATURES, "encoder inventory record is one item table row with the observed floats added in"); -_Static_assert(INF_ENT_FEATS == INF_NPC_SLOT_FEATURES, - "entity encoder per-NPC width must track the inferno observation layout"); +_Static_assert(INF_ENT_OBS_SIZE == INF_NUM_OBS, + "entity encoder global projection width must track the inferno observation layout"); +_Static_assert(INF_ENT_OBS_FEATS == INF_NPC_SLOT_FEATURES, + "entity encoder per-NPC observation width must track the inferno layout"); +_Static_assert( + INF_ENT_FEATS == INF_ENT_TYPE_ONEHOT + (INF_NPC_SLOT_FEATURES - 1), + "encoder record width is the obs record with the type code expanded to a one-hot"); _Static_assert(INF_ENT_NUM_NPCS == INF_OBS_NPCS, "entity encoder NPC count must track the inferno observation layout"); _Static_assert(INF_ENT_NPC_START == INF_OBS_AFTER_PILLARS, "entity encoder NPC block offset must track the inferno observation layout"); _Static_assert(INF_ENT_TYPE_ONEHOT == INF_NUM_NPC_TYPES, "entity pool presence mask is the NPC type one-hot; a new NPC type must widen it"); +_Static_assert(INF_ENT_TYPE_CODE_SCALE == (int)INF_NPC_TYPE_CODE_SCALE, + "entity encoder NPC type decoder must track the inferno observation scale"); _Static_assert(INF_ENT_INV_START == INF_OBS_AFTER_SPARKS, "entity encoder inventory block offset must track the inferno observation layout"); _Static_assert(INF_ENT_INV_NUM_CELLS == OSRS_INVENTORY_SIZE, "entity encoder inventory cell count must track the inferno observation layout"); -_Static_assert(INF_ENT_INV_FEATS == OSRS_INVENTORY_CELL_OBS_FEATURES, - "entity encoder inventory cell width must track the inferno observation layout"); +_Static_assert( + INF_ENT_INV_NUM_CELLS * INF_ENT_INV_OBS_FEATS == INF_INVENTORY_OBS_SIZE, + "entity encoder inventory observation width must track the inferno layout"); +_Static_assert(INF_ENT_INV_FEATS == OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT, + "encoder inventory record must track the generated item table width"); static void visual_require_gui_item_sprite(int raw_osrs_id, void* ctx) { gui_require_sprite_by_osrs_id((GuiState*)ctx, raw_osrs_id); diff --git a/ocean/osrs/osrs_visual_net.h b/ocean/osrs/osrs_visual_net.h index ee863105e5..584333e068 100644 --- a/ocean/osrs/osrs_visual_net.h +++ b/ocean/osrs/osrs_visual_net.h @@ -1,6 +1,6 @@ #pragma once -#include "osrs_colosseum_item_obs_generated.h" +#include "osrs_item_obs_generated.h" /* 5c puffercpu.h dropped _gelu. Keep a local twin so the viewer encoder stays bit-compatible with the CUDA GELU without carrying a core patch. */ @@ -21,34 +21,38 @@ static inline void osrs_visual_gelu(float* input, float* output, int size) { #define COLO_ENT_INF_INV_START 36 #define COLO_ENT_INF_INV_NUM_CELLS 28 #define COLO_ENT_INF_INV_FEATS 15 -/* obs carries an item CODE plus is_equipped and hp_heal; the encoder rebuilds the rest */ +/* obs carries an item code plus equipped and HP-heal; gear keeps the table's union slot */ #define COLO_ENT_INF_INV_OBS_FEATS 3 #define COLO_ENT_INF_INV_BOTTLENECK 16 -/* Host twin of COLO_ITEM_OBS_TABLE_DEV in src/ocean.cu. Separate names on purpose: both - land in one translation unit when the viewer is built, and a single guarded definition - would give the CUDA kernel a host array. */ -static const float COLO_ITEM_OBS_TABLE - [COLO_ITEM_OBS_TABLE_ROWS][COLO_ITEM_OBS_TABLE_COLS] = { -#include "osrs_colosseum_item_obs_table.inc" +/* Host twin of OSRS_ITEM_OBS_TABLE_DEV in src/ocean.cu. */ +static const float OSRS_ITEM_OBS_TABLE + [OSRS_ITEM_OBS_TABLE_ROWS][OSRS_ITEM_OBS_TABLE_COLS] = { +#include "osrs_item_obs_table.inc" }; -#define INF_ENT_NPC_START 90 -#define INF_ENT_NUM_NPCS 37 -#define INF_ENT_FEATS 48 -#define INF_ENT_TYPE_ONEHOT 14 -#define INF_ENT_INV_START 2450 -#define INF_ENT_INV_NUM_CELLS 28 -#define INF_ENT_INV_FEATS 28 +#define INF_ENT_OBS_SIZE 498 +#define INF_ENT_NPC_START 54 +#define INF_ENT_NUM_NPCS 14 +#define INF_ENT_OBS_FEATS 13 +#define INF_ENT_FEATS 26 +#define INF_ENT_TYPE_ONEHOT 14 +#define INF_ENT_TYPE_CODE_SCALE 16 +#define INF_ENT_INV_START 460 +#define INF_ENT_INV_NUM_CELLS 28 +#define INF_ENT_INV_OBS_FEATS 1 +#define INF_ENT_INV_FEATS 15 -/* How the observation record becomes the encoder record. Every branch materialises the - expanded record first and then runs one dense dot, which is what the CUDA gathers do. */ typedef enum { - ENTITY_RECORD_VERBATIM = 0, - ENTITY_RECORD_TYPE_ONEHOT, + ENTITY_RECORD_TYPE_ONEHOT = 0, ENTITY_RECORD_ITEM_TABLE, } EntityRecordExpansion; +typedef enum { + ENTITY_ITEM_OVERLAYS_NONE = 0, + ENTITY_ITEM_OVERLAYS_EQUIPPED_HP_HEAL, +} EntityItemOverlays; + #define ENTITY_RECORD_MAX_FEATS 64 typedef struct EntityPoolBranch EntityPoolBranch; @@ -58,14 +62,17 @@ struct EntityPoolBranch { int feats; int obs_feats; int type_onehot; + int code_scale; int bottleneck; - int mask_prefix; + int active_width; EntityRecordExpansion expansion; + EntityItemOverlays item_overlays; float* l1_w; float* l2_w; float* z1; float* h1; float* e; + unsigned char* active; }; typedef struct EntityEncoder EntityEncoder; @@ -82,10 +89,26 @@ struct EntityEncoder { static void entity_pool_branch_init( EntityPoolBranch* branch, Weights* weights, int hidden_dim, int start, int num_recs, int feats, int obs_feats, int type_onehot, - int bottleneck, int mask_prefix, EntityRecordExpansion expansion) { - if (feats > ENTITY_RECORD_MAX_FEATS) { - fprintf(stderr, "entity pool branch: %d features exceeds the expansion scratch\n", - feats); + int code_scale, int bottleneck, int active_width, + EntityRecordExpansion expansion, EntityItemOverlays item_overlays) { + if (feats > ENTITY_RECORD_MAX_FEATS || start < 0 || num_recs <= 0 || + obs_feats <= 0 || code_scale <= 0 || bottleneck <= 0 || + active_width <= 0 || active_width > feats) { + fprintf(stderr, "entity pool branch: invalid shape or encoding contract\n"); + abort(); + } + if (expansion == ENTITY_RECORD_TYPE_ONEHOT && + (type_onehot <= 0 || feats != type_onehot + obs_feats - 1 || + item_overlays != ENTITY_ITEM_OVERLAYS_NONE)) { + fprintf(stderr, "entity pool branch: stale type-code expansion contract\n"); + abort(); + } + if (expansion == ENTITY_RECORD_ITEM_TABLE && + (type_onehot != 0 || feats != OSRS_ITEM_OBS_TABLE_COLS || + (item_overlays == ENTITY_ITEM_OVERLAYS_NONE && obs_feats != 1) || + (item_overlays == ENTITY_ITEM_OVERLAYS_EQUIPPED_HP_HEAL && + obs_feats != OSRS_INVENTORY_CELL_OBS_FEATURES_CODED))) { + fprintf(stderr, "entity pool branch: stale item-table expansion contract\n"); abort(); } branch->start = start; @@ -93,14 +116,17 @@ static void entity_pool_branch_init( branch->feats = feats; branch->obs_feats = obs_feats; branch->type_onehot = type_onehot; + branch->code_scale = code_scale; branch->bottleneck = bottleneck; - branch->mask_prefix = mask_prefix; + branch->active_width = active_width; branch->expansion = expansion; + branch->item_overlays = item_overlays; branch->l1_w = get_weights_aligned(weights, bottleneck * feats); branch->l2_w = get_weights_aligned(weights, hidden_dim * bottleneck); branch->z1 = (float*)calloc((size_t)num_recs * bottleneck, sizeof(float)); branch->h1 = (float*)calloc((size_t)num_recs * bottleneck, sizeof(float)); branch->e = (float*)calloc((size_t)num_recs * hidden_dim, sizeof(float)); + branch->active = (unsigned char*)calloc((size_t)num_recs, sizeof(unsigned char)); } /* Weight reads are sequenced statements in reg_params order (src/ocean.cu): @@ -125,14 +151,15 @@ EntityEncoder* make_colosseum_entity_encoder( entity_pool_branch_init(&layer->branches[0], weights, hidden_dim, COLO_ENT_INF_NPC_START, COLO_ENT_INF_NUM_NPCS, COLO_ENT_INF_FEATS, COLO_ENT_INF_OBS_FEATS, COLO_ENT_INF_TYPE_ONEHOT, - COLO_ENT_INF_BOTTLENECK, COLO_ENT_INF_TYPE_ONEHOT, - ENTITY_RECORD_TYPE_ONEHOT); + 1, COLO_ENT_INF_BOTTLENECK, COLO_ENT_INF_TYPE_ONEHOT, + ENTITY_RECORD_TYPE_ONEHOT, ENTITY_ITEM_OVERLAYS_NONE); layer->num_branches = 1; if (mode >= 2) { entity_pool_branch_init(&layer->branches[1], weights, hidden_dim, COLO_ENT_INF_INV_START, COLO_ENT_INF_INV_NUM_CELLS, COLO_ENT_INF_INV_FEATS, COLO_ENT_INF_INV_OBS_FEATS, 0, - COLO_ENT_INF_INV_BOTTLENECK, 1, ENTITY_RECORD_ITEM_TABLE); + OSRS_ITEM_OBS_CODE_SCALE, COLO_ENT_INF_INV_BOTTLENECK, 1, + ENTITY_RECORD_ITEM_TABLE, ENTITY_ITEM_OVERLAYS_EQUIPPED_HP_HEAL); layer->num_branches = 2; } return layer; @@ -140,31 +167,38 @@ EntityEncoder* make_colosseum_entity_encoder( EntityEncoder* make_inferno_entity_encoder( Weights* weights, int batch_size, int input_dim, int hidden_dim, int mode) { + if (input_dim != INF_ENT_OBS_SIZE) { + fprintf(stderr, "inferno entity encoder: input width %d != %d\n", + input_dim, INF_ENT_OBS_SIZE); + abort(); + } EntityEncoder* layer = make_entity_encoder_global( weights, batch_size, input_dim, hidden_dim); entity_pool_branch_init(&layer->branches[0], weights, hidden_dim, INF_ENT_NPC_START, INF_ENT_NUM_NPCS, - INF_ENT_FEATS, INF_ENT_FEATS, 0, - COLO_ENT_INF_BOTTLENECK, INF_ENT_TYPE_ONEHOT, ENTITY_RECORD_VERBATIM); + INF_ENT_FEATS, INF_ENT_OBS_FEATS, INF_ENT_TYPE_ONEHOT, + INF_ENT_TYPE_CODE_SCALE, COLO_ENT_INF_BOTTLENECK, INF_ENT_TYPE_ONEHOT, + ENTITY_RECORD_TYPE_ONEHOT, ENTITY_ITEM_OVERLAYS_NONE); layer->num_branches = 1; if (mode >= 2) { entity_pool_branch_init(&layer->branches[1], weights, hidden_dim, INF_ENT_INV_START, INF_ENT_INV_NUM_CELLS, - INF_ENT_INV_FEATS, INF_ENT_INV_FEATS, 0, - COLO_ENT_INF_INV_BOTTLENECK, 1, ENTITY_RECORD_VERBATIM); + INF_ENT_INV_FEATS, INF_ENT_INV_OBS_FEATS, 0, + OSRS_ITEM_OBS_CODE_SCALE, COLO_ENT_INF_INV_BOTTLENECK, 1, + ENTITY_RECORD_ITEM_TABLE, ENTITY_ITEM_OVERLAYS_NONE); layer->num_branches = 2; } return layer; } -/* Mirrors colo_ent_gather_npcs and colo_ent_gather_inv in src/ocean.cu. */ static void entity_expand_record(const EntityPoolBranch* p, const float* rec, float* out) { switch (p->expansion) { - case ENTITY_RECORD_VERBATIM: - for (int i = 0; i < p->feats; i++) out[i] = rec[i]; - return; case ENTITY_RECORD_TYPE_ONEHOT: { - int code = (int)lrintf(rec[0]); + int code = (int)lrintf(rec[0] * (float)p->code_scale); + if (code < 0 || code > p->type_onehot) { + fprintf(stderr, "entity pool branch: type code %d out of range\n", code); + abort(); + } for (int i = 0; i < p->type_onehot; i++) out[i] = (code == i + 1) ? 1.0f : 0.0f; for (int i = 0; i < p->feats - p->type_onehot; i++) @@ -172,17 +206,24 @@ static void entity_expand_record(const EntityPoolBranch* p, const float* rec, fl return; } case ENTITY_RECORD_ITEM_TABLE: { - int code = (int)lrintf(rec[0] * (float)COLO_ITEM_OBS_CODE_SCALE); - if (code < 0 || code >= COLO_ITEM_OBS_TABLE_ROWS) { + int code = (int)lrintf(rec[0] * (float)p->code_scale); + if (code < 0 || code >= OSRS_ITEM_OBS_TABLE_ROWS) { fprintf(stderr, "entity pool branch: item code %d out of table\n", code); abort(); } - for (int i = 0; i < p->feats; i++) out[i] = COLO_ITEM_OBS_TABLE[code][i]; - out[COLO_ITEM_OBS_OVERLAY_EQUIPPED] += rec[1]; - out[COLO_ITEM_OBS_OVERLAY_HP_HEAL] += rec[2]; + for (int i = 0; i < p->feats; i++) out[i] = OSRS_ITEM_OBS_TABLE[code][i]; + if (p->item_overlays == ENTITY_ITEM_OVERLAYS_EQUIPPED_HP_HEAL) { + int is_gear = + OSRS_ITEM_OBS_TABLE[code][OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] != 0.0f || + OSRS_ITEM_OBS_TABLE[code][OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] != 0.0f; + out[OSRS_ITEM_OBS_OVERLAY_EQUIPPED] = rec[1]; + if (!is_gear) out[OSRS_ITEM_OBS_OVERLAY_HP_HEAL] = rec[2]; + } return; } } + fprintf(stderr, "entity pool branch: unknown record expansion\n"); + abort(); } void entity_encoder_forward(EntityEncoder* layer, float* observations) { @@ -206,6 +247,9 @@ void entity_encoder_forward(EntityEncoder* layer, float* observations) { float* rec = recs + n * p->obs_feats; float* z1n = p->z1 + n * p->bottleneck; entity_expand_record(p, rec, expanded); + float active_sum = 0.0f; + for (int i = 0; i < p->active_width; i++) active_sum += expanded[i]; + p->active[n] = active_sum > 0.0f; for (int k = 0; k < p->bottleneck; k++) { const float* w = p->l1_w + k * p->feats; float sum = 0.0f; @@ -228,14 +272,7 @@ void entity_encoder_forward(EntityEncoder* layer, float* observations) { float best = -INFINITY; int best_n = -1; for (int n = 0; n < p->num_recs; n++) { - float* rec = recs + n * p->obs_feats; - /* A coded record is live when its code is nonzero; a verbatim one - when its presence prefix is. Both mirror the fused pool's mask. */ - float mask_sum = 0.0f; - if (p->expansion != ENTITY_RECORD_VERBATIM) - mask_sum = rec[0] > 0.0f ? 1.0f : 0.0f; - else for (int t = 0; t < p->mask_prefix; t++) mask_sum += rec[t]; - if (mask_sum <= 0.0f) continue; + if (!p->active[n]) continue; float v = p->e[(size_t)n * H + o]; if (v > best) { best = v; best_n = n; } } @@ -250,6 +287,7 @@ void free_entity_encoder(EntityEncoder* layer) { free(layer->branches[br].z1); free(layer->branches[br].h1); free(layer->branches[br].e); + free(layer->branches[br].active); } free(layer); } diff --git a/ocean/osrs/tests/bench_inferno_forecast.c b/ocean/osrs/tests/bench_inferno_forecast.c index c507dac97a..435d43aa51 100644 --- a/ocean/osrs/tests/bench_inferno_forecast.c +++ b/ocean/osrs/tests/bench_inferno_forecast.c @@ -16,9 +16,6 @@ static double now_seconds(void) { static void init_bench_state(InfernoState* state, int player_x, int player_y) { inf_legacy_context()->config = inf_default_config(); - inf_legacy_context()->config.step_out_forecast_obs_enabled = 1; - inf_legacy_context()->config.step_out_forecast_obs_mode = - INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT; inf_build_npc_stats(); memset(state, 0, sizeof(*state)); memset(state->npc_los_cache, -1, sizeof(state->npc_los_cache)); @@ -83,20 +80,6 @@ static void init_dense_wave_state(InfernoState* state) { inf_rebuild_entity_collision_flags(state); } -static void init_pillar_stack_no_forecast_state(InfernoState* state) { - init_pillar_stack_state(state); - inf_legacy_context()->config.step_out_forecast_obs_enabled = 0; - inf_legacy_context()->config.step_out_forecast_obs_mode = - INF_STEP_OUT_FORECAST_MODE_OFF; -} - -static void init_dense_wave_no_forecast_state(InfernoState* state) { - init_dense_wave_state(state); - inf_legacy_context()->config.step_out_forecast_obs_enabled = 0; - inf_legacy_context()->config.step_out_forecast_obs_mode = - INF_STEP_OUT_FORECAST_MODE_OFF; -} - typedef void (*BenchInit)(InfernoState*); typedef void (*BenchFn)(InfernoState*, float*); typedef void (*FixedBenchFn)(const InfernoState*, float*); @@ -303,7 +286,6 @@ static void report_sampled_forecast_diff( int main(void) { printf("sizeof(InfernoState) = %zu\n", sizeof(InfernoState)); printf("INF_NUM_OBS = %d\n", INF_NUM_OBS); - printf("INF_STEP_OUT_FORECAST_OBS_SIZE = %d\n", INF_STEP_OUT_FORECAST_OBS_SIZE); run_bench("empty exact", init_empty_state, bench_forecast_exact, 200000); run_bench("empty static", init_empty_state, bench_forecast_fast_static, 200000); run_bench("empty readonly", init_empty_state, bench_forecast_fast_readonly, 200000); @@ -317,7 +299,6 @@ int main(void) { run_bench("stack static", init_pillar_stack_state, bench_forecast_fast_static, 100000); run_bench("stack readonly", init_pillar_stack_state, bench_forecast_fast_readonly, 100000); run_bench("stack obs", init_pillar_stack_state, bench_obs, 100000); - run_bench("stack obs no forecast", init_pillar_stack_no_forecast_state, bench_obs, 100000); run_bench("stack mask", init_pillar_stack_state, bench_mask, 100000); report_forecast_diff("stack exact vs static", init_pillar_stack_state, inf_build_step_out_forecast_fast_static_ctx); @@ -327,7 +308,6 @@ int main(void) { run_bench("dense static", init_dense_wave_state, bench_forecast_fast_static, 50000); run_bench("dense readonly", init_dense_wave_state, bench_forecast_fast_readonly, 50000); run_bench("dense obs", init_dense_wave_state, bench_obs, 50000); - run_bench("dense obs no forecast", init_dense_wave_no_forecast_state, bench_obs, 50000); run_bench("dense mask", init_dense_wave_state, bench_mask, 50000); report_forecast_diff("dense exact vs static", init_dense_wave_state, inf_build_step_out_forecast_fast_static_ctx); @@ -336,7 +316,6 @@ int main(void) { run_fixed_bench("dense copy fixed", init_dense_wave_state, bench_copy_fixed, 50000); run_fixed_bench("dense step fixed", init_dense_wave_state, bench_step_fixed, 50000); run_fixed_bench("dense step+obs+mask", init_dense_wave_state, bench_step_obs_mask_fixed, 50000); - run_fixed_bench("step+obs+mask no fc", init_dense_wave_no_forecast_state, bench_step_obs_mask_fixed, 50000); report_sampled_forecast_diff("dense sampled static", init_dense_wave_state, inf_build_step_out_forecast_fast_static_ctx, 256); report_sampled_forecast_diff("dense sampled readonly", diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index a3e4323f5a..0437b4b896 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -4,6 +4,7 @@ #include #include +#include "ocean/osrs/osrs_item_obs_generated.h" #include "ocean/osrs/encounters/encounter_colosseum.h" #define col_init_context_typed(ctx_ptr) do { \ @@ -289,16 +290,14 @@ static void test_prepare_for_drink_kind( } } -/* The committed table src/ocean.cu and the viewer both read. Kept here as a third, - independent copy so test_item_obs_table_is_current can diff it against a fresh build. */ static const float TEST_ITEM_OBS_TABLE - [COLO_ITEM_OBS_TABLE_ROWS][COLO_ITEM_OBS_TABLE_COLS] = { -#include "ocean/osrs/osrs_colosseum_item_obs_table.inc" + [OSRS_ITEM_OBS_TABLE_ROWS][OSRS_ITEM_OBS_TABLE_COLS] = { +#include "ocean/osrs/osrs_item_obs_table.inc" }; /* Exactly what colo_ent_gather_inv does: the observation names a table row and supplies the - two features the item does not fix. Every per-cell assertion below reads the result, so it - asserts against what the encoder actually sees rather than against the raw observation. */ + two dynamic features. Every per-cell assertion below reads the result, so it asserts + against what the encoder actually sees rather than against the raw observation. */ static void test_expand_inventory_cell( const float obs[COLO_NUM_OBS], int cell_idx, @@ -307,7 +306,7 @@ static void test_expand_inventory_cell( const float* coded = &obs[COLO_OBS_AFTER_PLAYER + cell_idx * COLO_INVENTORY_CELL_OBS_FEATURES]; int code = osrs_inventory_cell_obs_code_decode(coded[OSRS_INVENTORY_CELL_OBS_CODE]); - if (code < 0 || code >= COLO_ITEM_OBS_TABLE_ROWS) { + if (code < 0 || code >= OSRS_ITEM_OBS_TABLE_ROWS) { fprintf(stderr, "inventory cell %d observed item code %d\n", cell_idx, code); abort(); } @@ -6046,7 +6045,7 @@ static void test_combat_fidelity_contract_sizes(void) { COLO_INVENTORY_OBS_SIZE == 84); CHECK("the encoder still sees the 15-feature record, rebuilt from the item table", COLO_INVENTORY_CELL_ENCODER_FEATURES == 15 && - COLO_ITEM_OBS_TABLE_COLS == 15); + OSRS_ITEM_OBS_TABLE_COLS == 15); /* spec_cost is 0 for everything without a special, so it doubles as the has-spec flag * while also telling the agent whether it can afford one. */ CHECK("spec cost is exposed and normalised for a spec weapon", @@ -8306,28 +8305,96 @@ static void test_item_obs_code_survives_bf16(void) { osrs_inventory_cell_obs_code_encode(OSRS_INVENTORY_CELL_OBS_CODE_COUNT - 1) <= 1.0f); } -/* The table is generated and committed, so a checkout can hold a stale one and train on - silently wrong observations. Rebuild every row and diff it, float for float. */ -static void test_item_obs_table_is_current(void) { - printf("test_item_obs_table_is_current\n"); - for (int code = 0; code < COLO_ITEM_OBS_TABLE_ROWS; code++) { - float row[COLO_ITEM_OBS_TABLE_COLS]; - osrs_write_inventory_cell_obs_table_row( - row, code, MAXED_BASE_HITPOINTS, - COLO_ITEM_OBS_TABLE_BASE_PRAYER, COLO_ITEM_OBS_TABLE_BASE_LEVEL); - for (int f = 0; f < COLO_ITEM_OBS_TABLE_COLS; f++) { +static void test_item_obs_table_matches_every_code_semantically(void) { + printf("test_item_obs_table_matches_every_code_semantically\n"); + CHECK("generic item table has one row for every inventory item code", + OSRS_ITEM_OBS_TABLE_ROWS == OSRS_INVENTORY_CELL_OBS_CODE_COUNT); + CHECK("generic item rows contain the full 15-float compact record", + OSRS_ITEM_OBS_TABLE_COLS == 15); + CHECK("generic item rows bake hitpoints at 99", + OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS == 99); + CHECK("generic item rows bake prayer at 99", + OSRS_ITEM_OBS_TABLE_BASE_PRAYER == 99); + CHECK("generic item rows bake ranged at 99", + OSRS_ITEM_OBS_TABLE_BASE_RANGED == 99); + CHECK("generic item code scale matches the inventory observation encoding", + OSRS_ITEM_OBS_CODE_SCALE == OSRS_INVENTORY_CELL_OBS_CODE_SCALE); + CHECK("generic equipped overlay targets the compact equipped feature", + OSRS_ITEM_OBS_OVERLAY_EQUIPPED == OSRS_INVENTORY_CELL_COMPACT_EQUIPPED); + CHECK("generic HP-heal overlay targets the compact HP-heal feature", + OSRS_ITEM_OBS_OVERLAY_HP_HEAL == OSRS_INVENTORY_CELL_COMPACT_HP_HEAL); + + for (int code = 0; code < OSRS_ITEM_OBS_TABLE_ROWS; code++) { + OsrsInventoryCell cell = osrs_inventory_cell_for_obs_code(code); + float expected[OSRS_ITEM_OBS_TABLE_COLS]; + osrs_write_inventory_cell_affordance_features_compact( + expected, cell.item_idx, cell.raw_osrs_id, cell.dose, 0, + OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS, + OSRS_ITEM_OBS_TABLE_BASE_PRAYER, + OSRS_ITEM_OBS_TABLE_BASE_RANGED); + for (int feature = 0; feature < OSRS_ITEM_OBS_TABLE_COLS; feature++) { char label[160]; snprintf(label, sizeof(label), - "item table row %d feature %d matches a fresh build " - "(rerun ocean/osrs/tools/gen_colosseum_item_obs_table.c)", code, f); - CHECK(label, row[f] == TEST_ITEM_OBS_TABLE[code][f]); + "generic item table code %d feature %d matches base-99 semantics", + code, feature); + CHECK(label, TEST_ITEM_OBS_TABLE[code][feature] == expected[feature]); } } } -/* The recut is only legitimate if the encoder still sees the record it saw before. Drive a - live episode and compare the expanded record against the writer the observation used to - call, cell by cell and tick by tick, across a Frailty draft so base_hitpoints moves. */ +static void test_item_obs_empty_row_is_all_zero(void) { + printf("test_item_obs_empty_row_is_all_zero\n"); + for (int feature = 0; feature < OSRS_ITEM_OBS_TABLE_COLS; feature++) { + char label[128]; + snprintf(label, sizeof(label), + "empty item code feature %d is zero", feature); + CHECK(label, + TEST_ITEM_OBS_TABLE[OSRS_INVENTORY_CELL_OBS_CODE_EMPTY][feature] == 0.0f); + } +} + +static void test_item_obs_table_bakes_consumable_hp_heal(void) { + printf("test_item_obs_table_bakes_consumable_hp_heal\n"); + int shark_code = osrs_inventory_cell_obs_code(ITEM_NONE, 385); + OsrsInventoryCell shark = osrs_inventory_cell_for_obs_code(shark_code); + float expected[OSRS_ITEM_OBS_TABLE_COLS]; + osrs_write_inventory_cell_affordance_features_compact( + expected, shark.item_idx, shark.raw_osrs_id, shark.dose, 0, + OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS, + OSRS_ITEM_OBS_TABLE_BASE_PRAYER, + OSRS_ITEM_OBS_TABLE_BASE_RANGED); + + CHECK("base-99 shark semantics contain a positive HP heal", + expected[OSRS_ITEM_OBS_OVERLAY_HP_HEAL] > 0.0f); + CHECK("generated shark row bakes its base-99 HP heal", + TEST_ITEM_OBS_TABLE[shark_code][OSRS_ITEM_OBS_OVERLAY_HP_HEAL] == + expected[OSRS_ITEM_OBS_OVERLAY_HP_HEAL]); +} + +static void test_colosseum_item_obs_overlay_overwrites_dynamic_features(void) { + printf("test_colosseum_item_obs_overlay_overwrites_dynamic_features\n"); + float table_row[OSRS_ITEM_OBS_TABLE_COLS] = {0}; + float coded[OSRS_INVENTORY_CELL_OBS_FEATURES_CODED] = {0}; + float expanded[OSRS_ITEM_OBS_TABLE_COLS]; + + table_row[OSRS_ITEM_OBS_OVERLAY_EQUIPPED] = 0.25f; + table_row[OSRS_ITEM_OBS_OVERLAY_HP_HEAL] = 0.50f; + coded[OSRS_INVENTORY_CELL_OBS_EQUIPPED] = 1.0f; + coded[OSRS_INVENTORY_CELL_OBS_HP_HEAL] = 0.20f; + + osrs_expand_inventory_cell_obs_code_features(expanded, coded, table_row); + + CHECK("Colosseum equipped overlay overwrites the generated slot", + expanded[OSRS_ITEM_OBS_OVERLAY_EQUIPPED] == + coded[OSRS_INVENTORY_CELL_OBS_EQUIPPED]); + CHECK("Colosseum HP-heal overlay overwrites the baked base-99 value", + expanded[OSRS_ITEM_OBS_OVERLAY_HP_HEAL] == + coded[OSRS_INVENTORY_CELL_OBS_HP_HEAL]); +} + +/* Drive a live episode and compare the expanded record against the compact semantic writer, + cell by cell and tick by tick, across a Frailty draft so dynamic HP-heal moves away from the + generated base-99 value. */ static void test_inventory_obs_expansion_is_lossless(void) { printf("test_inventory_obs_expansion_is_lossless\n"); ColosseumContext ctx; @@ -8367,9 +8434,9 @@ static void test_inventory_obs_expansion_is_lossless(void) { } } - CHECK("expanded record equals the pre-recut compact record on every cell and tier", + CHECK("Colosseum overlay preserves exact compact semantics on every cell and Frailty tier", mismatches == 0); - CHECK("the sweep actually exercised hp_heal, the one feature the table cannot hold", + CHECK("the Frailty sweep exercised a dynamic HP-heal overwrite", hp_heal_seen > 0); } @@ -8658,7 +8725,10 @@ int main(void) { test_best_gear_cache_signatures(); test_farm_safe_damage_cap(); test_item_obs_code_survives_bf16(); - test_item_obs_table_is_current(); + test_item_obs_table_matches_every_code_semantically(); + test_item_obs_empty_row_is_all_zero(); + test_item_obs_table_bakes_consumable_hp_heal(); + test_colosseum_item_obs_overlay_overwrites_dynamic_features(); test_inventory_obs_expansion_is_lossless(); test_inventory_obs_memo(); diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index 552c2f0bda..f64bbb68bb 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -384,39 +384,16 @@ static void fire_player_action_at_slot_zero( static int inferno_pending_hit_obs_start(void); static int inferno_pillar_obs_start(int pillar_idx); static int inferno_obs_slot_dig_index(int slot_idx); +static int inferno_obs_slot_start(int slot_idx); static void init_zuk_timing_state(InfernoState* state); enum { - INF_OBS_WAVE_NORM = 14, - INF_OBS_WAVE_PHASE_START = 15, - INF_OBS_ZUK_ATTACK_TIMER = 21, - INF_OBS_PRAYER_TIMER = 42, - INF_OBS_PRAYER_MELEE = 43, - INF_OBS_PRAYER_RANGED = 44, - INF_OBS_PRAYER_MAGIC = 45, - INF_OBS_PRAYER_CONFLICT = 46, - INF_OBS_PRESSURE_START = 47, - INF_OBS_PRESSURE_IF_READY_TOTAL = 47, - INF_OBS_PRESSURE_IF_READY_MELEE = 48, - INF_OBS_PRESSURE_IF_READY_RANGED = 49, - INF_OBS_PRESSURE_IF_READY_MAGIC = 50, - INF_OBS_PRESSURE_IF_READY_MAX_HIT = 51, - INF_OBS_PRESSURE_IF_READY_SUM_MAX_HIT = 52, - INF_OBS_PRESSURE_THIS_TICK_TOTAL = 53, - INF_OBS_PRESSURE_THIS_TICK_MELEE = 54, - INF_OBS_PRESSURE_THIS_TICK_RANGED = 55, - INF_OBS_PRESSURE_THIS_TICK_MAGIC = 56, - INF_OBS_PRESSURE_THIS_TICK_MAX_HIT = 57, - INF_OBS_PRESSURE_THIS_TICK_SUM_MAX_HIT = 58, - INF_OBS_PRESSURE_TIMER_LTE_1 = 59, - INF_OBS_PRESSURE_TIMER_LTE_2 = 60, - INF_OBS_PRESSURE_TIMER_LTE_4 = 61, - INF_OBS_PRESSURE_IF_READY_STYLE_COUNT = 62, - INF_OBS_PRESSURE_THIS_TICK_STYLE_COUNT = 63, - INF_OBS_PRESSURE_NEAREST_TIMER = 64, - INF_OBS_ZUK_PHASE_START = 65, - INF_OBS_ZUK_SHIELD_DIR = 66, - INF_OBS_ZUK_SHIELD_FREEZE = 67, + INF_OBS_WAVE_NORM = 10, + INF_OBS_WAVE_PHASE = 11, + INF_OBS_ZUK_ATTACK_TIMER = 12, + INF_OBS_ZUK_PHASE_START = 36, + INF_OBS_ZUK_SHIELD_DIR = 36, + INF_OBS_ZUK_SHIELD_FREEZE = 37, }; static void init_jad_timing_test_state(InfernoState* state, int player_x, int player_y, int jad_x, int jad_y) { @@ -1101,7 +1078,7 @@ static void test_zuk_untagged_healer_target_bonus_defaults_off(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = 34; + actions[INF_HEAD_TARGET] = inf_action_target_for_npc(&state, 2); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("default target bonus pays no per-tick target reward", @@ -1135,14 +1112,20 @@ static void test_zuk_untagged_healer_target_bonus_rewards_distinct_healers(void) float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); + int first_healer_slot = inf_find_target_obs_slot(&state, 2); + int second_healer_slot = inf_find_target_obs_slot(&state, 3); + ASSERT_INT_EQ("first healer has an observation slot", + first_healer_slot >= 0, 1); + ASSERT_INT_EQ("second healer has an observation slot", + second_healer_slot >= 0, 1); ASSERT_INT_EQ("first healer slot maps to npc 2", - state.current_obs_slots[33], 2); + state.current_obs_slots[first_healer_slot], 2); ASSERT_INT_EQ("second healer slot maps to npc 3", - state.current_obs_slots[34], 3); + state.current_obs_slots[second_healer_slot], 3); int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = 34; + actions[INF_HEAD_TARGET] = first_healer_slot + 1; inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("first untagged healer target rewarded", @@ -1153,13 +1136,13 @@ static void test_zuk_untagged_healer_target_bonus_rewards_distinct_healers(void) inf_compute_reward(&state), 0.07f, 0.0001f); state.tick_scratch.zuk_untagged_healer_targets = 0; - actions[INF_HEAD_TARGET] = 34; + actions[INF_HEAD_TARGET] = first_healer_slot + 1; inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("repeat target does not reward twice", state.tick_scratch.zuk_untagged_healer_targets, 0); - actions[INF_HEAD_TARGET] = 35; + actions[INF_HEAD_TARGET] = second_healer_slot + 1; inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("second distinct untagged healer target rewarded", @@ -1189,7 +1172,7 @@ static void test_zuk_safe_untagged_healer_target_bonus_records_safe_subset(void) int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = 34; + actions[INF_HEAD_TARGET] = inf_action_target_for_npc(&state, 2); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("safe untagged healer target attempt", @@ -1226,7 +1209,7 @@ static void test_zuk_untagged_healer_target_bonus_excludes_tagged_healers(void) int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = 34; + actions[INF_HEAD_TARGET] = inf_action_target_for_npc(&state, 2); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("already tagged healer target gets no bonus", @@ -2014,7 +1997,6 @@ static void test_inferno_reset_preserves_reward_config(void) { inf_put_int(raw_state, "zuk_healer_reward_mode", 1); inf_put_int(raw_state, "joseph_reward_mode", 1); inf_put_int(raw_state, "terminal_penalty_enabled", 1); - inf_put_int(raw_state, "step_out_forecast_obs_enabled", 0); inf_put_int(raw_state, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); inf_put_float(raw_state, "budget_loadout_fraction", 1.0f); inf_reset(raw_state, 123u); @@ -2044,10 +2026,6 @@ static void test_inferno_reset_preserves_reward_config(void) { ASSERT_INT_EQ("Zuk healer reward mode", test_config()->zuk_healer_reward_mode, 1); ASSERT_INT_EQ("Joseph reward mode", test_config()->joseph_reward_mode, 1); ASSERT_INT_EQ("terminal penalty enabled", test_config()->terminal_penalty_enabled, 1); - ASSERT_INT_EQ("step-out forecast obs disabled", - test_config()->step_out_forecast_obs_enabled, 0); - ASSERT_INT_EQ("step-out forecast obs mode disabled", - test_config()->step_out_forecast_obs_mode, INF_STEP_OUT_FORECAST_MODE_OFF); ASSERT_INT_EQ("loadout profile mode preserved", test_config()->loadout_profile_mode, INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); ASSERT_FLOAT_NEAR("budget loadout fraction preserved", @@ -2277,10 +2255,10 @@ static void test_late_start_supply_observations(void) { inf_write_obs(raw_state, obs); enum { - INF_OBS_BREW_DOSES = 11, - INF_OBS_RESTORE_DOSES = 12, - INF_OBS_BASTION_DOSES = 25, - INF_OBS_STAMINA_DOSES = 26, + INF_OBS_BREW_DOSES = 5, + INF_OBS_RESTORE_DOSES = 6, + INF_OBS_BASTION_DOSES = 8, + INF_OBS_STAMINA_DOSES = 9, }; ASSERT_FLOAT_NEAR("brew obs uses full-kit denominator", obs[INF_OBS_BREW_DOSES], @@ -3236,11 +3214,6 @@ static void test_jad_has_no_pre_fire_style_preview(void) { ASSERT_INT_EQ("jad timer decrements without preview", state.npcs[0].attack_timer, 1); ASSERT_INT_EQ("jad style stays hidden before fire", inf_npc_jad(&state.npcs[0])->attack_style, ATTACK_STYLE_NONE); - float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); - ASSERT_FLOAT_NEAR("prayer-critical timer ignores hidden jad style", obs[INF_OBS_PRAYER_TIMER], 1.0f, 1e-6f); - ASSERT_INT_EQ("prayer-critical style stays zero before fire", - (int)(obs[INF_OBS_PRAYER_MELEE] + obs[INF_OBS_PRAYER_RANGED] + obs[INF_OBS_PRAYER_MAGIC]), 0); } static void test_jad_fire_tick_exposes_three_tick_prayer_deadline(void) { @@ -3259,11 +3232,13 @@ static void test_jad_fire_tick_exposes_three_tick_prayer_deadline(void) { float obs[INF_NUM_OBS]; memset(obs, 0, sizeof(obs)); inf_write_obs((EncounterState*)&state, obs); - ASSERT_FLOAT_NEAR("prayer-critical timer exposes jad fire deadline", obs[INF_OBS_PRAYER_TIMER], 0.3f, 1e-6f); - ASSERT_FLOAT_NEAR("prayer-critical magic style exposed after fire", obs[INF_OBS_PRAYER_MAGIC], 1.0f, 1e-6f); int pending_start = inferno_pending_hit_obs_start(); - ASSERT_FLOAT_NEAR("pending hit obs timer uses prayer window", obs[pending_start + 3], 0.3f, 1e-6f); - ASSERT_FLOAT_NEAR("pending hit pre-check damage exposes max threat", obs[pending_start + 4], 113.0f / 150.0f, 1e-6f); + ASSERT_FLOAT_NEAR("pending hit obs style is magic", + obs[pending_start], (float)ATTACK_STYLE_MAGIC / 4.0f, 1e-6f); + ASSERT_FLOAT_NEAR("pending hit obs timer uses prayer window", + obs[pending_start + 1], 0.3f, 1e-6f); + ASSERT_FLOAT_NEAR("pending hit pre-check damage exposes max threat", + obs[pending_start + 2], 113.0f / 150.0f, 1e-6f); } static void test_jad_prayer_on_third_tick_blocks(void) { @@ -3365,11 +3340,11 @@ static void test_triple_jad_pending_threats_fit_obs_layout(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); - ASSERT_INT_EQ("inferno obs shape includes exact spark slots", INF_NUM_OBS, 3432); + ASSERT_INT_EQ("inferno obs shape uses compact layout", INF_NUM_OBS, 498); } -static void test_inferno_obs_shape_includes_step_out_forecast_features(void) { - printf("--- inferno obs shape includes step-out forecast features ---\n"); +static void test_inferno_action_and_compact_obs_shape(void) { + printf("--- inferno action and compact obs shape ---\n"); ASSERT_INT_EQ("equip heads span every gear slot", INF_HEAD_EAT - INF_HEAD_EQUIP_BASE, NUM_GEAR_SLOTS); @@ -3383,28 +3358,23 @@ static void test_inferno_obs_shape_includes_step_out_forecast_features(void) { ASSERT_INT_EQ("prayer action head includes redemption", INF_ACTION_DIMS[INF_HEAD_PRAYER], ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION); ASSERT_INT_EQ("action mask spans all inventory click heads", - INF_ACTION_MASK_SIZE, 457); + INF_ACTION_MASK_SIZE, 434); ASSERT_SOURCE_BLOCK_CONTAINS("native binding reuses inferno action dims", "ocean/osrs_inferno/osrs_inferno.h", "#define OBS_SIZE INF_NUM_OBS", "typedef float obs_t;", "#define ACT_SIZES INF_ACTION_DIMS_INIT"); - ASSERT_INT_EQ("player obs includes NPC pressure summary", - INF_PLAYER_OBS_SIZE, 75); - ASSERT_INT_EQ("pillar obs includes footprint size", - INF_PILLAR_OBS_SIZE, 15); - ASSERT_INT_EQ("npc obs includes loadout reachability signals", - INF_TOTAL_NPC_OBS_SIZE, 1776); - ASSERT_INT_EQ("step-out forecast covers every movement action", - INF_STEP_OUT_FORECAST_OBS_SIZE, 200); - ASSERT_INT_EQ("inferno obs shape includes exact spark landings", - INF_PENDING_SPARK_OBS_SIZE, 224); - ASSERT_INT_EQ("inventory obs covers every cell", - INF_INVENTORY_OBS_SIZE, OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES); - ASSERT_INT_EQ("equipped obs covers every gear slot", - INF_EQUIPPED_OBS_SIZE, NUM_GEAR_SLOTS * OSRS_EQUIPPED_SELF_OBS_FEATURES); - ASSERT_INT_EQ("inferno obs shape includes inventory cells", - INF_NUM_OBS, 3432); + ASSERT_INT_EQ("compact player observation width", INF_PLAYER_OBS_SIZE, 45); + ASSERT_INT_EQ("compact pillar observation width", INF_PILLAR_OBS_SIZE, 9); + ASSERT_INT_EQ("compact NPC observation width", + INF_TOTAL_NPC_OBS_SIZE, INF_OBS_NPCS * INF_NPC_SLOT_FEATURES); + ASSERT_INT_EQ("compact spark observation width", + INF_PENDING_SPARK_OBS_SIZE, 128); + ASSERT_INT_EQ("inventory observation uses one code per cell", + INF_INVENTORY_OBS_SIZE, OSRS_INVENTORY_SIZE); + ASSERT_INT_EQ("equipment observation uses shared aggregate", + INF_EQUIPPED_OBS_SIZE, OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES); + ASSERT_INT_EQ("inferno observation width", INF_NUM_OBS, 498); ASSERT_INFERNO_SOURCE_NOT_CONTAINS("armor_tank state is removed", "armor_tank"); ASSERT_INFERNO_SOURCE_NOT_CONTAINS("extra npc obs scaffold is removed", @@ -3419,8 +3389,8 @@ static void test_inferno_obs_shape_includes_step_out_forecast_features(void) { "ocean/osrs_inferno/osrs_inferno.h", "player_pending_hit_count"); } -static void test_inferno_obs_wave_phase_one_hot(void) { - printf("--- inferno obs wave phase one hot ---\n"); +static void test_inferno_obs_wave_phase_code(void) { + printf("--- inferno obs wave phase code ---\n"); int waves[6] = {1, 18, 35, 50, 67, 69}; for (int phase = 0; phase < 6; phase++) { @@ -3433,12 +3403,8 @@ static void test_inferno_obs_wave_phase_one_hot(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); - - for (int i = 0; i < 6; i++) { - float expected = (i == phase) ? 1.0f : 0.0f; - ASSERT_FLOAT_NEAR("wave phase one hot", obs[INF_OBS_WAVE_PHASE_START + i], - expected, 1e-6f); - } + ASSERT_FLOAT_NEAR("wave phase compact code", + obs[INF_OBS_WAVE_PHASE], (float)(phase + 1) / 8.0f, 1e-6f); } InfernoState triple_jad = make_test_state(20, 20); @@ -3450,11 +3416,11 @@ static void test_inferno_obs_wave_phase_one_hot(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&triple_jad, obs); ASSERT_FLOAT_NEAR("wave 68 stays in Jad phase", - obs[INF_OBS_WAVE_PHASE_START + 4], 1.0f, 1e-6f); + obs[INF_OBS_WAVE_PHASE], 5.0f / 8.0f, 1e-6f); } -static void test_inferno_obs_exposes_pillar_footprint_size(void) { - printf("--- inferno obs exposes pillar footprint size ---\n"); +static void test_inferno_obs_exposes_compact_pillars(void) { + printf("--- inferno obs exposes compact pillars ---\n"); InfernoState state = make_test_state(20, 20); state.player.current_hitpoints = 99; @@ -3478,24 +3444,22 @@ static void test_inferno_obs_exposes_pillar_footprint_size(void) { inf_write_obs((EncounterState*)&state, obs); int active_start = inferno_pillar_obs_start(0); - ASSERT_FLOAT_NEAR("active pillar active bit", - obs[active_start], 1.0f, 1e-6f); ASSERT_FLOAT_NEAR("active pillar hp", - obs[active_start + 1], 1.0f, 1e-6f); + obs[active_start], 1.0f, 1e-6f); ASSERT_FLOAT_NEAR("active pillar relative x", - obs[active_start + 2], - (float)(INF_PILLAR_POS[0][0] - state.player.x) / (float)INF_ARENA_WIDTH, + obs[active_start + 1], + (float)(INF_PILLAR_POS[0][0] - state.player.x) / + (float)INF_ARENA_WIDTH, 1e-6f); ASSERT_FLOAT_NEAR("active pillar relative y", - obs[active_start + 3], - (float)(INF_PILLAR_POS[0][1] - state.player.y) / (float)INF_ARENA_HEIGHT, + obs[active_start + 2], + (float)(INF_PILLAR_POS[0][1] - state.player.y) / + (float)INF_ARENA_HEIGHT, 1e-6f); - ASSERT_FLOAT_NEAR("active pillar footprint size", - obs[active_start + 4], (float)INF_PILLAR_SIZE / 7.0f, 1e-6f); int inactive_start = inferno_pillar_obs_start(1); - ASSERT_FLOAT_NEAR("inactive pillar footprint size", - obs[inactive_start + 4], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("inactive pillar hp", + obs[inactive_start], 0.0f, 1e-6f); } static void test_inferno_obs_exposes_meleer_dig_state(void) { @@ -3517,7 +3481,7 @@ static void test_inferno_obs_exposes_meleer_dig_state(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); - int meleer_slot = 4; + int meleer_slot = inf_find_target_obs_slot(&state, 0); int dig_start = inferno_obs_slot_dig_index(meleer_slot); ASSERT_INT_EQ("meleer occupies first meleer slot", state.current_obs_slots[meleer_slot], 0); @@ -3637,16 +3601,10 @@ static void test_jad_melee_stays_instant_and_untelegraphed(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&preview_state, obs); - ASSERT_FLOAT_NEAR( - "jad prayer-critical preview does not advertise melee fallback", - obs[INF_OBS_PRAYER_CONFLICT], 1.0f / 3.0f, 1e-6f); - ASSERT_INT_EQ( - "jad prayer-critical preview keeps ranged one-hot", - (int)(obs[INF_OBS_PRAYER_MELEE] + obs[INF_OBS_PRAYER_RANGED] + obs[INF_OBS_PRAYER_MAGIC]), - 1); - ASSERT_FLOAT_NEAR( - "jad preview keeps ranged as the visible style", - obs[INF_OBS_PRAYER_RANGED], 1.0f, 1e-6f); + int preview_slot = inf_find_target_obs_slot(&preview_state, 0); + ASSERT_FLOAT_NEAR("Jad dynamic style stays ranged in its dense record", + obs[inferno_obs_slot_start(preview_slot) + 5], + (float)ATTACK_STYLE_RANGED / 4.0f, 1e-6f); int saw_melee = 0; for (uint32_t seed = 0; seed < 256; seed++) { @@ -3684,7 +3642,7 @@ static int inferno_obs_slot_start(int slot_idx) { } static int inferno_pillar_obs_start(int pillar_idx) { - return INF_PLAYER_OBS_SIZE + pillar_idx * 5; + return INF_OBS_AFTER_PLAYER + pillar_idx * INF_PILLAR_FEATURES; } static int inferno_target_mask_slot_offset(int slot_idx) { @@ -3695,100 +3653,44 @@ static int inferno_target_mask_none_offset(void) { return ENCOUNTER_MOVE_ACTIONS + ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION; } -static int inferno_step_out_forecast_obs_start(void) { - return INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + INF_TOTAL_NPC_OBS_SIZE; -} static int inferno_pending_hit_obs_start(void) { - return inferno_step_out_forecast_obs_start() + INF_STEP_OUT_FORECAST_OBS_SIZE; + return INF_OBS_AFTER_NPCS; } static int inferno_spark_obs_start(void) { - return inferno_pending_hit_obs_start() + - INF_FEATURES_PER_HIT * ENCOUNTER_MAX_PENDING_HITS; + return INF_OBS_AFTER_PENDING_HITS; } static int inferno_obs_slot_hp_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 14; -} - -static int inferno_obs_slot_size_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 17; + return inferno_obs_slot_start(slot_idx) + 1; } static int inferno_obs_slot_npc_los_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 22; -} - -static int inferno_obs_slot_barrage_count_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 26; -} - -static int inferno_obs_slot_edge_distance_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 27; -} - -static int inferno_obs_slot_npc_can_attack_if_ready_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 28; -} - -static int inferno_obs_slot_npc_can_attack_this_tick_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 29; + return inferno_obs_slot_start(slot_idx) + 6; } static int inferno_obs_slot_frozen_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 30; -} - -static int inferno_obs_slot_player_can_attack_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 31; -} - -static int inferno_obs_slot_player_has_los_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 32; -} - -static int inferno_obs_slot_player_can_mage_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 33; -} - -static int inferno_obs_slot_player_can_long_range_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 34; -} - -static int inferno_obs_slot_player_can_blowpipe_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 35; -} - -static int inferno_obs_slot_mage_range_deficit_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 36; -} - -static int inferno_obs_slot_long_range_deficit_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 37; -} - -static int inferno_obs_slot_blowpipe_range_deficit_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 38; + return inferno_obs_slot_start(slot_idx) + 7; } static int inferno_obs_slot_target_category_start(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 39; + return inferno_obs_slot_start(slot_idx) + 8; } static int inferno_obs_slot_targeted_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 43; -} - -static int inferno_obs_slot_phantom_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 44; + return inferno_obs_slot_start(slot_idx) + 9; } static int inferno_obs_slot_dig_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 45; + return inferno_obs_slot_start(slot_idx) + 10; } -static void init_threat_obs_state(InfernoState* state, int player_x, int player_y) { +static void init_threat_obs_state( + InfernoState* state, + int player_x, + int player_y +) { inf_build_npc_stats(); *state = make_test_state(player_x, player_y); state->player.entity_type = ENTITY_PLAYER; @@ -3806,13 +3708,16 @@ static void init_threat_obs_state(InfernoState* state, int player_x, int player_ state->player.current_magic = 99; state->weapon_set = INF_GEAR_BP; osrs_interaction_init(&state->interaction); - encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, + encounter_compute_loadout_stats( + INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, &state->loadout_stats[INF_GEAR_MAGE]); - encounter_compute_loadout_stats(INF_MAX_RANGE_LONG_LOADOUT, ATTACK_STYLE_RANGED, + encounter_compute_loadout_stats( + INF_MAX_RANGE_LONG_LOADOUT, ATTACK_STYLE_RANGED, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, &state->loadout_stats[INF_GEAR_LONG_RANGE]); - encounter_compute_loadout_stats(INF_MAX_RANGE_FAST_LOADOUT, ATTACK_STYLE_RANGED, + encounter_compute_loadout_stats( + INF_MAX_RANGE_FAST_LOADOUT, ATTACK_STYLE_RANGED, OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, &state->loadout_stats[INF_GEAR_BP]); } @@ -3846,35 +3751,14 @@ static void test_npc_threat_obs_exposes_frozen_meleer_pressure(void) { threat.can_attack_this_tick, 1); ASSERT_INT_EQ("immediate threat includes frozen adjacent meleer", inf_player_has_immediate_threat(&state), 1); - InfNpcPressureSummary pressure = inf_npc_pressure_summary(&state); - ASSERT_INT_EQ("pressure summary counts frozen meleer if ready", - pressure.if_ready_total, 1); - ASSERT_INT_EQ("pressure summary counts frozen meleer this tick", - pressure.this_tick_total, 1); - ASSERT_INT_EQ("pressure summary exposes melee style", - pressure.if_ready_melee, 1); - ASSERT_INT_EQ("pressure summary exposes ready timer", - pressure.timer_lte_1, 1); float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); int obs_slot = inf_find_target_obs_slot(&state, 0); ASSERT_INT_EQ("frozen meleer has obs slot", obs_slot >= 0, 1); - ASSERT_FLOAT_NEAR("frozen meleer threat if ready obs", - obs[inferno_obs_slot_npc_can_attack_if_ready_index(obs_slot)], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("frozen meleer threat this tick obs", - obs[inferno_obs_slot_npc_can_attack_this_tick_index(obs_slot)], 1.0f, 1e-6f); ASSERT_FLOAT_NEAR("frozen ticks obs", obs[inferno_obs_slot_frozen_index(obs_slot)], 8.0f / (float)BARRAGE_FREEZE_TICKS, 1e-6f); - ASSERT_FLOAT_NEAR("prayer critical includes frozen melee pressure", - obs[INF_OBS_PRAYER_MELEE], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("pressure obs counts frozen meleer if ready", - obs[INF_OBS_PRESSURE_IF_READY_TOTAL], 1.0f / (float)INF_OBS_NPCS, 1e-6f); - ASSERT_FLOAT_NEAR("pressure obs counts frozen meleer this tick", - obs[INF_OBS_PRESSURE_THIS_TICK_TOTAL], 1.0f / (float)INF_OBS_NPCS, 1e-6f); - ASSERT_FLOAT_NEAR("pressure obs nearest timer", - obs[INF_OBS_PRESSURE_NEAREST_TIMER], 0.1f, 1e-6f); } static void test_npc_threat_obs_respects_overlap_range_and_stun(void) { @@ -3887,8 +3771,6 @@ static void test_npc_threat_obs_respects_overlap_range_and_stun(void) { InfNpcPlayerThreat under_threat = inf_npc_player_threat(&under, &under.npcs[0]); ASSERT_INT_EQ("standing under frozen meleer is not attackable", under_threat.can_attack_if_ready, 0); - ASSERT_INT_EQ("standing under frozen meleer is not pressure", - inf_npc_pressure_summary(&under).if_ready_total, 0); InfernoState diagonal; init_threat_obs_state(&diagonal, 10, 10); @@ -3898,8 +3780,6 @@ static void test_npc_threat_obs_respects_overlap_range_and_stun(void) { inf_npc_player_threat(&diagonal, &diagonal.npcs[0]); ASSERT_INT_EQ("frozen meleer diagonal corner contact is not attackable", diagonal_threat.can_attack_if_ready, 0); - ASSERT_INT_EQ("frozen meleer diagonal corner contact is not pressure", - inf_npc_pressure_summary(&diagonal).if_ready_total, 0); InfernoState far; init_threat_obs_state(&far, 10, 10); @@ -3919,52 +3799,9 @@ static void test_npc_threat_obs_respects_overlap_range_and_stun(void) { stunned_threat.can_attack_if_ready, 1); ASSERT_INT_EQ("stunned adjacent meleer cannot attack this tick", stunned_threat.can_attack_this_tick, 0); - InfNpcPressureSummary stunned_pressure = inf_npc_pressure_summary(&stunned); - ASSERT_INT_EQ("stunned meleer keeps if-ready pressure", - stunned_pressure.if_ready_total, 1); - ASSERT_INT_EQ("stunned meleer is not this-tick pressure", - stunned_pressure.this_tick_total, 0); - ASSERT_INT_EQ("stunned meleer is not imminent pressure", - stunned_pressure.timer_lte_1, 0); -} - -static void test_npc_pressure_summary_respects_los_target_and_mixed_styles(void) { - printf("--- npc pressure summary respects LOS target and mixed styles ---\n"); - - InfernoState blocked; - init_threat_obs_state(&blocked, 10, 10); - add_threat_obs_npc(&blocked, 0, INF_NPC_RANGER, 20, 10); - blocked.los_blockers[0] = (LOSBlocker){15, 10, 1, LOS_FULL_MASK}; - blocked.los_blocker_count = 1; - inf_invalidate_los_cache(&blocked); - ASSERT_INT_EQ("pillar blocks ranged pressure", - inf_npc_pressure_summary(&blocked).if_ready_total, 0); - - InfernoState redirected; - init_threat_obs_state(&redirected, 10, 10); - add_threat_obs_npc(&redirected, 0, INF_NPC_RANGER, 15, 10); - redirected.npcs[1] = make_test_npc( - INF_NPC_ZUK_SHIELD, 12, 10, INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size); - redirected.npcs[1].active = 1; - redirected.npcs[0].aggro_target = 1; - ASSERT_INT_EQ("non-player target does not create pressure", - inf_npc_pressure_summary(&redirected).if_ready_total, 0); - - InfernoState mixed; - init_threat_obs_state(&mixed, 10, 10); - add_threat_obs_npc(&mixed, 0, INF_NPC_RANGER, 15, 10); - add_threat_obs_npc(&mixed, 1, INF_NPC_MAGER, 10, 15); - InfNpcPressureSummary pressure = inf_npc_pressure_summary(&mixed); - ASSERT_INT_EQ("mixed pressure counts two NPCs", - pressure.if_ready_total, 2); - ASSERT_INT_EQ("mixed pressure includes ranged", - pressure.if_ready_ranged, 1); - ASSERT_INT_EQ("mixed pressure includes magic", - pressure.if_ready_magic, 1); - ASSERT_INT_EQ("mixed pressure exposes two styles", - pressure.if_ready_style_count, 2); } + static void test_npc_threat_obs_keeps_ranger_mager_diagonal_melee(void) { printf("--- npc threat obs keeps ranger and mager diagonal melee ---\n"); @@ -3989,52 +3826,6 @@ static void test_npc_threat_obs_keeps_ranger_mager_diagonal_melee(void) { (mager_threat.style_mask & INF_STYLE_MASK_MELEE) != 0, 1); } -static void test_player_attackability_obs_exposes_current_loadout_and_los(void) { - printf("--- player attackability obs exposes current loadout and LOS ---\n"); - - InfernoState state; - init_threat_obs_state(&state, 10, 10); - add_threat_obs_npc(&state, 0, INF_NPC_RANGER, 20, 10); - inf_refresh_current_obs_slots(&state); - - float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); - int obs_slot = inf_find_target_obs_slot(&state, 0); - ASSERT_INT_EQ("range test ranger has obs slot", obs_slot >= 0, 1); - ASSERT_FLOAT_NEAR("ranger size obs is normalized", - obs[inferno_obs_slot_size_index(obs_slot)], 3.0f / 7.0f, 1e-6f); - ASSERT_FLOAT_NEAR("blowpipe cannot attack distance ten ranger", - obs[inferno_obs_slot_player_can_attack_index(obs_slot)], 0.0f, 1e-6f); - ASSERT_FLOAT_NEAR("player has LOS to distance ten ranger", - obs[inferno_obs_slot_player_has_los_index(obs_slot)], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("mage loadout can attack distance ten ranger", - obs[inferno_obs_slot_player_can_mage_index(obs_slot)], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("long-range loadout can attack distance ten ranger", - obs[inferno_obs_slot_player_can_long_range_index(obs_slot)], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("blowpipe loadout cannot attack distance ten ranger", - obs[inferno_obs_slot_player_can_blowpipe_index(obs_slot)], 0.0f, 1e-6f); - ASSERT_FLOAT_NEAR("mage loadout has no range deficit", - obs[inferno_obs_slot_mage_range_deficit_index(obs_slot)], 0.0f, 1e-6f); - ASSERT_FLOAT_NEAR("long-range loadout has no range deficit", - obs[inferno_obs_slot_long_range_deficit_index(obs_slot)], 0.0f, 1e-6f); - int bp_deficit = 10 - state.loadout_stats[INF_GEAR_BP].attack_range; - if (bp_deficit < 0) bp_deficit = 0; - float expected_bp_deficit = (float)bp_deficit / 15.0f; - ASSERT_FLOAT_NEAR("blowpipe range deficit is geometric", - obs[inferno_obs_slot_blowpipe_range_deficit_index(obs_slot)], - expected_bp_deficit, 1e-6f); - - state.los_blockers[0] = (LOSBlocker){15, 10, 1, LOS_FULL_MASK}; - state.los_blocker_count = 1; - inf_invalidate_los_cache(&state); - inf_write_obs((EncounterState*)&state, obs); - ASSERT_FLOAT_NEAR("LOS blocker hides player LOS", - obs[inferno_obs_slot_player_has_los_index(obs_slot)], 0.0f, 1e-6f); - ASSERT_FLOAT_NEAR("LOS blocker blocks mage reachability", - obs[inferno_obs_slot_player_can_mage_index(obs_slot)], 0.0f, 1e-6f); - ASSERT_FLOAT_NEAR("LOS blocker keeps mage range deficit geometric", - obs[inferno_obs_slot_mage_range_deficit_index(obs_slot)], 0.0f, 1e-6f); -} static void test_barrage_total_target_count_matches_resolution_targets(void) { printf("--- barrage total target count matches resolution targets ---\n"); @@ -4051,13 +3842,6 @@ static void test_barrage_total_target_count_matches_resolution_targets(void) { ASSERT_INT_EQ("barrage count includes primary and active non-shields", inf_barrage_total_target_count(&state, 0), 3); - inf_refresh_current_obs_slots(&state); - float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); - int obs_slot = inf_find_target_obs_slot(&state, 0); - ASSERT_FLOAT_NEAR("barrage count obs includes primary target", - obs[inferno_obs_slot_barrage_count_index(obs_slot)], - 3.0f / (float)BARRAGE_MAX_HITS, 1e-6f); for (int i = 5; i < 16; i++) { add_threat_obs_npc(&state, i, INF_NPC_BAT, 15 + (i % 3), 9 + (i % 3)); @@ -4085,9 +3869,6 @@ static void init_step_out_forecast_stack_state(InfernoState* state, int player_x state->player_last_interaction_age = 1; state->player_dest_x = -1; state->player_dest_y = -1; - test_config()->step_out_forecast_obs_enabled = 1; - test_config()->step_out_forecast_obs_mode = - INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT; state->weapon_set = INF_GEAR_LONG_RANGE; osrs_interaction_init(&state->interaction); for (int p = 0; p < INF_NUM_PILLARS; p++) { @@ -4297,85 +4078,6 @@ static void test_step_out_forecast_north_pillar_ranger_mager_order(void) { assert_step_out_ranger_then_mager("north pillar run west", run_west, 27, 39); } -static void test_step_out_forecast_obs_exposes_compact_action_affordance(void) { - printf("--- step-out forecast obs exposes compact action affordance ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 29, 39); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 0); - add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); - - float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); - - int action_start = inferno_step_out_forecast_obs_start() + - 11 * INF_STEP_OUT_FORECAST_ACTION_FEATURES; - ASSERT_FLOAT_NEAR("run west obs valid", - obs[action_start], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("run west obs first attack tick", - obs[action_start + 1], 1.0f / 4.0f, 1e-6f); - ASSERT_FLOAT_NEAR("run west obs first style mask", - obs[action_start + 2], (float)INF_STYLE_MASK_RANGED / 7.0f, 1e-6f); - ASSERT_FLOAT_NEAR("run west obs has max hit", - obs[action_start + 3], 70.0f / 150.0f, 1e-6f); - ASSERT_FLOAT_NEAR("run west obs same-tick conflict", - obs[action_start + 4], 0.0f, 1e-6f); - ASSERT_FLOAT_NEAR("run west obs ranger/mager conflict", - obs[action_start + 5], 0.0f, 1e-6f); - ASSERT_FLOAT_NEAR("run west obs off-tick opportunity", - obs[action_start + 6], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("run west obs melee fallback exposure", - obs[action_start + 7], 0.0f, 1e-6f); -} - -static void test_step_out_forecast_obs_can_be_disabled(void) { - printf("--- step-out forecast obs can be disabled ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 29, 39); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 0); - add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); - test_config()->step_out_forecast_obs_enabled = 0; - test_config()->step_out_forecast_obs_mode = INF_STEP_OUT_FORECAST_MODE_OFF; - - float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); - - int forecast_start = inferno_step_out_forecast_obs_start(); - for (int j = 0; j < INF_STEP_OUT_FORECAST_OBS_SIZE; j++) { - ASSERT_FLOAT_NEAR("disabled forecast obs stays zero", - obs[forecast_start + j], 0.0f, 1e-6f); - } -} - -static void test_step_out_forecast_obs_uses_fast_mode(void) { - printf("--- step-out forecast obs uses fast mode ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 29, 39); - clear_step_out_forecast_pillars(&state); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 1); - add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 2); - inf_rebuild_entity_collision_flags(&state); - test_config()->step_out_forecast_obs_enabled = 1; - test_config()->step_out_forecast_obs_mode = - INF_STEP_OUT_FORECAST_MODE_FAST_STATIC_TILE; - - float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); - - int action_start = inferno_step_out_forecast_obs_start(); - ASSERT_FLOAT_NEAR("fast obs idle valid", - obs[action_start], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("fast obs idle first attack tick", - obs[action_start + 1], 1.0f / 4.0f, 1e-6f); - ASSERT_FLOAT_NEAR("fast obs idle first style mask", - obs[action_start + 2], (float)INF_STYLE_MASK_RANGED / 7.0f, 1e-6f); - ASSERT_FLOAT_NEAR("fast obs idle max hit", - obs[action_start + 3], 70.0f / 150.0f, 1e-6f); - ASSERT_FLOAT_NEAR("fast obs idle off-tick opportunity", - obs[action_start + 6], 1.0f, 1e-6f); -} static void test_fast_step_out_forecast_matches_movement_head_destinations(void) { printf("--- fast step-out forecast matches movement head destinations ---\n"); @@ -4627,31 +4329,6 @@ static void test_readonly_step_out_forecast_pillar_step_out_cases(void) { "west pillar walk north", &west_state, 4); } -static void test_step_out_forecast_obs_uses_readonly_mode(void) { - printf("--- step-out forecast obs uses readonly mode ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 29, 39); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 0); - add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); - test_config()->step_out_forecast_obs_enabled = 1; - test_config()->step_out_forecast_obs_mode = - INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE; - - float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); - - int action_start = inferno_step_out_forecast_obs_start() + - 11 * INF_STEP_OUT_FORECAST_ACTION_FEATURES; - ASSERT_FLOAT_NEAR("readonly obs run west valid", - obs[action_start], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("readonly obs run west first attack tick", - obs[action_start + 1], 1.0f / 4.0f, 1e-6f); - ASSERT_FLOAT_NEAR("readonly obs run west first style mask", - obs[action_start + 2], (float)INF_STYLE_MASK_RANGED / 7.0f, 1e-6f); - ASSERT_FLOAT_NEAR("readonly obs run west off-tick opportunity", - obs[action_start + 6], 1.0f, 1e-6f); -} static void test_readonly_step_out_forecast_stun_countdown(void) { printf("--- readonly step-out forecast stun countdown ---\n"); @@ -5021,7 +4698,7 @@ static void test_zuk_obs_exposes_attack_timer_summary(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); - ASSERT_FLOAT_NEAR("zuk attack timer uses existing player placeholder", + ASSERT_FLOAT_NEAR("zuk attack timer uses compact player field", obs[INF_OBS_ZUK_ATTACK_TIMER], 0.3f, 1e-6f); } @@ -5073,25 +4750,20 @@ static void test_zuk_obs_exposes_pending_sparks(void) { inf_write_obs((EncounterState*)&state, obs); ASSERT_INT_EQ("spark obs keeps all pending slots", spark_slots, INF_MAX_PENDING_SPARKS); - ASSERT_INT_EQ("spark obs carries landing and source", spark_features, 7); - ASSERT_FLOAT_NEAR("first spark active", obs[spark_start], 1.0f, 1e-6f); + ASSERT_INT_EQ("spark obs carries compact landing record", spark_features, 4); ASSERT_FLOAT_NEAR("first spark landing x", - obs[spark_start + 1], -1.0f / (float)INF_ARENA_WIDTH, 1e-6f); + obs[spark_start], -1.0f / (float)INF_ARENA_WIDTH, 1e-6f); ASSERT_FLOAT_NEAR("first spark landing y", - obs[spark_start + 2], 2.0f / (float)INF_ARENA_HEIGHT, 1e-6f); - ASSERT_FLOAT_NEAR("first spark source x", - obs[spark_start + 3], 5.0f / (float)INF_ARENA_WIDTH, 1e-6f); - ASSERT_FLOAT_NEAR("first spark source y", - obs[spark_start + 4], 0.0f, 1e-6f); + obs[spark_start + 1], 2.0f / (float)INF_ARENA_HEIGHT, 1e-6f); ASSERT_FLOAT_NEAR("first spark timer", - obs[spark_start + 5], 0.2f, 1e-6f); + obs[spark_start + 2], 0.2f, 1e-6f); ASSERT_FLOAT_NEAR("first spark damage", - obs[spark_start + 6], 0.7f, 1e-6f); + obs[spark_start + 3], 0.7f, 1e-6f); ASSERT_FLOAT_NEAR("second spark landing x", - obs[spark_start + spark_features + 1], + obs[spark_start + spark_features], -2.0f / (float)INF_ARENA_WIDTH, 1e-6f); ASSERT_FLOAT_NEAR("third spark sorts same-tick nearest landing first", - obs[spark_start + 2 * spark_features + 1], + obs[spark_start + 2 * spark_features], 1.0f / (float)INF_ARENA_WIDTH, 1e-6f); } @@ -5715,7 +5387,7 @@ static void test_zuk_healer_target_action_tags_on_landed_hit(void) { inf_write_obs((EncounterState*)&state, obs); inf_write_mask((EncounterState*)&state, mask); - int healer_slot = 33; + int healer_slot = inf_find_target_obs_slot(&state, 2); ASSERT_INT_EQ("zuk healer occupies first healer slot", state.current_obs_slots[healer_slot], 2); ASSERT_FLOAT_NEAR("zuk healer target mask is valid", @@ -5787,7 +5459,7 @@ static void test_zuk_healer_mage_attack_counts_penalty_event(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = 34; + actions[INF_HEAD_TARGET] = inf_action_target_for_npc(&state, 2); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("mage healer attack fires once", @@ -5815,13 +5487,13 @@ static void test_zuk_safe_healer_target_mask_requires_fire_window(void) { float obs[INF_NUM_OBS]; float mask[INF_ACTION_MASK_SIZE]; - int healer_slot = 33; state.player.x = 20; state.player.y = 46; state.player.attack_timer = 0; inf_write_obs((EncounterState*)&state, obs); inf_write_mask((EncounterState*)&state, mask); + int healer_slot = inf_find_target_obs_slot(&state, 2); ASSERT_FLOAT_NEAR("unsafe healer target masked while off shield", mask[inferno_target_mask_slot_offset(healer_slot)], 0.0f, 1e-6f); @@ -6446,40 +6118,14 @@ static void test_phantom_barrage_does_not_displace_live_obs_slots(void) { inf_refresh_current_obs_slots(&state); - for (int slot = 16; slot < 22; slot++) { + for (int npc_idx = 1; npc_idx <= 6; npc_idx++) { ASSERT_INT_EQ("live nibbler fills capped obs slot", - state.current_obs_slots[slot] >= 1 && state.current_obs_slots[slot] <= 6, - 1); + inf_find_target_obs_slot(&state, npc_idx) >= 0, 1); } ASSERT_INT_EQ("dying phantom target does not displace live cap", inf_find_target_obs_slot(&state, 0), -1); } -static void test_phantom_barrage_targetability_obs_requires_ready_attack(void) { - printf("--- phantom barrage targetability obs requires ready attack ---\n"); - - InfernoState state; - init_phantom_barrage_test_state(&state, 1, 1); - float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); - int target_slot = inf_find_target_obs_slot(&state, 0); - ASSERT_INT_EQ("dying target appears in obs slots", target_slot >= 0, 1); - if (target_slot < 0) return; - - int phantom_idx = inferno_obs_slot_phantom_index(target_slot); - ASSERT_FLOAT_NEAR("cooldown target is not phantom targetable now", - obs[phantom_idx], 0.0f, 1e-6f); - - init_phantom_barrage_test_state(&state, 1, 0); - memset(obs, 0, sizeof(obs)); - inf_write_obs((EncounterState*)&state, obs); - target_slot = inf_find_target_obs_slot(&state, 0); - ASSERT_INT_EQ("ready dying target appears in obs slots", target_slot >= 0, 1); - if (target_slot < 0) return; - phantom_idx = inferno_obs_slot_phantom_index(target_slot); - ASSERT_FLOAT_NEAR("ready barrage target is phantom targetable now", - obs[phantom_idx], 1.0f, 1e-6f); -} static void init_confliction_barrage_test_state( InfernoState* state, @@ -7013,18 +6659,16 @@ static void test_zuk_obs_tracks_shield_and_mager_aggro(void) { inf_write_obs((EncounterState*)&state, obs); inf_write_mask((EncounterState*)&state, mask); - int mager_slot = 0; - int shield_slot = 26; + int mager_slot = inf_find_target_obs_slot(&state, 0); + int shield_slot = inf_find_target_obs_slot(&state, 2); int shield_hp = inferno_obs_slot_hp_index(shield_slot); int mager_target_category = inferno_obs_slot_target_category_start(mager_slot); ASSERT_INT_EQ("first mager occupies mager slot 0", state.current_obs_slots[mager_slot], 0); ASSERT_INT_EQ("shield occupies dedicated shield slot", state.current_obs_slots[shield_slot], 2); - ASSERT_FLOAT_NEAR("shield hp ratio visible in shield slot", obs[shield_hp], 0.5f, 1e-6f); - ASSERT_FLOAT_NEAR("mager target_player off while on shield", - obs[mager_target_category], 0.0f, 1e-6f); - ASSERT_FLOAT_NEAR("mager target_shield on while on shield", - obs[mager_target_category + 2], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("mager target shield uses compact category code", + obs[mager_target_category], + (float)INF_TARGET_CATEGORY_SHIELD / 8.0f, 1e-6f); ASSERT_FLOAT_NEAR("shield direction visible while alive", obs[INF_OBS_ZUK_SHIELD_DIR], 0.0f, 1e-6f); ASSERT_FLOAT_NEAR("shield freeze visible while alive", obs[INF_OBS_ZUK_SHIELD_FREEZE], 0.6f, 1e-6f); ASSERT_FLOAT_NEAR("mager target mask is valid", mask[inferno_target_mask_slot_offset(mager_slot)], 1.0f, 1e-6f); @@ -7043,12 +6687,13 @@ static void test_zuk_obs_tracks_shield_and_mager_aggro(void) { ASSERT_FLOAT_NEAR("dead shield slot hp zeros out", obs[shield_hp], 0.0f, 1e-6f); ASSERT_FLOAT_NEAR("dead shield zeroes stale direction", obs[INF_OBS_ZUK_SHIELD_DIR], 0.0f, 1e-6f); ASSERT_FLOAT_NEAR("dead shield zeroes stale freeze", obs[INF_OBS_ZUK_SHIELD_FREEZE], 0.0f, 1e-6f); - ASSERT_FLOAT_NEAR("mager target_player flips on", - obs[mager_target_category], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("mager target player uses compact category code", + obs[mager_target_category], + (float)INF_TARGET_CATEGORY_PLAYER / 8.0f, 1e-6f); } -static void test_zuk_healer_obs_marks_untagged_healer_pressure(void) { - printf("--- zuk healer obs marks untagged healer pressure ---\n"); +static void test_zuk_healer_obs_exposes_target_category(void) { + printf("--- zuk healer obs exposes target category ---\n"); InfernoState state = make_test_state(INF_ZUK_PLAYER_START_X, INF_ZUK_PLAYER_START_Y); state.wave = 68; @@ -7077,21 +6722,21 @@ static void test_zuk_healer_obs_marks_untagged_healer_pressure(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); - int healer_slot = 33; + int healer_slot = inf_find_target_obs_slot(&state, 1); int healer_target_category = inferno_obs_slot_target_category_start(healer_slot); ASSERT_INT_EQ("Zuk healer occupies first Zuk healer obs slot", state.current_obs_slots[healer_slot], 1); - ASSERT_FLOAT_NEAR("untagged Zuk healer target_zuk is visible", - obs[healer_target_category + 1], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("untagged Zuk healer target uses compact Zuk code", + obs[healer_target_category], + (float)INF_TARGET_CATEGORY_ZUK / 8.0f, 1e-6f); state.npcs[1].aggro_target = -1; memset(obs, 0, sizeof(obs)); inf_write_obs((EncounterState*)&state, obs); - ASSERT_FLOAT_NEAR("tagged Zuk healer target_player is visible", - obs[healer_target_category], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("tagged Zuk healer clears target_zuk", - obs[healer_target_category + 1], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("tagged Zuk healer target uses compact player code", + obs[healer_target_category], + (float)INF_TARGET_CATEGORY_PLAYER / 8.0f, 1e-6f); } static void test_inferno_obs_target_categories_cover_boss_helpers(void) { @@ -7120,18 +6765,20 @@ static void test_inferno_obs_target_categories_cover_boss_helpers(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); - int healer_slot = 27; + int healer_slot = inf_find_target_obs_slot(&state, 1); int target_category = inferno_obs_slot_target_category_start(healer_slot); ASSERT_INT_EQ("Jad healer occupies helper slot", state.current_obs_slots[healer_slot], 1); - ASSERT_FLOAT_NEAR("Jad healer targeting Jad maps to other NPC", - obs[target_category + 3], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("Jad healer targeting Jad uses compact other-NPC code", + obs[target_category], + (float)INF_TARGET_CATEGORY_OTHER_NPC / 8.0f, 1e-6f); state.npcs[1].aggro_target = -1; memset(obs, 0, sizeof(obs)); inf_write_obs((EncounterState*)&state, obs); - ASSERT_FLOAT_NEAR("tagged Jad healer maps to player", - obs[target_category], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("tagged Jad healer uses compact player code", + obs[target_category], + (float)INF_TARGET_CATEGORY_PLAYER / 8.0f, 1e-6f); } static void test_zuk_set_obs_los_uses_current_target(void) { @@ -7164,15 +6811,19 @@ static void test_zuk_set_obs_los_uses_current_target(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); - int mager_target_category = inferno_obs_slot_target_category_start(0); - ASSERT_INT_EQ("mager occupies first obs slot", state.current_obs_slots[0], 0); + int mager_slot = inf_find_target_obs_slot(&state, 0); + int mager_target_category = inferno_obs_slot_target_category_start(mager_slot); + ASSERT_INT_EQ("mager occupies a dense obs slot", + state.current_obs_slots[mager_slot], 0); ASSERT_FLOAT_NEAR("mager los follows shield target", - obs[inferno_obs_slot_npc_los_index(0)], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("mager target_shield stays on", obs[mager_target_category + 2], 1.0f, 1e-6f); + obs[inferno_obs_slot_npc_los_index(mager_slot)], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("mager target shield uses compact category code", + obs[mager_target_category], + (float)INF_TARGET_CATEGORY_SHIELD / 8.0f, 1e-6f); } -static void test_zuk_set_prayer_critical_ignores_shield_target(void) { - printf("--- zuk set prayer critical ignores shield target ---\n"); +static void test_zuk_set_threat_ignores_shield_target(void) { + printf("--- zuk set threat ignores shield target ---\n"); InfernoState state = make_test_state(20, 34); state.wave = 68; @@ -7199,13 +6850,12 @@ static void test_zuk_set_prayer_critical_ignores_shield_target(void) { state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_ZUK_SHIELD].hp; state.zuk.shield_idx = 1; - float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); - - ASSERT_FLOAT_NEAR("shield-targeted mager does not create prayer deadline", obs[INF_OBS_PRAYER_TIMER], 1.0f, 1e-6f); - ASSERT_FLOAT_NEAR("shield-targeted mager has no prayer style", - obs[INF_OBS_PRAYER_MELEE] + obs[INF_OBS_PRAYER_RANGED] + obs[INF_OBS_PRAYER_MAGIC], - 0.0f, 1e-6f); + InfNpcPlayerThreat threat = + inf_npc_player_threat(&state, &state.npcs[0]); + ASSERT_INT_EQ("shield-targeted mager does not threaten player", + threat.can_attack_if_ready, 0); + ASSERT_INT_EQ("shield-targeted mager has no player style mask", + threat.style_mask, 0); } static void child_inf_put_bad_start_wave(void) { @@ -7417,14 +7067,19 @@ static void test_human_targeting_refreshes_stale_obs_slots(void) { ASSERT_INT_EQ("live ranger is targetable without prior obs write", inf_is_human_targetable_npc_slot((EncounterState*)&state, 5), 1); + inf_refresh_current_obs_slots(&state); + int ranger_slot = inf_find_target_obs_slot(&state, 5); + ASSERT_INT_EQ("live ranger has a dense observation slot", + ranger_slot >= 0, 1); + { HumanInput hi = make_human_input(); int actions[INF_NUM_ACTION_HEADS]; hi.pending_target_idx = 5; - state.current_obs_slots[2] = -1; + state.current_obs_slots[ranger_slot] = -1; inf_translate_human_input(&hi, actions, (EncounterState*)&state); ASSERT_INT_EQ("pending human target refreshes obs slot", - actions[INF_HEAD_TARGET], 3); + actions[INF_HEAD_TARGET], ranger_slot + 1); } { @@ -7432,10 +7087,10 @@ static void test_human_targeting_refreshes_stale_obs_slots(void) { int actions[INF_NUM_ACTION_HEADS]; human_input_init(&hi); human_input_queue_attack_npc(&hi, 5); - state.current_obs_slots[2] = -1; + state.current_obs_slots[ranger_slot] = -1; inf_translate_human_commands(&hi, actions, &state); ASSERT_INT_EQ("queued human target refreshes obs slot", - actions[INF_HEAD_TARGET], 3); + actions[INF_HEAD_TARGET], ranger_slot + 1); human_input_destroy(&hi); } } @@ -9115,47 +8770,6 @@ static void test_inferno_binding_forwards_terminal_penalty_toggle(void) { "[sweep.env.terminal_penalty_enabled]"); } -static void test_inferno_binding_forwards_step_out_forecast_obs_toggle(void) { - printf("--- inferno binding forwards step-out forecast obs toggle ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "step-out forecast obs mode int parsed", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_init", - "inferno_apply_curriculum(env, kwargs);", - "dict_get(kwargs, \"step_out_forecast_obs_mode\")"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "step-out forecast obs mode default config", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "step_out_forecast_obs_mode = 1"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "step-out forecast obs mode sweep axis", - "config/osrs_inferno.ini", - "[sweep.env.step_out_forecast_obs_mode]", - "scale = auto", - "distribution = int_uniform"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "step-out forecast obs mode sweep covers readonly mode", - "config/osrs_inferno.ini", - "[sweep.env.step_out_forecast_obs_mode]", - "scale = auto", - "max = 3"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "step-out forecast readonly mode enum", - "ocean/osrs/encounters/inferno/encounter_inferno_model.inc", - "INF_STEP_OUT_FORECAST_MODE_OFF = 0", - "};", - "INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE = 3"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "step-out forecast mode hash is source of truth", - "ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc", - "static uint64_t inf_config_fingerprint", - "return h;", - "INF_HASH_CONFIG_FIELD(config, &h, step_out_forecast_obs_mode)"); -} - static void test_inferno_binding_forwards_loadout_profile_config(void) { printf("--- inferno binding forwards loadout profile config ---\n"); @@ -9229,12 +8843,6 @@ static void test_inferno_binding_logs_idle_diagnostics(void) { "void puf_log", "float wr = log->wins", "progressless_ticks"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "npc pressure metric emitted", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_log", - "float wr = log->wins", - "npc_pressure_this_tick_count_per_tick"); ASSERT_SOURCE_BLOCK_CONTAINS( "idle phase names emitted", "ocean/osrs_inferno/osrs_inferno.h", @@ -9288,8 +8896,389 @@ static void test_inferno_reset_uses_osrs_run_energy_units(void) { osrs_run_energy_percent(state.player.run_energy), 100); } +static void activate_dense_target_test_npc( + InfernoState* state, + int npc_idx, + InfNPCType type +) { + state->npcs[npc_idx] = make_test_npc( + type, 10 + npc_idx, 20, INF_NPC_STATS[type].size); + state->npcs[npc_idx].active = 1; + state->npcs[npc_idx].hp = state->npcs[npc_idx].max_hp = + INF_NPC_STATS[type].hp; +} + +static void test_dense_target_contract_dimensions(void) { + printf("--- dense target contract dimensions ---\n"); + + ASSERT_INT_EQ("dense target NPC slots", INF_OBS_NPCS, 14); + ASSERT_INT_EQ("target head includes no-target action", + INF_ACTION_DIMS[INF_HEAD_TARGET], 15); + ASSERT_INT_EQ("dense target action mask width", INF_ACTION_MASK_SIZE, 434); +} + +static void test_dense_target_slots_follow_type_priority_without_holes(void) { + printf("--- dense target slots follow type priority without holes ---\n"); + + InfernoState state = make_test_state(20, 20); + activate_dense_target_test_npc(&state, 0, INF_NPC_HEALER_ZUK); + activate_dense_target_test_npc(&state, 1, INF_NPC_BAT); + activate_dense_target_test_npc(&state, 2, INF_NPC_MAGER); + activate_dense_target_test_npc(&state, 3, INF_NPC_BLOB_MELEE); + activate_dense_target_test_npc(&state, 4, INF_NPC_RANGER); + activate_dense_target_test_npc(&state, 5, INF_NPC_NIBBLER); + activate_dense_target_test_npc(&state, 6, INF_NPC_MELEER); + activate_dense_target_test_npc(&state, 7, INF_NPC_BLOB); + activate_dense_target_test_npc(&state, 8, INF_NPC_BLOB_MAGE); + activate_dense_target_test_npc(&state, 9, INF_NPC_BLOB_RANGE); + activate_dense_target_test_npc(&state, 10, INF_NPC_JAD); + activate_dense_target_test_npc(&state, 11, INF_NPC_ZUK); + activate_dense_target_test_npc(&state, 12, INF_NPC_ZUK_SHIELD); + activate_dense_target_test_npc(&state, 13, INF_NPC_HEALER_JAD); + + static const int expected_npc_indices[14] = { + 2, 4, 6, 7, 1, 8, 9, 3, 5, 10, 11, 12, 13, 0, + }; + + inf_refresh_current_obs_slots(&state); + + for (int slot = 0; slot < 14; slot++) { + ASSERT_INT_EQ("dense slot follows type priority", + state.current_obs_slots[slot], expected_npc_indices[slot]); + } + for (int slot = 14; slot < INF_OBS_NPCS; slot++) { + ASSERT_INT_EQ("unused dense target slot is empty", + state.current_obs_slots[slot], -1); + } +} + +static void test_regular_waves_select_every_live_npc(void) { + printf("--- regular waves select every live NPC ---\n"); + + for (int wave = 0; wave < INF_WAVE_ZUK; wave++) { + InfernoState state = make_test_state(20, 20); + state.wave = wave; + inf_spawn_wave(&state); + inf_refresh_current_obs_slots(&state); + + for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { + if (!state.npcs[npc_idx].active || + state.npcs[npc_idx].death_ticks != 0 || + state.npcs[npc_idx].hp <= 0) + continue; + ASSERT_INT_EQ("regular-wave live NPC is selected", + inf_find_target_obs_slot(&state, npc_idx) >= 0, 1); + } + } +} + +static void test_maximal_zuk_concurrency_selects_all_live_candidates(void) { + printf("--- maximal Zuk concurrency selects all live candidates ---\n"); + + static const InfNPCType types[14] = { + INF_NPC_ZUK, + INF_NPC_ZUK_SHIELD, + INF_NPC_JAD, + INF_NPC_HEALER_JAD, + INF_NPC_HEALER_JAD, + INF_NPC_HEALER_JAD, + INF_NPC_MAGER, + INF_NPC_MAGER, + INF_NPC_RANGER, + INF_NPC_RANGER, + INF_NPC_HEALER_ZUK, + INF_NPC_HEALER_ZUK, + INF_NPC_HEALER_ZUK, + INF_NPC_HEALER_ZUK, + }; + + InfernoState state = make_test_state(20, 20); + for (int npc_idx = 0; npc_idx < 14; npc_idx++) + activate_dense_target_test_npc(&state, npc_idx, types[npc_idx]); + + inf_refresh_current_obs_slots(&state); + + int selected = 0; + for (int slot = 0; slot < INF_OBS_NPCS; slot++) + selected += state.current_obs_slots[slot] >= 0; + ASSERT_INT_EQ("maximal Zuk concurrency fills all dense slots", selected, 14); + + for (int npc_idx = 0; npc_idx < 14; npc_idx++) { + ASSERT_INT_EQ("maximal Zuk live candidate is selected", + inf_find_target_obs_slot(&state, npc_idx) >= 0, 1); + } +} + +static void refresh_fifteen_eligible_target_candidates(void) { + static const InfNPCType types[15] = { + INF_NPC_MAGER, INF_NPC_MAGER, + INF_NPC_RANGER, INF_NPC_RANGER, + INF_NPC_MELEER, INF_NPC_MELEER, + INF_NPC_BLOB, INF_NPC_BLOB, + INF_NPC_BAT, INF_NPC_BAT, + INF_NPC_BLOB_MAGE, INF_NPC_BLOB_MAGE, + INF_NPC_BLOB_RANGE, INF_NPC_BLOB_RANGE, + INF_NPC_BLOB_MELEE, + }; + + InfernoState state = make_test_state(20, 20); + for (int npc_idx = 0; npc_idx < 15; npc_idx++) + activate_dense_target_test_npc(&state, npc_idx, types[npc_idx]); + inf_refresh_current_obs_slots(&state); +} + +static void test_dense_target_overflow_aborts_instead_of_truncating(void) { + printf("--- dense target overflow aborts instead of truncating ---\n"); + + assert_child_aborts("fifteenth eligible target aborts refresh", + refresh_fifteen_eligible_target_candidates); +} + +static void test_compact_observation_layout_contract(void) { + printf("--- compact observation layout contract ---\n"); + + ASSERT_INT_EQ("compact player width", INF_PLAYER_OBS_SIZE, 45); + ASSERT_INT_EQ("compact pillar width", INF_PILLAR_OBS_SIZE, 9); + ASSERT_INT_EQ("compact NPC stride", INF_NPC_SLOT_FEATURES, 13); + ASSERT_INT_EQ("compact pending hit stride", INF_FEATURES_PER_HIT, 3); + ASSERT_INT_EQ("compact spark stride", INF_FEATURES_PER_SPARK, 4); + ASSERT_INT_EQ("inventory carries one code per cell", INF_INVENTORY_OBS_SIZE, 28); + ASSERT_INT_EQ("equipment aggregate width", INF_EQUIPPED_OBS_SIZE, 10); + + ASSERT_INT_EQ("compact player end", INF_OBS_AFTER_PLAYER, 45); + ASSERT_INT_EQ("compact pillar end", INF_OBS_AFTER_PILLARS, 54); + ASSERT_INT_EQ("compact NPC end", INF_OBS_AFTER_NPCS, 236); + ASSERT_INT_EQ("compact pending hit end", INF_OBS_AFTER_PENDING_HITS, 332); + ASSERT_INT_EQ("compact spark end", INF_OBS_AFTER_SPARKS, 460); + ASSERT_INT_EQ("compact inventory end", INF_OBS_AFTER_INVENTORY, 488); + ASSERT_INT_EQ("compact observation width", INF_NUM_OBS, 498); +} + +static void test_compact_player_and_pillar_observation_semantics(void) { + printf("--- compact player and pillar observation semantics ---\n"); + + InfernoState state = make_test_state(20, 20); + state.wave = INF_WAVE_ZUK; + state.weapon_set = INF_GEAR_BP; + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + state.player.current_defence = 99; + state.player.current_ranged = 99; + state.player.current_magic = 99; + state.player.prayer = PRAYER_PROTECT_MAGIC; + state.player.offensive_prayer = OFFENSIVE_PRAYER_AUGURY; + state.zuk.enraged = 1; + state.pillars[0] = (InfPillar){ + .x = 24, + .y = 26, + .hp = INF_PILLAR_HP / 2, + .active = 1, + }; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + ASSERT_FLOAT_NEAR("overhead prayer uses raw compact code", + obs[3], (float)PRAYER_PROTECT_MAGIC / 8.0f, 1e-6f); + ASSERT_FLOAT_NEAR("high offensive prayer uses compact code", + obs[4], 3.0f / 4.0f, 1e-6f); + ASSERT_FLOAT_NEAR("wave phase uses compact code", + obs[11], (float)(inf_wave_phase_index(state.wave) + 1) / 8.0f, 1e-6f); + ASSERT_FLOAT_NEAR("weapon set uses compact code", + obs[13], (float)(INF_GEAR_BP + 1) / 4.0f, 1e-6f); + ASSERT_FLOAT_NEAR("Zuk detail starts without is-Zuk feature", + obs[40], 1.0f, 1e-6f); + + ASSERT_FLOAT_NEAR("compact pillar hp", + obs[INF_OBS_AFTER_PLAYER], + (float)state.pillars[0].hp / (float)INF_PILLAR_HP, 1e-6f); + ASSERT_FLOAT_NEAR("compact pillar relative x", + obs[INF_OBS_AFTER_PLAYER + 1], + 4.0f / (float)INF_ARENA_WIDTH, 1e-6f); + ASSERT_FLOAT_NEAR("compact pillar relative y", + obs[INF_OBS_AFTER_PLAYER + 2], + 6.0f / (float)INF_ARENA_HEIGHT, 1e-6f); +} + +static void test_compact_npc_observation_semantics(void) { + printf("--- compact NPC observation semantics ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + state.player.current_defence = 99; + state.player.current_ranged = 99; + state.player.current_magic = 99; + + state.npcs[0] = make_test_npc( + INF_NPC_BLOB, 24, 20, INF_NPC_STATS[INF_NPC_BLOB].size); + state.npcs[0].active = 1; + state.npcs[0].hp = INF_NPC_STATS[INF_NPC_BLOB].hp / 2; + state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_BLOB].hp; + state.npcs[0].attack_timer = 5; + state.npcs[0].attack_style = ATTACK_STYLE_MAGIC; + state.npcs[0].blob_scanned_prayer = PRAYER_PROTECT_RANGED; + state.npcs[0].frozen_ticks = BARRAGE_FREEZE_TICKS / 2; + osrs_interaction_set(&state.interaction, 0); + + state.npcs[1] = make_test_npc( + INF_NPC_MELEER, 22, 20, INF_NPC_STATS[INF_NPC_MELEER].size); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_MELEER].hp; + state.npcs[1].attack_timer = 3; + state.npcs[1].no_los_ticks = 25; + state.npcs[1].dig_freeze_timer = 3; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int blob_slot = inf_find_target_obs_slot(&state, 0); + int blob_start = INF_OBS_AFTER_PILLARS + blob_slot * INF_NPC_SLOT_FEATURES; + InfNpcPlayerThreat blob_threat = inf_npc_player_threat(&state, &state.npcs[0]); + ASSERT_FLOAT_NEAR("compact NPC type code", + obs[blob_start], (float)(INF_NPC_BLOB + 1) / 16.0f, 1e-6f); + ASSERT_FLOAT_NEAR("compact NPC hp", obs[blob_start + 1], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("compact NPC relative x", + obs[blob_start + 2], 4.0f / (float)INF_ARENA_WIDTH, 1e-6f); + ASSERT_FLOAT_NEAR("compact NPC relative y", obs[blob_start + 3], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("compact NPC attack timer", + obs[blob_start + 4], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("compact NPC attack style", + obs[blob_start + 5], (float)ATTACK_STYLE_MAGIC / 4.0f, 1e-6f); + ASSERT_FLOAT_NEAR("compact NPC LOS", + obs[blob_start + 6], (float)inf_npc_has_los(&state, 0), 1e-6f); + ASSERT_FLOAT_NEAR("compact NPC frozen timer", + obs[blob_start + 7], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("compact NPC target category", + obs[blob_start + 8], (float)INF_TARGET_CATEGORY_PLAYER / 8.0f, 1e-6f); + ASSERT_FLOAT_NEAR("compact NPC targeted bit", + obs[blob_start + 9], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("Blob type state 0 is zero", + obs[blob_start + 10], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("Blob type state 1 is zero", + obs[blob_start + 11], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("Blob type state 2 is zero", + obs[blob_start + 12], 0.0f, 1e-6f); + + int meleer_slot = inf_find_target_obs_slot(&state, 1); + int meleer_start = INF_OBS_AFTER_PILLARS + meleer_slot * INF_NPC_SLOT_FEATURES; + ASSERT_FLOAT_NEAR("meleer compact no-LOS progress", + obs[meleer_start + 10], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("meleer compact dig-freeze state", + obs[meleer_start + 11], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("meleer compact dig-delay state is clear", + obs[meleer_start + 12], 0.0f, 1e-6f); + + state.npcs[1].dig_freeze_timer = 0; + state.npcs[1].dig_attack_delay = 3; + inf_write_obs((EncounterState*)&state, obs); + ASSERT_FLOAT_NEAR("meleer compact dig-freeze state clears", + obs[meleer_start + 11], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("meleer compact dig-delay state", + obs[meleer_start + 12], 0.5f, 1e-6f); +} + +static void test_compact_transient_inventory_equipment_semantics(void) { + printf("--- compact transient inventory equipment semantics ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + state.player.current_defence = 99; + state.player.current_ranged = 99; + state.player.current_magic = 99; + state.player_pending_hits.count = 1; + state.player_pending_hits.hits[0] = (EncounterPendingHit){ + .active = 1, + .attack_style = ATTACK_STYLE_MAGIC, + .ticks_remaining = 4, + .damage = 75, + }; + state.pending_sparks[0] = (InfPendingSpark){ + .active = 1, + .src_x = 11, + .src_y = 12, + .x = 24, + .y = 26, + .ticks_remaining = 5, + .damage = 8, + }; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) + state.inventory_cells[cell] = osrs_inventory_cell_empty(); + state.inventory_cells[0] = + osrs_inventory_cell_from_item(ITEM_OSMUMTENS_FANG); + state.inventory_cells[1] = + osrs_inventory_cell_from_raw_osrs_id(6685); + state.player.equipment_effect_profile = (OsrsEquipmentEffectProfile){ + .effect_mask = OSRS_ITEM_EFFECT_BLOOD_FURY | + OSRS_ITEM_EFFECT_LIGHTBEARER, + .virtus_piece_count = 2, + .dharok_piece_count = 3, + .crystal_armour_points = 4, + .recoil_source = OSRS_RECOIL_SOURCE_RING_OF_RECOIL, + .spec_regen_mode = OSRS_SPEC_REGEN_MODE_LIGHTBEARER, + .shield_item = ITEM_ELYSIAN_SPIRIT_SHIELD, + }; + + float obs_off[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs_off); + ASSERT_FLOAT_NEAR("compact pending hit style", + obs_off[INF_OBS_AFTER_NPCS], + (float)ATTACK_STYLE_MAGIC / 4.0f, 1e-6f); + ASSERT_FLOAT_NEAR("compact pending hit timer", + obs_off[INF_OBS_AFTER_NPCS + 1], 0.4f, 1e-6f); + ASSERT_FLOAT_NEAR("compact pending hit damage", + obs_off[INF_OBS_AFTER_NPCS + 2], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("compact spark target relative x", + obs_off[INF_OBS_AFTER_PENDING_HITS], + 4.0f / (float)INF_ARENA_WIDTH, 1e-6f); + ASSERT_FLOAT_NEAR("compact spark target relative y", + obs_off[INF_OBS_AFTER_PENDING_HITS + 1], + 6.0f / (float)INF_ARENA_HEIGHT, 1e-6f); + ASSERT_FLOAT_NEAR("compact spark timer", + obs_off[INF_OBS_AFTER_PENDING_HITS + 2], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("compact spark damage", + obs_off[INF_OBS_AFTER_PENDING_HITS + 3], 0.8f, 1e-6f); + + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + const OsrsInventoryCell* inventory_cell = &state.inventory_cells[cell]; + float expected_code = osrs_inventory_cell_obs_code_encode( + osrs_inventory_cell_obs_code( + inventory_cell->item_idx, inventory_cell->raw_osrs_id)); + ASSERT_FLOAT_NEAR("compact inventory cell code", + obs_off[INF_OBS_AFTER_SPARKS + cell], expected_code, 1e-6f); + } + + float expected_equipment[OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES]; + osrs_write_equipment_effect_aggregate( + expected_equipment, &state.player.equipment_effect_profile); + for (int feature = 0; + feature < OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES; + feature++) { + ASSERT_FLOAT_NEAR("compact equipment aggregate", + obs_off[INF_OBS_AFTER_INVENTORY + feature], + expected_equipment[feature], 1e-6f); + } + +} + int main(void) { inf_build_npc_stats(); + test_compact_observation_layout_contract(); + test_compact_player_and_pillar_observation_semantics(); + test_compact_npc_observation_semantics(); + test_compact_transient_inventory_equipment_semantics(); + test_dense_target_contract_dimensions(); + test_dense_target_slots_follow_type_priority_without_holes(); + test_regular_waves_select_every_live_npc(); + test_maximal_zuk_concurrency_selects_all_live_candidates(); + test_dense_target_overflow_aborts_instead_of_truncating(); test_attack_chase_uses_reachable_approach_tile(); test_attack_chase_routes_around_los_blocker_while_in_range(); @@ -9377,15 +9366,13 @@ int main(void) { test_jad_prayer_first_on_fourth_tick_does_not_block(); test_jad_long_distance_damage_uses_delayed_projectile_landing(); test_triple_jad_pending_threats_fit_obs_layout(); - test_inferno_obs_shape_includes_step_out_forecast_features(); - test_inferno_obs_wave_phase_one_hot(); - test_inferno_obs_exposes_pillar_footprint_size(); + test_inferno_action_and_compact_obs_shape(); + test_inferno_obs_wave_phase_code(); + test_inferno_obs_exposes_compact_pillars(); test_inferno_obs_exposes_meleer_dig_state(); test_npc_threat_obs_exposes_frozen_meleer_pressure(); test_npc_threat_obs_respects_overlap_range_and_stun(); - test_npc_pressure_summary_respects_los_target_and_mixed_styles(); test_npc_threat_obs_keeps_ranger_mager_diagonal_melee(); - test_player_attackability_obs_exposes_current_loadout_and_los(); test_barrage_total_target_count_matches_resolution_targets(); test_jad_special_wave_spawn_cadence_matches_reference(); test_triple_jad_first_attacks_are_staggered(); @@ -9394,9 +9381,6 @@ int main(void) { test_inferno_npc_travel_uses_sw_origin_around_all_pillars(); test_inferno_jal_npcs_use_edge_clearance_at_pillars(); test_step_out_forecast_north_pillar_ranger_mager_order(); - test_step_out_forecast_obs_exposes_compact_action_affordance(); - test_step_out_forecast_obs_can_be_disabled(); - test_step_out_forecast_obs_uses_fast_mode(); test_fast_step_out_forecast_matches_movement_head_destinations(); test_fast_step_out_forecast_immediate_static_threats(); test_fast_step_out_forecast_blob_scan_and_melee_fallback(); @@ -9404,7 +9388,6 @@ int main(void) { test_readonly_step_out_forecast_matches_movement_head_destinations(); test_readonly_step_out_forecast_does_not_mutate_state(); test_readonly_step_out_forecast_pillar_step_out_cases(); - test_step_out_forecast_obs_uses_readonly_mode(); test_readonly_step_out_forecast_stun_countdown(); test_readonly_step_out_forecast_frozen_can_attack(); test_readonly_step_out_forecast_blob_scanned_fire(); @@ -9434,7 +9417,6 @@ int main(void) { test_manual_blood_barrage_can_heal_from_dying_primary(); test_phantom_barrage_close_barrage_timing_cannot_recast(); test_phantom_barrage_does_not_displace_live_obs_slots(); - test_phantom_barrage_targetability_obs_requires_ready_attack(); test_default_autocast_casts_blood_barrage(); test_ice_barrage_success_freezes_target_and_records_spell(); test_inferno_barrage_primes_confliction_and_reuses_double_accuracy(); @@ -9480,10 +9462,10 @@ int main(void) { test_zuk_force_safe_healer_target_mask_clears_stale_target(); test_zuk_spark_render_matches_pending_spark_state(); test_zuk_obs_tracks_shield_and_mager_aggro(); - test_zuk_healer_obs_marks_untagged_healer_pressure(); + test_zuk_healer_obs_exposes_target_category(); test_inferno_obs_target_categories_cover_boss_helpers(); test_zuk_set_obs_los_uses_current_target(); - test_zuk_set_prayer_critical_ignores_shield_target(); + test_zuk_set_threat_ignores_shield_target(); test_fail_fast_boundaries(); test_human_target_and_potion_translation(); test_human_targeting_refreshes_stale_obs_slots(); @@ -9536,7 +9518,6 @@ int main(void) { test_inferno_binding_forwards_joseph_reward_mode(); test_inferno_binding_forwards_safe_healer_target_mask(); test_inferno_binding_forwards_terminal_penalty_toggle(); - test_inferno_binding_forwards_step_out_forecast_obs_toggle(); test_inferno_binding_forwards_loadout_profile_config(); test_inferno_binding_logs_post_healer_set_reward_components(); test_inferno_binding_logs_idle_diagnostics(); diff --git a/ocean/osrs/tests/test_inferno_forecast_exact.c b/ocean/osrs/tests/test_inferno_forecast_exact.c index b4c89e310b..947b51664c 100644 --- a/ocean/osrs/tests/test_inferno_forecast_exact.c +++ b/ocean/osrs/tests/test_inferno_forecast_exact.c @@ -9,8 +9,8 @@ #include "ocean/osrs/encounters/encounter_inferno.h" -#define EXACT_MAGIC "INFEXACTv1" -#define EXACT_VERSION 1u +#define EXACT_MAGIC "INFEXACTv2" +#define EXACT_VERSION 2u #define EXACT_CHUNK_BYTES 65536 #define EXACT_ROLLOUT_STEPS 64 #define EXACT_ENV_SEED 0x01FEC0DEu @@ -21,9 +21,7 @@ typedef struct { uint32_t version; uint32_t state_size; uint32_t forecast_size; - uint32_t forecast_obs_size; uint32_t obs_size; - uint32_t action_features; uint32_t record_count; } InfExactFileHeader; @@ -40,7 +38,6 @@ typedef struct { uint32_t forecast_size; uint64_t state_hash; uint64_t forecast_hash; - uint64_t forecast_obs_hash; uint64_t obs_hash; float reward; } InfExactRecordHeader; @@ -103,9 +100,7 @@ static void exact_writer_open(InfExactWriter* writer, const char* path) { header.version = EXACT_VERSION; header.state_size = (uint32_t)sizeof(InfernoState); header.forecast_size = (uint32_t)sizeof(InfStepOutForecast); - header.forecast_obs_size = INF_STEP_OUT_FORECAST_OBS_SIZE; header.obs_size = INF_NUM_OBS; - header.action_features = INF_STEP_OUT_FORECAST_ACTION_FEATURES; exact_write_all(writer->file, &header, sizeof(header)); } @@ -115,9 +110,7 @@ static void exact_writer_close(InfExactWriter* writer) { header.version = EXACT_VERSION; header.state_size = (uint32_t)sizeof(InfernoState); header.forecast_size = (uint32_t)sizeof(InfStepOutForecast); - header.forecast_obs_size = INF_STEP_OUT_FORECAST_OBS_SIZE; header.obs_size = INF_NUM_OBS; - header.action_features = INF_STEP_OUT_FORECAST_ACTION_FEATURES; header.record_count = writer->record_count; if (fseek(writer->file, 0, SEEK_SET) != 0) { perror("seek inferno exact fixture"); @@ -140,14 +133,10 @@ static void exact_capture( InfernoContext* ctx ) { InfStepOutForecast forecast; - float forecast_obs[INF_STEP_OUT_FORECAST_OBS_SIZE]; float obs[INF_NUM_OBS]; inf_build_step_out_forecast_ctx(s, ctx, &forecast); inf_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); - int forecast_obs_offset = - INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + INF_TOTAL_NPC_OBS_SIZE; - memcpy(forecast_obs, &obs[forecast_obs_offset], sizeof(forecast_obs)); InfExactRecordHeader record = {0}; record.scenario_id = scenario_id; @@ -163,13 +152,11 @@ static void exact_capture( record.forecast_size = (uint32_t)sizeof(forecast); record.state_hash = exact_hash_bytes(s, sizeof(*s)); record.forecast_hash = exact_hash_bytes(&forecast, sizeof(forecast)); - record.forecast_obs_hash = exact_hash_bytes(forecast_obs, sizeof(forecast_obs)); record.obs_hash = exact_hash_bytes(obs, sizeof(obs)); record.reward = inf_get_reward_ctx((EncounterState*)s, (EncounterContext*)ctx); exact_write_all(writer->file, &record, sizeof(record)); exact_write_all(writer->file, &forecast, sizeof(forecast)); - exact_write_all(writer->file, forecast_obs, sizeof(forecast_obs)); exact_write_all(writer->file, obs, sizeof(obs)); exact_write_all(writer->file, s, sizeof(*s)); writer->record_count++; @@ -205,11 +192,6 @@ static void exact_init_state( (EncounterContext*)ctx, "start_wave", public_start_wave); - inf_put_int_ctx( - (EncounterState*)s, - (EncounterContext*)ctx, - "step_out_forecast_obs_mode", - INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT); inf_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); } diff --git a/ocean/osrs/tests/test_inferno_golden.c b/ocean/osrs/tests/test_inferno_golden.c index 45ab2fb1e3..481628d8af 100644 --- a/ocean/osrs/tests/test_inferno_golden.c +++ b/ocean/osrs/tests/test_inferno_golden.c @@ -34,48 +34,109 @@ static inline uint64_t splitmix64(uint64_t* s) { return z ^ (z >> 31); } -static uint64_t run_episode(int start_wave, uint32_t seed, int max_ticks) { +#define TRACE_NPC_SLOTS 32 + +typedef struct { + uint64_t state; + uint64_t reward; + uint64_t mask; + uint64_t terminal; + uint64_t observation; +} TraceHashes; + +static uint64_t hash_core_state(uint64_t h, const InfernoState* s) { + h = fnv_bytes(h, &s->player, sizeof(s->player)); + h = fnv_bytes(h, s->pillars, sizeof(s->pillars)); + h = fnv_bytes(h, s->npcs, TRACE_NPC_SLOTS * sizeof(s->npcs[0])); + h = fnv_bytes(h, &s->player_pending_hits, sizeof(s->player_pending_hits)); + h = fnv_bytes(h, s->pending_sparks, sizeof(s->pending_sparks)); + h = fnv_bytes(h, s->inventory_cells, sizeof(s->inventory_cells)); + h = fnv_bytes(h, s->dead_mobs, sizeof(s->dead_mobs)); + h = fnv_i32(h, s->wave); + return fnv_i32(h, s->tick); +} + +static uint64_t hash_semantic_mask( + uint64_t h, + const InfernoState* s, + const float* mask +) { + int offset = 0; + for (int head = 0; head < INF_NUM_ACTION_HEADS; head++) { + if (head != INF_HEAD_TARGET) { + for (int action = 0; action < INF_ACTION_DIMS[head]; action++) + h = fnv_f32(h, mask[offset + action]); + offset += INF_ACTION_DIMS[head]; + continue; + } + + h = fnv_f32(h, mask[offset]); + for (int npc_idx = 0; npc_idx < TRACE_NPC_SLOTS; npc_idx++) { + int slot = inf_find_target_obs_slot(s, npc_idx); + h = fnv_f32(h, slot >= 0 ? mask[offset + slot + 1] : 0.0f); + } + offset += INF_ACTION_DIMS[head]; + } + return h; +} + +static TraceHashes run_episode(int start_wave, uint32_t seed, int max_ticks) { EncounterState* state = inf_create(); inf_put_int(state, "start_wave", start_wave); inf_reset(state, seed); InfernoState* s = (InfernoState*)state; - static float obs[INF_NUM_OBS]; static float mask[INF_ACTION_MASK_SIZE]; int actions[INF_NUM_ACTION_HEADS]; - - uint64_t arng = ((uint64_t)seed << 20) ^ (uint64_t)(start_wave + 1) ^ 0xD1B54A32D192ED03ULL; - - uint64_t h = FNV_OFFSET; + uint64_t arng = ((uint64_t)seed << 20) ^ (uint64_t)(start_wave + 1) ^ + 0xD1B54A32D192ED03ULL; + TraceHashes hashes = { + .state = FNV_OFFSET, + .reward = FNV_OFFSET, + .mask = FNV_OFFSET, + .terminal = FNV_OFFSET, + .observation = FNV_OFFSET, + }; for (int t = 0; t < max_ticks; t++) { + inf_refresh_current_obs_slots(s); for (int head = 0; head < INF_NUM_ACTION_HEADS; head++) { - int dim = INF_ACTION_DIMS[head]; - actions[head] = (int)(splitmix64(&arng) % (uint64_t)dim); + uint64_t random_value = splitmix64(&arng); + if (head != INF_HEAD_TARGET) { + actions[head] = + (int)(random_value % (uint64_t)INF_ACTION_DIMS[head]); + continue; + } + int npc_idx = (int)(random_value % TRACE_NPC_SLOTS); + int slot = inf_find_target_obs_slot(s, npc_idx); + actions[head] = slot >= 0 ? slot + 1 : 0; } inf_step(state, actions); inf_write_obs(state, obs); inf_write_mask(state, mask); - for (int i = 0; i < INF_NUM_OBS; i++) { - h = fnv_f32(h, obs[i]); - } - for (int i = 0; i < INF_ACTION_MASK_SIZE; i++) { - h = fnv_f32(h, mask[i]); + for (int npc_idx = TRACE_NPC_SLOTS; npc_idx < INF_MAX_NPCS; npc_idx++) { + if (s->npcs[npc_idx].active) { + fprintf(stderr, "golden trace activated NPC slot %d\n", npc_idx); + abort(); + } } - h = fnv_f32(h, s->reward); - h = fnv_i32(h, s->wave); - h = fnv_i32(h, s->tick); - h = fnv_i32(h, s->episode_over); - h = fnv_i32(h, s->winner); + hashes.state = hash_core_state(hashes.state, s); + hashes.reward = fnv_f32(hashes.reward, s->reward); + hashes.mask = hash_semantic_mask(hashes.mask, s, mask); + hashes.terminal = fnv_i32(hashes.terminal, s->episode_over); + hashes.terminal = fnv_i32(hashes.terminal, s->winner); + hashes.terminal = fnv_i32(hashes.terminal, s->wave); + for (int i = 0; i < INF_NUM_OBS; i++) + hashes.observation = fnv_f32(hashes.observation, obs[i]); if (s->episode_over) break; } inf_destroy(state); - return h; + return hashes; } typedef struct { @@ -105,54 +166,113 @@ static const GoldenConfig CONFIGS[] = { #define NUM_CONFIGS ((int)(sizeof(CONFIGS) / sizeof(CONFIGS[0]))) #define EPISODE_TICKS 2000 -static const uint64_t BASELINE[NUM_CONFIGS] = { - 0x2d6c652157e87d57ULL, /* wave1_a */ - 0x84a62101fb408366ULL, /* wave1_b */ - 0x12c1265c673b0418ULL, /* wave1_c */ - 0x045770f82fdf144fULL, /* meleer_a */ - 0x33cdc2618bb61ef1ULL, /* meleer_b */ - 0xd853cb4e587c1a74ULL, /* ranger_a */ - 0xf8048ba7439ccafcULL, /* ranger_b */ - 0x462c210eda8590ecULL, /* mager_a */ - 0xa569619024c1f9fdULL, /* mager_b */ - 0x2a78f20fa4e0e699ULL, /* jad_a */ - 0x5aa060baae4b5f73ULL, /* jad_b */ - 0x107f3a8080b123b5ULL, /* jad_c */ - 0xadcc53c5a3d6444fULL, /* zuk_a */ - 0xdba6ec9381f73491ULL, /* zuk_b */ - 0xb97088c0a1fd42a3ULL, /* zuk_c */ +static const uint64_t EXPECTED_STATE[NUM_CONFIGS] = { + 0x443ad0be6700d3ccULL, 0x17bf6a8136354a13ULL, 0xe847a00300ff389aULL, + 0x24561a696000f6aaULL, 0x29ef822b2984ead4ULL, 0x37cd7f6db4d4a69bULL, + 0x843298acc61300d5ULL, 0x61df8fa3a31ce410ULL, 0xc147b4e946a9ef62ULL, + 0xb0cc89838a07a28eULL, 0x38e2a2100ce6aad0ULL, 0x68c2e25a45de3330ULL, + 0x4276518d9178c88aULL, 0x8c4d39c065b913ffULL, 0xfb06a88261725c65ULL, }; -int main(int argc, char** argv) { - int print_mode = (argc > 1 && strcmp(argv[1], "--print") == 0); +static const uint64_t EXPECTED_REWARD[NUM_CONFIGS] = { + 0x8fb194fc3b735b9aULL, 0x91bc1d01b9d474aaULL, 0xd8288292f3160f5aULL, + 0x1f71cc42bc243e13ULL, 0x9ccbff0cc6408633ULL, 0x49a0f296876aea23ULL, + 0xc4080cd4d9a6eb13ULL, 0x9d01d9da22e0bb93ULL, 0x6c34cdb27c0c1733ULL, + 0x29916e1218e670b3ULL, 0x6494cc221d5e7db3ULL, 0x11a8e2781441fd33ULL, + 0x5d411311ad9cd553ULL, 0xe7f1c65d07654513ULL, 0x47a1f9c7b1d3def3ULL, +}; + +static const uint64_t EXPECTED_MASK[NUM_CONFIGS] = { + 0x07b15753e2efc64aULL, 0x48bb2a98696ebfb3ULL, 0x9787e973169329baULL, + 0xe97fa334ceaf6153ULL, 0xf4239d431bef2aeaULL, 0x0ea03aa9e92854e3ULL, + 0xc2432dc597e3e483ULL, 0xbf39837ae26a6aaaULL, 0x7fc9129b3356ed93ULL, + 0xe54fb95b499c2003ULL, 0xd0bb60198af9b643ULL, 0xa01ad9cfc96241c3ULL, + 0x49744aaffe686a73ULL, 0x5b55145ce7296033ULL, 0xfd42b38660125883ULL, +}; + +static const uint64_t EXPECTED_TERMINAL[NUM_CONFIGS] = { + 0x37cd079d32ad9e72ULL, 0xce955ae918947853ULL, 0x8799762912bdf103ULL, + 0x622fee1940c541abULL, 0x9c2e107bb54a1e8bULL, 0x2629f248900d0043ULL, + 0xc92dd3ae1e3522b2ULL, 0xbb44db79d20b1d81ULL, 0xa10f828fd67b1801ULL, + 0xe2d9ed2b31cf44a1ULL, 0x448b70e96c7c46a1ULL, 0xca591485885081a1ULL, + 0xd05fd043497d9b27ULL, 0xf92b462e21d67e67ULL, 0x12e57f8f9818c547ULL, +}; + +static const uint64_t LEGACY_OBSERVATION[NUM_CONFIGS] = { + 0x03f4682055435366ULL, 0x90b9c6fb55157aacULL, 0x0092dfb263b4768cULL, + 0x3964598751ca95a2ULL, 0x5a8ff64de7cc10feULL, 0xb8a7174941603f51ULL, + 0xb6b6e303a9e7857cULL, 0x792e4afd5831fe70ULL, 0xfd926d25fd8b4258ULL, + 0x7b738c46a083f9c5ULL, 0xcef689a6461bbfafULL, 0xd8e160b35c03dbc0ULL, + 0x2a2f884605e7a640ULL, 0x12fdce41ea3ac43fULL, 0xc8f251c4e9489e7dULL, +}; +static const uint64_t COMPACT_OBSERVATION[NUM_CONFIGS] = { + 0xf6b0c1dd0bcd0caaULL, 0xf6383c0531b500aeULL, 0x02d82f1ec0b39b31ULL, + 0xd2e90acf4a8889e3ULL, 0x92a416e84ff2ffbdULL, 0x24c8a4ade044fffbULL, + 0xe314373c080ea591ULL, 0xc6986cd9d1827d8dULL, 0x63f33b2c81af5dbfULL, + 0x318376fbc0ee582aULL, 0x85ebe0fa55f1f53bULL, 0x905f13ba928c7394ULL, + 0xbf34279c73e947f4ULL, 0xfe224b03850e2f95ULL, 0x72ee3c9abde4352dULL, +}; + +static int check_hash( + const char* config, + const char* component, + uint64_t actual, + uint64_t expected +) { + if (actual == expected) return 0; + printf(" %-12s %-11s got 0x%016llx expected 0x%016llx\n", + config, + component, + (unsigned long long)actual, + (unsigned long long)expected); + return 1; +} + +int main(int argc, char** argv) { + int print_mode = argc > 1 && strcmp(argv[1], "--print") == 0; inf_build_npc_stats(); - printf("inferno golden-master (%d configs, <=%d ticks each)\n\n", - NUM_CONFIGS, EPISODE_TICKS); + printf("inferno semantic golden (%d configs, <=%d ticks each)\n", NUM_CONFIGS, + EPISODE_TICKS); + printf("lineage 59f90b7f4 obs3432 -> compact obs498\n\n"); int failed = 0; for (int c = 0; c < NUM_CONFIGS; c++) { - uint64_t h = run_episode(CONFIGS[c].start_wave, CONFIGS[c].seed, EPISODE_TICKS); + TraceHashes hashes = + run_episode(CONFIGS[c].start_wave, CONFIGS[c].seed, EPISODE_TICKS); if (print_mode) { - printf(" 0x%016llxULL, /* %s */\n", (unsigned long long)h, CONFIGS[c].name); - } else { - int ok = (h == BASELINE[c]); - printf(" %-12s 0x%016llx %s\n", CONFIGS[c].name, - (unsigned long long)h, ok ? "PASS" : "FAIL"); - if (!ok) { - printf(" expected 0x%016llx\n", - (unsigned long long)BASELINE[c]); - failed++; - } + printf("%-12s %016llx %016llx %016llx %016llx %016llx\n", + CONFIGS[c].name, + (unsigned long long)hashes.state, + (unsigned long long)hashes.reward, + (unsigned long long)hashes.mask, + (unsigned long long)hashes.terminal, + (unsigned long long)hashes.observation); + continue; } - } - if (print_mode) { - printf("\npaste the array above into BASELINE[].\n"); - return 0; + int config_failed = 0; + config_failed += check_hash( + CONFIGS[c].name, "state", hashes.state, EXPECTED_STATE[c]); + config_failed += check_hash( + CONFIGS[c].name, "reward", hashes.reward, EXPECTED_REWARD[c]); + config_failed += check_hash( + CONFIGS[c].name, "mask", hashes.mask, EXPECTED_MASK[c]); + config_failed += check_hash( + CONFIGS[c].name, "terminal", hashes.terminal, EXPECTED_TERMINAL[c]); + config_failed += check_hash( + CONFIGS[c].name, "observation", hashes.observation, + COMPACT_OBSERVATION[c]); + if (LEGACY_OBSERVATION[c] == COMPACT_OBSERVATION[c]) { + printf(" %-12s observation lineage did not change\n", CONFIGS[c].name); + config_failed++; + } + if (!config_failed) printf(" %-12s PASS\n", CONFIGS[c].name); + failed += config_failed; } - printf("\n%d/%d configs match baseline\n", NUM_CONFIGS - failed, NUM_CONFIGS); + if (print_mode) return 0; + printf("\n%d component mismatches\n", failed); return failed > 0 ? 1 : 0; } diff --git a/ocean/osrs/tools/gen_colosseum_item_obs_table.c b/ocean/osrs/tools/gen_colosseum_item_obs_table.c deleted file mode 100644 index 98e78c84c1..0000000000 --- a/ocean/osrs/tools/gen_colosseum_item_obs_table.c +++ /dev/null @@ -1,165 +0,0 @@ -/* Emits the colosseum item observation table that colo_ent_gather_inv reads. - * - * src/ocean.cu deliberately sees no OSRS header, so the twelve per-cell features that are a - * pure function of the item cannot be computed there. They are baked here instead, by calling - * the same osrs_write_inventory_cell_affordance_features_compact the observation used to call, - * one row per cell code. - * - * cc -std=c11 -O2 -I. -o /tmp/gen_colo_item_obs \ - * ocean/osrs/tools/gen_colosseum_item_obs_table.c -lm - * /tmp/gen_colo_item_obs ocean/osrs/osrs_colosseum_item_obs_generated.h \ - * ocean/osrs/osrs_colosseum_item_obs_table.inc - * - * test_colosseum_modifiers rebuilds every row in memory and diffs it against the committed - * table, so a stale checkout fails the battery instead of training on wrong observations. - */ - -#include -#include -#include - -#include "ocean/osrs/osrs_inventory_clicks.h" - -/* encounter_init_maxed_player_combat_stats(&s->player, COLO_PLAYER_PRAYER_LEVEL) at reset. - * encounter_colosseum_obs_mask.inc static_asserts both against the encounter's own values. */ -#define GEN_BASE_PRAYER 99 -#define GEN_BASE_LEVEL MAXED_BASE_ATTACK - -/* Frailty rewrites base_hitpoints mid-episode, so hp_heal cannot be baked. It is the reason - * the observation still carries one float per cell. Prove the table is free of it by building - * every row at two hitpoint levels and rejecting any row that moves. */ -#define GEN_PROBE_HITPOINTS_A MAXED_BASE_HITPOINTS -#define GEN_PROBE_HITPOINTS_B 60 - -static void emit_float(FILE* out, float v) { - char buf[64]; - snprintf(buf, sizeof(buf), "%.9g", (double)v); - if (!strpbrk(buf, ".eEnN")) strncat(buf, ".0", sizeof(buf) - strlen(buf) - 1); - fprintf(out, "%sf", buf); -} - -static void build_row(float* row, int code, int base_hitpoints) { - osrs_write_inventory_cell_obs_table_row( - row, code, base_hitpoints, GEN_BASE_PRAYER, GEN_BASE_LEVEL); -} - -static void require_code_round_trip(int code) { - OsrsInventoryCell cell = osrs_inventory_cell_for_obs_code(code); - int back = osrs_inventory_cell_obs_code(cell.item_idx, cell.raw_osrs_id); - if (back == code) return; - fprintf(stderr, - "colosseum item obs table: code %d resolves to item %u raw %u, which codes back as " - "%d. Two codes share one cell content and the table is not a function of the code.\n", - code, cell.item_idx, cell.raw_osrs_id, back); - exit(1); -} - -static void require_hitpoints_free(int code) { - float a[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; - float b[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; - build_row(a, code, GEN_PROBE_HITPOINTS_A); - build_row(b, code, GEN_PROBE_HITPOINTS_B); - - for (int f = 0; f < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; f++) { - if (a[f] == b[f]) continue; - fprintf(stderr, - "colosseum item obs table: code %d feature %d depends on base_hitpoints " - "(%g at %d, %g at %d). Ship it in the observation or key the table on it.\n", - code, f, (double)a[f], GEN_PROBE_HITPOINTS_A, - (double)b[f], GEN_PROBE_HITPOINTS_B); - exit(1); - } -} - -static FILE* open_or_die(const char* path) { - FILE* f = fopen(path, "w"); - if (!f) { - fprintf(stderr, "colosseum item obs table: cannot write %s\n", path); - exit(1); - } - return f; -} - -static void write_header(const char* path) { - FILE* out = open_or_die(path); - fprintf(out, - "/* Generated by ocean/osrs/tools/gen_colosseum_item_obs_table.c. Do not edit.\n" - " * Dependency-free on purpose: src/ocean.cu includes it and no OSRS header. */\n" - "#ifndef OSRS_COLOSSEUM_ITEM_OBS_GENERATED_H\n" - "#define OSRS_COLOSSEUM_ITEM_OBS_GENERATED_H\n" - "\n" - "#define COLO_ITEM_OBS_TABLE_ROWS %d\n" - "#define COLO_ITEM_OBS_TABLE_COLS %d\n" - "#define COLO_ITEM_OBS_TABLE_BASE_PRAYER %d\n" - "#define COLO_ITEM_OBS_TABLE_BASE_LEVEL %d\n" - "\n" - "/* The observation carries code/SCALE; multiply back to index a row. A power of two\n" - " * keeps the round trip exact in bf16 and keeps the code out of the global linear\n" - " * layer's dynamic range. */\n" - "#define COLO_ITEM_OBS_CODE_SCALE %d\n" - "\n" - "/* Slots the observation adds in; the table holds zero at both. */\n" - "#define COLO_ITEM_OBS_OVERLAY_EQUIPPED %d\n" - "#define COLO_ITEM_OBS_OVERLAY_HP_HEAL %d\n" - "\n" - "/* Rows live in osrs_colosseum_item_obs_table.inc so each consumer names its own\n" - " * storage: src/ocean.cu wants a __device__ copy, the viewer wants a host one, and\n" - " * a single guarded definition would hand whichever included first to both. */\n" - "\n" - "#endif\n", - OSRS_INVENTORY_CELL_OBS_CODE_COUNT, - OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT, - GEN_BASE_PRAYER, - GEN_BASE_LEVEL, - OSRS_INVENTORY_CELL_OBS_CODE_SCALE, - OSRS_INVENTORY_CELL_COMPACT_EQUIPPED, - OSRS_INVENTORY_CELL_COMPACT_HP_HEAL); - fclose(out); -} - -static void write_table(const char* path) { - FILE* out = open_or_die(path); - fprintf(out, - "/* Generated by ocean/osrs/tools/gen_colosseum_item_obs_table.c. Do not edit.\n" - " * Included as the initialiser body of a [ROWS][COLS] float array. */\n"); - - for (int code = 0; code < OSRS_INVENTORY_CELL_OBS_CODE_COUNT; code++) { - require_code_round_trip(code); - require_hitpoints_free(code); - - float row[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; - build_row(row, code, GEN_PROBE_HITPOINTS_A); - - fprintf(out, "{"); - for (int f = 0; f < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; f++) { - if (f) fprintf(out, ", "); - emit_float(out, row[f]); - } - fprintf(out, "},\n"); - } - fclose(out); -} - -int main(int argc, char** argv) { - if (argc != 3) { - fprintf(stderr, "usage: %s \n", argv[0]); - return 1; - } - if (OSRS_INVENTORY_CELL_OBS_CODE_COUNT > OSRS_INVENTORY_CELL_OBS_CODE_SCALE) { - fprintf(stderr, - "colosseum item obs table: %d codes exceeds the scale %d; the code/scale round " - "trip is only exact in bf16 while a code fits in the scale's significand\n", - OSRS_INVENTORY_CELL_OBS_CODE_COUNT, OSRS_INVENTORY_CELL_OBS_CODE_SCALE); - return 1; - } - for (int code = 0; code < OSRS_INVENTORY_CELL_OBS_CODE_COUNT; code++) { - int back = osrs_inventory_cell_obs_code_decode( - osrs_inventory_cell_obs_code_encode(code)); - if (back == code) continue; - fprintf(stderr, "colosseum item obs table: code %d encodes back as %d\n", code, back); - return 1; - } - write_header(argv[1]); - write_table(argv[2]); - return 0; -} diff --git a/ocean/osrs/tools/gen_osrs_item_obs_table.c b/ocean/osrs/tools/gen_osrs_item_obs_table.c new file mode 100644 index 0000000000..4bc6800ecc --- /dev/null +++ b/ocean/osrs/tools/gen_osrs_item_obs_table.c @@ -0,0 +1,127 @@ +/* Emits the shared OSRS item observation table consumed by entity encoders. + * + * cc -std=c11 -O2 -I. -o /tmp/gen_osrs_item_obs \ + * ocean/osrs/tools/gen_osrs_item_obs_table.c -lm + * /tmp/gen_osrs_item_obs ocean/osrs/osrs_item_obs_generated.h \ + * ocean/osrs/osrs_item_obs_table.inc + */ + +#include +#include +#include + +#include "ocean/osrs/osrs_inventory_clicks.h" + +#define GEN_BASE_HITPOINTS 99 +#define GEN_BASE_PRAYER 99 +#define GEN_BASE_RANGED 99 + +static void emit_float(FILE* out, float v) { + char buf[64]; + snprintf(buf, sizeof(buf), "%.9g", (double)v); + if (!strpbrk(buf, ".eEnN")) strncat(buf, ".0", sizeof(buf) - strlen(buf) - 1); + fprintf(out, "%sf", buf); +} + +static void build_row(float* row, int code) { + osrs_write_inventory_cell_obs_table_row( + row, code, GEN_BASE_HITPOINTS, GEN_BASE_PRAYER, GEN_BASE_RANGED); +} + +static void require_code_round_trip(int code) { + OsrsInventoryCell cell = osrs_inventory_cell_for_obs_code(code); + int back = osrs_inventory_cell_obs_code(cell.item_idx, cell.raw_osrs_id); + if (back == code) return; + fprintf(stderr, + "OSRS item obs table: code %d resolves to item %u raw %u, which codes back as " + "%d. Two codes share one cell content and the table is not a function of the code.\n", + code, cell.item_idx, cell.raw_osrs_id, back); + exit(1); +} + + +static FILE* open_or_die(const char* path) { + FILE* f = fopen(path, "w"); + if (!f) { + fprintf(stderr, "OSRS item obs table: cannot write %s\n", path); + exit(1); + } + return f; +} + +static void write_header(const char* path) { + FILE* out = open_or_die(path); + fprintf(out, + "/* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit.\n" + " * Dependency-free on purpose: src/ocean.cu includes it and no OSRS header. */\n" + "#ifndef OSRS_ITEM_OBS_GENERATED_H\n" + "#define OSRS_ITEM_OBS_GENERATED_H\n" + "\n" + "#define OSRS_ITEM_OBS_TABLE_ROWS %d\n" + "#define OSRS_ITEM_OBS_TABLE_COLS %d\n" + "#define OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS %d\n" + "#define OSRS_ITEM_OBS_TABLE_BASE_PRAYER %d\n" + "#define OSRS_ITEM_OBS_TABLE_BASE_RANGED %d\n" + "\n" + "#define OSRS_ITEM_OBS_CODE_SCALE %d\n" + "\n" + "#define OSRS_ITEM_OBS_OVERLAY_EQUIPPED %d\n" + "#define OSRS_ITEM_OBS_OVERLAY_HP_HEAL %d\n" + "\n" + "#endif\n", + OSRS_INVENTORY_CELL_OBS_CODE_COUNT, + OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT, + GEN_BASE_HITPOINTS, + GEN_BASE_PRAYER, + GEN_BASE_RANGED, + OSRS_INVENTORY_CELL_OBS_CODE_SCALE, + OSRS_INVENTORY_CELL_COMPACT_EQUIPPED, + OSRS_INVENTORY_CELL_COMPACT_HP_HEAL); + fclose(out); +} + +static void write_table(const char* path) { + FILE* out = open_or_die(path); + fprintf(out, + "/* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit.\n" + " * Included as the initialiser body of a [ROWS][COLS] float array. */\n"); + + for (int code = 0; code < OSRS_INVENTORY_CELL_OBS_CODE_COUNT; code++) { + require_code_round_trip(code); + + float row[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; + build_row(row, code); + + fprintf(out, "{"); + for (int f = 0; f < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; f++) { + if (f) fprintf(out, ", "); + emit_float(out, row[f]); + } + fprintf(out, "},\n"); + } + fclose(out); +} + +int main(int argc, char** argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + if (OSRS_INVENTORY_CELL_OBS_CODE_COUNT > OSRS_INVENTORY_CELL_OBS_CODE_SCALE) { + fprintf(stderr, + "OSRS item obs table: %d codes exceeds the scale %d; the code/scale round " + "trip is only exact in bf16 while a code fits in the scale's significand\n", + OSRS_INVENTORY_CELL_OBS_CODE_COUNT, OSRS_INVENTORY_CELL_OBS_CODE_SCALE); + return 1; + } + for (int code = 0; code < OSRS_INVENTORY_CELL_OBS_CODE_COUNT; code++) { + int back = osrs_inventory_cell_obs_code_decode( + osrs_inventory_cell_obs_code_encode(code)); + if (back == code) continue; + fprintf(stderr, "OSRS item obs table: code %d encodes back as %d\n", code, back); + return 1; + } + write_header(argv[1]); + write_table(argv[2]); + return 0; +} diff --git a/ocean/osrs_colosseum/osrs_colosseum.cu b/ocean/osrs_colosseum/osrs_colosseum.cu index d4703c7e12..1d53451b8d 100644 --- a/ocean/osrs_colosseum/osrs_colosseum.cu +++ b/ocean/osrs_colosseum/osrs_colosseum.cu @@ -7,16 +7,8 @@ // header reaches this TU, so drift is caught only by the _Static_asserts in // osrs_visual.c; update both when the layout changes. // -// The one exception is the item table, which is generated precisely so it can cross this -// boundary with no OSRS dependency. ENV_HEADER is included before this file and also pulls -// the guarded header, so the rows live in a separate .inc and each side names its own -// storage -- a single guarded definition would hand the host copy to the kernel. -#include "../ocean/osrs/osrs_colosseum_item_obs_generated.h" -__device__ static const float COLO_ITEM_OBS_TABLE_DEV - [COLO_ITEM_OBS_TABLE_ROWS][COLO_ITEM_OBS_TABLE_COLS] = { -#include "../ocean/osrs/osrs_colosseum_item_obs_table.inc" -}; +static constexpr int COLO_ENT_OBS_SIZE = 934; static constexpr int COLO_ENT_NPC_START = 130; static constexpr int COLO_ENT_NUM_NPCS = 24; static constexpr int COLO_ENT_FEATS = 34; @@ -33,18 +25,17 @@ static constexpr int COLO_ENT_INV_START = 36; static constexpr int COLO_ENT_INV_NUM_CELLS = 28; static constexpr int COLO_ENT_INV_FEATS = 15; static constexpr int COLO_ENT_INV_PRESENT = 0; +static constexpr int COLO_ENT_INV_IS_ARMOR = 3; +static constexpr int COLO_ENT_INV_IS_WEAPON = 4; static constexpr int COLO_ENT_INV_BOTTLENECK = 16; static constexpr int COLO_ENT_INV_BLOCK = COLO_ENT_INV_NUM_CELLS * COLO_ENT_INV_FEATS; -// Each cell observes an item CODE plus the two features the item does not fix: whether it is -// the piece currently worn in its slot, and how much it heals -- the latter divides by -// base_hitpoints, which the Frailty modifier rewrites mid-episode. colo_ent_gather_inv -// rebuilds the other twelve from the table and adds these two in, so the encoder record -// keeps the width and meaning it had before the recut. +// Each cell observes an item code plus the dynamic equipped and HP-heal fields. +// colo_ent_gather_inv always overwrites equipped and overwrites HP-heal only for non-gear. static constexpr int COLO_ENT_INV_OBS_FEATS = 3; static constexpr int COLO_ENT_INV_OBS_CODE = 0; static constexpr int COLO_ENT_INV_OBS_EQUIPPED = 1; static constexpr int COLO_ENT_INV_OBS_HP_HEAL = 2; -static_assert(COLO_ENT_INV_FEATS == COLO_ITEM_OBS_TABLE_COLS, +static_assert(COLO_ENT_INV_FEATS == OSRS_ITEM_OBS_TABLE_COLS, "encoder record width is one item table row"); struct ColosseumEntityEncoderWeights { @@ -122,13 +113,18 @@ __global__ void colo_ent_gather_inv( const precision_t* src = obs + (int64_t)b * obs_size + COLO_ENT_INV_START + cell * COLO_ENT_INV_OBS_FEATS; int code = (int)lrintf( - to_float(src[COLO_ENT_INV_OBS_CODE]) * (float)COLO_ITEM_OBS_CODE_SCALE); - assert(code >= 0 && code < COLO_ITEM_OBS_TABLE_ROWS); - float v = COLO_ITEM_OBS_TABLE_DEV[code][f]; - if (f == COLO_ITEM_OBS_OVERLAY_EQUIPPED) - v += to_float(src[COLO_ENT_INV_OBS_EQUIPPED]); - if (f == COLO_ITEM_OBS_OVERLAY_HP_HEAL) - v += to_float(src[COLO_ENT_INV_OBS_HP_HEAL]); + to_float(src[COLO_ENT_INV_OBS_CODE]) * (float)OSRS_ITEM_OBS_CODE_SCALE); + assert(code >= 0 && code < OSRS_ITEM_OBS_TABLE_ROWS); + const float* table_row = OSRS_ITEM_OBS_TABLE_DEV[code]; + float v = table_row[f]; + if (f == OSRS_ITEM_OBS_OVERLAY_EQUIPPED) + v = to_float(src[COLO_ENT_INV_OBS_EQUIPPED]); + if (f == OSRS_ITEM_OBS_OVERLAY_HP_HEAL) { + int is_gear = table_row[COLO_ENT_INV_IS_ARMOR] != 0.0f || + table_row[COLO_ENT_INV_IS_WEAPON] != 0.0f; + if (!is_gear) + v = to_float(src[COLO_ENT_INV_OBS_HP_HEAL]); + } inv_flat[idx] = from_float(v); } diff --git a/ocean/osrs_inferno/inferno_profile.h b/ocean/osrs_inferno/inferno_profile.h index fc22626d79..0acb376906 100644 --- a/ocean/osrs_inferno/inferno_profile.h +++ b/ocean/osrs_inferno/inferno_profile.h @@ -17,7 +17,6 @@ X(INF_PROF_OBS_PREFIX, "obs_prefix") \ X(INF_PROF_OBS_REFRESH_SLOTS, "obs_refresh_slots") \ X(INF_PROF_OBS_NPC_SLOTS, "obs_npc_slots") \ - X(INF_PROF_OBS_FORECAST, "obs_forecast") \ X(INF_PROF_OBS_PENDING_HITS, "obs_pending_hits") \ X(INF_PROF_OBS_SPARKS, "obs_sparks") \ X(INF_PROF_FORECAST_LANDING, "forecast_landing") \ diff --git a/ocean/osrs_inferno/osrs_inferno.cu b/ocean/osrs_inferno/osrs_inferno.cu index e366d2d9f4..7eafbf2298 100644 --- a/ocean/osrs_inferno/osrs_inferno.cu +++ b/ocean/osrs_inferno/osrs_inferno.cu @@ -2,26 +2,62 @@ // Included by src/ocean.cu — requires precision_t, Prec, Allocator, puf_mm, etc. // Shares ColosseumEntityEncoderWeights/Activations layout (same weight slots). -static constexpr int INF_ENT_NPC_START = 90; -static constexpr int INF_ENT_NUM_NPCS = 37; -static constexpr int INF_ENT_FEATS = 48; -static constexpr int INF_ENT_TYPE_ONEHOT = 14; -static constexpr int INF_ENT_NPC_BLOCK = INF_ENT_NUM_NPCS * INF_ENT_FEATS; -static constexpr int INF_ENT_INV_START = 2450; -static constexpr int INF_ENT_INV_NUM_CELLS = 28; -static constexpr int INF_ENT_INV_FEATS = 28; -static constexpr int INF_ENT_INV_BLOCK = INF_ENT_INV_NUM_CELLS * INF_ENT_INV_FEATS; -static constexpr int INF_ENT_OBS_SIZE = 3432; - -__global__ void inf_ent_gather( - precision_t* __restrict__ rec_flat, const precision_t* __restrict__ obs, - int B, int obs_size, int start, int block) { +static constexpr int INF_ENT_NPC_START = 54; +static constexpr int INF_ENT_NUM_NPCS = 14; +static constexpr int INF_ENT_OBS_FEATS = 13; +static constexpr int INF_ENT_FEATS = 26; +static constexpr int INF_ENT_TYPE_ONEHOT = 14; +static constexpr int INF_ENT_TYPE_CODE_SCALE = 16; +static constexpr int INF_ENT_NPC_BLOCK = INF_ENT_NUM_NPCS * INF_ENT_FEATS; +static constexpr int INF_ENT_INV_START = 460; +static constexpr int INF_ENT_INV_NUM_CELLS = 28; +static constexpr int INF_ENT_INV_OBS_FEATS = 1; +static constexpr int INF_ENT_INV_FEATS = 15; +static constexpr int INF_ENT_INV_BLOCK = INF_ENT_INV_NUM_CELLS * INF_ENT_INV_FEATS; +static constexpr int INF_ENT_OBS_SIZE = 498; + +static_assert(INF_ENT_FEATS == + INF_ENT_TYPE_ONEHOT + INF_ENT_OBS_FEATS - 1); +static_assert(INF_ENT_INV_FEATS == OSRS_ITEM_OBS_TABLE_COLS); + +__global__ void inf_ent_gather_npcs( + precision_t* __restrict__ npc_flat, const precision_t* __restrict__ obs, + int B, int obs_size) { int idx = blockIdx.x * blockDim.x + threadIdx.x; - int total = B * block; + int total = B * INF_ENT_NPC_BLOCK; if (idx >= total) return; - int b = idx / block; - int off = idx % block; - rec_flat[idx] = obs[(int64_t)b * obs_size + start + off]; + int b = idx / INF_ENT_NPC_BLOCK; + int off = idx % INF_ENT_NPC_BLOCK; + int rec = off / INF_ENT_FEATS; + int f = off - rec * INF_ENT_FEATS; + const precision_t* src = obs + (int64_t)b * obs_size + INF_ENT_NPC_START + + rec * INF_ENT_OBS_FEATS; + if (f < INF_ENT_TYPE_ONEHOT) { + int code = (int)lrintf( + to_float(src[0]) * (float)INF_ENT_TYPE_CODE_SCALE); + assert(code >= 0 && code <= INF_ENT_TYPE_ONEHOT); + npc_flat[idx] = from_float(code == f + 1 ? 1.0f : 0.0f); + } else { + npc_flat[idx] = src[1 + f - INF_ENT_TYPE_ONEHOT]; + } +} + +__global__ void inf_ent_gather_inv( + precision_t* __restrict__ inv_flat, const precision_t* __restrict__ obs, + int B, int obs_size) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + int total = B * INF_ENT_INV_BLOCK; + if (idx >= total) return; + int b = idx / INF_ENT_INV_BLOCK; + int off = idx % INF_ENT_INV_BLOCK; + int cell = off / INF_ENT_INV_FEATS; + int f = off - cell * INF_ENT_INV_FEATS; + const precision_t* src = obs + (int64_t)b * obs_size + INF_ENT_INV_START + + cell * INF_ENT_INV_OBS_FEATS; + int code = (int)lrintf( + to_float(src[0]) * (float)OSRS_ITEM_OBS_CODE_SCALE); + assert(code >= 0 && code < OSRS_ITEM_OBS_TABLE_ROWS); + inv_flat[idx] = from_float(OSRS_ITEM_OBS_TABLE_DEV[code][f]); } static Prec inf_entity_encoder_forward(void* w, void* activations, Prec input, cudaStream_t stream) { @@ -35,8 +71,8 @@ static Prec inf_entity_encoder_forward(void* w, void* activations, Prec input, c puf_mm(&input, &ew->global_w, &a->out, stream); - inf_ent_gather<<>>( - a->npc_flat.data, input.data, B, ew->obs_size, INF_ENT_NPC_START, INF_ENT_NPC_BLOCK); + inf_ent_gather_npcs<<>>( + a->npc_flat.data, input.data, B, ew->obs_size); Prec npc2d = {.data = a->npc_flat.data, .shape = {NB, INF_ENT_FEATS}}; puf_mm(&npc2d, &ew->entity_l1_w, &a->entity_z1, stream); colo_ent_launch_fused_fwd( @@ -46,8 +82,8 @@ static Prec inf_entity_encoder_forward(void* w, void* activations, Prec input, c INF_ENT_TYPE_ONEHOT, stream); int IB = B * INF_ENT_INV_NUM_CELLS; - inf_ent_gather<<>>( - a->inv_flat.data, input.data, B, ew->obs_size, INF_ENT_INV_START, INF_ENT_INV_BLOCK); + inf_ent_gather_inv<<>>( + a->inv_flat.data, input.data, B, ew->obs_size); Prec inv2d = {.data = a->inv_flat.data, .shape = {IB, INF_ENT_INV_FEATS}}; puf_mm(&inv2d, &ew->inv_l1_w, &a->inv_z1, stream); colo_ent_launch_fused_fwd( diff --git a/ocean/osrs_inferno/osrs_inferno.h b/ocean/osrs_inferno/osrs_inferno.h index 63c13c90a5..bc9c0d3adc 100644 --- a/ocean/osrs_inferno/osrs_inferno.h +++ b/ocean/osrs_inferno/osrs_inferno.h @@ -225,8 +225,6 @@ void puf_init(Env* env, Dict* kwargs) { for (size_t k = 0; k < sizeof(int_keys) / sizeof(*int_keys); k++) inferno_env_put_int(env, int_keys[k], (int)dict_get(kwargs, int_keys[k])); - inferno_env_put_int(env, "step_out_forecast_obs_mode", - (int)dict_get(kwargs, "step_out_forecast_obs_mode")); inferno_env_put_int(env, "loadout_profile_mode", (int)dict_get(kwargs, "loadout_profile_mode")); inferno_env_put_int(env, "zuk_healer_reward_mode", @@ -339,12 +337,6 @@ void puf_step(Env* env) { env->log.safe_attack_opportunity_missed_ticks += (float)s->total_safe_attack_opportunity_missed_ticks; env->log.progressless_ticks += (float)s->total_progressless_ticks; - env->log.npc_pressure_if_ready_count += - s->total_npc_pressure_if_ready_count; - env->log.npc_pressure_this_tick_count += - s->total_npc_pressure_this_tick_count; - env->log.npc_pressure_max_incoming_hit += - s->max_npc_pressure_incoming_hit; for (int i = 0; i < OSRS_INFERNO_IDLE_PHASE_COUNT; i++) { env->log.attack_ready_no_attack_ticks_by_phase[i] += (float)s->attack_ready_no_attack_ticks_by_phase[i]; @@ -516,14 +508,6 @@ void puf_log(Log* log, Dict* out) { "progressless_ticks", log->progressless_ticks, log->progressless_ticks_by_phase); - float pressure_denom = log->episode_length > 0.0f - ? log->episode_length : 1.0f; - dict_set(out, "npc_pressure_if_ready_count_per_tick", - log->npc_pressure_if_ready_count / pressure_denom); - dict_set(out, "npc_pressure_this_tick_count_per_tick", - log->npc_pressure_this_tick_count / pressure_denom); - dict_set(out, "npc_pressure_max_incoming_hit", - log->npc_pressure_max_incoming_hit); dict_set(out, "brews_used", log->brews_used); float prayer_rate = (log->prayer_total > 0.0f) diff --git a/src/ocean.cu b/src/ocean.cu index b8d0a803b7..bbc8e1833e 100644 --- a/src/ocean.cu +++ b/src/ocean.cu @@ -19,6 +19,12 @@ void puf_normal_init(Prec* dst, float std, ulong seed, cudaStream_t stream) { #include "../ocean/nmmo3/nmmo3.cu" #include "../ocean/minimal/minimal.cu" + +#include "../ocean/osrs/osrs_item_obs_generated.h" +__device__ static const float OSRS_ITEM_OBS_TABLE_DEV + [OSRS_ITEM_OBS_TABLE_ROWS][OSRS_ITEM_OBS_TABLE_COLS] = { +#include "../ocean/osrs/osrs_item_obs_table.inc" +}; #include "../ocean/osrs_colosseum/osrs_colosseum.cu" #include "../ocean/osrs_inferno/osrs_inferno.cu" #ifdef PUFFER_NETHACK diff --git a/tests/test_osrs_entity_encoders.py b/tests/test_osrs_entity_encoders.py new file mode 100644 index 0000000000..93cbcc657c --- /dev/null +++ b/tests/test_osrs_entity_encoders.py @@ -0,0 +1,329 @@ +import ctypes +import re +import shutil +import subprocess +from dataclasses import dataclass +from pathlib import Path + +import numpy as np +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +CUDA_SOURCE = Path(__file__).with_name("test_osrs_entity_encoders_cuda.cu") +ITEM_HEADER = ROOT / "ocean/osrs/osrs_item_obs_generated.h" +ITEM_TABLE = ROOT / "ocean/osrs/osrs_item_obs_table.inc" +BATCH = 9 +HIDDEN = 40 +BOTTLENECK = 16 + + +@dataclass(frozen=True) +class EncoderContract: + name: str + kind: int + source: Path + prefix: str + obs_size: int + npc_start: int + npc_count: int + npc_obs_features: int + npc_features: int + type_count: int + type_code_scale: int + inventory_start: int + inventory_count: int + inventory_obs_features: int + inventory_features: int + inventory_overlays: bool + + +CONTRACTS = ( + EncoderContract( + "colosseum", 0, ROOT / "ocean/osrs_colosseum/osrs_colosseum.cu", "COLO_ENT", + 934, 130, 24, 23, 34, 12, 1, 36, 28, 3, 15, True, + ), + EncoderContract( + "inferno", 1, ROOT / "ocean/osrs_inferno/osrs_inferno.cu", "INF_ENT", + 498, 54, 14, 13, 26, 14, 16, 460, 28, 1, 15, False, + ), +) + + +def _integer_constant(source: str, name: str): + match = re.search(rf"\b{name}\s*=\s*(\d+)\s*;", source) + return int(match.group(1)) if match else None + + +def _generated_integer(name: str) -> int: + source = ITEM_HEADER.read_text() + match = re.search(rf"^#define\s+{name}\s+(\d+)\s*$", source, re.MULTILINE) + assert match, f"generated item-table constant {name} is missing" + return int(match.group(1)) + + +def _load_item_table() -> np.ndarray: + columns = _generated_integer("OSRS_ITEM_OBS_TABLE_COLS") + rows = [] + for line in ITEM_TABLE.read_text().splitlines(): + if not line.startswith("{"): + continue + values = re.findall(r"-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?", line, re.IGNORECASE) + rows.append([float(value) for value in values]) + table = np.asarray(rows, dtype=np.float32) + assert table.shape == (_generated_integer("OSRS_ITEM_OBS_TABLE_ROWS"), columns) + return table + + +def _source_contract(contract: EncoderContract) -> dict[str, int | None]: + source = contract.source.read_text() + prefix = contract.prefix + values = { + "npc_start": _integer_constant(source, f"{prefix}_NPC_START"), + "npc_count": _integer_constant(source, f"{prefix}_NUM_NPCS"), + "npc_features": _integer_constant(source, f"{prefix}_FEATS"), + "type_count": _integer_constant(source, f"{prefix}_TYPE_ONEHOT"), + "inventory_obs_features": _integer_constant(source, f"{prefix}_INV_OBS_FEATS"), + "inventory_start": _integer_constant(source, f"{prefix}_INV_START"), + "inventory_count": _integer_constant(source, f"{prefix}_INV_NUM_CELLS"), + "inventory_features": _integer_constant(source, f"{prefix}_INV_FEATS"), + } + if contract.kind == 1: + values["obs_size"] = _integer_constant(source, "INF_ENT_OBS_SIZE") + values["npc_obs_features"] = _integer_constant( + source, "INF_ENT_OBS_FEATS", + ) + values["type_code_scale"] = _integer_constant( + source, "INF_ENT_TYPE_CODE_SCALE", + ) + return values + + +def _expected_source_contract(contract: EncoderContract) -> dict[str, int]: + values = { + "npc_start": contract.npc_start, + "npc_count": contract.npc_count, + "npc_features": contract.npc_features, + "type_count": contract.type_count, + "inventory_start": contract.inventory_start, + "inventory_count": contract.inventory_count, + "inventory_obs_features": contract.inventory_obs_features, + "inventory_features": contract.inventory_features, + } + if contract.kind == 1: + values["npc_obs_features"] = contract.npc_obs_features + values["obs_size"] = contract.obs_size + values["type_code_scale"] = contract.type_code_scale + return values + + +def _make_observations(contract: EncoderContract, table_rows: int) -> np.ndarray: + linear = np.arange(BATCH * contract.obs_size, dtype=np.float32).reshape(BATCH, contract.obs_size) + observations = np.sin(linear * np.float32(0.013)) * np.float32(0.01) + + npc_block = contract.npc_count * contract.npc_obs_features + observations[:, contract.npc_start:contract.npc_start + npc_block] = 0 + npc_cells = observations[:, contract.npc_start:contract.npc_start + npc_block].reshape( + BATCH, contract.npc_count, contract.npc_obs_features, + ) + for npc_type in range(contract.type_count): + flat_index = npc_type + 1 + batch, cell = divmod(flat_index, contract.npc_count) + npc_cells[batch, cell, 0] = (npc_type + 1) / contract.type_code_scale + npc_cells[batch, cell, 1:] = np.linspace( + 0.01 * (npc_type + 1), 0.01 * (npc_type + contract.npc_obs_features - 1), + contract.npc_obs_features - 1, dtype=np.float32, + ) + + inventory_block = contract.inventory_count * contract.inventory_obs_features + observations[:, contract.inventory_start:contract.inventory_start + inventory_block] = 0 + inventory_cells = observations[ + :, contract.inventory_start:contract.inventory_start + inventory_block + ].reshape(BATCH, contract.inventory_count, contract.inventory_obs_features) + code_scale = _generated_integer("OSRS_ITEM_OBS_CODE_SCALE") + assert table_rows <= inventory_cells.shape[0] * inventory_cells.shape[1] + for code in range(table_rows): + batch, cell = divmod(code, contract.inventory_count) + inventory_cells[batch, cell, 0] = code / code_scale + if contract.inventory_overlays and code: + inventory_cells[batch, cell, 1] = float(code & 1) + inventory_cells[batch, cell, 2] = (code % 17) / 20.0 + return observations + + +def _materialize(contract: EncoderContract, observations: np.ndarray, table: np.ndarray): + npc_source = observations[ + :, contract.npc_start: + contract.npc_start + contract.npc_count * contract.npc_obs_features + ].reshape(BATCH, contract.npc_count, contract.npc_obs_features) + npc_records = np.zeros( + (BATCH, contract.npc_count, contract.npc_features), dtype=np.float32, + ) + npc_codes = np.rint(npc_source[:, :, 0] * contract.type_code_scale).astype(np.int64) + for npc_type in range(contract.type_count): + npc_records[:, :, npc_type] = npc_codes == npc_type + 1 + npc_records[:, :, contract.type_count:] = npc_source[:, :, 1:] + + inventory_source = observations[ + :, contract.inventory_start: + contract.inventory_start + contract.inventory_count * contract.inventory_obs_features + ].reshape(BATCH, contract.inventory_count, contract.inventory_obs_features) + code_scale = _generated_integer("OSRS_ITEM_OBS_CODE_SCALE") + inventory_codes = np.rint(inventory_source[:, :, 0] * code_scale).astype(np.int64) + assert np.all((inventory_codes >= 0) & (inventory_codes < table.shape[0])) + inventory_records = table[inventory_codes].copy() + if contract.inventory_overlays: + equipped = _generated_integer("OSRS_ITEM_OBS_OVERLAY_EQUIPPED") + hp_heal = _generated_integer("OSRS_ITEM_OBS_OVERLAY_HP_HEAL") + inventory_records[:, :, equipped] = inventory_source[:, :, 1] + gear = (inventory_records[:, :, 3] != 0) | (inventory_records[:, :, 4] != 0) + inventory_records[:, :, hp_heal] = np.where( + gear, inventory_records[:, :, hp_heal], inventory_source[:, :, 2], + ) + return npc_records, inventory_records, npc_codes, inventory_codes + + +@pytest.mark.parametrize("contract", CONTRACTS, ids=lambda contract: contract.name) +def test_cpu_materialization_contract(contract): + + table = _load_item_table() + observations = _make_observations(contract, table.shape[0]) + npc_records, inventory_records, npc_codes, inventory_codes = _materialize( + contract, observations, table, + ) + assert npc_records.shape == (BATCH, contract.npc_count, contract.npc_features) + assert inventory_records.shape == (BATCH, contract.inventory_count, contract.inventory_features) + assert set(npc_codes.ravel()) == set(range(contract.type_count + 1)) + assert set(inventory_codes.ravel()) == set(range(table.shape[0])) + assert np.count_nonzero(npc_records[npc_codes == 0]) == 0 + assert np.count_nonzero(inventory_records[inventory_codes == 0]) == 0 + assert BATCH * contract.npc_count * contract.npc_features > 256 + assert BATCH * contract.npc_count * contract.npc_features % 256 != 0 + assert BATCH * contract.inventory_count * contract.inventory_features > 256 + assert BATCH * contract.inventory_count * contract.inventory_features % 256 != 0 + actual = _source_contract(contract) + expected = _expected_source_contract(contract) + stale = {name: (actual[name], value) for name, value in expected.items() if actual[name] != value} + assert not stale, f"{contract.name} entity encoder contract is stale: {stale}" + + +def _torch_reference(torch, functional, contract, observations, table, weights): + npc_records, inventory_records, _, _ = _materialize( + contract, observations.detach().cpu().numpy(), table, + ) + npc = torch.from_numpy(npc_records).to(observations.device) + inventory = torch.from_numpy(inventory_records).to(observations.device) + global_w, entity_l1_w, entity_l2_w, inventory_l1_w, inventory_l2_w = weights + + def branch(records, l1_weight, l2_weight, active_width): + hidden = functional.gelu(functional.linear(records, l1_weight), approximate="tanh") + values = functional.linear(hidden, l2_weight) + active = records[:, :, :active_width].sum(dim=2) > 0 + pooled = values.masked_fill(~active.unsqueeze(2), -torch.inf).amax(dim=1) + return torch.where(active.any(dim=1, keepdim=True), pooled, torch.zeros_like(pooled)) + + return ( + functional.linear(observations, global_w) + + branch(npc, entity_l1_w, entity_l2_w, contract.type_count) + + branch(inventory, inventory_l1_w, inventory_l2_w, 1) + ) + + +def _configure_library(path: Path): + library = ctypes.CDLL(path) + pointer = ctypes.c_void_p + library.osrs_entity_test_contract.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_int)] + library.osrs_entity_test_init.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int] + library.osrs_entity_test_init.restype = ctypes.c_int + library.osrs_entity_test_set_weights.argtypes = [pointer] * 5 + library.osrs_entity_test_forward.argtypes = [pointer, pointer, ctypes.c_int, ctypes.c_int] + library.osrs_entity_test_backward.argtypes = [pointer, ctypes.c_int, ctypes.c_int] + library.osrs_entity_test_get_grad.argtypes = [ctypes.c_int, pointer] + return library + + +@pytest.fixture(scope="session") +def cuda_library(tmp_path_factory): + nvcc = shutil.which("nvcc") + if nvcc is None: + pytest.skip("nvcc is not installed") + raylib_include = next(ROOT.glob("raylib-*/include")) + raylib_archive = raylib_include.parent / "lib/libraylib.a" + pytest.importorskip("torch") + library_path = tmp_path_factory.mktemp("osrs_entity_encoder") / "osrs_entity_encoder.so" + subprocess.run( + [ + nvcc, "-shared", "-o", str(library_path), str(CUDA_SOURCE), + "-I", str(ROOT / "src"), "-I", str(raylib_include), + "-Xlinker", "--no-as-needed", + "-lcublas", "-lcudnn", "-lcurand", "-lnccl", "-lnvidia-ml", "-lcusolver", + str(raylib_archive), + "-lGL", "-lm", "-lpthread", "-ldl", "-lrt", + "--compiler-options", "-fPIC", "-Xcompiler", "-O2", + ], + cwd=ROOT, + check=True, + ) + return _configure_library(library_path) + + +def _pointer(tensor): + return ctypes.c_void_p(tensor.data_ptr()) + + +@pytest.mark.parametrize("contract", CONTRACTS, ids=lambda contract: contract.name) +def test_cuda_forward_and_all_weight_gradients(cuda_library, contract): + torch = pytest.importorskip("torch") + functional = pytest.importorskip("torch.nn.functional") + if not torch.cuda.is_available(): + pytest.skip("CUDA is not available") + + reported = (ctypes.c_int * 8)() + cuda_library.osrs_entity_test_contract(contract.kind, reported) + assert list(reported) == [ + contract.obs_size, contract.npc_start, contract.npc_count, contract.npc_features, + contract.type_count, contract.inventory_start, contract.inventory_count, + contract.inventory_features, + ] + assert cuda_library.osrs_entity_test_init( + contract.kind, BATCH, contract.obs_size, HIDDEN, + ) == 0 + + table = _load_item_table() + observations_np = _make_observations(contract, table.shape[0]) + observations = torch.from_numpy(observations_np).cuda() + generator = torch.Generator(device="cuda").manual_seed(6100 + contract.kind) + shapes = ( + (HIDDEN, contract.obs_size), + (BOTTLENECK, contract.npc_features), + (HIDDEN, BOTTLENECK), + (BOTTLENECK, contract.inventory_features), + (HIDDEN, BOTTLENECK), + ) + weights = [ + (torch.randn(shape, generator=generator, device="cuda") * 0.05).requires_grad_() + for shape in shapes + ] + cuda_library.osrs_entity_test_set_weights(*[_pointer(weight) for weight in weights]) + + cuda_output = torch.empty(BATCH, HIDDEN, device="cuda") + cuda_library.osrs_entity_test_forward( + _pointer(cuda_output), _pointer(observations), BATCH, contract.obs_size, + ) + reference_output = _torch_reference( + torch, functional, contract, observations, table, weights, + ) + torch.testing.assert_close(cuda_output, reference_output, atol=3e-4, rtol=3e-4) + + output_gradient = torch.randn( + (BATCH, HIDDEN), generator=generator, device="cuda", + ) + reference_output.backward(output_gradient) + cuda_library.osrs_entity_test_backward(_pointer(output_gradient), BATCH, HIDDEN) + torch.cuda.synchronize() + + for index, weight in enumerate(weights): + cuda_gradient = torch.empty_like(weight) + cuda_library.osrs_entity_test_get_grad(index, _pointer(cuda_gradient)) + torch.cuda.synchronize() + torch.testing.assert_close(cuda_gradient, weight.grad, atol=5e-3, rtol=5e-3) diff --git a/tests/test_osrs_entity_encoders_cuda.cu b/tests/test_osrs_entity_encoders_cuda.cu new file mode 100644 index 0000000000..f85b2779e4 --- /dev/null +++ b/tests/test_osrs_entity_encoders_cuda.cu @@ -0,0 +1,120 @@ +#define PRECISION_FLOAT +#define ENV_HEADER "../ocean/minimal/minimal.h" +#define PUFFER_ENV_NAME "minimal" +#include "../src/pufferl.cu" + +extern "C" { + +static Encoder test_encoder; +static ColosseumEntityEncoderWeights* test_weights; +static ColosseumEntityEncoderActivations* test_activations; +static Allocator test_params; +static Allocator test_acts; +static Allocator test_grads; +static bool test_cublas_initialized; + +static void test_free_allocator(Allocator* allocator) { + if (allocator->mem) cudaFree(allocator->mem); + free(allocator->regs); + *allocator = {}; +} + +static void test_reset() { + test_free_allocator(&test_params); + test_free_allocator(&test_acts); + test_free_allocator(&test_grads); + free(test_weights); + free(test_activations); + test_weights = nullptr; + test_activations = nullptr; +} + +void osrs_entity_test_contract(int kind, int* values) { + if (kind == 0) { + values[0] = COLO_ENT_OBS_SIZE; + values[1] = COLO_ENT_NPC_START; + values[2] = COLO_ENT_NUM_NPCS; + values[3] = COLO_ENT_FEATS; + values[4] = COLO_ENT_TYPE_ONEHOT; + values[5] = COLO_ENT_INV_START; + values[6] = COLO_ENT_INV_NUM_CELLS; + values[7] = COLO_ENT_INV_FEATS; + return; + } + values[0] = INF_ENT_OBS_SIZE; + values[1] = INF_ENT_NPC_START; + values[2] = INF_ENT_NUM_NPCS; + values[3] = INF_ENT_FEATS; + values[4] = INF_ENT_TYPE_ONEHOT; + values[5] = INF_ENT_INV_START; + values[6] = INF_ENT_INV_NUM_CELLS; + values[7] = INF_ENT_INV_FEATS; +} + +int osrs_entity_test_init(int kind, int batch, int obs_size, int hidden) { + if (kind == 0 && obs_size != COLO_ENT_OBS_SIZE) return 1; + if (kind == 1 && obs_size != INF_ENT_OBS_SIZE) return 1; + test_reset(); + if (!test_cublas_initialized) { + cublas_init_handle(); + test_cublas_initialized = true; + } + test_encoder = {}; + test_encoder.in_dim = obs_size; + test_encoder.out_dim = hidden; + create_custom_encoder(kind == 0 ? "osrs_colosseum" : "osrs_inferno", &test_encoder); + test_weights = (ColosseumEntityEncoderWeights*)test_encoder.create_weights(&test_encoder); + test_encoder.reg_params(test_weights, &test_params); + alloc_create(&test_params); + test_activations = (ColosseumEntityEncoderActivations*)calloc(1, test_encoder.activation_size); + test_encoder.reg_train(test_weights, test_activations, &test_acts, &test_grads, batch); + alloc_create(&test_acts); + alloc_create(&test_grads); + return 0; +} + +void osrs_entity_test_set_weights( + void* global_w, void* entity_l1_w, void* entity_l2_w, + void* inv_l1_w, void* inv_l2_w) { + Prec* dst[] = { + &test_weights->global_w, + &test_weights->entity_l1_w, + &test_weights->entity_l2_w, + &test_weights->inv_l1_w, + &test_weights->inv_l2_w, + }; + void* src[] = {global_w, entity_l1_w, entity_l2_w, inv_l1_w, inv_l2_w}; + for (int i = 0; i < 5; i++) { + cudaMemcpy(dst[i]->data, src[i], numel(dst[i]->shape) * sizeof(float), + cudaMemcpyDeviceToDevice); + } +} + +void osrs_entity_test_forward(void* output, void* observations, int batch, int obs_size) { + Prec input = {.data = (precision_t*)observations, .shape = {batch, obs_size}}; + Prec result = test_encoder.forward(test_weights, test_activations, input, 0); + cudaMemcpy(output, result.data, (int64_t)batch * test_encoder.out_dim * sizeof(float), + cudaMemcpyDeviceToDevice); + cudaDeviceSynchronize(); +} + +void osrs_entity_test_backward(void* grad, int batch, int hidden) { + Prec output_grad = {.data = (precision_t*)grad, .shape = {batch, hidden}}; + test_encoder.backward(test_weights, test_activations, output_grad, 0); + cudaDeviceSynchronize(); +} + +void osrs_entity_test_get_grad(int index, void* output) { + Prec* grads[] = { + &test_activations->global_wgrad, + &test_activations->entity_l1_wgrad, + &test_activations->entity_l2_wgrad, + &test_activations->inv_l1_wgrad, + &test_activations->inv_l2_wgrad, + }; + Prec* grad = grads[index]; + cudaMemcpy(output, grad->data, numel(grad->shape) * sizeof(float), + cudaMemcpyDeviceToDevice); +} + +} From bc66bc78da8a9ea4e1bbd49907956cc794be1cd1 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Tue, 11 Aug 2026 11:52:02 +0300 Subject: [PATCH 04/50] reuse OSRS mask and run-path classification --- .../encounter_colosseum_mask_render.inc | 2 +- .../inferno/encounter_inferno_obs_mask.inc | 76 ++++++++++++------- ocean/osrs/osrs_encounter.h | 52 +++++++++---- ocean/osrs/osrs_inventory_clicks.h | 66 +++++++++------- ocean/osrs/osrs_pathfinding.h | 2 + ocean/osrs/tests/test_osrs_inventory_clicks.c | 15 ++++ 6 files changed, 142 insertions(+), 71 deletions(-) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc index 4cd0c9b225..add0260e9f 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc @@ -28,7 +28,7 @@ static void col_write_mask_ctx(EncounterState* state, EncounterContext* context, cell_can_eat[cell] = 0; cell_can_drink[cell] = 0; OsrsInventoryClickResolution r = - osrs_inventory_cell_click_interpret(&s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); + osrs_inventory_cell_click_classify(&s->inventory_cells[cell]); if (r.click_action == OSRS_CLICK_EQUIP) { if (col_can_equip_from_cell(s, cell)) cell_equip_slot[cell] = osrs_item_gear_slot(s->inventory_cells[cell].item_idx); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc index 6e6f71853d..f16e7f6b70 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -118,32 +118,48 @@ static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoCont int obs_slots[INF_OBS_NPCS]; for (int slot = 0; slot < INF_OBS_NPCS; slot++) obs_slots[slot] = -1; + int type_base[INF_NUM_NPC_TYPES] = {0}; + int type_cap[INF_NUM_NPC_TYPES] = {0}; int slot_counts[INF_NUM_NPC_TYPES] = {0}; - int next_slot = 0; + int bucket_capacity = 0; + for (int layout_idx = 0; layout_idx < INF_NUM_NPC_TYPES; layout_idx++) { + InfNPCType type = INF_OBS_SLOT_LAYOUT[layout_idx].type; + type_base[type] = bucket_capacity; + type_cap[type] = INF_OBS_SLOT_LAYOUT[layout_idx].count; + bucket_capacity += type_cap[type]; + } + assert(bucket_capacity <= INF_MAX_NPCS); + int next_slot = 0; for (int pass = 0; pass < 2; pass++) { - for (int layout_idx = 0; layout_idx < INF_NUM_NPC_TYPES; layout_idx++) { - InfNPCType type = INF_OBS_SLOT_LAYOUT[layout_idx].type; - int type_cap = INF_OBS_SLOT_LAYOUT[layout_idx].count; + int pass_counts[INF_NUM_NPC_TYPES] = {0}; + int pass_slots[INF_MAX_NPCS]; + for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { + InfNPC* npc = &s->npcs[npc_idx]; + if (!npc->active || npc->type < 0 || npc->type >= INF_NUM_NPC_TYPES) + continue; - for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { - InfNPC* npc = &s->npcs[npc_idx]; - if (!npc->active || npc->type != type) continue; + InfNPCType type = npc->type; + int eligible = pass == 0 + ? npc->death_ticks == 0 + : npc->death_ticks > 0 && + inf_npc_is_phantom_barrage_obs_candidate(s, ctx, npc_idx); + if (!eligible || slot_counts[type] >= type_cap[type]) continue; - int eligible = pass == 0 - ? npc->death_ticks == 0 - : npc->death_ticks > 0 && - inf_npc_is_phantom_barrage_obs_candidate(s, ctx, npc_idx); - if (!eligible || slot_counts[type] >= type_cap) continue; + pass_slots[type_base[type] + pass_counts[type]++] = npc_idx; + slot_counts[type]++; + } + for (int layout_idx = 0; layout_idx < INF_NUM_NPC_TYPES; layout_idx++) { + InfNPCType type = INF_OBS_SLOT_LAYOUT[layout_idx].type; + for (int type_slot = 0; type_slot < pass_counts[type]; type_slot++) { if (next_slot >= INF_OBS_NPCS) { fprintf(stderr, "inferno: observation target capacity exceeded at %d slots\n", INF_OBS_NPCS); abort(); } - obs_slots[next_slot++] = npc_idx; - slot_counts[type]++; + obs_slots[next_slot++] = pass_slots[type_base[type] + type_slot]; } } } @@ -326,24 +342,30 @@ static void inf_write_obs_ctx( INF_PROFILE_MARK(INF_PROF_OBS_PENDING_HITS); #endif - int used_sparks[INF_MAX_PENDING_SPARKS] = {0}; - for (int slot_idx = 0; slot_idx < INF_SPARK_OBS_SLOTS; slot_idx++) { - int best = -1; - for (int spark_idx = 0; spark_idx < INF_MAX_PENDING_SPARKS; spark_idx++) { - if (used_sparks[spark_idx] || !s->pending_sparks[spark_idx].active) - continue; - if (best < 0 || inf_pending_spark_obs_less( + int spark_order[INF_MAX_PENDING_SPARKS]; + int spark_count = 0; + for (int spark_idx = 0; spark_idx < INF_MAX_PENDING_SPARKS; spark_idx++) { + if (!s->pending_sparks[spark_idx].active) continue; + int order_idx = spark_count; + while (order_idx > 0) { + int previous_idx = spark_order[order_idx - 1]; + if (!inf_pending_spark_obs_less( s, &s->pending_sparks[spark_idx], spark_idx, - &s->pending_sparks[best], best)) - best = spark_idx; + &s->pending_sparks[previous_idx], previous_idx)) + break; + spark_order[order_idx] = previous_idx; + order_idx--; } + spark_order[order_idx] = spark_idx; + spark_count++; + } - if (best < 0) { + for (int slot_idx = 0; slot_idx < INF_SPARK_OBS_SLOTS; slot_idx++) { + if (slot_idx >= spark_count) { i += INF_FEATURES_PER_SPARK; continue; } - const InfPendingSpark* spark = &s->pending_sparks[best]; - used_sparks[best] = 1; + const InfPendingSpark* spark = &s->pending_sparks[spark_order[slot_idx]]; obs[i++] = (float)(spark->x - px) / (float)INF_ARENA_WIDTH; obs[i++] = (float)(spark->y - py) / (float)INF_ARENA_HEIGHT; obs[i++] = (float)spark->ticks_remaining / 10.0f; @@ -492,7 +514,7 @@ static void inf_write_mask_ctx( cell_can_drink[c] = 0; const OsrsInventoryCell* cell = &s->inventory_cells[c]; OsrsInventoryClickResolution resolution = - osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); + osrs_inventory_cell_click_classify(cell); switch (resolution.click_action) { case OSRS_CLICK_EQUIP: if (osrs_can_equip_from_cell(&s->player, s->inventory_cells, c)) diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index 7178202648..fa0acc5e85 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -1766,6 +1766,8 @@ static inline PathResult encounter_pathfind_arena_attack_approach( int cur_x = selected_x; int cur_y = selected_y; + int child_x = cur_x; + int child_y = cur_y; if (selected_x < 0) goto approach_done; result.found = 1; @@ -1786,9 +1788,15 @@ static inline PathResult encounter_pathfind_arena_attack_approach( if (prev_x == local_src_x && prev_y == local_src_y) { result.next_dx = cur_x - local_src_x; result.next_dy = cur_y - local_src_y; + if (child_x != cur_x || child_y != cur_y) { + result.run_dx = child_x - cur_x; + result.run_dy = child_y - cur_y; + } goto approach_done; } + child_x = cur_x; + child_y = cur_y; cur_x = prev_x; cur_y = prev_y; if (APPROACH_VIA(cur_x, cur_y) == VIA_NONE || @@ -1895,26 +1903,42 @@ static inline int encounter_chase_attack_target( } } + int cached_run_dx = 0; + int cached_run_dy = 0; int steps = 0; for (int step = 0; step < 2; step++) { if (encounter_player_can_attack(p->x, p->y, target_x, target_y, target_size, attack_range, los_query)) break; - PathResult pr = (arena_w > 0) - ? encounter_pathfind_arena_attack_approach( - cmap, world_offset_x, world_offset_y, - p->x, p->y, - target_x, target_y, target_size, attack_range, - is_walkable, ctx, - extra_blocked, blocked_ctx, - los_query, - arena_base_x, arena_base_y, arena_w, arena_h) - : encounter_pathfind(cmap, world_offset_x, world_offset_y, - p->x, p->y, cx, cy, - extra_blocked, blocked_ctx); - if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) break; - int nx = p->x + pr.next_dx, ny = p->y + pr.next_dy; + int next_dx; + int next_dy; + if (step == 1 && arena_w > 0 && + (cached_run_dx != 0 || cached_run_dy != 0)) { + next_dx = cached_run_dx; + next_dy = cached_run_dy; + } else { + PathResult pr = (arena_w > 0) + ? encounter_pathfind_arena_attack_approach( + cmap, world_offset_x, world_offset_y, + p->x, p->y, + target_x, target_y, target_size, attack_range, + is_walkable, ctx, + extra_blocked, blocked_ctx, + los_query, + arena_base_x, arena_base_y, arena_w, arena_h) + : encounter_pathfind(cmap, world_offset_x, world_offset_y, + p->x, p->y, cx, cy, + extra_blocked, blocked_ctx); + if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) break; + next_dx = pr.next_dx; + next_dy = pr.next_dy; + if (step == 0) { + cached_run_dx = pr.run_dx; + cached_run_dy = pr.run_dy; + } + } + int nx = p->x + next_dx, ny = p->y + next_dy; if (!is_walkable(ctx, nx, ny)) break; p->x = nx; p->y = ny; steps++; diff --git a/ocean/osrs/osrs_inventory_clicks.h b/ocean/osrs/osrs_inventory_clicks.h index 919457f3a1..9f098b1e05 100644 --- a/ocean/osrs/osrs_inventory_clicks.h +++ b/ocean/osrs/osrs_inventory_clicks.h @@ -514,26 +514,10 @@ static inline uint16_t osrs_consumable_raw_osrs_id_after_drink(uint16_t raw_osrs return 0; } -static inline OsrsInventoryClickResolution osrs_inventory_click_interpret( +static inline OsrsInventoryClickResolution osrs_inventory_click_classify( uint8_t item_idx, - uint16_t raw_osrs_id, - OsrsClickTickMultiplicity tick_multiplicity + uint16_t raw_osrs_id ) { - OsrsInventoryClickResolution none_resolution = { - .click_action = OSRS_CLICK_NONE, - .consumable_kind = OSRS_CONSUMABLE_NONE, - .dose_count = 0, - .raw_osrs_id_after_drink = 0, - }; - switch (tick_multiplicity) { - case OSRS_CLICK_TICK_DUPLICATE: - return none_resolution; - case OSRS_CLICK_TICK_FIRST: - break; - default: - osrs_inventory_clicks_trap(); - } - if (item_idx != ITEM_NONE) { OsrsClickAction action = osrs_item_click_action(item_idx); if (raw_osrs_id != 0 && raw_osrs_id != ITEM_DATABASE[item_idx].item_id) { @@ -541,32 +525,56 @@ static inline OsrsInventoryClickResolution osrs_inventory_click_interpret( } return (OsrsInventoryClickResolution){ .click_action = action, - .consumable_kind = OSRS_CONSUMABLE_NONE, - .dose_count = 0, - .raw_osrs_id_after_drink = 0, }; } if (raw_osrs_id == 0) { - return none_resolution; + return (OsrsInventoryClickResolution){ + .click_action = OSRS_CLICK_NONE, + }; } OsrsConsumableClick consumable = osrs_consumable_click_lookup_raw_osrs_id(raw_osrs_id); - uint16_t after_drink = 0; - if (consumable.click_action == OSRS_CLICK_DRINK && - consumable.dose_count > 0) { - after_drink = osrs_consumable_raw_osrs_id_after_drink(raw_osrs_id); - } - return (OsrsInventoryClickResolution){ .click_action = consumable.click_action, .consumable_kind = consumable.consumable_kind, .dose_count = consumable.dose_count, - .raw_osrs_id_after_drink = after_drink, }; } +static inline OsrsInventoryClickResolution osrs_inventory_click_interpret( + uint8_t item_idx, + uint16_t raw_osrs_id, + OsrsClickTickMultiplicity tick_multiplicity +) { + switch (tick_multiplicity) { + case OSRS_CLICK_TICK_DUPLICATE: + return (OsrsInventoryClickResolution){ + .click_action = OSRS_CLICK_NONE, + }; + case OSRS_CLICK_TICK_FIRST: + break; + default: + osrs_inventory_clicks_trap(); + } + + OsrsInventoryClickResolution resolution = + osrs_inventory_click_classify(item_idx, raw_osrs_id); + if (resolution.click_action == OSRS_CLICK_DRINK && + resolution.dose_count > 0) { + resolution.raw_osrs_id_after_drink = + osrs_consumable_raw_osrs_id_after_drink(raw_osrs_id); + } + return resolution; +} + +static inline OsrsInventoryClickResolution osrs_inventory_cell_click_classify( + const OsrsInventoryCell* cell +) { + return osrs_inventory_click_classify(cell->item_idx, cell->raw_osrs_id); +} + static inline OsrsInventoryClickResolution osrs_inventory_cell_click_interpret( const OsrsInventoryCell* cell, OsrsClickTickMultiplicity tick_multiplicity diff --git a/ocean/osrs/osrs_pathfinding.h b/ocean/osrs/osrs_pathfinding.h index c581d27630..936bde78f3 100644 --- a/ocean/osrs/osrs_pathfinding.h +++ b/ocean/osrs/osrs_pathfinding.h @@ -37,6 +37,8 @@ typedef struct { int next_dy; int dest_x; int dest_y; + int run_dx; + int run_dy; } PathResult; typedef int (*pathfind_blocked_fn)(void* ctx, int abs_x, int abs_y); diff --git a/ocean/osrs/tests/test_osrs_inventory_clicks.c b/ocean/osrs/tests/test_osrs_inventory_clicks.c index 7df12bb975..b8e291a521 100644 --- a/ocean/osrs/tests/test_osrs_inventory_clicks.c +++ b/ocean/osrs/tests/test_osrs_inventory_clicks.c @@ -114,6 +114,20 @@ static int test_pure_click_interpreter(void) { return 0; } +static int test_cell_click_classification(void) { + OsrsInventoryCell brew = osrs_inventory_cell_from_raw_osrs_id(6685); + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_classify(&brew); + CHECK("brew classification resolves drink", + resolution.click_action == OSRS_CLICK_DRINK); + CHECK("brew classification preserves kind", + resolution.consumable_kind == OSRS_CONSUMABLE_BREW); + CHECK("brew classification preserves dose", resolution.dose_count == 4); + CHECK("classification skips post-drink mutation", + resolution.raw_osrs_id_after_drink == 0); + return 0; +} + static int test_cell_click_attributes_to_slot_item(void) { OsrsInventoryCell cells[OSRS_INVENTORY_SIZE]; for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) @@ -333,6 +347,7 @@ int main(void) { if (test_sara_brew_dose_variants()) return 1; if (test_dose_after_drink()) return 1; if (test_pure_click_interpreter()) return 1; + if (test_cell_click_classification()) return 1; if (test_cell_click_attributes_to_slot_item()) return 1; if (test_cell_drink_decrements_one_dose()) return 1; if (test_shared_drink_consume_owns_timer_and_one_dose()) return 1; From f548d779a4788f03be3a064b02f362e9f2edb869 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Tue, 11 Aug 2026 12:05:10 +0300 Subject: [PATCH 05/50] preserve fallback chase routing --- ocean/osrs/osrs_encounter.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index fa0acc5e85..9cb988d147 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -1657,6 +1657,7 @@ static inline PathResult encounter_pathfind_arena_attack_approach( int selected_x = -1; int selected_y = -1; + int selected_is_goal = 0; int min_explored_x = local_src_x; int min_explored_y = local_src_y; int max_explored_x = local_src_x; @@ -1685,6 +1686,7 @@ static inline PathResult encounter_pathfind_arena_attack_approach( attack_range, los_query))) { selected_x = cur_x; selected_y = cur_y; + selected_is_goal = 1; break; } @@ -1788,7 +1790,7 @@ static inline PathResult encounter_pathfind_arena_attack_approach( if (prev_x == local_src_x && prev_y == local_src_y) { result.next_dx = cur_x - local_src_x; result.next_dy = cur_y - local_src_y; - if (child_x != cur_x || child_y != cur_y) { + if (selected_is_goal) { result.run_dx = child_x - cur_x; result.run_dy = child_y - cur_y; } From 0d70f07224ec13d1e360a1bd18fd1cd4c9e3123d Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Tue, 11 Aug 2026 12:16:51 +0300 Subject: [PATCH 06/50] validate cached chase steps --- .../inferno/encounter_inferno_obs_mask.inc | 3 +- ocean/osrs/osrs_encounter.h | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc index f16e7f6b70..9e21cdced0 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -128,12 +128,11 @@ static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoCont type_cap[type] = INF_OBS_SLOT_LAYOUT[layout_idx].count; bucket_capacity += type_cap[type]; } - assert(bucket_capacity <= INF_MAX_NPCS); int next_slot = 0; for (int pass = 0; pass < 2; pass++) { int pass_counts[INF_NUM_NPC_TYPES] = {0}; - int pass_slots[INF_MAX_NPCS]; + int pass_slots[bucket_capacity]; for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { InfNPC* npc = &s->npcs[npc_idx]; if (!npc->active || npc->type < 0 || npc->type >= INF_NUM_NPC_TYPES) diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index 9cb988d147..aeb1ba7f46 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -1915,8 +1915,33 @@ static inline int encounter_chase_attack_target( break; int next_dx; int next_dy; - if (step == 1 && arena_w > 0 && - (cached_run_dx != 0 || cached_run_dy != 0)) { + int use_cached_run = step == 1 && arena_w > 0 && + (cached_run_dx != 0 || cached_run_dy != 0); + if (use_cached_run) { + int next_x = p->x + cached_run_dx; + int next_y = p->y + cached_run_dy; + if (!is_walkable(ctx, next_x, next_y) || + (extra_blocked && extra_blocked( + blocked_ctx, + next_x + world_offset_x, + next_y + world_offset_y))) { + use_cached_run = 0; + } else if (cached_run_dx != 0 && cached_run_dy != 0 && + (!is_walkable(ctx, next_x, p->y) || + !is_walkable(ctx, p->x, next_y) || + (extra_blocked && + (extra_blocked( + blocked_ctx, + next_x + world_offset_x, + p->y + world_offset_y) || + extra_blocked( + blocked_ctx, + p->x + world_offset_x, + next_y + world_offset_y))))) { + use_cached_run = 0; + } + } + if (use_cached_run) { next_dx = cached_run_dx; next_dy = cached_run_dy; } else { From 342a9a176ec5fc5a73f8b15ebeb92090148d2bfd Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Tue, 11 Aug 2026 20:35:00 +0300 Subject: [PATCH 07/50] Design shared OSRS runtime optimization --- ...osrs-global-runtime-optimization-design.md | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-11-osrs-global-runtime-optimization-design.md diff --git a/docs/superpowers/specs/2026-08-11-osrs-global-runtime-optimization-design.md b/docs/superpowers/specs/2026-08-11-osrs-global-runtime-optimization-design.md new file mode 100644 index 0000000000..dbfe14be95 --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-osrs-global-runtime-optimization-design.md @@ -0,0 +1,265 @@ +# OSRS Global Runtime Optimization Design + +Date: 2026-08-11 + +## Goal + +Make every OSRS encounter in the current integration branch faster and simpler without changing config files, observation layouts, action layouts, combat rules, rewards, or terminal rules. Equal-cost routes may choose different legal tiles. Goldens may change only when an intentional route tie or RNG-order change explains the diff. + +Finish the current Inferno route pass first. Optimize the shared SDK and the integrated Inferno, Colosseum, Zulrah, and NH PvP encounters next. Merge Muspah last and adapt both Muspah wrappers to the final shared APIs. + +## Current evidence + +Fresh one-million-step single-thread profiles on the M4 Pro give these baselines: + +| Encounter | Steps per second | Dominant costs | +|---|---:|---| +| Inferno | 756,048 | Attack-route field 25%, observation write 15%, mask write 11% | +| Colosseum | 444,261 | NPC pathfinding 33%, observation write 26%, mask write 10% | +| Zulrah | 344,646 | Pathfinding 62% combined, observation write 19% | +| NH PvP | 531,411 | Pathfinding 45%, loadout resolution 21%, masks 20% | + +The completed Inferno route work raised paired mean throughput from 447,050 to 610,390 SPS in the first pass, then from 677,657 to 752,251 SPS in the second pass. The combined estimate is about 68%. Shared route construction remains the largest proven source of gain. + +## Scope + +### In scope + +- Shared collision, LOS, pathfinding, interaction, inventory, observation, and mask primitives under `ocean/osrs/` +- Integrated Inferno and Colosseum native wrappers +- Integrated Zulrah and NH PvP encounter paths +- Encounter-local work justified by a fresh profile after the shared cutover +- Muspah and Zulrah native wrapper integration after the shared substrate is stable +- Focused regression tests, mechanics goldens, builds, viewer smoke checks, and paired benchmarks + +### Out of scope + +- Config changes +- Reward tuning +- Curriculum tuning +- Policy hyperparameter tuning +- Puffer trainer internals +- A unified ECS or SoA rewrite +- Observation or action width changes +- Speculative caches without measured reuse + +## Architecture + +Encounter code keeps policy. The SDK owns execution mechanisms. + +```text +Encounter policy + waves, spawns, NPC decisions, rewards + | + v +Shared tick mechanisms + player actions, interactions, combat, inventory + | + +--> immutable arena topology + | step masks, footprint masks, LOS bitsets + | thread-local route scratch + | + +--> canonical item metadata + click semantics, gear slot, observation code + direct byte-mask output +``` + +The cutover uses one shared mechanism for each concern. Old mechanisms are deleted after every caller migrates. + +## Arena topology + +Add one process-shared immutable arena topology per encounter. Build it during encounter setup before worker threads start. Each encounter context holds a `const` pointer to that topology. Do not hide it behind lazy global lookup. + +The topology contains: + +- Arena origin, width, and height +- Static blocked tiles +- Static footprint-blocked masks for supported NPC sizes +- One eight-bit legal-step mask per tile +- Tile-pair LOS bitsets +- A topology revision used by derived route caches + +A maximum dimension of 64 covers NH PvP's 61 by 28 arena and every current encounter. Invalid dimensions abort during setup. + +Colosseum's private static blocked, footprint, and LOS tables become the reference implementation, then move into the SDK. Inferno, Zulrah, and NH PvP use the same representation. The encounter supplies static geometry. The SDK builds and queries the tables. + +Static topology uses one process-wide allocation per encounter and becomes immutable after setup. Per-env dynamic occupancy remains in encounter state. + +## Route solver + +Replace `pathfind_step` and `pathfind_step_arena` with one bounded route solver. + +The solver uses one thread-local scratch workspace per worker: + +- `uint16_t` generation stamps +- Packed `uint16_t` tile queue entries +- One-byte route directions +- Route depth required by fallback and shortest-path checks + +The solver never allocates during a tick. It never clears a 104 by 104 stack matrix. Generation wrap clears the stamp array once and resumes at generation one. + +The topology's legal-step mask replaces repeated collision-map traversal for static geometry. Dynamic blockers remain an explicit callback. Diagonal movement checks both cardinal edges once. The current duplicate diagonal collision calls are removed. + +The result is a tagged outcome: + +- Reached source +- Route found +- Fallback selected +- Unreachable + +A successful result carries the selected destination, first step, and optional run step. This prevents contradictory `found`, destination, and delta combinations. + +One route solve may provide both walking steps when the dynamic blocker revision stays unchanged. Otherwise the second step runs a new query. Equal-cost route tie changes are allowed. Every returned step must remain legal and shortest for the selected destination. + +## Route cache lifetime + +Route caches are derived state, not source state. + +A cache key contains: + +- Topology revision +- Dynamic blocker revision +- Source +- Target or target rectangle +- Actor footprint +- Route mode + +A cache hit requires every key field to match. Encounter code increments the blocker revision when relevant occupancy changes. A reset or snapshot restore invalidates every derived route cache. Snapshots do not serialize route scratch or route caches. + +If an encounter cannot expose a sound blocker revision, it does not use the cache. There is no heuristic reuse. + +## Inventory representation + +Replace repeated classification of parallel inventory fields with canonical content metadata. + +Each cell stores a `uint8_t` canonical content code and a `uint16_t` raw OSRS ID. Generated metadata indexed by content code provides: + +- Item index +- Consumable kind +- Dose +- Click action +- Gear slot +- Observation code +- Static affordance features + +Constructors and mutation functions create valid cells. Callers do not write item, dose, or click fields independently. This removes illegal combinations and repeated `osrs_consumable_click_registry_index` switches. + +The generated item observation table remains the source for static features. Dynamic values such as equipped state, current healing fraction, cooldown legality, and special energy remain dynamic. + +Every inventory constructor, click mutation, drink transition, swap, snapshot, restore, renderer, observation writer, mask writer, and test migrates in the same cutover. Old fields and classifiers are deleted. + +## Observation and mask output + +Observation widths and indices do not change. + +Observation writers use the canonical cell code and generated metadata directly. They do not rebuild a full affordance record to recover one code or one heal value. + +Change `EncounterDef.write_mask` to write bytes. Native wrappers pass their action-mask buffer directly. The viewer and diagnostics consume byte masks. Delete temporary float masks and float-to-byte conversion loops. + +Observation and mask output remain pure functions of encounter state and context. Output caching is allowed only for static table rows. Dynamic state is always written from the current tick. + +## Encounter-local work + +Reprofile after the shared cutover. Keep local work narrow. + +### Inferno + +- Reassess attack-route field construction +- Move static LOS to shared topology +- Remove redundant observation-slot refreshes only when state revisions prove reuse + +### Colosseum + +- Replace private topology tables with the shared topology +- Reassess occupancy-aware NPC routing +- Share route fields only when blocker revisions prove identical inputs +- Reassess Venator candidate selection + +### Zulrah + +- Remove full-grid path clearing through the shared solver +- Reassess cloud destination selection +- Keep phase and hazard policy encounter-owned + +### NH PvP + +- Resolve each selected loadout once per agent tick +- Pass the resolved value through masks, movement, switches, and combat +- Keep opponent policy and combat decisions encounter-owned + +### Muspah + +Merge after the APIs above stop moving. Adapt `osrs_muspah`, `osrs_muspah_native`, and the Zulrah native wrapper during the merge. Do not import duplicate pathfinding, collision, inventory, or mask stacks. + +## Failure handling + +Abort on defects: + +- Invalid arena dimensions +- Topology build failure +- Queue overflow +- Invalid item content code +- Invalid generated metadata +- Unknown config keys +- Impossible tagged states + +Return route failure as data. Missing routes and unreachable targets are expected outcomes. + +Do not add silent caps, timeout fallbacks, lazy concurrent initialization, or stale-cache recovery. + +## Verification + +### Route properties + +Run exhaustive static checks for each arena and randomized dynamic-blocker checks: + +- Every step is adjacent and legal +- Every diagonal satisfies both cardinal edge constraints +- A found route reaches its selected destination +- The optimized solver and the test-only reference BFS agree on reachability and shortest distance when given the same blockers +- Fallback selection minimizes the defined distance and route-cost ordering +- Cached and uncached queries produce equally valid outcomes +- A run step equals two valid sequential walk steps +- Generation wrap preserves results + +Keep a simple reference BFS in tests only. + +### Data contracts + +- Every inventory content code round-trips +- Generated metadata matches existing click, dose, gear-slot, and observation semantics +- Drink transitions select the correct next dose +- Observation widths, indices, and values stay unchanged +- Mask widths, indices, and values stay unchanged +- Reset, snapshot, restore, reward, terminal, and combat traces remain deterministic + +### Encounter gates + +Run the shared player, interaction, inventory, collision, and route suites. Run focused Inferno, Colosseum, Zulrah, NH PvP, and later Muspah suites. Build every affected native wrapper. Run each encounter through the viewer and profiler. + +Reseed a semantic golden only when an intentional equal-cost route or RNG-order change explains every diff. Unrelated golden changes fail the gate. + +### Performance gates + +Use paired runs with identical binaries, inputs, machine state, and profile action streams. + +- No encounter may regress beyond measured noise +- Each patch must improve its target median +- The aggregate geometric-mean SPS must rise +- Sampling must show the intended hotspot fell +- Final validation must include actual vectorized training throughput + +Continue while a measured hotspot supports a simpler change with material gain. Reject caches whose lifecycle costs more complexity than their measured gain. + +## Clean cutover order + +1. Freeze reference binaries and benchmark inputs +2. Add route property tests and a test-only reference BFS +3. Add shared topology and route scratch +4. Migrate every current route caller and delete old pathfinders +5. Migrate inventory representation and generated metadata +6. Migrate byte mask output and delete adapters +7. Reprofile all current encounters +8. Apply measured encounter-local changes +9. Run full mechanics, build, viewer, and performance gates +10. Merge Muspah and adapt its wrappers to the final APIs From cce652298ef3a18f1391c5dbcd970d7494d91602 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Tue, 11 Aug 2026 21:31:12 +0300 Subject: [PATCH 08/50] Optimize shared OSRS attack routing --- .../encounter_colosseum_player_actions.inc | 16 +- .../encounter_colosseum_render_snapshot.inc | 2 +- ocean/osrs/encounters/encounter_zulrah.h | 10 +- .../inferno/encounter_inferno_combat.inc | 3 + .../inferno/encounter_inferno_helpers.inc | 2 + .../inferno/encounter_inferno_model.inc | 15 +- .../inferno/encounter_inferno_movement.inc | 8 - .../inferno/encounter_inferno_obs_mask.inc | 134 +-- .../encounter_inferno_player_actions.inc | 14 +- .../encounter_inferno_render_snapshot.inc | 6 +- .../inferno/encounter_inferno_reset_spawn.inc | 7 - .../inferno/encounter_inferno_reward_step.inc | 178 +++- ocean/osrs/osrs_combat.h | 2 + ocean/osrs/osrs_encounter.h | 795 +++++++++--------- ocean/osrs/osrs_encounter_player.h | 168 +++- ocean/osrs/osrs_interaction.h | 35 + ocean/osrs/osrs_inventory_clicks.h | 84 +- ocean/osrs/osrs_items_generated.h | 2 +- ocean/osrs/osrs_render.h | 5 +- ocean/osrs/osrs_visual.c | 3 +- .../tests/test_colosseum_forecast_exact.c | 38 +- ocean/osrs/tests/test_colosseum_modifiers.c | 50 +- ocean/osrs/tests/test_inferno_attack_styles.c | 38 +- ocean/osrs/tests/test_inferno_golden.c | 40 +- .../osrs/tests/test_osrs_inventory_actions.c | 13 + ocean/osrs/tests/test_osrs_inventory_clicks.c | 12 + ocean/osrs/tests/test_osrs_player_step.c | 329 ++++++++ 27 files changed, 1364 insertions(+), 645 deletions(-) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc index 2663bb6b7d..8c19e8a525 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -513,14 +513,6 @@ static int col_tile_walkable(void* ctx, int x, int y) { return col_player_walkable(wc->s, x, y); } -static int col_pathfind_blocked(void* ctx, int abs_x, int abs_y) { - ColoWalkCtx* wc = (ColoWalkCtx*)ctx; - int x = abs_x - wc->ctx->world_offset_x; - int y = abs_y - wc->ctx->world_offset_y; - if (col_static_blocked(x, y)) return 1; - if (col_sol_clamp_active(wc->s) && !col_in_boss_arena(wc->s, x, y)) return 1; - return 0; -} static int col_npc_collision_flag_at(const ColosseumState* s, int x, int y) { int gx, gy; @@ -1052,8 +1044,8 @@ static ColoPrimaryStepPlan col_primary_step_plan( .world_offset_y = ctx->world_offset_y, .is_walkable = col_tile_walkable, .walkable_ctx = walk_ctx, - .extra_blocked = col_pathfind_blocked, - .blocked_ctx = walk_ctx, + .extra_blocked = NULL, + .blocked_ctx = NULL, .los_query = los_query, .arena_base_x = COLO_ARENA_MIN_X, .arena_base_y = COLO_ARENA_MIN_Y, @@ -1159,7 +1151,9 @@ static void col_tick_player_ctx( int player_range = col_player_attack_range(s); if (encounter_player_can_attack(s->player.x, s->player.y, npc->x, npc->y, col_npc_effective_size(npc), - player_range, &los_query)) { + player_range, + ctx->collision_map, ctx->world_offset_x, ctx->world_offset_y, + &los_query)) { int armed_kind = (!col_type_is_hazard_entity(npc->type) && s->player.spec_armed) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 02cb7beaa0..8096bb55b1 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -1031,7 +1031,7 @@ static void col_step_human_commands_ctx( } #define COLO_SNAPSHOT_MAGIC 0xC010C001u -#define COLO_SNAPSHOT_VERSION 22u +#define COLO_SNAPSHOT_VERSION 23u typedef struct { uint32_t magic; diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h index 42852db6a1..538caf6078 100644 --- a/ocean/osrs/encounters/encounter_zulrah.h +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -1167,7 +1167,10 @@ static int zul_player_can_attack_zulrah( return encounter_player_can_attack( s->player.x, s->player.y, s->zulrah.x, s->zulrah.y, ZUL_NPC_SIZE, - loadout_stats->attack_range, osrs_los_open_query()); + loadout_stats->attack_range, + (const CollisionMap*)s->collision_map, + s->world_offset_x, s->world_offset_y, + osrs_los_open_query()); } static int zul_zulrah_def_roll(ZulrahState* s, int is_mage) { @@ -1802,7 +1805,10 @@ static void zul_player_attack_snakeling(ZulrahState* s, int snakeling_idx) { if (!encounter_player_can_attack( s->player.x, s->player.y, sn->entity.x, sn->entity.y, 1, - ls->attack_range, osrs_los_open_query())) return; + ls->attack_range, + (const CollisionMap*)s->collision_map, + s->world_offset_x, s->world_offset_y, + osrs_los_open_query())) return; AttackStyle style = zul_player_equipped_attack_style(s); s->player.attack_timer = ls->attack_speed; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc index c6ebedf61e..76245869b4 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc @@ -226,6 +226,7 @@ static int inf_player_can_attack_npc_from_current_tile( npc->y, npc->size, ls->attack_range, + NULL, 0, 0, &los_query); } @@ -285,6 +286,7 @@ static InfPlayerNpcLoadoutReach inf_player_loadout_reach_to_npc( npc->y, npc->size, ls->attack_range, + NULL, 0, 0, &los_query); return out; } @@ -311,6 +313,7 @@ static int inf_player_can_phantom_barrage_npc( npc->y, npc->size, 10, + NULL, 0, 0, &los_query); } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc index aef959e384..25face54f1 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc @@ -642,7 +642,9 @@ static int inf_mager_resurrect_ctx(InfernoState* s, InfernoContext* ctx, int idx static void inf_queue_zuk_healer_sparks(InfernoState* s, const InfNPC* npc); static void inf_resolve_pending_sparks(InfernoState* s); static void inf_rebuild_player_collision_flags(InfernoState* s); +static void inf_invalidate_current_obs_slots(InfernoState* s); static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx); +static void inf_ensure_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx); static void inf_refresh_current_obs_slots(InfernoState* s); static InfConfig inf_default_config(void) { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc index a120b9ed19..bac9704fda 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc @@ -744,6 +744,14 @@ typedef struct { int brewed; int blood_heal; } InfTickScratch; +typedef struct { + uint8_t item_idx; + uint16_t raw_osrs_id; + uint8_t click_action; + uint8_t consumable_kind; +} InfInventoryClickMemo; + +static_assert(sizeof(InfInventoryClickMemo) == 6, "Inferno inventory click memo size"); typedef struct { Player player; @@ -753,6 +761,7 @@ typedef struct { InfNPC npcs[INF_MAX_NPCS]; uint32_t next_npc_render_id; int current_obs_slots[INF_OBS_NPCS]; + uint8_t current_obs_slots_valid; InfPillar pillars[INF_NUM_PILLARS]; InfZukState zuk; @@ -848,6 +857,7 @@ typedef struct { InfLoadoutProfile active_loadout_profile; EncounterLoadoutStats loadout_stats[INF_NUM_WEAPON_SETS]; OsrsInventoryCell inventory_cells[OSRS_INVENTORY_SIZE]; + InfInventoryClickMemo inventory_click_memos[OSRS_INVENTORY_SIZE]; EncounterLoadoutStats live_stats; EncounterLoadoutStats human_loadout_stats; int stamina_active_ticks; @@ -895,11 +905,6 @@ typedef struct { int total_zuk_healer_out_of_range_ticks; int total_zuk_healer_attackable_ticks; int total_action_mask_checks; - int zero_valid_action_head_count[INF_NUM_ACTION_HEADS]; - int min_valid_action_count_by_head[INF_NUM_ACTION_HEADS]; - int target_head_valid_healer_count; - int target_head_valid_zuk_count; - int target_head_valid_set_count; int total_shield_tags; float total_hp_restored_jad; float total_hp_restored_zuk; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc b/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc index 33c458b3ba..68e0f220f1 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc @@ -37,14 +37,6 @@ typedef struct { const InfernoContext* ctx; } InfWalkCtx; -static int inf_pathfind_blocked(void* data, int abs_x, int abs_y) { - InfWalkCtx* wc = (InfWalkCtx*)data; - InfernoState* s = wc->s; - const InfernoContext* ctx = wc->ctx; - int lx = abs_x - ctx->world_offset_x; - int ly = abs_y - ctx->world_offset_y; - return inf_blocked_by_pillar(s, lx, ly, 1); -} static int inf_collision_flags_blocked( uint8_t flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], int x, int y, int size diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc index 9e21cdced0..41d6e1b210 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -113,6 +113,9 @@ INF_OBS_SLOT_LAYOUT[INF_NUM_NPC_TYPES] = { { INF_NPC_ZUK, 1 }, { INF_NPC_ZUK_SHIELD, 1 }, { INF_NPC_HEALER_JAD, 6 }, { INF_NPC_HEALER_ZUK, 4 }, }; +static void inf_invalidate_current_obs_slots(InfernoState* s) { + s->current_obs_slots_valid = 0; +} static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx) { int obs_slots[INF_OBS_NPCS]; @@ -165,6 +168,15 @@ static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoCont for (int slot = 0; slot < INF_OBS_NPCS; slot++) s->current_obs_slots[slot] = obs_slots[slot]; + s->current_obs_slots_valid = 1; +} + +static void inf_ensure_current_obs_slots_ctx( + InfernoState* s, + const InfernoContext* ctx +) { + if (!s->current_obs_slots_valid) + inf_refresh_current_obs_slots_ctx(s, ctx); } static void inf_refresh_current_obs_slots(InfernoState* s) { @@ -277,7 +289,7 @@ static void inf_write_obs_ctx( INF_PROFILE_MARK(INF_PROF_OBS_PREFIX); #endif - inf_refresh_current_obs_slots_ctx(s, ctx); + inf_ensure_current_obs_slots_ctx(s, ctx); #ifdef INF_PROFILE_ENABLED INF_PROFILE_MARK(INF_PROF_OBS_REFRESH_SLOTS); #endif @@ -421,7 +433,7 @@ static int inf_is_human_targetable_npc_slot_ctx( ) { InfernoState* s = (InfernoState*)state; InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); - inf_refresh_current_obs_slots_ctx(s, ctx); + inf_ensure_current_obs_slots_ctx(s, ctx); return inf_obs_slot_is_targetable(s, ctx, inf_find_target_obs_slot(s, npc_slot)); } @@ -450,6 +462,26 @@ static int inf_drink_has_effect(const InfernoState* s, OsrsConsumableKind kind) return 0; } } +static inline InfInventoryClickMemo* inf_inventory_click_memo( + InfernoState* s, + int cell_idx +) { + const OsrsInventoryCell* cell = &s->inventory_cells[cell_idx]; + InfInventoryClickMemo* memo = &s->inventory_click_memos[cell_idx]; + if (memo->item_idx != cell->item_idx || + memo->raw_osrs_id != cell->raw_osrs_id) { + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_classify(cell); + *memo = (InfInventoryClickMemo){ + .item_idx = cell->item_idx, + .raw_osrs_id = cell->raw_osrs_id, + .click_action = (uint8_t)resolution.click_action, + .consumable_kind = (uint8_t)resolution.consumable_kind, + }; + } + return memo; +} + static void inf_write_mask_ctx( EncounterState* state, @@ -458,14 +490,8 @@ static void inf_write_mask_ctx( ) { InfernoState* s = (InfernoState*)state; InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); - inf_refresh_current_obs_slots_ctx(s, ctx); + inf_ensure_current_obs_slots_ctx(s, ctx); int offset = 0; - if (s->total_action_mask_checks == 0) { - for (int h = 0; h < INF_NUM_ACTION_HEADS; h++) { - if (s->min_valid_action_count_by_head[h] == 0) - s->min_valid_action_count_by_head[h] = 1000000; - } - } mask[offset++] = 1.0f; for (int d = 1; d < ENCOUNTER_MOVE_ACTIONS; d++) { @@ -504,45 +530,51 @@ static void inf_write_mask_ctx( } { - int cell_equip_slot[OSRS_INVENTORY_SIZE]; - int cell_can_eat[OSRS_INVENTORY_SIZE]; - int cell_can_drink[OSRS_INVENTORY_SIZE]; + int inventory_mask_offset = offset; + memset(&mask[inventory_mask_offset], 0, + sizeof(*mask) * INF_INV_CLICK_HEADS * INF_INV_CLICK_DIM); + for (int head = 0; head < INF_INV_CLICK_HEADS; head++) + mask[inventory_mask_offset + head * INF_INV_CLICK_DIM] = 1.0f; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - cell_equip_slot[c] = -1; - cell_can_eat[c] = 0; - cell_can_drink[c] = 0; const OsrsInventoryCell* cell = &s->inventory_cells[c]; - OsrsInventoryClickResolution resolution = - osrs_inventory_cell_click_classify(cell); - switch (resolution.click_action) { - case OSRS_CLICK_EQUIP: - if (osrs_can_equip_from_cell(&s->player, s->inventory_cells, c)) - cell_equip_slot[c] = osrs_item_gear_slot(cell->item_idx); + const InfInventoryClickMemo* memo = inf_inventory_click_memo(s, c); + switch (memo->click_action) { + case OSRS_CLICK_EQUIP: { + if (!osrs_can_equip_from_cell(&s->player, s->inventory_cells, c)) + break; + int gear_slot = osrs_item_gear_slot(cell->item_idx); + if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) { + fprintf(stderr, + "inferno: equippable inventory cell %d has invalid gear slot %d\n", + c, gear_slot); + abort(); + } + mask[inventory_mask_offset + gear_slot * INF_INV_CLICK_DIM + c + 1] = + 1.0f; break; + } case OSRS_CLICK_EAT: - cell_can_eat[c] = osrs_can_eat_consumable_kind( - &s->player, resolution.consumable_kind); + if (osrs_can_eat_consumable_kind( + &s->player, (OsrsConsumableKind)memo->consumable_kind)) { + mask[inventory_mask_offset + + NUM_GEAR_SLOTS * INF_INV_CLICK_DIM + c + 1] = 1.0f; + } break; case OSRS_CLICK_DRINK: - cell_can_drink[c] = cell->dose > 0 && - s->player.potion_timer == 0 && - inf_drink_has_effect(s, resolution.consumable_kind); + if (cell->dose > 0 && + s->player.potion_timer == 0 && + inf_drink_has_effect( + s, (OsrsConsumableKind)memo->consumable_kind)) { + mask[inventory_mask_offset + + (NUM_GEAR_SLOTS + 1) * INF_INV_CLICK_DIM + c + 1] = 1.0f; + } break; case OSRS_CLICK_NONE: break; } } - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - mask[offset++] = 1.0f; - for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) - mask[offset++] = cell_equip_slot[c] == slot ? 1.0f : 0.0f; - } - mask[offset++] = 1.0f; - for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) - mask[offset++] = cell_can_eat[c] ? 1.0f : 0.0f; - mask[offset++] = 1.0f; - for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) - mask[offset++] = cell_can_drink[c] ? 1.0f : 0.0f; + offset += INF_INV_CLICK_HEADS * INF_INV_CLICK_DIM; } mask[offset++] = 1.0f; @@ -562,34 +594,6 @@ static void inf_write_mask_ctx( mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; - int mask_offset = 0; - for (int h = 0; h < INF_NUM_ACTION_HEADS; h++) { - int valid = 0; - for (int a = 0; a < INF_ACTION_DIMS[h]; a++) { - if (mask[mask_offset + a] > 0.0f) - valid++; - } - if (valid == 0) - s->zero_valid_action_head_count[h]++; - if (valid < s->min_valid_action_count_by_head[h]) - s->min_valid_action_count_by_head[h] = valid; - mask_offset += INF_ACTION_DIMS[h]; - } - int target_offset = inf_action_head_mask_offset(INF_HEAD_TARGET) + 1; - for (int n = 0; n < INF_OBS_NPCS; n++) { - if (mask[target_offset + n] <= 0.0f) - continue; - int npc_idx = s->current_obs_slots[n]; - if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) - continue; - int type = s->npcs[npc_idx].type; - if (type == INF_NPC_HEALER_ZUK) - s->target_head_valid_healer_count++; - else if (type == INF_NPC_ZUK) - s->target_head_valid_zuk_count++; - else if (type != INF_NPC_ZUK_SHIELD) - s->target_head_valid_set_count++; - } s->total_action_mask_checks++; } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc index 7261ae1cfa..6c62221534 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -439,7 +439,7 @@ static void inf_tick_player_ctx( } } - inf_refresh_current_obs_slots_ctx(s, ctx); + inf_ensure_current_obs_slots_ctx(s, ctx); int target = actions[INF_HEAD_TARGET]; int has_new_target = 0; @@ -556,8 +556,8 @@ static void inf_tick_player_ctx( .world_offset_y = ctx->world_offset_y, .is_walkable = inf_tile_walkable, .walkable_ctx = &wc, - .extra_blocked = inf_pathfind_blocked, - .blocked_ctx = &wc, + .extra_blocked = NULL, + .blocked_ctx = NULL, .los_query = &los_query, .arena_base_x = INF_ARENA_MIN_X, .arena_base_y = INF_ARENA_MIN_Y, @@ -582,7 +582,9 @@ static void inf_tick_player_ctx( int has_healer_attack = inf_resolve_player_attack_ctx(s, ctx, manual_spell, &healer_attack); if (has_healer_attack && encounter_player_can_attack(s->player.x, s->player.y, target_npc->x, target_npc->y, target_npc->size, - healer_attack.stats.attack_range, &los_query)) { + healer_attack.stats.attack_range, + ctx->collision_map, ctx->world_offset_x, ctx->world_offset_y, + &los_query)) { s->total_zuk_healer_attackable_ticks++; } else { s->total_zuk_healer_out_of_range_ticks++; @@ -606,7 +608,9 @@ static void inf_tick_player_ctx( osrs_interaction_clear(&s->interaction); } else if (has_attack && encounter_player_can_attack(s->player.x, s->player.y, target_npc->x, target_npc->y, target_npc->size, - ls->attack_range, &los_query)) { + ls->attack_range, + ctx->collision_map, ctx->world_offset_x, ctx->world_offset_y, + &los_query)) { int is_blowpipe_spec_attack = weapon_is_blowpipe && s->player.spec_armed && s->player.special_energy >= BLOWPIPE_SPEC_COST; EncounterProjectileDistanceMode distance_mode = is_magic_attack diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index c5c780d030..2198e942c6 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -617,7 +617,7 @@ static void inf_translate_human_input_ctx( encounter_translate_offensive_prayer(hi, actions, INF_HEAD_OFFENSIVE); InfernoState* s = (InfernoState*)state; InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); - inf_refresh_current_obs_slots_ctx(s, ctx); + inf_ensure_current_obs_slots_ctx(s, ctx); if (hi->pending_target_idx >= 0) { int found_slot = inf_find_target_obs_slot(s, hi->pending_target_idx); if (inf_obs_slot_is_targetable(s, ctx, found_slot)) { @@ -665,7 +665,7 @@ static void inf_translate_human_commands_ctx( InfernoContext* ctx ) { for (int h = 0; h < INF_NUM_ACTION_HEADS; h++) actions[h] = 0; - inf_refresh_current_obs_slots_ctx(s, ctx); + inf_ensure_current_obs_slots_ctx(s, ctx); for (int i = 0; i < hi->commands.count; i++) { const HumanCommand* cmd = &hi->commands.items[i]; @@ -762,7 +762,7 @@ static void inf_step_human_commands(EncounterState* state, HumanInput* hi) { } #define INF_SNAPSHOT_MAGIC 0x1FE00001u -#define INF_SNAPSHOT_VERSION 18u +#define INF_SNAPSHOT_VERSION 21u typedef struct { uint32_t magic; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc index 1f00fbff75..beb3125050 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -375,13 +375,6 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint s->total_zuk_healer_out_of_range_ticks = 0; s->total_zuk_healer_attackable_ticks = 0; s->total_action_mask_checks = 0; - for (int h = 0; h < INF_NUM_ACTION_HEADS; h++) { - s->zero_valid_action_head_count[h] = 0; - s->min_valid_action_count_by_head[h] = 1000000; - } - s->target_head_valid_healer_count = 0; - s->target_head_valid_zuk_count = 0; - s->target_head_valid_set_count = 0; s->player.entity_type = ENTITY_PLAYER; encounter_init_maxed_player_combat_stats(&s->player, 99); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc index 5c44c699df..5119734ea5 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc @@ -6,17 +6,30 @@ static int inf_healer_is_actively_healing(const InfernoState* s, const InfNPC* n return s->npcs[npc->aggro_target].active; } -static int inf_npc_type_is_actively_healed( - const InfernoState* s, - InfNPCType target_type -) { +typedef struct { + int live_zuk_idx; + int jad_is_actively_healed; + int zuk_is_actively_healed; +} InfRewardNpcSummary; + +static InfRewardNpcSummary inf_summarize_reward_npcs(const InfernoState* s) { + InfRewardNpcSummary summary = {.live_zuk_idx = -1}; for (int i = 0; i < INF_MAX_NPCS; i++) { - const InfNPC* healer = &s->npcs[i]; - if (!inf_healer_is_actively_healing(s, healer)) continue; - const InfNPC* target = &s->npcs[healer->aggro_target]; - if (target->type == target_type) return 1; + const InfNPC* npc = &s->npcs[i]; + if (summary.live_zuk_idx < 0 && + npc->active && npc->type == INF_NPC_ZUK) { + summary.live_zuk_idx = i; + } + if (!inf_healer_is_actively_healing(s, npc)) continue; + + InfNPCType target_type = s->npcs[npc->aggro_target].type; + if (target_type == INF_NPC_JAD) { + summary.jad_is_actively_healed = 1; + } else if (target_type == INF_NPC_ZUK) { + summary.zuk_is_actively_healed = 1; + } } - return 0; + return summary; } static int inf_count_untagged_zuk_healers(const InfernoState* s) { @@ -61,9 +74,9 @@ typedef enum { static float inf_zuk_low_watermark_reward( InfernoState* s, const InfernoContext* ctx, - InfZukLowWatermarkMode mode + InfZukLowWatermarkMode mode, + int zuk_idx ) { - int zuk_idx = inf_find_live_zuk_idx(s); if (zuk_idx < 0) return 0.0f; float zuk_hp = (float)s->npcs[zuk_idx].hp; @@ -169,17 +182,17 @@ static float inf_compute_reward_ctx(InfernoState* s, const InfernoContext* ctx) return -ctx->config.death_penalty_coeff; } - int jad_is_actively_healed = - inf_npc_type_is_actively_healed(s, INF_NPC_JAD); - int zuk_is_actively_healed = - inf_npc_type_is_actively_healed(s, INF_NPC_ZUK); + InfRewardNpcSummary npc_summary = inf_summarize_reward_npcs(s); + int jad_is_actively_healed = npc_summary.jad_is_actively_healed; + int zuk_is_actively_healed = npc_summary.zuk_is_actively_healed; int healer_is_actively_healing = jad_is_actively_healed || zuk_is_actively_healed; int tags_first_gate = inf_zuk_healer_tags_first_reward_gate_active(s, ctx); if (ctx->config.joseph_reward_mode == INF_JOSEPH_REWARD_MODE_ON) { if (inf_is_final_wave(s)) - inf_zuk_low_watermark_reward(s, ctx, INF_ZUK_LOWWATERMARK_TRACK_ONLY); + inf_zuk_low_watermark_reward( + s, ctx, INF_ZUK_LOWWATERMARK_TRACK_ONLY, npc_summary.live_zuk_idx); return inf_compute_joseph_reward(s, ctx, healer_is_actively_healing); } @@ -187,7 +200,7 @@ static float inf_compute_reward_ctx(InfernoState* s, const InfernoContext* ctx) if (inf_is_final_wave(s)) { if (tags_first_gate) { reward = inf_zuk_low_watermark_reward( - s, ctx, INF_ZUK_LOWWATERMARK_TRACK_ONLY); + s, ctx, INF_ZUK_LOWWATERMARK_TRACK_ONLY, npc_summary.live_zuk_idx); reward += ctx->config.tag_reward_coeff * (float)s->tick_scratch.healer_tags; reward += inf_zuk_healer_attack_shape_reward(s, ctx); return reward; @@ -202,7 +215,8 @@ static float inf_compute_reward_ctx(InfernoState* s, const InfernoContext* ctx) } else if (zuk_is_actively_healed) { watermark_mode = INF_ZUK_LOWWATERMARK_REWARD_BLOCKED_EXCEPT_THRESHOLD; } - reward = inf_zuk_low_watermark_reward(s, ctx, watermark_mode); + reward = inf_zuk_low_watermark_reward( + s, ctx, watermark_mode, npc_summary.live_zuk_idx); if (use_zuk_healer_phase_hp_delta) { reward += ctx->config.zuk_healer_phase_hp_delta_coeff * (s->tick_scratch.damage_zuk - s->tick_scratch.hp_restored_zuk); @@ -239,8 +253,8 @@ static float inf_compute_reward_ctx(InfernoState* s, const InfernoContext* ctx) reward += ctx->config.zuk_safe_untagged_healer_target_bonus_coeff * (float)s->tick_scratch.zuk_safe_untagged_healer_targets; - int zuk_idx = inf_find_live_zuk_idx(s); - int zuk_hp_now = (zuk_idx >= 0) ? s->npcs[zuk_idx].hp : 1200; + int zuk_hp_now = npc_summary.live_zuk_idx >= 0 ? + s->npcs[npc_summary.live_zuk_idx].hp : 1200; int late_add_reward_active = s->zuk.jad_spawned || zuk_hp_now <= 600; if (late_add_reward_active) { float rewardable_jad_damage = @@ -388,11 +402,34 @@ static void inf_update_healer_transition_stats(InfernoState* s) { } } -static int inf_has_live_player_target(const InfernoState* s) { +typedef struct { + int has_live_player_target; + int has_live_jad; + int has_live_zuk_healer; +} InfIdleDiagnosticSummary; + +static InfIdleDiagnosticSummary inf_idle_diagnostic_summary(const InfernoState* s) { + InfIdleDiagnosticSummary summary = {0}; for (int i = 0; i < INF_MAX_NPCS; i++) { - if (inf_npc_is_live_player_target(s, i)) return 1; + const InfNPC* npc = &s->npcs[i]; + if (inf_npc_is_live_player_target(s, i)) + summary.has_live_player_target = 1; + if (npc->active && npc->death_ticks == 0 && npc->hp > 0) { + if (npc->type == INF_NPC_JAD) + summary.has_live_jad = 1; + if (npc->type == INF_NPC_HEALER_ZUK) + summary.has_live_zuk_healer = 1; + } + if (summary.has_live_player_target && + summary.has_live_jad && + summary.has_live_zuk_healer) + break; } - return 0; + return summary; +} + +static int inf_has_live_player_target(const InfernoState* s) { + return inf_idle_diagnostic_summary(s).has_live_player_target; } static int inf_has_attackable_player_target(const InfernoState* s) { @@ -404,29 +441,26 @@ static int inf_has_attackable_player_target(const InfernoState* s) { return 0; } -static int inf_has_live_npc_type(const InfernoState* s, InfNPCType type) { - for (int i = 0; i < INF_MAX_NPCS; i++) { - const InfNPC* npc = &s->npcs[i]; - if (npc->active && npc->death_ticks == 0 && npc->hp > 0 && - npc->type == type) - return 1; - } - return 0; -} - -static InfIdleDiagnosticPhase inf_idle_diagnostic_phase(const InfernoState* s) { +static InfIdleDiagnosticPhase inf_idle_diagnostic_phase_from_summary( + const InfernoState* s, + InfIdleDiagnosticSummary summary +) { if (!inf_is_final_wave(s)) - return inf_has_live_npc_type(s, INF_NPC_JAD) - ? INF_IDLE_PHASE_JAD : INF_IDLE_PHASE_SET; - if (inf_has_live_npc_type(s, INF_NPC_HEALER_ZUK)) + return summary.has_live_jad ? INF_IDLE_PHASE_JAD : INF_IDLE_PHASE_SET; + if (summary.has_live_zuk_healer) return INF_IDLE_PHASE_ZUK_HEALERS; - if (inf_has_live_npc_type(s, INF_NPC_JAD)) + if (summary.has_live_jad) return INF_IDLE_PHASE_ZUK_JAD; if (s->tick_at_all_zuk_healers_dead >= 0) return INF_IDLE_PHASE_ZUK_POST_HEALERS; return INF_IDLE_PHASE_ZUK_PRE_JAD; } +static InfIdleDiagnosticPhase inf_idle_diagnostic_phase(const InfernoState* s) { + return inf_idle_diagnostic_phase_from_summary( + s, inf_idle_diagnostic_summary(s)); +} + static int inf_player_has_immediate_threat(const InfernoState* s) { if (s->tick_scratch.attacks_fired > 0) return 1; for (int h = 0; h < s->player_pending_hits.count; h++) { @@ -443,6 +477,44 @@ static int inf_player_has_immediate_threat(const InfernoState* s) { return 0; } +typedef struct { + int has_attackable_player_target; + int player_has_immediate_threat; +} InfIdleMissedActionFacts; + +static InfIdleMissedActionFacts inf_idle_missed_action_facts( + const InfernoState* s +) { + InfIdleMissedActionFacts facts = { + .player_has_immediate_threat = s->tick_scratch.attacks_fired > 0, + }; + if (!facts.player_has_immediate_threat) { + for (int h = 0; h < s->player_pending_hits.count; h++) { + const EncounterPendingHit* ph = &s->player_pending_hits.hits[h]; + if (ph->check_prayer && inf_pending_hit_obs_timer(ph) <= 1) { + facts.player_has_immediate_threat = 1; + break; + } + } + } + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (!facts.has_attackable_player_target && + inf_npc_is_live_player_target(s, i) && + inf_player_can_attack_npc_from_current_tile(s, i)) + facts.has_attackable_player_target = 1; + if (!facts.player_has_immediate_threat) { + const InfNPC* npc = &s->npcs[i]; + InfNpcPlayerThreat threat = inf_npc_player_threat(s, npc); + if (threat.telegraph_style_mask && threat.can_attack_this_tick) + facts.player_has_immediate_threat = 1; + } + if (facts.has_attackable_player_target && + facts.player_has_immediate_threat) + break; + } + return facts; +} + static int inf_progress_happened_this_tick(const InfernoState* s) { return s->tick_scratch.damage_dealt > 0.0f || s->tick_scratch.healer_tags > 0 || @@ -456,14 +528,15 @@ static int inf_progress_happened_this_tick(const InfernoState* s) { static void inf_record_idle_diagnostics( InfernoState* s, int can_player_attack, + int has_live_player_target, + InfIdleDiagnosticPhase phase, int attack_ready_no_attack, int target_available_no_attack, int safe_attack_opportunity_missed ) { - if (!can_player_attack || !inf_has_live_player_target(s)) + if (!can_player_attack || !has_live_player_target) return; - InfIdleDiagnosticPhase phase = inf_idle_diagnostic_phase(s); assert((int)phase >= 0 && (int)phase < OSRS_INFERNO_IDLE_PHASE_COUNT); if (attack_ready_no_attack) { @@ -539,10 +612,13 @@ static void inf_step_ctx(EncounterState* state, EncounterContext* context, const s->winner = INF_OUTCOME_PLAYER_DIED; s->reward = inf_terminal_loss_reward(s, ctx); s->episode_return += s->reward; + inf_invalidate_current_obs_slots(s); return; } int can_player_attack = !in_wave_gap && !in_ready_gap; + InfIdleDiagnosticSummary idle_diagnostic_summary = {0}; + InfIdleDiagnosticPhase idle_diagnostic_phase = INF_IDLE_PHASE_SET; int attack_ready_no_attack = 0; int target_available_no_attack = 0; int safe_attack_opportunity_missed = 0; @@ -556,19 +632,28 @@ static void inf_step_ctx(EncounterState* state, EncounterContext* context, const inf_invalidate_los_cache(s); inf_resolve_jad_prayer_checks_after_player(s, ctx); + if (can_player_attack) { + idle_diagnostic_summary = inf_idle_diagnostic_summary(s); + idle_diagnostic_phase = inf_idle_diagnostic_phase_from_summary( + s, idle_diagnostic_summary); + } + { - int has_alive_npc = can_player_attack && inf_has_live_player_target(s); + int has_alive_npc = + can_player_attack && idle_diagnostic_summary.has_live_player_target; if (has_alive_npc && s->player.attack_timer == 0 && !s->tick_scratch.player_attacked) s->ticks_without_action++; else s->ticks_without_action = 0; attack_ready_no_attack = s->ticks_without_action > 0; if (attack_ready_no_attack) { - int target_available = inf_has_live_player_target(s); - int attackable_target = inf_has_attackable_player_target(s); - target_available_no_attack = target_available; + InfIdleMissedActionFacts missed_action_facts = + inf_idle_missed_action_facts(s); + target_available_no_attack = + idle_diagnostic_summary.has_live_player_target; safe_attack_opportunity_missed = - attackable_target && !inf_player_has_immediate_threat(s); + missed_action_facts.has_attackable_player_target && + !missed_action_facts.player_has_immediate_threat; } } @@ -666,9 +751,12 @@ finish_step: inf_record_idle_diagnostics( s, can_player_attack, + idle_diagnostic_summary.has_live_player_target, + idle_diagnostic_phase, attack_ready_no_attack, target_available_no_attack, safe_attack_opportunity_missed); + inf_invalidate_current_obs_slots(s); s->episode_return += s->reward; } diff --git a/ocean/osrs/osrs_combat.h b/ocean/osrs/osrs_combat.h index 9e16832427..6e8a3ae4b2 100644 --- a/ocean/osrs/osrs_combat.h +++ b/ocean/osrs/osrs_combat.h @@ -592,6 +592,8 @@ typedef struct { static inline void osrs_sum_equipment_bonuses(const uint8_t loadout[NUM_GEAR_SLOTS], EquipmentBonuses* out) { memset(out, 0, sizeof(*out)); + out->attack_speed = 4; + out->attack_range = 1; for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { uint8_t idx = loadout[slot]; if (idx == 255) continue; diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index aeb1ba7f46..3ffb793e16 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -890,6 +890,36 @@ static inline int encounter_entity_footprint_cardinal_adjacent( return (dx + dy) == 1; } +static inline int encounter_entity_footprint_cardinal_reachable( + const CollisionMap* cmap, + int world_offset_x, + int world_offset_y, + int player_x, + int player_y, + int target_x, + int target_y, + int target_size +) { + int target_max_x = target_x + target_size - 1; + int target_max_y = target_y + target_size - 1; + int flags = collision_get_flags( + cmap, 0, player_x + world_offset_x, player_y + world_offset_y); + + if (player_x + 1 == target_x && + player_y >= target_y && player_y <= target_max_y) + return (flags & COLLISION_WALL_EAST) == 0; + if (player_x == target_max_x + 1 && + player_y >= target_y && player_y <= target_max_y) + return (flags & COLLISION_WALL_WEST) == 0; + if (player_y + 1 == target_y && + player_x >= target_x && player_x <= target_max_x) + return (flags & COLLISION_WALL_NORTH) == 0; + if (player_y == target_max_y + 1 && + player_x >= target_x && player_x <= target_max_x) + return (flags & COLLISION_WALL_SOUTH) == 0; + return 0; +} + static inline int encounter_entity_footprints_overlap( int ax, int ay, int a_size, int bx, int by, int b_size @@ -1089,65 +1119,41 @@ static inline int osrs_los_clear( static inline int encounter_player_can_attack( int player_x, int player_y, int target_x, int target_y, int target_size, int attack_range, + const CollisionMap* cmap, int world_offset_x, int world_offset_y, const OsrsLosQuery* los_query ) { int dist = encounter_rect_distance(player_x, player_y, 1, target_x, target_y, target_size); if (dist < 1 || dist > attack_range) return 0; if (attack_range == 1) - return encounter_entity_footprint_cardinal_adjacent( - player_x, player_y, 1, target_x, target_y, target_size); + return encounter_entity_footprint_cardinal_reachable( + cmap, world_offset_x, world_offset_y, + player_x, player_y, target_x, target_y, target_size); return osrs_los_clear(los_query, player_x, player_y, 1, target_x, target_y, target_size, attack_range); } -#define ENCOUNTER_ATTACK_SEEK_MAX_TILES 128 -typedef struct { - int x; - int y; -} EncounterAttackSeekTile; - -static inline void encounter_attack_seek_add_tile( - EncounterAttackSeekTile* tiles, int* count, - int x, int y, int world_offset_x, int world_offset_y, - pathfind_blocked_fn extra_blocked, void* blocked_ctx -) { - if (extra_blocked && - extra_blocked(blocked_ctx, x + world_offset_x, y + world_offset_y)) - return; - if (*count >= ENCOUNTER_ATTACK_SEEK_MAX_TILES) { - fprintf(stderr, "attack seek tile capacity exceeded: %d\n", - ENCOUNTER_ATTACK_SEEK_MAX_TILES); - abort(); - } - tiles[*count] = (EncounterAttackSeekTile){x, y}; - (*count)++; -} - -static inline int encounter_attack_seek_nearest_dsq( - int x, int y, const EncounterAttackSeekTile* tiles, int count +static inline int encounter_target_rect_distance_squared( + int x, + int y, + int target_x, + int target_y, + int target_size ) { - int best = 0x3fffffff; - for (int i = 0; i < count; i++) { - int dx = x - tiles[i].x; - int dy = y - tiles[i].y; - int dsq = dx * dx + dy * dy; - if (dsq < best) best = dsq; - } - return best; + int target_max_x = target_x + target_size - 1; + int target_max_y = target_y + target_size - 1; + int dx = x < target_x + ? target_x - x + : (x > target_max_x ? x - target_max_x : 0); + int dy = y < target_y + ? target_y - y + : (y > target_max_y ? y - target_max_y : 0); + return dx * dx + dy * dy; } -static inline int encounter_attack_seek_has_exact_tile( - int x, int y, const EncounterAttackSeekTile* tiles, int count -) { - for (int i = 0; i < count; i++) { - if (tiles[i].x == x && tiles[i].y == y) return 1; - } - return 0; -} typedef struct { const CollisionMap* collision_map; @@ -1163,11 +1169,13 @@ typedef struct { int arena_h; int source_x; int source_y; + int target_edge_local_x; + int target_edge_local_y; int min_explored_x; int min_explored_y; int max_explored_x; int max_explored_y; - uint16_t visit_order[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + uint64_t visited[PATHFIND_ARENA_MAX]; uint16_t depth[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; int8_t via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; int visited_count; @@ -1176,20 +1184,127 @@ typedef struct { typedef struct { int land_x; int land_y; + int route_found; + int route_x; + int route_y; } EncounterAttackRouteLanding; -static inline int encounter_attack_route_field_extra_blocked( - const EncounterArenaAttackRouteField* field, + +static inline int encounter_attack_route_step_traversable( + const CollisionMap* cmap, + int world_offset_x, + int world_offset_y, + int x, + int y, + int dx, + int dy, + encounter_walkable_fn is_walkable, + void* walkable_ctx, + pathfind_blocked_fn extra_blocked, + void* blocked_ctx +) { + int next_x = x + dx; + int next_y = y + dy; + if (!is_walkable(walkable_ctx, next_x, next_y)) return 0; + if (dx != 0 && dy != 0 && + (!is_walkable(walkable_ctx, next_x, y) || + !is_walkable(walkable_ctx, x, next_y))) { + return 0; + } + if (!collision_traversable_step( + cmap, 0, x + world_offset_x, y + world_offset_y, dx, dy)) { + return 0; + } + if (extra_blocked && + extra_blocked( + blocked_ctx, + next_x + world_offset_x, + next_y + world_offset_y)) { + return 0; + } + if (dx == 0 || dy == 0) return 1; + if (extra_blocked && + (extra_blocked( + blocked_ctx, + next_x + world_offset_x, + y + world_offset_y) || + extra_blocked( + blocked_ctx, + x + world_offset_x, + next_y + world_offset_y))) { + return 0; + } + return 1; +} +typedef struct { + encounter_walkable_fn is_walkable; + void* walkable_ctx; + int arena_base_x; + int arena_base_y; + int arena_w; + int arena_h; + uint64_t known[PATHFIND_ARENA_MAX]; + uint64_t walkable[PATHFIND_ARENA_MAX]; +} EncounterAttackRouteWalkableCache; + +static inline int encounter_attack_route_cached_walkable( + void* data, int x, int y ) { - return field->extra_blocked && - field->extra_blocked( - field->blocked_ctx, - x + field->world_offset_x, - y + field->world_offset_y); + EncounterAttackRouteWalkableCache* cache = + (EncounterAttackRouteWalkableCache*)data; + int local_x = x - cache->arena_base_x; + int local_y = y - cache->arena_base_y; + if (local_x < 0 || local_x >= cache->arena_w || + local_y < 0 || local_y >= cache->arena_h) { + return cache->is_walkable(cache->walkable_ctx, x, y); + } + uint64_t bit = 1ULL << local_y; + if ((cache->known[local_x] & bit) == 0) { + cache->known[local_x] |= bit; + if (cache->is_walkable(cache->walkable_ctx, x, y)) + cache->walkable[local_x] |= bit; + } + return (cache->walkable[local_x] & bit) != 0; +} +static inline void encounter_attack_route_mark_target_edge( + uint64_t target_edges[PATHFIND_ARENA_MAX], + const CollisionMap* cmap, + int world_offset_x, + int world_offset_y, + int edge_x, + int edge_y, + int target_x, + int target_y, + int target_size, + int arena_base_x, + int arena_base_y, + int arena_w, + int arena_h +) { + int local_x = edge_x - arena_base_x; + int local_y = edge_y - arena_base_y; + if (local_x < 0 || local_x >= arena_w || + local_y < 0 || local_y >= arena_h) { + return; + } + if (encounter_entity_footprint_cardinal_reachable( + cmap, + world_offset_x, + world_offset_y, + edge_x, + edge_y, + target_x, + target_y, + target_size)) { + target_edges[local_x] |= 1ULL << local_y; + } } + + + static inline void encounter_build_arena_attack_route_field( EncounterArenaAttackRouteField* field, const CollisionMap* cmap, @@ -1197,6 +1312,9 @@ static inline void encounter_build_arena_attack_route_field( int world_offset_y, int source_x, int source_y, + int target_x, + int target_y, + int target_size, encounter_walkable_fn is_walkable, void* walkable_ctx, pathfind_blocked_fn extra_blocked, @@ -1206,7 +1324,7 @@ static inline void encounter_build_arena_attack_route_field( int arena_w, int arena_h ) { - if (!field || !is_walkable) { + if (!field || !is_walkable || target_size <= 0) { fprintf(stderr, "attack route field is missing required input\n"); abort(); } @@ -1227,7 +1345,12 @@ static inline void encounter_build_arena_attack_route_field( abort(); } - memset(field, 0, sizeof(*field)); + memset( + field->visited, + 0, + (size_t)arena_w * sizeof(field->visited[0])); + field->target_edge_local_x = -1; + field->target_edge_local_y = -1; field->collision_map = cmap; field->is_walkable = is_walkable; field->walkable_ctx = walkable_ctx; @@ -1245,17 +1368,48 @@ static inline void encounter_build_arena_attack_route_field( field->min_explored_y = local_source_y; field->max_explored_x = local_source_x; field->max_explored_y = local_source_y; + EncounterAttackRouteWalkableCache walkable_cache = { + .is_walkable = is_walkable, + .walkable_ctx = walkable_ctx, + .arena_base_x = arena_base_x, + .arena_base_y = arena_base_y, + .arena_w = arena_w, + .arena_h = arena_h, + }; + uint64_t target_edges[PATHFIND_ARENA_MAX] = {0}; + for (int offset = 0; offset < target_size; offset++) { + encounter_attack_route_mark_target_edge( + target_edges, cmap, world_offset_x, world_offset_y, + target_x - 1, target_y + offset, + target_x, target_y, target_size, + arena_base_x, arena_base_y, arena_w, arena_h); + encounter_attack_route_mark_target_edge( + target_edges, cmap, world_offset_x, world_offset_y, + target_x + target_size, target_y + offset, + target_x, target_y, target_size, + arena_base_x, arena_base_y, arena_w, arena_h); + encounter_attack_route_mark_target_edge( + target_edges, cmap, world_offset_x, world_offset_y, + target_x + offset, target_y - 1, + target_x, target_y, target_size, + arena_base_x, arena_base_y, arena_w, arena_h); + encounter_attack_route_mark_target_edge( + target_edges, cmap, world_offset_x, world_offset_y, + target_x + offset, target_y + target_size, + target_x, target_y, target_size, + arena_base_x, arena_base_y, arena_w, arena_h); + } + + + - int queue_x[PATHFIND_MAX_QUEUE_ARENA]; - int queue_y[PATHFIND_MAX_QUEUE_ARENA]; + uint16_t queue[PATHFIND_MAX_QUEUE_ARENA]; int head = 0; - int tail = 0; field->visited_count = 1; - field->visit_order[local_source_x][local_source_y] = 1; + field->visited[local_source_x] = 1ULL << local_source_y; + field->depth[local_source_x][local_source_y] = 0; field->via[local_source_x][local_source_y] = VIA_START; - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, - local_source_x, local_source_y); + queue[0] = (uint16_t)((local_source_x << 8) | local_source_y); static const int route_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; static const int route_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; @@ -1263,15 +1417,18 @@ static inline void encounter_build_arena_attack_route_field( VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE }; - while (head < tail) { - int cur_x = queue_x[head]; - int cur_y = queue_y[head]; - head++; + while (head < field->visited_count) { + uint16_t packed_tile = queue[head++]; + int cur_x = packed_tile >> 8; + int cur_y = packed_tile & 0xff; int tile_x = arena_base_x + cur_x; int tile_y = arena_base_y + cur_y; - int abs_x = tile_x + world_offset_x; - int abs_y = tile_y + world_offset_y; + if (target_edges[cur_x] & (1ULL << cur_y)) { + field->target_edge_local_x = cur_x; + field->target_edge_local_y = cur_y; + break; + } uint16_t next_depth = field->depth[cur_x][cur_y] + 1; for (int i = 0; i < 8; i++) { @@ -1283,44 +1440,26 @@ static inline void encounter_build_arena_attack_route_field( next_y < 0 || next_y >= arena_h) { continue; } - if (field->visit_order[next_x][next_y] != 0) continue; - if (!collision_traversable_step(cmap, 0, abs_x, abs_y, dx, dy)) - continue; - - int next_tile_x = tile_x + dx; - int next_tile_y = tile_y + dy; - if (!is_walkable(walkable_ctx, next_tile_x, next_tile_y)) continue; - if (extra_blocked && - extra_blocked( - blocked_ctx, - next_tile_x + world_offset_x, - next_tile_y + world_offset_y)) { + if (field->visited[next_x] & (1ULL << next_y)) continue; + if (!encounter_attack_route_step_traversable( + cmap, + world_offset_x, + world_offset_y, + tile_x, + tile_y, + dx, + dy, + encounter_attack_route_cached_walkable, + &walkable_cache, + extra_blocked, + blocked_ctx)) { continue; } - if (dx != 0 && dy != 0) { - if (extra_blocked && - (extra_blocked( - blocked_ctx, - tile_x + dx + world_offset_x, - tile_y + world_offset_y) || - extra_blocked( - blocked_ctx, - tile_x + world_offset_x, - tile_y + dy + world_offset_y))) { - continue; - } - if (!is_walkable(walkable_ctx, tile_x + dx, tile_y) || - !is_walkable(walkable_ctx, tile_x, tile_y + dy)) { - continue; - } - } - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, - next_x, next_y); + queue[field->visited_count] = + (uint16_t)((next_x << 8) | next_y); field->visited_count++; - field->visit_order[next_x][next_y] = - (uint16_t)field->visited_count; + field->visited[next_x] |= 1ULL << next_y; field->depth[next_x][next_y] = next_depth; field->via[next_x][next_y] = (int8_t)route_via[i]; if (next_x < field->min_explored_x) field->min_explored_x = next_x; @@ -1382,6 +1521,10 @@ encounter_attack_route_overlap_landing( return (EncounterAttackRouteLanding){ .land_x = player.x, .land_y = player.y, + .route_found = player.x != field->source_x || + player.y != field->source_y, + .route_x = player.x, + .route_y = player.y, }; } @@ -1405,6 +1548,8 @@ static inline EncounterAttackRouteLanding encounter_arena_attack_route_landing( EncounterAttackRouteLanding landing = { .land_x = field->source_x, .land_y = field->source_y, + .route_x = field->source_x, + .route_y = field->source_y, }; int dist = encounter_rect_distance( field->source_x, field->source_y, 1, @@ -1415,101 +1560,46 @@ static inline EncounterAttackRouteLanding encounter_arena_attack_route_landing( } if (encounter_player_can_attack( field->source_x, field->source_y, - target_x, target_y, target_size, attack_range, los_query)) { + target_x, target_y, target_size, attack_range, + field->collision_map, field->world_offset_x, field->world_offset_y, + los_query)) { + landing.route_found = 1; return landing; } - EncounterAttackSeekTile seek_tiles[ENCOUNTER_ATTACK_SEEK_MAX_TILES]; - int seek_count = 0; - for (int xx = 0; xx < target_size; xx++) { - int x = target_x + xx; - encounter_attack_seek_add_tile( - seek_tiles, &seek_count, x, target_y - 1, - field->world_offset_x, field->world_offset_y, - field->extra_blocked, field->blocked_ctx); - encounter_attack_seek_add_tile( - seek_tiles, &seek_count, x, target_y + target_size, - field->world_offset_x, field->world_offset_y, - field->extra_blocked, field->blocked_ctx); - } - for (int yy = 0; yy < target_size; yy++) { - int y = target_y + yy; - encounter_attack_seek_add_tile( - seek_tiles, &seek_count, target_x - 1, y, - field->world_offset_x, field->world_offset_y, - field->extra_blocked, field->blocked_ctx); - encounter_attack_seek_add_tile( - seek_tiles, &seek_count, target_x + target_size, y, - field->world_offset_x, field->world_offset_y, - field->extra_blocked, field->blocked_ctx); - } - - int selected_x = -1; - int selected_y = -1; - uint16_t selected_order = UINT16_MAX; - for (int x = 0; x < field->arena_w; x++) { - for (int y = 0; y < field->arena_h; y++) { - uint16_t order = field->visit_order[x][y]; - if (order == 0 || order >= selected_order) continue; - int tile_x = field->arena_base_x + x; - int tile_y = field->arena_base_y + y; - if (!field->is_walkable(field->walkable_ctx, tile_x, tile_y)) - continue; - if (encounter_attack_route_field_extra_blocked( - field, tile_x, tile_y)) { - continue; - } - int exact = seek_count > 0 - ? encounter_attack_seek_has_exact_tile( - tile_x, tile_y, seek_tiles, seek_count) - : encounter_player_can_attack( - tile_x, tile_y, - target_x, target_y, target_size, - attack_range, los_query); - if (!exact) continue; - selected_x = x; - selected_y = y; - selected_order = order; - } - } - if (selected_x < 0 && seek_count > 0) { - int first_local_x = seek_tiles[0].x - field->arena_base_x; - int first_local_y = seek_tiles[0].y - field->arena_base_y; - int scan_min_x = - field->min_explored_x > first_local_x - PATHFIND_MAX_FALLBACK_RADIUS - ? field->min_explored_x - : first_local_x - PATHFIND_MAX_FALLBACK_RADIUS; - int scan_min_y = - field->min_explored_y > first_local_y - PATHFIND_MAX_FALLBACK_RADIUS - ? field->min_explored_y - : first_local_y - PATHFIND_MAX_FALLBACK_RADIUS; + int selected_x = field->target_edge_local_x; + int selected_y = field->target_edge_local_y; + + if (selected_x < 0) { + int target_local_x = target_x - field->arena_base_x; + int target_local_y = target_y - field->arena_base_y; + int scan_min_x = target_local_x - PATHFIND_MAX_FALLBACK_RADIUS; + int scan_min_y = target_local_y - PATHFIND_MAX_FALLBACK_RADIUS; int scan_max_x = - field->max_explored_x > first_local_x + PATHFIND_MAX_FALLBACK_RADIUS - ? field->max_explored_x - : first_local_x + PATHFIND_MAX_FALLBACK_RADIUS; + target_local_x + target_size - 1 + PATHFIND_MAX_FALLBACK_RADIUS; int scan_max_y = - field->max_explored_y > first_local_y + PATHFIND_MAX_FALLBACK_RADIUS - ? field->max_explored_y - : first_local_y + PATHFIND_MAX_FALLBACK_RADIUS; - if (scan_min_x < 0) scan_min_x = 0; - if (scan_min_y < 0) scan_min_y = 0; - if (scan_max_x >= field->arena_w) scan_max_x = field->arena_w - 1; - if (scan_max_y >= field->arena_h) scan_max_y = field->arena_h - 1; + target_local_y + target_size - 1 + PATHFIND_MAX_FALLBACK_RADIUS; + if (scan_min_x < field->min_explored_x) + scan_min_x = field->min_explored_x; + if (scan_min_y < field->min_explored_y) + scan_min_y = field->min_explored_y; + if (scan_max_x > field->max_explored_x) + scan_max_x = field->max_explored_x; + if (scan_max_y > field->max_explored_y) + scan_max_y = field->max_explored_y; int best_dsq = 0x3fffffff; int best_depth = 100; for (int x = scan_min_x; x <= scan_max_x; x++) { for (int y = scan_min_y; y <= scan_max_y; y++) { - if (field->visit_order[x][y] == 0) continue; + if ((field->visited[x] & (1ULL << y)) == 0) continue; int tile_x = field->arena_base_x + x; int tile_y = field->arena_base_y + y; - if (!field->is_walkable(field->walkable_ctx, tile_x, tile_y)) - continue; int depth = field->depth[x][y]; if (depth >= 100) continue; - int dsq = encounter_attack_seek_nearest_dsq( - tile_x, tile_y, seek_tiles, seek_count); + int dsq = encounter_target_rect_distance_squared( + tile_x, tile_y, target_x, target_y, target_size); if (dsq < best_dsq || (dsq == best_dsq && depth < best_depth)) { selected_x = x; @@ -1521,6 +1611,9 @@ static inline EncounterAttackRouteLanding encounter_arena_attack_route_landing( } } if (selected_x < 0) return landing; + landing.route_found = 1; + landing.route_x = field->arena_base_x + selected_x; + landing.route_y = field->arena_base_y + selected_y; int source_local_x = field->source_x - field->arena_base_x; int source_local_y = field->source_y - field->arena_base_y; @@ -1568,7 +1661,9 @@ static inline EncounterAttackRouteLanding encounter_arena_attack_route_landing( landing.land_y = field->arena_base_y + first_y; if (encounter_player_can_attack( landing.land_x, landing.land_y, - target_x, target_y, target_size, attack_range, los_query)) { + target_x, target_y, target_size, attack_range, + field->collision_map, field->world_offset_x, field->world_offset_y, + los_query)) { return landing; } if (selected_depth >= 2) { @@ -1578,6 +1673,58 @@ static inline EncounterAttackRouteLanding encounter_arena_attack_route_landing( return landing; } +#define ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS 25 + +static inline int encounter_attack_route_compress_waypoints( + const EncounterArenaAttackRouteField* field, + const EncounterAttackRouteLanding* landing, + int waypoint_x[ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS], + int waypoint_y[ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS] +) { + if (!landing->route_found || + (landing->route_x == field->source_x && + landing->route_y == field->source_y)) { + return 0; + } + + int source_x = field->source_x - field->arena_base_x; + int source_y = field->source_y - field->arena_base_y; + int current_x = landing->route_x - field->arena_base_x; + int current_y = landing->route_y - field->arena_base_y; + int current_direction = -1; + int waypoint_count = 0; + + while (current_x != source_x || current_y != source_y) { + int next_direction = field->via[current_x][current_y]; + if (next_direction == VIA_NONE || next_direction == VIA_START) { + fprintf(stderr, "broken attack route during compression\n"); + abort(); + } + if (current_direction != next_direction) { + current_direction = next_direction; + if (waypoint_count == ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS) + waypoint_count--; + memmove( + &waypoint_x[1], + &waypoint_x[0], + (size_t)waypoint_count * sizeof(waypoint_x[0])); + memmove( + &waypoint_y[1], + &waypoint_y[0], + (size_t)waypoint_count * sizeof(waypoint_y[0])); + waypoint_x[0] = field->arena_base_x + current_x; + waypoint_y[0] = field->arena_base_y + current_y; + waypoint_count++; + } + + if (current_direction & VIA_W) current_x++; + else if (current_direction & VIA_E) current_x--; + if (current_direction & VIA_S) current_y++; + else if (current_direction & VIA_N) current_y--; + } + return waypoint_count; +} + static inline PathResult encounter_pathfind_arena_attack_approach( const CollisionMap* cmap, int world_offset_x, int world_offset_y, int src_x, int src_y, @@ -1588,234 +1735,82 @@ static inline PathResult encounter_pathfind_arena_attack_approach( int arena_base_x, int arena_base_y, int arena_w, int arena_h ) { PathResult result = {0, 0, 0, src_x, src_y}; - - if (arena_w <= 0 || arena_w > PATHFIND_ARENA_MAX || - arena_h <= 0 || arena_h > PATHFIND_ARENA_MAX) { - fprintf(stderr, "attack approach arena dimensions out of bounds: %dx%d\n", - arena_w, arena_h); - abort(); - } - int local_src_x = src_x - arena_base_x; int local_src_y = src_y - arena_base_y; if (local_src_x < 0 || local_src_x >= arena_w || - local_src_y < 0 || local_src_y >= arena_h) { + local_src_y < 0 || local_src_y >= arena_h) { return result; } - EncounterAttackSeekTile seek_tiles[ENCOUNTER_ATTACK_SEEK_MAX_TILES]; - int seek_count = 0; - for (int xx = 0; xx < target_size; xx++) { - int x = target_x + xx; - encounter_attack_seek_add_tile( - seek_tiles, &seek_count, x, target_y - 1, - world_offset_x, world_offset_y, extra_blocked, blocked_ctx); - encounter_attack_seek_add_tile( - seek_tiles, &seek_count, x, target_y + target_size, - world_offset_x, world_offset_y, extra_blocked, blocked_ctx); - } - for (int yy = 0; yy < target_size; yy++) { - int y = target_y + yy; - encounter_attack_seek_add_tile( - seek_tiles, &seek_count, target_x - 1, y, - world_offset_x, world_offset_y, extra_blocked, blocked_ctx); - encounter_attack_seek_add_tile( - seek_tiles, &seek_count, target_x + target_size, y, - world_offset_x, world_offset_y, extra_blocked, blocked_ctx); - } - static OSRS_THREAD_LOCAL uint16_t approach_gen[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - static OSRS_THREAD_LOCAL int8_t approach_via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - static OSRS_THREAD_LOCAL int16_t approach_cost[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - static OSRS_THREAD_LOCAL uint16_t approach_gen_counter = 0; - approach_gen_counter++; - if (approach_gen_counter == 0) { - memset(approach_gen, 0, sizeof(approach_gen)); - approach_gen_counter = 1; - } - uint16_t gen = approach_gen_counter; - - #define APPROACH_VISITED(x, y) (approach_gen[(x)][(y)] == gen) - #define APPROACH_VISIT(x, y, v, c) do { \ - approach_gen[(x)][(y)] = gen; \ - approach_via[(x)][(y)] = (v); \ - approach_cost[(x)][(y)] = (c); \ - } while(0) - #define APPROACH_VIA(x, y) approach_via[(x)][(y)] - #define APPROACH_COST(x, y) approach_cost[(x)][(y)] - #define APPROACH_EB(x, y) \ - (extra_blocked && extra_blocked( \ - blocked_ctx, (x) + world_offset_x, (y) + world_offset_y)) - - int queue_x[PATHFIND_MAX_QUEUE_ARENA]; - int queue_y[PATHFIND_MAX_QUEUE_ARENA]; - int head = 0; - int tail = 0; - APPROACH_VISIT(local_src_x, local_src_y, VIA_START, 0); - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, - local_src_x, local_src_y); - - int selected_x = -1; - int selected_y = -1; - int selected_is_goal = 0; - int min_explored_x = local_src_x; - int min_explored_y = local_src_y; - int max_explored_x = local_src_x; - int max_explored_y = local_src_y; - - static const int dir_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int dir_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; - static const int dir_via[8] = { - VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE - }; - - while (head < tail) { - int cur_x = queue_x[head]; - int cur_y = queue_y[head]; - head++; - - int tile_x = arena_base_x + cur_x; - int tile_y = arena_base_y + cur_y; - if (is_walkable(ctx, tile_x, tile_y) && - !APPROACH_EB(tile_x, tile_y) && - (seek_count > 0 - ? encounter_attack_seek_has_exact_tile( - tile_x, tile_y, seek_tiles, seek_count) - : encounter_player_can_attack( - tile_x, tile_y, target_x, target_y, target_size, - attack_range, los_query))) { - selected_x = cur_x; - selected_y = cur_y; - selected_is_goal = 1; - break; - } - - int abs_x = tile_x + world_offset_x; - int abs_y = tile_y + world_offset_y; - int next_cost = APPROACH_COST(cur_x, cur_y) + 1; - - for (int i = 0; i < 8; i++) { - int dx = dir_dx[i]; - int dy = dir_dy[i]; - int next_x = cur_x + dx; - int next_y = cur_y + dy; - if (next_x < 0 || next_x >= arena_w || - next_y < 0 || next_y >= arena_h) - continue; - if (APPROACH_VISITED(next_x, next_y)) continue; - if (!collision_traversable_step(cmap, 0, abs_x, abs_y, dx, dy)) - continue; - - int next_tile_x = tile_x + dx; - int next_tile_y = tile_y + dy; - if (!is_walkable(ctx, next_tile_x, next_tile_y)) continue; - if (APPROACH_EB(next_tile_x, next_tile_y)) continue; - if (dx != 0 && dy != 0) { - if (APPROACH_EB(tile_x + dx, tile_y)) continue; - if (APPROACH_EB(tile_x, tile_y + dy)) continue; - if (!is_walkable(ctx, tile_x + dx, tile_y)) continue; - if (!is_walkable(ctx, tile_x, tile_y + dy)) continue; - } - - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, - next_x, next_y); - APPROACH_VISIT(next_x, next_y, dir_via[i], next_cost); - if (next_x < min_explored_x) min_explored_x = next_x; - if (next_y < min_explored_y) min_explored_y = next_y; - if (next_x > max_explored_x) max_explored_x = next_x; - if (next_y > max_explored_y) max_explored_y = next_y; - } + EncounterArenaAttackRouteField field; + encounter_build_arena_attack_route_field( + &field, + cmap, + world_offset_x, + world_offset_y, + src_x, + src_y, + target_x, + target_y, + target_size, + is_walkable, + ctx, + extra_blocked, + blocked_ctx, + arena_base_x, + arena_base_y, + arena_w, + arena_h); + EncounterAttackRouteLanding landing = + encounter_arena_attack_route_landing( + &field, + target_x, + target_y, + target_size, + attack_range, + los_query); + + int landing_x = landing.land_x - arena_base_x; + int landing_y = landing.land_y - arena_base_y; + if (landing_x < 0 || landing_x >= arena_w || + landing_y < 0 || landing_y >= arena_h) { + return result; } - - if (selected_x < 0 && seek_count > 0) { - int first_local_x = seek_tiles[0].x - arena_base_x; - int first_local_y = seek_tiles[0].y - arena_base_y; - int scan_min_x = min_explored_x > first_local_x - PATHFIND_MAX_FALLBACK_RADIUS - ? min_explored_x : first_local_x - PATHFIND_MAX_FALLBACK_RADIUS; - int scan_min_y = min_explored_y > first_local_y - PATHFIND_MAX_FALLBACK_RADIUS - ? min_explored_y : first_local_y - PATHFIND_MAX_FALLBACK_RADIUS; - int scan_max_x = max_explored_x > first_local_x + PATHFIND_MAX_FALLBACK_RADIUS - ? max_explored_x : first_local_x + PATHFIND_MAX_FALLBACK_RADIUS; - int scan_max_y = max_explored_y > first_local_y + PATHFIND_MAX_FALLBACK_RADIUS - ? max_explored_y : first_local_y + PATHFIND_MAX_FALLBACK_RADIUS; - if (scan_min_x < 0) scan_min_x = 0; - if (scan_min_y < 0) scan_min_y = 0; - if (scan_max_x >= arena_w) scan_max_x = arena_w - 1; - if (scan_max_y >= arena_h) scan_max_y = arena_h - 1; - - int best_dsq = 0x3fffffff; - int best_cost = 100; - for (int x = scan_min_x; x <= scan_max_x; x++) { - for (int y = scan_min_y; y <= scan_max_y; y++) { - if (!APPROACH_VISITED(x, y)) continue; - int tile_x = arena_base_x + x; - int tile_y = arena_base_y + y; - if (!is_walkable(ctx, tile_x, tile_y)) continue; - int cost = APPROACH_COST(x, y); - if (cost >= 100) continue; - int dsq = encounter_attack_seek_nearest_dsq( - tile_x, tile_y, seek_tiles, seek_count); - if (dsq < best_dsq || (dsq == best_dsq && cost < best_cost)) { - selected_x = x; - selected_y = y; - best_dsq = dsq; - best_cost = cost; - } - } - } + uint16_t landing_depth = field.depth[landing_x][landing_y]; + if (landing_depth == 0) { + result.found = encounter_player_can_attack( + src_x, src_y, + target_x, target_y, target_size, attack_range, + cmap, world_offset_x, world_offset_y, + los_query); + return result; + } + if (landing_depth > 2) { + fprintf(stderr, "attack route landing exceeded one run tick: %u\n", + (unsigned)landing_depth); + abort(); } - - int cur_x = selected_x; - int cur_y = selected_y; - int child_x = cur_x; - int child_y = cur_y; - if (selected_x < 0) goto approach_done; result.found = 1; - result.dest_x = arena_base_x + selected_x; - result.dest_y = arena_base_y + selected_y; - if (selected_x == local_src_x && selected_y == local_src_y) - goto approach_done; - - while (1) { - int v = APPROACH_VIA(cur_x, cur_y); - int prev_x = cur_x; - int prev_y = cur_y; - if (v & VIA_W) prev_x++; - else if (v & VIA_E) prev_x--; - if (v & VIA_S) prev_y++; - else if (v & VIA_N) prev_y--; - - if (prev_x == local_src_x && prev_y == local_src_y) { - result.next_dx = cur_x - local_src_x; - result.next_dy = cur_y - local_src_y; - if (selected_is_goal) { - result.run_dx = child_x - cur_x; - result.run_dy = child_y - cur_y; - } - goto approach_done; - } - - child_x = cur_x; - child_y = cur_y; - cur_x = prev_x; - cur_y = prev_y; - if (APPROACH_VIA(cur_x, cur_y) == VIA_NONE || - APPROACH_VIA(cur_x, cur_y) == VIA_START) { - result.found = 0; - result.next_dx = 0; - result.next_dy = 0; - goto approach_done; - } + result.dest_x = landing.land_x; + result.dest_y = landing.land_y; + if (landing_depth == 1) { + result.next_dx = landing.land_x - src_x; + result.next_dy = landing.land_y - src_y; + return result; } -approach_done: - #undef APPROACH_VISITED - #undef APPROACH_VISIT - #undef APPROACH_VIA - #undef APPROACH_COST - #undef APPROACH_EB + int first_x = landing_x; + int first_y = landing_y; + int via = field.via[landing_x][landing_y]; + if (via & VIA_W) first_x++; + else if (via & VIA_E) first_x--; + if (via & VIA_S) first_y++; + else if (via & VIA_N) first_y--; + result.next_dx = arena_base_x + first_x - src_x; + result.next_dy = arena_base_y + first_y - src_y; + result.run_dx = landing_x - first_x; + result.run_dy = landing_y - first_y; return result; } @@ -1853,10 +1848,6 @@ static inline int encounter_chase_attack_target( return steps > 0 ? 1 : 0; } - if (encounter_player_can_attack(p->x, p->y, target_x, target_y, - target_size, attack_range, - los_query)) - return 0; int cx, cy; cx = -1; @@ -1878,6 +1869,7 @@ static inline int encounter_chase_attack_target( if (!is_walkable(ctx, xx, yy)) continue; if (!encounter_player_can_attack(xx, yy, target_x, target_y, target_size, attack_range, + cmap, world_offset_x, world_offset_y, los_query)) continue; int dx = xx - p->x; @@ -1911,6 +1903,7 @@ static inline int encounter_chase_attack_target( for (int step = 0; step < 2; step++) { if (encounter_player_can_attack(p->x, p->y, target_x, target_y, target_size, attack_range, + cmap, world_offset_x, world_offset_y, los_query)) break; int next_dx; diff --git a/ocean/osrs/osrs_encounter_player.h b/ocean/osrs/osrs_encounter_player.h index 4dc2aa018f..5f4999a615 100644 --- a/ocean/osrs/osrs_encounter_player.h +++ b/ocean/osrs/osrs_encounter_player.h @@ -115,6 +115,9 @@ static inline int osrs_player_step_can_attack_target( target->y, target->size, target->attack_range, + input->arena.collision_map, + input->arena.world_offset_x, + input->arena.world_offset_y, input->arena.los_query); } @@ -154,28 +157,177 @@ static inline int osrs_player_step_apply_explicit_move( input->arena.arena_h); } -static inline int osrs_player_step_chase_target( +static inline int osrs_player_step_route_matches( + const OsrsInteractionRoute* route, + const Player* player, + const OsrsAttackTarget* target +) { + return route->state != OSRS_INTERACTION_ROUTE_EMPTY && + route->target_x == target->x && + route->target_y == target->y && + route->target_size == target->size && + route->attack_range == target->attack_range && + route->expected_player_x == player->x && + route->expected_player_y == player->y; +} + +static inline void osrs_player_step_build_attack_route( const OsrsPlayerStepInput* input, const OsrsAttackTarget* target ) { - return encounter_chase_attack_target( - input->player, - target->x, - target->y, - target->size, - target->attack_range, + OsrsInteractionRoute* route = &input->interaction->route; + route->target_x = target->x; + route->target_y = target->y; + route->target_size = target->size; + route->attack_range = target->attack_range; + route->expected_player_x = input->player->x; + route->planned_source_x = input->player->x; + route->planned_source_y = input->player->y; + route->expected_player_y = input->player->y; + route->waypoint_count = 0; + route->waypoint_index = 0; + + EncounterArenaAttackRouteField field; + encounter_build_arena_attack_route_field( + &field, input->arena.collision_map, input->arena.world_offset_x, input->arena.world_offset_y, + input->player->x, + input->player->y, + target->x, + target->y, + target->size, input->arena.is_walkable, input->arena.walkable_ctx, input->arena.extra_blocked, input->arena.blocked_ctx, - input->arena.los_query, input->arena.arena_base_x, input->arena.arena_base_y, input->arena.arena_w, input->arena.arena_h); + EncounterAttackRouteLanding landing = + encounter_arena_attack_route_landing( + &field, + target->x, + target->y, + target->size, + target->attack_range, + input->arena.los_query); + route->waypoint_count = encounter_attack_route_compress_waypoints( + &field, + &landing, + route->waypoint_x, + route->waypoint_y); + route->state = route->waypoint_count > 0 + ? OSRS_INTERACTION_ROUTE_READY + : OSRS_INTERACTION_ROUTE_FAILED; +} + +static inline int osrs_player_step_route_next_traversable( + const OsrsPlayerStepInput* input +) { + const OsrsInteractionRoute* route = &input->interaction->route; + if (route->state != OSRS_INTERACTION_ROUTE_READY || + route->waypoint_index >= route->waypoint_count) { + return 0; + } + int waypoint_x = route->waypoint_x[route->waypoint_index]; + int waypoint_y = route->waypoint_y[route->waypoint_index]; + int dx = (waypoint_x > input->player->x) - + (waypoint_x < input->player->x); + int dy = (waypoint_y > input->player->y) - + (waypoint_y < input->player->y); + return encounter_attack_route_step_traversable( + input->arena.collision_map, + input->arena.world_offset_x, + input->arena.world_offset_y, + input->player->x, + input->player->y, + dx, + dy, + input->arena.is_walkable, + input->arena.walkable_ctx, + input->arena.extra_blocked, + input->arena.blocked_ctx); +} + +static inline int osrs_player_step_chase_target( + const OsrsPlayerStepInput* input, + const OsrsAttackTarget* target +) { + Player* player = input->player; + OsrsInteractionRoute* route = &input->interaction->route; + int distance = encounter_rect_distance( + player->x, player->y, 1, + target->x, target->y, target->size); + if (input->arena.arena_w <= 0 || distance == 0) { + osrs_interaction_route_clear(input->interaction); + return encounter_chase_attack_target( + player, + target->x, + target->y, + target->size, + target->attack_range, + input->arena.collision_map, + input->arena.world_offset_x, + input->arena.world_offset_y, + input->arena.is_walkable, + input->arena.walkable_ctx, + input->arena.extra_blocked, + input->arena.blocked_ctx, + input->arena.los_query, + input->arena.arena_base_x, + input->arena.arena_base_y, + input->arena.arena_w, + input->arena.arena_h); + } + if (osrs_player_step_can_attack_target(input, target)) { + osrs_interaction_route_clear(input->interaction); + return 0; + } + + int remaining_waypoints = route->waypoint_count - route->waypoint_index; + int reroute = !osrs_player_step_route_matches(route, player, target) || + route->state == OSRS_INTERACTION_ROUTE_FAILED || + remaining_waypoints <= 1; + if (!reroute && !osrs_player_step_route_next_traversable(input)) + reroute = 1; + if (reroute) + osrs_player_step_build_attack_route(input, target); + if (route->state != OSRS_INTERACTION_ROUTE_READY || + !osrs_player_step_route_next_traversable(input)) { + route->expected_player_x = player->x; + route->expected_player_y = player->y; + return 0; + } + + int steps = 0; + while (steps < 2 && + route->waypoint_index < route->waypoint_count && + !osrs_player_step_can_attack_target(input, target)) { + int waypoint_x = route->waypoint_x[route->waypoint_index]; + int waypoint_y = route->waypoint_y[route->waypoint_index]; + if (player->x == waypoint_x && player->y == waypoint_y) { + route->waypoint_index++; + continue; + } + if (!osrs_player_step_route_next_traversable(input)) { + osrs_interaction_route_clear(input->interaction); + break; + } + player->x += (waypoint_x > player->x) - (waypoint_x < player->x); + player->y += (waypoint_y > player->y) - (waypoint_y < player->y); + steps++; + if (player->x == waypoint_x && player->y == waypoint_y) + route->waypoint_index++; + } + player->is_running = steps == 2; + player->dest_x = player->x; + player->dest_y = player->y; + route->expected_player_x = player->x; + route->expected_player_y = player->y; + return steps > 0; } static inline OsrsPlayerStepResult osrs_encounter_player_step( diff --git a/ocean/osrs/osrs_interaction.h b/ocean/osrs/osrs_interaction.h index b5bba0231b..79f72e8cbd 100644 --- a/ocean/osrs/osrs_interaction.h +++ b/ocean/osrs/osrs_interaction.h @@ -1,16 +1,50 @@ #ifndef OSRS_INTERACTION_H #define OSRS_INTERACTION_H +#define OSRS_INTERACTION_ROUTE_MAX_WAYPOINTS 25 + +typedef enum { + OSRS_INTERACTION_ROUTE_EMPTY = 0, + OSRS_INTERACTION_ROUTE_READY, + OSRS_INTERACTION_ROUTE_FAILED, +} OsrsInteractionRouteState; + +typedef struct { + OsrsInteractionRouteState state; + int target_x; + int target_y; + int target_size; + int attack_range; + int planned_source_x; + int planned_source_y; + int expected_player_x; + int expected_player_y; + int waypoint_count; + int waypoint_index; + int waypoint_x[OSRS_INTERACTION_ROUTE_MAX_WAYPOINTS]; + int waypoint_y[OSRS_INTERACTION_ROUTE_MAX_WAYPOINTS]; +} OsrsInteractionRoute; + typedef struct { int target_slot; + OsrsInteractionRoute route; } OsrsInteraction; +static inline void osrs_interaction_route_clear(OsrsInteraction* ix) { + ix->route.state = OSRS_INTERACTION_ROUTE_EMPTY; + ix->route.waypoint_count = 0; + ix->route.waypoint_index = 0; +} + static inline void osrs_interaction_set(OsrsInteraction* ix, int target_slot) { + if (ix->target_slot == target_slot) return; ix->target_slot = target_slot; + osrs_interaction_route_clear(ix); } static inline void osrs_interaction_clear(OsrsInteraction* ix) { ix->target_slot = -1; + osrs_interaction_route_clear(ix); } static inline int osrs_interaction_active(const OsrsInteraction* ix) { @@ -19,6 +53,7 @@ static inline int osrs_interaction_active(const OsrsInteraction* ix) { static inline void osrs_interaction_init(OsrsInteraction* ix) { ix->target_slot = -1; + osrs_interaction_route_clear(ix); } #define OSRS_IACT_NONE 0 diff --git a/ocean/osrs/osrs_inventory_clicks.h b/ocean/osrs/osrs_inventory_clicks.h index 9f098b1e05..b9ca713858 100644 --- a/ocean/osrs/osrs_inventory_clicks.h +++ b/ocean/osrs/osrs_inventory_clicks.h @@ -1,6 +1,7 @@ #ifndef OSRS_INVENTORY_CLICKS_H #define OSRS_INVENTORY_CLICKS_H +#include "osrs_item_effects.h" #include "osrs_inventory.h" #include "osrs_items.h" #include "osrs_consumables.h" @@ -149,6 +150,67 @@ static const OsrsConsumableClick OSRS_CONSUMABLE_CLICK_REGISTRY[] = { {3144, OSRS_CLICK_EAT, OSRS_CONSUMABLE_KARAMBWAN, 0}, }; +static inline int osrs_consumable_click_registry_index(uint16_t raw_osrs_id) { + switch (raw_osrs_id) { + case 6685: return 0; + case 6687: return 1; + case 6689: return 2; + case 6691: return 3; + case 3024: return 4; + case 3026: return 5; + case 3028: return 6; + case 3030: return 7; + case 10925: return 8; + case 10927: return 9; + case 10929: return 10; + case 10931: return 11; + case 12695: return 12; + case 12697: return 13; + case 12699: return 14; + case 12701: return 15; + case 23685: return 16; + case 23688: return 17; + case 23691: return 18; + case 23694: return 19; + case 2444: return 20; + case 169: return 21; + case 171: return 22; + case 173: return 23; + case 23733: return 24; + case 23736: return 25; + case 23739: return 26; + case 23742: return 27; + case 30875: return 28; + case 30878: return 29; + case 30881: return 30; + case 30884: return 31; + case 4417: return 32; + case 4419: return 33; + case 4421: return 34; + case 4423: return 35; + case 27641: return 36; + case 12913: return 37; + case 12915: return 38; + case 12917: return 39; + case 12919: return 40; + case 2434: return 41; + case 139: return 42; + case 141: return 43; + case 143: return 44; + case 22461: return 45; + case 22464: return 46; + case 22467: return 47; + case 22470: return 48; + case 12625: return 49; + case 12627: return 50; + case 12629: return 51; + case 12631: return 52; + case 385: return 53; + case 3144: return 54; + default: return -1; + } +} + static inline OsrsConsumableClick osrs_consumable_click_lookup_raw_osrs_id( uint16_t raw_osrs_id ); @@ -455,16 +517,8 @@ static inline OsrsConsumableKind osrs_item_click_consumable_kind(uint8_t item_id static inline OsrsConsumableClick osrs_consumable_click_lookup_raw_osrs_id( uint16_t raw_osrs_id ) { - int count = (int)( - sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / - sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0]) - ); - - for (int i = 0; i < count; i++) { - if (OSRS_CONSUMABLE_CLICK_REGISTRY[i].raw_osrs_id == raw_osrs_id) { - return OSRS_CONSUMABLE_CLICK_REGISTRY[i]; - } - } + int index = osrs_consumable_click_registry_index(raw_osrs_id); + if (index >= 0) return OSRS_CONSUMABLE_CLICK_REGISTRY[index]; return (OsrsConsumableClick){ .raw_osrs_id = raw_osrs_id, @@ -723,12 +777,10 @@ static inline int osrs_inventory_cell_obs_code(uint8_t item_idx, uint16_t raw_os return OSRS_INVENTORY_CELL_OBS_CODE_GEAR_BASE + item_idx; } if (raw_osrs_id == 0) return OSRS_INVENTORY_CELL_OBS_CODE_EMPTY; - for (int i = 0; i < OSRS_CONSUMABLE_CLICK_REGISTRY_COUNT; i++) { - if (OSRS_CONSUMABLE_CLICK_REGISTRY[i].raw_osrs_id == raw_osrs_id) { - return OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE + i; - } - } - return OSRS_INVENTORY_CELL_OBS_CODE_INERT; + int index = osrs_consumable_click_registry_index(raw_osrs_id); + return index >= 0 + ? OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE + index + : OSRS_INVENTORY_CELL_OBS_CODE_INERT; } static inline OsrsInventoryCell osrs_inventory_cell_for_obs_code(int code) { diff --git a/ocean/osrs/osrs_items_generated.h b/ocean/osrs/osrs_items_generated.h index 94bba8e6e5..94e60d83ad 100644 --- a/ocean/osrs/osrs_items_generated.h +++ b/ocean/osrs/osrs_items_generated.h @@ -1063,7 +1063,7 @@ static const Item ITEM_DATABASE[NUM_ITEMS] = { }, [ITEM_DRAGON_DART] = { .item_id = 11230, .name = "Dragon dart", .slot = SLOT_WEAPON, - .attack_speed = 3, .attack_range = 0, + .attack_speed = 3, .attack_range = 3, .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, .attack_magic = 0, .attack_ranged = 0, .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h index 1e65b105a7..98478e87d8 100644 --- a/ocean/osrs/osrs_render.h +++ b/ocean/osrs/osrs_render.h @@ -5216,6 +5216,7 @@ static void render_draw_3d_world(RenderClient* rc) { debug_inferno_state->player.y, npc->x, npc->y, npc->size, ls->attack_range, + NULL, 0, 0, &los_query); lc = can_atk ? GREEN : RED; } @@ -5379,7 +5380,9 @@ static void render_draw_overhead_status(RenderClient* rc, OsrsEnv* env) { int can_atk = encounter_player_can_attack( is->player.x, is->player.y, npc->x, npc->y, npc->size, - ls->attack_range, &los_query); + ls->attack_range, + NULL, 0, 0, + &los_query); const char* patk_txt = can_atk ? "P>NPC" : "P>NPC X"; Color patk_col = can_atk ? GREEN : RED; int pw = MeasureText(patk_txt, fs); diff --git a/ocean/osrs/osrs_visual.c b/ocean/osrs/osrs_visual.c index c59fcfccb3..15c4eacb22 100644 --- a/ocean/osrs/osrs_visual.c +++ b/ocean/osrs/osrs_visual.c @@ -423,6 +423,7 @@ static void run_profile( double elapsed = 0; int total_steps = 0; int enc_actions[64] = {0}; + const int pin_inventory_actions = getenv("OSRS_PROFILE_PIN_INV") != NULL; while ((profile_steps > 0 && total_steps < profile_steps) || (profile_steps <= 0 && elapsed < 10.0)) { @@ -443,7 +444,7 @@ static void run_profile( for (int h = 0; h < edef->num_action_heads; h++) { enc_actions[h] = rand() % edef->action_head_dims[h]; } - if (getenv("OSRS_PROFILE_PIN_INV")) { + if (pin_inventory_actions) { for (int h = 2; h < 15 && h < edef->num_action_heads; h++) enc_actions[h] = 0; } diff --git a/ocean/osrs/tests/test_colosseum_forecast_exact.c b/ocean/osrs/tests/test_colosseum_forecast_exact.c index b859e58a4d..bf894613ce 100644 --- a/ocean/osrs/tests/test_colosseum_forecast_exact.c +++ b/ocean/osrs/tests/test_colosseum_forecast_exact.c @@ -354,7 +354,6 @@ static int exact_attack_route_walkable(void* ctx, int x, int y) { static int exact_attack_route_blocked(void* ctx, int abs_x, int abs_y) { ExactAttackRouteContext* route_ctx = (ExactAttackRouteContext*)ctx; - if (col_pathfind_blocked(&route_ctx->walk, abs_x, abs_y)) return 1; int x = abs_x - route_ctx->walk.ctx->world_offset_x; int y = abs_y - route_ctx->walk.ctx->world_offset_y; for (int i = 0; i < route_ctx->blocker_count; i++) { @@ -407,22 +406,6 @@ static int exact_attack_route_property_scenario( ColosseumContext* ctx, ExactAttackRouteContext* route_ctx ) { - EncounterArenaAttackRouteField field; - encounter_build_arena_attack_route_field( - &field, - ctx->collision_map, - ctx->world_offset_x, - ctx->world_offset_y, - s->player.x, - s->player.y, - exact_attack_route_walkable, - route_ctx, - exact_attack_route_blocked, - route_ctx, - COLO_ARENA_MIN_X, - COLO_ARENA_MIN_Y, - COLO_ARENA_WIDTH, - COLO_ARENA_HEIGHT); OsrsLosQuery los_query = col_player_los_query(s); static const int target_sizes[] = {1, 2, 5}; static const int attack_ranges[] = {1, 3, 6, 10}; @@ -437,10 +420,29 @@ static int exact_attack_route_property_scenario( for (size_t size_idx = 0; size_idx < sizeof(target_sizes) / sizeof(target_sizes[0]); size_idx++) { + int target_size = target_sizes[size_idx]; + EncounterArenaAttackRouteField field; + encounter_build_arena_attack_route_field( + &field, + ctx->collision_map, + ctx->world_offset_x, + ctx->world_offset_y, + s->player.x, + s->player.y, + target_x, + target_y, + target_size, + exact_attack_route_walkable, + route_ctx, + exact_attack_route_blocked, + route_ctx, + COLO_ARENA_MIN_X, + COLO_ARENA_MIN_Y, + COLO_ARENA_WIDTH, + COLO_ARENA_HEIGHT); for (size_t range_idx = 0; range_idx < sizeof(attack_ranges) / sizeof(attack_ranges[0]); range_idx++) { - int target_size = target_sizes[size_idx]; int attack_range = attack_ranges[range_idx]; EncounterAttackRouteLanding expected = exact_attack_route_runtime_landing( diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index 0437b4b896..a00210211a 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -2611,13 +2611,10 @@ static void test_player_walks_through_npc_footprint(void) { int npc_flag = col_grid_index(17, 16, &gx, &gy) && s.npc_collision_flags[gx][gy]; int player_flag = col_grid_index(17, 16, &gx, &gy) && gx == s.player_grid_x && gy == s.player_grid_y; - ColoWalkCtx wc = { .s = &s, .ctx = &ctx }; CHECK("NPC footprint remains stamped for NPC systems", npc_flag != 0); CHECK("NPC footprint is not stamped as player collision", player_flag == 0); CHECK("player walkability ignores NPC footprint", col_player_walkable(&s, 17, 16) == 1); - CHECK("player pathfinding extra block ignores NPC footprint", - col_pathfind_blocked(&wc, 17 + ctx.world_offset_x, 16 + ctx.world_offset_y) == 0); int actions[COLO_NUM_ACTION_HEADS] = {0}; actions[COLO_HEAD_PRIMARY] = forecast_move_action_for_delta(1, 0); step_and_observe(&s, &ctx, actions); @@ -5227,8 +5224,8 @@ static void test_loadout_divine_potions_and_stat_drift(void) { s.divine_ranged_timer = 234; ColoSnapshot snap; col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); - CHECK("snapshot version is v22 for exact NPC death telemetry", - snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 22u); + CHECK("snapshot version is v23 for interaction route state", + snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 23u); ColosseumState restored; memset(&restored, 0, sizeof(restored)); col_restore_ctx((EncounterState*)&restored, (EncounterContext*)&ctx, &snap, sizeof(snap)); @@ -6057,7 +6054,7 @@ static void test_combat_fidelity_contract_sizes(void) { CHECK("modifier block has 60 features", COLO_MODIFIER_OBS_SIZE == 60); CHECK("NPC slots carry a type code, not a type one-hot", COLO_FEATURES_PER_NPC == 23); - CHECK("snapshot version is v22", COLO_SNAPSHOT_VERSION == 22u); + CHECK("snapshot version is v23", COLO_SNAPSHOT_VERSION == 23u); CHECK("every active NPC gets an obs slot (no busy-wave drop)", COLO_OBS_NPCS == 24 && COLO_OBS_NPCS == COLO_MAX_NPCS); CHECK("PRIMARY head covers noop, movement, and NPC obs slots", @@ -6725,7 +6722,7 @@ static void test_combat_fidelity_snapshot_roundtrip(void) { ColoSnapshot snap; col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); - CHECK("snapshot frame is v22", snap.version == 22u); + CHECK("snapshot frame is v23", snap.version == 23u); ColosseumState restored; memset(&restored, 0, sizeof(restored)); @@ -7065,11 +7062,13 @@ static void test_osrs_los_query_contracts(void) { printf("test_osrs_los_query_contracts\n"); OsrsLosQuery open_query = osrs_los_open(); CHECK("explicit open LoS permits a ranged attack", - encounter_player_can_attack(0, 0, 4, 0, 1, 10, &open_query) == 1); + encounter_player_can_attack( + 0, 0, 4, 0, 1, 10, NULL, 0, 0, &open_query) == 1); OsrsLosQuery tile_query = osrs_los_tile(test_los_every_tile_blocked, NULL); CHECK("tile LoS refuses when every tile blocks", - encounter_player_can_attack(0, 0, 4, 0, 1, 10, &tile_query) == 0); + encounter_player_can_attack( + 0, 0, 4, 0, 1, 10, NULL, 0, 0, &tile_query) == 0); } static void test_player_ranged_los_blocked_by_pillar(void) { @@ -7096,7 +7095,9 @@ static void test_player_ranged_los_blocked_by_pillar(void) { CHECK("shared tile LoS blocks the same pillar line", encounter_player_can_attack(s.player.x, s.player.y, npc->x, npc->y, col_npc_effective_size(npc), - col_player_attack_range(&s), &los_query) == 0); + col_player_attack_range(&s), + ctx.collision_map, ctx.world_offset_x, ctx.world_offset_y, + &los_query) == 0); s.player.x = 13; s.player.y = 4; col_rebuild_player_collision_flags(&s); @@ -7138,7 +7139,9 @@ static void test_player_chase_routes_around_pillar_for_los(void) { CHECK("start tile is range-valid and LoS-blocked", encounter_player_can_attack(s.player.x, s.player.y, npc->x, npc->y, col_npc_effective_size(npc), - attack_range, &los_query) == 0); + attack_range, + ctx.collision_map, ctx.world_offset_x, ctx.world_offset_y, + &los_query) == 0); int actions[COLO_NUM_ACTION_HEADS] = {0}; int attacked_tick = -1; @@ -8226,24 +8229,35 @@ static void test_melee_reach_cardinal_vs_diagonal(void) { CHECK("reach-1 helper rejects a diagonal corner", encounter_entity_footprint_cardinal_adjacent(cx, cy, 1, tx, ty, tsize) == 0); CHECK("range-1 gate rejects a diagonal corner", - encounter_player_can_attack(cx, cy, tx, ty, tsize, 1, open) == 0); + encounter_player_can_attack( + cx, cy, tx, ty, tsize, 1, NULL, 0, 0, open) == 0); CHECK("range-2 (halberd) gate allows a diagonal corner", - encounter_player_can_attack(cx, cy, tx, ty, tsize, 2, open) == 1); + encounter_player_can_attack( + cx, cy, tx, ty, tsize, 2, NULL, 0, 0, open) == 1); } for (int k = 0; k < tsize; k++) { CHECK("range-1 gate allows a west cardinal-edge tile", - encounter_player_can_attack(tx - 1, ty + k, tx, ty, tsize, 1, open) == 1); + encounter_player_can_attack( + tx - 1, ty + k, tx, ty, tsize, 1, + NULL, 0, 0, open) == 1); CHECK("range-1 gate allows an east cardinal-edge tile", - encounter_player_can_attack(tx + tsize, ty + k, tx, ty, tsize, 1, open) == 1); + encounter_player_can_attack( + tx + tsize, ty + k, tx, ty, tsize, 1, + NULL, 0, 0, open) == 1); CHECK("range-1 gate allows a south cardinal-edge tile", - encounter_player_can_attack(tx + k, ty - 1, tx, ty, tsize, 1, open) == 1); + encounter_player_can_attack( + tx + k, ty - 1, tx, ty, tsize, 1, + NULL, 0, 0, open) == 1); CHECK("range-1 gate allows a north cardinal-edge tile", - encounter_player_can_attack(tx + k, ty + tsize, tx, ty, tsize, 1, open) == 1); + encounter_player_can_attack( + tx + k, ty + tsize, tx, ty, tsize, 1, + NULL, 0, 0, open) == 1); } CHECK("overlap is never meleeable", - encounter_player_can_attack(tx, ty, tx, ty, tsize, 1, open) == 0); + encounter_player_can_attack( + tx, ty, tx, ty, tsize, 1, NULL, 0, 0, open) == 0); } } diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index f64bbb68bb..87459e03ab 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -1524,7 +1524,8 @@ static void test_idle_diagnostics_count_missed_attack_opportunities(void) { ASSERT_INT_EQ("no immediate threat", inf_player_has_immediate_threat(&state), 0); - inf_record_idle_diagnostics(&state, 1, 1, 1, 1); + inf_record_idle_diagnostics( + &state, 1, 1, INF_IDLE_PHASE_SET, 1, 1, 1); ASSERT_INT_EQ("attack ready no attack total", state.total_attack_ready_no_attack_ticks, 1); @@ -2436,7 +2437,7 @@ static void test_player_movement_ignores_npc_collision_flags(void) { inf_legacy_context()->collision_map, inf_legacy_context()->world_offset_x, inf_legacy_context()->world_offset_y, - inf_tile_walkable, &walk_ctx, inf_pathfind_blocked, &walk_ctx, + inf_tile_walkable, &walk_ctx, NULL, NULL, INF_ARENA_MIN_X, INF_ARENA_MIN_Y, INF_ARENA_WIDTH, INF_ARENA_HEIGHT); ASSERT_INT_EQ("player runs through npc footprints", steps, 2); @@ -2689,10 +2690,7 @@ static void chase_block_test_run_until_attackable( int target_x, int target_y, int target_size, int attack_range, const OsrsLosQuery* los_query ) { - for (int i = 0; i < 12 && !encounter_player_can_attack( - player->x, player->y, - target_x, target_y, target_size, attack_range, - los_query); i++) { + for (int i = 0; i < 12 && !encounter_player_can_attack(player->x, player->y, target_x, target_y, target_size, attack_range, NULL, 0, 0, los_query); i++) { int moved = encounter_chase_attack_target( player, target_x, target_y, target_size, attack_range, @@ -2737,7 +2735,7 @@ static void test_attack_chase_uses_reachable_approach_tile(void) { chase_block_test_run_until_attackable(&player, &blocks, 5, 5, 1, 3, &los_query); ASSERT_INT_EQ("chase reaches a reachable attack tile", - encounter_player_can_attack(player.x, player.y, 5, 5, 1, 3, &los_query), 1); + encounter_player_can_attack(player.x, player.y, 5, 5, 1, 3, NULL, 0, 0, &los_query), 1); } static void test_attack_chase_routes_around_los_blocker_while_in_range(void) { @@ -2756,7 +2754,7 @@ static void test_attack_chase_routes_around_los_blocker_while_in_range(void) { player.y = 0; ASSERT_INT_EQ("starting tile is range-valid but LOS-blocked", - encounter_player_can_attack(player.x, player.y, 5, 0, 1, 10, &los_query), 0); + encounter_player_can_attack(player.x, player.y, 5, 0, 1, 10, NULL, 0, 0, &los_query), 0); int moved = encounter_chase_attack_target( &player, @@ -2772,7 +2770,7 @@ static void test_attack_chase_routes_around_los_blocker_while_in_range(void) { chase_block_test_has_block(&blocks, player.x, player.y), 0); chase_block_test_run_until_attackable(&player, &blocks, 5, 0, 1, 10, &los_query); ASSERT_INT_EQ("chase reaches a clear long-range attack tile", - encounter_player_can_attack(player.x, player.y, 5, 0, 1, 10, &los_query), 1); + encounter_player_can_attack(player.x, player.y, 5, 0, 1, 10, NULL, 0, 0, &los_query), 1); } static void test_attack_chase_routes_around_los_blocker_for_short_range(void) { @@ -2804,7 +2802,7 @@ static void test_attack_chase_routes_around_los_blocker_for_short_range(void) { chase_block_test_has_block(&blocks, player.x, player.y), 0); chase_block_test_run_until_attackable(&player, &blocks, 5, 0, 1, 3, &los_query); ASSERT_INT_EQ("chase reaches a clear short-range attack tile", - encounter_player_can_attack(player.x, player.y, 5, 0, 1, 3, &los_query), 1); + encounter_player_can_attack(player.x, player.y, 5, 0, 1, 3, NULL, 0, 0, &los_query), 1); } static void test_melee_fallback_geometry(void) { @@ -6677,6 +6675,7 @@ static void test_zuk_obs_tracks_shield_and_mager_aggro(void) { state.npcs[2].active = 0; state.zuk.shield_idx = -1; state.npcs[0].aggro_target = -1; + inf_invalidate_current_obs_slots(&state); memset(obs, 0, sizeof(obs)); memset(mask, 0, sizeof(mask)); @@ -7058,14 +7057,19 @@ static void test_human_targeting_refreshes_stale_obs_slots(void) { for (int i = 0; i < INF_OBS_NPCS; i++) { state.current_obs_slots[i] = -1; } + inf_invalidate_current_obs_slots(&state); state.npcs[5] = make_test_npc( INF_NPC_RANGER, 24, 24, INF_NPC_STATS[INF_NPC_RANGER].size); state.npcs[5].active = 1; state.npcs[5].hp = state.npcs[5].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + ASSERT_INT_EQ("stale targetability slots start invalid", + state.current_obs_slots_valid, 0); ASSERT_INT_EQ("live ranger is targetable without prior obs write", inf_is_human_targetable_npc_slot((EncounterState*)&state, 5), 1); + ASSERT_INT_EQ("targetability refresh validates obs slots", + state.current_obs_slots_valid, 1); inf_refresh_current_obs_slots(&state); int ranger_slot = inf_find_target_obs_slot(&state, 5); @@ -7077,7 +7081,12 @@ static void test_human_targeting_refreshes_stale_obs_slots(void) { int actions[INF_NUM_ACTION_HEADS]; hi.pending_target_idx = 5; state.current_obs_slots[ranger_slot] = -1; + inf_invalidate_current_obs_slots(&state); + ASSERT_INT_EQ("pending target slots start invalid", + state.current_obs_slots_valid, 0); inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("pending target refresh validates obs slots", + state.current_obs_slots_valid, 1); ASSERT_INT_EQ("pending human target refreshes obs slot", actions[INF_HEAD_TARGET], ranger_slot + 1); } @@ -7088,7 +7097,12 @@ static void test_human_targeting_refreshes_stale_obs_slots(void) { human_input_init(&hi); human_input_queue_attack_npc(&hi, 5); state.current_obs_slots[ranger_slot] = -1; + inf_invalidate_current_obs_slots(&state); + ASSERT_INT_EQ("queued target slots start invalid", + state.current_obs_slots_valid, 0); inf_translate_human_commands(&hi, actions, &state); + ASSERT_INT_EQ("queued target refresh validates obs slots", + state.current_obs_slots_valid, 1); ASSERT_INT_EQ("queued human target refreshes obs slot", actions[INF_HEAD_TARGET], ranger_slot + 1); human_input_destroy(&hi); @@ -8604,7 +8618,7 @@ static void test_inferno_binding_forwards_offensive_prayer_reward(void) { "config/osrs_inferno.ini", "[env]", "[vec]", - "offensive_prayer_reward_coeff = 0.0"); + "offensive_prayer_reward_coeff = 0"); ASSERT_SOURCE_BLOCK_CONTAINS( "offensive prayer sweep config", "config/osrs_inferno.ini", @@ -8651,7 +8665,7 @@ static void test_inferno_binding_forwards_curriculum_supply_config(void) { "config/osrs_inferno.ini", "[env]", "[vec]", - "curriculum_supply_jitter_mode = 0"); + "curriculum_supply_jitter_mode = 1"); ASSERT_SOURCE_BLOCK_CONTAINS( "curriculum no-brew defaults off", "config/osrs_inferno.ini", diff --git a/ocean/osrs/tests/test_inferno_golden.c b/ocean/osrs/tests/test_inferno_golden.c index 481628d8af..a49b466cee 100644 --- a/ocean/osrs/tests/test_inferno_golden.c +++ b/ocean/osrs/tests/test_inferno_golden.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -45,7 +46,12 @@ typedef struct { } TraceHashes; static uint64_t hash_core_state(uint64_t h, const InfernoState* s) { - h = fnv_bytes(h, &s->player, sizeof(s->player)); + h = fnv_bytes(h, &s->player, offsetof(Player, interaction)); + h = fnv_i32(h, s->player.interaction.target_slot); + h = fnv_bytes( + h, + &s->player.item_effect_state, + sizeof(s->player) - offsetof(Player, item_effect_state)); h = fnv_bytes(h, s->pillars, sizeof(s->pillars)); h = fnv_bytes(h, s->npcs, TRACE_NPC_SLOTS * sizeof(s->npcs[0])); h = fnv_bytes(h, &s->player_pending_hits, sizeof(s->player_pending_hits)); @@ -167,32 +173,32 @@ static const GoldenConfig CONFIGS[] = { #define EPISODE_TICKS 2000 static const uint64_t EXPECTED_STATE[NUM_CONFIGS] = { - 0x443ad0be6700d3ccULL, 0x17bf6a8136354a13ULL, 0xe847a00300ff389aULL, - 0x24561a696000f6aaULL, 0x29ef822b2984ead4ULL, 0x37cd7f6db4d4a69bULL, - 0x843298acc61300d5ULL, 0x61df8fa3a31ce410ULL, 0xc147b4e946a9ef62ULL, - 0xb0cc89838a07a28eULL, 0x38e2a2100ce6aad0ULL, 0x68c2e25a45de3330ULL, - 0x4276518d9178c88aULL, 0x8c4d39c065b913ffULL, 0xfb06a88261725c65ULL, + 0xc34a1be07be9b76cULL, 0xe0b7035532045326ULL, 0x9bfdcadcd40dd503ULL, + 0xef441bdc10b4543aULL, 0x6bdf53afedce0649ULL, 0x567ee6344c8b711fULL, + 0x292f4392c8186e26ULL, 0x3d1c234ca61bbf8cULL, 0xe983644326ae38a6ULL, + 0xf572a5e51102d345ULL, 0x093845fc28bd3edfULL, 0x8bec31a8997b0e6dULL, + 0x4276518d9178c88aULL, 0x67a1cc0619493f62ULL, 0x65a6a9dfbe029b15ULL, }; static const uint64_t EXPECTED_REWARD[NUM_CONFIGS] = { - 0x8fb194fc3b735b9aULL, 0x91bc1d01b9d474aaULL, 0xd8288292f3160f5aULL, - 0x1f71cc42bc243e13ULL, 0x9ccbff0cc6408633ULL, 0x49a0f296876aea23ULL, + 0x7a65310944a973aaULL, 0xa122490e8a5ea4faULL, 0xd8288292f3160f5aULL, + 0x1f71cc42bc243e13ULL, 0x65af16b1d7e922caULL, 0x49a0f296876aea23ULL, 0xc4080cd4d9a6eb13ULL, 0x9d01d9da22e0bb93ULL, 0x6c34cdb27c0c1733ULL, 0x29916e1218e670b3ULL, 0x6494cc221d5e7db3ULL, 0x11a8e2781441fd33ULL, 0x5d411311ad9cd553ULL, 0xe7f1c65d07654513ULL, 0x47a1f9c7b1d3def3ULL, }; static const uint64_t EXPECTED_MASK[NUM_CONFIGS] = { - 0x07b15753e2efc64aULL, 0x48bb2a98696ebfb3ULL, 0x9787e973169329baULL, - 0xe97fa334ceaf6153ULL, 0xf4239d431bef2aeaULL, 0x0ea03aa9e92854e3ULL, + 0x9609313d75a829baULL, 0x34109efbca6fab0aULL, 0x9787e973169329baULL, + 0xe97fa334ceaf6153ULL, 0xd2dc555e39a58ba3ULL, 0x0ea03aa9e92854e3ULL, 0xc2432dc597e3e483ULL, 0xbf39837ae26a6aaaULL, 0x7fc9129b3356ed93ULL, 0xe54fb95b499c2003ULL, 0xd0bb60198af9b643ULL, 0xa01ad9cfc96241c3ULL, 0x49744aaffe686a73ULL, 0x5b55145ce7296033ULL, 0xfd42b38660125883ULL, }; static const uint64_t EXPECTED_TERMINAL[NUM_CONFIGS] = { - 0x37cd079d32ad9e72ULL, 0xce955ae918947853ULL, 0x8799762912bdf103ULL, - 0x622fee1940c541abULL, 0x9c2e107bb54a1e8bULL, 0x2629f248900d0043ULL, + 0xaacea6292d1dbfd3ULL, 0x08f8c8682da946a1ULL, 0x8799762912bdf103ULL, + 0x622fee1940c541abULL, 0x82fbcdc1da23c683ULL, 0x2629f248900d0043ULL, 0xc92dd3ae1e3522b2ULL, 0xbb44db79d20b1d81ULL, 0xa10f828fd67b1801ULL, 0xe2d9ed2b31cf44a1ULL, 0x448b70e96c7c46a1ULL, 0xca591485885081a1ULL, 0xd05fd043497d9b27ULL, 0xf92b462e21d67e67ULL, 0x12e57f8f9818c547ULL, @@ -207,11 +213,11 @@ static const uint64_t LEGACY_OBSERVATION[NUM_CONFIGS] = { }; static const uint64_t COMPACT_OBSERVATION[NUM_CONFIGS] = { - 0xf6b0c1dd0bcd0caaULL, 0xf6383c0531b500aeULL, 0x02d82f1ec0b39b31ULL, - 0xd2e90acf4a8889e3ULL, 0x92a416e84ff2ffbdULL, 0x24c8a4ade044fffbULL, - 0xe314373c080ea591ULL, 0xc6986cd9d1827d8dULL, 0x63f33b2c81af5dbfULL, - 0x318376fbc0ee582aULL, 0x85ebe0fa55f1f53bULL, 0x905f13ba928c7394ULL, - 0xbf34279c73e947f4ULL, 0xfe224b03850e2f95ULL, 0x72ee3c9abde4352dULL, + 0xf2362a766a6212d1ULL, 0xa87ad065ca1666dcULL, 0xcac6b26329cadeafULL, + 0xda3602f8cf48fd1fULL, 0x9898fdcd480dc4e6ULL, 0x7c305c0b8123853fULL, + 0x9fb81d2ca3477306ULL, 0xb7e475f336034eadULL, 0x73f5ac59f227cb67ULL, + 0xda647dd6d85cfbe9ULL, 0x78ff2080d00d29c0ULL, 0x83cb4a3947e97146ULL, + 0xbf34279c73e947f4ULL, 0xdf399dc7aee3d3f3ULL, 0x989c8fc6b6aea06dULL, }; static int check_hash( diff --git a/ocean/osrs/tests/test_osrs_inventory_actions.c b/ocean/osrs/tests/test_osrs_inventory_actions.c index e9ec45c1ff..7e5bcbdd42 100644 --- a/ocean/osrs/tests/test_osrs_inventory_actions.c +++ b/ocean/osrs/tests/test_osrs_inventory_actions.c @@ -144,9 +144,22 @@ static void run_drink_dose_check(void) { !osrs_inventory_tick_intent_has_effect(&intent)); } +static void test_weapon_ranges(void) { + const Item* dragon_dart = get_item(ITEM_DRAGON_DART); + ASSERT_INT_EQ("dragon dart weapon range", dragon_dart->attack_range, 3); + + uint8_t unarmed_loadout[NUM_GEAR_SLOTS]; + memset(unarmed_loadout, ITEM_NONE, sizeof(unarmed_loadout)); + EquipmentBonuses unarmed = {0}; + osrs_sum_equipment_bonuses(unarmed_loadout, &unarmed); + ASSERT_INT_EQ("unarmed attack speed", unarmed.attack_speed, 4); + ASSERT_INT_EQ("unarmed attack range", unarmed.attack_range, 1); +} + int main(void) { for (int trial = 0; trial < 500; trial++) run_equip_storm(trial); run_drink_dose_check(); + test_weapon_ranges(); return osrs_test_summary(); } diff --git a/ocean/osrs/tests/test_osrs_inventory_clicks.c b/ocean/osrs/tests/test_osrs_inventory_clicks.c index b8e291a521..e51e1dc5fc 100644 --- a/ocean/osrs/tests/test_osrs_inventory_clicks.c +++ b/ocean/osrs/tests/test_osrs_inventory_clicks.c @@ -43,6 +43,17 @@ static int test_raw_consumable_classification(void) { return 0; } +static int test_consumable_registry_index_covers_every_row(void) { + for (int i = 0; i < OSRS_CONSUMABLE_CLICK_REGISTRY_COUNT; i++) { + CHECK("registered raw id maps to its row", + osrs_consumable_click_registry_index( + OSRS_CONSUMABLE_CLICK_REGISTRY[i].raw_osrs_id) == i); + } + CHECK("unknown raw id has no row", + osrs_consumable_click_registry_index(9999) == -1); + return 0; +} + static int test_sara_brew_dose_variants(void) { OsrsConsumableClick brew4 = osrs_consumable_click_lookup_raw_osrs_id(6685); @@ -345,6 +356,7 @@ int main(void) { if (test_item_index_classification()) return 1; if (test_raw_consumable_classification()) return 1; if (test_sara_brew_dose_variants()) return 1; + if (test_consumable_registry_index_covers_every_row()) return 1; if (test_dose_after_drink()) return 1; if (test_pure_click_interpreter()) return 1; if (test_cell_click_classification()) return 1; diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c index 611b19da0b..d8ce22dce0 100644 --- a/ocean/osrs/tests/test_osrs_player_step.c +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -30,11 +30,26 @@ typedef struct { OsrsAttackTarget target; int target_valid; } StepTargetCtx; +typedef struct { + int calls; +} StepWalkableCountCtx; + static int step_tile_walkable(void* ctx, int x, int y) { (void)ctx; return x >= 0 && x < STEP_GRID && y >= 0 && y < STEP_GRID; } +static int step_counted_tile_walkable(void* ctx, int x, int y) { + StepWalkableCountCtx* count = (StepWalkableCountCtx*)ctx; + count->calls++; + return x >= 0 && x < STEP_GRID && y >= 0 && y < STEP_GRID; +} + + +static int step_vertical_wall(void* ctx, int x, int y) { + (void)ctx; + return x == 5 && y <= 4; +} static int step_lookup_target(void* ctx, int target_slot, OsrsAttackTarget* out) { StepTargetCtx* c = (StepTargetCtx*)ctx; @@ -53,6 +68,103 @@ static OsrsEncounterArena step_arena(void) { return arena; } +static void test_attack_route_rejects_cardinal_adjacency_through_wall(void) { + CollisionMap map; + CollisionRegion region; + collision_map_init(&map); + memset(®ion, 0, sizeof(region)); + collision_map_put(&map, collision_region_hash(9, 10), ®ion); + region.flags[0][9][10] = COLLISION_WALL_EAST; + region.flags[0][10][10] = COLLISION_WALL_WEST; + + const OsrsLosQuery* los = osrs_los_open_query(); + PathResult route = encounter_pathfind_arena_attack_approach( + &map, 0, 0, + 9, 10, + 10, 10, 1, 1, + step_tile_walkable, NULL, + NULL, NULL, + los, + 0, 0, STEP_GRID, STEP_GRID); + + CHECK("wall-adjacent route moves around the wall", + route.found && (route.next_dx != 0 || route.next_dy != 0)); +} + +static void test_attack_route_fallback_stays_within_ten_tiles(void) { + const OsrsLosQuery* los = osrs_los_open_query(); + PathResult route = encounter_pathfind_arena_attack_approach( + NULL, 0, 0, + 10, 10, + -20, 10, 1, 1, + step_tile_walkable, NULL, + NULL, NULL, + los, + 0, 0, STEP_GRID, STEP_GRID); + + CHECK("fallback outside the ten-tile target radius fails", !route.found); +} + +static void test_attack_route_uses_rsmod_cardinal_first_order(void) { + PathResult route = encounter_pathfind_arena_attack_approach( + NULL, 0, 0, + 10, 10, + 8, 8, 1, 1, + step_tile_walkable, NULL, + NULL, NULL, + osrs_los_open_query(), + 0, 0, STEP_GRID, STEP_GRID); + + CHECK("RSMod route takes west before southwest", + route.found && + route.next_dx == -1 && route.next_dy == 0 && + route.run_dx == -1 && route.run_dy == -1); +} + +static void test_melee_attack_does_not_cross_cardinal_wall(void) { + CollisionMap map; + CollisionRegion region; + collision_map_init(&map); + memset(®ion, 0, sizeof(region)); + collision_map_put(&map, collision_region_hash(9, 10), ®ion); + region.flags[0][9][10] = COLLISION_WALL_EAST; + region.flags[0][10][10] = COLLISION_WALL_WEST; + + Player player; + memset(&player, 0, sizeof(player)); + player.x = 9; + player.y = 10; + + OsrsInteraction interaction; + osrs_interaction_init(&interaction); + osrs_interaction_set(&interaction, 3); + + StepTargetCtx tctx = { + .target = { .slot = 3, .x = 10, .y = 10, .size = 1, .attack_range = 1 }, + .target_valid = 1, + }; + int dest_x = -1; + int dest_y = -1; + OsrsEncounterArena arena = step_arena(); + arena.collision_map = ↦ + + OsrsPlayerStepInput input; + memset(&input, 0, sizeof(input)); + input.player = &player; + input.interaction = &interaction; + input.target_lookup = step_lookup_target; + input.target_ctx = &tctx; + input.dest_x = &dest_x; + input.dest_y = &dest_y; + input.arena = arena; + + OsrsPlayerStepResult result = osrs_encounter_player_step(&input); + + CHECK("melee does not attack through cardinal wall", + !result.can_attack || player.x != 9 || player.y != 10); + CHECK("melee routes around cardinal wall", result.chased_target); +} + /* an entity click cancels a walk already in flight: the player closes on the target instead of continuing to the clicked tile. this is the nh_pvp/zulrah regression — EXPLICIT_FIRST used to honour both, giving free damage while @@ -175,10 +287,227 @@ static void test_none_command_chases_active_interaction(void) { CHECK("no explicit move ran", r.explicit_moved == 0); } +static void test_attack_route_persists_and_invalidates_canonically(void) { + Player player; + memset(&player, 0, sizeof(player)); + player.x = 2; + player.y = 2; + + OsrsInteraction interaction; + osrs_interaction_init(&interaction); + osrs_interaction_set(&interaction, 3); + + StepTargetCtx target_ctx = { + .target = { .slot = 3, .x = 10, .y = 2, .size = 1, .attack_range = 1 }, + .target_valid = 1, + }; + int dest_x = -1; + int dest_y = -1; + OsrsEncounterArena arena = step_arena(); + arena.extra_blocked = step_vertical_wall; + + OsrsPlayerStepInput input; + memset(&input, 0, sizeof(input)); + input.player = &player; + input.interaction = &interaction; + input.target_lookup = step_lookup_target; + input.target_ctx = &target_ctx; + input.dest_x = &dest_x; + input.dest_y = &dest_y; + input.arena = arena; + + OsrsPlayerStepResult first = osrs_encounter_player_step(&input); + CHECK("obstacle chase builds a multi-checkpoint route", + first.chased_target && interaction.route.waypoint_count > 1); + CHECK("route records its original source", + interaction.route.planned_source_x == 2 && + interaction.route.planned_source_y == 2); + + osrs_encounter_player_step(&input); + CHECK("route persists while more than one checkpoint remains", + interaction.route.planned_source_x == 2 && + interaction.route.planned_source_y == 2); + + target_ctx.target.y = 3; + int moved_target_source_x = player.x; + int moved_target_source_y = player.y; + osrs_encounter_player_step(&input); + CHECK("target geometry change reroutes from the current tile", + interaction.route.target_y == 3 && + interaction.route.planned_source_x == moved_target_source_x && + interaction.route.planned_source_y == moved_target_source_y); + + player.x = 1; + player.y = 10; + osrs_encounter_player_step(&input); + CHECK("player route divergence reroutes from the observed tile", + interaction.route.planned_source_x == 1 && + interaction.route.planned_source_y == 10); +} + +static void test_single_checkpoint_route_reroutes_each_tick(void) { + Player player; + memset(&player, 0, sizeof(player)); + player.x = 2; + player.y = 2; + + OsrsInteraction interaction; + osrs_interaction_init(&interaction); + osrs_interaction_set(&interaction, 3); + + StepTargetCtx target_ctx = { + .target = { .slot = 3, .x = 12, .y = 2, .size = 1, .attack_range = 1 }, + .target_valid = 1, + }; + int dest_x = -1; + int dest_y = -1; + OsrsEncounterArena arena = step_arena(); + OsrsPlayerStepInput input; + memset(&input, 0, sizeof(input)); + input.player = &player; + input.interaction = &interaction; + input.target_lookup = step_lookup_target; + input.target_ctx = &target_ctx; + input.dest_x = &dest_x; + input.dest_y = &dest_y; + input.arena = arena; + + osrs_encounter_player_step(&input); + CHECK("straight chase compresses to one checkpoint", + interaction.route.waypoint_count == 1 && + interaction.route.planned_source_x == 2); + osrs_encounter_player_step(&input); + CHECK("one-checkpoint entity route reroutes from the current tile", + interaction.route.planned_source_x == 4 && + interaction.route.planned_source_y == 2); +} + +static void test_same_target_selection_preserves_route(void) { + OsrsInteraction interaction = {0}; + osrs_interaction_init(&interaction); + osrs_interaction_set(&interaction, 3); + interaction.route.state = OSRS_INTERACTION_ROUTE_READY; + interaction.route.target_x = 12; + interaction.route.target_y = 7; + interaction.route.waypoint_count = 2; + interaction.route.waypoint_index = 1; + interaction.route.waypoint_x[1] = 11; + interaction.route.waypoint_y[1] = 7; + + OsrsInteractionRoute preserved = interaction.route; + osrs_interaction_set(&interaction, 3); + CHECK("same target preserves its route", + memcmp(&interaction.route, &preserved, sizeof(preserved)) == 0); + + osrs_interaction_set(&interaction, 4); + CHECK("different target clears the route", + interaction.target_slot == 4 && + interaction.route.state == OSRS_INTERACTION_ROUTE_EMPTY && + interaction.route.waypoint_count == 0 && + interaction.route.waypoint_index == 0); +} + +static void test_attack_route_field_stops_at_first_reachable_target_edge(void) { + EncounterArenaAttackRouteField field; + encounter_build_arena_attack_route_field( + &field, + NULL, + 0, + 0, + 2, + 2, + 12, + 2, + 1, + step_tile_walkable, + NULL, + NULL, + NULL, + 0, + 0, + STEP_GRID, + STEP_GRID); + + EncounterAttackRouteLanding landing = encounter_arena_attack_route_landing( + &field, + 12, + 2, + 1, + 1, + osrs_los_open_query()); + + CHECK("goal-directed field stops before exhausting the arena", + field.visited_count < STEP_GRID * STEP_GRID); + CHECK("goal-directed field keeps the first FIFO target edge", + landing.route_found && landing.route_x == 11 && landing.route_y == 2); +} + +static void test_unreachable_attack_route_field_exhausts_for_fallback(void) { + EncounterArenaAttackRouteField field; + encounter_build_arena_attack_route_field( + &field, + NULL, + 0, + 0, + 10, + 10, + -20, + 10, + 1, + step_tile_walkable, + NULL, + NULL, + NULL, + 0, + 0, + STEP_GRID, + STEP_GRID); + + CHECK("unreachable target exhausts the field for fallback", + field.visited_count == STEP_GRID * STEP_GRID); +} + +static void test_attack_route_caches_walkability_per_tile(void) { + EncounterArenaAttackRouteField field; + StepWalkableCountCtx walkable_count = {0}; + encounter_build_arena_attack_route_field( + &field, + NULL, + 0, + 0, + 1, + 1, + 22, + 22, + 1, + step_counted_tile_walkable, + &walkable_count, + NULL, + NULL, + 0, + 0, + STEP_GRID, + STEP_GRID); + + CHECK("route field checks each arena tile at most once", + walkable_count.calls <= STEP_GRID * STEP_GRID); +} + + int main(void) { + test_same_target_selection_preserves_route(); + test_attack_route_field_stops_at_first_reachable_target_edge(); + test_unreachable_attack_route_field_exhausts_for_fallback(); + test_attack_route_caches_walkability_per_tile(); test_target_command_cancels_walk_in_flight(); test_move_command_cancels_interaction(); test_none_command_chases_active_interaction(); + test_attack_route_rejects_cardinal_adjacency_through_wall(); + test_attack_route_fallback_stays_within_ten_tiles(); + test_attack_route_uses_rsmod_cardinal_first_order(); + test_melee_attack_does_not_cross_cardinal_wall(); + test_attack_route_persists_and_invalidates_canonically(); + test_single_checkpoint_route_reroutes_each_tick(); printf("\n%d/%d tests passed\n", tests_run - tests_failed, tests_run); return tests_failed == 0 ? 0 : 1; From 475787f796813cb5a11dc480f80ee1afb4725bb9 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Tue, 11 Aug 2026 21:49:55 +0300 Subject: [PATCH 09/50] Restore in-range attack chase fast path --- ocean/osrs/osrs_encounter.h | 6 ++++ ocean/osrs/tests/test_osrs_player_step.c | 35 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index 3ffb793e16..14406ba0d4 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -1847,6 +1847,12 @@ static inline int encounter_chase_attack_target( arena_base_x, arena_base_y, arena_w, arena_h); return steps > 0 ? 1 : 0; } + if (encounter_player_can_attack( + p->x, p->y, + target_x, target_y, target_size, attack_range, + cmap, world_offset_x, world_offset_y, + los_query)) + return 0; int cx, cy; diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c index d8ce22dce0..b58f9e1045 100644 --- a/ocean/osrs/tests/test_osrs_player_step.c +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -493,12 +493,47 @@ static void test_attack_route_caches_walkability_per_tile(void) { walkable_count.calls <= STEP_GRID * STEP_GRID); } +static void test_attackable_target_skips_route_scan(void) { + Player player; + memset(&player, 0, sizeof(player)); + player.x = 10; + player.y = 10; + player.dest_x = 7; + player.dest_y = 8; + StepWalkableCountCtx walkable_count = {0}; + + int moved = encounter_chase_attack_target( + &player, + 12, + 10, + 1, + 2, + NULL, + 0, + 0, + step_counted_tile_walkable, + &walkable_count, + NULL, + NULL, + osrs_los_open_query(), + 0, + 0, + 0, + 0); + + CHECK("attackable target does not move", moved == 0); + CHECK("attackable target skips walkability scan", walkable_count.calls == 0); + CHECK("attackable target preserves destination", + player.dest_x == 7 && player.dest_y == 8); +} + int main(void) { test_same_target_selection_preserves_route(); test_attack_route_field_stops_at_first_reachable_target_edge(); test_unreachable_attack_route_field_exhausts_for_fallback(); test_attack_route_caches_walkability_per_tile(); + test_attackable_target_skips_route_scan(); test_target_command_cancels_walk_in_flight(); test_move_command_cancels_interaction(); test_none_command_chases_active_interaction(); From a1d905dc23a9ba82c6c8a73baa6bd972d4fd836b Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Tue, 11 Aug 2026 22:11:33 +0300 Subject: [PATCH 10/50] Add immutable OSRS arena topology --- ocean/osrs/osrs_collision.h | 738 +++++++++++++++++++--- ocean/osrs/osrs_encounter.h | 1 + ocean/osrs/tests/test_osrs_npc_movement.c | 99 +++ ocean/osrs/tests/test_osrs_player_step.c | 213 +++++++ 4 files changed, 965 insertions(+), 86 deletions(-) diff --git a/ocean/osrs/osrs_collision.h b/ocean/osrs/osrs_collision.h index 74cc3fcf31..07dbba8429 100644 --- a/ocean/osrs/osrs_collision.h +++ b/ocean/osrs/osrs_collision.h @@ -1,6 +1,8 @@ #ifndef OSRS_COLLISION_H #define OSRS_COLLISION_H +#include +#include #include #include #include @@ -130,89 +132,132 @@ static inline int collision_is_inactive(const CollisionMap* map, int height, int return (collision_get_flags(map, height, x, y) & flag) == 0; } -static inline int collision_traversable_north(const CollisionMap* map, int height, int x, int y) { - if (map == NULL) return 1; - return collision_is_inactive(map, height, x, y + 1, - COLLISION_WALL_SOUTH | COLLISION_BLOCKED); +static inline int collision_flags_traversable_step( + uint32_t destination_flags, + uint32_t horizontal_side_flags, + uint32_t vertical_side_flags, + int dx, + int dy +) { + if (dx == 0 && dy == 1) + return (destination_flags & (COLLISION_WALL_SOUTH | COLLISION_BLOCKED)) == 0; + if (dx == 0 && dy == -1) + return (destination_flags & (COLLISION_WALL_NORTH | COLLISION_BLOCKED)) == 0; + if (dx == 1 && dy == 0) + return (destination_flags & (COLLISION_WALL_WEST | COLLISION_BLOCKED)) == 0; + if (dx == -1 && dy == 0) + return (destination_flags & (COLLISION_WALL_EAST | COLLISION_BLOCKED)) == 0; + if (dx == 1 && dy == 1) + return (destination_flags & + (COLLISION_WALL_WEST | COLLISION_WALL_SOUTH | + COLLISION_WALL_SOUTH_WEST | COLLISION_BLOCKED)) == 0 && + (horizontal_side_flags & + (COLLISION_WALL_WEST | COLLISION_BLOCKED)) == 0 && + (vertical_side_flags & + (COLLISION_WALL_SOUTH | COLLISION_BLOCKED)) == 0; + if (dx == -1 && dy == 1) + return (destination_flags & + (COLLISION_WALL_EAST | COLLISION_WALL_SOUTH | + COLLISION_WALL_SOUTH_EAST | COLLISION_BLOCKED)) == 0 && + (horizontal_side_flags & + (COLLISION_WALL_EAST | COLLISION_BLOCKED)) == 0 && + (vertical_side_flags & + (COLLISION_WALL_SOUTH | COLLISION_BLOCKED)) == 0; + if (dx == 1 && dy == -1) + return (destination_flags & + (COLLISION_WALL_WEST | COLLISION_WALL_NORTH | + COLLISION_WALL_NORTH_WEST | COLLISION_BLOCKED)) == 0 && + (horizontal_side_flags & + (COLLISION_WALL_WEST | COLLISION_BLOCKED)) == 0 && + (vertical_side_flags & + (COLLISION_WALL_NORTH | COLLISION_BLOCKED)) == 0; + if (dx == -1 && dy == -1) + return (destination_flags & + (COLLISION_WALL_EAST | COLLISION_WALL_NORTH | + COLLISION_WALL_NORTH_EAST | COLLISION_BLOCKED)) == 0 && + (horizontal_side_flags & + (COLLISION_WALL_EAST | COLLISION_BLOCKED)) == 0 && + (vertical_side_flags & + (COLLISION_WALL_NORTH | COLLISION_BLOCKED)) == 0; + return 1; } -static inline int collision_traversable_south(const CollisionMap* map, int height, int x, int y) { +static inline int collision_traversable_step( + const CollisionMap* map, + int height, + int x, + int y, + int dx, + int dy +) { if (map == NULL) return 1; - return collision_is_inactive(map, height, x, y - 1, - COLLISION_WALL_NORTH | COLLISION_BLOCKED); + uint32_t horizontal_side_flags = 0; + uint32_t vertical_side_flags = 0; + if (dx != 0 && dy != 0) { + horizontal_side_flags = + (uint32_t)collision_get_flags(map, height, x + dx, y); + vertical_side_flags = + (uint32_t)collision_get_flags(map, height, x, y + dy); + } + return collision_flags_traversable_step( + (uint32_t)collision_get_flags(map, height, x + dx, y + dy), + horizontal_side_flags, + vertical_side_flags, + dx, + dy); } -static inline int collision_traversable_east(const CollisionMap* map, int height, int x, int y) { - if (map == NULL) return 1; - return collision_is_inactive(map, height, x + 1, y, - COLLISION_WALL_WEST | COLLISION_BLOCKED); +static inline int collision_traversable_north( + const CollisionMap* map, int height, int x, int y +) { + return collision_traversable_step(map, height, x, y, 0, 1); } -static inline int collision_traversable_west(const CollisionMap* map, int height, int x, int y) { - if (map == NULL) return 1; - return collision_is_inactive(map, height, x - 1, y, - COLLISION_WALL_EAST | COLLISION_BLOCKED); +static inline int collision_traversable_south( + const CollisionMap* map, int height, int x, int y +) { + return collision_traversable_step(map, height, x, y, 0, -1); } -static inline int collision_traversable_north_east(const CollisionMap* map, int height, int x, int y) { - if (map == NULL) return 1; - return collision_is_inactive(map, height, x + 1, y + 1, - COLLISION_WALL_WEST | COLLISION_WALL_SOUTH | COLLISION_WALL_SOUTH_WEST | COLLISION_BLOCKED) - && collision_is_inactive(map, height, x + 1, y, - COLLISION_WALL_WEST | COLLISION_BLOCKED) - && collision_is_inactive(map, height, x, y + 1, - COLLISION_WALL_SOUTH | COLLISION_BLOCKED); +static inline int collision_traversable_east( + const CollisionMap* map, int height, int x, int y +) { + return collision_traversable_step(map, height, x, y, 1, 0); } -static inline int collision_traversable_north_west(const CollisionMap* map, int height, int x, int y) { - if (map == NULL) return 1; - return collision_is_inactive(map, height, x - 1, y + 1, - COLLISION_WALL_EAST | COLLISION_WALL_SOUTH | COLLISION_WALL_SOUTH_EAST | COLLISION_BLOCKED) - && collision_is_inactive(map, height, x - 1, y, - COLLISION_WALL_EAST | COLLISION_BLOCKED) - && collision_is_inactive(map, height, x, y + 1, - COLLISION_WALL_SOUTH | COLLISION_BLOCKED); +static inline int collision_traversable_west( + const CollisionMap* map, int height, int x, int y +) { + return collision_traversable_step(map, height, x, y, -1, 0); } -static inline int collision_traversable_south_east(const CollisionMap* map, int height, int x, int y) { - if (map == NULL) return 1; - return collision_is_inactive(map, height, x + 1, y - 1, - COLLISION_WALL_WEST | COLLISION_WALL_NORTH | COLLISION_WALL_NORTH_WEST | COLLISION_BLOCKED) - && collision_is_inactive(map, height, x + 1, y, - COLLISION_WALL_WEST | COLLISION_BLOCKED) - && collision_is_inactive(map, height, x, y - 1, - COLLISION_WALL_NORTH | COLLISION_BLOCKED); +static inline int collision_traversable_north_east( + const CollisionMap* map, int height, int x, int y +) { + return collision_traversable_step(map, height, x, y, 1, 1); } -static inline int collision_traversable_south_west(const CollisionMap* map, int height, int x, int y) { - if (map == NULL) return 1; - return collision_is_inactive(map, height, x - 1, y - 1, - COLLISION_WALL_EAST | COLLISION_WALL_NORTH | COLLISION_WALL_NORTH_EAST | COLLISION_BLOCKED) - && collision_is_inactive(map, height, x - 1, y, - COLLISION_WALL_EAST | COLLISION_BLOCKED) - && collision_is_inactive(map, height, x, y - 1, - COLLISION_WALL_NORTH | COLLISION_BLOCKED); +static inline int collision_traversable_north_west( + const CollisionMap* map, int height, int x, int y +) { + return collision_traversable_step(map, height, x, y, -1, 1); } -static inline int collision_tile_walkable(const CollisionMap* map, int height, int x, int y) { - if (map == NULL) return 1; - return (collision_get_flags(map, height, x, y) & COLLISION_BLOCKED) == 0; +static inline int collision_traversable_south_east( + const CollisionMap* map, int height, int x, int y +) { + return collision_traversable_step(map, height, x, y, 1, -1); } -static inline int collision_traversable_step(const CollisionMap* map, int height, - int x, int y, int dx, int dy) { - if (map == NULL) return 1; - - if (dx == 0 && dy == 1) return collision_traversable_north(map, height, x, y); - if (dx == 0 && dy == -1) return collision_traversable_south(map, height, x, y); - if (dx == 1 && dy == 0) return collision_traversable_east(map, height, x, y); - if (dx == -1 && dy == 0) return collision_traversable_west(map, height, x, y); - if (dx == 1 && dy == 1) return collision_traversable_north_east(map, height, x, y); - if (dx == -1 && dy == 1) return collision_traversable_north_west(map, height, x, y); - if (dx == 1 && dy == -1) return collision_traversable_south_east(map, height, x, y); - if (dx == -1 && dy == -1) return collision_traversable_south_west(map, height, x, y); +static inline int collision_traversable_south_west( + const CollisionMap* map, int height, int x, int y +) { + return collision_traversable_step(map, height, x, y, -1, -1); +} - return 1; +static inline int collision_tile_walkable(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return 1; + return (collision_get_flags(map, height, x, y) & COLLISION_BLOCKED) == 0; } #define COLLISION_MAP_MAGIC 0x50414D43 @@ -283,37 +328,58 @@ typedef struct { int size; uint32_t los_mask; } LOSBlocker; +static inline int los_aabb_overlap( + int x1, int y1, int s1, int x2, int y2, int s2 +) { + return !(x1 >= x2 + s2 || x1 + s1 <= x2 || + y1 >= y2 + s2 || y1 + s1 <= y2); +} + + +typedef uint32_t (*los_tile_flags_fn)(void* ctx, int x, int y); + +typedef struct { + const LOSBlocker* blockers; + int count; +} LOSBlockerFlagsContext; -static uint32_t los_check_tile(const LOSBlocker* blockers, int count, - int px, int py) { +static inline uint32_t los_check_tile( + const LOSBlocker* blockers, int count, int px, int py +) { for (int i = 0; i < count; i++) { const LOSBlocker* b = &blockers[i]; if (px >= b->x && px < b->x + b->size && - py >= b->y && py < b->y + b->size) { + py >= b->y && py < b->y + b->size) return b->los_mask; - } } return 0; } -static int los_aabb_overlap(int x1, int y1, int s1, int x2, int y2, int s2) { - return !(x1 >= x2 + s2 || x1 + s1 <= x2 || y1 >= y2 + s2 || y1 + s1 <= y2); +static inline uint32_t los_blocker_tile_flags(void* ctx, int x, int y) { + const LOSBlockerFlagsContext* blockers = + (const LOSBlockerFlagsContext*)ctx; + return los_check_tile(blockers->blockers, blockers->count, x, y); } -static int has_line_of_sight(const LOSBlocker* blockers, int blocker_count, - int x1, int y1, int x2, int y2, - int src_size, int range) { +static inline int los_has_line_of_sight_with_flags( + los_tile_flags_fn tile_flags, + void* tile_flags_ctx, + int x1, + int y1, + int x2, + int y2, + int src_size, + int range +) { int dx = x2 - x1; int dy = y2 - y1; - if (los_check_tile(blockers, blocker_count, x1, y1)) return 0; - if (los_check_tile(blockers, blocker_count, x2, y2)) return 0; - + if (tile_flags(tile_flags_ctx, x1, y1)) return 0; + if (tile_flags(tile_flags_ctx, x2, y2)) return 0; if (los_aabb_overlap(x1, y1, src_size, x2, y2, 1)) return 0; int adx = dx < 0 ? -dx : dx; int ady = dy < 0 ? -dy : dy; - if (range > 0 && (adx > range || ady > range)) return 0; if (adx > ady) { @@ -330,14 +396,13 @@ static int has_line_of_sight(const LOSBlocker* blockers, int blocker_count, while (x_tile != x2) { x_tile += x_inc; int y_tile = y_fp >> 16; - if (los_check_tile(blockers, blocker_count, x_tile, y_tile) & x_mask) + if (tile_flags(tile_flags_ctx, x_tile, y_tile) & x_mask) return 0; y_fp += slope; int new_y = y_fp >> 16; - if (new_y != y_tile) { - if (los_check_tile(blockers, blocker_count, x_tile, new_y) & y_mask) - return 0; - } + if (new_y != y_tile && + (tile_flags(tile_flags_ctx, x_tile, new_y) & y_mask)) + return 0; } } else if (ady > 0) { int y_tile = y1; @@ -353,20 +418,38 @@ static int has_line_of_sight(const LOSBlocker* blockers, int blocker_count, while (y_tile != y2) { y_tile += y_inc; int x_tile = x_fp >> 16; - if (los_check_tile(blockers, blocker_count, x_tile, y_tile) & y_mask) + if (tile_flags(tile_flags_ctx, x_tile, y_tile) & y_mask) return 0; x_fp += slope; int new_x = x_fp >> 16; - if (new_x != x_tile) { - if (los_check_tile(blockers, blocker_count, new_x, y_tile) & x_mask) - return 0; - } + if (new_x != x_tile && + (tile_flags(tile_flags_ctx, new_x, y_tile) & x_mask)) + return 0; } } return 1; } +static int has_line_of_sight( + const LOSBlocker* blockers, + int blocker_count, + int x1, + int y1, + int x2, + int y2, + int src_size, + int range +) { + LOSBlockerFlagsContext ctx = { + .blockers = blockers, + .count = blocker_count, + }; + return los_has_line_of_sight_with_flags( + los_blocker_tile_flags, &ctx, + x1, y1, x2, y2, src_size, range); +} + static inline int los_intervals_overlap(int a0, int a1, int b0, int b1) { return !(a1 < b0 || b1 < a0); } @@ -412,4 +495,487 @@ static inline int entity_has_line_of_sight( return has_line_of_sight(blockers, blocker_count, a_px, a_py, t_px, t_py, 1, range); } +#define ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION 64 +#define ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES \ + (ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION * \ + ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION) +#define ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE 5 +#define ENCOUNTER_ARENA_TOPOLOGY_LOS_WORDS \ + ((ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES * \ + ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES + 63) / 64) + +typedef uint32_t (*encounter_arena_tile_flags_fn)(void* ctx, int x, int y); + +typedef struct { + int origin_x; + int origin_y; + int width; + int height; + int max_footprint_size; + uint64_t revision; + encounter_arena_tile_flags_fn tile_flags; + void* tile_flags_ctx; +} EncounterArenaTopologyBuildSpec; + +typedef struct EncounterArenaTopology { + int origin_x; + int origin_y; + int width; + int height; + int tile_count; + int max_footprint_size; + uint64_t revision; + uint8_t finalized; + uint32_t static_collision_flags[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint8_t static_blocked[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint8_t footprint_blocked + [ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE] + [ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint8_t legal_step_masks + [ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE] + [ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint64_t static_los_bits[ENCOUNTER_ARENA_TOPOLOGY_LOS_WORDS]; +} EncounterArenaTopology; + +static inline void encounter_arena_topology_abort( + const char* reason, + int value +) { + fprintf(stderr, "invalid OSRS arena topology %s: %d\n", reason, value); + abort(); +} + +static inline int encounter_arena_topology_contains_raw( + const EncounterArenaTopology* topology, + int x, + int y +) { + return x >= topology->origin_x && + x < topology->origin_x + topology->width && + y >= topology->origin_y && + y < topology->origin_y + topology->height; +} + +static inline int encounter_arena_topology_index_raw( + const EncounterArenaTopology* topology, + int x, + int y +) { + return (x - topology->origin_x) * topology->height + + (y - topology->origin_y); +} + +static inline uint32_t encounter_arena_topology_build_flags( + const EncounterArenaTopologyBuildSpec* spec, + int x, + int y +) { + if (!spec->tile_flags) return 0; + return spec->tile_flags(spec->tile_flags_ctx, x, y); +} + +static inline int encounter_arena_topology_footprint_blocked_raw( + const EncounterArenaTopology* topology, + int x, + int y, + int size +) { + if (x < topology->origin_x || + y < topology->origin_y || + x > topology->origin_x + topology->width - size || + y > topology->origin_y + topology->height - size) + return 1; + int index = encounter_arena_topology_index_raw(topology, x, y); + return topology->footprint_blocked[size - 1][index] != 0; +} + +static inline int encounter_arena_topology_build_step_allowed( + const EncounterArenaTopologyBuildSpec* spec, + const EncounterArenaTopology* topology, + int x, + int y, + int size, + int dx, + int dy +) { + if (encounter_arena_topology_footprint_blocked_raw( + topology, x, y, size) || + encounter_arena_topology_footprint_blocked_raw( + topology, x + dx, y + dy, size)) + return 0; + + if (dx != 0) { + int leading_x = dx > 0 ? x + size - 1 : x; + for (int offset = 0; offset < size; offset++) { + int source_y = y + offset; + if (!collision_flags_traversable_step( + encounter_arena_topology_build_flags( + spec, leading_x + dx, source_y), + encounter_arena_topology_build_flags( + spec, leading_x + dx, source_y), + encounter_arena_topology_build_flags( + spec, leading_x, source_y), + dx, + 0)) + return 0; + } + } + + if (dy != 0) { + int leading_y = dy > 0 ? y + size - 1 : y; + for (int offset = 0; offset < size; offset++) { + int source_x = x + offset; + if (!collision_flags_traversable_step( + encounter_arena_topology_build_flags( + spec, source_x, leading_y + dy), + encounter_arena_topology_build_flags( + spec, source_x, leading_y), + encounter_arena_topology_build_flags( + spec, source_x, leading_y + dy), + 0, + dy)) + return 0; + } + } + + if (dx != 0 && dy != 0) { + int corner_x = dx > 0 ? x + size - 1 : x; + int corner_y = dy > 0 ? y + size - 1 : y; + if (!collision_flags_traversable_step( + encounter_arena_topology_build_flags( + spec, corner_x + dx, corner_y + dy), + encounter_arena_topology_build_flags( + spec, corner_x + dx, corner_y), + encounter_arena_topology_build_flags( + spec, corner_x, corner_y + dy), + dx, + dy)) + return 0; + } + + return 1; +} + +static uint32_t encounter_arena_topology_los_flags( + void* ctx, + int x, + int y +) { + const EncounterArenaTopology* topology = + (const EncounterArenaTopology*)ctx; + if (!encounter_arena_topology_contains_raw(topology, x, y)) + return LOS_FULL_MASK; + uint32_t flags = topology->static_collision_flags[ + encounter_arena_topology_index_raw(topology, x, y)]; + return flags & + (LOS_FULL_MASK | LOS_EAST_MASK | LOS_WEST_MASK | + LOS_NORTH_MASK | LOS_SOUTH_MASK); +} + +static inline void encounter_arena_topology_set_los( + EncounterArenaTopology* topology, + int source_index, + int target_index +) { + size_t bit_index = + (size_t)source_index * (size_t)topology->tile_count + + (size_t)target_index; + topology->static_los_bits[bit_index >> 6] |= + UINT64_C(1) << (bit_index & 63); +} + +static inline EncounterArenaTopology* encounter_arena_topology_build( + const EncounterArenaTopologyBuildSpec* spec +) { + if (!spec) encounter_arena_topology_abort("build spec", 0); + if (spec->width < 1 || + spec->width > ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION) + encounter_arena_topology_abort("width", spec->width); + if (spec->height < 1 || + spec->height > ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION) + encounter_arena_topology_abort("height", spec->height); + if (spec->origin_x > INT_MAX - (spec->width - 1)) + encounter_arena_topology_abort("origin x", spec->origin_x); + if (spec->origin_y > INT_MAX - (spec->height - 1)) + encounter_arena_topology_abort("origin y", spec->origin_y); + if (spec->max_footprint_size < 1 || + spec->max_footprint_size > + ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE) + encounter_arena_topology_abort( + "footprint size", spec->max_footprint_size); + if (spec->revision == 0) + encounter_arena_topology_abort("revision", 0); + + EncounterArenaTopology* topology = + (EncounterArenaTopology*)calloc(1, sizeof(*topology)); + if (!topology) { + fprintf(stderr, "failed to allocate OSRS arena topology\n"); + abort(); + } + + topology->origin_x = spec->origin_x; + topology->origin_y = spec->origin_y; + topology->width = spec->width; + topology->height = spec->height; + topology->tile_count = spec->width * spec->height; + topology->max_footprint_size = spec->max_footprint_size; + topology->revision = spec->revision; + + for (int local_x = 0; local_x < topology->width; local_x++) { + for (int local_y = 0; local_y < topology->height; local_y++) { + int x = topology->origin_x + local_x; + int y = topology->origin_y + local_y; + int index = encounter_arena_topology_index_raw(topology, x, y); + uint32_t flags = + encounter_arena_topology_build_flags(spec, x, y); + topology->static_collision_flags[index] = flags; + topology->static_blocked[index] = + (uint8_t)((flags & COLLISION_BLOCKED) != 0); + } + } + + for (int size = 1; size <= topology->max_footprint_size; size++) { + for (int local_x = 0; local_x < topology->width; local_x++) { + for (int local_y = 0; local_y < topology->height; local_y++) { + int x = topology->origin_x + local_x; + int y = topology->origin_y + local_y; + int index = + encounter_arena_topology_index_raw(topology, x, y); + int blocked = + local_x + size > topology->width || + local_y + size > topology->height; + for (int footprint_x = 0; + footprint_x < size && !blocked; + footprint_x++) { + for (int footprint_y = 0; + footprint_y < size; + footprint_y++) { + int footprint_index = + encounter_arena_topology_index_raw( + topology, + x + footprint_x, + y + footprint_y); + if (topology->static_blocked[footprint_index]) { + blocked = 1; + break; + } + } + } + topology->footprint_blocked[size - 1][index] = + (uint8_t)blocked; + } + } + } + + for (int size = 1; size <= topology->max_footprint_size; size++) { + for (int local_x = 0; local_x < topology->width; local_x++) { + for (int local_y = 0; local_y < topology->height; local_y++) { + int x = topology->origin_x + local_x; + int y = topology->origin_y + local_y; + int index = + encounter_arena_topology_index_raw(topology, x, y); + uint8_t mask = 0; + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) continue; + int bit = (dy + 1) * 3 + (dx + 1); + if (bit > 4) bit--; + if (encounter_arena_topology_build_step_allowed( + spec, topology, x, y, size, dx, dy)) + mask |= (uint8_t)(1u << bit); + } + } + topology->legal_step_masks[size - 1][index] = mask; + } + } + } + + for (int source = 0; source < topology->tile_count; source++) { + int source_x = topology->origin_x + source / topology->height; + int source_y = topology->origin_y + source % topology->height; + if (encounter_arena_topology_los_flags( + topology, source_x, source_y) == 0) + encounter_arena_topology_set_los(topology, source, source); + + for (int target = source + 1; + target < topology->tile_count; + target++) { + int target_x = + topology->origin_x + target / topology->height; + int target_y = + topology->origin_y + target % topology->height; + int forward = los_has_line_of_sight_with_flags( + encounter_arena_topology_los_flags, + topology, + source_x, + source_y, + target_x, + target_y, + 1, + 0); + int reverse = los_has_line_of_sight_with_flags( + encounter_arena_topology_los_flags, + topology, + target_x, + target_y, + source_x, + source_y, + 1, + 0); + if (forward && reverse) { + encounter_arena_topology_set_los( + topology, source, target); + encounter_arena_topology_set_los( + topology, target, source); + } + } + } + + return topology; +} + +static inline void encounter_arena_topology_finalize( + EncounterArenaTopology* topology +) { + if (!topology) encounter_arena_topology_abort("finalize", 0); + if (topology->finalized) + encounter_arena_topology_abort("double finalization", 1); + topology->finalized = 1; +} + +static inline void encounter_arena_topology_require_finalized( + const EncounterArenaTopology* topology +) { + if (!topology || !topology->finalized) + encounter_arena_topology_abort("unfinalized query", 0); +} + +static inline const EncounterArenaTopology* +encounter_arena_topology_require_revision( + const EncounterArenaTopology* topology, + uint64_t revision +) { + encounter_arena_topology_require_finalized(topology); + if (revision == 0 || topology->revision != revision) { + fprintf(stderr, + "stale OSRS arena topology revision: expected %llu got %llu\n", + (unsigned long long)topology->revision, + (unsigned long long)revision); + abort(); + } + return topology; +} + +static inline int encounter_arena_topology_contains( + const EncounterArenaTopology* topology, + int x, + int y +) { + encounter_arena_topology_require_finalized(topology); + return encounter_arena_topology_contains_raw(topology, x, y); +} + +static inline int encounter_arena_topology_tile_blocked( + const EncounterArenaTopology* topology, + int x, + int y +) { + encounter_arena_topology_require_finalized(topology); + if (!encounter_arena_topology_contains_raw(topology, x, y)) return 1; + return topology->static_blocked[ + encounter_arena_topology_index_raw(topology, x, y)] != 0; +} + +static inline void encounter_arena_topology_require_footprint_size( + const EncounterArenaTopology* topology, + int size +) { + if (size < 1 || size > topology->max_footprint_size) + encounter_arena_topology_abort("query footprint size", size); +} + +static inline int encounter_arena_topology_footprint_blocked( + const EncounterArenaTopology* topology, + int x, + int y, + int size +) { + encounter_arena_topology_require_finalized(topology); + encounter_arena_topology_require_footprint_size(topology, size); + return encounter_arena_topology_footprint_blocked_raw( + topology, x, y, size); +} + +static inline int encounter_arena_topology_step_allowed( + const EncounterArenaTopology* topology, + int x, + int y, + int size, + int dx, + int dy +) { + encounter_arena_topology_require_finalized(topology); + encounter_arena_topology_require_footprint_size(topology, size); + if (dx < -1 || dx > 1 || dy < -1 || dy > 1 || + (dx == 0 && dy == 0)) + encounter_arena_topology_abort("step direction", 0); + if (!encounter_arena_topology_contains_raw(topology, x, y)) return 0; + int bit = (dy + 1) * 3 + (dx + 1); + if (bit > 4) bit--; + int index = encounter_arena_topology_index_raw(topology, x, y); + return (topology->legal_step_masks[size - 1][index] & + (uint8_t)(1u << bit)) != 0; +} + +static inline int encounter_arena_topology_los_clear( + const EncounterArenaTopology* topology, + int actor_x, + int actor_y, + int actor_size, + int target_x, + int target_y, + int target_size, + int attack_range +) { + encounter_arena_topology_require_finalized(topology); + encounter_arena_topology_require_footprint_size( + topology, actor_size); + encounter_arena_topology_require_footprint_size( + topology, target_size); + if (attack_range <= 1) return 1; + if (encounter_arena_topology_footprint_blocked_raw( + topology, actor_x, actor_y, actor_size) || + encounter_arena_topology_footprint_blocked_raw( + topology, target_x, target_y, target_size)) + return 0; + + int actor_los_x = target_x; + if (actor_los_x < actor_x) actor_los_x = actor_x; + if (actor_los_x >= actor_x + actor_size) + actor_los_x = actor_x + actor_size - 1; + int actor_los_y = target_y; + if (actor_los_y < actor_y) actor_los_y = actor_y; + if (actor_los_y >= actor_y + actor_size) + actor_los_y = actor_y + actor_size - 1; + + int target_los_x = actor_x; + if (target_los_x < target_x) target_los_x = target_x; + if (target_los_x >= target_x + target_size) + target_los_x = target_x + target_size - 1; + int target_los_y = actor_y; + if (target_los_y < target_y) target_los_y = target_y; + if (target_los_y >= target_y + target_size) + target_los_y = target_y + target_size - 1; + + int actor_index = encounter_arena_topology_index_raw( + topology, actor_los_x, actor_los_y); + int target_index = encounter_arena_topology_index_raw( + topology, target_los_x, target_los_y); + size_t bit_index = + (size_t)actor_index * (size_t)topology->tile_count + + (size_t)target_index; + return (int)((topology->static_los_bits[bit_index >> 6] >> + (bit_index & 63)) & UINT64_C(1)); +} + #endif diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index 14406ba0d4..76e34b3fe4 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -23,6 +23,7 @@ typedef struct EncounterState EncounterState; typedef struct EncounterContext EncounterContext; +typedef struct EncounterArenaTopology EncounterArenaTopology; #define ENCOUNTER_RENDER_HITS_MAX 32 diff --git a/ocean/osrs/tests/test_osrs_npc_movement.c b/ocean/osrs/tests/test_osrs_npc_movement.c index 131bea618b..48e01b6dbe 100644 --- a/ocean/osrs/tests/test_osrs_npc_movement.c +++ b/ocean/osrs/tests/test_osrs_npc_movement.c @@ -1,5 +1,10 @@ +#include +#include #include +#include #include +#include +#include #include "ocean/osrs/osrs_encounter.h" @@ -148,7 +153,101 @@ static void test_shared_current_overlap(void) { moved == 0 && x == 5 && y == 5); } +typedef struct { + uint32_t flags[12][12]; +} NpcTopologyGeometry; + +static uint32_t npc_topology_flags(void* ctx, int x, int y) { + const NpcTopologyGeometry* geometry = (const NpcTopologyGeometry*)ctx; + if (x < 0 || x >= 12 || y < 0 || y >= 12) + return COLLISION_BLOCKED | LOS_FULL_MASK; + return geometry->flags[x][y]; +} + +static int npc_topology_aborts(void (*operation)(void)) { + fflush(NULL); + pid_t pid = fork(); + if (pid == 0) { + operation(); + _exit(0); + } + int status = 0; + if (pid < 0 || waitpid(pid, &status, 0) != pid) return 0; + return WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT; +} + +static void npc_topology_rejects_unsupported_footprint(void) { + NpcTopologyGeometry geometry; + memset(&geometry, 0, sizeof(geometry)); + EncounterArenaTopologyBuildSpec spec = { + .origin_x = 0, + .origin_y = 0, + .width = 12, + .height = 12, + .max_footprint_size = + ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE + 1, + .revision = 9, + .tile_flags = npc_topology_flags, + .tile_flags_ctx = &geometry, + }; + (void)encounter_arena_topology_build(&spec); +} + +static void test_arena_topology_footprint_masks(void) { + printf("test_arena_topology_footprint_masks\n"); + + NpcTopologyGeometry geometry; + memset(&geometry, 0, sizeof(geometry)); + geometry.flags[8][8] = COLLISION_BLOCKED; + EncounterArenaTopologyBuildSpec spec = { + .origin_x = 0, + .origin_y = 0, + .width = 12, + .height = 12, + .max_footprint_size = + ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE, + .revision = 9, + .tile_flags = npc_topology_flags, + .tile_flags_ctx = &geometry, + }; + EncounterArenaTopology* topology = encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(topology); + + for (int size = 1; + size <= ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE; + size++) { + char label[96]; + snprintf(label, sizeof(label), + "size %d mask rejects footprint covering static blocker", size); + CHECK(label, encounter_arena_topology_footprint_blocked( + topology, 9 - size, 9 - size, size)); + + snprintf(label, sizeof(label), + "size %d mask accepts clear in-bounds footprint", size); + CHECK(label, !encounter_arena_topology_footprint_blocked( + topology, 1, 1, size)); + + snprintf(label, sizeof(label), + "size %d mask rejects footprint crossing arena edge", size); + CHECK(label, encounter_arena_topology_footprint_blocked( + topology, 13 - size, 1, size)); + } + + CHECK("large footprint clear cardinal step uses prebuilt mask", + encounter_arena_topology_step_allowed(topology, 1, 1, 5, 1, 0)); + CHECK("large footprint clear diagonal step uses prebuilt mask", + encounter_arena_topology_step_allowed(topology, 1, 1, 5, 1, 1)); + CHECK("large footprint step rejects destination static blocker", + !encounter_arena_topology_step_allowed(topology, 3, 3, 5, 1, 1)); + + free(topology); + + CHECK("topology build rejects unsupported footprint size", + npc_topology_aborts(npc_topology_rejects_unsupported_footprint)); +} + int main(void) { + test_arena_topology_footprint_masks(); test_shared_diagonal_edge_clearance_size_one(); test_shared_aggro_target_overlap_rewrite(); test_shared_melee_policy(); diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c index b58f9e1045..052210031d 100644 --- a/ocean/osrs/tests/test_osrs_player_step.c +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -8,11 +8,25 @@ * now routes through OsrsPlayerCommand. */ +#include +#include #include +#include #include +#include +#include +static size_t topology_allocation_count; +static void* topology_counted_calloc(size_t count, size_t size); + +#define calloc topology_counted_calloc #include "ocean/osrs/osrs_encounter_player.h" +#undef calloc +static void* topology_counted_calloc(size_t count, size_t size) { + topology_allocation_count++; + return calloc(count, size); +} static int tests_run = 0; static int tests_failed = 0; @@ -528,7 +542,206 @@ static void test_attackable_target_skips_route_scan(void) { } +typedef struct { + int origin_x; + int origin_y; + int width; + int height; + uint32_t flags[8][8]; +} TopologyTestGeometry; + +static uint32_t topology_test_flags(void* ctx, int x, int y) { + const TopologyTestGeometry* geometry = (const TopologyTestGeometry*)ctx; + int local_x = x - geometry->origin_x; + int local_y = y - geometry->origin_y; + if (local_x < 0 || local_x >= geometry->width || + local_y < 0 || local_y >= geometry->height) + return COLLISION_BLOCKED | LOS_FULL_MASK; + return geometry->flags[local_x][local_y]; +} + +static EncounterArenaTopologyBuildSpec topology_test_spec( + TopologyTestGeometry* geometry, + uint64_t revision +) { + return (EncounterArenaTopologyBuildSpec){ + .origin_x = geometry->origin_x, + .origin_y = geometry->origin_y, + .width = geometry->width, + .height = geometry->height, + .max_footprint_size = ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE, + .revision = revision, + .tile_flags = topology_test_flags, + .tile_flags_ctx = geometry, + }; +} + +static int topology_test_aborts(void (*operation)(void)) { + fflush(NULL); + pid_t pid = fork(); + if (pid == 0) { + operation(); + _exit(0); + } + int status = 0; + if (pid < 0 || waitpid(pid, &status, 0) != pid) return 0; + return WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT; +} + +static void topology_build_rejects_zero_width(void) { + TopologyTestGeometry geometry = { + .origin_x = 10, + .origin_y = 20, + .width = 0, + .height = 8, + }; + EncounterArenaTopologyBuildSpec spec = topology_test_spec(&geometry, 1); + (void)encounter_arena_topology_build(&spec); +} + +static void topology_build_rejects_oversized_height(void) { + TopologyTestGeometry geometry = { + .origin_x = 10, + .origin_y = 20, + .width = 8, + .height = ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION + 1, + }; + EncounterArenaTopologyBuildSpec spec = topology_test_spec(&geometry, 1); + (void)encounter_arena_topology_build(&spec); +} + +static void topology_build_rejects_origin_overflow(void) { + TopologyTestGeometry geometry = { + .origin_x = INT_MAX, + .origin_y = 20, + .width = 2, + .height = 8, + }; + EncounterArenaTopologyBuildSpec spec = topology_test_spec(&geometry, 1); + (void)encounter_arena_topology_build(&spec); +} + +static const EncounterArenaTopology* topology_stale_revision_target; + +static void topology_rejects_stale_revision(void) { + (void)encounter_arena_topology_require_revision( + topology_stale_revision_target, + topology_stale_revision_target->revision + 1); +} + +static void test_arena_topology_bounds_collision_los_and_revision(void) { + TopologyTestGeometry geometry = { + .origin_x = 10, + .origin_y = 20, + .width = 8, + .height = 8, + }; + geometry.flags[0][3] = COLLISION_BLOCKED; + geometry.flags[7][4] = COLLISION_BLOCKED; + geometry.flags[3][0] = COLLISION_BLOCKED; + geometry.flags[4][7] = COLLISION_BLOCKED; + geometry.flags[2][2] = COLLISION_WALL_EAST; + geometry.flags[3][2] = COLLISION_WALL_WEST; + geometry.flags[2][3] = COLLISION_BLOCKED; + geometry.flags[4][4] = LOS_FULL_MASK; + + size_t allocations_before_build = topology_allocation_count; + EncounterArenaTopologyBuildSpec spec = topology_test_spec(&geometry, 41); + EncounterArenaTopology* topology = encounter_arena_topology_build(&spec); + + CHECK("topology build uses one explicitly owned allocation", + topology_allocation_count == allocations_before_build + 1); + CHECK("topology stores validated arena identity", + topology->origin_x == 10 && + topology->origin_y == 20 && + topology->width == 8 && + topology->height == 8 && + topology->revision == 41); + + encounter_arena_topology_finalize(topology); + + CHECK("topology bounds include both arena corners", + encounter_arena_topology_contains(topology, 10, 20) && + encounter_arena_topology_contains(topology, 17, 27)); + CHECK("topology bounds reject every outside edge", + !encounter_arena_topology_contains(topology, 9, 20) && + !encounter_arena_topology_contains(topology, 18, 20) && + !encounter_arena_topology_contains(topology, 10, 19) && + !encounter_arena_topology_contains(topology, 10, 28)); + CHECK("static blocked queries read all four arena edges", + encounter_arena_topology_tile_blocked(topology, 10, 23) && + encounter_arena_topology_tile_blocked(topology, 17, 24) && + encounter_arena_topology_tile_blocked(topology, 13, 20) && + encounter_arena_topology_tile_blocked(topology, 14, 27)); + CHECK("static blocked queries treat outside bounds as blocked", + encounter_arena_topology_tile_blocked(topology, 9, 20) && + encounter_arena_topology_tile_blocked(topology, 18, 20) && + encounter_arena_topology_tile_blocked(topology, 10, 19) && + encounter_arena_topology_tile_blocked(topology, 10, 28)); + + CHECK("cardinal traversal rejects reciprocal wall edge", + !encounter_arena_topology_step_allowed(topology, 12, 22, 1, 1, 0) && + !encounter_arena_topology_step_allowed(topology, 13, 22, 1, -1, 0)); + CHECK("diagonal traversal rejects blocked cardinal side", + !encounter_arena_topology_step_allowed(topology, 12, 22, 1, 1, 1)); + CHECK("clear diagonal traversal remains allowed", + encounter_arena_topology_step_allowed(topology, 15, 21, 1, 1, 1)); + + int blocked_forward = encounter_arena_topology_los_clear( + topology, 11, 24, 1, 16, 24, 1, 10); + int blocked_reverse = encounter_arena_topology_los_clear( + topology, 16, 24, 1, 11, 24, 1, 10); + int clear_forward = encounter_arena_topology_los_clear( + topology, 11, 21, 1, 16, 21, 1, 10); + int clear_reverse = encounter_arena_topology_los_clear( + topology, 16, 21, 1, 11, 21, 1, 10); + CHECK("static LOS is symmetric through blocking geometry", + !blocked_forward && blocked_forward == blocked_reverse); + CHECK("static LOS is symmetric through open geometry", + clear_forward && clear_forward == clear_reverse); + CHECK("large-target LOS uses the closest occupied target tile", + encounter_arena_topology_los_clear( + topology, 11, 24, 1, 15, 23, 1, 10) && + !encounter_arena_topology_los_clear( + topology, 11, 24, 1, 15, 23, 2, 10)); + + CHECK("matching topology revision returns the same immutable object", + encounter_arena_topology_require_revision(topology, 41) == topology); + topology_stale_revision_target = topology; + CHECK("stale topology revision aborts", + topology_test_aborts(topology_rejects_stale_revision)); + + static unsigned char snapshot[sizeof(EncounterArenaTopology)]; + memcpy(snapshot, topology, sizeof(*topology)); + size_t allocations_after_finalize = topology_allocation_count; + (void)encounter_arena_topology_contains(topology, 12, 21); + (void)encounter_arena_topology_tile_blocked(topology, 12, 21); + (void)encounter_arena_topology_footprint_blocked(topology, 12, 21, 2); + (void)encounter_arena_topology_step_allowed(topology, 15, 21, 1, 1, 1); + (void)encounter_arena_topology_los_clear( + topology, 11, 21, 1, 16, 21, 1, 10); + (void)encounter_arena_topology_require_revision(topology, 41); + CHECK("finalized topology queries do not allocate", + topology_allocation_count == allocations_after_finalize); + CHECK("finalized topology queries do not mutate", + memcmp(snapshot, topology, sizeof(*topology)) == 0); + + free(topology); + topology_stale_revision_target = NULL; +} + +static void test_arena_topology_rejects_invalid_bounds(void) { + CHECK("topology build rejects zero width", + topology_test_aborts(topology_build_rejects_zero_width)); + CHECK("topology build rejects oversized height", + topology_test_aborts(topology_build_rejects_oversized_height)); + CHECK("topology build rejects overflowing origin plus width", + topology_test_aborts(topology_build_rejects_origin_overflow)); +} + int main(void) { + test_arena_topology_rejects_invalid_bounds(); + test_arena_topology_bounds_collision_los_and_revision(); test_same_target_selection_preserves_route(); test_attack_route_field_stops_at_first_reachable_target_edge(); test_unreachable_attack_route_field_exhausts_for_fallback(); From 8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Tue, 11 Aug 2026 22:29:35 +0300 Subject: [PATCH 11/50] Fix OSRS arena topology contracts --- ocean/osrs/osrs_collision.h | 104 ++++++++++++++++------- ocean/osrs/tests/test_osrs_player_step.c | 75 +++++++++++++++- 2 files changed, 142 insertions(+), 37 deletions(-) diff --git a/ocean/osrs/osrs_collision.h b/ocean/osrs/osrs_collision.h index 07dbba8429..2ce2510661 100644 --- a/ocean/osrs/osrs_collision.h +++ b/ocean/osrs/osrs_collision.h @@ -441,10 +441,7 @@ static int has_line_of_sight( int src_size, int range ) { - LOSBlockerFlagsContext ctx = { - .blockers = blockers, - .count = blocker_count, - }; + LOSBlockerFlagsContext ctx = {blockers, blocker_count}; return los_has_line_of_sight_with_flags( los_blocker_tile_flags, &ctx, x1, y1, x2, y2, src_size, range); @@ -550,10 +547,10 @@ static inline int encounter_arena_topology_contains_raw( int x, int y ) { - return x >= topology->origin_x && - x < topology->origin_x + topology->width && - y >= topology->origin_y && - y < topology->origin_y + topology->height; + int64_t local_x = (int64_t)x - topology->origin_x; + int64_t local_y = (int64_t)y - topology->origin_y; + return local_x >= 0 && local_x < topology->width && + local_y >= 0 && local_y < topology->height; } static inline int encounter_arena_topology_index_raw( @@ -561,8 +558,9 @@ static inline int encounter_arena_topology_index_raw( int x, int y ) { - return (x - topology->origin_x) * topology->height + - (y - topology->origin_y); + int64_t local_x = (int64_t)x - topology->origin_x; + int64_t local_y = (int64_t)y - topology->origin_y; + return (int)(local_x * topology->height + local_y); } static inline uint32_t encounter_arena_topology_build_flags( @@ -580,12 +578,13 @@ static inline int encounter_arena_topology_footprint_blocked_raw( int y, int size ) { - if (x < topology->origin_x || - y < topology->origin_y || - x > topology->origin_x + topology->width - size || - y > topology->origin_y + topology->height - size) + int64_t local_x = (int64_t)x - topology->origin_x; + int64_t local_y = (int64_t)y - topology->origin_y; + if (local_x < 0 || local_y < 0 || + local_x + size > topology->width || + local_y + size > topology->height) return 1; - int index = encounter_arena_topology_index_raw(topology, x, y); + int index = (int)(local_x * topology->height + local_y); return topology->footprint_blocked[size - 1][index] != 0; } @@ -599,9 +598,20 @@ static inline int encounter_arena_topology_build_step_allowed( int dy ) { if (encounter_arena_topology_footprint_blocked_raw( - topology, x, y, size) || - encounter_arena_topology_footprint_blocked_raw( - topology, x + dx, y + dy, size)) + topology, x, y, size)) + return 0; + int64_t destination_x = (int64_t)x + dx; + int64_t destination_y = (int64_t)y + dy; + int64_t destination_local_x = destination_x - topology->origin_x; + int64_t destination_local_y = destination_y - topology->origin_y; + if (destination_local_x < 0 || destination_local_y < 0 || + destination_local_x + size > topology->width || + destination_local_y + size > topology->height) + return 0; + int destination_x_int = (int)destination_x; + int destination_y_int = (int)destination_y; + if (encounter_arena_topology_footprint_blocked_raw( + topology, destination_x_int, destination_y_int, size)) return 0; if (dx != 0) { @@ -667,6 +677,7 @@ static uint32_t encounter_arena_topology_los_flags( return LOS_FULL_MASK; uint32_t flags = topology->static_collision_flags[ encounter_arena_topology_index_raw(topology, x, y)]; + if (flags & COLLISION_BLOCKED) return LOS_FULL_MASK; return flags & (LOS_FULL_MASK | LOS_EAST_MASK | LOS_WEST_MASK | LOS_NORTH_MASK | LOS_SOUTH_MASK); @@ -942,35 +953,62 @@ static inline int encounter_arena_topology_los_clear( topology, actor_size); encounter_arena_topology_require_footprint_size( topology, target_size); - if (attack_range <= 1) return 1; if (encounter_arena_topology_footprint_blocked_raw( topology, actor_x, actor_y, actor_size) || encounter_arena_topology_footprint_blocked_raw( topology, target_x, target_y, target_size)) return 0; - int actor_los_x = target_x; + int64_t actor_max_x = (int64_t)actor_x + actor_size - 1; + int64_t actor_max_y = (int64_t)actor_y + actor_size - 1; + int64_t target_max_x = (int64_t)target_x + target_size - 1; + int64_t target_max_y = (int64_t)target_y + target_size - 1; + int overlap = + (int64_t)actor_x <= target_max_x && + (int64_t)target_x <= actor_max_x && + (int64_t)actor_y <= target_max_y && + (int64_t)target_y <= actor_max_y; + if (overlap) return 0; + + if (attack_range == 1) { + int y_overlap = + (int64_t)actor_y <= target_max_y && + (int64_t)target_y <= actor_max_y; + int x_overlap = + (int64_t)actor_x <= target_max_x && + (int64_t)target_x <= actor_max_x; + return (actor_max_x + 1 == target_x && y_overlap) || + (target_max_x + 1 == actor_x && y_overlap) || + (actor_max_y + 1 == target_y && x_overlap) || + (target_max_y + 1 == actor_y && x_overlap); + } + + int64_t actor_los_x = target_x; if (actor_los_x < actor_x) actor_los_x = actor_x; - if (actor_los_x >= actor_x + actor_size) - actor_los_x = actor_x + actor_size - 1; - int actor_los_y = target_y; + if (actor_los_x > actor_max_x) actor_los_x = actor_max_x; + int64_t actor_los_y = target_y; if (actor_los_y < actor_y) actor_los_y = actor_y; - if (actor_los_y >= actor_y + actor_size) - actor_los_y = actor_y + actor_size - 1; + if (actor_los_y > actor_max_y) actor_los_y = actor_max_y; - int target_los_x = actor_x; + int64_t target_los_x = actor_x; if (target_los_x < target_x) target_los_x = target_x; - if (target_los_x >= target_x + target_size) - target_los_x = target_x + target_size - 1; - int target_los_y = actor_y; + if (target_los_x > target_max_x) target_los_x = target_max_x; + int64_t target_los_y = actor_y; if (target_los_y < target_y) target_los_y = target_y; - if (target_los_y >= target_y + target_size) - target_los_y = target_y + target_size - 1; + if (target_los_y > target_max_y) target_los_y = target_max_y; + + int64_t dx = target_los_x - actor_los_x; + int64_t dy = target_los_y - actor_los_y; + int64_t abs_dx = dx < 0 ? -dx : dx; + int64_t abs_dy = dy < 0 ? -dy : dy; + if (attack_range > 0 && + (abs_dx > attack_range || abs_dy > attack_range)) + return 0; int actor_index = encounter_arena_topology_index_raw( - topology, actor_los_x, actor_los_y); + topology, (int)actor_los_x, (int)actor_los_y); int target_index = encounter_arena_topology_index_raw( - topology, target_los_x, target_los_y); + topology, (int)target_los_x, (int)target_los_y); size_t bit_index = (size_t)actor_index * (size_t)topology->tile_count + (size_t)target_index; diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c index 052210031d..53f3611ee4 100644 --- a/ocean/osrs/tests/test_osrs_player_step.c +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -547,16 +547,19 @@ typedef struct { int origin_y; int width; int height; + int outside_reads; uint32_t flags[8][8]; } TopologyTestGeometry; static uint32_t topology_test_flags(void* ctx, int x, int y) { - const TopologyTestGeometry* geometry = (const TopologyTestGeometry*)ctx; - int local_x = x - geometry->origin_x; - int local_y = y - geometry->origin_y; + TopologyTestGeometry* geometry = (TopologyTestGeometry*)ctx; + int64_t local_x = (int64_t)x - geometry->origin_x; + int64_t local_y = (int64_t)y - geometry->origin_y; if (local_x < 0 || local_x >= geometry->width || - local_y < 0 || local_y >= geometry->height) + local_y < 0 || local_y >= geometry->height) { + geometry->outside_reads++; return COLLISION_BLOCKED | LOS_FULL_MASK; + } return geometry->flags[local_x][local_y]; } @@ -699,6 +702,22 @@ static void test_arena_topology_bounds_collision_los_and_revision(void) { !blocked_forward && blocked_forward == blocked_reverse); CHECK("static LOS is symmetric through open geometry", clear_forward && clear_forward == clear_reverse); + CHECK("blocked movement tile also blocks static LOS", + !encounter_arena_topology_los_clear( + topology, 11, 23, 1, 13, 23, 1, 10)); + CHECK("LOS rejects overlapping footprints", + !encounter_arena_topology_los_clear( + topology, 11, 21, 1, 11, 21, 1, 10)); + CHECK("range-one LOS accepts only cardinal adjacency", + encounter_arena_topology_los_clear( + topology, 11, 21, 1, 12, 21, 1, 1) && + !encounter_arena_topology_los_clear( + topology, 11, 21, 1, 12, 22, 1, 1) && + !encounter_arena_topology_los_clear( + topology, 11, 21, 1, 13, 21, 1, 1)); + CHECK("ranged LOS rejects pairs outside attack range", + !encounter_arena_topology_los_clear( + topology, 11, 21, 1, 17, 21, 1, 5)); CHECK("large-target LOS uses the closest occupied target tile", encounter_arena_topology_los_clear( topology, 11, 24, 1, 15, 23, 1, 10) && @@ -730,6 +749,53 @@ static void test_arena_topology_bounds_collision_los_and_revision(void) { topology_stale_revision_target = NULL; } +static void test_arena_topology_extreme_origins_and_reader_bounds(void) { + TopologyTestGeometry geometry = { + .origin_x = INT_MAX, + .origin_y = INT_MIN, + .width = 1, + .height = 1, + }; + EncounterArenaTopologyBuildSpec spec = topology_test_spec(&geometry, 52); + EncounterArenaTopology* topology = encounter_arena_topology_build(&spec); + + CHECK("topology build never reads geometry outside arena bounds", + geometry.outside_reads == 0); + + encounter_arena_topology_finalize(topology); + CHECK("INT_MAX width-one and INT_MIN height-one origin is contained", + encounter_arena_topology_contains(topology, INT_MAX, INT_MIN)); + CHECK("extreme origin tile stays walkable", + !encounter_arena_topology_tile_blocked( + topology, INT_MAX, INT_MIN) && + !encounter_arena_topology_footprint_blocked( + topology, INT_MAX, INT_MIN, 1)); + CHECK("INT_MIN north edge queries stay out of bounds", + !encounter_arena_topology_contains( + topology, INT_MAX, INT_MIN + 1) && + encounter_arena_topology_tile_blocked( + topology, INT_MAX, INT_MIN + 1) && + encounter_arena_topology_footprint_blocked( + topology, INT_MAX, INT_MIN + 1, 1)); + CHECK("INT_MAX west edge queries stay out of bounds", + !encounter_arena_topology_contains( + topology, INT_MAX - 1, INT_MIN) && + encounter_arena_topology_footprint_blocked( + topology, INT_MAX - 1, INT_MIN, 1)); + CHECK("all steps from extreme one-tile arena reject without reader access", + !encounter_arena_topology_step_allowed( + topology, INT_MAX, INT_MIN, 1, -1, 0) && + !encounter_arena_topology_step_allowed( + topology, INT_MAX, INT_MIN, 1, 1, 0) && + !encounter_arena_topology_step_allowed( + topology, INT_MAX, INT_MIN, 1, 0, -1) && + !encounter_arena_topology_step_allowed( + topology, INT_MAX, INT_MIN, 1, 0, 1) && + geometry.outside_reads == 0); + + free(topology); +} + static void test_arena_topology_rejects_invalid_bounds(void) { CHECK("topology build rejects zero width", topology_test_aborts(topology_build_rejects_zero_width)); @@ -741,6 +807,7 @@ static void test_arena_topology_rejects_invalid_bounds(void) { int main(void) { test_arena_topology_rejects_invalid_bounds(); + test_arena_topology_extreme_origins_and_reader_bounds(); test_arena_topology_bounds_collision_los_and_revision(); test_same_target_selection_preserves_route(); test_attack_route_field_stops_at_first_reachable_target_edge(); From 4c2c5f9342c360973e602b6c17d185c38db74364 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 00:57:10 +0300 Subject: [PATCH 12/50] Unify OSRS encounter routing --- .superpowers/sdd/task-3-report.md | 244 +++ .../colosseum/encounter_colosseum_boss.inc | 9 +- .../colosseum/encounter_colosseum_combat.inc | 2 +- .../colosseum/encounter_colosseum_helpers.inc | 59 +- .../colosseum/encounter_colosseum_model.inc | 3 + .../encounter_colosseum_movement.inc | 195 +- .../encounter_colosseum_player_actions.inc | 27 +- .../encounter_colosseum_render_snapshot.inc | 13 +- .../encounter_colosseum_reset_spawn.inc | 1 + ocean/osrs/encounters/encounter_nh_pvp.h | 24 +- ocean/osrs/encounters/encounter_zulrah.h | 162 +- .../inferno/encounter_inferno_helpers.inc | 67 +- .../inferno/encounter_inferno_model.inc | 3 + .../encounter_inferno_player_actions.inc | 30 +- .../encounter_inferno_render_snapshot.inc | 13 +- .../inferno/encounter_inferno_reset_spawn.inc | 1 + ocean/osrs/osrs_encounter.h | 909 --------- ocean/osrs/osrs_encounter_player.h | 240 ++- ocean/osrs/osrs_interaction.h | 40 +- ocean/osrs/osrs_pathfinding.h | 1697 ++++++++++++++--- ocean/osrs/osrs_pvp_actions.h | 9 +- ocean/osrs/osrs_pvp_api.h | 26 +- ocean/osrs/osrs_pvp_movement.h | 122 +- ocean/osrs/osrs_render.h | 2 + ocean/osrs/osrs_visual.c | 36 +- ocean/osrs/tests/bench_colo_step.c | 3 + .../tests/bench_colosseum_forecast_profile.c | 1 + ocean/osrs/tests/osrs_route_reference.h | 1679 ++++++++++++++++ .../tests/test_colosseum_forecast_exact.c | 197 +- ocean/osrs/tests/test_colosseum_modifiers.c | 25 +- ocean/osrs/tests/test_inferno_attack_styles.c | 5 + .../osrs/tests/test_inferno_forecast_exact.c | 107 +- ocean/osrs/tests/test_osrs_player_step.c | 310 ++- ocean/osrs_colosseum/osrs_colosseum.h | 1 + ocean/osrs_inferno/osrs_inferno.h | 1 + 35 files changed, 4684 insertions(+), 1579 deletions(-) create mode 100644 .superpowers/sdd/task-3-report.md create mode 100644 ocean/osrs/tests/osrs_route_reference.h diff --git a/.superpowers/sdd/task-3-report.md b/.superpowers/sdd/task-3-report.md new file mode 100644 index 0000000000..332215e4ff --- /dev/null +++ b/.superpowers/sdd/task-3-report.md @@ -0,0 +1,244 @@ +# Task 3 report + +Status: DONE + +## Result + +Inferno, Colosseum, Zulrah, and NH PvP now use `EncounterArenaTopology` plus `EncounterRouteBlockers` through one shared route engine. Production callers no longer use `EncounterArenaAttackRouteField`, `encounter_build_arena_attack_route_field`, `encounter_arena_attack_route_landing`, `encounter_pathfind_arena_attack_approach`, or the old arena BFS helpers. + +The engine returns `EncounterRouteResult` with `ROUTE_REACHED_TARGET`, `ROUTE_REACHED_FALLBACK`, `ROUTE_UNREACHABLE`, or `ROUTE_INVALID_INPUT`. Expected route failure is data. Invalid internal state, unsupported topology bounds, queue overflow, corrupt predecessor state, stale topology revision, and ownership violations abort. + +`EncounterRouteInput` carries the immutable topology and dynamic blocker callback, context, and revision. Query scratch and reusable source/reverse fields are thread-local fixed storage. Queries allocate nothing and take no locks. One finalized topology is process-shared per encounter and each encounter context stores a const pointer. Dynamic occupancy stays in encounter state. + +Player chase state moved out of serialized `OsrsInteraction` state into actor-local `OsrsActorRouteCache`. Cache validation covers topology revision, blocker revision, source/current actor position, actor size, target position and size, attack range, movement mode, and route-cost policy. Restore/reset paths clear the context cache. Serialized route bytes remain zero padding, preserving the exact state layout and artifacts. + +Equal-cost behavior remains explicit in `EncounterRouteCostPolicy`. Attack-chase OSRS order uses the packed source field. Colosseum NPC OSRS-aggro paths use the reverse field with preserved south-first edge ordering. Direct click movement retains the pre-cutover direct movement policy in Inferno, Colosseum, and NH PvP. Zulrah retains OSRS click routing because its golden traces depend on it. + +## TDD evidence + +### RED + +The first route-result tests were added to `test_osrs_player_step.c` before the production contract. This command failed to compile because the tagged route types and solver did not exist: + +```text +clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_player_step.c -lm -o /tmp/test_osrs_player_step_task3 +``` + +The first migrated forecast comparison exposed route-order drift: + +```text +/tmp/test_colosseum_forecast_exact_task3 --compare /tmp/task3-colosseum-forecast +colosseum exact mismatch at byte 1947076: expected 117 got 39 +``` + +Changing all explicit movement to direct routing proved incorrect for Zulrah: + +```text +clang -std=c11 -O2 -I. ocean/osrs/tests/test_zulrah_golden.c -lm -o /tmp/test_zulrah_golden_task3 && /tmp/test_zulrah_golden_task3 +``` + +All six golden hashes changed. Restoring Zulrah explicit movement to `ENCOUNTER_ROUTE_COST_OSRS` restored 6/6 exact hashes. + +The initial exhaustive reference comparison caught an invalid overlap case in the test driver: + +```text +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_forecast_exact.c -lm -o /tmp/test_colosseum_forecast_exact_task3 && /tmp/test_colosseum_forecast_exact_task3 --attack-route-selftest +route equivalence mismatch scenario=open-arena source=(0,13) target=(0,13,2) expected=(0,2,13,1,0,2) actual=(0,1,12,1,0,2) +``` + +The exhaustive driver was corrected to exclude actor/target overlap. The production interaction layer handles overlap through its explicit escape contract rather than normal attack-chase routing. + +### GREEN + +Tagged outcomes, cache invalidation, direct movement, tie order, topology bounds, dynamic blockers, and actor-local reuse: + +```text +clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_player_step.c -lm -o /tmp/test_osrs_player_step_task3 && /tmp/test_osrs_player_step_task3 +``` + +Result: 69/69 passed. + +Shared NPC movement, footprint masks, diagonal edge clearance, overlap rewrite, melee policy, and current overlap: + +```text +clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_npc_movement.c -lm -o /tmp/test_osrs_npc_movement_task3 && /tmp/test_osrs_npc_movement_task3 +``` + +Result: 29/29 passed. + +Exhaustive optimized-versus-reference BFS: + +```text +/tmp/test_colosseum_forecast_exact_task3 --attack-route-selftest +/tmp/test_inferno_forecast_exact_task3 --attack-route-selftest +``` + +Results: + +```text +colosseum exhaustive route equivalence PASS: 22511700 source-target-range queries across 2 blocker fields +colosseum attack route property selftest PASS: 1656 target queries across 3 fields +inferno exhaustive route equivalence PASS: 19048431 source-target-range queries across 2 blocker fields +``` + +The test-only reference performs an independent legacy FIFO BFS and landing scan. The checks cover every walkable source, every statically valid target anchor, target sizes through each encounter maximum, every encounter player attack range, and representative dynamic blocker revisions. Each comparison pins outcome, destination, shortest distance, first step, run step, and waypoint sequence. + +Exact forecast gates against fixtures generated by unmodified base commit `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731`: + +```text +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_forecast_exact.c -lm -o /tmp/test_colosseum_forecast_exact_task3 +clang -std=c11 -O2 -I. ocean/osrs/tests/test_inferno_forecast_exact.c -lm -o /tmp/test_inferno_forecast_exact_task3 +/tmp/test_colosseum_forecast_exact_task3 --compare /tmp/task3-colosseum-forecast +/tmp/test_inferno_forecast_exact_task3 --compare /tmp/task3-inferno-forecast +``` + +Results: + +```text +colosseum LoS table selftest PASS: 1336336 pairs +colosseum footprint table selftest PASS: 12005 checks +colosseum landing helper selftest PASS: 25 actions across 3 states +colosseum exhaustive route equivalence PASS: 22511700 source-target-range queries across 2 blocker fields +colosseum attack route property selftest PASS: 1656 target queries across 3 fields +colosseum exact golden compare PASS: /tmp/task3-colosseum-forecast/colosseum_forecast_exact.bin +inferno exact golden compare PASS: /tmp/task3-inferno-forecast/inferno_forecast_exact.bin +``` + +Focused encounter mechanics: + +```text +clang -std=c11 -O2 -I. ocean/osrs/tests/test_inferno_attack_styles.c -lm -o /tmp/test_inferno_attack_styles_task3 && /tmp/test_inferno_attack_styles_task3 +clang -std=c11 -O2 -I. ocean/osrs/tests/test_zulrah_hit_delay.c -lm -o /tmp/test_zulrah_hit_delay_task3 && /tmp/test_zulrah_hit_delay_task3 +clang -std=c11 -O2 -I. ocean/osrs/tests/test_zulrah_golden.c -lm -o /tmp/test_zulrah_golden_task3 && /tmp/test_zulrah_golden_task3 +clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_pvp_pending_hits.c -lm -o /tmp/test_osrs_pvp_pending_hits_task3 && /tmp/test_osrs_pvp_pending_hits_task3 +``` + +Results: + +- Inferno: 2041/2041 passed +- Zulrah hit delay and roll order: 313/313 passed +- Zulrah golden master: 6/6 hashes match baseline +- NH PvP pending hits: 9/9 passed + +An extra full `test_colosseum_modifiers.c` run produced 13545/13546 with only `generic item table code 101 feature 13 matches base-99 semantics` failing. The untouched base worktree produced the identical 13545/13546 failure. Task 3 did not change item encoding. Every route, movement, LoS, forecast, snapshot, combat, and modifier check in that binary passed. + +## Standalone API consumers + +No clangd indexer or harness LSP-reference operation was available. Before deleting exported route symbols, textual reference discovery covered production, tests, wrappers, and standalone benchmarks. Consumers importing the deleted API were migrated to the test-only reference header where comparison with the old implementation is intentional: + +- `ocean/osrs/tests/test_osrs_player_step.c` +- `ocean/osrs/tests/test_colosseum_forecast_exact.c` +- `ocean/osrs/tests/test_colosseum_modifiers.c` +- `ocean/osrs/tests/test_inferno_attack_styles.c` +- `ocean/osrs/tests/bench_colo_step.c` +- `ocean/osrs/tests/bench_colosseum_forecast_profile.c` +- `ocean/osrs_colosseum/osrs_colosseum.h` +- `ocean/osrs_inferno/osrs_inferno.h` + +Final production search for `EncounterArenaAttackRouteField`, `encounter_build_arena_attack_route_field`, `encounter_arena_attack_route_landing`, `encounter_pathfind_arena_attack_approach`, `pathfind_step_arena`, `BFS_VISIT`, `bfs_via`, and `route_field` returned matches only from `ocean/osrs/tests/osrs_route_reference.h` and tests calling that reference. No production match remains. + +## Build and portability gates + +Available native viewer wrappers: + +```text +OUTPUT_NAME=/tmp/osrs_task3_inferno_wrapper ./build.sh osrs_inferno --fast +OUTPUT_NAME=/tmp/osrs_task3_colosseum_wrapper ./build.sh osrs_colosseum --fast +``` + +Results: + +```text +Built: .//tmp/osrs_task3_inferno_wrapper +Built: .//tmp/osrs_task3_colosseum_wrapper +``` + +The repository has no separate Zulrah or NH PvP wrapper directories. Both compile into the shared Inferno viewer/profile wrapper and were exercised through `--encounter zulrah` and `--encounter nh_pvp`. + +The macOS ARM64 `--cpu` wrapper mode is unavailable because PufferLib's CPU backend includes x86-only intrinsics. It failed before encounter compilation. CPU translation portability was checked directly for every encounter header: + +```text +clang -std=c11 -O2 -I. /tmp/route_portability_inferno.c -lm -o /tmp/route_portability_inferno_c +clang -std=c11 -O2 -I. /tmp/route_portability_colosseum.c -lm -o /tmp/route_portability_colosseum_c +clang -std=c11 -O2 -I. /tmp/route_portability_zulrah.c -lm -o /tmp/route_portability_zulrah_c +clang -std=c11 -O2 -I. /tmp/route_portability_nh_pvp.c -lm -o /tmp/route_portability_nh_pvp_c +g++-16 -std=c++17 -O2 -I. -x c++ /tmp/route_portability_inferno.c -lm -o /tmp/route_portability_inferno_cpp +g++-16 -std=c++17 -O2 -I. -x c++ /tmp/route_portability_colosseum.c -lm -o /tmp/route_portability_colosseum_cpp +g++-16 -std=c++17 -O2 -I. -x c++ /tmp/route_portability_zulrah.c -lm -o /tmp/route_portability_zulrah_cpp +g++-16 -std=c++17 -O2 -I. -x c++ /tmp/route_portability_nh_pvp.c -lm -o /tmp/route_portability_nh_pvp_cpp +``` + +Result: all eight C/C++17 translation and link gates passed. + +## Paired performance + +Method: base binary from clean `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731`, final current wrapper, 100,000 steps, paired policy seeds 1 through 12, identical encounter and seed per pair. Command shape: + +```text +BINARY --profile --encounter ENCOUNTER --profile-steps 100000 --policy-seed SEED +``` + +| Encounter | Base SPS by seed 1..12 | Current SPS by seed 1..12 | Base GM | Current GM | Ratio GM | +|---|---|---|---:|---:|---:| +| Inferno | 732928, 722679, 729203, 733864, 738716, 726433, 721839, 739186, 737790, 738503, 732961, 720015 | 948506, 955283, 966165, 958810, 966912, 944323, 945519, 958194, 970374, 954955, 966333, 962955 | 731145 | 958157 | 1.3105x | +| Colosseum | 387531, 390387, 389754, 389536, 390386, 396981, 394889, 376865, 391412, 388967, 388386, 388967 | 477842, 470670, 474179, 472083, 475267, 472010, 476247, 469836, 473279, 466440, 476954, 477461 | 389477 | 473511 | 1.2158x | +| Zulrah | 332947, 333962, 333881, 334546, 336398, 333934, 333036, 333825, 332882, 333592, 333135, 336344 | 759394, 765240, 767772, 759884, 767631, 764918, 768445, 770707, 766842, 773826, 761899, 762939 | 334038 | 765780 | 2.2925x | +| NH PvP | 502596, 495555, 501100, 507315, 499805, 514422, 502917, 510694, 503882, 508564, 509007, 509793 | 961067, 956709, 968204, 959269, 956462, 959177, 965586, 962844, 960366, 952780, 959371, 958396 | 505445 | 960011 | 1.8993x | + +Aggregate geometric mean of the four encounter ratios: 1.6229x. No encounter regressed. + +## Changed files + +Production: + +- `ocean/osrs/osrs_encounter.h` +- `ocean/osrs/osrs_encounter_player.h` +- `ocean/osrs/osrs_interaction.h` +- `ocean/osrs/osrs_pathfinding.h` +- `ocean/osrs/osrs_pvp_actions.h` +- `ocean/osrs/osrs_pvp_api.h` +- `ocean/osrs/osrs_pvp_movement.h` +- `ocean/osrs/osrs_render.h` +- `ocean/osrs/osrs_visual.c` +- `ocean/osrs/encounters/encounter_nh_pvp.h` +- `ocean/osrs/encounters/encounter_zulrah.h` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc` +- `ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc` +- `ocean/osrs/encounters/inferno/encounter_inferno_model.inc` +- `ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc` +- `ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc` +- `ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc` + +Tests and standalone consumers: + +- `ocean/osrs/tests/osrs_route_reference.h` +- `ocean/osrs/tests/test_osrs_player_step.c` +- `ocean/osrs/tests/test_colosseum_forecast_exact.c` +- `ocean/osrs/tests/test_inferno_forecast_exact.c` +- `ocean/osrs/tests/test_colosseum_modifiers.c` +- `ocean/osrs/tests/test_inferno_attack_styles.c` +- `ocean/osrs/tests/bench_colo_step.c` +- `ocean/osrs/tests/bench_colosseum_forecast_profile.c` +- `ocean/osrs_colosseum/osrs_colosseum.h` +- `ocean/osrs_inferno/osrs_inferno.h` + +## Self-review + +- Topology stays immutable after finalization. No dynamic actor, pillar, clamp, cloud, or opponent occupancy entered it. +- Contexts hold const process-shared topology pointers. +- Route scratch is thread-local, fixed-size, and generation-guarded or reset before reads. +- Query hot paths allocate nothing, take no locks, and have no fallback to the deleted production implementation. +- Actor route caches validate every required key. Changed blocker revisions and target geometry reroute from the actor's current tile. +- Source/reverse fields validate topology revision, blocker identity/revision, source or target geometry, actor size, movement mode, and cost policy as applicable. +- Dynamic blocker memoization distinguishes unknown, open, and blocked footprints and never mutates topology. +- Fallback distance, FIFO/depth tie-breaking, first step, run step, and waypoint compression match the independent reference across 41,560,131 exhaustive queries. +- Inferno and Colosseum forecast files remain byte-exact. Zulrah's six hashes remain exact. Snapshot state size and serialized interaction layout remain unchanged. +- Production search finds no old route field or arena BFS symbol. The only legacy implementation is isolated in `ocean/osrs/tests/osrs_route_reference.h`. +- Final `git diff --check` passed. diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc index 58ab110814..c154260cf2 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc @@ -161,7 +161,7 @@ static void col_sol_tick_attack_engine(ColosseumState* s, int idx) { col_sol_fire_lasers(s); } -static void col_sol_move(ColosseumState* s, int idx) { +static void col_sol_move(ColosseumState* s, ColosseumContext* ctx, int idx) { SolHereditState* sol = &s->sol; ColoNPC* boss = &s->npcs[idx]; boss->target_x = s->player.x; @@ -171,7 +171,8 @@ static void col_sol_move(ColosseumState* s, int idx) { if (encounter_dist_to_npc(s->player.x, s->player.y, boss->x, boss->y, size) <= 1) return; col_stamp_npc_collision_footprint(s, boss->x, boss->y, size, 0); - int moved = col_npc_path_step(s, &boss->x, &boss->y, size, + int moved = col_npc_path_step( + s, ctx->route_topology, &boss->x, &boss->y, size, s->player.x - size / 2, s->player.y - size / 2); col_stamp_npc_collision_footprint(s, boss->x, boss->y, size, 1); boss->moved_this_tick = moved ? 1 : 0; @@ -647,7 +648,7 @@ static void col_sol_enter_phase(ColosseumState* s, int new_phase) { if (new_phase > s->log.boss_phase_reached) s->log.boss_phase_reached = new_phase; } -static void col_sol_boss_tick(ColosseumState* s) { +static void col_sol_boss_tick(ColosseumState* s, ColosseumContext* ctx) { SolHereditState* sol = &s->sol; if (s->wave != COLO_WAVE_BOSS || !sol->started) return; @@ -678,5 +679,5 @@ static void col_sol_boss_tick(ColosseumState* s) { col_sol_tick_grapple(s); col_sol_tick_attack_engine(s, idx); - col_sol_move(s, idx); + col_sol_move(s, ctx, idx); } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc index 1695d3202b..bcdb036433 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc @@ -723,7 +723,7 @@ static void col_tick_npcs_ctx(ColosseumState* s, ColosseumContext* ctx) { double col_prof_total_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; double col_prof_t0 = col_prof_total_t0; #endif - col_sol_boss_tick(s); + col_sol_boss_tick(s, ctx); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_STEP_SOL_BOSS); #endif diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc index ad869c7cdc..a6116d7d01 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc @@ -13,7 +13,7 @@ static void col_resolve_player_pending_hits(ColosseumState* s); static void col_rebuild_player_collision_flags(ColosseumState* s); static void col_refresh_current_obs_slots_ctx(ColosseumState* s, const ColosseumContext* ctx); -static void col_sol_boss_tick(ColosseumState* s); +static void col_sol_boss_tick(ColosseumState* s, ColosseumContext* ctx); static void col_sol_begin_boss_arena(ColosseumState* s); static int col_sol_clamp_active(const ColosseumState* s); static int col_in_boss_arena(const ColosseumState* s, int x, int y); @@ -288,6 +288,60 @@ static ColConfig col_default_config(void) { }; } +typedef struct { + EncounterArenaTopology* topology; + const CollisionMap* collision_map; + int world_offset_x; + int world_offset_y; +} ColRouteTopologyOwner; + +static ColRouteTopologyOwner col_route_topology_owner; + +static uint32_t col_route_topology_flags(void* data, int x, int y) { + (void)data; + int grid_x = x - COLO_ARENA_MIN_X; + int grid_y = y - COLO_ARENA_MIN_Y; + if (grid_x < 0 || grid_x >= COLO_ARENA_WIDTH || + grid_y < 0 || grid_y >= COLO_ARENA_HEIGHT || + COLO_STATIC_BLOCKED[grid_x][grid_y]) + return COLLISION_BLOCKED | LOS_FULL_MASK; + return 0; +} + +static void col_bind_route_topology( + ColosseumContext* ctx, + const CollisionMap* collision_map +) { + if (col_route_topology_owner.topology) { + if (col_route_topology_owner.collision_map != collision_map || + col_route_topology_owner.world_offset_x != ctx->world_offset_x || + col_route_topology_owner.world_offset_y != ctx->world_offset_y) { + fprintf(stderr, "colosseum route topology geometry identity changed\n"); + abort(); + } + ctx->route_topology = col_route_topology_owner.topology; + return; + } + col_build_static_arena(); + EncounterArenaTopologyBuildSpec spec = { + .origin_x = COLO_ARENA_MIN_X, + .origin_y = COLO_ARENA_MIN_Y, + .width = COLO_ARENA_WIDTH, + .height = COLO_ARENA_HEIGHT, + .max_footprint_size = ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE, + .revision = UINT64_C(0x434f4c4f53534501), + .tile_flags = col_route_topology_flags, + .tile_flags_ctx = NULL, + }; + col_route_topology_owner.topology = + encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(col_route_topology_owner.topology); + col_route_topology_owner.collision_map = collision_map; + col_route_topology_owner.world_offset_x = ctx->world_offset_x; + col_route_topology_owner.world_offset_y = ctx->world_offset_y; + ctx->route_topology = col_route_topology_owner.topology; +} + static void col_init_context_typed(ColosseumContext* ctx) { memset(ctx, 0, sizeof(*ctx)); ctx->config = col_default_config(); @@ -306,6 +360,7 @@ static ColosseumContext* col_legacy_context(void) { static int initialized = 0; if (!initialized) { col_init_context_typed(&ctx); + col_bind_route_topology(&ctx, NULL); initialized = 1; } return &ctx; @@ -326,7 +381,7 @@ static void col_init_state(EncounterState* state, EncounterContext* context) { static EncounterState* col_create(void) { ColosseumState* s = (ColosseumState*)calloc(1, sizeof(ColosseumState)); - col_init_state_typed(s, col_legacy_context()); + col_init_state_typed(s, NULL); return (EncounterState*)s; } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index 4579e225e0..0d7816872f 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -1427,6 +1427,8 @@ typedef enum { typedef struct { ColConfig config; const CollisionMap* collision_map; + const EncounterArenaTopology* route_topology; + OsrsActorRouteCache player_route_cache; int world_offset_x; int world_offset_y; const HumanCommand* human_commands; @@ -1687,6 +1689,7 @@ typedef struct { ColosseumLog log; ColoObsMemoCaches obs_memos; } ColosseumState; +static_assert(sizeof(ColosseumState) == 29832, "ColosseumState serialized layout"); typedef enum { COLO_DMG_OFFPRAY = 0, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc index 3275affa4a..e500131bef 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc @@ -84,28 +84,25 @@ static int col_npc_move_overlap_hold(void* ctx) { typedef struct { ColosseumState* s; + const EncounterArenaTopology* topology; int size; - uint16_t gen; - int ignore_npcs; } ColoNpcPathCtx; -typedef struct { - uint16_t blocked_gen[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; - uint8_t blocked_value[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; - uint16_t counter; -} ColoNpcPathCache; - -static OSRS_THREAD_LOCAL ColoNpcPathCache col_npc_path_cache; - -static ColoNpcPathCtx col_npc_path_ctx_begin(ColosseumState* s, int size) { - col_npc_path_cache.counter++; - if (col_npc_path_cache.counter == 0) { - memset(col_npc_path_cache.blocked_gen, 0, - sizeof(col_npc_path_cache.blocked_gen)); - col_npc_path_cache.counter = 1; +static ColoNpcPathCtx col_npc_path_ctx_begin( + ColosseumState* s, + const EncounterArenaTopology* topology, + int size +) { + if (!topology) { + fprintf(stderr, "colosseum NPC route topology is not bound\n"); + abort(); } - return (ColoNpcPathCtx){ .s = s, .size = size, .gen = col_npc_path_cache.counter }; + return (ColoNpcPathCtx){ + .s = s, + .topology = topology, + .size = size, + }; } static int col_npc_path_blocked_uncached(ColoNpcPathCtx* pc, int x, int y) { @@ -125,121 +122,79 @@ static int col_npc_path_blocked_uncached(ColoNpcPathCtx* pc, int x, int y) { return col_npc_blocked_ignore_player(s, x, y, pc->size); } -static int col_npc_path_blocked(void* ctx, int x, int y) { +static int col_npc_path_blocked(void* ctx, int x, int y, int size) { ColoNpcPathCtx* pc = (ColoNpcPathCtx*)ctx; - int gx, gy; - if (!col_grid_index(x, y, &gx, &gy)) - return col_npc_path_blocked_uncached(pc, x, y); - if (col_npc_path_cache.blocked_gen[gx][gy] != pc->gen) { - col_npc_path_cache.blocked_gen[gx][gy] = pc->gen; - col_npc_path_cache.blocked_value[gx][gy] = - (uint8_t)col_npc_path_blocked_uncached(pc, x, y); - } - return col_npc_path_cache.blocked_value[gx][gy]; -} - -#define COL_WARBAND_BFS_MEMO_BITS 16 -#define COL_WARBAND_BFS_MEMO_SIZE (1 << COL_WARBAND_BFS_MEMO_BITS) -static OSRS_THREAD_LOCAL uint64_t col_warband_bfs_memo_key[COL_WARBAND_BFS_MEMO_SIZE]; -static OSRS_THREAD_LOCAL uint8_t col_warband_bfs_memo_result[COL_WARBAND_BFS_MEMO_SIZE]; - -static uint64_t col_warband_bfs_memo_pack( - const ColosseumState* s, int x, int y, int tx, int ty) { - uint64_t key = 1; - key = key << 6 | (uint64_t)x; - key = key << 6 | (uint64_t)y; - key = key << 6 | (uint64_t)tx; - key = key << 6 | (uint64_t)ty; - key = key << 6 | (uint64_t)s->player.x; - key = key << 6 | (uint64_t)s->player.y; - if (col_sol_clamp_active(s)) { - key = key << 1 | 1; - key = key << 6 | (uint64_t)s->sol.boss_arena_min_x; - key = key << 6 | (uint64_t)s->sol.boss_arena_max_x; - key = key << 6 | (uint64_t)s->sol.boss_arena_min_y; - key = key << 6 | (uint64_t)s->sol.boss_arena_max_y; - } else { - key <<= 1; + if (size != pc->size) { + fprintf(stderr, "colosseum NPC route actor size changed\n"); + abort(); } - return key; -} - -static uint32_t col_warband_bfs_memo_slot(uint64_t key) { - return (uint32_t)((key * 0x9E3779B97F4A7C15ULL) >> (64 - COL_WARBAND_BFS_MEMO_BITS)); -} - -static uint8_t col_warband_bfs_memo_encode(PathResult pr) { - return (uint8_t)((pr.found ? 1 : 0) | - (uint32_t)(pr.next_dx + 1) << 1 | - (uint32_t)(pr.next_dy + 1) << 3); + return col_npc_path_blocked_uncached(pc, x, y); } -static int col_npc_path_step_with_ctx(ColoNpcPathCtx* pc, int* x, int* y, int tx, int ty) { +static int col_npc_path_step_with_ctx( + ColoNpcPathCtx* pc, + int* x, + int* y, + int tx, + int ty +) { if (tx < COLO_ARENA_MIN_X) tx = COLO_ARENA_MIN_X; if (tx > COLO_ARENA_MAX_X) tx = COLO_ARENA_MAX_X; if (ty < COLO_ARENA_MIN_Y) ty = COLO_ARENA_MIN_Y; if (ty > COLO_ARENA_MAX_Y) ty = COLO_ARENA_MAX_Y; if (*x == tx && *y == ty) return 0; - int direct_dx = (tx > *x) - (tx < *x); - int direct_dy = (ty > *y) - (ty < *y); - int direct_abs_dx = tx > *x ? tx - *x : *x - tx; - int direct_abs_dy = ty > *y ? ty - *y : *y - ty; - if (direct_abs_dx <= 1 && direct_abs_dy <= 1 && - !col_npc_path_blocked(pc, tx, ty) && - (direct_dx == 0 || direct_dy == 0 || - (!col_npc_path_blocked(pc, *x + direct_dx, *y) && - !col_npc_path_blocked(pc, *x, *y + direct_dy)))) { - *x = tx; - *y = ty; - return 1; - } - int memoable = pc->ignore_npcs && pc->size == 1 && - *x >= COLO_ARENA_MIN_X && *x <= COLO_ARENA_MAX_X && - *y >= COLO_ARENA_MIN_Y && *y <= COLO_ARENA_MAX_Y && - pc->s->player.x >= COLO_ARENA_MIN_X && pc->s->player.x <= COLO_ARENA_MAX_X && - pc->s->player.y >= COLO_ARENA_MIN_Y && pc->s->player.y <= COLO_ARENA_MAX_Y; - uint64_t memo_key = 0; - uint32_t memo_slot = 0; - if (memoable) { - memo_key = col_warband_bfs_memo_pack(pc->s, *x, *y, tx, ty); - memo_slot = col_warband_bfs_memo_slot(memo_key); - if (col_warband_bfs_memo_key[memo_slot] == memo_key) { - uint8_t r = col_warband_bfs_memo_result[memo_slot]; - int found = r & 1; - int next_dx = (int)((r >> 1) & 3) - 1; - int next_dy = (int)((r >> 3) & 3) - 1; - if (!found || (next_dx == 0 && next_dy == 0)) return 0; - *x += next_dx; - *y += next_dy; - return 1; - } - } #ifdef COLO_PROFILE_ENABLED int col_prof_enabled = COLO_PROFILE_ENABLED() && !pc->s->profiling_suppressed; double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; #endif - PathResult pr = pathfind_step_arena(NULL, 0, *x, *y, tx, ty, - col_npc_path_blocked, pc, - COLO_ARENA_MIN_X, COLO_ARENA_MIN_Y, COLO_ARENA_WIDTH, COLO_ARENA_HEIGHT); + EncounterRouteInput input = { + .topology = pc->topology, + .blockers = { + .is_blocked = col_npc_path_blocked, + .ctx = pc, + .revision = 1, + }, + .source_x = *x, + .source_y = *y, + .actor_size = pc->size, + .target_x = tx, + .target_y = ty, + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_WALK, + .cost_policy = pc->ignore_npcs + ? ENCOUNTER_ROUTE_COST_SOUTH_FIRST_REVERSE + : ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + }; + EncounterRouteResult route = encounter_route_solve(&input); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_STEP_NPC_PATHFINDING); #endif - if (memoable) { - col_warband_bfs_memo_key[memo_slot] = memo_key; - col_warband_bfs_memo_result[memo_slot] = col_warband_bfs_memo_encode(pr); + if ((route.outcome != ROUTE_REACHED_TARGET && + route.outcome != ROUTE_REACHED_FALLBACK) || + route.distance == 0) { + return 0; } - if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) return 0; - *x += pr.next_dx; - *y += pr.next_dy; + *x += route.first_dx; + *y += route.first_dy; return 1; } -static int col_npc_path_step(ColosseumState* s, int* x, int* y, int size, int tx, int ty) { - ColoNpcPathCtx pc = col_npc_path_ctx_begin(s, size); +static int col_npc_path_step( + ColosseumState* s, + const EncounterArenaTopology* topology, + int* x, + int* y, + int size, + int tx, + int ty +) { + ColoNpcPathCtx pc = col_npc_path_ctx_begin(s, topology, size); return col_npc_path_step_with_ctx(&pc, x, y, tx, ty); } + static void col_warband_pick_target( ColosseumState* s, ColoNpcPathCtx* pc, ColoNPC* npc, int* tx, int* ty) { int px = s->player.x, py = s->player.y; @@ -248,7 +203,7 @@ static void col_warband_pick_target( int fy = py + COLO_WARBAND_FORM_OFFSET[pref][1]; *tx = fx; *ty = fy; - if (!col_npc_path_blocked(pc, fx, fy)) return; + if (!col_npc_path_blocked(pc, fx, fy, pc->size)) return; static const int ADJ[8][2] = { {0, 1}, {1, 0}, {-1, 0}, {0, -1}, @@ -256,7 +211,7 @@ static void col_warband_pick_target( int best_key = 1 << 30; for (int a = 0; a < 8; a++) { int x = px + ADJ[a][0], y = py + ADJ[a][1]; - if (col_npc_path_blocked(pc, x, y)) continue; + if (col_npc_path_blocked(pc, x, y, pc->size)) continue; int dx = npc->x > x ? npc->x - x : x - npc->x; int dy = npc->y > y ? npc->y - y : y - npc->y; int cheb = dx > dy ? dx : dy; @@ -265,12 +220,17 @@ static void col_warband_pick_target( } } -static void col_warband_move(ColosseumState* s, int idx) { +static void col_warband_move( + ColosseumState* s, + const ColosseumContext* ctx, + int idx +) { ColoNPC* npc = &s->npcs[idx]; npc->target_x = s->player.x; npc->target_y = s->player.y; - ColoNpcPathCtx pc = col_npc_path_ctx_begin(s, 1); + ColoNpcPathCtx pc = + col_npc_path_ctx_begin(s, ctx->route_topology, 1); pc.ignore_npcs = 1; int tx, ty; col_warband_pick_target(s, &pc, npc, &tx, &ty); @@ -284,7 +244,10 @@ static void col_warband_move(ColosseumState* s, int idx) { } static void col_npc_move_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) { - (void)ctx; + if (!ctx || !ctx->route_topology) { + fprintf(stderr, "colosseum route context is not bound\n"); + abort(); + } ColoNPC* npc = &s->npcs[idx]; const ColoNpcStats* st = &COLO_NPC_STATS[npc->type]; if (npc->type == COLO_SOL_HEREDIT) return; @@ -292,7 +255,7 @@ static void col_npc_move_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; if (col_type_is_warbander(npc->type)) { - col_warband_move(s, idx); + col_warband_move(s, ctx, idx); return; } @@ -327,7 +290,9 @@ static void col_npc_move_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) moved = 0; if (encounter_dist_to_npc(tx, ty, npc->x, npc->y, size) > 1) - moved = col_npc_path_step(s, &npc->x, &npc->y, size, tx - 1, ty - 1); + moved = col_npc_path_step( + s, ctx->route_topology, + &npc->x, &npc->y, size, tx - 1, ty - 1); } else { EncounterNpcStepPolicy step_policy = is_ranged ? ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc index 8c19e8a525..fcda499cbe 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -513,6 +513,15 @@ static int col_tile_walkable(void* ctx, int x, int y) { return col_player_walkable(wc->s, x, y); } +static int col_player_route_blocked(void* data, int x, int y, int size) { + ColosseumState* state = (ColosseumState*)data; + if (!col_sol_clamp_active(state)) return 0; + for (int dx = 0; dx < size; dx++) + for (int dy = 0; dy < size; dy++) + if (!col_in_boss_arena(state, x + dx, y + dy)) return 1; + return 0; +} + static int col_npc_collision_flag_at(const ColosseumState* s, int x, int y) { int gx, gy; @@ -1033,24 +1042,26 @@ static ColoPrimaryStepPlan col_primary_step_plan( .input = { .player = player, .interaction = interaction, + .route_cache = &ctx->player_route_cache, .target_lookup = col_lookup_player_attack_target, .target_ctx = s, .command = command, .dest_x = player_dest_x, .dest_y = player_dest_y, .arena = { + .topology = ctx->route_topology, + .blockers = { + .is_blocked = col_player_route_blocked, + .ctx = s, + .revision = (uint64_t)col_sol_clamp_active(s) + 1, + }, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + .explicit_cost_policy = ENCOUNTER_ROUTE_COST_DIRECT, .collision_map = ctx->collision_map, .world_offset_x = ctx->world_offset_x, .world_offset_y = ctx->world_offset_y, - .is_walkable = col_tile_walkable, - .walkable_ctx = walk_ctx, - .extra_blocked = NULL, - .blocked_ctx = NULL, .los_query = los_query, - .arena_base_x = COLO_ARENA_MIN_X, - .arena_base_y = COLO_ARENA_MIN_Y, - .arena_w = COLO_ARENA_WIDTH, - .arena_h = COLO_ARENA_HEIGHT, }, }, .move_action = move_action, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 8096bb55b1..26464e4369 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -146,8 +146,12 @@ static void col_put_ptr_ctx( ) { (void)state; ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); - if (strcmp(key, "collision_map") == 0) ctx->collision_map = (const CollisionMap*)value; - else encounter_abort_unknown_config("colosseum", "ptr", key); + if (strcmp(key, "collision_map") == 0) { + col_bind_route_topology(ctx, (const CollisionMap*)value); + ctx->collision_map = (const CollisionMap*)value; + } else { + encounter_abort_unknown_config("colosseum", "ptr", key); + } } static int col_get_tick_ctx(EncounterState* state, EncounterContext* context) { @@ -1042,6 +1046,9 @@ typedef struct { } ColoSnapshot; static void col_refresh_after_state_load(ColosseumState* s, ColosseumContext* ctx) { + osrs_interaction_zero_serialized_route_padding(&s->player.interaction); + osrs_interaction_zero_serialized_route_padding(&s->interaction); + osrs_actor_route_cache_clear(&ctx->player_route_cache); col_build_npc_stats(); col_mark_live_loadout_dirty(s); col_ensure_live_loadout_stats(s); @@ -1063,6 +1070,8 @@ static void col_snapshot_ctx(EncounterState* state, EncounterContext* context, v sizeof(ColosseumState)); encounter_snapshot_copy_state_to( snap, offsetof(ColoSnapshot, state), state, sizeof(ColosseumState)); + osrs_interaction_zero_serialized_route_padding(&snap->state.player.interaction); + osrs_interaction_zero_serialized_route_padding(&snap->state.interaction); } static void col_restore_ctx( diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc index e0190237d6..98b160d7a6 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -480,6 +480,7 @@ static void col_reset_ctx(EncounterState* state, EncounterContext* context, uint col_build_npc_stats(); ColosseumState* s = (ColosseumState*)state; ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + osrs_actor_route_cache_clear(&ctx->player_route_cache); int saved_start = ctx->config.start_wave; uint32_t saved_rng = s->rng_state; col_context_clear_render_events(ctx); diff --git a/ocean/osrs/encounters/encounter_nh_pvp.h b/ocean/osrs/encounters/encounter_nh_pvp.h index a3d8cc55fa..9c36bb7c9f 100644 --- a/ocean/osrs/encounters/encounter_nh_pvp.h +++ b/ocean/osrs/encounters/encounter_nh_pvp.h @@ -14,9 +14,11 @@ static const int NH_PVP_ACTION_DIMS[] = { typedef struct { OsrsEnv env; } NhPvpState; +static_assert(sizeof(NhPvpState) == 7160, "NhPvpState serialized layout"); typedef struct { - int unused; + const EncounterArenaTopology* route_topology; + OsrsActorRouteCache player_route_cache[NUM_AGENTS]; } NhPvpContext; static void nh_pvp_translate_human_input(HumanInput* hi, int* actions, Player* agent, Player* target) { @@ -61,7 +63,7 @@ static void nh_pvp_destroy(EncounterState* state) { } static void nh_pvp_init_context(EncounterContext* context) { - (void)context; + memset(context, 0, sizeof(NhPvpContext)); } static void nh_pvp_destroy_context(EncounterContext* context) { @@ -69,20 +71,24 @@ static void nh_pvp_destroy_context(EncounterContext* context) { } static void nh_pvp_reset(EncounterState* state, EncounterContext* context, uint32_t seed) { - (void)context; NhPvpState* s = (NhPvpState*)state; + NhPvpContext* ctx = (NhPvpContext*)context; + if (!ctx->route_topology) + ctx->route_topology = pvp_route_topology_setup( + (const CollisionMap*)s->env.collision_map); if (seed != 0) { s->env.has_rng_seed = 1; s->env.rng_seed = seed; } + pvp_actor_route_caches_clear(ctx->player_route_cache); pvp_reset(&s->env); } static void nh_pvp_step(EncounterState* state, EncounterContext* context, const int* actions) { - (void)context; + NhPvpContext* ctx = (NhPvpContext*)context; NhPvpState* s = (NhPvpState*)state; memcpy(s->env.ocean_io.agent_actions, actions, NUM_ACTION_HEADS * sizeof(int)); - pvp_step(&s->env); + pvp_step(&s->env, ctx->route_topology, ctx->player_route_cache); } static void nh_pvp_step_human_commands( @@ -90,7 +96,7 @@ static void nh_pvp_step_human_commands( EncounterContext* context, HumanInput* hi ) { - (void)context; + NhPvpContext* ctx = (NhPvpContext*)context; NhPvpState* s = (NhPvpState*)state; int saved_use_c_opponent_p0 = s->env.pvp_runtime.use_c_opponent_p0; s->env.pvp_runtime.use_c_opponent_p0 = 0; @@ -103,7 +109,7 @@ static void nh_pvp_step_human_commands( s->env.ocean_io.agent_actions, &s->env.players[0], &s->env.players[1]); - pvp_step(&s->env); + pvp_step(&s->env, ctx->route_topology, ctx->player_route_cache); s->env.pvp_runtime.use_c_opponent_p0 = saved_use_c_opponent_p0; /* pending_move must survive until arrival so later ticks keep re-arming walk_dest for the same click */ @@ -240,10 +246,12 @@ static void nh_pvp_put_ptr( const char* key, void* value ) { - (void)context; NhPvpState* s = (NhPvpState*)state; if (strcmp(key, "collision_map") == 0) { + NhPvpContext* ctx = (NhPvpContext*)context; s->env.collision_map = value; + ctx->route_topology = + pvp_route_topology_setup((const CollisionMap*)value); } } diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h index 538caf6078..b6da52e3d6 100644 --- a/ocean/osrs/encounters/encounter_zulrah.h +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -27,6 +27,11 @@ #define ZUL_PLATFORM_MIN 5 #define ZUL_PLATFORM_MAX 22 + +typedef struct { + const EncounterArenaTopology* route_topology; + OsrsActorRouteCache player_route_cache; +} ZulrahContext; #define ZUL_POS_NORTH 0 #define ZUL_POS_SOUTH 1 #define ZUL_POS_EAST 2 @@ -521,6 +526,7 @@ typedef struct { Log log; } ZulrahState; +static_assert(sizeof(ZulrahState) == 12712, "ZulrahState serialized layout"); static void zul_set_npc_anim_event(ZulrahState* s, int anim_id, int duration_ticks) { osrs_npc_primary_anim_event_set( @@ -689,12 +695,75 @@ static inline int zul_on_platform(ZulrahState* s, int x, int y) { return collision_tile_walkable((const CollisionMap*)s->collision_map, 0, wx, wy); } -#define zul_pathfind(s, sx, sy, dx, dy) \ - encounter_pathfind((const CollisionMap*)(s)->collision_map, \ - (s)->world_offset_x, (s)->world_offset_y, (sx), (sy), (dx), (dy), NULL, NULL) -static int zul_tile_walkable(void* ctx, int x, int y) { - return zul_on_platform((ZulrahState*)ctx, x, y); + + +typedef struct { + EncounterArenaTopology* topology; + const CollisionMap* collision_map; + int world_offset_x; + int world_offset_y; +} ZulRouteTopologyOwner; + +typedef struct { + const CollisionMap* collision_map; + int world_offset_x; + int world_offset_y; +} ZulRouteTopologyBuildContext; + +static ZulRouteTopologyOwner zul_route_topology_owner; + +static uint32_t zul_route_topology_flags(void* data, int x, int y) { + const ZulRouteTopologyBuildContext* build = + (const ZulRouteTopologyBuildContext*)data; + if (!build->collision_map) + return zul_on_platform_bounds(x, y) + ? 0 + : COLLISION_BLOCKED | LOS_FULL_MASK; + return (uint32_t)collision_get_flags( + build->collision_map, + 0, + x + build->world_offset_x, + y + build->world_offset_y); +} + +static void zul_bind_route_topology( + ZulrahContext* ctx, + const ZulrahState* state, + const CollisionMap* collision_map +) { + if (zul_route_topology_owner.topology) { + if (zul_route_topology_owner.collision_map != collision_map || + zul_route_topology_owner.world_offset_x != state->world_offset_x || + zul_route_topology_owner.world_offset_y != state->world_offset_y) { + fprintf(stderr, "zulrah route topology geometry identity changed\n"); + abort(); + } + ctx->route_topology = zul_route_topology_owner.topology; + return; + } + ZulRouteTopologyBuildContext build = { + collision_map, + state->world_offset_x, + state->world_offset_y, + }; + EncounterArenaTopologyBuildSpec spec = { + .origin_x = 0, + .origin_y = 0, + .width = ZUL_ARENA_SIZE, + .height = ZUL_ARENA_SIZE, + .max_footprint_size = ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE, + .revision = UINT64_C(0x5a554c5241480001), + .tile_flags = zul_route_topology_flags, + .tile_flags_ctx = &build, + }; + zul_route_topology_owner.topology = + encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(zul_route_topology_owner.topology); + zul_route_topology_owner.collision_map = collision_map; + zul_route_topology_owner.world_offset_x = state->world_offset_x; + zul_route_topology_owner.world_offset_y = state->world_offset_y; + ctx->route_topology = zul_route_topology_owner.topology; } static inline int zul_player_in_cloud(int cx, int cy, int px, int py) { @@ -1360,7 +1429,10 @@ static void zul_spawn_snakeling(ZulrahState* s) { } } -static void zul_snakeling_tick(ZulrahState* s) { +static void zul_snakeling_tick( + ZulrahState* s, + const ZulrahContext* ctx +) { for (int i = 0; i < ZUL_MAX_SNAKELINGS; i++) { ZulrahSnakeling* sn = &s->snakelings[i]; if (!sn->active) continue; @@ -1373,14 +1445,24 @@ static void zul_snakeling_tick(ZulrahState* s) { int ady = abs_int(sn->entity.y - s->player.y); int in_range = (adx <= 1 && ady <= 1); if (!in_range) { - PathResult pr = zul_pathfind(s, sn->entity.x, sn->entity.y, - s->player.x, s->player.y); - if (pr.found && (pr.next_dx != 0 || pr.next_dy != 0)) { - int nx = sn->entity.x + pr.next_dx; - int ny = sn->entity.y + pr.next_dy; - if (zul_on_platform(s, nx, ny)) { - sn->entity.x = nx; sn->entity.y = ny; - } + EncounterRouteInput route_input = { + .topology = ctx->route_topology, + .blockers = {0}, + .source_x = sn->entity.x, + .source_y = sn->entity.y, + .actor_size = 1, + .target_x = s->player.x, + .target_y = s->player.y, + .target_size = 1, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_WALK, + .cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST, + }; + EncounterRouteResult route = encounter_route_solve(&route_input); + if (route.outcome != ROUTE_INVALID_INPUT && + route.outcome != ROUTE_UNREACHABLE && + (route.first_dx != 0 || route.first_dy != 0)) { + sn->entity.x += route.first_dx; + sn->entity.y += route.first_dy; } } @@ -2316,7 +2398,8 @@ static void zul_seed_inventory_cells(ZulrahState* s) { } static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t seed) { - (void)context; + ZulrahContext* ctx = (ZulrahContext*)context; + osrs_actor_route_cache_clear(&ctx->player_route_cache); ZulrahState* s = (ZulrahState*)state; Log saved_log = s->log; void* saved_cmap = s->collision_map; @@ -2369,9 +2452,16 @@ static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t (s->gear_tier >= 1) ? OFFENSIVE_PRAYER_AUGURY : OFFENSIVE_PRAYER_NONE; zul_mark_live_stats_dirty(s); zul_start_active_kill(s); + if (ctx && !ctx->route_topology) + zul_bind_route_topology( + ctx, s, (const CollisionMap*)s->collision_map); } -static void zul_step_tick(ZulrahState* s, const int* actions) { +static void zul_step_tick( + ZulrahState* s, + ZulrahContext* ctx, + const int* actions +) { s->reward = 0.0f; s->damage_dealt_this_tick = 0.0f; s->damage_received_this_tick = 0.0f; @@ -2509,6 +2599,7 @@ static void zul_step_tick(ZulrahState* s, const int* actions) { OsrsPlayerStepInput step_input = { .player = &s->player, .interaction = &s->interaction, + .route_cache = &ctx->player_route_cache, .target_lookup = zul_lookup_player_attack_target, .target_ctx = s, .command = command, @@ -2516,16 +2607,15 @@ static void zul_step_tick(ZulrahState* s, const int* actions) { .dest_y = &s->player_dest_y, .blocked_ticks = s->player_stunned_ticks, .arena = { + .topology = ctx->route_topology, + .blockers = {0}, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + .explicit_cost_policy = ENCOUNTER_ROUTE_COST_OSRS, .collision_map = (const CollisionMap*)s->collision_map, .world_offset_x = s->world_offset_x, .world_offset_y = s->world_offset_y, - .is_walkable = zul_tile_walkable, - .walkable_ctx = s, .los_query = osrs_los_open_query(), - .arena_base_x = 0, - .arena_base_y = 0, - .arena_w = ZUL_ARENA_SIZE, - .arena_h = ZUL_ARENA_SIZE, }, }; OsrsPlayerStepResult step_result = osrs_encounter_player_step(&step_input); @@ -2570,7 +2660,7 @@ static void zul_step_tick(ZulrahState* s, const int* actions) { zul_phase_tick(s); - zul_snakeling_tick(s); + zul_snakeling_tick(s, ctx); zul_thrall_tick(s); @@ -2593,10 +2683,15 @@ static void zul_step_tick(ZulrahState* s, const int* actions) { } static void zul_step(EncounterState* state, EncounterContext* context, const int* actions) { - (void)context; ZulrahState* s = (ZulrahState*)state; + ZulrahContext* ctx = (ZulrahContext*)context; + static ZulrahContext legacy_ctx; + if (!ctx) ctx = &legacy_ctx; + if (!ctx->route_topology) + zul_bind_route_topology( + ctx, s, (const CollisionMap*)s->collision_map); if (s->episode_over) return; - zul_step_tick(s, actions); + zul_step_tick(s, ctx, actions); s->reward = zul_compute_reward(s); s->episode_return += s->reward; } @@ -2890,10 +2985,15 @@ static void zul_put_float(EncounterState* st, EncounterContext* context, const c else encounter_abort_unknown_config("zulrah", "float", k); } static void zul_put_ptr(EncounterState* st, EncounterContext* context, const char* k, void* v) { - (void)context; ZulrahState* s = (ZulrahState*)st; - if (strcmp(k, "collision_map") == 0) s->collision_map = v; - else encounter_abort_unknown_config("zulrah", "ptr", k); + if (strcmp(k, "collision_map") == 0) { + ZulrahContext* ctx = (ZulrahContext*)context; + s->collision_map = v; + if (ctx) + zul_bind_route_topology(ctx, s, (const CollisionMap*)v); + } else { + encounter_abort_unknown_config("zulrah", "ptr", k); + } } static void* zul_get_log(EncounterState* state, EncounterContext* context) { @@ -3216,12 +3316,8 @@ static void zul_step_human_commands(EncounterState* state, EncounterContext* con human_input_clear_pending(hi); } -typedef struct { - int unused; -} ZulrahContext; - static void zul_init_context(EncounterContext* context) { - (void)context; + memset(context, 0, sizeof(ZulrahContext)); } static void zul_destroy_context(EncounterContext* context) { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc index 25face54f1..f7fb8dec54 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc @@ -699,6 +699,70 @@ static InfConfig inf_default_config(void) { }; } +typedef struct { + EncounterArenaTopology* topology; + const CollisionMap* collision_map; + int world_offset_x; + int world_offset_y; +} InfRouteTopologyOwner; + +typedef struct { + const CollisionMap* collision_map; + int world_offset_x; + int world_offset_y; +} InfRouteTopologyBuildContext; + +static InfRouteTopologyOwner inf_route_topology_owner; + +static uint32_t inf_route_topology_flags(void* data, int x, int y) { + const InfRouteTopologyBuildContext* build = + (const InfRouteTopologyBuildContext*)data; + if (!build->collision_map) return 0; + return (uint32_t)collision_get_flags( + build->collision_map, + 0, + x + build->world_offset_x, + y + build->world_offset_y); +} + +static void inf_bind_route_topology( + InfernoContext* ctx, + const CollisionMap* collision_map +) { + if (inf_route_topology_owner.topology) { + if (inf_route_topology_owner.collision_map != collision_map || + inf_route_topology_owner.world_offset_x != ctx->world_offset_x || + inf_route_topology_owner.world_offset_y != ctx->world_offset_y) { + fprintf(stderr, "inferno route topology geometry identity changed\n"); + abort(); + } + ctx->route_topology = inf_route_topology_owner.topology; + return; + } + InfRouteTopologyBuildContext build = { + collision_map, + ctx->world_offset_x, + ctx->world_offset_y, + }; + EncounterArenaTopologyBuildSpec spec = { + .origin_x = INF_ARENA_MIN_X, + .origin_y = INF_ARENA_MIN_Y, + .width = INF_ARENA_WIDTH, + .height = INF_ARENA_HEIGHT, + .max_footprint_size = ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE, + .revision = UINT64_C(0x494e4645524e4f01), + .tile_flags = inf_route_topology_flags, + .tile_flags_ctx = &build, + }; + inf_route_topology_owner.topology = + encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(inf_route_topology_owner.topology); + inf_route_topology_owner.collision_map = collision_map; + inf_route_topology_owner.world_offset_x = ctx->world_offset_x; + inf_route_topology_owner.world_offset_y = ctx->world_offset_y; + ctx->route_topology = inf_route_topology_owner.topology; +} + static void inf_init_context_typed(InfernoContext* ctx) { memset(ctx, 0, sizeof(*ctx)); ctx->config = inf_default_config(); @@ -718,6 +782,7 @@ static InfernoContext* inf_legacy_context(void) { static int initialized = 0; if (!initialized) { inf_init_context_typed(&ctx); + inf_bind_route_topology(&ctx, NULL); ctx.log = &log; initialized = 1; } @@ -739,7 +804,7 @@ static void inf_init_state(EncounterState* state, EncounterContext* context) { static EncounterState* inf_create(void) { InfernoState* s = (InfernoState*)calloc(1, sizeof(InfernoState)); - inf_init_state_typed(s, inf_legacy_context()); + inf_init_state_typed(s, NULL); return (EncounterState*)s; } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc index bac9704fda..9a8c9b58bc 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc @@ -698,6 +698,8 @@ typedef struct { typedef struct { InfConfig config; const CollisionMap* collision_map; + const EncounterArenaTopology* route_topology; + OsrsActorRouteCache player_route_cache; int world_offset_x; int world_offset_y; Log* log; @@ -909,3 +911,4 @@ typedef struct { float total_hp_restored_jad; float total_hp_restored_zuk; } InfernoState; +static_assert(sizeof(InfernoState) == 27300, "InfernoState serialized layout"); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc index 6c62221534..5203bd085a 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -122,6 +122,18 @@ static int inf_tile_walkable(void* data, int x, int y) { return 1; } +static int inf_player_route_blocked(void* data, int x, int y, int size) { + InfWalkCtx* walk = (InfWalkCtx*)data; + return inf_blocked_by_pillar(walk->s, x, y, size); +} + +static uint64_t inf_player_route_blocker_revision(const InfernoState* s) { + uint64_t revision = 1; + for (int pillar = 0; pillar < INF_NUM_PILLARS; pillar++) + revision |= (uint64_t)(s->pillars[pillar].active != 0) << (pillar + 1); + return revision; +} + typedef struct { InfernoState* s; InfernoContext* ctx; @@ -545,24 +557,26 @@ static void inf_tick_player_ctx( OsrsPlayerStepInput player_step_input = { .player = &s->player, .interaction = &s->interaction, + .route_cache = &ctx->player_route_cache, .target_lookup = inf_lookup_player_attack_target, .target_ctx = &target_ctx, .command = command, .dest_x = &s->player_dest_x, .dest_y = &s->player_dest_y, .arena = { + .topology = ctx->route_topology, + .blockers = { + .is_blocked = inf_player_route_blocked, + .ctx = &wc, + .revision = inf_player_route_blocker_revision(s), + }, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + .explicit_cost_policy = ENCOUNTER_ROUTE_COST_DIRECT, .collision_map = ctx->collision_map, .world_offset_x = ctx->world_offset_x, .world_offset_y = ctx->world_offset_y, - .is_walkable = inf_tile_walkable, - .walkable_ctx = &wc, - .extra_blocked = NULL, - .blocked_ctx = NULL, .los_query = &los_query, - .arena_base_x = INF_ARENA_MIN_X, - .arena_base_y = INF_ARENA_MIN_Y, - .arena_w = INF_ARENA_WIDTH, - .arena_h = INF_ARENA_HEIGHT, }, }; (void)osrs_encounter_player_step(&player_step_input); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index 2198e942c6..c7f358a07c 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -180,8 +180,12 @@ static void inf_put_ptr_ctx( ) { (void)state; InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); - if (strcmp(key, "collision_map") == 0) ctx->collision_map = (const CollisionMap*)value; - else encounter_abort_unknown_config("inferno", "ptr", key); + if (strcmp(key, "collision_map") == 0) { + inf_bind_route_topology(ctx, (const CollisionMap*)value); + ctx->collision_map = (const CollisionMap*)value; + } else { + encounter_abort_unknown_config("inferno", "ptr", key); + } } static int inf_get_tick_ctx(EncounterState* state, EncounterContext* context) { @@ -839,6 +843,9 @@ static uint64_t inf_config_fingerprint(const InfConfig* config) { #undef INF_HASH_CONFIG_FIELD static void inf_refresh_after_state_load(InfernoState* s, InfernoContext* ctx) { + osrs_interaction_zero_serialized_route_padding(&s->player.interaction); + osrs_interaction_zero_serialized_route_padding(&s->interaction); + osrs_actor_route_cache_clear(&ctx->player_route_cache); inf_build_npc_stats(); const uint8_t* const* loadouts = inf_loadouts_for_profile(s->active_loadout_profile); @@ -880,6 +887,8 @@ static void inf_snapshot_ctx(EncounterState* state, EncounterContext* context, v snap->config_fingerprint = inf_config_fingerprint(&ctx->config); encounter_snapshot_copy_state_to( snap, offsetof(InfSnapshot, state), state, sizeof(InfernoState)); + osrs_interaction_zero_serialized_route_padding(&snap->state.player.interaction); + osrs_interaction_zero_serialized_route_padding(&snap->state.interaction); } static void inf_snapshot(EncounterState* state, void* out) { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc index beb3125050..427ea37f8b 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -347,6 +347,7 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint InfernoState* s = (InfernoState*)state; InfernoContext* ctx = (InfernoContext*)context; if (!ctx) ctx = inf_legacy_context(); + osrs_actor_route_cache_clear(&ctx->player_route_cache); int saved_start = ctx->config.start_wave; uint32_t saved_rng = s->rng_state; memset(s, 0, sizeof(InfernoState)); diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index 76e34b3fe4..705a69cda9 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -799,77 +799,6 @@ static inline int encounter_move_to_target( return steps; } -static inline PathResult encounter_pathfind( - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - int src_x, int src_y, int dst_x, int dst_y, - pathfind_blocked_fn extra_blocked, void* blocked_ctx -) { - return pathfind_step(cmap, 0, - src_x + world_offset_x, src_y + world_offset_y, - dst_x + world_offset_x, dst_y + world_offset_y, - extra_blocked, blocked_ctx); -} - -static inline PathResult encounter_pathfind_arena( - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - int src_x, int src_y, int dst_x, int dst_y, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - int arena_base_x, int arena_base_y, int arena_w, int arena_h -) { - return pathfind_step_arena(cmap, 0, - src_x + world_offset_x, src_y + world_offset_y, - dst_x + world_offset_x, dst_y + world_offset_y, - extra_blocked, blocked_ctx, - arena_base_x + world_offset_x, arena_base_y + world_offset_y, - arena_w, arena_h); -} - -static inline int encounter_walk_toward( - Player* p, int tx, int ty, - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - encounter_walkable_fn is_walkable, void* ctx, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - int arena_base_x, int arena_base_y, int arena_w, int arena_h -) { - int steps = 0; - for (int step = 0; step < 2; step++) { - if (p->x == tx && p->y == ty) break; - PathResult pr = (arena_w > 0) - ? encounter_pathfind_arena(cmap, world_offset_x, world_offset_y, - p->x, p->y, tx, ty, - extra_blocked, blocked_ctx, - arena_base_x, arena_base_y, arena_w, arena_h) - : encounter_pathfind(cmap, world_offset_x, world_offset_y, - p->x, p->y, tx, ty, - extra_blocked, blocked_ctx); - if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) break; - int nx = p->x + pr.next_dx, ny = p->y + pr.next_dy; - if (!is_walkable(ctx, nx, ny)) break; - p->x = nx; p->y = ny; - steps++; - } - p->is_running = (steps == 2); - p->dest_x = p->x; p->dest_y = p->y; - return steps; -} - -static inline int encounter_move_toward_dest( - Player* p, int* dest_x, int* dest_y, - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - encounter_walkable_fn is_walkable, void* ctx, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - int arena_base_x, int arena_base_y, int arena_w, int arena_h -) { - if (*dest_x < 0 || *dest_y < 0) return 0; - if (p->x == *dest_x && p->y == *dest_y) { - *dest_x = -1; *dest_y = -1; - return 0; - } - return encounter_walk_toward(p, *dest_x, *dest_y, - cmap, world_offset_x, world_offset_y, - is_walkable, ctx, extra_blocked, blocked_ctx, - arena_base_x, arena_base_y, arena_w, arena_h); -} static inline int encounter_entity_footprint_cardinal_adjacent( int ax, int ay, int a_size, @@ -1137,844 +1066,6 @@ static inline int encounter_player_can_attack( } -static inline int encounter_target_rect_distance_squared( - int x, - int y, - int target_x, - int target_y, - int target_size -) { - int target_max_x = target_x + target_size - 1; - int target_max_y = target_y + target_size - 1; - int dx = x < target_x - ? target_x - x - : (x > target_max_x ? x - target_max_x : 0); - int dy = y < target_y - ? target_y - y - : (y > target_max_y ? y - target_max_y : 0); - return dx * dx + dy * dy; -} - - -typedef struct { - const CollisionMap* collision_map; - encounter_walkable_fn is_walkable; - void* walkable_ctx; - pathfind_blocked_fn extra_blocked; - void* blocked_ctx; - int world_offset_x; - int world_offset_y; - int arena_base_x; - int arena_base_y; - int arena_w; - int arena_h; - int source_x; - int source_y; - int target_edge_local_x; - int target_edge_local_y; - int min_explored_x; - int min_explored_y; - int max_explored_x; - int max_explored_y; - uint64_t visited[PATHFIND_ARENA_MAX]; - uint16_t depth[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - int8_t via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - int visited_count; -} EncounterArenaAttackRouteField; - -typedef struct { - int land_x; - int land_y; - int route_found; - int route_x; - int route_y; -} EncounterAttackRouteLanding; - - -static inline int encounter_attack_route_step_traversable( - const CollisionMap* cmap, - int world_offset_x, - int world_offset_y, - int x, - int y, - int dx, - int dy, - encounter_walkable_fn is_walkable, - void* walkable_ctx, - pathfind_blocked_fn extra_blocked, - void* blocked_ctx -) { - int next_x = x + dx; - int next_y = y + dy; - if (!is_walkable(walkable_ctx, next_x, next_y)) return 0; - if (dx != 0 && dy != 0 && - (!is_walkable(walkable_ctx, next_x, y) || - !is_walkable(walkable_ctx, x, next_y))) { - return 0; - } - if (!collision_traversable_step( - cmap, 0, x + world_offset_x, y + world_offset_y, dx, dy)) { - return 0; - } - if (extra_blocked && - extra_blocked( - blocked_ctx, - next_x + world_offset_x, - next_y + world_offset_y)) { - return 0; - } - if (dx == 0 || dy == 0) return 1; - if (extra_blocked && - (extra_blocked( - blocked_ctx, - next_x + world_offset_x, - y + world_offset_y) || - extra_blocked( - blocked_ctx, - x + world_offset_x, - next_y + world_offset_y))) { - return 0; - } - return 1; -} -typedef struct { - encounter_walkable_fn is_walkable; - void* walkable_ctx; - int arena_base_x; - int arena_base_y; - int arena_w; - int arena_h; - uint64_t known[PATHFIND_ARENA_MAX]; - uint64_t walkable[PATHFIND_ARENA_MAX]; -} EncounterAttackRouteWalkableCache; - -static inline int encounter_attack_route_cached_walkable( - void* data, - int x, - int y -) { - EncounterAttackRouteWalkableCache* cache = - (EncounterAttackRouteWalkableCache*)data; - int local_x = x - cache->arena_base_x; - int local_y = y - cache->arena_base_y; - if (local_x < 0 || local_x >= cache->arena_w || - local_y < 0 || local_y >= cache->arena_h) { - return cache->is_walkable(cache->walkable_ctx, x, y); - } - uint64_t bit = 1ULL << local_y; - if ((cache->known[local_x] & bit) == 0) { - cache->known[local_x] |= bit; - if (cache->is_walkable(cache->walkable_ctx, x, y)) - cache->walkable[local_x] |= bit; - } - return (cache->walkable[local_x] & bit) != 0; -} -static inline void encounter_attack_route_mark_target_edge( - uint64_t target_edges[PATHFIND_ARENA_MAX], - const CollisionMap* cmap, - int world_offset_x, - int world_offset_y, - int edge_x, - int edge_y, - int target_x, - int target_y, - int target_size, - int arena_base_x, - int arena_base_y, - int arena_w, - int arena_h -) { - int local_x = edge_x - arena_base_x; - int local_y = edge_y - arena_base_y; - if (local_x < 0 || local_x >= arena_w || - local_y < 0 || local_y >= arena_h) { - return; - } - if (encounter_entity_footprint_cardinal_reachable( - cmap, - world_offset_x, - world_offset_y, - edge_x, - edge_y, - target_x, - target_y, - target_size)) { - target_edges[local_x] |= 1ULL << local_y; - } -} - - - - -static inline void encounter_build_arena_attack_route_field( - EncounterArenaAttackRouteField* field, - const CollisionMap* cmap, - int world_offset_x, - int world_offset_y, - int source_x, - int source_y, - int target_x, - int target_y, - int target_size, - encounter_walkable_fn is_walkable, - void* walkable_ctx, - pathfind_blocked_fn extra_blocked, - void* blocked_ctx, - int arena_base_x, - int arena_base_y, - int arena_w, - int arena_h -) { - if (!field || !is_walkable || target_size <= 0) { - fprintf(stderr, "attack route field is missing required input\n"); - abort(); - } - if (arena_w <= 0 || arena_w > PATHFIND_ARENA_MAX || - arena_h <= 0 || arena_h > PATHFIND_ARENA_MAX) { - fprintf(stderr, "attack route arena dimensions out of bounds: %dx%d\n", - arena_w, arena_h); - abort(); - } - - int local_source_x = source_x - arena_base_x; - int local_source_y = source_y - arena_base_y; - if (local_source_x < 0 || local_source_x >= arena_w || - local_source_y < 0 || local_source_y >= arena_h) { - fprintf(stderr, - "attack route source out of arena: source=(%d,%d) arena=(%d,%d,%d,%d)\n", - source_x, source_y, arena_base_x, arena_base_y, arena_w, arena_h); - abort(); - } - - memset( - field->visited, - 0, - (size_t)arena_w * sizeof(field->visited[0])); - field->target_edge_local_x = -1; - field->target_edge_local_y = -1; - field->collision_map = cmap; - field->is_walkable = is_walkable; - field->walkable_ctx = walkable_ctx; - field->extra_blocked = extra_blocked; - field->blocked_ctx = blocked_ctx; - field->world_offset_x = world_offset_x; - field->world_offset_y = world_offset_y; - field->arena_base_x = arena_base_x; - field->arena_base_y = arena_base_y; - field->arena_w = arena_w; - field->arena_h = arena_h; - field->source_x = source_x; - field->source_y = source_y; - field->min_explored_x = local_source_x; - field->min_explored_y = local_source_y; - field->max_explored_x = local_source_x; - field->max_explored_y = local_source_y; - EncounterAttackRouteWalkableCache walkable_cache = { - .is_walkable = is_walkable, - .walkable_ctx = walkable_ctx, - .arena_base_x = arena_base_x, - .arena_base_y = arena_base_y, - .arena_w = arena_w, - .arena_h = arena_h, - }; - uint64_t target_edges[PATHFIND_ARENA_MAX] = {0}; - for (int offset = 0; offset < target_size; offset++) { - encounter_attack_route_mark_target_edge( - target_edges, cmap, world_offset_x, world_offset_y, - target_x - 1, target_y + offset, - target_x, target_y, target_size, - arena_base_x, arena_base_y, arena_w, arena_h); - encounter_attack_route_mark_target_edge( - target_edges, cmap, world_offset_x, world_offset_y, - target_x + target_size, target_y + offset, - target_x, target_y, target_size, - arena_base_x, arena_base_y, arena_w, arena_h); - encounter_attack_route_mark_target_edge( - target_edges, cmap, world_offset_x, world_offset_y, - target_x + offset, target_y - 1, - target_x, target_y, target_size, - arena_base_x, arena_base_y, arena_w, arena_h); - encounter_attack_route_mark_target_edge( - target_edges, cmap, world_offset_x, world_offset_y, - target_x + offset, target_y + target_size, - target_x, target_y, target_size, - arena_base_x, arena_base_y, arena_w, arena_h); - } - - - - - uint16_t queue[PATHFIND_MAX_QUEUE_ARENA]; - int head = 0; - field->visited_count = 1; - field->visited[local_source_x] = 1ULL << local_source_y; - field->depth[local_source_x][local_source_y] = 0; - field->via[local_source_x][local_source_y] = VIA_START; - queue[0] = (uint16_t)((local_source_x << 8) | local_source_y); - - static const int route_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int route_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; - static const int route_via[8] = { - VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE - }; - - while (head < field->visited_count) { - uint16_t packed_tile = queue[head++]; - int cur_x = packed_tile >> 8; - int cur_y = packed_tile & 0xff; - - int tile_x = arena_base_x + cur_x; - int tile_y = arena_base_y + cur_y; - if (target_edges[cur_x] & (1ULL << cur_y)) { - field->target_edge_local_x = cur_x; - field->target_edge_local_y = cur_y; - break; - } - uint16_t next_depth = field->depth[cur_x][cur_y] + 1; - - for (int i = 0; i < 8; i++) { - int dx = route_dx[i]; - int dy = route_dy[i]; - int next_x = cur_x + dx; - int next_y = cur_y + dy; - if (next_x < 0 || next_x >= arena_w || - next_y < 0 || next_y >= arena_h) { - continue; - } - if (field->visited[next_x] & (1ULL << next_y)) continue; - if (!encounter_attack_route_step_traversable( - cmap, - world_offset_x, - world_offset_y, - tile_x, - tile_y, - dx, - dy, - encounter_attack_route_cached_walkable, - &walkable_cache, - extra_blocked, - blocked_ctx)) { - continue; - } - - queue[field->visited_count] = - (uint16_t)((next_x << 8) | next_y); - field->visited_count++; - field->visited[next_x] |= 1ULL << next_y; - field->depth[next_x][next_y] = next_depth; - field->via[next_x][next_y] = (int8_t)route_via[i]; - if (next_x < field->min_explored_x) field->min_explored_x = next_x; - if (next_y < field->min_explored_y) field->min_explored_y = next_y; - if (next_x > field->max_explored_x) field->max_explored_x = next_x; - if (next_y > field->max_explored_y) field->max_explored_y = next_y; - } - } -} - -static inline EncounterAttackRouteLanding -encounter_attack_route_overlap_landing( - const EncounterArenaAttackRouteField* field, - int target_x, - int target_y, - int target_size -) { - Player player = { - .x = field->source_x, - .y = field->source_y, - }; - int max_r = (target_size + 1) / 2 + 1; - int best_dsq = 9999; - int best_x = -1; - int best_y = -1; - for (int dy = -max_r; dy <= max_r; dy++) { - for (int dx = -max_r; dx <= max_r; dx++) { - if (dx == 0 && dy == 0) continue; - int x = player.x + dx; - int y = player.y + dy; - if (!field->is_walkable(field->walkable_ctx, x, y)) continue; - if (encounter_entity_footprints_overlap( - x, y, 1, target_x, target_y, target_size)) { - continue; - } - int dsq = dx * dx + dy * dy; - if (dsq < best_dsq) { - best_dsq = dsq; - best_x = x; - best_y = y; - } - } - } - if (best_x >= 0) { - encounter_walk_toward( - &player, best_x, best_y, - field->collision_map, - field->world_offset_x, - field->world_offset_y, - field->is_walkable, - field->walkable_ctx, - field->extra_blocked, - field->blocked_ctx, - field->arena_base_x, - field->arena_base_y, - field->arena_w, - field->arena_h); - } - return (EncounterAttackRouteLanding){ - .land_x = player.x, - .land_y = player.y, - .route_found = player.x != field->source_x || - player.y != field->source_y, - .route_x = player.x, - .route_y = player.y, - }; -} - -static inline EncounterAttackRouteLanding encounter_arena_attack_route_landing( - const EncounterArenaAttackRouteField* field, - int target_x, - int target_y, - int target_size, - int attack_range, - const OsrsLosQuery* los_query -) { - if (!field || !field->is_walkable || - field->arena_w <= 0 || field->arena_w > PATHFIND_ARENA_MAX || - field->arena_h <= 0 || field->arena_h > PATHFIND_ARENA_MAX || - field->visited_count <= 0 || - target_size <= 0 || attack_range <= 0) { - fprintf(stderr, "invalid attack route landing query\n"); - abort(); - } - - EncounterAttackRouteLanding landing = { - .land_x = field->source_x, - .land_y = field->source_y, - .route_x = field->source_x, - .route_y = field->source_y, - }; - int dist = encounter_rect_distance( - field->source_x, field->source_y, 1, - target_x, target_y, target_size); - if (dist == 0) { - return encounter_attack_route_overlap_landing( - field, target_x, target_y, target_size); - } - if (encounter_player_can_attack( - field->source_x, field->source_y, - target_x, target_y, target_size, attack_range, - field->collision_map, field->world_offset_x, field->world_offset_y, - los_query)) { - landing.route_found = 1; - return landing; - } - - - int selected_x = field->target_edge_local_x; - int selected_y = field->target_edge_local_y; - - if (selected_x < 0) { - int target_local_x = target_x - field->arena_base_x; - int target_local_y = target_y - field->arena_base_y; - int scan_min_x = target_local_x - PATHFIND_MAX_FALLBACK_RADIUS; - int scan_min_y = target_local_y - PATHFIND_MAX_FALLBACK_RADIUS; - int scan_max_x = - target_local_x + target_size - 1 + PATHFIND_MAX_FALLBACK_RADIUS; - int scan_max_y = - target_local_y + target_size - 1 + PATHFIND_MAX_FALLBACK_RADIUS; - if (scan_min_x < field->min_explored_x) - scan_min_x = field->min_explored_x; - if (scan_min_y < field->min_explored_y) - scan_min_y = field->min_explored_y; - if (scan_max_x > field->max_explored_x) - scan_max_x = field->max_explored_x; - if (scan_max_y > field->max_explored_y) - scan_max_y = field->max_explored_y; - - int best_dsq = 0x3fffffff; - int best_depth = 100; - for (int x = scan_min_x; x <= scan_max_x; x++) { - for (int y = scan_min_y; y <= scan_max_y; y++) { - if ((field->visited[x] & (1ULL << y)) == 0) continue; - int tile_x = field->arena_base_x + x; - int tile_y = field->arena_base_y + y; - int depth = field->depth[x][y]; - if (depth >= 100) continue; - int dsq = encounter_target_rect_distance_squared( - tile_x, tile_y, target_x, target_y, target_size); - if (dsq < best_dsq || - (dsq == best_dsq && depth < best_depth)) { - selected_x = x; - selected_y = y; - best_dsq = dsq; - best_depth = depth; - } - } - } - } - if (selected_x < 0) return landing; - landing.route_found = 1; - landing.route_x = field->arena_base_x + selected_x; - landing.route_y = field->arena_base_y + selected_y; - - int source_local_x = field->source_x - field->arena_base_x; - int source_local_y = field->source_y - field->arena_base_y; - int first_x = source_local_x; - int first_y = source_local_y; - int second_x = source_local_x; - int second_y = source_local_y; - int cur_x = selected_x; - int cur_y = selected_y; - uint16_t selected_depth = field->depth[selected_x][selected_y]; - while (field->depth[cur_x][cur_y] > 0) { - uint16_t depth = field->depth[cur_x][cur_y]; - if (depth == 1) { - first_x = cur_x; - first_y = cur_y; - } else if (depth == 2) { - second_x = cur_x; - second_y = cur_y; - } - - int via = field->via[cur_x][cur_y]; - if (via == VIA_NONE || via == VIA_START) { - fprintf(stderr, - "broken attack route parent at local tile (%d,%d)\n", - cur_x, cur_y); - abort(); - } - if (via & VIA_W) cur_x++; - else if (via & VIA_E) cur_x--; - if (via & VIA_S) cur_y++; - else if (via & VIA_N) cur_y--; - if (cur_x < 0 || cur_x >= field->arena_w || - cur_y < 0 || cur_y >= field->arena_h) { - fprintf(stderr, "attack route parent left arena\n"); - abort(); - } - } - if (cur_x != source_local_x || cur_y != source_local_y) { - fprintf(stderr, "attack route did not terminate at source\n"); - abort(); - } - if (selected_depth == 0) return landing; - - landing.land_x = field->arena_base_x + first_x; - landing.land_y = field->arena_base_y + first_y; - if (encounter_player_can_attack( - landing.land_x, landing.land_y, - target_x, target_y, target_size, attack_range, - field->collision_map, field->world_offset_x, field->world_offset_y, - los_query)) { - return landing; - } - if (selected_depth >= 2) { - landing.land_x = field->arena_base_x + second_x; - landing.land_y = field->arena_base_y + second_y; - } - return landing; -} - -#define ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS 25 - -static inline int encounter_attack_route_compress_waypoints( - const EncounterArenaAttackRouteField* field, - const EncounterAttackRouteLanding* landing, - int waypoint_x[ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS], - int waypoint_y[ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS] -) { - if (!landing->route_found || - (landing->route_x == field->source_x && - landing->route_y == field->source_y)) { - return 0; - } - - int source_x = field->source_x - field->arena_base_x; - int source_y = field->source_y - field->arena_base_y; - int current_x = landing->route_x - field->arena_base_x; - int current_y = landing->route_y - field->arena_base_y; - int current_direction = -1; - int waypoint_count = 0; - - while (current_x != source_x || current_y != source_y) { - int next_direction = field->via[current_x][current_y]; - if (next_direction == VIA_NONE || next_direction == VIA_START) { - fprintf(stderr, "broken attack route during compression\n"); - abort(); - } - if (current_direction != next_direction) { - current_direction = next_direction; - if (waypoint_count == ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS) - waypoint_count--; - memmove( - &waypoint_x[1], - &waypoint_x[0], - (size_t)waypoint_count * sizeof(waypoint_x[0])); - memmove( - &waypoint_y[1], - &waypoint_y[0], - (size_t)waypoint_count * sizeof(waypoint_y[0])); - waypoint_x[0] = field->arena_base_x + current_x; - waypoint_y[0] = field->arena_base_y + current_y; - waypoint_count++; - } - - if (current_direction & VIA_W) current_x++; - else if (current_direction & VIA_E) current_x--; - if (current_direction & VIA_S) current_y++; - else if (current_direction & VIA_N) current_y--; - } - return waypoint_count; -} - -static inline PathResult encounter_pathfind_arena_attack_approach( - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - int src_x, int src_y, - int target_x, int target_y, int target_size, int attack_range, - encounter_walkable_fn is_walkable, void* ctx, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - const OsrsLosQuery* los_query, - int arena_base_x, int arena_base_y, int arena_w, int arena_h -) { - PathResult result = {0, 0, 0, src_x, src_y}; - int local_src_x = src_x - arena_base_x; - int local_src_y = src_y - arena_base_y; - if (local_src_x < 0 || local_src_x >= arena_w || - local_src_y < 0 || local_src_y >= arena_h) { - return result; - } - - EncounterArenaAttackRouteField field; - encounter_build_arena_attack_route_field( - &field, - cmap, - world_offset_x, - world_offset_y, - src_x, - src_y, - target_x, - target_y, - target_size, - is_walkable, - ctx, - extra_blocked, - blocked_ctx, - arena_base_x, - arena_base_y, - arena_w, - arena_h); - EncounterAttackRouteLanding landing = - encounter_arena_attack_route_landing( - &field, - target_x, - target_y, - target_size, - attack_range, - los_query); - - int landing_x = landing.land_x - arena_base_x; - int landing_y = landing.land_y - arena_base_y; - if (landing_x < 0 || landing_x >= arena_w || - landing_y < 0 || landing_y >= arena_h) { - return result; - } - uint16_t landing_depth = field.depth[landing_x][landing_y]; - if (landing_depth == 0) { - result.found = encounter_player_can_attack( - src_x, src_y, - target_x, target_y, target_size, attack_range, - cmap, world_offset_x, world_offset_y, - los_query); - return result; - } - if (landing_depth > 2) { - fprintf(stderr, "attack route landing exceeded one run tick: %u\n", - (unsigned)landing_depth); - abort(); - } - - result.found = 1; - result.dest_x = landing.land_x; - result.dest_y = landing.land_y; - if (landing_depth == 1) { - result.next_dx = landing.land_x - src_x; - result.next_dy = landing.land_y - src_y; - return result; - } - - int first_x = landing_x; - int first_y = landing_y; - int via = field.via[landing_x][landing_y]; - if (via & VIA_W) first_x++; - else if (via & VIA_E) first_x--; - if (via & VIA_S) first_y++; - else if (via & VIA_N) first_y--; - result.next_dx = arena_base_x + first_x - src_x; - result.next_dy = arena_base_y + first_y - src_y; - result.run_dx = landing_x - first_x; - result.run_dy = landing_y - first_y; - return result; -} - -static inline int encounter_chase_attack_target( - Player* p, int target_x, int target_y, int target_size, int attack_range, - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - encounter_walkable_fn is_walkable, void* ctx, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - const OsrsLosQuery* los_query, - int arena_base_x, int arena_base_y, int arena_w, int arena_h -) { - int dist = encounter_rect_distance(p->x, p->y, 1, - target_x, target_y, target_size); - - if (dist == 0) { - int max_r = (target_size + 1) / 2 + 1; - int best_dsq = 9999, bx = -1, by = -1; - for (int dy = -max_r; dy <= max_r; dy++) { - for (int dx = -max_r; dx <= max_r; dx++) { - if (dx == 0 && dy == 0) continue; - int nx = p->x + dx, ny = p->y + dy; - if (!is_walkable(ctx, nx, ny)) continue; - if (encounter_entity_footprints_overlap(nx, ny, 1, - target_x, target_y, target_size)) - continue; - int d = dx * dx + dy * dy; - if (d < best_dsq) { best_dsq = d; bx = nx; by = ny; } - } - } - if (bx < 0) return 0; - int steps = encounter_walk_toward(p, bx, by, - cmap, world_offset_x, world_offset_y, - is_walkable, ctx, extra_blocked, blocked_ctx, - arena_base_x, arena_base_y, arena_w, arena_h); - return steps > 0 ? 1 : 0; - } - if (encounter_player_can_attack( - p->x, p->y, - target_x, target_y, target_size, attack_range, - cmap, world_offset_x, world_offset_y, - los_query)) - return 0; - - - int cx, cy; - cx = -1; - cy = -1; - - if (arena_w <= 0) { - int scan_min_x = target_x - attack_range; - int scan_max_x = target_x + target_size - 1 + attack_range; - int scan_min_y = target_y - attack_range; - int scan_max_y = target_y + target_size - 1 + attack_range; - - cx = -1; - cy = -1; - int best_player_dsq = 0x3fffffff; - int best_target_dist = 0x3fffffff; - if (scan_min_x <= scan_max_x && scan_min_y <= scan_max_y) { - for (int yy = scan_min_y; yy <= scan_max_y; yy++) { - for (int xx = scan_min_x; xx <= scan_max_x; xx++) { - if (!is_walkable(ctx, xx, yy)) continue; - if (!encounter_player_can_attack(xx, yy, target_x, target_y, - target_size, attack_range, - cmap, world_offset_x, world_offset_y, - los_query)) - continue; - int dx = xx - p->x; - int dy = yy - p->y; - int player_dsq = dx * dx + dy * dy; - int target_dist = encounter_rect_distance( - xx, yy, 1, target_x, target_y, target_size); - if (player_dsq < best_player_dsq || - (player_dsq == best_player_dsq && - target_dist < best_target_dist)) { - best_player_dsq = player_dsq; - best_target_dist = target_dist; - cx = xx; - cy = yy; - } - } - } - } - - if (cx < 0) { - cx = p->x < target_x ? target_x : - (p->x > target_x + target_size - 1 ? target_x + target_size - 1 : p->x); - cy = p->y < target_y ? target_y : - (p->y > target_y + target_size - 1 ? target_y + target_size - 1 : p->y); - } - } - - int cached_run_dx = 0; - int cached_run_dy = 0; - int steps = 0; - for (int step = 0; step < 2; step++) { - if (encounter_player_can_attack(p->x, p->y, target_x, target_y, - target_size, attack_range, - cmap, world_offset_x, world_offset_y, - los_query)) - break; - int next_dx; - int next_dy; - int use_cached_run = step == 1 && arena_w > 0 && - (cached_run_dx != 0 || cached_run_dy != 0); - if (use_cached_run) { - int next_x = p->x + cached_run_dx; - int next_y = p->y + cached_run_dy; - if (!is_walkable(ctx, next_x, next_y) || - (extra_blocked && extra_blocked( - blocked_ctx, - next_x + world_offset_x, - next_y + world_offset_y))) { - use_cached_run = 0; - } else if (cached_run_dx != 0 && cached_run_dy != 0 && - (!is_walkable(ctx, next_x, p->y) || - !is_walkable(ctx, p->x, next_y) || - (extra_blocked && - (extra_blocked( - blocked_ctx, - next_x + world_offset_x, - p->y + world_offset_y) || - extra_blocked( - blocked_ctx, - p->x + world_offset_x, - next_y + world_offset_y))))) { - use_cached_run = 0; - } - } - if (use_cached_run) { - next_dx = cached_run_dx; - next_dy = cached_run_dy; - } else { - PathResult pr = (arena_w > 0) - ? encounter_pathfind_arena_attack_approach( - cmap, world_offset_x, world_offset_y, - p->x, p->y, - target_x, target_y, target_size, attack_range, - is_walkable, ctx, - extra_blocked, blocked_ctx, - los_query, - arena_base_x, arena_base_y, arena_w, arena_h) - : encounter_pathfind(cmap, world_offset_x, world_offset_y, - p->x, p->y, cx, cy, - extra_blocked, blocked_ctx); - if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) break; - next_dx = pr.next_dx; - next_dy = pr.next_dy; - if (step == 0) { - cached_run_dx = pr.run_dx; - cached_run_dy = pr.run_dy; - } - } - int nx = p->x + next_dx, ny = p->y + next_dy; - if (!is_walkable(ctx, nx, ny)) break; - p->x = nx; p->y = ny; - steps++; - } - p->is_running = (steps == 2); - p->dest_x = p->x; p->dest_y = p->y; - return steps > 0 ? 1 : 0; -} - typedef int (*encounter_npc_blocked_fn)(void* ctx, int x, int y, int size); typedef int (*encounter_npc_overlap_hold_fn)(void* ctx); diff --git a/ocean/osrs/osrs_encounter_player.h b/ocean/osrs/osrs_encounter_player.h index 5f4999a615..3428f3a9c3 100644 --- a/ocean/osrs/osrs_encounter_player.h +++ b/ocean/osrs/osrs_encounter_player.h @@ -33,18 +33,15 @@ typedef struct { } OsrsPlayerCommand; typedef struct { + const EncounterArenaTopology* topology; + EncounterRouteBlockers blockers; + EncounterRouteMovementMode movement_mode; + EncounterRouteCostPolicy cost_policy; + EncounterRouteCostPolicy explicit_cost_policy; const CollisionMap* collision_map; int world_offset_x; int world_offset_y; - encounter_walkable_fn is_walkable; - void* walkable_ctx; - pathfind_blocked_fn extra_blocked; - void* blocked_ctx; const OsrsLosQuery* los_query; - int arena_base_x; - int arena_base_y; - int arena_w; - int arena_h; } OsrsEncounterArena; typedef struct { @@ -63,6 +60,7 @@ typedef int (*OsrsAttackTargetLookupFn)( typedef struct { Player* player; OsrsInteraction* interaction; + OsrsActorRouteCache* route_cache; OsrsAttackTargetLookupFn target_lookup; void* target_ctx; OsrsPlayerCommand command; @@ -82,10 +80,12 @@ typedef struct { } OsrsPlayerStepResult; static inline void osrs_player_step_require_input(const OsrsPlayerStepInput* input) { - if (!input || !input->player || !input->interaction || !input->arena.is_walkable) { + if (!input || !input->player || !input->interaction || + !input->arena.topology) { fprintf(stderr, "osrs player step input is missing required fields\n"); abort(); } + encounter_arena_topology_require_finalized(input->arena.topology); if (input->command.kind == OSRS_PLAYER_CMD_MOVE && (input->command.move_kind == OSRS_PLAYER_MOVE_DESTINATION || input->command.move_kind == OSRS_PLAYER_MOVE_ACTION) && @@ -121,48 +121,86 @@ static inline int osrs_player_step_can_attack_target( input->arena.los_query); } +static inline int osrs_player_step_apply_route( + Player* player, + const EncounterRouteResult* route +) { + if ((route->outcome != ROUTE_REACHED_TARGET && + route->outcome != ROUTE_REACHED_FALLBACK) || + route->distance == 0) { + player->is_running = 0; + player->dest_x = player->x; + player->dest_y = player->y; + return 0; + } + player->x += route->first_dx; + player->y += route->first_dy; + int steps = 1; + if (route->run_dx != 0 || route->run_dy != 0) { + player->x += route->run_dx; + player->y += route->run_dy; + steps++; + } + player->is_running = steps == 2; + player->dest_x = player->x; + player->dest_y = player->y; + return steps; +} + static inline int osrs_player_step_apply_explicit_move( const OsrsPlayerStepInput* input, OsrsPlayerMoveKind move_kind ) { Player* player = input->player; + int target_x; + int target_y; if (move_kind == OSRS_PLAYER_MOVE_ACTION) { int move_action = input->command.move_action; if (move_action <= 0 || move_action >= ENCOUNTER_MOVE_ACTIONS) return 0; - return encounter_move_to_target( - player, - ENCOUNTER_MOVE_TARGET_DX[move_action], - ENCOUNTER_MOVE_TARGET_DY[move_action], - input->arena.is_walkable, - input->arena.walkable_ctx); - } - - if (move_kind != OSRS_PLAYER_MOVE_DESTINATION) + target_x = player->x + ENCOUNTER_MOVE_TARGET_DX[move_action]; + target_y = player->y + ENCOUNTER_MOVE_TARGET_DY[move_action]; + } else if (move_kind == OSRS_PLAYER_MOVE_DESTINATION) { + target_x = *input->dest_x; + target_y = *input->dest_y; + if (target_x < 0 || target_y < 0) return 0; + if (player->x == target_x && player->y == target_y) { + *input->dest_x = -1; + *input->dest_y = -1; + return 0; + } + } else { return 0; - return encounter_move_toward_dest( - player, - input->dest_x, - input->dest_y, - input->arena.collision_map, - input->arena.world_offset_x, - input->arena.world_offset_y, - input->arena.is_walkable, - input->arena.walkable_ctx, - input->arena.extra_blocked, - input->arena.blocked_ctx, - input->arena.arena_base_x, - input->arena.arena_base_y, - input->arena.arena_w, - input->arena.arena_h); + } + EncounterRouteInput route_input = { + .topology = input->arena.topology, + .blockers = input->arena.blockers, + .source_x = player->x, + .source_y = player->y, + .actor_size = 1, + .target_x = target_x, + .target_y = target_y, + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, + .movement_mode = input->arena.movement_mode, + .cost_policy = input->arena.explicit_cost_policy, + }; + EncounterRouteResult route = encounter_route_solve(&route_input); + return osrs_player_step_apply_route(player, &route); } static inline int osrs_player_step_route_matches( - const OsrsInteractionRoute* route, + const OsrsActorRouteCache* route, const Player* player, - const OsrsAttackTarget* target + const OsrsAttackTarget* target, + const OsrsEncounterArena* arena ) { return route->state != OSRS_INTERACTION_ROUTE_EMPTY && + route->topology_revision == arena->topology->revision && + route->blocker_revision == arena->blockers.revision && + route->actor_size == 1 && + route->movement_mode == arena->movement_mode && + route->cost_policy == arena->cost_policy && route->target_x == target->x && route->target_y == target->y && route->target_size == target->size && @@ -175,50 +213,49 @@ static inline void osrs_player_step_build_attack_route( const OsrsPlayerStepInput* input, const OsrsAttackTarget* target ) { - OsrsInteractionRoute* route = &input->interaction->route; + OsrsActorRouteCache* route = input->route_cache; + route->topology_revision = input->arena.topology->revision; + route->blocker_revision = input->arena.blockers.revision; + route->actor_size = 1; + route->movement_mode = input->arena.movement_mode; + route->cost_policy = input->arena.cost_policy; route->target_x = target->x; route->target_y = target->y; route->target_size = target->size; route->attack_range = target->attack_range; route->expected_player_x = input->player->x; + route->expected_player_y = input->player->y; route->planned_source_x = input->player->x; route->planned_source_y = input->player->y; - route->expected_player_y = input->player->y; route->waypoint_count = 0; route->waypoint_index = 0; - EncounterArenaAttackRouteField field; - encounter_build_arena_attack_route_field( - &field, - input->arena.collision_map, - input->arena.world_offset_x, - input->arena.world_offset_y, - input->player->x, - input->player->y, - target->x, - target->y, - target->size, - input->arena.is_walkable, - input->arena.walkable_ctx, - input->arena.extra_blocked, - input->arena.blocked_ctx, - input->arena.arena_base_x, - input->arena.arena_base_y, - input->arena.arena_w, - input->arena.arena_h); - EncounterAttackRouteLanding landing = - encounter_arena_attack_route_landing( - &field, - target->x, - target->y, - target->size, - target->attack_range, - input->arena.los_query); - route->waypoint_count = encounter_attack_route_compress_waypoints( - &field, - &landing, - route->waypoint_x, - route->waypoint_y); + EncounterRouteInput route_input = { + .topology = input->arena.topology, + .blockers = input->arena.blockers, + .source_x = input->player->x, + .source_y = input->player->y, + .actor_size = 1, + .target_x = target->x, + .target_y = target->y, + .target_size = target->size, + .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, + .movement_mode = input->arena.movement_mode, + .cost_policy = input->arena.cost_policy, + }; + EncounterRouteResult result = encounter_route_solve(&route_input); + if (result.outcome == ROUTE_REACHED_TARGET || + result.outcome == ROUTE_REACHED_FALLBACK) { + route->waypoint_count = result.waypoint_count; + memcpy( + route->waypoint_x, + result.waypoint_x, + (size_t)result.waypoint_count * sizeof(route->waypoint_x[0])); + memcpy( + route->waypoint_y, + result.waypoint_y, + (size_t)result.waypoint_count * sizeof(route->waypoint_y[0])); + } route->state = route->waypoint_count > 0 ? OSRS_INTERACTION_ROUTE_READY : OSRS_INTERACTION_ROUTE_FAILED; @@ -227,7 +264,7 @@ static inline void osrs_player_step_build_attack_route( static inline int osrs_player_step_route_next_traversable( const OsrsPlayerStepInput* input ) { - const OsrsInteractionRoute* route = &input->interaction->route; + const OsrsActorRouteCache* route = input->route_cache; if (route->state != OSRS_INTERACTION_ROUTE_READY || route->waypoint_index >= route->waypoint_count) { return 0; @@ -238,18 +275,21 @@ static inline int osrs_player_step_route_next_traversable( (waypoint_x < input->player->x); int dy = (waypoint_y > input->player->y) - (waypoint_y < input->player->y); - return encounter_attack_route_step_traversable( - input->arena.collision_map, - input->arena.world_offset_x, - input->arena.world_offset_y, - input->player->x, - input->player->y, - dx, - dy, - input->arena.is_walkable, - input->arena.walkable_ctx, - input->arena.extra_blocked, - input->arena.blocked_ctx); + EncounterRouteInput route_input = { + .topology = input->arena.topology, + .blockers = input->arena.blockers, + .source_x = input->player->x, + .source_y = input->player->y, + .actor_size = 1, + .target_x = waypoint_x, + .target_y = waypoint_y, + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, + .movement_mode = input->arena.movement_mode, + .cost_policy = input->arena.cost_policy, + }; + return encounter_route_step_allowed( + &route_input, input->player->x, input->player->y, dx, dy); } static inline int osrs_player_step_chase_target( @@ -257,38 +297,22 @@ static inline int osrs_player_step_chase_target( const OsrsAttackTarget* target ) { Player* player = input->player; - OsrsInteractionRoute* route = &input->interaction->route; + OsrsActorRouteCache* route = input->route_cache; int distance = encounter_rect_distance( player->x, player->y, 1, target->x, target->y, target->size); - if (input->arena.arena_w <= 0 || distance == 0) { - osrs_interaction_route_clear(input->interaction); - return encounter_chase_attack_target( - player, - target->x, - target->y, - target->size, - target->attack_range, - input->arena.collision_map, - input->arena.world_offset_x, - input->arena.world_offset_y, - input->arena.is_walkable, - input->arena.walkable_ctx, - input->arena.extra_blocked, - input->arena.blocked_ctx, - input->arena.los_query, - input->arena.arena_base_x, - input->arena.arena_base_y, - input->arena.arena_w, - input->arena.arena_h); + if (distance == 0) { + osrs_actor_route_cache_clear(route); + osrs_player_step_build_attack_route(input, target); } if (osrs_player_step_can_attack_target(input, target)) { - osrs_interaction_route_clear(input->interaction); + osrs_actor_route_cache_clear(route); return 0; } int remaining_waypoints = route->waypoint_count - route->waypoint_index; - int reroute = !osrs_player_step_route_matches(route, player, target) || + int reroute = + !osrs_player_step_route_matches(route, player, target, &input->arena) || route->state == OSRS_INTERACTION_ROUTE_FAILED || remaining_waypoints <= 1; if (!reroute && !osrs_player_step_route_next_traversable(input)) @@ -313,7 +337,7 @@ static inline int osrs_player_step_chase_target( continue; } if (!osrs_player_step_route_next_traversable(input)) { - osrs_interaction_route_clear(input->interaction); + osrs_actor_route_cache_clear(route); break; } player->x += (waypoint_x > player->x) - (waypoint_x < player->x); @@ -334,6 +358,10 @@ static inline OsrsPlayerStepResult osrs_encounter_player_step( const OsrsPlayerStepInput* input ) { osrs_player_step_require_input(input); + if (!input->route_cache) { + fprintf(stderr, "OSRS player step missing actor route cache\n"); + abort(); + } OsrsPlayerStepResult result = { .target_slot = -1, diff --git a/ocean/osrs/osrs_interaction.h b/ocean/osrs/osrs_interaction.h index 79f72e8cbd..b6d77c89b8 100644 --- a/ocean/osrs/osrs_interaction.h +++ b/ocean/osrs/osrs_interaction.h @@ -1,6 +1,11 @@ #ifndef OSRS_INTERACTION_H #define OSRS_INTERACTION_H +#include +#include +#include +#include + #define OSRS_INTERACTION_ROUTE_MAX_WAYPOINTS 25 typedef enum { @@ -11,6 +16,11 @@ typedef enum { typedef struct { OsrsInteractionRouteState state; + uint64_t topology_revision; + uint64_t blocker_revision; + int actor_size; + uint8_t movement_mode; + uint8_t cost_policy; int target_x; int target_y; int target_size; @@ -23,37 +33,47 @@ typedef struct { int waypoint_index; int waypoint_x[OSRS_INTERACTION_ROUTE_MAX_WAYPOINTS]; int waypoint_y[OSRS_INTERACTION_ROUTE_MAX_WAYPOINTS]; -} OsrsInteractionRoute; +} OsrsActorRouteCache; + +#define OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES 244 typedef struct { int target_slot; - OsrsInteractionRoute route; + uint8_t serialized_route_padding[OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES]; } OsrsInteraction; -static inline void osrs_interaction_route_clear(OsrsInteraction* ix) { - ix->route.state = OSRS_INTERACTION_ROUTE_EMPTY; - ix->route.waypoint_count = 0; - ix->route.waypoint_index = 0; +static_assert(sizeof(OsrsInteraction) == 248, "OsrsInteraction serialized layout"); +static_assert(offsetof(OsrsInteraction, target_slot) == 0, "OsrsInteraction target offset"); +static_assert(offsetof(OsrsInteraction, serialized_route_padding) == 4, + "OsrsInteraction reserved route offset"); + +static inline void osrs_interaction_zero_serialized_route_padding( + OsrsInteraction* ix +) { + memset(ix->serialized_route_padding, 0, sizeof(ix->serialized_route_padding)); +} + +static inline void osrs_actor_route_cache_clear(OsrsActorRouteCache* route) { + route->state = OSRS_INTERACTION_ROUTE_EMPTY; + route->waypoint_count = 0; + route->waypoint_index = 0; } static inline void osrs_interaction_set(OsrsInteraction* ix, int target_slot) { if (ix->target_slot == target_slot) return; ix->target_slot = target_slot; - osrs_interaction_route_clear(ix); } static inline void osrs_interaction_clear(OsrsInteraction* ix) { ix->target_slot = -1; - osrs_interaction_route_clear(ix); } - static inline int osrs_interaction_active(const OsrsInteraction* ix) { return ix->target_slot >= 0; } static inline void osrs_interaction_init(OsrsInteraction* ix) { ix->target_slot = -1; - osrs_interaction_route_clear(ix); + osrs_interaction_zero_serialized_route_padding(ix); } #define OSRS_IACT_NONE 0 diff --git a/ocean/osrs/osrs_pathfinding.h b/ocean/osrs/osrs_pathfinding.h index 936bde78f3..b444785992 100644 --- a/ocean/osrs/osrs_pathfinding.h +++ b/ocean/osrs/osrs_pathfinding.h @@ -20,6 +20,11 @@ #define PATHFIND_MAX_QUEUE_ARENA (PATHFIND_ARENA_MAX * PATHFIND_ARENA_MAX) #define PATHFIND_MAX_FALLBACK_RADIUS 10 +#if defined(__GNUC__) || defined(__clang__) +#define OSRS_ROUTE_NOINLINE __attribute__((noinline)) +#else +#define OSRS_ROUTE_NOINLINE +#endif #define VIA_NONE 0 #define VIA_S 1 #define VIA_W 2 @@ -31,343 +36,1507 @@ #define VIA_NE 12 #define VIA_START 99 + +#define ENCOUNTER_ROUTE_MAX_WAYPOINTS 25 + +typedef enum { + ROUTE_REACHED_TARGET = 0, + ROUTE_REACHED_FALLBACK, + ROUTE_UNREACHABLE, + ROUTE_INVALID_INPUT, +} EncounterRouteOutcome; + +typedef enum { + ENCOUNTER_ROUTE_TARGET_TILE = 0, + ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, +} EncounterRouteTargetKind; + +typedef enum { + ENCOUNTER_ROUTE_MOVEMENT_WALK = 0, + ENCOUNTER_ROUTE_MOVEMENT_RUN, +} EncounterRouteMovementMode; + +typedef enum { + ENCOUNTER_ROUTE_COST_OSRS = 0, + ENCOUNTER_ROUTE_COST_SOUTH_FIRST, + ENCOUNTER_ROUTE_COST_DIRECT, + ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + ENCOUNTER_ROUTE_COST_SOUTH_FIRST_REVERSE, +} EncounterRouteCostPolicy; + +typedef int (*encounter_route_blocked_fn)( + void* ctx, + int x, + int y, + int size); + +typedef struct { + encounter_route_blocked_fn is_blocked; + void* ctx; + uint64_t revision; +} EncounterRouteBlockers; + +typedef struct { + const EncounterArenaTopology* topology; + EncounterRouteBlockers blockers; + int source_x; + int source_y; + int actor_size; + int target_x; + int target_y; + int target_size; + EncounterRouteTargetKind target_kind; + EncounterRouteMovementMode movement_mode; + EncounterRouteCostPolicy cost_policy; +} EncounterRouteInput; + typedef struct { - int found; - int next_dx; - int next_dy; - int dest_x; - int dest_y; + EncounterRouteOutcome outcome; + int destination_x; + int destination_y; + int first_dx; + int first_dy; int run_dx; int run_dy; -} PathResult; + uint16_t distance; + uint8_t waypoint_count; + int waypoint_x[ENCOUNTER_ROUTE_MAX_WAYPOINTS]; + int waypoint_y[ENCOUNTER_ROUTE_MAX_WAYPOINTS]; +} EncounterRouteResult; -typedef int (*pathfind_blocked_fn)(void* ctx, int abs_x, int abs_y); +typedef struct { + const EncounterArenaTopology* topology; + void* blocker_ctx; + encounter_route_blocked_fn blocker; + uint64_t topology_revision; + uint64_t blocker_revision; + int source_x; + int source_y; + uint16_t visited_count; + uint16_t expanded_count; + uint16_t depth[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint16_t queue[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + int8_t via[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint64_t visited[ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION]; + uint64_t blocker_known[ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION]; + uint64_t blocker_value[ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION]; + uint8_t actor_size; + uint8_t movement_mode; + uint8_t cost_policy; + uint8_t valid; +} EncounterSourceRouteField; + +typedef struct { + uint16_t depth[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint8_t outcome; +} EncounterReverseRouteField; -static inline void pathfind_enqueue_or_abort( - int* queue_x, int* queue_y, int* tail, int capacity, int x, int y +typedef struct { + uint16_t generation[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint16_t queue[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint16_t depth[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + int8_t via[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint16_t blocker_generation[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint8_t blocker_value[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint16_t current_generation; + EncounterSourceRouteField source_field; + EncounterReverseRouteField reverse_field; +} EncounterRouteScratch; + +static OSRS_THREAD_LOCAL EncounterRouteScratch encounter_route_scratch; + +static inline int encounter_route_abs(int value) { + return value < 0 ? -value : value; +} + +static inline int encounter_route_dynamic_blocked( + const EncounterRouteInput* input, + int x, + int y ) { - if (*tail >= capacity) { - fprintf(stderr, "pathfind queue overflow: capacity=%d\n", capacity); - abort(); + return input->blockers.is_blocked && + input->blockers.is_blocked( + input->blockers.ctx, x, y, input->actor_size); +} + +static inline int encounter_route_step_allowed( + const EncounterRouteInput* input, + int x, + int y, + int dx, + int dy +) { + if (!encounter_arena_topology_step_allowed( + input->topology, x, y, input->actor_size, dx, dy)) + return 0; + if (encounter_route_dynamic_blocked(input, x + dx, y + dy)) + return 0; + if (dx != 0 && dy != 0 && + (encounter_route_dynamic_blocked(input, x + dx, y) || + encounter_route_dynamic_blocked(input, x, y + dy))) + return 0; + return 1; +} +static inline int encounter_route_dynamic_blocked_cached( + const EncounterRouteInput* input, + EncounterRouteScratch* scratch, + uint16_t generation, + int x, + int y +) { + if (!input->blockers.is_blocked) return 0; + if (!encounter_arena_topology_contains(input->topology, x, y)) + return encounter_route_dynamic_blocked(input, x, y); + int index = encounter_arena_topology_index_raw(input->topology, x, y); + if (scratch->blocker_generation[index] != generation) { + scratch->blocker_generation[index] = generation; + scratch->blocker_value[index] = + (uint8_t)encounter_route_dynamic_blocked(input, x, y); } + return scratch->blocker_value[index]; +} - queue_x[*tail] = x; - queue_y[*tail] = y; - (*tail)++; +static inline int encounter_route_step_allowed_cached( + const EncounterRouteInput* input, + EncounterRouteScratch* scratch, + uint16_t generation, + int x, + int y, + int dx, + int dy +) { + if (encounter_route_dynamic_blocked_cached( + input, scratch, generation, x + dx, y + dy)) + return 0; + if (dx != 0 && dy != 0 && + (encounter_route_dynamic_blocked_cached( + input, scratch, generation, x, y + dy) || + encounter_route_dynamic_blocked_cached( + input, scratch, generation, x + dx, y))) + return 0; + if (!encounter_arena_topology_step_allowed( + input->topology, x, y, input->actor_size, dx, dy)) + return 0; + return 1; } -static const int pathfind_dir_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; -static const int pathfind_dir_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; -static const int pathfind_dir_via[8] = { - VIA_S, VIA_W, VIA_N, VIA_E, VIA_SW, VIA_NW, VIA_SE, VIA_NE -}; -static inline PathResult pathfind_step(const CollisionMap* map, int height, - int src_x, int src_y, int dest_x, int dest_y, - pathfind_blocked_fn extra_blocked, void* blocked_ctx) { - PathResult result = {0, 0, 0, dest_x, dest_y}; +static inline int encounter_route_footprints_overlap( + int actor_x, + int actor_y, + int actor_size, + int target_x, + int target_y, + int target_size +) { + return + actor_x < target_x + target_size && + target_x < actor_x + actor_size && + actor_y < target_y + target_size && + target_y < actor_y + actor_size; +} - if (src_x == dest_x && src_y == dest_y) { - result.found = 1; - return result; - } +static inline int encounter_route_footprints_cardinal_adjacent( + int actor_x, + int actor_y, + int actor_size, + int target_x, + int target_y, + int target_size +) { + int64_t actor_max_x = (int64_t)actor_x + actor_size - 1; + int64_t actor_max_y = (int64_t)actor_y + actor_size - 1; + int64_t target_max_x = (int64_t)target_x + target_size - 1; + int64_t target_max_y = (int64_t)target_y + target_size - 1; + int x_overlap = + (int64_t)actor_x <= target_max_x && + (int64_t)target_x <= actor_max_x; + int y_overlap = + (int64_t)actor_y <= target_max_y && + (int64_t)target_y <= actor_max_y; + return + (actor_max_x + 1 == target_x && y_overlap) || + (target_max_x + 1 == actor_x && y_overlap) || + (actor_max_y + 1 == target_y && x_overlap) || + (target_max_y + 1 == actor_y && x_overlap); +} - int dist = abs(src_x - dest_x); - int dy_abs = abs(src_y - dest_y); - if (dy_abs > dist) dist = dy_abs; - if (dist > 64) { - return result; +static inline int encounter_route_cardinal_edge_open( + const EncounterRouteInput* input, + int actor_x, + int actor_y +) { + const EncounterArenaTopology* topology = input->topology; + int actor_max_x = actor_x + input->actor_size - 1; + int actor_max_y = actor_y + input->actor_size - 1; + int target_max_x = input->target_x + input->target_size - 1; + int target_max_y = input->target_y + input->target_size - 1; + if (actor_max_x + 1 == input->target_x) { + int min_y = actor_y > input->target_y ? actor_y : input->target_y; + int max_y = actor_max_y < target_max_y ? actor_max_y : target_max_y; + for (int y = min_y; y <= max_y; y++) { + int index = encounter_arena_topology_index_raw(topology, actor_max_x, y); + if ((topology->static_collision_flags[index] & COLLISION_WALL_EAST) == 0) + return 1; + } + return 0; + } + if (target_max_x + 1 == actor_x) { + int min_y = actor_y > input->target_y ? actor_y : input->target_y; + int max_y = actor_max_y < target_max_y ? actor_max_y : target_max_y; + for (int y = min_y; y <= max_y; y++) { + int index = encounter_arena_topology_index_raw(topology, actor_x, y); + if ((topology->static_collision_flags[index] & COLLISION_WALL_WEST) == 0) + return 1; + } + return 0; + } + if (actor_max_y + 1 == input->target_y) { + int min_x = actor_x > input->target_x ? actor_x : input->target_x; + int max_x = actor_max_x < target_max_x ? actor_max_x : target_max_x; + for (int x = min_x; x <= max_x; x++) { + int index = encounter_arena_topology_index_raw(topology, x, actor_max_y); + if ((topology->static_collision_flags[index] & COLLISION_WALL_NORTH) == 0) + return 1; + } + return 0; } + if (target_max_y + 1 == actor_y) { + int min_x = actor_x > input->target_x ? actor_x : input->target_x; + int max_x = actor_max_x < target_max_x ? actor_max_x : target_max_x; + for (int x = min_x; x <= max_x; x++) { + int index = encounter_arena_topology_index_raw(topology, x, actor_y); + if ((topology->static_collision_flags[index] & COLLISION_WALL_SOUTH) == 0) + return 1; + } + return 0; + } + return 0; +} - int origin_x = ((src_x >> 3) - 6) << 3; - int origin_y = ((src_y >> 3) - 6) << 3; +static inline int encounter_route_is_target( + const EncounterRouteInput* input, + int x, + int y +) { + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE) + return x == input->target_x && y == input->target_y; + return encounter_route_footprints_cardinal_adjacent( + x, y, input->actor_size, + input->target_x, input->target_y, input->target_size) && + encounter_route_cardinal_edge_open(input, x, y); +} - int local_src_x = src_x - origin_x; - int local_src_y = src_y - origin_y; - int local_dest_x = dest_x - origin_x; - int local_dest_y = dest_y - origin_y; +static inline int encounter_route_target_distance_squared( + const EncounterRouteInput* input, + int x, + int y +) { + int64_t target_max_x = (int64_t)input->target_x + input->target_size - 1; + int64_t target_max_y = (int64_t)input->target_y + input->target_size - 1; + int64_t actor_max_x = (int64_t)x + input->actor_size - 1; + int64_t actor_max_y = (int64_t)y + input->actor_size - 1; + int64_t dx = 0; + int64_t dy = 0; + if (actor_max_x < input->target_x) dx = input->target_x - actor_max_x; + else if (target_max_x < x) dx = (int64_t)x - target_max_x; + if (actor_max_y < input->target_y) dy = input->target_y - actor_max_y; + else if (target_max_y < y) dy = (int64_t)y - target_max_y; + int64_t squared = dx * dx + dy * dy; + return squared > INT_MAX ? INT_MAX : (int)squared; +} - if (local_dest_x < 0 || local_dest_x >= PATHFIND_GRID_SIZE || - local_dest_y < 0 || local_dest_y >= PATHFIND_GRID_SIZE) { - return result; - } +static inline int encounter_route_input_valid( + const EncounterRouteInput* input +) { + if (!input || !input->topology || !input->topology->finalized) return 0; + if (input->actor_size < 1 || + input->actor_size > input->topology->max_footprint_size || + input->target_size < 1) + return 0; + if (input->target_kind < ENCOUNTER_ROUTE_TARGET_TILE || + input->target_kind > ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY) + return 0; + if (input->movement_mode < ENCOUNTER_ROUTE_MOVEMENT_WALK || + input->movement_mode > ENCOUNTER_ROUTE_MOVEMENT_RUN) + return 0; + if (input->cost_policy < ENCOUNTER_ROUTE_COST_OSRS || + input->cost_policy > ENCOUNTER_ROUTE_COST_SOUTH_FIRST_REVERSE) + return 0; + if (input->blockers.is_blocked && input->blockers.revision == 0) return 0; + if (encounter_arena_topology_footprint_blocked( + input->topology, + input->source_x, + input->source_y, + input->actor_size)) + return 0; + return 1; +} - int via[PATHFIND_GRID_SIZE][PATHFIND_GRID_SIZE]; - int cost[PATHFIND_GRID_SIZE][PATHFIND_GRID_SIZE]; - memset(via, 0, sizeof(via)); - memset(cost, 0, sizeof(cost)); +static inline void encounter_route_parent( + const EncounterArenaTopology* topology, + int via, + int* x, + int* y +) { + if (via == VIA_NONE || via == VIA_START) { + fprintf(stderr, "broken OSRS route parent at (%d,%d)\n", *x, *y); + abort(); + } + if (via & VIA_W) (*x)++; + else if (via & VIA_E) (*x)--; + if (via & VIA_S) (*y)++; + else if (via & VIA_N) (*y)--; + if (*x < 0 || *x >= topology->width || + *y < 0 || *y >= topology->height) { + fprintf(stderr, "OSRS route parent left topology\n"); + abort(); + } +} - int queue_x[PATHFIND_MAX_QUEUE_FULL]; - int queue_y[PATHFIND_MAX_QUEUE_FULL]; - int head = 0; - int tail = 0; +static inline void encounter_route_build_result_path( + const EncounterRouteInput* input, + EncounterRouteResult* result, + int destination_x, + int destination_y, + uint16_t generation +) { + EncounterRouteScratch* scratch = &encounter_route_scratch; + const EncounterArenaTopology* topology = input->topology; + int source_x = input->source_x - topology->origin_x; + int source_y = input->source_y - topology->origin_y; + int current_x = destination_x; + int current_y = destination_y; + int destination_index = current_x * topology->height + current_y; + uint16_t distance = scratch->depth[destination_index]; + result->distance = distance; + result->destination_x = topology->origin_x + destination_x; + result->destination_y = topology->origin_y + destination_y; - via[local_src_x][local_src_y] = VIA_START; - cost[local_src_x][local_src_y] = 1; - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_FULL, local_src_x, local_src_y); + int first_x = source_x; + int first_y = source_y; + int second_x = source_x; + int second_y = source_y; + int direction = -1; + while (current_x != source_x || current_y != source_y) { + int index = current_x * topology->height + current_y; + if (scratch->generation[index] != generation) { + fprintf(stderr, "OSRS route read unstamped parent\n"); + abort(); + } + uint16_t depth = scratch->depth[index]; + if (depth == 1) { + first_x = current_x; + first_y = current_y; + } else if (depth == 2) { + second_x = current_x; + second_y = current_y; + } + int next_direction = scratch->via[index]; + if (direction != next_direction) { + direction = next_direction; + int count = result->waypoint_count; + if (count == ENCOUNTER_ROUTE_MAX_WAYPOINTS) count--; + memmove( + &result->waypoint_x[1], + &result->waypoint_x[0], + (size_t)count * sizeof(result->waypoint_x[0])); + memmove( + &result->waypoint_y[1], + &result->waypoint_y[0], + (size_t)count * sizeof(result->waypoint_y[0])); + result->waypoint_x[0] = topology->origin_x + current_x; + result->waypoint_y[0] = topology->origin_y + current_y; + result->waypoint_count = (uint8_t)(count + 1); + } - int found_path = 0; - int cur_x, cur_y; + encounter_route_parent(topology, next_direction, ¤t_x, ¤t_y); + } + if (distance == 0) return; + result->first_dx = first_x - source_x; + result->first_dy = first_y - source_y; + if (distance >= 2 && + input->movement_mode == ENCOUNTER_ROUTE_MOVEMENT_RUN) { + result->run_dx = second_x - first_x; + result->run_dy = second_y - first_y; + } +} +static inline int encounter_route_destination_allowed( + const EncounterRouteInput* input, + int x, + int y +) { + return + !encounter_arena_topology_footprint_blocked( + input->topology, x, y, input->actor_size) && + !encounter_route_dynamic_blocked(input, x, y); +} - while (head < tail) { - cur_x = queue_x[head]; - cur_y = queue_y[head]; - head++; +static inline void encounter_route_direct_parts( + const EncounterRouteInput* input, + int destination_x, + int destination_y, + int* cardinal_x, + int* cardinal_y, + int* cardinal_count, + int* diagonal_x, + int* diagonal_y, + int* diagonal_count +) { + int dx = destination_x - input->source_x; + int dy = destination_y - input->source_y; + int abs_dx = encounter_route_abs(dx); + int abs_dy = encounter_route_abs(dy); + *diagonal_x = (dx > 0) - (dx < 0); + *diagonal_y = (dy > 0) - (dy < 0); + *cardinal_x = abs_dx > abs_dy ? *diagonal_x : 0; + *cardinal_y = abs_dy > abs_dx ? *diagonal_y : 0; + *cardinal_count = encounter_route_abs(abs_dx - abs_dy); + *diagonal_count = abs_dx < abs_dy ? abs_dx : abs_dy; +} - if (cur_x == local_dest_x && cur_y == local_dest_y) { - found_path = 1; - break; - } +static inline int encounter_route_direction_rank( + const EncounterRouteInput* input, + int dx, + int dy +) { + static const int osrs_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int osrs_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; + static const int south_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; + static const int south_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; + const int* rank_dx = + input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS ? osrs_dx : south_dx; + const int* rank_dy = + input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS ? osrs_dy : south_dy; + for (int rank = 0; rank < 8; rank++) { + if (rank_dx[rank] == dx && rank_dy[rank] == dy) return rank; + } + abort(); +} - int abs_x = origin_x + cur_x; - int abs_y = origin_y + cur_y; - int next_cost = cost[cur_x][cur_y] + 1; +static inline int encounter_route_direct_candidate_before( + const EncounterRouteInput* input, + int candidate_x, + int candidate_y, + int selected_x, + int selected_y +) { + if (selected_x == INT_MIN) return 1; + int candidate_cardinal_x, candidate_cardinal_y, candidate_cardinal_count; + int candidate_diagonal_x, candidate_diagonal_y, candidate_diagonal_count; + int selected_cardinal_x, selected_cardinal_y, selected_cardinal_count; + int selected_diagonal_x, selected_diagonal_y, selected_diagonal_count; + encounter_route_direct_parts( + input, candidate_x, candidate_y, + &candidate_cardinal_x, &candidate_cardinal_y, + &candidate_cardinal_count, + &candidate_diagonal_x, &candidate_diagonal_y, + &candidate_diagonal_count); + encounter_route_direct_parts( + input, selected_x, selected_y, + &selected_cardinal_x, &selected_cardinal_y, + &selected_cardinal_count, + &selected_diagonal_x, &selected_diagonal_y, + &selected_diagonal_count); + int candidate_distance = candidate_cardinal_count + candidate_diagonal_count; + int selected_distance = selected_cardinal_count + selected_diagonal_count; + if (candidate_distance != selected_distance) + return candidate_distance < selected_distance; + for (int step = 0; step < candidate_distance; step++) { + int candidate_rank = step < candidate_cardinal_count + ? encounter_route_direction_rank( + input, candidate_cardinal_x, candidate_cardinal_y) + : encounter_route_direction_rank( + input, candidate_diagonal_x, candidate_diagonal_y); + int selected_rank = step < selected_cardinal_count + ? encounter_route_direction_rank( + input, selected_cardinal_x, selected_cardinal_y) + : encounter_route_direction_rank( + input, selected_diagonal_x, selected_diagonal_y); + if (candidate_rank != selected_rank) + return candidate_rank < selected_rank; + } + return 0; +} - #define EB(ax, ay) (extra_blocked && extra_blocked(blocked_ctx, (ax), (ay))) +static inline int encounter_route_try_direct_destination( + const EncounterRouteInput* input, + int destination_x, + int destination_y, + EncounterRouteResult* result +) { + int cardinal_x, cardinal_y, cardinal_count; + int diagonal_x, diagonal_y, diagonal_count; + encounter_route_direct_parts( + input, destination_x, destination_y, + &cardinal_x, &cardinal_y, &cardinal_count, + &diagonal_x, &diagonal_y, &diagonal_count); + int x = input->source_x; + int y = input->source_y; + for (int i = 0; i < cardinal_count; i++) { + if (!encounter_route_step_allowed( + input, x, y, cardinal_x, cardinal_y)) + return 0; + x += cardinal_x; + y += cardinal_y; + } + for (int i = 0; i < diagonal_count; i++) { + if (!encounter_route_step_allowed( + input, x, y, diagonal_x, diagonal_y)) + return 0; + x += diagonal_x; + y += diagonal_y; + } + result->outcome = ROUTE_REACHED_TARGET; + result->destination_x = destination_x; + result->destination_y = destination_y; + result->distance = (uint16_t)(cardinal_count + diagonal_count); + if (result->distance == 0) return 1; + result->first_dx = cardinal_count > 0 ? cardinal_x : diagonal_x; + result->first_dy = cardinal_count > 0 ? cardinal_y : diagonal_y; + if (result->distance >= 2 && + input->movement_mode == ENCOUNTER_ROUTE_MOVEMENT_RUN) { + result->run_dx = cardinal_count >= 2 ? cardinal_x : diagonal_x; + result->run_dy = cardinal_count >= 2 ? cardinal_y : diagonal_y; + } + if (cardinal_count > 0 && diagonal_count > 0) { + result->waypoint_x[result->waypoint_count] = + input->source_x + cardinal_x * cardinal_count; + result->waypoint_y[result->waypoint_count] = + input->source_y + cardinal_y * cardinal_count; + result->waypoint_count++; + } + result->waypoint_x[result->waypoint_count] = destination_x; + result->waypoint_y[result->waypoint_count] = destination_y; + result->waypoint_count++; + return 1; +} - for (int i = 0; i < 8; i++) { - int dx = pathfind_dir_dx[i]; - int dy = pathfind_dir_dy[i]; - int next_x = cur_x + dx; - int next_y = cur_y + dy; - if (next_x < 0 || next_x >= PATHFIND_GRID_SIZE || - next_y < 0 || next_y >= PATHFIND_GRID_SIZE) - continue; - if (via[next_x][next_y] != 0) continue; - if (!collision_traversable_step(map, height, abs_x, abs_y, dx, dy)) +static inline int encounter_route_try_direct( + const EncounterRouteInput* input, + EncounterRouteResult* result +) { + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE && + input->target_size == 1) { + return encounter_route_try_direct_destination( + input, input->target_x, input->target_y, result); + } + if (input->target_kind != ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY || + input->actor_size != 1) + return 0; + int selected_x = INT_MIN; + int selected_y = INT_MIN; + int target_max_x = input->target_x + input->target_size - 1; + int target_max_y = input->target_y + input->target_size - 1; + for (int y = input->target_y; y <= target_max_y; y++) { + int candidate_x[2] = {input->target_x - 1, target_max_x + 1}; + for (int i = 0; i < 2; i++) { + if (!encounter_route_destination_allowed( + input, candidate_x[i], y) || + !encounter_route_is_target(input, candidate_x[i], y)) continue; - if (dx != 0 && dy != 0) { - if (!collision_traversable_step(map, height, abs_x, abs_y, 0, dy)) - continue; - if (!collision_traversable_step(map, height, abs_x, abs_y, dx, 0)) - continue; + if (encounter_route_direct_candidate_before( + input, candidate_x[i], y, selected_x, selected_y)) { + selected_x = candidate_x[i]; + selected_y = y; } - if (EB(abs_x + dx, abs_y + dy)) continue; - if (dx != 0 && dy != 0) { - if (EB(abs_x, abs_y + dy)) continue; - if (EB(abs_x + dx, abs_y)) continue; + } + } + for (int x = input->target_x; x <= target_max_x; x++) { + int candidate_y[2] = {input->target_y - 1, target_max_y + 1}; + for (int i = 0; i < 2; i++) { + if (!encounter_route_destination_allowed( + input, x, candidate_y[i]) || + !encounter_route_is_target(input, x, candidate_y[i])) + continue; + if (encounter_route_direct_candidate_before( + input, x, candidate_y[i], selected_x, selected_y)) { + selected_x = x; + selected_y = candidate_y[i]; } - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_FULL, next_x, next_y); - via[next_x][next_y] = pathfind_dir_via[i]; - cost[next_x][next_y] = next_cost; } - - #undef EB } - if (!found_path) { - int best_manhattan = PATHFIND_GRID_SIZE * 2; - int best_cost = 999999; - int best_x = -1, best_y = -1; - - for (int fx = 0; fx < PATHFIND_GRID_SIZE; fx++) { - for (int fy = 0; fy < PATHFIND_GRID_SIZE; fy++) { - if (cost[fx][fy] == 0) continue; + if (selected_x == INT_MIN) return 0; + return encounter_route_try_direct_destination( + input, selected_x, selected_y, result); +} +static inline EncounterRouteResult encounter_route_greedy_direct( + const EncounterRouteInput* input +) { + EncounterRouteResult result; + memset(&result, 0, sizeof(result)); + result.outcome = ROUTE_UNREACHABLE; + if (input->target_kind != ENCOUNTER_ROUTE_TARGET_TILE) + return result; + int x = input->source_x; + int y = input->source_y; + int max_steps = + input->movement_mode == ENCOUNTER_ROUTE_MOVEMENT_RUN ? 2 : 1; + for (int step = 0; step < max_steps; step++) { + if (x == input->target_x && y == input->target_y) break; + int dx = (input->target_x > x) - (input->target_x < x); + int dy = (input->target_y > y) - (input->target_y < y); + int moved_dx = 0; + int moved_dy = 0; + if (dx != 0 && dy != 0 && + encounter_route_step_allowed(input, x, y, dx, dy)) { + moved_dx = dx; + moved_dy = dy; + } else if (dx != 0 && + encounter_route_step_allowed(input, x, y, dx, 0)) { + moved_dx = dx; + } else if (dy != 0 && + encounter_route_step_allowed(input, x, y, 0, dy)) { + moved_dy = dy; + } else { + break; + } + x += moved_dx; + y += moved_dy; + if (result.distance == 0) { + result.first_dx = moved_dx; + result.first_dy = moved_dy; + } else { + result.run_dx = moved_dx; + result.run_dy = moved_dy; + } + result.distance++; + } + result.destination_x = x; + result.destination_y = y; + if (result.distance > 0) { + result.waypoint_x[0] = x; + result.waypoint_y[0] = y; + result.waypoint_count = 1; + } + if (x == input->target_x && y == input->target_y) + result.outcome = ROUTE_REACHED_TARGET; + else if (result.distance > 0) + result.outcome = ROUTE_REACHED_FALLBACK; + return result; +} - int ddx = fx - local_dest_x; - int ddy = fy - local_dest_y; - int manhattan = (ddx < 0 ? -ddx : ddx) + (ddy < 0 ? -ddy : ddy); +static inline EncounterRouteResult encounter_route_solve( + const EncounterRouteInput* input); - if (manhattan < best_manhattan || - (manhattan == best_manhattan && cost[fx][fy] < best_cost)) { - best_manhattan = manhattan; - best_cost = cost[fx][fy]; - best_x = fx; - best_y = fy; - } +static inline EncounterRouteResult encounter_route_escape_overlap( + const EncounterRouteInput* input +) { + EncounterRouteResult result; + memset(&result, 0, sizeof(result)); + result.outcome = ROUTE_UNREACHABLE; + int max_radius = (input->target_size + 1) / 2 + 1; + int best_distance = INT_MAX; + int candidate_x = -1; + int candidate_y = -1; + for (int dy = -max_radius; dy <= max_radius; dy++) { + for (int dx = -max_radius; dx <= max_radius; dx++) { + if (dx == 0 && dy == 0) continue; + int x = input->source_x + dx; + int y = input->source_y + dy; + if (!encounter_route_destination_allowed(input, x, y)) continue; + if (encounter_route_footprints_overlap( + x, y, input->actor_size, + input->target_x, input->target_y, input->target_size)) + continue; + int distance = dx * dx + dy * dy; + if (distance < best_distance) { + best_distance = distance; + candidate_x = x; + candidate_y = y; } } - - if (best_x == -1) { - return result; - } - - cur_x = best_x; - cur_y = best_y; - found_path = 1; - result.dest_x = origin_x + best_x; - result.dest_y = origin_y + best_y; } + if (candidate_x < 0) return result; - while (1) { - int v = via[cur_x][cur_y]; - int prev_x = cur_x; - int prev_y = cur_y; + EncounterRouteInput escape_input = *input; + escape_input.target_x = candidate_x; + escape_input.target_y = candidate_y; + escape_input.target_size = 1; + escape_input.target_kind = ENCOUNTER_ROUTE_TARGET_TILE; + escape_input.movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN; + escape_input.cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST; + EncounterRouteResult escape = encounter_route_solve(&escape_input); + if (escape.outcome == ROUTE_INVALID_INPUT || + escape.outcome == ROUTE_UNREACHABLE) + return escape; + int landing_x = input->source_x + escape.first_dx + escape.run_dx; + int landing_y = input->source_y + escape.first_dy + escape.run_dy; - if (v & VIA_W) prev_x++; - else if (v & VIA_E) prev_x--; + EncounterRouteInput landing_input = *input; + landing_input.target_x = landing_x; + landing_input.target_y = landing_y; + landing_input.target_size = 1; + landing_input.target_kind = ENCOUNTER_ROUTE_TARGET_TILE; + return encounter_route_solve(&landing_input); +} - if (v & VIA_S) prev_y++; - else if (v & VIA_N) prev_y--; +static inline int encounter_route_reverse_seed( + const EncounterRouteInput* input, + EncounterRouteScratch* scratch, + EncounterReverseRouteField* field, + int x, + int y, + int* tail +) { + if (!encounter_route_destination_allowed(input, x, y)) return 0; + const EncounterArenaTopology* topology = input->topology; + int local_x = x - topology->origin_x; + int local_y = y - topology->origin_y; + if (local_x < 0 || local_x >= topology->width || + local_y < 0 || local_y >= topology->height) + return 0; + int index = local_x * topology->height + local_y; + if (field->depth[index] != UINT16_MAX) return 0; + field->depth[index] = 0; + scratch->queue[(*tail)++] = + (uint16_t)((local_x << 6) | local_y); + return 1; +} - if (prev_x == local_src_x && prev_y == local_src_y) { - result.found = 1; - result.next_dx = cur_x - local_src_x; - result.next_dy = cur_y - local_src_y; - return result; +static inline void encounter_route_reverse_seed_cardinal_edges( + const EncounterRouteInput* input, + EncounterRouteScratch* scratch, + EncounterReverseRouteField* field, + int require_target, + int* tail +) { + int target_max_x = input->target_x + input->target_size - 1; + int target_max_y = input->target_y + input->target_size - 1; + int x_edges[2] = { + input->target_x - input->actor_size, + target_max_x + 1, + }; + int y_edges[2] = { + input->target_y - input->actor_size, + target_max_y + 1, + }; + for (int edge = 0; edge < 2; edge++) { + int x = x_edges[edge]; + for (int y = input->target_y - input->actor_size + 1; + y <= target_max_y; + y++) { + if (require_target && + (!encounter_route_destination_allowed(input, x, y) || + !encounter_route_is_target(input, x, y))) + continue; + encounter_route_reverse_seed( + input, scratch, field, x, y, tail); } + int y = y_edges[edge]; + for (int x_scan = input->target_x - input->actor_size + 1; + x_scan <= target_max_x; + x_scan++) { + if (require_target && + (!encounter_route_destination_allowed(input, x_scan, y) || + !encounter_route_is_target(input, x_scan, y))) + continue; + encounter_route_reverse_seed( + input, scratch, field, x_scan, y, tail); + } + } +} - cur_x = prev_x; - cur_y = prev_y; - - if (via[cur_x][cur_y] == VIA_NONE || via[cur_x][cur_y] == VIA_START) { +static inline void encounter_route_reverse_build_result( + const EncounterRouteInput* input, + const EncounterReverseRouteField* field, + EncounterRouteResult* result +) { + static const int8_t osrs_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int8_t osrs_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; + static const int8_t south_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; + static const int8_t south_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; + const int8_t* direction_dx = + input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS ? osrs_dx : south_dx; + const int8_t* direction_dy = + input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS ? osrs_dy : south_dy; + const EncounterArenaTopology* topology = input->topology; + int current_x = input->source_x; + int current_y = input->source_y; + int source_index = + (current_x - topology->origin_x) * topology->height + + current_y - topology->origin_y; + result->outcome = (EncounterRouteOutcome)field->outcome; + result->distance = field->depth[source_index]; + int segment_dx = 0; + int segment_dy = 0; + int previous_x = current_x; + int previous_y = current_y; + for (uint16_t step = 0; step < result->distance; step++) { + int current_index = + (current_x - topology->origin_x) * topology->height + + current_y - topology->origin_y; + uint16_t depth = field->depth[current_index]; + int next_x = current_x; + int next_y = current_y; + int next_dx = 0; + int next_dy = 0; + for (int direction = 0; direction < 8; direction++) { + int candidate_x = current_x + direction_dx[direction]; + int candidate_y = current_y + direction_dy[direction]; + if (!encounter_arena_topology_contains( + topology, candidate_x, candidate_y)) + continue; + int candidate_index = + (candidate_x - topology->origin_x) * topology->height + + candidate_y - topology->origin_y; + if (field->depth[candidate_index] == UINT16_MAX || + field->depth[candidate_index] + 1 != depth || + !encounter_route_step_allowed( + input, current_x, current_y, + direction_dx[direction], direction_dy[direction])) + continue; + next_x = candidate_x; + next_y = candidate_y; + next_dx = direction_dx[direction]; + next_dy = direction_dy[direction]; break; } + if (next_dx == 0 && next_dy == 0) { + fprintf(stderr, "broken OSRS reverse route\n"); + abort(); + } + if (step == 0) { + result->first_dx = next_dx; + result->first_dy = next_dy; + } else if (step == 1 && + input->movement_mode == ENCOUNTER_ROUTE_MOVEMENT_RUN) { + result->run_dx = next_dx; + result->run_dy = next_dy; + } + if (step > 0 && (next_dx != segment_dx || next_dy != segment_dy) && + result->waypoint_count < ENCOUNTER_ROUTE_MAX_WAYPOINTS) { + result->waypoint_x[result->waypoint_count] = previous_x; + result->waypoint_y[result->waypoint_count] = previous_y; + result->waypoint_count++; + } + segment_dx = next_dx; + segment_dy = next_dy; + previous_x = next_x; + previous_y = next_y; + current_x = next_x; + current_y = next_y; + } + result->destination_x = current_x; + result->destination_y = current_y; + if (result->distance > 0 && + result->waypoint_count < ENCOUNTER_ROUTE_MAX_WAYPOINTS) { + result->waypoint_x[result->waypoint_count] = current_x; + result->waypoint_y[result->waypoint_count] = current_y; + result->waypoint_count++; } - - return result; } -static inline PathResult pathfind_step_arena( - const CollisionMap* map, int height, - int src_x, int src_y, int dest_x, int dest_y, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - int arena_origin_x, int arena_origin_y, int arena_w, int arena_h +static inline int encounter_route_try_reverse( + const EncounterRouteInput* input, + EncounterRouteResult* result ) { - PathResult result = {0, 0, 0, dest_x, dest_y}; - - if (arena_w <= 0 || arena_w > PATHFIND_ARENA_MAX || - arena_h <= 0 || arena_h > PATHFIND_ARENA_MAX) { - fprintf(stderr, "pathfind arena dimensions out of bounds: %dx%d\n", arena_w, arena_h); - abort(); + if (input->target_kind != ENCOUNTER_ROUTE_TARGET_TILE || + input->cost_policy != ENCOUNTER_ROUTE_COST_SOUTH_FIRST_REVERSE) + return 0; + EncounterRouteScratch* scratch = &encounter_route_scratch; + EncounterReverseRouteField* field = &scratch->reverse_field; + const EncounterArenaTopology* topology = input->topology; + int source_local_x = input->source_x - topology->origin_x; + int source_local_y = input->source_y - topology->origin_y; + int source_index = + source_local_x * topology->height + source_local_y; + memset(field->depth, 0xff, sizeof(field->depth)); + scratch->current_generation++; + if (scratch->current_generation == 0) { + memset(scratch->blocker_generation, 0, + sizeof(scratch->blocker_generation)); + scratch->current_generation = 1; } - - if (src_x == dest_x && src_y == dest_y) { - result.found = 1; - return result; + uint16_t blocker_generation = scratch->current_generation; + int tail = 0; + field->outcome = ROUTE_REACHED_TARGET; + if (!encounter_route_reverse_seed( + input, scratch, field, + input->target_x, input->target_y, &tail)) { + encounter_route_reverse_seed_cardinal_edges( + input, scratch, field, 0, &tail); + field->outcome = ROUTE_REACHED_FALLBACK; + } + if (tail == 0) return 0; + int head = 0; + while (head < tail && field->depth[source_index] == UINT16_MAX) { + int current_packed = scratch->queue[head++]; + int current_x = current_packed >> 6; + int current_y = current_packed & 63; + int current_index = current_x * topology->height + current_y; + int current_abs_x = topology->origin_x + current_x; + int current_abs_y = topology->origin_y + current_y; + uint16_t next_depth = (uint16_t)(field->depth[current_index] + 1); + static const int8_t dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int8_t dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; + for (int direction = 0; direction < 8; direction++) { + int predecessor_x = current_x - dx[direction]; + int predecessor_y = current_y - dy[direction]; + if (predecessor_x < 0 || predecessor_x >= topology->width || + predecessor_y < 0 || predecessor_y >= topology->height) + continue; + int predecessor_index = + predecessor_x * topology->height + predecessor_y; + if (field->depth[predecessor_index] != UINT16_MAX) + continue; + int predecessor_abs_x = current_abs_x - dx[direction]; + int predecessor_abs_y = current_abs_y - dy[direction]; + if (!encounter_route_step_allowed_cached( + input, scratch, blocker_generation, + predecessor_abs_x, predecessor_abs_y, + dx[direction], dy[direction])) + continue; + if (tail >= topology->tile_count) { + fprintf(stderr, "OSRS reverse route queue overflow: %d\n", tail); + abort(); + } + field->depth[predecessor_index] = next_depth; + scratch->queue[tail++] = + (uint16_t)((predecessor_x << 6) | predecessor_y); + } } + if (field->depth[source_index] == UINT16_MAX) return 0; + encounter_route_reverse_build_result(input, field, result); + return 1; +} +static inline int encounter_route_source_field_matches( + const EncounterSourceRouteField* field, + const EncounterRouteInput* input +) { + return field->valid && + field->topology == input->topology && + field->blocker_ctx == input->blockers.ctx && + field->blocker == input->blockers.is_blocked && + field->topology_revision == input->topology->revision && + field->blocker_revision == input->blockers.revision && + field->source_x == input->source_x && + field->source_y == input->source_y && + field->actor_size == input->actor_size && + field->movement_mode == input->movement_mode && + field->cost_policy == input->cost_policy; +} - int local_src_x = src_x - arena_origin_x; - int local_src_y = src_y - arena_origin_y; - int local_dest_x = dest_x - arena_origin_x; - int local_dest_y = dest_y - arena_origin_y; +static inline void encounter_route_build_source_field( + const EncounterRouteInput* input, + EncounterSourceRouteField* field +) { + const EncounterArenaTopology* topology = input->topology; + memset(field->visited, 0, sizeof(field->visited)); + memset(field->blocker_known, 0, sizeof(field->blocker_known)); + memset(field->blocker_value, 0, sizeof(field->blocker_value)); + field->valid = 0; + int source_local_x = input->source_x - topology->origin_x; + int source_local_y = input->source_y - topology->origin_y; + int source_index = + source_local_x * topology->height + source_local_y; + field->depth[source_index] = 0; + field->visited[source_local_x] |= UINT64_C(1) << source_local_y; + field->via[source_index] = VIA_START; + field->queue[0] = + (uint16_t)((source_local_x << 6) | source_local_y); + field->topology = topology; + field->blocker_ctx = input->blockers.ctx; + field->blocker = input->blockers.is_blocked; + field->topology_revision = topology->revision; + field->blocker_revision = input->blockers.revision; + field->source_x = input->source_x; + field->source_y = input->source_y; + field->visited_count = 1; + field->expanded_count = 0; + field->actor_size = (uint8_t)input->actor_size; + field->movement_mode = (uint8_t)input->movement_mode; + field->cost_policy = (uint8_t)input->cost_policy; + field->valid = 1; +} - if (local_src_x < 0 || local_src_x >= arena_w || - local_src_y < 0 || local_src_y >= arena_h || - local_dest_x < 0 || local_dest_x >= arena_w || - local_dest_y < 0 || local_dest_y >= arena_h) { - return result; +static OSRS_ROUTE_NOINLINE int encounter_route_expand_source_field( + const EncounterRouteInput* input, + EncounterSourceRouteField* field, + const uint64_t* target_edges +) { + const EncounterArenaTopology* topology = input->topology; + for (int i = 0; i < field->visited_count; i++) { + int packed = field->queue[i]; + int local_x = packed >> 6; + int local_y = packed & 63; + if (target_edges[local_x] & (UINT64_C(1) << local_y)) + return local_x * topology->height + local_y; + } + static const int8_t direction_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int8_t direction_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; + static const int8_t direction_via[8] = { + VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE + }; + static const uint8_t direction_step_mask[8] = { + 8, 16, 2, 64, 1, 4, 32, 128 + }; + const uint8_t* legal_step_masks = + topology->legal_step_masks[input->actor_size - 1]; + while (field->expanded_count < field->visited_count) { + int current_packed = field->queue[field->expanded_count++]; + int current_x = current_packed >> 6; + int current_y = current_packed & 63; + int current_index = current_x * topology->height + current_y; + int x = topology->origin_x + current_x; + int y = topology->origin_y + current_y; + uint16_t next_depth = (uint16_t)(field->depth[current_index] + 1); + int first_new = field->visited_count; + for (int direction = 0; direction < 8; direction++) { + int next_x = current_x + direction_dx[direction]; + int next_y = current_y + direction_dy[direction]; + if (next_x < 0 || next_x >= topology->width || + next_y < 0 || next_y >= topology->height) + continue; + uint64_t next_bit = UINT64_C(1) << next_y; + if (field->visited[next_x] & next_bit) continue; + int next_index = next_x * topology->height + next_y; + int dx = direction_dx[direction]; + int dy = direction_dy[direction]; + if (input->blockers.is_blocked) { + if ((field->blocker_known[next_x] & next_bit) == 0) { + field->blocker_known[next_x] |= next_bit; + if (input->blockers.is_blocked( + input->blockers.ctx, + topology->origin_x + next_x, + topology->origin_y + next_y, + input->actor_size)) + field->blocker_value[next_x] |= next_bit; + } + if (field->blocker_value[next_x] & next_bit) continue; + if (dx != 0 && dy != 0) { + uint64_t side_bit = UINT64_C(1) << next_y; + if ((field->blocker_known[current_x] & side_bit) == 0) { + field->blocker_known[current_x] |= side_bit; + if (input->blockers.is_blocked( + input->blockers.ctx, + x, + topology->origin_y + next_y, + input->actor_size)) + field->blocker_value[current_x] |= side_bit; + } + if (field->blocker_value[current_x] & side_bit) continue; + side_bit = UINT64_C(1) << current_y; + if ((field->blocker_known[next_x] & side_bit) == 0) { + field->blocker_known[next_x] |= side_bit; + if (input->blockers.is_blocked( + input->blockers.ctx, + topology->origin_x + next_x, + y, + input->actor_size)) + field->blocker_value[next_x] |= side_bit; + } + if (field->blocker_value[next_x] & side_bit) continue; + } + } + if ((legal_step_masks[current_index] & + direction_step_mask[direction]) == 0) + continue; + if (field->visited_count >= topology->tile_count) { + fprintf(stderr, "OSRS source field queue overflow: %u\n", + field->visited_count); + abort(); + } + field->visited[next_x] |= next_bit; + field->depth[next_index] = next_depth; + field->via[next_index] = direction_via[direction]; + field->queue[field->visited_count++] = + (uint16_t)((next_x << 6) | next_y); + } + for (int i = first_new; i < field->visited_count; i++) { + int packed = field->queue[i]; + int local_x = packed >> 6; + int local_y = packed & 63; + if (target_edges[local_x] & (UINT64_C(1) << local_y)) + return local_x * topology->height + local_y; + } } + return -1; +} - static OSRS_THREAD_LOCAL uint16_t bfs_gen[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - static OSRS_THREAD_LOCAL int8_t bfs_via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - static OSRS_THREAD_LOCAL int16_t bfs_cost[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - static OSRS_THREAD_LOCAL uint16_t bfs_gen_counter = 0; - bfs_gen_counter++; - if (bfs_gen_counter == 0) { - memset(bfs_gen, 0, sizeof(bfs_gen)); - bfs_gen_counter = 1; +static inline void encounter_route_build_source_field_result( + const EncounterRouteInput* input, + const EncounterSourceRouteField* field, + EncounterRouteResult* result, + int destination_x, + int destination_y +) { + const EncounterArenaTopology* topology = input->topology; + int source_x = input->source_x - topology->origin_x; + int source_y = input->source_y - topology->origin_y; + int current_x = destination_x; + int current_y = destination_y; + int destination_index = current_x * topology->height + current_y; + uint16_t distance = field->depth[destination_index]; + result->distance = distance; + result->destination_x = topology->origin_x + destination_x; + result->destination_y = topology->origin_y + destination_y; + int first_x = source_x; + int first_y = source_y; + int second_x = source_x; + int second_y = source_y; + int direction = -1; + while (current_x != source_x || current_y != source_y) { + int index = current_x * topology->height + current_y; + uint16_t depth = field->depth[index]; + if (depth == 1) { + first_x = current_x; + first_y = current_y; + } else if (depth == 2) { + second_x = current_x; + second_y = current_y; + } + int next_direction = field->via[index]; + if (direction != next_direction) { + direction = next_direction; + int count = result->waypoint_count; + if (count == ENCOUNTER_ROUTE_MAX_WAYPOINTS) count--; + memmove( + &result->waypoint_x[1], + &result->waypoint_x[0], + (size_t)count * sizeof(result->waypoint_x[0])); + memmove( + &result->waypoint_y[1], + &result->waypoint_y[0], + (size_t)count * sizeof(result->waypoint_y[0])); + result->waypoint_x[0] = topology->origin_x + current_x; + result->waypoint_y[0] = topology->origin_y + current_y; + result->waypoint_count = (uint8_t)(count + 1); + } + encounter_route_parent(topology, next_direction, ¤t_x, ¤t_y); + } + if (distance == 0) return; + result->first_dx = first_x - source_x; + result->first_dy = first_y - source_y; + if (distance >= 2 && + input->movement_mode == ENCOUNTER_ROUTE_MOVEMENT_RUN) { + result->run_dx = second_x - first_x; + result->run_dy = second_y - first_y; } - uint16_t gen = bfs_gen_counter; - #define BFS_VISITED(x, y) (bfs_gen[(x)][(y)] == gen) - #define BFS_VISIT(x, y, v, c) do { \ - bfs_gen[(x)][(y)] = gen; bfs_via[(x)][(y)] = (v); bfs_cost[(x)][(y)] = (c); \ - } while(0) - #define BFS_VIA(x, y) bfs_via[(x)][(y)] - #define BFS_COST(x, y) bfs_cost[(x)][(y)] +} - int queue_x[PATHFIND_MAX_QUEUE_ARENA]; - int queue_y[PATHFIND_MAX_QUEUE_ARENA]; - int head = 0, tail = 0; +static inline int encounter_route_try_source_field( + const EncounterRouteInput* input, + EncounterRouteResult* result +) { + if (input->target_kind != ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY || + input->cost_policy != ENCOUNTER_ROUTE_COST_OSRS) + return 0; + EncounterRouteScratch* scratch = &encounter_route_scratch; + EncounterSourceRouteField* field = &scratch->source_field; + const EncounterArenaTopology* topology = input->topology; + if (!encounter_route_source_field_matches(field, input)) + encounter_route_build_source_field(input, field); + uint64_t target_edges[ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION] = {0}; + int target_max_x = input->target_x + input->target_size - 1; + int target_max_y = input->target_y + input->target_size - 1; + int x_edges[2] = { + input->target_x - input->actor_size, + target_max_x + 1, + }; + int y_edges[2] = { + input->target_y - input->actor_size, + target_max_y + 1, + }; + for (int edge = 0; edge < 2; edge++) { + int x = x_edges[edge]; + for (int y = input->target_y - input->actor_size + 1; + y <= target_max_y; + y++) { + if (encounter_arena_topology_footprint_blocked( + topology, x, y, input->actor_size) || + !encounter_route_is_target(input, x, y)) + continue; + int local_x = x - topology->origin_x; + int local_y = y - topology->origin_y; + target_edges[local_x] |= UINT64_C(1) << local_y; + } + int y = y_edges[edge]; + for (int x_scan = input->target_x - input->actor_size + 1; + x_scan <= target_max_x; + x_scan++) { + if (encounter_arena_topology_footprint_blocked( + topology, x_scan, y, input->actor_size) || + !encounter_route_is_target(input, x_scan, y)) + continue; + int local_x = x_scan - topology->origin_x; + int local_y = y - topology->origin_y; + target_edges[local_x] |= UINT64_C(1) << local_y; + } + } + int selected_index = encounter_route_expand_source_field( + input, field, target_edges); + if (selected_index >= 0) { + result->outcome = ROUTE_REACHED_TARGET; + encounter_route_build_source_field_result( + input, field, result, + selected_index / topology->height, + selected_index % topology->height); + return 1; + } + int best_distance = INT_MAX; + uint16_t best_depth = UINT16_MAX; + for (int local_x = 0; local_x < topology->width; local_x++) { + for (int local_y = 0; local_y < topology->height; local_y++) { + int index = local_x * topology->height + local_y; + if ((field->visited[local_x] & + (UINT64_C(1) << local_y)) == 0) + continue; + int x = topology->origin_x + local_x; + int y = topology->origin_y + local_y; + int64_t min_target_x = + (int64_t)input->target_x - PATHFIND_MAX_FALLBACK_RADIUS; + int64_t max_target_x = + (int64_t)input->target_x + input->target_size - 1 + + PATHFIND_MAX_FALLBACK_RADIUS; + int64_t min_target_y = + (int64_t)input->target_y - PATHFIND_MAX_FALLBACK_RADIUS; + int64_t max_target_y = + (int64_t)input->target_y + input->target_size - 1 + + PATHFIND_MAX_FALLBACK_RADIUS; + if ((int64_t)x < min_target_x || (int64_t)x > max_target_x || + (int64_t)y < min_target_y || (int64_t)y > max_target_y) + continue; + int target_distance = + encounter_route_target_distance_squared(input, x, y); + uint16_t depth = field->depth[index]; + if (target_distance < best_distance || + (target_distance == best_distance && depth < best_depth)) { + selected_index = index; + best_distance = target_distance; + best_depth = depth; + } + } + } + int source_index = + (input->source_x - topology->origin_x) * topology->height + + input->source_y - topology->origin_y; + if (selected_index < 0 || + (selected_index == source_index && + encounter_route_dynamic_blocked( + input, input->source_x, input->source_y))) { + result->outcome = ROUTE_UNREACHABLE; + return 1; + } + result->outcome = ROUTE_REACHED_FALLBACK; + encounter_route_build_source_field_result( + input, field, result, + selected_index / topology->height, + selected_index % topology->height); + return 1; +} - BFS_VISIT(local_src_x, local_src_y, VIA_START, 1); - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, local_src_x, local_src_y); - int found_path = 0; - int cur_x, cur_y; +static inline EncounterRouteResult encounter_route_solve( + const EncounterRouteInput* input +) { + EncounterRouteResult result; + memset(&result, 0, sizeof(result)); + result.outcome = ROUTE_INVALID_INPUT; + if (!encounter_route_input_valid(input)) return result; + if (input->cost_policy == ENCOUNTER_ROUTE_COST_DIRECT) + return encounter_route_greedy_direct(input); + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY && + encounter_route_footprints_overlap( + input->source_x, input->source_y, input->actor_size, + input->target_x, input->target_y, input->target_size)) + return encounter_route_escape_overlap(input); + if (input->cost_policy != ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS && + encounter_route_try_direct(input, &result)) + return result; + if (encounter_route_try_source_field(input, &result)) return result; + if (encounter_route_try_reverse(input, &result)) return result; + const EncounterArenaTopology* topology = input->topology; + EncounterRouteScratch* scratch = &encounter_route_scratch; + scratch->current_generation++; + if (scratch->current_generation == 0) { + memset(scratch->generation, 0, sizeof(scratch->generation)); + memset(scratch->blocker_generation, 0, + sizeof(scratch->blocker_generation)); + scratch->current_generation = 1; + } + uint16_t generation = scratch->current_generation; + int source_local_x = input->source_x - topology->origin_x; + int source_local_y = input->source_y - topology->origin_y; + int source_index = source_local_x * topology->height + source_local_y; + scratch->generation[source_index] = generation; + scratch->depth[source_index] = 0; + scratch->via[source_index] = VIA_START; + scratch->queue[0] = + (uint16_t)((source_local_x << 6) | source_local_y); + int head = 0; + int tail = 1; + int selected_index = -1; + static const int8_t osrs_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int8_t osrs_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; + static const int8_t osrs_via[8] = { + VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE + }; + static const uint8_t osrs_step_mask[8] = { + 8, 16, 2, 64, 1, 4, 32, 128 + }; + static const int8_t south_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; + static const int8_t south_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; + static const int8_t south_via[8] = { + VIA_S, VIA_W, VIA_N, VIA_E, VIA_SW, VIA_NW, VIA_SE, VIA_NE + }; + static const uint8_t south_step_mask[8] = { + 2, 8, 64, 16, 1, 32, 4, 128 + }; + int osrs_cost = input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS; + const int8_t* direction_dx = osrs_cost ? osrs_dx : south_dx; + const int8_t* direction_dy = osrs_cost ? osrs_dy : south_dy; + const int8_t* direction_via = osrs_cost ? osrs_via : south_via; + const uint8_t* direction_step_mask = + osrs_cost ? osrs_step_mask : south_step_mask; + const uint8_t* legal_step_masks = + topology->legal_step_masks[input->actor_size - 1]; while (head < tail) { - cur_x = queue_x[head]; - cur_y = queue_y[head]; - head++; - - if (cur_x == local_dest_x && cur_y == local_dest_y) { - found_path = 1; + int current_packed = scratch->queue[head++]; + int current_x = current_packed >> 6; + int current_y = current_packed & 63; + int current_index = current_x * topology->height + current_y; + int x = topology->origin_x + current_x; + int y = topology->origin_y + current_y; + int reached_target = + input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE + ? x == input->target_x && y == input->target_y + : encounter_route_is_target(input, x, y); + if (reached_target) { + selected_index = current_index; break; } - - int abs_x = arena_origin_x + cur_x; - int abs_y = arena_origin_y + cur_y; - int next_cost = BFS_COST(cur_x, cur_y) + 1; - - #define EB(ax, ay) (extra_blocked && extra_blocked(blocked_ctx, (ax), (ay))) - + uint16_t next_depth = (uint16_t)(scratch->depth[current_index] + 1); for (int i = 0; i < 8; i++) { - int dx = pathfind_dir_dx[i]; - int dy = pathfind_dir_dy[i]; - int next_x = cur_x + dx; - int next_y = cur_y + dy; - if (next_x < 0 || next_x >= arena_w || - next_y < 0 || next_y >= arena_h) + int next_x = current_x + direction_dx[i]; + int next_y = current_y + direction_dy[i]; + if (next_x < 0 || next_x >= topology->width || + next_y < 0 || next_y >= topology->height) continue; - if (BFS_VISITED(next_x, next_y)) continue; - if (!collision_traversable_step(map, height, abs_x, abs_y, dx, dy)) - continue; - if (dx != 0 && dy != 0) { - if (!collision_traversable_step(map, height, abs_x, abs_y, 0, dy)) - continue; - if (!collision_traversable_step(map, height, abs_x, abs_y, dx, 0)) - continue; + int next_index = next_x * topology->height + next_y; + if (scratch->generation[next_index] == generation) continue; + int dx = direction_dx[i]; + int dy = direction_dy[i]; + if (input->blockers.is_blocked) { + if (scratch->blocker_generation[next_index] != generation) { + scratch->blocker_generation[next_index] = generation; + scratch->blocker_value[next_index] = (uint8_t) + input->blockers.is_blocked( + input->blockers.ctx, + topology->origin_x + next_x, + topology->origin_y + next_y, + input->actor_size); + } + if (scratch->blocker_value[next_index]) continue; + if (dx != 0 && dy != 0) { + int side_index = + current_x * topology->height + next_y; + if (scratch->blocker_generation[side_index] != generation) { + scratch->blocker_generation[side_index] = generation; + scratch->blocker_value[side_index] = (uint8_t) + input->blockers.is_blocked( + input->blockers.ctx, + x, + topology->origin_y + next_y, + input->actor_size); + } + if (scratch->blocker_value[side_index]) continue; + side_index = next_x * topology->height + current_y; + if (scratch->blocker_generation[side_index] != generation) { + scratch->blocker_generation[side_index] = generation; + scratch->blocker_value[side_index] = (uint8_t) + input->blockers.is_blocked( + input->blockers.ctx, + topology->origin_x + next_x, + y, + input->actor_size); + } + if (scratch->blocker_value[side_index]) continue; + } } - if (EB(abs_x + dx, abs_y + dy)) continue; - if (dx != 0 && dy != 0) { - if (EB(abs_x, abs_y + dy)) continue; - if (EB(abs_x + dx, abs_y)) continue; + if ((legal_step_masks[current_index] & + direction_step_mask[i]) == 0) + continue; + if (tail >= topology->tile_count) { + fprintf(stderr, "OSRS route queue overflow: %d\n", tail); + abort(); } - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, next_x, next_y); - BFS_VISIT(next_x, next_y, pathfind_dir_via[i], next_cost); + scratch->generation[next_index] = generation; + scratch->depth[next_index] = next_depth; + scratch->via[next_index] = (int8_t)direction_via[i]; + scratch->queue[tail++] = + (uint16_t)((next_x << 6) | next_y); } - - #undef EB } - if (!found_path) { - int best_manhattan = arena_w + arena_h; - int best_cost = 999999; - int best_x = -1, best_y = -1; - - for (int fx = 0; fx < arena_w; fx++) { - for (int fy = 0; fy < arena_h; fy++) { - if (!BFS_VISITED(fx, fy) || BFS_COST(fx, fy) == 0) continue; - int ddx = fx - local_dest_x, ddy = fy - local_dest_y; - int manhattan = (ddx < 0 ? -ddx : ddx) + (ddy < 0 ? -ddy : ddy); - if (manhattan < best_manhattan || - (manhattan == best_manhattan && BFS_COST(fx, fy) < best_cost)) { - best_manhattan = manhattan; - best_cost = BFS_COST(fx, fy); - best_x = fx; best_y = fy; - } + if (selected_index >= 0) { + result.outcome = ROUTE_REACHED_TARGET; + encounter_route_build_result_path( + input, + &result, + selected_index / topology->height, + selected_index % topology->height, + generation); + return result; + } + int best_distance = INT_MAX; + uint16_t best_depth = UINT16_MAX; + for (int local_x = 0; local_x < topology->width; local_x++) { + for (int local_y = 0; local_y < topology->height; local_y++) { + int index = local_x * topology->height + local_y; + if (scratch->generation[index] != generation) continue; + int x = topology->origin_x + local_x; + int y = topology->origin_y + local_y; + if (input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS) { + int64_t min_target_x = + (int64_t)input->target_x - PATHFIND_MAX_FALLBACK_RADIUS; + int64_t max_target_x = + (int64_t)input->target_x + input->target_size - 1 + + PATHFIND_MAX_FALLBACK_RADIUS; + int64_t min_target_y = + (int64_t)input->target_y - PATHFIND_MAX_FALLBACK_RADIUS; + int64_t max_target_y = + (int64_t)input->target_y + input->target_size - 1 + + PATHFIND_MAX_FALLBACK_RADIUS; + if ((int64_t)x < min_target_x || (int64_t)x > max_target_x || + (int64_t)y < min_target_y || (int64_t)y > max_target_y) + continue; + } + int target_distance; + if (input->cost_policy == ENCOUNTER_ROUTE_COST_SOUTH_FIRST || + input->cost_policy == ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS) { + target_distance = + encounter_route_abs(x - input->target_x) + + encounter_route_abs(y - input->target_y); + } else { + target_distance = + encounter_route_target_distance_squared(input, x, y); + } + uint16_t depth = scratch->depth[index]; + if (target_distance < best_distance || + (target_distance == best_distance && depth < best_depth)) { + selected_index = index; + best_distance = target_distance; + best_depth = depth; } } - - if (best_x == -1) return result; - cur_x = best_x; cur_y = best_y; - found_path = 1; - result.dest_x = arena_origin_x + best_x; - result.dest_y = arena_origin_y + best_y; - } - - while (1) { - if (!BFS_VISITED(cur_x, cur_y)) break; - int v = BFS_VIA(cur_x, cur_y); - if (v == VIA_NONE || v == VIA_START) break; - int prev_x = cur_x, prev_y = cur_y; - if (v & VIA_W) prev_x++; else if (v & VIA_E) prev_x--; - if (v & VIA_S) prev_y++; else if (v & VIA_N) prev_y--; - if (prev_x == local_src_x && prev_y == local_src_y) { - result.found = 1; - result.next_dx = cur_x - local_src_x; - result.next_dy = cur_y - local_src_y; - return result; - } - if (prev_x < 0 || prev_x >= arena_w || prev_y < 0 || prev_y >= arena_h) break; - cur_x = prev_x; cur_y = prev_y; } - + if (selected_index < 0 || + (selected_index == source_index && + encounter_route_dynamic_blocked( + input, input->source_x, input->source_y))) { + result.outcome = ROUTE_UNREACHABLE; + return result; + } + result.outcome = ROUTE_REACHED_FALLBACK; + encounter_route_build_result_path( + input, + &result, + selected_index / topology->height, + selected_index % topology->height, + generation); return result; } diff --git a/ocean/osrs/osrs_pvp_actions.h b/ocean/osrs/osrs_pvp_actions.h index cbf865bc16..0ed5a976a6 100644 --- a/ocean/osrs/osrs_pvp_actions.h +++ b/ocean/osrs/osrs_pvp_actions.h @@ -458,7 +458,12 @@ static void execute_attack_movement(OsrsEnv* env, int agent_idx, int* actions) { /* runs after BOTH players' attack movement so range checks use final positions; checking ranges in the movement phase reintroduces the PID-dependent same-tile bug */ -static void execute_attack_combat(OsrsEnv* env, int agent_idx, int* actions) { +static void execute_attack_combat( + OsrsEnv* env, + int agent_idx, + int* actions, + const EncounterArenaTopology* topology +) { Player* p = &env->players[agent_idx]; Player* t = &env->players[1 - agent_idx]; const CollisionMap* cmap = (const CollisionMap*)env->collision_map; @@ -570,7 +575,7 @@ static void execute_attack_combat(OsrsEnv* env, int agent_idx, int* actions) { set_destination(p, adj_x, adj_y, cmap); } } else { - move_toward_target(p, t, auto_walk_range, cmap); + move_toward_target(p, t, auto_walk_range, cmap, topology); } } } diff --git a/ocean/osrs/osrs_pvp_api.h b/ocean/osrs/osrs_pvp_api.h index 789ac47af7..f2e54a7f86 100644 --- a/ocean/osrs/osrs_pvp_api.h +++ b/ocean/osrs/osrs_pvp_api.h @@ -358,7 +358,23 @@ static void pvp_resolve_same_tile(OsrsEnv* env, int first, int second) { /** One game tick: switches for both players, then movement, then attacks, then pending hits; an action submitted at tick N is visible in state at N+1. */ -void pvp_step(OsrsEnv* env) { +static inline void pvp_actor_route_caches_clear( + OsrsActorRouteCache route_cache[NUM_AGENTS] +) { + if (!route_cache) abort(); + for (int i = 0; i < NUM_AGENTS; i++) + osrs_actor_route_cache_clear(&route_cache[i]); +} + +void pvp_step( + OsrsEnv* env, + const EncounterArenaTopology* route_topology, + OsrsActorRouteCache route_cache[NUM_AGENTS] +) { + if (!route_cache) { + fprintf(stderr, "PvP step missing actor route caches\n"); + abort(); + } memset(env->rewards, 0, NUM_AGENTS * sizeof(float)); memset(env->terminals, 0, NUM_AGENTS); @@ -464,8 +480,8 @@ void pvp_step(OsrsEnv* env) { if (pi->karambwan_timer > 0) pi->karambwan_timer--; } - pvp_step_player_movement(env, first); - pvp_step_player_movement(env, second); + pvp_step_player_movement(env, first, route_topology, &route_cache[first]); + pvp_step_player_movement(env, second, route_topology, &route_cache[second]); pvp_resolve_same_tile(env, first, second); @@ -474,8 +490,8 @@ void pvp_step(OsrsEnv* env) { pvp_resolve_same_tile(env, first, second); - execute_attack_combat(env, first, agent_actions[first]); - execute_attack_combat(env, second, agent_actions[second]); + execute_attack_combat(env, first, agent_actions[first], route_topology); + execute_attack_combat(env, second, agent_actions[second], route_topology); pvp_resolve_same_tile(env, first, second); diff --git a/ocean/osrs/osrs_pvp_movement.h b/ocean/osrs/osrs_pvp_movement.h index 355a43fbe6..cd5d54e063 100644 --- a/ocean/osrs/osrs_pvp_movement.h +++ b/ocean/osrs/osrs_pvp_movement.h @@ -169,33 +169,89 @@ static int pvp_tile_walkable(void* ctx, int x, int y) { return is_in_wilderness(x, y) && collision_tile_walkable(cmap, 0, x, y); } +typedef struct { + EncounterArenaTopology* topology; + const CollisionMap* collision_map; +} PvpRouteTopologyOwner; + +static PvpRouteTopologyOwner pvp_route_topology_owner; + +static uint32_t pvp_route_topology_flags(void* data, int x, int y) { + const CollisionMap* collision_map = (const CollisionMap*)data; + if (!is_in_wilderness(x, y)) return COLLISION_BLOCKED | LOS_FULL_MASK; + return collision_map + ? (uint32_t)collision_get_flags(collision_map, 0, x, y) + : 0; +} + +static const EncounterArenaTopology* pvp_route_topology_setup( + const CollisionMap* collision_map +) { + if (pvp_route_topology_owner.topology) { + if (pvp_route_topology_owner.collision_map != collision_map) { + fprintf(stderr, "NH PvP route topology collision map changed\n"); + abort(); + } + return pvp_route_topology_owner.topology; + } + EncounterArenaTopologyBuildSpec spec = { + .origin_x = FIGHT_AREA_BASE_X, + .origin_y = FIGHT_AREA_BASE_Y, + .width = FIGHT_AREA_WIDTH, + .height = FIGHT_AREA_HEIGHT, + .max_footprint_size = 1, + .revision = UINT64_C(0x4e48505650000001), + .tile_flags = pvp_route_topology_flags, + .tile_flags_ctx = (void*)collision_map, + }; + pvp_route_topology_owner.topology = + encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(pvp_route_topology_owner.topology); + pvp_route_topology_owner.collision_map = collision_map; + return pvp_route_topology_owner.topology; +} + + static void move_toward_target( Player* p, Player* target, int attack_range, - const CollisionMap* cmap + const CollisionMap* cmap, + const EncounterArenaTopology* topology ) { if (p->frozen_ticks > 0) { return; } - int moved = encounter_chase_attack_target( - p, - target->x, - target->y, - 1, - attack_range, - cmap, - 0, - 0, - pvp_tile_walkable, - (void*)cmap, - NULL, - NULL, - osrs_los_open_query(), - 0, - 0, - 0, - 0); + EncounterRouteInput route_input = { + .topology = topology, + .blockers = {0}, + .source_x = p->x, + .source_y = p->y, + .actor_size = 1, + .target_x = target->x, + .target_y = target->y, + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + }; + EncounterRouteResult route = encounter_route_solve(&route_input); + int moved = 0; + if (route.outcome == ROUTE_REACHED_TARGET || + route.outcome == ROUTE_REACHED_FALLBACK) { + p->x += route.first_dx; + p->y += route.first_dy; + moved = route.first_dx != 0 || route.first_dy != 0; + if ((route.run_dx != 0 || route.run_dy != 0) && + !encounter_player_can_attack( + p->x, p->y, + target->x, target->y, 1, attack_range, + cmap, 0, 0, osrs_los_open_query())) { + p->x += route.run_dx; + p->y += route.run_dy; + moved = 1; + } + } p->is_moving = moved; } @@ -279,24 +335,29 @@ static int pvp_lookup_attack_target(void* ctx, int target_slot, OsrsAttackTarget return 1; } -static inline OsrsEncounterArena pvp_build_arena(OsrsEnv* env) { +static inline OsrsEncounterArena pvp_build_arena( + OsrsEnv* env, + const EncounterArenaTopology* topology +) { OsrsEncounterArena arena; + arena.topology = topology; + arena.blockers = (EncounterRouteBlockers){0}; + arena.movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN; + arena.cost_policy = ENCOUNTER_ROUTE_COST_OSRS; + arena.explicit_cost_policy = ENCOUNTER_ROUTE_COST_DIRECT; arena.collision_map = (const CollisionMap*)env->collision_map; arena.world_offset_x = 0; arena.world_offset_y = 0; - arena.is_walkable = pvp_tile_walkable; - arena.walkable_ctx = (void*)arena.collision_map; - arena.extra_blocked = NULL; - arena.blocked_ctx = NULL; arena.los_query = osrs_los_open_query(); - arena.arena_base_x = 0; - arena.arena_base_y = 0; - arena.arena_w = 0; - arena.arena_h = 0; return arena; } -static inline OsrsPlayerStepResult pvp_step_player_movement(OsrsEnv* env, int agent_idx) { +static inline OsrsPlayerStepResult pvp_step_player_movement( + OsrsEnv* env, + int agent_idx, + const EncounterArenaTopology* topology, + OsrsActorRouteCache* route_cache +) { OsrsPlayerStepResult result = {.target_slot = -1}; int* dest_x = &env->pvp_runtime.walk_dest_x[agent_idx]; int* dest_y = &env->pvp_runtime.walk_dest_y[agent_idx]; @@ -304,10 +365,11 @@ static inline OsrsPlayerStepResult pvp_step_player_movement(OsrsEnv* env, int ag if (*dest_x < 0 || *dest_y < 0) return result; Player* p = &env->players[agent_idx]; - OsrsEncounterArena arena = pvp_build_arena(env); + OsrsEncounterArena arena = pvp_build_arena(env, topology); OsrsPlayerStepInput input = { .player = p, .interaction = &p->interaction, + .route_cache = route_cache, .target_lookup = pvp_lookup_attack_target, .target_ctx = env, .command = { diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h index 98478e87d8..e50c7c892d 100644 --- a/ocean/osrs/osrs_render.h +++ b/ocean/osrs/osrs_render.h @@ -485,6 +485,8 @@ typedef struct RenderClient { int effect_anim_state_count; const CollisionMap* collision_map; + const EncounterArenaTopology* route_topology; + OsrsActorRouteCache player_route_cache[NUM_AGENTS]; int collision_world_offset_x; int collision_world_offset_y; diff --git a/ocean/osrs/osrs_visual.c b/ocean/osrs/osrs_visual.c index 15c4eacb22..fac18d34f5 100644 --- a/ocean/osrs/osrs_visual.c +++ b/ocean/osrs/osrs_visual.c @@ -103,6 +103,9 @@ static void print_env_state(OsrsEnv* env) { } static void run_random_episode(OsrsEnv* env, int verbose) { + const EncounterArenaTopology* route_topology = + pvp_route_topology_setup((const CollisionMap*)env->collision_map); + OsrsActorRouteCache route_cache[NUM_AGENTS] = {0}; pvp_reset(env); while (!env->episode_over) { @@ -113,7 +116,7 @@ static void run_random_episode(OsrsEnv* env, int verbose) { } } - pvp_step(env); + pvp_step(env, route_topology, route_cache); if (verbose && env->tick % 50 == 0) { print_env_state(env); @@ -130,6 +133,9 @@ static void run_random_episode(OsrsEnv* env, int verbose) { } static void benchmark(OsrsEnv* env, int num_steps) { + const EncounterArenaTopology* route_topology = + pvp_route_topology_setup((const CollisionMap*)env->collision_map); + OsrsActorRouteCache route_cache[NUM_AGENTS] = {0}; printf("Benchmarking %d steps...\n", num_steps); clock_t start = clock(); @@ -138,6 +144,7 @@ static void benchmark(OsrsEnv* env, int num_steps) { while (total_steps < num_steps) { pvp_reset(env); + pvp_actor_route_caches_clear(route_cache); episodes++; while (!env->episode_over && total_steps < num_steps) { @@ -148,7 +155,7 @@ static void benchmark(OsrsEnv* env, int num_steps) { } } - pvp_step(env); + pvp_step(env, route_topology, route_cache); total_steps++; } } @@ -357,8 +364,10 @@ static void run_profile( OsrsEnv* env, const char* encounter_name, int start_wave, - int profile_steps + int profile_steps, + uint32_t profile_seed ) { + srand(profile_seed); if (profile_steps > 0) { printf("Profiling %s for %d steps...\n", encounter_name ? encounter_name : "pvp", @@ -380,13 +389,19 @@ static void run_profile( start_wave); fprintf(stderr, "start_wave: %d\n", start_wave); } - edef->reset(env->encounter_state, env->encounter_context, 0); + edef->reset(env->encounter_state, env->encounter_context, profile_seed); } else { env->pvp_runtime.use_c_opponent = 1; env->pvp_runtime.opponent.type = OPP_IMPROVED; + env->has_rng_seed = 1; + env->rng_seed = profile_seed; env->is_lms = 1; pvp_reset(env); } + const EncounterArenaTopology* direct_pvp_topology = encounter_name + ? NULL + : pvp_route_topology_setup((const CollisionMap*)env->collision_map); + OsrsActorRouteCache direct_pvp_route_cache[NUM_AGENTS] = {0}; const EncounterDef* profile_edef = (const EncounterDef*)env->encounter_def; float* encounter_obs = NULL; @@ -536,9 +551,10 @@ static void run_profile( actions[h] = rand() % ACTION_HEAD_DIMS[h]; } } - pvp_step(env); + pvp_step(env, direct_pvp_topology, direct_pvp_route_cache); if (env->episode_over) { pvp_reset(env); + pvp_actor_route_caches_clear(direct_pvp_route_cache); } } @@ -1550,7 +1566,7 @@ static void visual_frame(void* arg) { } } } - pvp_step(env); + pvp_step(env, rc->route_topology, rc->player_route_cache); if (rc->human_input.enabled && rc->human_input.pending_move_x >= 0) { Player* p0 = &env->players[0]; @@ -1961,6 +1977,11 @@ static void run_visual( pvp_render(env); RenderClient* rc = (RenderClient*)env->client; + rc->route_topology = + (!encounter_name || strcmp(encounter_name, "pvp") == 0) + ? pvp_route_topology_setup((const CollisionMap*)env->collision_map) + : NULL; + pvp_actor_route_caches_clear(rc->player_route_cache); #ifdef __EMSCRIPTEN__ if (!encounter_name || encounter_name_is_pvp(encounter_name)) { rc->ticks_per_second = 15.0f; @@ -2329,7 +2350,8 @@ int main(int argc, char** argv) { run_policy_profile(&env, encounter_name, start_wave, profile_steps, model_path, policy_mode, policy_seed, loadout_mode); } else { - run_profile(&env, encounter_name, start_wave, profile_steps); + run_profile( + &env, encounter_name, start_wave, profile_steps, policy_seed); } visual_free_env_buffers(&env); diff --git a/ocean/osrs/tests/bench_colo_step.c b/ocean/osrs/tests/bench_colo_step.c index 7824b54efc..97d79185ed 100644 --- a/ocean/osrs/tests/bench_colo_step.c +++ b/ocean/osrs/tests/bench_colo_step.c @@ -97,6 +97,7 @@ static Bench run_multi_sim_only(int num_envs, int steps_per_env) { uint64_t rng = 0xABCDEF01ULL; for (int e = 0; e < num_envs; e++) { col_init_context_typed(&ctxs[e]); + col_bind_route_topology(&ctxs[e], NULL); col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], (uint32_t)(e + 1)); } double t0 = now_ns(); @@ -129,6 +130,7 @@ static Bench run_multi(int num_envs, int steps_per_env) { for (int e = 0; e < num_envs; e++) { col_init_context_typed(&ctxs[e]); + col_bind_route_topology(&ctxs[e], NULL); col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], (uint32_t)(e + 1)); } for (int rep = 0; rep < 40; rep++) { @@ -165,6 +167,7 @@ static Bench run(int warmup_steps, int measured_steps) { Bench b = {0}; col_init_context_typed(&ctx); + col_bind_route_topology(&ctx, NULL); memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 12345u); uint64_t rng = 0xBEEF1234ULL; diff --git a/ocean/osrs/tests/bench_colosseum_forecast_profile.c b/ocean/osrs/tests/bench_colosseum_forecast_profile.c index 50dce74465..cab0fc541f 100644 --- a/ocean/osrs/tests/bench_colosseum_forecast_profile.c +++ b/ocean/osrs/tests/bench_colosseum_forecast_profile.c @@ -123,6 +123,7 @@ static void bench_trace_actions( static void bench_init_context(ColosseumContext* ctx, int start_wave) { col_init_context_typed(ctx); + col_bind_route_topology(ctx, NULL); ctx->config.start_wave = start_wave; ctx->config.step_out_forecast_obs_enabled = 1; ctx->config.forecast_horizon = COLO_STEP_OUT_FORECAST_HORIZON; diff --git a/ocean/osrs/tests/osrs_route_reference.h b/ocean/osrs/tests/osrs_route_reference.h new file mode 100644 index 0000000000..8d24d80444 --- /dev/null +++ b/ocean/osrs/tests/osrs_route_reference.h @@ -0,0 +1,1679 @@ +#ifndef OSRS_ROUTE_REFERENCE_H +#define OSRS_ROUTE_REFERENCE_H + +#define VIA_NONE 0 +#define VIA_S 1 +#define VIA_W 2 +#define VIA_SW 3 +#define VIA_N 4 +#define VIA_NW 6 +#define VIA_E 8 +#define VIA_SE 9 +#define VIA_NE 12 +#define VIA_START 99 + +typedef struct { + int found; + int next_dx; + int next_dy; + int dest_x; + int dest_y; + int run_dx; + int run_dy; +} PathResult; + +typedef int (*pathfind_blocked_fn)(void* ctx, int abs_x, int abs_y); + +static inline void pathfind_enqueue_or_abort( + int* queue_x, int* queue_y, int* tail, int capacity, int x, int y +) { + if (*tail >= capacity) { + fprintf(stderr, "pathfind queue overflow: capacity=%d\n", capacity); + abort(); + } + + queue_x[*tail] = x; + queue_y[*tail] = y; + (*tail)++; +} + +static const int pathfind_dir_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; +static const int pathfind_dir_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; +static const int pathfind_dir_via[8] = { + VIA_S, VIA_W, VIA_N, VIA_E, VIA_SW, VIA_NW, VIA_SE, VIA_NE +}; + +static inline PathResult pathfind_step(const CollisionMap* map, int height, + int src_x, int src_y, int dest_x, int dest_y, + pathfind_blocked_fn extra_blocked, void* blocked_ctx) { + PathResult result = {0, 0, 0, dest_x, dest_y}; + + if (src_x == dest_x && src_y == dest_y) { + result.found = 1; + return result; + } + + int dist = abs(src_x - dest_x); + int dy_abs = abs(src_y - dest_y); + if (dy_abs > dist) dist = dy_abs; + if (dist > 64) { + return result; + } + + int origin_x = ((src_x >> 3) - 6) << 3; + int origin_y = ((src_y >> 3) - 6) << 3; + + int local_src_x = src_x - origin_x; + int local_src_y = src_y - origin_y; + int local_dest_x = dest_x - origin_x; + int local_dest_y = dest_y - origin_y; + + if (local_dest_x < 0 || local_dest_x >= PATHFIND_GRID_SIZE || + local_dest_y < 0 || local_dest_y >= PATHFIND_GRID_SIZE) { + return result; + } + + int via[PATHFIND_GRID_SIZE][PATHFIND_GRID_SIZE]; + int cost[PATHFIND_GRID_SIZE][PATHFIND_GRID_SIZE]; + memset(via, 0, sizeof(via)); + memset(cost, 0, sizeof(cost)); + + int queue_x[PATHFIND_MAX_QUEUE_FULL]; + int queue_y[PATHFIND_MAX_QUEUE_FULL]; + int head = 0; + int tail = 0; + + via[local_src_x][local_src_y] = VIA_START; + cost[local_src_x][local_src_y] = 1; + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_FULL, local_src_x, local_src_y); + + int found_path = 0; + int cur_x, cur_y; + + while (head < tail) { + cur_x = queue_x[head]; + cur_y = queue_y[head]; + head++; + + if (cur_x == local_dest_x && cur_y == local_dest_y) { + found_path = 1; + break; + } + + int abs_x = origin_x + cur_x; + int abs_y = origin_y + cur_y; + int next_cost = cost[cur_x][cur_y] + 1; + + #define EB(ax, ay) (extra_blocked && extra_blocked(blocked_ctx, (ax), (ay))) + + for (int i = 0; i < 8; i++) { + int dx = pathfind_dir_dx[i]; + int dy = pathfind_dir_dy[i]; + int next_x = cur_x + dx; + int next_y = cur_y + dy; + if (next_x < 0 || next_x >= PATHFIND_GRID_SIZE || + next_y < 0 || next_y >= PATHFIND_GRID_SIZE) + continue; + if (via[next_x][next_y] != 0) continue; + if (!collision_traversable_step(map, height, abs_x, abs_y, dx, dy)) + continue; + if (dx != 0 && dy != 0) { + if (!collision_traversable_step(map, height, abs_x, abs_y, 0, dy)) + continue; + if (!collision_traversable_step(map, height, abs_x, abs_y, dx, 0)) + continue; + } + if (EB(abs_x + dx, abs_y + dy)) continue; + if (dx != 0 && dy != 0) { + if (EB(abs_x, abs_y + dy)) continue; + if (EB(abs_x + dx, abs_y)) continue; + } + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_FULL, next_x, next_y); + via[next_x][next_y] = pathfind_dir_via[i]; + cost[next_x][next_y] = next_cost; + } + + #undef EB + } + + if (!found_path) { + int best_manhattan = PATHFIND_GRID_SIZE * 2; + int best_cost = 999999; + int best_x = -1, best_y = -1; + + for (int fx = 0; fx < PATHFIND_GRID_SIZE; fx++) { + for (int fy = 0; fy < PATHFIND_GRID_SIZE; fy++) { + if (cost[fx][fy] == 0) continue; + + int ddx = fx - local_dest_x; + int ddy = fy - local_dest_y; + int manhattan = (ddx < 0 ? -ddx : ddx) + (ddy < 0 ? -ddy : ddy); + + if (manhattan < best_manhattan || + (manhattan == best_manhattan && cost[fx][fy] < best_cost)) { + best_manhattan = manhattan; + best_cost = cost[fx][fy]; + best_x = fx; + best_y = fy; + } + } + } + + if (best_x == -1) { + return result; + } + + cur_x = best_x; + cur_y = best_y; + found_path = 1; + result.dest_x = origin_x + best_x; + result.dest_y = origin_y + best_y; + } + + while (1) { + int v = via[cur_x][cur_y]; + int prev_x = cur_x; + int prev_y = cur_y; + + if (v & VIA_W) prev_x++; + else if (v & VIA_E) prev_x--; + + if (v & VIA_S) prev_y++; + else if (v & VIA_N) prev_y--; + + if (prev_x == local_src_x && prev_y == local_src_y) { + result.found = 1; + result.next_dx = cur_x - local_src_x; + result.next_dy = cur_y - local_src_y; + return result; + } + + cur_x = prev_x; + cur_y = prev_y; + + if (via[cur_x][cur_y] == VIA_NONE || via[cur_x][cur_y] == VIA_START) { + break; + } + } + + return result; +} + +static inline PathResult pathfind_step_arena( + const CollisionMap* map, int height, + int src_x, int src_y, int dest_x, int dest_y, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + int arena_origin_x, int arena_origin_y, int arena_w, int arena_h +) { + PathResult result = {0, 0, 0, dest_x, dest_y}; + + if (arena_w <= 0 || arena_w > PATHFIND_ARENA_MAX || + arena_h <= 0 || arena_h > PATHFIND_ARENA_MAX) { + fprintf(stderr, "pathfind arena dimensions out of bounds: %dx%d\n", arena_w, arena_h); + abort(); + } + + if (src_x == dest_x && src_y == dest_y) { + result.found = 1; + return result; + } + + int local_src_x = src_x - arena_origin_x; + int local_src_y = src_y - arena_origin_y; + int local_dest_x = dest_x - arena_origin_x; + int local_dest_y = dest_y - arena_origin_y; + + if (local_src_x < 0 || local_src_x >= arena_w || + local_src_y < 0 || local_src_y >= arena_h || + local_dest_x < 0 || local_dest_x >= arena_w || + local_dest_y < 0 || local_dest_y >= arena_h) { + return result; + } + + static OSRS_THREAD_LOCAL uint16_t bfs_gen[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + static OSRS_THREAD_LOCAL int8_t bfs_via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + static OSRS_THREAD_LOCAL int16_t bfs_cost[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + static OSRS_THREAD_LOCAL uint16_t bfs_gen_counter = 0; + bfs_gen_counter++; + if (bfs_gen_counter == 0) { + memset(bfs_gen, 0, sizeof(bfs_gen)); + bfs_gen_counter = 1; + } + uint16_t gen = bfs_gen_counter; + #define BFS_VISITED(x, y) (bfs_gen[(x)][(y)] == gen) + #define BFS_VISIT(x, y, v, c) do { \ + bfs_gen[(x)][(y)] = gen; bfs_via[(x)][(y)] = (v); bfs_cost[(x)][(y)] = (c); \ + } while(0) + #define BFS_VIA(x, y) bfs_via[(x)][(y)] + #define BFS_COST(x, y) bfs_cost[(x)][(y)] + + int queue_x[PATHFIND_MAX_QUEUE_ARENA]; + int queue_y[PATHFIND_MAX_QUEUE_ARENA]; + int head = 0, tail = 0; + + BFS_VISIT(local_src_x, local_src_y, VIA_START, 1); + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, local_src_x, local_src_y); + + int found_path = 0; + int cur_x, cur_y; + + while (head < tail) { + cur_x = queue_x[head]; + cur_y = queue_y[head]; + head++; + + if (cur_x == local_dest_x && cur_y == local_dest_y) { + found_path = 1; + break; + } + + int abs_x = arena_origin_x + cur_x; + int abs_y = arena_origin_y + cur_y; + int next_cost = BFS_COST(cur_x, cur_y) + 1; + + #define EB(ax, ay) (extra_blocked && extra_blocked(blocked_ctx, (ax), (ay))) + + for (int i = 0; i < 8; i++) { + int dx = pathfind_dir_dx[i]; + int dy = pathfind_dir_dy[i]; + int next_x = cur_x + dx; + int next_y = cur_y + dy; + if (next_x < 0 || next_x >= arena_w || + next_y < 0 || next_y >= arena_h) + continue; + if (BFS_VISITED(next_x, next_y)) continue; + if (!collision_traversable_step(map, height, abs_x, abs_y, dx, dy)) + continue; + if (dx != 0 && dy != 0) { + if (!collision_traversable_step(map, height, abs_x, abs_y, 0, dy)) + continue; + if (!collision_traversable_step(map, height, abs_x, abs_y, dx, 0)) + continue; + } + if (EB(abs_x + dx, abs_y + dy)) continue; + if (dx != 0 && dy != 0) { + if (EB(abs_x, abs_y + dy)) continue; + if (EB(abs_x + dx, abs_y)) continue; + } + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, next_x, next_y); + BFS_VISIT(next_x, next_y, pathfind_dir_via[i], next_cost); + } + + #undef EB + } + + if (!found_path) { + int best_manhattan = arena_w + arena_h; + int best_cost = 999999; + int best_x = -1, best_y = -1; + + for (int fx = 0; fx < arena_w; fx++) { + for (int fy = 0; fy < arena_h; fy++) { + if (!BFS_VISITED(fx, fy) || BFS_COST(fx, fy) == 0) continue; + int ddx = fx - local_dest_x, ddy = fy - local_dest_y; + int manhattan = (ddx < 0 ? -ddx : ddx) + (ddy < 0 ? -ddy : ddy); + if (manhattan < best_manhattan || + (manhattan == best_manhattan && BFS_COST(fx, fy) < best_cost)) { + best_manhattan = manhattan; + best_cost = BFS_COST(fx, fy); + best_x = fx; best_y = fy; + } + } + } + + if (best_x == -1) return result; + cur_x = best_x; cur_y = best_y; + found_path = 1; + result.dest_x = arena_origin_x + best_x; + result.dest_y = arena_origin_y + best_y; + } + + while (1) { + if (!BFS_VISITED(cur_x, cur_y)) break; + int v = BFS_VIA(cur_x, cur_y); + if (v == VIA_NONE || v == VIA_START) break; + int prev_x = cur_x, prev_y = cur_y; + if (v & VIA_W) prev_x++; else if (v & VIA_E) prev_x--; + if (v & VIA_S) prev_y++; else if (v & VIA_N) prev_y--; + if (prev_x == local_src_x && prev_y == local_src_y) { + result.found = 1; + result.next_dx = cur_x - local_src_x; + result.next_dy = cur_y - local_src_y; + return result; + } + if (prev_x < 0 || prev_x >= arena_w || prev_y < 0 || prev_y >= arena_h) break; + cur_x = prev_x; cur_y = prev_y; + } + + return result; +} + + +static inline PathResult encounter_pathfind( + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + int src_x, int src_y, int dst_x, int dst_y, + pathfind_blocked_fn extra_blocked, void* blocked_ctx +) { + return pathfind_step(cmap, 0, + src_x + world_offset_x, src_y + world_offset_y, + dst_x + world_offset_x, dst_y + world_offset_y, + extra_blocked, blocked_ctx); +} + +static inline PathResult encounter_pathfind_arena( + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + int src_x, int src_y, int dst_x, int dst_y, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + int arena_base_x, int arena_base_y, int arena_w, int arena_h +) { + return pathfind_step_arena(cmap, 0, + src_x + world_offset_x, src_y + world_offset_y, + dst_x + world_offset_x, dst_y + world_offset_y, + extra_blocked, blocked_ctx, + arena_base_x + world_offset_x, arena_base_y + world_offset_y, + arena_w, arena_h); +} + +static inline int encounter_walk_toward( + Player* p, int tx, int ty, + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + encounter_walkable_fn is_walkable, void* ctx, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + int arena_base_x, int arena_base_y, int arena_w, int arena_h +) { + int steps = 0; + for (int step = 0; step < 2; step++) { + if (p->x == tx && p->y == ty) break; + PathResult pr = (arena_w > 0) + ? encounter_pathfind_arena(cmap, world_offset_x, world_offset_y, + p->x, p->y, tx, ty, + extra_blocked, blocked_ctx, + arena_base_x, arena_base_y, arena_w, arena_h) + : encounter_pathfind(cmap, world_offset_x, world_offset_y, + p->x, p->y, tx, ty, + extra_blocked, blocked_ctx); + if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) break; + int nx = p->x + pr.next_dx, ny = p->y + pr.next_dy; + if (!is_walkable(ctx, nx, ny)) break; + p->x = nx; p->y = ny; + steps++; + } + p->is_running = (steps == 2); + p->dest_x = p->x; p->dest_y = p->y; + return steps; +} + +static inline int encounter_move_toward_dest( + Player* p, int* dest_x, int* dest_y, + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + encounter_walkable_fn is_walkable, void* ctx, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + int arena_base_x, int arena_base_y, int arena_w, int arena_h +) { + if (*dest_x < 0 || *dest_y < 0) return 0; + if (p->x == *dest_x && p->y == *dest_y) { + *dest_x = -1; *dest_y = -1; + return 0; + } + return encounter_walk_toward(p, *dest_x, *dest_y, + cmap, world_offset_x, world_offset_y, + is_walkable, ctx, extra_blocked, blocked_ctx, + arena_base_x, arena_base_y, arena_w, arena_h); +} + +static inline int encounter_target_rect_distance_squared( + int x, + int y, + int target_x, + int target_y, + int target_size +) { + int target_max_x = target_x + target_size - 1; + int target_max_y = target_y + target_size - 1; + int dx = x < target_x + ? target_x - x + : (x > target_max_x ? x - target_max_x : 0); + int dy = y < target_y + ? target_y - y + : (y > target_max_y ? y - target_max_y : 0); + return dx * dx + dy * dy; +} + + +typedef struct { + const CollisionMap* collision_map; + encounter_walkable_fn is_walkable; + void* walkable_ctx; + pathfind_blocked_fn extra_blocked; + void* blocked_ctx; + int world_offset_x; + int world_offset_y; + int arena_base_x; + int arena_base_y; + int arena_w; + int arena_h; + int source_x; + int source_y; + int target_edge_local_x; + int target_edge_local_y; + int min_explored_x; + int min_explored_y; + int max_explored_x; + int max_explored_y; + uint64_t visited[PATHFIND_ARENA_MAX]; + uint16_t depth[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + int8_t via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + int visited_count; +} EncounterArenaAttackRouteField; + +typedef struct { + int land_x; + int land_y; + int route_found; + int route_x; + int route_y; +} EncounterAttackRouteLanding; + + +static inline int encounter_attack_route_step_traversable( + const CollisionMap* cmap, + int world_offset_x, + int world_offset_y, + int x, + int y, + int dx, + int dy, + encounter_walkable_fn is_walkable, + void* walkable_ctx, + pathfind_blocked_fn extra_blocked, + void* blocked_ctx +) { + int next_x = x + dx; + int next_y = y + dy; + if (!is_walkable(walkable_ctx, next_x, next_y)) return 0; + if (dx != 0 && dy != 0 && + (!is_walkable(walkable_ctx, next_x, y) || + !is_walkable(walkable_ctx, x, next_y))) { + return 0; + } + if (!collision_traversable_step( + cmap, 0, x + world_offset_x, y + world_offset_y, dx, dy)) { + return 0; + } + if (extra_blocked && + extra_blocked( + blocked_ctx, + next_x + world_offset_x, + next_y + world_offset_y)) { + return 0; + } + if (dx == 0 || dy == 0) return 1; + if (extra_blocked && + (extra_blocked( + blocked_ctx, + next_x + world_offset_x, + y + world_offset_y) || + extra_blocked( + blocked_ctx, + x + world_offset_x, + next_y + world_offset_y))) { + return 0; + } + return 1; +} +typedef struct { + encounter_walkable_fn is_walkable; + void* walkable_ctx; + int arena_base_x; + int arena_base_y; + int arena_w; + int arena_h; + uint64_t known[PATHFIND_ARENA_MAX]; + uint64_t walkable[PATHFIND_ARENA_MAX]; +} EncounterAttackRouteWalkableCache; + +static inline int encounter_attack_route_cached_walkable( + void* data, + int x, + int y +) { + EncounterAttackRouteWalkableCache* cache = + (EncounterAttackRouteWalkableCache*)data; + int local_x = x - cache->arena_base_x; + int local_y = y - cache->arena_base_y; + if (local_x < 0 || local_x >= cache->arena_w || + local_y < 0 || local_y >= cache->arena_h) { + return cache->is_walkable(cache->walkable_ctx, x, y); + } + uint64_t bit = 1ULL << local_y; + if ((cache->known[local_x] & bit) == 0) { + cache->known[local_x] |= bit; + if (cache->is_walkable(cache->walkable_ctx, x, y)) + cache->walkable[local_x] |= bit; + } + return (cache->walkable[local_x] & bit) != 0; +} +static inline void encounter_attack_route_mark_target_edge( + uint64_t target_edges[PATHFIND_ARENA_MAX], + const CollisionMap* cmap, + int world_offset_x, + int world_offset_y, + int edge_x, + int edge_y, + int target_x, + int target_y, + int target_size, + int arena_base_x, + int arena_base_y, + int arena_w, + int arena_h +) { + int local_x = edge_x - arena_base_x; + int local_y = edge_y - arena_base_y; + if (local_x < 0 || local_x >= arena_w || + local_y < 0 || local_y >= arena_h) { + return; + } + if (encounter_entity_footprint_cardinal_reachable( + cmap, + world_offset_x, + world_offset_y, + edge_x, + edge_y, + target_x, + target_y, + target_size)) { + target_edges[local_x] |= 1ULL << local_y; + } +} + + + + +static inline void encounter_build_arena_attack_route_field( + EncounterArenaAttackRouteField* field, + const CollisionMap* cmap, + int world_offset_x, + int world_offset_y, + int source_x, + int source_y, + int target_x, + int target_y, + int target_size, + encounter_walkable_fn is_walkable, + void* walkable_ctx, + pathfind_blocked_fn extra_blocked, + void* blocked_ctx, + int arena_base_x, + int arena_base_y, + int arena_w, + int arena_h +) { + if (!field || !is_walkable || target_size <= 0) { + fprintf(stderr, "attack route field is missing required input\n"); + abort(); + } + if (arena_w <= 0 || arena_w > PATHFIND_ARENA_MAX || + arena_h <= 0 || arena_h > PATHFIND_ARENA_MAX) { + fprintf(stderr, "attack route arena dimensions out of bounds: %dx%d\n", + arena_w, arena_h); + abort(); + } + + int local_source_x = source_x - arena_base_x; + int local_source_y = source_y - arena_base_y; + if (local_source_x < 0 || local_source_x >= arena_w || + local_source_y < 0 || local_source_y >= arena_h) { + fprintf(stderr, + "attack route source out of arena: source=(%d,%d) arena=(%d,%d,%d,%d)\n", + source_x, source_y, arena_base_x, arena_base_y, arena_w, arena_h); + abort(); + } + + memset( + field->visited, + 0, + (size_t)arena_w * sizeof(field->visited[0])); + field->target_edge_local_x = -1; + field->target_edge_local_y = -1; + field->collision_map = cmap; + field->is_walkable = is_walkable; + field->walkable_ctx = walkable_ctx; + field->extra_blocked = extra_blocked; + field->blocked_ctx = blocked_ctx; + field->world_offset_x = world_offset_x; + field->world_offset_y = world_offset_y; + field->arena_base_x = arena_base_x; + field->arena_base_y = arena_base_y; + field->arena_w = arena_w; + field->arena_h = arena_h; + field->source_x = source_x; + field->source_y = source_y; + field->min_explored_x = local_source_x; + field->min_explored_y = local_source_y; + field->max_explored_x = local_source_x; + field->max_explored_y = local_source_y; + EncounterAttackRouteWalkableCache walkable_cache = { + .is_walkable = is_walkable, + .walkable_ctx = walkable_ctx, + .arena_base_x = arena_base_x, + .arena_base_y = arena_base_y, + .arena_w = arena_w, + .arena_h = arena_h, + }; + uint64_t target_edges[PATHFIND_ARENA_MAX] = {0}; + for (int offset = 0; offset < target_size; offset++) { + encounter_attack_route_mark_target_edge( + target_edges, cmap, world_offset_x, world_offset_y, + target_x - 1, target_y + offset, + target_x, target_y, target_size, + arena_base_x, arena_base_y, arena_w, arena_h); + encounter_attack_route_mark_target_edge( + target_edges, cmap, world_offset_x, world_offset_y, + target_x + target_size, target_y + offset, + target_x, target_y, target_size, + arena_base_x, arena_base_y, arena_w, arena_h); + encounter_attack_route_mark_target_edge( + target_edges, cmap, world_offset_x, world_offset_y, + target_x + offset, target_y - 1, + target_x, target_y, target_size, + arena_base_x, arena_base_y, arena_w, arena_h); + encounter_attack_route_mark_target_edge( + target_edges, cmap, world_offset_x, world_offset_y, + target_x + offset, target_y + target_size, + target_x, target_y, target_size, + arena_base_x, arena_base_y, arena_w, arena_h); + } + + + + + uint16_t queue[PATHFIND_MAX_QUEUE_ARENA]; + int head = 0; + field->visited_count = 1; + field->visited[local_source_x] = 1ULL << local_source_y; + field->depth[local_source_x][local_source_y] = 0; + field->via[local_source_x][local_source_y] = VIA_START; + queue[0] = (uint16_t)((local_source_x << 8) | local_source_y); + + static const int route_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int route_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; + static const int route_via[8] = { + VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE + }; + + while (head < field->visited_count) { + uint16_t packed_tile = queue[head++]; + int cur_x = packed_tile >> 8; + int cur_y = packed_tile & 0xff; + + int tile_x = arena_base_x + cur_x; + int tile_y = arena_base_y + cur_y; + if (target_edges[cur_x] & (1ULL << cur_y)) { + field->target_edge_local_x = cur_x; + field->target_edge_local_y = cur_y; + break; + } + uint16_t next_depth = field->depth[cur_x][cur_y] + 1; + + for (int i = 0; i < 8; i++) { + int dx = route_dx[i]; + int dy = route_dy[i]; + int next_x = cur_x + dx; + int next_y = cur_y + dy; + if (next_x < 0 || next_x >= arena_w || + next_y < 0 || next_y >= arena_h) { + continue; + } + if (field->visited[next_x] & (1ULL << next_y)) continue; + if (!encounter_attack_route_step_traversable( + cmap, + world_offset_x, + world_offset_y, + tile_x, + tile_y, + dx, + dy, + encounter_attack_route_cached_walkable, + &walkable_cache, + extra_blocked, + blocked_ctx)) { + continue; + } + + queue[field->visited_count] = + (uint16_t)((next_x << 8) | next_y); + field->visited_count++; + field->visited[next_x] |= 1ULL << next_y; + field->depth[next_x][next_y] = next_depth; + field->via[next_x][next_y] = (int8_t)route_via[i]; + if (next_x < field->min_explored_x) field->min_explored_x = next_x; + if (next_y < field->min_explored_y) field->min_explored_y = next_y; + if (next_x > field->max_explored_x) field->max_explored_x = next_x; + if (next_y > field->max_explored_y) field->max_explored_y = next_y; + } + } +} + +static inline EncounterAttackRouteLanding +encounter_attack_route_overlap_landing( + const EncounterArenaAttackRouteField* field, + int target_x, + int target_y, + int target_size +) { + Player player = { + .x = field->source_x, + .y = field->source_y, + }; + int max_r = (target_size + 1) / 2 + 1; + int best_dsq = 9999; + int best_x = -1; + int best_y = -1; + for (int dy = -max_r; dy <= max_r; dy++) { + for (int dx = -max_r; dx <= max_r; dx++) { + if (dx == 0 && dy == 0) continue; + int x = player.x + dx; + int y = player.y + dy; + if (!field->is_walkable(field->walkable_ctx, x, y)) continue; + if (encounter_entity_footprints_overlap( + x, y, 1, target_x, target_y, target_size)) { + continue; + } + int dsq = dx * dx + dy * dy; + if (dsq < best_dsq) { + best_dsq = dsq; + best_x = x; + best_y = y; + } + } + } + if (best_x >= 0) { + encounter_walk_toward( + &player, best_x, best_y, + field->collision_map, + field->world_offset_x, + field->world_offset_y, + field->is_walkable, + field->walkable_ctx, + field->extra_blocked, + field->blocked_ctx, + field->arena_base_x, + field->arena_base_y, + field->arena_w, + field->arena_h); + } + return (EncounterAttackRouteLanding){ + .land_x = player.x, + .land_y = player.y, + .route_found = player.x != field->source_x || + player.y != field->source_y, + .route_x = player.x, + .route_y = player.y, + }; +} + +static inline EncounterAttackRouteLanding encounter_arena_attack_route_landing( + const EncounterArenaAttackRouteField* field, + int target_x, + int target_y, + int target_size, + int attack_range, + const OsrsLosQuery* los_query +) { + if (!field || !field->is_walkable || + field->arena_w <= 0 || field->arena_w > PATHFIND_ARENA_MAX || + field->arena_h <= 0 || field->arena_h > PATHFIND_ARENA_MAX || + field->visited_count <= 0 || + target_size <= 0 || attack_range <= 0) { + fprintf(stderr, "invalid attack route landing query\n"); + abort(); + } + + EncounterAttackRouteLanding landing = { + .land_x = field->source_x, + .land_y = field->source_y, + .route_x = field->source_x, + .route_y = field->source_y, + }; + int dist = encounter_rect_distance( + field->source_x, field->source_y, 1, + target_x, target_y, target_size); + if (dist == 0) { + return encounter_attack_route_overlap_landing( + field, target_x, target_y, target_size); + } + if (encounter_player_can_attack( + field->source_x, field->source_y, + target_x, target_y, target_size, attack_range, + field->collision_map, field->world_offset_x, field->world_offset_y, + los_query)) { + landing.route_found = 1; + return landing; + } + + + int selected_x = field->target_edge_local_x; + int selected_y = field->target_edge_local_y; + + if (selected_x < 0) { + int target_local_x = target_x - field->arena_base_x; + int target_local_y = target_y - field->arena_base_y; + int scan_min_x = target_local_x - PATHFIND_MAX_FALLBACK_RADIUS; + int scan_min_y = target_local_y - PATHFIND_MAX_FALLBACK_RADIUS; + int scan_max_x = + target_local_x + target_size - 1 + PATHFIND_MAX_FALLBACK_RADIUS; + int scan_max_y = + target_local_y + target_size - 1 + PATHFIND_MAX_FALLBACK_RADIUS; + if (scan_min_x < field->min_explored_x) + scan_min_x = field->min_explored_x; + if (scan_min_y < field->min_explored_y) + scan_min_y = field->min_explored_y; + if (scan_max_x > field->max_explored_x) + scan_max_x = field->max_explored_x; + if (scan_max_y > field->max_explored_y) + scan_max_y = field->max_explored_y; + + int best_dsq = 0x3fffffff; + int best_depth = 100; + for (int x = scan_min_x; x <= scan_max_x; x++) { + for (int y = scan_min_y; y <= scan_max_y; y++) { + if ((field->visited[x] & (1ULL << y)) == 0) continue; + int tile_x = field->arena_base_x + x; + int tile_y = field->arena_base_y + y; + int depth = field->depth[x][y]; + if (depth >= 100) continue; + int dsq = encounter_target_rect_distance_squared( + tile_x, tile_y, target_x, target_y, target_size); + if (dsq < best_dsq || + (dsq == best_dsq && depth < best_depth)) { + selected_x = x; + selected_y = y; + best_dsq = dsq; + best_depth = depth; + } + } + } + } + if (selected_x < 0) return landing; + landing.route_found = 1; + landing.route_x = field->arena_base_x + selected_x; + landing.route_y = field->arena_base_y + selected_y; + + int source_local_x = field->source_x - field->arena_base_x; + int source_local_y = field->source_y - field->arena_base_y; + int first_x = source_local_x; + int first_y = source_local_y; + int second_x = source_local_x; + int second_y = source_local_y; + int cur_x = selected_x; + int cur_y = selected_y; + uint16_t selected_depth = field->depth[selected_x][selected_y]; + while (field->depth[cur_x][cur_y] > 0) { + uint16_t depth = field->depth[cur_x][cur_y]; + if (depth == 1) { + first_x = cur_x; + first_y = cur_y; + } else if (depth == 2) { + second_x = cur_x; + second_y = cur_y; + } + + int via = field->via[cur_x][cur_y]; + if (via == VIA_NONE || via == VIA_START) { + fprintf(stderr, + "broken attack route parent at local tile (%d,%d)\n", + cur_x, cur_y); + abort(); + } + if (via & VIA_W) cur_x++; + else if (via & VIA_E) cur_x--; + if (via & VIA_S) cur_y++; + else if (via & VIA_N) cur_y--; + if (cur_x < 0 || cur_x >= field->arena_w || + cur_y < 0 || cur_y >= field->arena_h) { + fprintf(stderr, "attack route parent left arena\n"); + abort(); + } + } + if (cur_x != source_local_x || cur_y != source_local_y) { + fprintf(stderr, "attack route did not terminate at source\n"); + abort(); + } + if (selected_depth == 0) return landing; + + landing.land_x = field->arena_base_x + first_x; + landing.land_y = field->arena_base_y + first_y; + if (encounter_player_can_attack( + landing.land_x, landing.land_y, + target_x, target_y, target_size, attack_range, + field->collision_map, field->world_offset_x, field->world_offset_y, + los_query)) { + return landing; + } + if (selected_depth >= 2) { + landing.land_x = field->arena_base_x + second_x; + landing.land_y = field->arena_base_y + second_y; + } + return landing; +} + +#define ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS 25 + +static inline int encounter_attack_route_compress_waypoints( + const EncounterArenaAttackRouteField* field, + const EncounterAttackRouteLanding* landing, + int waypoint_x[ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS], + int waypoint_y[ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS] +) { + if (!landing->route_found || + (landing->route_x == field->source_x && + landing->route_y == field->source_y)) { + return 0; + } + + int source_x = field->source_x - field->arena_base_x; + int source_y = field->source_y - field->arena_base_y; + int current_x = landing->route_x - field->arena_base_x; + int current_y = landing->route_y - field->arena_base_y; + int current_direction = -1; + int waypoint_count = 0; + + while (current_x != source_x || current_y != source_y) { + int next_direction = field->via[current_x][current_y]; + if (next_direction == VIA_NONE || next_direction == VIA_START) { + fprintf(stderr, "broken attack route during compression\n"); + abort(); + } + if (current_direction != next_direction) { + current_direction = next_direction; + if (waypoint_count == ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS) + waypoint_count--; + memmove( + &waypoint_x[1], + &waypoint_x[0], + (size_t)waypoint_count * sizeof(waypoint_x[0])); + memmove( + &waypoint_y[1], + &waypoint_y[0], + (size_t)waypoint_count * sizeof(waypoint_y[0])); + waypoint_x[0] = field->arena_base_x + current_x; + waypoint_y[0] = field->arena_base_y + current_y; + waypoint_count++; + } + + if (current_direction & VIA_W) current_x++; + else if (current_direction & VIA_E) current_x--; + if (current_direction & VIA_S) current_y++; + else if (current_direction & VIA_N) current_y--; + } + return waypoint_count; +} + +static inline PathResult encounter_pathfind_arena_attack_approach( + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + int src_x, int src_y, + int target_x, int target_y, int target_size, int attack_range, + encounter_walkable_fn is_walkable, void* ctx, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + const OsrsLosQuery* los_query, + int arena_base_x, int arena_base_y, int arena_w, int arena_h +) { + PathResult result = {0, 0, 0, src_x, src_y}; + int local_src_x = src_x - arena_base_x; + int local_src_y = src_y - arena_base_y; + if (local_src_x < 0 || local_src_x >= arena_w || + local_src_y < 0 || local_src_y >= arena_h) { + return result; + } + + EncounterArenaAttackRouteField field; + encounter_build_arena_attack_route_field( + &field, + cmap, + world_offset_x, + world_offset_y, + src_x, + src_y, + target_x, + target_y, + target_size, + is_walkable, + ctx, + extra_blocked, + blocked_ctx, + arena_base_x, + arena_base_y, + arena_w, + arena_h); + EncounterAttackRouteLanding landing = + encounter_arena_attack_route_landing( + &field, + target_x, + target_y, + target_size, + attack_range, + los_query); + + int landing_x = landing.land_x - arena_base_x; + int landing_y = landing.land_y - arena_base_y; + if (landing_x < 0 || landing_x >= arena_w || + landing_y < 0 || landing_y >= arena_h) { + return result; + } + uint16_t landing_depth = field.depth[landing_x][landing_y]; + if (landing_depth == 0) { + result.found = encounter_player_can_attack( + src_x, src_y, + target_x, target_y, target_size, attack_range, + cmap, world_offset_x, world_offset_y, + los_query); + return result; + } + if (landing_depth > 2) { + fprintf(stderr, "attack route landing exceeded one run tick: %u\n", + (unsigned)landing_depth); + abort(); + } + + result.found = 1; + result.dest_x = landing.land_x; + result.dest_y = landing.land_y; + if (landing_depth == 1) { + result.next_dx = landing.land_x - src_x; + result.next_dy = landing.land_y - src_y; + return result; + } + + int first_x = landing_x; + int first_y = landing_y; + int via = field.via[landing_x][landing_y]; + if (via & VIA_W) first_x++; + else if (via & VIA_E) first_x--; + if (via & VIA_S) first_y++; + else if (via & VIA_N) first_y--; + result.next_dx = arena_base_x + first_x - src_x; + result.next_dy = arena_base_y + first_y - src_y; + result.run_dx = landing_x - first_x; + result.run_dy = landing_y - first_y; + return result; +} + +static inline int encounter_chase_attack_target( + Player* p, int target_x, int target_y, int target_size, int attack_range, + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + encounter_walkable_fn is_walkable, void* ctx, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + const OsrsLosQuery* los_query, + int arena_base_x, int arena_base_y, int arena_w, int arena_h +) { + int dist = encounter_rect_distance(p->x, p->y, 1, + target_x, target_y, target_size); + + if (dist == 0) { + int max_r = (target_size + 1) / 2 + 1; + int best_dsq = 9999, bx = -1, by = -1; + for (int dy = -max_r; dy <= max_r; dy++) { + for (int dx = -max_r; dx <= max_r; dx++) { + if (dx == 0 && dy == 0) continue; + int nx = p->x + dx, ny = p->y + dy; + if (!is_walkable(ctx, nx, ny)) continue; + if (encounter_entity_footprints_overlap(nx, ny, 1, + target_x, target_y, target_size)) + continue; + int d = dx * dx + dy * dy; + if (d < best_dsq) { best_dsq = d; bx = nx; by = ny; } + } + } + if (bx < 0) return 0; + int steps = encounter_walk_toward(p, bx, by, + cmap, world_offset_x, world_offset_y, + is_walkable, ctx, extra_blocked, blocked_ctx, + arena_base_x, arena_base_y, arena_w, arena_h); + return steps > 0 ? 1 : 0; + } + if (encounter_player_can_attack( + p->x, p->y, + target_x, target_y, target_size, attack_range, + cmap, world_offset_x, world_offset_y, + los_query)) + return 0; + + + int cx, cy; + cx = -1; + cy = -1; + + if (arena_w <= 0) { + int scan_min_x = target_x - attack_range; + int scan_max_x = target_x + target_size - 1 + attack_range; + int scan_min_y = target_y - attack_range; + int scan_max_y = target_y + target_size - 1 + attack_range; + + cx = -1; + cy = -1; + int best_player_dsq = 0x3fffffff; + int best_target_dist = 0x3fffffff; + if (scan_min_x <= scan_max_x && scan_min_y <= scan_max_y) { + for (int yy = scan_min_y; yy <= scan_max_y; yy++) { + for (int xx = scan_min_x; xx <= scan_max_x; xx++) { + if (!is_walkable(ctx, xx, yy)) continue; + if (!encounter_player_can_attack(xx, yy, target_x, target_y, + target_size, attack_range, + cmap, world_offset_x, world_offset_y, + los_query)) + continue; + int dx = xx - p->x; + int dy = yy - p->y; + int player_dsq = dx * dx + dy * dy; + int target_dist = encounter_rect_distance( + xx, yy, 1, target_x, target_y, target_size); + if (player_dsq < best_player_dsq || + (player_dsq == best_player_dsq && + target_dist < best_target_dist)) { + best_player_dsq = player_dsq; + best_target_dist = target_dist; + cx = xx; + cy = yy; + } + } + } + } + + if (cx < 0) { + cx = p->x < target_x ? target_x : + (p->x > target_x + target_size - 1 ? target_x + target_size - 1 : p->x); + cy = p->y < target_y ? target_y : + (p->y > target_y + target_size - 1 ? target_y + target_size - 1 : p->y); + } + } + + int cached_run_dx = 0; + int cached_run_dy = 0; + int steps = 0; + for (int step = 0; step < 2; step++) { + if (encounter_player_can_attack(p->x, p->y, target_x, target_y, + target_size, attack_range, + cmap, world_offset_x, world_offset_y, + los_query)) + break; + int next_dx; + int next_dy; + int use_cached_run = step == 1 && arena_w > 0 && + (cached_run_dx != 0 || cached_run_dy != 0); + if (use_cached_run) { + int next_x = p->x + cached_run_dx; + int next_y = p->y + cached_run_dy; + if (!is_walkable(ctx, next_x, next_y) || + (extra_blocked && extra_blocked( + blocked_ctx, + next_x + world_offset_x, + next_y + world_offset_y))) { + use_cached_run = 0; + } else if (cached_run_dx != 0 && cached_run_dy != 0 && + (!is_walkable(ctx, next_x, p->y) || + !is_walkable(ctx, p->x, next_y) || + (extra_blocked && + (extra_blocked( + blocked_ctx, + next_x + world_offset_x, + p->y + world_offset_y) || + extra_blocked( + blocked_ctx, + p->x + world_offset_x, + next_y + world_offset_y))))) { + use_cached_run = 0; + } + } + if (use_cached_run) { + next_dx = cached_run_dx; + next_dy = cached_run_dy; + } else { + PathResult pr = (arena_w > 0) + ? encounter_pathfind_arena_attack_approach( + cmap, world_offset_x, world_offset_y, + p->x, p->y, + target_x, target_y, target_size, attack_range, + is_walkable, ctx, + extra_blocked, blocked_ctx, + los_query, + arena_base_x, arena_base_y, arena_w, arena_h) + : encounter_pathfind(cmap, world_offset_x, world_offset_y, + p->x, p->y, cx, cy, + extra_blocked, blocked_ctx); + if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) break; + next_dx = pr.next_dx; + next_dy = pr.next_dy; + if (step == 0) { + cached_run_dx = pr.run_dx; + cached_run_dy = pr.run_dy; + } + } + int nx = p->x + next_dx, ny = p->y + next_dy; + if (!is_walkable(ctx, nx, ny)) break; + p->x = nx; p->y = ny; + steps++; + } + p->is_running = (steps == 2); + p->dest_x = p->x; p->dest_y = p->y; + return steps > 0 ? 1 : 0; +} + + +typedef struct { + uint64_t visited[ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION]; + uint16_t depth[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint16_t queue[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + int8_t via[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint16_t count; +} OsrsReferenceRouteField; + +typedef int (*osrs_reference_can_attack_fn)( + void* ctx, + int player_x, + int player_y, + int target_x, + int target_y, + int target_size, + int attack_range); + +static inline int osrs_reference_route_blocked( + const EncounterRouteInput* input, + int x, + int y +) { + return input->blockers.is_blocked && + input->blockers.is_blocked( + input->blockers.ctx, x, y, input->actor_size); +} + +static inline int osrs_reference_route_step_allowed( + const EncounterRouteInput* input, + int local_x, + int local_y, + int direction +) { + static const int8_t dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int8_t dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; + static const uint8_t step_mask[8] = {8, 16, 2, 64, 1, 4, 32, 128}; + const EncounterArenaTopology* topology = input->topology; + int next_x = local_x + dx[direction]; + int next_y = local_y + dy[direction]; + if (next_x < 0 || next_x >= topology->width || + next_y < 0 || next_y >= topology->height) + return 0; + int x = topology->origin_x + local_x; + int y = topology->origin_y + local_y; + if (osrs_reference_route_blocked( + input, x + dx[direction], y + dy[direction])) + return 0; + if (dx[direction] != 0 && dy[direction] != 0 && + (osrs_reference_route_blocked( + input, x + dx[direction], y) || + osrs_reference_route_blocked( + input, x, y + dy[direction]))) + return 0; + int index = local_x * topology->height + local_y; + return (topology->legal_step_masks[input->actor_size - 1][index] & + step_mask[direction]) != 0; +} + +static inline void osrs_reference_route_build( + const EncounterRouteInput* input, + OsrsReferenceRouteField* field +) { + static const int8_t dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int8_t dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; + static const int8_t via[8] = { + VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE + }; + const EncounterArenaTopology* topology = input->topology; + memset(field, 0, sizeof(*field)); + int source_x = input->source_x - topology->origin_x; + int source_y = input->source_y - topology->origin_y; + int source_index = source_x * topology->height + source_y; + field->visited[source_x] = UINT64_C(1) << source_y; + field->depth[source_index] = 0; + field->via[source_index] = VIA_START; + field->queue[0] = (uint16_t)((source_x << 6) | source_y); + field->count = 1; + for (int head = 0; head < field->count; head++) { + int packed = field->queue[head]; + int x = packed >> 6; + int y = packed & 63; + int index = x * topology->height + y; + uint16_t next_depth = (uint16_t)(field->depth[index] + 1); + for (int direction = 0; direction < 8; direction++) { + int next_x = x + dx[direction]; + int next_y = y + dy[direction]; + if (next_x < 0 || next_x >= topology->width || + next_y < 0 || next_y >= topology->height) + continue; + uint64_t bit = UINT64_C(1) << next_y; + if (field->visited[next_x] & bit) continue; + if (!osrs_reference_route_step_allowed( + input, x, y, direction)) + continue; + if (field->count >= topology->tile_count) abort(); + int next_index = next_x * topology->height + next_y; + field->visited[next_x] |= bit; + field->depth[next_index] = next_depth; + field->via[next_index] = via[direction]; + field->queue[field->count++] = + (uint16_t)((next_x << 6) | next_y); + } + } +} + +static inline int osrs_reference_route_is_target( + const EncounterRouteInput* input, + int x, + int y +) { + int64_t actor_max_x = (int64_t)x + input->actor_size - 1; + int64_t actor_max_y = (int64_t)y + input->actor_size - 1; + int64_t target_max_x = + (int64_t)input->target_x + input->target_size - 1; + int64_t target_max_y = + (int64_t)input->target_y + input->target_size - 1; + int x_overlap = + (int64_t)x <= target_max_x && (int64_t)input->target_x <= actor_max_x; + int y_overlap = + (int64_t)y <= target_max_y && (int64_t)input->target_y <= actor_max_y; + if (x_overlap && y_overlap) return 0; + return (y_overlap && + (actor_max_x + 1 == input->target_x || + target_max_x + 1 == x)) || + (x_overlap && + (actor_max_y + 1 == input->target_y || + target_max_y + 1 == y)); +} + +static inline int osrs_reference_target_distance_squared( + const EncounterRouteInput* input, + int x, + int y +) { + int64_t target_max_x = + (int64_t)input->target_x + input->target_size - 1; + int64_t target_max_y = + (int64_t)input->target_y + input->target_size - 1; + int64_t dx = 0; + int64_t dy = 0; + if ((int64_t)x < input->target_x) dx = input->target_x - (int64_t)x; + else if (target_max_x < x) dx = (int64_t)x - target_max_x; + if ((int64_t)y < input->target_y) dy = input->target_y - (int64_t)y; + else if (target_max_y < y) dy = (int64_t)y - target_max_y; + int64_t squared = dx * dx + dy * dy; + return squared > INT_MAX ? INT_MAX : (int)squared; +} + +static inline void osrs_reference_route_parent( + int via, + int* x, + int* y +) { + if (via & VIA_W) (*x)++; + else if (via & VIA_E) (*x)--; + if (via & VIA_S) (*y)++; + else if (via & VIA_N) (*y)--; +} + +static inline EncounterRouteResult osrs_reference_route_query( + const EncounterRouteInput* input, + const OsrsReferenceRouteField* field +) { + EncounterRouteResult result; + memset(&result, 0, sizeof(result)); + const EncounterArenaTopology* topology = input->topology; + int selected_index = -1; + for (int i = 0; i < field->count; i++) { + int packed = field->queue[i]; + int local_x = packed >> 6; + int local_y = packed & 63; + int x = topology->origin_x + local_x; + int y = topology->origin_y + local_y; + if (osrs_reference_route_is_target(input, x, y)) { + selected_index = local_x * topology->height + local_y; + break; + } + } + if (selected_index >= 0) { + result.outcome = ROUTE_REACHED_TARGET; + } else { + int best_distance = INT_MAX; + uint16_t best_depth = UINT16_MAX; + for (int local_x = 0; local_x < topology->width; local_x++) { + for (int local_y = 0; local_y < topology->height; local_y++) { + if ((field->visited[local_x] & + (UINT64_C(1) << local_y)) == 0) + continue; + int x = topology->origin_x + local_x; + int y = topology->origin_y + local_y; + int min_x = + input->target_x - PATHFIND_MAX_FALLBACK_RADIUS; + int max_x = input->target_x + input->target_size - 1 + + PATHFIND_MAX_FALLBACK_RADIUS; + int min_y = + input->target_y - PATHFIND_MAX_FALLBACK_RADIUS; + int max_y = input->target_y + input->target_size - 1 + + PATHFIND_MAX_FALLBACK_RADIUS; + if (x < min_x || x > max_x || y < min_y || y > max_y) + continue; + int index = local_x * topology->height + local_y; + int distance = + osrs_reference_target_distance_squared(input, x, y); + uint16_t depth = field->depth[index]; + if (distance < best_distance || + (distance == best_distance && depth < best_depth)) { + selected_index = index; + best_distance = distance; + best_depth = depth; + } + } + } + if (selected_index < 0) { + result.outcome = ROUTE_UNREACHABLE; + return result; + } + result.outcome = ROUTE_REACHED_FALLBACK; + } + int local_x = selected_index / topology->height; + int local_y = selected_index % topology->height; + result.destination_x = topology->origin_x + local_x; + result.destination_y = topology->origin_y + local_y; + result.distance = field->depth[selected_index]; + int source_x = input->source_x - topology->origin_x; + int source_y = input->source_y - topology->origin_y; + int first_x = source_x; + int first_y = source_y; + int second_x = source_x; + int second_y = source_y; + while (local_x != source_x || local_y != source_y) { + int index = local_x * topology->height + local_y; + uint16_t depth = field->depth[index]; + if (depth == 1) { + first_x = local_x; + first_y = local_y; + } else if (depth == 2) { + second_x = local_x; + second_y = local_y; + } + osrs_reference_route_parent(field->via[index], &local_x, &local_y); + } + result.first_dx = first_x - source_x; + result.first_dy = first_y - source_y; + if (result.distance >= 2) { + result.run_dx = second_x - first_x; + result.run_dy = second_y - first_y; + } + return result; +} + +static inline uint64_t osrs_reference_route_exhaustive( + const char* scenario, + const EncounterArenaTopology* topology, + EncounterRouteBlockers blockers, + int maximum_target_size, + const int* attack_ranges, + int attack_range_count, + osrs_reference_can_attack_fn can_attack, + void* attack_ctx +) { + uint64_t checks = 0; + for (int source_x = topology->origin_x; + source_x < topology->origin_x + topology->width; + source_x++) { + for (int source_y = topology->origin_y; + source_y < topology->origin_y + topology->height; + source_y++) { + if (encounter_arena_topology_footprint_blocked( + topology, source_x, source_y, 1) || + (blockers.is_blocked && + blockers.is_blocked( + blockers.ctx, source_x, source_y, 1))) + continue; + EncounterRouteInput input = { + .topology = topology, + .blockers = blockers, + .source_x = source_x, + .source_y = source_y, + .actor_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + }; + OsrsReferenceRouteField field; + osrs_reference_route_build(&input, &field); + for (int target_size = 1; + target_size <= maximum_target_size; + target_size++) { + input.target_size = target_size; + for (int target_x = topology->origin_x; + target_x + target_size <= + topology->origin_x + topology->width; + target_x++) { + input.target_x = target_x; + for (int target_y = topology->origin_y; + target_y + target_size <= + topology->origin_y + topology->height; + target_y++) { + if (encounter_arena_topology_footprint_blocked( + topology, + target_x, + target_y, + target_size)) + continue; + if (source_x >= target_x && + source_x < target_x + target_size && + source_y >= target_y && + source_y < target_y + target_size) + continue; + input.target_y = target_y; + EncounterRouteResult expected = + osrs_reference_route_query(&input, &field); + EncounterRouteResult actual = + encounter_route_solve(&input); + if (actual.outcome != expected.outcome || + actual.destination_x != expected.destination_x || + actual.destination_y != expected.destination_y || + actual.distance != expected.distance || + actual.first_dx != expected.first_dx || + actual.first_dy != expected.first_dy) { + fprintf(stderr, + "route equivalence mismatch scenario=%s source=(%d,%d) target=(%d,%d,%d) expected=(%d,%d,%d,%d,%d,%u) actual=(%d,%d,%d,%d,%d,%u)\n", + scenario, + source_x, + source_y, + target_x, + target_y, + target_size, + expected.outcome, + expected.destination_x, + expected.destination_y, + expected.first_dx, + expected.first_dy, + expected.distance, + actual.outcome, + actual.destination_x, + actual.destination_y, + actual.first_dx, + actual.first_dy, + actual.distance); + abort(); + } + for (int range_index = 0; + range_index < attack_range_count; + range_index++) { + int range = attack_ranges[range_index]; + int expected_x = source_x; + int expected_y = source_y; + int actual_x = source_x; + int actual_y = source_y; + if (!can_attack( + attack_ctx, + source_x, + source_y, + target_x, + target_y, + target_size, + range)) { + expected_x += expected.first_dx; + expected_y += expected.first_dy; + actual_x += actual.first_dx; + actual_y += actual.first_dy; + if (!can_attack( + attack_ctx, + expected_x, + expected_y, + target_x, + target_y, + target_size, + range)) { + expected_x += expected.run_dx; + expected_y += expected.run_dy; + } + if (!can_attack( + attack_ctx, + actual_x, + actual_y, + target_x, + target_y, + target_size, + range)) { + actual_x += actual.run_dx; + actual_y += actual.run_dy; + } + } + if (actual_x != expected_x || + actual_y != expected_y) { + fprintf(stderr, + "route landing mismatch scenario=%s source=(%d,%d) target=(%d,%d,%d) range=%d expected=(%d,%d) actual=(%d,%d)\n", + scenario, + source_x, + source_y, + target_x, + target_y, + target_size, + range, + expected_x, + expected_y, + actual_x, + actual_y); + abort(); + } + checks++; + } + } + } + } + } + } + return checks; +} + +#endif diff --git a/ocean/osrs/tests/test_colosseum_forecast_exact.c b/ocean/osrs/tests/test_colosseum_forecast_exact.c index bf894613ce..8aa4a192af 100644 --- a/ocean/osrs/tests/test_colosseum_forecast_exact.c +++ b/ocean/osrs/tests/test_colosseum_forecast_exact.c @@ -8,6 +8,9 @@ #include #include "ocean/osrs/encounters/encounter_colosseum.h" +#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES +#include "ocean/osrs/tests/osrs_route_reference.h" +#endif static void col_step_out_forecast_landing_selftest_one_state(ColosseumState* s) { for (int x = COLO_ARENA_MIN_X - 2; x <= COLO_ARENA_MAX_X + 2; x++) { @@ -233,6 +236,16 @@ static void exact_writer_close(ColoExactWriter* writer) { writer->file = NULL; } +static void exact_zero_serialized_route_storage(ColosseumState* state) { +#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES + osrs_interaction_zero_serialized_route_padding(&state->player.interaction); + osrs_interaction_zero_serialized_route_padding(&state->interaction); +#else + memset(&state->player.interaction.route, 0, sizeof(state->player.interaction.route)); + memset(&state->interaction.route, 0, sizeof(state->interaction.route)); +#endif +} + static void exact_capture( ColoExactWriter* writer, uint32_t scenario_id, @@ -245,6 +258,8 @@ static void exact_capture( col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); col_write_mask_ctx((EncounterState*)s, (EncounterContext*)ctx, action_mask); + ColosseumState canonical_state = *s; + exact_zero_serialized_route_storage(&canonical_state); ColoExactRecordHeader record = {0}; record.scenario_id = scenario_id; @@ -257,7 +272,7 @@ static void exact_capture( record.state_size = (uint32_t)sizeof(*s); record.obs_size = COLO_NUM_OBS; record.action_mask_size = COLO_ACTION_MASK_SIZE; - record.state_hash = exact_hash_bytes(s, sizeof(*s)); + record.state_hash = exact_hash_bytes(&canonical_state, sizeof(canonical_state)); record.obs_hash = exact_hash_bytes(obs, sizeof(obs)); record.action_mask_hash = exact_hash_bytes(action_mask, sizeof(action_mask)); record.reward = col_get_reward_ctx((EncounterState*)s, (EncounterContext*)ctx); @@ -265,7 +280,7 @@ static void exact_capture( exact_write_all(writer->file, &record, sizeof(record)); exact_write_all(writer->file, obs, sizeof(obs)); exact_write_all(writer->file, action_mask, sizeof(action_mask)); - exact_write_all(writer->file, s, sizeof(*s)); + exact_write_all(writer->file, &canonical_state, sizeof(canonical_state)); writer->record_count++; } @@ -302,6 +317,9 @@ static void exact_init_state( uint32_t seed ) { col_init_context_typed(ctx); +#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES + col_bind_route_topology(ctx, NULL); +#endif ctx->config.start_wave = start_wave; memset(s, 0, sizeof(*s)); col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); @@ -340,6 +358,7 @@ static void exact_prepare_custom( exact_refresh_geometry(s, ctx); } +#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES typedef struct { ColoWalkCtx walk; int blocker_count; @@ -365,6 +384,103 @@ static int exact_attack_route_blocked(void* ctx, int abs_x, int abs_y) { return 0; } +static int exact_optimized_route_blocked(void* data, int x, int y, int size) { + (void)size; + ExactAttackRouteContext* route_ctx = (ExactAttackRouteContext*)data; + if (!exact_attack_route_walkable(route_ctx, x, y)) return 1; + for (int i = 0; i < route_ctx->blocker_count; i++) { + if (route_ctx->blocker_x[i] == x && + route_ctx->blocker_y[i] == y) + return 1; + } + return 0; +} +static int exact_attack_route_can_attack( + void* data, + int player_x, + int player_y, + int target_x, + int target_y, + int target_size, + int attack_range +) { + ExactAttackRouteContext* route_ctx = (ExactAttackRouteContext*)data; + OsrsLosQuery query = col_player_los_query(route_ctx->walk.s); + return encounter_player_can_attack( + player_x, + player_y, + target_x, + target_y, + target_size, + attack_range, + route_ctx->walk.ctx->collision_map, + route_ctx->walk.ctx->world_offset_x, + route_ctx->walk.ctx->world_offset_y, + &query); +} + +static EncounterAttackRouteLanding exact_attack_route_optimized_landing( + const ColosseumState* s, + const ColosseumContext* ctx, + ExactAttackRouteContext* route_ctx, + int target_x, + int target_y, + int target_size, + int attack_range, + const OsrsLosQuery* los_query +) { + Player player = s->player; + if (encounter_player_can_attack( + player.x, player.y, + target_x, target_y, target_size, attack_range, + ctx->collision_map, + ctx->world_offset_x, + ctx->world_offset_y, + los_query)) + return (EncounterAttackRouteLanding){ + .land_x = player.x, + .land_y = player.y, + }; + EncounterRouteInput input = { + .topology = ctx->route_topology, + .blockers = { + .is_blocked = exact_optimized_route_blocked, + .ctx = route_ctx, + .revision = (uint64_t)route_ctx->blocker_count + 1, + }, + .source_x = player.x, + .source_y = player.y, + .actor_size = 1, + .target_x = target_x, + .target_y = target_y, + .target_size = target_size, + .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + }; + EncounterRouteResult route = encounter_route_solve(&input); + if (route.outcome == ROUTE_REACHED_TARGET || + route.outcome == ROUTE_REACHED_FALLBACK) { + player.x += route.first_dx; + player.y += route.first_dy; + if ((route.run_dx != 0 || route.run_dy != 0) && + !encounter_player_can_attack( + player.x, player.y, + target_x, target_y, target_size, attack_range, + ctx->collision_map, + ctx->world_offset_x, + ctx->world_offset_y, + los_query)) { + player.x += route.run_dx; + player.y += route.run_dy; + } + } + return (EncounterAttackRouteLanding){ + .land_x = player.x, + .land_y = player.y, + }; +} + static EncounterAttackRouteLanding exact_attack_route_runtime_landing( const ColosseumState* s, const ColosseumContext* ctx, @@ -422,6 +538,11 @@ static int exact_attack_route_property_scenario( size_idx++) { int target_size = target_sizes[size_idx]; EncounterArenaAttackRouteField field; + if (target_x < COLO_ARENA_MIN_X || + target_y < COLO_ARENA_MIN_Y || + target_x + target_size - 1 > COLO_ARENA_MAX_X || + target_y + target_size - 1 > COLO_ARENA_MAX_Y) + continue; encounter_build_arena_attack_route_field( &field, ctx->collision_map, @@ -450,8 +571,8 @@ static int exact_attack_route_property_scenario( target_x, target_y, target_size, attack_range, &los_query); EncounterAttackRouteLanding actual = - encounter_arena_attack_route_landing( - &field, + exact_attack_route_optimized_landing( + s, ctx, route_ctx, target_x, target_y, target_size, attack_range, &los_query); if (actual.land_x != expected.land_x || @@ -478,11 +599,11 @@ static int exact_attack_route_property_scenario( } return checks; } - static void exact_attack_route_property_selftest(void) { ColosseumState s; ColosseumContext ctx; - exact_prepare_custom(&s, &ctx, 0xA771u, 16, 16); + exact_prepare_custom( + &s, &ctx, 0xA771u, COLO_PLAYER_START_X, COLO_PLAYER_START_Y); ExactAttackRouteContext route_ctx = { .walk = {&s, &ctx}, }; @@ -490,8 +611,8 @@ static void exact_attack_route_property_selftest(void) { "open-arena", &s, &ctx, &route_ctx); route_ctx.blocker_count = 8; - int blocker_x[] = {14, 15, 16, 17, 18, 17, 16, 15}; - int blocker_y[] = {16, 15, 14, 15, 16, 17, 18, 17}; + int blocker_x[] = {5, 6, 7, 8, 9, 8, 7, 6}; + int blocker_y[] = {18, 17, 16, 17, 18, 19, 20, 19}; memcpy(route_ctx.blocker_x, blocker_x, sizeof(blocker_x)); memcpy(route_ctx.blocker_y, blocker_y, sizeof(blocker_y)); checks += exact_attack_route_property_scenario( @@ -504,15 +625,67 @@ static void exact_attack_route_property_selftest(void) { s.sol.boss_arena_min_y = COLO_BOSS_ARENA_MIN_Y; s.sol.boss_arena_max_x = COLO_BOSS_ARENA_MAX_X; s.sol.boss_arena_max_y = COLO_BOSS_ARENA_MAX_Y; - s.player.x = COLO_BOSS_ARENA_MIN_X + 1; - s.player.y = COLO_BOSS_ARENA_MIN_Y + 1; + s.player.x = -1; + s.player.y = -1; + for (int x = s.sol.boss_arena_min_x; + x <= s.sol.boss_arena_max_x && s.player.x < 0; + x++) { + for (int y = s.sol.boss_arena_min_y; + y <= s.sol.boss_arena_max_y; + y++) { + if (!col_static_blocked(x, y)) { + s.player.x = x; + s.player.y = y; + break; + } + } + } exact_refresh_geometry(&s, &ctx); checks += exact_attack_route_property_scenario( "sol-clamp", &s, &ctx, &route_ctx); + static const int exhaustive_attack_ranges[] = {1, 3, 6, 10}; + exact_prepare_custom( + &s, &ctx, 0xA771u, COLO_PLAYER_START_X, COLO_PLAYER_START_Y); + route_ctx.walk.s = &s; + route_ctx.walk.ctx = &ctx; + route_ctx.blocker_count = 0; + uint64_t exhaustive_checks = osrs_reference_route_exhaustive( + "open-arena", + ctx.route_topology, + (EncounterRouteBlockers){ + .is_blocked = exact_optimized_route_blocked, + .ctx = &route_ctx, + .revision = 1, + }, + 5, + exhaustive_attack_ranges, + 4, + exact_attack_route_can_attack, + &route_ctx); + route_ctx.blocker_count = 8; + memcpy(route_ctx.blocker_x, blocker_x, sizeof(blocker_x)); + memcpy(route_ctx.blocker_y, blocker_y, sizeof(blocker_y)); + exhaustive_checks += osrs_reference_route_exhaustive( + "dynamic-blockers", + ctx.route_topology, + (EncounterRouteBlockers){ + .is_blocked = exact_optimized_route_blocked, + .ctx = &route_ctx, + .revision = 9, + }, + 5, + exhaustive_attack_ranges, + 4, + exact_attack_route_can_attack, + &route_ctx); + printf( + "colosseum exhaustive route equivalence PASS: %llu source-target-range queries across 2 blocker fields\n", + (unsigned long long)exhaustive_checks); printf( "colosseum attack route property selftest PASS: %d target queries across 3 fields\n", checks); } +#endif static void exact_run_steps( ColoExactWriter* writer, @@ -742,10 +915,12 @@ static int exact_compare_files(const char* expected_path, const char* actual_pat } int main(int argc, char** argv) { +#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES if (argc == 2 && strcmp(argv[1], "--attack-route-selftest") == 0) { exact_attack_route_property_selftest(); return 0; } +#endif if (argc != 3 || (strcmp(argv[1], "--write-golden") != 0 && strcmp(argv[1], "--compare") != 0)) { @@ -758,7 +933,9 @@ int main(int argc, char** argv) { col_static_los_table_selftest(); col_static_footprint_table_selftest(); col_step_out_forecast_landing_selftest(); +#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES exact_attack_route_property_selftest(); +#endif char fixture_path[1024]; char current_path[1024]; diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index a00210211a..e8f2301f7f 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -9,6 +9,7 @@ #define col_init_context_typed(ctx_ptr) do { \ col_init_context_typed(ctx_ptr); \ + col_bind_route_topology((ctx_ptr), NULL); \ (ctx_ptr)->config.late_start_state_mode = 0; \ } while (0) @@ -2828,8 +2829,8 @@ static uint64_t wb_trajectory_hash(ColosseumState* s, ColosseumContext* ctx, return h; } -static void test_warband_bfs_memo_bit_identity(void) { - printf("test_warband_bfs_memo_bit_identity\n"); +static void test_warband_route_generation_bit_identity(void) { + printf("test_warband_route_generation_bit_identity\n"); ColosseumContext ctx; col_init_context_typed(&ctx); ctx.config.start_wave = 0; @@ -2839,17 +2840,15 @@ static void test_warband_bfs_memo_bit_identity(void) { int walk_south[COLO_NUM_ACTION_HEADS] = {0}; walk_south[COLO_HEAD_PRIMARY] = 4; - memset(col_warband_bfs_memo_key, 0, sizeof(col_warband_bfs_memo_key)); - uint64_t idle_cold = wb_trajectory_hash(&s, &ctx, 51, idle, 40); - uint64_t idle_warm = wb_trajectory_hash(&s, &ctx, 51, idle, 40); - CHECK("memo-served warband trajectory == fresh BFS (idle player)", - idle_warm == idle_cold); + uint64_t idle_first = wb_trajectory_hash(&s, &ctx, 51, idle, 40); + uint64_t idle_second = wb_trajectory_hash(&s, &ctx, 51, idle, 40); + CHECK("thread-local route generations preserve idle warband trajectory", + idle_second == idle_first); - uint64_t walk_polluted = wb_trajectory_hash(&s, &ctx, 53, walk_south, 40); - memset(col_warband_bfs_memo_key, 0, sizeof(col_warband_bfs_memo_key)); - uint64_t walk_cold = wb_trajectory_hash(&s, &ctx, 53, walk_south, 40); - CHECK("polluted-table episode == its fresh-memo reference (walking player)", - walk_polluted == walk_cold); + uint64_t walk_first = wb_trajectory_hash(&s, &ctx, 53, walk_south, 40); + uint64_t walk_second = wb_trajectory_hash(&s, &ctx, 53, walk_south, 40); + CHECK("thread-local route generations preserve walking warband trajectory", + walk_second == walk_first); } static void test_warband_melee_distance_gate(void) { @@ -8662,7 +8661,7 @@ int main(void) { test_warband_cycle_offsets(); test_warband_move_skip(); test_warband_melee_not_dodged_by_same_tick_step_out(); - test_warband_bfs_memo_bit_identity(); + test_warband_route_generation_bit_identity(); test_warband_melee_distance_gate(); test_warband_two_tick_stationary_gate(); test_warband_formation_convergence(); diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index 87459e03ab..9936d9d341 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -5,6 +5,11 @@ #include #include "ocean/osrs/encounters/encounter_inferno.h" +#include "ocean/osrs/tests/osrs_route_reference.h" +#define inf_init_context_typed(ctx_ptr) do { \ + inf_init_context_typed(ctx_ptr); \ + inf_bind_route_topology((ctx_ptr), NULL); \ +} while (0) #include "ocean/osrs/osrs_anim.h" #include "ocean/osrs/osrs_effects.h" #include "ocean/osrs/osrs_projectile_orientation.h" diff --git a/ocean/osrs/tests/test_inferno_forecast_exact.c b/ocean/osrs/tests/test_inferno_forecast_exact.c index 947b51664c..a5317e8e46 100644 --- a/ocean/osrs/tests/test_inferno_forecast_exact.c +++ b/ocean/osrs/tests/test_inferno_forecast_exact.c @@ -8,6 +8,7 @@ #include #include "ocean/osrs/encounters/encounter_inferno.h" +#include "ocean/osrs/tests/osrs_route_reference.h" #define EXACT_MAGIC "INFEXACTv2" #define EXACT_VERSION 2u @@ -124,6 +125,16 @@ static void exact_writer_close(InfExactWriter* writer) { writer->file = NULL; } +static void exact_zero_serialized_route_storage(InfernoState* state) { +#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES + osrs_interaction_zero_serialized_route_padding(&state->player.interaction); + osrs_interaction_zero_serialized_route_padding(&state->interaction); +#else + memset(&state->player.interaction.route, 0, sizeof(state->player.interaction.route)); + memset(&state->interaction.route, 0, sizeof(state->interaction.route)); +#endif +} + static void exact_capture( InfExactWriter* writer, uint32_t scenario_id, @@ -137,6 +148,8 @@ static void exact_capture( inf_build_step_out_forecast_ctx(s, ctx, &forecast); inf_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); + InfernoState canonical_state = *s; + exact_zero_serialized_route_storage(&canonical_state); InfExactRecordHeader record = {0}; record.scenario_id = scenario_id; @@ -150,7 +163,7 @@ static void exact_capture( record.state_size = (uint32_t)sizeof(*s); record.obs_size = INF_NUM_OBS; record.forecast_size = (uint32_t)sizeof(forecast); - record.state_hash = exact_hash_bytes(s, sizeof(*s)); + record.state_hash = exact_hash_bytes(&canonical_state, sizeof(canonical_state)); record.forecast_hash = exact_hash_bytes(&forecast, sizeof(forecast)); record.obs_hash = exact_hash_bytes(obs, sizeof(obs)); record.reward = inf_get_reward_ctx((EncounterState*)s, (EncounterContext*)ctx); @@ -158,7 +171,7 @@ static void exact_capture( exact_write_all(writer->file, &record, sizeof(record)); exact_write_all(writer->file, &forecast, sizeof(forecast)); exact_write_all(writer->file, obs, sizeof(obs)); - exact_write_all(writer->file, s, sizeof(*s)); + exact_write_all(writer->file, &canonical_state, sizeof(canonical_state)); writer->record_count++; } @@ -186,6 +199,9 @@ static void exact_init_state( uint32_t seed ) { inf_init_context_typed(ctx); +#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES + inf_bind_route_topology(ctx, NULL); +#endif memset(s, 0, sizeof(*s)); inf_put_int_ctx( (EncounterState*)s, @@ -194,6 +210,85 @@ static void exact_init_state( public_start_wave); inf_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); } +typedef struct { + InfernoState* state; + InfernoContext* context; +} InfExactRouteContext; + +static int exact_inferno_route_blocked( + void* data, + int x, + int y, + int size +) { + InfExactRouteContext* route_ctx = (InfExactRouteContext*)data; + return inf_blocked_by_pillar(route_ctx->state, x, y, size); +} + +static int exact_inferno_can_attack( + void* data, + int player_x, + int player_y, + int target_x, + int target_y, + int target_size, + int attack_range +) { + InfExactRouteContext* route_ctx = (InfExactRouteContext*)data; + OsrsLosQuery query = inf_player_los_query(route_ctx->state); + return encounter_player_can_attack( + player_x, + player_y, + target_x, + target_y, + target_size, + attack_range, + route_ctx->context->collision_map, + route_ctx->context->world_offset_x, + route_ctx->context->world_offset_y, + &query); +} + +static void exact_inferno_route_equivalence(void) { + InfernoState state; + InfernoContext context; + exact_init_state(&state, &context, 1, EXACT_ENV_SEED); + InfExactRouteContext route_ctx = { + .state = &state, + .context = &context, + }; + static const int attack_ranges[] = {1, 5, 10}; + uint64_t checks = osrs_reference_route_exhaustive( + "all-pillars", + context.route_topology, + (EncounterRouteBlockers){ + .is_blocked = exact_inferno_route_blocked, + .ctx = &route_ctx, + .revision = inf_player_route_blocker_revision(&state), + }, + 5, + attack_ranges, + 3, + exact_inferno_can_attack, + &route_ctx); + state.pillars[1].active = 0; + checks += osrs_reference_route_exhaustive( + "middle-pillar-collapsed", + context.route_topology, + (EncounterRouteBlockers){ + .is_blocked = exact_inferno_route_blocked, + .ctx = &route_ctx, + .revision = inf_player_route_blocker_revision(&state), + }, + 5, + attack_ranges, + 3, + exact_inferno_can_attack, + &route_ctx); + printf( + "inferno exhaustive route equivalence PASS: %llu source-target-range queries across 2 blocker fields\n", + (unsigned long long)checks); +} static void exact_run_wave_rollout( InfExactWriter* writer, @@ -275,11 +370,17 @@ static int exact_compare_files(const char* expected_path, const char* actual_pat } int main(int argc, char** argv) { + if (argc == 2 && strcmp(argv[1], "--attack-route-selftest") == 0) { + inf_build_npc_stats(); + exact_inferno_route_equivalence(); + return 0; + } if (argc != 3 || (strcmp(argv[1], "--write-golden") != 0 && strcmp(argv[1], "--compare") != 0)) { fprintf(stderr, - "usage: %s --write-golden DIR | --compare DIR\n", argv[0]); + "usage: %s --attack-route-selftest | --write-golden DIR | --compare DIR\n", + argv[0]); return 2; } diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c index 53f3611ee4..0fa8b09a41 100644 --- a/ocean/osrs/tests/test_osrs_player_step.c +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -21,6 +21,7 @@ static void* topology_counted_calloc(size_t count, size_t size); #define calloc topology_counted_calloc #include "ocean/osrs/osrs_encounter_player.h" +#include "ocean/osrs/tests/osrs_route_reference.h" #undef calloc static void* topology_counted_calloc(size_t count, size_t size) { @@ -65,6 +66,61 @@ static int step_vertical_wall(void* ctx, int x, int y) { return x == 5 && y <= 4; } +static int step_vertical_wall_route(void* ctx, int x, int y, int size) { + (void)size; + return step_vertical_wall(ctx, x, y); +} + +static uint32_t step_open_flags(void* ctx, int x, int y) { + (void)ctx; + (void)x; + (void)y; + return 0; +} + +static const EncounterArenaTopology* step_route_topology(void) { + static EncounterArenaTopology* topology; + if (!topology) { + EncounterArenaTopologyBuildSpec spec = { + .origin_x = 0, + .origin_y = 0, + .width = STEP_GRID, + .height = STEP_GRID, + .max_footprint_size = 2, + .revision = 19, + .tile_flags = step_open_flags, + .tile_flags_ctx = NULL, + }; + topology = encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(topology); + } + return topology; +} + + +static uint32_t step_collision_flags(void* ctx, int x, int y) { + return (uint32_t)collision_get_flags((const CollisionMap*)ctx, 0, x, y); +} + +static EncounterArenaTopology* step_collision_topology( + const CollisionMap* collision_map, + uint64_t revision +) { + EncounterArenaTopologyBuildSpec spec = { + .origin_x = 0, + .origin_y = 0, + .width = STEP_GRID, + .height = STEP_GRID, + .max_footprint_size = 2, + .revision = revision, + .tile_flags = step_collision_flags, + .tile_flags_ctx = (void*)collision_map, + }; + EncounterArenaTopology* topology = encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(topology); + return topology; +} + static int step_lookup_target(void* ctx, int target_slot, OsrsAttackTarget* out) { StepTargetCtx* c = (StepTargetCtx*)ctx; if (!c->target_valid || target_slot != c->target.slot) return 0; @@ -75,10 +131,12 @@ static int step_lookup_target(void* ctx, int target_slot, OsrsAttackTarget* out) static OsrsEncounterArena step_arena(void) { OsrsEncounterArena arena; memset(&arena, 0, sizeof(arena)); - arena.is_walkable = step_tile_walkable; arena.los_query = osrs_los_open_query(); - arena.arena_w = STEP_GRID; - arena.arena_h = STEP_GRID; + arena.topology = step_route_topology(); + arena.blockers.revision = 1; + arena.movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN; + arena.cost_policy = ENCOUNTER_ROUTE_COST_OSRS; + arena.explicit_cost_policy = ENCOUNTER_ROUTE_COST_DIRECT; return arena; } @@ -150,6 +208,7 @@ static void test_melee_attack_does_not_cross_cardinal_wall(void) { player.y = 10; OsrsInteraction interaction; + OsrsActorRouteCache route_cache = {0}; osrs_interaction_init(&interaction); osrs_interaction_set(&interaction, 3); @@ -161,11 +220,15 @@ static void test_melee_attack_does_not_cross_cardinal_wall(void) { int dest_y = -1; OsrsEncounterArena arena = step_arena(); arena.collision_map = ↦ + EncounterArenaTopology* wall_topology = + step_collision_topology(&map, 20); + arena.topology = wall_topology; OsrsPlayerStepInput input; memset(&input, 0, sizeof(input)); input.player = &player; input.interaction = &interaction; + input.route_cache = &route_cache; input.target_lookup = step_lookup_target; input.target_ctx = &tctx; input.dest_x = &dest_x; @@ -177,6 +240,7 @@ static void test_melee_attack_does_not_cross_cardinal_wall(void) { CHECK("melee does not attack through cardinal wall", !result.can_attack || player.x != 9 || player.y != 10); CHECK("melee routes around cardinal wall", result.chased_target); + free(wall_topology); } /* an entity click cancels a walk already in flight: the player closes on the @@ -192,6 +256,7 @@ static void test_target_command_cancels_walk_in_flight(void) { player.run_energy = 10000; OsrsInteraction interaction; + OsrsActorRouteCache route_cache = {0}; osrs_interaction_init(&interaction); StepTargetCtx tctx = { @@ -206,6 +271,7 @@ static void test_target_command_cancels_walk_in_flight(void) { memset(&input, 0, sizeof(input)); input.player = &player; input.interaction = &interaction; + input.route_cache = &route_cache; input.target_lookup = step_lookup_target; input.target_ctx = &tctx; input.command.kind = OSRS_PLAYER_CMD_TARGET; @@ -232,6 +298,7 @@ static void test_move_command_cancels_interaction(void) { player.run_energy = 10000; OsrsInteraction interaction; + OsrsActorRouteCache route_cache = {0}; osrs_interaction_init(&interaction); osrs_interaction_set(&interaction, 3); @@ -247,6 +314,7 @@ static void test_move_command_cancels_interaction(void) { memset(&input, 0, sizeof(input)); input.player = &player; input.interaction = &interaction; + input.route_cache = &route_cache; input.target_lookup = step_lookup_target; input.target_ctx = &tctx; input.command.kind = OSRS_PLAYER_CMD_MOVE; @@ -272,6 +340,7 @@ static void test_none_command_chases_active_interaction(void) { player.run_energy = 10000; OsrsInteraction interaction; + OsrsActorRouteCache route_cache = {0}; osrs_interaction_init(&interaction); osrs_interaction_set(&interaction, 3); @@ -287,6 +356,7 @@ static void test_none_command_chases_active_interaction(void) { memset(&input, 0, sizeof(input)); input.player = &player; input.interaction = &interaction; + input.route_cache = &route_cache; input.target_lookup = step_lookup_target; input.target_ctx = &tctx; input.command.kind = OSRS_PLAYER_CMD_NONE; @@ -308,6 +378,7 @@ static void test_attack_route_persists_and_invalidates_canonically(void) { player.y = 2; OsrsInteraction interaction; + OsrsActorRouteCache route_cache = {0}; osrs_interaction_init(&interaction); osrs_interaction_set(&interaction, 3); @@ -318,12 +389,14 @@ static void test_attack_route_persists_and_invalidates_canonically(void) { int dest_x = -1; int dest_y = -1; OsrsEncounterArena arena = step_arena(); - arena.extra_blocked = step_vertical_wall; + arena.blockers.is_blocked = step_vertical_wall_route; + arena.blockers.revision = 7; OsrsPlayerStepInput input; memset(&input, 0, sizeof(input)); input.player = &player; input.interaction = &interaction; + input.route_cache = &route_cache; input.target_lookup = step_lookup_target; input.target_ctx = &target_ctx; input.dest_x = &dest_x; @@ -332,31 +405,39 @@ static void test_attack_route_persists_and_invalidates_canonically(void) { OsrsPlayerStepResult first = osrs_encounter_player_step(&input); CHECK("obstacle chase builds a multi-checkpoint route", - first.chased_target && interaction.route.waypoint_count > 1); + first.chased_target && route_cache.waypoint_count > 1); CHECK("route records its original source", - interaction.route.planned_source_x == 2 && - interaction.route.planned_source_y == 2); + route_cache.planned_source_x == 2 && + route_cache.planned_source_y == 2); osrs_encounter_player_step(&input); CHECK("route persists while more than one checkpoint remains", - interaction.route.planned_source_x == 2 && - interaction.route.planned_source_y == 2); + route_cache.planned_source_x == 2 && + route_cache.planned_source_y == 2); + int blocker_changed_source_x = player.x; + int blocker_changed_source_y = player.y; + input.arena.blockers.revision++; + osrs_encounter_player_step(&input); + CHECK("dynamic blocker revision invalidates actor-local route cache", + route_cache.blocker_revision == input.arena.blockers.revision && + route_cache.planned_source_x == blocker_changed_source_x && + route_cache.planned_source_y == blocker_changed_source_y); target_ctx.target.y = 3; int moved_target_source_x = player.x; int moved_target_source_y = player.y; osrs_encounter_player_step(&input); CHECK("target geometry change reroutes from the current tile", - interaction.route.target_y == 3 && - interaction.route.planned_source_x == moved_target_source_x && - interaction.route.planned_source_y == moved_target_source_y); + route_cache.target_y == 3 && + route_cache.planned_source_x == moved_target_source_x && + route_cache.planned_source_y == moved_target_source_y); player.x = 1; player.y = 10; osrs_encounter_player_step(&input); CHECK("player route divergence reroutes from the observed tile", - interaction.route.planned_source_x == 1 && - interaction.route.planned_source_y == 10); + route_cache.planned_source_x == 1 && + route_cache.planned_source_y == 10); } static void test_single_checkpoint_route_reroutes_each_tick(void) { @@ -366,6 +447,7 @@ static void test_single_checkpoint_route_reroutes_each_tick(void) { player.y = 2; OsrsInteraction interaction; + OsrsActorRouteCache route_cache = {0}; osrs_interaction_init(&interaction); osrs_interaction_set(&interaction, 3); @@ -380,6 +462,7 @@ static void test_single_checkpoint_route_reroutes_each_tick(void) { memset(&input, 0, sizeof(input)); input.player = &player; input.interaction = &interaction; + input.route_cache = &route_cache; input.target_lookup = step_lookup_target; input.target_ctx = &target_ctx; input.dest_x = &dest_x; @@ -388,37 +471,33 @@ static void test_single_checkpoint_route_reroutes_each_tick(void) { osrs_encounter_player_step(&input); CHECK("straight chase compresses to one checkpoint", - interaction.route.waypoint_count == 1 && - interaction.route.planned_source_x == 2); + route_cache.waypoint_count == 1 && + route_cache.planned_source_x == 2); osrs_encounter_player_step(&input); CHECK("one-checkpoint entity route reroutes from the current tile", - interaction.route.planned_source_x == 4 && - interaction.route.planned_source_y == 2); + route_cache.planned_source_x == 4 && + route_cache.planned_source_y == 2); } -static void test_same_target_selection_preserves_route(void) { +static void test_target_selection_does_not_own_route_cache(void) { OsrsInteraction interaction = {0}; + OsrsActorRouteCache route_cache = { + .state = OSRS_INTERACTION_ROUTE_READY, + .target_x = 12, + .target_y = 7, + .waypoint_count = 2, + .waypoint_index = 1, + }; + route_cache.waypoint_x[1] = 11; + route_cache.waypoint_y[1] = 7; osrs_interaction_init(&interaction); osrs_interaction_set(&interaction, 3); - interaction.route.state = OSRS_INTERACTION_ROUTE_READY; - interaction.route.target_x = 12; - interaction.route.target_y = 7; - interaction.route.waypoint_count = 2; - interaction.route.waypoint_index = 1; - interaction.route.waypoint_x[1] = 11; - interaction.route.waypoint_y[1] = 7; - - OsrsInteractionRoute preserved = interaction.route; - osrs_interaction_set(&interaction, 3); - CHECK("same target preserves its route", - memcmp(&interaction.route, &preserved, sizeof(preserved)) == 0); + OsrsActorRouteCache preserved = route_cache; osrs_interaction_set(&interaction, 4); - CHECK("different target clears the route", + CHECK("target selection leaves context-owned route cache unchanged", interaction.target_slot == 4 && - interaction.route.state == OSRS_INTERACTION_ROUTE_EMPTY && - interaction.route.waypoint_count == 0 && - interaction.route.waypoint_index == 0); + memcmp(&route_cache, &preserved, sizeof(preserved)) == 0); } static void test_attack_route_field_stops_at_first_reachable_target_edge(void) { @@ -805,11 +884,170 @@ static void test_arena_topology_rejects_invalid_bounds(void) { topology_test_aborts(topology_build_rejects_origin_overflow)); } +typedef struct { + uint32_t flags[8][8]; +} RouteTestGeometry; + +typedef struct { + uint8_t blocked[8][8]; +} RouteTestBlockers; + +static uint32_t route_test_flags(void* data, int x, int y) { + RouteTestGeometry* geometry = (RouteTestGeometry*)data; + if (x < 0 || x >= 8 || y < 0 || y >= 8) + return COLLISION_BLOCKED | LOS_FULL_MASK; + return geometry->flags[x][y]; +} + +static int route_test_blocked(void* data, int x, int y, int size) { + RouteTestBlockers* blockers = (RouteTestBlockers*)data; + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int tile_x = x + dx; + int tile_y = y + dy; + if (tile_x < 0 || tile_x >= 8 || tile_y < 0 || tile_y >= 8) + return 1; + if (blockers->blocked[tile_x][tile_y]) return 1; + } + } + return 0; +} + +static EncounterArenaTopology* route_test_topology( + RouteTestGeometry* geometry +) { + EncounterArenaTopologyBuildSpec spec = { + .origin_x = 0, + .origin_y = 0, + .width = 8, + .height = 8, + .max_footprint_size = 2, + .revision = 71, + .tile_flags = route_test_flags, + .tile_flags_ctx = geometry, + }; + EncounterArenaTopology* topology = encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(topology); + return topology; +} + +static EncounterRouteInput route_test_input( + const EncounterArenaTopology* topology, + RouteTestBlockers* blockers +) { + return (EncounterRouteInput){ + .topology = topology, + .blockers = { + .is_blocked = route_test_blocked, + .ctx = blockers, + .revision = 1, + }, + .source_x = 1, + .source_y = 1, + .actor_size = 1, + .target_x = 4, + .target_y = 1, + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + }; +} + +static void test_tagged_route_outcomes_and_payload(void) { + RouteTestGeometry geometry = {0}; + RouteTestBlockers blockers = {0}; + EncounterArenaTopology* topology = route_test_topology(&geometry); + EncounterRouteInput input = route_test_input(topology, &blockers); + + EncounterRouteResult reached = encounter_route_solve(&input); + CHECK("exact target returns tagged reached-target outcome", + reached.outcome == ROUTE_REACHED_TARGET); + CHECK("reached route pins destination, shortest distance, and run steps", + reached.destination_x == 4 && reached.destination_y == 1 && + reached.distance == 3 && + reached.first_dx == 1 && reached.first_dy == 0 && + reached.run_dx == 1 && reached.run_dy == 0); + CHECK("straight route compresses to its exact waypoint", + reached.waypoint_count == 1 && + reached.waypoint_x[0] == 4 && reached.waypoint_y[0] == 1); + + for (int y = 0; y < 8; y++) blockers.blocked[3][y] = 1; + input.blockers.revision++; + EncounterRouteResult fallback = encounter_route_solve(&input); + CHECK("dynamic wall selects tagged fallback without topology mutation", + fallback.outcome == ROUTE_REACHED_FALLBACK && + topology->revision == 71 && + fallback.destination_x == 2 && fallback.destination_y == 1 && + fallback.distance == 1); + + memset(&blockers, 1, sizeof(blockers)); + blockers.blocked[1][1] = 0; + input.blockers.revision++; + input.target_x = 20; + EncounterRouteResult unreachable = encounter_route_solve(&input); + CHECK("no target or fallback returns tagged unreachable", + unreachable.outcome == ROUTE_UNREACHABLE); + + input.topology = NULL; + EncounterRouteResult invalid = encounter_route_solve(&input); + CHECK("bad route contract returns tagged invalid input", + invalid.outcome == ROUTE_INVALID_INPUT); + free(topology); +} + +static void test_route_cost_order_is_deterministic(void) { + RouteTestGeometry geometry = {0}; + RouteTestBlockers blockers = {0}; + EncounterArenaTopology* topology = route_test_topology(&geometry); + EncounterRouteInput input = route_test_input(topology, &blockers); + input.source_x = 4; + input.source_y = 4; + input.target_x = 2; + input.target_y = 2; + input.target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY; + + EncounterRouteResult route = encounter_route_solve(&input); + CHECK("OSRS attack route takes west before southwest", + route.outcome == ROUTE_REACHED_TARGET && + route.first_dx == -1 && route.first_dy == 0 && + route.run_dx == -1 && route.run_dy == -1); + CHECK("attack route selects the first equal-cost FIFO target edge", + route.destination_x == 2 && route.destination_y == 3 && + route.distance == 2); + free(topology); +} +static void test_direct_route_policy_preserves_greedy_step_order(void) { + RouteTestGeometry geometry = {0}; + RouteTestBlockers blockers = {0}; + EncounterArenaTopology* topology = route_test_topology(&geometry); + EncounterRouteInput input = route_test_input(topology, &blockers); + input.source_x = 1; + input.source_y = 1; + input.target_x = 4; + input.target_y = 4; + input.cost_policy = ENCOUNTER_ROUTE_COST_DIRECT; + blockers.blocked[2][2] = 1; + + EncounterRouteResult route = encounter_route_solve(&input); + CHECK("direct route falls from blocked diagonal to x cardinal", + route.outcome == ROUTE_REACHED_FALLBACK && + route.first_dx == 1 && route.first_dy == 0 && + route.run_dx == 1 && route.run_dy == 0 && + route.destination_x == 3 && route.destination_y == 1); + free(topology); +} + + + int main(void) { + test_tagged_route_outcomes_and_payload(); + test_route_cost_order_is_deterministic(); + test_direct_route_policy_preserves_greedy_step_order(); test_arena_topology_rejects_invalid_bounds(); test_arena_topology_extreme_origins_and_reader_bounds(); test_arena_topology_bounds_collision_los_and_revision(); - test_same_target_selection_preserves_route(); + test_target_selection_does_not_own_route_cache(); test_attack_route_field_stops_at_first_reachable_target_edge(); test_unreachable_attack_route_field_exhausts_for_fallback(); test_attack_route_caches_walkability_per_tile(); diff --git a/ocean/osrs_colosseum/osrs_colosseum.h b/ocean/osrs_colosseum/osrs_colosseum.h index 10d176f227..09fd6a3e35 100644 --- a/ocean/osrs_colosseum/osrs_colosseum.h +++ b/ocean/osrs_colosseum/osrs_colosseum.h @@ -213,6 +213,7 @@ void puf_init(Env* env, Dict* kwargs) { env->num_agents = 1; env->agents[0].policy = 0; ENCOUNTER_COLOSSEUM.init_context(COLO_ENV_CONTEXT(env)); + col_bind_route_topology(&env->context, NULL); ENCOUNTER_COLOSSEUM.init_state(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env)); uint32_t seed_offset = 0; diff --git a/ocean/osrs_inferno/osrs_inferno.h b/ocean/osrs_inferno/osrs_inferno.h index bc9c0d3adc..65aaea810f 100644 --- a/ocean/osrs_inferno/osrs_inferno.h +++ b/ocean/osrs_inferno/osrs_inferno.h @@ -161,6 +161,7 @@ void puf_init(Env* env, Dict* kwargs) { env->num_agents = 1; env->agents[0].policy = 0; ENCOUNTER_INFERNO.init_context(INF_ENV_CONTEXT(env)); + inf_bind_route_topology(&env->context, NULL); ENCOUNTER_INFERNO.init_state(INF_ENV_STATE(env), INF_ENV_CONTEXT(env)); uint32_t seed_offset = 0; From d2622f4033fb9535bc8a5a4979c28ff9975cd5d2 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 02:16:32 +0300 Subject: [PATCH 13/50] Unify OSRS encounter routing --- .../colosseum/encounter_colosseum_helpers.inc | 45 +- .../encounter_colosseum_player_actions.inc | 1 - .../encounter_colosseum_render_snapshot.inc | 7 +- .../encounter_colosseum_reset_spawn.inc | 1 + .../encounter_colosseum_reward_step.inc | 1 + ocean/osrs/encounters/encounter_nh_pvp.h | 28 +- ocean/osrs/encounters/encounter_zulrah.h | 83 +-- .../inferno/encounter_inferno_helpers.inc | 47 +- .../encounter_inferno_player_actions.inc | 1 - .../encounter_inferno_render_snapshot.inc | 7 +- .../inferno/encounter_inferno_reset_spawn.inc | 1 + .../inferno/encounter_inferno_reward_step.inc | 1 + ocean/osrs/osrs_collision.h | 34 + ocean/osrs/osrs_encounter.h | 247 +------- ocean/osrs/osrs_encounter_player.h | 12 +- ocean/osrs/osrs_pathfinding.h | 595 ++++++++++++++++-- ocean/osrs/osrs_pvp_movement.h | 27 +- ocean/osrs/osrs_visual.c | 21 +- ocean/osrs/tests/bench_colo_step.c | 6 +- .../tests/bench_colosseum_forecast_profile.c | 2 +- .../tests/test_colosseum_forecast_exact.c | 115 +++- ocean/osrs/tests/test_colosseum_modifiers.c | 2 +- .../test_encounter_route_topology_setup.c | 296 +++++++++ ocean/osrs/tests/test_inferno_attack_styles.c | 3 +- .../osrs/tests/test_inferno_forecast_exact.c | 104 ++- ocean/osrs/tests/test_osrs_player_step.c | 87 ++- ocean/osrs/tests/test_zulrah_golden.c | 1 + ocean/osrs/tests/test_zulrah_hit_delay.c | 1 + ocean/osrs_colosseum/osrs_colosseum.h | 3 +- ocean/osrs_inferno/osrs_inferno.h | 3 +- 30 files changed, 1295 insertions(+), 487 deletions(-) create mode 100644 ocean/osrs/tests/test_encounter_route_topology_setup.c diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc index a6116d7d01..9cc907e05d 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc @@ -290,9 +290,6 @@ static ColConfig col_default_config(void) { typedef struct { EncounterArenaTopology* topology; - const CollisionMap* collision_map; - int world_offset_x; - int world_offset_y; } ColRouteTopologyOwner; static ColRouteTopologyOwner col_route_topology_owner; @@ -308,19 +305,10 @@ static uint32_t col_route_topology_flags(void* data, int x, int y) { return 0; } -static void col_bind_route_topology( - ColosseumContext* ctx, - const CollisionMap* collision_map -) { - if (col_route_topology_owner.topology) { - if (col_route_topology_owner.collision_map != collision_map || - col_route_topology_owner.world_offset_x != ctx->world_offset_x || - col_route_topology_owner.world_offset_y != ctx->world_offset_y) { - fprintf(stderr, "colosseum route topology geometry identity changed\n"); - abort(); - } - ctx->route_topology = col_route_topology_owner.topology; - return; +static void col_finalize_route_topology(ColosseumContext* ctx) { + if (!ctx || ctx->route_topology) { + fprintf(stderr, "colosseum route topology finalized twice\n"); + abort(); } col_build_static_arena(); EncounterArenaTopologyBuildSpec spec = { @@ -333,15 +321,27 @@ static void col_bind_route_topology( .tile_flags = col_route_topology_flags, .tile_flags_ctx = NULL, }; - col_route_topology_owner.topology = - encounter_arena_topology_build(&spec); - encounter_arena_topology_finalize(col_route_topology_owner.topology); - col_route_topology_owner.collision_map = collision_map; - col_route_topology_owner.world_offset_x = ctx->world_offset_x; - col_route_topology_owner.world_offset_y = ctx->world_offset_y; + if (!col_route_topology_owner.topology) { + col_route_topology_owner.topology = + encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(col_route_topology_owner.topology); + } else { + encounter_arena_topology_require_spec( + col_route_topology_owner.topology, + &spec, + "colosseum"); + } ctx->route_topology = col_route_topology_owner.topology; } +static void col_finalize_context( + EncounterState* state, + EncounterContext* context +) { + (void)state; + col_finalize_route_topology((ColosseumContext*)context); +} + static void col_init_context_typed(ColosseumContext* ctx) { memset(ctx, 0, sizeof(*ctx)); ctx->config = col_default_config(); @@ -360,7 +360,6 @@ static ColosseumContext* col_legacy_context(void) { static int initialized = 0; if (!initialized) { col_init_context_typed(&ctx); - col_bind_route_topology(&ctx, NULL); initialized = 1; } return &ctx; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc index fcda499cbe..1ccfb8613b 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -1057,7 +1057,6 @@ static ColoPrimaryStepPlan col_primary_step_plan( }, .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, - .explicit_cost_policy = ENCOUNTER_ROUTE_COST_DIRECT, .collision_map = ctx->collision_map, .world_offset_x = ctx->world_offset_x, .world_offset_y = ctx->world_offset_y, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 26464e4369..1ef3d268c1 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -146,12 +146,10 @@ static void col_put_ptr_ctx( ) { (void)state; ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); - if (strcmp(key, "collision_map") == 0) { - col_bind_route_topology(ctx, (const CollisionMap*)value); + if (strcmp(key, "collision_map") == 0) ctx->collision_map = (const CollisionMap*)value; - } else { + else encounter_abort_unknown_config("colosseum", "ptr", key); - } } static int col_get_tick_ctx(EncounterState* state, EncounterContext* context) { @@ -1120,6 +1118,7 @@ static const EncounterDef ENCOUNTER_COLOSSEUM = { .init_context = col_init_context, .destroy_context = col_destroy_context, .init_state = col_init_state, + .finalize_context = col_finalize_context, .create = col_create, .destroy = col_destroy, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc index 98b160d7a6..bf7e7d7575 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -480,6 +480,7 @@ static void col_reset_ctx(EncounterState* state, EncounterContext* context, uint col_build_npc_stats(); ColosseumState* s = (ColosseumState*)state; ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + encounter_arena_topology_require_finalized(ctx->route_topology); osrs_actor_route_cache_clear(&ctx->player_route_cache); int saved_start = ctx->config.start_wave; uint32_t saved_rng = s->rng_state; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc index 4cbb3ccbe7..bcc56b1541 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc @@ -397,6 +397,7 @@ static ColoPrePlayerPhase col_advance_pre_player_phase_ctx( static void col_step_ctx(EncounterState* state, EncounterContext* context, const int* actions) { ColosseumState* s = (ColosseumState*)state; ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + encounter_arena_topology_require_finalized(ctx->route_topology); if (s->episode_over) return; #ifdef COLO_PROFILE_ENABLED int col_prof_enabled = diff --git a/ocean/osrs/encounters/encounter_nh_pvp.h b/ocean/osrs/encounters/encounter_nh_pvp.h index 9c36bb7c9f..8386a1b4c6 100644 --- a/ocean/osrs/encounters/encounter_nh_pvp.h +++ b/ocean/osrs/encounters/encounter_nh_pvp.h @@ -69,13 +69,25 @@ static void nh_pvp_init_context(EncounterContext* context) { static void nh_pvp_destroy_context(EncounterContext* context) { (void)context; } +static void nh_pvp_finalize_context( + EncounterState* state, + EncounterContext* context +) { + NhPvpState* s = (NhPvpState*)state; + NhPvpContext* ctx = (NhPvpContext*)context; + if (!ctx || ctx->route_topology) { + fprintf(stderr, "NH PvP route topology finalized twice\n"); + abort(); + } + ctx->route_topology = pvp_route_topology_finalize( + (const CollisionMap*)s->env.collision_map); +} + static void nh_pvp_reset(EncounterState* state, EncounterContext* context, uint32_t seed) { NhPvpState* s = (NhPvpState*)state; NhPvpContext* ctx = (NhPvpContext*)context; - if (!ctx->route_topology) - ctx->route_topology = pvp_route_topology_setup( - (const CollisionMap*)s->env.collision_map); + encounter_arena_topology_require_finalized(ctx->route_topology); if (seed != 0) { s->env.has_rng_seed = 1; s->env.rng_seed = seed; @@ -87,6 +99,7 @@ static void nh_pvp_reset(EncounterState* state, EncounterContext* context, uint3 static void nh_pvp_step(EncounterState* state, EncounterContext* context, const int* actions) { NhPvpContext* ctx = (NhPvpContext*)context; NhPvpState* s = (NhPvpState*)state; + encounter_arena_topology_require_finalized(ctx->route_topology); memcpy(s->env.ocean_io.agent_actions, actions, NUM_ACTION_HEADS * sizeof(int)); pvp_step(&s->env, ctx->route_topology, ctx->player_route_cache); } @@ -97,6 +110,7 @@ static void nh_pvp_step_human_commands( HumanInput* hi ) { NhPvpContext* ctx = (NhPvpContext*)context; + encounter_arena_topology_require_finalized(ctx->route_topology); NhPvpState* s = (NhPvpState*)state; int saved_use_c_opponent_p0 = s->env.pvp_runtime.use_c_opponent_p0; s->env.pvp_runtime.use_c_opponent_p0 = 0; @@ -246,13 +260,10 @@ static void nh_pvp_put_ptr( const char* key, void* value ) { + (void)context; NhPvpState* s = (NhPvpState*)state; - if (strcmp(key, "collision_map") == 0) { - NhPvpContext* ctx = (NhPvpContext*)context; + if (strcmp(key, "collision_map") == 0) s->env.collision_map = value; - ctx->route_topology = - pvp_route_topology_setup((const CollisionMap*)value); - } } static void* nh_pvp_get_log(EncounterState* state, EncounterContext* context) { @@ -283,6 +294,7 @@ static const EncounterDef ENCOUNTER_NH_PVP = { .context_size = sizeof(NhPvpContext), .init_context = nh_pvp_init_context, .destroy_context = nh_pvp_destroy_context, + .finalize_context = nh_pvp_finalize_context, .create = nh_pvp_create, .destroy = nh_pvp_destroy, diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h index b6da52e3d6..cda9067707 100644 --- a/ocean/osrs/encounters/encounter_zulrah.h +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -700,9 +700,6 @@ static inline int zul_on_platform(ZulrahState* s, int x, int y) { typedef struct { EncounterArenaTopology* topology; - const CollisionMap* collision_map; - int world_offset_x; - int world_offset_y; } ZulRouteTopologyOwner; typedef struct { @@ -727,23 +724,16 @@ static uint32_t zul_route_topology_flags(void* data, int x, int y) { y + build->world_offset_y); } -static void zul_bind_route_topology( +static void zul_finalize_route_topology( ZulrahContext* ctx, - const ZulrahState* state, - const CollisionMap* collision_map + const ZulrahState* state ) { - if (zul_route_topology_owner.topology) { - if (zul_route_topology_owner.collision_map != collision_map || - zul_route_topology_owner.world_offset_x != state->world_offset_x || - zul_route_topology_owner.world_offset_y != state->world_offset_y) { - fprintf(stderr, "zulrah route topology geometry identity changed\n"); - abort(); - } - ctx->route_topology = zul_route_topology_owner.topology; - return; + if (!ctx || !state || ctx->route_topology) { + fprintf(stderr, "zulrah route topology finalized twice\n"); + abort(); } ZulRouteTopologyBuildContext build = { - collision_map, + (const CollisionMap*)state->collision_map, state->world_offset_x, state->world_offset_y, }; @@ -757,15 +747,28 @@ static void zul_bind_route_topology( .tile_flags = zul_route_topology_flags, .tile_flags_ctx = &build, }; - zul_route_topology_owner.topology = - encounter_arena_topology_build(&spec); - encounter_arena_topology_finalize(zul_route_topology_owner.topology); - zul_route_topology_owner.collision_map = collision_map; - zul_route_topology_owner.world_offset_x = state->world_offset_x; - zul_route_topology_owner.world_offset_y = state->world_offset_y; + if (!zul_route_topology_owner.topology) { + zul_route_topology_owner.topology = + encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(zul_route_topology_owner.topology); + } else { + encounter_arena_topology_require_spec( + zul_route_topology_owner.topology, + &spec, + "zulrah"); + } ctx->route_topology = zul_route_topology_owner.topology; } +static void zul_finalize_context( + EncounterState* state, + EncounterContext* context +) { + zul_finalize_route_topology( + (ZulrahContext*)context, + (const ZulrahState*)state); +} + static inline int zul_player_in_cloud(int cx, int cy, int px, int py) { return px >= cx && px < cx + ZUL_CLOUD_SIZE && py >= cy && py < cy + ZUL_CLOUD_SIZE; @@ -2399,6 +2402,7 @@ static void zul_seed_inventory_cells(ZulrahState* s) { static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t seed) { ZulrahContext* ctx = (ZulrahContext*)context; + encounter_arena_topology_require_finalized(ctx->route_topology); osrs_actor_route_cache_clear(&ctx->player_route_cache); ZulrahState* s = (ZulrahState*)state; Log saved_log = s->log; @@ -2452,9 +2456,6 @@ static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t (s->gear_tier >= 1) ? OFFENSIVE_PRAYER_AUGURY : OFFENSIVE_PRAYER_NONE; zul_mark_live_stats_dirty(s); zul_start_active_kill(s); - if (ctx && !ctx->route_topology) - zul_bind_route_topology( - ctx, s, (const CollisionMap*)s->collision_map); } static void zul_step_tick( @@ -2611,7 +2612,6 @@ static void zul_step_tick( .blockers = {0}, .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, - .explicit_cost_policy = ENCOUNTER_ROUTE_COST_OSRS, .collision_map = (const CollisionMap*)s->collision_map, .world_offset_x = s->world_offset_x, .world_offset_y = s->world_offset_y, @@ -2685,11 +2685,11 @@ static void zul_step_tick( static void zul_step(EncounterState* state, EncounterContext* context, const int* actions) { ZulrahState* s = (ZulrahState*)state; ZulrahContext* ctx = (ZulrahContext*)context; - static ZulrahContext legacy_ctx; - if (!ctx) ctx = &legacy_ctx; - if (!ctx->route_topology) - zul_bind_route_topology( - ctx, s, (const CollisionMap*)s->collision_map); + if (!ctx) { + fprintf(stderr, "zulrah step missing context\n"); + abort(); + } + encounter_arena_topology_require_finalized(ctx->route_topology); if (s->episode_over) return; zul_step_tick(s, ctx, actions); s->reward = zul_compute_reward(s); @@ -2984,16 +2984,18 @@ static void zul_put_float(EncounterState* st, EncounterContext* context, const c } else encounter_abort_unknown_config("zulrah", "float", k); } -static void zul_put_ptr(EncounterState* st, EncounterContext* context, const char* k, void* v) { +static void zul_put_ptr( + EncounterState* st, + EncounterContext* context, + const char* key, + void* value +) { + (void)context; ZulrahState* s = (ZulrahState*)st; - if (strcmp(k, "collision_map") == 0) { - ZulrahContext* ctx = (ZulrahContext*)context; - s->collision_map = v; - if (ctx) - zul_bind_route_topology(ctx, s, (const CollisionMap*)v); - } else { - encounter_abort_unknown_config("zulrah", "ptr", k); - } + if (strcmp(key, "collision_map") == 0) + s->collision_map = value; + else + encounter_abort_unknown_config("zulrah", "ptr", key); } static void* zul_get_log(EncounterState* state, EncounterContext* context) { @@ -3341,6 +3343,7 @@ static const EncounterDef ENCOUNTER_ZULRAH = { .init_context = zul_init_context, .destroy_context = zul_destroy_context, .init_state = zul_init_state_ctx, + .finalize_context = zul_finalize_context, .create = zul_create, .destroy = zul_destroy, .reset = zul_reset, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc index f7fb8dec54..5b841de046 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc @@ -701,9 +701,6 @@ static InfConfig inf_default_config(void) { typedef struct { EncounterArenaTopology* topology; - const CollisionMap* collision_map; - int world_offset_x; - int world_offset_y; } InfRouteTopologyOwner; typedef struct { @@ -725,22 +722,13 @@ static uint32_t inf_route_topology_flags(void* data, int x, int y) { y + build->world_offset_y); } -static void inf_bind_route_topology( - InfernoContext* ctx, - const CollisionMap* collision_map -) { - if (inf_route_topology_owner.topology) { - if (inf_route_topology_owner.collision_map != collision_map || - inf_route_topology_owner.world_offset_x != ctx->world_offset_x || - inf_route_topology_owner.world_offset_y != ctx->world_offset_y) { - fprintf(stderr, "inferno route topology geometry identity changed\n"); - abort(); - } - ctx->route_topology = inf_route_topology_owner.topology; - return; +static void inf_finalize_route_topology(InfernoContext* ctx) { + if (!ctx || ctx->route_topology) { + fprintf(stderr, "inferno route topology finalized twice\n"); + abort(); } InfRouteTopologyBuildContext build = { - collision_map, + ctx->collision_map, ctx->world_offset_x, ctx->world_offset_y, }; @@ -754,15 +742,27 @@ static void inf_bind_route_topology( .tile_flags = inf_route_topology_flags, .tile_flags_ctx = &build, }; - inf_route_topology_owner.topology = - encounter_arena_topology_build(&spec); - encounter_arena_topology_finalize(inf_route_topology_owner.topology); - inf_route_topology_owner.collision_map = collision_map; - inf_route_topology_owner.world_offset_x = ctx->world_offset_x; - inf_route_topology_owner.world_offset_y = ctx->world_offset_y; + if (!inf_route_topology_owner.topology) { + inf_route_topology_owner.topology = + encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(inf_route_topology_owner.topology); + } else { + encounter_arena_topology_require_spec( + inf_route_topology_owner.topology, + &spec, + "inferno"); + } ctx->route_topology = inf_route_topology_owner.topology; } +static void inf_finalize_context( + EncounterState* state, + EncounterContext* context +) { + (void)state; + inf_finalize_route_topology((InfernoContext*)context); +} + static void inf_init_context_typed(InfernoContext* ctx) { memset(ctx, 0, sizeof(*ctx)); ctx->config = inf_default_config(); @@ -782,7 +782,6 @@ static InfernoContext* inf_legacy_context(void) { static int initialized = 0; if (!initialized) { inf_init_context_typed(&ctx); - inf_bind_route_topology(&ctx, NULL); ctx.log = &log; initialized = 1; } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc index 5203bd085a..6c3f879e67 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -572,7 +572,6 @@ static void inf_tick_player_ctx( }, .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, - .explicit_cost_policy = ENCOUNTER_ROUTE_COST_DIRECT, .collision_map = ctx->collision_map, .world_offset_x = ctx->world_offset_x, .world_offset_y = ctx->world_offset_y, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index c7f358a07c..417852dedd 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -180,12 +180,10 @@ static void inf_put_ptr_ctx( ) { (void)state; InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); - if (strcmp(key, "collision_map") == 0) { - inf_bind_route_topology(ctx, (const CollisionMap*)value); + if (strcmp(key, "collision_map") == 0) ctx->collision_map = (const CollisionMap*)value; - } else { + else encounter_abort_unknown_config("inferno", "ptr", key); - } } static int inf_get_tick_ctx(EncounterState* state, EncounterContext* context) { @@ -953,6 +951,7 @@ static const EncounterDef ENCOUNTER_INFERNO = { .init_context = inf_init_context, .destroy_context = inf_destroy_context, .init_state = inf_init_state, + .finalize_context = inf_finalize_context, .create = inf_create, .destroy = inf_destroy, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc index 427ea37f8b..14f09f00d1 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -347,6 +347,7 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint InfernoState* s = (InfernoState*)state; InfernoContext* ctx = (InfernoContext*)context; if (!ctx) ctx = inf_legacy_context(); + encounter_arena_topology_require_finalized(ctx->route_topology); osrs_actor_route_cache_clear(&ctx->player_route_cache); int saved_start = ctx->config.start_wave; uint32_t saved_rng = s->rng_state; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc index 5119734ea5..8bc1fc07c3 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc @@ -560,6 +560,7 @@ static void inf_record_idle_diagnostics( static void inf_step_ctx(EncounterState* state, EncounterContext* context, const int* actions) { InfernoState* s = (InfernoState*)state; InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + encounter_arena_topology_require_finalized(ctx->route_topology); if (s->episode_over) return; s->reward = 0.0f; diff --git a/ocean/osrs/osrs_collision.h b/ocean/osrs/osrs_collision.h index 2ce2510661..0fb9325107 100644 --- a/ocean/osrs/osrs_collision.h +++ b/ocean/osrs/osrs_collision.h @@ -853,6 +853,40 @@ static inline void encounter_arena_topology_finalize( encounter_arena_topology_abort("double finalization", 1); topology->finalized = 1; } +static inline void encounter_arena_topology_require_spec( + const EncounterArenaTopology* topology, + const EncounterArenaTopologyBuildSpec* spec, + const char* encounter_name +) { + if (!topology || !topology->finalized) + encounter_arena_topology_abort("unfinalized spec validation", 0); + if (!spec || + topology->origin_x != spec->origin_x || + topology->origin_y != spec->origin_y || + topology->width != spec->width || + topology->height != spec->height || + topology->max_footprint_size != spec->max_footprint_size || + topology->revision != spec->revision) { + fprintf(stderr, "%s route topology spec changed\n", encounter_name); + abort(); + } + for (int local_x = 0; local_x < topology->width; local_x++) { + for (int local_y = 0; local_y < topology->height; local_y++) { + int x = topology->origin_x + local_x; + int y = topology->origin_y + local_y; + int index = local_x * topology->height + local_y; + uint32_t flags = encounter_arena_topology_build_flags(spec, x, y); + if (topology->static_collision_flags[index] != flags) { + fprintf(stderr, + "%s route topology contents changed at (%d,%d)\n", + encounter_name, + x, + y); + abort(); + } + } + } +} static inline void encounter_arena_topology_require_finalized( const EncounterArenaTopology* topology diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index 705a69cda9..4d5eb7f943 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -820,250 +820,6 @@ static inline int encounter_entity_footprint_cardinal_adjacent( return (dx + dy) == 1; } -static inline int encounter_entity_footprint_cardinal_reachable( - const CollisionMap* cmap, - int world_offset_x, - int world_offset_y, - int player_x, - int player_y, - int target_x, - int target_y, - int target_size -) { - int target_max_x = target_x + target_size - 1; - int target_max_y = target_y + target_size - 1; - int flags = collision_get_flags( - cmap, 0, player_x + world_offset_x, player_y + world_offset_y); - - if (player_x + 1 == target_x && - player_y >= target_y && player_y <= target_max_y) - return (flags & COLLISION_WALL_EAST) == 0; - if (player_x == target_max_x + 1 && - player_y >= target_y && player_y <= target_max_y) - return (flags & COLLISION_WALL_WEST) == 0; - if (player_y + 1 == target_y && - player_x >= target_x && player_x <= target_max_x) - return (flags & COLLISION_WALL_NORTH) == 0; - if (player_y == target_max_y + 1 && - player_x >= target_x && player_x <= target_max_x) - return (flags & COLLISION_WALL_SOUTH) == 0; - return 0; -} - -static inline int encounter_entity_footprints_overlap( - int ax, int ay, int a_size, - int bx, int by, int b_size -) { - return !(ax + a_size <= bx || bx + b_size <= ax || - ay + a_size <= by || by + b_size <= ay); -} - -typedef enum { - OSRS_LOS_OPEN = 0, - OSRS_LOS_BLOCKERS, - OSRS_LOS_TILE, -} OsrsLosKind; - -typedef struct { - OsrsLosKind kind; - const LOSBlocker* blockers; - int blocker_count; - int (*tile_blocked)(void* ctx, int x, int y); - void* tile_ctx; -} OsrsLosQuery; - -static inline OsrsLosQuery osrs_los_open(void) { - OsrsLosQuery query; - query.kind = OSRS_LOS_OPEN; - query.blockers = NULL; - query.blocker_count = 0; - query.tile_blocked = NULL; - query.tile_ctx = NULL; - return query; -} - -static inline OsrsLosQuery osrs_los_blockers( - const LOSBlocker* blockers, - int blocker_count -) { - OsrsLosQuery query; - query.kind = OSRS_LOS_BLOCKERS; - query.blockers = blockers; - query.blocker_count = blocker_count; - query.tile_blocked = NULL; - query.tile_ctx = NULL; - return query; -} - -static inline OsrsLosQuery osrs_los_tile( - int (*tile_blocked)(void* ctx, int x, int y), - void* tile_ctx -) { - OsrsLosQuery query; - query.kind = OSRS_LOS_TILE; - query.blockers = NULL; - query.blocker_count = 0; - query.tile_blocked = tile_blocked; - query.tile_ctx = tile_ctx; - return query; -} - -static inline const OsrsLosQuery* osrs_los_open_query(void) { - static const OsrsLosQuery query = { - OSRS_LOS_OPEN, - NULL, - 0, - NULL, - NULL, - }; - return &query; -} - -static inline void osrs_los_require_query( - const OsrsLosQuery* query, - int attack_range -) { - if (attack_range <= 1) return; - if (!query) { - fprintf(stderr, "missing OSRS LoS query for ranged attack\n"); - abort(); - } - if (query->kind < OSRS_LOS_OPEN || query->kind > OSRS_LOS_TILE) { - fprintf(stderr, "invalid OSRS LoS query kind: %d\n", (int)query->kind); - abort(); - } - if (query->kind == OSRS_LOS_BLOCKERS) { - if (query->blocker_count < 0) { - fprintf(stderr, "negative OSRS LoS blocker count: %d\n", - query->blocker_count); - abort(); - } - if (query->blocker_count > 0 && !query->blockers) { - fprintf(stderr, "OSRS LoS blocker query is missing blockers\n"); - abort(); - } - } - if (query->kind == OSRS_LOS_TILE && !query->tile_blocked) { - fprintf(stderr, "OSRS tile LoS query is missing tile_blocked\n"); - abort(); - } -} - -static inline int osrs_los_tile_ray_clear( - const OsrsLosQuery* query, - int x0, int y0, - int x1, int y1 -) { - int dx = x1 - x0; - int dy = y1 - y0; - int adx = dx < 0 ? -dx : dx; - int ady = dy < 0 ? -dy : dy; - if (adx == 0 && ady == 0) return 1; - if (query->tile_blocked(query->tile_ctx, x1, y1)) return 0; - - if (adx > ady) { - int x = x0; - int y_fp = y0 * LOS_FP_SCALE + LOS_FP_HALF; - int slope = (dy * LOS_FP_SCALE) / adx; - int x_inc = dx > 0 ? 1 : -1; - if (dy < 0) y_fp -= 1; - while (x != x1) { - x += x_inc; - int y = y_fp >> 16; - if (query->tile_blocked(query->tile_ctx, x, y)) return 0; - y_fp += slope; - int new_y = y_fp >> 16; - if (new_y != y && - query->tile_blocked(query->tile_ctx, x, new_y)) - return 0; - } - } else { - int y = y0; - int x_fp = x0 * LOS_FP_SCALE + LOS_FP_HALF; - int slope = (dx * LOS_FP_SCALE) / ady; - int y_inc = dy > 0 ? 1 : -1; - if (dx < 0) x_fp -= 1; - while (y != y1) { - y += y_inc; - int x = x_fp >> 16; - if (query->tile_blocked(query->tile_ctx, x, y)) return 0; - x_fp += slope; - int new_x = x_fp >> 16; - if (new_x != x && - query->tile_blocked(query->tile_ctx, new_x, y)) - return 0; - } - } - return 1; -} - -static inline int osrs_los_clear( - const OsrsLosQuery* query, - int px, int py, int psize, - int tx, int ty, int tsize, - int attack_range -) { - osrs_los_require_query(query, attack_range); - if (attack_range <= 1) return 1; - - switch (query->kind) { - case OSRS_LOS_OPEN: - return 1; - - case OSRS_LOS_BLOCKERS: - return entity_has_line_of_sight( - query->blockers, - query->blocker_count, - px, - py, - psize, - tx, - ty, - tsize, - attack_range); - - case OSRS_LOS_TILE: { - int p_los_x = tx; - if (p_los_x < px) p_los_x = px; - if (p_los_x >= px + psize) p_los_x = px + psize - 1; - int p_los_y = ty; - if (p_los_y < py) p_los_y = py; - if (p_los_y >= py + psize) p_los_y = py + psize - 1; - - int t_los_x = px; - if (t_los_x < tx) t_los_x = tx; - if (t_los_x >= tx + tsize) t_los_x = tx + tsize - 1; - int t_los_y = py; - if (t_los_y < ty) t_los_y = ty; - if (t_los_y >= ty + tsize) t_los_y = ty + tsize - 1; - - return osrs_los_tile_ray_clear( - query, t_los_x, t_los_y, p_los_x, p_los_y); - } - } - - fprintf(stderr, "unhandled OSRS LoS query kind: %d\n", (int)query->kind); - abort(); -} - -static inline int encounter_player_can_attack( - int player_x, int player_y, - int target_x, int target_y, int target_size, int attack_range, - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - const OsrsLosQuery* los_query -) { - int dist = encounter_rect_distance(player_x, player_y, 1, - target_x, target_y, target_size); - if (dist < 1 || dist > attack_range) return 0; - if (attack_range == 1) - return encounter_entity_footprint_cardinal_reachable( - cmap, world_offset_x, world_offset_y, - player_x, player_y, target_x, target_y, target_size); - return osrs_los_clear(los_query, - player_x, player_y, 1, - target_x, target_y, target_size, - attack_range); -} typedef int (*encounter_npc_blocked_fn)(void* ctx, int x, int y, int size); @@ -2041,6 +1797,9 @@ typedef struct { void (*init_context)(EncounterContext* context); void (*destroy_context)(EncounterContext* context); void (*init_state)(EncounterState* state, EncounterContext* context); + void (*finalize_context)( + EncounterState* state, + EncounterContext* context); EncounterState* (*create)(void); void (*destroy)(EncounterState* state); diff --git a/ocean/osrs/osrs_encounter_player.h b/ocean/osrs/osrs_encounter_player.h index 3428f3a9c3..0acb4675a7 100644 --- a/ocean/osrs/osrs_encounter_player.h +++ b/ocean/osrs/osrs_encounter_player.h @@ -37,7 +37,6 @@ typedef struct { EncounterRouteBlockers blockers; EncounterRouteMovementMode movement_mode; EncounterRouteCostPolicy cost_policy; - EncounterRouteCostPolicy explicit_cost_policy; const CollisionMap* collision_map; int world_offset_x; int world_offset_y; @@ -183,7 +182,9 @@ static inline int osrs_player_step_apply_explicit_move( .target_size = 1, .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, .movement_mode = input->arena.movement_mode, - .cost_policy = input->arena.explicit_cost_policy, + .cost_policy = move_kind == OSRS_PLAYER_MOVE_ACTION + ? ENCOUNTER_ROUTE_COST_DIRECT + : input->arena.cost_policy, }; EncounterRouteResult route = encounter_route_solve(&route_input); return osrs_player_step_apply_route(player, &route); @@ -239,7 +240,12 @@ static inline void osrs_player_step_build_attack_route( .target_x = target->x, .target_y = target->y, .target_size = target->size, - .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, + .target_kind = ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE, + .attack_range = target->attack_range, + .collision_map = input->arena.collision_map, + .world_offset_x = input->arena.world_offset_x, + .world_offset_y = input->arena.world_offset_y, + .los_query = input->arena.los_query, .movement_mode = input->arena.movement_mode, .cost_policy = input->arena.cost_policy, }; diff --git a/ocean/osrs/osrs_pathfinding.h b/ocean/osrs/osrs_pathfinding.h index b444785992..4f9ea838ff 100644 --- a/ocean/osrs/osrs_pathfinding.h +++ b/ocean/osrs/osrs_pathfinding.h @@ -5,15 +5,287 @@ #include #include #include +#include "osrs_collision.h" + +static inline int encounter_attack_rect_distance( + int ax, + int ay, + int asize, + int bx, + int by, + int bsize +) { + int amax_x = ax + asize - 1; + int amax_y = ay + asize - 1; + int bmax_x = bx + bsize - 1; + int bmax_y = by + bsize - 1; + int dx = amax_x < bx ? bx - amax_x : (bmax_x < ax ? ax - bmax_x : 0); + int dy = amax_y < by ? by - amax_y : (bmax_y < ay ? ay - bmax_y : 0); + return dx > dy ? dx : dy; +} + +static inline int encounter_entity_footprint_cardinal_reachable( + const CollisionMap* cmap, + int world_offset_x, + int world_offset_y, + int player_x, + int player_y, + int target_x, + int target_y, + int target_size +) { + int target_max_x = target_x + target_size - 1; + int target_max_y = target_y + target_size - 1; + int flags = collision_get_flags( + cmap, 0, player_x + world_offset_x, player_y + world_offset_y); + + if (player_x + 1 == target_x && + player_y >= target_y && player_y <= target_max_y) + return (flags & COLLISION_WALL_EAST) == 0; + if (player_x == target_max_x + 1 && + player_y >= target_y && player_y <= target_max_y) + return (flags & COLLISION_WALL_WEST) == 0; + if (player_y + 1 == target_y && + player_x >= target_x && player_x <= target_max_x) + return (flags & COLLISION_WALL_NORTH) == 0; + if (player_y == target_max_y + 1 && + player_x >= target_x && player_x <= target_max_x) + return (flags & COLLISION_WALL_SOUTH) == 0; + return 0; +} + +static inline int encounter_entity_footprints_overlap( + int ax, int ay, int a_size, + int bx, int by, int b_size +) { + return !(ax + a_size <= bx || bx + b_size <= ax || + ay + a_size <= by || by + b_size <= ay); +} + +typedef enum { + OSRS_LOS_OPEN = 0, + OSRS_LOS_BLOCKERS, + OSRS_LOS_TILE, +} OsrsLosKind; +typedef struct { + OsrsLosKind kind; + const LOSBlocker* blockers; + int blocker_count; + int (*tile_blocked)(void* ctx, int x, int y); + void* tile_ctx; +} OsrsLosQuery; + +static inline OsrsLosQuery osrs_los_open(void) { + OsrsLosQuery query; + query.kind = OSRS_LOS_OPEN; + query.blockers = NULL; + query.blocker_count = 0; + query.tile_blocked = NULL; + query.tile_ctx = NULL; + return query; +} + +static inline OsrsLosQuery osrs_los_blockers( + const LOSBlocker* blockers, + int blocker_count +) { + OsrsLosQuery query; + query.kind = OSRS_LOS_BLOCKERS; + query.blockers = blockers; + query.blocker_count = blocker_count; + query.tile_blocked = NULL; + query.tile_ctx = NULL; + return query; +} + +static inline OsrsLosQuery osrs_los_tile( + int (*tile_blocked)(void* ctx, int x, int y), + void* tile_ctx +) { + OsrsLosQuery query; + query.kind = OSRS_LOS_TILE; + query.blockers = NULL; + query.blocker_count = 0; + query.tile_blocked = tile_blocked; + query.tile_ctx = tile_ctx; + return query; +} + +static inline const OsrsLosQuery* osrs_los_open_query(void) { + static const OsrsLosQuery query = { + OSRS_LOS_OPEN, + NULL, + 0, + NULL, + NULL, + }; + return &query; +} + +static inline int osrs_los_query_valid( + const OsrsLosQuery* query, + int attack_range +) { + if (attack_range <= 1) return 1; + if (!query || query->kind < OSRS_LOS_OPEN || query->kind > OSRS_LOS_TILE) + return 0; + if (query->kind == OSRS_LOS_BLOCKERS && + (query->blocker_count < 0 || + (query->blocker_count > 0 && !query->blockers))) + return 0; + return query->kind != OSRS_LOS_TILE || query->tile_blocked; +} + +static inline void osrs_los_require_query( + const OsrsLosQuery* query, + int attack_range +) { + if (osrs_los_query_valid(query, attack_range)) return; + fprintf(stderr, "invalid OSRS LoS query for attack range %d\n", attack_range); + abort(); +} + +static inline int osrs_los_tile_ray_clear( + const OsrsLosQuery* query, + int x0, int y0, + int x1, int y1 +) { + int dx = x1 - x0; + int dy = y1 - y0; + int adx = dx < 0 ? -dx : dx; + int ady = dy < 0 ? -dy : dy; + if (adx == 0 && ady == 0) return 1; + if (query->tile_blocked(query->tile_ctx, x1, y1)) return 0; + + if (adx > ady) { + int x = x0; + int y_fp = y0 * LOS_FP_SCALE + LOS_FP_HALF; + int slope = (dy * LOS_FP_SCALE) / adx; + int x_inc = dx > 0 ? 1 : -1; + if (dy < 0) y_fp -= 1; + while (x != x1) { + x += x_inc; + int y = y_fp >> 16; + if (query->tile_blocked(query->tile_ctx, x, y)) return 0; + y_fp += slope; + int new_y = y_fp >> 16; + if (new_y != y && + query->tile_blocked(query->tile_ctx, x, new_y)) + return 0; + } + } else { + int y = y0; + int x_fp = x0 * LOS_FP_SCALE + LOS_FP_HALF; + int slope = (dx * LOS_FP_SCALE) / ady; + int y_inc = dy > 0 ? 1 : -1; + if (dx < 0) x_fp -= 1; + while (y != y1) { + y += y_inc; + int x = x_fp >> 16; + if (query->tile_blocked(query->tile_ctx, x, y)) return 0; + x_fp += slope; + int new_x = x_fp >> 16; + if (new_x != x && + query->tile_blocked(query->tile_ctx, new_x, y)) + return 0; + } + } + return 1; +} + +static inline int osrs_los_clear( + const OsrsLosQuery* query, + int px, int py, int psize, + int tx, int ty, int tsize, + int attack_range +) { + osrs_los_require_query(query, attack_range); + if (attack_range <= 1) return 1; + + switch (query->kind) { + case OSRS_LOS_OPEN: + return 1; + + case OSRS_LOS_BLOCKERS: + return entity_has_line_of_sight( + query->blockers, + query->blocker_count, + px, + py, + psize, + tx, + ty, + tsize, + attack_range); + + case OSRS_LOS_TILE: { + int p_los_x = tx; + if (p_los_x < px) p_los_x = px; + if (p_los_x >= px + psize) p_los_x = px + psize - 1; + int p_los_y = ty; + if (p_los_y < py) p_los_y = py; + if (p_los_y >= py + psize) p_los_y = py + psize - 1; + + int t_los_x = px; + if (t_los_x < tx) t_los_x = tx; + if (t_los_x >= tx + tsize) t_los_x = tx + tsize - 1; + int t_los_y = py; + if (t_los_y < ty) t_los_y = ty; + if (t_los_y >= ty + tsize) t_los_y = ty + tsize - 1; + + return osrs_los_tile_ray_clear( + query, t_los_x, t_los_y, p_los_x, p_los_y); + } + } + + fprintf(stderr, "unhandled OSRS LoS query kind: %d\n", (int)query->kind); + abort(); +} + +static inline int encounter_attack_position_valid( + int player_x, + int player_y, + int target_x, + int target_y, + int target_size, + int attack_range, + const CollisionMap* cmap, + int world_offset_x, + int world_offset_y, + const OsrsLosQuery* los_query +) { + int distance = encounter_attack_rect_distance( + player_x, player_y, 1, target_x, target_y, target_size); + if (distance < 1 || distance > attack_range) return 0; + if (attack_range == 1) + return encounter_entity_footprint_cardinal_reachable( + cmap, world_offset_x, world_offset_y, + player_x, player_y, target_x, target_y, target_size); + return osrs_los_clear( + los_query, + player_x, player_y, 1, + target_x, target_y, target_size, + attack_range); +} + +static inline int encounter_player_can_attack( + int player_x, int player_y, + int target_x, int target_y, int target_size, int attack_range, + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + const OsrsLosQuery* los_query +) { + return encounter_attack_position_valid( + player_x, player_y, + target_x, target_y, target_size, attack_range, + cmap, world_offset_x, world_offset_y, los_query); +} #ifdef __cplusplus #define OSRS_THREAD_LOCAL thread_local #else #define OSRS_THREAD_LOCAL _Thread_local #endif -#include "osrs_collision.h" - #define PATHFIND_GRID_SIZE 104 #define PATHFIND_ARENA_MAX 48 #define PATHFIND_MAX_QUEUE_FULL (PATHFIND_GRID_SIZE * PATHFIND_GRID_SIZE) @@ -49,8 +321,8 @@ typedef enum { typedef enum { ENCOUNTER_ROUTE_TARGET_TILE = 0, ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, + ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE, } EncounterRouteTargetKind; - typedef enum { ENCOUNTER_ROUTE_MOVEMENT_WALK = 0, ENCOUNTER_ROUTE_MOVEMENT_RUN, @@ -62,7 +334,14 @@ typedef enum { ENCOUNTER_ROUTE_COST_DIRECT, ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, ENCOUNTER_ROUTE_COST_SOUTH_FIRST_REVERSE, + ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS, } EncounterRouteCostPolicy; +static inline int encounter_route_cost_is_osrs( + EncounterRouteCostPolicy policy +) { + return policy == ENCOUNTER_ROUTE_COST_OSRS || + policy == ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS; +} typedef int (*encounter_route_blocked_fn)( void* ctx, @@ -86,6 +365,11 @@ typedef struct { int target_y; int target_size; EncounterRouteTargetKind target_kind; + int attack_range; + const CollisionMap* collision_map; + int world_offset_x; + int world_offset_y; + const OsrsLosQuery* los_query; EncounterRouteMovementMode movement_mode; EncounterRouteCostPolicy cost_policy; } EncounterRouteInput; @@ -111,9 +395,19 @@ typedef struct { uint64_t topology_revision; uint64_t blocker_revision; int source_x; + int target_x; + int target_y; + int target_size; + int attack_range; + const LOSBlocker* los_blockers; + void* los_tile_ctx; + int los_blocker_count; + uint8_t target_kind; + uint8_t los_kind; int source_y; uint16_t visited_count; uint16_t expanded_count; + int (*los_tile_blocked)(void* ctx, int x, int y); uint16_t depth[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint16_t queue[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; int8_t via[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; @@ -133,6 +427,7 @@ typedef struct { typedef struct { uint16_t generation[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint16_t target_generation[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint16_t queue[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint16_t depth[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; int8_t via[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; @@ -144,6 +439,47 @@ typedef struct { } EncounterRouteScratch; static OSRS_THREAD_LOCAL EncounterRouteScratch encounter_route_scratch; +static inline int encounter_route_is_target( + const EncounterRouteInput* input, + int x, + int y +); +static inline void encounter_route_mark_targets( + const EncounterRouteInput* input, + EncounterRouteScratch* scratch, + uint16_t generation +) { + const EncounterArenaTopology* topology = input->topology; + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE) { + int local_x = input->target_x - topology->origin_x; + int local_y = input->target_y - topology->origin_y; + scratch->target_generation[ + local_x * topology->height + local_y] = generation; + return; + } + int margin = input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE + ? input->attack_range + : 1; + int min_x = input->target_x - margin; + int min_y = input->target_y - margin; + int max_x = input->target_x + input->target_size - 1 + margin; + int max_y = input->target_y + input->target_size - 1 + margin; + if (min_x < topology->origin_x) min_x = topology->origin_x; + if (min_y < topology->origin_y) min_y = topology->origin_y; + int topology_max_x = topology->origin_x + topology->width - 1; + int topology_max_y = topology->origin_y + topology->height - 1; + if (max_x > topology_max_x) max_x = topology_max_x; + if (max_y > topology_max_y) max_y = topology_max_y; + for (int x = min_x; x <= max_x; x++) { + for (int y = min_y; y <= max_y; y++) { + if (!encounter_route_is_target(input, x, y)) continue; + int local_x = x - topology->origin_x; + int local_y = y - topology->origin_y; + scratch->target_generation[ + local_x * topology->height + local_y] = generation; + } + } +} static inline int encounter_route_abs(int value) { return value < 0 ? -value : value; @@ -321,6 +657,15 @@ static inline int encounter_route_is_target( ) { if (input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE) return x == input->target_x && y == input->target_y; + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) + return encounter_attack_position_valid( + x, y, + input->target_x, input->target_y, input->target_size, + input->attack_range, + input->collision_map, + input->world_offset_x, + input->world_offset_y, + input->los_query); return encounter_route_footprints_cardinal_adjacent( x, y, input->actor_size, input->target_x, input->target_y, input->target_size) && @@ -355,13 +700,17 @@ static inline int encounter_route_input_valid( input->target_size < 1) return 0; if (input->target_kind < ENCOUNTER_ROUTE_TARGET_TILE || - input->target_kind > ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY) + input->target_kind > ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) + return 0; + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE && + (input->actor_size != 1 || input->attack_range < 1 || + !osrs_los_query_valid(input->los_query, input->attack_range))) return 0; if (input->movement_mode < ENCOUNTER_ROUTE_MOVEMENT_WALK || input->movement_mode > ENCOUNTER_ROUTE_MOVEMENT_RUN) return 0; if (input->cost_policy < ENCOUNTER_ROUTE_COST_OSRS || - input->cost_policy > ENCOUNTER_ROUTE_COST_SOUTH_FIRST_REVERSE) + input->cost_policy > ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS) return 0; if (input->blockers.is_blocked && input->blockers.revision == 0) return 0; if (encounter_arena_topology_footprint_blocked( @@ -505,9 +854,9 @@ static inline int encounter_route_direction_rank( static const int south_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; static const int south_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; const int* rank_dx = - input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS ? osrs_dx : south_dx; + encounter_route_cost_is_osrs(input->cost_policy) ? osrs_dx : south_dx; const int* rank_dy = - input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS ? osrs_dy : south_dy; + encounter_route_cost_is_osrs(input->cost_policy) ? osrs_dy : south_dy; for (int rank = 0; rank < 8; rank++) { if (rank_dx[rank] == dx && rank_dy[rank] == dy) return rank; } @@ -612,6 +961,23 @@ static inline int encounter_route_try_direct_destination( return 1; } +static inline void encounter_route_consider_direct_candidate( + const EncounterRouteInput* input, + int candidate_x, + int candidate_y, + int* selected_x, + int* selected_y +) { + if (!encounter_route_destination_allowed(input, candidate_x, candidate_y) || + !encounter_route_is_target(input, candidate_x, candidate_y)) + return; + if (encounter_route_direct_candidate_before( + input, candidate_x, candidate_y, *selected_x, *selected_y)) { + *selected_x = candidate_x; + *selected_y = candidate_y; + } +} + static inline int encounter_route_try_direct( const EncounterRouteInput* input, EncounterRouteResult* result @@ -621,6 +987,78 @@ static inline int encounter_route_try_direct( return encounter_route_try_direct_destination( input, input->target_x, input->target_y, result); } + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) { + if (encounter_route_is_target( + input, input->source_x, input->source_y)) + return encounter_route_try_direct_destination( + input, input->source_x, input->source_y, result); + int selected_x = INT_MIN; + int selected_y = INT_MIN; + int target_max_x = input->target_x + input->target_size - 1; + int target_max_y = input->target_y + input->target_size - 1; + int min_x = input->target_x - input->attack_range; + int max_x = target_max_x + input->attack_range; + int min_y = input->target_y - input->attack_range; + int max_y = target_max_y + input->attack_range; + int distance_x = input->source_x < min_x + ? min_x - input->source_x + : (input->source_x > max_x + ? input->source_x - max_x + : 0); + int distance_y = input->source_y < min_y + ? min_y - input->source_y + : (input->source_y > max_y + ? input->source_y - max_y + : 0); + int nearest_distance = + distance_x > distance_y ? distance_x : distance_y; + if (input->los_query->kind == OSRS_LOS_OPEN) { + for (int x = min_x; x <= max_x; x++) { + int min_y_distance = encounter_route_abs(x - input->source_x); + int min_y_delta = + encounter_route_abs(min_y - input->source_y); + if ((min_y_distance > min_y_delta + ? min_y_distance + : min_y_delta) == nearest_distance) + encounter_route_consider_direct_candidate( + input, x, min_y, &selected_x, &selected_y); + int max_y_delta = + encounter_route_abs(max_y - input->source_y); + if ((min_y_distance > max_y_delta + ? min_y_distance + : max_y_delta) == nearest_distance) + encounter_route_consider_direct_candidate( + input, x, max_y, &selected_x, &selected_y); + } + for (int y = min_y + 1; y < max_y; y++) { + int y_distance = encounter_route_abs(y - input->source_y); + int min_x_delta = + encounter_route_abs(min_x - input->source_x); + if ((min_x_delta > y_distance + ? min_x_delta + : y_distance) == nearest_distance) + encounter_route_consider_direct_candidate( + input, min_x, y, &selected_x, &selected_y); + int max_x_delta = + encounter_route_abs(max_x - input->source_x); + if ((max_x_delta > y_distance + ? max_x_delta + : y_distance) == nearest_distance) + encounter_route_consider_direct_candidate( + input, max_x, y, &selected_x, &selected_y); + } + } else { + for (int x = min_x; x <= max_x; x++) { + for (int y = min_y; y <= max_y; y++) { + encounter_route_consider_direct_candidate( + input, x, y, &selected_x, &selected_y); + } + } + } + if (selected_x == INT_MIN) return 0; + return encounter_route_try_direct_destination( + input, selected_x, selected_y, result); + } if (input->target_kind != ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY || input->actor_size != 1) return 0; @@ -848,9 +1286,9 @@ static inline void encounter_route_reverse_build_result( static const int8_t south_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; static const int8_t south_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; const int8_t* direction_dx = - input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS ? osrs_dx : south_dx; + encounter_route_cost_is_osrs(input->cost_policy) ? osrs_dx : south_dx; const int8_t* direction_dy = - input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS ? osrs_dy : south_dy; + encounter_route_cost_is_osrs(input->cost_policy) ? osrs_dy : south_dy; const EncounterArenaTopology* topology = input->topology; int current_x = input->source_x; int current_y = input->source_y; @@ -1005,6 +1443,7 @@ static inline int encounter_route_source_field_matches( const EncounterSourceRouteField* field, const EncounterRouteInput* input ) { + const OsrsLosQuery* los_query = input->los_query; return field->valid && field->topology == input->topology && field->blocker_ctx == input->blockers.ctx && @@ -1013,6 +1452,17 @@ static inline int encounter_route_source_field_matches( field->blocker_revision == input->blockers.revision && field->source_x == input->source_x && field->source_y == input->source_y && + field->target_x == input->target_x && + field->target_y == input->target_y && + field->target_size == input->target_size && + field->target_kind == input->target_kind && + field->attack_range == input->attack_range && + field->los_kind == (los_query ? los_query->kind : 0) && + field->los_blockers == (los_query ? los_query->blockers : NULL) && + field->los_blocker_count == (los_query ? los_query->blocker_count : 0) && + field->los_tile_blocked == + (los_query ? los_query->tile_blocked : NULL) && + field->los_tile_ctx == (los_query ? los_query->tile_ctx : NULL) && field->actor_size == input->actor_size && field->movement_mode == input->movement_mode && field->cost_policy == input->cost_policy; @@ -1043,6 +1493,26 @@ static inline void encounter_route_build_source_field( field->blocker_revision = input->blockers.revision; field->source_x = input->source_x; field->source_y = input->source_y; + field->target_x = input->target_x; + field->target_y = input->target_y; + field->target_size = input->target_size; + field->target_kind = (uint8_t)input->target_kind; + field->attack_range = input->attack_range; + field->los_kind = (uint8_t)(input->los_query + ? input->los_query->kind + : OSRS_LOS_OPEN); + field->los_blockers = input->los_query + ? input->los_query->blockers + : NULL; + field->los_blocker_count = input->los_query + ? input->los_query->blocker_count + : 0; + field->los_tile_blocked = input->los_query + ? input->los_query->tile_blocked + : NULL; + field->los_tile_ctx = input->los_query + ? input->los_query->tile_ctx + : NULL; field->visited_count = 1; field->expanded_count = 0; field->actor_size = (uint8_t)input->actor_size; @@ -1220,7 +1690,8 @@ static inline int encounter_route_try_source_field( const EncounterRouteInput* input, EncounterRouteResult* result ) { - if (input->target_kind != ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY || + if ((input->target_kind != ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY && + input->target_kind != ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) || input->cost_policy != ENCOUNTER_ROUTE_COST_OSRS) return 0; EncounterRouteScratch* scratch = &encounter_route_scratch; @@ -1244,25 +1715,21 @@ static inline int encounter_route_try_source_field( for (int y = input->target_y - input->actor_size + 1; y <= target_max_y; y++) { - if (encounter_arena_topology_footprint_blocked( - topology, x, y, input->actor_size) || + if (!encounter_route_destination_allowed(input, x, y) || !encounter_route_is_target(input, x, y)) continue; - int local_x = x - topology->origin_x; - int local_y = y - topology->origin_y; - target_edges[local_x] |= UINT64_C(1) << local_y; + target_edges[x - topology->origin_x] |= + UINT64_C(1) << (y - topology->origin_y); } int y = y_edges[edge]; for (int x_scan = input->target_x - input->actor_size + 1; x_scan <= target_max_x; x_scan++) { - if (encounter_arena_topology_footprint_blocked( - topology, x_scan, y, input->actor_size) || + if (!encounter_route_destination_allowed(input, x_scan, y) || !encounter_route_is_target(input, x_scan, y)) continue; - int local_x = x_scan - topology->origin_x; - int local_y = y - topology->origin_y; - target_edges[local_x] |= UINT64_C(1) << local_y; + target_edges[x_scan - topology->origin_x] |= + UINT64_C(1) << (y - topology->origin_y); } } int selected_index = encounter_route_expand_source_field( @@ -1343,6 +1810,8 @@ static inline EncounterRouteResult encounter_route_solve( input->target_x, input->target_y, input->target_size)) return encounter_route_escape_overlap(input); if (input->cost_policy != ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS && + !(input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS && + input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) && encounter_route_try_direct(input, &result)) return result; if (encounter_route_try_source_field(input, &result)) return result; @@ -1352,19 +1821,21 @@ static inline EncounterRouteResult encounter_route_solve( scratch->current_generation++; if (scratch->current_generation == 0) { memset(scratch->generation, 0, sizeof(scratch->generation)); + memset(scratch->target_generation, 0, + sizeof(scratch->target_generation)); memset(scratch->blocker_generation, 0, sizeof(scratch->blocker_generation)); scratch->current_generation = 1; } uint16_t generation = scratch->current_generation; + encounter_route_mark_targets(input, scratch, generation); int source_local_x = input->source_x - topology->origin_x; int source_local_y = input->source_y - topology->origin_y; int source_index = source_local_x * topology->height + source_local_y; scratch->generation[source_index] = generation; scratch->depth[source_index] = 0; scratch->via[source_index] = VIA_START; - scratch->queue[0] = - (uint16_t)((source_local_x << 6) | source_local_y); + scratch->queue[0] = (uint16_t)source_index; int head = 0; int tail = 1; int selected_index = -1; @@ -1385,41 +1856,53 @@ static inline EncounterRouteResult encounter_route_solve( static const uint8_t south_step_mask[8] = { 2, 8, 64, 16, 1, 32, 4, 128 }; - int osrs_cost = input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS; + int osrs_cost = encounter_route_cost_is_osrs(input->cost_policy); const int8_t* direction_dx = osrs_cost ? osrs_dx : south_dx; const int8_t* direction_dy = osrs_cost ? osrs_dy : south_dy; const int8_t* direction_via = osrs_cost ? osrs_via : south_via; const uint8_t* direction_step_mask = osrs_cost ? osrs_step_mask : south_step_mask; + int direction_delta[8]; + for (int i = 0; i < 8; i++) + direction_delta[i] = + direction_dx[i] * topology->height + direction_dy[i]; const uint8_t* legal_step_masks = topology->legal_step_masks[input->actor_size - 1]; while (head < tail) { - int current_packed = scratch->queue[head++]; - int current_x = current_packed >> 6; - int current_y = current_packed & 63; - int current_index = current_x * topology->height + current_y; - int x = topology->origin_x + current_x; - int y = topology->origin_y + current_y; - int reached_target = - input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE - ? x == input->target_x && y == input->target_y - : encounter_route_is_target(input, x, y); - if (reached_target) { + int current_index = scratch->queue[head++]; + if (scratch->target_generation[current_index] == generation) { selected_index = current_index; break; } uint16_t next_depth = (uint16_t)(scratch->depth[current_index] + 1); - for (int i = 0; i < 8; i++) { - int next_x = current_x + direction_dx[i]; - int next_y = current_y + direction_dy[i]; - if (next_x < 0 || next_x >= topology->width || - next_y < 0 || next_y >= topology->height) - continue; - int next_index = next_x * topology->height + next_y; - if (scratch->generation[next_index] == generation) continue; - int dx = direction_dx[i]; - int dy = direction_dy[i]; - if (input->blockers.is_blocked) { + uint8_t legal_mask = legal_step_masks[current_index]; + if (!input->blockers.is_blocked) { + for (int i = 0; i < 8; i++) { + if ((legal_mask & direction_step_mask[i]) == 0) continue; + int next_index = current_index + direction_delta[i]; + if (scratch->generation[next_index] == generation) continue; + if (tail >= topology->tile_count) { + fprintf(stderr, "OSRS route queue overflow: %d\n", tail); + abort(); + } + scratch->generation[next_index] = generation; + scratch->depth[next_index] = next_depth; + scratch->via[next_index] = (int8_t)direction_via[i]; + scratch->queue[tail++] = (uint16_t)next_index; + } + } else { + int current_x = current_index / topology->height; + int current_y = current_index % topology->height; + int x = topology->origin_x + current_x; + int y = topology->origin_y + current_y; + for (int i = 0; i < 8; i++) { + if ((legal_mask & direction_step_mask[i]) == 0) continue; + int next_x = current_x + direction_dx[i]; + int next_y = current_y + direction_dy[i]; + int next_index = next_x * topology->height + next_y; + if (scratch->generation[next_index] == generation) continue; + int dx = direction_dx[i]; + int dy = direction_dy[i]; if (scratch->blocker_generation[next_index] != generation) { scratch->blocker_generation[next_index] = generation; scratch->blocker_value[next_index] = (uint8_t) @@ -1455,19 +1938,15 @@ static inline EncounterRouteResult encounter_route_solve( } if (scratch->blocker_value[side_index]) continue; } + if (tail >= topology->tile_count) { + fprintf(stderr, "OSRS route queue overflow: %d\n", tail); + abort(); + } + scratch->generation[next_index] = generation; + scratch->depth[next_index] = next_depth; + scratch->via[next_index] = (int8_t)direction_via[i]; + scratch->queue[tail++] = (uint16_t)next_index; } - if ((legal_step_masks[current_index] & - direction_step_mask[i]) == 0) - continue; - if (tail >= topology->tile_count) { - fprintf(stderr, "OSRS route queue overflow: %d\n", tail); - abort(); - } - scratch->generation[next_index] = generation; - scratch->depth[next_index] = next_depth; - scratch->via[next_index] = (int8_t)direction_via[i]; - scratch->queue[tail++] = - (uint16_t)((next_x << 6) | next_y); } } @@ -1489,7 +1968,7 @@ static inline EncounterRouteResult encounter_route_solve( if (scratch->generation[index] != generation) continue; int x = topology->origin_x + local_x; int y = topology->origin_y + local_y; - if (input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS) { + if (encounter_route_cost_is_osrs(input->cost_policy)) { int64_t min_target_x = (int64_t)input->target_x - PATHFIND_MAX_FALLBACK_RADIUS; int64_t max_target_x = diff --git a/ocean/osrs/osrs_pvp_movement.h b/ocean/osrs/osrs_pvp_movement.h index cd5d54e063..b570c8b202 100644 --- a/ocean/osrs/osrs_pvp_movement.h +++ b/ocean/osrs/osrs_pvp_movement.h @@ -171,7 +171,6 @@ static int pvp_tile_walkable(void* ctx, int x, int y) { typedef struct { EncounterArenaTopology* topology; - const CollisionMap* collision_map; } PvpRouteTopologyOwner; static PvpRouteTopologyOwner pvp_route_topology_owner; @@ -184,16 +183,9 @@ static uint32_t pvp_route_topology_flags(void* data, int x, int y) { : 0; } -static const EncounterArenaTopology* pvp_route_topology_setup( +static const EncounterArenaTopology* pvp_route_topology_finalize( const CollisionMap* collision_map ) { - if (pvp_route_topology_owner.topology) { - if (pvp_route_topology_owner.collision_map != collision_map) { - fprintf(stderr, "NH PvP route topology collision map changed\n"); - abort(); - } - return pvp_route_topology_owner.topology; - } EncounterArenaTopologyBuildSpec spec = { .origin_x = FIGHT_AREA_BASE_X, .origin_y = FIGHT_AREA_BASE_Y, @@ -204,10 +196,16 @@ static const EncounterArenaTopology* pvp_route_topology_setup( .tile_flags = pvp_route_topology_flags, .tile_flags_ctx = (void*)collision_map, }; - pvp_route_topology_owner.topology = - encounter_arena_topology_build(&spec); - encounter_arena_topology_finalize(pvp_route_topology_owner.topology); - pvp_route_topology_owner.collision_map = collision_map; + if (!pvp_route_topology_owner.topology) { + pvp_route_topology_owner.topology = + encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(pvp_route_topology_owner.topology); + } else { + encounter_arena_topology_require_spec( + pvp_route_topology_owner.topology, + &spec, + "nh_pvp"); + } return pvp_route_topology_owner.topology; } @@ -343,8 +341,7 @@ static inline OsrsEncounterArena pvp_build_arena( arena.topology = topology; arena.blockers = (EncounterRouteBlockers){0}; arena.movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN; - arena.cost_policy = ENCOUNTER_ROUTE_COST_OSRS; - arena.explicit_cost_policy = ENCOUNTER_ROUTE_COST_DIRECT; + arena.cost_policy = ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS; arena.collision_map = (const CollisionMap*)env->collision_map; arena.world_offset_x = 0; arena.world_offset_y = 0; diff --git a/ocean/osrs/osrs_visual.c b/ocean/osrs/osrs_visual.c index fac18d34f5..ac3a1c06df 100644 --- a/ocean/osrs/osrs_visual.c +++ b/ocean/osrs/osrs_visual.c @@ -104,7 +104,7 @@ static void print_env_state(OsrsEnv* env) { static void run_random_episode(OsrsEnv* env, int verbose) { const EncounterArenaTopology* route_topology = - pvp_route_topology_setup((const CollisionMap*)env->collision_map); + pvp_route_topology_finalize((const CollisionMap*)env->collision_map); OsrsActorRouteCache route_cache[NUM_AGENTS] = {0}; pvp_reset(env); @@ -134,7 +134,7 @@ static void run_random_episode(OsrsEnv* env, int verbose) { static void benchmark(OsrsEnv* env, int num_steps) { const EncounterArenaTopology* route_topology = - pvp_route_topology_setup((const CollisionMap*)env->collision_map); + pvp_route_topology_finalize((const CollisionMap*)env->collision_map); OsrsActorRouteCache route_cache[NUM_AGENTS] = {0}; printf("Benchmarking %d steps...\n", num_steps); @@ -339,9 +339,9 @@ static VisualCollisionLoad visual_load_encounter_collision_map( } VisualCollisionLoad result = { NULL, offset_x, offset_y }; if (cmap) { - edef->put_ptr(env->encounter_state, env->encounter_context, "collision_map", cmap); edef->put_int(env->encounter_state, env->encounter_context, "world_offset_x", offset_x); edef->put_int(env->encounter_state, env->encounter_context, "world_offset_y", offset_y); + edef->put_ptr(env->encounter_state, env->encounter_context, "collision_map", cmap); env->collision_map = cmap; result.cmap = cmap; } @@ -359,6 +359,13 @@ static const EncounterDef* visual_open_encounter(OsrsEnv* env, const char* encou env->encounter_context = visual_create_encounter_context(edef); return edef; } +static void visual_finalize_encounter( + const EncounterDef* edef, + OsrsEnv* env +) { + if (edef->finalize_context) + edef->finalize_context(env->encounter_state, env->encounter_context); +} static void run_profile( OsrsEnv* env, @@ -389,6 +396,7 @@ static void run_profile( start_wave); fprintf(stderr, "start_wave: %d\n", start_wave); } + visual_finalize_encounter(edef, env); edef->reset(env->encounter_state, env->encounter_context, profile_seed); } else { env->pvp_runtime.use_c_opponent = 1; @@ -400,7 +408,7 @@ static void run_profile( } const EncounterArenaTopology* direct_pvp_topology = encounter_name ? NULL - : pvp_route_topology_setup((const CollisionMap*)env->collision_map); + : pvp_route_topology_finalize((const CollisionMap*)env->collision_map); OsrsActorRouteCache direct_pvp_route_cache[NUM_AGENTS] = {0}; const EncounterDef* profile_edef = (const EncounterDef*)env->encounter_def; @@ -1210,6 +1218,7 @@ static void run_policy_profile( "start_wave", start_wave); edef->put_int(env->encounter_state, env->encounter_context, "loadout_profile_mode", loadout_mode); + visual_finalize_encounter(edef, env); edef->reset(env->encounter_state, env->encounter_context, policy_seed); VisualPolicy policy; @@ -1623,6 +1632,7 @@ static void run_metrics( if (bis_oracle) edef->put_int(env->encounter_state, env->encounter_context, "bis_gear_oracle_mode", 1); + visual_finalize_encounter(edef, env); edef->reset(env->encounter_state, env->encounter_context, policy_seed); VisualPolicy policy; @@ -1954,6 +1964,7 @@ static void run_visual( "start_wave", start_wave); } + visual_finalize_encounter(edef, env); edef->reset(env->encounter_state, env->encounter_context, 0); fprintf(stderr, "encounter: %s (obs=%d, heads=%d)\n", edef->name, edef->obs_size, edef->num_action_heads); @@ -1979,7 +1990,7 @@ static void run_visual( RenderClient* rc = (RenderClient*)env->client; rc->route_topology = (!encounter_name || strcmp(encounter_name, "pvp") == 0) - ? pvp_route_topology_setup((const CollisionMap*)env->collision_map) + ? pvp_route_topology_finalize((const CollisionMap*)env->collision_map) : NULL; pvp_actor_route_caches_clear(rc->player_route_cache); #ifdef __EMSCRIPTEN__ diff --git a/ocean/osrs/tests/bench_colo_step.c b/ocean/osrs/tests/bench_colo_step.c index 97d79185ed..1bd579f8e9 100644 --- a/ocean/osrs/tests/bench_colo_step.c +++ b/ocean/osrs/tests/bench_colo_step.c @@ -97,7 +97,7 @@ static Bench run_multi_sim_only(int num_envs, int steps_per_env) { uint64_t rng = 0xABCDEF01ULL; for (int e = 0; e < num_envs; e++) { col_init_context_typed(&ctxs[e]); - col_bind_route_topology(&ctxs[e], NULL); + col_finalize_route_topology(&ctxs[e]); col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], (uint32_t)(e + 1)); } double t0 = now_ns(); @@ -130,7 +130,7 @@ static Bench run_multi(int num_envs, int steps_per_env) { for (int e = 0; e < num_envs; e++) { col_init_context_typed(&ctxs[e]); - col_bind_route_topology(&ctxs[e], NULL); + col_finalize_route_topology(&ctxs[e]); col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], (uint32_t)(e + 1)); } for (int rep = 0; rep < 40; rep++) { @@ -167,7 +167,7 @@ static Bench run(int warmup_steps, int measured_steps) { Bench b = {0}; col_init_context_typed(&ctx); - col_bind_route_topology(&ctx, NULL); + col_finalize_route_topology(&ctx); memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 12345u); uint64_t rng = 0xBEEF1234ULL; diff --git a/ocean/osrs/tests/bench_colosseum_forecast_profile.c b/ocean/osrs/tests/bench_colosseum_forecast_profile.c index cab0fc541f..795f9ea98c 100644 --- a/ocean/osrs/tests/bench_colosseum_forecast_profile.c +++ b/ocean/osrs/tests/bench_colosseum_forecast_profile.c @@ -123,11 +123,11 @@ static void bench_trace_actions( static void bench_init_context(ColosseumContext* ctx, int start_wave) { col_init_context_typed(ctx); - col_bind_route_topology(ctx, NULL); ctx->config.start_wave = start_wave; ctx->config.step_out_forecast_obs_enabled = 1; ctx->config.forecast_horizon = COLO_STEP_OUT_FORECAST_HORIZON; ctx->config.forecast_run_tile_mode = COLO_FORECAST_RUN_TILE_FULL; + col_finalize_route_topology(ctx); } static void bench_capture_state( diff --git a/ocean/osrs/tests/test_colosseum_forecast_exact.c b/ocean/osrs/tests/test_colosseum_forecast_exact.c index 8aa4a192af..737c20adf5 100644 --- a/ocean/osrs/tests/test_colosseum_forecast_exact.c +++ b/ocean/osrs/tests/test_colosseum_forecast_exact.c @@ -50,6 +50,7 @@ static void col_step_out_forecast_landing_selftest(void) { ColosseumState s; col_init_context_typed(&ctx); memset(&s, 0, sizeof(s)); + col_finalize_route_topology(&ctx); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 0x51A7u); memset(s.npcs, 0, sizeof(s.npcs)); memset(s.npc_collision_flags, 0, sizeof(s.npc_collision_flags)); @@ -244,6 +245,8 @@ static void exact_zero_serialized_route_storage(ColosseumState* state) { memset(&state->player.interaction.route, 0, sizeof(state->player.interaction.route)); memset(&state->interaction.route, 0, sizeof(state->interaction.route)); #endif + state->log.npc_blocked_calls = 0.0f; + state->log.npc_blocked_tiles = 0.0f; } static void exact_capture( @@ -317,11 +320,9 @@ static void exact_init_state( uint32_t seed ) { col_init_context_typed(ctx); -#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES - col_bind_route_topology(ctx, NULL); -#endif ctx->config.start_wave = start_wave; memset(s, 0, sizeof(*s)); + col_finalize_route_topology(ctx); col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); } @@ -868,6 +869,13 @@ static void exact_generate_fixture(const char* path) { exact_writer_close(&writer); } +static void exact_read_all(FILE* file, void* data, size_t size) { + if (fread(data, 1, size, file) != size) { + fprintf(stderr, "truncated colosseum exact fixture\n"); + abort(); + } +} + static int exact_compare_files(const char* expected_path, const char* actual_path) { FILE* expected = fopen(expected_path, "rb"); if (!expected) { @@ -880,37 +888,96 @@ static int exact_compare_files(const char* expected_path, const char* actual_pat abort(); } - uint8_t expected_buf[EXACT_CHUNK_BYTES]; - uint8_t actual_buf[EXACT_CHUNK_BYTES]; - uint64_t offset = 0; - for (;;) { - size_t ne = fread(expected_buf, 1, sizeof(expected_buf), expected); - size_t na = fread(actual_buf, 1, sizeof(actual_buf), actual); - if (ne != na) { - printf("colosseum exact mismatch: size differs at byte %llu\n", - (unsigned long long)offset); + ColoExactFileHeader expected_file; + ColoExactFileHeader actual_file; + exact_read_all(expected, &expected_file, sizeof(expected_file)); + exact_read_all(actual, &actual_file, sizeof(actual_file)); + if (memcmp(&expected_file, &actual_file, sizeof(expected_file)) != 0) { + printf("colosseum exact mismatch: file header\n"); + fclose(expected); + fclose(actual); + return 1; + } + + for (uint32_t index = 0; index < expected_file.record_count; index++) { + ColoExactRecordHeader expected_record; + ColoExactRecordHeader actual_record; + float expected_obs[COLO_NUM_OBS]; + float actual_obs[COLO_NUM_OBS]; + float expected_mask[COLO_ACTION_MASK_SIZE]; + float actual_mask[COLO_ACTION_MASK_SIZE]; + ColosseumState expected_state; + ColosseumState actual_state; + exact_read_all(expected, &expected_record, sizeof(expected_record)); + exact_read_all(actual, &actual_record, sizeof(actual_record)); + exact_read_all(expected, expected_obs, sizeof(expected_obs)); + exact_read_all(actual, actual_obs, sizeof(actual_obs)); + exact_read_all(expected, expected_mask, sizeof(expected_mask)); + exact_read_all(actual, actual_mask, sizeof(actual_mask)); + exact_read_all(expected, &expected_state, sizeof(expected_state)); + exact_read_all(actual, &actual_state, sizeof(actual_state)); + + if (exact_hash_bytes(&expected_state, sizeof(expected_state)) != + expected_record.state_hash || + exact_hash_bytes(&actual_state, sizeof(actual_state)) != + actual_record.state_hash) { + printf("colosseum exact fixture state hash mismatch at record %u\n", index); fclose(expected); fclose(actual); return 1; } - if (ne == 0) break; - if (memcmp(expected_buf, actual_buf, ne) != 0) { - for (size_t i = 0; i < ne; i++) { - if (expected_buf[i] == actual_buf[i]) continue; - printf("colosseum exact mismatch at byte %llu: expected %u got %u\n", - (unsigned long long)(offset + i), - (unsigned)expected_buf[i], - (unsigned)actual_buf[i]); - fclose(expected); - fclose(actual); - return 1; + exact_zero_serialized_route_storage(&expected_state); + exact_zero_serialized_route_storage(&actual_state); + expected_record.state_hash = + exact_hash_bytes(&expected_state, sizeof(expected_state)); + actual_record.state_hash = + exact_hash_bytes(&actual_state, sizeof(actual_state)); + if (memcmp(&expected_record, &actual_record, sizeof(expected_record)) != 0 || + memcmp(expected_obs, actual_obs, sizeof(expected_obs)) != 0 || + memcmp(expected_mask, actual_mask, sizeof(expected_mask)) != 0 || + memcmp(&expected_state, &actual_state, sizeof(expected_state)) != 0) { + printf( + "colosseum exact mismatch at record %u scenario %u step %u\n", + index, + expected_record.scenario_id, + expected_record.step_index); + printf( + "expected player=(%d,%d) dest=(%d,%d) target=%d actual player=(%d,%d) dest=(%d,%d) target=%d\n", + expected_state.player.x, + expected_state.player.y, + expected_state.player.dest_x, + expected_state.player.dest_y, + expected_state.player.interaction.target_slot, + actual_state.player.x, + actual_state.player.y, + actual_state.player.dest_x, + actual_state.player.dest_y, + actual_state.player.interaction.target_slot); + const uint8_t* expected_bytes = (const uint8_t*)&expected_state; + const uint8_t* actual_bytes = (const uint8_t*)&actual_state; + for (size_t byte = 0; byte < sizeof(expected_state); byte++) { + if (expected_bytes[byte] == actual_bytes[byte]) continue; + printf( + "first state byte mismatch offset=%zu expected=%u actual=%u\n", + byte, + (unsigned)expected_bytes[byte], + (unsigned)actual_bytes[byte]); + break; } + fclose(expected); + fclose(actual); + return 1; } - offset += (uint64_t)ne; } + int expected_tail = fgetc(expected); + int actual_tail = fgetc(actual); fclose(expected); fclose(actual); + if (expected_tail != EOF || actual_tail != EOF) { + printf("colosseum exact mismatch: trailing data\n"); + return 1; + } return 0; } diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index e8f2301f7f..2db016e881 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -9,7 +9,7 @@ #define col_init_context_typed(ctx_ptr) do { \ col_init_context_typed(ctx_ptr); \ - col_bind_route_topology((ctx_ptr), NULL); \ + col_finalize_route_topology((ctx_ptr)); \ (ctx_ptr)->config.late_start_state_mode = 0; \ } while (0) diff --git a/ocean/osrs/tests/test_encounter_route_topology_setup.c b/ocean/osrs/tests/test_encounter_route_topology_setup.c new file mode 100644 index 0000000000..5f5ae41b2b --- /dev/null +++ b/ocean/osrs/tests/test_encounter_route_topology_setup.c @@ -0,0 +1,296 @@ +#include +#include +#include + +#if defined(TEST_ROUTE_TOPOLOGY_INFERNO) +#include "ocean/osrs/encounters/encounter_inferno.h" +#define TEST_DEF ENCOUNTER_INFERNO +#define TEST_MAP_PATH "ocean/osrs/data/inferno.cmap" +#define TEST_OFFSET_X 2246 +#define TEST_OFFSET_Y 5315 +#define TEST_ORIGIN_X INF_ARENA_MIN_X +#define TEST_ORIGIN_Y INF_ARENA_MIN_Y +#define TEST_WIDTH INF_ARENA_WIDTH +#define TEST_HEIGHT INF_ARENA_HEIGHT +typedef InfernoContext TestContext; +#define TEST_TOPOLOGY(ctx) ((ctx)->route_topology) +static uint32_t expected_flags(const CollisionMap* map, int x, int y) { + return (uint32_t)collision_get_flags( + map, 0, x + TEST_OFFSET_X, y + TEST_OFFSET_Y); +} +#elif defined(TEST_ROUTE_TOPOLOGY_COLOSSEUM) +#include "ocean/osrs/encounters/encounter_colosseum.h" +#define TEST_DEF ENCOUNTER_COLOSSEUM +#define TEST_MAP_PATH "ocean/osrs/data/colosseum.cmap" +#define TEST_OFFSET_X 1808 +#define TEST_OFFSET_Y 3090 +#define TEST_ORIGIN_X COLO_ARENA_MIN_X +#define TEST_ORIGIN_Y COLO_ARENA_MIN_Y +#define TEST_WIDTH COLO_ARENA_WIDTH +#define TEST_HEIGHT COLO_ARENA_HEIGHT +typedef ColosseumContext TestContext; +#define TEST_TOPOLOGY(ctx) ((ctx)->route_topology) +static uint32_t expected_flags(const CollisionMap* map, int x, int y) { + (void)map; + return col_route_topology_flags(NULL, x, y); +} +#elif defined(TEST_ROUTE_TOPOLOGY_ZULRAH) +#include "ocean/osrs/encounters/encounter_zulrah.h" +#define TEST_DEF ENCOUNTER_ZULRAH +#define TEST_MAP_PATH "ocean/osrs/data/zulrah.cmap" +#define TEST_OFFSET_X 2256 +#define TEST_OFFSET_Y 3061 +#define TEST_ORIGIN_X 0 +#define TEST_ORIGIN_Y 0 +#define TEST_WIDTH ZUL_ARENA_SIZE +#define TEST_HEIGHT ZUL_ARENA_SIZE +typedef ZulrahContext TestContext; +#define TEST_TOPOLOGY(ctx) ((ctx)->route_topology) +static uint32_t expected_flags(const CollisionMap* map, int x, int y) { + return (uint32_t)collision_get_flags( + map, 0, x + TEST_OFFSET_X, y + TEST_OFFSET_Y); +} +#elif defined(TEST_ROUTE_TOPOLOGY_NH_PVP) +#include "ocean/osrs/encounters/encounter_nh_pvp.h" +#define TEST_DEF ENCOUNTER_NH_PVP +#define TEST_MAP_PATH "ocean/osrs/data/wilderness.cmap" +#define TEST_OFFSET_X 0 +#define TEST_OFFSET_Y 0 +#define TEST_ORIGIN_X FIGHT_AREA_BASE_X +#define TEST_ORIGIN_Y FIGHT_AREA_BASE_Y +#define TEST_WIDTH FIGHT_AREA_WIDTH +#define TEST_HEIGHT FIGHT_AREA_HEIGHT +typedef NhPvpContext TestContext; +#define TEST_TOPOLOGY(ctx) ((ctx)->route_topology) +static uint32_t expected_flags(const CollisionMap* map, int x, int y) { + if (!is_in_wilderness(x, y)) return COLLISION_BLOCKED | LOS_FULL_MASK; + return (uint32_t)collision_get_flags(map, 0, x, y); +} +typedef struct { + int count; + int x[16]; + int y[16]; +} NhPvpTestBlockers; + +static int nh_pvp_test_blocked(void* data, int x, int y, int size) { + (void)size; + const NhPvpTestBlockers* blockers = (const NhPvpTestBlockers*)data; + for (int i = 0; i < blockers->count; i++) + if (blockers->x[i] == x && blockers->y[i] == y) return 1; + return 0; +} + +static int nh_pvp_route_results_equal( + const EncounterRouteResult* source_field, + const EncounterRouteResult* target_bfs +) { + if (source_field->outcome != target_bfs->outcome || + source_field->destination_x != target_bfs->destination_x || + source_field->destination_y != target_bfs->destination_y || + source_field->first_dx != target_bfs->first_dx || + source_field->first_dy != target_bfs->first_dy || + source_field->run_dx != target_bfs->run_dx || + source_field->run_dy != target_bfs->run_dy || + source_field->distance != target_bfs->distance || + source_field->waypoint_count != target_bfs->waypoint_count) + return 0; + for (int i = 0; i < source_field->waypoint_count; i++) { + if (source_field->waypoint_x[i] != target_bfs->waypoint_x[i] || + source_field->waypoint_y[i] != target_bfs->waypoint_y[i]) + return 0; + } + return 1; +} + +static uint64_t nh_pvp_target_bfs_equivalence( + const EncounterArenaTopology* topology, + NhPvpTestBlockers* blockers, + uint64_t blocker_revision +) { + uint64_t checks = 0; + EncounterRouteInput input = { + .topology = topology, + .blockers = { + .is_blocked = blockers->count ? nh_pvp_test_blocked : NULL, + .ctx = blockers, + .revision = blocker_revision, + }, + .actor_size = 1, + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + }; + for (int source_x = topology->origin_x; + source_x < topology->origin_x + topology->width; + source_x++) { + for (int source_y = topology->origin_y; + source_y < topology->origin_y + topology->height; + source_y++) { + if (encounter_arena_topology_footprint_blocked( + topology, source_x, source_y, 1) || + nh_pvp_test_blocked(blockers, source_x, source_y, 1)) + continue; + input.source_x = source_x; + input.source_y = source_y; + for (int target_x = topology->origin_x; + target_x < topology->origin_x + topology->width; + target_x++) { + for (int target_y = topology->origin_y; + target_y < topology->origin_y + topology->height; + target_y++) { + if (encounter_arena_topology_footprint_blocked( + topology, target_x, target_y, 1)) + continue; + input.target_x = target_x; + input.target_y = target_y; + input.cost_policy = ENCOUNTER_ROUTE_COST_OSRS; + EncounterRouteResult source_field = + encounter_route_solve(&input); + input.cost_policy = ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS; + EncounterRouteResult target_bfs = + encounter_route_solve(&input); + if (!nh_pvp_route_results_equal(&source_field, &target_bfs)) { + fprintf( + stderr, + "nh_pvp target BFS mismatch source=(%d,%d) target=(%d,%d) blockers=%d\n", + source_x, + source_y, + target_x, + target_y, + blockers->count); + abort(); + } + checks++; + } + } + } + } + return checks; +} + +static void nh_pvp_target_bfs_selftest( + const EncounterArenaTopology* topology +) { + NhPvpTestBlockers blockers = {0}; + uint64_t checks = + nh_pvp_target_bfs_equivalence(topology, &blockers, 1); + blockers = (NhPvpTestBlockers){ + .count = 8, + .x = { + FIGHT_AREA_BASE_X + 17, + FIGHT_AREA_BASE_X + 17, + FIGHT_AREA_BASE_X + 17, + FIGHT_AREA_BASE_X + 17, + FIGHT_AREA_BASE_X + 29, + FIGHT_AREA_BASE_X + 29, + FIGHT_AREA_BASE_X + 30, + FIGHT_AREA_BASE_X + 30, + }, + .y = { + FIGHT_AREA_BASE_Y + 8, + FIGHT_AREA_BASE_Y + 9, + FIGHT_AREA_BASE_Y + 11, + FIGHT_AREA_BASE_Y + 12, + FIGHT_AREA_BASE_Y + 18, + FIGHT_AREA_BASE_Y + 19, + FIGHT_AREA_BASE_Y + 18, + FIGHT_AREA_BASE_Y + 19, + }, + }; + checks += nh_pvp_target_bfs_equivalence(topology, &blockers, 2); + printf( + "nh_pvp target-directed BFS equivalence PASS: %llu source-target queries across 2 blocker fields\n", + (unsigned long long)checks); +} +#else +#error "define one TEST_ROUTE_TOPOLOGY_* encounter" +#endif + +static void put_geometry( + EncounterState* state, + EncounterContext* context, + CollisionMap* map, + int map_first +) { + if (map_first) + TEST_DEF.put_ptr(state, context, "collision_map", map); +#if !defined(TEST_ROUTE_TOPOLOGY_NH_PVP) + TEST_DEF.put_int(state, context, "world_offset_x", TEST_OFFSET_X); + TEST_DEF.put_int(state, context, "world_offset_y", TEST_OFFSET_Y); +#endif + if (!map_first) + TEST_DEF.put_ptr(state, context, "collision_map", map); +} + +static const EncounterArenaTopology* finalize_with_map( + CollisionMap* map, + int map_first +) { + TestContext* context = calloc(1, sizeof(*context)); + if (!context) abort(); + TEST_DEF.init_context((EncounterContext*)context); + EncounterState* state = TEST_DEF.create(); + if (!state) abort(); + put_geometry(state, (EncounterContext*)context, map, map_first); + if (TEST_TOPOLOGY(context) != NULL) { + fprintf(stderr, "%s topology initialized before explicit finalization\n", + TEST_DEF.name); + abort(); + } + TEST_DEF.finalize_context(state, (EncounterContext*)context); + const EncounterArenaTopology* topology = TEST_TOPOLOGY(context); + if (!topology || !topology->finalized) abort(); + TEST_DEF.destroy(state); + free(context); + return topology; +} + +int main(void) { + CollisionMap* first_map = collision_map_load(TEST_MAP_PATH); + CollisionMap* second_map = collision_map_load(TEST_MAP_PATH); + if (!first_map || !second_map || first_map == second_map) abort(); + + const EncounterArenaTopology* first = finalize_with_map(first_map, 1); + int open_tiles = 0; + int blocked_tiles = 0; + for (int x = TEST_ORIGIN_X; x < TEST_ORIGIN_X + TEST_WIDTH; x++) { + for (int y = TEST_ORIGIN_Y; y < TEST_ORIGIN_Y + TEST_HEIGHT; y++) { + int index = (x - TEST_ORIGIN_X) * TEST_HEIGHT + + (y - TEST_ORIGIN_Y); + uint32_t expected = expected_flags(first_map, x, y); + if (first->static_collision_flags[index] != expected) { + fprintf(stderr, + "%s topology mismatch at (%d,%d): expected %u got %u\n", + TEST_DEF.name, + x, + y, + expected, + first->static_collision_flags[index]); + abort(); + } + if (first->static_blocked[index]) blocked_tiles++; + else open_tiles++; + } + } +#if defined(TEST_ROUTE_TOPOLOGY_ZULRAH) + if (open_tiles != 69 || blocked_tiles != 715) { + fprintf(stderr, "zulrah topology count mismatch: %d open %d blocked\n", + open_tiles, blocked_tiles); + abort(); + } +#endif + + const EncounterArenaTopology* second = finalize_with_map(second_map, 0); + if (second != first) { + fprintf(stderr, "%s did not reuse process topology\n", TEST_DEF.name); + abort(); + } +#if defined(TEST_ROUTE_TOPOLOGY_NH_PVP) + nh_pvp_target_bfs_selftest(first); +#endif + printf("%s route topology setup PASS: %d open %d blocked, order-independent, identical-map reuse\n", + TEST_DEF.name, open_tiles, blocked_tiles); + collision_map_free(first_map); + collision_map_free(second_map); + return 0; +} diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index 9936d9d341..f7d9d955ad 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -8,7 +8,7 @@ #include "ocean/osrs/tests/osrs_route_reference.h" #define inf_init_context_typed(ctx_ptr) do { \ inf_init_context_typed(ctx_ptr); \ - inf_bind_route_topology((ctx_ptr), NULL); \ + inf_finalize_route_topology((ctx_ptr)); \ } while (0) #include "ocean/osrs/osrs_anim.h" #include "ocean/osrs/osrs_effects.h" @@ -9289,6 +9289,7 @@ static void test_compact_transient_inventory_equipment_semantics(void) { int main(void) { inf_build_npc_stats(); + inf_finalize_route_topology(inf_legacy_context()); test_compact_observation_layout_contract(); test_compact_player_and_pillar_observation_semantics(); test_compact_npc_observation_semantics(); diff --git a/ocean/osrs/tests/test_inferno_forecast_exact.c b/ocean/osrs/tests/test_inferno_forecast_exact.c index a5317e8e46..20ac8ac64a 100644 --- a/ocean/osrs/tests/test_inferno_forecast_exact.c +++ b/ocean/osrs/tests/test_inferno_forecast_exact.c @@ -199,15 +199,13 @@ static void exact_init_state( uint32_t seed ) { inf_init_context_typed(ctx); -#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES - inf_bind_route_topology(ctx, NULL); -#endif memset(s, 0, sizeof(*s)); inf_put_int_ctx( (EncounterState*)s, (EncounterContext*)ctx, "start_wave", public_start_wave); + inf_finalize_route_topology(ctx); inf_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); } typedef struct { @@ -323,6 +321,13 @@ static void exact_generate_fixture(const char* path) { exact_writer_close(&writer); } +static void exact_read_all(FILE* file, void* data, size_t size) { + if (fread(data, 1, size, file) != size) { + fprintf(stderr, "truncated inferno exact fixture\n"); + abort(); + } +} + static int exact_compare_files(const char* expected_path, const char* actual_path) { FILE* expected = fopen(expected_path, "rb"); if (!expected) { @@ -335,37 +340,88 @@ static int exact_compare_files(const char* expected_path, const char* actual_pat abort(); } - uint8_t expected_buf[EXACT_CHUNK_BYTES]; - uint8_t actual_buf[EXACT_CHUNK_BYTES]; - uint64_t offset = 0; - for (;;) { - size_t ne = fread(expected_buf, 1, sizeof(expected_buf), expected); - size_t na = fread(actual_buf, 1, sizeof(actual_buf), actual); - if (ne != na) { - printf("inferno exact mismatch: size differs at byte %llu\n", - (unsigned long long)offset); + InfExactFileHeader expected_file; + InfExactFileHeader actual_file; + exact_read_all(expected, &expected_file, sizeof(expected_file)); + exact_read_all(actual, &actual_file, sizeof(actual_file)); + if (memcmp(&expected_file, &actual_file, sizeof(expected_file)) != 0) { + printf("inferno exact mismatch: file header\n"); + fclose(expected); + fclose(actual); + return 1; + } + + for (uint32_t index = 0; index < expected_file.record_count; index++) { + InfExactRecordHeader expected_record; + InfExactRecordHeader actual_record; + InfStepOutForecast expected_forecast; + InfStepOutForecast actual_forecast; + float expected_obs[INF_NUM_OBS]; + float actual_obs[INF_NUM_OBS]; + InfernoState expected_state; + InfernoState actual_state; + exact_read_all(expected, &expected_record, sizeof(expected_record)); + exact_read_all(actual, &actual_record, sizeof(actual_record)); + exact_read_all(expected, &expected_forecast, sizeof(expected_forecast)); + exact_read_all(actual, &actual_forecast, sizeof(actual_forecast)); + exact_read_all(expected, expected_obs, sizeof(expected_obs)); + exact_read_all(actual, actual_obs, sizeof(actual_obs)); + exact_read_all(expected, &expected_state, sizeof(expected_state)); + exact_read_all(actual, &actual_state, sizeof(actual_state)); + + if (exact_hash_bytes(&expected_state, sizeof(expected_state)) != + expected_record.state_hash || + exact_hash_bytes(&actual_state, sizeof(actual_state)) != + actual_record.state_hash) { + printf("inferno exact fixture state hash mismatch at record %u\n", index); fclose(expected); fclose(actual); return 1; } - if (ne == 0) break; - if (memcmp(expected_buf, actual_buf, ne) != 0) { - for (size_t i = 0; i < ne; i++) { - if (expected_buf[i] == actual_buf[i]) continue; - printf("inferno exact mismatch at byte %llu: expected %u got %u\n", - (unsigned long long)(offset + i), - (unsigned)expected_buf[i], - (unsigned)actual_buf[i]); - fclose(expected); - fclose(actual); - return 1; + exact_zero_serialized_route_storage(&expected_state); + exact_zero_serialized_route_storage(&actual_state); + expected_record.state_hash = + exact_hash_bytes(&expected_state, sizeof(expected_state)); + actual_record.state_hash = + exact_hash_bytes(&actual_state, sizeof(actual_state)); + if (memcmp(&expected_record, &actual_record, sizeof(expected_record)) != 0 || + memcmp(&expected_forecast, &actual_forecast, sizeof(expected_forecast)) != 0 || + memcmp(expected_obs, actual_obs, sizeof(expected_obs)) != 0 || + memcmp(&expected_state, &actual_state, sizeof(expected_state)) != 0) { + printf( + "inferno exact mismatch at record %u scenario %u step %u\n", + index, + expected_record.scenario_id, + expected_record.step_index); + const uint8_t* expected_bytes = (const uint8_t*)&expected_state; + const uint8_t* actual_bytes = (const uint8_t*)&actual_state; + for (size_t byte = 0; byte < sizeof(expected_state); byte++) { + if (expected_bytes[byte] == actual_bytes[byte]) continue; + printf( + "first state byte mismatch offset=%zu expected=%u actual=%u player expected=(%d,%d) actual=(%d,%d)\n", + byte, + (unsigned)expected_bytes[byte], + (unsigned)actual_bytes[byte], + expected_state.player.x, + expected_state.player.y, + actual_state.player.x, + actual_state.player.y); + break; } + fclose(expected); + fclose(actual); + return 1; } - offset += (uint64_t)ne; } + int expected_tail = fgetc(expected); + int actual_tail = fgetc(actual); fclose(expected); fclose(actual); + if (expected_tail != EOF || actual_tail != EOF) { + printf("inferno exact mismatch: trailing data\n"); + return 1; + } return 0; } diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c index 0fa8b09a41..e1dbba24c0 100644 --- a/ocean/osrs/tests/test_osrs_player_step.c +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -136,9 +136,53 @@ static OsrsEncounterArena step_arena(void) { arena.blockers.revision = 1; arena.movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN; arena.cost_policy = ENCOUNTER_ROUTE_COST_OSRS; - arena.explicit_cost_policy = ENCOUNTER_ROUTE_COST_DIRECT; return arena; } +static void test_ranged_chase_targets_nearest_attack_position(void) { + Player player; + memset(&player, 0, sizeof(player)); + player.x = 2; + player.y = 10; + OsrsInteraction interaction; + osrs_interaction_init(&interaction); + OsrsActorRouteCache route_cache = {0}; + StepTargetCtx target_ctx = { + .target = { + .slot = 3, + .x = 10, + .y = 10, + .size = 1, + .attack_range = 4, + }, + .target_valid = 1, + }; + int dest_x = -1; + int dest_y = -1; + OsrsPlayerStepInput input; + memset(&input, 0, sizeof(input)); + input.player = &player; + input.interaction = &interaction; + input.route_cache = &route_cache; + input.target_lookup = step_lookup_target; + input.target_ctx = &target_ctx; + input.command.kind = OSRS_PLAYER_CMD_TARGET; + input.command.target_slot = 3; + input.arena = step_arena(); + input.arena.cost_policy = ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS; + input.dest_x = &dest_x; + input.dest_y = &dest_y; + + OsrsPlayerStepResult result = osrs_encounter_player_step(&input); + CHECK("range-four chase moves two steps toward attack range", + result.chased_target && player.x == 4 && player.y == 10); + CHECK("range-four chase targets the nearest valid attack tile", + route_cache.waypoint_count == 1 && + route_cache.waypoint_x[0] == 6 && + route_cache.waypoint_y[0] == 10); + CHECK("range-four chase does not route to cardinal adjacency", + route_cache.waypoint_x[0] != 9); +} + static void test_attack_route_rejects_cardinal_adjacency_through_wall(void) { CollisionMap map; @@ -329,6 +373,45 @@ static void test_move_command_cancels_interaction(void) { CHECK("explicit move ran", r.explicit_moved == 1); CHECK("player walked toward the clicked tile", player.y < 5); } +static void test_destination_click_routes_around_wall(void) { + Player player; + memset(&player, 0, sizeof(player)); + player.x = 2; + player.y = 2; + player.run_energy = 10000; + + OsrsInteraction interaction; + OsrsActorRouteCache route_cache = {0}; + osrs_interaction_init(&interaction); + StepTargetCtx target_ctx = {0}; + int dest_x = 10; + int dest_y = 2; + OsrsEncounterArena arena = step_arena(); + arena.blockers = (EncounterRouteBlockers){ + .is_blocked = step_vertical_wall_route, + .revision = 2, + }; + + OsrsPlayerStepInput input; + memset(&input, 0, sizeof(input)); + input.player = &player; + input.interaction = &interaction; + input.route_cache = &route_cache; + input.target_lookup = step_lookup_target; + input.target_ctx = &target_ctx; + input.dest_x = &dest_x; + input.dest_y = &dest_y; + input.command.kind = OSRS_PLAYER_CMD_MOVE; + input.command.move_kind = OSRS_PLAYER_MOVE_DESTINATION; + input.arena = arena; + + for (int tick = 0; tick < 20 && dest_x >= 0; tick++) { + osrs_encounter_player_step(&input); + } + CHECK("destination click routes around a wall to the clicked tile", + player.x == 10 && player.y == 2 && dest_x == -1 && dest_y == -1); +} + /* no click: an active interaction keeps auto-chasing. */ static void test_none_command_chases_active_interaction(void) { @@ -1052,8 +1135,10 @@ int main(void) { test_unreachable_attack_route_field_exhausts_for_fallback(); test_attack_route_caches_walkability_per_tile(); test_attackable_target_skips_route_scan(); + test_ranged_chase_targets_nearest_attack_position(); test_target_command_cancels_walk_in_flight(); test_move_command_cancels_interaction(); + test_destination_click_routes_around_wall(); test_none_command_chases_active_interaction(); test_attack_route_rejects_cardinal_adjacency_through_wall(); test_attack_route_fallback_stays_within_ten_tiles(); diff --git a/ocean/osrs/tests/test_zulrah_golden.c b/ocean/osrs/tests/test_zulrah_golden.c index b239054667..b05bc2ac5e 100644 --- a/ocean/osrs/tests/test_zulrah_golden.c +++ b/ocean/osrs/tests/test_zulrah_golden.c @@ -72,6 +72,7 @@ static uint64_t run_episode(const GoldenConfig* cfg, int max_ticks, EpisodeStats ENCOUNTER_ZULRAH.init_context(context); ENCOUNTER_ZULRAH.init_state(state, context); apply_env_config(state, context, cfg); + ENCOUNTER_ZULRAH.finalize_context(state, context); ENCOUNTER_ZULRAH.reset(state, context, cfg->env_seed); uint64_t h = FNV_OFFSET; diff --git a/ocean/osrs/tests/test_zulrah_hit_delay.c b/ocean/osrs/tests/test_zulrah_hit_delay.c index a9d469519d..c3ff2a68d6 100644 --- a/ocean/osrs/tests/test_zulrah_hit_delay.c +++ b/ocean/osrs/tests/test_zulrah_hit_delay.c @@ -29,6 +29,7 @@ static ZulrahState* fresh_state(uint32_t seed) { ENCOUNTER_ZULRAH.put_int(state, context, "gear_tier", 0); ENCOUNTER_ZULRAH.put_int(state, context, "gear_tier_mode", ZUL_GEAR_TIER_FIXED); ENCOUNTER_ZULRAH.put_int(state, context, "episode_mode", ZUL_EPISODE_SINGLE_KILL); + ENCOUNTER_ZULRAH.finalize_context(state, context); ENCOUNTER_ZULRAH.reset(state, context, seed); return &g_state; } diff --git a/ocean/osrs_colosseum/osrs_colosseum.h b/ocean/osrs_colosseum/osrs_colosseum.h index 09fd6a3e35..7cd9ec927e 100644 --- a/ocean/osrs_colosseum/osrs_colosseum.h +++ b/ocean/osrs_colosseum/osrs_colosseum.h @@ -213,7 +213,6 @@ void puf_init(Env* env, Dict* kwargs) { env->num_agents = 1; env->agents[0].policy = 0; ENCOUNTER_COLOSSEUM.init_context(COLO_ENV_CONTEXT(env)); - col_bind_route_topology(&env->context, NULL); ENCOUNTER_COLOSSEUM.init_state(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env)); uint32_t seed_offset = 0; @@ -271,6 +270,8 @@ void puf_init(Env* env, Dict* kwargs) { } col_assign_curriculum_wave(env, kwargs); + ENCOUNTER_COLOSSEUM.finalize_context( + COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env)); } void puf_reset(Env* env) { diff --git a/ocean/osrs_inferno/osrs_inferno.h b/ocean/osrs_inferno/osrs_inferno.h index 65aaea810f..3cdb523274 100644 --- a/ocean/osrs_inferno/osrs_inferno.h +++ b/ocean/osrs_inferno/osrs_inferno.h @@ -161,7 +161,6 @@ void puf_init(Env* env, Dict* kwargs) { env->num_agents = 1; env->agents[0].policy = 0; ENCOUNTER_INFERNO.init_context(INF_ENV_CONTEXT(env)); - inf_bind_route_topology(&env->context, NULL); ENCOUNTER_INFERNO.init_state(INF_ENV_STATE(env), INF_ENV_CONTEXT(env)); uint32_t seed_offset = 0; @@ -241,6 +240,8 @@ void puf_init(Env* env, Dict* kwargs) { env->config_start_wave = (start_wave > 0) ? start_wave - 1 : 0; inferno_apply_curriculum(env, kwargs); + ENCOUNTER_INFERNO.finalize_context( + INF_ENV_STATE(env), INF_ENV_CONTEXT(env)); } static inline void inferno_env_write_obs(Env* env) { From 8d05bba20f8c990f8daed51c8b8154254f492298 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 02:16:47 +0300 Subject: [PATCH 14/50] Record OSRS route verification --- .superpowers/sdd/task-3-report.md | 34 ++++++++++++++----------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/.superpowers/sdd/task-3-report.md b/.superpowers/sdd/task-3-report.md index 332215e4ff..31752f1a02 100644 --- a/.superpowers/sdd/task-3-report.md +++ b/.superpowers/sdd/task-3-report.md @@ -56,7 +56,7 @@ Tagged outcomes, cache invalidation, direct movement, tie order, topology bounds clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_player_step.c -lm -o /tmp/test_osrs_player_step_task3 && /tmp/test_osrs_player_step_task3 ``` -Result: 69/69 passed. +Result: 73/73 passed. Shared NPC movement, footprint masks, diagonal edge clearance, overlap rewrite, melee policy, and current overlap: @@ -172,7 +172,7 @@ Result: all eight C/C++17 translation and link gates passed. ## Paired performance -Method: base binary from clean `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731`, final current wrapper, 100,000 steps, paired policy seeds 1 through 12, identical encounter and seed per pair. Command shape: +Method: base binary from clean `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731`, final current binary, 100,000 steps, paired policy seeds 1 through 12, identical encounter and seed per pair. Command shape: ```text BINARY --profile --encounter ENCOUNTER --profile-steps 100000 --policy-seed SEED @@ -180,50 +180,46 @@ BINARY --profile --encounter ENCOUNTER --profile-steps 100000 --policy-seed SEED | Encounter | Base SPS by seed 1..12 | Current SPS by seed 1..12 | Base GM | Current GM | Ratio GM | |---|---|---|---:|---:|---:| -| Inferno | 732928, 722679, 729203, 733864, 738716, 726433, 721839, 739186, 737790, 738503, 732961, 720015 | 948506, 955283, 966165, 958810, 966912, 944323, 945519, 958194, 970374, 954955, 966333, 962955 | 731145 | 958157 | 1.3105x | -| Colosseum | 387531, 390387, 389754, 389536, 390386, 396981, 394889, 376865, 391412, 388967, 388386, 388967 | 477842, 470670, 474179, 472083, 475267, 472010, 476247, 469836, 473279, 466440, 476954, 477461 | 389477 | 473511 | 1.2158x | -| Zulrah | 332947, 333962, 333881, 334546, 336398, 333934, 333036, 333825, 332882, 333592, 333135, 336344 | 759394, 765240, 767772, 759884, 767631, 764918, 768445, 770707, 766842, 773826, 761899, 762939 | 334038 | 765780 | 2.2925x | -| NH PvP | 502596, 495555, 501100, 507315, 499805, 514422, 502917, 510694, 503882, 508564, 509007, 509793 | 961067, 956709, 968204, 959269, 956462, 959177, 965586, 962844, 960366, 952780, 959371, 958396 | 505445 | 960011 | 1.8993x | +| Inferno | 722423, 774821, 776633, 793531, 785521, 799968, 761905, 773317, 781378, 791615, 784757, 772809 | 829187, 891234, 868523, 876939, 869664, 899159, 890076, 883556, 877093, 888976, 881073, 877332 | 776313 | 877566 | 1.1304x | +| Colosseum | 412613, 411399, 418468, 408986, 412565, 419336, 419925, 410521, 412223, 413810, 413414, 411748 | 498925, 497033, 496966, 491809, 495818, 499266, 500726, 492795, 498962, 489615, 505953, 498507 | 413737 | 497180 | 1.2017x | +| Zulrah | 345111, 352209, 344043, 345543, 346371, 344942, 341495, 348594, 360659, 345458, 345342, 351240 | 703438, 690322, 693577, 699364, 701095, 702134, 697024, 700535, 706869, 699658, 689422, 685594 | 347550 | 697392 | 2.0066x | +| NH PvP | 557336, 553547, 514634, 508024, 524115, 511470, 526840, 516726, 519591, 513313, 519651, 527209 | 683289, 663724, 655768, 688848, 675489, 674832, 669456, 679768, 674132, 672278, 678178, 672839 | 524161 | 673999 | 1.2859x | -Aggregate geometric mean of the four encounter ratios: 1.6229x. No encounter regressed. +Aggregate geometric mean of the four encounter ratios: 1.3683x. No encounter regressed. ## Changed files Production: +- `ocean/osrs/osrs_collision.h` - `ocean/osrs/osrs_encounter.h` - `ocean/osrs/osrs_encounter_player.h` -- `ocean/osrs/osrs_interaction.h` - `ocean/osrs/osrs_pathfinding.h` -- `ocean/osrs/osrs_pvp_actions.h` -- `ocean/osrs/osrs_pvp_api.h` - `ocean/osrs/osrs_pvp_movement.h` -- `ocean/osrs/osrs_render.h` - `ocean/osrs/osrs_visual.c` - `ocean/osrs/encounters/encounter_nh_pvp.h` - `ocean/osrs/encounters/encounter_zulrah.h` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc` - `ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc` - `ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc` - `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` - `ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc` - `ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc` -- `ocean/osrs/encounters/inferno/encounter_inferno_model.inc` - `ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc` - `ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc` - `ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc` +- `ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc` Tests and standalone consumers: -- `ocean/osrs/tests/osrs_route_reference.h` +- `ocean/osrs/tests/test_encounter_route_topology_setup.c` - `ocean/osrs/tests/test_osrs_player_step.c` - `ocean/osrs/tests/test_colosseum_forecast_exact.c` - `ocean/osrs/tests/test_inferno_forecast_exact.c` - `ocean/osrs/tests/test_colosseum_modifiers.c` - `ocean/osrs/tests/test_inferno_attack_styles.c` +- `ocean/osrs/tests/test_zulrah_golden.c` +- `ocean/osrs/tests/test_zulrah_hit_delay.c` - `ocean/osrs/tests/bench_colo_step.c` - `ocean/osrs/tests/bench_colosseum_forecast_profile.c` - `ocean/osrs_colosseum/osrs_colosseum.h` @@ -238,7 +234,7 @@ Tests and standalone consumers: - Actor route caches validate every required key. Changed blocker revisions and target geometry reroute from the actor's current tile. - Source/reverse fields validate topology revision, blocker identity/revision, source or target geometry, actor size, movement mode, and cost policy as applicable. - Dynamic blocker memoization distinguishes unknown, open, and blocked footprints and never mutates topology. -- Fallback distance, FIFO/depth tie-breaking, first step, run step, and waypoint compression match the independent reference across 41,560,131 exhaustive queries. -- Inferno and Colosseum forecast files remain byte-exact. Zulrah's six hashes remain exact. Snapshot state size and serialized interaction layout remain unchanged. +- Fallback distance, FIFO/depth tie-breaking, first step, run step, and waypoint compression match the independent references across 46,978,755 exhaustive queries. +- Inferno and Colosseum forecasts remain semantically byte-exact after canonicalizing serialized route storage and route-profiling counters. Zulrah's six hashes remain exact. Snapshot state size and serialized interaction layout remain unchanged. - Production search finds no old route field or arena BFS symbol. The only legacy implementation is isolated in `ocean/osrs/tests/osrs_route_reference.h`. - Final `git diff --check` passed. From fb57113c18974fe99bb71356428cdc35594f6f92 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 02:18:39 +0300 Subject: [PATCH 15/50] Correct OSRS route verification report --- .superpowers/sdd/task-3-report.md | 53 +++++++++++++++++++------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/.superpowers/sdd/task-3-report.md b/.superpowers/sdd/task-3-report.md index 31752f1a02..a9cf0f6167 100644 --- a/.superpowers/sdd/task-3-report.md +++ b/.superpowers/sdd/task-3-report.md @@ -10,9 +10,9 @@ The engine returns `EncounterRouteResult` with `ROUTE_REACHED_TARGET`, `ROUTE_RE `EncounterRouteInput` carries the immutable topology and dynamic blocker callback, context, and revision. Query scratch and reusable source/reverse fields are thread-local fixed storage. Queries allocate nothing and take no locks. One finalized topology is process-shared per encounter and each encounter context stores a const pointer. Dynamic occupancy stays in encounter state. -Player chase state moved out of serialized `OsrsInteraction` state into actor-local `OsrsActorRouteCache`. Cache validation covers topology revision, blocker revision, source/current actor position, actor size, target position and size, attack range, movement mode, and route-cost policy. Restore/reset paths clear the context cache. Serialized route bytes remain zero padding, preserving the exact state layout and artifacts. +Player chase state moved out of serialized `OsrsInteraction` state into actor-local `OsrsActorRouteCache`. Cache validation covers topology revision, blocker revision, source/current actor position, actor size, target position and size, attack range, movement mode, and route-cost policy. Restore/reset paths clear the context cache. `OsrsInteraction` retains its fixed 244-byte serialized route reserve, but raw base artifacts intentionally differ where the removed serialized cache had data. Forecast comparison validates each file's raw record hash before canonicalizing route storage. -Equal-cost behavior remains explicit in `EncounterRouteCostPolicy`. Attack-chase OSRS order uses the packed source field. Colosseum NPC OSRS-aggro paths use the reverse field with preserved south-first edge ordering. Direct click movement retains the pre-cutover direct movement policy in Inferno, Colosseum, and NH PvP. Zulrah retains OSRS click routing because its golden traces depend on it. +Equal-cost behavior remains explicit in `EncounterRouteCostPolicy`. Attack-chase OSRS order uses the packed source field. Colosseum NPC OSRS-aggro paths use the reverse field with preserved south-first edge ordering. `OSRS_PLAYER_MOVE_ACTION` uses the direct one-action policy. `OSRS_PLAYER_MOVE_DESTINATION` uses OSRS routing in Inferno, Colosseum, Zulrah, and NH PvP. ## TDD evidence @@ -71,6 +71,7 @@ Exhaustive optimized-versus-reference BFS: ```text /tmp/test_colosseum_forecast_exact_task3 --attack-route-selftest /tmp/test_inferno_forecast_exact_task3 --attack-route-selftest +clang -std=c11 -O2 -I. -DTEST_ROUTE_TOPOLOGY_NH_PVP ocean/osrs/tests/test_encounter_route_topology_setup.c -lm -o /tmp/test_route_topology_nh_pvp && /tmp/test_route_topology_nh_pvp ``` Results: @@ -79,11 +80,23 @@ Results: colosseum exhaustive route equivalence PASS: 22511700 source-target-range queries across 2 blocker fields colosseum attack route property selftest PASS: 1656 target queries across 3 fields inferno exhaustive route equivalence PASS: 19048431 source-target-range queries across 2 blocker fields +nh_pvp target-directed BFS equivalence PASS: 5418624 source-target queries across 2 blocker fields ``` The test-only reference performs an independent legacy FIFO BFS and landing scan. The checks cover every walkable source, every statically valid target anchor, target sizes through each encounter maximum, every encounter player attack range, and representative dynamic blocker revisions. Each comparison pins outcome, destination, shortest distance, first step, run step, and waypoint sequence. -Exact forecast gates against fixtures generated by unmodified base commit `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731`: +Topology finalization, immutable content identity, construction-order independence, process reuse, and bounds: + +```text +clang -std=c11 -O2 -I. -DTEST_ROUTE_TOPOLOGY_INFERNO ocean/osrs/tests/test_encounter_route_topology_setup.c -lm -o /tmp/test_route_topology_inferno && /tmp/test_route_topology_inferno +clang -std=c11 -O2 -I. -DTEST_ROUTE_TOPOLOGY_COLOSSEUM ocean/osrs/tests/test_encounter_route_topology_setup.c -lm -o /tmp/test_route_topology_colosseum && /tmp/test_route_topology_colosseum +clang -std=c11 -O2 -I. -DTEST_ROUTE_TOPOLOGY_ZULRAH ocean/osrs/tests/test_encounter_route_topology_setup.c -lm -o /tmp/test_route_topology_zulrah && /tmp/test_route_topology_zulrah +clang -std=c11 -O2 -I. -DTEST_ROUTE_TOPOLOGY_NH_PVP ocean/osrs/tests/test_encounter_route_topology_setup.c -lm -o /tmp/test_route_topology_nh_pvp && /tmp/test_route_topology_nh_pvp +``` + +Results: Inferno 870 open and 0 blocked, Colosseum 865 open and 291 blocked, Zulrah 69 open and 715 blocked, and NH PvP 1648 open and 60 blocked. Every setup test passed order independence and identical-map process reuse. + +Forecast fixtures came from base commit `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731`. Each expected and current raw state first validates against the state hash stored in its own record. The comparator then clears the removed serialized route storage in both states, clears Colosseum's route-profiling-only blocker counters, recomputes the canonical state hashes, and compares canonical record headers, forecasts, observations, masks, and state bytes. Raw base bytes are not claimed identical because the deleted route cache occupied the serialized reserve. ```text clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_forecast_exact.c -lm -o /tmp/test_colosseum_forecast_exact_task3 @@ -92,17 +105,7 @@ clang -std=c11 -O2 -I. ocean/osrs/tests/test_inferno_forecast_exact.c -lm -o /tm /tmp/test_inferno_forecast_exact_task3 --compare /tmp/task3-inferno-forecast ``` -Results: - -```text -colosseum LoS table selftest PASS: 1336336 pairs -colosseum footprint table selftest PASS: 12005 checks -colosseum landing helper selftest PASS: 25 actions across 3 states -colosseum exhaustive route equivalence PASS: 22511700 source-target-range queries across 2 blocker fields -colosseum attack route property selftest PASS: 1656 target queries across 3 fields -colosseum exact golden compare PASS: /tmp/task3-colosseum-forecast/colosseum_forecast_exact.bin -inferno exact golden compare PASS: /tmp/task3-inferno-forecast/inferno_forecast_exact.bin -``` +Results: both raw record-integrity checks and canonical semantic comparisons passed. Colosseum also passed 1,336,336 LoS pairs, 12,005 footprint checks, 25 landing actions, 22,511,700 exhaustive route queries, and 1,656 attack-route property queries. Focused encounter mechanics: @@ -172,7 +175,7 @@ Result: all eight C/C++17 translation and link gates passed. ## Paired performance -Method: base binary from clean `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731`, final current binary, 100,000 steps, paired policy seeds 1 through 12, identical encounter and seed per pair. Command shape: +Method: base binary from clean `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731`, final current binary, 100,000 steps, paired policy seeds 1 through 12, identical encounter and seed per pair. The clean base profiling copy received one benchmark-only seeding patch matching the final driver: parse `--policy-seed` and call `srand(profile_seed)` once before the profile loop. It changed no encounter, route, action, observation, reward, or terminal code. Both binaries therefore consumed the same deterministic policy stream for each pair. Command shape: ```text BINARY --profile --encounter ENCOUNTER --profile-steps 100000 --policy-seed SEED @@ -189,35 +192,43 @@ Aggregate geometric mean of the four encounter ratios: 1.3683x. No encounter reg ## Changed files -Production: +Full `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731..HEAD` task delta: +- `.superpowers/sdd/task-3-report.md` - `ocean/osrs/osrs_collision.h` - `ocean/osrs/osrs_encounter.h` - `ocean/osrs/osrs_encounter_player.h` +- `ocean/osrs/osrs_interaction.h` - `ocean/osrs/osrs_pathfinding.h` +- `ocean/osrs/osrs_pvp_actions.h` +- `ocean/osrs/osrs_pvp_api.h` - `ocean/osrs/osrs_pvp_movement.h` +- `ocean/osrs/osrs_render.h` - `ocean/osrs/osrs_visual.c` - `ocean/osrs/encounters/encounter_nh_pvp.h` - `ocean/osrs/encounters/encounter_zulrah.h` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc` - `ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` +- `ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc` - `ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc` - `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` - `ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc` - `ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc` - `ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc` +- `ocean/osrs/encounters/inferno/encounter_inferno_model.inc` - `ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc` - `ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc` - `ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc` - `ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc` - -Tests and standalone consumers: - +- `ocean/osrs/tests/osrs_route_reference.h` - `ocean/osrs/tests/test_encounter_route_topology_setup.c` - `ocean/osrs/tests/test_osrs_player_step.c` - `ocean/osrs/tests/test_colosseum_forecast_exact.c` -- `ocean/osrs/tests/test_inferno_forecast_exact.c` - `ocean/osrs/tests/test_colosseum_modifiers.c` - `ocean/osrs/tests/test_inferno_attack_styles.c` +- `ocean/osrs/tests/test_inferno_forecast_exact.c` - `ocean/osrs/tests/test_zulrah_golden.c` - `ocean/osrs/tests/test_zulrah_hit_delay.c` - `ocean/osrs/tests/bench_colo_step.c` @@ -235,6 +246,6 @@ Tests and standalone consumers: - Source/reverse fields validate topology revision, blocker identity/revision, source or target geometry, actor size, movement mode, and cost policy as applicable. - Dynamic blocker memoization distinguishes unknown, open, and blocked footprints and never mutates topology. - Fallback distance, FIFO/depth tie-breaking, first step, run step, and waypoint compression match the independent references across 46,978,755 exhaustive queries. -- Inferno and Colosseum forecasts remain semantically byte-exact after canonicalizing serialized route storage and route-profiling counters. Zulrah's six hashes remain exact. Snapshot state size and serialized interaction layout remain unchanged. +- Inferno and Colosseum canonical forecasts are byte-exact after clearing removed serialized route storage and route-profiling counters. Raw records first pass their own stored-hash integrity checks. Zulrah's six hashes remain exact. Snapshot state size and serialized interaction layout remain unchanged. - Production search finds no old route field or arena BFS symbol. The only legacy implementation is isolated in `ocean/osrs/tests/osrs_route_reference.h`. - Final `git diff --check` passed. From f8c2dbc9c5065d83de479dbee278f2f25ed72843 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 02:43:12 +0300 Subject: [PATCH 16/50] Preserve OSRS route overlap and tie semantics --- .superpowers/sdd/task-3-report.md | 29 +++++--- .../encounter_colosseum_player_actions.inc | 1 + ocean/osrs/encounters/encounter_zulrah.h | 1 + .../encounter_inferno_player_actions.inc | 1 + ocean/osrs/osrs_encounter_player.h | 3 +- ocean/osrs/osrs_pathfinding.h | 3 +- ocean/osrs/osrs_pvp_movement.h | 1 + ocean/osrs/tests/test_osrs_player_step.c | 68 +++++++++++++++++++ 8 files changed, 96 insertions(+), 11 deletions(-) diff --git a/.superpowers/sdd/task-3-report.md b/.superpowers/sdd/task-3-report.md index a9cf0f6167..a3a5d48c74 100644 --- a/.superpowers/sdd/task-3-report.md +++ b/.superpowers/sdd/task-3-report.md @@ -12,7 +12,7 @@ The engine returns `EncounterRouteResult` with `ROUTE_REACHED_TARGET`, `ROUTE_RE Player chase state moved out of serialized `OsrsInteraction` state into actor-local `OsrsActorRouteCache`. Cache validation covers topology revision, blocker revision, source/current actor position, actor size, target position and size, attack range, movement mode, and route-cost policy. Restore/reset paths clear the context cache. `OsrsInteraction` retains its fixed 244-byte serialized route reserve, but raw base artifacts intentionally differ where the removed serialized cache had data. Forecast comparison validates each file's raw record hash before canonicalizing route storage. -Equal-cost behavior remains explicit in `EncounterRouteCostPolicy`. Attack-chase OSRS order uses the packed source field. Colosseum NPC OSRS-aggro paths use the reverse field with preserved south-first edge ordering. `OSRS_PLAYER_MOVE_ACTION` uses the direct one-action policy. `OSRS_PLAYER_MOVE_DESTINATION` uses OSRS routing in Inferno, Colosseum, Zulrah, and NH PvP. +Equal-cost behavior remains explicit in `EncounterRouteCostPolicy`. Attack-chase OSRS order uses the packed source field. Colosseum NPC OSRS-aggro paths use the reverse field with preserved south-first edge ordering. `OSRS_PLAYER_MOVE_ACTION` uses the direct one-action policy. `OSRS_PLAYER_MOVE_DESTINATION` uses the legacy south-first BFS policy in all four encounters. Attack-range overlap enters the same deterministic north-first escape path as cardinal-adjacency overlap. ## TDD evidence @@ -48,15 +48,25 @@ route equivalence mismatch scenario=open-arena source=(0,13) target=(0,13,2) exp The exhaustive driver was corrected to exclude actor/target overlap. The production interaction layer handles overlap through its explicit escape contract rather than normal attack-chase routing. +The final overlap regression failed before the shared solver dispatched attack-range overlap to the escape path: + +```text +clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_player_step.c -lm -o /tmp/task3_overlap_tie_red && /tmp/task3_overlap_tie_red +FAIL: range overlap uses north-first deterministic escape +74/75 tests passed +``` + +The symmetric-obstacle regression also distinguishes OSRS attack order from legacy south-first destination order and drives `osrs_player_step_apply_explicit_move` through the destination policy. + ### GREEN -Tagged outcomes, cache invalidation, direct movement, tie order, topology bounds, dynamic blockers, and actor-local reuse: +Tagged outcomes, cache invalidation, direct movement, attack-range overlap escape, symmetric-obstacle destination tie order, topology bounds, dynamic blockers, and actor-local reuse: ```text -clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_player_step.c -lm -o /tmp/test_osrs_player_step_task3 && /tmp/test_osrs_player_step_task3 +clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_player_step.c -lm -o /tmp/task3_destination_tie_green && /tmp/task3_destination_tie_green ``` -Result: 73/73 passed. +Result: 76/76 passed. Shared NPC movement, footprint masks, diagonal edge clearance, overlap rewrite, melee policy, and current overlap: @@ -183,12 +193,12 @@ BINARY --profile --encounter ENCOUNTER --profile-steps 100000 --policy-seed SEED | Encounter | Base SPS by seed 1..12 | Current SPS by seed 1..12 | Base GM | Current GM | Ratio GM | |---|---|---|---:|---:|---:| -| Inferno | 722423, 774821, 776633, 793531, 785521, 799968, 761905, 773317, 781378, 791615, 784757, 772809 | 829187, 891234, 868523, 876939, 869664, 899159, 890076, 883556, 877093, 888976, 881073, 877332 | 776313 | 877566 | 1.1304x | -| Colosseum | 412613, 411399, 418468, 408986, 412565, 419336, 419925, 410521, 412223, 413810, 413414, 411748 | 498925, 497033, 496966, 491809, 495818, 499266, 500726, 492795, 498962, 489615, 505953, 498507 | 413737 | 497180 | 1.2017x | -| Zulrah | 345111, 352209, 344043, 345543, 346371, 344942, 341495, 348594, 360659, 345458, 345342, 351240 | 703438, 690322, 693577, 699364, 701095, 702134, 697024, 700535, 706869, 699658, 689422, 685594 | 347550 | 697392 | 2.0066x | -| NH PvP | 557336, 553547, 514634, 508024, 524115, 511470, 526840, 516726, 519591, 513313, 519651, 527209 | 683289, 663724, 655768, 688848, 675489, 674832, 669456, 679768, 674132, 672278, 678178, 672839 | 524161 | 673999 | 1.2859x | +| Inferno | 715533, 778610, 782075, 773880, 774833, 763091, 770297, 780482, 790451, 793796, 798869, 775958 | 823981, 877216, 891957, 881376, 876539, 879832, 892069, 879933, 878464, 887863, 884463, 905928 | 774547 | 879763 | 1.1358x | +| Colosseum | 410523, 409539, 414233, 411516, 411634, 415806, 412797, 408617, 417805, 414190, 411997, 410698 | 495086, 489165, 495774, 493320, 499146, 495108, 489340, 492587, 491034, 491615, 503603, 503547 | 412438 | 494921 | 1.2000x | +| Zulrah | 343538, 347723, 341180, 345094, 347872, 342274, 349550, 343881, 346293, 355130, 342277, 352433 | 714439, 724843, 710732, 709859, 701208, 706334, 713409, 724422, 711541, 702849, 694850, 726633 | 346413 | 711697 | 2.0545x | +| NH PvP | 528933, 508037, 508001, 518092, 512665, 521387, 524002, 515876, 546269, 524706, 533072, 513215 | 660703, 660375, 654279, 666898, 660009, 660707, 661673, 665602, 657173, 656146, 666871, 655295 | 521079 | 660465 | 1.2675x | -Aggregate geometric mean of the four encounter ratios: 1.3683x. No encounter regressed. +Aggregate geometric mean of the four encounter ratios: 1.3726x. No encounter regressed. ## Changed files @@ -246,6 +256,7 @@ Full `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731..HEAD` task delta: - Source/reverse fields validate topology revision, blocker identity/revision, source or target geometry, actor size, movement mode, and cost policy as applicable. - Dynamic blocker memoization distinguishes unknown, open, and blocked footprints and never mutates topology. - Fallback distance, FIFO/depth tie-breaking, first step, run step, and waypoint compression match the independent references across 46,978,755 exhaustive queries. +- Attack-range overlap uses the deterministic escape contract before attack routing. A symmetric obstacle pins destination movement to legacy south-first equal-cost order while attack routing retains OSRS ordering. - Inferno and Colosseum canonical forecasts are byte-exact after clearing removed serialized route storage and route-profiling counters. Raw records first pass their own stored-hash integrity checks. Zulrah's six hashes remain exact. Snapshot state size and serialized interaction layout remain unchanged. - Production search finds no old route field or arena BFS symbol. The only legacy implementation is isolated in `ocean/osrs/tests/osrs_route_reference.h`. - Final `git diff --check` passed. diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc index 1ccfb8613b..f36fe83cbd 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -1057,6 +1057,7 @@ static ColoPrimaryStepPlan col_primary_step_plan( }, .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + .destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, .collision_map = ctx->collision_map, .world_offset_x = ctx->world_offset_x, .world_offset_y = ctx->world_offset_y, diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h index cda9067707..90d7d9d101 100644 --- a/ocean/osrs/encounters/encounter_zulrah.h +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -2612,6 +2612,7 @@ static void zul_step_tick( .blockers = {0}, .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + .destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, .collision_map = (const CollisionMap*)s->collision_map, .world_offset_x = s->world_offset_x, .world_offset_y = s->world_offset_y, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc index 6c3f879e67..0f5c4025ad 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -572,6 +572,7 @@ static void inf_tick_player_ctx( }, .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + .destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, .collision_map = ctx->collision_map, .world_offset_x = ctx->world_offset_x, .world_offset_y = ctx->world_offset_y, diff --git a/ocean/osrs/osrs_encounter_player.h b/ocean/osrs/osrs_encounter_player.h index 0acb4675a7..a4c716b3c9 100644 --- a/ocean/osrs/osrs_encounter_player.h +++ b/ocean/osrs/osrs_encounter_player.h @@ -37,6 +37,7 @@ typedef struct { EncounterRouteBlockers blockers; EncounterRouteMovementMode movement_mode; EncounterRouteCostPolicy cost_policy; + EncounterRouteCostPolicy destination_cost_policy; const CollisionMap* collision_map; int world_offset_x; int world_offset_y; @@ -184,7 +185,7 @@ static inline int osrs_player_step_apply_explicit_move( .movement_mode = input->arena.movement_mode, .cost_policy = move_kind == OSRS_PLAYER_MOVE_ACTION ? ENCOUNTER_ROUTE_COST_DIRECT - : input->arena.cost_policy, + : input->arena.destination_cost_policy, }; EncounterRouteResult route = encounter_route_solve(&route_input); return osrs_player_step_apply_route(player, &route); diff --git a/ocean/osrs/osrs_pathfinding.h b/ocean/osrs/osrs_pathfinding.h index 4f9ea838ff..1147a43ce9 100644 --- a/ocean/osrs/osrs_pathfinding.h +++ b/ocean/osrs/osrs_pathfinding.h @@ -1804,7 +1804,8 @@ static inline EncounterRouteResult encounter_route_solve( if (!encounter_route_input_valid(input)) return result; if (input->cost_policy == ENCOUNTER_ROUTE_COST_DIRECT) return encounter_route_greedy_direct(input); - if (input->target_kind == ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY && + if ((input->target_kind == ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY || + input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) && encounter_route_footprints_overlap( input->source_x, input->source_y, input->actor_size, input->target_x, input->target_y, input->target_size)) diff --git a/ocean/osrs/osrs_pvp_movement.h b/ocean/osrs/osrs_pvp_movement.h index b570c8b202..fa43231dd3 100644 --- a/ocean/osrs/osrs_pvp_movement.h +++ b/ocean/osrs/osrs_pvp_movement.h @@ -342,6 +342,7 @@ static inline OsrsEncounterArena pvp_build_arena( arena.blockers = (EncounterRouteBlockers){0}; arena.movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN; arena.cost_policy = ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS; + arena.destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS; arena.collision_map = (const CollisionMap*)env->collision_map; arena.world_offset_x = 0; arena.world_offset_y = 0; diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c index e1dbba24c0..b82f2abd8a 100644 --- a/ocean/osrs/tests/test_osrs_player_step.c +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -136,6 +136,7 @@ static OsrsEncounterArena step_arena(void) { arena.blockers.revision = 1; arena.movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN; arena.cost_policy = ENCOUNTER_ROUTE_COST_OSRS; + arena.destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS; return arena; } static void test_ranged_chase_targets_nearest_attack_position(void) { @@ -1100,6 +1101,71 @@ static void test_route_cost_order_is_deterministic(void) { route.distance == 2); free(topology); } +static void test_attack_range_overlap_uses_deterministic_escape(void) { + RouteTestGeometry geometry = {0}; + RouteTestBlockers blockers = {0}; + EncounterArenaTopology* topology = route_test_topology(&geometry); + EncounterRouteInput input = route_test_input(topology, &blockers); + input.source_x = 2; + input.source_y = 2; + input.target_x = 2; + input.target_y = 2; + input.target_size = 2; + input.target_kind = ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE; + input.attack_range = 4; + input.los_query = osrs_los_open_query(); + + EncounterRouteResult route = encounter_route_solve(&input); + CHECK("range overlap uses north-first deterministic escape", + route.outcome == ROUTE_REACHED_TARGET && + route.first_dx == 0 && route.first_dy == -1 && + route.destination_x == 2 && route.destination_y == 1); + free(topology); +} + +static void test_destination_south_first_tie_order(void) { + RouteTestGeometry geometry = {0}; + RouteTestBlockers blockers = {0}; + blockers.blocked[3][3] = 1; + EncounterArenaTopology* topology = route_test_topology(&geometry); + EncounterRouteInput input = route_test_input(topology, &blockers); + input.source_x = 4; + input.source_y = 4; + input.target_x = 2; + input.target_y = 2; + + input.cost_policy = ENCOUNTER_ROUTE_COST_OSRS; + EncounterRouteResult osrs = encounter_route_solve(&input); + input.cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS; + EncounterRouteResult destination = encounter_route_solve(&input); + CHECK("symmetric obstacle exposes distinct equal-cost orders", + osrs.outcome == ROUTE_REACHED_TARGET && + destination.outcome == ROUTE_REACHED_TARGET && + osrs.distance == destination.distance && + osrs.first_dx == -1 && osrs.first_dy == 0 && + destination.first_dx == 0 && destination.first_dy == -1); + + Player player; + memset(&player, 0, sizeof(player)); + player.x = 4; + player.y = 4; + int dest_x = 2; + int dest_y = 2; + OsrsPlayerStepInput step_input; + memset(&step_input, 0, sizeof(step_input)); + step_input.player = &player; + step_input.dest_x = &dest_x; + step_input.dest_y = &dest_y; + step_input.arena = step_arena(); + step_input.arena.topology = topology; + step_input.arena.blockers = input.blockers; + int steps = osrs_player_step_apply_explicit_move( + &step_input, OSRS_PLAYER_MOVE_DESTINATION); + CHECK("destination movement uses south-first equal-cost order", + steps == 2 && player.x == 4 && player.y == 2); + free(topology); +} + static void test_direct_route_policy_preserves_greedy_step_order(void) { RouteTestGeometry geometry = {0}; RouteTestBlockers blockers = {0}; @@ -1126,6 +1192,8 @@ static void test_direct_route_policy_preserves_greedy_step_order(void) { int main(void) { test_tagged_route_outcomes_and_payload(); test_route_cost_order_is_deterministic(); + test_attack_range_overlap_uses_deterministic_escape(); + test_destination_south_first_tie_order(); test_direct_route_policy_preserves_greedy_step_order(); test_arena_topology_rejects_invalid_bounds(); test_arena_topology_extreme_origins_and_reader_bounds(); From 10b183c5d35e5be4d623cd14f779680d38cc7319 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 04:25:32 +0300 Subject: [PATCH 17/50] Use shared topology in Inferno --- .../inferno/encounter_inferno_combat.inc | 95 ++-- .../inferno/encounter_inferno_forecast.inc | 197 +++---- .../inferno/encounter_inferno_helpers.inc | 222 ++++++-- .../inferno/encounter_inferno_lab.inc | 17 +- .../inferno/encounter_inferno_model.inc | 19 +- .../inferno/encounter_inferno_movement.inc | 258 +++++----- .../inferno/encounter_inferno_obs_mask.inc | 20 +- .../encounter_inferno_player_actions.inc | 21 +- .../encounter_inferno_render_snapshot.inc | 5 +- .../inferno/encounter_inferno_reset_spawn.inc | 101 ++-- .../inferno/encounter_inferno_reward_step.inc | 42 +- ocean/osrs/osrs_collision.h | 223 ++++++-- ocean/osrs/osrs_pathfinding.h | 39 +- ocean/osrs/osrs_render.h | 27 +- ocean/osrs/tests/test_inferno_attack_styles.c | 481 ++++++++++++++---- .../osrs/tests/test_inferno_forecast_exact.c | 23 +- ocean/osrs/tests/test_inferno_golden.c | 34 +- ocean/osrs/tests/test_osrs_player_step.c | 35 ++ 18 files changed, 1196 insertions(+), 663 deletions(-) diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc index 76245869b4..d28231bde1 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc @@ -208,8 +208,9 @@ static int inf_is_untagged_live_zuk_healer_slot( return aggro->active && aggro->type == INF_NPC_ZUK; } -static int inf_player_can_attack_npc_from_current_tile( +static int inf_player_can_attack_npc_from_current_tile_ctx( const InfernoState* s, + const InfernoContext* ctx, int npc_idx ) { if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; @@ -218,21 +219,30 @@ static int inf_player_can_attack_npc_from_current_tile( if (!npc->active || npc->death_ticks != 0 || npc->hp <= 0) return 0; const EncounterLoadoutStats* ls = &s->live_stats; - OsrsLosQuery los_query = inf_player_los_query(s); - return encounter_player_can_attack( + return inf_los_clear_ctx( + s, + ctx, s->player.x, s->player.y, + 1, npc->x, npc->y, npc->size, - ls->attack_range, - NULL, 0, 0, - &los_query); + ls->attack_range); } -static int inf_player_has_los_to_npc_from_current_tile( +static int inf_player_can_attack_npc_from_current_tile( const InfernoState* s, int npc_idx +) { + return inf_player_can_attack_npc_from_current_tile_ctx( + s, inf_legacy_context(), npc_idx); +} + +static int inf_player_has_los_to_npc_from_current_tile_ctx( + const InfernoState* s, + const InfernoContext* ctx, + int npc_idx ) { if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; @@ -241,9 +251,9 @@ static int inf_player_has_los_to_npc_from_current_tile( npc->type == INF_NPC_ZUK_SHIELD) return 0; - return entity_has_line_of_sight( - s->los_blockers, - s->los_blocker_count, + return inf_los_clear_ctx( + s, + ctx, s->player.x, s->player.y, 1, @@ -253,44 +263,15 @@ static int inf_player_has_los_to_npc_from_current_tile( 0); } -typedef struct { - int can_attack_now; - int range_deficit; -} InfPlayerNpcLoadoutReach; - -static InfPlayerNpcLoadoutReach inf_player_loadout_reach_to_npc( +static int inf_player_has_los_to_npc_from_current_tile( const InfernoState* s, - int npc_idx, - int weapon_set + int npc_idx ) { - InfPlayerNpcLoadoutReach out = {0}; - if (!inf_npc_is_live_player_target(s, npc_idx)) - return out; - if (weapon_set < 0 || weapon_set >= INF_NUM_WEAPON_SETS) { - fprintf(stderr, "BUG: invalid Inferno weapon set %d\n", weapon_set); - abort(); - } - - const InfNPC* npc = &s->npcs[npc_idx]; - const EncounterLoadoutStats* ls = &s->loadout_stats[weapon_set]; - OsrsLosQuery los_query = inf_player_los_query(s); - int edge_distance = encounter_dist_to_npc( - s->player.x, s->player.y, npc->x, npc->y, npc->size); - out.range_deficit = edge_distance > ls->attack_range - ? edge_distance - ls->attack_range - : 0; - out.can_attack_now = encounter_player_can_attack( - s->player.x, - s->player.y, - npc->x, - npc->y, - npc->size, - ls->attack_range, - NULL, 0, 0, - &los_query); - return out; + return inf_player_has_los_to_npc_from_current_tile_ctx( + s, inf_legacy_context(), npc_idx); } + static int inf_player_can_phantom_barrage_npc( InfernoState* s, const InfernoContext* ctx, @@ -313,7 +294,9 @@ static int inf_player_can_phantom_barrage_npc( npc->y, npc->size, 10, - NULL, 0, 0, + ctx->collision_map, + ctx->world_offset_x, + ctx->world_offset_y, &los_query); } @@ -343,11 +326,10 @@ static int inf_untagged_zuk_healer_target_is_safe_now( const InfernoContext* ctx, int npc_idx ) { - (void)ctx; if (!inf_is_untagged_live_zuk_healer_slot(s, npc_idx)) return 1; return inf_player_behind_zuk_shield_now(s) && s->player.attack_timer == 0 && - inf_player_can_attack_npc_from_current_tile(s, npc_idx); + inf_player_can_attack_npc_from_current_tile_ctx(s, ctx, npc_idx); } static int inf_is_safe_untagged_zuk_healer_target_now( @@ -502,7 +484,8 @@ static void inf_npc_attack_ctx(InfernoState* s, InfernoContext* ctx, int idx) { int has_los_now = 0; if (stats->attack_range > 1) { - has_los_now = inf_npc_has_los(s, idx); + inf_sample_npc_player_los_frame(s, ctx, idx); + has_los_now = inf_npc_has_los_ctx(s, ctx, idx); } if (npc->type == INF_NPC_BLOB && @@ -536,8 +519,6 @@ static void inf_npc_attack_ctx(InfernoState* s, InfernoContext* ctx, int idx) { if (s->pillars[p].hp <= 0) { s->pillars[p].active = 0; s->tick_scratch.pillar_lost = p; - inf_rebuild_los(s); - inf_invalidate_los_cache(s); for (int n = 0; n < INF_MAX_NPCS; n++) { if (!s->npcs[n].active) continue; int ndx = s->npcs[n].x - s->pillars[p].x; @@ -598,8 +579,8 @@ static void inf_npc_attack_ctx(InfernoState* s, InfernoContext* ctx, int idx) { npc->attack_timer = stats->attack_speed; return; } - if (entity_has_line_of_sight( - s->los_blockers, s->los_blocker_count, + if (inf_los_clear_ctx( + s, ctx, npc->x, npc->y, 1, s->player.x, s->player.y, 1, 1)) { @@ -1026,10 +1007,11 @@ static void inf_zuk_tick(InfernoState* s) { if (s->zuk.shield_freeze > 0) { s->zuk.shield_freeze--; } else { - int ox = shield->x; - int oy = shield->y; + int old_x = shield->x; + int old_y = shield->y; if (inf_npc_sets_collision_flag(shield->type)) - inf_unstamp_npc_collision_footprint(s, ox, oy, shield->size); + inf_unstamp_npc_collision_footprint( + s, old_x, old_y, shield->size); shield->x += s->zuk.shield_dir; if (shield->x < 11) { s->zuk.shield_freeze = 4; @@ -1039,7 +1021,8 @@ static void inf_zuk_tick(InfernoState* s) { s->zuk.shield_dir = -1; } if (inf_npc_sets_collision_flag(shield->type)) - inf_stamp_npc_collision_footprint(s, shield->x, shield->y, shield->size); + inf_stamp_npc_collision_footprint( + s, shield->x, shield->y, shield->size); } } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc index 906b43ab0e..8b032a418e 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc @@ -55,13 +55,13 @@ typedef struct { int jad_attack_style; int attack_style; int aggro_target; - int los_to_player_cache; } InfForecastNpcLocal; typedef struct { const InfernoState* state; const InfernoContext* ctx; - uint8_t (*npc_flags)[INF_ARENA_HEIGHT]; + const InfForecastNpcLocal* npcs; + int self_idx; int player_x; int player_y; } InfForecastMoveCtx; @@ -92,11 +92,15 @@ static int inf_npc_targets_player_for_obs(const InfernoState* s, const InfNPC* n return target.is_player; } -static int inf_step_out_forecast_action_valid(InfernoState* s, int action) { +static int inf_step_out_forecast_action_valid( + const InfernoState* s, + const InfernoContext* ctx, + int action +) { if (action == 0) return 1; int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action]; int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action]; - return inf_in_arena(nx, ny) && !inf_blocked_by_pillar(s, nx, ny, 1); + return !inf_footprint_blocked_ctx(s, ctx, nx, ny, 1); } static void inf_step_out_forecast_action_landing_ctx( @@ -107,7 +111,7 @@ static void inf_step_out_forecast_action_landing_ctx( int* valid ) { *moved = s->player; - *valid = inf_step_out_forecast_action_valid((InfernoState*)s, action_idx); + *valid = inf_step_out_forecast_action_valid(s, ctx, action_idx); if (action_idx == 0) return; InfWalkCtx walk_ctx = { (InfernoState*)s, ctx }; encounter_move_to_target( @@ -236,6 +240,7 @@ static AttackStyle inf_forecast_blob_scan_style(OverheadPrayer prayer) { static void inf_step_out_forecast_npc_attack( InfernoState* s, + const InfernoContext* ctx, int idx, InfStepOutForecastAction* action, int tick_idx @@ -253,7 +258,7 @@ static void inf_step_out_forecast_npc_attack( int has_los_now = 0; if (npc->type == INF_NPC_BLOB && stats->attack_range > 1) { - has_los_now = inf_npc_has_los(s, idx); + has_los_now = inf_npc_has_los_ctx(s, ctx, idx); if (npc->blob_scanned_prayer < 0 && has_los_now && !npc->had_los_last_tick) { @@ -269,7 +274,7 @@ static void inf_step_out_forecast_npc_attack( if (npc->attack_timer > 0) return; if (npc->type != INF_NPC_BLOB && stats->attack_range > 1) - has_los_now = inf_npc_has_los(s, idx); + has_los_now = inf_npc_has_los_ctx(s, ctx, idx); if (stats->attack_range > 1 && !(npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) && @@ -341,7 +346,8 @@ static void inf_step_out_forecast_tick( if (npc->type == INF_NPC_MAGER && npc->resurrect_cooldown > 0) npc->resurrect_cooldown--; inf_npc_move_ctx(sim, ctx, i); - inf_step_out_forecast_npc_attack(sim, i, action, tick_idx); + inf_step_out_forecast_npc_attack( + sim, ctx, i, action, tick_idx); } } @@ -364,7 +370,8 @@ static void inf_step_out_forecast_tick_profiled( inf_npc_move_ctx(sim, ctx, i); double t1 = INF_PROFILE_NOW_MS(); INF_PROFILE_ADD(INF_PROF_FORECAST_NPC_MOVE, t1 - t0); - inf_step_out_forecast_npc_attack(sim, i, action, tick_idx); + inf_step_out_forecast_npc_attack( + sim, ctx, i, action, tick_idx); INF_PROFILE_ADD( INF_PROF_FORECAST_NPC_ATTACK, INF_PROFILE_NOW_MS() - t1); @@ -372,17 +379,6 @@ static void inf_step_out_forecast_tick_profiled( } #endif -static void inf_restore_step_out_forecast_player_collision_flags( - InfernoState* sim, - int prev_player_x, - int prev_player_y -) { - int gx, gy; - if (inf_grid_index(prev_player_x, prev_player_y, &gx, &gy)) - sim->player_collision_flags[gx][gy] = 0; - if (inf_grid_index(sim->player.x, sim->player.y, &gx, &gy)) - sim->player_collision_flags[gx][gy] = 1; -} static void inf_restore_step_out_forecast_action_state( InfernoState* sim, @@ -391,8 +387,6 @@ static void inf_restore_step_out_forecast_action_state( int slot_count, const Player* moved ) { - int prev_player_x = sim->player.x; - int prev_player_y = sim->player.y; sim->player.x = moved->x; sim->player.y = moved->y; sim->player.is_running = moved->is_running; @@ -401,13 +395,10 @@ static void inf_restore_step_out_forecast_action_state( sim->npcs[i] = s->npcs[i]; } sim->rng_state = s->rng_state; - inf_invalidate_los_cache(sim); memcpy( sim->npc_collision_flags, s->npc_collision_flags, sizeof(sim->npc_collision_flags)); - inf_restore_step_out_forecast_player_collision_flags( - sim, prev_player_x, prev_player_y); } static void inf_step_out_forecast_finalize_action( @@ -449,6 +440,7 @@ static int inf_step_out_forecast_npc_fire_tick_idx(const InfNPC* npc) { static void inf_step_out_forecast_fast_static_npc( const InfernoState* s, + const InfernoContext* ctx, const InfNPC* npc, InfStepOutForecastAction* action ) { @@ -471,7 +463,8 @@ static void inf_step_out_forecast_fast_static_npc( int has_los_now = 1; if (stats->attack_range > 1) - has_los_now = inf_npc_has_los_to_tile(s, npc, action->land_x, action->land_y); + has_los_now = inf_npc_has_los_to_tile_ctx( + s, ctx, npc, action->land_x, action->land_y); if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer < 0 && @@ -506,54 +499,22 @@ static void inf_step_out_forecast_fast_static_npc( action, tick_idx, npc->type, stats, style_mask); } -static void inf_forecast_local_stamp_npc( - uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], - int x, - int y, - int size -) { - for (int dx = 0; dx < size; dx++) { - for (int dy = 0; dy < size; dy++) { - int gx, gy; - if (inf_grid_index(x + dx, y + dy, &gx, &gy)) { - assert(npc_flags[gx][gy] < UINT8_MAX); - npc_flags[gx][gy]++; - } - } - } -} - -static void inf_forecast_local_unstamp_npc( - uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], - int x, - int y, - int size -) { - for (int dx = 0; dx < size; dx++) { - for (int dy = 0; dy < size; dy++) { - int gx, gy; - if (inf_grid_index(x + dx, y + dy, &gx, &gy) && - npc_flags[gx][gy] > 0) { - npc_flags[gx][gy]--; - } - } - } -} - -static int inf_forecast_local_npc_flags_blocked( - uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], +static int inf_forecast_local_npc_occupied( + const InfForecastNpcLocal npcs[INF_MAX_NPCS], + int self_idx, int x, int y, int size ) { - for (int dx = 0; dx < size; dx++) { - for (int dy = 0; dy < size; dy++) { - int gx, gy; - if (inf_grid_index(x + dx, y + dy, &gx, &gy) && - npc_flags[gx][gy] > 0) { - return 1; - } - } + for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { + if (npc_idx == self_idx) continue; + const InfForecastNpcLocal* npc = &npcs[npc_idx]; + if (!npc->active || npc->hp <= 0 || + !inf_npc_sets_collision_flag(npc->type)) + continue; + if (los_aabb_overlap( + x, y, size, npc->x, npc->y, npc->size)) + return 1; } return 0; } @@ -573,7 +534,8 @@ static int inf_forecast_local_npc_blocked(void* ctx, int x, int y, int size) { if (inf_forecast_local_overlaps_player( x, y, size, mc->player_x, mc->player_y)) return 1; - return inf_forecast_local_npc_flags_blocked(mc->npc_flags, x, y, size); + return inf_forecast_local_npc_occupied( + mc->npcs, mc->self_idx, x, y, size); } static int inf_forecast_local_npc_blocked_ignore_player( @@ -586,7 +548,8 @@ static int inf_forecast_local_npc_blocked_ignore_player( if (inf_npc_environment_blocked_ctx( (InfernoState*)mc->state, mc->ctx, x, y, size)) return 1; - return inf_forecast_local_npc_flags_blocked(mc->npc_flags, x, y, size); + return inf_forecast_local_npc_occupied( + mc->npcs, mc->self_idx, x, y, size); } static void inf_forecast_local_copy_npcs( @@ -616,58 +579,36 @@ static void inf_forecast_local_copy_npcs( : ATTACK_STYLE_NONE, .attack_style = npc->attack_style, .aggro_target = npc->aggro_target, - .los_to_player_cache = -1, }; } } -static void inf_forecast_local_rebuild_npc_flags( - const InfForecastNpcLocal npcs[INF_MAX_NPCS], - uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT] -) { - memset(npc_flags, 0, INF_ARENA_WIDTH * INF_ARENA_HEIGHT * sizeof(uint8_t)); - for (int i = 0; i < INF_MAX_NPCS; i++) { - const InfForecastNpcLocal* npc = &npcs[i]; - if (!npc->active || npc->hp <= 0) continue; - if (!inf_npc_sets_collision_flag(npc->type)) continue; - inf_forecast_local_stamp_npc(npc_flags, npc->x, npc->y, npc->size); - } -} static int inf_forecast_local_has_los_to_area( const InfernoState* s, + const InfernoContext* ctx, const InfForecastNpcLocal* npc, int target_x, int target_y, int target_size ) { const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; - return entity_has_line_of_sight(s->los_blockers, s->los_blocker_count, + return inf_los_clear_ctx( + s, ctx, npc->x, npc->y, npc->size, target_x, target_y, target_size, stats->attack_range); } -static int inf_forecast_local_has_los_to_tile( - const InfernoState* s, - const InfForecastNpcLocal* npc, - int target_x, - int target_y -) { - return inf_forecast_local_has_los_to_area(s, npc, target_x, target_y, 1); -} - static int inf_forecast_local_has_los_to_player( const InfernoState* s, - InfForecastNpcLocal* npc, + const InfernoContext* ctx, + const InfForecastNpcLocal* npc, int target_x, int target_y ) { - if (npc->los_to_player_cache >= 0) - return npc->los_to_player_cache; - npc->los_to_player_cache = inf_forecast_local_has_los_to_tile( - s, npc, target_x, target_y); - return npc->los_to_player_cache; + return inf_forecast_local_has_los_to_area( + s, ctx, npc, target_x, target_y, 1); } static InfTargetArea inf_forecast_local_target_area( @@ -788,7 +729,6 @@ static void inf_forecast_local_move_npc( const InfernoState* s, const InfernoContext* ctx, InfForecastNpcLocal npcs[INF_MAX_NPCS], - uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], int idx, InfStepOutForecastAction* action, int tick_idx @@ -803,15 +743,10 @@ static void inf_forecast_local_move_npc( const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; if (!stats->can_move) return; - int uses_collision_flag = inf_npc_sets_collision_flag(npc->type); - if (uses_collision_flag) - inf_forecast_local_unstamp_npc(npc_flags, npc->x, npc->y, npc->size); if (inf_forecast_local_overlaps_player( npc->x, npc->y, npc->size, action->land_x, action->land_y)) { inf_forecast_local_record_overlap_danger(s, npc, action, tick_idx); - if (uses_collision_flag) - inf_forecast_local_stamp_npc(npc_flags, npc->x, npc->y, npc->size); return; } @@ -821,24 +756,19 @@ static void inf_forecast_local_move_npc( npc->aggro_target = -1; int has_los = inf_forecast_local_has_los_to_area( - s, npc, target.x, target.y, target.size); - if (target.is_player) - npc->los_to_player_cache = has_los; + s, ctx, npc, target.x, target.y, target.size); if (has_los) { - if (uses_collision_flag) - inf_forecast_local_stamp_npc(npc_flags, npc->x, npc->y, npc->size); return; } if (npc->attack_timer > stats->attack_speed) { - if (uses_collision_flag) - inf_forecast_local_stamp_npc(npc_flags, npc->x, npc->y, npc->size); return; } InfForecastMoveCtx move_ctx = { .state = s, .ctx = ctx, - .npc_flags = npc_flags, + .npcs = npcs, + .self_idx = idx, .player_x = action->land_x, .player_y = action->land_y, }; @@ -859,14 +789,12 @@ static void inf_forecast_local_move_npc( encounter_npc_step_toward_policy( &npc->x, &npc->y, target.x, target.y, npc->size, target.size, step_policy, blocked, &move_ctx, NULL, &rng); - npc->los_to_player_cache = -1; - if (uses_collision_flag) - inf_forecast_local_stamp_npc(npc_flags, npc->x, npc->y, npc->size); } static void inf_forecast_local_attack_npc( const InfernoState* s, + const InfernoContext* ctx, InfForecastNpcLocal* npc, InfStepOutForecastAction* action, int tick_idx @@ -889,7 +817,7 @@ static void inf_forecast_local_attack_npc( int has_los_now = 0; if (npc->type == INF_NPC_BLOB && stats->attack_range > 1) { has_los_now = inf_forecast_local_has_los_to_player( - s, npc, action->land_x, action->land_y); + s, ctx, npc, action->land_x, action->land_y); if (npc->blob_scanned_prayer < 0 && has_los_now && !npc->had_los_last_tick) { @@ -906,7 +834,7 @@ static void inf_forecast_local_attack_npc( if (npc->type != INF_NPC_BLOB && stats->attack_range > 1) { has_los_now = inf_forecast_local_has_los_to_player( - s, npc, action->land_x, action->land_y); + s, ctx, npc, action->land_x, action->land_y); } if (stats->attack_range > 1 && @@ -994,7 +922,6 @@ static void inf_step_out_forecast_readonly_tick( const InfernoState* s, const InfernoContext* ctx, InfForecastNpcLocal npcs[INF_MAX_NPCS], - uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], InfStepOutForecastAction* action, int tick_idx, const int slots[INF_MAX_NPCS], @@ -1004,8 +931,10 @@ static void inf_step_out_forecast_readonly_tick( int i = slots[slot_idx]; InfForecastNpcLocal* npc = &npcs[i]; if (npc->frozen_ticks > 0) npc->frozen_ticks--; - inf_forecast_local_move_npc(s, ctx, npcs, npc_flags, i, action, tick_idx); - inf_forecast_local_attack_npc(s, npc, action, tick_idx); + inf_forecast_local_move_npc( + s, ctx, npcs, i, action, tick_idx); + inf_forecast_local_attack_npc( + s, ctx, npc, action, tick_idx); } } @@ -1026,7 +955,8 @@ static void inf_build_step_out_forecast_fast_static_ctx( for (int slot_idx = 0; slot_idx < forecast_slot_count; slot_idx++) { int i = forecast_slots[slot_idx]; - inf_step_out_forecast_fast_static_npc(s, &s->npcs[i], action); + inf_step_out_forecast_fast_static_npc( + s, ctx, &s->npcs[i], action); } inf_step_out_forecast_finalize_action(action); } @@ -1049,10 +979,8 @@ static void inf_build_step_out_forecast_fast_readonly_ctx( readonly_slots[readonly_slot_count++] = i; } InfForecastNpcLocal base_npcs[INF_MAX_NPCS]; - uint8_t base_npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT]; if (readonly_slot_count > 0) { inf_forecast_local_copy_npcs(s, base_npcs); - inf_forecast_local_rebuild_npc_flags(base_npcs, base_npc_flags); } for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { if (!inf_step_out_forecast_prepare_unique_action_ctx( @@ -1063,19 +991,18 @@ static void inf_build_step_out_forecast_fast_readonly_ctx( for (int slot_idx = 0; slot_idx < forecast_slot_count; slot_idx++) { int i = forecast_slots[slot_idx]; if (inf_forecast_npc_uses_readonly_sim(&s->npcs[i])) continue; - inf_step_out_forecast_fast_static_npc(s, &s->npcs[i], action); + inf_step_out_forecast_fast_static_npc( + s, ctx, &s->npcs[i], action); inf_step_out_forecast_conservative_meleer_npc(&s->npcs[i], action); } if (readonly_slot_count > 0) { InfForecastNpcLocal npcs[INF_MAX_NPCS]; - uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT]; memcpy(npcs, base_npcs, sizeof(base_npcs)); - memcpy(npc_flags, base_npc_flags, sizeof(base_npc_flags)); for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { inf_step_out_forecast_readonly_tick( - s, ctx, npcs, npc_flags, action, tick_idx, + s, ctx, npcs, action, tick_idx, readonly_slots, readonly_slot_count); } } @@ -1108,6 +1035,7 @@ static void inf_build_step_out_forecast_exact_ctx( double base_copy_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; #endif InfernoState sim = *s; + InfernoContext forecast_ctx = *ctx; #ifdef INF_PROFILE_ENABLED if (inf_prof_enabled) { INF_PROFILE_ADD( @@ -1120,8 +1048,9 @@ static void inf_build_step_out_forecast_exact_ctx( #ifdef INF_PROFILE_ENABLED double landing_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; #endif + inf_reset_npc_player_los_frame(&forecast_ctx); Player moved = inf_step_out_forecast_set_action_landing_ctx( - s, ctx, action_idx, action); + s, &forecast_ctx, action_idx, action); #ifdef INF_PROFILE_ENABLED if (inf_prof_enabled) { INF_PROFILE_ADD( @@ -1165,12 +1094,14 @@ static void inf_build_step_out_forecast_exact_ctx( #ifdef INF_PROFILE_ENABLED if (inf_prof_enabled) { inf_step_out_forecast_tick_profiled( - &sim, ctx, action, tick_idx, forecast_slots, forecast_slot_count); + &sim, &forecast_ctx, action, tick_idx, + forecast_slots, forecast_slot_count); continue; } #endif inf_step_out_forecast_tick( - &sim, ctx, action, tick_idx, forecast_slots, forecast_slot_count); + &sim, &forecast_ctx, action, tick_idx, + forecast_slots, forecast_slot_count); } inf_step_out_forecast_finalize_action(action); } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc index 5b841de046..bd3c88bcae 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc @@ -78,22 +78,80 @@ static void inf_shuffle_spawns(InfernoState* s) { s->spawn_order[i] = i; encounter_shuffle(s->spawn_order, INF_NUM_SPAWN_POS, &s->rng_state); } +static InfernoContext* inf_legacy_context(void); -static void inf_rebuild_los(InfernoState* s) { - s->los_blocker_count = 0; - for (int i = 0; i < INF_NUM_PILLARS; i++) { - if (s->pillars[i].active) { - LOSBlocker* b = &s->los_blockers[s->los_blocker_count++]; - b->x = s->pillars[i].x; - b->y = s->pillars[i].y; - b->size = INF_PILLAR_SIZE; - b->los_mask = LOS_FULL_MASK; - } + +static int inf_pillar_footprint_blocked( + const InfernoState* s, + int x, + int y, + int size +) { + for (int pillar_idx = 0; pillar_idx < INF_NUM_PILLARS; pillar_idx++) { + const InfPillar* pillar = &s->pillars[pillar_idx]; + if (!pillar->active) continue; + if (los_aabb_overlap( + x, y, size, + pillar->x, pillar->y, INF_PILLAR_SIZE)) + return 1; } + return 0; +} + +static uint32_t inf_pillar_los_flags(void* data, int x, int y) { + const InfernoState* s = (const InfernoState*)data; + return inf_pillar_footprint_blocked(s, x, y, 1) + ? LOS_FULL_MASK + : 0; } static OsrsLosQuery inf_player_los_query(const InfernoState* s) { - return osrs_los_blockers(s->los_blockers, s->los_blocker_count); + return osrs_los_flags(inf_pillar_los_flags, (void*)s); +} + +static int inf_footprint_blocked_ctx( + const InfernoState* s, + const InfernoContext* ctx, + int x, + int y, + int size +) { + return encounter_arena_topology_footprint_blocked( + ctx->route_topology, x, y, size) || + inf_pillar_footprint_blocked(s, x, y, size); +} + +static int inf_los_clear_ctx( + const InfernoState* s, + const InfernoContext* ctx, + int actor_x, + int actor_y, + int actor_size, + int target_x, + int target_y, + int target_size, + int attack_range +) { + if (!encounter_arena_topology_los_clear( + ctx->route_topology, + actor_x, + actor_y, + actor_size, + target_x, + target_y, + target_size, + attack_range)) + return 0; + return entity_has_line_of_sight_with_flags( + inf_pillar_los_flags, + (void*)s, + actor_x, + actor_y, + actor_size, + target_x, + target_y, + target_size, + attack_range); } typedef struct { @@ -146,46 +204,99 @@ static InfTargetArea inf_npc_current_target_area(const InfernoState* s, const In }; } -static int inf_npc_has_los_to_area( +static int inf_npc_has_los_to_area_ctx( const InfernoState* s, + const InfernoContext* ctx, const InfNPC* npc, int target_x, int target_y, int target_size ) { const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; - return entity_has_line_of_sight(s->los_blockers, s->los_blocker_count, - npc->x, npc->y, npc->size, - target_x, target_y, target_size, + return inf_los_clear_ctx( + s, + ctx, + npc->x, + npc->y, + npc->size, + target_x, + target_y, + target_size, stats->attack_range); } +static int inf_npc_has_los_to_area( + const InfernoState* s, + const InfNPC* npc, + int target_x, + int target_y, + int target_size +) { + return inf_npc_has_los_to_area_ctx( + s, inf_legacy_context(), npc, target_x, target_y, target_size); +} + +static int inf_npc_has_los_to_tile_ctx( + const InfernoState* s, + const InfernoContext* ctx, + const InfNPC* npc, + int target_x, + int target_y +) { + return inf_npc_has_los_to_area_ctx( + s, ctx, npc, target_x, target_y, 1); +} + static int inf_npc_has_los_to_tile( - const InfernoState* s, const InfNPC* npc, int target_x, int target_y + const InfernoState* s, + const InfNPC* npc, + int target_x, + int target_y ) { - return inf_npc_has_los_to_area(s, npc, target_x, target_y, 1); + return inf_npc_has_los_to_tile_ctx( + s, inf_legacy_context(), npc, target_x, target_y); } -static int inf_npc_has_los_direct(const InfernoState* s, int i) { - const InfNPC* npc = &s->npcs[i]; +static int inf_npc_has_los_direct_ctx( + const InfernoState* s, + const InfernoContext* ctx, + int npc_idx +) { + const InfNPC* npc = &s->npcs[npc_idx]; InfTargetArea target = inf_npc_current_target_area(s, npc); - return inf_npc_has_los_to_area(s, npc, target.x, target.y, target.size); + return inf_npc_has_los_to_area_ctx( + s, ctx, npc, target.x, target.y, target.size); +} + +static int inf_npc_has_los_direct(const InfernoState* s, int npc_idx) { + return inf_npc_has_los_direct_ctx( + s, inf_legacy_context(), npc_idx); +} + +static void inf_reset_npc_player_los_frame(InfernoContext* ctx) { + memset(ctx->npc_player_los_frame, -1, sizeof(ctx->npc_player_los_frame)); } -static int inf_npc_has_los(InfernoState* s, int i) { - if (s->npc_los_cache[i] >= 0) - return s->npc_los_cache[i]; - int result = inf_npc_has_los_direct(s, i); - s->npc_los_cache[i] = (int8_t)result; - return result; +static void inf_sample_npc_player_los_frame( + const InfernoState* s, + InfernoContext* ctx, + int npc_idx +) { + ctx->npc_player_los_frame[npc_idx] = + (int8_t)inf_npc_has_los_direct_ctx(s, ctx, npc_idx); } -static inline void inf_invalidate_los_cache(InfernoState* s) { - memset(s->npc_los_cache, -1, sizeof(s->npc_los_cache)); +static int inf_npc_has_los_ctx( + const InfernoState* s, + const InfernoContext* ctx, + int npc_idx +) { + int8_t sampled = ctx->npc_player_los_frame[npc_idx]; + return sampled >= 0 ? sampled : inf_npc_has_los_direct_ctx(s, ctx, npc_idx); } -static inline void inf_invalidate_npc_los_cache(InfernoState* s, int i) { - s->npc_los_cache[i] = -1; +static int inf_npc_has_los(const InfernoState* s, int npc_idx) { + return inf_npc_has_los_ctx(s, inf_legacy_context(), npc_idx); } static inline int inf_is_final_wave(const InfernoState* s) { @@ -387,8 +498,9 @@ static inline int inf_npc_type_has_player_pressure(InfNPCType type) { type != INF_NPC_HEALER_ZUK; } -static inline InfNpcPlayerThreat inf_npc_player_threat( +static inline InfNpcPlayerThreat inf_npc_player_threat_ctx( const InfernoState* s, + const InfernoContext* ctx, const InfNPC* npc ) { InfNpcPlayerThreat threat = {0}; @@ -415,7 +527,8 @@ static inline InfNpcPlayerThreat inf_npc_player_threat( if (stats->attack_range > 1 && !(npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) && - !inf_npc_has_los_to_tile(s, npc, s->player.x, s->player.y)) + !inf_npc_has_los_to_tile_ctx( + s, ctx, npc, s->player.x, s->player.y)) return threat; threat.can_attack_if_ready = 1; @@ -435,6 +548,14 @@ static inline InfNpcPlayerThreat inf_npc_player_threat( return threat; } +static inline InfNpcPlayerThreat inf_npc_player_threat( + const InfernoState* s, + const InfNPC* npc +) { + return inf_npc_player_threat_ctx( + s, inf_legacy_context(), npc); +} + static inline int inf_npc_is_live_player_target( const InfernoState* s, int npc_idx @@ -641,7 +762,6 @@ static void inf_apply_npc_death(InfernoState* s, int npc_idx); static int inf_mager_resurrect_ctx(InfernoState* s, InfernoContext* ctx, int idx); static void inf_queue_zuk_healer_sparks(InfernoState* s, const InfNPC* npc); static void inf_resolve_pending_sparks(InfernoState* s); -static void inf_rebuild_player_collision_flags(InfernoState* s); static void inf_invalidate_current_obs_slots(InfernoState* s); static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx); static void inf_ensure_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx); @@ -711,9 +831,11 @@ typedef struct { static InfRouteTopologyOwner inf_route_topology_owner; -static uint32_t inf_route_topology_flags(void* data, int x, int y) { - const InfRouteTopologyBuildContext* build = - (const InfRouteTopologyBuildContext*)data; +static uint32_t inf_topology_collision_flags( + const InfRouteTopologyBuildContext* build, + int x, + int y +) { if (!build->collision_map) return 0; return (uint32_t)collision_get_flags( build->collision_map, @@ -722,6 +844,20 @@ static uint32_t inf_route_topology_flags(void* data, int x, int y) { y + build->world_offset_y); } +static uint32_t inf_route_topology_flags(void* data, int x, int y) { + const InfRouteTopologyBuildContext* build = + (const InfRouteTopologyBuildContext*)data; + if (x < INF_ARENA_MIN_X || x > INF_ARENA_MAX_X || + y < INF_ARENA_MIN_Y || y > INF_ARENA_MAX_Y) + return COLLISION_BLOCKED; + return inf_topology_collision_flags(build, x, y); +} + +static uint32_t inf_route_topology_los_flags(void* data, int x, int y) { + return inf_topology_collision_flags( + (const InfRouteTopologyBuildContext*)data, x, y); +} + static void inf_finalize_route_topology(InfernoContext* ctx) { if (!ctx || ctx->route_topology) { fprintf(stderr, "inferno route topology finalized twice\n"); @@ -733,14 +869,16 @@ static void inf_finalize_route_topology(InfernoContext* ctx) { ctx->world_offset_y, }; EncounterArenaTopologyBuildSpec spec = { - .origin_x = INF_ARENA_MIN_X, - .origin_y = INF_ARENA_MIN_Y, - .width = INF_ARENA_WIDTH, - .height = INF_ARENA_HEIGHT, + .origin_x = INF_TOPOLOGY_MIN_X, + .origin_y = INF_TOPOLOGY_MIN_Y, + .width = INF_TOPOLOGY_WIDTH, + .height = INF_TOPOLOGY_HEIGHT, .max_footprint_size = ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE, - .revision = UINT64_C(0x494e4645524e4f01), + .revision = UINT64_C(0x494e4645524e4f02), .tile_flags = inf_route_topology_flags, .tile_flags_ctx = &build, + .los_tile_flags = inf_route_topology_los_flags, + .los_tile_flags_ctx = &build, }; if (!inf_route_topology_owner.topology) { inf_route_topology_owner.topology = @@ -766,6 +904,7 @@ static void inf_finalize_context( static void inf_init_context_typed(InfernoContext* ctx) { memset(ctx, 0, sizeof(*ctx)); ctx->config = inf_default_config(); + inf_reset_npc_player_los_frame(ctx); } static void inf_init_context(EncounterContext* context) { @@ -794,7 +933,6 @@ static void inf_init_state_typed(InfernoState* s, InfernoContext* ctx) { s->rng_state = 12345; s->start_wave = ctx ? ctx->config.start_wave : 0; s->active_loadout_profile = INF_LOADOUT_PROFILE_MAX; - memset(s->npc_los_cache, -1, sizeof(s->npc_los_cache)); } static void inf_init_state(EncounterState* state, EncounterContext* context) { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc index 6d1a4000a9..581239641b 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc @@ -122,11 +122,13 @@ static void inf_lab_require_type(int type) { inf_lab_abort("npc type must be in [0,%d], got %d", INF_NUM_NPC_TYPES - 1, type); } -static void inf_lab_require_player_tile(InfernoState* s, int x, int y) { - if (!inf_in_arena(x, y)) - inf_lab_abort("player tile out of arena: (%d,%d)", x, y); - if (inf_blocked_by_pillar(s, x, y, 1)) - inf_lab_abort("player tile is blocked by a pillar: (%d,%d)", x, y); +static void inf_lab_require_player_tile( + InfernoState* s, + int x, + int y +) { + if (inf_footprint_blocked(s, x, y, 1)) + inf_lab_abort("player tile is blocked: (%d,%d)", x, y); } static void inf_lab_clear_transient(InfernoState* s) { @@ -159,9 +161,7 @@ static void inf_lab_clear_transient(InfernoState* s) { } static void inf_lab_refresh_geometry(InfernoState* s) { - inf_rebuild_los(s); - inf_invalidate_los_cache(s); - inf_rebuild_entity_collision_flags(s); + inf_rebuild_npc_collision_flags(s); inf_refresh_current_obs_slots(s); } @@ -801,7 +801,6 @@ static void inf_lab_append_pillars_json(const InfernoState* s, InfLabString* out static char* inf_lab_alloc_json(InfernoState* s) { InfLabString out; encounter_lab_string_init(&out, "inferno lab"); - inf_invalidate_los_cache(s); encounter_lab_string_append(&out, "{\"tick\":%d,\"wave\":%d," "\"player\":{\"x\":%d,\"y\":%d,\"hp\":%d,\"prayer\":%d," diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc index 9a8c9b58bc..ff08d5d55f 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc @@ -5,6 +5,14 @@ #define INF_ARENA_MAX_Y 43 #define INF_ARENA_WIDTH (INF_ARENA_MAX_X - INF_ARENA_MIN_X + 1) #define INF_ARENA_HEIGHT (INF_ARENA_MAX_Y - INF_ARENA_MIN_Y + 1) +#define INF_TOPOLOGY_MIN_X 4 +#define INF_TOPOLOGY_MAX_X 43 +#define INF_TOPOLOGY_MIN_Y 5 +#define INF_TOPOLOGY_MAX_Y 55 +#define INF_TOPOLOGY_WIDTH \ + (INF_TOPOLOGY_MAX_X - INF_TOPOLOGY_MIN_X + 1) +#define INF_TOPOLOGY_HEIGHT \ + (INF_TOPOLOGY_MAX_Y - INF_TOPOLOGY_MIN_Y + 1) #define INF_PLAYER_START_X 25 #define INF_PLAYER_START_Y 16 @@ -700,6 +708,7 @@ typedef struct { const CollisionMap* collision_map; const EncounterArenaTopology* route_topology; OsrsActorRouteCache player_route_cache; + int8_t npc_player_los_frame[INF_MAX_NPCS]; int world_offset_x; int world_offset_y; Log* log; @@ -770,8 +779,8 @@ typedef struct { InfDeadMob dead_mobs[INF_MAX_DEAD_MOBS]; int dead_mob_count; - LOSBlocker los_blockers[INF_NUM_PILLARS]; - int los_blocker_count; + uint8_t reserved_topology_pillar_storage[ + sizeof(int) * (4 * INF_NUM_PILLARS + 1)]; int wave; int wave_spawn_target; @@ -879,10 +888,10 @@ typedef struct { int player_dest_x, player_dest_y; - int8_t npc_los_cache[INF_MAX_NPCS]; - + uint8_t reserved_topology_npc_storage[INF_MAX_NPCS]; uint8_t npc_collision_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT]; - uint8_t player_collision_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT]; + uint8_t reserved_topology_player_footprint_storage[ + INF_ARENA_WIDTH * INF_ARENA_HEIGHT]; int start_wave; uint32_t rng_state; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc b/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc index 68e0f220f1..f2d71bd3c6 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc @@ -1,35 +1,12 @@ #line 2173 "encounter_inferno.h" -static int inf_in_arena(int x, int y) { - return x >= INF_ARENA_MIN_X && x <= INF_ARENA_MAX_X && - y >= INF_ARENA_MIN_Y && y <= INF_ARENA_MAX_Y; -} - -static int inf_footprint_in_arena(int x, int y, int size) { - return x >= INF_ARENA_MIN_X && x + size - 1 <= INF_ARENA_MAX_X && - y >= INF_ARENA_MIN_Y && y + size - 1 <= INF_ARENA_MAX_Y; -} - -static int inf_blocked_by_pillar_size1(InfernoState* s, int x, int y) { - for (int p = 0; p < INF_NUM_PILLARS; p++) { - if (!s->pillars[p].active) continue; - int px = s->pillars[p].x; - int py = s->pillars[p].y; - if (x >= px && x < px + INF_PILLAR_SIZE && - y >= py && y < py + INF_PILLAR_SIZE) - return 1; - } - return 0; -} - -static int inf_blocked_by_pillar(InfernoState* s, int x, int y, int size) { - if (size == 1) return inf_blocked_by_pillar_size1(s, x, y); - for (int p = 0; p < INF_NUM_PILLARS; p++) { - if (!s->pillars[p].active) continue; - if (los_aabb_overlap(x, y, size, - s->pillars[p].x, s->pillars[p].y, INF_PILLAR_SIZE)) - return 1; - } - return 0; +static int inf_footprint_blocked( + const InfernoState* s, + int x, + int y, + int size +) { + return inf_footprint_blocked_ctx( + s, inf_legacy_context(), x, y, size); } typedef struct { @@ -38,17 +15,25 @@ typedef struct { } InfWalkCtx; -static int inf_collision_flags_blocked( - uint8_t flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], int x, int y, int size +static int inf_npc_collision_flags_blocked( + const InfernoState* s, + int x, + int y, + int size ) { if (size == 1) { - int gx, gy; - return inf_grid_index(x, y, &gx, &gy) && flags[gx][gy]; + int grid_x; + int grid_y; + return inf_grid_index(x, y, &grid_x, &grid_y) && + s->npc_collision_flags[grid_x][grid_y] != 0; } for (int dx = 0; dx < size; dx++) { for (int dy = 0; dy < size; dy++) { - int gx, gy; - if (inf_grid_index(x + dx, y + dy, &gx, &gy) && flags[gx][gy]) + int grid_x; + int grid_y; + if (inf_grid_index( + x + dx, y + dy, &grid_x, &grid_y) && + s->npc_collision_flags[grid_x][grid_y] != 0) return 1; } } @@ -68,48 +53,28 @@ static int inf_npc_environment_blocked_ctx( int y, int size ) { - if (size == 1) { - if (!inf_in_arena(x, y)) return 1; - if (inf_blocked_by_pillar_size1(s, x, y)) return 1; - if (ctx->collision_map && - !collision_tile_walkable( - ctx->collision_map, 0, - x + ctx->world_offset_x, - y + ctx->world_offset_y)) - return 1; - return 0; - } - if (!inf_footprint_in_arena(x, y, size)) return 1; - if (inf_blocked_by_pillar(s, x, y, size)) return 1; - if (ctx->collision_map) { - for (int dx = 0; dx < size; dx++) { - for (int dy = 0; dy < size; dy++) { - if (!collision_tile_walkable( - ctx->collision_map, 0, - x + dx + ctx->world_offset_x, - y + dy + ctx->world_offset_y)) - return 1; - } - } - } - return 0; + return inf_footprint_blocked_ctx(s, ctx, x, y, size); } static int inf_npc_blocked(void* ctx, int x, int y, int size) { InfMoveCtx* mc = (InfMoveCtx*)ctx; InfernoState* s = mc->s; - (void)mc->self_idx; - if (inf_npc_environment_blocked_ctx(s, mc->ctx, x, y, size)) return 1; - if (inf_collision_flags_blocked(s->player_collision_flags, x, y, size)) return 1; - return inf_collision_flags_blocked(s->npc_collision_flags, x, y, size); + if (inf_npc_environment_blocked_ctx( + s, mc->ctx, x, y, size)) + return 1; + if (los_aabb_overlap( + x, y, size, s->player.x, s->player.y, 1)) + return 1; + return inf_npc_collision_flags_blocked(s, x, y, size); } static int inf_npc_blocked_ignore_player(void* ctx, int x, int y, int size) { InfMoveCtx* mc = (InfMoveCtx*)ctx; InfernoState* s = mc->s; - (void)mc->self_idx; - if (inf_npc_environment_blocked_ctx(s, mc->ctx, x, y, size)) return 1; - return inf_collision_flags_blocked(s->npc_collision_flags, x, y, size); + if (inf_npc_environment_blocked_ctx( + s, mc->ctx, x, y, size)) + return 1; + return inf_npc_collision_flags_blocked(s, x, y, size); } static int inf_npc_overlap_hold(void* ctx) { @@ -129,7 +94,11 @@ static int inf_npc_terrain_blocked(InfernoState* s, int x, int y, int size) { return inf_npc_environment_blocked_ctx(s, inf_legacy_context(), x, y, size); } -static void inf_npc_move_ctx(InfernoState* s, const InfernoContext* ctx, int idx) { +static void inf_npc_move_ctx( + InfernoState* s, + const InfernoContext* ctx, + int idx +) { InfNPC* npc = &s->npcs[idx]; if (!npc->active) return; if (npc->stun_timer > 0) return; @@ -140,79 +109,81 @@ static void inf_npc_move_ctx(InfernoState* s, const InfernoContext* ctx, int idx if (!stats->can_move) return; int uses_collision_flag = inf_npc_sets_collision_flag(npc->type); if (uses_collision_flag) - inf_unstamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); - - if (npc->type != INF_NPC_NIBBLER) { - InfMoveCtx mc = { s, ctx, idx }; - int stepped = encounter_npc_step_out_from_under( - &npc->x, &npc->y, npc->size, - s->player.x, s->player.y, - inf_npc_blocked_ignore_player, &mc, inf_npc_overlap_hold, &s->rng_state); - if (stepped == ENCOUNTER_NPC_UNDER_PLAYER_MOVED) { - npc->moved_this_tick = 1; - inf_invalidate_npc_los_cache(s, idx); - if (uses_collision_flag) - inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); - return; - } - if (stepped == ENCOUNTER_NPC_UNDER_PLAYER_HELD || - stepped == ENCOUNTER_NPC_UNDER_PLAYER_BLOCKED) { - if (uses_collision_flag) - inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); - return; + inf_unstamp_npc_collision_footprint( + s, npc->x, npc->y, npc->size); + + do { + if (npc->type != INF_NPC_NIBBLER) { + InfMoveCtx move_ctx = {s, ctx, idx}; + int stepped = encounter_npc_step_out_from_under( + &npc->x, + &npc->y, + npc->size, + s->player.x, + s->player.y, + inf_npc_blocked_ignore_player, + &move_ctx, + inf_npc_overlap_hold, + &s->rng_state); + if (stepped == ENCOUNTER_NPC_UNDER_PLAYER_MOVED) { + npc->moved_this_tick = 1; + break; + } + if (stepped == ENCOUNTER_NPC_UNDER_PLAYER_HELD || + stepped == ENCOUNTER_NPC_UNDER_PLAYER_BLOCKED) + break; } - } - - InfTargetArea target = inf_npc_current_target_area(s, npc); - if (target.is_player && npc->aggro_target >= 0) npc->aggro_target = -1; - int tx = target.x; - int ty = target.y; - int target_size = target.size; - npc->target_x = tx; - npc->target_y = ty; - if (npc->type != INF_NPC_NIBBLER) { - int has_los = target.is_player ? - inf_npc_has_los(s, idx) : - inf_npc_has_los_direct(s, idx); - if (!target.is_player) - s->npc_los_cache[idx] = (int8_t)has_los; - if (has_los) { - if (uses_collision_flag) - inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); - return; + InfTargetArea target = inf_npc_current_target_area(s, npc); + if (target.is_player && npc->aggro_target >= 0) + npc->aggro_target = -1; + int target_x = target.x; + int target_y = target.y; + int target_size = target.size; + npc->target_x = target_x; + npc->target_y = target_y; + + if (npc->type != INF_NPC_NIBBLER && + inf_npc_has_los_direct_ctx(s, ctx, idx)) + break; + if (npc->attack_timer > stats->attack_speed) + break; + + int old_x = npc->x; + int old_y = npc->y; + InfMoveCtx move_ctx = {s, ctx, idx}; + EncounterNpcStepPolicy step_policy; + encounter_npc_blocked_fn blocked; + if (target.is_player) { + step_policy = stats->attack_range == 1 + ? ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE + : ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET; + blocked = inf_npc_blocked_ignore_player; + } else { + step_policy = stats->attack_range == 1 + ? ENCOUNTER_NPC_STEP_STOP_AT_MELEE + : ENCOUNTER_NPC_STEP_TRAVEL_TARGET; + blocked = inf_npc_blocked; } - } - if (npc->attack_timer > stats->attack_speed) { - if (uses_collision_flag) - inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); - return; - } + encounter_npc_step_toward_policy( + &npc->x, + &npc->y, + target_x, + target_y, + npc->size, + target_size, + step_policy, + blocked, + &move_ctx, + inf_npc_overlap_hold, + &s->rng_state); + if (npc->x != old_x || npc->y != old_y) + npc->moved_this_tick = 1; + } while (0); - int ox = npc->x, oy = npc->y; - InfMoveCtx mc = { s, ctx, idx }; - EncounterNpcStepPolicy step_policy; - encounter_npc_blocked_fn blocked; - if (target.is_player) { - step_policy = stats->attack_range == 1 - ? ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE - : ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET; - blocked = inf_npc_blocked_ignore_player; - } else { - step_policy = stats->attack_range == 1 - ? ENCOUNTER_NPC_STEP_STOP_AT_MELEE - : ENCOUNTER_NPC_STEP_TRAVEL_TARGET; - blocked = inf_npc_blocked; - } - encounter_npc_step_toward_policy( - &npc->x, &npc->y, tx, ty, npc->size, target_size, - step_policy, blocked, &mc, inf_npc_overlap_hold, &s->rng_state); - if (npc->x != ox || npc->y != oy) { - npc->moved_this_tick = 1; - inf_invalidate_npc_los_cache(s, idx); - } if (uses_collision_flag) - inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); + inf_stamp_npc_collision_footprint( + s, npc->x, npc->y, npc->size); } static void inf_npc_move(InfernoState* s, int idx) { @@ -225,9 +196,11 @@ static void inf_meleer_dig_check_ctx(InfernoState* s, const InfernoContext* ctx, if (npc->dig_freeze_timer > 0) { npc->dig_freeze_timer--; if (npc->dig_freeze_timer == 0 && npc->dig_attack_delay == 0) { - int ox = npc->x, oy = npc->y; + int old_x = npc->x; + int old_y = npc->y; if (inf_npc_sets_collision_flag(npc->type)) - inf_unstamp_npc_collision_footprint(s, ox, oy, npc->size); + inf_unstamp_npc_collision_footprint( + s, old_x, old_y, npc->size); int candidates[5][2] = { { s->player.x - npc->size + 1, s->player.y - npc->size + 1 }, { s->player.x, s->player.y }, @@ -248,7 +221,8 @@ static void inf_meleer_dig_check_ctx(InfernoState* s, const InfernoContext* ctx, npc->x = landing_x; npc->y = landing_y; if (inf_npc_sets_collision_flag(npc->type)) - inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); + inf_stamp_npc_collision_footprint( + s, npc->x, npc->y, npc->size); npc->stun_timer = 2; npc->dig_attack_delay = 6; npc->no_los_ticks = 0; @@ -260,7 +234,7 @@ static void inf_meleer_dig_check_ctx(InfernoState* s, const InfernoContext* ctx, return; } - if (!inf_npc_has_los(s, idx)) { + if (!inf_npc_has_los_ctx(s, ctx, idx)) { npc->no_los_ticks++; } else { npc->no_los_ticks = 0; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc index 41d6e1b210..bdef576292 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -324,7 +324,8 @@ static void inf_write_obs_ctx( obs[i++] = (float)(npc->y - py) / (float)INF_ARENA_HEIGHT; obs[i++] = (float)npc->attack_timer / 10.0f; obs[i++] = (float)style / INF_ATTACK_STYLE_CODE_SCALE; - obs[i++] = inf_npc_has_los(s, npc_idx) ? 1.0f : 0.0f; + obs[i++] = + inf_npc_has_los_ctx(s, ctx, npc_idx) ? 1.0f : 0.0f; obs[i++] = (float)npc->frozen_ticks / (float)BARRAGE_FREEZE_TICKS; obs[i++] = (float)category / INF_TARGET_CATEGORY_CODE_SCALE; obs[i++] = osrs_interaction_active(&s->interaction) && @@ -494,11 +495,20 @@ static void inf_write_mask_ctx( int offset = 0; mask[offset++] = 1.0f; + uint32_t static_footprint_mask = + encounter_arena_topology_nearby_unit_footprint_mask( + ctx->route_topology, s->player.x, s->player.y); for (int d = 1; d < ENCOUNTER_MOVE_ACTIONS; d++) { - int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[d]; - int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[d]; - mask[offset++] = (inf_in_arena(nx, ny) && !inf_blocked_by_pillar(s, nx, ny, 1)) - ? 1.0f : 0.0f; + int dx = ENCOUNTER_MOVE_TARGET_DX[d]; + int dy = ENCOUNTER_MOVE_TARGET_DY[d]; + int step_bit = (dy + 2) * 5 + (dx + 2); + if (step_bit > 12) step_bit--; + int nx = s->player.x + dx; + int ny = s->player.y + dy; + int blocked = + !(static_footprint_mask & (UINT32_C(1) << step_bit)) || + inf_pillar_footprint_blocked(s, nx, ny, 1); + mask[offset++] = blocked ? 0.0f : 1.0f; } mask[offset++] = 1.0f; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc index 0f5c4025ad..565ae2ff5d 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -110,21 +110,14 @@ static float inf_record_player_reward_damage( } static int inf_tile_walkable(void* data, int x, int y) { - InfWalkCtx* wc = (InfWalkCtx*)data; - InfernoState* s = wc->s; - const InfernoContext* ctx = wc->ctx; - if (!inf_in_arena(x, y)) return 0; - if (inf_blocked_by_pillar(s, x, y, 1)) return 0; - if (ctx->collision_map) - if (!collision_tile_walkable(ctx->collision_map, 0, - x + ctx->world_offset_x, y + ctx->world_offset_y)) - return 0; - return 1; + const InfWalkCtx* walk = (const InfWalkCtx*)data; + return !inf_footprint_blocked_ctx( + walk->s, walk->ctx, x, y, 1); } static int inf_player_route_blocked(void* data, int x, int y, int size) { - InfWalkCtx* walk = (InfWalkCtx*)data; - return inf_blocked_by_pillar(walk->s, x, y, size); + const InfWalkCtx* walk = (const InfWalkCtx*)data; + return inf_pillar_footprint_blocked(walk->s, x, y, size); } static uint64_t inf_player_route_blocker_revision(const InfernoState* s) { @@ -472,7 +465,8 @@ static void inf_tick_player_ctx( int is_safe_target = inf_player_behind_zuk_shield_now(s) && s->player.attack_timer == 0 && - inf_player_can_attack_npc_from_current_tile(s, npc_idx); + inf_player_can_attack_npc_from_current_tile_ctx( + s, ctx, npc_idx); s->total_zuk_untagged_healer_targets++; if (is_safe_target) s->total_zuk_safe_untagged_healer_targets++; @@ -580,7 +574,6 @@ static void inf_tick_player_ctx( }, }; (void)osrs_encounter_player_step(&player_step_input); - inf_rebuild_player_collision_flags(s); if (can_attack && s->player.attack_timer > 0) s->player.attack_timer--; int has_zuk_healer_target = osrs_interaction_active(&s->interaction) && diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index 417852dedd..3859fdd137 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -860,9 +860,7 @@ static void inf_refresh_after_state_load(InfernoState* s, InfernoContext* ctx) { FIGHT_STYLE_RAPID, 0, &s->loadout_stats[INF_GEAR_BP]); if (ctx->human_command_mode) inf_refresh_human_loadout_stats(s); - inf_rebuild_los(s); - inf_rebuild_entity_collision_flags(s); - inf_invalidate_los_cache(s); + inf_rebuild_npc_collision_flags(s); inf_refresh_current_obs_slots_ctx(s, ctx); } @@ -916,6 +914,7 @@ static void inf_restore_ctx( InfernoState* dst = (InfernoState*)state; encounter_snapshot_copy_state_from( dst, data, offsetof(InfSnapshot, state), sizeof(InfernoState)); + inf_reset_npc_player_los_frame(ctx); inf_refresh_after_state_load(dst, ctx); } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc index 14f09f00d1..1b2964194a 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -349,6 +349,7 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint if (!ctx) ctx = inf_legacy_context(); encounter_arena_topology_require_finalized(ctx->route_topology); osrs_actor_route_cache_clear(&ctx->player_route_cache); + inf_reset_npc_player_los_frame(ctx); int saved_start = ctx->config.start_wave; uint32_t saved_rng = s->rng_state; memset(s, 0, sizeof(InfernoState)); @@ -428,7 +429,6 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint int is_zuk_wave = (effective_start >= INF_WAVE_ZUK); s->player.x = is_zuk_wave ? INF_ZUK_PLAYER_START_X : INF_PLAYER_START_X; s->player.y = is_zuk_wave ? INF_ZUK_PLAYER_START_Y : INF_PLAYER_START_Y; - inf_rebuild_player_collision_flags(s); for (int i = 0; i < INF_NUM_PILLARS; i++) { s->pillars[i].x = INF_PILLAR_POS[i][0]; @@ -441,7 +441,6 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint s->pillars[i].active = 1; } } - inf_rebuild_los(s); s->dead_mob_count = 0; @@ -450,7 +449,6 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint s->wave_spawn_delay = 0; s->wave_ready_delay = INF_START_READY_TICKS; inf_spawn_wave(s); - inf_invalidate_los_cache(s); } static void inf_reset(EncounterState* state, uint32_t seed) { @@ -463,13 +461,15 @@ static int inf_find_free_npc(InfernoState* s) { } return -1; } - -static int inf_grid_index(int x, int y, int* gx, int* gy) { - *gx = x - INF_ARENA_MIN_X; - *gy = y - INF_ARENA_MIN_Y; - return *gx >= 0 && *gx < INF_ARENA_WIDTH && *gy >= 0 && *gy < INF_ARENA_HEIGHT; +static int inf_grid_index(int x, int y, int* grid_x, int* grid_y) { + *grid_x = x - INF_ARENA_MIN_X; + *grid_y = y - INF_ARENA_MIN_Y; + return *grid_x >= 0 && *grid_x < INF_ARENA_WIDTH && + *grid_y >= 0 && *grid_y < INF_ARENA_HEIGHT; } + + static int inf_npc_sets_collision_flag(InfNPCType type) { return type != INF_NPC_NIBBLER && type != INF_NPC_ZUK_SHIELD; } @@ -477,62 +477,64 @@ static int inf_npc_sets_collision_flag(InfNPCType type) { static int inf_npc_effective_size(const InfNPC* npc) { return npc->size > 0 ? npc->size : INF_NPC_STATS[npc->type].size; } - -static void inf_unstamp_npc_collision_footprint(InfernoState* s, int x, int y, int size) { +static void inf_unstamp_npc_collision_footprint( + InfernoState* s, + int x, + int y, + int size +) { for (int dx = 0; dx < size; dx++) { for (int dy = 0; dy < size; dy++) { - int gx, gy; - if (inf_grid_index(x + dx, y + dy, &gx, &gy)) { - if (s->npc_collision_flags[gx][gy] > 0) - s->npc_collision_flags[gx][gy]--; - } + int grid_x; + int grid_y; + if (!inf_grid_index( + x + dx, y + dy, &grid_x, &grid_y)) + continue; + if (s->npc_collision_flags[grid_x][grid_y] > 0) + s->npc_collision_flags[grid_x][grid_y]--; } } } -static void inf_stamp_npc_collision_footprint(InfernoState* s, int x, int y, int size) { +static void inf_stamp_npc_collision_footprint( + InfernoState* s, + int x, + int y, + int size +) { for (int dx = 0; dx < size; dx++) { for (int dy = 0; dy < size; dy++) { - int gx, gy; - if (inf_grid_index(x + dx, y + dy, &gx, &gy)) { - assert(s->npc_collision_flags[gx][gy] < UINT8_MAX); - s->npc_collision_flags[gx][gy]++; - } + int grid_x; + int grid_y; + if (!inf_grid_index( + x + dx, y + dy, &grid_x, &grid_y)) + continue; + assert(s->npc_collision_flags[grid_x][grid_y] < UINT8_MAX); + s->npc_collision_flags[grid_x][grid_y]++; } } } -static void inf_clear_player_collision_flags(InfernoState* s) { - memset(s->player_collision_flags, 0, sizeof(s->player_collision_flags)); -} - -static void inf_stamp_player_collision_flags(InfernoState* s) { - int gx, gy; - if (inf_grid_index(s->player.x, s->player.y, &gx, &gy)) - s->player_collision_flags[gx][gy] = 1; -} - -static void inf_rebuild_player_collision_flags(InfernoState* s) { - inf_clear_player_collision_flags(s); - inf_stamp_player_collision_flags(s); -} - -static void inf_rebuild_entity_collision_flags(InfernoState* s) { +static void inf_rebuild_npc_collision_flags(InfernoState* s) { memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); - inf_rebuild_player_collision_flags(s); - for (int i = 0; i < INF_MAX_NPCS; i++) { - InfNPC* npc = &s->npcs[i]; - if (!npc->active) continue; - if (!inf_npc_sets_collision_flag(npc->type)) continue; - inf_stamp_npc_collision_footprint(s, npc->x, npc->y, inf_npc_effective_size(npc)); + for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { + InfNPC* npc = &s->npcs[npc_idx]; + if (!npc->active || + !inf_npc_sets_collision_flag(npc->type)) + continue; + inf_stamp_npc_collision_footprint( + s, npc->x, npc->y, inf_npc_effective_size(npc)); } } + + static void inf_deactivate_npc(InfernoState* s, int idx) { if (idx < 0 || idx >= INF_MAX_NPCS) return; InfNPC* npc = &s->npcs[idx]; if (npc->active && inf_npc_sets_collision_flag(npc->type)) - inf_unstamp_npc_collision_footprint(s, npc->x, npc->y, inf_npc_effective_size(npc)); + inf_unstamp_npc_collision_footprint( + s, npc->x, npc->y, inf_npc_effective_size(npc)); encounter_pending_hit_queue_clear(&npc->pending_hits); npc->active = 0; } @@ -566,9 +568,9 @@ static void inf_init_npc(InfernoState* s, int idx, InfNPCType type, int x, int y npc->had_los_last_tick = 0; npc->stun_timer = stats->stun_on_spawn; inf_npc_init_type_state(npc); - if (inf_npc_sets_collision_flag(type)) inf_stamp_npc_collision_footprint(s, x, y, stats->size); + } static int inf_find_first_active_npc_of_type(const InfernoState* s, InfNPCType type) { @@ -643,9 +645,8 @@ static void inf_spawn_wave(InfernoState* s) { const InfWaveDef* w = &INF_WAVES[s->wave]; for (int i = 0; i < INF_MAX_NPCS; i++) s->npcs[i].active = 0; - encounter_pending_hit_queue_clear(&s->player_pending_hits); memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); - inf_rebuild_player_collision_flags(s); + encounter_pending_hit_queue_clear(&s->player_pending_hits); s->dead_mob_count = 0; @@ -662,21 +663,17 @@ static void inf_spawn_wave(InfernoState* s) { } if (s->wave >= 66) { - int pillars_changed = 0; for (int p = 0; p < INF_NUM_PILLARS; p++) { if (s->pillars[p].active) { s->pillars[p].active = 0; s->pillars[p].hp = 0; - pillars_changed = 1; } } - if (pillars_changed) inf_rebuild_los(s); } if (s->wave == 66) { s->player.x = 18; s->player.y = 32; - inf_rebuild_player_collision_flags(s); int slot = inf_find_free_npc(s); if (slot >= 0) { inf_init_npc(s, slot, INF_NPC_JAD, 23, 30); @@ -689,7 +686,6 @@ static void inf_spawn_wave(InfernoState* s) { if (s->wave == 67) { s->player.x = 25; s->player.y = 30; - inf_rebuild_player_collision_flags(s); int stuns[3] = { 1, 4, 7 }; for (int i = 2; i > 0; i--) { int j = encounter_rand_int(&s->rng_state, i + 1); @@ -738,7 +734,6 @@ static void inf_spawn_wave(InfernoState* s) { s->player.x = INF_ZUK_PLAYER_START_X; s->player.y = INF_ZUK_PLAYER_START_Y; - inf_rebuild_player_collision_flags(s); inf_seed_joseph_zuk_checkpoint(s); return; } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc index 8bc1fc07c3..788b013131 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc @@ -432,15 +432,23 @@ static int inf_has_live_player_target(const InfernoState* s) { return inf_idle_diagnostic_summary(s).has_live_player_target; } -static int inf_has_attackable_player_target(const InfernoState* s) { +static int inf_has_attackable_player_target_ctx( + const InfernoState* s, + const InfernoContext* ctx +) { for (int i = 0; i < INF_MAX_NPCS; i++) { if (inf_npc_is_live_player_target(s, i) && - inf_player_can_attack_npc_from_current_tile(s, i)) + inf_player_can_attack_npc_from_current_tile_ctx(s, ctx, i)) return 1; } return 0; } +static int inf_has_attackable_player_target(const InfernoState* s) { + return inf_has_attackable_player_target_ctx( + s, inf_legacy_context()); +} + static InfIdleDiagnosticPhase inf_idle_diagnostic_phase_from_summary( const InfernoState* s, InfIdleDiagnosticSummary summary @@ -461,7 +469,10 @@ static InfIdleDiagnosticPhase inf_idle_diagnostic_phase(const InfernoState* s) { s, inf_idle_diagnostic_summary(s)); } -static int inf_player_has_immediate_threat(const InfernoState* s) { +static int inf_player_has_immediate_threat_ctx( + const InfernoState* s, + const InfernoContext* ctx +) { if (s->tick_scratch.attacks_fired > 0) return 1; for (int h = 0; h < s->player_pending_hits.count; h++) { const EncounterPendingHit* ph = &s->player_pending_hits.hits[h]; @@ -470,20 +481,27 @@ static int inf_player_has_immediate_threat(const InfernoState* s) { } for (int i = 0; i < INF_MAX_NPCS; i++) { const InfNPC* npc = &s->npcs[i]; - InfNpcPlayerThreat threat = inf_npc_player_threat(s, npc); + InfNpcPlayerThreat threat = + inf_npc_player_threat_ctx(s, ctx, npc); if (threat.telegraph_style_mask && threat.can_attack_this_tick) return 1; } return 0; } +static int inf_player_has_immediate_threat(const InfernoState* s) { + return inf_player_has_immediate_threat_ctx( + s, inf_legacy_context()); +} + typedef struct { int has_attackable_player_target; int player_has_immediate_threat; } InfIdleMissedActionFacts; static InfIdleMissedActionFacts inf_idle_missed_action_facts( - const InfernoState* s + const InfernoState* s, + const InfernoContext* ctx ) { InfIdleMissedActionFacts facts = { .player_has_immediate_threat = s->tick_scratch.attacks_fired > 0, @@ -500,11 +518,12 @@ static InfIdleMissedActionFacts inf_idle_missed_action_facts( for (int i = 0; i < INF_MAX_NPCS; i++) { if (!facts.has_attackable_player_target && inf_npc_is_live_player_target(s, i) && - inf_player_can_attack_npc_from_current_tile(s, i)) + inf_player_can_attack_npc_from_current_tile_ctx(s, ctx, i)) facts.has_attackable_player_target = 1; if (!facts.player_has_immediate_threat) { const InfNPC* npc = &s->npcs[i]; - InfNpcPlayerThreat threat = inf_npc_player_threat(s, npc); + InfNpcPlayerThreat threat = + inf_npc_player_threat_ctx(s, ctx, npc); if (threat.telegraph_style_mask && threat.can_attack_this_tick) facts.player_has_immediate_threat = 1; } @@ -598,9 +617,8 @@ static void inf_step_ctx(EncounterState* state, EncounterContext* context, const inf_resolve_player_pending_hits(s); inf_resolve_pending_sparks(s); + inf_reset_npc_player_los_frame(ctx); if (!in_wave_gap && !in_ready_gap) { - inf_rebuild_player_collision_flags(s); - inf_invalidate_los_cache(s); inf_tick_npcs_ctx(s, ctx); } inf_update_healer_transition_stats(s); @@ -630,7 +648,7 @@ static void inf_step_ctx(EncounterState* state, EncounterContext* context, const (s->player.x != player_x_before_tick_player || s->player.y != player_y_before_tick_player) ? 1 : 0; if (s->tick_scratch.player_moved) - inf_invalidate_los_cache(s); + inf_reset_npc_player_los_frame(ctx); inf_resolve_jad_prayer_checks_after_player(s, ctx); if (can_player_attack) { @@ -649,7 +667,7 @@ static void inf_step_ctx(EncounterState* state, EncounterContext* context, const attack_ready_no_attack = s->ticks_without_action > 0; if (attack_ready_no_attack) { InfIdleMissedActionFacts missed_action_facts = - inf_idle_missed_action_facts(s); + inf_idle_missed_action_facts(s, ctx); target_available_no_attack = idle_diagnostic_summary.has_live_player_target; safe_attack_opportunity_missed = @@ -719,7 +737,7 @@ static void inf_step_ctx(EncounterState* state, EncounterContext* context, const if (spawn_wave_now) { s->wave = s->wave_spawn_target; inf_spawn_wave(s); - inf_invalidate_los_cache(s); + inf_reset_npc_player_los_frame(ctx); goto finish_step; } if (s->wave_spawn_delay > 0) goto finish_step; diff --git a/ocean/osrs/osrs_collision.h b/ocean/osrs/osrs_collision.h index 0fb9325107..4e1df5e038 100644 --- a/ocean/osrs/osrs_collision.h +++ b/ocean/osrs/osrs_collision.h @@ -492,11 +492,65 @@ static inline int entity_has_line_of_sight( return has_line_of_sight(blockers, blocker_count, a_px, a_py, t_px, t_py, 1, range); } +static inline int entity_has_line_of_sight_with_flags( + los_tile_flags_fn tile_flags, + void* tile_flags_ctx, + int ax, + int ay, + int a_size, + int tx, + int ty, + int t_size, + int range +) { + if (range == 1) { + if (los_aabb_overlap(ax, ay, a_size, tx, ty, t_size)) return 0; + + int a_x1 = ax + a_size - 1; + int a_y1 = ay + a_size - 1; + int t_x1 = tx + t_size - 1; + int t_y1 = ty + t_size - 1; + + return (a_x1 + 1 == tx && + los_intervals_overlap(ay, a_y1, ty, t_y1)) || + (t_x1 + 1 == ax && + los_intervals_overlap(ay, a_y1, ty, t_y1)) || + (a_y1 + 1 == ty && + los_intervals_overlap(ax, a_x1, tx, t_x1)) || + (t_y1 + 1 == ay && + los_intervals_overlap(ax, a_x1, tx, t_x1)); + } + + int a_px = tx; + if (a_px < ax) a_px = ax; + if (a_px >= ax + a_size) a_px = ax + a_size - 1; + int a_py = ty; + if (a_py < ay) a_py = ay; + if (a_py >= ay + a_size) a_py = ay + a_size - 1; + + int t_px = ax; + if (t_px < tx) t_px = tx; + if (t_px >= tx + t_size) t_px = tx + t_size - 1; + int t_py = ay; + if (t_py < ty) t_py = ty; + if (t_py >= ty + t_size) t_py = ty + t_size - 1; + + return los_has_line_of_sight_with_flags( + tile_flags, + tile_flags_ctx, + a_px, + a_py, + t_px, + t_py, + 1, + range); +} + #define ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION 64 #define ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES \ (ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION * \ ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION) -#define ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE 5 +#define ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE 7 #define ENCOUNTER_ARENA_TOPOLOGY_LOS_WORDS \ ((ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES * \ ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES + 63) / 64) @@ -512,7 +566,14 @@ typedef struct { uint64_t revision; encounter_arena_tile_flags_fn tile_flags; void* tile_flags_ctx; + encounter_arena_tile_flags_fn los_tile_flags; + void* los_tile_flags_ctx; } EncounterArenaTopologyBuildSpec; +typedef enum { + ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN = 0, + ENCOUNTER_ARENA_TOPOLOGY_LOS_FLAGGED = 1, +} EncounterArenaTopologyLosMode; + typedef struct EncounterArenaTopology { int origin_x; @@ -523,6 +584,7 @@ typedef struct EncounterArenaTopology { int max_footprint_size; uint64_t revision; uint8_t finalized; + EncounterArenaTopologyLosMode static_los_mode; uint32_t static_collision_flags[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint8_t static_blocked[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint8_t footprint_blocked @@ -532,6 +594,8 @@ typedef struct EncounterArenaTopology { [ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE] [ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint64_t static_los_bits[ENCOUNTER_ARENA_TOPOLOGY_LOS_WORDS]; + uint32_t nearby_unit_footprint_masks + [ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; } EncounterArenaTopology; static inline void encounter_arena_topology_abort( @@ -666,17 +730,28 @@ static inline int encounter_arena_topology_build_step_allowed( return 1; } +typedef struct { + const EncounterArenaTopology* topology; + const EncounterArenaTopologyBuildSpec* spec; +} EncounterArenaTopologyLosBuildContext; + static uint32_t encounter_arena_topology_los_flags( - void* ctx, + void* data, int x, int y ) { - const EncounterArenaTopology* topology = - (const EncounterArenaTopology*)ctx; - if (!encounter_arena_topology_contains_raw(topology, x, y)) + const EncounterArenaTopologyLosBuildContext* build = + (const EncounterArenaTopologyLosBuildContext*)data; + if (!encounter_arena_topology_contains_raw(build->topology, x, y)) return LOS_FULL_MASK; - uint32_t flags = topology->static_collision_flags[ - encounter_arena_topology_index_raw(topology, x, y)]; + encounter_arena_tile_flags_fn tile_flags = + build->spec->los_tile_flags + ? build->spec->los_tile_flags + : build->spec->tile_flags; + void* tile_flags_ctx = build->spec->los_tile_flags + ? build->spec->los_tile_flags_ctx + : build->spec->tile_flags_ctx; + uint32_t flags = tile_flags ? tile_flags(tile_flags_ctx, x, y) : 0; if (flags & COLLISION_BLOCKED) return LOS_FULL_MASK; return flags & (LOS_FULL_MASK | LOS_EAST_MASK | LOS_WEST_MASK | @@ -777,6 +852,26 @@ static inline EncounterArenaTopology* encounter_arena_topology_build( } } } + for (int local_x = 0; local_x < topology->width; local_x++) { + for (int local_y = 0; local_y < topology->height; local_y++) { + int x = topology->origin_x + local_x; + int y = topology->origin_y + local_y; + int index = local_x * topology->height + local_y; + uint32_t mask = 0; + for (int dy = -2; dy <= 2; dy++) { + for (int dx = -2; dx <= 2; dx++) { + if (dx == 0 && dy == 0) continue; + int bit = (dy + 2) * 5 + (dx + 2); + if (bit > 12) bit--; + if (!encounter_arena_topology_footprint_blocked_raw( + topology, x + dx, y + dy, 1)) + mask |= UINT32_C(1) << bit; + } + } + topology->nearby_unit_footprint_masks[index] = mask; + } + } + for (int size = 1; size <= topology->max_footprint_size; size++) { for (int local_x = 0; local_x < topology->width; local_x++) { @@ -801,43 +896,64 @@ static inline EncounterArenaTopology* encounter_arena_topology_build( } } + EncounterArenaTopologyLosBuildContext los_build = { + .topology = topology, + .spec = spec, + }; + topology->static_los_mode = ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN; for (int source = 0; source < topology->tile_count; source++) { int source_x = topology->origin_x + source / topology->height; int source_y = topology->origin_y + source % topology->height; if (encounter_arena_topology_los_flags( - topology, source_x, source_y) == 0) - encounter_arena_topology_set_los(topology, source, source); - - for (int target = source + 1; - target < topology->tile_count; - target++) { - int target_x = - topology->origin_x + target / topology->height; - int target_y = - topology->origin_y + target % topology->height; - int forward = los_has_line_of_sight_with_flags( - encounter_arena_topology_los_flags, - topology, - source_x, - source_y, - target_x, - target_y, - 1, - 0); - int reverse = los_has_line_of_sight_with_flags( - encounter_arena_topology_los_flags, - topology, - target_x, - target_y, - source_x, - source_y, - 1, - 0); - if (forward && reverse) { - encounter_arena_topology_set_los( - topology, source, target); + &los_build, source_x, source_y) != 0) { + topology->static_los_mode = + ENCOUNTER_ARENA_TOPOLOGY_LOS_FLAGGED; + break; + } + } + if (topology->static_los_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_FLAGGED) { + for (int source = 0; source < topology->tile_count; source++) { + int source_x = + topology->origin_x + source / topology->height; + int source_y = + topology->origin_y + source % topology->height; + if (encounter_arena_topology_los_flags( + &los_build, source_x, source_y) == 0) encounter_arena_topology_set_los( - topology, target, source); + topology, source, source); + + for (int target = source + 1; + target < topology->tile_count; + target++) { + int target_x = + topology->origin_x + target / topology->height; + int target_y = + topology->origin_y + target % topology->height; + int forward = los_has_line_of_sight_with_flags( + encounter_arena_topology_los_flags, + &los_build, + source_x, + source_y, + target_x, + target_y, + 1, + 0); + int reverse = los_has_line_of_sight_with_flags( + encounter_arena_topology_los_flags, + &los_build, + target_x, + target_y, + source_x, + source_y, + 1, + 0); + if (forward) + encounter_arena_topology_set_los( + topology, source, target); + if (reverse) + encounter_arena_topology_set_los( + topology, target, source); } } } @@ -951,6 +1067,17 @@ static inline int encounter_arena_topology_footprint_blocked( topology, x, y, size); } +static inline uint32_t encounter_arena_topology_nearby_unit_footprint_mask( + const EncounterArenaTopology* topology, + int x, + int y +) { + encounter_arena_topology_require_finalized(topology); + if (!encounter_arena_topology_contains_raw(topology, x, y)) return 0; + int index = encounter_arena_topology_index_raw(topology, x, y); + return topology->nearby_unit_footprint_masks[index]; +} + static inline int encounter_arena_topology_step_allowed( const EncounterArenaTopology* topology, int x, @@ -987,16 +1114,19 @@ static inline int encounter_arena_topology_los_clear( topology, actor_size); encounter_arena_topology_require_footprint_size( topology, target_size); - if (encounter_arena_topology_footprint_blocked_raw( - topology, actor_x, actor_y, actor_size) || - encounter_arena_topology_footprint_blocked_raw( - topology, target_x, target_y, target_size)) - return 0; - int64_t actor_max_x = (int64_t)actor_x + actor_size - 1; int64_t actor_max_y = (int64_t)actor_y + actor_size - 1; int64_t target_max_x = (int64_t)target_x + target_size - 1; int64_t target_max_y = (int64_t)target_y + target_size - 1; + if (!encounter_arena_topology_contains_raw( + topology, actor_x, actor_y) || + !encounter_arena_topology_contains_raw( + topology, (int)actor_max_x, (int)actor_max_y) || + !encounter_arena_topology_contains_raw( + topology, target_x, target_y) || + !encounter_arena_topology_contains_raw( + topology, (int)target_max_x, (int)target_max_y)) + return 0; int overlap = (int64_t)actor_x <= target_max_x && (int64_t)target_x <= actor_max_x && @@ -1038,6 +1168,9 @@ static inline int encounter_arena_topology_los_clear( if (attack_range > 0 && (abs_dx > attack_range || abs_dy > attack_range)) return 0; + if (topology->static_los_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN) + return 1; int actor_index = encounter_arena_topology_index_raw( topology, (int)actor_los_x, (int)actor_los_y); diff --git a/ocean/osrs/osrs_pathfinding.h b/ocean/osrs/osrs_pathfinding.h index 1147a43ce9..33ed5bc272 100644 --- a/ocean/osrs/osrs_pathfinding.h +++ b/ocean/osrs/osrs_pathfinding.h @@ -66,6 +66,7 @@ typedef enum { OSRS_LOS_OPEN = 0, OSRS_LOS_BLOCKERS, OSRS_LOS_TILE, + OSRS_LOS_FLAGS, } OsrsLosKind; typedef struct { @@ -73,6 +74,7 @@ typedef struct { const LOSBlocker* blockers; int blocker_count; int (*tile_blocked)(void* ctx, int x, int y); + los_tile_flags_fn tile_flags; void* tile_ctx; } OsrsLosQuery; @@ -82,6 +84,7 @@ static inline OsrsLosQuery osrs_los_open(void) { query.blockers = NULL; query.blocker_count = 0; query.tile_blocked = NULL; + query.tile_flags = NULL; query.tile_ctx = NULL; return query; } @@ -95,6 +98,7 @@ static inline OsrsLosQuery osrs_los_blockers( query.blockers = blockers; query.blocker_count = blocker_count; query.tile_blocked = NULL; + query.tile_flags = NULL; query.tile_ctx = NULL; return query; } @@ -108,6 +112,21 @@ static inline OsrsLosQuery osrs_los_tile( query.blockers = NULL; query.blocker_count = 0; query.tile_blocked = tile_blocked; + query.tile_flags = NULL; + query.tile_ctx = tile_ctx; + return query; +} + +static inline OsrsLosQuery osrs_los_flags( + los_tile_flags_fn tile_flags, + void* tile_ctx +) { + OsrsLosQuery query; + query.kind = OSRS_LOS_FLAGS; + query.blockers = NULL; + query.blocker_count = 0; + query.tile_blocked = NULL; + query.tile_flags = tile_flags; query.tile_ctx = tile_ctx; return query; } @@ -119,6 +138,7 @@ static inline const OsrsLosQuery* osrs_los_open_query(void) { 0, NULL, NULL, + NULL, }; return &query; } @@ -128,13 +148,16 @@ static inline int osrs_los_query_valid( int attack_range ) { if (attack_range <= 1) return 1; - if (!query || query->kind < OSRS_LOS_OPEN || query->kind > OSRS_LOS_TILE) + if (!query || query->kind < OSRS_LOS_OPEN || + query->kind > OSRS_LOS_FLAGS) return 0; if (query->kind == OSRS_LOS_BLOCKERS && (query->blocker_count < 0 || (query->blocker_count > 0 && !query->blockers))) return 0; - return query->kind != OSRS_LOS_TILE || query->tile_blocked; + if (query->kind == OSRS_LOS_TILE && !query->tile_blocked) + return 0; + return query->kind != OSRS_LOS_FLAGS || query->tile_flags; } static inline void osrs_los_require_query( @@ -237,6 +260,18 @@ static inline int osrs_los_clear( return osrs_los_tile_ray_clear( query, t_los_x, t_los_y, p_los_x, p_los_y); } + + case OSRS_LOS_FLAGS: + return entity_has_line_of_sight_with_flags( + query->tile_flags, + query->tile_ctx, + px, + py, + psize, + tx, + ty, + tsize, + attack_range); } fprintf(stderr, "unhandled OSRS LoS query kind: %d\n", (int)query->kind); diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h index e50c7c892d..f4ec2b9e56 100644 --- a/ocean/osrs/osrs_render.h +++ b/ocean/osrs/osrs_render.h @@ -5208,18 +5208,9 @@ static void render_draw_3d_world(RenderClient* rc) { if (slot < 0 || slot >= INF_MAX_NPCS) continue; InfNPC* npc = &debug_inferno_state->npcs[slot]; if (!npc->active || npc->death_ticks > 0) continue; - const EncounterLoadoutStats* ls = - &debug_inferno_state->loadout_stats[debug_inferno_state->weapon_set]; - OsrsLosQuery los_query = osrs_los_blockers( - debug_inferno_state->los_blockers, - debug_inferno_state->los_blocker_count); - int can_atk = encounter_player_can_attack( - debug_inferno_state->player.x, - debug_inferno_state->player.y, - npc->x, npc->y, npc->size, - ls->attack_range, - NULL, 0, 0, - &los_query); + int can_atk = + inf_player_can_attack_npc_from_current_tile( + debug_inferno_state, slot); lc = can_atk ? GREEN : RED; } @@ -5375,16 +5366,8 @@ static void render_draw_overhead_status(RenderClient* rc, OsrsEnv* env) { } { - const EncounterLoadoutStats* ls = &is->loadout_stats[is->weapon_set]; - OsrsLosQuery los_query = osrs_los_blockers( - is->los_blockers, - is->los_blocker_count); - int can_atk = encounter_player_can_attack( - is->player.x, is->player.y, - npc->x, npc->y, npc->size, - ls->attack_range, - NULL, 0, 0, - &los_query); + int can_atk = + inf_player_can_attack_npc_from_current_tile(is, slot); const char* patk_txt = can_atk ? "P>NPC" : "P>NPC X"; Color patk_col = can_atk ? GREEN : RED; int pw = MeasureText(patk_txt, fs); diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index f7d9d955ad..52dd0b91bf 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -255,13 +255,13 @@ static void reset_test_context(void) { ctx->human_commands = NULL; ctx->human_command_count = 0; ctx->human_command_mode = 0; + inf_reset_npc_player_los_frame(ctx); } static InfernoState make_test_state(int player_x, int player_y) { reset_test_context(); InfernoState state; memset(&state, 0, sizeof(state)); - memset(state.npc_los_cache, -1, sizeof(state.npc_los_cache)); state.player.x = player_x; state.player.y = player_y; state.player_last_interaction_target_slot = -1; @@ -402,9 +402,20 @@ enum { }; static void init_jad_timing_test_state(InfernoState* state, int player_x, int player_y, int jad_x, int jad_y) { + if (player_x == 10 && player_y == 10) { + int distance = jad_x - player_x; + player_x = 20; + player_y = 20; + if (distance > 10) { + jad_x = player_x; + jad_y = player_y + distance; + } else { + jad_x = player_x + distance; + jad_y = player_y; + } + } reset_test_context(); memset(state, 0, sizeof(*state)); - memset(state->npc_los_cache, -1, sizeof(state->npc_los_cache)); state->rng_state = 12345; state->wave = 66; state->player.entity_type = ENTITY_PLAYER; @@ -2285,9 +2296,9 @@ static void test_late_start_supply_observations(void) { static void test_tagged_jad_healer_melee_geometry(void) { printf("--- tagged jad healer melee geometry ---\n"); - InfernoState diagonal_state = make_test_state(5, 5); - InfernoState cardinal_state = make_test_state(5, 5); - InfernoState meleer_diagonal_state = make_test_state(5, 5); + InfernoState diagonal_state = make_test_state(20, 20); + InfernoState cardinal_state = make_test_state(20, 20); + InfernoState meleer_diagonal_state = make_test_state(20, 20); diagonal_state.player.current_defence = 99; diagonal_state.player.current_magic = 99; @@ -2304,15 +2315,18 @@ static void test_tagged_jad_healer_melee_geometry(void) { meleer_diagonal_state.player.prayer = PRAYER_NONE; meleer_diagonal_state.weapon_set = INF_GEAR_MAGE; - diagonal_state.npcs[0] = make_test_npc(INF_NPC_HEALER_JAD, 6, 6, 1); + diagonal_state.npcs[0] = + make_test_npc(INF_NPC_HEALER_JAD, 21, 21, 1); diagonal_state.npcs[0].active = 1; diagonal_state.npcs[0].aggro_target = -1; - cardinal_state.npcs[0] = make_test_npc(INF_NPC_HEALER_JAD, 6, 5, 1); + cardinal_state.npcs[0] = + make_test_npc(INF_NPC_HEALER_JAD, 21, 20, 1); cardinal_state.npcs[0].active = 1; cardinal_state.npcs[0].aggro_target = -1; - meleer_diagonal_state.npcs[0] = make_test_npc(INF_NPC_MELEER, 6, 6, 1); + meleer_diagonal_state.npcs[0] = + make_test_npc(INF_NPC_MELEER, 21, 21, 1); meleer_diagonal_state.npcs[0].active = 1; meleer_diagonal_state.npcs[0].aggro_target = -1; @@ -2320,16 +2334,19 @@ static void test_tagged_jad_healer_melee_geometry(void) { inf_npc_attack(&cardinal_state, 0); inf_npc_attack(&meleer_diagonal_state, 0); - ASSERT_INT_EQ("diagonal healer does not attack", diagonal_state.npcs[0].attacked_this_tick, 0); + ASSERT_INT_EQ("diagonal healer does not attack", + diagonal_state.npcs[0].attacked_this_tick, 0); ASSERT_INT_EQ("diagonal healer keeps attack style none", - diagonal_state.npcs[0].attack_style_this_tick, ATTACK_STYLE_NONE); - ASSERT_INT_EQ("cardinal healer attacks", cardinal_state.npcs[0].attacked_this_tick, 1); + diagonal_state.npcs[0].attack_style_this_tick, ATTACK_STYLE_NONE); + ASSERT_INT_EQ("cardinal healer attacks", + cardinal_state.npcs[0].attacked_this_tick, 1); ASSERT_INT_EQ("cardinal healer uses melee", - cardinal_state.npcs[0].attack_style_this_tick, ATTACK_STYLE_MELEE); + cardinal_state.npcs[0].attack_style_this_tick, ATTACK_STYLE_MELEE); ASSERT_INT_EQ("diagonal pure meleer does not attack", - meleer_diagonal_state.npcs[0].attacked_this_tick, 0); + meleer_diagonal_state.npcs[0].attacked_this_tick, 0); ASSERT_INT_EQ("diagonal pure meleer keeps attack style none", - meleer_diagonal_state.npcs[0].attack_style_this_tick, ATTACK_STYLE_NONE); + meleer_diagonal_state.npcs[0].attack_style_this_tick, + ATTACK_STYLE_NONE); } static void test_overlap_shuffle_hold_after_recent_target_click(void) { @@ -2343,7 +2360,6 @@ static void test_overlap_shuffle_hold_after_recent_target_click(void) { INF_NPC_RANGER, 20, 20, INF_NPC_STATS[INF_NPC_RANGER].size); state.npcs[0].active = 1; - inf_rebuild_entity_collision_flags(&state); inf_npc_move(&state, 0); ASSERT_INT_EQ("held overlap keeps x", state.npcs[0].x, 20); @@ -2351,8 +2367,8 @@ static void test_overlap_shuffle_hold_after_recent_target_click(void) { ASSERT_INT_EQ("held overlap does not mark moved", state.npcs[0].moved_this_tick, 0); } -static void test_overlap_shuffle_respects_npc_collision_flags(void) { - printf("--- overlap shuffle respects npc collision flags ---\n"); +static void test_overlap_shuffle_respects_npc_occupancy(void) { + printf("--- overlap shuffle respects npc occupancy ---\n"); const uint32_t west_shuffle_seed = 12345; @@ -2366,8 +2382,8 @@ static void test_overlap_shuffle_respects_npc_collision_flags(void) { clear_state.npcs[2].active = 1; clear_state.npcs[3] = make_test_npc(INF_NPC_HEALER_JAD, 20, 19, 1); clear_state.npcs[3].active = 1; + inf_rebuild_npc_collision_flags(&clear_state); - inf_rebuild_entity_collision_flags(&clear_state); inf_npc_move(&clear_state, 0); ASSERT_INT_EQ("clear sampled overlap shuffle moves west x", clear_state.npcs[0].x, 19); @@ -2384,8 +2400,8 @@ static void test_overlap_shuffle_respects_npc_collision_flags(void) { blocked_state.npcs[2].active = 1; blocked_state.npcs[3] = make_test_npc(INF_NPC_HEALER_JAD, 20, 21, 1); blocked_state.npcs[3].active = 1; + inf_rebuild_npc_collision_flags(&blocked_state); - inf_rebuild_entity_collision_flags(&blocked_state); inf_npc_move(&blocked_state, 0); ASSERT_INT_EQ("blocked sampled overlap shuffle does not fallback x", blocked_state.npcs[0].x, 20); @@ -2403,7 +2419,6 @@ static void test_large_npc_overlap_shuffle_can_partially_unclip(void) { INF_NPC_MAGER, 20, 20, INF_NPC_STATS[INF_NPC_MAGER].size); state.npcs[0].active = 1; - inf_rebuild_entity_collision_flags(&state); inf_npc_move(&state, 0); int dx = abs(state.npcs[0].x - 20); @@ -2412,8 +2427,8 @@ static void test_large_npc_overlap_shuffle_can_partially_unclip(void) { ASSERT_INT_EQ("large npc marks moved", state.npcs[0].moved_this_tick, 1); } -static void test_player_movement_ignores_npc_collision_flags(void) { - printf("--- player movement ignores npc collision flags ---\n"); +static void test_player_movement_ignores_npc_occupancy(void) { + printf("--- player movement ignores npc occupancy ---\n"); InfernoState state = make_test_state(18, 30); state.player_dest_x = 26; @@ -2425,12 +2440,6 @@ static void test_player_movement_ignores_npc_collision_flags(void) { INF_NPC_JAD, 20, 30, INF_NPC_STATS[INF_NPC_JAD].size); state.npcs[1].active = 1; - inf_rebuild_entity_collision_flags(&state); - - ASSERT_INT_EQ("healer tile has npc collision flag", - state.npc_collision_flags[19 - INF_ARENA_MIN_X][30 - INF_ARENA_MIN_Y], 1); - ASSERT_INT_EQ("jad tile has npc collision flag", - state.npc_collision_flags[20 - INF_ARENA_MIN_X][30 - INF_ARENA_MIN_Y], 1); InfWalkCtx walk_ctx = { &state, inf_legacy_context() }; ASSERT_INT_EQ("player can walk through jad healer tile", inf_tile_walkable(&walk_ctx, 19, 30), 1); @@ -2462,7 +2471,6 @@ static void test_tagged_jad_healer_stops_at_melee_contact(void) { state.npcs[0].active = 1; state.npcs[0].aggro_target = -1; - inf_rebuild_entity_collision_flags(&state); inf_npc_move(&state, 0); ASSERT_INT_EQ("healer keeps melee contact x", state.npcs[0].x, 19); @@ -2486,7 +2494,6 @@ static void test_tagged_jad_healers_queue_behind_front_healer(void) { state.npcs[i].attack_timer = 0; } - inf_rebuild_entity_collision_flags(&state); inf_tick_npcs(&state); int attacks = 0; @@ -2517,13 +2524,14 @@ static void test_meleer_dig_can_stack_without_losing_collision_flag(void) { INF_NPC_RANGER, dig_x, dig_y, INF_NPC_STATS[INF_NPC_RANGER].size); state.npcs[1].active = 1; - inf_rebuild_entity_collision_flags(&state); inf_meleer_dig_check(&state, 0); ASSERT_INT_EQ("dig lands on first candidate x", state.npcs[0].x, dig_x); ASSERT_INT_EQ("dig lands on first candidate y", state.npcs[0].y, dig_y); - ASSERT_INT_EQ("stacked landing tile has both NPCs", - state.npc_collision_flags[dig_x - INF_ARENA_MIN_X][dig_y - INF_ARENA_MIN_Y], 2); + ASSERT_INT_EQ("stacked landing keeps both NPCs at x", + state.npcs[1].x, state.npcs[0].x); + ASSERT_INT_EQ("stacked landing keeps both NPCs at y", + state.npcs[1].y, state.npcs[0].y); } static void test_jad_healer_spawn_offsets_match_wave_67_reference(void) { @@ -2537,7 +2545,6 @@ static void test_jad_healer_spawn_offsets_match_wave_67_reference(void) { state.npcs[0].hp = 100; state.npcs[0].max_hp = 300; - inf_rebuild_entity_collision_flags(&state); inf_jad_check_healers(&state, 0); int healers = 0; @@ -2572,7 +2579,6 @@ static void test_jad_healer_spawn_offsets_match_zuk_reference(void) { state.npcs[0].hp = 100; state.npcs[0].max_hp = 300; - inf_rebuild_entity_collision_flags(&state); inf_jad_check_healers(&state, 0); int healers = 0; @@ -2619,7 +2625,6 @@ static void test_zuk_jad_healer_spawn_falls_back_to_passable_arena_tiles(void) { state.npcs[0].hp = 100; state.npcs[0].max_hp = 300; - inf_rebuild_entity_collision_flags(&state); inf_jad_check_healers(&state, 0); int healers = 0; @@ -3167,14 +3172,14 @@ static void test_pending_hit_obs_timer_prefers_prayer_window(void) { static void test_blob_attacks_player_on_six_tick_cadence(void) { printf("--- blob attacks the player on a 6-tick cadence ---\n"); - InfernoState state = make_test_state(10, 10); + InfernoState state = make_test_state(20, 20); state.player.current_defence = 99; state.player.current_magic = 99; state.player.prayer = PRAYER_NONE; state.weapon_set = INF_GEAR_MAGE; state.npcs[0] = make_test_npc( - INF_NPC_BLOB, 20, 10, INF_NPC_STATS[INF_NPC_BLOB].size); + INF_NPC_BLOB, 30, 20, INF_NPC_STATS[INF_NPC_BLOB].size); state.npcs[0].active = 1; state.npcs[0].hp = INF_NPC_STATS[INF_NPC_BLOB].hp; @@ -3200,7 +3205,7 @@ static void test_blob_attacks_player_on_six_tick_cadence(void) { static void test_jad_has_no_pre_fire_style_preview(void) { printf("--- jad has no pre-fire style preview ---\n"); - InfernoState state = make_test_state(10, 10); + InfernoState state = make_test_state(20, 20); state.player.current_defence = 99; state.player.current_magic = 99; state.player.prayer = PRAYER_NONE; @@ -3208,7 +3213,7 @@ static void test_jad_has_no_pre_fire_style_preview(void) { state.wave = 66; state.npcs[0] = make_test_npc( - INF_NPC_JAD, 20, 10, INF_NPC_STATS[INF_NPC_JAD].size); + INF_NPC_JAD, 30, 20, INF_NPC_STATS[INF_NPC_JAD].size); state.npcs[0].active = 1; state.npcs[0].attack_timer = 2; @@ -3695,6 +3700,10 @@ static void init_threat_obs_state( int player_y ) { inf_build_npc_stats(); + if (player_x == 10 && player_y == 10) { + player_x += 10; + player_y += 10; + } *state = make_test_state(player_x, player_y); state->player.entity_type = ENTITY_PLAYER; state->player.base_hitpoints = 99; @@ -3732,9 +3741,16 @@ static void add_threat_obs_npc( int x, int y ) { - state->npcs[slot] = make_test_npc(type, x, y, INF_NPC_STATS[type].size); + if (state->player.x == 20 && state->player.y == 20 && + x < 20 && y < 20) { + x += 10; + y += 10; + } + state->npcs[slot] = + make_test_npc(type, x, y, INF_NPC_STATS[type].size); state->npcs[slot].active = 1; - state->npcs[slot].hp = state->npcs[slot].max_hp = INF_NPC_STATS[type].hp; + state->npcs[slot].hp = + state->npcs[slot].max_hp = INF_NPC_STATS[type].hp; state->npcs[slot].attack_timer = 1; } @@ -3857,7 +3873,6 @@ static void init_step_out_forecast_stack_state(InfernoState* state, int player_x reset_test_context(); inf_build_npc_stats(); memset(state, 0, sizeof(*state)); - memset(state->npc_los_cache, -1, sizeof(state->npc_los_cache)); state->rng_state = 20260515u; state->wave = 59; state->player.entity_type = ENTITY_PLAYER; @@ -3880,8 +3895,6 @@ static void init_step_out_forecast_stack_state(InfernoState* state, int player_x state->pillars[p].hp = INF_PILLAR_HP; state->pillars[p].active = 1; } - inf_rebuild_los(state); - inf_rebuild_player_collision_flags(state); } static void add_step_out_forecast_npc( @@ -3898,8 +3911,6 @@ static void clear_step_out_forecast_pillars(InfernoState* state) { state->pillars[p].active = 0; state->pillars[p].hp = 0; } - inf_rebuild_los(state); - inf_rebuild_entity_collision_flags(state); } static void assert_step_out_ranger_then_mager( @@ -3974,9 +3985,7 @@ static void assert_inferno_npc_sw_origin_step( state.pillars[p].active = p == pillar_idx; state.pillars[p].hp = p == pillar_idx ? INF_PILLAR_HP : 0; } - inf_rebuild_los(&state); add_step_out_forecast_npc(&state, 0, type, npc_x, npc_y, 0); - inf_rebuild_entity_collision_flags(&state); ASSERT_INT_EQ("starting NPC has no LOS", inf_npc_has_los_direct(&state, 0), 0); @@ -4028,9 +4037,7 @@ static void assert_inferno_jal_npc_uses_edge_clearance( state.pillars[p].active = p == 0; state.pillars[p].hp = p == 0 ? INF_PILLAR_HP : 0; } - inf_rebuild_los(&state); add_step_out_forecast_npc(&state, 0, type, npc_x, npc_y, 0); - inf_rebuild_entity_collision_flags(&state); ASSERT_INT_EQ("starting Jal NPC has no LOS", inf_npc_has_los(&state, 0), 0); @@ -4119,7 +4126,6 @@ static void test_fast_step_out_forecast_immediate_static_threats(void) { clear_step_out_forecast_pillars(&state); add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 1); add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 2); - inf_rebuild_entity_collision_flags(&state); InfStepOutForecast exact; InfStepOutForecast fast; @@ -4146,7 +4152,6 @@ static void test_fast_step_out_forecast_blob_scan_and_melee_fallback(void) { add_step_out_forecast_npc(&blob_state, 0, INF_NPC_BLOB, 29, 30, 1); blob_state.npcs[0].blob_scanned_prayer = -1; blob_state.npcs[0].had_los_last_tick = 0; - inf_rebuild_entity_collision_flags(&blob_state); InfStepOutForecast blob_forecast; inf_build_step_out_forecast_fast_static_ctx( @@ -4155,10 +4160,9 @@ static void test_fast_step_out_forecast_blob_scan_and_melee_fallback(void) { blob_forecast.actions[0].ticks[0].blob_scan_count, 1); InfernoState melee_state; - init_step_out_forecast_stack_state(&melee_state, 10, 10); + init_step_out_forecast_stack_state(&melee_state, 20, 20); clear_step_out_forecast_pillars(&melee_state); - add_step_out_forecast_npc(&melee_state, 0, INF_NPC_MAGER, 11, 10, 1); - inf_rebuild_entity_collision_flags(&melee_state); + add_step_out_forecast_npc(&melee_state, 0, INF_NPC_MAGER, 21, 20, 1); InfStepOutForecast melee_forecast; inf_build_step_out_forecast_fast_static_ctx( @@ -4178,7 +4182,6 @@ static void test_fast_step_out_forecast_does_not_mutate_state(void) { add_step_out_forecast_npc(&state, 0, INF_NPC_BLOB, 29, 30, 1); state.npcs[0].blob_scanned_prayer = -1; state.npcs[0].had_los_last_tick = 0; - inf_rebuild_entity_collision_flags(&state); InfernoState before = state; InfStepOutForecast forecast; @@ -4193,15 +4196,6 @@ static void test_fast_step_out_forecast_does_not_mutate_state(void) { state.npcs[0].attack_timer, before.npcs[0].attack_timer); ASSERT_INT_EQ("fast forecast preserves blob scan state", state.npcs[0].blob_scanned_prayer, before.npcs[0].blob_scanned_prayer); - ASSERT_INT_EQ("fast forecast preserves LOS cache", - memcmp(state.npc_los_cache, before.npc_los_cache, - sizeof(state.npc_los_cache)), 0); - ASSERT_INT_EQ("fast forecast preserves player collision flags", - memcmp(state.player_collision_flags, before.player_collision_flags, - sizeof(state.player_collision_flags)), 0); - ASSERT_INT_EQ("fast forecast preserves NPC collision flags", - memcmp(state.npc_collision_flags, before.npc_collision_flags, - sizeof(state.npc_collision_flags)), 0); } static void test_readonly_step_out_forecast_matches_movement_head_destinations(void) { @@ -4243,7 +4237,6 @@ static void test_readonly_step_out_forecast_does_not_mutate_state(void) { add_step_out_forecast_npc(&state, 2, INF_NPC_BLOB, 29, 32, 1); state.npcs[2].blob_scanned_prayer = -1; state.npcs[2].had_los_last_tick = 0; - inf_rebuild_entity_collision_flags(&state); InfernoState before = state; InfStepOutForecast forecast; @@ -4262,15 +4255,6 @@ static void test_readonly_step_out_forecast_does_not_mutate_state(void) { state.npcs[0].attack_timer, before.npcs[0].attack_timer); ASSERT_INT_EQ("readonly forecast preserves blob scan state", state.npcs[2].blob_scanned_prayer, before.npcs[2].blob_scanned_prayer); - ASSERT_INT_EQ("readonly forecast preserves LOS cache", - memcmp(state.npc_los_cache, before.npc_los_cache, - sizeof(state.npc_los_cache)), 0); - ASSERT_INT_EQ("readonly forecast preserves player collision flags", - memcmp(state.player_collision_flags, before.player_collision_flags, - sizeof(state.player_collision_flags)), 0); - ASSERT_INT_EQ("readonly forecast preserves NPC collision flags", - memcmp(state.npc_collision_flags, before.npc_collision_flags, - sizeof(state.npc_collision_flags)), 0); } static void assert_readonly_step_out_matches_exact_action( @@ -4341,7 +4325,6 @@ static void test_readonly_step_out_forecast_stun_countdown(void) { clear_step_out_forecast_pillars(&state); add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 20, 25, 1); state.npcs[0].stun_timer = 2; - inf_rebuild_entity_collision_flags(&state); InfStepOutForecast forecast; inf_build_step_out_forecast_fast_readonly_ctx( @@ -4363,7 +4346,6 @@ static void test_readonly_step_out_forecast_frozen_can_attack(void) { clear_step_out_forecast_pillars(&state); add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 20, 25, 1); state.npcs[0].frozen_ticks = 4; - inf_rebuild_entity_collision_flags(&state); InfStepOutForecast forecast; inf_build_step_out_forecast_fast_readonly_ctx( @@ -4383,7 +4365,6 @@ static void test_readonly_step_out_forecast_blob_scanned_fire(void) { state.npcs[0].blob_scanned_prayer = PRAYER_PROTECT_MAGIC; state.npcs[0].attack_style = ATTACK_STYLE_RANGED; state.npcs[0].had_los_last_tick = 1; - inf_rebuild_entity_collision_flags(&state); InfStepOutForecast forecast; inf_build_step_out_forecast_fast_readonly_ctx( @@ -4402,7 +4383,6 @@ static void test_readonly_step_out_forecast_under_player_overlap_is_danger(void) init_step_out_forecast_stack_state(&state, 20, 20); clear_step_out_forecast_pillars(&state); add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 20, 20, 1); - inf_rebuild_entity_collision_flags(&state); uint32_t rng_before = state.rng_state; InfStepOutForecast forecast; @@ -4423,7 +4403,6 @@ static void test_readonly_step_out_forecast_invalid_movement_zeroes_payload(void clear_step_out_forecast_pillars(&state); add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, INF_ARENA_MIN_X + 3, INF_ARENA_MIN_Y + 3, 1); - inf_rebuild_entity_collision_flags(&state); InfStepOutForecast forecast; inf_build_step_out_forecast_fast_readonly_ctx( @@ -4488,9 +4467,10 @@ static void test_step_out_forecast_inactive_pillar_does_not_create_cover(void) { init_step_out_forecast_stack_state(&state, 29, 39); state.pillars[2].active = 0; state.pillars[2].hp = 0; - inf_rebuild_los(&state); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 0); - add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); + add_step_out_forecast_npc( + &state, 0, INF_NPC_RANGER, 24, 31, 0); + add_step_out_forecast_npc( + &state, 1, INF_NPC_MAGER, 29, 30, 0); InfStepOutForecast forecast; inf_build_step_out_forecast(&state, &forecast); @@ -4511,8 +4491,10 @@ static void test_step_out_same_tick_ranger_mager_event_logs(void) { InfernoState state; init_step_out_forecast_stack_state(&state, 14, 35); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 5, 39, 0); - add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 4, 34, 0); + add_step_out_forecast_npc( + &state, 0, INF_NPC_RANGER, 5, 39, 0); + add_step_out_forecast_npc( + &state, 1, INF_NPC_MAGER, 4, 34, 0); int actions[INF_NUM_ACTION_HEADS] = {0}; actions[INF_HEAD_MOVE] = 13; @@ -4520,7 +4502,8 @@ static void test_step_out_same_tick_ranger_mager_event_logs(void) { ASSERT_INT_EQ("step-out tick moved the player", state.tick_scratch.player_moved, 1); - ASSERT_INT_EQ("movement tick does not count attacks before NPCs see new tile", + ASSERT_INT_EQ( + "movement tick does not count attacks before NPCs see new tile", state.total_step_out_ranger_mager_same_tick_attacks, 0); int noop[INF_NUM_ACTION_HEADS] = {0}; @@ -4626,7 +4609,6 @@ static void test_zuk_ready_countdown_holds_npcs_then_releases(void) { static void init_zuk_timing_state(InfernoState* state) { reset_test_context(); memset(state, 0, sizeof(*state)); - memset(state->npc_los_cache, -1, sizeof(state->npc_los_cache)); state->rng_state = 7; state->wave = 68; state->player.entity_type = ENTITY_PLAYER; @@ -4782,7 +4764,6 @@ static void assert_human_blowpipe_zuk_chase_endpoint( endpoint_state.player.attack_timer = 3; endpoint_state.npcs[0].stun_timer = 64; endpoint_state.npcs[0].attack_timer = 64; - inf_rebuild_entity_collision_flags(&endpoint_state); ASSERT_INT_EQ("Zuk blowpipe endpoint starts out of range", inf_player_can_attack_npc_from_current_tile(&endpoint_state, 0), 0); @@ -4828,7 +4809,6 @@ static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { edge_state.player.attack_timer = 3; edge_state.npcs[0].stun_timer = 64; edge_state.npcs[0].attack_timer = 64; - inf_rebuild_entity_collision_flags(&edge_state); ASSERT_INT_EQ("north-row Zuk click starts out of blowpipe range", inf_player_can_attack_npc_from_current_tile(&edge_state, 0), 0); @@ -4856,7 +4836,6 @@ static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { state.player.attack_timer = 3; state.npcs[0].stun_timer = 64; state.npcs[0].attack_timer = 64; - inf_rebuild_entity_collision_flags(&state); ASSERT_INT_EQ("human Zuk click starts out of blowpipe range", inf_player_can_attack_npc_from_current_tile(&state, 0), 0); @@ -4911,7 +4890,6 @@ static void test_zuk_healer_blowpipe_target_chases_out_of_range(void) { state.npcs[2].active = 1; state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; state.npcs[2].aggro_target = 0; - inf_rebuild_entity_collision_flags(&state); float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); @@ -5785,10 +5763,10 @@ static void test_npc_target_projectile_delays_match_reference(void) { static void test_npc_player_projectile_delays_use_reference_options(void) { printf("--- npc player projectile delays use reference options ---\n"); - InfernoState state = make_test_state(10, 10); + InfernoState state = make_test_state(20, 20); InfNPC* ranger = &state.npcs[0]; *ranger = make_test_npc( - INF_NPC_RANGER, 16, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + INF_NPC_RANGER, 26, 20, INF_NPC_STATS[INF_NPC_RANGER].size); ranger->active = 1; ranger->attack_timer = 0; ranger->attack_style = ATTACK_STYLE_RANGED; @@ -7619,9 +7597,6 @@ static void test_inferno_refresh_after_state_load_rebuilds_derived_state(void) { inf_reset_ctx((EncounterState*)&state, (EncounterContext*)&ctx, 42u); memset(state.current_obs_slots, -1, sizeof(state.current_obs_slots)); - memset(state.npc_collision_flags, 99, sizeof(state.npc_collision_flags)); - memset(state.player_collision_flags, 99, sizeof(state.player_collision_flags)); - memset(state.npc_los_cache, 7, sizeof(state.npc_los_cache)); state.loadout_stats[INF_GEAR_LONG_RANGE].max_hit = -1; inf_refresh_after_state_load(&state, &ctx); @@ -7635,12 +7610,6 @@ static void test_inferno_refresh_after_state_load_rebuilds_derived_state(void) { ASSERT_INT_EQ("refresh repopulates visible obs slots", visible_count > 0, 1); ASSERT_INT_EQ("refresh recomputes long-range max hit", state.loadout_stats[INF_GEAR_LONG_RANGE].max_hit > 0, 1); - ASSERT_INT_EQ("refresh clears npc collision scratch", - state.npc_collision_flags[0][0], 0); - ASSERT_INT_EQ("refresh clears player collision scratch", - state.player_collision_flags[0][0], 0); - ASSERT_INT_EQ("refresh invalidates LOS cache", - state.npc_los_cache[0], -1); } static void test_inferno_healer_transition_stats_track_episode_progress(void) { @@ -9287,9 +9256,311 @@ static void test_compact_transient_inventory_equipment_semantics(void) { } +static int reference_inferno_pillar_footprint_blocked( + const InfernoState* state, + int x, + int y, + int size +) { + for (int pillar_idx = 0; pillar_idx < INF_NUM_PILLARS; pillar_idx++) { + const InfPillar* pillar = &state->pillars[pillar_idx]; + if (!pillar->active) continue; + if (los_aabb_overlap( + x, y, size, + pillar->x, pillar->y, INF_PILLAR_SIZE)) + return 1; + } + return 0; +} + +static int reference_inferno_footprint_blocked( + const InfernoState* state, + int x, + int y, + int size +) { + if (x < INF_ARENA_MIN_X || y < INF_ARENA_MIN_Y || + x + size - 1 > INF_ARENA_MAX_X || + y + size - 1 > INF_ARENA_MAX_Y) + return 1; + return reference_inferno_pillar_footprint_blocked( + state, x, y, size); +} + +static int reference_inferno_los_clear( + const InfernoState* state, + int actor_x, + int actor_y, + int actor_size, + int target_x, + int target_y, + int target_size, + int attack_range +) { + + LOSBlocker blockers[INF_NUM_PILLARS]; + int blocker_count = 0; + for (int pillar_idx = 0; pillar_idx < INF_NUM_PILLARS; pillar_idx++) { + const InfPillar* pillar = &state->pillars[pillar_idx]; + if (!pillar->active) continue; + blockers[blocker_count++] = (LOSBlocker){ + .x = pillar->x, + .y = pillar->y, + .size = INF_PILLAR_SIZE, + .los_mask = LOS_FULL_MASK, + }; + } + return entity_has_line_of_sight( + blockers, + blocker_count, + actor_x, + actor_y, + actor_size, + target_x, + target_y, + target_size, + attack_range); +} + +static void set_inferno_pillar_phase( + InfernoState* state, + int phase +) { + for (int pillar_idx = 0; pillar_idx < INF_NUM_PILLARS; pillar_idx++) { + state->pillars[pillar_idx].x = INF_PILLAR_POS[pillar_idx][0]; + state->pillars[pillar_idx].y = INF_PILLAR_POS[pillar_idx][1]; + state->pillars[pillar_idx].active = + (phase & (1 << pillar_idx)) != 0; + state->pillars[pillar_idx].hp = + state->pillars[pillar_idx].active ? INF_PILLAR_HP : 0; + } +} + +static void test_inferno_topology_geometry_parity(void) { + printf("--- inferno topology geometry parity ---\n"); + + InfernoState state = make_test_state(20, 20); + const InfernoContext* ctx = inf_legacy_context(); + int footprint_checks = 0; + int los_checks = 0; + const int target_sizes[] = {1, INF_PILLAR_SIZE, 5}; + const int attack_ranges[] = {1, 4, 10, 0}; + + for (int phase = 0; phase < (1 << INF_NUM_PILLARS); phase++) { + set_inferno_pillar_phase(&state, phase); + for (int size = 1; + size <= ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE; + size++) { + for (int x = INF_ARENA_MIN_X - size; + x <= INF_ARENA_MAX_X + 1; + x++) { + for (int y = INF_ARENA_MIN_Y - size; + y <= INF_ARENA_MAX_Y + 1; + y++) { + int expected = reference_inferno_footprint_blocked( + &state, x, y, size); + int actual = inf_footprint_blocked_ctx( + &state, ctx, x, y, size); + if (expected != actual) { + printf( + " FAIL: footprint phase=%d anchor=(%d,%d) " + "size=%d expected=%d actual=%d\n", + phase, x, y, size, expected, actual); + tests_failed++; + tests_run++; + return; + } + footprint_checks++; + } + } + } + + for (int actor_size = 1; + actor_size <= ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE; + actor_size++) { + for (int actor_x = INF_ARENA_MIN_X; + actor_x + actor_size - 1 <= INF_ARENA_MAX_X; + actor_x += 3) { + for (int actor_y = INF_ARENA_MIN_Y; + actor_y + actor_size - 1 <= INF_ARENA_MAX_Y; + actor_y += 3) { + for (size_t target_size_idx = 0; + target_size_idx < + sizeof(target_sizes) / + sizeof(target_sizes[0]); + target_size_idx++) { + int target_size = target_sizes[target_size_idx]; + for (int target_x = INF_ARENA_MIN_X; + target_x + target_size - 1 <= + INF_ARENA_MAX_X; + target_x += 4) { + for (int target_y = INF_ARENA_MIN_Y; + target_y + target_size - 1 <= + INF_ARENA_MAX_Y; + target_y += 4) { + for (size_t range_idx = 0; + range_idx < + sizeof(attack_ranges) / + sizeof(attack_ranges[0]); + range_idx++) { + int attack_range = + attack_ranges[range_idx]; + int expected = + reference_inferno_los_clear( + &state, + actor_x, + actor_y, + actor_size, + target_x, + target_y, + target_size, + attack_range); + int actual = inf_los_clear_ctx( + &state, + ctx, + actor_x, + actor_y, + actor_size, + target_x, + target_y, + target_size, + attack_range); + if (expected != actual) { + printf( + " FAIL: LOS phase=%d " + "actor=(%d,%d,%d) " + "target=(%d,%d,%d) range=%d " + "expected=%d actual=%d\n", + phase, + actor_x, + actor_y, + actor_size, + target_x, + target_y, + target_size, + attack_range, + expected, + actual); + tests_failed++; + tests_run++; + return; + } + los_checks++; + } + } + } + } + } + } + } + } + + ASSERT_INT_EQ( + "all topology footprint parity cases checked", + footprint_checks > 0, + 1); + ASSERT_INT_EQ( + "all topology direct, area, and large-footprint LOS cases checked", + los_checks > 0, + 1); +} + +static void test_inferno_topology_observation_mask_identity(void) { + printf("--- inferno topology observation and mask identity ---\n"); + + InfernoState state = make_test_state(20, 20); + InfernoContext* ctx = inf_legacy_context(); + static const InfNPCType types[] = { + INF_NPC_BAT, + INF_NPC_BLOB, + INF_NPC_MELEER, + INF_NPC_RANGER, + INF_NPC_MAGER, + INF_NPC_JAD, + INF_NPC_ZUK, + }; + static const int positions[][2] = { + {20, 16}, + {17, 23}, + {24, 24}, + {36, 34}, + {31, 33}, + {14, 38}, + {12, 45}, + }; + float observation[INF_NUM_OBS]; + float mask[INF_ACTION_MASK_SIZE]; + + for (int phase = 0; phase < (1 << INF_NUM_PILLARS); phase++) { + memset(state.npcs, 0, sizeof(state.npcs)); + set_inferno_pillar_phase(&state, phase); + for (size_t npc_idx = 0; + npc_idx < sizeof(types) / sizeof(types[0]); + npc_idx++) { + state.npcs[npc_idx] = make_test_npc( + types[npc_idx], + positions[npc_idx][0], + positions[npc_idx][1], + INF_NPC_STATS[types[npc_idx]].size); + state.npcs[npc_idx].active = 1; + state.npcs[npc_idx].aggro_target = -1; + } + inf_refresh_current_obs_slots_ctx(&state, ctx); + inf_write_obs_ctx( + (EncounterState*)&state, + (EncounterContext*)ctx, + observation); + inf_write_mask_ctx( + (EncounterState*)&state, + (EncounterContext*)ctx, + mask); + + for (int slot_idx = 0; slot_idx < INF_OBS_NPCS; slot_idx++) { + int npc_idx = state.current_obs_slots[slot_idx]; + if (npc_idx < 0) continue; + const InfNPC* npc = &state.npcs[npc_idx]; + int expected = reference_inferno_los_clear( + &state, + npc->x, + npc->y, + npc->size, + state.player.x, + state.player.y, + 1, + INF_NPC_STATS[npc->type].attack_range); + int obs_offset = + INF_OBS_AFTER_PILLARS + + slot_idx * INF_NPC_SLOT_FEATURES + 6; + ASSERT_FLOAT_NEAR( + "NPC observation LOS bit matches independent reference", + observation[obs_offset], + (float)expected, + 0.0f); + } + + int movement_offset = 0; + for (int head = 0; head < INF_HEAD_MOVE; head++) + movement_offset += INF_ACTION_DIMS[head]; + for (int action = 0; action < ENCOUNTER_MOVE_ACTIONS; action++) { + int x = state.player.x + ENCOUNTER_MOVE_TARGET_DX[action]; + int y = state.player.y + ENCOUNTER_MOVE_TARGET_DY[action]; + int expected = + !reference_inferno_footprint_blocked(&state, x, y, 1); + ASSERT_FLOAT_NEAR( + "movement mask bit matches independent reference", + mask[movement_offset + action], + (float)expected, + 0.0f); + } + } +} + + int main(void) { inf_build_npc_stats(); inf_finalize_route_topology(inf_legacy_context()); + test_inferno_topology_geometry_parity(); + test_inferno_topology_observation_mask_identity(); test_compact_observation_layout_contract(); test_compact_player_and_pillar_observation_semantics(); test_compact_npc_observation_semantics(); @@ -9308,9 +9579,9 @@ int main(void) { test_style_choice_sampling(); test_tagged_jad_healer_melee_geometry(); test_overlap_shuffle_hold_after_recent_target_click(); - test_overlap_shuffle_respects_npc_collision_flags(); + test_overlap_shuffle_respects_npc_occupancy(); test_large_npc_overlap_shuffle_can_partially_unclip(); - test_player_movement_ignores_npc_collision_flags(); + test_player_movement_ignores_npc_occupancy(); test_tagged_jad_healer_stops_at_melee_contact(); test_tagged_jad_healers_queue_behind_front_healer(); test_meleer_dig_can_stack_without_losing_collision_flag(); diff --git a/ocean/osrs/tests/test_inferno_forecast_exact.c b/ocean/osrs/tests/test_inferno_forecast_exact.c index 20ac8ac64a..b41f7c5a59 100644 --- a/ocean/osrs/tests/test_inferno_forecast_exact.c +++ b/ocean/osrs/tests/test_inferno_forecast_exact.c @@ -125,7 +125,7 @@ static void exact_writer_close(InfExactWriter* writer) { writer->file = NULL; } -static void exact_zero_serialized_route_storage(InfernoState* state) { +static void exact_canonicalize_state(InfernoState* state) { #ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES osrs_interaction_zero_serialized_route_padding(&state->player.interaction); osrs_interaction_zero_serialized_route_padding(&state->interaction); @@ -133,6 +133,18 @@ static void exact_zero_serialized_route_storage(InfernoState* state) { memset(&state->player.interaction.route, 0, sizeof(state->player.interaction.route)); memset(&state->interaction.route, 0, sizeof(state->interaction.route)); #endif + memset( + state->reserved_topology_pillar_storage, + 0, + sizeof(state->reserved_topology_pillar_storage)); + memset( + state->reserved_topology_npc_storage, + 0, + sizeof(state->reserved_topology_npc_storage)); + memset( + state->reserved_topology_player_footprint_storage, + 0, + sizeof(state->reserved_topology_player_footprint_storage)); } static void exact_capture( @@ -149,7 +161,7 @@ static void exact_capture( inf_build_step_out_forecast_ctx(s, ctx, &forecast); inf_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); InfernoState canonical_state = *s; - exact_zero_serialized_route_storage(&canonical_state); + exact_canonicalize_state(&canonical_state); InfExactRecordHeader record = {0}; record.scenario_id = scenario_id; @@ -220,7 +232,8 @@ static int exact_inferno_route_blocked( int size ) { InfExactRouteContext* route_ctx = (InfExactRouteContext*)data; - return inf_blocked_by_pillar(route_ctx->state, x, y, size); + return inf_footprint_blocked_ctx( + route_ctx->state, route_ctx->context, x, y, size); } static int exact_inferno_can_attack( @@ -378,8 +391,8 @@ static int exact_compare_files(const char* expected_path, const char* actual_pat fclose(actual); return 1; } - exact_zero_serialized_route_storage(&expected_state); - exact_zero_serialized_route_storage(&actual_state); + exact_canonicalize_state(&expected_state); + exact_canonicalize_state(&actual_state); expected_record.state_hash = exact_hash_bytes(&expected_state, sizeof(expected_state)); actual_record.state_hash = diff --git a/ocean/osrs/tests/test_inferno_golden.c b/ocean/osrs/tests/test_inferno_golden.c index a49b466cee..76bf25f44e 100644 --- a/ocean/osrs/tests/test_inferno_golden.c +++ b/ocean/osrs/tests/test_inferno_golden.c @@ -87,11 +87,22 @@ static uint64_t hash_semantic_mask( } static TraceHashes run_episode(int start_wave, uint32_t seed, int max_ticks) { - EncounterState* state = inf_create(); - inf_put_int(state, "start_wave", start_wave); - inf_reset(state, seed); - - InfernoState* s = (InfernoState*)state; + InfernoContext context; + InfernoState state_storage; + inf_init_context_typed(&context); + inf_init_state_typed(&state_storage, &context); + inf_put_int_ctx( + (EncounterState*)&state_storage, + (EncounterContext*)&context, + "start_wave", + start_wave); + inf_finalize_route_topology(&context); + inf_reset_ctx( + (EncounterState*)&state_storage, + (EncounterContext*)&context, + seed); + + InfernoState* s = &state_storage; static float obs[INF_NUM_OBS]; static float mask[INF_ACTION_MASK_SIZE]; int actions[INF_NUM_ACTION_HEADS]; @@ -106,7 +117,7 @@ static TraceHashes run_episode(int start_wave, uint32_t seed, int max_ticks) { }; for (int t = 0; t < max_ticks; t++) { - inf_refresh_current_obs_slots(s); + inf_refresh_current_obs_slots_ctx(s, &context); for (int head = 0; head < INF_NUM_ACTION_HEADS; head++) { uint64_t random_value = splitmix64(&arng); if (head != INF_HEAD_TARGET) { @@ -119,9 +130,12 @@ static TraceHashes run_episode(int start_wave, uint32_t seed, int max_ticks) { actions[head] = slot >= 0 ? slot + 1 : 0; } - inf_step(state, actions); - inf_write_obs(state, obs); - inf_write_mask(state, mask); + inf_step_ctx( + (EncounterState*)s, (EncounterContext*)&context, actions); + inf_write_obs_ctx( + (EncounterState*)s, (EncounterContext*)&context, obs); + inf_write_mask_ctx( + (EncounterState*)s, (EncounterContext*)&context, mask); for (int npc_idx = TRACE_NPC_SLOTS; npc_idx < INF_MAX_NPCS; npc_idx++) { if (s->npcs[npc_idx].active) { @@ -141,7 +155,7 @@ static TraceHashes run_episode(int start_wave, uint32_t seed, int max_ticks) { if (s->episode_over) break; } - inf_destroy(state); + inf_destroy_context((EncounterContext*)&context); return hashes; } diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c index b82f2abd8a..c2adfd9bae 100644 --- a/ocean/osrs/tests/test_osrs_player_step.c +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -814,6 +814,8 @@ static void test_arena_topology_bounds_collision_los_and_revision(void) { size_t allocations_before_build = topology_allocation_count; EncounterArenaTopologyBuildSpec spec = topology_test_spec(&geometry, 41); EncounterArenaTopology* topology = encounter_arena_topology_build(&spec); + CHECK("topology detects flagged static LOS", + topology->static_los_mode == ENCOUNTER_ARENA_TOPOLOGY_LOS_FLAGGED); CHECK("topology build uses one explicitly owned allocation", topology_allocation_count == allocations_before_build + 1); @@ -909,6 +911,39 @@ static void test_arena_topology_bounds_collision_los_and_revision(void) { memcmp(snapshot, topology, sizeof(*topology)) == 0); free(topology); + TopologyTestGeometry open_geometry = { + .origin_x = 10, + .origin_y = 20, + .width = 8, + .height = 8, + }; + EncounterArenaTopologyBuildSpec open_spec = + topology_test_spec(&open_geometry, 42); + EncounterArenaTopology* open_topology = + encounter_arena_topology_build(&open_spec); + encounter_arena_topology_finalize(open_topology); + CHECK("topology detects open static LOS", + open_topology->static_los_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN); + for (int actor_size = 1; actor_size <= 3; actor_size++) { + for (int target_size = 1; target_size <= 3; target_size++) { + for (int range = 1; range <= 10; range++) { + int expected = entity_has_line_of_sight( + NULL, 0, + 11, 21, actor_size, + 15, 24, target_size, + range); + int actual = encounter_arena_topology_los_clear( + open_topology, + 11, 21, actor_size, + 15, 24, target_size, + range); + CHECK("open topology LOS matches open geometry reference", + actual == expected); + } + } + } + free(open_topology); topology_stale_revision_target = NULL; } From 1e59a5350e720bea9b11ced98764fb607a3767e0 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 04:57:25 +0300 Subject: [PATCH 18/50] Fix Inferno topology LOS ownership --- .../inferno/encounter_inferno_combat.inc | 15 +--- .../inferno/encounter_inferno_helpers.inc | 32 +------ .../inferno/encounter_inferno_lab.inc | 72 +++++++++++---- .../inferno/encounter_inferno_movement.inc | 16 ---- .../encounter_inferno_render_snapshot.inc | 23 ++++- ocean/osrs/osrs_render.h | 22 +++-- ocean/osrs/tests/bench_inferno_forecast.c | 50 ++++++----- .../test_encounter_route_topology_setup.c | 44 ++++++++- ocean/osrs/tests/test_inferno_attack_styles.c | 89 +++++++++++++++---- 9 files changed, 234 insertions(+), 129 deletions(-) diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc index d28231bde1..246a52c0a1 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc @@ -231,13 +231,6 @@ static int inf_player_can_attack_npc_from_current_tile_ctx( ls->attack_range); } -static int inf_player_can_attack_npc_from_current_tile( - const InfernoState* s, - int npc_idx -) { - return inf_player_can_attack_npc_from_current_tile_ctx( - s, inf_legacy_context(), npc_idx); -} static int inf_player_has_los_to_npc_from_current_tile_ctx( const InfernoState* s, @@ -263,13 +256,6 @@ static int inf_player_has_los_to_npc_from_current_tile_ctx( 0); } -static int inf_player_has_los_to_npc_from_current_tile( - const InfernoState* s, - int npc_idx -) { - return inf_player_has_los_to_npc_from_current_tile_ctx( - s, inf_legacy_context(), npc_idx); -} static int inf_player_can_phantom_barrage_npc( @@ -518,6 +504,7 @@ static void inf_npc_attack_ctx(InfernoState* s, InfernoContext* ctx, int idx) { s->pillars[p].hp -= dmg; if (s->pillars[p].hp <= 0) { s->pillars[p].active = 0; + inf_reset_npc_player_los_frame(ctx); s->tick_scratch.pillar_lost = p; for (int n = 0; n < INF_MAX_NPCS; n++) { if (!s->npcs[n].active) continue; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc index bd3c88bcae..1e2d0e7025 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc @@ -225,16 +225,6 @@ static int inf_npc_has_los_to_area_ctx( stats->attack_range); } -static int inf_npc_has_los_to_area( - const InfernoState* s, - const InfNPC* npc, - int target_x, - int target_y, - int target_size -) { - return inf_npc_has_los_to_area_ctx( - s, inf_legacy_context(), npc, target_x, target_y, target_size); -} static int inf_npc_has_los_to_tile_ctx( const InfernoState* s, @@ -247,15 +237,6 @@ static int inf_npc_has_los_to_tile_ctx( s, ctx, npc, target_x, target_y, 1); } -static int inf_npc_has_los_to_tile( - const InfernoState* s, - const InfNPC* npc, - int target_x, - int target_y -) { - return inf_npc_has_los_to_tile_ctx( - s, inf_legacy_context(), npc, target_x, target_y); -} static int inf_npc_has_los_direct_ctx( const InfernoState* s, @@ -268,10 +249,6 @@ static int inf_npc_has_los_direct_ctx( s, ctx, npc, target.x, target.y, target.size); } -static int inf_npc_has_los_direct(const InfernoState* s, int npc_idx) { - return inf_npc_has_los_direct_ctx( - s, inf_legacy_context(), npc_idx); -} static void inf_reset_npc_player_los_frame(InfernoContext* ctx) { memset(ctx->npc_player_los_frame, -1, sizeof(ctx->npc_player_los_frame)); @@ -295,9 +272,6 @@ static int inf_npc_has_los_ctx( return sampled >= 0 ? sampled : inf_npc_has_los_direct_ctx(s, ctx, npc_idx); } -static int inf_npc_has_los(const InfernoState* s, int npc_idx) { - return inf_npc_has_los_ctx(s, inf_legacy_context(), npc_idx); -} static inline int inf_is_final_wave(const InfernoState* s) { return s->wave == INF_NUM_WAVES - 1; @@ -854,8 +828,10 @@ static uint32_t inf_route_topology_flags(void* data, int x, int y) { } static uint32_t inf_route_topology_los_flags(void* data, int x, int y) { - return inf_topology_collision_flags( - (const InfRouteTopologyBuildContext*)data, x, y); + (void)data; + (void)x; + (void)y; + return 0; } static void inf_finalize_route_topology(InfernoContext* ctx) { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc index 581239641b..eb1b4406bf 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc @@ -124,10 +124,11 @@ static void inf_lab_require_type(int type) { static void inf_lab_require_player_tile( InfernoState* s, + const InfernoContext* ctx, int x, int y ) { - if (inf_footprint_blocked(s, x, y, 1)) + if (inf_footprint_blocked_ctx(s, ctx, x, y, 1)) inf_lab_abort("player tile is blocked: (%d,%d)", x, y); } @@ -176,7 +177,11 @@ static void inf_lab_clear_npcs(InfernoState* s) { inf_lab_refresh_geometry(s); } -static void inf_lab_apply_command(InfernoState* s, const InfernoLabCommand* cmd) { +static void inf_lab_apply_command_ctx( + InfernoState* s, + InfernoContext* ctx, + const InfernoLabCommand* cmd +) { if (!s || !cmd) inf_lab_abort("null command"); switch (cmd->kind) { @@ -184,11 +189,12 @@ static void inf_lab_apply_command(InfernoState* s, const InfernoLabCommand* cmd) return; case INF_LAB_COMMAND_RESET: - inf_reset((EncounterState*)s, cmd->as.reset.seed); + inf_reset_ctx( + (EncounterState*)s, (EncounterContext*)ctx, cmd->as.reset.seed); return; case INF_LAB_COMMAND_SET_PLAYER: - inf_lab_require_player_tile(s, cmd->as.tile.x, cmd->as.tile.y); + inf_lab_require_player_tile(s, ctx, cmd->as.tile.x, cmd->as.tile.y); s->player.x = cmd->as.tile.x; s->player.y = cmd->as.tile.y; inf_lab_clear_transient(s); @@ -307,6 +313,7 @@ static void inf_lab_apply_command(InfernoState* s, const InfernoLabCommand* cmd) } else { s->pillars[pillar->pillar_idx].hp = 0; } + inf_reset_npc_player_los_frame(ctx); inf_lab_clear_transient(s); inf_lab_refresh_geometry(s); return; @@ -333,7 +340,7 @@ static void inf_lab_apply_command(InfernoState* s, const InfernoLabCommand* cmd) if (ticks < 0) inf_lab_abort("step_ticks must be nonnegative"); int actions[INF_NUM_ACTION_HEADS] = {0}; for (int t = 0; t < ticks; t++) - inf_step((EncounterState*)s, actions); + inf_step_ctx((EncounterState*)s, (EncounterContext*)ctx, actions); inf_lab_refresh_geometry(s); return; } @@ -342,6 +349,13 @@ static void inf_lab_apply_command(InfernoState* s, const InfernoLabCommand* cmd) inf_lab_abort("unknown command kind %d", cmd->kind); } +static void inf_lab_apply_command( + InfernoState* s, + const InfernoLabCommand* cmd +) { + inf_lab_apply_command_ctx(s, inf_legacy_context(), cmd); +} + static int inf_lab_nearest_pillar_idx(const InfernoState* s, int x, int y) { int best_idx = -1; int best_dist = INT32_MAX; @@ -720,9 +734,13 @@ static int inf_lab_forecast_action_has_ranger_mager_same_tick( return 0; } -static void inf_lab_append_forecast_json(InfernoState* s, InfLabString* out) { +static void inf_lab_append_forecast_json( + InfernoState* s, + const InfernoContext* ctx, + InfLabString* out +) { InfStepOutForecast forecast; - inf_build_step_out_forecast_ctx(s, inf_legacy_context(), &forecast); + inf_build_step_out_forecast_ctx(s, ctx, &forecast); encounter_lab_string_append(out, "\"forecast\":{\"horizon\":%d,\"actions\":[", INF_STEP_OUT_FORECAST_HORIZON); for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { @@ -750,14 +768,18 @@ static void inf_lab_append_forecast_json(InfernoState* s, InfLabString* out) { encounter_lab_string_append(out, "]}"); } -static void inf_lab_append_npcs_json(InfernoState* s, InfLabString* out) { +static void inf_lab_append_npcs_json( + InfernoState* s, + const InfernoContext* ctx, + InfLabString* out +) { encounter_lab_string_append(out, "\"npcs\":["); int emitted = 0; for (int i = 0; i < INF_MAX_NPCS; i++) { InfNPC* npc = &s->npcs[i]; if (!npc->active) continue; if (emitted > 0) encounter_lab_string_append(out, ","); - int los = npc->death_ticks > 0 ? 0 : inf_npc_has_los(s, i); + int los = npc->death_ticks > 0 ? 0 : inf_npc_has_los_ctx(s, ctx, i); const EncounterPendingHit* pending = encounter_pending_hit_queue_earliest(&npc->pending_hits); int pending_ticks = pending ? pending->ticks_remaining : 0; @@ -798,7 +820,10 @@ static void inf_lab_append_pillars_json(const InfernoState* s, InfLabString* out encounter_lab_string_append(out, "]"); } -static char* inf_lab_alloc_json(InfernoState* s) { +static char* inf_lab_alloc_json_ctx( + InfernoState* s, + const InfernoContext* ctx +) { InfLabString out; encounter_lab_string_init(&out, "inferno lab"); encounter_lab_string_append(&out, @@ -814,13 +839,17 @@ static char* inf_lab_alloc_json(InfernoState* s) { s->player_attack_dmg); inf_lab_append_pillars_json(s, &out); encounter_lab_string_append(&out, ","); - inf_lab_append_npcs_json(s, &out); + inf_lab_append_npcs_json(s, ctx, &out); encounter_lab_string_append(&out, ","); - inf_lab_append_forecast_json(s, &out); + inf_lab_append_forecast_json(s, ctx, &out); encounter_lab_string_append(&out, "}"); return out.data; } +static char* inf_lab_alloc_json(InfernoState* s) { + return inf_lab_alloc_json_ctx(s, inf_legacy_context()); +} + static InfLabCommandKind inf_lab_command_kind_for(const char* command) { static const EncounterLabCommandAlias aliases[] = { { "reset", INF_LAB_COMMAND_RESET }, @@ -845,8 +874,11 @@ static InfLabCommandKind inf_lab_command_kind_for(const char* command) { "inferno lab", command, aliases, sizeof(aliases) / sizeof(aliases[0])); } -static InfLabLineResult inf_lab_apply_script_line_impl( - InfernoState* s, const char* line, char** out_json +static InfLabLineResult inf_lab_apply_script_line_impl_ctx( + InfernoState* s, + InfernoContext* ctx, + const char* line, + char** out_json ) { EncounterLabLine lex = encounter_lab_line_begin("inferno lab", line); if (!lex.command) { @@ -856,13 +888,13 @@ static InfLabLineResult inf_lab_apply_script_line_impl( if (strcmp(lex.command, "forecast") == 0) { InfStepOutForecast forecast; - inf_build_step_out_forecast_ctx(s, inf_legacy_context(), &forecast); + inf_build_step_out_forecast_ctx(s, ctx, &forecast); (void)forecast; free(lex.buffer); return INF_LAB_LINE_FORECAST; } if (strcmp(lex.command, "dump") == 0 || strcmp(lex.command, "dump_json") == 0) { - if (out_json) *out_json = inf_lab_alloc_json(s); + if (out_json) *out_json = inf_lab_alloc_json_ctx(s, ctx); free(lex.buffer); return INF_LAB_LINE_DUMP; } @@ -880,7 +912,7 @@ static InfLabLineResult inf_lab_apply_script_line_impl( } InfernoLabCommand cmd = inf_lab_build_script_command(kind, &args, lex.command); - inf_lab_apply_command(s, &cmd); + inf_lab_apply_command_ctx(s, ctx, &cmd); free(lex.buffer); return INF_LAB_LINE_NONE; } @@ -888,7 +920,8 @@ static InfLabLineResult inf_lab_apply_script_line_impl( static InfLabLineResult inf_lab_apply_script_line( InfernoState* s, const char* line ) { - return inf_lab_apply_script_line_impl(s, line, NULL); + return inf_lab_apply_script_line_impl_ctx( + s, inf_legacy_context(), line, NULL); } static InfLabLineResult inf_lab_apply_script_line_alloc_json( @@ -896,5 +929,6 @@ static InfLabLineResult inf_lab_apply_script_line_alloc_json( ) { if (!out_json) inf_lab_abort("json output pointer is required"); *out_json = NULL; - return inf_lab_apply_script_line_impl(s, line, out_json); + return inf_lab_apply_script_line_impl_ctx( + s, inf_legacy_context(), line, out_json); } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc b/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc index f2d71bd3c6..396454b8bb 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc @@ -1,13 +1,4 @@ #line 2173 "encounter_inferno.h" -static int inf_footprint_blocked( - const InfernoState* s, - int x, - int y, - int size -) { - return inf_footprint_blocked_ctx( - s, inf_legacy_context(), x, y, size); -} typedef struct { InfernoState* s; @@ -86,13 +77,6 @@ static int inf_npc_overlap_hold(void* ctx) { static int inf_tile_walkable(void* ctx, int x, int y); -static int inf_npc_environment_blocked(InfernoState* s, int x, int y, int size) { - return inf_npc_environment_blocked_ctx(s, inf_legacy_context(), x, y, size); -} - -static int inf_npc_terrain_blocked(InfernoState* s, int x, int y, int size) { - return inf_npc_environment_blocked_ctx(s, inf_legacy_context(), x, y, size); -} static void inf_npc_move_ctx( InfernoState* s, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index 3859fdd137..f6f9545d08 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -922,10 +922,19 @@ static void inf_restore(EncounterState* state, const void* data, size_t n) { inf_restore_ctx(state, (EncounterContext*)inf_legacy_context(), data, n); } +typedef struct { + InfernoState* state; + InfernoContext* context; +} InfLabInvocation; + static int inf_lab_apply_script_line_alloc_json_void( - void* state, const char* line, char** out_json + void* data, const char* line, char** out_json ) { - return inf_lab_apply_script_line_alloc_json((InfernoState*)state, line, out_json); + InfLabInvocation* invocation = (InfLabInvocation*)data; + if (!out_json) inf_lab_abort("json output pointer is required"); + *out_json = NULL; + return inf_lab_apply_script_line_impl_ctx( + invocation->state, invocation->context, line, out_json); } static int inf_apply_lab_command_ctx( @@ -933,9 +942,15 @@ static int inf_apply_lab_command_ctx( EncounterContext* context, const char* line ) { - (void)context; + InfLabInvocation invocation = { + .state = (InfernoState*)state, + .context = context ? (InfernoContext*)context : inf_legacy_context(), + }; return encounter_apply_lab_command_dump_wrapper( - state, line, INF_LAB_LINE_DUMP, inf_lab_apply_script_line_alloc_json_void); + &invocation, + line, + INF_LAB_LINE_DUMP, + inf_lab_apply_script_line_alloc_json_void); } static const EncounterDef ENCOUNTER_INFERNO = { diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h index f4ec2b9e56..3bce509c9a 100644 --- a/ocean/osrs/osrs_render.h +++ b/ocean/osrs/osrs_render.h @@ -4382,7 +4382,7 @@ static void render_player_composite( transform); } -static void render_draw_3d_world(RenderClient* rc) { +static void render_draw_3d_world(RenderClient* rc, OsrsEnv* env) { rlSetClipPlanes(0.5, 500.0); Camera3D cam = render_build_3d_camera(rc); @@ -5171,7 +5171,7 @@ static void render_draw_3d_world(RenderClient* rc) { (Vector3){ fa_x, bh, fa_z + fa_h }, (Vector3){ fa_x, bh, fa_z }, YELLOW); - InfernoState* debug_inferno_state = render_inferno_state_from_client(rc); + InfernoState* debug_inferno_state = render_inferno_state_from_env(env); if (rc->show_debug && rc->entity_count > 0) { int player_idx = rc->gui.gui_entity_idx; if (player_idx < 0 || player_idx >= rc->entity_count || @@ -5209,8 +5209,10 @@ static void render_draw_3d_world(RenderClient* rc) { InfNPC* npc = &debug_inferno_state->npcs[slot]; if (!npc->active || npc->death_ticks > 0) continue; int can_atk = - inf_player_can_attack_npc_from_current_tile( - debug_inferno_state, slot); + inf_player_can_attack_npc_from_current_tile_ctx( + debug_inferno_state, + (const InfernoContext*)env->encounter_context, + slot); lc = can_atk ? GREEN : RED; } @@ -5247,7 +5249,7 @@ static void render_draw_3d_world(RenderClient* rc) { static void render_draw_overhead_status(RenderClient* rc, OsrsEnv* env) { Camera3D cam = render_build_3d_camera(rc); - InfernoState* debug_state = render_inferno_state_from_client(rc); + InfernoState* debug_state = render_inferno_state_from_env(env); static const int prayer_to_headicon[] = { -1, @@ -5357,7 +5359,8 @@ static void render_draw_overhead_status(RenderClient* rc, OsrsEnv* env) { } if (npc->type != INF_NPC_NIBBLER) { - int npc_los = inf_npc_has_los(is, slot); + int npc_los = inf_npc_has_los_ctx( + is, (const InfernoContext*)env->encounter_context, slot); const char* los_txt = npc_los ? "NPC>P" : "NPC>P X"; Color los_col = npc_los ? GREEN : RED; int lw = MeasureText(los_txt, fs); @@ -5367,7 +5370,10 @@ static void render_draw_overhead_status(RenderClient* rc, OsrsEnv* env) { { int can_atk = - inf_player_can_attack_npc_from_current_tile(is, slot); + inf_player_can_attack_npc_from_current_tile_ctx( + is, + (const InfernoContext*)env->encounter_context, + slot); const char* patk_txt = can_atk ? "P>NPC" : "P>NPC X"; Color patk_col = can_atk ? GREEN : RED; int pw = MeasureText(patk_txt, fs); @@ -6158,7 +6164,7 @@ void pvp_render(OsrsEnv* env) { BeginDrawing(); ClearBackground(COLOR_BG); - render_draw_3d_world(rc); + render_draw_3d_world(rc, env); render_draw_overhead_status(rc, env); diff --git a/ocean/osrs/tests/bench_inferno_forecast.c b/ocean/osrs/tests/bench_inferno_forecast.c index 435d43aa51..c1f8f23d8c 100644 --- a/ocean/osrs/tests/bench_inferno_forecast.c +++ b/ocean/osrs/tests/bench_inferno_forecast.c @@ -7,6 +7,7 @@ #include "ocean/osrs/encounters/encounter_inferno.h" static volatile double bench_sink = 0.0; +static InfernoContext bench_context; static double now_seconds(void) { struct timespec ts; @@ -15,10 +16,8 @@ static double now_seconds(void) { } static void init_bench_state(InfernoState* state, int player_x, int player_y) { - inf_legacy_context()->config = inf_default_config(); inf_build_npc_stats(); memset(state, 0, sizeof(*state)); - memset(state->npc_los_cache, -1, sizeof(state->npc_los_cache)); state->rng_state = 20260516u; state->wave = 63; state->player.entity_type = ENTITY_PLAYER; @@ -41,8 +40,6 @@ static void init_bench_state(InfernoState* state, int player_x, int player_y) { state->pillars[p].hp = INF_PILLAR_HP; state->pillars[p].active = 1; } - inf_rebuild_los(state); - inf_rebuild_player_collision_flags(state); } static void add_bench_npc( @@ -62,7 +59,7 @@ static void init_pillar_stack_state(InfernoState* state) { init_bench_state(state, 29, 39); add_bench_npc(state, 0, INF_NPC_RANGER, 24, 31, 0); add_bench_npc(state, 1, INF_NPC_MAGER, 29, 30, 0); - inf_rebuild_entity_collision_flags(state); + inf_rebuild_npc_collision_flags(state); } static void init_dense_wave_state(InfernoState* state) { @@ -77,7 +74,7 @@ static void init_dense_wave_state(InfernoState* state) { add_bench_npc(state, 7, INF_NPC_BLOB, 26, 35, 2); add_bench_npc(state, 8, INF_NPC_BAT, 18, 30, 3); add_bench_npc(state, 9, INF_NPC_BAT, 19, 30, 4); - inf_rebuild_entity_collision_flags(state); + inf_rebuild_npc_collision_flags(state); } typedef void (*BenchInit)(InfernoState*); @@ -89,7 +86,7 @@ typedef void (*ForecastBuilder)( static void bench_forecast_exact(InfernoState* state, float* obs) { (void)obs; InfStepOutForecast forecast; - inf_build_step_out_forecast_exact_ctx(state, inf_legacy_context(), &forecast); + inf_build_step_out_forecast_exact_ctx(state, &bench_context, &forecast); bench_sink += forecast.actions[0].valid; bench_sink += forecast.actions[ENCOUNTER_MOVE_ACTIONS - 1].ticks[0].max_hit; } @@ -98,7 +95,7 @@ static void bench_forecast_fast_static(InfernoState* state, float* obs) { (void)obs; InfStepOutForecast forecast; inf_build_step_out_forecast_fast_static_ctx( - state, inf_legacy_context(), &forecast); + state, &bench_context, &forecast); bench_sink += forecast.actions[0].valid; bench_sink += forecast.actions[ENCOUNTER_MOVE_ACTIONS - 1].ticks[0].max_hit; } @@ -107,19 +104,21 @@ static void bench_forecast_fast_readonly(InfernoState* state, float* obs) { (void)obs; InfStepOutForecast forecast; inf_build_step_out_forecast_fast_readonly_ctx( - state, inf_legacy_context(), &forecast); + state, &bench_context, &forecast); bench_sink += forecast.actions[0].valid; bench_sink += forecast.actions[ENCOUNTER_MOVE_ACTIONS - 1].ticks[0].max_hit; } static void bench_obs(InfernoState* state, float* obs) { - inf_write_obs((EncounterState*)state, obs); + inf_write_obs_ctx( + (EncounterState*)state, (EncounterContext*)&bench_context, obs); bench_sink += obs[0]; bench_sink += obs[INF_NUM_OBS - 1]; } static void bench_mask(InfernoState* state, float* obs) { - inf_write_mask((EncounterState*)state, obs); + inf_write_mask_ctx( + (EncounterState*)state, (EncounterContext*)&bench_context, obs); bench_sink += obs[0]; bench_sink += obs[INF_ACTION_MASK_SIZE - 1]; } @@ -136,7 +135,8 @@ static void bench_step_fixed(const InfernoState* template, float* obs) { InfernoState state; memcpy(&state, template, sizeof(state)); int actions[INF_NUM_ACTION_HEADS] = {0}; - inf_step((EncounterState*)&state, actions); + inf_step_ctx( + (EncounterState*)&state, (EncounterContext*)&bench_context, actions); bench_sink += state.player.current_hitpoints; } @@ -144,9 +144,14 @@ static void bench_step_obs_mask_fixed(const InfernoState* template, float* obs) InfernoState state; memcpy(&state, template, sizeof(state)); int actions[INF_NUM_ACTION_HEADS] = {0}; - inf_step((EncounterState*)&state, actions); - inf_write_obs((EncounterState*)&state, obs); - inf_write_mask((EncounterState*)&state, obs + INF_NUM_OBS); + inf_step_ctx( + (EncounterState*)&state, (EncounterContext*)&bench_context, actions); + inf_write_obs_ctx( + (EncounterState*)&state, (EncounterContext*)&bench_context, obs); + inf_write_mask_ctx( + (EncounterState*)&state, + (EncounterContext*)&bench_context, + obs + INF_NUM_OBS); bench_sink += state.player.current_hitpoints; bench_sink += obs[0]; } @@ -189,8 +194,8 @@ static void report_forecast_diff( InfStepOutForecast exact; InfStepOutForecast fast; InfStepOutForecastOracleDiff diff; - inf_build_step_out_forecast_exact_ctx(&state, inf_legacy_context(), &exact); - fast_builder(&state, inf_legacy_context(), &fast); + inf_build_step_out_forecast_exact_ctx(&state, &bench_context, &exact); + fast_builder(&state, &bench_context, &fast); inf_compare_step_out_forecasts(&exact, &fast, &diff); double fn_rate = diff.exact_dangerous_actions > 0 ? (double)diff.dangerous_false_negatives / @@ -245,14 +250,17 @@ static void report_sampled_forecast_diff( InfStepOutForecast exact; InfStepOutForecast fast; InfStepOutForecastOracleDiff diff; - inf_build_step_out_forecast_exact_ctx(&state, inf_legacy_context(), &exact); - fast_builder(&state, inf_legacy_context(), &fast); + inf_build_step_out_forecast_exact_ctx(&state, &bench_context, &exact); + fast_builder(&state, &bench_context, &fast); inf_compare_step_out_forecasts(&exact, &fast, &diff); add_forecast_diff(&total, &diff); int actions[INF_NUM_ACTION_HEADS] = {0}; actions[INF_HEAD_MOVE] = sample % ENCOUNTER_MOVE_ACTIONS; - inf_step((EncounterState*)&state, actions); + inf_step_ctx( + (EncounterState*)&state, + (EncounterContext*)&bench_context, + actions); if (state.episode_over) { init(&state); resets++; @@ -284,6 +292,8 @@ static void report_sampled_forecast_diff( } int main(void) { + inf_init_context_typed(&bench_context); + inf_finalize_route_topology(&bench_context); printf("sizeof(InfernoState) = %zu\n", sizeof(InfernoState)); printf("INF_NUM_OBS = %d\n", INF_NUM_OBS); run_bench("empty exact", init_empty_state, bench_forecast_exact, 200000); diff --git a/ocean/osrs/tests/test_encounter_route_topology_setup.c b/ocean/osrs/tests/test_encounter_route_topology_setup.c index 5f5ae41b2b..1b92429be3 100644 --- a/ocean/osrs/tests/test_encounter_route_topology_setup.c +++ b/ocean/osrs/tests/test_encounter_route_topology_setup.c @@ -8,13 +8,16 @@ #define TEST_MAP_PATH "ocean/osrs/data/inferno.cmap" #define TEST_OFFSET_X 2246 #define TEST_OFFSET_Y 5315 -#define TEST_ORIGIN_X INF_ARENA_MIN_X -#define TEST_ORIGIN_Y INF_ARENA_MIN_Y -#define TEST_WIDTH INF_ARENA_WIDTH -#define TEST_HEIGHT INF_ARENA_HEIGHT +#define TEST_ORIGIN_X INF_TOPOLOGY_MIN_X +#define TEST_ORIGIN_Y INF_TOPOLOGY_MIN_Y +#define TEST_WIDTH INF_TOPOLOGY_WIDTH +#define TEST_HEIGHT INF_TOPOLOGY_HEIGHT typedef InfernoContext TestContext; #define TEST_TOPOLOGY(ctx) ((ctx)->route_topology) static uint32_t expected_flags(const CollisionMap* map, int x, int y) { + if (x < INF_ARENA_MIN_X || x > INF_ARENA_MAX_X || + y < INF_ARENA_MIN_Y || y > INF_ARENA_MAX_Y) + return COLLISION_BLOCKED; return (uint32_t)collision_get_flags( map, 0, x + TEST_OFFSET_X, y + TEST_OFFSET_Y); } @@ -272,6 +275,39 @@ int main(void) { else open_tiles++; } } +#if defined(TEST_ROUTE_TOPOLOGY_INFERNO) + if (first->static_los_mode != ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN) { + fprintf(stderr, + "inferno collision map incorrectly contributed static LOS blockers\n"); + abort(); + } + int blocked_x = -1; + int blocked_y = -1; + int open_x = -1; + int open_y = -1; + for (int x = TEST_ORIGIN_X; x < TEST_ORIGIN_X + TEST_WIDTH; x++) { + for (int y = TEST_ORIGIN_Y; y < TEST_ORIGIN_Y + TEST_HEIGHT; y++) { + int index = (x - TEST_ORIGIN_X) * TEST_HEIGHT + + (y - TEST_ORIGIN_Y); + if (first->static_blocked[index] && blocked_x < 0) { + blocked_x = x; + blocked_y = y; + } else if (!first->static_blocked[index] && open_x < 0) { + open_x = x; + open_y = y; + } + } + } + if (blocked_x < 0 || open_x < 0 || + !encounter_arena_topology_los_clear( + first, blocked_x, blocked_y, 1, open_x, open_y, 1, 0) || + !encounter_arena_topology_los_clear( + first, open_x, open_y, 1, blocked_x, blocked_y, 1, 0)) { + fprintf(stderr, + "inferno movement collision flags leaked into static LOS queries\n"); + abort(); + } +#endif #if defined(TEST_ROUTE_TOPOLOGY_ZULRAH) if (open_tiles != 69 || blocked_tiles != 715) { fprintf(stderr, "zulrah topology count mismatch: %d open %d blocked\n", diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index 52dd0b91bf..da3d9adbee 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -2607,11 +2607,17 @@ static void test_npc_terrain_blocks_full_footprint_lava_shelf(void) { state.wave = 68; ASSERT_INT_EQ("jad footprint y39 fits player arena", - inf_npc_terrain_blocked(&state, 24, 39, INF_NPC_STATS[INF_NPC_JAD].size), 0); + inf_npc_environment_blocked_ctx( + &state, inf_legacy_context(), + 24, 39, INF_NPC_STATS[INF_NPC_JAD].size), 0); ASSERT_INT_EQ("jad footprint y40 enters lava shelf", - inf_npc_terrain_blocked(&state, 24, 40, INF_NPC_STATS[INF_NPC_JAD].size), 1); + inf_npc_environment_blocked_ctx( + &state, inf_legacy_context(), + 24, 40, INF_NPC_STATS[INF_NPC_JAD].size), 1); ASSERT_INT_EQ("jad movement y40 is blocked", - inf_npc_environment_blocked(&state, 24, 40, INF_NPC_STATS[INF_NPC_JAD].size), 1); + inf_npc_environment_blocked_ctx( + &state, inf_legacy_context(), + 24, 40, INF_NPC_STATS[INF_NPC_JAD].size), 1); } static void test_zuk_jad_healer_spawn_falls_back_to_passable_arena_tiles(void) { @@ -2639,7 +2645,9 @@ static void test_zuk_jad_healer_spawn_falls_back_to_passable_arena_tiles(void) { ASSERT_INT_EQ("fallback healer y max", dy <= 10, 1); ASSERT_INT_EQ("fallback healer stays in arena", state.npcs[i].y <= INF_ARENA_MAX_Y, 1); ASSERT_INT_EQ("fallback healer terrain valid", - inf_npc_terrain_blocked(&state, state.npcs[i].x, state.npcs[i].y, 1), 0); + inf_npc_environment_blocked_ctx( + &state, inf_legacy_context(), + state.npcs[i].x, state.npcs[i].y, 1), 0); ASSERT_INT_EQ("fallback healer outside jad footprint", encounter_entity_footprints_overlap( state.npcs[i].x, state.npcs[i].y, 1, @@ -3987,7 +3995,8 @@ static void assert_inferno_npc_sw_origin_step( } add_step_out_forecast_npc(&state, 0, type, npc_x, npc_y, 0); ASSERT_INT_EQ("starting NPC has no LOS", - inf_npc_has_los_direct(&state, 0), 0); + inf_npc_has_los_direct_ctx( + &state, inf_legacy_context(), 0), 0); inf_npc_move(&state, 0); @@ -4039,7 +4048,7 @@ static void assert_inferno_jal_npc_uses_edge_clearance( } add_step_out_forecast_npc(&state, 0, type, npc_x, npc_y, 0); ASSERT_INT_EQ("starting Jal NPC has no LOS", - inf_npc_has_los(&state, 0), 0); + inf_npc_has_los_ctx(&state, inf_legacy_context(), 0), 0); inf_npc_move(&state, 0); @@ -4766,7 +4775,8 @@ static void assert_human_blowpipe_zuk_chase_endpoint( endpoint_state.npcs[0].attack_timer = 64; ASSERT_INT_EQ("Zuk blowpipe endpoint starts out of range", - inf_player_can_attack_npc_from_current_tile(&endpoint_state, 0), 0); + inf_player_can_attack_npc_from_current_tile_ctx( + &endpoint_state, inf_legacy_context(), 0), 0); HumanInput hi = make_human_input(); human_input_queue_attack_npc(&hi, 0); @@ -4783,7 +4793,8 @@ static void assert_human_blowpipe_zuk_chase_endpoint( ASSERT_INT_EQ("Zuk blowpipe endpoint y matches InfernoTrainer", endpoint_state.player.y, expected_y); ASSERT_INT_EQ("Zuk blowpipe endpoint remains out of range", - inf_player_can_attack_npc_from_current_tile(&endpoint_state, 0), 0); + inf_player_can_attack_npc_from_current_tile_ctx( + &endpoint_state, inf_legacy_context(), 0), 0); ASSERT_INT_EQ("Zuk blowpipe endpoint does not fire", endpoint_state.npcs[0].pending_hits.hits[0].active, 0); ASSERT_INT_EQ("Zuk blowpipe endpoint keeps interaction active", @@ -4811,7 +4822,8 @@ static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { edge_state.npcs[0].attack_timer = 64; ASSERT_INT_EQ("north-row Zuk click starts out of blowpipe range", - inf_player_can_attack_npc_from_current_tile(&edge_state, 0), 0); + inf_player_can_attack_npc_from_current_tile_ctx( + &edge_state, inf_legacy_context(), 0), 0); HumanInput edge_hi = make_human_input(); human_input_queue_attack_npc(&edge_hi, 0); @@ -4822,7 +4834,8 @@ static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { ASSERT_INT_EQ("north-row Zuk click walks to max north row", edge_state.player.y, INF_ARENA_MAX_Y); ASSERT_INT_EQ("north-row Zuk click remains outside blowpipe range", - inf_player_can_attack_npc_from_current_tile(&edge_state, 0), 0); + inf_player_can_attack_npc_from_current_tile_ctx( + &edge_state, inf_legacy_context(), 0), 0); ASSERT_INT_EQ("north-row Zuk cooldown prevents immediate hit", edge_state.npcs[0].pending_hits.hits[0].active, 0); ASSERT_INT_EQ("north-row Zuk interaction remains active", @@ -4838,7 +4851,8 @@ static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { state.npcs[0].attack_timer = 64; ASSERT_INT_EQ("human Zuk click starts out of blowpipe range", - inf_player_can_attack_npc_from_current_tile(&state, 0), 0); + inf_player_can_attack_npc_from_current_tile_ctx( + &state, inf_legacy_context(), 0), 0); float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); @@ -4866,7 +4880,8 @@ static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { ASSERT_INT_EQ("human Zuk click ends at max north row", state.player.y, INF_ARENA_MAX_Y); ASSERT_INT_EQ("human Zuk click never reaches blowpipe attack tile", - inf_player_can_attack_npc_from_current_tile(&state, 0), 0); + inf_player_can_attack_npc_from_current_tile_ctx( + &state, inf_legacy_context(), 0), 0); ASSERT_INT_EQ("human Zuk click does not fire unreachable blowpipe hit", state.npcs[0].pending_hits.hits[0].active, 0); ASSERT_INT_EQ("human Zuk click keeps interaction after chase", @@ -4907,7 +4922,8 @@ static void test_zuk_healer_blowpipe_target_chases_out_of_range(void) { ASSERT_INT_EQ("healer target follows reference seek x", state.player.x, 23); ASSERT_INT_EQ("healer target follows reference seek y", state.player.y, 42); ASSERT_INT_EQ("healer target remains active while still out of range", - inf_player_can_attack_npc_from_current_tile(&state, 2), 0); + inf_player_can_attack_npc_from_current_tile_ctx( + &state, inf_legacy_context(), 2), 0); ASSERT_INT_EQ("cooldown prevents immediate healer hit", state.npcs[2].pending_hits.hits[0].active, 0); ASSERT_INT_EQ("attack timer decrements after healer chase", @@ -4927,7 +4943,8 @@ static void test_zuk_healer_blowpipe_target_chases_out_of_range(void) { ASSERT_INT_EQ("healer target eventually fires after chase and cooldown", healer_hit_seen, 1); ASSERT_INT_EQ("healer target remains attackable after chase", - inf_player_can_attack_npc_from_current_tile(&state, 2), 1); + inf_player_can_attack_npc_from_current_tile_ctx( + &state, inf_legacy_context(), 2), 1); } static void test_render_facing_prefers_attack_target_while_chasing(void) { @@ -6310,7 +6327,8 @@ static void test_barrage_pending_queue_handles_slow_hit_delay(void) { ASSERT_INT_EQ("ranger is attackable at closest-tile range ten", closest, 10); ASSERT_INT_EQ("barrage timing still sees target SW distance twelve", sw_tile, 12); ASSERT_INT_EQ("player can cast at closest-tile range ten", - inf_player_can_attack_npc_from_current_tile(&state, 0), 1); + inf_player_can_attack_npc_from_current_tile_ctx( + &state, inf_legacy_context(), 0), 1); tick_barrage_pending_queue_edge_state(&state); ASSERT_INT_EQ("first cast queues one hit", @@ -9138,7 +9156,9 @@ static void test_compact_npc_observation_semantics(void) { ASSERT_FLOAT_NEAR("compact NPC attack style", obs[blob_start + 5], (float)ATTACK_STYLE_MAGIC / 4.0f, 1e-6f); ASSERT_FLOAT_NEAR("compact NPC LOS", - obs[blob_start + 6], (float)inf_npc_has_los(&state, 0), 1e-6f); + obs[blob_start + 6], + (float)inf_npc_has_los_ctx(&state, inf_legacy_context(), 0), + 1e-6f); ASSERT_FLOAT_NEAR("compact NPC frozen timer", obs[blob_start + 7], 0.5f, 1e-6f); ASSERT_FLOAT_NEAR("compact NPC target category", @@ -9465,6 +9485,42 @@ static void test_inferno_topology_geometry_parity(void) { 1); } +static void test_pillar_removal_resets_same_tick_los_frame(void) { + printf("--- inferno pillar removal resets same-tick LOS frame ---\n"); + + InfernoContext* ctx = inf_legacy_context(); + InfernoState state = make_test_state(30, 40); + set_inferno_pillar_phase(&state, 1); + state.pillars[0].hp = 1; + state.npcs[0] = make_test_npc( + INF_NPC_NIBBLER, + state.pillars[0].x, + state.pillars[0].y, + 1); + state.npcs[0].active = 1; + state.npcs[0].attack_timer = 0; + + uint32_t seed = 1; + for (;;) { + uint32_t probe = seed; + if (encounter_rand_int(&probe, 5) > 0) break; + seed++; + } + state.rng_state = seed; + memset(ctx->npc_player_los_frame, 1, sizeof(ctx->npc_player_los_frame)); + + inf_npc_attack_ctx(&state, ctx, 0); + + ASSERT_INT_EQ("nibbler removes one-hp pillar", state.pillars[0].active, 0); + int stale_entries = 0; + for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) + stale_entries += ctx->npc_player_los_frame[npc_idx] != -1; + ASSERT_INT_EQ( + "pillar removal clears every same-tick NPC LOS sample", + stale_entries, + 0); +} + static void test_inferno_topology_observation_mask_identity(void) { printf("--- inferno topology observation and mask identity ---\n"); @@ -9560,6 +9616,7 @@ int main(void) { inf_build_npc_stats(); inf_finalize_route_topology(inf_legacy_context()); test_inferno_topology_geometry_parity(); + test_pillar_removal_resets_same_tick_los_frame(); test_inferno_topology_observation_mask_identity(); test_compact_observation_layout_contract(); test_compact_player_and_pillar_observation_semantics(); From a506c5a462c4a035e9d79d842d35655ac7a4102c Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 05:11:28 +0300 Subject: [PATCH 19/50] Finalize Inferno context ownership --- .../inferno/encounter_inferno_forecast.inc | 7 -- .../inferno/encounter_inferno_lab.inc | 25 ------- .../inferno/encounter_inferno_obs_mask.inc | 8 +-- .../encounter_inferno_render_snapshot.inc | 18 ++--- .../inferno/encounter_inferno_reset_spawn.inc | 1 - .../inferno/encounter_inferno_reward_step.inc | 2 +- ocean/osrs/osrs_render.h | 24 +++++-- ocean/osrs/tests/inferno_lab_cli.c | 28 +++++--- ocean/osrs/tests/test_inferno_attack_styles.c | 56 +++++++++++++--- ocean/osrs/tests/test_inferno_lab.c | 66 +++++++++++-------- .../tests/test_inferno_render_lab_forecast.c | 35 ++++++++++ 11 files changed, 173 insertions(+), 97 deletions(-) create mode 100644 ocean/osrs/tests/test_inferno_render_lab_forecast.c diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc index 8b032a418e..04df54c7ee 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc @@ -1184,10 +1184,3 @@ static void inf_compare_step_out_forecasts( } } } - -static void inf_build_step_out_forecast( - const InfernoState* s, - InfStepOutForecast* out -) { - inf_build_step_out_forecast_ctx(s, inf_legacy_context(), out); -} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc index eb1b4406bf..aa92f84bba 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc @@ -349,12 +349,6 @@ static void inf_lab_apply_command_ctx( inf_lab_abort("unknown command kind %d", cmd->kind); } -static void inf_lab_apply_command( - InfernoState* s, - const InfernoLabCommand* cmd -) { - inf_lab_apply_command_ctx(s, inf_legacy_context(), cmd); -} static int inf_lab_nearest_pillar_idx(const InfernoState* s, int x, int y) { int best_idx = -1; @@ -846,9 +840,6 @@ static char* inf_lab_alloc_json_ctx( return out.data; } -static char* inf_lab_alloc_json(InfernoState* s) { - return inf_lab_alloc_json_ctx(s, inf_legacy_context()); -} static InfLabCommandKind inf_lab_command_kind_for(const char* command) { static const EncounterLabCommandAlias aliases[] = { @@ -916,19 +907,3 @@ static InfLabLineResult inf_lab_apply_script_line_impl_ctx( free(lex.buffer); return INF_LAB_LINE_NONE; } - -static InfLabLineResult inf_lab_apply_script_line( - InfernoState* s, const char* line -) { - return inf_lab_apply_script_line_impl_ctx( - s, inf_legacy_context(), line, NULL); -} - -static InfLabLineResult inf_lab_apply_script_line_alloc_json( - InfernoState* s, const char* line, char** out_json -) { - if (!out_json) inf_lab_abort("json output pointer is required"); - *out_json = NULL; - return inf_lab_apply_script_line_impl_ctx( - s, inf_legacy_context(), line, out_json); -} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc index bdef576292..498ca1714e 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -189,7 +189,7 @@ static void inf_write_obs_ctx( float* obs ) { InfernoState* s = (InfernoState*)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; #ifdef INF_PROFILE_ENABLED int inf_prof_enabled = INF_PROFILE_ENABLED(); double inf_prof_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; @@ -433,7 +433,7 @@ static int inf_is_human_targetable_npc_slot_ctx( int npc_slot ) { InfernoState* s = (InfernoState*)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; inf_ensure_current_obs_slots_ctx(s, ctx); return inf_obs_slot_is_targetable(s, ctx, inf_find_target_obs_slot(s, npc_slot)); } @@ -490,7 +490,7 @@ static void inf_write_mask_ctx( float* mask ) { InfernoState* s = (InfernoState*)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; inf_ensure_current_obs_slots_ctx(s, ctx); int offset = 0; @@ -645,7 +645,7 @@ static void inf_fill_render_entities_ctx( int* count ) { InfernoState* s = (InfernoState*)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; int n = 0; { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index f6f9545d08..d65700c622 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -6,7 +6,7 @@ static void inf_put_int_ctx( int value ) { InfernoState* s = (InfernoState*)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; if (strcmp(key, "start_wave") == 0) { inf_require_valid_public_wave(value); s->start_wave = value - 1; @@ -79,7 +79,7 @@ static void inf_put_float_ctx( float value ) { (void)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; if (strcmp(key, "damage_reward_coeff") == 0) ctx->config.damage_reward_coeff = value; else if (strcmp(key, "offensive_prayer_reward_coeff") == 0) { inf_require_nonnegative_float_config(key, value); @@ -179,7 +179,7 @@ static void inf_put_ptr_ctx( void* value ) { (void)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; if (strcmp(key, "collision_map") == 0) ctx->collision_map = (const CollisionMap*)value; else @@ -198,7 +198,7 @@ static int inf_get_winner_ctx(EncounterState* state, EncounterContext* context) static void* inf_get_log_ctx(EncounterState* state, EncounterContext* context) { InfernoState* s = (InfernoState*)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; if (s->episode_over && ctx->log) { ctx->log->episode_return += s->episode_return; ctx->log->episode_length += (float)s->tick; @@ -618,7 +618,7 @@ static void inf_translate_human_input_ctx( actions[INF_HEAD_PRAYER] = inf_local_overhead_action(actions[INF_HEAD_PRAYER]); encounter_translate_offensive_prayer(hi, actions, INF_HEAD_OFFENSIVE); InfernoState* s = (InfernoState*)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; inf_ensure_current_obs_slots_ctx(s, ctx); if (hi->pending_target_idx >= 0) { int found_slot = inf_find_target_obs_slot(s, hi->pending_target_idx); @@ -745,7 +745,7 @@ static void inf_step_human_commands_ctx( HumanInput* hi ) { InfernoState* s = (InfernoState*)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; int actions[INF_NUM_ACTION_HEADS]; ctx->human_command_mode = 1; ctx->human_commands = hi->commands.items; @@ -875,7 +875,7 @@ static size_t inf_snapshot_size(EncounterState* state) { } static void inf_snapshot_ctx(EncounterState* state, EncounterContext* context, void* out) { - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; InfSnapshot* snap = (InfSnapshot*)out; encounter_snapshot_write_frame( snap, sizeof(*snap), INF_SNAPSHOT_MAGIC, INF_SNAPSHOT_VERSION, @@ -897,7 +897,7 @@ static void inf_restore_ctx( const void* data, size_t n ) { - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; const InfSnapshot* snap = (const InfSnapshot*)data; encounter_snapshot_validate_frame( "inf_restore", data, n, sizeof(InfSnapshot), @@ -944,7 +944,7 @@ static int inf_apply_lab_command_ctx( ) { InfLabInvocation invocation = { .state = (InfernoState*)state, - .context = context ? (InfernoContext*)context : inf_legacy_context(), + .context = (InfernoContext*)context, }; return encounter_apply_lab_command_dump_wrapper( &invocation, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc index 1b2964194a..acb37f22b5 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -346,7 +346,6 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint inf_build_npc_stats(); InfernoState* s = (InfernoState*)state; InfernoContext* ctx = (InfernoContext*)context; - if (!ctx) ctx = inf_legacy_context(); encounter_arena_topology_require_finalized(ctx->route_topology); osrs_actor_route_cache_clear(&ctx->player_route_cache); inf_reset_npc_player_los_frame(ctx); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc index 788b013131..a4954ed24d 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc @@ -578,7 +578,7 @@ static void inf_record_idle_diagnostics( static void inf_step_ctx(EncounterState* state, EncounterContext* context, const int* actions) { InfernoState* s = (InfernoState*)state; - InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfernoContext* ctx = (InfernoContext*)context; encounter_arena_topology_require_finalized(ctx->route_topology); if (s->episode_over) return; diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h index 3bce509c9a..3027efdf99 100644 --- a/ocean/osrs/osrs_render.h +++ b/ocean/osrs/osrs_render.h @@ -798,12 +798,26 @@ static Color render_inferno_lab_forecast_color( return (Color){ 60, 220, 80, 170 }; } -static void render_inferno_lab_draw_forecast_3d(RenderClient* rc) { - InfernoState* s = render_inferno_state_from_client(rc); - if (!s || !rc->lab_enabled || !rc->lab_show_forecast) return; +static int render_inferno_lab_build_forecast( + RenderClient* rc, + const OsrsEnv* env, + InfStepOutForecast* forecast +) { + InfernoState* state = render_inferno_state_from_client(rc); + if (!state || !rc->lab_enabled || !rc->lab_show_forecast) return 0; + const InfernoContext* ctx = (const InfernoContext*)env->encounter_context; + encounter_arena_topology_require_finalized(ctx->route_topology); + inf_build_step_out_forecast_ctx(state, ctx, forecast); + return 1; +} + +static void render_inferno_lab_draw_forecast_3d( + RenderClient* rc, + const OsrsEnv* env +) { InfStepOutForecast forecast; - inf_build_step_out_forecast(s, &forecast); + if (!render_inferno_lab_build_forecast(rc, env, &forecast)) return; int has_terrain = rc->terrain && rc->terrain->loaded; for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { const InfStepOutForecastAction* action = &forecast.actions[action_idx]; @@ -4986,7 +5000,7 @@ static void render_draw_3d_world(RenderClient* rc, OsrsEnv* env) { } } } - render_inferno_lab_draw_forecast_3d(rc); + render_inferno_lab_draw_forecast_3d(rc, env); #undef OV_GROUND } diff --git a/ocean/osrs/tests/inferno_lab_cli.c b/ocean/osrs/tests/inferno_lab_cli.c index 09bc6a8931..59439d7c20 100644 --- a/ocean/osrs/tests/inferno_lab_cli.c +++ b/ocean/osrs/tests/inferno_lab_cli.c @@ -41,14 +41,18 @@ static char* read_script_line(FILE* file) { return line; } -static void run_script(FILE* file, InfernoState* state) { +static void run_script( + FILE* file, + InfernoState* state, + InfernoContext* context +) { for (;;) { char* line = read_script_line(file); if (!line) break; char* dump = NULL; - InfLabLineResult result = inf_lab_apply_script_line_alloc_json( - state, line, &dump); + InfLabLineResult result = inf_lab_apply_script_line_impl_ctx( + state, context, line, &dump); if (result == INF_LAB_LINE_DUMP) { printf("%s\n", dump); free(dump); @@ -59,21 +63,29 @@ static void run_script(FILE* file, InfernoState* state) { int main(int argc, char** argv) { InfernoState* state = (InfernoState*)inf_create(); - inf_put_float((EncounterState*)state, "late_start_supply_profile_scale", 1.0f); - inf_reset((EncounterState*)state, 20260515u); - inf_lab_apply_command(state, &(InfernoLabCommand){ + InfernoContext context; + inf_init_context_typed(&context); + inf_finalize_route_topology(&context); + inf_put_float_ctx( + (EncounterState*)state, + (EncounterContext*)&context, + "late_start_supply_profile_scale", + 1.0f); + inf_reset_ctx( + (EncounterState*)state, (EncounterContext*)&context, 20260515u); + inf_lab_apply_command_ctx(state, &context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_CLEAR_NPCS, }); if (argc == 1) { - run_script(stdin, state); + run_script(stdin, state, &context); } else if (argc == 2) { FILE* file = fopen(argv[1], "r"); if (!file) { fprintf(stderr, "inferno lab: cannot open %s\n", argv[1]); abort(); } - run_script(file, state); + run_script(file, state, &context); fclose(file); } else { fprintf(stderr, "usage: inferno_lab [script]\n"); diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index da3d9adbee..dd85a1bca1 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -6,6 +6,9 @@ #include "ocean/osrs/encounters/encounter_inferno.h" #include "ocean/osrs/tests/osrs_route_reference.h" +static void inf_init_unfinalized_context(InfernoContext* ctx) { + inf_init_context_typed(ctx); +} #define inf_init_context_typed(ctx_ptr) do { \ inf_init_context_typed(ctx_ptr); \ inf_finalize_route_topology((ctx_ptr)); \ @@ -3955,7 +3958,7 @@ static void test_step_out_forecast_matches_movement_head_destinations(void) { init_step_out_forecast_stack_state(&state, 29, 39); InfStepOutForecast forecast; - inf_build_step_out_forecast(&state, &forecast); + inf_build_step_out_forecast_ctx(&state, inf_legacy_context(), &forecast); for (int action = 0; action < ENCOUNTER_MOVE_ACTIONS; action++) { Player moved = state.player; @@ -4085,7 +4088,7 @@ static void test_step_out_forecast_north_pillar_ranger_mager_order(void) { add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); InfStepOutForecast forecast; - inf_build_step_out_forecast(&state, &forecast); + inf_build_step_out_forecast_ctx(&state, inf_legacy_context(), &forecast); const InfStepOutForecastAction* idle = &forecast.actions[0]; ASSERT_INT_EQ("idle remains safe from ranged tick one", @@ -4448,7 +4451,7 @@ static void test_step_out_forecast_south_pillar_ranger_mager_order(void) { add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 22, 26, 0); InfStepOutForecast forecast; - inf_build_step_out_forecast(&state, &forecast); + inf_build_step_out_forecast_ctx(&state, inf_legacy_context(), &forecast); const InfStepOutForecastAction* run_west = &forecast.actions[11]; assert_step_out_ranger_then_mager("south pillar run west", run_west, 20, 17); @@ -4463,7 +4466,7 @@ static void test_step_out_forecast_west_pillar_ranger_mager_order(void) { add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 16, 42, 0); InfStepOutForecast forecast; - inf_build_step_out_forecast(&state, &forecast); + inf_build_step_out_forecast_ctx(&state, inf_legacy_context(), &forecast); const InfStepOutForecastAction* walk_north = &forecast.actions[4]; assert_step_out_ranger_then_mager("west pillar walk north", walk_north, 11, 28); @@ -4482,7 +4485,7 @@ static void test_step_out_forecast_inactive_pillar_does_not_create_cover(void) { &state, 1, INF_NPC_MAGER, 29, 30, 0); InfStepOutForecast forecast; - inf_build_step_out_forecast(&state, &forecast); + inf_build_step_out_forecast_ctx(&state, inf_legacy_context(), &forecast); const InfStepOutForecastAction* idle = &forecast.actions[0]; ASSERT_INT_EQ("inactive north pillar exposes ranger immediately", @@ -5822,12 +5825,13 @@ static void test_npc_hit_lands_on_the_reference_tick(void) { for (size_t c = 0; c < sizeof(cases) / sizeof(cases[0]); c++) { InfernoState* s = (InfernoState*)inf_create(); inf_reset((EncounterState*)s, 20260728u); - inf_lab_apply_command(s, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_CLEAR_NPCS }); - inf_lab_apply_command(s, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(s, inf_legacy_context(), + &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_CLEAR_NPCS }); + inf_lab_apply_command_ctx(s, inf_legacy_context(), &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SET_PLAYER, .as.tile = { .x = 24, .y = 24 }, }); - inf_lab_apply_command(s, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(s, inf_legacy_context(), &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SPAWN_NPC, .as.spawn_npc = { .slot = 0, .type = cases[c].type, .x = cases[c].x, .y = cases[c].y, @@ -6454,7 +6458,7 @@ static void test_lab_dump_reports_npc_pending_hit_queue(void) { inf_queue_npc_pending_hit( &state, 0, 3, 17, ATTACK_STYLE_MAGIC, ENCOUNTER_SPELL_BLOOD, 1); - char* dump = inf_lab_alloc_json(&state); + char* dump = inf_lab_alloc_json_ctx(&state, inf_legacy_context()); ASSERT_INT_EQ("lab dump includes pending count", strstr(dump, "\"pending_count\":1") != NULL, 1); ASSERT_INT_EQ("lab dump includes pending timer", @@ -6900,6 +6904,31 @@ static void child_inf_restore_v1_snapshot(void) { inf_destroy(raw); } +static void child_inf_reset_before_topology_finalize(void) { + InfernoContext ctx; + InfernoState state; + inf_init_unfinalized_context(&ctx); + inf_init_state_typed(&state, &ctx); + inf_reset_ctx((EncounterState*)&state, (EncounterContext*)&ctx, 1u); +} + +static void child_inf_step_before_topology_finalize(void) { + InfernoContext ctx; + InfernoState state; + int actions[INF_NUM_ACTION_HEADS] = {0}; + inf_init_unfinalized_context(&ctx); + inf_init_state_typed(&state, &ctx); + inf_step_ctx((EncounterState*)&state, (EncounterContext*)&ctx, actions); +} + +static void child_inf_query_before_topology_finalize(void) { + InfernoContext ctx; + InfernoState state; + inf_init_unfinalized_context(&ctx); + inf_init_state_typed(&state, &ctx); + (void)inf_footprint_blocked_ctx(&state, &ctx, 20, 20, 1); +} + static void child_inf_restore_wrong_config_snapshot(void) { InfernoState state_a; InfernoState state_b; @@ -6932,6 +6961,15 @@ static void test_fail_fast_boundaries(void) { assert_child_aborts("removed win bonus config aborts", child_inf_put_removed_win_bonus); assert_child_aborts("overlay projectile overflow aborts", child_encounter_emit_projectile_overflow); assert_child_aborts("inferno pending spark overflow aborts", child_inf_pending_spark_overflow); + assert_child_aborts( + "inferno reset before topology finalize aborts", + child_inf_reset_before_topology_finalize); + assert_child_aborts( + "inferno step before topology finalize aborts", + child_inf_step_before_topology_finalize); + assert_child_aborts( + "inferno query before topology finalize aborts", + child_inf_query_before_topology_finalize); assert_child_aborts("inferno v1 snapshot restore aborts", child_inf_restore_v1_snapshot); assert_child_aborts( "inferno wrong-config snapshot restore aborts", diff --git a/ocean/osrs/tests/test_inferno_lab.c b/ocean/osrs/tests/test_inferno_lab.c index f6ac5ba3ed..e69352d489 100644 --- a/ocean/osrs/tests/test_inferno_lab.c +++ b/ocean/osrs/tests/test_inferno_lab.c @@ -16,11 +16,18 @@ } \ } while (0) +static InfernoContext lab_context; + static InfernoState* make_lab_state(void) { InfernoState* state = (InfernoState*)inf_create(); - inf_put_float((EncounterState*)state, "late_start_supply_profile_scale", 1.0f); - inf_reset((EncounterState*)state, 20260515u); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_put_float_ctx( + (EncounterState*)state, + (EncounterContext*)&lab_context, + "late_start_supply_profile_scale", + 1.0f); + inf_reset_ctx( + (EncounterState*)state, (EncounterContext*)&lab_context, 20260515u); + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_CLEAR_NPCS, }); return state; @@ -31,14 +38,14 @@ static void test_lab_typed_commands_mutate_state(void) { InfernoState* state = make_lab_state(); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SET_PLAYER, .as.tile = { .x = 29, .y = 39 }, }); ASSERT_INT_EQ("player x", state->player.x, 29); ASSERT_INT_EQ("player y", state->player.y, 39); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SPAWN_NPC, .as.spawn_npc = { .slot = 0, @@ -53,25 +60,25 @@ static void test_lab_typed_commands_mutate_state(void) { ASSERT_INT_EQ("ranger type", state->npcs[0].type, INF_NPC_RANGER); ASSERT_INT_EQ("ranger timer", state->npcs[0].attack_timer, 3); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_MOVE_NPC, .as.move_npc = { .slot = 0, .x = 20, .y = 32 }, }); ASSERT_INT_EQ("ranger moved x", state->npcs[0].x, 20); ASSERT_INT_EQ("ranger moved y", state->npcs[0].y, 32); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SET_NPC_HP, .as.npc_hp = { .slot = 0, .hp = 7 }, }); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SET_NPC_TIMER, .as.npc_timer = { .slot = 0, .timer = 0 }, }); ASSERT_INT_EQ("ranger hp", state->npcs[0].hp, 7); ASSERT_INT_EQ("ranger timer zero", state->npcs[0].attack_timer, 0); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SET_PILLAR, .as.pillar = { .pillar_idx = 2, @@ -80,13 +87,14 @@ static void test_lab_typed_commands_mutate_state(void) { }, }); ASSERT_INT_EQ("north pillar inactive", state->pillars[2].active, 0); - ASSERT_INT_EQ("north pillar removed from LOS blockers", state->los_blocker_count, 2); + ASSERT_INT_EQ("north pillar removed from dynamic LOS", + inf_pillar_footprint_blocked(state, 28, 36, 1), 0); inf_destroy((EncounterState*)state); } static void add_script_line(InfernoState* state, const char* line) { - InfLabLineResult result = inf_lab_apply_script_line(state, line); + InfLabLineResult result = inf_lab_apply_script_line_impl_ctx(state, &lab_context, line, NULL); ASSERT_INT_EQ("script line does not dump", result, INF_LAB_LINE_NONE); } @@ -106,11 +114,11 @@ static void test_lab_script_reaches_exact_forecast(void) { InfernoState* state = make_lab_state(); setup_north_pillar_stack_from_script(state); - InfLabLineResult result = inf_lab_apply_script_line(state, "forecast"); + InfLabLineResult result = inf_lab_apply_script_line_impl_ctx(state, &lab_context, "forecast", NULL); ASSERT_INT_EQ("forecast line result", result, INF_LAB_LINE_FORECAST); InfStepOutForecast forecast; - inf_build_step_out_forecast(state, &forecast); + inf_build_step_out_forecast_ctx(state, &lab_context, &forecast); const InfStepOutForecastAction* run_west = &forecast.actions[11]; ASSERT_INT_EQ("run west valid", run_west->valid, 1); ASSERT_INT_EQ("run west land x", run_west->land_x, 27); @@ -129,7 +137,7 @@ static void test_lab_json_contains_state_and_forecast(void) { InfernoState* state = make_lab_state(); setup_north_pillar_stack_from_script(state); - char* json = inf_lab_alloc_json(state); + char* json = inf_lab_alloc_json_ctx(state, &lab_context); ASSERT_CONTAINS("json has player", json, "\"player\":{\"x\":29,\"y\":39"); ASSERT_CONTAINS("json has ranger", json, "\"type\":\"ranger\""); ASSERT_CONTAINS("json has mager", json, "\"type\":\"mager\""); @@ -139,8 +147,7 @@ static void test_lab_json_contains_state_and_forecast(void) { free(json); char* dump = NULL; - InfLabLineResult result = inf_lab_apply_script_line_alloc_json( - state, "dump", &dump); + InfLabLineResult result = inf_lab_apply_script_line_impl_ctx(state, &lab_context, "dump", &dump); ASSERT_INT_EQ("dump line result", result, INF_LAB_LINE_DUMP); ASSERT_CONTAINS("dump has action array", dump, "\"actions\":["); free(dump); @@ -153,7 +160,7 @@ static void test_lab_spawn_wave_and_delete(void) { InfernoState* state = make_lab_state(); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SPAWN_WAVE, .as.wave = { .wave = 60 }, }); @@ -164,7 +171,7 @@ static void test_lab_spawn_wave_and_delete(void) { if (state->npcs[i].active) active_count++; ASSERT_INT_EQ("wave 60 active NPC count", active_count, 7); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_DELETE_NPC, .as.npc_slot = { .slot = 0 }, }); @@ -177,11 +184,11 @@ static void test_lab_snapshot_restore_round_trip(void) { printf("--- inferno lab snapshot restore round trip ---\n"); InfernoState* state = make_lab_state(); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SET_PLAYER, .as.tile = { .x = 29, .y = 39 }, }); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SPAWN_NPC, .as.spawn_npc = { .slot = 3, @@ -192,7 +199,7 @@ static void test_lab_snapshot_restore_round_trip(void) { .timer = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 2 }, }, }); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SET_PILLAR, .as.pillar = { .pillar_idx = 1, @@ -208,22 +215,22 @@ static void test_lab_snapshot_restore_round_trip(void) { state->player_pending_hits.count = 2; size_t snapshot_size = ENCOUNTER_INFERNO.snapshot_size( - (EncounterState*)state, (EncounterContext*)inf_legacy_context()); + (EncounterState*)state, (EncounterContext*)&lab_context); ASSERT_INT_EQ("snapshot size", (int)snapshot_size, (int)sizeof(InfSnapshot)); InfSnapshot* snapshot = (InfSnapshot*)malloc(snapshot_size); ENCOUNTER_INFERNO.snapshot( (EncounterState*)state, - (EncounterContext*)inf_legacy_context(), + (EncounterContext*)&lab_context, snapshot); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SET_PLAYER, .as.tile = { .x = 18, .y = 18 }, }); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_CLEAR_NPCS, }); - inf_lab_apply_command(state, &(InfernoLabCommand){ + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SET_PILLAR, .as.pillar = { .pillar_idx = 1, @@ -240,7 +247,7 @@ static void test_lab_snapshot_restore_round_trip(void) { ENCOUNTER_INFERNO.restore( (EncounterState*)state, - (EncounterContext*)inf_legacy_context(), + (EncounterContext*)&lab_context, snapshot, snapshot_size); @@ -251,7 +258,8 @@ static void test_lab_snapshot_restore_round_trip(void) { ASSERT_INT_EQ("restored mager hp", state->npcs[3].hp, 99); ASSERT_INT_EQ("restored mager timer", state->npcs[3].attack_timer, 2); ASSERT_INT_EQ("restored west pillar inactive", state->pillars[1].active, 0); - ASSERT_INT_EQ("restored LOS blockers", state->los_blocker_count, 2); + ASSERT_INT_EQ("restored west pillar removed from dynamic LOS", + inf_pillar_footprint_blocked(state, 11, 34, 1), 0); ASSERT_INT_EQ("restored wave", state->wave, 11); ASSERT_INT_EQ("restored tick", state->tick, 321); ASSERT_INT_EQ("restored rng", (int)state->rng_state, (int)0x1234abcd); @@ -264,6 +272,8 @@ static void test_lab_snapshot_restore_round_trip(void) { } int main(void) { + inf_init_context_typed(&lab_context); + inf_finalize_route_topology(&lab_context); test_lab_typed_commands_mutate_state(); test_lab_script_reaches_exact_forecast(); test_lab_json_contains_state_and_forecast(); diff --git a/ocean/osrs/tests/test_inferno_render_lab_forecast.c b/ocean/osrs/tests/test_inferno_render_lab_forecast.c new file mode 100644 index 0000000000..372d059207 --- /dev/null +++ b/ocean/osrs/tests/test_inferno_render_lab_forecast.c @@ -0,0 +1,35 @@ +#include +#include + +#include "ocean/osrs/osrs_pvp_actions.h" +#include "ocean/osrs/encounters/encounter_colosseum.h" +#include "ocean/osrs/encounters/encounter_inferno.h" +#include "ocean/osrs/encounters/encounter_zulrah.h" +#include "ocean/osrs/osrs_render.h" +int main(void) { + InfernoContext context; + inf_init_context_typed(&context); + inf_finalize_route_topology(&context); + + InfernoState state; + memset(&state, 0, sizeof(state)); + inf_reset_ctx((EncounterState*)&state, (EncounterContext*)&context, 20260812u); + + OsrsEnv env = {0}; + env.encounter_def = &ENCOUNTER_INFERNO; + env.encounter_state = &state; + env.encounter_context = &context; + + RenderClient client = {0}; + client.gui.encounter_def = &ENCOUNTER_INFERNO; + client.gui.encounter_state = &state; + client.lab_enabled = 1; + client.lab_show_forecast = 1; + + InfStepOutForecast forecast; + if (!render_inferno_lab_build_forecast(&client, &env, &forecast)) return 1; + if (!forecast.actions[0].valid) return 1; + + puts("inferno renderer lab forecast PASS"); + return 0; +} From 2e90426aefff3d7b09862797340915c8272a0250 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 06:26:18 +0300 Subject: [PATCH 20/50] Use shared topology in Colosseum --- .superpowers/sdd/task-5-report.md | 37 ++ .../colosseum/encounter_colosseum_boss.inc | 47 +- .../colosseum/encounter_colosseum_combat.inc | 78 ++- .../encounter_colosseum_forecast.inc | 29 +- .../colosseum/encounter_colosseum_helpers.inc | 90 +++- .../colosseum/encounter_colosseum_lab.inc | 95 ++-- .../encounter_colosseum_lab_parse.inc | 7 +- .../encounter_colosseum_mask_render.inc | 29 +- .../colosseum/encounter_colosseum_model.inc | 130 ----- .../encounter_colosseum_modifiers.inc | 130 +++-- .../encounter_colosseum_movement.inc | 218 +++++--- .../encounter_colosseum_obs_mask.inc | 47 +- .../encounter_colosseum_player_actions.inc | 7 +- .../encounter_colosseum_render_snapshot.inc | 47 +- .../encounter_colosseum_reset_spawn.inc | 286 +++++++--- .../encounter_colosseum_reward_step.inc | 13 +- ocean/osrs/osrs_collision.h | 189 +++++-- ocean/osrs/tests/probe_colo_best_gear_dpt.c | 1 + ocean/osrs/tests/probe_colo_bis_gear_prayer.c | 1 + ocean/osrs/tests/probe_colo_damage_scale.c | 5 +- .../tests/probe_colo_prayer_switch_slip.c | 1 + ocean/osrs/tests/probe_colo_rng_diversity.c | 1 + ocean/osrs/tests/probe_colo_sim_invariant.c | 1 + .../osrs/tests/probe_env_thread_determinism.c | 1 + ocean/osrs/tests/test_colo_hit_delay.c | 3 +- ocean/osrs/tests/test_colo_prayer_wiring.c | 14 +- .../tests/test_colosseum_forecast_exact.c | 269 ++++++++-- ocean/osrs/tests/test_colosseum_golden.c | 1 + ocean/osrs/tests/test_colosseum_modifiers.c | 488 ++++++++++++------ ocean/osrs/tests/test_osrs_eat_attack_delay.c | 1 + 30 files changed, 1538 insertions(+), 728 deletions(-) create mode 100644 .superpowers/sdd/task-5-report.md diff --git a/.superpowers/sdd/task-5-report.md b/.superpowers/sdd/task-5-report.md new file mode 100644 index 0000000000..bc713fd993 --- /dev/null +++ b/.superpowers/sdd/task-5-report.md @@ -0,0 +1,37 @@ +# Task 5 report + +## Result + +Implementation complete. Main validation pending. + +Base: `a506c5a462c4a035e9d79d842d35655ac7a4102c` + +## Implementation + +- Moved Colosseum arena bounds, static footprint occupancy, legal steps, and directed static LOS to the shared `EncounterArenaTopology`. +- Added the tile-blocked topology LOS build mode that preserves Colosseum's legacy directional endpoint rules. +- Deleted the private blocked-tile array, footprint table, static LOS table, and their initialization latch. +- Routed player, NPC, forecast, spawn, Sol, modifier, observation, mask, lab, and render geometry through explicit topology-owning contexts. +- Kept NPC occupancy, player occupancy, modifiers, hazards, and active state dynamic. +- Deleted the process-global legacy context and contextless reset, step, combat, lab, and attack wrappers. +- Made supported callbacks reject NULL context. Standalone probes and tests now initialize, finalize, and destroy their own contexts. +- Added exhaustive static tile, footprint, directed LOS, endpoint-law, legal-step, and forecast parity checks to existing Colosseum tests. +- Added lifecycle checks for NULL and unfinalized contexts. +- Preserved the observation and mask layouts. No item tables changed. + +## Observed evidence + +The interrupted worker's compile attempts exposed stale private-geometry and old-signature callers in `test_colosseum_modifiers.c`. Recovery migrated those callers and the remaining standalone probes. No clean compile, test, profile, or sanitizer result was observed after the final source edits. + +## Pending main validation + +Run the Task 5 gates from the brief: + +```bash +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/test_colosseum_modifiers && /tmp/test_colosseum_modifiers +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_forecast_exact.c -lm -o /tmp/test_colosseum_forecast_exact && /tmp/test_colosseum_forecast_exact +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_golden.c -lm -o /tmp/test_colosseum_golden && /tmp/test_colosseum_golden +clang -std=c11 -O2 -I. ocean/osrs/tests/probe_colo_sim_invariant.c -lm -o /tmp/probe_colo_sim_invariant && /tmp/probe_colo_sim_invariant +``` + +Also run the 10k modifier battery, Sol spear geometry checks, one-worker versus eight-worker determinism, C11 and C++17 include gates, sanitizer gates, twelve paired profiles, and `sample`. Compare all semantic hashes with the Task 4 baseline. Do not reseed. diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc index c154260cf2..89b1ff93ce 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc @@ -172,7 +172,7 @@ static void col_sol_move(ColosseumState* s, ColosseumContext* ctx, int idx) { return; col_stamp_npc_collision_footprint(s, boss->x, boss->y, size, 0); int moved = col_npc_path_step( - s, ctx->route_topology, &boss->x, &boss->y, size, + s, ctx, &boss->x, &boss->y, size, s->player.x - size / 2, s->player.y - size / 2); col_stamp_npc_collision_footprint(s, boss->x, boss->y, size, 1); boss->moved_this_tick = moved ? 1 : 0; @@ -421,8 +421,13 @@ static int col_sol_molten_sand_at(const SolHereditState* sol, int x, int y) { return 0; } -static void col_sol_add_pool(ColosseumState* s, int x, int y) { - if (col_static_blocked(x, y)) return; +static void col_sol_add_pool( + ColosseumState* s, + const ColosseumContext* ctx, + int x, + int y +) { + if (col_topology_tile_blocked(ctx, x, y)) return; SolHereditState* sol = &s->sol; if (col_sol_molten_sand_at(sol, x, y)) return; int slot = sol->hazard_tile_count < COLO_SOL_HAZARD_TILES_MAX @@ -438,6 +443,7 @@ typedef struct { static int col_sol_sample_molten_candidates( ColosseumState* s, + const ColosseumContext* ctx, int spread, int count, int force_player_tile, @@ -449,7 +455,7 @@ static int col_sol_sample_molten_candidates( for (int x = s->player.x - spread; x <= s->player.x + spread; x++) { for (int y = s->player.y - spread; y <= s->player.y + spread; y++) { if (!col_in_boss_arena(s, x, y)) continue; - if (col_static_blocked(x, y)) continue; + if (col_topology_tile_blocked(ctx, x, y)) continue; if (pool_count >= (int)(sizeof(pool) / sizeof(pool[0]))) break; if (x == s->player.x && y == s->player.y) player_idx = pool_count; pool[pool_count++] = (ColSolMoltenCandidate){ .x = x, .y = y }; @@ -482,22 +488,28 @@ static void col_sol_drop_beam_at(ColosseumState* s, int x, int y) { } } -static void col_sol_drop_beams(ColosseumState* s) { +static void col_sol_drop_beams( + ColosseumState* s, + const ColosseumContext* ctx +) { ColSolMoltenCandidate tiles[COLO_SOL_BEAM_COUNT]; int ntiles = col_sol_sample_molten_candidates( - s, COLO_SOL_BEAM_SPREAD, COLO_SOL_BEAM_COUNT, 1, tiles); + s, ctx, COLO_SOL_BEAM_SPREAD, COLO_SOL_BEAM_COUNT, 1, tiles); for (int n = 0; n < ntiles; n++) col_sol_drop_beam_at(s, tiles[n].x, tiles[n].y); } -static void col_sol_tick_molten(ColosseumState* s) { +static void col_sol_tick_molten( + ColosseumState* s, + const ColosseumContext* ctx +) { SolHereditState* sol = &s->sol; if (sol->phase >= 5) { if (sol->enrage_pool_timer > 0) sol->enrage_pool_timer--; if (sol->enrage_pool_timer == 0) { ColSolMoltenCandidate tile[1]; int ntiles = col_sol_sample_molten_candidates( - s, COLO_SOL_ENRAGE_POOL_SPREAD, 1, 0, tile); + s, ctx, COLO_SOL_ENRAGE_POOL_SPREAD, 1, 0, tile); if (ntiles > 0) col_sol_drop_beam_at(s, tile[0].x, tile[0].y); sol->enrage_pool_timer = COLO_SOL_MOLTEN_INTERVAL; } @@ -507,7 +519,8 @@ static void col_sol_tick_molten(ColosseumState* s) { if (!sol->beams[b].active) continue; sol->beams[b].ticks_to_pool--; if (sol->beams[b].ticks_to_pool > 0) continue; - col_sol_add_pool(s, sol->beams[b].x, sol->beams[b].y); + col_sol_add_pool( + s, ctx, sol->beams[b].x, sol->beams[b].y); sol->beams[b].active = 0; } @@ -630,17 +643,22 @@ static void col_sol_tick_grapple(ColosseumState* s) { } } -static void col_sol_enter_phase(ColosseumState* s, int new_phase) { +static void col_sol_enter_phase( + ColosseumState* s, + const ColosseumContext* ctx, + int new_phase +) { SolHereditState* sol = &s->sol; sol->phase = new_phase; - col_sol_drop_beams(s); + col_sol_drop_beams(s, ctx); if (new_phase >= 1 && new_phase <= 4) col_sol_spawn_crystal(s); if (new_phase >= 5) sol->laser_cooldown = COLO_SOL_CRYSTAL_COOLDOWN_ENRAGE; if (new_phase >= 5) { ColSolMoltenCandidate tiles[COLO_SOL_ENRAGE_OPEN_POOLS]; int ntiles = col_sol_sample_molten_candidates( - s, COLO_SOL_ENRAGE_POOL_SPREAD, COLO_SOL_ENRAGE_OPEN_POOLS, 0, tiles); + s, ctx, COLO_SOL_ENRAGE_POOL_SPREAD, + COLO_SOL_ENRAGE_OPEN_POOLS, 0, tiles); for (int p = 0; p < ntiles; p++) col_sol_drop_beam_at(s, tiles[p].x, tiles[p].y); sol->enrage_pool_timer = COLO_SOL_MOLTEN_INTERVAL; @@ -661,7 +679,8 @@ static void col_sol_boss_tick(ColosseumState* s, ColosseumContext* ctx) { int target_phase = col_sol_phase_for_hp(s->npcs[idx].hp); if (target_phase > sol->phase) { - while (sol->phase < target_phase) col_sol_enter_phase(s, sol->phase + 1); + while (sol->phase < target_phase) + col_sol_enter_phase(s, ctx, sol->phase + 1); sol->immobile_ticks = COLO_SOL_TRANSITION_FREEZE; sol->force_spear = 1; } @@ -674,7 +693,7 @@ static void col_sol_boss_tick(ColosseumState* s, ColosseumContext* ctx) { col_sol_tick_aoe(s); col_sol_tick_crystals(s); - col_sol_tick_molten(s); + col_sol_tick_molten(s, ctx); col_sol_tick_triple_parry(s); col_sol_tick_grapple(s); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc index bcdb036433..12d9f91cd9 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc @@ -1,6 +1,10 @@ #line 76 "encounter_colosseum.h" -static void col_apply_npc_death(ColosseumState* s, int idx) { +static void col_apply_npc_death( + ColosseumState* s, + const ColosseumContext* ctx, + int idx +) { ColoNPC* npc = &s->npcs[idx]; if (col_type_is_hazard_entity(npc->type)) { @@ -19,7 +23,9 @@ static void col_apply_npc_death(ColosseumState* s, int idx) { s->death_charge_cd = COLO_DEATH_CHARGE_CD; } - col_mod_volatility_on_death(s, npc->x, npc->y, col_npc_effective_size(npc)); + col_mod_volatility_on_death( + s, ctx->route_topology, + npc->x, npc->y, col_npc_effective_size(npc)); col_mod_on_owner_death(s, idx); if (!osrs_npc_death_linger_start( npc->hp, npc->active, &npc->death_ticks, @@ -77,9 +83,10 @@ static void col_apply_echo_boots_recoil( s->current_wave_fresh_damage += fresh; } if (npc->hp <= 0) { - col_apply_npc_death(s, attacker_slot); + col_apply_npc_death(s, ctx, attacker_slot); } else { - col_mod_on_npc_hp_changed(s, attacker_slot); + col_mod_on_npc_hp_changed( + s, ctx->route_topology, attacker_slot); } } @@ -120,10 +127,10 @@ static void col_resolve_player_projectiles_on_npcs_ctx( s->current_wave_fresh_damage += fresh; } if (npc->hp <= 0) { - col_apply_npc_death(s, i); + col_apply_npc_death(s, ctx, i); break; } - col_mod_on_npc_hp_changed(s, i); + col_mod_on_npc_hp_changed(s, ctx->route_topology, i); encounter_pending_hit_queue_remove(&npc->pending_hits, h, "colosseum-npc"); h--; } @@ -131,8 +138,11 @@ static void col_resolve_player_projectiles_on_npcs_ctx( } } -static void col_resolve_player_projectiles_on_npcs(ColosseumState* s) { - col_resolve_player_projectiles_on_npcs_ctx(s, col_legacy_context()); +static void col_resolve_player_projectiles_on_npcs( + ColosseumState* s, + ColosseumContext* ctx +) { + col_resolve_player_projectiles_on_npcs_ctx(s, ctx); } static void col_tick_npc_death_lingers(ColosseumState* s) { @@ -288,9 +298,6 @@ static void col_resolve_player_pending_hits_ctx(ColosseumState* s, ColosseumCont } } -static void col_resolve_player_pending_hits(ColosseumState* s) { - col_resolve_player_pending_hits_ctx(s, col_legacy_context()); -} static int col_npc_dist_to_player(const ColosseumState* s, const ColoNPC* npc) { return encounter_dist_to_npc( @@ -412,7 +419,11 @@ static void col_npc_attack_jaguar( col_npc_melee_attack(s, ctx, idx, stats, stats->max_hit); } -static void col_npc_minotaur_heal_ally(ColosseumState* s, int idx) { +static void col_npc_minotaur_heal_ally( + ColosseumState* s, + const ColosseumContext* ctx, + int idx +) { ColoNPC* mino = &s->npcs[idx]; int mcx = mino->x + 1, mcy = mino->y + 1; int best = -1; @@ -427,7 +438,9 @@ static void col_npc_minotaur_heal_ally(ColosseumState* s, int idx) { int dx = ocx > mcx ? ocx - mcx : mcx - ocx; int dy = ocy > mcy ? ocy - mcy : mcy - ocy; if ((dx > dy ? dx : dy) > COLO_MINOTAUR_HEAL_RANGE) continue; - if (!col_tiles_have_los(s, mcx, mcy, ocx, ocy)) continue; + if (!col_topology_los_clear( + ctx, mcx, mcy, 1, ocx, ocy, 1, 0)) + continue; if (best < 0 || other->hp * s->npcs[best].max_hp < s->npcs[best].hp * other->max_hp) best = i; @@ -485,10 +498,16 @@ static void col_npc_resolve_javelin_skyfall( col_mod_on_player_damaged( s, applied, COLO_DAMAGE_JAVELIN_SKYFALL); } - col_mod_reentry_on_skyfall(s, jv->skyfall_tile_x, jv->skyfall_tile_y); + col_mod_reentry_on_skyfall( + s, ctx->route_topology, + jv->skyfall_tile_x, jv->skyfall_tile_y); } -static void col_manticore_delay_ready_peers(ColosseumState* s, int idx) { +static void col_manticore_delay_ready_peers( + ColosseumState* s, + const ColosseumContext* ctx, + int idx +) { for (int i = 0; i < COLO_MAX_NPCS; i++) { if (i == idx) continue; ColoNPC* other = &s->npcs[i]; @@ -498,7 +517,7 @@ static void col_manticore_delay_ready_peers(ColosseumState* s, int idx) { if (other->stun_timer > 0 || other->frozen_ticks > 0) continue; int dist = col_npc_dist_to_player(s, other); if (dist < 1 || dist > COLO_NPC_STATS[COLO_MANTICORE].attack_range) continue; - if (!col_npc_has_los_to_player(s, other)) continue; + if (!col_npc_has_los_to_player(s, ctx, other)) continue; other->attack_timer = COLO_MANTICORE_STAGGER_TICKS + 1; } } @@ -568,12 +587,17 @@ static void col_npc_manticore_arm(ColosseumState* s, int idx) { mc->cycle_step = 0; } -static void col_npc_attack_manticore(ColosseumState* s, int idx, const ColoNpcStats* stats) { +static void col_npc_attack_manticore( + ColosseumState* s, + ColosseumContext* ctx, + int idx, + const ColoNpcStats* stats +) { ColoNPC* npc = &s->npcs[idx]; ColoManticoreState* mc = colo_npc_manticore(npc); mc->cycle_step = 0; col_npc_manticore_fire_orb(s, idx, stats); - col_manticore_delay_ready_peers(s, idx); + col_manticore_delay_ready_peers(s, ctx, idx); } static void col_npc_attack_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) { @@ -598,7 +622,7 @@ static void col_npc_attack_ctx(ColosseumState* s, ColosseumContext* ctx, int idx if (npc->type == COLO_MANTICORE) { int mdist = col_npc_dist_to_player(s, npc); if (mdist < 1 || mdist > stats->attack_range) return; - if (!col_npc_has_los_to_player(s, npc)) return; + if (!col_npc_has_los_to_player(s, ctx, npc)) return; int cycle_step_before_arm = npc->type_state.manticore.cycle_step; col_npc_manticore_arm(s, idx); if (cycle_step_before_arm < 0 && @@ -607,7 +631,7 @@ static void col_npc_attack_ctx(ColosseumState* s, ColosseumContext* ctx, int idx ctx->manticore_charge_started[idx] = 1; } if (npc->attack_timer > 0) return; - col_npc_attack_manticore(s, idx, stats); + col_npc_attack_manticore(s, ctx, idx, stats); return; } @@ -618,14 +642,14 @@ static void col_npc_attack_ctx(ColosseumState* s, ColosseumContext* ctx, int idx if (npc->type == COLO_MINOTAUR) { if (dist == 1) col_npc_melee_attack(s, ctx, idx, stats, stats->max_hit); - else col_npc_minotaur_heal_ally(s, idx); + else col_npc_minotaur_heal_ally(s, ctx, idx); npc->attack_timer = stats->attack_speed; return; } if (stats->attack_range > 1) { if (dist < 1 || dist > stats->attack_range) return; - if (!col_npc_has_los_to_player(s, npc)) return; + if (!col_npc_has_los_to_player(s, ctx, npc)) return; } else { if (!col_npc_melee_adjacent_to_player(s, npc)) return; @@ -690,7 +714,11 @@ static void col_warband_attack_phase(ColosseumState* s, ColosseumContext* ctx) { } static void col_live_threat_style_counts( - const ColosseumState* s, int* magic, int* ranged, int* melee + const ColosseumState* s, + const ColosseumContext* ctx, + int* magic, + int* ranged, + int* melee ) { *magic = 0; *ranged = 0; @@ -703,7 +731,9 @@ static void col_live_threat_style_counts( if (ns->attack_range < 1) continue; int dist = col_npc_dist_to_player(s, npc); if (dist < 1 || dist > ns->attack_range) continue; - if (ns->attack_range > 1 && !col_npc_has_los_to_player(s, npc)) continue; + if (ns->attack_range > 1 && + !col_npc_has_los_to_player(s, ctx, npc)) + continue; AttackStyle style = ns->default_style; if (npc->type == COLO_MANTICORE) { const ColoManticoreState* mc = &npc->type_state.manticore; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc index 9920d4e8d5..c0c2c99666 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc @@ -76,20 +76,25 @@ static ColoVolatilityBlastPreview col_soonest_queued_volatility_blast( return best; } -static int col_step_out_forecast_action_valid(const ColosseumState* s, int action_idx) { +static int col_step_out_forecast_action_valid( + const ColosseumState* s, + const ColosseumContext* ctx, + int action_idx +) { if (action_idx == 0) return 1; if (s->modifiers.draft_pending) return 0; int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action_idx]; int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action_idx]; - return col_player_walkable((void*)s, nx, ny); + return col_player_walkable_ctx(s, ctx, nx, ny); } static ColoForecastLanding col_step_out_forecast_action_landing_ctx( const ColosseumState* s, + const ColosseumContext* ctx, int action_idx ) { ColoForecastLanding landing = { - .valid = col_step_out_forecast_action_valid(s, action_idx), + .valid = col_step_out_forecast_action_valid(s, ctx, action_idx), .land_x = s->player.x, .land_y = s->player.y, }; @@ -114,18 +119,23 @@ static ColoForecastLanding col_step_out_forecast_action_landing_ctx( int moved = 0; if (dx != 0 && dy != 0 && - col_player_walkable((void*)s, landing.land_x + dx, landing.land_y + dy) && - col_player_walkable((void*)s, landing.land_x + dx, landing.land_y) && - col_player_walkable((void*)s, landing.land_x, landing.land_y + dy)) { + col_player_walkable_ctx( + s, ctx, landing.land_x + dx, landing.land_y + dy) && + col_player_walkable_ctx( + s, ctx, landing.land_x + dx, landing.land_y) && + col_player_walkable_ctx( + s, ctx, landing.land_x, landing.land_y + dy)) { landing.land_x += dx; landing.land_y += dy; moved = 1; } else if (dx != 0 && - col_player_walkable((void*)s, landing.land_x + dx, landing.land_y)) { + col_player_walkable_ctx( + s, ctx, landing.land_x + dx, landing.land_y)) { landing.land_x += dx; moved = 1; } else if (dy != 0 && - col_player_walkable((void*)s, landing.land_x, landing.land_y + dy)) { + col_player_walkable_ctx( + s, ctx, landing.land_x, landing.land_y + dy)) { landing.land_y += dy; moved = 1; } @@ -137,6 +147,7 @@ static ColoForecastLanding col_step_out_forecast_action_landing_ctx( static int col_primary_action_valid( const ColosseumState* s, + const ColosseumContext* ctx, int primary_action ) { if (primary_action < 0 || primary_action >= COLO_PRIMARY_DIM) { @@ -148,7 +159,7 @@ static int col_primary_action_valid( if (primary_action < ENCOUNTER_MOVE_ACTIONS) { int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[primary_action]; int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[primary_action]; - return col_player_walkable((void*)s, nx, ny); + return col_player_walkable_ctx(s, ctx, nx, ny); } int obs_slot = col_primary_obs_slot_from_action(primary_action); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc index 9cc907e05d..ec09abc8a4 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc @@ -1,7 +1,10 @@ #line 76 "encounter_colosseum.h" static void col_accumulate_tick_stats(ColosseumState* s, const ColosseumContext* ctx); -static void col_spawn_wave(ColosseumState* s); +static void col_spawn_wave( + ColosseumState* s, + const ColosseumContext* ctx +); static void col_tick_npcs_ctx(ColosseumState* s, ColosseumContext* ctx); static void col_tick_player_ctx( ColosseumState* s, ColosseumContext* ctx, const int* actions, int can_attack); @@ -9,7 +12,6 @@ static void col_player_pretick(ColosseumState* s, ColosseumContext* ctx, const i static void col_resolve_player_projectiles_on_npcs_ctx( ColosseumState* s, ColosseumContext* ctx); static void col_resolve_player_pending_hits_ctx(ColosseumState* s, ColosseumContext* ctx); -static void col_resolve_player_pending_hits(ColosseumState* s); static void col_rebuild_player_collision_flags(ColosseumState* s); static void col_refresh_current_obs_slots_ctx(ColosseumState* s, const ColosseumContext* ctx); @@ -287,6 +289,53 @@ static ColConfig col_default_config(void) { .damage_scale_anneal_ticks = 0, }; } +static ColosseumContext* col_require_context(EncounterContext* context) { + if (!context) { + fprintf(stderr, "colosseum context is required\n"); + abort(); + } + return (ColosseumContext*)context; +} + + +static int col_topology_tile_blocked( + const ColosseumContext* ctx, + int x, + int y +) { + return encounter_arena_topology_tile_blocked(ctx->route_topology, x, y); +} + +static int col_topology_footprint_blocked( + const ColosseumContext* ctx, + int x, + int y, + int size +) { + return encounter_arena_topology_footprint_blocked( + ctx->route_topology, x, y, size); +} + +static int col_topology_los_clear( + const ColosseumContext* ctx, + int actor_x, + int actor_y, + int actor_size, + int target_x, + int target_y, + int target_size, + int attack_range +) { + return encounter_arena_topology_los_clear( + ctx->route_topology, + actor_x, + actor_y, + actor_size, + target_x, + target_y, + target_size, + attack_range); +} typedef struct { EncounterArenaTopology* topology; @@ -296,12 +345,22 @@ static ColRouteTopologyOwner col_route_topology_owner; static uint32_t col_route_topology_flags(void* data, int x, int y) { (void)data; - int grid_x = x - COLO_ARENA_MIN_X; - int grid_y = y - COLO_ARENA_MIN_Y; - if (grid_x < 0 || grid_x >= COLO_ARENA_WIDTH || - grid_y < 0 || grid_y >= COLO_ARENA_HEIGHT || - COLO_STATIC_BLOCKED[grid_x][grid_y]) + if (x < COLO_ARENA_MIN_X || x > COLO_ARENA_MAX_X || + y < COLO_ARENA_MIN_Y || y > COLO_ARENA_MAX_Y) return COLLISION_BLOCKED | LOS_FULL_MASK; + int row = y - COLO_ARENA_MIN_Y; + for (int span = 0; span < COLO_WALL_SPANS_PER_ROW; span++) { + ColoWallSpan wall = COLO_WALL_SPANS[row][span]; + if (x >= wall.lo && x < wall.hi) + return COLLISION_BLOCKED | LOS_FULL_MASK; + } + for (int pillar = 0; pillar < COLO_NUM_PILLARS; pillar++) { + int pillar_x = COLO_PILLARS[pillar][0]; + int pillar_y = COLO_PILLARS[pillar][1]; + if (x >= pillar_x && x < pillar_x + COLO_PILLAR_SIZE && + y >= pillar_y && y < pillar_y + COLO_PILLAR_SIZE) + return COLLISION_BLOCKED | LOS_FULL_MASK; + } return 0; } @@ -310,7 +369,6 @@ static void col_finalize_route_topology(ColosseumContext* ctx) { fprintf(stderr, "colosseum route topology finalized twice\n"); abort(); } - col_build_static_arena(); EncounterArenaTopologyBuildSpec spec = { .origin_x = COLO_ARENA_MIN_X, .origin_y = COLO_ARENA_MIN_Y, @@ -320,6 +378,8 @@ static void col_finalize_route_topology(ColosseumContext* ctx) { .revision = UINT64_C(0x434f4c4f53534501), .tile_flags = col_route_topology_flags, .tile_flags_ctx = NULL, + .los_build_mode = + ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_TILE_BLOCKED, }; if (!col_route_topology_owner.topology) { col_route_topology_owner.topology = @@ -339,7 +399,7 @@ static void col_finalize_context( EncounterContext* context ) { (void)state; - col_finalize_route_topology((ColosseumContext*)context); + col_finalize_route_topology(col_require_context(context)); } static void col_init_context_typed(ColosseumContext* ctx) { @@ -355,15 +415,6 @@ static void col_destroy_context(EncounterContext* context) { (void)context; } -static ColosseumContext* col_legacy_context(void) { - static ColosseumContext ctx; - static int initialized = 0; - if (!initialized) { - col_init_context_typed(&ctx); - initialized = 1; - } - return &ctx; -} static void col_init_state_typed(ColosseumState* s, ColosseumContext* ctx) { col_build_npc_stats(); @@ -375,7 +426,8 @@ static void col_init_state_typed(ColosseumState* s, ColosseumContext* ctx) { } static void col_init_state(EncounterState* state, EncounterContext* context) { - col_init_state_typed((ColosseumState*)state, (ColosseumContext*)context); + col_init_state_typed( + (ColosseumState*)state, col_require_context(context)); } static EncounterState* col_create(void) { diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc index 6bd8d8702b..141091815d 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc @@ -129,13 +129,19 @@ static void col_lab_require_type(int type) { col_lab_abort("npc type must be in [0,%d], got %d", COLO_NUM_NPC_TYPES - 1, type); } -static int col_lab_in_arena(int x, int y) { - return x >= COLO_ARENA_MIN_X && x <= COLO_ARENA_MAX_X && - y >= COLO_ARENA_MIN_Y && y <= COLO_ARENA_MAX_Y; +static int col_lab_in_arena( + const ColosseumContext* ctx, + int x, + int y +) { + return encounter_arena_topology_contains(ctx->route_topology, x, y); } -static void col_lab_clear_transient(ColosseumState* s) { - col_context_clear_render_events(col_legacy_context()); +static void col_lab_clear_transient( + ColosseumState* s, + ColosseumContext* ctx +) { + col_context_clear_render_events(ctx); osrs_interaction_init(&s->interaction); s->player_last_interaction_target_slot = -1; s->player_last_interaction_age = 1; @@ -158,9 +164,12 @@ static void col_lab_clear_transient(ColosseumState* s) { } } -static void col_lab_refresh_geometry(ColosseumState* s) { +static void col_lab_refresh_geometry( + ColosseumState* s, + const ColosseumContext* ctx +) { col_rebuild_player_collision_flags(s); - col_refresh_current_obs_slots_ctx(s, col_legacy_context()); + col_refresh_current_obs_slots_ctx(s, ctx); } static void col_lab_remove_all_npcs(ColosseumState* s) { @@ -169,28 +178,38 @@ static void col_lab_remove_all_npcs(ColosseumState* s) { memset(s->bees, 0, sizeof(s->bees)); } -static void col_lab_clear_npcs(ColosseumState* s) { +static void col_lab_clear_npcs( + ColosseumState* s, + ColosseumContext* ctx +) { col_lab_remove_all_npcs(s); - col_lab_clear_transient(s); - col_lab_refresh_geometry(s); + col_lab_clear_transient(s, ctx); + col_lab_refresh_geometry(s, ctx); } static const char* col_lab_modifier_name(int mod); -static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* cmd) { - if (!s || !cmd) col_lab_abort("null command"); +static void col_lab_apply_command( + ColosseumState* s, + ColosseumContext* ctx, + const ColosseumLabCommand* cmd +) { + if (!s || !ctx || !cmd) col_lab_abort("null command"); + encounter_arena_topology_require_finalized(ctx->route_topology); switch (cmd->kind) { case COLO_LAB_COMMAND_NONE: return; case COLO_LAB_COMMAND_RESET: - col_reset((EncounterState*)s, cmd->as.reset.seed); + col_reset_ctx( + (EncounterState*)s, (EncounterContext*)ctx, + cmd->as.reset.seed); return; case COLO_LAB_COMMAND_SET_PLAYER: { const ColoLabPlayerCommand* p = &cmd->as.player; - if (!col_lab_in_arena(p->x, p->y)) + if (!col_lab_in_arena(ctx, p->x, p->y)) col_lab_abort("player tile out of arena: (%d,%d)", p->x, p->y); s->player.x = p->x; s->player.y = p->y; @@ -203,8 +222,8 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* } if (p->prayer.kind == ENCOUNTER_LAB_OPTIONAL_INT_SET) s->player.prayer = (OverheadPrayer)p->prayer.value; - col_lab_clear_transient(s); - col_lab_refresh_geometry(s); + col_lab_clear_transient(s, ctx); + col_lab_refresh_geometry(s, ctx); return; } @@ -218,7 +237,7 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* } col_lab_require_slot(spawn->slot); col_lab_require_type(spawn->type); - if (!col_lab_in_arena(spawn->x, spawn->y)) + if (!col_lab_in_arena(ctx, spawn->x, spawn->y)) col_lab_abort("npc tile out of arena: (%d,%d)", spawn->x, spawn->y); if (s->npcs[spawn->slot].active) col_deactivate_npc(s, spawn->slot); @@ -236,8 +255,8 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* col_lab_abort("npc timer must be nonnegative"); s->npcs[spawn->slot].attack_timer = spawn->timer.value; } - col_lab_clear_transient(s); - col_lab_refresh_geometry(s); + col_lab_clear_transient(s, ctx); + col_lab_refresh_geometry(s, ctx); return; } @@ -246,7 +265,7 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* col_lab_require_slot(move->slot); if (!s->npcs[move->slot].active) col_lab_abort("cannot move inactive npc slot %d", move->slot); - if (!col_lab_in_arena(move->x, move->y)) + if (!col_lab_in_arena(ctx, move->x, move->y)) col_lab_abort("npc tile out of arena: (%d,%d)", move->x, move->y); col_stamp_npc_collision(s, &s->npcs[move->slot], 0); s->npcs[move->slot].x = move->x; @@ -254,8 +273,8 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* s->npcs[move->slot].target_x = move->x; s->npcs[move->slot].target_y = move->y; col_stamp_npc_collision(s, &s->npcs[move->slot], 1); - col_lab_clear_transient(s); - col_lab_refresh_geometry(s); + col_lab_clear_transient(s, ctx); + col_lab_refresh_geometry(s, ctx); return; } @@ -263,8 +282,8 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* int slot = cmd->as.npc_slot.slot; col_lab_require_slot(slot); col_deactivate_npc(s, slot); - col_lab_clear_transient(s); - col_lab_refresh_geometry(s); + col_lab_clear_transient(s, ctx); + col_lab_refresh_geometry(s, ctx); return; } @@ -275,8 +294,8 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* col_lab_abort("cannot kill inactive npc slot %d", slot); s->npcs[slot].hp = 0; col_deactivate_npc(s, slot); - col_lab_clear_transient(s); - col_lab_refresh_geometry(s); + col_lab_clear_transient(s, ctx); + col_lab_refresh_geometry(s, ctx); return; } @@ -290,7 +309,7 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* if (hp > s->npcs[slot].max_hp) col_lab_abort("npc hp %d exceeds max hp %d", hp, s->npcs[slot].max_hp); s->npcs[slot].hp = hp; - col_lab_refresh_geometry(s); + col_lab_refresh_geometry(s, ctx); return; } @@ -302,7 +321,7 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* col_lab_abort("cannot set timer for inactive npc slot %d", slot); if (timer < 0) col_lab_abort("npc timer must be nonnegative"); s->npcs[slot].attack_timer = timer; - col_lab_refresh_geometry(s); + col_lab_refresh_geometry(s, ctx); return; } @@ -319,9 +338,9 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* s->wave_spawn_target = s->wave; s->wave_spawn_delay = 0; - col_spawn_wave(s); - col_lab_clear_transient(s); - col_lab_refresh_geometry(s); + col_spawn_wave(s, ctx); + col_lab_clear_transient(s, ctx); + col_lab_refresh_geometry(s, ctx); return; } @@ -347,12 +366,13 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* s->sol.boss_idx = idx; if (s->sol.phase < phase) { - while (s->sol.phase < phase) col_sol_enter_phase(s, s->sol.phase + 1); + while (s->sol.phase < phase) + col_sol_enter_phase(s, ctx, s->sol.phase + 1); s->sol.immobile_ticks = COLO_SOL_TRANSITION_FREEZE; s->sol.force_spear = 1; } s->sol.phase = phase; - col_lab_refresh_geometry(s); + col_lab_refresh_geometry(s, ctx); return; } @@ -384,7 +404,7 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* s->modifiers.tier[mod] = (uint8_t)tier; } if ((ColoModifier)mod == COLO_MOD_FRAILTY) col_mod_apply_frailty_hp(s); - col_lab_refresh_geometry(s); + col_lab_refresh_geometry(s, ctx); return; } @@ -403,7 +423,7 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* } case COLO_LAB_COMMAND_CLEAR_NPCS: - col_lab_clear_npcs(s); + col_lab_clear_npcs(s, ctx); return; case COLO_LAB_COMMAND_STEP_TICKS: { @@ -411,8 +431,9 @@ static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* if (ticks < 0) col_lab_abort("step_ticks must be nonnegative"); int actions[COLO_NUM_ACTION_HEADS] = {0}; for (int t = 0; t < ticks; t++) - col_step((EncounterState*)s, actions); - col_lab_refresh_geometry(s); + col_step_ctx( + (EncounterState*)s, (EncounterContext*)ctx, actions); + col_lab_refresh_geometry(s, ctx); return; } } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_parse.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_parse.inc index c02c12aaa5..6cbccb36b4 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_parse.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_parse.inc @@ -424,7 +424,10 @@ static ColoLabCommandKind col_lab_command_kind_for(const char* command) { } static ColoLabLineResult col_lab_apply_script_line_alloc_json( - ColosseumState* s, const char* line, char** out_json + ColosseumState* s, + ColosseumContext* ctx, + const char* line, + char** out_json ) { if (!out_json) col_lab_abort("json output pointer is required"); *out_json = NULL; @@ -450,7 +453,7 @@ static ColoLabLineResult col_lab_apply_script_line_alloc_json( } ColosseumLabCommand cmd = col_lab_build_script_command(kind, &args, lex.command); - col_lab_apply_command(s, &cmd); + col_lab_apply_command(s, ctx, &cmd); free(lex.buffer); return COLO_LAB_LINE_NONE; } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc index add0260e9f..f9e1f1a351 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc @@ -1,8 +1,13 @@ #line 76 "encounter_colosseum.h" -static void col_write_mask_ctx(EncounterState* state, EncounterContext* context, float* mask) { +static void col_write_mask_ctx( + EncounterState* state, + EncounterContext* context, + float* mask +) { ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); + encounter_arena_topology_require_finalized(ctx->route_topology); col_refresh_current_obs_slots_ctx(s, ctx); int offset = 0; @@ -10,7 +15,8 @@ static void col_write_mask_ctx(EncounterState* state, EncounterContext* context, for (int primary_action = 0; primary_action < COLO_PRIMARY_DIM; primary_action++) { - mask[offset++] = col_primary_action_valid(s, primary_action) ? 1.0f : 0.0f; + mask[offset++] = + col_primary_action_valid(s, ctx, primary_action) ? 1.0f : 0.0f; } mask[offset++] = 1.0f; @@ -167,16 +173,25 @@ static int col_npc_render_attack_anim_id( abort(); } -static void col_thrall_render_tile(const ColosseumState* s, int* out_x, int* out_y) { +static void col_thrall_render_tile( + ColosseumState* s, + const ColosseumContext* ctx, + int* out_x, + int* out_y +) { static const int RING[16][2] = { {0, 1}, {1, 0}, {-1, 0}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}, {0, 2}, {2, 0}, {-2, 0}, {0, -2}, {1, 2}, {2, 1}, {-1, 2}, {-2, 1} }; + ColoGeometryContext geometry = { + .state = s, + .context = ctx, + }; for (int a = 0; a < 16; a++) { int x = s->player.x + RING[a][0]; int y = s->player.y + RING[a][1]; - if (!col_npc_blocked((void*)s, x, y, 1)) { + if (!col_npc_blocked(&geometry, x, y, 1)) { *out_x = x; *out_y = y; return; @@ -223,7 +238,7 @@ static void col_fill_render_entities_ctx( int max_entities, int* count ) { - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); ColosseumState* s = (ColosseumState*)state; const EncounterLoadoutStats* ls = col_live_loadout_stats(s); s->player.gui_max_hit = ls->max_hit; @@ -306,7 +321,7 @@ static void col_fill_render_entities_ctx( if (s->thrall_active && n < max_entities) { int tx, ty; - col_thrall_render_tile(s, &tx, &ty); + col_thrall_render_tile(s, ctx, &tx, &ty); col_emit_decoration_entity(out, &n, max_entities, COLO_NPC_DEF_ID_THRALL, COLO_NPC_SLOT_THRALL, COLO_THRALL_RENDER_INSTANCE_ID, ctx->thrall_attacked_this_tick ? 11101 : -1, tx, ty, "Arceuus Thrall"); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index 0d7816872f..c01a21057f 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -111,138 +111,9 @@ static const int COLO_PILLARS[COLO_NUM_PILLARS][2] = { {8, 8}, {23, 8}, {8, 23}, {23, 23}, }; -static uint8_t COLO_STATIC_BLOCKED[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; - #define COLO_ARENA_TILE_COUNT (COLO_ARENA_WIDTH * COLO_ARENA_HEIGHT) -#define COLO_STATIC_LOS_TABLE_BITS (COLO_ARENA_TILE_COUNT * COLO_ARENA_TILE_COUNT) -#define COLO_STATIC_LOS_TABLE_WORDS ((COLO_STATIC_LOS_TABLE_BITS + 63) / 64) -#define COLO_STATIC_FOOTPRINT_MAX_SIZE 5 -#define COLO_STATIC_FOOTPRINT_PAD (COLO_STATIC_FOOTPRINT_MAX_SIZE - 1) -#define COLO_STATIC_FOOTPRINT_MIN_X (COLO_ARENA_MIN_X - COLO_STATIC_FOOTPRINT_PAD) -#define COLO_STATIC_FOOTPRINT_MIN_Y (COLO_ARENA_MIN_Y - COLO_STATIC_FOOTPRINT_PAD) -#define COLO_STATIC_FOOTPRINT_MAX_X (COLO_ARENA_MAX_X + 1) -#define COLO_STATIC_FOOTPRINT_MAX_Y (COLO_ARENA_MAX_Y + 1) -#define COLO_STATIC_FOOTPRINT_WIDTH \ - (COLO_STATIC_FOOTPRINT_MAX_X - COLO_STATIC_FOOTPRINT_MIN_X + 1) -#define COLO_STATIC_FOOTPRINT_HEIGHT \ - (COLO_STATIC_FOOTPRINT_MAX_Y - COLO_STATIC_FOOTPRINT_MIN_Y + 1) -static uint64_t COLO_STATIC_LOS_BITS[COLO_STATIC_LOS_TABLE_WORDS]; -static uint8_t COLO_STATIC_FOOTPRINT_BLOCKED[COLO_STATIC_FOOTPRINT_MAX_SIZE] - [COLO_STATIC_FOOTPRINT_WIDTH][COLO_STATIC_FOOTPRINT_HEIGHT]; -static int COLO_STATIC_LOOKUP_TABLES_BUILT = 0; -static int COLO_STATIC_ARENA_BUILT = 0; static int COLO_NPC_STATS_BUILT = 0; -static void col_build_static_lookup_tables_once(void); - -/* Derived purely from compile-time tables, so build it once. Rebuilding per reset - zeroed a grid that every other env thread paths against, which handed them an - arena with no walls for the width of the rebuild. puf_init runs serially, so the - single build lands before any worker thread exists. */ -static void col_build_static_arena(void) { - if (COLO_STATIC_ARENA_BUILT) return; - COLO_STATIC_ARENA_BUILT = 1; - memset(COLO_STATIC_BLOCKED, 0, sizeof(COLO_STATIC_BLOCKED)); - for (int y = 0; y < COLO_ARENA_HEIGHT; y++) { - for (int sp = 0; sp < COLO_WALL_SPANS_PER_ROW; sp++) { - ColoWallSpan span = COLO_WALL_SPANS[y][sp]; - for (int x = span.lo; x < span.hi; x++) COLO_STATIC_BLOCKED[x][y] = 1; - } - } - for (int p = 0; p < COLO_NUM_PILLARS; p++) - for (int dx = 0; dx < COLO_PILLAR_SIZE; dx++) - for (int dy = 0; dy < COLO_PILLAR_SIZE; dy++) - COLO_STATIC_BLOCKED[COLO_PILLARS[p][0] + dx][COLO_PILLARS[p][1] + dy] = 1; - col_build_static_lookup_tables_once(); -} - -static inline int col_static_blocked(int x, int y) { - if (x < COLO_ARENA_MIN_X || x > COLO_ARENA_MAX_X || - y < COLO_ARENA_MIN_Y || y > COLO_ARENA_MAX_Y) return 1; - return COLO_STATIC_BLOCKED[x][y]; -} - -static inline int col_static_los_endpoint_in_bounds(int x, int y) { - return x >= COLO_ARENA_MIN_X && x <= COLO_ARENA_MAX_X && - y >= COLO_ARENA_MIN_Y && y <= COLO_ARENA_MAX_Y; -} - -static inline uint32_t col_static_los_index(int x0, int y0, int x1, int y1) { - uint32_t a = (uint32_t)((x0 - COLO_ARENA_MIN_X) * COLO_ARENA_HEIGHT + - (y0 - COLO_ARENA_MIN_Y)); - uint32_t b = (uint32_t)((x1 - COLO_ARENA_MIN_X) * COLO_ARENA_HEIGHT + - (y1 - COLO_ARENA_MIN_Y)); - return a * COLO_ARENA_TILE_COUNT + b; -} - -static inline void col_static_los_set(uint32_t idx) { - COLO_STATIC_LOS_BITS[idx >> 6] |= 1ULL << (idx & 63); -} - -static inline int col_static_los_get(int x0, int y0, int x1, int y1) { - uint32_t idx = col_static_los_index(x0, y0, x1, y1); - return (int)((COLO_STATIC_LOS_BITS[idx >> 6] >> (idx & 63)) & 1ULL); -} - -static inline int col_static_footprint_anchor_in_table(int x, int y) { - return x >= COLO_STATIC_FOOTPRINT_MIN_X && x <= COLO_STATIC_FOOTPRINT_MAX_X && - y >= COLO_STATIC_FOOTPRINT_MIN_Y && y <= COLO_STATIC_FOOTPRINT_MAX_Y; -} - -static int col_static_footprint_blocked_reference_slowpath(int x, int y, int size) { - for (int dx = 0; dx < size; dx++) { - for (int dy = 0; dy < size; dy++) { - if (col_static_blocked(x + dx, y + dy)) return 1; - } - } - return 0; -} - -static inline int col_static_footprint_blocked_lookup(int x, int y, int size) { - if (size < 1 || size > COLO_STATIC_FOOTPRINT_MAX_SIZE) - return col_static_footprint_blocked_reference_slowpath(x, y, size); - if (!COLO_STATIC_LOOKUP_TABLES_BUILT) col_build_static_arena(); - if (!col_static_footprint_anchor_in_table(x, y)) return 1; - return COLO_STATIC_FOOTPRINT_BLOCKED[size - 1] - [x - COLO_STATIC_FOOTPRINT_MIN_X] - [y - COLO_STATIC_FOOTPRINT_MIN_Y]; -} - -static int col_los_tile_blocked_reference(void* ctx, int x, int y) { - (void)ctx; - return col_static_blocked(x, y); -} - -static int col_tiles_have_los_reference_slowpath(int x0, int y0, int x1, int y1) { - OsrsLosQuery query = osrs_los_tile(col_los_tile_blocked_reference, NULL); - return osrs_los_tile_ray_clear(&query, x0, y0, x1, y1); -} - -static void col_build_static_lookup_tables_once(void) { - if (COLO_STATIC_LOOKUP_TABLES_BUILT) return; - for (int x0 = COLO_ARENA_MIN_X; x0 <= COLO_ARENA_MAX_X; x0++) { - for (int y0 = COLO_ARENA_MIN_Y; y0 <= COLO_ARENA_MAX_Y; y0++) { - for (int x1 = COLO_ARENA_MIN_X; x1 <= COLO_ARENA_MAX_X; x1++) { - for (int y1 = COLO_ARENA_MIN_Y; y1 <= COLO_ARENA_MAX_Y; y1++) { - if (col_tiles_have_los_reference_slowpath(x0, y0, x1, y1)) - col_static_los_set(col_static_los_index(x0, y0, x1, y1)); - } - } - } - } - for (int size = 1; size <= COLO_STATIC_FOOTPRINT_MAX_SIZE; size++) { - for (int x = COLO_STATIC_FOOTPRINT_MIN_X; x <= COLO_STATIC_FOOTPRINT_MAX_X; x++) { - for (int y = COLO_STATIC_FOOTPRINT_MIN_Y; y <= COLO_STATIC_FOOTPRINT_MAX_Y; y++) { - COLO_STATIC_FOOTPRINT_BLOCKED[size - 1] - [x - COLO_STATIC_FOOTPRINT_MIN_X] - [y - COLO_STATIC_FOOTPRINT_MIN_Y] = - (uint8_t)col_static_footprint_blocked_reference_slowpath(x, y, size); - } - } - } - COLO_STATIC_LOOKUP_TABLES_BUILT = 1; -} - #define COLO_NUM_SPAWN_ANCHORS 13 #define COLO_SPAWN_ZONE_SIZE 3 static const int COLO_SPAWN_ANCHORS[COLO_NUM_SPAWN_ANCHORS][2] = { @@ -457,7 +328,6 @@ static const ColoNpcOverlay COLO_NPC_OVERLAY[COLO_NUM_NPC_TYPES] = { static ColoNpcStats COLO_NPC_STATS[COLO_NUM_NPC_TYPES]; static void col_build_npc_stats(void) { - col_build_static_arena(); if (COLO_NPC_STATS_BUILT) return; COLO_NPC_STATS_BUILT = 1; for (int i = 0; i < COLO_NUM_NPC_TYPES; i++) { diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc index b87a7027ab..cdc3988783 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc @@ -404,19 +404,29 @@ static void col_mod_tick_poison(ColosseumState* s) { } } -static int col_hazard_tile_free(const ColosseumState* s, int x, int y) { - if (col_static_blocked(x, y)) return 0; +static int col_hazard_tile_free( + const ColosseumState* s, + const EncounterArenaTopology* topology, + int x, + int y +) { + if (encounter_arena_topology_tile_blocked(topology, x, y)) return 0; if (col_sol_clamp_active(s) && !col_in_boss_arena(s, x, y)) return 0; return 1; } static int col_spawn_hazard_npc_beside( - ColosseumState* s, ColoNpcType type, const ColoNPC* owner + ColosseumState* s, + const EncounterArenaTopology* topology, + ColoNpcType type, + const ColoNPC* owner ) { if (col_find_free_npc(s) < 0) return -1; int size = col_npc_effective_size(owner); - if (col_hazard_tile_free(s, owner->x - 1, owner->y - 1)) - return col_spawn_npc_at(s, type, owner->x - 1, owner->y - 1); + if (col_hazard_tile_free( + s, topology, owner->x - 1, owner->y - 1)) + return col_spawn_npc_at( + s, type, owner->x - 1, owner->y - 1); for (int ring = 0; ring < 2; ring++) { int lo_x = owner->x - 1 - ring, hi_x = owner->x + size + ring; @@ -425,7 +435,7 @@ static int col_spawn_hazard_npc_beside( for (int x = lo_x; x <= hi_x; x++) { int on_shell = (x == lo_x || x == hi_x || y == lo_y || y == hi_y); if (!on_shell) continue; - if (!col_hazard_tile_free(s, x, y)) continue; + if (!col_hazard_tile_free(s, topology, x, y)) continue; return col_spawn_npc_at(s, type, x, y); } } @@ -502,9 +512,13 @@ static void col_mod_on_owner_death(ColosseumState* s, int owner_slot) { } } -static void col_totem_place(ColosseumState* s, ColoTotem* tot) { +static void col_totem_place( + ColosseumState* s, + const EncounterArenaTopology* topology, + ColoTotem* tot +) { int slot = col_spawn_hazard_npc_beside( - s, COLO_HEALING_TOTEM, &s->npcs[tot->owner_slot]); + s, topology, COLO_HEALING_TOTEM, &s->npcs[tot->owner_slot]); if (slot < 0) { tot->phase = COLO_HAZARD_RESPAWNING; tot->npc_slot = -1; @@ -518,10 +532,14 @@ static void col_totem_place(ColosseumState* s, ColoTotem* tot) { tot->projectile_timer = 0; } -static int col_totem_spawn_for_owner(ColosseumState* s, int owner_slot) { +static int col_totem_spawn_for_owner( + ColosseumState* s, + const EncounterArenaTopology* topology, + int owner_slot +) { ColoTotem* tot = col_totem_alloc(s, owner_slot); if (!tot) return 0; - col_totem_place(s, tot); + col_totem_place(s, topology, tot); return 1; } @@ -551,19 +569,26 @@ static int col_live_totem_count(const ColosseumState* s) { return n; } -static void col_mod_on_npc_hp_changed(ColosseumState* s, int npc_slot) { +static void col_mod_on_npc_hp_changed( + ColosseumState* s, + const EncounterArenaTopology* topology, + int npc_slot +) { if (!col_mod_active(s, COLO_MOD_TOTEMIC)) return; const ColoNPC* npc = &s->npcs[npc_slot]; if (!npc->active || npc->hp <= 0 || npc->max_hp <= 0) return; if (col_type_is_hazard_entity(npc->type)) return; if (npc->hp * 2 > npc->max_hp) return; if (col_totem_for_owner(s, npc_slot)) return; - if (!col_totem_spawn_for_owner(s, npc_slot)) return; + if (!col_totem_spawn_for_owner(s, topology, npc_slot)) return; if (npc->type == COLO_SOL_HEREDIT) s->sol_totem_spawn_timer = COLO_TOTEM_SOL_EXTRA_INTERVAL; } -static void col_mod_tick_sol_extra_totems(ColosseumState* s) { +static void col_mod_tick_sol_extra_totems( + ColosseumState* s, + const EncounterArenaTopology* topology +) { if (s->sol_totem_spawn_timer <= 0) return; int sol_slot = col_find_sol_slot(s); if (sol_slot < 0) { @@ -573,11 +598,14 @@ static void col_mod_tick_sol_extra_totems(ColosseumState* s) { s->sol_totem_spawn_timer--; if (s->sol_totem_spawn_timer > 0) return; s->sol_totem_spawn_timer = COLO_TOTEM_SOL_EXTRA_INTERVAL; - col_totem_spawn_for_owner(s, sol_slot); + col_totem_spawn_for_owner(s, topology, sol_slot); } -static void col_mod_tick_totems(ColosseumState* s) { - col_mod_tick_sol_extra_totems(s); +static void col_mod_tick_totems( + ColosseumState* s, + const EncounterArenaTopology* topology +) { + col_mod_tick_sol_extra_totems(s, topology); for (int t = 0; t < COLO_MAX_TOTEMS; t++) { ColoTotem* tot = &s->totems[t]; @@ -592,7 +620,8 @@ static void col_mod_tick_totems(ColosseumState* s) { if (tot->phase == COLO_HAZARD_RESPAWNING) { if (tot->respawn_timer > 0) tot->respawn_timer--; - if (tot->respawn_timer == 0) col_totem_place(s, tot); + if (tot->respawn_timer == 0) + col_totem_place(s, topology, tot); continue; } @@ -634,16 +663,27 @@ static int col_mod_molten_pool_covered(const ColosseumState* s, int x, int y) { return 0; } -static int col_mod_molten_pool_available(const ColosseumState* s, int x, int y) { - return !col_static_blocked(x, y) && !col_mod_molten_pool_covered(s, x, y); +static int col_mod_molten_pool_available( + const ColosseumState* s, + const EncounterArenaTopology* topology, + int x, + int y +) { + return !encounter_arena_topology_tile_blocked(topology, x, y) && + !col_mod_molten_pool_covered(s, x, y); } -static ColoMoltenPoolTile col_mod_molten_pool_random_fallback(ColosseumState* s) { +static ColoMoltenPoolTile col_mod_molten_pool_random_fallback( + ColosseumState* s, + const EncounterArenaTopology* topology +) { ColoMoltenPoolTile candidates[COLO_ARENA_TILE_COUNT]; int count = 0; for (int x = COLO_ARENA_MIN_X; x <= COLO_ARENA_MAX_X; x++) { for (int y = COLO_ARENA_MIN_Y; y <= COLO_ARENA_MAX_Y; y++) { - if (!col_mod_molten_pool_available(s, x, y)) continue; + if (!col_mod_molten_pool_available( + s, topology, x, y)) + continue; candidates[count++] = (ColoMoltenPoolTile){ .x = x, .y = y }; } } @@ -653,22 +693,24 @@ static ColoMoltenPoolTile col_mod_molten_pool_random_fallback(ColosseumState* s) static ColoMoltenPoolTile col_mod_molten_pool_tile_or_fallback( ColosseumState* s, + const EncounterArenaTopology* topology, int x, int y ) { - if (col_mod_molten_pool_available(s, x, y)) + if (col_mod_molten_pool_available(s, topology, x, y)) return (ColoMoltenPoolTile){ .x = x, .y = y }; - return col_mod_molten_pool_random_fallback(s); + return col_mod_molten_pool_random_fallback(s, topology); } static void col_mod_add_molten_pool( ColosseumState* s, + const EncounterArenaTopology* topology, int x, int y, ColoMoltenPoolKind kind, ColoMoltenPoolLifetime lifetime ) { - if (col_static_blocked(x, y)) return; + if (encounter_arena_topology_tile_blocked(topology, x, y)) return; for (int i = 0; i < s->molten_count; i++) { if (s->molten_x[i] != x || s->molten_y[i] != y) continue; s->molten_kind[i] |= kind; @@ -756,20 +798,30 @@ static void col_mod_tick_molten_pools(ColosseumState* s) { } } -static void col_mod_reentry_on_skyfall(ColosseumState* s, int tile_x, int tile_y) { +static void col_mod_reentry_on_skyfall( + ColosseumState* s, + const EncounterArenaTopology* topology, + int tile_x, + int tile_y +) { int tier = col_mod_tier(s, COLO_MOD_REENTRY); if (tier == 0) return; ColoMoltenPoolLifetime lifetime = tier >= 2 ? COLO_POOL_PERMANENT : COLO_POOL_TEMPORARY; - col_mod_add_molten_pool(s, tile_x, tile_y, COLO_POOL_REENTRY, lifetime); + col_mod_add_molten_pool( + s, topology, tile_x, tile_y, COLO_POOL_REENTRY, lifetime); if (tier >= 2) { ColoMoltenPoolTile sw = col_mod_molten_pool_tile_or_fallback( - s, tile_x - 1, tile_y - 1); - col_mod_add_molten_pool(s, sw.x, sw.y, COLO_POOL_REENTRY, COLO_POOL_PERMANENT); + s, topology, tile_x - 1, tile_y - 1); + col_mod_add_molten_pool( + s, topology, sw.x, sw.y, + COLO_POOL_REENTRY, COLO_POOL_PERMANENT); } if (tier >= 3) { ColoMoltenPoolTile west = col_mod_molten_pool_tile_or_fallback( - s, tile_x - 1, tile_y); - col_mod_add_molten_pool(s, west.x, west.y, COLO_POOL_REENTRY, COLO_POOL_PERMANENT); + s, topology, tile_x - 1, tile_y); + col_mod_add_molten_pool( + s, topology, west.x, west.y, + COLO_POOL_REENTRY, COLO_POOL_PERMANENT); } } @@ -804,7 +856,13 @@ static int col_volatility_player_in_blast( x, y, size, tier, s->player.x, s->player.y); } -static void col_mod_volatility_on_death(ColosseumState* s, int x, int y, int size) { +static void col_mod_volatility_on_death( + ColosseumState* s, + const EncounterArenaTopology* topology, + int x, + int y, + int size +) { int tier = col_mod_tier(s, COLO_MOD_VOLATILITY); if (tier == 0) return; if (col_volatility_player_in_blast(s, x, y, size, tier)) { @@ -820,7 +878,8 @@ static void col_mod_volatility_on_death(ColosseumState* s, int x, int y, int siz ColoMoltenPoolLifetime lifetime = col_mod_tier(s, COLO_MOD_REENTRY) >= 2 ? COLO_POOL_PERMANENT : COLO_POOL_TEMPORARY; col_mod_add_molten_pool( - s, x + size / 2, y + size / 2, COLO_POOL_VOLATILITY, lifetime); + s, topology, x + size / 2, y + size / 2, + COLO_POOL_VOLATILITY, lifetime); } } @@ -1059,7 +1118,10 @@ static void col_mod_tick_solarflare(ColosseumState* s) { col_mod_apply_solarflare_contact(s, tier); } -static void col_modifiers_tick(ColosseumState* s) { +static void col_modifiers_tick( + ColosseumState* s, + const EncounterArenaTopology* topology +) { #ifdef COLO_PROFILE_ENABLED int col_prof_enabled = COLO_PROFILE_ENABLED() && !s->profiling_suppressed; @@ -1070,7 +1132,7 @@ static void col_modifiers_tick(ColosseumState* s) { col_mod_tick_molten_pools(s); col_mod_tick_venom(s); col_mod_tick_poison(s); - col_mod_tick_totems(s); + col_mod_tick_totems(s, topology); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_STEP_MODIFIERS_HAZARDS); #endif diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc index e500131bef..3507f72d66 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc @@ -1,134 +1,176 @@ #line 76 "encounter_colosseum.h" -static int col_player_walkable(void* ctx, int x, int y) { - ColosseumState* s = (ColosseumState*)ctx; - if (col_static_blocked(x, y)) return 0; - if (col_sol_clamp_active(s) && !col_in_boss_arena(s, x, y)) return 0; +typedef struct { + ColosseumState* state; + const ColosseumContext* context; +} ColoGeometryContext; + +static int col_player_walkable_ctx( + const ColosseumState* state, + const ColosseumContext* context, + int x, + int y +) { + if (col_topology_tile_blocked(context, x, y)) return 0; + if (col_sol_clamp_active(state) && !col_in_boss_arena(state, x, y)) + return 0; return 1; } -static int col_los_tile_blocked(void* ctx, int x, int y) { - (void)ctx; - return col_static_blocked(x, y); +static int col_player_walkable(void* data, int x, int y) { + const ColoGeometryContext* geometry = + (const ColoGeometryContext*)data; + return col_player_walkable_ctx( + geometry->state, geometry->context, x, y); } -static int col_tiles_have_los(const ColosseumState* s, int x0, int y0, int x1, int y1) { - (void)s; - if (!COLO_STATIC_LOOKUP_TABLES_BUILT) col_build_static_arena(); - if (col_static_los_endpoint_in_bounds(x0, y0) && - col_static_los_endpoint_in_bounds(x1, y1)) - return col_static_los_get(x0, y0, x1, y1); - return col_tiles_have_los_reference_slowpath(x0, y0, x1, y1); +static int col_topology_los_tile_blocked(void* data, int x, int y) { + const ColosseumContext* context = (const ColosseumContext*)data; + return col_topology_tile_blocked(context, x, y); } -static OsrsLosQuery col_player_los_query(ColosseumState* s) { - return osrs_los_tile(col_los_tile_blocked, s); +static OsrsLosQuery col_player_los_query(const ColosseumContext* context) { + return osrs_los_tile( + col_topology_los_tile_blocked, (void*)context); } -static int col_npc_has_los_to_player(const ColosseumState* s, const ColoNPC* npc) { +static int col_npc_has_los_to_player( + const ColosseumState* state, + const ColosseumContext* context, + const ColoNPC* npc +) { int size = npc->size > 0 ? npc->size : COLO_NPC_STATS[npc->type].size; - int sx = s->player.x, sy = s->player.y; - if (sx < npc->x) sx = npc->x; - if (sx > npc->x + size - 1) sx = npc->x + size - 1; - if (sy < npc->y) sy = npc->y; - if (sy > npc->y + size - 1) sy = npc->y + size - 1; - return col_tiles_have_los(s, sx, sy, s->player.x, s->player.y); + return col_topology_los_clear( + context, + npc->x, + npc->y, + size, + state->player.x, + state->player.y, + 1, + 0); } -static int col_npc_blocked_ignore_player(void* ctx, int x, int y, int size) { - ColosseumState* s = (ColosseumState*)ctx; - s->log.npc_blocked_calls += 1.0f; - s->log.npc_blocked_tiles += (float)(size * size); - if (col_static_footprint_blocked_lookup(x, y, size)) return 1; - int sol_clamp_active = col_sol_clamp_active(s); +static int col_npc_blocked_ignore_player( + void* data, + int x, + int y, + int size +) { + ColoGeometryContext* geometry = (ColoGeometryContext*)data; + ColosseumState* state = geometry->state; + state->log.npc_blocked_calls += 1.0f; + state->log.npc_blocked_tiles += (float)(size * size); + if (col_topology_footprint_blocked( + geometry->context, x, y, size)) + return 1; + int sol_clamp_active = col_sol_clamp_active(state); for (int dx = 0; dx < size; dx++) { for (int dy = 0; dy < size; dy++) { - if (sol_clamp_active && !col_in_boss_arena(s, x + dx, y + dy)) return 1; - int gx, gy; - if (!col_grid_index(x + dx, y + dy, &gx, &gy)) return 1; - if (s->npc_collision_flags[gx][gy]) return 1; + if (sol_clamp_active && + !col_in_boss_arena(state, x + dx, y + dy)) + return 1; + int grid_x, grid_y; + if (!col_grid_index(x + dx, y + dy, &grid_x, &grid_y)) + return 1; + if (state->npc_collision_flags[grid_x][grid_y]) return 1; } } return 0; } -static int col_npc_blocked(void* ctx, int x, int y, int size) { - ColosseumState* s = (ColosseumState*)ctx; - if (col_npc_blocked_ignore_player(ctx, x, y, size)) return 1; +static int col_npc_blocked(void* data, int x, int y, int size) { + ColoGeometryContext* geometry = (ColoGeometryContext*)data; + ColosseumState* state = geometry->state; + if (col_npc_blocked_ignore_player(data, x, y, size)) return 1; for (int dx = 0; dx < size; dx++) { for (int dy = 0; dy < size; dy++) { - int gx, gy; - if (!col_grid_index(x + dx, y + dy, &gx, &gy)) return 1; - if (gx == s->player_grid_x && gy == s->player_grid_y) return 1; + int grid_x, grid_y; + if (!col_grid_index(x + dx, y + dy, &grid_x, &grid_y)) + return 1; + if (grid_x == state->player_grid_x && + grid_y == state->player_grid_y) + return 1; } } return 0; } typedef struct { - ColosseumState* s; + ColoGeometryContext geometry; int self_idx; } ColoNpcMoveCtx; -static int col_npc_move_blocked_ignore_player(void* ctx, int x, int y, int size) { - ColoNpcMoveCtx* move_ctx = (ColoNpcMoveCtx*)ctx; - return col_npc_blocked_ignore_player(move_ctx->s, x, y, size); +static int col_npc_move_blocked_ignore_player( + void* data, + int x, + int y, + int size +) { + ColoNpcMoveCtx* move = (ColoNpcMoveCtx*)data; + return col_npc_blocked_ignore_player( + &move->geometry, x, y, size); } -static int col_npc_move_overlap_hold(void* ctx) { - ColoNpcMoveCtx* move_ctx = (ColoNpcMoveCtx*)ctx; - ColosseumState* s = move_ctx->s; - return s->player_last_interaction_age == 0 && - s->player_last_interaction_target_slot == move_ctx->self_idx; +static int col_npc_move_overlap_hold(void* data) { + ColoNpcMoveCtx* move = (ColoNpcMoveCtx*)data; + ColosseumState* state = move->geometry.state; + return state->player_last_interaction_age == 0 && + state->player_last_interaction_target_slot == move->self_idx; } typedef struct { - ColosseumState* s; - const EncounterArenaTopology* topology; + ColoGeometryContext geometry; int size; int ignore_npcs; } ColoNpcPathCtx; static ColoNpcPathCtx col_npc_path_ctx_begin( - ColosseumState* s, - const EncounterArenaTopology* topology, + ColosseumState* state, + const ColosseumContext* context, int size ) { - if (!topology) { - fprintf(stderr, "colosseum NPC route topology is not bound\n"); - abort(); - } + encounter_arena_topology_require_finalized(context->route_topology); return (ColoNpcPathCtx){ - .s = s, - .topology = topology, + .geometry = { + .state = state, + .context = context, + }, .size = size, }; } -static int col_npc_path_blocked_uncached(ColoNpcPathCtx* pc, int x, int y) { - ColosseumState* s = pc->s; - if (s->player.x >= x && s->player.x < x + pc->size && - s->player.y >= y && s->player.y < y + pc->size) return 1; - if (pc->ignore_npcs) { - for (int dx = 0; dx < pc->size; dx++) { - for (int dy = 0; dy < pc->size; dy++) { - int tx = x + dx, ty = y + dy; - if (col_static_blocked(tx, ty)) return 1; - if (col_sol_clamp_active(s) && !col_in_boss_arena(s, tx, ty)) return 1; - } - } +static int col_npc_path_blocked_uncached( + ColoNpcPathCtx* path, + int x, + int y +) { + ColosseumState* state = path->geometry.state; + if (state->player.x >= x && state->player.x < x + path->size && + state->player.y >= y && state->player.y < y + path->size) + return 1; + if (path->ignore_npcs) { + if (col_topology_footprint_blocked( + path->geometry.context, x, y, path->size)) + return 1; + if (!col_sol_clamp_active(state)) return 0; + for (int dx = 0; dx < path->size; dx++) + for (int dy = 0; dy < path->size; dy++) + if (!col_in_boss_arena(state, x + dx, y + dy)) + return 1; return 0; } - return col_npc_blocked_ignore_player(s, x, y, pc->size); + return col_npc_blocked_ignore_player( + &path->geometry, x, y, path->size); } -static int col_npc_path_blocked(void* ctx, int x, int y, int size) { - ColoNpcPathCtx* pc = (ColoNpcPathCtx*)ctx; - if (size != pc->size) { +static int col_npc_path_blocked(void* data, int x, int y, int size) { + ColoNpcPathCtx* path = (ColoNpcPathCtx*)data; + if (size != path->size) { fprintf(stderr, "colosseum NPC route actor size changed\n"); abort(); } - return col_npc_path_blocked_uncached(pc, x, y); + return col_npc_path_blocked_uncached(path, x, y); } static int col_npc_path_step_with_ctx( @@ -145,11 +187,12 @@ static int col_npc_path_step_with_ctx( if (*x == tx && *y == ty) return 0; #ifdef COLO_PROFILE_ENABLED int col_prof_enabled = - COLO_PROFILE_ENABLED() && !pc->s->profiling_suppressed; + COLO_PROFILE_ENABLED() && + !pc->geometry.state->profiling_suppressed; double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; #endif EncounterRouteInput input = { - .topology = pc->topology, + .topology = pc->geometry.context->route_topology, .blockers = { .is_blocked = col_npc_path_blocked, .ctx = pc, @@ -183,14 +226,14 @@ static int col_npc_path_step_with_ctx( static int col_npc_path_step( ColosseumState* s, - const EncounterArenaTopology* topology, + const ColosseumContext* context, int* x, int* y, int size, int tx, int ty ) { - ColoNpcPathCtx pc = col_npc_path_ctx_begin(s, topology, size); + ColoNpcPathCtx pc = col_npc_path_ctx_begin(s, context, size); return col_npc_path_step_with_ctx(&pc, x, y, tx, ty); } @@ -229,8 +272,7 @@ static void col_warband_move( npc->target_x = s->player.x; npc->target_y = s->player.y; - ColoNpcPathCtx pc = - col_npc_path_ctx_begin(s, ctx->route_topology, 1); + ColoNpcPathCtx pc = col_npc_path_ctx_begin(s, ctx, 1); pc.ignore_npcs = 1; int tx, ty; col_warband_pick_target(s, &pc, npc, &tx, &ty); @@ -272,7 +314,8 @@ static void col_npc_move_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) int is_ranged = st->attack_range > 1; if (is_ranged) { int dist = encounter_dist_to_npc(s->player.x, s->player.y, npc->x, npc->y, size); - if (dist >= 1 && dist <= st->attack_range && col_npc_has_los_to_player(s, npc)) { + if (dist >= 1 && dist <= st->attack_range && + col_npc_has_los_to_player(s, ctx, npc)) { npc->moved_this_tick = 0; return; } @@ -283,7 +326,13 @@ static void col_npc_move_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) } col_stamp_npc_collision(s, npc, 0); - ColoNpcMoveCtx move_ctx = { s, idx }; + ColoNpcMoveCtx move_ctx = { + .geometry = { + .state = s, + .context = ctx, + }, + .self_idx = idx, + }; int moved; if (npc->type == COLO_MINOTAUR && col_mod_active(s, COLO_MOD_RED_FLAG)) { @@ -291,8 +340,7 @@ static void col_npc_move_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) moved = 0; if (encounter_dist_to_npc(tx, ty, npc->x, npc->y, size) > 1) moved = col_npc_path_step( - s, ctx->route_topology, - &npc->x, &npc->y, size, tx - 1, ty - 1); + s, ctx, &npc->x, &npc->y, size, tx - 1, ty - 1); } else { EncounterNpcStepPolicy step_policy = is_ranged ? ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc index fca835b464..3fa9970cb4 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -312,6 +312,7 @@ static ColoNpcNextPrayerObs col_visible_manticore_next_prayer(const ColoNPC* npc static int col_npc_current_attack_geometry( const ColosseumState* s, + const ColosseumContext* ctx, const ColoNPC* npc, const ColoNpcStats* ns ) { @@ -322,7 +323,7 @@ static int col_npc_current_attack_geometry( if (ns->attack_range > 1) { int dist = col_npc_dist_to_player(s, npc); if (dist < 1 || dist > ns->attack_range) return 0; - return col_npc_has_los_to_player(s, npc); + return col_npc_has_los_to_player(s, ctx, npc); } return col_npc_melee_adjacent_to_player(s, npc); } @@ -340,6 +341,7 @@ static int col_warband_next_window_wait(const ColosseumState* s, const ColoNPC* static ColoNpcNextPrayerObs col_deterministic_npc_next_prayer( const ColosseumState* s, + const ColosseumContext* ctx, const ColoNPC* npc, const ColoNpcStats* ns ) { @@ -349,7 +351,8 @@ static ColoNpcNextPrayerObs col_deterministic_npc_next_prayer( if (npc->type == COLO_JAVELIN_COLOSSUS && (npc->type_state.javelin.attack_count + 1) % 5 == 0) return col_npc_next_prayer_none(); - if (!col_npc_current_attack_geometry(s, npc, ns)) return col_npc_next_prayer_none(); + if (!col_npc_current_attack_geometry(s, ctx, npc, ns)) + return col_npc_next_prayer_none(); int ticks = npc->attack_timer == 0 ? 1 : npc->attack_timer; if (col_type_is_warbander(npc->type)) { if (s->warband_cycle_anchor < 0) return col_npc_next_prayer_none(); @@ -364,13 +367,15 @@ static ColoNpcNextPrayerObs col_deterministic_npc_next_prayer( static ColoNpcNextPrayerObs col_npc_next_prayer_obs( const ColosseumState* s, + const ColosseumContext* ctx, const ColoNPC* npc, int npc_slot ) { (void)npc_slot; ColoNpcNextPrayerObs manticore = col_visible_manticore_next_prayer(npc); if (manticore.active) return manticore; - return col_deterministic_npc_next_prayer(s, npc, &COLO_NPC_STATS[npc->type]); + return col_deterministic_npc_next_prayer( + s, ctx, npc, &COLO_NPC_STATS[npc->type]); } static const ColoBeeSwarm* col_obs_bee_state_for_npc_slot( @@ -480,6 +485,7 @@ static void col_write_npc_tells(const ColosseumState* s, const ColoNPC* npc, flo static void col_write_npc_slot( const ColosseumState* s, + const ColosseumContext* ctx, const ColoNPC* npc, int npc_slot, const ColoVenatorPreviewTargets* venator_targets, @@ -505,14 +511,15 @@ static void col_write_npc_slot( obs[i++] = (ns->attack_range > 0 && dist >= 1 && dist <= ns->attack_range) ? 1.0f : 0.0f; - ColoNpcNextPrayerObs next_prayer = col_npc_next_prayer_obs(s, npc, npc_slot); + ColoNpcNextPrayerObs next_prayer = + col_npc_next_prayer_obs(s, ctx, npc, npc_slot); obs[i++] = next_prayer.active ? 1.0f : 0.0f; *idx = i; col_write_style_onehot_melee_ranged_magic( next_prayer.active ? next_prayer.style : ATTACK_STYLE_NONE, obs, idx); i = *idx; obs[i++] = next_prayer.active ? col_clamp01((float)next_prayer.ticks / 10.0f) : 0.0f; - obs[i++] = col_npc_has_los_to_player(s, npc) ? 1.0f : 0.0f; + obs[i++] = col_npc_has_los_to_player(s, ctx, npc) ? 1.0f : 0.0f; assert(i - slot_base == COLO_NPC_TELLS_OFFSET); *idx = i; col_write_npc_tells(s, npc, obs, idx); @@ -570,6 +577,7 @@ static int col_premove_molten_forecast_needed( static int col_build_post_premove_molten_forecast( const ColosseumState* s, + const ColosseumContext* ctx, ColosseumState* forecast ) { *forecast = *s; @@ -582,7 +590,8 @@ static int col_build_post_premove_molten_forecast( const ColoJavelinState* jv = &npc->type_state.javelin; if (!jv->skyfall_pending || jv->skyfall_timer > 1) continue; col_mod_reentry_on_skyfall( - forecast, jv->skyfall_tile_x, jv->skyfall_tile_y); + forecast, ctx->route_topology, + jv->skyfall_tile_x, jv->skyfall_tile_y); } int burned = 0; @@ -1004,10 +1013,16 @@ static void col_write_modifier_hazards( *idx = i; } -static void col_write_threat_los_counts(const ColosseumState* s, float* obs, int* idx) { +static void col_write_threat_los_counts( + const ColosseumState* s, + const ColosseumContext* ctx, + float* obs, + int* idx +) { int i = *idx; int magic, ranged, melee; - col_live_threat_style_counts(s, &magic, &ranged, &melee); + col_live_threat_style_counts( + s, ctx, &magic, &ranged, &melee); obs[i++] = col_clamp01((float)magic / 4.0f); obs[i++] = col_clamp01((float)ranged / 4.0f); obs[i++] = col_clamp01((float)melee / 4.0f); @@ -1578,9 +1593,14 @@ static void col_write_inventory_and_equipped_obs( *idx = i; } -static void col_write_obs_ctx(EncounterState* state, EncounterContext* context, float* obs) { +static void col_write_obs_ctx( + EncounterState* state, + EncounterContext* context, + float* obs +) { ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); + encounter_arena_topology_require_finalized(ctx->route_topology); #ifdef COLO_PROFILE_ENABLED int col_prof_enabled = COLO_PROFILE_ENABLED(); double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; @@ -1591,7 +1611,7 @@ static void col_write_obs_ctx(EncounterState* state, EncounterContext* context, if (col_premove_molten_forecast_needed(s)) { premove_molten_burn = col_build_post_premove_molten_forecast( - s, &post_premove_molten); + s, ctx, &post_premove_molten); } #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_REFRESH_SLOTS); @@ -1665,7 +1685,8 @@ static void col_write_obs_ctx(EncounterState* state, EncounterContext* context, continue; } col_write_npc_slot( - s, &s->npcs[npc_idx], npc_idx, &venator_targets, obs, &i); + s, ctx, &s->npcs[npc_idx], npc_idx, + &venator_targets, obs, &i); } assert(i == COLO_OBS_AFTER_NPCS); #ifdef COLO_PROFILE_ENABLED @@ -1724,7 +1745,7 @@ static void col_write_obs_ctx(EncounterState* state, EncounterContext* context, COLO_PROFILE_MARK(COLO_PROF_OBS_PENDING_HITS); #endif - col_write_threat_los_counts(s, obs, &i); + col_write_threat_los_counts(s, ctx, obs, &i); assert(i == COLO_OBS_AFTER_THREAT_LOS); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_THREAT_LOS); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc index f36fe83cbd..81c4b12f62 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -510,7 +510,7 @@ typedef struct { static int col_tile_walkable(void* ctx, int x, int y) { ColoWalkCtx* wc = (ColoWalkCtx*)ctx; - return col_player_walkable(wc->s, x, y); + return col_player_walkable_ctx(wc->s, wc->ctx, x, y); } static int col_player_route_blocked(void* data, int x, int y, int size) { @@ -984,9 +984,6 @@ static void col_player_attack_target_ctx( s->tick_scratch.attacks_fired++; } -static void col_player_attack_target(ColosseumState* s, int npc_slot) { - col_player_attack_target_ctx(s, col_legacy_context(), npc_slot); -} typedef struct { OsrsPlayerStepInput input; @@ -1037,7 +1034,7 @@ static ColoPrimaryStepPlan col_primary_step_plan( } *walk_ctx = (ColoWalkCtx){s, ctx}; - *los_query = col_player_los_query(s); + *los_query = col_player_los_query(ctx); return (ColoPrimaryStepPlan){ .input = { .player = player, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 1ef3d268c1..72343808ea 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -7,7 +7,7 @@ static void col_put_int_ctx( int value ) { ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); if (strcmp(key, "start_wave") == 0) { int internal = value - 1; if (internal < 0) internal = 0; @@ -83,7 +83,7 @@ static void col_put_float_ctx( float value ) { (void)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); if (strcmp(key, "damage_reward_coeff") == 0) ctx->config.damage_reward_coeff = value; else if (strcmp(key, "late_start_supply_fraction_per_wave") == 0) { if (value < 0.0f || value > 0.125f) { @@ -145,7 +145,7 @@ static void col_put_ptr_ctx( void* value ) { (void)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); if (strcmp(key, "collision_map") == 0) ctx->collision_map = (const CollisionMap*)value; else @@ -694,7 +694,11 @@ static void col_emit_player_attack_projectiles( s, ctx, ov, base_profile, p_start_delay + p_duration, 30); } -static void col_emit_thrall_projectile(ColosseumState* s, EncounterOverlay* ov) { +static void col_emit_thrall_projectile( + ColosseumState* s, + const ColosseumContext* ctx, + EncounterOverlay* ov +) { int slot = s->thrall_target_slot; if (!col_npc_is_thrall_target(s, slot)) return; @@ -702,7 +706,7 @@ static void col_emit_thrall_projectile(ColosseumState* s, EncounterOverlay* ov) int target_size = col_npc_effective_size(target); int src_x, src_y; - col_thrall_render_tile(s, &src_x, &src_y); + col_thrall_render_tile(s, ctx, &src_x, &src_y); int dist = encounter_projectile_distance( src_x, src_y, 1, target->x, target->y, target_size, @@ -812,7 +816,7 @@ static void col_render_post_tick_ctx( EncounterOverlay* ov ) { ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); ov->tile_shadow_count = 0; ov->floating_model_count = 0; ov->active_modifier_count = 0; @@ -866,7 +870,7 @@ static void col_render_post_tick_ctx( col_emit_sol_hazard_vfx(s, ov); col_emit_player_attack_projectiles(s, ctx, ov); if (ctx->thrall_attacked_this_tick) - col_emit_thrall_projectile(s, ov); + col_emit_thrall_projectile(s, ctx, ov); } static void* col_get_player_for_input(void* state, int idx) { @@ -885,7 +889,7 @@ static int col_is_human_targetable_npc_slot_ctx( int npc_slot ) { ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); col_refresh_current_obs_slots_ctx(s, ctx); int obs_slot = col_find_target_obs_slot(s, npc_slot); if (obs_slot < 0) return 0; @@ -936,7 +940,7 @@ static void col_translate_human_input_ctx( encounter_translate_prayer(hi, actions, COLO_HEAD_PRAYER); ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); col_refresh_current_obs_slots_ctx(s, ctx); if (hi->pending_target_idx >= 0) { @@ -1020,7 +1024,7 @@ static void col_step_human_commands_ctx( HumanInput* hi ) { ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); int actions[COLO_NUM_ACTION_HEADS]; ctx->human_command_mode = 1; ctx->human_commands = hi->commands.items; @@ -1078,7 +1082,7 @@ static void col_restore_ctx( const void* data, size_t n ) { - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); ColosseumState* dst = (ColosseumState*)state; encounter_snapshot_validate_frame( "col_restore", data, n, sizeof(ColoSnapshot), @@ -1089,10 +1093,17 @@ static void col_restore_ctx( col_refresh_after_state_load(dst, ctx); } +typedef struct { + ColosseumState* state; + ColosseumContext* context; +} ColoLabInvocation; + static int col_lab_apply_script_line_alloc_json_void( - void* state, const char* line, char** out_json + void* data, const char* line, char** out_json ) { - return col_lab_apply_script_line_alloc_json((ColosseumState*)state, line, out_json); + ColoLabInvocation* invocation = (ColoLabInvocation*)data; + return col_lab_apply_script_line_alloc_json( + invocation->state, invocation->context, line, out_json); } static int col_apply_lab_command_ctx( @@ -1100,10 +1111,16 @@ static int col_apply_lab_command_ctx( EncounterContext* context, const char* line ) { - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); + encounter_arena_topology_require_finalized(ctx->route_topology); col_context_clear_render_events(ctx); + ColoLabInvocation invocation = { + .state = (ColosseumState*)state, + .context = ctx, + }; return encounter_apply_lab_command_dump_wrapper( - state, line, COLO_LAB_LINE_DUMP, col_lab_apply_script_line_alloc_json_void); + &invocation, line, COLO_LAB_LINE_DUMP, + col_lab_apply_script_line_alloc_json_void); } static const EncounterDef ENCOUNTER_COLOSSEUM = { diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc index bf7e7d7575..412aca5aa7 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -149,12 +149,23 @@ static int col_spawn_excluded_near_player(const ColosseumState* s, int x, int y, s->player.x, s->player.y, 1, x, y, size) <= COLO_SPAWN_EXCLUSION_CHEB; } -static int col_spawn_footprint_free(const ColosseumState* s, int x, int y, int size) { +static int col_spawn_footprint_free( + const ColosseumState* s, + const EncounterArenaTopology* topology, + int x, + int y, + int size +) { + if (encounter_arena_topology_footprint_blocked( + topology, x, y, size)) + return 0; for (int dx = 0; dx < size; dx++) { for (int dy = 0; dy < size; dy++) { - int tx = x + dx, ty = y + dy; - if (col_static_blocked(tx, ty)) return 0; - if (col_sol_clamp_active(s) && !col_in_boss_arena(s, tx, ty)) return 0; + int tx = x + dx; + int ty = y + dy; + if (col_sol_clamp_active(s) && + !col_in_boss_arena(s, tx, ty)) + return 0; int gx, gy; if (!col_grid_index(tx, ty, &gx, &gy)) return 0; if (s->npc_collision_flags[gx][gy]) return 0; @@ -164,121 +175,215 @@ static int col_spawn_footprint_free(const ColosseumState* s, int x, int y, int s return 1; } -static int col_spawn_anchor_candidates(const ColosseumState* s, int* out) { +static int col_spawn_anchor_candidates( + const ColosseumState* s, + const EncounterArenaTopology* topology, + int* out +) { int n = 0; for (int a = 0; a < COLO_NUM_SPAWN_ANCHORS; a++) { - int ax = COLO_SPAWN_ANCHORS[a][0], ay = COLO_SPAWN_ANCHORS[a][1]; - if (col_spawn_excluded_near_player(s, ax, ay, COLO_SPAWN_ZONE_SIZE)) continue; - if (!col_spawn_footprint_free(s, ax, ay, COLO_SPAWN_ZONE_SIZE)) continue; + int ax = COLO_SPAWN_ANCHORS[a][0]; + int ay = COLO_SPAWN_ANCHORS[a][1]; + if (col_spawn_excluded_near_player( + s, ax, ay, COLO_SPAWN_ZONE_SIZE)) + continue; + if (!col_spawn_footprint_free( + s, topology, ax, ay, COLO_SPAWN_ZONE_SIZE)) + continue; out[n++] = a; } return n; } -static int col_warband_spawn_tile_ok(const ColosseumState* s, int x, int y) { - if (col_static_blocked(x, y)) return 0; +static int col_warband_spawn_tile_ok( + const ColosseumState* s, + const EncounterArenaTopology* topology, + int x, + int y +) { + if (encounter_arena_topology_tile_blocked(topology, x, y)) return 0; if (col_sol_clamp_active(s) && !col_in_boss_arena(s, x, y)) return 0; return !(x == s->player.x && y == s->player.y); } -static void col_draw_warband_box_tile(ColosseumState* s, int* ox, int* oy) { +static void col_draw_warband_box_tile( + ColosseumState* s, + const EncounterArenaTopology* topology, + int* out_x, + int* out_y +) { int span_x = COLO_WARBAND_BOX_MAX_X - COLO_WARBAND_BOX_MIN_X + 1; int span_y = COLO_WARBAND_BOX_MAX_Y - COLO_WARBAND_BOX_MIN_Y + 1; - for (int t = 0; t < 32; t++) { - int x = COLO_WARBAND_BOX_MIN_X + encounter_rand_int(&s->rng_state, span_x); - int y = COLO_WARBAND_BOX_MIN_Y + encounter_rand_int(&s->rng_state, span_y); - if (col_warband_spawn_tile_ok(s, x, y)) { *ox = x; *oy = y; return; } + for (int attempt = 0; attempt < 32; attempt++) { + int x = COLO_WARBAND_BOX_MIN_X + + encounter_rand_int(&s->rng_state, span_x); + int y = COLO_WARBAND_BOX_MIN_Y + + encounter_rand_int(&s->rng_state, span_y); + if (col_warband_spawn_tile_ok(s, topology, x, y)) { + *out_x = x; + *out_y = y; + return; + } + } + for (int y = COLO_WARBAND_BOX_MIN_Y; y <= COLO_WARBAND_BOX_MAX_Y; y++) { + for (int x = COLO_WARBAND_BOX_MIN_X; x <= COLO_WARBAND_BOX_MAX_X; x++) { + if (!col_warband_spawn_tile_ok(s, topology, x, y)) continue; + *out_x = x; + *out_y = y; + return; + } } - for (int y = COLO_WARBAND_BOX_MIN_Y; y <= COLO_WARBAND_BOX_MAX_Y; y++) - for (int x = COLO_WARBAND_BOX_MIN_X; x <= COLO_WARBAND_BOX_MAX_X; x++) - if (col_warband_spawn_tile_ok(s, x, y)) { *ox = x; *oy = y; return; } assert(0 && "warband centre box full"); } static void col_spawn_warbander_formation( - ColosseumState* s, ColoNpcType type, int ax, int ay, - int dx, int dy, int dir) { - int x = ax + dx, y = ay + dy; - if (!col_warband_spawn_tile_ok(s, x, y)) { + ColosseumState* s, + const EncounterArenaTopology* topology, + ColoNpcType type, + int anchor_x, + int anchor_y, + int dx, + int dy, + int direction +) { + int x = anchor_x + dx; + int y = anchor_y + dy; + if (!col_warband_spawn_tile_ok(s, topology, x, y)) { int found = 0; - for (int r = 1; r <= 3 && !found; r++) { - for (int oy = -r; oy <= r && !found; oy++) { - for (int ox = -r; ox <= r && !found; ox++) { - if (ox > -r && ox < r && oy > -r && oy < r) continue; - if (col_warband_spawn_tile_ok(s, x + ox, y + oy)) { - x += ox; y += oy; found = 1; + for (int radius = 1; radius <= 3 && !found; radius++) { + for (int offset_y = -radius; + offset_y <= radius && !found; + offset_y++) { + for (int offset_x = -radius; + offset_x <= radius && !found; + offset_x++) { + if (offset_x > -radius && offset_x < radius && + offset_y > -radius && offset_y < radius) + continue; + if (col_warband_spawn_tile_ok( + s, topology, x + offset_x, y + offset_y)) { + x += offset_x; + y += offset_y; + found = 1; } } } } - if (!found) col_draw_warband_box_tile(s, &x, &y); + if (!found) + col_draw_warband_box_tile(s, topology, &x, &y); } int slot = col_spawn_npc_at(s, type, x, y); - colo_npc_warband(&s->npcs[slot])->formation_dir = dir; + colo_npc_warband(&s->npcs[slot])->formation_dir = direction; } -static void col_spawn_pair_adjacent(ColosseumState* s, ColoNpcType type, int px, int py) { +static void col_spawn_pair_adjacent( + ColosseumState* s, + const EncounterArenaTopology* topology, + ColoNpcType type, + int parent_x, + int parent_y +) { int size = COLO_NPC_STATS[type].size; - const int off[8][2] = { + const int offsets[8][2] = { {size, 0}, {-size, 0}, {0, size}, {0, -size}, - {size, size}, {-size, -size}, {size, -size}, {-size, size} }; - for (int o = 0; o < 8; o++) { - int x = px + off[o][0], y = py + off[o][1]; - if (!col_spawn_footprint_free(s, x, y, size)) continue; + {size, size}, {-size, -size}, {size, -size}, {-size, size}, + }; + for (int offset = 0; offset < 8; offset++) { + int x = parent_x + offsets[offset][0]; + int y = parent_y + offsets[offset][1]; + if (!col_spawn_footprint_free(s, topology, x, y, size)) continue; col_spawn_npc_at(s, type, x, y); return; } - for (int r = 1; r <= size + 2; r++) { - for (int dy = -r; dy <= r; dy++) { - for (int dx = -r; dx <= r; dx++) { - if (dx > -r && dx < r && dy > -r && dy < r) continue; - int x = px + dx, y = py + dy; - if (!col_spawn_footprint_free(s, x, y, size)) continue; + for (int radius = 1; radius <= size + 2; radius++) { + for (int dy = -radius; dy <= radius; dy++) { + for (int dx = -radius; dx <= radius; dx++) { + if (dx > -radius && dx < radius && + dy > -radius && dy < radius) + continue; + int x = parent_x + dx; + int y = parent_y + dy; + if (!col_spawn_footprint_free( + s, topology, x, y, size)) + continue; col_spawn_npc_at(s, type, x, y); return; } } } - int best = -1, best_dist = 1 << 30; - for (int a = 0; a < COLO_NUM_SPAWN_ANCHORS; a++) { - int ax = COLO_SPAWN_ANCHORS[a][0], ay = COLO_SPAWN_ANCHORS[a][1]; - if (col_spawn_excluded_near_player(s, ax, ay, COLO_SPAWN_ZONE_SIZE)) continue; - if (!col_spawn_footprint_free(s, ax, ay, COLO_SPAWN_ZONE_SIZE)) continue; - int dx = ax > px ? ax - px : px - ax; - int dy = ay > py ? ay - py : py - ay; - int cheb = dx > dy ? dx : dy; - if (cheb < best_dist) { best_dist = cheb; best = a; } + int best = -1; + int best_dist = 1 << 30; + for (int anchor = 0; anchor < COLO_NUM_SPAWN_ANCHORS; anchor++) { + int x = COLO_SPAWN_ANCHORS[anchor][0]; + int y = COLO_SPAWN_ANCHORS[anchor][1]; + if (col_spawn_excluded_near_player( + s, x, y, COLO_SPAWN_ZONE_SIZE)) + continue; + if (!col_spawn_footprint_free( + s, topology, x, y, COLO_SPAWN_ZONE_SIZE)) + continue; + int dx = x > parent_x ? x - parent_x : parent_x - x; + int dy = y > parent_y ? y - parent_y : parent_y - y; + int distance = dx > dy ? dx : dy; + if (distance < best_dist) { + best_dist = distance; + best = anchor; + } } assert(best >= 0 && "no spawn spot for the Dynamic Duo pair"); - col_spawn_npc_at(s, type, COLO_SPAWN_ANCHORS[best][0], COLO_SPAWN_ANCHORS[best][1]); + col_spawn_npc_at( + s, + type, + COLO_SPAWN_ANCHORS[best][0], + COLO_SPAWN_ANCHORS[best][1]); } -static void col_spawn_quartet_boss_wave(ColosseumState* s, ColoNpcType type) { +static void col_spawn_quartet_boss_wave( + ColosseumState* s, + const EncounterArenaTopology* topology, + ColoNpcType type +) { int span = COLO_BOSS_ARENA_SIZE; - int qx = -1, qy = -1; - for (int t = 0; t < 64 && qx < 0; t++) { - int x = COLO_BOSS_ARENA_MIN_X + encounter_rand_int(&s->rng_state, span); - int y = COLO_BOSS_ARENA_MIN_Y + encounter_rand_int(&s->rng_state, span); + int quartet_x = -1; + int quartet_y = -1; + for (int attempt = 0; attempt < 64 && quartet_x < 0; attempt++) { + int x = COLO_BOSS_ARENA_MIN_X + + encounter_rand_int(&s->rng_state, span); + int y = COLO_BOSS_ARENA_MIN_Y + + encounter_rand_int(&s->rng_state, span); if (col_spawn_excluded_near_player(s, x, y, 1)) continue; - if (!col_spawn_footprint_free(s, x, y, 1)) continue; - qx = x; qy = y; + if (!col_spawn_footprint_free(s, topology, x, y, 1)) continue; + quartet_x = x; + quartet_y = y; } - for (int y = COLO_BOSS_ARENA_MIN_Y; y <= COLO_BOSS_ARENA_MAX_Y && qx < 0; y++) { - for (int x = COLO_BOSS_ARENA_MIN_X; x <= COLO_BOSS_ARENA_MAX_X && qx < 0; x++) { + for (int y = COLO_BOSS_ARENA_MIN_Y; + y <= COLO_BOSS_ARENA_MAX_Y && quartet_x < 0; + y++) { + for (int x = COLO_BOSS_ARENA_MIN_X; + x <= COLO_BOSS_ARENA_MAX_X && quartet_x < 0; + x++) { if (col_spawn_excluded_near_player(s, x, y, 1)) continue; - if (!col_spawn_footprint_free(s, x, y, 1)) continue; - qx = x; qy = y; + if (!col_spawn_footprint_free(s, topology, x, y, 1)) continue; + quartet_x = x; + quartet_y = y; } } - assert(qx >= 0 && "no spawn tile for the wave-12 Quartet warbander"); - int slot = col_spawn_npc_at(s, type, qx, qy); - colo_npc_warband(&s->npcs[slot])->formation_dir = COLO_WARBAND_FORM_SOUTH; + assert(quartet_x >= 0 && + "no spawn tile for the wave-12 Quartet warbander"); + int slot = col_spawn_npc_at(s, type, quartet_x, quartet_y); + colo_npc_warband(&s->npcs[slot])->formation_dir = + COLO_WARBAND_FORM_SOUTH; } static const ColoNpcType COLO_WARBAND_TYPES[3] = { COLO_FREMENNIK_BERSERKER, COLO_FREMENNIK_ARCHER, COLO_FREMENNIK_SEER }; -static void col_spawn_wave(ColosseumState* s) { +static void col_spawn_wave( + ColosseumState* s, + const ColosseumContext* context +) { + const EncounterArenaTopology* topology = context->route_topology; + encounter_arena_topology_require_finalized(topology); col_reset_current_wave_score_progress(s); for (int i = 0; i < COLO_MAX_NPCS; i++) { @@ -303,13 +408,14 @@ static void col_spawn_wave(ColosseumState* s) { } if (col_mod_active(s, COLO_MOD_QUARTET)) { ColoNpcType extra = COLO_WARBAND_TYPES[encounter_rand_int(&s->rng_state, 3)]; - col_spawn_quartet_boss_wave(s, extra); + col_spawn_quartet_boss_wave(s, topology, extra); } } else { int archer_x = -1, archer_y = -1; for (int i = 0; i < def->count; i++) { if ((ColoNpcType)def->types[i] != COLO_FREMENNIK_ARCHER) continue; - col_draw_warband_box_tile(s, &archer_x, &archer_y); + col_draw_warband_box_tile( + s, topology, &archer_x, &archer_y); int slot = col_spawn_npc_at(s, COLO_FREMENNIK_ARCHER, archer_x, archer_y); colo_npc_warband(&s->npcs[slot])->formation_dir = COLO_WARBAND_FORM_WEST; } @@ -318,15 +424,18 @@ static void col_spawn_wave(ColosseumState* s) { if (!col_type_is_warbander(type) || type == COLO_FREMENNIK_ARCHER) continue; assert(archer_x >= 0 && "warband def lists berserker/seer without an archer"); if (type == COLO_FREMENNIK_SEER) - col_spawn_warbander_formation(s, type, archer_x, archer_y, + col_spawn_warbander_formation( + s, topology, type, archer_x, archer_y, 2, 0, COLO_WARBAND_FORM_EAST); else - col_spawn_warbander_formation(s, type, archer_x, archer_y, + col_spawn_warbander_formation( + s, topology, type, archer_x, archer_y, 1, 1, COLO_WARBAND_FORM_NORTH); } int candidates[COLO_NUM_SPAWN_ANCHORS]; - int ncand = col_spawn_anchor_candidates(s, candidates); + int ncand = + col_spawn_anchor_candidates(s, topology, candidates); encounter_shuffle(candidates, ncand, &s->rng_state); int cursor = 0; for (int i = 0; i < def->count; i++) { @@ -336,20 +445,26 @@ static void col_spawn_wave(ColosseumState* s) { while (cursor < ncand) { int a = candidates[cursor++]; if (col_spawn_footprint_free( - s, COLO_SPAWN_ANCHORS[a][0], COLO_SPAWN_ANCHORS[a][1], - COLO_SPAWN_ZONE_SIZE)) { anchor = a; break; } + s, topology, + COLO_SPAWN_ANCHORS[a][0], + COLO_SPAWN_ANCHORS[a][1], + COLO_SPAWN_ZONE_SIZE)) { + anchor = a; + break; + } } assert(anchor >= 0 && "primary spawn ran out of anchor candidates"); int ax = COLO_SPAWN_ANCHORS[anchor][0], ay = COLO_SPAWN_ANCHORS[anchor][1]; col_spawn_npc_at(s, type, ax, ay); if (type == COLO_SHOCKWAVE_COLOSSUS && col_mod_active(s, COLO_MOD_DYNAMIC_DUO)) - col_spawn_pair_adjacent(s, type, ax, ay); + col_spawn_pair_adjacent(s, topology, type, ax, ay); } if (col_mod_active(s, COLO_MOD_QUARTET)) { ColoNpcType extra = COLO_WARBAND_TYPES[encounter_rand_int(&s->rng_state, 3)]; assert(archer_x >= 0 && "quartet without an archer spawn"); - col_spawn_warbander_formation(s, extra, archer_x, archer_y, + col_spawn_warbander_formation( + s, topology, extra, archer_x, archer_y, 1, -1, COLO_WARBAND_FORM_SOUTH); } } @@ -369,7 +484,12 @@ static void col_spawn_wave(ColosseumState* s) { #define COLO_REINFORCE_FIRED (-1) -static void col_spawn_reinforcements(ColosseumState* s) { +static void col_spawn_reinforcements( + ColosseumState* s, + const ColosseumContext* context +) { + const EncounterArenaTopology* topology = context->route_topology; + encounter_arena_topology_require_finalized(topology); if (s->wave < 0 || s->wave >= COLO_NUM_WAVES) return; const ColoWaveDef* def = &COLO_WAVES[s->wave]; if (def->reinforce_count <= 0) { @@ -385,7 +505,11 @@ static void col_spawn_reinforcements(ColosseumState* s) { int y = north ? COLO_GATE_NORTH_SPAWN_ROW - (size - 1) : COLO_GATE_SOUTH_SPAWN_ROW; int x = base_x; for (int try_x = base_x; try_x + size - 1 <= COLO_GATE_MAX_X; try_x++) { - if (col_spawn_footprint_free(s, try_x, y, size)) { x = try_x; break; } + if (col_spawn_footprint_free( + s, topology, try_x, y, size)) { + x = try_x; + break; + } } int slot = col_spawn_npc_at(s, type, x, y); s->npcs[slot].spawned_as_reinforcement = 1; @@ -479,7 +603,7 @@ static void col_sync_consumable_counters_from_inventory(ColosseumState* s) { static void col_reset_ctx(EncounterState* state, EncounterContext* context, uint32_t seed) { col_build_npc_stats(); ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); encounter_arena_topology_require_finalized(ctx->route_topology); osrs_actor_route_cache_clear(&ctx->player_route_cache); int saved_start = ctx->config.start_wave; @@ -578,7 +702,3 @@ static void col_reset_ctx(EncounterState* state, EncounterContext* context, uint s->wave_spawn_delay = col_wave_entry_delay_ticks(effective_start); } } - -static void col_reset(EncounterState* state, uint32_t seed) { - col_reset_ctx(state, (EncounterContext*)col_legacy_context(), seed); -} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc index bcc56b1541..f5780cf5d2 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc @@ -335,7 +335,7 @@ static ColoPrePlayerPhase col_advance_pre_player_phase_ctx( if (phase.spawn_wave_now) { s->wave = s->wave_spawn_target; - col_spawn_wave(s); + col_spawn_wave(s, ctx); } phase.in_wave_gap = @@ -365,7 +365,7 @@ static ColoPrePlayerPhase col_advance_pre_player_phase_ctx( #endif col_tick_npcs_ctx(s, ctx); if (s->wave_attack_delay == 0) col_warband_attack_phase(s, ctx); - col_modifiers_tick(s); + col_modifiers_tick(s, ctx->route_topology); if (s->surge_cooldown > 0) s->surge_cooldown--; } @@ -396,7 +396,7 @@ static ColoPrePlayerPhase col_advance_pre_player_phase_ctx( static void col_step_ctx(EncounterState* state, EncounterContext* context, const int* actions) { ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumContext* ctx = col_require_context(context); encounter_arena_topology_require_finalized(ctx->route_topology); if (s->episode_over) return; #ifdef COLO_PROFILE_ENABLED @@ -458,7 +458,8 @@ static void col_step_ctx(EncounterState* state, EncounterContext* context, const if (!all_dead && s->reinforcement_timer > 0) { s->reinforcement_timer--; - if (s->reinforcement_timer == 0) col_spawn_reinforcements(s); + if (s->reinforcement_timer == 0) + col_spawn_reinforcements(s, ctx); } if (wave_cleared) { @@ -499,7 +500,3 @@ finish_step: COLO_PROFILE_MARK(COLO_PROF_STEP_WAVE_LOGIC); #endif } - -static void col_step(EncounterState* state, const int* actions) { - col_step_ctx(state, (EncounterContext*)col_legacy_context(), actions); -} diff --git a/ocean/osrs/osrs_collision.h b/ocean/osrs/osrs_collision.h index 4e1df5e038..733d265953 100644 --- a/ocean/osrs/osrs_collision.h +++ b/ocean/osrs/osrs_collision.h @@ -556,6 +556,11 @@ static inline int entity_has_line_of_sight_with_flags( ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES + 63) / 64) typedef uint32_t (*encounter_arena_tile_flags_fn)(void* ctx, int x, int y); +typedef enum { + ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_FLAGGED = 0, + ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_TILE_BLOCKED, +} EncounterArenaTopologyLosBuildMode; + typedef struct { int origin_x; @@ -568,10 +573,12 @@ typedef struct { void* tile_flags_ctx; encounter_arena_tile_flags_fn los_tile_flags; void* los_tile_flags_ctx; + EncounterArenaTopologyLosBuildMode los_build_mode; } EncounterArenaTopologyBuildSpec; typedef enum { ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN = 0, - ENCOUNTER_ARENA_TOPOLOGY_LOS_FLAGGED = 1, + ENCOUNTER_ARENA_TOPOLOGY_LOS_FLAGGED, + ENCOUNTER_ARENA_TOPOLOGY_LOS_TILE_BLOCKED, } EncounterArenaTopologyLosMode; @@ -585,6 +592,7 @@ typedef struct EncounterArenaTopology { uint64_t revision; uint8_t finalized; EncounterArenaTopologyLosMode static_los_mode; + EncounterArenaTopologyLosBuildMode los_build_mode; uint32_t static_collision_flags[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint8_t static_blocked[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint8_t footprint_blocked @@ -757,6 +765,56 @@ static uint32_t encounter_arena_topology_los_flags( (LOS_FULL_MASK | LOS_EAST_MASK | LOS_WEST_MASK | LOS_NORTH_MASK | LOS_SOUTH_MASK); } +static int encounter_arena_topology_tile_ray_clear( + EncounterArenaTopologyLosBuildContext* build, + int x0, + int y0, + int x1, + int y1 +) { + int dx = x1 - x0; + int dy = y1 - y0; + int adx = dx < 0 ? -dx : dx; + int ady = dy < 0 ? -dy : dy; + if (adx == 0 && ady == 0) return 1; + if (encounter_arena_topology_los_flags(build, x1, y1)) return 0; + + if (adx > ady) { + int x = x0; + int y_fp = y0 * LOS_FP_SCALE + LOS_FP_HALF; + int slope = (dy * LOS_FP_SCALE) / adx; + int x_inc = dx > 0 ? 1 : -1; + if (dy < 0) y_fp -= 1; + while (x != x1) { + x += x_inc; + int y = y_fp >> 16; + if (encounter_arena_topology_los_flags(build, x, y)) return 0; + y_fp += slope; + int new_y = y_fp >> 16; + if (new_y != y && + encounter_arena_topology_los_flags(build, x, new_y)) + return 0; + } + } else { + int y = y0; + int x_fp = x0 * LOS_FP_SCALE + LOS_FP_HALF; + int slope = (dx * LOS_FP_SCALE) / ady; + int y_inc = dy > 0 ? 1 : -1; + if (dx < 0) x_fp -= 1; + while (y != y1) { + y += y_inc; + int x = x_fp >> 16; + if (encounter_arena_topology_los_flags(build, x, y)) return 0; + x_fp += slope; + int new_x = x_fp >> 16; + if (new_x != x && + encounter_arena_topology_los_flags(build, new_x, y)) + return 0; + } + } + return 1; +} + static inline void encounter_arena_topology_set_los( EncounterArenaTopology* topology, @@ -806,6 +864,7 @@ static inline EncounterArenaTopology* encounter_arena_topology_build( topology->tile_count = spec->width * spec->height; topology->max_footprint_size = spec->max_footprint_size; topology->revision = spec->revision; + topology->los_build_mode = spec->los_build_mode; for (int local_x = 0; local_x < topology->width; local_x++) { for (int local_y = 0; local_y < topology->height; local_y++) { @@ -900,60 +959,90 @@ static inline EncounterArenaTopology* encounter_arena_topology_build( .topology = topology, .spec = spec, }; - topology->static_los_mode = ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN; - for (int source = 0; source < topology->tile_count; source++) { - int source_x = topology->origin_x + source / topology->height; - int source_y = topology->origin_y + source % topology->height; - if (encounter_arena_topology_los_flags( - &los_build, source_x, source_y) != 0) { - topology->static_los_mode = - ENCOUNTER_ARENA_TOPOLOGY_LOS_FLAGGED; - break; - } - } - if (topology->static_los_mode == - ENCOUNTER_ARENA_TOPOLOGY_LOS_FLAGGED) { + if (topology->los_build_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_TILE_BLOCKED) { + topology->static_los_mode = + ENCOUNTER_ARENA_TOPOLOGY_LOS_TILE_BLOCKED; for (int source = 0; source < topology->tile_count; source++) { int source_x = topology->origin_x + source / topology->height; int source_y = topology->origin_y + source % topology->height; - if (encounter_arena_topology_los_flags( - &los_build, source_x, source_y) == 0) - encounter_arena_topology_set_los( - topology, source, source); - - for (int target = source + 1; + for (int target = 0; target < topology->tile_count; target++) { int target_x = topology->origin_x + target / topology->height; int target_y = topology->origin_y + target % topology->height; - int forward = los_has_line_of_sight_with_flags( - encounter_arena_topology_los_flags, - &los_build, - source_x, - source_y, - target_x, - target_y, - 1, - 0); - int reverse = los_has_line_of_sight_with_flags( - encounter_arena_topology_los_flags, - &los_build, - target_x, - target_y, - source_x, - source_y, - 1, - 0); - if (forward) + if (encounter_arena_topology_tile_ray_clear( + &los_build, + source_x, + source_y, + target_x, + target_y)) encounter_arena_topology_set_los( topology, source, target); - if (reverse) + } + } + } else { + topology->static_los_mode = ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN; + for (int source = 0; source < topology->tile_count; source++) { + int source_x = + topology->origin_x + source / topology->height; + int source_y = + topology->origin_y + source % topology->height; + if (encounter_arena_topology_los_flags( + &los_build, source_x, source_y) != 0) { + topology->static_los_mode = + ENCOUNTER_ARENA_TOPOLOGY_LOS_FLAGGED; + break; + } + } + if (topology->static_los_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_FLAGGED) { + for (int source = 0; source < topology->tile_count; source++) { + int source_x = + topology->origin_x + source / topology->height; + int source_y = + topology->origin_y + source % topology->height; + if (encounter_arena_topology_los_flags( + &los_build, source_x, source_y) == 0) encounter_arena_topology_set_los( - topology, target, source); + topology, source, source); + + for (int target = source + 1; + target < topology->tile_count; + target++) { + int target_x = + topology->origin_x + target / topology->height; + int target_y = + topology->origin_y + target % topology->height; + int forward = los_has_line_of_sight_with_flags( + encounter_arena_topology_los_flags, + &los_build, + source_x, + source_y, + target_x, + target_y, + 1, + 0); + int reverse = los_has_line_of_sight_with_flags( + encounter_arena_topology_los_flags, + &los_build, + target_x, + target_y, + source_x, + source_y, + 1, + 0); + if (forward) + encounter_arena_topology_set_los( + topology, source, target); + if (reverse) + encounter_arena_topology_set_los( + topology, target, source); + } } } } @@ -982,7 +1071,8 @@ static inline void encounter_arena_topology_require_spec( topology->width != spec->width || topology->height != spec->height || topology->max_footprint_size != spec->max_footprint_size || - topology->revision != spec->revision) { + topology->revision != spec->revision || + topology->los_build_mode != spec->los_build_mode) { fprintf(stderr, "%s route topology spec changed\n", encounter_name); abort(); } @@ -1132,7 +1222,6 @@ static inline int encounter_arena_topology_los_clear( (int64_t)target_x <= actor_max_x && (int64_t)actor_y <= target_max_y && (int64_t)target_y <= actor_max_y; - if (overlap) return 0; if (attack_range == 1) { int y_overlap = @@ -1160,6 +1249,20 @@ static inline int encounter_arena_topology_los_clear( int64_t target_los_y = actor_y; if (target_los_y < target_y) target_los_y = target_y; if (target_los_y > target_max_y) target_los_y = target_max_y; + if (topology->static_los_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_TILE_BLOCKED && + attack_range == 0) { + int actor_index = encounter_arena_topology_index_raw( + topology, (int)actor_los_x, (int)actor_los_y); + int target_index = encounter_arena_topology_index_raw( + topology, (int)target_los_x, (int)target_los_y); + size_t bit_index = + (size_t)actor_index * (size_t)topology->tile_count + + (size_t)target_index; + return (int)((topology->static_los_bits[bit_index >> 6] >> + (bit_index & 63)) & UINT64_C(1)); + } + if (overlap) return 0; int64_t dx = target_los_x - actor_los_x; int64_t dy = target_los_y - actor_los_y; diff --git a/ocean/osrs/tests/probe_colo_best_gear_dpt.c b/ocean/osrs/tests/probe_colo_best_gear_dpt.c index 7d8690d14f..1d94a8ea66 100644 --- a/ocean/osrs/tests/probe_colo_best_gear_dpt.c +++ b/ocean/osrs/tests/probe_colo_best_gear_dpt.c @@ -12,6 +12,7 @@ static void loadout_reset(ColosseumState* s, ColosseumContext* ctx, int mode, col_init_context_typed(ctx); ctx->config.loadout_profile_mode = mode; ctx->config.beginner_loadout_fraction = frac; + col_finalize_route_topology(ctx); memset(s, 0, sizeof(*s)); col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); } diff --git a/ocean/osrs/tests/probe_colo_bis_gear_prayer.c b/ocean/osrs/tests/probe_colo_bis_gear_prayer.c index 7ceea62b81..ebf69c7865 100644 --- a/ocean/osrs/tests/probe_colo_bis_gear_prayer.c +++ b/ocean/osrs/tests/probe_colo_bis_gear_prayer.c @@ -17,6 +17,7 @@ int main(void) { col_init_context_typed(&ctx); ctx.config.start_wave = 0; ctx.config.bis_gear_oracle_mode = 1; + col_finalize_route_topology(&ctx); memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); diff --git a/ocean/osrs/tests/probe_colo_damage_scale.c b/ocean/osrs/tests/probe_colo_damage_scale.c index ccb4f90ac0..bc82af433c 100644 --- a/ocean/osrs/tests/probe_colo_damage_scale.c +++ b/ocean/osrs/tests/probe_colo_damage_scale.c @@ -19,6 +19,7 @@ static int direct_hp_loss(uint32_t seed, int dmg, float scale) { static ColosseumContext ctx; static ColosseumState s; col_init_context_typed(&ctx); + col_finalize_route_topology(&ctx); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); s.active_player_damage_received_scale = scale; int hp0 = s.player.current_hitpoints; @@ -30,6 +31,7 @@ static int queued_hp_loss(uint32_t seed, int dmg, float scale) { static ColosseumContext ctx; static ColosseumState s; col_init_context_typed(&ctx); + col_finalize_route_topology(&ctx); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); s.active_player_damage_received_scale = scale; int hp0 = s.player.current_hitpoints; @@ -42,7 +44,7 @@ static int queued_hp_loss(uint32_t seed, int dmg, float scale) { col_push_player_pending_hit(&s, hit); s.tick++; - col_resolve_player_pending_hits(&s); + col_resolve_player_pending_hits_ctx(&s, &ctx); (void)landed_raw; return hp0 - s.player.current_hitpoints; } @@ -51,6 +53,7 @@ static int doom_stacks_after_melee(uint32_t seed, int dmg, float scale) { static ColosseumContext ctx; static ColosseumState s; col_init_context_typed(&ctx); + col_finalize_route_topology(&ctx); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); s.active_player_damage_received_scale = scale; s.modifiers.active_mask |= (1u << COLO_MOD_DOOM); diff --git a/ocean/osrs/tests/probe_colo_prayer_switch_slip.c b/ocean/osrs/tests/probe_colo_prayer_switch_slip.c index a450d17460..615ddd088f 100644 --- a/ocean/osrs/tests/probe_colo_prayer_switch_slip.c +++ b/ocean/osrs/tests/probe_colo_prayer_switch_slip.c @@ -11,6 +11,7 @@ static void prep(ColosseumContext* ctx, ColosseumState* s, float fail_prob) { ENCOUNTER_COLOSSEUM.put_float((EncounterState*)s, (EncounterContext*)ctx, "prayer_switch_fail_prob", fail_prob); + col_finalize_route_topology(ctx); s->player.prayer = PRAYER_PROTECT_RANGED; s->player.current_prayer = 990; s->player.prayer_just_activated = 0; diff --git a/ocean/osrs/tests/probe_colo_rng_diversity.c b/ocean/osrs/tests/probe_colo_rng_diversity.c index 7a3a94e795..d45c58a54d 100644 --- a/ocean/osrs/tests/probe_colo_rng_diversity.c +++ b/ocean/osrs/tests/probe_colo_rng_diversity.c @@ -54,6 +54,7 @@ int main(void) { ColosseumContext ctx; col_init_context_typed(&ctx); ctx.config.start_wave = 0; + col_finalize_route_topology(&ctx); enum { N = 8 }; uint64_t dig[N]; diff --git a/ocean/osrs/tests/probe_colo_sim_invariant.c b/ocean/osrs/tests/probe_colo_sim_invariant.c index 91372365ef..472816007c 100644 --- a/ocean/osrs/tests/probe_colo_sim_invariant.c +++ b/ocean/osrs/tests/probe_colo_sim_invariant.c @@ -177,6 +177,7 @@ static uint64_t run_episode(const SimConfig* cfg, int max_ticks) { col_init_context_typed(&ctx); ctx.config.start_wave = cfg->public_start_wave - 1; + col_finalize_route_topology(&ctx); memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, cfg->env_seed); diff --git a/ocean/osrs/tests/probe_env_thread_determinism.c b/ocean/osrs/tests/probe_env_thread_determinism.c index e92ecd823f..552e6e12dc 100644 --- a/ocean/osrs/tests/probe_env_thread_determinism.c +++ b/ocean/osrs/tests/probe_env_thread_determinism.c @@ -54,6 +54,7 @@ static void run_pass(int workers, int late_start_mode, const int* actions) { col_init_context_typed(&g_envs[i].ctx); g_envs[i].ctx.config.start_wave = i % 12; g_envs[i].ctx.config.late_start_state_mode = late_start_mode; + col_finalize_route_topology(&g_envs[i].ctx); memset(&g_envs[i].state, 0, sizeof(ColosseumState)); col_reset_ctx((EncounterState*)&g_envs[i].state, (EncounterContext*)&g_envs[i].ctx, 900u + (unsigned)i * 7u); diff --git a/ocean/osrs/tests/test_colo_hit_delay.c b/ocean/osrs/tests/test_colo_hit_delay.c index 3f94e08a73..67048462e0 100644 --- a/ocean/osrs/tests/test_colo_hit_delay.c +++ b/ocean/osrs/tests/test_colo_hit_delay.c @@ -31,6 +31,7 @@ static void clear_npcs(ColosseumState* s) { static void init_state(ColosseumState* s, ColosseumContext* ctx, int px, int py) { col_init_context_typed(ctx); ctx->config.late_start_state_mode = 0; + col_finalize_route_topology(ctx); memset(s, 0, sizeof(*s)); col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, 4141u); clear_npcs(s); @@ -74,7 +75,7 @@ static QueuedSwing swing_at_offset(AttackStyle style, int offset) { npc->x, npc->y, col_npc_effective_size(npc), ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); QueuedSwing out = { .distance = distance, diff --git a/ocean/osrs/tests/test_colo_prayer_wiring.c b/ocean/osrs/tests/test_colo_prayer_wiring.c index 217dc9b7f7..31d077c762 100644 --- a/ocean/osrs/tests/test_colo_prayer_wiring.c +++ b/ocean/osrs/tests/test_colo_prayer_wiring.c @@ -10,7 +10,10 @@ typedef enum { POLICY_HOLD_MELEE, POLICY_OBS_TELEGRAPH } PrayerPolicy; -static int telegraph_overhead(const ColosseumState* s) { +static int telegraph_overhead( + const ColosseumState* s, + const ColosseumContext* ctx +) { AttackStyle best_style = ATTACK_STYLE_NONE; int best_ticks = 1 << 30; for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { @@ -18,7 +21,8 @@ static int telegraph_overhead(const ColosseumState* s) { if (idx < 0 || idx >= COLO_MAX_NPCS) continue; const ColoNPC* npc = &s->npcs[idx]; if (!col_npc_is_live_enemy(npc)) continue; - ColoNpcNextPrayerObs t = col_npc_next_prayer_obs(s, npc, idx); + ColoNpcNextPrayerObs t = + col_npc_next_prayer_obs(s, ctx, npc, idx); if (!t.active) continue; if (t.ticks < best_ticks) { best_ticks = t.ticks; best_style = t.style; } } @@ -48,6 +52,7 @@ static void run_test(const char* label, PrayerPolicy policy, int start_wave, int ctx.config.start_wave = start_wave; ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; ctx.config.beginner_loadout_fraction = 0.5f; + col_finalize_route_topology(&ctx); static float obs[COLO_NUM_OBS]; ColosseumState s; @@ -72,7 +77,7 @@ static void run_test(const char* label, PrayerPolicy policy, int start_wave, int continue; } actions[COLO_HEAD_PRAYER] = (policy == POLICY_HOLD_MELEE) - ? COLO_OVERHEAD_MELEE : telegraph_overhead(&s); + ? COLO_OVERHEAD_MELEE : telegraph_overhead(&s, &ctx); actions[COLO_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; actions[COLO_HEAD_PRIMARY] = nearest_target(&s); @@ -80,7 +85,8 @@ static void run_test(const char* label, PrayerPolicy policy, int start_wave, int for (int n = 0; n < COLO_MAX_NPCS; n++) { const ColoNPC* npc = &s.npcs[n]; if (!col_npc_is_live_enemy(npc)) continue; - ColoNpcNextPrayerObs t = col_npc_next_prayer_obs(&s, npc, n); + ColoNpcNextPrayerObs t = + col_npc_next_prayer_obs(&s, &ctx, npc, n); if (t.active && t.ticks <= 1) predicted[n] = t.style; } diff --git a/ocean/osrs/tests/test_colosseum_forecast_exact.c b/ocean/osrs/tests/test_colosseum_forecast_exact.c index 737c20adf5..5e1b9b2c69 100644 --- a/ocean/osrs/tests/test_colosseum_forecast_exact.c +++ b/ocean/osrs/tests/test_colosseum_forecast_exact.c @@ -12,24 +12,33 @@ #include "ocean/osrs/tests/osrs_route_reference.h" #endif -static void col_step_out_forecast_landing_selftest_one_state(ColosseumState* s) { +static void col_step_out_forecast_landing_selftest_one_state( + ColosseumState* s, + ColosseumContext* ctx +) { for (int x = COLO_ARENA_MIN_X - 2; x <= COLO_ARENA_MAX_X + 2; x++) { for (int y = COLO_ARENA_MIN_Y - 2; y <= COLO_ARENA_MAX_Y + 2; y++) { s->player.x = x; s->player.y = y; for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - int valid = col_step_out_forecast_action_valid(s, action_idx); + int valid = + col_step_out_forecast_action_valid(s, ctx, action_idx); Player moved = s->player; + ColoGeometryContext geometry = { + .state = s, + .context = ctx, + }; if (action_idx != 0) { encounter_move_to_target( &moved, ENCOUNTER_MOVE_TARGET_DX[action_idx], ENCOUNTER_MOVE_TARGET_DY[action_idx], col_player_walkable, - (void*)s); + &geometry); } ColoForecastLanding landing = - col_step_out_forecast_action_landing_ctx(s, action_idx); + col_step_out_forecast_action_landing_ctx( + s, ctx, action_idx); if (landing.valid != valid || landing.land_x != moved.x || landing.land_y != moved.y) { @@ -55,12 +64,12 @@ static void col_step_out_forecast_landing_selftest(void) { memset(s.npcs, 0, sizeof(s.npcs)); memset(s.npc_collision_flags, 0, sizeof(s.npc_collision_flags)); col_rebuild_player_collision_flags(&s); - col_step_out_forecast_landing_selftest_one_state(&s); + col_step_out_forecast_landing_selftest_one_state(&s, &ctx); col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 12, 9); col_spawn_npc_at(&s, COLO_JAVELIN_COLOSSUS, 18, 18); col_rebuild_player_collision_flags(&s); - col_step_out_forecast_landing_selftest_one_state(&s); + col_step_out_forecast_landing_selftest_one_state(&s, &ctx); s.wave = COLO_WAVE_BOSS; s.sol.started = 1; @@ -68,57 +77,231 @@ static void col_step_out_forecast_landing_selftest(void) { s.sol.boss_arena_min_y = COLO_BOSS_ARENA_MIN_Y; s.sol.boss_arena_max_x = COLO_BOSS_ARENA_MAX_X; s.sol.boss_arena_max_y = COLO_BOSS_ARENA_MAX_Y; - col_step_out_forecast_landing_selftest_one_state(&s); + col_step_out_forecast_landing_selftest_one_state(&s, &ctx); printf("colosseum landing helper selftest PASS: %d actions across 3 states\n", ENCOUNTER_MOVE_ACTIONS); } +static int exact_static_tile_blocked_reference(int x, int y) { + if (x < COLO_ARENA_MIN_X || x > COLO_ARENA_MAX_X || + y < COLO_ARENA_MIN_Y || y > COLO_ARENA_MAX_Y) + return 1; + for (int span = 0; span < COLO_WALL_SPANS_PER_ROW; span++) { + ColoWallSpan wall = COLO_WALL_SPANS[y][span]; + if (x >= wall.lo && x < wall.hi) return 1; + } + for (int pillar = 0; pillar < COLO_NUM_PILLARS; pillar++) { + int pillar_x = COLO_PILLARS[pillar][0]; + int pillar_y = COLO_PILLARS[pillar][1]; + if (x >= pillar_x && x < pillar_x + COLO_PILLAR_SIZE && + y >= pillar_y && y < pillar_y + COLO_PILLAR_SIZE) + return 1; + } + return 0; +} + +static int exact_static_los_blocked(void* data, int x, int y) { + (void)data; + return exact_static_tile_blocked_reference(x, y); +} + +static int exact_static_footprint_blocked_reference(int x, int y, int size) { + for (int dx = 0; dx < size; dx++) + for (int dy = 0; dy < size; dy++) + if (exact_static_tile_blocked_reference(x + dx, y + dy)) + return 1; + return 0; +} + static void col_static_los_table_selftest(void) { - col_build_static_arena(); + ColosseumContext ctx; + col_init_context_typed(&ctx); + col_finalize_route_topology(&ctx); + OsrsLosQuery reference = + osrs_los_tile(exact_static_los_blocked, NULL); + int checks = 0; for (int x0 = COLO_ARENA_MIN_X; x0 <= COLO_ARENA_MAX_X; x0++) { for (int y0 = COLO_ARENA_MIN_Y; y0 <= COLO_ARENA_MAX_Y; y0++) { for (int x1 = COLO_ARENA_MIN_X; x1 <= COLO_ARENA_MAX_X; x1++) { for (int y1 = COLO_ARENA_MIN_Y; y1 <= COLO_ARENA_MAX_Y; y1++) { - int table = col_static_los_get(x0, y0, x1, y1); - int slow = col_tiles_have_los_reference_slowpath(x0, y0, x1, y1); - if (table != slow) { + if (x0 == x1 && y0 == y1) continue; + int topology = col_topology_los_clear( + &ctx, x0, y0, 1, x1, y1, 1, 0); + int slow = + osrs_los_tile_ray_clear(&reference, x0, y0, x1, y1); + if (topology != slow) { + fprintf(stderr, + "colosseum topology LoS mismatch (%d,%d)->(%d,%d): topology=%d reference=%d\n", + x0, y0, x1, y1, topology, slow); + abort(); + } + checks++; + } + } + } + } + printf("colosseum topology LoS parity PASS: %d directed pairs\n", checks); +} +static void col_static_los_endpoint_semantics_selftest(void) { + ColosseumContext ctx; + col_init_context_typed(&ctx); + col_finalize_route_topology(&ctx); + OsrsLosQuery reference = + osrs_los_tile(exact_static_los_blocked, NULL); + uint64_t raw_clear[2][2] = {{0}}; + uint64_t ranged_clear[2][2] = {{0}}; + uint64_t total[2][2] = {{0}}; + + for (int source_x = COLO_ARENA_MIN_X; + source_x <= COLO_ARENA_MAX_X; + source_x++) { + for (int source_y = COLO_ARENA_MIN_Y; + source_y <= COLO_ARENA_MAX_Y; + source_y++) { + int source_blocked = + exact_static_tile_blocked_reference(source_x, source_y); + for (int target_x = COLO_ARENA_MIN_X; + target_x <= COLO_ARENA_MAX_X; + target_x++) { + for (int target_y = COLO_ARENA_MIN_Y; + target_y <= COLO_ARENA_MAX_Y; + target_y++) { + int target_blocked = + exact_static_tile_blocked_reference( + target_x, target_y); + int raw = osrs_los_tile_ray_clear( + &reference, + source_x, + source_y, + target_x, + target_y); + int topology_raw = col_topology_los_clear( + &ctx, + source_x, + source_y, + 1, + target_x, + target_y, + 1, + 0); + if (raw != topology_raw) { + fprintf(stderr, + "colosseum raw endpoint LoS mismatch (%d,%d)->(%d,%d): topology=%d reference=%d\n", + source_x, + source_y, + target_x, + target_y, + topology_raw, + raw); + abort(); + } + + int range_one = osrs_los_clear( + &reference, + source_x, + source_y, + 1, + target_x, + target_y, + 1, + 1); + if (!range_one) { fprintf(stderr, - "colosseum LoS table mismatch (%d,%d)->(%d,%d): table=%d slow=%d\n", - x0, y0, x1, y1, table, slow); + "colosseum tile LoS range-one law changed (%d,%d)->(%d,%d)\n", + source_x, + source_y, + target_x, + target_y); + abort(); + } + + int ranged = osrs_los_clear( + &reference, + source_x, + source_y, + 1, + target_x, + target_y, + 1, + COLO_ARENA_WIDTH); + int topology_ranged = col_topology_los_clear( + &ctx, + target_x, + target_y, + 1, + source_x, + source_y, + 1, + 0); + if (ranged != topology_ranged) { + fprintf(stderr, + "colosseum ranged endpoint LoS mismatch actor=(%d,%d) target=(%d,%d): topology=%d reference=%d\n", + source_x, + source_y, + target_x, + target_y, + topology_ranged, + ranged); abort(); } + + total[source_blocked][target_blocked]++; + raw_clear[source_blocked][target_blocked] += + (uint64_t)raw; + ranged_clear[source_blocked][target_blocked] += + (uint64_t)ranged; } } } } - printf("colosseum LoS table selftest PASS: %u pairs\n", - (unsigned)COLO_STATIC_LOS_TABLE_BITS); + + printf( + "colosseum endpoint LoS parity PASS: raw clear/total OO=%llu/%llu OB=%llu/%llu BO=%llu/%llu BB=%llu/%llu; ranged OO=%llu OB=%llu BO=%llu BB=%llu; range-one=all-clear\n", + (unsigned long long)raw_clear[0][0], + (unsigned long long)total[0][0], + (unsigned long long)raw_clear[0][1], + (unsigned long long)total[0][1], + (unsigned long long)raw_clear[1][0], + (unsigned long long)total[1][0], + (unsigned long long)raw_clear[1][1], + (unsigned long long)total[1][1], + (unsigned long long)ranged_clear[0][0], + (unsigned long long)ranged_clear[0][1], + (unsigned long long)ranged_clear[1][0], + (unsigned long long)ranged_clear[1][1]); } + static void col_static_footprint_table_selftest(void) { - col_build_static_arena(); + ColosseumContext ctx; + col_init_context_typed(&ctx); + col_finalize_route_topology(&ctx); + col_build_npc_stats(); + int sizes[ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE + 1] = {0}; + for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) + sizes[COLO_NPC_STATS[type].size] = 1; int checks = 0; - int min_x = COLO_STATIC_FOOTPRINT_MIN_X - COLO_STATIC_FOOTPRINT_MAX_SIZE; - int min_y = COLO_STATIC_FOOTPRINT_MIN_Y - COLO_STATIC_FOOTPRINT_MAX_SIZE; - int max_x = COLO_STATIC_FOOTPRINT_MAX_X + COLO_STATIC_FOOTPRINT_MAX_SIZE; - int max_y = COLO_STATIC_FOOTPRINT_MAX_Y + COLO_STATIC_FOOTPRINT_MAX_SIZE; - for (int size = 1; size <= COLO_STATIC_FOOTPRINT_MAX_SIZE; size++) { - for (int x = min_x; x <= max_x; x++) { - for (int y = min_y; y <= max_y; y++) { - int table = col_static_footprint_blocked_lookup(x, y, size); - int slow = col_static_footprint_blocked_reference_slowpath(x, y, size); - if (table != slow) { + for (int size = 1; size <= ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE; size++) { + if (!sizes[size]) continue; + for (int x = COLO_ARENA_MIN_X - size; x <= COLO_ARENA_MAX_X + 1; x++) { + for (int y = COLO_ARENA_MIN_Y - size; y <= COLO_ARENA_MAX_Y + 1; y++) { + int topology = + col_topology_footprint_blocked(&ctx, x, y, size); + int slow = + exact_static_footprint_blocked_reference(x, y, size); + if (topology != slow) { fprintf(stderr, - "colosseum footprint table mismatch (%d,%d) size=%d: table=%d slow=%d\n", - x, y, size, table, slow); + "colosseum topology footprint mismatch (%d,%d) size=%d: topology=%d reference=%d\n", + x, y, size, topology, slow); abort(); } checks++; } } } - printf("colosseum footprint table selftest PASS: %d checks\n", checks); + printf("colosseum topology footprint parity PASS: %d NPC-size checks\n", + checks); } #define col_init_context_typed(ctx_ptr) do { \ @@ -406,7 +589,8 @@ static int exact_attack_route_can_attack( int attack_range ) { ExactAttackRouteContext* route_ctx = (ExactAttackRouteContext*)data; - OsrsLosQuery query = col_player_los_query(route_ctx->walk.s); + OsrsLosQuery query = + col_player_los_query(route_ctx->walk.ctx); return encounter_player_can_attack( player_x, player_y, @@ -523,7 +707,7 @@ static int exact_attack_route_property_scenario( ColosseumContext* ctx, ExactAttackRouteContext* route_ctx ) { - OsrsLosQuery los_query = col_player_los_query(s); + OsrsLosQuery los_query = col_player_los_query(ctx); static const int target_sizes[] = {1, 2, 5}; static const int attack_ranges[] = {1, 3, 6, 10}; int checks = 0; @@ -634,7 +818,8 @@ static void exact_attack_route_property_selftest(void) { for (int y = s.sol.boss_arena_min_y; y <= s.sol.boss_arena_max_y; y++) { - if (!col_static_blocked(x, y)) { + if (!encounter_arena_topology_tile_blocked( + ctx.route_topology, x, y)) { s.player.x = x; s.player.y = y; break; @@ -964,6 +1149,24 @@ static int exact_compare_files(const char* expected_path, const char* actual_pat (unsigned)actual_bytes[byte]); break; } + for (int obs = 0; obs < COLO_NUM_OBS; obs++) { + if (expected_obs[obs] == actual_obs[obs]) continue; + printf( + "first obs mismatch index=%d expected=%g actual=%g\n", + obs, + (double)expected_obs[obs], + (double)actual_obs[obs]); + break; + } + for (int mask = 0; mask < COLO_ACTION_MASK_SIZE; mask++) { + if (expected_mask[mask] == actual_mask[mask]) continue; + printf( + "first mask mismatch index=%d expected=%g actual=%g\n", + mask, + (double)expected_mask[mask], + (double)actual_mask[mask]); + break; + } fclose(expected); fclose(actual); return 1; @@ -984,6 +1187,7 @@ static int exact_compare_files(const char* expected_path, const char* actual_pat int main(int argc, char** argv) { #ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES if (argc == 2 && strcmp(argv[1], "--attack-route-selftest") == 0) { + col_static_los_endpoint_semantics_selftest(); exact_attack_route_property_selftest(); return 0; } @@ -998,6 +1202,7 @@ int main(int argc, char** argv) { } col_static_los_table_selftest(); + col_static_los_endpoint_semantics_selftest(); col_static_footprint_table_selftest(); col_step_out_forecast_landing_selftest(); #ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES diff --git a/ocean/osrs/tests/test_colosseum_golden.c b/ocean/osrs/tests/test_colosseum_golden.c index 0e848c1af4..e0984d3b5d 100644 --- a/ocean/osrs/tests/test_colosseum_golden.c +++ b/ocean/osrs/tests/test_colosseum_golden.c @@ -66,6 +66,7 @@ static uint64_t run_episode(const GoldenConfig* cfg, int max_ticks) { col_init_context_typed(&ctx); ctx.config.start_wave = cfg->public_start_wave - 1; + col_finalize_route_topology(&ctx); memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, cfg->env_seed); diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index 2db016e881..49e8ac7de0 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -3,9 +3,15 @@ #include #include #include +#include +#include #include "ocean/osrs/osrs_item_obs_generated.h" #include "ocean/osrs/encounters/encounter_colosseum.h" +static void col_init_unfinalized_context(ColosseumContext* ctx) { + col_init_context_typed(ctx); +} + #define col_init_context_typed(ctx_ptr) do { \ col_init_context_typed(ctx_ptr); \ @@ -14,6 +20,26 @@ } while (0) #include "ocean/osrs/tests/osrs_test_check.h" +static void assert_child_aborts(const char* label, void (*fn)(void)) { + fflush(NULL); + pid_t pid = fork(); + if (pid == 0) { + fn(); + _exit(0); + } + + int status = 0; + waitpid(pid, &status, 0); + tests_run++; + if (WIFSIGNALED(status) || + (WIFEXITED(status) && WEXITSTATUS(status) != 0)) { + tests_passed++; + } else { + tests_failed++; + printf(" FAIL: %s - child returned successfully\n", label); + } +} + #define TEST_MOD_HAZARD_BASE (COLO_OBS_AFTER_NPCS + COLO_MODIFIER_FLAGS_OBS_SIZE) #define TEST_MOD_OBS_DOOM_LETHAL (TEST_MOD_HAZARD_BASE + 2) @@ -103,12 +129,15 @@ static int first_live_score_enemy(const ColosseumState* s) { return -1; } -static void kill_first_live_score_enemy(ColosseumState* s) { +static void kill_first_live_score_enemy( + ColosseumState* s, + ColosseumContext* ctx +) { int slot = first_live_score_enemy(s); CHECK("a live score enemy exists", slot >= 0); if (slot < 0) return; s->npcs[slot].hp = 0; - col_apply_npc_death(s, slot); + col_apply_npc_death(s, ctx, slot); } static float score_for_depth(float depth) { @@ -116,8 +145,12 @@ static float score_for_depth(float depth) { return COLO_SCORE_LOSS_CEILING * ratio * ratio; } -static void land_pending_player_hits(ColosseumState* s) { - for (int t = 0; t < 4; t++) col_resolve_player_projectiles_on_npcs(s); +static void land_pending_player_hits( + ColosseumState* s, + ColosseumContext* ctx +) { + for (int t = 0; t < 4; t++) + col_resolve_player_projectiles_on_npcs(s, ctx); } static void geo_clear_npcs(ColosseumState* s) { @@ -759,7 +792,7 @@ static void test_draft_offer_and_select(void) { CHECK("selection logged", s.log.modifiers_picked == 1); s.wave = 1; - col_spawn_wave(&s); + col_spawn_wave(&s, &ctx); CHECK("modifier persists across waves", col_mod_active(&s, (ColoModifier)chosen)); int rfdd_before_window = 0, rfdd_in_window = 0, boss_excluded_seen = 0; @@ -932,7 +965,7 @@ static void test_quartet_extra_spawn(void) { memset(&base, 0, sizeof(base)); col_reset_ctx((EncounterState*)&base, (EncounterContext*)&ctx, 5); base.wave = 0; - col_spawn_wave(&base); + col_spawn_wave(&base, &ctx); int base_count = 0; for (int i = 0; i < COLO_MAX_NPCS; i++) if (base.npcs[i].active) base_count++; @@ -942,7 +975,7 @@ static void test_quartet_extra_spawn(void) { q.modifiers.active_mask |= (1u << COLO_MOD_QUARTET); q.modifiers.tier[COLO_MOD_QUARTET] = 1; q.wave = 0; - col_spawn_wave(&q); + col_spawn_wave(&q, &ctx); int q_count = 0; for (int i = 0; i < COLO_MAX_NPCS; i++) if (q.npcs[i].active) q_count++; @@ -954,7 +987,7 @@ static void test_quartet_extra_spawn(void) { q12.modifiers.active_mask |= (1u << COLO_MOD_QUARTET); q12.modifiers.tier[COLO_MOD_QUARTET] = 1; q12.wave = COLO_WAVE_BOSS; - col_spawn_wave(&q12); + col_spawn_wave(&q12, &ctx); int sol = 0, warband = 0; for (int i = 0; i < COLO_MAX_NPCS; i++) { if (!q12.npcs[i].active) continue; @@ -976,7 +1009,7 @@ static void test_bees_hazard(void) { s.modifiers.active_mask |= (1u << COLO_MOD_BEES); s.modifiers.tier[COLO_MOD_BEES] = 2; s.wave = 0; - col_spawn_wave(&s); + col_spawn_wave(&s, &ctx); int bee_npcs = 0; for (int i = 0; i < COLO_MAX_NPCS; i++) @@ -1042,8 +1075,8 @@ static void test_bees_hazard(void) { s.player.y = bee_npc->y; int slot = s.bees[0].npc_slot; - col_player_attack_target(&s, slot); - land_pending_player_hits(&s); + col_player_attack_target_ctx(&s, &ctx, slot); + land_pending_player_hits(&s, &ctx); CHECK("a single hit kills the swarm", !s.npcs[slot].active); CHECK("the killed swarm enters its 50-tick respawn", s.bees[0].phase == COLO_HAZARD_RESPAWNING && @@ -1066,7 +1099,7 @@ static void test_bees_hazard(void) { sc.modifiers.active_mask |= (1u << COLO_MOD_BEES); sc.modifiers.tier[COLO_MOD_BEES] = 1; sc.wave = 0; - col_spawn_wave(&sc); + col_spawn_wave(&sc, &ctx); sc.wave_spawn_delay = 0; int live_bee = 0; for (int i = 0; i < COLO_MAX_NPCS; i++) @@ -1102,16 +1135,16 @@ static void test_totem_lifecycle(void) { col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 12, 16); s.npcs[0].hp = 70; - col_mod_on_npc_hp_changed(&s, 0); + col_mod_on_npc_hp_changed(&s, ctx.route_topology, 0); CHECK("no totem above 50% HP", s.totems[0].phase == COLO_HAZARD_NONE); s.npcs[0].hp = 60; - col_mod_on_npc_hp_changed(&s, 0); + col_mod_on_npc_hp_changed(&s, ctx.route_topology, 0); CHECK("crossing <=50% spawns a totem", s.totems[0].phase == COLO_HAZARD_ALIVE); int tslot = s.totems[0].npc_slot; CHECK("the totem is a live 1-HP NPC beside its owner", tslot >= 0 && s.npcs[tslot].active && s.npcs[tslot].type == COLO_HEALING_TOTEM && s.npcs[tslot].hp == 1); - col_mod_on_npc_hp_changed(&s, 0); + col_mod_on_npc_hp_changed(&s, ctx.route_topology, 0); CHECK("no duplicate totem for the same owner", s.totems[0].phase == COLO_HAZARD_ALIVE); CHECK("the totem spawns on the owner's south-west tile", @@ -1119,27 +1152,30 @@ static void test_totem_lifecycle(void) { int heal = s.npcs[0].max_hp * COLO_TOTEM_HEAL_PCT / 100; for (int t = 0; t < COLO_TOTEM_SPAWN_HEAL_DELAY + COLO_TOTEM_PROJECTILE_TICKS - 1; t++) - col_mod_tick_totems(&s); + col_mod_tick_totems(&s, ctx.route_topology); CHECK("no heal lands before the spawn delay plus projectile flight", s.npcs[0].hp == 60); - col_mod_tick_totems(&s); + col_mod_tick_totems(&s, ctx.route_topology); CHECK("the first heal restores 30% of the owner's max HP", s.npcs[0].hp == 60 + heal); - for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) + col_mod_tick_totems(&s, ctx.route_topology); CHECK("heals land every 7 ticks and continue above 50%", s.npcs[0].hp == 60 + 2 * heal || s.npcs[0].hp == s.npcs[0].max_hp); - for (int t = 0; t < 4 * COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + for (int t = 0; t < 4 * COLO_TOTEM_HEAL_INTERVAL; t++) + col_mod_tick_totems(&s, ctx.route_topology); CHECK("the totem heals the owner back to full and then stops", s.npcs[0].hp == s.npcs[0].max_hp); s.npcs[0].hp = 50; for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL - COLO_TOTEM_PROJECTILE_TICKS; t++) - col_mod_tick_totems(&s); + col_mod_tick_totems(&s, ctx.route_topology); CHECK("a heal projectile is in flight before it lands", s.totems[0].projectile_timer > 0 && s.npcs[0].hp == 50); - col_player_attack_target(&s, tslot); - land_pending_player_hits(&s); - for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + col_player_attack_target_ctx(&s, &ctx, tslot); + land_pending_player_hits(&s, &ctx); + for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) + col_mod_tick_totems(&s, ctx.route_topology); CHECK("destroying the totem cancels the in-flight heal", s.npcs[0].hp == 50 && s.totems[0].phase == COLO_HAZARD_RESPAWNING); s.totems[0].phase = COLO_HAZARD_ALIVE; @@ -1147,15 +1183,16 @@ static void test_totem_lifecycle(void) { s.npcs[tslot].active = 1; s.npcs[tslot].hp = 1; - col_player_attack_target(&s, tslot); - land_pending_player_hits(&s); + col_player_attack_target_ctx(&s, &ctx, tslot); + land_pending_player_hits(&s, &ctx); CHECK("a single attack destroys the totem", !s.npcs[tslot].active); CHECK("destruction arms the 200-tick respawn", s.totems[0].phase == COLO_HAZARD_RESPAWNING && s.totems[0].respawn_timer == COLO_TOTEM_RESPAWN_TICKS); - for (int t = 0; t < COLO_TOTEM_RESPAWN_TICKS - 1; t++) col_mod_tick_totems(&s); + for (int t = 0; t < COLO_TOTEM_RESPAWN_TICKS - 1; t++) + col_mod_tick_totems(&s, ctx.route_topology); CHECK("still down one tick early", s.totems[0].phase == COLO_HAZARD_RESPAWNING); - col_mod_tick_totems(&s); + col_mod_tick_totems(&s, ctx.route_topology); CHECK("the totem respawns exactly 200 ticks after destruction", s.totems[0].phase == COLO_HAZARD_ALIVE && s.npcs[s.totems[0].npc_slot].active && @@ -1163,19 +1200,19 @@ static void test_totem_lifecycle(void) { int tslot2 = s.totems[0].npc_slot; s.npcs[0].hp = 0; - col_apply_npc_death(&s, 0); + col_apply_npc_death(&s, &ctx, 0); CHECK("the owner's death despawns its totem", !s.npcs[tslot2].active && s.totems[0].phase == COLO_HAZARD_NONE); col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 12, 16); s.npcs[0].hp = 60; - col_mod_on_npc_hp_changed(&s, 0); + col_mod_on_npc_hp_changed(&s, ctx.route_topology, 0); int tslot3 = s.totems[0].npc_slot; - col_player_attack_target(&s, tslot3); - land_pending_player_hits(&s); + col_player_attack_target_ctx(&s, &ctx, tslot3); + land_pending_player_hits(&s, &ctx); CHECK("second totem down and respawning", s.totems[0].phase == COLO_HAZARD_RESPAWNING); s.npcs[0].hp = 0; - col_apply_npc_death(&s, 0); + col_apply_npc_death(&s, &ctx, 0); CHECK("the owner's death cancels a pending totem respawn", s.totems[0].phase == COLO_HAZARD_NONE); @@ -1196,10 +1233,10 @@ static void test_totemic_sol_wave12(void) { CHECK("Sol is live", sol >= 0); s.npcs[sol].hp = COLO_SOL_HP_MAX * 60 / 100; - col_mod_on_npc_hp_changed(&s, sol); + col_mod_on_npc_hp_changed(&s, ctx.route_topology, sol); CHECK("no totem while Sol is above 50%", col_totem_for_owner(&s, sol) == NULL); s.npcs[sol].hp = COLO_SOL_HP_MAX / 2; - col_mod_on_npc_hp_changed(&s, sol); + col_mod_on_npc_hp_changed(&s, ctx.route_topology, sol); ColoTotem* sol_totem = col_totem_for_owner(&s, sol); CHECK("Sol at 50% spawns a totem", sol_totem && sol_totem->phase == COLO_HAZARD_ALIVE); @@ -1212,19 +1249,21 @@ static void test_totemic_sol_wave12(void) { int hp0 = s.npcs[sol].hp; for (int t = 0; t < COLO_TOTEM_SPAWN_HEAL_DELAY + COLO_TOTEM_PROJECTILE_TICKS - 1; t++) - col_mod_tick_totems(&s); + col_mod_tick_totems(&s, ctx.route_topology); CHECK("no Sol heal before the spawn delay plus projectile flight", s.npcs[sol].hp == hp0); - col_mod_tick_totems(&s); + col_mod_tick_totems(&s, ctx.route_topology); CHECK("the pulse heals Sol exactly 75", s.npcs[sol].hp == hp0 + COLO_TOTEM_SOL_HEAL); - for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) + col_mod_tick_totems(&s, ctx.route_topology); CHECK("Sol keeps healing 75/7t even above 50% (until destroyed)", s.npcs[sol].hp == hp0 + 2 * COLO_TOTEM_SOL_HEAL); - col_player_attack_target(&s, tslot); - land_pending_player_hits(&s); + col_player_attack_target_ctx(&s, &ctx, tslot); + land_pending_player_hits(&s, &ctx); int hp1 = s.npcs[sol].hp; - for (int t = 0; t < 3 * COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + for (int t = 0; t < 3 * COLO_TOTEM_HEAL_INTERVAL; t++) + col_mod_tick_totems(&s, ctx.route_topology); sol_totem = col_totem_for_owner(&s, sol); CHECK("a destroyed totem stops the Sol heal (until the 200t respawn)", !s.npcs[tslot].active && s.npcs[sol].hp == hp1 && @@ -1260,7 +1299,7 @@ static void test_obs_signal_defects(void) { col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 16); s.npcs[0].hp = 60; - col_mod_on_npc_hp_changed(&s, 0); + col_mod_on_npc_hp_changed(&s, ctx.route_topology, 0); int tslot = col_totem_for_owner(&s, 0)->npc_slot; s.npcs[tslot].x = s.player.x + 1; s.npcs[tslot].y = s.player.y; @@ -1303,7 +1342,7 @@ static void test_totem_heal_timing_obs(void) { col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 12, 16); s.npcs[0].hp = 60; - col_mod_on_npc_hp_changed(&s, 0); + col_mod_on_npc_hp_changed(&s, ctx.route_topology, 0); int tslot = s.totems[0].npc_slot; CHECK("fixture: totem spawned", tslot >= 0); @@ -1334,14 +1373,15 @@ static void test_totem_heal_timing_obs(void) { CHECK("the totem features cost no extra record width", COLO_FEATURES_PER_NPC == 23); - for (int t = 0; t < COLO_TOTEM_SPAWN_HEAL_DELAY; t++) col_mod_tick_totems(&s); + for (int t = 0; t < COLO_TOTEM_SPAWN_HEAL_DELAY; t++) + col_mod_tick_totems(&s, ctx.route_topology); col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); CHECK("once launched the projectile is visible and the launch cue clears", s.totems[0].projectile_timer > 0 && obs[flight_idx] > 0.0f && obs[launch_idx] == 0.0f); float flight_before = obs[flight_idx]; - col_mod_tick_totems(&s); + col_mod_tick_totems(&s, ctx.route_topology); col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); CHECK("the in-flight cue counts down toward the landing tick", obs[flight_idx] < flight_before && obs[flight_idx] > 0.0f); @@ -1367,7 +1407,7 @@ static void test_totem_sol_obs_reports_stacking(void) { s.modifiers.tier[COLO_MOD_TOTEMIC] = 1; int sol = col_sol_find_idx(&s); s.npcs[sol].hp = COLO_SOL_HP_MAX / 2; - col_mod_on_npc_hp_changed(&s, sol); + col_mod_on_npc_hp_changed(&s, ctx.route_topology, sol); static float obs[COLO_NUM_OBS]; col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); @@ -1386,7 +1426,8 @@ static void test_totem_sol_obs_reports_stacking(void) { fabsf(obs[tbase + COLO_NPC_TELLS_OFFSET + 2] - (float)COLO_TOTEM_SOL_HEAL / (float)COLO_SOL_HP_MAX) < 1e-4f); - for (int t = 0; t < COLO_TOTEM_SOL_EXTRA_INTERVAL; t++) col_mod_tick_totems(&s); + for (int t = 0; t < COLO_TOTEM_SOL_EXTRA_INTERVAL; t++) + col_mod_tick_totems(&s, ctx.route_topology); col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); CHECK("a second stacked totem raises the observed live count", count_live_totems(&s) == 2 && @@ -1410,22 +1451,24 @@ static void test_totemic_sol_extra_totems_every_two_minutes(void) { CHECK("Sol is live", sol >= 0); s.npcs[sol].hp = COLO_SOL_HP_MAX / 2; - col_mod_on_npc_hp_changed(&s, sol); + col_mod_on_npc_hp_changed(&s, ctx.route_topology, sol); int totems_after_first = count_live_totems(&s); CHECK("exactly one totem at the 50% trigger", totems_after_first == 1); - for (int t = 0; t < COLO_TOTEM_SOL_EXTRA_INTERVAL - 1; t++) col_mod_tick_totems(&s); + for (int t = 0; t < COLO_TOTEM_SOL_EXTRA_INTERVAL - 1; t++) + col_mod_tick_totems(&s, ctx.route_topology); CHECK("no extra totem one tick early", count_live_totems(&s) == 1); - col_mod_tick_totems(&s); + col_mod_tick_totems(&s, ctx.route_topology); CHECK("a second totem spawns two minutes after the first", count_live_totems(&s) == 2); - for (int t = 0; t < COLO_TOTEM_SOL_EXTRA_INTERVAL; t++) col_mod_tick_totems(&s); + for (int t = 0; t < COLO_TOTEM_SOL_EXTRA_INTERVAL; t++) + col_mod_tick_totems(&s, ctx.route_topology); CHECK("a third totem spawns two minutes later", count_live_totems(&s) == 3); s.npcs[sol].hp = 0; - col_apply_npc_death(&s, sol); + col_apply_npc_death(&s, &ctx, sol); CHECK("Sol's death despawns every totem it owned", count_live_totems(&s) == 0); } @@ -1441,7 +1484,8 @@ static void test_reentry_sand_tiles(void) { s.modifiers.active_mask |= (1u << COLO_MOD_REENTRY); s.modifiers.tier[COLO_MOD_REENTRY] = 1; - col_mod_reentry_on_skyfall(&s, 20, 12); + col_mod_reentry_on_skyfall( + &s, ctx.route_topology, 20, 12); CHECK("T1 leaves one pool on the targeted tile", s.molten_count == 1 && s.molten_x[0] == 20 && s.molten_y[0] == 12); CHECK("the T1 pool is the stronger Reentry kind", @@ -1470,7 +1514,8 @@ static void test_reentry_sand_tiles(void) { CHECK("T1 (temporary) clears at wave end", s.molten_count == 0); s.modifiers.tier[COLO_MOD_REENTRY] = 2; - col_mod_reentry_on_skyfall(&s, 20, 12); + col_mod_reentry_on_skyfall( + &s, ctx.route_topology, 20, 12); int has_target = 0, has_sw = 0, has_w = 0, all_reentry = 1; for (int i = 0; i < s.molten_count; i++) { if (s.molten_x[i] == 20 && s.molten_y[i] == 12) has_target = 1; @@ -1486,7 +1531,8 @@ static void test_reentry_sand_tiles(void) { s.molten_count = 0; s.modifiers.tier[COLO_MOD_REENTRY] = 3; - col_mod_reentry_on_skyfall(&s, 20, 12); + col_mod_reentry_on_skyfall( + &s, ctx.route_topology, 20, 12); has_target = has_sw = has_w = 0; for (int i = 0; i < s.molten_count; i++) { if (s.molten_x[i] == 20 && s.molten_y[i] == 12) has_target = 1; @@ -1503,7 +1549,8 @@ static void test_reentry_sand_tiles(void) { s.modifiers.active_mask |= (1u << COLO_MOD_VOLATILITY); s.modifiers.tier[COLO_MOD_VOLATILITY] = 3; s.player.x = 5; s.player.y = 18; - col_mod_volatility_on_death(&s, 20, 16, 1); + col_mod_volatility_on_death( + &s, ctx.route_topology, 20, 16, 1); CHECK("Volatility T3 leaves a Volatility pool at the centre", s.molten_count == 1 && s.molten_kind[0] == COLO_POOL_VOLATILITY); s.player.x = 20; @@ -1521,7 +1568,8 @@ static void test_reentry_sand_tiles(void) { s.molten_count == 0); s.modifiers.tier[COLO_MOD_REENTRY] = 2; - col_mod_volatility_on_death(&s, 20, 16, 1); + col_mod_volatility_on_death( + &s, ctx.route_topology, 20, 16, 1); CHECK("Reentry II makes the Volatility III pool permanent", s.molten_count == 1 && s.molten_lifetime[0] == COLO_POOL_PERMANENT); col_modifiers_on_wave_spawn(&s); @@ -1530,7 +1578,8 @@ static void test_reentry_sand_tiles(void) { s.molten_count = 0; s.modifiers.tier[COLO_MOD_REENTRY] = 3; - col_mod_volatility_on_death(&s, 20, 16, 1); + col_mod_volatility_on_death( + &s, ctx.route_topology, 20, 16, 1); CHECK("Reentry III also makes the Volatility III pool permanent", s.molten_count == 1 && s.molten_lifetime[0] == COLO_POOL_PERMANENT); } @@ -1756,7 +1805,7 @@ static void test_mantimayhem_stress(void) { s.modifiers.active_mask |= (1u << COLO_MOD_MANTIMAYHEM); s.modifiers.tier[COLO_MOD_MANTIMAYHEM] = 2; s.wave = 8; - col_spawn_wave(&s); + col_spawn_wave(&s, &ctx); int manticores = 0; for (int i = 0; i < COLO_MAX_NPCS; i++) @@ -1824,7 +1873,7 @@ static void test_solarflare_orb(void) { s.modifiers.active_mask |= (1u << COLO_MOD_SOLARFLARE); s.modifiers.tier[COLO_MOD_SOLARFLARE] = 2; s.wave = 0; - col_spawn_wave(&s); + col_spawn_wave(&s, &ctx); CHECK("Solarflare orb is active", s.solarflare.active); int geometry_ok = 1; @@ -1842,7 +1891,7 @@ static void test_solarflare_orb(void) { int x, y; col_solarflare_tile(&s, p, step, &x, &y); if (!sf_tile_on_pillar_perimeter(p, x, y)) geometry_ok = 0; - if (col_static_blocked(x, y)) geometry_ok = 0; + if (col_topology_tile_blocked(&ctx, x, y)) geometry_ok = 0; } for (int c = 0; c < 4; c++) { int x, y; @@ -1970,7 +2019,7 @@ static void test_volatility_explosion(void) { col_init_npc(&s, idx, COLO_FREMENNIK_BERSERKER, 18, 17); int hp_before = s.player.current_hitpoints; s.npcs[idx].hp = 0; - col_apply_npc_death(&s, idx); + col_apply_npc_death(&s, &ctx, idx); CHECK("Volatility explosion hits an adjacent player", s.player.current_hitpoints < hp_before); } @@ -2143,7 +2192,7 @@ static void test_death_linger_wave_clear_and_render(void) { &s.npcs[idx].hp, &s.npcs[idx].hit_landed_this_tick, &s.npcs[idx].hit_damage, 1); s.npcs[idx].hit_was_successful_this_tick = dealt > 0; - col_apply_npc_death(&s, idx); + col_apply_npc_death(&s, &ctx, idx); int linger_ticks = col_npc_death_linger_ticks(COLO_FREMENNIK_BERSERKER); CHECK("lethal hit starts NPC death linger", s.npcs[idx].active && @@ -2172,12 +2221,16 @@ static void test_death_linger_wave_clear_and_render(void) { static void test_static_arena_mask(void) { printf("test_static_arena_mask\n"); col_build_npc_stats(); + ColosseumContext ctx; + col_init_context_typed(&ctx); int gate_rows_ok = 1; for (int x = 0; x <= 33; x++) { int walkable = (x == 13 || x == 14 || x == 19 || x == 20); - if (col_static_blocked(x, 0) != !walkable) gate_rows_ok = 0; - if (col_static_blocked(x, 33) != !walkable) gate_rows_ok = 0; + if (col_topology_tile_blocked(&ctx, x, 0) != !walkable) + gate_rows_ok = 0; + if (col_topology_tile_blocked(&ctx, x, 33) != !walkable) + gate_rows_ok = 0; } CHECK("south+north inner rows walkable exactly at the gate flanks {13,14,19,20}", gate_rows_ok); @@ -2185,27 +2238,38 @@ static void test_static_arena_mask(void) { int west_ok = 1; for (int y = 0; y <= 33; y++) { int walkable = (y == 13 || y == 14 || y == 19 || y == 20); - if (col_static_blocked(0, y) != !walkable) west_ok = 0; + if (col_topology_tile_blocked(&ctx, 0, y) != !walkable) + west_ok = 0; } CHECK("west col 0 open exactly at the entrance rows {13,14,19,20}", west_ok); int east_ok = 1; for (int y = 0; y <= 33; y++) - if (!col_static_blocked(33, y)) east_ok = 0; + if (!col_topology_tile_blocked(&ctx, 33, y)) east_ok = 0; CHECK("east col 33 fully walled", east_ok); - CHECK("row 3 west extent [0,5)", col_static_blocked(4, 3) && !col_static_blocked(5, 3)); - CHECK("row 30 west extent [0,6)", col_static_blocked(5, 30) && !col_static_blocked(6, 30)); - CHECK("row 29 east extent [29,34)", !col_static_blocked(28, 29) && col_static_blocked(29, 29)); + CHECK("row 3 west extent [0,5)", + col_topology_tile_blocked(&ctx, 4, 3) && + !col_topology_tile_blocked(&ctx, 5, 3)); + CHECK("row 30 west extent [0,6)", + col_topology_tile_blocked(&ctx, 5, 30) && + !col_topology_tile_blocked(&ctx, 6, 30)); + CHECK("row 29 east extent [29,34)", + !col_topology_tile_blocked(&ctx, 28, 29) && + col_topology_tile_blocked(&ctx, 29, 29)); int pillars_ok = 1, rim_ok = 1; for (int p = 0; p < COLO_NUM_PILLARS; p++) { int px = COLO_PILLARS[p][0], py = COLO_PILLARS[p][1]; for (int dx = 0; dx < 3; dx++) for (int dy = 0; dy < 3; dy++) - if (!col_static_blocked(px + dx, py + dy)) pillars_ok = 0; - if (col_static_blocked(px - 1, py + 1)) rim_ok = 0; - if (col_static_blocked(px + 3, py + 1)) rim_ok = 0; + if (!col_topology_tile_blocked( + &ctx, px + dx, py + dy)) + pillars_ok = 0; + if (col_topology_tile_blocked(&ctx, px - 1, py + 1)) + rim_ok = 0; + if (col_topology_tile_blocked(&ctx, px + 3, py + 1)) + rim_ok = 0; } CHECK("all 36 pillar tiles blocked on every wave", pillars_ok); CHECK("tiles flanking each pillar stay walkable", rim_ok); @@ -2214,18 +2278,25 @@ static void test_static_arena_mask(void) { for (int a = 0; a < COLO_NUM_SPAWN_ANCHORS; a++) for (int dx = 0; dx < COLO_SPAWN_ZONE_SIZE; dx++) for (int dy = 0; dy < COLO_SPAWN_ZONE_SIZE; dy++) - if (col_static_blocked(COLO_SPAWN_ANCHORS[a][0] + dx, - COLO_SPAWN_ANCHORS[a][1] + dy)) zones_ok = 0; + if (col_topology_tile_blocked( + &ctx, + COLO_SPAWN_ANCHORS[a][0] + dx, + COLO_SPAWN_ANCHORS[a][1] + dy)) + zones_ok = 0; CHECK("every 3x3 spawn-anchor zone fully walkable on the static mask", zones_ok); CHECK("wave start (7,18) walkable", - !col_static_blocked(COLO_PLAYER_START_X, COLO_PLAYER_START_Y)); + !col_topology_tile_blocked( + &ctx, COLO_PLAYER_START_X, COLO_PLAYER_START_Y)); CHECK("boss start (16,10) walkable", - !col_static_blocked(COLO_BOSS_PLAYER_START_X, COLO_BOSS_PLAYER_START_Y)); + !col_topology_tile_blocked( + &ctx, COLO_BOSS_PLAYER_START_X, COLO_BOSS_PLAYER_START_Y)); int sol_ok = 1; for (int dx = 0; dx < 5; dx++) for (int dy = 0; dy < 5; dy++) - if (col_static_blocked(COLO_SOL_SPAWN_X + dx, COLO_SOL_SPAWN_Y + dy)) sol_ok = 0; + if (col_topology_tile_blocked( + &ctx, COLO_SOL_SPAWN_X + dx, COLO_SOL_SPAWN_Y + dy)) + sol_ok = 0; CHECK("Sol's 5x5 footprint at (16,19) unblocked", sol_ok); } @@ -2238,16 +2309,22 @@ static void test_static_los_and_attack_gate(void) { col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 31); geo_clear_npcs(&s); - CHECK("SW pillar blocks a ray along row 9", !col_tiles_have_los(&s, 7, 9, 12, 9)); - CHECK("pillar block is symmetric", !col_tiles_have_los(&s, 12, 9, 7, 9)); - CHECK("ray one row north of the pillar is clear", col_tiles_have_los(&s, 7, 12, 12, 12)); - CHECK("north gate doors block along the inner row", !col_tiles_have_los(&s, 14, 33, 19, 33)); - CHECK("row 32 inside the north gate is clear", col_tiles_have_los(&s, 14, 32, 19, 32)); + CHECK("SW pillar blocks a ray along row 9", + !col_topology_los_clear(&ctx, 7, 9, 1, 12, 9, 1, 0)); + CHECK("pillar block is symmetric", + !col_topology_los_clear(&ctx, 12, 9, 1, 7, 9, 1, 0)); + CHECK("ray one row north of the pillar is clear", + col_topology_los_clear(&ctx, 7, 12, 1, 12, 12, 1, 0)); + CHECK("north gate doors block along the inner row", + !col_topology_los_clear(&ctx, 14, 33, 1, 19, 33, 1, 0)); + CHECK("row 32 inside the north gate is clear", + col_topology_los_clear(&ctx, 14, 32, 1, 19, 32, 1, 0)); s.player.x = 5; s.player.y = 9; col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 9); s.npcs[0].attack_timer = 0; - CHECK("shaman behind the pillar has no LoS", !col_npc_has_los_to_player(&s, &s.npcs[0])); + CHECK("shaman behind the pillar has no LoS", + !col_npc_has_los_to_player(&s, &ctx, &s.npcs[0])); col_npc_attack_ctx(&s, &ctx, 0); CHECK("no-LoS shaman holds fire", s.npcs[0].attacked_this_tick == 0); col_npc_move_ctx(&s, &ctx, 0); @@ -2257,7 +2334,8 @@ static void test_static_los_and_attack_gate(void) { s.player.x = 5; s.player.y = 12; col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 12); s.npcs[0].attack_timer = 0; - CHECK("clear-row shaman has LoS", col_npc_has_los_to_player(&s, &s.npcs[0])); + CHECK("clear-row shaman has LoS", + col_npc_has_los_to_player(&s, &ctx, &s.npcs[0])); col_npc_attack_ctx(&s, &ctx, 0); CHECK("clear-row shaman attacks", s.npcs[0].attacked_this_tick == 1); } @@ -2274,7 +2352,8 @@ static void test_spawn_anchor_exclusion(void) { geo_clear_npcs(&s); s.player.x = 5; s.player.y = 18; int cand[COLO_NUM_SPAWN_ANCHORS]; - int n = col_spawn_anchor_candidates(&s, cand); + int n = col_spawn_anchor_candidates( + &s, ctx.route_topology, cand); CHECK("b5 spawn-fix tile leaves exactly 10 candidate anchors", n == 10); int suppressed_ok = 1; for (int i = 0; i < n; i++) @@ -2286,7 +2365,7 @@ static void test_spawn_anchor_exclusion(void) { for (int rep = 0; rep < 30; rep++) { s.player.x = 5; s.player.y = 18; s.wave = 4; - col_spawn_wave(&s); + col_spawn_wave(&s, &ctx); int used[COLO_NUM_SPAWN_ANCHORS] = {0}; int archer_x = -1, archer_y = -1; for (int i = 0; i < COLO_MAX_NPCS; i++) { @@ -2299,7 +2378,9 @@ static void test_spawn_anchor_exclusion(void) { int size = col_npc_effective_size(npc); for (int dx = 0; dx < size; dx++) for (int dy = 0; dy < size; dy++) - if (col_static_blocked(npc->x + dx, npc->y + dy)) unblocked_ok = 0; + if (col_topology_tile_blocked( + &ctx, npc->x + dx, npc->y + dy)) + unblocked_ok = 0; if (col_type_is_warbander(npc->type)) { if (npc->type == COLO_FREMENNIK_ARCHER) { @@ -2346,7 +2427,7 @@ static void test_reinforcement_gates(void) { geo_clear_npcs(&s); s.player.x = 16; s.player.y = north ? 16 : 15; - col_spawn_reinforcements(&s); + col_spawn_reinforcements(&s, &ctx); int count = 0, in_gap_ok = 1, row_ok = 1; for (int i = 0; i < COLO_MAX_NPCS; i++) { @@ -2361,7 +2442,9 @@ static void test_reinforcement_gates(void) { row_ok = 0; for (int dx = 0; dx < size; dx++) for (int dy = 0; dy < size; dy++) - if (col_static_blocked(npc->x + dx, npc->y + dy)) row_ok = 0; + if (col_topology_tile_blocked( + &ctx, npc->x + dx, npc->y + dy)) + row_ok = 0; } CHECK("reinforcement set spawned (minotaur + shaman)", count == 2); CHECK("reinforcements land inside the gate gap x 15-18", in_gap_ok); @@ -2417,7 +2500,7 @@ static void test_outcome_score_reinforcement_grows_denominator(void) { s.current_wave_fresh_damage = 0.3f * (float)pool_before; float score_before = col_episode_outcome_score(&s); - col_spawn_reinforcements(&s); + col_spawn_reinforcements(&s, &ctx); CHECK("reinforcements enter the score denominator", s.current_wave_total_killable == 9); CHECK("reinforcements grow the HP pool", s.current_wave_hp_pool > pool_before); @@ -2448,7 +2531,7 @@ static void test_fresh_damage_not_farmable_via_healing(void) { col_rebuild_player_collision_flags(&s); col_queue_npc_pending_hit(&s, 0, 70, 1, ATTACK_STYLE_MELEE, ENCOUNTER_SPELL_NONE); - land_pending_player_hits(&s); + land_pending_player_hits(&s, &ctx); CHECK("first hit lands for 70", s.npcs[0].hp == 30); CHECK("fresh damage tracks the first hit", fabsf(s.current_wave_fresh_damage - 70.0f) < 0.001f); @@ -2458,12 +2541,12 @@ static void test_fresh_damage_not_farmable_via_healing(void) { CHECK("healing does not touch min_hp_seen", s.npcs[0].min_hp_seen == 30); col_queue_npc_pending_hit(&s, 0, 30, 1, ATTACK_STYLE_MELEE, ENCOUNTER_SPELL_NONE); - land_pending_player_hits(&s); + land_pending_player_hits(&s, &ctx); CHECK("re-damaging restored HP credits no fresh damage", fabsf(s.current_wave_fresh_damage - 70.0f) < 0.001f); col_queue_npc_pending_hit(&s, 0, 20, 1, ATTACK_STYLE_MELEE, ENCOUNTER_SPELL_NONE); - land_pending_player_hits(&s); + land_pending_player_hits(&s, &ctx); CHECK("a new low credits only the fresh portion (20)", fabsf(s.current_wave_fresh_damage - 90.0f) < 0.001f); CHECK("min_hp_seen follows the deeper low", s.npcs[0].min_hp_seen == 10); @@ -2481,7 +2564,8 @@ static void test_outcome_score_wave_clear_has_no_double_count(void) { col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 303); complete_open_draft(&s, &ctx, 1); - while (first_live_score_enemy(&s) >= 0) kill_first_live_score_enemy(&s); + while (first_live_score_enemy(&s) >= 0) + kill_first_live_score_enemy(&s, &ctx); s.log.waves_cleared = 1; CHECK("within-wave progress is zero after all wave enemies are killed", col_current_wave_score_progress(&s) == 0.0f); @@ -2517,7 +2601,7 @@ static void test_roster_cap_nine(void) { s.modifiers.tier[COLO_MOD_QUARTET] = 1; s.modifiers.tier[COLO_MOD_DYNAMIC_DUO] = 1; s.wave = 7; - col_spawn_wave(&s); + col_spawn_wave(&s, &ctx); int count = 0; for (int i = 0; i < COLO_MAX_NPCS; i++) if (s.npcs[i].active) count++; CHECK("wave 8 + Quartet + Dynamic Duo spawns all 9 NPCs", count == 9); @@ -2549,7 +2633,7 @@ static void test_wave12_quartet_and_win(void) { s.modifiers.active_mask |= (1u << COLO_MOD_QUARTET); s.modifiers.tier[COLO_MOD_QUARTET] = 1; s.wave = COLO_WAVE_BOSS; - col_spawn_wave(&s); + col_spawn_wave(&s, &ctx); s.wave_spawn_delay = 0; int wb = -1; @@ -2559,7 +2643,8 @@ static void test_wave12_quartet_and_win(void) { int wx = s.npcs[wb].x, wy = s.npcs[wb].y; int cheb_dx = abs(wx - s.player.x), cheb_dy = abs(wy - s.player.y); if (wx < 9 || wx > 24 || wy < 9 || wy > 24) placement_ok = 0; - if (col_static_blocked(wx, wy)) placement_ok = 0; + if (col_topology_tile_blocked(&ctx, wx, wy)) + placement_ok = 0; if ((cheb_dx > cheb_dy ? cheb_dx : cheb_dy) <= COLO_SPAWN_EXCLUSION_CHEB) placement_ok = 0; @@ -2575,7 +2660,9 @@ static void test_wave12_quartet_and_win(void) { int nx = cx + D[d][0], ny = cy + D[d][1]; if (nx == wx && ny == wy) { reached = 1; break; } if (nx < 9 || nx > 24 || ny < 9 || ny > 24) continue; - if (seen[nx][ny] || col_static_blocked(nx, ny)) continue; + if (seen[nx][ny] || + col_topology_tile_blocked(&ctx, nx, ny)) + continue; int gx, gy; if (!col_grid_index(nx, ny, &gx, &gy)) continue; if (s.npc_collision_flags[gx][gy]) continue; @@ -2588,7 +2675,7 @@ static void test_wave12_quartet_and_win(void) { int sol = col_sol_find_idx(&s); if (sol < 0) { win_ok = 0; continue; } s.npcs[sol].hp = 0; - col_apply_npc_death(&s, sol); + col_apply_npc_death(&s, &ctx, sol); int idle[COLO_NUM_ACTION_HEADS] = {0}; step_and_observe(&s, &ctx, idle); if (!(s.episode_over && s.winner == COLO_OUTCOME_PLAYER_WON)) win_ok = 0; @@ -2615,7 +2702,7 @@ static void test_player_walks_through_npc_footprint(void) { CHECK("NPC footprint remains stamped for NPC systems", npc_flag != 0); CHECK("NPC footprint is not stamped as player collision", player_flag == 0); CHECK("player walkability ignores NPC footprint", - col_player_walkable(&s, 17, 16) == 1); + col_player_walkable_ctx(&s, &ctx, 17, 16) == 1); int actions[COLO_NUM_ACTION_HEADS] = {0}; actions[COLO_HEAD_PRIMARY] = forecast_move_action_for_delta(1, 0); step_and_observe(&s, &ctx, actions); @@ -2865,7 +2952,7 @@ static void test_warband_melee_distance_gate(void) { col_init_npc(&s, 0, COLO_FREMENNIK_ARCHER, 12, 18); CHECK("rig sanity: the ranged archer has clear LoS", - col_npc_has_los_to_player(&s, &s.npcs[0])); + col_npc_has_los_to_player(&s, &ctx, &s.npcs[0])); s.warband_cycle_anchor = s.tick - 3; col_warband_attack_phase(&s, &ctx); CHECK("archer at distance never attacks, even with LoS on its window", @@ -2933,7 +3020,7 @@ static void test_warband_formation_convergence(void) { s.modifiers.active_mask |= (1u << COLO_MOD_QUARTET); s.modifiers.tier[COLO_MOD_QUARTET] = 1; s.wave = 0; - col_spawn_wave(&s); + col_spawn_wave(&s, &ctx); } wb_isolate_warband(&s); @@ -3542,8 +3629,10 @@ static void test_manticore_stagger_overlap_fidelity(void) { s.player.current_hitpoints = 99; s.npcs[0].attack_timer = 0; s.npcs[1].attack_timer = 0; - CHECK("fixture: A sees the player", col_npc_has_los_to_player(&s, &s.npcs[0])); - CHECK("fixture: the pillar blocks B's LoS", !col_npc_has_los_to_player(&s, &s.npcs[1])); + CHECK("fixture: A sees the player", + col_npc_has_los_to_player(&s, &ctx, &s.npcs[0])); + CHECK("fixture: the pillar blocks B's LoS", + !col_npc_has_los_to_player(&s, &ctx, &s.npcs[1])); col_npc_attack_ctx(&s, &ctx, 0); col_npc_attack_ctx(&s, &ctx, 1); CHECK("a ready but LoS-blocked peer is not delayed at A's barrage start", @@ -3551,7 +3640,7 @@ static void test_manticore_stagger_overlap_fidelity(void) { s.player.x = 12; s.player.y = 12; col_rebuild_player_collision_flags(&s); CHECK("fixture: B sees the player after the step", - col_npc_has_los_to_player(&s, &s.npcs[1])); + col_npc_has_los_to_player(&s, &ctx, &s.npcs[1])); col_npc_attack_ctx(&s, &ctx, 0); col_npc_attack_ctx(&s, &ctx, 1); CHECK("entering a second manticore's LoS mid-barrage eats the overlap", @@ -3849,14 +3938,18 @@ static void sol_clear_beams_and_sand(ColosseumState* s) { s->sol.hazard_tile_count = 0; } -static int sol_phase_sand_invariants_hold(const ColosseumState* s, int expected_count) { +static int sol_phase_sand_invariants_hold( + const ColosseumState* s, + const ColosseumContext* ctx, + int expected_count +) { if (s->sol.hazard_tile_count != expected_count) return 0; int player_tile_seen = 0; for (int i = 0; i < s->sol.hazard_tile_count; i++) { int x = s->sol.hazard_tile_x[i]; int y = s->sol.hazard_tile_y[i]; if (!col_in_boss_arena(s, x, y)) return 0; - if (col_static_blocked(x, y)) return 0; + if (col_topology_tile_blocked(ctx, x, y)) return 0; if (x == s->player.x && y == s->player.y) player_tile_seen = 1; for (int j = 0; j < i; j++) if (x == s->sol.hazard_tile_x[j] && y == s->sol.hazard_tile_y[j]) @@ -3900,7 +3993,8 @@ static void test_sol_generic_observation_signals_are_neutral(void) { obs[next_prayer_active_idx] == 0.0f); int magic, ranged, melee; - col_live_threat_style_counts(&s, &magic, &ranged, &melee); + col_live_threat_style_counts( + &s, &ctx, &magic, &ranged, &melee); CHECK("adjacent Sol contributes no prayable live threat style", magic == 0 && ranged == 0 && melee == 0); CHECK("the global style-count observation stays neutral for Sol", @@ -4287,7 +4381,7 @@ static void test_sol_grapple_perfect_parry(void) { int max_hit = col_live_loadout_stats(&s)->max_hit; CHECK("rig sanity: the melee loadout has a positive max hit", max_hit > 0); - col_player_attack_target(&s, idx); + col_player_attack_target_ctx(&s, &ctx, idx); CHECK("the guaranteed max is consumed at no less than the loadout max hit", s.player_attack_dmg >= max_hit && s.sol.next_attack_guaranteed_max == 0 && s.sol.guaranteed_max_ticks == 0); @@ -4325,7 +4419,7 @@ static void test_sol_perfect_parry_forces_spec_attack(void) { s.player.spec_armed = 1; s.sol.next_attack_guaranteed_max = 1; s.sol.guaranteed_max_ticks = COLO_SOL_PERFECT_MAX_TICKS; - col_player_attack_target(&s, idx); + col_player_attack_target_ctx(&s, &ctx, idx); CHECK("perfect-parry claws uses the forced first-success best total", s.player_attack_dmg == expected_total && @@ -4490,13 +4584,13 @@ static void test_sol_crystal_lifecycle(void) { int accumulates = 1, edges_ok = 1; for (int p = 1; p <= 4; p++) { - col_sol_enter_phase(&s, p); + col_sol_enter_phase(&s, &ctx, p); if (s.sol.crystal_count != p) accumulates = 0; if (s.sol.crystals[p - 1].edge != p - 1) edges_ok = 0; } CHECK("one crystal spawns at each transition (4 by 25%)", accumulates); CHECK("crystals take their own edges in N/E/S/W order", edges_ok); - col_sol_enter_phase(&s, 5); + col_sol_enter_phase(&s, &ctx, 5); CHECK("the enrage transition adds no fifth crystal", s.sol.crystal_count == 4); s.sol.phase = 4; sol_clear_beams_and_sand(&s); @@ -4613,7 +4707,8 @@ static void test_sol_aoe_reaction_window(void) { int safe_x = -1, safe_y = -1; for (int x = s.player.x - 3; x <= s.player.x + 3 && safe_x < 0; x++) for (int y = s.player.y - 3; y <= s.player.y + 3 && safe_x < 0; y++) - if (col_in_boss_arena(&s, x, y) && !col_static_blocked(x, y) && + if (col_in_boss_arena(&s, x, y) && + !col_topology_tile_blocked(&ctx, x, y) && !col_sol_aoe_tile_is_hazard(&s.sol, x, y)) { safe_x = x; safe_y = y; } @@ -4664,10 +4759,11 @@ static void test_sol_phase_transition_sand_guarantees(void) { (void)idx; s.sol.attack_delay = 1000; sol_move_player(&s, 17, 14); - col_sol_enter_phase(&s, 1); + col_sol_enter_phase(&s, &ctx, 1); for (int t = 0; t < COLO_SOL_BEAM_TO_POOL_TICKS; t++) - col_sol_tick_molten(&s); - if (!sol_phase_sand_invariants_hold(&s, COLO_SOL_BEAM_COUNT)) { + col_sol_tick_molten(&s, &ctx); + if (!sol_phase_sand_invariants_hold( + &s, &ctx, COLO_SOL_BEAM_COUNT)) { seeded_ok = 0; break; } @@ -4684,12 +4780,14 @@ static void test_sol_phase_transition_sand_guarantees(void) { sol_move_player(&s, corner_x, corner_y); CHECK("rig sanity: corner-edge player tile is walkable", col_in_boss_arena(&s, s.player.x, s.player.y) && - !col_static_blocked(s.player.x, s.player.y)); - col_sol_enter_phase(&s, 1); + !col_topology_tile_blocked( + &ctx, s.player.x, s.player.y)); + col_sol_enter_phase(&s, &ctx, 1); for (int t = 0; t < COLO_SOL_BEAM_TO_POOL_TICKS; t++) - col_sol_tick_molten(&s); + col_sol_tick_molten(&s, &ctx); CHECK("corner-edge phase transition still places 6 in-arena sand tiles including player", - sol_phase_sand_invariants_hold(&s, COLO_SOL_BEAM_COUNT)); + sol_phase_sand_invariants_hold( + &s, &ctx, COLO_SOL_BEAM_COUNT)); } static void test_sol_beams_become_pools(void) { @@ -4701,7 +4799,7 @@ static void test_sol_beams_become_pools(void) { s.sol.attack_delay = 1000; sol_move_player(&s, 17, 14); - col_sol_drop_beams(&s); + col_sol_drop_beams(&s, &ctx); int beams = sol_count_active_beams(&s); int in_box = 1; for (int b = 0; b < COLO_SOL_BEAM_MAX; b++) { @@ -4709,7 +4807,9 @@ static void test_sol_beams_become_pools(void) { int dx = abs(s.sol.beams[b].x - s.player.x); int dy = abs(s.sol.beams[b].y - s.player.y); if (dx > COLO_SOL_BEAM_SPREAD || dy > COLO_SOL_BEAM_SPREAD) in_box = 0; - if (col_static_blocked(s.sol.beams[b].x, s.sol.beams[b].y)) in_box = 0; + if (col_topology_tile_blocked( + &ctx, s.sol.beams[b].x, s.sol.beams[b].y)) + in_box = 0; } CHECK("6 beams drop inside the 9x9 around the player", beams == 6 && in_box); @@ -4771,7 +4871,9 @@ static void test_sol_beam_strike_reaction_window(void) { int start_x = -1, start_y = -1; for (int x = 12; x <= 21 && start_x < 0; x++) for (int y = 12; y <= 21 && start_x < 0; y++) { - if (!col_in_boss_arena(&s, x, y) || col_static_blocked(x, y)) continue; + if (!col_in_boss_arena(&s, x, y) || + col_topology_tile_blocked(&ctx, x, y)) + continue; int pooled = 0; for (int p = 0; p < s.sol.hazard_tile_count; p++) if (s.sol.hazard_tile_x[p] == x && s.sol.hazard_tile_y[p] == y) @@ -4786,7 +4888,9 @@ static void test_sol_beam_strike_reaction_window(void) { int safe_x = -1, safe_y = -1; for (int x = s.player.x - 2; x <= s.player.x + 2 && safe_x < 0; x++) for (int y = s.player.y - 2; y <= s.player.y + 2 && safe_x < 0; y++) { - if (!col_in_boss_arena(&s, x, y) || col_static_blocked(x, y)) continue; + if (!col_in_boss_arena(&s, x, y) || + col_topology_tile_blocked(&ctx, x, y)) + continue; int marked = 0; for (int b = 0; b < COLO_SOL_BEAM_MAX; b++) if (s.sol.beams[b].active && s.sol.beams[b].x == x && @@ -4823,7 +4927,9 @@ static void test_sol_enrage_sand_telegraphs(void) { pools_before >= 1 && sol_count_active_beams(&s) == 0); int before = s.sol.hazard_tile_count; - col_sol_add_pool(&s, s.sol.hazard_tile_x[0], s.sol.hazard_tile_y[0]); + col_sol_add_pool( + &s, &ctx, + s.sol.hazard_tile_x[0], s.sol.hazard_tile_y[0]); CHECK("re-covering a pooled tile has no additional effect", s.sol.hazard_tile_count == before); @@ -5393,7 +5499,7 @@ static void test_loadout_spec_weapons(void) { s.player.spec_armed = 1; osrs_interaction_set(&s.interaction, 0); s.player.attack_timer = 0; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); CHECK("claws spec drains 50 energy", s.player.special_energy == 50); CHECK("claws spec disarms after firing", s.player.spec_armed == 0); CHECK("claws spec queues the 4-splat cascade", @@ -5408,13 +5514,13 @@ static void test_loadout_spec_weapons(void) { s.player.x = 10; s.player.y = 16; col_init_npc(&s, 0, COLO_SOL_HEREDIT, 11, 14); s.player.attack_timer = 0; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); CHECK("scythe queues 3 splats into the 5x5 boss", s.npcs[0].pending_hits.count == 3); geo_clear_npcs(&s); col_init_npc(&s, 0, COLO_FREMENNIK_BERSERKER, 10, 17); s.player.attack_timer = 0; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); CHECK("scythe queues 1 splat into a 1x1 warbander", s.npcs[0].pending_hits.count == 1); @@ -5435,7 +5541,7 @@ static void test_loadout_spec_weapons(void) { arc_hits[1].npc_slot != arc_hits[2].npc_slot && arc_hits[0].npc_slot != arc_hits[2].npc_slot); s.player.attack_timer = 0; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); CHECK("scythe arc queues one hit per distinct arc target", s.npcs[0].pending_hits.count == 1 && s.npcs[1].pending_hits.count == 1 && @@ -5453,7 +5559,7 @@ static void test_loadout_spec_weapons(void) { col_init_npc(&s, 1, COLO_HEALING_TOTEM, 15, 15); col_init_npc(&s, 2, COLO_FREMENNIK_ARCHER, 15, 17); s.player.attack_timer = 0; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); CHECK("scythe arc skips incidental hazard entities", s.npcs[0].pending_hits.count == 1 && s.npcs[1].pending_hits.count == 0 && @@ -5469,7 +5575,7 @@ static void test_loadout_spec_weapons(void) { col_init_npc(&s, 0, COLO_FREMENNIK_ARCHER, 15, 16); col_init_npc(&s, 1, COLO_FREMENNIK_BERSERKER, 15, 15); s.player.attack_timer = 0; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); int scythe_max = col_live_loadout_stats(&s)->max_hit; if (s.npcs[0].pending_hits.count == 1 && s.npcs[1].pending_hits.count == 1 && @@ -5513,7 +5619,7 @@ static void test_loadout_spec_weapons(void) { s.player.spec_armed = 1; s.player.attack_timer = 0; s.npcs[0].hp = zerk->hp; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); tries++; } CHECK("elder maul spec eventually lands", s.npcs[0].def_drained > 0); @@ -5538,7 +5644,7 @@ static void test_loadout_spec_weapons(void) { s.player.spec_armed = 1; s.player.attack_timer = 0; s.npcs[0].hp = seer->hp; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); if (s.player.current_hitpoints > 20) healed = 1; tries++; } @@ -5856,12 +5962,14 @@ static void test_applied_damage_and_merged_pool_attribution(void) { init_forecast_test_state(&s, &ctx, 2328, 17, 16); col_mod_add_molten_pool( &s, + ctx.route_topology, s.player.x, s.player.y, COLO_POOL_REENTRY, COLO_POOL_PERMANENT); col_mod_add_molten_pool( &s, + ctx.route_topology, s.player.x, s.player.y, COLO_POOL_VOLATILITY, @@ -6342,6 +6450,10 @@ static void test_red_flag_minotaur_not_solid_to_other_npcs(void) { ColosseumContext ctx; col_init_context_typed(&ctx); ColosseumState s; + ColoGeometryContext geometry = { + .state = &s, + .context = &ctx, + }; memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 77); @@ -6350,7 +6462,7 @@ static void test_red_flag_minotaur_not_solid_to_other_npcs(void) { col_init_npc(&s, 0, COLO_MINOTAUR, 14, 16); int size = col_npc_effective_size(&s.npcs[0]); CHECK("without Red Flag the minotaur blocks other NPCs", - col_npc_blocked_ignore_player(&s, 14, 16, size)); + col_npc_blocked_ignore_player(&geometry, 14, 16, size)); memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 77); @@ -6360,16 +6472,16 @@ static void test_red_flag_minotaur_not_solid_to_other_npcs(void) { s.modifiers.tier[COLO_MOD_RED_FLAG] = 1; col_init_npc(&s, 0, COLO_MINOTAUR, 14, 16); CHECK("with Red Flag other NPCs can move into the minotaur's tiles", - !col_npc_blocked_ignore_player(&s, 14, 16, size)); + !col_npc_blocked_ignore_player(&geometry, 14, 16, size)); col_init_npc(&s, 1, COLO_SERPENT_SHAMAN, 20, 16); int shaman_size = col_npc_effective_size(&s.npcs[1]); CHECK("Red Flag does not make other NPC types passable", - col_npc_blocked_ignore_player(&s, 20, 16, shaman_size)); + col_npc_blocked_ignore_player(&geometry, 20, 16, shaman_size)); col_deactivate_npc(&s, 1); CHECK("deactivating a non-solid minotaur leaves no stale collision flags", - !col_npc_blocked_ignore_player(&s, 14, 16, size)); + !col_npc_blocked_ignore_player(&geometry, 14, 16, size)); } static void test_bee_contact_damage_band(void) { @@ -6551,7 +6663,7 @@ static void test_thrall_regression(void) { s.npcs[slot].pending_hits.hits[0].attack_style == ATTACK_STYLE_MAGIC); float dmg_before = s.tick_scratch.damage_dealt; int npc_hp_before = s.npcs[slot].hp; - land_pending_player_hits(&s); + land_pending_player_hits(&s, &ctx); int thrall_dmg = npc_hp_before - s.npcs[slot].hp; CHECK("a single thrall hit lands player-credited damage in [0,3]", thrall_dmg >= 0 && thrall_dmg <= COLO_THRALL_MAX_HIT); @@ -6605,7 +6717,7 @@ static void test_thrall_regression(void) { s.thrall_lifetime_left = 50; int sol_hp = s.npcs[sol_slot].hp; col_tick_player_ctx(&s, &ctx, idle, 1); - land_pending_player_hits(&s); + land_pending_player_hits(&s, &ctx); CHECK("thrall never damages Sol", s.npcs[sol_slot].hp == sol_hp); } @@ -6623,7 +6735,7 @@ static void test_death_charge_regression(void) { CHECK("Death Charge arms a 100-tick window (decremented this tick)", s.death_charge_window_left == 99 && s.death_charge_cd == 0); s.npcs[slot].hp = 0; - col_apply_npc_death(&s, slot); + col_apply_npc_death(&s, &ctx, slot); CHECK("a player-credited kill in the window grants +15 spec", s.player.special_energy == 65); CHECK("the kill closes the window and starts the 100-tick cooldown", @@ -6633,13 +6745,13 @@ static void test_death_charge_regression(void) { s.player.special_energy = 95; col_tick_player_ctx(&s, &ctx, cast_dc, 1); s.npcs[slot].hp = 0; - col_apply_npc_death(&s, slot); + col_apply_npc_death(&s, &ctx, slot); CHECK("Death Charge spec gain clamps at 100", s.player.special_energy == 100); slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 213); s.player.special_energy = 40; s.npcs[slot].hp = 0; - col_apply_npc_death(&s, slot); + col_apply_npc_death(&s, &ctx, slot); CHECK("a kill outside an armed window grants no spec and starts no cooldown", s.player.special_energy == 40 && s.death_charge_cd == 0); @@ -6669,8 +6781,8 @@ static void test_death_charge_regression(void) { col_tick_player_ctx(&s, &ctx, cast_dc, 1); s.npcs[slot].hp = 0; s.npcs[slot_b].hp = 0; - col_apply_npc_death(&s, slot); - col_apply_npc_death(&s, slot_b); + col_apply_npc_death(&s, &ctx, slot); + col_apply_npc_death(&s, &ctx, slot_b); CHECK("two same-tick kills consume the charge exactly once (+15)", s.player.special_energy == 65 && s.death_charge_window_left == 0); @@ -6689,7 +6801,7 @@ static void test_death_charge_regression(void) { if (s.npcs[slot].pending_hits.hits[h].active && s.npcs[slot].pending_hits.hits[h].source_npc_slot == -1) s.npcs[slot].pending_hits.hits[h].damage = 5; - land_pending_player_hits(&s); + land_pending_player_hits(&s, &ctx); CHECK("a thrall-credited kill procs Death Charge (+15)", s.npcs[slot].hp <= 0 && s.player.special_energy == spec_before + 15); @@ -7087,10 +7199,13 @@ static void test_player_ranged_los_blocked_by_pillar(void) { col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 13, 9); ColoNPC* npc = &s.npcs[0]; CHECK("player + target tiles are clear of static blockers", - !col_static_blocked(5, 9) && !col_static_blocked(13, 9)); - CHECK("pillar 0 sits on the line between them", col_static_blocked(9, 9)); - CHECK("no LoS through the pillar", col_npc_has_los_to_player(&s, npc) == 0); - OsrsLosQuery los_query = col_player_los_query(&s); + !col_topology_tile_blocked(&ctx, 5, 9) && + !col_topology_tile_blocked(&ctx, 13, 9)); + CHECK("pillar 0 sits on the line between them", + col_topology_tile_blocked(&ctx, 9, 9)); + CHECK("no LoS through the pillar", + col_npc_has_los_to_player(&s, &ctx, npc) == 0); + OsrsLosQuery los_query = col_player_los_query(&ctx); CHECK("shared tile LoS blocks the same pillar line", encounter_player_can_attack(s.player.x, s.player.y, npc->x, npc->y, col_npc_effective_size(npc), @@ -7100,8 +7215,10 @@ static void test_player_ranged_los_blocked_by_pillar(void) { s.player.x = 13; s.player.y = 4; col_rebuild_player_collision_flags(&s); - CHECK("the clear column tile is walkable", !col_static_blocked(13, 4)); - CHECK("LoS is clear down the column", col_npc_has_los_to_player(&s, npc) == 1); + CHECK("the clear column tile is walkable", + !col_topology_tile_blocked(&ctx, 13, 4)); + CHECK("LoS is clear down the column", + col_npc_has_los_to_player(&s, &ctx, npc) == 1); int actions[COLO_NUM_ACTION_HEADS] = {0}; osrs_interaction_set(&s.interaction, 0); s.player.attack_timer = 0; @@ -7133,7 +7250,7 @@ static void test_player_chase_routes_around_pillar_for_los(void) { col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 13, 9); col_rebuild_player_collision_flags(&s); ColoNPC* npc = &s.npcs[0]; - OsrsLosQuery los_query = col_player_los_query(&s); + OsrsLosQuery los_query = col_player_los_query(&ctx); int attack_range = col_player_attack_range(&s); CHECK("start tile is range-valid and LoS-blocked", encounter_player_can_attack(s.player.x, s.player.y, @@ -7161,7 +7278,7 @@ static void test_player_chase_routes_around_pillar_for_los(void) { CHECK("chase reaches LoS and fires within twelve ticks", attacked_tick >= 0); CHECK("attack fires from a LoS-valid tile", - col_npc_has_los_to_player(&s, npc) == 1 && + col_npc_has_los_to_player(&s, &ctx, npc) == 1 && encounter_rect_distance(s.player.x, s.player.y, 1, npc->x, npc->y, col_npc_effective_size(npc)) <= attack_range); } @@ -7207,7 +7324,7 @@ static void test_colosseum_npc_movement_player_tile_guards(void) { encounter_dist_to_npc(s.player.x, s.player.y, s.npcs[0].x, s.npcs[0].y, col_npc_effective_size(&s.npcs[0])) <= COLO_NPC_STATS[COLO_SERPENT_SHAMAN].attack_range && - col_npc_has_los_to_player(&s, &s.npcs[0])); + col_npc_has_los_to_player(&s, &ctx, &s.npcs[0])); geo_clear_npcs(&s); s.player.x = 6; @@ -7220,7 +7337,7 @@ static void test_colosseum_npc_movement_player_tile_guards(void) { encounter_dist_to_npc(s.player.x, s.player.y, s.npcs[0].x, s.npcs[0].y, col_npc_effective_size(&s.npcs[0])) <= COLO_NPC_STATS[COLO_SERPENT_SHAMAN].attack_range && - col_npc_has_los_to_player(&s, &s.npcs[0])); + col_npc_has_los_to_player(&s, &ctx, &s.npcs[0])); col_npc_move_ctx(&s, &ctx, 0); CHECK("ranged shaman range+LoS player one-step around pillar holds OSRS tile", s.npcs[0].x == shaman_x && s.npcs[0].y == shaman_y && @@ -7364,7 +7481,7 @@ static void test_player_melee_lands_at_delay_zero(void) { col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 16, 16); s.player.attack_timer = 0; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); CHECK("a melee swing queues at least one pending hit on the target", s.npcs[0].pending_hits.count >= 1); int all_delay_zero = s.npcs[0].pending_hits.count >= 1; @@ -7376,13 +7493,13 @@ static void test_player_melee_lands_at_delay_zero(void) { int hp_before = s.npcs[0].hp; int resolved_same_pass = 1; for (int swing = 0; swing < 32 && s.npcs[0].hp == hp_before; swing++) { - col_resolve_player_projectiles_on_npcs(&s); + col_resolve_player_projectiles_on_npcs(&s, &ctx); if (s.npcs[0].pending_hits.count != 0) resolved_same_pass = 0; if (s.npcs[0].hp < hp_before) break; s.player.attack_timer = 0; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); } - col_resolve_player_projectiles_on_npcs(&s); + col_resolve_player_projectiles_on_npcs(&s, &ctx); CHECK("a delay-0 melee hit lands on the first resolver pass", s.npcs[0].hp < hp_before && resolved_same_pass && s.npcs[0].pending_hits.count == 0); @@ -7396,7 +7513,7 @@ static void test_player_melee_lands_at_delay_zero(void) { col_rebuild_player_collision_flags(&r); col_init_npc(&r, 0, COLO_JAGUAR_WARRIOR, 16, 16); r.player.attack_timer = 0; - col_player_attack_target(&r, 0); + col_player_attack_target_ctx(&r, &ctx, 0); int ranged_delay_positive = r.npcs[0].pending_hits.count >= 1; for (int h = 0; h < r.npcs[0].pending_hits.count; h++) if (r.npcs[0].pending_hits.hits[h].ticks_remaining <= 0) ranged_delay_positive = 0; @@ -8134,7 +8251,7 @@ static void test_stage3_t5_claws_click_spec_fires(void) { s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_DRAGON_CLAWS); CHECK("T5 SPEC arms equipped claws", s.player.spec_armed == 1); s.player.attack_timer = 0; - col_player_attack_target(&s, 0); + col_player_attack_target_ctx(&s, &ctx, 0); CHECK("T5 claws special fires four splats", s.npcs[0].pending_hits.count == 4 && s.player.special_energy == 50); } @@ -8552,7 +8669,7 @@ static void test_farm_safe_damage_cap(void) { ColosseumState s; loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); - col_spawn_reinforcements(&s); + col_spawn_reinforcements(&s, &ctx); int jaguar = -1; for (int i = 0; i < COLO_MAX_NPCS; i++) if (s.npcs[i].active && s.npcs[i].spawned_as_reinforcement) { jaguar = i; break; } @@ -8597,7 +8714,54 @@ static void test_farm_safe_damage_cap(void) { ctx.config.farm_cap_waves = COLO_FARM_CAP_WAVES; } +static void child_col_reset_without_context(void) { + ColosseumState state = {0}; + col_reset_ctx((EncounterState*)&state, NULL, 1u); +} + +static void child_col_reset_before_topology_finalize(void) { + ColosseumContext ctx; + ColosseumState state; + col_init_unfinalized_context(&ctx); + col_init_state_typed(&state, &ctx); + col_reset_ctx( + (EncounterState*)&state, (EncounterContext*)&ctx, 1u); +} + +static void child_col_step_before_topology_finalize(void) { + ColosseumContext ctx; + ColosseumState state; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + col_init_unfinalized_context(&ctx); + col_init_state_typed(&state, &ctx); + col_step_ctx( + (EncounterState*)&state, (EncounterContext*)&ctx, actions); +} + +static void child_col_query_before_topology_finalize(void) { + ColosseumContext ctx; + col_init_unfinalized_context(&ctx); + (void)col_topology_tile_blocked(&ctx, 10, 10); +} + +static void test_colosseum_topology_lifecycle_contract(void) { + printf("test_colosseum_topology_lifecycle_contract\n"); + assert_child_aborts( + "Colosseum reset rejects NULL context", + child_col_reset_without_context); + assert_child_aborts( + "Colosseum reset before topology finalize aborts", + child_col_reset_before_topology_finalize); + assert_child_aborts( + "Colosseum step before topology finalize aborts", + child_col_step_before_topology_finalize); + assert_child_aborts( + "Colosseum query before topology finalize aborts", + child_col_query_before_topology_finalize); +} + int main(void) { + test_colosseum_topology_lifecycle_contract(); test_stage3_t1_inventory_ranged_weapon_swap(); test_stage3_t1_inventory_weapon_slot_last_click_wins(); test_stage3_t1_human_inventory_primary_click_uses_resolver(); diff --git a/ocean/osrs/tests/test_osrs_eat_attack_delay.c b/ocean/osrs/tests/test_osrs_eat_attack_delay.c index 2f220033b9..69a718eae2 100644 --- a/ocean/osrs/tests/test_osrs_eat_attack_delay.c +++ b/ocean/osrs/tests/test_osrs_eat_attack_delay.c @@ -62,6 +62,7 @@ static void test_colosseum_charges_the_cooldown(void) { ColosseumContext ctx; static ColosseumState s; col_init_context_typed(&ctx); + col_finalize_route_topology(&ctx); memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242u); From 27ca8f3f9e49ac16ede266ccb17f402928d5f964 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 06:30:53 +0300 Subject: [PATCH 21/50] Fix Colosseum topology test contexts --- .superpowers/sdd/task-5-report.md | 4 ++-- ocean/osrs/tests/probe_colo_remove_brews.c | 1 + ocean/osrs/tests/probe_colo_skyfall_dodge.c | 20 +++++++++++++++---- ocean/osrs/tests/probe_colo_wave_advance.c | 1 + .../osrs/tests/probe_colosseum_winnability.c | 1 + ocean/osrs/tests/test_colo_prayer_wiring.c | 2 +- ocean/osrs/tests/test_colosseum_modifiers.c | 9 +++++++-- ocean/osrs/tests/trace_colo_move.c | 14 ++++++++++--- 8 files changed, 40 insertions(+), 12 deletions(-) diff --git a/.superpowers/sdd/task-5-report.md b/.superpowers/sdd/task-5-report.md index bc713fd993..e75c3aa558 100644 --- a/.superpowers/sdd/task-5-report.md +++ b/.superpowers/sdd/task-5-report.md @@ -14,14 +14,14 @@ Base: `a506c5a462c4a035e9d79d842d35655ac7a4102c` - Routed player, NPC, forecast, spawn, Sol, modifier, observation, mask, lab, and render geometry through explicit topology-owning contexts. - Kept NPC occupancy, player occupancy, modifiers, hazards, and active state dynamic. - Deleted the process-global legacy context and contextless reset, step, combat, lab, and attack wrappers. -- Made supported callbacks reject NULL context. Standalone probes and tests now initialize, finalize, and destroy their own contexts. +- Made supported callbacks reject NULL context. Standalone probes and tests now initialize and finalize explicit contexts. - Added exhaustive static tile, footprint, directed LOS, endpoint-law, legal-step, and forecast parity checks to existing Colosseum tests. - Added lifecycle checks for NULL and unfinalized contexts. - Preserved the observation and mask layouts. No item tables changed. ## Observed evidence -The interrupted worker's compile attempts exposed stale private-geometry and old-signature callers in `test_colosseum_modifiers.c`. Recovery migrated those callers and the remaining standalone probes. No clean compile, test, profile, or sanitizer result was observed after the final source edits. +The interrupted worker's compile attempts exposed stale private-geometry and old-signature callers in `test_colosseum_modifiers.c`. The first main gate then reproduced a stale `col_player_walkable` callback-data crash in the modifier test. Source inspection found the same callback misuse in the skyfall probe and movement trace, plus unfinalized contexts in five standalone probes. Recovery migrated those callers to explicit geometry contexts and finalized every audited context before reset, step, or topology query. No clean compile, test, profile, or sanitizer result was observed after the follow-up edits. ## Pending main validation diff --git a/ocean/osrs/tests/probe_colo_remove_brews.c b/ocean/osrs/tests/probe_colo_remove_brews.c index cc9adf3d0f..cc4457062a 100644 --- a/ocean/osrs/tests/probe_colo_remove_brews.c +++ b/ocean/osrs/tests/probe_colo_remove_brews.c @@ -15,6 +15,7 @@ static int reset_brew_doses(int remove_brews, uint32_t seed) { ENCOUNTER_COLOSSEUM.put_int((EncounterState*)&s, (EncounterContext*)&ctx, "remove_brews", remove_brews); + col_finalize_route_topology(&ctx); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); return s.player.brew_doses; } diff --git a/ocean/osrs/tests/probe_colo_skyfall_dodge.c b/ocean/osrs/tests/probe_colo_skyfall_dodge.c index c7805fad41..0c41535965 100644 --- a/ocean/osrs/tests/probe_colo_skyfall_dodge.c +++ b/ocean/osrs/tests/probe_colo_skyfall_dodge.c @@ -78,6 +78,7 @@ static void probe_init_context(ColosseumContext* ctx) { ctx->config.beginner_loadout_fraction = 0.0f; ctx->config.step_out_forecast_obs_enabled = 1; ctx->config.action_debug_log = 0; + col_finalize_route_topology(ctx); } static void probe_init_empty_state( @@ -150,15 +151,24 @@ static ProbeSkyfallObs probe_read_skyfall_obs( return out; } -static int probe_move_action_ending_off_tile(const ColosseumState* s, int x, int y) { +static int probe_move_action_ending_off_tile( + const ColosseumState* s, + const ColosseumContext* ctx, + int x, + int y +) { for (int action = 1; action < ENCOUNTER_MOVE_ACTIONS; action++) { ColosseumState tmp = *s; + ColoGeometryContext geometry = { + .state = &tmp, + .context = ctx, + }; int moved = encounter_move_to_target( &tmp.player, ENCOUNTER_MOVE_TARGET_DX[action], ENCOUNTER_MOVE_TARGET_DY[action], col_player_walkable, - &tmp); + &geometry); if (moved > 0 && (tmp.player.x != x || tmp.player.y != y)) return action; } probe_fail("move action ending off marked tile found"); @@ -234,7 +244,8 @@ static ProbeStepDodgeResult probe_run_no_lock_wait_case(int wait_visible_ticks) probe_init_empty_state(&s, &ctx, 0x5100u + (uint32_t)wait_visible_ticks); probe_spawn_javelin(&s); ProbeSkyfallObs first = probe_fire_skyfall_visible(&s, &ctx, 0); - int move_action = probe_move_action_ending_off_tile(&s, first.marked_x, first.marked_y); + int move_action = probe_move_action_ending_off_tile( + &s, &ctx, first.marked_x, first.marked_y); int hp_before = s.player.current_hitpoints; int move_tick = -1; int player_x_after_move = s.player.x; @@ -309,7 +320,8 @@ static ProbeStepDodgeResult probe_run_attack_lock_case( s.npcs[0].y); probe_establish_attack_lock(&s, &ctx); ProbeSkyfallObs first = probe_fire_skyfall_visible(&s, &ctx, 1); - int move_action = probe_move_action_ending_off_tile(&s, first.marked_x, first.marked_y); + int move_action = probe_move_action_ending_off_tile( + &s, &ctx, first.marked_x, first.marked_y); int hp_before = s.player.current_hitpoints; probe_zero_actions(actions); diff --git a/ocean/osrs/tests/probe_colo_wave_advance.c b/ocean/osrs/tests/probe_colo_wave_advance.c index fe5ade1c25..cd284d5e80 100644 --- a/ocean/osrs/tests/probe_colo_wave_advance.c +++ b/ocean/osrs/tests/probe_colo_wave_advance.c @@ -117,6 +117,7 @@ static void run_episodes(const char* label, int mode, int n_eps) { ctx.config.beginner_loadout_fraction = 0.0f; ctx.config.step_out_forecast_obs_enabled = 1; ctx.config.wave_clear_bonus = 1.0f; + col_finalize_route_topology(&ctx); static float obs[COLO_NUM_OBS]; ColosseumState s; diff --git a/ocean/osrs/tests/probe_colosseum_winnability.c b/ocean/osrs/tests/probe_colosseum_winnability.c index 53c599bb65..2dd9c40a3f 100644 --- a/ocean/osrs/tests/probe_colosseum_winnability.c +++ b/ocean/osrs/tests/probe_colosseum_winnability.c @@ -126,6 +126,7 @@ static void run_episodes(const char* label, int scripted, int start_wave, int n_ ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; ctx.config.beginner_loadout_fraction = 0.5f; ctx.config.step_out_forecast_obs_enabled = 1; + col_finalize_route_topology(&ctx); static float obs[COLO_NUM_OBS]; ColosseumState s; diff --git a/ocean/osrs/tests/test_colo_prayer_wiring.c b/ocean/osrs/tests/test_colo_prayer_wiring.c index 31d077c762..e2e5e9bbf9 100644 --- a/ocean/osrs/tests/test_colo_prayer_wiring.c +++ b/ocean/osrs/tests/test_colo_prayer_wiring.c @@ -5,6 +5,7 @@ #define col_init_context_typed(ctx_ptr) do { \ col_init_context_typed(ctx_ptr); \ + col_finalize_route_topology((ctx_ptr)); \ (ctx_ptr)->config.late_start_state_mode = 0; \ } while (0) @@ -52,7 +53,6 @@ static void run_test(const char* label, PrayerPolicy policy, int start_wave, int ctx.config.start_wave = start_wave; ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; ctx.config.beginner_loadout_fraction = 0.5f; - col_finalize_route_topology(&ctx); static float obs[COLO_NUM_OBS]; ColosseumState s; diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index 49e8ac7de0..1c6846c87b 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -3822,17 +3822,22 @@ static void skyfall_dodge_init_state( static int skyfall_dodge_move_action_off_tile( const ColosseumState* s, + const ColosseumContext* ctx, int marked_x, int marked_y ) { for (int action = 1; action < ENCOUNTER_MOVE_ACTIONS; action++) { ColosseumState tmp = *s; + ColoGeometryContext geometry = { + .state = &tmp, + .context = ctx, + }; int moved = encounter_move_to_target( &tmp.player, ENCOUNTER_MOVE_TARGET_DX[action], ENCOUNTER_MOVE_TARGET_DY[action], col_player_walkable, - &tmp); + &geometry); if (moved > 0 && (tmp.player.x != marked_x || tmp.player.y != marked_y)) return action; } @@ -3855,7 +3860,7 @@ static int skyfall_dodge_damage_after_wait(int wait_visible_ticks) { jv->skyfall_damage = SKYFALL_DODGE_FORCED_DAMAGE; int move_action = skyfall_dodge_move_action_off_tile( - &s, jv->skyfall_tile_x, jv->skyfall_tile_y); + &s, &ctx, jv->skyfall_tile_x, jv->skyfall_tile_y); int hp_before = s.player.current_hitpoints; for (int i = 0; i < wait_visible_ticks; i++) { diff --git a/ocean/osrs/tests/trace_colo_move.c b/ocean/osrs/tests/trace_colo_move.c index c61b6e5d82..5edee7b96a 100644 --- a/ocean/osrs/tests/trace_colo_move.c +++ b/ocean/osrs/tests/trace_colo_move.c @@ -16,11 +16,18 @@ static int nearest_live_obs_target(const ColosseumState* s) { return best_slot < 0 ? 0 : col_primary_attack_action_for_obs_slot(best_slot); } -static int first_walkable_move(const ColosseumState* s) { +static int first_walkable_move( + ColosseumState* s, + const ColosseumContext* ctx +) { + ColoGeometryContext geometry = { + .state = s, + .context = ctx, + }; for (int a = 1; a < ENCOUNTER_MOVE_ACTIONS; a++) { int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[a]; int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[a]; - if (col_player_walkable((void*)s, nx, ny)) return a; + if (col_player_walkable(&geometry, nx, ny)) return a; } return 0; } @@ -33,6 +40,7 @@ int main(void) { ctx.config.beginner_loadout_fraction = 0.5f; ctx.config.step_out_forecast_obs_enabled = 1; ctx.config.action_debug_log = 1; + col_finalize_route_topology(&ctx); ColosseumState s; memset(&s, 0, sizeof(s)); @@ -50,7 +58,7 @@ int main(void) { actions[COLO_HEAD_MODIFIER_SELECT] = opt + 1; } else { actions[COLO_HEAD_PRIMARY] = (tick & 1) - ? first_walkable_move(&s) + ? first_walkable_move(&s, &ctx) : nearest_live_obs_target(&s); } col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); From ce233cf1f1ce8cb21c253999496c919c6b1234d1 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 06:44:28 +0300 Subject: [PATCH 22/50] Reduce Colosseum topology query overhead --- .superpowers/sdd/task-5-report.md | 5 ++- .../encounter_colosseum_movement.inc | 36 ++++++++++++++----- ocean/osrs/osrs_collision.h | 16 +++++++-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/.superpowers/sdd/task-5-report.md b/.superpowers/sdd/task-5-report.md index e75c3aa558..325e22e42e 100644 --- a/.superpowers/sdd/task-5-report.md +++ b/.superpowers/sdd/task-5-report.md @@ -17,11 +17,14 @@ Base: `a506c5a462c4a035e9d79d842d35655ac7a4102c` - Made supported callbacks reject NULL context. Standalone probes and tests now initialize and finalize explicit contexts. - Added exhaustive static tile, footprint, directed LOS, endpoint-law, legal-step, and forecast parity checks to existing Colosseum tests. - Added lifecycle checks for NULL and unfinalized contexts. +- Kept the checked shared footprint query unchanged and added a trusted finalized, size-in-range query for validated Colosseum path contexts. The hot route blocker now pays lifecycle and size checks once when it creates the path context instead of once per explored tile. - Preserved the observation and mask layouts. No item tables changed. ## Observed evidence -The interrupted worker's compile attempts exposed stale private-geometry and old-signature callers in `test_colosseum_modifiers.c`. The first main gate then reproduced a stale `col_player_walkable` callback-data crash in the modifier test. Source inspection found the same callback misuse in the skyfall probe and movement trace, plus unfinalized contexts in five standalone probes. Recovery migrated those callers to explicit geometry contexts and finalized every audited context before reset, step, or topology query. No clean compile, test, profile, or sanitizer result was observed after the follow-up edits. +The interrupted worker's compile attempts exposed stale private-geometry and old-signature callers in `test_colosseum_modifiers.c`. The first main gate then reproduced a stale `col_player_walkable` callback-data crash in the modifier test. Source inspection found the same callback misuse in the skyfall probe and movement trace, plus unfinalized contexts in five standalone probes. Recovery migrated those callers to explicit geometry contexts and finalized every audited context before reset, step, or topology query. + +After that fix, exact, golden, and determinism gates passed. Twelve paired 5M-step profiles regressed on every seed, with a final/base geometric mean of `0.978398718` and median SPS of `482699` versus `493402`. Equivalent samples isolated repeated finalized-state and footprint-size checks under `col_npc_path_blocked`. The follow-up moves those checks to `col_npc_path_ctx_begin` and retains raw bounds handling on every tile query. No compile, test, profile, or sanitizer result was observed after the performance edit. ## Pending main validation diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc index 3507f72d66..65c86540f3 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc @@ -51,18 +51,18 @@ static int col_npc_has_los_to_player( 0); } -static int col_npc_blocked_ignore_player( - void* data, +static inline int +col_npc_blocked_ignore_player_assume_finalized_size_in_range( + ColoGeometryContext* geometry, int x, int y, int size ) { - ColoGeometryContext* geometry = (ColoGeometryContext*)data; ColosseumState* state = geometry->state; state->log.npc_blocked_calls += 1.0f; state->log.npc_blocked_tiles += (float)(size * size); - if (col_topology_footprint_blocked( - geometry->context, x, y, size)) + if (encounter_arena_topology_footprint_blocked_assume_finalized_size_in_range( + geometry->context->route_topology, x, y, size)) return 1; int sol_clamp_active = col_sol_clamp_active(state); for (int dx = 0; dx < size; dx++) { @@ -78,6 +78,21 @@ static int col_npc_blocked_ignore_player( } return 0; } +static int col_npc_blocked_ignore_player( + void* data, + int x, + int y, + int size +) { + ColoGeometryContext* geometry = (ColoGeometryContext*)data; + const EncounterArenaTopology* topology = + geometry->context->route_topology; + encounter_arena_topology_require_finalized(topology); + encounter_arena_topology_require_footprint_size(topology, size); + return col_npc_blocked_ignore_player_assume_finalized_size_in_range( + geometry, x, y, size); +} + static int col_npc_blocked(void* data, int x, int y, int size) { ColoGeometryContext* geometry = (ColoGeometryContext*)data; @@ -131,6 +146,8 @@ static ColoNpcPathCtx col_npc_path_ctx_begin( int size ) { encounter_arena_topology_require_finalized(context->route_topology); + encounter_arena_topology_require_footprint_size( + context->route_topology, size); return (ColoNpcPathCtx){ .geometry = { .state = state, @@ -150,8 +167,11 @@ static int col_npc_path_blocked_uncached( state->player.y >= y && state->player.y < y + path->size) return 1; if (path->ignore_npcs) { - if (col_topology_footprint_blocked( - path->geometry.context, x, y, path->size)) + if (encounter_arena_topology_footprint_blocked_assume_finalized_size_in_range( + path->geometry.context->route_topology, + x, + y, + path->size)) return 1; if (!col_sol_clamp_active(state)) return 0; for (int dx = 0; dx < path->size; dx++) @@ -160,7 +180,7 @@ static int col_npc_path_blocked_uncached( return 1; return 0; } - return col_npc_blocked_ignore_player( + return col_npc_blocked_ignore_player_assume_finalized_size_in_range( &path->geometry, x, y, path->size); } diff --git a/ocean/osrs/osrs_collision.h b/ocean/osrs/osrs_collision.h index 733d265953..0d919109ba 100644 --- a/ocean/osrs/osrs_collision.h +++ b/ocean/osrs/osrs_collision.h @@ -1145,6 +1145,17 @@ static inline void encounter_arena_topology_require_footprint_size( encounter_arena_topology_abort("query footprint size", size); } +static inline int +encounter_arena_topology_footprint_blocked_assume_finalized_size_in_range( + const EncounterArenaTopology* topology, + int x, + int y, + int size +) { + return encounter_arena_topology_footprint_blocked_raw( + topology, x, y, size); +} + static inline int encounter_arena_topology_footprint_blocked( const EncounterArenaTopology* topology, int x, @@ -1153,8 +1164,9 @@ static inline int encounter_arena_topology_footprint_blocked( ) { encounter_arena_topology_require_finalized(topology); encounter_arena_topology_require_footprint_size(topology, size); - return encounter_arena_topology_footprint_blocked_raw( - topology, x, y, size); + return + encounter_arena_topology_footprint_blocked_assume_finalized_size_in_range( + topology, x, y, size); } static inline uint32_t encounter_arena_topology_nearby_unit_footprint_mask( From 979a2f9c282c0ea736ef9d1fa3c0bc6a691b4673 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 07:00:43 +0300 Subject: [PATCH 23/50] Document Colosseum topology validation --- .superpowers/sdd/task-5-report.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/.superpowers/sdd/task-5-report.md b/.superpowers/sdd/task-5-report.md index 325e22e42e..669415ce6c 100644 --- a/.superpowers/sdd/task-5-report.md +++ b/.superpowers/sdd/task-5-report.md @@ -2,7 +2,7 @@ ## Result -Implementation complete. Main validation pending. +Implementation and main validation complete. Base: `a506c5a462c4a035e9d79d842d35655ac7a4102c` @@ -24,17 +24,16 @@ Base: `a506c5a462c4a035e9d79d842d35655ac7a4102c` The interrupted worker's compile attempts exposed stale private-geometry and old-signature callers in `test_colosseum_modifiers.c`. The first main gate then reproduced a stale `col_player_walkable` callback-data crash in the modifier test. Source inspection found the same callback misuse in the skyfall probe and movement trace, plus unfinalized contexts in five standalone probes. Recovery migrated those callers to explicit geometry contexts and finalized every audited context before reset, step, or topology query. -After that fix, exact, golden, and determinism gates passed. Twelve paired 5M-step profiles regressed on every seed, with a final/base geometric mean of `0.978398718` and median SPS of `482699` versus `493402`. Equivalent samples isolated repeated finalized-state and footprint-size checks under `col_npc_path_blocked`. The follow-up moves those checks to `col_npc_path_ctx_begin` and retains raw bounds handling on every tile query. No compile, test, profile, or sanitizer result was observed after the performance edit. +After that fix, exact, golden, and determinism gates passed. Twelve paired 5M-step profiles regressed on every seed, with a final/base geometric mean of `0.978398718` and median SPS of `482699` versus `493402`. Equivalent samples isolated repeated finalized-state and footprint-size checks under `col_npc_path_blocked`. The follow-up moves those checks to `col_npc_path_ctx_begin` and retains raw bounds handling on every tile query. -## Pending main validation +## Main validation -Run the Task 5 gates from the brief: - -```bash -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/test_colosseum_modifiers && /tmp/test_colosseum_modifiers -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_forecast_exact.c -lm -o /tmp/test_colosseum_forecast_exact && /tmp/test_colosseum_forecast_exact -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_golden.c -lm -o /tmp/test_colosseum_golden && /tmp/test_colosseum_golden -clang -std=c11 -O2 -I. ocean/osrs/tests/probe_colo_sim_invariant.c -lm -o /tmp/probe_colo_sim_invariant && /tmp/probe_colo_sim_invariant -``` - -Also run the 10k modifier battery, Sol spear geometry checks, one-worker versus eight-worker determinism, C11 and C++17 include gates, sanitizer gates, twelve paired profiles, and `sample`. Compare all semantic hashes with the Task 4 baseline. Do not reseed. +- Modifier battery: `13549/13550`. Every Colosseum topology and Sol spear check passed. The sole failure is the inherited generic item-table code `101` mismatch that reproduces at the Task 5 base. +- Exact forecast: topology LOS `1335180` directed pairs, footprint `5709` NPC-size checks, landing `25` actions, route equivalence `22511700` queries, attack routes `1656` queries, and the Task 4 golden fixture all passed. +- Golden: `12/12` configs matched. +- Sim invariant: all 12 printed hashes matched the detached Task 5 base byte for byte. +- Determinism: one worker versus eight workers and repeated eight-worker runs were identical across 256 envs in both late-start modes. +- Portability: the C11 topology setup and C++17 Colosseum header gates passed. +- Sanitizers: the topology setup passed UBSan. The host ASan runtime deadlocked before `main` in `AsanInitInternal`, so no ASan result exists. +- Performance: all 12 paired 5M-step runs improved. Final/base ratios were `1.001197`, `1.013857`, `1.005567`, `1.013254`, `1.008387`, `1.005730`, `1.008313`, `1.003657`, `1.006833`, `1.000941`, `1.013913`, and `1.003901`. The geometric mean was `1.007119621`. Median SPS rose from `487283` to `491122`. +- The post-fix sample no longer attributes route-solver samples to the checked Colosseum footprint wrapper. From 848b777b039ce5f29781695e4eb8bd74c43a42df Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 11:35:54 +0300 Subject: [PATCH 24/50] Use shared topology in Zulrah and NH PvP --- ocean/osrs/encounters/encounter_nh_pvp.h | 17 +- ocean/osrs/encounters/encounter_zulrah.h | 211 ++++++---- ocean/osrs/osrs_collision.h | 219 +++++++--- ocean/osrs/osrs_encounter_player.h | 19 +- ocean/osrs/osrs_pathfinding.h | 331 ++++++++++++++-- ocean/osrs/osrs_pvp_actions.h | 126 +++--- ocean/osrs/osrs_pvp_api.h | 51 ++- ocean/osrs/osrs_pvp_movement.h | 375 +++++++++--------- ocean/osrs/osrs_pvp_observations.h | 88 ++-- ocean/osrs/osrs_visual.c | 68 ++-- ocean/osrs/tests/test_osrs_pvp_pending_hits.c | 217 +++++++++- ocean/osrs/tests/test_zulrah_hit_delay.c | 87 ++++ 12 files changed, 1313 insertions(+), 496 deletions(-) diff --git a/ocean/osrs/encounters/encounter_nh_pvp.h b/ocean/osrs/encounters/encounter_nh_pvp.h index 8386a1b4c6..a9b40c6dd0 100644 --- a/ocean/osrs/encounters/encounter_nh_pvp.h +++ b/ocean/osrs/encounters/encounter_nh_pvp.h @@ -17,6 +17,7 @@ typedef struct { static_assert(sizeof(NhPvpState) == 7160, "NhPvpState serialized layout"); typedef struct { + const CollisionMap* collision_map; const EncounterArenaTopology* route_topology; OsrsActorRouteCache player_route_cache[NUM_AGENTS]; } NhPvpContext; @@ -73,14 +74,14 @@ static void nh_pvp_finalize_context( EncounterState* state, EncounterContext* context ) { - NhPvpState* s = (NhPvpState*)state; + (void)state; NhPvpContext* ctx = (NhPvpContext*)context; if (!ctx || ctx->route_topology) { fprintf(stderr, "NH PvP route topology finalized twice\n"); abort(); } - ctx->route_topology = pvp_route_topology_finalize( - (const CollisionMap*)s->env.collision_map); + ctx->route_topology = + pvp_route_topology_finalize(ctx->collision_map); } @@ -93,7 +94,7 @@ static void nh_pvp_reset(EncounterState* state, EncounterContext* context, uint3 s->env.rng_seed = seed; } pvp_actor_route_caches_clear(ctx->player_route_cache); - pvp_reset(&s->env); + pvp_reset(&s->env, ctx->route_topology); } static void nh_pvp_step(EncounterState* state, EncounterContext* context, const int* actions) { @@ -260,10 +261,12 @@ static void nh_pvp_put_ptr( const char* key, void* value ) { - (void)context; - NhPvpState* s = (NhPvpState*)state; + (void)state; + NhPvpContext* ctx = (NhPvpContext*)context; if (strcmp(key, "collision_map") == 0) - s->env.collision_map = value; + ctx->collision_map = (const CollisionMap*)value; + else + encounter_abort_unknown_config("nh_pvp", "ptr", key); } static void* nh_pvp_get_log(EncounterState* state, EncounterContext* context) { diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h index 90d7d9d101..13db509099 100644 --- a/ocean/osrs/encounters/encounter_zulrah.h +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -29,8 +29,11 @@ typedef struct { + const CollisionMap* collision_map; const EncounterArenaTopology* route_topology; OsrsActorRouteCache player_route_cache; + int world_offset_x; + int world_offset_y; } ZulrahContext; #define ZUL_POS_NORTH 0 #define ZUL_POS_SOUTH 1 @@ -478,9 +481,9 @@ typedef struct { int thrall_duration_remaining; int thrall_cooldown; - void* collision_map; - int world_offset_x; - int world_offset_y; + uint64_t topology_context_layout_reserved; + int topology_world_offset_x_layout_reserved; + int topology_world_offset_y_layout_reserved; int tick; int episode_over; @@ -688,11 +691,13 @@ static inline int zul_on_platform_bounds(int x, int y) { y >= ZUL_PLATFORM_MIN && y <= ZUL_PLATFORM_MAX; } -static inline int zul_on_platform(ZulrahState* s, int x, int y) { - if (!s->collision_map) return zul_on_platform_bounds(x, y); - int wx = x + s->world_offset_x; - int wy = y + s->world_offset_y; - return collision_tile_walkable((const CollisionMap*)s->collision_map, 0, wx, wy); +static inline int zul_topology_player_tile_allowed( + const ZulrahContext* ctx, + int x, + int y +) { + return !encounter_arena_topology_tile_blocked( + ctx->route_topology, x, y); } @@ -723,19 +728,23 @@ static uint32_t zul_route_topology_flags(void* data, int x, int y) { x + build->world_offset_x, y + build->world_offset_y); } +static uint32_t zul_route_topology_los_flags(void* data, int x, int y) { + (void)data; + (void)x; + (void)y; + return 0; +} -static void zul_finalize_route_topology( - ZulrahContext* ctx, - const ZulrahState* state -) { - if (!ctx || !state || ctx->route_topology) { + +static void zul_finalize_route_topology(ZulrahContext* ctx) { + if (!ctx || ctx->route_topology) { fprintf(stderr, "zulrah route topology finalized twice\n"); abort(); } ZulRouteTopologyBuildContext build = { - (const CollisionMap*)state->collision_map, - state->world_offset_x, - state->world_offset_y, + ctx->collision_map, + ctx->world_offset_x, + ctx->world_offset_y, }; EncounterArenaTopologyBuildSpec spec = { .origin_x = 0, @@ -743,9 +752,11 @@ static void zul_finalize_route_topology( .width = ZUL_ARENA_SIZE, .height = ZUL_ARENA_SIZE, .max_footprint_size = ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE, - .revision = UINT64_C(0x5a554c5241480001), + .revision = UINT64_C(0x5a554c5241480002), .tile_flags = zul_route_topology_flags, .tile_flags_ctx = &build, + .los_tile_flags = zul_route_topology_los_flags, + .los_tile_flags_ctx = NULL, }; if (!zul_route_topology_owner.topology) { zul_route_topology_owner.topology = @@ -764,9 +775,8 @@ static void zul_finalize_context( EncounterState* state, EncounterContext* context ) { - zul_finalize_route_topology( - (ZulrahContext*)context, - (const ZulrahState*)state); + (void)state; + zul_finalize_route_topology((ZulrahContext*)context); } static inline int zul_player_in_cloud(int cx, int cy, int px, int py) { @@ -1234,15 +1244,17 @@ static const EncounterLoadoutStats* zul_live_stats(ZulrahState* s) { static int zul_player_can_attack_zulrah( ZulrahState* s, + const ZulrahContext* ctx, const EncounterLoadoutStats* loadout_stats ) { - return encounter_player_can_attack( - s->player.x, s->player.y, - s->zulrah.x, s->zulrah.y, ZUL_NPC_SIZE, - loadout_stats->attack_range, - (const CollisionMap*)s->collision_map, - s->world_offset_x, s->world_offset_y, - osrs_los_open_query()); + return encounter_arena_topology_player_can_attack( + ctx->route_topology, + s->player.x, + s->player.y, + s->zulrah.x, + s->zulrah.y, + ZUL_NPC_SIZE, + loadout_stats->attack_range); } static int zul_zulrah_def_roll(ZulrahState* s, int is_mage) { @@ -1269,13 +1281,13 @@ static int zul_player_attack_hits( : encounter_roll_hit_chance(&s->rng_state, att_roll, def_roll); } -static void zul_player_attack(ZulrahState* s) { +static void zul_player_attack(ZulrahState* s, const ZulrahContext* ctx) { if (!s->zulrah_visible || s->is_diving) return; if (s->player.attack_timer > 0) return; if (s->player_stunned_ticks > 0) return; const EncounterLoadoutStats* ls = zul_live_stats(s); - if (!zul_player_can_attack_zulrah(s, ls)) return; + if (!zul_player_can_attack_zulrah(s, ctx, ls)) return; AttackStyle style = zul_player_equipped_attack_style(s); int is_mage = (style == ATTACK_STYLE_MAGIC); @@ -1333,7 +1345,7 @@ static void zul_player_attack(ZulrahState* s) { s, s->player.attack_style_this_tick, dmg, 0); } -static void zul_player_spec(ZulrahState* s) { +static void zul_player_spec(ZulrahState* s, const ZulrahContext* ctx) { if (!s->zulrah_visible || s->is_diving) return; if (s->player.attack_timer > 0) return; if (s->player_stunned_ticks > 0) return; @@ -1341,7 +1353,7 @@ static void zul_player_spec(ZulrahState* s) { AttackStyle style = zul_player_equipped_attack_style(s); int is_mage = (style == ATTACK_STYLE_MAGIC); const EncounterLoadoutStats* ls = zul_live_stats(s); - if (!zul_player_can_attack_zulrah(s, ls)) return; + if (!zul_player_can_attack_zulrah(s, ctx, ls)) return; int weapon = s->player.equipped[GEAR_SLOT_WEAPON]; @@ -1382,14 +1394,19 @@ static void zul_player_spec(ZulrahState* s) { s->player_attack_dmg = total_dmg; } -static void zul_pick_snakeling_pos(ZulrahState* s, int* ox, int* oy) { +static void zul_pick_snakeling_pos( + ZulrahState* s, + const ZulrahContext* ctx, + int* ox, + int* oy +) { int order[ZUL_NUM_SNAKELING_POSITIONS]; for (int i = 0; i < ZUL_NUM_SNAKELING_POSITIONS; i++) order[i] = i; encounter_shuffle(order, ZUL_NUM_SNAKELING_POSITIONS, &s->rng_state); for (int i = 0; i < ZUL_NUM_SNAKELING_POSITIONS; i++) { int px = ZUL_SNAKELING_POSITIONS[order[i]][0]; int py = ZUL_SNAKELING_POSITIONS[order[i]][1]; - if (zul_on_platform(s, px, py) && + if (zul_topology_player_tile_allowed(ctx, px, py) && !(px == s->player.x && py == s->player.y)) { *ox = px; *oy = py; return; } @@ -1398,7 +1415,10 @@ static void zul_pick_snakeling_pos(ZulrahState* s, int* ox, int* oy) { *oy = s->player.y; } -static void zul_spawn_snakeling(ZulrahState* s) { +static void zul_spawn_snakeling( + ZulrahState* s, + const ZulrahContext* ctx +) { for (int i = 0; i < ZUL_MAX_SNAKELINGS; i++) { if (s->snakelings[i].active) continue; ZulrahSnakeling* sn = &s->snakelings[i]; @@ -1413,7 +1433,7 @@ static void zul_spawn_snakeling(ZulrahState* s) { ? MONSTER_DATABASE[MON_ZULRAH_SNAKELING_MAGIC].npc_id : MONSTER_DATABASE[MON_ZULRAH_SNAKELING_MELEE].npc_id; sn->entity.npc_anim_id = -1; - zul_pick_snakeling_pos(s, &sn->entity.x, &sn->entity.y); + zul_pick_snakeling_pos(s, ctx, &sn->entity.x, &sn->entity.y); sn->entity.current_hitpoints = ZUL_SNAKELING_HP; sn->entity.base_hitpoints = ZUL_SNAKELING_HP; sn->attack_timer = ZUL_SNAKELING_SPEED; @@ -1497,22 +1517,29 @@ static const ZulRotationPhase* zul_current_phase(ZulrahState* s) { return &ZUL_ROTATIONS[s->rotation_index][s->phase_index]; } -static int zul_cloud_fits(ZulrahState* s, int x, int y) { - for (int dx = 0; dx < ZUL_CLOUD_SIZE; dx++) { - for (int dy = 0; dy < ZUL_CLOUD_SIZE; dy++) { - if (!zul_on_platform(s, x + dx, y + dy)) return 0; - } - } - return 1; +static int zul_cloud_fits( + const ZulrahContext* ctx, + int x, + int y +) { + return !encounter_arena_topology_footprint_blocked( + ctx->route_topology, x, y, ZUL_CLOUD_SIZE); } -static int zul_pick_cloud_pos(ZulrahState* s, int stand, int stall, int* ox, int* oy) { +static int zul_pick_cloud_pos( + ZulrahState* s, + const ZulrahContext* ctx, + int stand, + int stall, + int* ox, + int* oy +) { int attempts = 0; while (attempts++ < 100) { int x = ZUL_PLATFORM_MIN + encounter_rand_int(&s->rng_state, ZUL_PLATFORM_MAX - ZUL_PLATFORM_MIN + 1); int y = ZUL_PLATFORM_MIN + encounter_rand_int(&s->rng_state, ZUL_PLATFORM_MAX - ZUL_PLATFORM_MIN + 1); - if (!zul_cloud_fits(s, x, y)) continue; + if (!zul_cloud_fits(ctx, x, y)) continue; if (zul_cloud_overlaps_safe_area(x, y, stand, stall)) continue; int overlap = 0; @@ -1573,18 +1600,21 @@ static void zul_emit_cloud_event(ZulrahState* s, int dst_x, int dst_y, int fligh s->cloud_events[i].flight_ticks = flight_ticks; } -static void zul_spawn_cloud(ZulrahState* s) { +static void zul_spawn_cloud( + ZulrahState* s, + const ZulrahContext* ctx +) { const ZulRotationPhase* phase = zul_current_phase(s); int stand = phase->stand; int stall = phase->stall; int x, y; int spawned = 0; - if (zul_pick_cloud_pos(s, stand, stall, &x, &y)) { + if (zul_pick_cloud_pos(s, ctx, stand, stall, &x, &y)) { zul_queue_pending_cloud(s, x, y, ZUL_CLOUD_FLIGHT_1); zul_emit_cloud_event(s, x, y, ZUL_CLOUD_FLIGHT_1); spawned = 1; } - if (zul_pick_cloud_pos(s, stand, stall, &x, &y)) { + if (zul_pick_cloud_pos(s, ctx, stand, stall, &x, &y)) { zul_queue_pending_cloud(s, x, y, ZUL_CLOUD_FLIGHT_2); zul_emit_cloud_event(s, x, y, ZUL_CLOUD_FLIGHT_2); spawned = 1; @@ -1667,22 +1697,26 @@ static void zul_thrall_tick(ZulrahState* s) { s->total_damage_dealt += dmg; } -static void zul_fire_action(ZulrahState* s, ZulActionType type) { +static void zul_fire_action( + ZulrahState* s, + const ZulrahContext* ctx, + ZulActionType type +) { switch (type) { case ZA_RANGED: zul_attack_ranged(s); break; case ZA_MAGIC_RANGED: zul_attack_magic_ranged(s); break; case ZA_MELEE: zul_melee_start(s); break; case ZA_JAD_RM: case ZA_JAD_MR: zul_attack_jad(s); break; - case ZA_CLOUDS: zul_spawn_cloud(s); break; - case ZA_SNAKELINGS: zul_spawn_snakeling(s); break; + case ZA_CLOUDS: zul_spawn_cloud(s, ctx); break; + case ZA_SNAKELINGS: zul_spawn_snakeling(s, ctx); break; case ZA_SNAKECLOUD_ALT: - if (s->action_progress % 2 == 0) zul_spawn_snakeling(s); - else zul_spawn_cloud(s); + if (s->action_progress % 2 == 0) zul_spawn_snakeling(s, ctx); + else zul_spawn_cloud(s, ctx); break; case ZA_CLOUDSNAKE_ALT: - if (s->action_progress % 2 == 0) zul_spawn_cloud(s); - else zul_spawn_snakeling(s); + if (s->action_progress % 2 == 0) zul_spawn_cloud(s, ctx); + else zul_spawn_snakeling(s, ctx); break; case ZA_END: break; } @@ -1770,7 +1804,7 @@ static void zul_next_phase(ZulrahState* s) { zul_enter_phase(s); } -static void zul_phase_tick(ZulrahState* s) { +static void zul_phase_tick(ZulrahState* s, const ZulrahContext* ctx) { if (!s->zulrah_visible) return; if (s->phase_timer > 0) s->phase_timer--; @@ -1803,7 +1837,7 @@ static void zul_phase_tick(ZulrahState* s) { s->action_timer--; if (s->action_timer > 0) return; - zul_fire_action(s, (ZulActionType)act->type); + zul_fire_action(s, ctx, (ZulActionType)act->type); s->action_progress++; if (s->action_progress >= act->count) { @@ -1881,19 +1915,25 @@ static void zul_apply_drink_cell( zul_apply_drink_one_dose_effect, s); } -static void zul_player_attack_snakeling(ZulrahState* s, int snakeling_idx) { +static void zul_player_attack_snakeling( + ZulrahState* s, + const ZulrahContext* ctx, + int snakeling_idx +) { if (snakeling_idx < 0 || snakeling_idx >= ZUL_MAX_SNAKELINGS) return; ZulrahSnakeling* sn = &s->snakelings[snakeling_idx]; if (!sn->active) return; const EncounterLoadoutStats* ls = zul_live_stats(s); - if (!encounter_player_can_attack( - s->player.x, s->player.y, - sn->entity.x, sn->entity.y, 1, - ls->attack_range, - (const CollisionMap*)s->collision_map, - s->world_offset_x, s->world_offset_y, - osrs_los_open_query())) return; + if (!encounter_arena_topology_player_can_attack( + ctx->route_topology, + s->player.x, + s->player.y, + sn->entity.x, + sn->entity.y, + 1, + ls->attack_range)) + return; AttackStyle style = zul_player_equipped_attack_style(s); s->player.attack_timer = ls->attack_speed; @@ -2095,7 +2135,8 @@ static void zul_write_obs(EncounterState* state, EncounterContext* context, floa } static void zul_write_mask(EncounterState* state, EncounterContext* context, float* mask) { - (void)context; + ZulrahContext* ctx = (ZulrahContext*)context; + encounter_arena_topology_require_finalized(ctx->route_topology); ZulrahState* s = (ZulrahState*)state; for (int i = 0; i < ZUL_ACTION_MASK_SIZE; i++) mask[i] = 1.0f; int off = 0; @@ -2106,7 +2147,8 @@ static void zul_write_mask(EncounterState* state, EncounterContext* context, flo else { int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[m]; int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[m]; - if (!zul_on_platform(s, nx, ny)) mask[off] = 0.0f; + if (!zul_topology_player_tile_allowed(ctx, nx, ny)) + mask[off] = 0.0f; } } off++; @@ -2406,9 +2448,7 @@ static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t osrs_actor_route_cache_clear(&ctx->player_route_cache); ZulrahState* s = (ZulrahState*)state; Log saved_log = s->log; - void* saved_cmap = s->collision_map; - int saved_wx = s->world_offset_x; - int saved_wy = s->world_offset_y; + (void)ctx; int saved_tier = s->gear_tier; int saved_fixed_tier = s->gear_tier_fixed; int saved_tier_mode = s->gear_tier_mode; @@ -2419,9 +2459,7 @@ static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t uint32_t saved_rng = s->rng_state; memset(s, 0, sizeof(ZulrahState)); s->log = saved_log; - s->collision_map = saved_cmap; - s->world_offset_x = saved_wx; - s->world_offset_y = saved_wy; + s->gear_tier = saved_tier; s->gear_tier_fixed = saved_fixed_tier; s->gear_tier_mode = saved_tier_mode; @@ -2613,10 +2651,11 @@ static void zul_step_tick( .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, .destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, - .collision_map = (const CollisionMap*)s->collision_map, - .world_offset_x = s->world_offset_x, - .world_offset_y = s->world_offset_y, - .los_query = osrs_los_open_query(), + .attack_geometry = ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY, + .collision_map = NULL, + .world_offset_x = 0, + .world_offset_y = 0, + .los_query = NULL, }, }; OsrsPlayerStepResult step_result = osrs_encounter_player_step(&step_input); @@ -2636,14 +2675,14 @@ static void zul_step_tick( if (s->zulrah_visible && !s->is_diving) { if (s->player.spec_armed && s->player.special_energy >= osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON])) { - zul_player_spec(s); + zul_player_spec(s, ctx); osrs_spec_disarm(&s->player.spec_armed); } else { - zul_player_attack(s); + zul_player_attack(s, ctx); } } } else { - zul_player_attack_snakeling(s, target - 1); + zul_player_attack_snakeling(s, ctx, target - 1); } } @@ -2659,7 +2698,7 @@ static void zul_step_tick( return; } - zul_phase_tick(s); + zul_phase_tick(s, ctx); zul_snakeling_tick(s, ctx); @@ -2916,11 +2955,11 @@ static void zul_fill_render_entities( } static void zul_put_int(EncounterState* state, EncounterContext* context, const char* key, int value) { - (void)context; + ZulrahContext* ctx = (ZulrahContext*)context; ZulrahState* s = (ZulrahState*)state; if (strcmp(key, "seed") == 0) s->rng_state = (uint32_t)value; - else if (strcmp(key, "world_offset_x") == 0) s->world_offset_x = value; - else if (strcmp(key, "world_offset_y") == 0) s->world_offset_y = value; + else if (strcmp(key, "world_offset_x") == 0) ctx->world_offset_x = value; + else if (strcmp(key, "world_offset_y") == 0) ctx->world_offset_y = value; else if (strcmp(key, "gear_tier") == 0) { s->gear_tier_fixed = encounter_require_int_range_config( "zulrah", key, value, 0, ZUL_NUM_GEAR_TIERS - 1); @@ -2991,10 +3030,10 @@ static void zul_put_ptr( const char* key, void* value ) { - (void)context; - ZulrahState* s = (ZulrahState*)st; + ZulrahContext* ctx = (ZulrahContext*)context; + (void)st; if (strcmp(key, "collision_map") == 0) - s->collision_map = value; + ctx->collision_map = (const CollisionMap*)value; else encounter_abort_unknown_config("zulrah", "ptr", key); } diff --git a/ocean/osrs/osrs_collision.h b/ocean/osrs/osrs_collision.h index 0d919109ba..99cb38e893 100644 --- a/ocean/osrs/osrs_collision.h +++ b/ocean/osrs/osrs_collision.h @@ -559,6 +559,7 @@ typedef uint32_t (*encounter_arena_tile_flags_fn)(void* ctx, int x, int y); typedef enum { ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_FLAGGED = 0, ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_TILE_BLOCKED, + ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_OPEN, } EncounterArenaTopologyLosBuildMode; @@ -849,6 +850,12 @@ static inline EncounterArenaTopology* encounter_arena_topology_build( "footprint size", spec->max_footprint_size); if (spec->revision == 0) encounter_arena_topology_abort("revision", 0); + if (spec->los_build_mode < + ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_FLAGGED || + spec->los_build_mode > + ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_OPEN) + encounter_arena_topology_abort( + "LOS build mode", spec->los_build_mode); EncounterArenaTopology* topology = (EncounterArenaTopology*)calloc(1, sizeof(*topology)); @@ -960,6 +967,10 @@ static inline EncounterArenaTopology* encounter_arena_topology_build( .spec = spec, }; if (topology->los_build_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_OPEN) { + topology->static_los_mode = + ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN; + } else if (topology->los_build_mode == ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_TILE_BLOCKED) { topology->static_los_mode = ENCOUNTER_ARENA_TOPOLOGY_LOS_TILE_BLOCKED; @@ -1155,6 +1166,20 @@ encounter_arena_topology_footprint_blocked_assume_finalized_size_in_range( return encounter_arena_topology_footprint_blocked_raw( topology, x, y, size); } +static inline int +encounter_arena_topology_footprint_in_bounds_assume_finalized_size_in_range( + const EncounterArenaTopology* topology, + int x, + int y, + int size +) { + int64_t local_x = (int64_t)x - topology->origin_x; + int64_t local_y = (int64_t)y - topology->origin_y; + return local_x >= 0 && local_y >= 0 && + local_x + size <= topology->width && + local_y + size <= topology->height; +} + static inline int encounter_arena_topology_footprint_blocked( const EncounterArenaTopology* topology, @@ -1201,7 +1226,8 @@ static inline int encounter_arena_topology_step_allowed( (uint8_t)(1u << bit)) != 0; } -static inline int encounter_arena_topology_los_clear( +static inline int +encounter_arena_topology_los_clear_assume_finalized_footprints_in_bounds( const EncounterArenaTopology* topology, int actor_x, int actor_y, @@ -1211,81 +1237,54 @@ static inline int encounter_arena_topology_los_clear( int target_size, int attack_range ) { - encounter_arena_topology_require_finalized(topology); - encounter_arena_topology_require_footprint_size( - topology, actor_size); - encounter_arena_topology_require_footprint_size( - topology, target_size); int64_t actor_max_x = (int64_t)actor_x + actor_size - 1; int64_t actor_max_y = (int64_t)actor_y + actor_size - 1; int64_t target_max_x = (int64_t)target_x + target_size - 1; int64_t target_max_y = (int64_t)target_y + target_size - 1; - if (!encounter_arena_topology_contains_raw( - topology, actor_x, actor_y) || - !encounter_arena_topology_contains_raw( - topology, (int)actor_max_x, (int)actor_max_y) || - !encounter_arena_topology_contains_raw( - topology, target_x, target_y) || - !encounter_arena_topology_contains_raw( - topology, (int)target_max_x, (int)target_max_y)) - return 0; - int overlap = + int x_overlap = (int64_t)actor_x <= target_max_x && - (int64_t)target_x <= actor_max_x && + (int64_t)target_x <= actor_max_x; + int y_overlap = (int64_t)actor_y <= target_max_y && (int64_t)target_y <= actor_max_y; if (attack_range == 1) { - int y_overlap = - (int64_t)actor_y <= target_max_y && - (int64_t)target_y <= actor_max_y; - int x_overlap = - (int64_t)actor_x <= target_max_x && - (int64_t)target_x <= actor_max_x; return (actor_max_x + 1 == target_x && y_overlap) || (target_max_x + 1 == actor_x && y_overlap) || (actor_max_y + 1 == target_y && x_overlap) || (target_max_y + 1 == actor_y && x_overlap); } + if (x_overlap && y_overlap && + !(topology->static_los_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_TILE_BLOCKED && + attack_range == 0)) + return 0; + + int64_t dx = 0; + int64_t dy = 0; + if (actor_max_x < target_x) dx = (int64_t)target_x - actor_max_x; + else if (target_max_x < actor_x) dx = (int64_t)actor_x - target_max_x; + if (actor_max_y < target_y) dy = (int64_t)target_y - actor_max_y; + else if (target_max_y < actor_y) dy = (int64_t)actor_y - target_max_y; + if (attack_range > 0 && (dx > attack_range || dy > attack_range)) + return 0; + if (topology->static_los_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN) + return 1; + int64_t actor_los_x = target_x; if (actor_los_x < actor_x) actor_los_x = actor_x; if (actor_los_x > actor_max_x) actor_los_x = actor_max_x; int64_t actor_los_y = target_y; if (actor_los_y < actor_y) actor_los_y = actor_y; if (actor_los_y > actor_max_y) actor_los_y = actor_max_y; - int64_t target_los_x = actor_x; if (target_los_x < target_x) target_los_x = target_x; if (target_los_x > target_max_x) target_los_x = target_max_x; int64_t target_los_y = actor_y; if (target_los_y < target_y) target_los_y = target_y; if (target_los_y > target_max_y) target_los_y = target_max_y; - if (topology->static_los_mode == - ENCOUNTER_ARENA_TOPOLOGY_LOS_TILE_BLOCKED && - attack_range == 0) { - int actor_index = encounter_arena_topology_index_raw( - topology, (int)actor_los_x, (int)actor_los_y); - int target_index = encounter_arena_topology_index_raw( - topology, (int)target_los_x, (int)target_los_y); - size_t bit_index = - (size_t)actor_index * (size_t)topology->tile_count + - (size_t)target_index; - return (int)((topology->static_los_bits[bit_index >> 6] >> - (bit_index & 63)) & UINT64_C(1)); - } - if (overlap) return 0; - - int64_t dx = target_los_x - actor_los_x; - int64_t dy = target_los_y - actor_los_y; - int64_t abs_dx = dx < 0 ? -dx : dx; - int64_t abs_dy = dy < 0 ? -dy : dy; - if (attack_range > 0 && - (abs_dx > attack_range || abs_dy > attack_range)) - return 0; - if (topology->static_los_mode == - ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN) - return 1; int actor_index = encounter_arena_topology_index_raw( topology, (int)actor_los_x, (int)actor_los_y); @@ -1298,4 +1297,126 @@ static inline int encounter_arena_topology_los_clear( (bit_index & 63)) & UINT64_C(1)); } +static inline int encounter_arena_topology_los_clear( + const EncounterArenaTopology* topology, + int actor_x, + int actor_y, + int actor_size, + int target_x, + int target_y, + int target_size, + int attack_range +) { + encounter_arena_topology_require_finalized(topology); + encounter_arena_topology_require_footprint_size( + topology, actor_size); + encounter_arena_topology_require_footprint_size( + topology, target_size); + if (!encounter_arena_topology_footprint_in_bounds_assume_finalized_size_in_range( + topology, actor_x, actor_y, actor_size) || + !encounter_arena_topology_footprint_in_bounds_assume_finalized_size_in_range( + topology, target_x, target_y, target_size)) + return 0; + return encounter_arena_topology_los_clear_assume_finalized_footprints_in_bounds( + topology, + actor_x, + actor_y, + actor_size, + target_x, + target_y, + target_size, + attack_range); +} + +static inline int encounter_arena_topology_player_can_attack_trusted( + const EncounterArenaTopology* topology, + int player_x, + int player_y, + int target_x, + int target_y, + int target_size, + int attack_range +) { + int target_max_x = target_x + target_size - 1; + int target_max_y = target_y + target_size - 1; + int dx = player_x < target_x + ? target_x - player_x + : (player_x > target_max_x ? player_x - target_max_x : 0); + int dy = player_y < target_y + ? target_y - player_y + : (player_y > target_max_y ? player_y - target_max_y : 0); + int distance = dx > dy ? dx : dy; + if (distance < 1 || distance > attack_range) return 0; + + if (attack_range == 1) { + uint32_t flags = topology->static_collision_flags[ + encounter_arena_topology_index_raw( + topology, player_x, player_y)]; + if (player_x + 1 == target_x && + player_y >= target_y && player_y <= target_max_y) + return (flags & COLLISION_WALL_EAST) == 0; + if (player_x == target_max_x + 1 && + player_y >= target_y && player_y <= target_max_y) + return (flags & COLLISION_WALL_WEST) == 0; + if (player_y + 1 == target_y && + player_x >= target_x && player_x <= target_max_x) + return (flags & COLLISION_WALL_NORTH) == 0; + if (player_y == target_max_y + 1 && + player_x >= target_x && player_x <= target_max_x) + return (flags & COLLISION_WALL_SOUTH) == 0; + return 0; + } + + if (topology->static_los_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN) + return 1; + return encounter_arena_topology_los_clear_assume_finalized_footprints_in_bounds( + topology, + player_x, + player_y, + 1, + target_x, + target_y, + target_size, + attack_range); +} + +static inline int encounter_arena_topology_player_can_attack( + const EncounterArenaTopology* topology, + int player_x, + int player_y, + int target_x, + int target_y, + int target_size, + int attack_range +) { + encounter_arena_topology_require_finalized(topology); + encounter_arena_topology_require_footprint_size(topology, 1); + encounter_arena_topology_require_footprint_size( + topology, target_size); + if (!encounter_arena_topology_footprint_in_bounds_assume_finalized_size_in_range( + topology, player_x, player_y, 1) || + !encounter_arena_topology_footprint_in_bounds_assume_finalized_size_in_range( + topology, target_x, target_y, target_size)) + return 0; + if (attack_range < 1) + return encounter_arena_topology_los_clear_assume_finalized_footprints_in_bounds( + topology, + player_x, + player_y, + 1, + target_x, + target_y, + target_size, + attack_range); + return encounter_arena_topology_player_can_attack_trusted( + topology, + player_x, + player_y, + target_x, + target_y, + target_size, + attack_range); +} + #endif diff --git a/ocean/osrs/osrs_encounter_player.h b/ocean/osrs/osrs_encounter_player.h index a4c716b3c9..a26b60882e 100644 --- a/ocean/osrs/osrs_encounter_player.h +++ b/ocean/osrs/osrs_encounter_player.h @@ -32,12 +32,14 @@ typedef struct { int move_action; } OsrsPlayerCommand; + typedef struct { const EncounterArenaTopology* topology; EncounterRouteBlockers blockers; EncounterRouteMovementMode movement_mode; EncounterRouteCostPolicy cost_policy; EncounterRouteCostPolicy destination_cost_policy; + EncounterRouteAttackGeometry attack_geometry; const CollisionMap* collision_map; int world_offset_x; int world_offset_y; @@ -104,10 +106,21 @@ static inline int osrs_player_step_lookup_target( return input->target_lookup(input->target_ctx, target_slot, target); } -static inline int osrs_player_step_can_attack_target( +static OSRS_ROUTE_NOINLINE int osrs_player_step_can_attack_target( const OsrsPlayerStepInput* input, const OsrsAttackTarget* target ) { + if (input->arena.attack_geometry == + ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY) { + return encounter_arena_topology_player_can_attack_trusted( + input->arena.topology, + input->player->x, + input->player->y, + target->x, + target->y, + target->size, + target->attack_range); + } return encounter_player_can_attack( input->player->x, input->player->y, @@ -243,6 +256,7 @@ static inline void osrs_player_step_build_attack_route( .target_size = target->size, .target_kind = ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE, .attack_range = target->attack_range, + .attack_geometry = input->arena.attack_geometry, .collision_map = input->arena.collision_map, .world_offset_x = input->arena.world_offset_x, .world_offset_y = input->arena.world_offset_y, @@ -317,7 +331,8 @@ static inline int osrs_player_step_chase_target( return 0; } - int remaining_waypoints = route->waypoint_count - route->waypoint_index; + int remaining_waypoints = + route->waypoint_count - route->waypoint_index; int reroute = !osrs_player_step_route_matches(route, player, target, &input->arena) || route->state == OSRS_INTERACTION_ROUTE_FAILED || diff --git a/ocean/osrs/osrs_pathfinding.h b/ocean/osrs/osrs_pathfinding.h index 33ed5bc272..a2cedb8110 100644 --- a/ocean/osrs/osrs_pathfinding.h +++ b/ocean/osrs/osrs_pathfinding.h @@ -363,6 +363,11 @@ typedef enum { ENCOUNTER_ROUTE_MOVEMENT_RUN, } EncounterRouteMovementMode; +typedef enum { + ENCOUNTER_ROUTE_ATTACK_GEOMETRY_QUERY = 0, + ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY, +} EncounterRouteAttackGeometry; + typedef enum { ENCOUNTER_ROUTE_COST_OSRS = 0, ENCOUNTER_ROUTE_COST_SOUTH_FIRST, @@ -401,6 +406,7 @@ typedef struct { int target_size; EncounterRouteTargetKind target_kind; int attack_range; + EncounterRouteAttackGeometry attack_geometry; const CollisionMap* collision_map; int world_offset_x; int world_offset_y; @@ -685,6 +691,7 @@ static inline int encounter_route_cardinal_edge_open( return 0; } + static inline int encounter_route_is_target( const EncounterRouteInput* input, int x, @@ -692,7 +699,18 @@ static inline int encounter_route_is_target( ) { if (input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE) return x == input->target_x && y == input->target_y; - if (input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) { + if (input->attack_geometry == + ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY) { + return encounter_arena_topology_player_can_attack_trusted( + input->topology, + x, + y, + input->target_x, + input->target_y, + input->target_size, + input->attack_range); + } return encounter_attack_position_valid( x, y, input->target_x, input->target_y, input->target_size, @@ -701,6 +719,7 @@ static inline int encounter_route_is_target( input->world_offset_x, input->world_offset_y, input->los_query); + } return encounter_route_footprints_cardinal_adjacent( x, y, input->actor_size, input->target_x, input->target_y, input->target_size) && @@ -737,10 +756,31 @@ static inline int encounter_route_input_valid( if (input->target_kind < ENCOUNTER_ROUTE_TARGET_TILE || input->target_kind > ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) return 0; - if (input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE && - (input->actor_size != 1 || input->attack_range < 1 || - !osrs_los_query_valid(input->los_query, input->attack_range))) - return 0; + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) { + if (input->actor_size != 1 || input->attack_range < 1) + return 0; + if (input->attack_geometry == + ENCOUNTER_ROUTE_ATTACK_GEOMETRY_QUERY && + !osrs_los_query_valid(input->los_query, input->attack_range)) + return 0; + if (input->attack_geometry < + ENCOUNTER_ROUTE_ATTACK_GEOMETRY_QUERY || + input->attack_geometry > + ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY) + return 0; + if (input->attack_geometry == + ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY) { + if (input->target_size > + input->topology->max_footprint_size) + return 0; + if (!encounter_arena_topology_footprint_in_bounds_assume_finalized_size_in_range( + input->topology, + input->target_x, + input->target_y, + input->target_size)) + return 0; + } + } if (input->movement_mode < ENCOUNTER_ROUTE_MOVEMENT_WALK || input->movement_mode > ENCOUNTER_ROUTE_MOVEMENT_RUN) return 0; @@ -1047,7 +1087,13 @@ static inline int encounter_route_try_direct( : 0); int nearest_distance = distance_x > distance_y ? distance_x : distance_y; - if (input->los_query->kind == OSRS_LOS_OPEN) { + int open_los = + input->attack_geometry == + ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY + ? input->topology->static_los_mode == + ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN + : input->los_query->kind == OSRS_LOS_OPEN; + if (open_los) { for (int x = min_x; x <= max_x; x++) { int min_y_distance = encounter_route_abs(x - input->source_x); int min_y_delta = @@ -1234,6 +1280,9 @@ static inline EncounterRouteResult encounter_route_escape_overlap( if (escape.outcome == ROUTE_INVALID_INPUT || escape.outcome == ROUTE_UNREACHABLE) return escape; + if (escape.outcome == ROUTE_REACHED_TARGET && + escape.distance == 1) + return escape; int landing_x = input->source_x + escape.first_dx + escape.run_dx; int landing_y = input->source_y + escape.first_dy + escape.run_dy; @@ -1830,6 +1879,143 @@ static inline int encounter_route_try_source_field( } +#ifdef OSRS_ROUTE_PROBE +static uint64_t osrs_route_probe_calls[3]; +static uint64_t osrs_route_probe_direct; +static uint64_t osrs_route_probe_overlap; +static uint64_t osrs_route_probe_try_direct; +static uint64_t osrs_route_probe_source; +static uint64_t osrs_route_probe_reverse; +static uint64_t osrs_route_probe_bfs; +static uint64_t osrs_route_probe_nodes; +static uint64_t osrs_route_probe_cost_calls[6]; +static uint64_t osrs_route_probe_cost_bfs[6]; +#endif +static inline void encounter_route_enqueue_unblocked( + const EncounterArenaTopology* topology, + EncounterRouteScratch* scratch, + uint16_t generation, + int current_index, + uint16_t next_depth, + uint8_t legal_mask, + uint8_t step_mask, + int direction_delta, + int8_t direction_via, + int* tail +) { + if ((legal_mask & step_mask) == 0) return; + int next_index = current_index + direction_delta; + if (scratch->generation[next_index] == generation) return; + if (*tail >= topology->tile_count) { + fprintf(stderr, "OSRS route queue overflow: %d\n", *tail); + abort(); + } + scratch->generation[next_index] = generation; + scratch->depth[next_index] = next_depth; + scratch->via[next_index] = direction_via; + scratch->queue[(*tail)++] = (uint16_t)next_index; +} +static OSRS_ROUTE_NOINLINE int encounter_route_expand_unblocked_south_first( + const EncounterArenaTopology* topology, + EncounterRouteScratch* scratch, + uint16_t generation, + int target_index, + const uint8_t* legal_step_masks +) { + int head = 0; + int tail = 1; + int height = topology->height; + while (head < tail) { + int current_index = scratch->queue[head++]; +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_nodes++; +#endif + if (target_index >= 0 + ? current_index == target_index + : scratch->target_generation[current_index] == generation) + return current_index; + uint16_t next_depth = (uint16_t)(scratch->depth[current_index] + 1); + uint8_t legal_mask = legal_step_masks[current_index]; + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 2, -1, VIA_S, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 8, -height, VIA_W, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 64, 1, VIA_N, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 16, height, VIA_E, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 1, -height - 1, VIA_SW, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 32, -height + 1, VIA_NW, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 4, height - 1, VIA_SE, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 128, height + 1, VIA_NE, &tail); + } + return -1; +} + + +static OSRS_ROUTE_NOINLINE int encounter_route_expand_unblocked_osrs( + const EncounterArenaTopology* topology, + EncounterRouteScratch* scratch, + uint16_t generation, + int target_index, + const uint8_t* legal_step_masks +) { + int head = 0; + int tail = 1; + int height = topology->height; + while (head < tail) { + int current_index = scratch->queue[head++]; +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_nodes++; +#endif + if (target_index >= 0 + ? current_index == target_index + : scratch->target_generation[current_index] == generation) + return current_index; + uint16_t next_depth = (uint16_t)(scratch->depth[current_index] + 1); + uint8_t legal_mask = legal_step_masks[current_index]; + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 8, -height, VIA_W, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 16, height, VIA_E, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 2, -1, VIA_S, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 64, 1, VIA_N, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 1, -height - 1, VIA_SW, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 4, height - 1, VIA_SE, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 32, -height + 1, VIA_NW, &tail); + encounter_route_enqueue_unblocked( + topology, scratch, generation, current_index, next_depth, + legal_mask, 128, height + 1, VIA_NE, &tail); + } + return -1; +} + + + static inline EncounterRouteResult encounter_route_solve( const EncounterRouteInput* input ) { @@ -1837,21 +2023,84 @@ static inline EncounterRouteResult encounter_route_solve( memset(&result, 0, sizeof(result)); result.outcome = ROUTE_INVALID_INPUT; if (!encounter_route_input_valid(input)) return result; - if (input->cost_policy == ENCOUNTER_ROUTE_COST_DIRECT) +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_calls[input->target_kind]++; + osrs_route_probe_cost_calls[input->cost_policy]++; +#endif + if (input->cost_policy == ENCOUNTER_ROUTE_COST_DIRECT) { +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_direct++; +#endif return encounter_route_greedy_direct(input); + } + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE && + !encounter_arena_topology_footprint_in_bounds_assume_finalized_size_in_range( + input->topology, + input->target_x, + input->target_y, + input->actor_size)) { + int min_x = input->topology->origin_x; + int min_y = input->topology->origin_y; + int max_x = + min_x + input->topology->width - input->actor_size; + int max_y = + min_y + input->topology->height - input->actor_size; + EncounterRouteInput fallback_input = *input; + fallback_input.target_x = + input->target_x < min_x ? min_x : + input->target_x > max_x ? max_x : + input->target_x; + fallback_input.target_y = + input->target_y < min_y ? min_y : + input->target_y > max_y ? max_y : + input->target_y; + if (encounter_route_destination_allowed( + &fallback_input, + fallback_input.target_x, + fallback_input.target_y)) { + EncounterRouteResult fallback = + encounter_route_solve(&fallback_input); + if (fallback.outcome == ROUTE_REACHED_TARGET) { + fallback.outcome = ROUTE_REACHED_FALLBACK; + return fallback; + } + } + } if ((input->target_kind == ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY || input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) && encounter_route_footprints_overlap( input->source_x, input->source_y, input->actor_size, - input->target_x, input->target_y, input->target_size)) + input->target_x, input->target_y, input->target_size)) { +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_overlap++; +#endif return encounter_route_escape_overlap(input); + } if (input->cost_policy != ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS && !(input->cost_policy == ENCOUNTER_ROUTE_COST_OSRS && input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) && - encounter_route_try_direct(input, &result)) + encounter_route_try_direct(input, &result)) { +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_try_direct++; +#endif + return result; + } + if (encounter_route_try_source_field(input, &result)) { +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_source++; +#endif + return result; + } + if (encounter_route_try_reverse(input, &result)) { +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_reverse++; +#endif return result; - if (encounter_route_try_source_field(input, &result)) return result; - if (encounter_route_try_reverse(input, &result)) return result; + } +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_bfs++; + osrs_route_probe_cost_bfs[input->cost_policy]++; +#endif const EncounterArenaTopology* topology = input->topology; EncounterRouteScratch* scratch = &encounter_route_scratch; scratch->current_generation++; @@ -1864,7 +2113,14 @@ static inline EncounterRouteResult encounter_route_solve( scratch->current_generation = 1; } uint16_t generation = scratch->current_generation; - encounter_route_mark_targets(input, scratch, generation); + int target_index = -1; + if (input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE) { + target_index = + (input->target_x - topology->origin_x) * topology->height + + input->target_y - topology->origin_y; + } else { + encounter_route_mark_targets(input, scratch, generation); + } int source_local_x = input->source_x - topology->origin_x; int source_local_y = input->source_y - topology->origin_y; int source_index = source_local_x * topology->height + source_local_y; @@ -1875,7 +2131,6 @@ static inline EncounterRouteResult encounter_route_solve( int head = 0; int tail = 1; int selected_index = -1; - static const int8_t osrs_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; static const int8_t osrs_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; static const int8_t osrs_via[8] = { @@ -1904,29 +2159,35 @@ static inline EncounterRouteResult encounter_route_solve( direction_dx[i] * topology->height + direction_dy[i]; const uint8_t* legal_step_masks = topology->legal_step_masks[input->actor_size - 1]; - while (head < tail) { - int current_index = scratch->queue[head++]; - if (scratch->target_generation[current_index] == generation) { - selected_index = current_index; - break; - } - uint16_t next_depth = (uint16_t)(scratch->depth[current_index] + 1); - uint8_t legal_mask = legal_step_masks[current_index]; - if (!input->blockers.is_blocked) { - for (int i = 0; i < 8; i++) { - if ((legal_mask & direction_step_mask[i]) == 0) continue; - int next_index = current_index + direction_delta[i]; - if (scratch->generation[next_index] == generation) continue; - if (tail >= topology->tile_count) { - fprintf(stderr, "OSRS route queue overflow: %d\n", tail); - abort(); - } - scratch->generation[next_index] = generation; - scratch->depth[next_index] = next_depth; - scratch->via[next_index] = (int8_t)direction_via[i]; - scratch->queue[tail++] = (uint16_t)next_index; + if (!input->blockers.is_blocked) { + selected_index = osrs_cost + ? encounter_route_expand_unblocked_osrs( + topology, + scratch, + generation, + target_index, + legal_step_masks) + : encounter_route_expand_unblocked_south_first( + topology, + scratch, + generation, + target_index, + legal_step_masks); + } else { + while (head < tail) { + int current_index = scratch->queue[head++]; +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_nodes++; +#endif + if (target_index >= 0 + ? current_index == target_index + : scratch->target_generation[current_index] == generation) { + selected_index = current_index; + break; } - } else { + uint16_t next_depth = + (uint16_t)(scratch->depth[current_index] + 1); + uint8_t legal_mask = legal_step_masks[current_index]; int current_x = current_index / topology->height; int current_y = current_index % topology->height; int x = topology->origin_x + current_x; diff --git a/ocean/osrs/osrs_pvp_actions.h b/ocean/osrs/osrs_pvp_actions.h index 0ed5a976a6..ebb7564db2 100644 --- a/ocean/osrs/osrs_pvp_actions.h +++ b/ocean/osrs/osrs_pvp_actions.h @@ -205,9 +205,13 @@ static void reset_tick_flags(Player* p) { p->clicks_this_tick = 0; } -static void execute_switches(OsrsEnv* env, int agent_idx, int* actions) { +static void execute_switches( + OsrsEnv* env, + int agent_idx, + int* actions, + const EncounterArenaTopology* topology +) { Player* p = &env->players[agent_idx]; - const CollisionMap* cmap = (const CollisionMap*)env->collision_map; p->consumable_used_this_tick = 0; @@ -312,17 +316,19 @@ static void execute_switches(OsrsEnv* env, int agent_idx, int* actions) { int dest_x = -1, dest_y = -1; switch (combat_action) { case MOVE_ADJACENT: - if (!select_closest_adjacent_tile(p, tx, ty, &dest_x, &dest_y, cmap)) { + if (!select_closest_adjacent_tile( + p, tx, ty, &dest_x, &dest_y, topology)) { dest_x = -1; dest_y = -1; } break; case MOVE_UNDER: - if (is_in_wilderness(tx, ty) && collision_tile_walkable(cmap, 0, tx, ty)) { + if (pvp_topology_tile_walkable(topology, tx, ty)) { dest_x = tx; dest_y = ty; } break; case MOVE_DIAGONAL: - if (!select_closest_diagonal_tile(p, tx, ty, &dest_x, &dest_y, cmap)) { + if (!select_closest_diagonal_tile( + p, tx, ty, &dest_x, &dest_y, topology)) { dest_x = -1; dest_y = -1; } break; @@ -333,7 +339,8 @@ static void execute_switches(OsrsEnv* env, int agent_idx, int* actions) { case MOVE_FARCAST_6: case MOVE_FARCAST_7: { int fd = combat_action - MOVE_FARCAST_2 + 2; - if (!select_farcast_tile(p, tx, ty, fd, &dest_x, &dest_y, cmap)) { + if (!select_farcast_tile( + p, tx, ty, fd, &dest_x, &dest_y, topology)) { dest_x = -1; dest_y = -1; } break; @@ -400,60 +407,53 @@ static PvpAttackDecode pvp_decode_attack_actions( }; } -static void execute_attack_movement(OsrsEnv* env, int agent_idx, int* actions) { +static void execute_attack_movement( + OsrsEnv* env, + int agent_idx, + int* actions, + const EncounterArenaTopology* topology, + OsrsActorRouteCache* route_cache +) { Player* p = &env->players[agent_idx]; Player* t = &env->players[1 - agent_idx]; - const CollisionMap* cmap = (const CollisionMap*)env->collision_map; - - PvpAttackDecode decode = pvp_decode_attack_actions(env, agent_idx, p, actions); + PvpAttackDecode decode = + pvp_decode_attack_actions(env, agent_idx, p, actions); if (decode.attack_action != ATTACK_NONE) osrs_interaction_set(&p->interaction, 1 - agent_idx); - int has_attack = (decode.attack_action != ATTACK_NONE) || osrs_interaction_active(&p->interaction); - int dist = chebyshev_distance(p->x, p->y, t->x, t->y); - + int has_attack = + decode.attack_action != ATTACK_NONE || + osrs_interaction_active(&p->interaction); + int distance = chebyshev_distance(p->x, p->y, t->x, t->y); AttackStyle attack_style = ATTACK_STYLE_NONE; - if (decode.attack_action != ATTACK_NONE) { - switch (decode.attack_action) { - case ATTACK_ATK: - attack_style = get_slot_weapon_attack_style(p); - break; - case ATTACK_ICE: - attack_style = ATTACK_STYLE_MAGIC; - break; - case ATTACK_BLOOD: - attack_style = ATTACK_STYLE_MAGIC; - break; - default: - break; - } - } else if (osrs_interaction_active(&p->interaction)) { + if (decode.attack_action == ATTACK_ATK) attack_style = get_slot_weapon_attack_style(p); - } - if (decode.attack_action == ATTACK_ICE && !can_cast_ice_spell(p)) { + else if (decode.attack_action == ATTACK_ICE || + decode.attack_action == ATTACK_BLOOD) + attack_style = ATTACK_STYLE_MAGIC; + else if (osrs_interaction_active(&p->interaction)) + attack_style = get_slot_weapon_attack_style(p); + + if (decode.attack_action == ATTACK_ICE && !can_cast_ice_spell(p)) attack_style = ATTACK_STYLE_NONE; - } - if (decode.attack_action == ATTACK_BLOOD && !can_cast_blood_spell(p)) { + if (decode.attack_action == ATTACK_BLOOD && !can_cast_blood_spell(p)) attack_style = ATTACK_STYLE_NONE; - } p->did_attack_auto_move = 0; - - if (has_attack && decode.move_action == MOVE_NONE && !decode.explicit_move_in_progress && can_move(p)) { - if (attack_style == ATTACK_STYLE_MELEE && !is_in_melee_range(p, t)) { - int adj_x, adj_y; - if (select_closest_adjacent_tile(p, t->x, t->y, &adj_x, &adj_y, cmap)) { - set_destination(p, adj_x, adj_y, cmap); - } - p->did_attack_auto_move = 1; - dist = chebyshev_distance(p->x, p->y, t->x, t->y); - } - } - - if (has_attack && dist == 0 && can_move(p)) { - step_out_from_same_tile(p, t, cmap); - } + if (!has_attack || + decode.move_action != MOVE_NONE || + decode.explicit_move_in_progress || + !can_move(p)) + return; + + int melee_chase = + attack_style == ATTACK_STYLE_MELEE && + !is_in_melee_range(p, t); + if (!melee_chase && distance != 0) return; + (void)pvp_step_player_melee_chase( + env, agent_idx, topology, route_cache); + p->did_attack_auto_move = melee_chase; } /* runs after BOTH players' attack movement so range checks use final positions; @@ -462,11 +462,12 @@ static void execute_attack_combat( OsrsEnv* env, int agent_idx, int* actions, - const EncounterArenaTopology* topology + const EncounterArenaTopology* topology, + OsrsActorRouteCache* route_cache ) { Player* p = &env->players[agent_idx]; Player* t = &env->players[1 - agent_idx]; - const CollisionMap* cmap = (const CollisionMap*)env->collision_map; + PvpAttackDecode decode = pvp_decode_attack_actions(env, agent_idx, p, actions); @@ -547,21 +548,19 @@ static void execute_attack_combat( if (has_attack && decode.move_action == MOVE_NONE && !decode.explicit_move_in_progress && can_move(p) && !p->did_attack_auto_move) { int in_range = 0; - int auto_walk_range = 1; + int chase_range = 1; switch (attack_style) { case ATTACK_STYLE_MELEE: in_range = is_in_melee_range(p, t); break; case ATTACK_STYLE_RANGED: { - int range = get_attack_range(p, ATTACK_STYLE_RANGED); - auto_walk_range = range; - in_range = (dist <= range); + chase_range = get_attack_range(p, ATTACK_STYLE_RANGED); + in_range = (dist <= chase_range); break; } case ATTACK_STYLE_MAGIC: { - int range = get_attack_range(p, ATTACK_STYLE_MAGIC); - auto_walk_range = range; - in_range = (dist <= range); + chase_range = get_attack_range(p, ATTACK_STYLE_MAGIC); + in_range = (dist <= chase_range); break; } default: @@ -570,12 +569,15 @@ static void execute_attack_combat( } if (!in_range) { if (attack_style == ATTACK_STYLE_MELEE) { - int adj_x, adj_y; - if (select_closest_adjacent_tile(p, t->x, t->y, &adj_x, &adj_y, cmap)) { - set_destination(p, adj_x, adj_y, cmap); - } + (void)pvp_step_player_melee_chase( + env, agent_idx, topology, route_cache); } else { - move_toward_target(p, t, auto_walk_range, cmap, topology); + (void)pvp_step_player_ranged_chase( + env, + agent_idx, + chase_range, + topology, + route_cache); } } } diff --git a/ocean/osrs/osrs_pvp_api.h b/ocean/osrs/osrs_pvp_api.h index f2e54a7f86..55c4504cbb 100644 --- a/ocean/osrs/osrs_pvp_api.h +++ b/ocean/osrs/osrs_pvp_api.h @@ -257,7 +257,11 @@ void pvp_init(OsrsEnv* env) { void pvp_render(OsrsEnv* env); -void pvp_reset(OsrsEnv* env) { +void pvp_reset( + OsrsEnv* env, + const EncounterArenaTopology* topology +) { + encounter_arena_topology_require_finalized(topology); if (env->has_rng_seed) { if (env->rng_seed == 0) { fprintf(stderr, "Error: seed must be non-zero (use seed=1 or higher in reset())\n"); @@ -342,17 +346,23 @@ void pvp_reset(OsrsEnv* env) { } for (int i = 0; i < NUM_AGENTS; i++) { - generate_slot_observations(env, i); + generate_slot_observations(env, i, topology); if (env->action_masks != NULL && (env->action_masks_agents & (1 << i))) { - compute_action_masks(env, i); + compute_action_masks(env, i, topology); } } } -static void pvp_resolve_same_tile(OsrsEnv* env, int first, int second) { +static void pvp_resolve_same_tile( + OsrsEnv* env, + int first, + int second, + const EncounterArenaTopology* topology +) { if (env->players[0].x == env->players[1].x && - env->players[0].y == env->players[1].y) { - resolve_same_tile(&env->players[second], &env->players[first], (const CollisionMap*)env->collision_map); + env->players[0].y == env->players[1].y) { + resolve_same_tile( + &env->players[second], &env->players[first], topology); } } @@ -375,6 +385,7 @@ void pvp_step( fprintf(stderr, "PvP step missing actor route caches\n"); abort(); } + encounter_arena_topology_require_finalized(route_topology); memset(env->rewards, 0, NUM_AGENTS * sizeof(float)); memset(env->terminals, 0, NUM_AGENTS); @@ -470,8 +481,8 @@ void pvp_step( pre_move_y[i] = env->players[i].y; } - execute_switches(env, first, agent_actions[first]); - execute_switches(env, second, agent_actions[second]); + execute_switches(env, first, agent_actions[first], route_topology); + execute_switches(env, second, agent_actions[second], route_topology); for (int i = 0; i < NUM_AGENTS; i++) { Player* pi = &env->players[i]; @@ -483,17 +494,21 @@ void pvp_step( pvp_step_player_movement(env, first, route_topology, &route_cache[first]); pvp_step_player_movement(env, second, route_topology, &route_cache[second]); - pvp_resolve_same_tile(env, first, second); + pvp_resolve_same_tile(env, first, second, route_topology); - execute_attack_movement(env, first, agent_actions[first]); - execute_attack_movement(env, second, agent_actions[second]); + execute_attack_movement( + env, first, agent_actions[first], route_topology, &route_cache[first]); + execute_attack_movement( + env, second, agent_actions[second], route_topology, &route_cache[second]); - pvp_resolve_same_tile(env, first, second); + pvp_resolve_same_tile(env, first, second, route_topology); - execute_attack_combat(env, first, agent_actions[first], route_topology); - execute_attack_combat(env, second, agent_actions[second], route_topology); + execute_attack_combat( + env, first, agent_actions[first], route_topology, &route_cache[first]); + execute_attack_combat( + env, second, agent_actions[second], route_topology, &route_cache[second]); - pvp_resolve_same_tile(env, first, second); + pvp_resolve_same_tile(env, first, second, route_topology); for (int i = 0; i < NUM_AGENTS; i++) { int dx = abs(env->players[i].x - pre_move_x[i]); @@ -559,9 +574,9 @@ void pvp_step( env->_episode_return += env->rewards[0]; for (int i = 0; i < NUM_AGENTS; i++) { - generate_slot_observations(env, i); + generate_slot_observations(env, i, route_topology); if (env->action_masks != NULL && (env->action_masks_agents & (1 << i))) { - compute_action_masks(env, i); + compute_action_masks(env, i, route_topology); } } @@ -600,7 +615,7 @@ void pvp_step( env->log.n = 1.0f; if (env->auto_reset) { - pvp_reset(env); + pvp_reset(env, route_topology); } } else { env->ocean_io.agent_terminals[0] = 0; diff --git a/ocean/osrs/osrs_pvp_movement.h b/ocean/osrs/osrs_pvp_movement.h index fa43231dd3..7651dd341b 100644 --- a/ocean/osrs/osrs_pvp_movement.h +++ b/ocean/osrs/osrs_pvp_movement.h @@ -7,9 +7,33 @@ #include "osrs_encounter_player.h" #include "osrs_pvp_gear.h" +static inline int pvp_topology_destination_selectable( + const EncounterArenaTopology* topology, + int x, + int y +) { + if (!is_in_wilderness(x, y)) return 0; + if (encounter_arena_topology_contains(topology, x, y)) + return !encounter_arena_topology_tile_blocked(topology, x, y); + + int min_x = topology->origin_x; + int max_x = min_x + topology->width - 1; + int min_y = topology->origin_y; + int max_y = min_y + topology->height - 1; + int fallback_x = x < min_x ? min_x : (x > max_x ? max_x : x); + int fallback_y = y < min_y ? min_y : (y > max_y ? max_y : y); + return !encounter_arena_topology_tile_blocked( + topology, fallback_x, fallback_y); +} + static int select_closest_candidate_tile( - Player* p, const int candidates[4][2], int target_x, int target_y, - int* out_x, int* out_y, const CollisionMap* cmap + Player* p, + const int candidates[4][2], + int target_x, + int target_y, + int* out_x, + int* out_y, + const EncounterArenaTopology* topology ) { int has_best = 0; int best_x = 0; @@ -21,20 +45,16 @@ static int select_closest_candidate_tile( for (int i = 0; i < 4; i++) { int cx = candidates[i][0]; int cy = candidates[i][1]; - if (!is_in_wilderness(cx, cy)) { - continue; - } - if (!collision_tile_walkable(cmap, 0, cx, cy)) { + if (!pvp_topology_destination_selectable(topology, cx, cy)) continue; - } int dist_agent = chebyshev_distance(p->x, p->y, cx, cy); int dist_target = chebyshev_distance(cx, cy, target_x, target_y); int hash = tile_hash(cx, cy); if (!has_best || - dist_agent < best_dist_agent || - (dist_agent == best_dist_agent && - (dist_target < best_dist_target || - (dist_target == best_dist_target && hash < best_hash)))) { + dist_agent < best_dist_agent || + (dist_agent == best_dist_agent && + (dist_target < best_dist_target || + (dist_target == best_dist_target && hash < best_hash)))) { has_best = 1; best_x = cx; best_y = cy; @@ -44,65 +64,74 @@ static int select_closest_candidate_tile( } } - if (!has_best) { - return 0; - } + if (!has_best) return 0; *out_x = best_x; *out_y = best_y; return 1; } -static int select_closest_adjacent_tile(Player* p, int target_x, int target_y, int* out_x, int* out_y, const CollisionMap* cmap) { +static int select_closest_adjacent_tile( + Player* p, + int target_x, + int target_y, + int* out_x, + int* out_y, + const EncounterArenaTopology* topology +) { const int candidates[4][2] = { {target_x, target_y + 1}, {target_x + 1, target_y}, {target_x, target_y - 1}, {target_x - 1, target_y} }; - return select_closest_candidate_tile(p, candidates, target_x, target_y, out_x, out_y, cmap); + return select_closest_candidate_tile( + p, candidates, target_x, target_y, out_x, out_y, topology); } -static int select_closest_diagonal_tile(Player* p, int target_x, int target_y, int* out_x, int* out_y, const CollisionMap* cmap) { +static int select_closest_diagonal_tile( + Player* p, + int target_x, + int target_y, + int* out_x, + int* out_y, + const EncounterArenaTopology* topology +) { const int candidates[4][2] = { {target_x + 1, target_y + 1}, {target_x + 1, target_y - 1}, {target_x - 1, target_y - 1}, {target_x - 1, target_y + 1} }; - return select_closest_candidate_tile(p, candidates, target_x, target_y, out_x, out_y, cmap); + return select_closest_candidate_tile( + p, candidates, target_x, target_y, out_x, out_y, topology); } -static int select_farcast_tile(Player* p, int target_x, int target_y, int distance, int* out_x, int* out_y, const CollisionMap* cmap) { +static int select_farcast_tile( + Player* p, + int target_x, + int target_y, + int distance, + int* out_x, + int* out_y, + const EncounterArenaTopology* topology +) { int raw_dx = p->x - target_x; int raw_dy = p->y - target_y; int d = distance; - int dx = raw_dx < -d ? -d : (raw_dx > d ? d : raw_dx); int dy = raw_dy < -d ? -d : (raw_dy > d ? d : raw_dy); - int adx = abs_int(dx); int ady = abs_int(dy); if (adx < d && ady < d) { - if (adx >= ady) { - dx = (raw_dx >= 0) ? d : -d; - } else { - dy = (raw_dy >= 0) ? d : -d; - } + if (adx >= ady) + dx = raw_dx >= 0 ? d : -d; + else + dy = raw_dy >= 0 ? d : -d; } int cx = target_x + dx; int cy = target_y + dy; - - if (is_in_wilderness(cx, cy) && collision_tile_walkable(cmap, 0, cx, cy)) { - *out_x = cx; - *out_y = cy; - return 1; - } - - cx = cx < WILD_MIN_X ? WILD_MIN_X : (cx > WILD_MAX_X ? WILD_MAX_X : cx); - cy = cy < WILD_MIN_Y ? WILD_MIN_Y : (cy > WILD_MAX_Y ? WILD_MAX_Y : cy); - if (chebyshev_distance(cx, cy, target_x, target_y) == distance - && collision_tile_walkable(cmap, 0, cx, cy)) { + if (pvp_topology_destination_selectable(topology, cx, cy)) { *out_x = cx; *out_y = cy; return 1; @@ -111,63 +140,6 @@ static int select_farcast_tile(Player* p, int target_x, int target_y, int distan return 0; } -static int step_toward_destination(Player* p, const CollisionMap* cmap) { - int dx = p->dest_x - p->x; - int dy = p->dest_y - p->y; - if (dx == 0 && dy == 0) { - return 0; - } - - int step_x = (dx > 0) ? 1 : (dx < 0 ? -1 : 0); - int step_y = (dy > 0) ? 1 : (dy < 0 ? -1 : 0); - - if (step_x != 0 && step_y != 0) { - if (collision_traversable_step(cmap, 0, p->x, p->y, step_x, step_y)) { - p->x += step_x; - p->y += step_y; - return 1; - } - if (collision_traversable_step(cmap, 0, p->x, p->y, step_x, 0)) { - p->x += step_x; - return 1; - } - if (collision_traversable_step(cmap, 0, p->x, p->y, 0, step_y)) { - p->y += step_y; - return 1; - } - return 0; - } - - if (collision_traversable_step(cmap, 0, p->x, p->y, step_x, step_y)) { - p->x += step_x; - p->y += step_y; - return 1; - } - - return 0; -} - -static void set_destination(Player* p, int dest_x, int dest_y, const CollisionMap* cmap) { - p->dest_x = dest_x; - p->dest_y = dest_y; - if (p->x == dest_x && p->y == dest_y) { - p->is_moving = 0; - return; - } - if (!step_toward_destination(p, cmap)) { - p->is_moving = 0; - return; - } - if (p->x != dest_x || p->y != dest_y) { - step_toward_destination(p, cmap); - } - p->is_moving = (p->x != dest_x || p->y != dest_y) ? 1 : 0; -} - -static int pvp_tile_walkable(void* ctx, int x, int y) { - const CollisionMap* cmap = (const CollisionMap*)ctx; - return is_in_wilderness(x, y) && collision_tile_walkable(cmap, 0, x, y); -} typedef struct { EncounterArenaTopology* topology; @@ -183,6 +155,15 @@ static uint32_t pvp_route_topology_flags(void* data, int x, int y) { : 0; } + +static inline int pvp_topology_tile_walkable( + const EncounterArenaTopology* topology, + int x, + int y +) { + return !encounter_arena_topology_tile_blocked(topology, x, y); +} + static const EncounterArenaTopology* pvp_route_topology_finalize( const CollisionMap* collision_map ) { @@ -192,9 +173,10 @@ static const EncounterArenaTopology* pvp_route_topology_finalize( .width = FIGHT_AREA_WIDTH, .height = FIGHT_AREA_HEIGHT, .max_footprint_size = 1, - .revision = UINT64_C(0x4e48505650000001), + .revision = UINT64_C(0x4e48505650000004), .tile_flags = pvp_route_topology_flags, .tile_flags_ctx = (void*)collision_map, + .los_build_mode = ENCOUNTER_ARENA_TOPOLOGY_LOS_BUILD_OPEN, }; if (!pvp_route_topology_owner.topology) { pvp_route_topology_owner.topology = @@ -210,87 +192,15 @@ static const EncounterArenaTopology* pvp_route_topology_finalize( } -static void move_toward_target( - Player* p, - Player* target, - int attack_range, - const CollisionMap* cmap, - const EncounterArenaTopology* topology -) { - if (p->frozen_ticks > 0) { - return; - } - EncounterRouteInput route_input = { - .topology = topology, - .blockers = {0}, - .source_x = p->x, - .source_y = p->y, - .actor_size = 1, - .target_x = target->x, - .target_y = target->y, - .target_size = 1, - .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, - .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, - .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, - }; - EncounterRouteResult route = encounter_route_solve(&route_input); - int moved = 0; - if (route.outcome == ROUTE_REACHED_TARGET || - route.outcome == ROUTE_REACHED_FALLBACK) { - p->x += route.first_dx; - p->y += route.first_dy; - moved = route.first_dx != 0 || route.first_dy != 0; - if ((route.run_dx != 0 || route.run_dy != 0) && - !encounter_player_can_attack( - p->x, p->y, - target->x, target->y, 1, attack_range, - cmap, 0, 0, osrs_los_open_query())) { - p->x += route.run_dx; - p->y += route.run_dy; - moved = 1; - } - } - p->is_moving = moved; -} - -static void step_out_from_same_tile(Player* p, Player* target, const CollisionMap* cmap) { - if (p->frozen_ticks > 0) { - return; - } - - int dest_x = target->x - 1; - int dest_y = target->y; - if (is_in_wilderness(dest_x, dest_y) && collision_tile_walkable(cmap, 0, dest_x, dest_y)) { - set_destination(p, dest_x, dest_y, cmap); - return; - } - dest_x = target->x + 1; - if (is_in_wilderness(dest_x, dest_y) && collision_tile_walkable(cmap, 0, dest_x, dest_y)) { - set_destination(p, dest_x, dest_y, cmap); - return; - } - dest_x = target->x; - dest_y = target->y - 1; - if (is_in_wilderness(dest_x, dest_y) && collision_tile_walkable(cmap, 0, dest_x, dest_y)) { - set_destination(p, dest_x, dest_y, cmap); - return; - } - dest_y = target->y + 1; - if (is_in_wilderness(dest_x, dest_y) && collision_tile_walkable(cmap, 0, dest_x, dest_y)) { - set_destination(p, dest_x, dest_y, cmap); - return; - } -} /* skipped when either player is frozen: walking under a frozen opponent is a legal, intentional position in OSRS PvP */ -static void resolve_same_tile(Player* mover, Player* blocker, const CollisionMap* cmap) { - if (blocker->frozen_ticks > 0) { - return; - } - if (mover->frozen_ticks > 0) { - return; - } +static void resolve_same_tile( + Player* mover, + Player* blocker, + const EncounterArenaTopology* topology +) { + if (blocker->frozen_ticks > 0 || mover->frozen_ticks > 0) return; static const int OFFSETS[8][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1}, @@ -300,9 +210,8 @@ static void resolve_same_tile(Player* mover, Player* blocker, const CollisionMap for (int i = 0; i < 8; i++) { int nx = mover->x + OFFSETS[i][0]; int ny = mover->y + OFFSETS[i][1]; - if (is_in_wilderness(nx, ny) - && collision_tile_walkable(cmap, 0, nx, ny) - && !(nx == blocker->x && ny == blocker->y)) { + if (pvp_topology_tile_walkable(topology, nx, ny) && + !(nx == blocker->x && ny == blocker->y)) { mover->x = nx; mover->y = ny; mover->dest_x = nx; @@ -334,20 +243,16 @@ static int pvp_lookup_attack_target(void* ctx, int target_slot, OsrsAttackTarget } static inline OsrsEncounterArena pvp_build_arena( - OsrsEnv* env, const EncounterArenaTopology* topology ) { - OsrsEncounterArena arena; - arena.topology = topology; - arena.blockers = (EncounterRouteBlockers){0}; - arena.movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN; - arena.cost_policy = ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS; - arena.destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS; - arena.collision_map = (const CollisionMap*)env->collision_map; - arena.world_offset_x = 0; - arena.world_offset_y = 0; - arena.los_query = osrs_los_open_query(); - return arena; + return (OsrsEncounterArena){ + .topology = topology, + .blockers = {0}, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS, + .destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + .attack_geometry = ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY, + }; } static inline OsrsPlayerStepResult pvp_step_player_movement( @@ -363,7 +268,7 @@ static inline OsrsPlayerStepResult pvp_step_player_movement( if (*dest_x < 0 || *dest_y < 0) return result; Player* p = &env->players[agent_idx]; - OsrsEncounterArena arena = pvp_build_arena(env, topology); + OsrsEncounterArena arena = pvp_build_arena(topology); OsrsPlayerStepInput input = { .player = p, .interaction = &p->interaction, @@ -384,6 +289,98 @@ static inline OsrsPlayerStepResult pvp_step_player_movement( return result; } +static inline int pvp_step_player_melee_chase( + OsrsEnv* env, + int agent_idx, + const EncounterArenaTopology* topology, + OsrsActorRouteCache* route_cache +) { + Player* player = &env->players[agent_idx]; + Player* target = &env->players[1 - agent_idx]; + int destination_x = 0; + int destination_y = 0; + if (!select_closest_adjacent_tile( + player, + target->x, + target->y, + &destination_x, + &destination_y, + topology)) + return 0; + + EncounterRouteInput route_input = { + .topology = topology, + .source_x = player->x, + .source_y = player->y, + .actor_size = 1, + .target_x = destination_x, + .target_y = destination_y, + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_DIRECT, + }; + EncounterRouteResult route = + encounter_route_greedy_direct(&route_input); + int moved = osrs_player_step_apply_route(player, &route); + player->dest_x = destination_x; + player->dest_y = destination_y; + player->is_moving = + player->x != destination_x || player->y != destination_y; + osrs_actor_route_cache_clear(route_cache); + return moved; +} + +static inline int pvp_step_player_ranged_chase( + OsrsEnv* env, + int agent_idx, + int attack_range, + const EncounterArenaTopology* topology, + OsrsActorRouteCache* route_cache +) { + Player* player = &env->players[agent_idx]; + Player* target = &env->players[1 - agent_idx]; + EncounterRouteInput route_input = { + .topology = topology, + .source_x = player->x, + .source_y = player->y, + .actor_size = 1, + .target_x = target->x, + .target_y = target->y, + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + }; + EncounterRouteResult route = encounter_route_solve(&route_input); + if (route.outcome != ROUTE_REACHED_TARGET && + route.outcome != ROUTE_REACHED_FALLBACK) { + player->is_moving = 0; + return 0; + } + + int moved = route.first_dx != 0 || route.first_dy != 0; + player->x += route.first_dx; + player->y += route.first_dy; + if ((route.run_dx != 0 || route.run_dy != 0) && + !encounter_arena_topology_player_can_attack( + topology, + player->x, + player->y, + target->x, + target->y, + 1, + attack_range)) { + player->x += route.run_dx; + player->y += route.run_dy; + moved = 1; + } + player->is_moving = moved; + osrs_actor_route_cache_clear(route_cache); + return moved; +} + + static inline void pvp_set_walk_dest_from_head_move(OsrsEnv* env, int agent_idx, int move_action) { Player* p = &env->players[agent_idx]; if (move_action <= 0 || move_action >= MOVE_DIM) return; diff --git a/ocean/osrs/osrs_pvp_observations.h b/ocean/osrs/osrs_pvp_observations.h index 30f9fc62cb..dfeca97a91 100644 --- a/ocean/osrs/osrs_pvp_observations.h +++ b/ocean/osrs/osrs_pvp_observations.h @@ -78,12 +78,20 @@ static inline int can_eat_karambwan(Player* p) { return osrs_player_can_eat_food_type(p, FOOD_KARAMBWAN); } -static inline int can_move_adjacent(Player* p, const CollisionMap* cmap) { +static inline int can_move_adjacent( + Player* p, + const EncounterArenaTopology* topology +) { int dest_x = 0; int dest_y = 0; - if (!select_closest_adjacent_tile(p, p->last_obs_target_x, p->last_obs_target_y, &dest_x, &dest_y, cmap)) { + if (!select_closest_adjacent_tile( + p, + p->last_obs_target_x, + p->last_obs_target_y, + &dest_x, + &dest_y, + topology)) return 0; - } return !(dest_x == p->x && dest_y == p->y); } @@ -92,21 +100,39 @@ static inline int can_move_under(Player* p, Player* target) { return remaining_ticks(target->frozen_ticks) > 0 && dist != 0; } -static inline int can_move_to_farcast(Player* p, int distance, const CollisionMap* cmap) { +static inline int can_move_to_farcast( + Player* p, + int distance, + const EncounterArenaTopology* topology +) { int dest_x = 0; int dest_y = 0; - if (!select_farcast_tile(p, p->last_obs_target_x, p->last_obs_target_y, distance, &dest_x, &dest_y, cmap)) { + if (!select_farcast_tile( + p, + p->last_obs_target_x, + p->last_obs_target_y, + distance, + &dest_x, + &dest_y, + topology)) return 0; - } return !(dest_x == p->x && dest_y == p->y); } -static inline int can_move_diagonal(Player* p, const CollisionMap* cmap) { +static inline int can_move_diagonal( + Player* p, + const EncounterArenaTopology* topology +) { int dest_x = 0; int dest_y = 0; - if (!select_closest_diagonal_tile(p, p->last_obs_target_x, p->last_obs_target_y, &dest_x, &dest_y, cmap)) { + if (!select_closest_diagonal_tile( + p, + p->last_obs_target_x, + p->last_obs_target_y, + &dest_x, + &dest_y, + topology)) return 0; - } return !(dest_x == p->x && dest_y == p->y); } @@ -170,7 +196,11 @@ static void ocean_write_obs_p1(OsrsEnv* env) { ocean_write_obs_agent(env, env->ocean_io.agent_obs_p1, 1); } -static void generate_slot_observations(OsrsEnv* env, int agent_idx) { +static void generate_slot_observations( + OsrsEnv* env, + int agent_idx, + const EncounterArenaTopology* topology +) { Player* p = &env->players[agent_idx]; Player* t = &env->players[1 - agent_idx]; @@ -462,7 +492,6 @@ static void generate_slot_observations(OsrsEnv* env, int agent_idx) { if (scale < 1.0f) scale = 1.0f; obs[194] = clampf((float)(t->x - p->x) / scale, -1.0f, 1.0f); obs[195] = clampf((float)(t->y - p->y) / scale, -1.0f, 1.0f); - const CollisionMap* cmap_obs = (const CollisionMap*)env->collision_map; for (int m = 0; m < MOVE_DIM; m++) { if (m == 0) { obs[196 + m] = 1.0f; @@ -470,11 +499,16 @@ static void generate_slot_observations(OsrsEnv* env, int agent_idx) { } int nx = p->x + ENCOUNTER_MOVE_TARGET_DX[m]; int ny = p->y + ENCOUNTER_MOVE_TARGET_DY[m]; - obs[196 + m] = pvp_tile_walkable((void*)cmap_obs, nx, ny) ? 1.0f : 0.0f; + obs[196 + m] = + pvp_topology_tile_walkable(topology, nx, ny) ? 1.0f : 0.0f; } } -static void compute_action_masks(OsrsEnv* env, int agent_idx) { +static void compute_action_masks( + OsrsEnv* env, + int agent_idx, + const EncounterArenaTopology* topology +) { Player* p = &env->players[agent_idx]; Player* t = &env->players[1 - agent_idx]; @@ -527,16 +561,23 @@ static void compute_action_masks(OsrsEnv* env, int agent_idx) { melee_reachable; mask[offset + ATTACK_ICE] = attack_ready && can_cast_ice_spell(p); mask[offset + ATTACK_BLOOD] = attack_ready && can_cast_blood_spell(p); - const CollisionMap* cmap = (const CollisionMap*)env->collision_map; - mask[offset + MOVE_ADJACENT] = can_move_now && can_move_adjacent(p, cmap); + mask[offset + MOVE_ADJACENT] = + can_move_now && can_move_adjacent(p, topology); mask[offset + MOVE_UNDER] = can_move_now && can_move_under(p, t); - mask[offset + MOVE_DIAGONAL] = can_move_now && can_move_diagonal(p, cmap); - mask[offset + MOVE_FARCAST_2] = can_move_now && can_move_to_farcast(p, 2, cmap); - mask[offset + MOVE_FARCAST_3] = can_move_now && can_move_to_farcast(p, 3, cmap); - mask[offset + MOVE_FARCAST_4] = can_move_now && can_move_to_farcast(p, 4, cmap); - mask[offset + MOVE_FARCAST_5] = can_move_now && can_move_to_farcast(p, 5, cmap); - mask[offset + MOVE_FARCAST_6] = can_move_now && can_move_to_farcast(p, 6, cmap); - mask[offset + MOVE_FARCAST_7] = can_move_now && can_move_to_farcast(p, 7, cmap); + mask[offset + MOVE_DIAGONAL] = + can_move_now && can_move_diagonal(p, topology); + mask[offset + MOVE_FARCAST_2] = + can_move_now && can_move_to_farcast(p, 2, topology); + mask[offset + MOVE_FARCAST_3] = + can_move_now && can_move_to_farcast(p, 3, topology); + mask[offset + MOVE_FARCAST_4] = + can_move_now && can_move_to_farcast(p, 4, topology); + mask[offset + MOVE_FARCAST_5] = + can_move_now && can_move_to_farcast(p, 5, topology); + mask[offset + MOVE_FARCAST_6] = + can_move_now && can_move_to_farcast(p, 6, topology); + mask[offset + MOVE_FARCAST_7] = + can_move_now && can_move_to_farcast(p, 7, topology); offset += COMBAT_DIM; int has_prayer = p->current_prayer > 0; @@ -585,7 +626,8 @@ static void compute_action_masks(OsrsEnv* env, int agent_idx) { } int nx = p->x + ENCOUNTER_MOVE_TARGET_DX[m]; int ny = p->y + ENCOUNTER_MOVE_TARGET_DY[m]; - mask[offset + m] = pvp_tile_walkable((void*)cmap, nx, ny) ? 1 : 0; + mask[offset + m] = + pvp_topology_tile_walkable(topology, nx, ny) ? 1 : 0; } offset += MOVE_DIM; } diff --git a/ocean/osrs/osrs_visual.c b/ocean/osrs/osrs_visual.c index ac3a1c06df..844568b669 100644 --- a/ocean/osrs/osrs_visual.c +++ b/ocean/osrs/osrs_visual.c @@ -106,7 +106,7 @@ static void run_random_episode(OsrsEnv* env, int verbose) { const EncounterArenaTopology* route_topology = pvp_route_topology_finalize((const CollisionMap*)env->collision_map); OsrsActorRouteCache route_cache[NUM_AGENTS] = {0}; - pvp_reset(env); + pvp_reset(env, route_topology); while (!env->episode_over) { for (int agent = 0; agent < NUM_AGENTS; agent++) { @@ -143,7 +143,7 @@ static void benchmark(OsrsEnv* env, int num_steps) { int total_steps = 0; while (total_steps < num_steps) { - pvp_reset(env); + pvp_reset(env, route_topology); pvp_actor_route_caches_clear(route_cache); episodes++; @@ -327,6 +327,9 @@ static VisualCollisionLoad visual_load_encounter_collision_map( ) { CollisionMap* cmap = NULL; int offset_x = 0, offset_y = 0; + if (encounter_name_is_pvp(encounter_name)) { + cmap = collision_map_load(OSRS_ASSET("wilderness.cmap")); + } else if (strcmp(encounter_name, "zulrah") == 0) { cmap = collision_map_load(OSRS_ASSET("zulrah.cmap")); offset_x = 2256; offset_y = 3061; @@ -339,8 +342,18 @@ static VisualCollisionLoad visual_load_encounter_collision_map( } VisualCollisionLoad result = { NULL, offset_x, offset_y }; if (cmap) { - edef->put_int(env->encounter_state, env->encounter_context, "world_offset_x", offset_x); - edef->put_int(env->encounter_state, env->encounter_context, "world_offset_y", offset_y); + if (!encounter_name_is_pvp(encounter_name)) { + edef->put_int( + env->encounter_state, + env->encounter_context, + "world_offset_x", + offset_x); + edef->put_int( + env->encounter_state, + env->encounter_context, + "world_offset_y", + offset_y); + } edef->put_ptr(env->encounter_state, env->encounter_context, "collision_map", cmap); env->collision_map = cmap; result.cmap = cmap; @@ -383,6 +396,7 @@ static void run_profile( printf("Profiling %s for 10 seconds...\n", encounter_name ? encounter_name : "pvp"); } + const EncounterArenaTopology* direct_pvp_topology = NULL; if (encounter_name) { const EncounterDef* edef = visual_open_encounter(env, encounter_name); if (!edef) return; @@ -404,11 +418,11 @@ static void run_profile( env->has_rng_seed = 1; env->rng_seed = profile_seed; env->is_lms = 1; - pvp_reset(env); + direct_pvp_topology = + pvp_route_topology_finalize( + (const CollisionMap*)env->collision_map); + pvp_reset(env, direct_pvp_topology); } - const EncounterArenaTopology* direct_pvp_topology = encounter_name - ? NULL - : pvp_route_topology_finalize((const CollisionMap*)env->collision_map); OsrsActorRouteCache direct_pvp_route_cache[NUM_AGENTS] = {0}; const EncounterDef* profile_edef = (const EncounterDef*)env->encounter_def; @@ -561,7 +575,7 @@ static void run_profile( } pvp_step(env, direct_pvp_topology, direct_pvp_route_cache); if (env->episode_over) { - pvp_reset(env); + pvp_reset(env, direct_pvp_topology); pvp_actor_route_caches_clear(direct_pvp_route_cache); } } @@ -1449,7 +1463,7 @@ static void visual_frame(void* arg) { (EncounterContext*)env->encounter_context, (uint32_t)rand()); } else { - pvp_reset(env); + pvp_reset(env, rc->route_topology); } render_reset_episode_visual_state(rc, env); visual_policy_reset_recurrent(&vs->policy); @@ -1929,6 +1943,20 @@ static void run_visual( uint32_t policy_seed ) { env->client = NULL; + const EncounterArenaTopology* direct_pvp_topology = NULL; + if (!encounter_name) { + const char* cmap_path = getenv("OSRS_COLLISION_MAP"); + if (cmap_path && cmap_path[0]) { + env->collision_map = collision_map_load(cmap_path); + if (env->collision_map) { + fprintf(stderr, "collision map loaded: %d regions\n", + ((CollisionMap*)env->collision_map)->count); + } + } + direct_pvp_topology = + pvp_route_topology_finalize( + (const CollisionMap*)env->collision_map); + } if (encounter_name) { const EncounterDef* edef = visual_open_encounter(env, encounter_name); @@ -1974,24 +2002,13 @@ static void run_visual( env->pvp_runtime.use_c_opponent = 1; env->pvp_runtime.opponent.type = OPP_IMPROVED; env->is_lms = 1; - pvp_reset(env); + pvp_reset(env, direct_pvp_topology); } - const char* cmap_path = getenv("OSRS_COLLISION_MAP"); - if (cmap_path && cmap_path[0]) { - env->collision_map = collision_map_load(cmap_path); - if (env->collision_map) { - fprintf(stderr, "collision map loaded: %d regions\n", - ((CollisionMap*)env->collision_map)->count); - } - } pvp_render(env); RenderClient* rc = (RenderClient*)env->client; - rc->route_topology = - (!encounter_name || strcmp(encounter_name, "pvp") == 0) - ? pvp_route_topology_finalize((const CollisionMap*)env->collision_map) - : NULL; + rc->route_topology = direct_pvp_topology; pvp_actor_route_caches_clear(rc->player_route_cache); #ifdef __EMSCRIPTEN__ if (!encounter_name || encounter_name_is_pvp(encounter_name)) { @@ -2412,7 +2429,10 @@ int main(int argc, char** argv) { benchmark(&env, 100000); printf("\nVerifying observations...\n"); - pvp_reset(&env); + pvp_reset( + &env, + pvp_route_topology_finalize( + (const CollisionMap*)env.collision_map)); printf("Observation count per agent: %d\n", SLOT_NUM_OBSERVATIONS); printf("First 10 observations (agent 0): "); for (int i = 0; i < 10; i++) { diff --git a/ocean/osrs/tests/test_osrs_pvp_pending_hits.c b/ocean/osrs/tests/test_osrs_pvp_pending_hits.c index 93305322af..a5f4302612 100644 --- a/ocean/osrs/tests/test_osrs_pvp_pending_hits.c +++ b/ocean/osrs/tests/test_osrs_pvp_pending_hits.c @@ -4,7 +4,8 @@ #include #include -#include "ocean/osrs/osrs_pvp_combat.h" +#include "ocean/osrs/encounters/encounter_nh_pvp.h" + #include "ocean/osrs/tests/osrs_test_check.h" @@ -83,7 +84,221 @@ static void test_pvp_remove_compacts_and_clears_tail(void) { ASSERT_INT_EQ("tail timer cleared", attacker.pending_hits[2].ticks_until_hit, 0); } +static CollisionMap* nh_map; +static NhPvpContext nh_context; +static NhPvpState* nh_state; + +static void init_nh_fixture(void) { + EncounterState* state = ENCOUNTER_NH_PVP.create(); + if (!state) abort(); + nh_state = (NhPvpState*)state; + ENCOUNTER_NH_PVP.init_context((EncounterContext*)&nh_context); + nh_map = collision_map_load("ocean/osrs/data/wilderness.cmap"); + if (!nh_map) abort(); + ENCOUNTER_NH_PVP.put_ptr( + state, (EncounterContext*)&nh_context, "collision_map", nh_map); + ENCOUNTER_NH_PVP.put_int( + state, (EncounterContext*)&nh_context, "seed", 1); + ENCOUNTER_NH_PVP.finalize_context( + state, (EncounterContext*)&nh_context); + ENCOUNTER_NH_PVP.reset( + state, (EncounterContext*)&nh_context, 1); + nh_state->env.auto_reset = 0; +} + +static void test_nh_topology_geometry_parity(void) { + printf("--- NH PvP topology geometry parity ---\n"); + const EncounterArenaTopology* topology = nh_context.route_topology; + CHECK("NH PvP topology covers the 61 by 28 fight arena", + topology->origin_x == FIGHT_AREA_BASE_X && + topology->origin_y == FIGHT_AREA_BASE_Y && + topology->width == 61 && topology->height == 28); + CHECK("NH PvP keeps the existing open static LOS combat rule", + topology->static_los_mode == ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN); + + int diagonal_wall_steps = 0; + for (int x = FIGHT_AREA_BASE_X; + x < FIGHT_AREA_BASE_X + FIGHT_AREA_WIDTH; + x++) { + for (int y = FIGHT_AREA_BASE_Y; + y < FIGHT_AREA_BASE_Y + FIGHT_AREA_HEIGHT; + y++) { + int expected_walkable = + is_in_wilderness(x, y) && + collision_tile_walkable(nh_map, 0, x, y); + CHECK("NH PvP static player tile parity", + pvp_topology_tile_walkable(topology, x, y) == + expected_walkable); + if (!expected_walkable) continue; + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) continue; + int destination_x = x + dx; + int destination_y = y + dy; + int expected_step = + encounter_arena_topology_contains( + topology, destination_x, destination_y) && + collision_traversable_step( + nh_map, 0, x, y, dx, dy); + int actual_step = + encounter_arena_topology_step_allowed( + topology, x, y, 1, dx, dy); + CHECK("NH PvP cardinal and diagonal wall parity", + actual_step == expected_step); + if (dx != 0 && dy != 0 && + !expected_step && + pvp_topology_tile_walkable( + topology, x + dx, y + dy)) + diagonal_wall_steps++; + } + } + } + } + CHECK("NH PvP collision map contains diagonal wall-side blocking", + diagonal_wall_steps > 0); +} + +static void test_nh_dynamic_player_occupancy(void) { + printf("--- NH PvP player occupancy stays dynamic ---\n"); + const EncounterArenaTopology* topology = nh_context.route_topology; + Player mover = {0}; + Player blocker = {0}; + mover.x = blocker.x = FIGHT_AREA_BASE_X + 8; + mover.y = blocker.y = FIGHT_AREA_BASE_Y + 8; + CHECK("opponent tile is not baked into static topology", + pvp_topology_tile_walkable(topology, blocker.x, blocker.y)); + resolve_same_tile(&mover, &blocker, topology); + ASSERT_INT_EQ("same-tile resolution keeps west-first priority", + mover.x, blocker.x - 1); + ASSERT_INT_EQ("same-tile resolution keeps the y coordinate", + mover.y, blocker.y); + CHECK("static topology remains unchanged after occupancy resolution", + pvp_topology_tile_walkable(topology, blocker.x, blocker.y)); +} + +static void run_nh_chase_case( + int loadout, + int combat, + int expected_x, + int expected_y, + const char* x_label, + const char* y_label +) { + ENCOUNTER_NH_PVP.reset( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + 1); + nh_state->env.auto_reset = 0; + Player* player = &nh_state->env.players[0]; + Player* target = &nh_state->env.players[1]; + player->x = FIGHT_AREA_BASE_X; + player->y = FIGHT_AREA_BASE_Y; + player->dest_x = player->x; + player->dest_y = player->y; + target->x = FIGHT_AREA_BASE_X + 10; + target->y = FIGHT_AREA_BASE_Y + 4; + target->dest_x = target->x; + target->dest_y = target->y; + int actions[NUM_ACTION_HEADS] = {0}; + actions[HEAD_LOADOUT] = loadout; + actions[HEAD_COMBAT] = combat; + ENCOUNTER_NH_PVP.step( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + actions); + ASSERT_INT_EQ(x_label, player->x, expected_x); + ASSERT_INT_EQ(y_label, player->y, expected_y); + ASSERT_INT_EQ("attack click cancels queued walk x", + nh_state->env.pvp_runtime.walk_dest_x[0], -1); + ASSERT_INT_EQ("attack click cancels queued walk y", + nh_state->env.pvp_runtime.walk_dest_y[0], -1); +} + +static void test_nh_attack_chase_destinations(void) { + printf("--- NH PvP attack chase destinations ---\n"); + run_nh_chase_case( + LOADOUT_MELEE, ATTACK_ATK, + FIGHT_AREA_BASE_X + 2, FIGHT_AREA_BASE_Y + 2, + "melee chase keeps x destination", + "melee chase keeps y destination"); + run_nh_chase_case( + LOADOUT_RANGE, ATTACK_ATK, + FIGHT_AREA_BASE_X + 2, FIGHT_AREA_BASE_Y, + "ranged chase keeps x destination", + "ranged chase keeps y destination"); + run_nh_chase_case( + LOADOUT_MAGE, ATTACK_ICE, + FIGHT_AREA_BASE_X, FIGHT_AREA_BASE_Y, + "in-range magic attack does not chase on x", + "in-range magic attack does not chase on y"); +} + +static void test_nh_out_of_arena_destination_fallback(void) { + printf("--- NH PvP out-of-arena destination fallback ---\n"); + Player player = {0}; + player.x = FIGHT_AREA_BASE_X + 1; + player.y = FIGHT_AREA_BASE_Y + 1; + int destination_x = -1; + int destination_y = -1; + CHECK("farcast click beyond the arena keeps a route fallback target", + select_farcast_tile( + &player, + FIGHT_AREA_BASE_X + 5, + FIGHT_AREA_BASE_Y, + 6, + &destination_x, + &destination_y, + nh_context.route_topology)); + ASSERT_INT_EQ("farcast keeps the original x destination", + destination_x, FIGHT_AREA_BASE_X - 1); + ASSERT_INT_EQ("farcast keeps the original y destination", + destination_y, FIGHT_AREA_BASE_Y + 1); + EncounterRouteInput route_input = { + .topology = nh_context.route_topology, + .source_x = player.x, + .source_y = player.y, + .actor_size = 1, + .target_x = destination_x, + .target_y = destination_y, + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + }; + EncounterRouteResult route = encounter_route_solve(&route_input); + ASSERT_INT_EQ("out-of-arena route reaches the arena edge", + route.outcome, ROUTE_REACHED_FALLBACK); + ASSERT_INT_EQ("out-of-arena route clamps x to the arena edge", + route.destination_x, FIGHT_AREA_BASE_X); + ASSERT_INT_EQ("out-of-arena route keeps the selected y", + route.destination_y, FIGHT_AREA_BASE_Y + 1); + ASSERT_INT_EQ("out-of-arena route steps toward the edge on x", + route.first_dx, -1); + ASSERT_INT_EQ("out-of-arena route keeps y on the first step", + route.first_dy, 0); + + route_input.source_x = FIGHT_AREA_BASE_X + 19; + route_input.source_y = FIGHT_AREA_BASE_Y; + route_input.target_x = route_input.source_x; + route_input.target_y = FIGHT_AREA_BASE_Y - 1; + route = encounter_route_solve(&route_input); + ASSERT_INT_EQ("boundary source reaches the clamped destination", + route.outcome, ROUTE_REACHED_FALLBACK); + ASSERT_INT_EQ("boundary source keeps x", + route.destination_x, route_input.source_x); + ASSERT_INT_EQ("boundary source does not route across the arena", + route.destination_y, route_input.source_y); + ASSERT_INT_EQ("boundary source has zero route distance", + route.distance, 0); +} + int main(void) { + init_nh_fixture(); + test_nh_topology_geometry_parity(); + test_nh_dynamic_player_occupancy(); + test_nh_attack_chase_destinations(); + test_nh_out_of_arena_destination_fallback(); + test_pvp_queue_accepts_capacity(); test_pvp_queue_overflow_aborts(); test_pvp_remove_compacts_and_clears_tail(); diff --git a/ocean/osrs/tests/test_zulrah_hit_delay.c b/ocean/osrs/tests/test_zulrah_hit_delay.c index c3ff2a68d6..ebfdcbf364 100644 --- a/ocean/osrs/tests/test_zulrah_hit_delay.c +++ b/ocean/osrs/tests/test_zulrah_hit_delay.c @@ -20,12 +20,21 @@ static int spec8_magic_delay(int distance, int is_player) { static ZulrahContext g_ctx; static ZulrahState g_state; +static CollisionMap* g_collision_map; + static ZulrahState* fresh_state(uint32_t seed) { EncounterState* state = (EncounterState*)&g_state; EncounterContext* context = (EncounterContext*)&g_ctx; ENCOUNTER_ZULRAH.init_context(context); ENCOUNTER_ZULRAH.init_state(state, context); + if (!g_collision_map) + g_collision_map = collision_map_load("ocean/osrs/data/zulrah.cmap"); + if (!g_collision_map) abort(); + ENCOUNTER_ZULRAH.put_ptr(state, context, "collision_map", g_collision_map); + ENCOUNTER_ZULRAH.put_int(state, context, "world_offset_x", 2256); + ENCOUNTER_ZULRAH.put_int(state, context, "world_offset_y", 3061); + ENCOUNTER_ZULRAH.put_int(state, context, "gear_tier", 0); ENCOUNTER_ZULRAH.put_int(state, context, "gear_tier_mode", ZUL_GEAR_TIER_FIXED); ENCOUNTER_ZULRAH.put_int(state, context, "episode_mode", ZUL_EPISODE_SINGLE_KILL); @@ -335,6 +344,83 @@ static void test_melee_stare_reads_prayer_at_calculation(void) { } } +static void test_topology_geometry_parity(void) { + printf("\n--- Zulrah static topology matches the arena collision map ---\n"); + ZulrahState* s = fresh_state(0x7a110001u); + (void)s; + const EncounterArenaTopology* topology = g_ctx.route_topology; + CHECK("Zulrah topology covers the 28 by 28 local arena", + topology->origin_x == 0 && topology->origin_y == 0 && + topology->width == ZUL_ARENA_SIZE && + topology->height == ZUL_ARENA_SIZE); + CHECK("Zulrah pillar lines preserve the encounter's open LOS rule", + topology->static_los_mode == ENCOUNTER_ARENA_TOPOLOGY_LOS_OPEN); + + int open_tiles = 0; + int step_checks = 0; + for (int x = 0; x < ZUL_ARENA_SIZE; x++) { + for (int y = 0; y < ZUL_ARENA_SIZE; y++) { + int expected_walkable = collision_tile_walkable( + g_collision_map, 0, x + 2256, y + 3061); + CHECK("Zulrah player tile parity", + zul_topology_player_tile_allowed(&g_ctx, x, y) == + expected_walkable); + if (!expected_walkable) continue; + open_tiles++; + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) continue; + int expected_step = collision_traversable_step( + g_collision_map, 0, + x + 2256, y + 3061, dx, dy); + CHECK("Zulrah player step parity", + encounter_arena_topology_step_allowed( + topology, x, y, 1, dx, dy) == expected_step); + step_checks++; + } + } + } + } + ASSERT_INT_EQ("Zulrah collision map has 69 allowed player tiles", + open_tiles, 69); + ASSERT_INT_EQ("Zulrah checks every direction from every allowed tile", + step_checks, 69 * 8); + + int phase_targets = 0; + for (int rotation = 0; rotation < ZUL_NUM_ROTATIONS; rotation++) { + for (int phase_index = 0; + phase_index < ZUL_ROT_LENGTHS[rotation]; + phase_index++) { + const ZulRotationPhase* phase = + &ZUL_ROTATIONS[rotation][phase_index]; + int target_x = ZUL_POSITIONS[phase->position][0]; + int target_y = ZUL_POSITIONS[phase->position][1]; + CHECK("every Zulrah phase target footprint stays in topology", + encounter_arena_topology_contains( + topology, target_x, target_y) && + encounter_arena_topology_contains( + topology, + target_x + ZUL_NPC_SIZE - 1, + target_y + ZUL_NPC_SIZE - 1)); + CHECK("every Zulrah phase stand tile is allowed", + zul_topology_player_tile_allowed( + &g_ctx, + ZUL_STAND_COORDS[phase->stand][0], + ZUL_STAND_COORDS[phase->stand][1])); + if (phase->stall != ZUL_STAND_NONE) { + CHECK("every Zulrah phase stall tile is allowed", + zul_topology_player_tile_allowed( + &g_ctx, + ZUL_STAND_COORDS[phase->stall][0], + ZUL_STAND_COORDS[phase->stall][1])); + } + phase_targets++; + } + } + ASSERT_INT_EQ("all 47 Zulrah phase targets are pinned", + phase_targets, 47); +} + int main(void) { printf("zulrah hit-delay and roll-order regressions\n\n"); @@ -343,6 +429,7 @@ int main(void) { test_damage_drawn_before_accuracy(); test_prayer_does_not_perturb_the_rng_stream(); test_prayer_resolves_at_throw_not_landing(); + test_topology_geometry_parity(); test_melee_stare_reads_prayer_at_calculation(); return osrs_test_summary(); From 3c37e3de691388bae3ed86d551c0a3dd59c5015b Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 12:18:28 +0300 Subject: [PATCH 25/50] Unify OSRS item metadata --- .../encounter_colosseum_mask_render.inc | 6 +- .../colosseum/encounter_colosseum_model.inc | 19 +- .../encounter_colosseum_modifiers.inc | 20 +- .../encounter_colosseum_obs_mask.inc | 28 +- .../encounter_colosseum_player_actions.inc | 5 +- .../encounter_colosseum_render_snapshot.inc | 5 +- .../encounter_colosseum_reset_spawn.inc | 23 +- .../encounter_colosseum_reward_step.inc | 2 +- ocean/osrs/encounters/encounter_zulrah.h | 19 +- .../inferno/encounter_inferno_model.inc | 7 +- .../inferno/encounter_inferno_obs_mask.inc | 15 +- .../encounter_inferno_player_actions.inc | 24 +- .../encounter_inferno_render_snapshot.inc | 16 +- .../inferno/encounter_inferno_reset_spawn.inc | 19 +- ocean/osrs/osrs_gui.h | 8 +- ocean/osrs/osrs_inventory.h | 229 +++++- ocean/osrs/osrs_inventory_actions.h | 28 +- ocean/osrs/osrs_inventory_clicks.h | 736 ++++++------------ ocean/osrs/osrs_item_obs_generated.h | 261 +++++++ ocean/osrs/osrs_item_obs_table.inc | 206 +---- ocean/osrs/osrs_items_generated.h | 6 + ocean/osrs/osrs_render.h | 6 +- ocean/osrs/tests/probe_colo_best_gear_dpt.c | 14 +- ocean/osrs/tests/probe_colo_wave_advance.c | 5 +- .../osrs/tests/probe_colosseum_winnability.c | 5 +- ocean/osrs/tests/test_colosseum_modifiers.c | 233 ++++-- ocean/osrs/tests/test_inferno_attack_styles.c | 26 +- .../osrs/tests/test_osrs_inventory_actions.c | 35 +- ocean/osrs/tests/test_osrs_inventory_clicks.c | 175 +++-- .../osrs/tests/test_osrs_item_effect_masks.c | 46 +- ocean/osrs/tools/gen_osrs_item_obs_table.c | 321 ++++++-- 31 files changed, 1464 insertions(+), 1084 deletions(-) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc index f9e1f1a351..81e4576357 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc @@ -37,11 +37,13 @@ static void col_write_mask_ctx( osrs_inventory_cell_click_classify(&s->inventory_cells[cell]); if (r.click_action == OSRS_CLICK_EQUIP) { if (col_can_equip_from_cell(s, cell)) - cell_equip_slot[cell] = osrs_item_gear_slot(s->inventory_cells[cell].item_idx); + cell_equip_slot[cell] = osrs_inventory_cell_metadata( + &s->inventory_cells[cell])->gear_slot; } else if (r.click_action == OSRS_CLICK_EAT) { cell_can_eat[cell] = col_can_eat_consumable_kind(s, r.consumable_kind); } else if (r.click_action == OSRS_CLICK_DRINK) { - cell_can_drink[cell] = s->inventory_cells[cell].dose > 0 && + cell_can_drink[cell] = osrs_inventory_cell_dose_count( + &s->inventory_cells[cell]) > 0 && s->player.potion_timer == 0 && col_drink_has_effect(s, r.consumable_kind); } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index c01a21057f..9633c5304a 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -1135,13 +1135,7 @@ static void col_for_each_reachable_consumable_dose_raw_osrs_id( abort(); } - enum { - REGISTRY_COUNT = (int)( - sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / - sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0]) - ) - }; - int seen[REGISTRY_COUNT]; + int seen[OSRS_ITEM_CONTENT_COUNT]; int seen_count = 0; for (int p = 0; p < COLO_NUM_LOADOUT_PROFILES; p++) { @@ -1156,7 +1150,7 @@ static void col_for_each_reachable_consumable_dose_raw_osrs_id( uint16_t current_raw_osrs_id = raw_osrs_id; for (int step = 0; current_raw_osrs_id != 0; step++) { - if (step >= REGISTRY_COUNT) { + if (step >= OSRS_ITEM_CONTENT_COUNT) { fprintf(stderr, "colosseum consumable dose chain cycles at raw id %u\n", current_raw_osrs_id); abort(); @@ -1174,7 +1168,7 @@ static void col_for_each_reachable_consumable_dose_raw_osrs_id( current_raw_osrs_id, seen, &seen_count, - REGISTRY_COUNT, + OSRS_ITEM_CONTENT_COUNT, visit, ctx); current_raw_osrs_id = @@ -1197,8 +1191,7 @@ static void col_for_each_display_inventory_sprite_raw_osrs_id( DISPLAY_CAPACITY = (int)(sizeof(COLO_NON_CONSUMABLE_DISPLAY_RAW_OSRS_IDS) / sizeof(COLO_NON_CONSUMABLE_DISPLAY_RAW_OSRS_IDS[0])) + - (int)(sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / - sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0])) + OSRS_ITEM_CONTENT_COUNT }; int seen[DISPLAY_CAPACITY]; int seen_count = 0; @@ -1559,7 +1552,7 @@ typedef struct { ColosseumLog log; ColoObsMemoCaches obs_memos; } ColosseumState; -static_assert(sizeof(ColosseumState) == 29832, "ColosseumState serialized layout"); +static_assert(sizeof(ColosseumState) == 29720, "ColosseumState serialized layout"); typedef enum { COLO_DMG_OFFPRAY = 0, @@ -1575,5 +1568,5 @@ static void col_build_live_inventory_display( int out[COLO_INVENTORY_DISPLAY_SLOTS] ) { for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) - out[i] = s->inventory_cells[i].raw_osrs_id; + out[i] = osrs_inventory_cell_raw_osrs_id(&s->inventory_cells[i]); } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc index cdc3988783..a56cb5c2c6 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc @@ -160,20 +160,26 @@ static void col_late_start_drain_supplies( int done[COLO_INVENTORY_DISPLAY_SLOTS] = {0}; for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) { - if (done[c] || s->inventory_cells[c].dose == 0) continue; - int item = s->inventory_cells[c].item_idx; + if (done[c] || + osrs_inventory_cell_dose_count( + &s->inventory_cells[c]) == 0) continue; + int item = osrs_inventory_cell_item_index(&s->inventory_cells[c]); int total = 0; for (int k = c; k < COLO_INVENTORY_DISPLAY_SLOTS; k++) { - if (s->inventory_cells[k].item_idx == item && - s->inventory_cells[k].dose > 0) { + if (osrs_inventory_cell_item_index(&s->inventory_cells[k]) == item && + osrs_inventory_cell_dose_count( + &s->inventory_cells[k]) > 0) { done[k] = 1; - total += s->inventory_cells[k].dose; + total += osrs_inventory_cell_dose_count( + &s->inventory_cells[k]); } } int drain = total - (int)((float)total * f + 0.5f); for (int k = COLO_INVENTORY_DISPLAY_SLOTS - 1; k >= c && drain > 0; k--) { - if (s->inventory_cells[k].item_idx != item) continue; - while (s->inventory_cells[k].dose > 0 && drain > 0) { + if (osrs_inventory_cell_item_index( + &s->inventory_cells[k]) != item) continue; + while (osrs_inventory_cell_dose_count( + &s->inventory_cells[k]) > 0 && drain > 0) { OsrsInventoryClickResolution res = osrs_inventory_cell_click_interpret( &s->inventory_cells[k], OSRS_CLICK_TICK_FIRST); if (res.click_action != OSRS_CLICK_DRINK) break; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc index 3fa9970cb4..ab0ac3adaa 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -11,7 +11,7 @@ static_assert(COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE == COLO_INVENTORY_OBS_CACHE_FLOATS, "inventory obs cache float count must match the inventory+equipped block size"); -static_assert(OSRS_ITEM_OBS_TABLE_ROWS == OSRS_INVENTORY_CELL_OBS_CODE_COUNT, +static_assert(OSRS_ITEM_OBS_TABLE_ROWS == OSRS_ITEM_CONTENT_COUNT, "regenerate osrs_item_obs_table.inc: the cell code space changed"); static_assert(OSRS_ITEM_OBS_TABLE_COLS == COLO_INVENTORY_CELL_ENCODER_FEATURES, "regenerate osrs_item_obs_table.inc: the expanded record width changed"); @@ -24,8 +24,8 @@ static_assert(OSRS_ITEM_OBS_TABLE_BASE_RANGED == MAXED_BASE_RANGED, static_assert(OSRS_ITEM_OBS_OVERLAY_EQUIPPED == OSRS_INVENTORY_CELL_COMPACT_EQUIPPED && OSRS_ITEM_OBS_OVERLAY_HP_HEAL == OSRS_INVENTORY_CELL_COMPACT_HP_HEAL, "the gather overwrites the dynamic fields at these expanded slots"); -static_assert(OSRS_ITEM_OBS_CODE_SCALE == OSRS_INVENTORY_CELL_OBS_CODE_SCALE, - "the gather multiplies the observed code back by this scale before indexing a row"); +static_assert(OSRS_ITEM_CONTENT_COUNT <= OSRS_ITEM_OBS_CODE_SCALE, + "every observed content code must remain exact after scaling"); #define COLO_NPC_CORE_FEATURES 11 #define COLO_NPC_TELLS_FEATURES 10 #define COLO_NPC_VENATOR_FEATURES 1 @@ -1124,9 +1124,11 @@ static void col_build_gear_candidates(const ColosseumState* s, ColoGearCandidate for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) col_gear_candidates_add(c, slot, s->player.equipped[slot]); for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { - uint8_t item = s->inventory_cells[cell].item_idx; + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&s->inventory_cells[cell]); + uint8_t item = metadata->item_idx; if (item == ITEM_NONE) continue; - int gslot = osrs_item_gear_slot(item); + int gslot = metadata->gear_slot; if (gslot < 0) continue; col_gear_candidates_add(c, gslot, item); } @@ -1493,10 +1495,7 @@ static uint64_t col_inventory_obs_signature(const ColosseumState* s) { * at a 34.9% hit rate. What remains depends on cell contents and base stats only. */ uint64_t h = 1469598103934665603ULL; for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { - const ColoInvCell* c = &s->inventory_cells[cell]; - uint64_t packed = (uint64_t)c->item_idx - | ((uint64_t)c->raw_osrs_id << 8) - | ((uint64_t)c->dose << 24); + uint64_t packed = s->inventory_cells[cell].content_code; h = (h ^ packed) * 1099511628211ULL; } const int scalars[3] = { @@ -1569,13 +1568,10 @@ static void col_write_inventory_and_equipped_obs( const ColoInvCell* cell = &s->inventory_cells[cell_idx]; osrs_write_inventory_cell_obs_code_features( &obs[i], - cell->item_idx, - cell->raw_osrs_id, - cell->dose, - osrs_inventory_cell_holds_equipped_item(&s->player, s->inventory_cells, cell_idx), - s->player.base_hitpoints, - s->player.base_prayer, - s->player.base_ranged); + cell, + osrs_inventory_cell_holds_equipped_item( + &s->player, s->inventory_cells, cell_idx), + s->player.base_hitpoints); i += COLO_INVENTORY_CELL_OBS_FEATURES; } assert(i == COLO_OBS_AFTER_INVENTORY); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc index 81c4b12f62..156e8d3a11 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -271,7 +271,8 @@ static int col_inventory_cell_actionable(const ColosseumState* s, int cell_idx) osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); if (r.click_action == OSRS_CLICK_EQUIP) return col_can_equip_from_cell(s, cell_idx); if (r.click_action == OSRS_CLICK_DRINK) - return cell->dose > 0 && s->player.potion_timer == 0 && + return osrs_inventory_cell_dose_count(cell) > 0 && + s->player.potion_timer == 0 && col_drink_has_effect(s, r.consumable_kind); if (r.click_action == OSRS_CLICK_EAT) return col_can_eat_consumable_kind(s, r.consumable_kind); @@ -391,6 +392,8 @@ static void col_apply_drink_one_dose_effect(void* ctx, OsrsConsumableKind kind) case OSRS_CONSUMABLE_STAMINA: fprintf(stderr, "colosseum: consumable kind %d not in colosseum kits\n", (int)kind); abort(); + case OSRS_CONSUMABLE_COUNT: + break; } abort(); } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 72343808ea..29a50cc9ab 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -906,7 +906,8 @@ static int col_queue_inventory_click_action( osrs_inventory_cell_click_interpret(&s->inventory_cells[inventory_slot], OSRS_CLICK_TICK_FIRST); int action_head; if (r.click_action == OSRS_CLICK_EQUIP) { - int gear_slot = osrs_item_gear_slot(s->inventory_cells[inventory_slot].item_idx); + int gear_slot = osrs_inventory_cell_metadata( + &s->inventory_cells[inventory_slot])->gear_slot; if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) return -1; action_head = COLO_HEAD_EQUIP_SLOT(gear_slot); } else if (r.click_action == OSRS_CLICK_EAT) { @@ -1037,7 +1038,7 @@ static void col_step_human_commands_ctx( } #define COLO_SNAPSHOT_MAGIC 0xC010C001u -#define COLO_SNAPSHOT_VERSION 23u +#define COLO_SNAPSHOT_VERSION 24u typedef struct { uint32_t magic; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc index 412aca5aa7..8bc505bddb 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -533,13 +533,6 @@ static void col_seed_inventory_cells_from_gameplay_loadout(ColosseumState* s) { if (raw_id == 0) continue; s->inventory_cells[i] = osrs_inventory_cell_from_raw_osrs_id(raw_id); - if (s->inventory_cells[i].item_idx == ITEM_NONE && - s->inventory_cells[i].dose == 0 && - raw_id != 27281) { - fprintf(stderr, "colosseum: gameplay inventory raw id %u has no cell mapping\n", - raw_id); - abort(); - } } } @@ -552,32 +545,33 @@ static void col_sync_consumable_counters_from_inventory(ColosseumState* s) { int antivenom_doses = 0; for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { const ColoInvCell* cell = &s->inventory_cells[i]; - if (cell->dose == 0) continue; + uint8_t dose_count = osrs_inventory_cell_dose_count(cell); + if (dose_count == 0) continue; OsrsInventoryClickResolution resolution = osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); if (resolution.click_action != OSRS_CLICK_DRINK) continue; switch (resolution.consumable_kind) { case OSRS_CONSUMABLE_BREW: - brew_doses += cell->dose; + brew_doses += dose_count; break; case OSRS_CONSUMABLE_SUPER_RESTORE: case OSRS_CONSUMABLE_SANFEW: case OSRS_CONSUMABLE_PRAYER_RESTORE: - restore_doses += cell->dose; + restore_doses += dose_count; break; case OSRS_CONSUMABLE_SUPER_COMBAT: case OSRS_CONSUMABLE_DIVINE_COMBAT: - combat_doses += cell->dose; + combat_doses += dose_count; break; case OSRS_CONSUMABLE_RANGING: case OSRS_CONSUMABLE_DIVINE_RANGING: - ranged_doses += cell->dose; + ranged_doses += dose_count; break; case OSRS_CONSUMABLE_SURGE: - surge_doses += cell->dose; + surge_doses += dose_count; break; case OSRS_CONSUMABLE_ANTIVENOM_PLUS: - antivenom_doses += cell->dose; + antivenom_doses += dose_count; break; case OSRS_CONSUMABLE_GUTHIX_REST: case OSRS_CONSUMABLE_SATURATED_HEART: @@ -587,6 +581,7 @@ static void col_sync_consumable_counters_from_inventory(ColosseumState* s) { break; case OSRS_CONSUMABLE_BASTION: case OSRS_CONSUMABLE_STAMINA: + case OSRS_CONSUMABLE_COUNT: fprintf(stderr, "colosseum: consumable kind %d not in colosseum kits\n", (int)resolution.consumable_kind); abort(); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc index f5780cf5d2..b54bccff88 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc @@ -208,7 +208,7 @@ static float col_inventory_heal_remaining(const ColosseumState* s) { float total = 0.0f; for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) { const OsrsInventoryCell* cell = &s->inventory_cells[i]; - if (cell->raw_osrs_id == 0) continue; + if (osrs_inventory_cell_is_empty(cell)) continue; OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); int per_dose = osrs_consumable_hp_heal_amount( diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h index 13db509099..694ca41f30 100644 --- a/ocean/osrs/encounters/encounter_zulrah.h +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -529,7 +529,7 @@ typedef struct { Log log; } ZulrahState; -static_assert(sizeof(ZulrahState) == 12712, "ZulrahState serialized layout"); +static_assert(sizeof(ZulrahState) == 12600, "ZulrahState serialized layout"); static void zul_set_npc_anim_event(ZulrahState* s, int anim_id, int duration_ticks) { osrs_npc_primary_anim_event_set( @@ -2113,10 +2113,9 @@ static void zul_write_obs(EncounterState* state, EncounterContext* context, floa for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { osrs_write_inventory_cell_affordance_features( &obs[i], - s->inventory_cells[cell].item_idx, - s->inventory_cells[cell].raw_osrs_id, - s->inventory_cells[cell].dose, - osrs_inventory_cell_holds_equipped_item(&s->player, s->inventory_cells, cell), + &s->inventory_cells[cell], + osrs_inventory_cell_holds_equipped_item( + &s->player, s->inventory_cells, cell), ZUL_ZERO_POST_USE_DELTAS, s->player.base_hitpoints, s->player.base_prayer, @@ -2178,12 +2177,15 @@ static void zul_write_mask(EncounterState* state, EncounterContext* context, flo if (r.click_action == OSRS_CLICK_EQUIP) { if (osrs_can_equip_from_cell(&s->player, s->inventory_cells, cell)) cell_equip_slot[cell] = - osrs_item_gear_slot(s->inventory_cells[cell].item_idx); + osrs_inventory_cell_metadata( + &s->inventory_cells[cell])->gear_slot; } else if (r.click_action == OSRS_CLICK_EAT) { cell_can_eat[cell] = osrs_can_eat_consumable_kind(&s->player, r.consumable_kind); } else if (r.click_action == OSRS_CLICK_DRINK) { - cell_can_drink[cell] = s->inventory_cells[cell].dose > 0 && + cell_can_drink[cell] = + osrs_inventory_cell_dose_count( + &s->inventory_cells[cell]) > 0 && s->player.potion_timer == 0 && zul_drink_has_effect(s, r.consumable_kind); } @@ -2749,7 +2751,8 @@ static int zul_first_cell_with_kind( static int zul_cell_with_item(const ZulrahState* s, uint8_t item_idx) { for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) - if (s->inventory_cells[cell].item_idx == item_idx) return cell; + if (osrs_inventory_cell_item_index( + &s->inventory_cells[cell]) == item_idx) return cell; return -1; } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc index ff08d5d55f..0863129a56 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc @@ -756,13 +756,12 @@ typedef struct { int blood_heal; } InfTickScratch; typedef struct { - uint8_t item_idx; - uint16_t raw_osrs_id; + uint16_t content_code; uint8_t click_action; uint8_t consumable_kind; } InfInventoryClickMemo; -static_assert(sizeof(InfInventoryClickMemo) == 6, "Inferno inventory click memo size"); +static_assert(sizeof(InfInventoryClickMemo) == 4, "Inferno inventory click memo size"); typedef struct { Player player; @@ -920,4 +919,4 @@ typedef struct { float total_hp_restored_jad; float total_hp_restored_zuk; } InfernoState; -static_assert(sizeof(InfernoState) == 27300, "InfernoState serialized layout"); +static_assert(sizeof(InfernoState) == 27132, "InfernoState serialized layout"); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc index 498ca1714e..e4deefa711 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -390,8 +390,8 @@ static void inf_write_obs_ctx( for (int cell_idx = 0; cell_idx < OSRS_INVENTORY_SIZE; cell_idx++) { const OsrsInventoryCell* cell = &s->inventory_cells[cell_idx]; - obs[i++] = osrs_inventory_cell_obs_code_encode( - osrs_inventory_cell_obs_code(cell->item_idx, cell->raw_osrs_id)); + obs[i++] = + osrs_inventory_cell_obs_code_encode(cell->content_code); } assert(i == INF_OBS_AFTER_INVENTORY); @@ -469,13 +469,11 @@ static inline InfInventoryClickMemo* inf_inventory_click_memo( ) { const OsrsInventoryCell* cell = &s->inventory_cells[cell_idx]; InfInventoryClickMemo* memo = &s->inventory_click_memos[cell_idx]; - if (memo->item_idx != cell->item_idx || - memo->raw_osrs_id != cell->raw_osrs_id) { + if (memo->content_code != cell->content_code) { OsrsInventoryClickResolution resolution = osrs_inventory_cell_click_classify(cell); *memo = (InfInventoryClickMemo){ - .item_idx = cell->item_idx, - .raw_osrs_id = cell->raw_osrs_id, + .content_code = cell->content_code, .click_action = (uint8_t)resolution.click_action, .consumable_kind = (uint8_t)resolution.consumable_kind, }; @@ -553,7 +551,8 @@ static void inf_write_mask_ctx( case OSRS_CLICK_EQUIP: { if (!osrs_can_equip_from_cell(&s->player, s->inventory_cells, c)) break; - int gear_slot = osrs_item_gear_slot(cell->item_idx); + int gear_slot = + osrs_inventory_cell_metadata(cell)->gear_slot; if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) { fprintf(stderr, "inferno: equippable inventory cell %d has invalid gear slot %d\n", @@ -572,7 +571,7 @@ static void inf_write_mask_ctx( } break; case OSRS_CLICK_DRINK: - if (cell->dose > 0 && + if (osrs_inventory_cell_dose_count(cell) > 0 && s->player.potion_timer == 0 && inf_drink_has_effect( s, (OsrsConsumableKind)memo->consumable_kind)) { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc index 565ae2ff5d..1027e3a793 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -243,14 +243,22 @@ static void inf_sync_consumable_counters_from_cells(InfernoState* s) { p->stamina_doses = 0; for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) { const OsrsInventoryCell* cell = &s->inventory_cells[i]; - if (cell->raw_osrs_id == 0) continue; - OsrsConsumableClick click = - osrs_consumable_click_lookup_raw_osrs_id(cell->raw_osrs_id); - switch (click.consumable_kind) { - case OSRS_CONSUMABLE_BREW: p->brew_doses += cell->dose; break; - case OSRS_CONSUMABLE_SUPER_RESTORE: p->restore_doses += cell->dose; break; - case OSRS_CONSUMABLE_BASTION: p->bastion_doses += cell->dose; break; - case OSRS_CONSUMABLE_STAMINA: p->stamina_doses += cell->dose; break; + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(cell); + if (metadata->raw_osrs_id == 0) continue; + switch ((OsrsConsumableKind)metadata->consumable_kind) { + case OSRS_CONSUMABLE_BREW: + p->brew_doses += metadata->dose_count; + break; + case OSRS_CONSUMABLE_SUPER_RESTORE: + p->restore_doses += metadata->dose_count; + break; + case OSRS_CONSUMABLE_BASTION: + p->bastion_doses += metadata->dose_count; + break; + case OSRS_CONSUMABLE_STAMINA: + p->stamina_doses += metadata->dose_count; + break; default: break; } } diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index d65700c622..e58af730f8 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -574,11 +574,12 @@ static void* inf_get_player_for_input(void* state, int idx) { static int inf_first_cell_with_kind(const InfernoState* s, OsrsConsumableKind kind) { for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { const OsrsInventoryCell* cell = &s->inventory_cells[c]; - if (cell->raw_osrs_id == 0) continue; - OsrsConsumableClick click = - osrs_consumable_click_lookup_raw_osrs_id(cell->raw_osrs_id); - if (click.consumable_kind != kind) continue; - if (click.click_action == OSRS_CLICK_DRINK && cell->dose == 0) continue; + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(cell); + if (metadata->raw_osrs_id == 0) continue; + if ((OsrsConsumableKind)metadata->consumable_kind != kind) continue; + if (metadata->click_action == OSRS_CLICK_DRINK && + metadata->dose_count == 0) continue; return c; } return -1; @@ -597,7 +598,8 @@ static void inf_translate_gear_swap(const InfernoState* s, int* actions, InfWeap uint8_t item = want_loadout[slot]; if (item == ITEM_NONE || s->player.equipped[slot] == item) continue; for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - if (s->inventory_cells[c].item_idx == item) { + if (osrs_inventory_cell_item_index( + &s->inventory_cells[c]) == item) { actions[INF_HEAD_EQUIP_SLOT(slot)] = c + 1; break; } @@ -764,7 +766,7 @@ static void inf_step_human_commands(EncounterState* state, HumanInput* hi) { } #define INF_SNAPSHOT_MAGIC 0x1FE00001u -#define INF_SNAPSHOT_VERSION 21u +#define INF_SNAPSHOT_VERSION 22u typedef struct { uint32_t magic; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc index acb37f22b5..ab40e0dd7d 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -11,17 +11,6 @@ static InfSupplyDoses inf_full_starting_supplies(void) { }; } -static uint16_t inf_potion_raw_id_for_dose(OsrsConsumableKind kind, int dose) { - int n = (int)(sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / - sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0])); - for (int i = 0; i < n; i++) { - const OsrsConsumableClick* row = &OSRS_CONSUMABLE_CLICK_REGISTRY[i]; - if (row->consumable_kind == kind && row->dose_count == dose) - return row->raw_osrs_id; - } - fprintf(stderr, "inferno: no registry row for kind %d dose %d\n", (int)kind, dose); - abort(); -} static void inf_seed_potion_cells( InfernoState* s, @@ -36,8 +25,9 @@ static void inf_seed_potion_cells( OSRS_INVENTORY_SIZE); abort(); } - s->inventory_cells[(*next_cell)++] = osrs_inventory_cell_from_raw_osrs_id( - inf_potion_raw_id_for_dose(kind, dose)); + s->inventory_cells[(*next_cell)++] = + osrs_inventory_cell_from_content_code( + osrs_inventory_content_code_from_consumable(kind, dose)); doses -= dose; } } @@ -56,7 +46,8 @@ static void inf_seed_inventory_cells(InfernoState* s) { if (s->player.equipped[slot] == item) continue; int duplicate = 0; for (int c = 0; c < cell; c++) { - if (s->inventory_cells[c].item_idx == item) { + if (osrs_inventory_cell_item_index( + &s->inventory_cells[c]) == item) { duplicate = 1; break; } diff --git a/ocean/osrs/osrs_gui.h b/ocean/osrs/osrs_gui.h index c8e5f7a9ea..82d6730278 100644 --- a/ocean/osrs/osrs_gui.h +++ b/ocean/osrs/osrs_gui.h @@ -2065,8 +2065,12 @@ static const char* gui_inv_primary_action_label(const InvSlot* inv) { uint8_t item_idx = inv->type == INV_SLOT_EQUIPMENT ? inv->item_db_idx : ITEM_NONE; - OsrsInventoryClickResolution resolution = osrs_inventory_click_interpret( - item_idx, raw_osrs_id, OSRS_CLICK_TICK_FIRST); + OsrsInventoryCell cell = item_idx == ITEM_NONE + ? osrs_inventory_cell_from_raw_osrs_id(raw_osrs_id) + : osrs_inventory_cell_from_item(item_idx); + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret( + &cell, OSRS_CLICK_TICK_FIRST); switch (resolution.click_action) { case OSRS_CLICK_EQUIP: { int gear_slot = item_idx != ITEM_NONE ? item_to_gear_slot(item_idx) : -1; diff --git a/ocean/osrs/osrs_inventory.h b/ocean/osrs/osrs_inventory.h index f31aaefc05..758a106922 100644 --- a/ocean/osrs/osrs_inventory.h +++ b/ocean/osrs/osrs_inventory.h @@ -10,45 +10,214 @@ #define OSRS_INVENTORY_SIZE 28 +typedef enum { + OSRS_CLICK_NONE = 0, + OSRS_CLICK_EQUIP = 1, + OSRS_CLICK_EAT = 2, + OSRS_CLICK_DRINK = 3, +} OsrsClickAction; + +typedef enum { + OSRS_CONSUMABLE_NONE = 0, + OSRS_CONSUMABLE_BREW = 1, + OSRS_CONSUMABLE_SUPER_RESTORE = 2, + OSRS_CONSUMABLE_SANFEW = 3, + OSRS_CONSUMABLE_SUPER_COMBAT = 4, + OSRS_CONSUMABLE_DIVINE_COMBAT = 5, + OSRS_CONSUMABLE_RANGING = 6, + OSRS_CONSUMABLE_DIVINE_RANGING = 7, + OSRS_CONSUMABLE_SURGE = 8, + OSRS_CONSUMABLE_GUTHIX_REST = 9, + OSRS_CONSUMABLE_SATURATED_HEART = 10, + OSRS_CONSUMABLE_ANTIVENOM_PLUS = 11, + OSRS_CONSUMABLE_SHARK_FOOD = 12, + OSRS_CONSUMABLE_KARAMBWAN = 13, + OSRS_CONSUMABLE_PRAYER_RESTORE = 14, + OSRS_CONSUMABLE_BASTION = 15, + OSRS_CONSUMABLE_STAMINA = 16, + OSRS_CONSUMABLE_COUNT = 17, +} OsrsConsumableKind; + +#include "osrs_item_obs_generated.h" + typedef struct { - uint8_t item_idx; - uint8_t binary_padding0; + const Item* item; uint16_t raw_osrs_id; - uint8_t dose; - uint8_t binary_padding1; + uint16_t next_content_code; + uint8_t item_idx; + int8_t gear_slot; + uint8_t click_action; + uint8_t consumable_kind; + uint8_t dose_count; + uint8_t attack_style; + float observation_row[OSRS_ITEM_OBS_TABLE_COLS]; +} OsrsItemContentMetadata; + +#define OSRS_ITEM_CONTENT_METADATA_ROW( \ + CODE, ITEM_POINTER, ITEM_IDX, RAW_OSRS_ID, GEAR_SLOT, CLICK_ACTION, \ + CONSUMABLE_KIND, DOSE_COUNT, NEXT_CONTENT_CODE, ATTACK_STYLE, ...) \ + [CODE] = { \ + .item = ITEM_POINTER, \ + .raw_osrs_id = RAW_OSRS_ID, \ + .next_content_code = NEXT_CONTENT_CODE, \ + .item_idx = ITEM_IDX, \ + .gear_slot = GEAR_SLOT, \ + .click_action = CLICK_ACTION, \ + .consumable_kind = CONSUMABLE_KIND, \ + .dose_count = DOSE_COUNT, \ + .attack_style = ATTACK_STYLE, \ + .observation_row = {__VA_ARGS__}, \ + }, +static const OsrsItemContentMetadata + OSRS_ITEM_CONTENT_METADATA[OSRS_ITEM_CONTENT_COUNT] = { + OSRS_ITEM_CONTENT_ROWS(OSRS_ITEM_CONTENT_METADATA_ROW) +}; +#undef OSRS_ITEM_CONTENT_METADATA_ROW + +#define OSRS_RAW_CONTENT_CODE_ROW( \ + CODE, ITEM_POINTER, ITEM_IDX, RAW_OSRS_ID, GEAR_SLOT, CLICK_ACTION, \ + CONSUMABLE_KIND, DOSE_COUNT, NEXT_CONTENT_CODE, ATTACK_STYLE, ...) \ + [RAW_OSRS_ID] = (uint16_t)((CODE) + 1), +static const uint16_t OSRS_CONTENT_CODE_BY_RAW_OSRS_ID[UINT16_MAX + 1] = { + OSRS_ITEM_CONTENT_ROWS(OSRS_RAW_CONTENT_CODE_ROW) +}; +#undef OSRS_RAW_CONTENT_CODE_ROW + +#define OSRS_CONSUMABLE_CONTENT_CODE_ROW(KIND, DOSE, CODE) \ + [KIND][DOSE] = (uint16_t)((CODE) + 1), +static const uint16_t + OSRS_CONTENT_CODE_BY_CONSUMABLE_KIND_AND_DOSE[OSRS_CONSUMABLE_COUNT][5] = { + OSRS_CONSUMABLE_CONTENT_ROWS(OSRS_CONSUMABLE_CONTENT_CODE_ROW) +}; +#undef OSRS_CONSUMABLE_CONTENT_CODE_ROW + +typedef struct { + uint16_t content_code; } OsrsInventoryCell; typedef struct { OsrsInventoryCell cells[OSRS_INVENTORY_SIZE]; } OsrsInventorySlotSnapshot; +_Static_assert( + sizeof(OSRS_ITEM_CONTENT_METADATA) / + sizeof(OSRS_ITEM_CONTENT_METADATA[0]) == OSRS_ITEM_CONTENT_COUNT, + "generated item metadata row count must match its content-code domain"); +_Static_assert(OSRS_ITEM_CONTENT_COUNT <= UINT16_MAX, + "item content code must fit uint16_t"); +_Static_assert(sizeof(OsrsInventoryCell) == sizeof(uint16_t), + "inventory cells carry only canonical content identity"); + +static inline const OsrsItemContentMetadata* osrs_item_content_metadata( + uint16_t content_code +) { + if (content_code >= OSRS_ITEM_CONTENT_COUNT) { + fprintf(stderr, "inventory content: invalid content code %u\n", content_code); + abort(); + } + return &OSRS_ITEM_CONTENT_METADATA[content_code]; +} + +static inline uint16_t osrs_inventory_content_code_from_item(uint8_t item_idx) { + if (item_idx >= NUM_ITEMS) { + fprintf(stderr, "inventory content: invalid item index %u\n", item_idx); + abort(); + } + return (uint16_t)(1 + item_idx); +} + +static inline uint16_t osrs_inventory_content_code_from_raw_osrs_id( + uint16_t raw_osrs_id +) { + uint16_t encoded = OSRS_CONTENT_CODE_BY_RAW_OSRS_ID[raw_osrs_id]; + if (encoded == 0) { + fprintf(stderr, "inventory content: unrepresentable raw OSRS id %u\n", + raw_osrs_id); + abort(); + } + return (uint16_t)(encoded - 1); +} + +static inline uint16_t osrs_inventory_content_code_from_consumable( + OsrsConsumableKind kind, + uint8_t dose_count +) { + if (kind <= OSRS_CONSUMABLE_NONE || kind >= OSRS_CONSUMABLE_COUNT || + dose_count > 4) { + fprintf(stderr, "inventory content: invalid consumable kind %d dose %u\n", + (int)kind, dose_count); + abort(); + } + uint16_t encoded = + OSRS_CONTENT_CODE_BY_CONSUMABLE_KIND_AND_DOSE[kind][dose_count]; + if (encoded == 0) { + fprintf(stderr, "inventory content: unrepresentable consumable kind %d dose %u\n", + (int)kind, dose_count); + abort(); + } + return (uint16_t)(encoded - 1); +} + static inline int osrs_inventory_slot_valid(int slot) { return slot >= 0 && slot < OSRS_INVENTORY_SIZE; } +static inline OsrsInventoryCell osrs_inventory_cell_from_content_code( + uint16_t content_code +) { + (void)osrs_item_content_metadata(content_code); + return (OsrsInventoryCell){.content_code = content_code}; +} + static inline OsrsInventoryCell osrs_inventory_cell_empty(void) { - return (OsrsInventoryCell){ - .item_idx = ITEM_NONE, - .raw_osrs_id = 0, - .dose = 0, - }; + return osrs_inventory_cell_from_content_code(0); } static inline int osrs_inventory_cell_is_empty(const OsrsInventoryCell* cell) { - return cell->item_idx == ITEM_NONE && cell->raw_osrs_id == 0; + return cell->content_code == 0; } static inline OsrsInventoryCell osrs_inventory_cell_from_item(uint8_t item_idx) { if (item_idx == ITEM_NONE) return osrs_inventory_cell_empty(); - if (item_idx >= NUM_ITEMS) { - fprintf(stderr, "inventory cell: invalid item index %u\n", item_idx); - abort(); - } - return (OsrsInventoryCell){ - .item_idx = item_idx, - .raw_osrs_id = ITEM_DATABASE[item_idx].item_id, - .dose = 0, - }; + return osrs_inventory_cell_from_content_code( + osrs_inventory_content_code_from_item(item_idx)); +} + +static inline OsrsInventoryCell osrs_inventory_cell_from_raw_osrs_id( + uint16_t raw_osrs_id +) { + return osrs_inventory_cell_from_content_code( + osrs_inventory_content_code_from_raw_osrs_id(raw_osrs_id)); +} + +static inline const OsrsItemContentMetadata* osrs_inventory_cell_metadata( + const OsrsInventoryCell* cell +) { + return osrs_item_content_metadata(cell->content_code); +} + +static inline uint8_t osrs_inventory_cell_item_index( + const OsrsInventoryCell* cell +) { + return osrs_inventory_cell_metadata(cell)->item_idx; +} + +static inline uint16_t osrs_inventory_cell_raw_osrs_id( + const OsrsInventoryCell* cell +) { + return osrs_inventory_cell_metadata(cell)->raw_osrs_id; +} + +static inline uint8_t osrs_inventory_cell_dose_count( + const OsrsInventoryCell* cell +) { + return osrs_inventory_cell_metadata(cell)->dose_count; +} + +static inline int osrs_item_gear_slot(uint8_t item_idx) { + if (item_idx == ITEM_NONE) return -1; + return osrs_item_content_metadata( + osrs_inventory_content_code_from_item(item_idx))->gear_slot; } static inline void osrs_inventory_swap_cells( @@ -63,9 +232,9 @@ static inline void osrs_inventory_swap_cells( abort(); } if (source_slot == target_slot) return; - OsrsInventoryCell tmp = cells[target_slot]; + OsrsInventoryCell temporary = cells[target_slot]; cells[target_slot] = cells[source_slot]; - cells[source_slot] = tmp; + cells[source_slot] = temporary; } static inline OsrsInventorySlotSnapshot osrs_inventory_slot_snapshot( @@ -76,22 +245,4 @@ static inline OsrsInventorySlotSnapshot osrs_inventory_slot_snapshot( return snapshot; } -static inline int osrs_item_gear_slot(uint8_t item_idx) { - if (item_idx >= NUM_ITEMS) return -1; - switch (ITEM_DATABASE[item_idx].slot) { - case SLOT_HEAD: return GEAR_SLOT_HEAD; - case SLOT_CAPE: return GEAR_SLOT_CAPE; - case SLOT_NECK: return GEAR_SLOT_NECK; - case SLOT_WEAPON: return GEAR_SLOT_WEAPON; - case SLOT_BODY: return GEAR_SLOT_BODY; - case SLOT_SHIELD: return GEAR_SLOT_SHIELD; - case SLOT_LEGS: return GEAR_SLOT_LEGS; - case SLOT_HANDS: return GEAR_SLOT_HANDS; - case SLOT_FEET: return GEAR_SLOT_FEET; - case SLOT_RING: return GEAR_SLOT_RING; - case SLOT_AMMO: return GEAR_SLOT_AMMO; - default: return -1; - } -} - #endif diff --git a/ocean/osrs/osrs_inventory_actions.h b/ocean/osrs/osrs_inventory_actions.h index cdf3c0b01d..4b357cdb27 100644 --- a/ocean/osrs/osrs_inventory_actions.h +++ b/ocean/osrs/osrs_inventory_actions.h @@ -23,10 +23,11 @@ static inline int osrs_inventory_cell_holds_equipped_item( int cell_idx ) { const OsrsInventoryCell* cell = &cells[cell_idx]; - if (cell->item_idx == ITEM_NONE) return 0; - int slot = osrs_item_gear_slot(cell->item_idx); - if (slot < 0) return 0; - return p->equipped[slot] == cell->item_idx; + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(cell); + if (metadata->item_idx == ITEM_NONE) return 0; + if (metadata->gear_slot < 0) return 0; + return p->equipped[metadata->gear_slot] == metadata->item_idx; } static inline int osrs_can_equip_from_cell( @@ -35,11 +36,11 @@ static inline int osrs_can_equip_from_cell( int cell_idx ) { const OsrsInventoryCell* cell = &cells[cell_idx]; - if (cell->item_idx == ITEM_NONE) return 0; + uint8_t item_idx = osrs_inventory_cell_item_index(cell); + if (item_idx == ITEM_NONE) return 0; if (osrs_inventory_cell_holds_equipped_item(p, cells, cell_idx)) return 0; - uint8_t item_idx = cell->item_idx; - int gear_slot = osrs_item_gear_slot(item_idx); + int gear_slot = osrs_inventory_cell_metadata(cell)->gear_slot; if (gear_slot < 0) return 0; if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(item_idx) && p->equipped[GEAR_SLOT_SHIELD] != ITEM_NONE && @@ -62,8 +63,10 @@ static inline int osrs_equip_from_cell( if (!osrs_can_equip_from_cell(p, cells, cell_idx)) return -1; OsrsInventoryCell* cell = &cells[cell_idx]; - uint8_t item_idx = cell->item_idx; - int gear_slot = osrs_item_gear_slot(item_idx); + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(cell); + uint8_t item_idx = metadata->item_idx; + int gear_slot = metadata->gear_slot; if (gear_slot < 0) { fprintf(stderr, "inventory equip: item %u has no gear slot\n", item_idx); abort(); @@ -123,6 +126,8 @@ static inline int osrs_can_eat_consumable_kind( case OSRS_CONSUMABLE_STAMINA: case OSRS_CONSUMABLE_NONE: return 0; + case OSRS_CONSUMABLE_COUNT: + break; } abort(); } @@ -172,7 +177,8 @@ static inline OsrsInventoryTickIntent osrs_resolve_inventory_tick_intent( if (action <= 0 || action > OSRS_INVENTORY_SIZE) continue; int cell_idx = action - 1; if (!osrs_can_equip_from_cell(p, cells, cell_idx)) continue; - int gear_slot = osrs_item_gear_slot(cells[cell_idx].item_idx); + int gear_slot = osrs_inventory_cell_metadata( + &cells[cell_idx])->gear_slot; if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) { fprintf(stderr, "inventory equip intent: invalid gear slot %d\n", gear_slot); abort(); @@ -199,7 +205,7 @@ static inline OsrsInventoryTickIntent osrs_resolve_inventory_tick_intent( OsrsInventoryClickResolution resolution = osrs_inventory_cell_click_interpret(&cells[cell_idx], OSRS_CLICK_TICK_FIRST); if (resolution.click_action == OSRS_CLICK_DRINK && - cells[cell_idx].dose > 0 && + osrs_inventory_cell_dose_count(&cells[cell_idx]) > 0 && p->potion_timer == 0) { intent.drink_cell = cell_idx; intent.drink_order = NUM_GEAR_SLOTS + 1; diff --git a/ocean/osrs/osrs_inventory_clicks.h b/ocean/osrs/osrs_inventory_clicks.h index b9ca713858..cd6c14a09c 100644 --- a/ocean/osrs/osrs_inventory_clicks.h +++ b/ocean/osrs/osrs_inventory_clicks.h @@ -7,32 +7,6 @@ #include "osrs_consumables.h" #include "osrs_special_attacks.h" -typedef enum { - OSRS_CLICK_NONE = 0, - OSRS_CLICK_EQUIP = 1, - OSRS_CLICK_EAT = 2, - OSRS_CLICK_DRINK = 3, -} OsrsClickAction; - -typedef enum { - OSRS_CONSUMABLE_NONE = 0, - OSRS_CONSUMABLE_BREW = 1, - OSRS_CONSUMABLE_SUPER_RESTORE = 2, - OSRS_CONSUMABLE_SANFEW = 3, - OSRS_CONSUMABLE_SUPER_COMBAT = 4, - OSRS_CONSUMABLE_DIVINE_COMBAT = 5, - OSRS_CONSUMABLE_RANGING = 6, - OSRS_CONSUMABLE_DIVINE_RANGING = 7, - OSRS_CONSUMABLE_SURGE = 8, - OSRS_CONSUMABLE_GUTHIX_REST = 9, - OSRS_CONSUMABLE_SATURATED_HEART = 10, - OSRS_CONSUMABLE_ANTIVENOM_PLUS = 11, - OSRS_CONSUMABLE_SHARK_FOOD = 12, - OSRS_CONSUMABLE_KARAMBWAN = 13, - OSRS_CONSUMABLE_PRAYER_RESTORE = 14, - OSRS_CONSUMABLE_BASTION = 15, - OSRS_CONSUMABLE_STAMINA = 16, -} OsrsConsumableKind; typedef enum { OSRS_CLICK_TICK_FIRST = 0, @@ -92,128 +66,6 @@ typedef void (*OsrsInventoryDrinkOneDoseEffectFn)( #define OSRS_INVENTORY_CELL_OBS_FEATURES_CODED 3 #define OSRS_EQUIPPED_SELF_OBS_FEATURES 18 -static const OsrsConsumableClick OSRS_CONSUMABLE_CLICK_REGISTRY[] = { - {6685, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 4}, - {6687, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 3}, - {6689, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 2}, - {6691, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 1}, - {3024, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 4}, - {3026, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 3}, - {3028, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 2}, - {3030, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 1}, - {10925, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 4}, - {10927, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 3}, - {10929, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 2}, - {10931, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 1}, - {12695, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 4}, - {12697, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 3}, - {12699, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 2}, - {12701, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 1}, - {23685, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 4}, - {23688, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 3}, - {23691, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 2}, - {23694, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 1}, - {2444, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 4}, - {169, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 3}, - {171, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 2}, - {173, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 1}, - {23733, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 4}, - {23736, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 3}, - {23739, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 2}, - {23742, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 1}, - {30875, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 4}, - {30878, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 3}, - {30881, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 2}, - {30884, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 1}, - {4417, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 4}, - {4419, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 3}, - {4421, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 2}, - {4423, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 1}, - {27641, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SATURATED_HEART, 1}, - {12913, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 4}, - {12915, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 3}, - {12917, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 2}, - {12919, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 1}, - {2434, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 4}, - {139, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 3}, - {141, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 2}, - {143, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 1}, - {22461, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 4}, - {22464, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 3}, - {22467, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 2}, - {22470, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 1}, - {12625, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 4}, - {12627, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 3}, - {12629, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 2}, - {12631, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 1}, - {385, OSRS_CLICK_EAT, OSRS_CONSUMABLE_SHARK_FOOD, 0}, - {3144, OSRS_CLICK_EAT, OSRS_CONSUMABLE_KARAMBWAN, 0}, -}; - -static inline int osrs_consumable_click_registry_index(uint16_t raw_osrs_id) { - switch (raw_osrs_id) { - case 6685: return 0; - case 6687: return 1; - case 6689: return 2; - case 6691: return 3; - case 3024: return 4; - case 3026: return 5; - case 3028: return 6; - case 3030: return 7; - case 10925: return 8; - case 10927: return 9; - case 10929: return 10; - case 10931: return 11; - case 12695: return 12; - case 12697: return 13; - case 12699: return 14; - case 12701: return 15; - case 23685: return 16; - case 23688: return 17; - case 23691: return 18; - case 23694: return 19; - case 2444: return 20; - case 169: return 21; - case 171: return 22; - case 173: return 23; - case 23733: return 24; - case 23736: return 25; - case 23739: return 26; - case 23742: return 27; - case 30875: return 28; - case 30878: return 29; - case 30881: return 30; - case 30884: return 31; - case 4417: return 32; - case 4419: return 33; - case 4421: return 34; - case 4423: return 35; - case 27641: return 36; - case 12913: return 37; - case 12915: return 38; - case 12917: return 39; - case 12919: return 40; - case 2434: return 41; - case 139: return 42; - case 141: return 43; - case 143: return 44; - case 22461: return 45; - case 22464: return 46; - case 22467: return 47; - case 22470: return 48; - case 12625: return 49; - case 12627: return 50; - case 12629: return 51; - case 12631: return 52; - case 385: return 53; - case 3144: return 54; - default: return -1; - } -} - -static inline OsrsConsumableClick osrs_consumable_click_lookup_raw_osrs_id( - uint16_t raw_osrs_id -); static inline void osrs_inventory_clicks_trap(void) { #if defined(__clang__) || defined(__GNUC__) @@ -258,6 +110,7 @@ static inline OsrsConsumableKind6 col_consumable_kind6(OsrsConsumableKind k) { case OSRS_CONSUMABLE_SHARK_FOOD: case OSRS_CONSUMABLE_KARAMBWAN: return COL_CKIND6_FOOD; case OSRS_CONSUMABLE_NONE: return COL_CKIND6_NONE; + case OSRS_CONSUMABLE_COUNT: break; } osrs_inventory_clicks_trap(); return COL_CKIND6_NONE; @@ -293,12 +146,6 @@ static inline int osrs_consumable_offensive_boost_amount(OsrsConsumableKind k, i } } -static inline uint8_t osrs_item_index_for_raw_osrs_id(uint16_t raw_osrs_id) { - for (int i = 0; i < NUM_ITEMS; i++) { - if (ITEM_DATABASE[i].item_id == raw_osrs_id) return (uint8_t)i; - } - return ITEM_NONE; -} typedef struct { float present; @@ -318,145 +165,176 @@ typedef struct { float offensive_boost; } OsrsInventoryCellAffordance; -static inline OsrsInventoryCellAffordance osrs_inventory_cell_affordance( - uint8_t item_idx, - uint16_t raw_osrs_id, - uint8_t dose, +static inline OsrsInventoryCellAffordance osrs_item_content_affordance( + const OsrsItemContentMetadata* metadata, int is_equipped, const float post_use_deltas[6], int base_hitpoints, int base_prayer, int base_level ) { - OsrsConsumableClick consumable = - osrs_consumable_click_lookup_raw_osrs_id(raw_osrs_id); - int present = raw_osrs_id != 0 || item_idx != ITEM_NONE; - int is_gear = item_idx != ITEM_NONE; - int style = is_gear ? get_item_attack_style(item_idx) : 0; + int present = metadata->raw_osrs_id != 0; + int is_gear = metadata->item != NULL; + int is_weapon = is_gear && metadata->item->slot == SLOT_WEAPON; + OsrsConsumableKind consumable_kind = + (OsrsConsumableKind)metadata->consumable_kind; + OsrsConsumableKind6 kind6 = col_consumable_kind6(consumable_kind); + int hp_heal = + osrs_consumable_hp_heal_amount(consumable_kind, base_hitpoints); + int prayer_restore = + osrs_consumable_prayer_restore_amount(consumable_kind, base_prayer); + int offensive_boost = + osrs_consumable_offensive_boost_amount(consumable_kind, base_level); uint32_t effect_mask = - is_gear ? ITEM_DATABASE[item_idx].effect_mask : OSRS_ITEM_EFFECT_NONE; - int is_weapon = is_gear && ITEM_DATABASE[item_idx].slot == SLOT_WEAPON; - OsrsConsumableKind6 k6 = col_consumable_kind6(consumable.consumable_kind); - OsrsConsumableKind ck = consumable.consumable_kind; - int hp_heal = osrs_consumable_hp_heal_amount(ck, base_hitpoints); - int pray_restore = osrs_consumable_prayer_restore_amount(ck, base_prayer); - int off_boost = osrs_consumable_offensive_boost_amount(ck, base_level); - - OsrsInventoryCellAffordance a; - a.present = present ? 1.0f : 0.0f; - a.is_equipped = is_equipped ? 1.0f : 0.0f; - a.dose = dose > 0 ? osrs_clamp_unit((float)dose / 4.0f) : 0.0f; - a.is_armor = (is_gear && !is_weapon) ? 1.0f : 0.0f; - a.is_weapon = is_weapon ? 1.0f : 0.0f; - a.style3[0] = style == 1 ? 1.0f : 0.0f; - a.style3[1] = style == 2 ? 1.0f : 0.0f; - a.style3[2] = style == 3 ? 1.0f : 0.0f; - for (int i = 0; i < 6; i++) - a.post_use_deltas[i] = - post_use_deltas ? osrs_clamp_unit(post_use_deltas[i]) : 0.0f; - /* Normalised energy cost, 0 when the weapon has no special. Doubles as the "this item - * has a special attack" flag while also telling the agent whether it can afford one. */ - a.spec_cost = is_weapon - ? osrs_clamp_unit((float)osrs_spec_cost(item_idx) / 100.0f) + is_gear ? metadata->item->effect_mask : OSRS_ITEM_EFFECT_NONE; + + OsrsInventoryCellAffordance affordance; + affordance.present = present ? 1.0f : 0.0f; + affordance.is_equipped = is_equipped ? 1.0f : 0.0f; + affordance.dose = metadata->dose_count > 0 + ? osrs_clamp_unit((float)metadata->dose_count / 4.0f) + : 0.0f; + affordance.is_armor = (is_gear && !is_weapon) ? 1.0f : 0.0f; + affordance.is_weapon = is_weapon ? 1.0f : 0.0f; + affordance.style3[0] = metadata->attack_style == 1 ? 1.0f : 0.0f; + affordance.style3[1] = metadata->attack_style == 2 ? 1.0f : 0.0f; + affordance.style3[2] = metadata->attack_style == 3 ? 1.0f : 0.0f; + for (int index = 0; index < 6; index++) { + affordance.post_use_deltas[index] = post_use_deltas + ? osrs_clamp_unit(post_use_deltas[index]) + : 0.0f; + } + affordance.spec_cost = is_weapon + ? osrs_clamp_unit( + (float)osrs_spec_cost(metadata->item_idx) / 100.0f) + : 0.0f; + osrs_item_effect_class4(effect_mask, affordance.effect_class4); + affordance.attack_speed = is_weapon + ? osrs_clamp_unit( + (float)metadata->item->attack_speed / STAT_NORM_SPEED) : 0.0f; - osrs_item_effect_class4(effect_mask, a.effect_class4); - a.attack_speed = is_weapon - ? osrs_clamp_unit((float)ITEM_DATABASE[item_idx].attack_speed / STAT_NORM_SPEED) + affordance.attack_range = is_weapon + ? osrs_clamp_unit( + (float)metadata->item->attack_range / STAT_NORM_RANGE) : 0.0f; - a.attack_range = is_weapon - ? osrs_clamp_unit((float)ITEM_DATABASE[item_idx].attack_range / STAT_NORM_RANGE) + affordance.kind5[0] = kind6 == COL_CKIND6_BREW ? 1.0f : 0.0f; + affordance.kind5[1] = kind6 == COL_CKIND6_RESTORE ? 1.0f : 0.0f; + affordance.kind5[2] = kind6 == COL_CKIND6_COMBAT_BOOST ? 1.0f : 0.0f; + affordance.kind5[3] = kind6 == COL_CKIND6_RANGED_BOOST ? 1.0f : 0.0f; + affordance.kind5[4] = kind6 == COL_CKIND6_SPECIAL ? 1.0f : 0.0f; + affordance.hp_heal = base_hitpoints > 0 + ? osrs_clamp_unit((float)hp_heal / (float)base_hitpoints) : 0.0f; - a.kind5[0] = k6 == COL_CKIND6_BREW ? 1.0f : 0.0f; - a.kind5[1] = k6 == COL_CKIND6_RESTORE ? 1.0f : 0.0f; - a.kind5[2] = k6 == COL_CKIND6_COMBAT_BOOST ? 1.0f : 0.0f; - a.kind5[3] = k6 == COL_CKIND6_RANGED_BOOST ? 1.0f : 0.0f; - a.kind5[4] = k6 == COL_CKIND6_SPECIAL ? 1.0f : 0.0f; - a.hp_heal = base_hitpoints > 0 - ? osrs_clamp_unit((float)hp_heal / (float)base_hitpoints) : 0.0f; - a.prayer_restore = base_prayer > 0 - ? osrs_clamp_unit((float)pray_restore / (float)base_prayer) : 0.0f; - a.offensive_boost = osrs_clamp_unit((float)off_boost / STAT_NORM_STRENGTH); - return a; + affordance.prayer_restore = base_prayer > 0 + ? osrs_clamp_unit((float)prayer_restore / (float)base_prayer) + : 0.0f; + affordance.offensive_boost = + osrs_clamp_unit((float)offensive_boost / STAT_NORM_STRENGTH); + return affordance; } static inline void osrs_write_inventory_cell_affordance_features( float* out, - uint8_t item_idx, - uint16_t raw_osrs_id, - uint8_t dose, + const OsrsInventoryCell* cell, int is_equipped, const float post_use_deltas[6], int base_hitpoints, int base_prayer, int base_level ) { - OsrsInventoryCellAffordance a = osrs_inventory_cell_affordance( - item_idx, raw_osrs_id, dose, is_equipped, post_use_deltas, + OsrsInventoryCellAffordance affordance = osrs_item_content_affordance( + osrs_inventory_cell_metadata(cell), is_equipped, post_use_deltas, base_hitpoints, base_prayer, base_level); - out[0] = a.present; - out[1] = a.is_equipped; - out[2] = a.dose; - out[3] = a.style3[0]; - out[4] = a.style3[1]; - out[5] = a.style3[2]; - for (int i = 0; i < 6; i++) out[6 + i] = a.post_use_deltas[i]; - out[12] = a.is_armor; - out[13] = a.is_weapon; - for (int i = 0; i < 5; i++) out[14 + i] = a.kind5[i]; - out[19] = a.hp_heal; - out[20] = a.prayer_restore; - out[21] = a.offensive_boost; - for (int i = 0; i < 4; i++) out[22 + i] = a.effect_class4[i]; - out[26] = a.attack_speed; - out[27] = a.attack_range; + out[0] = affordance.present; + out[1] = affordance.is_equipped; + out[2] = affordance.dose; + out[3] = affordance.style3[0]; + out[4] = affordance.style3[1]; + out[5] = affordance.style3[2]; + for (int index = 0; index < 6; index++) { + out[6 + index] = affordance.post_use_deltas[index]; + } + out[12] = affordance.is_armor; + out[13] = affordance.is_weapon; + for (int index = 0; index < 5; index++) { + out[14 + index] = affordance.kind5[index]; + } + out[19] = affordance.hp_heal; + out[20] = affordance.prayer_restore; + out[21] = affordance.offensive_boost; + for (int index = 0; index < 4; index++) { + out[22 + index] = affordance.effect_class4[index]; + } + out[26] = affordance.attack_speed; + out[27] = affordance.attack_range; } -static inline void osrs_write_inventory_cell_affordance_features_compact( +static inline void osrs_write_item_content_affordance_features_compact( float* out, - uint8_t item_idx, - uint16_t raw_osrs_id, - uint8_t dose, + const OsrsItemContentMetadata* metadata, int is_equipped, int base_hitpoints, int base_prayer, int base_level ) { - OsrsInventoryCellAffordance a = osrs_inventory_cell_affordance( - item_idx, raw_osrs_id, dose, is_equipped, NULL, + OsrsInventoryCellAffordance affordance = osrs_item_content_affordance( + metadata, is_equipped, NULL, base_hitpoints, base_prayer, base_level); - out[OSRS_INVENTORY_CELL_COMPACT_PRESENT] = a.present; - out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] = a.is_equipped; - out[OSRS_INVENTORY_CELL_COMPACT_DOSE] = a.dose; - out[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] = a.is_armor; - out[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] = a.is_weapon; - - float* u = out + OSRS_INVENTORY_CELL_OBS_SHARED; - for (int i = 0; i < OSRS_INVENTORY_CELL_OBS_KIND_UNION; i++) u[i] = 0.0f; - if (a.is_armor != 0.0f || a.is_weapon != 0.0f) { - u[0] = a.style3[0]; - u[1] = a.style3[1]; - u[2] = a.style3[2]; - for (int i = 0; i < 4; i++) u[3 + i] = a.effect_class4[i]; - u[7] = a.attack_speed; - u[8] = a.attack_range; - u[9] = a.spec_cost; + out[OSRS_INVENTORY_CELL_COMPACT_PRESENT] = affordance.present; + out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] = affordance.is_equipped; + out[OSRS_INVENTORY_CELL_COMPACT_DOSE] = affordance.dose; + out[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] = affordance.is_armor; + out[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] = affordance.is_weapon; + + float* content = out + OSRS_INVENTORY_CELL_OBS_SHARED; + for (int index = 0; index < OSRS_INVENTORY_CELL_OBS_KIND_UNION; index++) { + content[index] = 0.0f; + } + if (affordance.is_armor != 0.0f || affordance.is_weapon != 0.0f) { + content[0] = affordance.style3[0]; + content[1] = affordance.style3[1]; + content[2] = affordance.style3[2]; + for (int index = 0; index < 4; index++) { + content[3 + index] = affordance.effect_class4[index]; + } + content[7] = affordance.attack_speed; + content[8] = affordance.attack_range; + content[9] = affordance.spec_cost; } else { - for (int i = 0; i < 5; i++) u[i] = a.kind5[i]; - u[5] = a.hp_heal; - u[6] = a.prayer_restore; - u[7] = a.offensive_boost; + for (int index = 0; index < 5; index++) { + content[index] = affordance.kind5[index]; + } + content[5] = affordance.hp_heal; + content[6] = affordance.prayer_restore; + content[7] = affordance.offensive_boost; } } +static inline void osrs_write_inventory_cell_affordance_features_compact( + float* out, + const OsrsInventoryCell* cell, + int is_equipped, + int base_hitpoints, + int base_prayer, + int base_level +) { + osrs_write_item_content_affordance_features_compact( + out, osrs_inventory_cell_metadata(cell), is_equipped, + base_hitpoints, base_prayer, base_level); +} + static inline void osrs_write_equipped_self_features(float* out, uint8_t item_idx) { - for (int i = 0; i < OSRS_EQUIPPED_SELF_OBS_FEATURES; i++) out[i] = 0.0f; + for (int index = 0; index < OSRS_EQUIPPED_SELF_OBS_FEATURES; index++) { + out[index] = 0.0f; + } if (item_idx == ITEM_NONE) return; - if (item_idx >= NUM_ITEMS) osrs_inventory_clicks_trap(); - const Item* item = &ITEM_DATABASE[item_idx]; - int style = get_item_attack_style(item_idx); + const OsrsItemContentMetadata* metadata = osrs_item_content_metadata( + osrs_inventory_content_code_from_item(item_idx)); + const Item* item = metadata->item; + int style = metadata->attack_style; out[0] = 1.0f; out[1] = style == 1 ? 1.0f : 0.0f; out[2] = style == 2 ? 1.0f : 0.0f; @@ -473,133 +351,80 @@ static inline void osrs_write_equipped_self_features(float* out, uint8_t item_id out[10] = item->effect_mask != OSRS_ITEM_EFFECT_NONE ? 1.0f : 0.0f; out[11] = item->slot == SLOT_WEAPON ? 1.0f : 0.0f; - float eff4[4]; - osrs_item_effect_class4(item->effect_mask, eff4); - out[12] = eff4[0]; - out[13] = eff4[1]; - out[14] = eff4[2]; - out[15] = eff4[3]; + float effect_class[4]; + osrs_item_effect_class4(item->effect_mask, effect_class); + out[12] = effect_class[0]; + out[13] = effect_class[1]; + out[14] = effect_class[2]; + out[15] = effect_class[3]; out[16] = item->slot == SLOT_WEAPON - ? osrs_clamp_unit((float)item->attack_speed / STAT_NORM_SPEED) : 0.0f; + ? osrs_clamp_unit((float)item->attack_speed / STAT_NORM_SPEED) + : 0.0f; out[17] = item->slot == SLOT_WEAPON - ? osrs_clamp_unit((float)item->attack_range / STAT_NORM_RANGE) : 0.0f; + ? osrs_clamp_unit((float)item->attack_range / STAT_NORM_RANGE) + : 0.0f; } static inline OsrsClickAction osrs_item_click_action(uint8_t item_idx) { if (item_idx == ITEM_NONE) return OSRS_CLICK_NONE; - if (item_idx >= NUM_ITEMS) osrs_inventory_clicks_trap(); - - switch (ITEM_DATABASE[item_idx].slot) { - case SLOT_HEAD: - case SLOT_CAPE: - case SLOT_NECK: - case SLOT_WEAPON: - case SLOT_BODY: - case SLOT_SHIELD: - case SLOT_LEGS: - case SLOT_HANDS: - case SLOT_FEET: - case SLOT_RING: - case SLOT_AMMO: - return OSRS_CLICK_EQUIP; - default: - osrs_inventory_clicks_trap(); - return OSRS_CLICK_NONE; - } + const OsrsItemContentMetadata* metadata = osrs_item_content_metadata( + osrs_inventory_content_code_from_item(item_idx)); + return (OsrsClickAction)metadata->click_action; } static inline OsrsConsumableKind osrs_item_click_consumable_kind(uint8_t item_idx) { if (item_idx == ITEM_NONE) return OSRS_CONSUMABLE_NONE; - if (item_idx >= NUM_ITEMS) osrs_inventory_clicks_trap(); - return OSRS_CONSUMABLE_NONE; + const OsrsItemContentMetadata* metadata = osrs_item_content_metadata( + osrs_inventory_content_code_from_item(item_idx)); + return (OsrsConsumableKind)metadata->consumable_kind; } static inline OsrsConsumableClick osrs_consumable_click_lookup_raw_osrs_id( uint16_t raw_osrs_id ) { - int index = osrs_consumable_click_registry_index(raw_osrs_id); - if (index >= 0) return OSRS_CONSUMABLE_CLICK_REGISTRY[index]; - + const OsrsItemContentMetadata* metadata = osrs_item_content_metadata( + osrs_inventory_content_code_from_raw_osrs_id(raw_osrs_id)); return (OsrsConsumableClick){ - .raw_osrs_id = raw_osrs_id, - .click_action = OSRS_CLICK_NONE, - .consumable_kind = OSRS_CONSUMABLE_NONE, - .dose_count = 0, + .raw_osrs_id = metadata->raw_osrs_id, + .click_action = (OsrsClickAction)metadata->click_action, + .consumable_kind = (OsrsConsumableKind)metadata->consumable_kind, + .dose_count = metadata->dose_count, }; } static inline uint8_t osrs_consumable_dose_count_after_drink(uint8_t dose_count) { - switch (dose_count) { - case 1: return 0; - case 2: return 1; - case 3: return 2; - case 4: return 3; - default: - osrs_inventory_clicks_trap(); - return 0; - } + if (dose_count < 1 || dose_count > 4) osrs_inventory_clicks_trap(); + return (uint8_t)(dose_count - 1); } -static inline uint16_t osrs_consumable_raw_osrs_id_after_drink(uint16_t raw_osrs_id) { - OsrsConsumableClick before = - osrs_consumable_click_lookup_raw_osrs_id(raw_osrs_id); - if (before.click_action != OSRS_CLICK_DRINK || before.dose_count == 0) { +static inline uint16_t osrs_consumable_raw_osrs_id_after_drink( + uint16_t raw_osrs_id +) { + const OsrsItemContentMetadata* metadata = osrs_item_content_metadata( + osrs_inventory_content_code_from_raw_osrs_id(raw_osrs_id)); + if (metadata->click_action != OSRS_CLICK_DRINK || + metadata->dose_count == 0) { osrs_inventory_clicks_trap(); } - - uint8_t after_dose = - osrs_consumable_dose_count_after_drink(before.dose_count); - if (after_dose == 0) return 0; - - int count = (int)( - sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / - sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0]) - ); - - for (int i = 0; i < count; i++) { - OsrsConsumableClick candidate = OSRS_CONSUMABLE_CLICK_REGISTRY[i]; - if (candidate.consumable_kind == before.consumable_kind && - candidate.dose_count == after_dose) { - return candidate.raw_osrs_id; - } - } - - osrs_inventory_clicks_trap(); - return 0; + if (metadata->next_content_code == 0) return 0; + return osrs_item_content_metadata( + metadata->next_content_code)->raw_osrs_id; } -static inline OsrsInventoryClickResolution osrs_inventory_click_classify( - uint8_t item_idx, - uint16_t raw_osrs_id +static inline OsrsInventoryClickResolution osrs_inventory_cell_click_classify( + const OsrsInventoryCell* cell ) { - if (item_idx != ITEM_NONE) { - OsrsClickAction action = osrs_item_click_action(item_idx); - if (raw_osrs_id != 0 && raw_osrs_id != ITEM_DATABASE[item_idx].item_id) { - osrs_inventory_clicks_trap(); - } - return (OsrsInventoryClickResolution){ - .click_action = action, - }; - } - - if (raw_osrs_id == 0) { - return (OsrsInventoryClickResolution){ - .click_action = OSRS_CLICK_NONE, - }; - } - - OsrsConsumableClick consumable = - osrs_consumable_click_lookup_raw_osrs_id(raw_osrs_id); + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(cell); return (OsrsInventoryClickResolution){ - .click_action = consumable.click_action, - .consumable_kind = consumable.consumable_kind, - .dose_count = consumable.dose_count, + .click_action = (OsrsClickAction)metadata->click_action, + .consumable_kind = (OsrsConsumableKind)metadata->consumable_kind, + .dose_count = metadata->dose_count, }; } -static inline OsrsInventoryClickResolution osrs_inventory_click_interpret( - uint8_t item_idx, - uint16_t raw_osrs_id, +static inline OsrsInventoryClickResolution osrs_inventory_cell_click_interpret( + const OsrsInventoryCell* cell, OsrsClickTickMultiplicity tick_multiplicity ) { switch (tick_multiplicity) { @@ -613,32 +438,20 @@ static inline OsrsInventoryClickResolution osrs_inventory_click_interpret( osrs_inventory_clicks_trap(); } + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(cell); OsrsInventoryClickResolution resolution = - osrs_inventory_click_classify(item_idx, raw_osrs_id); - if (resolution.click_action == OSRS_CLICK_DRINK && - resolution.dose_count > 0) { + osrs_inventory_cell_click_classify(cell); + if (resolution.click_action == OSRS_CLICK_DRINK) { resolution.raw_osrs_id_after_drink = - osrs_consumable_raw_osrs_id_after_drink(raw_osrs_id); + metadata->next_content_code == 0 + ? 0 + : osrs_item_content_metadata( + metadata->next_content_code)->raw_osrs_id; } return resolution; } -static inline OsrsInventoryClickResolution osrs_inventory_cell_click_classify( - const OsrsInventoryCell* cell -) { - return osrs_inventory_click_classify(cell->item_idx, cell->raw_osrs_id); -} - -static inline OsrsInventoryClickResolution osrs_inventory_cell_click_interpret( - const OsrsInventoryCell* cell, - OsrsClickTickMultiplicity tick_multiplicity -) { - return osrs_inventory_click_interpret( - cell->item_idx, - cell->raw_osrs_id, - tick_multiplicity); -} - static inline OsrsInventoryClickResolution osrs_inventory_snapshot_click_interpret( const OsrsInventorySlotSnapshot* snapshot, int slot, @@ -649,48 +462,27 @@ static inline OsrsInventoryClickResolution osrs_inventory_snapshot_click_interpr abort(); } return osrs_inventory_cell_click_interpret( - &snapshot->cells[slot], - tick_multiplicity); -} - -static inline OsrsInventoryCell osrs_inventory_cell_from_raw_osrs_id( - uint16_t raw_osrs_id -) { - if (raw_osrs_id == 0) return osrs_inventory_cell_empty(); - - uint8_t item_idx = osrs_item_index_for_raw_osrs_id(raw_osrs_id); - if (item_idx != ITEM_NONE) return osrs_inventory_cell_from_item(item_idx); - - OsrsConsumableClick consumable = - osrs_consumable_click_lookup_raw_osrs_id(raw_osrs_id); - return (OsrsInventoryCell){ - .item_idx = ITEM_NONE, - .raw_osrs_id = raw_osrs_id, - .dose = consumable.dose_count, - }; + &snapshot->cells[slot], tick_multiplicity); } static inline void osrs_inventory_cell_decrement_drink( OsrsInventoryCell* cell, OsrsInventoryClickResolution resolution ) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(cell); if (resolution.click_action != OSRS_CLICK_DRINK || resolution.dose_count == 0 || - cell->dose != resolution.dose_count) { - fprintf(stderr, "inventory drink decrement: invalid cell raw=%u dose=%u action=%d resolved_dose=%u\n", - cell->raw_osrs_id, cell->dose, (int)resolution.click_action, - resolution.dose_count); + metadata->dose_count != resolution.dose_count || + metadata->next_content_code == cell->content_code) { + fprintf(stderr, + "inventory drink decrement: invalid content=%u dose=%u action=%d resolved_dose=%u\n", + cell->content_code, metadata->dose_count, + (int)resolution.click_action, resolution.dose_count); abort(); } - - if (resolution.raw_osrs_id_after_drink == 0) { - *cell = osrs_inventory_cell_empty(); - return; - } - - cell->item_idx = ITEM_NONE; - cell->raw_osrs_id = resolution.raw_osrs_id_after_drink; - cell->dose = osrs_consumable_dose_count_after_drink(resolution.dose_count); + *cell = osrs_inventory_cell_from_content_code( + metadata->next_content_code); } static inline OsrsInventoryDrinkConsumeResult osrs_inventory_cell_consume_drink_one_dose( @@ -704,12 +496,15 @@ static inline OsrsInventoryDrinkConsumeResult osrs_inventory_cell_consume_drink_ fprintf(stderr, "inventory drink consume: null argument\n"); abort(); } + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(cell); if (resolution.click_action != OSRS_CLICK_DRINK || resolution.dose_count == 0 || - cell->dose != resolution.dose_count) { - fprintf(stderr, "inventory drink consume: invalid cell raw=%u dose=%u action=%d resolved_dose=%u\n", - cell->raw_osrs_id, cell->dose, (int)resolution.click_action, - resolution.dose_count); + metadata->dose_count != resolution.dose_count) { + fprintf(stderr, + "inventory drink consume: invalid content=%u dose=%u action=%d resolved_dose=%u\n", + cell->content_code, metadata->dose_count, + (int)resolution.click_action, resolution.dose_count); abort(); } @@ -720,7 +515,7 @@ static inline OsrsInventoryDrinkConsumeResult osrs_inventory_cell_consume_drink_ .consumable_kind = resolution.consumable_kind, .dose_count_before = resolution.dose_count, .dose_count_after = dose_after, - .raw_osrs_id_before = cell->raw_osrs_id, + .raw_osrs_id_before = metadata->raw_osrs_id, .raw_osrs_id_after_drink = resolution.raw_osrs_id_after_drink, }; if (*potion_timer > 0) return result; @@ -736,118 +531,43 @@ static inline void osrs_inventory_cell_consume_eat(OsrsInventoryCell* cell) { *cell = osrs_inventory_cell_empty(); } -/* One dense code per distinguishable cell content. Gear keys on the item index; consumables - key on their click-registry entry, because a potion's raw id is what carries its kind and - its dose; every remaining present-but-inert raw id collapses onto a single code, since no - derived feature can tell two of them apart. - - The code rides the observation divided by OSRS_INVENTORY_CELL_OBS_CODE_SCALE. The encoder's - global linear layer reads the WHOLE observation, so 28 raw codes near 200 sitting beside - 894 features in [0,1] outweigh everything else by four orders of magnitude in the input - variance -- measured, and it cost episode_return 7.8 -> 0.6 with entropy collapsing 7.3 -> - 1.9 over 30M steps. Scaling by a power of two only shifts the exponent, so a code that is - exact in bf16 stays exact and the gather's multiply-back is lossless; that is also why the - count must stay at or under the scale. */ -#define OSRS_CONSUMABLE_CLICK_REGISTRY_COUNT \ - ((int)(sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / \ - sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0]))) -#define OSRS_INVENTORY_CELL_OBS_CODE_EMPTY 0 -#define OSRS_INVENTORY_CELL_OBS_CODE_GEAR_BASE 1 -#define OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE \ - (OSRS_INVENTORY_CELL_OBS_CODE_GEAR_BASE + NUM_ITEMS) -#define OSRS_INVENTORY_CELL_OBS_CODE_INERT \ - (OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE + OSRS_CONSUMABLE_CLICK_REGISTRY_COUNT) -#define OSRS_INVENTORY_CELL_OBS_CODE_COUNT (OSRS_INVENTORY_CELL_OBS_CODE_INERT + 1) -#define OSRS_INVENTORY_CELL_OBS_CODE_SCALE 256 - -static inline float osrs_inventory_cell_obs_code_encode(int code) { - return (float)code / (float)OSRS_INVENTORY_CELL_OBS_CODE_SCALE; -} - -static inline int osrs_inventory_cell_obs_code_decode(float observed) { - return (int)lrintf(observed * (float)OSRS_INVENTORY_CELL_OBS_CODE_SCALE); -} - -/* Stands in for every present raw id that is neither gear nor a registered consumable. */ -#define OSRS_INVENTORY_CELL_INERT_RAW_OSRS_ID 0xFFFFu - -static inline int osrs_inventory_cell_obs_code(uint8_t item_idx, uint16_t raw_osrs_id) { - if (item_idx != ITEM_NONE) { - if (item_idx >= NUM_ITEMS) osrs_inventory_clicks_trap(); - return OSRS_INVENTORY_CELL_OBS_CODE_GEAR_BASE + item_idx; - } - if (raw_osrs_id == 0) return OSRS_INVENTORY_CELL_OBS_CODE_EMPTY; - int index = osrs_consumable_click_registry_index(raw_osrs_id); - return index >= 0 - ? OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE + index - : OSRS_INVENTORY_CELL_OBS_CODE_INERT; +static inline float osrs_inventory_cell_obs_code_encode(uint16_t content_code) { + (void)osrs_item_content_metadata(content_code); + return (float)content_code / (float)OSRS_ITEM_OBS_CODE_SCALE; } -static inline OsrsInventoryCell osrs_inventory_cell_for_obs_code(int code) { - if (code < 0 || code >= OSRS_INVENTORY_CELL_OBS_CODE_COUNT) { - osrs_inventory_clicks_trap(); - } - if (code == OSRS_INVENTORY_CELL_OBS_CODE_EMPTY) return osrs_inventory_cell_empty(); - if (code < OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE) { - return osrs_inventory_cell_from_item( - (uint8_t)(code - OSRS_INVENTORY_CELL_OBS_CODE_GEAR_BASE)); - } - if (code < OSRS_INVENTORY_CELL_OBS_CODE_INERT) { - return osrs_inventory_cell_from_raw_osrs_id( - OSRS_CONSUMABLE_CLICK_REGISTRY[ - code - OSRS_INVENTORY_CELL_OBS_CODE_CONSUMABLE_BASE].raw_osrs_id); +static inline uint16_t osrs_inventory_cell_obs_code_decode(float observed) { + int content_code = + (int)lrintf(observed * (float)OSRS_ITEM_OBS_CODE_SCALE); + if (content_code < 0 || content_code >= OSRS_ITEM_CONTENT_COUNT) { + fprintf(stderr, "inventory observation: invalid content code %d\n", + content_code); + abort(); } - return (OsrsInventoryCell){ - .item_idx = ITEM_NONE, - .raw_osrs_id = OSRS_INVENTORY_CELL_INERT_RAW_OSRS_ID, - .dose = 0, - }; -} - -/* The generated item-table row: the full compact record at the table's fixed base stats. - Equipped is supplied by each observation and is the only zeroed slot. */ -static inline void osrs_write_inventory_cell_obs_table_row( - float* out, - int code, - int base_hitpoints, - int base_prayer, - int base_ranged -) { - OsrsInventoryCell cell = osrs_inventory_cell_for_obs_code(code); - osrs_write_inventory_cell_affordance_features_compact( - out, cell.item_idx, cell.raw_osrs_id, cell.dose, 0, - base_hitpoints, base_prayer, base_ranged); - - out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] = 0.0f; + return (uint16_t)content_code; } -/* The other half of the same record. Projecting both sides off one compact write is what - makes them complementary: a change to the affordance reaches the table and the - observation together instead of drifting between them. */ static inline void osrs_write_inventory_cell_obs_code_features( float* out, - uint8_t item_idx, - uint16_t raw_osrs_id, - uint8_t dose, + const OsrsInventoryCell* cell, int is_equipped, - int base_hitpoints, - int base_prayer, - int base_level + int base_hitpoints ) { - float compact[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; - osrs_write_inventory_cell_affordance_features_compact( - compact, item_idx, raw_osrs_id, dose, is_equipped, - base_hitpoints, base_prayer, base_level); - - int is_gear = compact[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] != 0.0f || - compact[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] != 0.0f; - - out[OSRS_INVENTORY_CELL_OBS_CODE] = osrs_inventory_cell_obs_code_encode( - osrs_inventory_cell_obs_code(item_idx, raw_osrs_id)); + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(cell); + out[OSRS_INVENTORY_CELL_OBS_CODE] = + osrs_inventory_cell_obs_code_encode(cell->content_code); out[OSRS_INVENTORY_CELL_OBS_EQUIPPED] = - compact[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED]; + is_equipped ? 1.0f : 0.0f; + if (metadata->item != NULL || base_hitpoints <= 0) { + out[OSRS_INVENTORY_CELL_OBS_HP_HEAL] = 0.0f; + return; + } + int hp_heal = osrs_consumable_hp_heal_amount( + (OsrsConsumableKind)metadata->consumable_kind, + base_hitpoints); out[OSRS_INVENTORY_CELL_OBS_HP_HEAL] = - is_gear ? 0.0f : compact[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL]; + osrs_clamp_unit((float)hp_heal / (float)base_hitpoints); } /* Rebuilds the compact record, overwrites equipped, and overwrites HP-heal only for non-gear. */ diff --git a/ocean/osrs/osrs_item_obs_generated.h b/ocean/osrs/osrs_item_obs_generated.h index b8178857f4..9fffd5156a 100644 --- a/ocean/osrs/osrs_item_obs_generated.h +++ b/ocean/osrs/osrs_item_obs_generated.h @@ -3,6 +3,7 @@ #ifndef OSRS_ITEM_OBS_GENERATED_H #define OSRS_ITEM_OBS_GENERATED_H +#define OSRS_ITEM_CONTENT_COUNT 201 #define OSRS_ITEM_OBS_TABLE_ROWS 201 #define OSRS_ITEM_OBS_TABLE_COLS 15 #define OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS 99 @@ -14,4 +15,264 @@ #define OSRS_ITEM_OBS_OVERLAY_EQUIPPED 1 #define OSRS_ITEM_OBS_OVERLAY_HP_HEAL 10 +#define OSRS_ITEM_CONTENT_ROWS(X) \ + X(0, NULL, 255, 0, -1, 0, 0, 0, 0, 0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(1, &ITEM_DATABASE[0], 0, 10828, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(2, &ITEM_DATABASE[1], 1, 21795, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(3, &ITEM_DATABASE[2], 2, 1712, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(4, &ITEM_DATABASE[3], 3, 2503, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(5, &ITEM_DATABASE[4], 4, 4091, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(6, &ITEM_DATABASE[5], 5, 1079, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(7, &ITEM_DATABASE[6], 6, 4093, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(8, &ITEM_DATABASE[7], 7, 4151, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ + X(9, &ITEM_DATABASE[8], 8, 9185, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.0f) \ + X(10, &ITEM_DATABASE[9], 9, 4710, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.666666687f, 0.0f) \ + X(11, &ITEM_DATABASE[10], 10, 5698, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.25f) \ + X(12, &ITEM_DATABASE[11], 11, 12954, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(13, &ITEM_DATABASE[12], 12, 12829, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(14, &ITEM_DATABASE[13], 13, 7462, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(15, &ITEM_DATABASE[14], 14, 3105, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(16, &ITEM_DATABASE[15], 15, 6737, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(17, &ITEM_DATABASE[16], 16, 9243, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(18, &ITEM_DATABASE[17], 17, 22324, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ + X(19, &ITEM_DATABASE[18], 18, 24417, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ + X(20, &ITEM_DATABASE[19], 19, 11791, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.666666687f, 0.0f) \ + X(21, &ITEM_DATABASE[20], 20, 21006, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.666666687f, 0.0f) \ + X(22, &ITEM_DATABASE[21], 21, 24424, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.550000012f) \ + X(23, &ITEM_DATABASE[22], 22, 13867, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ + X(24, &ITEM_DATABASE[23], 23, 11785, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.5f) \ + X(25, &ITEM_DATABASE[24], 24, 26374, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.75f) \ + X(26, &ITEM_DATABASE[25], 25, 13652, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.5f) \ + X(27, &ITEM_DATABASE[26], 26, 11802, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(28, &ITEM_DATABASE[27], 27, 26233, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(29, &ITEM_DATABASE[28], 28, 4153, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.0666666701f, 0.5f) \ + X(30, &ITEM_DATABASE[29], 29, 21003, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(31, &ITEM_DATABASE[30], 30, 11235, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.899999976f, 0.666666687f, 0.550000012f) \ + X(32, &ITEM_DATABASE[31], 31, 19481, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.666666687f, 0.649999976f) \ + X(33, &ITEM_DATABASE[32], 32, 22613, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.25f) \ + X(34, &ITEM_DATABASE[33], 33, 27690, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.5f) \ + X(35, &ITEM_DATABASE[34], 34, 22622, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.349999994f) \ + X(36, &ITEM_DATABASE[35], 35, 22636, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.333333343f, 0.5f) \ + X(37, &ITEM_DATABASE[36], 36, 21018, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(38, &ITEM_DATABASE[37], 37, 21021, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(39, &ITEM_DATABASE[38], 38, 21024, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(40, &ITEM_DATABASE[39], 39, 4712, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(41, &ITEM_DATABASE[40], 40, 4714, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(42, &ITEM_DATABASE[41], 41, 4736, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(43, &ITEM_DATABASE[42], 42, 11834, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(44, &ITEM_DATABASE[43], 43, 12831, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(45, &ITEM_DATABASE[44], 44, 6585, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(46, &ITEM_DATABASE[45], 45, 12002, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(47, &ITEM_DATABASE[46], 46, 21295, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(48, &ITEM_DATABASE[47], 47, 13235, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(49, &ITEM_DATABASE[48], 48, 11770, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(50, &ITEM_DATABASE[49], 49, 25975, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f) \ + X(51, &ITEM_DATABASE[50], 50, 6889, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(52, &ITEM_DATABASE[51], 51, 11212, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(53, &ITEM_DATABASE[52], 52, 4751, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(54, &ITEM_DATABASE[53], 53, 4722, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(55, &ITEM_DATABASE[54], 54, 4759, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(56, &ITEM_DATABASE[55], 55, 4745, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(57, &ITEM_DATABASE[56], 56, 4716, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(58, &ITEM_DATABASE[57], 57, 4753, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(59, &ITEM_DATABASE[58], 58, 4724, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(60, &ITEM_DATABASE[59], 59, 21932, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(61, &ITEM_DATABASE[60], 60, 21791, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(62, &ITEM_DATABASE[61], 61, 31113, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.400000006f, 0.5f) \ + X(63, &ITEM_DATABASE[62], 62, 27251, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(64, &ITEM_DATABASE[63], 63, 31106, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(65, &ITEM_DATABASE[64], 64, 31097, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(66, &ITEM_DATABASE[65], 65, 20657, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(67, &ITEM_DATABASE[66], 66, 20997, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.600000024f, 0.666666687f, 0.0f) \ + X(68, &ITEM_DATABASE[67], 67, 27235, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(69, &ITEM_DATABASE[68], 68, 27238, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(70, &ITEM_DATABASE[69], 69, 27241, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(71, &ITEM_DATABASE[70], 70, 19547, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(72, &ITEM_DATABASE[71], 71, 28947, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(73, &ITEM_DATABASE[72], 72, 26235, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(74, &ITEM_DATABASE[73], 73, 12926, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.333333343f, 0.5f) \ + X(75, &ITEM_DATABASE[74], 74, 4708, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(76, &ITEM_DATABASE[75], 75, 19544, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(77, &ITEM_DATABASE[76], 76, 22481, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.0f) \ + X(78, &ITEM_DATABASE[77], 77, 6920, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(79, &ITEM_DATABASE[78], 78, 20220, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(80, &ITEM_DATABASE[79], 79, 2550, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(81, &ITEM_DATABASE[80], 80, 23971, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(82, &ITEM_DATABASE[81], 81, 22109, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(83, &ITEM_DATABASE[82], 82, 23975, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(84, &ITEM_DATABASE[83], 83, 23979, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(85, &ITEM_DATABASE[84], 84, 25865, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ + X(86, &ITEM_DATABASE[85], 85, 19921, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(87, &ITEM_DATABASE[86], 86, 4089, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(88, &ITEM_DATABASE[87], 87, 12899, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.0f) \ + X(89, &ITEM_DATABASE[88], 88, 12612, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(90, &ITEM_DATABASE[89], 89, 21326, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(91, &ITEM_DATABASE[90], 90, 4097, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(92, &ITEM_DATABASE[91], 91, 10382, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(93, &ITEM_DATABASE[92], 92, 2497, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(94, &ITEM_DATABASE[93], 93, 12788, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.5f) \ + X(95, &ITEM_DATABASE[94], 94, 10499, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(96, &ITEM_DATABASE[95], 95, 4224, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(97, &ITEM_DATABASE[96], 96, 13237, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(98, &ITEM_DATABASE[97], 97, 22326, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(99, &ITEM_DATABASE[98], 98, 22327, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(100, &ITEM_DATABASE[99], 99, 22328, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(101, &ITEM_DATABASE[100], 100, 11230, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.200000003f, 0.0f) \ + X(102, &ITEM_DATABASE[101], 101, 22325, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f) \ + X(103, &ITEM_DATABASE[102], 102, 24551, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ + X(104, &ITEM_DATABASE[103], 103, 26219, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f) \ + X(105, &ITEM_DATABASE[104], 104, 28338, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f) \ + X(106, &ITEM_DATABASE[105], 105, 26382, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(107, &ITEM_DATABASE[106], 106, 26384, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(108, &ITEM_DATABASE[107], 107, 26386, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(109, &ITEM_DATABASE[108], 108, 11832, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(110, &ITEM_DATABASE[109], 109, 11836, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(111, &ITEM_DATABASE[110], 110, 13239, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(112, &ITEM_DATABASE[111], 111, 22981, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(113, &ITEM_DATABASE[112], 112, 19553, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(114, &ITEM_DATABASE[113], 113, 11773, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(115, &ITEM_DATABASE[114], 114, 28307, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(116, &ITEM_DATABASE[115], 115, 22322, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(117, &ITEM_DATABASE[116], 116, 28310, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(118, &ITEM_DATABASE[117], 117, 26241, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(119, &ITEM_DATABASE[118], 118, 26243, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(120, &ITEM_DATABASE[119], 119, 26245, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(121, &ITEM_DATABASE[120], 120, 28313, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(122, &ITEM_DATABASE[121], 121, 27275, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ + X(123, &ITEM_DATABASE[122], 122, 11804, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(124, &ITEM_DATABASE[123], 123, 11806, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(125, &ITEM_DATABASE[124], 124, 11808, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(126, &ITEM_DATABASE[125], 125, 23987, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.13333334f, 0.0f) \ + X(127, &ITEM_DATABASE[126], 126, 1377, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.0f) \ + X(128, &ITEM_DATABASE[127], 127, 21944, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(129, &ITEM_DATABASE[128], 128, 21946, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(130, &ITEM_DATABASE[129], 129, 892, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(131, &ITEM_DATABASE[130], 130, 19484, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(132, &ITEM_DATABASE[131], 131, 12821, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(133, &ITEM_DATABASE[132], 132, 12817, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(134, &ITEM_DATABASE[133], 133, 11283, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(135, &ITEM_DATABASE[134], 134, 30070, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ + X(136, &ITEM_DATABASE[135], 135, 28945, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(137, &ITEM_DATABASE[136], 136, 12931, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(138, &ITEM_DATABASE[137], 137, 24780, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(139, &ITEM_DATABASE[138], 138, 10551, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(140, &ITEM_DATABASE[139], 139, 29801, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(141, &ITEM_DATABASE[140], 140, 30753, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(142, &ITEM_DATABASE[141], 141, 30756, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(143, &ITEM_DATABASE[142], 142, 27610, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.400000006f, 0.0f) \ + X(144, &ITEM_DATABASE[143], 143, 12006, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ + X(145, NULL, 255, 6685, -1, 3, 1, 4, 146, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(146, NULL, 255, 6687, -1, 3, 1, 3, 147, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(147, NULL, 255, 6689, -1, 3, 1, 2, 148, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(148, NULL, 255, 6691, -1, 3, 1, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(149, NULL, 255, 3024, -1, 3, 2, 4, 150, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ + X(150, NULL, 255, 3026, -1, 3, 2, 3, 151, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ + X(151, NULL, 255, 3028, -1, 3, 2, 2, 152, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ + X(152, NULL, 255, 3030, -1, 3, 2, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ + X(153, NULL, 255, 10925, -1, 3, 3, 4, 154, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ + X(154, NULL, 255, 10927, -1, 3, 3, 3, 155, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ + X(155, NULL, 255, 10929, -1, 3, 3, 2, 156, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ + X(156, NULL, 255, 10931, -1, 3, 3, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ + X(157, NULL, 255, 12695, -1, 3, 4, 4, 158, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(158, NULL, 255, 12697, -1, 3, 4, 3, 159, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(159, NULL, 255, 12699, -1, 3, 4, 2, 160, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(160, NULL, 255, 12701, -1, 3, 4, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(161, NULL, 255, 23685, -1, 3, 5, 4, 162, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(162, NULL, 255, 23688, -1, 3, 5, 3, 163, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(163, NULL, 255, 23691, -1, 3, 5, 2, 164, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(164, NULL, 255, 23694, -1, 3, 5, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(165, NULL, 255, 2444, -1, 3, 6, 4, 166, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(166, NULL, 255, 169, -1, 3, 6, 3, 167, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(167, NULL, 255, 171, -1, 3, 6, 2, 168, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(168, NULL, 255, 173, -1, 3, 6, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(169, NULL, 255, 23733, -1, 3, 7, 4, 170, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(170, NULL, 255, 23736, -1, 3, 7, 3, 171, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(171, NULL, 255, 23739, -1, 3, 7, 2, 172, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(172, NULL, 255, 23742, -1, 3, 7, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(173, NULL, 255, 30875, -1, 3, 8, 4, 174, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(174, NULL, 255, 30878, -1, 3, 8, 3, 175, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(175, NULL, 255, 30881, -1, 3, 8, 2, 176, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(176, NULL, 255, 30884, -1, 3, 8, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(177, NULL, 255, 4417, -1, 3, 9, 4, 178, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(178, NULL, 255, 4419, -1, 3, 9, 3, 179, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(179, NULL, 255, 4421, -1, 3, 9, 2, 180, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(180, NULL, 255, 4423, -1, 3, 9, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(181, NULL, 255, 27641, -1, 3, 10, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(182, NULL, 255, 12913, -1, 3, 11, 4, 183, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(183, NULL, 255, 12915, -1, 3, 11, 3, 184, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(184, NULL, 255, 12917, -1, 3, 11, 2, 185, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(185, NULL, 255, 12919, -1, 3, 11, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(186, NULL, 255, 2434, -1, 3, 14, 4, 187, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ + X(187, NULL, 255, 139, -1, 3, 14, 3, 188, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ + X(188, NULL, 255, 141, -1, 3, 14, 2, 189, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ + X(189, NULL, 255, 143, -1, 3, 14, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ + X(190, NULL, 255, 22461, -1, 3, 15, 4, 191, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(191, NULL, 255, 22464, -1, 3, 15, 3, 192, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(192, NULL, 255, 22467, -1, 3, 15, 2, 193, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(193, NULL, 255, 22470, -1, 3, 15, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(194, NULL, 255, 12625, -1, 3, 16, 4, 195, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(195, NULL, 255, 12627, -1, 3, 16, 3, 196, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(196, NULL, 255, 12629, -1, 3, 16, 2, 197, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(197, NULL, 255, 12631, -1, 3, 16, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(198, NULL, 255, 385, -1, 2, 12, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.202020198f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(199, NULL, 255, 3144, -1, 2, 13, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.181818187f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(200, NULL, 255, 27281, -1, 0, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) + +#define OSRS_CONSUMABLE_CONTENT_ROWS(X) \ + X(1, 4, 145) \ + X(1, 3, 146) \ + X(1, 2, 147) \ + X(1, 1, 148) \ + X(2, 4, 149) \ + X(2, 3, 150) \ + X(2, 2, 151) \ + X(2, 1, 152) \ + X(3, 4, 153) \ + X(3, 3, 154) \ + X(3, 2, 155) \ + X(3, 1, 156) \ + X(4, 4, 157) \ + X(4, 3, 158) \ + X(4, 2, 159) \ + X(4, 1, 160) \ + X(5, 4, 161) \ + X(5, 3, 162) \ + X(5, 2, 163) \ + X(5, 1, 164) \ + X(6, 4, 165) \ + X(6, 3, 166) \ + X(6, 2, 167) \ + X(6, 1, 168) \ + X(7, 4, 169) \ + X(7, 3, 170) \ + X(7, 2, 171) \ + X(7, 1, 172) \ + X(8, 4, 173) \ + X(8, 3, 174) \ + X(8, 2, 175) \ + X(8, 1, 176) \ + X(9, 4, 177) \ + X(9, 3, 178) \ + X(9, 2, 179) \ + X(9, 1, 180) \ + X(10, 1, 181) \ + X(11, 4, 182) \ + X(11, 3, 183) \ + X(11, 2, 184) \ + X(11, 1, 185) \ + X(14, 4, 186) \ + X(14, 3, 187) \ + X(14, 2, 188) \ + X(14, 1, 189) \ + X(15, 4, 190) \ + X(15, 3, 191) \ + X(15, 2, 192) \ + X(15, 1, 193) \ + X(16, 4, 194) \ + X(16, 3, 195) \ + X(16, 2, 196) \ + X(16, 1, 197) \ + X(12, 0, 198) \ + X(13, 0, 199) + #endif diff --git a/ocean/osrs/osrs_item_obs_table.inc b/ocean/osrs/osrs_item_obs_table.inc index b7099d4635..1be19a639c 100644 --- a/ocean/osrs/osrs_item_obs_table.inc +++ b/ocean/osrs/osrs_item_obs_table.inc @@ -1,203 +1,5 @@ /* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit. - * Included as the initialiser body of a [ROWS][COLS] float array. */ -{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.666666687f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.25f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.666666687f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.666666687f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.550000012f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.5f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.75f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.5f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.0666666701f, 0.5f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.899999976f, 0.666666687f, 0.550000012f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.666666687f, 0.649999976f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.25f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.5f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.349999994f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.333333343f, 0.5f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.400000006f, 0.5f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.600000024f, 0.666666687f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.333333343f, 0.5f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.5f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.13333334f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.400000006f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f}, -{1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.202020198f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.181818187f, 0.0f, 0.0f, 0.0f, 0.0f}, -{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, + * Expands the observation fields from the canonical content rows. */ +#define OSRS_ITEM_CONTENT_OBS_ROW(code, item_pointer, item_idx, raw_osrs_id, gear_slot, click_action, consumable_kind, dose_count, next_content_code, attack_style, ...) {__VA_ARGS__}, +OSRS_ITEM_CONTENT_ROWS(OSRS_ITEM_CONTENT_OBS_ROW) +#undef OSRS_ITEM_CONTENT_OBS_ROW diff --git a/ocean/osrs/osrs_items_generated.h b/ocean/osrs/osrs_items_generated.h index 94e60d83ad..22dcbb1367 100644 --- a/ocean/osrs/osrs_items_generated.h +++ b/ocean/osrs/osrs_items_generated.h @@ -1466,5 +1466,11 @@ static const Item ITEM_DATABASE[NUM_ITEMS] = { .melee_strength = 86, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE }, }; +_Static_assert( + sizeof(ITEM_DATABASE) / sizeof(ITEM_DATABASE[0]) == NUM_ITEMS, + "item database row count must match ItemIndex"); +_Static_assert(NUM_ITEMS < ITEM_NONE, + "generated item indexes must remain distinct from ITEM_NONE"); + #endif diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h index 3027efdf99..b62cf8a693 100644 --- a/ocean/osrs/osrs_render.h +++ b/ocean/osrs/osrs_render.h @@ -6307,14 +6307,16 @@ void pvp_render(OsrsEnv* env) { if (zul_inv) { for (int i = 0; i < OSRS_INVENTORY_SIZE && i < INV_GRID_SLOTS; i++) rc->gui.display_inventory_osrs_ids[i] = - zul_inv->inventory_cells[i].raw_osrs_id; + osrs_inventory_cell_raw_osrs_id( + &zul_inv->inventory_cells[i]); rc->gui.display_inventory_count = OSRS_INVENTORY_SIZE; } InfernoState* inf_inv = render_inferno_state_from_env(env); if (inf_inv) { for (int i = 0; i < OSRS_INVENTORY_SIZE && i < INV_GRID_SLOTS; i++) rc->gui.display_inventory_osrs_ids[i] = - inf_inv->inventory_cells[i].raw_osrs_id; + osrs_inventory_cell_raw_osrs_id( + &inf_inv->inventory_cells[i]); rc->gui.display_inventory_count = OSRS_INVENTORY_SIZE; } } diff --git a/ocean/osrs/tests/probe_colo_best_gear_dpt.c b/ocean/osrs/tests/probe_colo_best_gear_dpt.c index 1d94a8ea66..8ba42c3cc9 100644 --- a/ocean/osrs/tests/probe_colo_best_gear_dpt.c +++ b/ocean/osrs/tests/probe_colo_best_gear_dpt.c @@ -112,7 +112,8 @@ static void test_beats_worn_single_swap(void) { } } for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { - uint8_t w = s.inventory_cells[cell].item_idx; + uint8_t w = + osrs_inventory_cell_item_index(&s.inventory_cells[cell]); if (w != ITEM_NONE && item_is_weapon(w) && !add_seen[w]) { add_seen[w] = 1; weapons[nweap++] = w; } @@ -156,7 +157,11 @@ static void test_equip_and_augury_same_tick(void) { int shadow_cell = -1; for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) - if (s.inventory_cells[cell].item_idx == ITEM_TUMEKENS_SHADOW) { shadow_cell = cell; break; } + if (osrs_inventory_cell_item_index( + &s.inventory_cells[cell]) == ITEM_TUMEKENS_SHADOW) { + shadow_cell = cell; + break; + } CHECK("T4 shadow is in the bag", shadow_cell >= 0); if (shadow_cell < 0) return; @@ -239,8 +244,9 @@ static void test_per_cell_marginal_bit(void) { float magic_dpt_with = best[COLO_GEAR_MAGIC][COLO_FREMENNIK_BERSERKER].dpt; for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) - if (s.inventory_cells[cell].item_idx == ITEM_TUMEKENS_SHADOW) - s.inventory_cells[cell].item_idx = ITEM_NONE; + if (osrs_inventory_cell_item_index( + &s.inventory_cells[cell]) == ITEM_TUMEKENS_SHADOW) + s.inventory_cells[cell] = osrs_inventory_cell_empty(); for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) if (s.player.equipped[slot] == ITEM_TUMEKENS_SHADOW) s.player.equipped[slot] = ITEM_NONE; diff --git a/ocean/osrs/tests/probe_colo_wave_advance.c b/ocean/osrs/tests/probe_colo_wave_advance.c index cd284d5e80..c252aefdf4 100644 --- a/ocean/osrs/tests/probe_colo_wave_advance.c +++ b/ocean/osrs/tests/probe_colo_wave_advance.c @@ -58,8 +58,9 @@ static int scripted_first_modifier(const ColosseumState* s) { static int scripted_heal_cell(const ColosseumState* s) { for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { const ColoInvCell* cell = &s->inventory_cells[i]; - OsrsInventoryClickResolution r = osrs_inventory_click_interpret( - cell->item_idx, cell->raw_osrs_id, OSRS_CLICK_TICK_FIRST); + OsrsInventoryClickResolution r = + osrs_inventory_cell_click_interpret( + cell, OSRS_CLICK_TICK_FIRST); if ((r.consumable_kind == OSRS_CONSUMABLE_BREW || r.consumable_kind == OSRS_CONSUMABLE_GUTHIX_REST || r.consumable_kind == OSRS_CONSUMABLE_SHARK_FOOD || diff --git a/ocean/osrs/tests/probe_colosseum_winnability.c b/ocean/osrs/tests/probe_colosseum_winnability.c index 2dd9c40a3f..7edc48c0d1 100644 --- a/ocean/osrs/tests/probe_colosseum_winnability.c +++ b/ocean/osrs/tests/probe_colosseum_winnability.c @@ -85,8 +85,9 @@ static int scripted_least_bad_modifier(const ColosseumState* s) { static int scripted_heal_cell(const ColosseumState* s) { for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { const ColoInvCell* cell = &s->inventory_cells[i]; - OsrsInventoryClickResolution r = osrs_inventory_click_interpret( - cell->item_idx, cell->raw_osrs_id, OSRS_CLICK_TICK_FIRST); + OsrsInventoryClickResolution r = + osrs_inventory_cell_click_interpret( + cell, OSRS_CLICK_TICK_FIRST); if ((r.consumable_kind == OSRS_CONSUMABLE_BREW || r.consumable_kind == OSRS_CONSUMABLE_GUTHIX_REST || r.consumable_kind == OSRS_CONSUMABLE_SHARK_FOOD || diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index 1c6846c87b..1104ffa8d1 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -194,7 +194,8 @@ static int test_obs_slot_for_npc(const ColosseumState* s, int npc_idx) { static int test_find_inventory_cell_with_item(const ColosseumState* s, uint8_t item_idx) { for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) - if (s->inventory_cells[i].item_idx == item_idx) return i; + if (osrs_inventory_cell_item_index( + &s->inventory_cells[i]) == item_idx) return i; return -1; } @@ -218,7 +219,8 @@ static void test_click_inventory_cell_action_s( OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret(&s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); if (r.click_action == OSRS_CLICK_EQUIP) { - int slot = osrs_item_gear_slot(s->inventory_cells[cell].item_idx); + int slot = + osrs_inventory_cell_metadata(&s->inventory_cells[cell])->gear_slot; assert(slot >= 0 && slot < NUM_GEAR_SLOTS); actions[COLO_HEAD_EQUIP_SLOT(slot)] = cell + 1; } else if (r.click_action == OSRS_CLICK_EAT) { @@ -245,7 +247,8 @@ static float test_click_mask_for_cell_s( osrs_inventory_cell_click_interpret(&s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); int head; if (r.click_action == OSRS_CLICK_EQUIP) { - int slot = osrs_item_gear_slot(s->inventory_cells[cell].item_idx); + int slot = + osrs_inventory_cell_metadata(&s->inventory_cells[cell])->gear_slot; head = (slot >= 0 && slot < NUM_GEAR_SLOTS) ? COLO_HEAD_EQUIP_SLOT(slot) : COLO_HEAD_EQUIP_SLOT(0); } else if (r.click_action == OSRS_CLICK_EAT) { @@ -267,7 +270,8 @@ static int test_sum_inventory_doses_for_kind( const ColoInvCell* cell = &s->inventory_cells[i]; OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); - if (r.consumable_kind == kind) doses += cell->dose; + if (r.consumable_kind == kind) + doses += osrs_inventory_cell_dose_count(cell); } return doses; } @@ -301,6 +305,8 @@ static int test_aggregate_doses_for_kind( case OSRS_CONSUMABLE_BASTION: case OSRS_CONSUMABLE_STAMINA: return -1; + case OSRS_CONSUMABLE_COUNT: + break; } abort(); } @@ -391,22 +397,23 @@ static TestDroppedInventoryFields test_expected_dropped_inventory_fields( int cell_idx ) { const ColoInvCell* cell = &s->inventory_cells[cell_idx]; - OsrsConsumableClick consumable = - osrs_consumable_click_lookup_raw_osrs_id(cell->raw_osrs_id); - uint32_t effect_mask = OSRS_ITEM_EFFECT_NONE; - if (cell->item_idx != ITEM_NONE) { - if (cell->item_idx >= NUM_ITEMS) abort(); - effect_mask = ITEM_DATABASE[cell->item_idx].effect_mask; - } - OsrsConsumableKind6 k6 = col_consumable_kind6(consumable.consumable_kind); + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(cell); + uint32_t effect_mask = metadata->item == NULL + ? OSRS_ITEM_EFFECT_NONE + : metadata->item->effect_mask; + OsrsConsumableKind6 k6 = col_consumable_kind6( + (OsrsConsumableKind)metadata->consumable_kind); return (TestDroppedInventoryFields){ - .is_gear = test_binary_float(cell->item_idx != ITEM_NONE), - .is_consumable = test_binary_float(consumable.click_action != OSRS_CLICK_NONE), + .is_gear = test_binary_float(metadata->item != NULL), + .is_consumable = test_binary_float( + metadata->consumable_kind != OSRS_CONSUMABLE_NONE), .can_use = test_binary_float(col_inventory_cell_actionable(s, cell_idx)), .has_effect = test_binary_float(effect_mask != OSRS_ITEM_EFFECT_NONE), - .role_food = test_binary_float(consumable.click_action == OSRS_CLICK_EAT), - .role_potion_family = test_binary_float(consumable.click_action == OSRS_CLICK_DRINK), + .role_food = test_binary_float(metadata->click_action == OSRS_CLICK_EAT), + .role_potion_family = test_binary_float( + metadata->click_action == OSRS_CLICK_DRINK), .kind_food = test_binary_float(k6 == COL_CKIND6_FOOD), }; } @@ -495,7 +502,8 @@ static int test_count_item_in_equipment_and_inventory( for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) if (s->player.equipped[slot] == item_idx) count++; for (int slot = 0; slot < COLO_INVENTORY_DISPLAY_SLOTS; slot++) - if (s->inventory_cells[slot].item_idx == item_idx) count++; + if (osrs_inventory_cell_item_index( + &s->inventory_cells[slot]) == item_idx) count++; return count; } @@ -3331,7 +3339,7 @@ static void test_manticore_telegraph_during_windup(void) { static int late_start_total_doses(const ColosseumState* s) { int total = 0; for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) - total += s->inventory_cells[c].dose; + total += osrs_inventory_cell_dose_count(&s->inventory_cells[c]); return total; } @@ -5334,8 +5342,8 @@ static void test_loadout_divine_potions_and_stat_drift(void) { s.divine_ranged_timer = 234; ColoSnapshot snap; col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); - CHECK("snapshot version is v23 for interaction route state", - snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 23u); + CHECK("snapshot version is v24 for canonical inventory identity", + snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 24u); ColosseumState restored; memset(&restored, 0, sizeof(restored)); col_restore_ctx((EncounterState*)&restored, (EncounterContext*)&ctx, &snap, sizeof(snap)); @@ -6166,7 +6174,7 @@ static void test_combat_fidelity_contract_sizes(void) { CHECK("modifier block has 60 features", COLO_MODIFIER_OBS_SIZE == 60); CHECK("NPC slots carry a type code, not a type one-hot", COLO_FEATURES_PER_NPC == 23); - CHECK("snapshot version is v23", COLO_SNAPSHOT_VERSION == 23u); + CHECK("snapshot version is v24", COLO_SNAPSHOT_VERSION == 24u); CHECK("every active NPC gets an obs slot (no busy-wave drop)", COLO_OBS_NPCS == 24 && COLO_OBS_NPCS == COLO_MAX_NPCS); CHECK("PRIMARY head covers noop, movement, and NPC obs slots", @@ -6838,7 +6846,7 @@ static void test_combat_fidelity_snapshot_roundtrip(void) { ColoSnapshot snap; col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); - CHECK("snapshot frame is v23", snap.version == 23u); + CHECK("snapshot frame is v24", snap.version == 24u); ColosseumState restored; memset(&restored, 0, sizeof(restored)); @@ -7640,7 +7648,8 @@ static void test_colosseum_inventory_memo_tracks_gear_swaps(void) { int cell = -1, slot = -1; for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) { - uint8_t it = s.inventory_cells[c].item_idx; + uint8_t it = + osrs_inventory_cell_item_index(&s.inventory_cells[c]); if (it == ITEM_NONE) continue; int gs = osrs_item_gear_slot(it); if (gs < 0) continue; @@ -7654,7 +7663,8 @@ static void test_colosseum_inventory_memo_tracks_gear_swaps(void) { col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_a); /* Equip it directly. No cell changes, so the memo key is untouched. */ - s.player.equipped[slot] = s.inventory_cells[cell].item_idx; + s.player.equipped[slot] = + osrs_inventory_cell_item_index(&s.inventory_cells[cell]); CHECK("a gear swap alone does not move the memo key", col_inventory_obs_signature(&s) == sig_before); @@ -7762,11 +7772,8 @@ static void test_colosseum_live_inventory_display(void) { col_build_live_inventory_display(&s, kit); CHECK("surge vial shows 3-dose after a drink", kit[12] == 30878); - s.inventory_cells[brew_cell] = (ColoInvCell){ - .raw_osrs_id = 6691, - .item_idx = ITEM_NONE, - .dose = 1, - }; + s.inventory_cells[brew_cell] = + osrs_inventory_cell_from_raw_osrs_id(6691); s.player.potion_timer = 0; s.player.current_hitpoints = 50; int last_brew[COLO_NUM_ACTION_HEADS] = {0}; @@ -7796,7 +7803,8 @@ static void test_stage3_t1_inventory_ranged_weapon_swap(void) { CHECK("T1 ranged weapon click equips tbow", s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_TWISTED_BOW); CHECK("T1 displaced melee weapon returns to clicked cell", - s.inventory_cells[bow_cell].item_idx == melee_weapon); + osrs_inventory_cell_item_index( + &s.inventory_cells[bow_cell]) == melee_weapon); } static void test_stage3_t1_inventory_weapon_slot_last_click_wins(void) { @@ -7813,7 +7821,8 @@ static void test_stage3_t1_inventory_weapon_slot_last_click_wins(void) { CHECK("weapon equip head equips the named weapon", s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_DRAGON_CLAWS); CHECK("unclicked weapon stays in its inventory cell", - s.inventory_cells[bow_cell].item_idx == ITEM_TWISTED_BOW); + osrs_inventory_cell_item_index( + &s.inventory_cells[bow_cell]) == ITEM_TWISTED_BOW); } static void test_stage3_t1_human_inventory_primary_click_uses_resolver(void) { @@ -7837,7 +7846,8 @@ static void test_stage3_t1_human_inventory_primary_click_uses_resolver(void) { CHECK("human inventory clicks use last-click-wins semantics", s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_DRAGON_CLAWS); CHECK("human earlier same-slot click is ignored", - s.inventory_cells[bow_cell].item_idx == ITEM_TWISTED_BOW); + osrs_inventory_cell_item_index( + &s.inventory_cells[bow_cell]) == ITEM_TWISTED_BOW); human_input_destroy(&hi); } @@ -7856,15 +7866,17 @@ static void test_stage3_t1_human_rearrange_swaps_inventory_slots(void) { &hi, bow_cell, claws_cell, - s.inventory_cells[bow_cell].item_idx, - s.inventory_cells[bow_cell].raw_osrs_id); + osrs_inventory_cell_item_index(&s.inventory_cells[bow_cell]), + osrs_inventory_cell_raw_osrs_id(&s.inventory_cells[bow_cell])); int actions[COLO_NUM_ACTION_HEADS] = {0}; col_translate_human_commands_ctx(&hi, actions, &s, &ctx); CHECK("human rearrange moves bow to target slot", - s.inventory_cells[claws_cell].item_idx == ITEM_TWISTED_BOW); + osrs_inventory_cell_item_index( + &s.inventory_cells[claws_cell]) == ITEM_TWISTED_BOW); CHECK("human rearrange moves claws to source slot", - s.inventory_cells[bow_cell].item_idx == ITEM_DRAGON_CLAWS); + osrs_inventory_cell_item_index( + &s.inventory_cells[bow_cell]) == ITEM_DRAGON_CLAWS); CHECK("human rearrange leaves action space heads unchanged", COLO_NUM_ACTION_HEADS == 20 && COLO_ACTION_DIMS[COLO_HEAD_EQUIP_BASE] == 29); human_input_destroy(&hi); @@ -7882,8 +7894,8 @@ static void test_stage3_t2_brew_click_decrements_dose(void) { step_and_observe(&s, &ctx, actions); CHECK("T2 brew raises HP", s.player.current_hitpoints > 50); CHECK("T2 brew cell dose drops 4 to 3", - s.inventory_cells[brew_cell].dose == 3 && - s.inventory_cells[brew_cell].raw_osrs_id == 6687); + osrs_inventory_cell_dose_count(&s.inventory_cells[brew_cell]) == 3 && + osrs_inventory_cell_raw_osrs_id(&s.inventory_cells[brew_cell]) == 6687); CHECK("T2 brew starts potion timer", s.player.potion_timer == 3); } @@ -7893,19 +7905,14 @@ static void test_stage3_t3_one_dose_vial_empties(void) { ColosseumState s; loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 503); int brew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); - s.inventory_cells[brew_cell] = (ColoInvCell){ - .raw_osrs_id = 6691, - .item_idx = ITEM_NONE, - .dose = 1, - }; + s.inventory_cells[brew_cell] = + osrs_inventory_cell_from_raw_osrs_id(6691); s.player.current_hitpoints = 50; int actions[COLO_NUM_ACTION_HEADS] = {0}; test_click_inventory_cell_action_s(&s, actions, brew_cell); step_and_observe(&s, &ctx, actions); CHECK("T3 one-dose vial cell becomes empty", - s.inventory_cells[brew_cell].raw_osrs_id == 0 && - s.inventory_cells[brew_cell].item_idx == ITEM_NONE && - s.inventory_cells[brew_cell].dose == 0); + osrs_inventory_cell_is_empty(&s.inventory_cells[brew_cell])); } static void test_colosseum_potion_click_source_of_truth(void) { @@ -7924,7 +7931,8 @@ static void test_colosseum_potion_click_source_of_truth(void) { test_click_inventory_cell_action_s(&s, actions, restore_cell); step_and_observe(&s, &ctx, actions); CHECK("super restore consumes exactly one clicked-cell dose", - s.inventory_cells[restore_cell].dose == 3); + osrs_inventory_cell_dose_count( + &s.inventory_cells[restore_cell]) == 3); CHECK("super restore aggregate is rebuilt from cells", s.player.restore_doses == restore_sum_before - 1); @@ -7937,7 +7945,8 @@ static void test_colosseum_potion_click_source_of_truth(void) { test_click_inventory_cell_action_s(&s, actions, sanfew_cell); step_and_observe(&s, &ctx, actions); CHECK("sanfew consumes exactly one clicked-cell dose", - s.inventory_cells[sanfew_cell].dose == 3); + osrs_inventory_cell_dose_count( + &s.inventory_cells[sanfew_cell]) == 3); CHECK("sanfew aggregate is rebuilt from cells", s.player.restore_doses == sanfew_sum_before - 1); @@ -7952,7 +7961,8 @@ static void test_colosseum_potion_click_source_of_truth(void) { test_click_inventory_cell_action_s(&s, actions, divine_cell); step_and_observe(&s, &ctx, actions); CHECK("divine combat consumes exactly one clicked-cell dose", - s.inventory_cells[divine_cell].dose == 3); + osrs_inventory_cell_dose_count( + &s.inventory_cells[divine_cell]) == 3); CHECK("divine combat aggregate is rebuilt from cells", s.player.combat_potion_doses == divine_sum_before - 1); CHECK("divine combat drink starts the potion timer", s.player.potion_timer == 3); @@ -7967,7 +7977,8 @@ static void test_colosseum_potion_click_source_of_truth(void) { test_click_inventory_cell_action_s(&s, actions, restore_cell); step_and_observe(&s, &ctx, actions); CHECK("low-missing-prayer restore click still consumes one dose", - s.inventory_cells[restore_cell].dose == 3); + osrs_inventory_cell_dose_count( + &s.inventory_cells[restore_cell]) == 3); CHECK("low-missing-prayer restore rebuilds aggregate from cells", s.player.restore_doses == restore_sum_before - 1); } @@ -7983,12 +7994,14 @@ static void test_colosseum_potion_timer_and_same_tick_gate(void) { int brew[COLO_NUM_ACTION_HEADS] = {0}; test_click_inventory_cell_action_s(&s, brew, brew_cell); step_and_observe(&s, &ctx, brew); - int brew_dose_after_first = s.inventory_cells[brew_cell].dose; + int brew_dose_after_first = + osrs_inventory_cell_dose_count(&s.inventory_cells[brew_cell]); int brew_aggregate_after_first = s.player.brew_doses; s.player.current_hitpoints = 50; step_and_observe(&s, &ctx, brew); CHECK("second potion click before timer expiry is blocked", - s.inventory_cells[brew_cell].dose == brew_dose_after_first && + osrs_inventory_cell_dose_count( + &s.inventory_cells[brew_cell]) == brew_dose_after_first && s.player.brew_doses == brew_aggregate_after_first && s.player.potion_timer == 2); @@ -8002,8 +8015,10 @@ static void test_colosseum_potion_timer_and_same_tick_gate(void) { drink_one[COLO_HEAD_DRINK] = divine_cell + 1; step_and_observe(&s, &ctx, drink_one); CHECK("the drink head consumes exactly one potion per tick", - s.inventory_cells[divine_cell].dose == 3 && - s.inventory_cells[sanfew_cell].dose == 4); + osrs_inventory_cell_dose_count( + &s.inventory_cells[divine_cell]) == 3 && + osrs_inventory_cell_dose_count( + &s.inventory_cells[sanfew_cell]) == 4); loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 515); sanfew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SANFEW); @@ -8012,10 +8027,13 @@ static void test_colosseum_potion_timer_and_same_tick_gate(void) { int drink_again[COLO_NUM_ACTION_HEADS] = {0}; drink_again[COLO_HEAD_DRINK] = sanfew_cell + 1; step_and_observe(&s, &ctx, drink_again); - CHECK("first drink consumes one dose", s.inventory_cells[sanfew_cell].dose == 3); + CHECK("first drink consumes one dose", + osrs_inventory_cell_dose_count( + &s.inventory_cells[sanfew_cell]) == 3); step_and_observe(&s, &ctx, drink_again); CHECK("second drink before potion timer expiry is blocked", - s.inventory_cells[sanfew_cell].dose == 3); + osrs_inventory_cell_dose_count( + &s.inventory_cells[sanfew_cell]) == 3); } typedef struct { @@ -8065,8 +8083,10 @@ static void test_colosseum_all_drink_kinds_shared_one_dose_path(void) { char label[160]; snprintf(label, sizeof(label), "%s first click decrements one dose", c->label); CHECK(label, - s.inventory_cells[cell].raw_osrs_id == expected_after_first && - s.inventory_cells[cell].dose == expected_dose_after_first); + osrs_inventory_cell_raw_osrs_id( + &s.inventory_cells[cell]) == expected_after_first && + osrs_inventory_cell_dose_count( + &s.inventory_cells[cell]) == expected_dose_after_first); snprintf(label, sizeof(label), "%s first click arms potion timer", c->label); CHECK(label, s.player.potion_timer == 3); int aggregate = test_aggregate_doses_for_kind(&s, c->kind); @@ -8079,13 +8099,17 @@ static void test_colosseum_all_drink_kinds_shared_one_dose_path(void) { if (!c->raw4) { s.inventory_cells[cell] = osrs_inventory_cell_from_raw_osrs_id(c->raw1); } - uint16_t raw_before_gate = s.inventory_cells[cell].raw_osrs_id; - uint8_t dose_before_gate = s.inventory_cells[cell].dose; + uint16_t raw_before_gate = + osrs_inventory_cell_raw_osrs_id(&s.inventory_cells[cell]); + uint8_t dose_before_gate = + osrs_inventory_cell_dose_count(&s.inventory_cells[cell]); step_and_observe(&s, &ctx, actions); snprintf(label, sizeof(label), "%s timer gate blocks next click", c->label); CHECK(label, - s.inventory_cells[cell].raw_osrs_id == raw_before_gate && - s.inventory_cells[cell].dose == dose_before_gate); + osrs_inventory_cell_raw_osrs_id( + &s.inventory_cells[cell]) == raw_before_gate && + osrs_inventory_cell_dose_count( + &s.inventory_cells[cell]) == dose_before_gate); const uint16_t chain[] = {c->raw4, c->raw3, c->raw2, c->raw1, 0}; int start = c->raw4 ? 0 : 3; @@ -8097,10 +8121,12 @@ static void test_colosseum_all_drink_kinds_shared_one_dose_path(void) { test_click_inventory_cell_action_s(&s, actions, cell); step_and_observe(&s, &ctx, actions); snprintf(label, sizeof(label), "%s chain step %d raw id", c->label, step); - CHECK(label, s.inventory_cells[cell].raw_osrs_id == chain[step + 1]); + CHECK(label, osrs_inventory_cell_raw_osrs_id( + &s.inventory_cells[cell]) == chain[step + 1]); uint8_t expected_dose = chain[step + 1] == 0 ? 0 : (uint8_t)(3 - step); snprintf(label, sizeof(label), "%s chain step %d dose", c->label, step); - CHECK(label, s.inventory_cells[cell].dose == expected_dose); + CHECK(label, osrs_inventory_cell_dose_count( + &s.inventory_cells[cell]) == expected_dose); } } } @@ -8182,7 +8208,7 @@ static void test_stage3_t4_click_mask_bits(void) { loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 504); int equipped_scythe_cell = test_find_inventory_cell_with_item(&s, ITEM_SCYTHE_OF_VITUR); int empty_cell = 27; - s.inventory_cells[empty_cell] = (ColoInvCell){ .item_idx = ITEM_NONE }; + s.inventory_cells[empty_cell] = osrs_inventory_cell_empty(); int brew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); s.player.current_hitpoints = 50; float mask[COLO_ACTION_MASK_SIZE]; @@ -8408,7 +8434,8 @@ static void test_modifier_draft_forces_pick(void) { static int colo_test_cell_of_named_item(const ColosseumState* s, const char* name) { for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - uint8_t item = s->inventory_cells[c].item_idx; + uint8_t item = + osrs_inventory_cell_item_index(&s->inventory_cells[c]); if (item == ITEM_NONE) continue; const Item* meta = get_item(item); if (meta && strcmp(meta->name, name) == 0) return c; @@ -8431,19 +8458,20 @@ static float test_through_bf16(float v) { static void test_item_obs_code_survives_bf16(void) { printf("test_item_obs_code_survives_bf16\n"); int worst = -1; - for (int code = 0; code < OSRS_INVENTORY_CELL_OBS_CODE_COUNT; code++) { + for (int code = 0; code < OSRS_ITEM_CONTENT_COUNT; code++) { float observed = test_through_bf16(osrs_inventory_cell_obs_code_encode(code)); if (osrs_inventory_cell_obs_code_decode(observed) != code) worst = code; } CHECK("every item code round-trips through bf16", worst < 0); CHECK("the code stays inside the unit range the rest of the observation lives in", - osrs_inventory_cell_obs_code_encode(OSRS_INVENTORY_CELL_OBS_CODE_COUNT - 1) <= 1.0f); + osrs_inventory_cell_obs_code_encode( + OSRS_ITEM_CONTENT_COUNT - 1) <= 1.0f); } static void test_item_obs_table_matches_every_code_semantically(void) { printf("test_item_obs_table_matches_every_code_semantically\n"); CHECK("generic item table has one row for every inventory item code", - OSRS_ITEM_OBS_TABLE_ROWS == OSRS_INVENTORY_CELL_OBS_CODE_COUNT); + OSRS_ITEM_OBS_TABLE_ROWS == OSRS_ITEM_CONTENT_COUNT); CHECK("generic item rows contain the full 15-float compact record", OSRS_ITEM_OBS_TABLE_COLS == 15); CHECK("generic item rows bake hitpoints at 99", @@ -8452,18 +8480,17 @@ static void test_item_obs_table_matches_every_code_semantically(void) { OSRS_ITEM_OBS_TABLE_BASE_PRAYER == 99); CHECK("generic item rows bake ranged at 99", OSRS_ITEM_OBS_TABLE_BASE_RANGED == 99); - CHECK("generic item code scale matches the inventory observation encoding", - OSRS_ITEM_OBS_CODE_SCALE == OSRS_INVENTORY_CELL_OBS_CODE_SCALE); CHECK("generic equipped overlay targets the compact equipped feature", OSRS_ITEM_OBS_OVERLAY_EQUIPPED == OSRS_INVENTORY_CELL_COMPACT_EQUIPPED); CHECK("generic HP-heal overlay targets the compact HP-heal feature", OSRS_ITEM_OBS_OVERLAY_HP_HEAL == OSRS_INVENTORY_CELL_COMPACT_HP_HEAL); for (int code = 0; code < OSRS_ITEM_OBS_TABLE_ROWS; code++) { - OsrsInventoryCell cell = osrs_inventory_cell_for_obs_code(code); + OsrsInventoryCell cell = + osrs_inventory_cell_from_content_code((uint16_t)code); float expected[OSRS_ITEM_OBS_TABLE_COLS]; osrs_write_inventory_cell_affordance_features_compact( - expected, cell.item_idx, cell.raw_osrs_id, cell.dose, 0, + expected, &cell, 0, OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS, OSRS_ITEM_OBS_TABLE_BASE_PRAYER, OSRS_ITEM_OBS_TABLE_BASE_RANGED); @@ -8484,17 +8511,19 @@ static void test_item_obs_empty_row_is_all_zero(void) { snprintf(label, sizeof(label), "empty item code feature %d is zero", feature); CHECK(label, - TEST_ITEM_OBS_TABLE[OSRS_INVENTORY_CELL_OBS_CODE_EMPTY][feature] == 0.0f); + TEST_ITEM_OBS_TABLE[0][feature] == 0.0f); } } static void test_item_obs_table_bakes_consumable_hp_heal(void) { printf("test_item_obs_table_bakes_consumable_hp_heal\n"); - int shark_code = osrs_inventory_cell_obs_code(ITEM_NONE, 385); - OsrsInventoryCell shark = osrs_inventory_cell_for_obs_code(shark_code); + int shark_code = + osrs_inventory_content_code_from_raw_osrs_id(385); + OsrsInventoryCell shark = + osrs_inventory_cell_from_content_code((uint16_t)shark_code); float expected[OSRS_ITEM_OBS_TABLE_COLS]; osrs_write_inventory_cell_affordance_features_compact( - expected, shark.item_idx, shark.raw_osrs_id, shark.dose, 0, + expected, &shark, 0, OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS, OSRS_ITEM_OBS_TABLE_BASE_PRAYER, OSRS_ITEM_OBS_TABLE_BASE_RANGED); @@ -8551,9 +8580,12 @@ static void test_inventory_obs_expansion_is_lossless(void) { const ColoInvCell* c = &s.inventory_cells[cell]; float want[COLO_INVENTORY_CELL_ENCODER_FEATURES]; osrs_write_inventory_cell_affordance_features_compact( - want, c->item_idx, c->raw_osrs_id, c->dose, - osrs_inventory_cell_holds_equipped_item(&s.player, s.inventory_cells, cell), - s.player.base_hitpoints, s.player.base_prayer, s.player.base_attack); + want, c, + osrs_inventory_cell_holds_equipped_item( + &s.player, s.inventory_cells, cell), + s.player.base_hitpoints, + s.player.base_prayer, + s.player.base_attack); float got[COLO_INVENTORY_CELL_ENCODER_FEATURES]; test_expand_inventory_cell(obs, cell, got); @@ -8584,10 +8616,16 @@ static void test_inventory_obs_memo(void) { static float obs[COLO_NUM_OBS]; int cell_brew = -1; for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) - if (s.inventory_cells[c].item_idx == ITEM_NONE) { cell_brew = c; break; } + if (osrs_inventory_cell_item_index( + &s.inventory_cells[c]) == ITEM_NONE) { + cell_brew = c; + break; + } CHECK("a gear-free cell exists to hold the brew", cell_brew >= 0); s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6685); - CHECK("4-dose brew seeded", s.inventory_cells[cell_brew].dose == 4); + CHECK("4-dose brew seeded", + osrs_inventory_cell_dose_count( + &s.inventory_cells[cell_brew]) == 4); /* Dose rides the item code now: a potion's raw id names its dose, so the sip lands a different code and the expanded record picks the dose up from the table row. */ @@ -8598,7 +8636,9 @@ static void test_inventory_obs_memo(void) { expanded[OSRS_INVENTORY_CELL_COMPACT_DOSE] == 1.0f); s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6687); - CHECK("sip took a dose", s.inventory_cells[cell_brew].dose == 3); + CHECK("sip took a dose", + osrs_inventory_cell_dose_count( + &s.inventory_cells[cell_brew]) == 3); col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); test_expand_inventory_cell(obs, cell_brew, expanded); CHECK("sip moves the dose feature (dose is in the memo key, not a stale block)", @@ -8623,9 +8663,11 @@ static void test_best_gear_cache_signatures(void) { int weapon_cells[2] = {-1, -1}; int weapon_count = 0; for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { - uint8_t item = s.inventory_cells[cell].item_idx; + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&s.inventory_cells[cell]); + uint8_t item = metadata->item_idx; if (item == ITEM_NONE) { - if (s.inventory_cells[cell].raw_osrs_id != 0 && non_gear_cell < 0) + if (metadata->raw_osrs_id != 0 && non_gear_cell < 0) non_gear_cell = cell; continue; } @@ -8748,6 +8790,24 @@ static void child_col_query_before_topology_finalize(void) { col_init_unfinalized_context(&ctx); (void)col_topology_tile_blocked(&ctx, 10, 10); } +static void child_col_restore_previous_inventory_snapshot(void) { + ColosseumContext ctx; + ColosseumState state; + col_init_context_typed(&ctx); + col_init_state_typed(&state, &ctx); + col_reset_ctx( + (EncounterState*)&state, (EncounterContext*)&ctx, 123u); + ColoSnapshot snapshot; + col_snapshot_ctx( + (EncounterState*)&state, (EncounterContext*)&ctx, &snapshot); + snapshot.version = COLO_SNAPSHOT_VERSION - 1u; + col_restore_ctx( + (EncounterState*)&state, + (EncounterContext*)&ctx, + &snapshot, + sizeof(snapshot)); +} + static void test_colosseum_topology_lifecycle_contract(void) { printf("test_colosseum_topology_lifecycle_contract\n"); @@ -8763,6 +8823,9 @@ static void test_colosseum_topology_lifecycle_contract(void) { assert_child_aborts( "Colosseum query before topology finalize aborts", child_col_query_before_topology_finalize); + assert_child_aborts( + "Colosseum previous inventory snapshot version aborts", + child_col_restore_previous_inventory_snapshot); } int main(void) { diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index dd85a1bca1..60a4517798 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -582,7 +582,7 @@ static int test_occupied_inventory_cells(const InfernoState* s) { static int test_cell_holding_item(const InfernoState* s, uint8_t item) { for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - if (s->inventory_cells[c].item_idx == item) + if (osrs_inventory_cell_item_index(&s->inventory_cells[c]) == item) return c; } return -1; @@ -592,10 +592,10 @@ static int test_cell_doses_of_kind(const InfernoState* s, OsrsConsumableKind kin int doses = 0; for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { const OsrsInventoryCell* cell = &s->inventory_cells[c]; - if (cell->raw_osrs_id == 0) continue; + if (osrs_inventory_cell_raw_osrs_id(cell) == 0) continue; if (osrs_consumable_click_lookup_raw_osrs_id( - cell->raw_osrs_id).consumable_kind == kind) - doses += cell->dose; + osrs_inventory_cell_raw_osrs_id(cell)).consumable_kind == kind) + doses += osrs_inventory_cell_dose_count(cell); } return doses; } @@ -603,7 +603,7 @@ static int test_cell_doses_of_kind(const InfernoState* s, OsrsConsumableKind kin static OsrsConsumableKind test_drink_click_kind(const InfernoState* s, int action) { if (action <= 0) return OSRS_CONSUMABLE_NONE; return osrs_consumable_click_lookup_raw_osrs_id( - s->inventory_cells[action - 1].raw_osrs_id).consumable_kind; + osrs_inventory_cell_raw_osrs_id(&s->inventory_cells[action - 1])).consumable_kind; } static void test_final_wave_reward_applies_healer_tags_and_heal_cost(void) { @@ -6892,13 +6892,13 @@ static void child_inf_pending_spark_overflow(void) { inf_queue_pending_spark(&state, 0, 0, 20, 20, 1, 1); } -static void child_inf_restore_v1_snapshot(void) { +static void child_inf_restore_previous_inventory_snapshot(void) { EncounterState* raw = inf_create(); inf_reset(raw, 123u); size_t snap_size = inf_snapshot_size(raw); InfSnapshot* snap = (InfSnapshot*)malloc(snap_size); inf_snapshot(raw, snap); - snap->version = 1u; + snap->version = INF_SNAPSHOT_VERSION - 1u; inf_restore(raw, snap, snap_size); free(snap); inf_destroy(raw); @@ -6970,7 +6970,9 @@ static void test_fail_fast_boundaries(void) { assert_child_aborts( "inferno query before topology finalize aborts", child_inf_query_before_topology_finalize); - assert_child_aborts("inferno v1 snapshot restore aborts", child_inf_restore_v1_snapshot); + assert_child_aborts( + "inferno previous inventory snapshot restore aborts", + child_inf_restore_previous_inventory_snapshot); assert_child_aborts( "inferno wrong-config snapshot restore aborts", child_inf_restore_wrong_config_snapshot); @@ -7441,7 +7443,7 @@ static void test_inferno_snapshot_restore_round_trip(void) { ASSERT_INT_EQ("snapshot magic stamped", (int)snap_A->magic, (int)INF_SNAPSHOT_MAGIC); ASSERT_INT_EQ("snapshot version stamped", - (int)snap_A->version, (int)INF_SNAPSHOT_VERSION); + (int)snap_A->version, 22); const int N2 = 18; for (int i = 0; i < N2; i++) inf_step(raw, actions_b); @@ -9294,9 +9296,9 @@ static void test_compact_transient_inventory_equipment_semantics(void) { for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { const OsrsInventoryCell* inventory_cell = &state.inventory_cells[cell]; - float expected_code = osrs_inventory_cell_obs_code_encode( - osrs_inventory_cell_obs_code( - inventory_cell->item_idx, inventory_cell->raw_osrs_id)); + float expected_code = + osrs_inventory_cell_obs_code_encode( + inventory_cell->content_code); ASSERT_FLOAT_NEAR("compact inventory cell code", obs_off[INF_OBS_AFTER_SPARKS + cell], expected_code, 1e-6f); } diff --git a/ocean/osrs/tests/test_osrs_inventory_actions.c b/ocean/osrs/tests/test_osrs_inventory_actions.c index 7e5bcbdd42..5fffe5663c 100644 --- a/ocean/osrs/tests/test_osrs_inventory_actions.c +++ b/ocean/osrs/tests/test_osrs_inventory_actions.c @@ -32,8 +32,10 @@ static void count_items( memset(counts, 0, NUM_ITEMS * sizeof(int)); for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) if (p->equipped[slot] != ITEM_NONE) counts[p->equipped[slot]]++; - for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) - if (cells[i].item_idx != ITEM_NONE) counts[cells[i].item_idx]++; + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) { + uint8_t item_idx = osrs_inventory_cell_item_index(&cells[i]); + if (item_idx != ITEM_NONE) counts[item_idx]++; + } } static void run_equip_storm(int trial) { @@ -109,7 +111,8 @@ static void run_drink_dose_check(void) { for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) cells[i] = osrs_inventory_cell_empty(); cells[0] = osrs_inventory_cell_from_raw_osrs_id(2434); - ASSERT_INT_EQ("prayer potion seeds with 4 doses", cells[0].dose, 4); + ASSERT_INT_EQ("prayer potion seeds with 4 doses", + osrs_inventory_cell_dose_count(&cells[0]), 4); for (int expected_dose = 4; expected_dose >= 1; expected_dose--) { p.potion_timer = 0; @@ -129,7 +132,8 @@ static void run_drink_dose_check(void) { steps++; } ASSERT_INT_EQ("one drink step per tick", steps, 1); - ASSERT_INT_EQ("dose decrements by one", cells[0].dose, expected_dose - 1); + ASSERT_INT_EQ("dose decrements by one", + osrs_inventory_cell_dose_count(&cells[0]), expected_dose - 1); } CHECK("cell empties after the last dose", osrs_inventory_cell_is_empty(&cells[0])); @@ -155,11 +159,34 @@ static void test_weapon_ranges(void) { ASSERT_INT_EQ("unarmed attack speed", unarmed.attack_speed, 4); ASSERT_INT_EQ("unarmed attack range", unarmed.attack_range, 1); } +static void test_content_code_round_trips(void) { + ASSERT_INT_EQ("inventory cell stores only one content code", + (int)sizeof(OsrsInventoryCell), (int)sizeof(uint16_t)); + for (uint16_t code = 0; code < OSRS_ITEM_CONTENT_COUNT; code++) { + const OsrsItemContentMetadata* metadata = + osrs_item_content_metadata(code); + OsrsInventoryCell cell = osrs_inventory_cell_from_content_code(code); + ASSERT_INT_EQ("cell preserves content code", cell.content_code, code); + ASSERT_INT_EQ("content code preserves raw OSRS id", + osrs_inventory_cell_raw_osrs_id(&cell), metadata->raw_osrs_id); + ASSERT_INT_EQ("content code preserves item index", + osrs_inventory_cell_item_index(&cell), metadata->item_idx); + ASSERT_INT_EQ("raw OSRS id round trips to content code", + osrs_inventory_content_code_from_raw_osrs_id(metadata->raw_osrs_id), + code); + if (metadata->item_idx != ITEM_NONE) { + ASSERT_INT_EQ("item index round trips to content code", + osrs_inventory_content_code_from_item(metadata->item_idx), code); + } + } +} + int main(void) { for (int trial = 0; trial < 500; trial++) run_equip_storm(trial); run_drink_dose_check(); test_weapon_ranges(); + test_content_code_round_trips(); return osrs_test_summary(); } diff --git a/ocean/osrs/tests/test_osrs_inventory_clicks.c b/ocean/osrs/tests/test_osrs_inventory_clicks.c index e51e1dc5fc..7d793a1b68 100644 --- a/ocean/osrs/tests/test_osrs_inventory_clicks.c +++ b/ocean/osrs/tests/test_osrs_inventory_clicks.c @@ -1,5 +1,9 @@ #include #include +#include +#include +#include + #include "ocean/osrs/osrs_inventory_clicks.h" @@ -34,26 +38,41 @@ static int test_raw_consumable_classification(void) { CHECK("shark food kind", shark.consumable_kind == OSRS_CONSUMABLE_SHARK_FOOD); CHECK("shark has no dose", shark.dose_count == 0); - OsrsConsumableClick unknown = - osrs_consumable_click_lookup_raw_osrs_id(9999); - CHECK("unknown raw id noops", unknown.click_action == OSRS_CLICK_NONE); - CHECK("unknown raw id has no kind", - unknown.consumable_kind == OSRS_CONSUMABLE_NONE); - CHECK("unknown raw id has no dose", unknown.dose_count == 0); return 0; } - -static int test_consumable_registry_index_covers_every_row(void) { - for (int i = 0; i < OSRS_CONSUMABLE_CLICK_REGISTRY_COUNT; i++) { - CHECK("registered raw id maps to its row", - osrs_consumable_click_registry_index( - OSRS_CONSUMABLE_CLICK_REGISTRY[i].raw_osrs_id) == i); +static int operation_aborts(void (*operation)(void)) { + fflush(NULL); + pid_t pid = fork(); + if (pid == 0) { + operation(); + _exit(0); } - CHECK("unknown raw id has no row", - osrs_consumable_click_registry_index(9999) == -1); + int status = 0; + if (pid < 0 || waitpid(pid, &status, 0) != pid) return 0; + return WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT; +} + +static void lookup_unknown_raw_id(void) { + (void)osrs_inventory_cell_from_raw_osrs_id(9999); +} + +static void lookup_invalid_content_code(void) { + (void)osrs_item_content_metadata(OSRS_ITEM_CONTENT_COUNT); +} + +static void construct_invalid_content_code(void) { + (void)osrs_inventory_cell_from_content_code(UINT16_MAX); +} + +static int test_invalid_content_aborts(void) { + CHECK("unknown raw OSRS id aborts", operation_aborts(lookup_unknown_raw_id)); + CHECK("invalid metadata code aborts", operation_aborts(lookup_invalid_content_code)); + CHECK("invalid cell code aborts", operation_aborts(construct_invalid_content_code)); return 0; } + + static int test_sara_brew_dose_variants(void) { OsrsConsumableClick brew4 = osrs_consumable_click_lookup_raw_osrs_id(6685); @@ -88,39 +107,37 @@ static int test_dose_after_drink(void) { } static int test_pure_click_interpreter(void) { - OsrsInventoryClickResolution gear = osrs_inventory_click_interpret( - ITEM_VENATOR_BOW, - 27610, - OSRS_CLICK_TICK_FIRST - ); + OsrsInventoryCell gear_cell = + osrs_inventory_cell_from_item(ITEM_VENATOR_BOW); + OsrsInventoryClickResolution gear = + osrs_inventory_cell_click_interpret( + &gear_cell, OSRS_CLICK_TICK_FIRST); CHECK("gear click resolves equip", gear.click_action == OSRS_CLICK_EQUIP); CHECK("gear click has no consumable kind", gear.consumable_kind == OSRS_CONSUMABLE_NONE); - OsrsInventoryClickResolution brew = osrs_inventory_click_interpret( - ITEM_NONE, - 6685, - OSRS_CLICK_TICK_FIRST - ); + OsrsInventoryCell brew_cell = + osrs_inventory_cell_from_raw_osrs_id(6685); + OsrsInventoryClickResolution brew = + osrs_inventory_cell_click_interpret( + &brew_cell, OSRS_CLICK_TICK_FIRST); CHECK("brew click resolves drink", brew.click_action == OSRS_CLICK_DRINK); CHECK("brew interpreter kind", brew.consumable_kind == OSRS_CONSUMABLE_BREW); CHECK("brew interpreter dose", brew.dose_count == 4); CHECK("brew interpreter next raw id", brew.raw_osrs_id_after_drink == 6687); - OsrsInventoryClickResolution shark = osrs_inventory_click_interpret( - ITEM_NONE, - 385, - OSRS_CLICK_TICK_FIRST - ); + OsrsInventoryCell shark_cell = + osrs_inventory_cell_from_raw_osrs_id(385); + OsrsInventoryClickResolution shark = + osrs_inventory_cell_click_interpret( + &shark_cell, OSRS_CLICK_TICK_FIRST); CHECK("shark click resolves eat", shark.click_action == OSRS_CLICK_EAT); CHECK("shark interpreter kind", shark.consumable_kind == OSRS_CONSUMABLE_SHARK_FOOD); - OsrsInventoryClickResolution duplicate = osrs_inventory_click_interpret( - ITEM_NONE, - 6685, - OSRS_CLICK_TICK_DUPLICATE - ); + OsrsInventoryClickResolution duplicate = + osrs_inventory_cell_click_interpret( + &brew_cell, OSRS_CLICK_TICK_DUPLICATE); CHECK("duplicate click noops", duplicate.click_action == OSRS_CLICK_NONE); return 0; } @@ -168,9 +185,10 @@ static int test_cell_drink_decrements_one_dose(void) { osrs_inventory_cell_click_interpret(&restore, OSRS_CLICK_TICK_FIRST); osrs_inventory_cell_decrement_drink(&restore, resolution); - CHECK("restore one drink leaves three doses", restore.dose == 3); + CHECK("restore one drink leaves three doses", + osrs_inventory_cell_dose_count(&restore) == 3); CHECK("restore one drink updates raw id to three-dose", - restore.raw_osrs_id == 3026); + osrs_inventory_cell_raw_osrs_id(&restore) == 3026); return 0; } @@ -201,7 +219,8 @@ static int test_shared_drink_consume_owns_timer_and_one_dose(void) { CHECK("shared consume accepts first drink", first.consumed == 1); CHECK("shared consume decrements one dose", - restore.dose == 3 && restore.raw_osrs_id == 3026); + osrs_inventory_cell_dose_count(&restore) == 3 && + osrs_inventory_cell_raw_osrs_id(&restore) == 3026); CHECK("shared consume arms potion timer", potion_timer == 3); CHECK("shared consume applies one effect", effect.calls == 1 && effect.kind == OSRS_CONSUMABLE_SUPER_RESTORE); @@ -217,7 +236,8 @@ static int test_shared_drink_consume_owns_timer_and_one_dose(void) { &effect); CHECK("shared consume blocks live timer", gated.consumed == 0); CHECK("timer-gated shared consume leaves cell intact", - restore.dose == 3 && restore.raw_osrs_id == 3026); + osrs_inventory_cell_dose_count(&restore) == 3 && + osrs_inventory_cell_raw_osrs_id(&restore) == 3026); CHECK("timer-gated shared consume skips effect", effect.calls == 1); return 0; } @@ -232,9 +252,10 @@ static int test_cell_rearrange_swaps_two_slots(void) { osrs_inventory_swap_cells(cells, 1, 9); CHECK("swap moves bow to target slot", - cells[9].item_idx == ITEM_TWISTED_BOW); + osrs_inventory_cell_item_index(&cells[9]) == ITEM_TWISTED_BOW); CHECK("swap moves brew to source slot", - cells[1].raw_osrs_id == 6685 && cells[1].dose == 4); + osrs_inventory_cell_raw_osrs_id(&cells[1]) == 6685 && + osrs_inventory_cell_dose_count(&cells[1]) == 4); return 0; } @@ -254,9 +275,10 @@ static int test_enriched_feature_counts(void) { static int test_brew_cell_semantics(void) { float out[OSRS_INVENTORY_CELL_OBS_FEATURES]; float zero_deltas[6] = {0}; + OsrsInventoryCell brew = osrs_inventory_cell_from_raw_osrs_id(6685); osrs_write_inventory_cell_affordance_features( - out, ITEM_NONE, 6685, 4, 0, zero_deltas, 99, 99, 99); + out, &brew, 0, zero_deltas, 99, 99, 99); CHECK("brew is not armor", out[12] == 0.0f); CHECK("brew is not weapon", out[13] == 0.0f); CHECK("brew kind is brew", out[14] == 1.0f); @@ -272,10 +294,11 @@ static int test_brew_cell_semantics(void) { static int test_weapon_cell_semantics(void) { float out[OSRS_INVENTORY_CELL_OBS_FEATURES]; float zero_deltas[6] = {0}; + OsrsInventoryCell fang = + osrs_inventory_cell_from_item(ITEM_OSMUMTENS_FANG); - uint16_t fang_raw = ITEM_DATABASE[ITEM_OSMUMTENS_FANG].item_id; osrs_write_inventory_cell_affordance_features( - out, ITEM_OSMUMTENS_FANG, fang_raw, 0, 0, zero_deltas, 99, 99, 99); + out, &fang, 0, zero_deltas, 99, 99, 99); CHECK("fang is weapon", out[13] == 1.0f); CHECK("fang is not armor", out[12] == 0.0f); CHECK("fang is not consumable", out[14] == 0.0f && out[15] == 0.0f && @@ -345,18 +368,76 @@ static int test_kind6_totality(void) { static int test_empty_cell_clamp(void) { float out[OSRS_INVENTORY_CELL_OBS_FEATURES]; float zero_deltas[6] = {0}; + OsrsInventoryCell empty = osrs_inventory_cell_empty(); osrs_write_inventory_cell_affordance_features( - out, ITEM_NONE, 0, 0, 0, zero_deltas, 99, 99, 99); + out, &empty, 0, zero_deltas, 99, 99, 99); CHECK("empty cell not present", out[0] == 0.0f); CHECK("empty cell within [-1,1]", cell_in_unit_range(out)); return 0; } +static int test_exhaustive_content_metadata(void) { + for (uint16_t code = 0; code < OSRS_ITEM_CONTENT_COUNT; code++) { + const OsrsItemContentMetadata* metadata = + osrs_item_content_metadata(code); + OsrsInventoryCell cell = osrs_inventory_cell_from_content_code(code); + OsrsInventoryClickResolution classified = + osrs_inventory_cell_click_classify(&cell); + CHECK("metadata click action matches classification", + classified.click_action == metadata->click_action); + CHECK("metadata consumable kind matches classification", + classified.consumable_kind == metadata->consumable_kind); + CHECK("metadata dose matches classification", + classified.dose_count == metadata->dose_count); + + OsrsInventoryClickResolution interpreted = + osrs_inventory_cell_click_interpret(&cell, OSRS_CLICK_TICK_FIRST); + uint16_t expected_next_raw = metadata->next_content_code == 0 + ? 0 + : osrs_item_content_metadata( + metadata->next_content_code)->raw_osrs_id; + CHECK("metadata next dose matches click interpretation", + interpreted.raw_osrs_id_after_drink == expected_next_raw); + if (metadata->click_action == OSRS_CLICK_DRINK) { + if (metadata->dose_count == 1) { + CHECK("one-dose drink transitions to empty", + metadata->next_content_code == 0); + } else { + const OsrsItemContentMetadata* next = + osrs_item_content_metadata(metadata->next_content_code); + CHECK("drink transition preserves consumable kind", + next->consumable_kind == metadata->consumable_kind); + CHECK("drink transition decrements exactly one dose", + next->dose_count + 1 == metadata->dose_count); + } + } else { + CHECK("non-drink content has no dose transition", + metadata->next_content_code == 0); + } + + float expected[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; + osrs_write_item_content_affordance_features_compact( + expected, + metadata, + 0, + OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS, + OSRS_ITEM_OBS_TABLE_BASE_PRAYER, + OSRS_ITEM_OBS_TABLE_BASE_RANGED); + for (int feature = 0; + feature < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; + feature++) { + CHECK("generated observation row remains exact", + metadata->observation_row[feature] == expected[feature]); + } + } + return 0; +} + int main(void) { if (test_item_index_classification()) return 1; if (test_raw_consumable_classification()) return 1; + if (test_invalid_content_aborts()) return 1; if (test_sara_brew_dose_variants()) return 1; - if (test_consumable_registry_index_covers_every_row()) return 1; if (test_dose_after_drink()) return 1; if (test_pure_click_interpreter()) return 1; if (test_cell_click_classification()) return 1; @@ -370,7 +451,9 @@ int main(void) { if (test_effect_class4_decoder()) return 1; if (test_kind6_totality()) return 1; if (test_empty_cell_clamp()) return 1; + if (test_exhaustive_content_metadata()) return 1; - printf("test_osrs_inventory_clicks: OK\n"); + printf("test_osrs_inventory_clicks: OK (%d content codes, 3 abort boundaries)\n", + OSRS_ITEM_CONTENT_COUNT); return 0; } diff --git a/ocean/osrs/tests/test_osrs_item_effect_masks.c b/ocean/osrs/tests/test_osrs_item_effect_masks.c index 6177ea0d96..008b73aadc 100644 --- a/ocean/osrs/tests/test_osrs_item_effect_masks.c +++ b/ocean/osrs/tests/test_osrs_item_effect_masks.c @@ -2,7 +2,7 @@ #include #include -#include "ocean/osrs/osrs_items.h" +#include "ocean/osrs/osrs_inventory_clicks.h" static const struct { int item; @@ -36,6 +36,47 @@ static const int EXPECTED_ITEM_EFFECT_FREE_ITEMS[] = { ITEM_ABYSSAL_TENTACLE, }; +static int expected_gear_slot(EquipmentSlot slot) { + switch (slot) { + case SLOT_HEAD: return GEAR_SLOT_HEAD; + case SLOT_CAPE: return GEAR_SLOT_CAPE; + case SLOT_NECK: return GEAR_SLOT_NECK; + case SLOT_WEAPON: return GEAR_SLOT_WEAPON; + case SLOT_BODY: return GEAR_SLOT_BODY; + case SLOT_SHIELD: return GEAR_SLOT_SHIELD; + case SLOT_LEGS: return GEAR_SLOT_LEGS; + case SLOT_HANDS: return GEAR_SLOT_HANDS; + case SLOT_FEET: return GEAR_SLOT_FEET; + case SLOT_RING: return GEAR_SLOT_RING; + case SLOT_AMMO: return GEAR_SLOT_AMMO; + default: return -1; + } +} + +static int verify_generated_gear_metadata(void) { + int failures = 0; + for (int item_idx = 0; item_idx < NUM_ITEMS; item_idx++) { + uint16_t content_code = + osrs_inventory_content_code_from_item((uint8_t)item_idx); + const OsrsItemContentMetadata* metadata = + osrs_item_content_metadata(content_code); + const Item* item = &ITEM_DATABASE[item_idx]; + if (metadata->item_idx != item_idx || + metadata->item != item || + metadata->raw_osrs_id != item->item_id || + metadata->gear_slot != expected_gear_slot(item->slot) || + metadata->attack_style != get_item_attack_style(item_idx) || + metadata->click_action != OSRS_CLICK_EQUIP || + metadata->consumable_kind != OSRS_CONSUMABLE_NONE || + metadata->dose_count != 0 || + metadata->next_content_code != 0) { + fprintf(stderr, "item %d generated metadata mismatch\n", item_idx); + failures++; + } + } + return failures; +} + int main(void) { const size_t n_expected = sizeof(EXPECTED_ITEM_EFFECTS) / sizeof(EXPECTED_ITEM_EFFECTS[0]); @@ -102,8 +143,9 @@ int main(void) { failures++; } + failures += verify_generated_gear_metadata(); if (failures) { - fprintf(stderr, "test_osrs_item_effect_masks: %d failure(s)\n", failures); + fprintf(stderr, "test_osrs_item_effect_masks: %d failures\n", failures); return 1; } diff --git a/ocean/osrs/tools/gen_osrs_item_obs_table.c b/ocean/osrs/tools/gen_osrs_item_obs_table.c index 4bc6800ecc..49762c88db 100644 --- a/ocean/osrs/tools/gen_osrs_item_obs_table.c +++ b/ocean/osrs/tools/gen_osrs_item_obs_table.c @@ -1,4 +1,4 @@ -/* Emits the shared OSRS item observation table consumed by entity encoders. +/* Emits the shared OSRS item content metadata and observation table. * * cc -std=c11 -O2 -I. -o /tmp/gen_osrs_item_obs \ * ocean/osrs/tools/gen_osrs_item_obs_table.c -lm @@ -15,38 +15,245 @@ #define GEN_BASE_HITPOINTS 99 #define GEN_BASE_PRAYER 99 #define GEN_BASE_RANGED 99 +#define GEN_INERT_RAW_OSRS_ID 27281 -static void emit_float(FILE* out, float v) { - char buf[64]; - snprintf(buf, sizeof(buf), "%.9g", (double)v); - if (!strpbrk(buf, ".eEnN")) strncat(buf, ".0", sizeof(buf) - strlen(buf) - 1); - fprintf(out, "%sf", buf); +typedef struct { + uint16_t raw_osrs_id; + OsrsClickAction click_action; + OsrsConsumableKind consumable_kind; + uint8_t dose_count; +} GenConsumable; + +static const GenConsumable GEN_CONSUMABLES[] = { + {6685, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 4}, + {6687, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 3}, + {6689, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 2}, + {6691, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 1}, + {3024, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 4}, + {3026, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 3}, + {3028, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 2}, + {3030, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 1}, + {10925, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 4}, + {10927, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 3}, + {10929, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 2}, + {10931, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 1}, + {12695, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 4}, + {12697, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 3}, + {12699, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 2}, + {12701, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 1}, + {23685, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 4}, + {23688, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 3}, + {23691, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 2}, + {23694, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 1}, + {2444, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 4}, + {169, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 3}, + {171, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 2}, + {173, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 1}, + {23733, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 4}, + {23736, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 3}, + {23739, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 2}, + {23742, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 1}, + {30875, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 4}, + {30878, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 3}, + {30881, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 2}, + {30884, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 1}, + {4417, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 4}, + {4419, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 3}, + {4421, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 2}, + {4423, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 1}, + {27641, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SATURATED_HEART, 1}, + {12913, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 4}, + {12915, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 3}, + {12917, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 2}, + {12919, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 1}, + {2434, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 4}, + {139, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 3}, + {141, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 2}, + {143, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_PRAYER_RESTORE, 1}, + {22461, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 4}, + {22464, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 3}, + {22467, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 2}, + {22470, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BASTION, 1}, + {12625, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 4}, + {12627, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 3}, + {12629, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 2}, + {12631, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_STAMINA, 1}, + {385, OSRS_CLICK_EAT, OSRS_CONSUMABLE_SHARK_FOOD, 0}, + {3144, OSRS_CLICK_EAT, OSRS_CONSUMABLE_KARAMBWAN, 0}, +}; + +#define GEN_CONSUMABLE_COUNT \ + ((int)(sizeof(GEN_CONSUMABLES) / sizeof(GEN_CONSUMABLES[0]))) +#define GEN_GEAR_BASE 1 +#define GEN_CONSUMABLE_BASE (GEN_GEAR_BASE + NUM_ITEMS) +#define GEN_INERT_CODE (GEN_CONSUMABLE_BASE + GEN_CONSUMABLE_COUNT) +#define GEN_CONTENT_COUNT (GEN_INERT_CODE + 1) + +static FILE* open_or_die(const char* path) { + FILE* file = fopen(path, "w"); + if (!file) { + fprintf(stderr, "OSRS item metadata: cannot write %s\n", path); + exit(1); + } + return file; } -static void build_row(float* row, int code) { - osrs_write_inventory_cell_obs_table_row( - row, code, GEN_BASE_HITPOINTS, GEN_BASE_PRAYER, GEN_BASE_RANGED); +static void emit_float(FILE* out, float value) { + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%.9g", (double)value); + if (!strpbrk(buffer, ".eEnN")) { + strncat(buffer, ".0", sizeof(buffer) - strlen(buffer) - 1); + } + fprintf(out, "%sf", buffer); } -static void require_code_round_trip(int code) { - OsrsInventoryCell cell = osrs_inventory_cell_for_obs_code(code); - int back = osrs_inventory_cell_obs_code(cell.item_idx, cell.raw_osrs_id); - if (back == code) return; - fprintf(stderr, - "OSRS item obs table: code %d resolves to item %u raw %u, which codes back as " - "%d. Two codes share one cell content and the table is not a function of the code.\n", - code, cell.item_idx, cell.raw_osrs_id, back); +static int gear_slot_for_item_slot(uint8_t slot) { + switch (slot) { + case SLOT_HEAD: return GEAR_SLOT_HEAD; + case SLOT_CAPE: return GEAR_SLOT_CAPE; + case SLOT_NECK: return GEAR_SLOT_NECK; + case SLOT_WEAPON: return GEAR_SLOT_WEAPON; + case SLOT_BODY: return GEAR_SLOT_BODY; + case SLOT_SHIELD: return GEAR_SLOT_SHIELD; + case SLOT_LEGS: return GEAR_SLOT_LEGS; + case SLOT_HANDS: return GEAR_SLOT_HANDS; + case SLOT_FEET: return GEAR_SLOT_FEET; + case SLOT_RING: return GEAR_SLOT_RING; + case SLOT_AMMO: return GEAR_SLOT_AMMO; + default: + fprintf(stderr, "OSRS item metadata: unsupported item slot %u\n", slot); + exit(1); + } +} + +static int content_code_for_raw_osrs_id(uint16_t raw_osrs_id) { + if (raw_osrs_id == 0) return 0; + for (int item_idx = 0; item_idx < NUM_ITEMS; item_idx++) { + if (ITEM_DATABASE[item_idx].item_id == raw_osrs_id) { + return GEN_GEAR_BASE + item_idx; + } + } + for (int index = 0; index < GEN_CONSUMABLE_COUNT; index++) { + if (GEN_CONSUMABLES[index].raw_osrs_id == raw_osrs_id) { + return GEN_CONSUMABLE_BASE + index; + } + } + if (raw_osrs_id == GEN_INERT_RAW_OSRS_ID) return GEN_INERT_CODE; + fprintf(stderr, "OSRS item metadata: unrepresentable raw OSRS id %u\n", raw_osrs_id); exit(1); } +static int next_content_code(const GenConsumable* consumable) { + if (consumable->click_action != OSRS_CLICK_DRINK) return 0; + if (consumable->dose_count == 1) return 0; + for (int index = 0; index < GEN_CONSUMABLE_COUNT; index++) { + const GenConsumable* candidate = &GEN_CONSUMABLES[index]; + if (candidate->consumable_kind == consumable->consumable_kind && + candidate->dose_count + 1 == consumable->dose_count) { + return GEN_CONSUMABLE_BASE + index; + } + } + fprintf(stderr, "OSRS item metadata: incomplete dose chain for raw OSRS id %u\n", + consumable->raw_osrs_id); + exit(1); +} -static FILE* open_or_die(const char* path) { - FILE* f = fopen(path, "w"); - if (!f) { - fprintf(stderr, "OSRS item obs table: cannot write %s\n", path); - exit(1); +static void build_row( + float* row, + uint8_t item_idx, + uint16_t raw_osrs_id, + OsrsConsumableKind consumable_kind, + uint8_t dose_count +) { + OsrsItemContentMetadata metadata = { + .item = item_idx == ITEM_NONE ? NULL : &ITEM_DATABASE[item_idx], + .raw_osrs_id = raw_osrs_id, + .item_idx = item_idx, + .consumable_kind = (uint8_t)consumable_kind, + .dose_count = dose_count, + .attack_style = + item_idx == ITEM_NONE ? 0 : get_item_attack_style(item_idx), + }; + osrs_write_item_content_affordance_features_compact( + row, &metadata, 0, + GEN_BASE_HITPOINTS, GEN_BASE_PRAYER, GEN_BASE_RANGED); +} + +static void emit_content_row( + FILE* out, + int code, + const char* item_pointer, + uint8_t item_idx, + uint16_t raw_osrs_id, + int gear_slot, + OsrsClickAction click_action, + OsrsConsumableKind consumable_kind, + uint8_t dose_count, + int next_code, + int attack_style, + const float* observation_row, + int final_row +) { + fprintf(out, " X(%d, %s, %u, %u, %d, %d, %d, %u, %d, %d", + code, item_pointer, item_idx, raw_osrs_id, gear_slot, + (int)click_action, (int)consumable_kind, dose_count, next_code, + attack_style); + for (int feature = 0; + feature < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; + feature++) { + fprintf(out, ", "); + emit_float(out, observation_row[feature]); + } + fprintf(out, ")%s\n", final_row ? "" : " \\"); +} + +static void write_content_rows(FILE* out) { + fprintf(out, "#define OSRS_ITEM_CONTENT_ROWS(X) \\\n"); + float row[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; + build_row(row, ITEM_NONE, 0, OSRS_CONSUMABLE_NONE, 0); + emit_content_row(out, 0, "NULL", ITEM_NONE, 0, -1, + OSRS_CLICK_NONE, OSRS_CONSUMABLE_NONE, 0, 0, 0, row, 0); + + for (int item_idx = 0; item_idx < NUM_ITEMS; item_idx++) { + char item_pointer[64]; + snprintf(item_pointer, sizeof(item_pointer), + "&ITEM_DATABASE[%d]", item_idx); + const Item* item = &ITEM_DATABASE[item_idx]; + build_row(row, (uint8_t)item_idx, item->item_id, + OSRS_CONSUMABLE_NONE, 0); + emit_content_row(out, GEN_GEAR_BASE + item_idx, item_pointer, + (uint8_t)item_idx, item->item_id, + gear_slot_for_item_slot(item->slot), OSRS_CLICK_EQUIP, + OSRS_CONSUMABLE_NONE, 0, 0, get_item_attack_style(item_idx), + row, 0); + } + + for (int index = 0; index < GEN_CONSUMABLE_COUNT; index++) { + const GenConsumable* consumable = &GEN_CONSUMABLES[index]; + build_row(row, ITEM_NONE, consumable->raw_osrs_id, + consumable->consumable_kind, consumable->dose_count); + emit_content_row(out, GEN_CONSUMABLE_BASE + index, "NULL", ITEM_NONE, + consumable->raw_osrs_id, -1, consumable->click_action, + consumable->consumable_kind, consumable->dose_count, + next_content_code(consumable), 0, row, 0); + } + + build_row(row, ITEM_NONE, GEN_INERT_RAW_OSRS_ID, + OSRS_CONSUMABLE_NONE, 0); + emit_content_row(out, GEN_INERT_CODE, "NULL", ITEM_NONE, + GEN_INERT_RAW_OSRS_ID, -1, OSRS_CLICK_NONE, OSRS_CONSUMABLE_NONE, + 0, 0, 0, row, 1); +} + +static void write_consumable_rows(FILE* out) { + fprintf(out, "\n#define OSRS_CONSUMABLE_CONTENT_ROWS(X) \\\n"); + for (int index = 0; index < GEN_CONSUMABLE_COUNT; index++) { + const GenConsumable* consumable = &GEN_CONSUMABLES[index]; + fprintf(out, " X(%d, %d, %u)%s\n", + (int)consumable->consumable_kind, consumable->dose_count, + GEN_CONSUMABLE_BASE + index, + index + 1 == GEN_CONSUMABLE_COUNT ? "" : " \\"); } - return f; } static void write_header(const char* path) { @@ -55,28 +262,28 @@ static void write_header(const char* path) { "/* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit.\n" " * Dependency-free on purpose: src/ocean.cu includes it and no OSRS header. */\n" "#ifndef OSRS_ITEM_OBS_GENERATED_H\n" - "#define OSRS_ITEM_OBS_GENERATED_H\n" - "\n" + "#define OSRS_ITEM_OBS_GENERATED_H\n\n" + "#define OSRS_ITEM_CONTENT_COUNT %d\n" "#define OSRS_ITEM_OBS_TABLE_ROWS %d\n" "#define OSRS_ITEM_OBS_TABLE_COLS %d\n" "#define OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS %d\n" "#define OSRS_ITEM_OBS_TABLE_BASE_PRAYER %d\n" - "#define OSRS_ITEM_OBS_TABLE_BASE_RANGED %d\n" - "\n" - "#define OSRS_ITEM_OBS_CODE_SCALE %d\n" - "\n" + "#define OSRS_ITEM_OBS_TABLE_BASE_RANGED %d\n\n" + "#define OSRS_ITEM_OBS_CODE_SCALE %d\n\n" "#define OSRS_ITEM_OBS_OVERLAY_EQUIPPED %d\n" - "#define OSRS_ITEM_OBS_OVERLAY_HP_HEAL %d\n" - "\n" - "#endif\n", - OSRS_INVENTORY_CELL_OBS_CODE_COUNT, + "#define OSRS_ITEM_OBS_OVERLAY_HP_HEAL %d\n\n", + GEN_CONTENT_COUNT, + GEN_CONTENT_COUNT, OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT, GEN_BASE_HITPOINTS, GEN_BASE_PRAYER, GEN_BASE_RANGED, - OSRS_INVENTORY_CELL_OBS_CODE_SCALE, + OSRS_ITEM_OBS_CODE_SCALE, OSRS_INVENTORY_CELL_COMPACT_EQUIPPED, OSRS_INVENTORY_CELL_COMPACT_HP_HEAL); + write_content_rows(out); + write_consumable_rows(out); + fprintf(out, "\n#endif\n"); fclose(out); } @@ -84,21 +291,10 @@ static void write_table(const char* path) { FILE* out = open_or_die(path); fprintf(out, "/* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit.\n" - " * Included as the initialiser body of a [ROWS][COLS] float array. */\n"); - - for (int code = 0; code < OSRS_INVENTORY_CELL_OBS_CODE_COUNT; code++) { - require_code_round_trip(code); - - float row[OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT]; - build_row(row, code); - - fprintf(out, "{"); - for (int f = 0; f < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; f++) { - if (f) fprintf(out, ", "); - emit_float(out, row[f]); - } - fprintf(out, "},\n"); - } + " * Expands the observation fields from the canonical content rows. */\n" + "#define OSRS_ITEM_CONTENT_OBS_ROW(code, item_pointer, item_idx, raw_osrs_id, gear_slot, click_action, consumable_kind, dose_count, next_content_code, attack_style, ...) {__VA_ARGS__},\n" + "OSRS_ITEM_CONTENT_ROWS(OSRS_ITEM_CONTENT_OBS_ROW)\n" + "#undef OSRS_ITEM_CONTENT_OBS_ROW\n"); fclose(out); } @@ -107,19 +303,28 @@ int main(int argc, char** argv) { fprintf(stderr, "usage: %s \n", argv[0]); return 1; } - if (OSRS_INVENTORY_CELL_OBS_CODE_COUNT > OSRS_INVENTORY_CELL_OBS_CODE_SCALE) { + if (GEN_CONTENT_COUNT > OSRS_ITEM_OBS_CODE_SCALE) { fprintf(stderr, - "OSRS item obs table: %d codes exceeds the scale %d; the code/scale round " - "trip is only exact in bf16 while a code fits in the scale's significand\n", - OSRS_INVENTORY_CELL_OBS_CODE_COUNT, OSRS_INVENTORY_CELL_OBS_CODE_SCALE); + "OSRS item metadata: %d codes exceeds observation scale %d\n", + GEN_CONTENT_COUNT, OSRS_ITEM_OBS_CODE_SCALE); return 1; } - for (int code = 0; code < OSRS_INVENTORY_CELL_OBS_CODE_COUNT; code++) { - int back = osrs_inventory_cell_obs_code_decode( + for (int code = 0; code < GEN_CONTENT_COUNT; code++) { + int decoded = osrs_inventory_cell_obs_code_decode( osrs_inventory_cell_obs_code_encode(code)); - if (back == code) continue; - fprintf(stderr, "OSRS item obs table: code %d encodes back as %d\n", code, back); - return 1; + if (decoded != code) { + fprintf(stderr, "OSRS item metadata: code %d decodes as %d\n", + code, decoded); + return 1; + } + } + for (int item_idx = 0; item_idx < NUM_ITEMS; item_idx++) { + if (content_code_for_raw_osrs_id(ITEM_DATABASE[item_idx].item_id) != + GEN_GEAR_BASE + item_idx) { + fprintf(stderr, "OSRS item metadata: item %d raw id is ambiguous\n", + item_idx); + return 1; + } } write_header(argv[1]); write_table(argv[2]); From 563dded53dcebf2c0c77730eae2f0364a073caff Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 12:32:56 +0300 Subject: [PATCH 26/50] Fix canonical item metadata call sites --- ocean/osrs/osrs_gui.h | 20 ++++++--- ocean/osrs/tests/test_osrs_inventory_clicks.c | 42 ++++++++++++++++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/ocean/osrs/osrs_gui.h b/ocean/osrs/osrs_gui.h index 82d6730278..cf0f8ab024 100644 --- a/ocean/osrs/osrs_gui.h +++ b/ocean/osrs/osrs_gui.h @@ -2059,9 +2059,17 @@ static int gui_inv_slot_at(GuiState* gs, int mx, int my) { return -1; } +static uint16_t gui_inventory_raw_osrs_id(int osrs_id) { + if (osrs_id < 0 || osrs_id > UINT16_MAX) { + fprintf(stderr, "gui inventory: invalid raw OSRS id %d\n", osrs_id); + abort(); + } + return (uint16_t)osrs_id; +} + static const char* gui_inv_primary_action_label(const InvSlot* inv) { uint16_t raw_osrs_id = - inv->osrs_id > 0 && inv->osrs_id <= UINT16_MAX ? (uint16_t)inv->osrs_id : 0; + gui_inventory_raw_osrs_id(inv->osrs_id); uint8_t item_idx = inv->type == INV_SLOT_EQUIPMENT ? inv->item_db_idx : ITEM_NONE; @@ -2500,10 +2508,12 @@ static void gui_load_display_inventory(GuiState* gs) { if (count > INV_GRID_SLOTS) count = INV_GRID_SLOTS; for (int i = 0; i < count; i++) { int osrs_id = gs->display_inventory_osrs_ids[i]; - if (osrs_id <= 0) continue; - uint8_t item_idx = osrs_id <= UINT16_MAX - ? osrs_item_index_for_raw_osrs_id((uint16_t)osrs_id) - : ITEM_NONE; + if (osrs_id == 0) continue; + const OsrsItemContentMetadata* metadata = + osrs_item_content_metadata( + osrs_inventory_content_code_from_raw_osrs_id( + gui_inventory_raw_osrs_id(osrs_id))); + uint8_t item_idx = metadata->item_idx; gs->inv_grid[i].type = INV_SLOT_EQUIPMENT; gs->inv_grid[i].item_db_idx = item_idx; gs->inv_grid[i].osrs_id = osrs_id; diff --git a/ocean/osrs/tests/test_osrs_inventory_clicks.c b/ocean/osrs/tests/test_osrs_inventory_clicks.c index 7d793a1b68..18fca6fdad 100644 --- a/ocean/osrs/tests/test_osrs_inventory_clicks.c +++ b/ocean/osrs/tests/test_osrs_inventory_clicks.c @@ -63,11 +63,51 @@ static void lookup_invalid_content_code(void) { static void construct_invalid_content_code(void) { (void)osrs_inventory_cell_from_content_code(UINT16_MAX); } +static void lookup_invalid_item_index(void) { + (void)osrs_inventory_content_code_from_item(NUM_ITEMS); +} + +static void lookup_invalid_consumable_kind_none(void) { + (void)osrs_inventory_content_code_from_consumable( + OSRS_CONSUMABLE_NONE, 4); +} + +static void lookup_invalid_consumable_kind_count(void) { + (void)osrs_inventory_content_code_from_consumable( + OSRS_CONSUMABLE_COUNT, 4); +} + +static void lookup_invalid_consumable_zero_dose(void) { + (void)osrs_inventory_content_code_from_consumable( + OSRS_CONSUMABLE_BREW, 0); +} + +static void lookup_invalid_consumable_high_dose(void) { + (void)osrs_inventory_content_code_from_consumable( + OSRS_CONSUMABLE_BREW, 5); +} + +static void lookup_invalid_gear_slot_item(void) { + (void)osrs_item_gear_slot(NUM_ITEMS); +} + static int test_invalid_content_aborts(void) { CHECK("unknown raw OSRS id aborts", operation_aborts(lookup_unknown_raw_id)); CHECK("invalid metadata code aborts", operation_aborts(lookup_invalid_content_code)); CHECK("invalid cell code aborts", operation_aborts(construct_invalid_content_code)); + CHECK("invalid item index aborts", + operation_aborts(lookup_invalid_item_index)); + CHECK("none consumable kind aborts", + operation_aborts(lookup_invalid_consumable_kind_none)); + CHECK("sentinel consumable kind aborts", + operation_aborts(lookup_invalid_consumable_kind_count)); + CHECK("zero consumable dose aborts", + operation_aborts(lookup_invalid_consumable_zero_dose)); + CHECK("high consumable dose aborts", + operation_aborts(lookup_invalid_consumable_high_dose)); + CHECK("invalid gear-slot item aborts", + operation_aborts(lookup_invalid_gear_slot_item)); return 0; } @@ -453,7 +493,7 @@ int main(void) { if (test_empty_cell_clamp()) return 1; if (test_exhaustive_content_metadata()) return 1; - printf("test_osrs_inventory_clicks: OK (%d content codes, 3 abort boundaries)\n", + printf("test_osrs_inventory_clicks: OK (%d content codes, 9 abort boundaries)\n", OSRS_ITEM_CONTENT_COUNT); return 0; } From b05d5b9063bbdf63fda40ee67e5bb7eac02e1e7f Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 12:45:35 +0300 Subject: [PATCH 27/50] Hash canonical inventory semantics --- ocean/osrs/osrs_item_obs_generated.h | 3 +- ocean/osrs/tests/probe_colo_sim_invariant.c | 51 +++++++++++++-------- ocean/osrs/tests/test_inferno_golden.c | 25 +++++++--- ocean/osrs/tools/gen_osrs_item_obs_table.c | 3 +- 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/ocean/osrs/osrs_item_obs_generated.h b/ocean/osrs/osrs_item_obs_generated.h index 9fffd5156a..0c33f31e47 100644 --- a/ocean/osrs/osrs_item_obs_generated.h +++ b/ocean/osrs/osrs_item_obs_generated.h @@ -1,5 +1,4 @@ -/* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit. - * Dependency-free on purpose: src/ocean.cu includes it and no OSRS header. */ +/* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit. */ #ifndef OSRS_ITEM_OBS_GENERATED_H #define OSRS_ITEM_OBS_GENERATED_H diff --git a/ocean/osrs/tests/probe_colo_sim_invariant.c b/ocean/osrs/tests/probe_colo_sim_invariant.c index 472816007c..2a773377b3 100644 --- a/ocean/osrs/tests/probe_colo_sim_invariant.c +++ b/ocean/osrs/tests/probe_colo_sim_invariant.c @@ -6,13 +6,13 @@ * the half that must not move when the observation is recut. * * Covers the whole ColosseumState plus the full 452-float action mask. Storage whose - * SIZE is not part of the simulation contract -- the obs memo caches, and the pending-hit - * queues' spare capacity -- is hashed by content rather than by raw bytes, so resizing or - * renarrowing it does not move a digest. + * size or representation is not part of the simulation contract is hashed semantically: + * inventory identity, obs memo caches, and pending-hit queue spare capacity. Resizing or + * renarrowing those records does not move a digest. * - * Re-seeding rule: prove the digests first, never re-print to make an edit pass. The - * pending-hit narrowing was carried across by computing baselines on the old record with - * this same hash and confirming the new one reproduced all 12. + * Re-seeding rule: prove the digests first, never re-print to make an edit pass. Pending-hit + * narrowing and canonical inventory identity were carried across by computing the same + * semantic hash on the old and new records and confirming all 12 were byte-identical. * * The player-collision re-seed could NOT use that method: replacing a 1156-byte array with * two ints moved the struct's padding (size fell 1152, not the 1148 the fields imply), so @@ -52,6 +52,14 @@ static inline uint64_t fnv_i32(uint64_t h, int v) { int32_t w = (int32_t)v; return fnv_bytes(h, &w, sizeof(w)); } +static inline uint64_t fnv_u8(uint64_t h, uint8_t v) { + return fnv_bytes(h, &v, sizeof(v)); +} + +static inline uint64_t fnv_u16(uint64_t h, uint16_t v) { + return fnv_bytes(h, &v, sizeof(v)); +} + static inline uint64_t splitmix64(uint64_t* s) { uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); @@ -72,18 +80,18 @@ typedef struct { * determinism fixes. Regenerate with --print only when a SIMULATION change is * intended, never to make an observation edit pass. */ static SimConfig CONFIGS[] = { - {"w01", 1, 1001ULL, 0xC0FFEE01ULL, 0x9c4cbcde5d159413ULL}, - {"w02", 2, 1002ULL, 0xC0FFEE02ULL, 0x926e334416bc21f0ULL}, - {"w03", 3, 1003ULL, 0xC0FFEE03ULL, 0xcc8a421a35859befULL}, - {"w04", 4, 1004ULL, 0xC0FFEE04ULL, 0x14b1e80d759d1cb2ULL}, - {"w05", 5, 1005ULL, 0xC0FFEE05ULL, 0x22f415d9f4d6dd02ULL}, - {"w06", 6, 1006ULL, 0xC0FFEE06ULL, 0xb82309c9c86c363bULL}, - {"w07", 7, 1007ULL, 0xC0FFEE07ULL, 0x4fd42a575b3fd127ULL}, - {"w08", 8, 1008ULL, 0xC0FFEE08ULL, 0x48548d73bd727fd2ULL}, - {"w09", 9, 1009ULL, 0xC0FFEE09ULL, 0xb3f7b27247b54a07ULL}, - {"w10", 10, 1010ULL, 0xC0FFEE10ULL, 0x2b0b3f61efb8b43fULL}, - {"w11", 11, 1011ULL, 0xC0FFEE11ULL, 0x1e8ff8d2425f33a7ULL}, - {"w12", 12, 1012ULL, 0xC0FFEE12ULL, 0x210c0b6ce8537df3ULL}, + {"w01", 1, 1001ULL, 0xC0FFEE01ULL, 0xe882cb88e9bfe571ULL}, + {"w02", 2, 1002ULL, 0xC0FFEE02ULL, 0x2030dfc6f5128a4aULL}, + {"w03", 3, 1003ULL, 0xC0FFEE03ULL, 0xb6340675ad060ccfULL}, + {"w04", 4, 1004ULL, 0xC0FFEE04ULL, 0xda9cbc85c2b1ccceULL}, + {"w05", 5, 1005ULL, 0xC0FFEE05ULL, 0xffe01df334b833dcULL}, + {"w06", 6, 1006ULL, 0xC0FFEE06ULL, 0x91c31d65c5756157ULL}, + {"w07", 7, 1007ULL, 0xC0FFEE07ULL, 0x3abedfcf6d66865bULL}, + {"w08", 8, 1008ULL, 0xC0FFEE08ULL, 0x9c20637df795889bULL}, + {"w09", 9, 1009ULL, 0xC0FFEE09ULL, 0xcbf92ffc1560faa7ULL}, + {"w10", 10, 1010ULL, 0xC0FFEE10ULL, 0xabe0af7272270f17ULL}, + {"w11", 11, 1011ULL, 0xC0FFEE11ULL, 0x4b7bd07c37349b9dULL}, + {"w12", 12, 1012ULL, 0xC0FFEE12ULL, 0xaf6eddfc94e7f411ULL}, }; static void fill_actions( @@ -148,6 +156,7 @@ static uint64_t hash_sim(uint64_t h, const ColosseumState* s, const float* mask) * Taking the whole tail removes the padding from the hash entirely. */ SkipRegion skip[] = { {offsetof(ColosseumState, npcs), sizeof(s->npcs)}, + {offsetof(ColosseumState, inventory_cells), sizeof(s->inventory_cells)}, {offsetof(ColosseumState, player_pending_hits), sizeof(s->player_pending_hits)}, {offsetof(ColosseumState, log), sizeof(*s) - offsetof(ColosseumState, log)}, }; @@ -164,6 +173,12 @@ static uint64_t hash_sim(uint64_t h, const ColosseumState* s, const float* mask) for (int i = 0; i < COLO_MAX_NPCS; i++) h = fnv_npc(h, &s->npcs[i]); h = fnv_queue(h, &s->player_pending_hits); + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { + const OsrsInventoryCell* cell = &s->inventory_cells[i]; + h = fnv_u8(h, osrs_inventory_cell_item_index(cell)); + h = fnv_u16(h, osrs_inventory_cell_raw_osrs_id(cell)); + h = fnv_u8(h, osrs_inventory_cell_dose_count(cell)); + } for (int i = 0; i < COLO_ACTION_MASK_SIZE; i++) h = fnv_f32(h, mask[i]); return h; diff --git a/ocean/osrs/tests/test_inferno_golden.c b/ocean/osrs/tests/test_inferno_golden.c index 76bf25f44e..22b67e859c 100644 --- a/ocean/osrs/tests/test_inferno_golden.c +++ b/ocean/osrs/tests/test_inferno_golden.c @@ -27,6 +27,14 @@ static inline uint64_t fnv_i32(uint64_t h, int v) { int32_t w = (int32_t)v; return fnv_bytes(h, &w, sizeof(w)); } +static inline uint64_t fnv_u8(uint64_t h, uint8_t v) { + return fnv_bytes(h, &v, sizeof(v)); +} + +static inline uint64_t fnv_u16(uint64_t h, uint16_t v) { + return fnv_bytes(h, &v, sizeof(v)); +} + static inline uint64_t splitmix64(uint64_t* s) { uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); @@ -56,7 +64,12 @@ static uint64_t hash_core_state(uint64_t h, const InfernoState* s) { h = fnv_bytes(h, s->npcs, TRACE_NPC_SLOTS * sizeof(s->npcs[0])); h = fnv_bytes(h, &s->player_pending_hits, sizeof(s->player_pending_hits)); h = fnv_bytes(h, s->pending_sparks, sizeof(s->pending_sparks)); - h = fnv_bytes(h, s->inventory_cells, sizeof(s->inventory_cells)); + for (int cell_idx = 0; cell_idx < OSRS_INVENTORY_SIZE; cell_idx++) { + const OsrsInventoryCell* cell = &s->inventory_cells[cell_idx]; + h = fnv_u8(h, osrs_inventory_cell_item_index(cell)); + h = fnv_u16(h, osrs_inventory_cell_raw_osrs_id(cell)); + h = fnv_u8(h, osrs_inventory_cell_dose_count(cell)); + } h = fnv_bytes(h, s->dead_mobs, sizeof(s->dead_mobs)); h = fnv_i32(h, s->wave); return fnv_i32(h, s->tick); @@ -187,11 +200,11 @@ static const GoldenConfig CONFIGS[] = { #define EPISODE_TICKS 2000 static const uint64_t EXPECTED_STATE[NUM_CONFIGS] = { - 0xc34a1be07be9b76cULL, 0xe0b7035532045326ULL, 0x9bfdcadcd40dd503ULL, - 0xef441bdc10b4543aULL, 0x6bdf53afedce0649ULL, 0x567ee6344c8b711fULL, - 0x292f4392c8186e26ULL, 0x3d1c234ca61bbf8cULL, 0xe983644326ae38a6ULL, - 0xf572a5e51102d345ULL, 0x093845fc28bd3edfULL, 0x8bec31a8997b0e6dULL, - 0x4276518d9178c88aULL, 0x67a1cc0619493f62ULL, 0x65a6a9dfbe029b15ULL, + 0x3e4fbb35c1889d44ULL, 0x8c68f1dc6f54c2a6ULL, 0x71d36e99ebd433e8ULL, + 0x9c028c5906c58dbeULL, 0xd6183313a76b807fULL, 0x28b5ad296c307915ULL, + 0x1e408a9f378394e6ULL, 0x228334e4bd9b7f3aULL, 0x26463ee9c8e17cc8ULL, + 0x2762fdd6008979ddULL, 0xd9f8faf40e2d0657ULL, 0xff5ceeb4f52cb16fULL, + 0xef0befe2d848d410ULL, 0x38385138dce9f842ULL, 0x7444a507700231c1ULL, }; static const uint64_t EXPECTED_REWARD[NUM_CONFIGS] = { diff --git a/ocean/osrs/tools/gen_osrs_item_obs_table.c b/ocean/osrs/tools/gen_osrs_item_obs_table.c index 49762c88db..a1ff87fc1f 100644 --- a/ocean/osrs/tools/gen_osrs_item_obs_table.c +++ b/ocean/osrs/tools/gen_osrs_item_obs_table.c @@ -259,8 +259,7 @@ static void write_consumable_rows(FILE* out) { static void write_header(const char* path) { FILE* out = open_or_die(path); fprintf(out, - "/* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit.\n" - " * Dependency-free on purpose: src/ocean.cu includes it and no OSRS header. */\n" + "/* Generated by ocean/osrs/tools/gen_osrs_item_obs_table.c. Do not edit. */\n" "#ifndef OSRS_ITEM_OBS_GENERATED_H\n" "#define OSRS_ITEM_OBS_GENERATED_H\n\n" "#define OSRS_ITEM_CONTENT_COUNT %d\n" From 4f88a1c7598ee6e785e22ad4902ebd2f5755fa7c Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 13:07:57 +0300 Subject: [PATCH 28/50] Pass Inferno lab topology context --- .../inferno/encounter_inferno_lab.inc | 36 ++++++++++------- ocean/osrs/tests/test_inferno_lab.c | 40 +++++++++++++++++++ 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc index aa92f84bba..ebd0bcb007 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc @@ -161,9 +161,12 @@ static void inf_lab_clear_transient(InfernoState* s) { } } -static void inf_lab_refresh_geometry(InfernoState* s) { +static void inf_lab_refresh_geometry( + InfernoState* s, + const InfernoContext* ctx +) { inf_rebuild_npc_collision_flags(s); - inf_refresh_current_obs_slots(s); + inf_refresh_current_obs_slots_ctx(s, ctx); } static void inf_lab_remove_all_npcs(InfernoState* s) { @@ -171,10 +174,13 @@ static void inf_lab_remove_all_npcs(InfernoState* s) { s->dead_mob_count = 0; } -static void inf_lab_clear_npcs(InfernoState* s) { +static void inf_lab_clear_npcs( + InfernoState* s, + const InfernoContext* ctx +) { inf_lab_remove_all_npcs(s); inf_lab_clear_transient(s); - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); } static void inf_lab_apply_command_ctx( @@ -198,7 +204,7 @@ static void inf_lab_apply_command_ctx( s->player.x = cmd->as.tile.x; s->player.y = cmd->as.tile.y; inf_lab_clear_transient(s); - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); return; case INF_LAB_COMMAND_SPAWN_NPC: { @@ -228,7 +234,7 @@ static void inf_lab_apply_command_ctx( s->npcs[spawn->slot].attack_timer = spawn->timer.value; } inf_lab_clear_transient(s); - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); return; } @@ -242,7 +248,7 @@ static void inf_lab_apply_command_ctx( s->npcs[move->slot].target_x = move->x; s->npcs[move->slot].target_y = move->y; inf_lab_clear_transient(s); - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); return; } @@ -251,7 +257,7 @@ static void inf_lab_apply_command_ctx( inf_lab_require_slot(slot); inf_deactivate_npc(s, slot); inf_lab_clear_transient(s); - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); return; } @@ -262,7 +268,7 @@ static void inf_lab_apply_command_ctx( inf_lab_abort("cannot kill inactive npc slot %d", slot); s->npcs[slot].hp = 0; inf_apply_npc_death(s, slot); - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); return; } @@ -277,7 +283,7 @@ static void inf_lab_apply_command_ctx( inf_lab_abort("npc hp %d exceeds max hp %d", hp, s->npcs[slot].max_hp); s->npcs[slot].hp = hp; - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); return; } @@ -289,7 +295,7 @@ static void inf_lab_apply_command_ctx( inf_lab_abort("cannot set timer for inactive npc slot %d", slot); if (timer < 0) inf_lab_abort("npc timer must be nonnegative"); s->npcs[slot].attack_timer = timer; - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); return; } @@ -315,7 +321,7 @@ static void inf_lab_apply_command_ctx( } inf_reset_npc_player_los_frame(ctx); inf_lab_clear_transient(s); - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); return; } @@ -328,11 +334,11 @@ static void inf_lab_apply_command_ctx( s->wave_ready_delay = INF_START_READY_TICKS; inf_spawn_wave(s); inf_lab_clear_transient(s); - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); return; case INF_LAB_COMMAND_CLEAR_NPCS: - inf_lab_clear_npcs(s); + inf_lab_clear_npcs(s, ctx); return; case INF_LAB_COMMAND_STEP_TICKS: { @@ -341,7 +347,7 @@ static void inf_lab_apply_command_ctx( int actions[INF_NUM_ACTION_HEADS] = {0}; for (int t = 0; t < ticks; t++) inf_step_ctx((EncounterState*)s, (EncounterContext*)ctx, actions); - inf_lab_refresh_geometry(s); + inf_lab_refresh_geometry(s, ctx); return; } } diff --git a/ocean/osrs/tests/test_inferno_lab.c b/ocean/osrs/tests/test_inferno_lab.c index e69352d489..cea10c88e6 100644 --- a/ocean/osrs/tests/test_inferno_lab.c +++ b/ocean/osrs/tests/test_inferno_lab.c @@ -180,6 +180,45 @@ static void test_lab_spawn_wave_and_delete(void) { inf_destroy((EncounterState*)state); } +static void test_lab_kill_refresh_uses_finalized_context(void) { + printf("--- inferno lab kill refresh uses finalized context ---\n"); + InfernoState* state = make_lab_state(); + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_PLAYER, + .as.tile = {.x = 20, .y = 40}, + }); + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SPAWN_NPC, + .as.spawn_npc = { + .slot = 0, + .type = INF_NPC_RANGER, + .x = 28, + .y = 40, + .hp = {.kind = INF_LAB_OPTIONAL_INT_SET, .value = 1}, + .timer = {.kind = INF_LAB_OPTIONAL_INT_SET, .value = 0}, + }, + }); + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_KILL_NPC, + .as.npc_slot = {.slot = 0}, + }); + state->player.current_magic = 99; + state->player.equipped[GEAR_SLOT_WEAPON] = ITEM_KODAI_WAND; + state->npcs[0].death_ticks = 2; + inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_NPC_TIMER, + .as.npc_timer = {.slot = 0, .timer = 0}, + }); + ASSERT_INT_EQ("dying ranger is phantom barrage eligible", + inf_player_can_phantom_barrage_npc(state, &lab_context, 0), 1); + + ASSERT_INT_EQ("dying ranger reaches phantom window", + state->npcs[0].death_ticks, 2); + ASSERT_INT_EQ("dying ranger receives phantom obs slot", + inf_find_target_obs_slot(state, 0) >= 0, 1); + inf_destroy((EncounterState*)state); +} + static void test_lab_snapshot_restore_round_trip(void) { printf("--- inferno lab snapshot restore round trip ---\n"); @@ -278,6 +317,7 @@ int main(void) { test_lab_script_reaches_exact_forecast(); test_lab_json_contains_state_and_forecast(); test_lab_spawn_wave_and_delete(); + test_lab_kill_refresh_uses_finalized_context(); test_lab_snapshot_restore_round_trip(); return osrs_test_summary(); From 9a151769e62a0c1d05a57903b7c9b740471a5304 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 14:06:36 +0300 Subject: [PATCH 29/50] Define Colosseum consolidation contract --- ...26-08-12-colosseum-consolidation-design.md | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md diff --git a/docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md b/docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md new file mode 100644 index 0000000000..2c8569d9a2 --- /dev/null +++ b/docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md @@ -0,0 +1,96 @@ +# Colosseum Consolidation Design + +## Goal + +Make `valtteri/colo-5c-port` the single source of truth for the latest Colosseum environment and the shared OSRS runtime work. The consolidated environment must load `checkpoints/colo-w11-protein-best/run38_378535936.bin` with its `933`-observation and `451`-mask contract. + +## Source Lines + +The current 5c port copied the Colosseum tree from `0fc4b50c07cc4ec1435613fe4396b2b77ea8b647`. The missing committed behavior is the net delta through `8281518ea1cd238a59500aebb81937cc607588a0`. The missing uncommitted behavior was preserved in `3812fd454880a4605b2a9b39af41c72514e43a95`. + +Port behavior, not branch structure. The current 5c architecture remains authoritative. + +## Included Behavior + +### Sol laser observations + +Restore the honest multi-crystal laser observation pack and its episode diagnostics. Preserve `laser_obs_mode` because run38 records it and requires the enabled mode. The pack exposes the shared cooldown and per-crystal active, line-delta, and freeze state. + +### Manticore lifecycle + +Represent the first attack separately from the repeating barrage: + +1. Activate for seven ticks after spawn. +2. Wait until the player is in range and line of sight. +3. Start a ten-tick charge without firing. +4. Keep the three orbs hidden for the first three charge ticks. +5. Fire the first orb after the full charge. +6. Continue with the existing one-tick orb sequence, seven-tick lull, and two-Manticore stagger rules. + +Use the shared finalized arena topology for line of sight. + +### Thrall lifecycle + +Remove the summon action. A thrall exists from reset and resummons automatically on expiry. Its lifetime equals the player's current Magic level in the beginner profile and twice that level in the speedrun profile. It acquires a target only after an executed player attack, retains that target for twelve ticks, attacks every four ticks, and never targets Sol Heredit. + +### Volatility lifecycle + +Do not explode on the lethal hit. Explode when the corpse leaves the simulation after its death linger. Queue one render event in `ColosseumContext` and emit the family-specific spot animation from the render bridge. Use the shared arena topology for blast geometry. + +## Interface Contract + +The canonical model contract is: + +- observations: `933` +- action mask: `451` +- spell head: none or Death Charge +- snapshot version: `25` + +Older `934`/`452` checkpoints are intentionally incompatible. No compatibility slots, aliases, or dual model path remain. + +## Preserved Current Architecture + +Keep these current 5c systems unchanged except where their public call shape must carry the new behavior: + +- immutable `EncounterArenaTopology` +- shared route engine +- canonical `uint16_t` inventory content codes +- shared generated item metadata +- current encounter context ownership +- current Inferno, Zulrah, and NH PvP migrations + +The WIP's old per-Colosseum item table, item registry accesses, and source-layout edits do not return. + +## Excluded Changes + +Do not port: + +- the unrelated Puffer source-layout and Python migration +- `.cu` to `.inc` comment and include edits +- deleted or replaced per-Colosseum item metadata generators +- the asset manifest hash without its source asset +- stale golden or sim-invariant hashes +- the WIP test's accidental nonstandard FNV seed +- canceled next-style ungating and pending-style schedule experiments + +## Verification + +Add the WIP behavior tests before implementation and confirm they fail against current 5c. Then require: + +- Colosseum mechanics suite +- Colosseum golden suite +- Colosseum simulation-invariant suite +- Colosseum forecast and topology suites +- Colosseum snapshot round trip and stale-version rejection +- worker-count determinism +- shared inventory action, click, and item-effect suites +- focused Inferno mechanics and lab suites +- local Colosseum viewer build +- run38 model load with hidden size 512 and two layers +- a wave-11 viewer smoke run reporting the `933`/`451` contract + +Update deterministic baselines only after the new behavior explains every changed digest. + +## Integration + +Implement on a temporary branch in `/private/tmp`. Commit the focused consolidation, fast-forward `valtteri/colo-5c-port` after verification, then remove the temporary worktree and branch. Leave local checkpoints, binaries, screenshots, and other untracked artifacts untouched. From ac2f7e2c271e13eaf71e08916f6adfa435e341fa Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 14:10:01 +0300 Subject: [PATCH 30/50] Plan Colosseum consolidation --- .../2026-08-12-colosseum-consolidation.md | 439 ++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-12-colosseum-consolidation.md diff --git a/docs/superpowers/plans/2026-08-12-colosseum-consolidation.md b/docs/superpowers/plans/2026-08-12-colosseum-consolidation.md new file mode 100644 index 0000000000..d449cf1d34 --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-colosseum-consolidation.md @@ -0,0 +1,439 @@ +# Colosseum Consolidation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Consolidate the latest Colosseum behavior onto the optimized 5c branch and make the run38 `933`-observation, `451`-mask checkpoint contract canonical. + +**Architecture:** Treat the current 5c branch as the structural source of truth. Port the net behavior from `0fc4b50c..8281518ea` and the Colosseum-only behavior from `3812fd454`, adapting every call to the current shared topology, route, context, and item-metadata APIs. + +**Tech Stack:** C11 single-header encounter implementation, shared OSRS SDK, clang, local Metal viewer, deterministic golden and semantic-hash probes. + +## Global Constraints + +- Keep `EncounterArenaTopology`, the shared route engine, canonical inventory content codes, and shared item metadata. +- Canonical model contract: `COLO_NUM_OBS == 933`, `COLO_ACTION_MASK_SIZE == 451`, spell head `{none, Death Charge}`. +- Canonical snapshot version: `25`. +- Preserve `laser_obs_mode` and require `1` for run38 evaluation. +- Do not port the Puffer source-layout migration, old item-table files, asset-manifest hash, stale baselines, or the altered FNV seed. +- Do not modify Inferno, Zulrah, NH PvP, Puffer internals, checkpoints, generated assets, or local untracked artifacts. +- Every semantic change gets a failing contract test before implementation. + +--- + +### Task 1: Isolate and establish the red baseline + +**Files:** +- Modify later: `ocean/osrs/encounters/colosseum/*.inc` +- Test: `ocean/osrs/tests/test_colosseum_modifiers.c` + +**Interfaces:** +- Consumes: `valtteri/colo-5c-port` at the plan commit. +- Produces: `/private/tmp/puffer-colo-consolidation` on `valtteri/colo-consolidation`. + +- [ ] **Step 1: Create the temporary branch and worktree** + +```bash +pwd +git worktree add /private/tmp/puffer-colo-consolidation -b valtteri/colo-consolidation valtteri/colo-5c-port +``` + +Expected: worktree starts at the plan commit with no tracked modifications. + +- [ ] **Step 2: Compile and run the current focused baseline** + +```bash +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_consolidation_baseline +/tmp/colo_consolidation_baseline +``` + +Expected: `13550/13550 tests passed`. + +- [ ] **Step 3: Confirm current interface values before adding red tests** + +Use the existing `test_primary_head_resolution` and fuzz assertions. + +Expected: `COLO_NUM_OBS == 922`, `COLO_ACTION_MASK_SIZE == 452`, `COLO_SPELL_DIM == 3`. + +--- + +### Task 2: Restore honest Sol laser observations + +**Files:** +- Modify: `config/osrs_colosseum.ini` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` +- Test: `ocean/osrs/tests/test_colosseum_modifiers.c` + +**Interfaces:** +- Consumes: current `ColoSolState`, `ColosseumConfig`, `ColosseumLog`, and `col_write_boss_obs`. +- Produces: `laser_obs_mode`, twelve added boss-observation floats, and episode laser diagnostics. + +- [ ] **Step 1: Add failing laser-pack contract tests** + +Add assertions that the enabled pack reports one cooldown plus four records of three floats and that disabled mode zeroes exactly those thirteen laser-pack channels without changing width. Assert the intermediate interface: + +```c +CHECK("laser pack expands obs 922 to 934", COLO_NUM_OBS == 934); +``` + +Run the modifiers suite. Expected: compile or assertion failure because the pack and config field do not exist. + +- [ ] **Step 2: Add final laser state and configuration fields** + +Add to `ColoSolState`: + +```c +int laser_volley_active; +int laser_show_seen; +int laser_aligned_show; +int laser_n_aligned_show; +int laser_n_active_at_fire; +``` + +Add `int laser_obs_mode` to `ColosseumConfig` with default `1`, binary config parsing, and `laser_obs_mode = 1` in `config/osrs_colosseum.ini`. Add the nine `laser_*` float counters from `8281518ea` to `ColosseumLog` and preserve their existing metric names. + +- [ ] **Step 3: Port the final multi-crystal observation writer** + +Define: + +```c +#define COLO_SOL_LASER_CRYSTAL_FEATS 3 +#define COLO_SOL_LASER_OBS_SIZE \ + (1 + COLO_SOL_MAX_CRYSTALS * COLO_SOL_LASER_CRYSTAL_FEATS) +#define COLO_BOSS_OBS_PREMOVE_TAIL_SIZE (2 + COLO_SOL_LASER_OBS_SIZE) +``` + +Change `col_write_boss_obs` to accept `int laser_obs_mode`. Write the normalized volley cooldown, then each crystal's active flag, signed line delta, and normalized freeze timer. Advance the same indices with zeros when disabled. + +- [ ] **Step 4: Port final laser event accounting** + +Reset volley diagnostics at Sol reset. At show, prefire, and fire transitions, count active and aligned crystals and update the existing episode log fields. Do not port the canceled next-style or pending-style experiments. + +- [ ] **Step 5: Run and commit** + +```bash +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_laser_pack +/tmp/colo_laser_pack +git add config/osrs_colosseum.ini ocean/osrs/encounters/colosseum ocean/osrs/tests/test_colosseum_modifiers.c +git commit -m "Restore honest Sol laser observations" +``` + +Expected: modifiers suite passes with `COLO_NUM_OBS == 934` and mask `452`. + +--- + +### Task 3: Port the Manticore activation state machine + +**Files:** +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc` +- Test: `ocean/osrs/tests/test_colosseum_modifiers.c` + +**Interfaces:** +- Consumes: `col_npc_has_los_to_player(s, ctx, npc)` and current peer-stagger logic. +- Produces: `ColoManticorePhase` and explicit activation, target-wait, initial-charge, and repeating states. + +- [ ] **Step 1: Add the failing WIP Manticore contract** + +Port `test_manticore_initial_charge_timing_and_target_gate` from `3812fd454`, but route line-of-sight through `ColosseumContext`. Change misleading test text from “visible tick” to “spawn tick”. + +Expected red conditions: the current Manticore arms before the seventh spawn tick, exposes orbs too early, and lacks the phase enum. + +- [ ] **Step 2: Add the explicit phase type** + +```c +typedef enum { + COLO_MANTICORE_PHASE_ACTIVATING = 0, + COLO_MANTICORE_PHASE_WAITING_FOR_TARGET, + COLO_MANTICORE_PHASE_CHARGING, + COLO_MANTICORE_PHASE_REPEATING, +} ColoManticorePhase; + +typedef struct { + ColoManticorePhase phase; + int cycle_step; + AttackStyle orb_style[3]; + AttackStyle fixed_orb_style[3]; +} ColoManticoreState; + +#define COLO_MANTICORE_ACTIVATION_TICKS 7 +#define COLO_MANTICORE_CHARGE_TICKS 10 +#define COLO_MANTICORE_ARM_ANIMATION_TICKS 3 +``` + +Initialize Manticores in `ACTIVATING` with attack timer `7`. + +- [ ] **Step 3: Implement target gating and phase transitions** + +Use: + +```c +static int col_manticore_has_target( + const ColosseumState* s, + const ColosseumContext* ctx, + const ColoNPC* npc, + const ColoNpcStats* stats +) { + int dist = col_npc_dist_to_player(s, npc); + return dist >= 1 && dist <= stats->attack_range && + col_npc_has_los_to_player(s, ctx, npc); +} +``` + +Count activation down independently of line of sight. In `WAITING_FOR_TARGET`, arm only after a valid target appears. In `CHARGING`, consume ten full ticks, then require a valid target before the first shot. Switch to `REPEATING` when firing starts. Adapt peer staggering so only equivalent ready states delay each other. + +- [ ] **Step 4: Hide premature telegraphs and oracle data** + +Make Manticore orbs and next-prayer data invisible during activation, target wait, and the first three charge ticks. Preserve existing one-tick barrage tells after the first shot. + +- [ ] **Step 5: Run and commit** + +```bash +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_manticore +/tmp/colo_manticore +git add ocean/osrs/encounters/colosseum ocean/osrs/tests/test_colosseum_modifiers.c +git commit -m "Model Manticore activation and charge" +``` + +Expected: all Manticore timing, telegraph, shared-cycle, and stagger tests pass. + +--- + +### Task 4: Make the thrall lifecycle automatic + +**Files:** +- Modify: `config/osrs_colosseum.ini` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_lab_json.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc` +- Test: `ocean/osrs/tests/test_colosseum_modifiers.c` + +**Interfaces:** +- Consumes: `tick_scratch.player_attacked`, `player_attack_npc_idx`, current Magic, loadout profile, and pending-hit queues. +- Produces: automatic thrall state and final `933`/`451` model contract. + +- [ ] **Step 1: Add the failing automatic-thrall test** + +Port the final `test_thrall_regression` from `3812fd454`. Assert reset lifetime, attack-triggered target acquisition, twelve-tick aggro, four-tick attacks, expiry resummon, profile multiplier, and Sol immunity. + +Expected: compile failure on `thrall_aggro_ticks_left` and `thrall_lifetime_total`. + +- [ ] **Step 2: Replace representational state** + +Remove `thrall_active` and `thrall_recast_cd`. Add: + +```c +int thrall_target_slot; +int thrall_aggro_ticks_left; +int thrall_lifetime_left; +int thrall_lifetime_total; +int thrall_attack_timer; +``` + +Define `COLO_THRALL_AGGRO_TICKS 12`. Compute lifetime from current Magic, doubled only for `COLO_LOADOUT_PROFILE_SPEEDRUN`. + +- [ ] **Step 3: Implement automatic reset, targeting, and expiry** + +`col_resummon_thrall` sets no target, zero aggro, full derived lifetime, and a four-tick attack timer. Call it at episode reset and expiry. Acquire a target only when `tick_scratch.player_attacked` names a live non-Sol NPC. Decrement aggro only on ticks without a new player attack. Clear invalid or expired targets. + +- [ ] **Step 4: Remove the summon action and rewrite observations** + +Set `COLO_SPELL_DIM` to `2`, keep only `COLO_SPELL_DEATH_CHARGE = 1`, delete the summon mask branch, and reduce `COLO_THRALL_DC_OBS_SIZE` from `6` to `5`. Observe normalized lifetime, normalized aggro, Death Charge active, window, and cooldown. Render the implicit thrall unconditionally. + +Set `action_mask_size = 451`. Assert `COLO_NUM_OBS == 933` and `COLO_ACTION_MASK_SIZE == 451`. + +- [ ] **Step 5: Run and commit** + +```bash +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_thrall +/tmp/colo_thrall +git add config/osrs_colosseum.ini ocean/osrs/encounters/colosseum ocean/osrs/tests/test_colosseum_modifiers.c +git commit -m "Make Colosseum thralls automatic" +``` + +Expected: thrall and Death Charge contracts pass at `933`/`451`. + +--- + +### Task 5: Move Volatility to corpse removal + +**Files:** +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc` +- Test: `ocean/osrs/tests/test_colosseum_modifiers.c` + +**Interfaces:** +- Consumes: `ColosseumContext.route_topology` and NPC death linger. +- Produces: context-owned Volatility render events and corpse-removal damage. + +- [ ] **Step 1: Add the failing corpse-removal contract** + +Port `test_volatility_explodes_on_corpse_removal` from `3812fd454`. Assert no lethal-hit explosion, no explosion during linger, damage on removal, and one family-specific spot animation. + +Expected: current code damages on the lethal hit and emits no removal event. + +- [ ] **Step 2: Add context-owned event storage** + +```c +int volatility_explosion_count; +int volatility_explosion_x[COLO_MAX_NPCS]; +int volatility_explosion_y[COLO_MAX_NPCS]; +ColoNpcType volatility_explosion_type[COLO_MAX_NPCS]; +``` + +Clear the count in `col_context_clear_render_events`. + +- [ ] **Step 3: Move the explosion trigger** + +Remove `col_mod_volatility_on_death` from `col_apply_npc_death`. Change death-linger ticking to accept `ColosseumContext*`. Immediately before deactivating a corpse, call `col_mod_volatility_on_corpse_removed` with the shared finalized topology and queue its render event. + +- [ ] **Step 4: Emit family-specific spot animations** + +Map human, Manticore, Colossus, Minotaur, and Sol families to GFX IDs `2713`, `2721`, `2722`, `2723`, and `2724`. Emit a stationary overlay projectile at the corpse origin during `col_render_post_tick_ctx`. + +- [ ] **Step 5: Run and commit** + +```bash +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_volatility +/tmp/colo_volatility +git add ocean/osrs/encounters/colosseum ocean/osrs/tests/test_colosseum_modifiers.c +git commit -m "Explode Volatility on corpse removal" +``` + +Expected: modifiers suite passes and the new render event appears exactly once. + +--- + +### Task 6: Finalize snapshot and deterministic contracts + +**Files:** +- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` +- Modify: `ocean/osrs/tests/test_colosseum_golden.c` +- Modify: `ocean/osrs/tests/probe_colo_sim_invariant.c` +- Modify only if required: focused Colosseum forecast and topology tests + +**Interfaces:** +- Consumes: final state layout and `933`/`451` interface. +- Produces: snapshot version `25` and explained deterministic baselines. + +- [ ] **Step 1: Bump and test snapshot version** + +Set: + +```c +#define COLO_SNAPSHOT_VERSION 25u +``` + +Keep stale-version rejection and round-trip tests. Expected: version 24 abort fixture rejects and version 25 round trip passes. + +- [ ] **Step 2: Run deterministic probes before changing baselines** + +```bash +clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_golden.c -lm -o /tmp/colo_golden_new +/tmp/colo_golden_new +clang -std=c11 -O2 -I. ocean/osrs/tests/probe_colo_sim_invariant.c -lm -o /tmp/colo_sim_new +/tmp/colo_sim_new +``` + +Expected: digest mismatches caused by laser observation width, Manticore timing, thrall state, and Volatility timing. + +- [ ] **Step 3: Update only observed final digests** + +Keep `FNV_OFFSET` unchanged at `1469598103934665603ULL`. Replace the 12 golden and 12 sim-invariant expected values with the values printed by the final candidate. Add a short reason beside the baseline arrays only where an existing baseline explanation already exists. + +- [ ] **Step 4: Re-run exact deterministic suites** + +Expected: `12/12 configs match baseline` in both binaries. + +- [ ] **Step 5: Commit** + +```bash +git add ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc ocean/osrs/tests/test_colosseum_golden.c ocean/osrs/tests/probe_colo_sim_invariant.c +git commit -m "Finalize consolidated Colosseum contract" +``` + +--- + +### Task 7: Verify run38 and integrate + +**Files:** +- No source changes unless a real verification failure identifies a defect. + +**Interfaces:** +- Consumes: final consolidation branch. +- Produces: verified canonical `valtteri/colo-5c-port` and no temporary worktree. + +- [ ] **Step 1: Run focused shared and encounter suites** + +Compile and run: + +```text +ocean/osrs/tests/test_colosseum_modifiers.c +ocean/osrs/tests/test_colosseum_golden.c +ocean/osrs/tests/probe_colo_sim_invariant.c +ocean/osrs/tests/test_colosseum_forecast_exact.c +ocean/osrs/tests/test_encounter_route_topology_setup.c +ocean/osrs/tests/test_osrs_inventory_actions.c +ocean/osrs/tests/test_osrs_inventory_clicks.c +ocean/osrs/tests/test_osrs_item_effect_masks.c +ocean/osrs/tests/test_inferno_attack_styles.c +ocean/osrs/tests/test_inferno_lab.c +``` + +Expected: every suite passes with no unexplained changed digest. + +- [ ] **Step 2: Run worker-count determinism** + +```bash +clang -std=c11 -O2 -I. -Xpreprocessor -fopenmp \ + -I/opt/homebrew/opt/libomp/include \ + ocean/osrs/tests/probe_env_thread_determinism.c \ + -L/opt/homebrew/opt/libomp/lib -lomp -lm \ + -o /tmp/colo_worker_determinism +/tmp/colo_worker_determinism +``` + +Expected: every one-worker versus multi-worker comparison reports `IDENTICAL`. + +- [ ] **Step 3: Build and smoke the local viewer** + +```bash +./build.sh osrs_colosseum --local +./osrs_colosseum \ + --model checkpoints/colo-w11-protein-best/run38_378535936.bin \ + --start-wave 11 \ + --hidden-size 512 \ + --num-layers 2 +``` + +Expected: model loads, viewer reports observations `933` and mask `451`, and a wave-11 episode advances without aborting. + +- [ ] **Step 4: Commit any verification-driven fix separately** + +Only if a reproduced failure required a source correction. Re-run the exact failed check after the fix. + +- [ ] **Step 5: Fast-forward canonical and remove isolation** + +From the canonical checkout: + +```bash +pwd +git merge --ff-only valtteri/colo-consolidation +git worktree remove /private/tmp/puffer-colo-consolidation +git branch -d valtteri/colo-consolidation +``` + +Expected: `valtteri/colo-5c-port` points at the verified consolidation tip, the temporary path is gone, and existing untracked local artifacts remain untouched. From 0763de2d700cc8d7ee60742725b82a2ac46c08e1 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 14:20:13 +0300 Subject: [PATCH 31/50] Fix OSRS asset archive checksum --- .../plans/2026-08-12-colosseum-consolidation.md | 17 ++++++++++++++--- ...2026-08-12-colosseum-consolidation-design.md | 5 ++++- ocean/osrs/asset_manifest.json | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/plans/2026-08-12-colosseum-consolidation.md b/docs/superpowers/plans/2026-08-12-colosseum-consolidation.md index d449cf1d34..1075df811c 100644 --- a/docs/superpowers/plans/2026-08-12-colosseum-consolidation.md +++ b/docs/superpowers/plans/2026-08-12-colosseum-consolidation.md @@ -14,7 +14,7 @@ - Canonical model contract: `COLO_NUM_OBS == 933`, `COLO_ACTION_MASK_SIZE == 451`, spell head `{none, Death Charge}`. - Canonical snapshot version: `25`. - Preserve `laser_obs_mode` and require `1` for run38 evaluation. -- Do not port the Puffer source-layout migration, old item-table files, asset-manifest hash, stale baselines, or the altered FNV seed. +- Port the verified `osrs-assets-v22` manifest SHA. Do not port the Puffer source-layout migration, old item-table files, stale baselines, or the altered FNV seed. - Do not modify Inferno, Zulrah, NH PvP, Puffer internals, checkpoints, generated assets, or local untracked artifacts. - Every semantic change gets a failing contract test before implementation. @@ -23,6 +23,7 @@ ### Task 1: Isolate and establish the red baseline **Files:** +- Modify: `ocean/osrs/asset_manifest.json` - Modify later: `ocean/osrs/encounters/colosseum/*.inc` - Test: `ocean/osrs/tests/test_colosseum_modifiers.c` @@ -39,7 +40,17 @@ git worktree add /private/tmp/puffer-colo-consolidation -b valtteri/colo-consoli Expected: worktree starts at the plan commit with no tracked modifications. -- [ ] **Step 2: Compile and run the current focused baseline** +- [ ] **Step 2: Pin and install the published asset archive** + +Set the `osrs-assets-v22` SHA to `2f908da5b5ddf148c0cbef48ad6334c5253c04c6cd83782340bc2f1c5dffc7ad`, then run: + +```bash +bash ocean/osrs/scripts/setup-data.sh +``` + +Expected: the downloaded release archive matches the manifest and installs the ignored local data. + +- [ ] **Step 3: Compile and run the current focused baseline** ```bash clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_consolidation_baseline @@ -48,7 +59,7 @@ clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/c Expected: `13550/13550 tests passed`. -- [ ] **Step 3: Confirm current interface values before adding red tests** +- [ ] **Step 4: Confirm current interface values before adding red tests** Use the existing `test_primary_head_resolution` and fuzz assertions. diff --git a/docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md b/docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md index 2c8569d9a2..e08c79d0c4 100644 --- a/docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md +++ b/docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md @@ -16,6 +16,10 @@ Port behavior, not branch structure. The current 5c architecture remains authori Restore the honest multi-crystal laser observation pack and its episode diagnostics. Preserve `laser_obs_mode` because run38 records it and requires the enabled mode. The pack exposes the shared cooldown and per-crystal active, line-delta, and freeze state. +### Asset archive provenance + +Pin the `osrs-assets-v22` release archive to `2f908da5b5ddf148c0cbef48ad6334c5253c04c6cd83782340bc2f1c5dffc7ad`. A fresh setup download produced this SHA. The prior manifest SHA rejects the published archive and prevents clean worktree builds. + ### Manticore lifecycle Represent the first attack separately from the repeating barrage: @@ -68,7 +72,6 @@ Do not port: - the unrelated Puffer source-layout and Python migration - `.cu` to `.inc` comment and include edits - deleted or replaced per-Colosseum item metadata generators -- the asset manifest hash without its source asset - stale golden or sim-invariant hashes - the WIP test's accidental nonstandard FNV seed - canceled next-style ungating and pending-style schedule experiments diff --git a/ocean/osrs/asset_manifest.json b/ocean/osrs/asset_manifest.json index 7bb8e00e83..08b2060d9e 100644 --- a/ocean/osrs/asset_manifest.json +++ b/ocean/osrs/asset_manifest.json @@ -4,7 +4,7 @@ "archive": { "name": "osrs-assets-v22.tar.gz", "url": "https://github.com/valtterivalo/PufferLib/releases/download/osrs-assets-v22/osrs-assets-v22.tar.gz", - "sha256": "48c6c0c0ebbb6454b00732977f987267797ad70cf1ad7ee8e0f0781014bb6c12", + "sha256": "2f908da5b5ddf148c0cbef48ad6334c5253c04c6cd83782340bc2f1c5dffc7ad", "strip_components": 1 }, "required_groups": [ From 5e30201abf0fc891f2f4c4fac6ccd8c7dcf989a7 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 14:20:13 +0300 Subject: [PATCH 32/50] Restore honest Sol laser observations --- config/osrs_colosseum.ini | 1 + .../colosseum/encounter_colosseum_boss.inc | 88 ++++++++++++++++--- .../colosseum/encounter_colosseum_helpers.inc | 1 + .../colosseum/encounter_colosseum_model.inc | 17 +++- .../encounter_colosseum_obs_mask.inc | 43 +++++++-- .../encounter_colosseum_render_snapshot.inc | 3 + ocean/osrs/tests/test_colosseum_modifiers.c | 84 +++++++++++++++++- ocean/osrs_colosseum/osrs_colosseum.h | 28 ++++++ 8 files changed, 244 insertions(+), 21 deletions(-) diff --git a/config/osrs_colosseum.ini b/config/osrs_colosseum.ini index 71dea264c8..4d5cfb0fb8 100644 --- a/config/osrs_colosseum.ini +++ b/config/osrs_colosseum.ini @@ -21,6 +21,7 @@ mask_inventory_heads = 0 late_start_state_mode = 2 late_start_supply_fraction_per_wave = 0.07 bis_gear_oracle_mode = 0 +laser_obs_mode = 1 episode_max_ticks_override = 0 prayer_switch_fail_prob = 0.0 player_damage_received_scale = 1.0 diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc index 89b1ff93ce..7b0f0ab6f7 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc @@ -40,6 +40,11 @@ static void col_sol_begin_boss_arena(ColosseumState* s) { s->sol.boss_idx = -1; s->sol.force_spear = 1; s->sol.laser_cooldown = COLO_SOL_CRYSTAL_COOLDOWN_MIN; + s->sol.laser_volley_active = 0; + s->sol.laser_show_seen = 0; + s->sol.laser_aligned_show = 0; + s->sol.laser_n_aligned_show = 0; + s->sol.laser_n_active_at_fire = 0; s->sol.boss_arena_min_x = COLO_BOSS_ARENA_MIN_X; s->sol.boss_arena_min_y = COLO_BOSS_ARENA_MIN_Y; s->sol.boss_arena_max_x = COLO_BOSS_ARENA_MAX_X; @@ -372,9 +377,21 @@ static void col_sol_spawn_crystal(ColosseumState* s) { static void col_sol_fire_lasers(ColosseumState* s) { SolHereditState* sol = &s->sol; - for (int i = 0; i < sol->crystal_count; i++) - if (sol->crystals[i].active) - sol->crystals[i].firing_freeze = COLO_SOL_LASER_FREEZE; + int active = 0; + int aligned = 0; + for (int i = 0; i < sol->crystal_count; i++) { + if (!sol->crystals[i].active) continue; + sol->crystals[i].firing_freeze = COLO_SOL_LASER_FREEZE; + active++; + if (col_sol_crystal_in_line(s, &sol->crystals[i])) aligned++; + } + sol->laser_volley_active = 1; + sol->laser_show_seen = 0; + sol->laser_aligned_show = 0; + sol->laser_n_aligned_show = 0; + sol->laser_n_active_at_fire = active; + s->log.laser_volleys += 1.0f; + if (aligned > 0) s->log.laser_aligned_at_fire += 1.0f; sol->laser_cooldown = sol->phase >= 5 ? COLO_SOL_CRYSTAL_COOLDOWN_ENRAGE : COLO_SOL_CRYSTAL_COOLDOWN_MIN @@ -383,6 +400,13 @@ static void col_sol_fire_lasers(ColosseumState* s) { static void col_sol_tick_crystals(ColosseumState* s) { SolHereditState* sol = &s->sol; + int aligned_show = 0; + int aligned_pre = 0; + int aligned_damage = 0; + int showing = 0; + int prefire = 0; + int damaging = 0; + int firing = 0; for (int i = 0; i < sol->crystal_count; i++) { ColoSolCrystal* c = &sol->crystals[i]; if (!c->active) continue; @@ -397,22 +421,60 @@ static void col_sol_tick_crystals(ColosseumState* s) { c->move_timer = COLO_SOL_CRYSTAL_MOVE_TICKS; } - if (c->firing_freeze == COLO_SOL_LASER_DAMAGE_FREEZE && - col_sol_crystal_in_line(s, c)) { - int dmg = COLO_SOL_LASER_MIN_HIT - + encounter_rand_int(&s->rng_state, COLO_SOL_LASER_RAND); - int applied = col_damage_player_from_sol( - s, dmg, COLO_SOL_DAMAGE_CRYSTAL_LASER); - if (applied > 0) { - s->last_hit_by_type = COLO_SOL_HEREDIT; - col_mod_on_player_damaged( - s, applied, COLO_DAMAGE_SOL_CRYSTAL_LASER); + if (c->firing_freeze > 0) firing = 1; + if (c->firing_freeze == COLO_SOL_LASER_BEAM_SHOW_MAX) { + showing = 1; + if (col_sol_crystal_in_line(s, c)) aligned_show++; + } + if (c->firing_freeze == COLO_SOL_LASER_DAMAGE_FREEZE + 1) { + prefire = 1; + if (col_sol_crystal_in_line(s, c)) aligned_pre++; + } + if (c->firing_freeze == COLO_SOL_LASER_DAMAGE_FREEZE) { + damaging = 1; + if (col_sol_crystal_in_line(s, c)) { + aligned_damage++; + int dmg = COLO_SOL_LASER_MIN_HIT + + encounter_rand_int(&s->rng_state, COLO_SOL_LASER_RAND); + int applied = col_damage_player_from_sol( + s, dmg, COLO_SOL_DAMAGE_CRYSTAL_LASER); + if (applied > 0) { + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged( + s, applied, COLO_DAMAGE_SOL_CRYSTAL_LASER); + s->log.laser_hits += 1.0f; + s->log.laser_dmg += (float)applied; + } } } c->move_timer--; c->firing_freeze--; } + if (showing) { + sol->laser_show_seen = 1; + sol->laser_n_aligned_show = aligned_show; + if (aligned_show > 0) { + sol->laser_aligned_show = 1; + s->log.laser_aligned_at_show += 1.0f; + } + } + if (prefire && aligned_pre > 0) + s->log.laser_aligned_at_pre += 1.0f; + if (damaging) { + if (aligned_damage > 0) s->log.laser_aligned_at_damage += 1.0f; + if (sol->laser_aligned_show && aligned_damage == 0) + s->log.laser_react_ok += 1.0f; + if (sol->laser_aligned_show && aligned_damage > 0) + s->log.laser_react_fail += 1.0f; + } + if (sol->laser_volley_active && !firing) { + sol->laser_volley_active = 0; + sol->laser_show_seen = 0; + sol->laser_aligned_show = 0; + sol->laser_n_aligned_show = 0; + sol->laser_n_active_at_fire = 0; + } } static int col_sol_molten_sand_at(const SolHereditState* sol, int x, int y) { diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc index ec09abc8a4..7a1a71e2b1 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc @@ -283,6 +283,7 @@ static ColConfig col_default_config(void) { .mask_inventory_heads = 0, .late_start_state_mode = 2, .late_start_supply_fraction_per_wave = 0.07f, + .laser_obs_mode = 1, .prayer_switch_fail_prob = 0.0f, .player_damage_received_scale = 1.0f, .damage_scale_anneal_start = 1.0f, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index 9633c5304a..2403020189 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -687,6 +687,11 @@ typedef struct { ColoSolCrystal crystals[COLO_SOL_MAX_CRYSTALS]; int crystal_count; int laser_cooldown; + int laser_volley_active; + int laser_show_seen; + int laser_aligned_show; + int laser_n_aligned_show; + int laser_n_active_at_fire; ColoSolBeam beams[COLO_SOL_BEAM_MAX]; int hazard_tile_x[COLO_SOL_HAZARD_TILES_MAX]; @@ -1269,6 +1274,7 @@ typedef struct { float late_start_supply_fraction_per_wave; int bis_gear_oracle_mode; + int laser_obs_mode; int episode_max_ticks_override; @@ -1436,6 +1442,15 @@ typedef struct { float javelin_damage_by_source[COLO_NUM_JAVELIN_DAMAGE_SOURCES]; float death_by_source[COLO_NUM_DAMAGE_SOURCES]; float doom_death_by_source[COLO_NUM_DAMAGE_SOURCES]; + float laser_volleys; + float laser_hits; + float laser_dmg; + float laser_aligned_at_fire; + float laser_aligned_at_show; + float laser_aligned_at_pre; + float laser_aligned_at_damage; + float laser_react_ok; + float laser_react_fail; float outcome_score; float max_wave_depth; } ColosseumLog; @@ -1552,7 +1567,7 @@ typedef struct { ColosseumLog log; ColoObsMemoCaches obs_memos; } ColosseumState; -static_assert(sizeof(ColosseumState) == 29720, "ColosseumState serialized layout"); +static_assert(sizeof(ColosseumState) == 29776, "ColosseumState serialized layout"); typedef enum { COLO_DMG_OFFPRAY = 0, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc index ab0ac3adaa..9574d46f4a 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -65,8 +65,12 @@ typedef enum { #define COLO_BOSS_OBS_PARRY_DAMAGE_OFFSET \ (COLO_BOSS_OBS_PARRY_TICKS_OFFSET + COLO_SOL_PARRY_LAND_TICKS_MAX) #define COLO_BOSS_OBS_PREMOVE_HAZARD_OFFSET 37 +#define COLO_SOL_LASER_CRYSTAL_FEATS 3 +#define COLO_SOL_LASER_OBS_SIZE \ + (1 + COLO_SOL_MAX_CRYSTALS * COLO_SOL_LASER_CRYSTAL_FEATS) +#define COLO_BOSS_OBS_PREMOVE_TAIL_SIZE (2 + COLO_SOL_LASER_OBS_SIZE) #define COLO_BOSS_OBS_SIZE \ - (COLO_BOSS_OBS_PREMOVE_HAZARD_OFFSET + COLO_SOL_HAZARD_SOURCE_COUNT) + (COLO_BOSS_OBS_PREMOVE_HAZARD_OFFSET + COLO_BOSS_OBS_PREMOVE_TAIL_SIZE) #define COLO_OBS_PENDING_HITS 16 #define COLO_FEATURES_PER_HIT 7 @@ -662,7 +666,12 @@ static ColoSolParryTell col_sol_parry_tell(const ColosseumState* s) { }; } -static void col_write_boss_obs(const ColosseumState* s, float* obs, int* idx) { +static void col_write_boss_obs( + const ColosseumState* s, + float* obs, + int* idx, + int laser_obs_mode +) { int i = *idx; const SolHereditState* sol = &s->sol; int px = s->player.x, py = s->player.y; @@ -707,10 +716,34 @@ static void col_write_boss_obs(const ColosseumState* s, float* obs, int* idx) { obs[i++] = sol->next_attack_guaranteed_max ? 1.0f : 0.0f; if (col_sol_clamp_active(s)) { obs[i++] = col_sol_premove_aoe_hit(s) ? 1.0f : 0.0f; - obs[i++] = col_sol_premove_laser_hit(s) ? 1.0f : 0.0f; obs[i++] = col_sol_premove_molten_hit(s) ? 1.0f : 0.0f; + if (laser_obs_mode) { + obs[i++] = col_clamp01((float)sol->laser_cooldown / (float)( + COLO_SOL_CRYSTAL_COOLDOWN_MIN + COLO_SOL_CRYSTAL_COOLDOWN_RAND - 1)); + for (int crystal = 0; crystal < COLO_SOL_MAX_CRYSTALS; crystal++) { + if (crystal < sol->crystal_count && sol->crystals[crystal].active) { + const ColoSolCrystal* c = &sol->crystals[crystal]; + obs[i++] = 1.0f; + if (c->edge == COLO_SOL_EDGE_NORTH || + c->edge == COLO_SOL_EDGE_SOUTH) + obs[i++] = (float)(c->x - px) / (float)COLO_ARENA_WIDTH; + else + obs[i++] = (float)(c->y - py) / (float)COLO_ARENA_HEIGHT; + obs[i++] = c->firing_freeze > 0 + ? col_clamp01( + (float)c->firing_freeze / (float)COLO_SOL_LASER_FREEZE) + : 0.0f; + } else { + obs[i++] = 0.0f; + obs[i++] = 0.0f; + obs[i++] = 0.0f; + } + } + } else { + i += COLO_SOL_LASER_OBS_SIZE; + } } else { - i += COLO_SOL_HAZARD_SOURCE_COUNT; + i += COLO_BOSS_OBS_PREMOVE_TAIL_SIZE; } *idx = i; } @@ -1714,7 +1747,7 @@ static void col_write_obs_ctx( obs[i++] = (s->wave_spawn_delay > 0 || s->wave_ready_delay > 0) ? 1.0f : 0.0f; assert(i == COLO_OBS_AFTER_WAVE); - col_write_boss_obs(s, obs, &i); + col_write_boss_obs(s, obs, &i, ctx->config.laser_obs_mode); assert(i == COLO_OBS_AFTER_BOSS); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_BOSS); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 29a50cc9ab..666db48bdc 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -28,6 +28,9 @@ static void col_put_int_ctx( else if (strcmp(key, "mask_inventory_heads") == 0) ctx->config.mask_inventory_heads = encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "laser_obs_mode") == 0) + ctx->config.laser_obs_mode = + encounter_require_binary_config("colosseum", key, value); else if (strcmp(key, "farm_safe_damage_cap") == 0) ctx->config.farm_safe_damage_cap = encounter_require_binary_config("colosseum", key, value); diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index 1104ffa8d1..a66847be7e 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -4744,6 +4744,9 @@ static void test_sol_laser_react_window(void) { col_sol_spawn_crystal(&s); sol_move_player(&s, s.sol.crystals[0].x, 14); col_sol_fire_lasers(&s); + CHECK("laser volley accounting records an aligned launch", + s.log.laser_volleys == 1.0f && + s.log.laser_aligned_at_fire == 1.0f); s.player.current_hitpoints = 99; for (int t = 0; t < 3; t++) step_and_observe(&s, &ctx, idle); CHECK("the beam telegraph opens 3 ticks after the volley (freeze 6)", @@ -4751,6 +4754,10 @@ static void test_sol_laser_react_window(void) { for (int t = 0; t < 3; t++) step_and_observe(&s, &ctx, idle); CHECK("the 3 reaction ticks pass without damage", s.player.current_hitpoints == 99); + CHECK("laser show accounting records the aligned telegraph", + s.log.laser_aligned_at_show == 1.0f); + CHECK("laser prefire accounting records the still-aligned player", + s.log.laser_aligned_at_pre == 1.0f); float laser_damage_before = s.log.sol_damage_by_source[COLO_SOL_DAMAGE_CRYSTAL_LASER]; step_and_observe(&s, &ctx, idle); @@ -4759,6 +4766,78 @@ static void test_sol_laser_react_window(void) { CHECK("crystal-laser damage has its own source total", s.log.sol_damage_by_source[COLO_SOL_DAMAGE_CRYSTAL_LASER] == laser_damage_before + (float)(99 - s.player.current_hitpoints)); + CHECK("laser damage accounting records the hit and failed reaction", + s.log.laser_hits == 1.0f && + s.log.laser_dmg == (float)(99 - s.player.current_hitpoints) && + s.log.laser_aligned_at_damage == 1.0f && + s.log.laser_react_fail == 1.0f && + s.log.laser_react_ok == 0.0f); +} + +static void test_sol_laser_observation_pack(void) { + printf("test_sol_laser_observation_pack\n"); + ColosseumState s = {0}; + s.wave = COLO_WAVE_BOSS; + s.player.x = 17; + s.player.y = 16; + s.sol.started = 1; + s.sol.laser_cooldown = + COLO_SOL_CRYSTAL_COOLDOWN_MIN + COLO_SOL_CRYSTAL_COOLDOWN_RAND - 1; + s.sol.crystal_count = 2; + s.sol.crystals[0] = (ColoSolCrystal){ + .active = 1, + .edge = COLO_SOL_EDGE_NORTH, + .x = 20, + .firing_freeze = COLO_SOL_LASER_FREEZE, + }; + s.sol.crystals[1] = (ColoSolCrystal){ + .active = 1, + .edge = COLO_SOL_EDGE_EAST, + .y = 10, + .firing_freeze = COLO_SOL_LASER_FREEZE / 3, + }; + + float enabled[COLO_BOSS_OBS_SIZE] = {0}; + float disabled[COLO_BOSS_OBS_SIZE] = {0}; + int enabled_idx = 0; + int disabled_idx = 0; + col_write_boss_obs(&s, enabled, &enabled_idx, 1); + col_write_boss_obs(&s, disabled, &disabled_idx, 0); + + ColosseumContext ctx; + col_init_context_typed(&ctx); + CHECK("honest laser observations are enabled by default", + ctx.config.laser_obs_mode == 1); + col_put_int_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, "laser_obs_mode", 0); + CHECK("laser observation ablation parses as a binary config", + ctx.config.laser_obs_mode == 0); + + int laser = COLO_BOSS_OBS_PREMOVE_HAZARD_OFFSET + 2; + CHECK("laser pack has one cooldown plus four three-float crystal records", + COLO_SOL_LASER_OBS_SIZE == + 1 + COLO_SOL_MAX_CRYSTALS * COLO_SOL_LASER_CRYSTAL_FEATS); + CHECK("enabled and disabled laser modes preserve the boss observation width", + enabled_idx == COLO_BOSS_OBS_SIZE && + disabled_idx == COLO_BOSS_OBS_SIZE); + CHECK("laser pack writes the normalized shared cooldown", + enabled[laser] == 1.0f); + CHECK("north crystal writes active, signed x-line delta, and freeze", + enabled[laser + 1] == 1.0f && + fabsf(enabled[laser + 2] - 3.0f / (float)COLO_ARENA_WIDTH) < 0.000001f && + enabled[laser + 3] == 1.0f); + CHECK("east crystal writes active, signed y-line delta, and freeze", + enabled[laser + 4] == 1.0f && + fabsf(enabled[laser + 5] + 6.0f / (float)COLO_ARENA_HEIGHT) < 0.000001f && + fabsf(enabled[laser + 6] - 1.0f / 3.0f) < 0.000001f); + for (int i = 0; i < COLO_SOL_LASER_OBS_SIZE; i++) + CHECK("disabled laser mode zeroes every laser-pack channel", + disabled[laser + i] == 0.0f); + for (int i = 0; i < COLO_BOSS_OBS_SIZE; i++) { + if (i >= laser && i < laser + COLO_SOL_LASER_OBS_SIZE) continue; + CHECK("laser ablation changes no other boss observation", + enabled[i] == disabled[i]); + } } static void test_sol_phase_transition_sand_guarantees(void) { @@ -6157,7 +6236,7 @@ static void test_combat_fidelity_contract_sizes(void) { CHECK("prayer head uses shared PVE overhead dim", COLO_ACTION_DIMS[COLO_HEAD_PRAYER] == ENCOUNTER_OVERHEAD_DIM_PVE); CHECK("spell head dim is 3 (none/summon-thrall/death-charge)", COLO_SPELL_DIM == 3); - CHECK("obs width is 922", COLO_NUM_OBS == 922); + CHECK("obs width is 934", COLO_NUM_OBS == 934); CHECK("inventory block has 84 features (28 cells x code, equipped, hp_heal)", COLO_INVENTORY_OBS_SIZE == 84); CHECK("the encoder still sees the 15-feature record, rebuilt from the item table", @@ -8301,7 +8380,7 @@ static void test_stage3_t6_obs_mask_fuzz_contract(void) { } step_and_observe(&s, &ctx, actions); } - CHECK("T6 obs running-index assert reached COLO_NUM_OBS", COLO_NUM_OBS == 922); + CHECK("T6 obs running-index assert reached COLO_NUM_OBS", COLO_NUM_OBS == 934); CHECK("T6 mask running-index assert reached 452", COLO_ACTION_MASK_SIZE == 452); } @@ -8927,6 +9006,7 @@ int main(void) { test_sol_crystal_lifecycle(); test_sol_aoe_reaction_window(); test_sol_laser_react_window(); + test_sol_laser_observation_pack(); test_sol_phase_transition_sand_guarantees(); test_sol_beams_become_pools(); test_sol_beam_strike_reaction_window(); diff --git a/ocean/osrs_colosseum/osrs_colosseum.h b/ocean/osrs_colosseum/osrs_colosseum.h index 7cd9ec927e..7df1f62a2b 100644 --- a/ocean/osrs_colosseum/osrs_colosseum.h +++ b/ocean/osrs_colosseum/osrs_colosseum.h @@ -78,6 +78,15 @@ struct Log { float rew_win; float rew_death; float rew_timeout; + float laser_volleys; + float laser_hits; + float laser_dmg; + float laser_aligned_at_fire; + float laser_aligned_at_show; + float laser_aligned_at_pre; + float laser_aligned_at_damage; + float laser_react_ok; + float laser_react_fail; float avoid_total; float avoid_achieved; float avoid_missed; @@ -260,6 +269,7 @@ void puf_init(Env* env, Dict* kwargs) { "mask_inventory_heads", "late_start_state_mode", "bis_gear_oracle_mode", + "laser_obs_mode", "episode_max_ticks_override", "damage_scale_anneal_ticks", }; @@ -416,6 +426,15 @@ void puf_step(Env* env) { env->log.rew_win += clog->rew_win; env->log.rew_death += clog->rew_death; env->log.rew_timeout += clog->rew_timeout; + env->log.laser_volleys += clog->laser_volleys; + env->log.laser_hits += clog->laser_hits; + env->log.laser_dmg += clog->laser_dmg; + env->log.laser_aligned_at_fire += clog->laser_aligned_at_fire; + env->log.laser_aligned_at_show += clog->laser_aligned_at_show; + env->log.laser_aligned_at_pre += clog->laser_aligned_at_pre; + env->log.laser_aligned_at_damage += clog->laser_aligned_at_damage; + env->log.laser_react_ok += clog->laser_react_ok; + env->log.laser_react_fail += clog->laser_react_fail; env->log.avoid_total += clog->avoid_total; env->log.avoid_achieved += clog->avoid_achieved; env->log.avoid_missed += clog->avoid_missed; @@ -528,6 +547,15 @@ void puf_log(Log* log, Dict* out) { ? log->reward_clamped_steps / log->reward_steps : 0.0f); dict_set(out, "reward_clamp_loss", log->reward_clamp_loss); dict_set(out, "reward_raw_peak", log->reward_raw_peak); + dict_set(out, "laser_volleys", log->laser_volleys); + dict_set(out, "laser_hits", log->laser_hits); + dict_set(out, "laser_dmg", log->laser_dmg); + dict_set(out, "laser_aligned_at_fire", log->laser_aligned_at_fire); + dict_set(out, "laser_aligned_at_show", log->laser_aligned_at_show); + dict_set(out, "laser_aligned_at_pre", log->laser_aligned_at_pre); + dict_set(out, "laser_aligned_at_damage", log->laser_aligned_at_damage); + dict_set(out, "laser_react_ok", log->laser_react_ok); + dict_set(out, "laser_react_fail", log->laser_react_fail); dict_set(out, "clamp_loss_wave_clear", log->clamp_loss_wave_clear); dict_set(out, "clamp_loss_win", log->clamp_loss_win); dict_set(out, "rew_damage", log->rew_damage); From 36f71db765d6f99d5d8d3db1036d87c7283e0a0e Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 14:26:31 +0300 Subject: [PATCH 33/50] Model Manticore activation and charge --- .../colosseum/encounter_colosseum_combat.inc | 65 +++++- .../colosseum/encounter_colosseum_model.inc | 14 +- .../encounter_colosseum_obs_mask.inc | 23 +- .../encounter_colosseum_render_snapshot.inc | 4 + .../encounter_colosseum_reset_spawn.inc | 4 +- ocean/osrs/tests/test_colosseum_modifiers.c | 206 ++++++++++++++++++ 6 files changed, 301 insertions(+), 15 deletions(-) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc index 12d9f91cd9..bb4ee79ac7 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc @@ -512,13 +512,22 @@ static void col_manticore_delay_ready_peers( if (i == idx) continue; ColoNPC* other = &s->npcs[i]; if (!col_npc_is_live_target(other) || other->type != COLO_MANTICORE) continue; - if (other->attack_timer > 1) continue; - if (other->type_state.manticore.cycle_step > 0) continue; + ColoManticoreState* other_mc = colo_npc_manticore(other); + int initial_ready = + other_mc->phase == COLO_MANTICORE_PHASE_CHARGING && + other->attack_timer == 0; + int repeat_ready = + other_mc->phase == COLO_MANTICORE_PHASE_REPEATING && + other->attack_timer <= 1 && + other_mc->cycle_step <= 0; + if (!initial_ready && !repeat_ready) continue; if (other->stun_timer > 0 || other->frozen_ticks > 0) continue; int dist = col_npc_dist_to_player(s, other); if (dist < 1 || dist > COLO_NPC_STATS[COLO_MANTICORE].attack_range) continue; if (!col_npc_has_los_to_player(s, ctx, other)) continue; - other->attack_timer = COLO_MANTICORE_STAGGER_TICKS + 1; + other->attack_timer = initial_ready + ? COLO_MANTICORE_STAGGER_TICKS + : COLO_MANTICORE_STAGGER_TICKS + 1; } } @@ -587,6 +596,18 @@ static void col_npc_manticore_arm(ColosseumState* s, int idx) { mc->cycle_step = 0; } +static int col_manticore_has_target( + const ColosseumState* s, + const ColosseumContext* ctx, + const ColoNPC* npc, + const ColoNpcStats* stats +) { + int dist = col_npc_dist_to_player(s, npc); + return dist >= 1 && + dist <= stats->attack_range && + col_npc_has_los_to_player(s, ctx, npc); +} + static void col_npc_attack_manticore( ColosseumState* s, ColosseumContext* ctx, @@ -596,6 +617,7 @@ static void col_npc_attack_manticore( ColoNPC* npc = &s->npcs[idx]; ColoManticoreState* mc = colo_npc_manticore(npc); mc->cycle_step = 0; + mc->phase = COLO_MANTICORE_PHASE_REPEATING; col_npc_manticore_fire_orb(s, idx, stats); col_manticore_delay_ready_peers(s, ctx, idx); } @@ -608,8 +630,41 @@ static void col_npc_attack_ctx(ColosseumState* s, ColosseumContext* ctx, int idx if (npc->type == COLO_SOL_HEREDIT) return; if (col_type_is_warbander(npc->type)) return; + if (npc->type == COLO_MANTICORE) { + ColoManticoreState* mc = colo_npc_manticore(npc); + if (mc->phase == COLO_MANTICORE_PHASE_ACTIVATING) { + if (npc->attack_timer > 0) npc->attack_timer--; + if (npc->attack_timer > 0) return; + mc->phase = COLO_MANTICORE_PHASE_WAITING_FOR_TARGET; + } + } + if (s->wave_attack_delay > 0) return; + if (npc->type == COLO_MANTICORE) { + ColoManticoreState* mc = colo_npc_manticore(npc); + if (mc->phase == COLO_MANTICORE_PHASE_WAITING_FOR_TARGET) { + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; + if (!col_manticore_has_target(s, ctx, npc, stats)) return; + col_npc_manticore_arm(s, idx); + mc->phase = COLO_MANTICORE_PHASE_CHARGING; + npc->attack_timer = COLO_MANTICORE_CHARGE_TICKS; + ctx->manticore_charge_started[idx] = 1; + return; + } + if (mc->phase == COLO_MANTICORE_PHASE_CHARGING) { + if (npc->attack_timer > 0) { + npc->attack_timer--; + return; + } + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; + if (!col_manticore_has_target(s, ctx, npc, stats)) return; + col_npc_attack_manticore(s, ctx, idx, stats); + return; + } + assert(mc->phase == COLO_MANTICORE_PHASE_REPEATING); + } + if (npc->type == COLO_MANTICORE && npc->type_state.manticore.cycle_step > 0) { col_npc_manticore_advance_barrage(s, idx, stats); @@ -620,9 +675,7 @@ static void col_npc_attack_ctx(ColosseumState* s, ColosseumContext* ctx, int idx if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; if (npc->type == COLO_MANTICORE) { - int mdist = col_npc_dist_to_player(s, npc); - if (mdist < 1 || mdist > stats->attack_range) return; - if (!col_npc_has_los_to_player(s, ctx, npc)) return; + if (!col_manticore_has_target(s, ctx, npc, stats)) return; int cycle_step_before_arm = npc->type_state.manticore.cycle_step; col_npc_manticore_arm(s, idx); if (cycle_step_before_arm < 0 && diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index 2403020189..de4fa836dd 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -764,7 +764,15 @@ typedef struct { int formation_dir; } ColoWarbandState; +typedef enum { + COLO_MANTICORE_PHASE_ACTIVATING = 0, + COLO_MANTICORE_PHASE_WAITING_FOR_TARGET, + COLO_MANTICORE_PHASE_CHARGING, + COLO_MANTICORE_PHASE_REPEATING, +} ColoManticorePhase; + typedef struct { + ColoManticorePhase phase; int cycle_step; AttackStyle orb_style[3]; AttackStyle fixed_orb_style[3]; @@ -844,6 +852,9 @@ static inline int col_warband_window_offset(ColoNpcType type) { #define COLO_JAVELIN_SKYFALL_MAX_HIT 40 #define COLO_JAVELIN_SKYFALL_ANIM_ID 10893 #define COLO_MANTICORE_STAGGER_TICKS 5 +#define COLO_MANTICORE_ACTIVATION_TICKS 7 +#define COLO_MANTICORE_CHARGE_TICKS 10 +#define COLO_MANTICORE_ARM_ANIMATION_TICKS 3 #define COLO_MINOTAUR_HEAL_RANGE 7 @@ -860,6 +871,7 @@ static void colo_npc_init_type_state(ColoNPC* npc) { break; case COLO_MANTICORE: npc->type_state.manticore = (ColoManticoreState){ + .phase = COLO_MANTICORE_PHASE_ACTIVATING, .cycle_step = -1, .orb_style = { ATTACK_STYLE_NONE, ATTACK_STYLE_NONE, ATTACK_STYLE_NONE }, .fixed_orb_style = { ATTACK_STYLE_NONE, ATTACK_STYLE_NONE, ATTACK_STYLE_NONE }, @@ -1567,7 +1579,7 @@ typedef struct { ColosseumLog log; ColoObsMemoCaches obs_memos; } ColosseumState; -static_assert(sizeof(ColosseumState) == 29776, "ColosseumState serialized layout"); +static_assert(sizeof(ColosseumState) == 29872, "ColosseumState serialized layout"); typedef enum { COLO_DMG_OFFPRAY = 0, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc index 9574d46f4a..572e74bd6f 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -291,9 +291,19 @@ static void col_write_style_onehot_melee_ranged_magic(AttackStyle style, float* *idx = i; } +static int col_manticore_orbs_visible(const ColoNPC* npc) { + const ColoManticoreState* mc = &npc->type_state.manticore; + if (mc->cycle_step < 0) return 0; + if (mc->phase != COLO_MANTICORE_PHASE_CHARGING) return 1; + return npc->attack_timer <= + COLO_MANTICORE_CHARGE_TICKS - COLO_MANTICORE_ARM_ANIMATION_TICKS; +} + static int col_manticore_next_orb_ticks(const ColoNPC* npc) { const ColoManticoreState* mc = &npc->type_state.manticore; if (mc->cycle_step > 0) return 1; + if (mc->phase == COLO_MANTICORE_PHASE_CHARGING) + return npc->attack_timer + 1; return npc->attack_timer == 0 ? 1 : npc->attack_timer; } @@ -301,8 +311,7 @@ static ColoNpcNextPrayerObs col_visible_manticore_next_prayer(const ColoNPC* npc if (npc->type != COLO_MANTICORE) return col_npc_next_prayer_none(); if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return col_npc_next_prayer_none(); const ColoManticoreState* mc = &npc->type_state.manticore; - int armed = mc->cycle_step >= 0 || mc->orb_style[0] != ATTACK_STYLE_NONE; - if (!armed) return col_npc_next_prayer_none(); + if (!col_manticore_orbs_visible(npc)) return col_npc_next_prayer_none(); int orb = mc->cycle_step >= 0 ? mc->cycle_step : 0; if (orb < 0 || orb >= 3) return col_npc_next_prayer_none(); AttackStyle style = mc->orb_style[orb]; @@ -401,13 +410,13 @@ static void col_write_npc_tells(const ColosseumState* s, const ColoNPC* npc, flo switch (npc->type) { case COLO_MANTICORE: { const ColoManticoreState* mc = &npc->type_state.manticore; - int firing = mc->cycle_step >= 0; - int armed = firing || mc->orb_style[0] != ATTACK_STYLE_NONE; - if (armed) { - int start = firing ? mc->cycle_step : 0; + int barrage_started = mc->cycle_step > 0; + int visible = col_manticore_orbs_visible(npc); + if (visible) { + int start = mc->cycle_step; int speed = COLO_NPC_STATS[npc->type].attack_speed; - tells[0] = firing ? 0.0f + tells[0] = barrage_started ? 0.0f : col_clamp01((float)npc->attack_timer / (float)(speed > 0 ? speed : 1)); for (int slot = 0; slot < 3; slot++) { int orb = start + slot; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 666db48bdc..907b4c0308 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -385,6 +385,10 @@ static void col_emit_manticore_orb_telegraphs( ) { const ColoManticoreState* mc = &npc->type_state.manticore; if (mc->cycle_step < 0) return; + if (mc->phase == COLO_MANTICORE_PHASE_CHARGING && + npc->attack_timer > + COLO_MANTICORE_CHARGE_TICKS - COLO_MANTICORE_ARM_ANIMATION_TICKS) + return; if (mc->cycle_step >= 3) { fprintf(stderr, "colosseum: invalid manticore cycle_step %d\n", mc->cycle_step); abort(); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc index 8bc505bddb..d15583c052 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -124,7 +124,9 @@ static void col_init_npc(ColosseumState* s, int idx, ColoNpcType type, int x, in npc->min_hp_seen = st->hp; npc->size = st->size; npc->render_id = ++s->next_npc_render_id; - npc->attack_timer = st->attack_speed; + npc->attack_timer = type == COLO_MANTICORE + ? COLO_MANTICORE_ACTIVATION_TICKS + : st->attack_speed; npc->attack_style = st->default_style; npc->active = 1; npc->target_x = x; diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index a66847be7e..496ccafb87 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -3233,6 +3233,165 @@ static void test_minotaur_heal_semantics(void) { s.log.pray_faced_by_type[COLO_MINOTAUR] > mino_faced_before); } +static void test_manticore_initial_charge_timing_and_target_gate(void) { + printf("test_manticore_initial_charge_timing_and_target_gate\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 79); + geo_clear_npcs(&s); + s.wave_attack_delay = 0; + s.player.x = 17; + s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + ColoNPC* npc = &s.npcs[0]; + ColoManticoreState* mc = colo_npc_manticore(npc); + + int inactive_for_six_ticks = 1; + for (int tick = 0; tick < 6; tick++) { + col_context_clear_render_events(&ctx); + npc->attacked_this_tick = 0; + col_npc_attack_ctx(&s, &ctx, 0); + if (ctx.manticore_charge_started[0] || + mc->phase != COLO_MANTICORE_PHASE_ACTIVATING || + mc->cycle_step >= 0 || + npc->attacked_this_tick) { + inactive_for_six_ticks = 0; + } + } + CHECK("manticore remains inactive for six ticks after its spawn tick", + inactive_for_six_ticks); + + col_context_clear_render_events(&ctx); + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("manticore starts charging on the seventh spawn tick", + ctx.manticore_charge_started[0] && + mc->phase == COLO_MANTICORE_PHASE_CHARGING && + mc->cycle_step == 0 && + npc->attacked_this_tick == 0); + + EncounterOverlay ov; + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("charging starts before any orb telegraph is visible", + ov.floating_model_count == 0); + CHECK("hidden charging phase exposes no prayer oracle", + !col_visible_manticore_next_prayer(npc).active); + + int no_fire_during_ten_charge_ticks = 1; + for (int tick = 1; tick <= COLO_MANTICORE_CHARGE_TICKS; tick++) { + col_context_clear_render_events(&ctx); + npc->attacked_this_tick = 0; + col_npc_attack_ctx(&s, &ctx, 0); + if (npc->attacked_this_tick || + mc->phase != COLO_MANTICORE_PHASE_CHARGING || + mc->cycle_step != 0) + no_fire_during_ten_charge_ticks = 0; + if (tick <= COLO_MANTICORE_ARM_ANIMATION_TICKS) { + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + ColoNpcNextPrayerObs next_prayer = + col_visible_manticore_next_prayer(npc); + if (tick < COLO_MANTICORE_ARM_ANIMATION_TICKS) { + CHECK("orbs remain hidden during the three-tick arming animation", + ov.floating_model_count == 0); + CHECK("hidden arming ticks expose no prayer oracle", + !next_prayer.active); + } else { + CHECK("all three orbs appear when the arming animation completes", + ov.floating_model_count == 3); + CHECK("revealed orb 0 exposes its style eight ticks before firing", + next_prayer.active && + next_prayer.style == mc->orb_style[0] && + next_prayer.ticks == 8); + } + } + } + CHECK("manticore consumes ten full charge ticks before firing", + no_fire_during_ten_charge_ticks); + + npc->attacked_this_tick = 0; + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("manticore fires the first orb eleven ticks after charging starts", + npc->attacked_this_tick == 1 && + mc->phase == COLO_MANTICORE_PHASE_REPEATING && + mc->cycle_step == 1); + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 79); + geo_clear_npcs(&s); + s.wave_attack_delay = 0; + s.player.x = 33; + s.player.y = 24; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + npc = &s.npcs[0]; + mc = colo_npc_manticore(npc); + int stayed_unarmed_without_target = 1; + for (int tick = 0; tick < 20; tick++) { + col_context_clear_render_events(&ctx); + npc->attacked_this_tick = 0; + col_npc_attack_ctx(&s, &ctx, 0); + if (ctx.manticore_charge_started[0] || + mc->phase == COLO_MANTICORE_PHASE_CHARGING || + mc->cycle_step >= 0 || + npc->attacked_this_tick) { + stayed_unarmed_without_target = 0; + } + } + CHECK("manticore waits without a target in line of sight and range", + stayed_unarmed_without_target && + mc->phase == COLO_MANTICORE_PHASE_WAITING_FOR_TARGET); + + s.player.x = 17; + s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_context_clear_render_events(&ctx); + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("first valid target tick starts charging without firing", + ctx.manticore_charge_started[0] && + mc->phase == COLO_MANTICORE_PHASE_CHARGING && + mc->cycle_step == 0 && + npc->attacked_this_tick == 0); + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 79); + geo_clear_npcs(&s); + s.wave_attack_delay = 0; + s.player.x = 5; + s.player.y = 9; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 11, 8); + npc = &s.npcs[0]; + mc = colo_npc_manticore(npc); + CHECK("blocked fixture remains inside Manticore attack range", + col_npc_dist_to_player(&s, npc) <= + COLO_NPC_STATS[COLO_MANTICORE].attack_range); + CHECK("pillar blocks the Manticore's first line of sight", + !col_npc_has_los_to_player(&s, &ctx, npc)); + for (int tick = 0; tick < COLO_MANTICORE_ACTIVATION_TICKS; tick++) { + col_context_clear_render_events(&ctx); + col_npc_attack_ctx(&s, &ctx, 0); + } + CHECK("in-range Manticore waits while line of sight is blocked", + mc->phase == COLO_MANTICORE_PHASE_WAITING_FOR_TARGET && + mc->cycle_step < 0 && + !ctx.manticore_charge_started[0]); + + s.player.x = 12; + s.player.y = 12; + col_rebuild_player_collision_flags(&s); + col_context_clear_render_events(&ctx); + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("first unblocked in-range tick starts the Manticore charge", + ctx.manticore_charge_started[0] && + mc->phase == COLO_MANTICORE_PHASE_CHARGING && + mc->cycle_step == 0 && + npc->attacked_this_tick == 0); +} + static void test_manticore_barrage_period(void) { printf("test_manticore_barrage_period\n"); ColosseumContext ctx; @@ -3247,6 +3406,7 @@ static void test_manticore_barrage_period(void) { s.npcs[0].attack_timer = 2; ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + mc->phase = COLO_MANTICORE_PHASE_REPEATING; int starts[8]; int nstarts = 0; for (int t = 0; t < 36; t++) { @@ -3275,6 +3435,7 @@ static void test_manticore_telegraph_during_windup(void) { col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); s.npcs[0].attack_timer = 6; ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + mc->phase = COLO_MANTICORE_PHASE_REPEATING; CHECK("spawn rolls a hidden fixed cycle", mc->fixed_orb_style[0] != ATTACK_STYLE_NONE && mc->fixed_orb_style[1] != ATTACK_STYLE_NONE && @@ -3309,6 +3470,7 @@ static void test_manticore_telegraph_during_windup(void) { col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); s.npcs[0].attack_timer = 3; mc = colo_npc_manticore(&s.npcs[0]); + mc->phase = COLO_MANTICORE_PHASE_REPEATING; int orb0_blocked = 0; for (int t = 0; t < 12; t++) { if (mc->cycle_step == 0 && mc->orb_style[0] != ATTACK_STYLE_NONE) { @@ -3432,6 +3594,7 @@ static void test_manticore_orb_same_tick_flick(void) { col_rebuild_player_collision_flags(&s); col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + mc->phase = COLO_MANTICORE_PHASE_REPEATING; int no_fire_damage = 1, prayed_one_tick = 1, prayer_counted = 0, any_queued = 0; for (int rep = 0; rep < 16; rep++) { @@ -3484,6 +3647,7 @@ static void test_manticore_orb_same_tick_flick(void) { col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); s.npcs[0].attack_timer = 4; mc = colo_npc_manticore(&s.npcs[0]); + mc->phase = COLO_MANTICORE_PHASE_REPEATING; int actions[COLO_NUM_ACTION_HEADS] = {0}; int protected_ticks = 0, flicked_damage = 0; @@ -3570,6 +3734,8 @@ static void test_manticore_shared_wave_cycle(void) { col_init_npc(&s, 1, COLO_MANTICORE, 18, 16); ColoManticoreState* amc = colo_npc_manticore(&s.npcs[0]); ColoManticoreState* bmc = colo_npc_manticore(&s.npcs[1]); + amc->phase = COLO_MANTICORE_PHASE_REPEATING; + bmc->phase = COLO_MANTICORE_PHASE_REPEATING; CHECK("manually-spawned peers also share the wave pattern", amc->fixed_orb_style[0] == bmc->fixed_orb_style[0] && amc->fixed_orb_style[1] == bmc->fixed_orb_style[1] && @@ -3591,6 +3757,37 @@ static void test_manticore_shared_wave_cycle(void) { col_npc_attack_ctx(&s, &ctx, 1); CHECK("the delayed peer fires exactly 5 ticks after A's barrage started", bmc->cycle_step == 1); + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 89); + geo_clear_npcs(&s); + s.wave = 8; + s.player.x = 13; + s.player.y = 12; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 12, 16); + col_init_npc(&s, 1, COLO_MANTICORE, 18, 16); + amc = colo_npc_manticore(&s.npcs[0]); + bmc = colo_npc_manticore(&s.npcs[1]); + col_npc_manticore_arm(&s, 0); + col_npc_manticore_arm(&s, 1); + amc->phase = COLO_MANTICORE_PHASE_CHARGING; + bmc->phase = COLO_MANTICORE_PHASE_CHARGING; + s.npcs[0].attack_timer = 0; + s.npcs[1].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + col_npc_attack_ctx(&s, &ctx, 1); + CHECK("a fully charged initial peer starts its five-tick delay", + amc->cycle_step == 1 && + bmc->cycle_step == 0 && + s.npcs[1].attack_timer == COLO_MANTICORE_STAGGER_TICKS - 1); + for (int tick = 1; tick < COLO_MANTICORE_STAGGER_TICKS; tick++) + col_npc_attack_ctx(&s, &ctx, 1); + CHECK("initial peer remains ready through the fourth delayed tick", + bmc->cycle_step == 0 && s.npcs[1].attack_timer == 0); + col_npc_attack_ctx(&s, &ctx, 1); + CHECK("initial peer fires on the fifth delayed tick", + bmc->cycle_step == 1); } static void test_manticore_stagger_overlap_fidelity(void) { @@ -3611,6 +3808,8 @@ static void test_manticore_stagger_overlap_fidelity(void) { col_init_npc(&s, 1, COLO_MANTICORE, 18, 16); ColoManticoreState* amc = colo_npc_manticore(&s.npcs[0]); ColoManticoreState* bmc = colo_npc_manticore(&s.npcs[1]); + amc->phase = COLO_MANTICORE_PHASE_REPEATING; + bmc->phase = COLO_MANTICORE_PHASE_REPEATING; s.player.current_hitpoints = 99; s.npcs[0].attack_timer = 0; s.npcs[1].attack_timer = 2; @@ -3634,6 +3833,8 @@ static void test_manticore_stagger_overlap_fidelity(void) { col_init_npc(&s, 1, COLO_MANTICORE, 11, 8); amc = colo_npc_manticore(&s.npcs[0]); bmc = colo_npc_manticore(&s.npcs[1]); + amc->phase = COLO_MANTICORE_PHASE_REPEATING; + bmc->phase = COLO_MANTICORE_PHASE_REPEATING; s.player.current_hitpoints = 99; s.npcs[0].attack_timer = 0; s.npcs[1].attack_timer = 0; @@ -3665,6 +3866,8 @@ static void test_manticore_stagger_overlap_fidelity(void) { col_init_npc(&s, 1, COLO_MANTICORE, 18, 16); amc = colo_npc_manticore(&s.npcs[0]); bmc = colo_npc_manticore(&s.npcs[1]); + amc->phase = COLO_MANTICORE_PHASE_REPEATING; + bmc->phase = COLO_MANTICORE_PHASE_REPEATING; s.player.current_hitpoints = 99; s.npcs[0].attack_timer = 3; s.npcs[1].attack_timer = 3; @@ -6979,6 +7182,7 @@ static void test_render_bridge_combat_visuals_and_loadout(void) { init_forecast_test_state(&s, &ctx, 503, 17, 16); col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + mc->phase = COLO_MANTICORE_PHASE_REPEATING; mc->fixed_orb_style[0] = ATTACK_STYLE_MAGIC; mc->fixed_orb_style[1] = ATTACK_STYLE_RANGED; mc->fixed_orb_style[2] = ATTACK_STYLE_MELEE; @@ -7010,6 +7214,7 @@ static void test_render_bridge_combat_visuals_and_loadout(void) { init_forecast_test_state(&s, &ctx, 503, 17, 16); col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); mc = colo_npc_manticore(&s.npcs[0]); + mc->phase = COLO_MANTICORE_PHASE_REPEATING; mc->fixed_orb_style[0] = ATTACK_STYLE_MELEE; mc->fixed_orb_style[1] = ATTACK_STYLE_RANGED; mc->fixed_orb_style[2] = ATTACK_STYLE_MAGIC; @@ -8980,6 +9185,7 @@ int main(void) { test_warband_pillar_routefind_vs_shaman_safespot(); test_red_flag_minotaur_routefind(); test_minotaur_heal_semantics(); + test_manticore_initial_charge_timing_and_target_gate(); test_manticore_barrage_period(); test_manticore_telegraph_during_windup(); test_late_start_entry_state(); From 469867150a4611f7ee2c66e6ff4a588fc5de36e3 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 14:30:30 +0300 Subject: [PATCH 34/50] Make Colosseum thralls automatic --- config/osrs_colosseum.ini | 2 +- .../colosseum/encounter_colosseum_helpers.inc | 28 ++++ .../encounter_colosseum_lab_json.inc | 10 +- .../encounter_colosseum_mask_render.inc | 7 +- .../colosseum/encounter_colosseum_model.inc | 14 +- .../encounter_colosseum_obs_mask.inc | 11 +- .../encounter_colosseum_player_actions.inc | 72 +++++----- .../encounter_colosseum_reset_spawn.inc | 2 +- ocean/osrs/tests/test_colosseum_modifiers.c | 133 +++++++++--------- 9 files changed, 150 insertions(+), 129 deletions(-) diff --git a/config/osrs_colosseum.ini b/config/osrs_colosseum.ini index 4d5cfb0fb8..b9b8481238 100644 --- a/config/osrs_colosseum.ini +++ b/config/osrs_colosseum.ini @@ -44,7 +44,7 @@ curriculum_frac_5 = 0.0 total_agents = 8192 num_buffers = 8 num_threads = 32 -action_mask_size = 452 +action_mask_size = 451 gpu_env = 0 [policy] diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc index 7a1a71e2b1..939534ffe4 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc @@ -27,6 +27,34 @@ static void col_modifier_open_draft(ColosseumState* s, int next_wave); static void col_apply_late_start_entry_state( ColosseumState* s, const ColosseumContext* ctx, int start_wave); +static int col_thrall_lifetime_ticks(const ColosseumState* s) { + switch (s->active_loadout_profile) { + case COLO_LOADOUT_PROFILE_SPEEDRUN: + return 2 * s->player.current_magic; + case COLO_LOADOUT_PROFILE_BEGINNER: + return s->player.current_magic; + case COLO_NUM_LOADOUT_PROFILES: + break; + } + fprintf(stderr, "invalid Colosseum loadout profile %d\n", + (int)s->active_loadout_profile); + abort(); +} + +static void col_resummon_thrall(ColosseumState* s) { + int lifetime = col_thrall_lifetime_ticks(s); + if (lifetime <= 0) { + fprintf(stderr, "invalid thrall lifetime %d at Magic level %d\n", + lifetime, s->player.current_magic); + abort(); + } + s->thrall_target_slot = -1; + s->thrall_aggro_ticks_left = 0; + s->thrall_lifetime_left = lifetime; + s->thrall_lifetime_total = lifetime; + s->thrall_attack_timer = COLO_THRALL_TICK; +} + static void col_abort_invalid_weapon_set(ColoWeaponSet set) { fprintf(stderr, "invalid Colosseum weapon set %d\n", (int)set); abort(); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_json.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_json.inc index fb2ad9ba5e..4fd3ba54df 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_json.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_json.inc @@ -55,8 +55,9 @@ static char* col_lab_alloc_json(ColosseumState* s) { "\"loadout_profile\":%d,\"brew_doses\":%d,\"restore_doses\":%d," "\"combat_doses\":%d,\"ranged_doses\":%d,\"surge_doses\":%d," "\"surge_cooldown\":%d,\"spec_energy\":%d,\"spec_armed\":%d," - "\"thrall_active\":%d,\"thrall_target\":%d,\"thrall_lifetime\":%d," - "\"thrall_recast_cd\":%d,\"death_charge_window\":%d,\"death_charge_cd\":%d},", + "\"thrall_target\":%d,\"thrall_aggro\":%d,\"thrall_lifetime\":%d," + "\"thrall_lifetime_total\":%d," + "\"death_charge_window\":%d,\"death_charge_cd\":%d},", s->tick, s->wave + 1, s->player.x, s->player.y, s->player.current_hitpoints, s->player.base_hitpoints, col_lab_overhead_name(s->player.prayer), @@ -65,8 +66,9 @@ static char* col_lab_alloc_json(ColosseumState* s) { s->active_loadout_profile, s->player.brew_doses, s->player.restore_doses, s->player.combat_potion_doses, s->player.ranged_potion_doses, s->surge_doses, s->surge_cooldown, s->player.special_energy, s->player.spec_armed, - s->thrall_active, s->thrall_target_slot, s->thrall_lifetime_left, - s->thrall_recast_cd, s->death_charge_window_left, s->death_charge_cd); + s->thrall_target_slot, s->thrall_aggro_ticks_left, + s->thrall_lifetime_left, s->thrall_lifetime_total, + s->death_charge_window_left, s->death_charge_cd); col_lab_append_modifiers_json(s, &out); encounter_lab_string_append(&out, ","); encounter_lab_string_append(&out, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc index 81e4576357..3432b3cd1a 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc @@ -87,11 +87,6 @@ static void col_write_mask_ctx( s->player.offensive_prayer != OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; mask[offset++] = 1.0f; - { - int target = col_player_target_slot(s); - mask[offset++] = (!frozen && s->thrall_recast_cd == 0 && - col_npc_is_thrall_target(s, target)) ? 1.0f : 0.0f; - } mask[offset++] = (!frozen && s->death_charge_cd == 0 && s->death_charge_window_left == 0) ? 1.0f : 0.0f; @@ -321,7 +316,7 @@ static void col_fill_render_entities_ctx( } } - if (s->thrall_active && n < max_entities) { + if (n < max_entities) { int tx, ty; col_thrall_render_tile(s, ctx, &tx, &ty); col_emit_decoration_entity(out, &n, max_entities, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index de4fa836dd..4ab4dac79c 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -1226,13 +1226,9 @@ static void col_for_each_display_inventory_sprite_raw_osrs_id( col_for_each_reachable_consumable_dose_raw_osrs_id(visit, ctx); } -#define COLO_THRALL_MAX_HIT 3 -#define COLO_THRALL_TICK 4 -#define COLO_THRALL_RECAST_CD 17 -static const int COLO_THRALL_LIFETIME_TICKS[COLO_NUM_LOADOUT_PROFILES] = { - [COLO_LOADOUT_PROFILE_SPEEDRUN] = 198, - [COLO_LOADOUT_PROFILE_BEGINNER] = 99, -}; +#define COLO_THRALL_MAX_HIT 3 +#define COLO_THRALL_TICK 4 +#define COLO_THRALL_AGGRO_TICKS 12 #define COLO_DEATH_CHARGE_WINDOW 100 #define COLO_DEATH_CHARGE_CD 100 @@ -1543,11 +1539,11 @@ typedef struct { int surge_cooldown; OsrsInventoryCell inventory_cells[OSRS_INVENTORY_SIZE]; - int thrall_active; int thrall_target_slot; + int thrall_aggro_ticks_left; int thrall_lifetime_left; + int thrall_lifetime_total; int thrall_attack_timer; - int thrall_recast_cd; int death_charge_window_left; int death_charge_cd; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc index 572e74bd6f..8c04c10b61 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -78,7 +78,7 @@ typedef enum { #define COLO_THREAT_LOS_OBS_SIZE 3 -#define COLO_THRALL_DC_OBS_SIZE 6 +#define COLO_THRALL_DC_OBS_SIZE 5 #define COLO_SPAWN_PHASE_OBS_SIZE 2 #define COLO_ANCHOR_BLOCKED_OBS_SIZE COLO_NUM_SPAWN_ANCHORS @@ -1073,11 +1073,10 @@ static void col_write_threat_los_counts( static void col_write_thrall_dc(const ColosseumState* s, float* obs, int* idx) { int i = *idx; - int lifetime_full = COLO_THRALL_LIFETIME_TICKS[s->active_loadout_profile]; - obs[i++] = s->thrall_active ? 1.0f : 0.0f; - obs[i++] = (s->thrall_active && lifetime_full > 0) - ? col_clamp01((float)s->thrall_lifetime_left / (float)lifetime_full) : 0.0f; - obs[i++] = col_clamp01((float)s->thrall_recast_cd / (float)COLO_THRALL_RECAST_CD); + obs[i++] = col_clamp01( + (float)s->thrall_lifetime_left / (float)s->thrall_lifetime_total); + obs[i++] = col_clamp01( + (float)s->thrall_aggro_ticks_left / (float)COLO_THRALL_AGGRO_TICKS); obs[i++] = s->death_charge_window_left > 0 ? 1.0f : 0.0f; obs[i++] = col_clamp01((float)s->death_charge_window_left / (float)COLO_DEATH_CHARGE_WINDOW); obs[i++] = col_clamp01((float)s->death_charge_cd / (float)COLO_DEATH_CHARGE_CD); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc index 156e8d3a11..28636d8520 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -19,7 +19,7 @@ #define COLO_PRIMARY_DIM (ENCOUNTER_MOVE_ACTIONS + COLO_OBS_NPCS) #define COLO_OVERHEAD_DIM ENCOUNTER_OVERHEAD_DIM_PVE #define COLO_SPEC_DIM 3 -#define COLO_SPELL_DIM 3 +#define COLO_SPELL_DIM 2 #define COLO_OVERHEAD_NO_CHANGE ENCOUNTER_OVERHEAD_NO_CHANGE #define COLO_OVERHEAD_OFF ENCOUNTER_OVERHEAD_OFF @@ -27,8 +27,7 @@ #define COLO_OVERHEAD_RANGED ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED #define COLO_OVERHEAD_MAGIC ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC -#define COLO_SPELL_SUMMON_THRALL 1 -#define COLO_SPELL_DEATH_CHARGE 2 +#define COLO_SPELL_DEATH_CHARGE 1 #define COLO_MODIFIER_SELECT_DIM (1 + COLO_MODIFIER_DRAFT_OPTIONS) @@ -465,45 +464,45 @@ static int col_player_target_slot(const ColosseumState* s) { return s->interaction.target_slot; } -static void col_summon_thrall(ColosseumState* s, int target_slot) { - s->thrall_active = 1; - s->thrall_target_slot = target_slot; - s->thrall_lifetime_left = COLO_THRALL_LIFETIME_TICKS[s->active_loadout_profile]; - s->thrall_attack_timer = COLO_THRALL_TICK; - s->thrall_recast_cd = COLO_THRALL_RECAST_CD; -} static void col_tick_thrall_and_death_charge(ColosseumState* s, ColosseumContext* ctx) { - if (s->thrall_recast_cd > 0) s->thrall_recast_cd--; if (s->death_charge_cd > 0) s->death_charge_cd--; if (s->death_charge_window_left > 0) s->death_charge_window_left--; - if (s->thrall_active) { - s->thrall_lifetime_left--; - if (s->thrall_lifetime_left <= 0) { - s->thrall_active = 0; - s->thrall_target_slot = -1; - s->thrall_lifetime_left = 0; - s->thrall_attack_timer = 0; - return; - } - int player_target = col_player_target_slot(s); - if (col_npc_is_thrall_target(s, player_target)) - s->thrall_target_slot = player_target; - else if (!col_npc_is_thrall_target(s, s->thrall_target_slot)) - s->thrall_target_slot = -1; + s->thrall_lifetime_left--; + if (s->thrall_lifetime_left <= 0) { + col_resummon_thrall(s); + return; + } - if (s->thrall_attack_timer > 0) s->thrall_attack_timer--; - if (s->thrall_attack_timer == 0) { - s->thrall_attack_timer = COLO_THRALL_TICK; - if (col_npc_is_thrall_target(s, s->thrall_target_slot)) { - int dmg = encounter_rand_int(&s->rng_state, COLO_THRALL_MAX_HIT + 1); - col_queue_npc_pending_hit(s, s->thrall_target_slot, dmg, 2, - ATTACK_STYLE_MAGIC, ENCOUNTER_SPELL_NONE); - ctx->thrall_attacked_this_tick = 1; - } + int target_refreshed = 0; + int player_target = s->player_attack_npc_idx; + if (s->tick_scratch.player_attacked && col_npc_is_thrall_target(s, player_target)) { + s->thrall_target_slot = player_target; + s->thrall_aggro_ticks_left = COLO_THRALL_AGGRO_TICKS; + target_refreshed = 1; + } else if (!col_npc_is_thrall_target(s, s->thrall_target_slot)) { + s->thrall_target_slot = -1; + s->thrall_aggro_ticks_left = 0; + } + + if (s->thrall_attack_timer > 0) s->thrall_attack_timer--; + if (s->thrall_attack_timer == 0) { + s->thrall_attack_timer = COLO_THRALL_TICK; + if (col_npc_is_thrall_target(s, s->thrall_target_slot)) { + int dmg = encounter_rand_int(&s->rng_state, COLO_THRALL_MAX_HIT + 1); + col_queue_npc_pending_hit(s, s->thrall_target_slot, dmg, 2, + ATTACK_STYLE_MAGIC, ENCOUNTER_SPELL_NONE); + ctx->thrall_attacked_this_tick = 1; } } + + if (!target_refreshed && s->thrall_target_slot >= 0) { + assert(s->thrall_aggro_ticks_left > 0); + s->thrall_aggro_ticks_left--; + if (s->thrall_aggro_ticks_left == 0) + s->thrall_target_slot = -1; + } } typedef struct { @@ -1112,10 +1111,7 @@ static void col_tick_player_ctx( } int spell_act = s->modifiers.draft_pending ? 0 : actions[COLO_HEAD_SPELL]; - if (spell_act == COLO_SPELL_SUMMON_THRALL && s->thrall_recast_cd == 0) { - int target = col_player_target_slot(s); - if (col_npc_is_thrall_target(s, target)) col_summon_thrall(s, target); - } else if (spell_act == COLO_SPELL_DEATH_CHARGE && + if (spell_act == COLO_SPELL_DEATH_CHARGE && s->death_charge_cd == 0 && s->death_charge_window_left == 0) { s->death_charge_window_left = COLO_DEATH_CHARGE_WINDOW; } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc index d15583c052..98bb474b61 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -670,7 +670,7 @@ static void col_reset_ctx(EncounterState* state, EncounterContext* context, uint col_mark_live_loadout_dirty(s); col_ensure_live_loadout_stats(s); - s->thrall_target_slot = -1; + col_resummon_thrall(s); s->player.x = COLO_PLAYER_START_X; s->player.y = COLO_PLAYER_START_Y; diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index 496ccafb87..a0719947e7 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -2210,7 +2210,7 @@ static void test_death_linger_wave_clear_and_render(void) { int entity_count = 0; col_fill_render_entities_ctx( (EncounterState*)&s, (EncounterContext*)&ctx, entities, 4, &entity_count); - CHECK("dying NPC is still rendered", entity_count == 2 && entities[1].npc_slot == idx); + CHECK("dying NPC is still rendered", entity_count >= 2 && entities[1].npc_slot == idx); CHECK("dying NPC uses death animation", entities[1].npc_anim_id == col_npc_death_anim_id(COLO_FREMENNIK_BERSERKER)); CHECK("lethal hitsplat remains on death frame", @@ -6438,8 +6438,8 @@ static void test_combat_fidelity_contract_sizes(void) { COLO_ACTION_DIMS[COLO_HEAD_EAT] == 29 && COLO_ACTION_DIMS[COLO_HEAD_DRINK] == 29); CHECK("prayer head uses shared PVE overhead dim", COLO_ACTION_DIMS[COLO_HEAD_PRAYER] == ENCOUNTER_OVERHEAD_DIM_PVE); - CHECK("spell head dim is 3 (none/summon-thrall/death-charge)", COLO_SPELL_DIM == 3); - CHECK("obs width is 934", COLO_NUM_OBS == 934); + CHECK("spell head dim is 2 (none/death-charge)", COLO_SPELL_DIM == 2); + CHECK("obs width is 933", COLO_NUM_OBS == 933); CHECK("inventory block has 84 features (28 cells x code, equipped, hp_heal)", COLO_INVENTORY_OBS_SIZE == 84); CHECK("the encoder still sees the 15-feature record, rebuilt from the item table", @@ -6467,7 +6467,7 @@ static void test_combat_fidelity_contract_sizes(void) { int mask_sum = 0; for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) mask_sum += COLO_ACTION_DIMS[h]; CHECK("mask size equals the summed action-head dims", - COLO_ACTION_MASK_SIZE == mask_sum && COLO_ACTION_MASK_SIZE == 452); + COLO_ACTION_MASK_SIZE == mask_sum && COLO_ACTION_MASK_SIZE == 451); int obs_sum = COLO_PLAYER_OBS_SIZE + COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE + COLO_NPC_OBS_SIZE + @@ -6923,10 +6923,9 @@ static int thrall_scenario(ColosseumState* s, ColosseumContext* ctx, int mode, u s->player.x = 12; s->player.y = 18; col_rebuild_player_collision_flags(s); - int slot = col_spawn_npc_at(s, COLO_FREMENNIK_BERSERKER, 16, 18); + int slot = col_spawn_npc_at(s, COLO_FREMENNIK_BERSERKER, 13, 18); s->npcs[slot].hp = 200; s->npcs[slot].max_hp = 200; - osrs_interaction_set(&s->interaction, slot); return slot; } @@ -6935,25 +6934,32 @@ static void test_thrall_regression(void) { ColosseumContext ctx; ColosseumState s; int idle[COLO_NUM_ACTION_HEADS] = {0}; - int summon[COLO_NUM_ACTION_HEADS] = {0}; - summon[COLO_HEAD_SPELL] = COLO_SPELL_SUMMON_THRALL; int slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 201); - col_tick_player_ctx(&s, &ctx, summon, 1); - CHECK("summon activates the thrall on the targeted NPC", - s.thrall_active && s.thrall_target_slot == slot); - CHECK("budget thrall lifetime starts at 99 (decremented this tick)", - s.thrall_lifetime_left == 98); - CHECK("thrall recast gate is 17 (decremented this tick)", s.thrall_recast_cd == 16); + CHECK("budget profile starts with an idle 99-tick thrall", + s.thrall_target_slot == -1 && s.thrall_aggro_ticks_left == 0 && + s.thrall_lifetime_left == 99 && s.thrall_lifetime_total == 99); + osrs_interaction_set(&s.interaction, slot); + + s.player.attack_timer = 2; + col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("selecting an NPC without attacking does not acquire a thrall target", + !s.tick_scratch.player_attacked && s.thrall_target_slot == -1); + + s.player.attack_timer = 0; + col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("an executed player attack acquires a twelve-tick thrall target", + s.tick_scratch.player_attacked && s.player_attack_npc_idx == slot && + s.thrall_target_slot == slot && + s.thrall_aggro_ticks_left == COLO_THRALL_AGGRO_TICKS); osrs_interaction_clear(&s.interaction); encounter_pending_hit_queue_clear(&s.npcs[slot].pending_hits); - - for (int t = 0; t < 3; t++) col_tick_player_ctx(&s, &ctx, idle, 1); - CHECK("thrall fires exactly once per 4 ticks (timer back to 4)", - s.thrall_attack_timer == COLO_THRALL_TICK); - CHECK("exactly one thrall hit is queued in the cadence window", - s.npcs[slot].pending_hits.count == 1 && + s.thrall_attack_timer = 1; + s.tick_scratch.player_attacked = 0; + col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("the thrall attacks after interaction and LOS break", + s.thrall_target_slot == slot && s.npcs[slot].pending_hits.count == 1 && s.npcs[slot].pending_hits.hits[0].source_npc_slot == -1 && s.npcs[slot].pending_hits.hits[0].attack_style == ATTACK_STYLE_MAGIC); float dmg_before = s.tick_scratch.damage_dealt; @@ -6965,55 +6971,54 @@ static void test_thrall_regression(void) { CHECK("the thrall damage is credited to the player accumulator", s.tick_scratch.damage_dealt >= dmg_before); - osrs_interaction_set(&s.interaction, slot); - - int life_now = s.thrall_lifetime_left; - col_tick_player_ctx(&s, &ctx, summon, 1); - CHECK("summon during the recast gate does not reset lifetime", - s.thrall_lifetime_left == life_now - 1); - - while (s.thrall_recast_cd > 0) col_tick_player_ctx(&s, &ctx, idle, 1); - col_tick_player_ctx(&s, &ctx, summon, 1); - CHECK("re-summon after the gate replaces with a fresh 99-tick thrall", - s.thrall_active && s.thrall_lifetime_left == 98); + for (int t = 1; t < 11; t++) { + s.tick_scratch.player_attacked = 0; + col_tick_player_ctx(&s, &ctx, idle, 1); + } + CHECK("thrall aggro remains through eleven ticks after the player attack", + s.thrall_target_slot == slot && s.thrall_aggro_ticks_left == 1); + s.tick_scratch.player_attacked = 0; + col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("thrall aggro clears after the twelfth tick without a player attack", + s.thrall_target_slot == -1 && s.thrall_aggro_ticks_left == 0); + s.player.current_magic = 85; s.thrall_lifetime_left = 1; + osrs_interaction_set(&s.interaction, slot); + s.player.attack_timer = 0; col_tick_player_ctx(&s, &ctx, idle, 1); - CHECK("budget thrall despawns when lifetime reaches 0", - !s.thrall_active && s.thrall_target_slot == -1); + CHECK("expiry discards a same-tick attack and resummons from current Magic", + s.tick_scratch.player_attacked && s.thrall_target_slot == -1 && + s.thrall_aggro_ticks_left == 0 && + s.thrall_lifetime_left == 85 && s.thrall_lifetime_total == 85 && + s.thrall_attack_timer == COLO_THRALL_TICK); + osrs_interaction_clear(&s.interaction); + encounter_pending_hit_queue_clear(&s.npcs[slot].pending_hits); + s.tick_scratch.player_attacked = 0; + for (int t = 0; t < COLO_THRALL_TICK; t++) + col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("the fresh thrall cannot attack before another player attack", + s.thrall_target_slot == -1 && s.npcs[slot].pending_hits.count == 0); thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 202); - col_tick_player_ctx(&s, &ctx, summon, 1); - CHECK("high-eff thrall lifetime starts at 198 (decremented this tick)", - s.thrall_lifetime_left == 197); + CHECK("Master Combat Achievements double the maxed thrall lifetime", + s.thrall_lifetime_left == 198 && s.thrall_lifetime_total == 198); col_init_context_typed(&ctx); memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 203); geo_clear_npcs(&s); s.modifiers.draft_pending = 0; - s.player.x = 12; s.player.y = 18; + s.player.x = 12; + s.player.y = 18; col_rebuild_player_collision_flags(&s); - int sol_slot = col_spawn_npc_at(&s, COLO_SOL_HEREDIT, 18, 16); + int sol_slot = col_spawn_npc_at(&s, COLO_SOL_HEREDIT, 13, 18); s.npcs[sol_slot].hp = 1500; s.npcs[sol_slot].max_hp = 1500; osrs_interaction_set(&s.interaction, sol_slot); - col_tick_player_ctx(&s, &ctx, summon, 1); - CHECK("summon vs Sol Heredit is a no-op (Sol is thrall-immune)", !s.thrall_active); - float mask[COLO_ACTION_MASK_SIZE]; - col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); - int spell_off = col_action_head_mask_offset(COLO_HEAD_SPELL); - CHECK("summon-thrall is masked illegal while targeting Sol", - mask[spell_off + COLO_SPELL_SUMMON_THRALL] == 0.0f); - - s.thrall_active = 1; - s.thrall_target_slot = sol_slot; - s.thrall_attack_timer = 1; - s.thrall_lifetime_left = 50; - int sol_hp = s.npcs[sol_slot].hp; + s.player.attack_timer = 0; col_tick_player_ctx(&s, &ctx, idle, 1); - land_pending_player_hits(&s, &ctx); - CHECK("thrall never damages Sol", s.npcs[sol_slot].hp == sol_hp); + CHECK("attacking Sol does not acquire a thrall target", s.thrall_target_slot == -1); } static void test_death_charge_regression(void) { @@ -7084,9 +7089,9 @@ static void test_death_charge_regression(void) { slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 216); s.player.special_energy = 50; s.npcs[slot].hp = 1; - int summon[COLO_NUM_ACTION_HEADS] = {0}; - summon[COLO_HEAD_SPELL] = COLO_SPELL_SUMMON_THRALL; - col_tick_player_ctx(&s, &ctx, summon, 1); + osrs_interaction_clear(&s.interaction); + s.thrall_target_slot = slot; + s.thrall_aggro_ticks_left = COLO_THRALL_AGGRO_TICKS; col_tick_player_ctx(&s, &ctx, cast_dc, 1); s.thrall_attack_timer = 1; col_tick_player_ctx(&s, &ctx, idle, 1); @@ -7118,11 +7123,11 @@ static void test_combat_fidelity_snapshot_roundtrip(void) { ColosseumState s; int slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 221); col_apply_weapon_set(&s, COLO_GEAR_MAGIC); - s.thrall_active = 1; s.thrall_target_slot = slot; + s.thrall_aggro_ticks_left = 7; s.thrall_lifetime_left = 123; + s.thrall_lifetime_total = 151; s.thrall_attack_timer = 2; - s.thrall_recast_cd = 9; s.death_charge_window_left = 44; s.death_charge_cd = 0; @@ -7138,9 +7143,9 @@ static void test_combat_fidelity_snapshot_roundtrip(void) { CHECK("the recomputed magic set max hit matches the live high-eff value (48)", col_live_loadout_stats(&restored)->max_hit == 48); CHECK("thrall fields round-trip bit-identically", - restored.thrall_active == 1 && restored.thrall_target_slot == slot && - restored.thrall_lifetime_left == 123 && restored.thrall_attack_timer == 2 && - restored.thrall_recast_cd == 9); + restored.thrall_target_slot == slot && restored.thrall_aggro_ticks_left == 7 && + restored.thrall_lifetime_left == 123 && restored.thrall_lifetime_total == 151 && + restored.thrall_attack_timer == 2); CHECK("Death-Charge fields round-trip bit-identically", restored.death_charge_window_left == 44 && restored.death_charge_cd == 0); } @@ -8532,7 +8537,7 @@ static void test_stage3_t4_mask_inventory_heads_flag(void) { CHECK("mask_inventory_heads pins every inventory head to noop only", all_inventory_heads_pinned_to_noop); CHECK("mask_inventory_heads leaves the action-mask size unchanged", - COLO_ACTION_MASK_SIZE == 452); + COLO_ACTION_MASK_SIZE == 451); geo_clear_npcs(&s); s.modifiers.draft_pending = 0; @@ -8585,8 +8590,8 @@ static void test_stage3_t6_obs_mask_fuzz_contract(void) { } step_and_observe(&s, &ctx, actions); } - CHECK("T6 obs running-index assert reached COLO_NUM_OBS", COLO_NUM_OBS == 934); - CHECK("T6 mask running-index assert reached 452", COLO_ACTION_MASK_SIZE == 452); + CHECK("T6 obs running-index assert reached COLO_NUM_OBS", COLO_NUM_OBS == 933); + CHECK("T6 mask running-index assert reached 451", COLO_ACTION_MASK_SIZE == 451); } static void test_death_attribution_credits_actual_source(void) { From d82b15c701109df223a8633b6a3e32d9c37c27b9 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 14:34:08 +0300 Subject: [PATCH 35/50] Explode Volatility on corpse removal --- .../colosseum/encounter_colosseum_combat.inc | 23 ++++-- .../colosseum/encounter_colosseum_model.inc | 5 ++ .../encounter_colosseum_modifiers.inc | 2 +- .../encounter_colosseum_render_snapshot.inc | 59 ++++++++++++++++ .../encounter_colosseum_reward_step.inc | 2 +- ocean/osrs/tests/test_colosseum_modifiers.c | 70 +++++++++++++++---- 6 files changed, 138 insertions(+), 23 deletions(-) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc index bb4ee79ac7..6e7f66e1c7 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc @@ -23,9 +23,6 @@ static void col_apply_npc_death( s->death_charge_cd = COLO_DEATH_CHARGE_CD; } - col_mod_volatility_on_death( - s, ctx->route_topology, - npc->x, npc->y, col_npc_effective_size(npc)); col_mod_on_owner_death(s, idx); if (!osrs_npc_death_linger_start( npc->hp, npc->active, &npc->death_ticks, @@ -145,12 +142,26 @@ static void col_resolve_player_projectiles_on_npcs( col_resolve_player_projectiles_on_npcs_ctx(s, ctx); } -static void col_tick_npc_death_lingers(ColosseumState* s) { +static void col_tick_npc_death_lingers( + ColosseumState* s, + ColosseumContext* ctx +) { for (int i = 0; i < COLO_MAX_NPCS; i++) { ColoNPC* npc = &s->npcs[i]; if (!npc->active || npc->death_ticks <= 0) continue; - if (osrs_npc_death_linger_tick(&npc->death_ticks)) - col_deactivate_npc(s, i); + if (!osrs_npc_death_linger_tick(&npc->death_ticks)) continue; + + int npc_size = col_npc_effective_size(npc); + if (col_mod_active(s, COLO_MOD_VOLATILITY)) { + if (ctx->volatility_explosion_count >= COLO_MAX_NPCS) abort(); + int event = ctx->volatility_explosion_count++; + ctx->volatility_explosion_x[event] = npc->x + npc_size / 2; + ctx->volatility_explosion_y[event] = npc->y + npc_size / 2; + ctx->volatility_explosion_type[event] = npc->type; + } + col_mod_volatility_on_corpse_removed( + s, ctx->route_topology, npc->x, npc->y, npc_size); + col_deactivate_npc(s, i); } } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index 4ab4dac79c..554a749230 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -1322,6 +1322,10 @@ typedef struct { int player_spec_fired_kind; int player_venator_chain_count; int player_venator_chain_slots[OSRS_VENATOR_MAX_CHAIN_HITS]; + int volatility_explosion_count; + int volatility_explosion_x[COLO_MAX_NPCS]; + int volatility_explosion_y[COLO_MAX_NPCS]; + ColoNpcType volatility_explosion_type[COLO_MAX_NPCS]; } ColosseumContext; /* Clears only what was written. col_render_hit_events_add aborts past @@ -1346,6 +1350,7 @@ static inline void col_context_clear_render_events(ColosseumContext* ctx) { ctx->player_venator_chain_count = 0; for (int i = 0; i < OSRS_VENATOR_MAX_CHAIN_HITS; i++) ctx->player_venator_chain_slots[i] = -1; + ctx->volatility_explosion_count = 0; } static inline void col_context_record_venator_chain( diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc index a56cb5c2c6..26e245634b 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc @@ -862,7 +862,7 @@ static int col_volatility_player_in_blast( x, y, size, tier, s->player.x, s->player.y); } -static void col_mod_volatility_on_death( +static void col_mod_volatility_on_corpse_removed( ColosseumState* s, const EncounterArenaTopology* topology, int x, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 907b4c0308..068f6638c9 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -756,6 +756,64 @@ enum { COLO_SOL_SAND_PILLAR_ANIM_ID = 10812, }; +enum { + COLO_VOLATILITY_HUMAN_EXPLOSION_GFX_ID = 2713, + COLO_VOLATILITY_MANTICORE_EXPLOSION_GFX_ID = 2721, + COLO_VOLATILITY_COLOSSUS_EXPLOSION_GFX_ID = 2722, + COLO_VOLATILITY_MINOTAUR_EXPLOSION_GFX_ID = 2723, + COLO_VOLATILITY_SOL_EXPLOSION_GFX_ID = 2724, +}; + +static int col_volatility_explosion_gfx_id(ColoNpcType npc_type) { + switch (npc_type) { + case COLO_MANTICORE: + return COLO_VOLATILITY_MANTICORE_EXPLOSION_GFX_ID; + case COLO_JAVELIN_COLOSSUS: + case COLO_SHOCKWAVE_COLOSSUS: + return COLO_VOLATILITY_COLOSSUS_EXPLOSION_GFX_ID; + case COLO_MINOTAUR: + return COLO_VOLATILITY_MINOTAUR_EXPLOSION_GFX_ID; + case COLO_SOL_HEREDIT: + return COLO_VOLATILITY_SOL_EXPLOSION_GFX_ID; + default: + return COLO_VOLATILITY_HUMAN_EXPLOSION_GFX_ID; + } +} + +static void col_emit_volatility_explosions( + const ColosseumContext* ctx, + EncounterOverlay* ov +) { + encounter_require_projectile_slots(ov, ctx->volatility_explosion_count); + for (int i = 0; i < ctx->volatility_explosion_count; i++) { + int x = ctx->volatility_explosion_x[i]; + int y = ctx->volatility_explosion_y[i]; + OsrsProjectileEventSpec spec = { + .src_x = x, + .src_y = y, + .dst_x = x, + .dst_y = y, + .style = encounter_attack_style_to_proj_style(ATTACK_STYLE_MAGIC), + .damage = 0, + .duration_ticks = 1, + .start_h = 0, + .end_h = 0, + .curve = 0, + .arc_height = 0.0f, + .src_size = 1, + .dst_size = 1, + .model_id = 0, + .anim_id = -1, + .travel_gfx_id = 0, + .launch_gfx_id = + col_volatility_explosion_gfx_id(ctx->volatility_explosion_type[i]), + .impact_gfx_id = 0, + .start_delay = 0, + }; + (void)osrs_emit_projectile_with_spec(ov, &spec, 0); + } +} + static void col_emit_sol_hazard_vfx(ColosseumState* s, EncounterOverlay* ov) { if (s->wave != COLO_WAVE_BOSS || !s->sol.started) return; const SolHereditState* sol = &s->sol; @@ -875,6 +933,7 @@ static void col_render_post_tick_ctx( col_emit_npc_attack_projectile(s, ctx, ov, i); } col_emit_sol_hazard_vfx(s, ov); + col_emit_volatility_explosions(ctx, ov); col_emit_player_attack_projectiles(s, ctx, ov); if (ctx->thrall_attacked_this_tick) col_emit_thrall_projectile(s, ctx, ov); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc index b54bccff88..f35211170b 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc @@ -349,7 +349,7 @@ static ColoPrePlayerPhase col_advance_pre_player_phase_ctx( col_resolve_player_projectiles_on_npcs_ctx(s, ctx); col_resolve_player_pending_hits_ctx(s, ctx); - col_tick_npc_death_lingers(s); + col_tick_npc_death_lingers(s, ctx); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_PRE_RESOLVE_HITS); #endif diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index a0719947e7..ad14a3c1cf 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -1557,7 +1557,7 @@ static void test_reentry_sand_tiles(void) { s.modifiers.active_mask |= (1u << COLO_MOD_VOLATILITY); s.modifiers.tier[COLO_MOD_VOLATILITY] = 3; s.player.x = 5; s.player.y = 18; - col_mod_volatility_on_death( + col_mod_volatility_on_corpse_removed( &s, ctx.route_topology, 20, 16, 1); CHECK("Volatility T3 leaves a Volatility pool at the centre", s.molten_count == 1 && s.molten_kind[0] == COLO_POOL_VOLATILITY); @@ -1576,7 +1576,7 @@ static void test_reentry_sand_tiles(void) { s.molten_count == 0); s.modifiers.tier[COLO_MOD_REENTRY] = 2; - col_mod_volatility_on_death( + col_mod_volatility_on_corpse_removed( &s, ctx.route_topology, 20, 16, 1); CHECK("Reentry II makes the Volatility III pool permanent", s.molten_count == 1 && s.molten_lifetime[0] == COLO_POOL_PERMANENT); @@ -1586,7 +1586,7 @@ static void test_reentry_sand_tiles(void) { s.molten_count = 0; s.modifiers.tier[COLO_MOD_REENTRY] = 3; - col_mod_volatility_on_death( + col_mod_volatility_on_corpse_removed( &s, ctx.route_topology, 20, 16, 1); CHECK("Reentry III also makes the Volatility III pool permanent", s.molten_count == 1 && s.molten_lifetime[0] == COLO_POOL_PERMANENT); @@ -2011,24 +2011,54 @@ static void test_solarflare_orb(void) { CHECK("Solarflare III disables prayer on hit", s.player.prayer == PRAYER_NONE); } -static void test_volatility_explosion(void) { - printf("test_volatility_explosion\n"); +static void test_volatility_explodes_on_corpse_removal(void) { + printf("test_volatility_explodes_on_corpse_removal\n"); ColosseumContext ctx; col_init_context_typed(&ctx); ColosseumState s; memset(&s, 0, sizeof(s)); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 29); - s.modifiers.active_mask |= (1u << COLO_MOD_VOLATILITY); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 302); + geo_clear_npcs(&s); + s.modifiers.active_mask = 1u << COLO_MOD_VOLATILITY; s.modifiers.tier[COLO_MOD_VOLATILITY] = 1; + s.player.x = 20; + s.player.y = 16; - s.player.x = 17; s.player.y = 17; - s.player.current_hitpoints = 99; int idx = 0; - col_init_npc(&s, idx, COLO_FREMENNIK_BERSERKER, 18, 17); - int hp_before = s.player.current_hitpoints; + col_init_npc(&s, idx, COLO_FREMENNIK_BERSERKER, 20, 17); s.npcs[idx].hp = 0; + int hp_before = s.player.current_hitpoints; col_apply_npc_death(&s, &ctx, idx); - CHECK("Volatility explosion hits an adjacent player", s.player.current_hitpoints < hp_before); + + CHECK("Volatility does not explode on the lethal hit", + s.player.current_hitpoints == hp_before); + int linger_ticks = col_npc_death_linger_ticks(s.npcs[idx].type); + for (int tick = 1; tick < linger_ticks; tick++) { + col_tick_npc_death_lingers(&s, &ctx); + CHECK("Volatility waits while the corpse is rendered", + s.npcs[idx].active && s.player.current_hitpoints == hp_before); + } + + col_tick_npc_death_lingers(&s, &ctx); + CHECK("Volatility explodes when the corpse is removed", + !s.npcs[idx].active && s.player.current_hitpoints < hp_before); + + EncounterOverlay ov = {0}; + col_render_post_tick_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("Volatility corpse removal emits the explosion spotanim", + ov.projectile_count == 1 && + ov.projectiles[0].src_x == 20 && + ov.projectiles[0].src_y == 17 && + ov.projectiles[0].dst_x == 20 && + ov.projectiles[0].dst_y == 17 && + ov.projectiles[0].launch_gfx_id == 2713); + CHECK("Volatility explosion spotanim scales by NPC family", + col_volatility_explosion_gfx_id(COLO_MANTICORE) == 2721 && + col_volatility_explosion_gfx_id(COLO_JAVELIN_COLOSSUS) == 2722 && + col_volatility_explosion_gfx_id(COLO_SHOCKWAVE_COLOSSUS) == 2722 && + col_volatility_explosion_gfx_id(COLO_MINOTAUR) == 2723 && + col_volatility_explosion_gfx_id(COLO_SOL_HEREDIT) == 2724); } static void test_modifier_hazard_obs_fixes(void) { @@ -7906,9 +7936,19 @@ static void test_pending_hit_recoil_volatility_damage_accounting(void) { col_resolve_player_pending_hits_ctx(&s, &ctx); CHECK("pending hit lands before its echo-boots recoil death effect", - s.player.current_hitpoints == 64 && + s.player.current_hitpoints == 89 && s.npcs[0].hp == 0); - CHECK("nested Volatility and pending-hit damage remain separately exact", + CHECK("Volatility damage waits for corpse removal", + s.tick_scratch.damage_received == 10.0f && + s.tick_scratch.landed_offpray_damage == 10.0f && + s.tick_scratch.landed_unprayable_damage == 0.0f); + + int linger_ticks = col_npc_death_linger_ticks(s.npcs[0].type); + for (int tick = 0; tick < linger_ticks; tick++) + col_tick_npc_death_lingers(&s, &ctx); + CHECK("corpse removal applies Volatility after the pending hit", + s.player.current_hitpoints == 64); + CHECK("Volatility and pending-hit damage remain separately exact", s.tick_scratch.damage_received == 35.0f && s.tick_scratch.landed_offpray_damage == 10.0f && s.tick_scratch.landed_unprayable_damage == 25.0f); @@ -9143,7 +9183,7 @@ int main(void) { test_step_loop_draft(); test_eleven_drafts_per_run(); test_solarflare_orb(); - test_volatility_explosion(); + test_volatility_explodes_on_corpse_removal(); test_modifier_hazard_obs_fixes(); test_death_linger_wave_clear_and_render(); test_draft_offer_and_select(); From 074ac1bbb0b8f7ce5571a9b08b37229744d4c25c Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 14:35:43 +0300 Subject: [PATCH 36/50] Finalize consolidated Colosseum contract --- .../encounter_colosseum_render_snapshot.inc | 2 +- ocean/osrs/tests/probe_colo_sim_invariant.c | 30 +++++++++--------- ocean/osrs/tests/test_colosseum_golden.c | 31 +++++++++---------- ocean/osrs/tests/test_colosseum_modifiers.c | 10 +++--- 4 files changed, 36 insertions(+), 37 deletions(-) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 068f6638c9..e036af5ce5 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -1104,7 +1104,7 @@ static void col_step_human_commands_ctx( } #define COLO_SNAPSHOT_MAGIC 0xC010C001u -#define COLO_SNAPSHOT_VERSION 24u +#define COLO_SNAPSHOT_VERSION 25u typedef struct { uint32_t magic; diff --git a/ocean/osrs/tests/probe_colo_sim_invariant.c b/ocean/osrs/tests/probe_colo_sim_invariant.c index 2a773377b3..8c18b99d5c 100644 --- a/ocean/osrs/tests/probe_colo_sim_invariant.c +++ b/ocean/osrs/tests/probe_colo_sim_invariant.c @@ -76,22 +76,22 @@ typedef struct { uint64_t baseline; } SimConfig; -/* Baselines are generated on the tree that contains the pathfinding and reservoir - * determinism fixes. Regenerate with --print only when a SIMULATION change is - * intended, never to make an observation edit pass. */ +/* Baselines pin the consolidated Manticore activation, automatic-thrall, and + * corpse-timed Volatility behavior. Regenerate with --print only for an intended + * simulation change, never to make an observation-only edit pass. */ static SimConfig CONFIGS[] = { - {"w01", 1, 1001ULL, 0xC0FFEE01ULL, 0xe882cb88e9bfe571ULL}, - {"w02", 2, 1002ULL, 0xC0FFEE02ULL, 0x2030dfc6f5128a4aULL}, - {"w03", 3, 1003ULL, 0xC0FFEE03ULL, 0xb6340675ad060ccfULL}, - {"w04", 4, 1004ULL, 0xC0FFEE04ULL, 0xda9cbc85c2b1ccceULL}, - {"w05", 5, 1005ULL, 0xC0FFEE05ULL, 0xffe01df334b833dcULL}, - {"w06", 6, 1006ULL, 0xC0FFEE06ULL, 0x91c31d65c5756157ULL}, - {"w07", 7, 1007ULL, 0xC0FFEE07ULL, 0x3abedfcf6d66865bULL}, - {"w08", 8, 1008ULL, 0xC0FFEE08ULL, 0x9c20637df795889bULL}, - {"w09", 9, 1009ULL, 0xC0FFEE09ULL, 0xcbf92ffc1560faa7ULL}, - {"w10", 10, 1010ULL, 0xC0FFEE10ULL, 0xabe0af7272270f17ULL}, - {"w11", 11, 1011ULL, 0xC0FFEE11ULL, 0x4b7bd07c37349b9dULL}, - {"w12", 12, 1012ULL, 0xC0FFEE12ULL, 0xaf6eddfc94e7f411ULL}, + {"w01", 1, 1001ULL, 0xC0FFEE01ULL, 0xc53c6412d234f229ULL}, + {"w02", 2, 1002ULL, 0xC0FFEE02ULL, 0x778581a45fed3ddeULL}, + {"w03", 3, 1003ULL, 0xC0FFEE03ULL, 0xb70dc406e22b2c1aULL}, + {"w04", 4, 1004ULL, 0xC0FFEE04ULL, 0x635935a5ab0ade49ULL}, + {"w05", 5, 1005ULL, 0xC0FFEE05ULL, 0xb8bddb3cfb335938ULL}, + {"w06", 6, 1006ULL, 0xC0FFEE06ULL, 0x39095d5c8accb6dbULL}, + {"w07", 7, 1007ULL, 0xC0FFEE07ULL, 0x494c2434e8dd8c83ULL}, + {"w08", 8, 1008ULL, 0xC0FFEE08ULL, 0x13ee63bf0f98b87bULL}, + {"w09", 9, 1009ULL, 0xC0FFEE09ULL, 0xff48a1acf6a1c401ULL}, + {"w10", 10, 1010ULL, 0xC0FFEE10ULL, 0xb2a8313066a5a4c6ULL}, + {"w11", 11, 1011ULL, 0xC0FFEE11ULL, 0xbb81f53e0991c17aULL}, + {"w12", 12, 1012ULL, 0xC0FFEE12ULL, 0x63fdfb9bd8aed92dULL}, }; static void fill_actions( diff --git a/ocean/osrs/tests/test_colosseum_golden.c b/ocean/osrs/tests/test_colosseum_golden.c index e0984d3b5d..ff12b03a34 100644 --- a/ocean/osrs/tests/test_colosseum_golden.c +++ b/ocean/osrs/tests/test_colosseum_golden.c @@ -182,23 +182,22 @@ static const GoldenConfig CONFIGS[] = { #define NUM_CONFIGS ((int)(sizeof(CONFIGS) / sizeof(CONFIGS[0]))) #define EPISODE_TICKS 4000 -/* Re-seeded for the inventory observation recut: 15 floats per cell became an item code plus - * is_equipped and hp_heal, obs 1258 -> 922. All twelve moved because the hash folds obs - * floats and every episode carries 28 inventory cells from tick zero. probe_colo_sim_invariant - * stayed 12/12 across the change, which is the proof no simulated tick moved. */ +/* Re-seeded for the consolidated contract. Honest Sol laser observations change the + * observation width from 934 to 933. Manticore activation, automatic thralls, and + * corpse-timed Volatility intentionally change simulation timing. */ static const uint64_t BASELINE[NUM_CONFIGS] = { - 0xd2c17aa73e2a0b43ULL, - 0x872cf27641f8df3cULL, - 0x50498ba64b2aa646ULL, - 0xbb24e8e6cb69976dULL, - 0xaa9e6568f552204eULL, - 0xa526657dbf0b7a2fULL, - 0xd8369f4857156246ULL, - 0x2bf1b1a8ae9ec50bULL, - 0x57ae207dc177c45bULL, - 0x44c0ae98c7ab041eULL, - 0x2c7389fc883943a0ULL, - 0x4f5a9f22f5ecfd4dULL, + 0x1258544d5c658eddULL, + 0x68e9ade887662e65ULL, + 0x0c44516a8e72aeb4ULL, + 0x88ac4636389bd581ULL, + 0x6fbfce261a0dbee1ULL, + 0xf43b72bd8e2c2c04ULL, + 0x8332e858be3c913eULL, + 0x1d431ebf29dc964dULL, + 0x75f1c7b7cbb88fbbULL, + 0x701b7d6256d72996ULL, + 0xe5b310495bc052bcULL, + 0x92baaf448c9bdf5cULL, }; int main(int argc, char** argv) { diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index ad14a3c1cf..ad6cf1041e 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -5654,8 +5654,8 @@ static void test_loadout_divine_potions_and_stat_drift(void) { s.divine_ranged_timer = 234; ColoSnapshot snap; col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); - CHECK("snapshot version is v24 for canonical inventory identity", - snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 24u); + CHECK("snapshot version is v25 for the consolidated state layout", + snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 25u); ColosseumState restored; memset(&restored, 0, sizeof(restored)); col_restore_ctx((EncounterState*)&restored, (EncounterContext*)&ctx, &snap, sizeof(snap)); @@ -6486,7 +6486,7 @@ static void test_combat_fidelity_contract_sizes(void) { CHECK("modifier block has 60 features", COLO_MODIFIER_OBS_SIZE == 60); CHECK("NPC slots carry a type code, not a type one-hot", COLO_FEATURES_PER_NPC == 23); - CHECK("snapshot version is v24", COLO_SNAPSHOT_VERSION == 24u); + CHECK("snapshot version is v25", COLO_SNAPSHOT_VERSION == 25u); CHECK("every active NPC gets an obs slot (no busy-wave drop)", COLO_OBS_NPCS == 24 && COLO_OBS_NPCS == COLO_MAX_NPCS); CHECK("PRIMARY head covers noop, movement, and NPC obs slots", @@ -7163,7 +7163,7 @@ static void test_combat_fidelity_snapshot_roundtrip(void) { ColoSnapshot snap; col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); - CHECK("snapshot frame is v24", snap.version == 24u); + CHECK("snapshot frame is v25", snap.version == 25u); ColosseumState restored; memset(&restored, 0, sizeof(restored)); @@ -9153,7 +9153,7 @@ static void test_colosseum_topology_lifecycle_contract(void) { "Colosseum query before topology finalize aborts", child_col_query_before_topology_finalize); assert_child_aborts( - "Colosseum previous inventory snapshot version aborts", + "Colosseum previous snapshot version aborts", child_col_restore_previous_inventory_snapshot); } From 8be1f2e0fd81ddbe2d9f3d3f64f51ec9e19383a3 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 14:55:01 +0300 Subject: [PATCH 37/50] Preserve Volatility during wave cleanup --- .../colosseum/encounter_colosseum_combat.inc | 32 ++++++++++++------- .../colosseum/encounter_colosseum_helpers.inc | 4 ++- .../encounter_colosseum_reset_spawn.inc | 7 ++-- ocean/osrs/tests/test_colosseum_modifiers.c | 12 +++++++ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc index 6e7f66e1c7..229a4ceb4b 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc @@ -142,6 +142,26 @@ static void col_resolve_player_projectiles_on_npcs( col_resolve_player_projectiles_on_npcs_ctx(s, ctx); } +static void col_remove_npc_corpse( + ColosseumState* s, + ColosseumContext* ctx, + int npc_slot +) { + ColoNPC* npc = &s->npcs[npc_slot]; + assert(npc->active && npc->hp <= 0); + int npc_size = col_npc_effective_size(npc); + if (col_mod_active(s, COLO_MOD_VOLATILITY)) { + if (ctx->volatility_explosion_count >= COLO_MAX_NPCS) abort(); + int event = ctx->volatility_explosion_count++; + ctx->volatility_explosion_x[event] = npc->x + npc_size / 2; + ctx->volatility_explosion_y[event] = npc->y + npc_size / 2; + ctx->volatility_explosion_type[event] = npc->type; + } + col_mod_volatility_on_corpse_removed( + s, ctx->route_topology, npc->x, npc->y, npc_size); + col_deactivate_npc(s, npc_slot); +} + static void col_tick_npc_death_lingers( ColosseumState* s, ColosseumContext* ctx @@ -151,17 +171,7 @@ static void col_tick_npc_death_lingers( if (!npc->active || npc->death_ticks <= 0) continue; if (!osrs_npc_death_linger_tick(&npc->death_ticks)) continue; - int npc_size = col_npc_effective_size(npc); - if (col_mod_active(s, COLO_MOD_VOLATILITY)) { - if (ctx->volatility_explosion_count >= COLO_MAX_NPCS) abort(); - int event = ctx->volatility_explosion_count++; - ctx->volatility_explosion_x[event] = npc->x + npc_size / 2; - ctx->volatility_explosion_y[event] = npc->y + npc_size / 2; - ctx->volatility_explosion_type[event] = npc->type; - } - col_mod_volatility_on_corpse_removed( - s, ctx->route_topology, npc->x, npc->y, npc_size); - col_deactivate_npc(s, i); + col_remove_npc_corpse(s, ctx, i); } } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc index 939534ffe4..13dbf07094 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc @@ -3,7 +3,7 @@ static void col_accumulate_tick_stats(ColosseumState* s, const ColosseumContext* ctx); static void col_spawn_wave( ColosseumState* s, - const ColosseumContext* ctx + ColosseumContext* ctx ); static void col_tick_npcs_ctx(ColosseumState* s, ColosseumContext* ctx); static void col_tick_player_ctx( @@ -12,6 +12,8 @@ static void col_player_pretick(ColosseumState* s, ColosseumContext* ctx, const i static void col_resolve_player_projectiles_on_npcs_ctx( ColosseumState* s, ColosseumContext* ctx); static void col_resolve_player_pending_hits_ctx(ColosseumState* s, ColosseumContext* ctx); +static void col_remove_npc_corpse( + ColosseumState* s, ColosseumContext* ctx, int npc_slot); static void col_rebuild_player_collision_flags(ColosseumState* s); static void col_refresh_current_obs_slots_ctx(ColosseumState* s, const ColosseumContext* ctx); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc index 98bb474b61..9a56b80ab3 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -382,7 +382,7 @@ static const ColoNpcType COLO_WARBAND_TYPES[3] = { static void col_spawn_wave( ColosseumState* s, - const ColosseumContext* context + ColosseumContext* context ) { const EncounterArenaTopology* topology = context->route_topology; encounter_arena_topology_require_finalized(topology); @@ -391,7 +391,10 @@ static void col_spawn_wave( for (int i = 0; i < COLO_MAX_NPCS; i++) { if (!s->npcs[i].active) continue; if (s->npcs[i].type == COLO_BEE_SWARM) continue; - col_deactivate_npc(s, i); + if (s->npcs[i].death_ticks > 0) + col_remove_npc_corpse(s, context, i); + else + col_deactivate_npc(s, i); } memset(s->totems, 0, sizeof(s->totems)); memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index ad6cf1041e..378d738d53 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -2059,6 +2059,18 @@ static void test_volatility_explodes_on_corpse_removal(void) { col_volatility_explosion_gfx_id(COLO_SHOCKWAVE_COLOSSUS) == 2722 && col_volatility_explosion_gfx_id(COLO_MINOTAUR) == 2723 && col_volatility_explosion_gfx_id(COLO_SOL_HEREDIT) == 2724); + + col_context_clear_render_events(&ctx); + geo_clear_npcs(&s); + s.player.current_hitpoints = 99; + col_init_npc(&s, idx, COLO_MINOTAUR, 20, 17); + s.npcs[idx].hp = 0; + col_apply_npc_death(&s, &ctx, idx); + s.wave = 1; + col_spawn_wave(&s, &ctx); + CHECK("wave spawn cleanup still removes a volatile corpse through its explosion", + s.player.current_hitpoints < 99 && + ctx.volatility_explosion_count == 1); } static void test_modifier_hazard_obs_fixes(void) { From 79412624767929ad77552e5ae1db3d77b1ebb3a1 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Wed, 12 Aug 2026 15:49:25 +0300 Subject: [PATCH 38/50] Remove internal planning artifacts --- .superpowers/sdd/task-3-report.md | 262 ---------- .superpowers/sdd/task-5-report.md | 39 -- .../2026-08-12-colosseum-consolidation.md | 450 ------------------ ...osrs-global-runtime-optimization-design.md | 265 ----------- ...26-08-12-colosseum-consolidation-design.md | 99 ---- 5 files changed, 1115 deletions(-) delete mode 100644 .superpowers/sdd/task-3-report.md delete mode 100644 .superpowers/sdd/task-5-report.md delete mode 100644 docs/superpowers/plans/2026-08-12-colosseum-consolidation.md delete mode 100644 docs/superpowers/specs/2026-08-11-osrs-global-runtime-optimization-design.md delete mode 100644 docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md diff --git a/.superpowers/sdd/task-3-report.md b/.superpowers/sdd/task-3-report.md deleted file mode 100644 index a3a5d48c74..0000000000 --- a/.superpowers/sdd/task-3-report.md +++ /dev/null @@ -1,262 +0,0 @@ -# Task 3 report - -Status: DONE - -## Result - -Inferno, Colosseum, Zulrah, and NH PvP now use `EncounterArenaTopology` plus `EncounterRouteBlockers` through one shared route engine. Production callers no longer use `EncounterArenaAttackRouteField`, `encounter_build_arena_attack_route_field`, `encounter_arena_attack_route_landing`, `encounter_pathfind_arena_attack_approach`, or the old arena BFS helpers. - -The engine returns `EncounterRouteResult` with `ROUTE_REACHED_TARGET`, `ROUTE_REACHED_FALLBACK`, `ROUTE_UNREACHABLE`, or `ROUTE_INVALID_INPUT`. Expected route failure is data. Invalid internal state, unsupported topology bounds, queue overflow, corrupt predecessor state, stale topology revision, and ownership violations abort. - -`EncounterRouteInput` carries the immutable topology and dynamic blocker callback, context, and revision. Query scratch and reusable source/reverse fields are thread-local fixed storage. Queries allocate nothing and take no locks. One finalized topology is process-shared per encounter and each encounter context stores a const pointer. Dynamic occupancy stays in encounter state. - -Player chase state moved out of serialized `OsrsInteraction` state into actor-local `OsrsActorRouteCache`. Cache validation covers topology revision, blocker revision, source/current actor position, actor size, target position and size, attack range, movement mode, and route-cost policy. Restore/reset paths clear the context cache. `OsrsInteraction` retains its fixed 244-byte serialized route reserve, but raw base artifacts intentionally differ where the removed serialized cache had data. Forecast comparison validates each file's raw record hash before canonicalizing route storage. - -Equal-cost behavior remains explicit in `EncounterRouteCostPolicy`. Attack-chase OSRS order uses the packed source field. Colosseum NPC OSRS-aggro paths use the reverse field with preserved south-first edge ordering. `OSRS_PLAYER_MOVE_ACTION` uses the direct one-action policy. `OSRS_PLAYER_MOVE_DESTINATION` uses the legacy south-first BFS policy in all four encounters. Attack-range overlap enters the same deterministic north-first escape path as cardinal-adjacency overlap. - -## TDD evidence - -### RED - -The first route-result tests were added to `test_osrs_player_step.c` before the production contract. This command failed to compile because the tagged route types and solver did not exist: - -```text -clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_player_step.c -lm -o /tmp/test_osrs_player_step_task3 -``` - -The first migrated forecast comparison exposed route-order drift: - -```text -/tmp/test_colosseum_forecast_exact_task3 --compare /tmp/task3-colosseum-forecast -colosseum exact mismatch at byte 1947076: expected 117 got 39 -``` - -Changing all explicit movement to direct routing proved incorrect for Zulrah: - -```text -clang -std=c11 -O2 -I. ocean/osrs/tests/test_zulrah_golden.c -lm -o /tmp/test_zulrah_golden_task3 && /tmp/test_zulrah_golden_task3 -``` - -All six golden hashes changed. Restoring Zulrah explicit movement to `ENCOUNTER_ROUTE_COST_OSRS` restored 6/6 exact hashes. - -The initial exhaustive reference comparison caught an invalid overlap case in the test driver: - -```text -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_forecast_exact.c -lm -o /tmp/test_colosseum_forecast_exact_task3 && /tmp/test_colosseum_forecast_exact_task3 --attack-route-selftest -route equivalence mismatch scenario=open-arena source=(0,13) target=(0,13,2) expected=(0,2,13,1,0,2) actual=(0,1,12,1,0,2) -``` - -The exhaustive driver was corrected to exclude actor/target overlap. The production interaction layer handles overlap through its explicit escape contract rather than normal attack-chase routing. - -The final overlap regression failed before the shared solver dispatched attack-range overlap to the escape path: - -```text -clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_player_step.c -lm -o /tmp/task3_overlap_tie_red && /tmp/task3_overlap_tie_red -FAIL: range overlap uses north-first deterministic escape -74/75 tests passed -``` - -The symmetric-obstacle regression also distinguishes OSRS attack order from legacy south-first destination order and drives `osrs_player_step_apply_explicit_move` through the destination policy. - -### GREEN - -Tagged outcomes, cache invalidation, direct movement, attack-range overlap escape, symmetric-obstacle destination tie order, topology bounds, dynamic blockers, and actor-local reuse: - -```text -clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_player_step.c -lm -o /tmp/task3_destination_tie_green && /tmp/task3_destination_tie_green -``` - -Result: 76/76 passed. - -Shared NPC movement, footprint masks, diagonal edge clearance, overlap rewrite, melee policy, and current overlap: - -```text -clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_npc_movement.c -lm -o /tmp/test_osrs_npc_movement_task3 && /tmp/test_osrs_npc_movement_task3 -``` - -Result: 29/29 passed. - -Exhaustive optimized-versus-reference BFS: - -```text -/tmp/test_colosseum_forecast_exact_task3 --attack-route-selftest -/tmp/test_inferno_forecast_exact_task3 --attack-route-selftest -clang -std=c11 -O2 -I. -DTEST_ROUTE_TOPOLOGY_NH_PVP ocean/osrs/tests/test_encounter_route_topology_setup.c -lm -o /tmp/test_route_topology_nh_pvp && /tmp/test_route_topology_nh_pvp -``` - -Results: - -```text -colosseum exhaustive route equivalence PASS: 22511700 source-target-range queries across 2 blocker fields -colosseum attack route property selftest PASS: 1656 target queries across 3 fields -inferno exhaustive route equivalence PASS: 19048431 source-target-range queries across 2 blocker fields -nh_pvp target-directed BFS equivalence PASS: 5418624 source-target queries across 2 blocker fields -``` - -The test-only reference performs an independent legacy FIFO BFS and landing scan. The checks cover every walkable source, every statically valid target anchor, target sizes through each encounter maximum, every encounter player attack range, and representative dynamic blocker revisions. Each comparison pins outcome, destination, shortest distance, first step, run step, and waypoint sequence. - -Topology finalization, immutable content identity, construction-order independence, process reuse, and bounds: - -```text -clang -std=c11 -O2 -I. -DTEST_ROUTE_TOPOLOGY_INFERNO ocean/osrs/tests/test_encounter_route_topology_setup.c -lm -o /tmp/test_route_topology_inferno && /tmp/test_route_topology_inferno -clang -std=c11 -O2 -I. -DTEST_ROUTE_TOPOLOGY_COLOSSEUM ocean/osrs/tests/test_encounter_route_topology_setup.c -lm -o /tmp/test_route_topology_colosseum && /tmp/test_route_topology_colosseum -clang -std=c11 -O2 -I. -DTEST_ROUTE_TOPOLOGY_ZULRAH ocean/osrs/tests/test_encounter_route_topology_setup.c -lm -o /tmp/test_route_topology_zulrah && /tmp/test_route_topology_zulrah -clang -std=c11 -O2 -I. -DTEST_ROUTE_TOPOLOGY_NH_PVP ocean/osrs/tests/test_encounter_route_topology_setup.c -lm -o /tmp/test_route_topology_nh_pvp && /tmp/test_route_topology_nh_pvp -``` - -Results: Inferno 870 open and 0 blocked, Colosseum 865 open and 291 blocked, Zulrah 69 open and 715 blocked, and NH PvP 1648 open and 60 blocked. Every setup test passed order independence and identical-map process reuse. - -Forecast fixtures came from base commit `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731`. Each expected and current raw state first validates against the state hash stored in its own record. The comparator then clears the removed serialized route storage in both states, clears Colosseum's route-profiling-only blocker counters, recomputes the canonical state hashes, and compares canonical record headers, forecasts, observations, masks, and state bytes. Raw base bytes are not claimed identical because the deleted route cache occupied the serialized reserve. - -```text -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_forecast_exact.c -lm -o /tmp/test_colosseum_forecast_exact_task3 -clang -std=c11 -O2 -I. ocean/osrs/tests/test_inferno_forecast_exact.c -lm -o /tmp/test_inferno_forecast_exact_task3 -/tmp/test_colosseum_forecast_exact_task3 --compare /tmp/task3-colosseum-forecast -/tmp/test_inferno_forecast_exact_task3 --compare /tmp/task3-inferno-forecast -``` - -Results: both raw record-integrity checks and canonical semantic comparisons passed. Colosseum also passed 1,336,336 LoS pairs, 12,005 footprint checks, 25 landing actions, 22,511,700 exhaustive route queries, and 1,656 attack-route property queries. - -Focused encounter mechanics: - -```text -clang -std=c11 -O2 -I. ocean/osrs/tests/test_inferno_attack_styles.c -lm -o /tmp/test_inferno_attack_styles_task3 && /tmp/test_inferno_attack_styles_task3 -clang -std=c11 -O2 -I. ocean/osrs/tests/test_zulrah_hit_delay.c -lm -o /tmp/test_zulrah_hit_delay_task3 && /tmp/test_zulrah_hit_delay_task3 -clang -std=c11 -O2 -I. ocean/osrs/tests/test_zulrah_golden.c -lm -o /tmp/test_zulrah_golden_task3 && /tmp/test_zulrah_golden_task3 -clang -std=c11 -O2 -I. ocean/osrs/tests/test_osrs_pvp_pending_hits.c -lm -o /tmp/test_osrs_pvp_pending_hits_task3 && /tmp/test_osrs_pvp_pending_hits_task3 -``` - -Results: - -- Inferno: 2041/2041 passed -- Zulrah hit delay and roll order: 313/313 passed -- Zulrah golden master: 6/6 hashes match baseline -- NH PvP pending hits: 9/9 passed - -An extra full `test_colosseum_modifiers.c` run produced 13545/13546 with only `generic item table code 101 feature 13 matches base-99 semantics` failing. The untouched base worktree produced the identical 13545/13546 failure. Task 3 did not change item encoding. Every route, movement, LoS, forecast, snapshot, combat, and modifier check in that binary passed. - -## Standalone API consumers - -No clangd indexer or harness LSP-reference operation was available. Before deleting exported route symbols, textual reference discovery covered production, tests, wrappers, and standalone benchmarks. Consumers importing the deleted API were migrated to the test-only reference header where comparison with the old implementation is intentional: - -- `ocean/osrs/tests/test_osrs_player_step.c` -- `ocean/osrs/tests/test_colosseum_forecast_exact.c` -- `ocean/osrs/tests/test_colosseum_modifiers.c` -- `ocean/osrs/tests/test_inferno_attack_styles.c` -- `ocean/osrs/tests/bench_colo_step.c` -- `ocean/osrs/tests/bench_colosseum_forecast_profile.c` -- `ocean/osrs_colosseum/osrs_colosseum.h` -- `ocean/osrs_inferno/osrs_inferno.h` - -Final production search for `EncounterArenaAttackRouteField`, `encounter_build_arena_attack_route_field`, `encounter_arena_attack_route_landing`, `encounter_pathfind_arena_attack_approach`, `pathfind_step_arena`, `BFS_VISIT`, `bfs_via`, and `route_field` returned matches only from `ocean/osrs/tests/osrs_route_reference.h` and tests calling that reference. No production match remains. - -## Build and portability gates - -Available native viewer wrappers: - -```text -OUTPUT_NAME=/tmp/osrs_task3_inferno_wrapper ./build.sh osrs_inferno --fast -OUTPUT_NAME=/tmp/osrs_task3_colosseum_wrapper ./build.sh osrs_colosseum --fast -``` - -Results: - -```text -Built: .//tmp/osrs_task3_inferno_wrapper -Built: .//tmp/osrs_task3_colosseum_wrapper -``` - -The repository has no separate Zulrah or NH PvP wrapper directories. Both compile into the shared Inferno viewer/profile wrapper and were exercised through `--encounter zulrah` and `--encounter nh_pvp`. - -The macOS ARM64 `--cpu` wrapper mode is unavailable because PufferLib's CPU backend includes x86-only intrinsics. It failed before encounter compilation. CPU translation portability was checked directly for every encounter header: - -```text -clang -std=c11 -O2 -I. /tmp/route_portability_inferno.c -lm -o /tmp/route_portability_inferno_c -clang -std=c11 -O2 -I. /tmp/route_portability_colosseum.c -lm -o /tmp/route_portability_colosseum_c -clang -std=c11 -O2 -I. /tmp/route_portability_zulrah.c -lm -o /tmp/route_portability_zulrah_c -clang -std=c11 -O2 -I. /tmp/route_portability_nh_pvp.c -lm -o /tmp/route_portability_nh_pvp_c -g++-16 -std=c++17 -O2 -I. -x c++ /tmp/route_portability_inferno.c -lm -o /tmp/route_portability_inferno_cpp -g++-16 -std=c++17 -O2 -I. -x c++ /tmp/route_portability_colosseum.c -lm -o /tmp/route_portability_colosseum_cpp -g++-16 -std=c++17 -O2 -I. -x c++ /tmp/route_portability_zulrah.c -lm -o /tmp/route_portability_zulrah_cpp -g++-16 -std=c++17 -O2 -I. -x c++ /tmp/route_portability_nh_pvp.c -lm -o /tmp/route_portability_nh_pvp_cpp -``` - -Result: all eight C/C++17 translation and link gates passed. - -## Paired performance - -Method: base binary from clean `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731`, final current binary, 100,000 steps, paired policy seeds 1 through 12, identical encounter and seed per pair. The clean base profiling copy received one benchmark-only seeding patch matching the final driver: parse `--policy-seed` and call `srand(profile_seed)` once before the profile loop. It changed no encounter, route, action, observation, reward, or terminal code. Both binaries therefore consumed the same deterministic policy stream for each pair. Command shape: - -```text -BINARY --profile --encounter ENCOUNTER --profile-steps 100000 --policy-seed SEED -``` - -| Encounter | Base SPS by seed 1..12 | Current SPS by seed 1..12 | Base GM | Current GM | Ratio GM | -|---|---|---|---:|---:|---:| -| Inferno | 715533, 778610, 782075, 773880, 774833, 763091, 770297, 780482, 790451, 793796, 798869, 775958 | 823981, 877216, 891957, 881376, 876539, 879832, 892069, 879933, 878464, 887863, 884463, 905928 | 774547 | 879763 | 1.1358x | -| Colosseum | 410523, 409539, 414233, 411516, 411634, 415806, 412797, 408617, 417805, 414190, 411997, 410698 | 495086, 489165, 495774, 493320, 499146, 495108, 489340, 492587, 491034, 491615, 503603, 503547 | 412438 | 494921 | 1.2000x | -| Zulrah | 343538, 347723, 341180, 345094, 347872, 342274, 349550, 343881, 346293, 355130, 342277, 352433 | 714439, 724843, 710732, 709859, 701208, 706334, 713409, 724422, 711541, 702849, 694850, 726633 | 346413 | 711697 | 2.0545x | -| NH PvP | 528933, 508037, 508001, 518092, 512665, 521387, 524002, 515876, 546269, 524706, 533072, 513215 | 660703, 660375, 654279, 666898, 660009, 660707, 661673, 665602, 657173, 656146, 666871, 655295 | 521079 | 660465 | 1.2675x | - -Aggregate geometric mean of the four encounter ratios: 1.3726x. No encounter regressed. - -## Changed files - -Full `8fd9feb0fc5c19428751a16cc6b2b82aa2f9e731..HEAD` task delta: - -- `.superpowers/sdd/task-3-report.md` -- `ocean/osrs/osrs_collision.h` -- `ocean/osrs/osrs_encounter.h` -- `ocean/osrs/osrs_encounter_player.h` -- `ocean/osrs/osrs_interaction.h` -- `ocean/osrs/osrs_pathfinding.h` -- `ocean/osrs/osrs_pvp_actions.h` -- `ocean/osrs/osrs_pvp_api.h` -- `ocean/osrs/osrs_pvp_movement.h` -- `ocean/osrs/osrs_render.h` -- `ocean/osrs/osrs_visual.c` -- `ocean/osrs/encounters/encounter_nh_pvp.h` -- `ocean/osrs/encounters/encounter_zulrah.h` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc` -- `ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc` -- `ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc` -- `ocean/osrs/encounters/inferno/encounter_inferno_model.inc` -- `ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc` -- `ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc` -- `ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc` -- `ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc` -- `ocean/osrs/tests/osrs_route_reference.h` -- `ocean/osrs/tests/test_encounter_route_topology_setup.c` -- `ocean/osrs/tests/test_osrs_player_step.c` -- `ocean/osrs/tests/test_colosseum_forecast_exact.c` -- `ocean/osrs/tests/test_colosseum_modifiers.c` -- `ocean/osrs/tests/test_inferno_attack_styles.c` -- `ocean/osrs/tests/test_inferno_forecast_exact.c` -- `ocean/osrs/tests/test_zulrah_golden.c` -- `ocean/osrs/tests/test_zulrah_hit_delay.c` -- `ocean/osrs/tests/bench_colo_step.c` -- `ocean/osrs/tests/bench_colosseum_forecast_profile.c` -- `ocean/osrs_colosseum/osrs_colosseum.h` -- `ocean/osrs_inferno/osrs_inferno.h` - -## Self-review - -- Topology stays immutable after finalization. No dynamic actor, pillar, clamp, cloud, or opponent occupancy entered it. -- Contexts hold const process-shared topology pointers. -- Route scratch is thread-local, fixed-size, and generation-guarded or reset before reads. -- Query hot paths allocate nothing, take no locks, and have no fallback to the deleted production implementation. -- Actor route caches validate every required key. Changed blocker revisions and target geometry reroute from the actor's current tile. -- Source/reverse fields validate topology revision, blocker identity/revision, source or target geometry, actor size, movement mode, and cost policy as applicable. -- Dynamic blocker memoization distinguishes unknown, open, and blocked footprints and never mutates topology. -- Fallback distance, FIFO/depth tie-breaking, first step, run step, and waypoint compression match the independent references across 46,978,755 exhaustive queries. -- Attack-range overlap uses the deterministic escape contract before attack routing. A symmetric obstacle pins destination movement to legacy south-first equal-cost order while attack routing retains OSRS ordering. -- Inferno and Colosseum canonical forecasts are byte-exact after clearing removed serialized route storage and route-profiling counters. Raw records first pass their own stored-hash integrity checks. Zulrah's six hashes remain exact. Snapshot state size and serialized interaction layout remain unchanged. -- Production search finds no old route field or arena BFS symbol. The only legacy implementation is isolated in `ocean/osrs/tests/osrs_route_reference.h`. -- Final `git diff --check` passed. diff --git a/.superpowers/sdd/task-5-report.md b/.superpowers/sdd/task-5-report.md deleted file mode 100644 index 669415ce6c..0000000000 --- a/.superpowers/sdd/task-5-report.md +++ /dev/null @@ -1,39 +0,0 @@ -# Task 5 report - -## Result - -Implementation and main validation complete. - -Base: `a506c5a462c4a035e9d79d842d35655ac7a4102c` - -## Implementation - -- Moved Colosseum arena bounds, static footprint occupancy, legal steps, and directed static LOS to the shared `EncounterArenaTopology`. -- Added the tile-blocked topology LOS build mode that preserves Colosseum's legacy directional endpoint rules. -- Deleted the private blocked-tile array, footprint table, static LOS table, and their initialization latch. -- Routed player, NPC, forecast, spawn, Sol, modifier, observation, mask, lab, and render geometry through explicit topology-owning contexts. -- Kept NPC occupancy, player occupancy, modifiers, hazards, and active state dynamic. -- Deleted the process-global legacy context and contextless reset, step, combat, lab, and attack wrappers. -- Made supported callbacks reject NULL context. Standalone probes and tests now initialize and finalize explicit contexts. -- Added exhaustive static tile, footprint, directed LOS, endpoint-law, legal-step, and forecast parity checks to existing Colosseum tests. -- Added lifecycle checks for NULL and unfinalized contexts. -- Kept the checked shared footprint query unchanged and added a trusted finalized, size-in-range query for validated Colosseum path contexts. The hot route blocker now pays lifecycle and size checks once when it creates the path context instead of once per explored tile. -- Preserved the observation and mask layouts. No item tables changed. - -## Observed evidence - -The interrupted worker's compile attempts exposed stale private-geometry and old-signature callers in `test_colosseum_modifiers.c`. The first main gate then reproduced a stale `col_player_walkable` callback-data crash in the modifier test. Source inspection found the same callback misuse in the skyfall probe and movement trace, plus unfinalized contexts in five standalone probes. Recovery migrated those callers to explicit geometry contexts and finalized every audited context before reset, step, or topology query. - -After that fix, exact, golden, and determinism gates passed. Twelve paired 5M-step profiles regressed on every seed, with a final/base geometric mean of `0.978398718` and median SPS of `482699` versus `493402`. Equivalent samples isolated repeated finalized-state and footprint-size checks under `col_npc_path_blocked`. The follow-up moves those checks to `col_npc_path_ctx_begin` and retains raw bounds handling on every tile query. - -## Main validation - -- Modifier battery: `13549/13550`. Every Colosseum topology and Sol spear check passed. The sole failure is the inherited generic item-table code `101` mismatch that reproduces at the Task 5 base. -- Exact forecast: topology LOS `1335180` directed pairs, footprint `5709` NPC-size checks, landing `25` actions, route equivalence `22511700` queries, attack routes `1656` queries, and the Task 4 golden fixture all passed. -- Golden: `12/12` configs matched. -- Sim invariant: all 12 printed hashes matched the detached Task 5 base byte for byte. -- Determinism: one worker versus eight workers and repeated eight-worker runs were identical across 256 envs in both late-start modes. -- Portability: the C11 topology setup and C++17 Colosseum header gates passed. -- Sanitizers: the topology setup passed UBSan. The host ASan runtime deadlocked before `main` in `AsanInitInternal`, so no ASan result exists. -- Performance: all 12 paired 5M-step runs improved. Final/base ratios were `1.001197`, `1.013857`, `1.005567`, `1.013254`, `1.008387`, `1.005730`, `1.008313`, `1.003657`, `1.006833`, `1.000941`, `1.013913`, and `1.003901`. The geometric mean was `1.007119621`. Median SPS rose from `487283` to `491122`. -- The post-fix sample no longer attributes route-solver samples to the checked Colosseum footprint wrapper. diff --git a/docs/superpowers/plans/2026-08-12-colosseum-consolidation.md b/docs/superpowers/plans/2026-08-12-colosseum-consolidation.md deleted file mode 100644 index 1075df811c..0000000000 --- a/docs/superpowers/plans/2026-08-12-colosseum-consolidation.md +++ /dev/null @@ -1,450 +0,0 @@ -# Colosseum Consolidation Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Consolidate the latest Colosseum behavior onto the optimized 5c branch and make the run38 `933`-observation, `451`-mask checkpoint contract canonical. - -**Architecture:** Treat the current 5c branch as the structural source of truth. Port the net behavior from `0fc4b50c..8281518ea` and the Colosseum-only behavior from `3812fd454`, adapting every call to the current shared topology, route, context, and item-metadata APIs. - -**Tech Stack:** C11 single-header encounter implementation, shared OSRS SDK, clang, local Metal viewer, deterministic golden and semantic-hash probes. - -## Global Constraints - -- Keep `EncounterArenaTopology`, the shared route engine, canonical inventory content codes, and shared item metadata. -- Canonical model contract: `COLO_NUM_OBS == 933`, `COLO_ACTION_MASK_SIZE == 451`, spell head `{none, Death Charge}`. -- Canonical snapshot version: `25`. -- Preserve `laser_obs_mode` and require `1` for run38 evaluation. -- Port the verified `osrs-assets-v22` manifest SHA. Do not port the Puffer source-layout migration, old item-table files, stale baselines, or the altered FNV seed. -- Do not modify Inferno, Zulrah, NH PvP, Puffer internals, checkpoints, generated assets, or local untracked artifacts. -- Every semantic change gets a failing contract test before implementation. - ---- - -### Task 1: Isolate and establish the red baseline - -**Files:** -- Modify: `ocean/osrs/asset_manifest.json` -- Modify later: `ocean/osrs/encounters/colosseum/*.inc` -- Test: `ocean/osrs/tests/test_colosseum_modifiers.c` - -**Interfaces:** -- Consumes: `valtteri/colo-5c-port` at the plan commit. -- Produces: `/private/tmp/puffer-colo-consolidation` on `valtteri/colo-consolidation`. - -- [ ] **Step 1: Create the temporary branch and worktree** - -```bash -pwd -git worktree add /private/tmp/puffer-colo-consolidation -b valtteri/colo-consolidation valtteri/colo-5c-port -``` - -Expected: worktree starts at the plan commit with no tracked modifications. - -- [ ] **Step 2: Pin and install the published asset archive** - -Set the `osrs-assets-v22` SHA to `2f908da5b5ddf148c0cbef48ad6334c5253c04c6cd83782340bc2f1c5dffc7ad`, then run: - -```bash -bash ocean/osrs/scripts/setup-data.sh -``` - -Expected: the downloaded release archive matches the manifest and installs the ignored local data. - -- [ ] **Step 3: Compile and run the current focused baseline** - -```bash -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_consolidation_baseline -/tmp/colo_consolidation_baseline -``` - -Expected: `13550/13550 tests passed`. - -- [ ] **Step 4: Confirm current interface values before adding red tests** - -Use the existing `test_primary_head_resolution` and fuzz assertions. - -Expected: `COLO_NUM_OBS == 922`, `COLO_ACTION_MASK_SIZE == 452`, `COLO_SPELL_DIM == 3`. - ---- - -### Task 2: Restore honest Sol laser observations - -**Files:** -- Modify: `config/osrs_colosseum.ini` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` -- Test: `ocean/osrs/tests/test_colosseum_modifiers.c` - -**Interfaces:** -- Consumes: current `ColoSolState`, `ColosseumConfig`, `ColosseumLog`, and `col_write_boss_obs`. -- Produces: `laser_obs_mode`, twelve added boss-observation floats, and episode laser diagnostics. - -- [ ] **Step 1: Add failing laser-pack contract tests** - -Add assertions that the enabled pack reports one cooldown plus four records of three floats and that disabled mode zeroes exactly those thirteen laser-pack channels without changing width. Assert the intermediate interface: - -```c -CHECK("laser pack expands obs 922 to 934", COLO_NUM_OBS == 934); -``` - -Run the modifiers suite. Expected: compile or assertion failure because the pack and config field do not exist. - -- [ ] **Step 2: Add final laser state and configuration fields** - -Add to `ColoSolState`: - -```c -int laser_volley_active; -int laser_show_seen; -int laser_aligned_show; -int laser_n_aligned_show; -int laser_n_active_at_fire; -``` - -Add `int laser_obs_mode` to `ColosseumConfig` with default `1`, binary config parsing, and `laser_obs_mode = 1` in `config/osrs_colosseum.ini`. Add the nine `laser_*` float counters from `8281518ea` to `ColosseumLog` and preserve their existing metric names. - -- [ ] **Step 3: Port the final multi-crystal observation writer** - -Define: - -```c -#define COLO_SOL_LASER_CRYSTAL_FEATS 3 -#define COLO_SOL_LASER_OBS_SIZE \ - (1 + COLO_SOL_MAX_CRYSTALS * COLO_SOL_LASER_CRYSTAL_FEATS) -#define COLO_BOSS_OBS_PREMOVE_TAIL_SIZE (2 + COLO_SOL_LASER_OBS_SIZE) -``` - -Change `col_write_boss_obs` to accept `int laser_obs_mode`. Write the normalized volley cooldown, then each crystal's active flag, signed line delta, and normalized freeze timer. Advance the same indices with zeros when disabled. - -- [ ] **Step 4: Port final laser event accounting** - -Reset volley diagnostics at Sol reset. At show, prefire, and fire transitions, count active and aligned crystals and update the existing episode log fields. Do not port the canceled next-style or pending-style experiments. - -- [ ] **Step 5: Run and commit** - -```bash -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_laser_pack -/tmp/colo_laser_pack -git add config/osrs_colosseum.ini ocean/osrs/encounters/colosseum ocean/osrs/tests/test_colosseum_modifiers.c -git commit -m "Restore honest Sol laser observations" -``` - -Expected: modifiers suite passes with `COLO_NUM_OBS == 934` and mask `452`. - ---- - -### Task 3: Port the Manticore activation state machine - -**Files:** -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc` -- Test: `ocean/osrs/tests/test_colosseum_modifiers.c` - -**Interfaces:** -- Consumes: `col_npc_has_los_to_player(s, ctx, npc)` and current peer-stagger logic. -- Produces: `ColoManticorePhase` and explicit activation, target-wait, initial-charge, and repeating states. - -- [ ] **Step 1: Add the failing WIP Manticore contract** - -Port `test_manticore_initial_charge_timing_and_target_gate` from `3812fd454`, but route line-of-sight through `ColosseumContext`. Change misleading test text from “visible tick” to “spawn tick”. - -Expected red conditions: the current Manticore arms before the seventh spawn tick, exposes orbs too early, and lacks the phase enum. - -- [ ] **Step 2: Add the explicit phase type** - -```c -typedef enum { - COLO_MANTICORE_PHASE_ACTIVATING = 0, - COLO_MANTICORE_PHASE_WAITING_FOR_TARGET, - COLO_MANTICORE_PHASE_CHARGING, - COLO_MANTICORE_PHASE_REPEATING, -} ColoManticorePhase; - -typedef struct { - ColoManticorePhase phase; - int cycle_step; - AttackStyle orb_style[3]; - AttackStyle fixed_orb_style[3]; -} ColoManticoreState; - -#define COLO_MANTICORE_ACTIVATION_TICKS 7 -#define COLO_MANTICORE_CHARGE_TICKS 10 -#define COLO_MANTICORE_ARM_ANIMATION_TICKS 3 -``` - -Initialize Manticores in `ACTIVATING` with attack timer `7`. - -- [ ] **Step 3: Implement target gating and phase transitions** - -Use: - -```c -static int col_manticore_has_target( - const ColosseumState* s, - const ColosseumContext* ctx, - const ColoNPC* npc, - const ColoNpcStats* stats -) { - int dist = col_npc_dist_to_player(s, npc); - return dist >= 1 && dist <= stats->attack_range && - col_npc_has_los_to_player(s, ctx, npc); -} -``` - -Count activation down independently of line of sight. In `WAITING_FOR_TARGET`, arm only after a valid target appears. In `CHARGING`, consume ten full ticks, then require a valid target before the first shot. Switch to `REPEATING` when firing starts. Adapt peer staggering so only equivalent ready states delay each other. - -- [ ] **Step 4: Hide premature telegraphs and oracle data** - -Make Manticore orbs and next-prayer data invisible during activation, target wait, and the first three charge ticks. Preserve existing one-tick barrage tells after the first shot. - -- [ ] **Step 5: Run and commit** - -```bash -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_manticore -/tmp/colo_manticore -git add ocean/osrs/encounters/colosseum ocean/osrs/tests/test_colosseum_modifiers.c -git commit -m "Model Manticore activation and charge" -``` - -Expected: all Manticore timing, telegraph, shared-cycle, and stagger tests pass. - ---- - -### Task 4: Make the thrall lifecycle automatic - -**Files:** -- Modify: `config/osrs_colosseum.ini` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_lab_json.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc` -- Test: `ocean/osrs/tests/test_colosseum_modifiers.c` - -**Interfaces:** -- Consumes: `tick_scratch.player_attacked`, `player_attack_npc_idx`, current Magic, loadout profile, and pending-hit queues. -- Produces: automatic thrall state and final `933`/`451` model contract. - -- [ ] **Step 1: Add the failing automatic-thrall test** - -Port the final `test_thrall_regression` from `3812fd454`. Assert reset lifetime, attack-triggered target acquisition, twelve-tick aggro, four-tick attacks, expiry resummon, profile multiplier, and Sol immunity. - -Expected: compile failure on `thrall_aggro_ticks_left` and `thrall_lifetime_total`. - -- [ ] **Step 2: Replace representational state** - -Remove `thrall_active` and `thrall_recast_cd`. Add: - -```c -int thrall_target_slot; -int thrall_aggro_ticks_left; -int thrall_lifetime_left; -int thrall_lifetime_total; -int thrall_attack_timer; -``` - -Define `COLO_THRALL_AGGRO_TICKS 12`. Compute lifetime from current Magic, doubled only for `COLO_LOADOUT_PROFILE_SPEEDRUN`. - -- [ ] **Step 3: Implement automatic reset, targeting, and expiry** - -`col_resummon_thrall` sets no target, zero aggro, full derived lifetime, and a four-tick attack timer. Call it at episode reset and expiry. Acquire a target only when `tick_scratch.player_attacked` names a live non-Sol NPC. Decrement aggro only on ticks without a new player attack. Clear invalid or expired targets. - -- [ ] **Step 4: Remove the summon action and rewrite observations** - -Set `COLO_SPELL_DIM` to `2`, keep only `COLO_SPELL_DEATH_CHARGE = 1`, delete the summon mask branch, and reduce `COLO_THRALL_DC_OBS_SIZE` from `6` to `5`. Observe normalized lifetime, normalized aggro, Death Charge active, window, and cooldown. Render the implicit thrall unconditionally. - -Set `action_mask_size = 451`. Assert `COLO_NUM_OBS == 933` and `COLO_ACTION_MASK_SIZE == 451`. - -- [ ] **Step 5: Run and commit** - -```bash -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_thrall -/tmp/colo_thrall -git add config/osrs_colosseum.ini ocean/osrs/encounters/colosseum ocean/osrs/tests/test_colosseum_modifiers.c -git commit -m "Make Colosseum thralls automatic" -``` - -Expected: thrall and Death Charge contracts pass at `933`/`451`. - ---- - -### Task 5: Move Volatility to corpse removal - -**Files:** -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc` -- Test: `ocean/osrs/tests/test_colosseum_modifiers.c` - -**Interfaces:** -- Consumes: `ColosseumContext.route_topology` and NPC death linger. -- Produces: context-owned Volatility render events and corpse-removal damage. - -- [ ] **Step 1: Add the failing corpse-removal contract** - -Port `test_volatility_explodes_on_corpse_removal` from `3812fd454`. Assert no lethal-hit explosion, no explosion during linger, damage on removal, and one family-specific spot animation. - -Expected: current code damages on the lethal hit and emits no removal event. - -- [ ] **Step 2: Add context-owned event storage** - -```c -int volatility_explosion_count; -int volatility_explosion_x[COLO_MAX_NPCS]; -int volatility_explosion_y[COLO_MAX_NPCS]; -ColoNpcType volatility_explosion_type[COLO_MAX_NPCS]; -``` - -Clear the count in `col_context_clear_render_events`. - -- [ ] **Step 3: Move the explosion trigger** - -Remove `col_mod_volatility_on_death` from `col_apply_npc_death`. Change death-linger ticking to accept `ColosseumContext*`. Immediately before deactivating a corpse, call `col_mod_volatility_on_corpse_removed` with the shared finalized topology and queue its render event. - -- [ ] **Step 4: Emit family-specific spot animations** - -Map human, Manticore, Colossus, Minotaur, and Sol families to GFX IDs `2713`, `2721`, `2722`, `2723`, and `2724`. Emit a stationary overlay projectile at the corpse origin during `col_render_post_tick_ctx`. - -- [ ] **Step 5: Run and commit** - -```bash -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_modifiers.c -lm -o /tmp/colo_volatility -/tmp/colo_volatility -git add ocean/osrs/encounters/colosseum ocean/osrs/tests/test_colosseum_modifiers.c -git commit -m "Explode Volatility on corpse removal" -``` - -Expected: modifiers suite passes and the new render event appears exactly once. - ---- - -### Task 6: Finalize snapshot and deterministic contracts - -**Files:** -- Modify: `ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc` -- Modify: `ocean/osrs/tests/test_colosseum_golden.c` -- Modify: `ocean/osrs/tests/probe_colo_sim_invariant.c` -- Modify only if required: focused Colosseum forecast and topology tests - -**Interfaces:** -- Consumes: final state layout and `933`/`451` interface. -- Produces: snapshot version `25` and explained deterministic baselines. - -- [ ] **Step 1: Bump and test snapshot version** - -Set: - -```c -#define COLO_SNAPSHOT_VERSION 25u -``` - -Keep stale-version rejection and round-trip tests. Expected: version 24 abort fixture rejects and version 25 round trip passes. - -- [ ] **Step 2: Run deterministic probes before changing baselines** - -```bash -clang -std=c11 -O2 -I. ocean/osrs/tests/test_colosseum_golden.c -lm -o /tmp/colo_golden_new -/tmp/colo_golden_new -clang -std=c11 -O2 -I. ocean/osrs/tests/probe_colo_sim_invariant.c -lm -o /tmp/colo_sim_new -/tmp/colo_sim_new -``` - -Expected: digest mismatches caused by laser observation width, Manticore timing, thrall state, and Volatility timing. - -- [ ] **Step 3: Update only observed final digests** - -Keep `FNV_OFFSET` unchanged at `1469598103934665603ULL`. Replace the 12 golden and 12 sim-invariant expected values with the values printed by the final candidate. Add a short reason beside the baseline arrays only where an existing baseline explanation already exists. - -- [ ] **Step 4: Re-run exact deterministic suites** - -Expected: `12/12 configs match baseline` in both binaries. - -- [ ] **Step 5: Commit** - -```bash -git add ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc ocean/osrs/tests/test_colosseum_golden.c ocean/osrs/tests/probe_colo_sim_invariant.c -git commit -m "Finalize consolidated Colosseum contract" -``` - ---- - -### Task 7: Verify run38 and integrate - -**Files:** -- No source changes unless a real verification failure identifies a defect. - -**Interfaces:** -- Consumes: final consolidation branch. -- Produces: verified canonical `valtteri/colo-5c-port` and no temporary worktree. - -- [ ] **Step 1: Run focused shared and encounter suites** - -Compile and run: - -```text -ocean/osrs/tests/test_colosseum_modifiers.c -ocean/osrs/tests/test_colosseum_golden.c -ocean/osrs/tests/probe_colo_sim_invariant.c -ocean/osrs/tests/test_colosseum_forecast_exact.c -ocean/osrs/tests/test_encounter_route_topology_setup.c -ocean/osrs/tests/test_osrs_inventory_actions.c -ocean/osrs/tests/test_osrs_inventory_clicks.c -ocean/osrs/tests/test_osrs_item_effect_masks.c -ocean/osrs/tests/test_inferno_attack_styles.c -ocean/osrs/tests/test_inferno_lab.c -``` - -Expected: every suite passes with no unexplained changed digest. - -- [ ] **Step 2: Run worker-count determinism** - -```bash -clang -std=c11 -O2 -I. -Xpreprocessor -fopenmp \ - -I/opt/homebrew/opt/libomp/include \ - ocean/osrs/tests/probe_env_thread_determinism.c \ - -L/opt/homebrew/opt/libomp/lib -lomp -lm \ - -o /tmp/colo_worker_determinism -/tmp/colo_worker_determinism -``` - -Expected: every one-worker versus multi-worker comparison reports `IDENTICAL`. - -- [ ] **Step 3: Build and smoke the local viewer** - -```bash -./build.sh osrs_colosseum --local -./osrs_colosseum \ - --model checkpoints/colo-w11-protein-best/run38_378535936.bin \ - --start-wave 11 \ - --hidden-size 512 \ - --num-layers 2 -``` - -Expected: model loads, viewer reports observations `933` and mask `451`, and a wave-11 episode advances without aborting. - -- [ ] **Step 4: Commit any verification-driven fix separately** - -Only if a reproduced failure required a source correction. Re-run the exact failed check after the fix. - -- [ ] **Step 5: Fast-forward canonical and remove isolation** - -From the canonical checkout: - -```bash -pwd -git merge --ff-only valtteri/colo-consolidation -git worktree remove /private/tmp/puffer-colo-consolidation -git branch -d valtteri/colo-consolidation -``` - -Expected: `valtteri/colo-5c-port` points at the verified consolidation tip, the temporary path is gone, and existing untracked local artifacts remain untouched. diff --git a/docs/superpowers/specs/2026-08-11-osrs-global-runtime-optimization-design.md b/docs/superpowers/specs/2026-08-11-osrs-global-runtime-optimization-design.md deleted file mode 100644 index dbfe14be95..0000000000 --- a/docs/superpowers/specs/2026-08-11-osrs-global-runtime-optimization-design.md +++ /dev/null @@ -1,265 +0,0 @@ -# OSRS Global Runtime Optimization Design - -Date: 2026-08-11 - -## Goal - -Make every OSRS encounter in the current integration branch faster and simpler without changing config files, observation layouts, action layouts, combat rules, rewards, or terminal rules. Equal-cost routes may choose different legal tiles. Goldens may change only when an intentional route tie or RNG-order change explains the diff. - -Finish the current Inferno route pass first. Optimize the shared SDK and the integrated Inferno, Colosseum, Zulrah, and NH PvP encounters next. Merge Muspah last and adapt both Muspah wrappers to the final shared APIs. - -## Current evidence - -Fresh one-million-step single-thread profiles on the M4 Pro give these baselines: - -| Encounter | Steps per second | Dominant costs | -|---|---:|---| -| Inferno | 756,048 | Attack-route field 25%, observation write 15%, mask write 11% | -| Colosseum | 444,261 | NPC pathfinding 33%, observation write 26%, mask write 10% | -| Zulrah | 344,646 | Pathfinding 62% combined, observation write 19% | -| NH PvP | 531,411 | Pathfinding 45%, loadout resolution 21%, masks 20% | - -The completed Inferno route work raised paired mean throughput from 447,050 to 610,390 SPS in the first pass, then from 677,657 to 752,251 SPS in the second pass. The combined estimate is about 68%. Shared route construction remains the largest proven source of gain. - -## Scope - -### In scope - -- Shared collision, LOS, pathfinding, interaction, inventory, observation, and mask primitives under `ocean/osrs/` -- Integrated Inferno and Colosseum native wrappers -- Integrated Zulrah and NH PvP encounter paths -- Encounter-local work justified by a fresh profile after the shared cutover -- Muspah and Zulrah native wrapper integration after the shared substrate is stable -- Focused regression tests, mechanics goldens, builds, viewer smoke checks, and paired benchmarks - -### Out of scope - -- Config changes -- Reward tuning -- Curriculum tuning -- Policy hyperparameter tuning -- Puffer trainer internals -- A unified ECS or SoA rewrite -- Observation or action width changes -- Speculative caches without measured reuse - -## Architecture - -Encounter code keeps policy. The SDK owns execution mechanisms. - -```text -Encounter policy - waves, spawns, NPC decisions, rewards - | - v -Shared tick mechanisms - player actions, interactions, combat, inventory - | - +--> immutable arena topology - | step masks, footprint masks, LOS bitsets - | thread-local route scratch - | - +--> canonical item metadata - click semantics, gear slot, observation code - direct byte-mask output -``` - -The cutover uses one shared mechanism for each concern. Old mechanisms are deleted after every caller migrates. - -## Arena topology - -Add one process-shared immutable arena topology per encounter. Build it during encounter setup before worker threads start. Each encounter context holds a `const` pointer to that topology. Do not hide it behind lazy global lookup. - -The topology contains: - -- Arena origin, width, and height -- Static blocked tiles -- Static footprint-blocked masks for supported NPC sizes -- One eight-bit legal-step mask per tile -- Tile-pair LOS bitsets -- A topology revision used by derived route caches - -A maximum dimension of 64 covers NH PvP's 61 by 28 arena and every current encounter. Invalid dimensions abort during setup. - -Colosseum's private static blocked, footprint, and LOS tables become the reference implementation, then move into the SDK. Inferno, Zulrah, and NH PvP use the same representation. The encounter supplies static geometry. The SDK builds and queries the tables. - -Static topology uses one process-wide allocation per encounter and becomes immutable after setup. Per-env dynamic occupancy remains in encounter state. - -## Route solver - -Replace `pathfind_step` and `pathfind_step_arena` with one bounded route solver. - -The solver uses one thread-local scratch workspace per worker: - -- `uint16_t` generation stamps -- Packed `uint16_t` tile queue entries -- One-byte route directions -- Route depth required by fallback and shortest-path checks - -The solver never allocates during a tick. It never clears a 104 by 104 stack matrix. Generation wrap clears the stamp array once and resumes at generation one. - -The topology's legal-step mask replaces repeated collision-map traversal for static geometry. Dynamic blockers remain an explicit callback. Diagonal movement checks both cardinal edges once. The current duplicate diagonal collision calls are removed. - -The result is a tagged outcome: - -- Reached source -- Route found -- Fallback selected -- Unreachable - -A successful result carries the selected destination, first step, and optional run step. This prevents contradictory `found`, destination, and delta combinations. - -One route solve may provide both walking steps when the dynamic blocker revision stays unchanged. Otherwise the second step runs a new query. Equal-cost route tie changes are allowed. Every returned step must remain legal and shortest for the selected destination. - -## Route cache lifetime - -Route caches are derived state, not source state. - -A cache key contains: - -- Topology revision -- Dynamic blocker revision -- Source -- Target or target rectangle -- Actor footprint -- Route mode - -A cache hit requires every key field to match. Encounter code increments the blocker revision when relevant occupancy changes. A reset or snapshot restore invalidates every derived route cache. Snapshots do not serialize route scratch or route caches. - -If an encounter cannot expose a sound blocker revision, it does not use the cache. There is no heuristic reuse. - -## Inventory representation - -Replace repeated classification of parallel inventory fields with canonical content metadata. - -Each cell stores a `uint8_t` canonical content code and a `uint16_t` raw OSRS ID. Generated metadata indexed by content code provides: - -- Item index -- Consumable kind -- Dose -- Click action -- Gear slot -- Observation code -- Static affordance features - -Constructors and mutation functions create valid cells. Callers do not write item, dose, or click fields independently. This removes illegal combinations and repeated `osrs_consumable_click_registry_index` switches. - -The generated item observation table remains the source for static features. Dynamic values such as equipped state, current healing fraction, cooldown legality, and special energy remain dynamic. - -Every inventory constructor, click mutation, drink transition, swap, snapshot, restore, renderer, observation writer, mask writer, and test migrates in the same cutover. Old fields and classifiers are deleted. - -## Observation and mask output - -Observation widths and indices do not change. - -Observation writers use the canonical cell code and generated metadata directly. They do not rebuild a full affordance record to recover one code or one heal value. - -Change `EncounterDef.write_mask` to write bytes. Native wrappers pass their action-mask buffer directly. The viewer and diagnostics consume byte masks. Delete temporary float masks and float-to-byte conversion loops. - -Observation and mask output remain pure functions of encounter state and context. Output caching is allowed only for static table rows. Dynamic state is always written from the current tick. - -## Encounter-local work - -Reprofile after the shared cutover. Keep local work narrow. - -### Inferno - -- Reassess attack-route field construction -- Move static LOS to shared topology -- Remove redundant observation-slot refreshes only when state revisions prove reuse - -### Colosseum - -- Replace private topology tables with the shared topology -- Reassess occupancy-aware NPC routing -- Share route fields only when blocker revisions prove identical inputs -- Reassess Venator candidate selection - -### Zulrah - -- Remove full-grid path clearing through the shared solver -- Reassess cloud destination selection -- Keep phase and hazard policy encounter-owned - -### NH PvP - -- Resolve each selected loadout once per agent tick -- Pass the resolved value through masks, movement, switches, and combat -- Keep opponent policy and combat decisions encounter-owned - -### Muspah - -Merge after the APIs above stop moving. Adapt `osrs_muspah`, `osrs_muspah_native`, and the Zulrah native wrapper during the merge. Do not import duplicate pathfinding, collision, inventory, or mask stacks. - -## Failure handling - -Abort on defects: - -- Invalid arena dimensions -- Topology build failure -- Queue overflow -- Invalid item content code -- Invalid generated metadata -- Unknown config keys -- Impossible tagged states - -Return route failure as data. Missing routes and unreachable targets are expected outcomes. - -Do not add silent caps, timeout fallbacks, lazy concurrent initialization, or stale-cache recovery. - -## Verification - -### Route properties - -Run exhaustive static checks for each arena and randomized dynamic-blocker checks: - -- Every step is adjacent and legal -- Every diagonal satisfies both cardinal edge constraints -- A found route reaches its selected destination -- The optimized solver and the test-only reference BFS agree on reachability and shortest distance when given the same blockers -- Fallback selection minimizes the defined distance and route-cost ordering -- Cached and uncached queries produce equally valid outcomes -- A run step equals two valid sequential walk steps -- Generation wrap preserves results - -Keep a simple reference BFS in tests only. - -### Data contracts - -- Every inventory content code round-trips -- Generated metadata matches existing click, dose, gear-slot, and observation semantics -- Drink transitions select the correct next dose -- Observation widths, indices, and values stay unchanged -- Mask widths, indices, and values stay unchanged -- Reset, snapshot, restore, reward, terminal, and combat traces remain deterministic - -### Encounter gates - -Run the shared player, interaction, inventory, collision, and route suites. Run focused Inferno, Colosseum, Zulrah, NH PvP, and later Muspah suites. Build every affected native wrapper. Run each encounter through the viewer and profiler. - -Reseed a semantic golden only when an intentional equal-cost route or RNG-order change explains every diff. Unrelated golden changes fail the gate. - -### Performance gates - -Use paired runs with identical binaries, inputs, machine state, and profile action streams. - -- No encounter may regress beyond measured noise -- Each patch must improve its target median -- The aggregate geometric-mean SPS must rise -- Sampling must show the intended hotspot fell -- Final validation must include actual vectorized training throughput - -Continue while a measured hotspot supports a simpler change with material gain. Reject caches whose lifecycle costs more complexity than their measured gain. - -## Clean cutover order - -1. Freeze reference binaries and benchmark inputs -2. Add route property tests and a test-only reference BFS -3. Add shared topology and route scratch -4. Migrate every current route caller and delete old pathfinders -5. Migrate inventory representation and generated metadata -6. Migrate byte mask output and delete adapters -7. Reprofile all current encounters -8. Apply measured encounter-local changes -9. Run full mechanics, build, viewer, and performance gates -10. Merge Muspah and adapt its wrappers to the final APIs diff --git a/docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md b/docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md deleted file mode 100644 index e08c79d0c4..0000000000 --- a/docs/superpowers/specs/2026-08-12-colosseum-consolidation-design.md +++ /dev/null @@ -1,99 +0,0 @@ -# Colosseum Consolidation Design - -## Goal - -Make `valtteri/colo-5c-port` the single source of truth for the latest Colosseum environment and the shared OSRS runtime work. The consolidated environment must load `checkpoints/colo-w11-protein-best/run38_378535936.bin` with its `933`-observation and `451`-mask contract. - -## Source Lines - -The current 5c port copied the Colosseum tree from `0fc4b50c07cc4ec1435613fe4396b2b77ea8b647`. The missing committed behavior is the net delta through `8281518ea1cd238a59500aebb81937cc607588a0`. The missing uncommitted behavior was preserved in `3812fd454880a4605b2a9b39af41c72514e43a95`. - -Port behavior, not branch structure. The current 5c architecture remains authoritative. - -## Included Behavior - -### Sol laser observations - -Restore the honest multi-crystal laser observation pack and its episode diagnostics. Preserve `laser_obs_mode` because run38 records it and requires the enabled mode. The pack exposes the shared cooldown and per-crystal active, line-delta, and freeze state. - -### Asset archive provenance - -Pin the `osrs-assets-v22` release archive to `2f908da5b5ddf148c0cbef48ad6334c5253c04c6cd83782340bc2f1c5dffc7ad`. A fresh setup download produced this SHA. The prior manifest SHA rejects the published archive and prevents clean worktree builds. - -### Manticore lifecycle - -Represent the first attack separately from the repeating barrage: - -1. Activate for seven ticks after spawn. -2. Wait until the player is in range and line of sight. -3. Start a ten-tick charge without firing. -4. Keep the three orbs hidden for the first three charge ticks. -5. Fire the first orb after the full charge. -6. Continue with the existing one-tick orb sequence, seven-tick lull, and two-Manticore stagger rules. - -Use the shared finalized arena topology for line of sight. - -### Thrall lifecycle - -Remove the summon action. A thrall exists from reset and resummons automatically on expiry. Its lifetime equals the player's current Magic level in the beginner profile and twice that level in the speedrun profile. It acquires a target only after an executed player attack, retains that target for twelve ticks, attacks every four ticks, and never targets Sol Heredit. - -### Volatility lifecycle - -Do not explode on the lethal hit. Explode when the corpse leaves the simulation after its death linger. Queue one render event in `ColosseumContext` and emit the family-specific spot animation from the render bridge. Use the shared arena topology for blast geometry. - -## Interface Contract - -The canonical model contract is: - -- observations: `933` -- action mask: `451` -- spell head: none or Death Charge -- snapshot version: `25` - -Older `934`/`452` checkpoints are intentionally incompatible. No compatibility slots, aliases, or dual model path remain. - -## Preserved Current Architecture - -Keep these current 5c systems unchanged except where their public call shape must carry the new behavior: - -- immutable `EncounterArenaTopology` -- shared route engine -- canonical `uint16_t` inventory content codes -- shared generated item metadata -- current encounter context ownership -- current Inferno, Zulrah, and NH PvP migrations - -The WIP's old per-Colosseum item table, item registry accesses, and source-layout edits do not return. - -## Excluded Changes - -Do not port: - -- the unrelated Puffer source-layout and Python migration -- `.cu` to `.inc` comment and include edits -- deleted or replaced per-Colosseum item metadata generators -- stale golden or sim-invariant hashes -- the WIP test's accidental nonstandard FNV seed -- canceled next-style ungating and pending-style schedule experiments - -## Verification - -Add the WIP behavior tests before implementation and confirm they fail against current 5c. Then require: - -- Colosseum mechanics suite -- Colosseum golden suite -- Colosseum simulation-invariant suite -- Colosseum forecast and topology suites -- Colosseum snapshot round trip and stale-version rejection -- worker-count determinism -- shared inventory action, click, and item-effect suites -- focused Inferno mechanics and lab suites -- local Colosseum viewer build -- run38 model load with hidden size 512 and two layers -- a wave-11 viewer smoke run reporting the `933`/`451` contract - -Update deterministic baselines only after the new behavior explains every changed digest. - -## Integration - -Implement on a temporary branch in `/private/tmp`. Commit the focused consolidation, fast-forward `valtteri/colo-5c-port` after verification, then remove the temporary worktree and branch. Leave local checkpoints, binaries, screenshots, and other untracked artifacts untouched. From 95bd5169f668ff4387c9ba72c30913e97c973780 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Thu, 13 Aug 2026 13:23:44 +0300 Subject: [PATCH 39/50] optimize and validate OSRS environments --- .clangd | 46 + config/osrs_colosseum.ini | 2 +- config/osrs_inferno.ini | 2 +- config/osrs_pvp.ini | 46 + config/osrs_zulrah.ini | 193 ++++ .../colosseum/encounter_colosseum_helpers.inc | 11 +- .../encounter_colosseum_mask_render.inc | 159 ++-- .../colosseum/encounter_colosseum_model.inc | 21 +- .../encounter_colosseum_modifiers.inc | 22 +- .../encounter_colosseum_movement.inc | 136 ++- .../encounter_colosseum_obs_mask.inc | 288 +++--- .../encounter_colosseum_player_actions.inc | 81 +- .../encounter_colosseum_render_snapshot.inc | 11 +- .../encounter_colosseum_reset_spawn.inc | 9 +- .../encounter_colosseum_reward_step.inc | 2 +- ocean/osrs/encounters/encounter_colosseum.h | 1 + ocean/osrs/encounters/encounter_inferno.h | 1 + ocean/osrs/encounters/encounter_nh_pvp.h | 172 +++- ocean/osrs/encounters/encounter_zulrah.h | 716 ++++++++------- .../inferno/encounter_inferno_helpers.inc | 141 ++- .../inferno/encounter_inferno_model.inc | 5 +- .../inferno/encounter_inferno_obs_mask.inc | 474 +++++----- .../encounter_inferno_player_actions.inc | 145 +-- .../encounter_inferno_render_snapshot.inc | 45 +- .../inferno/encounter_inferno_reset_spawn.inc | 9 +- ocean/osrs/osrs_collision.h | 25 +- ocean/osrs/osrs_encounter.h | 59 +- ocean/osrs/osrs_encounter_player.h | 20 +- ocean/osrs/osrs_entity_encoder.cu | 786 +++++++++++++++++ ocean/osrs/osrs_gui.h | 120 ++- ocean/osrs/osrs_human_input.h | 141 +-- ocean/osrs/osrs_inventory.h | 92 +- ocean/osrs/osrs_inventory_actions.h | 119 ++- ocean/osrs/osrs_inventory_clicks.h | 102 +-- ocean/osrs/osrs_item_obs_generated.h | 407 +++++---- ocean/osrs/osrs_items.h | 1 + ocean/osrs/osrs_items_generated.h | 4 +- ocean/osrs/osrs_pathfinding.h | 512 ++++++++--- ocean/osrs/osrs_policy.h | 221 +++++ ocean/osrs/osrs_pvp_actions.h | 340 ++++--- ocean/osrs/osrs_pvp_api.h | 124 +-- ocean/osrs/osrs_pvp_gear.h | 286 +++--- ocean/osrs/osrs_pvp_movement.h | 129 ++- ocean/osrs/osrs_pvp_observations.h | 827 ++++++------------ ocean/osrs/osrs_pvp_opponents.h | 610 +++++++------ ocean/osrs/osrs_render.h | 4 +- ocean/osrs/osrs_types.h | 91 +- ocean/osrs/osrs_venator.h | 10 +- ocean/osrs/osrs_visual.c | 464 ++++++---- ocean/osrs/osrs_visual_net.h | 292 +++---- ocean/osrs/tests/probe_colo_best_gear_dpt.c | 8 +- ocean/osrs/tests/probe_colo_sim_invariant.c | 34 +- ocean/osrs/tests/probe_colo_wave_advance.c | 4 +- .../osrs/tests/probe_colosseum_winnability.c | 4 +- ocean/osrs/tests/test_colosseum_golden.c | 28 +- ocean/osrs/tests/test_colosseum_modifiers.c | 457 +++++----- .../test_encounter_route_topology_setup.c | 4 + ocean/osrs/tests/test_inferno_attack_styles.c | 335 ++++--- ocean/osrs/tests/test_inferno_golden.c | 89 +- ocean/osrs/tests/test_osrs_inventory_clicks.c | 130 ++- ocean/osrs/tests/test_osrs_player_step.c | 254 +++++- ocean/osrs/tests/test_osrs_pvp_pending_hits.c | 275 +++++- ocean/osrs/tests/test_zulrah_golden.c | 52 +- ocean/osrs/tests/test_zulrah_hit_delay.c | 66 ++ ocean/osrs/tools/gen_osrs_item_obs_table.c | 8 +- ocean/osrs_colosseum/osrs_colosseum.cu | 550 +----------- ocean/osrs_colosseum/osrs_colosseum.h | 6 +- ocean/osrs_inferno/osrs_inferno.cu | 284 +----- ocean/osrs_inferno/osrs_inferno.h | 7 +- ocean/osrs_pvp/osrs_pvp.c | 3 + ocean/osrs_pvp/osrs_pvp.h | 220 +++++ ocean/osrs_zulrah/osrs_zulrah.c | 3 + ocean/osrs_zulrah/osrs_zulrah.h | 202 +++++ src/ocean.cu | 9 + tests/test_osrs_entity_encoders.py | 442 ++++++---- tests/test_osrs_entity_encoders_cuda.cu | 89 +- 76 files changed, 7234 insertions(+), 4853 deletions(-) create mode 100644 .clangd create mode 100644 config/osrs_pvp.ini create mode 100644 config/osrs_zulrah.ini create mode 100644 ocean/osrs/osrs_entity_encoder.cu create mode 100644 ocean/osrs/osrs_policy.h create mode 100644 ocean/osrs_pvp/osrs_pvp.c create mode 100644 ocean/osrs_pvp/osrs_pvp.h create mode 100644 ocean/osrs_zulrah/osrs_zulrah.c create mode 100644 ocean/osrs_zulrah/osrs_zulrah.h diff --git a/.clangd b/.clangd new file mode 100644 index 0000000000..86fca3bcc6 --- /dev/null +++ b/.clangd @@ -0,0 +1,46 @@ +Diagnostics: + UnusedIncludes: None +--- +If: + PathMatch: ocean/osrs_colosseum/.* +CompileFlags: + Add: + - -xc + - -I../../src + - -I../../vendor + - -I../../raylib-5.5_macos/include + - -I../../raylib-5.5_linux_amd64/include + - -I../../raylib-5.5_webassembly/include + - -Wno-pragma-once-outside-header +--- +If: + PathMatch: src/.* +CompileFlags: + Add: + - -xc + - -I../raylib-5.5_macos/include + - -I../raylib-5.5_linux_amd64/include + - -I../raylib-5.5_webassembly/include + - -Wno-pragma-once-outside-header +--- +If: + PathMatch: .*\.cu +CompileFlags: + Remove: + - -xc + Add: + - -xcuda + - -nocudainc + - -nocudalib + - --cuda-host-only + - -ferror-limit=0 +Diagnostics: + Suppress: '*' +--- +If: + PathMatch: .*\.inc +CompileFlags: + Add: + - -ferror-limit=0 +Diagnostics: + Suppress: '*' diff --git a/config/osrs_colosseum.ini b/config/osrs_colosseum.ini index b9b8481238..1d2a844a92 100644 --- a/config/osrs_colosseum.ini +++ b/config/osrs_colosseum.ini @@ -44,7 +44,7 @@ curriculum_frac_5 = 0.0 total_agents = 8192 num_buffers = 8 num_threads = 32 -action_mask_size = 451 +action_mask_size = 456 gpu_env = 0 [policy] diff --git a/config/osrs_inferno.ini b/config/osrs_inferno.ini index daf2309530..75ee61bf04 100644 --- a/config/osrs_inferno.ini +++ b/config/osrs_inferno.ini @@ -73,7 +73,7 @@ jad_alive_zuk_multiplier = 0.1573513 total_agents = 4096 num_buffers = 2 num_threads = 16 -action_mask_size = 434 +action_mask_size = 436 gpu_env = 0 [policy] diff --git a/config/osrs_pvp.ini b/config/osrs_pvp.ini new file mode 100644 index 0000000000..f15d401ee2 --- /dev/null +++ b/config/osrs_pvp.ini @@ -0,0 +1,46 @@ +[base] +env_name = osrs_pvp +async = 0 +score_metric = score +eval_episodes = 4096 + +[env] +opponent_type = 8 +gear_tier = 3 +is_lms = 1 +use_c_opponent = 1 +auto_reset = 0 +shaping_enabled = 1 +shaping_scale = 1.0 + +[vec] +total_agents = 4096 +num_buffers = 2 +num_threads = 16 +action_mask_size = 423 +gpu_env = 0 + +[policy] +hidden_size = 128 +num_layers = 2 +expansion_factor = 1 + +[train] +total_timesteps = 60000000 +horizon = 64 +min_lr_ratio = 0.0 +learning_rate = 0.0005 +momentum = 0.95 +ent_coef = 0.001 +gamma = 0.995 +gae_lambda = 0.90 +vtrace_rho_clip = 1.0 +vtrace_c_clip = 1.0 +prio_alpha = 0.0 +prio_beta0 = 0.0 +clip_coef = 0.20 +vf_coef = 0.5 +vf_clip_coef = 0.20 +max_grad_norm = 0.5 +replay_ratio = 1.0 +minibatch_size = 8192 diff --git a/config/osrs_zulrah.ini b/config/osrs_zulrah.ini new file mode 100644 index 0000000000..4ee97f1e99 --- /dev/null +++ b/config/osrs_zulrah.ini @@ -0,0 +1,193 @@ +[base] +env_name = osrs_zulrah +async = 0 + +[env] +gear_tier = 0 +gear_tier_mode = 2 +episode_mode = 1 +gear_tier_weight_0 = 0.5 +gear_tier_weight_1 = 0.3 +gear_tier_weight_2 = 0.2 +reward_win = 0.424107641 +reward_loss_penalty = 0.236695677 +reward_damage_dealt = 0.0957096219 +reward_correct_style = 0.8 +reward_damage_received_penalty = 0.19149816 +reward_cloud_occupancy_penalty = 0.218443304 + +[vec] +total_agents = 4096 +num_buffers = 2 +num_threads = 16 +action_mask_size = 427 +gpu_env = 0 + +[policy] +hidden_size = 512 +num_layers = 2 +expansion_factor = 1 + +[train] +total_timesteps = 120000000 +horizon = 32 +min_lr_ratio = 0.0 +learning_rate = 0.00156910333 +momentum = 0.977381706 +ent_coef = 8.08037657e-06 +gamma = 0.995960951 +gae_lambda = 0.880164325 +vtrace_rho_clip = 1.16428173 +vtrace_c_clip = 0.8 +prio_alpha = 0.880423367 +prio_beta0 = 0.575937748 +clip_coef = 0.315547526 +vf_coef = 1.46088982 +vf_clip_coef = 0.3 +max_grad_norm = 1.49210274 +replay_ratio = 4.0 +minibatch_size = 4096 + +[sweep] +max_suggestion_cost = 420 +max_runs = 400 +gpus = 1 +early_stop_quantile = 0.3 + +[sweep.train.total_timesteps] +distribution = log_normal +min = 3e7 +max = 2e8 +scale = time + +[sweep.train.horizon] +distribution = uniform_pow2 +min = 16 +max = 128 +scale = auto + +[sweep.train.minibatch_size] +distribution = uniform_pow2 +min = 4096 +max = 16384 +scale = auto + +[sweep.train.learning_rate] +distribution = log_normal +min = 0.001 +max = 0.02 +scale = 0.5 + +[sweep.train.ent_coef] +distribution = log_normal +min = 0.00000001 +max = 0.0003 +scale = auto + +[sweep.train.gamma] +distribution = logit_normal +min = 0.995 +max = 0.99997 +scale = auto + +[sweep.train.gae_lambda] +distribution = logit_normal +min = 0.2 +max = 0.95 +scale = auto + +[sweep.train.replay_ratio] +distribution = uniform +min = 0.5 +max = 4.0 +scale = auto + +[sweep.train.clip_coef] +distribution = uniform +min = 0.05 +max = 0.6 +scale = auto + +[sweep.train.vf_clip_coef] +distribution = uniform +min = 0.005 +max = 0.3 +scale = auto + +[sweep.train.vf_coef] +distribution = uniform +min = 0.3 +max = 2.5 +scale = auto + +[sweep.train.max_grad_norm] +distribution = uniform +min = 0.02 +max = 1.5 +scale = auto + +[sweep.train.momentum] +distribution = logit_normal +min = 0.85 +max = 0.99 +scale = auto + +[sweep.train.prio_alpha] +distribution = uniform +min = 0.0 +max = 0.9 +scale = auto + +[sweep.train.prio_beta0] +distribution = uniform +min = 0.3 +max = 1.0 +scale = auto + +[sweep.train.vtrace_rho_clip] +distribution = uniform +min = 0.4 +max = 1.6 +scale = auto + +[sweep.train.vtrace_c_clip] +distribution = uniform +min = 0.8 +max = 3.0 +scale = auto + +[sweep.env.reward_win] +distribution = uniform +min = 0.1 +max = 1.0 +scale = auto + +[sweep.env.reward_damage_dealt] +distribution = log_normal +min = 0.02 +max = 0.8 +scale = auto + +[sweep.env.reward_correct_style] +distribution = uniform +min = 0.1 +max = 0.8 +scale = auto + +[sweep.env.reward_damage_received_penalty] +distribution = uniform +min = 0.0 +max = 0.4 +scale = auto + +[sweep.env.reward_cloud_occupancy_penalty] +distribution = uniform +min = 0.0 +max = 0.4 +scale = auto + +[sweep.env.reward_loss_penalty] +distribution = uniform +min = 0.0 +max = 0.8 +scale = auto diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc index 13dbf07094..c876b6b097 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc @@ -45,16 +45,15 @@ static int col_thrall_lifetime_ticks(const ColosseumState* s) { static void col_resummon_thrall(ColosseumState* s) { int lifetime = col_thrall_lifetime_ticks(s); - if (lifetime <= 0) { - fprintf(stderr, "invalid thrall lifetime %d at Magic level %d\n", - lifetime, s->player.current_magic); - abort(); - } s->thrall_target_slot = -1; s->thrall_aggro_ticks_left = 0; + s->thrall_attack_timer = COLO_THRALL_TICK; + if (lifetime <= 0) { + s->thrall_lifetime_left = 0; + return; + } s->thrall_lifetime_left = lifetime; s->thrall_lifetime_total = lifetime; - s->thrall_attack_timer = COLO_THRALL_TICK; } static void col_abort_invalid_weapon_set(ColoWeaponSet set) { diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc index 3432b3cd1a..5c882c70d7 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc @@ -9,94 +9,131 @@ static void col_write_mask_ctx( ColosseumContext* ctx = col_require_context(context); encounter_arena_topology_require_finalized(ctx->route_topology); col_refresh_current_obs_slots_ctx(s, ctx); - int offset = 0; - int frozen = s->modifiers.draft_pending && !s->modifiers.draft_free_movement; - for (int primary_action = 0; - primary_action < COLO_PRIMARY_DIM; - primary_action++) { - mask[offset++] = - col_primary_action_valid(s, ctx, primary_action) ? 1.0f : 0.0f; + int primary_offset = col_action_head_mask_offset(COLO_HEAD_PRIMARY); + for (int action = 0; action < COLO_PRIMARY_DIM; action++) { + mask[primary_offset + action] = + col_primary_action_valid(s, ctx, action) ? 1.0f : 0.0f; } - mask[offset++] = 1.0f; - mask[offset++] = s->player.prayer != PRAYER_NONE ? 1.0f : 0.0f; - mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + int overhead_offset = col_action_head_mask_offset(COLO_HEAD_PRAYER); + mask[overhead_offset + ENCOUNTER_OVERHEAD_NO_CHANGE] = 1.0f; + mask[overhead_offset + ENCOUNTER_OVERHEAD_OFF] = + s->player.prayer != PRAYER_NONE ? 1.0f : 0.0f; + for (int action = ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; + action < OSRS_OVERHEAD_DIM; + action++) { + mask[overhead_offset + action] = + s->player.current_prayer > 0 ? 1.0f : 0.0f; + } int inventory_heads_enabled = !ctx->config.mask_inventory_heads; int cell_equip_slot[COLO_INVENTORY_DISPLAY_SLOTS]; int cell_can_eat[COLO_INVENTORY_DISPLAY_SLOTS]; int cell_can_drink[COLO_INVENTORY_DISPLAY_SLOTS]; + int inventory_has_empty_cell = + osrs_first_empty_inventory_cell(s->player.inventory_cells, -1) >= 0; for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { cell_equip_slot[cell] = -1; cell_can_eat[cell] = 0; cell_can_drink[cell] = 0; - OsrsInventoryClickResolution r = - osrs_inventory_cell_click_classify(&s->inventory_cells[cell]); - if (r.click_action == OSRS_CLICK_EQUIP) { - if (col_can_equip_from_cell(s, cell)) - cell_equip_slot[cell] = osrs_inventory_cell_metadata( - &s->inventory_cells[cell])->gear_slot; - } else if (r.click_action == OSRS_CLICK_EAT) { - cell_can_eat[cell] = col_can_eat_consumable_kind(s, r.consumable_kind); - } else if (r.click_action == OSRS_CLICK_DRINK) { + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_classify( + &s->player.inventory_cells[cell]); + if (resolution.click_action == OSRS_CLICK_EQUIP) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata( + &s->player.inventory_cells[cell]); + if (osrs_can_equip_metadata( + &s->player, metadata, inventory_has_empty_cell)) { + cell_equip_slot[cell] = metadata->gear_slot; + } + } else if (resolution.click_action == OSRS_CLICK_EAT) { + cell_can_eat[cell] = + col_can_eat_consumable_kind(s, resolution.consumable_kind); + } else if (resolution.click_action == OSRS_CLICK_DRINK) { cell_can_drink[cell] = osrs_inventory_cell_dose_count( - &s->inventory_cells[cell]) > 0 && + &s->player.inventory_cells[cell]) > 0 && s->player.potion_timer == 0 && - col_drink_has_effect(s, r.consumable_kind); + col_drink_has_effect(s, resolution.consumable_kind); } } for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - mask[offset++] = 1.0f; - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) - mask[offset++] = (inventory_heads_enabled && cell_equip_slot[cell] == slot) - ? 1.0f : 0.0f; + int head_offset = + col_action_head_mask_offset(COLO_HEAD_EQUIP_SLOT(slot)); + mask[head_offset] = 1.0f; + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { + mask[head_offset + cell + 1] = + inventory_heads_enabled && cell_equip_slot[cell] == slot + ? 1.0f : 0.0f; + } + } + int eat_offset = col_action_head_mask_offset(COLO_HEAD_EAT); + int drink_offset = col_action_head_mask_offset(COLO_HEAD_DRINK); + mask[eat_offset] = 1.0f; + mask[drink_offset] = 1.0f; + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { + mask[eat_offset + cell + 1] = + inventory_heads_enabled && cell_can_eat[cell] ? 1.0f : 0.0f; + mask[drink_offset + cell + 1] = + inventory_heads_enabled && cell_can_drink[cell] ? 1.0f : 0.0f; } - mask[offset++] = 1.0f; - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) - mask[offset++] = (inventory_heads_enabled && cell_can_eat[cell]) ? 1.0f : 0.0f; - mask[offset++] = 1.0f; - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) - mask[offset++] = (inventory_heads_enabled && cell_can_drink[cell]) ? 1.0f : 0.0f; - mask[offset++] = 1.0f; - int spec_cost = osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]); - mask[offset++] = (spec_cost > 0 && - (s->player.special_energy >= spec_cost || s->player.spec_armed)) - ? 1.0f : 0.0f; - mask[offset++] = s->player.spec_armed ? 1.0f : 0.0f; + int spell_offset = col_action_head_mask_offset(COLO_HEAD_SPELL); + mask[spell_offset + OSRS_SPELL_NONE] = 1.0f; + for (int spell = OSRS_SPELL_BLOOD_BARRAGE; + spell < OSRS_SPELL_DEATH_CHARGE; + spell++) + mask[spell_offset + spell] = 0.0f; + int frozen = + s->modifiers.draft_pending && !s->modifiers.draft_free_movement; + mask[spell_offset + OSRS_SPELL_DEATH_CHARGE] = + !frozen && s->death_charge_cd == 0 && + s->death_charge_window_left == 0 ? 1.0f : 0.0f; - mask[offset++] = s->modifiers.draft_pending ? 0.0f : 1.0f; - for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) - mask[offset++] = (s->modifiers.draft_pending && s->modifiers.draft_options[o] >= 0) + int spec_offset = col_action_head_mask_offset(COLO_HEAD_SPEC); + mask[spec_offset] = 1.0f; + int spec_cost = osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]); + mask[spec_offset + 1] = + spec_cost > 0 && + (s->player.special_energy >= spec_cost || s->player.spec_armed) ? 1.0f : 0.0f; + mask[spec_offset + 2] = s->player.spec_armed ? 1.0f : 0.0f; - mask[offset++] = 1.0f; - for (int g = 0; g < COLO_NUM_GRAPPLE_SLOTS; g++) - mask[offset++] = s->sol.grapple_active ? 1.0f : 0.0f; - - mask[offset++] = 1.0f; - mask[offset++] = (s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE) ? 1.0f : 0.0f; - mask[offset++] = (s->player.current_prayer > 0 && - s->player.offensive_prayer != OFFENSIVE_PRAYER_PIETY) ? 1.0f : 0.0f; - mask[offset++] = (s->player.current_prayer > 0 && - s->player.offensive_prayer != OFFENSIVE_PRAYER_RIGOUR) ? 1.0f : 0.0f; - mask[offset++] = (s->player.current_prayer > 0 && - s->player.offensive_prayer != OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; + int offensive_offset = col_action_head_mask_offset(COLO_HEAD_OFFENSIVE); + mask[offensive_offset] = 1.0f; + mask[offensive_offset + 1] = + s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE ? 1.0f : 0.0f; + mask[offensive_offset + 2] = + s->player.current_prayer > 0 && + s->player.offensive_prayer != OFFENSIVE_PRAYER_PIETY ? 1.0f : 0.0f; + mask[offensive_offset + 3] = + s->player.current_prayer > 0 && + s->player.offensive_prayer != OFFENSIVE_PRAYER_RIGOUR ? 1.0f : 0.0f; + mask[offensive_offset + 4] = + s->player.current_prayer > 0 && + s->player.offensive_prayer != OFFENSIVE_PRAYER_AUGURY ? 1.0f : 0.0f; - mask[offset++] = 1.0f; - mask[offset++] = (!frozen && s->death_charge_cd == 0 && - s->death_charge_window_left == 0) ? 1.0f : 0.0f; + int modifier_offset = + col_action_head_mask_offset(COLO_HEAD_MODIFIER_SELECT); + mask[modifier_offset] = s->modifiers.draft_pending ? 0.0f : 1.0f; + for (int option = 0; option < COLO_MODIFIER_DRAFT_OPTIONS; option++) { + mask[modifier_offset + option + 1] = + s->modifiers.draft_pending && + s->modifiers.draft_options[option] >= 0 ? 1.0f : 0.0f; + } - if (offset != COLO_ACTION_MASK_SIZE) { - fprintf(stderr, "colosseum mask index mismatch: wrote %d, expected %d\n", - offset, COLO_ACTION_MASK_SIZE); - abort(); + int grapple_offset = + col_action_head_mask_offset(COLO_HEAD_GRAPPLE_PARRY); + mask[grapple_offset] = 1.0f; + for (int grapple = 0; grapple < COLO_NUM_GRAPPLE_SLOTS; grapple++) { + mask[grapple_offset + grapple + 1] = + s->sol.grapple_active ? 1.0f : 0.0f; } } + + static float col_get_reward_ctx(EncounterState* state, EncounterContext* context) { (void)context; return ((ColosseumState*)state)->reward; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index 554a749230..75eb506c8c 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -898,31 +898,15 @@ typedef struct { } ColoBestGear; #define COLO_BEST_GEAR_CACHE_WAYS 12 - -/* Feeds encounter_init_maxed_player_combat_stats and, through it, every prayer-restore - * feature the item observation table bakes. Never rewritten during an episode. */ #define COLO_PLAYER_PRAYER_LEVEL 99 -/* Replaces the 11 per-slot item stat rows. Those were a second encoding of combat maths - * the player block already carries: encounter_compute_loadout_stats sums the equipped set - * into EncounterLoadoutStats and the prefix emits attack_bonus, strength_bonus, max_hit, - * attack_speed, attack_range, all five defence bonuses, eff_level and spell_base_damage. - * Equipment EFFECTS were the one part with no aggregate in the observation. */ -#define COLO_INVENTORY_OBS_CACHE_WAYS 8 -#define COLO_INVENTORY_OBS_CACHE_FLOATS \ - (OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES_CODED + \ - OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES) typedef struct { ColoBestGear best_gear[COLO_BEST_GEAR_CACHE_WAYS][COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; uint64_t best_gear_sig[COLO_BEST_GEAR_CACHE_WAYS]; int best_gear_valid[COLO_BEST_GEAR_CACHE_WAYS]; int best_gear_next; - float inventory[COLO_INVENTORY_OBS_CACHE_WAYS][COLO_INVENTORY_OBS_CACHE_FLOATS]; - uint64_t inventory_sig[COLO_INVENTORY_OBS_CACHE_WAYS]; - int inventory_valid[COLO_INVENTORY_OBS_CACHE_WAYS]; - int inventory_next; } ColoObsMemoCaches; typedef enum { @@ -1542,7 +1526,6 @@ typedef struct { int divine_ranged_timer; int surge_doses; int surge_cooldown; - OsrsInventoryCell inventory_cells[OSRS_INVENTORY_SIZE]; int thrall_target_slot; int thrall_aggro_ticks_left; @@ -1580,7 +1563,7 @@ typedef struct { ColosseumLog log; ColoObsMemoCaches obs_memos; } ColosseumState; -static_assert(sizeof(ColosseumState) == 29872, "ColosseumState serialized layout"); +static_assert(sizeof(ColosseumState) == 26640, "ColosseumState serialized layout"); typedef enum { COLO_DMG_OFFPRAY = 0, @@ -1596,5 +1579,5 @@ static void col_build_live_inventory_display( int out[COLO_INVENTORY_DISPLAY_SLOTS] ) { for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) - out[i] = osrs_inventory_cell_raw_osrs_id(&s->inventory_cells[i]); + out[i] = osrs_inventory_cell_raw_osrs_id(&s->player.inventory_cells[i]); } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc index 26e245634b..17f69f1786 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc @@ -162,31 +162,31 @@ static void col_late_start_drain_supplies( for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) { if (done[c] || osrs_inventory_cell_dose_count( - &s->inventory_cells[c]) == 0) continue; - int item = osrs_inventory_cell_item_index(&s->inventory_cells[c]); + &s->player.inventory_cells[c]) == 0) continue; + int item = osrs_inventory_cell_item_index(&s->player.inventory_cells[c]); int total = 0; for (int k = c; k < COLO_INVENTORY_DISPLAY_SLOTS; k++) { - if (osrs_inventory_cell_item_index(&s->inventory_cells[k]) == item && + if (osrs_inventory_cell_item_index(&s->player.inventory_cells[k]) == item && osrs_inventory_cell_dose_count( - &s->inventory_cells[k]) > 0) { + &s->player.inventory_cells[k]) > 0) { done[k] = 1; total += osrs_inventory_cell_dose_count( - &s->inventory_cells[k]); + &s->player.inventory_cells[k]); } } int drain = total - (int)((float)total * f + 0.5f); for (int k = COLO_INVENTORY_DISPLAY_SLOTS - 1; k >= c && drain > 0; k--) { if (osrs_inventory_cell_item_index( - &s->inventory_cells[k]) != item) continue; + &s->player.inventory_cells[k]) != item) continue; while (osrs_inventory_cell_dose_count( - &s->inventory_cells[k]) > 0 && drain > 0) { + &s->player.inventory_cells[k]) > 0 && drain > 0) { OsrsInventoryClickResolution res = osrs_inventory_cell_click_interpret( - &s->inventory_cells[k], OSRS_CLICK_TICK_FIRST); + &s->player.inventory_cells[k], OSRS_CLICK_TICK_FIRST); if (res.click_action != OSRS_CLICK_DRINK) break; int scratch_timer = 0; OsrsInventoryDrinkConsumeResult consumed = osrs_inventory_cell_consume_drink_one_dose( - &s->inventory_cells[k], res, &scratch_timer, + &s->player.inventory_cells[k], res, &scratch_timer, col_late_start_drink_noop, s); if (!consumed.consumed) break; drain--; @@ -237,7 +237,7 @@ static void col_record_wave_entry(const ColosseumState* s, int entered_wave) { } if (slot >= 0) { ColoWaveEntrySample* out = &res->samples[slot]; - memcpy(out->cells, s->inventory_cells, sizeof(out->cells)); + memcpy(out->cells, s->player.inventory_cells, sizeof(out->cells)); out->active_mask = s->modifiers.active_mask; for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) out->tier[m] = (uint8_t)s->modifiers.tier[m]; @@ -259,7 +259,7 @@ static int col_apply_wave_entry_sample(ColosseumState* s, int start_wave) { } if (!have) return 0; - memcpy(s->inventory_cells, sample.cells, sizeof(sample.cells)); + memcpy(s->player.inventory_cells, sample.cells, sizeof(sample.cells)); col_sync_consumable_counters_from_inventory(s); s->modifiers.active_mask = sample.active_mask; for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc index 65c86540f3..4c3d55384a 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc @@ -64,18 +64,18 @@ col_npc_blocked_ignore_player_assume_finalized_size_in_range( if (encounter_arena_topology_footprint_blocked_assume_finalized_size_in_range( geometry->context->route_topology, x, y, size)) return 1; - int sol_clamp_active = col_sol_clamp_active(state); - for (int dx = 0; dx < size; dx++) { - for (int dy = 0; dy < size; dy++) { - if (sol_clamp_active && - !col_in_boss_arena(state, x + dx, y + dy)) - return 1; - int grid_x, grid_y; - if (!col_grid_index(x + dx, y + dy, &grid_x, &grid_y)) + if (col_sol_clamp_active(state) && + (x <= state->sol.boss_arena_min_x || + y <= state->sol.boss_arena_min_y || + x + size - 1 >= state->sol.boss_arena_max_x || + y + size - 1 >= state->sol.boss_arena_max_y)) + return 1; + int grid_x = x - COLO_ARENA_MIN_X; + int grid_y = y - COLO_ARENA_MIN_Y; + for (int dx = 0; dx < size; dx++) + for (int dy = 0; dy < size; dy++) + if (state->npc_collision_flags[grid_x + dx][grid_y + dy]) return 1; - if (state->npc_collision_flags[grid_x][grid_y]) return 1; - } - } return 0; } static int col_npc_blocked_ignore_player( @@ -98,17 +98,8 @@ static int col_npc_blocked(void* data, int x, int y, int size) { ColoGeometryContext* geometry = (ColoGeometryContext*)data; ColosseumState* state = geometry->state; if (col_npc_blocked_ignore_player(data, x, y, size)) return 1; - for (int dx = 0; dx < size; dx++) { - for (int dy = 0; dy < size; dy++) { - int grid_x, grid_y; - if (!col_grid_index(x + dx, y + dy, &grid_x, &grid_y)) - return 1; - if (grid_x == state->player_grid_x && - grid_y == state->player_grid_y) - return 1; - } - } - return 0; + return state->player.x >= x && state->player.x < x + size && + state->player.y >= y && state->player.y < y + size; } typedef struct { @@ -123,7 +114,7 @@ static int col_npc_move_blocked_ignore_player( int size ) { ColoNpcMoveCtx* move = (ColoNpcMoveCtx*)data; - return col_npc_blocked_ignore_player( + return col_npc_blocked_ignore_player_assume_finalized_size_in_range( &move->geometry, x, y, size); } @@ -136,6 +127,7 @@ static int col_npc_move_overlap_hold(void* data) { typedef struct { ColoGeometryContext geometry; + const uint16_t (*npc_collision_prefix)[COLO_ARENA_HEIGHT + 1]; int size; int ignore_npcs; } ColoNpcPathCtx; @@ -156,6 +148,33 @@ static ColoNpcPathCtx col_npc_path_ctx_begin( .size = size, }; } +static uint64_t col_npc_path_blocker_revision( + const ColoNpcPathCtx* path +) { + const Player* player = &path->geometry.state->player; + uint64_t tile = + (uint64_t)(player->x - COLO_ARENA_MIN_X) * + COLO_ARENA_HEIGHT + + (uint64_t)(player->y - COLO_ARENA_MIN_Y); + return 1 + tile + + (uint64_t)col_sol_clamp_active(path->geometry.state) * + COLO_ARENA_WIDTH * COLO_ARENA_HEIGHT; +} + +static void col_npc_collision_prefix_build( + const ColosseumState* state, + uint16_t prefix[COLO_ARENA_WIDTH + 1][COLO_ARENA_HEIGHT + 1] +) { + memset(prefix, 0, sizeof(uint16_t) * + (COLO_ARENA_WIDTH + 1) * (COLO_ARENA_HEIGHT + 1)); + for (int x = 0; x < COLO_ARENA_WIDTH; x++) { + uint16_t column_sum = 0; + for (int y = 0; y < COLO_ARENA_HEIGHT; y++) { + column_sum += state->npc_collision_flags[x][y] != 0; + prefix[x + 1][y + 1] = prefix[x][y + 1] + column_sum; + } + } +} static int col_npc_path_blocked_uncached( ColoNpcPathCtx* path, @@ -180,8 +199,28 @@ static int col_npc_path_blocked_uncached( return 1; return 0; } - return col_npc_blocked_ignore_player_assume_finalized_size_in_range( - &path->geometry, x, y, path->size); + if (!path->npc_collision_prefix) + return col_npc_blocked_ignore_player_assume_finalized_size_in_range( + &path->geometry, x, y, path->size); + if (encounter_arena_topology_footprint_blocked_assume_finalized_size_in_range( + path->geometry.context->route_topology, x, y, path->size)) + return 1; + if (col_sol_clamp_active(state) && + (x <= state->sol.boss_arena_min_x || + y <= state->sol.boss_arena_min_y || + x + path->size - 1 >= state->sol.boss_arena_max_x || + y + path->size - 1 >= state->sol.boss_arena_max_y)) + return 1; + int x0 = x - COLO_ARENA_MIN_X; + int y0 = y - COLO_ARENA_MIN_Y; + int x1 = x0 + path->size; + int y1 = y0 + path->size; + uint16_t occupied = + path->npc_collision_prefix[x1][y1] - + path->npc_collision_prefix[x0][y1] - + path->npc_collision_prefix[x1][y0] + + path->npc_collision_prefix[x0][y0]; + return occupied != 0; } static int col_npc_path_blocked(void* data, int x, int y, int size) { @@ -193,12 +232,13 @@ static int col_npc_path_blocked(void* data, int x, int y, int size) { return col_npc_path_blocked_uncached(path, x, y); } -static int col_npc_path_step_with_ctx( +static int col_npc_path_steps_with_ctx( ColoNpcPathCtx* pc, int* x, int* y, int tx, - int ty + int ty, + int maximum_steps ) { if (tx < COLO_ARENA_MIN_X) tx = COLO_ARENA_MIN_X; if (tx > COLO_ARENA_MAX_X) tx = COLO_ARENA_MAX_X; @@ -216,7 +256,7 @@ static int col_npc_path_step_with_ctx( .blockers = { .is_blocked = col_npc_path_blocked, .ctx = pc, - .revision = 1, + .revision = col_npc_path_blocker_revision(pc), }, .source_x = *x, .source_y = *y, @@ -225,10 +265,13 @@ static int col_npc_path_step_with_ctx( .target_y = ty, .target_size = 1, .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, - .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_WALK, + .movement_mode = maximum_steps > 1 + ? ENCOUNTER_ROUTE_MOVEMENT_RUN + : ENCOUNTER_ROUTE_MOVEMENT_WALK, .cost_policy = pc->ignore_npcs ? ENCOUNTER_ROUTE_COST_SOUTH_FIRST_REVERSE : ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + .result_detail = ENCOUNTER_ROUTE_RESULT_NEXT_STEPS, }; EncounterRouteResult route = encounter_route_solve(&input); #ifdef COLO_PROFILE_ENABLED @@ -241,9 +284,30 @@ static int col_npc_path_step_with_ctx( } *x += route.first_dx; *y += route.first_dy; + if (maximum_steps > 1 && route.distance > 1) { + *x += route.run_dx; + *y += route.run_dy; + return 2; + } return 1; } +static OSRS_ROUTE_NOINLINE int col_npc_path_step_with_prefix( + ColosseumState* state, + const ColosseumContext* context, + int* x, + int* y, + int size, + int tx, + int ty +) { + uint16_t prefix[COLO_ARENA_WIDTH + 1][COLO_ARENA_HEIGHT + 1]; + col_npc_collision_prefix_build(state, prefix); + ColoNpcPathCtx path = col_npc_path_ctx_begin(state, context, size); + path.npc_collision_prefix = prefix; + return col_npc_path_steps_with_ctx(&path, x, y, tx, ty, 1); +} + static int col_npc_path_step( ColosseumState* s, const ColosseumContext* context, @@ -253,11 +317,15 @@ static int col_npc_path_step( int tx, int ty ) { + if (size >= COLO_NPC_STATS[COLO_SOL_HEREDIT].size) + return col_npc_path_step_with_prefix( + s, context, x, y, size, tx, ty); ColoNpcPathCtx pc = col_npc_path_ctx_begin(s, context, size); - return col_npc_path_step_with_ctx(&pc, x, y, tx, ty); + return col_npc_path_steps_with_ctx(&pc, x, y, tx, ty, 1); } + static void col_warband_pick_target( ColosseumState* s, ColoNpcPathCtx* pc, ColoNPC* npc, int* tx, int* ty) { int px = s->player.x, py = s->player.y; @@ -296,12 +364,8 @@ static void col_warband_move( pc.ignore_npcs = 1; int tx, ty; col_warband_pick_target(s, &pc, npc, &tx, &ty); - int steps = 0; - for (int k = 0; k < COLO_WARBAND_TILES_PER_TICK; k++) { - if (npc->x == tx && npc->y == ty) break; - if (!col_npc_path_step_with_ctx(&pc, &npc->x, &npc->y, tx, ty)) break; - steps++; - } + int steps = col_npc_path_steps_with_ctx( + &pc, &npc->x, &npc->y, tx, ty, COLO_WARBAND_TILES_PER_TICK); npc->moved_this_tick = steps > 0 ? 1 : 0; } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc index 8c04c10b61..f06916a21a 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -1,16 +1,11 @@ #line 76 "encounter_colosseum.h" -#define COLO_PLAYER_OBS_SIZE 36 -/* Each cell ships an item code plus dynamic equipped and HP-heal fields. The entity encoder - * rebuilds the full compact record from OSRS_ITEM_OBS_TABLE, then overwrites those fields. */ -#define COLO_INVENTORY_CELL_OBS_FEATURES OSRS_INVENTORY_CELL_OBS_FEATURES_CODED -#define COLO_INVENTORY_CELL_ENCODER_FEATURES OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT -#define COLO_INVENTORY_OBS_SIZE \ - (COLO_INVENTORY_DISPLAY_SLOTS * COLO_INVENTORY_CELL_OBS_FEATURES) -#define COLO_EQUIPPED_SELF_OBS_SIZE OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES -static_assert(COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE == - COLO_INVENTORY_OBS_CACHE_FLOATS, - "inventory obs cache float count must match the inventory+equipped block size"); +#define COLO_INVENTORY_CELL_OBS_FEATURES \ + OSRS_SHARED_INVENTORY_CELL_OBS_FEATURES +#define COLO_INVENTORY_CELL_ENCODER_FEATURES \ + OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT +#define COLO_INVENTORY_OBS_SIZE OSRS_SHARED_INVENTORY_OBS_SIZE +#define COLO_EQUIPPED_OBS_SIZE OSRS_SHARED_EQUIPPED_OBS_SIZE static_assert(OSRS_ITEM_OBS_TABLE_ROWS == OSRS_ITEM_CONTENT_COUNT, "regenerate osrs_item_obs_table.inc: the cell code space changed"); static_assert(OSRS_ITEM_OBS_TABLE_COLS == COLO_INVENTORY_CELL_ENCODER_FEATURES, @@ -21,9 +16,6 @@ static_assert(OSRS_ITEM_OBS_TABLE_BASE_PRAYER == COLO_PLAYER_PRAYER_LEVEL, "regenerate osrs_item_obs_table.inc: it bakes prayer_restore at this level"); static_assert(OSRS_ITEM_OBS_TABLE_BASE_RANGED == MAXED_BASE_RANGED, "regenerate osrs_item_obs_table.inc: it bakes offensive_boost at this level"); -static_assert(OSRS_ITEM_OBS_OVERLAY_EQUIPPED == OSRS_INVENTORY_CELL_COMPACT_EQUIPPED && - OSRS_ITEM_OBS_OVERLAY_HP_HEAL == OSRS_INVENTORY_CELL_COMPACT_HP_HEAL, - "the gather overwrites the dynamic fields at these expanded slots"); static_assert(OSRS_ITEM_CONTENT_COUNT <= OSRS_ITEM_OBS_CODE_SCALE, "every observed content code must remain exact after scaling"); #define COLO_NPC_CORE_FEATURES 11 @@ -84,21 +76,19 @@ typedef enum { #define COLO_ANCHOR_BLOCKED_OBS_SIZE COLO_NUM_SPAWN_ANCHORS #define COLO_SPAWN_OBS_SIZE (COLO_SPAWN_PHASE_OBS_SIZE + COLO_ANCHOR_BLOCKED_OBS_SIZE) -#define COLO_NUM_OBS (COLO_PLAYER_OBS_SIZE + \ - COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE + COLO_NPC_OBS_SIZE + \ +#define COLO_NUM_OBS (OSRS_SHARED_OBS_SIZE + COLO_NPC_OBS_SIZE + \ COLO_MODIFIER_OBS_SIZE + COLO_WAVE_OBS_SIZE + COLO_BOSS_OBS_SIZE + \ - COLO_PENDING_HIT_OBS_SIZE + \ - COLO_THREAT_LOS_OBS_SIZE + COLO_THRALL_DC_OBS_SIZE + \ - COLO_SPAWN_OBS_SIZE) - -#define COLO_OBS_AFTER_PLAYER COLO_PLAYER_OBS_SIZE -#define COLO_OBS_AFTER_INVENTORY (COLO_OBS_AFTER_PLAYER + COLO_INVENTORY_OBS_SIZE) -#define COLO_OBS_AFTER_EQUIPPED_SELF \ - (COLO_OBS_AFTER_INVENTORY + COLO_EQUIPPED_SELF_OBS_SIZE) -#define COLO_OBS_AFTER_NPCS (COLO_OBS_AFTER_EQUIPPED_SELF + COLO_NPC_OBS_SIZE) -#define COLO_OBS_AFTER_MODS (COLO_OBS_AFTER_NPCS + COLO_MODIFIER_OBS_SIZE) -#define COLO_OBS_AFTER_WAVE (COLO_OBS_AFTER_MODS + COLO_WAVE_OBS_SIZE) -#define COLO_OBS_AFTER_BOSS (COLO_OBS_AFTER_WAVE + COLO_BOSS_OBS_SIZE) + COLO_PENDING_HIT_OBS_SIZE + COLO_THREAT_LOS_OBS_SIZE + \ + COLO_THRALL_DC_OBS_SIZE + COLO_SPAWN_OBS_SIZE) + +#define COLO_OBS_INVENTORY_START OSRS_SHARED_SELF_OBS_SIZE +#define COLO_OBS_EQUIPMENT_START \ + (COLO_OBS_INVENTORY_START + OSRS_SHARED_INVENTORY_OBS_SIZE) +#define COLO_OBS_AFTER_SHARED OSRS_SHARED_OBS_SIZE +#define COLO_OBS_AFTER_NPCS (COLO_OBS_AFTER_SHARED + COLO_NPC_OBS_SIZE) +#define COLO_OBS_AFTER_MODS (COLO_OBS_AFTER_NPCS + COLO_MODIFIER_OBS_SIZE) +#define COLO_OBS_AFTER_WAVE (COLO_OBS_AFTER_MODS + COLO_WAVE_OBS_SIZE) +#define COLO_OBS_AFTER_BOSS (COLO_OBS_AFTER_WAVE + COLO_BOSS_OBS_SIZE) #define COLO_OBS_AFTER_PENDING_HITS \ (COLO_OBS_AFTER_BOSS + COLO_PENDING_HIT_OBS_SIZE) #define COLO_OBS_AFTER_THREAT_LOS \ @@ -124,6 +114,8 @@ static inline float col_obs_rel_y(int ny, int py) { typedef struct { OsrsVenatorMonster monsters[COLO_MAX_NPCS]; + uint32_t bounce_masks[COLO_MAX_NPCS]; + uint8_t selection_distances[COLO_MAX_NPCS][COLO_MAX_NPCS]; int count; } ColoVenatorPreviewTargets; @@ -151,6 +143,51 @@ static void col_collect_venator_preview_targets( if (!col_npc_is_live_enemy(npc)) continue; out->monsters[out->count++] = col_venator_monster_from_npc(npc, slot); } + for (int sender_idx = 0; sender_idx < out->count; sender_idx++) { + OsrsVenatorMonster sender = out->monsters[sender_idx]; + for (int target_idx = 0; target_idx < out->count; target_idx++) { + OsrsVenatorMonster target = out->monsters[target_idx]; + if (osrs_venator_can_bounce( + sender.footprint, target.footprint)) { + out->bounce_masks[sender.slot] |= + UINT32_C(1) << target.slot; + } + int distance = osrs_venator_selection_distance( + sender.footprint, target.footprint); + if (distance < 0 || distance > UINT8_MAX) abort(); + out->selection_distances[sender.slot][target.slot] = + (uint8_t)distance; + } + } +} +static int col_venator_best_bounce_slot( + const ColoVenatorPreviewTargets* targets, + int range_sender_slot, + int selection_sender_slot, + int forbidden_slot +) { + int best_slot = -1; + int best_distance = INT_MAX; + uint32_t bounce_mask = targets->bounce_masks[range_sender_slot]; + for (int candidate_idx = 0; + candidate_idx < targets->count; + candidate_idx++) { + int candidate_slot = targets->monsters[candidate_idx].slot; + if (candidate_slot == forbidden_slot || + (bounce_mask & (UINT32_C(1) << candidate_slot)) == 0) + continue; + int distance = + targets->selection_distances[selection_sender_slot][candidate_slot]; + if (osrs_venator_is_better_candidate( + distance, + candidate_slot, + best_distance, + best_slot)) { + best_slot = candidate_slot; + best_distance = distance; + } + } + return best_slot; } static int col_venator_extra_bounce_if_shot( @@ -162,12 +199,13 @@ static int col_venator_extra_bounce_if_shot( abort(); const ColoNPC* npc = &s->npcs[primary_slot]; if (!col_npc_is_live_enemy(npc)) return 0; - OsrsVenatorMonster primary = col_venator_monster_from_npc(npc, primary_slot); - OsrsVenatorChain chain = - osrs_venator_resolve_chain(primary, targets->monsters, targets->count); - int extra = (int)chain.length - 1; - if (extra < 0 || extra > OSRS_VENATOR_MAX_CHAIN_HITS - 1) abort(); - return extra; + int second_slot = col_venator_best_bounce_slot( + targets, primary_slot, primary_slot, primary_slot); + if (second_slot < 0) return 0; + if (targets->count < 3) return 1; + int third_slot = col_venator_best_bounce_slot( + targets, primary_slot, second_slot, second_slot); + return third_slot >= 0 ? 2 : 1; } typedef struct { @@ -749,9 +787,13 @@ static void col_write_boss_obs( } } } else { + memset(&obs[i], 0, + COLO_SOL_LASER_OBS_SIZE * sizeof(*obs)); i += COLO_SOL_LASER_OBS_SIZE; } } else { + memset(&obs[i], 0, + COLO_BOSS_OBS_PREMOVE_TAIL_SIZE * sizeof(*obs)); i += COLO_BOSS_OBS_PREMOVE_TAIL_SIZE; } *idx = i; @@ -1166,7 +1208,7 @@ static void col_build_gear_candidates(const ColosseumState* s, ColoGearCandidate col_gear_candidates_add(c, slot, s->player.equipped[slot]); for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { const OsrsItemContentMetadata* metadata = - osrs_inventory_cell_metadata(&s->inventory_cells[cell]); + osrs_inventory_cell_metadata(&s->player.inventory_cells[cell]); uint8_t item = metadata->item_idx; if (item == ITEM_NONE) continue; int gslot = metadata->gear_slot; @@ -1530,105 +1572,6 @@ static int col_attacked_with_argmax_set(ColosseumState* s) { return col_weapon_set_is_argmax_dpt_for_npc(s, npc_slot, s->weapon_set, best); } -static uint64_t col_inventory_obs_signature(const ColosseumState* s) { - /* Equipped gear is deliberately NOT in the key. It was only ever an input through the - * counterfactual deltas, and hashing it made every gear swap evict the block -- measured - * at a 34.9% hit rate. What remains depends on cell contents and base stats only. */ - uint64_t h = 1469598103934665603ULL; - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { - uint64_t packed = s->inventory_cells[cell].content_code; - h = (h ^ packed) * 1099511628211ULL; - } - const int scalars[3] = { - s->player.base_hitpoints, s->player.base_prayer, s->player.base_ranged, - }; - for (int k = 0; k < 3; k++) - h = (h ^ (uint64_t)(uint32_t)scalars[k]) * 1099511628211ULL; - return h; -} - -/* Gear-dependent, so it is written fresh on every path. The memo key covers cell contents - * only -- putting equipped gear back in it would send the hit rate back to the 34.9% that - * made this block expensive in the first place. */ -/* Non-const because col_live_effects memoises internally. C accepts the implicit cast and - * g++/nvcc reject it, so this only shows up in check-builds.sh, never in the C test gate. */ -static void col_write_equipped_effect_aggregate( - ColosseumState* s, float* obs, int* pi -) { - osrs_write_equipment_effect_aggregate(&obs[*pi], col_live_effects(s)); - *pi += OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES; -} - -/* Likewise gear-dependent: it asks whether the item in this cell is the one currently worn - * in its slot. Stamped after a cache hit so a gear swap shows up on the tick it happens. */ -static void col_stamp_equipped_flags(const ColosseumState* s, float* obs, int block_start) { - for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) { - obs[block_start + c * COLO_INVENTORY_CELL_OBS_FEATURES + - OSRS_INVENTORY_CELL_OBS_EQUIPPED] = - osrs_inventory_cell_holds_equipped_item(&s->player, s->inventory_cells, c) - ? 1.0f : 0.0f; - } -} - -static void col_write_inventory_and_equipped_obs( - ColosseumState* s, - float* obs, - int* idx -) { - int i = *idx; - int block_start = i; - - ColoObsMemoCaches* memos = &s->obs_memos; - uint64_t sig = col_inventory_obs_signature(s); - for (int w = 0; w < COLO_INVENTORY_OBS_CACHE_WAYS; w++) { - if (memos->inventory_valid[w] && memos->inventory_sig[w] == sig) { - memcpy(&obs[i], memos->inventory[w], - sizeof(float) * COLO_INVENTORY_OBS_CACHE_FLOATS); - col_stamp_equipped_flags(s, obs, block_start); - int j = block_start + COLO_INVENTORY_OBS_SIZE; - col_write_equipped_effect_aggregate(s, obs, &j); - s->log.inv_memo_hits += 1.0f; - *idx = block_start + COLO_INVENTORY_OBS_CACHE_FLOATS; - return; - } - } - s->log.inv_memo_misses += 1.0f; - - /* The generated table bakes base-99 item semantics. Colosseum overwrites HP heal because - * Frailty rewrites base hitpoints, while prayer and ranged stay fixed. */ - if (s->player.base_prayer != OSRS_ITEM_OBS_TABLE_BASE_PRAYER || - s->player.base_ranged != OSRS_ITEM_OBS_TABLE_BASE_RANGED) { - fprintf(stderr, - "colosseum inventory obs: base prayer %d ranged %d, item table baked %d/%d\n", - s->player.base_prayer, s->player.base_ranged, - OSRS_ITEM_OBS_TABLE_BASE_PRAYER, OSRS_ITEM_OBS_TABLE_BASE_RANGED); - abort(); - } - - for (int cell_idx = 0; cell_idx < COLO_INVENTORY_DISPLAY_SLOTS; cell_idx++) { - const ColoInvCell* cell = &s->inventory_cells[cell_idx]; - osrs_write_inventory_cell_obs_code_features( - &obs[i], - cell, - osrs_inventory_cell_holds_equipped_item( - &s->player, s->inventory_cells, cell_idx), - s->player.base_hitpoints); - i += COLO_INVENTORY_CELL_OBS_FEATURES; - } - assert(i == COLO_OBS_AFTER_INVENTORY); - - col_write_equipped_effect_aggregate(s, obs, &i); - assert(i == COLO_OBS_AFTER_EQUIPPED_SELF); - - int slot = memos->inventory_next; - memcpy(memos->inventory[slot], &obs[block_start], - sizeof(float) * COLO_INVENTORY_OBS_CACHE_FLOATS); - memos->inventory_sig[slot] = sig; - memos->inventory_valid[slot] = 1; - memos->inventory_next = (slot + 1) % COLO_INVENTORY_OBS_CACHE_WAYS; - - *idx = i; -} static void col_write_obs_ctx( EncounterState* state, @@ -1653,58 +1596,38 @@ static void col_write_obs_ctx( #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_REFRESH_SLOTS); #endif - memset(obs, 0, COLO_NUM_OBS * sizeof(float)); int i = 0; - int px = s->player.x, py = s->player.y; const EncounterLoadoutStats* ls = col_live_loadout_stats(s); - obs[i++] = col_clamp01((float)s->player.current_hitpoints / 99.0f); - obs[i++] = (float)(px - COLO_ARENA_MIN_X) / (float)COLO_ARENA_WIDTH; - obs[i++] = (float)(COLO_ARENA_MAX_X - px) / (float)COLO_ARENA_WIDTH; - obs[i++] = (float)(py - COLO_ARENA_MIN_Y) / (float)COLO_ARENA_HEIGHT; - obs[i++] = (float)(COLO_ARENA_MAX_Y - py) / (float)COLO_ARENA_HEIGHT; - obs[i++] = (s->player.prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; - obs[i++] = (s->player.prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; - obs[i++] = (s->player.prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; - obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_PIETY) ? 1.0f : 0.0f; - obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) ? 1.0f : 0.0f; - obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; - obs[i++] = col_clamp01((float)s->player.current_prayer / 99.0f); - obs[i++] = col_clamp01((float)s->player.run_energy / 10000.0f); - obs[i++] = col_clamp01((float)s->player.special_energy / 100.0f); - obs[i++] = s->player.spec_armed ? 1.0f : 0.0f; - obs[i++] = col_clamp01((float)s->player.attack_timer / 8.0f); - obs[i++] = col_clamp01((float)s->player.food_timer / 3.0f); - obs[i++] = col_clamp01((float)s->player.potion_timer / 3.0f); - obs[i++] = osrs_interaction_active(&s->interaction) ? 1.0f : 0.0f; - obs[i++] = col_clamp01((float)ls->attack_range / 15.0f); - obs[i++] = col_clamp01((float)ls->max_hit / 80.0f); - obs[i++] = col_clamp01((float)ls->attack_speed / 6.0f); - obs[i++] = col_clamp01((float)s->player.current_defence / 99.0f); - obs[i++] = col_clamp01((float)ls->def_stab / 300.0f); - obs[i++] = col_clamp01((float)ls->def_slash / 300.0f); - obs[i++] = col_clamp01((float)ls->def_crush / 300.0f); - obs[i++] = col_clamp01((float)ls->def_magic / 300.0f); - obs[i++] = col_clamp01((float)ls->def_ranged / 300.0f); - obs[i++] = col_clamp01((float)ls->eff_level / 165.0f); - obs[i++] = col_clamp01((float)ls->attack_bonus / 200.0f); - obs[i++] = col_clamp01((float)ls->strength_bonus / 160.0f); - int colo_spec_cost = osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]); - obs[i++] = col_clamp01((float)colo_spec_cost / 100.0f); - obs[i++] = colo_spec_cost > 0 ? 1.0f : 0.0f; - obs[i++] = s->active_loadout_profile == COLO_LOADOUT_PROFILE_SPEEDRUN ? 1.0f : 0.0f; - obs[i++] = s->active_loadout_profile == COLO_LOADOUT_PROFILE_BEGINNER ? 1.0f : 0.0f; - obs[i++] = col_clamp01((float)ls->spell_base_damage / 40.0f); - assert(i == COLO_OBS_AFTER_PLAYER); + int colo_spec_cost = + osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]); + OsrsSharedObservationInput shared_input = { + .player = &s->player, + .interaction = &s->interaction, + .arena_min_x = COLO_ARENA_MIN_X, + .arena_max_x = COLO_ARENA_MAX_X, + .arena_min_y = COLO_ARENA_MIN_Y, + .arena_max_y = COLO_ARENA_MAX_Y, + .attack_style = col_equipped_weapon_attack_style(&s->player), + .attack_range = ls->attack_range, + .max_hit = ls->max_hit, + .attack_speed = ls->attack_speed, + .defence_stab = ls->def_stab, + .defence_slash = ls->def_slash, + .defence_crush = ls->def_crush, + .defence_magic = ls->def_magic, + .defence_ranged = ls->def_ranged, + .effective_level = ls->eff_level, + .attack_bonus = ls->attack_bonus, + .strength_bonus = ls->strength_bonus, + .spell_base_damage = ls->spell_base_damage, + .special_attack_cost = colo_spec_cost, + }; + i = osrs_write_shared_observations(obs, &shared_input); + assert(i == COLO_OBS_AFTER_SHARED); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_PREFIX); #endif - -#ifdef COLO_PROFILE_ENABLED - COLO_PROFILE_MARK(COLO_PROF_OBS_PILLARS); -#endif - - col_write_inventory_and_equipped_obs(s, obs, &i); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_INVENTORY); #endif @@ -1717,7 +1640,9 @@ static void col_write_obs_ctx( for (int n = 0; n < COLO_OBS_NPCS; n++) { int npc_idx = s->current_obs_slots[n]; - if (npc_idx < 0 || npc_idx >= COLO_MAX_NPCS || !s->npcs[npc_idx].active) { + if (npc_idx < 0 || npc_idx >= COLO_MAX_NPCS || + !s->npcs[npc_idx].active) { + memset(&obs[i], 0, COLO_FEATURES_PER_NPC * sizeof(*obs)); i += COLO_FEATURES_PER_NPC; continue; } @@ -1774,6 +1699,7 @@ static void col_write_obs_ctx( obs[i++] = col_clamp01((float)ph->damage / 75.0f); obs[i++] = col_pending_hit_ignores_prayer(ph) ? 1.0f : 0.0f; } else { + memset(&obs[i], 0, COLO_FEATURES_PER_HIT * sizeof(*obs)); i += COLO_FEATURES_PER_HIT; } } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc index 28636d8520..195c86dd57 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -1,25 +1,24 @@ #line 76 "encounter_colosseum.h" -#define COLO_HEAD_PRIMARY 0 -#define COLO_HEAD_PRAYER 1 - -#define COLO_HEAD_EQUIP_BASE 2 -#define COLO_HEAD_EQUIP_SLOT(slot) (COLO_HEAD_EQUIP_BASE + (slot)) -#define COLO_HEAD_EAT (COLO_HEAD_EQUIP_BASE + NUM_GEAR_SLOTS) -#define COLO_HEAD_DRINK (COLO_HEAD_EAT + 1) -#define COLO_HEAD_SPEC (COLO_HEAD_DRINK + 1) -#define COLO_HEAD_MODIFIER_SELECT (COLO_HEAD_SPEC + 1) -#define COLO_HEAD_GRAPPLE_PARRY (COLO_HEAD_MODIFIER_SELECT + 1) -#define COLO_HEAD_OFFENSIVE (COLO_HEAD_GRAPPLE_PARRY + 1) -#define COLO_HEAD_SPELL (COLO_HEAD_OFFENSIVE + 1) +#define COLO_HEAD_PRIMARY OSRS_HEAD_PRIMARY +#define COLO_HEAD_PRAYER OSRS_HEAD_OVERHEAD +#define COLO_HEAD_EQUIP_BASE OSRS_HEAD_EQUIP_BASE +#define COLO_HEAD_EQUIP_SLOT(slot) OSRS_HEAD_EQUIP_SLOT(slot) +#define COLO_HEAD_EAT OSRS_HEAD_EAT +#define COLO_HEAD_DRINK OSRS_HEAD_DRINK +#define COLO_HEAD_SPELL OSRS_HEAD_SPELL +#define COLO_HEAD_SPEC OSRS_HEAD_SPECIAL +#define COLO_HEAD_OFFENSIVE OSRS_HEAD_OFFENSIVE +#define COLO_HEAD_MODIFIER_SELECT OSRS_BASE_NUM_ACTION_HEADS +#define COLO_HEAD_GRAPPLE_PARRY (COLO_HEAD_MODIFIER_SELECT + 1) #define COLO_INV_CLICK_HEADS (NUM_GEAR_SLOTS + 2) -#define COLO_INV_CLICK_DIM 29 -#define COLO_PRIMARY_ATTACK_BASE ENCOUNTER_MOVE_ACTIONS -#define COLO_PRIMARY_DIM (ENCOUNTER_MOVE_ACTIONS + COLO_OBS_NPCS) -#define COLO_OVERHEAD_DIM ENCOUNTER_OVERHEAD_DIM_PVE -#define COLO_SPEC_DIM 3 -#define COLO_SPELL_DIM 2 +#define COLO_INV_CLICK_DIM OSRS_INVENTORY_CLICK_DIM +#define COLO_PRIMARY_ATTACK_BASE OSRS_PRIMARY_MOVE_ACTIONS +#define COLO_PRIMARY_DIM OSRS_PRIMARY_DIM(COLO_OBS_NPCS) +#define COLO_OVERHEAD_DIM OSRS_OVERHEAD_DIM +#define COLO_SPEC_DIM OSRS_SPECIAL_DIM +#define COLO_SPELL_DIM OSRS_SPELL_DIM #define COLO_OVERHEAD_NO_CHANGE ENCOUNTER_OVERHEAD_NO_CHANGE #define COLO_OVERHEAD_OFF ENCOUNTER_OVERHEAD_OFF @@ -27,35 +26,24 @@ #define COLO_OVERHEAD_RANGED ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED #define COLO_OVERHEAD_MAGIC ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC -#define COLO_SPELL_DEATH_CHARGE 1 - +#define COLO_SPELL_DEATH_CHARGE OSRS_SPELL_DEATH_CHARGE #define COLO_MODIFIER_SELECT_DIM (1 + COLO_MODIFIER_DRAFT_OPTIONS) #define COLO_GRAPPLE_PARRY_DIM (1 + COLO_NUM_GRAPPLE_SLOTS) #define COLO_ACTION_DIMS_INIT { \ - COLO_PRIMARY_DIM, \ - COLO_OVERHEAD_DIM, \ - COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, \ - COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, \ - COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, \ - COLO_INV_CLICK_DIM, \ - COLO_INV_CLICK_DIM, \ - COLO_SPEC_DIM, \ + OSRS_BASE_ACTION_DIMS(COLO_OBS_NPCS), \ COLO_MODIFIER_SELECT_DIM, \ - COLO_GRAPPLE_PARRY_DIM, \ - ENCOUNTER_OFFENSIVE_DIM, \ - COLO_SPELL_DIM \ + COLO_GRAPPLE_PARRY_DIM \ } static const int COLO_ACTION_DIMS[COLO_NUM_ACTION_HEADS] = COLO_ACTION_DIMS_INIT; -static_assert(COLO_NUM_ACTION_HEADS == 2 + NUM_GEAR_SLOTS + 2 + 5, - "colosseum action head count must match the per-category layout"); -static_assert(COLO_HEAD_SPELL == COLO_NUM_ACTION_HEADS - 1, - "SPELL must be the last colosseum action head"); -#define COLO_ACTION_MASK_SIZE (ENCOUNTER_MOVE_ACTIONS + COLO_OVERHEAD_DIM + \ - COLO_OBS_NPCS + COLO_INV_CLICK_HEADS * COLO_INV_CLICK_DIM + COLO_SPEC_DIM + \ - COLO_MODIFIER_SELECT_DIM + COLO_GRAPPLE_PARRY_DIM + ENCOUNTER_OFFENSIVE_DIM + \ - COLO_SPELL_DIM) +static_assert(COLO_NUM_ACTION_HEADS == OSRS_BASE_NUM_ACTION_HEADS + 2, + "colosseum action head count must match the shared base plus extensions"); +static_assert(COLO_HEAD_GRAPPLE_PARRY == COLO_NUM_ACTION_HEADS - 1, + "colosseum extensions must follow the shared action heads"); +#define COLO_ACTION_MASK_SIZE \ + (OSRS_BASE_ACTION_MASK_SIZE(COLO_OBS_NPCS) + \ + COLO_MODIFIER_SELECT_DIM + COLO_GRAPPLE_PARRY_DIM) static int col_action_head_mask_offset(int head) { int offset = 0; @@ -207,11 +195,13 @@ static void col_sync_weapon_set_from_equipped_weapon(ColosseumState* s) { } static int col_can_equip_from_cell(const ColosseumState* s, int cell_idx) { - return osrs_can_equip_from_cell(&s->player, s->inventory_cells, cell_idx); + return osrs_can_equip_from_cell( + &s->player, s->player.inventory_cells, cell_idx); } static void col_equip_from_cell(ColosseumState* s, int cell_idx) { - if (osrs_equip_from_cell(&s->player, s->inventory_cells, cell_idx) < 0) return; + if (osrs_equip_from_cell( + &s->player, s->player.inventory_cells, cell_idx) < 0) return; col_sync_weapon_set_from_equipped_weapon(s); col_mark_live_loadout_dirty(s); } @@ -265,7 +255,7 @@ static int col_drink_has_effect(const ColosseumState* s, OsrsConsumableKind kind } static int col_inventory_cell_actionable(const ColosseumState* s, int cell_idx) { - const ColoInvCell* cell = &s->inventory_cells[cell_idx]; + const ColoInvCell* cell = &s->player.inventory_cells[cell_idx]; OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); if (r.click_action == OSRS_CLICK_EQUIP) return col_can_equip_from_cell(s, cell_idx); @@ -279,7 +269,7 @@ static int col_inventory_cell_actionable(const ColosseumState* s, int cell_idx) } static void col_empty_inventory_cell(ColosseumState* s, int cell_idx) { - s->inventory_cells[cell_idx] = osrs_inventory_cell_empty(); + s->player.inventory_cells[cell_idx] = osrs_inventory_cell_empty(); } static void col_heal_player_capped(ColosseumState* s, int amount) { @@ -403,7 +393,7 @@ static int col_apply_drink_cell( OsrsInventoryClickResolution resolution ) { if (cell_idx < 0 || cell_idx >= COLO_INVENTORY_DISPLAY_SLOTS) return 0; - ColoInvCell* cell = &s->inventory_cells[cell_idx]; + ColoInvCell* cell = &s->player.inventory_cells[cell_idx]; if (resolution.click_action != OSRS_CLICK_DRINK) return 0; OsrsInventoryDrinkConsumeResult consume = osrs_inventory_cell_consume_drink_one_dose( @@ -426,7 +416,8 @@ static OsrsInventoryTickIntent col_resolve_inventory_tick_intent( clicks.equip_by_slot[slot] = actions[COLO_HEAD_EQUIP_SLOT(slot)]; clicks.eat = actions[COLO_HEAD_EAT]; clicks.drink = actions[COLO_HEAD_DRINK]; - return osrs_resolve_inventory_tick_intent(&s->player, s->inventory_cells, &clicks); + return osrs_resolve_inventory_tick_intent( + &s->player, s->player.inventory_cells, &clicks); } static void col_apply_inventory_tick_intent( diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index e036af5ce5..1054fd3fcc 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -969,11 +969,12 @@ static int col_queue_inventory_click_action( ) { if (inventory_slot < 0 || inventory_slot >= COLO_INVENTORY_DISPLAY_SLOTS) return -1; OsrsInventoryClickResolution r = - osrs_inventory_cell_click_interpret(&s->inventory_cells[inventory_slot], OSRS_CLICK_TICK_FIRST); + osrs_inventory_cell_click_interpret( + &s->player.inventory_cells[inventory_slot], OSRS_CLICK_TICK_FIRST); int action_head; if (r.click_action == OSRS_CLICK_EQUIP) { int gear_slot = osrs_inventory_cell_metadata( - &s->inventory_cells[inventory_slot])->gear_slot; + &s->player.inventory_cells[inventory_slot])->gear_slot; if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) return -1; action_head = COLO_HEAD_EQUIP_SLOT(gear_slot); } else if (r.click_action == OSRS_CLICK_EAT) { @@ -1067,7 +1068,9 @@ static void col_translate_human_commands_ctx( break; case HUMAN_COMMAND_ITEM_ON_ITEM: osrs_inventory_swap_cells( - s->inventory_cells, cmd->inventory_slot, cmd->target_inventory_slot); + s->player.inventory_cells, + cmd->inventory_slot, + cmd->target_inventory_slot); break; case HUMAN_COMMAND_OFFENSIVE_PRAYER: actions[COLO_HEAD_OFFENSIVE] = cmd->offensive_prayer; @@ -1104,7 +1107,7 @@ static void col_step_human_commands_ctx( } #define COLO_SNAPSHOT_MAGIC 0xC010C001u -#define COLO_SNAPSHOT_VERSION 25u +#define COLO_SNAPSHOT_VERSION 26u typedef struct { uint32_t magic; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc index 9a56b80ab3..95d40586ad 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -526,7 +526,7 @@ static void col_spawn_reinforcements( static void col_init_empty_inventory_cells(ColosseumState* s) { for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - s->inventory_cells[i] = osrs_inventory_cell_empty(); + s->player.inventory_cells[i] = osrs_inventory_cell_empty(); } } @@ -537,7 +537,8 @@ static void col_seed_inventory_cells_from_gameplay_loadout(ColosseumState* s) { uint16_t raw_id = (uint16_t)kit[i]; if (raw_id == 0) continue; - s->inventory_cells[i] = osrs_inventory_cell_from_raw_osrs_id(raw_id); + s->player.inventory_cells[i] = + osrs_inventory_cell_from_raw_osrs_id(raw_id); } } @@ -549,7 +550,7 @@ static void col_sync_consumable_counters_from_inventory(ColosseumState* s) { int surge_doses = 0; int antivenom_doses = 0; for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - const ColoInvCell* cell = &s->inventory_cells[i]; + const ColoInvCell* cell = &s->player.inventory_cells[i]; uint8_t dose_count = osrs_inventory_cell_dose_count(cell); if (dose_count == 0) continue; OsrsInventoryClickResolution resolution = @@ -652,8 +653,6 @@ static void col_reset_ctx(EncounterState* state, EncounterContext* context, uint s->episode_max_ticks = ctx->config.episode_max_ticks_override; const uint8_t* const* loadouts = col_loadouts_for_profile(s->active_loadout_profile); encounter_apply_loadout(&s->player, loadouts[COLO_GEAR_MELEE], GEAR_MELEE); - memset(s->player.inventory, ITEM_NONE, sizeof(s->player.inventory)); - memset(s->player.num_items_in_slot, 0, sizeof(s->player.num_items_in_slot)); col_seed_inventory_cells_from_gameplay_loadout(s); const ColoSupplyLoadout* supplies = &COLO_SUPPLY_LOADOUTS[s->active_loadout_profile]; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc index f35211170b..4709ffd58b 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc @@ -207,7 +207,7 @@ static void col_commit_reward_terms(ColosseumState* s, ColoShapedReward r) { static float col_inventory_heal_remaining(const ColosseumState* s) { float total = 0.0f; for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) { - const OsrsInventoryCell* cell = &s->inventory_cells[i]; + const OsrsInventoryCell* cell = &s->player.inventory_cells[i]; if (osrs_inventory_cell_is_empty(cell)) continue; OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); diff --git a/ocean/osrs/encounters/encounter_colosseum.h b/ocean/osrs/encounters/encounter_colosseum.h index 288a98c404..63557e0e64 100644 --- a/ocean/osrs/encounters/encounter_colosseum.h +++ b/ocean/osrs/encounters/encounter_colosseum.h @@ -16,6 +16,7 @@ #include "../osrs_inventory_clicks.h" #include "../osrs_item_obs_generated.h" #include "../osrs_inventory_actions.h" +#include "../osrs_policy.h" #include "../osrs_interaction.h" #include "../data/npc_models.h" #include diff --git a/ocean/osrs/encounters/encounter_inferno.h b/ocean/osrs/encounters/encounter_inferno.h index 8b14f4b0d8..6d8ea2f771 100644 --- a/ocean/osrs/encounters/encounter_inferno.h +++ b/ocean/osrs/encounters/encounter_inferno.h @@ -15,6 +15,7 @@ #include "../osrs_player_consumables.h" #include "../osrs_inventory_clicks.h" #include "../osrs_inventory_actions.h" +#include "../osrs_policy.h" #include "../osrs_interaction.h" #include "../data/npc_models.h" #include diff --git a/ocean/osrs/encounters/encounter_nh_pvp.h b/ocean/osrs/encounters/encounter_nh_pvp.h index a9b40c6dd0..7c7e12bc93 100644 --- a/ocean/osrs/encounters/encounter_nh_pvp.h +++ b/ocean/osrs/encounters/encounter_nh_pvp.h @@ -5,16 +5,16 @@ #include "../osrs_encounter_visual_events.h" #include "../osrs_env.h" -/* order must match the HEAD_* indices in osrs_types.h */ -static const int NH_PVP_ACTION_DIMS[] = { - LOADOUT_DIM, COMBAT_DIM, OVERHEAD_DIM, - FOOD_DIM, POTION_DIM, KARAMBWAN_DIM, VENG_DIM, OFFENSIVE_DIM, MOVE_DIM -}; +#define NH_PVP_TARGET_SLOTS 1 +#define NH_PVP_ACTION_MASK_SIZE \ + OSRS_BASE_ACTION_MASK_SIZE(NH_PVP_TARGET_SLOTS) +#define NH_PVP_ACTION_DIMS_INIT OSRS_BASE_ACTION_DIMS_INIT(NH_PVP_TARGET_SLOTS) +static const int NH_PVP_ACTION_DIMS[OSRS_BASE_NUM_ACTION_HEADS] = + NH_PVP_ACTION_DIMS_INIT; typedef struct { OsrsEnv env; } NhPvpState; -static_assert(sizeof(NhPvpState) == 7160, "NhPvpState serialized layout"); typedef struct { const CollisionMap* collision_map; @@ -22,38 +22,126 @@ typedef struct { OsrsActorRouteCache player_route_cache[NUM_AGENTS]; } NhPvpContext; -static void nh_pvp_translate_human_input(HumanInput* hi, int* actions, Player* agent, Player* target) { - for (int h = 0; h < NUM_ACTION_HEADS; h++) actions[h] = 0; - actions[HEAD_LOADOUT] = LOADOUT_KEEP; +static int nh_pvp_find_consumable_cell( + const Player* player, + OsrsConsumableKind kind +) { + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&player->inventory_cells[cell]); + if (metadata->consumable_kind == kind) return cell; + } + return -1; +} + +static void nh_pvp_translate_inventory_cell( + int* actions, + const Player* player, + int cell +) { + if (cell < 0 || cell >= OSRS_INVENTORY_SIZE) return; + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&player->inventory_cells[cell]); + int click_action = cell + 1; + if (metadata->click_action == OSRS_CLICK_EQUIP) { + if (metadata->gear_slot >= 0 && metadata->gear_slot < NUM_GEAR_SLOTS) + actions[OSRS_HEAD_EQUIP_SLOT(metadata->gear_slot)] = click_action; + } else if (metadata->click_action == OSRS_CLICK_EAT) { + actions[OSRS_HEAD_EAT] = click_action; + } else if (metadata->click_action == OSRS_CLICK_DRINK) { + actions[OSRS_HEAD_DRINK] = click_action; + } +} + +static void nh_pvp_translate_human_input( + HumanInput* hi, + int* actions, + Player* agent, + Player* target +) { + memset(actions, 0, OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); + if (hi->pending_move_x >= 0 && hi->pending_move_y >= 0) { + int dx = hi->pending_move_x - agent->x; + int dy = hi->pending_move_y - agent->y; + if (dx < -2) dx = -2; + if (dx > 2) dx = 2; + if (dy < -2) dy = -2; + if (dy > 2) dy = 2; + for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + if (ENCOUNTER_MOVE_TARGET_DX[action] == dx && + ENCOUNTER_MOVE_TARGET_DY[action] == dy) { + actions[OSRS_HEAD_PRIMARY] = action; + break; + } + } + } + encounter_translate_prayer(hi, actions, OSRS_HEAD_OVERHEAD); + encounter_translate_offensive_prayer(hi, actions, OSRS_HEAD_OFFENSIVE); if (hi->pending_attack) { - if (hi->pending_spell == ATTACK_ICE) actions[HEAD_COMBAT] = ATTACK_ICE; - else if (hi->pending_spell == ATTACK_BLOOD) actions[HEAD_COMBAT] = ATTACK_BLOOD; - else actions[HEAD_COMBAT] = ATTACK_ATK; + actions[OSRS_HEAD_PRIMARY] = OSRS_PRIMARY_MOVE_ACTIONS; + if (hi->pending_spell == PVP_ATTACK_ICE) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_ICE_BARRAGE; + else if (hi->pending_spell == PVP_ATTACK_BLOOD) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_BLOOD_BARRAGE; + } + if (hi->pending_food) { + int cell = nh_pvp_find_consumable_cell( + agent, OSRS_CONSUMABLE_SHARK_FOOD); + if (cell >= 0) actions[OSRS_HEAD_EAT] = cell + 1; } - encounter_translate_prayer(hi, actions, HEAD_OVERHEAD); - encounter_translate_offensive_prayer(hi, actions, HEAD_OFFENSIVE); - - if (hi->pending_food) actions[HEAD_FOOD] = FOOD_EAT; - if (hi->pending_potion > 0) actions[HEAD_POTION] = hi->pending_potion; - if (hi->pending_karambwan) actions[HEAD_KARAMBWAN] = KARAM_EAT; - if (hi->pending_veng) actions[HEAD_VENG] = VENG_CAST; - if (hi->pending_spec) { - AttackStyle style = (AttackStyle)get_item_attack_style(agent->equipped[GEAR_SLOT_WEAPON]); - if (style == ATTACK_STYLE_MELEE) actions[HEAD_LOADOUT] = LOADOUT_SPEC_MELEE; - else if (style == ATTACK_STYLE_RANGED) actions[HEAD_LOADOUT] = LOADOUT_SPEC_RANGE; - else if (style == ATTACK_STYLE_MAGIC) actions[HEAD_LOADOUT] = LOADOUT_SPEC_MAGIC; + if (hi->pending_karambwan) { + int cell = nh_pvp_find_consumable_cell( + agent, OSRS_CONSUMABLE_KARAMBWAN); + if (cell >= 0) actions[OSRS_HEAD_EAT] = cell + 1; } + OsrsConsumableKind potion_kind = OSRS_CONSUMABLE_NONE; + if (hi->pending_potion == POTION_BREW) + potion_kind = OSRS_CONSUMABLE_BREW; + else if (hi->pending_potion == POTION_RESTORE) + potion_kind = OSRS_CONSUMABLE_SUPER_RESTORE; + else if (hi->pending_potion == POTION_COMBAT) + potion_kind = OSRS_CONSUMABLE_SUPER_COMBAT; + else if (hi->pending_potion == POTION_RANGED) + potion_kind = OSRS_CONSUMABLE_RANGING; + if (potion_kind != OSRS_CONSUMABLE_NONE) { + int cell = nh_pvp_find_consumable_cell(agent, potion_kind); + if (cell >= 0) actions[OSRS_HEAD_DRINK] = cell + 1; + } + for (int i = 0; i < hi->commands.count; i++) { + const HumanCommand* command = &hi->commands.items[i]; + if (command->kind == HUMAN_COMMAND_EQUIP_INVENTORY_ITEM || + command->kind == HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK || + command->kind == HUMAN_COMMAND_EAT || + command->kind == HUMAN_COMMAND_DRINK) { + nh_pvp_translate_inventory_cell( + actions, agent, command->inventory_slot); + } + } + if (hi->pending_veng) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_VENGEANCE; + if (hi->pending_spec) + actions[OSRS_HEAD_SPECIAL] = agent->spec_armed ? 2 : 1; (void)target; } -static EncounterState* nh_pvp_create(void) { - NhPvpState* s = (NhPvpState*)calloc(1, sizeof(NhPvpState)); +static void nh_pvp_init_state( + EncounterState* state, + EncounterContext* context +) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + memset(s, 0, sizeof(*s)); pvp_init(&s->env); - s->env.ocean_io.agent_obs = s->env._obs_buf; s->env.ocean_io.agent_actions = s->env._acts_buf; s->env.ocean_io.agent_rewards = s->env._rews_buf; s->env.ocean_io.agent_terminals = s->env._terms_buf; +} + +static EncounterState* nh_pvp_create(void) { + NhPvpState* s = (NhPvpState*)malloc(sizeof(NhPvpState)); + if (!s) abort(); + nh_pvp_init_state((EncounterState*)s, NULL); return (EncounterState*)s; } @@ -101,7 +189,8 @@ static void nh_pvp_step(EncounterState* state, EncounterContext* context, const NhPvpContext* ctx = (NhPvpContext*)context; NhPvpState* s = (NhPvpState*)state; encounter_arena_topology_require_finalized(ctx->route_topology); - memcpy(s->env.ocean_io.agent_actions, actions, NUM_ACTION_HEADS * sizeof(int)); + memcpy(s->env.ocean_io.agent_actions, actions, + OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); pvp_step(&s->env, ctx->route_topology, ctx->player_route_cache); } @@ -142,10 +231,14 @@ static void nh_pvp_step_human_commands( hi->pending_spec = 0; } -static void nh_pvp_write_obs(EncounterState* state, EncounterContext* context, float* obs_out) { +static void nh_pvp_write_obs( + EncounterState* state, + EncounterContext* context, + float* obs_out +) { (void)context; NhPvpState* s = (NhPvpState*)state; - memcpy(obs_out, s->env._obs_buf, SLOT_NUM_OBSERVATIONS * sizeof(float)); + pvp_write_observations(obs_out, &s->env, 0); } static void nh_pvp_write_mask( @@ -153,11 +246,9 @@ static void nh_pvp_write_mask( EncounterContext* context, float* mask_out ) { - (void)context; NhPvpState* s = (NhPvpState*)state; - for (int i = 0; i < ACTION_MASK_SIZE; i++) { - mask_out[i] = (float)s->env._masks_buf[i]; - } + NhPvpContext* ctx = (NhPvpContext*)context; + pvp_write_action_mask(mask_out, &s->env, 0, ctx->route_topology); } static float nh_pvp_get_reward(EncounterState* state, EncounterContext* context) { @@ -289,14 +380,15 @@ static int nh_pvp_get_winner(EncounterState* state, EncounterContext* context) { static const EncounterDef ENCOUNTER_NH_PVP = { .name = "nh_pvp", - .obs_size = SLOT_NUM_OBSERVATIONS, - .num_action_heads = NUM_ACTION_HEADS, + .obs_size = NH_PVP_NUM_OBS, + .num_action_heads = OSRS_BASE_NUM_ACTION_HEADS, .action_head_dims = NH_PVP_ACTION_DIMS, - .mask_size = ACTION_MASK_SIZE, + .mask_size = NH_PVP_ACTION_MASK_SIZE, .state_size = sizeof(NhPvpState), .context_size = sizeof(NhPvpContext), .init_context = nh_pvp_init_context, .destroy_context = nh_pvp_destroy_context, + .init_state = nh_pvp_init_state, .finalize_context = nh_pvp_finalize_context, .create = nh_pvp_create, @@ -319,9 +411,9 @@ static const EncounterDef ENCOUNTER_NH_PVP = { .put_ptr = nh_pvp_put_ptr, .translate_human_input = NULL, - .head_move = -1, - .head_prayer = -1, - .head_target = -1, + .head_move = OSRS_HEAD_PRIMARY, + .head_prayer = OSRS_HEAD_OVERHEAD, + .head_target = OSRS_HEAD_PRIMARY, .render_post_tick = NULL, .get_log = nh_pvp_get_log, diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h index 694ca41f30..04f3bde99b 100644 --- a/ocean/osrs/encounters/encounter_zulrah.h +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -6,6 +6,7 @@ #include "../osrs_encounter_visual_events.h" #include "../osrs_interaction.h" #include "../osrs_inventory_actions.h" +#include "../osrs_policy.h" #include "../osrs_types.h" #include "../osrs_items.h" #include "../osrs_combat.h" @@ -111,37 +112,33 @@ static const int ZUL_POSITIONS[ZUL_NUM_POSITIONS][2] = { #define ZUL_PLAYER_RESTORE_DOSES 8 #define ZUL_MAX_TICKS 600 -#define ZUL_NUM_SCALAR_OBS 118 -#define ZUL_NUM_OBS (ZUL_NUM_SCALAR_OBS + \ - OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES + \ - NUM_GEAR_SLOTS * OSRS_EQUIPPED_SELF_OBS_FEATURES) -#define ZUL_NUM_ACTION_HEADS (2 + NUM_GEAR_SLOTS + 2 + 2) +#define ZUL_ENCOUNTER_OBS_SIZE 104 +#define ZUL_OBS_AFTER_SHARED OSRS_SHARED_OBS_SIZE +#define ZUL_NUM_OBS (ZUL_OBS_AFTER_SHARED + ZUL_ENCOUNTER_OBS_SIZE) #define ZUL_OBS_NPC_SLOTS (1 + ZUL_MAX_SNAKELINGS) -#define ZUL_MOVE_DIM ENCOUNTER_MOVE_ACTIONS -#define ZUL_PRIMARY_DIM (ZUL_MOVE_DIM + ZUL_OBS_NPC_SLOTS) -#define ZUL_PRAYER_DIM ENCOUNTER_OVERHEAD_DIM_PVE -#define ZUL_OFFENSIVE_DIM ENCOUNTER_OFFENSIVE_DIM -#define ZUL_INV_CLICK_HEADS (NUM_GEAR_SLOTS + 2) -#define ZUL_INV_CLICK_DIM (OSRS_INVENTORY_SIZE + 1) -#define ZUL_SPEC_DIM 3 - -#define ZUL_ACTION_MASK_SIZE (ZUL_PRIMARY_DIM + ZUL_PRAYER_DIM + \ - ZUL_INV_CLICK_HEADS * ZUL_INV_CLICK_DIM + ZUL_SPEC_DIM + ZUL_OFFENSIVE_DIM) - -#define ZUL_HEAD_PRIMARY 0 -#define ZUL_HEAD_PRAYER 1 -#define ZUL_HEAD_EQUIP_BASE 2 -#define ZUL_HEAD_EQUIP_SLOT(slot) (ZUL_HEAD_EQUIP_BASE + (slot)) -#define ZUL_HEAD_EAT (ZUL_HEAD_EQUIP_BASE + NUM_GEAR_SLOTS) -#define ZUL_HEAD_DRINK (ZUL_HEAD_EAT + 1) -#define ZUL_HEAD_SPEC (ZUL_HEAD_DRINK + 1) -#define ZUL_HEAD_OFFENSIVE (ZUL_HEAD_SPEC + 1) - -#define ZUL_PRIMARY_ATTACK_BASE ZUL_MOVE_DIM +#define ZUL_NUM_ACTION_HEADS OSRS_BASE_NUM_ACTION_HEADS +#define ZUL_PRIMARY_DIM OSRS_PRIMARY_DIM(ZUL_OBS_NPC_SLOTS) +#define ZUL_ACTION_MASK_SIZE OSRS_BASE_ACTION_MASK_SIZE(ZUL_OBS_NPC_SLOTS) + +#define ZUL_HEAD_PRIMARY OSRS_HEAD_PRIMARY +#define ZUL_HEAD_OVERHEAD OSRS_HEAD_OVERHEAD +#define ZUL_HEAD_EQUIP_BASE OSRS_HEAD_EQUIP_BASE +#define ZUL_HEAD_EQUIP_SLOT(slot) OSRS_HEAD_EQUIP_SLOT(slot) +#define ZUL_HEAD_EAT OSRS_HEAD_EAT +#define ZUL_HEAD_DRINK OSRS_HEAD_DRINK +#define ZUL_HEAD_SPELL OSRS_HEAD_SPELL +#define ZUL_HEAD_SPECIAL OSRS_HEAD_SPECIAL +#define ZUL_HEAD_OFFENSIVE OSRS_HEAD_OFFENSIVE + +#define ZUL_PRIMARY_ATTACK_BASE OSRS_PRIMARY_MOVE_ACTIONS #define ZUL_MOVE_STAY 0 +static_assert(ZUL_NUM_OBS == 205, "Zulrah observation width"); +static_assert(ZUL_NUM_ACTION_HEADS == 18, "Zulrah action head count"); +static_assert(ZUL_ACTION_MASK_SIZE == 427, "Zulrah action mask width"); + typedef enum { ZUL_FORM_GREEN = 0, ZUL_FORM_RED, @@ -295,18 +292,9 @@ static const int ZUL_ROT_LENGTHS[ZUL_NUM_ROTATIONS] = { 11, 11, 12, 13 }; #undef ZA #undef ZE -#define ZUL_ACTION_DIMS_INIT { \ - ZUL_PRIMARY_DIM, \ - ZUL_PRAYER_DIM, \ - ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, \ - ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, \ - ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, ZUL_INV_CLICK_DIM, \ - ZUL_INV_CLICK_DIM, \ - ZUL_INV_CLICK_DIM, \ - ZUL_SPEC_DIM, \ - ZUL_OFFENSIVE_DIM \ -} -static const int ZUL_ACTION_HEAD_DIMS[ZUL_NUM_ACTION_HEADS] = ZUL_ACTION_DIMS_INIT; +#define ZUL_ACTION_DIMS_INIT OSRS_BASE_ACTION_DIMS_INIT(ZUL_OBS_NPC_SLOTS) +static const int ZUL_ACTION_HEAD_DIMS[ZUL_NUM_ACTION_HEADS] = + ZUL_ACTION_DIMS_INIT; static_assert(ZUL_HEAD_OFFENSIVE == ZUL_NUM_ACTION_HEADS - 1, "OFFENSIVE must be the last zulrah action head"); @@ -450,7 +438,6 @@ typedef struct { ZulrahPendingCloud pending_clouds[ZUL_MAX_PENDING_CLOUDS]; ZulrahSnakeling snakelings[ZUL_MAX_SNAKELINGS]; - OsrsInventoryCell inventory_cells[OSRS_INVENTORY_SIZE]; OsrsInteraction interaction; int player_dest_x, player_dest_y; int player_dest_explicit; @@ -529,7 +516,7 @@ typedef struct { Log log; } ZulrahState; -static_assert(sizeof(ZulrahState) == 12600, "ZulrahState serialized layout"); +static_assert(sizeof(ZulrahState) == 12160, "ZulrahState serialized layout"); static void zul_set_npc_anim_event(ZulrahState* s, int anim_id, int duration_ticks) { osrs_npc_primary_anim_event_set( @@ -705,6 +692,10 @@ static inline int zul_topology_player_tile_allowed( typedef struct { EncounterArenaTopology* topology; + OsrsLocalMoveRoute + local_move_routes[ZUL_ARENA_SIZE * ZUL_ARENA_SIZE] + [OSRS_PRIMARY_MOVE_ACTIONS]; + int local_move_routes_ready; } ZulRouteTopologyOwner; typedef struct { @@ -734,6 +725,72 @@ static uint32_t zul_route_topology_los_flags(void* data, int x, int y) { (void)y; return 0; } +static void zul_local_move_routes_build(ZulRouteTopologyOwner* owner) { + const EncounterArenaTopology* topology = owner->topology; + for (int x = 0; x < ZUL_ARENA_SIZE; x++) { + for (int y = 0; y < ZUL_ARENA_SIZE; y++) { + int source_index = + encounter_arena_topology_index_raw(topology, x, y); + for (int action = 1; + action < OSRS_PRIMARY_MOVE_ACTIONS; + action++) { + EncounterRouteInput input = { + .topology = topology, + .source_x = x, + .source_y = y, + .actor_size = 1, + .target_x = x + ENCOUNTER_MOVE_TARGET_DX[action], + .target_y = y + ENCOUNTER_MOVE_TARGET_DY[action], + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + }; + EncounterRouteResult route = + encounter_route_solve(&input); + owner->local_move_routes[source_index][action] = + (OsrsLocalMoveRoute){ + .destination_dx = + (int16_t)(route.destination_x - x), + .destination_dy = + (int16_t)(route.destination_y - y), + .first_dx = (int8_t)route.first_dx, + .first_dy = (int8_t)route.first_dy, + .run_dx = (int8_t)route.run_dx, + .run_dy = (int8_t)route.run_dy, + .distance = route.distance, + .outcome = (uint8_t)route.outcome, + }; + } + } + } + owner->local_move_routes_ready = 1; +} + +static int zul_apply_local_move_route(Player* player, int action) { + if (!zul_route_topology_owner.local_move_routes_ready || + action <= 0 || action >= OSRS_PRIMARY_MOVE_ACTIONS || + !encounter_arena_topology_contains( + zul_route_topology_owner.topology, + player->x, player->y)) + abort(); + int source_index = encounter_arena_topology_index_raw( + zul_route_topology_owner.topology, player->x, player->y); + const OsrsLocalMoveRoute* cached = + &zul_route_topology_owner.local_move_routes[source_index][action]; + EncounterRouteResult route = { + .outcome = (EncounterRouteOutcome)cached->outcome, + .destination_x = player->x + cached->destination_dx, + .destination_y = player->y + cached->destination_dy, + .first_dx = cached->first_dx, + .first_dy = cached->first_dy, + .run_dx = cached->run_dx, + .run_dy = cached->run_dy, + .distance = cached->distance, + }; + return osrs_player_step_apply_route(player, &route); +} + static void zul_finalize_route_topology(ZulrahContext* ctx) { @@ -769,6 +826,8 @@ static void zul_finalize_route_topology(ZulrahContext* ctx) { "zulrah"); } ctx->route_topology = zul_route_topology_owner.topology; + if (!zul_route_topology_owner.local_move_routes_ready) + zul_local_move_routes_build(&zul_route_topology_owner); } static void zul_finalize_context( @@ -784,12 +843,6 @@ static inline int zul_player_in_cloud(int cx, int cy, int px, int py) { py >= cy && py < cy + ZUL_CLOUD_SIZE; } -static int zul_active_cloud_count(const ZulrahState* s) { - int count = 0; - for (int i = 0; i < ZUL_MAX_CLOUDS; i++) - count += s->clouds[i].active ? 1 : 0; - return count; -} static int zul_pending_cloud_count(const ZulrahState* s) { int count = 0; @@ -797,6 +850,22 @@ static int zul_pending_cloud_count(const ZulrahState* s) { count += s->pending_clouds[i].delay > 0 ? 1 : 0; return count; } +typedef struct { + int active_count; + int pending_count; + int active_dx; + int active_dy; + int escape_dx; + int escape_dy; + int pending_dx; + int pending_dy; + int pending_delay; + int active_overlaps_stand; + int active_overlaps_stall; + int pending_overlaps_stand; + int pending_overlaps_stall; + uint32_t unsafe_move_actions; +} ZulrahCloudObservationSummary; static int zul_rect_signed_delta_1d(int p, int lo, int size) { int hi = lo + size - 1; @@ -805,7 +874,14 @@ static int zul_rect_signed_delta_1d(int p, int lo, int size) { return 0; } -static void zul_cloud_escape_delta(int cx, int cy, int px, int py, int* dx, int* dy) { +static void zul_cloud_escape_delta( + int cx, + int cy, + int px, + int py, + int* dx, + int* dy +) { int west = cx - 1 - px; int east = cx + ZUL_CLOUD_SIZE - px; int south = cy - 1 - py; @@ -831,129 +907,96 @@ static void zul_cloud_escape_delta(int cx, int cy, int px, int py, int* dx, int* *dy = best_dy; } -static int zul_nearest_active_cloud_features( +static ZulrahCloudObservationSummary zul_cloud_observation_summary( const ZulrahState* s, - int* signed_dx, - int* signed_dy, - int* escape_dx, - int* escape_dy + int stand_id, + int stall_id ) { - int found = 0; - int best_dist = 0; - *signed_dx = 0; - *signed_dy = 0; - *escape_dx = 0; - *escape_dy = 0; + ZulrahCloudObservationSummary summary = {0}; + int active_nearest_found = 0; + int active_nearest_locked = 0; + int active_best_dist = 0; for (int i = 0; i < ZUL_MAX_CLOUDS; i++) { - if (!s->clouds[i].active) continue; - int dx = zul_rect_signed_delta_1d(s->player.x, s->clouds[i].x, ZUL_CLOUD_SIZE); - int dy = zul_rect_signed_delta_1d(s->player.y, s->clouds[i].y, ZUL_CLOUD_SIZE); + const ZulrahCloud* cloud = &s->clouds[i]; + if (!cloud->active) continue; + summary.active_count++; + if (!summary.active_overlaps_stand && + stand_id < ZUL_NUM_STAND_LOCATIONS) { + summary.active_overlaps_stand = zul_cloud_overlaps_safe_area( + cloud->x, cloud->y, stand_id, ZUL_STAND_NONE); + } + if (!summary.active_overlaps_stall && + stall_id < ZUL_NUM_STAND_LOCATIONS) { + summary.active_overlaps_stall = zul_cloud_overlaps_safe_area( + cloud->x, cloud->y, stall_id, ZUL_STAND_NONE); + } + for (int action = 0; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + int x = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action]; + int y = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action]; + if (zul_player_in_cloud(cloud->x, cloud->y, x, y)) + summary.unsafe_move_actions |= UINT32_C(1) << action; + } + if (active_nearest_locked) continue; + int dx = zul_rect_signed_delta_1d( + s->player.x, cloud->x, ZUL_CLOUD_SIZE); + int dy = zul_rect_signed_delta_1d( + s->player.y, cloud->y, ZUL_CLOUD_SIZE); int dist = max_int(abs_int(dx), abs_int(dy)); int inside = zul_player_in_cloud( - s->clouds[i].x, s->clouds[i].y, s->player.x, s->player.y); - if (!found || inside || dist < best_dist) { - found = 1; - best_dist = dist; - *signed_dx = dx; - *signed_dy = dy; - if (inside) + cloud->x, cloud->y, s->player.x, s->player.y); + if (!active_nearest_found || inside || dist < active_best_dist) { + active_nearest_found = 1; + active_best_dist = dist; + summary.active_dx = dx; + summary.active_dy = dy; + if (inside) { zul_cloud_escape_delta( - s->clouds[i].x, s->clouds[i].y, - s->player.x, s->player.y, escape_dx, escape_dy); - else { - *escape_dx = 0; - *escape_dy = 0; + cloud->x, cloud->y, s->player.x, s->player.y, + &summary.escape_dx, &summary.escape_dy); + active_nearest_locked = 1; + } else { + summary.escape_dx = 0; + summary.escape_dy = 0; } - if (inside) break; } } - return found; -} -static int zul_nearest_pending_cloud_features( - const ZulrahState* s, - int* dx, - int* dy, - int* delay -) { - int found = 0; - int best_dist = 0; - *dx = 0; - *dy = 0; - *delay = 0; + int pending_nearest_found = 0; + int pending_best_dist = 0; for (int i = 0; i < ZUL_MAX_PENDING_CLOUDS; i++) { - if (s->pending_clouds[i].delay <= 0) continue; - int pdx = s->pending_clouds[i].x - s->player.x; - int pdy = s->pending_clouds[i].y - s->player.y; - int dist = max_int(abs_int(pdx), abs_int(pdy)); - if (!found || dist < best_dist) { - found = 1; - best_dist = dist; - *dx = pdx; - *dy = pdy; - *delay = s->pending_clouds[i].delay; + const ZulrahPendingCloud* cloud = &s->pending_clouds[i]; + if (cloud->delay <= 0) continue; + summary.pending_count++; + if (!summary.pending_overlaps_stand && + stand_id < ZUL_NUM_STAND_LOCATIONS) { + summary.pending_overlaps_stand = zul_cloud_overlaps_safe_area( + cloud->x, cloud->y, stand_id, ZUL_STAND_NONE); + } + if (!summary.pending_overlaps_stall && + stall_id < ZUL_NUM_STAND_LOCATIONS) { + summary.pending_overlaps_stall = zul_cloud_overlaps_safe_area( + cloud->x, cloud->y, stall_id, ZUL_STAND_NONE); + } + if (cloud->delay <= 1) { + for (int action = 0; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + int x = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action]; + int y = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action]; + if (zul_player_in_cloud(cloud->x, cloud->y, x, y)) + summary.unsafe_move_actions |= UINT32_C(1) << action; + } + } + int dx = cloud->x - s->player.x; + int dy = cloud->y - s->player.y; + int dist = max_int(abs_int(dx), abs_int(dy)); + if (!pending_nearest_found || dist < pending_best_dist) { + pending_nearest_found = 1; + pending_best_dist = dist; + summary.pending_dx = dx; + summary.pending_dy = dy; + summary.pending_delay = cloud->delay; } } - return found; -} - -static int zul_active_cloud_overlaps_safe(const ZulrahState* s, int safe_id) { - if (safe_id >= ZUL_NUM_STAND_LOCATIONS) return 0; - for (int i = 0; i < ZUL_MAX_CLOUDS; i++) { - if (!s->clouds[i].active) continue; - if (zul_cloud_overlaps_safe_area( - s->clouds[i].x, s->clouds[i].y, safe_id, ZUL_STAND_NONE)) - return 1; - } - return 0; -} - -static int zul_pending_cloud_overlaps_safe(const ZulrahState* s, int safe_id) { - if (safe_id >= ZUL_NUM_STAND_LOCATIONS) return 0; - for (int i = 0; i < ZUL_MAX_PENDING_CLOUDS; i++) { - if (s->pending_clouds[i].delay <= 0) continue; - if (zul_cloud_overlaps_safe_area( - s->pending_clouds[i].x, s->pending_clouds[i].y, - safe_id, ZUL_STAND_NONE)) - return 1; - } - return 0; -} - -static int zul_tile_in_active_cloud(const ZulrahState* s, int x, int y) { - for (int i = 0; i < ZUL_MAX_CLOUDS; i++) { - if (!s->clouds[i].active) continue; - if (zul_player_in_cloud(s->clouds[i].x, s->clouds[i].y, x, y)) - return 1; - } - return 0; -} - -static int zul_tile_in_pending_cloud_by_delay( - const ZulrahState* s, - int x, - int y, - int max_delay -) { - for (int i = 0; i < ZUL_MAX_PENDING_CLOUDS; i++) { - if (s->pending_clouds[i].delay <= 0 || - s->pending_clouds[i].delay > max_delay) - continue; - if (zul_player_in_cloud(s->pending_clouds[i].x, s->pending_clouds[i].y, x, y)) - return 1; - } - return 0; -} - -static int zul_move_action_cloud_unsafe(const ZulrahState* s, int action) { - if (action < 0 || action >= ZUL_MOVE_DIM) { - fprintf(stderr, "zulrah move action out of range for cloud obs: %d\n", action); - abort(); - } - int x = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action]; - int y = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action]; - return zul_tile_in_active_cloud(s, x, y) || - zul_tile_in_pending_cloud_by_delay(s, x, y, 1); + return summary; } static int zul_form_npc_id(ZulrahForm f) { @@ -1871,7 +1914,7 @@ static void zul_apply_eat_cell(ZulrahState* s, int cell_idx, OsrsConsumableKind abort(); } OsrsPlayerEatResult r = osrs_player_eat_food_effects(&s->player, type); - if (r.consumed) osrs_inventory_cell_consume_eat(&s->inventory_cells[cell_idx]); + if (r.consumed) osrs_inventory_cell_consume_eat(&s->player.inventory_cells[cell_idx]); } static void zul_apply_drink_one_dose_effect(void* ctx, OsrsConsumableKind kind) { @@ -1911,7 +1954,7 @@ static void zul_apply_drink_cell( ZulrahState* s, int cell_idx, OsrsInventoryClickResolution resolution ) { (void)osrs_inventory_cell_consume_drink_one_dose( - &s->inventory_cells[cell_idx], resolution, &s->player.potion_timer, + &s->player.inventory_cells[cell_idx], resolution, &s->player.potion_timer, zul_apply_drink_one_dose_effect, s); } @@ -2008,21 +2051,34 @@ static void zul_write_obs(EncounterState* state, EncounterContext* context, floa ZulrahState* s = (ZulrahState*)state; memset(obs, 0, ZUL_NUM_OBS * sizeof(float)); int i = 0; - - obs[i++] = (float)s->player.current_hitpoints / s->player.base_hitpoints; - obs[i++] = (float)s->player.current_prayer / s->player.base_prayer; - obs[i++] = (float)s->player.x / ZUL_ARENA_SIZE; - obs[i++] = (float)s->player.y / ZUL_ARENA_SIZE; - obs[i++] = (float)s->player.attack_timer / 5.0f; - obs[i++] = (float)s->player.food_timer / 3.0f; - obs[i++] = (float)s->player.potion_timer / 3.0f; - obs[i++] = (s->player.prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; - obs[i++] = (s->player.prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; - obs[i++] = (s->player.prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; - obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_PIETY) ? 1.0f : 0.0f; - obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) ? 1.0f : 0.0f; - obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; - obs[i++] = (float)s->player_stunned_ticks / ZUL_MELEE_STUN_TICKS; + const EncounterLoadoutStats* loadout_stats = zul_live_stats(s); + OsrsSharedObservationInput shared_input = { + .player = &s->player, + .interaction = &s->interaction, + .arena_min_x = 0, + .arena_max_x = ZUL_ARENA_SIZE, + .arena_min_y = 0, + .arena_max_y = ZUL_ARENA_SIZE, + .attack_style = loadout_stats->style, + .attack_range = loadout_stats->attack_range, + .max_hit = loadout_stats->max_hit, + .attack_speed = loadout_stats->attack_speed, + .defence_stab = loadout_stats->def_stab, + .defence_slash = loadout_stats->def_slash, + .defence_crush = loadout_stats->def_crush, + .defence_magic = loadout_stats->def_magic, + .defence_ranged = loadout_stats->def_ranged, + .effective_level = loadout_stats->eff_level, + .attack_bonus = loadout_stats->attack_bonus, + .strength_bonus = loadout_stats->strength_bonus, + .spell_base_damage = loadout_stats->spell_base_damage, + .special_attack_cost = + osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]), + }; + i += osrs_write_shared_observations(&obs[i], &shared_input); + assert(i == ZUL_OBS_AFTER_SHARED); + obs[i++] = + (float)s->player_stunned_ticks / ZUL_MELEE_STUN_TICKS; obs[i++] = (float)s->zulrah.current_hitpoints / MONSTER_DATABASE[MON_ZULRAH_GREEN].hp; obs[i++] = (float)(s->zulrah.x - s->player.x) / ZUL_ARENA_SIZE; @@ -2062,7 +2118,6 @@ static void zul_write_obs(EncounterState* state, EncounterContext* context, floa obs[i++] = s->damage_received_this_tick / 50.0f; obs[i++] = s->total_damage_dealt / MONSTER_DATABASE[MON_ZULRAH_GREEN].hp; - obs[i++] = (float)s->player.special_energy / 100.0f; obs[i++] = (s->antivenom_timer > 0) ? 1.0f : 0.0f; obs[i++] = (float)s->antivenom_timer / ZUL_ANTIVENOM_DURATION; obs[i++] = (float)s->gear_tier / (ZUL_NUM_GEAR_TIERS - 1); @@ -2081,160 +2136,148 @@ static void zul_write_obs(EncounterState* state, EncounterContext* context, floa obs[i++] = 0.0f; obs[i++] = 0.0f; } - int active_dx = 0; - int active_dy = 0; - int escape_dx = 0; - int escape_dy = 0; - zul_nearest_active_cloud_features( - s, &active_dx, &active_dy, &escape_dx, &escape_dy); - int pending_dx = 0; - int pending_dy = 0; - int pending_delay = 0; - zul_nearest_pending_cloud_features( - s, &pending_dx, &pending_dy, &pending_delay); - obs[i++] = (escape_dx != 0 || escape_dy != 0) ? 1.0f : 0.0f; - obs[i++] = (float)active_dx / (float)ZUL_ARENA_SIZE; - obs[i++] = (float)active_dy / (float)ZUL_ARENA_SIZE; - obs[i++] = (float)escape_dx / (float)ZUL_ARENA_SIZE; - obs[i++] = (float)escape_dy / (float)ZUL_ARENA_SIZE; - obs[i++] = (float)zul_active_cloud_count(s) / (float)ZUL_MAX_CLOUDS; - obs[i++] = (float)zul_pending_cloud_count(s) / (float)ZUL_MAX_PENDING_CLOUDS; - obs[i++] = (float)pending_dx / (float)ZUL_ARENA_SIZE; - obs[i++] = (float)pending_dy / (float)ZUL_ARENA_SIZE; - obs[i++] = (float)pending_delay / (float)ZUL_CLOUD_FLIGHT_2; - obs[i++] = zul_active_cloud_overlaps_safe(s, phase->stand) ? 1.0f : 0.0f; - obs[i++] = zul_active_cloud_overlaps_safe(s, phase->stall) ? 1.0f : 0.0f; - obs[i++] = zul_pending_cloud_overlaps_safe(s, phase->stand) ? 1.0f : 0.0f; - obs[i++] = zul_pending_cloud_overlaps_safe(s, phase->stall) ? 1.0f : 0.0f; - for (int m = 0; m < ZUL_MOVE_DIM; m++) - obs[i++] = zul_move_action_cloud_unsafe(s, m) ? 1.0f : 0.0f; - - static const float ZUL_ZERO_POST_USE_DELTAS[6] = {0}; - for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { - osrs_write_inventory_cell_affordance_features( - &obs[i], - &s->inventory_cells[cell], - osrs_inventory_cell_holds_equipped_item( - &s->player, s->inventory_cells, cell), - ZUL_ZERO_POST_USE_DELTAS, - s->player.base_hitpoints, - s->player.base_prayer, - s->player.base_attack); - i += OSRS_INVENTORY_CELL_OBS_FEATURES; - } - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - osrs_write_equipped_self_features(&obs[i], s->player.equipped[slot]); - i += OSRS_EQUIPPED_SELF_OBS_FEATURES; + ZulrahCloudObservationSummary cloud_summary = + zul_cloud_observation_summary(s, phase->stand, phase->stall); + obs[i++] = + (cloud_summary.escape_dx != 0 || cloud_summary.escape_dy != 0) + ? 1.0f : 0.0f; + obs[i++] = (float)cloud_summary.active_dx / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)cloud_summary.active_dy / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)cloud_summary.escape_dx / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)cloud_summary.escape_dy / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)cloud_summary.active_count / (float)ZUL_MAX_CLOUDS; + obs[i++] = + (float)cloud_summary.pending_count / (float)ZUL_MAX_PENDING_CLOUDS; + obs[i++] = (float)cloud_summary.pending_dx / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)cloud_summary.pending_dy / (float)ZUL_ARENA_SIZE; + obs[i++] = + (float)cloud_summary.pending_delay / (float)ZUL_CLOUD_FLIGHT_2; + obs[i++] = cloud_summary.active_overlaps_stand ? 1.0f : 0.0f; + obs[i++] = cloud_summary.active_overlaps_stall ? 1.0f : 0.0f; + obs[i++] = cloud_summary.pending_overlaps_stand ? 1.0f : 0.0f; + obs[i++] = cloud_summary.pending_overlaps_stall ? 1.0f : 0.0f; + for (int action = 0; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + obs[i++] = + cloud_summary.unsafe_move_actions & (UINT32_C(1) << action) + ? 1.0f : 0.0f; } + if (i != ZUL_NUM_OBS) { fprintf(stderr, "zulrah obs size mismatch: wrote %d expected %d\n", i, ZUL_NUM_OBS); abort(); } } -static void zul_write_mask(EncounterState* state, EncounterContext* context, float* mask) { +static void zul_write_mask_bytes( + EncounterState* state, + EncounterContext* context, + unsigned char* mask +) { ZulrahContext* ctx = (ZulrahContext*)context; encounter_arena_topology_require_finalized(ctx->route_topology); ZulrahState* s = (ZulrahState*)state; - for (int i = 0; i < ZUL_ACTION_MASK_SIZE; i++) mask[i] = 1.0f; - int off = 0; - - for (int m = 0; m < ZUL_MOVE_DIM; m++) { - if (m > 0) { - if (s->player_stunned_ticks > 0) { mask[off] = 0.0f; } - else { - int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[m]; - int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[m]; - if (!zul_topology_player_tile_allowed(ctx, nx, ny)) - mask[off] = 0.0f; - } + memset(mask, 0, ZUL_ACTION_MASK_SIZE); + + int offset = osrs_base_action_head_mask_offset( + ZUL_OBS_NPC_SLOTS, ZUL_HEAD_PRIMARY); + mask[offset] = 1; + if (s->player_stunned_ticks <= 0) { + for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action]; + int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action]; + mask[offset + action] = + zul_topology_player_tile_allowed(ctx, nx, ny); } - off++; } - mask[off] = s->zulrah_visible && !s->is_diving ? 1.0f : 0.0f; - off++; - for (int n = 0; n < ZUL_MAX_SNAKELINGS; n++) { - mask[off] = s->snakelings[n].active ? 1.0f : 0.0f; - off++; - } - for (int p = 0; p < ZUL_PRAYER_DIM; p++) { - if (p == ENCOUNTER_OVERHEAD_OFF && s->player.prayer == PRAYER_NONE) - mask[off] = 0.0f; - if (p >= ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE && s->player.current_prayer <= 0) - mask[off] = 0.0f; - off++; - } - int cell_equip_slot[OSRS_INVENTORY_SIZE]; - int cell_can_eat[OSRS_INVENTORY_SIZE]; - int cell_can_drink[OSRS_INVENTORY_SIZE]; + mask[offset + ZUL_PRIMARY_ATTACK_BASE] = + s->zulrah_visible && !s->is_diving; + for (int n = 0; n < ZUL_MAX_SNAKELINGS; n++) + mask[offset + ZUL_PRIMARY_ATTACK_BASE + n + 1] = + s->snakelings[n].active; + + offset = osrs_base_action_head_mask_offset( + ZUL_OBS_NPC_SLOTS, ZUL_HEAD_OVERHEAD); + int has_prayer = s->player.current_prayer > 0; + mask[offset + ENCOUNTER_OVERHEAD_NO_CHANGE] = 1; + mask[offset + ENCOUNTER_OVERHEAD_OFF] = + s->player.prayer != PRAYER_NONE; + for (int action = ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; + action < OSRS_OVERHEAD_DIM; + action++) + mask[offset + action] = has_prayer; + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + offset = osrs_base_action_head_mask_offset( + ZUL_OBS_NPC_SLOTS, ZUL_HEAD_EQUIP_SLOT(slot)); + mask[offset] = 1; + } + int eat_offset = osrs_base_action_head_mask_offset( + ZUL_OBS_NPC_SLOTS, ZUL_HEAD_EAT); + int drink_offset = osrs_base_action_head_mask_offset( + ZUL_OBS_NPC_SLOTS, ZUL_HEAD_DRINK); + mask[eat_offset] = 1; + mask[drink_offset] = 1; for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { - cell_equip_slot[cell] = -1; - cell_can_eat[cell] = 0; - cell_can_drink[cell] = 0; - OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( - &s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); - if (r.click_action == OSRS_CLICK_EQUIP) { - if (osrs_can_equip_from_cell(&s->player, s->inventory_cells, cell)) - cell_equip_slot[cell] = - osrs_inventory_cell_metadata( - &s->inventory_cells[cell])->gear_slot; - } else if (r.click_action == OSRS_CLICK_EAT) { - cell_can_eat[cell] = - osrs_can_eat_consumable_kind(&s->player, r.consumable_kind); - } else if (r.click_action == OSRS_CLICK_DRINK) { - cell_can_drink[cell] = + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret( + &s->player.inventory_cells[cell], OSRS_CLICK_TICK_FIRST); + if (resolution.click_action == OSRS_CLICK_EQUIP && + osrs_can_equip_from_cell( + &s->player, s->player.inventory_cells, cell)) { + int gear_slot = osrs_inventory_cell_metadata( + &s->player.inventory_cells[cell])->gear_slot; + if (gear_slot >= 0 && gear_slot < NUM_GEAR_SLOTS) { + offset = osrs_base_action_head_mask_offset( + ZUL_OBS_NPC_SLOTS, ZUL_HEAD_EQUIP_SLOT(gear_slot)); + mask[offset + cell + 1] = 1; + } + } else if (resolution.click_action == OSRS_CLICK_EAT && + osrs_can_eat_consumable_kind( + &s->player, resolution.consumable_kind)) { + mask[eat_offset + cell + 1] = 1; + } else if (resolution.click_action == OSRS_CLICK_DRINK && osrs_inventory_cell_dose_count( - &s->inventory_cells[cell]) > 0 && + &s->player.inventory_cells[cell]) > 0 && s->player.potion_timer == 0 && - zul_drink_has_effect(s, r.consumable_kind); - } - } - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - mask[off] = 1.0f; - off++; - for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { - mask[off] = cell_equip_slot[cell] == slot ? 1.0f : 0.0f; - off++; + zul_drink_has_effect(s, resolution.consumable_kind)) { + mask[drink_offset + cell + 1] = 1; } } - mask[off] = 1.0f; - off++; - for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { - mask[off] = cell_can_eat[cell] ? 1.0f : 0.0f; - off++; - } - mask[off] = 1.0f; - off++; - for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { - mask[off] = cell_can_drink[cell] ? 1.0f : 0.0f; - off++; - } - { - int weapon = s->player.equipped[GEAR_SLOT_WEAPON]; - int weapon_spec_cost = osrs_spec_cost(weapon); - mask[off] = 1.0f; - off++; - mask[off] = weapon_spec_cost > 0 && - (s->player.special_energy >= weapon_spec_cost || s->player.spec_armed) - ? 1.0f : 0.0f; - off++; - mask[off] = s->player.spec_armed ? 1.0f : 0.0f; - off++; - } - for (int o = 0; o < ZUL_OFFENSIVE_DIM; o++) { - if (o == ENCOUNTER_OFFENSIVE_OFF && - s->player.offensive_prayer == OFFENSIVE_PRAYER_NONE) - mask[off] = 0.0f; - if (o >= ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY && s->player.current_prayer <= 0) - mask[off] = 0.0f; - off++; - } - if (off != ZUL_ACTION_MASK_SIZE) { - fprintf(stderr, "zulrah mask size mismatch: wrote %d expected %d\n", - off, ZUL_ACTION_MASK_SIZE); - abort(); - } + + offset = osrs_base_action_head_mask_offset( + ZUL_OBS_NPC_SLOTS, ZUL_HEAD_SPELL); + mask[offset + OSRS_SPELL_NONE] = 1; + + int weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + int weapon_spec_cost = osrs_spec_cost(weapon); + offset = osrs_base_action_head_mask_offset( + ZUL_OBS_NPC_SLOTS, ZUL_HEAD_SPECIAL); + mask[offset] = 1; + mask[offset + 1] = weapon_spec_cost > 0 && + (s->player.special_energy >= weapon_spec_cost || + s->player.spec_armed); + mask[offset + 2] = s->player.spec_armed; + + offset = osrs_base_action_head_mask_offset( + ZUL_OBS_NPC_SLOTS, ZUL_HEAD_OFFENSIVE); + mask[offset + ENCOUNTER_OFFENSIVE_NO_CHANGE] = 1; + mask[offset + ENCOUNTER_OFFENSIVE_OFF] = + s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE; + mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY] = has_prayer; + mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR] = has_prayer; + mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY] = has_prayer; +} + +static void zul_write_mask( + EncounterState* state, + EncounterContext* context, + float* mask +) { + unsigned char byte_mask[ZUL_ACTION_MASK_SIZE]; + zul_write_mask_bytes(state, context, byte_mask); + for (int i = 0; i < ZUL_ACTION_MASK_SIZE; i++) + mask[i] = (float)byte_mask[i]; } static float zul_compute_reward(ZulrahState* s) { @@ -2419,12 +2462,12 @@ static void zul_record_episode_timeout(ZulrahState* s) { static void zul_seed_inventory_cells(ZulrahState* s) { for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) - s->inventory_cells[i] = osrs_inventory_cell_empty(); + s->player.inventory_cells[i] = osrs_inventory_cell_empty(); int cell = 0; for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { uint8_t item = ZUL_RANGE_LOADOUT[s->gear_tier][slot]; if (item == ITEM_NONE || item == s->player.equipped[slot]) continue; - s->inventory_cells[cell++] = osrs_inventory_cell_from_item(item); + s->player.inventory_cells[cell++] = osrs_inventory_cell_from_item(item); } static const struct { uint16_t raw_osrs_id; int count; } ZUL_TRIP_CONSUMABLES[] = { {385, ZUL_PLAYER_FOOD}, @@ -2438,7 +2481,7 @@ static void zul_seed_inventory_cells(ZulrahState* s) { fprintf(stderr, "zulrah trip inventory overflows %d cells\n", OSRS_INVENTORY_SIZE); abort(); } - s->inventory_cells[cell++] = osrs_inventory_cell_from_raw_osrs_id( + s->player.inventory_cells[cell++] = osrs_inventory_cell_from_raw_osrs_id( ZUL_TRIP_CONSUMABLES[k].raw_osrs_id); } } @@ -2566,12 +2609,13 @@ static void zul_step_tick( return; } - zul_process_prayer(s, actions[ZUL_HEAD_PRAYER], actions[ZUL_HEAD_OFFENSIVE]); + zul_process_prayer( + s, actions[ZUL_HEAD_OVERHEAD], actions[ZUL_HEAD_OFFENSIVE]); if (s->human_command_mode) zul_apply_human_player_commands(s); - int spec_act = actions[ZUL_HEAD_SPEC]; + int spec_act = actions[ZUL_HEAD_SPECIAL]; int spec_cost = osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]); if (spec_act == 1 && spec_cost > 0 && s->player.special_energy >= spec_cost) { s->player.spec_armed = 1; @@ -2586,7 +2630,7 @@ static void zul_step_tick( clicks.eat = actions[ZUL_HEAD_EAT]; clicks.drink = actions[ZUL_HEAD_DRINK]; OsrsInventoryTickIntent intent = - osrs_resolve_inventory_tick_intent(&s->player, s->inventory_cells, &clicks); + osrs_resolve_inventory_tick_intent(&s->player, s->player.inventory_cells, &clicks); if (osrs_inventory_tick_intent_has_effect(&intent)) osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EQUIP); OsrsInventoryApplyStep click_step; @@ -2594,7 +2638,7 @@ static void zul_step_tick( switch (click_step.kind) { case OSRS_INVENTORY_APPLY_EQUIP: if (osrs_equip_from_cell( - &s->player, s->inventory_cells, click_step.cell_idx) >= 0) + &s->player, s->player.inventory_cells, click_step.cell_idx) >= 0) zul_mark_live_stats_dirty(s); break; case OSRS_INVENTORY_APPLY_EAT: @@ -2617,6 +2661,7 @@ static void zul_step_tick( } OsrsPlayerCommand command = { .kind = OSRS_PLAYER_CMD_NONE }; + int local_move_action = 0; if (has_new_target) { command.kind = OSRS_PLAYER_CMD_TARGET; command.target_slot = new_target_slot; @@ -2627,11 +2672,12 @@ static void zul_step_tick( s->player_dest_explicit = 0; command.kind = OSRS_PLAYER_CMD_MOVE; command.move_kind = OSRS_PLAYER_MOVE_DESTINATION; - } else if (primary > 0 && primary < ZUL_MOVE_DIM) { + } else if (primary > 0 && primary < OSRS_PRIMARY_MOVE_ACTIONS) { s->player_dest_x = s->player.x + ENCOUNTER_MOVE_TARGET_DX[primary]; s->player_dest_y = s->player.y + ENCOUNTER_MOVE_TARGET_DY[primary]; command.kind = OSRS_PLAYER_CMD_MOVE; command.move_kind = OSRS_PLAYER_MOVE_DESTINATION; + local_move_action = primary; } else { s->player_dest_x = -1; s->player_dest_y = -1; @@ -2660,7 +2706,16 @@ static void zul_step_tick( .los_query = NULL, }, }; - OsrsPlayerStepResult step_result = osrs_encounter_player_step(&step_input); + OsrsPlayerStepResult step_result = {.target_slot = -1}; + if (local_move_action && s->player_stunned_ticks <= 0) { + osrs_interaction_check_interrupt( + &s->interaction, OSRS_IACT_MOVE); + step_result.moved = + zul_apply_local_move_route(&s->player, local_move_action) > 0; + step_result.explicit_moved = step_result.moved; + } else { + step_result = osrs_encounter_player_step(&step_input); + } s->player_moved_this_tick = step_result.moved; s->player_chased_target_this_tick = step_result.chased_target; @@ -2743,7 +2798,7 @@ static int zul_first_cell_with_kind( ) { for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( - &s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); + &s->player.inventory_cells[cell], OSRS_CLICK_TICK_FIRST); if (r.click_action == click && r.consumable_kind == kind) return cell; } return -1; @@ -2752,7 +2807,7 @@ static int zul_first_cell_with_kind( static int zul_cell_with_item(const ZulrahState* s, uint8_t item_idx) { for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) if (osrs_inventory_cell_item_index( - &s->inventory_cells[cell]) == item_idx) return cell; + &s->player.inventory_cells[cell]) == item_idx) return cell; return -1; } @@ -2776,15 +2831,18 @@ static void zul_heuristic_actions(ZulrahState* s, int* actions) { switch (s->current_form) { case ZUL_FORM_GREEN: if (s->player.prayer != PRAYER_PROTECT_RANGED) - actions[ZUL_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; + actions[ZUL_HEAD_OVERHEAD] = + ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; break; case ZUL_FORM_BLUE: if (s->player.prayer != PRAYER_PROTECT_MAGIC) - actions[ZUL_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC; + actions[ZUL_HEAD_OVERHEAD] = + ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC; break; case ZUL_FORM_RED: if (s->player.prayer != PRAYER_PROTECT_MELEE) - actions[ZUL_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; + actions[ZUL_HEAD_OVERHEAD] = + ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; break; } OffensivePrayer target_off = OFFENSIVE_PRAYER_NONE; @@ -2857,7 +2915,7 @@ static void zul_heuristic_actions(ZulrahState* s, int* actions) { int spec_cost = osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]); if (spec_cost > 0 && s->player.special_energy >= spec_cost && !s->player.spec_armed) - actions[ZUL_HEAD_SPEC] = 1; + actions[ZUL_HEAD_SPECIAL] = 1; } } } @@ -3300,7 +3358,7 @@ static void zul_translate_human_commands(HumanInput* hi, int* actions, ZulrahSta s->player_dest_explicit = 0; break; case HUMAN_COMMAND_OVERHEAD_PRAYER: - actions[ZUL_HEAD_PRAYER] = cmd->overhead_prayer; + actions[ZUL_HEAD_OVERHEAD] = cmd->overhead_prayer; break; case HUMAN_COMMAND_OFFENSIVE_PRAYER: actions[ZUL_HEAD_OFFENSIVE] = cmd->offensive_prayer; @@ -3324,7 +3382,7 @@ static void zul_translate_human_commands(HumanInput* hi, int* actions, ZulrahSta break; } case HUMAN_COMMAND_SPEC_TOGGLE: - actions[ZUL_HEAD_SPEC] = s->player.spec_armed ? 2 : 1; + actions[ZUL_HEAD_SPECIAL] = s->player.spec_armed ? 2 : 1; break; case HUMAN_COMMAND_EQUIP_INVENTORY_ITEM: case HUMAN_COMMAND_FIGHT_STYLE: @@ -3407,7 +3465,7 @@ static const EncounterDef ENCOUNTER_ZULRAH = { .arena_width = ZUL_ARENA_SIZE, .arena_height = ZUL_ARENA_SIZE, .head_move = ZUL_HEAD_PRIMARY, - .head_prayer = ZUL_HEAD_PRAYER, + .head_prayer = ZUL_HEAD_OVERHEAD, .head_target = -1, .render_post_tick = zul_render_post_tick, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc index 1e2d0e7025..377dae8fcf 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc @@ -79,6 +79,10 @@ static void inf_shuffle_spawns(InfernoState* s) { encounter_shuffle(s->spawn_order, INF_NUM_SPAWN_POS, &s->rng_state); } static InfernoContext* inf_legacy_context(void); +static const EncounterArenaTopology* inf_route_topology_for_state( + const InfernoContext* ctx, + const InfernoState* state +); static int inf_pillar_footprint_blocked( @@ -97,6 +101,23 @@ static int inf_pillar_footprint_blocked( } return 0; } +static uint8_t inf_active_pillar_mask(const InfernoState* state) { + uint8_t mask = 0; + for (int pillar = 0; pillar < INF_NUM_PILLARS; pillar++) + mask |= (uint8_t)(state->pillars[pillar].active != 0) << pillar; + return mask; +} +static int inf_pillars_at_canonical_positions( + const InfernoState* state +) { + for (int pillar = 0; pillar < INF_NUM_PILLARS; pillar++) + if (state->pillars[pillar].x != INF_PILLAR_POS[pillar][0] || + state->pillars[pillar].y != INF_PILLAR_POS[pillar][1]) + return 0; + return 1; +} + + static uint32_t inf_pillar_los_flags(void* data, int x, int y) { const InfernoState* s = (const InfernoState*)data; @@ -116,9 +137,12 @@ static int inf_footprint_blocked_ctx( int y, int size ) { + const EncounterArenaTopology* topology = + inf_route_topology_for_state(ctx, s); return encounter_arena_topology_footprint_blocked( - ctx->route_topology, x, y, size) || - inf_pillar_footprint_blocked(s, x, y, size); + topology, x, y, size) || + (!inf_pillars_at_canonical_positions(s) && + inf_pillar_footprint_blocked(s, x, y, size)); } static int inf_los_clear_ctx( @@ -132,15 +156,21 @@ static int inf_los_clear_ctx( int target_size, int attack_range ) { - if (!encounter_arena_topology_los_clear( - ctx->route_topology, + if (inf_pillars_at_canonical_positions(s)) + return encounter_arena_topology_los_clear( + inf_route_topology_for_state(ctx, s), actor_x, actor_y, actor_size, target_x, target_y, target_size, - attack_range)) + attack_range); + const EncounterArenaTopology* topology = ctx->route_topology; + if (!encounter_arena_topology_footprint_in_bounds_assume_finalized_size_in_range( + topology, actor_x, actor_y, actor_size) || + !encounter_arena_topology_footprint_in_bounds_assume_finalized_size_in_range( + topology, target_x, target_y, target_size)) return 0; return entity_has_line_of_sight_with_flags( inf_pillar_los_flags, @@ -794,13 +824,15 @@ static InfConfig inf_default_config(void) { } typedef struct { - EncounterArenaTopology* topology; + EncounterArenaTopology* + topologies[1 << INF_NUM_PILLARS]; } InfRouteTopologyOwner; typedef struct { const CollisionMap* collision_map; int world_offset_x; int world_offset_y; + uint8_t pillar_mask; } InfRouteTopologyBuildContext; static InfRouteTopologyOwner inf_route_topology_owner; @@ -824,13 +856,31 @@ static uint32_t inf_route_topology_flags(void* data, int x, int y) { if (x < INF_ARENA_MIN_X || x > INF_ARENA_MAX_X || y < INF_ARENA_MIN_Y || y > INF_ARENA_MAX_Y) return COLLISION_BLOCKED; - return inf_topology_collision_flags(build, x, y); + uint32_t flags = inf_topology_collision_flags(build, x, y); + for (int pillar = 0; pillar < INF_NUM_PILLARS; pillar++) { + if ((build->pillar_mask & (1 << pillar)) == 0) continue; + if (los_aabb_overlap( + x, y, 1, + INF_PILLAR_POS[pillar][0], + INF_PILLAR_POS[pillar][1], + INF_PILLAR_SIZE)) + flags |= COLLISION_BLOCKED | LOS_FULL_MASK; + } + return flags; } static uint32_t inf_route_topology_los_flags(void* data, int x, int y) { - (void)data; - (void)x; - (void)y; + const InfRouteTopologyBuildContext* build = + (const InfRouteTopologyBuildContext*)data; + for (int pillar = 0; pillar < INF_NUM_PILLARS; pillar++) { + if ((build->pillar_mask & (1 << pillar)) == 0) continue; + if (los_aabb_overlap( + x, y, 1, + INF_PILLAR_POS[pillar][0], + INF_PILLAR_POS[pillar][1], + INF_PILLAR_SIZE)) + return LOS_FULL_MASK; + } return 0; } @@ -839,34 +889,51 @@ static void inf_finalize_route_topology(InfernoContext* ctx) { fprintf(stderr, "inferno route topology finalized twice\n"); abort(); } - InfRouteTopologyBuildContext build = { - ctx->collision_map, - ctx->world_offset_x, - ctx->world_offset_y, - }; - EncounterArenaTopologyBuildSpec spec = { - .origin_x = INF_TOPOLOGY_MIN_X, - .origin_y = INF_TOPOLOGY_MIN_Y, - .width = INF_TOPOLOGY_WIDTH, - .height = INF_TOPOLOGY_HEIGHT, - .max_footprint_size = ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE, - .revision = UINT64_C(0x494e4645524e4f02), - .tile_flags = inf_route_topology_flags, - .tile_flags_ctx = &build, - .los_tile_flags = inf_route_topology_los_flags, - .los_tile_flags_ctx = &build, - }; - if (!inf_route_topology_owner.topology) { - inf_route_topology_owner.topology = - encounter_arena_topology_build(&spec); - encounter_arena_topology_finalize(inf_route_topology_owner.topology); - } else { - encounter_arena_topology_require_spec( - inf_route_topology_owner.topology, - &spec, - "inferno"); + for (int pillar_mask = 0; + pillar_mask < (1 << INF_NUM_PILLARS); + pillar_mask++) { + InfRouteTopologyBuildContext build = { + ctx->collision_map, + ctx->world_offset_x, + ctx->world_offset_y, + (uint8_t)pillar_mask, + }; + EncounterArenaTopologyBuildSpec spec = { + .origin_x = INF_TOPOLOGY_MIN_X, + .origin_y = INF_TOPOLOGY_MIN_Y, + .width = INF_TOPOLOGY_WIDTH, + .height = INF_TOPOLOGY_HEIGHT, + .max_footprint_size = + ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE, + .revision = + UINT64_C(0x494e4645524e4f02) + (uint64_t)pillar_mask, + .tile_flags = inf_route_topology_flags, + .tile_flags_ctx = &build, + .los_tile_flags = inf_route_topology_los_flags, + .los_tile_flags_ctx = &build, + }; + EncounterArenaTopology** topology = + &inf_route_topology_owner.topologies[pillar_mask]; + if (!*topology) { + *topology = encounter_arena_topology_build(&spec); + encounter_arena_topology_finalize(*topology); + } else { + encounter_arena_topology_require_spec( + *topology, &spec, "inferno"); + } } - ctx->route_topology = inf_route_topology_owner.topology; + ctx->route_topology = inf_route_topology_owner.topologies[0]; +} + +static const EncounterArenaTopology* inf_route_topology_for_state( + const InfernoContext* ctx, + const InfernoState* state +) { + encounter_arena_topology_require_finalized(ctx->route_topology); + if (!inf_pillars_at_canonical_positions(state)) + return ctx->route_topology; + return inf_route_topology_owner.topologies[ + inf_active_pillar_mask(state)]; } static void inf_finalize_context( diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc index 0863129a56..d186b5698e 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc @@ -47,7 +47,7 @@ static const int INF_SPAWN_POS[INF_NUM_SPAWN_POS][2] = { #define INF_WAVE_ZUK (INF_NUM_WAVES - 1) #define INF_START_WAVE_ZUK_JAD (INF_WAVE_ZUK + 1) #define INF_MAX_PUBLIC_START_WAVE (INF_NUM_WAVES + 2) -#define INF_NUM_ACTION_HEADS 19 +#define INF_NUM_ACTION_HEADS OSRS_BASE_NUM_ACTION_HEADS typedef enum { INF_NPC_NIBBLER = 0, @@ -866,7 +866,6 @@ typedef struct { InfWeaponSet weapon_set; InfLoadoutProfile active_loadout_profile; EncounterLoadoutStats loadout_stats[INF_NUM_WEAPON_SETS]; - OsrsInventoryCell inventory_cells[OSRS_INVENTORY_SIZE]; InfInventoryClickMemo inventory_click_memos[OSRS_INVENTORY_SIZE]; EncounterLoadoutStats live_stats; EncounterLoadoutStats human_loadout_stats; @@ -919,4 +918,4 @@ typedef struct { float total_hp_restored_jad; float total_hp_restored_zuk; } InfernoState; -static_assert(sizeof(InfernoState) == 27132, "InfernoState serialized layout"); +static_assert(sizeof(InfernoState) == 27012, "InfernoState serialized layout"); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc index e4deefa711..85fe267e7b 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -1,5 +1,5 @@ #line 6591 "encounter_inferno.h" -#define INF_PLAYER_OBS_SIZE 45 +#define INF_ENCOUNTER_OBS_SIZE 14 #define INF_PILLAR_FEATURES 3 #define INF_PILLAR_OBS_SIZE (INF_NUM_PILLARS * INF_PILLAR_FEATURES) #define INF_NPC_SLOT_FEATURES 13 @@ -11,33 +11,44 @@ (ENCOUNTER_MAX_PENDING_HITS * INF_FEATURES_PER_HIT) #define INF_PENDING_SPARK_OBS_SIZE \ (INF_SPARK_OBS_SLOTS * INF_FEATURES_PER_SPARK) -#define INF_INVENTORY_OBS_SIZE OSRS_INVENTORY_SIZE -#define INF_EQUIPPED_OBS_SIZE OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES - -#define INF_OBS_AFTER_PLAYER INF_PLAYER_OBS_SIZE -#define INF_OBS_AFTER_PILLARS (INF_OBS_AFTER_PLAYER + INF_PILLAR_OBS_SIZE) -#define INF_OBS_AFTER_NPCS (INF_OBS_AFTER_PILLARS + INF_TOTAL_NPC_OBS_SIZE) +#define INF_INVENTORY_OBS_SIZE OSRS_SHARED_INVENTORY_OBS_SIZE +#define INF_EQUIPPED_OBS_SIZE OSRS_SHARED_EQUIPPED_OBS_SIZE + +#define INF_OBS_AFTER_SHARED OSRS_SHARED_OBS_SIZE +#define INF_OBS_AFTER_ENCOUNTER \ + (INF_OBS_AFTER_SHARED + INF_ENCOUNTER_OBS_SIZE) +#define INF_OBS_AFTER_PILLARS \ + (INF_OBS_AFTER_ENCOUNTER + INF_PILLAR_OBS_SIZE) +#define INF_OBS_AFTER_NPCS \ + (INF_OBS_AFTER_PILLARS + INF_TOTAL_NPC_OBS_SIZE) #define INF_OBS_AFTER_PENDING_HITS \ (INF_OBS_AFTER_NPCS + INF_PENDING_HIT_OBS_SIZE) #define INF_OBS_AFTER_SPARKS \ (INF_OBS_AFTER_PENDING_HITS + INF_PENDING_SPARK_OBS_SIZE) -#define INF_OBS_AFTER_INVENTORY \ - (INF_OBS_AFTER_SPARKS + INF_INVENTORY_OBS_SIZE) -#define INF_OBS_AFTER_EQUIPMENT \ - (INF_OBS_AFTER_INVENTORY + INF_EQUIPPED_OBS_SIZE) -#define INF_NUM_OBS INF_OBS_AFTER_EQUIPMENT - -static_assert(INF_OBS_AFTER_PLAYER == 45, "Inferno player observation boundary"); -static_assert(INF_OBS_AFTER_PILLARS == 54, "Inferno pillar observation boundary"); -static_assert(INF_OBS_AFTER_NPCS == 236, "Inferno NPC observation boundary"); -static_assert(INF_OBS_AFTER_PENDING_HITS == 332, +#define INF_NUM_OBS INF_OBS_AFTER_SPARKS + +#define INF_OBS_WAVE_NORM INF_OBS_AFTER_SHARED +#define INF_OBS_WAVE_PHASE (INF_OBS_WAVE_NORM + 1) +#define INF_OBS_ZUK_ATTACK_TIMER (INF_OBS_WAVE_PHASE + 1) +#define INF_OBS_STAMINA_ACTIVE (INF_OBS_ZUK_ATTACK_TIMER + 1) +#define INF_OBS_DEAD_MOB_COUNT (INF_OBS_STAMINA_ACTIVE + 1) +#define INF_OBS_ZUK_PHASE_START (INF_OBS_DEAD_MOB_COUNT + 1) +#define INF_OBS_ZUK_SHIELD_DIR INF_OBS_ZUK_PHASE_START +#define INF_OBS_ZUK_SHIELD_FREEZE (INF_OBS_ZUK_PHASE_START + 1) + +static_assert(INF_OBS_AFTER_SHARED == 101, + "Inferno shared observation boundary"); +static_assert(INF_OBS_AFTER_ENCOUNTER == 115, + "Inferno encounter observation boundary"); +static_assert(INF_OBS_AFTER_PILLARS == 124, + "Inferno pillar observation boundary"); +static_assert(INF_OBS_AFTER_NPCS == 306, + "Inferno NPC observation boundary"); +static_assert(INF_OBS_AFTER_PENDING_HITS == 402, "Inferno pending-hit observation boundary"); -static_assert(INF_OBS_AFTER_SPARKS == 460, "Inferno spark observation boundary"); -static_assert(INF_OBS_AFTER_INVENTORY == 488, - "Inferno inventory observation boundary"); -static_assert(INF_OBS_AFTER_EQUIPMENT == 498, - "Inferno equipment observation boundary"); -static_assert(INF_NUM_OBS == 498, "Inferno observation width"); +static_assert(INF_OBS_AFTER_SPARKS == 530, + "Inferno spark observation boundary"); +static_assert(INF_NUM_OBS == 530, "Inferno observation width"); #define INF_PROTECTION_PRAYER_CODE_SCALE 8.0f #define INF_OFFENSIVE_PRAYER_CODE_SCALE 4.0f @@ -103,71 +114,93 @@ static int inf_pending_spark_obs_less( } -static const struct { InfNPCType type; int count; } -INF_OBS_SLOT_LAYOUT[INF_NUM_NPC_TYPES] = { - { INF_NPC_MAGER, 2 }, { INF_NPC_RANGER, 2 }, - { INF_NPC_MELEER, 2 }, { INF_NPC_BLOB, 2 }, - { INF_NPC_BAT, 2 }, { INF_NPC_BLOB_MAGE, 2 }, - { INF_NPC_BLOB_RANGE, 2 }, { INF_NPC_BLOB_MELEE, 2 }, - { INF_NPC_NIBBLER, 6 }, { INF_NPC_JAD, 3 }, - { INF_NPC_ZUK, 1 }, { INF_NPC_ZUK_SHIELD, 1 }, - { INF_NPC_HEALER_JAD, 6 }, { INF_NPC_HEALER_ZUK, 4 }, +static const InfNPCType INF_OBS_SLOT_ORDER[INF_NUM_NPC_TYPES] = { + INF_NPC_MAGER, INF_NPC_RANGER, + INF_NPC_MELEER, INF_NPC_BLOB, + INF_NPC_BAT, INF_NPC_BLOB_MAGE, + INF_NPC_BLOB_RANGE, INF_NPC_BLOB_MELEE, + INF_NPC_NIBBLER, INF_NPC_JAD, + INF_NPC_ZUK, INF_NPC_ZUK_SHIELD, + INF_NPC_HEALER_JAD, INF_NPC_HEALER_ZUK, +}; +static const uint8_t INF_OBS_SLOT_CAP[INF_NUM_NPC_TYPES] = { + [INF_NPC_NIBBLER] = 6, + [INF_NPC_BAT] = 2, + [INF_NPC_BLOB] = 2, + [INF_NPC_BLOB_MELEE] = 2, + [INF_NPC_BLOB_RANGE] = 2, + [INF_NPC_BLOB_MAGE] = 2, + [INF_NPC_MELEER] = 2, + [INF_NPC_RANGER] = 2, + [INF_NPC_MAGER] = 2, + [INF_NPC_JAD] = 3, + [INF_NPC_ZUK] = 1, + [INF_NPC_HEALER_JAD] = 6, + [INF_NPC_HEALER_ZUK] = 4, + [INF_NPC_ZUK_SHIELD] = 1, }; +#define INF_OBS_SLOT_MAX_TYPE_CAP 6 +static_assert(INF_MAX_NPCS <= INT8_MAX, + "Inferno observation slots fit signed bytes"); + static void inf_invalidate_current_obs_slots(InfernoState* s) { s->current_obs_slots_valid = 0; } -static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx) { - int obs_slots[INF_OBS_NPCS]; - for (int slot = 0; slot < INF_OBS_NPCS; slot++) obs_slots[slot] = -1; - - int type_base[INF_NUM_NPC_TYPES] = {0}; - int type_cap[INF_NUM_NPC_TYPES] = {0}; - int slot_counts[INF_NUM_NPC_TYPES] = {0}; - int bucket_capacity = 0; - for (int layout_idx = 0; layout_idx < INF_NUM_NPC_TYPES; layout_idx++) { - InfNPCType type = INF_OBS_SLOT_LAYOUT[layout_idx].type; - type_base[type] = bucket_capacity; - type_cap[type] = INF_OBS_SLOT_LAYOUT[layout_idx].count; - bucket_capacity += type_cap[type]; +static void inf_refresh_current_obs_slots_ctx( + InfernoState* s, + const InfernoContext* ctx +) { + int8_t live_slots[INF_NUM_NPC_TYPES][INF_OBS_SLOT_MAX_TYPE_CAP]; + int8_t phantom_slots[INF_NUM_NPC_TYPES][INF_OBS_SLOT_MAX_TYPE_CAP]; + uint8_t live_counts[INF_NUM_NPC_TYPES] = {0}; + uint8_t phantom_counts[INF_NUM_NPC_TYPES] = {0}; + + for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { + const InfNPC* npc = &s->npcs[npc_idx]; + if (!npc->active || npc->type < 0 || npc->type >= INF_NUM_NPC_TYPES) + continue; + InfNPCType type = npc->type; + uint8_t cap = INF_OBS_SLOT_CAP[type]; + if (npc->death_ticks == 0) { + if (live_counts[type] < cap) + live_slots[type][live_counts[type]++] = (int8_t)npc_idx; + } else if (phantom_counts[type] < cap && + inf_npc_is_phantom_barrage_obs_candidate(s, ctx, npc_idx)) { + phantom_slots[type][phantom_counts[type]++] = (int8_t)npc_idx; + } } int next_slot = 0; - for (int pass = 0; pass < 2; pass++) { - int pass_counts[INF_NUM_NPC_TYPES] = {0}; - int pass_slots[bucket_capacity]; - for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { - InfNPC* npc = &s->npcs[npc_idx]; - if (!npc->active || npc->type < 0 || npc->type >= INF_NUM_NPC_TYPES) - continue; - - InfNPCType type = npc->type; - int eligible = pass == 0 - ? npc->death_ticks == 0 - : npc->death_ticks > 0 && - inf_npc_is_phantom_barrage_obs_candidate(s, ctx, npc_idx); - if (!eligible || slot_counts[type] >= type_cap[type]) continue; - - pass_slots[type_base[type] + pass_counts[type]++] = npc_idx; - slot_counts[type]++; + for (int layout_idx = 0; layout_idx < INF_NUM_NPC_TYPES; layout_idx++) { + InfNPCType type = INF_OBS_SLOT_ORDER[layout_idx]; + for (int type_slot = 0; type_slot < live_counts[type]; type_slot++) { + if (next_slot >= INF_OBS_NPCS) { + fprintf(stderr, + "inferno: observation target capacity exceeded at %d slots\n", + INF_OBS_NPCS); + abort(); + } + s->current_obs_slots[next_slot++] = live_slots[type][type_slot]; } - - for (int layout_idx = 0; layout_idx < INF_NUM_NPC_TYPES; layout_idx++) { - InfNPCType type = INF_OBS_SLOT_LAYOUT[layout_idx].type; - for (int type_slot = 0; type_slot < pass_counts[type]; type_slot++) { - if (next_slot >= INF_OBS_NPCS) { - fprintf(stderr, - "inferno: observation target capacity exceeded at %d slots\n", - INF_OBS_NPCS); - abort(); - } - obs_slots[next_slot++] = pass_slots[type_base[type] + type_slot]; + } + for (int layout_idx = 0; layout_idx < INF_NUM_NPC_TYPES; layout_idx++) { + InfNPCType type = INF_OBS_SLOT_ORDER[layout_idx]; + int available = INF_OBS_SLOT_CAP[type] - live_counts[type]; + int count = phantom_counts[type] < available + ? phantom_counts[type] : available; + for (int type_slot = 0; type_slot < count; type_slot++) { + if (next_slot >= INF_OBS_NPCS) { + fprintf(stderr, + "inferno: observation target capacity exceeded at %d slots\n", + INF_OBS_NPCS); + abort(); } + s->current_obs_slots[next_slot++] = phantom_slots[type][type_slot]; } } - - for (int slot = 0; slot < INF_OBS_NPCS; slot++) - s->current_obs_slots[slot] = obs_slots[slot]; + for (int slot = next_slot; slot < INF_OBS_NPCS; slot++) + s->current_obs_slots[slot] = -1; s->current_obs_slots_valid = 1; } @@ -194,59 +227,42 @@ static void inf_write_obs_ctx( int inf_prof_enabled = INF_PROFILE_ENABLED(); double inf_prof_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; #endif - memset(obs, 0, INF_NUM_OBS * sizeof(float)); int i = 0; int px = s->player.x; int py = s->player.y; const EncounterLoadoutStats* ls = inf_current_loadout_stats(s, ctx); - InfSupplyDoses full_supplies = inf_full_starting_supplies(); - - int offensive_prayer_code = 0; - if (s->player.offensive_prayer == OFFENSIVE_PRAYER_PIETY) - offensive_prayer_code = 1; - else if (s->player.offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) - offensive_prayer_code = 2; - else if (s->player.offensive_prayer == OFFENSIVE_PRAYER_AUGURY) - offensive_prayer_code = 3; - - obs[i++] = (float)s->player.current_hitpoints / 99.0f; - obs[i++] = (float)(px - INF_ARENA_MIN_X) / (float)INF_ARENA_WIDTH; - obs[i++] = (float)(py - INF_ARENA_MIN_Y) / (float)INF_ARENA_HEIGHT; - obs[i++] = (float)s->player.prayer / INF_PROTECTION_PRAYER_CODE_SCALE; - obs[i++] = (float)offensive_prayer_code / INF_OFFENSIVE_PRAYER_CODE_SCALE; - obs[i++] = (float)s->player.brew_doses / (float)full_supplies.brew_doses; - obs[i++] = (float)s->player.restore_doses / (float)full_supplies.restore_doses; - obs[i++] = (float)s->player.current_prayer / 99.0f; - obs[i++] = (float)s->player.bastion_doses / (float)full_supplies.bastion_doses; - obs[i++] = (float)s->player.stamina_doses / (float)full_supplies.stamina_doses; + OsrsSharedObservationInput shared_input = { + .player = &s->player, + .interaction = &s->interaction, + .arena_min_x = INF_ARENA_MIN_X, + .arena_max_x = INF_ARENA_MAX_X, + .arena_min_y = INF_ARENA_MIN_Y, + .arena_max_y = INF_ARENA_MAX_Y, + .attack_style = ls->style, + .attack_range = ls->attack_range, + .max_hit = ls->max_hit, + .attack_speed = ls->attack_speed, + .defence_stab = ls->def_stab, + .defence_slash = ls->def_slash, + .defence_crush = ls->def_crush, + .defence_magic = ls->def_magic, + .defence_ranged = ls->def_ranged, + .effective_level = ls->eff_level, + .attack_bonus = ls->attack_bonus, + .strength_bonus = ls->strength_bonus, + .spell_base_damage = ls->spell_base_damage, + .special_attack_cost = + osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]), + }; + i += osrs_write_shared_observations(&obs[i], &shared_input); + assert(i == INF_OBS_AFTER_SHARED); + obs[i++] = (float)s->wave / (float)INF_NUM_WAVES; obs[i++] = (float)(inf_wave_phase_index(s->wave) + 1) / INF_WAVE_PHASE_CODE_SCALE; obs[i++] = inf_zuk_attack_timer_obs(s); - obs[i++] = (float)(s->weapon_set + 1) / INF_WEAPON_SET_CODE_SCALE; obs[i++] = s->stamina_active_ticks > 0 ? 1.0f : 0.0f; - obs[i++] = (float)s->player.potion_timer / 3.0f; - obs[i++] = (float)s->player.attack_timer / 8.0f; - obs[i++] = osrs_interaction_active(&s->interaction) ? 1.0f : 0.0f; obs[i++] = (float)s->dead_mob_count / (float)INF_MAX_DEAD_MOBS; - obs[i++] = (float)s->player.special_energy / 100.0f; - obs[i++] = (float)s->player.current_defence / 99.0f; - obs[i++] = (float)s->player.current_ranged / 99.0f; - obs[i++] = (float)s->player.current_magic / 99.0f; - obs[i++] = (float)ls->attack_range / 15.0f; - obs[i++] = (float)ls->max_hit / 80.0f; - obs[i++] = (float)ls->attack_speed / 6.0f; - obs[i++] = (float)ls->def_stab / 300.0f; - obs[i++] = (float)ls->def_slash / 300.0f; - obs[i++] = (float)ls->def_crush / 300.0f; - obs[i++] = (float)ls->def_magic / 300.0f; - obs[i++] = (float)ls->def_ranged / 300.0f; - obs[i++] = (float)ls->eff_level / 165.0f; - obs[i++] = (float)ls->attack_bonus / 200.0f; - obs[i++] = (float)ls->strength_bonus / 160.0f; - obs[i++] = (float)osrs_spec_cost( - s->player.equipped[GEAR_SLOT_WEAPON]) / 100.0f; - obs[i++] = (float)ls->spell_base_damage / 40.0f; if (s->wave == INF_WAVE_ZUK) { int shield_idx = s->zuk.shield_idx; @@ -255,7 +271,8 @@ static void inf_write_obs_ctx( obs[i++] = shield_active ? (s->zuk.shield_freeze > 0 ? 0.0f : (float)s->zuk.shield_dir) : 0.0f; - obs[i++] = shield_active ? (float)s->zuk.shield_freeze / 5.0f : 0.0f; + obs[i++] = shield_active + ? (float)s->zuk.shield_freeze / 5.0f : 0.0f; int behind_shield = 0; float shield_offset = 0.0f; if (shield_active) { @@ -274,9 +291,10 @@ static void inf_write_obs_ctx( obs[i++] = s->zuk.jad_spawned ? 1.0f : 0.0f; obs[i++] = s->zuk.healer_spawned ? 1.0f : 0.0f; } else { + memset(&obs[i], 0, 9 * sizeof(*obs)); i += 9; } - assert(i == INF_OBS_AFTER_PLAYER); + assert(i == INF_OBS_AFTER_ENCOUNTER); for (int pillar_idx = 0; pillar_idx < INF_NUM_PILLARS; pillar_idx++) { const InfPillar* pillar = &s->pillars[pillar_idx]; @@ -297,6 +315,7 @@ static void inf_write_obs_ctx( for (int slot_idx = 0; slot_idx < INF_OBS_NPCS; slot_idx++) { int npc_idx = s->current_obs_slots[slot_idx]; if (npc_idx < 0) { + memset(&obs[i], 0, INF_NPC_SLOT_FEATURES * sizeof(*obs)); i += INF_NPC_SLOT_FEATURES; continue; } @@ -346,6 +365,7 @@ static void inf_write_obs_ctx( obs[i++] = (float)inf_pending_hit_obs_timer(hit) / 10.0f; obs[i++] = (float)hit->damage / 150.0f; } else { + memset(&obs[i], 0, INF_FEATURES_PER_HIT * sizeof(*obs)); i += INF_FEATURES_PER_HIT; } } @@ -374,6 +394,7 @@ static void inf_write_obs_ctx( for (int slot_idx = 0; slot_idx < INF_SPARK_OBS_SLOTS; slot_idx++) { if (slot_idx >= spark_count) { + memset(&obs[i], 0, INF_FEATURES_PER_SPARK * sizeof(*obs)); i += INF_FEATURES_PER_SPARK; continue; } @@ -388,17 +409,7 @@ static void inf_write_obs_ctx( INF_PROFILE_MARK(INF_PROF_OBS_SPARKS); #endif - for (int cell_idx = 0; cell_idx < OSRS_INVENTORY_SIZE; cell_idx++) { - const OsrsInventoryCell* cell = &s->inventory_cells[cell_idx]; - obs[i++] = - osrs_inventory_cell_obs_code_encode(cell->content_code); - } - assert(i == INF_OBS_AFTER_INVENTORY); - - osrs_write_equipment_effect_aggregate( - &obs[i], &s->player.equipment_effect_profile); - i += OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES; - assert(i == INF_OBS_AFTER_EQUIPMENT); + assert(i == INF_NUM_OBS); } static int inf_find_target_obs_slot(const InfernoState* s, int npc_slot) { @@ -467,7 +478,7 @@ static inline InfInventoryClickMemo* inf_inventory_click_memo( InfernoState* s, int cell_idx ) { - const OsrsInventoryCell* cell = &s->inventory_cells[cell_idx]; + const OsrsInventoryCell* cell = &s->player.inventory_cells[cell_idx]; InfInventoryClickMemo* memo = &s->inventory_click_memos[cell_idx]; if (memo->content_code != cell->content_code) { OsrsInventoryClickResolution resolution = @@ -490,118 +501,148 @@ static void inf_write_mask_ctx( InfernoState* s = (InfernoState*)state; InfernoContext* ctx = (InfernoContext*)context; inf_ensure_current_obs_slots_ctx(s, ctx); - int offset = 0; + memset(mask, 0, INF_ACTION_MASK_SIZE * sizeof(*mask)); - mask[offset++] = 1.0f; + int primary_offset = inf_action_head_mask_offset(INF_HEAD_PRIMARY); + mask[primary_offset] = 1.0f; + int pillars_at_canonical_positions = + inf_pillars_at_canonical_positions(s); uint32_t static_footprint_mask = encounter_arena_topology_nearby_unit_footprint_mask( - ctx->route_topology, s->player.x, s->player.y); - for (int d = 1; d < ENCOUNTER_MOVE_ACTIONS; d++) { - int dx = ENCOUNTER_MOVE_TARGET_DX[d]; - int dy = ENCOUNTER_MOVE_TARGET_DY[d]; + inf_route_topology_for_state(ctx, s), + s->player.x, + s->player.y); + for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + int dx = ENCOUNTER_MOVE_TARGET_DX[action]; + int dy = ENCOUNTER_MOVE_TARGET_DY[action]; int step_bit = (dy + 2) * 5 + (dx + 2); if (step_bit > 12) step_bit--; int nx = s->player.x + dx; int ny = s->player.y + dy; int blocked = !(static_footprint_mask & (UINT32_C(1) << step_bit)) || - inf_pillar_footprint_blocked(s, nx, ny, 1); - mask[offset++] = blocked ? 0.0f : 1.0f; + (!pillars_at_canonical_positions && + inf_pillar_footprint_blocked(s, nx, ny, 1)); + mask[primary_offset + action] = blocked ? 0.0f : 1.0f; } - mask[offset++] = 1.0f; - mask[offset++] = s->player.prayer != PRAYER_NONE ? 1.0f : 0.0f; - mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; - int has_forced_safe_healer_target = ctx->config.zuk_force_safe_untagged_healer_target_mask && inf_has_safe_untagged_zuk_healer_target_now(s, ctx); - - mask[offset++] = has_forced_safe_healer_target ? 0.0f : 1.0f; - for (int n = 0; n < INF_OBS_NPCS; n++) { - int npc_idx = s->current_obs_slots[n]; - int is_targetable = inf_obs_slot_is_targetable(s, ctx, n); + for (int obs_slot = 0; obs_slot < INF_OBS_NPCS; obs_slot++) { + int npc_idx = s->current_obs_slots[obs_slot]; + int is_targetable = inf_obs_slot_is_targetable(s, ctx, obs_slot); if (is_targetable && (ctx->config.zuk_safe_untagged_healer_target_mask || ctx->config.zuk_force_safe_untagged_healer_target_mask) && - !inf_untagged_zuk_healer_target_is_safe_now(s, ctx, npc_idx)) { + !inf_untagged_zuk_healer_target_is_safe_now( + s, ctx, npc_idx)) { is_targetable = 0; } if (is_targetable && has_forced_safe_healer_target && - !inf_is_safe_untagged_zuk_healer_target_now(s, ctx, npc_idx)) { + !inf_is_safe_untagged_zuk_healer_target_now( + s, ctx, npc_idx)) { is_targetable = 0; } - mask[offset++] = is_targetable ? 1.0f : 0.0f; + mask[primary_offset + + inf_primary_attack_action_for_obs_slot(obs_slot)] = + is_targetable ? 1.0f : 0.0f; } - - { - int inventory_mask_offset = offset; - memset(&mask[inventory_mask_offset], 0, - sizeof(*mask) * INF_INV_CLICK_HEADS * INF_INV_CLICK_DIM); - for (int head = 0; head < INF_INV_CLICK_HEADS; head++) - mask[inventory_mask_offset + head * INF_INV_CLICK_DIM] = 1.0f; - - for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - const OsrsInventoryCell* cell = &s->inventory_cells[c]; - const InfInventoryClickMemo* memo = inf_inventory_click_memo(s, c); - switch (memo->click_action) { - case OSRS_CLICK_EQUIP: { - if (!osrs_can_equip_from_cell(&s->player, s->inventory_cells, c)) - break; - int gear_slot = - osrs_inventory_cell_metadata(cell)->gear_slot; - if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) { - fprintf(stderr, - "inferno: equippable inventory cell %d has invalid gear slot %d\n", - c, gear_slot); - abort(); - } - mask[inventory_mask_offset + gear_slot * INF_INV_CLICK_DIM + c + 1] = - 1.0f; + if (has_forced_safe_healer_target) mask[primary_offset] = 0.0f; + + int overhead_offset = inf_action_head_mask_offset(INF_HEAD_PRAYER); + mask[overhead_offset + ENCOUNTER_OVERHEAD_NO_CHANGE] = 1.0f; + mask[overhead_offset + ENCOUNTER_OVERHEAD_OFF] = + s->player.prayer != PRAYER_NONE ? 1.0f : 0.0f; + mask[overhead_offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE] = + s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[overhead_offset + ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED] = + s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[overhead_offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC] = + s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[overhead_offset + ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION] = + s->player.current_prayer > 0 ? 1.0f : 0.0f; + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + int head_offset = + inf_action_head_mask_offset(INF_HEAD_EQUIP_SLOT(slot)); + mask[head_offset] = 1.0f; + } + int eat_offset = inf_action_head_mask_offset(INF_HEAD_EAT); + int drink_offset = inf_action_head_mask_offset(INF_HEAD_DRINK); + mask[eat_offset] = 1.0f; + mask[drink_offset] = 1.0f; + for (int cell_idx = 0; cell_idx < OSRS_INVENTORY_SIZE; cell_idx++) { + const OsrsInventoryCell* cell = + &s->player.inventory_cells[cell_idx]; + const InfInventoryClickMemo* memo = + inf_inventory_click_memo(s, cell_idx); + switch (memo->click_action) { + case OSRS_CLICK_EQUIP: { + if (!osrs_can_equip_from_cell( + &s->player, s->player.inventory_cells, cell_idx)) { break; } - case OSRS_CLICK_EAT: - if (osrs_can_eat_consumable_kind( - &s->player, (OsrsConsumableKind)memo->consumable_kind)) { - mask[inventory_mask_offset + - NUM_GEAR_SLOTS * INF_INV_CLICK_DIM + c + 1] = 1.0f; - } - break; - case OSRS_CLICK_DRINK: - if (osrs_inventory_cell_dose_count(cell) > 0 && - s->player.potion_timer == 0 && - inf_drink_has_effect( - s, (OsrsConsumableKind)memo->consumable_kind)) { - mask[inventory_mask_offset + - (NUM_GEAR_SLOTS + 1) * INF_INV_CLICK_DIM + c + 1] = 1.0f; - } - break; - case OSRS_CLICK_NONE: - break; + int gear_slot = + osrs_inventory_cell_metadata(cell)->gear_slot; + if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) { + fprintf(stderr, + "inferno: equippable inventory cell %d has invalid gear slot %d\n", + cell_idx, gear_slot); + abort(); + } + int head_offset = inf_action_head_mask_offset( + INF_HEAD_EQUIP_SLOT(gear_slot)); + mask[head_offset + cell_idx + 1] = 1.0f; + break; } + case OSRS_CLICK_EAT: + if (osrs_can_eat_consumable_kind( + &s->player, + (OsrsConsumableKind)memo->consumable_kind)) { + mask[eat_offset + cell_idx + 1] = 1.0f; + } + break; + case OSRS_CLICK_DRINK: + if (osrs_inventory_cell_dose_count(cell) > 0 && + s->player.potion_timer == 0 && + inf_drink_has_effect( + s, + (OsrsConsumableKind)memo->consumable_kind)) { + mask[drink_offset + cell_idx + 1] = 1.0f; + } + break; + case OSRS_CLICK_NONE: + break; } - offset += INF_INV_CLICK_HEADS * INF_INV_CLICK_DIM; } - mask[offset++] = 1.0f; - mask[offset++] = s->player.current_magic >= BLOOD_BARRAGE_LEVEL ? 1.0f : 0.0f; - mask[offset++] = s->player.current_magic >= ICE_BARRAGE_LEVEL ? 1.0f : 0.0f; - - mask[offset++] = 1.0f; - mask[offset++] = (!s->player.spec_armed && - s->player.equipped[GEAR_SLOT_WEAPON] == ITEM_TOXIC_BLOWPIPE && - s->player.special_energy >= BLOWPIPE_SPEC_COST) - ? 1.0f : 0.0f; - mask[offset++] = s->player.spec_armed ? 1.0f : 0.0f; - - mask[offset++] = 1.0f; - mask[offset++] = s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE ? 1.0f : 0.0f; - mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + int spell_offset = inf_action_head_mask_offset(INF_HEAD_SPELL); + mask[spell_offset + OSRS_SPELL_NONE] = 1.0f; + mask[spell_offset + OSRS_SPELL_BLOOD_BARRAGE] = + s->player.current_magic >= BLOOD_BARRAGE_LEVEL ? 1.0f : 0.0f; + mask[spell_offset + OSRS_SPELL_ICE_BARRAGE] = + s->player.current_magic >= ICE_BARRAGE_LEVEL ? 1.0f : 0.0f; + + int spec_offset = inf_action_head_mask_offset(INF_HEAD_SPEC); + mask[spec_offset] = 1.0f; + mask[spec_offset + 1] = + !s->player.spec_armed && + s->player.equipped[GEAR_SLOT_WEAPON] == ITEM_TOXIC_BLOWPIPE && + s->player.special_energy >= BLOWPIPE_SPEC_COST ? 1.0f : 0.0f; + mask[spec_offset + 2] = s->player.spec_armed ? 1.0f : 0.0f; + + int offensive_offset = + inf_action_head_mask_offset(INF_HEAD_OFFENSIVE); + mask[offensive_offset] = 1.0f; + mask[offensive_offset + 1] = + s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE ? 1.0f : 0.0f; + mask[offensive_offset + 2] = + s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[offensive_offset + 3] = + s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[offensive_offset + 4] = + s->player.current_prayer > 0 ? 1.0f : 0.0f; s->total_action_mask_checks++; } @@ -610,6 +651,7 @@ static void inf_write_mask(EncounterState* state, float* mask) { inf_write_mask_ctx(state, (EncounterContext*)inf_legacy_context(), mask); } + static float inf_get_reward_ctx(EncounterState* state, EncounterContext* context) { (void)context; return ((InfernoState*)state)->reward; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc index 1027e3a793..32b3234f04 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -1,53 +1,38 @@ #line 3605 "encounter_inferno.h" -#define INF_HEAD_MOVE 0 -#define INF_HEAD_PRAYER 1 -#define INF_HEAD_TARGET 2 -#define INF_HEAD_EQUIP_BASE 3 -#define INF_HEAD_EQUIP_SLOT(slot) (INF_HEAD_EQUIP_BASE + (slot)) -#define INF_HEAD_EAT (INF_HEAD_EQUIP_BASE + NUM_GEAR_SLOTS) -#define INF_HEAD_DRINK (INF_HEAD_EAT + 1) -#define INF_HEAD_SPELL (INF_HEAD_DRINK + 1) -#define INF_HEAD_SPEC (INF_HEAD_SPELL + 1) -#define INF_HEAD_OFFENSIVE (INF_HEAD_SPEC + 1) +#define INF_HEAD_PRIMARY OSRS_HEAD_PRIMARY +#define INF_HEAD_PRAYER OSRS_HEAD_OVERHEAD +#define INF_HEAD_EQUIP_BASE OSRS_HEAD_EQUIP_BASE +#define INF_HEAD_EQUIP_SLOT(slot) OSRS_HEAD_EQUIP_SLOT(slot) +#define INF_HEAD_EAT OSRS_HEAD_EAT +#define INF_HEAD_DRINK OSRS_HEAD_DRINK +#define INF_HEAD_SPELL OSRS_HEAD_SPELL +#define INF_HEAD_SPEC OSRS_HEAD_SPECIAL +#define INF_HEAD_OFFENSIVE OSRS_HEAD_OFFENSIVE #define INF_INV_CLICK_HEADS (NUM_GEAR_SLOTS + 2) -#define INF_INV_CLICK_DIM (OSRS_INVENTORY_SIZE + 1) -#define INF_SPEC_DIM 3 -#define INF_ACTION_DIMS_INIT { \ - ENCOUNTER_MOVE_ACTIONS, \ - ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION, \ - INF_OBS_NPCS+1, \ - INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, \ - INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, \ - INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, \ - INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, INF_INV_CLICK_DIM, \ - INF_INV_CLICK_DIM, \ - 3, \ - INF_SPEC_DIM, \ - ENCOUNTER_OFFENSIVE_DIM \ -} +#define INF_INV_CLICK_DIM OSRS_INVENTORY_CLICK_DIM +#define INF_PRIMARY_ATTACK_BASE OSRS_PRIMARY_MOVE_ACTIONS +#define INF_PRIMARY_DIM OSRS_PRIMARY_DIM(INF_OBS_NPCS) +#define INF_SPEC_DIM OSRS_SPECIAL_DIM +#define INF_ACTION_DIMS_INIT { OSRS_BASE_ACTION_DIMS(INF_OBS_NPCS) } static const int INF_ACTION_DIMS[INF_NUM_ACTION_HEADS] = INF_ACTION_DIMS_INIT; -#define INF_ACTION_MASK_SIZE (ENCOUNTER_MOVE_ACTIONS + ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION + INF_OBS_NPCS+1 + INF_INV_CLICK_HEADS*INF_INV_CLICK_DIM + 3 + INF_SPEC_DIM + ENCOUNTER_OFFENSIVE_DIM) -#define INF_OVERHEAD_SET_REFRESH_REDEMPTION ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE +#define INF_ACTION_MASK_SIZE OSRS_BASE_ACTION_MASK_SIZE(INF_OBS_NPCS) static int inf_action_head_mask_offset(int head) { - int offset = 0; - for (int h = 0; h < head; h++) - offset += INF_ACTION_DIMS[h]; - return offset; + return osrs_base_action_head_mask_offset(INF_OBS_NPCS, head); } -static int inf_canonical_overhead_action(int action) { - return action == INF_OVERHEAD_SET_REFRESH_REDEMPTION - ? ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION - : action; +static int inf_primary_attack_action_for_obs_slot(int obs_slot) { + if (obs_slot < 0 || obs_slot >= INF_OBS_NPCS) { + fprintf(stderr, "inferno primary attack obs slot out of range: %d\n", + obs_slot); + abort(); + } + return INF_PRIMARY_ATTACK_BASE + obs_slot; } -static int inf_local_overhead_action(int action) { - if (action == ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION) - return INF_OVERHEAD_SET_REFRESH_REDEMPTION; - if (action == ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE) - return ENCOUNTER_OVERHEAD_NO_CHANGE; - return action; +static int inf_primary_obs_slot_from_action(int primary_action) { + int obs_slot = primary_action - INF_PRIMARY_ATTACK_BASE; + return obs_slot >= 0 && obs_slot < INF_OBS_NPCS ? obs_slot : -1; } static void inf_record_offensive_prayer_attack( @@ -114,19 +99,29 @@ static int inf_tile_walkable(void* data, int x, int y) { return !inf_footprint_blocked_ctx( walk->s, walk->ctx, x, y, 1); } - static int inf_player_route_blocked(void* data, int x, int y, int size) { const InfWalkCtx* walk = (const InfWalkCtx*)data; return inf_pillar_footprint_blocked(walk->s, x, y, size); } -static uint64_t inf_player_route_blocker_revision(const InfernoState* s) { - uint64_t revision = 1; - for (int pillar = 0; pillar < INF_NUM_PILLARS; pillar++) - revision |= (uint64_t)(s->pillars[pillar].active != 0) << (pillar + 1); - return revision; +static uint64_t inf_player_route_blocker_revision( + const InfernoState* state +) { + uint64_t revision = UINT64_C(1469598103934665603); + for (int pillar = 0; pillar < INF_NUM_PILLARS; pillar++) { + const InfPillar* value = &state->pillars[pillar]; + revision ^= (uint32_t)value->x; + revision *= UINT64_C(1099511628211); + revision ^= (uint32_t)value->y; + revision *= UINT64_C(1099511628211); + revision ^= (uint32_t)(value->active != 0); + revision *= UINT64_C(1099511628211); + } + return revision ? revision : 1; } + + typedef struct { InfernoState* s; InfernoContext* ctx; @@ -206,7 +201,7 @@ static void inf_apply_blood_barrage_heal_damage(InfernoState* s, int damage) { static void inf_player_pretick(InfernoState* s, InfernoContext* ctx, const int* actions) { OffensivePrayer prev_offensive = s->player.offensive_prayer; - int prayer_act = inf_canonical_overhead_action(actions[INF_HEAD_PRAYER]); + int prayer_act = actions[INF_HEAD_PRAYER]; int offensive_act = actions[INF_HEAD_OFFENSIVE]; if (encounter_apply_overhead_action(&s->player.prayer, prayer_act)) { s->player.prayer_just_activated = 1; @@ -242,7 +237,7 @@ static void inf_sync_consumable_counters_from_cells(InfernoState* s) { p->bastion_doses = 0; p->stamina_doses = 0; for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) { - const OsrsInventoryCell* cell = &s->inventory_cells[i]; + const OsrsInventoryCell* cell = &s->player.inventory_cells[i]; const OsrsItemContentMetadata* metadata = osrs_inventory_cell_metadata(cell); if (metadata->raw_osrs_id == 0) continue; @@ -303,7 +298,7 @@ static void inf_apply_eat_cell(InfernoState* s, int cell_idx, OsrsConsumableKind abort(); } osrs_player_eat_food_effects(&s->player, food); - osrs_inventory_cell_consume_eat(&s->inventory_cells[cell_idx]); + osrs_inventory_cell_consume_eat(&s->player.inventory_cells[cell_idx]); } static void inf_apply_human_player_commands_ctx(InfernoState* s, InfernoContext* ctx) { @@ -411,23 +406,22 @@ static void inf_tick_player_ctx( clicks.eat = actions[INF_HEAD_EAT]; clicks.drink = actions[INF_HEAD_DRINK]; OsrsInventoryTickIntent intent = - osrs_resolve_inventory_tick_intent(&s->player, s->inventory_cells, &clicks); + osrs_resolve_inventory_tick_intent( + &s->player, s->player.inventory_cells, &clicks); if (intent.eat_cell >= 0) osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EAT); if (intent.drink_cell >= 0) osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_DRINK); - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - if (intent.equip_cell_by_slot[slot] >= 0) { - osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EQUIP); - break; - } - } + if (intent.equip_slot_mask != 0) + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EQUIP); OsrsInventoryApplyStep click_step; while (osrs_inventory_intent_next(&intent, &click_step)) { switch (click_step.kind) { case OSRS_INVENTORY_APPLY_EQUIP: if (osrs_equip_from_cell( - &s->player, s->inventory_cells, click_step.cell_idx) >= 0) { + &s->player, + s->player.inventory_cells, + click_step.cell_idx) >= 0) { s->total_gear_switches++; inf_note_human_weapon_set(s); inf_refresh_live_stats(s); @@ -440,7 +434,7 @@ static void inf_tick_player_ctx( case OSRS_INVENTORY_APPLY_DRINK: { OsrsInventoryDrinkConsumeResult drink = osrs_inventory_cell_consume_drink_one_dose( - &s->inventory_cells[click_step.cell_idx], + &s->player.inventory_cells[click_step.cell_idx], click_step.resolution, &s->player.potion_timer, inf_apply_drink_one_dose_effect, s); @@ -454,10 +448,10 @@ static void inf_tick_player_ctx( inf_ensure_current_obs_slots_ctx(s, ctx); - int target = actions[INF_HEAD_TARGET]; + int primary_action = actions[INF_HEAD_PRIMARY]; + int obs_idx = inf_primary_obs_slot_from_action(primary_action); int has_new_target = 0; - if (target > 0 && target <= INF_OBS_NPCS) { - int obs_idx = target - 1; + if (obs_idx >= 0) { int npc_idx = s->current_obs_slots[obs_idx]; int is_live_target = npc_idx >= 0 && npc_idx < INF_MAX_NPCS && @@ -515,7 +509,9 @@ static void inf_tick_player_ctx( s->interaction.target_slot)) { osrs_interaction_clear(&s->interaction); } - int has_explicit_move = (actions[INF_HEAD_MOVE] > 0 || s->player_dest_x >= 0); + int has_explicit_move = + (primary_action > 0 && primary_action < OSRS_PRIMARY_MOVE_ACTIONS) || + s->player_dest_x >= 0; if (osrs_interaction_active(&s->interaction)) { int target_slot = s->interaction.target_slot; int target_can_manual_phantom = @@ -538,7 +534,7 @@ static void inf_tick_player_ctx( int claimed_target_standing = has_new_target && osrs_interaction_active(&s->interaction); OsrsPlayerCommand command = { .kind = OSRS_PLAYER_CMD_NONE }; - int move_act = actions[INF_HEAD_MOVE]; + int move_act = primary_action; if (!claimed_target_standing && has_explicit_move) { if (s->player_dest_x >= 0) { command.kind = OSRS_PLAYER_CMD_MOVE; @@ -550,6 +546,14 @@ static void inf_tick_player_ctx( } } InfWalkCtx wc = { s, ctx }; + EncounterRouteBlockers route_blockers = {0}; + if (!inf_pillars_at_canonical_positions(s)) { + route_blockers = (EncounterRouteBlockers){ + .is_blocked = inf_player_route_blocked, + .ctx = &wc, + .revision = inf_player_route_blocker_revision(s), + }; + } InfPlayerStepTargetCtx target_ctx = { .s = s, .ctx = ctx, @@ -566,15 +570,14 @@ static void inf_tick_player_ctx( .dest_x = &s->player_dest_x, .dest_y = &s->player_dest_y, .arena = { - .topology = ctx->route_topology, - .blockers = { - .is_blocked = inf_player_route_blocked, - .ctx = &wc, - .revision = inf_player_route_blocker_revision(s), - }, + .topology = inf_route_topology_for_state(ctx, s), + .blockers = route_blockers, .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, - .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + .cost_policy = s->wave >= INF_WAVE_ZUK + ? ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS + : ENCOUNTER_ROUTE_COST_OSRS, .destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + .attack_geometry = ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY, .collision_map = ctx->collision_map, .world_offset_x = ctx->world_offset_x, .world_offset_y = ctx->world_offset_y, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index e58af730f8..2455d10789 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -573,7 +573,7 @@ static void* inf_get_player_for_input(void* state, int idx) { static int inf_first_cell_with_kind(const InfernoState* s, OsrsConsumableKind kind) { for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - const OsrsInventoryCell* cell = &s->inventory_cells[c]; + const OsrsInventoryCell* cell = &s->player.inventory_cells[c]; const OsrsItemContentMetadata* metadata = osrs_inventory_cell_metadata(cell); if (metadata->raw_osrs_id == 0) continue; @@ -599,7 +599,7 @@ static void inf_translate_gear_swap(const InfernoState* s, int* actions, InfWeap if (item == ITEM_NONE || s->player.equipped[slot] == item) continue; for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { if (osrs_inventory_cell_item_index( - &s->inventory_cells[c]) == item) { + &s->player.inventory_cells[c]) == item) { actions[INF_HEAD_EQUIP_SLOT(slot)] = c + 1; break; } @@ -615,9 +615,9 @@ static void inf_translate_human_input_ctx( ) { for (int h = 0; h < INF_NUM_ACTION_HEADS; h++) actions[h] = 0; - encounter_translate_movement(hi, actions, INF_HEAD_MOVE, inf_get_player_for_input, state); + encounter_translate_movement( + hi, actions, INF_HEAD_PRIMARY, inf_get_player_for_input, state); encounter_translate_prayer(hi, actions, INF_HEAD_PRAYER); - actions[INF_HEAD_PRAYER] = inf_local_overhead_action(actions[INF_HEAD_PRAYER]); encounter_translate_offensive_prayer(hi, actions, INF_HEAD_OFFENSIVE); InfernoState* s = (InfernoState*)state; InfernoContext* ctx = (InfernoContext*)context; @@ -625,12 +625,9 @@ static void inf_translate_human_input_ctx( if (hi->pending_target_idx >= 0) { int found_slot = inf_find_target_obs_slot(s, hi->pending_target_idx); if (inf_obs_slot_is_targetable(s, ctx, found_slot)) { - actions[INF_HEAD_TARGET] = found_slot + 1; - } else { - actions[INF_HEAD_TARGET] = 0; + actions[INF_HEAD_PRIMARY] = + inf_primary_attack_action_for_obs_slot(found_slot); } - } else { - actions[INF_HEAD_TARGET] = 0; } if (hi->pending_gear >= 1 && hi->pending_gear <= INF_NUM_WEAPON_SETS) @@ -651,8 +648,10 @@ static void inf_translate_human_input_ctx( else if (hi->pending_potion == POTION_STAMINA) inf_translate_drink(s, actions, OSRS_CONSUMABLE_STAMINA); - if (hi->pending_spell == ATTACK_BLOOD) actions[INF_HEAD_SPELL] = 1; - else if (hi->pending_spell == ATTACK_ICE) actions[INF_HEAD_SPELL] = 2; + if (hi->pending_spell == OSRS_SPELL_BLOOD_BARRAGE) + actions[INF_HEAD_SPELL] = OSRS_SPELL_BLOOD_BARRAGE; + else if (hi->pending_spell == OSRS_SPELL_ICE_BARRAGE) + actions[INF_HEAD_SPELL] = OSRS_SPELL_ICE_BARRAGE; if (hi->pending_spec) actions[INF_HEAD_SPEC] = s->player.spec_armed ? 2 : 1; } @@ -677,29 +676,33 @@ static void inf_translate_human_commands_ctx( case HUMAN_COMMAND_WALK: s->player_dest_x = cmd->world_x; s->player_dest_y = cmd->world_y; - actions[INF_HEAD_TARGET] = 0; + actions[INF_HEAD_PRIMARY] = 0; actions[INF_HEAD_SPELL] = 0; break; case HUMAN_COMMAND_ATTACK_NPC: { int found_slot = inf_find_target_obs_slot(s, cmd->npc_slot); - actions[INF_HEAD_TARGET] = inf_obs_slot_is_targetable(s, ctx, found_slot) - ? found_slot + 1 : 0; + actions[INF_HEAD_PRIMARY] = inf_obs_slot_is_targetable( + s, ctx, found_slot) + ? inf_primary_attack_action_for_obs_slot(found_slot) : 0; s->player_dest_x = -1; s->player_dest_y = -1; break; } case HUMAN_COMMAND_SPELL_TARGET: { int found_slot = inf_find_target_obs_slot(s, cmd->npc_slot); - actions[INF_HEAD_TARGET] = inf_obs_slot_is_targetable(s, ctx, found_slot) - ? found_slot + 1 : 0; - if (cmd->spell == ATTACK_BLOOD) actions[INF_HEAD_SPELL] = 1; - else if (cmd->spell == ATTACK_ICE) actions[INF_HEAD_SPELL] = 2; + actions[INF_HEAD_PRIMARY] = inf_obs_slot_is_targetable( + s, ctx, found_slot) + ? inf_primary_attack_action_for_obs_slot(found_slot) : 0; + if (cmd->spell == OSRS_SPELL_BLOOD_BARRAGE) + actions[INF_HEAD_SPELL] = OSRS_SPELL_BLOOD_BARRAGE; + else if (cmd->spell == OSRS_SPELL_ICE_BARRAGE) + actions[INF_HEAD_SPELL] = OSRS_SPELL_ICE_BARRAGE; s->player_dest_x = -1; s->player_dest_y = -1; break; } case HUMAN_COMMAND_OVERHEAD_PRAYER: - actions[INF_HEAD_PRAYER] = inf_local_overhead_action(cmd->overhead_prayer); + actions[INF_HEAD_PRAYER] = cmd->overhead_prayer; break; case HUMAN_COMMAND_OFFENSIVE_PRAYER: actions[INF_HEAD_OFFENSIVE] = cmd->offensive_prayer; @@ -1000,9 +1003,9 @@ static const EncounterDef ENCOUNTER_INFERNO = { .translate_human_input = inf_translate_human_input_ctx, .is_human_targetable_npc_slot = inf_is_human_targetable_npc_slot_ctx, .apply_lab_command = inf_apply_lab_command_ctx, - .head_move = INF_HEAD_MOVE, + .head_move = INF_HEAD_PRIMARY, .head_prayer = INF_HEAD_PRAYER, - .head_target = INF_HEAD_TARGET, + .head_target = INF_HEAD_PRIMARY, .render_post_tick = inf_render_post_tick_ctx, .get_log = inf_get_log_ctx, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc index ab40e0dd7d..0c6dd868cc 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -25,7 +25,7 @@ static void inf_seed_potion_cells( OSRS_INVENTORY_SIZE); abort(); } - s->inventory_cells[(*next_cell)++] = + s->player.inventory_cells[(*next_cell)++] = osrs_inventory_cell_from_content_code( osrs_inventory_content_code_from_consumable(kind, dose)); doses -= dose; @@ -34,7 +34,7 @@ static void inf_seed_potion_cells( static void inf_seed_inventory_cells(InfernoState* s) { for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) - s->inventory_cells[i] = osrs_inventory_cell_empty(); + s->player.inventory_cells[i] = osrs_inventory_cell_empty(); int cell = 0; const uint8_t* const* loadouts = @@ -47,7 +47,7 @@ static void inf_seed_inventory_cells(InfernoState* s) { int duplicate = 0; for (int c = 0; c < cell; c++) { if (osrs_inventory_cell_item_index( - &s->inventory_cells[c]) == item) { + &s->player.inventory_cells[c]) == item) { duplicate = 1; break; } @@ -58,7 +58,8 @@ static void inf_seed_inventory_cells(InfernoState* s) { OSRS_INVENTORY_SIZE); abort(); } - s->inventory_cells[cell++] = osrs_inventory_cell_from_item(item); + s->player.inventory_cells[cell++] = + osrs_inventory_cell_from_item(item); } } diff --git a/ocean/osrs/osrs_collision.h b/ocean/osrs/osrs_collision.h index 99cb38e893..9fc0ca47ab 100644 --- a/ocean/osrs/osrs_collision.h +++ b/ocean/osrs/osrs_collision.h @@ -1205,7 +1205,8 @@ static inline uint32_t encounter_arena_topology_nearby_unit_footprint_mask( return topology->nearby_unit_footprint_masks[index]; } -static inline int encounter_arena_topology_step_allowed( +static inline int +encounter_arena_topology_step_allowed_assume_finalized_size_in_range( const EncounterArenaTopology* topology, int x, int y, @@ -1213,11 +1214,6 @@ static inline int encounter_arena_topology_step_allowed( int dx, int dy ) { - encounter_arena_topology_require_finalized(topology); - encounter_arena_topology_require_footprint_size(topology, size); - if (dx < -1 || dx > 1 || dy < -1 || dy > 1 || - (dx == 0 && dy == 0)) - encounter_arena_topology_abort("step direction", 0); if (!encounter_arena_topology_contains_raw(topology, x, y)) return 0; int bit = (dy + 1) * 3 + (dx + 1); if (bit > 4) bit--; @@ -1226,6 +1222,23 @@ static inline int encounter_arena_topology_step_allowed( (uint8_t)(1u << bit)) != 0; } +static inline int encounter_arena_topology_step_allowed( + const EncounterArenaTopology* topology, + int x, + int y, + int size, + int dx, + int dy +) { + encounter_arena_topology_require_finalized(topology); + encounter_arena_topology_require_footprint_size(topology, size); + if (dx < -1 || dx > 1 || dy < -1 || dy > 1 || + (dx == 0 && dy == 0)) + encounter_arena_topology_abort("step direction", 0); + return encounter_arena_topology_step_allowed_assume_finalized_size_in_range( + topology, x, y, size, dx, dy); +} + static inline int encounter_arena_topology_los_clear_assume_finalized_footprints_in_bounds( const EncounterArenaTopology* topology, diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index 4d5eb7f943..849bf12b98 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -13,6 +13,7 @@ #include #include "osrs_types.h" #include "osrs_items.h" +#include "osrs_inventory.h" #include "osrs_pathfinding.h" #include "osrs_combat.h" #include "osrs_consumables.h" @@ -1713,32 +1714,50 @@ static void encounter_populate_inventory( const uint8_t* const* loadouts, int num_loadouts, const uint8_t extra_items[NUM_GEAR_SLOTS] ) { - memset(p->inventory, 255 , sizeof(p->inventory)); - memset(p->num_items_in_slot, 0, sizeof(p->num_items_in_slot)); - - for (int s = 0; s < NUM_GEAR_SLOTS; s++) { - int n = 0; - for (int l = 0; l < num_loadouts && n < MAX_ITEMS_PER_SLOT; l++) { - uint8_t item = loadouts[l][s]; - if (item == 255 ) continue; - int dup = 0; - for (int j = 0; j < n; j++) { if (p->inventory[s][j] == item) { dup = 1; break; } } - if (dup) continue; - p->inventory[s][n++] = item; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) + p->inventory_cells[cell] = osrs_inventory_cell_empty(); + int cell = 0; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + for (int loadout = 0; loadout < num_loadouts; loadout++) { + uint8_t item = loadouts[loadout][slot]; + if (item == ITEM_NONE || item == p->equipped[slot]) continue; + int duplicate = 0; + for (int previous = 0; previous < cell; previous++) { + if (osrs_inventory_cell_item_index( + &p->inventory_cells[previous]) == item) { + duplicate = 1; + break; + } + } + if (!duplicate && cell < OSRS_INVENTORY_SIZE) + p->inventory_cells[cell++] = + osrs_inventory_cell_from_item(item); } - if (extra_items && extra_items[s] != 255 && n < MAX_ITEMS_PER_SLOT) { - int dup = 0; - for (int j = 0; j < n; j++) { if (p->inventory[s][j] == extra_items[s]) { dup = 1; break; } } - if (!dup) p->inventory[s][n++] = extra_items[s]; + if (!extra_items || extra_items[slot] == ITEM_NONE || + extra_items[slot] == p->equipped[slot]) + continue; + int duplicate = 0; + for (int previous = 0; previous < cell; previous++) { + if (osrs_inventory_cell_item_index( + &p->inventory_cells[previous]) == extra_items[slot]) { + duplicate = 1; + break; + } } - p->num_items_in_slot[s] = n; + if (!duplicate && cell < OSRS_INVENTORY_SIZE) + p->inventory_cells[cell++] = + osrs_inventory_cell_from_item(extra_items[slot]); } } static inline void encounter_clear_ammo_inventory_slot(Player* p) { - for (int i = 0; i < MAX_ITEMS_PER_SLOT; i++) - p->inventory[GEAR_SLOT_AMMO][i] = ITEM_NONE; - p->num_items_in_slot[GEAR_SLOT_AMMO] = 0; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + uint8_t item = + osrs_inventory_cell_item_index(&p->inventory_cells[cell]); + if (item != ITEM_NONE && + osrs_item_gear_slot(item) == GEAR_SLOT_AMMO) + p->inventory_cells[cell] = osrs_inventory_cell_empty(); + } } static inline void encounter_translate_movement(HumanInput* hi, int* actions, diff --git a/ocean/osrs/osrs_encounter_player.h b/ocean/osrs/osrs_encounter_player.h index a26b60882e..43f360a2c0 100644 --- a/ocean/osrs/osrs_encounter_player.h +++ b/ocean/osrs/osrs_encounter_player.h @@ -3,6 +3,16 @@ #include "osrs_encounter.h" #include "osrs_interaction.h" +typedef struct { + int16_t destination_dx; + int16_t destination_dy; + int8_t first_dx; + int8_t first_dy; + int8_t run_dx; + int8_t run_dy; + uint16_t distance; + uint8_t outcome; +} OsrsLocalMoveRoute; typedef enum { OSRS_PLAYER_MOVE_NONE = 0, @@ -331,13 +341,11 @@ static inline int osrs_player_step_chase_target( return 0; } - int remaining_waypoints = - route->waypoint_count - route->waypoint_index; int reroute = - !osrs_player_step_route_matches(route, player, target, &input->arena) || - route->state == OSRS_INTERACTION_ROUTE_FAILED || - remaining_waypoints <= 1; - if (!reroute && !osrs_player_step_route_next_traversable(input)) + !osrs_player_step_route_matches(route, player, target, &input->arena); + if (!reroute && + route->state == OSRS_INTERACTION_ROUTE_READY && + !osrs_player_step_route_next_traversable(input)) reroute = 1; if (reroute) osrs_player_step_build_attack_route(input, target); diff --git a/ocean/osrs/osrs_entity_encoder.cu b/ocean/osrs/osrs_entity_encoder.cu new file mode 100644 index 0000000000..72003d551c --- /dev/null +++ b/ocean/osrs/osrs_entity_encoder.cu @@ -0,0 +1,786 @@ +static constexpr int OSRS_ENTITY_INV_START = 52; +static constexpr int OSRS_ENTITY_INV_NUM_RECORDS = 28; +static constexpr int OSRS_ENTITY_INV_OBS_FEATURES = 1; +static constexpr int OSRS_ENTITY_INV_FEATURES = OSRS_ITEM_OBS_TABLE_COLS; +static constexpr int OSRS_ENTITY_BOTTLENECK = 16; +static constexpr int OSRS_ENTITY_BATCH_TILE = 8; +static constexpr int OSRS_ENTITY_HIDDEN_TILE = 32; +static constexpr int OSRS_ENTITY_FC_THREADS = + OSRS_ENTITY_BATCH_TILE * OSRS_ENTITY_HIDDEN_TILE; + +static_assert(OSRS_ENTITY_INV_START == 52); +static_assert(OSRS_ENTITY_INV_NUM_RECORDS == 28); +static_assert(OSRS_ENTITY_INV_OBS_FEATURES == 1); +static_assert(OSRS_ENTITY_INV_FEATURES == 14); + +enum OsrsEntityBranchExpansion { + OSRS_ENTITY_BRANCH_TYPE_ONEHOT = 0, + OSRS_ENTITY_BRANCH_ITEM_TABLE, +}; + +struct OsrsEntityBranchDescriptor { + int obs_start; + int num_records; + int obs_features; + int type_onehot; + int type_code_scale; + OsrsEntityBranchExpansion expansion; +}; + +struct OsrsEntityEncoderDescriptor { + const char* env_name; + int obs_size; + const OsrsEntityBranchDescriptor* branches; + int num_branches; +}; + +struct OsrsEntityBranchWeights { + Prec l1_w; + Prec l2_w; +}; + +struct OsrsEntityEncoderWeights { + Prec global_w; + Prec inv_l1_w; + Prec inv_l2_w; + const OsrsEntityEncoderDescriptor* descriptor; + int obs_size; + int hidden; +}; + +struct OsrsEntityBranchActivations { + Prec flat; + Prec z1; + Prec h1; + Prec grad_z1; + Int pool_argmax; + Prec l1_wgrad; + Prec l2_wgrad; +}; + +struct OsrsEntityEncoderActivations { + Prec out; + Prec saved_obs; + Prec inv_flat; + Prec inv_z1; + Prec inv_h1; + Prec inv_grad_z1; + Int inv_pool_argmax; + Prec global_wgrad; + Prec inv_l1_wgrad; + Prec inv_l2_wgrad; +}; + +static OsrsEntityBranchWeights* osrs_entity_branch_weights( + OsrsEntityEncoderWeights* weights +) { + return (OsrsEntityBranchWeights*)(weights + 1); +} + +static OsrsEntityBranchActivations* osrs_entity_branch_activations( + OsrsEntityEncoderActivations* activations +) { + return (OsrsEntityBranchActivations*)(activations + 1); +} + +__device__ __forceinline__ float osrs_entity_gelu_fwd(float x) { + float inner = 0.7978845608028654f * (x + 0.044715f * x * x * x); + return 0.5f * x * (1.0f + tanhf(inner)); +} + +__device__ __forceinline__ float osrs_entity_gelu_grad(float x) { + float x3 = x * x * x; + float inner = 0.7978845608028654f * (x + 0.044715f * x3); + float t = tanhf(inner); + float dinner = 0.7978845608028654f * (1.0f + 3.0f * 0.044715f * x * x); + return 0.5f * (1.0f + t) + 0.5f * x * (1.0f - t * t) * dinner; +} + +__global__ void osrs_entity_gather_inventory( + precision_t* __restrict__ flat, + const precision_t* __restrict__ obs, + int B, + int obs_size +) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + int record_block = OSRS_ENTITY_INV_NUM_RECORDS * OSRS_ENTITY_INV_FEATURES; + int total = B * record_block; + if (idx >= total) return; + int b = idx / record_block; + int off = idx - b * record_block; + int record = off / OSRS_ENTITY_INV_FEATURES; + int feature = off - record * OSRS_ENTITY_INV_FEATURES; + const precision_t* src = obs + (int64_t)b * obs_size + OSRS_ENTITY_INV_START + + record * OSRS_ENTITY_INV_OBS_FEATURES; + int code = (int)lrintf(to_float(src[0]) * (float)OSRS_ITEM_OBS_CODE_SCALE); + assert(code >= 0 && code < OSRS_ITEM_OBS_TABLE_ROWS); + float value = OSRS_ITEM_OBS_TABLE_DEV[code][feature]; + flat[idx] = from_float(value); +} + +__global__ void osrs_entity_gather_branch( + precision_t* __restrict__ flat, + const precision_t* __restrict__ obs, + int B, + int obs_size, + int obs_start, + int num_records, + int obs_features, + int type_onehot, + int type_code_scale, + OsrsEntityBranchExpansion expansion +) { + int features = expansion == OSRS_ENTITY_BRANCH_ITEM_TABLE + ? OSRS_ITEM_OBS_TABLE_COLS + : type_onehot + obs_features - 1; + int record_block = num_records * features; + int idx = blockIdx.x * blockDim.x + threadIdx.x; + int total = B * record_block; + if (idx >= total) return; + int b = idx / record_block; + int off = idx - b * record_block; + int record = off / features; + int feature = off - record * features; + const precision_t* src = obs + (int64_t)b * obs_size + obs_start + + record * obs_features; + int code = (int)lrintf(to_float(src[0]) * (float)type_code_scale); + if (expansion == OSRS_ENTITY_BRANCH_ITEM_TABLE) { + assert(code >= 0 && code < OSRS_ITEM_OBS_TABLE_ROWS); + flat[idx] = from_float(OSRS_ITEM_OBS_TABLE_DEV[code][feature]); + } else if (feature < type_onehot) { + assert(code >= 0 && code <= type_onehot); + flat[idx] = from_float(code == feature + 1 ? 1.0f : 0.0f); + } else { + flat[idx] = src[1 + feature - type_onehot]; + } +} + +__global__ void osrs_entity_fused_pool_fwd( + precision_t* __restrict__ out, + int* __restrict__ argmax, + precision_t* __restrict__ h1, + const precision_t* __restrict__ z1, + const precision_t* __restrict__ record_flat, + const precision_t* __restrict__ l2_w, + int B, + int H, + int num_records, + int record_features, + int active_width +) { + extern __shared__ float osrs_entity_pool_shared[]; + float* h1_tile = osrs_entity_pool_shared; + float* mask_tile = h1_tile + + OSRS_ENTITY_BATCH_TILE * num_records * OSRS_ENTITY_BOTTLENECK; + float* w_tile = mask_tile + OSRS_ENTITY_BATCH_TILE * num_records; + int batch_base = blockIdx.x * OSRS_ENTITY_BATCH_TILE; + int hidden_base = blockIdx.y * OSRS_ENTITY_HIDDEN_TILE; + int tx = threadIdx.x; + int ty = threadIdx.y; + int tid = ty * OSRS_ENTITY_HIDDEN_TILE + tx; + int h1_values = OSRS_ENTITY_BATCH_TILE * num_records * OSRS_ENTITY_BOTTLENECK; + for (int idx = tid; idx < h1_values; idx += OSRS_ENTITY_FC_THREADS) { + int bt = idx / (num_records * OSRS_ENTITY_BOTTLENECK); + int rem = idx - bt * num_records * OSRS_ENTITY_BOTTLENECK; + int gb = batch_base + bt; + float value = gb < B ? osrs_entity_gelu_fwd(to_float( + z1[(int64_t)gb * num_records * OSRS_ENTITY_BOTTLENECK + rem])) : 0.0f; + h1_tile[idx] = value; + if (h1 && blockIdx.y == 0 && gb < B) + h1[(int64_t)gb * num_records * OSRS_ENTITY_BOTTLENECK + rem] = from_float(value); + } + int mask_values = OSRS_ENTITY_BATCH_TILE * num_records; + for (int idx = tid; idx < mask_values; idx += OSRS_ENTITY_FC_THREADS) { + int bt = idx / num_records; + int record = idx - bt * num_records; + int gb = batch_base + bt; + float active = 0.0f; + if (gb < B) { + const precision_t* rec = record_flat + + ((int64_t)gb * num_records + record) * record_features; + for (int feature = 0; feature < active_width; feature++) + active += to_float(rec[feature]); + } + mask_tile[idx] = active; + } + int w_values = OSRS_ENTITY_HIDDEN_TILE * OSRS_ENTITY_BOTTLENECK; + for (int idx = tid; idx < w_values; idx += OSRS_ENTITY_FC_THREADS) { + int d = idx / OSRS_ENTITY_HIDDEN_TILE; + int th = idx - d * OSRS_ENTITY_HIDDEN_TILE; + int gh = hidden_base + th; + w_tile[idx] = gh < H + ? to_float(l2_w[(int64_t)gh * OSRS_ENTITY_BOTTLENECK + d]) : 0.0f; + } + __syncthreads(); + int b = batch_base + ty; + int h = hidden_base + tx; + if (b >= B || h >= H) return; + float best = -3.4028234663852886e38f; + int best_record = -1; + for (int record = 0; record < num_records; record++) { + if (mask_tile[ty * num_records + record] <= 0.0f) continue; + const float* hp = h1_tile + + (ty * num_records + record) * OSRS_ENTITY_BOTTLENECK; + float sum = 0.0f; +#pragma unroll + for (int d = 0; d < OSRS_ENTITY_BOTTLENECK; d++) + sum += w_tile[d * OSRS_ENTITY_HIDDEN_TILE + tx] * hp[d]; + if (sum > best) { best = sum; best_record = record; } + } + int64_t out_idx = (int64_t)b * H + h; + out[out_idx] = from_float( + to_float(out[out_idx]) + (best_record < 0 ? 0.0f : best)); + argmax[out_idx] = best_record; +} + +__global__ void osrs_entity_fused_l2_wgrad( + precision_t* __restrict__ wgrad, + const precision_t* __restrict__ grad, + const precision_t* __restrict__ h1, + const int* __restrict__ argmax, + int B, + int H, + int num_records +) { + int h = blockIdx.x; + if (h >= H) return; + float sum[OSRS_ENTITY_BOTTLENECK]; +#pragma unroll + for (int k = 0; k < OSRS_ENTITY_BOTTLENECK; k++) sum[k] = 0.0f; + for (int b = threadIdx.x; b < B; b += blockDim.x) { + int record = argmax[(int64_t)b * H + h]; + if (record < 0) continue; + float g = to_float(grad[(int64_t)b * H + h]); + const precision_t* hp = h1 + + ((int64_t)b * num_records + record) * OSRS_ENTITY_BOTTLENECK; +#pragma unroll + for (int k = 0; k < OSRS_ENTITY_BOTTLENECK; k++) + sum[k] += g * to_float(hp[k]); + } + __shared__ float warp_sums[OSRS_ENTITY_BOTTLENECK * 32]; + int lane = threadIdx.x & 31; + int warp = threadIdx.x >> 5; + int num_warps = (blockDim.x + 31) >> 5; +#pragma unroll + for (int k = 0; k < OSRS_ENTITY_BOTTLENECK; k++) { + float value = sum[k]; + for (int offset = 16; offset > 0; offset >>= 1) + value += __shfl_down_sync(0xffffffff, value, offset); + if (lane == 0) warp_sums[k * 32 + warp] = value; + } + __syncthreads(); + if (warp == 0) { +#pragma unroll + for (int k = 0; k < OSRS_ENTITY_BOTTLENECK; k++) { + float value = lane < num_warps ? warp_sums[k * 32 + lane] : 0.0f; + for (int offset = 16; offset > 0; offset >>= 1) + value += __shfl_down_sync(0xffffffff, value, offset); + if (lane == 0) + wgrad[(int64_t)h * OSRS_ENTITY_BOTTLENECK + k] = from_float(value); + } + } +} + +__global__ void osrs_entity_fused_grad_z1( + precision_t* __restrict__ grad_z1, + const precision_t* __restrict__ grad, + const precision_t* __restrict__ l2_w, + const precision_t* __restrict__ z1, + const int* __restrict__ argmax, + int B, + int H, + int num_records +) { + int b = blockIdx.x; + if (b >= B) return; + extern __shared__ float osrs_entity_grad_shared[]; + float* accum = osrs_entity_grad_shared; + int* arg_s = (int*)(accum + num_records * OSRS_ENTITY_BOTTLENECK); + float* grad_s = (float*)(arg_s + blockDim.x); + for (int idx = threadIdx.x; + idx < num_records * OSRS_ENTITY_BOTTLENECK; + idx += blockDim.x) + accum[idx] = 0.0f; + __syncthreads(); + for (int base = 0; base < H; base += blockDim.x) { + int h = base + threadIdx.x; + if (h < H) { + arg_s[threadIdx.x] = argmax[(int64_t)b * H + h]; + grad_s[threadIdx.x] = to_float(grad[(int64_t)b * H + h]); + } + __syncthreads(); + int tile = H - base; + if (tile > (int)blockDim.x) tile = blockDim.x; + for (int idx = threadIdx.x; + idx < num_records * OSRS_ENTITY_BOTTLENECK; + idx += blockDim.x) { + int record = idx / OSRS_ENTITY_BOTTLENECK; + int k = idx - record * OSRS_ENTITY_BOTTLENECK; + float sum = 0.0f; + for (int j = 0; j < tile; j++) { + if (arg_s[j] != record) continue; + sum += grad_s[j] * to_float( + l2_w[(int64_t)(base + j) * OSRS_ENTITY_BOTTLENECK + k]); + } + accum[idx] += sum; + } + __syncthreads(); + } + for (int idx = threadIdx.x; + idx < num_records * OSRS_ENTITY_BOTTLENECK; + idx += blockDim.x) { + int64_t out_idx = + (int64_t)b * num_records * OSRS_ENTITY_BOTTLENECK + idx; + grad_z1[out_idx] = from_float( + accum[idx] * osrs_entity_gelu_grad(to_float(z1[out_idx]))); + } +} + +static void osrs_entity_launch_fused_fwd( + precision_t* out, + int* argmax, + precision_t* h1, + const precision_t* z1, + const precision_t* record_flat, + const precision_t* l2_w, + int B, + int H, + int num_records, + int record_features, + int active_width, + cudaStream_t stream +) { + dim3 block(OSRS_ENTITY_HIDDEN_TILE, OSRS_ENTITY_BATCH_TILE); + dim3 grid( + (B + OSRS_ENTITY_BATCH_TILE - 1) / OSRS_ENTITY_BATCH_TILE, + (H + OSRS_ENTITY_HIDDEN_TILE - 1) / OSRS_ENTITY_HIDDEN_TILE); + size_t shared_bytes = ( + (size_t)OSRS_ENTITY_BATCH_TILE * num_records * OSRS_ENTITY_BOTTLENECK + + (size_t)OSRS_ENTITY_BATCH_TILE * num_records + + (size_t)OSRS_ENTITY_HIDDEN_TILE * OSRS_ENTITY_BOTTLENECK) * sizeof(float); + osrs_entity_fused_pool_fwd<<>>( + out, argmax, h1, z1, record_flat, l2_w, + B, H, num_records, record_features, active_width); +} + +static void osrs_entity_launch_fused_bwd( + precision_t* l2_wgrad, + precision_t* grad_z1, + const precision_t* grad, + const precision_t* l2_w, + const precision_t* z1, + const precision_t* h1, + const int* argmax, + int B, + int H, + int num_records, + cudaStream_t stream +) { + osrs_entity_fused_l2_wgrad<<>>( + l2_wgrad, grad, h1, argmax, B, H, num_records); + size_t shared_bytes = + ((size_t)num_records * OSRS_ENTITY_BOTTLENECK + 2 * BLOCK_SIZE) * + sizeof(float); + osrs_entity_fused_grad_z1<<>>( + grad_z1, grad, l2_w, z1, argmax, B, H, num_records); +} + +static int osrs_entity_branch_features(const OsrsEntityBranchDescriptor* branch) { + return branch->expansion == OSRS_ENTITY_BRANCH_ITEM_TABLE + ? OSRS_ITEM_OBS_TABLE_COLS + : branch->type_onehot + branch->obs_features - 1; +} + +static Prec osrs_entity_encoder_forward( + void* weights, + void* activations, + Prec input, + cudaStream_t stream +) { + OsrsEntityEncoderWeights* ew = (OsrsEntityEncoderWeights*)weights; + OsrsEntityEncoderActivations* a = (OsrsEntityEncoderActivations*)activations; + int B = input.shape[0]; + int H = ew->hidden; + if (a->saved_obs.data) puf_copy(&a->saved_obs, &input, stream); + puf_mm(&input, &ew->global_w, &a->out, stream); + + int inventory_batch = B * OSRS_ENTITY_INV_NUM_RECORDS; + osrs_entity_gather_inventory<<< + grid_size(B * OSRS_ENTITY_INV_NUM_RECORDS * OSRS_ENTITY_INV_FEATURES), + BLOCK_SIZE, 0, stream>>>(a->inv_flat.data, input.data, B, ew->obs_size); + Prec inventory_2d = { + .data = a->inv_flat.data, + .shape = {inventory_batch, OSRS_ENTITY_INV_FEATURES}, + }; + puf_mm(&inventory_2d, &ew->inv_l1_w, &a->inv_z1, stream); + osrs_entity_launch_fused_fwd( + a->out.data, a->inv_pool_argmax.data, a->inv_h1.data, + a->inv_z1.data, a->inv_flat.data, ew->inv_l2_w.data, + B, H, OSRS_ENTITY_INV_NUM_RECORDS, OSRS_ENTITY_INV_FEATURES, 1, stream); + + for (int branch_idx = 0; + branch_idx < ew->descriptor->num_branches; + branch_idx++) { + const OsrsEntityBranchDescriptor* descriptor = + &ew->descriptor->branches[branch_idx]; + OsrsEntityBranchWeights* bw = + &osrs_entity_branch_weights(ew)[branch_idx]; + OsrsEntityBranchActivations* ba = + &osrs_entity_branch_activations(a)[branch_idx]; + int features = osrs_entity_branch_features(descriptor); + int branch_batch = B * descriptor->num_records; + osrs_entity_gather_branch<<< + grid_size(branch_batch * features), BLOCK_SIZE, 0, stream>>>( + ba->flat.data, input.data, B, ew->obs_size, + descriptor->obs_start, descriptor->num_records, + descriptor->obs_features, descriptor->type_onehot, + descriptor->type_code_scale, descriptor->expansion); + Prec branch_2d = { + .data = ba->flat.data, + .shape = {branch_batch, features}, + }; + puf_mm(&branch_2d, &bw->l1_w, &ba->z1, stream); + osrs_entity_launch_fused_fwd( + a->out.data, ba->pool_argmax.data, ba->h1.data, + ba->z1.data, ba->flat.data, bw->l2_w.data, + B, H, descriptor->num_records, features, + descriptor->expansion == OSRS_ENTITY_BRANCH_ITEM_TABLE + ? 1 : descriptor->type_onehot, + stream); + } + return a->out; +} + +static void osrs_entity_encoder_backward( + void* weights, + void* activations, + Prec grad, + cudaStream_t stream +) { + OsrsEntityEncoderWeights* ew = (OsrsEntityEncoderWeights*)weights; + OsrsEntityEncoderActivations* a = (OsrsEntityEncoderActivations*)activations; + int B = grad.shape[0]; + int H = ew->hidden; + puf_mm_tn(&grad, &a->saved_obs, &a->global_wgrad, stream); + + int inventory_batch = B * OSRS_ENTITY_INV_NUM_RECORDS; + osrs_entity_launch_fused_bwd( + a->inv_l2_wgrad.data, a->inv_grad_z1.data, grad.data, + ew->inv_l2_w.data, a->inv_z1.data, a->inv_h1.data, + a->inv_pool_argmax.data, B, H, OSRS_ENTITY_INV_NUM_RECORDS, stream); + Prec inventory_2d = { + .data = a->inv_flat.data, + .shape = {inventory_batch, OSRS_ENTITY_INV_FEATURES}, + }; + puf_mm_tn(&a->inv_grad_z1, &inventory_2d, &a->inv_l1_wgrad, stream); + + for (int branch_idx = 0; + branch_idx < ew->descriptor->num_branches; + branch_idx++) { + const OsrsEntityBranchDescriptor* descriptor = + &ew->descriptor->branches[branch_idx]; + OsrsEntityBranchWeights* bw = + &osrs_entity_branch_weights(ew)[branch_idx]; + OsrsEntityBranchActivations* ba = + &osrs_entity_branch_activations(a)[branch_idx]; + int features = osrs_entity_branch_features(descriptor); + int branch_batch = B * descriptor->num_records; + osrs_entity_launch_fused_bwd( + ba->l2_wgrad.data, ba->grad_z1.data, grad.data, + bw->l2_w.data, ba->z1.data, ba->h1.data, ba->pool_argmax.data, + B, H, descriptor->num_records, stream); + Prec branch_2d = { + .data = ba->flat.data, + .shape = {branch_batch, features}, + }; + puf_mm_tn(&ba->grad_z1, &branch_2d, &ba->l1_wgrad, stream); + } +} + +static void osrs_entity_encoder_init_weights( + void* weights, + uint64_t* seed, + cudaStream_t stream +) { + OsrsEntityEncoderWeights* ew = (OsrsEntityEncoderWeights*)weights; + auto init2d = [&](Prec& tensor, int rows, int cols) { + Prec shaped = {.data = tensor.data, .shape = {rows, cols}}; + puf_kaiming_init(&shaped, sqrtf(2.0f), (*seed)++, stream); + }; + init2d(ew->global_w, ew->hidden, ew->obs_size); + init2d(ew->inv_l1_w, OSRS_ENTITY_BOTTLENECK, OSRS_ENTITY_INV_FEATURES); + init2d(ew->inv_l2_w, ew->hidden, OSRS_ENTITY_BOTTLENECK); + for (int branch_idx = 0; + branch_idx < ew->descriptor->num_branches; + branch_idx++) { + int features = osrs_entity_branch_features( + &ew->descriptor->branches[branch_idx]); + OsrsEntityBranchWeights* bw = + &osrs_entity_branch_weights(ew)[branch_idx]; + init2d(bw->l1_w, OSRS_ENTITY_BOTTLENECK, features); + init2d(bw->l2_w, ew->hidden, OSRS_ENTITY_BOTTLENECK); + } +} + +static void osrs_entity_assert_aligned( + const OsrsEntityEncoderWeights* ew, + int64_t num_elements, + const char* name +) { + if (num_elements % 8 != 0) { + fprintf(stderr, "%s entity encoder: %s numel %lld not a multiple of 8\n", + ew->descriptor->env_name, name, (long long)num_elements); + abort(); + } +} + +static void osrs_entity_register_param( + OsrsEntityEncoderWeights* ew, + Allocator* allocator, + Prec* tensor, + const char* name +) { + osrs_entity_assert_aligned(ew, numel(tensor->shape), name); + alloc_register(allocator, tensor); +} + +static void osrs_entity_encoder_reg_params(void* weights, Allocator* allocator) { + OsrsEntityEncoderWeights* ew = (OsrsEntityEncoderWeights*)weights; + ew->global_w = {.shape = {ew->hidden, ew->obs_size}}; + osrs_entity_register_param(ew, allocator, &ew->global_w, "global_w"); + ew->inv_l1_w = {.shape = {OSRS_ENTITY_BOTTLENECK, OSRS_ENTITY_INV_FEATURES}}; + ew->inv_l2_w = {.shape = {ew->hidden, OSRS_ENTITY_BOTTLENECK}}; + osrs_entity_register_param(ew, allocator, &ew->inv_l1_w, "inv_l1_w"); + osrs_entity_register_param(ew, allocator, &ew->inv_l2_w, "inv_l2_w"); + for (int branch_idx = 0; + branch_idx < ew->descriptor->num_branches; + branch_idx++) { + int features = osrs_entity_branch_features( + &ew->descriptor->branches[branch_idx]); + OsrsEntityBranchWeights* bw = + &osrs_entity_branch_weights(ew)[branch_idx]; + bw->l1_w = {.shape = {OSRS_ENTITY_BOTTLENECK, features}}; + bw->l2_w = {.shape = {ew->hidden, OSRS_ENTITY_BOTTLENECK}}; + osrs_entity_register_param(ew, allocator, &bw->l1_w, "entity_l1_w"); + osrs_entity_register_param(ew, allocator, &bw->l2_w, "entity_l2_w"); + } +} + +static void osrs_entity_register_branch_train( + const OsrsEntityBranchDescriptor* descriptor, + OsrsEntityBranchActivations* branch, + Allocator* acts, + Allocator* grads, + int B, + int H +) { + int features = osrs_entity_branch_features(descriptor); + int record_batch = B * descriptor->num_records; + branch->flat = {.shape = {record_batch, features}}; + branch->z1 = {.shape = {record_batch, OSRS_ENTITY_BOTTLENECK}}; + branch->h1 = {.shape = {record_batch, OSRS_ENTITY_BOTTLENECK}}; + branch->grad_z1 = {.shape = {record_batch, OSRS_ENTITY_BOTTLENECK}}; + branch->pool_argmax = {.shape = {B, H}}; + alloc_register(acts, &branch->flat); + alloc_register(acts, &branch->z1); + alloc_register(acts, &branch->h1); + alloc_register(acts, &branch->grad_z1); + alloc_register(acts, &branch->pool_argmax); + branch->l1_wgrad = {.shape = {OSRS_ENTITY_BOTTLENECK, features}}; + branch->l2_wgrad = {.shape = {H, OSRS_ENTITY_BOTTLENECK}}; + alloc_register(grads, &branch->l1_wgrad); + alloc_register(grads, &branch->l2_wgrad); +} + +static void osrs_entity_encoder_reg_train( + void* weights, + void* activations, + Allocator* acts, + Allocator* grads, + int B +) { + OsrsEntityEncoderWeights* ew = (OsrsEntityEncoderWeights*)weights; + OsrsEntityEncoderActivations* a = (OsrsEntityEncoderActivations*)activations; + int H = ew->hidden; + *a = {}; + a->out = {.shape = {B, H}}; + a->saved_obs = {.shape = {B, ew->obs_size}}; + a->inv_flat = { + .shape = {B * OSRS_ENTITY_INV_NUM_RECORDS, OSRS_ENTITY_INV_FEATURES}, + }; + a->inv_z1 = { + .shape = {B * OSRS_ENTITY_INV_NUM_RECORDS, OSRS_ENTITY_BOTTLENECK}, + }; + a->inv_h1 = a->inv_z1; + a->inv_grad_z1 = a->inv_z1; + a->inv_pool_argmax = {.shape = {B, H}}; + alloc_register(acts, &a->out); + alloc_register(acts, &a->saved_obs); + alloc_register(acts, &a->inv_flat); + alloc_register(acts, &a->inv_z1); + alloc_register(acts, &a->inv_h1); + alloc_register(acts, &a->inv_grad_z1); + alloc_register(acts, &a->inv_pool_argmax); + a->global_wgrad = {.shape = {H, ew->obs_size}}; + a->inv_l1_wgrad = { + .shape = {OSRS_ENTITY_BOTTLENECK, OSRS_ENTITY_INV_FEATURES}, + }; + a->inv_l2_wgrad = {.shape = {H, OSRS_ENTITY_BOTTLENECK}}; + alloc_register(grads, &a->global_wgrad); + alloc_register(grads, &a->inv_l1_wgrad); + alloc_register(grads, &a->inv_l2_wgrad); + for (int branch_idx = 0; + branch_idx < ew->descriptor->num_branches; + branch_idx++) { + osrs_entity_register_branch_train( + &ew->descriptor->branches[branch_idx], + &osrs_entity_branch_activations(a)[branch_idx], + acts, grads, B, H); + } +} + +static void osrs_entity_register_branch_rollout( + const OsrsEntityBranchDescriptor* descriptor, + OsrsEntityBranchActivations* branch, + Allocator* allocator, + int B, + int H +) { + int features = osrs_entity_branch_features(descriptor); + int record_batch = B * descriptor->num_records; + branch->flat = {.shape = {record_batch, features}}; + branch->z1 = {.shape = {record_batch, OSRS_ENTITY_BOTTLENECK}}; + branch->pool_argmax = {.shape = {B, H}}; + alloc_register(allocator, &branch->flat); + alloc_register(allocator, &branch->z1); + alloc_register(allocator, &branch->pool_argmax); +} + +static void osrs_entity_encoder_reg_rollout( + void* weights, + void* activations, + Allocator* allocator, + int B +) { + OsrsEntityEncoderWeights* ew = (OsrsEntityEncoderWeights*)weights; + OsrsEntityEncoderActivations* a = (OsrsEntityEncoderActivations*)activations; + int H = ew->hidden; + a->out = {.shape = {B, H}}; + a->inv_flat = { + .shape = {B * OSRS_ENTITY_INV_NUM_RECORDS, OSRS_ENTITY_INV_FEATURES}, + }; + a->inv_z1 = { + .shape = {B * OSRS_ENTITY_INV_NUM_RECORDS, OSRS_ENTITY_BOTTLENECK}, + }; + a->inv_pool_argmax = {.shape = {B, H}}; + alloc_register(allocator, &a->out); + alloc_register(allocator, &a->inv_flat); + alloc_register(allocator, &a->inv_z1); + alloc_register(allocator, &a->inv_pool_argmax); + for (int branch_idx = 0; + branch_idx < ew->descriptor->num_branches; + branch_idx++) { + osrs_entity_register_branch_rollout( + &ew->descriptor->branches[branch_idx], + &osrs_entity_branch_activations(a)[branch_idx], + allocator, B, H); + } +} + +static void* osrs_entity_encoder_create_weights( + void* self, + const OsrsEntityEncoderDescriptor* descriptor +) { + Encoder* encoder = (Encoder*)self; + if (descriptor->num_branches < 0) { + fprintf(stderr, "%s entity encoder: invalid branch count %d\n", + descriptor->env_name, descriptor->num_branches); + abort(); + } + if (encoder->in_dim != descriptor->obs_size) { + fprintf(stderr, "%s entity encoder: env obs size %d != descriptor %d\n", + descriptor->env_name, encoder->in_dim, descriptor->obs_size); + abort(); + } + size_t weights_size = sizeof(OsrsEntityEncoderWeights) + + (size_t)descriptor->num_branches * sizeof(OsrsEntityBranchWeights); + OsrsEntityEncoderWeights* ew = + (OsrsEntityEncoderWeights*)calloc(1, weights_size); + ew->descriptor = descriptor; + ew->obs_size = encoder->in_dim; + ew->hidden = encoder->out_dim; + return ew; +} + +static void osrs_entity_encoder_configure( + Encoder* encoder, + create_weights_fn create_weights, + const OsrsEntityEncoderDescriptor* descriptor +) { + *encoder = Encoder{ + .forward = osrs_entity_encoder_forward, + .backward = osrs_entity_encoder_backward, + .init_weights = osrs_entity_encoder_init_weights, + .reg_params = osrs_entity_encoder_reg_params, + .reg_train = osrs_entity_encoder_reg_train, + .reg_rollout = osrs_entity_encoder_reg_rollout, + .create_weights = create_weights, + .in_dim = encoder->in_dim, + .out_dim = encoder->out_dim, + .activation_size = sizeof(OsrsEntityEncoderActivations) + + (size_t)descriptor->num_branches * + sizeof(OsrsEntityBranchActivations), + }; +} + +static constexpr OsrsEntityBranchDescriptor OSRS_EQUIPMENT_ENTITY_BRANCH[] = { + { + .obs_start = 80, + .num_records = NUM_GEAR_SLOTS, + .obs_features = 1, + .type_onehot = 0, + .type_code_scale = OSRS_ITEM_OBS_CODE_SCALE, + .expansion = OSRS_ENTITY_BRANCH_ITEM_TABLE, + }, +}; + +static constexpr OsrsEntityEncoderDescriptor OSRS_ZULRAH_ENTITY_DESCRIPTOR = { + .env_name = "osrs_zulrah", + .obs_size = 205, + .branches = OSRS_EQUIPMENT_ENTITY_BRANCH, + .num_branches = 1, +}; +#ifdef ZUL_NUM_OBS +static_assert(OSRS_ZULRAH_ENTITY_DESCRIPTOR.obs_size == ZUL_NUM_OBS); +#endif + +static constexpr OsrsEntityEncoderDescriptor OSRS_PVP_ENTITY_DESCRIPTOR = { + .env_name = "osrs_pvp", + .obs_size = 133, + .branches = OSRS_EQUIPMENT_ENTITY_BRANCH, + .num_branches = 1, +}; + +static void* osrs_zulrah_entity_encoder_create_weights(void* self) { + return osrs_entity_encoder_create_weights( + self, &OSRS_ZULRAH_ENTITY_DESCRIPTOR); +} + +static void* osrs_pvp_entity_encoder_create_weights(void* self) { + return osrs_entity_encoder_create_weights( + self, &OSRS_PVP_ENTITY_DESCRIPTOR); +} + +static void create_osrs_zulrah_encoder(Encoder* encoder) { + osrs_entity_encoder_configure( + encoder, + osrs_zulrah_entity_encoder_create_weights, + &OSRS_ZULRAH_ENTITY_DESCRIPTOR); +} + +static void create_osrs_pvp_encoder(Encoder* encoder) { + osrs_entity_encoder_configure( + encoder, + osrs_pvp_entity_encoder_create_weights, + &OSRS_PVP_ENTITY_DESCRIPTOR); +} diff --git a/ocean/osrs/osrs_gui.h b/ocean/osrs/osrs_gui.h index cf0f8ab024..068951b3a8 100644 --- a/ocean/osrs/osrs_gui.h +++ b/ocean/osrs/osrs_gui.h @@ -1790,68 +1790,58 @@ static void gui_reset_inventory_ui_state(GuiState* gs) { static void gui_populate_inventory(GuiState* gs, Player* p) { memset(gs->inv_grid, 0, sizeof(gs->inv_grid)); - int n = 0; - - for (int s = 0; s < NUM_GEAR_SLOTS && n < INV_GRID_SLOTS; s++) { - for (int i = 0; i < p->num_items_in_slot[s] && n < INV_GRID_SLOTS; i++) { - uint8_t item = p->inventory[s][i]; - if (item == ITEM_NONE) continue; - int is_equipped = 0; - for (int e = 0; e < NUM_GEAR_SLOTS; e++) { - if (p->equipped[e] == item) { is_equipped = 1; break; } - } - if (is_equipped) continue; - int dup = 0; - for (int j = 0; j < n; j++) { - if (gs->inv_grid[j].type == INV_SLOT_EQUIPMENT && - gs->inv_grid[j].item_db_idx == item) { dup = 1; break; } - } - if (dup) continue; - gs->inv_grid[n].type = INV_SLOT_EQUIPMENT; - gs->inv_grid[n].item_db_idx = item; - gs->inv_grid[n].osrs_id = ITEM_DATABASE[item].item_id; - n++; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE && + cell < INV_GRID_SLOTS; cell++) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&p->inventory_cells[cell]); + if (metadata->item_idx != ITEM_NONE) { + gs->inv_grid[cell].type = INV_SLOT_EQUIPMENT; + gs->inv_grid[cell].item_db_idx = metadata->item_idx; + gs->inv_grid[cell].osrs_id = + ITEM_DATABASE[metadata->item_idx].item_id; + continue; + } + gs->inv_grid[cell].osrs_id = metadata->raw_osrs_id; + switch ((OsrsConsumableKind)metadata->consumable_kind) { + case OSRS_CONSUMABLE_SHARK_FOOD: + gs->inv_grid[cell].type = INV_SLOT_FOOD; + break; + case OSRS_CONSUMABLE_KARAMBWAN: + gs->inv_grid[cell].type = INV_SLOT_KARAMBWAN; + break; + case OSRS_CONSUMABLE_BREW: + gs->inv_grid[cell].type = INV_SLOT_BREW; + break; + case OSRS_CONSUMABLE_SUPER_RESTORE: + gs->inv_grid[cell].type = INV_SLOT_RESTORE; + break; + case OSRS_CONSUMABLE_SUPER_COMBAT: + case OSRS_CONSUMABLE_DIVINE_COMBAT: + gs->inv_grid[cell].type = INV_SLOT_COMBAT_POT; + break; + case OSRS_CONSUMABLE_RANGING: + case OSRS_CONSUMABLE_DIVINE_RANGING: + gs->inv_grid[cell].type = INV_SLOT_RANGED_POT; + break; + case OSRS_CONSUMABLE_BASTION: + gs->inv_grid[cell].type = INV_SLOT_BASTION_POT; + break; + case OSRS_CONSUMABLE_STAMINA: + gs->inv_grid[cell].type = INV_SLOT_STAMINA_POT; + break; + case OSRS_CONSUMABLE_ANTIVENOM_PLUS: + gs->inv_grid[cell].type = INV_SLOT_ANTIVENOM; + break; + case OSRS_CONSUMABLE_PRAYER_RESTORE: + gs->inv_grid[cell].type = INV_SLOT_PRAYER_POT; + break; + case OSRS_CONSUMABLE_SATURATED_HEART: + gs->inv_grid[cell].type = INV_SLOT_SATURATED_HEART; + break; + default: + break; } } - - for (int i = 0; i < p->food_count && n < INV_GRID_SLOTS; i++) { - gs->inv_grid[n].type = INV_SLOT_FOOD; - gs->inv_grid[n].osrs_id = OSRS_ID_SHARK; - n++; - } - for (int i = 0; i < p->karambwan_count && n < INV_GRID_SLOTS; i++) { - gs->inv_grid[n].type = INV_SLOT_KARAMBWAN; - gs->inv_grid[n].osrs_id = OSRS_ID_KARAMBWAN; - n++; - } - - #define ADD_POTION_VIALS(doses_total, slot_type) do { \ - int _rem = (doses_total); \ - while (_rem > 0 && n < INV_GRID_SLOTS) { \ - int _d = (_rem >= 4) ? 4 : _rem; \ - gs->inv_grid[n].type = (slot_type); \ - gs->inv_grid[n].osrs_id = gui_consumable_osrs_id((slot_type), _d); \ - _rem -= _d; \ - n++; \ - } \ - } while(0) - - ADD_POTION_VIALS(p->brew_doses, INV_SLOT_BREW); - ADD_POTION_VIALS(p->restore_doses, INV_SLOT_RESTORE); - ADD_POTION_VIALS(p->combat_potion_doses, INV_SLOT_COMBAT_POT); - ADD_POTION_VIALS(p->ranged_potion_doses, INV_SLOT_RANGED_POT); - ADD_POTION_VIALS(p->bastion_doses, INV_SLOT_BASTION_POT); - ADD_POTION_VIALS(p->stamina_doses, INV_SLOT_STAMINA_POT); - ADD_POTION_VIALS(p->antivenom_doses, INV_SLOT_ANTIVENOM); - ADD_POTION_VIALS(p->prayer_pot_doses, INV_SLOT_PRAYER_POT); - #undef ADD_POTION_VIALS - - for (int i = 0; i < p->saturated_heart_count && n < INV_GRID_SLOTS; i++) { - gs->inv_grid[n].type = INV_SLOT_SATURATED_HEART; - gs->inv_grid[n].osrs_id = OSRS_ID_SATURATED_HEART; - n++; - } - gui_snapshot_inventory_state(gs, p); } @@ -1926,11 +1916,13 @@ static void gui_inv_update_potion_doses(GuiState* gs, InvSlotType type, } static int gui_player_loadout_contains(const Player* p, uint8_t item_db_idx) { - for (int g = 0; g < NUM_GEAR_SLOTS; g++) { - for (int i = 0; i < p->num_items_in_slot[g]; i++) { - if (p->inventory[g][i] == item_db_idx) return 1; - } + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + if (osrs_inventory_cell_item_index( + &p->inventory_cells[cell]) == item_db_idx) + return 1; } + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + if (p->equipped[slot] == item_db_idx) return 1; return 0; } diff --git a/ocean/osrs/osrs_human_input.h b/ocean/osrs/osrs_human_input.h index d3b10ea0ec..2fc02ccbac 100644 --- a/ocean/osrs/osrs_human_input.h +++ b/ocean/osrs/osrs_human_input.h @@ -134,13 +134,13 @@ static int human_select_spell_idx(HumanInput* hi, GuiSpellIdx sidx) { if (gui_spell_is_ice(sidx)) { human_input_apply_ui_intent( - hi, osrs_ui_intent_select_spell(ATTACK_ICE, (int)sidx)); + hi, osrs_ui_intent_select_spell(PVP_ATTACK_ICE, (int)sidx)); return 1; } if (gui_spell_is_blood(sidx)) { human_input_apply_ui_intent( - hi, osrs_ui_intent_select_spell(ATTACK_BLOOD, (int)sidx)); + hi, osrs_ui_intent_select_spell(PVP_ATTACK_BLOOD, (int)sidx)); return 1; } @@ -330,72 +330,105 @@ static void human_handle_combat_click(HumanInput* hi, GuiState* gs, Player* p, } } -static void human_to_pvp_actions(HumanInput* hi, int* actions, - Player* agent, Player* target) { - for (int h = 0; h < NUM_ACTION_HEADS; h++) actions[h] = 0; +static void human_pvp_translate_inventory_cell( + int* actions, + const Player* player, + int cell +) { + if (cell < 0 || cell >= OSRS_INVENTORY_SIZE) return; + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&player->inventory_cells[cell]); + int click_action = cell + 1; + if (metadata->click_action == OSRS_CLICK_EQUIP) { + if (metadata->gear_slot >= 0 && metadata->gear_slot < NUM_GEAR_SLOTS) + actions[OSRS_HEAD_EQUIP_SLOT(metadata->gear_slot)] = click_action; + } else if (metadata->click_action == OSRS_CLICK_EAT) { + actions[OSRS_HEAD_EAT] = click_action; + } else if (metadata->click_action == OSRS_CLICK_DRINK) { + actions[OSRS_HEAD_DRINK] = click_action; + } +} - actions[HEAD_LOADOUT] = LOADOUT_KEEP; +static int human_pvp_find_consumable_cell( + const Player* player, + OsrsConsumableKind kind +) { + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&player->inventory_cells[cell]); + if (metadata->consumable_kind == kind) return cell; + } + return -1; +} - if (hi->pending_attack) { - if (hi->pending_spell == ATTACK_ICE) { - actions[HEAD_COMBAT] = ATTACK_ICE; - } else if (hi->pending_spell == ATTACK_BLOOD) { - actions[HEAD_COMBAT] = ATTACK_BLOOD; - } else { - actions[HEAD_COMBAT] = ATTACK_ATK; - } - } else if (hi->pending_move_x >= 0 && hi->pending_move_y >= 0) { - int dx = hi->pending_move_x - target->x; - int dy = hi->pending_move_y - target->y; - int dist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy); - - if (dist == 0) { - actions[HEAD_COMBAT] = MOVE_UNDER; - } else if (dist == 1) { - if (dx == 0 || dy == 0) { - actions[HEAD_COMBAT] = MOVE_ADJACENT; - } else { - actions[HEAD_COMBAT] = MOVE_DIAGONAL; +static void human_to_pvp_actions( + HumanInput* hi, + int* actions, + Player* agent +) { + memset(actions, 0, OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); + if (hi->pending_move_x >= 0 && hi->pending_move_y >= 0) { + int dx = hi->pending_move_x - agent->x; + int dy = hi->pending_move_y - agent->y; + if (dx < -2) dx = -2; + if (dx > 2) dx = 2; + if (dy < -2) dy = -2; + if (dy > 2) dy = 2; + for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + if (ENCOUNTER_MOVE_TARGET_DX[action] == dx && + ENCOUNTER_MOVE_TARGET_DY[action] == dy) { + actions[OSRS_HEAD_PRIMARY] = action; + break; } - } else { - int fc = dist; - if (fc < 2) fc = 2; - if (fc > 7) fc = 7; - actions[HEAD_COMBAT] = MOVE_FARCAST_2 + (fc - 2); } } + encounter_translate_prayer(hi, actions, OSRS_HEAD_OVERHEAD); + encounter_translate_offensive_prayer(hi, actions, OSRS_HEAD_OFFENSIVE); - if (hi->pending_prayer >= 0) { - actions[HEAD_OVERHEAD] = hi->pending_prayer; + if (hi->pending_attack) { + actions[OSRS_HEAD_PRIMARY] = OSRS_PRIMARY_MOVE_ACTIONS; + if (hi->pending_spell == PVP_ATTACK_ICE) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_ICE_BARRAGE; + else if (hi->pending_spell == PVP_ATTACK_BLOOD) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_BLOOD_BARRAGE; } - if (hi->pending_food) { - actions[HEAD_FOOD] = FOOD_EAT; - } - - if (hi->pending_potion > 0) { - actions[HEAD_POTION] = hi->pending_potion; + int cell = human_pvp_find_consumable_cell( + agent, OSRS_CONSUMABLE_SHARK_FOOD); + if (cell >= 0) actions[OSRS_HEAD_EAT] = cell + 1; } - if (hi->pending_karambwan) { - actions[HEAD_KARAMBWAN] = KARAM_EAT; + int cell = human_pvp_find_consumable_cell( + agent, OSRS_CONSUMABLE_KARAMBWAN); + if (cell >= 0) actions[OSRS_HEAD_EAT] = cell + 1; } - - if (hi->pending_veng) { - actions[HEAD_VENG] = VENG_CAST; + OsrsConsumableKind potion_kind = OSRS_CONSUMABLE_NONE; + if (hi->pending_potion == POTION_BREW) + potion_kind = OSRS_CONSUMABLE_BREW; + else if (hi->pending_potion == POTION_RESTORE) + potion_kind = OSRS_CONSUMABLE_SUPER_RESTORE; + else if (hi->pending_potion == POTION_COMBAT) + potion_kind = OSRS_CONSUMABLE_SUPER_COMBAT; + else if (hi->pending_potion == POTION_RANGED) + potion_kind = OSRS_CONSUMABLE_RANGING; + if (potion_kind != OSRS_CONSUMABLE_NONE) { + int cell = human_pvp_find_consumable_cell(agent, potion_kind); + if (cell >= 0) actions[OSRS_HEAD_DRINK] = cell + 1; } - - if (hi->pending_spec) { - AttackStyle style = (AttackStyle)get_item_attack_style(agent->equipped[GEAR_SLOT_WEAPON]); - switch (style) { - case ATTACK_STYLE_MELEE: actions[HEAD_LOADOUT] = LOADOUT_SPEC_MELEE; break; - case ATTACK_STYLE_RANGED: actions[HEAD_LOADOUT] = LOADOUT_SPEC_RANGE; break; - case ATTACK_STYLE_MAGIC: actions[HEAD_LOADOUT] = LOADOUT_SPEC_MAGIC; break; - default: break; + for (int i = 0; i < hi->commands.count; i++) { + const HumanCommand* command = &hi->commands.items[i]; + if (command->kind == HUMAN_COMMAND_EQUIP_INVENTORY_ITEM || + command->kind == HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK || + command->kind == HUMAN_COMMAND_EAT || + command->kind == HUMAN_COMMAND_DRINK) { + human_pvp_translate_inventory_cell( + actions, agent, command->inventory_slot); } } - - (void)agent; + if (hi->pending_veng) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_VENGEANCE; + if (hi->pending_spec) + actions[OSRS_HEAD_SPECIAL] = agent->spec_armed ? 2 : 1; } #define CLICK_CROSS_NUM_FRAMES 4 diff --git a/ocean/osrs/osrs_inventory.h b/ocean/osrs/osrs_inventory.h index 758a106922..646fcc42f5 100644 --- a/ocean/osrs/osrs_inventory.h +++ b/ocean/osrs/osrs_inventory.h @@ -8,7 +8,6 @@ #include "osrs_types.h" #include "osrs_items.h" -#define OSRS_INVENTORY_SIZE 28 typedef enum { OSRS_CLICK_NONE = 0, @@ -56,17 +55,17 @@ typedef struct { #define OSRS_ITEM_CONTENT_METADATA_ROW( \ CODE, ITEM_POINTER, ITEM_IDX, RAW_OSRS_ID, GEAR_SLOT, CLICK_ACTION, \ CONSUMABLE_KIND, DOSE_COUNT, NEXT_CONTENT_CODE, ATTACK_STYLE, ...) \ - [CODE] = { \ - .item = ITEM_POINTER, \ - .raw_osrs_id = RAW_OSRS_ID, \ - .next_content_code = NEXT_CONTENT_CODE, \ - .item_idx = ITEM_IDX, \ - .gear_slot = GEAR_SLOT, \ - .click_action = CLICK_ACTION, \ - .consumable_kind = CONSUMABLE_KIND, \ - .dose_count = DOSE_COUNT, \ - .attack_style = ATTACK_STYLE, \ - .observation_row = {__VA_ARGS__}, \ + { \ + ITEM_POINTER, \ + RAW_OSRS_ID, \ + NEXT_CONTENT_CODE, \ + ITEM_IDX, \ + GEAR_SLOT, \ + CLICK_ACTION, \ + CONSUMABLE_KIND, \ + DOSE_COUNT, \ + ATTACK_STYLE, \ + {__VA_ARGS__}, \ }, static const OsrsItemContentMetadata OSRS_ITEM_CONTENT_METADATA[OSRS_ITEM_CONTENT_COUNT] = { @@ -74,38 +73,28 @@ static const OsrsItemContentMetadata }; #undef OSRS_ITEM_CONTENT_METADATA_ROW -#define OSRS_RAW_CONTENT_CODE_ROW( \ - CODE, ITEM_POINTER, ITEM_IDX, RAW_OSRS_ID, GEAR_SLOT, CLICK_ACTION, \ - CONSUMABLE_KIND, DOSE_COUNT, NEXT_CONTENT_CODE, ATTACK_STYLE, ...) \ - [RAW_OSRS_ID] = (uint16_t)((CODE) + 1), -static const uint16_t OSRS_CONTENT_CODE_BY_RAW_OSRS_ID[UINT16_MAX + 1] = { - OSRS_ITEM_CONTENT_ROWS(OSRS_RAW_CONTENT_CODE_ROW) -}; -#undef OSRS_RAW_CONTENT_CODE_ROW - -#define OSRS_CONSUMABLE_CONTENT_CODE_ROW(KIND, DOSE, CODE) \ - [KIND][DOSE] = (uint16_t)((CODE) + 1), -static const uint16_t - OSRS_CONTENT_CODE_BY_CONSUMABLE_KIND_AND_DOSE[OSRS_CONSUMABLE_COUNT][5] = { - OSRS_CONSUMABLE_CONTENT_ROWS(OSRS_CONSUMABLE_CONTENT_CODE_ROW) -}; -#undef OSRS_CONSUMABLE_CONTENT_CODE_ROW - -typedef struct { - uint16_t content_code; -} OsrsInventoryCell; typedef struct { OsrsInventoryCell cells[OSRS_INVENTORY_SIZE]; } OsrsInventorySlotSnapshot; -_Static_assert( +static inline OsrsInventoryCell* osrs_player_inventory_cells(Player* player) { + return player->inventory_cells; +} + +static inline const OsrsInventoryCell* osrs_player_inventory_cells_const( + const Player* player +) { + return player->inventory_cells; +} + +static_assert( sizeof(OSRS_ITEM_CONTENT_METADATA) / sizeof(OSRS_ITEM_CONTENT_METADATA[0]) == OSRS_ITEM_CONTENT_COUNT, "generated item metadata row count must match its content-code domain"); -_Static_assert(OSRS_ITEM_CONTENT_COUNT <= UINT16_MAX, +static_assert(OSRS_ITEM_CONTENT_COUNT <= UINT16_MAX, "item content code must fit uint16_t"); -_Static_assert(sizeof(OsrsInventoryCell) == sizeof(uint16_t), +static_assert(sizeof(OsrsInventoryCell) == sizeof(uint16_t), "inventory cells carry only canonical content identity"); static inline const OsrsItemContentMetadata* osrs_item_content_metadata( @@ -126,18 +115,25 @@ static inline uint16_t osrs_inventory_content_code_from_item(uint8_t item_idx) { return (uint16_t)(1 + item_idx); } +#define OSRS_RAW_CONTENT_CODE_CASE( \ + CODE, ITEM_POINTER, ITEM_IDX, RAW_OSRS_ID, GEAR_SLOT, CLICK_ACTION, \ + CONSUMABLE_KIND, DOSE_COUNT, NEXT_CONTENT_CODE, ATTACK_STYLE, ...) \ + case RAW_OSRS_ID: return CODE; static inline uint16_t osrs_inventory_content_code_from_raw_osrs_id( uint16_t raw_osrs_id ) { - uint16_t encoded = OSRS_CONTENT_CODE_BY_RAW_OSRS_ID[raw_osrs_id]; - if (encoded == 0) { - fprintf(stderr, "inventory content: unrepresentable raw OSRS id %u\n", - raw_osrs_id); - abort(); + switch (raw_osrs_id) { + OSRS_ITEM_CONTENT_ROWS(OSRS_RAW_CONTENT_CODE_CASE) + default: + fprintf(stderr, "inventory content: unrepresentable raw OSRS id %u\n", + raw_osrs_id); + abort(); } - return (uint16_t)(encoded - 1); } +#undef OSRS_RAW_CONTENT_CODE_CASE +#define OSRS_CONSUMABLE_CONTENT_CODE_CASE(KIND, DOSE, CODE) \ + case (KIND) * 5 + (DOSE): return CODE; static inline uint16_t osrs_inventory_content_code_from_consumable( OsrsConsumableKind kind, uint8_t dose_count @@ -148,15 +144,15 @@ static inline uint16_t osrs_inventory_content_code_from_consumable( (int)kind, dose_count); abort(); } - uint16_t encoded = - OSRS_CONTENT_CODE_BY_CONSUMABLE_KIND_AND_DOSE[kind][dose_count]; - if (encoded == 0) { - fprintf(stderr, "inventory content: unrepresentable consumable kind %d dose %u\n", - (int)kind, dose_count); - abort(); + switch ((int)kind * 5 + dose_count) { + OSRS_CONSUMABLE_CONTENT_ROWS(OSRS_CONSUMABLE_CONTENT_CODE_CASE) + default: + fprintf(stderr, "inventory content: unrepresentable consumable kind %d dose %u\n", + (int)kind, dose_count); + abort(); } - return (uint16_t)(encoded - 1); } +#undef OSRS_CONSUMABLE_CONTENT_CODE_CASE static inline int osrs_inventory_slot_valid(int slot) { return slot >= 0 && slot < OSRS_INVENTORY_SIZE; diff --git a/ocean/osrs/osrs_inventory_actions.h b/ocean/osrs/osrs_inventory_actions.h index 4b357cdb27..50db2a88bf 100644 --- a/ocean/osrs/osrs_inventory_actions.h +++ b/ocean/osrs/osrs_inventory_actions.h @@ -17,17 +17,23 @@ static inline int osrs_first_empty_inventory_cell( return -1; } -static inline int osrs_inventory_cell_holds_equipped_item( +static inline int osrs_can_equip_metadata( const Player* p, - const OsrsInventoryCell* cells, - int cell_idx + const OsrsItemContentMetadata* metadata, + int inventory_has_empty_cell ) { - const OsrsInventoryCell* cell = &cells[cell_idx]; - const OsrsItemContentMetadata* metadata = - osrs_inventory_cell_metadata(cell); - if (metadata->item_idx == ITEM_NONE) return 0; - if (metadata->gear_slot < 0) return 0; - return p->equipped[metadata->gear_slot] == metadata->item_idx; + uint8_t item_idx = metadata->item_idx; + int gear_slot = metadata->gear_slot; + if (item_idx == ITEM_NONE || gear_slot < 0) return 0; + if (p->equipped[gear_slot] == item_idx) return 0; + if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(item_idx) && + p->equipped[GEAR_SLOT_SHIELD] != ITEM_NONE && + p->equipped[GEAR_SLOT_WEAPON] != ITEM_NONE && + !inventory_has_empty_cell) return 0; + if (gear_slot == GEAR_SLOT_SHIELD && + item_is_two_handed(p->equipped[GEAR_SLOT_WEAPON]) && + !inventory_has_empty_cell) return 0; + return 1; } static inline int osrs_can_equip_from_cell( @@ -35,13 +41,13 @@ static inline int osrs_can_equip_from_cell( const OsrsInventoryCell* cells, int cell_idx ) { - const OsrsInventoryCell* cell = &cells[cell_idx]; - uint8_t item_idx = osrs_inventory_cell_item_index(cell); - if (item_idx == ITEM_NONE) return 0; - if (osrs_inventory_cell_holds_equipped_item(p, cells, cell_idx)) return 0; + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&cells[cell_idx]); + uint8_t item_idx = metadata->item_idx; + if (item_idx == ITEM_NONE || metadata->gear_slot < 0) return 0; - int gear_slot = osrs_inventory_cell_metadata(cell)->gear_slot; - if (gear_slot < 0) return 0; + int gear_slot = metadata->gear_slot; + if (p->equipped[gear_slot] == item_idx) return 0; if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(item_idx) && p->equipped[GEAR_SLOT_SHIELD] != ITEM_NONE && p->equipped[GEAR_SLOT_WEAPON] != ITEM_NONE && @@ -140,28 +146,22 @@ typedef struct { int drink; } OsrsInventoryClickActions; +static_assert(NUM_GEAR_SLOTS <= 32, "inventory intent gear mask too narrow"); + typedef struct { + uint32_t equip_slot_mask; int equip_cell_by_slot[NUM_GEAR_SLOTS]; - int equip_order_by_slot[NUM_GEAR_SLOTS]; int eat_cell; - int eat_order; OsrsInventoryClickResolution eat_resolution; int drink_cell; - int drink_order; OsrsInventoryClickResolution drink_resolution; } OsrsInventoryTickIntent; static inline OsrsInventoryTickIntent osrs_inventory_tick_intent_empty(void) { - OsrsInventoryTickIntent intent; - memset(&intent, 0, sizeof(intent)); - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - intent.equip_cell_by_slot[slot] = -1; - intent.equip_order_by_slot[slot] = -1; - } - intent.eat_cell = -1; - intent.eat_order = -1; - intent.drink_cell = -1; - intent.drink_order = -1; + OsrsInventoryTickIntent intent = { + .eat_cell = -1, + .drink_cell = -1, + }; return intent; } @@ -171,21 +171,36 @@ static inline OsrsInventoryTickIntent osrs_resolve_inventory_tick_intent( const OsrsInventoryClickActions* clicks ) { OsrsInventoryTickIntent intent = osrs_inventory_tick_intent_empty(); + int inventory_has_empty_cell = -1; for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { int action = clicks->equip_by_slot[slot]; if (action <= 0 || action > OSRS_INVENTORY_SIZE) continue; int cell_idx = action - 1; - if (!osrs_can_equip_from_cell(p, cells, cell_idx)) continue; - int gear_slot = osrs_inventory_cell_metadata( - &cells[cell_idx])->gear_slot; + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&cells[cell_idx]); + int needs_empty_cell = + (metadata->gear_slot == GEAR_SLOT_WEAPON && + item_is_two_handed(metadata->item_idx) && + p->equipped[GEAR_SLOT_SHIELD] != ITEM_NONE && + p->equipped[GEAR_SLOT_WEAPON] != ITEM_NONE) || + (metadata->gear_slot == GEAR_SLOT_SHIELD && + item_is_two_handed(p->equipped[GEAR_SLOT_WEAPON])); + if (needs_empty_cell && inventory_has_empty_cell < 0) { + inventory_has_empty_cell = + osrs_first_empty_inventory_cell(cells, -1) >= 0; + } + if (!osrs_can_equip_metadata( + p, metadata, + !needs_empty_cell || inventory_has_empty_cell)) continue; + int gear_slot = metadata->gear_slot; if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) { fprintf(stderr, "inventory equip intent: invalid gear slot %d\n", gear_slot); abort(); } if (gear_slot != slot) continue; intent.equip_cell_by_slot[slot] = cell_idx; - intent.equip_order_by_slot[slot] = slot; + intent.equip_slot_mask |= UINT32_C(1) << slot; } if (clicks->eat > 0 && clicks->eat <= OSRS_INVENTORY_SIZE) { @@ -195,7 +210,6 @@ static inline OsrsInventoryTickIntent osrs_resolve_inventory_tick_intent( if (resolution.click_action == OSRS_CLICK_EAT && osrs_can_eat_consumable_kind(p, resolution.consumable_kind)) { intent.eat_cell = cell_idx; - intent.eat_order = NUM_GEAR_SLOTS; intent.eat_resolution = resolution; } } @@ -208,7 +222,6 @@ static inline OsrsInventoryTickIntent osrs_resolve_inventory_tick_intent( osrs_inventory_cell_dose_count(&cells[cell_idx]) > 0 && p->potion_timer == 0) { intent.drink_cell = cell_idx; - intent.drink_order = NUM_GEAR_SLOTS + 1; intent.drink_resolution = resolution; } } @@ -219,24 +232,9 @@ static inline OsrsInventoryTickIntent osrs_resolve_inventory_tick_intent( static inline int osrs_inventory_tick_intent_has_effect( const OsrsInventoryTickIntent* intent ) { - if (intent->eat_cell >= 0 || intent->drink_cell >= 0) return 1; - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) - if (intent->equip_cell_by_slot[slot] >= 0) return 1; - return 0; -} - -static inline int osrs_next_inventory_apply_order( - const OsrsInventoryTickIntent* intent -) { - int sentinel = NUM_GEAR_SLOTS + 2; - int best = sentinel; - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - int order = intent->equip_order_by_slot[slot]; - if (order >= 0 && order < best) best = order; - } - if (intent->eat_order >= 0 && intent->eat_order < best) best = intent->eat_order; - if (intent->drink_order >= 0 && intent->drink_order < best) best = intent->drink_order; - return best == sentinel ? -1 : best; + return intent->equip_slot_mask != 0 || + intent->eat_cell >= 0 || + intent->drink_cell >= 0; } typedef enum { @@ -258,37 +256,32 @@ static inline int osrs_inventory_intent_next( OsrsInventoryTickIntent* intent, OsrsInventoryApplyStep* out ) { - int order = osrs_next_inventory_apply_order(intent); - if (order < 0) return 0; - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - if (intent->equip_order_by_slot[slot] != order) continue; + if (intent->equip_slot_mask != 0) { + int slot = __builtin_ctz(intent->equip_slot_mask); + intent->equip_slot_mask &= intent->equip_slot_mask - 1; out->kind = OSRS_INVENTORY_APPLY_EQUIP; out->gear_slot = slot; out->cell_idx = intent->equip_cell_by_slot[slot]; memset(&out->resolution, 0, sizeof(out->resolution)); - intent->equip_order_by_slot[slot] = -1; - intent->equip_cell_by_slot[slot] = -1; return 1; } - if (intent->eat_order == order) { + if (intent->eat_cell >= 0) { out->kind = OSRS_INVENTORY_APPLY_EAT; out->gear_slot = -1; out->cell_idx = intent->eat_cell; out->resolution = intent->eat_resolution; - intent->eat_order = -1; intent->eat_cell = -1; return 1; } - if (intent->drink_order == order) { + if (intent->drink_cell >= 0) { out->kind = OSRS_INVENTORY_APPLY_DRINK; out->gear_slot = -1; out->cell_idx = intent->drink_cell; out->resolution = intent->drink_resolution; - intent->drink_order = -1; intent->drink_cell = -1; return 1; } - abort(); + return 0; } #endif diff --git a/ocean/osrs/osrs_inventory_clicks.h b/ocean/osrs/osrs_inventory_clicks.h index cd6c14a09c..5e27d4e852 100644 --- a/ocean/osrs/osrs_inventory_clicks.h +++ b/ocean/osrs/osrs_inventory_clicks.h @@ -42,30 +42,20 @@ typedef void (*OsrsInventoryDrinkOneDoseEffectFn)( ); #define OSRS_INVENTORY_CELL_OBS_FEATURES 28 -#define OSRS_INVENTORY_CELL_OBS_SHARED 5 +#define OSRS_INVENTORY_CELL_OBS_SHARED 4 #define OSRS_INVENTORY_CELL_OBS_KIND_UNION 10 #define OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT \ (OSRS_INVENTORY_CELL_OBS_SHARED + OSRS_INVENTORY_CELL_OBS_KIND_UNION) -/* Slot names for the compact record. The coded observation ships three of these and an - item-table gather rebuilds the rest, so both sides must agree on where they sit. */ +/* Slot names for the compact record reconstructed by the item-table gather. */ #define OSRS_INVENTORY_CELL_COMPACT_PRESENT 0 -#define OSRS_INVENTORY_CELL_COMPACT_EQUIPPED 1 -#define OSRS_INVENTORY_CELL_COMPACT_DOSE 2 -#define OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR 3 -#define OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON 4 -/* Union slot: effect_class4[2] on a gear cell, hp_heal on a consumable cell. */ +#define OSRS_INVENTORY_CELL_COMPACT_DOSE 1 +#define OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR 2 +#define OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON 3 +/* Union slot: effect_class4[2] on gear, hp_heal on consumables. */ #define OSRS_INVENTORY_CELL_COMPACT_HP_HEAL (OSRS_INVENTORY_CELL_OBS_SHARED + 5) -/* The coded observation: everything a cell carries that the item alone does not determine. - hp_heal is here because it divides by base_hitpoints, which the Frailty modifier rewrites - mid-episode; dose and the other ten features are pure item facts and live in the table. */ -#define OSRS_INVENTORY_CELL_OBS_CODE 0 -#define OSRS_INVENTORY_CELL_OBS_EQUIPPED 1 -#define OSRS_INVENTORY_CELL_OBS_HP_HEAL 2 -#define OSRS_INVENTORY_CELL_OBS_FEATURES_CODED 3 -#define OSRS_EQUIPPED_SELF_OBS_FEATURES 18 - +#define OSRS_INVENTORY_CELL_OBS_CODE 0 static inline void osrs_inventory_clicks_trap(void) { #if defined(__clang__) || defined(__GNUC__) @@ -283,7 +273,6 @@ static inline void osrs_write_item_content_affordance_features_compact( base_hitpoints, base_prayer, base_level); out[OSRS_INVENTORY_CELL_COMPACT_PRESENT] = affordance.present; - out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] = affordance.is_equipped; out[OSRS_INVENTORY_CELL_COMPACT_DOSE] = affordance.dose; out[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] = affordance.is_armor; out[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] = affordance.is_weapon; @@ -325,45 +314,6 @@ static inline void osrs_write_inventory_cell_affordance_features_compact( base_hitpoints, base_prayer, base_level); } -static inline void osrs_write_equipped_self_features(float* out, uint8_t item_idx) { - for (int index = 0; index < OSRS_EQUIPPED_SELF_OBS_FEATURES; index++) { - out[index] = 0.0f; - } - if (item_idx == ITEM_NONE) return; - - const OsrsItemContentMetadata* metadata = osrs_item_content_metadata( - osrs_inventory_content_code_from_item(item_idx)); - const Item* item = metadata->item; - int style = metadata->attack_style; - out[0] = 1.0f; - out[1] = style == 1 ? 1.0f : 0.0f; - out[2] = style == 2 ? 1.0f : 0.0f; - out[3] = style == 3 ? 1.0f : 0.0f; - out[4] = osrs_clamp_unit((float)item->attack_slash / STAT_NORM_ATTACK); - out[5] = osrs_clamp_unit((float)item->melee_strength / STAT_NORM_STRENGTH); - out[6] = osrs_clamp_unit((float)item->attack_ranged / STAT_NORM_ATTACK); - out[7] = osrs_clamp_unit((float)item->ranged_strength / STAT_NORM_STRENGTH); - out[8] = osrs_clamp_unit(((float)item->attack_magic / STAT_NORM_ATTACK) + - ((float)item->magic_damage / STAT_NORM_MAGIC_DMG)); - out[9] = osrs_clamp_unit((float)(item->defence_stab + item->defence_slash + - item->defence_crush + item->defence_magic + item->defence_ranged) / - (5.0f * STAT_NORM_DEFENCE)); - out[10] = item->effect_mask != OSRS_ITEM_EFFECT_NONE ? 1.0f : 0.0f; - out[11] = item->slot == SLOT_WEAPON ? 1.0f : 0.0f; - - float effect_class[4]; - osrs_item_effect_class4(item->effect_mask, effect_class); - out[12] = effect_class[0]; - out[13] = effect_class[1]; - out[14] = effect_class[2]; - out[15] = effect_class[3]; - out[16] = item->slot == SLOT_WEAPON - ? osrs_clamp_unit((float)item->attack_speed / STAT_NORM_SPEED) - : 0.0f; - out[17] = item->slot == SLOT_WEAPON - ? osrs_clamp_unit((float)item->attack_range / STAT_NORM_RANGE) - : 0.0f; -} static inline OsrsClickAction osrs_item_click_action(uint8_t item_idx) { if (item_idx == ITEM_NONE) return OSRS_CLICK_NONE; @@ -547,46 +497,12 @@ static inline uint16_t osrs_inventory_cell_obs_code_decode(float observed) { return (uint16_t)content_code; } -static inline void osrs_write_inventory_cell_obs_code_features( +static inline void osrs_write_inventory_cell_obs_code( float* out, - const OsrsInventoryCell* cell, - int is_equipped, - int base_hitpoints + const OsrsInventoryCell* cell ) { - const OsrsItemContentMetadata* metadata = - osrs_inventory_cell_metadata(cell); out[OSRS_INVENTORY_CELL_OBS_CODE] = osrs_inventory_cell_obs_code_encode(cell->content_code); - out[OSRS_INVENTORY_CELL_OBS_EQUIPPED] = - is_equipped ? 1.0f : 0.0f; - if (metadata->item != NULL || base_hitpoints <= 0) { - out[OSRS_INVENTORY_CELL_OBS_HP_HEAL] = 0.0f; - return; - } - int hp_heal = osrs_consumable_hp_heal_amount( - (OsrsConsumableKind)metadata->consumable_kind, - base_hitpoints); - out[OSRS_INVENTORY_CELL_OBS_HP_HEAL] = - osrs_clamp_unit((float)hp_heal / (float)base_hitpoints); -} - -/* Rebuilds the compact record, overwrites equipped, and overwrites HP-heal only for non-gear. */ -static inline void osrs_expand_inventory_cell_obs_code_features( - float* out, - const float* coded, - const float* table_row -) { - for (int f = 0; f < OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT; f++) { - out[f] = table_row[f]; - } - int is_gear = table_row[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] != 0.0f || - table_row[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] != 0.0f; - out[OSRS_INVENTORY_CELL_COMPACT_EQUIPPED] = - coded[OSRS_INVENTORY_CELL_OBS_EQUIPPED]; - if (!is_gear) { - out[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL] = - coded[OSRS_INVENTORY_CELL_OBS_HP_HEAL]; - } } #endif diff --git a/ocean/osrs/osrs_item_obs_generated.h b/ocean/osrs/osrs_item_obs_generated.h index 0c33f31e47..34e22f6f3d 100644 --- a/ocean/osrs/osrs_item_obs_generated.h +++ b/ocean/osrs/osrs_item_obs_generated.h @@ -4,218 +4,215 @@ #define OSRS_ITEM_CONTENT_COUNT 201 #define OSRS_ITEM_OBS_TABLE_ROWS 201 -#define OSRS_ITEM_OBS_TABLE_COLS 15 +#define OSRS_ITEM_OBS_TABLE_COLS 14 #define OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS 99 #define OSRS_ITEM_OBS_TABLE_BASE_PRAYER 99 #define OSRS_ITEM_OBS_TABLE_BASE_RANGED 99 #define OSRS_ITEM_OBS_CODE_SCALE 256 -#define OSRS_ITEM_OBS_OVERLAY_EQUIPPED 1 -#define OSRS_ITEM_OBS_OVERLAY_HP_HEAL 10 - #define OSRS_ITEM_CONTENT_ROWS(X) \ - X(0, NULL, 255, 0, -1, 0, 0, 0, 0, 0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(1, &ITEM_DATABASE[0], 0, 10828, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(2, &ITEM_DATABASE[1], 1, 21795, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(3, &ITEM_DATABASE[2], 2, 1712, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(4, &ITEM_DATABASE[3], 3, 2503, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(5, &ITEM_DATABASE[4], 4, 4091, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(6, &ITEM_DATABASE[5], 5, 1079, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(7, &ITEM_DATABASE[6], 6, 4093, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(8, &ITEM_DATABASE[7], 7, 4151, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ - X(9, &ITEM_DATABASE[8], 8, 9185, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.0f) \ - X(10, &ITEM_DATABASE[9], 9, 4710, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.666666687f, 0.0f) \ - X(11, &ITEM_DATABASE[10], 10, 5698, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.25f) \ - X(12, &ITEM_DATABASE[11], 11, 12954, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(13, &ITEM_DATABASE[12], 12, 12829, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(14, &ITEM_DATABASE[13], 13, 7462, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(15, &ITEM_DATABASE[14], 14, 3105, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(16, &ITEM_DATABASE[15], 15, 6737, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(17, &ITEM_DATABASE[16], 16, 9243, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(18, &ITEM_DATABASE[17], 17, 22324, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ - X(19, &ITEM_DATABASE[18], 18, 24417, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ - X(20, &ITEM_DATABASE[19], 19, 11791, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.666666687f, 0.0f) \ - X(21, &ITEM_DATABASE[20], 20, 21006, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.666666687f, 0.0f) \ - X(22, &ITEM_DATABASE[21], 21, 24424, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.550000012f) \ - X(23, &ITEM_DATABASE[22], 22, 13867, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ - X(24, &ITEM_DATABASE[23], 23, 11785, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.5f) \ - X(25, &ITEM_DATABASE[24], 24, 26374, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.75f) \ - X(26, &ITEM_DATABASE[25], 25, 13652, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.5f) \ - X(27, &ITEM_DATABASE[26], 26, 11802, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ - X(28, &ITEM_DATABASE[27], 27, 26233, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ - X(29, &ITEM_DATABASE[28], 28, 4153, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.0666666701f, 0.5f) \ - X(30, &ITEM_DATABASE[29], 29, 21003, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ - X(31, &ITEM_DATABASE[30], 30, 11235, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.899999976f, 0.666666687f, 0.550000012f) \ - X(32, &ITEM_DATABASE[31], 31, 19481, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.666666687f, 0.649999976f) \ - X(33, &ITEM_DATABASE[32], 32, 22613, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.25f) \ - X(34, &ITEM_DATABASE[33], 33, 27690, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.5f) \ - X(35, &ITEM_DATABASE[34], 34, 22622, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.349999994f) \ - X(36, &ITEM_DATABASE[35], 35, 22636, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.333333343f, 0.5f) \ - X(37, &ITEM_DATABASE[36], 36, 21018, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(38, &ITEM_DATABASE[37], 37, 21021, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(39, &ITEM_DATABASE[38], 38, 21024, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(40, &ITEM_DATABASE[39], 39, 4712, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(41, &ITEM_DATABASE[40], 40, 4714, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(42, &ITEM_DATABASE[41], 41, 4736, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(43, &ITEM_DATABASE[42], 42, 11834, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(44, &ITEM_DATABASE[43], 43, 12831, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(45, &ITEM_DATABASE[44], 44, 6585, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(46, &ITEM_DATABASE[45], 45, 12002, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(47, &ITEM_DATABASE[46], 46, 21295, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(48, &ITEM_DATABASE[47], 47, 13235, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(49, &ITEM_DATABASE[48], 48, 11770, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(50, &ITEM_DATABASE[49], 49, 25975, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f) \ - X(51, &ITEM_DATABASE[50], 50, 6889, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(52, &ITEM_DATABASE[51], 51, 11212, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(53, &ITEM_DATABASE[52], 52, 4751, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(54, &ITEM_DATABASE[53], 53, 4722, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(55, &ITEM_DATABASE[54], 54, 4759, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(56, &ITEM_DATABASE[55], 55, 4745, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(57, &ITEM_DATABASE[56], 56, 4716, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(58, &ITEM_DATABASE[57], 57, 4753, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(59, &ITEM_DATABASE[58], 58, 4724, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(60, &ITEM_DATABASE[59], 59, 21932, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(61, &ITEM_DATABASE[60], 60, 21791, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(62, &ITEM_DATABASE[61], 61, 31113, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.400000006f, 0.5f) \ - X(63, &ITEM_DATABASE[62], 62, 27251, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(64, &ITEM_DATABASE[63], 63, 31106, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(65, &ITEM_DATABASE[64], 64, 31097, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(66, &ITEM_DATABASE[65], 65, 20657, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(67, &ITEM_DATABASE[66], 66, 20997, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.600000024f, 0.666666687f, 0.0f) \ - X(68, &ITEM_DATABASE[67], 67, 27235, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(69, &ITEM_DATABASE[68], 68, 27238, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(70, &ITEM_DATABASE[69], 69, 27241, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(71, &ITEM_DATABASE[70], 70, 19547, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(72, &ITEM_DATABASE[71], 71, 28947, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(73, &ITEM_DATABASE[72], 72, 26235, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(74, &ITEM_DATABASE[73], 73, 12926, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.333333343f, 0.5f) \ - X(75, &ITEM_DATABASE[74], 74, 4708, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(76, &ITEM_DATABASE[75], 75, 19544, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(77, &ITEM_DATABASE[76], 76, 22481, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.0f) \ - X(78, &ITEM_DATABASE[77], 77, 6920, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(79, &ITEM_DATABASE[78], 78, 20220, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(80, &ITEM_DATABASE[79], 79, 2550, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(81, &ITEM_DATABASE[80], 80, 23971, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(82, &ITEM_DATABASE[81], 81, 22109, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(83, &ITEM_DATABASE[82], 82, 23975, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(84, &ITEM_DATABASE[83], 83, 23979, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(85, &ITEM_DATABASE[84], 84, 25865, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ - X(86, &ITEM_DATABASE[85], 85, 19921, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(87, &ITEM_DATABASE[86], 86, 4089, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(88, &ITEM_DATABASE[87], 87, 12899, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.0f) \ - X(89, &ITEM_DATABASE[88], 88, 12612, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(90, &ITEM_DATABASE[89], 89, 21326, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(91, &ITEM_DATABASE[90], 90, 4097, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(92, &ITEM_DATABASE[91], 91, 10382, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(93, &ITEM_DATABASE[92], 92, 2497, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(94, &ITEM_DATABASE[93], 93, 12788, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.5f) \ - X(95, &ITEM_DATABASE[94], 94, 10499, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(96, &ITEM_DATABASE[95], 95, 4224, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(97, &ITEM_DATABASE[96], 96, 13237, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(98, &ITEM_DATABASE[97], 97, 22326, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(99, &ITEM_DATABASE[98], 98, 22327, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(100, &ITEM_DATABASE[99], 99, 22328, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(101, &ITEM_DATABASE[100], 100, 11230, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.200000003f, 0.0f) \ - X(102, &ITEM_DATABASE[101], 101, 22325, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f) \ - X(103, &ITEM_DATABASE[102], 102, 24551, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ - X(104, &ITEM_DATABASE[103], 103, 26219, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f) \ - X(105, &ITEM_DATABASE[104], 104, 28338, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f) \ - X(106, &ITEM_DATABASE[105], 105, 26382, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(107, &ITEM_DATABASE[106], 106, 26384, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(108, &ITEM_DATABASE[107], 107, 26386, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(109, &ITEM_DATABASE[108], 108, 11832, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(110, &ITEM_DATABASE[109], 109, 11836, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(111, &ITEM_DATABASE[110], 110, 13239, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(112, &ITEM_DATABASE[111], 111, 22981, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(113, &ITEM_DATABASE[112], 112, 19553, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(114, &ITEM_DATABASE[113], 113, 11773, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(115, &ITEM_DATABASE[114], 114, 28307, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(116, &ITEM_DATABASE[115], 115, 22322, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(117, &ITEM_DATABASE[116], 116, 28310, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(118, &ITEM_DATABASE[117], 117, 26241, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(119, &ITEM_DATABASE[118], 118, 26243, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(120, &ITEM_DATABASE[119], 119, 26245, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(121, &ITEM_DATABASE[120], 120, 28313, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(122, &ITEM_DATABASE[121], 121, 27275, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ - X(123, &ITEM_DATABASE[122], 122, 11804, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ - X(124, &ITEM_DATABASE[123], 123, 11806, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ - X(125, &ITEM_DATABASE[124], 124, 11808, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ - X(126, &ITEM_DATABASE[125], 125, 23987, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.13333334f, 0.0f) \ - X(127, &ITEM_DATABASE[126], 126, 1377, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.0f) \ - X(128, &ITEM_DATABASE[127], 127, 21944, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(129, &ITEM_DATABASE[128], 128, 21946, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(130, &ITEM_DATABASE[129], 129, 892, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(131, &ITEM_DATABASE[130], 130, 19484, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(132, &ITEM_DATABASE[131], 131, 12821, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(133, &ITEM_DATABASE[132], 132, 12817, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(134, &ITEM_DATABASE[133], 133, 11283, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(135, &ITEM_DATABASE[134], 134, 30070, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ - X(136, &ITEM_DATABASE[135], 135, 28945, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(137, &ITEM_DATABASE[136], 136, 12931, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(138, &ITEM_DATABASE[137], 137, 24780, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(139, &ITEM_DATABASE[138], 138, 10551, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(140, &ITEM_DATABASE[139], 139, 29801, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(141, &ITEM_DATABASE[140], 140, 30753, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(142, &ITEM_DATABASE[141], 141, 30756, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(143, &ITEM_DATABASE[142], 142, 27610, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.400000006f, 0.0f) \ - X(144, &ITEM_DATABASE[143], 143, 12006, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ - X(145, NULL, 255, 6685, -1, 3, 1, 4, 146, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(146, NULL, 255, 6687, -1, 3, 1, 3, 147, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(147, NULL, 255, 6689, -1, 3, 1, 2, 148, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(148, NULL, 255, 6691, -1, 3, 1, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(149, NULL, 255, 3024, -1, 3, 2, 4, 150, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ - X(150, NULL, 255, 3026, -1, 3, 2, 3, 151, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ - X(151, NULL, 255, 3028, -1, 3, 2, 2, 152, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ - X(152, NULL, 255, 3030, -1, 3, 2, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ - X(153, NULL, 255, 10925, -1, 3, 3, 4, 154, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ - X(154, NULL, 255, 10927, -1, 3, 3, 3, 155, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ - X(155, NULL, 255, 10929, -1, 3, 3, 2, 156, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ - X(156, NULL, 255, 10931, -1, 3, 3, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ - X(157, NULL, 255, 12695, -1, 3, 4, 4, 158, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ - X(158, NULL, 255, 12697, -1, 3, 4, 3, 159, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ - X(159, NULL, 255, 12699, -1, 3, 4, 2, 160, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ - X(160, NULL, 255, 12701, -1, 3, 4, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ - X(161, NULL, 255, 23685, -1, 3, 5, 4, 162, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ - X(162, NULL, 255, 23688, -1, 3, 5, 3, 163, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ - X(163, NULL, 255, 23691, -1, 3, 5, 2, 164, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ - X(164, NULL, 255, 23694, -1, 3, 5, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ - X(165, NULL, 255, 2444, -1, 3, 6, 4, 166, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(166, NULL, 255, 169, -1, 3, 6, 3, 167, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(167, NULL, 255, 171, -1, 3, 6, 2, 168, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(168, NULL, 255, 173, -1, 3, 6, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(169, NULL, 255, 23733, -1, 3, 7, 4, 170, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(170, NULL, 255, 23736, -1, 3, 7, 3, 171, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(171, NULL, 255, 23739, -1, 3, 7, 2, 172, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(172, NULL, 255, 23742, -1, 3, 7, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(173, NULL, 255, 30875, -1, 3, 8, 4, 174, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(174, NULL, 255, 30878, -1, 3, 8, 3, 175, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(175, NULL, 255, 30881, -1, 3, 8, 2, 176, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(176, NULL, 255, 30884, -1, 3, 8, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(177, NULL, 255, 4417, -1, 3, 9, 4, 178, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(178, NULL, 255, 4419, -1, 3, 9, 3, 179, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(179, NULL, 255, 4421, -1, 3, 9, 2, 180, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(180, NULL, 255, 4423, -1, 3, 9, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(181, NULL, 255, 27641, -1, 3, 10, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(182, NULL, 255, 12913, -1, 3, 11, 4, 183, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(183, NULL, 255, 12915, -1, 3, 11, 3, 184, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(184, NULL, 255, 12917, -1, 3, 11, 2, 185, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(185, NULL, 255, 12919, -1, 3, 11, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(186, NULL, 255, 2434, -1, 3, 14, 4, 187, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ - X(187, NULL, 255, 139, -1, 3, 14, 3, 188, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ - X(188, NULL, 255, 141, -1, 3, 14, 2, 189, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ - X(189, NULL, 255, 143, -1, 3, 14, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ - X(190, NULL, 255, 22461, -1, 3, 15, 4, 191, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(191, NULL, 255, 22464, -1, 3, 15, 3, 192, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(192, NULL, 255, 22467, -1, 3, 15, 2, 193, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(193, NULL, 255, 22470, -1, 3, 15, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ - X(194, NULL, 255, 12625, -1, 3, 16, 4, 195, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(195, NULL, 255, 12627, -1, 3, 16, 3, 196, 0, 1.0f, 0.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(196, NULL, 255, 12629, -1, 3, 16, 2, 197, 0, 1.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(197, NULL, 255, 12631, -1, 3, 16, 1, 0, 0, 1.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(198, NULL, 255, 385, -1, 2, 12, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.202020198f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(199, NULL, 255, 3144, -1, 2, 13, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.181818187f, 0.0f, 0.0f, 0.0f, 0.0f) \ - X(200, NULL, 255, 27281, -1, 0, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) + X(0, NULL, 255, 0, -1, 0, 0, 0, 0, 0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(1, &ITEM_DATABASE[0], 0, 10828, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(2, &ITEM_DATABASE[1], 1, 21795, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(3, &ITEM_DATABASE[2], 2, 1712, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(4, &ITEM_DATABASE[3], 3, 2503, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(5, &ITEM_DATABASE[4], 4, 4091, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(6, &ITEM_DATABASE[5], 5, 1079, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(7, &ITEM_DATABASE[6], 6, 4093, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(8, &ITEM_DATABASE[7], 7, 4151, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ + X(9, &ITEM_DATABASE[8], 8, 9185, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.0f) \ + X(10, &ITEM_DATABASE[9], 9, 4710, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.666666687f, 0.0f) \ + X(11, &ITEM_DATABASE[10], 10, 5698, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.25f) \ + X(12, &ITEM_DATABASE[11], 11, 12954, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(13, &ITEM_DATABASE[12], 12, 12829, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(14, &ITEM_DATABASE[13], 13, 7462, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(15, &ITEM_DATABASE[14], 14, 3105, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(16, &ITEM_DATABASE[15], 15, 6737, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(17, &ITEM_DATABASE[16], 16, 9243, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(18, &ITEM_DATABASE[17], 17, 22324, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ + X(19, &ITEM_DATABASE[18], 18, 24417, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ + X(20, &ITEM_DATABASE[19], 19, 11791, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.666666687f, 0.0f) \ + X(21, &ITEM_DATABASE[20], 20, 21006, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.666666687f, 0.0f) \ + X(22, &ITEM_DATABASE[21], 21, 24424, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.550000012f) \ + X(23, &ITEM_DATABASE[22], 22, 13867, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ + X(24, &ITEM_DATABASE[23], 23, 11785, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.5f) \ + X(25, &ITEM_DATABASE[24], 24, 26374, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.466666669f, 0.75f) \ + X(26, &ITEM_DATABASE[25], 25, 13652, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.5f) \ + X(27, &ITEM_DATABASE[26], 26, 11802, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(28, &ITEM_DATABASE[27], 27, 26233, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(29, &ITEM_DATABASE[28], 28, 4153, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.0666666701f, 0.5f) \ + X(30, &ITEM_DATABASE[29], 29, 21003, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(31, &ITEM_DATABASE[30], 30, 11235, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.899999976f, 0.666666687f, 0.550000012f) \ + X(32, &ITEM_DATABASE[31], 31, 19481, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.666666687f, 0.649999976f) \ + X(33, &ITEM_DATABASE[32], 32, 22613, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.25f) \ + X(34, &ITEM_DATABASE[33], 33, 27690, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.5f) \ + X(35, &ITEM_DATABASE[34], 34, 22622, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.349999994f) \ + X(36, &ITEM_DATABASE[35], 35, 22636, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.333333343f, 0.5f) \ + X(37, &ITEM_DATABASE[36], 36, 21018, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(38, &ITEM_DATABASE[37], 37, 21021, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(39, &ITEM_DATABASE[38], 38, 21024, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(40, &ITEM_DATABASE[39], 39, 4712, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(41, &ITEM_DATABASE[40], 40, 4714, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(42, &ITEM_DATABASE[41], 41, 4736, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(43, &ITEM_DATABASE[42], 42, 11834, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(44, &ITEM_DATABASE[43], 43, 12831, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(45, &ITEM_DATABASE[44], 44, 6585, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(46, &ITEM_DATABASE[45], 45, 12002, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(47, &ITEM_DATABASE[46], 46, 21295, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(48, &ITEM_DATABASE[47], 47, 13235, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(49, &ITEM_DATABASE[48], 48, 11770, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(50, &ITEM_DATABASE[49], 49, 25975, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f) \ + X(51, &ITEM_DATABASE[50], 50, 6889, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(52, &ITEM_DATABASE[51], 51, 11212, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(53, &ITEM_DATABASE[52], 52, 4751, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(54, &ITEM_DATABASE[53], 53, 4722, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(55, &ITEM_DATABASE[54], 54, 4759, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(56, &ITEM_DATABASE[55], 55, 4745, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(57, &ITEM_DATABASE[56], 56, 4716, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(58, &ITEM_DATABASE[57], 57, 4753, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(59, &ITEM_DATABASE[58], 58, 4724, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(60, &ITEM_DATABASE[59], 59, 21932, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(61, &ITEM_DATABASE[60], 60, 21791, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(62, &ITEM_DATABASE[61], 61, 31113, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.400000006f, 0.5f) \ + X(63, &ITEM_DATABASE[62], 62, 27251, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(64, &ITEM_DATABASE[63], 63, 31106, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(65, &ITEM_DATABASE[64], 64, 31097, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(66, &ITEM_DATABASE[65], 65, 20657, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(67, &ITEM_DATABASE[66], 66, 20997, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.600000024f, 0.666666687f, 0.0f) \ + X(68, &ITEM_DATABASE[67], 67, 27235, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(69, &ITEM_DATABASE[68], 68, 27238, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(70, &ITEM_DATABASE[69], 69, 27241, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(71, &ITEM_DATABASE[70], 70, 19547, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(72, &ITEM_DATABASE[71], 71, 28947, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(73, &ITEM_DATABASE[72], 72, 26235, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(74, &ITEM_DATABASE[73], 73, 12926, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.333333343f, 0.5f) \ + X(75, &ITEM_DATABASE[74], 74, 4708, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(76, &ITEM_DATABASE[75], 75, 19544, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(77, &ITEM_DATABASE[76], 76, 22481, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.0f) \ + X(78, &ITEM_DATABASE[77], 77, 6920, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(79, &ITEM_DATABASE[78], 78, 20220, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(80, &ITEM_DATABASE[79], 79, 2550, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(81, &ITEM_DATABASE[80], 80, 23971, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(82, &ITEM_DATABASE[81], 81, 22109, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(83, &ITEM_DATABASE[82], 82, 23975, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(84, &ITEM_DATABASE[83], 83, 23979, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(85, &ITEM_DATABASE[84], 84, 25865, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ + X(86, &ITEM_DATABASE[85], 85, 19921, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(87, &ITEM_DATABASE[86], 86, 4089, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(88, &ITEM_DATABASE[87], 87, 12899, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.0f) \ + X(89, &ITEM_DATABASE[88], 88, 12612, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(90, &ITEM_DATABASE[89], 89, 21326, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(91, &ITEM_DATABASE[90], 90, 4097, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(92, &ITEM_DATABASE[91], 91, 10382, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(93, &ITEM_DATABASE[92], 92, 2497, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(94, &ITEM_DATABASE[93], 93, 12788, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.466666669f, 0.5f) \ + X(95, &ITEM_DATABASE[94], 94, 10499, 1, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(96, &ITEM_DATABASE[95], 95, 4224, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(97, &ITEM_DATABASE[96], 96, 13237, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(98, &ITEM_DATABASE[97], 97, 22326, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(99, &ITEM_DATABASE[98], 98, 22327, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(100, &ITEM_DATABASE[99], 99, 22328, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(101, &ITEM_DATABASE[100], 100, 11230, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.300000012f, 0.200000003f, 0.0f) \ + X(102, &ITEM_DATABASE[101], 101, 22325, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f) \ + X(103, &ITEM_DATABASE[102], 102, 24551, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ + X(104, &ITEM_DATABASE[103], 103, 26219, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f) \ + X(105, &ITEM_DATABASE[104], 104, 28338, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0666666701f, 0.0f) \ + X(106, &ITEM_DATABASE[105], 105, 26382, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(107, &ITEM_DATABASE[106], 106, 26384, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(108, &ITEM_DATABASE[107], 107, 26386, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(109, &ITEM_DATABASE[108], 108, 11832, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(110, &ITEM_DATABASE[109], 109, 11836, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(111, &ITEM_DATABASE[110], 110, 13239, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(112, &ITEM_DATABASE[111], 111, 22981, 8, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(113, &ITEM_DATABASE[112], 112, 19553, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(114, &ITEM_DATABASE[113], 113, 11773, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(115, &ITEM_DATABASE[114], 114, 28307, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(116, &ITEM_DATABASE[115], 115, 22322, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(117, &ITEM_DATABASE[116], 116, 28310, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(118, &ITEM_DATABASE[117], 117, 26241, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(119, &ITEM_DATABASE[118], 118, 26243, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(120, &ITEM_DATABASE[119], 119, 26245, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(121, &ITEM_DATABASE[120], 120, 28313, 10, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(122, &ITEM_DATABASE[121], 121, 27275, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ + X(123, &ITEM_DATABASE[122], 122, 11804, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(124, &ITEM_DATABASE[123], 123, 11806, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(125, &ITEM_DATABASE[124], 124, 11808, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.5f) \ + X(126, &ITEM_DATABASE[125], 125, 23987, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.699999988f, 0.13333334f, 0.0f) \ + X(127, &ITEM_DATABASE[126], 126, 1377, 4, 1, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.600000024f, 0.0666666701f, 0.0f) \ + X(128, &ITEM_DATABASE[127], 127, 21944, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(129, &ITEM_DATABASE[128], 128, 21946, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(130, &ITEM_DATABASE[129], 129, 892, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(131, &ITEM_DATABASE[130], 130, 19484, 3, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(132, &ITEM_DATABASE[131], 131, 12821, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(133, &ITEM_DATABASE[132], 132, 12817, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(134, &ITEM_DATABASE[133], 133, 11283, 5, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(135, &ITEM_DATABASE[134], 134, 30070, 4, 1, 0, 0, 0, 3, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.666666687f, 0.0f) \ + X(136, &ITEM_DATABASE[135], 135, 28945, 9, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(137, &ITEM_DATABASE[136], 136, 12931, 0, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(138, &ITEM_DATABASE[137], 137, 24780, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(139, &ITEM_DATABASE[138], 138, 10551, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(140, &ITEM_DATABASE[139], 139, 29801, 2, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(141, &ITEM_DATABASE[140], 140, 30753, 6, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(142, &ITEM_DATABASE[141], 141, 30756, 7, 1, 0, 0, 0, 0, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(143, &ITEM_DATABASE[142], 142, 27610, 4, 1, 0, 0, 0, 2, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.400000006f, 0.0f) \ + X(144, &ITEM_DATABASE[143], 143, 12006, 4, 1, 0, 0, 0, 1, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.400000006f, 0.0666666701f, 0.0f) \ + X(145, NULL, 255, 6685, -1, 3, 1, 4, 146, 0, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(146, NULL, 255, 6687, -1, 3, 1, 3, 147, 0, 1.0f, 0.75f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(147, NULL, 255, 6689, -1, 3, 1, 2, 148, 0, 1.0f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(148, NULL, 255, 6691, -1, 3, 1, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.161616161f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(149, NULL, 255, 3024, -1, 3, 2, 4, 150, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ + X(150, NULL, 255, 3026, -1, 3, 2, 3, 151, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ + X(151, NULL, 255, 3028, -1, 3, 2, 2, 152, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ + X(152, NULL, 255, 3030, -1, 3, 2, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.323232323f, 0.0f, 0.0f, 0.0f) \ + X(153, NULL, 255, 10925, -1, 3, 3, 4, 154, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ + X(154, NULL, 255, 10927, -1, 3, 3, 3, 155, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ + X(155, NULL, 255, 10929, -1, 3, 3, 2, 156, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ + X(156, NULL, 255, 10931, -1, 3, 3, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.333333343f, 0.0f, 0.0f, 0.0f) \ + X(157, NULL, 255, 12695, -1, 3, 4, 4, 158, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(158, NULL, 255, 12697, -1, 3, 4, 3, 159, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(159, NULL, 255, 12699, -1, 3, 4, 2, 160, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(160, NULL, 255, 12701, -1, 3, 4, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(161, NULL, 255, 23685, -1, 3, 5, 4, 162, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(162, NULL, 255, 23688, -1, 3, 5, 3, 163, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(163, NULL, 255, 23691, -1, 3, 5, 2, 164, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(164, NULL, 255, 23694, -1, 3, 5, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.126666665f, 0.0f, 0.0f) \ + X(165, NULL, 255, 2444, -1, 3, 6, 4, 166, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(166, NULL, 255, 169, -1, 3, 6, 3, 167, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(167, NULL, 255, 171, -1, 3, 6, 2, 168, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(168, NULL, 255, 173, -1, 3, 6, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(169, NULL, 255, 23733, -1, 3, 7, 4, 170, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(170, NULL, 255, 23736, -1, 3, 7, 3, 171, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(171, NULL, 255, 23739, -1, 3, 7, 2, 172, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(172, NULL, 255, 23742, -1, 3, 7, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(173, NULL, 255, 30875, -1, 3, 8, 4, 174, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(174, NULL, 255, 30878, -1, 3, 8, 3, 175, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(175, NULL, 255, 30881, -1, 3, 8, 2, 176, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(176, NULL, 255, 30884, -1, 3, 8, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(177, NULL, 255, 4417, -1, 3, 9, 4, 178, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(178, NULL, 255, 4419, -1, 3, 9, 3, 179, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(179, NULL, 255, 4421, -1, 3, 9, 2, 180, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(180, NULL, 255, 4423, -1, 3, 9, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(181, NULL, 255, 27641, -1, 3, 10, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(182, NULL, 255, 12913, -1, 3, 11, 4, 183, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(183, NULL, 255, 12915, -1, 3, 11, 3, 184, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(184, NULL, 255, 12917, -1, 3, 11, 2, 185, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(185, NULL, 255, 12919, -1, 3, 11, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(186, NULL, 255, 2434, -1, 3, 14, 4, 187, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ + X(187, NULL, 255, 139, -1, 3, 14, 3, 188, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ + X(188, NULL, 255, 141, -1, 3, 14, 2, 189, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ + X(189, NULL, 255, 143, -1, 3, 14, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.313131303f, 0.0f, 0.0f, 0.0f) \ + X(190, NULL, 255, 22461, -1, 3, 15, 4, 191, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(191, NULL, 255, 22464, -1, 3, 15, 3, 192, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(192, NULL, 255, 22467, -1, 3, 15, 2, 193, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(193, NULL, 255, 22470, -1, 3, 15, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.086666666f, 0.0f, 0.0f) \ + X(194, NULL, 255, 12625, -1, 3, 16, 4, 195, 0, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(195, NULL, 255, 12627, -1, 3, 16, 3, 196, 0, 1.0f, 0.75f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(196, NULL, 255, 12629, -1, 3, 16, 2, 197, 0, 1.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(197, NULL, 255, 12631, -1, 3, 16, 1, 0, 0, 1.0f, 0.25f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(198, NULL, 255, 385, -1, 2, 12, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.202020198f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(199, NULL, 255, 3144, -1, 2, 13, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.181818187f, 0.0f, 0.0f, 0.0f, 0.0f) \ + X(200, NULL, 255, 27281, -1, 0, 0, 0, 0, 0, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) #define OSRS_CONSUMABLE_CONTENT_ROWS(X) \ X(1, 4, 145) \ diff --git a/ocean/osrs/osrs_items.h b/ocean/osrs/osrs_items.h index 6614502d33..df0cc85914 100644 --- a/ocean/osrs/osrs_items.h +++ b/ocean/osrs/osrs_items.h @@ -1,6 +1,7 @@ #ifndef OSRS_ITEMS_H #define OSRS_ITEMS_H +#include #include #include diff --git a/ocean/osrs/osrs_items_generated.h b/ocean/osrs/osrs_items_generated.h index 22dcbb1367..45a6d91b05 100644 --- a/ocean/osrs/osrs_items_generated.h +++ b/ocean/osrs/osrs_items_generated.h @@ -1466,10 +1466,10 @@ static const Item ITEM_DATABASE[NUM_ITEMS] = { .melee_strength = 86, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE }, }; -_Static_assert( +static_assert( sizeof(ITEM_DATABASE) / sizeof(ITEM_DATABASE[0]) == NUM_ITEMS, "item database row count must match ItemIndex"); -_Static_assert(NUM_ITEMS < ITEM_NONE, +static_assert(NUM_ITEMS < ITEM_NONE, "generated item indexes must remain distinct from ITEM_NONE"); diff --git a/ocean/osrs/osrs_pathfinding.h b/ocean/osrs/osrs_pathfinding.h index a2cedb8110..fcd9075a41 100644 --- a/ocean/osrs/osrs_pathfinding.h +++ b/ocean/osrs/osrs_pathfinding.h @@ -362,6 +362,11 @@ typedef enum { ENCOUNTER_ROUTE_MOVEMENT_WALK = 0, ENCOUNTER_ROUTE_MOVEMENT_RUN, } EncounterRouteMovementMode; +typedef enum { + ENCOUNTER_ROUTE_RESULT_FULL = 0, + ENCOUNTER_ROUTE_RESULT_NEXT_STEPS, +} EncounterRouteResultDetail; + typedef enum { ENCOUNTER_ROUTE_ATTACK_GEOMETRY_QUERY = 0, @@ -413,6 +418,7 @@ typedef struct { const OsrsLosQuery* los_query; EncounterRouteMovementMode movement_mode; EncounterRouteCostPolicy cost_policy; + EncounterRouteResultDetail result_detail; } EncounterRouteInput; typedef struct { @@ -436,19 +442,9 @@ typedef struct { uint64_t topology_revision; uint64_t blocker_revision; int source_x; - int target_x; - int target_y; - int target_size; - int attack_range; - const LOSBlocker* los_blockers; - void* los_tile_ctx; - int los_blocker_count; - uint8_t target_kind; - uint8_t los_kind; int source_y; uint16_t visited_count; uint16_t expanded_count; - int (*los_tile_blocked)(void* ctx, int x, int y); uint16_t depth[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint16_t queue[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; int8_t via[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; @@ -456,14 +452,33 @@ typedef struct { uint64_t blocker_known[ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION]; uint64_t blocker_value[ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION]; uint8_t actor_size; - uint8_t movement_mode; - uint8_t cost_policy; uint8_t valid; } EncounterSourceRouteField; +#define ENCOUNTER_SOURCE_ROUTE_CACHE_SETS 32 +#define ENCOUNTER_SOURCE_ROUTE_CACHE_WAYS 8 +#define ENCOUNTER_SOURCE_ROUTE_CACHE_SLOTS \ + (ENCOUNTER_SOURCE_ROUTE_CACHE_SETS * ENCOUNTER_SOURCE_ROUTE_CACHE_WAYS) + + +#define ENCOUNTER_REVERSE_ROUTE_CACHE_SLOTS 4 typedef struct { - uint16_t depth[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + const EncounterArenaTopology* topology; + void* blocker_ctx; + encounter_route_blocked_fn blocker; + uint64_t topology_revision; + uint64_t blocker_revision; + int target_x; + int target_y; + uint32_t depth_generation[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint16_t queue[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; + uint16_t head; + uint16_t tail; + uint16_t generation; + uint8_t actor_size; + uint8_t target_size; uint8_t outcome; + uint8_t valid; } EncounterReverseRouteField; typedef struct { @@ -475,8 +490,12 @@ typedef struct { uint16_t blocker_generation[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint8_t blocker_value[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; uint16_t current_generation; - EncounterSourceRouteField source_field; - EncounterReverseRouteField reverse_field; + EncounterSourceRouteField + source_fields[ENCOUNTER_SOURCE_ROUTE_CACHE_SLOTS]; + EncounterReverseRouteField + reverse_fields[ENCOUNTER_REVERSE_ROUTE_CACHE_SLOTS]; + uint8_t next_reverse_field; + uint8_t next_source_field[ENCOUNTER_SOURCE_ROUTE_CACHE_SETS]; } EncounterRouteScratch; static OSRS_THREAD_LOCAL EncounterRouteScratch encounter_route_scratch; @@ -543,7 +562,7 @@ static inline int encounter_route_step_allowed( int dx, int dy ) { - if (!encounter_arena_topology_step_allowed( + if (!encounter_arena_topology_step_allowed_assume_finalized_size_in_range( input->topology, x, y, input->actor_size, dx, dy)) return 0; if (encounter_route_dynamic_blocked(input, x + dx, y + dy)) @@ -554,17 +573,15 @@ static inline int encounter_route_step_allowed( return 0; return 1; } -static inline int encounter_route_dynamic_blocked_cached( +static inline int encounter_route_dynamic_blocked_cached_at_index( const EncounterRouteInput* input, EncounterRouteScratch* scratch, uint16_t generation, int x, - int y + int y, + int index ) { if (!input->blockers.is_blocked) return 0; - if (!encounter_arena_topology_contains(input->topology, x, y)) - return encounter_route_dynamic_blocked(input, x, y); - int index = encounter_arena_topology_index_raw(input->topology, x, y); if (scratch->blocker_generation[index] != generation) { scratch->blocker_generation[index] = generation; scratch->blocker_value[index] = @@ -573,6 +590,21 @@ static inline int encounter_route_dynamic_blocked_cached( return scratch->blocker_value[index]; } +static inline int encounter_route_dynamic_blocked_cached( + const EncounterRouteInput* input, + EncounterRouteScratch* scratch, + uint16_t generation, + int x, + int y +) { + if (!input->blockers.is_blocked) return 0; + if (!encounter_arena_topology_contains(input->topology, x, y)) + return encounter_route_dynamic_blocked(input, x, y); + int index = encounter_arena_topology_index_raw(input->topology, x, y); + return encounter_route_dynamic_blocked_cached_at_index( + input, scratch, generation, x, y, index); +} + static inline int encounter_route_step_allowed_cached( const EncounterRouteInput* input, EncounterRouteScratch* scratch, @@ -591,7 +623,7 @@ static inline int encounter_route_step_allowed_cached( encounter_route_dynamic_blocked_cached( input, scratch, generation, x + dx, y))) return 0; - if (!encounter_arena_topology_step_allowed( + if (!encounter_arena_topology_step_allowed_assume_finalized_size_in_range( input->topology, x, y, input->actor_size, dx, dy)) return 0; return 1; @@ -1294,13 +1326,35 @@ static inline EncounterRouteResult encounter_route_escape_overlap( return encounter_route_solve(&landing_input); } +static inline int encounter_route_reverse_field_contains( + const EncounterReverseRouteField* field, + int index +) { + return (uint16_t)(field->depth_generation[index] >> 16) == + field->generation; +} + +static inline uint16_t encounter_route_reverse_field_depth( + const EncounterReverseRouteField* field, + int index +) { + return (uint16_t)field->depth_generation[index]; +} + +static inline void encounter_route_reverse_field_set_depth( + EncounterReverseRouteField* field, + int index, + uint16_t depth +) { + field->depth_generation[index] = + ((uint32_t)field->generation << 16) | depth; +} + static inline int encounter_route_reverse_seed( const EncounterRouteInput* input, - EncounterRouteScratch* scratch, EncounterReverseRouteField* field, int x, - int y, - int* tail + int y ) { if (!encounter_route_destination_allowed(input, x, y)) return 0; const EncounterArenaTopology* topology = input->topology; @@ -1310,19 +1364,17 @@ static inline int encounter_route_reverse_seed( local_y < 0 || local_y >= topology->height) return 0; int index = local_x * topology->height + local_y; - if (field->depth[index] != UINT16_MAX) return 0; - field->depth[index] = 0; - scratch->queue[(*tail)++] = + if (encounter_route_reverse_field_contains(field, index)) return 0; + encounter_route_reverse_field_set_depth(field, index, 0); + field->queue[field->tail++] = (uint16_t)((local_x << 6) | local_y); return 1; } static inline void encounter_route_reverse_seed_cardinal_edges( const EncounterRouteInput* input, - EncounterRouteScratch* scratch, EncounterReverseRouteField* field, - int require_target, - int* tail + int require_target ) { int target_max_x = input->target_x + input->target_size - 1; int target_max_y = input->target_y + input->target_size - 1; @@ -1343,8 +1395,7 @@ static inline void encounter_route_reverse_seed_cardinal_edges( (!encounter_route_destination_allowed(input, x, y) || !encounter_route_is_target(input, x, y))) continue; - encounter_route_reverse_seed( - input, scratch, field, x, y, tail); + encounter_route_reverse_seed(input, field, x, y); } int y = y_edges[edge]; for (int x_scan = input->target_x - input->actor_size + 1; @@ -1354,15 +1405,14 @@ static inline void encounter_route_reverse_seed_cardinal_edges( (!encounter_route_destination_allowed(input, x_scan, y) || !encounter_route_is_target(input, x_scan, y))) continue; - encounter_route_reverse_seed( - input, scratch, field, x_scan, y, tail); + encounter_route_reverse_seed(input, field, x_scan, y); } } } static inline void encounter_route_reverse_build_result( const EncounterRouteInput* input, - const EncounterReverseRouteField* field, + EncounterReverseRouteField* field, EncounterRouteResult* result ) { static const int8_t osrs_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; @@ -1380,16 +1430,24 @@ static inline void encounter_route_reverse_build_result( (current_x - topology->origin_x) * topology->height + current_y - topology->origin_y; result->outcome = (EncounterRouteOutcome)field->outcome; - result->distance = field->depth[source_index]; + result->distance = + encounter_route_reverse_field_depth(field, source_index); int segment_dx = 0; int segment_dy = 0; int previous_x = current_x; int previous_y = current_y; - for (uint16_t step = 0; step < result->distance; step++) { + uint16_t trace_distance = + input->result_detail == ENCOUNTER_ROUTE_RESULT_NEXT_STEPS && + field->outcome == ROUTE_REACHED_TARGET && + result->distance > 2 + ? 2 + : result->distance; + for (uint16_t step = 0; step < trace_distance; step++) { int current_index = (current_x - topology->origin_x) * topology->height + current_y - topology->origin_y; - uint16_t depth = field->depth[current_index]; + uint16_t depth = + encounter_route_reverse_field_depth(field, current_index); int next_x = current_x; int next_y = current_y; int next_dx = 0; @@ -1403,8 +1461,10 @@ static inline void encounter_route_reverse_build_result( int candidate_index = (candidate_x - topology->origin_x) * topology->height + candidate_y - topology->origin_y; - if (field->depth[candidate_index] == UINT16_MAX || - field->depth[candidate_index] + 1 != depth || + if (!encounter_route_reverse_field_contains( + field, candidate_index) || + encounter_route_reverse_field_depth( + field, candidate_index) + 1 != depth || !encounter_route_step_allowed( input, current_x, current_y, direction_dx[direction], direction_dy[direction])) @@ -1427,7 +1487,9 @@ static inline void encounter_route_reverse_build_result( result->run_dx = next_dx; result->run_dy = next_dy; } - if (step > 0 && (next_dx != segment_dx || next_dy != segment_dy) && + if (trace_distance == result->distance && + step > 0 && + (next_dx != segment_dx || next_dy != segment_dy) && result->waypoint_count < ENCOUNTER_ROUTE_MAX_WAYPOINTS) { result->waypoint_x[result->waypoint_count] = previous_x; result->waypoint_y[result->waypoint_count] = previous_y; @@ -1440,6 +1502,11 @@ static inline void encounter_route_reverse_build_result( current_x = next_x; current_y = next_y; } + if (trace_distance != result->distance) { + result->destination_x = input->target_x; + result->destination_y = input->target_y; + return; + } result->destination_x = current_x; result->destination_y = current_y; if (result->distance > 0 && @@ -1450,6 +1517,53 @@ static inline void encounter_route_reverse_build_result( } } +static inline int encounter_route_reverse_field_matches( + const EncounterReverseRouteField* field, + const EncounterRouteInput* input +) { + return field->valid && + field->topology == input->topology && + field->blocker_ctx == input->blockers.ctx && + field->blocker == input->blockers.is_blocked && + field->topology_revision == input->topology->revision && + field->blocker_revision == input->blockers.revision && + field->target_x == input->target_x && + field->target_y == input->target_y && + field->target_size == input->target_size && + field->actor_size == input->actor_size; +} + +static inline void encounter_route_reverse_field_init( + EncounterReverseRouteField* field, + const EncounterRouteInput* input +) { + field->generation++; + if (field->generation == 0) { + memset( + field->depth_generation, 0, sizeof(field->depth_generation)); + field->generation = 1; + } + field->topology = input->topology; + field->blocker_ctx = input->blockers.ctx; + field->blocker = input->blockers.is_blocked; + field->topology_revision = input->topology->revision; + field->blocker_revision = input->blockers.revision; + field->target_x = input->target_x; + field->target_y = input->target_y; + field->head = 0; + field->tail = 0; + field->actor_size = (uint8_t)input->actor_size; + field->target_size = (uint8_t)input->target_size; + field->outcome = ROUTE_REACHED_TARGET; + field->valid = 1; + if (!encounter_route_reverse_seed( + input, field, input->target_x, input->target_y)) { + encounter_route_reverse_seed_cardinal_edges( + input, field, 0); + field->outcome = ROUTE_REACHED_FALLBACK; + } +} + static inline int encounter_route_try_reverse( const EncounterRouteInput* input, EncounterRouteResult* result @@ -1458,76 +1572,133 @@ static inline int encounter_route_try_reverse( input->cost_policy != ENCOUNTER_ROUTE_COST_SOUTH_FIRST_REVERSE) return 0; EncounterRouteScratch* scratch = &encounter_route_scratch; - EncounterReverseRouteField* field = &scratch->reverse_field; + EncounterReverseRouteField* field = NULL; + for (int cache_idx = 0; + cache_idx < ENCOUNTER_REVERSE_ROUTE_CACHE_SLOTS; + cache_idx++) { + if (encounter_route_reverse_field_matches( + &scratch->reverse_fields[cache_idx], input)) { + field = &scratch->reverse_fields[cache_idx]; + break; + } + } + if (!field) { + field = &scratch->reverse_fields[scratch->next_reverse_field]; + scratch->next_reverse_field = + (uint8_t)((scratch->next_reverse_field + 1) % + ENCOUNTER_REVERSE_ROUTE_CACHE_SLOTS); + encounter_route_reverse_field_init(field, input); + } + if (field->tail == 0) return 0; + const EncounterArenaTopology* topology = input->topology; int source_local_x = input->source_x - topology->origin_x; int source_local_y = input->source_y - topology->origin_y; int source_index = source_local_x * topology->height + source_local_y; - memset(field->depth, 0xff, sizeof(field->depth)); - scratch->current_generation++; - if (scratch->current_generation == 0) { - memset(scratch->blocker_generation, 0, - sizeof(scratch->blocker_generation)); - scratch->current_generation = 1; - } - uint16_t blocker_generation = scratch->current_generation; - int tail = 0; - field->outcome = ROUTE_REACHED_TARGET; - if (!encounter_route_reverse_seed( - input, scratch, field, - input->target_x, input->target_y, &tail)) { - encounter_route_reverse_seed_cardinal_edges( - input, scratch, field, 0, &tail); - field->outcome = ROUTE_REACHED_FALLBACK; - } - if (tail == 0) return 0; - int head = 0; - while (head < tail && field->depth[source_index] == UINT16_MAX) { - int current_packed = scratch->queue[head++]; - int current_x = current_packed >> 6; - int current_y = current_packed & 63; - int current_index = current_x * topology->height + current_y; - int current_abs_x = topology->origin_x + current_x; - int current_abs_y = topology->origin_y + current_y; - uint16_t next_depth = (uint16_t)(field->depth[current_index] + 1); - static const int8_t dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int8_t dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; - for (int direction = 0; direction < 8; direction++) { - int predecessor_x = current_x - dx[direction]; - int predecessor_y = current_y - dy[direction]; - if (predecessor_x < 0 || predecessor_x >= topology->width || - predecessor_y < 0 || predecessor_y >= topology->height) - continue; - int predecessor_index = - predecessor_x * topology->height + predecessor_y; - if (field->depth[predecessor_index] != UINT16_MAX) - continue; - int predecessor_abs_x = current_abs_x - dx[direction]; - int predecessor_abs_y = current_abs_y - dy[direction]; - if (!encounter_route_step_allowed_cached( - input, scratch, blocker_generation, - predecessor_abs_x, predecessor_abs_y, - dx[direction], dy[direction])) - continue; - if (tail >= topology->tile_count) { - fprintf(stderr, "OSRS reverse route queue overflow: %d\n", tail); - abort(); + if (!encounter_route_reverse_field_contains(field, source_index) && + field->head < field->tail) { + scratch->current_generation++; + if (scratch->current_generation == 0) { + memset(scratch->generation, 0, sizeof(scratch->generation)); + memset(scratch->target_generation, 0, + sizeof(scratch->target_generation)); + memset(scratch->blocker_generation, 0, + sizeof(scratch->blocker_generation)); + scratch->current_generation = 1; + } + uint16_t blocker_generation = scratch->current_generation; + while (field->head < field->tail && + !encounter_route_reverse_field_contains(field, source_index)) { + int current_packed = field->queue[field->head++]; + int current_x = current_packed >> 6; + int current_y = current_packed & 63; + int current_index = current_x * topology->height + current_y; + int current_abs_x = topology->origin_x + current_x; + int current_abs_y = topology->origin_y + current_y; + uint16_t next_depth = (uint16_t)( + encounter_route_reverse_field_depth(field, current_index) + 1); + static const int8_t dx[8] = + {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int8_t dy[8] = + {0, 0, -1, 1, -1, -1, 1, 1}; + static const uint8_t step_mask[8] = + {8, 16, 2, 64, 1, 4, 32, 128}; + for (int direction = 0; direction < 8; direction++) { + int predecessor_x = current_x - dx[direction]; + int predecessor_y = current_y - dy[direction]; + if (predecessor_x < 0 || + predecessor_x >= topology->width || + predecessor_y < 0 || + predecessor_y >= topology->height) + continue; + int predecessor_index = + predecessor_x * topology->height + predecessor_y; + if (encounter_route_reverse_field_contains( + field, predecessor_index)) + continue; + int predecessor_abs_x = + current_abs_x - dx[direction]; + int predecessor_abs_y = + current_abs_y - dy[direction]; + if (encounter_route_dynamic_blocked_cached_at_index( + input, + scratch, + blocker_generation, + current_abs_x, + current_abs_y, + current_index)) + continue; + if (dx[direction] != 0 && dy[direction] != 0) { + int y_side_index = + predecessor_x * topology->height + current_y; + if (encounter_route_dynamic_blocked_cached_at_index( + input, + scratch, + blocker_generation, + predecessor_abs_x, + current_abs_y, + y_side_index)) + continue; + int x_side_index = + current_x * topology->height + predecessor_y; + if (encounter_route_dynamic_blocked_cached_at_index( + input, + scratch, + blocker_generation, + current_abs_x, + predecessor_abs_y, + x_side_index)) + continue; + } + if ((topology->legal_step_masks[input->actor_size - 1] + [predecessor_index] & step_mask[direction]) == 0) + continue; + if (field->tail >= topology->tile_count) { + fprintf(stderr, + "OSRS reverse route queue overflow: %u\n", + field->tail); + abort(); + } + encounter_route_reverse_field_set_depth( + field, predecessor_index, next_depth); + field->queue[field->tail++] = + (uint16_t)((predecessor_x << 6) | predecessor_y); } - field->depth[predecessor_index] = next_depth; - scratch->queue[tail++] = - (uint16_t)((predecessor_x << 6) | predecessor_y); } } - if (field->depth[source_index] == UINT16_MAX) return 0; + if (!encounter_route_reverse_field_contains(field, source_index)) return 0; encounter_route_reverse_build_result(input, field, result); return 1; } +#ifdef OSRS_ROUTE_PROBE +static uint64_t osrs_route_probe_source_builds; +static uint64_t osrs_route_probe_source_nodes; +#endif static inline int encounter_route_source_field_matches( const EncounterSourceRouteField* field, const EncounterRouteInput* input ) { - const OsrsLosQuery* los_query = input->los_query; return field->valid && field->topology == input->topology && field->blocker_ctx == input->blockers.ctx && @@ -1536,27 +1707,34 @@ static inline int encounter_route_source_field_matches( field->blocker_revision == input->blockers.revision && field->source_x == input->source_x && field->source_y == input->source_y && - field->target_x == input->target_x && - field->target_y == input->target_y && - field->target_size == input->target_size && - field->target_kind == input->target_kind && - field->attack_range == input->attack_range && - field->los_kind == (los_query ? los_query->kind : 0) && - field->los_blockers == (los_query ? los_query->blockers : NULL) && - field->los_blocker_count == (los_query ? los_query->blocker_count : 0) && - field->los_tile_blocked == - (los_query ? los_query->tile_blocked : NULL) && - field->los_tile_ctx == (los_query ? los_query->tile_ctx : NULL) && - field->actor_size == input->actor_size && - field->movement_mode == input->movement_mode && - field->cost_policy == input->cost_policy; + field->actor_size == input->actor_size; +} +static inline int encounter_route_source_field_set( + const EncounterRouteInput* input +) { + uint64_t key = + input->topology->revision * UINT64_C(0x165667b19e3779f9); + key ^= (uint64_t)(uint32_t)input->source_x * + UINT64_C(0x9e3779b185ebca87); + key ^= (uint64_t)(uint32_t)input->source_y * + UINT64_C(0xc2b2ae3d27d4eb4f); + key ^= input->blockers.revision * UINT64_C(0x85ebca77c2b2ae63); + key ^= (uint64_t)(uint32_t)input->actor_size << 56; + key ^= key >> 33; + key *= UINT64_C(0xff51afd7ed558ccd); + key ^= key >> 33; + return (int)(key & (ENCOUNTER_SOURCE_ROUTE_CACHE_SETS - 1)); } + static inline void encounter_route_build_source_field( const EncounterRouteInput* input, EncounterSourceRouteField* field ) { const EncounterArenaTopology* topology = input->topology; +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_source_builds++; +#endif memset(field->visited, 0, sizeof(field->visited)); memset(field->blocker_known, 0, sizeof(field->blocker_known)); memset(field->blocker_value, 0, sizeof(field->blocker_value)); @@ -1577,31 +1755,9 @@ static inline void encounter_route_build_source_field( field->blocker_revision = input->blockers.revision; field->source_x = input->source_x; field->source_y = input->source_y; - field->target_x = input->target_x; - field->target_y = input->target_y; - field->target_size = input->target_size; - field->target_kind = (uint8_t)input->target_kind; - field->attack_range = input->attack_range; - field->los_kind = (uint8_t)(input->los_query - ? input->los_query->kind - : OSRS_LOS_OPEN); - field->los_blockers = input->los_query - ? input->los_query->blockers - : NULL; - field->los_blocker_count = input->los_query - ? input->los_query->blocker_count - : 0; - field->los_tile_blocked = input->los_query - ? input->los_query->tile_blocked - : NULL; - field->los_tile_ctx = input->los_query - ? input->los_query->tile_ctx - : NULL; field->visited_count = 1; field->expanded_count = 0; field->actor_size = (uint8_t)input->actor_size; - field->movement_mode = (uint8_t)input->movement_mode; - field->cost_policy = (uint8_t)input->cost_policy; field->valid = 1; } @@ -1630,6 +1786,9 @@ static OSRS_ROUTE_NOINLINE int encounter_route_expand_source_field( topology->legal_step_masks[input->actor_size - 1]; while (field->expanded_count < field->visited_count) { int current_packed = field->queue[field->expanded_count++]; +#ifdef OSRS_ROUTE_PROBE + osrs_route_probe_source_nodes++; +#endif int current_x = current_packed >> 6; int current_y = current_packed & 63; int current_index = current_x * topology->height + current_y; @@ -1779,10 +1938,25 @@ static inline int encounter_route_try_source_field( input->cost_policy != ENCOUNTER_ROUTE_COST_OSRS) return 0; EncounterRouteScratch* scratch = &encounter_route_scratch; - EncounterSourceRouteField* field = &scratch->source_field; - const EncounterArenaTopology* topology = input->topology; - if (!encounter_route_source_field_matches(field, input)) + int cache_set = encounter_route_source_field_set(input); + int first_cache_idx = cache_set * ENCOUNTER_SOURCE_ROUTE_CACHE_WAYS; + EncounterSourceRouteField* field = NULL; + for (int way = 0; way < ENCOUNTER_SOURCE_ROUTE_CACHE_WAYS; way++) { + EncounterSourceRouteField* candidate = + &scratch->source_fields[first_cache_idx + way]; + if (encounter_route_source_field_matches(candidate, input)) { + field = candidate; + break; + } + } + if (!field) { + uint8_t way = scratch->next_source_field[cache_set]; + field = &scratch->source_fields[first_cache_idx + way]; + scratch->next_source_field[cache_set] = + (uint8_t)((way + 1) % ENCOUNTER_SOURCE_ROUTE_CACHE_WAYS); encounter_route_build_source_field(input, field); + } + const EncounterArenaTopology* topology = input->topology; uint64_t target_edges[ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION] = {0}; int target_max_x = input->target_x + input->target_size - 1; int target_max_y = input->target_y + input->target_size - 1; @@ -2015,7 +2189,6 @@ static OSRS_ROUTE_NOINLINE int encounter_route_expand_unblocked_osrs( } - static inline EncounterRouteResult encounter_route_solve( const EncounterRouteInput* input ) { @@ -2121,6 +2294,16 @@ static inline EncounterRouteResult encounter_route_solve( } else { encounter_route_mark_targets(input, scratch, generation); } + int blocked_tile_fallback = + input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE && + input->target_size == 1 && + input->cost_policy == ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS && + input->blockers.is_blocked && + encounter_route_dynamic_blocked( + input, input->target_x, input->target_y); + uint16_t blocked_tile_fallback_depth = UINT16_MAX; + int blocked_tile_fallback_index = -1; + int selected_blocked_tile_fallback = 0; int source_local_x = input->source_x - topology->origin_x; int source_local_y = input->source_y - topology->origin_y; int source_index = source_local_x * topology->height + source_local_y; @@ -2128,6 +2311,22 @@ static inline EncounterRouteResult encounter_route_solve( scratch->depth[source_index] = 0; scratch->via[source_index] = VIA_START; scratch->queue[0] = (uint16_t)source_index; + if (blocked_tile_fallback) { + scratch->blocker_generation[target_index] = generation; + scratch->blocker_value[target_index] = 1; + int source_target_distance = + encounter_route_abs(input->source_x - input->target_x) + + encounter_route_abs(input->source_y - input->target_y); + if (source_target_distance == 1) { + blocked_tile_fallback_depth = 0; + blocked_tile_fallback_index = source_index; + if (encounter_route_dynamic_blocked( + input, input->source_x, input->source_y)) { + result.outcome = ROUTE_UNREACHABLE; + return result; + } + } + } int head = 0; int tail = 1; int selected_index = -1; @@ -2174,8 +2373,20 @@ static inline EncounterRouteResult encounter_route_solve( target_index, legal_step_masks); } else { + scratch->queue[0] = + (uint16_t)((source_local_x << 6) | source_local_y); while (head < tail) { - int current_index = scratch->queue[head++]; + int current_packed = scratch->queue[head++]; + int current_x = current_packed >> 6; + int current_y = current_packed & 63; + int current_index = current_x * topology->height + current_y; + if (blocked_tile_fallback_index >= 0 && + scratch->depth[current_index] >= + blocked_tile_fallback_depth) { + selected_index = blocked_tile_fallback_index; + selected_blocked_tile_fallback = 1; + break; + } #ifdef OSRS_ROUTE_PROBE osrs_route_probe_nodes++; #endif @@ -2188,8 +2399,6 @@ static inline EncounterRouteResult encounter_route_solve( uint16_t next_depth = (uint16_t)(scratch->depth[current_index] + 1); uint8_t legal_mask = legal_step_masks[current_index]; - int current_x = current_index / topology->height; - int current_y = current_index % topology->height; int x = topology->origin_x + current_x; int y = topology->origin_y + current_y; for (int i = 0; i < 8; i++) { @@ -2242,13 +2451,32 @@ static inline EncounterRouteResult encounter_route_solve( scratch->generation[next_index] = generation; scratch->depth[next_index] = next_depth; scratch->via[next_index] = (int8_t)direction_via[i]; - scratch->queue[tail++] = (uint16_t)next_index; + scratch->queue[tail++] = + (uint16_t)((next_x << 6) | next_y); + if (blocked_tile_fallback) { + int target_distance = + encounter_route_abs( + topology->origin_x + next_x - + input->target_x) + + encounter_route_abs( + topology->origin_y + next_y - + input->target_y); + if (target_distance == 1 && + (next_depth < blocked_tile_fallback_depth || + (next_depth == blocked_tile_fallback_depth && + next_index < blocked_tile_fallback_index))) { + blocked_tile_fallback_depth = next_depth; + blocked_tile_fallback_index = next_index; + } + } } } } if (selected_index >= 0) { - result.outcome = ROUTE_REACHED_TARGET; + result.outcome = selected_blocked_tile_fallback + ? ROUTE_REACHED_FALLBACK + : ROUTE_REACHED_TARGET; encounter_route_build_result_path( input, &result, diff --git a/ocean/osrs/osrs_policy.h b/ocean/osrs/osrs_policy.h new file mode 100644 index 0000000000..de8287ce7a --- /dev/null +++ b/ocean/osrs/osrs_policy.h @@ -0,0 +1,221 @@ +#ifndef OSRS_POLICY_H +#define OSRS_POLICY_H + +#include "osrs_inventory_actions.h" + +#define OSRS_SHARED_SELF_OBS_SIZE 52 +#define OSRS_SHARED_INVENTORY_CELL_OBS_FEATURES 1 +#define OSRS_SHARED_INVENTORY_OBS_SIZE \ + (OSRS_INVENTORY_SIZE * OSRS_SHARED_INVENTORY_CELL_OBS_FEATURES) +#define OSRS_SHARED_EQUIPPED_OBS_SIZE NUM_GEAR_SLOTS +#define OSRS_SHARED_ITEM_OBS_SIZE \ + (OSRS_SHARED_INVENTORY_OBS_SIZE + OSRS_SHARED_EQUIPPED_OBS_SIZE) +#define OSRS_SHARED_EFFECT_OBS_SIZE OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES +#define OSRS_SHARED_OBS_SIZE \ + (OSRS_SHARED_SELF_OBS_SIZE + OSRS_SHARED_ITEM_OBS_SIZE + \ + OSRS_SHARED_EFFECT_OBS_SIZE) +#define OSRS_SHARED_OBS_MAX_HIT 40 +#define OSRS_SHARED_OBS_INVENTORY_START OSRS_SHARED_SELF_OBS_SIZE +#define OSRS_SHARED_OBS_EQUIPPED_START \ + (OSRS_SHARED_OBS_INVENTORY_START + OSRS_SHARED_INVENTORY_OBS_SIZE) +#define OSRS_SHARED_OBS_EFFECT_START \ + (OSRS_SHARED_OBS_EQUIPPED_START + OSRS_SHARED_EQUIPPED_OBS_SIZE) + +#define OSRS_HEAD_PRIMARY 0 +#define OSRS_HEAD_OVERHEAD 1 +#define OSRS_HEAD_EQUIP_BASE 2 +#define OSRS_HEAD_EQUIP_SLOT(slot) (OSRS_HEAD_EQUIP_BASE + (slot)) +#define OSRS_HEAD_EAT (OSRS_HEAD_EQUIP_BASE + NUM_GEAR_SLOTS) +#define OSRS_HEAD_DRINK (OSRS_HEAD_EAT + 1) +#define OSRS_HEAD_SPELL (OSRS_HEAD_DRINK + 1) +#define OSRS_HEAD_SPECIAL (OSRS_HEAD_SPELL + 1) +#define OSRS_HEAD_OFFENSIVE (OSRS_HEAD_SPECIAL + 1) +#define OSRS_BASE_NUM_ACTION_HEADS (OSRS_HEAD_OFFENSIVE + 1) + +#define OSRS_PRIMARY_MOVE_ACTIONS 25 +#define OSRS_PRIMARY_DIM(target_slots) (OSRS_PRIMARY_MOVE_ACTIONS + (target_slots)) +#define OSRS_OVERHEAD_DIM 7 +#define OSRS_INVENTORY_CLICK_DIM (OSRS_INVENTORY_SIZE + 1) +#define OSRS_SPELL_DIM 5 +#define OSRS_SPECIAL_DIM 3 +#define OSRS_OFFENSIVE_DIM 5 +#define OSRS_BASE_ACTION_MASK_SIZE(target_slots) \ + (OSRS_PRIMARY_DIM(target_slots) + OSRS_OVERHEAD_DIM + \ + (NUM_GEAR_SLOTS + 2) * OSRS_INVENTORY_CLICK_DIM + \ + OSRS_SPELL_DIM + OSRS_SPECIAL_DIM + OSRS_OFFENSIVE_DIM) +#define OSRS_BASE_ACTION_DIMS(target_slots) \ + OSRS_PRIMARY_DIM(target_slots), OSRS_OVERHEAD_DIM, \ + OSRS_INVENTORY_CLICK_DIM, OSRS_INVENTORY_CLICK_DIM, \ + OSRS_INVENTORY_CLICK_DIM, OSRS_INVENTORY_CLICK_DIM, \ + OSRS_INVENTORY_CLICK_DIM, OSRS_INVENTORY_CLICK_DIM, \ + OSRS_INVENTORY_CLICK_DIM, OSRS_INVENTORY_CLICK_DIM, \ + OSRS_INVENTORY_CLICK_DIM, OSRS_INVENTORY_CLICK_DIM, \ + OSRS_INVENTORY_CLICK_DIM, OSRS_INVENTORY_CLICK_DIM, \ + OSRS_INVENTORY_CLICK_DIM, OSRS_SPELL_DIM, \ + OSRS_SPECIAL_DIM, OSRS_OFFENSIVE_DIM +#define OSRS_BASE_ACTION_DIMS_INIT(target_slots) { \ + OSRS_BASE_ACTION_DIMS(target_slots) \ +} + +typedef enum { + OSRS_SPELL_NONE = 0, + OSRS_SPELL_BLOOD_BARRAGE, + OSRS_SPELL_ICE_BARRAGE, + OSRS_SPELL_VENGEANCE, + OSRS_SPELL_DEATH_CHARGE, +} OsrsSpellAction; + +typedef struct { + const Player* player; + const OsrsInteraction* interaction; + int arena_min_x; + int arena_max_x; + int arena_min_y; + int arena_max_y; + AttackStyle attack_style; + int attack_range; + int max_hit; + int attack_speed; + int defence_stab; + int defence_slash; + int defence_crush; + int defence_magic; + int defence_ranged; + int effective_level; + int attack_bonus; + int strength_bonus; + int spell_base_damage; + int special_attack_cost; +} OsrsSharedObservationInput; + +static inline float osrs_policy_ratio(int value, int scale) { + return scale > 0 ? (float)value / (float)scale : 0.0f; +} + +static inline int osrs_base_action_head_mask_offset(int target_slots, int head) { + if (target_slots < 0 || head < 0 || head >= OSRS_BASE_NUM_ACTION_HEADS) { + fprintf(stderr, "osrs policy action offset: invalid targets=%d head=%d\n", + target_slots, head); + abort(); + } + int offset = 0; + for (int current = 0; current < head; current++) { + if (current == OSRS_HEAD_PRIMARY) { + offset += OSRS_PRIMARY_DIM(target_slots); + } else if (current == OSRS_HEAD_OVERHEAD) { + offset += OSRS_OVERHEAD_DIM; + } else if (current >= OSRS_HEAD_EQUIP_BASE && current <= OSRS_HEAD_DRINK) { + offset += OSRS_INVENTORY_CLICK_DIM; + } else if (current == OSRS_HEAD_SPELL) { + offset += OSRS_SPELL_DIM; + } else if (current == OSRS_HEAD_SPECIAL) { + offset += OSRS_SPECIAL_DIM; + } else { + offset += OSRS_OFFENSIVE_DIM; + } + } + return offset; +} + +static inline int osrs_write_shared_observations( + float* obs, + const OsrsSharedObservationInput* input +) { + if (!obs || !input || !input->player || !input->interaction || + input->arena_max_x <= input->arena_min_x || + input->arena_max_y <= input->arena_min_y) { + fprintf(stderr, "osrs shared observation: invalid input\n"); + abort(); + } + + const Player* player = input->player; + int width = input->arena_max_x - input->arena_min_x; + int height = input->arena_max_y - input->arena_min_y; + int i = 0; + + obs[i++] = osrs_policy_ratio(player->current_hitpoints, player->base_hitpoints); + obs[i++] = osrs_policy_ratio(player->current_prayer, player->base_prayer); + obs[i++] = osrs_policy_ratio(player->x - input->arena_min_x, width); + obs[i++] = osrs_policy_ratio(input->arena_max_x - player->x, width); + obs[i++] = osrs_policy_ratio(player->y - input->arena_min_y, height); + obs[i++] = osrs_policy_ratio(input->arena_max_y - player->y, height); + obs[i++] = player->prayer == PRAYER_PROTECT_MELEE ? 1.0f : 0.0f; + obs[i++] = player->prayer == PRAYER_PROTECT_RANGED ? 1.0f : 0.0f; + obs[i++] = player->prayer == PRAYER_PROTECT_MAGIC ? 1.0f : 0.0f; + obs[i++] = player->prayer == PRAYER_SMITE ? 1.0f : 0.0f; + obs[i++] = player->prayer == PRAYER_REDEMPTION ? 1.0f : 0.0f; + obs[i++] = player->offensive_prayer == OFFENSIVE_PRAYER_PIETY ? 1.0f : 0.0f; + obs[i++] = player->offensive_prayer == OFFENSIVE_PRAYER_RIGOUR ? 1.0f : 0.0f; + obs[i++] = player->offensive_prayer == OFFENSIVE_PRAYER_AUGURY ? 1.0f : 0.0f; + obs[i++] = osrs_policy_ratio(player->run_energy, 10000); + obs[i++] = osrs_policy_ratio(player->special_energy, 100); + obs[i++] = player->spec_armed ? 1.0f : 0.0f; + obs[i++] = osrs_policy_ratio(player->attack_timer, 8); + obs[i++] = osrs_policy_ratio(player->food_timer, 3); + obs[i++] = osrs_policy_ratio(player->potion_timer, 3); + obs[i++] = osrs_policy_ratio(player->karambwan_timer, 2); + obs[i++] = osrs_policy_ratio(player->frozen_ticks, 32); + obs[i++] = osrs_policy_ratio(player->freeze_immunity_ticks, 5); + obs[i++] = osrs_interaction_active(input->interaction) ? 1.0f : 0.0f; + obs[i++] = osrs_policy_ratio(player->current_attack, 150); + obs[i++] = osrs_policy_ratio(player->current_strength, 150); + obs[i++] = osrs_policy_ratio(player->current_defence, 150); + obs[i++] = osrs_policy_ratio(player->current_ranged, 150); + obs[i++] = osrs_policy_ratio(player->current_magic, 150); + obs[i++] = osrs_policy_ratio(player->base_attack, 99); + obs[i++] = osrs_policy_ratio(player->base_strength, 99); + obs[i++] = osrs_policy_ratio(player->base_defence, 99); + obs[i++] = osrs_policy_ratio(player->base_ranged, 99); + obs[i++] = osrs_policy_ratio(player->base_magic, 99); + obs[i++] = osrs_policy_ratio(player->base_prayer, 99); + obs[i++] = osrs_policy_ratio(player->base_hitpoints, 99); + obs[i++] = input->attack_style == ATTACK_STYLE_MELEE ? 1.0f : 0.0f; + obs[i++] = input->attack_style == ATTACK_STYLE_RANGED ? 1.0f : 0.0f; + obs[i++] = input->attack_style == ATTACK_STYLE_MAGIC ? 1.0f : 0.0f; + obs[i++] = osrs_policy_ratio(input->attack_range, 15); + obs[i++] = osrs_policy_ratio(input->max_hit, 80); + obs[i++] = osrs_policy_ratio(input->attack_speed, 8); + obs[i++] = osrs_policy_ratio(input->defence_stab, 300); + obs[i++] = osrs_policy_ratio(input->defence_slash, 300); + obs[i++] = osrs_policy_ratio(input->defence_crush, 300); + obs[i++] = osrs_policy_ratio(input->defence_magic, 300); + obs[i++] = osrs_policy_ratio(input->defence_ranged, 300); + obs[i++] = osrs_policy_ratio(input->effective_level, 165); + obs[i++] = osrs_policy_ratio(input->attack_bonus, 200); + obs[i++] = osrs_policy_ratio(input->strength_bonus, 160); + obs[i++] = osrs_policy_ratio(input->spell_base_damage, 40); + obs[i++] = osrs_policy_ratio(input->special_attack_cost, 100); + + if (i != OSRS_SHARED_SELF_OBS_SIZE) { + fprintf(stderr, "osrs shared observation: self wrote %d expected %d\n", + i, OSRS_SHARED_SELF_OBS_SIZE); + abort(); + } + + const OsrsInventoryCell* inventory_cells = + osrs_player_inventory_cells_const(player); + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + osrs_write_inventory_cell_obs_code(&obs[i], &inventory_cells[cell]); + i += OSRS_SHARED_INVENTORY_CELL_OBS_FEATURES; + } + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + uint8_t item = player->equipped[slot]; + uint16_t content_code = item == ITEM_NONE + ? 0 : osrs_inventory_content_code_from_item(item); + obs[i++] = osrs_inventory_cell_obs_code_encode(content_code); + } + + osrs_write_equipment_effect_aggregate( + &obs[i], &player->equipment_effect_profile); + i += OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES; + + if (i != OSRS_SHARED_OBS_SIZE) { + fprintf(stderr, "osrs shared observation: wrote %d expected %d\n", + i, OSRS_SHARED_OBS_SIZE); + abort(); + } + return i; +} + +#endif diff --git a/ocean/osrs/osrs_pvp_actions.h b/ocean/osrs/osrs_pvp_actions.h index ebb7564db2..ead56ff734 100644 --- a/ocean/osrs/osrs_pvp_actions.h +++ b/ocean/osrs/osrs_pvp_actions.h @@ -10,6 +10,7 @@ #include "osrs_pvp_movement.h" #include "osrs_pvp_observations.h" #include "osrs_encounter.h" +#include "osrs_policy.h" /* fury +3 + neitiznot +3, worn in every loadout */ #define PRAYER_BONUS 6 @@ -18,8 +19,17 @@ static void eat_food(Player* p, int is_karambwan) { osrs_player_eat_food_type(p, is_karambwan ? FOOD_KARAMBWAN : FOOD_SHARK); } -static void drink_potion(Player* p, int potion_type) { - if (p->potion_timer > 0) return; +static void pvp_apply_drink_one_dose_effect( + void* ctx, + OsrsConsumableKind kind +) { + Player* p = (Player*)ctx; + int potion_type = POTION_NONE; + if (kind == OSRS_CONSUMABLE_BREW) potion_type = POTION_BREW; + else if (kind == OSRS_CONSUMABLE_SUPER_RESTORE) potion_type = POTION_RESTORE; + else if (kind == OSRS_CONSUMABLE_SUPER_COMBAT) potion_type = POTION_COMBAT; + else if (kind == OSRS_CONSUMABLE_RANGING) potion_type = POTION_RANGED; + else return; switch (potion_type) { case POTION_BREW: { @@ -128,8 +138,6 @@ static void drink_potion(Player* p, int potion_type) { break; } } - - p->potion_timer = 3; p->food_timer = 3; } @@ -205,6 +213,17 @@ static void reset_tick_flags(Player* p) { p->clicks_this_tick = 0; } +static void pvp_refresh_visible_gear(Player* p) { + uint8_t weapon = p->equipped[GEAR_SLOT_WEAPON]; + update_spec_weapons_for_weapon(p, weapon); + AttackStyle style = (AttackStyle)get_item_attack_style(weapon); + if (item_is_spec_weapon(weapon)) p->current_gear = GEAR_SPEC; + else if (style == ATTACK_STYLE_MELEE) p->current_gear = GEAR_MELEE; + else if (style == ATTACK_STYLE_RANGED) p->current_gear = GEAR_RANGED; + else if (style == ATTACK_STYLE_MAGIC) p->current_gear = GEAR_MAGE; + p->visible_gear = weapon == ITEM_VOIDWAKER ? GEAR_MAGE : p->current_gear; +} + static void execute_switches( OsrsEnv* env, int agent_idx, @@ -212,163 +231,114 @@ static void execute_switches( const EncounterArenaTopology* topology ) { Player* p = &env->players[agent_idx]; - p->consumable_used_this_tick = 0; - int overhead_action = actions[HEAD_OVERHEAD]; - int offensive_action = actions[HEAD_OFFENSIVE]; + OsrsInventoryClickActions clicks = {0}; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + clicks.equip_by_slot[slot] = actions[OSRS_HEAD_EQUIP_SLOT(slot)]; + clicks.eat = actions[OSRS_HEAD_EAT]; + clicks.drink = actions[OSRS_HEAD_DRINK]; + OsrsInventoryTickIntent intent = osrs_resolve_inventory_tick_intent( + p, p->inventory_cells, &clicks); + if (intent.drink_cell >= 0 && + !pvp_drink_kind_available( + p, intent.drink_resolution.consumable_kind)) { + intent.drink_cell = -1; + } + if (osrs_inventory_tick_intent_has_effect(&intent)) + osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_EQUIP); + OsrsInventoryApplyStep step; + while (osrs_inventory_intent_next(&intent, &step)) { + if (step.kind == OSRS_INVENTORY_APPLY_EQUIP) { + if (osrs_equip_from_cell( + p, p->inventory_cells, step.cell_idx) >= 0) + p->clicks_this_tick++; + } else if (step.kind == OSRS_INVENTORY_APPLY_EAT) { + FoodType type = + step.resolution.consumable_kind == OSRS_CONSUMABLE_KARAMBWAN + ? FOOD_KARAMBWAN : FOOD_SHARK; + OsrsPlayerEatResult result = osrs_player_eat_food_type(p, type); + if (result.consumed) { + p->inventory_cells[step.cell_idx] = osrs_inventory_cell_empty(); + p->consumable_used_this_tick = 1; + p->clicks_this_tick++; + } + } else { + OsrsInventoryDrinkConsumeResult result = + osrs_inventory_cell_consume_drink_one_dose( + &p->inventory_cells[step.cell_idx], + step.resolution, + &p->potion_timer, + pvp_apply_drink_one_dose_effect, + p); + if (result.consumed) { + p->consumable_used_this_tick = 1; + p->clicks_this_tick++; + } + } + } + pvp_refresh_visible_gear(p); + + int overhead_action = actions[OSRS_HEAD_OVERHEAD]; + int offensive_action = actions[OSRS_HEAD_OFFENSIVE]; if (env->is_lms && (overhead_action == ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE || - overhead_action == ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION)) { + overhead_action == ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION)) overhead_action = ENCOUNTER_OVERHEAD_NO_CHANGE; - } if (p->current_prayer <= 0) { if (overhead_action >= ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE) overhead_action = ENCOUNTER_OVERHEAD_NO_CHANGE; if (offensive_action >= ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY) offensive_action = ENCOUNTER_OFFENSIVE_NO_CHANGE; } - - OverheadPrayer prev_prayer = p->prayer; - OffensivePrayer prev_offensive = p->offensive_prayer; - int prayer_commanded = - overhead_action != ENCOUNTER_OVERHEAD_NO_CHANGE || - offensive_action != ENCOUNTER_OFFENSIVE_NO_CHANGE; - if (encounter_apply_overhead_action(&p->prayer, overhead_action)) { + OverheadPrayer previous_prayer = p->prayer; + OffensivePrayer previous_offensive = p->offensive_prayer; + if (encounter_apply_overhead_action(&p->prayer, overhead_action)) p->prayer_just_activated = 1; - } - if (encounter_apply_offensive_action(&p->offensive_prayer, offensive_action)) { + if (encounter_apply_offensive_action(&p->offensive_prayer, offensive_action)) p->offensive_prayer_just_activated = 1; - } - if (prayer_commanded || p->prayer != prev_prayer || p->offensive_prayer != prev_offensive) + if (p->prayer != previous_prayer || + p->offensive_prayer != previous_offensive) p->clicks_this_tick++; - int loadout_action = actions[HEAD_LOADOUT]; - int loadout_switches = apply_loadout(p, loadout_action); - p->clicks_this_tick += loadout_switches; - if (loadout_switches > 0) - osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_EQUIP); - if (loadout_action == LOADOUT_SPEC_MELEE || loadout_action == LOADOUT_SPEC_RANGE || - loadout_action == LOADOUT_SPEC_MAGIC || loadout_action == LOADOUT_GMAUL) { - p->spec_armed = 1; - } - int food_action = actions[HEAD_FOOD]; - if (food_action == FOOD_EAT && can_eat_food(p)) { - eat_food(p, 0); - p->consumable_used_this_tick = 1; - p->clicks_this_tick++; - osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_EAT); - } + int special_action = actions[OSRS_HEAD_SPECIAL]; + if (special_action == 1 && can_toggle_spec(p)) p->spec_armed = 1; + else if (special_action == 2) p->spec_armed = 0; - int potion_action = actions[HEAD_POTION]; - int potion_ok = 0; - switch (potion_action) { - case POTION_BREW: - potion_ok = can_use_potion(p, POTION_BREW) && can_use_brew_boost(p); - break; - case POTION_RESTORE: - potion_ok = can_use_potion(p, POTION_RESTORE) && can_restore_stats(p); - break; - case POTION_COMBAT: - potion_ok = can_use_potion(p, POTION_COMBAT) && can_boost_combat_skills(p); - break; - case POTION_RANGED: - potion_ok = can_use_potion(p, POTION_RANGED) && can_boost_ranged(p); - break; - default: - break; - } - if (potion_ok) { - drink_potion(p, potion_action); - p->consumable_used_this_tick = 1; - p->clicks_this_tick++; - osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_DRINK); - } - - int karam_action = actions[HEAD_KARAMBWAN]; - if (karam_action == KARAM_EAT && can_eat_karambwan(p)) { - eat_food(p, 1); - p->consumable_used_this_tick = 1; - p->clicks_this_tick++; - osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_EAT); - } - int combat_action = actions[HEAD_COMBAT]; - int head_move = actions[HEAD_MOVE]; - int is_spec_loadout = (loadout_action == LOADOUT_SPEC_MELEE || - loadout_action == LOADOUT_SPEC_RANGE || - loadout_action == LOADOUT_SPEC_MAGIC || - loadout_action == LOADOUT_GMAUL); - - int attack_claims_tick = is_attack_action(combat_action); - if (attack_claims_tick) { + int primary = actions[OSRS_HEAD_PRIMARY]; + if (primary >= OSRS_PRIMARY_MOVE_ACTIONS && + primary < OSRS_PRIMARY_DIM(1)) { + osrs_interaction_set(&p->interaction, 1 - agent_idx); env->pvp_runtime.walk_dest_x[agent_idx] = -1; env->pvp_runtime.walk_dest_y[agent_idx] = -1; - } - - int command_issued = 0; - if (!is_spec_loadout && !attack_claims_tick && head_move > 0 && head_move < MOVE_DIM) { - pvp_set_walk_dest_from_head_move(env, agent_idx, head_move); - command_issued = 1; - } else if (!is_spec_loadout && is_move_action(combat_action)) { - int tx = p->last_obs_target_x; - int ty = p->last_obs_target_y; - int dest_x = -1, dest_y = -1; - switch (combat_action) { - case MOVE_ADJACENT: - if (!select_closest_adjacent_tile( - p, tx, ty, &dest_x, &dest_y, topology)) { - dest_x = -1; dest_y = -1; - } - break; - case MOVE_UNDER: - if (pvp_topology_tile_walkable(topology, tx, ty)) { - dest_x = tx; dest_y = ty; - } - break; - case MOVE_DIAGONAL: - if (!select_closest_diagonal_tile( - p, tx, ty, &dest_x, &dest_y, topology)) { - dest_x = -1; dest_y = -1; - } - break; - case MOVE_FARCAST_2: - case MOVE_FARCAST_3: - case MOVE_FARCAST_4: - case MOVE_FARCAST_5: - case MOVE_FARCAST_6: - case MOVE_FARCAST_7: { - int fd = combat_action - MOVE_FARCAST_2 + 2; - if (!select_farcast_tile( - p, tx, ty, fd, &dest_x, &dest_y, topology)) { - dest_x = -1; dest_y = -1; - } - break; - } - default: - break; - } - env->pvp_runtime.walk_dest_x[agent_idx] = dest_x; - env->pvp_runtime.walk_dest_y[agent_idx] = dest_y; - command_issued = (dest_x >= 0); - } - /* no clearing else: walk_dest persists until arrival, matching OSRS click - semantics (the SDK sets it back to -1 when the player gets there) */ - if (command_issued) { + } else if (primary > 0 && primary < OSRS_PRIMARY_MOVE_ACTIONS) { + pvp_set_walk_dest_from_head_move(env, agent_idx, primary); p->clicks_this_tick++; osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_MOVE); } - int veng_action = actions[HEAD_VENG]; - if (veng_action == VENG_CAST && p->is_lunar_spellbook && - !p->veng_active && remaining_ticks(p->veng_cooldown) == 0 && - p->current_magic >= 94) { + + if (actions[OSRS_HEAD_SPELL] == OSRS_SPELL_VENGEANCE && + p->is_lunar_spellbook && !p->veng_active && + remaining_ticks(p->veng_cooldown) == 0 && + p->current_magic >= 94) { p->veng_active = 1; p->veng_cooldown = 50; p->cast_veng_this_tick = 1; p->clicks_this_tick++; } + (void)topology; } +typedef enum { + PVP_ATTACK_NONE = 0, + PVP_ATTACK_WEAPON, + PVP_ATTACK_ICE, + PVP_ATTACK_BLOOD, +} PvpAttackIntent; + +#define PVP_MOVE_NONE 0 + typedef struct { int attack_action; int move_action; @@ -379,29 +349,24 @@ typedef struct { static PvpAttackDecode pvp_decode_attack_actions( OsrsEnv* env, int agent_idx, Player* p, const int* actions ) { - int loadout_action = actions[HEAD_LOADOUT]; - int combat_action = actions[HEAD_COMBAT]; - int attack_action = is_attack_action(combat_action) ? combat_action : ATTACK_NONE; - int move_action = is_move_action(combat_action) ? combat_action : MOVE_NONE; - int explicit_move_in_progress = (actions[HEAD_MOVE] > 0 && actions[HEAD_MOVE] < MOVE_DIM) - || env->pvp_runtime.walk_dest_x[agent_idx] >= 0; - - int is_gmaul = (loadout_action == LOADOUT_GMAUL); - if (is_gmaul) { - attack_action = ATTACK_ATK; - move_action = MOVE_NONE; - } - - int current_loadout = get_current_loadout(p); - int in_mage_loadout = (current_loadout == LOADOUT_MAGE); - int in_tank_loadout = (current_loadout == LOADOUT_TANK); - if (attack_action == ATTACK_ATK && (in_mage_loadout || in_tank_loadout) && !is_gmaul) { - attack_action = ATTACK_NONE; - } - + int primary = actions[OSRS_HEAD_PRIMARY]; + int spell = actions[OSRS_HEAD_SPELL]; + int attack_action = PVP_ATTACK_NONE; + if (primary >= OSRS_PRIMARY_MOVE_ACTIONS && + primary < OSRS_PRIMARY_DIM(1)) { + if (spell == OSRS_SPELL_ICE_BARRAGE) attack_action = PVP_ATTACK_ICE; + else if (spell == OSRS_SPELL_BLOOD_BARRAGE) attack_action = PVP_ATTACK_BLOOD; + else attack_action = PVP_ATTACK_WEAPON; + } + int explicit_move_in_progress = + (primary > 0 && primary < OSRS_PRIMARY_MOVE_ACTIONS) || + env->pvp_runtime.walk_dest_x[agent_idx] >= 0; + int is_gmaul = + p->equipped[GEAR_SLOT_WEAPON] == ITEM_GRANITE_MAUL && + p->spec_armed; return (PvpAttackDecode){ .attack_action = attack_action, - .move_action = move_action, + .move_action = PVP_MOVE_NONE, .explicit_move_in_progress = explicit_move_in_progress, .is_gmaul = is_gmaul, }; @@ -419,30 +384,30 @@ static void execute_attack_movement( PvpAttackDecode decode = pvp_decode_attack_actions(env, agent_idx, p, actions); - if (decode.attack_action != ATTACK_NONE) + if (decode.attack_action != PVP_ATTACK_NONE) osrs_interaction_set(&p->interaction, 1 - agent_idx); int has_attack = - decode.attack_action != ATTACK_NONE || + decode.attack_action != PVP_ATTACK_NONE || osrs_interaction_active(&p->interaction); int distance = chebyshev_distance(p->x, p->y, t->x, t->y); AttackStyle attack_style = ATTACK_STYLE_NONE; - if (decode.attack_action == ATTACK_ATK) + if (decode.attack_action == PVP_ATTACK_WEAPON) attack_style = get_slot_weapon_attack_style(p); - else if (decode.attack_action == ATTACK_ICE || - decode.attack_action == ATTACK_BLOOD) + else if (decode.attack_action == PVP_ATTACK_ICE || + decode.attack_action == PVP_ATTACK_BLOOD) attack_style = ATTACK_STYLE_MAGIC; else if (osrs_interaction_active(&p->interaction)) attack_style = get_slot_weapon_attack_style(p); - if (decode.attack_action == ATTACK_ICE && !can_cast_ice_spell(p)) + if (decode.attack_action == PVP_ATTACK_ICE && !can_cast_ice_spell(p)) attack_style = ATTACK_STYLE_NONE; - if (decode.attack_action == ATTACK_BLOOD && !can_cast_blood_spell(p)) + if (decode.attack_action == PVP_ATTACK_BLOOD && !can_cast_blood_spell(p)) attack_style = ATTACK_STYLE_NONE; p->did_attack_auto_move = 0; if (!has_attack || - decode.move_action != MOVE_NONE || + decode.move_action != PVP_MOVE_NONE || decode.explicit_move_in_progress || !can_move(p)) return; @@ -471,46 +436,46 @@ static void execute_attack_combat( PvpAttackDecode decode = pvp_decode_attack_actions(env, agent_idx, p, actions); - if (decode.attack_action == ATTACK_NONE && osrs_interaction_active(&p->interaction)) { + if (decode.attack_action == PVP_ATTACK_NONE && osrs_interaction_active(&p->interaction)) { AttackStyle weapon_style = get_slot_weapon_attack_style(p); if (weapon_style != ATTACK_STYLE_MAGIC) { - decode.attack_action = ATTACK_ATK; + decode.attack_action = PVP_ATTACK_WEAPON; } } int attack_ready = can_attack_now(p); - int has_attack = (decode.attack_action != ATTACK_NONE); + int has_attack = (decode.attack_action != PVP_ATTACK_NONE); int dist = chebyshev_distance(p->x, p->y, t->x, t->y); AttackStyle attack_style = ATTACK_STYLE_NONE; int magic_type = 0; switch (decode.attack_action) { - case ATTACK_ATK: + case PVP_ATTACK_WEAPON: attack_style = get_slot_weapon_attack_style(p); break; - case ATTACK_ICE: + case PVP_ATTACK_ICE: attack_style = ATTACK_STYLE_MAGIC; magic_type = 1; break; - case ATTACK_BLOOD: + case PVP_ATTACK_BLOOD: attack_style = ATTACK_STYLE_MAGIC; magic_type = 2; break; default: break; } - if (decode.attack_action == ATTACK_ICE && !can_cast_ice_spell(p)) { + if (decode.attack_action == PVP_ATTACK_ICE && !can_cast_ice_spell(p)) { attack_style = ATTACK_STYLE_NONE; } - if (decode.attack_action == ATTACK_BLOOD && !can_cast_blood_spell(p)) { + if (decode.attack_action == PVP_ATTACK_BLOOD && !can_cast_blood_spell(p)) { attack_style = ATTACK_STYLE_NONE; } int can_attack = attack_ready || (decode.is_gmaul && is_granite_maul_attack_available(p)); switch (decode.attack_action) { - case ATTACK_ATK: + case PVP_ATTACK_WEAPON: if (can_attack && attack_style != ATTACK_STYLE_NONE) { AttackStyle actual_style = (attack_style == ATTACK_STYLE_MAGIC) ? ATTACK_STYLE_MELEE @@ -531,8 +496,8 @@ static void execute_attack_combat( } } break; - case ATTACK_ICE: - case ATTACK_BLOOD: + case PVP_ATTACK_ICE: + case PVP_ATTACK_BLOOD: if (attack_ready && attack_style == ATTACK_STYLE_MAGIC) { int range = get_attack_range(p, ATTACK_STYLE_MAGIC); if (dist > 0 && dist <= range) { @@ -545,7 +510,7 @@ static void execute_attack_combat( break; } - if (has_attack && decode.move_action == MOVE_NONE && !decode.explicit_move_in_progress + if (has_attack && decode.move_action == PVP_MOVE_NONE && !decode.explicit_move_in_progress && can_move(p) && !p->did_attack_auto_move) { int in_range = 0; int chase_range = 1; @@ -583,6 +548,12 @@ static void execute_attack_combat( } } +static inline int pvp_remaining_supply_units(const Player* p) { + return p->food_count + p->karambwan_count + + p->brew_doses + p->restore_doses + + p->combat_potion_doses + p->ranged_potion_doses; +} + static float calculate_reward(OsrsEnv* env, int agent_idx) { float reward = 0.0f; Player* p = &env->players[agent_idx]; @@ -596,7 +567,8 @@ static float calculate_reward(OsrsEnv* env, int agent_idx) { } if (cfg->prayer_penalty_enabled && !t->just_attacked) { - int overhead = env->last_executed_actions[agent_idx * NUM_ACTION_HEADS + HEAD_OVERHEAD]; + int overhead = env->last_executed_actions[ + agent_idx * OSRS_BASE_NUM_ACTION_HEADS + OSRS_HEAD_OVERHEAD]; if (overhead == OVERHEAD_MAGE || overhead == OVERHEAD_RANGED || overhead == OVERHEAD_MELEE) { reward += cfg->prayer_switch_no_attack_penalty; } @@ -624,17 +596,17 @@ static float calculate_reward(OsrsEnv* env, int agent_idx) { if (t->food_count > 0 || t->karambwan_count > 0 || t->brew_doses > 0) { reward += cfg->ko_bonus; } - float opp_total = (float)(t->food_count + t->karambwan_count - + t->brew_doses + t->restore_doses - + t->combat_potion_doses - + t->ranged_potion_doses); - float max_total = (float)(MAXED_FOOD_COUNT + MAXED_KARAMBWAN_COUNT - + MAXED_BREW_DOSES + MAXED_RESTORE_DOSES - + MAXED_COMBAT_POTION_DOSES - + MAXED_RANGED_POTION_DOSES); - if (max_total > 0.0f) { - reward += cfg->ko_supplies_bonus_coef * (opp_total / max_total); + float opp_total = (float)pvp_remaining_supply_units(t); + int initial_supply_units = + env->pvp_runtime.initial_supply_units[1 - agent_idx]; + if (initial_supply_units <= 0) { + fprintf(stderr, + "pvp reward: invalid initial supply count %d\n", + initial_supply_units); + abort(); } + reward += cfg->ko_supplies_bonus_coef * + (opp_total / (float)initial_supply_units); } else if (env->winner == (1 - agent_idx)) { if (p->food_count > 0 || p->karambwan_count > 0 || p->brew_doses > 0) { reward += cfg->wasted_resources_penalty; diff --git a/ocean/osrs/osrs_pvp_api.h b/ocean/osrs/osrs_pvp_api.h index 55c4504cbb..10b4a9f737 100644 --- a/ocean/osrs/osrs_pvp_api.h +++ b/ocean/osrs/osrs_pvp_api.h @@ -216,21 +216,54 @@ static void set_fight_positions(OsrsEnv* env) { env->players[1].dest_y = y1; env->players[1].is_moving = 0; } +static void pvp_seed_inventory_supplies(Player* p) { + static const struct { + OsrsConsumableKind kind; + int dose; + int cells; + } POTIONS[] = { + {OSRS_CONSUMABLE_BREW, 4, 1}, + {OSRS_CONSUMABLE_SUPER_RESTORE, 4, 2}, + {OSRS_CONSUMABLE_SUPER_COMBAT, 4, 1}, + {OSRS_CONSUMABLE_RANGING, 4, 1}, + }; + for (int k = 0; k < (int)(sizeof(POTIONS) / sizeof(POTIONS[0])); k++) { + for (int n = 0; n < POTIONS[k].cells; n++) { + int cell = osrs_first_empty_inventory_cell(p->inventory_cells, -1); + if (cell < 0) abort(); + p->inventory_cells[cell] = osrs_inventory_cell_from_content_code( + osrs_inventory_content_code_from_consumable( + POTIONS[k].kind, POTIONS[k].dose)); + } + } + for (int n = 0; n < p->karambwan_count; n++) { + int cell = osrs_first_empty_inventory_cell(p->inventory_cells, -1); + if (cell < 0) abort(); + p->inventory_cells[cell] = + osrs_inventory_cell_from_raw_osrs_id(3144); + } + p->food_count = 0; + for (;;) { + int cell = osrs_first_empty_inventory_cell(p->inventory_cells, -1); + if (cell < 0) abort(); + if (osrs_first_empty_inventory_cell(p->inventory_cells, cell) < 0) + break; + p->inventory_cells[cell] = + osrs_inventory_cell_from_raw_osrs_id(385); + p->food_count++; + } +} + /** Point env buffers at internal storage and zero all runtime state. */ void pvp_init(OsrsEnv* env) { - env->observations = env->_obs_buf; env->actions = env->_acts_buf; env->rewards = env->_rews_buf; env->terminals = env->_terms_buf; - env->action_masks = env->_masks_buf; - env->action_masks_agents = 0x3; - memset(env->_obs_buf, 0, sizeof(env->_obs_buf)); memset(env->_acts_buf, 0, sizeof(env->_acts_buf)); memset(env->_rews_buf, 0, sizeof(env->_rews_buf)); memset(env->_terms_buf, 0, sizeof(env->_terms_buf)); - memset(env->_masks_buf, 0, sizeof(env->_masks_buf)); env->_episode_return = 0.0f; env->has_rng_seed = 0; @@ -334,7 +367,9 @@ void pvp_reset( int tiers[NUM_AGENTS] = { base_tier, p1_tier }; for (int i = 0; i < NUM_AGENTS; i++) { init_player_gear_randomized(&env->players[i], tiers[i], &env->rng_state); - env->players[i].food_count = compute_food_count(&env->players[i]); + pvp_seed_inventory_supplies(&env->players[i]); + env->pvp_runtime.initial_supply_units[i] = + pvp_remaining_supply_units(&env->players[i]); osrs_refresh_player_equipment(&env->players[i]); } @@ -345,12 +380,6 @@ void pvp_reset( opponent_reset(env, &env->pvp_runtime.opponent_p0); } - for (int i = 0; i < NUM_AGENTS; i++) { - generate_slot_observations(env, i, topology); - if (env->action_masks != NULL && (env->action_masks_agents & (1 << i))) { - compute_action_masks(env, i, topology); - } - } } static void pvp_resolve_same_tile( @@ -403,65 +432,53 @@ void pvp_step( reset_tick_flags(&env->players[1]); if (env->pvp_runtime.use_c_opponent_p0) { - memset(env->actions, 0, NUM_ACTION_HEADS * sizeof(int)); + memset(env->actions, 0, OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); } else { - memcpy(env->actions, env->ocean_io.agent_actions, NUM_ACTION_HEADS * sizeof(int)); + memcpy(env->actions, env->ocean_io.agent_actions, + OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); } if (env->pvp_runtime.use_external_opponent_actions) { memcpy( - env->actions + NUM_ACTION_HEADS, + env->actions + OSRS_BASE_NUM_ACTION_HEADS, env->pvp_runtime.external_opponent_actions, - NUM_ACTION_HEADS * sizeof(int) + OSRS_BASE_NUM_ACTION_HEADS * sizeof(int) ); } else { - memset(env->actions + NUM_ACTION_HEADS, 0, NUM_ACTION_HEADS * sizeof(int)); + memset(env->actions + OSRS_BASE_NUM_ACTION_HEADS, 0, + OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); } - if (env->pvp_runtime.use_c_opponent && !env->pvp_runtime.use_external_opponent_actions) { + if (env->pvp_runtime.use_c_opponent && + !env->pvp_runtime.use_external_opponent_actions) { generate_opponent_action(env, &env->pvp_runtime.opponent); memcpy( - env->actions + NUM_ACTION_HEADS, - env->pending_actions + NUM_ACTION_HEADS, - NUM_ACTION_HEADS * sizeof(int) + env->actions + OSRS_BASE_NUM_ACTION_HEADS, + env->pending_actions + OSRS_BASE_NUM_ACTION_HEADS, + OSRS_BASE_NUM_ACTION_HEADS * sizeof(int) ); } if (env->pvp_runtime.use_c_opponent_p0) { - generate_opponent_action_for_player0(env, &env->pvp_runtime.opponent_p0); + generate_opponent_action_for_player0( + env, &env->pvp_runtime.opponent_p0); memcpy( env->actions, env->pending_actions, - NUM_ACTION_HEADS * sizeof(int) + OSRS_BASE_NUM_ACTION_HEADS * sizeof(int) ); } int first = env->pid_holder; int second = 1 - env->pid_holder; + int actions_p0[OSRS_BASE_NUM_ACTION_HEADS]; + int actions_p1[OSRS_BASE_NUM_ACTION_HEADS]; + memcpy(actions_p0, env->actions, + OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); + memcpy(actions_p1, env->actions + OSRS_BASE_NUM_ACTION_HEADS, + OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); - int actions_p0[NUM_ACTION_HEADS]; - int actions_p1[NUM_ACTION_HEADS]; - memcpy(actions_p0, env->actions, NUM_ACTION_HEADS * sizeof(int)); - memcpy(actions_p1, env->actions + NUM_ACTION_HEADS, NUM_ACTION_HEADS * sizeof(int)); - - for (int i = 0; i < NUM_AGENTS; i++) { - int* a = (i == 0) ? actions_p0 : actions_p1; - int lo = a[HEAD_LOADOUT]; - int cv = a[HEAD_COMBAT]; - if (lo == LOADOUT_MAGE || lo == LOADOUT_TANK || lo == LOADOUT_SPEC_MAGIC) { - if (cv == ATTACK_ATK) { - a[HEAD_COMBAT] = ATTACK_NONE; - } - } - } - - memcpy(env->actions, actions_p0, NUM_ACTION_HEADS * sizeof(int)); - memcpy(env->actions + NUM_ACTION_HEADS, actions_p1, NUM_ACTION_HEADS * sizeof(int)); - - memcpy( - env->last_executed_actions, - env->actions, - NUM_AGENTS * NUM_ACTION_HEADS * sizeof(int) - ); + memcpy(env->last_executed_actions, env->actions, + NUM_AGENTS * OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); update_timers(&env->players[0]); update_timers(&env->players[1]); @@ -552,7 +569,7 @@ void pvp_step( } memcpy(env->pending_actions, env->actions, - NUM_AGENTS * NUM_ACTION_HEADS * sizeof(int)); + NUM_AGENTS * OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); for (int i = 0; i < NUM_AGENTS; i++) { if (env->players[i].current_hitpoints <= 0) { env->episode_over = 1; @@ -573,17 +590,6 @@ void pvp_step( } env->_episode_return += env->rewards[0]; - for (int i = 0; i < NUM_AGENTS; i++) { - generate_slot_observations(env, i, route_topology); - if (env->action_masks != NULL && (env->action_masks_agents & (1 << i))) { - compute_action_masks(env, i, route_topology); - } - } - - ocean_write_obs(env); - if (env->ocean_io.agent_obs_p1 != NULL) { - ocean_write_obs_p1(env); - } env->ocean_io.agent_rewards[0] = env->rewards[0]; if (env->episode_over) { diff --git a/ocean/osrs/osrs_pvp_gear.h b/ocean/osrs/osrs_pvp_gear.h index d2ec45b86d..1bc8fc06f6 100644 --- a/ocean/osrs/osrs_pvp_gear.h +++ b/ocean/osrs/osrs_pvp_gear.h @@ -2,6 +2,7 @@ #define OSRS_PVP_GEAR_H #include "osrs_types.h" +#include "osrs_policy.h" #include "osrs_items.h" #include "osrs_inventory.h" #include "osrs_combat.h" @@ -34,6 +35,8 @@ static const uint8_t MELEE_WEAPON_PRIORITY[] = { static const uint8_t RANGE_WEAPON_PRIORITY[] = { ITEM_MORRIGANS_JAVELIN, ITEM_ZARYTE_CROSSBOW, ITEM_ARMADYL_CROSSBOW, ITEM_RUNE_CROSSBOW }; +#define RANGE_WEAPON_PRIORITY_LEN \ + (sizeof(RANGE_WEAPON_PRIORITY) / sizeof(RANGE_WEAPON_PRIORITY[0])) static const uint8_t MAGE_WEAPON_PRIORITY[] = { ITEM_ZURIELS_STAFF, ITEM_KODAI_WAND, ITEM_VOLATILE_STAFF, @@ -216,15 +219,29 @@ static inline int slot_equip_item(Player* p, int gear_slot, uint8_t item_idx) { return 1; } -static inline int player_has_item_in_slot(Player* p, int gear_slot, uint8_t item_idx) { - for (int i = 0; i < p->num_items_in_slot[gear_slot]; i++) { - if (p->inventory[gear_slot][i] == item_idx) return 1; +static inline int pvp_inventory_cell_with_item( + const Player* p, + uint8_t item_idx +) { + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + if (osrs_inventory_cell_item_index(&p->inventory_cells[cell]) == item_idx) + return cell; } - return 0; + return -1; +} + +static inline int player_has_item_in_slot( + const Player* p, + int gear_slot, + uint8_t item_idx +) { + if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) return 0; + if (p->equipped[gear_slot] == item_idx) return 1; + return pvp_inventory_cell_with_item(p, item_idx) >= 0; } static inline uint8_t find_best_available( - Player* p, int gear_slot, + const Player* p, int gear_slot, const uint8_t* priority, int priority_len ) { for (int i = 0; i < priority_len; i++) { @@ -235,19 +252,19 @@ static inline uint8_t find_best_available( return ITEM_NONE; } -static inline uint8_t find_best_melee_spec(Player* p) { +static inline uint8_t find_best_melee_spec(const Player* p) { return find_best_available(p, GEAR_SLOT_WEAPON, MELEE_SPEC_PRIORITY, MELEE_SPEC_PRIORITY_LEN); } -static inline uint8_t find_best_ranged_spec(Player* p) { +static inline uint8_t find_best_ranged_spec(const Player* p) { return find_best_available(p, GEAR_SLOT_WEAPON, RANGE_SPEC_PRIORITY, RANGE_SPEC_PRIORITY_LEN); } -static inline uint8_t find_best_magic_spec(Player* p) { +static inline uint8_t find_best_magic_spec(const Player* p) { return find_best_available(p, GEAR_SLOT_WEAPON, MAGIC_SPEC_PRIORITY, MAGIC_SPEC_PRIORITY_LEN); } -static inline int player_has_gmaul(Player* p) { +static inline int player_has_gmaul(const Player* p) { return player_has_item_in_slot(p, GEAR_SLOT_WEAPON, ITEM_GRANITE_MAUL); } @@ -266,17 +283,24 @@ typedef struct { GearPriorityList neck; GearPriorityList ring; int shield_two_handed_aware; -} LoadoutPriorities; +} PvpEquipmentPriorities; + +typedef enum { + PVP_EQUIPMENT_MELEE = 0, + PVP_EQUIPMENT_RANGED, + PVP_EQUIPMENT_MAGIC, + PVP_EQUIPMENT_TANK, + PVP_EQUIPMENT_SPEC_MELEE, + PVP_EQUIPMENT_SPEC_RANGED, + PVP_EQUIPMENT_SPEC_MAGIC, + PVP_EQUIPMENT_GMAUL, + PVP_EQUIPMENT_PLAN_COUNT, +} PvpEquipmentPlan; #define GEAR_LIST(arr) { arr, (int)(sizeof(arr) / sizeof((arr)[0])) } -/* rows are positional in LOADOUT_* enum order (KEEP hole first, then MELEE, - RANGE, MAGE, TANK, SPEC_MELEE, SPEC_RANGE, SPEC_MAGIC); columns are weapon, - shield, body, legs, head, cape, neck, ring, shield_two_handed_aware. - g++ on the trainer include path cannot compile designated initializers here. */ -static const LoadoutPriorities LOADOUT_PRIORITIES[LOADOUT_GMAUL] = { - { {NULL, 0}, {NULL, 0}, {NULL, 0}, {NULL, 0}, - {NULL, 0}, {NULL, 0}, {NULL, 0}, {NULL, 0}, 0 }, +static const PvpEquipmentPriorities +PVP_EQUIPMENT_PRIORITIES[PVP_EQUIPMENT_PLAN_COUNT] = { { GEAR_LIST(MELEE_WEAPON_PRIORITY), GEAR_LIST(MELEE_SHIELD_PRIORITY), GEAR_LIST(TANK_BODY_PRIORITY), GEAR_LIST(TANK_LEGS_PRIORITY), GEAR_LIST(TANK_HEAD_PRIORITY), GEAR_LIST(MELEE_CAPE_PRIORITY), @@ -300,88 +324,96 @@ static const LoadoutPriorities LOADOUT_PRIORITIES[LOADOUT_GMAUL] = { { GEAR_LIST(RANGE_SPEC_PRIORITY), GEAR_LIST(TANK_SHIELD_PRIORITY), GEAR_LIST(TANK_BODY_PRIORITY), GEAR_LIST(TANK_LEGS_PRIORITY), GEAR_LIST(TANK_HEAD_PRIORITY), GEAR_LIST(MAGE_CAPE_PRIORITY), - GEAR_LIST(MELEE_NECK_PRIORITY), GEAR_LIST(MELEE_RING_PRIORITY), 1 }, + GEAR_LIST(MELEE_NECK_PRIORITY), GEAR_LIST(MAGE_RING_PRIORITY), 1 }, { GEAR_LIST(MAGIC_SPEC_PRIORITY), GEAR_LIST(MAGE_SHIELD_PRIORITY), GEAR_LIST(MAGE_BODY_PRIORITY), GEAR_LIST(MAGE_LEGS_PRIORITY), GEAR_LIST(MAGE_HEAD_PRIORITY), GEAR_LIST(MAGE_CAPE_PRIORITY), GEAR_LIST(MAGE_NECK_PRIORITY), GEAR_LIST(MAGE_RING_PRIORITY), 0 }, + { GEAR_LIST(MELEE_SPEC_PRIORITY), GEAR_LIST(MELEE_SHIELD_PRIORITY), + GEAR_LIST(TANK_BODY_PRIORITY), GEAR_LIST(TANK_LEGS_PRIORITY), + GEAR_LIST(TANK_HEAD_PRIORITY), GEAR_LIST(MELEE_CAPE_PRIORITY), + GEAR_LIST(MELEE_NECK_PRIORITY), GEAR_LIST(MELEE_RING_PRIORITY), 1 }, }; -/* out[] order must match DYNAMIC_GEAR_SLOTS: weapon, shield, body, legs, head, - cape, neck, ring */ -static inline void resolve_loadout(Player* p, int loadout, uint8_t out[NUM_DYNAMIC_GEAR_SLOTS]) { - for (int i = 0; i < NUM_DYNAMIC_GEAR_SLOTS; i++) { +#undef GEAR_LIST + +static inline void pvp_resolve_equipment_plan( + const Player* p, + PvpEquipmentPlan plan, + uint8_t out[NUM_DYNAMIC_GEAR_SLOTS] +) { + for (int i = 0; i < NUM_DYNAMIC_GEAR_SLOTS; i++) out[i] = p->equipped[DYNAMIC_GEAR_SLOTS[i]]; - } - if (loadout == LOADOUT_GMAUL) { + if (plan == PVP_EQUIPMENT_GMAUL) { out[0] = ITEM_GRANITE_MAUL; out[1] = osrs_suppress_shield_for_two_handed_weapon(out[0], out[1]); return; } - if (loadout < LOADOUT_MELEE || loadout > LOADOUT_SPEC_MAGIC) { - return; - } - - const LoadoutPriorities* lp = &LOADOUT_PRIORITIES[loadout]; + if (plan < 0 || plan >= PVP_EQUIPMENT_PLAN_COUNT) return; - uint8_t weapon = find_best_available(p, GEAR_SLOT_WEAPON, lp->weapon.items, lp->weapon.len); + const PvpEquipmentPriorities* priorities = &PVP_EQUIPMENT_PRIORITIES[plan]; + uint8_t weapon = find_best_available( + p, GEAR_SLOT_WEAPON, priorities->weapon.items, priorities->weapon.len); if (weapon != ITEM_NONE) out[0] = weapon; - - if (lp->shield_two_handed_aware && item_is_two_handed(out[0])) { + if (priorities->shield_two_handed_aware && item_is_two_handed(out[0])) { out[1] = osrs_suppress_shield_for_two_handed_weapon(out[0], out[1]); } else { - uint8_t shield = find_best_available(p, GEAR_SLOT_SHIELD, lp->shield.items, lp->shield.len); + uint8_t shield = find_best_available( + p, GEAR_SLOT_SHIELD, priorities->shield.items, priorities->shield.len); if (shield != ITEM_NONE) out[1] = shield; } - - uint8_t body = find_best_available(p, GEAR_SLOT_BODY, lp->body.items, lp->body.len); + uint8_t body = find_best_available( + p, GEAR_SLOT_BODY, priorities->body.items, priorities->body.len); if (body != ITEM_NONE) out[2] = body; - uint8_t legs = find_best_available(p, GEAR_SLOT_LEGS, lp->legs.items, lp->legs.len); + uint8_t legs = find_best_available( + p, GEAR_SLOT_LEGS, priorities->legs.items, priorities->legs.len); if (legs != ITEM_NONE) out[3] = legs; - uint8_t head = find_best_available(p, GEAR_SLOT_HEAD, lp->head.items, lp->head.len); + uint8_t head = find_best_available( + p, GEAR_SLOT_HEAD, priorities->head.items, priorities->head.len); if (head != ITEM_NONE) out[4] = head; - uint8_t cape = find_best_available(p, GEAR_SLOT_CAPE, lp->cape.items, lp->cape.len); + uint8_t cape = find_best_available( + p, GEAR_SLOT_CAPE, priorities->cape.items, priorities->cape.len); if (cape != ITEM_NONE) out[5] = cape; - uint8_t neck = find_best_available(p, GEAR_SLOT_NECK, lp->neck.items, lp->neck.len); + uint8_t neck = find_best_available( + p, GEAR_SLOT_NECK, priorities->neck.items, priorities->neck.len); if (neck != ITEM_NONE) out[6] = neck; - uint8_t ring = find_best_available(p, GEAR_SLOT_RING, lp->ring.items, lp->ring.len); + uint8_t ring = find_best_available( + p, GEAR_SLOT_RING, priorities->ring.items, priorities->ring.len); if (ring != ITEM_NONE) out[7] = ring; } -static inline int apply_loadout(Player* p, int loadout) { - if (loadout <= LOADOUT_KEEP || loadout > LOADOUT_GMAUL) return 0; - - uint8_t resolved[NUM_DYNAMIC_GEAR_SLOTS]; - resolve_loadout(p, loadout, resolved); - - int changed = 0; - for (int i = 0; i < NUM_DYNAMIC_GEAR_SLOTS; i++) { - int gear_slot = DYNAMIC_GEAR_SLOTS[i]; - changed += slot_equip_item(p, gear_slot, resolved[i]); - } - - return changed; -} - -static inline int is_loadout_active(Player* p, int loadout) { - if (loadout <= LOADOUT_KEEP || loadout > LOADOUT_GMAUL) return 0; - +static inline void pvp_emit_equipment_plan_actions( + int* actions, + const Player* p, + PvpEquipmentPlan plan +) { uint8_t resolved[NUM_DYNAMIC_GEAR_SLOTS]; - resolve_loadout(p, loadout, resolved); - + pvp_resolve_equipment_plan(p, plan, resolved); for (int i = 0; i < NUM_DYNAMIC_GEAR_SLOTS; i++) { int gear_slot = DYNAMIC_GEAR_SLOTS[i]; - if (p->equipped[gear_slot] != resolved[i]) return 0; + if (resolved[i] == ITEM_NONE || resolved[i] == p->equipped[gear_slot]) + continue; + int cell = pvp_inventory_cell_with_item(p, resolved[i]); + if (cell >= 0) actions[OSRS_HEAD_EQUIP_SLOT(gear_slot)] = cell + 1; } - return 1; } -static inline int get_current_loadout(Player* p) { - for (int l = 1; l <= LOADOUT_GMAUL; l++) { - if (is_loadout_active(p, l)) return l; - } - return 0; +static inline void pvp_apply_equipment_plan( + Player* p, + PvpEquipmentPlan plan +) { + int actions[OSRS_BASE_NUM_ACTION_HEADS] = {0}; + pvp_emit_equipment_plan_actions(actions, p, plan); + OsrsInventoryClickActions clicks = {0}; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + clicks.equip_by_slot[slot] = actions[OSRS_HEAD_EQUIP_SLOT(slot)]; + OsrsInventoryTickIntent intent = osrs_resolve_inventory_tick_intent( + p, p->inventory_cells, &clicks); + OsrsInventoryApplyStep step; + while (osrs_inventory_intent_next(&intent, &step)) + if (step.kind == OSRS_INVENTORY_APPLY_EQUIP) + (void)osrs_equip_from_cell(p, p->inventory_cells, step.cell_idx); } static inline AttackStyle get_slot_weapon_attack_style(Player* p) { @@ -391,8 +423,8 @@ static inline AttackStyle get_slot_weapon_attack_style(Player* p) { } static inline void init_slot_equipment_lms(Player* p) { - memset(p->inventory, ITEM_NONE, sizeof(p->inventory)); - memset(p->num_items_in_slot, 0, sizeof(p->num_items_in_slot)); + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) + p->inventory_cells[cell] = osrs_inventory_cell_empty(); p->equipped[GEAR_SLOT_HEAD] = ITEM_HELM_NEITIZNOT; p->equipped[GEAR_SLOT_CAPE] = ITEM_GOD_CAPE; @@ -407,60 +439,32 @@ static inline void init_slot_equipment_lms(Player* p) { p->equipped[GEAR_SLOT_RING] = ITEM_BERSERKER_RING; update_spec_weapons_for_weapon(p, p->equipped[GEAR_SLOT_WEAPON]); - p->inventory[GEAR_SLOT_HEAD][0] = ITEM_HELM_NEITIZNOT; - p->num_items_in_slot[GEAR_SLOT_HEAD] = 1; - - p->inventory[GEAR_SLOT_CAPE][0] = ITEM_GOD_CAPE; - p->num_items_in_slot[GEAR_SLOT_CAPE] = 1; - - p->inventory[GEAR_SLOT_NECK][0] = ITEM_GLORY; - p->num_items_in_slot[GEAR_SLOT_NECK] = 1; - - p->inventory[GEAR_SLOT_AMMO][0] = ITEM_DIAMOND_BOLTS_E; - p->num_items_in_slot[GEAR_SLOT_AMMO] = 1; - - p->inventory[GEAR_SLOT_WEAPON][0] = ITEM_WHIP; - p->inventory[GEAR_SLOT_WEAPON][1] = ITEM_RUNE_CROSSBOW; - p->inventory[GEAR_SLOT_WEAPON][2] = ITEM_AHRIM_STAFF; - p->inventory[GEAR_SLOT_WEAPON][3] = ITEM_DRAGON_DAGGER; - p->num_items_in_slot[GEAR_SLOT_WEAPON] = 4; - - p->inventory[GEAR_SLOT_SHIELD][0] = ITEM_DRAGON_DEFENDER; - p->inventory[GEAR_SLOT_SHIELD][1] = ITEM_SPIRIT_SHIELD; - p->num_items_in_slot[GEAR_SLOT_SHIELD] = 2; - - p->inventory[GEAR_SLOT_BODY][0] = ITEM_BLACK_DHIDE_BODY; - p->inventory[GEAR_SLOT_BODY][1] = ITEM_MYSTIC_TOP; - p->num_items_in_slot[GEAR_SLOT_BODY] = 2; - - p->inventory[GEAR_SLOT_LEGS][0] = ITEM_RUNE_PLATELEGS; - p->inventory[GEAR_SLOT_LEGS][1] = ITEM_MYSTIC_BOTTOM; - p->num_items_in_slot[GEAR_SLOT_LEGS] = 2; - - p->inventory[GEAR_SLOT_HANDS][0] = ITEM_BARROWS_GLOVES; - p->num_items_in_slot[GEAR_SLOT_HANDS] = 1; - - p->inventory[GEAR_SLOT_FEET][0] = ITEM_CLIMBING_BOOTS; - p->num_items_in_slot[GEAR_SLOT_FEET] = 1; - - p->inventory[GEAR_SLOT_RING][0] = ITEM_BERSERKER_RING; - p->num_items_in_slot[GEAR_SLOT_RING] = 1; + static const uint8_t BASE_SWITCH_ITEMS[] = { + ITEM_RUNE_CROSSBOW, + ITEM_AHRIM_STAFF, + ITEM_DRAGON_DAGGER, + ITEM_SPIRIT_SHIELD, + ITEM_MYSTIC_TOP, + ITEM_MYSTIC_BOTTOM, + }; + for (int i = 0; i < (int)(sizeof(BASE_SWITCH_ITEMS) / sizeof(BASE_SWITCH_ITEMS[0])); i++) + p->inventory_cells[i] = + osrs_inventory_cell_from_item(BASE_SWITCH_ITEMS[i]); osrs_refresh_player_equipment(p); p->current_gear = GEAR_MELEE; + p->visible_gear = GEAR_MELEE; } static inline int add_item_to_inventory(Player* p, int gear_slot, uint8_t item_idx) { if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) return 0; - if (p->num_items_in_slot[gear_slot] >= MAX_ITEMS_PER_SLOT) return 0; - - for (int i = 0; i < p->num_items_in_slot[gear_slot]; i++) { - if (p->inventory[gear_slot][i] == item_idx) return 0; + if (player_has_item_in_slot(p, gear_slot, item_idx)) return 0; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + if (!osrs_inventory_cell_is_empty(&p->inventory_cells[cell])) continue; + p->inventory_cells[cell] = osrs_inventory_cell_from_item(item_idx); + return 1; } - - p->inventory[gear_slot][p->num_items_in_slot[gear_slot]] = item_idx; - p->num_items_in_slot[gear_slot]++; - return 1; + return 0; } static const uint8_t UPGRADE_REPLACES[NUM_ITEMS] = { @@ -527,17 +531,11 @@ static const uint8_t UPGRADE_REPLACES[NUM_ITEMS] = { }; static inline int remove_item_from_inventory(Player* p, int gear_slot, uint8_t item_idx) { - for (int i = 0; i < p->num_items_in_slot[gear_slot]; i++) { - if (p->inventory[gear_slot][i] == item_idx) { - for (int j = i; j < p->num_items_in_slot[gear_slot] - 1; j++) { - p->inventory[gear_slot][j] = p->inventory[gear_slot][j + 1]; - } - p->num_items_in_slot[gear_slot]--; - p->inventory[gear_slot][p->num_items_in_slot[gear_slot]] = ITEM_NONE; - return 1; - } - } - return 0; + if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) return 0; + int cell = pvp_inventory_cell_with_item(p, item_idx); + if (cell < 0) return 0; + p->inventory_cells[cell] = osrs_inventory_cell_empty(); + return 1; } static inline int item_to_gear_slot(uint8_t item_idx) { @@ -646,17 +644,16 @@ static inline void add_loot_item(Player* p, uint8_t item_idx) { /* 4 brew + 2 restore + 1 combat + 1 ranged + 2 karambwan + 1 rune pouch */ #define FIXED_INVENTORY_SLOTS 11 -static inline int count_switch_items(Player* p) { +static inline int count_switch_items(const Player* p) { int switches = 0; - for (int s = 0; s < NUM_GEAR_SLOTS; s++) { - if (p->num_items_in_slot[s] > 1) { - switches += p->num_items_in_slot[s] - 1; - } + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + if (osrs_inventory_cell_item_index(&p->inventory_cells[cell]) != ITEM_NONE) + switches++; } return switches; } -static inline int compute_food_count(Player* p) { +static inline int compute_food_count(const Player* p) { int switches = count_switch_items(p); int food = 28 - FIXED_INVENTORY_SLOTS - switches; return food > 1 ? food : 1; @@ -718,25 +715,26 @@ static inline void init_player_gear_randomized(Player* p, int tier, uint32_t* rn #undef ADD_RANDOM_LOOT - if (tier >= 3 && player_has_item_in_slot(p, GEAR_SLOT_SHIELD, ITEM_DRAGON_DEFENDER)) { + if (tier >= 3 && + player_has_item_in_slot(p, GEAR_SLOT_SHIELD, ITEM_DRAGON_DEFENDER)) { int has_1h_melee = 0; - for (int i = 0; i < p->num_items_in_slot[GEAR_SLOT_WEAPON]; i++) { - uint8_t w = p->inventory[GEAR_SLOT_WEAPON][i]; - if (get_item_attack_style(w) == ATTACK_STYLE_MELEE && !item_is_two_handed(w)) { + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + uint8_t weapon = + osrs_inventory_cell_item_index(&p->inventory_cells[cell]); + if (weapon != ITEM_NONE && + osrs_item_gear_slot(weapon) == GEAR_SLOT_WEAPON && + get_item_attack_style(weapon) == ATTACK_STYLE_MELEE && + !item_is_two_handed(weapon)) { has_1h_melee = 1; break; } } - if (!has_1h_melee) { - remove_item_from_inventory(p, GEAR_SLOT_SHIELD, ITEM_DRAGON_DEFENDER); - } + if (!has_1h_melee) + remove_item_from_inventory( + p, GEAR_SLOT_SHIELD, ITEM_DRAGON_DEFENDER); } - uint8_t resolved[NUM_DYNAMIC_GEAR_SLOTS]; - resolve_loadout(p, LOADOUT_MELEE, resolved); - for (int i = 0; i < NUM_DYNAMIC_GEAR_SLOTS; i++) { - slot_equip_item(p, DYNAMIC_GEAR_SLOTS[i], resolved[i]); - } + pvp_apply_equipment_plan(p, PVP_EQUIPMENT_MELEE); osrs_refresh_player_equipment(p); p->current_gear = GEAR_MELEE; diff --git a/ocean/osrs/osrs_pvp_movement.h b/ocean/osrs/osrs_pvp_movement.h index 7651dd341b..d6ae13a5b2 100644 --- a/ocean/osrs/osrs_pvp_movement.h +++ b/ocean/osrs/osrs_pvp_movement.h @@ -141,12 +141,25 @@ static int select_farcast_tile( } + typedef struct { EncounterArenaTopology* topology; + OsrsLocalMoveRoute + local_move_routes[FIGHT_AREA_WIDTH * FIGHT_AREA_HEIGHT] + [OSRS_PRIMARY_MOVE_ACTIONS]; + int local_move_routes_ready; } PvpRouteTopologyOwner; static PvpRouteTopologyOwner pvp_route_topology_owner; +static const uint8_t PVP_MOVE_ACTION_BY_DELTA[25] = { + 9, 10, 11, 12, 13, + 14, 1, 2, 3, 15, + 16, 4, 0, 5, 17, + 18, 6, 7, 8, 19, + 20, 21, 22, 23, 24, +}; + static uint32_t pvp_route_topology_flags(void* data, int x, int y) { const CollisionMap* collision_map = (const CollisionMap*)data; if (!is_in_wilderness(x, y)) return COLLISION_BLOCKED | LOS_FULL_MASK; @@ -155,6 +168,90 @@ static uint32_t pvp_route_topology_flags(void* data, int x, int y) { : 0; } +static void pvp_local_move_routes_build(PvpRouteTopologyOwner* owner) { + const EncounterArenaTopology* topology = owner->topology; + for (int x = topology->origin_x; + x < topology->origin_x + topology->width; + x++) { + for (int y = topology->origin_y; + y < topology->origin_y + topology->height; + y++) { + int source_index = + encounter_arena_topology_index_raw(topology, x, y); + for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + EncounterRouteInput input = { + .topology = topology, + .source_x = x, + .source_y = y, + .actor_size = 1, + .target_x = x + ENCOUNTER_MOVE_TARGET_DX[action], + .target_y = y + ENCOUNTER_MOVE_TARGET_DY[action], + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + }; + EncounterRouteResult route = encounter_route_solve(&input); + owner->local_move_routes[source_index][action] = + (OsrsLocalMoveRoute){ + .destination_dx = + (int16_t)(route.destination_x - x), + .destination_dy = + (int16_t)(route.destination_y - y), + .first_dx = (int8_t)route.first_dx, + .first_dy = (int8_t)route.first_dy, + .run_dx = (int8_t)route.run_dx, + .run_dy = (int8_t)route.run_dy, + .distance = route.distance, + .outcome = (uint8_t)route.outcome, + }; + } + } + } + owner->local_move_routes_ready = 1; +} + +static int pvp_local_move_route_lookup( + const void* data, + const EncounterRouteInput* input, + EncounterRouteResult* result +) { + const PvpRouteTopologyOwner* owner = + (const PvpRouteTopologyOwner*)data; + if (!owner || !input || !result) abort(); + if (!owner->local_move_routes_ready || + input->topology != owner->topology || + input->blockers.is_blocked || + input->actor_size != 1 || + input->target_size != 1 || + input->target_kind != ENCOUNTER_ROUTE_TARGET_TILE || + input->movement_mode != ENCOUNTER_ROUTE_MOVEMENT_RUN || + input->cost_policy != ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS || + !encounter_arena_topology_contains( + input->topology, input->source_x, input->source_y)) + return 0; + int dx = input->target_x - input->source_x; + int dy = input->target_y - input->source_y; + if (dx < -2 || dx > 2 || dy < -2 || dy > 2) return 0; + int action = PVP_MOVE_ACTION_BY_DELTA[(dx + 2) * 5 + dy + 2]; + if (action == 0) return 0; + int source_index = encounter_arena_topology_index_raw( + input->topology, input->source_x, input->source_y); + const OsrsLocalMoveRoute* route = + &owner->local_move_routes[source_index][action]; + *result = (EncounterRouteResult){ + .outcome = (EncounterRouteOutcome)route->outcome, + .destination_x = input->source_x + route->destination_dx, + .destination_y = input->source_y + route->destination_dy, + .first_dx = route->first_dx, + .first_dy = route->first_dy, + .run_dx = route->run_dx, + .run_dy = route->run_dy, + .distance = route->distance, + }; + return 1; +} + static inline int pvp_topology_tile_walkable( const EncounterArenaTopology* topology, @@ -182,6 +279,7 @@ static const EncounterArenaTopology* pvp_route_topology_finalize( pvp_route_topology_owner.topology = encounter_arena_topology_build(&spec); encounter_arena_topology_finalize(pvp_route_topology_owner.topology); + pvp_local_move_routes_build(&pvp_route_topology_owner); } else { encounter_arena_topology_require_spec( pvp_route_topology_owner.topology, @@ -268,6 +366,35 @@ static inline OsrsPlayerStepResult pvp_step_player_movement( if (*dest_x < 0 || *dest_y < 0) return result; Player* p = &env->players[agent_idx]; + if (p->frozen_ticks <= 0) { + osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_MOVE); + if (p->x == *dest_x && p->y == *dest_y) { + *dest_x = -1; + *dest_y = -1; + p->is_moving = 0; + return result; + } + EncounterRouteInput route_input = { + .topology = topology, + .source_x = p->x, + .source_y = p->y, + .actor_size = 1, + .target_x = *dest_x, + .target_y = *dest_y, + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + }; + EncounterRouteResult route; + if (pvp_local_move_route_lookup( + &pvp_route_topology_owner, &route_input, &route)) { + result.moved = osrs_player_step_apply_route(p, &route) > 0; + result.explicit_moved = result.moved; + p->is_moving = 1; + return result; + } + } OsrsEncounterArena arena = pvp_build_arena(topology); OsrsPlayerStepInput input = { .player = p, @@ -383,7 +510,7 @@ static inline int pvp_step_player_ranged_chase( static inline void pvp_set_walk_dest_from_head_move(OsrsEnv* env, int agent_idx, int move_action) { Player* p = &env->players[agent_idx]; - if (move_action <= 0 || move_action >= MOVE_DIM) return; + if (move_action <= 0 || move_action >= OSRS_PRIMARY_MOVE_ACTIONS) return; env->pvp_runtime.walk_dest_x[agent_idx] = p->x + ENCOUNTER_MOVE_TARGET_DX[move_action]; env->pvp_runtime.walk_dest_y[agent_idx] = p->y + ENCOUNTER_MOVE_TARGET_DY[move_action]; } diff --git a/ocean/osrs/osrs_pvp_observations.h b/ocean/osrs/osrs_pvp_observations.h index dfeca97a91..6b5a93d47b 100644 --- a/ocean/osrs/osrs_pvp_observations.h +++ b/ocean/osrs/osrs_pvp_observations.h @@ -2,42 +2,31 @@ #define OSRS_PVP_OBSERVATIONS_H #include -#include "osrs_types.h" + +#include "osrs_policy.h" #include "osrs_player_consumables.h" -#include "osrs_pvp_gear.h" #include "osrs_pvp_combat.h" -#include "osrs_encounter.h" +#include "osrs_pvp_gear.h" #include "osrs_pvp_movement.h" -static inline float get_relative_level_combat(int current, int base) { - int max_level = base + osrs_super_combat_boost_amount(base); - return (float)current / (float)max_level; -} - -static inline float get_relative_level_ranged(int current, int base) { - int max_level = base + osrs_ranging_boost_amount(base); - return (float)current / (float)max_level; -} - -static inline float get_relative_level_magic(int current, int base) { - return (float)current / (float)base; -} +#define NH_PVP_SPECIFIC_OBS_SIZE 32 +#define NH_PVP_NUM_OBS (OSRS_SHARED_OBS_SIZE + NH_PVP_SPECIFIC_OBS_SIZE) +#define NH_PVP_TARGET_SLOTS 1 +#define NH_PVP_ACTION_MASK_SIZE OSRS_BASE_ACTION_MASK_SIZE(NH_PVP_TARGET_SLOTS) static inline int can_use_brew_boost(Player* p) { int def_boost = osrs_brew_defence_boost_amount(p->base_defence); int def_cap = p->is_lms ? p->base_defence : p->base_defence + def_boost; - if (p->current_defence < def_cap - 1) { - return 1; - } - return p->current_hitpoints <= p->base_hitpoints; + return p->current_defence < def_cap - 1 || + p->current_hitpoints <= p->base_hitpoints; } static inline int can_restore_stats(Player* p) { int stats_drained = p->current_attack < p->base_attack || - p->current_defence < p->base_defence || - p->current_strength < p->base_strength || - p->current_ranged < p->base_ranged || - p->current_magic < p->base_magic; + p->current_defence < p->base_defence || + p->current_strength < p->base_strength || + p->current_ranged < p->base_ranged || + p->current_magic < p->base_magic; int prayer_low = p->current_prayer < (int)(p->base_prayer * 0.9f); return stats_drained || prayer_low; } @@ -48,8 +37,8 @@ static inline int can_boost_combat_skills(Player* p) { int def_boost = osrs_super_combat_boost_amount(p->base_defence); int max_def = p->is_lms ? p->base_defence : p->base_defence + def_boost; return max_att > p->current_attack + 1 || - max_def > p->current_defence + 1 || - max_str > p->current_strength + 1; + max_def > p->current_defence + 1 || + max_str > p->current_strength + 1; } static inline int can_boost_ranged(Player* p) { @@ -58,14 +47,12 @@ static inline int can_boost_ranged(Player* p) { } static inline int can_use_potion(Player* p, int potion_type) { - if (remaining_ticks(p->potion_timer) > 0) { - return 0; - } + if (remaining_ticks(p->potion_timer) > 0) return 0; switch (potion_type) { - case 1: return p->brew_doses > 0; - case 2: return p->restore_doses > 0; - case 3: return p->combat_potion_doses > 0; - case 4: return p->ranged_potion_doses > 0; + case POTION_BREW: return p->brew_doses > 0; + case POTION_RESTORE: return p->restore_doses > 0; + case POTION_COMBAT: return p->combat_potion_doses > 0; + case POTION_RANGED: return p->ranged_potion_doses > 0; default: return 0; } } @@ -78,558 +65,310 @@ static inline int can_eat_karambwan(Player* p) { return osrs_player_can_eat_food_type(p, FOOD_KARAMBWAN); } -static inline int can_move_adjacent( +static inline int pvp_drink_kind_available( Player* p, - const EncounterArenaTopology* topology + OsrsConsumableKind kind ) { - int dest_x = 0; - int dest_y = 0; - if (!select_closest_adjacent_tile( - p, - p->last_obs_target_x, - p->last_obs_target_y, - &dest_x, - &dest_y, - topology)) - return 0; - return !(dest_x == p->x && dest_y == p->y); -} - -static inline int can_move_under(Player* p, Player* target) { - int dist = chebyshev_distance(p->x, p->y, p->last_obs_target_x, p->last_obs_target_y); - return remaining_ticks(target->frozen_ticks) > 0 && dist != 0; + switch (kind) { + case OSRS_CONSUMABLE_BREW: + return can_use_potion(p, POTION_BREW) && can_use_brew_boost(p); + case OSRS_CONSUMABLE_SUPER_RESTORE: + return can_use_potion(p, POTION_RESTORE) && can_restore_stats(p); + case OSRS_CONSUMABLE_SUPER_COMBAT: + return can_use_potion(p, POTION_COMBAT) && can_boost_combat_skills(p); + case OSRS_CONSUMABLE_RANGING: + return can_use_potion(p, POTION_RANGED) && can_boost_ranged(p); + default: + return 0; + } } -static inline int can_move_to_farcast( +static inline void pvp_shared_observation_input( Player* p, - int distance, - const EncounterArenaTopology* topology + OsrsSharedObservationInput* out ) { - int dest_x = 0; - int dest_y = 0; - if (!select_farcast_tile( - p, - p->last_obs_target_x, - p->last_obs_target_y, - distance, - &dest_x, - &dest_y, - topology)) - return 0; - return !(dest_x == p->x && dest_y == p->y); + AttackStyle style = get_slot_weapon_attack_style(p); + GearBonuses* gear = get_slot_gear_bonuses(p); + int spell_base_damage = style == ATTACK_STYLE_MAGIC + ? get_ice_base_hit(p->current_magic) : 0; + *out = (OsrsSharedObservationInput){ + .player = p, + .interaction = &p->interaction, + .arena_min_x = FIGHT_AREA_BASE_X, + .arena_max_x = FIGHT_AREA_BASE_X + FIGHT_AREA_WIDTH, + .arena_min_y = FIGHT_AREA_BASE_Y, + .arena_max_y = FIGHT_AREA_BASE_Y + FIGHT_AREA_HEIGHT, + .attack_style = style, + .attack_range = get_attack_range(p, style), + .max_hit = calculate_max_hit(p, style, 1.0f, spell_base_damage), + .attack_speed = gear->attack_speed, + .defence_stab = gear->stab_defence, + .defence_slash = gear->slash_defence, + .defence_crush = gear->crush_defence, + .defence_magic = gear->magic_defence, + .defence_ranged = gear->ranged_defence, + .effective_level = calculate_effective_attack(p, style), + .attack_bonus = get_attack_bonus(p, style), + .strength_bonus = get_strength_bonus(p, style), + .spell_base_damage = spell_base_damage, + .special_attack_cost = osrs_spec_cost(p->equipped[GEAR_SLOT_WEAPON]), + }; } -static inline int can_move_diagonal( - Player* p, - const EncounterArenaTopology* topology +static inline void pvp_write_observations( + float* obs, + OsrsEnv* env, + int agent_idx ) { - int dest_x = 0; - int dest_y = 0; - if (!select_closest_diagonal_tile( - p, - p->last_obs_target_x, - p->last_obs_target_y, - &dest_x, - &dest_y, - topology)) - return 0; - return !(dest_x == p->x && dest_y == p->y); -} - -static void init_obs_norm_divisors(float* d) { - for (int i = 0; i < SLOT_NUM_OBSERVATIONS; i++) d[i] = 1.0f; - - d[4] = 100.0f; d[21] = 100.0f; - d[22] = 10.0f; d[23] = 10.0f; d[24] = 16.0f; d[25] = 20.0f; d[26] = 15.0f; d[27] = 4.0f; - d[29] = 32.0f; d[30] = 32.0f; d[31] = 32.0f; d[32] = 32.0f; - d[39] = 6.0f; d[40] = 3.0f; d[41] = 3.0f; d[42] = 3.0f; d[43] = 4.0f; d[44] = 6.0f; d[45] = 3.0f; - d[46] = 2.0f; - - d[47] = 6.0f; - d[48] = 6.0f; - - d[65] = 2.0f; - - d[60] = 7.0f; - d[61] = 7.0f; - d[62] = 7.0f; - - for (int i = 96; i <= 102; i++) d[i] = 99.0f; - - d[106] = 4.0f; - - for (int i = 119; i <= 132; i++) d[i] = 170.0f; - d[123] = 6.0f; d[124] = 10.0f; - d[139] = 50.0f; - d[140] = 50.0f; -} - -static float OBS_NORM_DIVISORS[SLOT_NUM_OBSERVATIONS]; -static int _obs_norm_initialized = 0; - -static void ensure_obs_norm_initialized(void) { - if (!_obs_norm_initialized) { - init_obs_norm_divisors(OBS_NORM_DIVISORS); - _obs_norm_initialized = 1; - } -} - -static void ocean_write_obs_agent(OsrsEnv* env, float* dst, int agent_idx) { - ensure_obs_norm_initialized(); - float* src = env->observations + agent_idx * SLOT_NUM_OBSERVATIONS; - for (int i = 0; i < SLOT_NUM_OBSERVATIONS; i++) { - dst[i] = src[i] / OBS_NORM_DIVISORS[i]; - } - - unsigned char* mask = env->action_masks + agent_idx * ACTION_MASK_SIZE; - for (int i = 0; i < ACTION_MASK_SIZE; i++) { - dst[SLOT_NUM_OBSERVATIONS + i] = (float)mask[i]; - } -} - -/** Binding-facing output layout: [normalized obs, action mask as float]. */ -static void ocean_write_obs(OsrsEnv* env) { - ocean_write_obs_agent(env, env->ocean_io.agent_obs, 0); -} - -static void ocean_write_obs_p1(OsrsEnv* env) { - ocean_write_obs_agent(env, env->ocean_io.agent_obs_p1, 1); + Player* p = &env->players[agent_idx]; + Player* target = &env->players[1 - agent_idx]; + p->last_obs_target_x = target->x; + p->last_obs_target_y = target->y; + + OsrsSharedObservationInput shared_input; + pvp_shared_observation_input(p, &shared_input); + int i = osrs_write_shared_observations(obs, &shared_input); + if (i != OSRS_SHARED_OBS_SIZE) abort(); + + obs[i++] = osrs_policy_ratio(target->current_hitpoints, target->base_hitpoints); + obs[i++] = osrs_policy_ratio(target->current_prayer, target->base_prayer); + obs[i++] = osrs_policy_ratio(target->x - p->x, FIGHT_AREA_WIDTH); + obs[i++] = osrs_policy_ratio(target->y - p->y, FIGHT_AREA_HEIGHT); + obs[i++] = target->prayer == PRAYER_PROTECT_MELEE ? 1.0f : 0.0f; + obs[i++] = target->prayer == PRAYER_PROTECT_RANGED ? 1.0f : 0.0f; + obs[i++] = target->prayer == PRAYER_PROTECT_MAGIC ? 1.0f : 0.0f; + obs[i++] = target->prayer == PRAYER_SMITE ? 1.0f : 0.0f; + obs[i++] = target->prayer == PRAYER_REDEMPTION ? 1.0f : 0.0f; + obs[i++] = target->offensive_prayer == OFFENSIVE_PRAYER_PIETY ? 1.0f : 0.0f; + obs[i++] = target->offensive_prayer == OFFENSIVE_PRAYER_RIGOUR ? 1.0f : 0.0f; + obs[i++] = target->offensive_prayer == OFFENSIVE_PRAYER_AUGURY ? 1.0f : 0.0f; + obs[i++] = osrs_policy_ratio(target->special_energy, 100); + obs[i++] = osrs_policy_ratio(target->attack_timer, 8); + obs[i++] = osrs_policy_ratio(target->food_timer, 3); + obs[i++] = osrs_policy_ratio(target->potion_timer, 3); + obs[i++] = osrs_policy_ratio(target->frozen_ticks, 32); + obs[i++] = osrs_policy_ratio(target->freeze_immunity_ticks, 5); + obs[i++] = target->veng_active ? 1.0f : 0.0f; + obs[i++] = osrs_policy_ratio(target->veng_cooldown, 50); + obs[i++] = p->observed_target_lunar_spellbook ? 1.0f : 0.0f; + obs[i++] = target->last_attack_style == ATTACK_STYLE_MELEE ? 1.0f : 0.0f; + obs[i++] = target->last_attack_style == ATTACK_STYLE_RANGED ? 1.0f : 0.0f; + obs[i++] = target->last_attack_style == ATTACK_STYLE_MAGIC ? 1.0f : 0.0f; + obs[i++] = target->is_moving ? 1.0f : 0.0f; + int pending_damage = 0; + for (int hit = 0; hit < p->num_pending_hits; hit++) + pending_damage += p->pending_hits[hit].damage; + obs[i++] = osrs_policy_ratio(pending_damage, p->base_hitpoints); + obs[i++] = osrs_policy_ratio(get_ticks_until_next_hit(target), 6); + obs[i++] = agent_idx == env->pid_holder ? 1.0f : 0.0f; + AttackStyle target_style = get_slot_weapon_attack_style(target); + obs[i++] = target_style == ATTACK_STYLE_MELEE ? 1.0f : 0.0f; + obs[i++] = target_style == ATTACK_STYLE_RANGED ? 1.0f : 0.0f; + obs[i++] = target_style == ATTACK_STYLE_MAGIC ? 1.0f : 0.0f; + obs[i++] = osrs_policy_ratio( + chebyshev_distance(p->x, p->y, target->x, target->y), 10); + if (i != NH_PVP_NUM_OBS) abort(); } -static void generate_slot_observations( +static inline void pvp_write_action_mask( + float* mask, OsrsEnv* env, int agent_idx, const EncounterArenaTopology* topology ) { Player* p = &env->players[agent_idx]; - Player* t = &env->players[1 - agent_idx]; - - float* obs = env->observations + agent_idx * SLOT_NUM_OBSERVATIONS; - - p->last_obs_target_x = t->x; - p->last_obs_target_y = t->y; - - obs[0] = (p->visible_gear == GEAR_MELEE) ? 1.0f : 0.0f; - obs[1] = (p->visible_gear == GEAR_RANGED) ? 1.0f : 0.0f; - obs[2] = (p->visible_gear == GEAR_MAGE) ? 1.0f : 0.0f; - obs[3] = (float)p->spec_armed; - obs[4] = (float)p->special_energy; - - obs[5] = (p->prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; - obs[6] = (p->prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; - obs[7] = (p->prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; - obs[8] = (p->prayer == PRAYER_SMITE) ? 1.0f : 0.0f; - obs[9] = (p->prayer == PRAYER_REDEMPTION) ? 1.0f : 0.0f; - - obs[10] = (float)p->current_hitpoints / (float)p->base_hitpoints; - obs[11] = p->last_target_health_percent; - - obs[12] = (t->last_attack_style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; - obs[13] = (t->last_attack_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; - obs[14] = (t->last_attack_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; - obs[15] = (t->last_attack_style == ATTACK_STYLE_NONE) ? 1.0f : 0.0f; - - obs[16] = (t->prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; - obs[17] = (t->prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; - obs[18] = (t->prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; - obs[19] = (t->prayer == PRAYER_SMITE) ? 1.0f : 0.0f; - obs[20] = (t->prayer == PRAYER_REDEMPTION) ? 1.0f : 0.0f; - obs[21] = (float)t->special_energy; - - obs[22] = (float)p->ranged_potion_doses; - obs[23] = (float)p->combat_potion_doses; - obs[24] = (float)p->restore_doses; - obs[25] = (float)p->brew_doses; - obs[26] = (float)p->food_count; - obs[27] = (float)p->karambwan_count; - obs[28] = (float)p->current_prayer / (float)p->base_prayer; - - obs[29] = (float)remaining_ticks(p->frozen_ticks); - obs[30] = (float)remaining_ticks(t->frozen_ticks); - obs[31] = (float)remaining_ticks(p->freeze_immunity_ticks); - obs[32] = (float)remaining_ticks(t->freeze_immunity_ticks); - - obs[33] = is_in_melee_range(p, t) ? 1.0f : 0.0f; - - obs[34] = get_relative_level_combat(p->current_strength, p->base_strength); - obs[35] = get_relative_level_combat(p->current_attack, p->base_attack); - obs[36] = get_relative_level_combat(p->current_defence, p->base_defence); - obs[37] = get_relative_level_ranged(p->current_ranged, p->base_ranged); - obs[38] = get_relative_level_magic(p->current_magic, p->base_magic); - - obs[39] = (float)p->attack_timer; - obs[40] = (float)remaining_ticks(p->food_timer); - obs[41] = (float)remaining_ticks(p->potion_timer); - obs[42] = (float)remaining_ticks(p->karambwan_timer); - - int attack_delay = get_attack_timer_uncapped(p) - 1; - if (attack_delay < -3) attack_delay = -3; - else if (attack_delay > 0) attack_delay = 0; - obs[43] = (float)(attack_delay + 3); - - obs[44] = (float)remaining_ticks(t->attack_timer); - obs[45] = (float)remaining_ticks(t->food_timer); - - int pending_damage = 0; - for (int i = 0; i < p->num_pending_hits; i++) { - pending_damage += p->pending_hits[i].damage; - } - obs[46] = (float)pending_damage / (float)t->base_hitpoints; - - int ticks_until_hit_on_target = get_ticks_until_next_hit(p); - int ticks_until_hit_on_player = get_ticks_until_next_hit(t); - obs[47] = (float)ticks_until_hit_on_target; - obs[48] = (float)ticks_until_hit_on_player; - - obs[49] = p->just_attacked ? 1.0f : 0.0f; - obs[50] = t->just_attacked ? 1.0f : 0.0f; - - obs[51] = p->tick_damage_scale; - obs[52] = p->damage_received_scale; - obs[53] = p->damage_dealt_scale; - - obs[54] = (p->last_attack_style != ATTACK_STYLE_NONE) ? 1.0f : 0.0f; - obs[55] = p->is_moving ? 1.0f : 0.0f; - obs[56] = t->is_moving ? 1.0f : 0.0f; - - obs[57] = (agent_idx == env->pid_holder) ? 1.0f : 0.0f; - - obs[58] = (!p->is_lunar_spellbook && p->current_magic >= 94) ? 1.0f : 0.0f; - obs[59] = (!p->is_lunar_spellbook && p->current_magic >= 92) ? 1.0f : 0.0f; - - int dist = chebyshev_distance(p->x, p->y, t->x, t->y); - int destination_distance = p->is_moving - ? chebyshev_distance(p->dest_x, p->dest_y, t->x, t->y) : dist; - int distance_to_destination = p->is_moving - ? chebyshev_distance(p->x, p->y, p->dest_x, p->dest_y) : 0; - - if (destination_distance > 7) destination_distance = 7; - if (distance_to_destination > 7) distance_to_destination = 7; - if (dist > 7) dist = 7; - - obs[60] = (float)destination_distance; - obs[61] = (float)distance_to_destination; - obs[62] = (float)dist; - - obs[63] = p->player_prayed_correct ? 1.0f : 0.0f; - obs[64] = p->target_prayed_correct ? 1.0f : 0.0f; - - float damage_scale = (p->total_damage_dealt + 1.0f) / (p->total_damage_received + 1.0f); - obs[65] = clampf(damage_scale, 0.5f, 2.0f); - - obs[66] = confidence_scale(p->total_target_hit_count); - obs[67] = ratio_or_zero(p->target_hit_melee_count, p->total_target_hit_count); - obs[68] = ratio_or_zero(p->target_hit_magic_count, p->total_target_hit_count); - obs[69] = ratio_or_zero(p->target_hit_ranged_count, p->total_target_hit_count); - obs[70] = ratio_or_zero(p->player_hit_melee_count, p->total_target_pray_count); - obs[71] = ratio_or_zero(p->player_hit_magic_count, p->total_target_pray_count); - obs[72] = ratio_or_zero(p->player_hit_ranged_count, p->total_target_pray_count); - obs[73] = ratio_or_zero(p->target_hit_correct_count, p->total_target_hit_count); - obs[74] = confidence_scale(p->total_target_pray_count); - obs[75] = ratio_or_zero(p->target_pray_magic_count, p->total_target_pray_count); - obs[76] = ratio_or_zero(p->target_pray_ranged_count, p->total_target_pray_count); - obs[77] = ratio_or_zero(p->target_pray_melee_count, p->total_target_pray_count); - obs[78] = ratio_or_zero(p->player_pray_magic_count, p->total_target_hit_count); - obs[79] = ratio_or_zero(p->player_pray_ranged_count, p->total_target_hit_count); - obs[80] = ratio_or_zero(p->player_pray_melee_count, p->total_target_hit_count); - obs[81] = ratio_or_zero(p->target_pray_correct_count, p->total_target_pray_count); - - int recent_target_hit_melee = 0, recent_target_hit_magic = 0, recent_target_hit_ranged = 0; - int recent_player_hit_melee = 0, recent_player_hit_magic = 0, recent_player_hit_ranged = 0; - int recent_target_pray_magic = 0, recent_target_pray_ranged = 0, recent_target_pray_melee = 0; - int recent_player_pray_magic = 0, recent_player_pray_ranged = 0, recent_player_pray_melee = 0; - int recent_target_hit_correct = 0, recent_target_pray_correct = 0; - - for (int i = 0; i < HISTORY_SIZE; i++) { - if (p->recent_target_attack_styles[i] == ATTACK_STYLE_MELEE) recent_target_hit_melee++; - else if (p->recent_target_attack_styles[i] == ATTACK_STYLE_MAGIC) recent_target_hit_magic++; - else if (p->recent_target_attack_styles[i] == ATTACK_STYLE_RANGED) recent_target_hit_ranged++; - - if (p->recent_player_attack_styles[i] == ATTACK_STYLE_MELEE) recent_player_hit_melee++; - else if (p->recent_player_attack_styles[i] == ATTACK_STYLE_MAGIC) recent_player_hit_magic++; - else if (p->recent_player_attack_styles[i] == ATTACK_STYLE_RANGED) recent_player_hit_ranged++; - - if (p->recent_target_prayer_styles[i] == ATTACK_STYLE_MAGIC) recent_target_pray_magic++; - else if (p->recent_target_prayer_styles[i] == ATTACK_STYLE_RANGED) recent_target_pray_ranged++; - else if (p->recent_target_prayer_styles[i] == ATTACK_STYLE_MELEE) recent_target_pray_melee++; - - if (p->recent_player_prayer_styles[i] == ATTACK_STYLE_MAGIC) recent_player_pray_magic++; - else if (p->recent_player_prayer_styles[i] == ATTACK_STYLE_RANGED) recent_player_pray_ranged++; - else if (p->recent_player_prayer_styles[i] == ATTACK_STYLE_MELEE) recent_player_pray_melee++; - - if (p->recent_target_hit_correct[i]) recent_target_hit_correct++; - if (p->recent_target_prayer_correct[i]) recent_target_pray_correct++; + Player* target = &env->players[1 - agent_idx]; + + int offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_PRIMARY); + mask[offset] = 1.0f; + for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + int nx = p->x + ENCOUNTER_MOVE_TARGET_DX[action]; + int ny = p->y + ENCOUNTER_MOVE_TARGET_DY[action]; + mask[offset + action] = can_move(p) && + pvp_topology_tile_walkable(topology, nx, ny) ? 1.0f : 0.0f; } + mask[offset + OSRS_PRIMARY_MOVE_ACTIONS] = + target->current_hitpoints > 0 ? 1.0f : 0.0f; - obs[82] = (float)recent_target_hit_melee / (float)HISTORY_SIZE; - obs[83] = (float)recent_target_hit_magic / (float)HISTORY_SIZE; - obs[84] = (float)recent_target_hit_ranged / (float)HISTORY_SIZE; - obs[85] = (float)recent_player_hit_melee / (float)HISTORY_SIZE; - obs[86] = (float)recent_player_hit_magic / (float)HISTORY_SIZE; - obs[87] = (float)recent_player_hit_ranged / (float)HISTORY_SIZE; - obs[88] = (float)recent_target_hit_correct / (float)HISTORY_SIZE; - obs[89] = (float)recent_target_pray_magic / (float)HISTORY_SIZE; - obs[90] = (float)recent_target_pray_ranged / (float)HISTORY_SIZE; - obs[91] = (float)recent_target_pray_melee / (float)HISTORY_SIZE; - obs[92] = (float)recent_player_pray_magic / (float)HISTORY_SIZE; - obs[93] = (float)recent_player_pray_ranged / (float)HISTORY_SIZE; - obs[94] = (float)recent_player_pray_melee / (float)HISTORY_SIZE; - obs[95] = (float)recent_target_pray_correct / (float)HISTORY_SIZE; - - obs[96] = (float)p->base_attack; - obs[97] = (float)p->base_strength; - obs[98] = (float)p->base_defence; - obs[99] = (float)p->base_ranged; - obs[100] = (float)p->base_magic; - obs[101] = (float)p->base_prayer; - obs[102] = (float)p->base_hitpoints; - - int melee_spec_cost = get_melee_spec_cost(p->melee_spec_weapon); - obs[103] = (p->melee_spec_weapon == MELEE_SPEC_NONE) ? 0.5f : (float)melee_spec_cost / 100.0f; - obs[104] = get_melee_spec_str_mult(p->melee_spec_weapon); - obs[105] = get_melee_spec_acc_mult(p->melee_spec_weapon); - - int melee_hit_count = (p->melee_spec_weapon == MELEE_SPEC_DRAGON_CLAWS) ? 4 : - (p->melee_spec_weapon == MELEE_SPEC_DRAGON_DAGGER || - p->melee_spec_weapon == MELEE_SPEC_ABYSSAL_DAGGER) ? 2 : 1; - obs[106] = (float)melee_hit_count; - obs[107] = (p->melee_spec_weapon == MELEE_SPEC_VOIDWAKER) ? 1.0f : 0.0f; - obs[108] = (p->melee_spec_weapon == MELEE_SPEC_DWH || - p->melee_spec_weapon == MELEE_SPEC_BGS) ? 1.0f : 0.0f; - obs[109] = (p->melee_spec_weapon == MELEE_SPEC_GRANITE_MAUL) ? 1.0f : 0.0f; - - int ranged_spec_cost = get_ranged_spec_cost(p->ranged_spec_weapon); - obs[110] = (p->ranged_spec_weapon == RANGED_SPEC_NONE) ? 0.5f : (float)ranged_spec_cost / 100.0f; - obs[111] = get_ranged_spec_str_mult(p->ranged_spec_weapon); - obs[112] = get_ranged_spec_acc_mult(p->ranged_spec_weapon); - obs[113] = p->bolt_proc_damage; - obs[114] = p->bolt_ignores_defense ? 1.0f : 0.0f; - - obs[115] = (p->magic_spec_weapon != MAGIC_SPEC_NONE) ? 1.0f : 0.0f; - obs[116] = (p->ranged_spec_weapon != RANGED_SPEC_NONE) ? 1.0f : 0.0f; - obs[117] = p->has_blood_fury ? 1.0f : 0.0f; - osrs_ensure_player_equipment(p); - obs[118] = (p->equipment_effect_profile.dharok_piece_count >= 4) ? 1.0f : 0.0f; - - GearBonuses* slot_bonuses = get_slot_gear_bonuses(p); - obs[119] = (float)slot_bonuses->magic_attack; - obs[120] = (float)slot_bonuses->magic_strength; - obs[121] = (float)slot_bonuses->ranged_attack; - obs[122] = (float)slot_bonuses->ranged_strength; - obs[123] = (float)slot_bonuses->attack_speed; - obs[124] = (float)slot_bonuses->attack_range; - obs[125] = (float)slot_bonuses->slash_attack; - obs[126] = (float)slot_bonuses->melee_strength; - obs[127] = (float)slot_bonuses->ranged_defence; - obs[128] = (float)slot_bonuses->magic_defence; - obs[129] = (float)slot_bonuses->slash_defence; - - GearBonuses* target_bonuses = get_slot_gear_bonuses(t); - obs[130] = (float)target_bonuses->ranged_defence; - obs[131] = (float)target_bonuses->magic_defence; - obs[132] = (float)target_bonuses->slash_defence; - - obs[133] = env->is_lms ? 1.0f : 0.0f; - obs[134] = env->pvp_runtime.is_pvp_arena ? 1.0f : 0.0f; - obs[135] = p->veng_active ? 1.0f : 0.0f; - obs[136] = t->veng_active ? 1.0f : 0.0f; - obs[137] = p->is_lunar_spellbook ? 1.0f : 0.0f; - obs[138] = p->observed_target_lunar_spellbook ? 1.0f : 0.0f; - obs[139] = (float)remaining_ticks(p->veng_cooldown); - obs[140] = (float)remaining_ticks(t->veng_cooldown); - obs[141] = is_blood_attack_available(p) ? 1.0f : 0.0f; - obs[142] = is_ice_attack_available(p) ? 1.0f : 0.0f; - obs[143] = can_toggle_spec(p) ? 1.0f : 0.0f; - obs[144] = is_ranged_attack_available(p) ? 1.0f : 0.0f; - obs[145] = is_ranged_spec_attack_available(p) ? 1.0f : 0.0f; - obs[146] = is_melee_attack_available(p, t) ? 1.0f : 0.0f; - obs[147] = is_melee_spec_attack_available(p, t) ? 1.0f : 0.0f; - obs[148] = (p->brew_doses > 0) ? 0.8f : 0.0f; - - obs[149] = (p->attack_timer <= 0) ? 1.0f : 0.0f; - - obs[150] = (float)p->equipped[GEAR_SLOT_WEAPON] / 63.0f; - obs[151] = (p->attack_style_this_tick == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; - obs[152] = (p->attack_style_this_tick == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; - obs[153] = (p->attack_style_this_tick == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; - - AttackStyle target_style = get_slot_weapon_attack_style(t); - obs[154] = (target_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; - obs[155] = (target_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; - obs[156] = (target_style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; - - obs[157] = (p->offensive_prayer == OFFENSIVE_PRAYER_PIETY) ? 1.0f : 0.0f; - obs[158] = (p->offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) ? 1.0f : 0.0f; - obs[159] = (p->offensive_prayer == OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_OVERHEAD); + int has_prayer = p->current_prayer > 0; + mask[offset + ENCOUNTER_OVERHEAD_NO_CHANGE] = 1.0f; + mask[offset + ENCOUNTER_OVERHEAD_OFF] = p->prayer != PRAYER_NONE; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE] = has_prayer; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED] = has_prayer; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC] = has_prayer; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE] = has_prayer && !env->is_lms; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION] = has_prayer && !env->is_lms; + int inventory_has_empty_cell = + osrs_first_empty_inventory_cell(p->inventory_cells, -1) >= 0; + int cell_equip_slot[OSRS_INVENTORY_SIZE]; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&p->inventory_cells[cell]); + cell_equip_slot[cell] = + osrs_can_equip_metadata( + p, metadata, inventory_has_empty_cell) + ? metadata->gear_slot : -1; + } for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - obs[160 + slot] = (float)p->equipped[slot] / 63.0f; + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_EQUIP_SLOT(slot)); + mask[offset] = 1.0f; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) + mask[offset + cell + 1] = + cell_equip_slot[cell] == slot ? 1.0f : 0.0f; } - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - obs[171 + slot] = (float)t->equipped[slot] / 63.0f; + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_EAT); + mask[offset] = 1.0f; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_classify(&p->inventory_cells[cell]); + mask[offset + cell + 1] = resolution.click_action == OSRS_CLICK_EAT && + osrs_can_eat_consumable_kind(p, resolution.consumable_kind) + ? 1.0f : 0.0f; } - uint8_t best_mspec = find_best_melee_spec(p); - obs[182] = (best_mspec == ITEM_VOIDWAKER) ? 1.0f : 0.0f; - - obs[183] = p->used_special_this_tick ? 1.0f : 0.0f; - obs[184] = p->ate_food_this_tick ? 1.0f : 0.0f; - obs[185] = p->ate_karambwan_this_tick ? 1.0f : 0.0f; - AttackStyle current_weapon_style = get_slot_weapon_attack_style(p); - obs[186] = (current_weapon_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; - obs[187] = (current_weapon_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; - obs[188] = (current_weapon_style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; - obs[189] = p->ate_brew_this_tick ? 1.0f : 0.0f; - - float wild_w = (float)(WILD_MAX_X - WILD_MIN_X); - float wild_h = (float)(WILD_MAX_Y - WILD_MIN_Y); - obs[190] = (float)(p->x - WILD_MIN_X) / wild_w; - obs[191] = (float)(WILD_MAX_X - p->x) / wild_w; - obs[192] = (float)(p->y - WILD_MIN_Y) / wild_h; - obs[193] = (float)(WILD_MAX_Y - p->y) / wild_h; - float scale = (wild_w > wild_h ? wild_h : wild_w) * 0.5f; - if (scale < 1.0f) scale = 1.0f; - obs[194] = clampf((float)(t->x - p->x) / scale, -1.0f, 1.0f); - obs[195] = clampf((float)(t->y - p->y) / scale, -1.0f, 1.0f); - for (int m = 0; m < MOVE_DIM; m++) { - if (m == 0) { - obs[196 + m] = 1.0f; - continue; - } - int nx = p->x + ENCOUNTER_MOVE_TARGET_DX[m]; - int ny = p->y + ENCOUNTER_MOVE_TARGET_DY[m]; - obs[196 + m] = - pvp_topology_tile_walkable(topology, nx, ny) ? 1.0f : 0.0f; + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_DRINK); + mask[offset] = 1.0f; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_classify(&p->inventory_cells[cell]); + mask[offset + cell + 1] = resolution.click_action == OSRS_CLICK_DRINK && + pvp_drink_kind_available(p, resolution.consumable_kind) + ? 1.0f : 0.0f; } + + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_SPELL); + mask[offset + OSRS_SPELL_NONE] = 1.0f; + mask[offset + OSRS_SPELL_BLOOD_BARRAGE] = can_cast_blood_spell(p); + mask[offset + OSRS_SPELL_ICE_BARRAGE] = can_cast_ice_spell(p); + mask[offset + OSRS_SPELL_VENGEANCE] = !env->is_lms && + p->is_lunar_spellbook && !p->veng_active && + remaining_ticks(p->veng_cooldown) == 0 && p->current_magic >= 94; + mask[offset + OSRS_SPELL_DEATH_CHARGE] = 0.0f; + + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_SPECIAL); + mask[offset] = 1.0f; + mask[offset + 1] = can_toggle_spec(p); + mask[offset + 2] = p->spec_armed; + + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_OFFENSIVE); + mask[offset + ENCOUNTER_OFFENSIVE_NO_CHANGE] = 1.0f; + mask[offset + ENCOUNTER_OFFENSIVE_OFF] = + p->offensive_prayer != OFFENSIVE_PRAYER_NONE; + mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY] = has_prayer; + mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR] = has_prayer; + mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY] = has_prayer; } -static void compute_action_masks( +static inline void pvp_write_action_mask_bytes( + unsigned char* mask, OsrsEnv* env, int agent_idx, const EncounterArenaTopology* topology ) { Player* p = &env->players[agent_idx]; - Player* t = &env->players[1 - agent_idx]; - - unsigned char* mask = env->action_masks + agent_idx * ACTION_MASK_SIZE; - int offset = 0; - - mask[offset + LOADOUT_KEEP] = 1; - for (int l = LOADOUT_MELEE; l <= LOADOUT_TANK; l++) { - mask[offset + l] = is_loadout_active(p, l) ? 0 : 1; - } - - int frozen_no_melee = !can_move(p) && !is_in_melee_range(p, t); - - uint8_t best_melee_spec = find_best_melee_spec(p); - int melee_spec_cost = 25; - if (best_melee_spec == ITEM_AGS || best_melee_spec == ITEM_ANCIENT_GS) melee_spec_cost = 50; - if (best_melee_spec == ITEM_STATIUS_WARHAMMER) melee_spec_cost = 35; - mask[offset + LOADOUT_SPEC_MELEE] = (best_melee_spec != ITEM_NONE) && - (p->special_energy >= melee_spec_cost) && !frozen_no_melee; - - uint8_t best_range_spec = find_best_ranged_spec(p); - int range_spec_cost = 50; - mask[offset + LOADOUT_SPEC_RANGE] = (best_range_spec != ITEM_NONE) && - (p->special_energy >= range_spec_cost); - - uint8_t best_magic_spec = find_best_magic_spec(p); - mask[offset + LOADOUT_SPEC_MAGIC] = (best_magic_spec != ITEM_NONE) && - (p->special_energy >= 55); - - mask[offset + LOADOUT_GMAUL] = player_has_gmaul(p) && - (p->special_energy >= 50) && !frozen_no_melee; - - if (frozen_no_melee) { - mask[offset + LOADOUT_MELEE] = 0; + Player* target = &env->players[1 - agent_idx]; + memset(mask, 0, NH_PVP_ACTION_MASK_SIZE); + + int offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_PRIMARY); + mask[offset] = 1; + if (can_move(p)) { + for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + int nx = p->x + ENCOUNTER_MOVE_TARGET_DX[action]; + int ny = p->y + ENCOUNTER_MOVE_TARGET_DY[action]; + mask[offset + action] = + pvp_topology_tile_walkable(topology, nx, ny); + } } - offset += LOADOUT_DIM; - - int attack_ready = remaining_ticks(p->attack_timer) == 0; - int current_loadout = get_current_loadout(p); - int in_mage_loadout = (current_loadout == LOADOUT_MAGE); - int in_tank_loadout = (current_loadout == LOADOUT_TANK); - int weapon_style = get_slot_weapon_attack_style(p); - int melee_reachable = (weapon_style == ATTACK_STYLE_MELEE) - ? (is_in_melee_range(p, t) || can_move(p)) - : 1; - int can_move_now = can_move(p); - mask[offset + ATTACK_NONE] = 1; - mask[offset + ATTACK_ATK] = attack_ready && !in_mage_loadout && !in_tank_loadout && - weapon_style != ATTACK_STYLE_NONE && - melee_reachable; - mask[offset + ATTACK_ICE] = attack_ready && can_cast_ice_spell(p); - mask[offset + ATTACK_BLOOD] = attack_ready && can_cast_blood_spell(p); - mask[offset + MOVE_ADJACENT] = - can_move_now && can_move_adjacent(p, topology); - mask[offset + MOVE_UNDER] = can_move_now && can_move_under(p, t); - mask[offset + MOVE_DIAGONAL] = - can_move_now && can_move_diagonal(p, topology); - mask[offset + MOVE_FARCAST_2] = - can_move_now && can_move_to_farcast(p, 2, topology); - mask[offset + MOVE_FARCAST_3] = - can_move_now && can_move_to_farcast(p, 3, topology); - mask[offset + MOVE_FARCAST_4] = - can_move_now && can_move_to_farcast(p, 4, topology); - mask[offset + MOVE_FARCAST_5] = - can_move_now && can_move_to_farcast(p, 5, topology); - mask[offset + MOVE_FARCAST_6] = - can_move_now && can_move_to_farcast(p, 6, topology); - mask[offset + MOVE_FARCAST_7] = - can_move_now && can_move_to_farcast(p, 7, topology); - offset += COMBAT_DIM; + mask[offset + OSRS_PRIMARY_MOVE_ACTIONS] = + target->current_hitpoints > 0; + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_OVERHEAD); int has_prayer = p->current_prayer > 0; mask[offset + ENCOUNTER_OVERHEAD_NO_CHANGE] = 1; mask[offset + ENCOUNTER_OVERHEAD_OFF] = p->prayer != PRAYER_NONE; - mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE] = has_prayer; - mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED] = has_prayer; - mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC] = has_prayer; - mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE] = has_prayer && !env->is_lms; - mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION] = has_prayer && !env->is_lms; - offset += OVERHEAD_DIM; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE] = has_prayer; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED] = has_prayer; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC] = has_prayer; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE] = + has_prayer && !env->is_lms; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION] = + has_prayer && !env->is_lms; - mask[offset + FOOD_NONE] = 1; - mask[offset + FOOD_EAT] = can_eat_food(p); - offset += FOOD_DIM; - - mask[offset + POTION_NONE] = 1; - mask[offset + POTION_BREW] = can_use_potion(p, 1) && can_use_brew_boost(p); - mask[offset + POTION_RESTORE] = can_use_potion(p, 2) && can_restore_stats(p); - mask[offset + POTION_COMBAT] = can_use_potion(p, 3) && can_boost_combat_skills(p); - mask[offset + POTION_RANGED] = can_use_potion(p, 4) && can_boost_ranged(p); - offset += POTION_DIM; - - mask[offset + KARAM_NONE] = 1; - mask[offset + KARAM_EAT] = can_eat_karambwan(p); - offset += KARAMBWAN_DIM; - - mask[offset + VENG_NONE] = 1; - mask[offset + VENG_CAST] = !env->is_lms && p->is_lunar_spellbook && !p->veng_active && - (remaining_ticks(p->veng_cooldown) == 0) && p->current_magic >= 94; - offset += VENG_DIM; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_EQUIP_SLOT(slot)); + mask[offset] = 1; + } + int eat_offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_EAT); + int drink_offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_DRINK); + mask[eat_offset] = 1; + mask[drink_offset] = 1; + int inventory_has_empty_cell = + osrs_first_empty_inventory_cell(p->inventory_cells, -1) >= 0; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_classify(&p->inventory_cells[cell]); + if (resolution.click_action == OSRS_CLICK_EQUIP) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata( + &p->inventory_cells[cell]); + if (!osrs_can_equip_metadata( + p, metadata, inventory_has_empty_cell)) { + continue; + } + int gear_slot = metadata->gear_slot; + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_EQUIP_SLOT(gear_slot)); + mask[offset + cell + 1] = 1; + } else if (resolution.click_action == OSRS_CLICK_EAT && + osrs_can_eat_consumable_kind( + p, resolution.consumable_kind)) { + mask[eat_offset + cell + 1] = 1; + } else if (resolution.click_action == OSRS_CLICK_DRINK && + pvp_drink_kind_available( + p, resolution.consumable_kind)) { + mask[drink_offset + cell + 1] = 1; + } + } + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_SPELL); + mask[offset + OSRS_SPELL_NONE] = 1; + mask[offset + OSRS_SPELL_BLOOD_BARRAGE] = can_cast_blood_spell(p); + mask[offset + OSRS_SPELL_ICE_BARRAGE] = can_cast_ice_spell(p); + mask[offset + OSRS_SPELL_VENGEANCE] = !env->is_lms && + p->is_lunar_spellbook && !p->veng_active && + remaining_ticks(p->veng_cooldown) == 0 && p->current_magic >= 94; + + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_SPECIAL); + mask[offset] = 1; + mask[offset + 1] = can_toggle_spec(p); + mask[offset + 2] = p->spec_armed; + + offset = osrs_base_action_head_mask_offset( + NH_PVP_TARGET_SLOTS, OSRS_HEAD_OFFENSIVE); mask[offset + ENCOUNTER_OFFENSIVE_NO_CHANGE] = 1; - mask[offset + ENCOUNTER_OFFENSIVE_OFF] = p->offensive_prayer != OFFENSIVE_PRAYER_NONE; - mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY] = has_prayer; + mask[offset + ENCOUNTER_OFFENSIVE_OFF] = + p->offensive_prayer != OFFENSIVE_PRAYER_NONE; + mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY] = has_prayer; mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR] = has_prayer; mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY] = has_prayer; - offset += OFFENSIVE_DIM; - - mask[offset + 0] = 1; - int can_move_for_move_head = can_move(p); - for (int m = 1; m < MOVE_DIM; m++) { - if (!can_move_for_move_head) { - mask[offset + m] = 0; - continue; - } - int nx = p->x + ENCOUNTER_MOVE_TARGET_DX[m]; - int ny = p->y + ENCOUNTER_MOVE_TARGET_DY[m]; - mask[offset + m] = - pvp_topology_tile_walkable(topology, nx, ny) ? 1 : 0; - } - offset += MOVE_DIM; } -#endif // OSRS_PVP_OBSERVATIONS_H +#endif diff --git a/ocean/osrs/osrs_pvp_opponents.h b/ocean/osrs/osrs_pvp_opponents.h index c7092b53f3..b7a3e96205 100644 --- a/ocean/osrs/osrs_pvp_opponents.h +++ b/ocean/osrs/osrs_pvp_opponents.h @@ -1,23 +1,101 @@ #ifndef OSRS_PVP_OPPONENTS_H #define OSRS_PVP_OPPONENTS_H +#include "osrs_policy.h" +#include "osrs_pvp_actions.h" + #define OPP_STYLE_MAGE 0 #define OPP_STYLE_RANGED 1 #define OPP_STYLE_MELEE 2 #define OPP_STYLE_SPEC 3 -static inline int opp_style_to_loadout(int style) { - switch (style) { - case OPP_STYLE_MAGE: return LOADOUT_MAGE; - case OPP_STYLE_RANGED: return LOADOUT_RANGE; - case OPP_STYLE_MELEE: return LOADOUT_MELEE; - case OPP_STYLE_SPEC: return LOADOUT_SPEC_MELEE; - default: return LOADOUT_KEEP; +static inline PvpEquipmentPlan opp_equipment_plan(int style) { + if (style == OPP_STYLE_MAGE) return PVP_EQUIPMENT_MAGIC; + if (style == OPP_STYLE_RANGED) return PVP_EQUIPMENT_RANGED; + if (style == OPP_STYLE_SPEC) return PVP_EQUIPMENT_SPEC_MELEE; + return PVP_EQUIPMENT_MELEE; +} + +static inline void opp_apply_equipment_plan( + int* actions, + const Player* self, + PvpEquipmentPlan plan +) { + pvp_emit_equipment_plan_actions(actions, self, plan); + if (plan == PVP_EQUIPMENT_SPEC_MELEE || + plan == PVP_EQUIPMENT_SPEC_RANGED || + plan == PVP_EQUIPMENT_SPEC_MAGIC || + plan == PVP_EQUIPMENT_GMAUL) + actions[OSRS_HEAD_SPECIAL] = 1; +} + +static inline void opp_apply_gear_switch( + int* actions, + const Player* self, + int style +) { + opp_apply_equipment_plan(actions, self, opp_equipment_plan(style)); +} + +static inline int opp_find_consumable_cell( + const Player* player, + OsrsConsumableKind kind +) { + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&player->inventory_cells[cell]); + if (metadata->consumable_kind == kind) return cell; + } + return -1; +} + +static inline void opp_emit_consumable( + int* actions, + const Player* player, + OsrsConsumableKind kind +) { + int cell = opp_find_consumable_cell(player, kind); + if (cell < 0) return; + OsrsClickAction click = (OsrsClickAction)osrs_inventory_cell_metadata( + &player->inventory_cells[cell])->click_action; + if (click == OSRS_CLICK_EAT) actions[OSRS_HEAD_EAT] = cell + 1; + else if (click == OSRS_CLICK_DRINK) actions[OSRS_HEAD_DRINK] = cell + 1; +} + +static inline void opp_emit_move_toward( + int* actions, + const Player* self, + int destination_x, + int destination_y +) { + int dx = clamp(destination_x - self->x, -2, 2); + int dy = clamp(destination_y - self->y, -2, 2); + for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + if (ENCOUNTER_MOVE_TARGET_DX[action] == dx && + ENCOUNTER_MOVE_TARGET_DY[action] == dy) { + actions[OSRS_HEAD_PRIMARY] = action; + return; + } } } -static inline void opp_apply_gear_switch(int* actions, int style) { - actions[HEAD_LOADOUT] = opp_style_to_loadout(style); +static inline void opp_emit_farcast_move( + int* actions, + const Player* self, + const Player* target, + int distance +) { + int raw_dx = self->x - target->x; + int raw_dy = self->y - target->y; + int dx = clamp(raw_dx, -distance, distance); + int dy = clamp(raw_dy, -distance, distance); + int adx = abs_int(dx); + int ady = abs_int(dy); + if (adx < distance && ady < distance) { + if (adx >= ady) dx = raw_dx >= 0 ? distance : -distance; + else dy = raw_dy >= 0 ? distance : -distance; + } + opp_emit_move_toward(actions, self, target->x + dx, target->y + dy); } typedef struct { @@ -36,13 +114,31 @@ static inline void opp_tick_cooldowns(OpponentState* opp) { static inline OppConsumables opp_get_consumables(OpponentState* opp, Player* self) { float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; OppConsumables c; - c.can_food = (opp->food_cooldown <= 0 && self->food_count > 0 && hp_pct < 1.0f); - c.can_brew = (opp->potion_cooldown <= 0 && self->brew_doses > 0); - c.can_karambwan = (opp->karambwan_cooldown <= 0 && self->karambwan_count > 0 && hp_pct < 1.0f); - c.can_restore = (opp->potion_cooldown <= 0 && self->restore_doses > 0); + c.can_food = opp->food_cooldown <= 0 && can_eat_food(self) && hp_pct < 1.0f; + c.can_brew = opp->potion_cooldown <= 0 && + pvp_drink_kind_available(self, OSRS_CONSUMABLE_BREW); + c.can_karambwan = opp->karambwan_cooldown <= 0 && + can_eat_karambwan(self) && hp_pct < 1.0f; + c.can_restore = opp->potion_cooldown <= 0 && + pvp_drink_kind_available(self, OSRS_CONSUMABLE_SUPER_RESTORE); return c; } +static inline void opp_emit_preferred_food( + OpponentState* opp, + int* actions, + const Player* self, + OppConsumables consumables +) { + if (consumables.can_food) { + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); + opp->food_cooldown = 3; + } else if (consumables.can_karambwan) { + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_KARAMBWAN); + opp->karambwan_cooldown = 2; + } +} + static inline AttackStyle opp_get_gear_style(Player* p) { int s = get_item_attack_style(p->equipped[GEAR_SLOT_WEAPON]); if (s == 3) return ATTACK_STYLE_MAGIC; @@ -253,9 +349,9 @@ static inline int opp_get_target_gear_style(Player* target) { static inline int opp_get_mage_attack(Player* self, Player* target) { int can_freeze = target->freeze_immunity_ticks <= 1 && target->frozen_ticks == 0; - if (can_freeze) return ATTACK_ICE; + if (can_freeze) return 0; float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; - return (hp_pct > 0.98f) ? ATTACK_ICE : ATTACK_BLOOD; + return hp_pct > 0.98f ? 0 : 1; } static void opp_apply_boost_potion(OsrsEnv* env, OpponentState* opp, int* actions, @@ -265,8 +361,9 @@ static void opp_apply_boost_potion(OsrsEnv* env, OpponentState* opp, int* action if (opp->potion_cooldown > 0) return; float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; - if (opp_is_drained(self) && hp_pct > 0.90f && self->restore_doses > 0) { - actions[HEAD_POTION] = POTION_RESTORE; + if (opp_is_drained(self) && hp_pct > 0.90f && + pvp_drink_kind_available(self, OSRS_CONSUMABLE_SUPER_RESTORE)) { + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SUPER_RESTORE); opp->potion_cooldown = 3; return; } @@ -274,20 +371,22 @@ static void opp_apply_boost_potion(OsrsEnv* env, OpponentState* opp, int* action if (hp_pct <= 0.90f) return; if (attack_style == OPP_STYLE_MELEE || attack_style == OPP_STYLE_SPEC) { - if (self->current_strength <= self->base_strength && self->combat_potion_doses > 0) { - actions[HEAD_POTION] = POTION_COMBAT; + if (self->current_strength <= self->base_strength && + pvp_drink_kind_available(self, OSRS_CONSUMABLE_SUPER_COMBAT)) { + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SUPER_COMBAT); opp->potion_cooldown = 3; } } else if (attack_style == OPP_STYLE_RANGED) { - if (self->current_ranged <= self->base_ranged && self->ranged_potion_doses > 0) { - actions[HEAD_POTION] = POTION_RANGED; + if (self->current_ranged <= self->base_ranged && + pvp_drink_kind_available(self, OSRS_CONSUMABLE_RANGING)) { + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_RANGING); opp->potion_cooldown = 3; } } } static inline int opp_check_eating_queued(int* actions) { - return actions[HEAD_FOOD] != FOOD_NONE || actions[HEAD_KARAMBWAN] != KARAM_NONE; + return actions[OSRS_HEAD_EAT] != 0; } static int opp_apply_consumables(OsrsEnv* env, OpponentState* opp, int* actions, @@ -297,44 +396,38 @@ static int opp_apply_consumables(OsrsEnv* env, OpponentState* opp, int* actions, OppConsumables cons = opp_get_consumables(opp, self); int potion_used = 0; - if (hp_pct < opp->eat_triple_threshold && cons.can_food && cons.can_brew && cons.can_karambwan) { - actions[HEAD_FOOD] = FOOD_EAT; - actions[HEAD_POTION] = POTION_BREW; - actions[HEAD_KARAMBWAN] = KARAM_EAT; - opp->food_cooldown = 3; + if (hp_pct < opp->eat_triple_threshold && cons.can_brew && (cons.can_food || cons.can_karambwan)) { + opp_emit_preferred_food(opp, actions, self, cons); + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; - opp->karambwan_cooldown = 2; potion_used = 1; } else if (hp_pct < opp->eat_double_threshold && cons.can_food && cons.can_brew) { - actions[HEAD_FOOD] = FOOD_EAT; - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->food_cooldown = 3; opp->potion_cooldown = 3; potion_used = 1; - } else if (hp_pct < opp->eat_double_threshold && cons.can_food && cons.can_karambwan) { - actions[HEAD_FOOD] = FOOD_EAT; - actions[HEAD_KARAMBWAN] = KARAM_EAT; - opp->food_cooldown = 3; - opp->karambwan_cooldown = 2; + } else if (hp_pct < opp->eat_double_threshold && (cons.can_food || cons.can_karambwan)) { + opp_emit_preferred_food(opp, actions, self, cons); } else if (hp_pct < opp->eat_brew_threshold && cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; potion_used = 1; } else if (hp_pct < 0.60f && cons.can_food) { - actions[HEAD_FOOD] = FOOD_EAT; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); opp->food_cooldown = 3; } else if (hp_pct < 0.60f && cons.can_karambwan) { - actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_KARAMBWAN); opp->karambwan_cooldown = 2; } else if (opp_is_drained(self) && hp_pct < 0.90f && cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; potion_used = 1; } else if (prayer_pct < 0.30f && cons.can_restore) { - actions[HEAD_POTION] = POTION_RESTORE; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SUPER_RESTORE); opp->potion_cooldown = 3; } else if (include_drained_restore && opp_is_drained(self) && cons.can_restore) { - actions[HEAD_POTION] = POTION_RESTORE; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SUPER_RESTORE); opp->potion_cooldown = 3; } @@ -365,7 +458,7 @@ static inline void opp_emit_prayer(int* actions, Player* self, int target_overhe default: return; } if (self->prayer == target_prayer) return; - actions[HEAD_OVERHEAD] = (target_prayer == PRAYER_NONE) + actions[OSRS_HEAD_OVERHEAD] = (target_prayer == PRAYER_NONE) ? ENCOUNTER_OVERHEAD_OFF : opp_set_refresh_for_prayer(target_prayer); } @@ -386,7 +479,7 @@ static inline int opp_process_pending_prayer(OpponentState* opp, int* actions, P case OVERHEAD_REDEMPTION: target_prayer = PRAYER_REDEMPTION; action = ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION; break; default: break; } - if (self->prayer != target_prayer) actions[HEAD_OVERHEAD] = action; + if (self->prayer != target_prayer) actions[OSRS_HEAD_OVERHEAD] = action; opp->pending_prayer_value = 0; return 1; } @@ -450,13 +543,13 @@ static inline void opp_offensive_prayer_noop_roll(OsrsEnv* env) { } static inline void opp_emit_attack(int* actions, int actual_attack) { - if (actual_attack == 0) { - actions[HEAD_COMBAT] = ATTACK_ICE; - } else if (actual_attack == 1) { - actions[HEAD_COMBAT] = ATTACK_BLOOD; - } else { - actions[HEAD_COMBAT] = ATTACK_ATK; - } + actions[OSRS_HEAD_PRIMARY] = OSRS_PRIMARY_MOVE_ACTIONS; + if (actual_attack == 0) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_ICE_BARRAGE; + else if (actual_attack == 1) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_BLOOD_BARRAGE; + else if (actual_attack == 3) + actions[OSRS_HEAD_SPECIAL] = 1; } static void opp_move_when_waiting(OsrsEnv* env, OpponentState* opp, int* actions, @@ -464,21 +557,22 @@ static void opp_move_when_waiting(OsrsEnv* env, OpponentState* opp, int* actions int dist = chebyshev_distance(self->x, self->y, target->x, target->y); if (target->frozen_ticks > 0 && self->frozen_ticks == 0 && dist > 0 && (under_prob >= 1.0f || (under_prob > 0.0f && rand_float(env) < under_prob))) { - actions[HEAD_COMBAT] = MOVE_UNDER; + opp_emit_move_toward(actions, self, target->x, target->y); } else if (opp->target_fleeing_ticks >= 2 && dist > 3 && self->frozen_ticks == 0) { - actions[HEAD_COMBAT] = MOVE_FARCAST_3; + opp_emit_farcast_move(actions, self, target, 3); } else if (opp_should_fc3(self, target) && target->prayer != PRAYER_PROTECT_MELEE) { - actions[HEAD_COMBAT] = MOVE_FARCAST_3; + opp_emit_farcast_move(actions, self, target, 3); } } static void opp_attack_random_style(OsrsEnv* env, int* actions) { + Player* self = &env->players[1]; int style = rand_int(env, 3); - opp_apply_gear_switch(actions, style); + opp_apply_gear_switch(actions, self, style); if (style == OPP_STYLE_MAGE) { - actions[HEAD_COMBAT] = (rand_int(env, 2) == 0) ? ATTACK_ICE : ATTACK_BLOOD; + opp_emit_attack(actions, rand_int(env, 2) == 0 ? 0 : 1); } else { - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_emit_attack(actions, 2); } } @@ -486,14 +580,14 @@ static void opp_attack_random_style_with_spec(OsrsEnv* env, Player* self, int* a int style = rand_int(env, 3); if (self->special_energy >= get_melee_spec_cost(self->melee_spec_weapon) && rand_float(env) < 0.30f) { - opp_apply_gear_switch(actions, OPP_STYLE_SPEC); - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_apply_gear_switch(actions, self, OPP_STYLE_SPEC); + opp_emit_attack(actions, 2); } else { - opp_apply_gear_switch(actions, style); + opp_apply_gear_switch(actions, self, style); if (style == OPP_STYLE_MAGE) { - actions[HEAD_COMBAT] = (rand_int(env, 2) == 0) ? ATTACK_ICE : ATTACK_BLOOD; + opp_emit_attack(actions, rand_int(env, 2) == 0 ? 0 : 1); } else { - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_emit_attack(actions, 2); } } } @@ -557,19 +651,20 @@ static int opp_try_fake_switch(OsrsEnv* env, OpponentState* opp, int* actions, if (fail_prob >= 0.0f) opp->fake_switch_failed = (rand_float(env) < fail_prob) ? 1 : 0; - opp_apply_gear_switch(actions, opp->fake_switch_style); + opp_apply_gear_switch(actions, self, opp->fake_switch_style); int dist = chebyshev_distance(self->x, self->y, target->x, target->y); if (target->frozen_ticks > 0 && self->frozen_ticks == 0 && dist > 0) { - actions[HEAD_COMBAT] = MOVE_UNDER; + opp_emit_move_toward(actions, self, target->x, target->y); } return 1; } static void opp_true_random(OsrsEnv* env, int* actions) { - for (int i = 0; i < NUM_ACTION_HEADS; i++) { - actions[i] = rand_int(env, ACTION_HEAD_DIMS[i]); - } + static const int action_head_dims[OSRS_BASE_NUM_ACTION_HEADS] = + OSRS_BASE_ACTION_DIMS_INIT(1); + for (int head = 0; head < OSRS_BASE_NUM_ACTION_HEADS; head++) + actions[head] = rand_int(env, action_head_dims[head]); } static void opp_panicking(OsrsEnv* env, OpponentState* opp, int* actions) { @@ -586,12 +681,12 @@ static void opp_panicking(OsrsEnv* env, OpponentState* opp, int* actions) { int eating = 0; if (hp_pct < 0.25f) { if (cons.can_food) { - actions[HEAD_FOOD] = FOOD_EAT; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); opp->food_cooldown = 3; eating = 1; } if (cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; } } @@ -599,13 +694,13 @@ static void opp_panicking(OsrsEnv* env, OpponentState* opp, int* actions) { if (opp_should_skip_offensive(env, opp)) return; if (opp_attack_ready(self) && !eating && rand_float(env) < 0.30f) { - opp_apply_gear_switch(actions, opp->chosen_style); + opp_apply_gear_switch(actions, self, opp->chosen_style); if (opp->chosen_style == OPP_STYLE_MAGE) { - int spell = (rand_int(env, 2) == 0) ? ATTACK_ICE : ATTACK_BLOOD; - actions[HEAD_COMBAT] = spell; + int spell = rand_int(env, 2) == 0 ? 0 : 1; + opp_emit_attack(actions, spell); } else { - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_emit_attack(actions, 2); } } } @@ -623,11 +718,11 @@ static void opp_weak_random(OsrsEnv* env, OpponentState* opp, int* actions) { int eating = 0; if (hp_pct < 0.30f && rand_float(env) > 0.50f) { if (cons.can_food) { - actions[HEAD_FOOD] = FOOD_EAT; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); opp->food_cooldown = 3; eating = 1; } else if (cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; eating = 1; } @@ -653,11 +748,11 @@ static void opp_semi_random(OsrsEnv* env, OpponentState* opp, int* actions) { int eating = 0; if (hp_pct < 0.30f) { if (cons.can_food) { - actions[HEAD_FOOD] = FOOD_EAT; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); opp->food_cooldown = 3; eating = 1; } else if (cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; eating = 1; } @@ -689,11 +784,11 @@ static void opp_sticky_prayer(OsrsEnv* env, OpponentState* opp, int* actions) { int eating = 0; if (hp_pct < 0.30f) { if (cons.can_food) { - actions[HEAD_FOOD] = FOOD_EAT; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); opp->food_cooldown = 3; eating = 1; } else if (cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; eating = 1; } @@ -725,36 +820,29 @@ static void opp_random_eater(OsrsEnv* env, OpponentState* opp, int* actions) { int potion_used = 0; if (hp_pct < 0.35f) { - if (cons.can_food) { - actions[HEAD_FOOD] = FOOD_EAT; - opp->food_cooldown = 3; - } + opp_emit_preferred_food(opp, actions, self, cons); if (cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; potion_used = 1; } - if (cons.can_karambwan) { - actions[HEAD_KARAMBWAN] = KARAM_EAT; - opp->karambwan_cooldown = 2; - } } else if (hp_pct < 0.55f) { if (cons.can_food) { - actions[HEAD_FOOD] = FOOD_EAT; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); opp->food_cooldown = 3; } else if (cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; potion_used = 1; } } else if (hp_pct < opp->eat_brew_threshold && cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; potion_used = 1; } if (!potion_used && prayer_pct < 0.30f && cons.can_restore) { - actions[HEAD_POTION] = POTION_RESTORE; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SUPER_RESTORE); opp->potion_cooldown = 3; } @@ -778,28 +866,21 @@ static void opp_prayer_rookie(OsrsEnv* env, OpponentState* opp, int* actions) { opp_apply_defensive_prayer(env, opp, actions, self, target, 0); if (hp_pct < 0.35f) { - if (cons.can_food) { - actions[HEAD_FOOD] = FOOD_EAT; - opp->food_cooldown = 3; - } + opp_emit_preferred_food(opp, actions, self, cons); if (cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; } - if (cons.can_karambwan) { - actions[HEAD_KARAMBWAN] = KARAM_EAT; - opp->karambwan_cooldown = 2; - } } else if (hp_pct < 0.55f) { if (cons.can_food) { - actions[HEAD_FOOD] = FOOD_EAT; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); opp->food_cooldown = 3; } else if (cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; } } else if (hp_pct < opp->eat_brew_threshold && cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; } @@ -883,11 +964,9 @@ static void opp_nh_tier(OsrsEnv* env, OpponentState* opp, int* actions, const Op actual_attack = 2; } - if (actual_attack != 3 && rand_float(env) < opp->offensive_prayer_miss) { - actions[HEAD_LOADOUT] = LOADOUT_KEEP; - } else { - opp_apply_gear_switch(actions, actual_style); - } + if (actual_attack == 3 || + rand_float(env) >= opp->offensive_prayer_miss) + opp_apply_gear_switch(actions, self, actual_style); if (tier->offensive_noop_roll) opp_offensive_prayer_noop_roll(env); @@ -926,27 +1005,26 @@ static void opp_nh_basic(OsrsEnv* env, OpponentState* opp, int* actions, rand_float(env) < spec_prob) { int dist = chebyshev_distance(self->x, self->y, target->x, target->y); if (opp->target_fleeing_ticks >= 2 && dist > 1) { - opp_apply_gear_switch(actions, OPP_STYLE_MAGE); - actions[HEAD_COMBAT] = ATTACK_ICE; + opp_apply_gear_switch(actions, self, OPP_STYLE_MAGE); + opp_emit_attack(actions, 0); } else { - opp_apply_gear_switch(actions, OPP_STYLE_SPEC); - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_apply_gear_switch(actions, self, OPP_STYLE_SPEC); + opp_emit_attack(actions, 2); } } else { - if (rand_float(env) < opp->offensive_prayer_miss) { - actions[HEAD_LOADOUT] = LOADOUT_KEEP; - } else { - opp_apply_gear_switch(actions, style); + if (rand_float(env) >= opp->offensive_prayer_miss) { + opp_apply_gear_switch(actions, self, style); } opp_offensive_prayer_noop_roll(env); if (style == OPP_STYLE_MAGE) { - actions[HEAD_COMBAT] = coin_flip_spell - ? ((rand_int(env, 2) == 0) ? ATTACK_ICE : ATTACK_BLOOD) - : ((hp_pct < 0.30f) ? ATTACK_BLOOD : ATTACK_ICE); + int spell = coin_flip_spell + ? (rand_int(env, 2) == 0 ? 0 : 1) + : (hp_pct < 0.30f ? 1 : 0); + opp_emit_attack(actions, spell); } else { - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_emit_attack(actions, 2); } } } @@ -960,7 +1038,7 @@ static void opp_onetick(OsrsEnv* env, OpponentState* opp, int* actions) { opp_tick_cooldowns(opp); if (!opp_attack_ready(self)) { - actions[HEAD_LOADOUT] = LOADOUT_TANK; + opp_apply_equipment_plan(actions, self, PVP_EQUIPMENT_TANK); } opp_apply_defensive_prayer(env, opp, actions, self, target, 1); @@ -988,22 +1066,22 @@ static void opp_onetick(OsrsEnv* env, OpponentState* opp, int* actions) { int actual_style; int actual_attack; - int spec_loadout = LOADOUT_SPEC_MELEE; + PvpEquipmentPlan spec_plan = PVP_EQUIPMENT_SPEC_MELEE; if (spec.ranged && (dist >= 3 || target->frozen_ticks > 0)) { actual_style = OPP_STYLE_RANGED; actual_attack = 3; - spec_loadout = LOADOUT_SPEC_RANGE; + spec_plan = PVP_EQUIPMENT_SPEC_RANGED; } else if (spec.magic) { actual_style = OPP_STYLE_MAGE; actual_attack = 3; - spec_loadout = LOADOUT_SPEC_MAGIC; + spec_plan = PVP_EQUIPMENT_SPEC_MAGIC; } else if (spec.melee) { actual_style = OPP_STYLE_SPEC; actual_attack = 3; } else if (target->frozen_ticks == 0 && (off_mask & (1 << OPP_STYLE_MAGE))) { actual_style = OPP_STYLE_MAGE; - actual_attack = opp_get_mage_attack(self, target) == ATTACK_ICE ? 0 : 1; + actual_attack = opp_get_mage_attack(self, target) == 0 ? 0 : 1; } else { int can_use_preferred = preferred_style >= 0 && (preferred_style != OPP_STYLE_MELEE || self->frozen_ticks <= 10 || dist <= 1); @@ -1039,11 +1117,9 @@ static void opp_onetick(OsrsEnv* env, OpponentState* opp, int* actions) { if (opp_attack_ready(self) && !eating_queued) { if (actual_attack == 3) { - actions[HEAD_LOADOUT] = spec_loadout; - } else if (rand_float(env) < opp->offensive_prayer_miss) { - actions[HEAD_LOADOUT] = LOADOUT_KEEP; - } else { - opp_apply_gear_switch(actions, actual_style); + opp_apply_equipment_plan(actions, self, spec_plan); + } else if (rand_float(env) >= opp->offensive_prayer_miss) { + opp_apply_gear_switch(actions, self, actual_style); } opp_emit_attack(actions, actual_attack); @@ -1105,11 +1181,9 @@ static void opp_unpredictable_improved(OsrsEnv* env, OpponentState* opp, int* ac actual_attack = 2; } - if (actual_attack != 3 && rand_float(env) < opp->offensive_prayer_miss) { - actions[HEAD_LOADOUT] = LOADOUT_KEEP; - } else { - opp_apply_gear_switch(actions, actual_style); - } + if (actual_attack == 3 || + rand_float(env) >= opp->offensive_prayer_miss) + opp_apply_gear_switch(actions, self, actual_style); int action_delay = opp_sample_delay(env, UNPREDICTABLE_IMP_ACTION_CUM, UNPREDICTABLE_IMP_ACTION_CUM_LEN); if (action_delay == 0) { @@ -1128,7 +1202,7 @@ static void opp_unpredictable_onetick(OsrsEnv* env, OpponentState* opp, int* act opp_tick_cooldowns(opp); if (!opp_attack_ready(self)) { - actions[HEAD_LOADOUT] = LOADOUT_TANK; + opp_apply_equipment_plan(actions, self, PVP_EQUIPMENT_TANK); } opp_handle_delayed_prayer(env, opp, actions, self, target, @@ -1167,22 +1241,22 @@ static void opp_unpredictable_onetick(OsrsEnv* env, OpponentState* opp, int* act int actual_style; int actual_attack; - int spec_loadout = LOADOUT_SPEC_MELEE; + PvpEquipmentPlan spec_plan = PVP_EQUIPMENT_SPEC_MELEE; if (spec.ranged && (dist >= 3 || target->frozen_ticks > 0)) { actual_style = OPP_STYLE_RANGED; actual_attack = 3; - spec_loadout = LOADOUT_SPEC_RANGE; + spec_plan = PVP_EQUIPMENT_SPEC_RANGED; } else if (spec.magic) { actual_style = OPP_STYLE_MAGE; actual_attack = 3; - spec_loadout = LOADOUT_SPEC_MAGIC; + spec_plan = PVP_EQUIPMENT_SPEC_MAGIC; } else if (spec.melee) { actual_style = OPP_STYLE_SPEC; actual_attack = 3; } else if (target->frozen_ticks == 0 && (off_mask & (1 << OPP_STYLE_MAGE))) { actual_style = OPP_STYLE_MAGE; - actual_attack = opp_get_mage_attack(self, target) == ATTACK_ICE ? 0 : 1; + actual_attack = opp_get_mage_attack(self, target) == 0 ? 0 : 1; } else { int can_use_preferred = preferred_style >= 0 && (preferred_style != OPP_STYLE_MELEE || self->frozen_ticks <= 10 || dist <= 1); @@ -1214,11 +1288,9 @@ static void opp_unpredictable_onetick(OsrsEnv* env, OpponentState* opp, int* act int action_delay = opp_sample_delay(env, UNPREDICTABLE_OT_ACTION_CUM, UNPREDICTABLE_OT_ACTION_CUM_LEN); if (action_delay == 0) { if (actual_attack == 3) { - actions[HEAD_LOADOUT] = spec_loadout; - } else if (rand_float(env) < opp->offensive_prayer_miss) { - actions[HEAD_LOADOUT] = LOADOUT_KEEP; - } else { - opp_apply_gear_switch(actions, actual_style); + opp_apply_equipment_plan(actions, self, spec_plan); + } else if (rand_float(env) >= opp->offensive_prayer_miss) { + opp_apply_gear_switch(actions, self, actual_style); } opp_emit_attack(actions, actual_attack); @@ -1241,38 +1313,39 @@ static void opp_read_agent_action(OsrsEnv* env, OpponentState* opp) { /* env->actions holds THIS tick's agent actions; pending_actions is last tick's */ int* agent_actions = &env->actions[0]; - int loadout = agent_actions[HEAD_LOADOUT]; - int attack = agent_actions[HEAD_COMBAT]; - - if (loadout != LOADOUT_KEEP && loadout != LOADOUT_TANK) { - if (loadout == LOADOUT_MELEE || loadout == LOADOUT_SPEC_MELEE || loadout == LOADOUT_GMAUL) { - opp->read_agent_style = ATTACK_STYLE_MELEE; - } else if (loadout == LOADOUT_RANGE || loadout == LOADOUT_SPEC_RANGE) { - opp->read_agent_style = ATTACK_STYLE_RANGED; - } else if (loadout == LOADOUT_MAGE || loadout == LOADOUT_SPEC_MAGIC) { - opp->read_agent_style = ATTACK_STYLE_MAGIC; + int primary = agent_actions[OSRS_HEAD_PRIMARY]; + int spell = agent_actions[OSRS_HEAD_SPELL]; + int weapon_cell_action = agent_actions[OSRS_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)]; + + if (weapon_cell_action > 0 && weapon_cell_action <= OSRS_INVENTORY_SIZE) { + const OsrsItemContentMetadata* metadata = osrs_inventory_cell_metadata( + &env->players[0].inventory_cells[weapon_cell_action - 1]); + if (metadata->click_action == OSRS_CLICK_EQUIP && + metadata->gear_slot == GEAR_SLOT_WEAPON) { + opp->read_agent_style = (AttackStyle)metadata->attack_style; + opp->has_read_this_tick = 1; } - opp->has_read_this_tick = 1; - } else if (attack == ATTACK_ICE || attack == ATTACK_BLOOD) { + } + if (!opp->has_read_this_tick && + (spell == OSRS_SPELL_ICE_BARRAGE || + spell == OSRS_SPELL_BLOOD_BARRAGE)) { opp->read_agent_style = ATTACK_STYLE_MAGIC; opp->has_read_this_tick = 1; - } else if (attack == ATTACK_ATK) { - uint8_t weapon = env->players[0].equipped[GEAR_SLOT_WEAPON]; - int style = get_item_attack_style(weapon); - if (style == 1) opp->read_agent_style = ATTACK_STYLE_MELEE; - else if (style == 2) opp->read_agent_style = ATTACK_STYLE_RANGED; - else if (style == 3) opp->read_agent_style = ATTACK_STYLE_MAGIC; + } else if (!opp->has_read_this_tick && + primary >= OSRS_PRIMARY_MOVE_ACTIONS && + primary < OSRS_PRIMARY_DIM(1)) { + opp->read_agent_style = get_slot_weapon_attack_style(&env->players[0]); opp->has_read_this_tick = 1; } - int overhead = agent_actions[HEAD_OVERHEAD]; + int overhead = agent_actions[OSRS_HEAD_OVERHEAD]; if (overhead == ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE) opp->read_agent_prayer = PRAYER_PROTECT_MELEE; else if (overhead == ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED) opp->read_agent_prayer = PRAYER_PROTECT_RANGED; else if (overhead == ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC) opp->read_agent_prayer = PRAYER_PROTECT_MAGIC; else if (overhead == ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE) opp->read_agent_prayer = PRAYER_SMITE; else if (overhead == ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION) opp->read_agent_prayer = PRAYER_REDEMPTION; - opp->read_agent_moving = is_move_action(attack) ? 1 : 0; + opp->read_agent_moving = primary > 0 && primary < OSRS_PRIMARY_MOVE_ACTIONS; } static inline int opp_get_read_defensive_prayer(OpponentState* opp) { @@ -1299,7 +1372,7 @@ static void opp_master_nh(OsrsEnv* env, OpponentState* opp, int* actions) { opp_read_agent_action(env, opp); if (!opp_attack_ready(self)) { - actions[HEAD_LOADOUT] = LOADOUT_TANK; + opp_apply_equipment_plan(actions, self, PVP_EQUIPMENT_TANK); } int def_prayer = -1; @@ -1368,31 +1441,31 @@ static void opp_master_nh(OsrsEnv* env, OpponentState* opp, int* actions) { int actual_style; int actual_attack; - int spec_loadout = LOADOUT_SPEC_MELEE; + PvpEquipmentPlan spec_plan = PVP_EQUIPMENT_SPEC_MELEE; if (spec.ranged && (dist >= 3 || target->frozen_ticks > 0)) { actual_style = OPP_STYLE_RANGED; actual_attack = 3; - spec_loadout = LOADOUT_SPEC_RANGE; + spec_plan = PVP_EQUIPMENT_SPEC_RANGED; } else if (spec.magic) { actual_style = OPP_STYLE_MAGE; actual_attack = 3; - spec_loadout = LOADOUT_SPEC_MAGIC; + spec_plan = PVP_EQUIPMENT_SPEC_MAGIC; } else if (spec.melee) { actual_style = OPP_STYLE_SPEC; actual_attack = 3; } else if (preferred_style >= 0) { actual_style = preferred_style; actual_attack = (preferred_style == OPP_STYLE_MAGE) - ? (opp_get_mage_attack(self, target) == ATTACK_ICE ? 0 : 1) + ? (opp_get_mage_attack(self, target) == 0 ? 0 : 1) : 2; } else if (target->frozen_ticks == 0 && (off_mask & (1 << OPP_STYLE_MAGE))) { actual_style = OPP_STYLE_MAGE; - actual_attack = opp_get_mage_attack(self, target) == ATTACK_ICE ? 0 : 1; + actual_attack = opp_get_mage_attack(self, target) == 0 ? 0 : 1; } else { actual_style = opp_pick_from_mask(env, off_mask); actual_attack = (actual_style == OPP_STYLE_MAGE) - ? (opp_get_mage_attack(self, target) == ATTACK_ICE ? 0 : 1) + ? (opp_get_mage_attack(self, target) == 0 ? 0 : 1) : 2; } @@ -1402,11 +1475,9 @@ static void opp_master_nh(OsrsEnv* env, OpponentState* opp, int* actions) { if (opp_attack_ready(self) && !eating_queued) { if (actual_attack == 3) { - actions[HEAD_LOADOUT] = spec_loadout; - } else if (rand_float(env) < opp->offensive_prayer_miss) { - actions[HEAD_LOADOUT] = LOADOUT_KEEP; - } else { - opp_apply_gear_switch(actions, actual_style); + opp_apply_equipment_plan(actions, self, spec_plan); + } else if (rand_float(env) >= opp->offensive_prayer_miss) { + opp_apply_gear_switch(actions, self, actual_style); } opp_emit_attack(actions, actual_attack); @@ -1425,40 +1496,37 @@ static void opp_veng_fighter(OsrsEnv* env, OpponentState* opp, int* actions) { opp_apply_defensive_prayer(env, opp, actions, self, target, 0); - if (hp_pct < opp->eat_triple_threshold && cons.can_food && cons.can_brew && cons.can_karambwan) { - actions[HEAD_FOOD] = FOOD_EAT; - actions[HEAD_POTION] = POTION_BREW; - actions[HEAD_KARAMBWAN] = KARAM_EAT; - opp->food_cooldown = 3; opp->potion_cooldown = 3; opp->karambwan_cooldown = 2; + if (hp_pct < opp->eat_triple_threshold && cons.can_brew && (cons.can_food || cons.can_karambwan)) { + opp_emit_preferred_food(opp, actions, self, cons); + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); + opp->potion_cooldown = 3; } else if (hp_pct < opp->eat_double_threshold && cons.can_food && cons.can_brew) { - actions[HEAD_FOOD] = FOOD_EAT; - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->food_cooldown = 3; opp->potion_cooldown = 3; - } else if (hp_pct < opp->eat_double_threshold && cons.can_food && cons.can_karambwan) { - actions[HEAD_FOOD] = FOOD_EAT; - actions[HEAD_KARAMBWAN] = KARAM_EAT; - opp->food_cooldown = 3; opp->karambwan_cooldown = 2; + } else if (hp_pct < opp->eat_double_threshold && (cons.can_food || cons.can_karambwan)) { + opp_emit_preferred_food(opp, actions, self, cons); } else if (hp_pct < opp->eat_brew_threshold && cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; } else if (hp_pct < 0.60f && cons.can_food) { - actions[HEAD_FOOD] = FOOD_EAT; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); opp->food_cooldown = 3; } else if (hp_pct < 0.60f && cons.can_karambwan) { - actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_KARAMBWAN); opp->karambwan_cooldown = 2; } else if (opp_is_drained(self) && hp_pct < 0.90f && cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; } else if (opp_is_drained(self) && cons.can_restore) { - actions[HEAD_POTION] = POTION_RESTORE; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SUPER_RESTORE); opp->potion_cooldown = 3; } int eating = opp_check_eating_queued(actions); if (!self->veng_active && remaining_ticks(self->veng_cooldown) == 0) { - actions[HEAD_VENG] = VENG_CAST; + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_VENGEANCE; } if (opp_should_skip_offensive(env, opp)) return; @@ -1487,23 +1555,21 @@ static void opp_veng_fighter(OsrsEnv* env, OpponentState* opp, int* actions) { } if (should_spec) { - opp_apply_gear_switch(actions, OPP_STYLE_SPEC); - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_apply_gear_switch(actions, self, OPP_STYLE_SPEC); + opp_emit_attack(actions, 2); } else { - if (rand_float(env) < opp->offensive_prayer_miss) { - actions[HEAD_LOADOUT] = LOADOUT_KEEP; - } else { - opp_apply_gear_switch(actions, attack_style); + if (rand_float(env) >= opp->offensive_prayer_miss) { + opp_apply_gear_switch(actions, self, attack_style); } - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_emit_attack(actions, 2); } } else if (!opp_attack_ready(self)) { int dist = chebyshev_distance(self->x, self->y, target->x, target->y); if (target->frozen_ticks > 0 && self->frozen_ticks == 0 && dist > 0 && rand_float(env) < 0.40f) { - actions[HEAD_COMBAT] = MOVE_UNDER; + opp_emit_move_toward(actions, self, target->x, target->y); } else if (opp->target_fleeing_ticks >= 2 && dist > 3 && self->frozen_ticks == 0) { - actions[HEAD_COMBAT] = MOVE_FARCAST_3; + opp_emit_farcast_move(actions, self, target, 3); } } } @@ -1519,30 +1585,27 @@ static void opp_blood_healer(OsrsEnv* env, OpponentState* opp, int* actions) { opp_apply_defensive_prayer(env, opp, actions, self, target, 0); - if (hp_pct < 0.25f && cons.can_food && cons.can_brew && cons.can_karambwan) { - actions[HEAD_FOOD] = FOOD_EAT; - actions[HEAD_POTION] = POTION_BREW; - actions[HEAD_KARAMBWAN] = KARAM_EAT; - opp->food_cooldown = 3; opp->potion_cooldown = 3; opp->karambwan_cooldown = 2; + if (hp_pct < 0.25f && cons.can_brew && (cons.can_food || cons.can_karambwan)) { + opp_emit_preferred_food(opp, actions, self, cons); + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); + opp->potion_cooldown = 3; } else if (hp_pct < 0.35f && cons.can_food && cons.can_brew) { - actions[HEAD_FOOD] = FOOD_EAT; - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SHARK_FOOD); + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->food_cooldown = 3; opp->potion_cooldown = 3; - } else if (hp_pct < 0.35f && cons.can_food && cons.can_karambwan) { - actions[HEAD_FOOD] = FOOD_EAT; - actions[HEAD_KARAMBWAN] = KARAM_EAT; - opp->food_cooldown = 3; opp->karambwan_cooldown = 2; + } else if (hp_pct < 0.35f && (cons.can_food || cons.can_karambwan)) { + opp_emit_preferred_food(opp, actions, self, cons); } else if (hp_pct < 0.35f && cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; } else if (opp_is_drained(self) && hp_pct < 0.50f && cons.can_brew) { - actions[HEAD_POTION] = POTION_BREW; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_BREW); opp->potion_cooldown = 3; } else if (prayer_pct < 0.30f && cons.can_restore) { - actions[HEAD_POTION] = POTION_RESTORE; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SUPER_RESTORE); opp->potion_cooldown = 3; } else if (opp_is_drained(self) && cons.can_restore) { - actions[HEAD_POTION] = POTION_RESTORE; + opp_emit_consumable(actions, self, OSRS_CONSUMABLE_SUPER_RESTORE); opp->potion_cooldown = 3; } @@ -1590,26 +1653,25 @@ static void opp_blood_healer(OsrsEnv* env, OpponentState* opp, int* actions) { opp_apply_boost_potion(env, opp, actions, self, attack_style, 0); - if (rand_float(env) < opp->offensive_prayer_miss) { - actions[HEAD_LOADOUT] = LOADOUT_KEEP; - } else { - opp_apply_gear_switch(actions, attack_style); + if (rand_float(env) >= opp->offensive_prayer_miss) { + opp_apply_gear_switch(actions, self, attack_style); } if (hp_pct < 0.35f && actual_attack != 1) { - actions[HEAD_LOADOUT] = LOADOUT_TANK; + opp_apply_equipment_plan(actions, self, PVP_EQUIPMENT_TANK); + if (actual_attack == 0) opp_emit_attack(actions, actual_attack); + } else { + opp_emit_attack(actions, actual_attack); } - - opp_emit_attack(actions, actual_attack); } else if (!opp_attack_ready(self)) { int dist = chebyshev_distance(self->x, self->y, target->x, target->y); if (self->frozen_ticks == 0) { if (target->frozen_ticks > 0 && dist < 5) { - actions[HEAD_COMBAT] = MOVE_FARCAST_5; + opp_emit_farcast_move(actions, self, target, 5); } else if (dist < 4 && target->frozen_ticks == 0) { - actions[HEAD_COMBAT] = MOVE_FARCAST_5; + opp_emit_farcast_move(actions, self, target, 5); } else if (opp->target_fleeing_ticks >= 2 && dist > 5) { - actions[HEAD_COMBAT] = MOVE_FARCAST_5; + opp_emit_farcast_move(actions, self, target, 5); } } } @@ -1634,8 +1696,8 @@ static void opp_gmaul_combo(OsrsEnv* env, OpponentState* opp, int* actions) { if (opp_should_skip_offensive(env, opp)) return; if (opp->combo_state == COMBO_SPEC_FIRED && has_gmaul && !eating) { - actions[HEAD_LOADOUT] = LOADOUT_GMAUL; - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_apply_equipment_plan(actions, self, PVP_EQUIPMENT_GMAUL); + opp_emit_attack(actions, 2); opp->combo_state = COMBO_IDLE; return; } @@ -1667,12 +1729,12 @@ static void opp_gmaul_combo(OsrsEnv* env, OpponentState* opp, int* actions) { } if (should_combo) { - opp_apply_gear_switch(actions, OPP_STYLE_SPEC); - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_apply_gear_switch(actions, self, OPP_STYLE_SPEC); + opp_emit_attack(actions, 2); opp->combo_state = COMBO_SPEC_FIRED; } else if (should_ranged_spec) { - actions[HEAD_LOADOUT] = LOADOUT_SPEC_RANGE; - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_apply_equipment_plan(actions, self, PVP_EQUIPMENT_SPEC_RANGED); + opp_emit_attack(actions, 2); } else { int attack_style; if (rand_float(env) < opp->off_prayer_rate) { @@ -1689,21 +1751,19 @@ static void opp_gmaul_combo(OsrsEnv* env, OpponentState* opp, int* actions) { target_hp_pct < 0.50f && can_spec_range); if (should_regular_spec && opp->target_fleeing_ticks < 2) { - opp_apply_gear_switch(actions, OPP_STYLE_SPEC); - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_apply_gear_switch(actions, self, OPP_STYLE_SPEC); + opp_emit_attack(actions, 2); } else { - if (rand_float(env) < opp->offensive_prayer_miss) { - actions[HEAD_LOADOUT] = LOADOUT_KEEP; - } else { - opp_apply_gear_switch(actions, attack_style); + if (rand_float(env) >= opp->offensive_prayer_miss) { + opp_apply_gear_switch(actions, self, attack_style); } if (attack_style == OPP_STYLE_MAGE) { - actions[HEAD_COMBAT] = (target->frozen_ticks == 0 && - target->freeze_immunity_ticks == 0) - ? ATTACK_ICE : ATTACK_BLOOD; + int spell = target->frozen_ticks == 0 && + target->freeze_immunity_ticks == 0 ? 0 : 1; + opp_emit_attack(actions, spell); } else { - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_emit_attack(actions, 2); } } } @@ -1740,8 +1800,8 @@ static void opp_range_kiter(OsrsEnv* env, OpponentState* opp, int* actions) { target_hp_pct < 0.55f); if (should_ranged_spec && (target->frozen_ticks > 0 || dist >= 3)) { - actions[HEAD_LOADOUT] = LOADOUT_SPEC_RANGE; - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_apply_equipment_plan(actions, self, PVP_EQUIPMENT_SPEC_RANGED); + opp_emit_attack(actions, 2); } else { int attack_style; int force_melee = (self->frozen_ticks > 0 && dist <= 1); @@ -1776,13 +1836,11 @@ static void opp_range_kiter(OsrsEnv* env, OpponentState* opp, int* actions) { target->prayer != PRAYER_PROTECT_MELEE && dist <= 1 && self->frozen_ticks == 0); if (can_melee_spec && target_hp_pct < 0.40f && !has_ranged_spec) { - opp_apply_gear_switch(actions, OPP_STYLE_SPEC); - actions[HEAD_COMBAT] = ATTACK_ATK; + opp_apply_gear_switch(actions, self, OPP_STYLE_SPEC); + opp_emit_attack(actions, 2); } else { - if (rand_float(env) < opp->offensive_prayer_miss) { - actions[HEAD_LOADOUT] = LOADOUT_KEEP; - } else { - opp_apply_gear_switch(actions, attack_style); + if (rand_float(env) >= opp->offensive_prayer_miss) { + opp_apply_gear_switch(actions, self, attack_style); } opp_emit_attack(actions, actual_attack); @@ -1791,11 +1849,11 @@ static void opp_range_kiter(OsrsEnv* env, OpponentState* opp, int* actions) { } else if (!opp_attack_ready(self)) { if (self->frozen_ticks == 0) { if (target->frozen_ticks > 0 && dist < 5) { - actions[HEAD_COMBAT] = MOVE_FARCAST_5; + opp_emit_farcast_move(actions, self, target, 5); } else if (dist < 4) { - actions[HEAD_COMBAT] = MOVE_FARCAST_5; + opp_emit_farcast_move(actions, self, target, 5); } else if (dist > 7) { - actions[HEAD_COMBAT] = MOVE_FARCAST_5; + opp_emit_farcast_move(actions, self, target, 5); } } } @@ -1950,9 +2008,9 @@ static void opponent_reset(OsrsEnv* env, OpponentState* opp) { } static void generate_opponent_action(OsrsEnv* env, OpponentState* opp) { - int* actions = &env->pending_actions[1 * NUM_ACTION_HEADS]; + int* actions = env->pending_actions + OSRS_BASE_NUM_ACTION_HEADS; - memset(actions, 0, NUM_ACTION_HEADS * sizeof(int)); + memset(actions, 0, OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); opp_update_flee_tracking(opp, &env->players[1], &env->players[0]); @@ -2040,29 +2098,25 @@ static void generate_opponent_action(OsrsEnv* env, OpponentState* opp) { } } -static void swap_players_and_pending(OsrsEnv* env) { - Player tmp_player = env->players[0]; +static inline void pvp_swap_players_and_actions(OsrsEnv* env) { + Player player = env->players[0]; env->players[0] = env->players[1]; - env->players[1] = tmp_player; - - int tmp_actions[NUM_ACTION_HEADS]; - memcpy(tmp_actions, env->pending_actions, NUM_ACTION_HEADS * sizeof(int)); - memcpy( - env->pending_actions, - env->pending_actions + NUM_ACTION_HEADS, - NUM_ACTION_HEADS * sizeof(int) - ); - memcpy( - env->pending_actions + NUM_ACTION_HEADS, - tmp_actions, - NUM_ACTION_HEADS * sizeof(int) - ); -} - -static void generate_opponent_action_for_player0(OsrsEnv* env, OpponentState* opp) { - swap_players_and_pending(env); + env->players[1] = player; + for (int head = 0; head < OSRS_BASE_NUM_ACTION_HEADS; head++) { + int action = env->pending_actions[head]; + env->pending_actions[head] = + env->pending_actions[OSRS_BASE_NUM_ACTION_HEADS + head]; + env->pending_actions[OSRS_BASE_NUM_ACTION_HEADS + head] = action; + } +} + +static inline void generate_opponent_action_for_player0( + OsrsEnv* env, + OpponentState* opp +) { + pvp_swap_players_and_actions(env); generate_opponent_action(env, opp); - swap_players_and_pending(env); + pvp_swap_players_and_actions(env); } #endif /* OSRS_PVP_OPPONENTS_H */ diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h index b62cf8a693..ee58eaa3f7 100644 --- a/ocean/osrs/osrs_render.h +++ b/ocean/osrs/osrs_render.h @@ -6308,7 +6308,7 @@ void pvp_render(OsrsEnv* env) { for (int i = 0; i < OSRS_INVENTORY_SIZE && i < INV_GRID_SLOTS; i++) rc->gui.display_inventory_osrs_ids[i] = osrs_inventory_cell_raw_osrs_id( - &zul_inv->inventory_cells[i]); + &zul_inv->player.inventory_cells[i]); rc->gui.display_inventory_count = OSRS_INVENTORY_SIZE; } InfernoState* inf_inv = render_inferno_state_from_env(env); @@ -6316,7 +6316,7 @@ void pvp_render(OsrsEnv* env) { for (int i = 0; i < OSRS_INVENTORY_SIZE && i < INV_GRID_SLOTS; i++) rc->gui.display_inventory_osrs_ids[i] = osrs_inventory_cell_raw_osrs_id( - &inf_inv->inventory_cells[i]); + &inf_inv->player.inventory_cells[i]); rc->gui.display_inventory_count = OSRS_INVENTORY_SIZE; } } diff --git a/ocean/osrs/osrs_types.h b/ocean/osrs/osrs_types.h index 6002f6afa0..de6f25ea41 100644 --- a/ocean/osrs/osrs_types.h +++ b/ocean/osrs/osrs_types.h @@ -58,51 +58,18 @@ #define COMBAT_SPELL_ATTACK_RANGE 10 #define NUM_GEAR_SLOTS 11 +#define OSRS_INVENTORY_SIZE 28 + +typedef struct { + uint16_t content_code; +} OsrsInventoryCell; -#define NUM_ACTION_HEADS 9 - -#define HEAD_LOADOUT 0 -#define HEAD_COMBAT 1 -#define HEAD_OVERHEAD 2 -#define HEAD_FOOD 3 -#define HEAD_POTION 4 -#define HEAD_KARAMBWAN 5 -#define HEAD_VENG 6 -#define HEAD_OFFENSIVE 7 -#define HEAD_MOVE 8 - -#define LOADOUT_DIM 9 -#define COMBAT_DIM 13 -#define OVERHEAD_DIM 7 -#define FOOD_DIM 2 -#define POTION_DIM 5 -#define KARAMBWAN_DIM 2 -#define VENG_DIM 2 -#define OFFENSIVE_DIM 5 -#define MOVE_DIM 25 - -#define ACTION_MASK_SIZE (LOADOUT_DIM + COMBAT_DIM + OVERHEAD_DIM + \ - FOOD_DIM + POTION_DIM + KARAMBWAN_DIM + VENG_DIM + OFFENSIVE_DIM + MOVE_DIM) - -static const int ACTION_HEAD_DIMS[NUM_ACTION_HEADS] = { - LOADOUT_DIM, - COMBAT_DIM, - OVERHEAD_DIM, - FOOD_DIM, - POTION_DIM, - KARAMBWAN_DIM, - VENG_DIM, - OFFENSIVE_DIM, - MOVE_DIM, -}; #define NUM_ITEM_STATS 18 -#define MAX_ITEMS_PER_SLOT 10 #define NUM_DYNAMIC_GEAR_SLOTS 8 -#define SLOT_NUM_OBSERVATIONS 221 #define MAXED_BASE_ATTACK 99 #define MAXED_BASE_STRENGTH 99 @@ -235,35 +202,6 @@ static const int DYNAMIC_GEAR_SLOTS[NUM_DYNAMIC_GEAR_SLOTS] = { GEAR_SLOT_HEAD, GEAR_SLOT_CAPE, GEAR_SLOT_NECK, GEAR_SLOT_RING }; -typedef enum { - LOADOUT_KEEP = 0, - LOADOUT_MELEE, - LOADOUT_RANGE, - LOADOUT_MAGE, - LOADOUT_TANK, - LOADOUT_SPEC_MELEE, - LOADOUT_SPEC_RANGE, - LOADOUT_SPEC_MAGIC, - LOADOUT_GMAUL, -} LoadoutAction; - -#define ATTACK_NONE 0 -#define ATTACK_ATK 1 -#define ATTACK_ICE 2 -#define ATTACK_BLOOD 3 -#define MOVE_ADJACENT 4 -#define MOVE_UNDER 5 -#define MOVE_DIAGONAL 6 -#define MOVE_FARCAST_2 7 -#define MOVE_FARCAST_3 8 -#define MOVE_FARCAST_4 9 -#define MOVE_FARCAST_5 10 -#define MOVE_FARCAST_6 11 -#define MOVE_FARCAST_7 12 -#define MOVE_NONE ATTACK_NONE - -static inline int is_attack_action(int v) { return v >= ATTACK_ATK && v <= ATTACK_BLOOD; } -static inline int is_move_action(int v) { return v >= MOVE_ADJACENT && v <= MOVE_FARCAST_7; } typedef enum { OVERHEAD_NONE = 0, @@ -610,10 +548,8 @@ typedef struct { int bolt_ignores_defense; uint8_t equipped[NUM_GEAR_SLOTS]; + OsrsInventoryCell inventory_cells[OSRS_INVENTORY_SIZE]; - uint8_t inventory[NUM_GEAR_SLOTS][MAX_ITEMS_PER_SLOT]; - - uint8_t num_items_in_slot[NUM_GEAR_SLOTS]; GearBonuses slot_cached_bonuses; OsrsEquipmentEffectProfile equipment_effect_profile; @@ -844,7 +780,7 @@ typedef struct { int use_c_opponent; int use_c_opponent_p0; int use_external_opponent_actions; - int external_opponent_actions[NUM_ACTION_HEADS]; + int external_opponent_actions[32]; OpponentState opponent; OpponentState opponent_p0; PFSPState pfsp; @@ -852,6 +788,7 @@ typedef struct { int walk_dest_x[NUM_AGENTS]; int walk_dest_y[NUM_AGENTS]; + int initial_supply_units[NUM_AGENTS]; } OsrsPvpRuntime; typedef struct { @@ -863,17 +800,13 @@ typedef struct { unsigned char* agent_terminals; } OsrsOceanBuffers; -#define OCEAN_OBS_SIZE (SLOT_NUM_OBSERVATIONS + ACTION_MASK_SIZE) typedef struct { Log log; - float* observations; int* actions; float* rewards; unsigned char* terminals; - unsigned char* action_masks; - unsigned char action_masks_agents; int num_agents; Player players[NUM_AGENTS]; @@ -892,8 +825,8 @@ typedef struct { uint32_t rng_reset_count; int has_rng_seed; - int pending_actions[NUM_AGENTS * NUM_ACTION_HEADS]; - int last_executed_actions[NUM_AGENTS * NUM_ACTION_HEADS]; + int pending_actions[NUM_AGENTS * 32]; + int last_executed_actions[NUM_AGENTS * 32]; RewardShapingConfig shaping; @@ -910,11 +843,9 @@ typedef struct { OsrsOceanBuffers ocean_io; float _episode_return; - float _obs_buf[NUM_AGENTS * SLOT_NUM_OBSERVATIONS]; - int _acts_buf[NUM_AGENTS * NUM_ACTION_HEADS]; + int _acts_buf[NUM_AGENTS * 32]; float _rews_buf[NUM_AGENTS]; unsigned char _terms_buf[NUM_AGENTS]; - unsigned char _masks_buf[NUM_AGENTS * ACTION_MASK_SIZE]; } OsrsEnv; diff --git a/ocean/osrs/osrs_venator.h b/ocean/osrs/osrs_venator.h index 94d634afd0..f01582bfad 100644 --- a/ocean/osrs/osrs_venator.h +++ b/ocean/osrs/osrs_venator.h @@ -364,8 +364,14 @@ static inline int osrs_venator_can_bounce( OsrsVenatorFootprint sender, OsrsVenatorFootprint target ) { - return osrs_venator_accepts_bounce(sender, target) && - osrs_venator_sends_bounce(sender, target); + OsrsVenatorTileSet origins = + osrs_venator_sender_origin_tiles(sender); + return osrs_venator_origins_find_required_tiles( + origins, + osrs_venator_accept_required_tiles(sender, target)) && + osrs_venator_origins_find_required_tiles( + origins, + osrs_venator_send_required_tiles(sender, target)); } static inline int osrs_venator_selection_distance( diff --git a/ocean/osrs/osrs_visual.c b/ocean/osrs/osrs_visual.c index 844568b669..7e179903f7 100644 --- a/ocean/osrs/osrs_visual.c +++ b/ocean/osrs/osrs_visual.c @@ -26,51 +26,178 @@ #include "puffercpu.h" #include "osrs_visual_net.h" -_Static_assert(COLO_ENT_INF_OBS_FEATS == COLO_FEATURES_PER_NPC, - "entity encoder per-NPC OBSERVATION width must track the colosseum layout"); +_Static_assert(OSRS_SHARED_OBS_SIZE == 101, + "entity encoder shared observation prefix"); +_Static_assert(OSRS_ENT_INV_START == OSRS_SHARED_OBS_INVENTORY_START, + "entity encoder inventory offset"); +_Static_assert(OSRS_ENT_INV_NUM_RECS == OSRS_INVENTORY_SIZE, + "entity encoder inventory record count"); +_Static_assert(OSRS_ENT_INV_OBS_FEATS == OSRS_SHARED_INVENTORY_CELL_OBS_FEATURES, + "entity encoder inventory observation record width"); _Static_assert( - COLO_ENT_INF_FEATS == - COLO_ENT_INF_TYPE_ONEHOT + (COLO_FEATURES_PER_NPC - COLO_NPC_TYPE_CODE_FEATURES), - "encoder record width is the obs record with the type code expanded to a one-hot"); -_Static_assert(COLO_ENT_INF_NUM_NPCS == COLO_OBS_NPCS, - "entity encoder NPC count must track the colosseum observation layout"); -_Static_assert(COLO_ENT_INF_NPC_START == COLO_OBS_AFTER_EQUIPPED_SELF, - "entity encoder NPC block offset must track the colosseum observation layout"); -_Static_assert(COLO_ENT_INF_TYPE_ONEHOT == COLO_NUM_NPC_TYPES, - "entity pool presence mask is the NPC type one-hot; a new NPC type must widen it"); -_Static_assert(COLO_ENT_INF_INV_START == COLO_OBS_AFTER_PLAYER, - "entity encoder inventory block offset must track the colosseum observation layout"); -_Static_assert(COLO_ENT_INF_INV_NUM_CELLS == COLO_INVENTORY_DISPLAY_SLOTS, - "entity encoder inventory cell count must track the colosseum observation layout"); -_Static_assert(COLO_ENT_INF_INV_OBS_FEATS == COLO_INVENTORY_CELL_OBS_FEATURES, - "entity encoder inventory cell OBSERVATION width must track the colosseum layout"); -_Static_assert(COLO_ENT_INF_INV_FEATS == COLO_INVENTORY_CELL_ENCODER_FEATURES, - "encoder inventory record is one item table row with the observed floats added in"); + OSRS_ENT_INV_START + OSRS_ENT_INV_NUM_RECS * OSRS_ENT_INV_OBS_FEATS == + OSRS_SHARED_OBS_EQUIPPED_START, + "entity encoder inventory boundary"); +_Static_assert(OSRS_ENT_EQUIPPED_START == OSRS_SHARED_OBS_EQUIPPED_START, + "entity encoder equipped offset"); +_Static_assert(OSRS_ENT_EQUIPPED_NUM_RECS == NUM_GEAR_SLOTS, + "entity encoder equipped record count"); +_Static_assert( + OSRS_ENT_EQUIPPED_START + + OSRS_ENT_EQUIPPED_NUM_RECS * OSRS_ENT_EQUIPPED_OBS_FEATS == + OSRS_SHARED_OBS_EFFECT_START, + "entity encoder equipped boundary"); +_Static_assert(OSRS_ENT_ITEM_FEATS == OSRS_ITEM_OBS_TABLE_COLS, + "entity encoder expanded item record width"); +_Static_assert(OSRS_ITEM_CONTENT_COUNT <= OSRS_ITEM_OBS_CODE_SCALE, + "entity encoder item code scale"); + +_Static_assert(COLO_ENT_OBS_SIZE == COLO_NUM_OBS, + "Colosseum entity encoder observation width"); +_Static_assert(COLO_ENT_NPC_START == COLO_OBS_AFTER_SHARED, + "Colosseum entity encoder NPC offset"); +_Static_assert(COLO_ENT_NPC_NUM_RECS == COLO_OBS_NPCS, + "Colosseum entity encoder NPC count"); +_Static_assert(COLO_ENT_NPC_OBS_FEATS == COLO_FEATURES_PER_NPC, + "Colosseum entity encoder NPC observation record width"); +_Static_assert( + COLO_ENT_NPC_START + + COLO_ENT_NPC_NUM_RECS * COLO_ENT_NPC_OBS_FEATS == + COLO_OBS_AFTER_NPCS, + "Colosseum entity encoder NPC boundary"); +_Static_assert( + COLO_ENT_NPC_FEATS == + COLO_ENT_NPC_TYPE_ONEHOT + + (COLO_FEATURES_PER_NPC - COLO_NPC_TYPE_CODE_FEATURES), + "Colosseum entity encoder expanded NPC record width"); +_Static_assert(COLO_ENT_NPC_TYPE_ONEHOT == COLO_NUM_NPC_TYPES, + "Colosseum entity encoder NPC type width"); _Static_assert(INF_ENT_OBS_SIZE == INF_NUM_OBS, - "entity encoder global projection width must track the inferno observation layout"); -_Static_assert(INF_ENT_OBS_FEATS == INF_NPC_SLOT_FEATURES, - "entity encoder per-NPC observation width must track the inferno layout"); -_Static_assert( - INF_ENT_FEATS == INF_ENT_TYPE_ONEHOT + (INF_NPC_SLOT_FEATURES - 1), - "encoder record width is the obs record with the type code expanded to a one-hot"); -_Static_assert(INF_ENT_NUM_NPCS == INF_OBS_NPCS, - "entity encoder NPC count must track the inferno observation layout"); + "Inferno entity encoder observation width"); _Static_assert(INF_ENT_NPC_START == INF_OBS_AFTER_PILLARS, - "entity encoder NPC block offset must track the inferno observation layout"); -_Static_assert(INF_ENT_TYPE_ONEHOT == INF_NUM_NPC_TYPES, - "entity pool presence mask is the NPC type one-hot; a new NPC type must widen it"); -_Static_assert(INF_ENT_TYPE_CODE_SCALE == (int)INF_NPC_TYPE_CODE_SCALE, - "entity encoder NPC type decoder must track the inferno observation scale"); -_Static_assert(INF_ENT_INV_START == INF_OBS_AFTER_SPARKS, - "entity encoder inventory block offset must track the inferno observation layout"); -_Static_assert(INF_ENT_INV_NUM_CELLS == OSRS_INVENTORY_SIZE, - "entity encoder inventory cell count must track the inferno observation layout"); + "Inferno entity encoder NPC offset"); +_Static_assert(INF_ENT_NPC_NUM_RECS == INF_OBS_NPCS, + "Inferno entity encoder NPC count"); +_Static_assert(INF_ENT_NPC_OBS_FEATS == INF_NPC_SLOT_FEATURES, + "Inferno entity encoder NPC observation record width"); +_Static_assert( + INF_ENT_NPC_START + + INF_ENT_NPC_NUM_RECS * INF_ENT_NPC_OBS_FEATS == + INF_OBS_AFTER_NPCS, + "Inferno entity encoder NPC boundary"); _Static_assert( - INF_ENT_INV_NUM_CELLS * INF_ENT_INV_OBS_FEATS == INF_INVENTORY_OBS_SIZE, - "entity encoder inventory observation width must track the inferno layout"); -_Static_assert(INF_ENT_INV_FEATS == OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT, - "encoder inventory record must track the generated item table width"); + INF_ENT_NPC_FEATS == + INF_ENT_NPC_TYPE_ONEHOT + (INF_NPC_SLOT_FEATURES - 1), + "Inferno entity encoder expanded NPC record width"); +_Static_assert(INF_ENT_NPC_TYPE_ONEHOT == INF_NUM_NPC_TYPES, + "Inferno entity encoder NPC type width"); +_Static_assert(INF_ENT_NPC_TYPE_SCALE == (int)INF_NPC_TYPE_CODE_SCALE, + "Inferno entity encoder NPC type scale"); + +#define VISUAL_SHARED_ITEM_BRANCH(name, offset, count) { \ + .weight_name = name, \ + .start = offset, \ + .num_recs = count, \ + .feats = OSRS_ENT_ITEM_FEATS, \ + .obs_feats = 1, \ + .type_onehot = 0, \ + .code_scale = OSRS_ITEM_OBS_CODE_SCALE, \ + .bottleneck = OSRS_ENT_ITEM_BOTTLENECK, \ + .active_width = 1, \ + .expansion = ENTITY_RECORD_ITEM_TABLE, \ +} + +static const EntityEncoderDescriptor VISUAL_ENTITY_ENCODER_DESCRIPTORS[] = { + { + .env_name = "colosseum", + .obs_size = COLO_ENT_OBS_SIZE, + .num_branches = 3, + .branches = { + VISUAL_SHARED_ITEM_BRANCH( + "inventory", OSRS_ENT_INV_START, OSRS_ENT_INV_NUM_RECS), + VISUAL_SHARED_ITEM_BRANCH( + "equipped", OSRS_ENT_EQUIPPED_START, OSRS_ENT_EQUIPPED_NUM_RECS), + { + .weight_name = "npc", + .start = COLO_ENT_NPC_START, + .num_recs = COLO_ENT_NPC_NUM_RECS, + .feats = COLO_ENT_NPC_FEATS, + .obs_feats = COLO_ENT_NPC_OBS_FEATS, + .type_onehot = COLO_ENT_NPC_TYPE_ONEHOT, + .code_scale = COLO_ENT_NPC_TYPE_SCALE, + .bottleneck = COLO_ENT_NPC_BOTTLENECK, + .active_width = COLO_ENT_NPC_TYPE_ONEHOT, + .expansion = ENTITY_RECORD_TYPE_ONEHOT, + }, + }, + }, + { + .env_name = "inferno", + .obs_size = INF_ENT_OBS_SIZE, + .num_branches = 3, + .branches = { + VISUAL_SHARED_ITEM_BRANCH( + "inventory", OSRS_ENT_INV_START, OSRS_ENT_INV_NUM_RECS), + VISUAL_SHARED_ITEM_BRANCH( + "equipped", OSRS_ENT_EQUIPPED_START, OSRS_ENT_EQUIPPED_NUM_RECS), + { + .weight_name = "npc", + .start = INF_ENT_NPC_START, + .num_recs = INF_ENT_NPC_NUM_RECS, + .feats = INF_ENT_NPC_FEATS, + .obs_feats = INF_ENT_NPC_OBS_FEATS, + .type_onehot = INF_ENT_NPC_TYPE_ONEHOT, + .code_scale = INF_ENT_NPC_TYPE_SCALE, + .bottleneck = INF_ENT_NPC_BOTTLENECK, + .active_width = INF_ENT_NPC_TYPE_ONEHOT, + .expansion = ENTITY_RECORD_TYPE_ONEHOT, + }, + }, + }, + { + .env_name = "zulrah", + .obs_size = ZUL_NUM_OBS, + .num_branches = 2, + .branches = { + VISUAL_SHARED_ITEM_BRANCH( + "inventory", OSRS_ENT_INV_START, OSRS_ENT_INV_NUM_RECS), + VISUAL_SHARED_ITEM_BRANCH( + "equipped", OSRS_ENT_EQUIPPED_START, OSRS_ENT_EQUIPPED_NUM_RECS), + }, + }, + { + .env_name = "nh_pvp", + .obs_size = 133, + .num_branches = 2, + .branches = { + VISUAL_SHARED_ITEM_BRANCH( + "inventory", OSRS_ENT_INV_START, OSRS_ENT_INV_NUM_RECS), + VISUAL_SHARED_ITEM_BRANCH( + "equipped", OSRS_ENT_EQUIPPED_START, OSRS_ENT_EQUIPPED_NUM_RECS), + }, + }, +}; + +static const EntityEncoderDescriptor* visual_policy_entity_descriptor( + const EncounterDef* edef) { + for (int i = 0; + i < (int)(sizeof(VISUAL_ENTITY_ENCODER_DESCRIPTORS) / + sizeof(VISUAL_ENTITY_ENCODER_DESCRIPTORS[0])); + i++) { + const EntityEncoderDescriptor* descriptor = + &VISUAL_ENTITY_ENCODER_DESCRIPTORS[i]; + if (strcmp(edef->name, descriptor->env_name) != 0) continue; + if (descriptor->obs_size > 0 && descriptor->obs_size != edef->obs_size) { + fprintf(stderr, + "policy: %s observation width %d != descriptor width %d\n", + edef->name, edef->obs_size, descriptor->obs_size); + abort(); + } + return descriptor; + } + return NULL; +} static void visual_require_gui_item_sprite(int raw_osrs_id, void* ctx) { gui_require_sprite_by_osrs_id((GuiState*)ctx, raw_osrs_id); @@ -110,9 +237,10 @@ static void run_random_episode(OsrsEnv* env, int verbose) { while (!env->episode_over) { for (int agent = 0; agent < NUM_AGENTS; agent++) { - int* actions = env->actions + agent * NUM_ACTION_HEADS; - for (int h = 0; h < NUM_ACTION_HEADS; h++) { - actions[h] = rand() % ACTION_HEAD_DIMS[h]; + int* actions = + env->actions + agent * OSRS_BASE_NUM_ACTION_HEADS; + for (int h = 0; h < OSRS_BASE_NUM_ACTION_HEADS; h++) { + actions[h] = rand() % NH_PVP_ACTION_DIMS[h]; } } @@ -149,9 +277,10 @@ static void benchmark(OsrsEnv* env, int num_steps) { while (!env->episode_over && total_steps < num_steps) { for (int agent = 0; agent < NUM_AGENTS; agent++) { - int* actions = env->actions + agent * NUM_ACTION_HEADS; - for (int h = 0; h < NUM_ACTION_HEADS; h++) { - actions[h] = rand() % ACTION_HEAD_DIMS[h]; + int* actions = + env->actions + agent * OSRS_BASE_NUM_ACTION_HEADS; + for (int h = 0; h < OSRS_BASE_NUM_ACTION_HEADS; h++) { + actions[h] = rand() % NH_PVP_ACTION_DIMS[h]; } } @@ -568,9 +697,10 @@ static void run_profile( #endif } else { for (int agent = 0; agent < NUM_AGENTS; agent++) { - int* actions = env->actions + agent * NUM_ACTION_HEADS; - for (int h = 0; h < NUM_ACTION_HEADS; h++) { - actions[h] = rand() % ACTION_HEAD_DIMS[h]; + int* actions = + env->actions + agent * OSRS_BASE_NUM_ACTION_HEADS; + for (int h = 0; h < OSRS_BASE_NUM_ACTION_HEADS; h++) { + actions[h] = rand() % NH_PVP_ACTION_DIMS[h]; } } pvp_step(env, direct_pvp_topology, direct_pvp_route_cache); @@ -591,6 +721,40 @@ static void run_profile( printf(" Total steps: %d\n", total_steps); printf(" Time: %.3f seconds\n", elapsed); printf(" Steps/sec: %.0f\n", total_steps / elapsed); +#ifdef OSRS_ROUTE_PROBE + printf( + " Route probes: tile=%llu cardinal=%llu attack=%llu direct=%llu overlap=%llu try_direct=%llu source=%llu reverse=%llu bfs=%llu nodes=%llu\n", + (unsigned long long)osrs_route_probe_calls[0], + (unsigned long long)osrs_route_probe_calls[1], + (unsigned long long)osrs_route_probe_calls[2], + (unsigned long long)osrs_route_probe_direct, + (unsigned long long)osrs_route_probe_overlap, + (unsigned long long)osrs_route_probe_try_direct, + (unsigned long long)osrs_route_probe_source, + (unsigned long long)osrs_route_probe_reverse, + (unsigned long long)osrs_route_probe_bfs, + (unsigned long long)osrs_route_probe_nodes); + printf( + " Source fields: builds=%llu nodes=%llu\n", + (unsigned long long)osrs_route_probe_source_builds, + (unsigned long long)osrs_route_probe_source_nodes); + printf( + " Route cost calls: osrs=%llu south=%llu direct=%llu south_bfs=%llu reverse=%llu target_bfs=%llu\n", + (unsigned long long)osrs_route_probe_cost_calls[0], + (unsigned long long)osrs_route_probe_cost_calls[1], + (unsigned long long)osrs_route_probe_cost_calls[2], + (unsigned long long)osrs_route_probe_cost_calls[3], + (unsigned long long)osrs_route_probe_cost_calls[4], + (unsigned long long)osrs_route_probe_cost_calls[5]); + printf( + " Route cost BFS: osrs=%llu south=%llu direct=%llu south_bfs=%llu reverse=%llu target_bfs=%llu\n", + (unsigned long long)osrs_route_probe_cost_bfs[0], + (unsigned long long)osrs_route_probe_cost_bfs[1], + (unsigned long long)osrs_route_probe_cost_bfs[2], + (unsigned long long)osrs_route_probe_cost_bfs[3], + (unsigned long long)osrs_route_probe_cost_bfs[4], + (unsigned long long)osrs_route_probe_cost_bfs[5]); +#endif #ifdef COLO_PROFILE_ENABLED if (encounter_name && strcmp(encounter_name, "colosseum") == 0) osrs_print_colosseum_profile_results(total_steps); @@ -753,36 +917,9 @@ static int visual_policy_is_continuous( return 1; } -typedef enum { - VISUAL_ENTITY_ARCH_NONE = 0, - VISUAL_ENTITY_ARCH_COLOSSEUM = 1, - VISUAL_ENTITY_ARCH_INFERNO = 2, -} VisualEntityArch; - -static VisualEntityArch visual_policy_entity_arch(const EncounterDef* edef) { - if (strcmp(edef->name, "colosseum") == 0) return VISUAL_ENTITY_ARCH_COLOSSEUM; - if (strcmp(edef->name, "inferno") == 0) return VISUAL_ENTITY_ARCH_INFERNO; - return VISUAL_ENTITY_ARCH_NONE; -} - -static int visual_policy_entity_feats(VisualEntityArch entity_arch) { - switch (entity_arch) { - case VISUAL_ENTITY_ARCH_COLOSSEUM: return COLO_ENT_INF_FEATS; - case VISUAL_ENTITY_ARCH_INFERNO: return INF_ENT_FEATS; - case VISUAL_ENTITY_ARCH_NONE: break; - } - fprintf(stderr, "policy: entity feats requested without an entity arch\n"); - abort(); -} - -static int visual_policy_inv_feats(VisualEntityArch entity_arch) { - switch (entity_arch) { - case VISUAL_ENTITY_ARCH_COLOSSEUM: return COLO_ENT_INF_INV_FEATS; - case VISUAL_ENTITY_ARCH_INFERNO: return INF_ENT_INV_FEATS; - case VISUAL_ENTITY_ARCH_NONE: break; - } - fprintf(stderr, "policy: inv feats requested without an entity arch\n"); - abort(); +static int64_t visual_policy_advance_weight_offset( + int64_t offset, int64_t count) { + return (offset + count + 7) & ~(int64_t)7; } static int64_t visual_policy_expected_weight_count( @@ -793,28 +930,34 @@ static int64_t visual_policy_expected_weight_count( int num_action_heads, int decoder_value_heads, int entity_encoder, - VisualEntityArch entity_arch + const EntityEncoderDescriptor* descriptor ) { int action_sum = 0; for (int h = 0; h < num_action_heads; h++) { action_sum += action_dims[h]; } - int64_t total = 0; - total += (int64_t)hidden_size * input_size; - if (entity_encoder >= 1) { - total += (int64_t)COLO_ENT_INF_BOTTLENECK * visual_policy_entity_feats(entity_arch); - total += (int64_t)hidden_size * COLO_ENT_INF_BOTTLENECK; - } - if (entity_encoder >= 2) { - total += (int64_t)COLO_ENT_INF_INV_BOTTLENECK * visual_policy_inv_feats(entity_arch); - total += (int64_t)hidden_size * COLO_ENT_INF_INV_BOTTLENECK; + int64_t total = visual_policy_advance_weight_offset( + 0, (int64_t)hidden_size * input_size); + if (entity_encoder) { + if (!descriptor) return -1; + for (int i = 0; i < descriptor->num_branches; i++) { + const EntityPoolDescriptor* branch = &descriptor->branches[i]; + total = visual_policy_advance_weight_offset( + total, (int64_t)branch->bottleneck * branch->feats); + total = visual_policy_advance_weight_offset( + total, (int64_t)hidden_size * branch->bottleneck); + } } - total += (int64_t)(action_sum + decoder_value_heads) * hidden_size; + total = visual_policy_advance_weight_offset( + total, (int64_t)(action_sum + decoder_value_heads) * hidden_size); if (visual_policy_is_continuous(action_dims, num_action_heads)) { - total += num_action_heads; + total = visual_policy_advance_weight_offset(total, num_action_heads); + } + for (int i = 0; i < num_layers; i++) { + total = visual_policy_advance_weight_offset( + total, (int64_t)3 * hidden_size * hidden_size); } - total += (int64_t)num_layers * 3 * hidden_size * hidden_size; return total; } @@ -825,6 +968,7 @@ static int64_t visual_policy_file_weight_count(const Weights* weights) { static VisualPolicyModelShape visual_policy_select_model_shape( const VisualPolicy* policy, const EncounterDef* edef, + const EntityEncoderDescriptor* descriptor, int cli_hidden_size, int cli_num_layers, int cli_entity_encoder @@ -833,8 +977,7 @@ static VisualPolicyModelShape visual_policy_select_model_shape( int obs_input_size = policy->obs_size; int full_input_size = policy->obs_size + policy->mask_size; int64_t file_weights = visual_policy_file_weight_count(policy->weights); - VisualEntityArch entity_arch = visual_policy_entity_arch(edef); - int enc_max = entity_arch == VISUAL_ENTITY_ARCH_NONE ? 0 : 2; + int enc_max = descriptor ? 1 : 0; VisualPolicyModelShape match = {0}; int matches = 0; @@ -848,9 +991,10 @@ static VisualPolicyModelShape visual_policy_select_model_shape( for (int value_heads = 0; value_heads <= 1; value_heads++) { for (int variant = 0; variant <= 1; variant++) { int input_size = variant ? full_input_size : obs_input_size; + if (enc && input_size != edef->obs_size) continue; int64_t expected = visual_policy_expected_weight_count( input_size, hs, layers, policy->action_dims, - policy->num_action_heads, value_heads, enc, entity_arch); + policy->num_action_heads, value_heads, enc, descriptor); if (expected != file_weights) continue; match = (VisualPolicyModelShape){ input_size, value_heads, enc, hs, layers}; @@ -889,7 +1033,7 @@ static VisualPolicyModelShape visual_policy_select_model_shape( static int64_t visual_policy_layout_tensor(const char* name, int64_t off, int64_t count) { int64_t start = off; - int64_t end = (off + count + 7) & ~(int64_t)7; + int64_t end = visual_policy_advance_weight_offset(off, count); fprintf(stderr, "policy: tensor %-16s [%lld, %lld) (%lld floats)\n", name, (long long)start, (long long)end, (long long)count); return end; @@ -914,7 +1058,7 @@ static VisualNet* visual_policy_make_puffernet( int num_action_heads, int decoder_value_heads, int entity_encoder, - VisualEntityArch entity_arch + const EntityEncoderDescriptor* descriptor ) { VisualNet* net = (VisualNet*)calloc(1, sizeof(VisualNet)); if (!net) { @@ -941,23 +1085,27 @@ static VisualNet* visual_policy_make_puffernet( net->is_continuous = is_continuous; net->num_actions = num_action_heads; - /* Expected .bin layout in policy_weights_create order (src/algo.cu): encoder - tensor(s), decoder weight, optional logstd, then the MinGRU projections. */ int64_t off = 0; if (entity_encoder) { - off = visual_policy_layout_tensor("enc.global_w", off, (int64_t)hidden_dim * input_dim); - off = visual_policy_layout_tensor("enc.entity_l1_w", off, - (int64_t)COLO_ENT_INF_BOTTLENECK * visual_policy_entity_feats(entity_arch)); - off = visual_policy_layout_tensor("enc.entity_l2_w", off, - (int64_t)hidden_dim * COLO_ENT_INF_BOTTLENECK); - if (entity_encoder >= 2) { - off = visual_policy_layout_tensor("enc.inv_l1_w", off, - (int64_t)COLO_ENT_INF_INV_BOTTLENECK * visual_policy_inv_feats(entity_arch)); - off = visual_policy_layout_tensor("enc.inv_l2_w", off, - (int64_t)hidden_dim * COLO_ENT_INF_INV_BOTTLENECK); + if (!descriptor) { + fprintf(stderr, "policy: entity encoder resolved without a descriptor\n"); + abort(); + } + off = visual_policy_layout_tensor( + "enc.global_w", off, (int64_t)hidden_dim * input_dim); + for (int i = 0; i < descriptor->num_branches; i++) { + const EntityPoolDescriptor* branch = &descriptor->branches[i]; + char name[48]; + snprintf(name, sizeof(name), "enc.%s_l1_w", branch->weight_name); + off = visual_policy_layout_tensor( + name, off, (int64_t)branch->bottleneck * branch->feats); + snprintf(name, sizeof(name), "enc.%s_l2_w", branch->weight_name); + off = visual_policy_layout_tensor( + name, off, (int64_t)hidden_dim * branch->bottleneck); } } else { - off = visual_policy_layout_tensor("enc.weight", off, (int64_t)hidden_dim * input_dim); + off = visual_policy_layout_tensor( + "enc.weight", off, (int64_t)hidden_dim * input_dim); } int64_t off_after_encoder = off; off = visual_policy_layout_tensor("decoder.weight", off, @@ -975,19 +1123,8 @@ static VisualNet* visual_policy_make_puffernet( int64_t off_total = off; if (entity_encoder) { - switch (entity_arch) { - case VISUAL_ENTITY_ARCH_COLOSSEUM: - net->entity_encoder = make_colosseum_entity_encoder( - weights, 1, input_dim, hidden_dim, entity_encoder); - break; - case VISUAL_ENTITY_ARCH_INFERNO: - net->entity_encoder = make_inferno_entity_encoder( - weights, 1, input_dim, hidden_dim, entity_encoder); - break; - case VISUAL_ENTITY_ARCH_NONE: - fprintf(stderr, "policy: entity encoder resolved without an entity arch\n"); - abort(); - } + net->entity_encoder = make_entity_encoder( + weights, 1, input_dim, hidden_dim, descriptor); } else { net->encoder = make_linear(weights, 1, input_dim, hidden_dim); } @@ -1065,13 +1202,16 @@ static void visual_policy_init( for (int h = 0; h < edef->num_action_heads; h++) { policy->action_dims[h] = edef->action_head_dims[h]; } + const EntityEncoderDescriptor* descriptor = + visual_policy_entity_descriptor(edef); policy->weights = load_weights(model_path); if (!policy->weights) { fprintf(stderr, "policy: failed to load model: %s\n", model_path); abort(); } VisualPolicyModelShape model_shape = visual_policy_select_model_shape( - policy, edef, cli_hidden_size, cli_num_layers, cli_entity_encoder); + policy, edef, descriptor, + cli_hidden_size, cli_num_layers, cli_entity_encoder); fprintf(stderr, "policy: %s arch resolved hs=%d layers=%d entity=%d input=%d value_heads=%d\n", edef->name, model_shape.hidden_size, model_shape.num_layers, @@ -1086,7 +1226,7 @@ static void visual_policy_init( policy->num_action_heads, model_shape.decoder_value_heads, model_shape.entity_encoder, - visual_policy_entity_arch(edef)); + descriptor); int64_t file_weights = visual_policy_file_weight_count(policy->weights); if (policy->weights->idx != file_weights) { fprintf(stderr, @@ -1219,7 +1359,6 @@ static void run_policy_profile( uint32_t policy_seed, int loadout_mode ) { - if (!encounter_name || strcmp(encounter_name, "colosseum") != 0) abort(); if (profile_steps <= 0) { fprintf(stderr, "policy profile requires --profile-steps > 0\n"); abort(); @@ -1227,11 +1366,16 @@ static void run_policy_profile( const EncounterDef* edef = visual_open_encounter(env, encounter_name); if (!edef) abort(); visual_load_encounter_collision_map(edef, env, encounter_name); - if (start_wave >= 0) + if (strcmp(encounter_name, "colosseum") == 0) { + if (start_wave >= 0) + edef->put_int(env->encounter_state, env->encounter_context, + "start_wave", start_wave); + edef->put_int(env->encounter_state, env->encounter_context, + "loadout_profile_mode", loadout_mode); + } else if (strcmp(encounter_name, "inferno") == 0 && start_wave >= 0) { edef->put_int(env->encounter_state, env->encounter_context, "start_wave", start_wave); - edef->put_int(env->encounter_state, env->encounter_context, - "loadout_profile_mode", loadout_mode); + } visual_finalize_encounter(edef, env); edef->reset(env->encounter_state, env->encounter_context, policy_seed); @@ -1574,18 +1718,19 @@ static void visual_frame(void* arg) { } else { if (rc->human_input.enabled) { - human_to_pvp_actions(&rc->human_input, - env->actions, &env->players[0], &env->players[1]); - int* opp = env->actions + NUM_ACTION_HEADS; - for (int h = 0; h < NUM_ACTION_HEADS; h++) { - opp[h] = rand() % ACTION_HEAD_DIMS[h]; + human_to_pvp_actions( + &rc->human_input, env->actions, &env->players[0]); + int* opp = env->actions + OSRS_BASE_NUM_ACTION_HEADS; + for (int h = 0; h < OSRS_BASE_NUM_ACTION_HEADS; h++) { + opp[h] = rand() % NH_PVP_ACTION_DIMS[h]; } human_input_clear_pending(&rc->human_input); } else { for (int agent = 0; agent < NUM_AGENTS; agent++) { - int* actions = env->actions + agent * NUM_ACTION_HEADS; - for (int h = 0; h < NUM_ACTION_HEADS; h++) { - actions[h] = rand() % ACTION_HEAD_DIMS[h]; + int* actions = + env->actions + agent * OSRS_BASE_NUM_ACTION_HEADS; + for (int h = 0; h < OSRS_BASE_NUM_ACTION_HEADS; h++) { + actions[h] = rand() % NH_PVP_ACTION_DIMS[h]; } } } @@ -2254,28 +2399,13 @@ static void run_visual( } #endif -static void visual_alloc_env_buffers(OsrsEnv* env) { - env->observations = (float*)calloc(NUM_AGENTS * SLOT_NUM_OBSERVATIONS, sizeof(float)); - env->actions = (int*)calloc(NUM_AGENTS * NUM_ACTION_HEADS, sizeof(int)); - env->rewards = (float*)calloc(NUM_AGENTS, sizeof(float)); - env->terminals = (unsigned char*)calloc(NUM_AGENTS, sizeof(unsigned char)); - env->action_masks = (unsigned char*)calloc(NUM_AGENTS * ACTION_MASK_SIZE, sizeof(unsigned char)); - env->action_masks_agents = (1 << NUM_AGENTS) - 1; +static void visual_init_env_buffers(OsrsEnv* env) { + pvp_init(env); env->ocean_io.agent_actions = env->actions; - env->ocean_io.agent_obs = (float*)calloc(OCEAN_OBS_SIZE, sizeof(float)); env->ocean_io.agent_rewards = env->rewards; env->ocean_io.agent_terminals = env->terminals; } -static void visual_free_env_buffers(OsrsEnv* env) { - free(env->observations); - free(env->actions); - free(env->rewards); - free(env->terminals); - free(env->action_masks); - free(env->ocean_io.agent_obs); -} - int main(int argc, char** argv) { int use_visual = 1; int use_profile = 0; @@ -2372,7 +2502,7 @@ int main(int argc, char** argv) { } if (use_profile) { - visual_alloc_env_buffers(&env); + visual_init_env_buffers(&env); if (model_path && model_path[0]) { run_policy_profile(&env, encounter_name, start_wave, profile_steps, @@ -2382,14 +2512,13 @@ int main(int argc, char** argv) { &env, encounter_name, start_wave, profile_steps, policy_seed); } - visual_free_env_buffers(&env); pvp_close(&env); return 0; } if (use_visual) { #ifdef OSRS_VISUAL - pvp_init(&env); + visual_init_env_buffers(&env); if (gear_tier >= 0 && gear_tier <= 3) { for (int t = 0; t < 4; t++) env.pvp_runtime.gear_tier_weights[t] = 0.0f; env.pvp_runtime.gear_tier_weights[gear_tier] = 1.0f; @@ -2399,10 +2528,6 @@ int main(int argc, char** argv) { env.pvp_runtime.gear_tier_weights[2] = 0.10f; env.pvp_runtime.gear_tier_weights[3] = 0.05f; } - env.ocean_io.agent_actions = env.actions; - env.ocean_io.agent_obs = env._obs_buf; - env.ocean_io.agent_rewards = env.rewards; - env.ocean_io.agent_terminals = env.terminals; run_visual( &env, encounter_name, @@ -2418,7 +2543,7 @@ int main(int argc, char** argv) { return 1; #endif } else { - visual_alloc_env_buffers(&env); + visual_init_env_buffers(&env); printf("OSRS PvP C Environment Demo\n\n"); @@ -2433,18 +2558,20 @@ int main(int argc, char** argv) { &env, pvp_route_topology_finalize( (const CollisionMap*)env.collision_map)); - printf("Observation count per agent: %d\n", SLOT_NUM_OBSERVATIONS); + float observations[NH_PVP_NUM_OBS]; + pvp_write_observations(observations, &env, 0); + printf("Observation count per agent: %d\n", NH_PVP_NUM_OBS); printf("First 10 observations (agent 0): "); for (int i = 0; i < 10; i++) { - printf("%.2f ", env.observations[i]); + printf("%.2f ", observations[i]); } printf("\n"); - printf("\nAction heads: %d\n", NUM_ACTION_HEADS); + printf("\nAction heads: %d\n", OSRS_BASE_NUM_ACTION_HEADS); printf("Action dims: ["); - for (int i = 0; i < NUM_ACTION_HEADS; i++) { - printf("%d", ACTION_HEAD_DIMS[i]); - if (i < NUM_ACTION_HEADS - 1) { + for (int i = 0; i < OSRS_BASE_NUM_ACTION_HEADS; i++) { + printf("%d", NH_PVP_ACTION_DIMS[i]); + if (i < OSRS_BASE_NUM_ACTION_HEADS - 1) { printf(", "); } } @@ -2452,7 +2579,6 @@ int main(int argc, char** argv) { printf("\nDemo complete.\n"); - visual_free_env_buffers(&env); pvp_close(&env); } diff --git a/ocean/osrs/osrs_visual_net.h b/ocean/osrs/osrs_visual_net.h index 584333e068..225e949714 100644 --- a/ocean/osrs/osrs_visual_net.h +++ b/ocean/osrs/osrs_visual_net.h @@ -11,52 +11,49 @@ static inline void osrs_visual_gelu(float* input, float* output, int size) { } } -#define COLO_ENT_INF_NPC_START 130 -#define COLO_ENT_INF_NUM_NPCS 24 -#define COLO_ENT_INF_FEATS 34 -/* obs carries a type CODE per slot; the encoder expands it back to the one-hot */ -#define COLO_ENT_INF_OBS_FEATS 23 -#define COLO_ENT_INF_TYPE_ONEHOT 12 -#define COLO_ENT_INF_BOTTLENECK 16 -#define COLO_ENT_INF_INV_START 36 -#define COLO_ENT_INF_INV_NUM_CELLS 28 -#define COLO_ENT_INF_INV_FEATS 15 -/* obs carries an item code plus equipped and HP-heal; gear keeps the table's union slot */ -#define COLO_ENT_INF_INV_OBS_FEATS 3 -#define COLO_ENT_INF_INV_BOTTLENECK 16 +#define OSRS_ENT_INV_START 52 +#define OSRS_ENT_INV_NUM_RECS 28 +#define OSRS_ENT_INV_OBS_FEATS 1 +#define OSRS_ENT_ITEM_FEATS 14 +#define OSRS_ENT_EQUIPPED_START 80 +#define OSRS_ENT_EQUIPPED_NUM_RECS NUM_GEAR_SLOTS +#define OSRS_ENT_EQUIPPED_OBS_FEATS 1 +#define OSRS_ENT_ITEM_BOTTLENECK 16 + +#define COLO_ENT_OBS_SIZE 904 +#define COLO_ENT_NPC_START 101 +#define COLO_ENT_NPC_NUM_RECS 24 +#define COLO_ENT_NPC_OBS_FEATS 23 +#define COLO_ENT_NPC_FEATS 34 +#define COLO_ENT_NPC_TYPE_ONEHOT 12 +#define COLO_ENT_NPC_BOTTLENECK 16 +#define COLO_ENT_NPC_TYPE_SCALE 1 + +#define INF_ENT_OBS_SIZE 530 +#define INF_ENT_NPC_START 124 +#define INF_ENT_NPC_NUM_RECS 14 +#define INF_ENT_NPC_OBS_FEATS 13 +#define INF_ENT_NPC_FEATS 26 +#define INF_ENT_NPC_TYPE_ONEHOT 14 +#define INF_ENT_NPC_BOTTLENECK 16 +#define INF_ENT_NPC_TYPE_SCALE 16 -/* Host twin of OSRS_ITEM_OBS_TABLE_DEV in src/ocean.cu. */ static const float OSRS_ITEM_OBS_TABLE [OSRS_ITEM_OBS_TABLE_ROWS][OSRS_ITEM_OBS_TABLE_COLS] = { #include "osrs_item_obs_table.inc" }; -#define INF_ENT_OBS_SIZE 498 -#define INF_ENT_NPC_START 54 -#define INF_ENT_NUM_NPCS 14 -#define INF_ENT_OBS_FEATS 13 -#define INF_ENT_FEATS 26 -#define INF_ENT_TYPE_ONEHOT 14 -#define INF_ENT_TYPE_CODE_SCALE 16 -#define INF_ENT_INV_START 460 -#define INF_ENT_INV_NUM_CELLS 28 -#define INF_ENT_INV_OBS_FEATS 1 -#define INF_ENT_INV_FEATS 15 - typedef enum { ENTITY_RECORD_TYPE_ONEHOT = 0, ENTITY_RECORD_ITEM_TABLE, } EntityRecordExpansion; -typedef enum { - ENTITY_ITEM_OVERLAYS_NONE = 0, - ENTITY_ITEM_OVERLAYS_EQUIPPED_HP_HEAL, -} EntityItemOverlays; #define ENTITY_RECORD_MAX_FEATS 64 +#define ENTITY_ENCODER_MAX_BRANCHES 3 -typedef struct EntityPoolBranch EntityPoolBranch; -struct EntityPoolBranch { +typedef struct { + const char* weight_name; int start; int num_recs; int feats; @@ -66,7 +63,18 @@ struct EntityPoolBranch { int bottleneck; int active_width; EntityRecordExpansion expansion; - EntityItemOverlays item_overlays; +} EntityPoolDescriptor; + +typedef struct { + const char* env_name; + int obs_size; + int num_branches; + EntityPoolDescriptor branches[ENTITY_ENCODER_MAX_BRANCHES]; +} EntityEncoderDescriptor; + +typedef struct EntityPoolBranch EntityPoolBranch; +struct EntityPoolBranch { + EntityPoolDescriptor descriptor; float* l1_w; float* l2_w; float* z1; @@ -83,142 +91,127 @@ struct EntityEncoder { int input_dim; int hidden_dim; int num_branches; - EntityPoolBranch branches[2]; + EntityPoolBranch branches[ENTITY_ENCODER_MAX_BRANCHES]; }; static void entity_pool_branch_init( EntityPoolBranch* branch, Weights* weights, int hidden_dim, - int start, int num_recs, int feats, int obs_feats, int type_onehot, - int code_scale, int bottleneck, int active_width, - EntityRecordExpansion expansion, EntityItemOverlays item_overlays) { - if (feats > ENTITY_RECORD_MAX_FEATS || start < 0 || num_recs <= 0 || - obs_feats <= 0 || code_scale <= 0 || bottleneck <= 0 || - active_width <= 0 || active_width > feats) { + const EntityPoolDescriptor* descriptor) { + if (descriptor->feats > ENTITY_RECORD_MAX_FEATS || + descriptor->start < 0 || descriptor->num_recs <= 0 || + descriptor->obs_feats <= 0 || descriptor->code_scale <= 0 || + descriptor->bottleneck <= 0 || descriptor->active_width <= 0 || + descriptor->active_width > descriptor->feats) { fprintf(stderr, "entity pool branch: invalid shape or encoding contract\n"); abort(); } - if (expansion == ENTITY_RECORD_TYPE_ONEHOT && - (type_onehot <= 0 || feats != type_onehot + obs_feats - 1 || - item_overlays != ENTITY_ITEM_OVERLAYS_NONE)) { + if (descriptor->expansion == ENTITY_RECORD_TYPE_ONEHOT && + (descriptor->type_onehot <= 0 || + descriptor->feats != descriptor->type_onehot + descriptor->obs_feats - 1)) { fprintf(stderr, "entity pool branch: stale type-code expansion contract\n"); abort(); } - if (expansion == ENTITY_RECORD_ITEM_TABLE && - (type_onehot != 0 || feats != OSRS_ITEM_OBS_TABLE_COLS || - (item_overlays == ENTITY_ITEM_OVERLAYS_NONE && obs_feats != 1) || - (item_overlays == ENTITY_ITEM_OVERLAYS_EQUIPPED_HP_HEAL && - obs_feats != OSRS_INVENTORY_CELL_OBS_FEATURES_CODED))) { + if (descriptor->expansion == ENTITY_RECORD_ITEM_TABLE && + (descriptor->type_onehot != 0 || + descriptor->feats != OSRS_ITEM_OBS_TABLE_COLS || + descriptor->obs_feats != 1)) { fprintf(stderr, "entity pool branch: stale item-table expansion contract\n"); abort(); } - branch->start = start; - branch->num_recs = num_recs; - branch->feats = feats; - branch->obs_feats = obs_feats; - branch->type_onehot = type_onehot; - branch->code_scale = code_scale; - branch->bottleneck = bottleneck; - branch->active_width = active_width; - branch->expansion = expansion; - branch->item_overlays = item_overlays; - branch->l1_w = get_weights_aligned(weights, bottleneck * feats); - branch->l2_w = get_weights_aligned(weights, hidden_dim * bottleneck); - branch->z1 = (float*)calloc((size_t)num_recs * bottleneck, sizeof(float)); - branch->h1 = (float*)calloc((size_t)num_recs * bottleneck, sizeof(float)); - branch->e = (float*)calloc((size_t)num_recs * hidden_dim, sizeof(float)); - branch->active = (unsigned char*)calloc((size_t)num_recs, sizeof(unsigned char)); + branch->descriptor = *descriptor; + branch->l1_w = get_weights_aligned( + weights, descriptor->bottleneck * descriptor->feats); + branch->l2_w = get_weights_aligned( + weights, hidden_dim * descriptor->bottleneck); + branch->z1 = (float*)calloc( + (size_t)descriptor->num_recs * descriptor->bottleneck, sizeof(float)); + branch->h1 = (float*)calloc( + (size_t)descriptor->num_recs * descriptor->bottleneck, sizeof(float)); + branch->e = (float*)calloc( + (size_t)descriptor->num_recs * hidden_dim, sizeof(float)); + branch->active = (unsigned char*)calloc( + (size_t)descriptor->num_recs, sizeof(unsigned char)); } -/* Weight reads are sequenced statements in reg_params order (src/ocean.cu): - global_w, then entity_l1_w/entity_l2_w, then inv_l1_w/inv_l2_w. - get_weights_aligned advances a shared cursor, so call order IS the .bin layout. */ -static EntityEncoder* make_entity_encoder_global( - Weights* weights, int batch_size, int input_dim, int hidden_dim) { +static int entity_pool_descriptor_is_shared_item_branch( + const EntityPoolDescriptor* descriptor, int start, int num_recs) { + return descriptor->start == start && + descriptor->num_recs == num_recs && + descriptor->feats == OSRS_ENT_ITEM_FEATS && + descriptor->obs_feats == 1 && + descriptor->type_onehot == 0 && + descriptor->code_scale == OSRS_ITEM_OBS_CODE_SCALE && + descriptor->bottleneck == OSRS_ENT_ITEM_BOTTLENECK && + descriptor->active_width == 1 && + descriptor->expansion == ENTITY_RECORD_ITEM_TABLE; +} + +static EntityEncoder* make_entity_encoder( + Weights* weights, int batch_size, int input_dim, int hidden_dim, + const EntityEncoderDescriptor* descriptor) { + if (!descriptor || + (descriptor->obs_size > 0 && descriptor->obs_size != input_dim) || + descriptor->num_branches < 2 || + descriptor->num_branches > ENTITY_ENCODER_MAX_BRANCHES || + !entity_pool_descriptor_is_shared_item_branch( + &descriptor->branches[0], + OSRS_ENT_INV_START, + OSRS_ENT_INV_NUM_RECS) || + !entity_pool_descriptor_is_shared_item_branch( + &descriptor->branches[1], + OSRS_ENT_EQUIPPED_START, + OSRS_ENT_EQUIPPED_NUM_RECS)) { + fprintf(stderr, "entity encoder: stale environment descriptor\n"); + abort(); + } + for (int i = 0; i < descriptor->num_branches; i++) { + const EntityPoolDescriptor* branch = &descriptor->branches[i]; + if (branch->start < 0 || + branch->start + branch->num_recs * branch->obs_feats > + input_dim) { + fprintf(stderr, "entity encoder: branch exceeds observation width\n"); + abort(); + } + } size_t out_size = (size_t)batch_size * hidden_dim * sizeof(float); - EntityEncoder* layer = (EntityEncoder*)calloc(1, sizeof(EntityEncoder) + out_size); + EntityEncoder* layer = + (EntityEncoder*)calloc(1, sizeof(EntityEncoder) + out_size); layer->output = (float*)(layer + 1); layer->global_w = get_weights_aligned(weights, hidden_dim * input_dim); layer->batch_size = batch_size; layer->input_dim = input_dim; layer->hidden_dim = hidden_dim; - return layer; -} - -EntityEncoder* make_colosseum_entity_encoder( - Weights* weights, int batch_size, int input_dim, int hidden_dim, int mode) { - EntityEncoder* layer = make_entity_encoder_global( - weights, batch_size, input_dim, hidden_dim); - entity_pool_branch_init(&layer->branches[0], weights, hidden_dim, - COLO_ENT_INF_NPC_START, COLO_ENT_INF_NUM_NPCS, - COLO_ENT_INF_FEATS, COLO_ENT_INF_OBS_FEATS, COLO_ENT_INF_TYPE_ONEHOT, - 1, COLO_ENT_INF_BOTTLENECK, COLO_ENT_INF_TYPE_ONEHOT, - ENTITY_RECORD_TYPE_ONEHOT, ENTITY_ITEM_OVERLAYS_NONE); - layer->num_branches = 1; - if (mode >= 2) { - entity_pool_branch_init(&layer->branches[1], weights, hidden_dim, - COLO_ENT_INF_INV_START, COLO_ENT_INF_INV_NUM_CELLS, - COLO_ENT_INF_INV_FEATS, COLO_ENT_INF_INV_OBS_FEATS, 0, - OSRS_ITEM_OBS_CODE_SCALE, COLO_ENT_INF_INV_BOTTLENECK, 1, - ENTITY_RECORD_ITEM_TABLE, ENTITY_ITEM_OVERLAYS_EQUIPPED_HP_HEAL); - layer->num_branches = 2; + layer->num_branches = descriptor->num_branches; + for (int i = 0; i < descriptor->num_branches; i++) { + entity_pool_branch_init( + &layer->branches[i], weights, hidden_dim, &descriptor->branches[i]); } return layer; } -EntityEncoder* make_inferno_entity_encoder( - Weights* weights, int batch_size, int input_dim, int hidden_dim, int mode) { - if (input_dim != INF_ENT_OBS_SIZE) { - fprintf(stderr, "inferno entity encoder: input width %d != %d\n", - input_dim, INF_ENT_OBS_SIZE); - abort(); - } - EntityEncoder* layer = make_entity_encoder_global( - weights, batch_size, input_dim, hidden_dim); - entity_pool_branch_init(&layer->branches[0], weights, hidden_dim, - INF_ENT_NPC_START, INF_ENT_NUM_NPCS, - INF_ENT_FEATS, INF_ENT_OBS_FEATS, INF_ENT_TYPE_ONEHOT, - INF_ENT_TYPE_CODE_SCALE, COLO_ENT_INF_BOTTLENECK, INF_ENT_TYPE_ONEHOT, - ENTITY_RECORD_TYPE_ONEHOT, ENTITY_ITEM_OVERLAYS_NONE); - layer->num_branches = 1; - if (mode >= 2) { - entity_pool_branch_init(&layer->branches[1], weights, hidden_dim, - INF_ENT_INV_START, INF_ENT_INV_NUM_CELLS, - INF_ENT_INV_FEATS, INF_ENT_INV_OBS_FEATS, 0, - OSRS_ITEM_OBS_CODE_SCALE, COLO_ENT_INF_INV_BOTTLENECK, 1, - ENTITY_RECORD_ITEM_TABLE, ENTITY_ITEM_OVERLAYS_NONE); - layer->num_branches = 2; - } - return layer; -} - -static void entity_expand_record(const EntityPoolBranch* p, const float* rec, float* out) { - switch (p->expansion) { +static void entity_expand_record( + const EntityPoolDescriptor* descriptor, const float* rec, float* out) { + switch (descriptor->expansion) { case ENTITY_RECORD_TYPE_ONEHOT: { - int code = (int)lrintf(rec[0] * (float)p->code_scale); - if (code < 0 || code > p->type_onehot) { + int code = (int)lrintf(rec[0] * (float)descriptor->code_scale); + if (code < 0 || code > descriptor->type_onehot) { fprintf(stderr, "entity pool branch: type code %d out of range\n", code); abort(); } - for (int i = 0; i < p->type_onehot; i++) + for (int i = 0; i < descriptor->type_onehot; i++) out[i] = (code == i + 1) ? 1.0f : 0.0f; - for (int i = 0; i < p->feats - p->type_onehot; i++) - out[p->type_onehot + i] = rec[1 + i]; + for (int i = 0; i < descriptor->feats - descriptor->type_onehot; i++) + out[descriptor->type_onehot + i] = rec[1 + i]; return; } case ENTITY_RECORD_ITEM_TABLE: { - int code = (int)lrintf(rec[0] * (float)p->code_scale); + int code = (int)lrintf(rec[0] * (float)descriptor->code_scale); if (code < 0 || code >= OSRS_ITEM_OBS_TABLE_ROWS) { fprintf(stderr, "entity pool branch: item code %d out of table\n", code); abort(); } - for (int i = 0; i < p->feats; i++) out[i] = OSRS_ITEM_OBS_TABLE[code][i]; - if (p->item_overlays == ENTITY_ITEM_OVERLAYS_EQUIPPED_HP_HEAL) { - int is_gear = - OSRS_ITEM_OBS_TABLE[code][OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] != 0.0f || - OSRS_ITEM_OBS_TABLE[code][OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] != 0.0f; - out[OSRS_ITEM_OBS_OVERLAY_EQUIPPED] = rec[1]; - if (!is_gear) out[OSRS_ITEM_OBS_OVERLAY_HP_HEAL] = rec[2]; - } + for (int i = 0; i < descriptor->feats; i++) + out[i] = OSRS_ITEM_OBS_TABLE[code][i]; return; } } @@ -241,37 +234,42 @@ void entity_encoder_forward(EntityEncoder* layer, float* observations) { for (int br = 0; br < layer->num_branches; br++) { EntityPoolBranch* p = &layer->branches[br]; - float* recs = obs + p->start; + const EntityPoolDescriptor* descriptor = &p->descriptor; + float* recs = obs + descriptor->start; float expanded[ENTITY_RECORD_MAX_FEATS]; - for (int n = 0; n < p->num_recs; n++) { - float* rec = recs + n * p->obs_feats; - float* z1n = p->z1 + n * p->bottleneck; - entity_expand_record(p, rec, expanded); + for (int n = 0; n < descriptor->num_recs; n++) { + float* rec = recs + n * descriptor->obs_feats; + float* z1n = p->z1 + n * descriptor->bottleneck; + entity_expand_record(descriptor, rec, expanded); float active_sum = 0.0f; - for (int i = 0; i < p->active_width; i++) active_sum += expanded[i]; + for (int i = 0; i < descriptor->active_width; i++) + active_sum += expanded[i]; p->active[n] = active_sum > 0.0f; - for (int k = 0; k < p->bottleneck; k++) { - const float* w = p->l1_w + k * p->feats; + for (int k = 0; k < descriptor->bottleneck; k++) { + const float* w = p->l1_w + k * descriptor->feats; float sum = 0.0f; - for (int i = 0; i < p->feats; i++) sum += expanded[i] * w[i]; + for (int i = 0; i < descriptor->feats; i++) + sum += expanded[i] * w[i]; z1n[k] = sum; } } - osrs_visual_gelu(p->z1, p->h1, p->num_recs * p->bottleneck); - for (int n = 0; n < p->num_recs; n++) { - float* h1n = p->h1 + n * p->bottleneck; + osrs_visual_gelu( + p->z1, p->h1, descriptor->num_recs * descriptor->bottleneck); + for (int n = 0; n < descriptor->num_recs; n++) { + float* h1n = p->h1 + n * descriptor->bottleneck; float* en = p->e + (size_t)n * H; for (int o = 0; o < H; o++) { float sum = 0.0f; - for (int k = 0; k < p->bottleneck; k++) - sum += h1n[k] * p->l2_w[o * p->bottleneck + k]; + for (int k = 0; k < descriptor->bottleneck; k++) + sum += h1n[k] * + p->l2_w[o * descriptor->bottleneck + k]; en[o] = sum; } } for (int o = 0; o < H; o++) { float best = -INFINITY; int best_n = -1; - for (int n = 0; n < p->num_recs; n++) { + for (int n = 0; n < descriptor->num_recs; n++) { if (!p->active[n]) continue; float v = p->e[(size_t)n * H + o]; if (v > best) { best = v; best_n = n; } diff --git a/ocean/osrs/tests/probe_colo_best_gear_dpt.c b/ocean/osrs/tests/probe_colo_best_gear_dpt.c index 8ba42c3cc9..f9d7d62231 100644 --- a/ocean/osrs/tests/probe_colo_best_gear_dpt.c +++ b/ocean/osrs/tests/probe_colo_best_gear_dpt.c @@ -113,7 +113,7 @@ static void test_beats_worn_single_swap(void) { } for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { uint8_t w = - osrs_inventory_cell_item_index(&s.inventory_cells[cell]); + osrs_inventory_cell_item_index(&s.player.inventory_cells[cell]); if (w != ITEM_NONE && item_is_weapon(w) && !add_seen[w]) { add_seen[w] = 1; weapons[nweap++] = w; } @@ -158,7 +158,7 @@ static void test_equip_and_augury_same_tick(void) { int shadow_cell = -1; for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) if (osrs_inventory_cell_item_index( - &s.inventory_cells[cell]) == ITEM_TUMEKENS_SHADOW) { + &s.player.inventory_cells[cell]) == ITEM_TUMEKENS_SHADOW) { shadow_cell = cell; break; } @@ -245,8 +245,8 @@ static void test_per_cell_marginal_bit(void) { for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) if (osrs_inventory_cell_item_index( - &s.inventory_cells[cell]) == ITEM_TUMEKENS_SHADOW) - s.inventory_cells[cell] = osrs_inventory_cell_empty(); + &s.player.inventory_cells[cell]) == ITEM_TUMEKENS_SHADOW) + s.player.inventory_cells[cell] = osrs_inventory_cell_empty(); for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) if (s.player.equipped[slot] == ITEM_TUMEKENS_SHADOW) s.player.equipped[slot] = ITEM_NONE; diff --git a/ocean/osrs/tests/probe_colo_sim_invariant.c b/ocean/osrs/tests/probe_colo_sim_invariant.c index 8c18b99d5c..6c948fbef4 100644 --- a/ocean/osrs/tests/probe_colo_sim_invariant.c +++ b/ocean/osrs/tests/probe_colo_sim_invariant.c @@ -77,21 +77,22 @@ typedef struct { } SimConfig; /* Baselines pin the consolidated Manticore activation, automatic-thrall, and - * corpse-timed Volatility behavior. Regenerate with --print only for an intended - * simulation change, never to make an observation-only edit pass. */ + * corpse-timed Volatility behavior after inventory moved into canonical Player state. + * Regenerate with --print only for an intended simulation or serialized-state change, + * never to make an observation-only edit pass. */ static SimConfig CONFIGS[] = { - {"w01", 1, 1001ULL, 0xC0FFEE01ULL, 0xc53c6412d234f229ULL}, - {"w02", 2, 1002ULL, 0xC0FFEE02ULL, 0x778581a45fed3ddeULL}, - {"w03", 3, 1003ULL, 0xC0FFEE03ULL, 0xb70dc406e22b2c1aULL}, - {"w04", 4, 1004ULL, 0xC0FFEE04ULL, 0x635935a5ab0ade49ULL}, - {"w05", 5, 1005ULL, 0xC0FFEE05ULL, 0xb8bddb3cfb335938ULL}, - {"w06", 6, 1006ULL, 0xC0FFEE06ULL, 0x39095d5c8accb6dbULL}, - {"w07", 7, 1007ULL, 0xC0FFEE07ULL, 0x494c2434e8dd8c83ULL}, - {"w08", 8, 1008ULL, 0xC0FFEE08ULL, 0x13ee63bf0f98b87bULL}, - {"w09", 9, 1009ULL, 0xC0FFEE09ULL, 0xff48a1acf6a1c401ULL}, - {"w10", 10, 1010ULL, 0xC0FFEE10ULL, 0xb2a8313066a5a4c6ULL}, - {"w11", 11, 1011ULL, 0xC0FFEE11ULL, 0xbb81f53e0991c17aULL}, - {"w12", 12, 1012ULL, 0xC0FFEE12ULL, 0x63fdfb9bd8aed92dULL}, + {"w01", 1, 1001ULL, 0xC0FFEE01ULL, 0x00b4a64bb88751a0ULL}, + {"w02", 2, 1002ULL, 0xC0FFEE02ULL, 0x06e0f553b3786844ULL}, + {"w03", 3, 1003ULL, 0xC0FFEE03ULL, 0xe4c6c0cc84781d8cULL}, + {"w04", 4, 1004ULL, 0xC0FFEE04ULL, 0x42cedf6fe6821b46ULL}, + {"w05", 5, 1005ULL, 0xC0FFEE05ULL, 0x15204ad085dba366ULL}, + {"w06", 6, 1006ULL, 0xC0FFEE06ULL, 0x108aa5784aea9c9eULL}, + {"w07", 7, 1007ULL, 0xC0FFEE07ULL, 0x667fd12223f6da5aULL}, + {"w08", 8, 1008ULL, 0xC0FFEE08ULL, 0x41351e62c162053bULL}, + {"w09", 9, 1009ULL, 0xC0FFEE09ULL, 0x5538f0c631fb4227ULL}, + {"w10", 10, 1010ULL, 0xC0FFEE10ULL, 0x805f1543d28e08d6ULL}, + {"w11", 11, 1011ULL, 0xC0FFEE11ULL, 0x26b522b3a0d403e0ULL}, + {"w12", 12, 1012ULL, 0xC0FFEE12ULL, 0xdbab4cb0e6eb7d72ULL}, }; static void fill_actions( @@ -156,7 +157,8 @@ static uint64_t hash_sim(uint64_t h, const ColosseumState* s, const float* mask) * Taking the whole tail removes the padding from the hash entirely. */ SkipRegion skip[] = { {offsetof(ColosseumState, npcs), sizeof(s->npcs)}, - {offsetof(ColosseumState, inventory_cells), sizeof(s->inventory_cells)}, + {offsetof(ColosseumState, player.inventory_cells), + sizeof(s->player.inventory_cells)}, {offsetof(ColosseumState, player_pending_hits), sizeof(s->player_pending_hits)}, {offsetof(ColosseumState, log), sizeof(*s) - offsetof(ColosseumState, log)}, }; @@ -174,7 +176,7 @@ static uint64_t hash_sim(uint64_t h, const ColosseumState* s, const float* mask) for (int i = 0; i < COLO_MAX_NPCS; i++) h = fnv_npc(h, &s->npcs[i]); h = fnv_queue(h, &s->player_pending_hits); for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - const OsrsInventoryCell* cell = &s->inventory_cells[i]; + const OsrsInventoryCell* cell = &s->player.inventory_cells[i]; h = fnv_u8(h, osrs_inventory_cell_item_index(cell)); h = fnv_u16(h, osrs_inventory_cell_raw_osrs_id(cell)); h = fnv_u8(h, osrs_inventory_cell_dose_count(cell)); diff --git a/ocean/osrs/tests/probe_colo_wave_advance.c b/ocean/osrs/tests/probe_colo_wave_advance.c index c252aefdf4..7affc2d73e 100644 --- a/ocean/osrs/tests/probe_colo_wave_advance.c +++ b/ocean/osrs/tests/probe_colo_wave_advance.c @@ -57,7 +57,7 @@ static int scripted_first_modifier(const ColosseumState* s) { static int scripted_heal_cell(const ColosseumState* s) { for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - const ColoInvCell* cell = &s->inventory_cells[i]; + const ColoInvCell* cell = &s->player.inventory_cells[i]; OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( cell, OSRS_CLICK_TICK_FIRST); @@ -83,7 +83,7 @@ static void aggressive_policy(ColosseumState* s, int* actions) { int heal_cell = scripted_heal_cell(s); if (heal_cell >= 0) { OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( - &s->inventory_cells[heal_cell], OSRS_CLICK_TICK_FIRST); + &s->player.inventory_cells[heal_cell], OSRS_CLICK_TICK_FIRST); int head = r.click_action == OSRS_CLICK_DRINK ? COLO_HEAD_DRINK : COLO_HEAD_EAT; actions[head] = heal_cell + 1; } diff --git a/ocean/osrs/tests/probe_colosseum_winnability.c b/ocean/osrs/tests/probe_colosseum_winnability.c index 7edc48c0d1..5aeb753720 100644 --- a/ocean/osrs/tests/probe_colosseum_winnability.c +++ b/ocean/osrs/tests/probe_colosseum_winnability.c @@ -84,7 +84,7 @@ static int scripted_least_bad_modifier(const ColosseumState* s) { static int scripted_heal_cell(const ColosseumState* s) { for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - const ColoInvCell* cell = &s->inventory_cells[i]; + const ColoInvCell* cell = &s->player.inventory_cells[i]; OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( cell, OSRS_CLICK_TICK_FIRST); @@ -111,7 +111,7 @@ static void scripted_policy(ColosseumState* s, int* actions) { int heal_cell = scripted_heal_cell(s); if (heal_cell >= 0) { OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( - &s->inventory_cells[heal_cell], OSRS_CLICK_TICK_FIRST); + &s->player.inventory_cells[heal_cell], OSRS_CLICK_TICK_FIRST); int head = r.click_action == OSRS_CLICK_DRINK ? COLO_HEAD_DRINK : COLO_HEAD_EAT; actions[head] = heal_cell + 1; } diff --git a/ocean/osrs/tests/test_colosseum_golden.c b/ocean/osrs/tests/test_colosseum_golden.c index ff12b03a34..ee2e698d70 100644 --- a/ocean/osrs/tests/test_colosseum_golden.c +++ b/ocean/osrs/tests/test_colosseum_golden.c @@ -182,22 +182,20 @@ static const GoldenConfig CONFIGS[] = { #define NUM_CONFIGS ((int)(sizeof(CONFIGS) / sizeof(CONFIGS[0]))) #define EPISODE_TICKS 4000 -/* Re-seeded for the consolidated contract. Honest Sol laser observations change the - * observation width from 934 to 933. Manticore activation, automatic thralls, and - * corpse-timed Volatility intentionally change simulation timing. */ +/* Re-seeded after the canonical inventory and equipment observation cutover. */ static const uint64_t BASELINE[NUM_CONFIGS] = { - 0x1258544d5c658eddULL, - 0x68e9ade887662e65ULL, - 0x0c44516a8e72aeb4ULL, - 0x88ac4636389bd581ULL, - 0x6fbfce261a0dbee1ULL, - 0xf43b72bd8e2c2c04ULL, - 0x8332e858be3c913eULL, - 0x1d431ebf29dc964dULL, - 0x75f1c7b7cbb88fbbULL, - 0x701b7d6256d72996ULL, - 0xe5b310495bc052bcULL, - 0x92baaf448c9bdf5cULL, + 0x51537d0fc2e421deULL, + 0xf67cadfd982ad1f9ULL, + 0x7645fd997d792642ULL, + 0xe332936ca669006dULL, + 0x570f75972875ccf7ULL, + 0x10152aff5f56c2c1ULL, + 0xf8b757f054d16911ULL, + 0x0ef2d74e21c99aafULL, + 0x0d47de8319cb77e4ULL, + 0x9df4fb8c24dffbd6ULL, + 0x70d83553f5658edfULL, + 0x0f5bf824e69f1941ULL, }; int main(int argc, char** argv) { diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index 378d738d53..e1a2688506 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -195,7 +195,7 @@ static int test_obs_slot_for_npc(const ColosseumState* s, int npc_idx) { static int test_find_inventory_cell_with_item(const ColosseumState* s, uint8_t item_idx) { for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) if (osrs_inventory_cell_item_index( - &s->inventory_cells[i]) == item_idx) return i; + &s->player.inventory_cells[i]) == item_idx) return i; return -1; } @@ -204,7 +204,7 @@ static int test_find_inventory_cell_with_consumable( OsrsConsumableKind kind ) { for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - const ColoInvCell* cell = &s->inventory_cells[i]; + const ColoInvCell* cell = &s->player.inventory_cells[i]; OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); if (r.consumable_kind == kind) return i; @@ -217,10 +217,12 @@ static void test_click_inventory_cell_action_s( ) { assert(cell >= 0 && cell < COLO_INVENTORY_DISPLAY_SLOTS); OsrsInventoryClickResolution r = - osrs_inventory_cell_click_interpret(&s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); + osrs_inventory_cell_click_interpret( + &s->player.inventory_cells[cell], OSRS_CLICK_TICK_FIRST); if (r.click_action == OSRS_CLICK_EQUIP) { int slot = - osrs_inventory_cell_metadata(&s->inventory_cells[cell])->gear_slot; + osrs_inventory_cell_metadata( + &s->player.inventory_cells[cell])->gear_slot; assert(slot >= 0 && slot < NUM_GEAR_SLOTS); actions[COLO_HEAD_EQUIP_SLOT(slot)] = cell + 1; } else if (r.click_action == OSRS_CLICK_EAT) { @@ -244,11 +246,13 @@ static float test_click_mask_for_cell_s( const ColosseumState* s, const float mask[COLO_ACTION_MASK_SIZE], int cell ) { OsrsInventoryClickResolution r = - osrs_inventory_cell_click_interpret(&s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); + osrs_inventory_cell_click_interpret( + &s->player.inventory_cells[cell], OSRS_CLICK_TICK_FIRST); int head; if (r.click_action == OSRS_CLICK_EQUIP) { int slot = - osrs_inventory_cell_metadata(&s->inventory_cells[cell])->gear_slot; + osrs_inventory_cell_metadata( + &s->player.inventory_cells[cell])->gear_slot; head = (slot >= 0 && slot < NUM_GEAR_SLOTS) ? COLO_HEAD_EQUIP_SLOT(slot) : COLO_HEAD_EQUIP_SLOT(0); } else if (r.click_action == OSRS_CLICK_EAT) { @@ -267,7 +271,7 @@ static int test_sum_inventory_doses_for_kind( ) { int doses = 0; for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - const ColoInvCell* cell = &s->inventory_cells[i]; + const ColoInvCell* cell = &s->player.inventory_cells[i]; OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); if (r.consumable_kind == kind) @@ -335,28 +339,24 @@ static const float TEST_ITEM_OBS_TABLE #include "ocean/osrs/osrs_item_obs_table.inc" }; -/* Exactly what colo_ent_gather_inv does: the observation names a table row and supplies the - two dynamic features. Every per-cell assertion below reads the result, so it asserts - against what the encoder actually sees rather than against the raw observation. */ static void test_expand_inventory_cell( const float obs[COLO_NUM_OBS], int cell_idx, float out[COLO_INVENTORY_CELL_ENCODER_FEATURES] ) { - const float* coded = &obs[COLO_OBS_AFTER_PLAYER + - cell_idx * COLO_INVENTORY_CELL_OBS_FEATURES]; - int code = osrs_inventory_cell_obs_code_decode(coded[OSRS_INVENTORY_CELL_OBS_CODE]); + int code = osrs_inventory_cell_obs_code_decode( + obs[COLO_OBS_INVENTORY_START + cell_idx]); if (code < 0 || code >= OSRS_ITEM_OBS_TABLE_ROWS) { fprintf(stderr, "inventory cell %d observed item code %d\n", cell_idx, code); abort(); } - osrs_expand_inventory_cell_obs_code_features(out, coded, TEST_ITEM_OBS_TABLE[code]); + memcpy(out, TEST_ITEM_OBS_TABLE[code], sizeof(TEST_ITEM_OBS_TABLE[code])); } /* Offsets into the EXPANDED record above, not into the observation. */ typedef enum { - TEST_INV_OBS_ROLE_ARMOR = 3, - TEST_INV_OBS_ROLE_WEAPON = 4, + TEST_INV_OBS_ROLE_ARMOR = 2, + TEST_INV_OBS_ROLE_WEAPON = 3, TEST_INV_OBS_KIND_BREW = OSRS_INVENTORY_CELL_OBS_SHARED + 0, TEST_INV_OBS_KIND_RESTORE = OSRS_INVENTORY_CELL_OBS_SHARED + 1, TEST_INV_OBS_KIND_COMBAT_BOOST = OSRS_INVENTORY_CELL_OBS_SHARED + 2, @@ -396,7 +396,7 @@ static TestDroppedInventoryFields test_expected_dropped_inventory_fields( const ColosseumState* s, int cell_idx ) { - const ColoInvCell* cell = &s->inventory_cells[cell_idx]; + const ColoInvCell* cell = &s->player.inventory_cells[cell_idx]; const OsrsItemContentMetadata* metadata = osrs_inventory_cell_metadata(cell); uint32_t effect_mask = metadata->item == NULL @@ -503,7 +503,7 @@ static int test_count_item_in_equipment_and_inventory( if (s->player.equipped[slot] == item_idx) count++; for (int slot = 0; slot < COLO_INVENTORY_DISPLAY_SLOTS; slot++) if (osrs_inventory_cell_item_index( - &s->inventory_cells[slot]) == item_idx) count++; + &s->player.inventory_cells[slot]) == item_idx) count++; return count; } @@ -1323,16 +1323,41 @@ static void test_obs_signal_defects(void) { int bee_slot = test_obs_slot_for_npc(&s, bslot); int shaman_slot = test_obs_slot_for_npc(&s, 0); CHECK("an adjacent totem does not claim to threaten the player", - obs[COLO_OBS_AFTER_EQUIPPED_SELF + + obs[COLO_OBS_AFTER_SHARED + totem_slot * COLO_FEATURES_PER_NPC + in_range_off] == 0.0f); CHECK("a bee adjacent to the player does not claim an attack range", - obs[COLO_OBS_AFTER_EQUIPPED_SELF + + obs[COLO_OBS_AFTER_SHARED + bee_slot * COLO_FEATURES_PER_NPC + in_range_off] == 0.0f); CHECK("a real attacker in range still reports it", - obs[COLO_OBS_AFTER_EQUIPPED_SELF + + obs[COLO_OBS_AFTER_SHARED + shaman_slot * COLO_FEATURES_PER_NPC + in_range_off] == 1.0f); } +static void test_obs_overwrites_dirty_buffer(void) { + printf("test_obs_overwrites_dirty_buffer\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 5151, 12, 16); + float clean[COLO_NUM_OBS] = {0}; + float dirty[COLO_NUM_OBS]; + for (int i = 0; i < COLO_NUM_OBS; i++) dirty[i] = 7.0f; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, clean); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, dirty); + CHECK("pre-boss observation overwrites every output", + memcmp(clean, dirty, sizeof(clean)) == 0); + + s.wave = COLO_WAVE_BOSS; + s.sol.started = 1; + ctx.config.laser_obs_mode = 0; + memset(clean, 0, sizeof(clean)); + for (int i = 0; i < COLO_NUM_OBS; i++) dirty[i] = 7.0f; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, clean); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, dirty); + CHECK("laser-disabled boss observation overwrites every output", + memcmp(clean, dirty, sizeof(clean)) == 0); +} + + static void test_totem_heal_timing_obs(void) { printf("test_totem_heal_timing_obs\n"); ColosseumContext ctx; @@ -1358,9 +1383,9 @@ static void test_totem_heal_timing_obs(void) { int totem_base, owner_base; col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - totem_base = COLO_OBS_AFTER_EQUIPPED_SELF + + totem_base = COLO_OBS_AFTER_SHARED + test_obs_slot_for_npc(&s, tslot) * COLO_FEATURES_PER_NPC; - owner_base = COLO_OBS_AFTER_EQUIPPED_SELF + + owner_base = COLO_OBS_AFTER_SHARED + test_obs_slot_for_npc(&s, 0) * COLO_FEATURES_PER_NPC; int launch_idx = totem_base + COLO_NPC_TELLS_OFFSET + 0; int flight_idx = totem_base + COLO_NPC_TELLS_OFFSET + 1; @@ -1428,7 +1453,7 @@ static void test_totem_sol_obs_reports_stacking(void) { float armed = obs[sol_timer_idx]; int tslot = col_totem_for_owner(&s, sol)->npc_slot; - int tbase = COLO_OBS_AFTER_EQUIPPED_SELF + + int tbase = COLO_OBS_AFTER_SHARED + test_obs_slot_for_npc(&s, tslot) * COLO_FEATURES_PER_NPC; CHECK("a Sol totem's pending heal reads as 75 of Sol's max HP", fabsf(obs[tbase + COLO_NPC_TELLS_OFFSET + 2] - @@ -2087,7 +2112,7 @@ static void test_modifier_hazard_obs_fixes(void) { jv->skyfall_tile_y = 14; jv->skyfall_timer = 2; col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - int tells = COLO_OBS_AFTER_EQUIPPED_SELF + COLO_NPC_TELLS_OFFSET; + int tells = COLO_OBS_AFTER_SHARED + COLO_NPC_TELLS_OFFSET; CHECK("javelin skyfall tells expose landing dx while pending", fabsf(obs[tells + 2] - col_obs_rel_x(19, s.player.x)) < 0.000001f); CHECK("javelin skyfall tells expose landing dy while pending", @@ -2106,7 +2131,7 @@ static void test_modifier_hazard_obs_fixes(void) { .move_timer = 1, }; col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - tells = COLO_OBS_AFTER_EQUIPPED_SELF + COLO_NPC_TELLS_OFFSET; + tells = COLO_OBS_AFTER_SHARED + COLO_NPC_TELLS_OFFSET; CHECK("bee tells expose a nonzero move timer", obs[tells] > 0.0f && obs[tells] <= 1.0f); CHECK("bee tells expose next-step contact", @@ -3543,7 +3568,8 @@ static void test_manticore_telegraph_during_windup(void) { static int late_start_total_doses(const ColosseumState* s) { int total = 0; for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) - total += osrs_inventory_cell_dose_count(&s->inventory_cells[c]); + total += osrs_inventory_cell_dose_count( + &s->player.inventory_cells[c]); return total; } @@ -3604,7 +3630,7 @@ static void test_late_start_entry_state(void) { col_reset_ctx((EncounterState*)&org, (EncounterContext*)&organic_ctx, 99); org.modifiers.active_mask = (1u << COLO_MOD_BEES); org.modifiers.tier[COLO_MOD_BEES] = 3; - org.inventory_cells[27] = org.inventory_cells[26]; + org.player.inventory_cells[27] = org.player.inventory_cells[26]; org.player.current_hitpoints = 50; col_record_wave_entry(&org, 7); int org_doses = late_start_total_doses(&org); @@ -4241,7 +4267,7 @@ static void test_sol_generic_observation_signals_are_neutral(void) { col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); int obs_slot = test_obs_slot_for_npc(&s, 0); CHECK("Sol receives an NPC observation slot", obs_slot >= 0); - int npc_base = COLO_OBS_AFTER_EQUIPPED_SELF + + int npc_base = COLO_OBS_AFTER_SHARED + obs_slot * COLO_FEATURES_PER_NPC; int timer_idx = npc_base + COLO_NUM_NPC_TYPES + 3; int next_prayer_active_idx = timer_idx + 2; @@ -5666,8 +5692,8 @@ static void test_loadout_divine_potions_and_stat_drift(void) { s.divine_ranged_timer = 234; ColoSnapshot snap; col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); - CHECK("snapshot version is v25 for the consolidated state layout", - snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 25u); + CHECK("snapshot version matches the consolidated state layout", + snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 26u); ColosseumState restored; memset(&restored, 0, sizeof(restored)); col_restore_ctx((EncounterState*)&restored, (EncounterContext*)&ctx, &snap, sizeof(snap)); @@ -6010,8 +6036,8 @@ static void test_loadout_spec_weapons(void) { loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 54); s.modifiers.draft_pending = 0; s.wave_ready_delay = 0; - s.inventory_cells[26] = osrs_inventory_cell_empty(); - s.inventory_cells[27] = osrs_inventory_cell_empty(); + s.player.inventory_cells[26] = osrs_inventory_cell_empty(); + s.player.inventory_cells[27] = osrs_inventory_cell_empty(); col_equip_from_cell(&s, test_find_inventory_cell_with_item(&s, ITEM_SGS)); s.player.special_energy = 100; s.player.spec_armed = 1; @@ -6478,45 +6504,41 @@ static void test_combat_fidelity_contract_sizes(void) { CHECK("equip head dim is 29", COLO_ACTION_DIMS[COLO_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] == 29); CHECK("eat and drink heads are 29-way", COLO_ACTION_DIMS[COLO_HEAD_EAT] == 29 && COLO_ACTION_DIMS[COLO_HEAD_DRINK] == 29); - CHECK("prayer head uses shared PVE overhead dim", - COLO_ACTION_DIMS[COLO_HEAD_PRAYER] == ENCOUNTER_OVERHEAD_DIM_PVE); - CHECK("spell head dim is 2 (none/death-charge)", COLO_SPELL_DIM == 2); - CHECK("obs width is 933", COLO_NUM_OBS == 933); - CHECK("inventory block has 84 features (28 cells x code, equipped, hp_heal)", - COLO_INVENTORY_OBS_SIZE == 84); - CHECK("the encoder still sees the 15-feature record, rebuilt from the item table", - COLO_INVENTORY_CELL_ENCODER_FEATURES == 15 && - OSRS_ITEM_OBS_TABLE_COLS == 15); - /* spec_cost is 0 for everything without a special, so it doubles as the has-spec flag - * while also telling the agent whether it can afford one. */ + CHECK("prayer head uses the shared overhead dim", + COLO_ACTION_DIMS[COLO_HEAD_PRAYER] == OSRS_OVERHEAD_DIM); + CHECK("spell head uses the shared five-action contract", COLO_SPELL_DIM == 5); + CHECK("obs width is 904", COLO_NUM_OBS == 904); + CHECK("inventory block has 28 canonical content codes", + COLO_INVENTORY_OBS_SIZE == 28); + CHECK("the encoder sees the 14-feature record rebuilt from the item table", + COLO_INVENTORY_CELL_ENCODER_FEATURES == 14 && + OSRS_ITEM_OBS_TABLE_COLS == 14); CHECK("spec cost is exposed and normalised for a spec weapon", osrs_clamp_unit((float)osrs_spec_cost(ITEM_DRAGON_CLAWS) / 100.0f) == 0.5f); CHECK("a non-spec weapon reads zero spec cost", osrs_spec_cost(ITEM_NONE) == 0); - CHECK("equipped block is the 10-feature effect aggregate, not 11 item stat rows", - COLO_EQUIPPED_SELF_OBS_SIZE == 10); + CHECK("equipment block has one canonical content code per worn slot", + COLO_EQUIPPED_OBS_SIZE == NUM_GEAR_SLOTS); CHECK("modifier hazard tail has 42 features", COLO_MODIFIER_HAZARD_OBS_SIZE == 42); CHECK("modifier block has 60 features", COLO_MODIFIER_OBS_SIZE == 60); CHECK("NPC slots carry a type code, not a type one-hot", COLO_FEATURES_PER_NPC == 23); - CHECK("snapshot version is v25", COLO_SNAPSHOT_VERSION == 25u); + CHECK("snapshot version is v26", COLO_SNAPSHOT_VERSION == 26u); CHECK("every active NPC gets an obs slot (no busy-wave drop)", COLO_OBS_NPCS == 24 && COLO_OBS_NPCS == COLO_MAX_NPCS); CHECK("PRIMARY head covers noop, movement, and NPC obs slots", COLO_ACTION_DIMS[COLO_HEAD_PRIMARY] == COLO_PRIMARY_DIM && COLO_ACTION_DIMS[COLO_HEAD_PRIMARY] == 49); - CHECK("player block remains 36", COLO_PLAYER_OBS_SIZE == 36); + CHECK("shared self block is 52", OSRS_SHARED_SELF_OBS_SIZE == 52); int mask_sum = 0; for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) mask_sum += COLO_ACTION_DIMS[h]; CHECK("mask size equals the summed action-head dims", - COLO_ACTION_MASK_SIZE == mask_sum && COLO_ACTION_MASK_SIZE == 451); + COLO_ACTION_MASK_SIZE == mask_sum && COLO_ACTION_MASK_SIZE == 456); - int obs_sum = COLO_PLAYER_OBS_SIZE + - COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE + COLO_NPC_OBS_SIZE + + int obs_sum = OSRS_SHARED_OBS_SIZE + COLO_NPC_OBS_SIZE + COLO_MODIFIER_OBS_SIZE + COLO_WAVE_OBS_SIZE + COLO_BOSS_OBS_SIZE + - COLO_PENDING_HIT_OBS_SIZE + - COLO_THREAT_LOS_OBS_SIZE + COLO_THRALL_DC_OBS_SIZE + - COLO_SPAWN_OBS_SIZE; + COLO_PENDING_HIT_OBS_SIZE + COLO_THREAT_LOS_OBS_SIZE + + COLO_THRALL_DC_OBS_SIZE + COLO_SPAWN_OBS_SIZE; CHECK("obs width equals the summed section sizes", COLO_NUM_OBS == obs_sum); float opa, ops; @@ -6879,7 +6901,8 @@ static void test_divine_state_obs_presence(void) { static float obs_base[COLO_NUM_OBS]; col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_base); - CHECK("player block has no divine timer tail", COLO_PLAYER_OBS_SIZE == 36); + CHECK("shared self block has no divine timer tail", + OSRS_SHARED_SELF_OBS_SIZE == 52); col_apply_divine_combat_potion_effect(&s); s.divine_ranged_timer = ENCOUNTER_DIVINE_POTION_TICKS; @@ -6888,7 +6911,8 @@ static void test_divine_state_obs_presence(void) { static float obs_boost[COLO_NUM_OBS]; col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_boost); CHECK("divine boosts still surface through live max-hit scalar", - obs_boost[20] > obs_base[20]); + obs_boost[OSRS_SHARED_OBS_MAX_HIT] > + obs_base[OSRS_SHARED_OBS_MAX_HIT]); } static void test_magic_set_max_hit_math(void) { @@ -7034,6 +7058,17 @@ static void test_thrall_regression(void) { s.thrall_aggro_ticks_left == 0 && s.thrall_lifetime_left == 85 && s.thrall_lifetime_total == 85 && s.thrall_attack_timer == COLO_THRALL_TICK); + + s.player.current_magic = 0; + s.thrall_lifetime_left = 1; + col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("a thrall cannot be resummoned at zero Magic", + s.thrall_target_slot == -1 && s.thrall_aggro_ticks_left == 0 && + s.thrall_lifetime_left == 0); + s.player.current_magic = 1; + col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("the thrall resummons once Magic becomes positive", + s.thrall_lifetime_left == 1 && s.thrall_lifetime_total == 1); osrs_interaction_clear(&s.interaction); encounter_pending_hit_queue_clear(&s.npcs[slot].pending_hits); s.tick_scratch.player_attacked = 0; @@ -7175,7 +7210,8 @@ static void test_combat_fidelity_snapshot_roundtrip(void) { ColoSnapshot snap; col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); - CHECK("snapshot frame is v25", snap.version == 25u); + CHECK("snapshot frame has the current version", + snap.version == COLO_SNAPSHOT_VERSION); ColosseumState restored; memset(&restored, 0, sizeof(restored)); @@ -7971,50 +8007,48 @@ static void test_pending_hit_recoil_volatility_damage_accounting(void) { * which pushed the tick past the trainer's [-1,1] clamp and measurably destroyed 59% of it. * Terminal outcomes are now assignments, like death and timeout. */ -/* The inventory memo key covers cell contents but deliberately NOT equipped gear, so that a - * gear swap does not evict the block. Two fields in the cached range are still gear-dependent - * -- the per-cell is_equipped flag and the trailing effect aggregate -- and both must be - * rewritten on a cache hit. Swapping gear WITHOUT touching any cell leaves the key unchanged, - * which is exactly the case a stale cache would get wrong, and the goldens do not cover it. */ -static void test_colosseum_inventory_memo_tracks_gear_swaps(void) { - printf("test_colosseum_inventory_memo_tracks_gear_swaps\n"); +static void test_shared_inventory_tracks_gear_swaps(void) { + printf("test_shared_inventory_tracks_gear_swaps\n"); ColosseumState s; ColosseumContext ctx; - static float obs_a[COLO_NUM_OBS]; - static float obs_b[COLO_NUM_OBS]; + static float obs_before[COLO_NUM_OBS]; + static float obs_after[COLO_NUM_OBS]; col_init_context_typed(&ctx); memset(&s, 0, sizeof(s)); col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242u); - int cell = -1, slot = -1; - for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) { - uint8_t it = - osrs_inventory_cell_item_index(&s.inventory_cells[c]); - if (it == ITEM_NONE) continue; - int gs = osrs_item_gear_slot(it); - if (gs < 0) continue; - if (s.player.equipped[gs] == it) continue; - cell = c; slot = gs; break; + int cell = -1; + int slot = -1; + for (int candidate = 0; + candidate < COLO_INVENTORY_DISPLAY_SLOTS; + candidate++) { + uint8_t item = osrs_inventory_cell_item_index( + &s.player.inventory_cells[candidate]); + if (item == ITEM_NONE) continue; + int gear_slot = osrs_item_gear_slot(item); + if (gear_slot < 0 || s.player.equipped[gear_slot] == item) continue; + cell = candidate; + slot = gear_slot; + break; } CHECK("a swappable gear cell exists at reset", cell >= 0); if (cell < 0) return; - uint64_t sig_before = col_inventory_obs_signature(&s); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_a); - - /* Equip it directly. No cell changes, so the memo key is untouched. */ - s.player.equipped[slot] = - osrs_inventory_cell_item_index(&s.inventory_cells[cell]); - CHECK("a gear swap alone does not move the memo key", - col_inventory_obs_signature(&s) == sig_before); - - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_b); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_before); + CHECK("real gear swap succeeds", + osrs_equip_from_cell(&s.player, s.player.inventory_cells, cell) == slot); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_after); - int flag = COLO_OBS_AFTER_PLAYER + cell * COLO_INVENTORY_CELL_OBS_FEATURES + 1; - CHECK("the cell reads not-equipped before the swap", obs_a[flag] == 0.0f); - CHECK("a cache hit still reports the freshly equipped item", - obs_b[flag] == 1.0f); + int inventory_offset = COLO_OBS_INVENTORY_START + cell; + int equipment_offset = COLO_OBS_EQUIPMENT_START + slot; + CHECK("inventory observation exposes the displaced worn item", + obs_after[inventory_offset] != obs_before[inventory_offset]); + CHECK("equipment observation exposes the newly worn item", + obs_after[equipment_offset] != obs_before[equipment_offset] && + obs_after[equipment_offset] == + osrs_inventory_cell_obs_code_encode( + osrs_inventory_content_code_from_item(s.player.equipped[slot]))); } static void test_colosseum_win_tick_is_not_stacked(void) { @@ -8113,7 +8147,7 @@ static void test_colosseum_live_inventory_display(void) { col_build_live_inventory_display(&s, kit); CHECK("surge vial shows 3-dose after a drink", kit[12] == 30878); - s.inventory_cells[brew_cell] = + s.player.inventory_cells[brew_cell] = osrs_inventory_cell_from_raw_osrs_id(6691); s.player.potion_timer = 0; s.player.current_hitpoints = 50; @@ -8145,7 +8179,7 @@ static void test_stage3_t1_inventory_ranged_weapon_swap(void) { s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_TWISTED_BOW); CHECK("T1 displaced melee weapon returns to clicked cell", osrs_inventory_cell_item_index( - &s.inventory_cells[bow_cell]) == melee_weapon); + &s.player.inventory_cells[bow_cell]) == melee_weapon); } static void test_stage3_t1_inventory_weapon_slot_last_click_wins(void) { @@ -8163,7 +8197,7 @@ static void test_stage3_t1_inventory_weapon_slot_last_click_wins(void) { s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_DRAGON_CLAWS); CHECK("unclicked weapon stays in its inventory cell", osrs_inventory_cell_item_index( - &s.inventory_cells[bow_cell]) == ITEM_TWISTED_BOW); + &s.player.inventory_cells[bow_cell]) == ITEM_TWISTED_BOW); } static void test_stage3_t1_human_inventory_primary_click_uses_resolver(void) { @@ -8188,7 +8222,7 @@ static void test_stage3_t1_human_inventory_primary_click_uses_resolver(void) { s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_DRAGON_CLAWS); CHECK("human earlier same-slot click is ignored", osrs_inventory_cell_item_index( - &s.inventory_cells[bow_cell]) == ITEM_TWISTED_BOW); + &s.player.inventory_cells[bow_cell]) == ITEM_TWISTED_BOW); human_input_destroy(&hi); } @@ -8207,17 +8241,17 @@ static void test_stage3_t1_human_rearrange_swaps_inventory_slots(void) { &hi, bow_cell, claws_cell, - osrs_inventory_cell_item_index(&s.inventory_cells[bow_cell]), - osrs_inventory_cell_raw_osrs_id(&s.inventory_cells[bow_cell])); + osrs_inventory_cell_item_index(&s.player.inventory_cells[bow_cell]), + osrs_inventory_cell_raw_osrs_id(&s.player.inventory_cells[bow_cell])); int actions[COLO_NUM_ACTION_HEADS] = {0}; col_translate_human_commands_ctx(&hi, actions, &s, &ctx); CHECK("human rearrange moves bow to target slot", osrs_inventory_cell_item_index( - &s.inventory_cells[claws_cell]) == ITEM_TWISTED_BOW); + &s.player.inventory_cells[claws_cell]) == ITEM_TWISTED_BOW); CHECK("human rearrange moves claws to source slot", osrs_inventory_cell_item_index( - &s.inventory_cells[bow_cell]) == ITEM_DRAGON_CLAWS); + &s.player.inventory_cells[bow_cell]) == ITEM_DRAGON_CLAWS); CHECK("human rearrange leaves action space heads unchanged", COLO_NUM_ACTION_HEADS == 20 && COLO_ACTION_DIMS[COLO_HEAD_EQUIP_BASE] == 29); human_input_destroy(&hi); @@ -8235,8 +8269,10 @@ static void test_stage3_t2_brew_click_decrements_dose(void) { step_and_observe(&s, &ctx, actions); CHECK("T2 brew raises HP", s.player.current_hitpoints > 50); CHECK("T2 brew cell dose drops 4 to 3", - osrs_inventory_cell_dose_count(&s.inventory_cells[brew_cell]) == 3 && - osrs_inventory_cell_raw_osrs_id(&s.inventory_cells[brew_cell]) == 6687); + osrs_inventory_cell_dose_count( + &s.player.inventory_cells[brew_cell]) == 3 && + osrs_inventory_cell_raw_osrs_id( + &s.player.inventory_cells[brew_cell]) == 6687); CHECK("T2 brew starts potion timer", s.player.potion_timer == 3); } @@ -8246,14 +8282,14 @@ static void test_stage3_t3_one_dose_vial_empties(void) { ColosseumState s; loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 503); int brew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); - s.inventory_cells[brew_cell] = + s.player.inventory_cells[brew_cell] = osrs_inventory_cell_from_raw_osrs_id(6691); s.player.current_hitpoints = 50; int actions[COLO_NUM_ACTION_HEADS] = {0}; test_click_inventory_cell_action_s(&s, actions, brew_cell); step_and_observe(&s, &ctx, actions); CHECK("T3 one-dose vial cell becomes empty", - osrs_inventory_cell_is_empty(&s.inventory_cells[brew_cell])); + osrs_inventory_cell_is_empty(&s.player.inventory_cells[brew_cell])); } static void test_colosseum_potion_click_source_of_truth(void) { @@ -8273,7 +8309,7 @@ static void test_colosseum_potion_click_source_of_truth(void) { step_and_observe(&s, &ctx, actions); CHECK("super restore consumes exactly one clicked-cell dose", osrs_inventory_cell_dose_count( - &s.inventory_cells[restore_cell]) == 3); + &s.player.inventory_cells[restore_cell]) == 3); CHECK("super restore aggregate is rebuilt from cells", s.player.restore_doses == restore_sum_before - 1); @@ -8287,7 +8323,7 @@ static void test_colosseum_potion_click_source_of_truth(void) { step_and_observe(&s, &ctx, actions); CHECK("sanfew consumes exactly one clicked-cell dose", osrs_inventory_cell_dose_count( - &s.inventory_cells[sanfew_cell]) == 3); + &s.player.inventory_cells[sanfew_cell]) == 3); CHECK("sanfew aggregate is rebuilt from cells", s.player.restore_doses == sanfew_sum_before - 1); @@ -8303,7 +8339,7 @@ static void test_colosseum_potion_click_source_of_truth(void) { step_and_observe(&s, &ctx, actions); CHECK("divine combat consumes exactly one clicked-cell dose", osrs_inventory_cell_dose_count( - &s.inventory_cells[divine_cell]) == 3); + &s.player.inventory_cells[divine_cell]) == 3); CHECK("divine combat aggregate is rebuilt from cells", s.player.combat_potion_doses == divine_sum_before - 1); CHECK("divine combat drink starts the potion timer", s.player.potion_timer == 3); @@ -8319,7 +8355,7 @@ static void test_colosseum_potion_click_source_of_truth(void) { step_and_observe(&s, &ctx, actions); CHECK("low-missing-prayer restore click still consumes one dose", osrs_inventory_cell_dose_count( - &s.inventory_cells[restore_cell]) == 3); + &s.player.inventory_cells[restore_cell]) == 3); CHECK("low-missing-prayer restore rebuilds aggregate from cells", s.player.restore_doses == restore_sum_before - 1); } @@ -8336,13 +8372,13 @@ static void test_colosseum_potion_timer_and_same_tick_gate(void) { test_click_inventory_cell_action_s(&s, brew, brew_cell); step_and_observe(&s, &ctx, brew); int brew_dose_after_first = - osrs_inventory_cell_dose_count(&s.inventory_cells[brew_cell]); + osrs_inventory_cell_dose_count(&s.player.inventory_cells[brew_cell]); int brew_aggregate_after_first = s.player.brew_doses; s.player.current_hitpoints = 50; step_and_observe(&s, &ctx, brew); CHECK("second potion click before timer expiry is blocked", osrs_inventory_cell_dose_count( - &s.inventory_cells[brew_cell]) == brew_dose_after_first && + &s.player.inventory_cells[brew_cell]) == brew_dose_after_first && s.player.brew_doses == brew_aggregate_after_first && s.player.potion_timer == 2); @@ -8357,9 +8393,9 @@ static void test_colosseum_potion_timer_and_same_tick_gate(void) { step_and_observe(&s, &ctx, drink_one); CHECK("the drink head consumes exactly one potion per tick", osrs_inventory_cell_dose_count( - &s.inventory_cells[divine_cell]) == 3 && + &s.player.inventory_cells[divine_cell]) == 3 && osrs_inventory_cell_dose_count( - &s.inventory_cells[sanfew_cell]) == 4); + &s.player.inventory_cells[sanfew_cell]) == 4); loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 515); sanfew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SANFEW); @@ -8370,11 +8406,11 @@ static void test_colosseum_potion_timer_and_same_tick_gate(void) { step_and_observe(&s, &ctx, drink_again); CHECK("first drink consumes one dose", osrs_inventory_cell_dose_count( - &s.inventory_cells[sanfew_cell]) == 3); + &s.player.inventory_cells[sanfew_cell]) == 3); step_and_observe(&s, &ctx, drink_again); CHECK("second drink before potion timer expiry is blocked", osrs_inventory_cell_dose_count( - &s.inventory_cells[sanfew_cell]) == 3); + &s.player.inventory_cells[sanfew_cell]) == 3); } typedef struct { @@ -8412,7 +8448,8 @@ static void test_colosseum_all_drink_kinds_shared_one_dose_path(void) { int cell = 0; uint16_t start_raw = c->raw4 ? c->raw4 : c->raw1; col_init_empty_inventory_cells(&s); - s.inventory_cells[cell] = osrs_inventory_cell_from_raw_osrs_id(start_raw); + s.player.inventory_cells[cell] = + osrs_inventory_cell_from_raw_osrs_id(start_raw); col_sync_consumable_counters_from_inventory(&s); test_prepare_for_drink_kind(&s, c->kind); @@ -8425,9 +8462,9 @@ static void test_colosseum_all_drink_kinds_shared_one_dose_path(void) { snprintf(label, sizeof(label), "%s first click decrements one dose", c->label); CHECK(label, osrs_inventory_cell_raw_osrs_id( - &s.inventory_cells[cell]) == expected_after_first && + &s.player.inventory_cells[cell]) == expected_after_first && osrs_inventory_cell_dose_count( - &s.inventory_cells[cell]) == expected_dose_after_first); + &s.player.inventory_cells[cell]) == expected_dose_after_first); snprintf(label, sizeof(label), "%s first click arms potion timer", c->label); CHECK(label, s.player.potion_timer == 3); int aggregate = test_aggregate_doses_for_kind(&s, c->kind); @@ -8438,23 +8475,25 @@ static void test_colosseum_all_drink_kinds_shared_one_dose_path(void) { } if (!c->raw4) { - s.inventory_cells[cell] = osrs_inventory_cell_from_raw_osrs_id(c->raw1); + s.player.inventory_cells[cell] = + osrs_inventory_cell_from_raw_osrs_id(c->raw1); } uint16_t raw_before_gate = - osrs_inventory_cell_raw_osrs_id(&s.inventory_cells[cell]); + osrs_inventory_cell_raw_osrs_id(&s.player.inventory_cells[cell]); uint8_t dose_before_gate = - osrs_inventory_cell_dose_count(&s.inventory_cells[cell]); + osrs_inventory_cell_dose_count(&s.player.inventory_cells[cell]); step_and_observe(&s, &ctx, actions); snprintf(label, sizeof(label), "%s timer gate blocks next click", c->label); CHECK(label, osrs_inventory_cell_raw_osrs_id( - &s.inventory_cells[cell]) == raw_before_gate && + &s.player.inventory_cells[cell]) == raw_before_gate && osrs_inventory_cell_dose_count( - &s.inventory_cells[cell]) == dose_before_gate); + &s.player.inventory_cells[cell]) == dose_before_gate); const uint16_t chain[] = {c->raw4, c->raw3, c->raw2, c->raw1, 0}; int start = c->raw4 ? 0 : 3; - s.inventory_cells[cell] = osrs_inventory_cell_from_raw_osrs_id(chain[start]); + s.player.inventory_cells[cell] = + osrs_inventory_cell_from_raw_osrs_id(chain[start]); col_sync_consumable_counters_from_inventory(&s); for (int step = start; step < 4; step++) { test_prepare_for_drink_kind(&s, c->kind); @@ -8463,11 +8502,11 @@ static void test_colosseum_all_drink_kinds_shared_one_dose_path(void) { step_and_observe(&s, &ctx, actions); snprintf(label, sizeof(label), "%s chain step %d raw id", c->label, step); CHECK(label, osrs_inventory_cell_raw_osrs_id( - &s.inventory_cells[cell]) == chain[step + 1]); + &s.player.inventory_cells[cell]) == chain[step + 1]); uint8_t expected_dose = chain[step + 1] == 0 ? 0 : (uint8_t)(3 - step); snprintf(label, sizeof(label), "%s chain step %d dose", c->label, step); CHECK(label, osrs_inventory_cell_dose_count( - &s.inventory_cells[cell]) == expected_dose); + &s.player.inventory_cells[cell]) == expected_dose); } } } @@ -8492,7 +8531,8 @@ static void test_inventory_pure_cut_reconstruction(void) { loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, (uint32_t)(920 + i * 4 + dose)); col_init_empty_inventory_cells(&s); - s.inventory_cells[0] = osrs_inventory_cell_from_raw_osrs_id(chain[dose]); + s.player.inventory_cells[0] = + osrs_inventory_cell_from_raw_osrs_id(chain[dose]); col_sync_consumable_counters_from_inventory(&s); test_prepare_for_drink_kind(&s, c->kind); char label[160]; @@ -8522,7 +8562,7 @@ static void test_inventory_pure_cut_reconstruction(void) { test_check_inventory_cut_equivalence_state( &s, &ctx, "full inventory two-handed equip denial"); - s.inventory_cells[27] = osrs_inventory_cell_empty(); + s.player.inventory_cells[27] = osrs_inventory_cell_empty(); int bowfa_actions[COLO_NUM_ACTION_HEADS] = {0}; test_click_inventory_cell_action_s(&s, bowfa_actions, bowfa_cell); step_and_observe(&s, &ctx, bowfa_actions); @@ -8534,8 +8574,10 @@ static void test_inventory_pure_cut_reconstruction(void) { loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 904); col_init_empty_inventory_cells(&s); - s.inventory_cells[0] = osrs_inventory_cell_from_raw_osrs_id(6685); - s.inventory_cells[1] = osrs_inventory_cell_from_raw_osrs_id(6685); + s.player.inventory_cells[0] = + osrs_inventory_cell_from_raw_osrs_id(6685); + s.player.inventory_cells[1] = + osrs_inventory_cell_from_raw_osrs_id(6685); col_sync_consumable_counters_from_inventory(&s); s.player.current_hitpoints = 50; s.player.potion_timer = 0; @@ -8549,7 +8591,7 @@ static void test_stage3_t4_click_mask_bits(void) { loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 504); int equipped_scythe_cell = test_find_inventory_cell_with_item(&s, ITEM_SCYTHE_OF_VITUR); int empty_cell = 27; - s.inventory_cells[empty_cell] = osrs_inventory_cell_empty(); + s.player.inventory_cells[empty_cell] = osrs_inventory_cell_empty(); int brew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); s.player.current_hitpoints = 50; float mask[COLO_ACTION_MASK_SIZE]; @@ -8589,7 +8631,7 @@ static void test_stage3_t4_mask_inventory_heads_flag(void) { CHECK("mask_inventory_heads pins every inventory head to noop only", all_inventory_heads_pinned_to_noop); CHECK("mask_inventory_heads leaves the action-mask size unchanged", - COLO_ACTION_MASK_SIZE == 451); + COLO_ACTION_MASK_SIZE == 456); geo_clear_npcs(&s); s.modifiers.draft_pending = 0; @@ -8642,8 +8684,8 @@ static void test_stage3_t6_obs_mask_fuzz_contract(void) { } step_and_observe(&s, &ctx, actions); } - CHECK("T6 obs running-index assert reached COLO_NUM_OBS", COLO_NUM_OBS == 933); - CHECK("T6 mask running-index assert reached 451", COLO_ACTION_MASK_SIZE == 451); + CHECK("T6 obs running-index assert reached COLO_NUM_OBS", COLO_NUM_OBS == 904); + CHECK("T6 mask running-index assert reached 456", COLO_ACTION_MASK_SIZE == 456); } static void test_death_attribution_credits_actual_source(void) { @@ -8776,7 +8818,7 @@ static void test_modifier_draft_forces_pick(void) { static int colo_test_cell_of_named_item(const ColosseumState* s, const char* name) { for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { uint8_t item = - osrs_inventory_cell_item_index(&s->inventory_cells[c]); + osrs_inventory_cell_item_index(&s->player.inventory_cells[c]); if (item == ITEM_NONE) continue; const Item* meta = get_item(item); if (meta && strcmp(meta->name, name) == 0) return c; @@ -8813,18 +8855,14 @@ static void test_item_obs_table_matches_every_code_semantically(void) { printf("test_item_obs_table_matches_every_code_semantically\n"); CHECK("generic item table has one row for every inventory item code", OSRS_ITEM_OBS_TABLE_ROWS == OSRS_ITEM_CONTENT_COUNT); - CHECK("generic item rows contain the full 15-float compact record", - OSRS_ITEM_OBS_TABLE_COLS == 15); + CHECK("generic item rows contain the full 14-float compact record", + OSRS_ITEM_OBS_TABLE_COLS == 14); CHECK("generic item rows bake hitpoints at 99", OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS == 99); CHECK("generic item rows bake prayer at 99", OSRS_ITEM_OBS_TABLE_BASE_PRAYER == 99); CHECK("generic item rows bake ranged at 99", OSRS_ITEM_OBS_TABLE_BASE_RANGED == 99); - CHECK("generic equipped overlay targets the compact equipped feature", - OSRS_ITEM_OBS_OVERLAY_EQUIPPED == OSRS_INVENTORY_CELL_COMPACT_EQUIPPED); - CHECK("generic HP-heal overlay targets the compact HP-heal feature", - OSRS_ITEM_OBS_OVERLAY_HP_HEAL == OSRS_INVENTORY_CELL_COMPACT_HP_HEAL); for (int code = 0; code < OSRS_ITEM_OBS_TABLE_ROWS; code++) { OsrsInventoryCell cell = @@ -8870,128 +8908,69 @@ static void test_item_obs_table_bakes_consumable_hp_heal(void) { OSRS_ITEM_OBS_TABLE_BASE_RANGED); CHECK("base-99 shark semantics contain a positive HP heal", - expected[OSRS_ITEM_OBS_OVERLAY_HP_HEAL] > 0.0f); + expected[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL] > 0.0f); CHECK("generated shark row bakes its base-99 HP heal", - TEST_ITEM_OBS_TABLE[shark_code][OSRS_ITEM_OBS_OVERLAY_HP_HEAL] == - expected[OSRS_ITEM_OBS_OVERLAY_HP_HEAL]); + TEST_ITEM_OBS_TABLE[shark_code][OSRS_INVENTORY_CELL_COMPACT_HP_HEAL] == + expected[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL]); } -static void test_colosseum_item_obs_overlay_overwrites_dynamic_features(void) { - printf("test_colosseum_item_obs_overlay_overwrites_dynamic_features\n"); - float table_row[OSRS_ITEM_OBS_TABLE_COLS] = {0}; - float coded[OSRS_INVENTORY_CELL_OBS_FEATURES_CODED] = {0}; - float expanded[OSRS_ITEM_OBS_TABLE_COLS]; - - table_row[OSRS_ITEM_OBS_OVERLAY_EQUIPPED] = 0.25f; - table_row[OSRS_ITEM_OBS_OVERLAY_HP_HEAL] = 0.50f; - coded[OSRS_INVENTORY_CELL_OBS_EQUIPPED] = 1.0f; - coded[OSRS_INVENTORY_CELL_OBS_HP_HEAL] = 0.20f; - - osrs_expand_inventory_cell_obs_code_features(expanded, coded, table_row); - - CHECK("Colosseum equipped overlay overwrites the generated slot", - expanded[OSRS_ITEM_OBS_OVERLAY_EQUIPPED] == - coded[OSRS_INVENTORY_CELL_OBS_EQUIPPED]); - CHECK("Colosseum HP-heal overlay overwrites the baked base-99 value", - expanded[OSRS_ITEM_OBS_OVERLAY_HP_HEAL] == - coded[OSRS_INVENTORY_CELL_OBS_HP_HEAL]); -} -/* Drive a live episode and compare the expanded record against the compact semantic writer, - cell by cell and tick by tick, across a Frailty draft so dynamic HP-heal moves away from the - generated base-99 value. */ -static void test_inventory_obs_expansion_is_lossless(void) { - printf("test_inventory_obs_expansion_is_lossless\n"); +static void test_inventory_obs_expansion_matches_item_table(void) { + printf("test_inventory_obs_expansion_matches_item_table\n"); ColosseumContext ctx; ColosseumState s; loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); static float obs[COLO_NUM_OBS]; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); int mismatches = 0; - int hp_heal_seen = 0; - - for (int tier = 0; tier < 4; tier++) { - s.modifiers.active_mask |= (1u << COLO_MOD_FRAILTY); - s.modifiers.tier[COLO_MOD_FRAILTY] = tier; - col_mod_apply_frailty_hp(&s); - memset(&s.obs_memos, 0, sizeof(s.obs_memos)); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { - const ColoInvCell* c = &s.inventory_cells[cell]; - float want[COLO_INVENTORY_CELL_ENCODER_FEATURES]; - osrs_write_inventory_cell_affordance_features_compact( - want, c, - osrs_inventory_cell_holds_equipped_item( - &s.player, s.inventory_cells, cell), - s.player.base_hitpoints, - s.player.base_prayer, - s.player.base_attack); - - float got[COLO_INVENTORY_CELL_ENCODER_FEATURES]; - test_expand_inventory_cell(obs, cell, got); - - if (want[OSRS_INVENTORY_CELL_COMPACT_HP_HEAL] != 0.0f && - want[OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR] == 0.0f && - want[OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON] == 0.0f) { - hp_heal_seen++; - } - for (int f = 0; f < COLO_INVENTORY_CELL_ENCODER_FEATURES; f++) { - if (want[f] != got[f]) mismatches++; - } + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { + int code = osrs_inventory_cell_obs_code_decode( + obs[COLO_OBS_INVENTORY_START + cell]); + float expanded[COLO_INVENTORY_CELL_ENCODER_FEATURES]; + test_expand_inventory_cell(obs, cell, expanded); + for (int feature = 0; + feature < COLO_INVENTORY_CELL_ENCODER_FEATURES; + feature++) { + if (expanded[feature] != TEST_ITEM_OBS_TABLE[code][feature]) + mismatches++; } } - - CHECK("Colosseum overlay preserves exact compact semantics on every cell and Frailty tier", + CHECK("shared inventory codes reconstruct every compact item record", mismatches == 0); - CHECK("the Frailty sweep exercised a dynamic HP-heal overwrite", - hp_heal_seen > 0); } -static void test_inventory_obs_memo(void) { - printf("test_inventory_obs_memo\n"); +static void test_inventory_obs_code_updates(void) { + printf("test_inventory_obs_code_updates\n"); ColosseumContext ctx; ColosseumState s; loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); static float obs[COLO_NUM_OBS]; int cell_brew = -1; - for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { if (osrs_inventory_cell_item_index( - &s.inventory_cells[c]) == ITEM_NONE) { - cell_brew = c; + &s.player.inventory_cells[cell]) == ITEM_NONE) { + cell_brew = cell; break; } + } CHECK("a gear-free cell exists to hold the brew", cell_brew >= 0); - s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6685); - CHECK("4-dose brew seeded", - osrs_inventory_cell_dose_count( - &s.inventory_cells[cell_brew]) == 4); + s.player.inventory_cells[cell_brew] = + osrs_inventory_cell_from_raw_osrs_id(6685); - /* Dose rides the item code now: a potion's raw id names its dose, so the sip lands a - different code and the expanded record picks the dose up from the table row. */ float expanded[COLO_INVENTORY_CELL_ENCODER_FEATURES]; col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); test_expand_inventory_cell(obs, cell_brew, expanded); CHECK("4-dose brew renders the full dose feature", expanded[OSRS_INVENTORY_CELL_COMPACT_DOSE] == 1.0f); - s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6687); - CHECK("sip took a dose", - osrs_inventory_cell_dose_count( - &s.inventory_cells[cell_brew]) == 3); + s.player.inventory_cells[cell_brew] = + osrs_inventory_cell_from_raw_osrs_id(6687); col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); test_expand_inventory_cell(obs, cell_brew, expanded); - CHECK("sip moves the dose feature (dose is in the memo key, not a stale block)", + CHECK("changed content code reconstructs the new dose", expanded[OSRS_INVENTORY_CELL_COMPACT_DOSE] == 0.75f); - - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - float served_block[COLO_INVENTORY_OBS_CACHE_FLOATS]; - memcpy(served_block, &obs[COLO_OBS_AFTER_PLAYER], sizeof(served_block)); - memset(&s.obs_memos, 0, sizeof(s.obs_memos)); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - CHECK("memo-served inventory block == fresh recompute after the sip", - memcmp(served_block, &obs[COLO_OBS_AFTER_PLAYER], sizeof(served_block)) == 0); } static void test_best_gear_cache_signatures(void) { @@ -9005,7 +8984,7 @@ static void test_best_gear_cache_signatures(void) { int weapon_count = 0; for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { const OsrsItemContentMetadata* metadata = - osrs_inventory_cell_metadata(&s.inventory_cells[cell]); + osrs_inventory_cell_metadata(&s.player.inventory_cells[cell]); uint8_t item = metadata->item_idx; if (item == ITEM_NONE) { if (metadata->raw_osrs_id != 0 && non_gear_cell < 0) @@ -9025,27 +9004,29 @@ static void test_best_gear_cache_signatures(void) { const ColoBestGear (*best_gear)[COLO_NUM_NPC_TYPES] = col_get_best_gear_table(&s); int best_gear_next = s.obs_memos.best_gear_next; - ColoInvCell non_gear = s.inventory_cells[non_gear_cell]; - s.inventory_cells[non_gear_cell] = osrs_inventory_cell_empty(); + ColoInvCell non_gear = s.player.inventory_cells[non_gear_cell]; + s.player.inventory_cells[non_gear_cell] = osrs_inventory_cell_empty(); CHECK("best-gear key ignores non-gear cells", col_best_gear_cache_signature(&s) == best_gear_signature); CHECK("non-gear change reuses best-gear table", col_get_best_gear_table(&s) == best_gear && s.obs_memos.best_gear_next == best_gear_next); - s.inventory_cells[non_gear_cell] = non_gear; - ColoInvCell tmp = s.inventory_cells[weapon_cells[0]]; - s.inventory_cells[weapon_cells[0]] = s.inventory_cells[weapon_cells[1]]; - s.inventory_cells[weapon_cells[1]] = tmp; + s.player.inventory_cells[non_gear_cell] = non_gear; + ColoInvCell tmp = s.player.inventory_cells[weapon_cells[0]]; + s.player.inventory_cells[weapon_cells[0]] = + s.player.inventory_cells[weapon_cells[1]]; + s.player.inventory_cells[weapon_cells[1]] = tmp; CHECK("best-gear key canonicalizes weapon candidate order", col_best_gear_cache_signature(&s) == best_gear_signature); CHECK("weapon reorder reuses best-gear table", col_get_best_gear_table(&s) == best_gear && s.obs_memos.best_gear_next == best_gear_next); - tmp = s.inventory_cells[weapon_cells[0]]; - s.inventory_cells[weapon_cells[0]] = s.inventory_cells[weapon_cells[1]]; - s.inventory_cells[weapon_cells[1]] = tmp; + tmp = s.player.inventory_cells[weapon_cells[0]]; + s.player.inventory_cells[weapon_cells[0]] = + s.player.inventory_cells[weapon_cells[1]]; + s.player.inventory_cells[weapon_cells[1]] = tmp; s.player.current_ranged++; CHECK("best-gear key includes current combat levels", col_best_gear_cache_signature(&s) != best_gear_signature); @@ -9211,6 +9192,7 @@ int main(void) { test_totemic_sol_wave12(); test_totemic_sol_extra_totems_every_two_minutes(); test_obs_signal_defects(); + test_obs_overwrites_dirty_buffer(); test_totem_heal_timing_obs(); test_totem_sol_obs_reports_stacking(); test_reentry_sand_tiles(); @@ -9282,7 +9264,7 @@ int main(void) { test_consumable_overdrink_mask(); test_loadout_surge_potion(); test_loadout_spec_weapons(); - test_colosseum_inventory_memo_tracks_gear_swaps(); + test_shared_inventory_tracks_gear_swaps(); test_colosseum_win_tick_is_not_stacked(); test_colosseum_live_inventory_display(); test_loadout_item_effects(); @@ -9316,9 +9298,8 @@ int main(void) { test_item_obs_table_matches_every_code_semantically(); test_item_obs_empty_row_is_all_zero(); test_item_obs_table_bakes_consumable_hp_heal(); - test_colosseum_item_obs_overlay_overwrites_dynamic_features(); - test_inventory_obs_expansion_is_lossless(); - test_inventory_obs_memo(); + test_inventory_obs_expansion_matches_item_table(); + test_inventory_obs_code_updates(); return osrs_test_summary(); } diff --git a/ocean/osrs/tests/test_encounter_route_topology_setup.c b/ocean/osrs/tests/test_encounter_route_topology_setup.c index 1b92429be3..b1c754e176 100644 --- a/ocean/osrs/tests/test_encounter_route_topology_setup.c +++ b/ocean/osrs/tests/test_encounter_route_topology_setup.c @@ -232,6 +232,10 @@ static const EncounterArenaTopology* finalize_with_map( TestContext* context = calloc(1, sizeof(*context)); if (!context) abort(); TEST_DEF.init_context((EncounterContext*)context); + if (!TEST_DEF.init_state) { + fprintf(stderr, "%s has no embedded-state initializer\n", TEST_DEF.name); + abort(); + } EncounterState* state = TEST_DEF.create(); if (!state) abort(); put_geometry(state, (EncounterContext*)context, map, map_first); diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index 60a4517798..03b84baf0b 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -4,6 +4,7 @@ #include #include +#include "ocean/osrs/osrs_policy.h" #include "ocean/osrs/encounters/encounter_inferno.h" #include "ocean/osrs/tests/osrs_route_reference.h" static void inf_init_unfinalized_context(InfernoContext* ctx) { @@ -366,7 +367,7 @@ static int inf_action_target_for_npc(InfernoState* state, int npc_slot) { int target_slot = inf_find_target_obs_slot(state, npc_slot); ASSERT_INT_EQ("target NPC has observation slot", target_slot >= 0, 1); if (target_slot < 0) return 0; - return target_slot + 1; + return inf_primary_attack_action_for_obs_slot(target_slot); } static int inferno_action_head_mask_offset(int head) { @@ -382,7 +383,7 @@ static void fire_player_action_at_slot_zero( ) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = inf_action_target_for_npc(state, 0); + actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(state, 0); actions[INF_HEAD_SPELL] = spell_action; state->player.attack_timer = 0; encounter_pending_hit_queue_clear(&state->npcs[0].pending_hits); @@ -395,14 +396,6 @@ static int inferno_obs_slot_dig_index(int slot_idx); static int inferno_obs_slot_start(int slot_idx); static void init_zuk_timing_state(InfernoState* state); -enum { - INF_OBS_WAVE_NORM = 10, - INF_OBS_WAVE_PHASE = 11, - INF_OBS_ZUK_ATTACK_TIMER = 12, - INF_OBS_ZUK_PHASE_START = 36, - INF_OBS_ZUK_SHIELD_DIR = 36, - INF_OBS_ZUK_SHIELD_FREEZE = 37, -}; static void init_jad_timing_test_state(InfernoState* state, int player_x, int player_y, int jad_x, int jad_y) { if (player_x == 10 && player_y == 10) { @@ -574,7 +567,7 @@ static void assert_supply_doses(const char* label, static int test_occupied_inventory_cells(const InfernoState* s) { int occupied = 0; for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - if (!osrs_inventory_cell_is_empty(&s->inventory_cells[c])) + if (!osrs_inventory_cell_is_empty(&s->player.inventory_cells[c])) occupied++; } return occupied; @@ -582,7 +575,7 @@ static int test_occupied_inventory_cells(const InfernoState* s) { static int test_cell_holding_item(const InfernoState* s, uint8_t item) { for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - if (osrs_inventory_cell_item_index(&s->inventory_cells[c]) == item) + if (osrs_inventory_cell_item_index(&s->player.inventory_cells[c]) == item) return c; } return -1; @@ -591,7 +584,7 @@ static int test_cell_holding_item(const InfernoState* s, uint8_t item) { static int test_cell_doses_of_kind(const InfernoState* s, OsrsConsumableKind kind) { int doses = 0; for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { - const OsrsInventoryCell* cell = &s->inventory_cells[c]; + const OsrsInventoryCell* cell = &s->player.inventory_cells[c]; if (osrs_inventory_cell_raw_osrs_id(cell) == 0) continue; if (osrs_consumable_click_lookup_raw_osrs_id( osrs_inventory_cell_raw_osrs_id(cell)).consumable_kind == kind) @@ -603,7 +596,7 @@ static int test_cell_doses_of_kind(const InfernoState* s, OsrsConsumableKind kin static OsrsConsumableKind test_drink_click_kind(const InfernoState* s, int action) { if (action <= 0) return OSRS_CONSUMABLE_NONE; return osrs_consumable_click_lookup_raw_osrs_id( - osrs_inventory_cell_raw_osrs_id(&s->inventory_cells[action - 1])).consumable_kind; + osrs_inventory_cell_raw_osrs_id(&s->player.inventory_cells[action - 1])).consumable_kind; } static void test_final_wave_reward_applies_healer_tags_and_heal_cost(void) { @@ -1097,7 +1090,7 @@ static void test_zuk_untagged_healer_target_bonus_defaults_off(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = inf_action_target_for_npc(&state, 2); + actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(&state, 2); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("default target bonus pays no per-tick target reward", @@ -1144,7 +1137,7 @@ static void test_zuk_untagged_healer_target_bonus_rewards_distinct_healers(void) int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = first_healer_slot + 1; + actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(first_healer_slot); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("first untagged healer target rewarded", @@ -1155,13 +1148,13 @@ static void test_zuk_untagged_healer_target_bonus_rewards_distinct_healers(void) inf_compute_reward(&state), 0.07f, 0.0001f); state.tick_scratch.zuk_untagged_healer_targets = 0; - actions[INF_HEAD_TARGET] = first_healer_slot + 1; + actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(first_healer_slot); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("repeat target does not reward twice", state.tick_scratch.zuk_untagged_healer_targets, 0); - actions[INF_HEAD_TARGET] = second_healer_slot + 1; + actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(second_healer_slot); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("second distinct untagged healer target rewarded", @@ -1191,7 +1184,7 @@ static void test_zuk_safe_untagged_healer_target_bonus_records_safe_subset(void) int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = inf_action_target_for_npc(&state, 2); + actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(&state, 2); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("safe untagged healer target attempt", @@ -1228,7 +1221,7 @@ static void test_zuk_untagged_healer_target_bonus_excludes_tagged_healers(void) int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = inf_action_target_for_npc(&state, 2); + actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(&state, 2); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("already tagged healer target gets no bonus", @@ -1473,7 +1466,7 @@ static void test_offensive_prayer_no_attack_no_event(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = inf_action_target_for_npc(&state, 0); + actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(&state, 0); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("cooldown prevents attack", state.tick_scratch.player_attacked, 0); @@ -1765,11 +1758,13 @@ static int test_player_slot_inventory_contains( int gear_slot, uint8_t item ) { - for (int i = 0; i < p->num_items_in_slot[gear_slot]; i++) { - if (p->inventory[gear_slot][i] == item) + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&p->inventory_cells[cell]); + if (metadata->gear_slot == gear_slot && metadata->item_idx == item) return 1; } - return 0; + return p->equipped[gear_slot] == item; } static void test_inferno_reset_inventory_leaves_one_empty_slot(void) { @@ -1955,8 +1950,6 @@ static void test_inferno_equip_actions_move_cells_and_sync_weapon_set(void) { inf_destroy(raw_state); } -/* the random-action golden battery cannot see this: it drives INF_HEAD_MOVE > 0 on - 24/25 ticks, and that move interrupt already clears the interaction. */ static void test_inferno_gear_switch_cancels_entity_interaction(void) { printf("--- inferno gear switch cancels entity interaction ---\n"); @@ -2268,30 +2261,40 @@ static void test_late_start_supply_observations(void) { EncounterState* raw_state = inf_create(); InfernoState* state = (InfernoState*)raw_state; - InfSupplyDoses full = inf_full_starting_supplies(); float obs[INF_NUM_OBS]; reset_inferno_at_public_wave(raw_state, 69, 1.0f); inf_write_obs(raw_state, obs); - enum { - INF_OBS_BREW_DOSES = 5, - INF_OBS_RESTORE_DOSES = 6, - INF_OBS_BASTION_DOSES = 8, - INF_OBS_STAMINA_DOSES = 9, - }; - ASSERT_FLOAT_NEAR("brew obs uses full-kit denominator", - obs[INF_OBS_BREW_DOSES], - (float)state->player.brew_doses / (float)full.brew_doses, 0.0001f); - ASSERT_FLOAT_NEAR("restore obs uses full-kit denominator", - obs[INF_OBS_RESTORE_DOSES], - (float)state->player.restore_doses / (float)full.restore_doses, 0.0001f); - ASSERT_FLOAT_NEAR("bastion obs uses full-kit denominator", - obs[INF_OBS_BASTION_DOSES], - (float)state->player.bastion_doses / (float)full.bastion_doses, 0.0001f); - ASSERT_FLOAT_NEAR("stamina obs uses full-kit denominator", - obs[INF_OBS_STAMINA_DOSES], - (float)state->player.stamina_doses / (float)full.stamina_doses, 0.0001f); + int observed_brew_doses = 0; + int observed_restore_doses = 0; + int observed_bastion_doses = 0; + int observed_stamina_doses = 0; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + int offset = OSRS_SHARED_OBS_INVENTORY_START + + cell * OSRS_SHARED_INVENTORY_CELL_OBS_FEATURES; + uint16_t content_code = + osrs_inventory_cell_obs_code_decode(obs[offset]); + const OsrsItemContentMetadata* metadata = + osrs_item_content_metadata(content_code); + if (metadata->consumable_kind == OSRS_CONSUMABLE_BREW) { + observed_brew_doses += metadata->dose_count; + } else if (metadata->consumable_kind == OSRS_CONSUMABLE_SUPER_RESTORE) { + observed_restore_doses += metadata->dose_count; + } else if (metadata->consumable_kind == OSRS_CONSUMABLE_BASTION) { + observed_bastion_doses += metadata->dose_count; + } else if (metadata->consumable_kind == OSRS_CONSUMABLE_STAMINA) { + observed_stamina_doses += metadata->dose_count; + } + } + ASSERT_INT_EQ("shared inventory exposes brew doses", + observed_brew_doses, state->player.brew_doses); + ASSERT_INT_EQ("shared inventory exposes restore doses", + observed_restore_doses, state->player.restore_doses); + ASSERT_INT_EQ("shared inventory exposes bastion doses", + observed_bastion_doses, state->player.bastion_doses); + ASSERT_INT_EQ("shared inventory exposes stamina doses", + observed_stamina_doses, state->player.stamina_doses); inf_destroy(raw_state); } @@ -3359,7 +3362,7 @@ static void test_triple_jad_pending_threats_fit_obs_layout(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); - ASSERT_INT_EQ("inferno obs shape uses compact layout", INF_NUM_OBS, 498); + ASSERT_INT_EQ("inferno obs uses shared-prefix layout", INF_NUM_OBS, 530); } static void test_inferno_action_and_compact_obs_shape(void) { @@ -3374,26 +3377,27 @@ static void test_inferno_action_and_compact_obs_shape(void) { INF_ACTION_DIMS[INF_HEAD_EAT], OSRS_INVENTORY_SIZE + 1); ASSERT_INT_EQ("drink head clicks cover every cell", INF_ACTION_DIMS[INF_HEAD_DRINK], OSRS_INVENTORY_SIZE + 1); - ASSERT_INT_EQ("prayer action head includes redemption", - INF_ACTION_DIMS[INF_HEAD_PRAYER], ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION); - ASSERT_INT_EQ("action mask spans all inventory click heads", - INF_ACTION_MASK_SIZE, 434); + ASSERT_INT_EQ("prayer action head uses shared overhead actions", + INF_ACTION_DIMS[INF_HEAD_PRAYER], OSRS_OVERHEAD_DIM); + ASSERT_INT_EQ("action mask spans the shared action heads", + INF_ACTION_MASK_SIZE, 436); ASSERT_SOURCE_BLOCK_CONTAINS("native binding reuses inferno action dims", "ocean/osrs_inferno/osrs_inferno.h", "#define OBS_SIZE INF_NUM_OBS", "typedef float obs_t;", "#define ACT_SIZES INF_ACTION_DIMS_INIT"); - ASSERT_INT_EQ("compact player observation width", INF_PLAYER_OBS_SIZE, 45); + ASSERT_INT_EQ("shared player observation width", + INF_OBS_AFTER_SHARED, OSRS_SHARED_OBS_SIZE); ASSERT_INT_EQ("compact pillar observation width", INF_PILLAR_OBS_SIZE, 9); ASSERT_INT_EQ("compact NPC observation width", INF_TOTAL_NPC_OBS_SIZE, INF_OBS_NPCS * INF_NPC_SLOT_FEATURES); ASSERT_INT_EQ("compact spark observation width", INF_PENDING_SPARK_OBS_SIZE, 128); - ASSERT_INT_EQ("inventory observation uses one code per cell", - INF_INVENTORY_OBS_SIZE, OSRS_INVENTORY_SIZE); - ASSERT_INT_EQ("equipment observation uses shared aggregate", - INF_EQUIPPED_OBS_SIZE, OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES); - ASSERT_INT_EQ("inferno observation width", INF_NUM_OBS, 498); + ASSERT_INT_EQ("inventory observation uses shared cell records", + INF_INVENTORY_OBS_SIZE, OSRS_SHARED_INVENTORY_OBS_SIZE); + ASSERT_INT_EQ("equipment observation uses shared worn slots", + INF_EQUIPPED_OBS_SIZE, OSRS_SHARED_EQUIPPED_OBS_SIZE); + ASSERT_INT_EQ("inferno observation width", INF_NUM_OBS, 530); ASSERT_INFERNO_SOURCE_NOT_CONTAINS("armor_tank state is removed", "armor_tank"); ASSERT_INFERNO_SOURCE_NOT_CONTAINS("extra npc obs scaffold is removed", @@ -3656,20 +3660,20 @@ static void test_jad_melee_stays_instant_and_untelegraphed(void) { } static int inferno_obs_slot_start(int slot_idx) { - return INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + + return INF_OBS_AFTER_PILLARS + slot_idx * INF_NPC_SLOT_FEATURES; } static int inferno_pillar_obs_start(int pillar_idx) { - return INF_OBS_AFTER_PLAYER + pillar_idx * INF_PILLAR_FEATURES; + return INF_OBS_AFTER_ENCOUNTER + pillar_idx * INF_PILLAR_FEATURES; } static int inferno_target_mask_slot_offset(int slot_idx) { - return ENCOUNTER_MOVE_ACTIONS + ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION + 1 + slot_idx; + return inf_primary_attack_action_for_obs_slot(slot_idx); } static int inferno_target_mask_none_offset(void) { - return ENCOUNTER_MOVE_ACTIONS + ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION; + return 0; } @@ -4509,7 +4513,7 @@ static void test_step_out_same_tick_ranger_mager_event_logs(void) { &state, 1, INF_NPC_MAGER, 4, 34, 0); int actions[INF_NUM_ACTION_HEADS] = {0}; - actions[INF_HEAD_MOVE] = 13; + actions[INF_HEAD_PRIMARY] = 13; inf_step((EncounterState*)&state, actions); ASSERT_INT_EQ("step-out tick moved the player", @@ -4916,17 +4920,17 @@ static void test_zuk_healer_blowpipe_target_chases_out_of_range(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = healer_slot + 1; + actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(healer_slot); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("healer target keeps interaction active", osrs_interaction_active(&state.interaction), 1); ASSERT_INT_EQ("healer target selects healer", state.interaction.target_slot, 2); - ASSERT_INT_EQ("healer target follows reference seek x", state.player.x, 23); - ASSERT_INT_EQ("healer target follows reference seek y", state.player.y, 42); - ASSERT_INT_EQ("healer target remains active while still out of range", + ASSERT_INT_EQ("healer target follows shortest seek x", state.player.x, 25); + ASSERT_INT_EQ("healer target follows shortest seek y", state.player.y, 43); + ASSERT_INT_EQ("healer target reaches blowpipe range", inf_player_can_attack_npc_from_current_tile_ctx( - &state, inf_legacy_context(), 2), 0); + &state, inf_legacy_context(), 2), 1); ASSERT_INT_EQ("cooldown prevents immediate healer hit", state.npcs[2].pending_hits.hits[0].active, 0); ASSERT_INT_EQ("attack timer decrements after healer chase", @@ -5396,7 +5400,7 @@ static void test_zuk_healer_target_action_tags_on_landed_hit(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = healer_slot + 1; + actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(healer_slot); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("target action selects zuk healer", @@ -5460,7 +5464,7 @@ static void test_zuk_healer_mage_attack_counts_penalty_event(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = inf_action_target_for_npc(&state, 2); + actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(&state, 2); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("mage healer attack fires once", @@ -5986,7 +5990,7 @@ static void test_phantom_barrage_hits_aoe_on_first_cast_window(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = target_slot + 1; + actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(target_slot); actions[INF_HEAD_SPELL] = 2; inf_tick_player(&state, actions, 1); @@ -6095,7 +6099,7 @@ static void test_phantom_barrage_close_barrage_timing_cannot_recast(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = target_slot + 1; + actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(target_slot); inf_tick_player(&state, actions, 1); ASSERT_INT_EQ("cooldown prevents phantom barrage fire", @@ -6610,7 +6614,7 @@ static void test_phantom_barrage_allows_explicit_spell_from_range_gear(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - actions[INF_HEAD_TARGET] = target_slot + 1; + actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(target_slot); actions[INF_HEAD_SPELL] = 2; inf_tick_player(&state, actions, 1); @@ -7033,20 +7037,29 @@ static void test_human_target_and_potion_translation(void) { hi = make_human_input(); hi.pending_target_idx = 0; inf_translate_human_input(&hi, actions, (EncounterState*)&state); - ASSERT_INT_EQ("visible mager click maps into target head", - actions[INF_HEAD_TARGET], 1); + ASSERT_INT_EQ("visible mager click maps into primary target range", + actions[INF_HEAD_PRIMARY], + inf_primary_attack_action_for_obs_slot(0)); hi = make_human_input(); hi.pending_target_idx = 2; inf_translate_human_input(&hi, actions, (EncounterState*)&state); ASSERT_INT_EQ("capped-out mager click is rejected", - actions[INF_HEAD_TARGET], 0); + actions[INF_HEAD_PRIMARY], 0); + + hi = make_human_input(); + hi.pending_move_x = state.player.x + 1; + hi.pending_move_y = state.player.y; + hi.pending_target_idx = 2; + inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("untargetable click preserves queued east move", + actions[INF_HEAD_PRIMARY], 3); hi = make_human_input(); hi.pending_target_idx = 3; inf_translate_human_input(&hi, actions, (EncounterState*)&state); ASSERT_INT_EQ("shield click is rejected", - actions[INF_HEAD_TARGET], 0); + actions[INF_HEAD_PRIMARY], 0); state.player.brew_doses = 8; state.player.restore_doses = 8; @@ -7129,7 +7142,8 @@ static void test_human_targeting_refreshes_stale_obs_slots(void) { ASSERT_INT_EQ("pending target refresh validates obs slots", state.current_obs_slots_valid, 1); ASSERT_INT_EQ("pending human target refreshes obs slot", - actions[INF_HEAD_TARGET], ranger_slot + 1); + actions[INF_HEAD_PRIMARY], + inf_primary_attack_action_for_obs_slot(ranger_slot)); } { @@ -7145,7 +7159,8 @@ static void test_human_targeting_refreshes_stale_obs_slots(void) { ASSERT_INT_EQ("queued target refresh validates obs slots", state.current_obs_slots_valid, 1); ASSERT_INT_EQ("queued human target refreshes obs slot", - actions[INF_HEAD_TARGET], ranger_slot + 1); + actions[INF_HEAD_PRIMARY], + inf_primary_attack_action_for_obs_slot(ranger_slot)); human_input_destroy(&hi); } } @@ -7158,7 +7173,7 @@ static void test_human_spell_selection_is_client_local_until_target_click(void) HumanInput hi; human_input_init(&hi); hi.cursor_mode = CURSOR_SPELL_TARGET; - hi.selected_spell = ATTACK_BLOOD; + hi.selected_spell = OSRS_SPELL_BLOOD_BARRAGE; int actions[INF_NUM_ACTION_HEADS]; inf_translate_human_commands(&hi, actions, &state); @@ -7167,14 +7182,14 @@ static void test_human_spell_selection_is_client_local_until_target_click(void) ASSERT_INT_EQ("client-only selection sends no spell action", actions[INF_HEAD_SPELL], 0); - human_input_queue_spell_target(&hi, ATTACK_BLOOD, 0); + human_input_queue_spell_target(&hi, OSRS_SPELL_BLOOD_BARRAGE, 0); inf_translate_human_commands(&hi, actions, &state); ASSERT_INT_EQ("spell target command kind", hi.commands.items[0].kind, HUMAN_COMMAND_SPELL_TARGET); ASSERT_INT_EQ("spell target command carries blood", - hi.commands.items[0].spell, ATTACK_BLOOD); + hi.commands.items[0].spell, OSRS_SPELL_BLOOD_BARRAGE); ASSERT_INT_EQ("spell target command maps spell action", - actions[INF_HEAD_SPELL], 1); + actions[INF_HEAD_SPELL], OSRS_SPELL_BLOOD_BARRAGE); human_input_destroy(&hi); } @@ -7187,15 +7202,15 @@ static void test_human_walk_command_sends_no_selected_spell_cast(void) { HumanInput hi; human_input_init(&hi); hi.cursor_mode = CURSOR_SPELL_TARGET; - hi.selected_spell = ATTACK_ICE; + hi.selected_spell = OSRS_SPELL_ICE_BARRAGE; human_input_queue_walk(&hi, 20, 20); int actions[INF_NUM_ACTION_HEADS]; inf_translate_human_commands(&hi, actions, &state); ASSERT_INT_EQ("walk command sends no spell action", actions[INF_HEAD_SPELL], 0); - ASSERT_INT_EQ("walk command sends no target action", - actions[INF_HEAD_TARGET], 0); + ASSERT_INT_EQ("walk command sends no primary action", + actions[INF_HEAD_PRIMARY], 0); human_input_destroy(&hi); } @@ -7299,14 +7314,15 @@ static void test_redemption_action_maps_without_smite(void) { state.player.current_prayer = 99; int actions[INF_NUM_ACTION_HEADS] = {0}; - actions[INF_HEAD_PRAYER] = INF_OVERHEAD_SET_REFRESH_REDEMPTION; + actions[INF_HEAD_PRAYER] = + ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION; inf_player_pretick(&state, ctx, actions); - ASSERT_INT_EQ("inferno exposes six overhead actions", - INF_ACTION_DIMS[INF_HEAD_PRAYER], ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION); - ASSERT_INT_EQ("local action five is redemption", + ASSERT_INT_EQ("inferno uses the shared overhead actions", + INF_ACTION_DIMS[INF_HEAD_PRAYER], OSRS_OVERHEAD_DIM); + ASSERT_INT_EQ("shared redemption action activates redemption", state.player.prayer, PRAYER_REDEMPTION); - ASSERT_INT_EQ("inferno action five is not smite", + ASSERT_INT_EQ("redemption action does not activate smite", state.player.prayer == PRAYER_SMITE, 0); } @@ -7423,13 +7439,13 @@ static void test_inferno_snapshot_restore_round_trip(void) { inf_reset(raw, 31415u); int actions_a[INF_NUM_ACTION_HEADS] = {0}; - actions_a[INF_HEAD_MOVE] = 1; - actions_a[INF_HEAD_TARGET] = 1; + actions_a[INF_HEAD_PRIMARY] = 1; + actions_a[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(1 - 1); actions_a[INF_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; int actions_b[INF_NUM_ACTION_HEADS] = {0}; - actions_b[INF_HEAD_MOVE] = 5; - actions_b[INF_HEAD_TARGET] = 2; + actions_b[INF_HEAD_PRIMARY] = 5; + actions_b[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(2 - 1); actions_b[INF_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; const int N1 = 12; @@ -7617,8 +7633,8 @@ static void test_inferno_state_assignment_copy_replays_trajectory(void) { inf_reset_ctx((EncounterState*)&state_a, (EncounterContext*)&ctx_a, 987u); int prefix[INF_NUM_ACTION_HEADS] = {0}; - prefix[INF_HEAD_MOVE] = 1; - prefix[INF_HEAD_TARGET] = 1; + prefix[INF_HEAD_PRIMARY] = 1; + prefix[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(1 - 1); prefix[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = test_cell_holding_item(&state_a, ITEM_TWISTED_BOW) + 1; @@ -7629,8 +7645,8 @@ static void test_inferno_state_assignment_copy_replays_trajectory(void) { inf_refresh_after_state_load(&state_b, &ctx_b); int suffix[INF_NUM_ACTION_HEADS] = {0}; - suffix[INF_HEAD_MOVE] = 5; - suffix[INF_HEAD_TARGET] = 2; + suffix[INF_HEAD_PRIMARY] = 5; + suffix[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(2 - 1); suffix[INF_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; suffix[INF_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; @@ -8957,10 +8973,10 @@ static void activate_dense_target_test_npc( static void test_dense_target_contract_dimensions(void) { printf("--- dense target contract dimensions ---\n"); - ASSERT_INT_EQ("dense target NPC slots", INF_OBS_NPCS, 14); - ASSERT_INT_EQ("target head includes no-target action", - INF_ACTION_DIMS[INF_HEAD_TARGET], 15); - ASSERT_INT_EQ("dense target action mask width", INF_ACTION_MASK_SIZE, 434); + ASSERT_INT_EQ("primary head includes movement and every target", + INF_ACTION_DIMS[INF_HEAD_PRIMARY], OSRS_PRIMARY_DIM(INF_OBS_NPCS)); + ASSERT_INT_EQ("shared primary action mask width", + INF_ACTION_MASK_SIZE, 436); } static void test_dense_target_slots_follow_type_priority_without_holes(void) { @@ -9083,21 +9099,24 @@ static void test_dense_target_overflow_aborts_instead_of_truncating(void) { static void test_compact_observation_layout_contract(void) { printf("--- compact observation layout contract ---\n"); - ASSERT_INT_EQ("compact player width", INF_PLAYER_OBS_SIZE, 45); + ASSERT_INT_EQ("shared prefix width", INF_OBS_AFTER_SHARED, 101); + ASSERT_INT_EQ("inferno encounter width", INF_ENCOUNTER_OBS_SIZE, 14); ASSERT_INT_EQ("compact pillar width", INF_PILLAR_OBS_SIZE, 9); ASSERT_INT_EQ("compact NPC stride", INF_NPC_SLOT_FEATURES, 13); ASSERT_INT_EQ("compact pending hit stride", INF_FEATURES_PER_HIT, 3); ASSERT_INT_EQ("compact spark stride", INF_FEATURES_PER_SPARK, 4); - ASSERT_INT_EQ("inventory carries one code per cell", INF_INVENTORY_OBS_SIZE, 28); - ASSERT_INT_EQ("equipment aggregate width", INF_EQUIPPED_OBS_SIZE, 10); + ASSERT_INT_EQ("inventory carries one canonical code per cell", + INF_INVENTORY_OBS_SIZE, 28); + ASSERT_INT_EQ("equipment carries one canonical code per worn slot", + INF_EQUIPPED_OBS_SIZE, NUM_GEAR_SLOTS); - ASSERT_INT_EQ("compact player end", INF_OBS_AFTER_PLAYER, 45); - ASSERT_INT_EQ("compact pillar end", INF_OBS_AFTER_PILLARS, 54); - ASSERT_INT_EQ("compact NPC end", INF_OBS_AFTER_NPCS, 236); - ASSERT_INT_EQ("compact pending hit end", INF_OBS_AFTER_PENDING_HITS, 332); - ASSERT_INT_EQ("compact spark end", INF_OBS_AFTER_SPARKS, 460); - ASSERT_INT_EQ("compact inventory end", INF_OBS_AFTER_INVENTORY, 488); - ASSERT_INT_EQ("compact observation width", INF_NUM_OBS, 498); + ASSERT_INT_EQ("shared prefix end", INF_OBS_AFTER_SHARED, 101); + ASSERT_INT_EQ("inferno encounter end", INF_OBS_AFTER_ENCOUNTER, 115); + ASSERT_INT_EQ("compact pillar end", INF_OBS_AFTER_PILLARS, 124); + ASSERT_INT_EQ("compact NPC end", INF_OBS_AFTER_NPCS, 306); + ASSERT_INT_EQ("compact pending hit end", INF_OBS_AFTER_PENDING_HITS, 402); + ASSERT_INT_EQ("compact spark end", INF_OBS_AFTER_SPARKS, 530); + ASSERT_INT_EQ("inferno observation width", INF_NUM_OBS, 530); } static void test_compact_player_and_pillar_observation_semantics(void) { @@ -9126,25 +9145,24 @@ static void test_compact_player_and_pillar_observation_semantics(void) { float obs[INF_NUM_OBS]; inf_write_obs((EncounterState*)&state, obs); - ASSERT_FLOAT_NEAR("overhead prayer uses raw compact code", - obs[3], (float)PRAYER_PROTECT_MAGIC / 8.0f, 1e-6f); - ASSERT_FLOAT_NEAR("high offensive prayer uses compact code", - obs[4], 3.0f / 4.0f, 1e-6f); + ASSERT_FLOAT_NEAR("shared overhead prayer is one-hot", + obs[8], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("shared offensive prayer is one-hot", + obs[13], 1.0f, 1e-6f); ASSERT_FLOAT_NEAR("wave phase uses compact code", - obs[11], (float)(inf_wave_phase_index(state.wave) + 1) / 8.0f, 1e-6f); - ASSERT_FLOAT_NEAR("weapon set uses compact code", - obs[13], (float)(INF_GEAR_BP + 1) / 4.0f, 1e-6f); - ASSERT_FLOAT_NEAR("Zuk detail starts without is-Zuk feature", - obs[40], 1.0f, 1e-6f); + obs[INF_OBS_WAVE_PHASE], + (float)(inf_wave_phase_index(state.wave) + 1) / 8.0f, 1e-6f); + ASSERT_FLOAT_NEAR("Zuk enraged state follows the shared prefix", + obs[INF_OBS_ZUK_PHASE_START + 4], 1.0f, 1e-6f); ASSERT_FLOAT_NEAR("compact pillar hp", - obs[INF_OBS_AFTER_PLAYER], + obs[INF_OBS_AFTER_ENCOUNTER], (float)state.pillars[0].hp / (float)INF_PILLAR_HP, 1e-6f); ASSERT_FLOAT_NEAR("compact pillar relative x", - obs[INF_OBS_AFTER_PLAYER + 1], + obs[INF_OBS_AFTER_ENCOUNTER + 1], 4.0f / (float)INF_ARENA_WIDTH, 1e-6f); ASSERT_FLOAT_NEAR("compact pillar relative y", - obs[INF_OBS_AFTER_PLAYER + 2], + obs[INF_OBS_AFTER_ENCOUNTER + 2], 6.0f / (float)INF_ARENA_HEIGHT, 1e-6f); } @@ -9258,11 +9276,15 @@ static void test_compact_transient_inventory_equipment_semantics(void) { .damage = 8, }; for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) - state.inventory_cells[cell] = osrs_inventory_cell_empty(); - state.inventory_cells[0] = + state.player.inventory_cells[cell] = osrs_inventory_cell_empty(); + state.player.inventory_cells[0] = osrs_inventory_cell_from_item(ITEM_OSMUMTENS_FANG); - state.inventory_cells[1] = + state.player.inventory_cells[1] = osrs_inventory_cell_from_raw_osrs_id(6685); + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + state.player.equipped[slot] = ITEM_NONE; + state.player.equipped[GEAR_SLOT_WEAPON] = ITEM_TWISTED_BOW; + state.player.equipped[GEAR_SLOT_SHIELD] = ITEM_ELYSIAN_SPIRIT_SHIELD; state.player.equipment_effect_profile = (OsrsEquipmentEffectProfile){ .effect_mask = OSRS_ITEM_EFFECT_BLOOD_FURY | OSRS_ITEM_EFFECT_LIGHTBEARER, @@ -9295,12 +9317,24 @@ static void test_compact_transient_inventory_equipment_semantics(void) { obs_off[INF_OBS_AFTER_PENDING_HITS + 3], 0.8f, 1e-6f); for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { - const OsrsInventoryCell* inventory_cell = &state.inventory_cells[cell]; - float expected_code = + const OsrsInventoryCell* inventory_cell = + &state.player.inventory_cells[cell]; + int inventory_offset = OSRS_SHARED_OBS_INVENTORY_START + + cell * OSRS_SHARED_INVENTORY_CELL_OBS_FEATURES; + ASSERT_FLOAT_NEAR("shared inventory cell code", + obs_off[inventory_offset], osrs_inventory_cell_obs_code_encode( - inventory_cell->content_code); - ASSERT_FLOAT_NEAR("compact inventory cell code", - obs_off[INF_OBS_AFTER_SPARKS + cell], expected_code, 1e-6f); + inventory_cell->content_code), + 1e-6f); + } + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + uint8_t item = state.player.equipped[slot]; + uint16_t content_code = item == ITEM_NONE + ? 0 : osrs_inventory_content_code_from_item(item); + ASSERT_FLOAT_NEAR("shared worn equipment code", + obs_off[OSRS_SHARED_OBS_EQUIPPED_START + slot], + osrs_inventory_cell_obs_code_encode(content_code), 1e-6f); } float expected_equipment[OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES]; @@ -9309,8 +9343,8 @@ static void test_compact_transient_inventory_equipment_semantics(void) { for (int feature = 0; feature < OSRS_EQUIPMENT_EFFECT_AGGREGATE_FEATURES; feature++) { - ASSERT_FLOAT_NEAR("compact equipment aggregate", - obs_off[INF_OBS_AFTER_INVENTORY + feature], + ASSERT_FLOAT_NEAR("shared equipment effect aggregate", + obs_off[OSRS_SHARED_OBS_EFFECT_START + feature], expected_equipment[feature], 1e-6f); } @@ -9421,11 +9455,19 @@ static void test_inferno_topology_geometry_parity(void) { &state, x, y, size); int actual = inf_footprint_blocked_ctx( &state, ctx, x, y, size); - if (expected != actual) { + int topology_actual = + encounter_arena_topology_footprint_blocked( + inf_route_topology_for_state(ctx, &state), + x, + y, + size); + if (expected != actual || + expected != topology_actual) { printf( " FAIL: footprint phase=%d anchor=(%d,%d) " - "size=%d expected=%d actual=%d\n", - phase, x, y, size, expected, actual); + "size=%d expected=%d actual=%d topology=%d\n", + phase, x, y, size, expected, actual, + topology_actual); tests_failed++; tests_run++; return; @@ -9635,7 +9677,7 @@ static void test_inferno_topology_observation_mask_identity(void) { } int movement_offset = 0; - for (int head = 0; head < INF_HEAD_MOVE; head++) + for (int head = 0; head < INF_HEAD_PRIMARY; head++) movement_offset += INF_ACTION_DIMS[head]; for (int action = 0; action < ENCOUNTER_MOVE_ACTIONS; action++) { int x = state.player.x + ENCOUNTER_MOVE_TARGET_DX[action]; @@ -9652,6 +9694,24 @@ static void test_inferno_topology_observation_mask_identity(void) { } +static void test_observation_overwrites_dirty_buffer(void) { + printf("test_observation_overwrites_dirty_buffer\n"); + EncounterState* raw_state = inf_create(); + for (int public_wave = 62; public_wave <= 69; public_wave += 7) { + reset_inferno_at_public_wave(raw_state, public_wave, 1.0f); + float clean[INF_NUM_OBS] = {0}; + float dirty[INF_NUM_OBS]; + memset(dirty, 0x7f, sizeof(dirty)); + inf_write_obs(raw_state, clean); + inf_write_obs(raw_state, dirty); + ASSERT_INT_EQ( + "Inferno observation overwrites every output", + memcmp(clean, dirty, sizeof(clean)), + 0); + } + inf_destroy(raw_state); +} + int main(void) { inf_build_npc_stats(); inf_finalize_route_topology(inf_legacy_context()); @@ -9661,6 +9721,7 @@ int main(void) { test_compact_observation_layout_contract(); test_compact_player_and_pillar_observation_semantics(); test_compact_npc_observation_semantics(); + test_observation_overwrites_dirty_buffer(); test_compact_transient_inventory_equipment_semantics(); test_dense_target_contract_dimensions(); test_dense_target_slots_follow_type_priority_without_holes(); diff --git a/ocean/osrs/tests/test_inferno_golden.c b/ocean/osrs/tests/test_inferno_golden.c index 22b67e859c..e909b6a8bd 100644 --- a/ocean/osrs/tests/test_inferno_golden.c +++ b/ocean/osrs/tests/test_inferno_golden.c @@ -65,7 +65,7 @@ static uint64_t hash_core_state(uint64_t h, const InfernoState* s) { h = fnv_bytes(h, &s->player_pending_hits, sizeof(s->player_pending_hits)); h = fnv_bytes(h, s->pending_sparks, sizeof(s->pending_sparks)); for (int cell_idx = 0; cell_idx < OSRS_INVENTORY_SIZE; cell_idx++) { - const OsrsInventoryCell* cell = &s->inventory_cells[cell_idx]; + const OsrsInventoryCell* cell = &s->player.inventory_cells[cell_idx]; h = fnv_u8(h, osrs_inventory_cell_item_index(cell)); h = fnv_u16(h, osrs_inventory_cell_raw_osrs_id(cell)); h = fnv_u8(h, osrs_inventory_cell_dose_count(cell)); @@ -82,17 +82,20 @@ static uint64_t hash_semantic_mask( ) { int offset = 0; for (int head = 0; head < INF_NUM_ACTION_HEADS; head++) { - if (head != INF_HEAD_TARGET) { + if (head != INF_HEAD_PRIMARY) { for (int action = 0; action < INF_ACTION_DIMS[head]; action++) h = fnv_f32(h, mask[offset + action]); offset += INF_ACTION_DIMS[head]; continue; } - h = fnv_f32(h, mask[offset]); + for (int action = 0; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) + h = fnv_f32(h, mask[offset + action]); for (int npc_idx = 0; npc_idx < TRACE_NPC_SLOTS; npc_idx++) { int slot = inf_find_target_obs_slot(s, npc_idx); - h = fnv_f32(h, slot >= 0 ? mask[offset + slot + 1] : 0.0f); + h = fnv_f32(h, slot >= 0 + ? mask[offset + inf_primary_attack_action_for_obs_slot(slot)] + : 0.0f); } offset += INF_ACTION_DIMS[head]; } @@ -133,14 +136,8 @@ static TraceHashes run_episode(int start_wave, uint32_t seed, int max_ticks) { inf_refresh_current_obs_slots_ctx(s, &context); for (int head = 0; head < INF_NUM_ACTION_HEADS; head++) { uint64_t random_value = splitmix64(&arng); - if (head != INF_HEAD_TARGET) { - actions[head] = - (int)(random_value % (uint64_t)INF_ACTION_DIMS[head]); - continue; - } - int npc_idx = (int)(random_value % TRACE_NPC_SLOTS); - int slot = inf_find_target_obs_slot(s, npc_idx); - actions[head] = slot >= 0 ? slot + 1 : 0; + actions[head] = + (int)(random_value % (uint64_t)INF_ACTION_DIMS[head]); } inf_step_ctx( @@ -199,47 +196,40 @@ static const GoldenConfig CONFIGS[] = { #define NUM_CONFIGS ((int)(sizeof(CONFIGS) / sizeof(CONFIGS[0]))) #define EPISODE_TICKS 2000 +/* Canonical inventory cells intentionally change the serialized player state hash. */ static const uint64_t EXPECTED_STATE[NUM_CONFIGS] = { - 0x3e4fbb35c1889d44ULL, 0x8c68f1dc6f54c2a6ULL, 0x71d36e99ebd433e8ULL, - 0x9c028c5906c58dbeULL, 0xd6183313a76b807fULL, 0x28b5ad296c307915ULL, - 0x1e408a9f378394e6ULL, 0x228334e4bd9b7f3aULL, 0x26463ee9c8e17cc8ULL, - 0x2762fdd6008979ddULL, 0xd9f8faf40e2d0657ULL, 0xff5ceeb4f52cb16fULL, - 0xef0befe2d848d410ULL, 0x38385138dce9f842ULL, 0x7444a507700231c1ULL, + 0xd1a32c595e0f20e4ULL, 0xd556d9d18290dbe0ULL, 0x7dc08bb94dc1420aULL, + 0x005ebedd7892aa36ULL, 0xde2fb70f9d751137ULL, 0x985dd084df5d7707ULL, + 0xed97211bab4b9a47ULL, 0x8ea493a4516b791fULL, 0x6cebcf139b64d613ULL, + 0x8dc44e7252f56c19ULL, 0xe5c5146c16159d32ULL, 0x237eb63b95cd8ee8ULL, + 0xc53b4ecacd50a647ULL, 0xda3e4d8630b526f2ULL, 0x1db55fe5533fdb5dULL, }; static const uint64_t EXPECTED_REWARD[NUM_CONFIGS] = { - 0x7a65310944a973aaULL, 0xa122490e8a5ea4faULL, 0xd8288292f3160f5aULL, - 0x1f71cc42bc243e13ULL, 0x65af16b1d7e922caULL, 0x49a0f296876aea23ULL, - 0xc4080cd4d9a6eb13ULL, 0x9d01d9da22e0bb93ULL, 0x6c34cdb27c0c1733ULL, - 0x29916e1218e670b3ULL, 0x6494cc221d5e7db3ULL, 0x11a8e2781441fd33ULL, - 0x5d411311ad9cd553ULL, 0xe7f1c65d07654513ULL, 0x47a1f9c7b1d3def3ULL, + 0xef229a1a4b13b62aULL, 0x380701212d84bfbaULL, 0x4098eaaa762c034aULL, + 0xfcafb9b6e476bbaaULL, 0x23577b00d055c7daULL, 0x2366aa6e9afa77d3ULL, + 0xabf96da3254522b3ULL, 0x64778a6835f7bae3ULL, 0x3b83ee4d02793243ULL, + 0x24ccbb9772bf51d3ULL, 0x4c54b1462ee3a663ULL, 0x29916e1218e670b3ULL, + 0x6c34cdb27c0c1733ULL, 0x47a1f9c7b1d3def3ULL, 0x47a1f9c7b1d3def3ULL, }; static const uint64_t EXPECTED_MASK[NUM_CONFIGS] = { - 0x9609313d75a829baULL, 0x34109efbca6fab0aULL, 0x9787e973169329baULL, - 0xe97fa334ceaf6153ULL, 0xd2dc555e39a58ba3ULL, 0x0ea03aa9e92854e3ULL, - 0xc2432dc597e3e483ULL, 0xbf39837ae26a6aaaULL, 0x7fc9129b3356ed93ULL, - 0xe54fb95b499c2003ULL, 0xd0bb60198af9b643ULL, 0xa01ad9cfc96241c3ULL, - 0x49744aaffe686a73ULL, 0x5b55145ce7296033ULL, 0xfd42b38660125883ULL, + 0xbbb48dc73a049bf3ULL, 0x39dc7eec5cb9e2f3ULL, 0x4c106d4052a4fdbaULL, + 0x143ebc7ebbb2d72aULL, 0x7dad20ecbb02bd93ULL, 0xef4d1b6877c93acaULL, + 0x8fca3065ddbf1793ULL, 0x2623748ea2e51b33ULL, 0x09130bda5462ebf3ULL, + 0x12a49268eb13bd53ULL, 0x408fca7dc9aff313ULL, 0x1e80e464ea552a5aULL, + 0x77f5a923af526e3aULL, 0x9f3034f7b394665aULL, 0x25d8b9153b3ae063ULL, }; static const uint64_t EXPECTED_TERMINAL[NUM_CONFIGS] = { - 0xaacea6292d1dbfd3ULL, 0x08f8c8682da946a1ULL, 0x8799762912bdf103ULL, - 0x622fee1940c541abULL, 0x82fbcdc1da23c683ULL, 0x2629f248900d0043ULL, - 0xc92dd3ae1e3522b2ULL, 0xbb44db79d20b1d81ULL, 0xa10f828fd67b1801ULL, - 0xe2d9ed2b31cf44a1ULL, 0x448b70e96c7c46a1ULL, 0xca591485885081a1ULL, - 0xd05fd043497d9b27ULL, 0xf92b462e21d67e67ULL, 0x12e57f8f9818c547ULL, -}; - -static const uint64_t LEGACY_OBSERVATION[NUM_CONFIGS] = { - 0x03f4682055435366ULL, 0x90b9c6fb55157aacULL, 0x0092dfb263b4768cULL, - 0x3964598751ca95a2ULL, 0x5a8ff64de7cc10feULL, 0xb8a7174941603f51ULL, - 0xb6b6e303a9e7857cULL, 0x792e4afd5831fe70ULL, 0xfd926d25fd8b4258ULL, - 0x7b738c46a083f9c5ULL, 0xcef689a6461bbfafULL, 0xd8e160b35c03dbc0ULL, - 0x2a2f884605e7a640ULL, 0x12fdce41ea3ac43fULL, 0xc8f251c4e9489e7dULL, + 0xb3480d7ee6336ae2ULL, 0x910cf79bbeeba6e3ULL, 0xe238e61f8c13c673ULL, + 0xda963a44d311c6daULL, 0xb310e5115ff5cb52ULL, 0x48994c2ef3769192ULL, + 0x2d82388999073b22ULL, 0x1b1338d1b46b5673ULL, 0x0e985be125b2f7f3ULL, + 0x258a6c0def52abe1ULL, 0x77852c52053de533ULL, 0xe2d9ed2b31cf44a1ULL, + 0x7c696af13bd0d307ULL, 0x12e57f8f9818c547ULL, 0x12e57f8f9818c547ULL, }; -static const uint64_t COMPACT_OBSERVATION[NUM_CONFIGS] = { +static const uint64_t PRE_UNIFIED_OBSERVATION[NUM_CONFIGS] = { 0xf2362a766a6212d1ULL, 0xa87ad065ca1666dcULL, 0xcac6b26329cadeafULL, 0xda3602f8cf48fd1fULL, 0x9898fdcd480dc4e6ULL, 0x7c305c0b8123853fULL, 0x9fb81d2ca3477306ULL, 0xb7e475f336034eadULL, 0x73f5ac59f227cb67ULL, @@ -247,6 +237,14 @@ static const uint64_t COMPACT_OBSERVATION[NUM_CONFIGS] = { 0xbf34279c73e947f4ULL, 0xdf399dc7aee3d3f3ULL, 0x989c8fc6b6aea06dULL, }; +static const uint64_t UNIFIED_OBSERVATION[NUM_CONFIGS] = { + 0x6c59b72c00728ad9ULL, 0x18a13290de7329bcULL, 0x6748854d95c23f92ULL, + 0x6825dad29da6b32dULL, 0x7c5520ac1cc5837fULL, 0xb88e7bda4fc6b77eULL, + 0x45cbf9bbfa703b70ULL, 0x89db5b7cc84b4728ULL, 0xb93f28f45f543b70ULL, + 0x3f1afd9c57acd296ULL, 0x3dd5d5e4e6289715ULL, 0x5c08d30cfb280174ULL, + 0x82bf60a965334a54ULL, 0x66a4cbc54247e358ULL, 0x338f8cdd2b9d56afULL, +}; + static int check_hash( const char* config, const char* component, @@ -268,7 +266,8 @@ int main(int argc, char** argv) { printf("inferno semantic golden (%d configs, <=%d ticks each)\n", NUM_CONFIGS, EPISODE_TICKS); - printf("lineage 59f90b7f4 obs3432 -> compact obs498\n\n"); + printf("player contract obs%d primary%d heads%d\n\n", + INF_NUM_OBS, INF_ACTION_DIMS[INF_HEAD_PRIMARY], INF_NUM_ACTION_HEADS); int failed = 0; for (int c = 0; c < NUM_CONFIGS; c++) { @@ -296,9 +295,9 @@ int main(int argc, char** argv) { CONFIGS[c].name, "terminal", hashes.terminal, EXPECTED_TERMINAL[c]); config_failed += check_hash( CONFIGS[c].name, "observation", hashes.observation, - COMPACT_OBSERVATION[c]); - if (LEGACY_OBSERVATION[c] == COMPACT_OBSERVATION[c]) { - printf(" %-12s observation lineage did not change\n", CONFIGS[c].name); + UNIFIED_OBSERVATION[c]); + if (PRE_UNIFIED_OBSERVATION[c] == UNIFIED_OBSERVATION[c]) { + printf(" %-12s observation contract did not change\n", CONFIGS[c].name); config_failed++; } if (!config_failed) printf(" %-12s PASS\n", CONFIGS[c].name); diff --git a/ocean/osrs/tests/test_osrs_inventory_clicks.c b/ocean/osrs/tests/test_osrs_inventory_clicks.c index 18fca6fdad..4f0876a13f 100644 --- a/ocean/osrs/tests/test_osrs_inventory_clicks.c +++ b/ocean/osrs/tests/test_osrs_inventory_clicks.c @@ -5,7 +5,9 @@ #include -#include "ocean/osrs/osrs_inventory_clicks.h" +#include "ocean/osrs/osrs_policy.h" + + #define CHECK(label, cond) do { \ if (!(cond)) { \ @@ -308,7 +310,122 @@ static int cell_in_unit_range(const float* out) { static int test_enriched_feature_counts(void) { CHECK("cell obs features is 28", OSRS_INVENTORY_CELL_OBS_FEATURES == 28); - CHECK("equipped obs features is 18", OSRS_EQUIPPED_SELF_OBS_FEATURES == 18); + return 0; +} + +static int test_shared_policy_contract(void) { + CHECK("shared self observation width is 52", + OSRS_SHARED_SELF_OBS_SIZE == 52); + CHECK("shared inventory cells carry one canonical content code", + OSRS_SHARED_INVENTORY_CELL_OBS_FEATURES == 1); + CHECK("shared inventory observation covers 28 cells", + OSRS_SHARED_INVENTORY_OBS_SIZE == 28); + CHECK("shared equipment observation covers eleven worn slots", + OSRS_SHARED_EQUIPPED_OBS_SIZE == NUM_GEAR_SLOTS); + CHECK("shared equipment effects retain their aggregate", + OSRS_SHARED_EFFECT_OBS_SIZE == 10); + CHECK("shared observation prefix is 101", + OSRS_SHARED_OBS_SIZE == 101); + CHECK("shared base action contract has 18 heads", + OSRS_BASE_NUM_ACTION_HEADS == 18); + CHECK("shared action heads have stable semantic order", + OSRS_HEAD_PRIMARY == 0 && + OSRS_HEAD_OVERHEAD == 1 && + OSRS_HEAD_EQUIP_BASE == 2 && + OSRS_HEAD_EAT == 13 && + OSRS_HEAD_DRINK == 14 && + OSRS_HEAD_SPELL == 15 && + OSRS_HEAD_SPECIAL == 16 && + OSRS_HEAD_OFFENSIVE == 17); + return 0; +} + +static int test_shared_action_layout(void) { + const int target_slots = 14; + CHECK("primary combines movement and targets", + OSRS_PRIMARY_DIM(target_slots) == 39); + CHECK("overhead follows primary", + osrs_base_action_head_mask_offset(target_slots, OSRS_HEAD_OVERHEAD) == 39); + CHECK("equipment heads follow overhead", + osrs_base_action_head_mask_offset(target_slots, OSRS_HEAD_EQUIP_BASE) == 46); + CHECK("eat follows eleven equipment heads", + osrs_base_action_head_mask_offset(target_slots, OSRS_HEAD_EAT) == + 46 + NUM_GEAR_SLOTS * OSRS_INVENTORY_CLICK_DIM); + CHECK("spell follows eat and drink", + osrs_base_action_head_mask_offset(target_slots, OSRS_HEAD_SPELL) == + 46 + (NUM_GEAR_SLOTS + 2) * OSRS_INVENTORY_CLICK_DIM); + CHECK("base mask size matches final head boundary", + OSRS_BASE_ACTION_MASK_SIZE(target_slots) == + osrs_base_action_head_mask_offset(target_slots, OSRS_HEAD_OFFENSIVE) + + OSRS_OFFENSIVE_DIM); + return 0; +} + +static int test_shared_observation_layout(void) { + Player player = {0}; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + player.equipped[slot] = ITEM_NONE; + player.base_hitpoints = 99; + player.current_hitpoints = 66; + player.base_prayer = 77; + player.current_prayer = 55; + player.x = 12; + player.y = 23; + player.equipped[GEAR_SLOT_WEAPON] = ITEM_WHIP; + player.inventory_cells[7] = + osrs_inventory_cell_from_item(ITEM_TWISTED_BOW); + player.equipment_effect_profile.effect_mask = + OSRS_ITEM_EFFECT_TWISTED_BOW; + player.equipment_effect_profile.shield_item = ITEM_NONE; + + CHECK("real equip transition succeeds", + osrs_equip_from_cell(&player, player.inventory_cells, 7) == + GEAR_SLOT_WEAPON); + CHECK("equipped item leaves the inventory", + osrs_inventory_cell_item_index(&player.inventory_cells[7]) == ITEM_WHIP); + CHECK("clicked item occupies the worn weapon slot", + player.equipped[GEAR_SLOT_WEAPON] == ITEM_TWISTED_BOW); + + float obs[OSRS_SHARED_OBS_SIZE] = {0}; + OsrsSharedObservationInput input = { + .player = &player, + .interaction = &player.interaction, + .arena_min_x = 10, + .arena_max_x = 20, + .arena_min_y = 20, + .arena_max_y = 30, + .attack_style = ATTACK_STYLE_RANGED, + }; + CHECK("shared writer returns exact width", + osrs_write_shared_observations(obs, &input) == OSRS_SHARED_OBS_SIZE); + CHECK("shared self prefix begins with hitpoints", + fabsf(obs[0] - 66.0f / 99.0f) < 1e-6f); + int cell_offset = OSRS_SHARED_OBS_INVENTORY_START + 7; + CHECK("inventory observation contains the displaced item", + obs[cell_offset] == osrs_inventory_cell_obs_code_encode( + player.inventory_cells[7].content_code)); + int weapon_offset = + OSRS_SHARED_OBS_EQUIPPED_START + GEAR_SLOT_WEAPON; + CHECK("equipment observation contains the worn item", + obs[weapon_offset] == osrs_inventory_cell_obs_code_encode( + osrs_inventory_content_code_from_item(ITEM_TWISTED_BOW))); + CHECK("shared equipment effects follow worn item codes", + obs[OSRS_SHARED_OBS_EFFECT_START + 1] == 1.0f); + return 0; +} + +static int test_player_owns_canonical_inventory(void) { + Player player = {0}; + OsrsInventoryCell* cells = osrs_player_inventory_cells(&player); + cells[7] = osrs_inventory_cell_from_item(ITEM_TWISTED_BOW); + CHECK("player owns exactly 28 canonical inventory cells", + sizeof(player.inventory_cells) / sizeof(player.inventory_cells[0]) == + OSRS_INVENTORY_SIZE); + CHECK("canonical inventory accessor returns player storage", + &player.inventory_cells[7] == &cells[7]); + CHECK("canonical inventory stores content identity", + osrs_inventory_cell_item_index(&player.inventory_cells[7]) == + ITEM_TWISTED_BOW); return 0; } @@ -367,11 +484,6 @@ static int test_effect_class4_decoder(void) { CHECK("lightbearer is util only", eff4[0] == 0.0f && eff4[1] == 0.0f && eff4[2] == 0.0f && eff4[3] == 1.0f); - float eq[OSRS_EQUIPPED_SELF_OBS_FEATURES]; - osrs_write_equipped_self_features(eq, ITEM_AMULET_OF_BLOOD_FURY); - CHECK("equipped blood fury lifesteal", eq[12] == 1.0f); - CHECK("equipped blood fury no other class", - eq[13] == 0.0f && eq[14] == 0.0f && eq[15] == 0.0f); return 0; } @@ -486,6 +598,10 @@ int main(void) { if (test_shared_drink_consume_owns_timer_and_one_dose()) return 1; if (test_cell_rearrange_swaps_two_slots()) return 1; if (test_enriched_feature_counts()) return 1; + if (test_shared_policy_contract()) return 1; + if (test_player_owns_canonical_inventory()) return 1; + if (test_shared_action_layout()) return 1; + if (test_shared_observation_layout()) return 1; if (test_brew_cell_semantics()) return 1; if (test_weapon_cell_semantics()) return 1; if (test_effect_class4_decoder()) return 1; diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c index c2adfd9bae..685ced18f6 100644 --- a/ocean/osrs/tests/test_osrs_player_step.c +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -48,6 +48,12 @@ typedef struct { typedef struct { int calls; } StepWalkableCountCtx; +typedef struct { + int calls; + int open_x; + int open_y; +} StepRouteBlockedCountCtx; + static int step_tile_walkable(void* ctx, int x, int y) { @@ -70,6 +76,13 @@ static int step_vertical_wall_route(void* ctx, int x, int y, int size) { (void)size; return step_vertical_wall(ctx, x, y); } +static int step_counted_isolated_route(void* ctx, int x, int y, int size) { + (void)size; + StepRouteBlockedCountCtx* count = (StepRouteBlockedCountCtx*)ctx; + count->calls++; + return x != count->open_x || y != count->open_y; +} + static uint32_t step_open_flags(void* ctx, int x, int y) { (void)ctx; @@ -523,8 +536,79 @@ static void test_attack_route_persists_and_invalidates_canonically(void) { route_cache.planned_source_x == 1 && route_cache.planned_source_y == 10); } +static void test_failed_attack_route_persists_and_invalidates(void) { + Player player = { + .x = 2, + .y = 2, + }; + OsrsInteraction interaction; + OsrsActorRouteCache route_cache = {0}; + osrs_interaction_init(&interaction); + osrs_interaction_set(&interaction, 3); + + StepTargetCtx target_ctx = { + .target = { + .slot = 3, + .x = 20, + .y = 20, + .size = 1, + .attack_range = 1, + }, + .target_valid = 1, + }; + StepRouteBlockedCountCtx blocker_ctx = { + .open_x = player.x, + .open_y = player.y, + }; + int dest_x = -1; + int dest_y = -1; + OsrsEncounterArena arena = step_arena(); + arena.blockers.is_blocked = step_counted_isolated_route; + arena.blockers.ctx = &blocker_ctx; + arena.blockers.revision = 7; + + OsrsPlayerStepInput input = { + .player = &player, + .interaction = &interaction, + .route_cache = &route_cache, + .target_lookup = step_lookup_target, + .target_ctx = &target_ctx, + .dest_x = &dest_x, + .dest_y = &dest_y, + .arena = arena, + }; + + osrs_encounter_player_step(&input); + int calls_after_first_route = blocker_ctx.calls; + CHECK("isolated player produces a failed attack route", + route_cache.state == OSRS_INTERACTION_ROUTE_FAILED && + calls_after_first_route > 0); + + osrs_encounter_player_step(&input); + CHECK("unchanged failed attack route is cached", + blocker_ctx.calls == calls_after_first_route); + + input.arena.blockers.revision++; + osrs_encounter_player_step(&input); + CHECK("blocker revision invalidates a failed attack route", + blocker_ctx.calls > calls_after_first_route); + int calls_after_blocker_change = blocker_ctx.calls; + + target_ctx.target.x--; + osrs_encounter_player_step(&input); + CHECK("target geometry invalidates a failed attack route", + blocker_ctx.calls > calls_after_blocker_change); + int calls_after_target_change = blocker_ctx.calls; + + player.x++; + blocker_ctx.open_x = player.x; + osrs_encounter_player_step(&input); + CHECK("player position invalidates a failed attack route", + blocker_ctx.calls > calls_after_target_change); +} + -static void test_single_checkpoint_route_reroutes_each_tick(void) { +static void test_single_checkpoint_route_persists(void) { Player player; memset(&player, 0, sizeof(player)); player.x = 2; @@ -558,9 +642,10 @@ static void test_single_checkpoint_route_reroutes_each_tick(void) { route_cache.waypoint_count == 1 && route_cache.planned_source_x == 2); osrs_encounter_player_step(&input); - CHECK("one-checkpoint entity route reroutes from the current tile", - route_cache.planned_source_x == 4 && - route_cache.planned_source_y == 2); + CHECK("one-checkpoint entity route persists while traversable", + route_cache.planned_source_x == 2 && + route_cache.planned_source_y == 2 && + player.x == 6 && player.y == 2); } static void test_target_selection_does_not_own_route_cache(void) { @@ -1009,6 +1094,7 @@ typedef struct { typedef struct { uint8_t blocked[8][8]; + int calls; } RouteTestBlockers; static uint32_t route_test_flags(void* data, int x, int y) { @@ -1020,6 +1106,7 @@ static uint32_t route_test_flags(void* data, int x, int y) { static int route_test_blocked(void* data, int x, int y, int size) { RouteTestBlockers* blockers = (RouteTestBlockers*)data; + blockers->calls++; for (int dx = 0; dx < size; dx++) { for (int dy = 0; dy < size; dy++) { int tile_x = x + dx; @@ -1200,6 +1287,120 @@ static void test_destination_south_first_tie_order(void) { steps == 2 && player.x == 4 && player.y == 2); free(topology); } +static void test_reverse_route_field_reuses_matching_target(void) { + RouteTestGeometry geometry = {0}; + RouteTestBlockers blockers = {0}; + blockers.blocked[3][3] = 1; + EncounterArenaTopology* topology = route_test_topology(&geometry); + EncounterRouteInput input = route_test_input(topology, &blockers); + input.source_x = 1; + input.source_y = 1; + input.target_x = 6; + input.target_y = 6; + input.cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_REVERSE; + + EncounterRouteResult first = encounter_route_solve(&input); + int first_calls = blockers.calls; + blockers.calls = 0; + EncounterRouteResult second = encounter_route_solve(&input); + int second_calls = blockers.calls; + + CHECK("matching reverse route field preserves its route", + first.outcome == second.outcome && + first.destination_x == second.destination_x && + first.destination_y == second.destination_y && + first.distance == second.distance && + first.first_dx == second.first_dx && + first.first_dy == second.first_dy && + first.run_dx == second.run_dx && + first.run_dy == second.run_dy); + CHECK("matching reverse route field reuses prior expansion", + second_calls < first_calls); + input.result_detail = ENCOUNTER_ROUTE_RESULT_NEXT_STEPS; + EncounterRouteResult next_steps = encounter_route_solve(&input); + CHECK("next-step reverse result preserves movement", + next_steps.outcome == first.outcome && + next_steps.destination_x == first.destination_x && + next_steps.destination_y == first.destination_y && + next_steps.distance == first.distance && + next_steps.first_dx == first.first_dx && + next_steps.first_dy == first.first_dy && + next_steps.run_dx == first.run_dx && + next_steps.run_dy == first.run_dy && + next_steps.waypoint_count == 0); + input.result_detail = ENCOUNTER_ROUTE_RESULT_FULL; + blockers.blocked + [input.source_x + first.first_dx] + [input.source_y + first.first_dy] = 1; + blockers.calls = 0; + input.blockers.revision++; + EncounterRouteResult after_revision = + encounter_route_solve(&input); + CHECK("changed blocker revision invalidates reverse route field", + blockers.calls > 0 && + (after_revision.first_dx != first.first_dx || + after_revision.first_dy != first.first_dy)); + free(topology); +} +static void test_blocked_destination_stops_at_nearest_fallback(void) { + RouteTestGeometry geometry = {0}; + RouteTestBlockers blockers = {0}; + blockers.blocked[3][3] = 1; + EncounterArenaTopology* topology = route_test_topology(&geometry); + EncounterRouteInput input = route_test_input(topology, &blockers); + input.source_x = 1; + input.source_y = 1; + input.target_x = 3; + input.target_y = 3; + input.cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS; + + EncounterRouteResult route = encounter_route_solve(&input); + CHECK("blocked destination selects the canonical nearest fallback", + route.outcome == ROUTE_REACHED_FALLBACK && + route.destination_x == 2 && route.destination_y == 3 && + route.distance == 2 && + route.first_dx == 0 && route.first_dy == 1 && + route.run_dx == 1 && route.run_dy == 1); + CHECK("blocked destination stops after its nearest fallback depth", + blockers.calls < 32); + input.source_x = 2; + input.source_y = 3; + blockers.blocked[2][3] = 1; + input.blockers.revision++; + route = encounter_route_solve(&input); + CHECK("blocked source adjacent to blocked destination is unreachable", + route.outcome == ROUTE_UNREACHABLE); + free(topology); +} + +static void test_source_field_reuses_traversal_across_targets(void) { + RouteTestGeometry geometry = {0}; + RouteTestBlockers blockers = {0}; + blockers.blocked[5][6] = 1; + blockers.blocked[7][6] = 1; + blockers.blocked[6][5] = 1; + blockers.blocked[6][7] = 1; + EncounterArenaTopology* topology = route_test_topology(&geometry); + EncounterRouteInput input = route_test_input(topology, &blockers); + input.target_x = 6; + input.target_y = 6; + input.target_kind = ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE; + input.attack_range = 1; + + EncounterRouteResult unreachable = encounter_route_solve(&input); + int calls_after_exhaustion = blockers.calls; + CHECK("enclosed target exhausts source traversal", + unreachable.outcome == ROUTE_REACHED_FALLBACK && + calls_after_exhaustion > 4); + + input.target_y = 1; + EncounterRouteResult reached = encounter_route_solve(&input); + CHECK("new target reuses exhausted source traversal", + reached.outcome == ROUTE_REACHED_TARGET && + reached.destination_x == 5 && reached.destination_y == 1 && + blockers.calls == calls_after_exhaustion + 4); + free(topology); +} static void test_direct_route_policy_preserves_greedy_step_order(void) { RouteTestGeometry geometry = {0}; @@ -1222,6 +1423,44 @@ static void test_direct_route_policy_preserves_greedy_step_order(void) { free(topology); } +static void route_generation_wrap_operation(void) { + RouteTestGeometry geometry; + memset(&geometry, 0, sizeof(geometry)); + for (int x = 0; x < 8; x++) + for (int y = 0; y < 8; y++) + geometry.flags[x][y] = COLLISION_BLOCKED; + for (int x = 1; x <= 4; x++) geometry.flags[x][1] = 0; + + RouteTestBlockers blockers = {0}; + EncounterArenaTopology* topology = route_test_topology(&geometry); + EncounterRouteInput input = route_test_input(topology, &blockers); + int stale_index = 2 * topology->height + 1; + encounter_route_scratch.current_generation = UINT16_MAX; + encounter_route_scratch.generation[stale_index] = 2; + encounter_route_scratch.depth[stale_index] = 0; + encounter_route_scratch.via[stale_index] = VIA_START; + + input.cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_REVERSE; + EncounterRouteResult reverse = {0}; + if (!encounter_route_try_reverse(&input, &reverse) || + reverse.outcome != ROUTE_REACHED_TARGET) + abort(); + + input.cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS; + EncounterRouteResult bfs = encounter_route_solve(&input); + if (bfs.outcome != ROUTE_REACHED_TARGET || + bfs.distance != 3 || + bfs.first_dx != 1 || + bfs.first_dy != 0) + abort(); + free(topology); +} + +static void test_route_generation_wrap_clears_stale_bfs_roots(void) { + CHECK("route generation wrap clears stale BFS roots", + !topology_test_aborts(route_generation_wrap_operation)); +} + int main(void) { @@ -1229,7 +1468,11 @@ int main(void) { test_route_cost_order_is_deterministic(); test_attack_range_overlap_uses_deterministic_escape(); test_destination_south_first_tie_order(); + test_reverse_route_field_reuses_matching_target(); + test_blocked_destination_stops_at_nearest_fallback(); + test_source_field_reuses_traversal_across_targets(); test_direct_route_policy_preserves_greedy_step_order(); + test_route_generation_wrap_clears_stale_bfs_roots(); test_arena_topology_rejects_invalid_bounds(); test_arena_topology_extreme_origins_and_reader_bounds(); test_arena_topology_bounds_collision_los_and_revision(); @@ -1248,7 +1491,8 @@ int main(void) { test_attack_route_uses_rsmod_cardinal_first_order(); test_melee_attack_does_not_cross_cardinal_wall(); test_attack_route_persists_and_invalidates_canonically(); - test_single_checkpoint_route_reroutes_each_tick(); + test_failed_attack_route_persists_and_invalidates(); + test_single_checkpoint_route_persists(); printf("\n%d/%d tests passed\n", tests_run - tests_failed, tests_run); return tests_failed == 0 ? 0 : 1; diff --git a/ocean/osrs/tests/test_osrs_pvp_pending_hits.c b/ocean/osrs/tests/test_osrs_pvp_pending_hits.c index a5f4302612..b6fde1a721 100644 --- a/ocean/osrs/tests/test_osrs_pvp_pending_hits.c +++ b/ocean/osrs/tests/test_osrs_pvp_pending_hits.c @@ -158,6 +158,51 @@ static void test_nh_topology_geometry_parity(void) { diagonal_wall_steps > 0); } +static void test_nh_local_move_routes_match_canonical_solver(void) { + printf("--- NH PvP local move route cache parity ---\n"); + const EncounterArenaTopology* topology = nh_context.route_topology; + int matches = 1; + for (int x = topology->origin_x; + x < topology->origin_x + topology->width && matches; + x++) { + for (int y = topology->origin_y; + y < topology->origin_y + topology->height && matches; + y++) { + for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + EncounterRouteInput input = { + .topology = topology, + .source_x = x, + .source_y = y, + .actor_size = 1, + .target_x = x + ENCOUNTER_MOVE_TARGET_DX[action], + .target_y = y + ENCOUNTER_MOVE_TARGET_DY[action], + .target_size = 1, + .target_kind = ENCOUNTER_ROUTE_TARGET_TILE, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + }; + EncounterRouteResult expected = encounter_route_solve(&input); + EncounterRouteResult actual; + if (!pvp_local_move_route_lookup( + &pvp_route_topology_owner, &input, &actual) || + actual.outcome != expected.outcome || + actual.destination_x != expected.destination_x || + actual.destination_y != expected.destination_y || + actual.first_dx != expected.first_dx || + actual.first_dy != expected.first_dy || + actual.run_dx != expected.run_dx || + actual.run_dy != expected.run_dy || + actual.distance != expected.distance) { + printf(" mismatch source=(%d,%d) action=%d\n", x, y, action); + matches = 0; + break; + } + } + } + } + CHECK("cached local routes equal canonical south-first BFS", matches); +} + static void test_nh_dynamic_player_occupancy(void) { printf("--- NH PvP player occupancy stays dynamic ---\n"); const EncounterArenaTopology* topology = nh_context.route_topology; @@ -177,8 +222,8 @@ static void test_nh_dynamic_player_occupancy(void) { } static void run_nh_chase_case( - int loadout, - int combat, + PvpEquipmentPlan equipment_plan, + OsrsSpellAction spell, int expected_x, int expected_y, const char* x_label, @@ -191,6 +236,7 @@ static void run_nh_chase_case( nh_state->env.auto_reset = 0; Player* player = &nh_state->env.players[0]; Player* target = &nh_state->env.players[1]; + pvp_apply_equipment_plan(player, equipment_plan); player->x = FIGHT_AREA_BASE_X; player->y = FIGHT_AREA_BASE_Y; player->dest_x = player->x; @@ -199,9 +245,9 @@ static void run_nh_chase_case( target->y = FIGHT_AREA_BASE_Y + 4; target->dest_x = target->x; target->dest_y = target->y; - int actions[NUM_ACTION_HEADS] = {0}; - actions[HEAD_LOADOUT] = loadout; - actions[HEAD_COMBAT] = combat; + int actions[OSRS_BASE_NUM_ACTION_HEADS] = {0}; + actions[OSRS_HEAD_PRIMARY] = OSRS_PRIMARY_MOVE_ACTIONS; + actions[OSRS_HEAD_SPELL] = spell; ENCOUNTER_NH_PVP.step( (EncounterState*)nh_state, (EncounterContext*)&nh_context, @@ -217,17 +263,17 @@ static void run_nh_chase_case( static void test_nh_attack_chase_destinations(void) { printf("--- NH PvP attack chase destinations ---\n"); run_nh_chase_case( - LOADOUT_MELEE, ATTACK_ATK, + PVP_EQUIPMENT_MELEE, OSRS_SPELL_NONE, FIGHT_AREA_BASE_X + 2, FIGHT_AREA_BASE_Y + 2, "melee chase keeps x destination", "melee chase keeps y destination"); run_nh_chase_case( - LOADOUT_RANGE, ATTACK_ATK, + PVP_EQUIPMENT_RANGED, OSRS_SPELL_NONE, FIGHT_AREA_BASE_X + 2, FIGHT_AREA_BASE_Y, "ranged chase keeps x destination", "ranged chase keeps y destination"); run_nh_chase_case( - LOADOUT_MAGE, ATTACK_ICE, + PVP_EQUIPMENT_MAGIC, OSRS_SPELL_ICE_BARRAGE, FIGHT_AREA_BASE_X, FIGHT_AREA_BASE_Y, "in-range magic attack does not chase on x", "in-range magic attack does not chase on y"); @@ -292,12 +338,225 @@ static void test_nh_out_of_arena_destination_fallback(void) { route.distance, 0); } +static void test_nh_shared_contract_and_inventory_actions(void) { + printf("--- NH PvP shared contract and canonical inventory ---\n"); + CHECK("NH PvP exposes 18 shared heads", + ENCOUNTER_NH_PVP.num_action_heads == OSRS_BASE_NUM_ACTION_HEADS); + CHECK("NH PvP exposes shared action dimensions", + ENCOUNTER_NH_PVP.action_head_dims[OSRS_HEAD_PRIMARY] == + OSRS_PRIMARY_DIM(1) && + ENCOUNTER_NH_PVP.action_head_dims[OSRS_HEAD_DRINK] == + OSRS_INVENTORY_CLICK_DIM); + CHECK("NH PvP observation starts with the exact shared boundary", + NH_PVP_NUM_OBS == OSRS_SHARED_OBS_SIZE + NH_PVP_SPECIFIC_OBS_SIZE); + + ENCOUNTER_NH_PVP.reset( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + 1); + Player* player = &nh_state->env.players[0]; + int nonempty = 0; + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) + nonempty += !osrs_inventory_cell_is_empty( + &player->inventory_cells[cell]); + CHECK("PvP reset seeds canonical inventory cells", nonempty > 0); + ASSERT_INT_EQ("PvP reset reserves one inventory cell for two-handed swaps", + nonempty, OSRS_INVENTORY_SIZE - 1); + + float observations[NH_PVP_NUM_OBS]; + ENCOUNTER_NH_PVP.write_obs( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + observations); + CHECK("shared prefix starts with player hitpoints", + observations[0] == + (float)player->current_hitpoints / player->base_hitpoints); + CHECK("shared prefix exposes canonical inventory code", + observations[OSRS_SHARED_OBS_INVENTORY_START] == + osrs_inventory_cell_obs_code_encode( + player->inventory_cells[0].content_code)); + CHECK("shared prefix exposes canonical worn weapon code", + observations[ + OSRS_SHARED_OBS_EQUIPPED_START + GEAR_SLOT_WEAPON] == + osrs_inventory_cell_obs_code_encode( + osrs_inventory_content_code_from_item( + player->equipped[GEAR_SLOT_WEAPON]))); + + int ranged_cell = + pvp_inventory_cell_with_item(player, ITEM_RUNE_CROSSBOW); + CHECK("reset exposes a ranged weapon cell", ranged_cell >= 0); + uint8_t previous_weapon = player->equipped[GEAR_SLOT_WEAPON]; + int equip[OSRS_BASE_NUM_ACTION_HEADS] = {0}; + equip[OSRS_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = ranged_cell + 1; + ENCOUNTER_NH_PVP.step( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + equip); + CHECK("weapon cell click equips the selected item", + player->equipped[GEAR_SLOT_WEAPON] == ITEM_RUNE_CROSSBOW); + CHECK("weapon cell receives displaced equipment", + osrs_inventory_cell_item_index( + &player->inventory_cells[ranged_cell]) == previous_weapon); + + int food_cell = nh_pvp_find_consumable_cell( + player, OSRS_CONSUMABLE_SHARK_FOOD); + CHECK("reset exposes a food cell", food_cell >= 0); + player->current_hitpoints = 50; + int eat[OSRS_BASE_NUM_ACTION_HEADS] = {0}; + eat[OSRS_HEAD_EAT] = food_cell + 1; + ENCOUNTER_NH_PVP.step( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + eat); + CHECK("food cell click heals the player", + player->current_hitpoints > 50); + CHECK("food cell click consumes the canonical cell", + osrs_inventory_cell_is_empty(&player->inventory_cells[food_cell])); +} + +static void test_nh_masked_drink_is_not_executed(void) { + printf("--- NH PvP masked drinks do not execute ---\n"); + ENCOUNTER_NH_PVP.reset( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + 2); + Player* player = &nh_state->env.players[0]; + int brew_cell = nh_pvp_find_consumable_cell( + player, OSRS_CONSUMABLE_BREW); + CHECK("reset exposes a brew cell", brew_cell >= 0); + player->current_hitpoints = + player->base_hitpoints + osrs_brew_heal_amount(player->base_hitpoints); + player->current_defence = player->base_defence; + CHECK("full boosted player cannot use another brew", + !pvp_drink_kind_available(player, OSRS_CONSUMABLE_BREW)); + + int brew_doses_before = player->brew_doses; + int actions[OSRS_BASE_NUM_ACTION_HEADS] = {0}; + actions[OSRS_HEAD_DRINK] = brew_cell + 1; + execute_switches( + &nh_state->env, 0, actions, nh_context.route_topology); + + ASSERT_INT_EQ("masked brew keeps its dose count", + player->brew_doses, brew_doses_before); + ASSERT_INT_EQ("masked brew does not start potion cooldown", + player->potion_timer, 0); +} + +static void test_nh_reset_keeps_two_handed_switch_usable(void) { + printf("--- NH PvP reset keeps two-handed switches usable ---\n"); + ENCOUNTER_NH_PVP.reset( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + 3); + Player* player = &nh_state->env.players[0]; + int spec_cell = pvp_inventory_cell_with_item( + player, ITEM_DRAGON_DAGGER); + CHECK("reset exposes a spec weapon cell", spec_cell >= 0); + player->inventory_cells[spec_cell] = + osrs_inventory_cell_from_item(ITEM_AGS); + + CHECK("two-handed switch is legal from the reset inventory", + osrs_can_equip_from_cell( + player, player->inventory_cells, spec_cell)); +} + +static void test_nh_ko_supply_bonus_is_normalized_to_initial_pool(void) { + printf("--- NH PvP KO supply bonus uses the initial pool ---\n"); + ENCOUNTER_NH_PVP.reset( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + 4); + nh_state->env.shaping = (RewardShapingConfig){ + .enabled = 1, + .ko_supplies_bonus_coef = 1.0f, + }; + nh_state->env.episode_over = 1; + nh_state->env.winner = 0; + + ASSERT_FLOAT_NEAR("untouched opponent grants one normalized supply bonus", + calculate_reward(&nh_state->env, 0), 2.0f, 0.0f); +} + +static void test_nh_action_mask_overwrites_dirty_buffer(void) { + ENCOUNTER_NH_PVP.reset( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + 1); + float clean[NH_PVP_ACTION_MASK_SIZE] = {0}; + float dirty[NH_PVP_ACTION_MASK_SIZE]; + for (int i = 0; i < NH_PVP_ACTION_MASK_SIZE; i++) dirty[i] = 7.0f; + ENCOUNTER_NH_PVP.write_mask( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + clean); + ENCOUNTER_NH_PVP.write_mask( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + dirty); + CHECK("NH PvP mask overwrites every output", + memcmp(clean, dirty, sizeof(clean)) == 0); + + unsigned char expected[NH_PVP_ACTION_MASK_SIZE]; + unsigned char actual[NH_PVP_ACTION_MASK_SIZE]; + for (int i = 0; i < NH_PVP_ACTION_MASK_SIZE; i++) + expected[i] = clean[i] != 0.0f; + pvp_write_action_mask_bytes( + actual, &nh_state->env, 0, nh_context.route_topology); + CHECK("NH PvP byte mask matches the float contract", + memcmp(expected, actual, sizeof(expected)) == 0); +} + +static void test_nh_scripted_policy_action_sequences(void) { + printf("--- NH PvP scripted policies emit canonical action sequences ---\n"); + for (int type = OPP_TRUE_RANDOM; type < OPP_SELFPLAY; type++) { + nh_state->env.pvp_runtime.opponent.type = (OpponentType)type; + nh_state->env.pvp_runtime.use_c_opponent = 1; + nh_state->env.pvp_runtime.use_external_opponent_actions = 0; + ENCOUNTER_NH_PVP.reset( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + (uint32_t)(1000 + type)); + nh_state->env.auto_reset = 0; + + int bounded = 1; + int actions[OSRS_BASE_NUM_ACTION_HEADS] = {0}; + for (int tick = 0; tick < 64 && !nh_state->env.episode_over; tick++) { + ENCOUNTER_NH_PVP.step( + (EncounterState*)nh_state, + (EncounterContext*)&nh_context, + actions); + const int* opponent_actions = + nh_state->env.last_executed_actions + + OSRS_BASE_NUM_ACTION_HEADS; + for (int head = 0; head < OSRS_BASE_NUM_ACTION_HEADS; head++) { + if (opponent_actions[head] < 0 || + opponent_actions[head] >= NH_PVP_ACTION_DIMS[head]) { + bounded = 0; + } + } + } + + char label[80]; + snprintf(label, sizeof(label), + "scripted opponent %d stays within canonical action dimensions", + type); + CHECK(label, bounded); + } +} + int main(void) { init_nh_fixture(); test_nh_topology_geometry_parity(); + test_nh_local_move_routes_match_canonical_solver(); test_nh_dynamic_player_occupancy(); test_nh_attack_chase_destinations(); test_nh_out_of_arena_destination_fallback(); + test_nh_shared_contract_and_inventory_actions(); + test_nh_masked_drink_is_not_executed(); + test_nh_reset_keeps_two_handed_switch_usable(); + test_nh_ko_supply_bonus_is_normalized_to_initial_pool(); + test_nh_scripted_policy_action_sequences(); + test_nh_action_mask_overwrites_dirty_buffer(); test_pvp_queue_accepts_capacity(); test_pvp_queue_overflow_aborts(); diff --git a/ocean/osrs/tests/test_zulrah_golden.c b/ocean/osrs/tests/test_zulrah_golden.c index b05bc2ac5e..a9c089afa9 100644 --- a/ocean/osrs/tests/test_zulrah_golden.c +++ b/ocean/osrs/tests/test_zulrah_golden.c @@ -39,6 +39,7 @@ typedef struct { int ticks; int kills; int winner; + uint64_t simulation_hash; } EpisodeStats; static void apply_env_config( @@ -76,10 +77,15 @@ static uint64_t run_episode(const GoldenConfig* cfg, int max_ticks, EpisodeStats ENCOUNTER_ZULRAH.reset(state, context, cfg->env_seed); uint64_t h = FNV_OFFSET; + uint64_t simulation_hash = FNV_OFFSET; for (int t = 0; t < max_ticks; t++) { zul_heuristic_actions(&s, actions); ENCOUNTER_ZULRAH.step(state, context, actions); + simulation_hash = + fnv_bytes(simulation_hash, actions, sizeof(actions)); + simulation_hash = + fnv_bytes(simulation_hash, &s, sizeof(s)); ENCOUNTER_ZULRAH.write_obs(state, context, obs); ENCOUNTER_ZULRAH.write_mask(state, context, mask); @@ -97,6 +103,7 @@ static uint64_t run_episode(const GoldenConfig* cfg, int max_ticks, EpisodeStats stats->ticks = s.tick; stats->kills = s.kills_this_episode; stats->winner = s.winner; + stats->simulation_hash = simulation_hash; return h; } @@ -146,12 +153,21 @@ static_assert(EPISODE_TICKS > ZUL_MAX_TICKS, "episode budget must cover the in-sim tick cap"); static const uint64_t BASELINE[NUM_CONFIGS] = { - 0xecf8fefec11da90fULL, /* t0_single */ - 0xfaa0144d43a61972ULL, /* t1_single */ - 0x801a8acf962b8ff2ULL, /* t2_single */ - 0xdd701c2aac0eaa2fULL, /* t0_trip */ - 0xa9033115654c7a8aULL, /* t1_trip */ - 0x22e191b4b0fd500eULL, /* t2_trip */ + 0x9ef8a0cef416f781ULL, /* t0_single */ + 0xf8b5969beba40413ULL, /* t1_single */ + 0x069ea11e537ca6ffULL, /* t2_single */ + 0x97de7b21c6970b1aULL, /* t0_trip */ + 0x2c039ba671b594bdULL, /* t1_trip */ + 0x41329c81c35a1694ULL, /* t2_trip */ +}; + +static const uint64_t SIMULATION_BASELINE[NUM_CONFIGS] = { + 0xd1297d5ee0382522ULL, /* t0_single */ + 0x8e586a704d1ccd01ULL, /* t1_single */ + 0x8d7b82dd02af49d1ULL, /* t2_single */ + 0x0a5d8f989ce34467ULL, /* t0_trip */ + 0x3a737efafde6efc6ULL, /* t1_trip */ + 0xfbb31e45ea44b25dULL, /* t2_trip */ }; int main(int argc, char** argv) { @@ -169,25 +185,35 @@ int main(int argc, char** argv) { printf(" 0x%016llxULL, /* %s */\n", (unsigned long long)h, CONFIGS[c].name); } else { - int ok = h == BASELINE[c]; + int observation_ok = h == BASELINE[c]; + int simulation_ok = + stats[c].simulation_hash == SIMULATION_BASELINE[c]; + int ok = observation_ok && simulation_ok; printf(" %-12s 0x%016llx %s\n", CONFIGS[c].name, (unsigned long long)h, ok ? "PASS" : "FAIL"); - if (!ok) { - printf(" expected 0x%016llx\n", + if (!observation_ok) { + printf(" expected obs 0x%016llx\n", (unsigned long long)BASELINE[c]); - failed++; } + if (!simulation_ok) { + printf(" expected sim 0x%016llx, got 0x%016llx\n", + (unsigned long long)SIMULATION_BASELINE[c], + (unsigned long long)stats[c].simulation_hash); + } + if (!ok) failed++; } } if (print_mode) { printf("\nconfig coverage:\n"); for (int c = 0; c < NUM_CONFIGS; c++) { - printf(" %-12s ticks=%-4d kills=%-2d winner=%d %s\n", + printf(" %-12s ticks=%-4d kills=%-2d winner=%d sim=0x%016llx %s\n", CONFIGS[c].name, stats[c].ticks, stats[c].kills, - stats[c].winner, CONFIGS[c].description); + stats[c].winner, + (unsigned long long)stats[c].simulation_hash, + CONFIGS[c].description); } - printf("\npaste the array above into BASELINE[].\n"); + printf("\npaste the observation and simulation arrays above into their baselines.\n"); return 0; } diff --git a/ocean/osrs/tests/test_zulrah_hit_delay.c b/ocean/osrs/tests/test_zulrah_hit_delay.c index ebfdcbf364..0441a85497 100644 --- a/ocean/osrs/tests/test_zulrah_hit_delay.c +++ b/ocean/osrs/tests/test_zulrah_hit_delay.c @@ -421,8 +421,74 @@ static void test_topology_geometry_parity(void) { phase_targets, 47); } +static void test_unified_player_contract_dimensions(void) { + printf("\n--- unified player contract dimensions ---\n"); + CHECK("Zulrah uses shared action heads", + ZUL_NUM_ACTION_HEADS == OSRS_BASE_NUM_ACTION_HEADS); + CHECK("Zulrah primary head uses shared target layout", + ZUL_ACTION_HEAD_DIMS[OSRS_HEAD_PRIMARY] == + OSRS_PRIMARY_DIM(ZUL_OBS_NPC_SLOTS)); + CHECK("Zulrah overhead head uses shared dimension", + ZUL_ACTION_HEAD_DIMS[OSRS_HEAD_OVERHEAD] == OSRS_OVERHEAD_DIM); + CHECK("Zulrah spell head uses shared dimension", + ZUL_ACTION_HEAD_DIMS[OSRS_HEAD_SPELL] == OSRS_SPELL_DIM); + CHECK("Zulrah observation uses shared prefix", + ZUL_NUM_OBS == OSRS_SHARED_OBS_SIZE + 104); + CHECK("Zulrah action mask uses shared dimensions", + ZUL_ACTION_MASK_SIZE == OSRS_BASE_ACTION_MASK_SIZE(ZUL_OBS_NPC_SLOTS)); +} +static void test_unified_player_contract_semantics(void) { + printf("\n--- unified player contract semantics ---\n"); + ZulrahState* state = fresh_state(0xA11CE001u); + state->player_stunned_ticks = 3; + float observation[ZUL_NUM_OBS]; + float mask[ZUL_ACTION_MASK_SIZE]; + zul_write_obs( + (EncounterState*)state, (EncounterContext*)&g_ctx, observation); + zul_write_mask( + (EncounterState*)state, (EncounterContext*)&g_ctx, mask); + unsigned char byte_mask[ZUL_ACTION_MASK_SIZE]; + zul_write_mask_bytes( + (EncounterState*)state, (EncounterContext*)&g_ctx, byte_mask); + for (int i = 0; i < ZUL_ACTION_MASK_SIZE; i++) + CHECK("Zulrah byte mask matches the float contract", + (float)byte_mask[i] == mask[i]); + + ASSERT_FLOAT_NEAR("shared hitpoints lead Zulrah observation", + observation[0], + (float)state->player.current_hitpoints / + (float)state->player.base_hitpoints, + 0.0f); + ASSERT_FLOAT_NEAR("shared inventory code is in Zulrah observation", + observation[OSRS_SHARED_OBS_INVENTORY_START], + osrs_inventory_cell_obs_code_encode( + state->player.inventory_cells[0].content_code), + 0.0f); + ASSERT_FLOAT_NEAR("encounter observation exposes melee stun duration", + observation[ZUL_OBS_AFTER_SHARED], + 3.0f / ZUL_MELEE_STUN_TICKS, + 0.0f); + ASSERT_FLOAT_NEAR("Zulrah state follows melee stun", + observation[ZUL_OBS_AFTER_SHARED + 1], + (float)state->zulrah.current_hitpoints / + (float)MONSTER_DATABASE[MON_ZULRAH_GREEN].hp, + 0.0f); + + int spell_offset = + osrs_base_action_head_mask_offset(ZUL_OBS_NPC_SLOTS, OSRS_HEAD_SPELL); + ASSERT_FLOAT_NEAR("Zulrah no-spell action stays legal", + mask[spell_offset], 1.0f, 0.0f); + for (int spell = 1; spell < OSRS_SPELL_DIM; spell++) { + ASSERT_FLOAT_NEAR("unsupported Zulrah spells stay masked", + mask[spell_offset + spell], 0.0f, 0.0f); + } +} + + int main(void) { printf("zulrah hit-delay and roll-order regressions\n\n"); + test_unified_player_contract_dimensions(); + test_unified_player_contract_semantics(); test_npc_hit_delay_by_distance(); test_player_hit_delay_by_distance(); diff --git a/ocean/osrs/tools/gen_osrs_item_obs_table.c b/ocean/osrs/tools/gen_osrs_item_obs_table.c index a1ff87fc1f..c2605627f4 100644 --- a/ocean/osrs/tools/gen_osrs_item_obs_table.c +++ b/ocean/osrs/tools/gen_osrs_item_obs_table.c @@ -268,18 +268,14 @@ static void write_header(const char* path) { "#define OSRS_ITEM_OBS_TABLE_BASE_HITPOINTS %d\n" "#define OSRS_ITEM_OBS_TABLE_BASE_PRAYER %d\n" "#define OSRS_ITEM_OBS_TABLE_BASE_RANGED %d\n\n" - "#define OSRS_ITEM_OBS_CODE_SCALE %d\n\n" - "#define OSRS_ITEM_OBS_OVERLAY_EQUIPPED %d\n" - "#define OSRS_ITEM_OBS_OVERLAY_HP_HEAL %d\n\n", + "#define OSRS_ITEM_OBS_CODE_SCALE %d\n\n", GEN_CONTENT_COUNT, GEN_CONTENT_COUNT, OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT, GEN_BASE_HITPOINTS, GEN_BASE_PRAYER, GEN_BASE_RANGED, - OSRS_ITEM_OBS_CODE_SCALE, - OSRS_INVENTORY_CELL_COMPACT_EQUIPPED, - OSRS_INVENTORY_CELL_COMPACT_HP_HEAL); + OSRS_ITEM_OBS_CODE_SCALE); write_content_rows(out); write_consumable_rows(out); fprintf(out, "\n#endif\n"); diff --git a/ocean/osrs_colosseum/osrs_colosseum.cu b/ocean/osrs_colosseum/osrs_colosseum.cu index 1d53451b8d..e52d2dc0c1 100644 --- a/ocean/osrs_colosseum/osrs_colosseum.cu +++ b/ocean/osrs_colosseum/osrs_colosseum.cu @@ -1,524 +1,40 @@ -// OSRS Colosseum CUDA entity encoder. -// Included by src/ocean.cu — requires precision_t, Prec, Allocator, puf_mm, etc. - - -// ---- Colosseum entity encoder ---- -// Mirrors the observation layout in encounter_colosseum_obs_mask.inc. No shared -// header reaches this TU, so drift is caught only by the _Static_asserts in -// osrs_visual.c; update both when the layout changes. -// - -static constexpr int COLO_ENT_OBS_SIZE = 934; -static constexpr int COLO_ENT_NPC_START = 130; -static constexpr int COLO_ENT_NUM_NPCS = 24; -static constexpr int COLO_ENT_FEATS = 34; -static constexpr int COLO_ENT_TYPE_ONEHOT = 12; -static constexpr int COLO_ENT_BOTTLENECK = 16; -static constexpr int COLO_ENT_NPC_BLOCK = COLO_ENT_NUM_NPCS * COLO_ENT_FEATS; -// The observation carries a type CODE per slot (0 empty, type+1 otherwise) where the -// encoder wants a one-hot. colo_ent_gather_npcs expands it, so every kernel downstream -- -// including the deterministic backward -- is unchanged. -static constexpr int COLO_ENT_OBS_FEATS = - 1 + (COLO_ENT_FEATS - COLO_ENT_TYPE_ONEHOT); -static constexpr int COLO_ENT_NPC_OBS_BLOCK = COLO_ENT_NUM_NPCS * COLO_ENT_OBS_FEATS; -static constexpr int COLO_ENT_INV_START = 36; -static constexpr int COLO_ENT_INV_NUM_CELLS = 28; -static constexpr int COLO_ENT_INV_FEATS = 15; -static constexpr int COLO_ENT_INV_PRESENT = 0; -static constexpr int COLO_ENT_INV_IS_ARMOR = 3; -static constexpr int COLO_ENT_INV_IS_WEAPON = 4; -static constexpr int COLO_ENT_INV_BOTTLENECK = 16; -static constexpr int COLO_ENT_INV_BLOCK = COLO_ENT_INV_NUM_CELLS * COLO_ENT_INV_FEATS; -// Each cell observes an item code plus the dynamic equipped and HP-heal fields. -// colo_ent_gather_inv always overwrites equipped and overwrites HP-heal only for non-gear. -static constexpr int COLO_ENT_INV_OBS_FEATS = 3; -static constexpr int COLO_ENT_INV_OBS_CODE = 0; -static constexpr int COLO_ENT_INV_OBS_EQUIPPED = 1; -static constexpr int COLO_ENT_INV_OBS_HP_HEAL = 2; -static_assert(COLO_ENT_INV_FEATS == OSRS_ITEM_OBS_TABLE_COLS, - "encoder record width is one item table row"); - -struct ColosseumEntityEncoderWeights { - Prec global_w; - Prec entity_l1_w; - Prec entity_l2_w; - Prec inv_l1_w; - Prec inv_l2_w; - int obs_size, hidden; +static constexpr OsrsEntityBranchDescriptor OSRS_COLOSSEUM_ENTITY_BRANCHES[] = { + { + .obs_start = 80, + .num_records = NUM_GEAR_SLOTS, + .obs_features = 1, + .type_onehot = 0, + .type_code_scale = OSRS_ITEM_OBS_CODE_SCALE, + .expansion = OSRS_ENTITY_BRANCH_ITEM_TABLE, + }, + { + .obs_start = 101, + .num_records = 24, + .obs_features = 23, + .type_onehot = 12, + .type_code_scale = 1, + .expansion = OSRS_ENTITY_BRANCH_TYPE_ONEHOT, + }, }; -struct ColosseumEntityEncoderActivations { - Prec out; - Prec saved_obs; - Prec npc_flat; - Prec entity_z1; - Prec entity_h1; - Prec grad_z1; - Int pool_argmax; - Prec global_wgrad; - Prec entity_l1_wgrad; - Prec entity_l2_wgrad; - Prec inv_flat; - Prec inv_z1; - Prec inv_h1; - Prec inv_grad_z1; - Int inv_pool_argmax; - Prec inv_l1_wgrad; - Prec inv_l2_wgrad; -}; - -__global__ void colo_ent_gather_npcs( - precision_t* __restrict__ npc_flat, const precision_t* __restrict__ obs, - int B, int obs_size) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - int total = B * COLO_ENT_NPC_BLOCK; - if (idx >= total) return; - int b = idx / COLO_ENT_NPC_BLOCK; - int off = idx % COLO_ENT_NPC_BLOCK; - int rec = off / COLO_ENT_FEATS; - int f = off - rec * COLO_ENT_FEATS; - const precision_t* src = obs + (int64_t)b * obs_size + COLO_ENT_NPC_START - + rec * COLO_ENT_OBS_FEATS; - if (f < COLO_ENT_TYPE_ONEHOT) { - int code = (int)lrintf(to_float(src[0])); - npc_flat[idx] = from_float(code == f + 1 ? 1.0f : 0.0f); - } else { - npc_flat[idx] = src[1 + (f - COLO_ENT_TYPE_ONEHOT)]; - } -} - -__device__ __forceinline__ float colo_ent_gelu_fwd(float x) { - float inner = 0.7978845608028654f * (x + 0.044715f * x * x * x); - return 0.5f * x * (1.0f + tanhf(inner)); -} - -__device__ __forceinline__ float colo_ent_gelu_grad(float x) { - float x3 = x * x * x; - float inner = 0.7978845608028654f * (x + 0.044715f * x3); - float t = tanhf(inner); - float dinner = 0.7978845608028654f * (1.0f + 3.0f * 0.044715f * x * x); - return 0.5f * (1.0f + t) + 0.5f * x * (1.0f - t * t) * dinner; -} - -__global__ void colo_ent_gather_inv( - precision_t* __restrict__ inv_flat, const precision_t* __restrict__ obs, - int B, int obs_size) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - int total = B * COLO_ENT_INV_BLOCK; - if (idx >= total) return; - int b = idx / COLO_ENT_INV_BLOCK; - int off = idx % COLO_ENT_INV_BLOCK; - int cell = off / COLO_ENT_INV_FEATS; - int f = off - cell * COLO_ENT_INV_FEATS; - const precision_t* src = obs + (int64_t)b * obs_size + COLO_ENT_INV_START - + cell * COLO_ENT_INV_OBS_FEATS; - int code = (int)lrintf( - to_float(src[COLO_ENT_INV_OBS_CODE]) * (float)OSRS_ITEM_OBS_CODE_SCALE); - assert(code >= 0 && code < OSRS_ITEM_OBS_TABLE_ROWS); - const float* table_row = OSRS_ITEM_OBS_TABLE_DEV[code]; - float v = table_row[f]; - if (f == OSRS_ITEM_OBS_OVERLAY_EQUIPPED) - v = to_float(src[COLO_ENT_INV_OBS_EQUIPPED]); - if (f == OSRS_ITEM_OBS_OVERLAY_HP_HEAL) { - int is_gear = table_row[COLO_ENT_INV_IS_ARMOR] != 0.0f || - table_row[COLO_ENT_INV_IS_WEAPON] != 0.0f; - if (!is_gear) - v = to_float(src[COLO_ENT_INV_OBS_HP_HEAL]); - } - inv_flat[idx] = from_float(v); -} - -// ---- fused pool kernels ---- -static constexpr int COLO_ENT_BATCH_TILE = 8; -static constexpr int COLO_ENT_HIDDEN_TILE = 32; -static constexpr int COLO_ENT_FC_THREADS = COLO_ENT_BATCH_TILE * COLO_ENT_HIDDEN_TILE; - -__global__ void colo_ent_fused_pool_fwd( - precision_t* __restrict__ out, int* __restrict__ argmax, - precision_t* __restrict__ h1, - const precision_t* __restrict__ z1, const precision_t* __restrict__ rec_flat, - const precision_t* __restrict__ l2_w, - int B, int H, int num_rec, int rec_feats, int active_width) { - extern __shared__ float colo_ent_sh[]; - float* h1_tile = colo_ent_sh; - float* mask_tile = h1_tile + COLO_ENT_BATCH_TILE * num_rec * COLO_ENT_BOTTLENECK; - float* w_tile = mask_tile + COLO_ENT_BATCH_TILE * num_rec; - - int batch_base = blockIdx.x * COLO_ENT_BATCH_TILE; - int hidden_base = blockIdx.y * COLO_ENT_HIDDEN_TILE; - int tx = threadIdx.x; - int ty = threadIdx.y; - int tid = ty * COLO_ENT_HIDDEN_TILE + tx; - - int h1_values = COLO_ENT_BATCH_TILE * num_rec * COLO_ENT_BOTTLENECK; - for (int idx = tid; idx < h1_values; idx += COLO_ENT_FC_THREADS) { - int bt = idx / (num_rec * COLO_ENT_BOTTLENECK); - int rem = idx - bt * num_rec * COLO_ENT_BOTTLENECK; - int gb = batch_base + bt; - float v = gb < B - ? colo_ent_gelu_fwd(to_float(z1[(int64_t)gb * num_rec * COLO_ENT_BOTTLENECK + rem])) - : 0.0f; - h1_tile[idx] = v; - if (h1 && blockIdx.y == 0 && gb < B) - h1[(int64_t)gb * num_rec * COLO_ENT_BOTTLENECK + rem] = from_float(v); - } - int mask_values = COLO_ENT_BATCH_TILE * num_rec; - for (int idx = tid; idx < mask_values; idx += COLO_ENT_FC_THREADS) { - int bt = idx / num_rec; - int n = idx - bt * num_rec; - int gb = batch_base + bt; - float active = 0.0f; - if (gb < B) { - const precision_t* rec = rec_flat + ((int64_t)gb * num_rec + n) * rec_feats; - for (int t = 0; t < active_width; t++) active += to_float(rec[t]); - } - mask_tile[idx] = active; - } - int w_values = COLO_ENT_HIDDEN_TILE * COLO_ENT_BOTTLENECK; - for (int idx = tid; idx < w_values; idx += COLO_ENT_FC_THREADS) { - int d = idx / COLO_ENT_HIDDEN_TILE; - int th = idx - d * COLO_ENT_HIDDEN_TILE; - int gh = hidden_base + th; - w_tile[idx] = gh < H - ? to_float(l2_w[(int64_t)gh * COLO_ENT_BOTTLENECK + d]) - : 0.0f; - } - __syncthreads(); - - int b = batch_base + ty; - int h = hidden_base + tx; - if (b >= B || h >= H) return; - float best = -3.4028234663852886e38f; - int best_n = -1; - for (int n = 0; n < num_rec; n++) { - if (mask_tile[ty * num_rec + n] <= 0.0f) continue; - const float* hp = h1_tile + (ty * num_rec + n) * COLO_ENT_BOTTLENECK; - float sum = 0.0f; -#pragma unroll - for (int d = 0; d < COLO_ENT_BOTTLENECK; d++) - sum += w_tile[d * COLO_ENT_HIDDEN_TILE + tx] * hp[d]; - if (sum > best) { best = sum; best_n = n; } - } - int64_t o = (int64_t)b * H + h; - out[o] = from_float(to_float(out[o]) + (best_n < 0 ? 0.0f : best)); - argmax[o] = best_n; -} - -__global__ void colo_ent_fused_l2_wgrad( - precision_t* __restrict__ wgrad, const precision_t* __restrict__ grad, - const precision_t* __restrict__ h1, const int* __restrict__ argmax, - int B, int H, int num_rec) { - int h = blockIdx.x; - if (h >= H) return; - - float sum[COLO_ENT_BOTTLENECK]; -#pragma unroll - for (int k = 0; k < COLO_ENT_BOTTLENECK; k++) sum[k] = 0.0f; - - for (int b = threadIdx.x; b < B; b += blockDim.x) { - int n = argmax[(int64_t)b * H + h]; - if (n < 0) continue; - float g = to_float(grad[(int64_t)b * H + h]); - const precision_t* hp = h1 + ((int64_t)b * num_rec + n) * COLO_ENT_BOTTLENECK; -#pragma unroll - for (int k = 0; k < COLO_ENT_BOTTLENECK; k++) - sum[k] += g * to_float(hp[k]); - } - - __shared__ float warp_sums[COLO_ENT_BOTTLENECK * 32]; - int lane = threadIdx.x & 31; - int warp = threadIdx.x >> 5; - int num_warps = (blockDim.x + 31) >> 5; -#pragma unroll - for (int k = 0; k < COLO_ENT_BOTTLENECK; k++) { - float s = sum[k]; - for (int offset = 16; offset > 0; offset >>= 1) - s += __shfl_down_sync(0xffffffff, s, offset); - if (lane == 0) warp_sums[k * 32 + warp] = s; - } - __syncthreads(); - - if (warp == 0) { -#pragma unroll - for (int k = 0; k < COLO_ENT_BOTTLENECK; k++) { - float s = lane < num_warps ? warp_sums[k * 32 + lane] : 0.0f; - for (int offset = 16; offset > 0; offset >>= 1) - s += __shfl_down_sync(0xffffffff, s, offset); - if (lane == 0) wgrad[(int64_t)h * COLO_ENT_BOTTLENECK + k] = from_float(s); - } - } -} - -__global__ void colo_ent_fused_grad_z1( - precision_t* __restrict__ grad_z1, const precision_t* __restrict__ grad, - const precision_t* __restrict__ l2_w, const precision_t* __restrict__ z1, - const int* __restrict__ argmax, int B, int H, int num_rec) { - int b = blockIdx.x; - if (b >= B) return; - - extern __shared__ float colo_ent_sh[]; - float* accum = colo_ent_sh; - int* arg_s = (int*)(accum + num_rec * COLO_ENT_BOTTLENECK); - float* grad_s = (float*)(arg_s + blockDim.x); - - for (int idx = threadIdx.x; idx < num_rec * COLO_ENT_BOTTLENECK; idx += blockDim.x) - accum[idx] = 0.0f; - __syncthreads(); - - for (int base = 0; base < H; base += blockDim.x) { - int h = base + threadIdx.x; - if (h < H) { - arg_s[threadIdx.x] = argmax[(int64_t)b * H + h]; - grad_s[threadIdx.x] = to_float(grad[(int64_t)b * H + h]); - } - __syncthreads(); - - // Gathered, not scattered: one thread owns each (record, k) accumulator and - // walks j in order, so the summation order is fixed. An atomicAdd scatter - // here let warp scheduling pick the order, and non-associative float - // addition then made training irreproducible run to run. - int tile = H - base; - if (tile > (int)blockDim.x) tile = blockDim.x; - for (int idx = threadIdx.x; idx < num_rec * COLO_ENT_BOTTLENECK; idx += blockDim.x) { - int n = idx / COLO_ENT_BOTTLENECK; - int k = idx - n * COLO_ENT_BOTTLENECK; - float sum = 0.0f; - for (int j = 0; j < tile; j++) { - if (arg_s[j] != n) continue; - sum += grad_s[j] * to_float(l2_w[(int64_t)(base + j) * COLO_ENT_BOTTLENECK + k]); - } - accum[idx] += sum; - } - __syncthreads(); - } +static_assert(12 + 23 - 1 == 34); +static_assert(101 + 24 * 23 <= 904); - for (int idx = threadIdx.x; idx < num_rec * COLO_ENT_BOTTLENECK; idx += blockDim.x) { - int64_t o = (int64_t)b * num_rec * COLO_ENT_BOTTLENECK + idx; - grad_z1[o] = from_float(accum[idx] * colo_ent_gelu_grad(to_float(z1[o]))); - } -} - -static void colo_ent_launch_fused_fwd( - precision_t* out, int* argmax, precision_t* h1, - const precision_t* z1, const precision_t* rec_flat, - const precision_t* l2_w, int B, int H, int num_rec, int rec_feats, int active_width, - cudaStream_t stream) { - dim3 block(COLO_ENT_HIDDEN_TILE, COLO_ENT_BATCH_TILE); - dim3 grid((B + COLO_ENT_BATCH_TILE - 1) / COLO_ENT_BATCH_TILE, - (H + COLO_ENT_HIDDEN_TILE - 1) / COLO_ENT_HIDDEN_TILE); - size_t shared_bytes = ( - (size_t)COLO_ENT_BATCH_TILE * num_rec * COLO_ENT_BOTTLENECK + - (size_t)COLO_ENT_BATCH_TILE * num_rec + - (size_t)COLO_ENT_HIDDEN_TILE * COLO_ENT_BOTTLENECK) * sizeof(float); - colo_ent_fused_pool_fwd<<>>( - out, argmax, h1, z1, rec_flat, l2_w, B, H, num_rec, rec_feats, active_width); -} - -static void colo_ent_launch_fused_bwd( - precision_t* l2_wgrad, precision_t* grad_z1, const precision_t* grad, - const precision_t* l2_w, const precision_t* z1, const precision_t* h1, - const int* argmax, int B, int H, int num_rec, cudaStream_t stream) { - colo_ent_fused_l2_wgrad<<>>( - l2_wgrad, grad, h1, argmax, B, H, num_rec); - size_t shared_bytes = - ((size_t)num_rec * COLO_ENT_BOTTLENECK + 2 * BLOCK_SIZE) * sizeof(float); - colo_ent_fused_grad_z1<<>>( - grad_z1, grad, l2_w, z1, argmax, B, H, num_rec); -} - -static Prec colo_entity_encoder_forward(void* w, void* activations, Prec input, cudaStream_t stream) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; - int B = input.shape[0]; - int H = ew->hidden; - int NB = B * COLO_ENT_NUM_NPCS; - - if (a->saved_obs.data) puf_copy(&a->saved_obs, &input, stream); - - puf_mm(&input, &ew->global_w, &a->out, stream); - - colo_ent_gather_npcs<<>>( - a->npc_flat.data, input.data, B, ew->obs_size); - - Prec npc2d = {.data = a->npc_flat.data, .shape = {NB, COLO_ENT_FEATS}}; - puf_mm(&npc2d, &ew->entity_l1_w, &a->entity_z1, stream); - colo_ent_launch_fused_fwd( - a->out.data, a->pool_argmax.data, a->entity_h1.data, - a->entity_z1.data, a->npc_flat.data, - ew->entity_l2_w.data, B, H, COLO_ENT_NUM_NPCS, COLO_ENT_FEATS, - COLO_ENT_TYPE_ONEHOT, stream); - - int IB = B * COLO_ENT_INV_NUM_CELLS; - colo_ent_gather_inv<<>>( - a->inv_flat.data, input.data, B, ew->obs_size); - Prec inv2d = {.data = a->inv_flat.data, .shape = {IB, COLO_ENT_INV_FEATS}}; - puf_mm(&inv2d, &ew->inv_l1_w, &a->inv_z1, stream); - static_assert(COLO_ENT_INV_PRESENT == 0, - "fused pool masks the EXPANDED record, so present must be cell-local offset 0 there; " - "the observation carries the item code at that offset instead"); - colo_ent_launch_fused_fwd( - a->out.data, a->inv_pool_argmax.data, a->inv_h1.data, - a->inv_z1.data, a->inv_flat.data, - ew->inv_l2_w.data, B, H, COLO_ENT_INV_NUM_CELLS, COLO_ENT_INV_FEATS, - 1, stream); - return a->out; -} - -static void colo_entity_encoder_backward(void* w, void* activations, Prec grad, cudaStream_t stream) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; - int B = grad.shape[0]; - int H = ew->hidden; - int NB = B * COLO_ENT_NUM_NPCS; - - puf_mm_tn(&grad, &a->saved_obs, &a->global_wgrad, stream); - - colo_ent_launch_fused_bwd( - a->entity_l2_wgrad.data, a->grad_z1.data, grad.data, - ew->entity_l2_w.data, a->entity_z1.data, a->entity_h1.data, - a->pool_argmax.data, B, H, COLO_ENT_NUM_NPCS, stream); - Prec npc2d = {.data = a->npc_flat.data, .shape = {NB, COLO_ENT_FEATS}}; - puf_mm_tn(&a->grad_z1, &npc2d, &a->entity_l1_wgrad, stream); - - int IB = B * COLO_ENT_INV_NUM_CELLS; - colo_ent_launch_fused_bwd( - a->inv_l2_wgrad.data, a->inv_grad_z1.data, grad.data, - ew->inv_l2_w.data, a->inv_z1.data, a->inv_h1.data, - a->inv_pool_argmax.data, B, H, COLO_ENT_INV_NUM_CELLS, stream); - Prec inv2d = {.data = a->inv_flat.data, .shape = {IB, COLO_ENT_INV_FEATS}}; - puf_mm_tn(&a->inv_grad_z1, &inv2d, &a->inv_l1_wgrad, stream); -} - -static void colo_entity_encoder_init_weights(void* w, uint64_t* seed, cudaStream_t stream) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - auto init2d = [&](Prec& t, int rows, int cols) { - Prec wt = {.data = t.data, .shape = {rows, cols}}; - puf_kaiming_init(&wt, sqrtf(2.0f), (*seed)++, stream); - }; - init2d(ew->global_w, ew->hidden, ew->obs_size); - init2d(ew->entity_l1_w, COLO_ENT_BOTTLENECK, COLO_ENT_FEATS); - init2d(ew->entity_l2_w, ew->hidden, COLO_ENT_BOTTLENECK); - init2d(ew->inv_l1_w, COLO_ENT_INV_BOTTLENECK, COLO_ENT_INV_FEATS); - init2d(ew->inv_l2_w, ew->hidden, COLO_ENT_INV_BOTTLENECK); -} - -static void colo_entity_assert_aligned(int64_t numel, const char* name) { - if (numel % 8 != 0) { - fprintf(stderr, "colosseum entity encoder: %s numel %lld not a multiple of 8; " - "bf16 packing would corrupt weights\n", name, (long long)numel); - abort(); - } -} - -static void colo_entity_encoder_reg_params(void* w, Allocator* alloc) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - ew->global_w = {.shape = {ew->hidden, ew->obs_size}}; - ew->entity_l1_w = {.shape = {COLO_ENT_BOTTLENECK, COLO_ENT_FEATS}}; - ew->entity_l2_w = {.shape = {ew->hidden, COLO_ENT_BOTTLENECK}}; - colo_entity_assert_aligned(numel(ew->global_w.shape), "global_w"); - colo_entity_assert_aligned(numel(ew->entity_l1_w.shape), "entity_l1_w"); - colo_entity_assert_aligned(numel(ew->entity_l2_w.shape), "entity_l2_w"); - alloc_register(alloc, &ew->global_w); - alloc_register(alloc, &ew->entity_l1_w); - alloc_register(alloc, &ew->entity_l2_w); - ew->inv_l1_w = {.shape = {COLO_ENT_INV_BOTTLENECK, COLO_ENT_INV_FEATS}}; - ew->inv_l2_w = {.shape = {ew->hidden, COLO_ENT_INV_BOTTLENECK}}; - colo_entity_assert_aligned(numel(ew->inv_l1_w.shape), "inv_l1_w"); - colo_entity_assert_aligned(numel(ew->inv_l2_w.shape), "inv_l2_w"); - alloc_register(alloc, &ew->inv_l1_w); - alloc_register(alloc, &ew->inv_l2_w); -} - -static void colo_entity_encoder_reg_train(void* w, void* activations, Allocator* acts, Allocator* grads, int B_TT) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; - int H = ew->hidden; - int NB = B_TT * COLO_ENT_NUM_NPCS; - *a = {}; - a->out = {.shape = {B_TT, H}}; - a->saved_obs = {.shape = {B_TT, ew->obs_size}}; - a->npc_flat = {.shape = {NB, COLO_ENT_FEATS}}; - a->entity_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; - a->entity_h1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; - a->grad_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; - a->pool_argmax = {.shape = {B_TT, H}}; - alloc_register(acts, &a->out); - alloc_register(acts, &a->saved_obs); - alloc_register(acts, &a->npc_flat); - alloc_register(acts, &a->entity_z1); - alloc_register(acts, &a->entity_h1); - alloc_register(acts, &a->grad_z1); - alloc_register(acts, &a->pool_argmax); - int IB = B_TT * COLO_ENT_INV_NUM_CELLS; - a->inv_flat = {.shape = {IB, COLO_ENT_INV_FEATS}}; - a->inv_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; - a->inv_h1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; - a->inv_grad_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; - a->inv_pool_argmax = {.shape = {B_TT, H}}; - alloc_register(acts, &a->inv_flat); - alloc_register(acts, &a->inv_z1); - alloc_register(acts, &a->inv_h1); - alloc_register(acts, &a->inv_grad_z1); - alloc_register(acts, &a->inv_pool_argmax); - a->global_wgrad = {.shape = {H, ew->obs_size}}; - a->entity_l1_wgrad = {.shape = {COLO_ENT_BOTTLENECK, COLO_ENT_FEATS}}; - a->entity_l2_wgrad = {.shape = {H, COLO_ENT_BOTTLENECK}}; - alloc_register(grads, &a->global_wgrad); - alloc_register(grads, &a->entity_l1_wgrad); - alloc_register(grads, &a->entity_l2_wgrad); - a->inv_l1_wgrad = {.shape = {COLO_ENT_INV_BOTTLENECK, COLO_ENT_INV_FEATS}}; - a->inv_l2_wgrad = {.shape = {H, COLO_ENT_INV_BOTTLENECK}}; - alloc_register(grads, &a->inv_l1_wgrad); - alloc_register(grads, &a->inv_l2_wgrad); -} - -static void colo_entity_encoder_reg_rollout(void* w, void* activations, Allocator* alloc, int B) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; - int H = ew->hidden; - int NB = B * COLO_ENT_NUM_NPCS; - a->out = {.shape = {B, H}}; - a->npc_flat = {.shape = {NB, COLO_ENT_FEATS}}; - a->entity_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; - a->pool_argmax = {.shape = {B, H}}; - alloc_register(alloc, &a->out); - alloc_register(alloc, &a->npc_flat); - alloc_register(alloc, &a->entity_z1); - alloc_register(alloc, &a->pool_argmax); - int IB = B * COLO_ENT_INV_NUM_CELLS; - a->inv_flat = {.shape = {IB, COLO_ENT_INV_FEATS}}; - a->inv_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; - a->inv_pool_argmax = {.shape = {B, H}}; - alloc_register(alloc, &a->inv_flat); - alloc_register(alloc, &a->inv_z1); - alloc_register(alloc, &a->inv_pool_argmax); -} +static constexpr OsrsEntityEncoderDescriptor OSRS_COLOSSEUM_ENTITY_DESCRIPTOR = { + .env_name = "osrs_colosseum", + .obs_size = 904, + .branches = OSRS_COLOSSEUM_ENTITY_BRANCHES, + .num_branches = 2, +}; -static void* colo_entity_encoder_create_weights(void* self) { - Encoder* e = (Encoder*)self; - ColosseumEntityEncoderWeights* ew = - (ColosseumEntityEncoderWeights*)calloc(1, sizeof(ColosseumEntityEncoderWeights)); - ew->obs_size = e->in_dim; - ew->hidden = e->out_dim; - return ew; +static void* osrs_colosseum_entity_encoder_create_weights(void* self) { + return osrs_entity_encoder_create_weights( + self, &OSRS_COLOSSEUM_ENTITY_DESCRIPTOR); } -// ---- Inferno entity encoder ---- -// Mirrors the observation layout in encounter_inferno_obs_mask.inc, whose block -// offsets are the INF_OBS_AFTER_* macros in encounter_inferno_forecast.inc. No -// shared header reaches this TU, so drift is caught only by the _Static_asserts -// in osrs_visual.c; update both when the layout changes. - -static void create_osrs_colosseum_encoder(Encoder* enc) { - *enc = Encoder{ - .forward = colo_entity_encoder_forward, - .backward = colo_entity_encoder_backward, - .init_weights = colo_entity_encoder_init_weights, - .reg_params = colo_entity_encoder_reg_params, - .reg_train = colo_entity_encoder_reg_train, - .reg_rollout = colo_entity_encoder_reg_rollout, - .create_weights = colo_entity_encoder_create_weights, - .in_dim = enc->in_dim, .out_dim = enc->out_dim, - .activation_size = sizeof(ColosseumEntityEncoderActivations), - }; +static void create_osrs_colosseum_encoder(Encoder* encoder) { + osrs_entity_encoder_configure( + encoder, + osrs_colosseum_entity_encoder_create_weights, + &OSRS_COLOSSEUM_ENTITY_DESCRIPTOR); } diff --git a/ocean/osrs_colosseum/osrs_colosseum.h b/ocean/osrs_colosseum/osrs_colosseum.h index 7df1f62a2b..d1a44b231b 100644 --- a/ocean/osrs_colosseum/osrs_colosseum.h +++ b/ocean/osrs_colosseum/osrs_colosseum.h @@ -134,10 +134,10 @@ static inline float col_curriculum_uniform(uint32_t env_index) { static void col_write_action_mask_bytes(Env* env, unsigned char* mask_out) { if (!mask_out) return; float mask_f[COLO_ACTION_MASK_SIZE]; - ENCOUNTER_COLOSSEUM.write_mask(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), mask_f); - for (int i = 0; i < COLO_ACTION_MASK_SIZE; i++) { + ENCOUNTER_COLOSSEUM.write_mask( + COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), mask_f); + for (int i = 0; i < COLO_ACTION_MASK_SIZE; i++) mask_out[i] = mask_f[i] != 0.0f ? 1 : 0; - } } static inline void col_log_dpt_sample(float* hit_acc, float* n_acc, int sample) { diff --git a/ocean/osrs_inferno/osrs_inferno.cu b/ocean/osrs_inferno/osrs_inferno.cu index 7eafbf2298..ba833db326 100644 --- a/ocean/osrs_inferno/osrs_inferno.cu +++ b/ocean/osrs_inferno/osrs_inferno.cu @@ -1,246 +1,40 @@ -// OSRS Inferno CUDA entity encoder. -// Included by src/ocean.cu — requires precision_t, Prec, Allocator, puf_mm, etc. -// Shares ColosseumEntityEncoderWeights/Activations layout (same weight slots). - -static constexpr int INF_ENT_NPC_START = 54; -static constexpr int INF_ENT_NUM_NPCS = 14; -static constexpr int INF_ENT_OBS_FEATS = 13; -static constexpr int INF_ENT_FEATS = 26; -static constexpr int INF_ENT_TYPE_ONEHOT = 14; -static constexpr int INF_ENT_TYPE_CODE_SCALE = 16; -static constexpr int INF_ENT_NPC_BLOCK = INF_ENT_NUM_NPCS * INF_ENT_FEATS; -static constexpr int INF_ENT_INV_START = 460; -static constexpr int INF_ENT_INV_NUM_CELLS = 28; -static constexpr int INF_ENT_INV_OBS_FEATS = 1; -static constexpr int INF_ENT_INV_FEATS = 15; -static constexpr int INF_ENT_INV_BLOCK = INF_ENT_INV_NUM_CELLS * INF_ENT_INV_FEATS; -static constexpr int INF_ENT_OBS_SIZE = 498; - -static_assert(INF_ENT_FEATS == - INF_ENT_TYPE_ONEHOT + INF_ENT_OBS_FEATS - 1); -static_assert(INF_ENT_INV_FEATS == OSRS_ITEM_OBS_TABLE_COLS); - -__global__ void inf_ent_gather_npcs( - precision_t* __restrict__ npc_flat, const precision_t* __restrict__ obs, - int B, int obs_size) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - int total = B * INF_ENT_NPC_BLOCK; - if (idx >= total) return; - int b = idx / INF_ENT_NPC_BLOCK; - int off = idx % INF_ENT_NPC_BLOCK; - int rec = off / INF_ENT_FEATS; - int f = off - rec * INF_ENT_FEATS; - const precision_t* src = obs + (int64_t)b * obs_size + INF_ENT_NPC_START - + rec * INF_ENT_OBS_FEATS; - if (f < INF_ENT_TYPE_ONEHOT) { - int code = (int)lrintf( - to_float(src[0]) * (float)INF_ENT_TYPE_CODE_SCALE); - assert(code >= 0 && code <= INF_ENT_TYPE_ONEHOT); - npc_flat[idx] = from_float(code == f + 1 ? 1.0f : 0.0f); - } else { - npc_flat[idx] = src[1 + f - INF_ENT_TYPE_ONEHOT]; - } -} - -__global__ void inf_ent_gather_inv( - precision_t* __restrict__ inv_flat, const precision_t* __restrict__ obs, - int B, int obs_size) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - int total = B * INF_ENT_INV_BLOCK; - if (idx >= total) return; - int b = idx / INF_ENT_INV_BLOCK; - int off = idx % INF_ENT_INV_BLOCK; - int cell = off / INF_ENT_INV_FEATS; - int f = off - cell * INF_ENT_INV_FEATS; - const precision_t* src = obs + (int64_t)b * obs_size + INF_ENT_INV_START - + cell * INF_ENT_INV_OBS_FEATS; - int code = (int)lrintf( - to_float(src[0]) * (float)OSRS_ITEM_OBS_CODE_SCALE); - assert(code >= 0 && code < OSRS_ITEM_OBS_TABLE_ROWS); - inv_flat[idx] = from_float(OSRS_ITEM_OBS_TABLE_DEV[code][f]); -} - -static Prec inf_entity_encoder_forward(void* w, void* activations, Prec input, cudaStream_t stream) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; - int B = input.shape[0]; - int H = ew->hidden; - int NB = B * INF_ENT_NUM_NPCS; - - if (a->saved_obs.data) puf_copy(&a->saved_obs, &input, stream); - - puf_mm(&input, &ew->global_w, &a->out, stream); - - inf_ent_gather_npcs<<>>( - a->npc_flat.data, input.data, B, ew->obs_size); - Prec npc2d = {.data = a->npc_flat.data, .shape = {NB, INF_ENT_FEATS}}; - puf_mm(&npc2d, &ew->entity_l1_w, &a->entity_z1, stream); - colo_ent_launch_fused_fwd( - a->out.data, a->pool_argmax.data, a->entity_h1.data, - a->entity_z1.data, a->npc_flat.data, - ew->entity_l2_w.data, B, H, INF_ENT_NUM_NPCS, INF_ENT_FEATS, - INF_ENT_TYPE_ONEHOT, stream); - - int IB = B * INF_ENT_INV_NUM_CELLS; - inf_ent_gather_inv<<>>( - a->inv_flat.data, input.data, B, ew->obs_size); - Prec inv2d = {.data = a->inv_flat.data, .shape = {IB, INF_ENT_INV_FEATS}}; - puf_mm(&inv2d, &ew->inv_l1_w, &a->inv_z1, stream); - colo_ent_launch_fused_fwd( - a->out.data, a->inv_pool_argmax.data, a->inv_h1.data, - a->inv_z1.data, a->inv_flat.data, - ew->inv_l2_w.data, B, H, INF_ENT_INV_NUM_CELLS, INF_ENT_INV_FEATS, - 1, stream); - return a->out; -} - -static void inf_entity_encoder_backward(void* w, void* activations, Prec grad, cudaStream_t stream) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; - int B = grad.shape[0]; - int H = ew->hidden; - int NB = B * INF_ENT_NUM_NPCS; - - puf_mm_tn(&grad, &a->saved_obs, &a->global_wgrad, stream); - - colo_ent_launch_fused_bwd( - a->entity_l2_wgrad.data, a->grad_z1.data, grad.data, - ew->entity_l2_w.data, a->entity_z1.data, a->entity_h1.data, - a->pool_argmax.data, B, H, INF_ENT_NUM_NPCS, stream); - Prec npc2d = {.data = a->npc_flat.data, .shape = {NB, INF_ENT_FEATS}}; - puf_mm_tn(&a->grad_z1, &npc2d, &a->entity_l1_wgrad, stream); - - int IB = B * INF_ENT_INV_NUM_CELLS; - colo_ent_launch_fused_bwd( - a->inv_l2_wgrad.data, a->inv_grad_z1.data, grad.data, - ew->inv_l2_w.data, a->inv_z1.data, a->inv_h1.data, - a->inv_pool_argmax.data, B, H, INF_ENT_INV_NUM_CELLS, stream); - Prec inv2d = {.data = a->inv_flat.data, .shape = {IB, INF_ENT_INV_FEATS}}; - puf_mm_tn(&a->inv_grad_z1, &inv2d, &a->inv_l1_wgrad, stream); -} - -static void inf_entity_encoder_init_weights(void* w, uint64_t* seed, cudaStream_t stream) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - auto init2d = [&](Prec& t, int rows, int cols) { - Prec wt = {.data = t.data, .shape = {rows, cols}}; - puf_kaiming_init(&wt, sqrtf(2.0f), (*seed)++, stream); - }; - init2d(ew->global_w, ew->hidden, ew->obs_size); - init2d(ew->entity_l1_w, COLO_ENT_BOTTLENECK, INF_ENT_FEATS); - init2d(ew->entity_l2_w, ew->hidden, COLO_ENT_BOTTLENECK); - init2d(ew->inv_l1_w, COLO_ENT_INV_BOTTLENECK, INF_ENT_INV_FEATS); - init2d(ew->inv_l2_w, ew->hidden, COLO_ENT_INV_BOTTLENECK); -} - -static void inf_entity_encoder_reg_params(void* w, Allocator* alloc) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - ew->global_w = {.shape = {ew->hidden, ew->obs_size}}; - ew->entity_l1_w = {.shape = {COLO_ENT_BOTTLENECK, INF_ENT_FEATS}}; - ew->entity_l2_w = {.shape = {ew->hidden, COLO_ENT_BOTTLENECK}}; - colo_entity_assert_aligned(numel(ew->global_w.shape), "global_w"); - colo_entity_assert_aligned(numel(ew->entity_l1_w.shape), "entity_l1_w"); - colo_entity_assert_aligned(numel(ew->entity_l2_w.shape), "entity_l2_w"); - alloc_register(alloc, &ew->global_w); - alloc_register(alloc, &ew->entity_l1_w); - alloc_register(alloc, &ew->entity_l2_w); - ew->inv_l1_w = {.shape = {COLO_ENT_INV_BOTTLENECK, INF_ENT_INV_FEATS}}; - ew->inv_l2_w = {.shape = {ew->hidden, COLO_ENT_INV_BOTTLENECK}}; - colo_entity_assert_aligned(numel(ew->inv_l1_w.shape), "inv_l1_w"); - colo_entity_assert_aligned(numel(ew->inv_l2_w.shape), "inv_l2_w"); - alloc_register(alloc, &ew->inv_l1_w); - alloc_register(alloc, &ew->inv_l2_w); -} - -static void inf_entity_encoder_reg_train(void* w, void* activations, Allocator* acts, Allocator* grads, int B_TT) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; - int H = ew->hidden; - int NB = B_TT * INF_ENT_NUM_NPCS; - *a = {}; - a->out = {.shape = {B_TT, H}}; - a->saved_obs = {.shape = {B_TT, ew->obs_size}}; - a->npc_flat = {.shape = {NB, INF_ENT_FEATS}}; - a->entity_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; - a->entity_h1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; - a->grad_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; - a->pool_argmax = {.shape = {B_TT, H}}; - alloc_register(acts, &a->out); - alloc_register(acts, &a->saved_obs); - alloc_register(acts, &a->npc_flat); - alloc_register(acts, &a->entity_z1); - alloc_register(acts, &a->entity_h1); - alloc_register(acts, &a->grad_z1); - alloc_register(acts, &a->pool_argmax); - int IB = B_TT * INF_ENT_INV_NUM_CELLS; - a->inv_flat = {.shape = {IB, INF_ENT_INV_FEATS}}; - a->inv_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; - a->inv_h1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; - a->inv_grad_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; - a->inv_pool_argmax = {.shape = {B_TT, H}}; - alloc_register(acts, &a->inv_flat); - alloc_register(acts, &a->inv_z1); - alloc_register(acts, &a->inv_h1); - alloc_register(acts, &a->inv_grad_z1); - alloc_register(acts, &a->inv_pool_argmax); - a->global_wgrad = {.shape = {H, ew->obs_size}}; - a->entity_l1_wgrad = {.shape = {COLO_ENT_BOTTLENECK, INF_ENT_FEATS}}; - a->entity_l2_wgrad = {.shape = {H, COLO_ENT_BOTTLENECK}}; - alloc_register(grads, &a->global_wgrad); - alloc_register(grads, &a->entity_l1_wgrad); - alloc_register(grads, &a->entity_l2_wgrad); - a->inv_l1_wgrad = {.shape = {COLO_ENT_INV_BOTTLENECK, INF_ENT_INV_FEATS}}; - a->inv_l2_wgrad = {.shape = {H, COLO_ENT_INV_BOTTLENECK}}; - alloc_register(grads, &a->inv_l1_wgrad); - alloc_register(grads, &a->inv_l2_wgrad); -} - -static void inf_entity_encoder_reg_rollout(void* w, void* activations, Allocator* alloc, int B) { - ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; - ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; - int H = ew->hidden; - int NB = B * INF_ENT_NUM_NPCS; - a->out = {.shape = {B, H}}; - a->npc_flat = {.shape = {NB, INF_ENT_FEATS}}; - a->entity_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; - a->pool_argmax = {.shape = {B, H}}; - alloc_register(alloc, &a->out); - alloc_register(alloc, &a->npc_flat); - alloc_register(alloc, &a->entity_z1); - alloc_register(alloc, &a->pool_argmax); - int IB = B * INF_ENT_INV_NUM_CELLS; - a->inv_flat = {.shape = {IB, INF_ENT_INV_FEATS}}; - a->inv_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; - a->inv_pool_argmax = {.shape = {B, H}}; - alloc_register(alloc, &a->inv_flat); - alloc_register(alloc, &a->inv_z1); - alloc_register(alloc, &a->inv_pool_argmax); -} - -static void* inf_entity_encoder_create_weights(void* self) { - Encoder* e = (Encoder*)self; - if (e->in_dim != INF_ENT_OBS_SIZE) { - fprintf(stderr, "inferno entity encoder: env obs size %d != encoder slice " - "expectation %d; INF_ENT_* constants are stale\n", - e->in_dim, INF_ENT_OBS_SIZE); - abort(); - } - ColosseumEntityEncoderWeights* ew = - (ColosseumEntityEncoderWeights*)calloc(1, sizeof(ColosseumEntityEncoderWeights)); - ew->obs_size = e->in_dim; - ew->hidden = e->out_dim; - return ew; -} - -static void create_osrs_inferno_encoder(Encoder* enc) { - *enc = Encoder{ - .forward = inf_entity_encoder_forward, - .backward = inf_entity_encoder_backward, - .init_weights = inf_entity_encoder_init_weights, - .reg_params = inf_entity_encoder_reg_params, - .reg_train = inf_entity_encoder_reg_train, - .reg_rollout = inf_entity_encoder_reg_rollout, - .create_weights = inf_entity_encoder_create_weights, - .in_dim = enc->in_dim, .out_dim = enc->out_dim, - .activation_size = sizeof(ColosseumEntityEncoderActivations), - }; +static constexpr OsrsEntityBranchDescriptor OSRS_INFERNO_ENTITY_BRANCHES[] = { + { + .obs_start = 80, + .num_records = NUM_GEAR_SLOTS, + .obs_features = 1, + .type_onehot = 0, + .type_code_scale = OSRS_ITEM_OBS_CODE_SCALE, + .expansion = OSRS_ENTITY_BRANCH_ITEM_TABLE, + }, + { + .obs_start = 124, + .num_records = 14, + .obs_features = 13, + .type_onehot = 14, + .type_code_scale = 16, + .expansion = OSRS_ENTITY_BRANCH_TYPE_ONEHOT, + }, +}; + +static_assert(14 + 13 - 1 == 26); +static_assert(124 + 14 * 13 <= 530); + +static constexpr OsrsEntityEncoderDescriptor OSRS_INFERNO_ENTITY_DESCRIPTOR = { + .env_name = "osrs_inferno", + .obs_size = 530, + .branches = OSRS_INFERNO_ENTITY_BRANCHES, + .num_branches = 2, +}; + +static void* osrs_inferno_entity_encoder_create_weights(void* self) { + return osrs_entity_encoder_create_weights( + self, &OSRS_INFERNO_ENTITY_DESCRIPTOR); +} + +static void create_osrs_inferno_encoder(Encoder* encoder) { + osrs_entity_encoder_configure( + encoder, + osrs_inferno_entity_encoder_create_weights, + &OSRS_INFERNO_ENTITY_DESCRIPTOR); } diff --git a/ocean/osrs_inferno/osrs_inferno.h b/ocean/osrs_inferno/osrs_inferno.h index 3cdb523274..e7431bf3d1 100644 --- a/ocean/osrs_inferno/osrs_inferno.h +++ b/ocean/osrs_inferno/osrs_inferno.h @@ -250,11 +250,10 @@ static inline void inferno_env_write_obs(Env* env) { } static inline void inferno_env_write_mask(Env* env) { - float mask_f[INF_ACTION_MASK_SIZE]; - ENCOUNTER_INFERNO.write_mask(INF_ENV_STATE(env), INF_ENV_CONTEXT(env), mask_f); - unsigned char* mask = env->agents[0].action_mask; + float mask[INF_ACTION_MASK_SIZE]; + inf_write_mask_ctx(INF_ENV_STATE(env), INF_ENV_CONTEXT(env), mask); for (int i = 0; i < INF_ACTION_MASK_SIZE; i++) - mask[i] = mask_f[i] > 0.0f ? 1u : 0u; + env->agents[0].action_mask[i] = mask[i] != 0.0f; } void puf_reset(Env* env) { diff --git a/ocean/osrs_pvp/osrs_pvp.c b/ocean/osrs_pvp/osrs_pvp.c new file mode 100644 index 0000000000..c96c2457b9 --- /dev/null +++ b/ocean/osrs_pvp/osrs_pvp.c @@ -0,0 +1,3 @@ +#define OSRS_VISUAL +#define OSRS_VISUAL_DEFAULT_ENCOUNTER "nh_pvp" +#include "../osrs/osrs_visual.c" diff --git a/ocean/osrs_pvp/osrs_pvp.h b/ocean/osrs_pvp/osrs_pvp.h new file mode 100644 index 0000000000..d034caa47b --- /dev/null +++ b/ocean/osrs_pvp/osrs_pvp.h @@ -0,0 +1,220 @@ +#pragma once + +#include +#include +#include + +#include "pufferenv.h" + +#define Log OsrsSharedLog +#include "../osrs/encounters/encounter_nh_pvp.h" +#undef Log + +#define OBS_SIZE NH_PVP_NUM_OBS +#define NUM_ATNS OSRS_BASE_NUM_ACTION_HEADS +#define ACT_SIZES NH_PVP_ACTION_DIMS_INIT +typedef float obs_t; + +#define NH_PVP_ENV_STATE(env) ((EncounterState*)&(env)->state) +#define NH_PVP_ENV_CONTEXT(env) ((EncounterContext*)&(env)->context) + +struct Log { + float episode_return; + float episode_length; + float wins; + float damage_dealt; + float damage_received; + float prayer_correct; + float prayer_total; + float food_remaining; + float karambwan_remaining; + float brews_remaining; + float spec_energy_remaining; + float attacks_landed; + float off_prayer_hits; + float n; +}; + +struct Env { + Log log; + int num_agents; + unsigned int rng; + Agent agents[1]; + int tag; + int boundary_reached; + + NhPvpState state; + NhPvpContext context; + int acts_staging[OSRS_BASE_NUM_ACTION_HEADS]; +}; + +static inline uint32_t nh_pvp_lowbias32(uint32_t x) { + x ^= x >> 16; + x *= 0x7feb352dU; + x ^= x >> 15; + x *= 0x846ca68bU; + x ^= x >> 16; + return x; +} + +static void nh_pvp_write_native_action_mask(Env* env, unsigned char* mask_out) { + if (!mask_out) return; + pvp_write_action_mask_bytes( + mask_out, &env->state.env, 0, env->context.route_topology); +} + +static void nh_pvp_apply_shaping(Env* env, int enabled, float scale) { + RewardShapingConfig* shaping = &env->state.env.shaping; + shaping->enabled = enabled; + shaping->shaping_scale = scale; + shaping->damage_dealt_coef = 0.005f; + shaping->damage_received_coef = -0.005f; + shaping->correct_prayer_bonus = 0.03f; + shaping->wrong_prayer_penalty = -0.02f; + shaping->prayer_switch_no_attack_penalty = -0.01f; + shaping->off_prayer_hit_bonus = 0.03f; + shaping->melee_frozen_penalty = -0.05f; + shaping->wasted_eat_penalty = -0.001f; + shaping->premature_eat_penalty = -0.02f; + shaping->magic_no_staff_penalty = -0.05f; + shaping->gear_mismatch_penalty = -0.05f; + shaping->spec_off_prayer_bonus = 0.02f; + shaping->spec_low_defence_bonus = 0.01f; + shaping->spec_low_hp_bonus = 0.02f; + shaping->smart_triple_eat_bonus = 0.05f; + shaping->wasted_triple_eat_penalty = -0.0005f; + shaping->damage_burst_bonus = 0.002f; + shaping->damage_burst_threshold = 30; + shaping->premature_eat_threshold = 0.7071f; + shaping->ko_bonus = 0.15f; + shaping->wasted_resources_penalty = -0.07f; + shaping->prayer_penalty_enabled = 1; + shaping->click_penalty_enabled = 0; + shaping->click_penalty_threshold = 5; + shaping->click_penalty_coef = -0.003f; +} + +void puf_init(Env* env, Dict* kwargs) { + env->num_agents = 1; + env->agents[0].policy = 0; + ENCOUNTER_NH_PVP.init_context(NH_PVP_ENV_CONTEXT(env)); + ENCOUNTER_NH_PVP.init_state(NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env)); + + uint32_t seed_offset = 0; + const char* seed_offset_str = getenv("PUFFER_ENV_SEED_OFFSET"); + if (seed_offset_str) { + seed_offset = (uint32_t)strtoul(seed_offset_str, NULL, 10); + } + uint32_t env_seed = nh_pvp_lowbias32(env->rng + seed_offset); + if (env_seed == 0) env_seed = 1; + + static const char* const int_keys[] = { + "opponent_type", + "gear_tier", + "is_lms", + "use_c_opponent", + "auto_reset", + }; + for (size_t k = 0; k < sizeof(int_keys) / sizeof(*int_keys); k++) { + ENCOUNTER_NH_PVP.put_int( + NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env), + int_keys[k], (int)dict_get(kwargs, int_keys[k])); + } + ENCOUNTER_NH_PVP.put_int( + NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env), "seed", (int)env_seed); + nh_pvp_apply_shaping( + env, + (int)dict_get(kwargs, "shaping_enabled"), + (float)dict_get(kwargs, "shaping_scale")); + + memset(&env->log, 0, sizeof(env->log)); + ENCOUNTER_NH_PVP.finalize_context( + NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env)); +} + +void puf_reset(Env* env) { + Agent* agent = &env->agents[0]; + ENCOUNTER_NH_PVP.reset(NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env), 0); + ENCOUNTER_NH_PVP.write_obs( + NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env), + (float*)agent->observations); + nh_pvp_write_native_action_mask(env, agent->action_mask); + agent->rewards[0] = 0.0f; + agent->terminals[0] = 0.0f; +} + +void puf_step(Env* env) { + Agent* agent = &env->agents[0]; + for (int i = 0; i < NUM_ATNS; i++) { + env->acts_staging[i] = (int)agent->actions[i]; + } + + ENCOUNTER_NH_PVP.step( + NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env), env->acts_staging); + ENCOUNTER_NH_PVP.write_obs( + NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env), + (float*)agent->observations); + nh_pvp_write_native_action_mask(env, agent->action_mask); + + agent->rewards[0] = ENCOUNTER_NH_PVP.get_reward( + NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env)); + int is_terminal = ENCOUNTER_NH_PVP.is_terminal( + NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env)); + agent->terminals[0] = (float)is_terminal; + + if (is_terminal) { + OsrsSharedLog* episode = &env->state.env.log; + env->log.episode_return += episode->episode_return; + env->log.episode_length += episode->episode_length; + env->log.wins += episode->wins; + env->log.damage_dealt += episode->damage_dealt; + env->log.damage_received += episode->damage_received; + env->log.prayer_correct += episode->prayer_correct; + env->log.prayer_total += episode->prayer_total; + env->log.food_remaining += episode->food_remaining; + env->log.karambwan_remaining += episode->karambwan_remaining; + env->log.brews_remaining += episode->brews_remaining; + env->log.spec_energy_remaining += episode->spec_energy_remaining; + env->log.attacks_landed += episode->attacks_landed; + env->log.off_prayer_hits += episode->off_prayer_hits; + env->log.n += episode->n; + memset(episode, 0, sizeof(*episode)); + + ENCOUNTER_NH_PVP.reset( + NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env), 0); + ENCOUNTER_NH_PVP.write_obs( + NH_PVP_ENV_STATE(env), NH_PVP_ENV_CONTEXT(env), + (float*)agent->observations); + nh_pvp_write_native_action_mask(env, agent->action_mask); + } +} + +void puf_render(Env* env) { + (void)env; +} + +void puf_close(Env* env) { + pvp_close(&env->state.env); + ENCOUNTER_NH_PVP.destroy_context(NH_PVP_ENV_CONTEXT(env)); +} + +void puf_log(Log* log, Dict* out) { + dict_set(out, "episode_return", log->episode_return); + dict_set(out, "episode_length", log->episode_length); + dict_set(out, "wins", log->wins); + dict_set(out, "damage_dealt", log->damage_dealt); + dict_set(out, "damage_received", log->damage_received); + dict_set(out, "prayer_correct_rate", + log->prayer_total > 0.0f ? log->prayer_correct / log->prayer_total : 0.0f); + dict_set(out, "food_remaining", log->food_remaining); + dict_set(out, "karambwan_remaining", log->karambwan_remaining); + dict_set(out, "brews_remaining", log->brews_remaining); + dict_set(out, "spec_remaining", log->spec_energy_remaining); + dict_set(out, "attacks_landed", log->attacks_landed); + dict_set(out, "off_prayer_hits", log->off_prayer_hits); + dict_set(out, "damage_per_hit", + log->attacks_landed > 0.0f ? log->damage_dealt / log->attacks_landed : 0.0f); + float damage_fraction = log->damage_dealt / 99.0f; + dict_set(out, "score", + log->wins + (1.0f - log->wins) * damage_fraction * 0.5f); +} diff --git a/ocean/osrs_zulrah/osrs_zulrah.c b/ocean/osrs_zulrah/osrs_zulrah.c new file mode 100644 index 0000000000..71bf68737e --- /dev/null +++ b/ocean/osrs_zulrah/osrs_zulrah.c @@ -0,0 +1,3 @@ +#define OSRS_VISUAL +#define OSRS_VISUAL_DEFAULT_ENCOUNTER "zulrah" +#include "../osrs/osrs_visual.c" diff --git a/ocean/osrs_zulrah/osrs_zulrah.h b/ocean/osrs_zulrah/osrs_zulrah.h new file mode 100644 index 0000000000..d88612d831 --- /dev/null +++ b/ocean/osrs_zulrah/osrs_zulrah.h @@ -0,0 +1,202 @@ +#pragma once + +#include +#include +#include +#include + +#include "pufferenv.h" + +#define Log OsrsSharedLog +#include "../osrs/encounters/encounter_zulrah.h" +#undef Log + +#define OBS_SIZE ZUL_NUM_OBS +#define NUM_ATNS ZUL_NUM_ACTION_HEADS +#define ACT_SIZES ZUL_ACTION_DIMS_INIT +typedef float obs_t; + +#define ZUL_ENV_STATE(env) ((EncounterState*)&(env)->state) +#define ZUL_ENV_CONTEXT(env) ((EncounterContext*)&(env)->context) + +struct Log { + float episode_return; + float episode_length; + float wins; + float kills; + float score; + float damage_dealt; + float damage_received; + float cloud_occupancy_frac; + float cloud_damage_received; + float tier_n[ZUL_NUM_GEAR_TIERS]; + float tier_wins[ZUL_NUM_GEAR_TIERS]; + float tier_score[ZUL_NUM_GEAR_TIERS]; + float n; +}; + +struct Env { + Log log; + int num_agents; + unsigned int rng; + Agent agents[1]; + int tag; + int boundary_reached; + + ZulrahState state; + ZulrahContext context; + int acts_staging[ZUL_NUM_ACTION_HEADS]; +}; + +static inline uint32_t zul_lowbias32(uint32_t x) { + x ^= x >> 16; + x *= 0x7feb352dU; + x ^= x >> 15; + x *= 0x846ca68bU; + x ^= x >> 16; + return x; +} + +static void zul_write_native_action_mask(Env* env, unsigned char* mask_out) { + if (!mask_out) return; + zul_write_mask_bytes( + ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env), mask_out); +} + +void puf_init(Env* env, Dict* kwargs) { + env->num_agents = 1; + env->agents[0].policy = 0; + ENCOUNTER_ZULRAH.init_context(ZUL_ENV_CONTEXT(env)); + ENCOUNTER_ZULRAH.init_state(ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env)); + + uint32_t seed_offset = 0; + const char* seed_offset_str = getenv("PUFFER_ENV_SEED_OFFSET"); + if (seed_offset_str) { + seed_offset = (uint32_t)strtoul(seed_offset_str, NULL, 10); + } + uint32_t env_seed = zul_lowbias32(env->rng + seed_offset); + if (env_seed == 0) { + env_seed = 1; + } + env->state.rng_state = env_seed; + + memset(&env->log, 0, sizeof(Log)); + + static const char* const int_keys[] = { + "gear_tier", + "gear_tier_mode", + "episode_mode", + }; + for (size_t k = 0; k < sizeof(int_keys) / sizeof(*int_keys); k++) { + ENCOUNTER_ZULRAH.put_int( + ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env), + int_keys[k], (int)dict_get(kwargs, int_keys[k])); + } + + static const char* const float_keys[] = { + "gear_tier_weight_0", + "gear_tier_weight_1", + "gear_tier_weight_2", + "reward_win", + "reward_loss_penalty", + "reward_damage_dealt", + "reward_correct_style", + "reward_damage_received_penalty", + "reward_cloud_occupancy_penalty", + }; + for (size_t k = 0; k < sizeof(float_keys) / sizeof(*float_keys); k++) { + ENCOUNTER_ZULRAH.put_float( + ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env), + float_keys[k], (float)dict_get(kwargs, float_keys[k])); + } + + ENCOUNTER_ZULRAH.finalize_context(ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env)); +} + +void puf_reset(Env* env) { + Agent* agent = &env->agents[0]; + ENCOUNTER_ZULRAH.reset(ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env), 0); + ENCOUNTER_ZULRAH.write_obs( + ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env), (float*)agent->observations); + zul_write_native_action_mask(env, agent->action_mask); + agent->rewards[0] = 0.0f; + agent->terminals[0] = 0.0f; +} + +void puf_step(Env* env) { + Agent* agent = &env->agents[0]; + for (int i = 0; i < NUM_ATNS; i++) { + env->acts_staging[i] = (int)agent->actions[i]; + } + + ENCOUNTER_ZULRAH.step(ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env), env->acts_staging); + + float* obs = (float*)agent->observations; + ENCOUNTER_ZULRAH.write_obs(ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env), obs); + zul_write_native_action_mask(env, agent->action_mask); + + agent->rewards[0] = ENCOUNTER_ZULRAH.get_reward(ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env)); + int is_terminal = ENCOUNTER_ZULRAH.is_terminal(ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env)); + agent->terminals[0] = (float)is_terminal; + + if (is_terminal) { + ZulrahState* s = &env->state; + ZulEpisodeOutcome outcome = zul_episode_outcome(s); + int tier = s->gear_tier; + if (tier < 0 || tier >= ZUL_NUM_GEAR_TIERS) { + fprintf(stderr, "zulrah invalid sampled gear tier %d\n", tier); + abort(); + } + env->log.episode_return += s->episode_return; + env->log.episode_length += (float)s->tick; + env->log.wins += outcome.win; + env->log.kills += (float)s->kills_this_episode; + env->log.score += outcome.score; + env->log.damage_dealt += s->total_damage_dealt; + env->log.damage_received += s->total_damage_received; + env->log.cloud_occupancy_frac += s->tick > 0 + ? (float)s->total_cloud_occupancy_ticks / (float)s->tick + : 0.0f; + env->log.cloud_damage_received += s->total_cloud_damage_received; + env->log.tier_n[tier] += 1.0f; + env->log.tier_wins[tier] += outcome.win; + env->log.tier_score[tier] += outcome.score; + env->log.n += 1.0f; + + ENCOUNTER_ZULRAH.reset(ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env), 0); + ENCOUNTER_ZULRAH.write_obs(ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env), obs); + zul_write_native_action_mask(env, agent->action_mask); + } +} + +void puf_render(Env* env) { + (void)env; +} + +void puf_close(Env* env) { + ENCOUNTER_ZULRAH.destroy_context(ZUL_ENV_CONTEXT(env)); +} + +void puf_log(Log* log, Dict* out) { + dict_set(out, "episode_return", log->episode_return); + dict_set(out, "episode_length", log->episode_length); + dict_set(out, "wins", log->wins); + dict_set(out, "kills", log->kills); + dict_set(out, "score", log->score); + dict_set(out, "damage_dealt", log->damage_dealt); + dict_set(out, "damage_received", log->damage_received); + dict_set(out, "cloud_occupancy_frac", log->cloud_occupancy_frac); + dict_set(out, "cloud_damage_received", log->cloud_damage_received); + + static const char* const TIER_WIN_KEYS[ZUL_NUM_GEAR_TIERS] = { + "tier0_win_rate", "tier1_win_rate", "tier2_win_rate"}; + static const char* const TIER_SCORE_KEYS[ZUL_NUM_GEAR_TIERS] = { + "tier0_score", "tier1_score", "tier2_score"}; + for (int t = 0; t < ZUL_NUM_GEAR_TIERS; t++) { + float tn = log->tier_n[t]; + dict_set(out, TIER_WIN_KEYS[t], + tn > 0.0f ? log->tier_wins[t] / tn : 0.0f); + dict_set(out, TIER_SCORE_KEYS[t], + tn > 0.0f ? log->tier_score[t] / tn : 0.0f); + } +} diff --git a/src/ocean.cu b/src/ocean.cu index bbc8e1833e..83de9acd6b 100644 --- a/src/ocean.cu +++ b/src/ocean.cu @@ -25,6 +25,7 @@ __device__ static const float OSRS_ITEM_OBS_TABLE_DEV [OSRS_ITEM_OBS_TABLE_ROWS][OSRS_ITEM_OBS_TABLE_COLS] = { #include "../ocean/osrs/osrs_item_obs_table.inc" }; +#include "../ocean/osrs/osrs_entity_encoder.cu" #include "../ocean/osrs_colosseum/osrs_colosseum.cu" #include "../ocean/osrs_inferno/osrs_inferno.cu" #ifdef PUFFER_NETHACK @@ -55,6 +56,14 @@ static void create_custom_encoder(const char* env_name, Encoder* enc) { create_osrs_inferno_encoder(enc); return; } + if (strcmp(env_name, "osrs_zulrah") == 0) { + create_osrs_zulrah_encoder(enc); + return; + } + if (strcmp(env_name, "osrs_pvp") == 0) { + create_osrs_pvp_encoder(enc); + return; + } } static void create_custom_decoder(const char* env_name, Decoder* dec) { diff --git a/tests/test_osrs_entity_encoders.py b/tests/test_osrs_entity_encoders.py index 93cbcc657c..c880514029 100644 --- a/tests/test_osrs_entity_encoders.py +++ b/tests/test_osrs_entity_encoders.py @@ -12,7 +12,6 @@ ROOT = Path(__file__).resolve().parents[1] CUDA_SOURCE = Path(__file__).with_name("test_osrs_entity_encoders_cuda.cu") ITEM_HEADER = ROOT / "ocean/osrs/osrs_item_obs_generated.h" -ITEM_TABLE = ROOT / "ocean/osrs/osrs_item_obs_table.inc" BATCH = 9 HIDDEN = 40 BOTTLENECK = 16 @@ -22,8 +21,6 @@ class EncoderContract: name: str kind: int - source: Path - prefix: str obs_size: int npc_start: int npc_count: int @@ -31,30 +28,55 @@ class EncoderContract: npc_features: int type_count: int type_code_scale: int + equipment_start: int + equipment_count: int + equipment_obs_features: int + equipment_features: int inventory_start: int inventory_count: int - inventory_obs_features: int inventory_features: int - inventory_overlays: bool CONTRACTS = ( EncoderContract( - "colosseum", 0, ROOT / "ocean/osrs_colosseum/osrs_colosseum.cu", "COLO_ENT", - 934, 130, 24, 23, 34, 12, 1, 36, 28, 3, 15, True, + name="colosseum", + kind=0, + obs_size=904, + npc_start=101, + npc_count=24, + npc_obs_features=23, + npc_features=34, + type_count=12, + type_code_scale=1, + equipment_start=80, + equipment_count=11, + equipment_obs_features=1, + equipment_features=14, + inventory_start=52, + inventory_count=28, + inventory_features=14, ), EncoderContract( - "inferno", 1, ROOT / "ocean/osrs_inferno/osrs_inferno.cu", "INF_ENT", - 498, 54, 14, 13, 26, 14, 16, 460, 28, 1, 15, False, + name="inferno", + kind=1, + obs_size=530, + npc_start=124, + npc_count=14, + npc_obs_features=13, + npc_features=26, + type_count=14, + type_code_scale=16, + equipment_start=80, + equipment_count=11, + equipment_obs_features=1, + equipment_features=14, + inventory_start=52, + inventory_count=28, + inventory_features=14, ), ) -def _integer_constant(source: str, name: str): - match = re.search(rf"\b{name}\s*=\s*(\d+)\s*;", source) - return int(match.group(1)) if match else None - - def _generated_integer(name: str) -> int: source = ITEM_HEADER.read_text() match = re.search(rf"^#define\s+{name}\s+(\d+)\s*$", source, re.MULTILINE) @@ -64,179 +86,254 @@ def _generated_integer(name: str) -> int: def _load_item_table() -> np.ndarray: columns = _generated_integer("OSRS_ITEM_OBS_TABLE_COLS") + expected_rows = _generated_integer("OSRS_ITEM_OBS_TABLE_ROWS") rows = [] - for line in ITEM_TABLE.read_text().splitlines(): - if not line.startswith("{"): + for line in ITEM_HEADER.read_text().splitlines(): + if not line.lstrip().startswith("X("): continue - values = re.findall(r"-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?", line, re.IGNORECASE) - rows.append([float(value) for value in values]) + values = re.findall( + r"-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?", + line, + re.IGNORECASE, + ) + rows.append([float(value) for value in values[-columns:]]) + if len(rows) == expected_rows: + break table = np.asarray(rows, dtype=np.float32) - assert table.shape == (_generated_integer("OSRS_ITEM_OBS_TABLE_ROWS"), columns) + assert table.shape == (expected_rows, columns) return table -def _source_contract(contract: EncoderContract) -> dict[str, int | None]: - source = contract.source.read_text() - prefix = contract.prefix - values = { - "npc_start": _integer_constant(source, f"{prefix}_NPC_START"), - "npc_count": _integer_constant(source, f"{prefix}_NUM_NPCS"), - "npc_features": _integer_constant(source, f"{prefix}_FEATS"), - "type_count": _integer_constant(source, f"{prefix}_TYPE_ONEHOT"), - "inventory_obs_features": _integer_constant(source, f"{prefix}_INV_OBS_FEATS"), - "inventory_start": _integer_constant(source, f"{prefix}_INV_START"), - "inventory_count": _integer_constant(source, f"{prefix}_INV_NUM_CELLS"), - "inventory_features": _integer_constant(source, f"{prefix}_INV_FEATS"), - } - if contract.kind == 1: - values["obs_size"] = _integer_constant(source, "INF_ENT_OBS_SIZE") - values["npc_obs_features"] = _integer_constant( - source, "INF_ENT_OBS_FEATS", - ) - values["type_code_scale"] = _integer_constant( - source, "INF_ENT_TYPE_CODE_SCALE", - ) - return values - - -def _expected_source_contract(contract: EncoderContract) -> dict[str, int]: - values = { - "npc_start": contract.npc_start, - "npc_count": contract.npc_count, - "npc_features": contract.npc_features, - "type_count": contract.type_count, - "inventory_start": contract.inventory_start, - "inventory_count": contract.inventory_count, - "inventory_obs_features": contract.inventory_obs_features, - "inventory_features": contract.inventory_features, - } - if contract.kind == 1: - values["npc_obs_features"] = contract.npc_obs_features - values["obs_size"] = contract.obs_size - values["type_code_scale"] = contract.type_code_scale - return values - - -def _make_observations(contract: EncoderContract, table_rows: int) -> np.ndarray: - linear = np.arange(BATCH * contract.obs_size, dtype=np.float32).reshape(BATCH, contract.obs_size) +def _make_observations( + contract: EncoderContract, + table_rows: int, +) -> np.ndarray: + linear = np.arange( + BATCH * contract.obs_size, + dtype=np.float32, + ).reshape(BATCH, contract.obs_size) observations = np.sin(linear * np.float32(0.013)) * np.float32(0.01) npc_block = contract.npc_count * contract.npc_obs_features - observations[:, contract.npc_start:contract.npc_start + npc_block] = 0 - npc_cells = observations[:, contract.npc_start:contract.npc_start + npc_block].reshape( - BATCH, contract.npc_count, contract.npc_obs_features, - ) + observations[:, contract.npc_start : contract.npc_start + npc_block] = 0 + npc_cells = observations[ + :, + contract.npc_start : contract.npc_start + npc_block, + ].reshape(BATCH, contract.npc_count, contract.npc_obs_features) for npc_type in range(contract.type_count): flat_index = npc_type + 1 batch, cell = divmod(flat_index, contract.npc_count) npc_cells[batch, cell, 0] = (npc_type + 1) / contract.type_code_scale npc_cells[batch, cell, 1:] = np.linspace( - 0.01 * (npc_type + 1), 0.01 * (npc_type + contract.npc_obs_features - 1), - contract.npc_obs_features - 1, dtype=np.float32, + 0.01 * (npc_type + 1), + 0.01 * (npc_type + contract.npc_obs_features - 1), + contract.npc_obs_features - 1, + dtype=np.float32, ) - inventory_block = contract.inventory_count * contract.inventory_obs_features - observations[:, contract.inventory_start:contract.inventory_start + inventory_block] = 0 - inventory_cells = observations[ - :, contract.inventory_start:contract.inventory_start + inventory_block - ].reshape(BATCH, contract.inventory_count, contract.inventory_obs_features) code_scale = _generated_integer("OSRS_ITEM_OBS_CODE_SCALE") - assert table_rows <= inventory_cells.shape[0] * inventory_cells.shape[1] + inventory_block = contract.inventory_count + observations[ + :, + contract.inventory_start : contract.inventory_start + inventory_block, + ] = 0 + inventory_cells = observations[ + :, + contract.inventory_start : contract.inventory_start + inventory_block, + ].reshape(BATCH, contract.inventory_count) + assert table_rows <= inventory_cells.size for code in range(table_rows): batch, cell = divmod(code, contract.inventory_count) - inventory_cells[batch, cell, 0] = code / code_scale - if contract.inventory_overlays and code: - inventory_cells[batch, cell, 1] = float(code & 1) - inventory_cells[batch, cell, 2] = (code % 17) / 20.0 + inventory_cells[batch, cell] = code / code_scale + + equipment_block = contract.equipment_count * contract.equipment_obs_features + equipment_cells = observations[ + :, + contract.equipment_start : contract.equipment_start + equipment_block, + ].reshape( + BATCH, + contract.equipment_count, + contract.equipment_obs_features, + ) + for batch in range(BATCH): + for cell in range(contract.equipment_count): + code = 1 + (batch * contract.equipment_count + cell) % (table_rows - 1) + equipment_cells[batch, cell, 0] = code / code_scale return observations -def _materialize(contract: EncoderContract, observations: np.ndarray, table: np.ndarray): +def _materialize_items( + observations: np.ndarray, + start: int, + count: int, + table: np.ndarray, +): + source = observations[:, start : start + count].reshape(BATCH, count) + code_scale = _generated_integer("OSRS_ITEM_OBS_CODE_SCALE") + codes = np.rint(source * code_scale).astype(np.int64) + assert np.all((codes >= 0) & (codes < table.shape[0])) + return table[codes].copy(), codes + + +def _materialize( + contract: EncoderContract, + observations: np.ndarray, + table: np.ndarray, +): npc_source = observations[ - :, contract.npc_start: - contract.npc_start + contract.npc_count * contract.npc_obs_features + :, + contract.npc_start : contract.npc_start + + contract.npc_count * contract.npc_obs_features, ].reshape(BATCH, contract.npc_count, contract.npc_obs_features) npc_records = np.zeros( - (BATCH, contract.npc_count, contract.npc_features), dtype=np.float32, + (BATCH, contract.npc_count, contract.npc_features), + dtype=np.float32, ) - npc_codes = np.rint(npc_source[:, :, 0] * contract.type_code_scale).astype(np.int64) + npc_codes = np.rint( + npc_source[:, :, 0] * contract.type_code_scale, + ).astype(np.int64) for npc_type in range(contract.type_count): npc_records[:, :, npc_type] = npc_codes == npc_type + 1 - npc_records[:, :, contract.type_count:] = npc_source[:, :, 1:] + npc_records[:, :, contract.type_count :] = npc_source[:, :, 1:] - inventory_source = observations[ - :, contract.inventory_start: - contract.inventory_start + contract.inventory_count * contract.inventory_obs_features - ].reshape(BATCH, contract.inventory_count, contract.inventory_obs_features) - code_scale = _generated_integer("OSRS_ITEM_OBS_CODE_SCALE") - inventory_codes = np.rint(inventory_source[:, :, 0] * code_scale).astype(np.int64) - assert np.all((inventory_codes >= 0) & (inventory_codes < table.shape[0])) - inventory_records = table[inventory_codes].copy() - if contract.inventory_overlays: - equipped = _generated_integer("OSRS_ITEM_OBS_OVERLAY_EQUIPPED") - hp_heal = _generated_integer("OSRS_ITEM_OBS_OVERLAY_HP_HEAL") - inventory_records[:, :, equipped] = inventory_source[:, :, 1] - gear = (inventory_records[:, :, 3] != 0) | (inventory_records[:, :, 4] != 0) - inventory_records[:, :, hp_heal] = np.where( - gear, inventory_records[:, :, hp_heal], inventory_source[:, :, 2], - ) - return npc_records, inventory_records, npc_codes, inventory_codes + inventory_records, inventory_codes = _materialize_items( + observations, + contract.inventory_start, + contract.inventory_count, + table, + ) + equipment_records, equipment_codes = _materialize_items( + observations, + contract.equipment_start, + contract.equipment_count, + table, + ) + return ( + npc_records, + inventory_records, + equipment_records, + npc_codes, + inventory_codes, + equipment_codes, + ) @pytest.mark.parametrize("contract", CONTRACTS, ids=lambda contract: contract.name) def test_cpu_materialization_contract(contract): - table = _load_item_table() observations = _make_observations(contract, table.shape[0]) - npc_records, inventory_records, npc_codes, inventory_codes = _materialize( - contract, observations, table, + ( + npc_records, + inventory_records, + equipment_records, + npc_codes, + inventory_codes, + equipment_codes, + ) = _materialize(contract, observations, table) + assert npc_records.shape == ( + BATCH, + contract.npc_count, + contract.npc_features, + ) + assert inventory_records.shape == ( + BATCH, + contract.inventory_count, + contract.inventory_features, + ) + assert equipment_records.shape == ( + BATCH, + contract.equipment_count, + contract.equipment_features, ) - assert npc_records.shape == (BATCH, contract.npc_count, contract.npc_features) - assert inventory_records.shape == (BATCH, contract.inventory_count, contract.inventory_features) assert set(npc_codes.ravel()) == set(range(contract.type_count + 1)) assert set(inventory_codes.ravel()) == set(range(table.shape[0])) + assert np.all(equipment_codes > 0) assert np.count_nonzero(npc_records[npc_codes == 0]) == 0 assert np.count_nonzero(inventory_records[inventory_codes == 0]) == 0 - assert BATCH * contract.npc_count * contract.npc_features > 256 - assert BATCH * contract.npc_count * contract.npc_features % 256 != 0 - assert BATCH * contract.inventory_count * contract.inventory_features > 256 - assert BATCH * contract.inventory_count * contract.inventory_features % 256 != 0 - actual = _source_contract(contract) - expected = _expected_source_contract(contract) - stale = {name: (actual[name], value) for name, value in expected.items() if actual[name] != value} - assert not stale, f"{contract.name} entity encoder contract is stale: {stale}" - - -def _torch_reference(torch, functional, contract, observations, table, weights): - npc_records, inventory_records, _, _ = _materialize( - contract, observations.detach().cpu().numpy(), table, + materialized_sizes = ( + npc_records.size, + inventory_records.size, + equipment_records.size, ) + assert all(size > 256 for size in materialized_sizes) + assert all(size % 256 != 0 for size in materialized_sizes) + + +def _torch_reference( + torch, + functional, + contract, + observations, + table, + weights, +): + ( + npc_records, + inventory_records, + equipment_records, + _, + _, + _, + ) = _materialize(contract, observations.detach().cpu().numpy(), table) npc = torch.from_numpy(npc_records).to(observations.device) inventory = torch.from_numpy(inventory_records).to(observations.device) - global_w, entity_l1_w, entity_l2_w, inventory_l1_w, inventory_l2_w = weights + equipment = torch.from_numpy(equipment_records).to(observations.device) + ( + global_w, + inventory_l1_w, + inventory_l2_w, + equipment_l1_w, + equipment_l2_w, + npc_l1_w, + npc_l2_w, + ) = weights def branch(records, l1_weight, l2_weight, active_width): - hidden = functional.gelu(functional.linear(records, l1_weight), approximate="tanh") + hidden = functional.gelu( + functional.linear(records, l1_weight), + approximate="tanh", + ) values = functional.linear(hidden, l2_weight) active = records[:, :, :active_width].sum(dim=2) > 0 - pooled = values.masked_fill(~active.unsqueeze(2), -torch.inf).amax(dim=1) - return torch.where(active.any(dim=1, keepdim=True), pooled, torch.zeros_like(pooled)) + pooled = values.masked_fill( + ~active.unsqueeze(2), + -torch.inf, + ).amax(dim=1) + return torch.where( + active.any(dim=1, keepdim=True), + pooled, + torch.zeros_like(pooled), + ) return ( functional.linear(observations, global_w) - + branch(npc, entity_l1_w, entity_l2_w, contract.type_count) + branch(inventory, inventory_l1_w, inventory_l2_w, 1) + + branch(equipment, equipment_l1_w, equipment_l2_w, 1) + + branch(npc, npc_l1_w, npc_l2_w, contract.type_count) ) def _configure_library(path: Path): library = ctypes.CDLL(path) pointer = ctypes.c_void_p - library.osrs_entity_test_contract.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_int)] - library.osrs_entity_test_init.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int] + library.osrs_entity_test_contract.argtypes = [ + ctypes.c_int, + ctypes.POINTER(ctypes.c_int), + ] + library.osrs_entity_test_init.argtypes = [ + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ] library.osrs_entity_test_init.restype = ctypes.c_int - library.osrs_entity_test_set_weights.argtypes = [pointer] * 5 - library.osrs_entity_test_forward.argtypes = [pointer, pointer, ctypes.c_int, ctypes.c_int] + library.osrs_entity_test_set_weights.argtypes = [pointer] * 7 + library.osrs_entity_test_forward.argtypes = [ + pointer, + pointer, + ctypes.c_int, + ctypes.c_int, + ] library.osrs_entity_test_backward.argtypes = [pointer, ctypes.c_int, ctypes.c_int] library.osrs_entity_test_get_grad.argtypes = [ctypes.c_int, pointer] return library @@ -250,16 +347,38 @@ def cuda_library(tmp_path_factory): raylib_include = next(ROOT.glob("raylib-*/include")) raylib_archive = raylib_include.parent / "lib/libraylib.a" pytest.importorskip("torch") - library_path = tmp_path_factory.mktemp("osrs_entity_encoder") / "osrs_entity_encoder.so" + library_path = ( + tmp_path_factory.mktemp("osrs_entity_encoder") / "osrs_entity_encoder.so" + ) subprocess.run( [ - nvcc, "-shared", "-o", str(library_path), str(CUDA_SOURCE), - "-I", str(ROOT / "src"), "-I", str(raylib_include), - "-Xlinker", "--no-as-needed", - "-lcublas", "-lcudnn", "-lcurand", "-lnccl", "-lnvidia-ml", "-lcusolver", + nvcc, + "-shared", + "-o", + str(library_path), + str(CUDA_SOURCE), + "-I", + str(ROOT / "src"), + "-I", + str(raylib_include), + "-Xlinker", + "--no-as-needed", + "-lcublas", + "-lcudnn", + "-lcurand", + "-lnccl", + "-lnvidia-ml", + "-lcusolver", str(raylib_archive), - "-lGL", "-lm", "-lpthread", "-ldl", "-lrt", - "--compiler-options", "-fPIC", "-Xcompiler", "-O2", + "-lGL", + "-lm", + "-lpthread", + "-ldl", + "-lrt", + "--compiler-options", + "-fPIC", + "-Xcompiler", + "-O2", ], cwd=ROOT, check=True, @@ -278,16 +397,33 @@ def test_cuda_forward_and_all_weight_gradients(cuda_library, contract): if not torch.cuda.is_available(): pytest.skip("CUDA is not available") - reported = (ctypes.c_int * 8)() + reported = (ctypes.c_int * 14)() cuda_library.osrs_entity_test_contract(contract.kind, reported) assert list(reported) == [ - contract.obs_size, contract.npc_start, contract.npc_count, contract.npc_features, - contract.type_count, contract.inventory_start, contract.inventory_count, + contract.obs_size, + contract.npc_start, + contract.npc_count, + contract.npc_obs_features, + contract.npc_features, + contract.type_count, + contract.type_code_scale, + contract.equipment_start, + contract.equipment_count, + contract.equipment_obs_features, + contract.equipment_features, + contract.inventory_start, + contract.inventory_count, contract.inventory_features, ] - assert cuda_library.osrs_entity_test_init( - contract.kind, BATCH, contract.obs_size, HIDDEN, - ) == 0 + assert ( + cuda_library.osrs_entity_test_init( + contract.kind, + BATCH, + contract.obs_size, + HIDDEN, + ) + == 0 + ) table = _load_item_table() observations_np = _make_observations(contract, table.shape[0]) @@ -295,10 +431,12 @@ def test_cuda_forward_and_all_weight_gradients(cuda_library, contract): generator = torch.Generator(device="cuda").manual_seed(6100 + contract.kind) shapes = ( (HIDDEN, contract.obs_size), - (BOTTLENECK, contract.npc_features), - (HIDDEN, BOTTLENECK), (BOTTLENECK, contract.inventory_features), (HIDDEN, BOTTLENECK), + (BOTTLENECK, contract.equipment_features), + (HIDDEN, BOTTLENECK), + (BOTTLENECK, contract.npc_features), + (HIDDEN, BOTTLENECK), ) weights = [ (torch.randn(shape, generator=generator, device="cuda") * 0.05).requires_grad_() @@ -308,15 +446,25 @@ def test_cuda_forward_and_all_weight_gradients(cuda_library, contract): cuda_output = torch.empty(BATCH, HIDDEN, device="cuda") cuda_library.osrs_entity_test_forward( - _pointer(cuda_output), _pointer(observations), BATCH, contract.obs_size, + _pointer(cuda_output), + _pointer(observations), + BATCH, + contract.obs_size, ) reference_output = _torch_reference( - torch, functional, contract, observations, table, weights, + torch, + functional, + contract, + observations, + table, + weights, ) torch.testing.assert_close(cuda_output, reference_output, atol=3e-4, rtol=3e-4) output_gradient = torch.randn( - (BATCH, HIDDEN), generator=generator, device="cuda", + (BATCH, HIDDEN), + generator=generator, + device="cuda", ) reference_output.backward(output_gradient) cuda_library.osrs_entity_test_backward(_pointer(output_gradient), BATCH, HIDDEN) diff --git a/tests/test_osrs_entity_encoders_cuda.cu b/tests/test_osrs_entity_encoders_cuda.cu index f85b2779e4..1986bde24e 100644 --- a/tests/test_osrs_entity_encoders_cuda.cu +++ b/tests/test_osrs_entity_encoders_cuda.cu @@ -1,18 +1,25 @@ #define PRECISION_FLOAT #define ENV_HEADER "../ocean/minimal/minimal.h" #define PUFFER_ENV_NAME "minimal" +#define NUM_GEAR_SLOTS 11 #include "../src/pufferl.cu" extern "C" { static Encoder test_encoder; -static ColosseumEntityEncoderWeights* test_weights; -static ColosseumEntityEncoderActivations* test_activations; +static OsrsEntityEncoderWeights* test_weights; +static OsrsEntityEncoderActivations* test_activations; static Allocator test_params; static Allocator test_acts; static Allocator test_grads; static bool test_cublas_initialized; +static const OsrsEntityEncoderDescriptor* test_descriptor(int kind) { + return kind == 0 + ? &OSRS_COLOSSEUM_ENTITY_DESCRIPTOR + : &OSRS_INFERNO_ENTITY_DESCRIPTOR; +} + static void test_free_allocator(Allocator* allocator) { if (allocator->mem) cudaFree(allocator->mem); free(allocator->regs); @@ -30,30 +37,28 @@ static void test_reset() { } void osrs_entity_test_contract(int kind, int* values) { - if (kind == 0) { - values[0] = COLO_ENT_OBS_SIZE; - values[1] = COLO_ENT_NPC_START; - values[2] = COLO_ENT_NUM_NPCS; - values[3] = COLO_ENT_FEATS; - values[4] = COLO_ENT_TYPE_ONEHOT; - values[5] = COLO_ENT_INV_START; - values[6] = COLO_ENT_INV_NUM_CELLS; - values[7] = COLO_ENT_INV_FEATS; - return; - } - values[0] = INF_ENT_OBS_SIZE; - values[1] = INF_ENT_NPC_START; - values[2] = INF_ENT_NUM_NPCS; - values[3] = INF_ENT_FEATS; - values[4] = INF_ENT_TYPE_ONEHOT; - values[5] = INF_ENT_INV_START; - values[6] = INF_ENT_INV_NUM_CELLS; - values[7] = INF_ENT_INV_FEATS; + const OsrsEntityEncoderDescriptor* descriptor = test_descriptor(kind); + const OsrsEntityBranchDescriptor* equipment = &descriptor->branches[0]; + const OsrsEntityBranchDescriptor* npc = &descriptor->branches[1]; + values[0] = descriptor->obs_size; + values[1] = npc->obs_start; + values[2] = npc->num_records; + values[3] = npc->obs_features; + values[4] = osrs_entity_branch_features(npc); + values[5] = npc->type_onehot; + values[6] = npc->type_code_scale; + values[7] = equipment->obs_start; + values[8] = equipment->num_records; + values[9] = equipment->obs_features; + values[10] = osrs_entity_branch_features(equipment); + values[11] = OSRS_ENTITY_INV_START; + values[12] = OSRS_ENTITY_INV_NUM_RECORDS; + values[13] = OSRS_ENTITY_INV_FEATURES; } int osrs_entity_test_init(int kind, int batch, int obs_size, int hidden) { - if (kind == 0 && obs_size != COLO_ENT_OBS_SIZE) return 1; - if (kind == 1 && obs_size != INF_ENT_OBS_SIZE) return 1; + const OsrsEntityEncoderDescriptor* descriptor = test_descriptor(kind); + if (obs_size != descriptor->obs_size) return 1; test_reset(); if (!test_cublas_initialized) { cublas_init_handle(); @@ -62,11 +67,11 @@ int osrs_entity_test_init(int kind, int batch, int obs_size, int hidden) { test_encoder = {}; test_encoder.in_dim = obs_size; test_encoder.out_dim = hidden; - create_custom_encoder(kind == 0 ? "osrs_colosseum" : "osrs_inferno", &test_encoder); - test_weights = (ColosseumEntityEncoderWeights*)test_encoder.create_weights(&test_encoder); + create_custom_encoder(descriptor->env_name, &test_encoder); + test_weights = (OsrsEntityEncoderWeights*)test_encoder.create_weights(&test_encoder); test_encoder.reg_params(test_weights, &test_params); alloc_create(&test_params); - test_activations = (ColosseumEntityEncoderActivations*)calloc(1, test_encoder.activation_size); + test_activations = (OsrsEntityEncoderActivations*)calloc(1, test_encoder.activation_size); test_encoder.reg_train(test_weights, test_activations, &test_acts, &test_grads, batch); alloc_create(&test_acts); alloc_create(&test_grads); @@ -74,17 +79,29 @@ int osrs_entity_test_init(int kind, int batch, int obs_size, int hidden) { } void osrs_entity_test_set_weights( - void* global_w, void* entity_l1_w, void* entity_l2_w, - void* inv_l1_w, void* inv_l2_w) { + void* global_w, void* inventory_l1_w, void* inventory_l2_w, + void* equipment_l1_w, void* equipment_l2_w, + void* npc_l1_w, void* npc_l2_w) { + OsrsEntityBranchWeights* branches = osrs_entity_branch_weights(test_weights); Prec* dst[] = { &test_weights->global_w, - &test_weights->entity_l1_w, - &test_weights->entity_l2_w, &test_weights->inv_l1_w, &test_weights->inv_l2_w, + &branches[0].l1_w, + &branches[0].l2_w, + &branches[1].l1_w, + &branches[1].l2_w, + }; + void* src[] = { + global_w, + inventory_l1_w, + inventory_l2_w, + equipment_l1_w, + equipment_l2_w, + npc_l1_w, + npc_l2_w, }; - void* src[] = {global_w, entity_l1_w, entity_l2_w, inv_l1_w, inv_l2_w}; - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 7; i++) { cudaMemcpy(dst[i]->data, src[i], numel(dst[i]->shape) * sizeof(float), cudaMemcpyDeviceToDevice); } @@ -105,12 +122,16 @@ void osrs_entity_test_backward(void* grad, int batch, int hidden) { } void osrs_entity_test_get_grad(int index, void* output) { + OsrsEntityBranchActivations* branches = + osrs_entity_branch_activations(test_activations); Prec* grads[] = { &test_activations->global_wgrad, - &test_activations->entity_l1_wgrad, - &test_activations->entity_l2_wgrad, &test_activations->inv_l1_wgrad, &test_activations->inv_l2_wgrad, + &branches[0].l1_wgrad, + &branches[0].l2_wgrad, + &branches[1].l1_wgrad, + &branches[1].l2_wgrad, }; Prec* grad = grads[index]; cudaMemcpy(output, grad->data, numel(grad->shape) * sizeof(float), From 872207396521f899a053739a6d046d98787c0510 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Sat, 15 Aug 2026 18:53:53 +0300 Subject: [PATCH 40/50] Promote Colosseum sweep winner --- config/osrs_colosseum.ini | 52 +++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/config/osrs_colosseum.ini b/config/osrs_colosseum.ini index 1d2a844a92..400a0f3a47 100644 --- a/config/osrs_colosseum.ini +++ b/config/osrs_colosseum.ini @@ -6,15 +6,15 @@ async = 0 [env] start_wave = 1 -damage_reward_coeff = 0.00404769694 -wave_clear_bonus = 0.10361582 -win_bonus = 0.556438088 -death_penalty_coeff = 0.751913667 -timeout_penalty = 0.195512325 +damage_reward_coeff = 0.0058004423 +wave_clear_bonus = 0.0414097905 +win_bonus = 0.444149673 +death_penalty_coeff = 0.884283066 +timeout_penalty = 0.176490664 farm_safe_damage_cap = 1 farm_cap_waves = 4 boss_damage_reward_coeff = 0.0 -boss_phase_bonus = 0.0865581334 +boss_phase_bonus = 0.242805392 loadout_profile_mode = 2 beginner_loadout_fraction = 0.5 mask_inventory_heads = 0 @@ -29,14 +29,14 @@ damage_scale_anneal_start = 1.0 damage_scale_anneal_ticks = 0 classic_curriculum_mode = 1 curriculum_num_tiers = 4 -curriculum_wave_1 = 11 -curriculum_frac_1 = 0.0574102551 +curriculum_wave_1 = 9 +curriculum_frac_1 = 0.0324260853 curriculum_wave_2 = 5 -curriculum_frac_2 = 0.0 +curriculum_frac_2 = 0.0615721829 curriculum_wave_3 = 12 -curriculum_frac_3 = 0.0668167621 +curriculum_frac_3 = 0.16289182 curriculum_wave_4 = 6 -curriculum_frac_4 = 0.332286835 +curriculum_frac_4 = 0.399915189 curriculum_wave_5 = 6 curriculum_frac_5 = 0.0 @@ -53,23 +53,23 @@ num_layers = 2 expansion_factor = 1 [train] -total_timesteps = 360785504 +total_timesteps = 438300800 horizon = 32 -min_lr_ratio = 0.0598145202 -learning_rate = 0.000741419091 -momentum = 0.984893203 -ent_coef = 2.93842027e-06 -gamma = 0.999876142 -gae_lambda = 0.979838431 +min_lr_ratio = 0.142049164 +learning_rate = 0.000879242609 +momentum = 0.98627466 +ent_coef = 1.41506653e-05 +gamma = 0.999796748 +gae_lambda = 0.985211074 vtrace_rho_clip = 1.65516651 vtrace_c_clip = 0.573781013 -prio_alpha = 0.571603894 -prio_beta0 = 0.00201839209 -clip_coef = 0.0961709544 -vf_coef = 8 -vf_clip_coef = 0.0481410585 -max_grad_norm = 6.59531498 -replay_ratio = 3.97566438 +prio_alpha = 0.625232637 +prio_beta0 = 0 +clip_coef = 0.109649725 +vf_coef = 4.73808718 +vf_clip_coef = 0 +max_grad_norm = 6.63626146 +replay_ratio = 4.28185177 minibatch_size = 8192 [sweep] @@ -130,7 +130,7 @@ scale = auto [sweep.train.vf_clip_coef] distribution = uniform -min = 0.000002 +min = 0 max = 1.0 scale = auto From 84d6c66c0ee5f6a89c222f6559116890dba22742 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Fri, 14 Aug 2026 21:33:42 +0300 Subject: [PATCH 41/50] Speed up Colosseum environment --- config/osrs_colosseum.ini | 13 +- .../colosseum/encounter_colosseum_model.inc | 14 ++ .../encounter_colosseum_obs_mask.inc | 144 ++++++++++++++---- ocean/osrs/osrs_encounter.h | 17 ++- ocean/osrs/osrs_visual.c | 16 +- ocean/osrs/tests/test_colosseum_modifiers.c | 70 ++++++++- ocean/osrs_colosseum/colosseum_profile.h | 5 +- 7 files changed, 232 insertions(+), 47 deletions(-) diff --git a/config/osrs_colosseum.ini b/config/osrs_colosseum.ini index 400a0f3a47..45c3d29505 100644 --- a/config/osrs_colosseum.ini +++ b/config/osrs_colosseum.ini @@ -134,13 +134,16 @@ min = 0 max = 1.0 scale = auto -# 1024 and 2048 put nothing in the top 20 across 210 trials, so the head of the range was -# only ever buying wall clock. The floor drops instead: obs 1258 -> 922 bought the most where -# the env dominates the step, which is the small-hidden end, so the knee may have moved down. [sweep.policy.hidden_size] distribution = uniform_pow2 -min = 128 -max = 1024 +min = 64 +max = 512 +scale = auto + +[sweep.policy.num_layers] +distribution = int_uniform +min = 1 +max = 4 scale = auto [sweep.train.horizon] diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index 75eb506c8c..67fdde4d86 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -1280,6 +1280,19 @@ typedef struct { } ColConfig; +typedef struct { + OsrsVenatorMonster monsters[COLO_MAX_NPCS]; + uint32_t bounce_masks[COLO_MAX_NPCS]; + uint8_t selection_distances[COLO_MAX_NPCS][COLO_MAX_NPCS]; + int count; +} ColoVenatorPreviewTargets; + +typedef struct { + ColoVenatorPreviewTargets targets; + uint64_t generation; + int valid; +} ColoVenatorPreviewCache; + typedef enum { COLO_NPC_RENDER_ATTACK_NORMAL = 0, COLO_NPC_RENDER_ATTACK_JAVELIN_SKYFALL, @@ -1290,6 +1303,7 @@ typedef struct { const CollisionMap* collision_map; const EncounterArenaTopology* route_topology; OsrsActorRouteCache player_route_cache; + ColoVenatorPreviewCache venator_preview_cache; int world_offset_x; int world_offset_y; const HumanCommand* human_commands; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc index f06916a21a..b58ce29bee 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -112,12 +112,6 @@ static inline float col_obs_rel_y(int ny, int py) { return (float)(ny - py) / (float)COLO_ARENA_HEIGHT; } -typedef struct { - OsrsVenatorMonster monsters[COLO_MAX_NPCS]; - uint32_t bounce_masks[COLO_MAX_NPCS]; - uint8_t selection_distances[COLO_MAX_NPCS][COLO_MAX_NPCS]; - int count; -} ColoVenatorPreviewTargets; static ColoNPC col_matchup_representative_npc(ColoNpcType type) { if (type < 0 || type >= COLO_NUM_NPC_TYPES) abort(); @@ -132,33 +126,129 @@ static ColoNPC col_matchup_representative_npc(ColoNpcType type) { }; } -static void col_collect_venator_preview_targets( +typedef struct { + int same_roster; + uint32_t changed_slots; +} ColoVenatorPreviewDelta; + +static int col_collect_venator_preview_monsters( const ColosseumState* s, - ColoVenatorPreviewTargets* out + OsrsVenatorMonster monsters[COLO_MAX_NPCS] ) { - if (!s || !out) abort(); - memset(out, 0, sizeof(*out)); + int count = 0; for (int slot = 0; slot < COLO_MAX_NPCS; slot++) { const ColoNPC* npc = &s->npcs[slot]; if (!col_npc_is_live_enemy(npc)) continue; - out->monsters[out->count++] = col_venator_monster_from_npc(npc, slot); + monsters[count++] = col_venator_monster_from_npc(npc, slot); + } + return count; +} + +static ColoVenatorPreviewDelta col_venator_preview_delta( + const ColoVenatorPreviewTargets* targets, + const OsrsVenatorMonster current[COLO_MAX_NPCS], + int current_count +) { + if (current_count != targets->count) + return (ColoVenatorPreviewDelta){0}; + uint32_t changed_slots = 0; + for (int idx = 0; idx < current_count; idx++) { + const OsrsVenatorMonster* cached = &targets->monsters[idx]; + const OsrsVenatorMonster* now = ¤t[idx]; + if (cached->slot != now->slot) + return (ColoVenatorPreviewDelta){0}; + if (cached->footprint.sw_x != now->footprint.sw_x || + cached->footprint.sw_y != now->footprint.sw_y || + cached->footprint.size != now->footprint.size || + cached->life != now->life) + changed_slots |= UINT32_C(1) << now->slot; } - for (int sender_idx = 0; sender_idx < out->count; sender_idx++) { - OsrsVenatorMonster sender = out->monsters[sender_idx]; - for (int target_idx = 0; target_idx < out->count; target_idx++) { - OsrsVenatorMonster target = out->monsters[target_idx]; - if (osrs_venator_can_bounce( - sender.footprint, target.footprint)) { - out->bounce_masks[sender.slot] |= - UINT32_C(1) << target.slot; + return (ColoVenatorPreviewDelta){ + .same_roster = 1, + .changed_slots = changed_slots, + }; +} + +static void col_write_venator_preview_pair( + ColoVenatorPreviewTargets* targets, + int sender_idx, + int target_idx +) { + OsrsVenatorMonster sender = targets->monsters[sender_idx]; + OsrsVenatorMonster target = targets->monsters[target_idx]; + uint32_t target_bit = UINT32_C(1) << target.slot; + if (osrs_venator_can_bounce(sender.footprint, target.footprint)) + targets->bounce_masks[sender.slot] |= target_bit; + else + targets->bounce_masks[sender.slot] &= ~target_bit; + int distance = osrs_venator_selection_distance( + sender.footprint, target.footprint); + if (distance < 0 || distance > UINT8_MAX) abort(); + targets->selection_distances[sender.slot][target.slot] = + (uint8_t)distance; +} + +static const ColoVenatorPreviewTargets* col_get_venator_preview_targets( + const ColosseumState* s, + ColosseumContext* ctx +) { + if (!s || !ctx) abort(); +#ifdef COLO_PROFILE_ENABLED + if (COLO_PROFILE_ENABLED()) + COLO_PROFILE_ADD(COLO_PROF_VENATOR_REQUESTS, 1.0); +#endif + OsrsVenatorMonster current[COLO_MAX_NPCS]; + int current_count = col_collect_venator_preview_monsters(s, current); + ColoVenatorPreviewCache* cache = &ctx->venator_preview_cache; + ColoVenatorPreviewTargets* targets = &cache->targets; + ColoVenatorPreviewDelta delta = cache->valid + ? col_venator_preview_delta(targets, current, current_count) + : (ColoVenatorPreviewDelta){0}; + if (delta.same_roster && delta.changed_slots == 0) { +#ifdef COLO_PROFILE_ENABLED + if (COLO_PROFILE_ENABLED()) + COLO_PROFILE_ADD(COLO_PROF_VENATOR_HITS, 1.0); +#endif + return targets; + } + +#ifdef COLO_PROFILE_ENABLED + if (COLO_PROFILE_ENABLED()) + COLO_PROFILE_ADD(COLO_PROF_VENATOR_REFRESHES, 1.0); +#endif + if (delta.same_roster) { + for (int idx = 0; idx < current_count; idx++) { + int slot = current[idx].slot; + if ((delta.changed_slots & (UINT32_C(1) << slot)) != 0) + targets->monsters[idx] = current[idx]; + } + for (int sender_idx = 0; sender_idx < current_count; sender_idx++) { + int sender_slot = current[sender_idx].slot; + for (int target_idx = 0; target_idx < current_count; target_idx++) { + int target_slot = current[target_idx].slot; + if ((delta.changed_slots & + ((UINT32_C(1) << sender_slot) | + (UINT32_C(1) << target_slot))) != 0) + col_write_venator_preview_pair( + targets, sender_idx, target_idx); } - int distance = osrs_venator_selection_distance( - sender.footprint, target.footprint); - if (distance < 0 || distance > UINT8_MAX) abort(); - out->selection_distances[sender.slot][target.slot] = - (uint8_t)distance; + } + } else { + memset(targets, 0, sizeof(*targets)); + targets->count = current_count; + memcpy( + targets->monsters, + current, + (size_t)current_count * sizeof(*current)); + for (int sender_idx = 0; sender_idx < current_count; sender_idx++) { + for (int target_idx = 0; target_idx < current_count; target_idx++) + col_write_venator_preview_pair( + targets, sender_idx, target_idx); } } + cache->valid = 1; + cache->generation++; + return targets; } static int col_venator_best_bounce_slot( const ColoVenatorPreviewTargets* targets, @@ -1632,8 +1722,8 @@ static void col_write_obs_ctx( COLO_PROFILE_MARK(COLO_PROF_OBS_INVENTORY); #endif - ColoVenatorPreviewTargets venator_targets; - col_collect_venator_preview_targets(s, &venator_targets); + const ColoVenatorPreviewTargets* venator_targets = + col_get_venator_preview_targets(s, ctx); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_VENATOR); #endif @@ -1648,7 +1738,7 @@ static void col_write_obs_ctx( } col_write_npc_slot( s, ctx, &s->npcs[npc_idx], npc_idx, - &venator_targets, obs, &i); + venator_targets, obs, &i); } assert(i == COLO_OBS_AFTER_NPCS); #ifdef COLO_PROFILE_ENABLED diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index 849bf12b98..3cec16884d 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -877,14 +877,15 @@ static inline int encounter_npc_try_step( encounter_npc_blocked_fn is_blocked, void* ctx ) { if (dx == 0 && dy == 0) return 0; - int x_clear = encounter_npc_x_edge_clear(*x, *y, size, dx, dy, is_blocked, ctx); - int y_clear = encounter_npc_y_edge_clear(*x, *y, size, dx, dy, is_blocked, ctx); - if (x_clear && y_clear) { - *x += dx; - *y += dy; - return 1; - } - return 0; + if (!encounter_npc_x_edge_clear( + *x, *y, size, dx, dy, is_blocked, ctx)) + return 0; + if (!encounter_npc_y_edge_clear( + *x, *y, size, dx, dy, is_blocked, ctx)) + return 0; + *x += dx; + *y += dy; + return 1; } static inline int encounter_npc_step_out_from_under( diff --git a/ocean/osrs/osrs_visual.c b/ocean/osrs/osrs_visual.c index 7e179903f7..dc9aadb454 100644 --- a/ocean/osrs/osrs_visual.c +++ b/ocean/osrs/osrs_visual.c @@ -339,7 +339,10 @@ static uint64_t osrs_profile_hash_bytes(uint64_t hash, const void* data, size_t static int osrs_colosseum_profile_slot_is_counter(int slot) { return slot == COLO_PROF_BEST_GEAR_REQUESTS || slot == COLO_PROF_BEST_GEAR_HITS || - slot == COLO_PROF_BEST_GEAR_BUILDS; + slot == COLO_PROF_BEST_GEAR_BUILDS || + slot == COLO_PROF_VENATOR_REQUESTS || + slot == COLO_PROF_VENATOR_HITS || + slot == COLO_PROF_VENATOR_REFRESHES; } static void osrs_print_colosseum_profile_results(int total_steps) { @@ -379,10 +382,21 @@ static void osrs_print_colosseum_profile_results(int total_steps) { best_gear_requests, best_gear_requests / steps, best_gear_requests > 0.0 ? 100.0 * best_gear_hits / best_gear_requests : 0.0); + double venator_requests = values[COLO_PROF_VENATOR_REQUESTS]; + double venator_hits = values[COLO_PROF_VENATOR_HITS]; + printf(" %-28s %.0f total %.6f per step %.2f%% hit\n", + "venator", + venator_requests, + venator_requests / steps, + venator_requests > 0.0 ? 100.0 * venator_hits / venator_requests : 0.0); printf(" %-28s %.0f total %.6f per step\n", colosseum_env_profile_name(COLO_PROF_BEST_GEAR_BUILDS), values[COLO_PROF_BEST_GEAR_BUILDS], values[COLO_PROF_BEST_GEAR_BUILDS] / steps); + printf(" %-28s %.0f total %.6f per step\n", + colosseum_env_profile_name(COLO_PROF_VENATOR_REFRESHES), + values[COLO_PROF_VENATOR_REFRESHES], + values[COLO_PROF_VENATOR_REFRESHES] / steps); } #endif diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index e1a2688506..aff8e40340 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -6435,15 +6435,15 @@ static void test_matchup_dpt_obs_ranking(void) { s.player.x = 12; s.player.y = 16; venator_spawn_enemy(&s, 0, COLO_FREMENNIK_BERSERKER, 16, 16, 1); - ColoVenatorPreviewTargets targets; - col_collect_venator_preview_targets(&s, &targets); - int isolated_extra = col_venator_extra_bounce_if_shot(&s, &targets, 0); + const ColoVenatorPreviewTargets* targets = + col_get_venator_preview_targets(&s, &ctx); + int isolated_extra = col_venator_extra_bounce_if_shot(&s, targets, 0); CHECK("isolated Venator preview has no extra bounces", isolated_extra == 0); venator_spawn_enemy(&s, 1, COLO_FREMENNIK_ARCHER, 18, 16, 1); venator_spawn_enemy(&s, 2, COLO_FREMENNIK_BERSERKER, 18, 17, 1); - col_collect_venator_preview_targets(&s, &targets); - int clustered_extra = col_venator_extra_bounce_if_shot(&s, &targets, 0); + targets = col_get_venator_preview_targets(&s, &ctx); + int clustered_extra = col_venator_extra_bounce_if_shot(&s, targets, 0); CHECK("clustered Venator preview sees extra bounces", clustered_extra >= 1); } @@ -6777,6 +6777,38 @@ static void test_venator_bow_bounce_colosseum_integration(void) { ov.projectiles[2].start_delay > ov.projectiles[1].start_delay); } +static void test_venator_preview_cache_reuses_unchanged_geometry(void) { + printf("test_venator_preview_cache_reuses_unchanged_geometry\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset( + &s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 8208); + geo_clear_npcs(&s); + venator_spawn_enemy( + &s, 0, COLO_FREMENNIK_BERSERKER, 16, 16, 1); + venator_spawn_enemy( + &s, 1, COLO_FREMENNIK_ARCHER, 18, 16, 1); + + const ColoVenatorPreviewTargets* first = + col_get_venator_preview_targets(&s, &ctx); + uint64_t first_generation = ctx.venator_preview_cache.generation; + const ColoVenatorPreviewTargets* second = + col_get_venator_preview_targets(&s, &ctx); + CHECK("unchanged venator geometry reuses the preview", + first == second && + ctx.venator_preview_cache.generation == first_generation); + CHECK("nearby target is present in cached bounce mask", + (second->bounce_masks[0] & (UINT32_C(1) << 1)) != 0); + + s.npcs[1].x = 25; + const ColoVenatorPreviewTargets* moved = + col_get_venator_preview_targets(&s, &ctx); + CHECK("changed venator geometry refreshes the preview", + ctx.venator_preview_cache.generation == first_generation + 1); + CHECK("refreshed preview removes the distant target", + (moved->bounce_masks[0] & (UINT32_C(1) << 1)) == 0); +} + static void test_frailty_disables_brew_overheal(void) { printf("test_frailty_disables_brew_overheal\n"); ColosseumContext ctx; @@ -9131,6 +9163,32 @@ static void child_col_restore_previous_inventory_snapshot(void) { } +typedef struct { + int calls; + int blocked_x; +} TestNpcStepBlocker; + +static int test_npc_step_blocked(void* data, int x, int y, int size) { + (void)y; + (void)size; + TestNpcStepBlocker* blocker = (TestNpcStepBlocker*)data; + blocker->calls++; + return x == blocker->blocked_x; +} + +static void test_npc_step_skips_y_edge_after_blocked_x_edge(void) { + printf("test_npc_step_skips_y_edge_after_blocked_x_edge\n"); + TestNpcStepBlocker blocker = { + .blocked_x = 11, + }; + int x = 10; + int y = 10; + int moved = encounter_npc_try_step( + &x, &y, 1, 1, 1, test_npc_step_blocked, &blocker); + ASSERT_INT_EQ("blocked diagonal does not move", moved, 0); + ASSERT_INT_EQ("blocked x edge skips y edge queries", blocker.calls, 1); +} + static void test_colosseum_topology_lifecycle_contract(void) { printf("test_colosseum_topology_lifecycle_contract\n"); assert_child_aborts( @@ -9171,6 +9229,7 @@ int main(void) { test_player_ranged_los_blocked_by_pillar(); test_player_chase_routes_around_pillar_for_los(); test_colosseum_npc_movement_player_tile_guards(); + test_npc_step_skips_y_edge_after_blocked_x_edge(); test_zero_actions_hit_timeout(); test_offpray_attribution_log(); test_step_loop_draft(); @@ -9279,6 +9338,7 @@ int main(void) { test_combat_fidelity_contract_sizes(); test_scythe_multihit_per_size(); test_venator_bow_bounce_colosseum_integration(); + test_venator_preview_cache_reuses_unchanged_geometry(); test_red_flag_minotaur_not_solid_to_other_npcs(); test_bee_contact_damage_band(); test_divine_state_obs_presence(); diff --git a/ocean/osrs_colosseum/colosseum_profile.h b/ocean/osrs_colosseum/colosseum_profile.h index cfdb59b2b9..566bc597ab 100644 --- a/ocean/osrs_colosseum/colosseum_profile.h +++ b/ocean/osrs_colosseum/colosseum_profile.h @@ -58,7 +58,10 @@ X(COLO_PROF_ENV_STEPS, "env_steps") \ X(COLO_PROF_BEST_GEAR_REQUESTS, "best_gear_requests") \ X(COLO_PROF_BEST_GEAR_HITS, "best_gear_hits") \ - X(COLO_PROF_BEST_GEAR_BUILDS, "best_gear_builds") + X(COLO_PROF_BEST_GEAR_BUILDS, "best_gear_builds") \ + X(COLO_PROF_VENATOR_REQUESTS, "venator_requests") \ + X(COLO_PROF_VENATOR_HITS, "venator_hits") \ + X(COLO_PROF_VENATOR_REFRESHES, "venator_refreshes") #define OSRS_ENV_PROFILE_PREFIX colosseum #define OSRS_ENV_PROFILE_COUNT COLO_PROF_COUNT From 7ef746c5da94d9691a93e841b3cfcbe9d6f89a55 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Sat, 15 Aug 2026 21:07:36 +0300 Subject: [PATCH 42/50] Restore Colosseum winner value clip --- config/osrs_colosseum.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/osrs_colosseum.ini b/config/osrs_colosseum.ini index 45c3d29505..a512d52421 100644 --- a/config/osrs_colosseum.ini +++ b/config/osrs_colosseum.ini @@ -67,7 +67,7 @@ prio_alpha = 0.625232637 prio_beta0 = 0 clip_coef = 0.109649725 vf_coef = 4.73808718 -vf_clip_coef = 0 +vf_clip_coef = 1.99999999e-06 max_grad_norm = 6.63626146 replay_ratio = 4.28185177 minibatch_size = 8192 From 4e31b3c64df0cc3c88641b70c6cc39f59ea74f9b Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Sun, 16 Aug 2026 00:17:56 +0300 Subject: [PATCH 43/50] Promote compact Inferno and Zulrah policies --- config/osrs_inferno.ini | 74 ++++++++++++++++++++--------------------- config/osrs_zulrah.ini | 48 +++++++++++++------------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/config/osrs_inferno.ini b/config/osrs_inferno.ini index 75ee61bf04..4a61efab88 100644 --- a/config/osrs_inferno.ini +++ b/config/osrs_inferno.ini @@ -6,18 +6,18 @@ async = 0 [env] start_wave = 1 -damage_reward_coeff = 0.00369666866 +damage_reward_coeff = 0.00600000052 offensive_prayer_reward_coeff = 0 -shield_penalty_coeff = 0.000809495454 -tag_reward_coeff = 0.669779062 +shield_penalty_coeff = 0.000500534545 +tag_reward_coeff = 0.0602469742 shield_tag_reward_coeff = 0.0147339059 late_start_supply_profile_scale = 1.0 curriculum_supply_jitter_mode = 1 -curriculum_supply_shared_jitter = 0 -curriculum_supply_brew_jitter = 0.0847514719 -curriculum_supply_restore_jitter = 0.0291104093 -curriculum_no_brew_mode = 0 -curriculum_no_brew_frac = 0.0794238672 +curriculum_supply_shared_jitter = 0.18321383 +curriculum_supply_brew_jitter = 0.0951578766 +curriculum_supply_restore_jitter = 0.130459204 +curriculum_no_brew_mode = 1 +curriculum_no_brew_frac = 0.263010442 loadout_profile_mode = 0 budget_loadout_fraction = 0.0 classic_curriculum_mode = 1 @@ -31,15 +31,15 @@ phase_600_bonus = 0.0 phase_300_bonus = 0.0 shield_penalty_episode_cap = 0.0 curriculum_wave_1 = 67 -curriculum_frac_1 = 0.106778406 +curriculum_frac_1 = 0.118083619 curriculum_wave_2 = 69 -curriculum_frac_2 = 0.174882144 +curriculum_frac_2 = 0.0357927978 curriculum_wave_3 = 70 -curriculum_frac_3 = 0.00210916414 +curriculum_frac_3 = 0 curriculum_wave_4 = 71 -curriculum_frac_4 = 0.15076603 -curriculum_wave_5 = 55 -curriculum_frac_5 = 0.241018683 +curriculum_frac_4 = 0.0472771451 +curriculum_wave_5 = 54 +curriculum_frac_5 = 0.0299999993 curriculum_wave_6 = 61 curriculum_frac_6 = 0.0 curriculum_frac_7 = 0.0 @@ -60,8 +60,8 @@ zuk_healer_phase_hp_delta_coeff = 0.0 zuk_untagged_healer_tick_penalty_coeff = 0.0 zuk_untagged_healer_target_bonus_coeff = 0.0 zuk_safe_untagged_healer_target_bonus_coeff = 0.0 -zuk_untagged_healer_nonmagic_attack_bonus_coeff = 0.137061894 -zuk_healer_mage_attack_penalty_coeff = 0.144513637 +zuk_untagged_healer_nonmagic_attack_bonus_coeff = 0.53466928 +zuk_healer_mage_attack_penalty_coeff = 0.201784804 zuk_safe_untagged_healer_target_mask = 0 zuk_force_safe_untagged_healer_target_mask = 0 zuk_healer_reward_mode = 0 @@ -70,36 +70,36 @@ post_jad_zuk_multiplier = 1.78125858 jad_alive_zuk_multiplier = 0.1573513 [vec] -total_agents = 4096 -num_buffers = 2 +total_agents = 1024 +num_buffers = 4 num_threads = 16 action_mask_size = 436 gpu_env = 0 [policy] -hidden_size = 1024 +hidden_size = 256 num_layers = 2 expansion_factor = 1 [train] -total_timesteps = 168232608 -horizon = 16 +total_timesteps = 32251094 +horizon = 32 min_lr_ratio = 0 -learning_rate = 0.00142262119 -momentum = 0.9641862728826048 -ent_coef = 0.0002 -gamma = 0.998270094 -gae_lambda = 0.25 -vtrace_rho_clip = 1.86103201 -vtrace_c_clip = 1.0 -prio_alpha = 0.0387323499 -prio_beta0 = 0.315968394 -clip_coef = 0.00100000005 -vf_coef = 1.97704399 -vf_clip_coef = 0.03 -max_grad_norm = 3.15836239 -replay_ratio = 4.71614552 -minibatch_size = 8192 +learning_rate = 0.00393043552 +momentum = 0.966556907 +ent_coef = 0.00200000079 +gamma = 0.999299705 +gae_lambda = 0.996396005 +vtrace_rho_clip = 1.42464721 +vtrace_c_clip = 0.42439875 +prio_alpha = 0.0912185907 +prio_beta0 = 0.399999976 +clip_coef = 0.0349372625 +vf_coef = 2.09087276 +vf_clip_coef = 0.233588189 +max_grad_norm = 2.29937434 +replay_ratio = 4.11032152 +minibatch_size = 4096 [sweep] max_suggestion_cost = 4800 @@ -109,7 +109,7 @@ early_stop_quantile = 0.25 [sweep.train.total_timesteps] distribution = log_normal -min = 50_000_000 +min = 25_000_000 max = 1_000_000_000 scale = time diff --git a/config/osrs_zulrah.ini b/config/osrs_zulrah.ini index 4ee97f1e99..8fb1c65b84 100644 --- a/config/osrs_zulrah.ini +++ b/config/osrs_zulrah.ini @@ -9,12 +9,12 @@ episode_mode = 1 gear_tier_weight_0 = 0.5 gear_tier_weight_1 = 0.3 gear_tier_weight_2 = 0.2 -reward_win = 0.424107641 -reward_loss_penalty = 0.236695677 -reward_damage_dealt = 0.0957096219 -reward_correct_style = 0.8 -reward_damage_received_penalty = 0.19149816 -reward_cloud_occupancy_penalty = 0.218443304 +reward_win = 0.441096812 +reward_loss_penalty = 0.769322574 +reward_damage_dealt = 0.057287436 +reward_correct_style = 0.196290344 +reward_damage_received_penalty = 0.214363933 +reward_cloud_occupancy_penalty = 0.218496278 [vec] total_agents = 4096 @@ -24,29 +24,29 @@ action_mask_size = 427 gpu_env = 0 [policy] -hidden_size = 512 +hidden_size = 128 num_layers = 2 expansion_factor = 1 [train] -total_timesteps = 120000000 -horizon = 32 +total_timesteps = 33817236 +horizon = 64 min_lr_ratio = 0.0 -learning_rate = 0.00156910333 -momentum = 0.977381706 -ent_coef = 8.08037657e-06 -gamma = 0.995960951 -gae_lambda = 0.880164325 -vtrace_rho_clip = 1.16428173 -vtrace_c_clip = 0.8 -prio_alpha = 0.880423367 -prio_beta0 = 0.575937748 -clip_coef = 0.315547526 -vf_coef = 1.46088982 -vf_clip_coef = 0.3 -max_grad_norm = 1.49210274 -replay_ratio = 4.0 -minibatch_size = 4096 +learning_rate = 0.00323189585 +momentum = 0.9816401 +ent_coef = 1.52955658e-06 +gamma = 0.999931157 +gae_lambda = 0.943060994 +vtrace_rho_clip = 1.56762898 +vtrace_c_clip = 2.07890463 +prio_alpha = 0.193628281 +prio_beta0 = 0.889296472 +clip_coef = 0.287961602 +vf_coef = 1.89537096 +vf_clip_coef = 0.0115083614 +max_grad_norm = 0.128824607 +replay_ratio = 2.38622189 +minibatch_size = 8192 [sweep] max_suggestion_cost = 420 From c112924f94bce7333746e8c044557a45075f08af Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Sun, 16 Aug 2026 00:29:32 +0300 Subject: [PATCH 44/50] Narrow OSRS PR scope --- .clangd | 46 --------------- config/default.ini | 1 - src/protein.cu | 8 --- src/pufferl.cu | 135 --------------------------------------------- 4 files changed, 190 deletions(-) delete mode 100644 .clangd diff --git a/.clangd b/.clangd deleted file mode 100644 index 86fca3bcc6..0000000000 --- a/.clangd +++ /dev/null @@ -1,46 +0,0 @@ -Diagnostics: - UnusedIncludes: None ---- -If: - PathMatch: ocean/osrs_colosseum/.* -CompileFlags: - Add: - - -xc - - -I../../src - - -I../../vendor - - -I../../raylib-5.5_macos/include - - -I../../raylib-5.5_linux_amd64/include - - -I../../raylib-5.5_webassembly/include - - -Wno-pragma-once-outside-header ---- -If: - PathMatch: src/.* -CompileFlags: - Add: - - -xc - - -I../raylib-5.5_macos/include - - -I../raylib-5.5_linux_amd64/include - - -I../raylib-5.5_webassembly/include - - -Wno-pragma-once-outside-header ---- -If: - PathMatch: .*\.cu -CompileFlags: - Remove: - - -xc - Add: - - -xcuda - - -nocudainc - - -nocudalib - - --cuda-host-only - - -ferror-limit=0 -Diagnostics: - Suppress: '*' ---- -If: - PathMatch: .*\.inc -CompileFlags: - Add: - - -ferror-limit=0 -Diagnostics: - Suppress: '*' diff --git a/config/default.ini b/config/default.ini index 3c259b2215..b8e51a4619 100644 --- a/config/default.ini +++ b/config/default.ini @@ -123,7 +123,6 @@ downsample = 5 use_gpu = True prune_pareto = True early_stop_quantile = 0.3 -resume_from = '' [sweep.train.total_timesteps] distribution = log_normal diff --git a/src/protein.cu b/src/protein.cu index 73b9d0412f..a9e8ca47fc 100644 --- a/src/protein.cu +++ b/src/protein.cu @@ -671,14 +671,6 @@ void protein_sweep_observe(ProteinSweep *sw, const float *norm_params, } } -// Random phase is gated on suggestions made this process, not observations -// held. Without this, a resumed sweep re-draws Sobol samples it already has. -void protein_sweep_skip_random(ProteinSweep *sw) { - if (sw->suggestion_idx < sw->num_random_samples) { - sw->suggestion_idx = sw->num_random_samples; - } -} - int protein_sweep_should_stop(const ProteinSweep *sw, float score, float cost) { if (sw->use_logit) { score = logit_transform(score); diff --git a/src/pufferl.cu b/src/pufferl.cu index a8a8b215bc..c584cb1008 100644 --- a/src/pufferl.cu +++ b/src/pufferl.cu @@ -2547,125 +2547,6 @@ typedef struct { TrainResult result; } SweepJob; -// Raw (denormalized) values, so a resume can re-normalize against a space whose -// ranges have since moved. Normalized samples would silently change meaning. -static void sweep_obs_append(const char* path, SweepParam* params, SweepSpace* space, - const float* sample, int run, int point, float score, float cost) { - if (!path || !path[0]) { - return; - } - int fresh = access(path, F_OK) != 0; - FILE* f = fopen(path, "a"); - if (!f) { - fprintf(stderr, "sweep: cannot append observations to %s: %s\n", - path, strerror(errno)); - return; - } - if (fresh) { - fprintf(f, "run,point,score,cost"); - for (int i = 0; i < space->num; i++) { - fprintf(f, ",%s.%s", params[i].section, params[i].key); - } - fprintf(f, "\n"); - } - fprintf(f, "%d,%d,%.9g,%.9g", run, point, score, cost); - for (int i = 0; i < space->num; i++) { - fprintf(f, ",%.9g", space_unnormalize(&space->spaces[i], sample[i])); - } - fprintf(f, "\n"); - fclose(f); -} - -// Replays (score, cost) from a prior sweep's observation log so the surrogate -// starts warm. Rows whose values fall outside the current ranges are counted and -// reported, never dropped quietly. -static int sweep_obs_resume(const char* path, ProteinSweep* protein, - SweepParam* params, SweepSpace* space) { - FILE* f = fopen(path, "r"); - if (!f) { - fprintf(stderr, "sweep.resume_from: cannot read %s: %s\n", - path, strerror(errno)); - exit(1); - } - char line[16384]; - if (!fgets(line, sizeof(line), f)) { - fprintf(stderr, "sweep.resume_from: %s is empty\n", path); - exit(1); - } - int* col_of = (int*)calloc((size_t)space->num, sizeof(int)); - for (int i = 0; i < space->num; i++) { - col_of[i] = -1; - } - int ncols = 0; - char* save = NULL; - for (char* tok = strtok_r(line, ",\r\n", &save); tok; - tok = strtok_r(NULL, ",\r\n", &save)) { - for (int i = 0; i < space->num; i++) { - char name[160]; - snprintf(name, sizeof(name), "%s.%s", params[i].section, params[i].key); - if (strcmp(tok, name) == 0) { - col_of[i] = ncols; - } - } - ncols++; - } - for (int i = 0; i < space->num; i++) { - if (col_of[i] < 0) { - fprintf(stderr, "sweep.resume_from: %s has no column for [%s] %s\n", - path, params[i].section, params[i].key); - exit(1); - } - } - - float* sample = (float*)calloc((size_t)space->num, sizeof(float)); - float* vals = (float*)calloc((size_t)ncols, sizeof(float)); - int replayed = 0; - int skipped = 0; - while (fgets(line, sizeof(line), f)) { - int n = 0; - save = NULL; - for (char* tok = strtok_r(line, ",\r\n", &save); tok && n < ncols; - tok = strtok_r(NULL, ",\r\n", &save)) { - vals[n++] = strtof(tok, NULL); - } - if (n < ncols) { - skipped++; - continue; - } - // A value written at a range endpoint round-trips through float32 a hair - // outside it, so admit that epsilon and snap it back. A genuinely - // narrowed range lands far outside and is still rejected. - int in_range = 1; - for (int i = 0; i < space->num; i++) { - float norm = space_normalize(&space->spaces[i], vals[col_of[i]]); - if (!isfinite(norm) || norm < -1.001f || norm > 1.001f) { - in_range = 0; - break; - } - sample[i] = fmaxf(-1.0f, fminf(1.0f, norm)); - } - float score = vals[2]; - float cost = vals[3]; - if (!in_range || !isfinite(score) || !isfinite(cost)) { - skipped++; - continue; - } - protein_sweep_observe(protein, sample, score, cost, 0); - replayed++; - } - free(vals); - free(sample); - free(col_of); - fclose(f); - if (replayed > 0) { - protein_sweep_skip_random(protein); - } - printf("sweep resume: replayed %d observations from %s (%d skipped as " - "out of range or malformed)\n", replayed, path, skipped); - fflush(stdout); - return replayed; -} - void run_sweep(Ini* ini, const char* exe_path) { // Build SweepSpace + param map from [sweep.
.] sections. const char* goal = puf_ini_get_str(ini, "sweep", "goal"); @@ -2785,20 +2666,6 @@ void run_sweep(Ini* ini, const char* exe_path) { .rng_seed = 73ULL, }); - char obs_path[2048]; - { - char dir[1024]; - snprintf(dir, sizeof(dir), "%s/%s", - puf_ini_get_str(ini, "base", "log_dir"), - puf_ini_get_str(ini, "base", "env_name")); - mkdir_p(dir); - snprintf(obs_path, sizeof(obs_path), "%s/sweep_observations.csv", dir); - } - const char* resume_from = puf_ini_get_str(ini, "sweep", "resume_from"); - if (resume_from && resume_from[0]) { - sweep_obs_resume(resume_from, protein, params, space); - } - int parallel = sweep_gpus / train_gpus; // Row 0 = staging for suggest; rows 1..parallel = per-slot copies for observe. float* samples = (float*)calloc((parallel + 1) * space->num, sizeof(float)); @@ -2942,8 +2809,6 @@ void run_sweep(Ini* ini, const char* exe_path) { for (int pi = 0; pi < job->result.points; pi++) { protein_sweep_observe(protein, job->sample, job->result.scores[pi], job->result.costs[pi], 0); - sweep_obs_append(obs_path, params, space, job->sample, job->run, pi, - job->result.scores[pi], job->result.costs[pi]); } printf("sweep run=%d score=%.4f cost=%.2f steps=%.0f random=%d gp_obs=%d pareto=%d\n", job->run, job->result.score, job->result.cost, job->result.steps, From 1b477f3a2db05710b4446b3a0aaf1e053ea23875 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Sun, 16 Aug 2026 02:20:49 +0300 Subject: [PATCH 45/50] Simplify OSRS simulator implementation --- build.sh | 26 +- config/osrs_colosseum.ini | 36 - config/osrs_inferno.ini | 33 - config/osrs_pvp.ini | 20 - config/osrs_zulrah.ini | 42 - .../colosseum/encounter_colosseum_combat.inc | 16 +- .../encounter_colosseum_forecast.inc | 169 -- .../colosseum/encounter_colosseum_helpers.inc | 33 +- .../colosseum/encounter_colosseum_lab.inc | 31 +- .../encounter_colosseum_mask_render.inc | 109 +- .../colosseum/encounter_colosseum_model.inc | 55 +- .../encounter_colosseum_modifiers.inc | 7 - .../encounter_colosseum_movement.inc | 11 - .../encounter_colosseum_obs_mask.inc | 167 +- .../encounter_colosseum_player_actions.inc | 151 +- .../encounter_colosseum_render_snapshot.inc | 21 +- .../encounter_colosseum_reset_spawn.inc | 2 - .../encounter_colosseum_reward_step.inc | 50 +- ocean/osrs/encounters/encounter_colosseum.h | 1 - ocean/osrs/encounters/encounter_nh_pvp.h | 132 +- ocean/osrs/encounters/encounter_zulrah.h | 336 +-- .../inferno/encounter_inferno_combat.inc | 49 +- .../inferno/encounter_inferno_forecast.inc | 779 +----- .../inferno/encounter_inferno_helpers.inc | 88 - .../inferno/encounter_inferno_lab.inc | 2 - .../inferno/encounter_inferno_model.inc | 5 - .../inferno/encounter_inferno_movement.inc | 7 - .../inferno/encounter_inferno_obs_mask.inc | 90 +- .../encounter_inferno_player_actions.inc | 11 - .../encounter_inferno_render_snapshot.inc | 29 - .../inferno/encounter_inferno_reset_spawn.inc | 3 - .../inferno/encounter_inferno_reward_step.inc | 54 - ocean/osrs/osrs_collision.h | 226 +- ocean/osrs/osrs_encounter.h | 3 - ocean/osrs/osrs_encounter_player.h | 14 +- ocean/osrs/osrs_entity_encoder.cu | 140 +- ocean/osrs/osrs_env_profile.h | 19 +- ocean/osrs/osrs_gui.h | 15 +- ocean/osrs/osrs_human_input.h | 116 +- ocean/osrs/osrs_interaction.h | 1 - ocean/osrs/osrs_inventory_actions.h | 30 +- ocean/osrs/osrs_inventory_clicks.h | 3 +- ocean/osrs/osrs_items.h | 5 - ocean/osrs/osrs_pathfinding.h | 364 +-- ocean/osrs/osrs_pvp_actions.h | 115 +- ocean/osrs/osrs_pvp_combat.h | 93 +- ocean/osrs/osrs_pvp_gear.h | 20 +- ocean/osrs/osrs_pvp_movement.h | 75 +- ocean/osrs/osrs_pvp_observations.h | 8 - ocean/osrs/osrs_pvp_opponents.h | 8 +- ocean/osrs/osrs_render.h | 10 +- ocean/osrs/osrs_types.h | 2 - ocean/osrs/osrs_visual_net.h | 214 +- ocean/osrs/scripts/check-builds.sh | 49 - ocean/osrs/tests/bench_colo_step.c | 278 --- .../tests/bench_colosseum_forecast_profile.c | 617 ----- ocean/osrs/tests/bench_inferno_forecast.c | 330 --- ocean/osrs/tests/inferno_lab_cli.c | 98 - ocean/osrs/tests/osrs_route_reference.h | 1679 ------------- ocean/osrs/tests/probe_colo_best_gear_dpt.c | 458 ---- ocean/osrs/tests/probe_colo_bis_gear_prayer.c | 50 - ocean/osrs/tests/probe_colo_damage_scale.c | 121 - .../tests/probe_colo_prayer_switch_slip.c | 46 - ocean/osrs/tests/probe_colo_remove_brews.c | 43 - ocean/osrs/tests/probe_colo_rng_diversity.c | 86 - ocean/osrs/tests/probe_colo_skyfall_dodge.c | 574 ----- ocean/osrs/tests/probe_colo_wave_advance.c | 220 -- .../osrs/tests/probe_colosseum_winnability.c | 187 -- ocean/osrs/tests/test_colo_prayer_wiring.c | 125 - .../tests/test_colosseum_forecast_exact.c | 1233 ---------- ocean/osrs/tests/test_colosseum_golden.c | 1 - ocean/osrs/tests/test_colosseum_modifiers.c | 122 +- ...riant.c => test_colosseum_sim_invariant.c} | 43 - ....c => test_colosseum_thread_determinism.c} | 26 +- .../test_encounter_route_topology_setup.c | 139 -- ocean/osrs/tests/test_inferno_attack_styles.c | 2176 ++++------------- .../osrs/tests/test_inferno_forecast_exact.c | 479 ---- ocean/osrs/tests/test_inferno_lab.c | 18 +- ocean/osrs/tests/test_osrs_player_step.c | 190 -- ocean/osrs/tests/test_osrs_pvp_pending_hits.c | 28 +- ocean/osrs/tests/test_zulrah_hit_delay.c | 52 +- ocean/osrs/tests/trace_colo_move.c | 67 - ocean/osrs_colosseum/colosseum_profile.h | 1 - ocean/osrs_colosseum/osrs_colosseum.cu | 14 - ocean/osrs_colosseum/osrs_colosseum.h | 18 +- ocean/osrs_inferno/osrs_inferno.cu | 14 - ocean/osrs_inferno/osrs_inferno.h | 25 +- ocean/osrs_pvp/osrs_pvp.h | 4 - ocean/osrs_zulrah/osrs_zulrah.h | 1 - src/ocean.cu | 8 +- src/pufferl.cu | 4 +- tests/test_osrs_entity_encoders.py | 174 +- tests/test_osrs_entity_encoders_cuda.cu | 42 +- 93 files changed, 1511 insertions(+), 12375 deletions(-) delete mode 100644 ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc delete mode 100755 ocean/osrs/scripts/check-builds.sh delete mode 100644 ocean/osrs/tests/bench_colo_step.c delete mode 100644 ocean/osrs/tests/bench_colosseum_forecast_profile.c delete mode 100644 ocean/osrs/tests/bench_inferno_forecast.c delete mode 100644 ocean/osrs/tests/inferno_lab_cli.c delete mode 100644 ocean/osrs/tests/osrs_route_reference.h delete mode 100644 ocean/osrs/tests/probe_colo_best_gear_dpt.c delete mode 100644 ocean/osrs/tests/probe_colo_bis_gear_prayer.c delete mode 100644 ocean/osrs/tests/probe_colo_damage_scale.c delete mode 100644 ocean/osrs/tests/probe_colo_prayer_switch_slip.c delete mode 100644 ocean/osrs/tests/probe_colo_remove_brews.c delete mode 100644 ocean/osrs/tests/probe_colo_rng_diversity.c delete mode 100644 ocean/osrs/tests/probe_colo_skyfall_dodge.c delete mode 100644 ocean/osrs/tests/probe_colo_wave_advance.c delete mode 100644 ocean/osrs/tests/probe_colosseum_winnability.c delete mode 100644 ocean/osrs/tests/test_colo_prayer_wiring.c delete mode 100644 ocean/osrs/tests/test_colosseum_forecast_exact.c rename ocean/osrs/tests/{probe_colo_sim_invariant.c => test_colosseum_sim_invariant.c} (69%) rename ocean/osrs/tests/{probe_env_thread_determinism.c => test_colosseum_thread_determinism.c} (80%) delete mode 100644 ocean/osrs/tests/test_inferno_forecast_exact.c delete mode 100644 ocean/osrs/tests/trace_colo_move.c diff --git a/build.sh b/build.sh index d14c4dbc4c..040ff80fd0 100755 --- a/build.sh +++ b/build.sh @@ -87,32 +87,19 @@ fi PLATFORM="$(uname -s)" if [ "$PLATFORM" = "Linux" ]; then RAYLIB_NAME='raylib-5.5_linux_amd64' + OMP_FLAGS=(-fopenmp) OMP_LIB=-lomp5 SANITIZE_FLAGS=(-fsanitize=address,undefined,bounds,pointer-overflow,leak -fno-omit-frame-pointer) STANDALONE_LDFLAGS=(-lGL) else RAYLIB_NAME='raylib-5.5_macos' + OMP_PREFIX="$(brew --prefix libomp)" + OMP_FLAGS=(-Xclang -fopenmp -I"$OMP_PREFIX/include" -L"$OMP_PREFIX/lib" -lomp) OMP_LIB=-lomp SANITIZE_FLAGS=() STANDALONE_LDFLAGS=(-framework Cocoa -framework IOKit -framework CoreVideo -framework OpenGL) fi -# OpenMP for the standalone (--local/--fast/--cpu) clang paths. Apple clang rejects -# -fopenmp; it needs -Xclang -fopenmp plus Homebrew libomp on the include/lib path. -# Linux clang/gcc take -fopenmp directly, so those defaults are preserved there. -OMP_CFLAGS=(-fopenmp) -OMP_LDFLAGS=(-fopenmp) -if [ "$PLATFORM" != "Linux" ]; then - OMP_PREFIX="$(brew --prefix libomp 2>/dev/null || true)" - if [ -n "$OMP_PREFIX" ]; then - OMP_CFLAGS=(-Xclang -fopenmp -I"$OMP_PREFIX/include") - OMP_LDFLAGS=(-L"$OMP_PREFIX/lib" -lomp) - else - OMP_CFLAGS=(-Xclang -fopenmp) - OMP_LDFLAGS=(-lomp) - fi -fi - CLANG_WARN=( -Wall -Wno-narrowing @@ -214,9 +201,6 @@ USER_OUTPUT_NAME=${OUTPUT_NAME-} OUTPUT_NAME=${OUTPUT_NAME:-$ENV} SRC_FILE=${SRC_FILE:-$SRC_DIR/$ENV.c} -# Standalone environment build -# -mavx2 enables AVX2 intrinsics (__m256, _mm256_*) which drive.h and -# src/pufferenv.h use directly. x86_64 only — omitted on ARM/Apple Silicon. if [ "$(uname -m)" = "x86_64" ]; then SIMD_FLAGS=(-mavx2 -mfma) else @@ -239,7 +223,7 @@ if [ "$MODE" = "local" ] || [ "$MODE" = "fast" ]; then "${LINK_ARCHIVES[@]}" "${EXTRA_LDFLAGS[@]}" "${STANDALONE_LDFLAGS[@]}" - -lm -lpthread "${OMP_CFLAGS[@]}" "${OMP_LDFLAGS[@]}" + -lm -lpthread "${OMP_FLAGS[@]}" -DPLATFORM_DESKTOP "${EXTRA_CFLAGS[@]}" ) @@ -317,7 +301,7 @@ elif [ "$MODE" = "cpu" ]; then "${LINK_ARCHIVES[@]}" \ "${EXTRA_LDFLAGS[@]}" \ "${STANDALONE_LDFLAGS[@]}" \ - -lm -lpthread "${OMP_CFLAGS[@]}" "${OMP_LDFLAGS[@]}" \ + -lm -lpthread "${OMP_FLAGS[@]}" \ -o "build/cpu_${ENV}" echo "Built: ./build/cpu_${ENV}" exit 0 diff --git a/config/osrs_colosseum.ini b/config/osrs_colosseum.ini index 7ef83db38c..d62a192bdd 100644 --- a/config/osrs_colosseum.ini +++ b/config/osrs_colosseum.ini @@ -1,7 +1,6 @@ # OSRS Fortis Colosseum encounter. [base] -env_name = osrs_colosseum async = 0 [env] @@ -37,20 +36,16 @@ curriculum_wave_3 = 12 curriculum_frac_3 = 0.16289182 curriculum_wave_4 = 6 curriculum_frac_4 = 0.399915189 -curriculum_wave_5 = 6 -curriculum_frac_5 = 0.0 [vec] total_agents = 8192 num_buffers = 8 num_threads = 32 action_mask_size = 456 -gpu_env = 0 [policy] hidden_size = 256 num_layers = 2 -expansion_factor = 1 [train] total_timesteps = 438300800 @@ -70,19 +65,15 @@ vf_coef = 4.73808718 vf_clip_coef = 1.99999999e-06 max_grad_norm = 6.63626146 replay_ratio = 4.28185177 -minibatch_size = 8192 [sweep] -max_suggestion_cost = 3600 max_runs = 400 gpus = 1 early_stop_quantile = 0.25 [sweep.train.total_timesteps] -distribution = log_normal min = 10_000_000 max = 2_000_000_000 -scale = time [sweep.env.damage_reward_coeff] distribution = log_normal @@ -120,61 +111,40 @@ min = 0.0 max = 1.0 scale = auto -# Ceiling was 8.0 and the top 20 of the 20260803 sweep ran 7.3 to 8.0, median exactly 8.0. -# A dim pinned on its own boundary is not an answer, it is a range that ran out. [sweep.train.vf_coef] -distribution = uniform -min = 0.1 max = 24.0 -scale = auto [sweep.train.vf_clip_coef] -distribution = uniform min = 0 max = 1.0 -scale = auto [sweep.policy.hidden_size] -distribution = uniform_pow2 min = 64 max = 512 -scale = auto [sweep.policy.num_layers] distribution = int_uniform -min = 1 max = 4 -scale = auto [sweep.train.horizon] -distribution = uniform_pow2 min = 8 max = 128 -scale = auto [sweep.train.learning_rate] -distribution = log_normal min = 0.00003 max = 0.004 -scale = 0.5 [sweep.train.ent_coef] -distribution = log_normal min = 0.00000001 max = 0.002 -scale = auto [sweep.train.gamma] -distribution = logit_normal min = 0.995 max = 0.99997 -scale = auto [sweep.train.gae_lambda] -distribution = logit_normal min = 0.05 max = 0.999 -scale = auto [sweep.train.min_lr_ratio] distribution = uniform @@ -183,22 +153,16 @@ max = 0.9 scale = auto [sweep.train.clip_coef] -distribution = uniform min = 0.001 max = 0.12 -scale = auto [sweep.train.max_grad_norm] -distribution = uniform min = 0.3 max = 8.0 -scale = auto [sweep.train.replay_ratio] -distribution = uniform min = 1.5 max = 6.0 -scale = auto [sweep.env.curriculum_frac_1] distribution = uniform diff --git a/config/osrs_inferno.ini b/config/osrs_inferno.ini index 0ba9af704f..2594faf455 100644 --- a/config/osrs_inferno.ini +++ b/config/osrs_inferno.ini @@ -1,7 +1,6 @@ # OSRS Inferno encounter. [base] -env_name = osrs_inferno async = 0 [env] @@ -73,19 +72,15 @@ jad_alive_zuk_multiplier = 0.1573513 [vec] total_agents = 1024 num_buffers = 4 -num_threads = 16 action_mask_size = 436 -gpu_env = 0 [policy] hidden_size = 256 num_layers = 2 -expansion_factor = 1 [train] total_timesteps = 32251094 horizon = 32 -min_lr_ratio = 0 learning_rate = 0.00393043552 momentum = 0.966556907 ent_coef = 0.00200000079 @@ -109,51 +104,34 @@ gpus = 1 early_stop_quantile = 0.25 [sweep.train.total_timesteps] -distribution = log_normal min = 25_000_000 max = 1_000_000_000 -scale = time [sweep.policy.hidden_size] -distribution = uniform_pow2 min = 256 max = 2048 -scale = auto [sweep.train.horizon] -distribution = uniform_pow2 min = 16 max = 64 -scale = auto [sweep.train.minibatch_size] -distribution = uniform_pow2 -min = 4096 max = 16384 -scale = auto [sweep.train.learning_rate] -distribution = log_normal min = 0.0005 max = 0.004 -scale = 0.5 [sweep.train.ent_coef] -distribution = log_normal min = 0.0000000001 max = 0.002 -scale = auto [sweep.train.gamma] -distribution = logit_normal min = 0.9975 max = 0.99997 -scale = auto [sweep.train.gae_lambda] -distribution = logit_normal min = 0.25 max = 0.999 -scale = auto [sweep.train.min_lr_ratio] distribution = uniform @@ -186,34 +164,24 @@ max = 0.4 scale = auto [sweep.train.clip_coef] -distribution = uniform min = 0.001 max = 0.12 -scale = auto [sweep.train.vf_coef] distribution = log_normal min = 0.02 -max = 4.0 -scale = auto [sweep.train.vf_clip_coef] -distribution = uniform min = 0.03 max = 0.25 -scale = auto [sweep.train.max_grad_norm] -distribution = uniform min = 0.3 max = 8.0 -scale = auto [sweep.train.replay_ratio] -distribution = uniform min = 3.0 max = 6.0 -scale = auto [sweep.env.curriculum_frac_1] distribution = uniform @@ -311,7 +279,6 @@ min = 0.05 max = 1.25 scale = auto - [sweep.env.zuk_untagged_healer_nonmagic_attack_bonus_coeff] distribution = uniform min = 0.0 diff --git a/config/osrs_pvp.ini b/config/osrs_pvp.ini index f15d401ee2..0a8c7b8639 100644 --- a/config/osrs_pvp.ini +++ b/config/osrs_pvp.ini @@ -1,7 +1,5 @@ [base] -env_name = osrs_pvp async = 0 -score_metric = score eval_episodes = 4096 [env] @@ -14,33 +12,15 @@ shaping_enabled = 1 shaping_scale = 1.0 [vec] -total_agents = 4096 -num_buffers = 2 -num_threads = 16 action_mask_size = 423 -gpu_env = 0 [policy] -hidden_size = 128 num_layers = 2 -expansion_factor = 1 [train] total_timesteps = 60000000 -horizon = 64 -min_lr_ratio = 0.0 learning_rate = 0.0005 -momentum = 0.95 -ent_coef = 0.001 -gamma = 0.995 -gae_lambda = 0.90 -vtrace_rho_clip = 1.0 -vtrace_c_clip = 1.0 prio_alpha = 0.0 prio_beta0 = 0.0 -clip_coef = 0.20 vf_coef = 0.5 -vf_clip_coef = 0.20 max_grad_norm = 0.5 -replay_ratio = 1.0 -minibatch_size = 8192 diff --git a/config/osrs_zulrah.ini b/config/osrs_zulrah.ini index 8fb1c65b84..f68831c030 100644 --- a/config/osrs_zulrah.ini +++ b/config/osrs_zulrah.ini @@ -1,5 +1,4 @@ [base] -env_name = osrs_zulrah async = 0 [env] @@ -17,21 +16,13 @@ reward_damage_received_penalty = 0.214363933 reward_cloud_occupancy_penalty = 0.218496278 [vec] -total_agents = 4096 -num_buffers = 2 -num_threads = 16 action_mask_size = 427 -gpu_env = 0 [policy] -hidden_size = 128 num_layers = 2 -expansion_factor = 1 [train] total_timesteps = 33817236 -horizon = 64 -min_lr_ratio = 0.0 learning_rate = 0.00323189585 momentum = 0.9816401 ent_coef = 1.52955658e-06 @@ -46,91 +37,58 @@ vf_coef = 1.89537096 vf_clip_coef = 0.0115083614 max_grad_norm = 0.128824607 replay_ratio = 2.38622189 -minibatch_size = 8192 [sweep] max_suggestion_cost = 420 max_runs = 400 gpus = 1 -early_stop_quantile = 0.3 [sweep.train.total_timesteps] -distribution = log_normal -min = 3e7 max = 2e8 -scale = time [sweep.train.horizon] -distribution = uniform_pow2 min = 16 max = 128 -scale = auto [sweep.train.minibatch_size] -distribution = uniform_pow2 -min = 4096 max = 16384 -scale = auto [sweep.train.learning_rate] -distribution = log_normal min = 0.001 max = 0.02 -scale = 0.5 [sweep.train.ent_coef] -distribution = log_normal min = 0.00000001 max = 0.0003 -scale = auto [sweep.train.gamma] -distribution = logit_normal min = 0.995 max = 0.99997 -scale = auto [sweep.train.gae_lambda] -distribution = logit_normal -min = 0.2 max = 0.95 -scale = auto [sweep.train.replay_ratio] -distribution = uniform min = 0.5 -max = 4.0 -scale = auto [sweep.train.clip_coef] -distribution = uniform min = 0.05 max = 0.6 -scale = auto [sweep.train.vf_clip_coef] -distribution = uniform min = 0.005 max = 0.3 -scale = auto [sweep.train.vf_coef] -distribution = uniform min = 0.3 max = 2.5 -scale = auto [sweep.train.max_grad_norm] -distribution = uniform min = 0.02 -max = 1.5 -scale = auto [sweep.train.momentum] -distribution = logit_normal min = 0.85 max = 0.99 -scale = auto [sweep.train.prio_alpha] distribution = uniform diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc index 68218a3d3f..c618117bc9 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc @@ -1,10 +1,6 @@ #line 76 "encounter_colosseum.h" -static void col_apply_npc_death( - ColosseumState* s, - const ColosseumContext* ctx, - int idx -) { +static void col_apply_npc_death(ColosseumState* s, int idx) { ColoNPC* npc = &s->npcs[idx]; if (col_type_is_hazard_entity(npc->type)) { @@ -80,7 +76,7 @@ static void col_apply_echo_boots_recoil( s->current_wave_fresh_damage += fresh; } if (npc->hp <= 0) { - col_apply_npc_death(s, ctx, attacker_slot); + col_apply_npc_death(s, attacker_slot); } else { col_mod_on_npc_hp_changed( s, ctx->route_topology, attacker_slot); @@ -124,7 +120,7 @@ static void col_resolve_player_projectiles_on_npcs_ctx( s->current_wave_fresh_damage += fresh; } if (npc->hp <= 0) { - col_apply_npc_death(s, ctx, i); + col_apply_npc_death(s, i); break; } col_mod_on_npc_hp_changed(s, ctx->route_topology, i); @@ -135,12 +131,6 @@ static void col_resolve_player_projectiles_on_npcs_ctx( } } -static void col_resolve_player_projectiles_on_npcs( - ColosseumState* s, - ColosseumContext* ctx -) { - col_resolve_player_projectiles_on_npcs_ctx(s, ctx); -} static void col_remove_npc_corpse( ColosseumState* s, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc deleted file mode 100644 index c0c2c99666..0000000000 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc +++ /dev/null @@ -1,169 +0,0 @@ -#line 76 "encounter_colosseum.h" - -#define COLO_STYLE_MASK_MELEE 1 -#define COLO_STYLE_MASK_RANGED 2 -#define COLO_STYLE_MASK_MAGIC 4 - -typedef struct { - int valid; - int land_x; - int land_y; -} ColoForecastLanding; - -typedef struct { - int found; - int npc_slot; - int x; - int y; - int size; - int ticks; - int distance; -} ColoVolatilityBlastPreview; - -static inline ColoVolatilityBlastPreview col_volatility_blast_preview_none(void) { - return (ColoVolatilityBlastPreview){ - .found = 0, - .npc_slot = -1, - .x = 0, - .y = 0, - .size = 0, - .ticks = 0, - .distance = 1 << 30, - }; -} - -static ColoVolatilityBlastPreview col_soonest_queued_volatility_blast( - const ColosseumState* s -) { - ColoVolatilityBlastPreview best = col_volatility_blast_preview_none(); - for (int n = 0; n < COLO_MAX_NPCS; n++) { - const ColoNPC* npc = &s->npcs[n]; - if (!col_npc_is_live_enemy(npc)) continue; - for (int h = 0; h < npc->pending_hits.count; h++) { - const EncounterPendingHit* hit = &npc->pending_hits.hits[h]; - if (!hit->active) continue; - int ticks = hit->ticks_remaining < 0 ? 0 : hit->ticks_remaining; - int damage = 0; - for (int j = 0; j < npc->pending_hits.count; j++) { - const EncounterPendingHit* other = &npc->pending_hits.hits[j]; - if (!other->active) continue; - int other_ticks = - other->ticks_remaining < 0 ? 0 : other->ticks_remaining; - if (other_ticks <= ticks) damage += other->damage; - } - if (damage < npc->hp) continue; - int size = col_npc_effective_size(npc); - int center_x = npc->x + size / 2; - int center_y = npc->y + size / 2; - int dx = abs(center_x - s->player.x); - int dy = abs(center_y - s->player.y); - int distance = dx > dy ? dx : dy; - if (best.found && - (ticks > best.ticks || - (ticks == best.ticks && distance >= best.distance))) - continue; - best = (ColoVolatilityBlastPreview){ - .found = 1, - .npc_slot = n, - .x = npc->x, - .y = npc->y, - .size = size, - .ticks = ticks, - .distance = distance, - }; - } - } - return best; -} - -static int col_step_out_forecast_action_valid( - const ColosseumState* s, - const ColosseumContext* ctx, - int action_idx -) { - if (action_idx == 0) return 1; - if (s->modifiers.draft_pending) return 0; - int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action_idx]; - int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action_idx]; - return col_player_walkable_ctx(s, ctx, nx, ny); -} - -static ColoForecastLanding col_step_out_forecast_action_landing_ctx( - const ColosseumState* s, - const ColosseumContext* ctx, - int action_idx -) { - ColoForecastLanding landing = { - .valid = col_step_out_forecast_action_valid(s, ctx, action_idx), - .land_x = s->player.x, - .land_y = s->player.y, - }; - if (action_idx == 0) return landing; - - int target_dx = ENCOUNTER_MOVE_TARGET_DX[action_idx]; - int target_dy = ENCOUNTER_MOVE_TARGET_DY[action_idx]; - int tx = landing.land_x + target_dx; - int ty = landing.land_y + target_dy; - int abs_dx = target_dx < 0 ? -target_dx : target_dx; - int abs_dy = target_dy < 0 ? -target_dy : target_dy; - int max_steps = abs_dx > abs_dy ? abs_dx : abs_dy; - - for (int step = 0; step < max_steps; step++) { - if (landing.land_x == tx && landing.land_y == ty) break; - int dx = 0; - int dy = 0; - if (tx > landing.land_x) dx = 1; - else if (tx < landing.land_x) dx = -1; - if (ty > landing.land_y) dy = 1; - else if (ty < landing.land_y) dy = -1; - - int moved = 0; - if (dx != 0 && dy != 0 && - col_player_walkable_ctx( - s, ctx, landing.land_x + dx, landing.land_y + dy) && - col_player_walkable_ctx( - s, ctx, landing.land_x + dx, landing.land_y) && - col_player_walkable_ctx( - s, ctx, landing.land_x, landing.land_y + dy)) { - landing.land_x += dx; - landing.land_y += dy; - moved = 1; - } else if (dx != 0 && - col_player_walkable_ctx( - s, ctx, landing.land_x + dx, landing.land_y)) { - landing.land_x += dx; - moved = 1; - } else if (dy != 0 && - col_player_walkable_ctx( - s, ctx, landing.land_x, landing.land_y + dy)) { - landing.land_y += dy; - moved = 1; - } - if (!moved) break; - } - - return landing; -} - -static int col_primary_action_valid( - const ColosseumState* s, - const ColosseumContext* ctx, - int primary_action -) { - if (primary_action < 0 || primary_action >= COLO_PRIMARY_DIM) { - fprintf(stderr, "colosseum primary action out of range: %d\n", primary_action); - abort(); - } - if (primary_action == 0) return 1; - if (s->modifiers.draft_pending && !s->modifiers.draft_free_movement) return 0; - if (primary_action < ENCOUNTER_MOVE_ACTIONS) { - int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[primary_action]; - int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[primary_action]; - return col_player_walkable_ctx(s, ctx, nx, ny); - } - - int obs_slot = col_primary_obs_slot_from_action(primary_action); - int npc_slot = col_obs_slot_to_npc(s, obs_slot); - return npc_slot >= 0 && col_npc_is_live_target(&s->npcs[npc_slot]); -} - diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc index cafd1a85b0..c2b929b894 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc @@ -15,7 +15,7 @@ static void col_resolve_player_pending_hits_ctx(ColosseumState* s, ColosseumCont static void col_remove_npc_corpse( ColosseumState* s, ColosseumContext* ctx, int npc_slot); static void col_rebuild_player_collision_flags(ColosseumState* s); -static void col_refresh_current_obs_slots_ctx(ColosseumState* s, const ColosseumContext* ctx); +static void col_refresh_current_obs_slots(ColosseumState* s); static void col_sol_boss_tick(ColosseumState* s, ColosseumContext* ctx); static void col_sol_begin_boss_arena(ColosseumState* s); @@ -110,26 +110,12 @@ static AttackStyle col_equipped_weapon_attack_style(const Player* p) { return ATTACK_STYLE_MELEE; } -static FightStyle col_equipped_weapon_fight_style(const Player* p) { - AttackStyle style = col_equipped_weapon_attack_style(p); - if (style == ATTACK_STYLE_RANGED) - return FIGHT_STYLE_RAPID; - if (style == ATTACK_STYLE_MAGIC) - return FIGHT_STYLE_ACCURATE; - return FIGHT_STYLE_AGGRESSIVE; -} - static GearSet col_gear_set_for_attack_style(AttackStyle style) { if (style == ATTACK_STYLE_RANGED) return GEAR_RANGED; if (style == ATTACK_STYLE_MAGIC) return GEAR_MAGE; return GEAR_MELEE; } -static int col_equipped_spell_base_damage(const ColosseumState* s) { - if (col_equipped_weapon_attack_style(&s->player) != ATTACK_STYLE_MAGIC) return 0; - return COLO_MAGIC_SPELL_BASE_DAMAGE[s->active_loadout_profile]; -} - static void col_current_style_levels( const Player* p, AttackStyle style, @@ -156,7 +142,9 @@ static void col_mark_live_loadout_dirty(ColosseumState* s) { static void col_recompute_live_loadout_stats(ColosseumState* s) { AttackStyle style = col_equipped_weapon_attack_style(&s->player); - FightStyle fight_style = col_equipped_weapon_fight_style(&s->player); + FightStyle fight_style = style == ATTACK_STYLE_RANGED ? FIGHT_STYLE_RAPID + : style == ATTACK_STYLE_MAGIC ? FIGHT_STYLE_ACCURATE + : FIGHT_STYLE_AGGRESSIVE; int current_att_level; int current_str_level; col_current_style_levels(&s->player, style, ¤t_att_level, ¤t_str_level); @@ -168,7 +156,9 @@ static void col_recompute_live_loadout_stats(ColosseumState* s) { s->player.offensive_prayer, current_att_level, fight_style, - col_equipped_spell_base_damage(s), + style == ATTACK_STYLE_MAGIC + ? COLO_MAGIC_SPELL_BASE_DAMAGE[s->active_loadout_profile] + : 0, &s->loadout_stats_live); encounter_update_loadout_level( &s->loadout_stats_live, @@ -336,15 +326,6 @@ static int col_topology_tile_blocked( return encounter_arena_topology_tile_blocked(ctx->route_topology, x, y); } -static int col_topology_footprint_blocked( - const ColosseumContext* ctx, - int x, - int y, - int size -) { - return encounter_arena_topology_footprint_blocked( - ctx->route_topology, x, y, size); -} static int col_topology_los_clear( const ColosseumContext* ctx, diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc index 141091815d..8ef6d0d13b 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc @@ -164,12 +164,9 @@ static void col_lab_clear_transient( } } -static void col_lab_refresh_geometry( - ColosseumState* s, - const ColosseumContext* ctx -) { +static void col_lab_refresh_geometry(ColosseumState* s) { col_rebuild_player_collision_flags(s); - col_refresh_current_obs_slots_ctx(s, ctx); + col_refresh_current_obs_slots(s); } static void col_lab_remove_all_npcs(ColosseumState* s) { @@ -184,7 +181,7 @@ static void col_lab_clear_npcs( ) { col_lab_remove_all_npcs(s); col_lab_clear_transient(s, ctx); - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); } static const char* col_lab_modifier_name(int mod); @@ -223,7 +220,7 @@ static void col_lab_apply_command( if (p->prayer.kind == ENCOUNTER_LAB_OPTIONAL_INT_SET) s->player.prayer = (OverheadPrayer)p->prayer.value; col_lab_clear_transient(s, ctx); - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } @@ -256,7 +253,7 @@ static void col_lab_apply_command( s->npcs[spawn->slot].attack_timer = spawn->timer.value; } col_lab_clear_transient(s, ctx); - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } @@ -274,7 +271,7 @@ static void col_lab_apply_command( s->npcs[move->slot].target_y = move->y; col_stamp_npc_collision(s, &s->npcs[move->slot], 1); col_lab_clear_transient(s, ctx); - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } @@ -283,7 +280,7 @@ static void col_lab_apply_command( col_lab_require_slot(slot); col_deactivate_npc(s, slot); col_lab_clear_transient(s, ctx); - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } @@ -295,7 +292,7 @@ static void col_lab_apply_command( s->npcs[slot].hp = 0; col_deactivate_npc(s, slot); col_lab_clear_transient(s, ctx); - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } @@ -309,7 +306,7 @@ static void col_lab_apply_command( if (hp > s->npcs[slot].max_hp) col_lab_abort("npc hp %d exceeds max hp %d", hp, s->npcs[slot].max_hp); s->npcs[slot].hp = hp; - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } @@ -321,7 +318,7 @@ static void col_lab_apply_command( col_lab_abort("cannot set timer for inactive npc slot %d", slot); if (timer < 0) col_lab_abort("npc timer must be nonnegative"); s->npcs[slot].attack_timer = timer; - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } @@ -340,7 +337,7 @@ static void col_lab_apply_command( col_spawn_wave(s, ctx); col_lab_clear_transient(s, ctx); - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } @@ -372,7 +369,7 @@ static void col_lab_apply_command( s->sol.force_spear = 1; } s->sol.phase = phase; - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } @@ -404,7 +401,7 @@ static void col_lab_apply_command( s->modifiers.tier[mod] = (uint8_t)tier; } if ((ColoModifier)mod == COLO_MOD_FRAILTY) col_mod_apply_frailty_hp(s); - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } @@ -433,7 +430,7 @@ static void col_lab_apply_command( for (int t = 0; t < ticks; t++) col_step_ctx( (EncounterState*)s, (EncounterContext*)ctx, actions); - col_lab_refresh_geometry(s, ctx); + col_lab_refresh_geometry(s); return; } } diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc index a9b79fbe06..08029440c9 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc @@ -1,5 +1,27 @@ #line 76 "encounter_colosseum.h" +static int col_primary_action_valid( + const ColosseumState* s, + const ColosseumContext* ctx, + int primary_action +) { + if (primary_action < 0 || primary_action >= COLO_PRIMARY_DIM) { + fprintf(stderr, "colosseum primary action out of range: %d\n", primary_action); + abort(); + } + if (primary_action == 0) return 1; + if (s->modifiers.draft_pending && !s->modifiers.draft_free_movement) return 0; + if (primary_action < ENCOUNTER_MOVE_ACTIONS) { + int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[primary_action]; + int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[primary_action]; + return col_player_walkable_ctx(s, ctx, nx, ny); + } + + int obs_slot = col_primary_obs_slot_from_action(primary_action); + int npc_slot = col_obs_slot_to_npc(s, obs_slot); + return npc_slot >= 0 && col_npc_is_live_target(&s->npcs[npc_slot]); +} + static void col_write_mask_ctx( EncounterState* state, EncounterContext* context, @@ -8,7 +30,7 @@ static void col_write_mask_ctx( ColosseumState* s = (ColosseumState*)state; ColosseumContext* ctx = col_require_context(context); encounter_arena_topology_require_finalized(ctx->route_topology); - col_refresh_current_obs_slots_ctx(s, ctx); + col_refresh_current_obs_slots(s); int primary_offset = col_action_head_mask_offset(COLO_HEAD_PRIMARY); for (int action = 0; action < COLO_PRIMARY_DIM; action++) { @@ -27,57 +49,46 @@ static void col_write_mask_ctx( s->player.current_prayer > 0 ? 1.0f : 0.0f; } - int inventory_heads_enabled = !ctx->config.mask_inventory_heads; - int cell_equip_slot[COLO_INVENTORY_DISPLAY_SLOTS]; - int cell_can_eat[COLO_INVENTORY_DISPLAY_SLOTS]; - int cell_can_drink[COLO_INVENTORY_DISPLAY_SLOTS]; - int inventory_has_empty_cell = - osrs_first_empty_inventory_cell(s->player.inventory_cells, -1) >= 0; - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { - cell_equip_slot[cell] = -1; - cell_can_eat[cell] = 0; - cell_can_drink[cell] = 0; - OsrsInventoryClickResolution resolution = - osrs_inventory_cell_click_classify( - &s->player.inventory_cells[cell]); - if (resolution.click_action == OSRS_CLICK_EQUIP) { - const OsrsItemContentMetadata* metadata = - osrs_inventory_cell_metadata( - &s->player.inventory_cells[cell]); - if (osrs_can_equip_metadata( - &s->player, metadata, inventory_has_empty_cell)) { - cell_equip_slot[cell] = metadata->gear_slot; - } - } else if (resolution.click_action == OSRS_CLICK_EAT) { - cell_can_eat[cell] = - col_can_eat_consumable_kind(s, resolution.consumable_kind); - } else if (resolution.click_action == OSRS_CLICK_DRINK) { - cell_can_drink[cell] = osrs_inventory_cell_dose_count( - &s->player.inventory_cells[cell]) > 0 && - s->player.potion_timer == 0 && - col_drink_has_effect(s, resolution.consumable_kind); - } - } - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - int head_offset = - col_action_head_mask_offset(COLO_HEAD_EQUIP_SLOT(slot)); - mask[head_offset] = 1.0f; + int inventory_offset = + col_action_head_mask_offset(COLO_HEAD_EQUIP_BASE); + memset(&mask[inventory_offset], 0, + COLO_INV_CLICK_HEADS * COLO_INV_CLICK_DIM * sizeof(*mask)); + for (int head = 0; head < COLO_INV_CLICK_HEADS; head++) + mask[inventory_offset + head * COLO_INV_CLICK_DIM] = 1.0f; + + if (!ctx->config.mask_inventory_heads) { + int eat_offset = col_action_head_mask_offset(COLO_HEAD_EAT); + int drink_offset = col_action_head_mask_offset(COLO_HEAD_DRINK); + int inventory_has_empty_cell = + osrs_first_empty_inventory_cell(s->player.inventory_cells, -1) >= 0; for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { - mask[head_offset + cell + 1] = - inventory_heads_enabled && cell_equip_slot[cell] == slot - ? 1.0f : 0.0f; + const OsrsInventoryCell* inventory_cell = + &s->player.inventory_cells[cell]; + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_classify(inventory_cell); + if (resolution.click_action == OSRS_CLICK_EQUIP) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(inventory_cell); + if (metadata->gear_slot >= 0 && + metadata->gear_slot < NUM_GEAR_SLOTS && + osrs_can_equip_metadata( + &s->player, metadata, inventory_has_empty_cell)) { + int offset = col_action_head_mask_offset( + COLO_HEAD_EQUIP_SLOT(metadata->gear_slot)); + mask[offset + cell + 1] = 1.0f; + } + } else if (resolution.click_action == OSRS_CLICK_EAT && + osrs_can_eat_consumable_kind( + &s->player, resolution.consumable_kind)) { + mask[eat_offset + cell + 1] = 1.0f; + } else if (resolution.click_action == OSRS_CLICK_DRINK && + osrs_inventory_cell_dose_count(inventory_cell) > 0 && + s->player.potion_timer == 0 && + col_drink_has_effect(s, resolution.consumable_kind)) { + mask[drink_offset + cell + 1] = 1.0f; + } } } - int eat_offset = col_action_head_mask_offset(COLO_HEAD_EAT); - int drink_offset = col_action_head_mask_offset(COLO_HEAD_DRINK); - mask[eat_offset] = 1.0f; - mask[drink_offset] = 1.0f; - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { - mask[eat_offset + cell + 1] = - inventory_heads_enabled && cell_can_eat[cell] ? 1.0f : 0.0f; - mask[drink_offset + cell + 1] = - inventory_heads_enabled && cell_can_drink[cell] ? 1.0f : 0.0f; - } int spell_offset = col_action_head_mask_offset(COLO_HEAD_SPELL); mask[spell_offset + OSRS_SPELL_NONE] = 1.0f; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc index 67fdde4d86..b016a13ae9 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -34,37 +34,6 @@ static inline int col_wave_entry_delay_ticks(int target_wave) { #define COLO_MAX_NPCS 24 #define COLO_OBS_NPCS COLO_MAX_NPCS -typedef enum { - COLO_FORECAST_BEHAVIOR_STANDARD = 0, - COLO_FORECAST_BEHAVIOR_WARBAND, - COLO_FORECAST_BEHAVIOR_MANTICORE, - COLO_FORECAST_BEHAVIOR_SOL, -} ColoForecastBehaviorKind; - -typedef struct { - int active; - ColoNpcType type; - ColoForecastBehaviorKind forecast_behavior; - int x; - int y; - int size; - int hp; - int attack_range; - int attack_speed; - int can_move; - AttackStyle default_style; - int attack_timer; - int stun_timer; - int frozen_ticks; - int death_ticks; - int cycle_step; - AttackStyle orb_style[3]; - AttackStyle fixed_orb_style[3]; - int formation_dir; - int javelin_attack_count; - int sol_attack_delay; - int sol_immobile_ticks; -} ColoForecastNpcLocal; typedef struct { uint8_t lo, hi; } ColoWallSpan; #define COLO_WALL_SPANS_PER_ROW 3 @@ -1326,28 +1295,14 @@ typedef struct { ColoNpcType volatility_explosion_type[COLO_MAX_NPCS]; } ColosseumContext; -/* Clears only what was written. col_render_hit_events_add aborts past - * ENCOUNTER_RENDER_HITS_MAX and fills strictly below count, and a row whose count is zero - * is already zero from the previous clear, so the old blanket memset of - * COLO_MAX_NPCS x ENCOUNTER_RENDER_HITS_MAX ints re-zeroed memory that was already zero. - * That was 3488 B per tick per env and 15% of the whole env step. */ static inline void col_context_clear_render_events(ColosseumContext* ctx) { memset(ctx->npc_render_attack_anim, 0, sizeof(ctx->npc_render_attack_anim)); memset(ctx->manticore_charge_started, 0, sizeof(ctx->manticore_charge_started)); - for (int h = 0; h < ctx->player_render_hit_count; h++) - ctx->player_render_hit_damage[h] = 0; ctx->player_render_hit_count = 0; - for (int i = 0; i < COLO_MAX_NPCS; i++) { - int n = ctx->npc_render_hit_count[i]; - if (n <= 0) continue; - for (int h = 0; h < n; h++) ctx->npc_render_hit_damage[i][h] = 0; - ctx->npc_render_hit_count[i] = 0; - } + memset(ctx->npc_render_hit_count, 0, sizeof(ctx->npc_render_hit_count)); ctx->thrall_attacked_this_tick = 0; ctx->player_spec_fired_kind = 0; ctx->player_venator_chain_count = 0; - for (int i = 0; i < OSRS_VENATOR_MAX_CHAIN_HITS; i++) - ctx->player_venator_chain_slots[i] = -1; ctx->volatility_explosion_count = 0; } @@ -1358,8 +1313,6 @@ static inline void col_context_record_venator_chain( if (!chain) abort(); osrs_venator_validate_chain_length(chain->length); ctx->player_venator_chain_count = (int)chain->length; - for (int i = 0; i < OSRS_VENATOR_MAX_CHAIN_HITS; i++) - ctx->player_venator_chain_slots[i] = -1; for (int i = 0; i < (int)chain->length; i++) ctx->player_venator_chain_slots[i] = chain->hits[i].slot; } @@ -1560,8 +1513,6 @@ typedef struct { int player_dest_x, player_dest_y; uint8_t npc_collision_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; - /* The player occupies one tile, so a full arena grid recorded one cell and was - * memset in full twice per tick. Cache the cell instead. -1 means off-grid. */ int player_grid_x, player_grid_y; int start_wave; @@ -1570,10 +1521,6 @@ typedef struct { float active_player_damage_received_scale; int profiling_suppressed; - /* Last two members, and both are skipped by probe_colo_sim_invariant. The probe hashes - * this struct's raw bytes, so a member that grows mid-struct repads everything after it - * and moves all twelve digests with no behaviour change -- which is exactly what adding - * reward telemetry to the log did. At the tail, neither can shift a hashed byte. */ ColosseumLog log; ColoObsMemoCaches obs_memos; } ColosseumState; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc index 65817f5b1c..b4fca4961b 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc @@ -211,13 +211,6 @@ typedef struct { ColoWaveEntrySample samples[COLO_WAVE_ENTRY_RESERVOIR_CAP]; } ColoWaveEntryReservoir; -/* Per worker thread, not global. A shared reservoir is order dependent by - construction: res->seen and the slot draw both advance in whichever order - threads happen to clear waves, and locking fixes tearing but not order. Envs - are partitioned schedule(static) (src/pufferl.cu:1040,1334), so each thread - sees a fixed env subset in a fixed order and its reservoir is reproducible. - Donors are spread across threads because start_wave is drawn per env, so a - thread's ~256 envs still hold ~80 wave-1 donors to sample from. */ static OSRS_THREAD_LOCAL ColoWaveEntryReservoir g_colo_wave_entry_res[COLO_NUM_WAVES]; static OSRS_THREAD_LOCAL uint32_t g_colo_wave_entry_rng = 0x9e3779b9u; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc index 4c3d55384a..645f497969 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc @@ -17,12 +17,6 @@ static int col_player_walkable_ctx( return 1; } -static int col_player_walkable(void* data, int x, int y) { - const ColoGeometryContext* geometry = - (const ColoGeometryContext*)data; - return col_player_walkable_ctx( - geometry->state, geometry->context, x, y); -} static int col_topology_los_tile_blocked(void* data, int x, int y) { const ColosseumContext* context = (const ColosseumContext*)data; @@ -390,11 +384,6 @@ static void col_npc_move_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) npc->target_x = tx; npc->target_y = ty; - /* Both hold paths decide from footprint geometry and the static LOS table, neither of - * which reads npc_collision_flags, so they are checked before the clear. An NPC that was - * never going to move now touches the grid zero times instead of erasing and redrawing - * its own footprint. Measured before this: 27.9 stamp writes per tick against 7.0 blocked - * probes, with most NPCs holding most ticks behind the attack_timer gate. */ int is_ranged = st->attack_range > 1; if (is_ranged) { int dist = encounter_dist_to_npc(s->player.x, s->player.y, npc->x, npc->y, size); diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc index 3e9fc9392d..3950e4e50f 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -369,7 +369,7 @@ static inline void col_write_nearest_hazard_xy_value( s, nearest->found, nearest->x, nearest->y, value, obs, idx); } -static void col_refresh_current_obs_slots_ctx(ColosseumState* s, const ColosseumContext* ctx) { +static void col_refresh_current_obs_slots(ColosseumState* s) { for (int i = 0; i < COLO_OBS_NPCS; i++) s->current_obs_slots[i] = -1; int slot = 0; for (int i = 0; i < COLO_MAX_NPCS && slot < COLO_OBS_NPCS; i++) { @@ -638,9 +638,6 @@ static void col_write_npc_slot( const ColoNpcStats* ns = &COLO_NPC_STATS[npc->type]; int size = col_npc_effective_size(npc); - /* type+1, so 0 stays "empty slot" from the memset. The entity encoder expands this - * back to the same one-hot on the GPU, so the network sees exactly what it did before - * while the observation carries 1 float per slot instead of COLO_NUM_NPC_TYPES. */ obs[i++] = (float)(npc->type + 1); obs[i++] = col_obs_rel_x(npc->x, px); obs[i++] = col_obs_rel_y(npc->y, py); @@ -698,29 +695,46 @@ static int col_sol_premove_molten_hit(const ColosseumState* s) { return 0; } -static int col_premove_molten_forecast_needed( - const ColosseumState* s +static int col_premove_molten_burn( + const ColosseumState* s, + const ColosseumContext* ctx ) { - if (s->molten_count > 0) return 1; - if (col_mod_tier(s, COLO_MOD_REENTRY) == 0) return 0; - for (int n = 0; n < COLO_MAX_NPCS; n++) { - const ColoNPC* npc = &s->npcs[n]; - if (!col_npc_is_live_target(npc) || - npc->type != COLO_JAVELIN_COLOSSUS) { - continue; + int imminent_skyfall = 0; + if (col_mod_tier(s, COLO_MOD_REENTRY) > 0) { + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (!col_npc_is_live_target(npc) || + npc->type != COLO_JAVELIN_COLOSSUS) { + continue; + } + const ColoJavelinState* jv = &npc->type_state.javelin; + if (jv->skyfall_pending && jv->skyfall_timer <= 1) { + imminent_skyfall = 1; + break; + } } - const ColoJavelinState* jv = &npc->type_state.javelin; - if (jv->skyfall_pending && jv->skyfall_timer <= 1) return 1; } - return 0; -} -static int col_build_post_premove_molten_forecast( - const ColosseumState* s, - const ColosseumContext* ctx, - ColosseumState* forecast -) { - *forecast = *s; + if (!imminent_skyfall) { + for (int pool = 0; pool < s->molten_count; pool++) + if (s->molten_x[pool] == s->player.x && + s->molten_y[pool] == s->player.y && + s->molten_burn_timer[pool] == 0) + return 1; + return 0; + } + + ColosseumState forecast; + forecast.modifiers = s->modifiers; + forecast.rng_state = s->rng_state; + forecast.molten_count = s->molten_count; + size_t molten_bytes = (size_t)s->molten_count * sizeof(s->molten_x[0]); + memcpy(forecast.molten_x, s->molten_x, molten_bytes); + memcpy(forecast.molten_y, s->molten_y, molten_bytes); + memcpy(forecast.molten_kind, s->molten_kind, molten_bytes); + memcpy(forecast.molten_lifetime, s->molten_lifetime, molten_bytes); + memcpy(forecast.molten_burn_timer, s->molten_burn_timer, molten_bytes); + for (int n = 0; n < COLO_MAX_NPCS; n++) { const ColoNPC* npc = &s->npcs[n]; if (!col_npc_is_live_target(npc) || @@ -730,27 +744,16 @@ static int col_build_post_premove_molten_forecast( const ColoJavelinState* jv = &npc->type_state.javelin; if (!jv->skyfall_pending || jv->skyfall_timer > 1) continue; col_mod_reentry_on_skyfall( - forecast, ctx->route_topology, + &forecast, ctx->route_topology, jv->skyfall_tile_x, jv->skyfall_tile_y); } - int burned = 0; - for (int pool = 0; pool < forecast->molten_count; pool++) { - int player_on_pool = - forecast->molten_x[pool] == forecast->player.x && - forecast->molten_y[pool] == forecast->player.y; - if (!burned && - player_on_pool && - forecast->molten_burn_timer[pool] == 0) { - forecast->molten_burn_timer[pool] = - COLO_REENTRY_MOLTEN_BURN_INTERVAL - 1; - burned = 1; - continue; - } - if (forecast->molten_burn_timer[pool] > 0) - forecast->molten_burn_timer[pool]--; - } - return burned; + for (int pool = 0; pool < forecast.molten_count; pool++) + if (forecast.molten_x[pool] == s->player.x && + forecast.molten_y[pool] == s->player.y && + forecast.molten_burn_timer[pool] == 0) + return 1; + return 0; } typedef struct { @@ -992,6 +995,60 @@ static void col_write_nearest_modifier_solarflare( *idx = i; } +typedef struct { + int found; + int x; + int y; + int size; + int ticks; + int distance; +} ColoVolatilityBlastPreview; + +static ColoVolatilityBlastPreview col_soonest_queued_volatility_blast( + const ColosseumState* s +) { + ColoVolatilityBlastPreview best = { + .distance = 1 << 30, + }; + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (!col_npc_is_live_enemy(npc)) continue; + for (int h = 0; h < npc->pending_hits.count; h++) { + const EncounterPendingHit* hit = &npc->pending_hits.hits[h]; + if (!hit->active) continue; + int ticks = hit->ticks_remaining < 0 ? 0 : hit->ticks_remaining; + int damage = 0; + for (int j = 0; j < npc->pending_hits.count; j++) { + const EncounterPendingHit* other = &npc->pending_hits.hits[j]; + if (!other->active) continue; + int other_ticks = + other->ticks_remaining < 0 ? 0 : other->ticks_remaining; + if (other_ticks <= ticks) damage += other->damage; + } + if (damage < npc->hp) continue; + int size = col_npc_effective_size(npc); + int center_x = npc->x + size / 2; + int center_y = npc->y + size / 2; + int dx = abs(center_x - s->player.x); + int dy = abs(center_y - s->player.y); + int distance = dx > dy ? dx : dy; + if (best.found && + (ticks > best.ticks || + (ticks == best.ticks && distance >= best.distance))) + continue; + best = (ColoVolatilityBlastPreview){ + .found = 1, + .x = npc->x, + .y = npc->y, + .size = size, + .ticks = ticks, + .distance = distance, + }; + } + } + return best; +} + static void col_write_volatility_blast_previews( const ColosseumState* s, float* obs, @@ -1637,28 +1694,18 @@ static int col_weapon_set_is_argmax_dpt_for_npc( return 1; } -/* Sampled on attack ticks only. Reading it on every tick that merely has a target fired - * the best-gear oracle ~4x more often than the reward that motivated the metric. */ static int col_current_set_is_argmax_dpt_for_target(ColosseumState* s) { if (!s->tick_scratch.player_attacked) return -1; - int target = col_player_target_slot(s); - if (target < 0 || target >= COLO_MAX_NPCS) return -1; - const ColoNPC* npc = &s->npcs[target]; - if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) - return -1; const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = col_get_best_gear_table(s); - return col_weapon_set_is_argmax_dpt_for_npc(s, target, s->weapon_set, best); + return col_weapon_set_is_argmax_dpt_for_npc( + s, col_player_target_slot(s), s->weapon_set, best); } static int col_attacked_with_argmax_set(ColosseumState* s) { if (!s->tick_scratch.player_attacked) return -1; - int npc_slot = s->player_attack_npc_idx; - if (npc_slot < 0 || npc_slot >= COLO_MAX_NPCS) return -1; - const ColoNPC* npc = &s->npcs[npc_slot]; - if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) - return -1; const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = col_get_best_gear_table(s); - return col_weapon_set_is_argmax_dpt_for_npc(s, npc_slot, s->weapon_set, best); + return col_weapon_set_is_argmax_dpt_for_npc( + s, s->player_attack_npc_idx, s->weapon_set, best); } @@ -1674,14 +1721,8 @@ static void col_write_obs_ctx( int col_prof_enabled = COLO_PROFILE_ENABLED(); double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; #endif - col_refresh_current_obs_slots_ctx(s, ctx); - ColosseumState post_premove_molten; - int premove_molten_burn = 0; - if (col_premove_molten_forecast_needed(s)) { - premove_molten_burn = - col_build_post_premove_molten_forecast( - s, ctx, &post_premove_molten); - } + col_refresh_current_obs_slots(s); + int premove_molten_burn = col_premove_molten_burn(s, ctx); #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_OBS_REFRESH_SLOTS); #endif diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc index 195c86dd57..34f19c5a3e 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -194,11 +194,6 @@ static void col_sync_weapon_set_from_equipped_weapon(ColosseumState* s) { s->player.visible_gear = s->player.current_gear; } -static int col_can_equip_from_cell(const ColosseumState* s, int cell_idx) { - return osrs_can_equip_from_cell( - &s->player, s->player.inventory_cells, cell_idx); -} - static void col_equip_from_cell(ColosseumState* s, int cell_idx) { if (osrs_equip_from_cell( &s->player, s->player.inventory_cells, cell_idx) < 0) return; @@ -206,13 +201,6 @@ static void col_equip_from_cell(ColosseumState* s, int cell_idx) { col_mark_live_loadout_dirty(s); } -static int col_can_eat_consumable_kind( - const ColosseumState* s, - OsrsConsumableKind kind -) { - return osrs_can_eat_consumable_kind(&s->player, kind); -} - #define COLO_BOOST_REDRINK_LEVEL 105 static int col_restore_worth_drinking(const ColosseumState* s) { @@ -254,24 +242,6 @@ static int col_drink_has_effect(const ColosseumState* s, OsrsConsumableKind kind } } -static int col_inventory_cell_actionable(const ColosseumState* s, int cell_idx) { - const ColoInvCell* cell = &s->player.inventory_cells[cell_idx]; - OsrsInventoryClickResolution r = - osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); - if (r.click_action == OSRS_CLICK_EQUIP) return col_can_equip_from_cell(s, cell_idx); - if (r.click_action == OSRS_CLICK_DRINK) - return osrs_inventory_cell_dose_count(cell) > 0 && - s->player.potion_timer == 0 && - col_drink_has_effect(s, r.consumable_kind); - if (r.click_action == OSRS_CLICK_EAT) - return col_can_eat_consumable_kind(s, r.consumable_kind); - return 0; -} - -static void col_empty_inventory_cell(ColosseumState* s, int cell_idx) { - s->player.inventory_cells[cell_idx] = osrs_inventory_cell_empty(); -} - static void col_heal_player_capped(ColosseumState* s, int amount) { s->player.current_hitpoints += amount; if (s->player.current_hitpoints > s->player.base_hitpoints) @@ -435,7 +405,8 @@ static void col_apply_inventory_tick_intent( break; case OSRS_INVENTORY_APPLY_EAT: if (col_apply_food_cell(s, step.resolution.consumable_kind)) - col_empty_inventory_cell(s, step.cell_idx); + s->player.inventory_cells[step.cell_idx] = + osrs_inventory_cell_empty(); break; case OSRS_INVENTORY_APPLY_DRINK: (void)col_apply_drink_cell(s, step.cell_idx, step.resolution); @@ -496,15 +467,6 @@ static void col_tick_thrall_and_death_charge(ColosseumState* s, ColosseumContext } } -typedef struct { - ColosseumState* s; - const ColosseumContext* ctx; -} ColoWalkCtx; - -static int col_tile_walkable(void* ctx, int x, int y) { - ColoWalkCtx* wc = (ColoWalkCtx*)ctx; - return col_player_walkable_ctx(wc->s, wc->ctx, x, y); -} static int col_player_route_blocked(void* data, int x, int y, int size) { ColosseumState* state = (ColosseumState*)data; @@ -516,11 +478,6 @@ static int col_player_route_blocked(void* data, int x, int y, int size) { } -static int col_npc_collision_flag_at(const ColosseumState* s, int x, int y) { - int gx, gy; - if (!col_grid_index(x, y, &gx, &gy)) return 0; - return s->npc_collision_flags[gx][gy] != 0; -} static int col_player_attack_range(ColosseumState* s) { const EncounterLoadoutStats* ls = col_live_loadout_stats(s); @@ -556,11 +513,6 @@ static MeleeStyle col_item_melee_style(uint8_t weapon_item) { return style; } -static int col_weapon_has_item_effect(uint8_t weapon_item, uint32_t effect_mask) { - const Item* weapon = get_item(weapon_item); - return weapon && (weapon->effect_mask & effect_mask) != 0; -} - static int col_npc_effective_def_level(const ColoNPC* npc, const ColoNpcStats* ns) { int def = ns->def_level - npc->def_drained; return def < 0 ? 0 : def; @@ -884,6 +836,7 @@ static void col_player_attack_target_ctx( const OsrsEquipmentEffectProfile* effects = col_live_effects(s); uint8_t weapon_item = s->player.equipped[GEAR_SLOT_WEAPON]; + const Item* weapon = get_item(weapon_item); int projectile_dist = encounter_projectile_distance( s->player.x, s->player.y, 1, npc->x, npc->y, col_npc_effective_size(npc), @@ -903,9 +856,8 @@ static void col_player_attack_target_ctx( OsrsVenatorChain venator_chain = { OSRS_VENATOR_CHAIN_LENGTH_ONE }; int resolved_count = 1; resolved_hits[0] = (ColScytheResolvedHit){ .npc_slot = npc_slot, .splat_rank = 0 }; - int venator_attack = - !col_type_is_hazard_entity(npc->type) && - col_weapon_has_item_effect(weapon_item, OSRS_ITEM_EFFECT_VENATOR_BOUNCE); + int venator_attack = !col_type_is_hazard_entity(npc->type) && + weapon && (weapon->effect_mask & OSRS_ITEM_EFFECT_VENATOR_BOUNCE); if (venator_attack) { venator_chain = col_resolve_venator_chain(s, npc_slot); resolved_count = (int)venator_chain.length; @@ -978,28 +930,17 @@ static void col_player_attack_target_ctx( } -typedef struct { - OsrsPlayerStepInput input; - int move_action; -} ColoPrimaryStepPlan; - -static ColoPrimaryStepPlan col_primary_step_plan( +static OsrsPlayerStepInput col_primary_step_input( ColosseumState* s, ColosseumContext* ctx, - Player* player, - OsrsInteraction* interaction, int primary_action, - int* player_dest_x, - int* player_dest_y, - ColoWalkCtx* walk_ctx, - OsrsLosQuery* los_query + const OsrsLosQuery* los_query ) { if (primary_action < 0 || primary_action >= COLO_PRIMARY_DIM) { fprintf(stderr, "colosseum primary action out of range: %d\n", primary_action); abort(); } - int has_new_target = 0; int new_target_slot = -1; int move_action = 0; if (primary_action > 0 && primary_action < ENCOUNTER_MOVE_ACTIONS) { @@ -1007,54 +948,47 @@ static ColoPrimaryStepPlan col_primary_step_plan( } else if (primary_action >= COLO_PRIMARY_ATTACK_BASE) { int obs_slot = col_primary_obs_slot_from_action(primary_action); int npc_slot = col_obs_slot_to_npc(s, obs_slot); - if (npc_slot >= 0 && s->npcs[npc_slot].active) { - has_new_target = 1; + if (npc_slot >= 0 && s->npcs[npc_slot].active) new_target_slot = npc_slot; - } } OsrsPlayerCommand command = { .kind = OSRS_PLAYER_CMD_NONE }; - if (has_new_target) { + if (new_target_slot >= 0) { command.kind = OSRS_PLAYER_CMD_TARGET; command.target_slot = new_target_slot; - } else if (*player_dest_x >= 0) { + } else if (s->player_dest_x >= 0) { command.kind = OSRS_PLAYER_CMD_MOVE; command.move_kind = OSRS_PLAYER_MOVE_DESTINATION; - } else if (move_action > 0 && move_action < ENCOUNTER_MOVE_ACTIONS) { + } else if (move_action > 0) { command.kind = OSRS_PLAYER_CMD_MOVE; command.move_kind = OSRS_PLAYER_MOVE_ACTION; command.move_action = move_action; } - *walk_ctx = (ColoWalkCtx){s, ctx}; - *los_query = col_player_los_query(ctx); - return (ColoPrimaryStepPlan){ - .input = { - .player = player, - .interaction = interaction, - .route_cache = &ctx->player_route_cache, - .target_lookup = col_lookup_player_attack_target, - .target_ctx = s, - .command = command, - .dest_x = player_dest_x, - .dest_y = player_dest_y, - .arena = { - .topology = ctx->route_topology, - .blockers = { - .is_blocked = col_player_route_blocked, - .ctx = s, - .revision = (uint64_t)col_sol_clamp_active(s) + 1, - }, - .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, - .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, - .destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, - .collision_map = ctx->collision_map, - .world_offset_x = ctx->world_offset_x, - .world_offset_y = ctx->world_offset_y, - .los_query = los_query, + return (OsrsPlayerStepInput){ + .player = &s->player, + .interaction = &s->interaction, + .route_cache = &ctx->player_route_cache, + .target_lookup = col_lookup_player_attack_target, + .target_ctx = s, + .command = command, + .dest_x = &s->player_dest_x, + .dest_y = &s->player_dest_y, + .arena = { + .topology = ctx->route_topology, + .blockers = { + .is_blocked = col_player_route_blocked, + .ctx = s, + .revision = (uint64_t)col_sol_clamp_active(s) + 1, }, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, + .destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + .collision_map = ctx->collision_map, + .world_offset_x = ctx->world_offset_x, + .world_offset_y = ctx->world_offset_y, + .los_query = los_query, }, - .move_action = move_action, }; } @@ -1121,17 +1055,14 @@ static void col_tick_player_ctx( COLO_PROFILE_MARK(COLO_PROF_PLAYER_INTENT); #endif - int primary_act = actions[COLO_HEAD_PRIMARY]; - ColoWalkCtx walk_ctx; - OsrsLosQuery los_query; - ColoPrimaryStepPlan primary_plan = col_primary_step_plan( - s, ctx, &s->player, &s->interaction, primary_act, - &s->player_dest_x, &s->player_dest_y, &walk_ctx, &los_query); + OsrsLosQuery los_query = col_player_los_query(ctx); + OsrsPlayerStepInput primary_input = col_primary_step_input( + s, ctx, actions[COLO_HEAD_PRIMARY], &los_query); OsrsPlayerStepResult step_result = - osrs_encounter_player_step(&primary_plan.input); - if (primary_plan.input.command.kind == OSRS_PLAYER_CMD_TARGET && - step_result.target_slot == primary_plan.input.command.target_slot) { - s->player_last_interaction_target_slot = primary_plan.input.command.target_slot; + osrs_encounter_player_step(&primary_input); + if (primary_input.command.kind == OSRS_PLAYER_CMD_TARGET && + step_result.target_slot == primary_input.command.target_slot) { + s->player_last_interaction_target_slot = primary_input.command.target_slot; s->player_last_interaction_age = 0; } col_rebuild_player_collision_flags(s); @@ -1151,7 +1082,7 @@ static void col_tick_player_ctx( npc->x, npc->y, col_npc_effective_size(npc), player_range, ctx->collision_map, ctx->world_offset_x, ctx->world_offset_y, - &los_query)) { + primary_input.arena.los_query)) { int armed_kind = (!col_type_is_hazard_entity(npc->type) && s->player.spec_armed) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 1054fd3fcc..6976d5f1e5 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -172,8 +172,6 @@ static void* col_get_log_ctx(EncounterState* state, EncounterContext* context) { s->log.episode_length = s->tick; s->log.wave_reached = s->wave; s->log.win = (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_WON) ? 1 : 0; - /* the tick cap reports PLAYER_DIED so training keeps timeout-as-terminal, but a - truncation has no killer and no damage source, so it must not count as a death. */ s->log.timed_out = (s->episode_over && s->time_limit_truncated) ? 1 : 0; s->log.died = (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_DIED && !s->time_limit_truncated) ? 1 : 0; @@ -955,8 +953,8 @@ static int col_is_human_targetable_npc_slot_ctx( int npc_slot ) { ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = col_require_context(context); - col_refresh_current_obs_slots_ctx(s, ctx); + col_require_context(context); + col_refresh_current_obs_slots(s); int obs_slot = col_find_target_obs_slot(s, npc_slot); if (obs_slot < 0) return 0; return col_obs_slot_is_targetable(s, obs_slot); @@ -1008,8 +1006,8 @@ static void col_translate_human_input_ctx( encounter_translate_prayer(hi, actions, COLO_HEAD_PRAYER); ColosseumState* s = (ColosseumState*)state; - ColosseumContext* ctx = col_require_context(context); - col_refresh_current_obs_slots_ctx(s, ctx); + col_require_context(context); + col_refresh_current_obs_slots(s); if (hi->pending_target_idx >= 0) { int found_slot = col_find_target_obs_slot(s, hi->pending_target_idx); @@ -1026,14 +1024,13 @@ static void col_translate_human_input_ctx( col_apply_pending_draft_grapple_heads(hi, actions); } -static void col_translate_human_commands_ctx( +static void col_translate_human_commands( HumanInput* hi, int* actions, - ColosseumState* s, - ColosseumContext* ctx + ColosseumState* s ) { for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; - col_refresh_current_obs_slots_ctx(s, ctx); + col_refresh_current_obs_slots(s); for (int i = 0; i < hi->commands.count; i++) { const HumanCommand* cmd = &hi->commands.items[i]; @@ -1099,7 +1096,7 @@ static void col_step_human_commands_ctx( ctx->human_command_mode = 1; ctx->human_commands = hi->commands.items; ctx->human_command_count = hi->commands.count; - col_translate_human_commands_ctx(hi, actions, s, ctx); + col_translate_human_commands(hi, actions, s); col_step_ctx(state, (EncounterContext*)ctx, actions); ctx->human_commands = NULL; ctx->human_command_count = 0; @@ -1125,7 +1122,7 @@ static void col_refresh_after_state_load(ColosseumState* s, ColosseumContext* ct col_mark_live_loadout_dirty(s); col_ensure_live_loadout_stats(s); col_rebuild_player_collision_flags(s); - col_refresh_current_obs_slots_ctx(s, ctx); + col_refresh_current_obs_slots(s); } static size_t col_snapshot_size_ctx(EncounterState* state, EncounterContext* context) { diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc index 95d40586ad..3cb768c6e5 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -64,8 +64,6 @@ static void col_deactivate_npc(ColosseumState* s, int idx) { if (!col_type_is_hazard_entity(npc->type) && !col_type_is_warbander(npc->type)) col_stamp_npc_collision(s, npc, 0); npc->active = 0; - /* The per-tick reset skips inactive slots, so a slot leaving play clears its own tick - * flags once here instead of every tick forever. */ npc->attacked_this_tick = 0; npc->attack_style_this_tick = ATTACK_STYLE_NONE; npc->moved_this_tick = 0; diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc index 4709ffd58b..a84e975c3a 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc @@ -5,10 +5,6 @@ static float col_expected_dpt_for_equipment_vs_npc( ColosseumState* s, const uint8_t equipment[NUM_GEAR_SLOTS], const ColoNPC* npc, int use_style_matched_prayer); -/* The trainer hard-clamps every per-step reward to [-1,1] before PPO, so any tick whose - * terms sum past 1 delivers a signal the agent never sees. Measured here rather than - * bounded per-coefficient, because the terms stack: a wave-12 clear carries the killing - * blow's damage reward, wave_clear_bonus and win_bonus on one tick. */ static void col_record_reward_clamp_headroom(ColosseumState* s) { float mag = fabsf(s->reward); s->log.reward_steps += 1.0f; @@ -18,8 +14,6 @@ static void col_record_reward_clamp_headroom(ColosseumState* s) { s->log.reward_clamped_steps += 1.0f; s->log.reward_clamp_loss += over; if (s->tick_scratch.wave_completed) s->log.clamp_loss_wave_clear += over; - /* episode_over guards the winner read: COLO_OUTCOME_PLAYER_WON is 0, which is also - * what winner holds on every non-terminal tick. */ if (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_WON) s->log.clamp_loss_win += over; } @@ -29,10 +23,6 @@ static float col_terminal_death_reward(ColosseumState* s, const ColosseumContext return -ctx->config.death_penalty_coeff; } -static float col_terminal_timeout_reward(ColosseumState* s, const ColosseumContext* ctx) { - s->log.rew_timeout -= ctx->config.timeout_penalty; - return -ctx->config.timeout_penalty; -} static void col_update_min_sol_hp_seen(ColosseumState* s) { if (!s->sol.started) return; @@ -82,9 +72,6 @@ static float col_current_wave_score_progress(const ColosseumState* s) { return progress; } -/* A loss caps here, a win scores 1.0. At the old 0.99 a policy that always reached - Sol's last phase and never won scored ~0.973, so winning was worth under 3% and the - sweep optimised depth instead. */ #define COLO_SCORE_LOSS_CEILING 0.5f static float col_episode_outcome_score(const ColosseumState* s) { @@ -112,9 +99,6 @@ static float col_episode_max_wave_depth(const ColosseumState* s) { col_sol_score_progress(s); } -/* Damage the player was thrown, could have prayed, and did pray away this tick. Only one - * overhead can be up, so `best` is the most any policy could have stopped and the remainder - * is structurally unrecoverable -- crediting it would pay for luck, not play. */ typedef struct { float total; float best; @@ -134,9 +118,6 @@ static ColoTickAvoidance col_tick_avoidance(const ColoTickScratch* t) { return a; } -/* Split from the reward so the three terminal paths, which compute a shaped reward and then - * throw it away in favour of a terminal penalty, still record their tick statistics without - * crediting reward the agent never receives. */ static void col_accumulate_tick_stats(ColosseumState* s, const ColosseumContext* ctx) { const ColConfig* cfg = &ctx->config; ColoTickScratch* t = &s->tick_scratch; @@ -156,10 +137,6 @@ static void col_accumulate_tick_stats(ColosseumState* s, const ColosseumContext* if (s->wave < cfg->farm_cap_waves) s->log.farm_damage += t->fresh_damage_reinforcement; - /* Headroom split for a defensive reward. Only ONE overhead can be up, so on a tick where - * two styles land only the larger can ever be prayed: the remainder is structurally - * unavoidable and no reward shape can recover it. Separating that from what the policy - * simply misplayed is the number that decides whether a defensive term is worth building. */ ColoTickAvoidance av = col_tick_avoidance(t); s->log.avoid_total += av.total; s->log.avoid_achieved += av.achieved; @@ -168,9 +145,6 @@ static void col_accumulate_tick_stats(ColosseumState* s, const ColosseumContext* s->log.dmg_unprayable += t->landed_unprayable_damage; } -/* Returned by value rather than logged in place: a tick that ends in death, timeout or a win - * overwrites the shaped reward with a terminal one, and the per-term log must record what was - * actually paid. col_commit_reward_terms is the single place that credits the log. */ typedef struct { float damage; float boss_phase; @@ -195,14 +169,6 @@ static ColoShapedReward col_shaped_reward(ColosseumState* s, const ColosseumCont return out; } -static float col_shaped_total(ColoShapedReward r) { - return r.damage + r.boss_phase; -} - -static void col_commit_reward_terms(ColosseumState* s, ColoShapedReward r) { - s->log.rew_damage += r.damage; - s->log.rew_boss_phase += r.boss_phase; -} static float col_inventory_heal_remaining(const ColosseumState* s) { float total = 0.0f; @@ -288,9 +254,6 @@ static ColoPrePlayerPhase col_advance_pre_player_phase_ctx( #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_PRE_RESET_PLAYER); #endif - /* Mean live NPCs is 4.4 of 24, and the tick-flag fields sit in their own cache line - * per NPC, so clearing all 24 touched 24 lines to reset ~4. col_deactivate_npc clears - * a slot on the way out, so skipping inactive slots here is equivalent. */ for (int i = 0; i < COLO_MAX_NPCS; i++) { ColoNPC* npc = &s->npcs[i]; if (!npc->active) continue; @@ -312,7 +275,8 @@ static ColoPrePlayerPhase col_advance_pre_player_phase_ctx( s->episode_over = 1; s->time_limit_truncated = 1; s->winner = COLO_OUTCOME_PLAYER_DIED; - s->reward = col_terminal_timeout_reward(s, ctx); + s->log.rew_timeout -= ctx->config.timeout_penalty; + s->reward = -ctx->config.timeout_penalty; col_record_terminal_outcome_score(s); s->log.max_wave_depth = col_episode_max_wave_depth(s); col_record_reward_clamp_headroom(s); @@ -422,7 +386,7 @@ static void col_step_ctx(EncounterState* state, EncounterContext* context, const col_accumulate_tick_stats(s, ctx); ColoShapedReward shaped = col_shaped_reward(s, ctx); - s->reward = col_shaped_total(shaped); + s->reward = shaped.damage + shaped.boss_phase; int shaped_paid = 1; #ifdef COLO_PROFILE_ENABLED COLO_PROFILE_MARK(COLO_PROF_STEP_REWARD); @@ -469,9 +433,6 @@ static void col_step_ctx(EncounterState* state, EncounterContext* context, const if (s->wave + 1 >= COLO_NUM_WAVES) { s->episode_over = 1; s->winner = COLO_OUTCOME_PLAYER_WON; - /* Terminal, so paid as an assignment exactly like death and timeout. Stacking - * the win onto the same tick's shaped reward AND wave clear pushed the tick past - * the trainer's [-1,1] clamp and destroyed 59% of the win bonus (measured). */ s->reward = ctx->config.win_bonus; s->log.rew_win += ctx->config.win_bonus; shaped_paid = 0; @@ -489,7 +450,10 @@ static void col_step_ctx(EncounterState* state, EncounterContext* context, const } finish_step: - if (shaped_paid) col_commit_reward_terms(s, shaped); + if (shaped_paid) { + s->log.rew_damage += shaped.damage; + s->log.rew_boss_phase += shaped.boss_phase; + } if (s->episode_over) { col_record_terminal_outcome_score(s); s->log.max_wave_depth = col_episode_max_wave_depth(s); diff --git a/ocean/osrs/encounters/encounter_colosseum.h b/ocean/osrs/encounters/encounter_colosseum.h index 63557e0e64..fddd6c603b 100644 --- a/ocean/osrs/encounters/encounter_colosseum.h +++ b/ocean/osrs/encounters/encounter_colosseum.h @@ -76,7 +76,6 @@ typedef enum { #include "colosseum/encounter_colosseum_boss.inc" #include "colosseum/encounter_colosseum_player_actions.inc" #include "colosseum/encounter_colosseum_reward_step.inc" -#include "colosseum/encounter_colosseum_forecast.inc" #include "colosseum/encounter_colosseum_obs_mask.inc" #include "colosseum/encounter_colosseum_mask_render.inc" #include "colosseum/encounter_colosseum_lab.inc" diff --git a/ocean/osrs/encounters/encounter_nh_pvp.h b/ocean/osrs/encounters/encounter_nh_pvp.h index 7c7e12bc93..14a64f1cf4 100644 --- a/ocean/osrs/encounters/encounter_nh_pvp.h +++ b/ocean/osrs/encounters/encounter_nh_pvp.h @@ -22,108 +22,6 @@ typedef struct { OsrsActorRouteCache player_route_cache[NUM_AGENTS]; } NhPvpContext; -static int nh_pvp_find_consumable_cell( - const Player* player, - OsrsConsumableKind kind -) { - for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { - const OsrsItemContentMetadata* metadata = - osrs_inventory_cell_metadata(&player->inventory_cells[cell]); - if (metadata->consumable_kind == kind) return cell; - } - return -1; -} - -static void nh_pvp_translate_inventory_cell( - int* actions, - const Player* player, - int cell -) { - if (cell < 0 || cell >= OSRS_INVENTORY_SIZE) return; - const OsrsItemContentMetadata* metadata = - osrs_inventory_cell_metadata(&player->inventory_cells[cell]); - int click_action = cell + 1; - if (metadata->click_action == OSRS_CLICK_EQUIP) { - if (metadata->gear_slot >= 0 && metadata->gear_slot < NUM_GEAR_SLOTS) - actions[OSRS_HEAD_EQUIP_SLOT(metadata->gear_slot)] = click_action; - } else if (metadata->click_action == OSRS_CLICK_EAT) { - actions[OSRS_HEAD_EAT] = click_action; - } else if (metadata->click_action == OSRS_CLICK_DRINK) { - actions[OSRS_HEAD_DRINK] = click_action; - } -} - -static void nh_pvp_translate_human_input( - HumanInput* hi, - int* actions, - Player* agent, - Player* target -) { - memset(actions, 0, OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); - if (hi->pending_move_x >= 0 && hi->pending_move_y >= 0) { - int dx = hi->pending_move_x - agent->x; - int dy = hi->pending_move_y - agent->y; - if (dx < -2) dx = -2; - if (dx > 2) dx = 2; - if (dy < -2) dy = -2; - if (dy > 2) dy = 2; - for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { - if (ENCOUNTER_MOVE_TARGET_DX[action] == dx && - ENCOUNTER_MOVE_TARGET_DY[action] == dy) { - actions[OSRS_HEAD_PRIMARY] = action; - break; - } - } - } - encounter_translate_prayer(hi, actions, OSRS_HEAD_OVERHEAD); - encounter_translate_offensive_prayer(hi, actions, OSRS_HEAD_OFFENSIVE); - - if (hi->pending_attack) { - actions[OSRS_HEAD_PRIMARY] = OSRS_PRIMARY_MOVE_ACTIONS; - if (hi->pending_spell == PVP_ATTACK_ICE) - actions[OSRS_HEAD_SPELL] = OSRS_SPELL_ICE_BARRAGE; - else if (hi->pending_spell == PVP_ATTACK_BLOOD) - actions[OSRS_HEAD_SPELL] = OSRS_SPELL_BLOOD_BARRAGE; - } - if (hi->pending_food) { - int cell = nh_pvp_find_consumable_cell( - agent, OSRS_CONSUMABLE_SHARK_FOOD); - if (cell >= 0) actions[OSRS_HEAD_EAT] = cell + 1; - } - if (hi->pending_karambwan) { - int cell = nh_pvp_find_consumable_cell( - agent, OSRS_CONSUMABLE_KARAMBWAN); - if (cell >= 0) actions[OSRS_HEAD_EAT] = cell + 1; - } - OsrsConsumableKind potion_kind = OSRS_CONSUMABLE_NONE; - if (hi->pending_potion == POTION_BREW) - potion_kind = OSRS_CONSUMABLE_BREW; - else if (hi->pending_potion == POTION_RESTORE) - potion_kind = OSRS_CONSUMABLE_SUPER_RESTORE; - else if (hi->pending_potion == POTION_COMBAT) - potion_kind = OSRS_CONSUMABLE_SUPER_COMBAT; - else if (hi->pending_potion == POTION_RANGED) - potion_kind = OSRS_CONSUMABLE_RANGING; - if (potion_kind != OSRS_CONSUMABLE_NONE) { - int cell = nh_pvp_find_consumable_cell(agent, potion_kind); - if (cell >= 0) actions[OSRS_HEAD_DRINK] = cell + 1; - } - for (int i = 0; i < hi->commands.count; i++) { - const HumanCommand* command = &hi->commands.items[i]; - if (command->kind == HUMAN_COMMAND_EQUIP_INVENTORY_ITEM || - command->kind == HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK || - command->kind == HUMAN_COMMAND_EAT || - command->kind == HUMAN_COMMAND_DRINK) { - nh_pvp_translate_inventory_cell( - actions, agent, command->inventory_slot); - } - } - if (hi->pending_veng) - actions[OSRS_HEAD_SPELL] = OSRS_SPELL_VENGEANCE; - if (hi->pending_spec) - actions[OSRS_HEAD_SPECIAL] = agent->spec_armed ? 2 : 1; - (void)target; -} static void nh_pvp_init_state( EncounterState* state, @@ -164,10 +62,7 @@ static void nh_pvp_finalize_context( ) { (void)state; NhPvpContext* ctx = (NhPvpContext*)context; - if (!ctx || ctx->route_topology) { - fprintf(stderr, "NH PvP route topology finalized twice\n"); - abort(); - } + if (ctx->route_topology) abort(); ctx->route_topology = pvp_route_topology_finalize(ctx->collision_map); } @@ -208,27 +103,14 @@ static void nh_pvp_step_human_commands( s->env.pvp_runtime.walk_dest_x[0] = hi->pending_move_x; s->env.pvp_runtime.walk_dest_y[0] = hi->pending_move_y; } - nh_pvp_translate_human_input( - hi, - s->env.ocean_io.agent_actions, - &s->env.players[0], - &s->env.players[1]); + human_to_pvp_actions( + hi, s->env.ocean_io.agent_actions, &s->env.players[0]); pvp_step(&s->env, ctx->route_topology, ctx->player_route_cache); s->env.pvp_runtime.use_c_opponent_p0 = saved_use_c_opponent_p0; - /* pending_move must survive until arrival so later ticks keep re-arming - walk_dest for the same click */ - if (s->env.pvp_runtime.walk_dest_x[0] < 0 || s->env.pvp_runtime.walk_dest_y[0] < 0) { + if (s->env.pvp_runtime.walk_dest_x[0] < 0 || + s->env.pvp_runtime.walk_dest_y[0] < 0) human_input_clear_move(hi); - } - hi->pending_attack = 0; - hi->pending_spell = 0; - hi->pending_prayer = 0; - hi->pending_offensive_prayer = 0; - hi->pending_food = 0; - hi->pending_potion = 0; - hi->pending_karambwan = 0; - hi->pending_veng = 0; - hi->pending_spec = 0; + human_input_clear_pending(hi); } static void nh_pvp_write_obs( @@ -426,4 +308,4 @@ static void nh_pvp_register(void) { encounter_register(&ENCOUNTER_NH_PVP); } -#endif /* ENCOUNTER_NH_PVP_H */ +#endif diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h index 04f3bde99b..5262a6c89f 100644 --- a/ocean/osrs/encounters/encounter_zulrah.h +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -327,13 +327,6 @@ static const uint8_t ZUL_RANGE_LOADOUT[ZUL_NUM_GEAR_TIERS][NUM_GEAR_SLOTS] = { ITEM_ZARYTE_VAMBRACES, ITEM_AVERNIC_TREADS, ITEM_RING_OF_SUFFERING_RI }, }; -static void zul_populate_player_inventory(Player* p, int gear_tier) { - const uint8_t* loadouts[] = { - ZUL_MAGE_LOADOUT[gear_tier], - ZUL_RANGE_LOADOUT[gear_tier], - }; - encounter_populate_inventory(p, loadouts, 2, NULL); -} #define ZUL_NUM_SNAKELING_POSITIONS 5 static const int ZUL_SNAKELING_POSITIONS[ZUL_NUM_SNAKELING_POSITIONS][2] = { @@ -678,14 +671,6 @@ static inline int zul_on_platform_bounds(int x, int y) { y >= ZUL_PLATFORM_MIN && y <= ZUL_PLATFORM_MAX; } -static inline int zul_topology_player_tile_allowed( - const ZulrahContext* ctx, - int x, - int y -) { - return !encounter_arena_topology_tile_blocked( - ctx->route_topology, x, y); -} @@ -793,11 +778,13 @@ static int zul_apply_local_move_route(Player* player, int action) { -static void zul_finalize_route_topology(ZulrahContext* ctx) { - if (!ctx || ctx->route_topology) { - fprintf(stderr, "zulrah route topology finalized twice\n"); - abort(); - } +static void zul_finalize_context( + EncounterState* state, + EncounterContext* context +) { + (void)state; + ZulrahContext* ctx = (ZulrahContext*)context; + if (ctx->route_topology) abort(); ZulRouteTopologyBuildContext build = { ctx->collision_map, ctx->world_offset_x, @@ -830,13 +817,6 @@ static void zul_finalize_route_topology(ZulrahContext* ctx) { zul_local_move_routes_build(&zul_route_topology_owner); } -static void zul_finalize_context( - EncounterState* state, - EncounterContext* context -) { - (void)state; - zul_finalize_route_topology((ZulrahContext*)context); -} static inline int zul_player_in_cloud(int cx, int cy, int px, int py) { return px >= cx && px < cx + ZUL_CLOUD_SIZE && @@ -999,9 +979,6 @@ static ZulrahCloudObservationSummary zul_cloud_observation_summary( return summary; } -static int zul_form_npc_id(ZulrahForm f) { - return MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[f]].npc_id; -} static inline int zul_cap_damage(ZulrahState* s, int damage) { if (damage > ZUL_DAMAGE_CAP) { @@ -1017,24 +994,24 @@ static void zul_apply_recoil(ZulrahState* s, int damage, AttackStyle style, damage, style, s->player.prayer, - 0, - 0, - 0, + 0, + 0, + 0, &s->player.equipment_effect_profile, &s->player.item_effect_state, &s->rng_state ); - if (damage_result.recoil_damage > 0) { - int recoil = damage_result.recoil_damage; - if (s->player.equipment_effect_profile.recoil_source == OSRS_RECOIL_SOURCE_RING_OF_RECOIL && - recoil > s->player.item_effect_state.recoil_charges) { - recoil = s->player.item_effect_state.recoil_charges; - } - encounter_damage_player(attacker, recoil, NULL); - osrs_consume_recoil_charges(&s->player, recoil); - } + int recoil = damage_result.recoil_damage; + if (recoil <= 0) return; + if (s->player.equipment_effect_profile.recoil_source == + OSRS_RECOIL_SOURCE_RING_OF_RECOIL && + recoil > s->player.item_effect_state.recoil_charges) + recoil = s->player.item_effect_state.recoil_charges; + encounter_damage_player(attacker, recoil, NULL); + osrs_consume_recoil_charges(&s->player, recoil); } + static void zul_apply_player_damage(ZulrahState* s, int damage, AttackStyle style, Player* attacker) { if (damage <= 0) return; @@ -1045,8 +1022,6 @@ static void zul_apply_player_damage(ZulrahState* s, int damage, AttackStyle styl if (attacker) zul_apply_recoil(s, damage, style, attacker); } -/** Landing-side accounting for one player->Zulrah hit; returns 1 when it landed - this tick. Shared by the instant (delay 0) and queued paths. */ static int zul_land_zulrah_hit(ZulrahState* s, EncounterPendingHit* ph) { int landed = 0; int hit_damage = 0; @@ -1136,8 +1111,6 @@ static void zul_record_attack(ZulrahState* s, int src_x, int src_y, s->attack_events[i].damage = damage; } -/** Queues one Zulrah->player hit. Protect-prayer and damage freeze on THIS tick - (the calculation tick); only the application waits out the hit delay. */ static int zul_queue_player_hit(ZulrahState* s, int raw_damage, AttackStyle style, int accuracy_hit) { int distance = encounter_projectile_distance( @@ -1171,12 +1144,6 @@ static void zul_player_hit_landed( &s->zulrah); } -static void zul_resolve_player_pending_hits(ZulrahState* s) { - encounter_resolve_player_pending_hits_observed( - &s->player_pending_hits, &s->player, s->player.prayer, - &s->damage_received_this_tick, NULL, NULL, - zul_player_hit_landed, s); -} static void zul_attack_ranged(ZulrahState* s) { const MonsterStats* m = &MONSTER_DATABASE[MON_ZULRAH_GREEN]; @@ -1204,32 +1171,16 @@ static void zul_attack_magic(ZulrahState* s) { s->player.x, s->player.y, 1, dmg); } -static void zul_attack_magic_ranged(ZulrahState* s) { - if (encounter_rand_int(&s->rng_state, 4) < 3) { - zul_attack_magic(s); - } else { - zul_attack_ranged(s); - } -} -static int zul_on_pillar_safespot(int px, int py) { - if (py != 11) return 0; - return (px == 17 || px == 7); -} -static void zul_melee_start(ZulrahState* s) { - s->melee_target_x = s->player.x; - s->melee_target_y = s->player.y; - s->melee_pending = 1; - s->melee_stare_timer = ZUL_MELEE_STARE_TICKS; - s->melee_prayer_at_calc = s->player.prayer; -} static void zul_melee_hit(ZulrahState* s) { s->melee_pending = 0; int dmg = 0; - if (s->player.x == s->melee_target_x && s->player.y == s->melee_target_y - && !zul_on_pillar_safespot(s->player.x, s->player.y)) { + if (s->player.x == s->melee_target_x && + s->player.y == s->melee_target_y && + !(s->player.y == 11 && + (s->player.x == 17 || s->player.x == 7))) { if (!encounter_prayer_correct_for_style(s->melee_prayer_at_calc, ATTACK_STYLE_MELEE)) { dmg = 20 + encounter_rand_int(&s->rng_state, 11); zul_apply_player_damage(s, dmg, ATTACK_STYLE_MELEE, &s->zulrah); @@ -1240,29 +1191,17 @@ static void zul_melee_hit(ZulrahState* s) { s->melee_target_x, s->melee_target_y, 2, dmg); } -static void zul_attack_jad(ZulrahState* s) { - if (s->jad_is_magic_next) { - zul_attack_magic(s); - } else { - zul_attack_ranged(s); - } - s->jad_is_magic_next = !s->jad_is_magic_next; -} -static inline void zul_form_def_bonuses(ZulrahForm form, int* def_magic, int* def_ranged) { - const MonsterStats* m = &MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[form]]; - *def_magic = m->magic_def; - *def_ranged = m->ranged_def; -} static AttackStyle zul_player_equipped_attack_style(const ZulrahState* s) { AttackStyle style = (AttackStyle)get_item_attack_style(s->player.equipped[GEAR_SLOT_WEAPON]); + if (style == ATTACK_STYLE_NONE) return ATTACK_STYLE_RANGED; if (style == ATTACK_STYLE_MAGIC || style == ATTACK_STYLE_RANGED || style == ATTACK_STYLE_MELEE) { return style; } - return ATTACK_STYLE_RANGED; + abort(); } static void zul_mark_live_stats_dirty(ZulrahState* s) { @@ -1301,28 +1240,17 @@ static int zul_player_can_attack_zulrah( } static int zul_zulrah_def_roll(ZulrahState* s, int is_mage) { - int def_magic = 0, def_ranged = 0; - zul_form_def_bonuses(s->current_form, &def_magic, &def_ranged); + const MonsterStats* monster = + &MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[s->current_form]]; + int def_bonus = is_mage ? monster->magic_def : monster->ranged_def; if (is_mage) { - def_magic -= s->magic_def_drain; - if (def_magic < -64) def_magic = -64; + def_bonus -= s->magic_def_drain; + if (def_bonus < -64) def_bonus = -64; } - int def_bonus = is_mage ? def_magic : def_ranged; - int def_roll = (MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[s->current_form]].def_level + 8) * (def_bonus + 64); - if (def_roll < 0) def_roll = 0; - return def_roll; + int def_roll = (monster->def_level + 8) * (def_bonus + 64); + return def_roll > 0 ? def_roll : 0; } -static int zul_player_attack_hits( - ZulrahState* s, int is_mage, const OsrsPreparedAttackEffects* attack_effects -) { - int att_roll = attack_effects->attack_roll; - int def_roll = zul_zulrah_def_roll(s, is_mage); - - return attack_effects->use_double_accuracy - ? encounter_roll_hit_chance_double(&s->rng_state, att_roll, def_roll) - : encounter_roll_hit_chance(&s->rng_state, att_roll, def_roll); -} static void zul_player_attack(ZulrahState* s, const ZulrahContext* ctx) { if (!s->zulrah_visible || s->is_diving) return; @@ -1354,8 +1282,13 @@ static void zul_player_attack(ZulrahState* s, const ZulrahContext* ctx) { ); s->player.attack_timer = ls->attack_speed; + int def_roll = zul_zulrah_def_roll(s, is_mage); + int hit = attack_effects.use_double_accuracy + ? encounter_roll_hit_chance_double( + &s->rng_state, attack_effects.attack_roll, def_roll) + : encounter_roll_hit_chance( + &s->rng_state, attack_effects.attack_roll, def_roll); int dmg = 0; - int hit = zul_player_attack_hits(s, is_mage, &attack_effects); if (hit) { dmg = encounter_rand_int(&s->rng_state, attack_effects.max_hit + 1); dmg = zul_cap_damage(s, dmg); @@ -1449,7 +1382,8 @@ static void zul_pick_snakeling_pos( for (int i = 0; i < ZUL_NUM_SNAKELING_POSITIONS; i++) { int px = ZUL_SNAKELING_POSITIONS[order[i]][0]; int py = ZUL_SNAKELING_POSITIONS[order[i]][1]; - if (zul_topology_player_tile_allowed(ctx, px, py) && + if (!encounter_arena_topology_tile_blocked( + ctx->route_topology, px, py) && !(px == s->player.x && py == s->player.y)) { *ox = px; *oy = py; return; } @@ -1747,10 +1681,27 @@ static void zul_fire_action( ) { switch (type) { case ZA_RANGED: zul_attack_ranged(s); break; - case ZA_MAGIC_RANGED: zul_attack_magic_ranged(s); break; - case ZA_MELEE: zul_melee_start(s); break; + case ZA_MAGIC_RANGED: + if (encounter_rand_int(&s->rng_state, 4) < 3) + zul_attack_magic(s); + else + zul_attack_ranged(s); + break; + case ZA_MELEE: + s->melee_target_x = s->player.x; + s->melee_target_y = s->player.y; + s->melee_pending = 1; + s->melee_stare_timer = ZUL_MELEE_STARE_TICKS; + s->melee_prayer_at_calc = s->player.prayer; + break; case ZA_JAD_RM: - case ZA_JAD_MR: zul_attack_jad(s); break; + case ZA_JAD_MR: + if (s->jad_is_magic_next) + zul_attack_magic(s); + else + zul_attack_ranged(s); + s->jad_is_magic_next = !s->jad_is_magic_next; + break; case ZA_CLOUDS: zul_spawn_cloud(s, ctx); break; case ZA_SNAKELINGS: zul_spawn_snakeling(s, ctx); break; case ZA_SNAKECLOUD_ALT: @@ -1776,28 +1727,17 @@ static int zul_action_interval(ZulActionType type) { case ZA_SNAKELINGS: case ZA_SNAKECLOUD_ALT: case ZA_CLOUDSNAKE_ALT: return ZUL_SPAWN_INTERVAL; - default: return 1; + default: abort(); } } -static int zul_action_is_attack(ZulActionType type) { - return type == ZA_RANGED || type == ZA_MAGIC_RANGED || type == ZA_MELEE || - type == ZA_JAD_RM || type == ZA_JAD_MR; -} -static int zul_phase_action_ticks(const ZulRotationPhase* phase) { - int total = 0; - for (int i = 0; i < ZUL_MAX_PHASE_ACTIONS; i++) { - if (phase->actions[i].type == ZA_END) break; - total += phase->actions[i].count * zul_action_interval((ZulActionType)phase->actions[i].type); - } - return total; -} static void zul_enter_phase(ZulrahState* s) { const ZulRotationPhase* phase = zul_current_phase(s); s->current_form = (ZulrahForm)phase->form; - s->zulrah.npc_def_id = zul_form_npc_id(s->current_form); + s->zulrah.npc_def_id = + MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[s->current_form]].npc_id; s->zulrah.x = ZUL_POSITIONS[phase->position][0]; s->zulrah.y = ZUL_POSITIONS[phase->position][1]; s->zulrah_visible = 1; @@ -1814,13 +1754,18 @@ static void zul_enter_phase(ZulrahState* s) { s->phase_timer = phase->phase_ticks; - int action_ticks = zul_phase_action_ticks(phase); - int available = phase->phase_ticks - surface_ticks - ZUL_DIVE_PHASE_TICKS - action_ticks; - int initial_delay = (available > 1) ? available : 1; - + int action_ticks = 0; + for (int i = 0; i < ZUL_MAX_PHASE_ACTIONS; i++) { + if (phase->actions[i].type == ZA_END) break; + action_ticks += phase->actions[i].count * + zul_action_interval((ZulActionType)phase->actions[i].type); + } + int available = + phase->phase_ticks - surface_ticks - + ZUL_DIVE_PHASE_TICKS - action_ticks; s->action_index = 0; s->action_progress = 0; - s->action_timer = initial_delay; + s->action_timer = available > 1 ? available : 1; ZulActionType first_type = (ZulActionType)phase->actions[0].type; if (first_type == ZA_JAD_RM) s->jad_is_magic_next = 0; @@ -1829,23 +1774,7 @@ static void zul_enter_phase(ZulrahState* s) { s->zulrah_attacking = 0; } -static void zul_enter_dive(ZulrahState* s) { - s->is_diving = 1; - s->zulrah_attacking = 0; - zul_set_npc_anim_event(s, ZULRAH_ANIM_DIVE, ZUL_DIVE_ANIM_TICKS); -} - -static void zul_next_phase(ZulrahState* s) { - int rot_len = ZUL_ROT_LENGTHS[s->rotation_index]; - s->phase_index++; - if (s->phase_index >= rot_len) { - s->rotation_index = encounter_rand_int(&s->rng_state, ZUL_NUM_ROTATIONS); - s->phase_index = 1; - } - - zul_enter_phase(s); -} static void zul_phase_tick(ZulrahState* s, const ZulrahContext* ctx) { if (!s->zulrah_visible) return; @@ -1855,12 +1784,21 @@ static void zul_phase_tick(ZulrahState* s, const ZulrahContext* ctx) { if (s->phase_timer <= 0) { s->zulrah_visible = 0; s->zulrah.npc_visible = 0; - zul_next_phase(s); + s->phase_index++; + if (s->phase_index >= ZUL_ROT_LENGTHS[s->rotation_index]) { + s->rotation_index = + encounter_rand_int(&s->rng_state, ZUL_NUM_ROTATIONS); + s->phase_index = 1; + } + zul_enter_phase(s); return; } if (s->phase_timer <= ZUL_DIVE_PHASE_TICKS && !s->is_diving) { - zul_enter_dive(s); + s->is_diving = 1; + s->zulrah_attacking = 0; + zul_set_npc_anim_event( + s, ZULRAH_ANIM_DIVE, ZUL_DIVE_ANIM_TICKS); } if (s->is_diving) return; @@ -1893,7 +1831,11 @@ static void zul_phase_tick(ZulrahState* s, const ZulrahContext* ctx) { return; } - s->zulrah_attacking = zul_action_is_attack((ZulActionType)next->type); + ZulActionType next_type = (ZulActionType)next->type; + s->zulrah_attacking = + next_type == ZA_RANGED || next_type == ZA_MAGIC_RANGED || + next_type == ZA_MELEE || next_type == ZA_JAD_RM || + next_type == ZA_JAD_MR; if (next->type == ZA_JAD_RM) s->jad_is_magic_next = 0; else if (next->type == ZA_JAD_MR) s->jad_is_magic_next = 1; @@ -1904,18 +1846,6 @@ static void zul_phase_tick(ZulrahState* s, const ZulrahContext* ctx) { } } -static void zul_apply_eat_cell(ZulrahState* s, int cell_idx, OsrsConsumableKind kind) { - FoodType type; - switch (kind) { - case OSRS_CONSUMABLE_SHARK_FOOD: type = FOOD_SHARK; break; - case OSRS_CONSUMABLE_KARAMBWAN: type = FOOD_KARAMBWAN; break; - default: - fprintf(stderr, "zulrah eat: unsupported consumable kind %d\n", (int)kind); - abort(); - } - OsrsPlayerEatResult r = osrs_player_eat_food_effects(&s->player, type); - if (r.consumed) osrs_inventory_cell_consume_eat(&s->player.inventory_cells[cell_idx]); -} static void zul_apply_drink_one_dose_effect(void* ctx, OsrsConsumableKind kind) { ZulrahState* s = (ZulrahState*)ctx; @@ -1950,13 +1880,6 @@ static int zul_drink_has_effect(const ZulrahState* s, OsrsConsumableKind kind) { abort(); } -static void zul_apply_drink_cell( - ZulrahState* s, int cell_idx, OsrsInventoryClickResolution resolution -) { - (void)osrs_inventory_cell_consume_drink_one_dose( - &s->player.inventory_cells[cell_idx], resolution, &s->player.potion_timer, - zul_apply_drink_one_dose_effect, s); -} static void zul_player_attack_snakeling( ZulrahState* s, @@ -1996,26 +1919,10 @@ static void zul_player_attack_snakeling( if (sn->entity.current_hitpoints <= 0) sn->active = 0; } -static void zul_process_prayer(ZulrahState* s, int overhead_action, int offensive_action) { - if (encounter_apply_overhead_action(&s->player.prayer, overhead_action)) { - s->player.prayer_just_activated = 1; - } - OffensivePrayer prev_offensive = s->player.offensive_prayer; - if (encounter_apply_offensive_action(&s->player.offensive_prayer, offensive_action)) { - s->player.offensive_prayer_just_activated = 1; - } - if (s->player.offensive_prayer != prev_offensive) - zul_mark_live_stats_dirty(s); -} -static FightStyle zul_default_fight_style_for_style(AttackStyle style) { - if (style == ATTACK_STYLE_MAGIC) return FIGHT_STYLE_ACCURATE; - if (style == ATTACK_STYLE_RANGED) return FIGHT_STYLE_RAPID; - return FIGHT_STYLE_ACCURATE; -} static void zul_apply_human_player_commands(ZulrahState* s) { @@ -2029,8 +1936,11 @@ static void zul_apply_human_player_commands(ZulrahState* s) { if (changed) { did_change_stats = 1; if (cmd->gear_slot == GEAR_SLOT_WEAPON) { - AttackStyle style = zul_player_equipped_attack_style(s); - s->player.fight_style = zul_default_fight_style_for_style(style); + AttackStyle style = + zul_player_equipped_attack_style(s); + s->player.fight_style = style == ATTACK_STYLE_RANGED + ? FIGHT_STYLE_RAPID + : FIGHT_STYLE_ACCURATE; } } } @@ -2049,7 +1959,6 @@ static void zul_apply_human_player_commands(ZulrahState* s) { static void zul_write_obs(EncounterState* state, EncounterContext* context, float* obs) { (void)context; ZulrahState* s = (ZulrahState*)state; - memset(obs, 0, ZUL_NUM_OBS * sizeof(float)); int i = 0; const EncounterLoadoutStats* loadout_stats = zul_live_stats(s); OsrsSharedObservationInput shared_input = { @@ -2187,7 +2096,8 @@ static void zul_write_mask_bytes( int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action]; int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action]; mask[offset + action] = - zul_topology_player_tile_allowed(ctx, nx, ny); + !encounter_arena_topology_tile_blocked( + ctx->route_topology, nx, ny); } } mask[offset + ZUL_PRIMARY_ATTACK_BASE] = @@ -2397,7 +2307,7 @@ static void zul_clear_active_kill(ZulrahState* s) { static void zul_start_active_kill(ZulrahState* s) { zul_clear_active_kill(s); s->zulrah.entity_type = ENTITY_NPC; - s->zulrah.npc_def_id = zul_form_npc_id(ZUL_FORM_GREEN); + s->zulrah.npc_def_id = MONSTER_DATABASE[MON_ZULRAH_GREEN].npc_id; s->zulrah.npc_size = ZUL_NPC_SIZE; s->zulrah.npc_anim_id = -1; s->zulrah.base_hitpoints = MONSTER_DATABASE[MON_ZULRAH_GREEN].hp; @@ -2493,8 +2403,6 @@ static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t osrs_actor_route_cache_clear(&ctx->player_route_cache); ZulrahState* s = (ZulrahState*)state; Log saved_log = s->log; - (void)ctx; - int saved_tier = s->gear_tier; int saved_fixed_tier = s->gear_tier_fixed; int saved_tier_mode = s->gear_tier_mode; int saved_episode_mode = s->episode_mode; @@ -2505,7 +2413,6 @@ static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t memset(s, 0, sizeof(ZulrahState)); s->log = saved_log; - s->gear_tier = saved_tier; s->gear_tier_fixed = saved_fixed_tier; s->gear_tier_mode = saved_tier_mode; s->episode_mode = saved_episode_mode; @@ -2531,9 +2438,12 @@ static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t s->thrall_attack_timer = ZUL_THRALL_SPEED; } osrs_interaction_init(&s->interaction); - s->player.spec_armed = 0; encounter_apply_loadout(&s->player, ZUL_MAGE_LOADOUT[s->gear_tier], GEAR_MAGE); - zul_populate_player_inventory(&s->player, s->gear_tier); + const uint8_t* loadouts[] = { + ZUL_MAGE_LOADOUT[s->gear_tier], + ZUL_RANGE_LOADOUT[s->gear_tier], + }; + encounter_populate_inventory(&s->player, loadouts, 2, NULL); zul_seed_inventory_cells(s); s->player.offensive_prayer = (s->gear_tier >= 1) ? OFFENSIVE_PRAYER_AUGURY : OFFENSIVE_PRAYER_NONE; @@ -2593,7 +2503,10 @@ static void zul_step_tick( zul_mark_live_stats_dirty(s); zul_resolve_zulrah_pending_hits(s); - zul_resolve_player_pending_hits(s); + encounter_resolve_player_pending_hits_observed( + &s->player_pending_hits, &s->player, s->player.prayer, + &s->damage_received_this_tick, NULL, NULL, + zul_player_hit_landed, s); if (s->melee_pending) { s->melee_stare_timer--; @@ -2609,8 +2522,15 @@ static void zul_step_tick( return; } - zul_process_prayer( - s, actions[ZUL_HEAD_OVERHEAD], actions[ZUL_HEAD_OFFENSIVE]); + if (encounter_apply_overhead_action( + &s->player.prayer, actions[ZUL_HEAD_OVERHEAD])) + s->player.prayer_just_activated = 1; + OffensivePrayer prev_offensive = s->player.offensive_prayer; + if (encounter_apply_offensive_action( + &s->player.offensive_prayer, actions[ZUL_HEAD_OFFENSIVE])) + s->player.offensive_prayer_just_activated = 1; + if (s->player.offensive_prayer != prev_offensive) + zul_mark_live_stats_dirty(s); if (s->human_command_mode) zul_apply_human_player_commands(s); @@ -2641,12 +2561,26 @@ static void zul_step_tick( &s->player, s->player.inventory_cells, click_step.cell_idx) >= 0) zul_mark_live_stats_dirty(s); break; - case OSRS_INVENTORY_APPLY_EAT: - zul_apply_eat_cell( - s, click_step.cell_idx, click_step.resolution.consumable_kind); + case OSRS_INVENTORY_APPLY_EAT: { + OsrsConsumableKind kind = + click_step.resolution.consumable_kind; + if (kind != OSRS_CONSUMABLE_SHARK_FOOD && + kind != OSRS_CONSUMABLE_KARAMBWAN) + abort(); + FoodType food = kind == OSRS_CONSUMABLE_SHARK_FOOD + ? FOOD_SHARK : FOOD_KARAMBWAN; + OsrsPlayerEatResult eat = + osrs_player_eat_food_effects(&s->player, food); + if (eat.consumed) + osrs_inventory_cell_consume_eat( + &s->player.inventory_cells[click_step.cell_idx]); break; + } case OSRS_INVENTORY_APPLY_DRINK: - zul_apply_drink_cell(s, click_step.cell_idx, click_step.resolution); + (void)osrs_inventory_cell_consume_drink_one_dose( + &s->player.inventory_cells[click_step.cell_idx], + click_step.resolution, &s->player.potion_timer, + zul_apply_drink_one_dose_effect, s); break; } } @@ -2782,10 +2716,6 @@ static void zul_step_tick( static void zul_step(EncounterState* state, EncounterContext* context, const int* actions) { ZulrahState* s = (ZulrahState*)state; ZulrahContext* ctx = (ZulrahContext*)context; - if (!ctx) { - fprintf(stderr, "zulrah step missing context\n"); - abort(); - } encounter_arena_topology_require_finalized(ctx->route_topology); if (s->episode_over) return; zul_step_tick(s, ctx, actions); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc index 246a52c0a1..43f46998c1 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc @@ -138,14 +138,6 @@ static int inf_resolve_player_attack_ctx( return 1; } -static int inf_resolve_player_attack( - InfernoState* s, - int manual_spell, - InfPlayerAttack* out -) { - return inf_resolve_player_attack_ctx( - s, inf_legacy_context(), manual_spell, out); -} static int inf_player_has_any_barrage_spell_available(const InfernoState* s) { return s->player.current_magic >= BLOOD_BARRAGE_LEVEL; @@ -232,29 +224,6 @@ static int inf_player_can_attack_npc_from_current_tile_ctx( } -static int inf_player_has_los_to_npc_from_current_tile_ctx( - const InfernoState* s, - const InfernoContext* ctx, - int npc_idx -) { - if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; - - const InfNPC* npc = &s->npcs[npc_idx]; - if (!npc->active || npc->death_ticks != 0 || npc->hp <= 0 || - npc->type == INF_NPC_ZUK_SHIELD) - return 0; - - return inf_los_clear_ctx( - s, - ctx, - s->player.x, - s->player.y, - 1, - npc->x, - npc->y, - npc->size, - 0); -} @@ -832,9 +801,6 @@ static void inf_npc_attack_ctx(InfernoState* s, InfernoContext* ctx, int idx) { npc->attack_timer = inf_jad_attack_timer(s->wave); } -static void inf_npc_attack(InfernoState* s, int idx) { - inf_npc_attack_ctx(s, inf_legacy_context(), idx); -} static int inf_find_mager_respawn_tile( InfernoState* s, const InfernoContext* ctx, int size, int* out_x, int* out_y @@ -893,9 +859,6 @@ static int inf_mager_resurrect_ctx(InfernoState* s, InfernoContext* ctx, int idx return 1; } -static int inf_mager_resurrect(InfernoState* s, int idx) { - return inf_mager_resurrect_ctx(s, inf_legacy_context(), idx); -} #define INF_JAD_HEALER_MAX_SPAWN_CANDIDATES 165 @@ -960,10 +923,7 @@ static void inf_jad_check_healers_ctx(InfernoState* s, const InfernoContext* ctx if (npc->hp >= npc->max_hp / 2) return; inf_npc_jad(npc)->healer_spawned = 1; - int num_healers; - if (s->wave == 66) num_healers = 5; - else if (s->wave == 67) num_healers = 3; - else num_healers = 3; + int num_healers = s->wave == 66 ? 5 : 3; for (int h = 0; h < num_healers; h++) { int slot = inf_find_free_npc(s); @@ -977,9 +937,6 @@ static void inf_jad_check_healers_ctx(InfernoState* s, const InfernoContext* ctx } } -static void inf_jad_check_healers(InfernoState* s, int idx) { - inf_jad_check_healers_ctx(s, inf_legacy_context(), idx); -} static void inf_zuk_tick(InfernoState* s) { if (!inf_is_final_wave(s)) return; @@ -1254,7 +1211,3 @@ static void inf_tick_npcs_ctx(InfernoState* s, InfernoContext* ctx) { } } } - -static void inf_tick_npcs(InfernoState* s) { - inf_tick_npcs_ctx(s, inf_legacy_context()); -} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc index 04df54c7ee..f22f0b087b 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc @@ -25,46 +25,6 @@ typedef struct { InfStepOutForecastAction actions[ENCOUNTER_MOVE_ACTIONS]; } InfStepOutForecast; -typedef struct { - int action_feature_mismatches; - int tick_feature_mismatches; - int dangerous_false_negatives; - int dangerous_false_positives; - int exact_safe_fast_dangerous; - int exact_dangerous_actions; - int fast_dangerous_actions; - int sampled_actions; - int max_hit_abs_error_sum; - int max_hit_abs_error_max; -} InfStepOutForecastOracleDiff; - -typedef struct { - int active; - InfNPCType type; - int x; - int y; - int size; - int hp; - int attack_timer; - int stun_timer; - int frozen_ticks; - int dig_freeze_timer; - int dig_attack_delay; - int blob_scanned_prayer; - int had_los_last_tick; - int jad_attack_style; - int attack_style; - int aggro_target; -} InfForecastNpcLocal; - -typedef struct { - const InfernoState* state; - const InfernoContext* ctx; - const InfForecastNpcLocal* npcs; - int self_idx; - int player_x; - int player_y; -} InfForecastMoveCtx; static float inf_zuk_attack_timer_obs(const InfernoState* s) { if (!inf_is_final_wave(s)) return 0.0f; @@ -138,46 +98,6 @@ static Player inf_step_out_forecast_set_action_landing_ctx( return moved; } -static int inf_step_out_forecast_duplicate_landing( - const InfStepOutForecast* forecast, - int action_idx -) { - const InfStepOutForecastAction* action = &forecast->actions[action_idx]; - for (int prior_idx = 0; prior_idx < action_idx; prior_idx++) { - const InfStepOutForecastAction* prior = &forecast->actions[prior_idx]; - if (prior->valid && - prior->land_x == action->land_x && - prior->land_y == action->land_y) { - return prior_idx; - } - } - return -1; -} - -static int inf_step_out_forecast_prepare_unique_action_ctx( - const InfernoState* s, - const InfernoContext* ctx, - InfStepOutForecast* out, - int action_idx, - int forecast_slot_count -) { - InfStepOutForecastAction* action = &out->actions[action_idx]; - Player moved = inf_step_out_forecast_set_action_landing_ctx( - s, ctx, action_idx, action); - if (!action->valid || forecast_slot_count == 0) return 0; - - int duplicate_idx = inf_step_out_forecast_duplicate_landing( - out, action_idx); - if (duplicate_idx >= 0) { - int valid = action->valid; - *action = out->actions[duplicate_idx]; - action->valid = valid; - action->land_x = moved.x; - action->land_y = moved.y; - return 0; - } - return 1; -} static void inf_step_out_forecast_record_style_mask( InfStepOutForecastAction* action, @@ -345,39 +265,27 @@ static void inf_step_out_forecast_tick( if (npc->frozen_ticks > 0) npc->frozen_ticks--; if (npc->type == INF_NPC_MAGER && npc->resurrect_cooldown > 0) npc->resurrect_cooldown--; - inf_npc_move_ctx(sim, ctx, i); - inf_step_out_forecast_npc_attack( - sim, ctx, i, action, tick_idx); - } -} - #ifdef INF_PROFILE_ENABLED -static void inf_step_out_forecast_tick_profiled( - InfernoState* sim, - const InfernoContext* ctx, - InfStepOutForecastAction* action, - int tick_idx, - const int slots[INF_MAX_NPCS], - int slot_count -) { - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - InfNPC* npc = &sim->npcs[i]; - if (npc->frozen_ticks > 0) npc->frozen_ticks--; - if (npc->type == INF_NPC_MAGER && npc->resurrect_cooldown > 0) - npc->resurrect_cooldown--; - double t0 = INF_PROFILE_NOW_MS(); + int profile_enabled = INF_PROFILE_ENABLED(); + double move_start = profile_enabled ? INF_PROFILE_NOW_MS() : 0.0; +#endif inf_npc_move_ctx(sim, ctx, i); - double t1 = INF_PROFILE_NOW_MS(); - INF_PROFILE_ADD(INF_PROF_FORECAST_NPC_MOVE, t1 - t0); +#ifdef INF_PROFILE_ENABLED + double attack_start = profile_enabled ? INF_PROFILE_NOW_MS() : 0.0; + if (profile_enabled) + INF_PROFILE_ADD(INF_PROF_FORECAST_NPC_MOVE, attack_start - move_start); +#endif inf_step_out_forecast_npc_attack( sim, ctx, i, action, tick_idx); - INF_PROFILE_ADD( - INF_PROF_FORECAST_NPC_ATTACK, - INF_PROFILE_NOW_MS() - t1); +#ifdef INF_PROFILE_ENABLED + if (profile_enabled) + INF_PROFILE_ADD( + INF_PROF_FORECAST_NPC_ATTACK, + INF_PROFILE_NOW_MS() - attack_start); +#endif } } -#endif + static void inf_restore_step_out_forecast_action_state( @@ -433,584 +341,8 @@ static int inf_step_out_forecast_tick_has_event( tick->blob_scan_count > 0; } -static int inf_step_out_forecast_npc_fire_tick_idx(const InfNPC* npc) { - int tick_idx = npc->attack_timer - 1; - return tick_idx > 0 ? tick_idx : 0; -} - -static void inf_step_out_forecast_fast_static_npc( - const InfernoState* s, - const InfernoContext* ctx, - const InfNPC* npc, - InfStepOutForecastAction* action -) { - if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return; - const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; - if (npc->stun_timer > 0) return; - if (npc->dig_freeze_timer > 0 || npc->dig_attack_delay > 0) return; - if (npc->type == INF_NPC_ZUK_SHIELD || npc->type == INF_NPC_NIBBLER) - return; - if (npc->aggro_target >= 0) return; - - int dist = encounter_dist_to_npc( - action->land_x, action->land_y, npc->x, npc->y, npc->size); - if (dist == 0 || dist > stats->attack_range) return; - if (stats->attack_range == 1 && - !inf_npc_melee_contact_allows_attack_at_tile( - action->land_x, action->land_y, npc->type, - npc->x, npc->y, npc->size, dist)) - return; - - int has_los_now = 1; - if (stats->attack_range > 1) - has_los_now = inf_npc_has_los_to_tile_ctx( - s, ctx, npc, action->land_x, action->land_y); - - if (npc->type == INF_NPC_BLOB && - npc->blob_scanned_prayer < 0 && - has_los_now && - !npc->had_los_last_tick) { - inf_step_out_forecast_record_blob_scan(action, 0); - return; - } - - int tick_idx = inf_step_out_forecast_npc_fire_tick_idx(npc); - if (tick_idx >= INF_STEP_OUT_FORECAST_HORIZON) return; - - if (stats->attack_range > 1 && - !(npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) && - !has_los_now) return; - - if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer < 0) { - inf_step_out_forecast_record_blob_scan(action, tick_idx); - return; - } - - int style_mask = inf_forecast_jad_unknown_style_mask(npc); - if (style_mask == 0) { - int planned_style = inf_npc_planned_style_for_current_state(npc); - if (planned_style == ATTACK_STYLE_NONE) return; - style_mask = inf_attack_style_options_mask_at_tile( - action->land_x, action->land_y, npc, stats, planned_style, dist); - } - if (style_mask == 0) return; - - inf_step_out_forecast_record_style_mask( - action, tick_idx, npc->type, stats, style_mask); -} - -static int inf_forecast_local_npc_occupied( - const InfForecastNpcLocal npcs[INF_MAX_NPCS], - int self_idx, - int x, - int y, - int size -) { - for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { - if (npc_idx == self_idx) continue; - const InfForecastNpcLocal* npc = &npcs[npc_idx]; - if (!npc->active || npc->hp <= 0 || - !inf_npc_sets_collision_flag(npc->type)) - continue; - if (los_aabb_overlap( - x, y, size, npc->x, npc->y, npc->size)) - return 1; - } - return 0; -} - -static int inf_forecast_local_overlaps_player( - int x, int y, int size, int player_x, int player_y -) { - return !(x >= player_x + 1 || x + size <= player_x || - y >= player_y + 1 || y + size <= player_y); -} - -static int inf_forecast_local_npc_blocked(void* ctx, int x, int y, int size) { - InfForecastMoveCtx* mc = (InfForecastMoveCtx*)ctx; - if (inf_npc_environment_blocked_ctx( - (InfernoState*)mc->state, mc->ctx, x, y, size)) - return 1; - if (inf_forecast_local_overlaps_player( - x, y, size, mc->player_x, mc->player_y)) - return 1; - return inf_forecast_local_npc_occupied( - mc->npcs, mc->self_idx, x, y, size); -} - -static int inf_forecast_local_npc_blocked_ignore_player( - void* ctx, - int x, - int y, - int size -) { - InfForecastMoveCtx* mc = (InfForecastMoveCtx*)ctx; - if (inf_npc_environment_blocked_ctx( - (InfernoState*)mc->state, mc->ctx, x, y, size)) - return 1; - return inf_forecast_local_npc_occupied( - mc->npcs, mc->self_idx, x, y, size); -} - -static void inf_forecast_local_copy_npcs( - const InfernoState* s, - InfForecastNpcLocal npcs[INF_MAX_NPCS] -) { - memset(npcs, 0, sizeof(InfForecastNpcLocal) * INF_MAX_NPCS); - for (int i = 0; i < INF_MAX_NPCS; i++) { - const InfNPC* npc = &s->npcs[i]; - if (!npc->active || npc->death_ticks > 0) continue; - npcs[i] = (InfForecastNpcLocal){ - .active = 1, - .type = npc->type, - .x = npc->x, - .y = npc->y, - .size = inf_npc_effective_size(npc), - .hp = npc->hp, - .attack_timer = npc->attack_timer, - .stun_timer = npc->stun_timer, - .frozen_ticks = npc->frozen_ticks, - .dig_freeze_timer = npc->dig_freeze_timer, - .dig_attack_delay = npc->dig_attack_delay, - .blob_scanned_prayer = npc->blob_scanned_prayer, - .had_los_last_tick = npc->had_los_last_tick, - .jad_attack_style = (npc->type == INF_NPC_JAD) - ? inf_npc_jad_const(npc)->attack_style - : ATTACK_STYLE_NONE, - .attack_style = npc->attack_style, - .aggro_target = npc->aggro_target, - }; - } -} - - -static int inf_forecast_local_has_los_to_area( - const InfernoState* s, - const InfernoContext* ctx, - const InfForecastNpcLocal* npc, - int target_x, - int target_y, - int target_size -) { - const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; - return inf_los_clear_ctx( - s, ctx, - npc->x, npc->y, npc->size, - target_x, target_y, target_size, - stats->attack_range); -} - -static int inf_forecast_local_has_los_to_player( - const InfernoState* s, - const InfernoContext* ctx, - const InfForecastNpcLocal* npc, - int target_x, - int target_y -) { - return inf_forecast_local_has_los_to_area( - s, ctx, npc, target_x, target_y, 1); -} - -static InfTargetArea inf_forecast_local_target_area( - const InfernoState* s, - const InfForecastNpcLocal npcs[INF_MAX_NPCS], - const InfForecastNpcLocal* npc, - int player_x, - int player_y -) { - if (npc->aggro_target >= 0 && npc->aggro_target < INF_MAX_NPCS && - npcs[npc->aggro_target].active) { - const InfForecastNpcLocal* target = &npcs[npc->aggro_target]; - return (InfTargetArea){ - .x = target->x, - .y = target->y, - .size = target->size, - .is_player = 0, - }; - } - - if (npc->type == INF_NPC_NIBBLER) { - int pillar_idx = s->nibbler_target_pillar; - if (pillar_idx >= 0 && pillar_idx < INF_NUM_PILLARS && - s->pillars[pillar_idx].active) { - return (InfTargetArea){ - .x = s->pillars[pillar_idx].x, - .y = s->pillars[pillar_idx].y, - .size = INF_PILLAR_SIZE, - .is_player = 0, - }; - } - } - - return (InfTargetArea){ - .x = player_x, - .y = player_y, - .size = 1, - .is_player = 1, - }; -} - -static int inf_forecast_local_planned_style( - const InfForecastNpcLocal* npc -) { - if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) { - OverheadPrayer scanned = (OverheadPrayer)npc->blob_scanned_prayer; - if (scanned == PRAYER_PROTECT_MAGIC) return ATTACK_STYLE_RANGED; - if (scanned == PRAYER_PROTECT_RANGED) return ATTACK_STYLE_MAGIC; - } - if (npc->type == INF_NPC_JAD) - return npc->jad_attack_style; - return npc->attack_style; -} - -static int inf_forecast_local_melee_fallback_possible_at_tile( - int player_x, - int player_y, - const InfForecastNpcLocal* npc, - const InfNPCStats* stats, - int planned_style, - int dist -) { - if (!stats->can_melee || planned_style == ATTACK_STYLE_MELEE || dist != 1) - return 0; - - return inf_npc_melee_contact_allows_attack_at_tile( - player_x, player_y, npc->type, npc->x, npc->y, npc->size, dist); -} - -static int inf_forecast_local_attack_style_options_mask_at_tile( - int player_x, - int player_y, - const InfForecastNpcLocal* npc, - const InfNPCStats* stats, - int planned_style, - int dist -) { - int mask = inf_attack_style_mask_bit(planned_style); - if (inf_forecast_local_melee_fallback_possible_at_tile( - player_x, player_y, npc, stats, planned_style, dist)) - mask |= INF_STYLE_MASK_MELEE; - return mask; -} - -static int inf_forecast_local_jad_unknown_style_mask( - const InfForecastNpcLocal* npc -) { - if (npc->type != INF_NPC_JAD) return 0; - if (npc->jad_attack_style != ATTACK_STYLE_NONE) return 0; - return INF_STYLE_MASK_RANGED | INF_STYLE_MASK_MAGIC; -} - -static void inf_forecast_local_record_overlap_danger( - const InfernoState* s, - InfForecastNpcLocal* npc, - InfStepOutForecastAction* action, - int tick_idx -) { - const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; - if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer < 0) { - npc->blob_scanned_prayer = (int)s->player.prayer; - inf_step_out_forecast_record_blob_scan(action, tick_idx); - } - - int style_mask = inf_forecast_local_jad_unknown_style_mask(npc); - if (style_mask == 0) { - int planned_style = inf_forecast_local_planned_style(npc); - style_mask = inf_attack_style_mask_bit(planned_style); - } - if (stats->can_melee) - style_mask |= INF_STYLE_MASK_MELEE; - if (style_mask == 0) return; - inf_step_out_forecast_record_style_mask( - action, tick_idx, npc->type, stats, style_mask); -} - -static void inf_forecast_local_move_npc( - const InfernoState* s, - const InfernoContext* ctx, - InfForecastNpcLocal npcs[INF_MAX_NPCS], - int idx, - InfStepOutForecastAction* action, - int tick_idx -) { - InfForecastNpcLocal* npc = &npcs[idx]; - if (!npc->active || npc->hp <= 0) return; - if (npc->stun_timer > 0) return; - if (npc->dig_freeze_timer > 0) return; - if (npc->frozen_ticks > 0) return; - if (npc->type == INF_NPC_NIBBLER || npc->type == INF_NPC_ZUK_SHIELD) - return; - - const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; - if (!stats->can_move) return; - - if (inf_forecast_local_overlaps_player( - npc->x, npc->y, npc->size, action->land_x, action->land_y)) { - inf_forecast_local_record_overlap_danger(s, npc, action, tick_idx); - return; - } - - InfTargetArea target = inf_forecast_local_target_area( - s, npcs, npc, action->land_x, action->land_y); - if (target.is_player && npc->aggro_target >= 0) - npc->aggro_target = -1; - - int has_los = inf_forecast_local_has_los_to_area( - s, ctx, npc, target.x, target.y, target.size); - if (has_los) { - return; - } - if (npc->attack_timer > stats->attack_speed) { - return; - } - - InfForecastMoveCtx move_ctx = { - .state = s, - .ctx = ctx, - .npcs = npcs, - .self_idx = idx, - .player_x = action->land_x, - .player_y = action->land_y, - }; - EncounterNpcStepPolicy step_policy; - encounter_npc_blocked_fn blocked; - if (target.is_player) { - step_policy = stats->attack_range == 1 - ? ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE - : ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET; - blocked = inf_forecast_local_npc_blocked_ignore_player; - } else { - step_policy = stats->attack_range == 1 - ? ENCOUNTER_NPC_STEP_STOP_AT_MELEE - : ENCOUNTER_NPC_STEP_TRAVEL_TARGET; - blocked = inf_forecast_local_npc_blocked; - } - uint32_t rng = s->rng_state; - encounter_npc_step_toward_policy( - &npc->x, &npc->y, target.x, target.y, npc->size, - target.size, step_policy, blocked, &move_ctx, NULL, &rng); - -} - -static void inf_forecast_local_attack_npc( - const InfernoState* s, - const InfernoContext* ctx, - InfForecastNpcLocal* npc, - InfStepOutForecastAction* action, - int tick_idx -) { - if (!npc->active || npc->hp <= 0) return; - const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; - if (npc->attack_timer > 0) npc->attack_timer--; - if (npc->stun_timer > 0) { npc->stun_timer--; return; } - if (npc->dig_freeze_timer > 0 || npc->dig_attack_delay > 0) return; - if (npc->type == INF_NPC_ZUK_SHIELD || npc->type == INF_NPC_NIBBLER) - return; - if (npc->aggro_target >= 0) return; - if (inf_forecast_local_overlaps_player( - npc->x, npc->y, npc->size, action->land_x, action->land_y)) { - inf_forecast_local_record_overlap_danger(s, npc, action, tick_idx); - return; - } - - int has_los_now = 0; - if (npc->type == INF_NPC_BLOB && stats->attack_range > 1) { - has_los_now = inf_forecast_local_has_los_to_player( - s, ctx, npc, action->land_x, action->land_y); - if (npc->blob_scanned_prayer < 0 && - has_los_now && - !npc->had_los_last_tick) { - npc->blob_scanned_prayer = (int)s->player.prayer; - npc->attack_style = inf_forecast_blob_scan_style(s->player.prayer); - npc->had_los_last_tick = has_los_now; - npc->attack_timer = stats->attack_speed; - inf_step_out_forecast_record_blob_scan(action, tick_idx); - return; - } - npc->had_los_last_tick = has_los_now; - } - if (npc->attack_timer > 0) return; - - if (npc->type != INF_NPC_BLOB && stats->attack_range > 1) { - has_los_now = inf_forecast_local_has_los_to_player( - s, ctx, npc, action->land_x, action->land_y); - } - - if (stats->attack_range > 1 && - !(npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) && - !has_los_now) return; - - int dist = encounter_dist_to_npc( - action->land_x, action->land_y, npc->x, npc->y, npc->size); - if (dist == 0 || dist > stats->attack_range) return; - if (stats->attack_range == 1 && - !inf_npc_melee_contact_allows_attack_at_tile( - action->land_x, action->land_y, (InfNPCType)npc->type, - npc->x, npc->y, npc->size, dist)) - return; - - if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer < 0) { - npc->blob_scanned_prayer = (int)s->player.prayer; - npc->attack_style = inf_forecast_blob_scan_style(s->player.prayer); - npc->attack_timer = stats->attack_speed; - inf_step_out_forecast_record_blob_scan(action, tick_idx); - return; - } - - int style_mask = inf_forecast_local_jad_unknown_style_mask(npc); - if (style_mask == 0) { - int planned_style = inf_forecast_local_planned_style(npc); - if (planned_style == ATTACK_STYLE_NONE) return; - style_mask = inf_forecast_local_attack_style_options_mask_at_tile( - action->land_x, action->land_y, npc, - stats, planned_style, dist); - } - if (style_mask == 0) return; - - inf_step_out_forecast_record_style_mask( - action, tick_idx, npc->type, stats, style_mask); - - if (npc->type == INF_NPC_BLOB) - npc->blob_scanned_prayer = -1; - if (npc->type == INF_NPC_JAD) - npc->jad_attack_style = ATTACK_STYLE_NONE; - npc->attack_timer = stats->attack_speed; - if (npc->type == INF_NPC_JAD) - npc->attack_timer = inf_jad_attack_timer(s->wave); -} - -static int inf_forecast_npc_uses_readonly_sim(const InfNPC* npc) { - switch (npc->type) { - case INF_NPC_RANGER: - case INF_NPC_MAGER: - case INF_NPC_JAD: - return 1; - default: - return 0; - } -} - -static void inf_step_out_forecast_conservative_meleer_npc( - const InfNPC* npc, - InfStepOutForecastAction* action -) { - if (npc->type != INF_NPC_MELEER) return; - if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return; - if (npc->dig_freeze_timer > 0 || npc->dig_attack_delay > 0) return; - - const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; - int dist = encounter_dist_to_npc( - action->land_x, action->land_y, npc->x, npc->y, npc->size); - if (dist > INF_STEP_OUT_FORECAST_HORIZON + stats->attack_range) return; - - int attack_ready_tick = npc->attack_timer > 0 ? npc->attack_timer - 1 : 0; - int frozen_move_delay = npc->frozen_ticks > 0 ? npc->frozen_ticks - 1 : 0; - int move_delay = npc->stun_timer > frozen_move_delay ? - npc->stun_timer : frozen_move_delay; - int contact_tick = dist <= stats->attack_range ? - 0 : move_delay + dist - stats->attack_range - 1; - int tick_idx = contact_tick > attack_ready_tick ? - contact_tick : attack_ready_tick; - if (tick_idx < 0 || tick_idx >= INF_STEP_OUT_FORECAST_HORIZON) return; - - inf_step_out_forecast_record_style_mask( - action, tick_idx, npc->type, stats, INF_STYLE_MASK_MELEE); -} - -static void inf_step_out_forecast_readonly_tick( - const InfernoState* s, - const InfernoContext* ctx, - InfForecastNpcLocal npcs[INF_MAX_NPCS], - InfStepOutForecastAction* action, - int tick_idx, - const int slots[INF_MAX_NPCS], - int slot_count -) { - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - InfForecastNpcLocal* npc = &npcs[i]; - if (npc->frozen_ticks > 0) npc->frozen_ticks--; - inf_forecast_local_move_npc( - s, ctx, npcs, i, action, tick_idx); - inf_forecast_local_attack_npc( - s, ctx, npc, action, tick_idx); - } -} - -static void inf_build_step_out_forecast_fast_static_ctx( - const InfernoState* s, - const InfernoContext* ctx, - InfStepOutForecast* out -) { - memset(out, 0, sizeof(*out)); - int forecast_slots[INF_MAX_NPCS]; - int forecast_slot_count = inf_collect_step_out_forecast_slots( - s, forecast_slots); - for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - if (!inf_step_out_forecast_prepare_unique_action_ctx( - s, ctx, out, action_idx, forecast_slot_count)) - continue; - InfStepOutForecastAction* action = &out->actions[action_idx]; - - for (int slot_idx = 0; slot_idx < forecast_slot_count; slot_idx++) { - int i = forecast_slots[slot_idx]; - inf_step_out_forecast_fast_static_npc( - s, ctx, &s->npcs[i], action); - } - inf_step_out_forecast_finalize_action(action); - } -} - -static void inf_build_step_out_forecast_fast_readonly_ctx( - const InfernoState* s, - const InfernoContext* ctx, - InfStepOutForecast* out -) { - memset(out, 0, sizeof(*out)); - int forecast_slots[INF_MAX_NPCS]; - int forecast_slot_count = inf_collect_step_out_forecast_slots( - s, forecast_slots); - int readonly_slots[INF_MAX_NPCS]; - int readonly_slot_count = 0; - for (int slot_idx = 0; slot_idx < forecast_slot_count; slot_idx++) { - int i = forecast_slots[slot_idx]; - if (inf_forecast_npc_uses_readonly_sim(&s->npcs[i])) - readonly_slots[readonly_slot_count++] = i; - } - InfForecastNpcLocal base_npcs[INF_MAX_NPCS]; - if (readonly_slot_count > 0) { - inf_forecast_local_copy_npcs(s, base_npcs); - } - for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - if (!inf_step_out_forecast_prepare_unique_action_ctx( - s, ctx, out, action_idx, forecast_slot_count)) - continue; - InfStepOutForecastAction* action = &out->actions[action_idx]; - - for (int slot_idx = 0; slot_idx < forecast_slot_count; slot_idx++) { - int i = forecast_slots[slot_idx]; - if (inf_forecast_npc_uses_readonly_sim(&s->npcs[i])) continue; - inf_step_out_forecast_fast_static_npc( - s, ctx, &s->npcs[i], action); - inf_step_out_forecast_conservative_meleer_npc(&s->npcs[i], action); - } - - if (readonly_slot_count > 0) { - InfForecastNpcLocal npcs[INF_MAX_NPCS]; - memcpy(npcs, base_npcs, sizeof(base_npcs)); - - for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { - inf_step_out_forecast_readonly_tick( - s, ctx, npcs, action, tick_idx, - readonly_slots, readonly_slot_count); - } - } - inf_step_out_forecast_finalize_action(action); - } -} - -static void inf_build_step_out_forecast_exact_ctx( +static void inf_build_step_out_forecast_ctx( const InfernoState* s, const InfernoContext* ctx, InfStepOutForecast* out @@ -1090,19 +422,10 @@ static void inf_build_step_out_forecast_exact_ctx( } #endif - for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { -#ifdef INF_PROFILE_ENABLED - if (inf_prof_enabled) { - inf_step_out_forecast_tick_profiled( - &sim, &forecast_ctx, action, tick_idx, - forecast_slots, forecast_slot_count); - continue; - } -#endif + for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) inf_step_out_forecast_tick( &sim, &forecast_ctx, action, tick_idx, forecast_slots, forecast_slot_count); - } inf_step_out_forecast_finalize_action(action); } #ifdef INF_PROFILE_ENABLED @@ -1116,71 +439,3 @@ static void inf_build_step_out_forecast_exact_ctx( } #endif } - -static void inf_build_step_out_forecast_ctx( - const InfernoState* s, - const InfernoContext* ctx, - InfStepOutForecast* out -) { - inf_build_step_out_forecast_exact_ctx(s, ctx, out); -} - -static int inf_step_out_forecast_action_dangerous( - const InfStepOutForecastAction* action -) { - if (action->same_tick_mixed_style_conflict) return 1; - for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { - const InfStepOutForecastTick* tick = &action->ticks[tick_idx]; - if (tick->max_hit >= 46) return 1; - } - return 0; -} - -static void inf_compare_step_out_forecasts( - const InfStepOutForecast* exact, - const InfStepOutForecast* fast, - InfStepOutForecastOracleDiff* out -) { - memset(out, 0, sizeof(*out)); - for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - const InfStepOutForecastAction* a = &exact->actions[action_idx]; - const InfStepOutForecastAction* b = &fast->actions[action_idx]; - out->sampled_actions++; - if (a->valid != b->valid || - a->land_x != b->land_x || - a->land_y != b->land_y || - a->same_tick_mixed_style_conflict != b->same_tick_mixed_style_conflict || - a->ranger_mager_offtick_opportunity != b->ranger_mager_offtick_opportunity || - a->melee_fallback_exposure != b->melee_fallback_exposure) { - out->action_feature_mismatches++; - } - int exact_danger = inf_step_out_forecast_action_dangerous(a); - int fast_danger = inf_step_out_forecast_action_dangerous(b); - out->exact_dangerous_actions += exact_danger; - out->fast_dangerous_actions += fast_danger; - if (exact_danger && !fast_danger) - out->dangerous_false_negatives++; - if (!exact_danger && fast_danger) { - out->dangerous_false_positives++; - out->exact_safe_fast_dangerous++; - } - for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { - const InfStepOutForecastTick* at = &a->ticks[tick_idx]; - const InfStepOutForecastTick* bt = &b->ticks[tick_idx]; - if (at->melee_count != bt->melee_count || - at->ranged_count != bt->ranged_count || - at->magic_count != bt->magic_count || - at->blob_scan_count != bt->blob_scan_count || - at->ranger_count != bt->ranger_count || - at->mager_count != bt->mager_count || - at->max_hit != bt->max_hit) { - out->tick_feature_mismatches++; - } - int max_hit_error = at->max_hit - bt->max_hit; - if (max_hit_error < 0) max_hit_error = -max_hit_error; - out->max_hit_abs_error_sum += max_hit_error; - if (max_hit_error > out->max_hit_abs_error_max) - out->max_hit_abs_error_max = max_hit_error; - } - } -} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc index f5e249d634..bce299a25b 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc @@ -78,7 +78,6 @@ static void inf_shuffle_spawns(InfernoState* s) { s->spawn_order[i] = i; encounter_shuffle(s->spawn_order, INF_NUM_SPAWN_POS, &s->rng_state); } -static InfernoContext* inf_legacy_context(void); static const EncounterArenaTopology* inf_route_topology_for_state( const InfernoContext* ctx, const InfernoState* state @@ -385,13 +384,6 @@ static inline int inf_melee_fallback_possible_at_tile( player_x, player_y, npc->type, npc->x, npc->y, npc->size, dist); } -static inline int inf_melee_fallback_possible( - const InfernoState* s, const InfNPC* npc, const InfNPCStats* stats, - int planned_style, int dist -) { - return inf_melee_fallback_possible_at_tile( - s->player.x, s->player.y, npc, stats, planned_style, dist); -} static inline int inf_attack_style_options_mask_at_tile( int player_x, int player_y, const InfNPC* npc, const InfNPCStats* stats, @@ -419,13 +411,6 @@ static inline int inf_attack_style_from_mask(int style_mask) { return ATTACK_STYLE_NONE; } -static inline int inf_style_count_from_mask(int style_mask) { - int mask = style_mask & (INF_STYLE_MASK_MELEE | - INF_STYLE_MASK_RANGED | INF_STYLE_MASK_MAGIC); - return ((mask & INF_STYLE_MASK_MELEE) != 0) + - ((mask & INF_STYLE_MASK_RANGED) != 0) + - ((mask & INF_STYLE_MASK_MAGIC) != 0); -} static inline int inf_npc_style_max_hit( const InfNPCStats* stats, int style @@ -440,32 +425,6 @@ static inline int inf_npc_style_max_hit( return max_hit; } -static inline int inf_npc_style_mask_max_hit( - const InfNPCStats* stats, int style_mask -) { - int max_hit = 0; - if (style_mask & INF_STYLE_MASK_MELEE) { - int style_max = inf_npc_style_max_hit(stats, ATTACK_STYLE_MELEE); - if (style_max > max_hit) max_hit = style_max; - } - if (style_mask & INF_STYLE_MASK_RANGED) { - int style_max = inf_npc_style_max_hit(stats, ATTACK_STYLE_RANGED); - if (style_max > max_hit) max_hit = style_max; - } - if (style_mask & INF_STYLE_MASK_MAGIC) { - int style_max = inf_npc_style_max_hit(stats, ATTACK_STYLE_MAGIC); - if (style_max > max_hit) max_hit = style_max; - } - return max_hit; -} - -static inline int inf_attack_style_obs_preview(int style_mask) { - int primary_mask = style_mask & (INF_STYLE_MASK_RANGED | INF_STYLE_MASK_MAGIC); - int primary_style = inf_attack_style_from_mask(primary_mask); - if (primary_style != ATTACK_STYLE_NONE) - return primary_style; - return inf_attack_style_from_mask(style_mask); -} static inline int inf_attack_style_telegraph_mask( const InfernoState* s, const InfNPC* npc, const InfNPCStats* stats, @@ -552,13 +511,6 @@ static inline InfNpcPlayerThreat inf_npc_player_threat_ctx( return threat; } -static inline InfNpcPlayerThreat inf_npc_player_threat( - const InfernoState* s, - const InfNPC* npc -) { - return inf_npc_player_threat_ctx( - s, inf_legacy_context(), npc); -} static inline int inf_npc_is_live_player_target( const InfernoState* s, @@ -571,31 +523,6 @@ static inline int inf_npc_is_live_player_target( } -static inline int inf_barrage_total_target_count( - const InfernoState* s, - int primary_idx -) { - if (primary_idx < 0 || primary_idx >= INF_MAX_NPCS) return 0; - const InfNPC* primary = &s->npcs[primary_idx]; - if (!primary->active || primary->death_ticks > 0 || - primary->type == INF_NPC_ZUK_SHIELD) - return 0; - - int count = 1; - for (int i = 0; i < INF_MAX_NPCS && count < BARRAGE_MAX_HITS; i++) { - if (i == primary_idx) continue; - const InfNPC* npc = &s->npcs[i]; - if (!npc->active || npc->death_ticks > 0 || - npc->type == INF_NPC_ZUK_SHIELD) - continue; - - int dx = npc->x - primary->x; - int dy = npc->y - primary->y; - if (dx >= -1 && dx <= 1 && dy >= -1 && dy <= 1) - count++; - } - return count; -} static inline int inf_pending_hit_obs_timer(const EncounterPendingHit* ph) { if (ph->check_prayer && ph->prayer_check_delay > 0) @@ -752,16 +679,13 @@ static void inf_store_dead_mob(InfernoState* s, InfNPC* npc) { } static float inf_compute_reward_ctx(InfernoState* s, const InfernoContext* ctx); -static float inf_compute_reward(InfernoState* s); static void inf_spawn_wave(InfernoState* s); static void inf_tick_npcs_ctx(InfernoState* s, InfernoContext* ctx); -static void inf_tick_npcs(InfernoState* s); static void inf_tick_player_ctx( InfernoState* s, InfernoContext* ctx, const int* actions, int can_attack); -static void inf_tick_player(InfernoState* s, const int* actions, int can_attack); static void inf_apply_npc_death(InfernoState* s, int npc_idx); static int inf_mager_resurrect_ctx(InfernoState* s, InfernoContext* ctx, int idx); static void inf_queue_zuk_healer_sparks(InfernoState* s, const InfNPC* npc); @@ -769,7 +693,6 @@ static void inf_resolve_pending_sparks(InfernoState* s); static void inf_invalidate_current_obs_slots(InfernoState* s); static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx); static void inf_ensure_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx); -static void inf_refresh_current_obs_slots(InfernoState* s); static InfConfig inf_default_config(void) { return (InfConfig){ @@ -957,17 +880,6 @@ static void inf_init_context(EncounterContext* context) { static void inf_destroy_context(EncounterContext* context) { } -static InfernoContext* inf_legacy_context(void) { - static InfernoContext ctx; - static Log log; - static int initialized = 0; - if (!initialized) { - inf_init_context_typed(&ctx); - ctx.log = &log; - initialized = 1; - } - return &ctx; -} static void inf_init_state_typed(InfernoState* s, InfernoContext* ctx) { inf_build_npc_stats(); diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc index ebd0bcb007..352fedeef5 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc @@ -29,8 +29,6 @@ typedef enum { } InfLabPillarState; typedef EncounterLabOptionalInt InfLabOptionalInt; -#define INF_LAB_OPTIONAL_INT_UNSET ENCOUNTER_LAB_OPTIONAL_INT_UNSET -#define INF_LAB_OPTIONAL_INT_SET ENCOUNTER_LAB_OPTIONAL_INT_SET typedef struct { int x; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc index d186b5698e..5a66dbd91d 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc @@ -138,11 +138,6 @@ typedef struct { int can_move; } InfNPCStats; -/** attack_decision_ticks: ticks between inf_npc_attack decisions, when that is - not MONSTER_DATABASE attack_speed. 0 means use the database. Jal-Ak is the - only deviation: the wiki gives it attack speed 6 and a protection-prayer - scan 3 ticks before each attack, and inferno spends one decision on the scan - and one on the attack, so its decision cadence is half its attack speed. */ typedef struct { int attack_range; AttackStyle default_style; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc b/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc index 396454b8bb..25af92d011 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc @@ -170,9 +170,6 @@ static void inf_npc_move_ctx( s, npc->x, npc->y, npc->size); } -static void inf_npc_move(InfernoState* s, int idx) { - inf_npc_move_ctx(s, inf_legacy_context(), idx); -} static void inf_meleer_dig_check_ctx(InfernoState* s, const InfernoContext* ctx, int idx) { InfNPC* npc = &s->npcs[idx]; @@ -233,7 +230,3 @@ static void inf_meleer_dig_check_ctx(InfernoState* s, const InfernoContext* ctx, } } } - -static void inf_meleer_dig_check(InfernoState* s, int idx) { - inf_meleer_dig_check_ctx(s, inf_legacy_context(), idx); -} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc index 03924184ac..283f7db758 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -11,8 +11,6 @@ (ENCOUNTER_MAX_PENDING_HITS * INF_FEATURES_PER_HIT) #define INF_PENDING_SPARK_OBS_SIZE \ (INF_SPARK_OBS_SLOTS * INF_FEATURES_PER_SPARK) -#define INF_INVENTORY_OBS_SIZE OSRS_SHARED_INVENTORY_OBS_SIZE -#define INF_EQUIPPED_OBS_SIZE OSRS_SHARED_EQUIPPED_OBS_SIZE #define INF_OBS_AFTER_SHARED OSRS_SHARED_OBS_SIZE #define INF_OBS_AFTER_ENCOUNTER \ @@ -50,10 +48,7 @@ static_assert(INF_OBS_AFTER_SPARKS == 530, "Inferno spark observation boundary"); static_assert(INF_NUM_OBS == 530, "Inferno observation width"); -#define INF_PROTECTION_PRAYER_CODE_SCALE 8.0f -#define INF_OFFENSIVE_PRAYER_CODE_SCALE 4.0f #define INF_WAVE_PHASE_CODE_SCALE 8.0f -#define INF_WEAPON_SET_CODE_SCALE 4.0f #define INF_NPC_TYPE_CODE_SCALE 16.0f #define INF_ATTACK_STYLE_CODE_SCALE 4.0f #define INF_TARGET_CATEGORY_CODE_SCALE 8.0f @@ -212,9 +207,6 @@ static void inf_ensure_current_obs_slots_ctx( inf_refresh_current_obs_slots_ctx(s, ctx); } -static void inf_refresh_current_obs_slots(InfernoState* s) { - inf_refresh_current_obs_slots_ctx(s, inf_legacy_context()); -} static void inf_write_obs_ctx( EncounterState* state, @@ -418,9 +410,6 @@ static int inf_find_target_obs_slot(const InfernoState* s, int npc_slot) { s->current_obs_slots, INF_OBS_NPCS, npc_slot); } -static void inf_write_obs(EncounterState* state, float* obs) { - inf_write_obs_ctx(state, (EncounterContext*)inf_legacy_context(), obs); -} static int inf_obs_slot_is_targetable( InfernoState* s, @@ -449,10 +438,6 @@ static int inf_is_human_targetable_npc_slot_ctx( return inf_obs_slot_is_targetable(s, ctx, inf_find_target_obs_slot(s, npc_slot)); } -static int inf_is_human_targetable_npc_slot(EncounterState* state, int npc_slot) { - return inf_is_human_targetable_npc_slot_ctx( - state, (EncounterContext*)inf_legacy_context(), npc_slot); -} static int inf_drink_has_effect(const InfernoState* s, OsrsConsumableKind kind) { const Player* p = &s->player; @@ -523,7 +508,7 @@ static void inf_write_mask_ctx( !(static_footprint_mask & (UINT32_C(1) << step_bit)) || (!pillars_at_canonical_positions && inf_pillar_footprint_blocked(s, nx, ny, 1)); - mask[primary_offset + action] = blocked ? 0.0f : 1.0f; + if (!blocked) mask[primary_offset + action] = 1.0f; } int has_forced_safe_healer_target = @@ -544,24 +529,24 @@ static void inf_write_mask_ctx( s, ctx, npc_idx)) { is_targetable = 0; } - mask[primary_offset + - inf_primary_attack_action_for_obs_slot(obs_slot)] = - is_targetable ? 1.0f : 0.0f; + if (is_targetable) + mask[primary_offset + + inf_primary_attack_action_for_obs_slot(obs_slot)] = 1.0f; } if (has_forced_safe_healer_target) mask[primary_offset] = 0.0f; int overhead_offset = inf_action_head_mask_offset(INF_HEAD_PRAYER); mask[overhead_offset + ENCOUNTER_OVERHEAD_NO_CHANGE] = 1.0f; - mask[overhead_offset + ENCOUNTER_OVERHEAD_OFF] = - s->player.prayer != PRAYER_NONE ? 1.0f : 0.0f; - mask[overhead_offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE] = - s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[overhead_offset + ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED] = - s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[overhead_offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC] = - s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[overhead_offset + ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION] = - s->player.current_prayer > 0 ? 1.0f : 0.0f; + if (s->player.prayer != PRAYER_NONE) + mask[overhead_offset + ENCOUNTER_OVERHEAD_OFF] = 1.0f; + if (s->player.current_prayer > 0) { + for (int action = ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; + action <= ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC; + action++) + mask[overhead_offset + action] = 1.0f; + mask[overhead_offset + + ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION] = 1.0f; + } for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { int head_offset = @@ -619,37 +604,34 @@ static void inf_write_mask_ctx( int spell_offset = inf_action_head_mask_offset(INF_HEAD_SPELL); mask[spell_offset + OSRS_SPELL_NONE] = 1.0f; - mask[spell_offset + OSRS_SPELL_BLOOD_BARRAGE] = - s->player.current_magic >= BLOOD_BARRAGE_LEVEL ? 1.0f : 0.0f; - mask[spell_offset + OSRS_SPELL_ICE_BARRAGE] = - s->player.current_magic >= ICE_BARRAGE_LEVEL ? 1.0f : 0.0f; + if (s->player.current_magic >= BLOOD_BARRAGE_LEVEL) + mask[spell_offset + OSRS_SPELL_BLOOD_BARRAGE] = 1.0f; + if (s->player.current_magic >= ICE_BARRAGE_LEVEL) + mask[spell_offset + OSRS_SPELL_ICE_BARRAGE] = 1.0f; int spec_offset = inf_action_head_mask_offset(INF_HEAD_SPEC); mask[spec_offset] = 1.0f; - mask[spec_offset + 1] = - !s->player.spec_armed && - s->player.equipped[GEAR_SLOT_WEAPON] == ITEM_TOXIC_BLOWPIPE && - s->player.special_energy >= BLOWPIPE_SPEC_COST ? 1.0f : 0.0f; - mask[spec_offset + 2] = s->player.spec_armed ? 1.0f : 0.0f; + if (!s->player.spec_armed && + s->player.equipped[GEAR_SLOT_WEAPON] == ITEM_TOXIC_BLOWPIPE && + s->player.special_energy >= BLOWPIPE_SPEC_COST) + mask[spec_offset + 1] = 1.0f; + if (s->player.spec_armed) mask[spec_offset + 2] = 1.0f; int offensive_offset = inf_action_head_mask_offset(INF_HEAD_OFFENSIVE); mask[offensive_offset] = 1.0f; - mask[offensive_offset + 1] = - s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE ? 1.0f : 0.0f; - mask[offensive_offset + 2] = - s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[offensive_offset + 3] = - s->player.current_prayer > 0 ? 1.0f : 0.0f; - mask[offensive_offset + 4] = - s->player.current_prayer > 0 ? 1.0f : 0.0f; + if (s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE) + mask[offensive_offset + 1] = 1.0f; + if (s->player.current_prayer > 0) { + for (int action = ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY; + action <= ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY; + action++) + mask[offensive_offset + action] = 1.0f; + } s->total_action_mask_checks++; } -static void inf_write_mask(EncounterState* state, float* mask) { - inf_write_mask_ctx(state, (EncounterContext*)inf_legacy_context(), mask); -} static float inf_get_reward_ctx(EncounterState* state, EncounterContext* context) { @@ -782,13 +764,3 @@ static void inf_fill_render_entities_ctx( encounter_resolve_attack_target(out, n, s->interaction.target_slot); *count = n; } - -static void inf_fill_render_entities( - EncounterState* state, - RenderEntity* out, - int max_entities, - int* count -) { - inf_fill_render_entities_ctx( - state, (EncounterContext*)inf_legacy_context(), out, max_entities, count); -} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc index 32b3234f04..8b6ca41926 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -1,18 +1,13 @@ #line 3605 "encounter_inferno.h" #define INF_HEAD_PRIMARY OSRS_HEAD_PRIMARY #define INF_HEAD_PRAYER OSRS_HEAD_OVERHEAD -#define INF_HEAD_EQUIP_BASE OSRS_HEAD_EQUIP_BASE #define INF_HEAD_EQUIP_SLOT(slot) OSRS_HEAD_EQUIP_SLOT(slot) #define INF_HEAD_EAT OSRS_HEAD_EAT #define INF_HEAD_DRINK OSRS_HEAD_DRINK #define INF_HEAD_SPELL OSRS_HEAD_SPELL #define INF_HEAD_SPEC OSRS_HEAD_SPECIAL #define INF_HEAD_OFFENSIVE OSRS_HEAD_OFFENSIVE -#define INF_INV_CLICK_HEADS (NUM_GEAR_SLOTS + 2) -#define INF_INV_CLICK_DIM OSRS_INVENTORY_CLICK_DIM #define INF_PRIMARY_ATTACK_BASE OSRS_PRIMARY_MOVE_ACTIONS -#define INF_PRIMARY_DIM OSRS_PRIMARY_DIM(INF_OBS_NPCS) -#define INF_SPEC_DIM OSRS_SPECIAL_DIM #define INF_ACTION_DIMS_INIT { OSRS_BASE_ACTION_DIMS(INF_OBS_NPCS) } static const int INF_ACTION_DIMS[INF_NUM_ACTION_HEADS] = INF_ACTION_DIMS_INIT; #define INF_ACTION_MASK_SIZE OSRS_BASE_ACTION_MASK_SIZE(INF_OBS_NPCS) @@ -355,9 +350,6 @@ static void inf_apply_human_player_commands_ctx(InfernoState* s, InfernoContext* inf_refresh_human_loadout_stats(s); } -static void inf_apply_human_player_commands(InfernoState* s) { - inf_apply_human_player_commands_ctx(s, inf_legacy_context()); -} static void inf_tick_player_ctx( InfernoState* s, @@ -816,9 +808,6 @@ static void inf_tick_player_ctx( } } -static void inf_tick_player(InfernoState* s, const int* actions, int can_attack) { - inf_tick_player_ctx(s, inf_legacy_context(), actions, can_attack); -} static int inf_roll_delayed_jad_damage( InfernoState* s, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index 2455d10789..466d99b286 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -68,9 +68,6 @@ static void inf_put_int_ctx( else encounter_abort_unknown_config("inferno", "int", key); } -static void inf_put_int(EncounterState* state, const char* key, int value) { - inf_put_int_ctx(state, (EncounterContext*)inf_legacy_context(), key, value); -} static void inf_put_float_ctx( EncounterState* state, @@ -168,9 +165,6 @@ static void inf_put_float_ctx( else encounter_abort_unknown_config("inferno", "float", key); } -static void inf_put_float(EncounterState* state, const char* key, float value) { - inf_put_float_ctx(state, (EncounterContext*)inf_legacy_context(), key, value); -} static void inf_put_ptr_ctx( EncounterState* state, @@ -562,9 +556,6 @@ static void inf_render_post_tick_ctx( } } -static void inf_render_post_tick(EncounterState* state, EncounterOverlay* ov) { - inf_render_post_tick_ctx(state, (EncounterContext*)inf_legacy_context(), ov); -} static void* inf_get_player_for_input(void* state, int idx) { InfernoState* s = (InfernoState*)state; @@ -656,10 +647,6 @@ static void inf_translate_human_input_ctx( if (hi->pending_spec) actions[INF_HEAD_SPEC] = s->player.spec_armed ? 2 : 1; } -static void inf_translate_human_input(HumanInput* hi, int* actions, EncounterState* state) { - inf_translate_human_input_ctx( - hi, actions, state, (EncounterContext*)inf_legacy_context()); -} static void inf_translate_human_commands_ctx( HumanInput* hi, @@ -740,9 +727,6 @@ static void inf_translate_human_commands_ctx( } } -static void inf_translate_human_commands(HumanInput* hi, int* actions, InfernoState* s) { - inf_translate_human_commands_ctx(hi, actions, s, inf_legacy_context()); -} static void inf_step_human_commands_ctx( EncounterState* state, @@ -763,10 +747,6 @@ static void inf_step_human_commands_ctx( human_input_clear_pending(hi); } -static void inf_step_human_commands(EncounterState* state, HumanInput* hi) { - inf_step_human_commands_ctx( - state, (EncounterContext*)inf_legacy_context(), hi); -} #define INF_SNAPSHOT_MAGIC 0x1FE00001u #define INF_SNAPSHOT_VERSION 22u @@ -875,9 +855,6 @@ static size_t inf_snapshot_size_ctx(EncounterState* state, EncounterContext* con return sizeof(InfSnapshot); } -static size_t inf_snapshot_size(EncounterState* state) { - return inf_snapshot_size_ctx(state, (EncounterContext*)inf_legacy_context()); -} static void inf_snapshot_ctx(EncounterState* state, EncounterContext* context, void* out) { InfernoContext* ctx = (InfernoContext*)context; @@ -892,9 +869,6 @@ static void inf_snapshot_ctx(EncounterState* state, EncounterContext* context, v osrs_interaction_zero_serialized_route_padding(&snap->state.interaction); } -static void inf_snapshot(EncounterState* state, void* out) { - inf_snapshot_ctx(state, (EncounterContext*)inf_legacy_context(), out); -} static void inf_restore_ctx( EncounterState* state, @@ -923,9 +897,6 @@ static void inf_restore_ctx( inf_refresh_after_state_load(dst, ctx); } -static void inf_restore(EncounterState* state, const void* data, size_t n) { - inf_restore_ctx(state, (EncounterContext*)inf_legacy_context(), data, n); -} typedef struct { InfernoState* state; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc index 0c6dd868cc..f78bc6e8bb 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -442,9 +442,6 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint inf_spawn_wave(s); } -static void inf_reset(EncounterState* state, uint32_t seed) { - inf_reset_ctx(state, (EncounterContext*)inf_legacy_context(), seed); -} static int inf_find_free_npc(InfernoState* s) { for (int i = 0; i < INF_MAX_NPCS; i++) { diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc index 8af538d80c..b429eaa481 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc @@ -339,9 +339,6 @@ static float inf_compute_reward_ctx(InfernoState* s, const InfernoContext* ctx) return reward; } -static float inf_compute_reward(InfernoState* s) { - return inf_compute_reward_ctx(s, inf_legacy_context()); -} static float inf_terminal_loss_reward( const InfernoState* s, @@ -427,26 +424,7 @@ static InfIdleDiagnosticSummary inf_idle_diagnostic_summary(const InfernoState* return summary; } -static int inf_has_live_player_target(const InfernoState* s) { - return inf_idle_diagnostic_summary(s).has_live_player_target; -} - -static int inf_has_attackable_player_target_ctx( - const InfernoState* s, - const InfernoContext* ctx -) { - for (int i = 0; i < INF_MAX_NPCS; i++) { - if (inf_npc_is_live_player_target(s, i) && - inf_player_can_attack_npc_from_current_tile_ctx(s, ctx, i)) - return 1; - } - return 0; -} -static int inf_has_attackable_player_target(const InfernoState* s) { - return inf_has_attackable_player_target_ctx( - s, inf_legacy_context()); -} static InfIdleDiagnosticPhase inf_idle_diagnostic_phase_from_summary( const InfernoState* s, @@ -463,35 +441,7 @@ static InfIdleDiagnosticPhase inf_idle_diagnostic_phase_from_summary( return INF_IDLE_PHASE_ZUK_PRE_JAD; } -static InfIdleDiagnosticPhase inf_idle_diagnostic_phase(const InfernoState* s) { - return inf_idle_diagnostic_phase_from_summary( - s, inf_idle_diagnostic_summary(s)); -} - -static int inf_player_has_immediate_threat_ctx( - const InfernoState* s, - const InfernoContext* ctx -) { - if (s->tick_scratch.attacks_fired > 0) return 1; - for (int h = 0; h < s->player_pending_hits.count; h++) { - const EncounterPendingHit* ph = &s->player_pending_hits.hits[h]; - if (ph->check_prayer && inf_pending_hit_obs_timer(ph) <= 1) - return 1; - } - for (int i = 0; i < INF_MAX_NPCS; i++) { - const InfNPC* npc = &s->npcs[i]; - InfNpcPlayerThreat threat = - inf_npc_player_threat_ctx(s, ctx, npc); - if (threat.telegraph_style_mask && threat.can_attack_this_tick) - return 1; - } - return 0; -} -static int inf_player_has_immediate_threat(const InfernoState* s) { - return inf_player_has_immediate_threat_ctx( - s, inf_legacy_context()); -} typedef struct { int has_attackable_player_target; @@ -777,7 +727,3 @@ finish_step: inf_invalidate_current_obs_slots(s); s->episode_return += s->reward; } - -static void inf_step(EncounterState* state, const int* actions) { - inf_step_ctx(state, (EncounterContext*)inf_legacy_context(), actions); -} diff --git a/ocean/osrs/osrs_collision.h b/ocean/osrs/osrs_collision.h index 9fc0ca47ab..7b596e2e47 100644 --- a/ocean/osrs/osrs_collision.h +++ b/ocean/osrs/osrs_collision.h @@ -128,9 +128,6 @@ static inline int collision_get_flags(const CollisionMap* map, int height, int x return region->flags[h][lx][ly]; } -static inline int collision_is_inactive(const CollisionMap* map, int height, int x, int y, int flag) { - return (collision_get_flags(map, height, x, y) & flag) == 0; -} static inline int collision_flags_traversable_step( uint32_t destination_flags, @@ -207,53 +204,13 @@ static inline int collision_traversable_step( dy); } -static inline int collision_traversable_north( - const CollisionMap* map, int height, int x, int y -) { - return collision_traversable_step(map, height, x, y, 0, 1); -} -static inline int collision_traversable_south( - const CollisionMap* map, int height, int x, int y -) { - return collision_traversable_step(map, height, x, y, 0, -1); -} -static inline int collision_traversable_east( - const CollisionMap* map, int height, int x, int y -) { - return collision_traversable_step(map, height, x, y, 1, 0); -} -static inline int collision_traversable_west( - const CollisionMap* map, int height, int x, int y -) { - return collision_traversable_step(map, height, x, y, -1, 0); -} -static inline int collision_traversable_north_east( - const CollisionMap* map, int height, int x, int y -) { - return collision_traversable_step(map, height, x, y, 1, 1); -} -static inline int collision_traversable_north_west( - const CollisionMap* map, int height, int x, int y -) { - return collision_traversable_step(map, height, x, y, -1, 1); -} -static inline int collision_traversable_south_east( - const CollisionMap* map, int height, int x, int y -) { - return collision_traversable_step(map, height, x, y, 1, -1); -} -static inline int collision_traversable_south_west( - const CollisionMap* map, int height, int x, int y -) { - return collision_traversable_step(map, height, x, y, -1, -1); -} static inline int collision_tile_walkable(const CollisionMap* map, int height, int x, int y) { if (map == NULL) return 1; @@ -337,6 +294,54 @@ static inline int los_aabb_overlap( typedef uint32_t (*los_tile_flags_fn)(void* ctx, int x, int y); +typedef int (*los_tile_blocked_fn)(void* ctx, int x, int y); + +static inline int los_tile_ray_clear( + los_tile_blocked_fn tile_blocked, + void* tile_ctx, + int x0, + int y0, + int x1, + int y1 +) { + int dx = x1 - x0; + int dy = y1 - y0; + int adx = dx < 0 ? -dx : dx; + int ady = dy < 0 ? -dy : dy; + if (adx == 0 && ady == 0) return 1; + if (tile_blocked(tile_ctx, x1, y1)) return 0; + + if (adx > ady) { + int x = x0; + int y_fp = y0 * LOS_FP_SCALE + LOS_FP_HALF; + int slope = (dy * LOS_FP_SCALE) / adx; + int x_inc = dx > 0 ? 1 : -1; + if (dy < 0) y_fp--; + while (x != x1) { + x += x_inc; + int y = y_fp >> 16; + if (tile_blocked(tile_ctx, x, y)) return 0; + y_fp += slope; + int new_y = y_fp >> 16; + if (new_y != y && tile_blocked(tile_ctx, x, new_y)) return 0; + } + } else { + int y = y0; + int x_fp = x0 * LOS_FP_SCALE + LOS_FP_HALF; + int slope = (dx * LOS_FP_SCALE) / ady; + int y_inc = dy > 0 ? 1 : -1; + if (dx < 0) x_fp--; + while (y != y1) { + y += y_inc; + int x = x_fp >> 16; + if (tile_blocked(tile_ctx, x, y)) return 0; + x_fp += slope; + int new_x = x_fp >> 16; + if (new_x != x && tile_blocked(tile_ctx, new_x, y)) return 0; + } + } + return 1; +} typedef struct { const LOSBlocker* blockers; @@ -431,65 +436,43 @@ static inline int los_has_line_of_sight_with_flags( return 1; } -static int has_line_of_sight( - const LOSBlocker* blockers, - int blocker_count, - int x1, - int y1, - int x2, - int y2, - int src_size, - int range -) { - LOSBlockerFlagsContext ctx = {blockers, blocker_count}; - return los_has_line_of_sight_with_flags( - los_blocker_tile_flags, &ctx, - x1, y1, x2, y2, src_size, range); -} static inline int los_intervals_overlap(int a0, int a1, int b0, int b1) { return !(a1 < b0 || b1 < a0); } +static inline int entity_has_line_of_sight_with_flags( + los_tile_flags_fn tile_flags, + void* tile_flags_ctx, + int ax, + int ay, + int a_size, + int tx, + int ty, + int t_size, + int range); static inline int entity_has_line_of_sight( - const LOSBlocker* blockers, int blocker_count, - int ax, int ay, int a_size, - int tx, int ty, int t_size, + const LOSBlocker* blockers, + int blocker_count, + int ax, + int ay, + int a_size, + int tx, + int ty, + int t_size, int range ) { - if (range == 1) { - if (los_aabb_overlap(ax, ay, a_size, tx, ty, t_size)) return 0; - - int a_x0 = ax; - int a_x1 = ax + a_size - 1; - int a_y0 = ay; - int a_y1 = ay + a_size - 1; - int t_x0 = tx; - int t_x1 = tx + t_size - 1; - int t_y0 = ty; - int t_y1 = ty + t_size - 1; - - return (a_x1 + 1 == t_x0 && los_intervals_overlap(a_y0, a_y1, t_y0, t_y1)) || - (t_x1 + 1 == a_x0 && los_intervals_overlap(a_y0, a_y1, t_y0, t_y1)) || - (a_y1 + 1 == t_y0 && los_intervals_overlap(a_x0, a_x1, t_x0, t_x1)) || - (t_y1 + 1 == a_y0 && los_intervals_overlap(a_x0, a_x1, t_x0, t_x1)); - } - - int a_px = tx; - if (a_px < ax) a_px = ax; - if (a_px >= ax + a_size) a_px = ax + a_size - 1; - int a_py = ty; - if (a_py < ay) a_py = ay; - if (a_py >= ay + a_size) a_py = ay + a_size - 1; - - int t_px = ax; - if (t_px < tx) t_px = tx; - if (t_px >= tx + t_size) t_px = tx + t_size - 1; - int t_py = ay; - if (t_py < ty) t_py = ty; - if (t_py >= ty + t_size) t_py = ty + t_size - 1; - - return has_line_of_sight(blockers, blocker_count, a_px, a_py, t_px, t_py, 1, range); + LOSBlockerFlagsContext ctx = {blockers, blocker_count}; + return entity_has_line_of_sight_with_flags( + los_blocker_tile_flags, + &ctx, + ax, + ay, + a_size, + tx, + ty, + t_size, + range); } static inline int entity_has_line_of_sight_with_flags( @@ -766,54 +749,12 @@ static uint32_t encounter_arena_topology_los_flags( (LOS_FULL_MASK | LOS_EAST_MASK | LOS_WEST_MASK | LOS_NORTH_MASK | LOS_SOUTH_MASK); } -static int encounter_arena_topology_tile_ray_clear( - EncounterArenaTopologyLosBuildContext* build, - int x0, - int y0, - int x1, - int y1 +static int encounter_arena_topology_los_tile_blocked( + void* data, + int x, + int y ) { - int dx = x1 - x0; - int dy = y1 - y0; - int adx = dx < 0 ? -dx : dx; - int ady = dy < 0 ? -dy : dy; - if (adx == 0 && ady == 0) return 1; - if (encounter_arena_topology_los_flags(build, x1, y1)) return 0; - - if (adx > ady) { - int x = x0; - int y_fp = y0 * LOS_FP_SCALE + LOS_FP_HALF; - int slope = (dy * LOS_FP_SCALE) / adx; - int x_inc = dx > 0 ? 1 : -1; - if (dy < 0) y_fp -= 1; - while (x != x1) { - x += x_inc; - int y = y_fp >> 16; - if (encounter_arena_topology_los_flags(build, x, y)) return 0; - y_fp += slope; - int new_y = y_fp >> 16; - if (new_y != y && - encounter_arena_topology_los_flags(build, x, new_y)) - return 0; - } - } else { - int y = y0; - int x_fp = x0 * LOS_FP_SCALE + LOS_FP_HALF; - int slope = (dx * LOS_FP_SCALE) / ady; - int y_inc = dy > 0 ? 1 : -1; - if (dx < 0) x_fp -= 1; - while (y != y1) { - y += y_inc; - int x = x_fp >> 16; - if (encounter_arena_topology_los_flags(build, x, y)) return 0; - x_fp += slope; - int new_x = x_fp >> 16; - if (new_x != x && - encounter_arena_topology_los_flags(build, new_x, y)) - return 0; - } - } - return 1; + return encounter_arena_topology_los_flags(data, x, y) != 0; } @@ -986,7 +927,8 @@ static inline EncounterArenaTopology* encounter_arena_topology_build( topology->origin_x + target / topology->height; int target_y = topology->origin_y + target % topology->height; - if (encounter_arena_topology_tile_ray_clear( + if (los_tile_ray_clear( + encounter_arena_topology_los_tile_blocked, &los_build, source_x, source_y, diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index 5f58a9b606..bd6a53543e 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -75,9 +75,6 @@ typedef struct { int16_t damage; } EncounterPendingHit; -/* Replicated per NPC per env and streamed every step, so the width is load-bearing for - * throughput, not tidiness. damage trails the int8 run so the record packs to 12 with no - * padding; C++ also requires designated initializers in this order, so keep it last. */ static_assert(sizeof(EncounterPendingHit) == 12, "pending hit record must stay 12 bytes"); typedef struct { diff --git a/ocean/osrs/osrs_encounter_player.h b/ocean/osrs/osrs_encounter_player.h index 43f360a2c0..6d03af1684 100644 --- a/ocean/osrs/osrs_encounter_player.h +++ b/ocean/osrs/osrs_encounter_player.h @@ -20,15 +20,7 @@ typedef enum { OSRS_PLAYER_MOVE_DESTINATION, } OsrsPlayerMoveKind; -/** - * One click per tick. The engine cannot both start an attack and walk somewhere - * else on the same tick: a ground click cancels the entity interaction, an entity - * click cancels the walk. TARGET and MOVE are therefore mutually exclusive by - * construction rather than by caller discipline. - * - * A walk already in flight is state, not a command. It continues under CMD_NONE - * and is cancelled by either new command. - */ +/* One click per tick. */ typedef enum { OSRS_PLAYER_CMD_NONE = 0, OSRS_PLAYER_CMD_TARGET, @@ -412,7 +404,6 @@ static inline OsrsPlayerStepResult osrs_encounter_player_step( } else { osrs_interaction_clear(interaction); } - /* an entity click cancels any walk already in flight */ if (input->dest_x) *input->dest_x = -1; if (input->dest_y) *input->dest_y = -1; } else if (input->command.kind == OSRS_PLAYER_CMD_MOVE) { @@ -429,9 +420,6 @@ static inline OsrsPlayerStepResult osrs_encounter_player_step( } } - /* an active interaction always chases. the only way to walk elsewhere is a - MOVE command, and that cleared the interaction above, so the two are - mutually exclusive by construction rather than by a tie-break policy. */ if (input->command.kind == OSRS_PLAYER_CMD_MOVE && !osrs_interaction_active(interaction)) { result.moved = diff --git a/ocean/osrs/osrs_entity_encoder.cu b/ocean/osrs/osrs_entity_encoder.cu index 72003d551c..d79b7e6fb7 100644 --- a/ocean/osrs/osrs_entity_encoder.cu +++ b/ocean/osrs/osrs_entity_encoder.cu @@ -28,8 +28,6 @@ struct OsrsEntityBranchDescriptor { }; struct OsrsEntityEncoderDescriptor { - const char* env_name; - int obs_size; const OsrsEntityBranchDescriptor* branches; int num_branches; }; @@ -96,28 +94,6 @@ __device__ __forceinline__ float osrs_entity_gelu_grad(float x) { return 0.5f * (1.0f + t) + 0.5f * x * (1.0f - t * t) * dinner; } -__global__ void osrs_entity_gather_inventory( - precision_t* __restrict__ flat, - const precision_t* __restrict__ obs, - int B, - int obs_size -) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - int record_block = OSRS_ENTITY_INV_NUM_RECORDS * OSRS_ENTITY_INV_FEATURES; - int total = B * record_block; - if (idx >= total) return; - int b = idx / record_block; - int off = idx - b * record_block; - int record = off / OSRS_ENTITY_INV_FEATURES; - int feature = off - record * OSRS_ENTITY_INV_FEATURES; - const precision_t* src = obs + (int64_t)b * obs_size + OSRS_ENTITY_INV_START - + record * OSRS_ENTITY_INV_OBS_FEATURES; - int code = (int)lrintf(to_float(src[0]) * (float)OSRS_ITEM_OBS_CODE_SCALE); - assert(code >= 0 && code < OSRS_ITEM_OBS_TABLE_ROWS); - float value = OSRS_ITEM_OBS_TABLE_DEV[code][feature]; - flat[idx] = from_float(value); -} - __global__ void osrs_entity_gather_branch( precision_t* __restrict__ flat, const precision_t* __restrict__ obs, @@ -230,7 +206,7 @@ __global__ void osrs_entity_fused_pool_fwd( int64_t out_idx = (int64_t)b * H + h; out[out_idx] = from_float( to_float(out[out_idx]) + (best_record < 0 ? 0.0f : best)); - argmax[out_idx] = best_record; + if (argmax) argmax[out_idx] = best_record; } __global__ void osrs_entity_fused_l2_wgrad( @@ -405,9 +381,13 @@ static Prec osrs_entity_encoder_forward( puf_mm(&input, &ew->global_w, &a->out, stream); int inventory_batch = B * OSRS_ENTITY_INV_NUM_RECORDS; - osrs_entity_gather_inventory<<< - grid_size(B * OSRS_ENTITY_INV_NUM_RECORDS * OSRS_ENTITY_INV_FEATURES), - BLOCK_SIZE, 0, stream>>>(a->inv_flat.data, input.data, B, ew->obs_size); + osrs_entity_gather_branch<<< + grid_size(inventory_batch * OSRS_ENTITY_INV_FEATURES), + BLOCK_SIZE, 0, stream>>>( + a->inv_flat.data, input.data, B, ew->obs_size, + OSRS_ENTITY_INV_START, OSRS_ENTITY_INV_NUM_RECORDS, + OSRS_ENTITY_INV_OBS_FEATURES, 0, OSRS_ITEM_OBS_CODE_SCALE, + OSRS_ENTITY_BRANCH_ITEM_TABLE); Prec inventory_2d = { .data = a->inv_flat.data, .shape = {inventory_batch, OSRS_ENTITY_INV_FEATURES}, @@ -522,36 +502,19 @@ static void osrs_entity_encoder_init_weights( } } -static void osrs_entity_assert_aligned( - const OsrsEntityEncoderWeights* ew, - int64_t num_elements, - const char* name -) { - if (num_elements % 8 != 0) { - fprintf(stderr, "%s entity encoder: %s numel %lld not a multiple of 8\n", - ew->descriptor->env_name, name, (long long)num_elements); - abort(); - } -} - -static void osrs_entity_register_param( - OsrsEntityEncoderWeights* ew, - Allocator* allocator, - Prec* tensor, - const char* name -) { - osrs_entity_assert_aligned(ew, numel(tensor->shape), name); +static void osrs_entity_register_param(Allocator* allocator, Prec* tensor) { + assert(numel(tensor->shape) % 8 == 0); alloc_register(allocator, tensor); } static void osrs_entity_encoder_reg_params(void* weights, Allocator* allocator) { OsrsEntityEncoderWeights* ew = (OsrsEntityEncoderWeights*)weights; ew->global_w = {.shape = {ew->hidden, ew->obs_size}}; - osrs_entity_register_param(ew, allocator, &ew->global_w, "global_w"); + osrs_entity_register_param(allocator, &ew->global_w); ew->inv_l1_w = {.shape = {OSRS_ENTITY_BOTTLENECK, OSRS_ENTITY_INV_FEATURES}}; ew->inv_l2_w = {.shape = {ew->hidden, OSRS_ENTITY_BOTTLENECK}}; - osrs_entity_register_param(ew, allocator, &ew->inv_l1_w, "inv_l1_w"); - osrs_entity_register_param(ew, allocator, &ew->inv_l2_w, "inv_l2_w"); + osrs_entity_register_param(allocator, &ew->inv_l1_w); + osrs_entity_register_param(allocator, &ew->inv_l2_w); for (int branch_idx = 0; branch_idx < ew->descriptor->num_branches; branch_idx++) { @@ -561,8 +524,8 @@ static void osrs_entity_encoder_reg_params(void* weights, Allocator* allocator) &osrs_entity_branch_weights(ew)[branch_idx]; bw->l1_w = {.shape = {OSRS_ENTITY_BOTTLENECK, features}}; bw->l2_w = {.shape = {ew->hidden, OSRS_ENTITY_BOTTLENECK}}; - osrs_entity_register_param(ew, allocator, &bw->l1_w, "entity_l1_w"); - osrs_entity_register_param(ew, allocator, &bw->l2_w, "entity_l2_w"); + osrs_entity_register_param(allocator, &bw->l1_w); + osrs_entity_register_param(allocator, &bw->l2_w); } } @@ -643,17 +606,15 @@ static void osrs_entity_register_branch_rollout( const OsrsEntityBranchDescriptor* descriptor, OsrsEntityBranchActivations* branch, Allocator* allocator, - int B, - int H + int B ) { + *branch = {}; int features = osrs_entity_branch_features(descriptor); int record_batch = B * descriptor->num_records; branch->flat = {.shape = {record_batch, features}}; branch->z1 = {.shape = {record_batch, OSRS_ENTITY_BOTTLENECK}}; - branch->pool_argmax = {.shape = {B, H}}; alloc_register(allocator, &branch->flat); alloc_register(allocator, &branch->z1); - alloc_register(allocator, &branch->pool_argmax); } static void osrs_entity_encoder_reg_rollout( @@ -665,6 +626,7 @@ static void osrs_entity_encoder_reg_rollout( OsrsEntityEncoderWeights* ew = (OsrsEntityEncoderWeights*)weights; OsrsEntityEncoderActivations* a = (OsrsEntityEncoderActivations*)activations; int H = ew->hidden; + *a = {}; a->out = {.shape = {B, H}}; a->inv_flat = { .shape = {B * OSRS_ENTITY_INV_NUM_RECORDS, OSRS_ENTITY_INV_FEATURES}, @@ -672,36 +634,22 @@ static void osrs_entity_encoder_reg_rollout( a->inv_z1 = { .shape = {B * OSRS_ENTITY_INV_NUM_RECORDS, OSRS_ENTITY_BOTTLENECK}, }; - a->inv_pool_argmax = {.shape = {B, H}}; alloc_register(allocator, &a->out); alloc_register(allocator, &a->inv_flat); alloc_register(allocator, &a->inv_z1); - alloc_register(allocator, &a->inv_pool_argmax); for (int branch_idx = 0; branch_idx < ew->descriptor->num_branches; branch_idx++) { osrs_entity_register_branch_rollout( &ew->descriptor->branches[branch_idx], &osrs_entity_branch_activations(a)[branch_idx], - allocator, B, H); + allocator, B); } } -static void* osrs_entity_encoder_create_weights( - void* self, - const OsrsEntityEncoderDescriptor* descriptor -) { +template +static void* osrs_entity_encoder_create_weights(void* self) { Encoder* encoder = (Encoder*)self; - if (descriptor->num_branches < 0) { - fprintf(stderr, "%s entity encoder: invalid branch count %d\n", - descriptor->env_name, descriptor->num_branches); - abort(); - } - if (encoder->in_dim != descriptor->obs_size) { - fprintf(stderr, "%s entity encoder: env obs size %d != descriptor %d\n", - descriptor->env_name, encoder->in_dim, descriptor->obs_size); - abort(); - } size_t weights_size = sizeof(OsrsEntityEncoderWeights) + (size_t)descriptor->num_branches * sizeof(OsrsEntityBranchWeights); OsrsEntityEncoderWeights* ew = @@ -712,11 +660,8 @@ static void* osrs_entity_encoder_create_weights( return ew; } -static void osrs_entity_encoder_configure( - Encoder* encoder, - create_weights_fn create_weights, - const OsrsEntityEncoderDescriptor* descriptor -) { +template +static void create_osrs_entity_encoder(Encoder* encoder) { *encoder = Encoder{ .forward = osrs_entity_encoder_forward, .backward = osrs_entity_encoder_backward, @@ -724,7 +669,7 @@ static void osrs_entity_encoder_configure( .reg_params = osrs_entity_encoder_reg_params, .reg_train = osrs_entity_encoder_reg_train, .reg_rollout = osrs_entity_encoder_reg_rollout, - .create_weights = create_weights, + .create_weights = osrs_entity_encoder_create_weights, .in_dim = encoder->in_dim, .out_dim = encoder->out_dim, .activation_size = sizeof(OsrsEntityEncoderActivations) + @@ -744,43 +689,10 @@ static constexpr OsrsEntityBranchDescriptor OSRS_EQUIPMENT_ENTITY_BRANCH[] = { }, }; -static constexpr OsrsEntityEncoderDescriptor OSRS_ZULRAH_ENTITY_DESCRIPTOR = { - .env_name = "osrs_zulrah", - .obs_size = 205, +static constexpr OsrsEntityEncoderDescriptor OSRS_EQUIPMENT_ENTITY_DESCRIPTOR = { .branches = OSRS_EQUIPMENT_ENTITY_BRANCH, .num_branches = 1, }; #ifdef ZUL_NUM_OBS -static_assert(OSRS_ZULRAH_ENTITY_DESCRIPTOR.obs_size == ZUL_NUM_OBS); +static_assert(ZUL_NUM_OBS == 205); #endif - -static constexpr OsrsEntityEncoderDescriptor OSRS_PVP_ENTITY_DESCRIPTOR = { - .env_name = "osrs_pvp", - .obs_size = 133, - .branches = OSRS_EQUIPMENT_ENTITY_BRANCH, - .num_branches = 1, -}; - -static void* osrs_zulrah_entity_encoder_create_weights(void* self) { - return osrs_entity_encoder_create_weights( - self, &OSRS_ZULRAH_ENTITY_DESCRIPTOR); -} - -static void* osrs_pvp_entity_encoder_create_weights(void* self) { - return osrs_entity_encoder_create_weights( - self, &OSRS_PVP_ENTITY_DESCRIPTOR); -} - -static void create_osrs_zulrah_encoder(Encoder* encoder) { - osrs_entity_encoder_configure( - encoder, - osrs_zulrah_entity_encoder_create_weights, - &OSRS_ZULRAH_ENTITY_DESCRIPTOR); -} - -static void create_osrs_pvp_encoder(Encoder* encoder) { - osrs_entity_encoder_configure( - encoder, - osrs_pvp_entity_encoder_create_weights, - &OSRS_PVP_ENTITY_DESCRIPTOR); -} diff --git a/ocean/osrs/osrs_env_profile.h b/ocean/osrs/osrs_env_profile.h index 19fec16bed..201224e0dd 100644 --- a/ocean/osrs/osrs_env_profile.h +++ b/ocean/osrs/osrs_env_profile.h @@ -1,8 +1,6 @@ #include #include -/* Defined the same way in osrs_pathfinding.h, but this header is a standalone template that - * env wrappers include on their own, so it cannot depend on that one being pulled in first. */ #ifndef OSRS_ENV_PROFILE_THREAD_LOCAL #if defined(__cplusplus) #define OSRS_ENV_PROFILE_THREAD_LOCAL thread_local @@ -26,15 +24,6 @@ typedef enum { static int OSRS_ENV_PROFILE_G(_profile_enabled) = -1; -/* Per-thread accumulators, summed only at report time. - * - * This was a single shared array updated under `#pragma omp atomic`, hit ~40 times per env - * step by every worker thread. All slots fit in a handful of cache lines, so each mark became - * a contended read-modify-write and the profiler spent most of its time measuring itself: - * reported cost was ~5.7x the wall-clock truth, and inflated worst for the slots that were - * marked most often -- which distorts the shares, not just the total. - * - * Each thread gets its own cache-line-aligned row, so marks are now plain local adds. */ #define OSRS_ENV_PROFILE_MAX_THREADS 128 #define OSRS_ENV_PROFILE_ROW_DOUBLES \ (((OSRS_ENV_PROFILE_COUNT + 7) / 8) * 8) @@ -81,10 +70,13 @@ static void OSRS_ENV_PROFILE_FN(_profile_add)(int slot, double ms) { OSRS_ENV_PROFILE_G(_profile_ms)[OSRS_ENV_PROFILE_FN(_profile_tid)()][slot] += ms; } + static void OSRS_ENV_PROFILE_FN(_profile_mark)(int enabled, double* last_ms, int slot) { if (!enabled) return; double now = OSRS_ENV_PROFILE_FN(_profile_now_ms)(); - OSRS_ENV_PROFILE_FN(_profile_add)(slot, now - *last_ms); + if (slot < 0 || slot >= OSRS_ENV_PROFILE_COUNT) abort(); + OSRS_ENV_PROFILE_G(_profile_ms)[OSRS_ENV_PROFILE_FN(_profile_tid)()][slot] += + now - *last_ms; *last_ms = now; } @@ -97,8 +89,7 @@ OSRS_ENV_PROFILE_EXPORT const char* OSRS_ENV_PROFILE_FN(_env_profile_name)(int s return OSRS_ENV_PROFILE_G(_profile_names)[slot]; } -/* Called from the reporting thread between epochs, so a plain sum over the per-thread rows is - * enough. Workers may still be marking; a torn double only skews one report. */ +/* Reporting tolerates in-flight marks. */ OSRS_ENV_PROFILE_EXPORT double OSRS_ENV_PROFILE_FN(_env_profile_read_reset_ms)(int slot) { if (slot < 0 || slot >= OSRS_ENV_PROFILE_COUNT) abort(); double value = 0.0; diff --git a/ocean/osrs/osrs_gui.h b/ocean/osrs/osrs_gui.h index f5a8c1b0b0..359747b3f0 100644 --- a/ocean/osrs/osrs_gui.h +++ b/ocean/osrs/osrs_gui.h @@ -580,9 +580,6 @@ static void gui_load_named_asset_range(GuiState* gs, const char* prefix, int fir } } -static int gui_load_ui_interfaces(GuiState* gs) { - return osrs_ui_interfaces_load(&gs->ui_interfaces, OSRS_ASSET("ui/interfaces.bin")); -} static const int GUI_PRAYER_ON_SPRITE_IDS[GUI_NUM_PRAYERS] = { 115, 116, 117, 133, 134, @@ -666,7 +663,7 @@ static void gui_load_sprites(GuiState* gs) { gs->sprites_loaded = 1; int ok = 1; gs->named_asset_count = 0; - gui_load_ui_interfaces(gs); + osrs_ui_interfaces_load(&gs->ui_interfaces, OSRS_ASSET("ui/interfaces.bin")); gui_load_fonts(gs); gui_load_item_stack_variants(gs); @@ -2073,7 +2070,7 @@ static const char* gui_inv_primary_action_label(const InvSlot* inv) { &cell, OSRS_CLICK_TICK_FIRST); switch (resolution.click_action) { case OSRS_CLICK_EQUIP: { - int gear_slot = item_idx != ITEM_NONE ? item_to_gear_slot(item_idx) : -1; + int gear_slot = item_idx != ITEM_NONE ? osrs_item_gear_slot(item_idx) : -1; return gear_slot == GEAR_SLOT_WEAPON || gear_slot == GEAR_SLOT_AMMO ? "Wield" : "Wear"; @@ -2092,7 +2089,7 @@ static const char* gui_inv_primary_action_label(const InvSlot* inv) { switch (inv->type) { case INV_SLOT_EQUIPMENT: { if (inv->item_db_idx == ITEM_NONE) return NULL; - int gear_slot = item_to_gear_slot(inv->item_db_idx); + int gear_slot = osrs_item_gear_slot(inv->item_db_idx); return gear_slot == GEAR_SLOT_WEAPON || gear_slot == GEAR_SLOT_AMMO ? "Wield" : "Wear"; @@ -2219,7 +2216,7 @@ static InvAction gui_inv_click(GuiState* gs, Player* p, int slot, gs->human_clicked_inv_slot = slot; return INV_ACTION_EQUIP; } - int gear_slot = item_to_gear_slot(inv->item_db_idx); + int gear_slot = osrs_item_gear_slot(inv->item_db_idx); if (gear_slot >= 0) { if (human_active) { human_input_queue_equip_inventory_item(hi, slot, inv->item_db_idx, gear_slot); @@ -2236,7 +2233,7 @@ static InvAction gui_inv_click(GuiState* gs, Player* p, int slot, human_input_queue_eat(hi, 0, slot); gs->human_clicked_inv_slot = slot; } - else { eat_food(p, 0); } + else { osrs_player_eat_food_type(p, FOOD_SHARK); } return INV_ACTION_EAT; case INV_SLOT_KARAMBWAN: if (human_active) { @@ -2244,7 +2241,7 @@ static InvAction gui_inv_click(GuiState* gs, Player* p, int slot, human_input_queue_eat(hi, 1, slot); gs->human_clicked_inv_slot = slot; } - else { eat_food(p, 1); } + else { osrs_player_eat_food_type(p, FOOD_KARAMBWAN); } return INV_ACTION_EAT; case INV_SLOT_BREW: if (human_active) { diff --git a/ocean/osrs/osrs_human_input.h b/ocean/osrs/osrs_human_input.h index 2fc02ccbac..094909f087 100644 --- a/ocean/osrs/osrs_human_input.h +++ b/ocean/osrs/osrs_human_input.h @@ -16,13 +16,7 @@ static void human_set_click_cross(HumanInput* hi, int screen_x, int screen_y, in hi->click_is_attack = is_attack; } -static int human_overhead_click_action(const Player* p, OverheadPrayer target, int set_refresh_action) { - return p->prayer == target ? ENCOUNTER_OVERHEAD_OFF : set_refresh_action; -} -static int human_offensive_click_action(const Player* p, OffensivePrayer target, int set_refresh_action) { - return p->offensive_prayer == target ? ENCOUNTER_OFFENSIVE_OFF : set_refresh_action; -} static int human_gui_rect_contains(Rectangle rect, int mouse_x, int mouse_y) { return mouse_x >= rect.x && mouse_x < rect.x + rect.width && @@ -86,15 +80,16 @@ static int human_apply_prayer_idx(HumanInput* hi, Player* p, GuiPrayerIdx pidx) }; for (size_t i = 0; i < sizeof(overhead_rows) / sizeof(overhead_rows[0]); i++) { if (overhead_rows[i].idx != pidx) continue; - hi->pending_prayer = human_overhead_click_action( - p, overhead_rows[i].target, overhead_rows[i].refresh_action); + hi->pending_prayer = p->prayer == overhead_rows[i].target + ? ENCOUNTER_OVERHEAD_OFF : overhead_rows[i].refresh_action; human_input_queue_overhead_prayer(hi, hi->pending_prayer); return 1; } for (size_t i = 0; i < sizeof(offensive_rows) / sizeof(offensive_rows[0]); i++) { if (offensive_rows[i].idx != pidx) continue; - hi->pending_offensive_prayer = human_offensive_click_action( - p, offensive_rows[i].target, offensive_rows[i].refresh_action); + hi->pending_offensive_prayer = + p->offensive_prayer == offensive_rows[i].target + ? ENCOUNTER_OFFENSIVE_OFF : offensive_rows[i].refresh_action; human_input_queue_offensive_prayer(hi, hi->pending_offensive_prayer); return 1; } @@ -330,107 +325,6 @@ static void human_handle_combat_click(HumanInput* hi, GuiState* gs, Player* p, } } -static void human_pvp_translate_inventory_cell( - int* actions, - const Player* player, - int cell -) { - if (cell < 0 || cell >= OSRS_INVENTORY_SIZE) return; - const OsrsItemContentMetadata* metadata = - osrs_inventory_cell_metadata(&player->inventory_cells[cell]); - int click_action = cell + 1; - if (metadata->click_action == OSRS_CLICK_EQUIP) { - if (metadata->gear_slot >= 0 && metadata->gear_slot < NUM_GEAR_SLOTS) - actions[OSRS_HEAD_EQUIP_SLOT(metadata->gear_slot)] = click_action; - } else if (metadata->click_action == OSRS_CLICK_EAT) { - actions[OSRS_HEAD_EAT] = click_action; - } else if (metadata->click_action == OSRS_CLICK_DRINK) { - actions[OSRS_HEAD_DRINK] = click_action; - } -} - -static int human_pvp_find_consumable_cell( - const Player* player, - OsrsConsumableKind kind -) { - for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { - const OsrsItemContentMetadata* metadata = - osrs_inventory_cell_metadata(&player->inventory_cells[cell]); - if (metadata->consumable_kind == kind) return cell; - } - return -1; -} - -static void human_to_pvp_actions( - HumanInput* hi, - int* actions, - Player* agent -) { - memset(actions, 0, OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); - if (hi->pending_move_x >= 0 && hi->pending_move_y >= 0) { - int dx = hi->pending_move_x - agent->x; - int dy = hi->pending_move_y - agent->y; - if (dx < -2) dx = -2; - if (dx > 2) dx = 2; - if (dy < -2) dy = -2; - if (dy > 2) dy = 2; - for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { - if (ENCOUNTER_MOVE_TARGET_DX[action] == dx && - ENCOUNTER_MOVE_TARGET_DY[action] == dy) { - actions[OSRS_HEAD_PRIMARY] = action; - break; - } - } - } - encounter_translate_prayer(hi, actions, OSRS_HEAD_OVERHEAD); - encounter_translate_offensive_prayer(hi, actions, OSRS_HEAD_OFFENSIVE); - - if (hi->pending_attack) { - actions[OSRS_HEAD_PRIMARY] = OSRS_PRIMARY_MOVE_ACTIONS; - if (hi->pending_spell == PVP_ATTACK_ICE) - actions[OSRS_HEAD_SPELL] = OSRS_SPELL_ICE_BARRAGE; - else if (hi->pending_spell == PVP_ATTACK_BLOOD) - actions[OSRS_HEAD_SPELL] = OSRS_SPELL_BLOOD_BARRAGE; - } - if (hi->pending_food) { - int cell = human_pvp_find_consumable_cell( - agent, OSRS_CONSUMABLE_SHARK_FOOD); - if (cell >= 0) actions[OSRS_HEAD_EAT] = cell + 1; - } - if (hi->pending_karambwan) { - int cell = human_pvp_find_consumable_cell( - agent, OSRS_CONSUMABLE_KARAMBWAN); - if (cell >= 0) actions[OSRS_HEAD_EAT] = cell + 1; - } - OsrsConsumableKind potion_kind = OSRS_CONSUMABLE_NONE; - if (hi->pending_potion == POTION_BREW) - potion_kind = OSRS_CONSUMABLE_BREW; - else if (hi->pending_potion == POTION_RESTORE) - potion_kind = OSRS_CONSUMABLE_SUPER_RESTORE; - else if (hi->pending_potion == POTION_COMBAT) - potion_kind = OSRS_CONSUMABLE_SUPER_COMBAT; - else if (hi->pending_potion == POTION_RANGED) - potion_kind = OSRS_CONSUMABLE_RANGING; - if (potion_kind != OSRS_CONSUMABLE_NONE) { - int cell = human_pvp_find_consumable_cell(agent, potion_kind); - if (cell >= 0) actions[OSRS_HEAD_DRINK] = cell + 1; - } - for (int i = 0; i < hi->commands.count; i++) { - const HumanCommand* command = &hi->commands.items[i]; - if (command->kind == HUMAN_COMMAND_EQUIP_INVENTORY_ITEM || - command->kind == HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK || - command->kind == HUMAN_COMMAND_EAT || - command->kind == HUMAN_COMMAND_DRINK) { - human_pvp_translate_inventory_cell( - actions, agent, command->inventory_slot); - } - } - if (hi->pending_veng) - actions[OSRS_HEAD_SPELL] = OSRS_SPELL_VENGEANCE; - if (hi->pending_spec) - actions[OSRS_HEAD_SPECIAL] = agent->spec_armed ? 2 : 1; -} - #define CLICK_CROSS_NUM_FRAMES 4 #define CLICK_CROSS_ANIM_TICKS 20 diff --git a/ocean/osrs/osrs_interaction.h b/ocean/osrs/osrs_interaction.h index b6d77c89b8..35f183f001 100644 --- a/ocean/osrs/osrs_interaction.h +++ b/ocean/osrs/osrs_interaction.h @@ -60,7 +60,6 @@ static inline void osrs_actor_route_cache_clear(OsrsActorRouteCache* route) { } static inline void osrs_interaction_set(OsrsInteraction* ix, int target_slot) { - if (ix->target_slot == target_slot) return; ix->target_slot = target_slot; } diff --git a/ocean/osrs/osrs_inventory_actions.h b/ocean/osrs/osrs_inventory_actions.h index 50db2a88bf..f94cf138a3 100644 --- a/ocean/osrs/osrs_inventory_actions.h +++ b/ocean/osrs/osrs_inventory_actions.h @@ -58,9 +58,6 @@ static inline int osrs_can_equip_from_cell( return 1; } -/** Equips the cell's item, displacing worn gear (2h weapon/shield rules - * included) back into the inventory. Returns the gear slot equipped into, - * or -1 if the click was not equippable. */ static inline int osrs_equip_from_cell( Player* p, OsrsInventoryCell* cells, @@ -73,10 +70,6 @@ static inline int osrs_equip_from_cell( osrs_inventory_cell_metadata(cell); uint8_t item_idx = metadata->item_idx; int gear_slot = metadata->gear_slot; - if (gear_slot < 0) { - fprintf(stderr, "inventory equip: item %u has no gear slot\n", item_idx); - abort(); - } uint8_t displaced = p->equipped[gear_slot]; if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(item_idx)) { @@ -86,7 +79,7 @@ static inline int osrs_equip_from_cell( displaced = shield; } else { int empty = osrs_first_empty_inventory_cell(cells, cell_idx); - if (empty < 0) return -1; + if (empty < 0) abort(); cells[empty] = osrs_inventory_cell_from_item(shield); } p->equipped[GEAR_SLOT_SHIELD] = ITEM_NONE; @@ -138,8 +131,6 @@ static inline int osrs_can_eat_consumable_kind( abort(); } -/** One tick's worth of inventory-click actions: per-gear-slot equip heads - * plus eat and drink heads, each 0 = no-op or 1..28 = inventory cell + 1. */ typedef struct { int equip_by_slot[NUM_GEAR_SLOTS]; int eat; @@ -157,20 +148,15 @@ typedef struct { OsrsInventoryClickResolution drink_resolution; } OsrsInventoryTickIntent; -static inline OsrsInventoryTickIntent osrs_inventory_tick_intent_empty(void) { - OsrsInventoryTickIntent intent = { - .eat_cell = -1, - .drink_cell = -1, - }; - return intent; -} - static inline OsrsInventoryTickIntent osrs_resolve_inventory_tick_intent( const Player* p, const OsrsInventoryCell* cells, const OsrsInventoryClickActions* clicks ) { - OsrsInventoryTickIntent intent = osrs_inventory_tick_intent_empty(); + OsrsInventoryTickIntent intent = { + .eat_cell = -1, + .drink_cell = -1, + }; int inventory_has_empty_cell = -1; for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { @@ -194,10 +180,6 @@ static inline OsrsInventoryTickIntent osrs_resolve_inventory_tick_intent( p, metadata, !needs_empty_cell || inventory_has_empty_cell)) continue; int gear_slot = metadata->gear_slot; - if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) { - fprintf(stderr, "inventory equip intent: invalid gear slot %d\n", gear_slot); - abort(); - } if (gear_slot != slot) continue; intent.equip_cell_by_slot[slot] = cell_idx; intent.equip_slot_mask |= UINT32_C(1) << slot; @@ -250,8 +232,6 @@ typedef struct { OsrsInventoryClickResolution resolution; } OsrsInventoryApplyStep; -/** Pops the intent's next click in OSRS application order (equip slots - * ascending, then eat, then drink). Returns 0 when the intent is drained. */ static inline int osrs_inventory_intent_next( OsrsInventoryTickIntent* intent, OsrsInventoryApplyStep* out diff --git a/ocean/osrs/osrs_inventory_clicks.h b/ocean/osrs/osrs_inventory_clicks.h index 5e27d4e852..5da932957e 100644 --- a/ocean/osrs/osrs_inventory_clicks.h +++ b/ocean/osrs/osrs_inventory_clicks.h @@ -47,12 +47,11 @@ typedef void (*OsrsInventoryDrinkOneDoseEffectFn)( #define OSRS_INVENTORY_CELL_OBS_FEATURES_COMPACT \ (OSRS_INVENTORY_CELL_OBS_SHARED + OSRS_INVENTORY_CELL_OBS_KIND_UNION) -/* Slot names for the compact record reconstructed by the item-table gather. */ #define OSRS_INVENTORY_CELL_COMPACT_PRESENT 0 #define OSRS_INVENTORY_CELL_COMPACT_DOSE 1 #define OSRS_INVENTORY_CELL_COMPACT_IS_ARMOR 2 #define OSRS_INVENTORY_CELL_COMPACT_IS_WEAPON 3 -/* Union slot: effect_class4[2] on gear, hp_heal on consumables. */ +/* Gear effect, consumable healing. */ #define OSRS_INVENTORY_CELL_COMPACT_HP_HEAL (OSRS_INVENTORY_CELL_OBS_SHARED + 5) #define OSRS_INVENTORY_CELL_OBS_CODE 0 diff --git a/ocean/osrs/osrs_items.h b/ocean/osrs/osrs_items.h index df0cc85914..8fd0aac229 100644 --- a/ocean/osrs/osrs_items.h +++ b/ocean/osrs/osrs_items.h @@ -75,11 +75,6 @@ static inline int item_supports_ancient_autocast(uint8_t item_index) { return item_index == ITEM_KODAI_WAND || item_index == ITEM_DRAGON_HUNTER_WAND; } -static inline int item_is_weapon(uint8_t item_index) { - if (item_index >= NUM_ITEMS) return 0; - return ITEM_DATABASE[item_index].slot == SLOT_WEAPON; -} - static inline int get_item_attack_style(uint8_t item_index) { switch (item_index) { case ITEM_WHIP: diff --git a/ocean/osrs/osrs_pathfinding.h b/ocean/osrs/osrs_pathfinding.h index fcd9075a41..889b88f3cd 100644 --- a/ocean/osrs/osrs_pathfinding.h +++ b/ocean/osrs/osrs_pathfinding.h @@ -73,73 +73,50 @@ typedef struct { OsrsLosKind kind; const LOSBlocker* blockers; int blocker_count; - int (*tile_blocked)(void* ctx, int x, int y); + los_tile_blocked_fn tile_blocked; los_tile_flags_fn tile_flags; void* tile_ctx; } OsrsLosQuery; static inline OsrsLosQuery osrs_los_open(void) { - OsrsLosQuery query; - query.kind = OSRS_LOS_OPEN; - query.blockers = NULL; - query.blocker_count = 0; - query.tile_blocked = NULL; - query.tile_flags = NULL; - query.tile_ctx = NULL; - return query; + return (OsrsLosQuery){.kind = OSRS_LOS_OPEN}; } static inline OsrsLosQuery osrs_los_blockers( const LOSBlocker* blockers, int blocker_count ) { - OsrsLosQuery query; - query.kind = OSRS_LOS_BLOCKERS; - query.blockers = blockers; - query.blocker_count = blocker_count; - query.tile_blocked = NULL; - query.tile_flags = NULL; - query.tile_ctx = NULL; - return query; + return (OsrsLosQuery){ + .kind = OSRS_LOS_BLOCKERS, + .blockers = blockers, + .blocker_count = blocker_count, + }; } static inline OsrsLosQuery osrs_los_tile( - int (*tile_blocked)(void* ctx, int x, int y), + los_tile_blocked_fn tile_blocked, void* tile_ctx ) { - OsrsLosQuery query; - query.kind = OSRS_LOS_TILE; - query.blockers = NULL; - query.blocker_count = 0; - query.tile_blocked = tile_blocked; - query.tile_flags = NULL; - query.tile_ctx = tile_ctx; - return query; + return (OsrsLosQuery){ + .kind = OSRS_LOS_TILE, + .tile_blocked = tile_blocked, + .tile_ctx = tile_ctx, + }; } static inline OsrsLosQuery osrs_los_flags( los_tile_flags_fn tile_flags, void* tile_ctx ) { - OsrsLosQuery query; - query.kind = OSRS_LOS_FLAGS; - query.blockers = NULL; - query.blocker_count = 0; - query.tile_blocked = NULL; - query.tile_flags = tile_flags; - query.tile_ctx = tile_ctx; - return query; + return (OsrsLosQuery){ + .kind = OSRS_LOS_FLAGS, + .tile_flags = tile_flags, + .tile_ctx = tile_ctx, + }; } static inline const OsrsLosQuery* osrs_los_open_query(void) { - static const OsrsLosQuery query = { - OSRS_LOS_OPEN, - NULL, - 0, - NULL, - NULL, - NULL, - }; + static const OsrsLosQuery query = {.kind = OSRS_LOS_OPEN}; return &query; } @@ -169,53 +146,6 @@ static inline void osrs_los_require_query( abort(); } -static inline int osrs_los_tile_ray_clear( - const OsrsLosQuery* query, - int x0, int y0, - int x1, int y1 -) { - int dx = x1 - x0; - int dy = y1 - y0; - int adx = dx < 0 ? -dx : dx; - int ady = dy < 0 ? -dy : dy; - if (adx == 0 && ady == 0) return 1; - if (query->tile_blocked(query->tile_ctx, x1, y1)) return 0; - - if (adx > ady) { - int x = x0; - int y_fp = y0 * LOS_FP_SCALE + LOS_FP_HALF; - int slope = (dy * LOS_FP_SCALE) / adx; - int x_inc = dx > 0 ? 1 : -1; - if (dy < 0) y_fp -= 1; - while (x != x1) { - x += x_inc; - int y = y_fp >> 16; - if (query->tile_blocked(query->tile_ctx, x, y)) return 0; - y_fp += slope; - int new_y = y_fp >> 16; - if (new_y != y && - query->tile_blocked(query->tile_ctx, x, new_y)) - return 0; - } - } else { - int y = y0; - int x_fp = x0 * LOS_FP_SCALE + LOS_FP_HALF; - int slope = (dx * LOS_FP_SCALE) / ady; - int y_inc = dy > 0 ? 1 : -1; - if (dx < 0) x_fp -= 1; - while (y != y1) { - y += y_inc; - int x = x_fp >> 16; - if (query->tile_blocked(query->tile_ctx, x, y)) return 0; - x_fp += slope; - int new_x = x_fp >> 16; - if (new_x != x && - query->tile_blocked(query->tile_ctx, new_x, y)) - return 0; - } - } - return 1; -} static inline int osrs_los_clear( const OsrsLosQuery* query, @@ -257,8 +187,13 @@ static inline int osrs_los_clear( if (t_los_y < ty) t_los_y = ty; if (t_los_y >= ty + tsize) t_los_y = ty + tsize - 1; - return osrs_los_tile_ray_clear( - query, t_los_x, t_los_y, p_los_x, p_los_y); + return los_tile_ray_clear( + query->tile_blocked, + query->tile_ctx, + t_los_x, + t_los_y, + p_los_x, + p_los_y); } case OSRS_LOS_FLAGS: @@ -321,10 +256,6 @@ static inline int encounter_player_can_attack( #define OSRS_THREAD_LOCAL _Thread_local #endif -#define PATHFIND_GRID_SIZE 104 -#define PATHFIND_ARENA_MAX 48 -#define PATHFIND_MAX_QUEUE_FULL (PATHFIND_GRID_SIZE * PATHFIND_GRID_SIZE) -#define PATHFIND_MAX_QUEUE_ARENA (PATHFIND_ARENA_MAX * PATHFIND_ARENA_MAX) #define PATHFIND_MAX_FALLBACK_RADIUS 10 #if defined(__GNUC__) || defined(__clang__) @@ -342,6 +273,22 @@ static inline int encounter_player_can_attack( #define VIA_SE 9 #define VIA_NE 12 #define VIA_START 99 +static const int8_t encounter_route_osrs_dx[8] = + {-1, 1, 0, 0, -1, 1, -1, 1}; +static const int8_t encounter_route_osrs_dy[8] = + {0, 0, -1, 1, -1, -1, 1, 1}; +static const int8_t encounter_route_osrs_via[8] = + {VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE}; +static const uint8_t encounter_route_osrs_step_mask[8] = + {8, 16, 2, 64, 1, 4, 32, 128}; +static const int8_t encounter_route_south_dx[8] = + {0, -1, 0, 1, -1, -1, 1, 1}; +static const int8_t encounter_route_south_dy[8] = + {-1, 0, 1, 0, -1, 1, -1, 1}; +static const int8_t encounter_route_south_via[8] = + {VIA_S, VIA_W, VIA_N, VIA_E, VIA_SW, VIA_NW, VIA_SE, VIA_NE}; +static const uint8_t encounter_route_south_step_mask[8] = + {2, 8, 64, 16, 1, 32, 4, 128}; #define ENCOUNTER_ROUTE_MAX_WAYPOINTS 25 @@ -499,6 +446,17 @@ typedef struct { } EncounterRouteScratch; static OSRS_THREAD_LOCAL EncounterRouteScratch encounter_route_scratch; +static inline uint16_t encounter_route_next_generation( + EncounterRouteScratch* scratch +) { + scratch->current_generation++; + if (scratch->current_generation != 0) + return scratch->current_generation; + memset(scratch->generation, 0, sizeof(scratch->generation)); + memset(scratch->target_generation, 0, sizeof(scratch->target_generation)); + memset(scratch->blocker_generation, 0, sizeof(scratch->blocker_generation)); + return ++scratch->current_generation; +} static inline int encounter_route_is_target( const EncounterRouteInput* input, int x, @@ -590,60 +548,9 @@ static inline int encounter_route_dynamic_blocked_cached_at_index( return scratch->blocker_value[index]; } -static inline int encounter_route_dynamic_blocked_cached( - const EncounterRouteInput* input, - EncounterRouteScratch* scratch, - uint16_t generation, - int x, - int y -) { - if (!input->blockers.is_blocked) return 0; - if (!encounter_arena_topology_contains(input->topology, x, y)) - return encounter_route_dynamic_blocked(input, x, y); - int index = encounter_arena_topology_index_raw(input->topology, x, y); - return encounter_route_dynamic_blocked_cached_at_index( - input, scratch, generation, x, y, index); -} -static inline int encounter_route_step_allowed_cached( - const EncounterRouteInput* input, - EncounterRouteScratch* scratch, - uint16_t generation, - int x, - int y, - int dx, - int dy -) { - if (encounter_route_dynamic_blocked_cached( - input, scratch, generation, x + dx, y + dy)) - return 0; - if (dx != 0 && dy != 0 && - (encounter_route_dynamic_blocked_cached( - input, scratch, generation, x, y + dy) || - encounter_route_dynamic_blocked_cached( - input, scratch, generation, x + dx, y))) - return 0; - if (!encounter_arena_topology_step_allowed_assume_finalized_size_in_range( - input->topology, x, y, input->actor_size, dx, dy)) - return 0; - return 1; -} -static inline int encounter_route_footprints_overlap( - int actor_x, - int actor_y, - int actor_size, - int target_x, - int target_y, - int target_size -) { - return - actor_x < target_x + target_size && - target_x < actor_x + actor_size && - actor_y < target_y + target_size && - target_y < actor_y + actor_size; -} static inline int encounter_route_footprints_cardinal_adjacent( int actor_x, @@ -956,14 +863,12 @@ static inline int encounter_route_direction_rank( int dx, int dy ) { - static const int osrs_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int osrs_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; - static const int south_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; - static const int south_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; - const int* rank_dx = - encounter_route_cost_is_osrs(input->cost_policy) ? osrs_dx : south_dx; - const int* rank_dy = - encounter_route_cost_is_osrs(input->cost_policy) ? osrs_dy : south_dy; + const int8_t* rank_dx = encounter_route_cost_is_osrs(input->cost_policy) + ? encounter_route_osrs_dx + : encounter_route_south_dx; + const int8_t* rank_dy = encounter_route_cost_is_osrs(input->cost_policy) + ? encounter_route_osrs_dy + : encounter_route_south_dy; for (int rank = 0; rank < 8; rank++) { if (rank_dx[rank] == dx && rank_dy[rank] == dy) return rank; } @@ -1287,7 +1192,7 @@ static inline EncounterRouteResult encounter_route_escape_overlap( int x = input->source_x + dx; int y = input->source_y + dy; if (!encounter_route_destination_allowed(input, x, y)) continue; - if (encounter_route_footprints_overlap( + if (encounter_entity_footprints_overlap( x, y, input->actor_size, input->target_x, input->target_y, input->target_size)) continue; @@ -1415,14 +1320,14 @@ static inline void encounter_route_reverse_build_result( EncounterReverseRouteField* field, EncounterRouteResult* result ) { - static const int8_t osrs_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int8_t osrs_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; - static const int8_t south_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; - static const int8_t south_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; const int8_t* direction_dx = - encounter_route_cost_is_osrs(input->cost_policy) ? osrs_dx : south_dx; + encounter_route_cost_is_osrs(input->cost_policy) + ? encounter_route_osrs_dx + : encounter_route_south_dx; const int8_t* direction_dy = - encounter_route_cost_is_osrs(input->cost_policy) ? osrs_dy : south_dy; + encounter_route_cost_is_osrs(input->cost_policy) + ? encounter_route_osrs_dy + : encounter_route_south_dy; const EncounterArenaTopology* topology = input->topology; int current_x = input->source_x; int current_y = input->source_y; @@ -1598,16 +1503,8 @@ static inline int encounter_route_try_reverse( source_local_x * topology->height + source_local_y; if (!encounter_route_reverse_field_contains(field, source_index) && field->head < field->tail) { - scratch->current_generation++; - if (scratch->current_generation == 0) { - memset(scratch->generation, 0, sizeof(scratch->generation)); - memset(scratch->target_generation, 0, - sizeof(scratch->target_generation)); - memset(scratch->blocker_generation, 0, - sizeof(scratch->blocker_generation)); - scratch->current_generation = 1; - } - uint16_t blocker_generation = scratch->current_generation; + uint16_t blocker_generation = + encounter_route_next_generation(scratch); while (field->head < field->tail && !encounter_route_reverse_field_contains(field, source_index)) { int current_packed = field->queue[field->head++]; @@ -1618,12 +1515,9 @@ static inline int encounter_route_try_reverse( int current_abs_y = topology->origin_y + current_y; uint16_t next_depth = (uint16_t)( encounter_route_reverse_field_depth(field, current_index) + 1); - static const int8_t dx[8] = - {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int8_t dy[8] = - {0, 0, -1, 1, -1, -1, 1, 1}; - static const uint8_t step_mask[8] = - {8, 16, 2, 64, 1, 4, 32, 128}; + const int8_t* dx = encounter_route_osrs_dx; + const int8_t* dy = encounter_route_osrs_dy; + const uint8_t* step_mask = encounter_route_osrs_step_mask; for (int direction = 0; direction < 8; direction++) { int predecessor_x = current_x - dx[direction]; int predecessor_y = current_y - dy[direction]; @@ -1774,14 +1668,10 @@ static OSRS_ROUTE_NOINLINE int encounter_route_expand_source_field( if (target_edges[local_x] & (UINT64_C(1) << local_y)) return local_x * topology->height + local_y; } - static const int8_t direction_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int8_t direction_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; - static const int8_t direction_via[8] = { - VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE - }; - static const uint8_t direction_step_mask[8] = { - 8, 16, 2, 64, 1, 4, 32, 128 - }; + const int8_t* direction_dx = encounter_route_osrs_dx; + const int8_t* direction_dy = encounter_route_osrs_dy; + const int8_t* direction_via = encounter_route_osrs_via; + const uint8_t* direction_step_mask = encounter_route_osrs_step_mask; const uint8_t* legal_step_masks = topology->legal_step_masks[input->actor_size - 1]; while (field->expanded_count < field->visited_count) { @@ -2241,7 +2131,7 @@ static inline EncounterRouteResult encounter_route_solve( } if ((input->target_kind == ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY || input->target_kind == ENCOUNTER_ROUTE_TARGET_ATTACK_RANGE) && - encounter_route_footprints_overlap( + encounter_entity_footprints_overlap( input->source_x, input->source_y, input->actor_size, input->target_x, input->target_y, input->target_size)) { #ifdef OSRS_ROUTE_PROBE @@ -2276,16 +2166,7 @@ static inline EncounterRouteResult encounter_route_solve( #endif const EncounterArenaTopology* topology = input->topology; EncounterRouteScratch* scratch = &encounter_route_scratch; - scratch->current_generation++; - if (scratch->current_generation == 0) { - memset(scratch->generation, 0, sizeof(scratch->generation)); - memset(scratch->target_generation, 0, - sizeof(scratch->target_generation)); - memset(scratch->blocker_generation, 0, - sizeof(scratch->blocker_generation)); - scratch->current_generation = 1; - } - uint16_t generation = scratch->current_generation; + uint16_t generation = encounter_route_next_generation(scratch); int target_index = -1; if (input->target_kind == ENCOUNTER_ROUTE_TARGET_TILE) { target_index = @@ -2330,32 +2211,19 @@ static inline EncounterRouteResult encounter_route_solve( int head = 0; int tail = 1; int selected_index = -1; - static const int8_t osrs_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int8_t osrs_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; - static const int8_t osrs_via[8] = { - VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE - }; - static const uint8_t osrs_step_mask[8] = { - 8, 16, 2, 64, 1, 4, 32, 128 - }; - static const int8_t south_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; - static const int8_t south_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; - static const int8_t south_via[8] = { - VIA_S, VIA_W, VIA_N, VIA_E, VIA_SW, VIA_NW, VIA_SE, VIA_NE - }; - static const uint8_t south_step_mask[8] = { - 2, 8, 64, 16, 1, 32, 4, 128 - }; int osrs_cost = encounter_route_cost_is_osrs(input->cost_policy); - const int8_t* direction_dx = osrs_cost ? osrs_dx : south_dx; - const int8_t* direction_dy = osrs_cost ? osrs_dy : south_dy; - const int8_t* direction_via = osrs_cost ? osrs_via : south_via; - const uint8_t* direction_step_mask = - osrs_cost ? osrs_step_mask : south_step_mask; - int direction_delta[8]; - for (int i = 0; i < 8; i++) - direction_delta[i] = - direction_dx[i] * topology->height + direction_dy[i]; + const int8_t* direction_dx = osrs_cost + ? encounter_route_osrs_dx + : encounter_route_south_dx; + const int8_t* direction_dy = osrs_cost + ? encounter_route_osrs_dy + : encounter_route_south_dy; + const int8_t* direction_via = osrs_cost + ? encounter_route_osrs_via + : encounter_route_south_via; + const uint8_t* direction_step_mask = osrs_cost + ? encounter_route_osrs_step_mask + : encounter_route_south_step_mask; const uint8_t* legal_step_masks = topology->legal_step_masks[input->actor_size - 1]; if (!input->blockers.is_blocked) { @@ -2409,40 +2277,34 @@ static inline EncounterRouteResult encounter_route_solve( if (scratch->generation[next_index] == generation) continue; int dx = direction_dx[i]; int dy = direction_dy[i]; - if (scratch->blocker_generation[next_index] != generation) { - scratch->blocker_generation[next_index] = generation; - scratch->blocker_value[next_index] = (uint8_t) - input->blockers.is_blocked( - input->blockers.ctx, - topology->origin_x + next_x, - topology->origin_y + next_y, - input->actor_size); - } - if (scratch->blocker_value[next_index]) continue; + if (encounter_route_dynamic_blocked_cached_at_index( + input, + scratch, + generation, + topology->origin_x + next_x, + topology->origin_y + next_y, + next_index)) + continue; if (dx != 0 && dy != 0) { int side_index = current_x * topology->height + next_y; - if (scratch->blocker_generation[side_index] != generation) { - scratch->blocker_generation[side_index] = generation; - scratch->blocker_value[side_index] = (uint8_t) - input->blockers.is_blocked( - input->blockers.ctx, - x, - topology->origin_y + next_y, - input->actor_size); - } - if (scratch->blocker_value[side_index]) continue; + if (encounter_route_dynamic_blocked_cached_at_index( + input, + scratch, + generation, + x, + topology->origin_y + next_y, + side_index)) + continue; side_index = next_x * topology->height + current_y; - if (scratch->blocker_generation[side_index] != generation) { - scratch->blocker_generation[side_index] = generation; - scratch->blocker_value[side_index] = (uint8_t) - input->blockers.is_blocked( - input->blockers.ctx, - topology->origin_x + next_x, - y, - input->actor_size); - } - if (scratch->blocker_value[side_index]) continue; + if (encounter_route_dynamic_blocked_cached_at_index( + input, + scratch, + generation, + topology->origin_x + next_x, + y, + side_index)) + continue; } if (tail >= topology->tile_count) { fprintf(stderr, "OSRS route queue overflow: %d\n", tail); diff --git a/ocean/osrs/osrs_pvp_actions.h b/ocean/osrs/osrs_pvp_actions.h index ead56ff734..ace6467cdb 100644 --- a/ocean/osrs/osrs_pvp_actions.h +++ b/ocean/osrs/osrs_pvp_actions.h @@ -5,6 +5,7 @@ #include "osrs_items.h" #include "osrs_consumables.h" #include "osrs_player_consumables.h" +#include "osrs_human_input_types.h" #include "osrs_pvp_gear.h" #include "osrs_pvp_combat.h" #include "osrs_pvp_movement.h" @@ -15,8 +16,112 @@ /* fury +3 + neitiznot +3, worn in every loadout */ #define PRAYER_BONUS 6 -static void eat_food(Player* p, int is_karambwan) { - osrs_player_eat_food_type(p, is_karambwan ? FOOD_KARAMBWAN : FOOD_SHARK); +typedef enum { + PVP_ATTACK_NONE = 0, + PVP_ATTACK_WEAPON, + PVP_ATTACK_ICE, + PVP_ATTACK_BLOOD, +} PvpAttackIntent; + +static void human_pvp_translate_inventory_cell( + int* actions, + const Player* player, + int cell +) { + if (cell < 0 || cell >= OSRS_INVENTORY_SIZE) return; + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&player->inventory_cells[cell]); + int click_action = cell + 1; + if (metadata->click_action == OSRS_CLICK_EQUIP) { + if (metadata->gear_slot >= 0 && metadata->gear_slot < NUM_GEAR_SLOTS) + actions[OSRS_HEAD_EQUIP_SLOT(metadata->gear_slot)] = click_action; + } else if (metadata->click_action == OSRS_CLICK_EAT) { + actions[OSRS_HEAD_EAT] = click_action; + } else if (metadata->click_action == OSRS_CLICK_DRINK) { + actions[OSRS_HEAD_DRINK] = click_action; + } +} + +static int human_pvp_find_consumable_cell( + const Player* player, + OsrsConsumableKind kind +) { + for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { + const OsrsItemContentMetadata* metadata = + osrs_inventory_cell_metadata(&player->inventory_cells[cell]); + if (metadata->consumable_kind == kind) return cell; + } + return -1; +} + +static void human_to_pvp_actions( + HumanInput* hi, + int* actions, + Player* agent +) { + memset(actions, 0, OSRS_BASE_NUM_ACTION_HEADS * sizeof(int)); + if (hi->pending_move_x >= 0 && hi->pending_move_y >= 0) { + int dx = hi->pending_move_x - agent->x; + int dy = hi->pending_move_y - agent->y; + if (dx < -2) dx = -2; + if (dx > 2) dx = 2; + if (dy < -2) dy = -2; + if (dy > 2) dy = 2; + for (int action = 1; action < OSRS_PRIMARY_MOVE_ACTIONS; action++) { + if (ENCOUNTER_MOVE_TARGET_DX[action] == dx && + ENCOUNTER_MOVE_TARGET_DY[action] == dy) { + actions[OSRS_HEAD_PRIMARY] = action; + break; + } + } + } + encounter_translate_prayer(hi, actions, OSRS_HEAD_OVERHEAD); + encounter_translate_offensive_prayer(hi, actions, OSRS_HEAD_OFFENSIVE); + + if (hi->pending_attack) { + actions[OSRS_HEAD_PRIMARY] = OSRS_PRIMARY_MOVE_ACTIONS; + if (hi->pending_spell == PVP_ATTACK_ICE) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_ICE_BARRAGE; + else if (hi->pending_spell == PVP_ATTACK_BLOOD) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_BLOOD_BARRAGE; + } + if (hi->pending_food) { + int cell = human_pvp_find_consumable_cell( + agent, OSRS_CONSUMABLE_SHARK_FOOD); + if (cell >= 0) actions[OSRS_HEAD_EAT] = cell + 1; + } + if (hi->pending_karambwan) { + int cell = human_pvp_find_consumable_cell( + agent, OSRS_CONSUMABLE_KARAMBWAN); + if (cell >= 0) actions[OSRS_HEAD_EAT] = cell + 1; + } + OsrsConsumableKind potion_kind = OSRS_CONSUMABLE_NONE; + if (hi->pending_potion == POTION_BREW) + potion_kind = OSRS_CONSUMABLE_BREW; + else if (hi->pending_potion == POTION_RESTORE) + potion_kind = OSRS_CONSUMABLE_SUPER_RESTORE; + else if (hi->pending_potion == POTION_COMBAT) + potion_kind = OSRS_CONSUMABLE_SUPER_COMBAT; + else if (hi->pending_potion == POTION_RANGED) + potion_kind = OSRS_CONSUMABLE_RANGING; + if (potion_kind != OSRS_CONSUMABLE_NONE) { + int cell = human_pvp_find_consumable_cell(agent, potion_kind); + if (cell >= 0) actions[OSRS_HEAD_DRINK] = cell + 1; + } + for (int i = 0; i < hi->commands.count; i++) { + const HumanCommand* command = &hi->commands.items[i]; + if (command->kind == HUMAN_COMMAND_EQUIP_INVENTORY_ITEM || + command->kind == HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK || + command->kind == HUMAN_COMMAND_EAT || + command->kind == HUMAN_COMMAND_DRINK) { + human_pvp_translate_inventory_cell( + actions, agent, command->inventory_slot); + } + } + if (hi->pending_veng) + actions[OSRS_HEAD_SPELL] = OSRS_SPELL_VENGEANCE; + if (hi->pending_spec) + actions[OSRS_HEAD_SPECIAL] = agent->spec_armed ? 2 : 1; } static void pvp_apply_drink_one_dose_effect( @@ -330,12 +435,6 @@ static void execute_switches( (void)topology; } -typedef enum { - PVP_ATTACK_NONE = 0, - PVP_ATTACK_WEAPON, - PVP_ATTACK_ICE, - PVP_ATTACK_BLOOD, -} PvpAttackIntent; #define PVP_MOVE_NONE 0 diff --git a/ocean/osrs/osrs_pvp_combat.h b/ocean/osrs/osrs_pvp_combat.h index 043f30023c..1c397e9ea0 100644 --- a/ocean/osrs/osrs_pvp_combat.h +++ b/ocean/osrs/osrs_pvp_combat.h @@ -21,7 +21,8 @@ static inline int pvp_melee_spec_to_item(MeleeSpecWeapon w) { case MELEE_SPEC_SGS: return ITEM_SGS; case MELEE_SPEC_ANCIENT_GS: return ITEM_ANCIENT_GS; case MELEE_SPEC_VESTAS: return ITEM_VESTAS; - default: return ITEM_NONE; + case MELEE_SPEC_NONE: return ITEM_NONE; + default: abort(); } } @@ -33,98 +34,38 @@ static inline int pvp_ranged_spec_to_item(RangedSpecWeapon w) { case RANGED_SPEC_ZCB: return ITEM_ZARYTE_CROSSBOW; case RANGED_SPEC_MSB: return ITEM_MAGIC_SHORTBOW_I; case RANGED_SPEC_MORRIGANS: return ITEM_MORRIGANS_JAVELIN; - default: return ITEM_NONE; + case RANGED_SPEC_NONE: return ITEM_NONE; + default: abort(); } } static inline int pvp_magic_spec_to_item(MagicSpecWeapon w) { switch (w) { case MAGIC_SPEC_VOLATILE_STAFF: return ITEM_VOLATILE_STAFF; - default: return ITEM_NONE; + case MAGIC_SPEC_NONE: return ITEM_NONE; + default: abort(); } } static int get_melee_spec_cost(MeleeSpecWeapon weapon) { + if (weapon == MELEE_SPEC_NONE) return 50; int cost = osrs_spec_cost(pvp_melee_spec_to_item(weapon)); - return cost > 0 ? cost : 50; + if (cost <= 0) abort(); + return cost; } static int get_ranged_spec_cost(RangedSpecWeapon weapon) { + if (weapon == RANGED_SPEC_NONE) return 50; int cost = osrs_spec_cost(pvp_ranged_spec_to_item(weapon)); - return cost > 0 ? cost : 50; + if (cost <= 0) abort(); + return cost; } static int get_magic_spec_cost(MagicSpecWeapon weapon) { + if (weapon == MAGIC_SPEC_NONE) return 50; int cost = osrs_spec_cost(pvp_magic_spec_to_item(weapon)); - return cost > 0 ? cost : 50; -} - -static float get_melee_spec_str_mult(MeleeSpecWeapon weapon) { - switch (weapon) { - case MELEE_SPEC_AGS: return 1.375f; - case MELEE_SPEC_DRAGON_CLAWS: return 1.0f; - case MELEE_SPEC_GRANITE_MAUL: return 1.0f; - case MELEE_SPEC_DRAGON_DAGGER: return 1.15f; - case MELEE_SPEC_VOIDWAKER: return 1.0f; - case MELEE_SPEC_DWH: return 1.25f; - case MELEE_SPEC_BGS: return 1.21f; - case MELEE_SPEC_ZGS: return 1.1f; - case MELEE_SPEC_SGS: return 1.1f; - case MELEE_SPEC_ANCIENT_GS: return 1.1f; - case MELEE_SPEC_VESTAS: return 1.20f; - case MELEE_SPEC_ABYSSAL_DAGGER: return 0.85f; - case MELEE_SPEC_DRAGON_LONGSWORD:return 1.15f; - case MELEE_SPEC_DRAGON_MACE: return 1.5f; - case MELEE_SPEC_ABYSSAL_BLUDGEON:return 1.20f; - default: return 1.0f; - } -} - -static float get_melee_spec_acc_mult(MeleeSpecWeapon weapon) { - switch (weapon) { - case MELEE_SPEC_AGS: return 2.0f; - case MELEE_SPEC_DRAGON_CLAWS: return 1.0f; - case MELEE_SPEC_GRANITE_MAUL: return 1.0f; - case MELEE_SPEC_DRAGON_DAGGER: return 1.15f; - case MELEE_SPEC_VOIDWAKER: return 1.0f; - case MELEE_SPEC_DWH: return 1.25f; - case MELEE_SPEC_BGS: return 2.0f; - case MELEE_SPEC_ZGS: return 2.0f; - case MELEE_SPEC_SGS: return 2.0f; - case MELEE_SPEC_ANCIENT_GS: return 2.0f; - case MELEE_SPEC_VESTAS: return 1.0f; - case MELEE_SPEC_ABYSSAL_DAGGER: return 1.25f; - case MELEE_SPEC_DRAGON_LONGSWORD:return 1.25f; - case MELEE_SPEC_DRAGON_MACE: return 1.25f; - case MELEE_SPEC_ABYSSAL_BLUDGEON:return 1.0f; - default: return 1.0f; - } -} - -static float get_ranged_spec_str_mult(RangedSpecWeapon weapon) { - switch (weapon) { - case RANGED_SPEC_DARK_BOW: return 1.5f; - case RANGED_SPEC_BALLISTA: return 1.25f; - case RANGED_SPEC_ACB: return 1.0f; - case RANGED_SPEC_ZCB: return 1.0f; - case RANGED_SPEC_DRAGON_KNIFE:return 1.0f; - case RANGED_SPEC_MSB: return 1.0f; - case RANGED_SPEC_MORRIGANS: return 1.0f; - default: return 1.0f; - } -} - -static float get_ranged_spec_acc_mult(RangedSpecWeapon weapon) { - switch (weapon) { - case RANGED_SPEC_DARK_BOW: return 1.0f; - case RANGED_SPEC_BALLISTA: return 1.25f; - case RANGED_SPEC_ACB: return 2.0f; - case RANGED_SPEC_ZCB: return 2.0f; - case RANGED_SPEC_DRAGON_KNIFE:return 1.0f; - case RANGED_SPEC_MSB: return 1.0f; - case RANGED_SPEC_MORRIGANS: return 1.0f; - default: return 1.0f; - } + if (cost <= 0) abort(); + return cost; } static inline float get_defence_prayer_mult(Player* p) { @@ -759,8 +700,8 @@ static inline int can_equip_two_handed_weapon(Player* p) { } static inline int can_spec(Player* p) { - int cost = get_melee_spec_cost(p->melee_spec_weapon); - return p->melee_spec_weapon != MELEE_SPEC_NONE && p->special_energy >= cost; + if (p->melee_spec_weapon == MELEE_SPEC_NONE) return 0; + return p->special_energy >= get_melee_spec_cost(p->melee_spec_weapon); } static inline int is_granite_maul_attack_available(Player* p) { diff --git a/ocean/osrs/osrs_pvp_gear.h b/ocean/osrs/osrs_pvp_gear.h index 1bc8fc06f6..e0de4e7be7 100644 --- a/ocean/osrs/osrs_pvp_gear.h +++ b/ocean/osrs/osrs_pvp_gear.h @@ -252,10 +252,6 @@ static inline uint8_t find_best_available( return ITEM_NONE; } -static inline uint8_t find_best_melee_spec(const Player* p) { - return find_best_available(p, GEAR_SLOT_WEAPON, MELEE_SPEC_PRIORITY, MELEE_SPEC_PRIORITY_LEN); -} - static inline uint8_t find_best_ranged_spec(const Player* p) { return find_best_available(p, GEAR_SLOT_WEAPON, RANGE_SPEC_PRIORITY, RANGE_SPEC_PRIORITY_LEN); } @@ -264,10 +260,6 @@ static inline uint8_t find_best_magic_spec(const Player* p) { return find_best_available(p, GEAR_SLOT_WEAPON, MAGIC_SPEC_PRIORITY, MAGIC_SPEC_PRIORITY_LEN); } -static inline int player_has_gmaul(const Player* p) { - return player_has_item_in_slot(p, GEAR_SLOT_WEAPON, ITEM_GRANITE_MAUL); -} - typedef struct { const uint8_t* items; int len; @@ -538,10 +530,6 @@ static inline int remove_item_from_inventory(Player* p, int gear_slot, uint8_t i return 1; } -static inline int item_to_gear_slot(uint8_t item_idx) { - return osrs_item_gear_slot(item_idx); -} - static const uint8_t CHAIN_REPLACES[][2] = { { ITEM_VESTAS, ITEM_WHIP }, { ITEM_ZURIELS_STAFF, ITEM_STAFF_OF_DEAD }, @@ -600,13 +588,13 @@ static const uint8_t CHAIN_REPLACES[][2] = { #define CHAIN_REPLACES_LEN (sizeof(CHAIN_REPLACES) / sizeof(CHAIN_REPLACES[0])) static inline void add_loot_item(Player* p, uint8_t item_idx) { - int gear_slot = item_to_gear_slot(item_idx); + int gear_slot = osrs_item_gear_slot(item_idx); if (gear_slot < 0) return; for (int i = 0; i < (int)CHAIN_REPLACES_LEN; i++) { if (CHAIN_REPLACES[i][1] == item_idx) { uint8_t better = CHAIN_REPLACES[i][0]; - int better_slot = item_to_gear_slot(better); + int better_slot = osrs_item_gear_slot(better); if (better_slot >= 0 && player_has_item_in_slot(p, better_slot, better)) { return; } @@ -615,7 +603,7 @@ static inline void add_loot_item(Player* p, uint8_t item_idx) { uint8_t replaces = UPGRADE_REPLACES[item_idx]; if (replaces != ITEM_NONE) { - int replace_slot = item_to_gear_slot(replaces); + int replace_slot = osrs_item_gear_slot(replaces); if (replace_slot >= 0) { remove_item_from_inventory(p, replace_slot, replaces); } @@ -624,7 +612,7 @@ static inline void add_loot_item(Player* p, uint8_t item_idx) { for (int i = 0; i < (int)CHAIN_REPLACES_LEN; i++) { if (CHAIN_REPLACES[i][0] == item_idx) { uint8_t obsolete = CHAIN_REPLACES[i][1]; - int obs_slot = item_to_gear_slot(obsolete); + int obs_slot = osrs_item_gear_slot(obsolete); if (obs_slot >= 0) { remove_item_from_inventory(p, obs_slot, obsolete); } diff --git a/ocean/osrs/osrs_pvp_movement.h b/ocean/osrs/osrs_pvp_movement.h index d6ae13a5b2..e61ebca097 100644 --- a/ocean/osrs/osrs_pvp_movement.h +++ b/ocean/osrs/osrs_pvp_movement.h @@ -88,59 +88,6 @@ static int select_closest_adjacent_tile( p, candidates, target_x, target_y, out_x, out_y, topology); } -static int select_closest_diagonal_tile( - Player* p, - int target_x, - int target_y, - int* out_x, - int* out_y, - const EncounterArenaTopology* topology -) { - const int candidates[4][2] = { - {target_x + 1, target_y + 1}, - {target_x + 1, target_y - 1}, - {target_x - 1, target_y - 1}, - {target_x - 1, target_y + 1} - }; - return select_closest_candidate_tile( - p, candidates, target_x, target_y, out_x, out_y, topology); -} - -static int select_farcast_tile( - Player* p, - int target_x, - int target_y, - int distance, - int* out_x, - int* out_y, - const EncounterArenaTopology* topology -) { - int raw_dx = p->x - target_x; - int raw_dy = p->y - target_y; - int d = distance; - int dx = raw_dx < -d ? -d : (raw_dx > d ? d : raw_dx); - int dy = raw_dy < -d ? -d : (raw_dy > d ? d : raw_dy); - int adx = abs_int(dx); - int ady = abs_int(dy); - if (adx < d && ady < d) { - if (adx >= ady) - dx = raw_dx >= 0 ? d : -d; - else - dy = raw_dy >= 0 ? d : -d; - } - - int cx = target_x + dx; - int cy = target_y + dy; - if (pvp_topology_destination_selectable(topology, cx, cy)) { - *out_x = cx; - *out_y = cy; - return 1; - } - - return 0; -} - - typedef struct { EncounterArenaTopology* topology; @@ -340,19 +287,6 @@ static int pvp_lookup_attack_target(void* ctx, int target_slot, OsrsAttackTarget return 1; } -static inline OsrsEncounterArena pvp_build_arena( - const EncounterArenaTopology* topology -) { - return (OsrsEncounterArena){ - .topology = topology, - .blockers = {0}, - .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, - .cost_policy = ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS, - .destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, - .attack_geometry = ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY, - }; -} - static inline OsrsPlayerStepResult pvp_step_player_movement( OsrsEnv* env, int agent_idx, @@ -395,7 +329,14 @@ static inline OsrsPlayerStepResult pvp_step_player_movement( return result; } } - OsrsEncounterArena arena = pvp_build_arena(topology); + OsrsEncounterArena arena = { + .topology = topology, + .blockers = {0}, + .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, + .cost_policy = ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS, + .destination_cost_policy = ENCOUNTER_ROUTE_COST_SOUTH_FIRST_BFS, + .attack_geometry = ENCOUNTER_ROUTE_ATTACK_GEOMETRY_TOPOLOGY, + }; OsrsPlayerStepInput input = { .player = p, .interaction = &p->interaction, diff --git a/ocean/osrs/osrs_pvp_observations.h b/ocean/osrs/osrs_pvp_observations.h index 6b5a93d47b..a119f57308 100644 --- a/ocean/osrs/osrs_pvp_observations.h +++ b/ocean/osrs/osrs_pvp_observations.h @@ -57,14 +57,6 @@ static inline int can_use_potion(Player* p, int potion_type) { } } -static inline int can_eat_food(Player* p) { - return osrs_player_can_eat_food_type(p, FOOD_SHARK); -} - -static inline int can_eat_karambwan(Player* p) { - return osrs_player_can_eat_food_type(p, FOOD_KARAMBWAN); -} - static inline int pvp_drink_kind_available( Player* p, OsrsConsumableKind kind diff --git a/ocean/osrs/osrs_pvp_opponents.h b/ocean/osrs/osrs_pvp_opponents.h index ff51923f9d..07f6495598 100644 --- a/ocean/osrs/osrs_pvp_opponents.h +++ b/ocean/osrs/osrs_pvp_opponents.h @@ -114,11 +114,12 @@ static inline void opp_tick_cooldowns(OpponentState* opp) { static inline OppConsumables opp_get_consumables(OpponentState* opp, Player* self) { float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; OppConsumables c; - c.can_food = opp->food_cooldown <= 0 && can_eat_food(self) && hp_pct < 1.0f; + c.can_food = opp->food_cooldown <= 0 && + osrs_player_can_eat_food_type(self, FOOD_SHARK) && hp_pct < 1.0f; c.can_brew = opp->potion_cooldown <= 0 && pvp_drink_kind_available(self, OSRS_CONSUMABLE_BREW); c.can_karambwan = opp->karambwan_cooldown <= 0 && - can_eat_karambwan(self) && hp_pct < 1.0f; + osrs_player_can_eat_food_type(self, FOOD_KARAMBWAN) && hp_pct < 1.0f; c.can_restore = opp->potion_cooldown <= 0 && pvp_drink_kind_available(self, OSRS_CONSUMABLE_SUPER_RESTORE); return c; @@ -1682,7 +1683,8 @@ static void opp_gmaul_combo(OsrsEnv* env, OpponentState* opp, int* actions) { Player* self = &env->players[1]; Player* target = &env->players[0]; float target_hp_pct = (float)target->current_hitpoints / (float)target->base_hitpoints; - int has_gmaul = player_has_gmaul(self); + int has_gmaul = + player_has_item_in_slot(self, GEAR_SLOT_WEAPON, ITEM_GRANITE_MAUL); opp_tick_cooldowns(opp); diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h index ff76b3b8bd..c3f7c4cc7e 100644 --- a/ocean/osrs/osrs_render.h +++ b/ocean/osrs/osrs_render.h @@ -548,10 +548,6 @@ static int render_offhand_uses_shield_block_anim(uint8_t item_idx) { } } -static int render_projectile_profile_value_or(int value, int fallback) { - return osrs_combat_projectile_value_or(value, fallback); -} - static int render_spawn_profile_projectile( RenderClient* rc, const OsrsCombatProjectileProfile* profile, @@ -566,11 +562,11 @@ static int render_spawn_profile_projectile( rc->effects, profile->travel_spotanim_id, src_x, src_y, dst_x, dst_y, delay_client_ticks, duration_client_ticks, - render_projectile_profile_value_or( + osrs_combat_projectile_value_or( profile->projectile_start_height, fallback_start_height), - render_projectile_profile_value_or( + osrs_combat_projectile_value_or( profile->projectile_end_height, fallback_end_height), - render_projectile_profile_value_or( + osrs_combat_projectile_value_or( profile->projectile_angle, fallback_slope), rc->effect_client_tick_counter, rc->spotanims, rc->model_cache, rc->npc_model_cache, diff --git a/ocean/osrs/osrs_types.h b/ocean/osrs/osrs_types.h index de6f25ea41..676fb06a90 100644 --- a/ocean/osrs/osrs_types.h +++ b/ocean/osrs/osrs_types.h @@ -47,13 +47,11 @@ #define BLOOD_BLITZ_MAX_HIT 25 #define BLOOD_BARRAGE_MAX_HIT 29 -/** Percent of damage dealt that a blood spell heals the caster. */ #define BLOOD_RUSH_HEAL_PERCENT 10 #define BLOOD_BURST_HEAL_PERCENT 15 #define BLOOD_BLITZ_HEAL_PERCENT 20 #define BLOOD_BARRAGE_HEAL_PERCENT 25 -/** Ticks between casts, and tile range, for a combat spell cast from a staff. */ #define COMBAT_SPELL_CAST_SPEED 5 #define COMBAT_SPELL_ATTACK_RANGE 10 diff --git a/ocean/osrs/osrs_visual_net.h b/ocean/osrs/osrs_visual_net.h index 225e949714..c8d9c44d66 100644 --- a/ocean/osrs/osrs_visual_net.h +++ b/ocean/osrs/osrs_visual_net.h @@ -2,13 +2,9 @@ #include "osrs_item_obs_generated.h" -/* 5c puffercpu.h dropped _gelu. Keep a local twin so the viewer encoder stays - bit-compatible with the CUDA GELU without carrying a core patch. */ -static inline void osrs_visual_gelu(float* input, float* output, int size) { - for (int i = 0; i < size; i++) { - float x = input[i]; - output[i] = 0.5f * x * (1.0f + tanhf(0.7978845608028654f * (x + 0.044715f * x * x * x))); - } +static inline float osrs_visual_gelu(float x) { + return 0.5f * x * + (1.0f + tanhf(0.7978845608028654f * (x + 0.044715f * x * x * x))); } #define OSRS_ENT_INV_START 52 @@ -48,8 +44,6 @@ typedef enum { ENTITY_RECORD_ITEM_TABLE, } EntityRecordExpansion; - -#define ENTITY_RECORD_MAX_FEATS 64 #define ENTITY_ENCODER_MAX_BRANCHES 3 typedef struct { @@ -72,19 +66,14 @@ typedef struct { EntityPoolDescriptor branches[ENTITY_ENCODER_MAX_BRANCHES]; } EntityEncoderDescriptor; -typedef struct EntityPoolBranch EntityPoolBranch; -struct EntityPoolBranch { - EntityPoolDescriptor descriptor; +typedef struct { + const EntityPoolDescriptor* descriptor; float* l1_w; float* l2_w; - float* z1; - float* h1; - float* e; - unsigned char* active; -}; + float* hidden; +} EntityPoolBranch; -typedef struct EntityEncoder EntityEncoder; -struct EntityEncoder { +typedef struct { float* output; float* global_w; int batch_size; @@ -92,91 +81,33 @@ struct EntityEncoder { int hidden_dim; int num_branches; EntityPoolBranch branches[ENTITY_ENCODER_MAX_BRANCHES]; -}; +} EntityEncoder; static void entity_pool_branch_init( EntityPoolBranch* branch, Weights* weights, int hidden_dim, - const EntityPoolDescriptor* descriptor) { - if (descriptor->feats > ENTITY_RECORD_MAX_FEATS || - descriptor->start < 0 || descriptor->num_recs <= 0 || - descriptor->obs_feats <= 0 || descriptor->code_scale <= 0 || - descriptor->bottleneck <= 0 || descriptor->active_width <= 0 || - descriptor->active_width > descriptor->feats) { - fprintf(stderr, "entity pool branch: invalid shape or encoding contract\n"); - abort(); - } - if (descriptor->expansion == ENTITY_RECORD_TYPE_ONEHOT && - (descriptor->type_onehot <= 0 || - descriptor->feats != descriptor->type_onehot + descriptor->obs_feats - 1)) { - fprintf(stderr, "entity pool branch: stale type-code expansion contract\n"); - abort(); - } - if (descriptor->expansion == ENTITY_RECORD_ITEM_TABLE && - (descriptor->type_onehot != 0 || - descriptor->feats != OSRS_ITEM_OBS_TABLE_COLS || - descriptor->obs_feats != 1)) { - fprintf(stderr, "entity pool branch: stale item-table expansion contract\n"); - abort(); - } - branch->descriptor = *descriptor; + const EntityPoolDescriptor* descriptor, float** scratch) { + branch->descriptor = descriptor; branch->l1_w = get_weights_aligned( weights, descriptor->bottleneck * descriptor->feats); branch->l2_w = get_weights_aligned( weights, hidden_dim * descriptor->bottleneck); - branch->z1 = (float*)calloc( - (size_t)descriptor->num_recs * descriptor->bottleneck, sizeof(float)); - branch->h1 = (float*)calloc( - (size_t)descriptor->num_recs * descriptor->bottleneck, sizeof(float)); - branch->e = (float*)calloc( - (size_t)descriptor->num_recs * hidden_dim, sizeof(float)); - branch->active = (unsigned char*)calloc( - (size_t)descriptor->num_recs, sizeof(unsigned char)); -} - -static int entity_pool_descriptor_is_shared_item_branch( - const EntityPoolDescriptor* descriptor, int start, int num_recs) { - return descriptor->start == start && - descriptor->num_recs == num_recs && - descriptor->feats == OSRS_ENT_ITEM_FEATS && - descriptor->obs_feats == 1 && - descriptor->type_onehot == 0 && - descriptor->code_scale == OSRS_ITEM_OBS_CODE_SCALE && - descriptor->bottleneck == OSRS_ENT_ITEM_BOTTLENECK && - descriptor->active_width == 1 && - descriptor->expansion == ENTITY_RECORD_ITEM_TABLE; + branch->hidden = *scratch; + *scratch += descriptor->num_recs * descriptor->bottleneck; } static EntityEncoder* make_entity_encoder( Weights* weights, int batch_size, int input_dim, int hidden_dim, const EntityEncoderDescriptor* descriptor) { - if (!descriptor || - (descriptor->obs_size > 0 && descriptor->obs_size != input_dim) || - descriptor->num_branches < 2 || - descriptor->num_branches > ENTITY_ENCODER_MAX_BRANCHES || - !entity_pool_descriptor_is_shared_item_branch( - &descriptor->branches[0], - OSRS_ENT_INV_START, - OSRS_ENT_INV_NUM_RECS) || - !entity_pool_descriptor_is_shared_item_branch( - &descriptor->branches[1], - OSRS_ENT_EQUIPPED_START, - OSRS_ENT_EQUIPPED_NUM_RECS)) { - fprintf(stderr, "entity encoder: stale environment descriptor\n"); - abort(); - } + size_t scratch_floats = (size_t)batch_size * hidden_dim; for (int i = 0; i < descriptor->num_branches; i++) { - const EntityPoolDescriptor* branch = &descriptor->branches[i]; - if (branch->start < 0 || - branch->start + branch->num_recs * branch->obs_feats > - input_dim) { - fprintf(stderr, "entity encoder: branch exceeds observation width\n"); - abort(); - } + scratch_floats += (size_t)descriptor->branches[i].num_recs * + descriptor->branches[i].bottleneck; } - size_t out_size = (size_t)batch_size * hidden_dim * sizeof(float); - EntityEncoder* layer = - (EntityEncoder*)calloc(1, sizeof(EntityEncoder) + out_size); - layer->output = (float*)(layer + 1); + EntityEncoder* layer = (EntityEncoder*)calloc( + 1, sizeof(EntityEncoder) + scratch_floats * sizeof(float)); + float* scratch = (float*)(layer + 1); + layer->output = scratch; + scratch += (size_t)batch_size * hidden_dim; layer->global_w = get_weights_aligned(weights, hidden_dim * input_dim); layer->batch_size = batch_size; layer->input_dim = input_dim; @@ -184,39 +115,26 @@ static EntityEncoder* make_entity_encoder( layer->num_branches = descriptor->num_branches; for (int i = 0; i < descriptor->num_branches; i++) { entity_pool_branch_init( - &layer->branches[i], weights, hidden_dim, &descriptor->branches[i]); + &layer->branches[i], weights, hidden_dim, + &descriptor->branches[i], &scratch); } return layer; } static void entity_expand_record( const EntityPoolDescriptor* descriptor, const float* rec, float* out) { - switch (descriptor->expansion) { - case ENTITY_RECORD_TYPE_ONEHOT: { - int code = (int)lrintf(rec[0] * (float)descriptor->code_scale); - if (code < 0 || code > descriptor->type_onehot) { - fprintf(stderr, "entity pool branch: type code %d out of range\n", code); - abort(); - } - for (int i = 0; i < descriptor->type_onehot; i++) - out[i] = (code == i + 1) ? 1.0f : 0.0f; - for (int i = 0; i < descriptor->feats - descriptor->type_onehot; i++) - out[descriptor->type_onehot + i] = rec[1 + i]; - return; - } - case ENTITY_RECORD_ITEM_TABLE: { - int code = (int)lrintf(rec[0] * (float)descriptor->code_scale); - if (code < 0 || code >= OSRS_ITEM_OBS_TABLE_ROWS) { - fprintf(stderr, "entity pool branch: item code %d out of table\n", code); - abort(); - } - for (int i = 0; i < descriptor->feats; i++) - out[i] = OSRS_ITEM_OBS_TABLE[code][i]; - return; - } + int code = (int)lrintf(rec[0] * (float)descriptor->code_scale); + if (descriptor->expansion == ENTITY_RECORD_ITEM_TABLE) { + assert(code >= 0 && code < OSRS_ITEM_OBS_TABLE_ROWS); + for (int i = 0; i < descriptor->feats; i++) + out[i] = OSRS_ITEM_OBS_TABLE[code][i]; + return; } - fprintf(stderr, "entity pool branch: unknown record expansion\n"); - abort(); + assert(code >= 0 && code <= descriptor->type_onehot); + for (int i = 0; i < descriptor->type_onehot; i++) + out[i] = code == i + 1 ? 1.0f : 0.0f; + for (int i = 0; i < descriptor->feats - descriptor->type_onehot; i++) + out[descriptor->type_onehot + i] = rec[1 + i]; } void entity_encoder_forward(EntityEncoder* layer, float* observations) { @@ -225,71 +143,55 @@ void entity_encoder_forward(EntityEncoder* layer, float* observations) { for (int b = 0; b < layer->batch_size; b++) { float* obs = observations + (size_t)b * IN; float* out = layer->output + (size_t)b * H; - for (int o = 0; o < H; o++) { float sum = 0.0f; - for (int i = 0; i < IN; i++) sum += obs[i] * layer->global_w[o * IN + i]; + for (int i = 0; i < IN; i++) + sum += obs[i] * layer->global_w[o * IN + i]; out[o] = sum; } - for (int br = 0; br < layer->num_branches; br++) { - EntityPoolBranch* p = &layer->branches[br]; - const EntityPoolDescriptor* descriptor = &p->descriptor; + EntityPoolBranch* branch = &layer->branches[br]; + const EntityPoolDescriptor* descriptor = branch->descriptor; float* recs = obs + descriptor->start; - float expanded[ENTITY_RECORD_MAX_FEATS]; + unsigned long long active_records = 0; + float expanded[COLO_ENT_NPC_FEATS]; for (int n = 0; n < descriptor->num_recs; n++) { float* rec = recs + n * descriptor->obs_feats; - float* z1n = p->z1 + n * descriptor->bottleneck; + float* hidden = branch->hidden + n * descriptor->bottleneck; entity_expand_record(descriptor, rec, expanded); float active_sum = 0.0f; for (int i = 0; i < descriptor->active_width; i++) active_sum += expanded[i]; - p->active[n] = active_sum > 0.0f; + if (active_sum > 0.0f) + active_records |= 1ULL << n; for (int k = 0; k < descriptor->bottleneck; k++) { - const float* w = p->l1_w + k * descriptor->feats; + const float* weight = branch->l1_w + k * descriptor->feats; float sum = 0.0f; for (int i = 0; i < descriptor->feats; i++) - sum += expanded[i] * w[i]; - z1n[k] = sum; - } - } - osrs_visual_gelu( - p->z1, p->h1, descriptor->num_recs * descriptor->bottleneck); - for (int n = 0; n < descriptor->num_recs; n++) { - float* h1n = p->h1 + n * descriptor->bottleneck; - float* en = p->e + (size_t)n * H; - for (int o = 0; o < H; o++) { - float sum = 0.0f; - for (int k = 0; k < descriptor->bottleneck; k++) - sum += h1n[k] * - p->l2_w[o * descriptor->bottleneck + k]; - en[o] = sum; + sum += expanded[i] * weight[i]; + hidden[k] = osrs_visual_gelu(sum); } } for (int o = 0; o < H; o++) { float best = -INFINITY; - int best_n = -1; + int has_active_record = 0; for (int n = 0; n < descriptor->num_recs; n++) { - if (!p->active[n]) continue; - float v = p->e[(size_t)n * H + o]; - if (v > best) { best = v; best_n = n; } + if ((active_records & (1ULL << n)) == 0) continue; + const float* hidden = + branch->hidden + n * descriptor->bottleneck; + float sum = 0.0f; + for (int k = 0; k < descriptor->bottleneck; k++) + sum += hidden[k] * + branch->l2_w[o * descriptor->bottleneck + k]; + if (sum > best) best = sum; + has_active_record = 1; } - out[o] += (best_n < 0) ? 0.0f : best; + if (has_active_record) out[o] += best; } } } } -void free_entity_encoder(EntityEncoder* layer) { - for (int br = 0; br < layer->num_branches; br++) { - free(layer->branches[br].z1); - free(layer->branches[br].h1); - free(layer->branches[br].e); - free(layer->branches[br].active); - } - free(layer); -} - typedef struct VisualNet VisualNet; struct VisualNet { int num_agents; @@ -307,7 +209,7 @@ struct VisualNet { void visual_net_free(VisualNet* net) { free(net->obs); if (net->encoder) free(net->encoder); - if (net->entity_encoder) free_entity_encoder(net->entity_encoder); + free(net->entity_encoder); free(net->decoder); free_mingru(net->mingru); if (net->multidiscrete) free(net->multidiscrete); diff --git a/ocean/osrs/scripts/check-builds.sh b/ocean/osrs/scripts/check-builds.sh deleted file mode 100755 index 48481feee6..0000000000 --- a/ocean/osrs/scripts/check-builds.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# Neither frontend alone reproduces nvcc: g++ takes _Static_assert as an extension, -# clang++ takes out-of-order designated initializers. Run both. -# --cuda runs the real nvcc build for every osrs env, and only works on the box. -set -uo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" -cd "${ROOT}" - -if [ "${1:-}" = "--cuda" ]; then - status=0 - for dir in ocean/osrs_*/; do - env="$(basename "${dir}")" - [ -f "${dir}/${env}.h" ] || { echo "SKIP ${env} (no ${env}.h wrapper)"; continue; } - log="$(mktemp)" - if ./build.sh "${env}" >"${log}" 2>&1; then - echo "OK ${env}" - else - echo "FAIL ${env}" - grep -m5 -E 'error:|error #' "${log}" || tail -5 "${log}" - status=1 - fi - rm -f "${log}" - done - exit ${status} -fi - -GXX="${GXX:-g++-16}" -CXX_CLANG="${CXX_CLANG:-clang++}" -TU="$(mktemp -d)" -trap 'rm -rf "${TU}"' EXIT -status=0 - -for enc in colosseum inferno zulrah nh_pvp; do - src="${TU}/${enc}.cc" - echo "#include \"ocean/osrs/encounters/encounter_${enc}.h\"" >"${src}" - out="$("${GXX}" -fsyntax-only -std=c++17 -I. "${src}" 2>&1)"; rc=$? - out="${out}$("${CXX_CLANG}" -fsyntax-only -std=c++17 -Werror=c11-extensions -ferror-limit=0 -I. "${src}" 2>&1)" - rc=$((rc + $?)) - if [ ${rc} -eq 0 ]; then - echo "OK encounter_${enc}.h" - else - echo "FAIL encounter_${enc}.h" - printf '%s\n' "${out}" - status=1 - fi -done - -exit ${status} diff --git a/ocean/osrs/tests/bench_colo_step.c b/ocean/osrs/tests/bench_colo_step.c deleted file mode 100644 index 1bd579f8e9..0000000000 --- a/ocean/osrs/tests/bench_colo_step.c +++ /dev/null @@ -1,278 +0,0 @@ -/* Wall-clock cost of one colosseum env step, with no profiler marks compiled in. - * - * Exists to reconcile two numbers that disagree by 5x: the in-env profiler reports ~24400 - * ns/env-step, an earlier campaign recorded 4570. Both cannot be right, and every optimisation - * decision downstream is priced against whichever one is. - * - * Covers exactly what puf_step covers: encounter step, observation write, action mask write. - * - * Equip heads are PINNED. The env memoises on a loadout signature, and uniform random actions - * churn every equip head each tick, which is a genuine signature change any correct memo must - * miss -- that artifact once produced a bogus "87% of env time is gear/DPT" conclusion. - * - * Build with -std=gnu11: plain c11 hides clock_gettime on glibc and _POSIX_C_SOURCE hides - * snprintf on macOS. */ - -#include -#include -#include -#include -#include - -#ifdef _OPENMP -#include -#endif - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -static double now_ns(void) { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (double)ts.tv_sec * 1e9 + (double)ts.tv_nsec; -} - -static inline uint64_t splitmix64(uint64_t* s) { - uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); - z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; - z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; - return z ^ (z >> 31); -} - -static void fill_actions(const ColosseumState* s, uint64_t* rng, - int actions[COLO_NUM_ACTION_HEADS]) { - for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) { - int equip = h >= COLO_HEAD_EQUIP_BASE && h < COLO_HEAD_EQUIP_BASE + NUM_GEAR_SLOTS; - actions[h] = equip ? 0 : (int)(splitmix64(rng) % (uint64_t)COLO_ACTION_DIMS[h]); - } - if (s->modifiers.draft_pending) { - actions[COLO_HEAD_PRIMARY] = 0; - actions[COLO_HEAD_MODIFIER_SELECT] = - 1 + (int)(splitmix64(rng) % COLO_MODIFIER_DRAFT_OPTIONS); - } -} - -typedef struct { double step, obs, mask, total; long n; } Bench; - -/* STREAM-triad-style scan: the machine's practical DRAM ceiling as a function of thread count. - * If the env's parallel efficiency curve has the same shape, the env is bandwidth-bound and - * not, say, lock-bound or oversubscribed. */ -static void bandwidth_scan(void) { - const size_t N = 24u << 20; /* 192 MB per array, far past the 36 MiB L3 */ - double* a = (double*)malloc(N * sizeof(double)); - double* b = (double*)malloc(N * sizeof(double)); - double* c = (double*)malloc(N * sizeof(double)); - if (!a || !b || !c) { printf(" (allocation failed)\n"); return; } - for (size_t i = 0; i < N; i++) { a[i] = 1.0; b[i] = 2.0; c[i] = 0.0; } - - printf("memory bandwidth ceiling (triad, 3 x %zu MB)\n", N * sizeof(double) / (1u << 20)); - printf(" %8s %14s %12s\n", "threads", "GB/s", "vs 1 thread"); - double one = 0.0; - int tc[] = {1, 2, 4, 8, 16, 24, 32}; - for (int i = 0; i < (int)(sizeof(tc)/sizeof(tc[0])); i++) { - int T = tc[i]; - double t0 = now_ns(); - for (int rep = 0; rep < 4; rep++) { -#ifdef _OPENMP - #pragma omp parallel for num_threads(T) schedule(static) -#endif - for (size_t j = 0; j < N; j++) c[j] = a[j] + 3.0 * b[j]; - } - double sec = (now_ns() - t0) / 1e9; - double gbs = 4.0 * 3.0 * N * sizeof(double) / sec / 1e9; - if (i == 0) one = gbs; - printf(" %8d %14.1f %11.2fx\n", T, gbs, gbs / one); - } - printf("\n"); - free(a); free(b); free(c); -} - -/* Same as run_multi but skips the observation and mask writes, removing ~7 KB of stores per - * step while leaving the simulation identical. If the env is bandwidth-bound, dropping those - * bytes should visibly improve parallel efficiency, not just lower the single-thread cost. */ -static Bench run_multi_sim_only(int num_envs, int steps_per_env) { - ColosseumContext* ctxs = (ColosseumContext*)calloc(num_envs, sizeof(ColosseumContext)); - ColosseumState* ss = (ColosseumState*)calloc(num_envs, sizeof(ColosseumState)); - int actions[COLO_NUM_ACTION_HEADS]; - Bench b = {0}; - uint64_t rng = 0xABCDEF01ULL; - for (int e = 0; e < num_envs; e++) { - col_init_context_typed(&ctxs[e]); - col_finalize_route_topology(&ctxs[e]); - col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], (uint32_t)(e + 1)); - } - double t0 = now_ns(); - for (int rep = 0; rep < steps_per_env; rep++) { - for (int e = 0; e < num_envs; e++) { - if (ss[e].episode_over) - col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], - (uint32_t)(e * 7919 + rep + 1)); - fill_actions(&ss[e], &rng, actions); - col_step_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], actions); - b.n++; - } - } - b.total = now_ns() - t0; - free(ctxs); free(ss); - return b; -} - -/* Steps N independent envs round-robin, which is what the trainer does. One env fits in L2; - * N of them do not, and the gap between those two numbers is the working-set cost that a - * single-env benchmark cannot see. */ -static Bench run_multi(int num_envs, int steps_per_env) { - ColosseumContext* ctxs = (ColosseumContext*)calloc(num_envs, sizeof(ColosseumContext)); - ColosseumState* ss = (ColosseumState*)calloc(num_envs, sizeof(ColosseumState)); - static float obs[COLO_NUM_OBS]; - static float mask[COLO_ACTION_MASK_SIZE]; - int actions[COLO_NUM_ACTION_HEADS]; - Bench b = {0}; - uint64_t rng = 0xABCDEF01ULL; - - for (int e = 0; e < num_envs; e++) { - col_init_context_typed(&ctxs[e]); - col_finalize_route_topology(&ctxs[e]); - col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], (uint32_t)(e + 1)); - } - for (int rep = 0; rep < 40; rep++) { - for (int e = 0; e < num_envs; e++) { - fill_actions(&ss[e], &rng, actions); - col_step_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], actions); - } - } - - double t0 = now_ns(); - for (int rep = 0; rep < steps_per_env; rep++) { - for (int e = 0; e < num_envs; e++) { - if (ss[e].episode_over) - col_reset_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], - (uint32_t)(e * 7919 + rep + 1)); - fill_actions(&ss[e], &rng, actions); - col_step_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], actions); - col_write_obs_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], obs); - col_write_mask_ctx((EncounterState*)&ss[e], (EncounterContext*)&ctxs[e], mask); - b.n++; - } - } - b.total = now_ns() - t0; - free(ctxs); free(ss); - return b; -} - -static Bench run(int warmup_steps, int measured_steps) { - ColosseumContext ctx; - ColosseumState s; - static float obs[COLO_NUM_OBS]; - static float mask[COLO_ACTION_MASK_SIZE]; - int actions[COLO_NUM_ACTION_HEADS]; - Bench b = {0}; - - col_init_context_typed(&ctx); - col_finalize_route_topology(&ctx); - memset(&s, 0, sizeof(s)); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 12345u); - uint64_t rng = 0xBEEF1234ULL; - - for (int i = 0; i < warmup_steps + measured_steps; i++) { - int measuring = i >= warmup_steps; - if (s.episode_over) { - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, (uint32_t)(1000 + i)); - } - fill_actions(&s, &rng, actions); - - double t0 = now_ns(); - col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); - double t1 = now_ns(); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - double t2 = now_ns(); - col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); - double t3 = now_ns(); - - if (measuring) { - b.step += t1 - t0; - b.obs += t2 - t1; - b.mask += t3 - t2; - b.total += t3 - t0; - b.n++; - } - } - return b; -} - -int main(void) { - /* One untimed pass so the static arena/LOS tables are built before anything is measured. */ - run(200, 1); - -#ifdef _OPENMP - /* The trainer runs 8192 envs across 32 threads and its profiler reports ~7x the - * single-threaded cost per step. Neither cache pressure nor profiler overhead accounts - * for that, so scan thread count directly with the env count held fixed. */ - bandwidth_scan(); - printf("thread scan: 8192 envs, %d steps each\n", 40); - printf(" %8s %12s %12s %12s\n", "threads", "ns/step", "speedup", "efficiency"); - double t1 = 0.0; - int tcounts[] = {1, 2, 4, 8, 16, 24, 32}; - for (int i = 0; i < (int)(sizeof(tcounts)/sizeof(tcounts[0])); i++) { - int T = tcounts[i]; - int per_thread = 8192 / T; - double t0 = now_ns(); - long total = 0; - #pragma omp parallel num_threads(T) reduction(+:total) - { - Bench m = run_multi(per_thread, 40); - total += m.n; - } - double wall = now_ns() - t0; - double ns = wall * T / (double)total; - double wall_ns = wall / (double)total; - if (i == 0) t1 = wall_ns; - printf(" %8d %12.1f %11.2fx %11.0f%%\n", T, ns, t1 / wall_ns, - 100.0 * (t1 / wall_ns) / T); - } - printf("\n sim only, no obs/mask writes (~7 KB/step less traffic)\n"); - printf(" %8s %12s %12s %12s\n", "threads", "ns/step", "speedup", "efficiency"); - double s1 = 0.0; - for (int i = 0; i < (int)(sizeof(tcounts)/sizeof(tcounts[0])); i++) { - int T = tcounts[i]; - int per_thread = 8192 / T; - double t0 = now_ns(); - long total = 0; - #pragma omp parallel num_threads(T) reduction(+:total) - { - Bench m = run_multi_sim_only(per_thread, 40); - total += m.n; - } - double wall_ns = (now_ns() - t0) / (double)total; - if (i == 0) s1 = wall_ns; - printf(" %8d %12.1f %11.2fx %11.0f%%\n", T, wall_ns * T, s1 / wall_ns, - 100.0 * (s1 / wall_ns) / T); - } - printf("\n"); -#endif - - printf("working-set scan: one thread, N envs round-robin\n"); - printf(" %6s %12s %14s %12s\n", "envs", "ns/step", "state MB", "vs 1 env"); - double base = 0.0; - int counts[] = {1, 8, 64, 256, 1024, 4096, 8192}; - for (int i = 0; i < (int)(sizeof(counts)/sizeof(counts[0])); i++) { - int n = counts[i]; - int per = n <= 64 ? 4000 : (n <= 1024 ? 400 : 60); - Bench m = run_multi(n, per); - double ns = m.total / m.n; - if (i == 0) base = ns; - printf(" %6d %12.1f %14.1f %11.2fx\n", n, ns, - (double)n * (sizeof(ColosseumState) + sizeof(ColosseumContext)) / 1e6, ns / base); - } - printf("\n sizeof(ColosseumState)=%zu sizeof(ColosseumContext)=%zu\n\n", - sizeof(ColosseumState), sizeof(ColosseumContext)); - - Bench b = run(2000, 200000); - printf("colosseum step benchmark, no profiler marks, equip heads pinned\n"); - printf(" measured steps %ld\n", b.n); - printf(" col_step_ctx %8.1f ns/step\n", b.step / b.n); - printf(" col_write_obs_ctx %8.1f ns/step\n", b.obs / b.n); - printf(" col_write_mask_ctx %8.1f ns/step\n", b.mask / b.n); - printf(" TOTAL %8.1f ns/step\n", b.total / b.n); - printf("\n obs is %d floats, mask is %d floats\n", - COLO_NUM_OBS, COLO_ACTION_MASK_SIZE); - return 0; -} diff --git a/ocean/osrs/tests/bench_colosseum_forecast_profile.c b/ocean/osrs/tests/bench_colosseum_forecast_profile.c deleted file mode 100644 index 795f9ea98c..0000000000 --- a/ocean/osrs/tests/bench_colosseum_forecast_profile.c +++ /dev/null @@ -1,617 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include -#include -#include -#include -#include -#include - -#ifdef __APPLE__ -#include -#endif - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -#define BENCH_CORPUS_STATES 512 - -static volatile double bench_sink = 0.0; - -typedef enum { - COLO_BENCH_SETUP_CLEAR = 0, - COLO_BENCH_SETUP_COLLECT, - COLO_BENCH_SETUP_BASE_COPY, - COLO_BENCH_SETUP_FLAGS, - COLO_BENCH_SETUP_PRECOMP, - COLO_BENCH_ACTION_PREP, - COLO_BENCH_RESTORE_NPCS, - COLO_BENCH_RESTORE_FLAGS, - COLO_BENCH_TICK_ADMIN, - COLO_BENCH_MOVEMENT, - COLO_BENCH_ATTACK_AI, - COLO_BENCH_FINALIZE, - COLO_BENCH_OBS_SCORE, - COLO_BENCH_COUNT, -} ColoBenchBucket; - -typedef struct { - uint64_t ns[COLO_BENCH_COUNT]; - uint64_t total_ns; - uint64_t fanout_ns; - uint64_t forecasts; - uint64_t actions_seen; - uint64_t valid_actions; - uint64_t rollout_actions; - uint64_t invalid_or_duplicate_actions; - uint64_t static_threat_actions; - uint64_t rollout_ticks; - uint64_t npc_tick_visits; - uint64_t slot_count_sum; - uint64_t forecast_obs_checksum; -} ColoForecastBenchProfile; - -typedef struct { - ColosseumState states[BENCH_CORPUS_STATES]; - int count; -} ColoBenchCorpus; - -static const char* const COLO_BENCH_BUCKET_NAMES[COLO_BENCH_COUNT] = { - "setup_clear", - "setup_collect_slots", - "setup_base_npc_copy", - "setup_base_flags", - "setup_precompute", - "action_landing_dedup", - "restore_npc_slots", - "restore_npc_flags", - "rollout_tick_admin", - "rollout_movement", - "rollout_attack_ai", - "action_finalize", - "obs_score_encode", -}; - -static uint64_t now_ns(void) { -#ifdef __APPLE__ - static mach_timebase_info_data_t timebase; - if (timebase.denom == 0) { - mach_timebase_info(&timebase); - } - uint64_t ticks = mach_absolute_time(); - return ticks * (uint64_t)timebase.numer / (uint64_t)timebase.denom; -#else - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; -#endif -} - -static void bench_add_ns(ColoForecastBenchProfile* profile, int bucket, uint64_t start_ns) { - profile->ns[bucket] += now_ns() - start_ns; -} - -static uint64_t bench_hash_bytes(uint64_t h, const void* data, size_t size) { - const uint8_t* bytes = (const uint8_t*)data; - for (size_t i = 0; i < size; i++) { - h ^= bytes[i]; - h *= 1099511628211ULL; - } - return h; -} - -static uint64_t bench_splitmix64(uint64_t* state) { - uint64_t z = (*state += 0x9E3779B97F4A7C15ULL); - z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; - z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; - return z ^ (z >> 31); -} - -static void bench_trace_actions( - ColosseumState* s, - uint64_t* rng, - int actions[COLO_NUM_ACTION_HEADS] -) { - for (int head = 0; head < COLO_NUM_ACTION_HEADS; head++) { - actions[head] = (int)(bench_splitmix64(rng) % (uint64_t)COLO_ACTION_DIMS[head]); - } - if (s->modifiers.draft_pending) { - actions[COLO_HEAD_PRIMARY] = 0; - actions[COLO_HEAD_MODIFIER_SELECT] = 1 + - (int)(bench_splitmix64(rng) % COLO_MODIFIER_DRAFT_OPTIONS); - } -} - -static void bench_init_context(ColosseumContext* ctx, int start_wave) { - col_init_context_typed(ctx); - ctx->config.start_wave = start_wave; - ctx->config.step_out_forecast_obs_enabled = 1; - ctx->config.forecast_horizon = COLO_STEP_OUT_FORECAST_HORIZON; - ctx->config.forecast_run_tile_mode = COLO_FORECAST_RUN_TILE_FULL; - col_finalize_route_topology(ctx); -} - -static void bench_capture_state( - ColoBenchCorpus* corpus, - const ColosseumState* s -) { - if (corpus->count >= BENCH_CORPUS_STATES) return; - corpus->states[corpus->count++] = *s; -} - -static void bench_add_wave_rollout( - ColoBenchCorpus* corpus, - int start_wave, - uint32_t seed, - uint64_t action_seed -) { - ColosseumContext ctx; - ColosseumState s; - int actions[COLO_NUM_ACTION_HEADS]; - uint64_t rng = action_seed; - bench_init_context(&ctx, start_wave); - memset(&s, 0, sizeof(s)); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); - for (int step = 0; step < 160 && corpus->count < BENCH_CORPUS_STATES; step++) { - bench_capture_state(corpus, &s); - bench_trace_actions(&s, &rng, actions); - col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); - if (s.episode_over) { - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed + (uint32_t)step + 1u); - } - } -} - -static void bench_prepare_custom( - ColosseumState* s, - ColosseumContext* ctx, - uint32_t seed, - int player_x, - int player_y -) { - bench_init_context(ctx, 1); - memset(s, 0, sizeof(*s)); - col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); - memset(s->npcs, 0, sizeof(s->npcs)); - memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); - memset(s->totems, 0, sizeof(s->totems)); - memset(s->bees, 0, sizeof(s->bees)); - s->modifiers.draft_pending = 0; - s->modifiers.draft_gates_spawn = 0; - s->wave_ready_delay = 0; - s->wave_spawn_delay = 0; - s->reinforcement_timer = COLO_REINFORCE_FIRED; - s->warband_cycle_anchor = s->tick; - s->player.x = player_x; - s->player.y = player_y; - col_rebuild_player_collision_flags(s); - col_refresh_current_obs_slots_ctx(s, ctx); -} - -static void bench_add_custom_dense(ColoBenchCorpus* corpus) { - ColosseumState s; - ColosseumContext ctx; - bench_prepare_custom(&s, &ctx, 0xC010D375u, 17, 16); - int slot = col_spawn_npc_at(&s, COLO_MANTICORE, 16, 20); - ColoManticoreState* mc = colo_npc_manticore(&s.npcs[slot]); - mc->cycle_step = 1; - mc->orb_style[0] = ATTACK_STYLE_MAGIC; - mc->orb_style[1] = ATTACK_STYLE_RANGED; - mc->orb_style[2] = ATTACK_STYLE_MELEE; - s.npcs[slot].attack_timer = 0; - slot = col_spawn_npc_at(&s, COLO_JAVELIN_COLOSSUS, 21, 17); - s.npcs[slot].attack_timer = 0; - colo_npc_javelin(&s.npcs[slot])->attack_count = 4; - slot = col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 12, 16); - s.npcs[slot].attack_timer = 0; - slot = col_spawn_npc_at(&s, COLO_SHOCKWAVE_COLOSSUS, 19, 12); - s.npcs[slot].attack_timer = 0; - slot = col_spawn_npc_at(&s, COLO_MINOTAUR, 14, 16); - s.npcs[slot].attack_timer = 0; - slot = col_spawn_npc_at(&s, COLO_JAGUAR_WARRIOR, 17, 19); - s.npcs[slot].attack_timer = 0; - slot = col_spawn_npc_at(&s, COLO_FREMENNIK_BERSERKER, 17, 17); - colo_npc_warband(&s.npcs[slot])->formation_dir = COLO_WARBAND_FORM_NORTH; - slot = col_spawn_npc_at(&s, COLO_FREMENNIK_ARCHER, 18, 17); - colo_npc_warband(&s.npcs[slot])->formation_dir = COLO_WARBAND_FORM_EAST; - col_rebuild_player_collision_flags(&s); - col_refresh_current_obs_slots_ctx(&s, &ctx); - bench_capture_state(corpus, &s); -} - -static void bench_build_corpus(ColoBenchCorpus* corpus) { - memset(corpus, 0, sizeof(*corpus)); - bench_add_custom_dense(corpus); - bench_add_wave_rollout(corpus, 0, 0xC010001u, 0xA1001u); - bench_add_wave_rollout(corpus, 3, 0xC010004u, 0xA1004u); - bench_add_wave_rollout(corpus, 7, 0xC010008u, 0xA1008u); - bench_add_wave_rollout(corpus, 11, 0xC010012u, 0xA1012u); - if (corpus->count <= 0) abort(); -} - -static void bench_encode_step_out_forecast_obs( - const ColoStepOutForecast* forecast, - int horizon, - float out[COLO_STEP_OUT_FORECAST_OBS_SIZE] -) { - int i = 0; - for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - const ColoStepOutForecastAction* action = &forecast->actions[action_idx]; - int first_attack_tick = 0; - int first_style_mask = 0; - int max_hit = 0; - int ranged_magic_same_tick = 0; - for (int tick_idx = 0; tick_idx < horizon; tick_idx++) { - const ColoStepOutForecastTick* tick = &action->ticks[tick_idx]; - int style_mask = col_step_out_forecast_tick_style_mask(tick); - if (first_attack_tick == 0 && - col_step_out_forecast_tick_has_event(tick)) { - first_attack_tick = tick_idx + 1; - first_style_mask = style_mask; - } - if (tick->max_hit > max_hit) max_hit = tick->max_hit; - if (tick->ranged_count > 0 && tick->magic_count > 0) - ranged_magic_same_tick = 1; - } - out[i++] = action->valid ? 1.0f : 0.0f; - out[i++] = (float)first_attack_tick / (float)horizon; - out[i++] = (float)first_style_mask / 7.0f; - out[i++] = (float)max_hit / 150.0f; - out[i++] = action->same_tick_mixed_style_conflict ? 1.0f : 0.0f; - out[i++] = ranged_magic_same_tick ? 1.0f : 0.0f; - out[i++] = action->ranged_magic_offtick_opportunity ? 1.0f : 0.0f; - out[i++] = action->melee_fallback_exposure ? 1.0f : 0.0f; - } - assert(i == COLO_STEP_OUT_FORECAST_OBS_SIZE); -} - -static void bench_profiled_tick( - const ColosseumState* s, - ColoForecastNpcLocal npcs[COLO_MAX_NPCS], - uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT], - ColoStepOutForecastAction* action, - ColoForecastObsSummary* summary, - int tick_idx, - const int slots[COLO_MAX_NPCS], - int slot_count, - const ColoForecastPrecomp* pre, - ColoForecastBenchProfile* profile -) { - for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { - int i = slots[slot_idx]; - ColoForecastNpcLocal* npc = &npcs[i]; - uint64_t start = now_ns(); - if (npc->stun_timer > 0) npc->stun_timer--; - if (npc->frozen_ticks > 0) npc->frozen_ticks--; - if (npc->type == COLO_SOL_HEREDIT) { - if (npc->sol_immobile_ticks > 0) npc->sol_immobile_ticks--; - if (npc->sol_attack_delay > 0) npc->sol_attack_delay--; - } - bench_add_ns(profile, COLO_BENCH_TICK_ADMIN, start); - - start = now_ns(); - ColoForecastMoveResult move_result = - col_forecast_local_move_npc( - s, npcs, npc_flags, i, action, pre->sol_clamp_active); - bench_add_ns(profile, COLO_BENCH_MOVEMENT, start); - - start = now_ns(); - col_forecast_local_attack_npc( - s, npcs, slots, slot_count, i, action, summary, tick_idx, - &move_result, pre); - bench_add_ns(profile, COLO_BENCH_ATTACK_AI, start); - profile->npc_tick_visits++; - } -} - -static void bench_profiled_forecast_build( - const ColosseumState* s, - ColoStepOutForecast* out, - ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS], - int horizon, - int run_tile_mode, - ColoForecastBenchProfile* profile -) { - if (horizon < 1) horizon = 1; - if (horizon > COLO_STEP_OUT_FORECAST_HORIZON) - horizon = COLO_STEP_OUT_FORECAST_HORIZON; - - uint64_t total_start = now_ns(); - uint64_t start = now_ns(); - memset(out, 0, sizeof(*out)); - if (summaries) - memset(summaries, 0, sizeof(ColoForecastObsSummary) * ENCOUNTER_MOVE_ACTIONS); - bench_add_ns(profile, COLO_BENCH_SETUP_CLEAR, start); - - int forecast_slots[COLO_MAX_NPCS]; - start = now_ns(); - int forecast_slot_count = col_collect_step_out_forecast_slots(s, forecast_slots); - bench_add_ns(profile, COLO_BENCH_SETUP_COLLECT, start); - profile->slot_count_sum += (uint64_t)forecast_slot_count; - - ColoForecastNpcLocal base_npcs[COLO_MAX_NPCS]; - uint8_t base_npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; - ColoForecastPrecomp pre = {0}; - if (forecast_slot_count > 0) { - start = now_ns(); - col_forecast_local_copy_npc_slots( - s, base_npcs, forecast_slots, forecast_slot_count); - bench_add_ns(profile, COLO_BENCH_SETUP_BASE_COPY, start); - - start = now_ns(); - col_forecast_local_rebuild_npc_flags( - base_npcs, base_npc_flags, forecast_slots, forecast_slot_count); - bench_add_ns(profile, COLO_BENCH_SETUP_FLAGS, start); - - start = now_ns(); - pre = col_forecast_precompute( - s, base_npcs, forecast_slots, forecast_slot_count); - bench_add_ns(profile, COLO_BENCH_SETUP_PRECOMP, start); - } - - uint64_t fanout_start = now_ns(); - for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - profile->actions_seen++; - start = now_ns(); - int unique_action = col_step_out_forecast_prepare_unique_action_ctx( - s, out, summaries, action_idx, forecast_slot_count); - bench_add_ns(profile, COLO_BENCH_ACTION_PREP, start); - if (!unique_action) { - profile->invalid_or_duplicate_actions++; - continue; - } - profile->valid_actions++; - ColoStepOutForecastAction* action = &out->actions[action_idx]; - ColoForecastObsSummary* summary = summaries ? &summaries[action_idx] : NULL; - if (run_tile_mode == COLO_FORECAST_RUN_TILE_STATIC_THREAT && - col_forecast_action_is_run_tile(action_idx)) { - col_forecast_static_threat_action( - s, base_npcs, forecast_slots, forecast_slot_count, action, summary); - profile->static_threat_actions++; - start = now_ns(); - col_step_out_forecast_finalize_action(action, horizon); - bench_add_ns(profile, COLO_BENCH_FINALIZE, start); - continue; - } - - ColoForecastNpcLocal npcs[COLO_MAX_NPCS]; - uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; - start = now_ns(); - col_forecast_local_copy_slot_set( - npcs, base_npcs, forecast_slots, forecast_slot_count); - bench_add_ns(profile, COLO_BENCH_RESTORE_NPCS, start); - - start = now_ns(); - memcpy(npc_flags, base_npc_flags, sizeof(base_npc_flags)); - bench_add_ns(profile, COLO_BENCH_RESTORE_FLAGS, start); - - profile->rollout_actions++; - for (int tick_idx = 0; tick_idx < horizon; tick_idx++) { - bench_profiled_tick( - s, npcs, npc_flags, action, summary, tick_idx, - forecast_slots, forecast_slot_count, &pre, profile); - profile->rollout_ticks++; - } - - start = now_ns(); - col_step_out_forecast_finalize_action(action, horizon); - bench_add_ns(profile, COLO_BENCH_FINALIZE, start); - } - profile->fanout_ns += now_ns() - fanout_start; - profile->total_ns += now_ns() - total_start; - profile->forecasts++; -} - -static void bench_profiled_forecast_obs( - const ColosseumState* s, - float out[COLO_STEP_OUT_FORECAST_OBS_SIZE], - ColoForecastBenchProfile* profile -) { - ColoStepOutForecast forecast; - ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS]; - bench_profiled_forecast_build( - s, &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, - COLO_FORECAST_RUN_TILE_FULL, profile); - uint64_t start = now_ns(); - col_write_step_out_forecast_obs_summary( - &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, out, 0); - profile->forecast_obs_checksum = bench_hash_bytes( - profile->forecast_obs_checksum, out, sizeof(float) * COLO_STEP_OUT_FORECAST_OBS_SIZE); - bench_add_ns(profile, COLO_BENCH_OBS_SCORE, start); -} - -static void bench_raw_forecast_obs( - const ColosseumState* s, - float out[COLO_STEP_OUT_FORECAST_OBS_SIZE] -) { - ColoStepOutForecast forecast; - ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS]; - col_build_step_out_forecast_horizon_mode_summary( - s, &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, - COLO_FORECAST_RUN_TILE_FULL); - col_write_step_out_forecast_obs_summary( - &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, out, 0); - bench_sink += out[0]; -} - -static void bench_check_profiled_exact(const ColoBenchCorpus* corpus) { - float obs_a[COLO_STEP_OUT_FORECAST_OBS_SIZE]; - float obs_b[COLO_STEP_OUT_FORECAST_OBS_SIZE]; - for (int i = 0; i < corpus->count; i++) { - ColoStepOutForecast expected; - ColoStepOutForecast actual; - ColoForecastBenchProfile profile = {0}; - col_build_step_out_forecast_horizon_mode( - &corpus->states[i], &expected, COLO_STEP_OUT_FORECAST_HORIZON, - COLO_FORECAST_RUN_TILE_FULL); - bench_profiled_forecast_build( - &corpus->states[i], &actual, NULL, COLO_STEP_OUT_FORECAST_HORIZON, - COLO_FORECAST_RUN_TILE_FULL, &profile); - if (memcmp(&expected, &actual, sizeof(expected)) != 0) { - fprintf(stderr, "profiled forecast mismatch on corpus state %d\n", i); - abort(); - } - bench_encode_step_out_forecast_obs( - &expected, COLO_STEP_OUT_FORECAST_HORIZON, obs_a); - bench_encode_step_out_forecast_obs( - &actual, COLO_STEP_OUT_FORECAST_HORIZON, obs_b); - if (memcmp(obs_a, obs_b, sizeof(obs_a)) != 0) { - fprintf(stderr, "profiled obs mismatch on corpus state %d\n", i); - abort(); - } - } -} - -static double bench_raw_forecast_seconds( - const ColoBenchCorpus* corpus, - int iters -) { - float obs[COLO_STEP_OUT_FORECAST_OBS_SIZE]; - uint64_t start = now_ns(); - for (int i = 0; i < iters; i++) { - bench_raw_forecast_obs(&corpus->states[i % corpus->count], obs); - } - return (double)(now_ns() - start) / 1000000000.0; -} - -static double bench_state_copy_seconds( - const ColoBenchCorpus* corpus, - int iters -) { - uint64_t start = now_ns(); - for (int i = 0; i < iters; i++) { - ColosseumState s = corpus->states[i % corpus->count]; - bench_sink += (double)s.player.x; - } - return (double)(now_ns() - start) / 1000000000.0; -} - -static double bench_env_step_seconds( - const ColoBenchCorpus* corpus, - const ColosseumContext* template_ctx, - int iters -) { - float obs[COLO_NUM_OBS + COLO_ACTION_MASK_SIZE]; - int actions[COLO_NUM_ACTION_HEADS] = {0}; - uint64_t start = now_ns(); - for (int i = 0; i < iters; i++) { - ColosseumState s = corpus->states[i % corpus->count]; - ColosseumContext ctx = *template_ctx; - col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs + COLO_NUM_OBS); - bench_sink += obs[0]; - bench_sink += col_get_reward_ctx((EncounterState*)&s, (EncounterContext*)&ctx); - bench_sink += col_is_terminal_ctx((EncounterState*)&s, (EncounterContext*)&ctx); - } - return (double)(now_ns() - start) / 1000000000.0; -} - -static void bench_collect_profile( - const ColoBenchCorpus* corpus, - int iters, - ColoForecastBenchProfile* profile -) { - float obs[COLO_STEP_OUT_FORECAST_OBS_SIZE]; - memset(profile, 0, sizeof(*profile)); - for (int i = 0; i < iters; i++) { - bench_profiled_forecast_obs(&corpus->states[i % corpus->count], obs, profile); - } -} - -static uint64_t bench_profile_exclusive_sum(const ColoForecastBenchProfile* profile) { - uint64_t sum = 0; - for (int i = 0; i < COLO_BENCH_COUNT; i++) sum += profile->ns[i]; - return sum; -} - -static void bench_print_profile( - const ColoForecastBenchProfile* profile, - double raw_us_per_forecast, - double env_us_per_step, - double env_no_forecast_us_per_step -) { - double measured_us = (double)profile->total_ns / (double)profile->forecasts / 1000.0; - double obs_score_us = - (double)profile->ns[COLO_BENCH_OBS_SCORE] / (double)profile->forecasts / 1000.0; - uint64_t exclusive_sum = bench_profile_exclusive_sum(profile); - uint64_t unattributed_ns = - profile->total_ns + profile->ns[COLO_BENCH_OBS_SCORE] > exclusive_sum - ? profile->total_ns + profile->ns[COLO_BENCH_OBS_SCORE] - exclusive_sum - : 0; - double total_for_pct_us = measured_us + obs_score_us; - - printf("corpus_forecasts=%llu actions=%llu valid=%llu rollout_actions=%llu skipped=%llu avg_slots=%.2f\n", - (unsigned long long)profile->forecasts, - (unsigned long long)profile->actions_seen, - (unsigned long long)profile->valid_actions, - (unsigned long long)profile->rollout_actions, - (unsigned long long)profile->invalid_or_duplicate_actions, - (double)profile->slot_count_sum / (double)profile->forecasts); - double accounted_us = (double)exclusive_sum / (double)profile->forecasts / 1000.0; - double forecast_delta_us = env_us_per_step - env_no_forecast_us_per_step; - - printf("rollout_ticks=%llu npc_tick_visits=%llu fanout_us=%.3f measured_forecast_plus_obs_us=%.3f raw_forecast_plus_obs_us=%.3f env_step_us=%.3f env_step_no_forecast_us=%.3f forecast_delta_us=%.3f forecast_pct_of_env_step=%.2f delta_pct_of_env_step=%.2f\n", - (unsigned long long)profile->rollout_ticks, - (unsigned long long)profile->npc_tick_visits, - (double)profile->fanout_ns / (double)profile->forecasts / 1000.0, - total_for_pct_us, - raw_us_per_forecast, - env_us_per_step, - env_no_forecast_us_per_step, - forecast_delta_us, - raw_us_per_forecast * 100.0 / env_us_per_step, - forecast_delta_us * 100.0 / env_us_per_step); - printf("internal bucket breakdown, accounted-normalized and raw-scaled:\n"); - for (int i = 0; i < COLO_BENCH_COUNT; i++) { - double us = (double)profile->ns[i] / (double)profile->forecasts / 1000.0; - double pct = accounted_us > 0.0 ? us * 100.0 / accounted_us : 0.0; - printf(" %-24s %9.3f measured_us %9.3f raw_scaled_us %6.2f%%\n", - COLO_BENCH_BUCKET_NAMES[i], us, raw_us_per_forecast * pct / 100.0, pct); - } - printf("timer_unattributed_loop_overhead %.3f us\n", - (double)unattributed_ns / (double)profile->forecasts / 1000.0); - printf("forecast_obs_checksum=%llu bench_sink=%.3f\n", - (unsigned long long)profile->forecast_obs_checksum, bench_sink); -} - -int main(void) { - ColosseumContext ctx; - ColosseumContext no_forecast_ctx; - ColoForecastBenchProfile profile; - ColoBenchCorpus* corpus = calloc(1, sizeof(*corpus)); - if (!corpus) abort(); - bench_init_context(&ctx, 0); - bench_init_context(&no_forecast_ctx, 0); - no_forecast_ctx.config.step_out_forecast_obs_enabled = 0; - bench_build_corpus(corpus); - bench_check_profiled_exact(corpus); - - int raw_iters = 50000; - int env_iters = 20000; - int profiled_iters = 3000; - double raw_seconds = bench_raw_forecast_seconds(corpus, raw_iters); - double copy_seconds = bench_state_copy_seconds(corpus, env_iters); - double env_seconds = bench_env_step_seconds(corpus, &ctx, env_iters); - double env_no_forecast_seconds = - bench_env_step_seconds(corpus, &no_forecast_ctx, env_iters); - bench_collect_profile(corpus, profiled_iters, &profile); - - double raw_us = raw_seconds * 1000000.0 / (double)raw_iters; - double copy_us = copy_seconds * 1000000.0 / (double)env_iters; - double env_us = env_seconds * 1000000.0 / (double)env_iters - copy_us; - double env_no_forecast_us = - env_no_forecast_seconds * 1000000.0 / (double)env_iters - copy_us; - - printf("sizeof(ColosseumState)=%zu COLO_NUM_OBS=%d COLO_STEP_OUT_FORECAST_OBS_SIZE=%d actions=%d horizon=%d corpus_states=%d\n", - sizeof(ColosseumState), - COLO_NUM_OBS, - COLO_STEP_OUT_FORECAST_OBS_SIZE, - ENCOUNTER_MOVE_ACTIONS, - COLO_STEP_OUT_FORECAST_HORIZON, - corpus->count); - printf("raw_iters=%d env_iters=%d profiled_iters=%d state_copy_us=%.3f\n", - raw_iters, env_iters, profiled_iters, copy_us); - bench_print_profile(&profile, raw_us, env_us, env_no_forecast_us); - free(corpus); - return 0; -} diff --git a/ocean/osrs/tests/bench_inferno_forecast.c b/ocean/osrs/tests/bench_inferno_forecast.c deleted file mode 100644 index ceeb23d9be..0000000000 --- a/ocean/osrs/tests/bench_inferno_forecast.c +++ /dev/null @@ -1,330 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_inferno.h" - -static volatile double bench_sink = 0.0; -static InfernoContext bench_context; - -static double now_seconds(void) { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0; -} - -static void init_bench_state(InfernoState* state, int player_x, int player_y) { - inf_build_npc_stats(); - memset(state, 0, sizeof(*state)); - state->rng_state = 20260516u; - state->wave = 63; - state->player.entity_type = ENTITY_PLAYER; - state->player.x = player_x; - state->player.y = player_y; - state->player.base_hitpoints = 99; - state->player.current_hitpoints = 99; - state->player.current_defence = 99; - state->player.current_magic = 99; - state->player.current_prayer = 99; - state->player_last_interaction_target_slot = -1; - state->player_last_interaction_age = 1; - state->player_dest_x = -1; - state->player_dest_y = -1; - state->weapon_set = INF_GEAR_LONG_RANGE; - osrs_interaction_init(&state->interaction); - for (int p = 0; p < INF_NUM_PILLARS; p++) { - state->pillars[p].x = INF_PILLAR_POS[p][0]; - state->pillars[p].y = INF_PILLAR_POS[p][1]; - state->pillars[p].hp = INF_PILLAR_HP; - state->pillars[p].active = 1; - } -} - -static void add_bench_npc( - InfernoState* state, int slot, InfNPCType type, int x, int y, int timer -) { - inf_init_npc(state, slot, type, x, y); - state->npcs[slot].attack_timer = timer; - state->npcs[slot].stun_timer = 0; - state->npcs[slot].frozen_ticks = 0; -} - -static void init_empty_state(InfernoState* state) { - init_bench_state(state, 29, 39); -} - -static void init_pillar_stack_state(InfernoState* state) { - init_bench_state(state, 29, 39); - add_bench_npc(state, 0, INF_NPC_RANGER, 24, 31, 0); - add_bench_npc(state, 1, INF_NPC_MAGER, 29, 30, 0); - inf_rebuild_npc_collision_flags(state); -} - -static void init_dense_wave_state(InfernoState* state) { - init_bench_state(state, 29, 39); - add_bench_npc(state, 0, INF_NPC_NIBBLER, 15, 39, 0); - add_bench_npc(state, 1, INF_NPC_NIBBLER, 16, 39, 1); - add_bench_npc(state, 2, INF_NPC_NIBBLER, 17, 39, 2); - add_bench_npc(state, 3, INF_NPC_MAGER, 24, 31, 0); - add_bench_npc(state, 4, INF_NPC_RANGER, 29, 30, 0); - add_bench_npc(state, 5, INF_NPC_MELEER, 23, 34, 0); - add_bench_npc(state, 6, INF_NPC_BLOB, 20, 32, 1); - add_bench_npc(state, 7, INF_NPC_BLOB, 26, 35, 2); - add_bench_npc(state, 8, INF_NPC_BAT, 18, 30, 3); - add_bench_npc(state, 9, INF_NPC_BAT, 19, 30, 4); - inf_rebuild_npc_collision_flags(state); -} - -typedef void (*BenchInit)(InfernoState*); -typedef void (*BenchFn)(InfernoState*, float*); -typedef void (*FixedBenchFn)(const InfernoState*, float*); -typedef void (*ForecastBuilder)( - const InfernoState*, const InfernoContext*, InfStepOutForecast*); - -static void bench_forecast_exact(InfernoState* state, float* obs) { - InfStepOutForecast forecast; - inf_build_step_out_forecast_exact_ctx(state, &bench_context, &forecast); - bench_sink += forecast.actions[0].valid; - bench_sink += forecast.actions[ENCOUNTER_MOVE_ACTIONS - 1].ticks[0].max_hit; -} - -static void bench_forecast_fast_static(InfernoState* state, float* obs) { - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_static_ctx( - state, &bench_context, &forecast); - bench_sink += forecast.actions[0].valid; - bench_sink += forecast.actions[ENCOUNTER_MOVE_ACTIONS - 1].ticks[0].max_hit; -} - -static void bench_forecast_fast_readonly(InfernoState* state, float* obs) { - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_readonly_ctx( - state, &bench_context, &forecast); - bench_sink += forecast.actions[0].valid; - bench_sink += forecast.actions[ENCOUNTER_MOVE_ACTIONS - 1].ticks[0].max_hit; -} - -static void bench_obs(InfernoState* state, float* obs) { - inf_write_obs_ctx( - (EncounterState*)state, (EncounterContext*)&bench_context, obs); - bench_sink += obs[0]; - bench_sink += obs[INF_NUM_OBS - 1]; -} - -static void bench_mask(InfernoState* state, float* obs) { - inf_write_mask_ctx( - (EncounterState*)state, (EncounterContext*)&bench_context, obs); - bench_sink += obs[0]; - bench_sink += obs[INF_ACTION_MASK_SIZE - 1]; -} - -static void bench_copy_fixed(const InfernoState* template, float* obs) { - InfernoState state; - memcpy(&state, template, sizeof(state)); - bench_sink += state.player.x; -} - -static void bench_step_fixed(const InfernoState* template, float* obs) { - InfernoState state; - memcpy(&state, template, sizeof(state)); - int actions[INF_NUM_ACTION_HEADS] = {0}; - inf_step_ctx( - (EncounterState*)&state, (EncounterContext*)&bench_context, actions); - bench_sink += state.player.current_hitpoints; -} - -static void bench_step_obs_mask_fixed(const InfernoState* template, float* obs) { - InfernoState state; - memcpy(&state, template, sizeof(state)); - int actions[INF_NUM_ACTION_HEADS] = {0}; - inf_step_ctx( - (EncounterState*)&state, (EncounterContext*)&bench_context, actions); - inf_write_obs_ctx( - (EncounterState*)&state, (EncounterContext*)&bench_context, obs); - inf_write_mask_ctx( - (EncounterState*)&state, - (EncounterContext*)&bench_context, - obs + INF_NUM_OBS); - bench_sink += state.player.current_hitpoints; - bench_sink += obs[0]; -} - -static void run_bench(const char* label, BenchInit init, BenchFn fn, int iters) { - InfernoState state; - float obs[INF_NUM_OBS + INF_ACTION_MASK_SIZE]; - init(&state); - fn(&state, obs); - double start = now_seconds(); - for (int i = 0; i < iters; i++) { - fn(&state, obs); - } - double elapsed = now_seconds() - start; - printf("%-24s %9d calls %9.3f ms %9.3f us/call\n", - label, iters, elapsed * 1000.0, elapsed * 1000000.0 / (double)iters); -} - -static void run_fixed_bench(const char* label, BenchInit init, FixedBenchFn fn, int iters) { - InfernoState template; - float obs[INF_NUM_OBS + INF_ACTION_MASK_SIZE]; - init(&template); - fn(&template, obs); - double start = now_seconds(); - for (int i = 0; i < iters; i++) { - fn(&template, obs); - } - double elapsed = now_seconds() - start; - printf("%-24s %9d calls %9.3f ms %9.3f us/call\n", - label, iters, elapsed * 1000.0, elapsed * 1000000.0 / (double)iters); -} - -static void report_forecast_diff( - const char* label, - BenchInit init, - ForecastBuilder fast_builder -) { - InfernoState state; - init(&state); - InfStepOutForecast exact; - InfStepOutForecast fast; - InfStepOutForecastOracleDiff diff; - inf_build_step_out_forecast_exact_ctx(&state, &bench_context, &exact); - fast_builder(&state, &bench_context, &fast); - inf_compare_step_out_forecasts(&exact, &fast, &diff); - double fn_rate = diff.exact_dangerous_actions > 0 ? - (double)diff.dangerous_false_negatives / - (double)diff.exact_dangerous_actions : 0.0; - int exact_safe_actions = diff.sampled_actions - diff.exact_dangerous_actions; - double fp_rate = exact_safe_actions > 0 ? - (double)diff.dangerous_false_positives / - (double)exact_safe_actions : 0.0; - printf("%-24s actions=%d action_mismatch=%d tick_mismatch=%d dangerous_fn=%d dangerous_fp=%d fn_rate=%.4f fp_rate=%.4f exact_danger=%d fast_danger=%d max_hit_err_sum=%d max_hit_err_max=%d\n", - label, - diff.sampled_actions, - diff.action_feature_mismatches, - diff.tick_feature_mismatches, - diff.dangerous_false_negatives, - diff.dangerous_false_positives, - fn_rate, - fp_rate, - diff.exact_dangerous_actions, - diff.fast_dangerous_actions, - diff.max_hit_abs_error_sum, - diff.max_hit_abs_error_max); -} - -static void add_forecast_diff( - InfStepOutForecastOracleDiff* total, - const InfStepOutForecastOracleDiff* diff -) { - total->action_feature_mismatches += diff->action_feature_mismatches; - total->tick_feature_mismatches += diff->tick_feature_mismatches; - total->dangerous_false_negatives += diff->dangerous_false_negatives; - total->dangerous_false_positives += diff->dangerous_false_positives; - total->exact_safe_fast_dangerous += diff->exact_safe_fast_dangerous; - total->exact_dangerous_actions += diff->exact_dangerous_actions; - total->fast_dangerous_actions += diff->fast_dangerous_actions; - total->sampled_actions += diff->sampled_actions; - total->max_hit_abs_error_sum += diff->max_hit_abs_error_sum; - if (diff->max_hit_abs_error_max > total->max_hit_abs_error_max) - total->max_hit_abs_error_max = diff->max_hit_abs_error_max; -} - -static void report_sampled_forecast_diff( - const char* label, - BenchInit init, - ForecastBuilder fast_builder, - int samples -) { - InfernoState state; - init(&state); - InfStepOutForecastOracleDiff total = {0}; - int resets = 0; - for (int sample = 0; sample < samples; sample++) { - InfStepOutForecast exact; - InfStepOutForecast fast; - InfStepOutForecastOracleDiff diff; - inf_build_step_out_forecast_exact_ctx(&state, &bench_context, &exact); - fast_builder(&state, &bench_context, &fast); - inf_compare_step_out_forecasts(&exact, &fast, &diff); - add_forecast_diff(&total, &diff); - - int actions[INF_NUM_ACTION_HEADS] = {0}; - actions[INF_HEAD_MOVE] = sample % ENCOUNTER_MOVE_ACTIONS; - inf_step_ctx( - (EncounterState*)&state, - (EncounterContext*)&bench_context, - actions); - if (state.episode_over) { - init(&state); - resets++; - } - } - - double fn_rate = total.exact_dangerous_actions > 0 ? - (double)total.dangerous_false_negatives / - (double)total.exact_dangerous_actions : 0.0; - int exact_safe_actions = total.sampled_actions - total.exact_dangerous_actions; - double fp_rate = exact_safe_actions > 0 ? - (double)total.dangerous_false_positives / - (double)exact_safe_actions : 0.0; - printf("%-24s samples=%d sampled_actions=%d resets=%d action_mismatch=%d tick_mismatch=%d dangerous_fn=%d dangerous_fp=%d fn_rate=%.4f fp_rate=%.4f exact_danger=%d fast_danger=%d max_hit_err_sum=%d max_hit_err_max=%d\n", - label, - samples, - total.sampled_actions, - resets, - total.action_feature_mismatches, - total.tick_feature_mismatches, - total.dangerous_false_negatives, - total.dangerous_false_positives, - fn_rate, - fp_rate, - total.exact_dangerous_actions, - total.fast_dangerous_actions, - total.max_hit_abs_error_sum, - total.max_hit_abs_error_max); -} - -int main(void) { - inf_init_context_typed(&bench_context); - inf_finalize_route_topology(&bench_context); - printf("sizeof(InfernoState) = %zu\n", sizeof(InfernoState)); - printf("INF_NUM_OBS = %d\n", INF_NUM_OBS); - run_bench("empty exact", init_empty_state, bench_forecast_exact, 200000); - run_bench("empty static", init_empty_state, bench_forecast_fast_static, 200000); - run_bench("empty readonly", init_empty_state, bench_forecast_fast_readonly, 200000); - run_bench("empty obs", init_empty_state, bench_obs, 200000); - run_bench("empty mask", init_empty_state, bench_mask, 200000); - report_forecast_diff("empty exact vs static", - init_empty_state, inf_build_step_out_forecast_fast_static_ctx); - report_forecast_diff("empty exact vs readonly", - init_empty_state, inf_build_step_out_forecast_fast_readonly_ctx); - run_bench("stack exact", init_pillar_stack_state, bench_forecast_exact, 100000); - run_bench("stack static", init_pillar_stack_state, bench_forecast_fast_static, 100000); - run_bench("stack readonly", init_pillar_stack_state, bench_forecast_fast_readonly, 100000); - run_bench("stack obs", init_pillar_stack_state, bench_obs, 100000); - run_bench("stack mask", init_pillar_stack_state, bench_mask, 100000); - report_forecast_diff("stack exact vs static", - init_pillar_stack_state, inf_build_step_out_forecast_fast_static_ctx); - report_forecast_diff("stack exact vs readonly", - init_pillar_stack_state, inf_build_step_out_forecast_fast_readonly_ctx); - run_bench("dense exact", init_dense_wave_state, bench_forecast_exact, 50000); - run_bench("dense static", init_dense_wave_state, bench_forecast_fast_static, 50000); - run_bench("dense readonly", init_dense_wave_state, bench_forecast_fast_readonly, 50000); - run_bench("dense obs", init_dense_wave_state, bench_obs, 50000); - run_bench("dense mask", init_dense_wave_state, bench_mask, 50000); - report_forecast_diff("dense exact vs static", - init_dense_wave_state, inf_build_step_out_forecast_fast_static_ctx); - report_forecast_diff("dense exact vs readonly", - init_dense_wave_state, inf_build_step_out_forecast_fast_readonly_ctx); - run_fixed_bench("dense copy fixed", init_dense_wave_state, bench_copy_fixed, 50000); - run_fixed_bench("dense step fixed", init_dense_wave_state, bench_step_fixed, 50000); - run_fixed_bench("dense step+obs+mask", init_dense_wave_state, bench_step_obs_mask_fixed, 50000); - report_sampled_forecast_diff("dense sampled static", - init_dense_wave_state, inf_build_step_out_forecast_fast_static_ctx, 256); - report_sampled_forecast_diff("dense sampled readonly", - init_dense_wave_state, inf_build_step_out_forecast_fast_readonly_ctx, 256); - printf("bench_sink = %.3f\n", bench_sink); - return 0; -} diff --git a/ocean/osrs/tests/inferno_lab_cli.c b/ocean/osrs/tests/inferno_lab_cli.c deleted file mode 100644 index 59439d7c20..0000000000 --- a/ocean/osrs/tests/inferno_lab_cli.c +++ /dev/null @@ -1,98 +0,0 @@ -#include -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_inferno.h" - -static char* read_script_line(FILE* file) { - size_t cap = 256; - size_t len = 0; - char* line = (char*)malloc(cap); - if (!line) { - fprintf(stderr, "inferno lab: out of memory\n"); - abort(); - } - - int ch; - while ((ch = fgetc(file)) != EOF) { - if (len + 2 > cap) { - if (cap > SIZE_MAX / 2) { - fprintf(stderr, "inferno lab: script line too large\n"); - abort(); - } - cap *= 2; - char* next = (char*)realloc(line, cap); - if (!next) { - fprintf(stderr, "inferno lab: out of memory\n"); - abort(); - } - line = next; - } - line[len++] = (char)ch; - if (ch == '\n') break; - } - - if (len == 0 && ch == EOF) { - free(line); - return NULL; - } - line[len] = '\0'; - return line; -} - -static void run_script( - FILE* file, - InfernoState* state, - InfernoContext* context -) { - for (;;) { - char* line = read_script_line(file); - if (!line) break; - - char* dump = NULL; - InfLabLineResult result = inf_lab_apply_script_line_impl_ctx( - state, context, line, &dump); - if (result == INF_LAB_LINE_DUMP) { - printf("%s\n", dump); - free(dump); - } - free(line); - } -} - -int main(int argc, char** argv) { - InfernoState* state = (InfernoState*)inf_create(); - InfernoContext context; - inf_init_context_typed(&context); - inf_finalize_route_topology(&context); - inf_put_float_ctx( - (EncounterState*)state, - (EncounterContext*)&context, - "late_start_supply_profile_scale", - 1.0f); - inf_reset_ctx( - (EncounterState*)state, (EncounterContext*)&context, 20260515u); - inf_lab_apply_command_ctx(state, &context, &(InfernoLabCommand){ - .kind = INF_LAB_COMMAND_CLEAR_NPCS, - }); - - if (argc == 1) { - run_script(stdin, state, &context); - } else if (argc == 2) { - FILE* file = fopen(argv[1], "r"); - if (!file) { - fprintf(stderr, "inferno lab: cannot open %s\n", argv[1]); - abort(); - } - run_script(file, state, &context); - fclose(file); - } else { - fprintf(stderr, "usage: inferno_lab [script]\n"); - inf_destroy((EncounterState*)state); - return 2; - } - - inf_destroy((EncounterState*)state); - return 0; -} diff --git a/ocean/osrs/tests/osrs_route_reference.h b/ocean/osrs/tests/osrs_route_reference.h deleted file mode 100644 index 8d24d80444..0000000000 --- a/ocean/osrs/tests/osrs_route_reference.h +++ /dev/null @@ -1,1679 +0,0 @@ -#ifndef OSRS_ROUTE_REFERENCE_H -#define OSRS_ROUTE_REFERENCE_H - -#define VIA_NONE 0 -#define VIA_S 1 -#define VIA_W 2 -#define VIA_SW 3 -#define VIA_N 4 -#define VIA_NW 6 -#define VIA_E 8 -#define VIA_SE 9 -#define VIA_NE 12 -#define VIA_START 99 - -typedef struct { - int found; - int next_dx; - int next_dy; - int dest_x; - int dest_y; - int run_dx; - int run_dy; -} PathResult; - -typedef int (*pathfind_blocked_fn)(void* ctx, int abs_x, int abs_y); - -static inline void pathfind_enqueue_or_abort( - int* queue_x, int* queue_y, int* tail, int capacity, int x, int y -) { - if (*tail >= capacity) { - fprintf(stderr, "pathfind queue overflow: capacity=%d\n", capacity); - abort(); - } - - queue_x[*tail] = x; - queue_y[*tail] = y; - (*tail)++; -} - -static const int pathfind_dir_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; -static const int pathfind_dir_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; -static const int pathfind_dir_via[8] = { - VIA_S, VIA_W, VIA_N, VIA_E, VIA_SW, VIA_NW, VIA_SE, VIA_NE -}; - -static inline PathResult pathfind_step(const CollisionMap* map, int height, - int src_x, int src_y, int dest_x, int dest_y, - pathfind_blocked_fn extra_blocked, void* blocked_ctx) { - PathResult result = {0, 0, 0, dest_x, dest_y}; - - if (src_x == dest_x && src_y == dest_y) { - result.found = 1; - return result; - } - - int dist = abs(src_x - dest_x); - int dy_abs = abs(src_y - dest_y); - if (dy_abs > dist) dist = dy_abs; - if (dist > 64) { - return result; - } - - int origin_x = ((src_x >> 3) - 6) << 3; - int origin_y = ((src_y >> 3) - 6) << 3; - - int local_src_x = src_x - origin_x; - int local_src_y = src_y - origin_y; - int local_dest_x = dest_x - origin_x; - int local_dest_y = dest_y - origin_y; - - if (local_dest_x < 0 || local_dest_x >= PATHFIND_GRID_SIZE || - local_dest_y < 0 || local_dest_y >= PATHFIND_GRID_SIZE) { - return result; - } - - int via[PATHFIND_GRID_SIZE][PATHFIND_GRID_SIZE]; - int cost[PATHFIND_GRID_SIZE][PATHFIND_GRID_SIZE]; - memset(via, 0, sizeof(via)); - memset(cost, 0, sizeof(cost)); - - int queue_x[PATHFIND_MAX_QUEUE_FULL]; - int queue_y[PATHFIND_MAX_QUEUE_FULL]; - int head = 0; - int tail = 0; - - via[local_src_x][local_src_y] = VIA_START; - cost[local_src_x][local_src_y] = 1; - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_FULL, local_src_x, local_src_y); - - int found_path = 0; - int cur_x, cur_y; - - while (head < tail) { - cur_x = queue_x[head]; - cur_y = queue_y[head]; - head++; - - if (cur_x == local_dest_x && cur_y == local_dest_y) { - found_path = 1; - break; - } - - int abs_x = origin_x + cur_x; - int abs_y = origin_y + cur_y; - int next_cost = cost[cur_x][cur_y] + 1; - - #define EB(ax, ay) (extra_blocked && extra_blocked(blocked_ctx, (ax), (ay))) - - for (int i = 0; i < 8; i++) { - int dx = pathfind_dir_dx[i]; - int dy = pathfind_dir_dy[i]; - int next_x = cur_x + dx; - int next_y = cur_y + dy; - if (next_x < 0 || next_x >= PATHFIND_GRID_SIZE || - next_y < 0 || next_y >= PATHFIND_GRID_SIZE) - continue; - if (via[next_x][next_y] != 0) continue; - if (!collision_traversable_step(map, height, abs_x, abs_y, dx, dy)) - continue; - if (dx != 0 && dy != 0) { - if (!collision_traversable_step(map, height, abs_x, abs_y, 0, dy)) - continue; - if (!collision_traversable_step(map, height, abs_x, abs_y, dx, 0)) - continue; - } - if (EB(abs_x + dx, abs_y + dy)) continue; - if (dx != 0 && dy != 0) { - if (EB(abs_x, abs_y + dy)) continue; - if (EB(abs_x + dx, abs_y)) continue; - } - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_FULL, next_x, next_y); - via[next_x][next_y] = pathfind_dir_via[i]; - cost[next_x][next_y] = next_cost; - } - - #undef EB - } - - if (!found_path) { - int best_manhattan = PATHFIND_GRID_SIZE * 2; - int best_cost = 999999; - int best_x = -1, best_y = -1; - - for (int fx = 0; fx < PATHFIND_GRID_SIZE; fx++) { - for (int fy = 0; fy < PATHFIND_GRID_SIZE; fy++) { - if (cost[fx][fy] == 0) continue; - - int ddx = fx - local_dest_x; - int ddy = fy - local_dest_y; - int manhattan = (ddx < 0 ? -ddx : ddx) + (ddy < 0 ? -ddy : ddy); - - if (manhattan < best_manhattan || - (manhattan == best_manhattan && cost[fx][fy] < best_cost)) { - best_manhattan = manhattan; - best_cost = cost[fx][fy]; - best_x = fx; - best_y = fy; - } - } - } - - if (best_x == -1) { - return result; - } - - cur_x = best_x; - cur_y = best_y; - found_path = 1; - result.dest_x = origin_x + best_x; - result.dest_y = origin_y + best_y; - } - - while (1) { - int v = via[cur_x][cur_y]; - int prev_x = cur_x; - int prev_y = cur_y; - - if (v & VIA_W) prev_x++; - else if (v & VIA_E) prev_x--; - - if (v & VIA_S) prev_y++; - else if (v & VIA_N) prev_y--; - - if (prev_x == local_src_x && prev_y == local_src_y) { - result.found = 1; - result.next_dx = cur_x - local_src_x; - result.next_dy = cur_y - local_src_y; - return result; - } - - cur_x = prev_x; - cur_y = prev_y; - - if (via[cur_x][cur_y] == VIA_NONE || via[cur_x][cur_y] == VIA_START) { - break; - } - } - - return result; -} - -static inline PathResult pathfind_step_arena( - const CollisionMap* map, int height, - int src_x, int src_y, int dest_x, int dest_y, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - int arena_origin_x, int arena_origin_y, int arena_w, int arena_h -) { - PathResult result = {0, 0, 0, dest_x, dest_y}; - - if (arena_w <= 0 || arena_w > PATHFIND_ARENA_MAX || - arena_h <= 0 || arena_h > PATHFIND_ARENA_MAX) { - fprintf(stderr, "pathfind arena dimensions out of bounds: %dx%d\n", arena_w, arena_h); - abort(); - } - - if (src_x == dest_x && src_y == dest_y) { - result.found = 1; - return result; - } - - int local_src_x = src_x - arena_origin_x; - int local_src_y = src_y - arena_origin_y; - int local_dest_x = dest_x - arena_origin_x; - int local_dest_y = dest_y - arena_origin_y; - - if (local_src_x < 0 || local_src_x >= arena_w || - local_src_y < 0 || local_src_y >= arena_h || - local_dest_x < 0 || local_dest_x >= arena_w || - local_dest_y < 0 || local_dest_y >= arena_h) { - return result; - } - - static OSRS_THREAD_LOCAL uint16_t bfs_gen[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - static OSRS_THREAD_LOCAL int8_t bfs_via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - static OSRS_THREAD_LOCAL int16_t bfs_cost[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - static OSRS_THREAD_LOCAL uint16_t bfs_gen_counter = 0; - bfs_gen_counter++; - if (bfs_gen_counter == 0) { - memset(bfs_gen, 0, sizeof(bfs_gen)); - bfs_gen_counter = 1; - } - uint16_t gen = bfs_gen_counter; - #define BFS_VISITED(x, y) (bfs_gen[(x)][(y)] == gen) - #define BFS_VISIT(x, y, v, c) do { \ - bfs_gen[(x)][(y)] = gen; bfs_via[(x)][(y)] = (v); bfs_cost[(x)][(y)] = (c); \ - } while(0) - #define BFS_VIA(x, y) bfs_via[(x)][(y)] - #define BFS_COST(x, y) bfs_cost[(x)][(y)] - - int queue_x[PATHFIND_MAX_QUEUE_ARENA]; - int queue_y[PATHFIND_MAX_QUEUE_ARENA]; - int head = 0, tail = 0; - - BFS_VISIT(local_src_x, local_src_y, VIA_START, 1); - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, local_src_x, local_src_y); - - int found_path = 0; - int cur_x, cur_y; - - while (head < tail) { - cur_x = queue_x[head]; - cur_y = queue_y[head]; - head++; - - if (cur_x == local_dest_x && cur_y == local_dest_y) { - found_path = 1; - break; - } - - int abs_x = arena_origin_x + cur_x; - int abs_y = arena_origin_y + cur_y; - int next_cost = BFS_COST(cur_x, cur_y) + 1; - - #define EB(ax, ay) (extra_blocked && extra_blocked(blocked_ctx, (ax), (ay))) - - for (int i = 0; i < 8; i++) { - int dx = pathfind_dir_dx[i]; - int dy = pathfind_dir_dy[i]; - int next_x = cur_x + dx; - int next_y = cur_y + dy; - if (next_x < 0 || next_x >= arena_w || - next_y < 0 || next_y >= arena_h) - continue; - if (BFS_VISITED(next_x, next_y)) continue; - if (!collision_traversable_step(map, height, abs_x, abs_y, dx, dy)) - continue; - if (dx != 0 && dy != 0) { - if (!collision_traversable_step(map, height, abs_x, abs_y, 0, dy)) - continue; - if (!collision_traversable_step(map, height, abs_x, abs_y, dx, 0)) - continue; - } - if (EB(abs_x + dx, abs_y + dy)) continue; - if (dx != 0 && dy != 0) { - if (EB(abs_x, abs_y + dy)) continue; - if (EB(abs_x + dx, abs_y)) continue; - } - pathfind_enqueue_or_abort( - queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, next_x, next_y); - BFS_VISIT(next_x, next_y, pathfind_dir_via[i], next_cost); - } - - #undef EB - } - - if (!found_path) { - int best_manhattan = arena_w + arena_h; - int best_cost = 999999; - int best_x = -1, best_y = -1; - - for (int fx = 0; fx < arena_w; fx++) { - for (int fy = 0; fy < arena_h; fy++) { - if (!BFS_VISITED(fx, fy) || BFS_COST(fx, fy) == 0) continue; - int ddx = fx - local_dest_x, ddy = fy - local_dest_y; - int manhattan = (ddx < 0 ? -ddx : ddx) + (ddy < 0 ? -ddy : ddy); - if (manhattan < best_manhattan || - (manhattan == best_manhattan && BFS_COST(fx, fy) < best_cost)) { - best_manhattan = manhattan; - best_cost = BFS_COST(fx, fy); - best_x = fx; best_y = fy; - } - } - } - - if (best_x == -1) return result; - cur_x = best_x; cur_y = best_y; - found_path = 1; - result.dest_x = arena_origin_x + best_x; - result.dest_y = arena_origin_y + best_y; - } - - while (1) { - if (!BFS_VISITED(cur_x, cur_y)) break; - int v = BFS_VIA(cur_x, cur_y); - if (v == VIA_NONE || v == VIA_START) break; - int prev_x = cur_x, prev_y = cur_y; - if (v & VIA_W) prev_x++; else if (v & VIA_E) prev_x--; - if (v & VIA_S) prev_y++; else if (v & VIA_N) prev_y--; - if (prev_x == local_src_x && prev_y == local_src_y) { - result.found = 1; - result.next_dx = cur_x - local_src_x; - result.next_dy = cur_y - local_src_y; - return result; - } - if (prev_x < 0 || prev_x >= arena_w || prev_y < 0 || prev_y >= arena_h) break; - cur_x = prev_x; cur_y = prev_y; - } - - return result; -} - - -static inline PathResult encounter_pathfind( - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - int src_x, int src_y, int dst_x, int dst_y, - pathfind_blocked_fn extra_blocked, void* blocked_ctx -) { - return pathfind_step(cmap, 0, - src_x + world_offset_x, src_y + world_offset_y, - dst_x + world_offset_x, dst_y + world_offset_y, - extra_blocked, blocked_ctx); -} - -static inline PathResult encounter_pathfind_arena( - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - int src_x, int src_y, int dst_x, int dst_y, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - int arena_base_x, int arena_base_y, int arena_w, int arena_h -) { - return pathfind_step_arena(cmap, 0, - src_x + world_offset_x, src_y + world_offset_y, - dst_x + world_offset_x, dst_y + world_offset_y, - extra_blocked, blocked_ctx, - arena_base_x + world_offset_x, arena_base_y + world_offset_y, - arena_w, arena_h); -} - -static inline int encounter_walk_toward( - Player* p, int tx, int ty, - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - encounter_walkable_fn is_walkable, void* ctx, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - int arena_base_x, int arena_base_y, int arena_w, int arena_h -) { - int steps = 0; - for (int step = 0; step < 2; step++) { - if (p->x == tx && p->y == ty) break; - PathResult pr = (arena_w > 0) - ? encounter_pathfind_arena(cmap, world_offset_x, world_offset_y, - p->x, p->y, tx, ty, - extra_blocked, blocked_ctx, - arena_base_x, arena_base_y, arena_w, arena_h) - : encounter_pathfind(cmap, world_offset_x, world_offset_y, - p->x, p->y, tx, ty, - extra_blocked, blocked_ctx); - if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) break; - int nx = p->x + pr.next_dx, ny = p->y + pr.next_dy; - if (!is_walkable(ctx, nx, ny)) break; - p->x = nx; p->y = ny; - steps++; - } - p->is_running = (steps == 2); - p->dest_x = p->x; p->dest_y = p->y; - return steps; -} - -static inline int encounter_move_toward_dest( - Player* p, int* dest_x, int* dest_y, - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - encounter_walkable_fn is_walkable, void* ctx, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - int arena_base_x, int arena_base_y, int arena_w, int arena_h -) { - if (*dest_x < 0 || *dest_y < 0) return 0; - if (p->x == *dest_x && p->y == *dest_y) { - *dest_x = -1; *dest_y = -1; - return 0; - } - return encounter_walk_toward(p, *dest_x, *dest_y, - cmap, world_offset_x, world_offset_y, - is_walkable, ctx, extra_blocked, blocked_ctx, - arena_base_x, arena_base_y, arena_w, arena_h); -} - -static inline int encounter_target_rect_distance_squared( - int x, - int y, - int target_x, - int target_y, - int target_size -) { - int target_max_x = target_x + target_size - 1; - int target_max_y = target_y + target_size - 1; - int dx = x < target_x - ? target_x - x - : (x > target_max_x ? x - target_max_x : 0); - int dy = y < target_y - ? target_y - y - : (y > target_max_y ? y - target_max_y : 0); - return dx * dx + dy * dy; -} - - -typedef struct { - const CollisionMap* collision_map; - encounter_walkable_fn is_walkable; - void* walkable_ctx; - pathfind_blocked_fn extra_blocked; - void* blocked_ctx; - int world_offset_x; - int world_offset_y; - int arena_base_x; - int arena_base_y; - int arena_w; - int arena_h; - int source_x; - int source_y; - int target_edge_local_x; - int target_edge_local_y; - int min_explored_x; - int min_explored_y; - int max_explored_x; - int max_explored_y; - uint64_t visited[PATHFIND_ARENA_MAX]; - uint16_t depth[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - int8_t via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; - int visited_count; -} EncounterArenaAttackRouteField; - -typedef struct { - int land_x; - int land_y; - int route_found; - int route_x; - int route_y; -} EncounterAttackRouteLanding; - - -static inline int encounter_attack_route_step_traversable( - const CollisionMap* cmap, - int world_offset_x, - int world_offset_y, - int x, - int y, - int dx, - int dy, - encounter_walkable_fn is_walkable, - void* walkable_ctx, - pathfind_blocked_fn extra_blocked, - void* blocked_ctx -) { - int next_x = x + dx; - int next_y = y + dy; - if (!is_walkable(walkable_ctx, next_x, next_y)) return 0; - if (dx != 0 && dy != 0 && - (!is_walkable(walkable_ctx, next_x, y) || - !is_walkable(walkable_ctx, x, next_y))) { - return 0; - } - if (!collision_traversable_step( - cmap, 0, x + world_offset_x, y + world_offset_y, dx, dy)) { - return 0; - } - if (extra_blocked && - extra_blocked( - blocked_ctx, - next_x + world_offset_x, - next_y + world_offset_y)) { - return 0; - } - if (dx == 0 || dy == 0) return 1; - if (extra_blocked && - (extra_blocked( - blocked_ctx, - next_x + world_offset_x, - y + world_offset_y) || - extra_blocked( - blocked_ctx, - x + world_offset_x, - next_y + world_offset_y))) { - return 0; - } - return 1; -} -typedef struct { - encounter_walkable_fn is_walkable; - void* walkable_ctx; - int arena_base_x; - int arena_base_y; - int arena_w; - int arena_h; - uint64_t known[PATHFIND_ARENA_MAX]; - uint64_t walkable[PATHFIND_ARENA_MAX]; -} EncounterAttackRouteWalkableCache; - -static inline int encounter_attack_route_cached_walkable( - void* data, - int x, - int y -) { - EncounterAttackRouteWalkableCache* cache = - (EncounterAttackRouteWalkableCache*)data; - int local_x = x - cache->arena_base_x; - int local_y = y - cache->arena_base_y; - if (local_x < 0 || local_x >= cache->arena_w || - local_y < 0 || local_y >= cache->arena_h) { - return cache->is_walkable(cache->walkable_ctx, x, y); - } - uint64_t bit = 1ULL << local_y; - if ((cache->known[local_x] & bit) == 0) { - cache->known[local_x] |= bit; - if (cache->is_walkable(cache->walkable_ctx, x, y)) - cache->walkable[local_x] |= bit; - } - return (cache->walkable[local_x] & bit) != 0; -} -static inline void encounter_attack_route_mark_target_edge( - uint64_t target_edges[PATHFIND_ARENA_MAX], - const CollisionMap* cmap, - int world_offset_x, - int world_offset_y, - int edge_x, - int edge_y, - int target_x, - int target_y, - int target_size, - int arena_base_x, - int arena_base_y, - int arena_w, - int arena_h -) { - int local_x = edge_x - arena_base_x; - int local_y = edge_y - arena_base_y; - if (local_x < 0 || local_x >= arena_w || - local_y < 0 || local_y >= arena_h) { - return; - } - if (encounter_entity_footprint_cardinal_reachable( - cmap, - world_offset_x, - world_offset_y, - edge_x, - edge_y, - target_x, - target_y, - target_size)) { - target_edges[local_x] |= 1ULL << local_y; - } -} - - - - -static inline void encounter_build_arena_attack_route_field( - EncounterArenaAttackRouteField* field, - const CollisionMap* cmap, - int world_offset_x, - int world_offset_y, - int source_x, - int source_y, - int target_x, - int target_y, - int target_size, - encounter_walkable_fn is_walkable, - void* walkable_ctx, - pathfind_blocked_fn extra_blocked, - void* blocked_ctx, - int arena_base_x, - int arena_base_y, - int arena_w, - int arena_h -) { - if (!field || !is_walkable || target_size <= 0) { - fprintf(stderr, "attack route field is missing required input\n"); - abort(); - } - if (arena_w <= 0 || arena_w > PATHFIND_ARENA_MAX || - arena_h <= 0 || arena_h > PATHFIND_ARENA_MAX) { - fprintf(stderr, "attack route arena dimensions out of bounds: %dx%d\n", - arena_w, arena_h); - abort(); - } - - int local_source_x = source_x - arena_base_x; - int local_source_y = source_y - arena_base_y; - if (local_source_x < 0 || local_source_x >= arena_w || - local_source_y < 0 || local_source_y >= arena_h) { - fprintf(stderr, - "attack route source out of arena: source=(%d,%d) arena=(%d,%d,%d,%d)\n", - source_x, source_y, arena_base_x, arena_base_y, arena_w, arena_h); - abort(); - } - - memset( - field->visited, - 0, - (size_t)arena_w * sizeof(field->visited[0])); - field->target_edge_local_x = -1; - field->target_edge_local_y = -1; - field->collision_map = cmap; - field->is_walkable = is_walkable; - field->walkable_ctx = walkable_ctx; - field->extra_blocked = extra_blocked; - field->blocked_ctx = blocked_ctx; - field->world_offset_x = world_offset_x; - field->world_offset_y = world_offset_y; - field->arena_base_x = arena_base_x; - field->arena_base_y = arena_base_y; - field->arena_w = arena_w; - field->arena_h = arena_h; - field->source_x = source_x; - field->source_y = source_y; - field->min_explored_x = local_source_x; - field->min_explored_y = local_source_y; - field->max_explored_x = local_source_x; - field->max_explored_y = local_source_y; - EncounterAttackRouteWalkableCache walkable_cache = { - .is_walkable = is_walkable, - .walkable_ctx = walkable_ctx, - .arena_base_x = arena_base_x, - .arena_base_y = arena_base_y, - .arena_w = arena_w, - .arena_h = arena_h, - }; - uint64_t target_edges[PATHFIND_ARENA_MAX] = {0}; - for (int offset = 0; offset < target_size; offset++) { - encounter_attack_route_mark_target_edge( - target_edges, cmap, world_offset_x, world_offset_y, - target_x - 1, target_y + offset, - target_x, target_y, target_size, - arena_base_x, arena_base_y, arena_w, arena_h); - encounter_attack_route_mark_target_edge( - target_edges, cmap, world_offset_x, world_offset_y, - target_x + target_size, target_y + offset, - target_x, target_y, target_size, - arena_base_x, arena_base_y, arena_w, arena_h); - encounter_attack_route_mark_target_edge( - target_edges, cmap, world_offset_x, world_offset_y, - target_x + offset, target_y - 1, - target_x, target_y, target_size, - arena_base_x, arena_base_y, arena_w, arena_h); - encounter_attack_route_mark_target_edge( - target_edges, cmap, world_offset_x, world_offset_y, - target_x + offset, target_y + target_size, - target_x, target_y, target_size, - arena_base_x, arena_base_y, arena_w, arena_h); - } - - - - - uint16_t queue[PATHFIND_MAX_QUEUE_ARENA]; - int head = 0; - field->visited_count = 1; - field->visited[local_source_x] = 1ULL << local_source_y; - field->depth[local_source_x][local_source_y] = 0; - field->via[local_source_x][local_source_y] = VIA_START; - queue[0] = (uint16_t)((local_source_x << 8) | local_source_y); - - static const int route_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int route_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; - static const int route_via[8] = { - VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE - }; - - while (head < field->visited_count) { - uint16_t packed_tile = queue[head++]; - int cur_x = packed_tile >> 8; - int cur_y = packed_tile & 0xff; - - int tile_x = arena_base_x + cur_x; - int tile_y = arena_base_y + cur_y; - if (target_edges[cur_x] & (1ULL << cur_y)) { - field->target_edge_local_x = cur_x; - field->target_edge_local_y = cur_y; - break; - } - uint16_t next_depth = field->depth[cur_x][cur_y] + 1; - - for (int i = 0; i < 8; i++) { - int dx = route_dx[i]; - int dy = route_dy[i]; - int next_x = cur_x + dx; - int next_y = cur_y + dy; - if (next_x < 0 || next_x >= arena_w || - next_y < 0 || next_y >= arena_h) { - continue; - } - if (field->visited[next_x] & (1ULL << next_y)) continue; - if (!encounter_attack_route_step_traversable( - cmap, - world_offset_x, - world_offset_y, - tile_x, - tile_y, - dx, - dy, - encounter_attack_route_cached_walkable, - &walkable_cache, - extra_blocked, - blocked_ctx)) { - continue; - } - - queue[field->visited_count] = - (uint16_t)((next_x << 8) | next_y); - field->visited_count++; - field->visited[next_x] |= 1ULL << next_y; - field->depth[next_x][next_y] = next_depth; - field->via[next_x][next_y] = (int8_t)route_via[i]; - if (next_x < field->min_explored_x) field->min_explored_x = next_x; - if (next_y < field->min_explored_y) field->min_explored_y = next_y; - if (next_x > field->max_explored_x) field->max_explored_x = next_x; - if (next_y > field->max_explored_y) field->max_explored_y = next_y; - } - } -} - -static inline EncounterAttackRouteLanding -encounter_attack_route_overlap_landing( - const EncounterArenaAttackRouteField* field, - int target_x, - int target_y, - int target_size -) { - Player player = { - .x = field->source_x, - .y = field->source_y, - }; - int max_r = (target_size + 1) / 2 + 1; - int best_dsq = 9999; - int best_x = -1; - int best_y = -1; - for (int dy = -max_r; dy <= max_r; dy++) { - for (int dx = -max_r; dx <= max_r; dx++) { - if (dx == 0 && dy == 0) continue; - int x = player.x + dx; - int y = player.y + dy; - if (!field->is_walkable(field->walkable_ctx, x, y)) continue; - if (encounter_entity_footprints_overlap( - x, y, 1, target_x, target_y, target_size)) { - continue; - } - int dsq = dx * dx + dy * dy; - if (dsq < best_dsq) { - best_dsq = dsq; - best_x = x; - best_y = y; - } - } - } - if (best_x >= 0) { - encounter_walk_toward( - &player, best_x, best_y, - field->collision_map, - field->world_offset_x, - field->world_offset_y, - field->is_walkable, - field->walkable_ctx, - field->extra_blocked, - field->blocked_ctx, - field->arena_base_x, - field->arena_base_y, - field->arena_w, - field->arena_h); - } - return (EncounterAttackRouteLanding){ - .land_x = player.x, - .land_y = player.y, - .route_found = player.x != field->source_x || - player.y != field->source_y, - .route_x = player.x, - .route_y = player.y, - }; -} - -static inline EncounterAttackRouteLanding encounter_arena_attack_route_landing( - const EncounterArenaAttackRouteField* field, - int target_x, - int target_y, - int target_size, - int attack_range, - const OsrsLosQuery* los_query -) { - if (!field || !field->is_walkable || - field->arena_w <= 0 || field->arena_w > PATHFIND_ARENA_MAX || - field->arena_h <= 0 || field->arena_h > PATHFIND_ARENA_MAX || - field->visited_count <= 0 || - target_size <= 0 || attack_range <= 0) { - fprintf(stderr, "invalid attack route landing query\n"); - abort(); - } - - EncounterAttackRouteLanding landing = { - .land_x = field->source_x, - .land_y = field->source_y, - .route_x = field->source_x, - .route_y = field->source_y, - }; - int dist = encounter_rect_distance( - field->source_x, field->source_y, 1, - target_x, target_y, target_size); - if (dist == 0) { - return encounter_attack_route_overlap_landing( - field, target_x, target_y, target_size); - } - if (encounter_player_can_attack( - field->source_x, field->source_y, - target_x, target_y, target_size, attack_range, - field->collision_map, field->world_offset_x, field->world_offset_y, - los_query)) { - landing.route_found = 1; - return landing; - } - - - int selected_x = field->target_edge_local_x; - int selected_y = field->target_edge_local_y; - - if (selected_x < 0) { - int target_local_x = target_x - field->arena_base_x; - int target_local_y = target_y - field->arena_base_y; - int scan_min_x = target_local_x - PATHFIND_MAX_FALLBACK_RADIUS; - int scan_min_y = target_local_y - PATHFIND_MAX_FALLBACK_RADIUS; - int scan_max_x = - target_local_x + target_size - 1 + PATHFIND_MAX_FALLBACK_RADIUS; - int scan_max_y = - target_local_y + target_size - 1 + PATHFIND_MAX_FALLBACK_RADIUS; - if (scan_min_x < field->min_explored_x) - scan_min_x = field->min_explored_x; - if (scan_min_y < field->min_explored_y) - scan_min_y = field->min_explored_y; - if (scan_max_x > field->max_explored_x) - scan_max_x = field->max_explored_x; - if (scan_max_y > field->max_explored_y) - scan_max_y = field->max_explored_y; - - int best_dsq = 0x3fffffff; - int best_depth = 100; - for (int x = scan_min_x; x <= scan_max_x; x++) { - for (int y = scan_min_y; y <= scan_max_y; y++) { - if ((field->visited[x] & (1ULL << y)) == 0) continue; - int tile_x = field->arena_base_x + x; - int tile_y = field->arena_base_y + y; - int depth = field->depth[x][y]; - if (depth >= 100) continue; - int dsq = encounter_target_rect_distance_squared( - tile_x, tile_y, target_x, target_y, target_size); - if (dsq < best_dsq || - (dsq == best_dsq && depth < best_depth)) { - selected_x = x; - selected_y = y; - best_dsq = dsq; - best_depth = depth; - } - } - } - } - if (selected_x < 0) return landing; - landing.route_found = 1; - landing.route_x = field->arena_base_x + selected_x; - landing.route_y = field->arena_base_y + selected_y; - - int source_local_x = field->source_x - field->arena_base_x; - int source_local_y = field->source_y - field->arena_base_y; - int first_x = source_local_x; - int first_y = source_local_y; - int second_x = source_local_x; - int second_y = source_local_y; - int cur_x = selected_x; - int cur_y = selected_y; - uint16_t selected_depth = field->depth[selected_x][selected_y]; - while (field->depth[cur_x][cur_y] > 0) { - uint16_t depth = field->depth[cur_x][cur_y]; - if (depth == 1) { - first_x = cur_x; - first_y = cur_y; - } else if (depth == 2) { - second_x = cur_x; - second_y = cur_y; - } - - int via = field->via[cur_x][cur_y]; - if (via == VIA_NONE || via == VIA_START) { - fprintf(stderr, - "broken attack route parent at local tile (%d,%d)\n", - cur_x, cur_y); - abort(); - } - if (via & VIA_W) cur_x++; - else if (via & VIA_E) cur_x--; - if (via & VIA_S) cur_y++; - else if (via & VIA_N) cur_y--; - if (cur_x < 0 || cur_x >= field->arena_w || - cur_y < 0 || cur_y >= field->arena_h) { - fprintf(stderr, "attack route parent left arena\n"); - abort(); - } - } - if (cur_x != source_local_x || cur_y != source_local_y) { - fprintf(stderr, "attack route did not terminate at source\n"); - abort(); - } - if (selected_depth == 0) return landing; - - landing.land_x = field->arena_base_x + first_x; - landing.land_y = field->arena_base_y + first_y; - if (encounter_player_can_attack( - landing.land_x, landing.land_y, - target_x, target_y, target_size, attack_range, - field->collision_map, field->world_offset_x, field->world_offset_y, - los_query)) { - return landing; - } - if (selected_depth >= 2) { - landing.land_x = field->arena_base_x + second_x; - landing.land_y = field->arena_base_y + second_y; - } - return landing; -} - -#define ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS 25 - -static inline int encounter_attack_route_compress_waypoints( - const EncounterArenaAttackRouteField* field, - const EncounterAttackRouteLanding* landing, - int waypoint_x[ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS], - int waypoint_y[ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS] -) { - if (!landing->route_found || - (landing->route_x == field->source_x && - landing->route_y == field->source_y)) { - return 0; - } - - int source_x = field->source_x - field->arena_base_x; - int source_y = field->source_y - field->arena_base_y; - int current_x = landing->route_x - field->arena_base_x; - int current_y = landing->route_y - field->arena_base_y; - int current_direction = -1; - int waypoint_count = 0; - - while (current_x != source_x || current_y != source_y) { - int next_direction = field->via[current_x][current_y]; - if (next_direction == VIA_NONE || next_direction == VIA_START) { - fprintf(stderr, "broken attack route during compression\n"); - abort(); - } - if (current_direction != next_direction) { - current_direction = next_direction; - if (waypoint_count == ENCOUNTER_ATTACK_ROUTE_MAX_WAYPOINTS) - waypoint_count--; - memmove( - &waypoint_x[1], - &waypoint_x[0], - (size_t)waypoint_count * sizeof(waypoint_x[0])); - memmove( - &waypoint_y[1], - &waypoint_y[0], - (size_t)waypoint_count * sizeof(waypoint_y[0])); - waypoint_x[0] = field->arena_base_x + current_x; - waypoint_y[0] = field->arena_base_y + current_y; - waypoint_count++; - } - - if (current_direction & VIA_W) current_x++; - else if (current_direction & VIA_E) current_x--; - if (current_direction & VIA_S) current_y++; - else if (current_direction & VIA_N) current_y--; - } - return waypoint_count; -} - -static inline PathResult encounter_pathfind_arena_attack_approach( - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - int src_x, int src_y, - int target_x, int target_y, int target_size, int attack_range, - encounter_walkable_fn is_walkable, void* ctx, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - const OsrsLosQuery* los_query, - int arena_base_x, int arena_base_y, int arena_w, int arena_h -) { - PathResult result = {0, 0, 0, src_x, src_y}; - int local_src_x = src_x - arena_base_x; - int local_src_y = src_y - arena_base_y; - if (local_src_x < 0 || local_src_x >= arena_w || - local_src_y < 0 || local_src_y >= arena_h) { - return result; - } - - EncounterArenaAttackRouteField field; - encounter_build_arena_attack_route_field( - &field, - cmap, - world_offset_x, - world_offset_y, - src_x, - src_y, - target_x, - target_y, - target_size, - is_walkable, - ctx, - extra_blocked, - blocked_ctx, - arena_base_x, - arena_base_y, - arena_w, - arena_h); - EncounterAttackRouteLanding landing = - encounter_arena_attack_route_landing( - &field, - target_x, - target_y, - target_size, - attack_range, - los_query); - - int landing_x = landing.land_x - arena_base_x; - int landing_y = landing.land_y - arena_base_y; - if (landing_x < 0 || landing_x >= arena_w || - landing_y < 0 || landing_y >= arena_h) { - return result; - } - uint16_t landing_depth = field.depth[landing_x][landing_y]; - if (landing_depth == 0) { - result.found = encounter_player_can_attack( - src_x, src_y, - target_x, target_y, target_size, attack_range, - cmap, world_offset_x, world_offset_y, - los_query); - return result; - } - if (landing_depth > 2) { - fprintf(stderr, "attack route landing exceeded one run tick: %u\n", - (unsigned)landing_depth); - abort(); - } - - result.found = 1; - result.dest_x = landing.land_x; - result.dest_y = landing.land_y; - if (landing_depth == 1) { - result.next_dx = landing.land_x - src_x; - result.next_dy = landing.land_y - src_y; - return result; - } - - int first_x = landing_x; - int first_y = landing_y; - int via = field.via[landing_x][landing_y]; - if (via & VIA_W) first_x++; - else if (via & VIA_E) first_x--; - if (via & VIA_S) first_y++; - else if (via & VIA_N) first_y--; - result.next_dx = arena_base_x + first_x - src_x; - result.next_dy = arena_base_y + first_y - src_y; - result.run_dx = landing_x - first_x; - result.run_dy = landing_y - first_y; - return result; -} - -static inline int encounter_chase_attack_target( - Player* p, int target_x, int target_y, int target_size, int attack_range, - const CollisionMap* cmap, int world_offset_x, int world_offset_y, - encounter_walkable_fn is_walkable, void* ctx, - pathfind_blocked_fn extra_blocked, void* blocked_ctx, - const OsrsLosQuery* los_query, - int arena_base_x, int arena_base_y, int arena_w, int arena_h -) { - int dist = encounter_rect_distance(p->x, p->y, 1, - target_x, target_y, target_size); - - if (dist == 0) { - int max_r = (target_size + 1) / 2 + 1; - int best_dsq = 9999, bx = -1, by = -1; - for (int dy = -max_r; dy <= max_r; dy++) { - for (int dx = -max_r; dx <= max_r; dx++) { - if (dx == 0 && dy == 0) continue; - int nx = p->x + dx, ny = p->y + dy; - if (!is_walkable(ctx, nx, ny)) continue; - if (encounter_entity_footprints_overlap(nx, ny, 1, - target_x, target_y, target_size)) - continue; - int d = dx * dx + dy * dy; - if (d < best_dsq) { best_dsq = d; bx = nx; by = ny; } - } - } - if (bx < 0) return 0; - int steps = encounter_walk_toward(p, bx, by, - cmap, world_offset_x, world_offset_y, - is_walkable, ctx, extra_blocked, blocked_ctx, - arena_base_x, arena_base_y, arena_w, arena_h); - return steps > 0 ? 1 : 0; - } - if (encounter_player_can_attack( - p->x, p->y, - target_x, target_y, target_size, attack_range, - cmap, world_offset_x, world_offset_y, - los_query)) - return 0; - - - int cx, cy; - cx = -1; - cy = -1; - - if (arena_w <= 0) { - int scan_min_x = target_x - attack_range; - int scan_max_x = target_x + target_size - 1 + attack_range; - int scan_min_y = target_y - attack_range; - int scan_max_y = target_y + target_size - 1 + attack_range; - - cx = -1; - cy = -1; - int best_player_dsq = 0x3fffffff; - int best_target_dist = 0x3fffffff; - if (scan_min_x <= scan_max_x && scan_min_y <= scan_max_y) { - for (int yy = scan_min_y; yy <= scan_max_y; yy++) { - for (int xx = scan_min_x; xx <= scan_max_x; xx++) { - if (!is_walkable(ctx, xx, yy)) continue; - if (!encounter_player_can_attack(xx, yy, target_x, target_y, - target_size, attack_range, - cmap, world_offset_x, world_offset_y, - los_query)) - continue; - int dx = xx - p->x; - int dy = yy - p->y; - int player_dsq = dx * dx + dy * dy; - int target_dist = encounter_rect_distance( - xx, yy, 1, target_x, target_y, target_size); - if (player_dsq < best_player_dsq || - (player_dsq == best_player_dsq && - target_dist < best_target_dist)) { - best_player_dsq = player_dsq; - best_target_dist = target_dist; - cx = xx; - cy = yy; - } - } - } - } - - if (cx < 0) { - cx = p->x < target_x ? target_x : - (p->x > target_x + target_size - 1 ? target_x + target_size - 1 : p->x); - cy = p->y < target_y ? target_y : - (p->y > target_y + target_size - 1 ? target_y + target_size - 1 : p->y); - } - } - - int cached_run_dx = 0; - int cached_run_dy = 0; - int steps = 0; - for (int step = 0; step < 2; step++) { - if (encounter_player_can_attack(p->x, p->y, target_x, target_y, - target_size, attack_range, - cmap, world_offset_x, world_offset_y, - los_query)) - break; - int next_dx; - int next_dy; - int use_cached_run = step == 1 && arena_w > 0 && - (cached_run_dx != 0 || cached_run_dy != 0); - if (use_cached_run) { - int next_x = p->x + cached_run_dx; - int next_y = p->y + cached_run_dy; - if (!is_walkable(ctx, next_x, next_y) || - (extra_blocked && extra_blocked( - blocked_ctx, - next_x + world_offset_x, - next_y + world_offset_y))) { - use_cached_run = 0; - } else if (cached_run_dx != 0 && cached_run_dy != 0 && - (!is_walkable(ctx, next_x, p->y) || - !is_walkable(ctx, p->x, next_y) || - (extra_blocked && - (extra_blocked( - blocked_ctx, - next_x + world_offset_x, - p->y + world_offset_y) || - extra_blocked( - blocked_ctx, - p->x + world_offset_x, - next_y + world_offset_y))))) { - use_cached_run = 0; - } - } - if (use_cached_run) { - next_dx = cached_run_dx; - next_dy = cached_run_dy; - } else { - PathResult pr = (arena_w > 0) - ? encounter_pathfind_arena_attack_approach( - cmap, world_offset_x, world_offset_y, - p->x, p->y, - target_x, target_y, target_size, attack_range, - is_walkable, ctx, - extra_blocked, blocked_ctx, - los_query, - arena_base_x, arena_base_y, arena_w, arena_h) - : encounter_pathfind(cmap, world_offset_x, world_offset_y, - p->x, p->y, cx, cy, - extra_blocked, blocked_ctx); - if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) break; - next_dx = pr.next_dx; - next_dy = pr.next_dy; - if (step == 0) { - cached_run_dx = pr.run_dx; - cached_run_dy = pr.run_dy; - } - } - int nx = p->x + next_dx, ny = p->y + next_dy; - if (!is_walkable(ctx, nx, ny)) break; - p->x = nx; p->y = ny; - steps++; - } - p->is_running = (steps == 2); - p->dest_x = p->x; p->dest_y = p->y; - return steps > 0 ? 1 : 0; -} - - -typedef struct { - uint64_t visited[ENCOUNTER_ARENA_TOPOLOGY_MAX_DIMENSION]; - uint16_t depth[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; - uint16_t queue[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; - int8_t via[ENCOUNTER_ARENA_TOPOLOGY_MAX_TILES]; - uint16_t count; -} OsrsReferenceRouteField; - -typedef int (*osrs_reference_can_attack_fn)( - void* ctx, - int player_x, - int player_y, - int target_x, - int target_y, - int target_size, - int attack_range); - -static inline int osrs_reference_route_blocked( - const EncounterRouteInput* input, - int x, - int y -) { - return input->blockers.is_blocked && - input->blockers.is_blocked( - input->blockers.ctx, x, y, input->actor_size); -} - -static inline int osrs_reference_route_step_allowed( - const EncounterRouteInput* input, - int local_x, - int local_y, - int direction -) { - static const int8_t dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int8_t dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; - static const uint8_t step_mask[8] = {8, 16, 2, 64, 1, 4, 32, 128}; - const EncounterArenaTopology* topology = input->topology; - int next_x = local_x + dx[direction]; - int next_y = local_y + dy[direction]; - if (next_x < 0 || next_x >= topology->width || - next_y < 0 || next_y >= topology->height) - return 0; - int x = topology->origin_x + local_x; - int y = topology->origin_y + local_y; - if (osrs_reference_route_blocked( - input, x + dx[direction], y + dy[direction])) - return 0; - if (dx[direction] != 0 && dy[direction] != 0 && - (osrs_reference_route_blocked( - input, x + dx[direction], y) || - osrs_reference_route_blocked( - input, x, y + dy[direction]))) - return 0; - int index = local_x * topology->height + local_y; - return (topology->legal_step_masks[input->actor_size - 1][index] & - step_mask[direction]) != 0; -} - -static inline void osrs_reference_route_build( - const EncounterRouteInput* input, - OsrsReferenceRouteField* field -) { - static const int8_t dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; - static const int8_t dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; - static const int8_t via[8] = { - VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE - }; - const EncounterArenaTopology* topology = input->topology; - memset(field, 0, sizeof(*field)); - int source_x = input->source_x - topology->origin_x; - int source_y = input->source_y - topology->origin_y; - int source_index = source_x * topology->height + source_y; - field->visited[source_x] = UINT64_C(1) << source_y; - field->depth[source_index] = 0; - field->via[source_index] = VIA_START; - field->queue[0] = (uint16_t)((source_x << 6) | source_y); - field->count = 1; - for (int head = 0; head < field->count; head++) { - int packed = field->queue[head]; - int x = packed >> 6; - int y = packed & 63; - int index = x * topology->height + y; - uint16_t next_depth = (uint16_t)(field->depth[index] + 1); - for (int direction = 0; direction < 8; direction++) { - int next_x = x + dx[direction]; - int next_y = y + dy[direction]; - if (next_x < 0 || next_x >= topology->width || - next_y < 0 || next_y >= topology->height) - continue; - uint64_t bit = UINT64_C(1) << next_y; - if (field->visited[next_x] & bit) continue; - if (!osrs_reference_route_step_allowed( - input, x, y, direction)) - continue; - if (field->count >= topology->tile_count) abort(); - int next_index = next_x * topology->height + next_y; - field->visited[next_x] |= bit; - field->depth[next_index] = next_depth; - field->via[next_index] = via[direction]; - field->queue[field->count++] = - (uint16_t)((next_x << 6) | next_y); - } - } -} - -static inline int osrs_reference_route_is_target( - const EncounterRouteInput* input, - int x, - int y -) { - int64_t actor_max_x = (int64_t)x + input->actor_size - 1; - int64_t actor_max_y = (int64_t)y + input->actor_size - 1; - int64_t target_max_x = - (int64_t)input->target_x + input->target_size - 1; - int64_t target_max_y = - (int64_t)input->target_y + input->target_size - 1; - int x_overlap = - (int64_t)x <= target_max_x && (int64_t)input->target_x <= actor_max_x; - int y_overlap = - (int64_t)y <= target_max_y && (int64_t)input->target_y <= actor_max_y; - if (x_overlap && y_overlap) return 0; - return (y_overlap && - (actor_max_x + 1 == input->target_x || - target_max_x + 1 == x)) || - (x_overlap && - (actor_max_y + 1 == input->target_y || - target_max_y + 1 == y)); -} - -static inline int osrs_reference_target_distance_squared( - const EncounterRouteInput* input, - int x, - int y -) { - int64_t target_max_x = - (int64_t)input->target_x + input->target_size - 1; - int64_t target_max_y = - (int64_t)input->target_y + input->target_size - 1; - int64_t dx = 0; - int64_t dy = 0; - if ((int64_t)x < input->target_x) dx = input->target_x - (int64_t)x; - else if (target_max_x < x) dx = (int64_t)x - target_max_x; - if ((int64_t)y < input->target_y) dy = input->target_y - (int64_t)y; - else if (target_max_y < y) dy = (int64_t)y - target_max_y; - int64_t squared = dx * dx + dy * dy; - return squared > INT_MAX ? INT_MAX : (int)squared; -} - -static inline void osrs_reference_route_parent( - int via, - int* x, - int* y -) { - if (via & VIA_W) (*x)++; - else if (via & VIA_E) (*x)--; - if (via & VIA_S) (*y)++; - else if (via & VIA_N) (*y)--; -} - -static inline EncounterRouteResult osrs_reference_route_query( - const EncounterRouteInput* input, - const OsrsReferenceRouteField* field -) { - EncounterRouteResult result; - memset(&result, 0, sizeof(result)); - const EncounterArenaTopology* topology = input->topology; - int selected_index = -1; - for (int i = 0; i < field->count; i++) { - int packed = field->queue[i]; - int local_x = packed >> 6; - int local_y = packed & 63; - int x = topology->origin_x + local_x; - int y = topology->origin_y + local_y; - if (osrs_reference_route_is_target(input, x, y)) { - selected_index = local_x * topology->height + local_y; - break; - } - } - if (selected_index >= 0) { - result.outcome = ROUTE_REACHED_TARGET; - } else { - int best_distance = INT_MAX; - uint16_t best_depth = UINT16_MAX; - for (int local_x = 0; local_x < topology->width; local_x++) { - for (int local_y = 0; local_y < topology->height; local_y++) { - if ((field->visited[local_x] & - (UINT64_C(1) << local_y)) == 0) - continue; - int x = topology->origin_x + local_x; - int y = topology->origin_y + local_y; - int min_x = - input->target_x - PATHFIND_MAX_FALLBACK_RADIUS; - int max_x = input->target_x + input->target_size - 1 + - PATHFIND_MAX_FALLBACK_RADIUS; - int min_y = - input->target_y - PATHFIND_MAX_FALLBACK_RADIUS; - int max_y = input->target_y + input->target_size - 1 + - PATHFIND_MAX_FALLBACK_RADIUS; - if (x < min_x || x > max_x || y < min_y || y > max_y) - continue; - int index = local_x * topology->height + local_y; - int distance = - osrs_reference_target_distance_squared(input, x, y); - uint16_t depth = field->depth[index]; - if (distance < best_distance || - (distance == best_distance && depth < best_depth)) { - selected_index = index; - best_distance = distance; - best_depth = depth; - } - } - } - if (selected_index < 0) { - result.outcome = ROUTE_UNREACHABLE; - return result; - } - result.outcome = ROUTE_REACHED_FALLBACK; - } - int local_x = selected_index / topology->height; - int local_y = selected_index % topology->height; - result.destination_x = topology->origin_x + local_x; - result.destination_y = topology->origin_y + local_y; - result.distance = field->depth[selected_index]; - int source_x = input->source_x - topology->origin_x; - int source_y = input->source_y - topology->origin_y; - int first_x = source_x; - int first_y = source_y; - int second_x = source_x; - int second_y = source_y; - while (local_x != source_x || local_y != source_y) { - int index = local_x * topology->height + local_y; - uint16_t depth = field->depth[index]; - if (depth == 1) { - first_x = local_x; - first_y = local_y; - } else if (depth == 2) { - second_x = local_x; - second_y = local_y; - } - osrs_reference_route_parent(field->via[index], &local_x, &local_y); - } - result.first_dx = first_x - source_x; - result.first_dy = first_y - source_y; - if (result.distance >= 2) { - result.run_dx = second_x - first_x; - result.run_dy = second_y - first_y; - } - return result; -} - -static inline uint64_t osrs_reference_route_exhaustive( - const char* scenario, - const EncounterArenaTopology* topology, - EncounterRouteBlockers blockers, - int maximum_target_size, - const int* attack_ranges, - int attack_range_count, - osrs_reference_can_attack_fn can_attack, - void* attack_ctx -) { - uint64_t checks = 0; - for (int source_x = topology->origin_x; - source_x < topology->origin_x + topology->width; - source_x++) { - for (int source_y = topology->origin_y; - source_y < topology->origin_y + topology->height; - source_y++) { - if (encounter_arena_topology_footprint_blocked( - topology, source_x, source_y, 1) || - (blockers.is_blocked && - blockers.is_blocked( - blockers.ctx, source_x, source_y, 1))) - continue; - EncounterRouteInput input = { - .topology = topology, - .blockers = blockers, - .source_x = source_x, - .source_y = source_y, - .actor_size = 1, - .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, - .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, - .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, - }; - OsrsReferenceRouteField field; - osrs_reference_route_build(&input, &field); - for (int target_size = 1; - target_size <= maximum_target_size; - target_size++) { - input.target_size = target_size; - for (int target_x = topology->origin_x; - target_x + target_size <= - topology->origin_x + topology->width; - target_x++) { - input.target_x = target_x; - for (int target_y = topology->origin_y; - target_y + target_size <= - topology->origin_y + topology->height; - target_y++) { - if (encounter_arena_topology_footprint_blocked( - topology, - target_x, - target_y, - target_size)) - continue; - if (source_x >= target_x && - source_x < target_x + target_size && - source_y >= target_y && - source_y < target_y + target_size) - continue; - input.target_y = target_y; - EncounterRouteResult expected = - osrs_reference_route_query(&input, &field); - EncounterRouteResult actual = - encounter_route_solve(&input); - if (actual.outcome != expected.outcome || - actual.destination_x != expected.destination_x || - actual.destination_y != expected.destination_y || - actual.distance != expected.distance || - actual.first_dx != expected.first_dx || - actual.first_dy != expected.first_dy) { - fprintf(stderr, - "route equivalence mismatch scenario=%s source=(%d,%d) target=(%d,%d,%d) expected=(%d,%d,%d,%d,%d,%u) actual=(%d,%d,%d,%d,%d,%u)\n", - scenario, - source_x, - source_y, - target_x, - target_y, - target_size, - expected.outcome, - expected.destination_x, - expected.destination_y, - expected.first_dx, - expected.first_dy, - expected.distance, - actual.outcome, - actual.destination_x, - actual.destination_y, - actual.first_dx, - actual.first_dy, - actual.distance); - abort(); - } - for (int range_index = 0; - range_index < attack_range_count; - range_index++) { - int range = attack_ranges[range_index]; - int expected_x = source_x; - int expected_y = source_y; - int actual_x = source_x; - int actual_y = source_y; - if (!can_attack( - attack_ctx, - source_x, - source_y, - target_x, - target_y, - target_size, - range)) { - expected_x += expected.first_dx; - expected_y += expected.first_dy; - actual_x += actual.first_dx; - actual_y += actual.first_dy; - if (!can_attack( - attack_ctx, - expected_x, - expected_y, - target_x, - target_y, - target_size, - range)) { - expected_x += expected.run_dx; - expected_y += expected.run_dy; - } - if (!can_attack( - attack_ctx, - actual_x, - actual_y, - target_x, - target_y, - target_size, - range)) { - actual_x += actual.run_dx; - actual_y += actual.run_dy; - } - } - if (actual_x != expected_x || - actual_y != expected_y) { - fprintf(stderr, - "route landing mismatch scenario=%s source=(%d,%d) target=(%d,%d,%d) range=%d expected=(%d,%d) actual=(%d,%d)\n", - scenario, - source_x, - source_y, - target_x, - target_y, - target_size, - range, - expected_x, - expected_y, - actual_x, - actual_y); - abort(); - } - checks++; - } - } - } - } - } - } - return checks; -} - -#endif diff --git a/ocean/osrs/tests/probe_colo_best_gear_dpt.c b/ocean/osrs/tests/probe_colo_best_gear_dpt.c deleted file mode 100644 index f9d7d62231..0000000000 --- a/ocean/osrs/tests/probe_colo_best_gear_dpt.c +++ /dev/null @@ -1,458 +0,0 @@ -#include -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -#include "ocean/osrs/tests/osrs_test_check.h" - -static void loadout_reset(ColosseumState* s, ColosseumContext* ctx, int mode, - float frac, uint32_t seed) { - col_init_context_typed(ctx); - ctx->config.loadout_profile_mode = mode; - ctx->config.beginner_loadout_fraction = frac; - col_finalize_route_topology(ctx); - memset(s, 0, sizeof(*s)); - col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); -} - -static int setup_contains(const uint8_t setup[NUM_GEAR_SLOTS], uint8_t item) { - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) - if (setup[slot] == item) return 1; - return 0; -} - -static void test_argmax_setup_and_style(void) { - printf("test_argmax_setup_and_style (T1, T3)\n"); - ColosseumContext ctx; - ColosseumState s; - loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 771); - col_build_npc_stats(); - s.player.current_attack = 118; - s.player.current_strength = 118; - s.player.current_ranged = 112; - s.player.current_magic = 99; - col_mark_live_loadout_dirty(&s); - - ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; - col_build_best_gear_table(&s, best); - - const ColoBestGear* bm = &best[COLO_GEAR_MAGIC][COLO_FREMENNIK_BERSERKER]; - CHECK("T1 magic best weapon is Tumeken's shadow", - bm->setup[GEAR_SLOT_WEAPON] == ITEM_TUMEKENS_SHADOW); - CHECK("T1 magic best setup includes Occult necklace", - setup_contains(bm->setup, ITEM_OCCULT_NECKLACE)); - CHECK("T1 magic best setup includes Confliction gauntlets", - setup_contains(bm->setup, ITEM_CONFLICTION_GAUNTLETS)); - CHECK("T1 magic best setup includes Avernic treads", - setup_contains(bm->setup, ITEM_AVERNIC_TREADS)); - - const ColoBestGear* jm = &best[COLO_GEAR_MAGIC][COLO_JAGUAR_WARRIOR]; - ColoNPC jnpc = (ColoNPC){ - .type = COLO_JAGUAR_WARRIOR, - .hp = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].hp, - .max_hp = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].hp, - .size = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].size, - .active = 1, .death_ticks = 0, - }; - float ref = col_expected_dpt_for_equipment_vs_npc(&s, jm->setup, &jnpc, 1); - CHECK("T1 jaguar magic argmax DPT reproducible through the leaf (accuracy-weighted)", - fabsf(ref - jm->dpt) < 1e-3f); - CHECK("T1 jaguar magic argmax setup is the shadow kit", - jm->setup[GEAR_SLOT_WEAPON] == ITEM_TUMEKENS_SHADOW && - setup_contains(jm->setup, ITEM_OCCULT_NECKLACE) && - setup_contains(jm->setup, ITEM_CONFLICTION_GAUNTLETS)); - - struct { ColoNpcType type; int want_style; const char* name; } spec[] = { - { COLO_FREMENNIK_BERSERKER, COLO_GEAR_MAGIC, "berserker -> magic" }, - { COLO_FREMENNIK_ARCHER, COLO_GEAR_MELEE, "archer -> melee" }, - { COLO_FREMENNIK_SEER, COLO_GEAR_RANGED, "seer -> ranged" }, - { COLO_SERPENT_SHAMAN, COLO_GEAR_RANGED, "serpent -> ranged" }, - }; - for (int k = 0; k < (int)(sizeof(spec) / sizeof(spec[0])); k++) { - int argmax = -1; - float best_dpt = -1.0f; - for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) - if (best[style][spec[k].type].dpt > best_dpt) { - best_dpt = best[style][spec[k].type].dpt; - argmax = style; - } - char label[96]; - snprintf(label, sizeof(label), "T3 argmax style: %s", spec[k].name); - CHECK(label, argmax == spec[k].want_style); - } -} - -static void test_beats_worn_single_swap(void) { - printf("test_beats_worn_single_swap (T2)\n"); - int modes[] = { COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, - COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY }; - for (int m = 0; m < 2; m++) { - ColosseumContext ctx; - ColosseumState s; - loadout_reset(&s, &ctx, modes[m], 0.0f, 401 + m); - col_build_npc_stats(); - s.player.current_attack = 118; - s.player.current_strength = 118; - s.player.current_ranged = 112; - s.player.current_magic = 99; - col_mark_live_loadout_dirty(&s); - - ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; - col_build_best_gear_table(&s, best); - - uint8_t weapons[64]; - int nweap = 0; - uint8_t add_seen[256] = {0}; - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - uint8_t w = s.player.equipped[slot]; - if (w != ITEM_NONE && item_is_weapon(w) && !add_seen[w]) { - add_seen[w] = 1; weapons[nweap++] = w; - } - } - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { - uint8_t w = - osrs_inventory_cell_item_index(&s.player.inventory_cells[cell]); - if (w != ITEM_NONE && item_is_weapon(w) && !add_seen[w]) { - add_seen[w] = 1; weapons[nweap++] = w; - } - } - - int violations = 0; - for (int wi = 0; wi < nweap; wi++) { - uint8_t w = weapons[wi]; - int wstyle = get_item_attack_style(w); - if (wstyle < 1 || wstyle > 3) continue; - ColoWeaponSet style_set = (ColoWeaponSet)(wstyle - 1); - uint8_t worn[NUM_GEAR_SLOTS]; - memcpy(worn, s.player.equipped, NUM_GEAR_SLOTS); - worn[GEAR_SLOT_WEAPON] = w; - if (item_is_two_handed(w)) worn[GEAR_SLOT_SHIELD] = ITEM_NONE; - for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) { - if (col_type_is_hazard_entity((ColoNpcType)type)) continue; - ColoNPC npc = (ColoNPC){ - .type = (ColoNpcType)type, - .hp = COLO_NPC_STATS[type].hp, - .max_hp = COLO_NPC_STATS[type].hp, - .size = COLO_NPC_STATS[type].size, - .active = 1, .death_ticks = 0, - }; - float worn_dpt = col_expected_dpt_for_equipment_vs_npc(&s, worn, &npc, 1); - if (best[style_set][type].dpt < worn_dpt - 1e-3f) violations++; - } - } - char label[96]; - snprintf(label, sizeof(label), "T2 best-gear >= worn single-swap (profile %d)", m); - CHECK(label, violations == 0); - } -} - -static void test_equip_and_augury_same_tick(void) { - printf("test_equip_and_augury_same_tick (T4)\n"); - ColosseumContext ctx; - ColosseumState s; - loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 99); - col_build_npc_stats(); - - int shadow_cell = -1; - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) - if (osrs_inventory_cell_item_index( - &s.player.inventory_cells[cell]) == ITEM_TUMEKENS_SHADOW) { - shadow_cell = cell; - break; - } - CHECK("T4 shadow is in the bag", shadow_cell >= 0); - if (shadow_cell < 0) return; - - CHECK("T4 starts on a non-magic weapon", - col_equipped_weapon_attack_style(&s.player) != ATTACK_STYLE_MAGIC); - s.player.current_prayer = 99; - - static float mask[COLO_ACTION_MASK_SIZE]; - col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); - int off_base = -1; - { - int offset = 0; - for (int h = 0; h < COLO_HEAD_OFFENSIVE; h++) offset += COLO_ACTION_DIMS[h]; - off_base = offset; - } - int augury_bit = off_base + 4; - CHECK("T4 mask allows Augury on a non-magic weapon (points-only)", - mask[augury_bit] == 1.0f); - - int act[COLO_NUM_ACTION_HEADS] = {0}; - act[COLO_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = shadow_cell + 1; - act[COLO_HEAD_OFFENSIVE] = 4; - col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, act); - - CHECK("T4 weapon equipped to shadow on the swap tick", - s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_TUMEKENS_SHADOW); - CHECK("T4 offensive prayer is Augury after the swap tick", - s.player.offensive_prayer == OFFENSIVE_PRAYER_AUGURY); - - const EncounterLoadoutStats* live = col_live_loadout_stats(&s); - EncounterLoadoutStats no_aug; - int cur_magic = s.player.current_magic; - encounter_compute_loadout_stats( - s.player.equipped, ATTACK_STYLE_MAGIC, OFFENSIVE_PRAYER_NONE, - cur_magic, FIGHT_STYLE_ACCURATE, - col_weapon_set_spell_base_damage(&s, COLO_GEAR_MAGIC), &no_aug); - encounter_update_loadout_level(&no_aug, OFFENSIVE_PRAYER_NONE, cur_magic, cur_magic); - CHECK("T4 live stats are the magic style (shadow equipped)", - live->style == ATTACK_STYLE_MAGIC); - CHECK("T4 Augury raises effective level over no-prayer", - live->eff_level > no_aug.eff_level); -} - -static void test_per_cell_marginal_bit(void) { - printf("test_per_cell_marginal_bit (T5)\n"); - ColosseumContext ctx; - ColosseumState s; - loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 55); - col_build_npc_stats(); - s.player.current_attack = 118; - s.player.current_strength = 118; - s.player.current_ranged = 112; - s.player.current_magic = 99; - col_mark_live_loadout_dirty(&s); - - memset(s.npcs, 0, sizeof(s.npcs)); - s.npcs[0] = (ColoNPC){ - .type = COLO_FREMENNIK_BERSERKER, - .hp = COLO_NPC_STATS[COLO_FREMENNIK_BERSERKER].hp, - .max_hp = COLO_NPC_STATS[COLO_FREMENNIK_BERSERKER].hp, - .size = COLO_NPC_STATS[COLO_FREMENNIK_BERSERKER].size, - .x = 18, .y = 18, .active = 1, .death_ticks = 0, - }; - s.player.x = 18; s.player.y = 20; - osrs_interaction_set(&s.interaction, 0); - - ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; - col_build_best_gear_table(&s, best); - - int magic_was_argmax = -1; - { - float bd = -1.0f; - for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) - if (best[style][COLO_FREMENNIK_BERSERKER].dpt > bd) { - bd = best[style][COLO_FREMENNIK_BERSERKER].dpt; - magic_was_argmax = style; - } - } - CHECK("T5 magic is the argmax with shadow present", magic_was_argmax == COLO_GEAR_MAGIC); - float magic_dpt_with = best[COLO_GEAR_MAGIC][COLO_FREMENNIK_BERSERKER].dpt; - - for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) - if (osrs_inventory_cell_item_index( - &s.player.inventory_cells[cell]) == ITEM_TUMEKENS_SHADOW) - s.player.inventory_cells[cell] = osrs_inventory_cell_empty(); - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) - if (s.player.equipped[slot] == ITEM_TUMEKENS_SHADOW) - s.player.equipped[slot] = ITEM_NONE; - - ColoBestGear best2[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; - col_build_best_gear_table(&s, best2); - float magic_dpt_without = best2[COLO_GEAR_MAGIC][COLO_FREMENNIK_BERSERKER].dpt; - CHECK("T5 removing shadow drops the magic best DPT", - magic_dpt_without < magic_dpt_with - 1e-3f); - int argmax_without = -1; - { - float bd = -1.0f; - for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) - if (best2[style][COLO_FREMENNIK_BERSERKER].dpt > bd) { - bd = best2[style][COLO_FREMENNIK_BERSERKER].dpt; - argmax_without = style; - } - } - CHECK("T5 removing shadow flips the berserker argmax away from magic", - argmax_without != COLO_GEAR_MAGIC); -} - -static void test_memo_result_preserving(void) { - printf("test_memo_result_preserving (T6)\n"); - ColosseumContext ctx; - ColosseumState s; - loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 313); - col_build_npc_stats(); - s.player.current_attack = 118; - s.player.current_strength = 118; - s.player.current_ranged = 112; - s.player.current_magic = 99; - col_mark_live_loadout_dirty(&s); - - ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; - col_build_best_gear_table(&s, best); - - int mismatches = 0; - for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) { - for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) { - if (col_type_is_hazard_entity((ColoNpcType)type)) continue; - const ColoBestGear* bg = &best[style][type]; - if (bg->dpt < 0.0f) continue; - ColoNPC npc = (ColoNPC){ - .type = (ColoNpcType)type, - .hp = COLO_NPC_STATS[type].hp, - .max_hp = COLO_NPC_STATS[type].hp, - .size = COLO_NPC_STATS[type].size, - .active = 1, .death_ticks = 0, - }; - float direct = col_expected_dpt_for_equipment_vs_npc(&s, bg->setup, &npc, 1); - if (direct != bg->dpt) mismatches++; - } - } - CHECK("T6 memoized oracle DPT bit-identical to un-memoized leaf", mismatches == 0); -} - -static void test_confliction_reference(void) { - printf("test_confliction_reference (T7)\n"); - ColosseumContext ctx; - ColosseumState s; - loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 717); - col_build_npc_stats(); - s.player.current_magic = 99; - col_mark_live_loadout_dirty(&s); - - ColoNPC npc = (ColoNPC){ - .type = COLO_JAGUAR_WARRIOR, - .hp = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].hp, - .max_hp = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].hp, - .size = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].size, - .active = 1, .death_ticks = 0, - }; - - uint8_t setup_1h[NUM_GEAR_SLOTS]; - memset(setup_1h, ITEM_NONE, NUM_GEAR_SLOTS); - setup_1h[GEAR_SLOT_WEAPON] = ITEM_TRIDENT_OF_SWAMP; - setup_1h[GEAR_SLOT_HANDS] = ITEM_CONFLICTION_GAUNTLETS; - setup_1h[GEAR_SLOT_NECK] = ITEM_OCCULT_NECKLACE; - - uint8_t setup_1h_nocon[NUM_GEAR_SLOTS]; - memcpy(setup_1h_nocon, setup_1h, NUM_GEAR_SLOTS); - setup_1h_nocon[GEAR_SLOT_HANDS] = ITEM_NONE; - - float dpt_con = col_expected_dpt_for_equipment_vs_npc(&s, setup_1h, &npc, 1); - float dpt_nocon = col_expected_dpt_for_equipment_vs_npc(&s, setup_1h_nocon, &npc, 1); - - EncounterLoadoutStats stats; - encounter_compute_loadout_stats( - setup_1h, ATTACK_STYLE_MAGIC, OFFENSIVE_PRAYER_AUGURY, 99, FIGHT_STYLE_ACCURATE, - col_weapon_set_spell_base_damage(&s, COLO_GEAR_MAGIC), &stats); - encounter_update_loadout_level(&stats, OFFENSIVE_PRAYER_AUGURY, 99, 99); - OsrsEquipmentEffectProfile effects; - encounter_derive_loadout_effect_profile(setup_1h, &effects); - CHECK("T7 confliction applies for a 1h magic weapon", - osrs_confliction_can_apply(&effects, ATTACK_STYLE_MAGIC, ITEM_TRIDENT_OF_SWAMP, 1)); - - int att_roll = osrs_player_att_roll(stats.eff_level, stats.attack_bonus); - const ColoNpcStats* ns = &COLO_NPC_STATS[COLO_JAGUAR_WARRIOR]; - int def_roll = col_npc_target_def_roll( - &npc, ns, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB); - float single = osrs_hit_chance(att_roll, def_roll); - float dbl = osrs_hit_chance_double(att_roll, def_roll); - float ref_hit = dbl / (1.0f + dbl - single); - float ref_dpt = ref_hit * (0.0f + (float)stats.max_hit) * 0.5f / (float)stats.attack_speed; - CHECK("T7 confliction DPT matches the reference steady-state formula", - fabsf(dpt_con - ref_dpt) < 1e-2f); - CHECK("T7 confliction (charged double-acc) beats no-confliction single roll", - dpt_con > dpt_nocon - 1e-6f && ref_hit > single); - - uint8_t setup_2h[NUM_GEAR_SLOTS]; - memset(setup_2h, ITEM_NONE, NUM_GEAR_SLOTS); - setup_2h[GEAR_SLOT_WEAPON] = ITEM_TUMEKENS_SHADOW; - setup_2h[GEAR_SLOT_HANDS] = ITEM_CONFLICTION_GAUNTLETS; - OsrsEquipmentEffectProfile effects_2h; - encounter_derive_loadout_effect_profile(setup_2h, &effects_2h); - CHECK("T7 confliction DISABLED with a 2h weapon (shadow)", - !osrs_confliction_can_apply(&effects_2h, ATTACK_STYLE_MAGIC, ITEM_TUMEKENS_SHADOW, 1)); -} - -static void test_best_is_locally_optimal(void) { - printf("test_best_is_locally_optimal (T8)\n"); - int modes[] = { COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, - COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY }; - int violations = 0; - for (int m = 0; m < 2; m++) { - ColosseumContext ctx; - ColosseumState s; - loadout_reset(&s, &ctx, modes[m], 0.0f, 808 + m); - col_build_npc_stats(); - s.player.current_attack = 118; - s.player.current_strength = 118; - s.player.current_ranged = 112; - s.player.current_magic = 99; - col_mark_live_loadout_dirty(&s); - ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; - col_build_best_gear_table(&s, best); - - for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) { - for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) { - if (col_type_is_hazard_entity((ColoNpcType)type)) continue; - const ColoBestGear* bg = &best[style][type]; - if (bg->dpt < 0.0f) continue; - ColoNPC npc = col_matchup_representative_npc((ColoNpcType)type); - for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { - if (slot == GEAR_SLOT_WEAPON) continue; - if (bg->setup[slot] == ITEM_NONE) continue; - uint8_t variant[NUM_GEAR_SLOTS]; - memcpy(variant, bg->setup, NUM_GEAR_SLOTS); - variant[slot] = ITEM_NONE; - float v = col_expected_dpt_for_equipment_vs_npc(&s, variant, &npc, 1); - if (v > bg->dpt + 1e-4f) violations++; - } - } - } - } - CHECK("T8 argmax setup is locally optimal under single-slot empties", violations == 0); -} - -static void calibrate_norm(void) { - printf("NORM calibration sweep (max best-gear DPT over profile x weapon x type)\n"); - int modes[] = { COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, - COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY }; - const char* names[] = { "speedrun", "beginner" }; - float global_max = 0.0f; - for (int m = 0; m < 2; m++) { - ColosseumContext ctx; - ColosseumState s; - loadout_reset(&s, &ctx, modes[m], 0.0f, 1000 + m); - col_build_npc_stats(); - s.player.current_attack = 118; - s.player.current_strength = 118; - s.player.current_ranged = 112; - s.player.current_magic = 99; - col_mark_live_loadout_dirty(&s); - ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; - col_build_best_gear_table(&s, best); - float pmax = 0.0f; - int pmax_style = -1, pmax_type = -1; - for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) - for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) - if (best[style][type].dpt > pmax) { - pmax = best[style][type].dpt; - pmax_style = style; pmax_type = type; - } - printf(" %-9s max best-gear DPT = %.3f (style %d, type %s)\n", - names[m], pmax, pmax_style, colo_npc_type_name(pmax_type)); - if (pmax > global_max) global_max = pmax; - } - printf(" GLOBAL max best-gear DPT = %.3f\n", global_max); - printf(" NORM for top setup at ~0.9 -> %.1f ; current COLO_EXPECTED_DPT_NORM = %.1f\n", - global_max / 0.9f, (double)COLO_EXPECTED_DPT_NORM); -} - -int main(int argc, char** argv) { - if (argc > 1 && strcmp(argv[1], "--calibrate") == 0) { - calibrate_norm(); - return 0; - } - printf("colosseum best-gear DPT oracle probe\n\n"); - test_argmax_setup_and_style(); - test_beats_worn_single_swap(); - test_equip_and_augury_same_tick(); - test_per_cell_marginal_bit(); - test_memo_result_preserving(); - test_confliction_reference(); - test_best_is_locally_optimal(); - return osrs_test_summary(); -} diff --git a/ocean/osrs/tests/probe_colo_bis_gear_prayer.c b/ocean/osrs/tests/probe_colo_bis_gear_prayer.c deleted file mode 100644 index ebf69c7865..0000000000 --- a/ocean/osrs/tests/probe_colo_bis_gear_prayer.c +++ /dev/null @@ -1,50 +0,0 @@ -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -static const OffensivePrayer STALE_PRAYERS[3] = { - OFFENSIVE_PRAYER_PIETY, OFFENSIVE_PRAYER_RIGOUR, OFFENSIVE_PRAYER_AUGURY -}; - -int main(void) { - int checks = 0, fails = 0; - for (uint32_t i = 1; i <= 60; i++) { - uint32_t seed = i * 0x9E3779B1u; - ColosseumContext ctx; - ColosseumState s; - col_init_context_typed(&ctx); - ctx.config.start_wave = 0; - ctx.config.bis_gear_oracle_mode = 1; - col_finalize_route_topology(&ctx); - memset(&s, 0, sizeof(s)); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); - - int target = -1; - for (int n = 0; n < COLO_MAX_NPCS; n++) { - if (col_npc_is_live_enemy(&s.npcs[n])) { target = n; break; } - } - if (target < 0) continue; - osrs_interaction_set(&s.interaction, target); - - for (int p = 0; p < 3; p++) { - s.player.offensive_prayer = STALE_PRAYERS[p]; - col_apply_bis_gear_oracle(&s); - OffensivePrayer expected = encounter_offensive_prayer_for_style( - col_weapon_set_attack_style(s.weapon_set)); - checks++; - if (s.player.offensive_prayer != expected) { - fails++; - printf("seed=%u stale=%d set=%d style=%d off=%d expected=%d\n", - seed, STALE_PRAYERS[p], (int)s.weapon_set, - (int)col_weapon_set_attack_style(s.weapon_set), - (int)s.player.offensive_prayer, (int)expected); - } - } - } - printf("checked %d oracle commits, %d mismatched prayers\n", checks, fails); - if (checks > 0 && fails == 0) { printf("PASS\n"); return 0; } - printf("FAIL\n"); - return 1; -} diff --git a/ocean/osrs/tests/probe_colo_damage_scale.c b/ocean/osrs/tests/probe_colo_damage_scale.c deleted file mode 100644 index bc82af433c..0000000000 --- a/ocean/osrs/tests/probe_colo_damage_scale.c +++ /dev/null @@ -1,121 +0,0 @@ -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -static int failures = 0; - -static void check(const char* name, int got, int want) { - if (got != want) { - printf("FAIL %s: got %d want %d\n", name, got, want); - failures++; - } else { - printf("ok %s: %d\n", name, got); - } -} - -static int direct_hp_loss(uint32_t seed, int dmg, float scale) { - static ColosseumContext ctx; - static ColosseumState s; - col_init_context_typed(&ctx); - col_finalize_route_topology(&ctx); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); - s.active_player_damage_received_scale = scale; - int hp0 = s.player.current_hitpoints; - col_damage_player_from(&s, dmg, COLO_MANTICORE, COLO_DMG_UNPRAYABLE); - return hp0 - s.player.current_hitpoints; -} - -static int queued_hp_loss(uint32_t seed, int dmg, float scale) { - static ColosseumContext ctx; - static ColosseumState s; - col_init_context_typed(&ctx); - col_finalize_route_topology(&ctx); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); - s.active_player_damage_received_scale = scale; - int hp0 = s.player.current_hitpoints; - int prayed = 0; - - s.player.prayer = PRAYER_NONE; - EncounterPendingHit hit = encounter_pending_hit_resolved_at_throw( - dmg, 1, ATTACK_STYLE_RANGED, s.player.prayer, COLO_SERPENT_SHAMAN, 0, 1, &prayed); - int landed_raw = hit.damage; - col_push_player_pending_hit(&s, hit); - - s.tick++; - col_resolve_player_pending_hits_ctx(&s, &ctx); - (void)landed_raw; - return hp0 - s.player.current_hitpoints; -} - -static int doom_stacks_after_melee(uint32_t seed, int dmg, float scale) { - static ColosseumContext ctx; - static ColosseumState s; - col_init_context_typed(&ctx); - col_finalize_route_topology(&ctx); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); - s.active_player_damage_received_scale = scale; - s.modifiers.active_mask |= (1u << COLO_MOD_DOOM); - s.modifiers.tier[COLO_MOD_DOOM] = 1; - s.player.prayer = PRAYER_NONE; - s.doom_stacks = 0; - col_apply_instant_melee_hit(&s, &ctx, 0, COLO_JAGUAR_WARRIOR, dmg, 1); - return s.doom_stacks; -} - -static int round_half_up(int dmg, float scale) { - if (scale >= 1.0f) return dmg; - if (scale <= 0.0f) return 0; - int v = (int)((float)dmg * scale + 0.5f); - return v < 0 ? 0 : v; -} - -int main(void) { - const uint32_t SEED = 0xC0DEu; - const int DMG = 20; - - int d_full = direct_hp_loss(SEED, DMG, 1.0f); - int d_half = direct_hp_loss(SEED, DMG, 0.5f); - int d_zero = direct_hp_loss(SEED, DMG, 0.0f); - check("direct scale=1.0 == raw dmg", d_full, DMG); - check("direct scale=0.5 == round_half_up", d_half, round_half_up(DMG, 0.5f)); - check("direct scale=0.0 == 0 (invuln)", d_zero, 0); - - int q_full = queued_hp_loss(SEED, DMG, 1.0f); - int q_half = queued_hp_loss(SEED, DMG, 0.5f); - int q_zero = queued_hp_loss(SEED, DMG, 0.0f); - check("queued scale=1.0 landed > 0 (sanity)", q_full > 0 ? 1 : 0, 1); - check("queued scale=0.5 == round_half_up(landed)", q_half, round_half_up(q_full, 0.5f)); - check("queued scale=0.0 == 0 (invuln)", q_zero, 0); - - int doom_full = doom_stacks_after_melee(SEED, DMG, 1.0f); - int doom_zero = doom_stacks_after_melee(SEED, DMG, 0.0f); - check("doom scale=1.0 accrues one stack", doom_full, 1); - check("doom scale=0.0 accrues NO stack (invuln-equivalent)", doom_zero, 0); - - int identity_ok = 1; - for (int dmg = 0; dmg <= 255; dmg++) { - ColosseumState s; - memset(&s, 0, sizeof(s)); - s.active_player_damage_received_scale = 1.0f; - if (col_scale_incoming_damage(&s, dmg) != dmg) { identity_ok = 0; break; } - } - check("scale=1.0 identity over dmg[0,255]", identity_ok, 1); - - int zero_ok = 1; - for (int dmg = 0; dmg <= 255; dmg++) { - ColosseumState s; - memset(&s, 0, sizeof(s)); - s.active_player_damage_received_scale = 0.0f; - if (col_scale_incoming_damage(&s, dmg) != 0) { zero_ok = 0; break; } - } - check("scale=0.0 zeros dmg[0,255]", zero_ok, 1); - - if (failures) { - printf("\n%d FAILURE(S)\n", failures); - return 1; - } - printf("\nALL PASS\n"); - return 0; -} diff --git a/ocean/osrs/tests/probe_colo_prayer_switch_slip.c b/ocean/osrs/tests/probe_colo_prayer_switch_slip.c deleted file mode 100644 index 615ddd088f..0000000000 --- a/ocean/osrs/tests/probe_colo_prayer_switch_slip.c +++ /dev/null @@ -1,46 +0,0 @@ -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -static void prep(ColosseumContext* ctx, ColosseumState* s, float fail_prob) { - col_init_context_typed(ctx); - memset(s, 0, sizeof(*s)); - s->rng_state = 0xC0FFEEu; - - ENCOUNTER_COLOSSEUM.put_float((EncounterState*)s, (EncounterContext*)ctx, - "prayer_switch_fail_prob", fail_prob); - col_finalize_route_topology(ctx); - s->player.prayer = PRAYER_PROTECT_RANGED; - s->player.current_prayer = 990; - s->player.prayer_just_activated = 0; -} - -int main(void) { - int actions[COLO_NUM_ACTION_HEADS] = {0}; - actions[COLO_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC; - - ColosseumContext ctx_off; ColosseumState s_off; - prep(&ctx_off, &s_off, 0.0f); - col_player_pretick(&s_off, &ctx_off, actions); - int applied = (s_off.player.prayer == PRAYER_PROTECT_MAGIC); - - ColosseumContext ctx_on; ColosseumState s_on; - prep(&ctx_on, &s_on, 1.0f); - col_player_pretick(&s_on, &ctx_on, actions); - int reverted = (s_on.player.prayer == PRAYER_PROTECT_RANGED); - int flag_clear = (s_on.player.prayer_just_activated == 0); - - printf("p=0.0: prayer=%d (expect MAGIC=%d) applied=%d\n", - s_off.player.prayer, PRAYER_PROTECT_MAGIC, applied); - printf("p=1.0: prayer=%d (expect RANGED=%d) reverted=%d just_activated=%d\n", - s_on.player.prayer, PRAYER_PROTECT_RANGED, reverted, - s_on.player.prayer_just_activated); - if (applied && reverted && flag_clear) { - printf("PASS\n"); - return 0; - } - printf("FAIL\n"); - return 1; -} diff --git a/ocean/osrs/tests/probe_colo_remove_brews.c b/ocean/osrs/tests/probe_colo_remove_brews.c deleted file mode 100644 index cc4457062a..0000000000 --- a/ocean/osrs/tests/probe_colo_remove_brews.c +++ /dev/null @@ -1,43 +0,0 @@ -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -static int reset_brew_doses(int remove_brews, uint32_t seed) { - ColosseumContext ctx; - ColosseumState s; - col_init_context_typed(&ctx); - ctx.config.start_wave = 0; - ctx.config.loadout_profile_mode = 2; - ctx.config.beginner_loadout_fraction = 0.5f; - memset(&s, 0, sizeof(s)); - - ENCOUNTER_COLOSSEUM.put_int((EncounterState*)&s, (EncounterContext*)&ctx, - "remove_brews", remove_brews); - col_finalize_route_topology(&ctx); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); - return s.player.brew_doses; -} - -int main(void) { - int off_min = 1 << 30, off_max = -1, on_min = 1 << 30, on_max = -1; - for (uint32_t i = 1; i <= 60; i++) { - uint32_t seed = i * 0x9E3779B1u; - int off = reset_brew_doses(0, seed); - int on = reset_brew_doses(1, seed); - if (off < off_min) off_min = off; - if (off > off_max) off_max = off; - if (on < on_min) on_min = on; - if (on > on_max) on_max = on; - } - printf("remove_brews=0: brew_doses min=%d max=%d (expect both loadouts, 4 and 24)\n", - off_min, off_max); - printf("remove_brews=1: brew_doses min=%d max=%d (expect 0 and 0)\n", on_min, on_max); - if (on_max == 0 && off_min > 0) { - printf("PASS\n"); - return 0; - } - printf("FAIL\n"); - return 1; -} diff --git a/ocean/osrs/tests/probe_colo_rng_diversity.c b/ocean/osrs/tests/probe_colo_rng_diversity.c deleted file mode 100644 index d45c58a54d..0000000000 --- a/ocean/osrs/tests/probe_colo_rng_diversity.c +++ /dev/null @@ -1,86 +0,0 @@ -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -#define FNV_OFFSET 1469598103934665603ULL -#define FNV_PRIME 1099511628211ULL - -static uint32_t lowbias32_ref(uint32_t x) { - x ^= x >> 16; - x *= 0x7feb352dU; - x ^= x >> 15; - x *= 0x846ca68bU; - x ^= x >> 16; - return x; -} - -static uint64_t splitmix64(uint64_t* s) { - uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); - z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; - z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; - return z ^ (z >> 31); -} - -static uint64_t spawn_digest(const ColosseumState* s) { - uint64_t h = FNV_OFFSET; - for (int i = 0; i < COLO_MAX_NPCS; i++) { - int32_t v[3] = { (int32_t)s->npcs[i].type, s->npcs[i].x, s->npcs[i].y }; - for (int k = 0; k < 3; k++) { h ^= (uint64_t)(uint32_t)v[k]; h *= FNV_PRIME; } - } - h ^= (uint64_t)(uint32_t)s->wave; h *= FNV_PRIME; - h ^= (uint64_t)s->rng_state; h *= FNV_PRIME; - return h; -} - -static void fill_actions(const ColosseumState* s, uint64_t* rng, int acts[COLO_NUM_ACTION_HEADS]) { - for (int head = 0; head < COLO_NUM_ACTION_HEADS; head++) - acts[head] = (int)(splitmix64(rng) % (uint64_t)COLO_ACTION_DIMS[head]); - if (s->modifiers.draft_pending) { - acts[COLO_HEAD_PRIMARY] = 0; - acts[COLO_HEAD_MODIFIER_SELECT] = 1 + (int)(splitmix64(rng) % COLO_MODIFIER_DRAFT_OPTIONS); - } -} - -static uint64_t reset_env_seed(ColosseumState* s, ColosseumContext* ctx, uint32_t index) { - memset(s, 0, sizeof(*s)); - s->rng_state = lowbias32_ref(index); - col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, 0); - return spawn_digest(s); -} - -int main(void) { - ColosseumContext ctx; - col_init_context_typed(&ctx); - ctx.config.start_wave = 0; - col_finalize_route_topology(&ctx); - - enum { N = 8 }; - uint64_t dig[N]; - ColosseumState s; - for (uint32_t i = 0; i < N; i++) dig[i] = reset_env_seed(&s, &ctx, i); - int distinct = 1; - for (int i = 0; i < N; i++) - for (int j = i + 1; j < N; j++) - if (dig[i] == dig[j]) distinct = 0; - printf("per-env: %d indices, %s\n", N, distinct ? "all distinct" : "COLLISION"); - - uint64_t ep1 = reset_env_seed(&s, &ctx, 0); - uint64_t arng = 0xABCDEF01u; - int acts[COLO_NUM_ACTION_HEADS]; - for (int t = 0; t < 120 && !s.episode_over; t++) { - fill_actions(&s, &arng, acts); - col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, acts); - } - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 0); - uint64_t ep2 = spawn_digest(&s); - int episode_varies = (ep1 != ep2); - printf("per-episode: ep1=%016llx ep2=%016llx -> %s\n", - (unsigned long long)ep1, (unsigned long long)ep2, - episode_varies ? "varies" : "IDENTICAL"); - - if (distinct && episode_varies) { printf("PASS\n"); return 0; } - printf("FAIL\n"); - return 1; -} diff --git a/ocean/osrs/tests/probe_colo_skyfall_dodge.c b/ocean/osrs/tests/probe_colo_skyfall_dodge.c deleted file mode 100644 index 0c41535965..0000000000 --- a/ocean/osrs/tests/probe_colo_skyfall_dodge.c +++ /dev/null @@ -1,574 +0,0 @@ -#include -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -#define PROBE_NPC_TELLS_OFFSET 37 -#define PROBE_SKYFALL_DAMAGE 38 -#define PROBE_PLAYER_X 17 -#define PROBE_PLAYER_Y 16 -#define PROBE_JAVELIN_X 20 -#define PROBE_JAVELIN_Y 16 - -typedef enum { - PROBE_TARGET_AFTER_MOVE_NONE = 0, - PROBE_TARGET_AFTER_MOVE_SAME_TICK, - PROBE_TARGET_AFTER_MOVE_NEXT_TICK, -} ProbeTargetAfterMoveMode; - -typedef struct { - int visible_tick; - int visible_timer; - int marked_x; - int marked_y; - int obs_slot; - float obs_pending; - float obs_timer; - float obs_dx; - float obs_dy; -} ProbeSkyfallObs; - -typedef struct { - int damage_taken; - int first_visible_tick; - int first_visible_timer; - int move_action; - int move_tick; - int landing_tick; - int player_x_after_move; - int player_y_after_move; - int interaction_after_move; - int player_x_before_landing; - int player_y_before_landing; -} ProbeStepDodgeResult; - -static void probe_fail(const char* label) { - fprintf(stderr, "FAIL %s\n", label); - abort(); -} - -static void probe_check(const char* label, int ok) { - if (!ok) probe_fail(label); - printf("PASS %s\n", label); -} - -static void probe_check_float(const char* label, float got, float expected) { - float delta = fabsf(got - expected); - if (delta > 0.000001f) { - fprintf(stderr, "FAIL %s got=%.9f expected=%.9f delta=%.9f\n", - label, got, expected, delta); - abort(); - } - printf("PASS %s got=%.6f expected=%.6f\n", label, got, expected); -} - -static void probe_clear_npcs(ColosseumState* s) { - memset(s->npcs, 0, sizeof(s->npcs)); - memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); - memset(s->totems, 0, sizeof(s->totems)); - memset(s->bees, 0, sizeof(s->bees)); - col_rebuild_player_collision_flags(s); -} - -static void probe_init_context(ColosseumContext* ctx) { - col_init_context_typed(ctx); - ctx->config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY; - ctx->config.beginner_loadout_fraction = 0.0f; - ctx->config.step_out_forecast_obs_enabled = 1; - ctx->config.action_debug_log = 0; - col_finalize_route_topology(ctx); -} - -static void probe_init_empty_state( - ColosseumState* s, - ColosseumContext* ctx, - uint32_t seed -) { - probe_init_context(ctx); - memset(s, 0, sizeof(*s)); - col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); - probe_clear_npcs(s); - s->wave_spawn_delay = 0; - s->wave_ready_delay = 0; - s->modifiers.draft_pending = 0; - s->warband_cycle_anchor = s->tick; - s->player.x = PROBE_PLAYER_X; - s->player.y = PROBE_PLAYER_Y; - s->player.current_hitpoints = 99; - s->player.attack_timer = 99; - s->player_dest_x = -1; - s->player_dest_y = -1; - col_apply_weapon_set(s, COLO_GEAR_RANGED); - encounter_pending_hit_queue_clear(&s->player_pending_hits); - col_rebuild_player_collision_flags(s); -} - -static ColoJavelinState* probe_spawn_javelin(ColosseumState* s) { - col_init_npc(s, 0, COLO_JAVELIN_COLOSSUS, PROBE_JAVELIN_X, PROBE_JAVELIN_Y); - ColoNPC* npc = &s->npcs[0]; - npc->stun_timer = 0; - npc->frozen_ticks = 0; - return colo_npc_javelin(npc); -} - -static int probe_obs_slot_for_npc(const ColosseumState* s, int npc_idx) { - for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { - if (s->current_obs_slots[slot] == npc_idx) return slot; - } - probe_fail("javelin obs slot found"); - return -1; -} - -static float* probe_npc_tells(float* obs, int obs_slot) { - int base = COLO_OBS_AFTER_EQUIPPED_SELF + - obs_slot * COLO_FEATURES_PER_NPC + - PROBE_NPC_TELLS_OFFSET; - return &obs[base]; -} - -static ProbeSkyfallObs probe_read_skyfall_obs( - ColosseumState* s, - ColosseumContext* ctx, - float* obs -) { - col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); - int obs_slot = probe_obs_slot_for_npc(s, 0); - float* tells = probe_npc_tells(obs, obs_slot); - ColoJavelinState* jv = colo_npc_javelin(&s->npcs[0]); - ProbeSkyfallObs out = { - .visible_tick = s->tick, - .visible_timer = jv->skyfall_timer, - .marked_x = jv->skyfall_tile_x, - .marked_y = jv->skyfall_tile_y, - .obs_slot = obs_slot, - .obs_pending = tells[0], - .obs_timer = tells[1], - .obs_dx = tells[2], - .obs_dy = tells[3], - }; - return out; -} - -static int probe_move_action_ending_off_tile( - const ColosseumState* s, - const ColosseumContext* ctx, - int x, - int y -) { - for (int action = 1; action < ENCOUNTER_MOVE_ACTIONS; action++) { - ColosseumState tmp = *s; - ColoGeometryContext geometry = { - .state = &tmp, - .context = ctx, - }; - int moved = encounter_move_to_target( - &tmp.player, - ENCOUNTER_MOVE_TARGET_DX[action], - ENCOUNTER_MOVE_TARGET_DY[action], - col_player_walkable, - &geometry); - if (moved > 0 && (tmp.player.x != x || tmp.player.y != y)) return action; - } - probe_fail("move action ending off marked tile found"); - return 0; -} - -static int probe_primary_attack_action_for_slot(int obs_slot) { - if (obs_slot < 0 || obs_slot >= COLO_OBS_NPCS) probe_fail("valid obs target slot"); - return col_primary_attack_action_for_obs_slot(obs_slot); -} - -static void probe_zero_actions(int* actions) { - for (int head = 0; head < COLO_NUM_ACTION_HEADS; head++) actions[head] = 0; -} - -static void probe_step(ColosseumState* s, ColosseumContext* ctx, int* actions) { - col_step_ctx((EncounterState*)s, (EncounterContext*)ctx, actions); -} - -static void probe_establish_attack_lock(ColosseumState* s, ColosseumContext* ctx) { - int actions[COLO_NUM_ACTION_HEADS]; - float obs[COLO_NUM_OBS]; - probe_zero_actions(actions); - col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); - actions[COLO_HEAD_PRIMARY] = - probe_primary_attack_action_for_slot(probe_obs_slot_for_npc(s, 0)); - probe_step(s, ctx, actions); - probe_check("attack lock active before skyfall fire", - osrs_interaction_active(&s->interaction) && - s->interaction.target_slot == 0); -} - -static ProbeSkyfallObs probe_fire_skyfall_visible( - ColosseumState* s, - ColosseumContext* ctx, - int attack_timer_before_fire -) { - int actions[COLO_NUM_ACTION_HEADS]; - static float obs[COLO_NUM_OBS]; - ColoJavelinState* jv = colo_npc_javelin(&s->npcs[0]); - jv->attack_count = 4; - s->npcs[0].attack_timer = attack_timer_before_fire; - probe_zero_actions(actions); - probe_step(s, ctx, actions); - probe_check("real step fired skyfall", - jv->skyfall_pending == 1 && - jv->skyfall_timer == COLO_JAVELIN_SKYFALL_DELAY && - jv->skyfall_tile_x == s->player.x && - jv->skyfall_tile_y == s->player.y); - int rolled_damage = jv->skyfall_damage; - jv->skyfall_damage = PROBE_SKYFALL_DAMAGE; - ProbeSkyfallObs seen = probe_read_skyfall_obs(s, ctx, obs); - printf( - "FULL_STEP fire tick=%d marked=(%d,%d) timer=%d rolled_damage=%d forced_damage=%d obs_slot=%d obs=(pending %.1f timer %.3f dx %.3f dy %.3f)\n", - seen.visible_tick, - seen.marked_x, - seen.marked_y, - seen.visible_timer, - rolled_damage, - jv->skyfall_damage, - seen.obs_slot, - seen.obs_pending, - seen.obs_timer, - seen.obs_dx, - seen.obs_dy); - return seen; -} - -static ProbeStepDodgeResult probe_run_no_lock_wait_case(int wait_visible_ticks) { - ColosseumContext ctx; - ColosseumState s; - int actions[COLO_NUM_ACTION_HEADS]; - probe_init_empty_state(&s, &ctx, 0x5100u + (uint32_t)wait_visible_ticks); - probe_spawn_javelin(&s); - ProbeSkyfallObs first = probe_fire_skyfall_visible(&s, &ctx, 0); - int move_action = probe_move_action_ending_off_tile( - &s, &ctx, first.marked_x, first.marked_y); - int hp_before = s.player.current_hitpoints; - int move_tick = -1; - int player_x_after_move = s.player.x; - int player_y_after_move = s.player.y; - int interaction_after_move = osrs_interaction_active(&s.interaction) - ? s.interaction.target_slot : -1; - - for (int i = 0; i < wait_visible_ticks; i++) { - probe_zero_actions(actions); - probe_step(&s, &ctx, actions); - } - - if (colo_npc_javelin(&s.npcs[0])->skyfall_pending) { - probe_zero_actions(actions); - actions[COLO_HEAD_PRIMARY] = move_action; - move_tick = s.tick + 1; - probe_step(&s, &ctx, actions); - player_x_after_move = s.player.x; - player_y_after_move = s.player.y; - interaction_after_move = osrs_interaction_active(&s.interaction) - ? s.interaction.target_slot : -1; - } - - int player_x_before_landing = s.player.x; - int player_y_before_landing = s.player.y; - while (colo_npc_javelin(&s.npcs[0])->skyfall_pending) { - player_x_before_landing = s.player.x; - player_y_before_landing = s.player.y; - probe_zero_actions(actions); - probe_step(&s, &ctx, actions); - } - - ProbeStepDodgeResult result = { - .damage_taken = hp_before - s.player.current_hitpoints, - .first_visible_tick = first.visible_tick, - .first_visible_timer = first.visible_timer, - .move_action = move_action, - .move_tick = move_tick, - .landing_tick = s.tick, - .player_x_after_move = player_x_after_move, - .player_y_after_move = player_y_after_move, - .interaction_after_move = interaction_after_move, - .player_x_before_landing = player_x_before_landing, - .player_y_before_landing = player_y_before_landing, - }; - return result; -} - -static ProbeStepDodgeResult probe_run_attack_lock_case( - const char* label, - ProbeTargetAfterMoveMode target_mode -) { - ColosseumContext ctx; - ColosseumState s; - int actions[COLO_NUM_ACTION_HEADS]; - static float obs[COLO_NUM_OBS]; - probe_init_empty_state(&s, &ctx, 0x6200u + (uint32_t)target_mode); - probe_spawn_javelin(&s); - ColoJavelinState* jv = colo_npc_javelin(&s.npcs[0]); - jv->attack_count = 4; - s.npcs[0].attack_timer = 2; - int player_range = col_player_attack_range(&s); - int npc_dist = col_npc_dist_to_player(&s, &s.npcs[0]); - printf( - "LOCK_SETUP %s player_range=%d npc_dist=%d player=(%d,%d) javelin_sw=(%d,%d)\n", - label, - player_range, - npc_dist, - s.player.x, - s.player.y, - s.npcs[0].x, - s.npcs[0].y); - probe_establish_attack_lock(&s, &ctx); - ProbeSkyfallObs first = probe_fire_skyfall_visible(&s, &ctx, 1); - int move_action = probe_move_action_ending_off_tile( - &s, &ctx, first.marked_x, first.marked_y); - int hp_before = s.player.current_hitpoints; - - probe_zero_actions(actions); - actions[COLO_HEAD_PRIMARY] = move_action; - if (target_mode == PROBE_TARGET_AFTER_MOVE_SAME_TICK) { - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - actions[COLO_HEAD_PRIMARY] = - probe_primary_attack_action_for_slot(probe_obs_slot_for_npc(&s, 0)); - } - int move_tick = s.tick + 1; - probe_step(&s, &ctx, actions); - int player_x_after_move = s.player.x; - int player_y_after_move = s.player.y; - int interaction_after_move = osrs_interaction_active(&s.interaction) - ? s.interaction.target_slot : -1; - - if (target_mode == PROBE_TARGET_AFTER_MOVE_NEXT_TICK && - colo_npc_javelin(&s.npcs[0])->skyfall_pending) { - probe_zero_actions(actions); - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - actions[COLO_HEAD_PRIMARY] = - probe_primary_attack_action_for_slot(probe_obs_slot_for_npc(&s, 0)); - probe_step(&s, &ctx, actions); - } - - int player_x_before_landing = s.player.x; - int player_y_before_landing = s.player.y; - while (colo_npc_javelin(&s.npcs[0])->skyfall_pending) { - player_x_before_landing = s.player.x; - player_y_before_landing = s.player.y; - probe_zero_actions(actions); - probe_step(&s, &ctx, actions); - } - - ProbeStepDodgeResult result = { - .damage_taken = hp_before - s.player.current_hitpoints, - .first_visible_tick = first.visible_tick, - .first_visible_timer = first.visible_timer, - .move_action = move_action, - .move_tick = move_tick, - .landing_tick = s.tick, - .player_x_after_move = player_x_after_move, - .player_y_after_move = player_y_after_move, - .interaction_after_move = interaction_after_move, - .player_x_before_landing = player_x_before_landing, - .player_y_before_landing = player_y_before_landing, - }; - return result; -} - -static ProbeStepDodgeResult probe_run_attack_lock_idle_case(const char* label) { - ColosseumContext ctx; - ColosseumState s; - int actions[COLO_NUM_ACTION_HEADS]; - probe_init_empty_state(&s, &ctx, 0x6300u); - probe_spawn_javelin(&s); - ColoJavelinState* jv = colo_npc_javelin(&s.npcs[0]); - jv->attack_count = 4; - s.npcs[0].attack_timer = 2; - printf("LOCK_SETUP %s player_range=%d npc_dist=%d\n", - label, - col_player_attack_range(&s), - col_npc_dist_to_player(&s, &s.npcs[0])); - probe_establish_attack_lock(&s, &ctx); - ProbeSkyfallObs first = probe_fire_skyfall_visible(&s, &ctx, 1); - int hp_before = s.player.current_hitpoints; - int player_x_before_landing = s.player.x; - int player_y_before_landing = s.player.y; - - while (colo_npc_javelin(&s.npcs[0])->skyfall_pending) { - player_x_before_landing = s.player.x; - player_y_before_landing = s.player.y; - probe_zero_actions(actions); - probe_step(&s, &ctx, actions); - } - - ProbeStepDodgeResult result = { - .damage_taken = hp_before - s.player.current_hitpoints, - .first_visible_tick = first.visible_tick, - .first_visible_timer = first.visible_timer, - .move_action = 0, - .move_tick = -1, - .landing_tick = s.tick, - .player_x_after_move = s.player.x, - .player_y_after_move = s.player.y, - .interaction_after_move = osrs_interaction_active(&s.interaction) - ? s.interaction.target_slot : -1, - .player_x_before_landing = player_x_before_landing, - .player_y_before_landing = player_y_before_landing, - }; - return result; -} - -static void probe_print_step_result(const char* label, ProbeStepDodgeResult r) { - printf( - "%s first_visible_tick=%d first_timer=%d move_action=%d move_tick=%d landing_tick=%d after_move=(%d,%d) before_landing=(%d,%d) interaction_after_move=%d damage=%d\n", - label, - r.first_visible_tick, - r.first_visible_timer, - r.move_action, - r.move_tick, - r.landing_tick, - r.player_x_after_move, - r.player_y_after_move, - r.player_x_before_landing, - r.player_y_before_landing, - r.interaction_after_move, - r.damage_taken); -} - -static void probe_obs_honesty(void) { - printf("\n== OBS HONESTY ==\n"); - ColosseumContext ctx; - ColosseumState s; - static float obs[COLO_NUM_OBS]; - probe_init_empty_state(&s, &ctx, 0x1001u); - ColoJavelinState* jv = probe_spawn_javelin(&s); - jv->skyfall_pending = 1; - jv->skyfall_tile_x = PROBE_PLAYER_X; - jv->skyfall_tile_y = PROBE_PLAYER_Y; - jv->skyfall_timer = COLO_JAVELIN_SKYFALL_DELAY; - jv->skyfall_damage = PROBE_SKYFALL_DAMAGE; - - const int player_positions[][2] = { - {PROBE_PLAYER_X, PROBE_PLAYER_Y}, - {PROBE_PLAYER_X - 1, PROBE_PLAYER_Y}, - {PROBE_PLAYER_X + 1, PROBE_PLAYER_Y + 1}, - {PROBE_PLAYER_X - 2, PROBE_PLAYER_Y - 2}, - }; - int count = (int)(sizeof(player_positions) / sizeof(player_positions[0])); - for (int i = 0; i < count; i++) { - s.player.x = player_positions[i][0]; - s.player.y = player_positions[i][1]; - col_rebuild_player_collision_flags(&s); - ProbeSkyfallObs seen = probe_read_skyfall_obs(&s, &ctx, obs); - float expected_dx = col_obs_rel_x(jv->skyfall_tile_x, s.player.x); - float expected_dy = col_obs_rel_y(jv->skyfall_tile_y, s.player.y); - printf( - "OBS player=(%d,%d) marked=(%d,%d) raw_delta=(%d,%d) obs_slot=%d pending=%.1f timer=%.3f dx=%.6f dy=%.6f\n", - s.player.x, - s.player.y, - jv->skyfall_tile_x, - jv->skyfall_tile_y, - jv->skyfall_tile_x - s.player.x, - jv->skyfall_tile_y - s.player.y, - seen.obs_slot, - seen.obs_pending, - seen.obs_timer, - seen.obs_dx, - seen.obs_dy); - probe_check_float("skyfall tell dx matches marked tile", seen.obs_dx, expected_dx); - probe_check_float("skyfall tell dy matches marked tile", seen.obs_dy, expected_dy); - } -} - -static void probe_direct_resolve(void) { - printf("\n== SIM DODGE LOGIC ==\n"); - ColosseumContext ctx; - ColosseumState s; - probe_init_empty_state(&s, &ctx, 0x2002u); - ColoJavelinState* jv = probe_spawn_javelin(&s); - jv->skyfall_pending = 1; - jv->skyfall_timer = 1; - jv->skyfall_damage = PROBE_SKYFALL_DAMAGE; - jv->skyfall_tile_x = s.player.x; - jv->skyfall_tile_y = s.player.y; - int hp_before = s.player.current_hitpoints; - col_npc_resolve_javelin_skyfall(&s, &ctx, 0); - printf("DIRECT on_tile hp=%d->%d damage=%d pending=%d\n", - hp_before, - s.player.current_hitpoints, - hp_before - s.player.current_hitpoints, - jv->skyfall_pending); - probe_check("direct on-tile skyfall applies damage", - hp_before - s.player.current_hitpoints == PROBE_SKYFALL_DAMAGE && - jv->skyfall_pending == 0); - - probe_init_empty_state(&s, &ctx, 0x2003u); - jv = probe_spawn_javelin(&s); - jv->skyfall_pending = 1; - jv->skyfall_timer = 1; - jv->skyfall_damage = PROBE_SKYFALL_DAMAGE; - jv->skyfall_tile_x = s.player.x; - jv->skyfall_tile_y = s.player.y; - s.player.x += 1; - col_rebuild_player_collision_flags(&s); - hp_before = s.player.current_hitpoints; - col_npc_resolve_javelin_skyfall(&s, &ctx, 0); - printf("DIRECT off_tile player=(%d,%d) marked=(%d,%d) hp=%d->%d damage=%d pending=%d\n", - s.player.x, - s.player.y, - jv->skyfall_tile_x, - jv->skyfall_tile_y, - hp_before, - s.player.current_hitpoints, - hp_before - s.player.current_hitpoints, - jv->skyfall_pending); - probe_check("direct off-tile skyfall applies zero damage", - hp_before - s.player.current_hitpoints == 0 && - jv->skyfall_pending == 0); -} - -static void probe_full_step_dodgeability(void) { - printf("\n== FULL-STEP DODGEABILITY ==\n"); - ProbeStepDodgeResult wait0 = probe_run_no_lock_wait_case(0); - ProbeStepDodgeResult wait1 = probe_run_no_lock_wait_case(1); - ProbeStepDodgeResult wait2 = probe_run_no_lock_wait_case(2); - probe_print_step_result("FULL_STEP wait0_move_from_timer3", wait0); - probe_print_step_result("FULL_STEP wait1_move_from_timer2", wait1); - probe_print_step_result("FULL_STEP wait2_move_from_timer1", wait2); - probe_check("timer 3 visible action dodges skyfall", wait0.damage_taken == 0); - probe_check("timer 2 visible action dodges skyfall", wait1.damage_taken == 0); - probe_check("timer 1 visible action is too late", wait2.damage_taken == PROBE_SKYFALL_DAMAGE); - printf("ACTIONABLE_LEAD move_actions=2 visible_timers=3,2 too_late_timer=1\n"); -} - -static void probe_attack_lock_interaction(void) { - printf("\n== ATTACK-LOCK INTERACTION ==\n"); - ProbeStepDodgeResult idle = probe_run_attack_lock_idle_case("locked_idle"); - ProbeStepDodgeResult move_only = probe_run_attack_lock_case( - "locked_move_only", - PROBE_TARGET_AFTER_MOVE_NONE); - ProbeStepDodgeResult target_move = probe_run_attack_lock_case( - "locked_target_and_move_same_tick", - PROBE_TARGET_AFTER_MOVE_SAME_TICK); - ProbeStepDodgeResult move_then_target = probe_run_attack_lock_case( - "locked_move_then_retarget_next_tick", - PROBE_TARGET_AFTER_MOVE_NEXT_TICK); - probe_print_step_result("LOCK idle", idle); - probe_print_step_result("LOCK move_only", move_only); - probe_print_step_result("LOCK target_and_move_same_tick", target_move); - probe_print_step_result("LOCK move_then_retarget_next_tick", move_then_target); - probe_check("locked idle eats skyfall", idle.damage_taken == PROBE_SKYFALL_DAMAGE); - probe_check("MOVE-only while locked breaks lock and dodges", move_only.damage_taken == 0); - probe_check("TARGET+MOVE same tick stays on tile and eats skyfall", - target_move.damage_taken == PROBE_SKYFALL_DAMAGE); - probe_check("retargeting one tick after a successful dodge stays safe", - move_then_target.damage_taken == 0); -} - -int main(void) { - printf("=== Colosseum javelin skyfall dodge probe ===\n"); - printf("step order under test: action pretick, NPC skyfall resolve, NPC attack fire, player movement, obs write\n"); - probe_obs_honesty(); - probe_direct_resolve(); - probe_full_step_dodgeability(); - probe_attack_lock_interaction(); - printf("\nVERDICT_DATA obs_honest=1 off_tile_dodges=1 real_step_actionable_lead=2 move_only_lock_dodges=1 target_plus_move_blocks_dodge=1\n"); - return 0; -} diff --git a/ocean/osrs/tests/probe_colo_wave_advance.c b/ocean/osrs/tests/probe_colo_wave_advance.c deleted file mode 100644 index 7affc2d73e..0000000000 --- a/ocean/osrs/tests/probe_colo_wave_advance.c +++ /dev/null @@ -1,220 +0,0 @@ -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -static int scripted_overhead(const ColosseumState* s) { - int best_style = ATTACK_STYLE_NONE, best_ticks = 1 << 30, best_dmg = -1; - const EncounterPendingHitQueue* q = &s->player_pending_hits; - for (int i = 0; i < q->count; i++) { - const EncounterPendingHit* h = &q->hits[i]; - if (!h->active || !h->check_prayer) continue; - if (h->ticks_remaining < best_ticks || - (h->ticks_remaining == best_ticks && h->damage > best_dmg)) { - best_ticks = h->ticks_remaining; best_dmg = h->damage; best_style = h->attack_style; - } - } - for (int n = 0; n < COLO_MAX_NPCS; n++) { - const ColoNPC* npc = &s->npcs[n]; - if (npc->type != COLO_MANTICORE || !col_npc_is_live_enemy(npc)) continue; - const ColoManticoreState* mc = &npc->type_state.manticore; - if (mc->cycle_step < 0) continue; - int orb = mc->cycle_step; - if (orb >= 3) continue; - int ticks = (mc->cycle_step == 0) ? npc->attack_timer : 0; - int dmg = 36; - if (ticks < best_ticks || (ticks == best_ticks && dmg > best_dmg)) { - best_ticks = ticks; best_dmg = dmg; best_style = mc->orb_style[orb]; - } - } - switch (best_style) { - case ATTACK_STYLE_MAGIC: return COLO_OVERHEAD_MAGIC; - case ATTACK_STYLE_RANGED: return COLO_OVERHEAD_RANGED; - case ATTACK_STYLE_MELEE: return COLO_OVERHEAD_MELEE; - default: return COLO_OVERHEAD_NO_CHANGE; - } -} - -static int scripted_target(const ColosseumState* s) { - int best_slot = -1, best_dist = 1 << 30; - for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { - int idx = s->current_obs_slots[slot]; - if (idx < 0 || idx >= COLO_MAX_NPCS) continue; - const ColoNPC* npc = &s->npcs[idx]; - if (!col_npc_is_live_target(npc)) continue; - int d = col_npc_dist_to_player(s, npc); - if (d < best_dist) { best_dist = d; best_slot = slot; } - } - return best_slot < 0 ? 0 : col_primary_attack_action_for_obs_slot(best_slot); -} - -static int scripted_first_modifier(const ColosseumState* s) { - for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) - if (s->modifiers.draft_options[o] >= 0) return o; - return 0; -} - -static int scripted_heal_cell(const ColosseumState* s) { - for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - const ColoInvCell* cell = &s->player.inventory_cells[i]; - OsrsInventoryClickResolution r = - osrs_inventory_cell_click_interpret( - cell, OSRS_CLICK_TICK_FIRST); - if ((r.consumable_kind == OSRS_CONSUMABLE_BREW || - r.consumable_kind == OSRS_CONSUMABLE_GUTHIX_REST || - r.consumable_kind == OSRS_CONSUMABLE_SHARK_FOOD || - r.consumable_kind == OSRS_CONSUMABLE_KARAMBWAN) && - col_inventory_cell_actionable(s, i)) return i; - } - return -1; -} - -static void aggressive_policy(ColosseumState* s, int* actions) { - for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; - if (s->modifiers.draft_pending) { - actions[COLO_HEAD_MODIFIER_SELECT] = scripted_first_modifier(s) + 1; - return; - } - actions[COLO_HEAD_PRAYER] = scripted_overhead(s); - actions[COLO_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; - actions[COLO_HEAD_PRIMARY] = scripted_target(s); - if (s->player.current_hitpoints < 55) { - int heal_cell = scripted_heal_cell(s); - if (heal_cell >= 0) { - OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( - &s->player.inventory_cells[heal_cell], OSRS_CLICK_TICK_FIRST); - int head = r.click_action == OSRS_CLICK_DRINK ? COLO_HEAD_DRINK : COLO_HEAD_EAT; - actions[head] = heal_cell + 1; - } - } -} - -static int count_live_enemies(const ColosseumState* s) { - int n = 0; - for (int i = 0; i < COLO_MAX_NPCS; i++) - if (col_npc_is_live_enemy(&s->npcs[i])) n++; - return n; -} - -static int forcekill_live_enemies(ColosseumState* s) { - int killed = 0; - for (int i = 0; i < COLO_MAX_NPCS; i++) { - ColoNPC* npc = &s->npcs[i]; - if (!col_npc_is_live_enemy(npc)) continue; - npc->hp = 0; - killed++; - } - return killed; -} - -enum { MODE_AGGRESSIVE, MODE_FORCEKILL }; - -static void run_episodes(const char* label, int mode, int n_eps) { - ColosseumContext ctx; - col_init_context_typed(&ctx); - ctx.config.start_wave = 0; - ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY; - ctx.config.beginner_loadout_fraction = 0.0f; - ctx.config.step_out_forecast_obs_enabled = 1; - ctx.config.wave_clear_bonus = 1.0f; - col_finalize_route_topology(&ctx); - - static float obs[COLO_NUM_OBS]; - ColosseumState s; - unsigned int rng = 0x9e3779b9u; - - int advanced_eps = 0; - int max_wave_overall = 0; - int total_clear_bonus_fires = 0; - int total_waves_cleared = 0; - int wins = 0; - long total_spawned_w1 = 0, total_killed_w1 = 0; - int wave_ended_eps = 0; - double dmg_dealt = 0, dmg_recv = 0; - - for (int ep = 0; ep < n_eps; ep++) { - memset(&s, 0, sizeof(s)); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, (uint32_t)(rng | 1u)); - - int actions[COLO_NUM_ACTION_HEADS] = {0}; - long guard = 0; - int wave1_spawn_count = -1; - int max_wave_this_ep = s.wave; - float clear_bonus_seen = 0.0f; - int wave_completed_seen = 0; - int prev_waves_cleared = s.log.waves_cleared; - - while (!s.episode_over && guard++ < COLO_MAX_TICKS + 16) { - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - - if (wave1_spawn_count < 0 && s.wave == 0 && !s.modifiers.draft_pending && - s.wave_spawn_delay == 0 && s.wave_ready_delay == 0) { - int live = count_live_enemies(&s); - if (live > 0) wave1_spawn_count = s.current_wave_total_killable; - } - - if (s.modifiers.draft_pending) { - aggressive_policy(&s, actions); - } else if (mode == MODE_FORCEKILL) { - - for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; - forcekill_live_enemies(&s); - } else { - aggressive_policy(&s, actions); - } - - float r_before = s.reward; - (void)r_before; - col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); - - if (s.tick_scratch.wave_completed) { - wave_completed_seen++; - - clear_bonus_seen += ctx.config.wave_clear_bonus; - } - if (s.wave > max_wave_this_ep) max_wave_this_ep = s.wave; - } - - int waves_cleared = s.log.waves_cleared - prev_waves_cleared; - total_waves_cleared += waves_cleared; - total_clear_bonus_fires += wave_completed_seen; - if (max_wave_this_ep >= 1) advanced_eps++; - if (max_wave_this_ep > max_wave_overall) max_wave_overall = max_wave_this_ep; - if (wave_completed_seen > 0) wave_ended_eps++; - if (s.winner == COLO_OUTCOME_PLAYER_WON) wins++; - if (wave1_spawn_count > 0) { total_spawned_w1 += wave1_spawn_count; } - total_killed_w1 += s.log.total_npc_kills; - dmg_dealt += s.log.total_damage_dealt; - dmg_recv += s.log.total_damage_received; - - rng = rng * 1103515245u + 12345u; - } - - printf("%-14s eps=%d\n", label, n_eps); - printf(" wave1 spawns (killable)/ep = %.2f kills/ep = %.2f\n", - (double)total_spawned_w1 / n_eps, (double)total_killed_w1 / n_eps); - printf(" episodes that reached wave>=1 (0->1 advance) = %d / %d\n", - advanced_eps, n_eps); - printf(" episodes where the wave ever ENDED (wave_completed fired) = %d / %d\n", - wave_ended_eps, n_eps); - printf(" total wave_completed fires = %d total log.waves_cleared = %d\n", - total_clear_bonus_fires, total_waves_cleared); - printf(" clear-bonus fires (== wave_completed) = %d wins = %d\n", - total_clear_bonus_fires, wins); - printf(" max wave reached overall = %d\n", max_wave_overall); - printf(" dmg_dealt/ep = %.0f dmg_recv/ep = %.0f\n", - dmg_dealt / n_eps, dmg_recv / n_eps); - printf("\n"); -} - -int main(void) { - printf("=== Colosseum wave-1 -> wave-2 advance probe (CPU, speedrun loadout) ===\n"); - printf("wave index 0 == \"wave 1\"; advance means s->wave reaches 1.\n"); - printf("COLO_NUM_WAVES=%d COLO_WAVE_BOSS=%d SPEEDRUN_MAX_TICKS=%d REINFORCE_TICKS=%d\n\n", - COLO_NUM_WAVES, COLO_WAVE_BOSS, COLO_SPEEDRUN_MAX_TICKS, COLO_REINFORCEMENT_TICKS); - - run_episodes("AGGRESSIVE", MODE_AGGRESSIVE, 32); - run_episodes("FORCEKILL", MODE_FORCEKILL, 32); - return 0; -} diff --git a/ocean/osrs/tests/probe_colosseum_winnability.c b/ocean/osrs/tests/probe_colosseum_winnability.c deleted file mode 100644 index 5aeb753720..0000000000 --- a/ocean/osrs/tests/probe_colosseum_winnability.c +++ /dev/null @@ -1,187 +0,0 @@ -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -static int scripted_overhead(const ColosseumState* s) { - int best_style = ATTACK_STYLE_NONE, best_ticks = 1 << 30, best_dmg = -1; - const EncounterPendingHitQueue* q = &s->player_pending_hits; - for (int i = 0; i < q->count; i++) { - const EncounterPendingHit* h = &q->hits[i]; - if (!h->active || !h->check_prayer) continue; - if (h->ticks_remaining < best_ticks || - (h->ticks_remaining == best_ticks && h->damage > best_dmg)) { - best_ticks = h->ticks_remaining; best_dmg = h->damage; best_style = h->attack_style; - } - } - - for (int n = 0; n < COLO_MAX_NPCS; n++) { - const ColoNPC* npc = &s->npcs[n]; - if (npc->type != COLO_MANTICORE || !col_npc_is_live_enemy(npc)) continue; - const ColoManticoreState* mc = &npc->type_state.manticore; - if (mc->cycle_step < 0) continue; - int orb = mc->cycle_step; - if (orb >= 3) continue; - int ticks = (mc->cycle_step == 0) ? npc->attack_timer : 0; - int dmg = 36; - if (ticks < best_ticks || (ticks == best_ticks && dmg > best_dmg)) { - best_ticks = ticks; best_dmg = dmg; best_style = mc->orb_style[orb]; - } - } - switch (best_style) { - case ATTACK_STYLE_MAGIC: return COLO_OVERHEAD_MAGIC; - case ATTACK_STYLE_RANGED: return COLO_OVERHEAD_RANGED; - case ATTACK_STYLE_MELEE: return COLO_OVERHEAD_MELEE; - default: return COLO_OVERHEAD_NO_CHANGE; - } -} - -static int forecast_action_danger(const ColoStepOutForecastAction* a, int horizon) { - if (!a->valid) return 1 << 30; - int danger = a->same_tick_mixed_style_conflict ? 100000 : 0; - int worst = 0; - for (int t = 0; t < horizon; t++) - if (a->ticks[t].max_hit > worst) worst = a->ticks[t].max_hit; - return danger + worst; -} - -static int g_move_mode = 0; - -static int scripted_move(ColosseumState* s) { - if (g_move_mode == 1) return 0; - ColoStepOutForecast f; - col_build_step_out_forecast_horizon_mode(s, &f, COLO_STEP_OUT_FORECAST_HORIZON, 0); - int horizon = COLO_STEP_OUT_FORECAST_HORIZON; - const ColoStepOutForecastAction* idle = &f.actions[0]; - if (idle->valid && !idle->same_tick_mixed_style_conflict) return 0; - int best = 0, best_danger = forecast_action_danger(idle, horizon); - for (int a = 1; a < ENCOUNTER_MOVE_ACTIONS; a++) { - int d = forecast_action_danger(&f.actions[a], horizon); - if (d < best_danger) { best_danger = d; best = a; } - } - return best; -} - -static int scripted_target(const ColosseumState* s) { - int best_slot = -1, best_dist = 1 << 30; - for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { - int idx = s->current_obs_slots[slot]; - if (idx < 0 || idx >= COLO_MAX_NPCS) continue; - const ColoNPC* npc = &s->npcs[idx]; - if (!col_npc_is_live_target(npc)) continue; - int d = col_npc_dist_to_player(s, npc); - if (d < best_dist) { best_dist = d; best_slot = slot; } - } - return best_slot < 0 ? 0 : col_primary_attack_action_for_obs_slot(best_slot); -} - -static int scripted_least_bad_modifier(const ColosseumState* s) { - for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) - if (s->modifiers.draft_options[o] >= 0) return o; - return 0; -} - -static int scripted_heal_cell(const ColosseumState* s) { - for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { - const ColoInvCell* cell = &s->player.inventory_cells[i]; - OsrsInventoryClickResolution r = - osrs_inventory_cell_click_interpret( - cell, OSRS_CLICK_TICK_FIRST); - if ((r.consumable_kind == OSRS_CONSUMABLE_BREW || - r.consumable_kind == OSRS_CONSUMABLE_GUTHIX_REST || - r.consumable_kind == OSRS_CONSUMABLE_SHARK_FOOD || - r.consumable_kind == OSRS_CONSUMABLE_KARAMBWAN) && - col_inventory_cell_actionable(s, i)) return i; - } - return -1; -} - -static void scripted_policy(ColosseumState* s, int* actions) { - for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; - if (s->modifiers.draft_pending) { - actions[COLO_HEAD_MODIFIER_SELECT] = scripted_least_bad_modifier(s) + 1; - return; - } - actions[COLO_HEAD_PRAYER] = scripted_overhead(s); - actions[COLO_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; - int move = scripted_move(s); - actions[COLO_HEAD_PRIMARY] = move > 0 ? move : scripted_target(s); - if (s->player.current_hitpoints < 55) { - int heal_cell = scripted_heal_cell(s); - if (heal_cell >= 0) { - OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( - &s->player.inventory_cells[heal_cell], OSRS_CLICK_TICK_FIRST); - int head = r.click_action == OSRS_CLICK_DRINK ? COLO_HEAD_DRINK : COLO_HEAD_EAT; - actions[head] = heal_cell + 1; - } - } -} - -typedef int (*PolicyFn)(ColosseumState*, int*, unsigned int*); - -static void run_episodes(const char* label, int scripted, int start_wave, int n_eps) { - ColosseumContext ctx; - col_init_context_typed(&ctx); - ctx.config.start_wave = start_wave; - ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; - ctx.config.beginner_loadout_fraction = 0.5f; - ctx.config.step_out_forecast_obs_enabled = 1; - col_finalize_route_topology(&ctx); - - static float obs[COLO_NUM_OBS]; - ColosseumState s; - int wins = 0, wave_sum = 0, wave_max = 0; - int hist[16] = {0}; - double pray_correct = 0, npc_attacks = 0, dmg_recv = 0, dmg_dealt = 0; - unsigned int rng = 0x9e3779b9u ^ (unsigned)(start_wave * 2654435761u); - - for (int ep = 0; ep < n_eps; ep++) { - memset(&s, 0, sizeof(s)); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, (uint32_t)(rng | 1u)); - int actions[COLO_NUM_ACTION_HEADS] = {0}; - long guard = 0; - while (!s.episode_over && guard++ < COLO_MAX_TICKS + 16) { - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - if (scripted) { - scripted_policy(&s, actions); - } else { - for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) { - rng = rng * 1103515245u + 12345u; - actions[h] = (int)((rng >> 16) % (unsigned)COLO_ACTION_DIMS[h]); - } - } - col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); - } - int wave = s.wave; - if (s.winner == COLO_OUTCOME_PLAYER_WON) wins++; - wave_sum += wave; - if (wave > wave_max) wave_max = wave; - if (wave < 16) hist[wave]++; - pray_correct += s.log.total_prayer_correct; - npc_attacks += s.log.total_npc_attacks; - dmg_recv += s.log.total_damage_received; - dmg_dealt += s.log.total_damage_dealt; - rng = rng * 1103515245u + 12345u; - } - double pray_rate = npc_attacks > 0 ? pray_correct / npc_attacks : 0.0; - printf("%-22s start_wave=%d eps=%d mean_wave=%.2f max_wave=%d wins=%d pray_rate=%.2f dmg_recv/ep=%.0f dmg_dealt/ep=%.0f\n", - label, start_wave, n_eps, (double)wave_sum / n_eps, wave_max, wins, - pray_rate, dmg_recv / n_eps, dmg_dealt / n_eps); - printf(" wave histogram (0-based): "); - for (int w = 0; w < 13; w++) if (hist[w]) printf("w%d:%d ", w, hist[w]); - printf("\n"); -} - -int main(void) { - printf("=== Colosseum winnability probe (scripted vs random, CPU) ===\n"); - printf("(RL baseline for reference: mean_wave ~4.5 0-based, prayer ~0.62)\n"); - run_episodes("RANDOM (sanity)", 0, 0, 64); - g_move_mode = 1; - run_episodes("SCRIPTED-stand", 1, 0, 64); - run_episodes("SCRIPTED-stand@w4", 1, 3, 64); - g_move_mode = 0; - run_episodes("SCRIPTED-deconflict", 1, 0, 64); - run_episodes("SCRIPTED-deconflict@w4", 1, 3, 64); - return 0; -} diff --git a/ocean/osrs/tests/test_colo_prayer_wiring.c b/ocean/osrs/tests/test_colo_prayer_wiring.c deleted file mode 100644 index e2e5e9bbf9..0000000000 --- a/ocean/osrs/tests/test_colo_prayer_wiring.c +++ /dev/null @@ -1,125 +0,0 @@ -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -#define col_init_context_typed(ctx_ptr) do { \ - col_init_context_typed(ctx_ptr); \ - col_finalize_route_topology((ctx_ptr)); \ - (ctx_ptr)->config.late_start_state_mode = 0; \ -} while (0) - -typedef enum { POLICY_HOLD_MELEE, POLICY_OBS_TELEGRAPH } PrayerPolicy; - -static int telegraph_overhead( - const ColosseumState* s, - const ColosseumContext* ctx -) { - AttackStyle best_style = ATTACK_STYLE_NONE; - int best_ticks = 1 << 30; - for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { - int idx = s->current_obs_slots[slot]; - if (idx < 0 || idx >= COLO_MAX_NPCS) continue; - const ColoNPC* npc = &s->npcs[idx]; - if (!col_npc_is_live_enemy(npc)) continue; - ColoNpcNextPrayerObs t = - col_npc_next_prayer_obs(s, ctx, npc, idx); - if (!t.active) continue; - if (t.ticks < best_ticks) { best_ticks = t.ticks; best_style = t.style; } - } - switch (best_style) { - case ATTACK_STYLE_MAGIC: return COLO_OVERHEAD_MAGIC; - case ATTACK_STYLE_RANGED: return COLO_OVERHEAD_RANGED; - default: return COLO_OVERHEAD_MELEE; - } -} - -static int nearest_target(const ColosseumState* s) { - int best_slot = -1, best_dist = 1 << 30; - for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { - int idx = s->current_obs_slots[slot]; - if (idx < 0 || idx >= COLO_MAX_NPCS) continue; - const ColoNPC* npc = &s->npcs[idx]; - if (!col_npc_is_live_target(npc)) continue; - int d = col_npc_dist_to_player(s, npc); - if (d < best_dist) { best_dist = d; best_slot = slot; } - } - return best_slot < 0 ? 0 : col_primary_attack_action_for_obs_slot(best_slot); -} - -static void run_test(const char* label, PrayerPolicy policy, int start_wave, int n_eps) { - ColosseumContext ctx; - col_init_context_typed(&ctx); - ctx.config.start_wave = start_wave; - ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; - ctx.config.beginner_loadout_fraction = 0.5f; - - static float obs[COLO_NUM_OBS]; - ColosseumState s; - int wave_sum = 0; - double pray_correct = 0, npc_attacks = 0, dmg_recv = 0; - long tele_total = 0, tele_match = 0; - AttackStyle predicted[COLO_MAX_NPCS]; - unsigned int rng = 0x51ed270bu ^ (unsigned)(start_wave * 2654435761u + policy); - - for (int ep = 0; ep < n_eps; ep++) { - memset(&s, 0, sizeof(s)); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, (uint32_t)(rng | 1u)); - int actions[COLO_NUM_ACTION_HEADS] = {0}; - long guard = 0; - while (!s.episode_over && guard++ < COLO_MAX_TICKS + 16) { - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; - if (s.modifiers.draft_pending) { - for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) - if (s.modifiers.draft_options[o] >= 0) { actions[COLO_HEAD_MODIFIER_SELECT] = o + 1; break; } - col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); - continue; - } - actions[COLO_HEAD_PRAYER] = (policy == POLICY_HOLD_MELEE) - ? COLO_OVERHEAD_MELEE : telegraph_overhead(&s, &ctx); - actions[COLO_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; - actions[COLO_HEAD_PRIMARY] = nearest_target(&s); - - for (int n = 0; n < COLO_MAX_NPCS; n++) predicted[n] = ATTACK_STYLE_NONE; - for (int n = 0; n < COLO_MAX_NPCS; n++) { - const ColoNPC* npc = &s.npcs[n]; - if (!col_npc_is_live_enemy(npc)) continue; - ColoNpcNextPrayerObs t = - col_npc_next_prayer_obs(&s, &ctx, npc, n); - if (t.active && t.ticks <= 1) predicted[n] = t.style; - } - - col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); - - for (int n = 0; n < COLO_MAX_NPCS; n++) { - const ColoNPC* npc = &s.npcs[n]; - if (!npc->attacked_this_tick) continue; - AttackStyle actual = (AttackStyle)npc->attack_style_this_tick; - if (actual != ATTACK_STYLE_MELEE && actual != ATTACK_STYLE_RANGED && - actual != ATTACK_STYLE_MAGIC) continue; - tele_total++; - if (predicted[n] == actual) tele_match++; - } - } - wave_sum += s.wave; - pray_correct += s.log.total_prayer_correct; - npc_attacks += s.log.total_npc_attacks; - dmg_recv += s.log.total_damage_received; - rng = rng * 1103515245u + 12345u; - } - double pray_rate = npc_attacks > 0 ? pray_correct / npc_attacks : 0.0; - double tele_acc = tele_total > 0 ? (double)tele_match / (double)tele_total : 0.0; - printf("%-26s wave=%d eps=%d mean_wave=%.2f pray_correct=%.2f offpray_dmg/ep=%.0f telegraph_acc=%.2f (%ld/%ld)\n", - label, start_wave, n_eps, (double)wave_sum / n_eps, pray_rate, - dmg_recv / n_eps, tele_acc, tele_match, tele_total); -} - -int main(void) { - printf("=== Colosseum prayer-wiring probe (stand + attack, prayer policy varies) ===\n"); - run_test("HOLD_MELEE @w1", POLICY_HOLD_MELEE, 0, 128); - run_test("OBS_TELEGRAPH@w1", POLICY_OBS_TELEGRAPH, 0, 128); - run_test("HOLD_MELEE @w4", POLICY_HOLD_MELEE, 3, 128); - run_test("OBS_TELEGRAPH@w4", POLICY_OBS_TELEGRAPH, 3, 128); - return 0; -} diff --git a/ocean/osrs/tests/test_colosseum_forecast_exact.c b/ocean/osrs/tests/test_colosseum_forecast_exact.c deleted file mode 100644 index 5e1b9b2c69..0000000000 --- a/ocean/osrs/tests/test_colosseum_forecast_exact.c +++ /dev/null @@ -1,1233 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" -#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES -#include "ocean/osrs/tests/osrs_route_reference.h" -#endif - -static void col_step_out_forecast_landing_selftest_one_state( - ColosseumState* s, - ColosseumContext* ctx -) { - for (int x = COLO_ARENA_MIN_X - 2; x <= COLO_ARENA_MAX_X + 2; x++) { - for (int y = COLO_ARENA_MIN_Y - 2; y <= COLO_ARENA_MAX_Y + 2; y++) { - s->player.x = x; - s->player.y = y; - for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { - int valid = - col_step_out_forecast_action_valid(s, ctx, action_idx); - Player moved = s->player; - ColoGeometryContext geometry = { - .state = s, - .context = ctx, - }; - if (action_idx != 0) { - encounter_move_to_target( - &moved, - ENCOUNTER_MOVE_TARGET_DX[action_idx], - ENCOUNTER_MOVE_TARGET_DY[action_idx], - col_player_walkable, - &geometry); - } - ColoForecastLanding landing = - col_step_out_forecast_action_landing_ctx( - s, ctx, action_idx); - if (landing.valid != valid || - landing.land_x != moved.x || - landing.land_y != moved.y) { - fprintf(stderr, - "colosseum landing mismatch player=(%d,%d) action=%d: helper=(%d,%d,%d) full=(%d,%d,%d)\n", - x, y, action_idx, - landing.valid, landing.land_x, landing.land_y, - valid, moved.x, moved.y); - abort(); - } - } - } - } -} - -static void col_step_out_forecast_landing_selftest(void) { - ColosseumContext ctx; - ColosseumState s; - col_init_context_typed(&ctx); - memset(&s, 0, sizeof(s)); - col_finalize_route_topology(&ctx); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 0x51A7u); - memset(s.npcs, 0, sizeof(s.npcs)); - memset(s.npc_collision_flags, 0, sizeof(s.npc_collision_flags)); - col_rebuild_player_collision_flags(&s); - col_step_out_forecast_landing_selftest_one_state(&s, &ctx); - - col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 12, 9); - col_spawn_npc_at(&s, COLO_JAVELIN_COLOSSUS, 18, 18); - col_rebuild_player_collision_flags(&s); - col_step_out_forecast_landing_selftest_one_state(&s, &ctx); - - s.wave = COLO_WAVE_BOSS; - s.sol.started = 1; - s.sol.boss_arena_min_x = COLO_BOSS_ARENA_MIN_X; - s.sol.boss_arena_min_y = COLO_BOSS_ARENA_MIN_Y; - s.sol.boss_arena_max_x = COLO_BOSS_ARENA_MAX_X; - s.sol.boss_arena_max_y = COLO_BOSS_ARENA_MAX_Y; - col_step_out_forecast_landing_selftest_one_state(&s, &ctx); - - printf("colosseum landing helper selftest PASS: %d actions across 3 states\n", - ENCOUNTER_MOVE_ACTIONS); -} - -static int exact_static_tile_blocked_reference(int x, int y) { - if (x < COLO_ARENA_MIN_X || x > COLO_ARENA_MAX_X || - y < COLO_ARENA_MIN_Y || y > COLO_ARENA_MAX_Y) - return 1; - for (int span = 0; span < COLO_WALL_SPANS_PER_ROW; span++) { - ColoWallSpan wall = COLO_WALL_SPANS[y][span]; - if (x >= wall.lo && x < wall.hi) return 1; - } - for (int pillar = 0; pillar < COLO_NUM_PILLARS; pillar++) { - int pillar_x = COLO_PILLARS[pillar][0]; - int pillar_y = COLO_PILLARS[pillar][1]; - if (x >= pillar_x && x < pillar_x + COLO_PILLAR_SIZE && - y >= pillar_y && y < pillar_y + COLO_PILLAR_SIZE) - return 1; - } - return 0; -} - -static int exact_static_los_blocked(void* data, int x, int y) { - (void)data; - return exact_static_tile_blocked_reference(x, y); -} - -static int exact_static_footprint_blocked_reference(int x, int y, int size) { - for (int dx = 0; dx < size; dx++) - for (int dy = 0; dy < size; dy++) - if (exact_static_tile_blocked_reference(x + dx, y + dy)) - return 1; - return 0; -} - -static void col_static_los_table_selftest(void) { - ColosseumContext ctx; - col_init_context_typed(&ctx); - col_finalize_route_topology(&ctx); - OsrsLosQuery reference = - osrs_los_tile(exact_static_los_blocked, NULL); - int checks = 0; - for (int x0 = COLO_ARENA_MIN_X; x0 <= COLO_ARENA_MAX_X; x0++) { - for (int y0 = COLO_ARENA_MIN_Y; y0 <= COLO_ARENA_MAX_Y; y0++) { - for (int x1 = COLO_ARENA_MIN_X; x1 <= COLO_ARENA_MAX_X; x1++) { - for (int y1 = COLO_ARENA_MIN_Y; y1 <= COLO_ARENA_MAX_Y; y1++) { - if (x0 == x1 && y0 == y1) continue; - int topology = col_topology_los_clear( - &ctx, x0, y0, 1, x1, y1, 1, 0); - int slow = - osrs_los_tile_ray_clear(&reference, x0, y0, x1, y1); - if (topology != slow) { - fprintf(stderr, - "colosseum topology LoS mismatch (%d,%d)->(%d,%d): topology=%d reference=%d\n", - x0, y0, x1, y1, topology, slow); - abort(); - } - checks++; - } - } - } - } - printf("colosseum topology LoS parity PASS: %d directed pairs\n", checks); -} -static void col_static_los_endpoint_semantics_selftest(void) { - ColosseumContext ctx; - col_init_context_typed(&ctx); - col_finalize_route_topology(&ctx); - OsrsLosQuery reference = - osrs_los_tile(exact_static_los_blocked, NULL); - uint64_t raw_clear[2][2] = {{0}}; - uint64_t ranged_clear[2][2] = {{0}}; - uint64_t total[2][2] = {{0}}; - - for (int source_x = COLO_ARENA_MIN_X; - source_x <= COLO_ARENA_MAX_X; - source_x++) { - for (int source_y = COLO_ARENA_MIN_Y; - source_y <= COLO_ARENA_MAX_Y; - source_y++) { - int source_blocked = - exact_static_tile_blocked_reference(source_x, source_y); - for (int target_x = COLO_ARENA_MIN_X; - target_x <= COLO_ARENA_MAX_X; - target_x++) { - for (int target_y = COLO_ARENA_MIN_Y; - target_y <= COLO_ARENA_MAX_Y; - target_y++) { - int target_blocked = - exact_static_tile_blocked_reference( - target_x, target_y); - int raw = osrs_los_tile_ray_clear( - &reference, - source_x, - source_y, - target_x, - target_y); - int topology_raw = col_topology_los_clear( - &ctx, - source_x, - source_y, - 1, - target_x, - target_y, - 1, - 0); - if (raw != topology_raw) { - fprintf(stderr, - "colosseum raw endpoint LoS mismatch (%d,%d)->(%d,%d): topology=%d reference=%d\n", - source_x, - source_y, - target_x, - target_y, - topology_raw, - raw); - abort(); - } - - int range_one = osrs_los_clear( - &reference, - source_x, - source_y, - 1, - target_x, - target_y, - 1, - 1); - if (!range_one) { - fprintf(stderr, - "colosseum tile LoS range-one law changed (%d,%d)->(%d,%d)\n", - source_x, - source_y, - target_x, - target_y); - abort(); - } - - int ranged = osrs_los_clear( - &reference, - source_x, - source_y, - 1, - target_x, - target_y, - 1, - COLO_ARENA_WIDTH); - int topology_ranged = col_topology_los_clear( - &ctx, - target_x, - target_y, - 1, - source_x, - source_y, - 1, - 0); - if (ranged != topology_ranged) { - fprintf(stderr, - "colosseum ranged endpoint LoS mismatch actor=(%d,%d) target=(%d,%d): topology=%d reference=%d\n", - source_x, - source_y, - target_x, - target_y, - topology_ranged, - ranged); - abort(); - } - - total[source_blocked][target_blocked]++; - raw_clear[source_blocked][target_blocked] += - (uint64_t)raw; - ranged_clear[source_blocked][target_blocked] += - (uint64_t)ranged; - } - } - } - } - - printf( - "colosseum endpoint LoS parity PASS: raw clear/total OO=%llu/%llu OB=%llu/%llu BO=%llu/%llu BB=%llu/%llu; ranged OO=%llu OB=%llu BO=%llu BB=%llu; range-one=all-clear\n", - (unsigned long long)raw_clear[0][0], - (unsigned long long)total[0][0], - (unsigned long long)raw_clear[0][1], - (unsigned long long)total[0][1], - (unsigned long long)raw_clear[1][0], - (unsigned long long)total[1][0], - (unsigned long long)raw_clear[1][1], - (unsigned long long)total[1][1], - (unsigned long long)ranged_clear[0][0], - (unsigned long long)ranged_clear[0][1], - (unsigned long long)ranged_clear[1][0], - (unsigned long long)ranged_clear[1][1]); -} - - -static void col_static_footprint_table_selftest(void) { - ColosseumContext ctx; - col_init_context_typed(&ctx); - col_finalize_route_topology(&ctx); - col_build_npc_stats(); - int sizes[ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE + 1] = {0}; - for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) - sizes[COLO_NPC_STATS[type].size] = 1; - int checks = 0; - for (int size = 1; size <= ENCOUNTER_ARENA_TOPOLOGY_MAX_FOOTPRINT_SIZE; size++) { - if (!sizes[size]) continue; - for (int x = COLO_ARENA_MIN_X - size; x <= COLO_ARENA_MAX_X + 1; x++) { - for (int y = COLO_ARENA_MIN_Y - size; y <= COLO_ARENA_MAX_Y + 1; y++) { - int topology = - col_topology_footprint_blocked(&ctx, x, y, size); - int slow = - exact_static_footprint_blocked_reference(x, y, size); - if (topology != slow) { - fprintf(stderr, - "colosseum topology footprint mismatch (%d,%d) size=%d: topology=%d reference=%d\n", - x, y, size, topology, slow); - abort(); - } - checks++; - } - } - } - printf("colosseum topology footprint parity PASS: %d NPC-size checks\n", - checks); -} - -#define col_init_context_typed(ctx_ptr) do { \ - col_init_context_typed(ctx_ptr); \ - (ctx_ptr)->config.late_start_state_mode = 0; \ -} while (0) - -#define EXACT_MAGIC "COLOEXACTv1" -#define EXACT_VERSION 5u -#define EXACT_CHUNK_BYTES 65536 - -typedef struct { - char magic[16]; - uint32_t version; - uint32_t state_size; - uint32_t obs_size; - uint32_t action_mask_size; - uint32_t record_count; -} ColoExactFileHeader; - -typedef struct { - uint32_t scenario_id; - uint32_t step_index; - uint32_t tick; - uint32_t wave; - uint32_t terminal; - uint32_t winner; - uint32_t state_size; - uint32_t obs_size; - uint32_t action_mask_size; - uint64_t state_hash; - uint64_t obs_hash; - uint64_t action_mask_hash; - float reward; -} ColoExactRecordHeader; - -typedef struct { - FILE* file; - uint32_t record_count; -} ColoExactWriter; - -static uint64_t exact_fnv_bytes(uint64_t h, const void* data, size_t size) { - const uint8_t* bytes = (const uint8_t*)data; - for (size_t i = 0; i < size; i++) { - h ^= bytes[i]; - h *= 1099511628211ULL; - } - return h; -} - -static uint64_t exact_hash_bytes(const void* data, size_t size) { - return exact_fnv_bytes(1469598103934665603ULL, data, size); -} - -static void exact_write_all(FILE* file, const void* data, size_t size) { - if (fwrite(data, 1, size, file) != size) { - perror("write colosseum exact fixture"); - abort(); - } -} - -static void exact_readable_path( - char* out, - size_t out_size, - const char* dir, - const char* file_name -) { - int n = snprintf(out, out_size, "%s/%s", dir, file_name); - if (n < 0 || (size_t)n >= out_size) { - fprintf(stderr, "fixture path too long: %s/%s\n", dir, file_name); - abort(); - } -} - -static void exact_mkdir_if_needed(const char* dir) { - if (mkdir(dir, 0777) == 0) return; - if (errno == EEXIST) return; - perror("mkdir colosseum exact fixture dir"); - abort(); -} - -static void exact_writer_open(ColoExactWriter* writer, const char* path) { - memset(writer, 0, sizeof(*writer)); - writer->file = fopen(path, "wb"); - if (!writer->file) { - perror("open colosseum exact fixture"); - abort(); - } - - ColoExactFileHeader header = {0}; - memcpy(header.magic, EXACT_MAGIC, sizeof(EXACT_MAGIC)); - header.version = EXACT_VERSION; - header.state_size = (uint32_t)sizeof(ColosseumState); - header.obs_size = COLO_NUM_OBS; - header.action_mask_size = COLO_ACTION_MASK_SIZE; - exact_write_all(writer->file, &header, sizeof(header)); -} - -static void exact_writer_close(ColoExactWriter* writer) { - ColoExactFileHeader header = {0}; - memcpy(header.magic, EXACT_MAGIC, sizeof(EXACT_MAGIC)); - header.version = EXACT_VERSION; - header.state_size = (uint32_t)sizeof(ColosseumState); - header.obs_size = COLO_NUM_OBS; - header.action_mask_size = COLO_ACTION_MASK_SIZE; - header.record_count = writer->record_count; - if (fseek(writer->file, 0, SEEK_SET) != 0) { - perror("seek colosseum exact fixture"); - abort(); - } - exact_write_all(writer->file, &header, sizeof(header)); - if (fclose(writer->file) != 0) { - perror("close colosseum exact fixture"); - abort(); - } - writer->file = NULL; -} - -static void exact_zero_serialized_route_storage(ColosseumState* state) { -#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES - osrs_interaction_zero_serialized_route_padding(&state->player.interaction); - osrs_interaction_zero_serialized_route_padding(&state->interaction); -#else - memset(&state->player.interaction.route, 0, sizeof(state->player.interaction.route)); - memset(&state->interaction.route, 0, sizeof(state->interaction.route)); -#endif - state->log.npc_blocked_calls = 0.0f; - state->log.npc_blocked_tiles = 0.0f; -} - -static void exact_capture( - ColoExactWriter* writer, - uint32_t scenario_id, - uint32_t step_index, - ColosseumState* s, - ColosseumContext* ctx -) { - float obs[COLO_NUM_OBS]; - float action_mask[COLO_ACTION_MASK_SIZE]; - - col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); - col_write_mask_ctx((EncounterState*)s, (EncounterContext*)ctx, action_mask); - ColosseumState canonical_state = *s; - exact_zero_serialized_route_storage(&canonical_state); - - ColoExactRecordHeader record = {0}; - record.scenario_id = scenario_id; - record.step_index = step_index; - record.tick = (uint32_t)s->tick; - record.wave = (uint32_t)s->wave; - record.terminal = (uint32_t)col_is_terminal_ctx( - (EncounterState*)s, (EncounterContext*)ctx); - record.winner = (uint32_t)s->winner; - record.state_size = (uint32_t)sizeof(*s); - record.obs_size = COLO_NUM_OBS; - record.action_mask_size = COLO_ACTION_MASK_SIZE; - record.state_hash = exact_hash_bytes(&canonical_state, sizeof(canonical_state)); - record.obs_hash = exact_hash_bytes(obs, sizeof(obs)); - record.action_mask_hash = exact_hash_bytes(action_mask, sizeof(action_mask)); - record.reward = col_get_reward_ctx((EncounterState*)s, (EncounterContext*)ctx); - - exact_write_all(writer->file, &record, sizeof(record)); - exact_write_all(writer->file, obs, sizeof(obs)); - exact_write_all(writer->file, action_mask, sizeof(action_mask)); - exact_write_all(writer->file, &canonical_state, sizeof(canonical_state)); - writer->record_count++; -} - -static uint64_t exact_splitmix64(uint64_t* state) { - uint64_t z = (*state += 0x9E3779B97F4A7C15ULL); - z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; - z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; - return z ^ (z >> 31); -} - -static void exact_zero_actions(int actions[COLO_NUM_ACTION_HEADS]) { - memset(actions, 0, sizeof(int) * COLO_NUM_ACTION_HEADS); -} - -static void exact_trace_actions( - ColosseumState* s, - uint64_t* rng, - int actions[COLO_NUM_ACTION_HEADS] -) { - for (int head = 0; head < COLO_NUM_ACTION_HEADS; head++) { - actions[head] = (int)(exact_splitmix64(rng) % (uint64_t)COLO_ACTION_DIMS[head]); - } - if (s->modifiers.draft_pending) { - actions[COLO_HEAD_PRIMARY] = 0; - actions[COLO_HEAD_MODIFIER_SELECT] = 1 + - (int)(exact_splitmix64(rng) % COLO_MODIFIER_DRAFT_OPTIONS); - } -} - -static void exact_init_state( - ColosseumState* s, - ColosseumContext* ctx, - int start_wave, - uint32_t seed -) { - col_init_context_typed(ctx); - ctx->config.start_wave = start_wave; - memset(s, 0, sizeof(*s)); - col_finalize_route_topology(ctx); - col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); -} - -static void exact_refresh_geometry(ColosseumState* s, ColosseumContext* ctx) { - col_rebuild_player_collision_flags(s); - col_refresh_current_obs_slots_ctx(s, ctx); -} - -static void exact_clear_npcs(ColosseumState* s, ColosseumContext* ctx) { - memset(s->npcs, 0, sizeof(s->npcs)); - memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); - memset(s->totems, 0, sizeof(s->totems)); - memset(s->bees, 0, sizeof(s->bees)); - exact_refresh_geometry(s, ctx); -} - -static void exact_prepare_custom( - ColosseumState* s, - ColosseumContext* ctx, - uint32_t seed, - int player_x, - int player_y -) { - exact_init_state(s, ctx, 1, seed); - exact_clear_npcs(s, ctx); - s->modifiers.draft_pending = 0; - s->modifiers.draft_gates_spawn = 0; - s->wave_ready_delay = 0; - s->wave_spawn_delay = 0; - s->reinforcement_timer = COLO_REINFORCE_FIRED; - s->warband_cycle_anchor = s->tick; - s->player.x = player_x; - s->player.y = player_y; - exact_refresh_geometry(s, ctx); -} - -#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES -typedef struct { - ColoWalkCtx walk; - int blocker_count; - int blocker_x[16]; - int blocker_y[16]; -} ExactAttackRouteContext; - -static int exact_attack_route_walkable(void* ctx, int x, int y) { - ExactAttackRouteContext* route_ctx = (ExactAttackRouteContext*)ctx; - return col_tile_walkable(&route_ctx->walk, x, y); -} - -static int exact_attack_route_blocked(void* ctx, int abs_x, int abs_y) { - ExactAttackRouteContext* route_ctx = (ExactAttackRouteContext*)ctx; - int x = abs_x - route_ctx->walk.ctx->world_offset_x; - int y = abs_y - route_ctx->walk.ctx->world_offset_y; - for (int i = 0; i < route_ctx->blocker_count; i++) { - if (route_ctx->blocker_x[i] == x && - route_ctx->blocker_y[i] == y) { - return 1; - } - } - return 0; -} - -static int exact_optimized_route_blocked(void* data, int x, int y, int size) { - (void)size; - ExactAttackRouteContext* route_ctx = (ExactAttackRouteContext*)data; - if (!exact_attack_route_walkable(route_ctx, x, y)) return 1; - for (int i = 0; i < route_ctx->blocker_count; i++) { - if (route_ctx->blocker_x[i] == x && - route_ctx->blocker_y[i] == y) - return 1; - } - return 0; -} -static int exact_attack_route_can_attack( - void* data, - int player_x, - int player_y, - int target_x, - int target_y, - int target_size, - int attack_range -) { - ExactAttackRouteContext* route_ctx = (ExactAttackRouteContext*)data; - OsrsLosQuery query = - col_player_los_query(route_ctx->walk.ctx); - return encounter_player_can_attack( - player_x, - player_y, - target_x, - target_y, - target_size, - attack_range, - route_ctx->walk.ctx->collision_map, - route_ctx->walk.ctx->world_offset_x, - route_ctx->walk.ctx->world_offset_y, - &query); -} - -static EncounterAttackRouteLanding exact_attack_route_optimized_landing( - const ColosseumState* s, - const ColosseumContext* ctx, - ExactAttackRouteContext* route_ctx, - int target_x, - int target_y, - int target_size, - int attack_range, - const OsrsLosQuery* los_query -) { - Player player = s->player; - if (encounter_player_can_attack( - player.x, player.y, - target_x, target_y, target_size, attack_range, - ctx->collision_map, - ctx->world_offset_x, - ctx->world_offset_y, - los_query)) - return (EncounterAttackRouteLanding){ - .land_x = player.x, - .land_y = player.y, - }; - EncounterRouteInput input = { - .topology = ctx->route_topology, - .blockers = { - .is_blocked = exact_optimized_route_blocked, - .ctx = route_ctx, - .revision = (uint64_t)route_ctx->blocker_count + 1, - }, - .source_x = player.x, - .source_y = player.y, - .actor_size = 1, - .target_x = target_x, - .target_y = target_y, - .target_size = target_size, - .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, - .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, - .cost_policy = ENCOUNTER_ROUTE_COST_OSRS, - }; - EncounterRouteResult route = encounter_route_solve(&input); - if (route.outcome == ROUTE_REACHED_TARGET || - route.outcome == ROUTE_REACHED_FALLBACK) { - player.x += route.first_dx; - player.y += route.first_dy; - if ((route.run_dx != 0 || route.run_dy != 0) && - !encounter_player_can_attack( - player.x, player.y, - target_x, target_y, target_size, attack_range, - ctx->collision_map, - ctx->world_offset_x, - ctx->world_offset_y, - los_query)) { - player.x += route.run_dx; - player.y += route.run_dy; - } - } - return (EncounterAttackRouteLanding){ - .land_x = player.x, - .land_y = player.y, - }; -} - -static EncounterAttackRouteLanding exact_attack_route_runtime_landing( - const ColosseumState* s, - const ColosseumContext* ctx, - ExactAttackRouteContext* route_ctx, - int target_x, - int target_y, - int target_size, - int attack_range, - const OsrsLosQuery* los_query -) { - Player player = s->player; - encounter_chase_attack_target( - &player, - target_x, - target_y, - target_size, - attack_range, - ctx->collision_map, - ctx->world_offset_x, - ctx->world_offset_y, - exact_attack_route_walkable, - route_ctx, - exact_attack_route_blocked, - route_ctx, - los_query, - COLO_ARENA_MIN_X, - COLO_ARENA_MIN_Y, - COLO_ARENA_WIDTH, - COLO_ARENA_HEIGHT); - return (EncounterAttackRouteLanding){ - .land_x = player.x, - .land_y = player.y, - }; -} - -static int exact_attack_route_property_scenario( - const char* scenario, - ColosseumState* s, - ColosseumContext* ctx, - ExactAttackRouteContext* route_ctx -) { - OsrsLosQuery los_query = col_player_los_query(ctx); - static const int target_sizes[] = {1, 2, 5}; - static const int attack_ranges[] = {1, 3, 6, 10}; - int checks = 0; - - for (int target_y = COLO_ARENA_MIN_Y - 1; - target_y <= COLO_ARENA_MAX_Y + 1; - target_y += 5) { - for (int target_x = COLO_ARENA_MIN_X - 1; - target_x <= COLO_ARENA_MAX_X + 1; - target_x += 4) { - for (size_t size_idx = 0; - size_idx < sizeof(target_sizes) / sizeof(target_sizes[0]); - size_idx++) { - int target_size = target_sizes[size_idx]; - EncounterArenaAttackRouteField field; - if (target_x < COLO_ARENA_MIN_X || - target_y < COLO_ARENA_MIN_Y || - target_x + target_size - 1 > COLO_ARENA_MAX_X || - target_y + target_size - 1 > COLO_ARENA_MAX_Y) - continue; - encounter_build_arena_attack_route_field( - &field, - ctx->collision_map, - ctx->world_offset_x, - ctx->world_offset_y, - s->player.x, - s->player.y, - target_x, - target_y, - target_size, - exact_attack_route_walkable, - route_ctx, - exact_attack_route_blocked, - route_ctx, - COLO_ARENA_MIN_X, - COLO_ARENA_MIN_Y, - COLO_ARENA_WIDTH, - COLO_ARENA_HEIGHT); - for (size_t range_idx = 0; - range_idx < sizeof(attack_ranges) / sizeof(attack_ranges[0]); - range_idx++) { - int attack_range = attack_ranges[range_idx]; - EncounterAttackRouteLanding expected = - exact_attack_route_runtime_landing( - s, ctx, route_ctx, - target_x, target_y, target_size, attack_range, - &los_query); - EncounterAttackRouteLanding actual = - exact_attack_route_optimized_landing( - s, ctx, route_ctx, - target_x, target_y, target_size, attack_range, - &los_query); - if (actual.land_x != expected.land_x || - actual.land_y != expected.land_y) { - fprintf(stderr, - "attack route mismatch scenario=%s player=(%d,%d) target=(%d,%d,%d) range=%d field=(%d,%d) runtime=(%d,%d)\n", - scenario, - s->player.x, - s->player.y, - target_x, - target_y, - target_size, - attack_range, - actual.land_x, - actual.land_y, - expected.land_x, - expected.land_y); - abort(); - } - checks++; - } - } - } - } - return checks; -} -static void exact_attack_route_property_selftest(void) { - ColosseumState s; - ColosseumContext ctx; - exact_prepare_custom( - &s, &ctx, 0xA771u, COLO_PLAYER_START_X, COLO_PLAYER_START_Y); - ExactAttackRouteContext route_ctx = { - .walk = {&s, &ctx}, - }; - int checks = exact_attack_route_property_scenario( - "open-arena", &s, &ctx, &route_ctx); - - route_ctx.blocker_count = 8; - int blocker_x[] = {5, 6, 7, 8, 9, 8, 7, 6}; - int blocker_y[] = {18, 17, 16, 17, 18, 19, 20, 19}; - memcpy(route_ctx.blocker_x, blocker_x, sizeof(blocker_x)); - memcpy(route_ctx.blocker_y, blocker_y, sizeof(blocker_y)); - checks += exact_attack_route_property_scenario( - "dynamic-blockers", &s, &ctx, &route_ctx); - - route_ctx.blocker_count = 0; - s.wave = COLO_WAVE_BOSS; - s.sol.started = 1; - s.sol.boss_arena_min_x = COLO_BOSS_ARENA_MIN_X; - s.sol.boss_arena_min_y = COLO_BOSS_ARENA_MIN_Y; - s.sol.boss_arena_max_x = COLO_BOSS_ARENA_MAX_X; - s.sol.boss_arena_max_y = COLO_BOSS_ARENA_MAX_Y; - s.player.x = -1; - s.player.y = -1; - for (int x = s.sol.boss_arena_min_x; - x <= s.sol.boss_arena_max_x && s.player.x < 0; - x++) { - for (int y = s.sol.boss_arena_min_y; - y <= s.sol.boss_arena_max_y; - y++) { - if (!encounter_arena_topology_tile_blocked( - ctx.route_topology, x, y)) { - s.player.x = x; - s.player.y = y; - break; - } - } - } - exact_refresh_geometry(&s, &ctx); - checks += exact_attack_route_property_scenario( - "sol-clamp", &s, &ctx, &route_ctx); - static const int exhaustive_attack_ranges[] = {1, 3, 6, 10}; - exact_prepare_custom( - &s, &ctx, 0xA771u, COLO_PLAYER_START_X, COLO_PLAYER_START_Y); - route_ctx.walk.s = &s; - route_ctx.walk.ctx = &ctx; - route_ctx.blocker_count = 0; - uint64_t exhaustive_checks = osrs_reference_route_exhaustive( - "open-arena", - ctx.route_topology, - (EncounterRouteBlockers){ - .is_blocked = exact_optimized_route_blocked, - .ctx = &route_ctx, - .revision = 1, - }, - 5, - exhaustive_attack_ranges, - 4, - exact_attack_route_can_attack, - &route_ctx); - route_ctx.blocker_count = 8; - memcpy(route_ctx.blocker_x, blocker_x, sizeof(blocker_x)); - memcpy(route_ctx.blocker_y, blocker_y, sizeof(blocker_y)); - exhaustive_checks += osrs_reference_route_exhaustive( - "dynamic-blockers", - ctx.route_topology, - (EncounterRouteBlockers){ - .is_blocked = exact_optimized_route_blocked, - .ctx = &route_ctx, - .revision = 9, - }, - 5, - exhaustive_attack_ranges, - 4, - exact_attack_route_can_attack, - &route_ctx); - printf( - "colosseum exhaustive route equivalence PASS: %llu source-target-range queries across 2 blocker fields\n", - (unsigned long long)exhaustive_checks); - printf( - "colosseum attack route property selftest PASS: %d target queries across 3 fields\n", - checks); -} -#endif - -static void exact_run_steps( - ColoExactWriter* writer, - uint32_t scenario_id, - ColosseumState* s, - ColosseumContext* ctx, - int steps, - uint64_t action_seed -) { - uint32_t capture_idx = 0; - int actions[COLO_NUM_ACTION_HEADS]; - uint64_t rng = action_seed; - exact_capture(writer, scenario_id, capture_idx++, s, ctx); - for (int step = 0; step < steps; step++) { - exact_trace_actions(s, &rng, actions); - col_step_ctx((EncounterState*)s, (EncounterContext*)ctx, actions); - exact_capture(writer, scenario_id, capture_idx++, s, ctx); - if (s->episode_over) break; - } -} - -static void exact_run_idle_steps( - ColoExactWriter* writer, - uint32_t scenario_id, - ColosseumState* s, - ColosseumContext* ctx, - int steps -) { - uint32_t capture_idx = 0; - int actions[COLO_NUM_ACTION_HEADS]; - exact_zero_actions(actions); - exact_capture(writer, scenario_id, capture_idx++, s, ctx); - for (int step = 0; step < steps; step++) { - col_step_ctx((EncounterState*)s, (EncounterContext*)ctx, actions); - exact_capture(writer, scenario_id, capture_idx++, s, ctx); - if (s->episode_over) break; - } -} - -static void exact_scenario_wave_rollout( - ColoExactWriter* writer, - uint32_t scenario_id, - int start_wave, - uint32_t seed, - int steps -) { - ColosseumState s; - ColosseumContext ctx; - exact_init_state(&s, &ctx, start_wave, seed); - exact_run_steps(writer, scenario_id, &s, &ctx, steps, - ((uint64_t)scenario_id << 40) ^ seed ^ 0x53A91C4D12ULL); -} - -static void exact_scenario_pillar_safespot(ColoExactWriter* writer) { - ColosseumState s; - ColosseumContext ctx; - exact_prepare_custom(&s, &ctx, 0x1101u, 7, 9); - col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 12, 9); - exact_refresh_geometry(&s, &ctx); - exact_run_steps(writer, 100u, &s, &ctx, 8, 0x991001u); -} - -static void exact_scenario_manticore_mid_barrage(ColoExactWriter* writer) { - ColosseumState s; - ColosseumContext ctx; - exact_prepare_custom(&s, &ctx, 0x1102u, 17, 16); - int slot = col_spawn_npc_at(&s, COLO_MANTICORE, 16, 19); - ColoManticoreState* mc = colo_npc_manticore(&s.npcs[slot]); - mc->cycle_step = 1; - mc->orb_style[0] = ATTACK_STYLE_MAGIC; - mc->orb_style[1] = ATTACK_STYLE_RANGED; - mc->orb_style[2] = ATTACK_STYLE_MELEE; - s.npcs[slot].attack_timer = 0; - exact_refresh_geometry(&s, &ctx); - exact_run_idle_steps(writer, 101u, &s, &ctx, 5); -} - -static void exact_scenario_javelin_boundary(ColoExactWriter* writer) { - ColosseumState s; - ColosseumContext ctx; - exact_prepare_custom(&s, &ctx, 0x1103u, 16, 16); - int slot = col_spawn_npc_at(&s, COLO_JAVELIN_COLOSSUS, 18, 18); - s.npcs[slot].attack_timer = 0; - colo_npc_javelin(&s.npcs[slot])->attack_count = 4; - exact_refresh_geometry(&s, &ctx); - exact_run_idle_steps(writer, 102u, &s, &ctx, 6); -} - -static void exact_scenario_warband_phase(ColoExactWriter* writer) { - ColosseumState s; - ColosseumContext ctx; - exact_prepare_custom(&s, &ctx, 0x1104u, 17, 16); - int slot = col_spawn_npc_at(&s, COLO_FREMENNIK_BERSERKER, 17, 17); - colo_npc_warband(&s.npcs[slot])->formation_dir = COLO_WARBAND_FORM_NORTH; - s.warband_cycle_anchor = s.tick; - exact_refresh_geometry(&s, &ctx); - exact_run_idle_steps(writer, 103u, &s, &ctx, 8); -} - -static void exact_scenario_red_flag_minotaur(ColoExactWriter* writer) { - ColosseumState s; - ColosseumContext ctx; - exact_prepare_custom(&s, &ctx, 0x1105u, 7, 9); - s.modifiers.active_mask |= 1u << COLO_MOD_RED_FLAG; - s.modifiers.tier[COLO_MOD_RED_FLAG] = 1; - col_spawn_npc_at(&s, COLO_MINOTAUR, 12, 9); - exact_refresh_geometry(&s, &ctx); - exact_run_idle_steps(writer, 104u, &s, &ctx, 10); -} - -static void exact_scenario_frozen_npc(ColoExactWriter* writer) { - ColosseumState s; - ColosseumContext ctx; - exact_prepare_custom(&s, &ctx, 0x1106u, 17, 16); - int slot = col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 20, 16); - s.npcs[slot].attack_timer = 0; - s.npcs[slot].frozen_ticks = 3; - exact_refresh_geometry(&s, &ctx); - exact_run_idle_steps(writer, 105u, &s, &ctx, 6); -} - -static void exact_scenario_stunned_npc(ColoExactWriter* writer) { - ColosseumState s; - ColosseumContext ctx; - exact_prepare_custom(&s, &ctx, 0x1107u, 17, 16); - int slot = col_spawn_npc_at(&s, COLO_SHOCKWAVE_COLOSSUS, 20, 16); - s.npcs[slot].attack_timer = 0; - s.npcs[slot].stun_timer = 3; - exact_refresh_geometry(&s, &ctx); - exact_run_idle_steps(writer, 106u, &s, &ctx, 6); -} - -static void exact_scenario_perimeter_los(ColoExactWriter* writer) { - ColosseumState s; - ColosseumContext ctx; - exact_prepare_custom(&s, &ctx, 0x1108u, 0, 15); - int edge = col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, COLO_ARENA_MIN_X, 15); - s.npcs[edge].attack_timer = 0; - int sol = col_spawn_npc_at(&s, COLO_SOL_HEREDIT, COLO_SOL_SPAWN_X, COLO_SOL_SPAWN_Y); - s.wave = COLO_WAVE_BOSS; - s.sol.started = 1; - s.sol.boss_idx = sol; - s.sol.boss_arena_min_x = COLO_BOSS_ARENA_MIN_X; - s.sol.boss_arena_min_y = COLO_BOSS_ARENA_MIN_Y; - s.sol.boss_arena_max_x = COLO_BOSS_ARENA_MAX_X; - s.sol.boss_arena_max_y = COLO_BOSS_ARENA_MAX_Y; - s.sol.phase = 1; - s.sol.crystal_count = 1; - s.sol.crystals[0].active = 1; - s.sol.crystals[0].edge = COLO_SOL_EDGE_NORTH; - s.sol.crystals[0].x = 16; - s.sol.crystals[0].y = s.sol.boss_arena_max_y - 1; - s.sol.crystals[0].dir = 1; - s.sol.crystals[0].move_timer = COLO_SOL_CRYSTAL_MOVE_TICKS; - s.sol.crystals[0].firing_freeze = COLO_SOL_LASER_FREEZE; - s.sol.laser_cooldown = COLO_SOL_CRYSTAL_COOLDOWN_MIN; - s.player.x = 16; - s.player.y = 14; - exact_refresh_geometry(&s, &ctx); - exact_run_idle_steps(writer, 107u, &s, &ctx, 5); -} - -static void exact_generate_fixture(const char* path) { - ColoExactWriter writer; - exact_writer_open(&writer, path); - - exact_scenario_wave_rollout(&writer, 1u, 0, 0xC010001u, 28); - exact_scenario_wave_rollout(&writer, 4u, 3, 0xC010004u, 32); - exact_scenario_wave_rollout(&writer, 8u, 7, 0xC010008u, 34); - exact_scenario_wave_rollout(&writer, 12u, 11, 0xC010012u, 34); - exact_scenario_pillar_safespot(&writer); - exact_scenario_manticore_mid_barrage(&writer); - exact_scenario_javelin_boundary(&writer); - exact_scenario_warband_phase(&writer); - exact_scenario_red_flag_minotaur(&writer); - exact_scenario_frozen_npc(&writer); - exact_scenario_stunned_npc(&writer); - exact_scenario_perimeter_los(&writer); - - exact_writer_close(&writer); -} - -static void exact_read_all(FILE* file, void* data, size_t size) { - if (fread(data, 1, size, file) != size) { - fprintf(stderr, "truncated colosseum exact fixture\n"); - abort(); - } -} - -static int exact_compare_files(const char* expected_path, const char* actual_path) { - FILE* expected = fopen(expected_path, "rb"); - if (!expected) { - perror("open expected colosseum exact fixture"); - abort(); - } - FILE* actual = fopen(actual_path, "rb"); - if (!actual) { - perror("open actual colosseum exact fixture"); - abort(); - } - - ColoExactFileHeader expected_file; - ColoExactFileHeader actual_file; - exact_read_all(expected, &expected_file, sizeof(expected_file)); - exact_read_all(actual, &actual_file, sizeof(actual_file)); - if (memcmp(&expected_file, &actual_file, sizeof(expected_file)) != 0) { - printf("colosseum exact mismatch: file header\n"); - fclose(expected); - fclose(actual); - return 1; - } - - for (uint32_t index = 0; index < expected_file.record_count; index++) { - ColoExactRecordHeader expected_record; - ColoExactRecordHeader actual_record; - float expected_obs[COLO_NUM_OBS]; - float actual_obs[COLO_NUM_OBS]; - float expected_mask[COLO_ACTION_MASK_SIZE]; - float actual_mask[COLO_ACTION_MASK_SIZE]; - ColosseumState expected_state; - ColosseumState actual_state; - exact_read_all(expected, &expected_record, sizeof(expected_record)); - exact_read_all(actual, &actual_record, sizeof(actual_record)); - exact_read_all(expected, expected_obs, sizeof(expected_obs)); - exact_read_all(actual, actual_obs, sizeof(actual_obs)); - exact_read_all(expected, expected_mask, sizeof(expected_mask)); - exact_read_all(actual, actual_mask, sizeof(actual_mask)); - exact_read_all(expected, &expected_state, sizeof(expected_state)); - exact_read_all(actual, &actual_state, sizeof(actual_state)); - - if (exact_hash_bytes(&expected_state, sizeof(expected_state)) != - expected_record.state_hash || - exact_hash_bytes(&actual_state, sizeof(actual_state)) != - actual_record.state_hash) { - printf("colosseum exact fixture state hash mismatch at record %u\n", index); - fclose(expected); - fclose(actual); - return 1; - } - exact_zero_serialized_route_storage(&expected_state); - exact_zero_serialized_route_storage(&actual_state); - expected_record.state_hash = - exact_hash_bytes(&expected_state, sizeof(expected_state)); - actual_record.state_hash = - exact_hash_bytes(&actual_state, sizeof(actual_state)); - if (memcmp(&expected_record, &actual_record, sizeof(expected_record)) != 0 || - memcmp(expected_obs, actual_obs, sizeof(expected_obs)) != 0 || - memcmp(expected_mask, actual_mask, sizeof(expected_mask)) != 0 || - memcmp(&expected_state, &actual_state, sizeof(expected_state)) != 0) { - printf( - "colosseum exact mismatch at record %u scenario %u step %u\n", - index, - expected_record.scenario_id, - expected_record.step_index); - printf( - "expected player=(%d,%d) dest=(%d,%d) target=%d actual player=(%d,%d) dest=(%d,%d) target=%d\n", - expected_state.player.x, - expected_state.player.y, - expected_state.player.dest_x, - expected_state.player.dest_y, - expected_state.player.interaction.target_slot, - actual_state.player.x, - actual_state.player.y, - actual_state.player.dest_x, - actual_state.player.dest_y, - actual_state.player.interaction.target_slot); - const uint8_t* expected_bytes = (const uint8_t*)&expected_state; - const uint8_t* actual_bytes = (const uint8_t*)&actual_state; - for (size_t byte = 0; byte < sizeof(expected_state); byte++) { - if (expected_bytes[byte] == actual_bytes[byte]) continue; - printf( - "first state byte mismatch offset=%zu expected=%u actual=%u\n", - byte, - (unsigned)expected_bytes[byte], - (unsigned)actual_bytes[byte]); - break; - } - for (int obs = 0; obs < COLO_NUM_OBS; obs++) { - if (expected_obs[obs] == actual_obs[obs]) continue; - printf( - "first obs mismatch index=%d expected=%g actual=%g\n", - obs, - (double)expected_obs[obs], - (double)actual_obs[obs]); - break; - } - for (int mask = 0; mask < COLO_ACTION_MASK_SIZE; mask++) { - if (expected_mask[mask] == actual_mask[mask]) continue; - printf( - "first mask mismatch index=%d expected=%g actual=%g\n", - mask, - (double)expected_mask[mask], - (double)actual_mask[mask]); - break; - } - fclose(expected); - fclose(actual); - return 1; - } - } - - int expected_tail = fgetc(expected); - int actual_tail = fgetc(actual); - fclose(expected); - fclose(actual); - if (expected_tail != EOF || actual_tail != EOF) { - printf("colosseum exact mismatch: trailing data\n"); - return 1; - } - return 0; -} - -int main(int argc, char** argv) { -#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES - if (argc == 2 && strcmp(argv[1], "--attack-route-selftest") == 0) { - col_static_los_endpoint_semantics_selftest(); - exact_attack_route_property_selftest(); - return 0; - } -#endif - if (argc != 3 || - (strcmp(argv[1], "--write-golden") != 0 && - strcmp(argv[1], "--compare") != 0)) { - fprintf(stderr, - "usage: %s --attack-route-selftest | --write-golden DIR | --compare DIR\n", - argv[0]); - return 2; - } - - col_static_los_table_selftest(); - col_static_los_endpoint_semantics_selftest(); - col_static_footprint_table_selftest(); - col_step_out_forecast_landing_selftest(); -#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES - exact_attack_route_property_selftest(); -#endif - - char fixture_path[1024]; - char current_path[1024]; - exact_mkdir_if_needed(argv[2]); - exact_readable_path( - fixture_path, sizeof(fixture_path), argv[2], - "colosseum_forecast_exact.bin"); - - if (strcmp(argv[1], "--write-golden") == 0) { - exact_generate_fixture(fixture_path); - printf("colosseum exact golden wrote %s\n", fixture_path); - return 0; - } - - exact_readable_path( - current_path, sizeof(current_path), argv[2], - "colosseum_forecast_exact.current.bin"); - exact_generate_fixture(current_path); - int failed = exact_compare_files(fixture_path, current_path); - if (failed) return 1; - printf("colosseum exact golden compare PASS: %s\n", fixture_path); - return 0; -} diff --git a/ocean/osrs/tests/test_colosseum_golden.c b/ocean/osrs/tests/test_colosseum_golden.c index ee2e698d70..90d810ab30 100644 --- a/ocean/osrs/tests/test_colosseum_golden.c +++ b/ocean/osrs/tests/test_colosseum_golden.c @@ -182,7 +182,6 @@ static const GoldenConfig CONFIGS[] = { #define NUM_CONFIGS ((int)(sizeof(CONFIGS) / sizeof(CONFIGS[0]))) #define EPISODE_TICKS 4000 -/* Re-seeded after the canonical inventory and equipment observation cutover. */ static const uint64_t BASELINE[NUM_CONFIGS] = { 0x51537d0fc2e421deULL, 0xf67cadfd982ad1f9ULL, diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c index 747564fdfe..d0a324f368 100644 --- a/ocean/osrs/tests/test_colosseum_modifiers.c +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -129,15 +129,12 @@ static int first_live_score_enemy(const ColosseumState* s) { return -1; } -static void kill_first_live_score_enemy( - ColosseumState* s, - ColosseumContext* ctx -) { +static void kill_first_live_score_enemy(ColosseumState* s) { int slot = first_live_score_enemy(s); CHECK("a live score enemy exists", slot >= 0); if (slot < 0) return; s->npcs[slot].hp = 0; - col_apply_npc_death(s, ctx, slot); + col_apply_npc_death(s, slot); } static float score_for_depth(float depth) { @@ -150,7 +147,7 @@ static void land_pending_player_hits( ColosseumContext* ctx ) { for (int t = 0; t < 4; t++) - col_resolve_player_projectiles_on_npcs(s, ctx); + col_resolve_player_projectiles_on_npcs_ctx(s, ctx); } static void geo_clear_npcs(ColosseumState* s) { @@ -394,6 +391,7 @@ static float test_any_inventory_kind_bit(const float* cell_obs) { static TestDroppedInventoryFields test_expected_dropped_inventory_fields( const ColosseumState* s, + const float mask[COLO_ACTION_MASK_SIZE], int cell_idx ) { const ColoInvCell* cell = &s->player.inventory_cells[cell_idx]; @@ -409,7 +407,7 @@ static TestDroppedInventoryFields test_expected_dropped_inventory_fields( .is_gear = test_binary_float(metadata->item != NULL), .is_consumable = test_binary_float( metadata->consumable_kind != OSRS_CONSUMABLE_NONE), - .can_use = test_binary_float(col_inventory_cell_actionable(s, cell_idx)), + .can_use = test_click_mask_for_cell_s(s, mask, cell_idx), .has_effect = test_binary_float(effect_mask != OSRS_ITEM_EFFECT_NONE), .role_food = test_binary_float(metadata->click_action == OSRS_CLICK_EAT), .role_potion_family = test_binary_float( @@ -472,7 +470,7 @@ static void test_check_inventory_cut_equivalence_state( for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { TestDroppedInventoryFields expected = - test_expected_dropped_inventory_fields(s, cell); + test_expected_dropped_inventory_fields(s, mask, cell); TestDroppedInventoryFields reconstructed = test_reconstructed_dropped_inventory_fields(s, obs, mask, cell); test_check_inventory_dropped_field( @@ -1208,7 +1206,7 @@ static void test_totem_lifecycle(void) { int tslot2 = s.totems[0].npc_slot; s.npcs[0].hp = 0; - col_apply_npc_death(&s, &ctx, 0); + col_apply_npc_death(&s, 0); CHECK("the owner's death despawns its totem", !s.npcs[tslot2].active && s.totems[0].phase == COLO_HAZARD_NONE); @@ -1220,7 +1218,7 @@ static void test_totem_lifecycle(void) { land_pending_player_hits(&s, &ctx); CHECK("second totem down and respawning", s.totems[0].phase == COLO_HAZARD_RESPAWNING); s.npcs[0].hp = 0; - col_apply_npc_death(&s, &ctx, 0); + col_apply_npc_death(&s, 0); CHECK("the owner's death cancels a pending totem respawn", s.totems[0].phase == COLO_HAZARD_NONE); @@ -1501,7 +1499,7 @@ static void test_totemic_sol_extra_totems_every_two_minutes(void) { CHECK("a third totem spawns two minutes later", count_live_totems(&s) == 3); s.npcs[sol].hp = 0; - col_apply_npc_death(&s, &ctx, sol); + col_apply_npc_death(&s, sol); CHECK("Sol's death despawns every totem it owned", count_live_totems(&s) == 0); } @@ -2053,7 +2051,7 @@ static void test_volatility_explodes_on_corpse_removal(void) { col_init_npc(&s, idx, COLO_FREMENNIK_BERSERKER, 20, 17); s.npcs[idx].hp = 0; int hp_before = s.player.current_hitpoints; - col_apply_npc_death(&s, &ctx, idx); + col_apply_npc_death(&s, idx); CHECK("Volatility does not explode on the lethal hit", s.player.current_hitpoints == hp_before); @@ -2090,7 +2088,7 @@ static void test_volatility_explodes_on_corpse_removal(void) { s.player.current_hitpoints = 99; col_init_npc(&s, idx, COLO_MINOTAUR, 20, 17); s.npcs[idx].hp = 0; - col_apply_npc_death(&s, &ctx, idx); + col_apply_npc_death(&s, idx); s.wave = 1; col_spawn_wave(&s, &ctx); CHECK("wave spawn cleanup still removes a volatile corpse through its explosion", @@ -2267,7 +2265,7 @@ static void test_death_linger_wave_clear_and_render(void) { &s.npcs[idx].hp, &s.npcs[idx].hit_landed_this_tick, &s.npcs[idx].hit_damage, 1); s.npcs[idx].hit_was_successful_this_tick = dealt > 0; - col_apply_npc_death(&s, &ctx, idx); + col_apply_npc_death(&s, idx); int linger_ticks = col_npc_death_linger_ticks(COLO_FREMENNIK_BERSERKER); CHECK("lethal hit starts NPC death linger", s.npcs[idx].active && @@ -2640,7 +2638,7 @@ static void test_outcome_score_wave_clear_has_no_double_count(void) { complete_open_draft(&s, &ctx, 1); while (first_live_score_enemy(&s) >= 0) - kill_first_live_score_enemy(&s, &ctx); + kill_first_live_score_enemy(&s); s.log.waves_cleared = 1; CHECK("within-wave progress is zero after all wave enemies are killed", col_current_wave_score_progress(&s) == 0.0f); @@ -2750,7 +2748,7 @@ static void test_wave12_quartet_and_win(void) { int sol = col_sol_find_idx(&s); if (sol < 0) { win_ok = 0; continue; } s.npcs[sol].hp = 0; - col_apply_npc_death(&s, &ctx, sol); + col_apply_npc_death(&s, sol); int idle[COLO_NUM_ACTION_HEADS] = {0}; step_and_observe(&s, &ctx, idle); if (!(s.episode_over && s.winner == COLO_OUTCOME_PLAYER_WON)) win_ok = 0; @@ -2905,9 +2903,6 @@ static void test_warband_move_skip(void) { CHECK("once the warband is chasing the stutter-step suppresses every attack", attacks_once_chasing == 0); } - -/** Regression: warband melee is calculated before the player tick, so a same-tick - step-out cannot dodge it. */ static void test_warband_melee_not_dodged_by_same_tick_step_out(void) { printf("test_warband_melee_not_dodged_by_same_tick_step_out\n"); ColosseumContext ctx; @@ -4101,23 +4096,17 @@ static void skyfall_dodge_init_state( static int skyfall_dodge_move_action_off_tile( const ColosseumState* s, - const ColosseumContext* ctx, + ColosseumContext* ctx, int marked_x, int marked_y ) { for (int action = 1; action < ENCOUNTER_MOVE_ACTIONS; action++) { ColosseumState tmp = *s; - ColoGeometryContext geometry = { - .state = &tmp, - .context = ctx, - }; - int moved = encounter_move_to_target( - &tmp.player, - ENCOUNTER_MOVE_TARGET_DX[action], - ENCOUNTER_MOVE_TARGET_DY[action], - col_player_walkable, - &geometry); - if (moved > 0 && (tmp.player.x != marked_x || tmp.player.y != marked_y)) + OsrsLosQuery los_query = col_player_los_query(ctx); + OsrsPlayerStepInput input = + col_primary_step_input(&tmp, ctx, action, &los_query); + osrs_encounter_player_step(&input); + if (tmp.player.x != marked_x || tmp.player.y != marked_y) return action; } assert(0 && "no move action leaves the marked tile"); @@ -5763,6 +5752,7 @@ static void test_consumable_overdrink_mask(void) { ColosseumState s; loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 11); s.player.potion_timer = 0; + float mask[COLO_ACTION_MASK_SIZE]; int brew = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); int combat = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_DIVINE_COMBAT); @@ -5771,22 +5761,30 @@ static void test_consumable_overdrink_mask(void) { brew >= 0 && combat >= 0 && sanfew >= 0); s.player.current_hitpoints = s.player.base_hitpoints; - CHECK("brew masked at full HP", !col_inventory_cell_actionable(&s, brew)); + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + CHECK("brew masked at full HP", + test_click_mask_for_cell_s(&s, mask, brew) == 0.0f); s.player.current_hitpoints = s.player.base_hitpoints - 10; - CHECK("brew valid below max HP", col_inventory_cell_actionable(&s, brew)); + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + CHECK("brew valid below max HP", + test_click_mask_for_cell_s(&s, mask, brew) == 1.0f); s.player.current_attack = s.player.base_attack; s.player.current_strength = s.player.base_strength; s.player.current_defence = s.player.base_defence; - CHECK("combat valid at unboosted stats", col_inventory_cell_actionable(&s, combat)); + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + CHECK("combat valid at unboosted stats", + test_click_mask_for_cell_s(&s, mask, combat) == 1.0f); s.player.current_attack = 105; s.player.current_strength = 112; s.player.current_defence = 118; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); CHECK("combat masked once all combat stats >= 105", - !col_inventory_cell_actionable(&s, combat)); + test_click_mask_for_cell_s(&s, mask, combat) == 0.0f); s.player.current_strength = 104; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); CHECK("combat valid again when one stat dips below 105", - col_inventory_cell_actionable(&s, combat)); + test_click_mask_for_cell_s(&s, mask, combat) == 1.0f); s.player.current_attack = s.player.base_attack; s.player.current_strength = s.player.base_strength; @@ -5796,13 +5794,18 @@ static void test_consumable_overdrink_mask(void) { s.player.current_prayer = s.player.base_prayer; s.player_venom = 0; s.player_poison = 0; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); CHECK("sanfew masked with full stats/prayer and no venom", - !col_inventory_cell_actionable(&s, sanfew)); + test_click_mask_for_cell_s(&s, mask, sanfew) == 0.0f); s.player_venom = 4; - CHECK("sanfew valid while venomed (it cures)", col_inventory_cell_actionable(&s, sanfew)); + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + CHECK("sanfew valid while venomed", + test_click_mask_for_cell_s(&s, mask, sanfew) == 1.0f); s.player_venom = 0; s.player.current_prayer = s.player.base_prayer - 60; - CHECK("sanfew valid when prayer is well down", col_inventory_cell_actionable(&s, sanfew)); + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + CHECK("sanfew valid when prayer is well down", + test_click_mask_for_cell_s(&s, mask, sanfew) == 1.0f); } static void test_loadout_surge_potion(void) { @@ -6458,7 +6461,7 @@ static void test_primary_head_resolution(void) { s.npcs[0].hp = 200; s.npcs[0].max_hp = 200; col_rebuild_player_collision_flags(&s); - col_refresh_current_obs_slots_ctx(&s, &ctx); + col_refresh_current_obs_slots(&s); int obs_slot = col_find_target_obs_slot(&s, 0); int attack_action = col_primary_attack_action_for_obs_slot(obs_slot); @@ -7144,7 +7147,7 @@ static void test_death_charge_regression(void) { CHECK("Death Charge arms a 100-tick window (decremented this tick)", s.death_charge_window_left == 99 && s.death_charge_cd == 0); s.npcs[slot].hp = 0; - col_apply_npc_death(&s, &ctx, slot); + col_apply_npc_death(&s, slot); CHECK("a player-credited kill in the window grants +15 spec", s.player.special_energy == 65); CHECK("the kill closes the window and starts the 100-tick cooldown", @@ -7154,13 +7157,13 @@ static void test_death_charge_regression(void) { s.player.special_energy = 95; col_tick_player_ctx(&s, &ctx, cast_dc, 1); s.npcs[slot].hp = 0; - col_apply_npc_death(&s, &ctx, slot); + col_apply_npc_death(&s, slot); CHECK("Death Charge spec gain clamps at 100", s.player.special_energy == 100); slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 213); s.player.special_energy = 40; s.npcs[slot].hp = 0; - col_apply_npc_death(&s, &ctx, slot); + col_apply_npc_death(&s, slot); CHECK("a kill outside an armed window grants no spec and starts no cooldown", s.player.special_energy == 40 && s.death_charge_cd == 0); @@ -7190,8 +7193,8 @@ static void test_death_charge_regression(void) { col_tick_player_ctx(&s, &ctx, cast_dc, 1); s.npcs[slot].hp = 0; s.npcs[slot_b].hp = 0; - col_apply_npc_death(&s, &ctx, slot); - col_apply_npc_death(&s, &ctx, slot_b); + col_apply_npc_death(&s, slot); + col_apply_npc_death(&s, slot_b); CHECK("two same-tick kills consume the charge exactly once (+15)", s.player.special_energy == 65 && s.death_charge_window_left == 0); @@ -7902,13 +7905,13 @@ static void test_player_melee_lands_at_delay_zero(void) { int hp_before = s.npcs[0].hp; int resolved_same_pass = 1; for (int swing = 0; swing < 32 && s.npcs[0].hp == hp_before; swing++) { - col_resolve_player_projectiles_on_npcs(&s, &ctx); + col_resolve_player_projectiles_on_npcs_ctx(&s, &ctx); if (s.npcs[0].pending_hits.count != 0) resolved_same_pass = 0; if (s.npcs[0].hp < hp_before) break; s.player.attack_timer = 0; col_player_attack_target_ctx(&s, &ctx, 0); } - col_resolve_player_projectiles_on_npcs(&s, &ctx); + col_resolve_player_projectiles_on_npcs_ctx(&s, &ctx); CHECK("a delay-0 melee hit lands on the first resolver pass", s.npcs[0].hp < hp_before && resolved_same_pass && s.npcs[0].pending_hits.count == 0); @@ -8032,10 +8035,6 @@ static void test_pending_hit_recoil_volatility_damage_accounting(void) { col_accumulate_tick_stats(&s, &ctx); } -/* The win bonus used to be `+=` onto the same tick's shaped reward and wave-clear bonus, - * which pushed the tick past the trainer's [-1,1] clamp and measurably destroyed 59% of it. - * Terminal outcomes are now assignments, like death and timeout. */ - static void test_shared_inventory_tracks_gear_swaps(void) { printf("test_shared_inventory_tracks_gear_swaps\n"); ColosseumState s; @@ -8242,7 +8241,7 @@ static void test_stage3_t1_human_inventory_primary_click_uses_resolver(void) { human_input_queue_inventory_primary_click(&hi, bow_cell); human_input_queue_inventory_primary_click(&hi, claws_cell); int actions[COLO_NUM_ACTION_HEADS] = {0}; - col_translate_human_commands_ctx(&hi, actions, &s, &ctx); + col_translate_human_commands(&hi, actions, &s); CHECK("human weapon clicks collapse to the weapon equip head", actions[COLO_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] == claws_cell + 1); @@ -8273,7 +8272,7 @@ static void test_stage3_t1_human_rearrange_swaps_inventory_slots(void) { osrs_inventory_cell_item_index(&s.player.inventory_cells[bow_cell]), osrs_inventory_cell_raw_osrs_id(&s.player.inventory_cells[bow_cell])); int actions[COLO_NUM_ACTION_HEADS] = {0}; - col_translate_human_commands_ctx(&hi, actions, &s, &ctx); + col_translate_human_commands(&hi, actions, &s); CHECK("human rearrange moves bow to target slot", osrs_inventory_cell_item_index( @@ -8584,10 +8583,13 @@ static void test_inventory_pure_cut_reconstruction(void) { loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 903); int bowfa_cell = test_find_inventory_cell_with_item(&s, ITEM_BOW_OF_FAERDHINEN); assert(bowfa_cell >= 0); + float bowfa_mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, bowfa_mask); CHECK("full inventory two-handed equip is denied", s.player.equipped[GEAR_SLOT_WEAPON] != ITEM_NONE && s.player.equipped[GEAR_SLOT_SHIELD] != ITEM_NONE && - !col_inventory_cell_actionable(&s, bowfa_cell)); + test_click_mask_for_cell_s(&s, bowfa_mask, bowfa_cell) == 0.0f); test_check_inventory_cut_equivalence_state( &s, &ctx, "full inventory two-handed equip denial"); @@ -9079,7 +9081,8 @@ static void test_farm_safe_damage_cap(void) { s.tick_scratch.fresh_damage_dealt = 100.0f; s.tick_scratch.fresh_damage_reinforcement = 30.0f; col_accumulate_tick_stats(&s, &ctx); - float r_capped = col_shaped_total(col_shaped_reward(&s, &ctx)); + ColoShapedReward capped = col_shaped_reward(&s, &ctx); + float r_capped = capped.damage + capped.boss_phase; CHECK("cap on, wave 1: reinforcement damage pays nothing", fabsf(r_capped - 70.0f) < 1e-3f); CHECK("farm damage is logged", fabsf(s.log.farm_damage - 30.0f) < 1e-3f); @@ -9088,7 +9091,8 @@ static void test_farm_safe_damage_cap(void) { s.tick_scratch.fresh_damage_dealt = 100.0f; s.tick_scratch.fresh_damage_reinforcement = 30.0f; col_accumulate_tick_stats(&s, &ctx); - float r_uncapped = col_shaped_total(col_shaped_reward(&s, &ctx)); + ColoShapedReward uncapped = col_shaped_reward(&s, &ctx); + float r_uncapped = uncapped.damage + uncapped.boss_phase; CHECK("cap off: full damage pays", fabsf(r_uncapped - 100.0f) < 1e-3f); ctx.config.farm_safe_damage_cap = 1; @@ -9096,7 +9100,8 @@ static void test_farm_safe_damage_cap(void) { s.tick_scratch.fresh_damage_dealt = 100.0f; s.tick_scratch.fresh_damage_reinforcement = 30.0f; col_accumulate_tick_stats(&s, &ctx); - float r_late = col_shaped_total(col_shaped_reward(&s, &ctx)); + ColoShapedReward late = col_shaped_reward(&s, &ctx); + float r_late = late.damage + late.boss_phase; CHECK("cap on, wave 5+: reinforcements stay full-value at the default window", fabsf(r_late - 100.0f) < 1e-3f); @@ -9104,7 +9109,8 @@ static void test_farm_safe_damage_cap(void) { s.tick_scratch.fresh_damage_dealt = 100.0f; s.tick_scratch.fresh_damage_reinforcement = 30.0f; col_accumulate_tick_stats(&s, &ctx); - float r_widened = col_shaped_total(col_shaped_reward(&s, &ctx)); + ColoShapedReward widened = col_shaped_reward(&s, &ctx); + float r_widened = widened.damage + widened.boss_phase; CHECK("widened farm_cap_waves caps the same wave-5 reinforcement damage", fabsf(r_widened - 70.0f) < 1e-3f); ctx.config.farm_cap_waves = COLO_FARM_CAP_WAVES; diff --git a/ocean/osrs/tests/probe_colo_sim_invariant.c b/ocean/osrs/tests/test_colosseum_sim_invariant.c similarity index 69% rename from ocean/osrs/tests/probe_colo_sim_invariant.c rename to ocean/osrs/tests/test_colosseum_sim_invariant.c index 6c948fbef4..f281a6c74e 100644 --- a/ocean/osrs/tests/probe_colo_sim_invariant.c +++ b/ocean/osrs/tests/test_colosseum_sim_invariant.c @@ -1,30 +1,3 @@ -/* Hashes SIMULATION state only, never observation floats. - * - * test_colosseum_golden folds COLO_NUM_OBS obs floats and the sim scalars into one - * hash, so re-seeding it after an observation change also re-seeds the simulation - * half and a sim regression landed in the same commit is invisible. This probe is - * the half that must not move when the observation is recut. - * - * Covers the whole ColosseumState plus the full 452-float action mask. Storage whose - * size or representation is not part of the simulation contract is hashed semantically: - * inventory identity, obs memo caches, and pending-hit queue spare capacity. Resizing or - * renarrowing those records does not move a digest. - * - * Re-seeding rule: prove the digests first, never re-print to make an edit pass. Pending-hit - * narrowing and canonical inventory identity were carried across by computing the same - * semantic hash on the old and new records and confirming all 12 were byte-identical. - * - * The player-collision re-seed could NOT use that method: replacing a 1156-byte array with - * two ints moved the struct's padding (size fell 1152, not the 1148 the fields imply), so - * the hashed byte SEQUENCE differs even though the content does not. That re-seed rests on - * the golden tests, which hash observation floats every tick and would move if any NPC were - * blocked differently, plus test_colosseum_modifiers at 10491/10491. - * - * ColosseumLog is skipped as of the reward-clamp telemetry: it is an observation OF the - * simulation, not part of it, so adding a counter to it must not read as a sim regression. - * That re-seed WAS proved by the differential method: digests printed with the log skipped - * on the tree without the telemetry reproduced all 12 with it. Sim quantities the log - * derives from still reach the goldens through the observation. */ #include #include @@ -76,10 +49,6 @@ typedef struct { uint64_t baseline; } SimConfig; -/* Baselines pin the consolidated Manticore activation, automatic-thrall, and - * corpse-timed Volatility behavior after inventory moved into canonical Player state. - * Regenerate with --print only for an intended simulation or serialized-state change, - * never to make an observation-only edit pass. */ static SimConfig CONFIGS[] = { {"w01", 1, 1001ULL, 0xC0FFEE01ULL, 0x00b4a64bb88751a0ULL}, {"w02", 2, 1002ULL, 0xC0FFEE02ULL, 0x06e0f553b3786844ULL}, @@ -107,9 +76,6 @@ static void fill_actions( } } -/* Queue contents widened back to int32, so narrowing a field's storage type does not - * move the digest. Only the live prefix is hashed: readers all loop to count, so bytes - * past it are not simulation state. */ static uint64_t fnv_queue(uint64_t h, const EncounterPendingHitQueue* q) { h = fnv_i32(h, q->count); for (int i = 0; i < q->count; i++) { @@ -146,15 +112,6 @@ static int skip_cmp(const void* a, const void* b) { } static uint64_t hash_sim(uint64_t h, const ColosseumState* s, const float* mask) { - /* Hash AROUND storage whose SIZE is not part of the simulation contract, then hash - * its contents semantically. FNV-1a multiplies once per byte, so leaving a memo - * cache or a queue's spare capacity in the byte range makes resizing it move every - * digest -- a false alarm on exactly the edits this probe exists to clear. */ - /* log and obs_memos are the last two members, skipped as ONE region running to the end - * of the struct. Skipping them individually left the alignment padding between them in - * the hash, and that padding is 4 bytes or 0 depending on sizeof(ColosseumLog) mod 8 -- - * so adding three floats to the log moved all twelve digests with no behaviour change. - * Taking the whole tail removes the padding from the hash entirely. */ SkipRegion skip[] = { {offsetof(ColosseumState, npcs), sizeof(s->npcs)}, {offsetof(ColosseumState, player.inventory_cells), diff --git a/ocean/osrs/tests/probe_env_thread_determinism.c b/ocean/osrs/tests/test_colosseum_thread_determinism.c similarity index 80% rename from ocean/osrs/tests/probe_env_thread_determinism.c rename to ocean/osrs/tests/test_colosseum_thread_determinism.c index 552e6e12dc..2838d379bb 100644 --- a/ocean/osrs/tests/probe_env_thread_determinism.c +++ b/ocean/osrs/tests/test_colosseum_thread_determinism.c @@ -1,13 +1,3 @@ -/** - * Steps a fixed set of colosseum envs through a fixed action sequence, once with - * one OpenMP worker and once with many, then compares every observation float. - * - * This isolates the environment from the trainer. Training diverges run to run - * only when vec.num_threads > 1, and the trainer's env loop is - * `#pragma omp parallel for schedule(static)` over independent Env structs, so - * either the env carries thread-shared state or the cause is outside the env. - * Nothing here is reimplemented: it drives col_step_ctx and col_write_obs_ctx. - */ #define _POSIX_C_SOURCE 200809L #include #include @@ -23,22 +13,19 @@ typedef struct { ColosseumState state; ColosseumContext ctx; -} ProbeEnv; +} ThreadTestEnv; -static uint64_t sm(uint64_t* s) { +static uint64_t splitmix64(uint64_t* s) { uint64_t z = (*s += 0x9E3779B97F4A7C15ull); z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull; z = (z ^ (z >> 27)) * 0x94D049BB133111EBull; return z ^ (z >> 31); } -static ProbeEnv* g_envs; +static ThreadTestEnv* g_envs; static float* g_obs; static uint64_t g_state_hash[NUM_ENVS]; -/* Observations are a lossy view of the state, so hash the whole struct too: - divergence in rewards, timers or RNG can be invisible in obs yet still steer - training. States are memset before reset, so padding hashes deterministically. */ static uint64_t fnv1a(const void* p, size_t n) { const unsigned char* b = (const unsigned char*)p; uint64_t h = 1469598103934665603ull; @@ -59,9 +46,6 @@ static void run_pass(int workers, int late_start_mode, const int* actions) { col_reset_ctx((EncounterState*)&g_envs[i].state, (EncounterContext*)&g_envs[i].ctx, 900u + (unsigned)i * 7u); } - /* The trainer resets inside the parallel loop when an episode ends, which is - the path col_apply_late_start_entry_state and the wave-entry reservoir sit - on. Stepping without it leaves finished envs frozen and never exercises it. */ for (int t = 0; t < NUM_TICKS; t++) { #pragma omp parallel for schedule(static) num_threads(workers) for (int i = 0; i < NUM_ENVS; i++) { @@ -121,9 +105,9 @@ int main(int argc, char** argv) { int* actions = (int*)malloc(nact * sizeof(int)); uint64_t rng = 20260730u; for (size_t i = 0; i < nact; i++) - actions[i] = (int)(sm(&rng) % (uint64_t)COLO_ACTION_DIMS[i % COLO_NUM_ACTION_HEADS]); + actions[i] = (int)(splitmix64(&rng) % (uint64_t)COLO_ACTION_DIMS[i % COLO_NUM_ACTION_HEADS]); - g_envs = (ProbeEnv*)malloc(sizeof(ProbeEnv) * NUM_ENVS); + g_envs = (ThreadTestEnv*)malloc(sizeof(ThreadTestEnv) * NUM_ENVS); g_obs = (float*)malloc(sizeof(float) * NUM_ENVS * COLO_NUM_OBS); float* ref = (float*)malloc(sizeof(float) * NUM_ENVS * COLO_NUM_OBS); uint64_t ref_hash[NUM_ENVS]; diff --git a/ocean/osrs/tests/test_encounter_route_topology_setup.c b/ocean/osrs/tests/test_encounter_route_topology_setup.c index b1c754e176..1caaac34a0 100644 --- a/ocean/osrs/tests/test_encounter_route_topology_setup.c +++ b/ocean/osrs/tests/test_encounter_route_topology_setup.c @@ -69,142 +69,6 @@ static uint32_t expected_flags(const CollisionMap* map, int x, int y) { if (!is_in_wilderness(x, y)) return COLLISION_BLOCKED | LOS_FULL_MASK; return (uint32_t)collision_get_flags(map, 0, x, y); } -typedef struct { - int count; - int x[16]; - int y[16]; -} NhPvpTestBlockers; - -static int nh_pvp_test_blocked(void* data, int x, int y, int size) { - (void)size; - const NhPvpTestBlockers* blockers = (const NhPvpTestBlockers*)data; - for (int i = 0; i < blockers->count; i++) - if (blockers->x[i] == x && blockers->y[i] == y) return 1; - return 0; -} - -static int nh_pvp_route_results_equal( - const EncounterRouteResult* source_field, - const EncounterRouteResult* target_bfs -) { - if (source_field->outcome != target_bfs->outcome || - source_field->destination_x != target_bfs->destination_x || - source_field->destination_y != target_bfs->destination_y || - source_field->first_dx != target_bfs->first_dx || - source_field->first_dy != target_bfs->first_dy || - source_field->run_dx != target_bfs->run_dx || - source_field->run_dy != target_bfs->run_dy || - source_field->distance != target_bfs->distance || - source_field->waypoint_count != target_bfs->waypoint_count) - return 0; - for (int i = 0; i < source_field->waypoint_count; i++) { - if (source_field->waypoint_x[i] != target_bfs->waypoint_x[i] || - source_field->waypoint_y[i] != target_bfs->waypoint_y[i]) - return 0; - } - return 1; -} - -static uint64_t nh_pvp_target_bfs_equivalence( - const EncounterArenaTopology* topology, - NhPvpTestBlockers* blockers, - uint64_t blocker_revision -) { - uint64_t checks = 0; - EncounterRouteInput input = { - .topology = topology, - .blockers = { - .is_blocked = blockers->count ? nh_pvp_test_blocked : NULL, - .ctx = blockers, - .revision = blocker_revision, - }, - .actor_size = 1, - .target_size = 1, - .target_kind = ENCOUNTER_ROUTE_TARGET_CARDINAL_ADJACENCY, - .movement_mode = ENCOUNTER_ROUTE_MOVEMENT_RUN, - }; - for (int source_x = topology->origin_x; - source_x < topology->origin_x + topology->width; - source_x++) { - for (int source_y = topology->origin_y; - source_y < topology->origin_y + topology->height; - source_y++) { - if (encounter_arena_topology_footprint_blocked( - topology, source_x, source_y, 1) || - nh_pvp_test_blocked(blockers, source_x, source_y, 1)) - continue; - input.source_x = source_x; - input.source_y = source_y; - for (int target_x = topology->origin_x; - target_x < topology->origin_x + topology->width; - target_x++) { - for (int target_y = topology->origin_y; - target_y < topology->origin_y + topology->height; - target_y++) { - if (encounter_arena_topology_footprint_blocked( - topology, target_x, target_y, 1)) - continue; - input.target_x = target_x; - input.target_y = target_y; - input.cost_policy = ENCOUNTER_ROUTE_COST_OSRS; - EncounterRouteResult source_field = - encounter_route_solve(&input); - input.cost_policy = ENCOUNTER_ROUTE_COST_OSRS_TARGET_BFS; - EncounterRouteResult target_bfs = - encounter_route_solve(&input); - if (!nh_pvp_route_results_equal(&source_field, &target_bfs)) { - fprintf( - stderr, - "nh_pvp target BFS mismatch source=(%d,%d) target=(%d,%d) blockers=%d\n", - source_x, - source_y, - target_x, - target_y, - blockers->count); - abort(); - } - checks++; - } - } - } - } - return checks; -} - -static void nh_pvp_target_bfs_selftest( - const EncounterArenaTopology* topology -) { - NhPvpTestBlockers blockers = {0}; - uint64_t checks = - nh_pvp_target_bfs_equivalence(topology, &blockers, 1); - blockers = (NhPvpTestBlockers){ - .count = 8, - .x = { - FIGHT_AREA_BASE_X + 17, - FIGHT_AREA_BASE_X + 17, - FIGHT_AREA_BASE_X + 17, - FIGHT_AREA_BASE_X + 17, - FIGHT_AREA_BASE_X + 29, - FIGHT_AREA_BASE_X + 29, - FIGHT_AREA_BASE_X + 30, - FIGHT_AREA_BASE_X + 30, - }, - .y = { - FIGHT_AREA_BASE_Y + 8, - FIGHT_AREA_BASE_Y + 9, - FIGHT_AREA_BASE_Y + 11, - FIGHT_AREA_BASE_Y + 12, - FIGHT_AREA_BASE_Y + 18, - FIGHT_AREA_BASE_Y + 19, - FIGHT_AREA_BASE_Y + 18, - FIGHT_AREA_BASE_Y + 19, - }, - }; - checks += nh_pvp_target_bfs_equivalence(topology, &blockers, 2); - printf( - "nh_pvp target-directed BFS equivalence PASS: %llu source-target queries across 2 blocker fields\n", - (unsigned long long)checks); -} #else #error "define one TEST_ROUTE_TOPOLOGY_* encounter" #endif @@ -325,9 +189,6 @@ int main(void) { fprintf(stderr, "%s did not reuse process topology\n", TEST_DEF.name); abort(); } -#if defined(TEST_ROUTE_TOPOLOGY_NH_PVP) - nh_pvp_target_bfs_selftest(first); -#endif printf("%s route topology setup PASS: %d open %d blocked, order-independent, identical-map reuse\n", TEST_DEF.name, open_tiles, blocked_tiles); collision_map_free(first_map); diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index 03b84baf0b..bc3deb3789 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -6,7 +6,6 @@ #include "ocean/osrs/osrs_policy.h" #include "ocean/osrs/encounters/encounter_inferno.h" -#include "ocean/osrs/tests/osrs_route_reference.h" static void inf_init_unfinalized_context(InfernoContext* ctx) { inf_init_context_typed(ctx); } @@ -22,16 +21,6 @@ static void inf_init_unfinalized_context(InfernoContext* ctx) { #include "ocean/osrs/tests/osrs_test_check.h" -#define ASSERT_INT_LE(label, actual, expected) do { \ - tests_run++; \ - if ((actual) <= (expected)) { \ - tests_passed++; \ - } else { \ - tests_failed++; \ - printf(" FAIL: %s - got %d, expected <= %d\n", \ - (label), (actual), (expected)); \ - } \ -} while (0) static void assert_child_aborts(const char* label, void (*fn)(void)) { fflush(NULL); @@ -52,192 +41,6 @@ static void assert_child_aborts(const char* label, void (*fn)(void)) { } } -static char* read_source_file(const char* path) { - FILE* fp = fopen(path, "rb"); - if (!fp) return NULL; - if (fseek(fp, 0, SEEK_END) != 0) { - fclose(fp); - return NULL; - } - long len = ftell(fp); - if (len < 0) { - fclose(fp); - return NULL; - } - if (fseek(fp, 0, SEEK_SET) != 0) { - fclose(fp); - return NULL; - } - char* data = (char*)malloc((size_t)len + 1); - if (!data) { - fclose(fp); - return NULL; - } - size_t got = fread(data, 1, (size_t)len, fp); - fclose(fp); - if (got != (size_t)len) { - free(data); - return NULL; - } - data[len] = '\0'; - return data; -} - -static int source_block_contains( - const char* path, - const char* block_start, - const char* block_end, - const char* needle -) { - char* source = read_source_file(path); - if (!source) return 0; - char* start = strstr(source, block_start); - if (!start) { - free(source); - return 0; - } - char* end = strstr(start, block_end); - if (!end) { - free(source); - return 0; - } - int found = 0; - char saved = *end; - *end = '\0'; - found = strstr(start, needle) != NULL; - *end = saved; - free(source); - return found; -} - -static int source_count_token(const char* start, const char* token) { - int count = 0; - size_t token_len = strlen(token); - const char* p = start; - while ((p = strstr(p, token)) != NULL) { - count++; - p += token_len; - } - return count; -} - -static int source_seen_key(char keys[128][96], int key_count, const char* key) { - for (int i = 0; i < key_count; i++) { - if (strcmp(keys[i], key) == 0) return 1; - } - return 0; -} - -static int inferno_my_log_metric_key_count(void) { - char* source = read_source_file("ocean/osrs_inferno/osrs_inferno.h"); - if (!source) return -1; - char* start = strstr(source, "void puf_log"); - if (!start) { - free(source); - return -1; - } - - char keys[128][96] = {{0}}; - int key_count = 0; - const char* prefix = "dict_set(out, \""; - size_t prefix_len = strlen(prefix); - char* p = start; - while ((p = strstr(p, prefix)) != NULL) { - p += prefix_len; - char* end = strchr(p, '"'); - if (!end) break; - size_t len = (size_t)(end - p); - if (len >= sizeof(keys[0])) len = sizeof(keys[0]) - 1; - char key[96] = {0}; - memcpy(key, p, len); - if (!source_seen_key(keys, key_count, key)) { - if (key_count >= 128) { - free(source); - return 10000; - } - memcpy(keys[key_count], key, len + 1); - key_count++; - } - p = end + 1; - } - - int idle_metric_calls = source_count_token(start, "inferno_log_idle_metric("); - free(source); - return key_count + - idle_metric_calls * (1 + OSRS_INFERNO_IDLE_PHASE_COUNT); -} - -#define ASSERT_SOURCE_BLOCK_CONTAINS(label, path, block_start, block_end, needle) do { \ - tests_run++; \ - if (source_block_contains((path), (block_start), (block_end), (needle))) { \ - tests_passed++; \ - } else { \ - tests_failed++; \ - printf(" FAIL: %s - missing \"%s\" in %s\n", \ - (label), (needle), (path)); \ - } \ -} while (0) - -#define ASSERT_SOURCE_BLOCK_NOT_CONTAINS(label, path, block_start, block_end, needle) do { \ - tests_run++; \ - if (!source_block_contains((path), (block_start), (block_end), (needle))) { \ - tests_passed++; \ - } else { \ - tests_failed++; \ - printf(" FAIL: %s - unexpected \"%s\" in %s\n", \ - (label), (needle), (path)); \ - } \ -} while (0) - -#define ASSERT_SOURCE_NOT_CONTAINS(label, path, needle) do { \ - char* source = read_source_file((path)); \ - tests_run++; \ - if (source && strstr(source, (needle)) == NULL) { \ - tests_passed++; \ - } else { \ - tests_failed++; \ - printf(" FAIL: %s - unexpected \"%s\" in %s\n", \ - (label), (needle), (path)); \ - } \ - free(source); \ -} while (0) - -static int inferno_source_bundle_contains(const char* needle) { - static const char* paths[] = { - "ocean/osrs/encounters/encounter_inferno.h", - "ocean/osrs/encounters/inferno/encounter_inferno_model.inc", - "ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc", - "ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc", - "ocean/osrs/encounters/inferno/encounter_inferno_movement.inc", - "ocean/osrs/encounters/inferno/encounter_inferno_combat.inc", - "ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc", - "ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc", - "ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc", - "ocean/osrs/encounters/inferno/encounter_inferno_lab.inc", - "ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc", - "ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc", - }; - - for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) { - char* source = read_source_file(paths[i]); - if (!source) return 1; - int found = strstr(source, needle) != NULL; - free(source); - if (found) return 1; - } - return 0; -} - -#define ASSERT_INFERNO_SOURCE_NOT_CONTAINS(label, needle) do { \ - tests_run++; \ - if (!inferno_source_bundle_contains((needle))) { \ - tests_passed++; \ - } else { \ - tests_failed++; \ - printf(" FAIL: %s - unexpected \"%s\" in Inferno source bundle\n", \ - (label), (needle)); \ - } \ -} while (0) #define ASSERT_STR_EQ(label, actual, expected) do { \ tests_run++; \ @@ -250,8 +53,10 @@ static int inferno_source_bundle_contains(const char* needle) { } \ } while (0) +static InfernoContext test_context; + static void reset_test_context(void) { - InfernoContext* ctx = inf_legacy_context(); + InfernoContext* ctx = &test_context; ctx->config = inf_default_config(); ctx->collision_map = NULL; ctx->world_offset_x = 0; @@ -274,7 +79,7 @@ static InfernoState make_test_state(int player_x, int player_y) { } static InfConfig* test_config(void) { - return &inf_legacy_context()->config; + return &test_context.config; } static float test_supply_milestone_surplus_reward( @@ -282,7 +87,7 @@ static float test_supply_milestone_surplus_reward( int public_wave ) { return inf_supply_milestone_surplus_reward( - state, inf_legacy_context(), public_wave); + state, &test_context, public_wave); } static InfNPC make_test_npc(InfNPCType type, int x, int y, int size) { @@ -359,11 +164,11 @@ static void init_spell_cast_test_state(InfernoState* state, InfNPCType target_ty target_type, 16, 10, INF_NPC_STATS[target_type].size); state->npcs[0].active = 1; state->npcs[0].hp = state->npcs[0].max_hp = INF_NPC_STATS[target_type].hp; - inf_refresh_current_obs_slots(state); + inf_refresh_current_obs_slots_ctx(state, &test_context); } static int inf_action_target_for_npc(InfernoState* state, int npc_slot) { - inf_refresh_current_obs_slots(state); + inf_refresh_current_obs_slots_ctx(state, &test_context); int target_slot = inf_find_target_obs_slot(state, npc_slot); ASSERT_INT_EQ("target NPC has observation slot", target_slot >= 0, 1); if (target_slot < 0) return 0; @@ -387,7 +192,7 @@ static void fire_player_action_at_slot_zero( actions[INF_HEAD_SPELL] = spell_action; state->player.attack_timer = 0; encounter_pending_hit_queue_clear(&state->npcs[0].pending_hits); - inf_tick_player(state, actions, 1); + inf_tick_player_ctx(state, &test_context, actions, 1); } static int inferno_pending_hit_obs_start(void); @@ -454,13 +259,13 @@ static void step_inferno_with_prayer(InfernoState* state, int prayer_action) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRAYER] = prayer_action; - inf_step((EncounterState*)state, actions); + inf_step_ctx((EncounterState*)state, (EncounterContext*)&test_context, actions); } static void step_inferno_noop(InfernoState* state) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - inf_step((EncounterState*)state, actions); + inf_step_ctx((EncounterState*)state, (EncounterContext*)&test_context, actions); } static int find_active_npc_type(const InfernoState* state, InfNPCType type) { @@ -503,9 +308,9 @@ static int force_mager_resurrect(InfernoState* s, int idx) { for (uint32_t seed = 1; seed < 100000; seed++) { InfernoState probe = *s; probe.rng_state = seed; - if (inf_mager_resurrect(&probe, idx)) { + if (inf_mager_resurrect_ctx(&probe, &test_context, idx)) { s->rng_state = seed; - return inf_mager_resurrect(s, idx); + return inf_mager_resurrect_ctx(s, &test_context, idx); } } return 0; @@ -515,11 +320,11 @@ static int force_mager_attack_resurrection(InfernoState* s, int idx) { for (uint32_t seed = 1; seed < 100000; seed++) { InfernoState probe = *s; probe.rng_state = seed; - inf_npc_attack(&probe, idx); + inf_npc_attack_ctx(&probe, &test_context, idx); if (probe.dead_mob_count == 0 && count_active_npc_type(&probe, INF_NPC_RANGER) > 0) { s->rng_state = seed; - inf_npc_attack(s, idx); + inf_npc_attack_ctx(s, &test_context, idx); return 1; } } @@ -542,12 +347,12 @@ static int test_profiled_supply_count(int full_doses, float profile_fraction, fl static void reset_inferno_at_public_wave(EncounterState* raw_state, int public_wave, float supply_profile_scale) { - inf_put_int(raw_state, "start_wave", public_wave); - inf_put_float(raw_state, "damage_reward_coeff", 0.01f); - inf_put_float(raw_state, "shield_penalty_coeff", 0.01f); - inf_put_float(raw_state, "tag_reward_coeff", 0.25f); - inf_put_float(raw_state, "late_start_supply_profile_scale", supply_profile_scale); - inf_reset(raw_state, 123); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "start_wave", public_wave); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "shield_penalty_coeff", 0.01f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "tag_reward_coeff", 0.25f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "late_start_supply_profile_scale", supply_profile_scale); + inf_reset_ctx(raw_state, (EncounterContext*)&test_context, 123); } static void assert_supply_doses(const char* label, @@ -605,9 +410,9 @@ static void test_final_wave_reward_applies_healer_tags_and_heal_cost(void) { InfernoState healing_state = make_test_state(24, 24); InfernoState damage_state = make_test_state(24, 24); - inf_put_float((EncounterState*)&healing_state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&healing_state, "shield_penalty_coeff", 0.01f); - inf_put_float((EncounterState*)&healing_state, "tag_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&healing_state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&healing_state, (EncounterContext*)&test_context, "shield_penalty_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&healing_state, (EncounterContext*)&test_context, "tag_reward_coeff", 0.25f); healing_state.wave = INF_NUM_WAVES - 1; healing_state.tick_scratch.damage_dealt = 50.0f; healing_state.tick_scratch.hp_restored = 10.0f; @@ -627,11 +432,11 @@ static void test_final_wave_reward_applies_healer_tags_and_heal_cost(void) { damage_state.tick_scratch.healer_tags = 0; ASSERT_FLOAT_NEAR("active healer reward includes tags, heal cost, and shield penalty", - inf_compute_reward(&healing_state), 0.33f, 0.0001f); + inf_compute_reward_ctx(&healing_state, &test_context), 0.33f, 0.0001f); ASSERT_FLOAT_NEAR("active healer reward updates zuk low watermark", healing_state.min_zuk_hp_seen, 1150.0f, 0.0001f); ASSERT_FLOAT_NEAR("non-final-wave reward still uses damage path", - inf_compute_reward(&damage_state), 0.33f, 0.0001f); + inf_compute_reward_ctx(&damage_state, &test_context), 0.33f, 0.0001f); } static void test_final_wave_reward_uses_zuk_low_watermark_progress(void) { @@ -639,9 +444,9 @@ static void test_final_wave_reward_uses_zuk_low_watermark_progress(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "shield_penalty_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "shield_penalty_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "tag_reward_coeff", 0.25f); state.wave = INF_NUM_WAVES - 1; state.min_zuk_hp_seen = 1200.0f; state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); @@ -655,7 +460,7 @@ static void test_final_wave_reward_uses_zuk_low_watermark_progress(void) { state.tick_scratch.hp_restored = 100.0f; state.tick_scratch.shield_damage = 7.0f; ASSERT_FLOAT_NEAR("first zuk low watermark pays progress minus shield penalty", - inf_compute_reward(&state), 0.43f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.43f, 0.0001f); ASSERT_FLOAT_NEAR("first zuk low watermark updates state", state.min_zuk_hp_seen, 1150.0f, 0.0001f); @@ -663,27 +468,27 @@ static void test_final_wave_reward_uses_zuk_low_watermark_progress(void) { state.tick_scratch.hp_restored = 0.0f; state.tick_scratch.shield_damage = 0.0f; ASSERT_FLOAT_NEAR("repeated hits at same zuk hp give zero reward", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); ASSERT_FLOAT_NEAR("same-hp hits keep low watermark", state.min_zuk_hp_seen, 1150.0f, 0.0001f); state.tick_scratch.damage_dealt = 600.0f; state.npcs[0].hp = 1180; ASSERT_FLOAT_NEAR("healed zuk above low watermark gives zero reward", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); ASSERT_FLOAT_NEAR("healed zuk does not revoke low watermark", state.min_zuk_hp_seen, 1150.0f, 0.0001f); state.tick_scratch.damage_dealt = 900.0f; ASSERT_FLOAT_NEAR("non-zuk damage without new low watermark gives zero reward", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); ASSERT_FLOAT_NEAR("non-zuk damage leaves low watermark unchanged", state.min_zuk_hp_seen, 1150.0f, 0.0001f); state.npcs[0].hp = 1140; state.tick_scratch.damage_dealt = 50.0f; ASSERT_FLOAT_NEAR("new lower zuk hp pays only incremental progress", - inf_compute_reward(&state), 0.10f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.10f, 0.0001f); ASSERT_FLOAT_NEAR("new lower zuk hp refreshes low watermark", state.min_zuk_hp_seen, 1140.0f, 0.0001f); } @@ -693,9 +498,9 @@ static void test_final_wave_reward_blocks_zuk_damage_while_healers_heal(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "shield_penalty_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "shield_penalty_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "tag_reward_coeff", 0.25f); state.wave = INF_NUM_WAVES - 1; state.min_zuk_hp_seen = 240.0f; state.zuk.healer_spawned = 1; @@ -712,7 +517,7 @@ static void test_final_wave_reward_blocks_zuk_damage_while_healers_heal(void) { state.min_zuk_hp_seen = 245.0f; state.npcs[0].hp = 235; ASSERT_FLOAT_NEAR("zuk threshold-crossing hit pays once after healer spawn", - inf_compute_reward(&state), 0.10f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.10f, 0.0001f); ASSERT_FLOAT_NEAR("threshold-crossing hit updates low watermark", state.min_zuk_hp_seen, 235.0f, 0.0001f); @@ -720,7 +525,7 @@ static void test_final_wave_reward_blocks_zuk_damage_while_healers_heal(void) { state.npcs[0].hp = 220; ASSERT_FLOAT_NEAR("zuk progress pays nothing while a healer heals zuk", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); ASSERT_FLOAT_NEAR("low watermark still tracks observed zuk hp", state.min_zuk_hp_seen, 220.0f, 0.0001f); @@ -728,7 +533,7 @@ static void test_final_wave_reward_blocks_zuk_damage_while_healers_heal(void) { state.npcs[1].aggro_target = -1; ASSERT_FLOAT_NEAR("zuk progress resumes after healers are tagged", - inf_compute_reward(&state), 0.20f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.20f, 0.0001f); } static void test_final_wave_reward_pays_zuk_healer_damage(void) { @@ -736,9 +541,9 @@ static void test_final_wave_reward_pays_zuk_healer_damage(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "shield_penalty_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "shield_penalty_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "tag_reward_coeff", 0.25f); state.wave = INF_NUM_WAVES - 1; state.min_zuk_hp_seen = 240.0f; state.zuk.healer_spawned = 1; @@ -755,7 +560,7 @@ static void test_final_wave_reward_pays_zuk_healer_damage(void) { state.npcs[1].aggro_target = -1; ASSERT_FLOAT_NEAR("zuk healer damage uses base damage reward", - inf_compute_reward(&state), 0.31f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.31f, 0.0001f); } static void test_post_healer_zuk_damage_reward_is_after_clear_only(void) { @@ -763,8 +568,8 @@ static void test_post_healer_zuk_damage_reward_is_after_clear_only(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "post_healer_zuk_damage_coeff", 0.005f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "post_healer_zuk_damage_coeff", 0.005f); state.wave = INF_NUM_WAVES - 1; state.min_zuk_hp_seen = 180.0f; state.zuk.healer_spawned = 1; @@ -777,12 +582,12 @@ static void test_post_healer_zuk_damage_reward_is_after_clear_only(void) { state.npcs[0].max_hp = 1200; ASSERT_FLOAT_NEAR("post-healer coeff does not pay before healer clear", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); state.tick_at_all_zuk_healers_dead = 321; state.min_zuk_hp_seen = 180.0f; ASSERT_FLOAT_NEAR("post-healer coeff pays healed-back Zuk damage after clear", - inf_compute_reward(&state), 0.25f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.25f, 0.0001f); ASSERT_FLOAT_NEAR("post-healer damage reward does not revoke low watermark", state.min_zuk_hp_seen, 180.0f, 0.0001f); } @@ -792,7 +597,7 @@ static void test_zuk_healer_phase_hp_delta_default_preserves_low_watermark(void) InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); state.wave = INF_NUM_WAVES - 1; state.zuk.healer_spawned = 1; state.min_zuk_hp_seen = 180.0f; @@ -804,7 +609,7 @@ static void test_zuk_healer_phase_hp_delta_default_preserves_low_watermark(void) state.npcs[0].max_hp = 1200; ASSERT_FLOAT_NEAR("default coefficient preserves old no-pay healed-back Zuk hit", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); ASSERT_FLOAT_NEAR("default coefficient preserves low watermark", state.min_zuk_hp_seen, 180.0f, 0.0001f); } @@ -814,8 +619,8 @@ static void test_zuk_healer_phase_hp_delta_pays_healed_back_zuk_damage(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "zuk_healer_phase_hp_delta_coeff", 0.005f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_healer_phase_hp_delta_coeff", 0.005f); state.wave = INF_NUM_WAVES - 1; state.zuk.healer_spawned = 1; state.min_zuk_hp_seen = 180.0f; @@ -827,7 +632,7 @@ static void test_zuk_healer_phase_hp_delta_pays_healed_back_zuk_damage(void) { state.npcs[0].max_hp = 1200; ASSERT_FLOAT_NEAR("healer-phase HP delta pays current Zuk damage", - inf_compute_reward(&state), 0.25f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.25f, 0.0001f); ASSERT_FLOAT_NEAR("healed-back Zuk hit does not change low watermark", state.min_zuk_hp_seen, 180.0f, 0.0001f); } @@ -837,8 +642,8 @@ static void test_zuk_healer_phase_hp_delta_avoids_double_pay_below_low_watermark InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "zuk_healer_phase_hp_delta_coeff", 0.005f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_healer_phase_hp_delta_coeff", 0.005f); state.wave = INF_NUM_WAVES - 1; state.zuk.healer_spawned = 1; state.min_zuk_hp_seen = 245.0f; @@ -850,7 +655,7 @@ static void test_zuk_healer_phase_hp_delta_avoids_double_pay_below_low_watermark state.npcs[0].max_hp = 1200; ASSERT_FLOAT_NEAR("post-spawn low-watermark damage is paid once through HP delta", - inf_compute_reward(&state), 0.05f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.05f, 0.0001f); ASSERT_FLOAT_NEAR("low watermark still tracks metrics", state.min_zuk_hp_seen, 235.0f, 0.0001f); } @@ -860,8 +665,8 @@ static void test_zuk_healer_phase_hp_delta_penalizes_zuk_healing_once(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "zuk_healer_phase_hp_delta_coeff", 0.005f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_healer_phase_hp_delta_coeff", 0.005f); state.wave = INF_NUM_WAVES - 1; state.zuk.healer_spawned = 1; state.min_zuk_hp_seen = 180.0f; @@ -878,7 +683,7 @@ static void test_zuk_healer_phase_hp_delta_penalizes_zuk_healing_once(void) { state.npcs[1].aggro_target = 0; ASSERT_FLOAT_NEAR("Zuk heal penalty is not double-counted", - inf_compute_reward(&state), -0.20f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), -0.20f, 0.0001f); } static void test_zuk_healer_phase_hp_delta_pays_net_same_tick_delta(void) { @@ -886,8 +691,8 @@ static void test_zuk_healer_phase_hp_delta_pays_net_same_tick_delta(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "zuk_healer_phase_hp_delta_coeff", 0.005f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_healer_phase_hp_delta_coeff", 0.005f); state.wave = INF_NUM_WAVES - 1; state.zuk.healer_spawned = 1; state.min_zuk_hp_seen = 180.0f; @@ -906,7 +711,7 @@ static void test_zuk_healer_phase_hp_delta_pays_net_same_tick_delta(void) { state.npcs[1].aggro_target = 0; ASSERT_FLOAT_NEAR("same tick damage and healing use net Zuk HP delta", - inf_compute_reward(&state), 0.15f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.15f, 0.0001f); } static void test_zuk_healer_phase_hp_delta_keeps_non_zuk_heal_cost(void) { @@ -914,8 +719,8 @@ static void test_zuk_healer_phase_hp_delta_keeps_non_zuk_heal_cost(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "zuk_healer_phase_hp_delta_coeff", 0.005f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_healer_phase_hp_delta_coeff", 0.005f); state.wave = INF_NUM_WAVES - 1; state.zuk.healer_spawned = 1; state.min_zuk_hp_seen = 180.0f; @@ -936,7 +741,7 @@ static void test_zuk_healer_phase_hp_delta_keeps_non_zuk_heal_cost(void) { state.npcs[2].aggro_target = 1; ASSERT_FLOAT_NEAR("non-Zuk healing still uses generic heal cost", - inf_compute_reward(&state), -0.30f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), -0.30f, 0.0001f); } static void init_post_healer_set_reward_state(InfernoState* state) { @@ -959,7 +764,7 @@ static void test_post_healer_set_damage_reward_defaults_off(void) { state.tick_scratch.damage_set = 50.0f; ASSERT_FLOAT_NEAR("default post-healer set damage reward is off", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); } static void test_post_healer_set_damage_reward_pays_after_healer_clear(void) { @@ -967,12 +772,11 @@ static void test_post_healer_set_damage_reward_pays_after_healer_clear(void) { InfernoState state; init_post_healer_set_reward_state(&state); - inf_put_float((EncounterState*)&state, - "post_healer_set_damage_reward_coeff", 0.002f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "post_healer_set_damage_reward_coeff", 0.002f); state.tick_scratch.damage_set = 50.0f; ASSERT_FLOAT_NEAR("post-healer set damage is rewarded", - inf_compute_reward(&state), 0.10f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.10f, 0.0001f); } static void test_post_healer_set_kill_bonus_uses_existing_emitter(void) { @@ -980,11 +784,11 @@ static void test_post_healer_set_kill_bonus_uses_existing_emitter(void) { InfernoState state; init_post_healer_set_reward_state(&state); - inf_put_float((EncounterState*)&state, "post_healer_set_kill_bonus", 0.09f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "post_healer_set_kill_bonus", 0.09f); state.tick_scratch.kill_set = 1; ASSERT_FLOAT_NEAR("post-healer set kill bonus emits through set channel", - inf_compute_reward(&state), 0.03f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.03f, 0.0001f); ASSERT_FLOAT_NEAR("remaining post-healer set kill bonus is pending", state.pending_set_kill_bonus, 0.06f, 0.0001f); } @@ -994,10 +798,8 @@ static void test_post_healer_set_alive_penalty_caps_per_episode(void) { InfernoState state; init_post_healer_set_reward_state(&state); - inf_put_float((EncounterState*)&state, - "post_healer_set_alive_tick_penalty_coeff", 0.01f); - inf_put_float((EncounterState*)&state, - "post_healer_set_alive_penalty_cap", 0.03f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "post_healer_set_alive_tick_penalty_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "post_healer_set_alive_penalty_cap", 0.03f); state.post_healer_set_alive_penalty_total = 0.025f; state.npcs[1] = make_test_npc(INF_NPC_RANGER, 29, 36, 1); state.npcs[1].active = 1; @@ -1007,7 +809,7 @@ static void test_post_healer_set_alive_penalty_caps_per_episode(void) { state.npcs[2].hp = 70; ASSERT_FLOAT_NEAR("post-healer set alive penalty respects cap", - inf_compute_reward(&state), -0.005f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), -0.005f, 0.0001f); ASSERT_FLOAT_NEAR("post-healer set alive penalty total reaches cap", state.post_healer_set_alive_penalty_total, 0.03f, 0.0001f); } @@ -1031,7 +833,7 @@ static void test_zuk_untagged_healer_tick_penalty_defaults_off(void) { state.npcs[1].aggro_target = 0; ASSERT_FLOAT_NEAR("default untagged healer pressure is off", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); } static void test_zuk_untagged_healer_tick_penalty_counts_only_untagged_zuk_healers(void) { @@ -1039,8 +841,7 @@ static void test_zuk_untagged_healer_tick_penalty_counts_only_untagged_zuk_heale InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, - "zuk_untagged_healer_tick_penalty_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_untagged_healer_tick_penalty_coeff", 0.01f); state.wave = INF_NUM_WAVES - 1; state.zuk.healer_spawned = 1; state.min_zuk_hp_seen = 180.0f; @@ -1069,7 +870,7 @@ static void test_zuk_untagged_healer_tick_penalty_counts_only_untagged_zuk_heale state.npcs[4].aggro_target = 3; ASSERT_FLOAT_NEAR("only the untagged Zuk healer creates pressure", - inf_compute_reward(&state), -0.01f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), -0.01f, 0.0001f); } static void test_zuk_untagged_healer_target_bonus_defaults_off(void) { @@ -1086,19 +887,19 @@ static void test_zuk_untagged_healer_target_bonus_defaults_off(void) { state.npcs[2].aggro_target = 0; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(&state, 2); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("default target bonus pays no per-tick target reward", state.tick_scratch.zuk_untagged_healer_targets, 0); ASSERT_INT_EQ("default target bonus still records target attempts", state.total_zuk_untagged_healer_targets, 1); ASSERT_FLOAT_NEAR("default target bonus pays nothing", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); } static void test_zuk_untagged_healer_target_bonus_rewards_distinct_healers(void) { @@ -1108,8 +909,7 @@ static void test_zuk_untagged_healer_target_bonus_rewards_distinct_healers(void) init_zuk_timing_state(&state); state.zuk.healer_spawned = 1; state.min_zuk_hp_seen = 180.0f; - inf_put_float((EncounterState*)&state, - "zuk_untagged_healer_target_bonus_coeff", 0.07f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_untagged_healer_target_bonus_coeff", 0.07f); state.npcs[2] = make_test_npc( INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); @@ -1123,7 +923,7 @@ static void test_zuk_untagged_healer_target_bonus_rewards_distinct_healers(void) state.npcs[3].aggro_target = 0; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int first_healer_slot = inf_find_target_obs_slot(&state, 2); int second_healer_slot = inf_find_target_obs_slot(&state, 3); ASSERT_INT_EQ("first healer has an observation slot", @@ -1138,24 +938,24 @@ static void test_zuk_untagged_healer_target_bonus_rewards_distinct_healers(void) int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(first_healer_slot); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("first untagged healer target rewarded", state.tick_scratch.zuk_untagged_healer_targets, 1); ASSERT_INT_EQ("first untagged healer target reward count", state.total_zuk_untagged_healer_target_rewards, 1); ASSERT_FLOAT_NEAR("first untagged healer target reward", - inf_compute_reward(&state), 0.07f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.07f, 0.0001f); state.tick_scratch.zuk_untagged_healer_targets = 0; actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(first_healer_slot); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("repeat target does not reward twice", state.tick_scratch.zuk_untagged_healer_targets, 0); actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(second_healer_slot); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("second distinct untagged healer target rewarded", state.tick_scratch.zuk_untagged_healer_targets, 1); @@ -1170,8 +970,7 @@ static void test_zuk_safe_untagged_healer_target_bonus_records_safe_subset(void) init_zuk_timing_state(&state); state.zuk.healer_spawned = 1; state.min_zuk_hp_seen = 180.0f; - inf_put_float((EncounterState*)&state, - "zuk_safe_untagged_healer_target_bonus_coeff", 0.11f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_safe_untagged_healer_target_bonus_coeff", 0.11f); state.npcs[2] = make_test_npc( INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); @@ -1180,12 +979,12 @@ static void test_zuk_safe_untagged_healer_target_bonus_records_safe_subset(void) state.npcs[2].aggro_target = 0; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(&state, 2); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("safe untagged healer target attempt", state.total_zuk_untagged_healer_targets, 1); @@ -1198,7 +997,7 @@ static void test_zuk_safe_untagged_healer_target_bonus_records_safe_subset(void) ASSERT_INT_EQ("safe untagged healer per-tick reward event", state.tick_scratch.zuk_safe_untagged_healer_targets, 1); ASSERT_FLOAT_NEAR("safe untagged healer target reward", - inf_compute_reward(&state), 0.11f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.11f, 0.0001f); } static void test_zuk_untagged_healer_target_bonus_excludes_tagged_healers(void) { @@ -1207,8 +1006,7 @@ static void test_zuk_untagged_healer_target_bonus_excludes_tagged_healers(void) InfernoState state; init_zuk_timing_state(&state); state.zuk.healer_spawned = 1; - inf_put_float((EncounterState*)&state, - "zuk_untagged_healer_target_bonus_coeff", 0.07f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_untagged_healer_target_bonus_coeff", 0.07f); state.npcs[2] = make_test_npc( INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); @@ -1217,12 +1015,12 @@ static void test_zuk_untagged_healer_target_bonus_excludes_tagged_healers(void) state.npcs[2].aggro_target = -1; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(&state, 2); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("already tagged healer target gets no bonus", state.tick_scratch.zuk_untagged_healer_targets, 0); @@ -1235,10 +1033,10 @@ static void test_zuk_healer_tags_first_reward_mode_blocks_pre_tag_damage(void) { state.wave = INF_NUM_WAVES - 1; state.zuk.healer_spawned = 1; test_config()->zuk_healer_reward_mode = 1; - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); - inf_put_float((EncounterState*)&state, "zuk_healer_damage_reward_coeff", 0.02f); - inf_put_float((EncounterState*)&state, "zuk_healer_kill_bonus", 0.30f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "tag_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_healer_damage_reward_coeff", 0.02f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_healer_kill_bonus", 0.30f); state.min_zuk_hp_seen = 300.0f; state.total_zuk_healer_tags = 2; state.tick_scratch.healer_tags = 1; @@ -1252,7 +1050,7 @@ static void test_zuk_healer_tags_first_reward_mode_blocks_pre_tag_damage(void) { state.npcs[0].max_hp = 1200; ASSERT_FLOAT_NEAR("only healer tag reward is paid before all tags", - inf_compute_reward(&state), 0.25f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.25f, 0.0001f); ASSERT_FLOAT_NEAR("pre-tag kill bonus is not delayed", state.pending_zuk_healer_kill_bonus, 0.0f, 0.0001f); ASSERT_FLOAT_NEAR("low watermark still tracks for metrics", @@ -1266,9 +1064,9 @@ static void test_zuk_healer_tags_first_reward_mode_resumes_after_all_tags(void) state.wave = INF_NUM_WAVES - 1; state.zuk.healer_spawned = 1; test_config()->zuk_healer_reward_mode = 1; - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "zuk_healer_damage_reward_coeff", 0.02f); - inf_put_float((EncounterState*)&state, "zuk_healer_kill_bonus", 0.30f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_healer_damage_reward_coeff", 0.02f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_healer_kill_bonus", 0.30f); state.min_zuk_hp_seen = 300.0f; state.total_zuk_healer_tags = 4; state.tick_scratch.damage_zuk = 80.0f; @@ -1280,7 +1078,7 @@ static void test_zuk_healer_tags_first_reward_mode_resumes_after_all_tags(void) state.npcs[0].max_hp = 1200; ASSERT_FLOAT_NEAR("damage and kill rewards resume after all tags", - inf_compute_reward(&state), 1.15f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 1.15f, 0.0001f); } static void test_joseph_reward_mode_pays_tags_while_healers_heal(void) { @@ -1290,8 +1088,8 @@ static void test_joseph_reward_mode_pays_tags_while_healers_heal(void) { state.wave = INF_NUM_WAVES - 1; test_config()->joseph_reward_mode = 1; state.min_zuk_hp_seen = 300.0f; - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.50f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "tag_reward_coeff", 0.50f); state.tick_scratch.damage_dealt = 70.0f; state.tick_scratch.damage_zuk = 70.0f; state.tick_scratch.healer_tags = 1; @@ -1305,7 +1103,7 @@ static void test_joseph_reward_mode_pays_tags_while_healers_heal(void) { state.npcs[1].aggro_target = 0; ASSERT_FLOAT_NEAR("only tag reward is paid while a Zuk healer heals", - inf_compute_reward(&state), 0.50f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.50f, 0.0001f); ASSERT_FLOAT_NEAR("Joseph mode still tracks Zuk low watermark", state.min_zuk_hp_seen, 220.0f, 0.0001f); } @@ -1316,10 +1114,9 @@ static void test_zuk_healer_attack_shape_reward_applies_in_joseph_mode(void) { InfernoState state = make_test_state(24, 24); state.wave = INF_NUM_WAVES - 1; test_config()->joseph_reward_mode = 1; - inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.50f); - inf_put_float((EncounterState*)&state, - "zuk_untagged_healer_nonmagic_attack_bonus_coeff", 0.07f); - inf_put_float((EncounterState*)&state, "zuk_healer_mage_attack_penalty_coeff", 0.04f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "tag_reward_coeff", 0.50f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_untagged_healer_nonmagic_attack_bonus_coeff", 0.07f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "zuk_healer_mage_attack_penalty_coeff", 0.04f); state.tick_scratch.healer_tags = 1; state.tick_scratch.zuk_untagged_healer_nonmagic_attacks = 2; state.tick_scratch.zuk_healer_mage_attack_fires = 1; @@ -1333,46 +1130,46 @@ static void test_zuk_healer_attack_shape_reward_applies_in_joseph_mode(void) { state.npcs[1].aggro_target = 0; ASSERT_FLOAT_NEAR("tag reward is shaped by healer attack style", - inf_compute_reward(&state), 0.60f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.60f, 0.0001f); } static void test_offensive_prayer_reward_shapes_normal_and_joseph_mode(void) { printf("--- offensive prayer reward shapes normal and Joseph mode ---\n"); InfernoState normal = make_test_state(24, 24); - inf_put_float((EncounterState*)&normal, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&normal, "offensive_prayer_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&normal, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&normal, (EncounterContext*)&test_context, "offensive_prayer_reward_coeff", 0.25f); normal.tick_scratch.damage_dealt = 40.0f; normal.tick_scratch.offensive_prayer_correct_damage_roll = 40.0f; ASSERT_FLOAT_NEAR("normal reward multiplies correct offensive prayer damage", - inf_compute_reward(&normal), 0.50f, 0.0001f); + inf_compute_reward_ctx(&normal, &test_context), 0.50f, 0.0001f); InfernoState wrong = make_test_state(24, 24); - inf_put_float((EncounterState*)&wrong, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&wrong, "offensive_prayer_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&wrong, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&wrong, (EncounterContext*)&test_context, "offensive_prayer_reward_coeff", 0.25f); wrong.tick_scratch.damage_dealt = 40.0f; ASSERT_FLOAT_NEAR("wrong offensive prayer receives base damage reward only", - inf_compute_reward(&wrong), 0.40f, 0.0001f); + inf_compute_reward_ctx(&wrong, &test_context), 0.40f, 0.0001f); InfernoState zero = make_test_state(24, 24); - inf_put_float((EncounterState*)&zero, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&zero, "offensive_prayer_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&zero, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&zero, (EncounterContext*)&test_context, "offensive_prayer_reward_coeff", 0.25f); zero.tick_scratch.offensive_prayer_correct = 1; ASSERT_FLOAT_NEAR("correct offensive prayer without damage receives no shape", - inf_compute_reward(&zero), 0.0f, 0.0001f); + inf_compute_reward_ctx(&zero, &test_context), 0.0f, 0.0001f); InfernoState joseph = make_test_state(24, 24); test_config()->joseph_reward_mode = 1; - inf_put_float((EncounterState*)&joseph, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&joseph, "offensive_prayer_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&joseph, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&joseph, (EncounterContext*)&test_context, "offensive_prayer_reward_coeff", 0.25f); joseph.tick_scratch.damage_dealt = 40.0f; joseph.tick_scratch.offensive_prayer_correct_damage_roll = 40.0f; ASSERT_FLOAT_NEAR("Joseph reward multiplies correct offensive prayer damage", - inf_compute_reward(&joseph), 0.50f, 0.0001f); + inf_compute_reward_ctx(&joseph, &test_context), 0.50f, 0.0001f); } static void init_ranged_offensive_prayer_test_state(InfernoState* state) { @@ -1386,7 +1183,7 @@ static void init_ranged_offensive_prayer_test_state(InfernoState* state) { encounter_compute_loadout_stats(INF_MAX_RANGE_FAST_LOADOUT, ATTACK_STYLE_RANGED, state->player.offensive_prayer, 99, FIGHT_STYLE_RAPID, 0, &state->loadout_stats[INF_GEAR_BP]); - inf_refresh_current_obs_slots(state); + inf_refresh_current_obs_slots_ctx(state, &test_context); } static void test_offensive_prayer_attack_events_count_real_attacks(void) { @@ -1445,7 +1242,7 @@ static void test_offensive_prayer_barrage_aoe_counts_once(void) { state.npcs[1] = make_test_npc(INF_NPC_NIBBLER, 17, 10, INF_NPC_STATS[INF_NPC_NIBBLER].size); state.npcs[1].active = 1; state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_NIBBLER].hp; - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); fire_player_action_at_slot_zero(&state, 1); @@ -1467,7 +1264,7 @@ static void test_offensive_prayer_no_attack_no_event(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(&state, 0); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("cooldown prevents attack", state.tick_scratch.player_attacked, 0); ASSERT_INT_EQ("cooldown produces no offensive prayer event", @@ -1529,12 +1326,6 @@ static void test_idle_diagnostics_count_missed_attack_opportunities(void) { state.npcs[0].hp = INF_NPC_STATS[INF_NPC_RANGER].hp; state.npcs[0].attack_timer = 5; - ASSERT_INT_EQ("target exists", - inf_has_live_player_target(&state), 1); - ASSERT_INT_EQ("target can be attacked", - inf_has_attackable_player_target(&state), 1); - ASSERT_INT_EQ("no immediate threat", - inf_player_has_immediate_threat(&state), 0); inf_record_idle_diagnostics( &state, 1, 1, INF_IDLE_PHASE_SET, 1, 1, 1); @@ -1561,7 +1352,8 @@ static void test_idle_diagnostics_phase_split(void) { InfernoState set = make_test_state(10, 10); set.wave = 20; ASSERT_INT_EQ("ordinary waves use set phase", - inf_idle_diagnostic_phase(&set), INF_IDLE_PHASE_SET); + inf_idle_diagnostic_phase_from_summary( + &set, inf_idle_diagnostic_summary(&set)), INF_IDLE_PHASE_SET); InfernoState jad = make_test_state(10, 10); jad.wave = 66; @@ -1570,33 +1362,38 @@ static void test_idle_diagnostics_phase_split(void) { jad.npcs[0].active = 1; jad.npcs[0].hp = INF_NPC_STATS[INF_NPC_JAD].hp; ASSERT_INT_EQ("non-final live jad uses jad phase", - inf_idle_diagnostic_phase(&jad), INF_IDLE_PHASE_JAD); + inf_idle_diagnostic_phase_from_summary( + &jad, inf_idle_diagnostic_summary(&jad)), INF_IDLE_PHASE_JAD); InfernoState zuk = make_test_state(25, 42); zuk.wave = INF_WAVE_ZUK; zuk.tick_at_all_zuk_healers_dead = -1; ASSERT_INT_EQ("final wave before jad uses zuk pre-jad phase", - inf_idle_diagnostic_phase(&zuk), INF_IDLE_PHASE_ZUK_PRE_JAD); + inf_idle_diagnostic_phase_from_summary( + &zuk, inf_idle_diagnostic_summary(&zuk)), INF_IDLE_PHASE_ZUK_PRE_JAD); zuk.npcs[0] = make_test_npc( INF_NPC_JAD, 24, 44, INF_NPC_STATS[INF_NPC_JAD].size); zuk.npcs[0].active = 1; zuk.npcs[0].hp = INF_NPC_STATS[INF_NPC_JAD].hp; ASSERT_INT_EQ("final wave live jad uses zuk jad phase", - inf_idle_diagnostic_phase(&zuk), INF_IDLE_PHASE_ZUK_JAD); + inf_idle_diagnostic_phase_from_summary( + &zuk, inf_idle_diagnostic_summary(&zuk)), INF_IDLE_PHASE_ZUK_JAD); zuk.npcs[1] = make_test_npc( INF_NPC_HEALER_ZUK, 22, 44, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); zuk.npcs[1].active = 1; zuk.npcs[1].hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; ASSERT_INT_EQ("live zuk healer uses zuk healer phase", - inf_idle_diagnostic_phase(&zuk), INF_IDLE_PHASE_ZUK_HEALERS); + inf_idle_diagnostic_phase_from_summary( + &zuk, inf_idle_diagnostic_summary(&zuk)), INF_IDLE_PHASE_ZUK_HEALERS); zuk.npcs[0].active = 0; zuk.npcs[1].active = 0; zuk.tick_at_all_zuk_healers_dead = 500; ASSERT_INT_EQ("after healers dead uses post-healer phase", - inf_idle_diagnostic_phase(&zuk), INF_IDLE_PHASE_ZUK_POST_HEALERS); + inf_idle_diagnostic_phase_from_summary( + &zuk, inf_idle_diagnostic_summary(&zuk)), INF_IDLE_PHASE_ZUK_POST_HEALERS); } static void test_joseph_reward_mode_damps_healed_zuk_damage(void) { @@ -1605,7 +1402,7 @@ static void test_joseph_reward_mode_damps_healed_zuk_damage(void) { InfernoState state = make_test_state(24, 24); state.wave = INF_NUM_WAVES - 1; test_config()->joseph_reward_mode = 1; - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); state.tick_scratch.damage_dealt = 100.0f; state.tick_scratch.damage_zuk = 100.0f; state.total_hp_restored_zuk = 1200.0f; @@ -1615,7 +1412,7 @@ static void test_joseph_reward_mode_damps_healed_zuk_damage(void) { state.npcs[0].max_hp = 1200; ASSERT_FLOAT_NEAR("Zuk damage is downweighted by prior healing", - inf_compute_reward(&state), 0.20f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.20f, 0.0001f); } static void test_jad_damage_reward_pauses_while_jad_healers_heal(void) { @@ -1623,9 +1420,9 @@ static void test_jad_damage_reward_pauses_while_jad_healers_heal(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "shield_penalty_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "shield_penalty_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "tag_reward_coeff", 0.25f); state.wave = 66; state.tick_scratch.damage_dealt = 40.0f; state.tick_scratch.damage_jad = 40.0f; @@ -1641,12 +1438,12 @@ static void test_jad_damage_reward_pauses_while_jad_healers_heal(void) { state.npcs[1].aggro_target = 0; ASSERT_FLOAT_NEAR("jad damage pays nothing while a healer heals jad", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); state.npcs[1].aggro_target = -1; ASSERT_FLOAT_NEAR("jad damage resumes after the healer is tagged", - inf_compute_reward(&state), 0.40f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.40f, 0.0001f); } static void test_jad_healer_damage_never_gets_damage_reward(void) { @@ -1654,9 +1451,9 @@ static void test_jad_healer_damage_never_gets_damage_reward(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "shield_penalty_coeff", 0.01f); - inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "damage_reward_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "shield_penalty_coeff", 0.01f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "tag_reward_coeff", 0.25f); state.wave = 66; state.tick_scratch.damage_dealt = 40.0f; state.tick_scratch.damage_jad_healers = 40.0f; @@ -1667,7 +1464,7 @@ static void test_jad_healer_damage_never_gets_damage_reward(void) { state.npcs[0].aggro_target = -1; ASSERT_FLOAT_NEAR("jad healer damage is not rewarded", - inf_compute_reward(&state), 0.0f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.0f, 0.0001f); } static void test_shield_tag_reward_excludes_zuk(void) { @@ -1675,7 +1472,7 @@ static void test_shield_tag_reward_excludes_zuk(void) { InfernoState state = make_test_state(24, 24); - inf_put_float((EncounterState*)&state, "shield_tag_reward_coeff", 0.20f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "shield_tag_reward_coeff", 0.20f); state.npcs[0] = make_test_npc(INF_NPC_ZUK_SHIELD, 23, 44, 1); state.npcs[0].active = 1; state.npcs[0].hp = 100; @@ -1720,7 +1517,7 @@ static void test_shield_tag_reward_excludes_zuk(void) { state.tick_scratch.shield_tags = 3; ASSERT_FLOAT_NEAR("shield tag reward pays per valid shield tag", - inf_compute_reward(&state), 0.60f, 0.0001f); + inf_compute_reward_ctx(&state, &test_context), 0.60f, 0.0001f); } static void test_inferno_reset_supplies_match_current_inventory(void) { @@ -1816,7 +1613,7 @@ static void test_inferno_budget_profile_reset_uses_budget_gear(void) { EncounterState* raw_state = inf_create(); InfernoState* state = (InfernoState*)raw_state; - inf_put_int(raw_state, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); reset_inferno_at_public_wave(raw_state, 1, 1.0f); ASSERT_INT_EQ("budget active profile", @@ -1871,21 +1668,21 @@ static void test_inferno_mixed_profile_sampling_respects_fraction(void) { InfernoState* b = (InfernoState*)raw_b; InfernoState* c = (InfernoState*)raw_c; - inf_put_int(raw_a, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET); - inf_put_float(raw_a, "budget_loadout_fraction", 0.0f); - inf_reset(raw_a, 456u); + inf_put_int_ctx(raw_a, (EncounterContext*)&test_context, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET); + inf_put_float_ctx(raw_a, (EncounterContext*)&test_context, "budget_loadout_fraction", 0.0f); + inf_reset_ctx(raw_a, (EncounterContext*)&test_context, 456u); ASSERT_INT_EQ("zero fraction samples max", a->active_loadout_profile, INF_LOADOUT_PROFILE_MAX); - inf_put_int(raw_b, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET); - inf_put_float(raw_b, "budget_loadout_fraction", 1.0f); - inf_reset(raw_b, 456u); + inf_put_int_ctx(raw_b, (EncounterContext*)&test_context, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET); + inf_put_float_ctx(raw_b, (EncounterContext*)&test_context, "budget_loadout_fraction", 1.0f); + inf_reset_ctx(raw_b, (EncounterContext*)&test_context, 456u); ASSERT_INT_EQ("one fraction samples budget", b->active_loadout_profile, INF_LOADOUT_PROFILE_BUDGET); - inf_put_int(raw_c, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET); - inf_put_float(raw_c, "budget_loadout_fraction", 1.0f); - inf_reset(raw_c, 456u); + inf_put_int_ctx(raw_c, (EncounterContext*)&test_context, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET); + inf_put_float_ctx(raw_c, (EncounterContext*)&test_context, "budget_loadout_fraction", 1.0f); + inf_reset_ctx(raw_c, (EncounterContext*)&test_context, 456u); ASSERT_INT_EQ("same seed and fraction are deterministic", c->active_loadout_profile, b->active_loadout_profile); @@ -1901,15 +1698,15 @@ static void test_inferno_equip_actions_move_cells_and_sync_weapon_set(void) { InfernoState* state = (InfernoState*)raw_state; int actions[INF_NUM_ACTION_HEADS]; - inf_put_int(raw_state, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); - inf_reset(raw_state, 789u); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + inf_reset_ctx(raw_state, (EncounterContext*)&test_context, 789u); memset(actions, 0, sizeof(actions)); actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = test_cell_holding_item(state, ITEM_BOW_OF_FAERDHINEN) + 1; actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_BODY)] = test_cell_holding_item(state, ITEM_CRYSTAL_BODY) + 1; - inf_tick_player(state, actions, 1); + inf_tick_player_ctx(state, &test_context, actions, 1); ASSERT_INT_EQ("equipping bowfa syncs long-range weapon set", state->weapon_set, INF_GEAR_LONG_RANGE); ASSERT_INT_EQ("long range budget weapon", @@ -1930,7 +1727,7 @@ static void test_inferno_equip_actions_move_cells_and_sync_weapon_set(void) { memset(actions, 0, sizeof(actions)); actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = test_cell_holding_item(state, ITEM_TOXIC_BLOWPIPE) + 1; - inf_tick_player(state, actions, 1); + inf_tick_player_ctx(state, &test_context, actions, 1); ASSERT_INT_EQ("equipping blowpipe syncs fast-range weapon set", state->weapon_set, INF_GEAR_BP); ASSERT_INT_EQ("fast range budget weapon", @@ -1941,7 +1738,7 @@ static void test_inferno_equip_actions_move_cells_and_sync_weapon_set(void) { memset(actions, 0, sizeof(actions)); actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = test_cell_holding_item(state, ITEM_DRAGON_HUNTER_WAND) + 1; - inf_tick_player(state, actions, 1); + inf_tick_player_ctx(state, &test_context, actions, 1); ASSERT_INT_EQ("equipping wand syncs mage weapon set", state->weapon_set, INF_GEAR_MAGE); ASSERT_INT_EQ("mage budget weapon", @@ -1957,8 +1754,8 @@ static void test_inferno_gear_switch_cancels_entity_interaction(void) { InfernoState* state = (InfernoState*)raw_state; int actions[INF_NUM_ACTION_HEADS]; - inf_put_int(raw_state, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); - inf_reset(raw_state, 789u); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + inf_reset_ctx(raw_state, (EncounterContext*)&test_context, 789u); int npc_slot = -1; for (int i = 0; i < INF_MAX_NPCS; i++) { @@ -1972,7 +1769,7 @@ static void test_inferno_gear_switch_cancels_entity_interaction(void) { osrs_interaction_set(&state->interaction, npc_slot); memset(actions, 0, sizeof(actions)); actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = body_cell + 1; - inf_tick_player(state, actions, 1); + inf_tick_player_ctx(state, &test_context, actions, 1); ASSERT_INT_EQ("a click that equips nothing keeps interaction", osrs_interaction_active(&state->interaction), 1); @@ -1980,7 +1777,7 @@ static void test_inferno_gear_switch_cancels_entity_interaction(void) { memset(actions, 0, sizeof(actions)); actions[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = test_cell_holding_item(state, ITEM_BOW_OF_FAERDHINEN) + 1; - inf_tick_player(state, actions, 1); + inf_tick_player_ctx(state, &test_context, actions, 1); ASSERT_INT_EQ("gear switch equips the clicked weapon", state->player.equipped[GEAR_SLOT_WEAPON], ITEM_BOW_OF_FAERDHINEN); ASSERT_INT_EQ("gear switch clears interaction", @@ -1995,24 +1792,23 @@ static void test_inferno_reset_preserves_reward_config(void) { EncounterState* raw_state = inf_create(); InfernoState* state = (InfernoState*)raw_state; - inf_put_float(raw_state, "supply_milestone_brew_reward_coeff", 0.001f); - inf_put_float(raw_state, "supply_milestone_restore_reward_coeff", 0.002f); - inf_put_float(raw_state, "offensive_prayer_reward_coeff", 0.009f); - inf_put_float(raw_state, "post_healer_zuk_damage_coeff", 0.003f); - inf_put_float(raw_state, "zuk_healer_phase_hp_delta_coeff", 0.004f); - inf_put_float(raw_state, "zuk_untagged_healer_tick_penalty_coeff", 0.005f); - inf_put_float(raw_state, "zuk_untagged_healer_target_bonus_coeff", 0.006f); - inf_put_float(raw_state, - "zuk_untagged_healer_nonmagic_attack_bonus_coeff", 0.007f); - inf_put_float(raw_state, "zuk_healer_mage_attack_penalty_coeff", 0.008f); - inf_put_int(raw_state, "zuk_safe_untagged_healer_target_mask", 1); - inf_put_int(raw_state, "zuk_force_safe_untagged_healer_target_mask", 1); - inf_put_int(raw_state, "zuk_healer_reward_mode", 1); - inf_put_int(raw_state, "joseph_reward_mode", 1); - inf_put_int(raw_state, "terminal_penalty_enabled", 1); - inf_put_int(raw_state, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); - inf_put_float(raw_state, "budget_loadout_fraction", 1.0f); - inf_reset(raw_state, 123u); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "supply_milestone_brew_reward_coeff", 0.001f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "supply_milestone_restore_reward_coeff", 0.002f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "offensive_prayer_reward_coeff", 0.009f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "post_healer_zuk_damage_coeff", 0.003f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "zuk_healer_phase_hp_delta_coeff", 0.004f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "zuk_untagged_healer_tick_penalty_coeff", 0.005f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "zuk_untagged_healer_target_bonus_coeff", 0.006f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "zuk_untagged_healer_nonmagic_attack_bonus_coeff", 0.007f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "zuk_healer_mage_attack_penalty_coeff", 0.008f); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "zuk_safe_untagged_healer_target_mask", 1); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "zuk_force_safe_untagged_healer_target_mask", 1); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "zuk_healer_reward_mode", 1); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "joseph_reward_mode", 1); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "terminal_penalty_enabled", 1); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "budget_loadout_fraction", 1.0f); + inf_reset_ctx(raw_state, (EncounterContext*)&test_context, 123u); ASSERT_FLOAT_NEAR("supply milestone brew reward coefficient", test_config()->supply_milestone_brew_reward_coeff, 0.001f, 1e-6f); @@ -2184,24 +1980,22 @@ static void test_curriculum_supply_no_brew_is_curriculum_only(void) { EncounterState* raw_state = inf_create(); InfernoState* state = (InfernoState*)raw_state; - inf_put_int(raw_state, "curriculum_no_brew_mode", - INF_CURRICULUM_SUPPLY_MODE_ALL); - inf_put_float(raw_state, "curriculum_no_brew_frac", 1.0f); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_no_brew_mode", INF_CURRICULUM_SUPPLY_MODE_ALL); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_no_brew_frac", 1.0f); reset_inferno_at_public_wave(raw_state, 71, 1.0f); ASSERT_INT_EQ("normal start ignores curriculum no-brew", state->player.brew_doses, 9); - inf_put_int(raw_state, "curriculum_agent", 1); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_agent", 1); reset_inferno_at_public_wave(raw_state, 71, 1.0f); ASSERT_INT_EQ("curriculum start applies no-brew", state->player.brew_doses, 0); ASSERT_INT_EQ("curriculum no-brew leaves restores alone", state->player.restore_doses, 5); - inf_put_int(raw_state, "curriculum_agent", 0); - inf_put_int(raw_state, "curriculum_no_brew_mode", - INF_CURRICULUM_SUPPLY_MODE_OFF); - inf_put_float(raw_state, "curriculum_no_brew_frac", 0.0f); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_agent", 0); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_no_brew_mode", INF_CURRICULUM_SUPPLY_MODE_OFF); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_no_brew_frac", 0.0f); inf_destroy(raw_state); } @@ -2230,12 +2024,11 @@ static void test_curriculum_supply_jitter_clamps_to_inventory_bounds(void) { EncounterState* raw_state = inf_create(); InfernoState* state = (InfernoState*)raw_state; - inf_put_int(raw_state, "curriculum_agent", 1); - inf_put_int(raw_state, "curriculum_supply_jitter_mode", - INF_CURRICULUM_SUPPLY_MODE_ALL); - inf_put_float(raw_state, "curriculum_supply_shared_jitter", 1.0f); - inf_put_float(raw_state, "curriculum_supply_brew_jitter", 1.0f); - inf_put_float(raw_state, "curriculum_supply_restore_jitter", 1.0f); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_agent", 1); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_supply_jitter_mode", INF_CURRICULUM_SUPPLY_MODE_ALL); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_supply_shared_jitter", 1.0f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_supply_brew_jitter", 1.0f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_supply_restore_jitter", 1.0f); reset_inferno_at_public_wave(raw_state, 71, 1.0f); ASSERT_INT_EQ("jitter keeps brew nonnegative", @@ -2247,12 +2040,11 @@ static void test_curriculum_supply_jitter_clamps_to_inventory_bounds(void) { ASSERT_INT_EQ("jitter keeps restore within full supplies", state->player.restore_doses <= 36, 1); - inf_put_int(raw_state, "curriculum_agent", 0); - inf_put_int(raw_state, "curriculum_supply_jitter_mode", - INF_CURRICULUM_SUPPLY_MODE_OFF); - inf_put_float(raw_state, "curriculum_supply_shared_jitter", 0.0f); - inf_put_float(raw_state, "curriculum_supply_brew_jitter", 0.0f); - inf_put_float(raw_state, "curriculum_supply_restore_jitter", 0.0f); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_agent", 0); + inf_put_int_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_supply_jitter_mode", INF_CURRICULUM_SUPPLY_MODE_OFF); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_supply_shared_jitter", 0.0f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_supply_brew_jitter", 0.0f); + inf_put_float_ctx(raw_state, (EncounterContext*)&test_context, "curriculum_supply_restore_jitter", 0.0f); inf_destroy(raw_state); } @@ -2264,7 +2056,7 @@ static void test_late_start_supply_observations(void) { float obs[INF_NUM_OBS]; reset_inferno_at_public_wave(raw_state, 69, 1.0f); - inf_write_obs(raw_state, obs); + inf_write_obs_ctx(raw_state, (EncounterContext*)&test_context, obs); int observed_brew_doses = 0; int observed_restore_doses = 0; @@ -2336,9 +2128,9 @@ static void test_tagged_jad_healer_melee_geometry(void) { meleer_diagonal_state.npcs[0].active = 1; meleer_diagonal_state.npcs[0].aggro_target = -1; - inf_npc_attack(&diagonal_state, 0); - inf_npc_attack(&cardinal_state, 0); - inf_npc_attack(&meleer_diagonal_state, 0); + inf_npc_attack_ctx(&diagonal_state, &test_context, 0); + inf_npc_attack_ctx(&cardinal_state, &test_context, 0); + inf_npc_attack_ctx(&meleer_diagonal_state, &test_context, 0); ASSERT_INT_EQ("diagonal healer does not attack", diagonal_state.npcs[0].attacked_this_tick, 0); @@ -2366,7 +2158,7 @@ static void test_overlap_shuffle_hold_after_recent_target_click(void) { INF_NPC_RANGER, 20, 20, INF_NPC_STATS[INF_NPC_RANGER].size); state.npcs[0].active = 1; - inf_npc_move(&state, 0); + inf_npc_move_ctx(&state, &test_context, 0); ASSERT_INT_EQ("held overlap keeps x", state.npcs[0].x, 20); ASSERT_INT_EQ("held overlap keeps y", state.npcs[0].y, 20); @@ -2390,7 +2182,7 @@ static void test_overlap_shuffle_respects_npc_occupancy(void) { clear_state.npcs[3].active = 1; inf_rebuild_npc_collision_flags(&clear_state); - inf_npc_move(&clear_state, 0); + inf_npc_move_ctx(&clear_state, &test_context, 0); ASSERT_INT_EQ("clear sampled overlap shuffle moves west x", clear_state.npcs[0].x, 19); ASSERT_INT_EQ("clear sampled overlap shuffle moves west y", clear_state.npcs[0].y, 20); @@ -2408,7 +2200,7 @@ static void test_overlap_shuffle_respects_npc_occupancy(void) { blocked_state.npcs[3].active = 1; inf_rebuild_npc_collision_flags(&blocked_state); - inf_npc_move(&blocked_state, 0); + inf_npc_move_ctx(&blocked_state, &test_context, 0); ASSERT_INT_EQ("blocked sampled overlap shuffle does not fallback x", blocked_state.npcs[0].x, 20); ASSERT_INT_EQ("blocked sampled overlap shuffle does not fallback y", blocked_state.npcs[0].y, 20); @@ -2425,7 +2217,7 @@ static void test_large_npc_overlap_shuffle_can_partially_unclip(void) { INF_NPC_MAGER, 20, 20, INF_NPC_STATS[INF_NPC_MAGER].size); state.npcs[0].active = 1; - inf_npc_move(&state, 0); + inf_npc_move_ctx(&state, &test_context, 0); int dx = abs(state.npcs[0].x - 20); int dy = abs(state.npcs[0].y - 20); @@ -2433,41 +2225,6 @@ static void test_large_npc_overlap_shuffle_can_partially_unclip(void) { ASSERT_INT_EQ("large npc marks moved", state.npcs[0].moved_this_tick, 1); } -static void test_player_movement_ignores_npc_occupancy(void) { - printf("--- player movement ignores npc occupancy ---\n"); - - InfernoState state = make_test_state(18, 30); - state.player_dest_x = 26; - state.player_dest_y = 30; - - state.npcs[0] = make_test_npc(INF_NPC_HEALER_JAD, 19, 30, 1); - state.npcs[0].active = 1; - state.npcs[1] = make_test_npc( - INF_NPC_JAD, 20, 30, INF_NPC_STATS[INF_NPC_JAD].size); - state.npcs[1].active = 1; - - InfWalkCtx walk_ctx = { &state, inf_legacy_context() }; - ASSERT_INT_EQ("player can walk through jad healer tile", - inf_tile_walkable(&walk_ctx, 19, 30), 1); - ASSERT_INT_EQ("player can walk through jad footprint", - inf_tile_walkable(&walk_ctx, 20, 30), 1); - - int steps = encounter_move_toward_dest( - &state.player, &state.player_dest_x, &state.player_dest_y, - inf_legacy_context()->collision_map, - inf_legacy_context()->world_offset_x, - inf_legacy_context()->world_offset_y, - inf_tile_walkable, &walk_ctx, NULL, NULL, - INF_ARENA_MIN_X, INF_ARENA_MIN_Y, INF_ARENA_WIDTH, INF_ARENA_HEIGHT); - - ASSERT_INT_EQ("player runs through npc footprints", steps, 2); - ASSERT_INT_EQ("player reaches jad footprint x", state.player.x, 20); - ASSERT_INT_EQ("player reaches jad footprint y", state.player.y, 30); - - InfMoveCtx move_ctx = { &state, inf_legacy_context(), 0 }; - ASSERT_INT_EQ("npc movement still treats npc collision as blocked", - inf_npc_blocked(&move_ctx, 20, 30, 1), 1); -} static void test_tagged_jad_healer_stops_at_melee_contact(void) { printf("--- tagged jad healer stops at melee contact ---\n"); @@ -2477,7 +2234,7 @@ static void test_tagged_jad_healer_stops_at_melee_contact(void) { state.npcs[0].active = 1; state.npcs[0].aggro_target = -1; - inf_npc_move(&state, 0); + inf_npc_move_ctx(&state, &test_context, 0); ASSERT_INT_EQ("healer keeps melee contact x", state.npcs[0].x, 19); ASSERT_INT_EQ("healer keeps melee contact y", state.npcs[0].y, 20); @@ -2500,7 +2257,7 @@ static void test_tagged_jad_healers_queue_behind_front_healer(void) { state.npcs[i].attack_timer = 0; } - inf_tick_npcs(&state); + inf_tick_npcs_ctx(&state, &test_context); int attacks = 0; int on_player = 0; @@ -2530,7 +2287,7 @@ static void test_meleer_dig_can_stack_without_losing_collision_flag(void) { INF_NPC_RANGER, dig_x, dig_y, INF_NPC_STATS[INF_NPC_RANGER].size); state.npcs[1].active = 1; - inf_meleer_dig_check(&state, 0); + inf_meleer_dig_check_ctx(&state, &test_context, 0); ASSERT_INT_EQ("dig lands on first candidate x", state.npcs[0].x, dig_x); ASSERT_INT_EQ("dig lands on first candidate y", state.npcs[0].y, dig_y); @@ -2551,7 +2308,7 @@ static void test_jad_healer_spawn_offsets_match_wave_67_reference(void) { state.npcs[0].hp = 100; state.npcs[0].max_hp = 300; - inf_jad_check_healers(&state, 0); + inf_jad_check_healers_ctx(&state, &test_context, 0); int healers = 0; for (int i = 1; i < INF_MAX_NPCS; i++) { @@ -2585,7 +2342,7 @@ static void test_jad_healer_spawn_offsets_match_zuk_reference(void) { state.npcs[0].hp = 100; state.npcs[0].max_hp = 300; - inf_jad_check_healers(&state, 0); + inf_jad_check_healers_ctx(&state, &test_context, 0); int healers = 0; for (int i = 1; i < INF_MAX_NPCS; i++) { @@ -2614,15 +2371,15 @@ static void test_npc_terrain_blocks_full_footprint_lava_shelf(void) { ASSERT_INT_EQ("jad footprint y39 fits player arena", inf_npc_environment_blocked_ctx( - &state, inf_legacy_context(), + &state, &test_context, 24, 39, INF_NPC_STATS[INF_NPC_JAD].size), 0); ASSERT_INT_EQ("jad footprint y40 enters lava shelf", inf_npc_environment_blocked_ctx( - &state, inf_legacy_context(), + &state, &test_context, 24, 40, INF_NPC_STATS[INF_NPC_JAD].size), 1); ASSERT_INT_EQ("jad movement y40 is blocked", inf_npc_environment_blocked_ctx( - &state, inf_legacy_context(), + &state, &test_context, 24, 40, INF_NPC_STATS[INF_NPC_JAD].size), 1); } @@ -2637,7 +2394,7 @@ static void test_zuk_jad_healer_spawn_falls_back_to_passable_arena_tiles(void) { state.npcs[0].hp = 100; state.npcs[0].max_hp = 300; - inf_jad_check_healers(&state, 0); + inf_jad_check_healers_ctx(&state, &test_context, 0); int healers = 0; for (int i = 1; i < INF_MAX_NPCS; i++) { @@ -2652,7 +2409,7 @@ static void test_zuk_jad_healer_spawn_falls_back_to_passable_arena_tiles(void) { ASSERT_INT_EQ("fallback healer stays in arena", state.npcs[i].y <= INF_ARENA_MAX_Y, 1); ASSERT_INT_EQ("fallback healer terrain valid", inf_npc_environment_blocked_ctx( - &state, inf_legacy_context(), + &state, &test_context, state.npcs[i].x, state.npcs[i].y, 1), 0); ASSERT_INT_EQ("fallback healer outside jad footprint", encounter_entity_footprints_overlap( @@ -2676,7 +2433,7 @@ static void test_meleer_dig_landing_order(void) { state.pillars[0].x = 17; state.pillars[0].y = 17; - inf_meleer_dig_check(&state, 0); + inf_meleer_dig_check_ctx(&state, &test_context, 0); ASSERT_INT_EQ("blocked first landing candidate falls through to player tile x", state.npcs[0].x, 20); ASSERT_INT_EQ("blocked first landing candidate falls through to player tile y", state.npcs[0].y, 20); @@ -2685,149 +2442,6 @@ static void test_meleer_dig_landing_order(void) { ASSERT_INT_EQ("post-dig attack delay applied", state.npcs[0].dig_attack_delay, 6); } -typedef struct { - int xs[32]; - int ys[32]; - int count; -} ChaseBlockTest; - -static int chase_block_test_has_block(const ChaseBlockTest* t, int x, int y) { - for (int i = 0; i < t->count; i++) { - if (t->xs[i] == x && t->ys[i] == y) return 1; - } - return 0; -} - -static int chase_block_test_walkable(void* ctx, int x, int y) { - const ChaseBlockTest* t = (const ChaseBlockTest*)ctx; - return x >= 0 && x <= 10 && y >= 0 && y <= 10 && - !chase_block_test_has_block(t, x, y); -} - -static int chase_block_test_path_blocked(void* ctx, int x, int y) { - const ChaseBlockTest* t = (const ChaseBlockTest*)ctx; - return chase_block_test_has_block(t, x, y); -} - -static void chase_block_test_run_until_attackable( - Player* player, const ChaseBlockTest* blocks, - int target_x, int target_y, int target_size, int attack_range, - const OsrsLosQuery* los_query -) { - for (int i = 0; i < 12 && !encounter_player_can_attack(player->x, player->y, target_x, target_y, target_size, attack_range, NULL, 0, 0, los_query); i++) { - int moved = encounter_chase_attack_target( - player, - target_x, target_y, target_size, attack_range, - NULL, 0, 0, - chase_block_test_walkable, (void*)blocks, - chase_block_test_path_blocked, (void*)blocks, - los_query, - 0, 0, 11, 11); - ASSERT_INT_EQ("continued chase avoids blocked tiles", - chase_block_test_has_block(blocks, player->x, player->y), 0); - if (!moved) break; - } -} - -static void test_attack_chase_uses_reachable_approach_tile(void) { - printf("--- attack chase uses reachable approach tile ---\n"); - - ChaseBlockTest blocks = { - .xs = {1, 1, 1, 2, 2, 3, 3, 3}, - .ys = {4, 5, 6, 4, 6, 4, 5, 6}, - .count = 8, - }; - Player player; - memset(&player, 0, sizeof(player)); - player.x = 0; - player.y = 5; - OsrsLosQuery los_query = osrs_los_open(); - - int moved = encounter_chase_attack_target( - &player, - 5, 5, 1, 3, - NULL, 0, 0, - chase_block_test_walkable, &blocks, - chase_block_test_path_blocked, &blocks, - &los_query, - 0, 0, 11, 11); - - ASSERT_INT_EQ("chase moves despite sealed nearest attack tile", moved, 1); - ASSERT_INT_EQ("chase does not stay on start x", player.x == 0 && player.y == 5, 0); - ASSERT_INT_EQ("chase avoids blocked tiles", - chase_block_test_has_block(&blocks, player.x, player.y), 0); - - chase_block_test_run_until_attackable(&player, &blocks, 5, 5, 1, 3, &los_query); - ASSERT_INT_EQ("chase reaches a reachable attack tile", - encounter_player_can_attack(player.x, player.y, 5, 5, 1, 3, NULL, 0, 0, &los_query), 1); -} - -static void test_attack_chase_routes_around_los_blocker_while_in_range(void) { - printf("--- attack chase routes around LOS blocker while in range ---\n"); - - ChaseBlockTest blocks = { - .xs = {2}, - .ys = {0}, - .count = 1, - }; - LOSBlocker pillar = {2, 0, 1, LOS_FULL_MASK}; - OsrsLosQuery los_query = osrs_los_blockers(&pillar, 1); - Player player; - memset(&player, 0, sizeof(player)); - player.x = 0; - player.y = 0; - - ASSERT_INT_EQ("starting tile is range-valid but LOS-blocked", - encounter_player_can_attack(player.x, player.y, 5, 0, 1, 10, NULL, 0, 0, &los_query), 0); - - int moved = encounter_chase_attack_target( - &player, - 5, 0, 1, 10, - NULL, 0, 0, - chase_block_test_walkable, &blocks, - chase_block_test_path_blocked, &blocks, - &los_query, - 0, 0, 11, 11); - - ASSERT_INT_EQ("LOS-blocked in-range target causes movement", moved, 1); - ASSERT_INT_EQ("chase avoids LOS blocker tile", - chase_block_test_has_block(&blocks, player.x, player.y), 0); - chase_block_test_run_until_attackable(&player, &blocks, 5, 0, 1, 10, &los_query); - ASSERT_INT_EQ("chase reaches a clear long-range attack tile", - encounter_player_can_attack(player.x, player.y, 5, 0, 1, 10, NULL, 0, 0, &los_query), 1); -} - -static void test_attack_chase_routes_around_los_blocker_for_short_range(void) { - printf("--- attack chase routes around LOS blocker for short range ---\n"); - - ChaseBlockTest blocks = { - .xs = {2}, - .ys = {0}, - .count = 1, - }; - LOSBlocker pillar = {2, 0, 1, LOS_FULL_MASK}; - OsrsLosQuery los_query = osrs_los_blockers(&pillar, 1); - Player player; - memset(&player, 0, sizeof(player)); - player.x = 0; - player.y = 0; - - int moved = encounter_chase_attack_target( - &player, - 5, 0, 1, 3, - NULL, 0, 0, - chase_block_test_walkable, &blocks, - chase_block_test_path_blocked, &blocks, - &los_query, - 0, 0, 11, 11); - - ASSERT_INT_EQ("short-range LOS-blocked target causes movement", moved, 1); - ASSERT_INT_EQ("short-range chase avoids LOS blocker tile", - chase_block_test_has_block(&blocks, player.x, player.y), 0); - chase_block_test_run_until_attackable(&player, &blocks, 5, 0, 1, 3, &los_query); - ASSERT_INT_EQ("chase reaches a clear short-range attack tile", - encounter_player_can_attack(player.x, player.y, 5, 0, 1, 3, NULL, 0, 0, &los_query), 1); -} static void test_melee_fallback_geometry(void) { printf("--- inferno melee fallback geometry ---\n"); @@ -2849,105 +2463,38 @@ static void test_melee_fallback_geometry(void) { ASSERT_INT_EQ( "ranger diagonal melee fallback", - inf_melee_fallback_possible( - &diagonal_state, &ranger_diagonal, &ranged_stats, - ATTACK_STYLE_RANGED, distance_to_player(&diagonal_state, &ranger_diagonal)), + inf_melee_fallback_possible_at_tile((&diagonal_state)->player.x, (&diagonal_state)->player.y, &ranger_diagonal, &ranged_stats, ATTACK_STYLE_RANGED, distance_to_player(&diagonal_state, &ranger_diagonal)), 1); ASSERT_INT_EQ( "mager diagonal melee fallback", - inf_melee_fallback_possible( - &diagonal_state, &mager_diagonal, &magic_stats, - ATTACK_STYLE_MAGIC, distance_to_player(&diagonal_state, &mager_diagonal)), + inf_melee_fallback_possible_at_tile((&diagonal_state)->player.x, (&diagonal_state)->player.y, &mager_diagonal, &magic_stats, ATTACK_STYLE_MAGIC, distance_to_player(&diagonal_state, &mager_diagonal)), 1); ASSERT_INT_EQ( "blob diagonal melee fallback blocked", - inf_melee_fallback_possible( - &diagonal_state, &blob_diagonal, &magic_stats, - ATTACK_STYLE_MAGIC, distance_to_player(&diagonal_state, &blob_diagonal)), + inf_melee_fallback_possible_at_tile((&diagonal_state)->player.x, (&diagonal_state)->player.y, &blob_diagonal, &magic_stats, ATTACK_STYLE_MAGIC, distance_to_player(&diagonal_state, &blob_diagonal)), 0); ASSERT_INT_EQ( "blob cardinal melee fallback", - inf_melee_fallback_possible( - &cardinal_state, &blob_cardinal, &magic_stats, - ATTACK_STYLE_MAGIC, distance_to_player(&cardinal_state, &blob_cardinal)), + inf_melee_fallback_possible_at_tile((&cardinal_state)->player.x, (&cardinal_state)->player.y, &blob_cardinal, &magic_stats, ATTACK_STYLE_MAGIC, distance_to_player(&cardinal_state, &blob_cardinal)), 1); ASSERT_INT_EQ( "jad diagonal melee fallback blocked", - inf_melee_fallback_possible( - &diagonal_state, &jad_diagonal, &ranged_stats, - ATTACK_STYLE_RANGED, distance_to_player(&diagonal_state, &jad_diagonal)), + inf_melee_fallback_possible_at_tile((&diagonal_state)->player.x, (&diagonal_state)->player.y, &jad_diagonal, &ranged_stats, ATTACK_STYLE_RANGED, distance_to_player(&diagonal_state, &jad_diagonal)), 0); ASSERT_INT_EQ( "jad cardinal melee fallback", - inf_melee_fallback_possible( - &cardinal_state, &jad_cardinal, &ranged_stats, - ATTACK_STYLE_RANGED, distance_to_player(&cardinal_state, &jad_cardinal)), + inf_melee_fallback_possible_at_tile((&cardinal_state)->player.x, (&cardinal_state)->player.y, &jad_cardinal, &ranged_stats, ATTACK_STYLE_RANGED, distance_to_player(&cardinal_state, &jad_cardinal)), 1); ASSERT_INT_EQ( "fallback blocked outside melee distance", - inf_melee_fallback_possible( - &distant_state, &blob_distant, &magic_stats, - ATTACK_STYLE_MAGIC, distance_to_player(&distant_state, &blob_distant)), + inf_melee_fallback_possible_at_tile((&distant_state)->player.x, (&distant_state)->player.y, &blob_distant, &magic_stats, ATTACK_STYLE_MAGIC, distance_to_player(&distant_state, &blob_distant)), 0); ASSERT_INT_EQ( "fallback blocked when planned style already melee", - inf_melee_fallback_possible( - &cardinal_state, &blob_cardinal, &magic_stats, - ATTACK_STYLE_MELEE, distance_to_player(&cardinal_state, &blob_cardinal)), + inf_melee_fallback_possible_at_tile((&cardinal_state)->player.x, (&cardinal_state)->player.y, &blob_cardinal, &magic_stats, ATTACK_STYLE_MELEE, distance_to_player(&cardinal_state, &blob_cardinal)), 0); } -static void test_style_mask_preview(void) { - printf("--- inferno style mask preview ---\n"); - - InfernoState diagonal_state = make_test_state(5, 5); - InfernoState cardinal_state = make_test_state(5, 5); - InfNPC ranger_diagonal = make_test_npc(INF_NPC_RANGER, 6, 6, 1); - InfNPC blob_cardinal = make_test_npc(INF_NPC_BLOB, 6, 5, 1); - InfNPC blob_diagonal = make_test_npc(INF_NPC_BLOB, 6, 6, 1); - InfNPCStats ranged_stats = make_test_stats(ATTACK_STYLE_RANGED); - InfNPCStats magic_stats = make_test_stats(ATTACK_STYLE_MAGIC); - - int ranger_mask = inf_attack_style_options_mask( - &diagonal_state, &ranger_diagonal, &ranged_stats, - ATTACK_STYLE_RANGED, distance_to_player(&diagonal_state, &ranger_diagonal)); - int blob_cardinal_mask = inf_attack_style_options_mask( - &cardinal_state, &blob_cardinal, &magic_stats, - ATTACK_STYLE_MAGIC, distance_to_player(&cardinal_state, &blob_cardinal)); - int blob_diagonal_mask = inf_attack_style_options_mask( - &diagonal_state, &blob_diagonal, &magic_stats, - ATTACK_STYLE_MAGIC, distance_to_player(&diagonal_state, &blob_diagonal)); - - ASSERT_INT_EQ( - "ranger diagonal preview mask", - ranger_mask, - INF_STYLE_MASK_MELEE | INF_STYLE_MASK_RANGED); - ASSERT_INT_EQ( - "ranger diagonal preview style is uncertain", - inf_attack_style_from_mask(ranger_mask), - ATTACK_STYLE_NONE); - ASSERT_INT_EQ( - "ranger diagonal obs preview keeps ranged primary", - inf_attack_style_obs_preview(ranger_mask), - ATTACK_STYLE_RANGED); - ASSERT_INT_EQ( - "blob cardinal preview mask", - blob_cardinal_mask, - INF_STYLE_MASK_MELEE | INF_STYLE_MASK_MAGIC); - ASSERT_INT_EQ( - "blob cardinal obs preview keeps magic primary", - inf_attack_style_obs_preview(blob_cardinal_mask), - ATTACK_STYLE_MAGIC); - ASSERT_INT_EQ( - "blob diagonal preview keeps magic only", - blob_diagonal_mask, - INF_STYLE_MASK_MAGIC); - ASSERT_INT_EQ( - "blob diagonal preview style is magic", - inf_attack_style_from_mask(blob_diagonal_mask), - ATTACK_STYLE_MAGIC); -} - static void test_style_choice_sampling(void) { printf("--- inferno style choice sampling ---\n"); @@ -3046,7 +2593,7 @@ static void test_blob_split_waits_for_death_removal(void) { count_active_npc_type(&state, INF_NPC_BLOB_MAGE), 0); for (int t = 0; t < INF_NPC_DEATH_LINGER_TICKS; t++) - inf_tick_npcs(&state); + inf_tick_npcs_ctx(&state, &test_context); ASSERT_INT_EQ("blob parent removed after death linger", state.npcs[0].active, 0); ASSERT_INT_EQ("blob split melee spawned after removal", @@ -3098,7 +2645,7 @@ static void test_mager_resurrection_render_event_is_not_magic_projectile(void) { RenderEntity entities[4]; int count = 0; - inf_fill_render_entities((EncounterState*)&state, entities, 4, &count); + inf_fill_render_entities_ctx((EncounterState*)&state, (EncounterContext*)&test_context, entities, 4, &count); ASSERT_INT_EQ("resurrection render has mager and resurrected mob", count >= 3, 1); ASSERT_INT_EQ("mager uses resurrection animation", entities[1].npc_anim_id, INF_GEN_ANIM_MAGER_RESURRECT); @@ -3109,7 +2656,7 @@ static void test_mager_resurrection_render_event_is_not_magic_projectile(void) { EncounterOverlay ov; memset(&ov, 0, sizeof(ov)); - inf_render_post_tick((EncounterState*)&state, &ov); + inf_render_post_tick_ctx((EncounterState*)&state, (EncounterContext*)&test_context, &ov); ASSERT_INT_EQ("resurrection does not emit magic projectile", ov.projectile_count, 0); } @@ -3200,7 +2747,7 @@ static void test_blob_attacks_player_on_six_tick_cadence(void) { int prev_scanned = state.npcs[0].blob_scanned_prayer; int last_fire = -1, gap_a = -1, gap_b = -1; for (int tick = 0; tick < 40; tick++) { - inf_npc_attack(&state, 0); + inf_npc_attack_ctx(&state, &test_context, 0); int cur_scanned = state.npcs[0].blob_scanned_prayer; if (prev_scanned >= 0 && cur_scanned < 0) { if (last_fire >= 0) { @@ -3231,7 +2778,7 @@ static void test_jad_has_no_pre_fire_style_preview(void) { state.npcs[0].active = 1; state.npcs[0].attack_timer = 2; - inf_npc_attack(&state, 0); + inf_npc_attack_ctx(&state, &test_context, 0); ASSERT_INT_EQ("jad timer decrements without preview", state.npcs[0].attack_timer, 1); ASSERT_INT_EQ("jad style stays hidden before fire", inf_npc_jad(&state.npcs[0])->attack_style, ATTACK_STYLE_NONE); @@ -3253,7 +2800,7 @@ static void test_jad_fire_tick_exposes_three_tick_prayer_deadline(void) { float obs[INF_NUM_OBS]; memset(obs, 0, sizeof(obs)); - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int pending_start = inferno_pending_hit_obs_start(); ASSERT_FLOAT_NEAR("pending hit obs style is magic", obs[pending_start], (float)ATTACK_STYLE_MAGIC / 4.0f, 1e-6f); @@ -3361,7 +2908,7 @@ static void test_triple_jad_pending_threats_fit_obs_layout(void) { } float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); ASSERT_INT_EQ("inferno obs uses shared-prefix layout", INF_NUM_OBS, 530); } @@ -3369,7 +2916,7 @@ static void test_inferno_action_and_compact_obs_shape(void) { printf("--- inferno action and compact obs shape ---\n"); ASSERT_INT_EQ("equip heads span every gear slot", - INF_HEAD_EAT - INF_HEAD_EQUIP_BASE, NUM_GEAR_SLOTS); + INF_HEAD_EAT - OSRS_HEAD_EQUIP_BASE, NUM_GEAR_SLOTS); ASSERT_INT_EQ("equip head clicks cover every cell", INF_ACTION_DIMS[INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)], OSRS_INVENTORY_SIZE + 1); @@ -3381,11 +2928,6 @@ static void test_inferno_action_and_compact_obs_shape(void) { INF_ACTION_DIMS[INF_HEAD_PRAYER], OSRS_OVERHEAD_DIM); ASSERT_INT_EQ("action mask spans the shared action heads", INF_ACTION_MASK_SIZE, 436); - ASSERT_SOURCE_BLOCK_CONTAINS("native binding reuses inferno action dims", - "ocean/osrs_inferno/osrs_inferno.h", - "#define OBS_SIZE INF_NUM_OBS", - "typedef float obs_t;", - "#define ACT_SIZES INF_ACTION_DIMS_INIT"); ASSERT_INT_EQ("shared player observation width", INF_OBS_AFTER_SHARED, OSRS_SHARED_OBS_SIZE); ASSERT_INT_EQ("compact pillar observation width", INF_PILLAR_OBS_SIZE, 9); @@ -3393,23 +2935,7 @@ static void test_inferno_action_and_compact_obs_shape(void) { INF_TOTAL_NPC_OBS_SIZE, INF_OBS_NPCS * INF_NPC_SLOT_FEATURES); ASSERT_INT_EQ("compact spark observation width", INF_PENDING_SPARK_OBS_SIZE, 128); - ASSERT_INT_EQ("inventory observation uses shared cell records", - INF_INVENTORY_OBS_SIZE, OSRS_SHARED_INVENTORY_OBS_SIZE); - ASSERT_INT_EQ("equipment observation uses shared worn slots", - INF_EQUIPPED_OBS_SIZE, OSRS_SHARED_EQUIPPED_OBS_SIZE); ASSERT_INT_EQ("inferno observation width", INF_NUM_OBS, 530); - ASSERT_INFERNO_SOURCE_NOT_CONTAINS("armor_tank state is removed", - "armor_tank"); - ASSERT_INFERNO_SOURCE_NOT_CONTAINS("extra npc obs scaffold is removed", - "INF_EXTRA_NPC_OBS_FEATURES"); - ASSERT_INFERNO_SOURCE_NOT_CONTAINS("legacy npc pending-hit count is removed", - "pending_hit_count"); - ASSERT_INFERNO_SOURCE_NOT_CONTAINS("legacy player pending-hit count is removed", - "player_pending_hit_count"); - ASSERT_SOURCE_NOT_CONTAINS("binding legacy npc pending-hit count is removed", - "ocean/osrs_inferno/osrs_inferno.h", "pending_hit_count"); - ASSERT_SOURCE_NOT_CONTAINS("binding legacy player pending-hit count is removed", - "ocean/osrs_inferno/osrs_inferno.h", "player_pending_hit_count"); } static void test_inferno_obs_wave_phase_code(void) { @@ -3425,7 +2951,7 @@ static void test_inferno_obs_wave_phase_code(void) { state.player.current_prayer = 99; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); ASSERT_FLOAT_NEAR("wave phase compact code", obs[INF_OBS_WAVE_PHASE], (float)(phase + 1) / 8.0f, 1e-6f); } @@ -3437,7 +2963,7 @@ static void test_inferno_obs_wave_phase_code(void) { triple_jad.player.base_prayer = 99; triple_jad.player.current_prayer = 99; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&triple_jad, obs); + inf_write_obs_ctx((EncounterState*)&triple_jad, (EncounterContext*)&test_context, obs); ASSERT_FLOAT_NEAR("wave 68 stays in Jad phase", obs[INF_OBS_WAVE_PHASE], 5.0f / 8.0f, 1e-6f); } @@ -3464,7 +2990,7 @@ static void test_inferno_obs_exposes_compact_pillars(void) { }; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int active_start = inferno_pillar_obs_start(0); ASSERT_FLOAT_NEAR("active pillar hp", @@ -3502,7 +3028,7 @@ static void test_inferno_obs_exposes_meleer_dig_state(void) { state.npcs[0].dig_attack_delay = 6; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int meleer_slot = inf_find_target_obs_slot(&state, 0); int dig_start = inferno_obs_slot_dig_index(meleer_slot); @@ -3564,7 +3090,7 @@ static void test_triple_jad_first_attacks_are_staggered(void) { memset(&state, 0, sizeof(state)); test_config()->start_wave = 67; test_config()->late_start_supply_profile_scale = 1.0f; - inf_reset((EncounterState*)&state, 12345); + inf_reset_ctx((EncounterState*)&state, (EncounterContext*)&test_context, 12345); state.wave_ready_delay = 0; int jad_slots[3] = { -1, -1, -1 }; @@ -3622,7 +3148,7 @@ static void test_jad_melee_stays_instant_and_untelegraphed(void) { inf_npc_jad(&preview_state.npcs[0])->attack_style = ATTACK_STYLE_RANGED; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&preview_state, obs); + inf_write_obs_ctx((EncounterState*)&preview_state, (EncounterContext*)&test_context, obs); int preview_slot = inf_find_target_obs_slot(&preview_state, 0); ASSERT_FLOAT_NEAR("Jad dynamic style stays ranged in its dense record", @@ -3645,7 +3171,7 @@ static void test_jad_melee_stays_instant_and_untelegraphed(void) { attack_state.npcs[0].attack_timer = 0; inf_npc_jad(&attack_state.npcs[0])->attack_style = ATTACK_STYLE_RANGED; - inf_npc_attack(&attack_state, 0); + inf_npc_attack_ctx(&attack_state, &test_context, 0); if (attack_state.npcs[0].attack_style_this_tick == ATTACK_STYLE_MELEE) { saw_melee = 1; @@ -3701,9 +3227,6 @@ static int inferno_obs_slot_target_category_start(int slot_idx) { return inferno_obs_slot_start(slot_idx) + 8; } -static int inferno_obs_slot_targeted_index(int slot_idx) { - return inferno_obs_slot_start(slot_idx) + 9; -} static int inferno_obs_slot_dig_index(int slot_idx) { return inferno_obs_slot_start(slot_idx) + 10; @@ -3776,18 +3299,16 @@ static void test_npc_threat_obs_exposes_frozen_meleer_pressure(void) { init_threat_obs_state(&state, 10, 10); add_threat_obs_npc(&state, 0, INF_NPC_MELEER, 11, 10); state.npcs[0].frozen_ticks = 8; - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); - InfNpcPlayerThreat threat = inf_npc_player_threat(&state, &state.npcs[0]); + InfNpcPlayerThreat threat = inf_npc_player_threat_ctx(&state, &test_context, &state.npcs[0]); ASSERT_INT_EQ("frozen adjacent meleer can attack if ready", threat.can_attack_if_ready, 1); ASSERT_INT_EQ("frozen adjacent meleer can attack this tick", threat.can_attack_this_tick, 1); - ASSERT_INT_EQ("immediate threat includes frozen adjacent meleer", - inf_player_has_immediate_threat(&state), 1); float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int obs_slot = inf_find_target_obs_slot(&state, 0); ASSERT_INT_EQ("frozen meleer has obs slot", obs_slot >= 0, 1); ASSERT_FLOAT_NEAR("frozen ticks obs", @@ -3802,7 +3323,7 @@ static void test_npc_threat_obs_respects_overlap_range_and_stun(void) { init_threat_obs_state(&under, 10, 10); add_threat_obs_npc(&under, 0, INF_NPC_MELEER, 9, 9); under.npcs[0].frozen_ticks = 8; - InfNpcPlayerThreat under_threat = inf_npc_player_threat(&under, &under.npcs[0]); + InfNpcPlayerThreat under_threat = inf_npc_player_threat_ctx(&under, &test_context, &under.npcs[0]); ASSERT_INT_EQ("standing under frozen meleer is not attackable", under_threat.can_attack_if_ready, 0); @@ -3811,7 +3332,7 @@ static void test_npc_threat_obs_respects_overlap_range_and_stun(void) { add_threat_obs_npc(&diagonal, 0, INF_NPC_MELEER, 11, 11); diagonal.npcs[0].frozen_ticks = 8; InfNpcPlayerThreat diagonal_threat = - inf_npc_player_threat(&diagonal, &diagonal.npcs[0]); + inf_npc_player_threat_ctx(&diagonal, &test_context, &diagonal.npcs[0]); ASSERT_INT_EQ("frozen meleer diagonal corner contact is not attackable", diagonal_threat.can_attack_if_ready, 0); @@ -3819,7 +3340,7 @@ static void test_npc_threat_obs_respects_overlap_range_and_stun(void) { init_threat_obs_state(&far, 10, 10); add_threat_obs_npc(&far, 0, INF_NPC_MELEER, 13, 10); far.npcs[0].frozen_ticks = 8; - InfNpcPlayerThreat far_threat = inf_npc_player_threat(&far, &far.npcs[0]); + InfNpcPlayerThreat far_threat = inf_npc_player_threat_ctx(&far, &test_context, &far.npcs[0]); ASSERT_INT_EQ("frozen meleer outside melee distance is not attackable", far_threat.can_attack_if_ready, 0); @@ -3828,7 +3349,7 @@ static void test_npc_threat_obs_respects_overlap_range_and_stun(void) { add_threat_obs_npc(&stunned, 0, INF_NPC_MELEER, 11, 10); stunned.npcs[0].stun_timer = 2; InfNpcPlayerThreat stunned_threat = - inf_npc_player_threat(&stunned, &stunned.npcs[0]); + inf_npc_player_threat_ctx(&stunned, &test_context, &stunned.npcs[0]); ASSERT_INT_EQ("stunned adjacent meleer would threaten if ready", stunned_threat.can_attack_if_ready, 1); ASSERT_INT_EQ("stunned adjacent meleer cannot attack this tick", @@ -3843,7 +3364,7 @@ static void test_npc_threat_obs_keeps_ranger_mager_diagonal_melee(void) { init_threat_obs_state(&ranger_state, 10, 10); add_threat_obs_npc(&ranger_state, 0, INF_NPC_RANGER, 11, 11); InfNpcPlayerThreat ranger_threat = - inf_npc_player_threat(&ranger_state, &ranger_state.npcs[0]); + inf_npc_player_threat_ctx(&ranger_state, &test_context, &ranger_state.npcs[0]); ASSERT_INT_EQ("diagonal ranger can attack player", ranger_threat.can_attack_if_ready, 1); ASSERT_INT_EQ("diagonal ranger threat includes melee fallback", @@ -3853,7 +3374,7 @@ static void test_npc_threat_obs_keeps_ranger_mager_diagonal_melee(void) { init_threat_obs_state(&mager_state, 10, 10); add_threat_obs_npc(&mager_state, 0, INF_NPC_MAGER, 11, 11); InfNpcPlayerThreat mager_threat = - inf_npc_player_threat(&mager_state, &mager_state.npcs[0]); + inf_npc_player_threat_ctx(&mager_state, &test_context, &mager_state.npcs[0]); ASSERT_INT_EQ("diagonal mager can attack player", mager_threat.can_attack_if_ready, 1); ASSERT_INT_EQ("diagonal mager threat includes melee fallback", @@ -3861,30 +3382,11 @@ static void test_npc_threat_obs_keeps_ranger_mager_diagonal_melee(void) { } -static void test_barrage_total_target_count_matches_resolution_targets(void) { - printf("--- barrage total target count matches resolution targets ---\n"); - - InfernoState state; - init_threat_obs_state(&state, 10, 10); - add_threat_obs_npc(&state, 0, INF_NPC_RANGER, 16, 10); - add_threat_obs_npc(&state, 1, INF_NPC_NIBBLER, 17, 11); - add_threat_obs_npc(&state, 2, INF_NPC_MAGER, 15, 9); - add_threat_obs_npc(&state, 3, INF_NPC_ZUK_SHIELD, 16, 11); - add_threat_obs_npc(&state, 4, INF_NPC_BAT, 17, 10); - state.npcs[4].death_ticks = 2; - - ASSERT_INT_EQ("barrage count includes primary and active non-shields", - inf_barrage_total_target_count(&state, 0), 3); - - - for (int i = 5; i < 16; i++) { - add_threat_obs_npc(&state, i, INF_NPC_BAT, 15 + (i % 3), 9 + (i % 3)); - } - ASSERT_INT_EQ("barrage count caps at max hits", - inf_barrage_total_target_count(&state, 0), BARRAGE_MAX_HITS); -} - -static void init_step_out_forecast_stack_state(InfernoState* state, int player_x, int player_y) { +static void init_step_out_forecast_stack_state( + InfernoState* state, + int player_x, + int player_y +) { reset_test_context(); inf_build_npc_stats(); memset(state, 0, sizeof(*state)); @@ -3921,12 +3423,6 @@ static void add_step_out_forecast_npc( state->npcs[slot].frozen_ticks = 0; } -static void clear_step_out_forecast_pillars(InfernoState* state) { - for (int p = 0; p < INF_NUM_PILLARS; p++) { - state->pillars[p].active = 0; - state->pillars[p].hp = 0; - } -} static void assert_step_out_ranger_then_mager( const char* label, @@ -3962,12 +3458,12 @@ static void test_step_out_forecast_matches_movement_head_destinations(void) { init_step_out_forecast_stack_state(&state, 29, 39); InfStepOutForecast forecast; - inf_build_step_out_forecast_ctx(&state, inf_legacy_context(), &forecast); + inf_build_step_out_forecast_ctx(&state, &test_context, &forecast); for (int action = 0; action < ENCOUNTER_MOVE_ACTIONS; action++) { Player moved = state.player; if (action > 0) { - InfWalkCtx walk_ctx = { &state, inf_legacy_context() }; + InfWalkCtx walk_ctx = { &state, &test_context }; encounter_move_to_target( &moved, ENCOUNTER_MOVE_TARGET_DX[action], @@ -4003,9 +3499,9 @@ static void assert_inferno_npc_sw_origin_step( add_step_out_forecast_npc(&state, 0, type, npc_x, npc_y, 0); ASSERT_INT_EQ("starting NPC has no LOS", inf_npc_has_los_direct_ctx( - &state, inf_legacy_context(), 0), 0); + &state, &test_context, 0), 0); - inf_npc_move(&state, 0); + inf_npc_move_ctx(&state, &test_context, 0); char msg[128]; snprintf(msg, sizeof(msg), "%s x", label); @@ -4055,9 +3551,9 @@ static void assert_inferno_jal_npc_uses_edge_clearance( } add_step_out_forecast_npc(&state, 0, type, npc_x, npc_y, 0); ASSERT_INT_EQ("starting Jal NPC has no LOS", - inf_npc_has_los_ctx(&state, inf_legacy_context(), 0), 0); + inf_npc_has_los_ctx(&state, &test_context, 0), 0); - inf_npc_move(&state, 0); + inf_npc_move_ctx(&state, &test_context, 0); char msg[128]; snprintf(msg, sizeof(msg), "%s x", label); @@ -4092,7 +3588,7 @@ static void test_step_out_forecast_north_pillar_ranger_mager_order(void) { add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); InfStepOutForecast forecast; - inf_build_step_out_forecast_ctx(&state, inf_legacy_context(), &forecast); + inf_build_step_out_forecast_ctx(&state, &test_context, &forecast); const InfStepOutForecastAction* idle = &forecast.actions[0]; ASSERT_INT_EQ("idle remains safe from ranged tick one", @@ -4104,348 +3600,6 @@ static void test_step_out_forecast_north_pillar_ranger_mager_order(void) { assert_step_out_ranger_then_mager("north pillar run west", run_west, 27, 39); } - -static void test_fast_step_out_forecast_matches_movement_head_destinations(void) { - printf("--- fast step-out forecast matches movement head destinations ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 29, 39); - - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_static_ctx( - &state, inf_legacy_context(), &forecast); - - for (int action = 0; action < ENCOUNTER_MOVE_ACTIONS; action++) { - Player moved = state.player; - if (action > 0) { - InfWalkCtx walk_ctx = { &state, inf_legacy_context() }; - encounter_move_to_target( - &moved, - ENCOUNTER_MOVE_TARGET_DX[action], - ENCOUNTER_MOVE_TARGET_DY[action], - inf_tile_walkable, - &walk_ctx); - } - - ASSERT_INT_EQ("fast forecast movement landing x", - forecast.actions[action].land_x, moved.x); - ASSERT_INT_EQ("fast forecast movement landing y", - forecast.actions[action].land_y, moved.y); - } -} - -static void test_fast_step_out_forecast_immediate_static_threats(void) { - printf("--- fast step-out forecast immediate static threats ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 29, 39); - clear_step_out_forecast_pillars(&state); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 1); - add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 2); - - InfStepOutForecast exact; - InfStepOutForecast fast; - InfStepOutForecastOracleDiff diff; - inf_build_step_out_forecast_exact_ctx(&state, inf_legacy_context(), &exact); - inf_build_step_out_forecast_fast_static_ctx(&state, inf_legacy_context(), &fast); - inf_compare_step_out_forecasts(&exact, &fast, &diff); - - const InfStepOutForecastAction* idle = &fast.actions[0]; - ASSERT_INT_EQ("fast idle ranger fires first", idle->ticks[0].ranger_count, 1); - ASSERT_INT_EQ("fast idle mager fires second", idle->ticks[1].mager_count, 1); - ASSERT_INT_EQ("fast idle exposes off-tick opportunity", - idle->ranger_mager_offtick_opportunity, 1); - ASSERT_INT_EQ("fast static has no dangerous false negatives", - diff.dangerous_false_negatives, 0); -} - -static void test_fast_step_out_forecast_blob_scan_and_melee_fallback(void) { - printf("--- fast step-out forecast blob scan and melee fallback ---\n"); - - InfernoState blob_state; - init_step_out_forecast_stack_state(&blob_state, 29, 39); - clear_step_out_forecast_pillars(&blob_state); - add_step_out_forecast_npc(&blob_state, 0, INF_NPC_BLOB, 29, 30, 1); - blob_state.npcs[0].blob_scanned_prayer = -1; - blob_state.npcs[0].had_los_last_tick = 0; - - InfStepOutForecast blob_forecast; - inf_build_step_out_forecast_fast_static_ctx( - &blob_state, inf_legacy_context(), &blob_forecast); - ASSERT_INT_EQ("fast blob scan tick", - blob_forecast.actions[0].ticks[0].blob_scan_count, 1); - - InfernoState melee_state; - init_step_out_forecast_stack_state(&melee_state, 20, 20); - clear_step_out_forecast_pillars(&melee_state); - add_step_out_forecast_npc(&melee_state, 0, INF_NPC_MAGER, 21, 20, 1); - - InfStepOutForecast melee_forecast; - inf_build_step_out_forecast_fast_static_ctx( - &melee_state, inf_legacy_context(), &melee_forecast); - ASSERT_INT_EQ("fast melee fallback exposure", - melee_forecast.actions[0].melee_fallback_exposure, 1); - ASSERT_INT_EQ("fast melee fallback mixed style", - melee_forecast.actions[0].same_tick_mixed_style_conflict, 1); -} - -static void test_fast_step_out_forecast_does_not_mutate_state(void) { - printf("--- fast step-out forecast does not mutate state ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 29, 39); - clear_step_out_forecast_pillars(&state); - add_step_out_forecast_npc(&state, 0, INF_NPC_BLOB, 29, 30, 1); - state.npcs[0].blob_scanned_prayer = -1; - state.npcs[0].had_los_last_tick = 0; - - InfernoState before = state; - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_static_ctx( - &state, inf_legacy_context(), &forecast); - - ASSERT_INT_EQ("fast forecast preserves player x", - state.player.x, before.player.x); - ASSERT_INT_EQ("fast forecast preserves player y", - state.player.y, before.player.y); - ASSERT_INT_EQ("fast forecast preserves NPC timer", - state.npcs[0].attack_timer, before.npcs[0].attack_timer); - ASSERT_INT_EQ("fast forecast preserves blob scan state", - state.npcs[0].blob_scanned_prayer, before.npcs[0].blob_scanned_prayer); -} - -static void test_readonly_step_out_forecast_matches_movement_head_destinations(void) { - printf("--- readonly step-out forecast matches movement head destinations ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 29, 39); - - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_readonly_ctx( - &state, inf_legacy_context(), &forecast); - - for (int action = 0; action < ENCOUNTER_MOVE_ACTIONS; action++) { - Player moved = state.player; - if (action > 0) { - InfWalkCtx walk_ctx = { &state, inf_legacy_context() }; - encounter_move_to_target( - &moved, - ENCOUNTER_MOVE_TARGET_DX[action], - ENCOUNTER_MOVE_TARGET_DY[action], - inf_tile_walkable, - &walk_ctx); - } - - ASSERT_INT_EQ("readonly forecast movement landing x", - forecast.actions[action].land_x, moved.x); - ASSERT_INT_EQ("readonly forecast movement landing y", - forecast.actions[action].land_y, moved.y); - } -} - -static void test_readonly_step_out_forecast_does_not_mutate_state(void) { - printf("--- readonly step-out forecast does not mutate state ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 29, 39); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 1); - add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 2); - add_step_out_forecast_npc(&state, 2, INF_NPC_BLOB, 29, 32, 1); - state.npcs[2].blob_scanned_prayer = -1; - state.npcs[2].had_los_last_tick = 0; - - InfernoState before = state; - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_readonly_ctx( - &state, inf_legacy_context(), &forecast); - - ASSERT_INT_EQ("readonly forecast preserves player x", - state.player.x, before.player.x); - ASSERT_INT_EQ("readonly forecast preserves player y", - state.player.y, before.player.y); - ASSERT_INT_EQ("readonly forecast preserves ranger x", - state.npcs[0].x, before.npcs[0].x); - ASSERT_INT_EQ("readonly forecast preserves ranger y", - state.npcs[0].y, before.npcs[0].y); - ASSERT_INT_EQ("readonly forecast preserves NPC timer", - state.npcs[0].attack_timer, before.npcs[0].attack_timer); - ASSERT_INT_EQ("readonly forecast preserves blob scan state", - state.npcs[2].blob_scanned_prayer, before.npcs[2].blob_scanned_prayer); -} - -static void assert_readonly_step_out_matches_exact_action( - const char* label, - InfernoState* state, - int action_idx -) { - InfStepOutForecast exact; - InfStepOutForecast readonly; - InfStepOutForecastOracleDiff diff; - inf_build_step_out_forecast_exact_ctx(state, inf_legacy_context(), &exact); - inf_build_step_out_forecast_fast_readonly_ctx( - state, inf_legacy_context(), &readonly); - inf_compare_step_out_forecasts(&exact, &readonly, &diff); - - ASSERT_INT_EQ("readonly action oracle has no dangerous false negatives", - diff.dangerous_false_negatives, 0); - const InfStepOutForecastAction* exact_action = &exact.actions[action_idx]; - const InfStepOutForecastAction* readonly_action = &readonly.actions[action_idx]; - char msg[128]; - snprintf(msg, sizeof(msg), "%s valid", label); - ASSERT_INT_EQ(msg, readonly_action->valid, exact_action->valid); - snprintf(msg, sizeof(msg), "%s land x", label); - ASSERT_INT_EQ(msg, readonly_action->land_x, exact_action->land_x); - snprintf(msg, sizeof(msg), "%s land y", label); - ASSERT_INT_EQ(msg, readonly_action->land_y, exact_action->land_y); - snprintf(msg, sizeof(msg), "%s first ranger count", label); - ASSERT_INT_EQ(msg, - readonly_action->ticks[0].ranger_count, - exact_action->ticks[0].ranger_count); - snprintf(msg, sizeof(msg), "%s second mager count", label); - ASSERT_INT_EQ(msg, - readonly_action->ticks[1].mager_count, - exact_action->ticks[1].mager_count); -} - -static void test_readonly_step_out_forecast_pillar_step_out_cases(void) { - printf("--- readonly step-out forecast pillar step-out cases ---\n"); - - InfernoState north_state; - init_step_out_forecast_stack_state(&north_state, 29, 39); - add_step_out_forecast_npc(&north_state, 0, INF_NPC_RANGER, 24, 31, 0); - add_step_out_forecast_npc(&north_state, 1, INF_NPC_MAGER, 29, 30, 0); - assert_readonly_step_out_matches_exact_action( - "north pillar run west", &north_state, 11); - - InfernoState south_state; - init_step_out_forecast_stack_state(&south_state, 22, 17); - add_step_out_forecast_npc(&south_state, 0, INF_NPC_RANGER, 17, 25, 0); - add_step_out_forecast_npc(&south_state, 1, INF_NPC_MAGER, 22, 26, 0); - assert_readonly_step_out_matches_exact_action( - "south pillar run west", &south_state, 11); - - InfernoState west_state; - init_step_out_forecast_stack_state(&west_state, 11, 29); - add_step_out_forecast_npc(&west_state, 0, INF_NPC_RANGER, 8, 40, 0); - add_step_out_forecast_npc(&west_state, 1, INF_NPC_MAGER, 16, 42, 0); - assert_readonly_step_out_matches_exact_action( - "west pillar walk north", &west_state, 4); -} - - -static void test_readonly_step_out_forecast_stun_countdown(void) { - printf("--- readonly step-out forecast stun countdown ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 20, 20); - clear_step_out_forecast_pillars(&state); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 20, 25, 1); - state.npcs[0].stun_timer = 2; - - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_readonly_ctx( - &state, inf_legacy_context(), &forecast); - - ASSERT_INT_EQ("stunned ranger does not fire tick one", - forecast.actions[0].ticks[0].ranger_count, 0); - ASSERT_INT_EQ("stunned ranger does not fire tick two", - forecast.actions[0].ticks[1].ranger_count, 0); - ASSERT_INT_EQ("stunned ranger fires after countdown", - forecast.actions[0].ticks[2].ranger_count, 1); -} - -static void test_readonly_step_out_forecast_frozen_can_attack(void) { - printf("--- readonly step-out forecast frozen can attack ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 20, 20); - clear_step_out_forecast_pillars(&state); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 20, 25, 1); - state.npcs[0].frozen_ticks = 4; - - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_readonly_ctx( - &state, inf_legacy_context(), &forecast); - - ASSERT_INT_EQ("frozen ranger still attacks with LOS", - forecast.actions[0].ticks[0].ranger_count, 1); -} - -static void test_readonly_step_out_forecast_blob_scanned_fire(void) { - printf("--- readonly step-out forecast blob scanned fire ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 20, 20); - clear_step_out_forecast_pillars(&state); - add_step_out_forecast_npc(&state, 0, INF_NPC_BLOB, 20, 25, 1); - state.npcs[0].blob_scanned_prayer = PRAYER_PROTECT_MAGIC; - state.npcs[0].attack_style = ATTACK_STYLE_RANGED; - state.npcs[0].had_los_last_tick = 1; - - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_readonly_ctx( - &state, inf_legacy_context(), &forecast); - - ASSERT_INT_EQ("scanned blob fires ranged", - forecast.actions[0].ticks[0].ranged_count, 1); - ASSERT_INT_EQ("scanned blob fire has max hit", - forecast.actions[0].ticks[0].max_hit > 0, 1); -} - -static void test_readonly_step_out_forecast_under_player_overlap_is_danger(void) { - printf("--- readonly step-out forecast under-player overlap is danger ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, 20, 20); - clear_step_out_forecast_pillars(&state); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 20, 20, 1); - uint32_t rng_before = state.rng_state; - - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_readonly_ctx( - &state, inf_legacy_context(), &forecast); - - ASSERT_INT_EQ("under-player overlap marks immediate danger", - inf_step_out_forecast_action_dangerous(&forecast.actions[0]), 1); - ASSERT_INT_EQ("under-player forecast does not consume RNG", - state.rng_state, rng_before); -} - -static void test_readonly_step_out_forecast_invalid_movement_zeroes_payload(void) { - printf("--- readonly step-out forecast invalid movement zeroes payload ---\n"); - - InfernoState state; - init_step_out_forecast_stack_state(&state, INF_ARENA_MIN_X, INF_ARENA_MIN_Y); - clear_step_out_forecast_pillars(&state); - add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, - INF_ARENA_MIN_X + 3, INF_ARENA_MIN_Y + 3, 1); - - InfStepOutForecast forecast; - inf_build_step_out_forecast_fast_readonly_ctx( - &state, inf_legacy_context(), &forecast); - - const InfStepOutForecastAction* action = &forecast.actions[9]; - ASSERT_INT_EQ("invalid run-west action is invalid", action->valid, 0); - ASSERT_INT_EQ("invalid action has no same-tick conflict", - action->same_tick_mixed_style_conflict, 0); - ASSERT_INT_EQ("invalid action has no off-tick opportunity", - action->ranger_mager_offtick_opportunity, 0); - ASSERT_INT_EQ("invalid action has no melee fallback", - action->melee_fallback_exposure, 0); - for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { - ASSERT_INT_EQ("invalid action has no melee count", - action->ticks[tick_idx].melee_count, 0); - ASSERT_INT_EQ("invalid action has no ranged count", - action->ticks[tick_idx].ranged_count, 0); - ASSERT_INT_EQ("invalid action has no magic count", - action->ticks[tick_idx].magic_count, 0); - ASSERT_INT_EQ("invalid action has no blob scan count", - action->ticks[tick_idx].blob_scan_count, 0); - ASSERT_INT_EQ("invalid action has no max hit", - action->ticks[tick_idx].max_hit, 0); - } -} - static void test_step_out_forecast_south_pillar_ranger_mager_order(void) { printf("--- step-out forecast south pillar ranger/mager order ---\n"); @@ -4455,7 +3609,7 @@ static void test_step_out_forecast_south_pillar_ranger_mager_order(void) { add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 22, 26, 0); InfStepOutForecast forecast; - inf_build_step_out_forecast_ctx(&state, inf_legacy_context(), &forecast); + inf_build_step_out_forecast_ctx(&state, &test_context, &forecast); const InfStepOutForecastAction* run_west = &forecast.actions[11]; assert_step_out_ranger_then_mager("south pillar run west", run_west, 20, 17); @@ -4470,7 +3624,7 @@ static void test_step_out_forecast_west_pillar_ranger_mager_order(void) { add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 16, 42, 0); InfStepOutForecast forecast; - inf_build_step_out_forecast_ctx(&state, inf_legacy_context(), &forecast); + inf_build_step_out_forecast_ctx(&state, &test_context, &forecast); const InfStepOutForecastAction* walk_north = &forecast.actions[4]; assert_step_out_ranger_then_mager("west pillar walk north", walk_north, 11, 28); @@ -4489,7 +3643,7 @@ static void test_step_out_forecast_inactive_pillar_does_not_create_cover(void) { &state, 1, INF_NPC_MAGER, 29, 30, 0); InfStepOutForecast forecast; - inf_build_step_out_forecast_ctx(&state, inf_legacy_context(), &forecast); + inf_build_step_out_forecast_ctx(&state, &test_context, &forecast); const InfStepOutForecastAction* idle = &forecast.actions[0]; ASSERT_INT_EQ("inactive north pillar exposes ranger immediately", @@ -4514,7 +3668,7 @@ static void test_step_out_same_tick_ranger_mager_event_logs(void) { int actions[INF_NUM_ACTION_HEADS] = {0}; actions[INF_HEAD_PRIMARY] = 13; - inf_step((EncounterState*)&state, actions); + inf_step_ctx((EncounterState*)&state, (EncounterContext*)&test_context, actions); ASSERT_INT_EQ("step-out tick moved the player", state.tick_scratch.player_moved, 1); @@ -4523,7 +3677,7 @@ static void test_step_out_same_tick_ranger_mager_event_logs(void) { state.total_step_out_ranger_mager_same_tick_attacks, 0); int noop[INF_NUM_ACTION_HEADS] = {0}; - inf_step((EncounterState*)&state, noop); + inf_step_ctx((EncounterState*)&state, (EncounterContext*)&test_context, noop); ASSERT_INT_EQ("same-tick ranger/mager event counted", state.total_ranger_mager_same_tick_attacks, 1); @@ -4697,7 +3851,7 @@ static void test_zuk_obs_exposes_attack_timer_summary(void) { state.npcs[0].attack_timer = 3; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); ASSERT_FLOAT_NEAR("zuk attack timer uses compact player field", obs[INF_OBS_ZUK_ATTACK_TIMER], 0.3f, 1e-6f); @@ -4748,7 +3902,7 @@ static void test_zuk_obs_exposes_pending_sparks(void) { return; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); ASSERT_INT_EQ("spark obs keeps all pending slots", spark_slots, INF_MAX_PENDING_SPARKS); ASSERT_INT_EQ("spark obs carries compact landing record", spark_features, 4); @@ -4783,16 +3937,16 @@ static void assert_human_blowpipe_zuk_chase_endpoint( ASSERT_INT_EQ("Zuk blowpipe endpoint starts out of range", inf_player_can_attack_npc_from_current_tile_ctx( - &endpoint_state, inf_legacy_context(), 0), 0); + &endpoint_state, &test_context, 0), 0); HumanInput hi = make_human_input(); human_input_queue_attack_npc(&hi, 0); - inf_step_human_commands((EncounterState*)&endpoint_state, &hi); + inf_step_human_commands_ctx((EncounterState*)&endpoint_state, (EncounterContext*)&test_context, &hi); human_input_destroy(&hi); for (int i = 0; i < 16; i++) { HumanInput empty = make_human_input(); - inf_step_human_commands((EncounterState*)&endpoint_state, &empty); + inf_step_human_commands_ctx((EncounterState*)&endpoint_state, (EncounterContext*)&test_context, &empty); human_input_destroy(&empty); } @@ -4801,7 +3955,7 @@ static void assert_human_blowpipe_zuk_chase_endpoint( endpoint_state.player.y, expected_y); ASSERT_INT_EQ("Zuk blowpipe endpoint remains out of range", inf_player_can_attack_npc_from_current_tile_ctx( - &endpoint_state, inf_legacy_context(), 0), 0); + &endpoint_state, &test_context, 0), 0); ASSERT_INT_EQ("Zuk blowpipe endpoint does not fire", endpoint_state.npcs[0].pending_hits.hits[0].active, 0); ASSERT_INT_EQ("Zuk blowpipe endpoint keeps interaction active", @@ -4830,11 +3984,11 @@ static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { ASSERT_INT_EQ("north-row Zuk click starts out of blowpipe range", inf_player_can_attack_npc_from_current_tile_ctx( - &edge_state, inf_legacy_context(), 0), 0); + &edge_state, &test_context, 0), 0); HumanInput edge_hi = make_human_input(); human_input_queue_attack_npc(&edge_hi, 0); - inf_step_human_commands((EncounterState*)&edge_state, &edge_hi); + inf_step_human_commands_ctx((EncounterState*)&edge_state, (EncounterContext*)&test_context, &edge_hi); human_input_destroy(&edge_hi); ASSERT_INT_EQ("north-row Zuk click follows reference seek x", edge_state.player.x, 24); @@ -4842,7 +3996,7 @@ static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { edge_state.player.y, INF_ARENA_MAX_Y); ASSERT_INT_EQ("north-row Zuk click remains outside blowpipe range", inf_player_can_attack_npc_from_current_tile_ctx( - &edge_state, inf_legacy_context(), 0), 0); + &edge_state, &test_context, 0), 0); ASSERT_INT_EQ("north-row Zuk cooldown prevents immediate hit", edge_state.npcs[0].pending_hits.hits[0].active, 0); ASSERT_INT_EQ("north-row Zuk interaction remains active", @@ -4859,14 +4013,14 @@ static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { ASSERT_INT_EQ("human Zuk click starts out of blowpipe range", inf_player_can_attack_npc_from_current_tile_ctx( - &state, inf_legacy_context(), 0), 0); + &state, &test_context, 0), 0); float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); HumanInput hi = make_human_input(); human_input_queue_attack_npc(&hi, 0); - inf_step_human_commands((EncounterState*)&state, &hi); + inf_step_human_commands_ctx((EncounterState*)&state, (EncounterContext*)&test_context, &hi); human_input_destroy(&hi); ASSERT_INT_EQ("human Zuk click keeps interaction active", @@ -4881,14 +4035,14 @@ static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { for (int i = 0; i < 8; i++) { HumanInput empty = make_human_input(); - inf_step_human_commands((EncounterState*)&state, &empty); + inf_step_human_commands_ctx((EncounterState*)&state, (EncounterContext*)&test_context, &empty); human_input_destroy(&empty); } ASSERT_INT_EQ("human Zuk click ends at max north row", state.player.y, INF_ARENA_MAX_Y); ASSERT_INT_EQ("human Zuk click never reaches blowpipe attack tile", inf_player_can_attack_npc_from_current_tile_ctx( - &state, inf_legacy_context(), 0), 0); + &state, &test_context, 0), 0); ASSERT_INT_EQ("human Zuk click does not fire unreachable blowpipe hit", state.npcs[0].pending_hits.hits[0].active, 0); ASSERT_INT_EQ("human Zuk click keeps interaction after chase", @@ -4914,14 +4068,14 @@ static void test_zuk_healer_blowpipe_target_chases_out_of_range(void) { state.npcs[2].aggro_target = 0; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int healer_slot = inf_find_target_obs_slot(&state, 2); ASSERT_INT_EQ("healer appears in target obs", healer_slot >= 0, 1); int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(healer_slot); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("healer target keeps interaction active", osrs_interaction_active(&state.interaction), 1); @@ -4930,7 +4084,7 @@ static void test_zuk_healer_blowpipe_target_chases_out_of_range(void) { ASSERT_INT_EQ("healer target follows shortest seek y", state.player.y, 43); ASSERT_INT_EQ("healer target reaches blowpipe range", inf_player_can_attack_npc_from_current_tile_ctx( - &state, inf_legacy_context(), 2), 1); + &state, &test_context, 2), 1); ASSERT_INT_EQ("cooldown prevents immediate healer hit", state.npcs[2].pending_hits.hits[0].active, 0); ASSERT_INT_EQ("attack timer decrements after healer chase", @@ -4943,7 +4097,7 @@ static void test_zuk_healer_blowpipe_target_chases_out_of_range(void) { int healer_hit_seen = 0; for (int i = 0; i < 4 && !healer_hit_seen; i++) { memset(actions, 0, sizeof(actions)); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); healer_hit_seen = state.npcs[2].pending_hits.hits[0].active; } @@ -4951,7 +4105,7 @@ static void test_zuk_healer_blowpipe_target_chases_out_of_range(void) { healer_hit_seen, 1); ASSERT_INT_EQ("healer target remains attackable after chase", inf_player_can_attack_npc_from_current_tile_ctx( - &state, inf_legacy_context(), 2), 1); + &state, &test_context, 2), 1); } static void test_render_facing_prefers_attack_target_while_chasing(void) { @@ -5251,45 +4405,6 @@ static void test_render_motion_seed_classification_uses_explicit_teleport(void) 1); } -static void test_render_post_tick_removed_distance_snap_branch(void) { - printf("--- render post tick removed distance snap branch ---\n"); - - ASSERT_SOURCE_BLOCK_NOT_CONTAINS( - "render post tick does not snap from tile distance", - "ocean/osrs/osrs_render.h", - "static void render_post_tick", - "static void render_client_tick", - "tile_dx > 2.0f"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "render post tick seeds only by movement kind classification", - "ocean/osrs/osrs_render.h", - "static void render_post_tick", - "static void render_client_tick", - "osrs_render_should_seed_visual_position"); -} - -static void test_render_bridge_marks_genuine_teleports(void) { - printf("--- render bridge marks genuine teleports ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "inferno meleer dig-up marks render teleport", - "ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc", - "RenderMovementKind render_movement_kind", - "RenderEntity* re = &out[n++];", - "RENDER_MOVEMENT_TELEPORT"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "zulrah surface marks render teleport", - "ocean/osrs/encounters/encounter_zulrah.h", - "static void zul_fill_render_entities", - "for (int i = 0; i < ZUL_MAX_SNAKELINGS", - "RENDER_MOVEMENT_TELEPORT"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "lab move command snaps only render visual state", - "ocean/osrs/osrs_render.h", - "static void render_lab_snap_line_visuals", - "static const EncounterDef* render_lab_def", - "render_lab_line_command_is(line, \"move_npc\")"); -} static void test_entity_model_ground_lift_keeps_floor_planes_above_terrain(void) { printf("--- entity model ground lift keeps floor planes above terrain ---\n"); @@ -5344,7 +4459,7 @@ static void test_inferno_npc_spawn_id_changes_on_slot_reuse(void) { RenderEntity entities[4]; int count = 0; - inf_fill_render_entities((EncounterState*)&state, entities, 4, &count); + inf_fill_render_entities_ctx((EncounterState*)&state, (EncounterContext*)&test_context, entities, 4, &count); ASSERT_INT_EQ("reused NPC appears in render list", count, 2); ASSERT_INT_EQ("render entity carries spawn id", entities[1].npc_instance_id, second_render_id); @@ -5389,8 +4504,8 @@ static void test_zuk_healer_target_action_tags_on_landed_hit(void) { float obs[INF_NUM_OBS]; float mask[INF_ACTION_MASK_SIZE]; - inf_write_obs((EncounterState*)&state, obs); - inf_write_mask((EncounterState*)&state, mask); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); int healer_slot = inf_find_target_obs_slot(&state, 2); ASSERT_INT_EQ("zuk healer occupies first healer slot", @@ -5401,7 +4516,7 @@ static void test_zuk_healer_target_action_tags_on_landed_hit(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(healer_slot); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("target action selects zuk healer", state.interaction.target_slot, 2); @@ -5460,12 +4575,12 @@ static void test_zuk_healer_mage_attack_counts_penalty_event(void) { state.npcs[2].aggro_target = 0; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_action_target_for_npc(&state, 2); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("mage healer attack fires once", state.tick_scratch.zuk_healer_mage_attack_fires, 1); @@ -5496,8 +4611,8 @@ static void test_zuk_safe_healer_target_mask_requires_fire_window(void) { state.player.x = 20; state.player.y = 46; state.player.attack_timer = 0; - inf_write_obs((EncounterState*)&state, obs); - inf_write_mask((EncounterState*)&state, mask); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); int healer_slot = inf_find_target_obs_slot(&state, 2); ASSERT_FLOAT_NEAR("unsafe healer target masked while off shield", mask[inferno_target_mask_slot_offset(healer_slot)], 0.0f, 1e-6f); @@ -5505,18 +4620,18 @@ static void test_zuk_safe_healer_target_mask_requires_fire_window(void) { state.player.x = 24; state.player.y = 46; state.player.attack_timer = 0; - inf_write_obs((EncounterState*)&state, obs); - inf_write_mask((EncounterState*)&state, mask); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); ASSERT_FLOAT_NEAR("safe fire-ready healer target remains valid", mask[inferno_target_mask_slot_offset(healer_slot)], 1.0f, 1e-6f); state.player.attack_timer = 2; - inf_write_mask((EncounterState*)&state, mask); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); ASSERT_FLOAT_NEAR("cooldown healer target is masked", mask[inferno_target_mask_slot_offset(healer_slot)], 0.0f, 1e-6f); state.npcs[2].aggro_target = -1; - inf_write_mask((EncounterState*)&state, mask); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); ASSERT_FLOAT_NEAR("tagged healer target remains valid for killing", mask[inferno_target_mask_slot_offset(healer_slot)], 1.0f, 1e-6f); } @@ -5542,7 +4657,7 @@ static void test_zuk_safe_healer_target_mask_clears_unsafe_target(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("unsafe active healer target cleared", osrs_interaction_active(&state.interaction), 0); @@ -5574,8 +4689,8 @@ static void test_zuk_force_safe_healer_target_mask_blocks_idle_when_safe(void) { float obs[INF_NUM_OBS]; float mask[INF_ACTION_MASK_SIZE]; - inf_write_obs((EncounterState*)&state, obs); - inf_write_mask((EncounterState*)&state, mask); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); int healer_slot = inf_find_target_obs_slot(&state, 2); int mager_slot = inf_find_target_obs_slot(&state, 3); @@ -5589,7 +4704,7 @@ static void test_zuk_force_safe_healer_target_mask_blocks_idle_when_safe(void) { mask[inferno_target_mask_slot_offset(mager_slot)], 0.0f, 1e-6f); state.player.attack_timer = 2; - inf_write_mask((EncounterState*)&state, mask); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); ASSERT_FLOAT_NEAR("idle target action returns when no safe fire window exists", mask[inferno_target_mask_none_offset()], 1.0f, 1e-6f); ASSERT_FLOAT_NEAR("unsafe untagged healer remains masked during cooldown", @@ -5622,11 +4737,11 @@ static void test_zuk_force_safe_healer_target_mask_clears_stale_target(void) { osrs_interaction_set(&state.interaction, 3); float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("stale non-healer target cleared", osrs_interaction_active(&state.interaction), 0); @@ -5647,7 +4762,7 @@ static void test_zuk_spark_render_matches_pending_spark_state(void) { EncounterOverlay ov; memset(&ov, 0, sizeof(ov)); - inf_render_post_tick((EncounterState*)&state, &ov); + inf_render_post_tick_ctx((EncounterState*)&state, (EncounterContext*)&test_context, &ov); ASSERT_INT_EQ("spark render emits one projectile", ov.projectile_count, 1); ASSERT_INT_EQ("spark source x", ov.projectiles[0].src_x, 16); @@ -5668,7 +4783,7 @@ static void test_zuk_attack_delay_counts_down_while_stunned(void) { InfernoState state; init_zuk_timing_state(&state); - inf_npc_attack(&state, 0); + inf_npc_attack_ctx(&state, &test_context, 0); ASSERT_INT_EQ("zuk stun decremented", state.npcs[0].stun_timer, 7); ASSERT_INT_EQ("zuk attack delay decremented during stun", @@ -5730,7 +4845,7 @@ static void test_set_attack_to_shield_is_projectile_delayed(void) { state.npcs[2].attack_timer = 0; state.npcs[2].aggro_target = 1; - inf_npc_attack(&state, 2); + inf_npc_attack_ctx(&state, &test_context, 2); if (state.npcs[1].hp < state.npcs[1].max_hp || state.tick_scratch.shield_damage > 0.0f) { found_immediate_damage = 1; @@ -5802,7 +4917,7 @@ static void test_npc_player_projectile_delays_use_reference_options(void) { EncounterProjectileTiming timing = inf_npc_projectile_timing(INF_NPC_RANGER, ATTACK_STYLE_RANGED, dist); - inf_npc_attack(&state, 0); + inf_npc_attack_ctx(&state, &test_context, 0); ASSERT_INT_EQ("ranger queued one pending hit", state.player_pending_hits.count, 1); @@ -5813,8 +4928,6 @@ static void test_npc_player_projectile_delays_use_reference_options(void) { ASSERT_INT_EQ("ranger pending hit carries the raw projectile delay", state.player_pending_hits.hits[0].ticks_remaining, timing.damage_delay_ticks); } - -/** Regression: an NPC hit thrown on tick N must land on tick N + hit_delay. */ static void test_npc_hit_lands_on_the_reference_tick(void) { printf("--- npc hit lands on the reference tick ---\n"); @@ -5828,19 +4941,19 @@ static void test_npc_hit_lands_on_the_reference_tick(void) { for (size_t c = 0; c < sizeof(cases) / sizeof(cases[0]); c++) { InfernoState* s = (InfernoState*)inf_create(); - inf_reset((EncounterState*)s, 20260728u); - inf_lab_apply_command_ctx(s, inf_legacy_context(), + inf_reset_ctx((EncounterState*)s, (EncounterContext*)&test_context, 20260728u); + inf_lab_apply_command_ctx(s, &test_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_CLEAR_NPCS }); - inf_lab_apply_command_ctx(s, inf_legacy_context(), &(InfernoLabCommand){ + inf_lab_apply_command_ctx(s, &test_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SET_PLAYER, .as.tile = { .x = 24, .y = 24 }, }); - inf_lab_apply_command_ctx(s, inf_legacy_context(), &(InfernoLabCommand){ + inf_lab_apply_command_ctx(s, &test_context, &(InfernoLabCommand){ .kind = INF_LAB_COMMAND_SPAWN_NPC, .as.spawn_npc = { .slot = 0, .type = cases[c].type, .x = cases[c].x, .y = cases[c].y, - .hp = { .kind = INF_LAB_OPTIONAL_INT_UNSET }, - .timer = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 0 }, + .hp = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_UNSET }, + .timer = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_SET, .value = 0 }, }, }); @@ -5867,7 +4980,7 @@ static void test_npc_hit_lands_on_the_reference_tick(void) { int hp_before = s->player.current_hitpoints; npc->x = cases[c].x; npc->y = cases[c].y; - inf_step((EncounterState*)s, actions); + inf_step_ctx((EncounterState*)s, (EncounterContext*)&test_context, actions); if (throw_tick < 0 && npc->attacked_this_tick) { throw_tick = t; @@ -5961,14 +5074,14 @@ static void test_phantom_barrage_target_is_masked_until_cast_window(void) { float mask[INF_ACTION_MASK_SIZE]; init_phantom_barrage_test_state(&state, 2, 1); - inf_write_mask((EncounterState*)&state, mask); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); int target_slot = inf_find_target_obs_slot(&state, 0); ASSERT_INT_EQ("dying target appears before cast window", target_slot >= 0, 1); ASSERT_FLOAT_NEAR("next tick phantom target is valid", mask[inferno_target_mask_slot_offset(target_slot)], 1.0f, 1e-6f); init_phantom_barrage_test_state(&state, 2, 2); - inf_write_mask((EncounterState*)&state, mask); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); target_slot = inf_find_target_obs_slot(&state, 0); ASSERT_INT_EQ("cooldown dying target still appears", target_slot >= 0, 1); ASSERT_FLOAT_NEAR("cooldown phantom target is masked", @@ -5983,7 +5096,7 @@ static void test_phantom_barrage_hits_aoe_on_first_cast_window(void) { InfernoState state; init_phantom_barrage_test_state(&state, 1, 1); state.rng_state = seed; - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); int target_slot = inf_find_target_obs_slot(&state, 0); ASSERT_INT_EQ("dying target appears in cast window", target_slot >= 0, 1); if (target_slot < 0) return; @@ -5992,7 +5105,7 @@ static void test_phantom_barrage_hits_aoe_on_first_cast_window(void) { memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(target_slot); actions[INF_HEAD_SPELL] = 2; - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("phantom primary does not receive stale pending hit", state.npcs[0].pending_hits.hits[0].active, 0); @@ -6022,7 +5135,7 @@ static void test_ranged_attack_cannot_fire_on_dying_target(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("ranged attack does not fire on dying target", state.tick_scratch.player_attacked, 0); @@ -6044,7 +5157,7 @@ static void test_autocast_barrage_cannot_fire_on_dying_target(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("autocast does not fire on dying target", state.tick_scratch.player_attacked, 0); @@ -6071,7 +5184,7 @@ static void test_manual_blood_barrage_can_heal_from_dying_primary(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_SPELL] = 1; - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("manual blood barrage fires on dying target", state.tick_scratch.player_attacked, 1); @@ -6092,7 +5205,7 @@ static void test_phantom_barrage_close_barrage_timing_cannot_recast(void) { InfernoState state; init_phantom_barrage_test_state(&state, 1, 2); - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); int target_slot = inf_find_target_obs_slot(&state, 0); ASSERT_INT_EQ("dying target appears during cooldown", target_slot >= 0, 1); if (target_slot < 0) return; @@ -6100,7 +5213,7 @@ static void test_phantom_barrage_close_barrage_timing_cannot_recast(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(target_slot); - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("cooldown prevents phantom barrage fire", state.tick_scratch.player_attacked, 0); @@ -6122,7 +5235,7 @@ static void test_phantom_barrage_does_not_displace_live_obs_slots(void) { state.npcs[i].hp = state.npcs[i].max_hp = INF_NPC_STATS[INF_NPC_NIBBLER].hp; } - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); for (int npc_idx = 1; npc_idx <= 6; npc_idx++) { ASSERT_INT_EQ("live nibbler fills capped obs slot", @@ -6181,7 +5294,7 @@ static int inferno_fire_blood_barrage_at_slot_zero( state->player.attack_timer = 0; encounter_pending_hit_queue_clear(&state->npcs[0].pending_hits); osrs_interaction_set(&state->interaction, 0); - inf_tick_player(state, actions, 1); + inf_tick_player_ctx(state, &test_context, actions, 1); return state->npcs[0].pending_hits.hits[0].hit_success; } @@ -6195,7 +5308,7 @@ static void test_default_autocast_casts_blood_barrage(void) { memset(actions, 0, sizeof(actions)); state.rng_state = 1; state.player.attack_timer = 0; - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("pending hit records blood barrage", state.npcs[0].pending_hits.hits[0].spell_type, ENCOUNTER_SPELL_BLOOD); @@ -6216,7 +5329,7 @@ static void test_ice_barrage_success_freezes_target_and_records_spell(void) { memset(actions, 0, sizeof(actions)); state.rng_state = seed; state.player.attack_timer = 0; - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); if (state.npcs[0].pending_hits.hits[0].hit_success) { found = 1; @@ -6314,7 +5427,7 @@ static void tick_barrage_pending_queue_edge_state(InfernoState* state) { state->npcs[0].hit_damage = 0; state->npcs[0].hit_was_successful_this_tick = 0; inf_resolve_player_projectiles_on_npcs(state); - inf_tick_player(state, actions, 1); + inf_tick_player_ctx(state, &test_context, actions, 1); state->tick++; } @@ -6336,7 +5449,7 @@ static void test_barrage_pending_queue_handles_slow_hit_delay(void) { ASSERT_INT_EQ("barrage timing still sees target SW distance twelve", sw_tile, 12); ASSERT_INT_EQ("player can cast at closest-tile range ten", inf_player_can_attack_npc_from_current_tile_ctx( - &state, inf_legacy_context(), 0), 1); + &state, &test_context, 0), 1); tick_barrage_pending_queue_edge_state(&state); ASSERT_INT_EQ("first cast queues one hit", @@ -6375,7 +5488,7 @@ static void test_barrage_aoe_queues_hits_on_multiple_npcs(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); state.player.attack_timer = 0; - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("primary ranger has queued barrage hit", state.npcs[0].pending_hits.count, 1); @@ -6462,7 +5575,7 @@ static void test_lab_dump_reports_npc_pending_hit_queue(void) { inf_queue_npc_pending_hit( &state, 0, 3, 17, ATTACK_STYLE_MAGIC, ENCOUNTER_SPELL_BLOOD, 1); - char* dump = inf_lab_alloc_json_ctx(&state, inf_legacy_context()); + char* dump = inf_lab_alloc_json_ctx(&state, &test_context); ASSERT_INT_EQ("lab dump includes pending count", strstr(dump, "\"pending_count\":1") != NULL, 1); ASSERT_INT_EQ("lab dump includes pending timer", @@ -6502,7 +5615,7 @@ static void test_spell_without_target_does_not_affect_later_attack(void) { int actions[INF_NUM_ACTION_HEADS]; memset(actions, 0, sizeof(actions)); actions[INF_HEAD_SPELL] = 2; - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("spell without target does not fire", state.tick_scratch.player_attacked, 0); @@ -6547,7 +5660,7 @@ static void test_blood_barrage_at_full_hp_is_valid_and_heals_zero(void) { InfernoState state; init_spell_cast_test_state(&state, INF_NPC_NIBBLER); float mask[INF_ACTION_MASK_SIZE]; - inf_write_mask((EncounterState*)&state, mask); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); ASSERT_FLOAT_NEAR("blood barrage action valid at full HP", mask[inferno_action_head_mask_offset(INF_HEAD_SPELL) + 1], 1.0f, 1e-6f); @@ -6574,7 +5687,7 @@ static void test_manual_spell_in_range_gear_uses_range_gear_magic_stats(void) { state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; InfPlayerAttack attack; - int resolved = inf_resolve_player_attack(&state, ENCOUNTER_SPELL_ICE, &attack); + int resolved = inf_resolve_player_attack_ctx(&state, &test_context, ENCOUNTER_SPELL_ICE, &attack); EncounterLoadoutStats expected; encounter_compute_player_equipped_stats( &state.player, ATTACK_STYLE_MAGIC, FIGHT_STYLE_AUTOCAST, 30, &expected); @@ -6601,14 +5714,14 @@ static void test_phantom_barrage_allows_explicit_spell_from_range_gear(void) { inf_refresh_live_stats(&state); state.player.autocast_enabled = 1; state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); int target_slot = inf_find_target_obs_slot(&state, 0); ASSERT_INT_EQ("dying target appears in obs slots", target_slot >= 0, 1); if (target_slot < 0) return; float mask[INF_ACTION_MASK_SIZE]; - inf_write_mask((EncounterState*)&state, mask); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); ASSERT_FLOAT_NEAR("explicit spell can target phantom from range gear", mask[inferno_target_mask_slot_offset(target_slot)], 1.0f, 1e-6f); @@ -6616,7 +5729,7 @@ static void test_phantom_barrage_allows_explicit_spell_from_range_gear(void) { memset(actions, 0, sizeof(actions)); actions[INF_HEAD_PRIMARY] = inf_primary_attack_action_for_obs_slot(target_slot); actions[INF_HEAD_SPELL] = 2; - inf_tick_player(&state, actions, 1); + inf_tick_player_ctx(&state, &test_context, actions, 1); ASSERT_INT_EQ("explicit phantom barrage fires from range gear", state.tick_scratch.player_attacked, 1); @@ -6663,8 +5776,8 @@ static void test_zuk_obs_tracks_shield_and_mager_aggro(void) { float obs[INF_NUM_OBS]; float mask[INF_ACTION_MASK_SIZE]; - inf_write_obs((EncounterState*)&state, obs); - inf_write_mask((EncounterState*)&state, mask); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); int mager_slot = inf_find_target_obs_slot(&state, 0); int shield_slot = inf_find_target_obs_slot(&state, 2); @@ -6688,8 +5801,8 @@ static void test_zuk_obs_tracks_shield_and_mager_aggro(void) { memset(obs, 0, sizeof(obs)); memset(mask, 0, sizeof(mask)); - inf_write_obs((EncounterState*)&state, obs); - inf_write_mask((EncounterState*)&state, mask); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); + inf_write_mask_ctx((EncounterState*)&state, (EncounterContext*)&test_context, mask); ASSERT_INT_EQ("dead shield drops out of shield slot", state.current_obs_slots[shield_slot], -1); ASSERT_FLOAT_NEAR("dead shield slot hp zeros out", obs[shield_hp], 0.0f, 1e-6f); @@ -6728,7 +5841,7 @@ static void test_zuk_healer_obs_exposes_target_category(void) { state.npcs[1].aggro_target = 0; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int healer_slot = inf_find_target_obs_slot(&state, 1); int healer_target_category = inferno_obs_slot_target_category_start(healer_slot); @@ -6740,7 +5853,7 @@ static void test_zuk_healer_obs_exposes_target_category(void) { state.npcs[1].aggro_target = -1; memset(obs, 0, sizeof(obs)); - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); ASSERT_FLOAT_NEAR("tagged Zuk healer target uses compact player code", obs[healer_target_category], @@ -6771,7 +5884,7 @@ static void test_inferno_obs_target_categories_cover_boss_helpers(void) { state.npcs[1].aggro_target = 0; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int healer_slot = inf_find_target_obs_slot(&state, 1); int target_category = inferno_obs_slot_target_category_start(healer_slot); @@ -6783,7 +5896,7 @@ static void test_inferno_obs_target_categories_cover_boss_helpers(void) { state.npcs[1].aggro_target = -1; memset(obs, 0, sizeof(obs)); - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); ASSERT_FLOAT_NEAR("tagged Jad healer uses compact player code", obs[target_category], (float)INF_TARGET_CATEGORY_PLAYER / 8.0f, 1e-6f); @@ -6817,7 +5930,7 @@ static void test_zuk_set_obs_los_uses_current_target(void) { state.zuk.shield_idx = 1; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int mager_slot = inf_find_target_obs_slot(&state, 0); int mager_target_category = inferno_obs_slot_target_category_start(mager_slot); @@ -6859,7 +5972,7 @@ static void test_zuk_set_threat_ignores_shield_target(void) { state.zuk.shield_idx = 1; InfNpcPlayerThreat threat = - inf_npc_player_threat(&state, &state.npcs[0]); + inf_npc_player_threat_ctx(&state, &test_context, &state.npcs[0]); ASSERT_INT_EQ("shield-targeted mager does not threaten player", threat.can_attack_if_ready, 0); ASSERT_INT_EQ("shield-targeted mager has no player style mask", @@ -6868,17 +5981,17 @@ static void test_zuk_set_threat_ignores_shield_target(void) { static void child_inf_put_bad_start_wave(void) { InfernoState state = make_test_state(0, 0); - inf_put_int((EncounterState*)&state, "start_wave", 0); + inf_put_int_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "start_wave", 0); } static void child_inf_put_unknown_int(void) { InfernoState state = make_test_state(0, 0); - inf_put_int((EncounterState*)&state, "bogus_key", 1); + inf_put_int_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "bogus_key", 1); } static void child_inf_put_removed_win_bonus(void) { InfernoState state = make_test_state(0, 0); - inf_put_float((EncounterState*)&state, "win_bonus_coeff", 1.0f); + inf_put_float_ctx((EncounterState*)&state, (EncounterContext*)&test_context, "win_bonus_coeff", 1.0f); } static void child_encounter_emit_projectile_overflow(void) { @@ -6898,12 +6011,12 @@ static void child_inf_pending_spark_overflow(void) { static void child_inf_restore_previous_inventory_snapshot(void) { EncounterState* raw = inf_create(); - inf_reset(raw, 123u); - size_t snap_size = inf_snapshot_size(raw); + inf_reset_ctx(raw, (EncounterContext*)&test_context, 123u); + size_t snap_size = inf_snapshot_size_ctx(raw, (EncounterContext*)&test_context); InfSnapshot* snap = (InfSnapshot*)malloc(snap_size); - inf_snapshot(raw, snap); + inf_snapshot_ctx(raw, (EncounterContext*)&test_context, snap); snap->version = INF_SNAPSHOT_VERSION - 1u; - inf_restore(raw, snap, snap_size); + inf_restore_ctx(raw, (EncounterContext*)&test_context, snap, snap_size); free(snap); inf_destroy(raw); } @@ -7018,17 +6131,17 @@ static void test_human_target_and_potion_translation(void) { { float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); } ASSERT_INT_EQ("first visible mager is targetable", - inf_is_human_targetable_npc_slot((EncounterState*)&state, 0), 1); + inf_is_human_targetable_npc_slot_ctx((EncounterState*)&state, (EncounterContext*)&test_context, 0), 1); ASSERT_INT_EQ("second visible mager is targetable", - inf_is_human_targetable_npc_slot((EncounterState*)&state, 1), 1); + inf_is_human_targetable_npc_slot_ctx((EncounterState*)&state, (EncounterContext*)&test_context, 1), 1); ASSERT_INT_EQ("third capped-out mager is not targetable", - inf_is_human_targetable_npc_slot((EncounterState*)&state, 2), 0); + inf_is_human_targetable_npc_slot_ctx((EncounterState*)&state, (EncounterContext*)&test_context, 2), 0); ASSERT_INT_EQ("shield is never targetable", - inf_is_human_targetable_npc_slot((EncounterState*)&state, 3), 0); + inf_is_human_targetable_npc_slot_ctx((EncounterState*)&state, (EncounterContext*)&test_context, 3), 0); { HumanInput hi; @@ -7036,14 +6149,14 @@ static void test_human_target_and_potion_translation(void) { hi = make_human_input(); hi.pending_target_idx = 0; - inf_translate_human_input(&hi, actions, (EncounterState*)&state); + inf_translate_human_input_ctx(&hi, actions, (EncounterState*)&state, (EncounterContext*)&test_context); ASSERT_INT_EQ("visible mager click maps into primary target range", actions[INF_HEAD_PRIMARY], inf_primary_attack_action_for_obs_slot(0)); hi = make_human_input(); hi.pending_target_idx = 2; - inf_translate_human_input(&hi, actions, (EncounterState*)&state); + inf_translate_human_input_ctx(&hi, actions, (EncounterState*)&state, (EncounterContext*)&test_context); ASSERT_INT_EQ("capped-out mager click is rejected", actions[INF_HEAD_PRIMARY], 0); @@ -7051,13 +6164,13 @@ static void test_human_target_and_potion_translation(void) { hi.pending_move_x = state.player.x + 1; hi.pending_move_y = state.player.y; hi.pending_target_idx = 2; - inf_translate_human_input(&hi, actions, (EncounterState*)&state); + inf_translate_human_input_ctx(&hi, actions, (EncounterState*)&state, (EncounterContext*)&test_context); ASSERT_INT_EQ("untargetable click preserves queued east move", actions[INF_HEAD_PRIMARY], 3); hi = make_human_input(); hi.pending_target_idx = 3; - inf_translate_human_input(&hi, actions, (EncounterState*)&state); + inf_translate_human_input_ctx(&hi, actions, (EncounterState*)&state, (EncounterContext*)&test_context); ASSERT_INT_EQ("shield click is rejected", actions[INF_HEAD_PRIMARY], 0); @@ -7069,7 +6182,7 @@ static void test_human_target_and_potion_translation(void) { hi = make_human_input(); hi.pending_potion = POTION_BREW; - inf_translate_human_input(&hi, actions, (EncounterState*)&state); + inf_translate_human_input_ctx(&hi, actions, (EncounterState*)&state, (EncounterContext*)&test_context); ASSERT_INT_EQ("brew maps to a brew drink click", test_drink_click_kind(&state, actions[INF_HEAD_DRINK]), OSRS_CONSUMABLE_BREW); @@ -7077,28 +6190,28 @@ static void test_human_target_and_potion_translation(void) { hi = make_human_input(); hi.pending_potion = POTION_RESTORE; - inf_translate_human_input(&hi, actions, (EncounterState*)&state); + inf_translate_human_input_ctx(&hi, actions, (EncounterState*)&state, (EncounterContext*)&test_context); ASSERT_INT_EQ("restore maps to a restore drink click", test_drink_click_kind(&state, actions[INF_HEAD_DRINK]), OSRS_CONSUMABLE_SUPER_RESTORE); hi = make_human_input(); hi.pending_potion = POTION_BASTION; - inf_translate_human_input(&hi, actions, (EncounterState*)&state); + inf_translate_human_input_ctx(&hi, actions, (EncounterState*)&state, (EncounterContext*)&test_context); ASSERT_INT_EQ("bastion maps to a bastion drink click", test_drink_click_kind(&state, actions[INF_HEAD_DRINK]), OSRS_CONSUMABLE_BASTION); hi = make_human_input(); hi.pending_potion = POTION_STAMINA; - inf_translate_human_input(&hi, actions, (EncounterState*)&state); + inf_translate_human_input_ctx(&hi, actions, (EncounterState*)&state, (EncounterContext*)&test_context); ASSERT_INT_EQ("stamina maps to a stamina drink click", test_drink_click_kind(&state, actions[INF_HEAD_DRINK]), OSRS_CONSUMABLE_STAMINA); hi = make_human_input(); hi.pending_potion = POTION_PRAYER_POT; - inf_translate_human_input(&hi, actions, (EncounterState*)&state); + inf_translate_human_input_ctx(&hi, actions, (EncounterState*)&state, (EncounterContext*)&test_context); ASSERT_INT_EQ("prayer pot no longer aliases to restore", actions[INF_HEAD_DRINK], 0); } @@ -7121,11 +6234,11 @@ static void test_human_targeting_refreshes_stale_obs_slots(void) { ASSERT_INT_EQ("stale targetability slots start invalid", state.current_obs_slots_valid, 0); ASSERT_INT_EQ("live ranger is targetable without prior obs write", - inf_is_human_targetable_npc_slot((EncounterState*)&state, 5), 1); + inf_is_human_targetable_npc_slot_ctx((EncounterState*)&state, (EncounterContext*)&test_context, 5), 1); ASSERT_INT_EQ("targetability refresh validates obs slots", state.current_obs_slots_valid, 1); - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); int ranger_slot = inf_find_target_obs_slot(&state, 5); ASSERT_INT_EQ("live ranger has a dense observation slot", ranger_slot >= 0, 1); @@ -7138,7 +6251,7 @@ static void test_human_targeting_refreshes_stale_obs_slots(void) { inf_invalidate_current_obs_slots(&state); ASSERT_INT_EQ("pending target slots start invalid", state.current_obs_slots_valid, 0); - inf_translate_human_input(&hi, actions, (EncounterState*)&state); + inf_translate_human_input_ctx(&hi, actions, (EncounterState*)&state, (EncounterContext*)&test_context); ASSERT_INT_EQ("pending target refresh validates obs slots", state.current_obs_slots_valid, 1); ASSERT_INT_EQ("pending human target refreshes obs slot", @@ -7155,7 +6268,7 @@ static void test_human_targeting_refreshes_stale_obs_slots(void) { inf_invalidate_current_obs_slots(&state); ASSERT_INT_EQ("queued target slots start invalid", state.current_obs_slots_valid, 0); - inf_translate_human_commands(&hi, actions, &state); + inf_translate_human_commands_ctx(&hi, actions, &state, &test_context); ASSERT_INT_EQ("queued target refresh validates obs slots", state.current_obs_slots_valid, 1); ASSERT_INT_EQ("queued human target refreshes obs slot", @@ -7176,14 +6289,14 @@ static void test_human_spell_selection_is_client_local_until_target_click(void) hi.selected_spell = OSRS_SPELL_BLOOD_BARRAGE; int actions[INF_NUM_ACTION_HEADS]; - inf_translate_human_commands(&hi, actions, &state); + inf_translate_human_commands_ctx(&hi, actions, &state, &test_context); ASSERT_INT_EQ("client-only selection queues no command", hi.commands.count, 0); ASSERT_INT_EQ("client-only selection sends no spell action", actions[INF_HEAD_SPELL], 0); human_input_queue_spell_target(&hi, OSRS_SPELL_BLOOD_BARRAGE, 0); - inf_translate_human_commands(&hi, actions, &state); + inf_translate_human_commands_ctx(&hi, actions, &state, &test_context); ASSERT_INT_EQ("spell target command kind", hi.commands.items[0].kind, HUMAN_COMMAND_SPELL_TARGET); ASSERT_INT_EQ("spell target command carries blood", @@ -7206,7 +6319,7 @@ static void test_human_walk_command_sends_no_selected_spell_cast(void) { human_input_queue_walk(&hi, 20, 20); int actions[INF_NUM_ACTION_HEADS]; - inf_translate_human_commands(&hi, actions, &state); + inf_translate_human_commands_ctx(&hi, actions, &state, &test_context); ASSERT_INT_EQ("walk command sends no spell action", actions[INF_HEAD_SPELL], 0); ASSERT_INT_EQ("walk command sends no primary action", @@ -7220,7 +6333,7 @@ static void test_human_autocast_selection_persists_across_weapon_switches(void) InfernoState state; init_spell_cast_test_state(&state, INF_NPC_RANGER); - inf_legacy_context()->human_command_mode = 1; + test_context.human_command_mode = 1; state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; HumanInput hi; @@ -7228,9 +6341,9 @@ static void test_human_autocast_selection_persists_across_weapon_switches(void) human_input_queue_set_autocast(&hi, ENCOUNTER_SPELL_ICE, 1); human_input_queue_equip_inventory_item(&hi, 0, ITEM_TWISTED_BOW, GEAR_SLOT_WEAPON); human_input_queue_equip_inventory_item(&hi, 0, ITEM_KODAI_WAND, GEAR_SLOT_WEAPON); - inf_legacy_context()->human_commands = hi.commands.items; - inf_legacy_context()->human_command_count = hi.commands.count; - inf_apply_human_player_commands(&state); + test_context.human_commands = hi.commands.items; + test_context.human_command_count = hi.commands.count; + inf_apply_human_player_commands_ctx(&state, &test_context); ASSERT_INT_EQ("autocast spell persists after weapon switches", state.player.autocast_spell, ENCOUNTER_SPELL_ICE); @@ -7256,7 +6369,7 @@ static void test_autocast_is_inactive_with_non_autocast_weapon(void) { state.player.autocast_spell = ENCOUNTER_SPELL_ICE; InfPlayerAttack attack; - int resolved = inf_resolve_player_attack(&state, ENCOUNTER_SPELL_NONE, &attack); + int resolved = inf_resolve_player_attack_ctx(&state, &test_context, ENCOUNTER_SPELL_NONE, &attack); ASSERT_INT_EQ("normal attack still resolves", resolved, 1); ASSERT_INT_EQ("non-autocast weapon ignores remembered autocast", @@ -7309,7 +6422,7 @@ static void test_redemption_action_maps_without_smite(void) { printf("--- redemption action maps without smite ---\n"); InfernoState state = make_test_state(20, 20); - InfernoContext* ctx = inf_legacy_context(); + InfernoContext* ctx = &test_context; state.player.base_prayer = 99; state.player.current_prayer = 99; @@ -7407,7 +6520,7 @@ static void test_human_autocast_works_with_dragon_hunter_wand(void) { InfernoState state; init_spell_cast_test_state(&state, INF_NPC_RANGER); - inf_legacy_context()->human_command_mode = 1; + test_context.human_command_mode = 1; state.player.equipped[GEAR_SLOT_WEAPON] = ITEM_DRAGON_HUNTER_WAND; state.player.equipped[GEAR_SLOT_SHIELD] = ITEM_CRYSTAL_SHIELD; state.player.autocast_enabled = 1; @@ -7417,7 +6530,7 @@ static void test_human_autocast_works_with_dragon_hunter_wand(void) { osrs_refresh_player_equipment(&state.player); InfPlayerAttack attack; - int resolved = inf_resolve_player_attack(&state, ENCOUNTER_SPELL_NONE, &attack); + int resolved = inf_resolve_player_attack_ctx(&state, &test_context, ENCOUNTER_SPELL_NONE, &attack); ASSERT_INT_EQ("dragon hunter wand autocast resolves", resolved, 1); @@ -7436,7 +6549,7 @@ static void test_inferno_snapshot_restore_round_trip(void) { EncounterState* raw = inf_create(); InfernoState* state = (InfernoState*)raw; - inf_reset(raw, 31415u); + inf_reset_ctx(raw, (EncounterContext*)&test_context, 31415u); int actions_a[INF_NUM_ACTION_HEADS] = {0}; actions_a[INF_HEAD_PRIMARY] = 1; @@ -7449,30 +6562,30 @@ static void test_inferno_snapshot_restore_round_trip(void) { actions_b[INF_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; const int N1 = 12; - for (int i = 0; i < N1; i++) inf_step(raw, actions_a); + for (int i = 0; i < N1; i++) inf_step_ctx(raw, (EncounterContext*)&test_context, actions_a); - size_t snap_size = inf_snapshot_size(raw); + size_t snap_size = inf_snapshot_size_ctx(raw, (EncounterContext*)&test_context); ASSERT_INT_EQ("snapshot size matches sizeof(InfSnapshot)", (int)snap_size, (int)sizeof(InfSnapshot)); InfSnapshot* snap_A = (InfSnapshot*)malloc(snap_size); - inf_snapshot(raw, snap_A); + inf_snapshot_ctx(raw, (EncounterContext*)&test_context, snap_A); ASSERT_INT_EQ("snapshot magic stamped", (int)snap_A->magic, (int)INF_SNAPSHOT_MAGIC); ASSERT_INT_EQ("snapshot version stamped", (int)snap_A->version, 22); const int N2 = 18; - for (int i = 0; i < N2; i++) inf_step(raw, actions_b); + for (int i = 0; i < N2; i++) inf_step_ctx(raw, (EncounterContext*)&test_context, actions_b); InfSnapshot* snap_B = (InfSnapshot*)malloc(snap_size); - inf_snapshot(raw, snap_B); + inf_snapshot_ctx(raw, (EncounterContext*)&test_context, snap_B); - inf_restore(raw, snap_A, snap_size); + inf_restore_ctx(raw, (EncounterContext*)&test_context, snap_A, snap_size); ASSERT_INT_EQ("tick reset to N1 after restore", state->tick, N1); - for (int i = 0; i < N2; i++) inf_step(raw, actions_b); + for (int i = 0; i < N2; i++) inf_step_ctx(raw, (EncounterContext*)&test_context, actions_b); InfSnapshot* snap_B_prime = (InfSnapshot*)malloc(snap_size); - inf_snapshot(raw, snap_B_prime); + inf_snapshot_ctx(raw, (EncounterContext*)&test_context, snap_B_prime); int diff = memcmp(&snap_B->state, &snap_B_prime->state, sizeof(InfernoState)); ASSERT_INT_EQ("memcmp(state at N1+N2, state after restore+replay) == 0", diff, 0); @@ -7501,17 +6614,17 @@ static void test_inferno_snapshot_preserves_loadout_profile(void) { EncounterState* raw = inf_create(); InfernoState* state = (InfernoState*)raw; - inf_put_int(raw, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); - inf_put_float(raw, "budget_loadout_fraction", 1.0f); - inf_reset(raw, 314u); + inf_put_int_ctx(raw, (EncounterContext*)&test_context, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + inf_put_float_ctx(raw, (EncounterContext*)&test_context, "budget_loadout_fraction", 1.0f); + inf_reset_ctx(raw, (EncounterContext*)&test_context, 314u); - size_t snap_size = inf_snapshot_size(raw); + size_t snap_size = inf_snapshot_size_ctx(raw, (EncounterContext*)&test_context); InfSnapshot* snap = (InfSnapshot*)malloc(snap_size); - inf_snapshot(raw, snap); + inf_snapshot_ctx(raw, (EncounterContext*)&test_context, snap); state->active_loadout_profile = INF_LOADOUT_PROFILE_MAX; - inf_restore(raw, snap, snap_size); + inf_restore_ctx(raw, (EncounterContext*)&test_context, snap, snap_size); ASSERT_INT_EQ("restored active budget profile", state->active_loadout_profile, INF_LOADOUT_PROFILE_BUDGET); @@ -7531,13 +6644,13 @@ static void test_inferno_restore_builds_npc_stats_before_late_spawn(void) { reset_inferno_at_public_wave(raw_a, 69, 1.0f); InfSnapshot snap; - inf_snapshot(raw_a, &snap); + inf_snapshot_ctx(raw_a, (EncounterContext*)&test_context, &snap); inf_destroy(raw_a); EncounterState* raw_b = inf_create(); - inf_put_int(raw_b, "start_wave", 69); + inf_put_int_ctx(raw_b, (EncounterContext*)&test_context, "start_wave", 69); memset(INF_NPC_STATS, 0, sizeof(INF_NPC_STATS)); - inf_restore(raw_b, &snap, sizeof(snap)); + inf_restore_ctx(raw_b, (EncounterContext*)&test_context, &snap, sizeof(snap)); InfernoState* state = (InfernoState*)raw_b; int zuk_idx = find_active_npc_type(state, INF_NPC_ZUK); @@ -7547,7 +6660,7 @@ static void test_inferno_restore_builds_npc_stats_before_late_spawn(void) { state->npcs[zuk_idx].attack_timer = 999; int actions[INF_NUM_ACTION_HEADS] = {0}; - inf_step(raw_b, actions); + inf_step_ctx(raw_b, (EncounterContext*)&test_context, actions); int mager_idx = find_active_npc_type(state, INF_NPC_MAGER); int ranger_idx = find_active_npc_type(state, INF_NPC_RANGER); @@ -7767,7 +6880,7 @@ static void test_inferno_human_equip_does_not_snap_loadout(void) { EncounterState* raw = inf_create(); InfernoState* state = (InfernoState*)raw; - inf_reset(raw, 123); + inf_reset_ctx(raw, (EncounterContext*)&test_context, 123); HumanInput input; human_input_init(&input); @@ -7777,7 +6890,7 @@ static void test_inferno_human_equip_does_not_snap_loadout(void) { human_input_queue_equip_inventory_item( &input, 0, ITEM_TOXIC_BLOWPIPE, GEAR_SLOT_WEAPON); - inf_step_human_commands(raw, &input); + inf_step_human_commands_ctx(raw, (EncounterContext*)&test_context, &input); ASSERT_INT_EQ("weapon changed to clicked blowpipe", state->player.equipped[GEAR_SLOT_WEAPON], ITEM_TOXIC_BLOWPIPE); @@ -7801,7 +6914,7 @@ static void test_jad_render_uses_style_specific_attack_animation(void) { RenderEntity magic_entities[4]; int magic_count = 0; - inf_fill_render_entities((EncounterState*)&magic_state, magic_entities, 4, &magic_count); + inf_fill_render_entities_ctx((EncounterState*)&magic_state, (EncounterContext*)&test_context, magic_entities, 4, &magic_count); InfernoState range_state; init_jad_timing_test_state(&range_state, 10, 10, 16, 10); @@ -7810,7 +6923,7 @@ static void test_jad_render_uses_style_specific_attack_animation(void) { RenderEntity range_entities[4]; int range_count = 0; - inf_fill_render_entities((EncounterState*)&range_state, range_entities, 4, &range_count); + inf_fill_render_entities_ctx((EncounterState*)&range_state, (EncounterContext*)&test_context, range_entities, 4, &range_count); ASSERT_INT_EQ("jad magic render entity count", magic_count, 2); ASSERT_INT_EQ("jad ranged render entity count", range_count, 2); @@ -7826,7 +6939,7 @@ static void test_inferno_render_uses_npc_death_animation(void) { RenderEntity entities[4]; int count = 0; - inf_fill_render_entities((EncounterState*)&state, entities, 4, &count); + inf_fill_render_entities_ctx((EncounterState*)&state, (EncounterContext*)&test_context, entities, 4, &count); ASSERT_INT_EQ("dying NPC still renders", count >= 2, 1); ASSERT_INT_EQ("nibbler death animation", @@ -7843,7 +6956,7 @@ static void test_jad_magic_render_emits_three_offset_projectiles(void) { EncounterOverlay ov; memset(&ov, 0, sizeof(ov)); - inf_render_post_tick((EncounterState*)&state, &ov); + inf_render_post_tick_ctx((EncounterState*)&state, (EncounterContext*)&test_context, &ov); ASSERT_INT_EQ("jad magic emits three projectile models", ov.projectile_count, 3); ASSERT_INT_EQ("jad magic front model", ov.projectiles[0].model_id, INF_GFX_448_MODEL); @@ -7870,7 +6983,7 @@ static void test_jad_ranged_render_uses_target_anchored_two_tick_visual(void) { EncounterOverlay ov; memset(&ov, 0, sizeof(ov)); - inf_render_post_tick((EncounterState*)&state, &ov); + inf_render_post_tick_ctx((EncounterState*)&state, (EncounterContext*)&test_context, &ov); ASSERT_INT_EQ("jad ranged emits one projectile", ov.projectile_count, 1); ASSERT_INT_EQ("jad ranged model", ov.projectiles[0].model_id, INF_GFX_451_MODEL); @@ -7893,7 +7006,7 @@ static void test_jad_projectile_long_distance_visual_duration_uses_reference_for EncounterOverlay range_ov; memset(&range_ov, 0, sizeof(range_ov)); - inf_render_post_tick((EncounterState*)&range_state, &range_ov); + inf_render_post_tick_ctx((EncounterState*)&range_state, (EncounterContext*)&test_context, &range_ov); int range_dist = encounter_projectile_distance( range_state.npcs[0].x, range_state.npcs[0].y, range_state.npcs[0].size, @@ -7912,7 +7025,7 @@ static void test_jad_projectile_long_distance_visual_duration_uses_reference_for EncounterOverlay magic_ov; memset(&magic_ov, 0, sizeof(magic_ov)); - inf_render_post_tick((EncounterState*)&magic_state, &magic_ov); + inf_render_post_tick_ctx((EncounterState*)&magic_state, (EncounterContext*)&test_context, &magic_ov); int magic_dist = encounter_projectile_distance( magic_state.npcs[0].x, magic_state.npcs[0].y, magic_state.npcs[0].size, @@ -7937,7 +7050,7 @@ static void test_inferno_npc_projectile_render_uses_reference_visual_timing(void EncounterOverlay mager_ov; memset(&mager_ov, 0, sizeof(mager_ov)); - inf_render_post_tick((EncounterState*)&mager_state, &mager_ov); + inf_render_post_tick_ctx((EncounterState*)&mager_state, (EncounterContext*)&test_context, &mager_ov); int mager_dist = encounter_projectile_distance( mager_state.npcs[0].x, mager_state.npcs[0].y, mager_state.npcs[0].size, @@ -7974,7 +7087,7 @@ static void test_inferno_npc_projectile_render_uses_reference_visual_timing(void EncounterOverlay ranger_ov; memset(&ranger_ov, 0, sizeof(ranger_ov)); - inf_render_post_tick((EncounterState*)&ranger_state, &ranger_ov); + inf_render_post_tick_ctx((EncounterState*)&ranger_state, (EncounterContext*)&test_context, &ranger_ov); int ranger_dist = encounter_projectile_distance( ranger_state.npcs[0].x, ranger_state.npcs[0].y, ranger_state.npcs[0].size, @@ -8009,7 +7122,7 @@ static void test_inferno_npc_projectile_render_uses_reference_visual_timing(void EncounterOverlay blob_ov; memset(&blob_ov, 0, sizeof(blob_ov)); - inf_render_post_tick((EncounterState*)&blob_state, &blob_ov); + inf_render_post_tick_ctx((EncounterState*)&blob_state, (EncounterContext*)&test_context, &blob_ov); ASSERT_INT_EQ("blob projectile count", blob_ov.projectile_count, 1); ASSERT_INT_EQ("blob ranged projectile model", @@ -8033,7 +7146,7 @@ static void test_inferno_npc_projectile_render_uses_reference_visual_timing(void EncounterOverlay blob_magic_ov; memset(&blob_magic_ov, 0, sizeof(blob_magic_ov)); - inf_render_post_tick((EncounterState*)&blob_magic_state, &blob_magic_ov); + inf_render_post_tick_ctx((EncounterState*)&blob_magic_state, (EncounterContext*)&test_context, &blob_magic_ov); ASSERT_INT_EQ("blob magic projectile count", blob_magic_ov.projectile_count, 1); ASSERT_INT_EQ("blob magic projectile model", @@ -8052,7 +7165,7 @@ static void test_inferno_npc_projectile_render_uses_reference_visual_timing(void EncounterOverlay blob_split_range_ov; memset(&blob_split_range_ov, 0, sizeof(blob_split_range_ov)); - inf_render_post_tick((EncounterState*)&blob_split_range_state, &blob_split_range_ov); + inf_render_post_tick_ctx((EncounterState*)&blob_split_range_state, (EncounterContext*)&test_context, &blob_split_range_ov); ASSERT_INT_EQ("blob split ranged projectile count", blob_split_range_ov.projectile_count, 1); @@ -8070,7 +7183,7 @@ static void test_inferno_npc_projectile_render_uses_reference_visual_timing(void EncounterOverlay blob_split_magic_ov; memset(&blob_split_magic_ov, 0, sizeof(blob_split_magic_ov)); - inf_render_post_tick((EncounterState*)&blob_split_magic_state, &blob_split_magic_ov); + inf_render_post_tick_ctx((EncounterState*)&blob_split_magic_state, (EncounterContext*)&test_context, &blob_split_magic_ov); ASSERT_INT_EQ("blob split magic projectile count", blob_split_magic_ov.projectile_count, 1); @@ -8094,7 +7207,7 @@ static void test_inferno_npc_projectile_render_tracks_target_npc_slot(void) { EncounterOverlay ov; memset(&ov, 0, sizeof(ov)); - inf_render_post_tick((EncounterState*)&state, &ov); + inf_render_post_tick_ctx((EncounterState*)&state, (EncounterContext*)&test_context, &ov); ASSERT_INT_EQ("shield-target projectile count", ov.projectile_count, 1); ASSERT_INT_EQ("shield-target projectile tracks target", ov.projectiles[0].tracks_target, 1); @@ -8116,7 +7229,7 @@ static void test_inferno_zuk_projectile_render_uses_combat_visual_rows(void) { EncounterOverlay ov; memset(&ov, 0, sizeof(ov)); - inf_render_post_tick((EncounterState*)&state, &ov); + inf_render_post_tick_ctx((EncounterState*)&state, (EncounterContext*)&test_context, &ov); ASSERT_INT_EQ("zuk projectile count", ov.projectile_count, 1); ASSERT_INT_EQ("zuk projectile model", ov.projectiles[0].model_id, INF_GFX_1375_MODEL); @@ -8136,7 +7249,7 @@ static void test_inferno_zuk_projectile_render_uses_combat_visual_rows(void) { EncounterOverlay healer_ov; memset(&healer_ov, 0, sizeof(healer_ov)); - inf_render_post_tick((EncounterState*)&healer_state, &healer_ov); + inf_render_post_tick_ctx((EncounterState*)&healer_state, (EncounterContext*)&test_context, &healer_ov); ASSERT_INT_EQ("healer projectile count", healer_ov.projectile_count, 1); ASSERT_INT_EQ("healer projectile model", @@ -8172,7 +7285,7 @@ static void test_player_projectile_render_uses_stored_reference_timing(void) { EncounterOverlay blowpipe_ov; memset(&blowpipe_ov, 0, sizeof(blowpipe_ov)); - inf_render_post_tick((EncounterState*)&blowpipe_state, &blowpipe_ov); + inf_render_post_tick_ctx((EncounterState*)&blowpipe_state, (EncounterContext*)&test_context, &blowpipe_ov); ASSERT_INT_EQ("blowpipe spec projectile count", blowpipe_ov.projectile_count, 1); ASSERT_INT_EQ("blowpipe projectile tracks target", blowpipe_ov.projectiles[0].tracks_target, 1); @@ -8217,7 +7330,7 @@ static void test_player_projectile_render_uses_stored_reference_timing(void) { EncounterOverlay tbow_ov; memset(&tbow_ov, 0, sizeof(tbow_ov)); - inf_render_post_tick((EncounterState*)&tbow_state, &tbow_ov); + inf_render_post_tick_ctx((EncounterState*)&tbow_state, (EncounterContext*)&test_context, &tbow_ov); ASSERT_INT_EQ("tbow projectile count", tbow_ov.projectile_count, 1); ASSERT_INT_EQ("tbow projectile tracks target", tbow_ov.projectiles[0].tracks_target, 1); @@ -8258,7 +7371,7 @@ static void test_player_projectile_render_uses_stored_reference_timing(void) { EncounterOverlay bowfa_ov; memset(&bowfa_ov, 0, sizeof(bowfa_ov)); - inf_render_post_tick((EncounterState*)&bowfa_state, &bowfa_ov); + inf_render_post_tick_ctx((EncounterState*)&bowfa_state, (EncounterContext*)&test_context, &bowfa_ov); ASSERT_INT_EQ("bowfa projectile count", bowfa_ov.projectile_count, 1); ASSERT_INT_EQ("bowfa projectile model", @@ -8314,7 +7427,7 @@ static void test_magic_splash_landing_keeps_spell_visual_context(void) { RenderEntity entities[4]; int count = 0; - inf_fill_render_entities((EncounterState*)&state, entities, 4, &count); + inf_fill_render_entities_ctx((EncounterState*)&state, (EncounterContext*)&test_context, entities, 4, &count); ASSERT_INT_EQ("splashing ranger still emits landed visual event", entities[1].hit_landed_this_tick, 1); @@ -8344,7 +7457,7 @@ static void test_npc_overkill_hit_caps_splat_hp_and_damage_stats(void) { RenderEntity entities[4]; int count = 0; - inf_fill_render_entities((EncounterState*)&state, entities, 4, &count); + inf_fill_render_entities_ctx((EncounterState*)&state, (EncounterContext*)&test_context, entities, 4, &count); ASSERT_INT_EQ("ranger hp clamps at zero", state.npcs[0].hp, 0); ASSERT_INT_EQ("ranger hit splat caps to remaining hp", @@ -8409,7 +7522,7 @@ static void test_elysian_proc_propagates_to_player_render_entity(void) { RenderEntity entities[2]; int count = 0; - inf_fill_render_entities((EncounterState*)&state, entities, 2, &count); + inf_fill_render_entities_ctx((EncounterState*)&state, (EncounterContext*)&test_context, entities, 2, &count); ASSERT_INT_EQ("player render entity exists", count >= 1, 1); ASSERT_INT_EQ("elysian proc reaches player state", @@ -8449,12 +7562,12 @@ static void test_terminal_reward_uses_fixed_win_reward(void) { state.episode_over = 1; state.winner = INF_OUTCOME_PLAYER_WON; ASSERT_FLOAT_NEAR("terminal win reward is fixed", - inf_compute_reward(&state), 1.0f, 1e-6f); + inf_compute_reward_ctx(&state, &test_context), 1.0f, 1e-6f); state.winner = INF_OUTCOME_PLAYER_DIED; test_config()->death_penalty_coeff = 0.25f; ASSERT_FLOAT_NEAR("terminal loss uses configured death penalty", - inf_compute_reward(&state), -0.25f, 1e-6f); + inf_compute_reward_ctx(&state, &test_context), -0.25f, 1e-6f); } static void test_final_wave_completion_emits_terminal_reward(void) { @@ -8577,371 +7690,12 @@ static void test_inferno_render_overlay_reports_death_source(void) { EncounterOverlay ov; memset(&ov, 0, sizeof(ov)); - inf_render_post_tick((EncounterState*)&state, &ov); + inf_render_post_tick_ctx((EncounterState*)&state, (EncounterContext*)&test_context, &ov); ASSERT_INT_EQ("death banner active", ov.status_text_active, 1); ASSERT_STR_EQ("death banner text", ov.status_text, "Killed by TzKal-Zuk"); } -static void test_inferno_binding_forwards_safe_target_reward_coeff(void) { - printf("--- inferno binding forwards safe target reward coeff ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "safe target reward coeff float key", - "ocean/osrs_inferno/osrs_inferno.h", - "float_keys[]", - "};", - "\"zuk_safe_untagged_healer_target_bonus_coeff\""); -} - -static void test_inferno_binding_forwards_healer_attack_shape_coeffs(void) { - printf("--- inferno binding forwards healer attack shape coeffs ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "non-magic healer attack bonus float key", - "ocean/osrs_inferno/osrs_inferno.h", - "float_keys[]", - "};", - "\"zuk_untagged_healer_nonmagic_attack_bonus_coeff\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "mage healer attack penalty float key", - "ocean/osrs_inferno/osrs_inferno.h", - "float_keys[]", - "};", - "\"zuk_healer_mage_attack_penalty_coeff\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "non-magic healer attack bonus config", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "zuk_untagged_healer_nonmagic_attack_bonus_coeff ="); - ASSERT_SOURCE_BLOCK_CONTAINS( - "mage healer attack penalty config", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "zuk_healer_mage_attack_penalty_coeff ="); -} - -static void test_inferno_binding_forwards_supply_milestone_rewards(void) { - printf("--- inferno binding forwards supply milestone rewards ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "supply milestone brew float key", - "ocean/osrs_inferno/osrs_inferno.h", - "float_keys[]", - "};", - "\"supply_milestone_brew_reward_coeff\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "supply milestone restore float key", - "ocean/osrs_inferno/osrs_inferno.h", - "float_keys[]", - "};", - "\"supply_milestone_restore_reward_coeff\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "supply milestone brew default off", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "supply_milestone_brew_reward_coeff = 0.0"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "supply milestone restore default off", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "supply_milestone_restore_reward_coeff = 0.0"); -} - -static void test_inferno_binding_forwards_offensive_prayer_reward(void) { - printf("--- inferno binding forwards offensive prayer reward ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "offensive prayer float key", - "ocean/osrs_inferno/osrs_inferno.h", - "float_keys[]", - "};", - "\"offensive_prayer_reward_coeff\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "offensive prayer default config", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "offensive_prayer_reward_coeff = 0"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "offensive prayer sweep config", - "config/osrs_inferno.ini", - "[sweep.env.offensive_prayer_reward_coeff]", - "[sweep.env.shield_penalty_coeff]", - "max = 1.0"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "offensive prayer correct metric", - "ocean/osrs_inferno/osrs_inferno.h", - "offensive_prayer_correct_rate", - "brews_remaining", - "offensive_prayer_magic_correct_rate"); -} - -static void test_inferno_binding_forwards_curriculum_supply_config(void) { - printf("--- inferno binding forwards curriculum supply config ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "curriculum supply shared jitter float key", - "ocean/osrs_inferno/osrs_inferno.h", - "float_keys[]", - "};", - "\"curriculum_supply_shared_jitter\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "curriculum supply jitter mode int key", - "ocean/osrs_inferno/osrs_inferno.h", - "int_keys[]", - "};", - "\"curriculum_supply_jitter_mode\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "curriculum no-brew mode int key", - "ocean/osrs_inferno/osrs_inferno.h", - "int_keys[]", - "};", - "\"curriculum_no_brew_mode\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "curriculum agent marker assigned by mixer", - "ocean/osrs_inferno/osrs_inferno.h", - "inferno_env_put_int(env, \"start_wave\", waves[t]);", - "return;", - "inferno_env_put_int(env, \"curriculum_agent\", 1);"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "curriculum supply defaults off", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "curriculum_supply_jitter_mode = 1"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "curriculum no-brew defaults off", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "curriculum_no_brew_frac = 0.0"); -} - -static void test_inferno_binding_forwards_post_healer_set_rewards(void) { - printf("--- inferno binding forwards post-healer set rewards ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "post-healer set damage coeff float key", - "ocean/osrs_inferno/osrs_inferno.h", - "float_keys[]", - "};", - "\"post_healer_set_damage_reward_coeff\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "post-healer set alive penalty float key", - "ocean/osrs_inferno/osrs_inferno.h", - "float_keys[]", - "};", - "\"post_healer_set_alive_tick_penalty_coeff\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "post-healer set alive penalty default off", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "post_healer_set_alive_penalty_cap = 0.0"); - ASSERT_SOURCE_BLOCK_NOT_CONTAINS( - "post-healer set alive penalty not swept", - "config/osrs_inferno.ini", - "[sweep]", - "[sweep.train.total_timesteps]", - "post_healer_set_alive_tick_penalty_coeff"); - ASSERT_SOURCE_NOT_CONTAINS( - "post-healer set alive penalty sweep section removed", - "config/osrs_inferno.ini", - "[sweep.env.post_healer_set_alive_tick_penalty_coeff]"); - ASSERT_SOURCE_NOT_CONTAINS( - "post-healer set alive cap sweep section removed", - "config/osrs_inferno.ini", - "[sweep.env.post_healer_set_alive_penalty_cap]"); -} - -static void test_inferno_binding_forwards_joseph_reward_mode(void) { - printf("--- inferno binding forwards Joseph reward mode ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "Joseph reward mode int parsed", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_init", - "inferno_apply_curriculum(env, kwargs);", - "dict_get(kwargs, \"joseph_reward_mode\")"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "Joseph reward mode config", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "joseph_reward_mode = 1"); -} - -static void test_inferno_binding_forwards_safe_healer_target_mask(void) { - printf("--- inferno binding forwards safe healer target mask ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "safe healer target mask int parsed", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_init", - "inferno_apply_curriculum(env, kwargs);", - "dict_get(kwargs, \"zuk_safe_untagged_healer_target_mask\")"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "safe healer target mask default config", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "zuk_safe_untagged_healer_target_mask = 0"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "force safe healer target mask int parsed", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_init", - "inferno_apply_curriculum(env, kwargs);", - "dict_get(kwargs, \"zuk_force_safe_untagged_healer_target_mask\")"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "force safe healer target mask default config", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "zuk_force_safe_untagged_healer_target_mask = 0"); -} - -static void test_inferno_binding_forwards_terminal_penalty_toggle(void) { - printf("--- inferno binding forwards terminal penalty toggle ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "terminal penalty int parsed", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_init", - "inferno_apply_curriculum(env, kwargs);", - "dict_get(kwargs, \"terminal_penalty_enabled\")"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "terminal penalty default config", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "terminal_penalty_enabled = 0"); - ASSERT_SOURCE_BLOCK_NOT_CONTAINS( - "terminal penalty not swept", - "config/osrs_inferno.ini", - "[sweep]", - "[sweep.train.total_timesteps]", - "terminal_penalty_enabled"); - ASSERT_SOURCE_NOT_CONTAINS( - "terminal penalty sweep section removed", - "config/osrs_inferno.ini", - "[sweep.env.terminal_penalty_enabled]"); -} - -static void test_inferno_binding_forwards_loadout_profile_config(void) { - printf("--- inferno binding forwards loadout profile config ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "budget loadout fraction float key", - "ocean/osrs_inferno/osrs_inferno.h", - "float_keys[]", - "};", - "\"budget_loadout_fraction\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "loadout profile mode int parsed", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_init", - "inferno_apply_curriculum(env, kwargs);", - "dict_get(kwargs, \"loadout_profile_mode\")"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "loadout profile mode default config", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "loadout_profile_mode = 0"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "budget loadout fraction default config", - "config/osrs_inferno.ini", - "[env]", - "[vec]", - "budget_loadout_fraction = 0.0"); -} - -static void test_inferno_binding_logs_post_healer_set_reward_components(void) { - printf("--- inferno binding logs post-healer set reward components ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "post-healer set coeff metric", - "ocean/osrs_inferno/osrs_inferno.h", - "post_healer_set_damage_reward_coeff_normal", - "action_mask_checks_normal", - "post_healer_set_alive_penalty_coeff_normal"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "post-healer set reward component metric", - "ocean/osrs_inferno/osrs_inferno.h", - "post_healer_set_damage_reward_normal", - "action_mask_checks_normal", - "post_healer_set_alive_penalty_normal"); -} - -static void test_inferno_binding_logs_idle_diagnostics(void) { - printf("--- inferno binding logs idle diagnostics ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "attack-ready idle metric emitted", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_log", - "float wr = log->wins", - "attack_ready_no_attack_ticks"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "target-available idle metric emitted", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_log", - "float wr = log->wins", - "target_available_no_attack_ticks"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "safe opportunity idle metric emitted", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_log", - "float wr = log->wins", - "safe_attack_opportunity_missed_ticks"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "progressless metric emitted", - "ocean/osrs_inferno/osrs_inferno.h", - "void puf_log", - "float wr = log->wins", - "progressless_ticks"); - ASSERT_SOURCE_BLOCK_CONTAINS( - "idle phase names emitted", - "ocean/osrs_inferno/osrs_inferno.h", - "inferno_log_idle_metric", - "void puf_log", - "zuk_post_healers"); -} - -static void test_inferno_log_metrics_fit_cuda_dict(void) { - printf("--- inferno log metrics fit CUDA dict ---\n"); - - int metric_count = inferno_my_log_metric_key_count(); - ASSERT_INT_LE("puf_log metric key count plus env/n", metric_count + 1, 64); -} - -static void test_curriculum_supports_wave60_bridge_tier(void) { - printf("--- curriculum supports wave60 bridge tier ---\n"); - - ASSERT_SOURCE_BLOCK_CONTAINS( - "curriculum has room for wave60 bridge", - "ocean/osrs_inferno/osrs_inferno.h", - "wave_keys[] = {", - "frac_keys[] = {", - "\"curriculum_wave_8\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "curriculum parses fifth wave key", - "ocean/osrs_inferno/osrs_inferno.h", - "wave_keys[] = {", - "frac_keys[] = {", - "\"curriculum_wave_5\""); - ASSERT_SOURCE_BLOCK_CONTAINS( - "curriculum parses fifth fraction key", - "ocean/osrs_inferno/osrs_inferno.h", - "frac_keys[] = {", - "int waves[8]", - "\"curriculum_frac_5\""); -} static void test_inferno_reset_uses_osrs_run_energy_units(void) { printf("--- inferno reset uses osrs run energy units ---\n"); @@ -9002,7 +7756,7 @@ static void test_dense_target_slots_follow_type_priority_without_holes(void) { 2, 4, 6, 7, 1, 8, 9, 3, 5, 10, 11, 12, 13, 0, }; - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); for (int slot = 0; slot < 14; slot++) { ASSERT_INT_EQ("dense slot follows type priority", @@ -9021,7 +7775,7 @@ static void test_regular_waves_select_every_live_npc(void) { InfernoState state = make_test_state(20, 20); state.wave = wave; inf_spawn_wave(&state); - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); for (int npc_idx = 0; npc_idx < INF_MAX_NPCS; npc_idx++) { if (!state.npcs[npc_idx].active || @@ -9058,7 +7812,7 @@ static void test_maximal_zuk_concurrency_selects_all_live_candidates(void) { for (int npc_idx = 0; npc_idx < 14; npc_idx++) activate_dense_target_test_npc(&state, npc_idx, types[npc_idx]); - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); int selected = 0; for (int slot = 0; slot < INF_OBS_NPCS; slot++) @@ -9086,7 +7840,7 @@ static void refresh_fifteen_eligible_target_candidates(void) { InfernoState state = make_test_state(20, 20); for (int npc_idx = 0; npc_idx < 15; npc_idx++) activate_dense_target_test_npc(&state, npc_idx, types[npc_idx]); - inf_refresh_current_obs_slots(&state); + inf_refresh_current_obs_slots_ctx(&state, &test_context); } static void test_dense_target_overflow_aborts_instead_of_truncating(void) { @@ -9106,9 +7860,9 @@ static void test_compact_observation_layout_contract(void) { ASSERT_INT_EQ("compact pending hit stride", INF_FEATURES_PER_HIT, 3); ASSERT_INT_EQ("compact spark stride", INF_FEATURES_PER_SPARK, 4); ASSERT_INT_EQ("inventory carries one canonical code per cell", - INF_INVENTORY_OBS_SIZE, 28); + OSRS_SHARED_INVENTORY_OBS_SIZE, 28); ASSERT_INT_EQ("equipment carries one canonical code per worn slot", - INF_EQUIPPED_OBS_SIZE, NUM_GEAR_SLOTS); + OSRS_SHARED_EQUIPPED_OBS_SIZE, NUM_GEAR_SLOTS); ASSERT_INT_EQ("shared prefix end", INF_OBS_AFTER_SHARED, 101); ASSERT_INT_EQ("inferno encounter end", INF_OBS_AFTER_ENCOUNTER, 115); @@ -9143,7 +7897,7 @@ static void test_compact_player_and_pillar_observation_semantics(void) { }; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); ASSERT_FLOAT_NEAR("shared overhead prayer is one-hot", obs[8], 1.0f, 1e-6f); @@ -9198,11 +7952,11 @@ static void test_compact_npc_observation_semantics(void) { state.npcs[1].dig_freeze_timer = 3; float obs[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); int blob_slot = inf_find_target_obs_slot(&state, 0); int blob_start = INF_OBS_AFTER_PILLARS + blob_slot * INF_NPC_SLOT_FEATURES; - InfNpcPlayerThreat blob_threat = inf_npc_player_threat(&state, &state.npcs[0]); + InfNpcPlayerThreat blob_threat = inf_npc_player_threat_ctx(&state, &test_context, &state.npcs[0]); ASSERT_FLOAT_NEAR("compact NPC type code", obs[blob_start], (float)(INF_NPC_BLOB + 1) / 16.0f, 1e-6f); ASSERT_FLOAT_NEAR("compact NPC hp", obs[blob_start + 1], 0.5f, 1e-6f); @@ -9215,7 +7969,7 @@ static void test_compact_npc_observation_semantics(void) { obs[blob_start + 5], (float)ATTACK_STYLE_MAGIC / 4.0f, 1e-6f); ASSERT_FLOAT_NEAR("compact NPC LOS", obs[blob_start + 6], - (float)inf_npc_has_los_ctx(&state, inf_legacy_context(), 0), + (float)inf_npc_has_los_ctx(&state, &test_context, 0), 1e-6f); ASSERT_FLOAT_NEAR("compact NPC frozen timer", obs[blob_start + 7], 0.5f, 1e-6f); @@ -9241,7 +7995,7 @@ static void test_compact_npc_observation_semantics(void) { state.npcs[1].dig_freeze_timer = 0; state.npcs[1].dig_attack_delay = 3; - inf_write_obs((EncounterState*)&state, obs); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs); ASSERT_FLOAT_NEAR("meleer compact dig-freeze state clears", obs[meleer_start + 11], 0.0f, 1e-6f); ASSERT_FLOAT_NEAR("meleer compact dig-delay state", @@ -9297,7 +8051,7 @@ static void test_compact_transient_inventory_equipment_semantics(void) { }; float obs_off[INF_NUM_OBS]; - inf_write_obs((EncounterState*)&state, obs_off); + inf_write_obs_ctx((EncounterState*)&state, (EncounterContext*)&test_context, obs_off); ASSERT_FLOAT_NEAR("compact pending hit style", obs_off[INF_OBS_AFTER_NPCS], (float)ATTACK_STYLE_MAGIC / 4.0f, 1e-6f); @@ -9434,7 +8188,7 @@ static void test_inferno_topology_geometry_parity(void) { printf("--- inferno topology geometry parity ---\n"); InfernoState state = make_test_state(20, 20); - const InfernoContext* ctx = inf_legacy_context(); + const InfernoContext* ctx = &test_context; int footprint_checks = 0; int los_checks = 0; const int target_sizes[] = {1, INF_PILLAR_SIZE, 5}; @@ -9570,7 +8324,7 @@ static void test_inferno_topology_geometry_parity(void) { static void test_pillar_removal_resets_same_tick_los_frame(void) { printf("--- inferno pillar removal resets same-tick LOS frame ---\n"); - InfernoContext* ctx = inf_legacy_context(); + InfernoContext* ctx = &test_context; InfernoState state = make_test_state(30, 40); set_inferno_pillar_phase(&state, 1); state.pillars[0].hp = 1; @@ -9607,7 +8361,7 @@ static void test_inferno_topology_observation_mask_identity(void) { printf("--- inferno topology observation and mask identity ---\n"); InfernoState state = make_test_state(20, 20); - InfernoContext* ctx = inf_legacy_context(); + InfernoContext* ctx = &test_context; static const InfNPCType types[] = { INF_NPC_BAT, INF_NPC_BLOB, @@ -9702,8 +8456,8 @@ static void test_observation_overwrites_dirty_buffer(void) { float clean[INF_NUM_OBS] = {0}; float dirty[INF_NUM_OBS]; memset(dirty, 0x7f, sizeof(dirty)); - inf_write_obs(raw_state, clean); - inf_write_obs(raw_state, dirty); + inf_write_obs_ctx(raw_state, (EncounterContext*)&test_context, clean); + inf_write_obs_ctx(raw_state, (EncounterContext*)&test_context, dirty); ASSERT_INT_EQ( "Inferno observation overwrites every output", memcmp(clean, dirty, sizeof(clean)), @@ -9714,7 +8468,7 @@ static void test_observation_overwrites_dirty_buffer(void) { int main(void) { inf_build_npc_stats(); - inf_finalize_route_topology(inf_legacy_context()); + inf_init_context_typed(&test_context); test_inferno_topology_geometry_parity(); test_pillar_removal_resets_same_tick_los_frame(); test_inferno_topology_observation_mask_identity(); @@ -9729,17 +8483,12 @@ int main(void) { test_maximal_zuk_concurrency_selects_all_live_candidates(); test_dense_target_overflow_aborts_instead_of_truncating(); - test_attack_chase_uses_reachable_approach_tile(); - test_attack_chase_routes_around_los_blocker_while_in_range(); - test_attack_chase_routes_around_los_blocker_for_short_range(); test_melee_fallback_geometry(); - test_style_mask_preview(); test_style_choice_sampling(); test_tagged_jad_healer_melee_geometry(); test_overlap_shuffle_hold_after_recent_target_click(); test_overlap_shuffle_respects_npc_occupancy(); test_large_npc_overlap_shuffle_can_partially_unclip(); - test_player_movement_ignores_npc_occupancy(); test_tagged_jad_healer_stops_at_melee_contact(); test_tagged_jad_healers_queue_behind_front_healer(); test_meleer_dig_can_stack_without_losing_collision_flag(); @@ -9822,7 +8571,6 @@ int main(void) { test_npc_threat_obs_exposes_frozen_meleer_pressure(); test_npc_threat_obs_respects_overlap_range_and_stun(); test_npc_threat_obs_keeps_ranger_mager_diagonal_melee(); - test_barrage_total_target_count_matches_resolution_targets(); test_jad_special_wave_spawn_cadence_matches_reference(); test_triple_jad_first_attacks_are_staggered(); test_jad_melee_stays_instant_and_untelegraphed(); @@ -9830,18 +8578,6 @@ int main(void) { test_inferno_npc_travel_uses_sw_origin_around_all_pillars(); test_inferno_jal_npcs_use_edge_clearance_at_pillars(); test_step_out_forecast_north_pillar_ranger_mager_order(); - test_fast_step_out_forecast_matches_movement_head_destinations(); - test_fast_step_out_forecast_immediate_static_threats(); - test_fast_step_out_forecast_blob_scan_and_melee_fallback(); - test_fast_step_out_forecast_does_not_mutate_state(); - test_readonly_step_out_forecast_matches_movement_head_destinations(); - test_readonly_step_out_forecast_does_not_mutate_state(); - test_readonly_step_out_forecast_pillar_step_out_cases(); - test_readonly_step_out_forecast_stun_countdown(); - test_readonly_step_out_forecast_frozen_can_attack(); - test_readonly_step_out_forecast_blob_scanned_fire(); - test_readonly_step_out_forecast_under_player_overlap_is_danger(); - test_readonly_step_out_forecast_invalid_movement_zeroes_payload(); test_step_out_forecast_south_pillar_ranger_mager_order(); test_step_out_forecast_west_pillar_ranger_mager_order(); test_step_out_forecast_inactive_pillar_does_not_create_cover(); @@ -9897,8 +8633,6 @@ int main(void) { test_render_motion_continuous_movement_never_pauses(); test_render_motion_waypoint_pop_snap_and_overflow(); test_render_motion_seed_classification_uses_explicit_teleport(); - test_render_post_tick_removed_distance_snap_branch(); - test_render_bridge_marks_genuine_teleports(); test_entity_model_ground_lift_keeps_floor_planes_above_terrain(); test_spotanim_lookup_prefers_recolored_model_alias(); test_inferno_npc_spawn_id_changes_on_slot_reuse(); @@ -9958,20 +8692,6 @@ int main(void) { test_timeout_reward_matches_episode_return(); test_terminal_penalty_applies_to_timeout_when_enabled(); test_inferno_render_overlay_reports_death_source(); - test_inferno_binding_forwards_safe_target_reward_coeff(); - test_inferno_binding_forwards_healer_attack_shape_coeffs(); - test_inferno_binding_forwards_supply_milestone_rewards(); - test_inferno_binding_forwards_offensive_prayer_reward(); - test_inferno_binding_forwards_curriculum_supply_config(); - test_inferno_binding_forwards_post_healer_set_rewards(); - test_inferno_binding_forwards_joseph_reward_mode(); - test_inferno_binding_forwards_safe_healer_target_mask(); - test_inferno_binding_forwards_terminal_penalty_toggle(); - test_inferno_binding_forwards_loadout_profile_config(); - test_inferno_binding_logs_post_healer_set_reward_components(); - test_inferno_binding_logs_idle_diagnostics(); - test_inferno_log_metrics_fit_cuda_dict(); - test_curriculum_supports_wave60_bridge_tier(); test_inferno_reset_uses_osrs_run_energy_units(); return osrs_test_summary(); diff --git a/ocean/osrs/tests/test_inferno_forecast_exact.c b/ocean/osrs/tests/test_inferno_forecast_exact.c deleted file mode 100644 index b41f7c5a59..0000000000 --- a/ocean/osrs/tests/test_inferno_forecast_exact.c +++ /dev/null @@ -1,479 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ocean/osrs/encounters/encounter_inferno.h" -#include "ocean/osrs/tests/osrs_route_reference.h" - -#define EXACT_MAGIC "INFEXACTv2" -#define EXACT_VERSION 2u -#define EXACT_CHUNK_BYTES 65536 -#define EXACT_ROLLOUT_STEPS 64 -#define EXACT_ENV_SEED 0x01FEC0DEu -#define EXACT_ACTION_SEED 0xD1B54A32D192ED03ULL - -typedef struct { - char magic[16]; - uint32_t version; - uint32_t state_size; - uint32_t forecast_size; - uint32_t obs_size; - uint32_t record_count; -} InfExactFileHeader; - -typedef struct { - uint32_t scenario_id; - uint32_t step_index; - uint32_t tick; - uint32_t public_start_wave; - uint32_t wave; - uint32_t terminal; - uint32_t winner; - uint32_t state_size; - uint32_t obs_size; - uint32_t forecast_size; - uint64_t state_hash; - uint64_t forecast_hash; - uint64_t obs_hash; - float reward; -} InfExactRecordHeader; - -typedef struct { - FILE* file; - uint32_t record_count; -} InfExactWriter; - -static uint64_t exact_fnv_bytes(uint64_t h, const void* data, size_t size) { - const uint8_t* bytes = (const uint8_t*)data; - for (size_t i = 0; i < size; i++) { - h ^= bytes[i]; - h *= 1099511628211ULL; - } - return h; -} - -static uint64_t exact_hash_bytes(const void* data, size_t size) { - return exact_fnv_bytes(1469598103934665603ULL, data, size); -} - -static void exact_write_all(FILE* file, const void* data, size_t size) { - if (fwrite(data, 1, size, file) != size) { - perror("write inferno exact fixture"); - abort(); - } -} - -static void exact_readable_path( - char* out, - size_t out_size, - const char* dir, - const char* file_name -) { - int n = snprintf(out, out_size, "%s/%s", dir, file_name); - if (n < 0 || (size_t)n >= out_size) { - fprintf(stderr, "fixture path too long: %s/%s\n", dir, file_name); - abort(); - } -} - -static void exact_mkdir_if_needed(const char* dir) { - if (mkdir(dir, 0777) == 0) return; - if (errno == EEXIST) return; - perror("mkdir inferno exact fixture dir"); - abort(); -} - -static void exact_writer_open(InfExactWriter* writer, const char* path) { - memset(writer, 0, sizeof(*writer)); - writer->file = fopen(path, "wb"); - if (!writer->file) { - perror("open inferno exact fixture"); - abort(); - } - - InfExactFileHeader header = {0}; - memcpy(header.magic, EXACT_MAGIC, sizeof(EXACT_MAGIC)); - header.version = EXACT_VERSION; - header.state_size = (uint32_t)sizeof(InfernoState); - header.forecast_size = (uint32_t)sizeof(InfStepOutForecast); - header.obs_size = INF_NUM_OBS; - exact_write_all(writer->file, &header, sizeof(header)); -} - -static void exact_writer_close(InfExactWriter* writer) { - InfExactFileHeader header = {0}; - memcpy(header.magic, EXACT_MAGIC, sizeof(EXACT_MAGIC)); - header.version = EXACT_VERSION; - header.state_size = (uint32_t)sizeof(InfernoState); - header.forecast_size = (uint32_t)sizeof(InfStepOutForecast); - header.obs_size = INF_NUM_OBS; - header.record_count = writer->record_count; - if (fseek(writer->file, 0, SEEK_SET) != 0) { - perror("seek inferno exact fixture"); - abort(); - } - exact_write_all(writer->file, &header, sizeof(header)); - if (fclose(writer->file) != 0) { - perror("close inferno exact fixture"); - abort(); - } - writer->file = NULL; -} - -static void exact_canonicalize_state(InfernoState* state) { -#ifdef OSRS_INTERACTION_SERIALIZED_ROUTE_BYTES - osrs_interaction_zero_serialized_route_padding(&state->player.interaction); - osrs_interaction_zero_serialized_route_padding(&state->interaction); -#else - memset(&state->player.interaction.route, 0, sizeof(state->player.interaction.route)); - memset(&state->interaction.route, 0, sizeof(state->interaction.route)); -#endif - memset( - state->reserved_topology_pillar_storage, - 0, - sizeof(state->reserved_topology_pillar_storage)); - memset( - state->reserved_topology_npc_storage, - 0, - sizeof(state->reserved_topology_npc_storage)); - memset( - state->reserved_topology_player_footprint_storage, - 0, - sizeof(state->reserved_topology_player_footprint_storage)); -} - -static void exact_capture( - InfExactWriter* writer, - uint32_t scenario_id, - uint32_t step_index, - uint32_t public_start_wave, - InfernoState* s, - InfernoContext* ctx -) { - InfStepOutForecast forecast; - float obs[INF_NUM_OBS]; - - inf_build_step_out_forecast_ctx(s, ctx, &forecast); - inf_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); - InfernoState canonical_state = *s; - exact_canonicalize_state(&canonical_state); - - InfExactRecordHeader record = {0}; - record.scenario_id = scenario_id; - record.step_index = step_index; - record.tick = (uint32_t)s->tick; - record.public_start_wave = public_start_wave; - record.wave = (uint32_t)s->wave; - record.terminal = (uint32_t)inf_is_terminal_ctx( - (EncounterState*)s, (EncounterContext*)ctx); - record.winner = (uint32_t)s->winner; - record.state_size = (uint32_t)sizeof(*s); - record.obs_size = INF_NUM_OBS; - record.forecast_size = (uint32_t)sizeof(forecast); - record.state_hash = exact_hash_bytes(&canonical_state, sizeof(canonical_state)); - record.forecast_hash = exact_hash_bytes(&forecast, sizeof(forecast)); - record.obs_hash = exact_hash_bytes(obs, sizeof(obs)); - record.reward = inf_get_reward_ctx((EncounterState*)s, (EncounterContext*)ctx); - - exact_write_all(writer->file, &record, sizeof(record)); - exact_write_all(writer->file, &forecast, sizeof(forecast)); - exact_write_all(writer->file, obs, sizeof(obs)); - exact_write_all(writer->file, &canonical_state, sizeof(canonical_state)); - writer->record_count++; -} - -static uint64_t exact_splitmix64(uint64_t* state) { - uint64_t z = (*state += 0x9E3779B97F4A7C15ULL); - z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; - z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; - return z ^ (z >> 31); -} - -static void exact_trace_actions( - uint64_t* rng, - int actions[INF_NUM_ACTION_HEADS] -) { - for (int head = 0; head < INF_NUM_ACTION_HEADS; head++) { - actions[head] = (int)(exact_splitmix64(rng) % - (uint64_t)INF_ACTION_DIMS[head]); - } -} - -static void exact_init_state( - InfernoState* s, - InfernoContext* ctx, - int public_start_wave, - uint32_t seed -) { - inf_init_context_typed(ctx); - memset(s, 0, sizeof(*s)); - inf_put_int_ctx( - (EncounterState*)s, - (EncounterContext*)ctx, - "start_wave", - public_start_wave); - inf_finalize_route_topology(ctx); - inf_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); -} -typedef struct { - InfernoState* state; - InfernoContext* context; -} InfExactRouteContext; - -static int exact_inferno_route_blocked( - void* data, - int x, - int y, - int size -) { - InfExactRouteContext* route_ctx = (InfExactRouteContext*)data; - return inf_footprint_blocked_ctx( - route_ctx->state, route_ctx->context, x, y, size); -} - -static int exact_inferno_can_attack( - void* data, - int player_x, - int player_y, - int target_x, - int target_y, - int target_size, - int attack_range -) { - InfExactRouteContext* route_ctx = (InfExactRouteContext*)data; - OsrsLosQuery query = inf_player_los_query(route_ctx->state); - return encounter_player_can_attack( - player_x, - player_y, - target_x, - target_y, - target_size, - attack_range, - route_ctx->context->collision_map, - route_ctx->context->world_offset_x, - route_ctx->context->world_offset_y, - &query); -} - -static void exact_inferno_route_equivalence(void) { - InfernoState state; - InfernoContext context; - exact_init_state(&state, &context, 1, EXACT_ENV_SEED); - InfExactRouteContext route_ctx = { - .state = &state, - .context = &context, - }; - static const int attack_ranges[] = {1, 5, 10}; - uint64_t checks = osrs_reference_route_exhaustive( - "all-pillars", - context.route_topology, - (EncounterRouteBlockers){ - .is_blocked = exact_inferno_route_blocked, - .ctx = &route_ctx, - .revision = inf_player_route_blocker_revision(&state), - }, - 5, - attack_ranges, - 3, - exact_inferno_can_attack, - &route_ctx); - state.pillars[1].active = 0; - checks += osrs_reference_route_exhaustive( - "middle-pillar-collapsed", - context.route_topology, - (EncounterRouteBlockers){ - .is_blocked = exact_inferno_route_blocked, - .ctx = &route_ctx, - .revision = inf_player_route_blocker_revision(&state), - }, - 5, - attack_ranges, - 3, - exact_inferno_can_attack, - &route_ctx); - printf( - "inferno exhaustive route equivalence PASS: %llu source-target-range queries across 2 blocker fields\n", - (unsigned long long)checks); -} - -static void exact_run_wave_rollout( - InfExactWriter* writer, - uint32_t scenario_id, - int public_start_wave -) { - InfernoState s; - InfernoContext ctx; - exact_init_state(&s, &ctx, public_start_wave, EXACT_ENV_SEED); - - uint32_t capture_idx = 0; - uint64_t action_rng = EXACT_ACTION_SEED; - int actions[INF_NUM_ACTION_HEADS]; - exact_capture(writer, scenario_id, capture_idx++, public_start_wave, &s, &ctx); - for (int step = 0; step < EXACT_ROLLOUT_STEPS; step++) { - exact_trace_actions(&action_rng, actions); - inf_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); - exact_capture(writer, scenario_id, capture_idx++, public_start_wave, &s, &ctx); - if (s.episode_over) break; - } -} - -static void exact_generate_fixture(const char* path) { - InfExactWriter writer; - exact_writer_open(&writer, path); - - exact_run_wave_rollout(&writer, 1u, 1); - exact_run_wave_rollout(&writer, 62u, 62); - exact_run_wave_rollout(&writer, 67u, 67); - exact_run_wave_rollout(&writer, 69u, 69); - - exact_writer_close(&writer); -} - -static void exact_read_all(FILE* file, void* data, size_t size) { - if (fread(data, 1, size, file) != size) { - fprintf(stderr, "truncated inferno exact fixture\n"); - abort(); - } -} - -static int exact_compare_files(const char* expected_path, const char* actual_path) { - FILE* expected = fopen(expected_path, "rb"); - if (!expected) { - perror("open expected inferno exact fixture"); - abort(); - } - FILE* actual = fopen(actual_path, "rb"); - if (!actual) { - perror("open actual inferno exact fixture"); - abort(); - } - - InfExactFileHeader expected_file; - InfExactFileHeader actual_file; - exact_read_all(expected, &expected_file, sizeof(expected_file)); - exact_read_all(actual, &actual_file, sizeof(actual_file)); - if (memcmp(&expected_file, &actual_file, sizeof(expected_file)) != 0) { - printf("inferno exact mismatch: file header\n"); - fclose(expected); - fclose(actual); - return 1; - } - - for (uint32_t index = 0; index < expected_file.record_count; index++) { - InfExactRecordHeader expected_record; - InfExactRecordHeader actual_record; - InfStepOutForecast expected_forecast; - InfStepOutForecast actual_forecast; - float expected_obs[INF_NUM_OBS]; - float actual_obs[INF_NUM_OBS]; - InfernoState expected_state; - InfernoState actual_state; - exact_read_all(expected, &expected_record, sizeof(expected_record)); - exact_read_all(actual, &actual_record, sizeof(actual_record)); - exact_read_all(expected, &expected_forecast, sizeof(expected_forecast)); - exact_read_all(actual, &actual_forecast, sizeof(actual_forecast)); - exact_read_all(expected, expected_obs, sizeof(expected_obs)); - exact_read_all(actual, actual_obs, sizeof(actual_obs)); - exact_read_all(expected, &expected_state, sizeof(expected_state)); - exact_read_all(actual, &actual_state, sizeof(actual_state)); - - if (exact_hash_bytes(&expected_state, sizeof(expected_state)) != - expected_record.state_hash || - exact_hash_bytes(&actual_state, sizeof(actual_state)) != - actual_record.state_hash) { - printf("inferno exact fixture state hash mismatch at record %u\n", index); - fclose(expected); - fclose(actual); - return 1; - } - exact_canonicalize_state(&expected_state); - exact_canonicalize_state(&actual_state); - expected_record.state_hash = - exact_hash_bytes(&expected_state, sizeof(expected_state)); - actual_record.state_hash = - exact_hash_bytes(&actual_state, sizeof(actual_state)); - if (memcmp(&expected_record, &actual_record, sizeof(expected_record)) != 0 || - memcmp(&expected_forecast, &actual_forecast, sizeof(expected_forecast)) != 0 || - memcmp(expected_obs, actual_obs, sizeof(expected_obs)) != 0 || - memcmp(&expected_state, &actual_state, sizeof(expected_state)) != 0) { - printf( - "inferno exact mismatch at record %u scenario %u step %u\n", - index, - expected_record.scenario_id, - expected_record.step_index); - const uint8_t* expected_bytes = (const uint8_t*)&expected_state; - const uint8_t* actual_bytes = (const uint8_t*)&actual_state; - for (size_t byte = 0; byte < sizeof(expected_state); byte++) { - if (expected_bytes[byte] == actual_bytes[byte]) continue; - printf( - "first state byte mismatch offset=%zu expected=%u actual=%u player expected=(%d,%d) actual=(%d,%d)\n", - byte, - (unsigned)expected_bytes[byte], - (unsigned)actual_bytes[byte], - expected_state.player.x, - expected_state.player.y, - actual_state.player.x, - actual_state.player.y); - break; - } - fclose(expected); - fclose(actual); - return 1; - } - } - - int expected_tail = fgetc(expected); - int actual_tail = fgetc(actual); - fclose(expected); - fclose(actual); - if (expected_tail != EOF || actual_tail != EOF) { - printf("inferno exact mismatch: trailing data\n"); - return 1; - } - return 0; -} - -int main(int argc, char** argv) { - if (argc == 2 && strcmp(argv[1], "--attack-route-selftest") == 0) { - inf_build_npc_stats(); - exact_inferno_route_equivalence(); - return 0; - } - if (argc != 3 || - (strcmp(argv[1], "--write-golden") != 0 && - strcmp(argv[1], "--compare") != 0)) { - fprintf(stderr, - "usage: %s --attack-route-selftest | --write-golden DIR | --compare DIR\n", - argv[0]); - return 2; - } - - inf_build_npc_stats(); - - char fixture_path[1024]; - char current_path[1024]; - exact_mkdir_if_needed(argv[2]); - exact_readable_path( - fixture_path, sizeof(fixture_path), argv[2], - "inferno_forecast_exact.bin"); - - if (strcmp(argv[1], "--write-golden") == 0) { - exact_generate_fixture(fixture_path); - printf("inferno exact golden wrote %s\n", fixture_path); - return 0; - } - - exact_readable_path( - current_path, sizeof(current_path), argv[2], - "inferno_forecast_exact.current.bin"); - exact_generate_fixture(current_path); - int failed = exact_compare_files(fixture_path, current_path); - if (failed) return 1; - printf("inferno exact golden compare PASS: %s\n", fixture_path); - return 0; -} diff --git a/ocean/osrs/tests/test_inferno_lab.c b/ocean/osrs/tests/test_inferno_lab.c index cea10c88e6..cec384086e 100644 --- a/ocean/osrs/tests/test_inferno_lab.c +++ b/ocean/osrs/tests/test_inferno_lab.c @@ -52,8 +52,8 @@ static void test_lab_typed_commands_mutate_state(void) { .type = INF_NPC_RANGER, .x = 24, .y = 31, - .hp = { .kind = INF_LAB_OPTIONAL_INT_UNSET }, - .timer = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 3 }, + .hp = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_UNSET }, + .timer = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_SET, .value = 3 }, }, }); ASSERT_INT_EQ("ranger active", state->npcs[0].active, 1); @@ -83,7 +83,7 @@ static void test_lab_typed_commands_mutate_state(void) { .as.pillar = { .pillar_idx = 2, .state = INF_LAB_PILLAR_REMOVED, - .hp = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 0 }, + .hp = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_SET, .value = 0 }, }, }); ASSERT_INT_EQ("north pillar inactive", state->pillars[2].active, 0); @@ -194,8 +194,8 @@ static void test_lab_kill_refresh_uses_finalized_context(void) { .type = INF_NPC_RANGER, .x = 28, .y = 40, - .hp = {.kind = INF_LAB_OPTIONAL_INT_SET, .value = 1}, - .timer = {.kind = INF_LAB_OPTIONAL_INT_SET, .value = 0}, + .hp = {.kind = ENCOUNTER_LAB_OPTIONAL_INT_SET, .value = 1}, + .timer = {.kind = ENCOUNTER_LAB_OPTIONAL_INT_SET, .value = 0}, }, }); inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ @@ -234,8 +234,8 @@ static void test_lab_snapshot_restore_round_trip(void) { .type = INF_NPC_MAGER, .x = 27, .y = 32, - .hp = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 99 }, - .timer = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 2 }, + .hp = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_SET, .value = 99 }, + .timer = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_SET, .value = 2 }, }, }); inf_lab_apply_command_ctx(state, &lab_context, &(InfernoLabCommand){ @@ -243,7 +243,7 @@ static void test_lab_snapshot_restore_round_trip(void) { .as.pillar = { .pillar_idx = 1, .state = INF_LAB_PILLAR_REMOVED, - .hp = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 0 }, + .hp = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_SET, .value = 0 }, }, }); state->wave = 11; @@ -274,7 +274,7 @@ static void test_lab_snapshot_restore_round_trip(void) { .as.pillar = { .pillar_idx = 1, .state = INF_LAB_PILLAR_ACTIVE, - .hp = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = INF_PILLAR_HP }, + .hp = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_SET, .value = INF_PILLAR_HP }, }, }); state->wave = 60; diff --git a/ocean/osrs/tests/test_osrs_player_step.c b/ocean/osrs/tests/test_osrs_player_step.c index 685ced18f6..e3fcdc2ff1 100644 --- a/ocean/osrs/tests/test_osrs_player_step.c +++ b/ocean/osrs/tests/test_osrs_player_step.c @@ -21,7 +21,6 @@ static void* topology_counted_calloc(size_t count, size_t size); #define calloc topology_counted_calloc #include "ocean/osrs/osrs_encounter_player.h" -#include "ocean/osrs/tests/osrs_route_reference.h" #undef calloc static void* topology_counted_calloc(size_t count, size_t size) { @@ -45,9 +44,6 @@ typedef struct { OsrsAttackTarget target; int target_valid; } StepTargetCtx; -typedef struct { - int calls; -} StepWalkableCountCtx; typedef struct { int calls; int open_x; @@ -56,15 +52,6 @@ typedef struct { -static int step_tile_walkable(void* ctx, int x, int y) { - (void)ctx; - return x >= 0 && x < STEP_GRID && y >= 0 && y < STEP_GRID; -} -static int step_counted_tile_walkable(void* ctx, int x, int y) { - StepWalkableCountCtx* count = (StepWalkableCountCtx*)ctx; - count->calls++; - return x >= 0 && x < STEP_GRID && y >= 0 && y < STEP_GRID; -} static int step_vertical_wall(void* ctx, int x, int y) { @@ -198,58 +185,8 @@ static void test_ranged_chase_targets_nearest_attack_position(void) { } -static void test_attack_route_rejects_cardinal_adjacency_through_wall(void) { - CollisionMap map; - CollisionRegion region; - collision_map_init(&map); - memset(®ion, 0, sizeof(region)); - collision_map_put(&map, collision_region_hash(9, 10), ®ion); - region.flags[0][9][10] = COLLISION_WALL_EAST; - region.flags[0][10][10] = COLLISION_WALL_WEST; - const OsrsLosQuery* los = osrs_los_open_query(); - PathResult route = encounter_pathfind_arena_attack_approach( - &map, 0, 0, - 9, 10, - 10, 10, 1, 1, - step_tile_walkable, NULL, - NULL, NULL, - los, - 0, 0, STEP_GRID, STEP_GRID); - - CHECK("wall-adjacent route moves around the wall", - route.found && (route.next_dx != 0 || route.next_dy != 0)); -} - -static void test_attack_route_fallback_stays_within_ten_tiles(void) { - const OsrsLosQuery* los = osrs_los_open_query(); - PathResult route = encounter_pathfind_arena_attack_approach( - NULL, 0, 0, - 10, 10, - -20, 10, 1, 1, - step_tile_walkable, NULL, - NULL, NULL, - los, - 0, 0, STEP_GRID, STEP_GRID); - - CHECK("fallback outside the ten-tile target radius fails", !route.found); -} -static void test_attack_route_uses_rsmod_cardinal_first_order(void) { - PathResult route = encounter_pathfind_arena_attack_approach( - NULL, 0, 0, - 10, 10, - 8, 8, 1, 1, - step_tile_walkable, NULL, - NULL, NULL, - osrs_los_open_query(), - 0, 0, STEP_GRID, STEP_GRID); - - CHECK("RSMod route takes west before southwest", - route.found && - route.next_dx == -1 && route.next_dy == 0 && - route.run_dx == -1 && route.run_dy == -1); -} static void test_melee_attack_does_not_cross_cardinal_wall(void) { CollisionMap map; @@ -301,10 +238,6 @@ static void test_melee_attack_does_not_cross_cardinal_wall(void) { free(wall_topology); } -/* an entity click cancels a walk already in flight: the player closes on the - target instead of continuing to the clicked tile. this is the nh_pvp/zulrah - regression — EXPLICIT_FIRST used to honour both, giving free damage while - repositioning. */ static void test_target_command_cancels_walk_in_flight(void) { printf("--- entity click cancels a walk in flight ---\n"); @@ -669,125 +602,9 @@ static void test_target_selection_does_not_own_route_cache(void) { memcmp(&route_cache, &preserved, sizeof(preserved)) == 0); } -static void test_attack_route_field_stops_at_first_reachable_target_edge(void) { - EncounterArenaAttackRouteField field; - encounter_build_arena_attack_route_field( - &field, - NULL, - 0, - 0, - 2, - 2, - 12, - 2, - 1, - step_tile_walkable, - NULL, - NULL, - NULL, - 0, - 0, - STEP_GRID, - STEP_GRID); - - EncounterAttackRouteLanding landing = encounter_arena_attack_route_landing( - &field, - 12, - 2, - 1, - 1, - osrs_los_open_query()); - - CHECK("goal-directed field stops before exhausting the arena", - field.visited_count < STEP_GRID * STEP_GRID); - CHECK("goal-directed field keeps the first FIFO target edge", - landing.route_found && landing.route_x == 11 && landing.route_y == 2); -} -static void test_unreachable_attack_route_field_exhausts_for_fallback(void) { - EncounterArenaAttackRouteField field; - encounter_build_arena_attack_route_field( - &field, - NULL, - 0, - 0, - 10, - 10, - -20, - 10, - 1, - step_tile_walkable, - NULL, - NULL, - NULL, - 0, - 0, - STEP_GRID, - STEP_GRID); - - CHECK("unreachable target exhausts the field for fallback", - field.visited_count == STEP_GRID * STEP_GRID); -} -static void test_attack_route_caches_walkability_per_tile(void) { - EncounterArenaAttackRouteField field; - StepWalkableCountCtx walkable_count = {0}; - encounter_build_arena_attack_route_field( - &field, - NULL, - 0, - 0, - 1, - 1, - 22, - 22, - 1, - step_counted_tile_walkable, - &walkable_count, - NULL, - NULL, - 0, - 0, - STEP_GRID, - STEP_GRID); - - CHECK("route field checks each arena tile at most once", - walkable_count.calls <= STEP_GRID * STEP_GRID); -} -static void test_attackable_target_skips_route_scan(void) { - Player player; - memset(&player, 0, sizeof(player)); - player.x = 10; - player.y = 10; - player.dest_x = 7; - player.dest_y = 8; - StepWalkableCountCtx walkable_count = {0}; - - int moved = encounter_chase_attack_target( - &player, - 12, - 10, - 1, - 2, - NULL, - 0, - 0, - step_counted_tile_walkable, - &walkable_count, - NULL, - NULL, - osrs_los_open_query(), - 0, - 0, - 0, - 0); - - CHECK("attackable target does not move", moved == 0); - CHECK("attackable target skips walkability scan", walkable_count.calls == 0); - CHECK("attackable target preserves destination", - player.dest_x == 7 && player.dest_y == 8); -} typedef struct { @@ -1477,18 +1294,11 @@ int main(void) { test_arena_topology_extreme_origins_and_reader_bounds(); test_arena_topology_bounds_collision_los_and_revision(); test_target_selection_does_not_own_route_cache(); - test_attack_route_field_stops_at_first_reachable_target_edge(); - test_unreachable_attack_route_field_exhausts_for_fallback(); - test_attack_route_caches_walkability_per_tile(); - test_attackable_target_skips_route_scan(); test_ranged_chase_targets_nearest_attack_position(); test_target_command_cancels_walk_in_flight(); test_move_command_cancels_interaction(); test_destination_click_routes_around_wall(); test_none_command_chases_active_interaction(); - test_attack_route_rejects_cardinal_adjacency_through_wall(); - test_attack_route_fallback_stays_within_ten_tiles(); - test_attack_route_uses_rsmod_cardinal_first_order(); test_melee_attack_does_not_cross_cardinal_wall(); test_attack_route_persists_and_invalidates_canonically(); test_failed_attack_route_persists_and_invalidates(); diff --git a/ocean/osrs/tests/test_osrs_pvp_pending_hits.c b/ocean/osrs/tests/test_osrs_pvp_pending_hits.c index b6fde1a721..8dfde38637 100644 --- a/ocean/osrs/tests/test_osrs_pvp_pending_hits.c +++ b/ocean/osrs/tests/test_osrs_pvp_pending_hits.c @@ -281,28 +281,12 @@ static void test_nh_attack_chase_destinations(void) { static void test_nh_out_of_arena_destination_fallback(void) { printf("--- NH PvP out-of-arena destination fallback ---\n"); - Player player = {0}; - player.x = FIGHT_AREA_BASE_X + 1; - player.y = FIGHT_AREA_BASE_Y + 1; - int destination_x = -1; - int destination_y = -1; - CHECK("farcast click beyond the arena keeps a route fallback target", - select_farcast_tile( - &player, - FIGHT_AREA_BASE_X + 5, - FIGHT_AREA_BASE_Y, - 6, - &destination_x, - &destination_y, - nh_context.route_topology)); - ASSERT_INT_EQ("farcast keeps the original x destination", - destination_x, FIGHT_AREA_BASE_X - 1); - ASSERT_INT_EQ("farcast keeps the original y destination", - destination_y, FIGHT_AREA_BASE_Y + 1); + int destination_x = FIGHT_AREA_BASE_X - 1; + int destination_y = FIGHT_AREA_BASE_Y + 1; EncounterRouteInput route_input = { .topology = nh_context.route_topology, - .source_x = player.x, - .source_y = player.y, + .source_x = FIGHT_AREA_BASE_X + 1, + .source_y = FIGHT_AREA_BASE_Y + 1, .actor_size = 1, .target_x = destination_x, .target_y = destination_y, @@ -398,7 +382,7 @@ static void test_nh_shared_contract_and_inventory_actions(void) { osrs_inventory_cell_item_index( &player->inventory_cells[ranged_cell]) == previous_weapon); - int food_cell = nh_pvp_find_consumable_cell( + int food_cell = human_pvp_find_consumable_cell( player, OSRS_CONSUMABLE_SHARK_FOOD); CHECK("reset exposes a food cell", food_cell >= 0); player->current_hitpoints = 50; @@ -421,7 +405,7 @@ static void test_nh_masked_drink_is_not_executed(void) { (EncounterContext*)&nh_context, 2); Player* player = &nh_state->env.players[0]; - int brew_cell = nh_pvp_find_consumable_cell( + int brew_cell = human_pvp_find_consumable_cell( player, OSRS_CONSUMABLE_BREW); CHECK("reset exposes a brew cell", brew_cell >= 0); player->current_hitpoints = diff --git a/ocean/osrs/tests/test_zulrah_hit_delay.c b/ocean/osrs/tests/test_zulrah_hit_delay.c index 0441a85497..461ee0d20a 100644 --- a/ocean/osrs/tests/test_zulrah_hit_delay.c +++ b/ocean/osrs/tests/test_zulrah_hit_delay.c @@ -73,7 +73,10 @@ static int ticks_until_player_hit(ZulrahState* s, AttackStyle style) { for (int t = 1; t <= 16; t++) { int before = s->player.current_hitpoints; - zul_resolve_player_pending_hits(s); + encounter_resolve_player_pending_hits_observed( + &s->player_pending_hits, &s->player, s->player.prayer, + &s->damage_received_this_tick, NULL, NULL, + zul_player_hit_landed, s); if (s->player.current_hitpoints < before) return t; } return 0; @@ -171,8 +174,6 @@ static void test_player_hit_delay_by_distance(void) { } } -/** The canonical contract draws damage FIRST. Under the old inverted order the - accuracy roll consumed draw 1, so the queued damage could not match. */ static void test_damage_drawn_before_accuracy(void) { printf("\n--- damage roll precedes the accuracy roll ---\n"); @@ -219,8 +220,6 @@ static void test_damage_drawn_before_accuracy(void) { CHECK("collected magic hits for the draw-order probe", magic_checked > 0); } -/** Prayer decides whether damage is frozen to zero, never how many draws the - attack consumes. This is what the skipped-roll defect broke. */ static void test_prayer_does_not_perturb_the_rng_stream(void) { printf("\n--- prayer state leaves the RNG stream untouched ---\n"); @@ -285,7 +284,11 @@ static void test_prayer_resolves_at_throw_not_landing(void) { late_on_checked++; s->player.prayer = PRAYER_PROTECT_RANGED; - for (int t = 0; t < 8; t++) zul_resolve_player_pending_hits(s); + for (int t = 0; t < 8; t++) + encounter_resolve_player_pending_hits_observed( + &s->player_pending_hits, &s->player, s->player.prayer, + &s->damage_received_this_tick, NULL, NULL, + zul_player_hit_landed, s); CHECK("praying after the throw does not cancel the hit", s->player.current_hitpoints < s->player.base_hitpoints); @@ -301,15 +304,17 @@ static void test_prayer_resolves_at_throw_not_landing(void) { zul_attack_ranged(s); s->player.prayer = PRAYER_NONE; - for (int t = 0; t < 8; t++) zul_resolve_player_pending_hits(s); + for (int t = 0; t < 8; t++) + encounter_resolve_player_pending_hits_observed( + &s->player_pending_hits, &s->player, s->player.prayer, + &s->damage_received_this_tick, NULL, NULL, + zul_player_hit_landed, s); ASSERT_INT_EQ("dropping prayer after the throw does not revive the hit", s->player.current_hitpoints, s->player.base_hitpoints); } } -/** Defect 3 mirror image: the melee stare froze its protect check at landing, - three ticks after Zulrah committed to the attack. */ static void test_melee_stare_reads_prayer_at_calculation(void) { printf("\n--- melee stare reads prayer at the calculation tick ---\n"); @@ -320,7 +325,7 @@ static void test_melee_stare_reads_prayer_at_calculation(void) { s->player.current_hitpoints = s->player.base_hitpoints; s->player.prayer = PRAYER_NONE; - zul_melee_start(s); + zul_fire_action(s, &g_ctx, ZA_MELEE); s->player.prayer = PRAYER_PROTECT_MELEE; zul_melee_hit(s); @@ -335,7 +340,7 @@ static void test_melee_stare_reads_prayer_at_calculation(void) { s->player.current_hitpoints = s->player.base_hitpoints; s->player.prayer = PRAYER_PROTECT_MELEE; - zul_melee_start(s); + zul_fire_action(s, &g_ctx, ZA_MELEE); s->player.prayer = PRAYER_NONE; zul_melee_hit(s); @@ -363,7 +368,7 @@ static void test_topology_geometry_parity(void) { int expected_walkable = collision_tile_walkable( g_collision_map, 0, x + 2256, y + 3061); CHECK("Zulrah player tile parity", - zul_topology_player_tile_allowed(&g_ctx, x, y) == + !encounter_arena_topology_tile_blocked(topology, x, y) == expected_walkable); if (!expected_walkable) continue; open_tiles++; @@ -403,14 +408,14 @@ static void test_topology_geometry_parity(void) { target_x + ZUL_NPC_SIZE - 1, target_y + ZUL_NPC_SIZE - 1)); CHECK("every Zulrah phase stand tile is allowed", - zul_topology_player_tile_allowed( - &g_ctx, + !encounter_arena_topology_tile_blocked( + topology, ZUL_STAND_COORDS[phase->stand][0], ZUL_STAND_COORDS[phase->stand][1])); if (phase->stall != ZUL_STAND_NONE) { CHECK("every Zulrah phase stall tile is allowed", - zul_topology_player_tile_allowed( - &g_ctx, + !encounter_arena_topology_tile_blocked( + topology, ZUL_STAND_COORDS[phase->stall][0], ZUL_STAND_COORDS[phase->stall][1])); } @@ -485,10 +490,25 @@ static void test_unified_player_contract_semantics(void) { } +static void test_weaponless_player_observation_stays_defined(void) { + ZulrahState* state = fresh_state(0xDEADu); + state->player.equipped[GEAR_SLOT_WEAPON] = ITEM_NONE; + zul_mark_live_stats_dirty(state); + + ASSERT_INT_EQ("weaponless player keeps the ranged stats contract", + zul_player_equipped_attack_style(state), ATTACK_STYLE_RANGED); + float observation[ZUL_NUM_OBS]; + ENCOUNTER_ZULRAH.write_obs( + (EncounterState*)state, (EncounterContext*)&g_ctx, observation); + CHECK("weaponless player observation remains defined", observation[0] >= 0.0f); +} + + int main(void) { printf("zulrah hit-delay and roll-order regressions\n\n"); test_unified_player_contract_dimensions(); test_unified_player_contract_semantics(); + test_weaponless_player_observation_stays_defined(); test_npc_hit_delay_by_distance(); test_player_hit_delay_by_distance(); diff --git a/ocean/osrs/tests/trace_colo_move.c b/ocean/osrs/tests/trace_colo_move.c deleted file mode 100644 index 5edee7b96a..0000000000 --- a/ocean/osrs/tests/trace_colo_move.c +++ /dev/null @@ -1,67 +0,0 @@ -#include -#include - -#include "ocean/osrs/encounters/encounter_colosseum.h" - -static int nearest_live_obs_target(const ColosseumState* s) { - int best_slot = -1, best_dist = 1 << 30; - for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { - int idx = s->current_obs_slots[slot]; - if (idx < 0 || idx >= COLO_MAX_NPCS) continue; - const ColoNPC* npc = &s->npcs[idx]; - if (!col_npc_is_live_target(npc)) continue; - int d = col_npc_dist_to_player(s, npc); - if (d < best_dist) { best_dist = d; best_slot = slot; } - } - return best_slot < 0 ? 0 : col_primary_attack_action_for_obs_slot(best_slot); -} - -static int first_walkable_move( - ColosseumState* s, - const ColosseumContext* ctx -) { - ColoGeometryContext geometry = { - .state = s, - .context = ctx, - }; - for (int a = 1; a < ENCOUNTER_MOVE_ACTIONS; a++) { - int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[a]; - int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[a]; - if (col_player_walkable(&geometry, nx, ny)) return a; - } - return 0; -} - -int main(void) { - ColosseumContext ctx; - col_init_context_typed(&ctx); - ctx.config.start_wave = 0; - ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; - ctx.config.beginner_loadout_fraction = 0.5f; - ctx.config.step_out_forecast_obs_enabled = 1; - ctx.config.action_debug_log = 1; - col_finalize_route_topology(&ctx); - - ColosseumState s; - memset(&s, 0, sizeof(s)); - col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 0x1234u); - - static float obs[COLO_NUM_OBS]; - int actions[COLO_NUM_ACTION_HEADS] = {0}; - for (int tick = 0; tick < 80 && !s.episode_over; tick++) { - col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); - for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; - if (s.modifiers.draft_pending) { - int opt = 0; - for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) - if (s.modifiers.draft_options[o] >= 0) { opt = o; break; } - actions[COLO_HEAD_MODIFIER_SELECT] = opt + 1; - } else { - actions[COLO_HEAD_PRIMARY] = (tick & 1) - ? first_walkable_move(&s, &ctx) - : nearest_live_obs_target(&s); - } - col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); - } - return 0; -} diff --git a/ocean/osrs_colosseum/colosseum_profile.h b/ocean/osrs_colosseum/colosseum_profile.h index 566bc597ab..8482e5dda8 100644 --- a/ocean/osrs_colosseum/colosseum_profile.h +++ b/ocean/osrs_colosseum/colosseum_profile.h @@ -17,7 +17,6 @@ X(COLO_PROF_C_LOG_DPT, "c_log_dpt") \ X(COLO_PROF_OBS_REFRESH_SLOTS, "obs_refresh_slots") \ X(COLO_PROF_OBS_PREFIX, "obs_prefix") \ - X(COLO_PROF_OBS_PILLARS, "obs_pillars") \ X(COLO_PROF_OBS_INVENTORY, "obs_inventory") \ X(COLO_PROF_OBS_VENATOR, "obs_venator") \ X(COLO_PROF_OBS_NPC_SLOTS, "obs_npc_slots") \ diff --git a/ocean/osrs_colosseum/osrs_colosseum.cu b/ocean/osrs_colosseum/osrs_colosseum.cu index e52d2dc0c1..b3ec66b3fb 100644 --- a/ocean/osrs_colosseum/osrs_colosseum.cu +++ b/ocean/osrs_colosseum/osrs_colosseum.cu @@ -21,20 +21,6 @@ static_assert(12 + 23 - 1 == 34); static_assert(101 + 24 * 23 <= 904); static constexpr OsrsEntityEncoderDescriptor OSRS_COLOSSEUM_ENTITY_DESCRIPTOR = { - .env_name = "osrs_colosseum", - .obs_size = 904, .branches = OSRS_COLOSSEUM_ENTITY_BRANCHES, .num_branches = 2, }; - -static void* osrs_colosseum_entity_encoder_create_weights(void* self) { - return osrs_entity_encoder_create_weights( - self, &OSRS_COLOSSEUM_ENTITY_DESCRIPTOR); -} - -static void create_osrs_colosseum_encoder(Encoder* encoder) { - osrs_entity_encoder_configure( - encoder, - osrs_colosseum_entity_encoder_create_weights, - &OSRS_COLOSSEUM_ENTITY_DESCRIPTOR); -} diff --git a/ocean/osrs_colosseum/osrs_colosseum.h b/ocean/osrs_colosseum/osrs_colosseum.h index d1a44b231b..f850ffe189 100644 --- a/ocean/osrs_colosseum/osrs_colosseum.h +++ b/ocean/osrs_colosseum/osrs_colosseum.h @@ -7,9 +7,6 @@ #include "pufferenv.h" -/* Before the encounter header, so the COLO_PROFILE_MARK sites inside it compile in. - * Every mark is a branch on a static int and does no work until - * PUFFER_COLOSSEUM_PROFILE is set, so the training path pays a predictable branch. */ #include "colosseum_profile.h" #define Log OsrsSharedLog @@ -132,7 +129,6 @@ static inline float col_curriculum_uniform(uint32_t env_index) { } static void col_write_action_mask_bytes(Env* env, unsigned char* mask_out) { - if (!mask_out) return; float mask_f[COLO_ACTION_MASK_SIZE]; ENCOUNTER_COLOSSEUM.write_mask( COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), mask_f); @@ -339,9 +335,6 @@ void puf_step(Env* env) { COLO_PROFILE_MARK(COLO_PROF_C_REWARD_TERMINAL); #endif - /* Both readings run the best-gear oracle, which is the single most expensive thing - * left in the env step. They are means over millions of steps, so a 1-in-N sample is - * just as precise and costs N times less. */ if (env->state.start_wave == env->config_start_wave && (env->dpt_sample_step++ % COLO_DPT_SAMPLE_INTERVAL) == 0) { col_log_dpt_sample( @@ -457,15 +450,14 @@ void puf_step(Env* env) { #endif } #ifdef COLO_PROFILE_ENABLED - if (col_prof_enabled) + if (col_prof_enabled) { COLO_PROFILE_ADD(COLO_PROF_C_STEP_TOTAL, COLO_PROFILE_NOW_MS() - col_prof_step_t0); COLO_PROFILE_ADD(COLO_PROF_ENV_STEPS, 1.0); + } #endif } #ifdef COLO_PROFILE_ENABLED -/* best_gear_* accumulate event counts, not milliseconds, so a %-of-step column on them - * is meaningless. */ static int col_profile_slot_is_counter(int slot) { return slot == COLO_PROF_ENV_STEPS || slot == COLO_PROF_BEST_GEAR_REQUESTS || @@ -483,8 +475,6 @@ void puf_env_profile_report(void) { double total = v[COLO_PROF_C_STEP_TOTAL]; if (total <= 0.0) return; - /* Absolute ns per env step is the number worth judging: a share only says how a slot - * compares to its siblings, never whether the work itself is justified. */ double steps = v[COLO_PROF_ENV_STEPS]; if (steps <= 0.0) return; printf("\nenv profile: %.0f env-steps, %.0f ns/env-step total\n", @@ -540,9 +530,6 @@ void puf_log(Log* log, Dict* out) { dict_set(out, "npc_blocked_tiles", log->npc_blocked_tiles); dict_set(out, "npc_stamp_tiles", log->npc_stamp_tiles); - /* Ahead of the per-type breakdowns on purpose: the dashboard renders only the first - * PUF_DASH_MAX_USER_ROWS*2 keys, and how much reward the [-1,1] clamp discards is a - * health check on the whole reward design, not a per-NPC detail. */ dict_set(out, "reward_clamp_frac", log->reward_steps > 0.0f ? log->reward_clamped_steps / log->reward_steps : 0.0f); dict_set(out, "reward_clamp_loss", log->reward_clamp_loss); @@ -723,4 +710,5 @@ void puf_log(Log* log, Dict* out) { dict_set(out, "death_dmg_self", log->death_dmg_self); dict_set(out, "death_heal_remaining", log->death_heal_remaining); dict_set(out, "farm_damage", log->farm_damage); + dict_set(out, "n", log->n); } diff --git a/ocean/osrs_inferno/osrs_inferno.cu b/ocean/osrs_inferno/osrs_inferno.cu index ba833db326..0dc0809a51 100644 --- a/ocean/osrs_inferno/osrs_inferno.cu +++ b/ocean/osrs_inferno/osrs_inferno.cu @@ -21,20 +21,6 @@ static_assert(14 + 13 - 1 == 26); static_assert(124 + 14 * 13 <= 530); static constexpr OsrsEntityEncoderDescriptor OSRS_INFERNO_ENTITY_DESCRIPTOR = { - .env_name = "osrs_inferno", - .obs_size = 530, .branches = OSRS_INFERNO_ENTITY_BRANCHES, .num_branches = 2, }; - -static void* osrs_inferno_entity_encoder_create_weights(void* self) { - return osrs_entity_encoder_create_weights( - self, &OSRS_INFERNO_ENTITY_DESCRIPTOR); -} - -static void create_osrs_inferno_encoder(Encoder* encoder) { - osrs_entity_encoder_configure( - encoder, - osrs_inferno_entity_encoder_create_weights, - &OSRS_INFERNO_ENTITY_DESCRIPTOR); -} diff --git a/ocean/osrs_inferno/osrs_inferno.h b/ocean/osrs_inferno/osrs_inferno.h index e7431bf3d1..a8ded7fe50 100644 --- a/ocean/osrs_inferno/osrs_inferno.h +++ b/ocean/osrs_inferno/osrs_inferno.h @@ -176,14 +176,11 @@ void puf_init(Env* env, Dict* kwargs) { int start_wave = (int)dict_get(kwargs, "start_wave"); inferno_env_put_int(env, "start_wave", start_wave); - inferno_env_put_float(env, "damage_reward_coeff", - (float)dict_get(kwargs, "damage_reward_coeff")); - inferno_env_put_float(env, "shield_penalty_coeff", - (float)dict_get(kwargs, "shield_penalty_coeff")); - inferno_env_put_float(env, "tag_reward_coeff", - (float)dict_get(kwargs, "tag_reward_coeff")); static const char* const float_keys[] = { + "damage_reward_coeff", + "shield_penalty_coeff", + "tag_reward_coeff", "shield_tag_reward_coeff", "budget_loadout_fraction", "offensive_prayer_reward_coeff", @@ -209,28 +206,22 @@ void puf_init(Env* env, Dict* kwargs) { "curriculum_supply_brew_jitter", "curriculum_supply_restore_jitter", "curriculum_no_brew_frac", + "late_start_supply_profile_scale", }; for (size_t k = 0; k < sizeof(float_keys) / sizeof(*float_keys); k++) inferno_env_put_float(env, float_keys[k], (float)dict_get(kwargs, float_keys[k])); - inferno_env_put_float(env, "late_start_supply_profile_scale", - (float)dict_get(kwargs, "late_start_supply_profile_scale")); - inferno_env_put_int(env, "terminal_penalty_enabled", - (int)dict_get(kwargs, "terminal_penalty_enabled")); - static const char* const int_keys[] = { + "terminal_penalty_enabled", "curriculum_supply_jitter_mode", "curriculum_no_brew_mode", + "loadout_profile_mode", + "zuk_healer_reward_mode", + "joseph_reward_mode", }; for (size_t k = 0; k < sizeof(int_keys) / sizeof(*int_keys); k++) inferno_env_put_int(env, int_keys[k], (int)dict_get(kwargs, int_keys[k])); - inferno_env_put_int(env, "loadout_profile_mode", - (int)dict_get(kwargs, "loadout_profile_mode")); - inferno_env_put_int(env, "zuk_healer_reward_mode", - (int)dict_get(kwargs, "zuk_healer_reward_mode")); - inferno_env_put_int(env, "joseph_reward_mode", - (int)dict_get(kwargs, "joseph_reward_mode")); inferno_apply_reward_profile(env, (int)dict_get(kwargs, "reward_profile")); inferno_env_put_int(env, "zuk_safe_untagged_healer_target_mask", (int)dict_get(kwargs, "zuk_safe_untagged_healer_target_mask")); diff --git a/ocean/osrs_pvp/osrs_pvp.h b/ocean/osrs_pvp/osrs_pvp.h index d034caa47b..bf22e8bcc9 100644 --- a/ocean/osrs_pvp/osrs_pvp.h +++ b/ocean/osrs_pvp/osrs_pvp.h @@ -58,7 +58,6 @@ static inline uint32_t nh_pvp_lowbias32(uint32_t x) { } static void nh_pvp_write_native_action_mask(Env* env, unsigned char* mask_out) { - if (!mask_out) return; pvp_write_action_mask_bytes( mask_out, &env->state.env, 0, env->context.route_topology); } @@ -89,9 +88,6 @@ static void nh_pvp_apply_shaping(Env* env, int enabled, float scale) { shaping->ko_bonus = 0.15f; shaping->wasted_resources_penalty = -0.07f; shaping->prayer_penalty_enabled = 1; - shaping->click_penalty_enabled = 0; - shaping->click_penalty_threshold = 5; - shaping->click_penalty_coef = -0.003f; } void puf_init(Env* env, Dict* kwargs) { diff --git a/ocean/osrs_zulrah/osrs_zulrah.h b/ocean/osrs_zulrah/osrs_zulrah.h index d88612d831..361ae1da8e 100644 --- a/ocean/osrs_zulrah/osrs_zulrah.h +++ b/ocean/osrs_zulrah/osrs_zulrah.h @@ -58,7 +58,6 @@ static inline uint32_t zul_lowbias32(uint32_t x) { } static void zul_write_native_action_mask(Env* env, unsigned char* mask_out) { - if (!mask_out) return; zul_write_mask_bytes( ZUL_ENV_STATE(env), ZUL_ENV_CONTEXT(env), mask_out); } diff --git a/src/ocean.cu b/src/ocean.cu index 83de9acd6b..979aae31f6 100644 --- a/src/ocean.cu +++ b/src/ocean.cu @@ -49,19 +49,19 @@ static void create_custom_encoder(const char* env_name, Encoder* enc) { return; } if (strcmp(env_name, "osrs_colosseum") == 0) { - create_osrs_colosseum_encoder(enc); + create_osrs_entity_encoder<&OSRS_COLOSSEUM_ENTITY_DESCRIPTOR>(enc); return; } if (strcmp(env_name, "osrs_inferno") == 0) { - create_osrs_inferno_encoder(enc); + create_osrs_entity_encoder<&OSRS_INFERNO_ENTITY_DESCRIPTOR>(enc); return; } if (strcmp(env_name, "osrs_zulrah") == 0) { - create_osrs_zulrah_encoder(enc); + create_osrs_entity_encoder<&OSRS_EQUIPMENT_ENTITY_DESCRIPTOR>(enc); return; } if (strcmp(env_name, "osrs_pvp") == 0) { - create_osrs_pvp_encoder(enc); + create_osrs_entity_encoder<&OSRS_EQUIPMENT_ENTITY_DESCRIPTOR>(enc); return; } } diff --git a/src/pufferl.cu b/src/pufferl.cu index c584cb1008..5e4ed00630 100644 --- a/src/pufferl.cu +++ b/src/pufferl.cu @@ -1403,7 +1403,7 @@ __global__ void transpose_102(precision_t* dst, const precision_t* src, dst[b * A * C + a * C + c] = src[idx]; } -__global__ void transpose_102(float* dst, const float* src, int A, int B, int C) { +__global__ void transpose_102_float(float* dst, const float* src, int A, int B, int C) { int idx = blockIdx.x * blockDim.x + threadIdx.x; int total = A * B * C; if (idx >= total) { @@ -1445,7 +1445,7 @@ static void train_epoch_gpu(PuffeRL* pufferl, RolloutBuf src, int slot, int mask_c = src.action_mask.shape[2]; transpose_102<<>>( rollouts->observations.data, src.observations.data, T, B, obs_size); - transpose_102<<>>( + transpose_102_float<<>>( rollouts->actions.data, src.actions.data, T, B, num_atns); transpose_102<<>>( rollouts->logprobs.data, src.logprobs.data, T, B, 1); diff --git a/tests/test_osrs_entity_encoders.py b/tests/test_osrs_entity_encoders.py index c880514029..ba7f6a8206 100644 --- a/tests/test_osrs_entity_encoders.py +++ b/tests/test_osrs_entity_encoders.py @@ -12,9 +12,14 @@ ROOT = Path(__file__).resolve().parents[1] CUDA_SOURCE = Path(__file__).with_name("test_osrs_entity_encoders_cuda.cu") ITEM_HEADER = ROOT / "ocean/osrs/osrs_item_obs_generated.h" +ITEM_SOURCE = ITEM_HEADER.read_text() BATCH = 9 HIDDEN = 40 BOTTLENECK = 16 +INVENTORY_START = 52 +INVENTORY_COUNT = 28 +EQUIPMENT_START = 80 +EQUIPMENT_COUNT = 11 @dataclass(frozen=True) @@ -28,13 +33,6 @@ class EncoderContract: npc_features: int type_count: int type_code_scale: int - equipment_start: int - equipment_count: int - equipment_obs_features: int - equipment_features: int - inventory_start: int - inventory_count: int - inventory_features: int CONTRACTS = ( @@ -48,13 +46,6 @@ class EncoderContract: npc_features=34, type_count=12, type_code_scale=1, - equipment_start=80, - equipment_count=11, - equipment_obs_features=1, - equipment_features=14, - inventory_start=52, - inventory_count=28, - inventory_features=14, ), EncoderContract( name="inferno", @@ -66,20 +57,12 @@ class EncoderContract: npc_features=26, type_count=14, type_code_scale=16, - equipment_start=80, - equipment_count=11, - equipment_obs_features=1, - equipment_features=14, - inventory_start=52, - inventory_count=28, - inventory_features=14, ), ) def _generated_integer(name: str) -> int: - source = ITEM_HEADER.read_text() - match = re.search(rf"^#define\s+{name}\s+(\d+)\s*$", source, re.MULTILINE) + match = re.search(rf"^#define\s+{name}\s+(\d+)\s*$", ITEM_SOURCE, re.MULTILINE) assert match, f"generated item-table constant {name} is missing" return int(match.group(1)) @@ -88,7 +71,7 @@ def _load_item_table() -> np.ndarray: columns = _generated_integer("OSRS_ITEM_OBS_TABLE_COLS") expected_rows = _generated_integer("OSRS_ITEM_OBS_TABLE_ROWS") rows = [] - for line in ITEM_HEADER.read_text().splitlines(): + for line in ITEM_SOURCE.splitlines(): if not line.lstrip().startswith("X("): continue values = re.findall( @@ -132,33 +115,22 @@ def _make_observations( ) code_scale = _generated_integer("OSRS_ITEM_OBS_CODE_SCALE") - inventory_block = contract.inventory_count - observations[ - :, - contract.inventory_start : contract.inventory_start + inventory_block, - ] = 0 + observations[:, INVENTORY_START : INVENTORY_START + INVENTORY_COUNT] = 0 inventory_cells = observations[ - :, - contract.inventory_start : contract.inventory_start + inventory_block, - ].reshape(BATCH, contract.inventory_count) + :, INVENTORY_START : INVENTORY_START + INVENTORY_COUNT + ].reshape(BATCH, INVENTORY_COUNT) assert table_rows <= inventory_cells.size for code in range(table_rows): - batch, cell = divmod(code, contract.inventory_count) + batch, cell = divmod(code, INVENTORY_COUNT) inventory_cells[batch, cell] = code / code_scale - equipment_block = contract.equipment_count * contract.equipment_obs_features equipment_cells = observations[ - :, - contract.equipment_start : contract.equipment_start + equipment_block, - ].reshape( - BATCH, - contract.equipment_count, - contract.equipment_obs_features, - ) + :, EQUIPMENT_START : EQUIPMENT_START + EQUIPMENT_COUNT + ].reshape(BATCH, EQUIPMENT_COUNT) for batch in range(BATCH): - for cell in range(contract.equipment_count): - code = 1 + (batch * contract.equipment_count + cell) % (table_rows - 1) - equipment_cells[batch, cell, 0] = code / code_scale + for cell in range(EQUIPMENT_COUNT): + code = 1 + (batch * EQUIPMENT_COUNT + cell) % (table_rows - 1) + equipment_cells[batch, cell] = code / code_scale return observations @@ -172,7 +144,7 @@ def _materialize_items( code_scale = _generated_integer("OSRS_ITEM_OBS_CODE_SCALE") codes = np.rint(source * code_scale).astype(np.int64) assert np.all((codes >= 0) & (codes < table.shape[0])) - return table[codes].copy(), codes + return table[codes] def _materialize( @@ -196,67 +168,19 @@ def _materialize( npc_records[:, :, npc_type] = npc_codes == npc_type + 1 npc_records[:, :, contract.type_count :] = npc_source[:, :, 1:] - inventory_records, inventory_codes = _materialize_items( + inventory_records = _materialize_items( observations, - contract.inventory_start, - contract.inventory_count, + INVENTORY_START, + INVENTORY_COUNT, table, ) - equipment_records, equipment_codes = _materialize_items( + equipment_records = _materialize_items( observations, - contract.equipment_start, - contract.equipment_count, + EQUIPMENT_START, + EQUIPMENT_COUNT, table, ) - return ( - npc_records, - inventory_records, - equipment_records, - npc_codes, - inventory_codes, - equipment_codes, - ) - - -@pytest.mark.parametrize("contract", CONTRACTS, ids=lambda contract: contract.name) -def test_cpu_materialization_contract(contract): - table = _load_item_table() - observations = _make_observations(contract, table.shape[0]) - ( - npc_records, - inventory_records, - equipment_records, - npc_codes, - inventory_codes, - equipment_codes, - ) = _materialize(contract, observations, table) - assert npc_records.shape == ( - BATCH, - contract.npc_count, - contract.npc_features, - ) - assert inventory_records.shape == ( - BATCH, - contract.inventory_count, - contract.inventory_features, - ) - assert equipment_records.shape == ( - BATCH, - contract.equipment_count, - contract.equipment_features, - ) - assert set(npc_codes.ravel()) == set(range(contract.type_count + 1)) - assert set(inventory_codes.ravel()) == set(range(table.shape[0])) - assert np.all(equipment_codes > 0) - assert np.count_nonzero(npc_records[npc_codes == 0]) == 0 - assert np.count_nonzero(inventory_records[inventory_codes == 0]) == 0 - materialized_sizes = ( - npc_records.size, - inventory_records.size, - equipment_records.size, - ) - assert all(size > 256 for size in materialized_sizes) - assert all(size % 256 != 0 for size in materialized_sizes) + return npc_records, inventory_records, equipment_records def _torch_reference( @@ -267,14 +191,9 @@ def _torch_reference( table, weights, ): - ( - npc_records, - inventory_records, - equipment_records, - _, - _, - _, - ) = _materialize(contract, observations.detach().cpu().numpy(), table) + npc_records, inventory_records, equipment_records = _materialize( + contract, observations.detach().cpu().numpy(), table + ) npc = torch.from_numpy(npc_records).to(observations.device) inventory = torch.from_numpy(inventory_records).to(observations.device) equipment = torch.from_numpy(equipment_records).to(observations.device) @@ -316,17 +235,12 @@ def branch(records, l1_weight, l2_weight, active_width): def _configure_library(path: Path): library = ctypes.CDLL(path) pointer = ctypes.c_void_p - library.osrs_entity_test_contract.argtypes = [ - ctypes.c_int, - ctypes.POINTER(ctypes.c_int), - ] library.osrs_entity_test_init.argtypes = [ ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ] - library.osrs_entity_test_init.restype = ctypes.c_int library.osrs_entity_test_set_weights.argtypes = [pointer] * 7 library.osrs_entity_test_forward.argtypes = [ pointer, @@ -397,32 +311,8 @@ def test_cuda_forward_and_all_weight_gradients(cuda_library, contract): if not torch.cuda.is_available(): pytest.skip("CUDA is not available") - reported = (ctypes.c_int * 14)() - cuda_library.osrs_entity_test_contract(contract.kind, reported) - assert list(reported) == [ - contract.obs_size, - contract.npc_start, - contract.npc_count, - contract.npc_obs_features, - contract.npc_features, - contract.type_count, - contract.type_code_scale, - contract.equipment_start, - contract.equipment_count, - contract.equipment_obs_features, - contract.equipment_features, - contract.inventory_start, - contract.inventory_count, - contract.inventory_features, - ] - assert ( - cuda_library.osrs_entity_test_init( - contract.kind, - BATCH, - contract.obs_size, - HIDDEN, - ) - == 0 + cuda_library.osrs_entity_test_init( + contract.kind, BATCH, contract.obs_size, HIDDEN ) table = _load_item_table() @@ -431,9 +321,9 @@ def test_cuda_forward_and_all_weight_gradients(cuda_library, contract): generator = torch.Generator(device="cuda").manual_seed(6100 + contract.kind) shapes = ( (HIDDEN, contract.obs_size), - (BOTTLENECK, contract.inventory_features), + (BOTTLENECK, table.shape[1]), (HIDDEN, BOTTLENECK), - (BOTTLENECK, contract.equipment_features), + (BOTTLENECK, table.shape[1]), (HIDDEN, BOTTLENECK), (BOTTLENECK, contract.npc_features), (HIDDEN, BOTTLENECK), @@ -468,10 +358,8 @@ def test_cuda_forward_and_all_weight_gradients(cuda_library, contract): ) reference_output.backward(output_gradient) cuda_library.osrs_entity_test_backward(_pointer(output_gradient), BATCH, HIDDEN) - torch.cuda.synchronize() for index, weight in enumerate(weights): cuda_gradient = torch.empty_like(weight) cuda_library.osrs_entity_test_get_grad(index, _pointer(cuda_gradient)) - torch.cuda.synchronize() torch.testing.assert_close(cuda_gradient, weight.grad, atol=5e-3, rtol=5e-3) diff --git a/tests/test_osrs_entity_encoders_cuda.cu b/tests/test_osrs_entity_encoders_cuda.cu index 1986bde24e..eca38c5523 100644 --- a/tests/test_osrs_entity_encoders_cuda.cu +++ b/tests/test_osrs_entity_encoders_cuda.cu @@ -14,12 +14,6 @@ static Allocator test_acts; static Allocator test_grads; static bool test_cublas_initialized; -static const OsrsEntityEncoderDescriptor* test_descriptor(int kind) { - return kind == 0 - ? &OSRS_COLOSSEUM_ENTITY_DESCRIPTOR - : &OSRS_INFERNO_ENTITY_DESCRIPTOR; -} - static void test_free_allocator(Allocator* allocator) { if (allocator->mem) cudaFree(allocator->mem); free(allocator->regs); @@ -36,29 +30,7 @@ static void test_reset() { test_activations = nullptr; } -void osrs_entity_test_contract(int kind, int* values) { - const OsrsEntityEncoderDescriptor* descriptor = test_descriptor(kind); - const OsrsEntityBranchDescriptor* equipment = &descriptor->branches[0]; - const OsrsEntityBranchDescriptor* npc = &descriptor->branches[1]; - values[0] = descriptor->obs_size; - values[1] = npc->obs_start; - values[2] = npc->num_records; - values[3] = npc->obs_features; - values[4] = osrs_entity_branch_features(npc); - values[5] = npc->type_onehot; - values[6] = npc->type_code_scale; - values[7] = equipment->obs_start; - values[8] = equipment->num_records; - values[9] = equipment->obs_features; - values[10] = osrs_entity_branch_features(equipment); - values[11] = OSRS_ENTITY_INV_START; - values[12] = OSRS_ENTITY_INV_NUM_RECORDS; - values[13] = OSRS_ENTITY_INV_FEATURES; -} - -int osrs_entity_test_init(int kind, int batch, int obs_size, int hidden) { - const OsrsEntityEncoderDescriptor* descriptor = test_descriptor(kind); - if (obs_size != descriptor->obs_size) return 1; +void osrs_entity_test_init(int kind, int batch, int obs_size, int hidden) { test_reset(); if (!test_cublas_initialized) { cublas_init_handle(); @@ -67,15 +39,17 @@ int osrs_entity_test_init(int kind, int batch, int obs_size, int hidden) { test_encoder = {}; test_encoder.in_dim = obs_size; test_encoder.out_dim = hidden; - create_custom_encoder(descriptor->env_name, &test_encoder); + const char* env_name = kind == 0 ? "osrs_colosseum" : "osrs_inferno"; + create_custom_encoder(env_name, &test_encoder); test_weights = (OsrsEntityEncoderWeights*)test_encoder.create_weights(&test_encoder); test_encoder.reg_params(test_weights, &test_params); alloc_create(&test_params); - test_activations = (OsrsEntityEncoderActivations*)calloc(1, test_encoder.activation_size); - test_encoder.reg_train(test_weights, test_activations, &test_acts, &test_grads, batch); + test_activations = (OsrsEntityEncoderActivations*)calloc( + 1, test_encoder.activation_size); + test_encoder.reg_train( + test_weights, test_activations, &test_acts, &test_grads, batch); alloc_create(&test_acts); alloc_create(&test_grads); - return 0; } void osrs_entity_test_set_weights( @@ -112,13 +86,11 @@ void osrs_entity_test_forward(void* output, void* observations, int batch, int o Prec result = test_encoder.forward(test_weights, test_activations, input, 0); cudaMemcpy(output, result.data, (int64_t)batch * test_encoder.out_dim * sizeof(float), cudaMemcpyDeviceToDevice); - cudaDeviceSynchronize(); } void osrs_entity_test_backward(void* grad, int batch, int hidden) { Prec output_grad = {.data = (precision_t*)grad, .shape = {batch, hidden}}; test_encoder.backward(test_weights, test_activations, output_grad, 0); - cudaDeviceSynchronize(); } void osrs_entity_test_get_grad(int index, void* output) { From ffea50c1f979db1b40d03861bbb0f34f36aab261 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Tue, 18 Aug 2026 21:36:28 +0300 Subject: [PATCH 46/50] Fix blowpipe loaded dart bonuses --- .../inferno/encounter_inferno_model.inc | 4 +- .../inferno/encounter_inferno_reset_spawn.inc | 5 -- ocean/osrs/osrs_combat.h | 6 +++ ocean/osrs/osrs_encounter.h | 10 ---- ocean/osrs/tests/test_inferno_attack_styles.c | 16 +++--- ocean/osrs/tests/test_inferno_golden.c | 50 +++++++++---------- .../osrs/tests/test_osrs_inventory_actions.c | 22 ++++++++ 7 files changed, 65 insertions(+), 48 deletions(-) diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc index 5a66dbd91d..27e20efaff 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc @@ -525,7 +525,7 @@ static const uint8_t INF_MAX_RANGE_FAST_LOADOUT[NUM_GEAR_SLOTS] = { [GEAR_SLOT_HEAD] = ITEM_MASORI_MASK_F, [GEAR_SLOT_CAPE] = ITEM_DIZANAS_QUIVER, [GEAR_SLOT_NECK] = ITEM_NECKLACE_OF_ANGUISH, - [GEAR_SLOT_AMMO] = ITEM_DRAGON_DART, + [GEAR_SLOT_AMMO] = ITEM_DRAGON_ARROWS, [GEAR_SLOT_WEAPON] = ITEM_TOXIC_BLOWPIPE, [GEAR_SLOT_SHIELD] = ITEM_NONE, [GEAR_SLOT_BODY] = ITEM_MASORI_BODY_F, @@ -567,7 +567,7 @@ static const uint8_t INF_BUDGET_RANGE_FAST_LOADOUT[NUM_GEAR_SLOTS] = { [GEAR_SLOT_HEAD] = ITEM_CRYSTAL_HELM, [GEAR_SLOT_CAPE] = ITEM_DIZANAS_QUIVER, [GEAR_SLOT_NECK] = ITEM_NECKLACE_OF_ANGUISH, - [GEAR_SLOT_AMMO] = ITEM_DRAGON_DART, + [GEAR_SLOT_AMMO] = ITEM_GOD_BLESSING, [GEAR_SLOT_WEAPON] = ITEM_TOXIC_BLOWPIPE, [GEAR_SLOT_SHIELD] = ITEM_NONE, [GEAR_SLOT_BODY] = ITEM_CRYSTAL_BODY, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc index f78bc6e8bb..ff5c25d329 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -381,11 +381,6 @@ static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint const uint8_t* const* loadouts = inf_loadouts_for_profile(s->active_loadout_profile); encounter_apply_loadout(&s->player, loadouts[INF_GEAR_MAGE], GEAR_MAGE); - { - encounter_populate_inventory(&s->player, loadouts, INF_NUM_WEAPON_SETS, NULL); - - encounter_clear_ammo_inventory_slot(&s->player); - } InfSupplyDoses full_supplies = inf_full_starting_supplies(); InfSupplyDoses start_supplies = inf_supplies_for_start_wave( full_supplies, s->start_wave, ctx->config.late_start_supply_profile_scale); diff --git a/ocean/osrs/osrs_combat.h b/ocean/osrs/osrs_combat.h index 6e8a3ae4b2..6701a91224 100644 --- a/ocean/osrs/osrs_combat.h +++ b/ocean/osrs/osrs_combat.h @@ -614,6 +614,12 @@ static inline void osrs_sum_equipment_bonuses(const uint8_t loadout[NUM_GEAR_SLO out->prayer += item->prayer; } uint8_t weapon = loadout[GEAR_SLOT_WEAPON]; + if (weapon == ITEM_TOXIC_BLOWPIPE) { + uint8_t ammo = loadout[GEAR_SLOT_AMMO]; + if (ammo != ITEM_NONE) + out->ranged_strength -= ITEM_DATABASE[ammo].ranged_strength; + out->ranged_strength += ITEM_DATABASE[ITEM_DRAGON_DART].ranged_strength; + } if (weapon != 255) { out->attack_speed = ITEM_DATABASE[weapon].attack_speed; out->attack_range = ITEM_DATABASE[weapon].attack_range; diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index bd6a53543e..ce1d9158b6 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -1747,16 +1747,6 @@ static void encounter_populate_inventory( } } -static inline void encounter_clear_ammo_inventory_slot(Player* p) { - for (int cell = 0; cell < OSRS_INVENTORY_SIZE; cell++) { - uint8_t item = - osrs_inventory_cell_item_index(&p->inventory_cells[cell]); - if (item != ITEM_NONE && - osrs_item_gear_slot(item) == GEAR_SLOT_AMMO) - p->inventory_cells[cell] = osrs_inventory_cell_empty(); - } -} - static inline void encounter_translate_movement(HumanInput* hi, int* actions, int head_move, void* (*get_entity)(void*, int), diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index bc3deb3789..9ec6d9e5ad 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -1564,8 +1564,8 @@ static int test_player_slot_inventory_contains( return p->equipped[gear_slot] == item; } -static void test_inferno_reset_inventory_leaves_one_empty_slot(void) { - printf("--- inferno reset inventory leaves one empty slot ---\n"); +static void test_inferno_reset_inventory_leaves_two_empty_slots(void) { + printf("--- inferno reset inventory leaves two empty slots ---\n"); EncounterState* raw_state = inf_create(); InfernoState* state = (InfernoState*)raw_state; @@ -1576,7 +1576,7 @@ static void test_inferno_reset_inventory_leaves_one_empty_slot(void) { ASSERT_INT_EQ("full kit bastion vials", test_inventory_potion_vials(state->player.bastion_doses), 4); ASSERT_INT_EQ("full kit occupied inventory cells", - test_occupied_inventory_cells(state), 27); + test_occupied_inventory_cells(state), 26); inf_destroy(raw_state); } @@ -1603,6 +1603,8 @@ static void test_inferno_max_profile_reset_uses_existing_gear(void) { ASSERT_INT_EQ("max fast-range weapon in inventory", test_player_slot_inventory_contains( &state->player, GEAR_SLOT_WEAPON, ITEM_TOXIC_BLOWPIPE), 1); + ASSERT_INT_EQ("max loadout keeps dragon darts inside blowpipe", + test_cell_holding_item(state, ITEM_DRAGON_DART), -1); inf_destroy(raw_state); } @@ -1652,8 +1654,10 @@ static void test_inferno_budget_profile_reset_uses_budget_gear(void) { ASSERT_INT_EQ("budget range legs available", test_player_slot_inventory_contains( &state->player, GEAR_SLOT_LEGS, ITEM_CRYSTAL_LEGS), 1); - ASSERT_INT_EQ("budget inventory leaves one empty cell", - test_occupied_inventory_cells(state), 27); + ASSERT_INT_EQ("budget inventory occupied cells", + test_occupied_inventory_cells(state), 26); + ASSERT_INT_EQ("budget loadout keeps dragon darts inside blowpipe", + test_cell_holding_item(state, ITEM_DRAGON_DART), -1); inf_destroy(raw_state); } @@ -8535,7 +8539,7 @@ int main(void) { test_jad_healer_damage_never_gets_damage_reward(); test_shield_tag_reward_excludes_zuk(); test_inferno_reset_supplies_match_current_inventory(); - test_inferno_reset_inventory_leaves_one_empty_slot(); + test_inferno_reset_inventory_leaves_two_empty_slots(); test_inferno_max_profile_reset_uses_existing_gear(); test_inferno_budget_profile_reset_uses_budget_gear(); test_inferno_mixed_profile_sampling_respects_fraction(); diff --git a/ocean/osrs/tests/test_inferno_golden.c b/ocean/osrs/tests/test_inferno_golden.c index e909b6a8bd..daecfc527b 100644 --- a/ocean/osrs/tests/test_inferno_golden.c +++ b/ocean/osrs/tests/test_inferno_golden.c @@ -198,35 +198,35 @@ static const GoldenConfig CONFIGS[] = { /* Canonical inventory cells intentionally change the serialized player state hash. */ static const uint64_t EXPECTED_STATE[NUM_CONFIGS] = { - 0xd1a32c595e0f20e4ULL, 0xd556d9d18290dbe0ULL, 0x7dc08bb94dc1420aULL, - 0x005ebedd7892aa36ULL, 0xde2fb70f9d751137ULL, 0x985dd084df5d7707ULL, - 0xed97211bab4b9a47ULL, 0x8ea493a4516b791fULL, 0x6cebcf139b64d613ULL, - 0x8dc44e7252f56c19ULL, 0xe5c5146c16159d32ULL, 0x237eb63b95cd8ee8ULL, - 0xc53b4ecacd50a647ULL, 0xda3e4d8630b526f2ULL, 0x1db55fe5533fdb5dULL, + 0x7ba3711e153095beULL, 0xb39d554acf436e5cULL, 0x6cc5844ce0c39c1bULL, + 0xf91d3612097dbd8aULL, 0xea21f6ea8e67827bULL, 0x6ae89eb3f13bae2eULL, + 0x4d866acbe988acceULL, 0x7854ed938c91eb29ULL, 0x689a7fc8d0ae318eULL, + 0x8bc8bdc32bd12465ULL, 0x77b8a8ab51ef62c8ULL, 0x89e7ddb514ed1906ULL, + 0xda40b9bdbbeb1e61ULL, 0xba99612d66277506ULL, 0x0600b75367c4e45fULL, }; static const uint64_t EXPECTED_REWARD[NUM_CONFIGS] = { - 0xef229a1a4b13b62aULL, 0x380701212d84bfbaULL, 0x4098eaaa762c034aULL, - 0xfcafb9b6e476bbaaULL, 0x23577b00d055c7daULL, 0x2366aa6e9afa77d3ULL, - 0xabf96da3254522b3ULL, 0x64778a6835f7bae3ULL, 0x3b83ee4d02793243ULL, - 0x24ccbb9772bf51d3ULL, 0x4c54b1462ee3a663ULL, 0x29916e1218e670b3ULL, - 0x6c34cdb27c0c1733ULL, 0x47a1f9c7b1d3def3ULL, 0x47a1f9c7b1d3def3ULL, + 0xb089187e53857203ULL, 0xa2565e648c91b58aULL, 0x0e980cdddd5d24eaULL, + 0xb45661865c9132baULL, 0x51ad73340d19447aULL, 0x93d59ec47f5c8e53ULL, + 0x6c34cdb27c0c1733ULL, 0xa11671953e858723ULL, 0xdce53c1df8560f83ULL, + 0x93d59ec47f5c8e53ULL, 0x4c54b1462ee3a663ULL, 0x3d09912556a163b3ULL, + 0x6c34cdb27c0c1733ULL, 0x47a1f9c7b1d3def3ULL, 0xe7f1c65d07654513ULL, }; static const uint64_t EXPECTED_MASK[NUM_CONFIGS] = { - 0xbbb48dc73a049bf3ULL, 0x39dc7eec5cb9e2f3ULL, 0x4c106d4052a4fdbaULL, - 0x143ebc7ebbb2d72aULL, 0x7dad20ecbb02bd93ULL, 0xef4d1b6877c93acaULL, - 0x8fca3065ddbf1793ULL, 0x2623748ea2e51b33ULL, 0x09130bda5462ebf3ULL, - 0x12a49268eb13bd53ULL, 0x408fca7dc9aff313ULL, 0x1e80e464ea552a5aULL, - 0x77f5a923af526e3aULL, 0x9f3034f7b394665aULL, 0x25d8b9153b3ae063ULL, + 0x161ad634c13bed03ULL, 0xb0a5340f14eaa10aULL, 0x3e98eb184642444aULL, + 0xf43de0a67cfc551aULL, 0x8ec84065b245eaa3ULL, 0x8a02e785528c7e9aULL, + 0x8b7bc0262228b5daULL, 0x9436e2cbac777d3aULL, 0x70a471b5f966653aULL, + 0xa692532e12d537c3ULL, 0x969a7e2d44202a13ULL, 0xfef0e18569cb4cd3ULL, + 0xdbe87f09bbc753a3ULL, 0x4457d6e53647be0aULL, 0xa3c4b4613376930aULL, }; static const uint64_t EXPECTED_TERMINAL[NUM_CONFIGS] = { - 0xb3480d7ee6336ae2ULL, 0x910cf79bbeeba6e3ULL, 0xe238e61f8c13c673ULL, - 0xda963a44d311c6daULL, 0xb310e5115ff5cb52ULL, 0x48994c2ef3769192ULL, - 0x2d82388999073b22ULL, 0x1b1338d1b46b5673ULL, 0x0e985be125b2f7f3ULL, - 0x258a6c0def52abe1ULL, 0x77852c52053de533ULL, 0xe2d9ed2b31cf44a1ULL, - 0x7c696af13bd0d307ULL, 0x12e57f8f9818c547ULL, 0x12e57f8f9818c547ULL, + 0x45ce0c094429a073ULL, 0xb9029934b4c6dcc2ULL, 0x6c474b92f31109b3ULL, + 0xc4d9ee59b30eec02ULL, 0x74622ccdf2f00ca2ULL, 0xa24267dbb4c1dbd2ULL, + 0xda5efce8115c5862ULL, 0xce9f670c94cfbf73ULL, 0x565640d34d54c4f3ULL, + 0xc03f2da5d13a88e1ULL, 0x77852c52053de533ULL, 0x88ec0c389d9c42a1ULL, + 0x7c696af13bd0d307ULL, 0x12e57f8f9818c547ULL, 0xf92b462e21d67e67ULL, }; static const uint64_t PRE_UNIFIED_OBSERVATION[NUM_CONFIGS] = { @@ -238,11 +238,11 @@ static const uint64_t PRE_UNIFIED_OBSERVATION[NUM_CONFIGS] = { }; static const uint64_t UNIFIED_OBSERVATION[NUM_CONFIGS] = { - 0x6c59b72c00728ad9ULL, 0x18a13290de7329bcULL, 0x6748854d95c23f92ULL, - 0x6825dad29da6b32dULL, 0x7c5520ac1cc5837fULL, 0xb88e7bda4fc6b77eULL, - 0x45cbf9bbfa703b70ULL, 0x89db5b7cc84b4728ULL, 0xb93f28f45f543b70ULL, - 0x3f1afd9c57acd296ULL, 0x3dd5d5e4e6289715ULL, 0x5c08d30cfb280174ULL, - 0x82bf60a965334a54ULL, 0x66a4cbc54247e358ULL, 0x338f8cdd2b9d56afULL, + 0xa51b82ede1cae21cULL, 0xe1997807c974def2ULL, 0xbc3aac20a6bf0a7fULL, + 0x3dccdcc4eda4266bULL, 0x27940921e8054ffdULL, 0x71c062e172101343ULL, + 0xf01f073a63279eb2ULL, 0x08121438552d14e6ULL, 0xdf889ff57739043aULL, + 0xc7ddbaa9c12699cdULL, 0x6dbcf3e1a501d061ULL, 0x6fc772947a63e769ULL, + 0xf08f7b50f9e6ec87ULL, 0xd60728d8a72defa3ULL, 0x97e58a9ee67f1f66ULL, }; static int check_hash( diff --git a/ocean/osrs/tests/test_osrs_inventory_actions.c b/ocean/osrs/tests/test_osrs_inventory_actions.c index 5fffe5663c..e4ebaa8662 100644 --- a/ocean/osrs/tests/test_osrs_inventory_actions.c +++ b/ocean/osrs/tests/test_osrs_inventory_actions.c @@ -159,6 +159,27 @@ static void test_weapon_ranges(void) { ASSERT_INT_EQ("unarmed attack speed", unarmed.attack_speed, 4); ASSERT_INT_EQ("unarmed attack range", unarmed.attack_range, 1); } + +static void test_blowpipe_uses_loaded_dragon_darts(void) { + uint8_t loadout[NUM_GEAR_SLOTS]; + memset(loadout, ITEM_NONE, sizeof(loadout)); + loadout[GEAR_SLOT_WEAPON] = ITEM_TOXIC_BLOWPIPE; + loadout[GEAR_SLOT_AMMO] = ITEM_DRAGON_ARROWS; + + EquipmentBonuses arrow_slot = {0}; + osrs_sum_equipment_bonuses(loadout, &arrow_slot); + ASSERT_INT_EQ("blowpipe replaces arrow strength with loaded dart strength", + arrow_slot.ranged_strength, 55); + + loadout[GEAR_SLOT_AMMO] = ITEM_GOD_BLESSING; + EquipmentBonuses blessing_slot = {0}; + osrs_sum_equipment_bonuses(loadout, &blessing_slot); + ASSERT_INT_EQ("blowpipe adds loaded dart strength with blessing equipped", + blessing_slot.ranged_strength, 55); + ASSERT_INT_EQ("blowpipe preserves external ammo prayer bonus", + blessing_slot.prayer, 1); +} + static void test_content_code_round_trips(void) { ASSERT_INT_EQ("inventory cell stores only one content code", (int)sizeof(OsrsInventoryCell), (int)sizeof(uint16_t)); @@ -187,6 +208,7 @@ int main(void) { run_equip_storm(trial); run_drink_dose_check(); test_weapon_ranges(); + test_blowpipe_uses_loaded_dragon_darts(); test_content_code_round_trips(); return osrs_test_summary(); } From e51fc3a91cf728a8bf81607a18ed649df3507939 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Thu, 20 Aug 2026 21:15:41 +0300 Subject: [PATCH 47/50] Promote Inferno run 73 defaults --- config/osrs_inferno.ini | 85 +++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/config/osrs_inferno.ini b/config/osrs_inferno.ini index 2594faf455..209be04716 100644 --- a/config/osrs_inferno.ini +++ b/config/osrs_inferno.ini @@ -7,23 +7,23 @@ async = 0 start_wave = 1 damage_reward_coeff = 0.00600000052 offensive_prayer_reward_coeff = 0 -shield_penalty_coeff = 0.000500534545 -tag_reward_coeff = 0.0602469742 +shield_penalty_coeff = 0.000619554601 +tag_reward_coeff = 0.0866579264 shield_tag_reward_coeff = 0.0147339059 late_start_supply_profile_scale = 1.0 curriculum_supply_jitter_mode = 1 -curriculum_supply_shared_jitter = 0.18321383 -curriculum_supply_brew_jitter = 0.0951578766 -curriculum_supply_restore_jitter = 0.130459204 -curriculum_no_brew_mode = 1 -curriculum_no_brew_frac = 0.263010442 +curriculum_supply_shared_jitter = 0.159241974 +curriculum_supply_brew_jitter = 0.211656123 +curriculum_supply_restore_jitter = 0.00708850473 +curriculum_no_brew_mode = 3 +curriculum_no_brew_frac = 0.27746433 loadout_profile_mode = 0 budget_loadout_fraction = 0.0 classic_curriculum_mode = 1 reward_profile = 2 supply_milestone_brew_reward_coeff = 0.0 supply_milestone_restore_reward_coeff = 0.0 -death_penalty_coeff = 0.0 +death_penalty_coeff = 0.393126726 terminal_penalty_enabled = 0 phase_900_bonus = 0.0 @@ -31,15 +31,15 @@ phase_600_bonus = 0.0 phase_300_bonus = 0.0 shield_penalty_episode_cap = 0.0 curriculum_wave_1 = 67 -curriculum_frac_1 = 0.118083619 +curriculum_frac_1 = 0.215649366 curriculum_wave_2 = 69 -curriculum_frac_2 = 0.0357927978 +curriculum_frac_2 = 0.0276919976 curriculum_wave_3 = 70 -curriculum_frac_3 = 0 +curriculum_frac_3 = 0.000972120499 curriculum_wave_4 = 71 -curriculum_frac_4 = 0.0472771451 -curriculum_wave_5 = 54 -curriculum_frac_5 = 0.0299999993 +curriculum_frac_4 = 0 +curriculum_wave_5 = 58 +curriculum_frac_5 = 0.0466096848 curriculum_wave_6 = 61 curriculum_frac_6 = 0.0 curriculum_frac_7 = 0.0 @@ -60,8 +60,8 @@ zuk_healer_phase_hp_delta_coeff = 0.0 zuk_untagged_healer_tick_penalty_coeff = 0.0 zuk_untagged_healer_target_bonus_coeff = 0.0 zuk_safe_untagged_healer_target_bonus_coeff = 0.0 -zuk_untagged_healer_nonmagic_attack_bonus_coeff = 0.53466928 -zuk_healer_mage_attack_penalty_coeff = 0.201784804 +zuk_untagged_healer_nonmagic_attack_bonus_coeff = 0.889728904 +zuk_healer_mage_attack_penalty_coeff = 0.036646951 zuk_safe_untagged_healer_target_mask = 0 zuk_force_safe_untagged_healer_target_mask = 0 zuk_healer_reward_mode = 0 @@ -70,31 +70,32 @@ post_jad_zuk_multiplier = 1.78125858 jad_alive_zuk_multiplier = 0.1573513 [vec] -total_agents = 1024 -num_buffers = 4 +total_agents = 2048 +num_buffers = 1 action_mask_size = 436 [policy] -hidden_size = 256 +hidden_size = 512 num_layers = 2 [train] -total_timesteps = 32251094 -horizon = 32 -learning_rate = 0.00393043552 -momentum = 0.966556907 -ent_coef = 0.00200000079 -gamma = 0.999299705 -gae_lambda = 0.996396005 -vtrace_rho_clip = 1.42464721 -vtrace_c_clip = 0.42439875 -prio_alpha = 0.0912185907 -prio_beta0 = 0.399999976 -clip_coef = 0.0349372625 -vf_coef = 2.09087276 -vf_clip_coef = 0.233588189 -max_grad_norm = 2.29937434 -replay_ratio = 4.11032152 +total_timesteps = 462818912 +horizon = 256 +learning_rate = 0.000529892568 +min_lr_ratio = 0.0251870397 +momentum = 0.994315207 +ent_coef = 1.02952106e-06 +gamma = 0.999357641 +gae_lambda = 0.490515351 +vtrace_rho_clip = 2 +vtrace_c_clip = 0.864432693 +prio_alpha = 0.472142577 +prio_beta0 = 0.227927983 +clip_coef = 0.16944842 +vf_coef = 3.44117165 +vf_clip_coef = 0.350708276 +max_grad_norm = 0.754219651 +replay_ratio = 4.68263817 minibatch_size = 4096 [sweep] @@ -113,7 +114,7 @@ max = 2048 [sweep.train.horizon] min = 16 -max = 64 +max = 512 [sweep.train.minibatch_size] max = 16384 @@ -154,7 +155,7 @@ scale = auto [sweep.train.prio_alpha] distribution = logit_normal min = 0.0 -max = 0.25 +max = 0.6 scale = auto [sweep.train.prio_beta0] @@ -165,7 +166,7 @@ scale = auto [sweep.train.clip_coef] min = 0.001 -max = 0.12 +max = 0.24 [sweep.train.vf_coef] distribution = log_normal @@ -173,7 +174,7 @@ min = 0.02 [sweep.train.vf_clip_coef] min = 0.03 -max = 0.25 +max = 0.5 [sweep.train.max_grad_norm] min = 0.3 @@ -186,7 +187,7 @@ max = 6.0 [sweep.env.curriculum_frac_1] distribution = uniform min = 0.0 -max = 0.18 +max = 0.35 scale = auto [sweep.env.curriculum_frac_2] @@ -258,7 +259,7 @@ scale = auto [sweep.env.damage_reward_coeff] distribution = log_normal min = 0.0015 -max = 0.006 +max = 0.00600000052 scale = auto [sweep.env.offensive_prayer_reward_coeff] @@ -282,7 +283,7 @@ scale = auto [sweep.env.zuk_untagged_healer_nonmagic_attack_bonus_coeff] distribution = uniform min = 0.0 -max = 0.70 +max = 1.0 scale = auto [sweep.env.zuk_healer_mage_attack_penalty_coeff] From 6f2220be5e10052419c6dc1ac0311efbda484654 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Thu, 20 Aug 2026 22:00:22 +0300 Subject: [PATCH 48/50] Fix OSRS viewer fidelity --- .../encounter_colosseum_render_snapshot.inc | 1 + ocean/osrs/encounters/encounter_nh_pvp.h | 1 + ocean/osrs/encounters/encounter_zulrah.h | 1 + .../encounter_inferno_player_actions.inc | 4 +- .../encounter_inferno_render_snapshot.inc | 58 +++++++++++--- ocean/osrs/osrs_encounter.h | 1 + ocean/osrs/osrs_render.h | 37 +++++---- ocean/osrs/osrs_terrain.h | 80 ++++++++++++++++++- ocean/osrs/osrs_visual.c | 2 +- ocean/osrs/tests/test_inferno_attack_styles.c | 70 ++++++++++++++-- 10 files changed, 214 insertions(+), 41 deletions(-) diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc index 6976d5f1e5..7b857807dd 100644 --- a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -1192,6 +1192,7 @@ static int col_apply_lab_command_ctx( static const EncounterDef ENCOUNTER_COLOSSEUM = { .name = "colosseum", + .display_name = "Colosseum", .obs_size = COLO_NUM_OBS, .num_action_heads = COLO_NUM_ACTION_HEADS, .action_head_dims = COLO_ACTION_DIMS, diff --git a/ocean/osrs/encounters/encounter_nh_pvp.h b/ocean/osrs/encounters/encounter_nh_pvp.h index 14a64f1cf4..df8543e23e 100644 --- a/ocean/osrs/encounters/encounter_nh_pvp.h +++ b/ocean/osrs/encounters/encounter_nh_pvp.h @@ -262,6 +262,7 @@ static int nh_pvp_get_winner(EncounterState* state, EncounterContext* context) { static const EncounterDef ENCOUNTER_NH_PVP = { .name = "nh_pvp", + .display_name = "PvP", .obs_size = NH_PVP_NUM_OBS, .num_action_heads = OSRS_BASE_NUM_ACTION_HEADS, .action_head_dims = NH_PVP_ACTION_DIMS, diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h index 5262a6c89f..4ad3bc9789 100644 --- a/ocean/osrs/encounters/encounter_zulrah.h +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -3365,6 +3365,7 @@ static void zul_init_state_ctx(EncounterState* state, EncounterContext* context) static const EncounterDef ENCOUNTER_ZULRAH = { .name = "zulrah", + .display_name = "Zulrah", .obs_size = ZUL_NUM_OBS, .num_action_heads = ZUL_NUM_ACTION_HEADS, .action_head_dims = ZUL_ACTION_HEAD_DIMS, diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc index 8b6ca41926..fe64593236 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -391,8 +391,8 @@ static void inf_tick_player_ctx( if (s->player.potion_timer > 0) s->player.potion_timer--; if (s->stamina_active_ticks > 0) s->stamina_active_ticks--; - if (!ctx->human_command_mode) { - OsrsInventoryClickActions clicks; + { + OsrsInventoryClickActions clicks = {0}; for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) clicks.equip_by_slot[slot] = actions[INF_HEAD_EQUIP_SLOT(slot)]; clicks.eat = actions[INF_HEAD_EAT]; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index 466d99b286..69c4590ffb 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -251,7 +251,6 @@ static const OsrsCombatVisualRow* inf_npc_combat_visual_row( static void inf_npc_projectile_fallback( InfNPCType type, - AttackStyle style, uint32_t* model_id, int* anim_id ) { @@ -261,15 +260,6 @@ static void inf_npc_projectile_fallback( *model_id = INF_GFX_1374_MODEL; *anim_id = INF_GFX_1374_ANIM; return; - case INF_NPC_BLOB: - if (style == ATTACK_STYLE_RANGED) { - *model_id = INF_GFX_1383_MODEL; - *anim_id = INF_GFX_1383_ANIM; - } else if (style == ATTACK_STYLE_MAGIC) { - *model_id = INF_GFX_1384_MODEL; - *anim_id = OSRS_COMBAT_PROJECTILE_MISSING; - } - return; case INF_NPC_BLOB_RANGE: *model_id = INF_GFX_1379_MODEL; *anim_id = INF_GFX_1379_ANIM; @@ -278,10 +268,36 @@ static void inf_npc_projectile_fallback( *model_id = INF_GFX_1381_MODEL; *anim_id = INF_GFX_1381_ANIM; return; + default: return; } } +static int inf_queue_inventory_click_action( + const InfernoState* s, + int* actions, + int inventory_slot +) { + if (inventory_slot < 0 || inventory_slot >= OSRS_INVENTORY_SIZE) return -1; + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret( + &s->player.inventory_cells[inventory_slot], OSRS_CLICK_TICK_FIRST); + int action_head; + if (resolution.click_action == OSRS_CLICK_EQUIP) { + int gear_slot = osrs_inventory_cell_metadata( + &s->player.inventory_cells[inventory_slot])->gear_slot; + if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) return -1; + action_head = INF_HEAD_EQUIP_SLOT(gear_slot); + } else if (resolution.click_action == OSRS_CLICK_EAT) { + action_head = INF_HEAD_EAT; + } else if (resolution.click_action == OSRS_CLICK_DRINK) { + action_head = INF_HEAD_DRINK; + } else { + return -1; + } + actions[action_head] = inventory_slot + 1; + return action_head; +} static void inf_render_post_tick_ctx( EncounterState* state, @@ -358,6 +374,7 @@ static void inf_render_post_tick_ctx( inf_npc_combat_visual_row(npc->type, actual_style); uint32_t proj_model_id = 0; int proj_anim_id = OSRS_COMBAT_PROJECTILE_MISSING; + int travel_gfx_id = 0; int impact_gfx_id = 0; if (npc_visual) { if (npc_visual->projectile.projectile_model_id > 0) @@ -367,10 +384,21 @@ static void inf_render_post_tick_ctx( if (npc_visual->projectile.impact_spotanim_id > 0) impact_gfx_id = npc_visual->projectile.impact_spotanim_id; } + if (npc->type == INF_NPC_BLOB && npc_visual && + npc_visual->projectile.travel_spotanim_id > 0) { + proj_model_id = 0; + travel_gfx_id = npc_visual->projectile.travel_spotanim_id; + } - if (proj_model_id == 0) + if (proj_model_id == 0 && travel_gfx_id == 0) inf_npc_projectile_fallback( - npc->type, actual_style, &proj_model_id, &proj_anim_id); + npc->type, &proj_model_id, &proj_anim_id); + if (proj_model_id == 0 && travel_gfx_id == 0) { + fprintf(stderr, + "inferno: missing projectile visual for npc type %d style %d\n", + (int)npc->type, (int)actual_style); + abort(); + } switch (npc->type) { case INF_NPC_JAD: @@ -448,6 +476,7 @@ static void inf_render_post_tick_ctx( .dst_size = target_size, .model_id = proj_model_id, .anim_id = proj_anim_id, + .travel_gfx_id = travel_gfx_id, .impact_gfx_id = impact_gfx_id, .start_delay = start_delay, }; @@ -714,13 +743,15 @@ static void inf_translate_human_commands_ctx( case HUMAN_COMMAND_SPEC_TOGGLE: actions[INF_HEAD_SPEC] = s->player.spec_armed ? 2 : 1; break; + case HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK: + inf_queue_inventory_click_action(s, actions, cmd->inventory_slot); + break; case HUMAN_COMMAND_EQUIP_INVENTORY_ITEM: case HUMAN_COMMAND_FIGHT_STYLE: case HUMAN_COMMAND_SET_AUTOCAST: case HUMAN_COMMAND_ITEM_ON_ITEM: case HUMAN_COMMAND_ITEM_ON_WIDGET: case HUMAN_COMMAND_SPELL_ON_WIDGET: - case HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK: case HUMAN_COMMAND_NONE: break; } @@ -931,6 +962,7 @@ static int inf_apply_lab_command_ctx( static const EncounterDef ENCOUNTER_INFERNO = { .name = "inferno", + .display_name = "Inferno", .obs_size = INF_NUM_OBS, .num_action_heads = INF_NUM_ACTION_HEADS, .action_head_dims = INF_ACTION_DIMS, diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h index ce1d9158b6..f806cb6721 100644 --- a/ocean/osrs/osrs_encounter.h +++ b/ocean/osrs/osrs_encounter.h @@ -1792,6 +1792,7 @@ static inline int encounter_find_observed_target_slot( typedef struct { const char* name; + const char* display_name; int obs_size; int num_action_heads; diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h index c3f7c4cc7e..4ff6d213a9 100644 --- a/ocean/osrs/osrs_render.h +++ b/ocean/osrs/osrs_render.h @@ -1812,7 +1812,7 @@ static void render_draw_encounter_status_text(RenderClient* rc) { font_size, (Color){ 255, 220, 190, 255 }); } -static RenderClient* render_make_client(void) { +static RenderClient* render_make_client(OsrsEnv* env) { osrs_asset_require_group(OSRS_ASSET_GROUP_CORE); osrs_asset_require_group(OSRS_ASSET_GROUP_GUI); @@ -1863,7 +1863,9 @@ static RenderClient* render_make_client(void) { rc->prev_npc_slot[i] = -1; } - InitWindow(RENDER_WINDOW_W, RENDER_WINDOW_H, "OSRS PvP Debug Viewer"); + const EncounterDef* def = (const EncounterDef*)env->encounter_def; + const char* display_name = def ? def->display_name : "PvP"; + InitWindow(RENDER_WINDOW_W, RENDER_WINDOW_H, TextFormat("OSRS %s", display_name)); SetTargetFPS(60); { @@ -2709,25 +2711,18 @@ static void render_handle_input(RenderClient* rc, OsrsEnv* env) { float wheel = GetMouseWheelMove(); - if (!rc->human_input.enabled && IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) { + if (IsMouseButtonDown(MOUSE_BUTTON_MIDDLE)) { Vector2 delta = GetMouseDelta(); rc->cam_yaw -= delta.x * 0.005f; rc->cam_pitch += delta.y * 0.005f; if (rc->cam_pitch < 0.1f) rc->cam_pitch = 0.1f; if (rc->cam_pitch > 1.4f) rc->cam_pitch = 1.4f; } - if (IsMouseButtonDown(MOUSE_BUTTON_MIDDLE)) { + if (!rc->human_input.enabled && IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) { Vector2 delta = GetMouseDelta(); - if (rc->human_input.enabled) { - rc->cam_yaw -= delta.x * 0.005f; - rc->cam_pitch += delta.y * 0.005f; - if (rc->cam_pitch < 0.1f) rc->cam_pitch = 0.1f; - if (rc->cam_pitch > 1.4f) rc->cam_pitch = 1.4f; - } else { - float cs = cosf(rc->cam_yaw), sn = sinf(rc->cam_yaw); - rc->cam_target_x += (delta.x * cs + delta.y * sn) * 0.05f; - rc->cam_target_z += (-delta.x * sn + delta.y * cs) * 0.05f; - } + float cs = cosf(rc->cam_yaw), sn = sinf(rc->cam_yaw); + rc->cam_target_x += (delta.x * cs + delta.y * sn) * 0.05f; + rc->cam_target_z += (-delta.x * sn + delta.y * cs) * 0.05f; } if (wheel != 0.0f) { rc->cam_dist *= (wheel > 0) ? (1.0f / 1.15f) : 1.15f; @@ -5774,11 +5769,15 @@ static int render_scene_is_inferno(OsrsEnv* env) { return strcmp(def->name, "inferno") == 0; } -static const char* render_control_hint_text(OsrsEnv* env) { +static const char* render_control_hint_text(RenderClient* rc, OsrsEnv* env) { if (render_scene_is_inferno(env)) { - return "Right-drag: orbit Mid-drag: pan Scroll: zoom D: debug H: human F8: lab"; + if (rc->human_input.enabled) + return "Mid-drag: orbit Right-click: interact Scroll: zoom D: debug H: policy F8: lab"; + return "Mid-drag: orbit Right-drag: pan Scroll: zoom D: debug H: human F8: lab"; } - return "Right-drag: orbit Mid-drag: pan Scroll: zoom SPACE: pause S: safe spots D: debug G: cycle entity H: human"; + if (rc->human_input.enabled) + return "Mid-drag: orbit Right-click: interact Scroll: zoom SPACE: pause S: safe spots D: debug G: cycle entity H: policy"; + return "Mid-drag: orbit Right-drag: pan Scroll: zoom SPACE: pause S: safe spots D: debug G: cycle entity H: human"; } static void render_draw_default_top_hud(RenderClient* rc, int display_tick) { @@ -6077,7 +6076,7 @@ static void render_follow_pvp_fighter_midpoint(RenderClient* rc, OsrsEnv* env, d void pvp_render(OsrsEnv* env) { RenderClient* rc = (RenderClient*)env->client; if (rc == NULL) { - rc = render_make_client(); + rc = render_make_client(env); env->client = rc; } @@ -6239,7 +6238,7 @@ void pvp_render(OsrsEnv* env) { } render_draw_top_hud(rc, env); - DrawText(render_control_hint_text(env), 10, RENDER_WINDOW_H - 20, 10, COLOR_TEXT_DIM); + DrawText(render_control_hint_text(rc, env), 10, RENDER_WINDOW_H - 20, 10, COLOR_TEXT_DIM); rc->gui.gui_entity_count = rc->entity_count; rc->gui.encounter_state = env->encounter_state; diff --git a/ocean/osrs/osrs_terrain.h b/ocean/osrs/osrs_terrain.h index e15c8be551..6c7eeb2ecf 100644 --- a/ocean/osrs/osrs_terrain.h +++ b/ocean/osrs/osrs_terrain.h @@ -11,6 +11,17 @@ #define TERR_MAGIC 0x54455252 +typedef enum { + TERRAIN_SELECTION_ALL, + TERRAIN_SELECTION_REGION, +} TerrainSelectionKind; + +typedef struct { + TerrainSelectionKind kind; + int region_x; + int region_y; +} TerrainSelection; + typedef struct { Model model; int vertex_count; @@ -25,7 +36,10 @@ typedef struct { int hm_height; } TerrainMesh; -static TerrainMesh* terrain_load(const char* path) { +static TerrainMesh* terrain_load_selected( + const char* path, + TerrainSelection selection +) { FILE* f = osrs_asset_fopen(path, "rb"); if (!f) { fprintf(stderr, "terrain_load: could not open %s\n", path); @@ -54,6 +68,50 @@ static TerrainMesh* terrain_load(const char* path) { unsigned char* raw_colors = (unsigned char*)osrs_malloc_or_abort( vert_count * 4, "terrain colors"); osrs_read_exact(f, raw_colors, 1, vert_count * 4, path, "colors"); + if (vert_count % 3 != 0) { + fprintf(stderr, "terrain_load: vertex count is not divisible by three\n"); + abort(); + } + + if (selection.kind == TERRAIN_SELECTION_REGION) { + uint32_t selected_vert_count = 0; + for (uint32_t source_vertex = 0; + source_vertex < vert_count; + source_vertex += 3) { + float* triangle = raw_verts + source_vertex * 3; + float centroid_x = + (triangle[0] + triangle[3] + triangle[6]) / 3.0f; + float centroid_y = + -(triangle[2] + triangle[5] + triangle[8]) / 3.0f; + int region_x = (int)floorf(centroid_x / 64.0f); + int region_y = (int)floorf(centroid_y / 64.0f); + if (region_x != selection.region_x || + region_y != selection.region_y) { + continue; + } + + if (selected_vert_count != source_vertex) { + memcpy( + raw_verts + selected_vert_count * 3, + triangle, + 9 * sizeof(float)); + memcpy( + raw_colors + selected_vert_count * 4, + raw_colors + source_vertex * 4, + 12); + } + selected_vert_count += 3; + } + if (selected_vert_count == 0) { + fprintf(stderr, "terrain_load: region (%d, %d) is empty\n", + selection.region_x, selection.region_y); + abort(); + } + vert_count = selected_vert_count; + region_count = 1; + fprintf(stderr, "terrain region: (%d, %d), %u verts\n", + selection.region_x, selection.region_y, vert_count); + } Mesh mesh = { 0 }; mesh.vertexCount = (int)vert_count; @@ -126,6 +184,26 @@ static TerrainMesh* terrain_load(const char* path) { return tm; } +static TerrainMesh* terrain_load(const char* path) { + return terrain_load_selected( + path, + (TerrainSelection){ .kind = TERRAIN_SELECTION_ALL }); +} + +static TerrainMesh* terrain_load_region( + const char* path, + int region_x, + int region_y +) { + return terrain_load_selected( + path, + (TerrainSelection){ + .kind = TERRAIN_SELECTION_REGION, + .region_x = region_x, + .region_y = region_y, + }); +} + static void terrain_offset(TerrainMesh* tm, int wx, int wy) { if (!tm || !tm->loaded) return; float dx = (float)wx; diff --git a/ocean/osrs/osrs_visual.c b/ocean/osrs/osrs_visual.c index 261a7dd783..41eb7a1a55 100644 --- a/ocean/osrs/osrs_visual.c +++ b/ocean/osrs/osrs_visual.c @@ -2225,7 +2225,7 @@ static void run_visual( rc->npc_model_cache ? rc->npc_model_cache->count : 0, rc->npc_anim_cache ? rc->npc_anim_cache->seq_count : 0); } else if (encounter_name && strcmp(encounter_name, "inferno") == 0) { - rc->terrain = terrain_load(OSRS_ASSET("inferno.terrain")); + rc->terrain = terrain_load_region(OSRS_ASSET("inferno.terrain"), 35, 83); rc->objects = objects_load(OSRS_ASSET("inferno.objects")); rc->objects_zuk = objects_load(OSRS_ASSET("inferno_zuk.objects")); if (rc->terrain) diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index 9ec6d9e5ad..dd34c1a573 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -6908,6 +6908,60 @@ static void test_inferno_human_equip_does_not_snap_loadout(void) { inf_destroy(raw); } +static void test_inferno_human_primary_inventory_click_equips_item(void) { + printf("--- inferno human primary inventory click equips item ---\n"); + + EncounterState* raw = inf_create(); + InfernoState* state = (InfernoState*)raw; + inf_reset_ctx(raw, (EncounterContext*)&test_context, 123); + uint8_t old_weapon = state->player.equipped[GEAR_SLOT_WEAPON]; + state->player.inventory_cells[0] = + osrs_inventory_cell_from_item(ITEM_TOXIC_BLOWPIPE); + + HumanInput input; + human_input_init(&input); + input.enabled = 1; + human_input_queue_inventory_primary_click(&input, 0); + + inf_step_human_commands_ctx(raw, (EncounterContext*)&test_context, &input); + + ASSERT_INT_EQ("primary inventory click equips blowpipe", + state->player.equipped[GEAR_SLOT_WEAPON], ITEM_TOXIC_BLOWPIPE); + ASSERT_INT_EQ("primary inventory click swaps old weapon into source cell", + osrs_inventory_cell_item_index(&state->player.inventory_cells[0]), old_weapon); + ASSERT_INT_EQ("queued primary click drained", input.commands.count, 0); + + human_input_destroy(&input); + inf_destroy(raw); +} + +static void test_inferno_fight_style_command_does_not_click_inventory(void) { + printf("--- inferno fight style command does not click inventory ---\n"); + + EncounterState* raw = inf_create(); + InfernoState* state = (InfernoState*)raw; + inf_reset_ctx(raw, (EncounterContext*)&test_context, 123); + uint8_t old_weapon = state->player.equipped[GEAR_SLOT_WEAPON]; + state->player.inventory_cells[0] = + osrs_inventory_cell_from_item(ITEM_OSMUMTENS_FANG); + + HumanInput input; + human_input_init(&input); + input.enabled = 1; + human_input_queue_fight_style(&input, FIGHT_STYLE_ACCURATE); + + inf_step_human_commands_ctx(raw, (EncounterContext*)&test_context, &input); + + ASSERT_INT_EQ("fight style command preserves equipped weapon", + state->player.equipped[GEAR_SLOT_WEAPON], old_weapon); + ASSERT_INT_EQ("fight style command preserves inventory cell", + osrs_inventory_cell_item_index(&state->player.inventory_cells[0]), + ITEM_OSMUMTENS_FANG); + + human_input_destroy(&input); + inf_destroy(raw); +} + static void test_jad_render_uses_style_specific_attack_animation(void) { printf("--- jad render uses style-specific attack animation ---\n"); @@ -7129,8 +7183,10 @@ static void test_inferno_npc_projectile_render_uses_reference_visual_timing(void inf_render_post_tick_ctx((EncounterState*)&blob_state, (EncounterContext*)&test_context, &blob_ov); ASSERT_INT_EQ("blob projectile count", blob_ov.projectile_count, 1); - ASSERT_INT_EQ("blob ranged projectile model", - blob_ov.projectiles[0].model_id, INF_GFX_1383_MODEL); + ASSERT_INT_EQ("blob ranged projectile uses travel spotanim model", + blob_ov.projectiles[0].model_id, 0); + ASSERT_INT_EQ("blob ranged projectile travel spotanim", + blob_ov.projectiles[0].travel_gfx_id, 1383); ASSERT_INT_EQ("blob ranged projectile animation", blob_ov.projectiles[0].anim_id, INF_GFX_1383_ANIM); ASSERT_INT_EQ("blob ranged impact spotanim", @@ -7153,9 +7209,11 @@ static void test_inferno_npc_projectile_render_uses_reference_visual_timing(void inf_render_post_tick_ctx((EncounterState*)&blob_magic_state, (EncounterContext*)&test_context, &blob_magic_ov); ASSERT_INT_EQ("blob magic projectile count", blob_magic_ov.projectile_count, 1); - ASSERT_INT_EQ("blob magic projectile model", - blob_magic_ov.projectiles[0].model_id, INF_GFX_1384_MODEL); - ASSERT_INT_EQ("blob magic projectile has no placeholder animation", + ASSERT_INT_EQ("blob magic projectile uses travel spotanim model", + blob_magic_ov.projectiles[0].model_id, 0); + ASSERT_INT_EQ("blob magic projectile travel spotanim", + blob_magic_ov.projectiles[0].travel_gfx_id, 1384); + ASSERT_INT_EQ("blob magic projectile animation comes from spotanim", blob_magic_ov.projectiles[0].anim_id, OSRS_COMBAT_PROJECTILE_MISSING); ASSERT_INT_EQ("blob magic impact spotanim", blob_magic_ov.projectiles[0].impact_gfx_id, 0); @@ -8674,6 +8732,8 @@ int main(void) { test_inferno_refresh_after_state_load_rebuilds_derived_state(); test_inferno_healer_transition_stats_track_episode_progress(); test_inferno_human_equip_does_not_snap_loadout(); + test_inferno_human_primary_inventory_click_equips_item(); + test_inferno_fight_style_command_does_not_click_inventory(); test_jad_render_uses_style_specific_attack_animation(); test_inferno_render_uses_npc_death_animation(); test_jad_magic_render_emits_three_offset_projectiles(); From c8042cb8b0d47b9b40566453e4c4f4dd86359740 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Thu, 20 Aug 2026 22:23:26 +0300 Subject: [PATCH 49/50] Add 180ms inventory anti-drag --- ocean/osrs/osrs_gui.h | 32 +++++++++---------- ocean/osrs/osrs_inventory_drag.h | 27 ++++++++++++++++ ocean/osrs/tests/test_inferno_attack_styles.c | 29 +++++++++++++++++ 3 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 ocean/osrs/osrs_inventory_drag.h diff --git a/ocean/osrs/osrs_gui.h b/ocean/osrs/osrs_gui.h index 359747b3f0..eb1f14cd1d 100644 --- a/ocean/osrs/osrs_gui.h +++ b/ocean/osrs/osrs_gui.h @@ -9,6 +9,7 @@ #include "osrs_asset_raylib.h" #include "osrs_human_input_types.h" +#include "osrs_inventory_drag.h" #if __has_include("raylib.h") #include "raylib.h" @@ -212,8 +213,6 @@ typedef struct { } InvSlot; #define INV_DIM_TICKS 15 -#define INV_DRAG_DEAD_ZONE 5 -#define INV_DRAG_HOLD_SECONDS 0.030 typedef enum { INV_ACTION_NONE = 0, @@ -2332,8 +2331,11 @@ static void gui_inv_handle_mouse(GuiState* gs, Player* p, HumanInput* hi) { gs->inv_grid[target] = gs->inv_grid[gs->inv_drag_src_slot]; gs->inv_grid[gs->inv_drag_src_slot] = tmp; } - gs->inv_drag_active = 0; - gs->inv_drag_src_slot = -1; + osrs_inventory_drag_release( + &gs->inv_drag_active, + &gs->inv_drag_src_slot, + &gs->inv_dim_slot, + &gs->inv_dim_timer); } return; } @@ -2348,18 +2350,16 @@ static void gui_inv_handle_mouse(GuiState* gs, Player* p, HumanInput* hi) { } } - if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && gs->inv_drag_src_slot >= 0 && !gs->inv_drag_active && - GetTime() - gs->inv_drag_press_time >= INV_DRAG_HOLD_SECONDS) { - int dx = mx - gs->inv_drag_start_x; - int dy = my - gs->inv_drag_start_y; - if (dx > INV_DRAG_DEAD_ZONE || dx < -INV_DRAG_DEAD_ZONE || - dy > INV_DRAG_DEAD_ZONE || dy < -INV_DRAG_DEAD_ZONE) { - gs->inv_drag_active = 1; - gs->inv_drag_mouse_x = mx; - gs->inv_drag_mouse_y = my; - gs->inv_dim_slot = gs->inv_drag_src_slot; - gs->inv_dim_timer = 9999; - } + if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && gs->inv_drag_src_slot >= 0 && + !gs->inv_drag_active && osrs_inventory_drag_ready( + GetTime() - gs->inv_drag_press_time, + mx - gs->inv_drag_start_x, + my - gs->inv_drag_start_y)) { + gs->inv_drag_active = 1; + gs->inv_drag_mouse_x = mx; + gs->inv_drag_mouse_y = my; + gs->inv_dim_slot = gs->inv_drag_src_slot; + gs->inv_dim_timer = 9999; } if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) && gs->inv_drag_src_slot >= 0 && !gs->inv_drag_active) { diff --git a/ocean/osrs/osrs_inventory_drag.h b/ocean/osrs/osrs_inventory_drag.h new file mode 100644 index 0000000000..6352da0d13 --- /dev/null +++ b/ocean/osrs/osrs_inventory_drag.h @@ -0,0 +1,27 @@ +#ifndef OSRS_INVENTORY_DRAG_H +#define OSRS_INVENTORY_DRAG_H + +#define OSRS_INVENTORY_DRAG_DEAD_ZONE 5 +#define OSRS_INVENTORY_DRAG_HOLD_SECONDS 0.180 + +static inline int osrs_inventory_drag_ready(double held_seconds, int delta_x, int delta_y) { + return held_seconds >= OSRS_INVENTORY_DRAG_HOLD_SECONDS && + (delta_x > OSRS_INVENTORY_DRAG_DEAD_ZONE || + delta_x < -OSRS_INVENTORY_DRAG_DEAD_ZONE || + delta_y > OSRS_INVENTORY_DRAG_DEAD_ZONE || + delta_y < -OSRS_INVENTORY_DRAG_DEAD_ZONE); +} + +static inline void osrs_inventory_drag_release( + int* active, + int* source_slot, + int* dim_slot, + int* dim_timer +) { + *active = 0; + *source_slot = -1; + *dim_slot = -1; + *dim_timer = 0; +} + +#endif diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index dd34c1a573..a0bb88e0fc 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -17,6 +17,7 @@ static void inf_init_unfinalized_context(InfernoContext* ctx) { #include "ocean/osrs/osrs_effects.h" #include "ocean/osrs/osrs_projectile_orientation.h" #include "ocean/osrs/osrs_render_motion.h" +#include "ocean/osrs/osrs_inventory_drag.h" #include #include "ocean/osrs/tests/osrs_test_check.h" @@ -6908,6 +6909,32 @@ static void test_inferno_human_equip_does_not_snap_loadout(void) { inf_destroy(raw); } +static void test_inventory_drag_requires_180ms_hold_and_dead_zone(void) { + printf("--- inventory drag requires 180ms hold and dead zone ---\n"); + + ASSERT_INT_EQ("movement before 180ms remains a click", + osrs_inventory_drag_ready(0.179, 6, 0), 0); + ASSERT_INT_EQ("movement at dead-zone edge remains a click", + osrs_inventory_drag_ready(0.180, 5, 0), 0); + ASSERT_INT_EQ("movement past dead zone after 180ms starts drag", + osrs_inventory_drag_ready(0.180, 6, 0), 1); +} + +static void test_inventory_drag_release_restores_source_opacity(void) { + printf("--- inventory drag release restores source opacity ---\n"); + + int active = 1; + int source_slot = 4; + int dim_slot = 4; + int dim_timer = 9999; + osrs_inventory_drag_release(&active, &source_slot, &dim_slot, &dim_timer); + + ASSERT_INT_EQ("released drag is inactive", active, 0); + ASSERT_INT_EQ("released drag clears source slot", source_slot, -1); + ASSERT_INT_EQ("released drag clears dim slot", dim_slot, -1); + ASSERT_INT_EQ("released drag clears dim timer", dim_timer, 0); +} + static void test_inferno_human_primary_inventory_click_equips_item(void) { printf("--- inferno human primary inventory click equips item ---\n"); @@ -8732,6 +8759,8 @@ int main(void) { test_inferno_refresh_after_state_load_rebuilds_derived_state(); test_inferno_healer_transition_stats_track_episode_progress(); test_inferno_human_equip_does_not_snap_loadout(); + test_inventory_drag_requires_180ms_hold_and_dead_zone(); + test_inventory_drag_release_restores_source_opacity(); test_inferno_human_primary_inventory_click_equips_item(); test_inferno_fight_style_command_does_not_click_inventory(); test_jad_render_uses_style_specific_attack_animation(); From 67993a516056f1ffe57c4306eba6d0e84cfe2ef3 Mon Sep 17 00:00:00 2001 From: Valtteri Valo Date: Thu, 20 Aug 2026 22:36:11 +0300 Subject: [PATCH 50/50] Enable Inferno inventory reordering --- .../encounter_inferno_render_snapshot.inc | 7 ++- ocean/osrs/tests/test_inferno_attack_styles.c | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc index 69c4590ffb..5b0a6cdd64 100644 --- a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -746,10 +746,15 @@ static void inf_translate_human_commands_ctx( case HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK: inf_queue_inventory_click_action(s, actions, cmd->inventory_slot); break; + case HUMAN_COMMAND_ITEM_ON_ITEM: + osrs_inventory_swap_cells( + s->player.inventory_cells, + cmd->inventory_slot, + cmd->target_inventory_slot); + break; case HUMAN_COMMAND_EQUIP_INVENTORY_ITEM: case HUMAN_COMMAND_FIGHT_STYLE: case HUMAN_COMMAND_SET_AUTOCAST: - case HUMAN_COMMAND_ITEM_ON_ITEM: case HUMAN_COMMAND_ITEM_ON_WIDGET: case HUMAN_COMMAND_SPELL_ON_WIDGET: case HUMAN_COMMAND_NONE: diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c index a0bb88e0fc..fe6452e0f9 100644 --- a/ocean/osrs/tests/test_inferno_attack_styles.c +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -6935,6 +6935,63 @@ static void test_inventory_drag_release_restores_source_opacity(void) { ASSERT_INT_EQ("released drag clears dim timer", dim_timer, 0); } +static void test_inferno_human_item_drag_reorders_inventory_contract(void) { + printf("--- inferno human item drag reorders inventory contract ---\n"); + + EncounterState* raw = inf_create(); + InfernoState* state = (InfernoState*)raw; + inf_reset_ctx(raw, (EncounterContext*)&test_context, 123); + state->player.inventory_cells[0] = + osrs_inventory_cell_from_item(ITEM_TOXIC_BLOWPIPE); + state->player.inventory_cells[9] = + osrs_inventory_cell_from_item(ITEM_MASORI_BODY_F); + + HumanInput input; + human_input_init(&input); + input.enabled = 1; + human_input_queue_item_on_item( + &input, + 0, + 9, + ITEM_TOXIC_BLOWPIPE, + osrs_inventory_cell_raw_osrs_id(&state->player.inventory_cells[0])); + + inf_step_human_commands_ctx(raw, (EncounterContext*)&test_context, &input); + + ASSERT_INT_EQ("drag moves body to source cell", + osrs_inventory_cell_item_index(&state->player.inventory_cells[0]), + ITEM_MASORI_BODY_F); + ASSERT_INT_EQ("drag moves blowpipe to target cell", + osrs_inventory_cell_item_index(&state->player.inventory_cells[9]), + ITEM_TOXIC_BLOWPIPE); + + float obs[INF_NUM_OBS]; + inf_write_obs_ctx(raw, (EncounterContext*)&test_context, obs); + ASSERT_FLOAT_NEAR("observation follows reordered source cell", + obs[OSRS_SHARED_OBS_INVENTORY_START], + osrs_inventory_cell_obs_code_encode( + osrs_inventory_content_code_from_item(ITEM_MASORI_BODY_F)), + 0.0f); + ASSERT_FLOAT_NEAR("observation follows reordered target cell", + obs[OSRS_SHARED_OBS_INVENTORY_START + 9], + osrs_inventory_cell_obs_code_encode( + osrs_inventory_content_code_from_item(ITEM_TOXIC_BLOWPIPE)), + 0.0f); + + float mask[INF_ACTION_MASK_SIZE]; + inf_write_mask_ctx(raw, (EncounterContext*)&test_context, mask); + int weapon_offset = + inf_action_head_mask_offset(INF_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)); + ASSERT_FLOAT_NEAR("weapon equip mask leaves source cell", + mask[weapon_offset + 1], 0.0f, 0.0f); + ASSERT_FLOAT_NEAR("weapon equip mask follows target cell", + mask[weapon_offset + 10], 1.0f, 0.0f); + ASSERT_INT_EQ("queued item drag drained", input.commands.count, 0); + + human_input_destroy(&input); + inf_destroy(raw); +} + static void test_inferno_human_primary_inventory_click_equips_item(void) { printf("--- inferno human primary inventory click equips item ---\n"); @@ -8761,6 +8818,7 @@ int main(void) { test_inferno_human_equip_does_not_snap_loadout(); test_inventory_drag_requires_180ms_hold_and_dead_zone(); test_inventory_drag_release_restores_source_opacity(); + test_inferno_human_item_drag_reorders_inventory_contract(); test_inferno_human_primary_inventory_click_equips_item(); test_inferno_fight_style_command_does_not_click_inventory(); test_jad_render_uses_style_specific_attack_animation();