Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/module-durable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,23 @@ jobs:
args=()
for target in "${targets[@]}"; do args+=(--test "$target"); done
echo "targets: ${targets[*]}"
cargo test -p lyracore-module "${args[@]}" -- --ignored --test-threads=4 --skip deadmines --skip lethal_floor
cargo test -p lyracore-module "${args[@]}" -- --ignored --test-threads=4 --skip deadmines --skip lethal_floor --skip populated_old_module_upgrades_without_clearing

- name: Check out the populated-upgrade baseline
if: matrix.shard == 2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: 44b04878f2050936faa61d6f087f3aa1a7a78a46
path: .lyracore-upgrade-baseline
persist-credentials: false

- name: Verify the populated Module upgrade
if: matrix.shard == 2
run: |
baseline_target="$RUNNER_TEMP/lyracore-upgrade-baseline-target"
cargo build --manifest-path .lyracore-upgrade-baseline/Cargo.toml --target-dir "$baseline_target" --release --target wasm32-unknown-unknown -p lyracore-module --features=debug_reducers,package_test_fixture
export LYRACORE_TEST_BASELINE_WASM="$baseline_target/wasm32-unknown-unknown/release/lyracore_module.wasm"
cargo test -p lyracore-module --test additive_upgrade populated_old_module_upgrades_without_clearing -- --ignored --exact --test-threads=1

- name: Run the single-shard auction Refusal regression
if: matrix.shard == 0
Expand Down
22 changes: 11 additions & 11 deletions crates/lyracore-shared/src/vmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ pub enum RayFlavor {
}

// Triangles span cells, so choose the globally nearest hit across every crossed cell.
pub fn cast_ray(
fetch: &mut impl FnMut(u16, u16) -> Option<Vec<VmapTri>>,
pub fn cast_ray<T: AsRef<[VmapTri]>>(
fetch: &mut impl FnMut(u16, u16) -> Option<T>,
a: [f32; 3],
b: [f32; 3],
flavor: RayFlavor,
Expand All @@ -111,8 +111,8 @@ pub fn cast_ray(
nearest_hit(fetch, a, b, want).map(|(t, _)| point_at(a, b, t))
}

pub fn cast_floor(
fetch: &mut impl FnMut(u16, u16) -> Option<Vec<VmapTri>>,
pub fn cast_floor<T: AsRef<[VmapTri]>>(
fetch: &mut impl FnMut(u16, u16) -> Option<T>,
a: [f32; 3],
b: [f32; 3],
) -> Option<[f32; 3]> {
Expand All @@ -134,8 +134,8 @@ pub fn supports_walking(normal: [f32; 3], up: [f32; 3]) -> bool {
&& alignment * alignment >= 50.0f32.to_radians().cos().powi(2) * lengths_squared
}

fn nearest_hit(
fetch: &mut impl FnMut(u16, u16) -> Option<Vec<VmapTri>>,
fn nearest_hit<T: AsRef<[VmapTri]>>(
fetch: &mut impl FnMut(u16, u16) -> Option<T>,
a: [f32; 3],
b: [f32; 3],
want: impl Fn(&VmapTri) -> bool,
Expand All @@ -145,7 +145,7 @@ fn nearest_hit(
let Some(tris) = fetch(cx, cy) else {
continue;
};
for t in &tris {
for t in tris.as_ref() {
if !want(t) {
continue;
}
Expand Down Expand Up @@ -176,8 +176,8 @@ pub fn group_is_indoor(mogp_flags: u32) -> bool {

pub const AREA_PROBE_DOWN_YD: f32 = 10.0;

pub fn cast_ray_area(
fetch: &mut impl FnMut(u16, u16) -> Option<Vec<VmapTri>>,
pub fn cast_ray_area<T: AsRef<[VmapTri]>>(
fetch: &mut impl FnMut(u16, u16) -> Option<T>,
a: [f32; 3],
b: [f32; 3],
) -> Option<AreaInfo> {
Expand Down Expand Up @@ -386,7 +386,7 @@ mod tests {
let bottom = [0.25, 0.25, 0.0];
let mut fetch = |_, _| Some(vec![tri(5.0, class), tri(10.0, TriClass::M2)]);
assert!((cast_floor(&mut fetch, top, bottom).unwrap()[2] - 10.0).abs() < 0.001);
assert!(cast_floor(&mut |_, _| None, top, bottom).is_none());
assert!(cast_floor(&mut |_, _| None::<Vec<VmapTri>>, top, bottom).is_none());
}

#[test]
Expand Down Expand Up @@ -678,7 +678,7 @@ mod tests {

#[test]
fn a_probe_with_no_wmo_geometry_in_range_finds_no_area() {
let mut fetch = |_fx: u16, _fy: u16| None;
let mut fetch = |_fx: u16, _fy: u16| None::<Vec<VmapTri>>;
assert_eq!(
cast_ray_area(&mut fetch, [0.0, 0.0, 15.0], [0.0, 0.0, -5.0]),
None
Expand Down
16 changes: 16 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@ dedicated Instance firings must not repeat them. The
[decay](../module/src/creatures/tick/lifecycle.rs) arms a future respawn deadline to prevent a corpse
from returning in the same firing.

Simulation candidates come from maintained WorldEntity predicates. Entity insert/update methods stamp
`regeneration_pending`, `combat_active` and `simulation_indexed` in the same transaction as the game
fields. A migrated row defaults to `simulation_indexed=false`. Each creature firing backfills at most
512 rows through an index. Creature phases pause until all rows have current predicates; other
scheduled reducers and ordinary gameplay continue. The upgrade pause grows with the existing entity
count, so test it on an isolated copy of the target Shard before rollout. Rows whose health and power
need no regeneration leave the work set; Rage stays pending until depleted. Pet sensing uses the owner
index, Character snapshots use the entry index, and dedicated Instance firings advance only that
Instance's splines. The phase order stays unchanged.
Operator SQL that changes health, power, death or combat flags must also set
`simulation_indexed=false`; ordinary reducer writes stamp these predicates automatically.

Creature firings reuse decoded static collision cells within the firing, keyed by generation and cell.
The cache holds at most 128 cells or 4 MiB of triangles. Dynamic GameObject colliders remain fresh.
Uncached queries keep owned triangle vectors, and no cache persists across reducer transactions.

#### Navigation legs

The [shared leg search](../crates/lyracore-shared/src/nav.rs) plans short movement legs. Callers
Expand Down
5 changes: 5 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ CI runs every target in three parallel jobs with `--test-threads=4 --skip deadmi
lethal_floor`. The Deadmines targets need the separately installed dungeons Package, and the
`lethal_floor` ranged phase is flaky.

CI runs `populated_old_module_upgrades_without_clearing` separately. It builds the Module at
`44b04878f2050936faa61d6f087f3aa1a7a78a46`, sets `LYRACORE_TEST_BASELINE_WASM` to that Wasm,
and publishes the current Module over the populated fixture without clearing it. Other durable
runs must skip that test unless the baseline Wasm is available.

`scripts/durable-test-targets.py` assigns targets by the durations in
`.github/durable-test-seconds.json`. It includes every current target once and estimates 20 seconds
for a new target. `JOB_SECONDS` reserves time for each job's builds, setup and Gateway checks.
Expand Down
3 changes: 3 additions & 0 deletions gateway/src/codec/entity/subscriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,9 @@ pub(crate) mod tests {
sheet_ranged_ap: 0,
sheet_ranged_dmg_min: 0,
sheet_ranged_dmg_max: 0,
combat_active: false,
regeneration_pending: false,
simulation_indexed: false,
}
}

Expand Down
2 changes: 1 addition & 1 deletion gateway/tests/schema_parity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ parity_test!(parity_game_world_entity, "game_world_entity", lyracore_module::Wor
sheet_str_bonus, sheet_agi_bonus, sheet_sta_bonus, sheet_int_bonus, sheet_spi_bonus,
sheet_ap_base, sheet_ap_mods, sheet_dmg_min, sheet_dmg_max, sheet_crit_bp, unit_bytes_2,
bank_bag_slots, mount_display_id, zone_id, sheet_ranged_ap, sheet_ranged_dmg_min,
sheet_ranged_dmg_max,
sheet_ranged_dmg_max, regeneration_pending, combat_active, simulation_indexed,
});
parity_test!(parity_game_zone_weather, "game_zone_weather", lyracore_module::ZoneWeather, bindings::zone_weather_type::ZoneWeather, {
zone_id, weather_type, intensity, changed_at_micros,
Expand Down
2 changes: 1 addition & 1 deletion importer/src/vmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ fn walkable_floor(
rows.iter()
.copied()
.filter(|tri| matches!(tri.class, TriClass::Wmo { .. }))
.collect()
.collect::<Vec<_>>()
})
},
top,
Expand Down
8 changes: 4 additions & 4 deletions module/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ pub(crate) fn restore_sessionless_body(
ctx: &ReducerContext,
character_guid: u64,
) -> Option<crate::tables::WorldEntity> {
use crate::tables::{game_creature_spline, game_world_entity};
use spacetimedb::{Identity, Table};
use crate::tables::game_creature_spline;
use spacetimedb::Identity;

if let Ok(entity) = crate::helpers::live_entity(ctx, character_guid) {
return Some(entity);
Expand All @@ -282,7 +282,7 @@ pub(crate) fn restore_sessionless_body(
entity.player_flags = player_flags;
entity.unit_bytes_1 = unit_bytes_1;
}
let entity = ctx.db.game_world_entity().insert(entity);
let entity = entity.insert(ctx);
ctx.db.game_creature_spline().guid().delete(character_guid);
Some(entity)
}
Expand All @@ -302,7 +302,7 @@ pub(crate) fn stop_movement(ctx: &ReducerContext, actor_guid: u64) {
}
if let Some(mut entity) = ctx.db.game_world_entity().guid().find(actor_guid) {
crate::creatures::tick::stop_where_rendered(ctx, &mut entity);
ctx.db.game_world_entity().guid().update(entity);
entity.update(ctx);
} else {
ctx.db.game_creature_spline().guid().delete(actor_guid);
}
Expand Down
6 changes: 3 additions & 3 deletions module/src/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1761,7 +1761,7 @@ fn consume_listing_value(ctx: &ReducerContext, listing: &PreparedListing) {
let mut seller = crate::helpers::acting_entity_by_guid(ctx, listing.request.seller_guid)
.expect("validated listing seller remains present in the reducer transaction");
seller.money -= listing.deposit;
ctx.db.game_world_entity().guid().update(seller);
seller.update(ctx);
ctx.db
.game_item_instance()
.guid()
Expand Down Expand Up @@ -1920,7 +1920,7 @@ impl BidSource for CtxBidSource<'_> {
.money
.checked_sub(request.offer)
.ok_or(AuctionRefusal::NotEnoughMoney)?;
self.ctx.db.game_world_entity().guid().update(bidder);
bidder.update(self.ctx);
self.ctx.db.game_auction_bid_hold().insert(AuctionBidHold {
operation_id: request.operation_id,
bidder_guid: request.bidder_guid,
Expand Down Expand Up @@ -1951,7 +1951,7 @@ impl BidSource for CtxBidSource<'_> {
.ok_or(AuctionRefusal::Database)?;
let (money, deferred_refund) = crate::mail::split_refund(bidder.money, refund);
bidder.money = money;
self.ctx.db.game_world_entity().guid().update(bidder);
bidder.update(self.ctx);
deferred_refund
} else {
0
Expand Down
2 changes: 1 addition & 1 deletion module/src/auction/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ pub fn debug_stage_auction_cancel_fixture(
.ok_or_else(|| format!("auctioneer {auctioneer_guid} is not in the world"))?;
auctioneer.faction_template = CANCEL_FIXTURE_FACTION_TEMPLATE;
auctioneer.npc_flags |= lyracore_shared::constants::npc_flags::AUCTIONEER;
ctx.db.game_world_entity().guid().update(auctioneer);
auctioneer.update(ctx);
}

let expires_micros = ctx
Expand Down
3 changes: 1 addition & 2 deletions module/src/away.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use spacetimedb::{table, ReducerContext, Table};

use crate::game_world_entity;
use lyracore_shared::chat::chat_kind;
use lyracore_shared::constants::player_flags;

Expand Down Expand Up @@ -74,7 +73,7 @@ pub(crate) fn apply_set_away(
let character_guid = entity.guid;
if change.player_flags != entity.player_flags {
entity.player_flags = change.player_flags;
ctx.db.game_world_entity().guid().update(entity);
entity.update(ctx);
}
write_reply(ctx, character_guid, change.reply);
Ok(())
Expand Down
4 changes: 2 additions & 2 deletions module/src/breath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ pub fn tick_breath(ctx: &ReducerContext, _schedule: BreathSchedule) {
if let Some(mut pre_death) = ctx.db.game_world_entity().guid().find(entity_guid)
{
pre_death.health = health;
ctx.db.game_world_entity().guid().update(pre_death);
pre_death.update(ctx);
}
}
crate::combat::kill_player(ctx, entity_guid, entity_guid);
Expand All @@ -137,7 +137,7 @@ pub fn tick_breath(ctx: &ReducerContext, _schedule: BreathSchedule) {
if !died && health != entity.health {
let mut hurt = entity;
hurt.health = health;
ctx.db.game_world_entity().guid().update(hurt);
hurt.update(ctx);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion module/src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ fn write_chat_event(
if lyracore_shared::chat::broadcast_chat::is_zone_wide(chat_type) {
sender.zone_id =
crate::terrain::zone_id_at(ctx, sender.map_id, sender.x, sender.y).unwrap_or(0);
sender = ctx.db.game_world_entity().guid().update(sender);
sender = sender.update(ctx);
}
ctx.db.game_chat_event_live().insert(ChatEvent {
id: 0,
Expand Down
16 changes: 8 additions & 8 deletions module/src/combat/death.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ pub fn debug_stage_lethal_damage_floor_fixture(
}
creature.health = 10;
creature.stance = 1;
entities.guid().update(creature);
creature.update(ctx);
let definition_revision = crate::creatures::current_definition_revision(ctx, creature_guid);
if definition_revision.value == 0
|| !set_lethal_damage_floor(ctx, creature_guid, definition_revision.value, true)
Expand Down Expand Up @@ -292,7 +292,7 @@ pub fn debug_set_lethal_damage_floor_health_fixture(
return Err("fixture health must keep the creature alive".to_string());
}
creature.health = health;
entities.guid().update(creature);
creature.update(ctx);
Ok(())
}

Expand Down Expand Up @@ -493,7 +493,7 @@ fn kill_creature_with_attribution(
roll_corpse_loot(ctx, &mut target, target_guid, entitlement.as_ref());
target.health = 0;
target.dead = true;
entities.guid().update(target);
target.update(ctx);
crate::creatures::begin_death_dispatch(ctx, target_guid, killer);
// Snapshot threat before disengage clears the ledger used by death hooks.
let threat_snapshot: Vec<(u64, i64)> = ctx
Expand All @@ -519,7 +519,7 @@ fn kill_creature_with_attribution(
if !is_engaged(ctx, killer_guid) {
if let Some(mut k) = entities.guid().find(killer_guid) {
k.combat_until_ms = (ctx.timestamp.to_micros_since_unix_epoch() / 1000) as u64;
entities.guid().update(k);
k.update(ctx);
}
}
}
Expand Down Expand Up @@ -656,7 +656,7 @@ pub(crate) fn kill_player(ctx: &ReducerContext, victim_guid: u64, killer_guid: u
victim.health = 0;
victim.dead = true;
victim.combat_until_ms = (ctx.timestamp.to_micros_since_unix_epoch() / 1000) as u64;
entities.guid().update(victim);
victim.update(ctx);
let killer = (killer_guid != 0).then_some(killer_guid);
crate::creatures::begin_death_dispatch(ctx, victim_guid, killer);
disengage(ctx, victim_guid);
Expand Down Expand Up @@ -842,7 +842,7 @@ pub(crate) fn apply_hit(
if is_rage_user(a) {
if let Some(mut fresh) = entities.guid().find(attacker_guid) {
fresh.power = (fresh.power + rage_from_damage(dmg, true)).min(fresh.max_power);
entities.guid().update(fresh);
fresh.update(ctx);
}
}
if attacker_is_player && !target_is_player {
Expand All @@ -853,7 +853,7 @@ pub(crate) fn apply_hit(

if is_lethal(target.health, dmg) && duel_takes_lethal_hit(ctx, attacker_guid, &target) {
target.health = 1;
entities.guid().update(target);
target.update(ctx);
if let Some(duel) = crate::duel::active_duel_between(ctx, attacker_guid, target_guid) {
crate::duel::complete_duel(
ctx,
Expand Down Expand Up @@ -897,7 +897,7 @@ pub(crate) fn apply_hit(
if weapon && is_rage_user(&target) {
target.power = (target.power + rage_from_damage(dmg, false)).min(target.max_power);
}
entities.guid().update(target);
target.update(ctx);
if damage.lethal_prevented {
commit_death_prevention(ctx, target_guid, attacker_guid);
}
Expand Down
Loading
Loading