feat(runtime): run resource schedules during world steps
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
This commit is contained in:
5 files changed
+245
-49
No files matched your search
@@ -1560,27 +1560,36 @@ fn prepare_first_preview_assets<R: ResourceRepository>(
|
|||||||
Ok(MissionAssets::default())
|
Ok(MissionAssets::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Steps headless mode.
|
/// Steps the world and due Behavior resource distributors at an explicit queue clock.
|
||||||
|
///
|
||||||
|
/// Resource updates are prepared before queued world commands and deferred deletion,
|
||||||
|
/// and published only after the world step succeeds. This does not yet execute the
|
||||||
|
/// separate Control clock, movement, or general mission script handlers.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns [`EngineError`] when the world step fails.
|
/// Returns [`EngineError`] when the resource or world step fails; neither stage
|
||||||
|
/// publishes partial state on failure.
|
||||||
pub fn step_headless(
|
pub fn step_headless(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
input: InputSnapshot,
|
input: InputSnapshot,
|
||||||
|
queue_time_ms: u32,
|
||||||
) -> Result<FrameResult, EngineError> {
|
) -> Result<FrameResult, EngineError> {
|
||||||
Ok(FrameResult {
|
let resources = prepare_loaded_mission_resources(engine, queue_time_ms)?;
|
||||||
snapshot: step(&mut engine.world, &input)?,
|
let snapshot = step(&mut engine.world, &input)?;
|
||||||
})
|
if let Some(resources) = resources {
|
||||||
|
resources.apply(engine);
|
||||||
|
}
|
||||||
|
Ok(FrameResult { snapshot })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Steps rendered mode.
|
/// Steps rendered mode with the same queue-clock semantics as [`step_headless`].
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns [`EngineError`] when the world step fails.
|
/// Returns [`EngineError`] when the resource or world step fails.
|
||||||
pub fn frame(engine: &mut Engine) -> Result<FrameResult, EngineError> {
|
pub fn frame(engine: &mut Engine, queue_time_ms: u32) -> Result<FrameResult, EngineError> {
|
||||||
step_headless(engine, InputSnapshot)
|
step_headless(engine, InputSnapshot, queue_time_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the loaded mission summary.
|
/// Returns the loaded mission summary.
|
||||||
@@ -1962,9 +1971,65 @@ pub fn advance_loaded_mission_resources(
|
|||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
queue_time_ms: u32,
|
queue_time_ms: u32,
|
||||||
) -> Result<usize, EngineError> {
|
) -> Result<usize, EngineError> {
|
||||||
let Some(loaded) = engine.loaded.as_ref() else {
|
let Some(resources) = prepare_loaded_mission_resources(engine, queue_time_ms)? else {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
};
|
};
|
||||||
|
let scheduled_ticks = resources.scheduled_ticks;
|
||||||
|
resources.apply(engine);
|
||||||
|
Ok(scheduled_ticks)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreparedMissionResources {
|
||||||
|
distributors: Vec<BehaviorDistributorState>,
|
||||||
|
ore_states: Vec<BehaviorOreState>,
|
||||||
|
control_tables: Vec<MissionControlDeviceTableState>,
|
||||||
|
warm_starts: Vec<Option<BehaviorWarmStartState>>,
|
||||||
|
rng_state: u32,
|
||||||
|
scheduled_ticks: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreparedMissionResources {
|
||||||
|
fn apply(self, engine: &mut Engine) {
|
||||||
|
let loaded = engine
|
||||||
|
.loaded
|
||||||
|
.as_mut()
|
||||||
|
.expect("prepared resources require a loaded mission");
|
||||||
|
loaded.behavior_distributors = self.distributors;
|
||||||
|
loaded.behavior_ore_states = self.ore_states;
|
||||||
|
loaded.control_device_tables = self.control_tables;
|
||||||
|
for (bundle, warm_start) in loaded.script_bundles.iter_mut().zip(self.warm_starts) {
|
||||||
|
bundle.behavior_warm_start = warm_start;
|
||||||
|
}
|
||||||
|
engine.behavior_rng_state = self.rng_state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_loaded_mission_resources(
|
||||||
|
engine: &Engine,
|
||||||
|
queue_time_ms: u32,
|
||||||
|
) -> Result<Option<PreparedMissionResources>, EngineError> {
|
||||||
|
let Some(loaded) = engine.loaded.as_ref() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if loaded
|
||||||
|
.behavior_distributors
|
||||||
|
.iter()
|
||||||
|
.filter(|distributor| distributor.super_ai_visible)
|
||||||
|
.all(|distributor| {
|
||||||
|
loaded
|
||||||
|
.script_bundles
|
||||||
|
.iter()
|
||||||
|
.find(|bundle| bundle.clan_index == distributor.clan_index)
|
||||||
|
.and_then(|bundle| bundle.behavior_warm_start)
|
||||||
|
.is_some_and(|schedule| {
|
||||||
|
schedule.first_time_ms != 0
|
||||||
|
&& schedule.next_deadline_ms != 0
|
||||||
|
&& queue_time_ms <= schedule.next_deadline_ms
|
||||||
|
})
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
let mut distributors = loaded.behavior_distributors.clone();
|
let mut distributors = loaded.behavior_distributors.clone();
|
||||||
let mut ore_states = loaded.behavior_ore_states.clone();
|
let mut ore_states = loaded.behavior_ore_states.clone();
|
||||||
@@ -2023,18 +2088,14 @@ pub fn advance_loaded_mission_resources(
|
|||||||
schedule.cleared_words = totals;
|
schedule.cleared_words = totals;
|
||||||
}
|
}
|
||||||
|
|
||||||
let loaded = engine
|
Ok(Some(PreparedMissionResources {
|
||||||
.loaded
|
distributors,
|
||||||
.as_mut()
|
ore_states,
|
||||||
.expect("loaded mission was checked above");
|
control_tables,
|
||||||
loaded.behavior_distributors = distributors;
|
warm_starts,
|
||||||
loaded.behavior_ore_states = ore_states;
|
rng_state,
|
||||||
loaded.control_device_tables = control_tables;
|
scheduled_ticks,
|
||||||
for (bundle, warm_start) in loaded.script_bundles.iter_mut().zip(warm_starts) {
|
}))
|
||||||
bundle.behavior_warm_start = warm_start;
|
|
||||||
}
|
|
||||||
engine.behavior_rng_state = rng_state;
|
|
||||||
Ok(scheduled_ticks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets native Control global-table selector 1 on rows whose selector-0 value
|
/// Sets native Control global-table selector 1 on rows whose selector-0 value
|
||||||
@@ -5169,7 +5230,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn load_mission_requires_vfs_and_keeps_world_unchanged_on_error() {
|
fn load_mission_requires_vfs_and_keeps_world_unchanged_on_error() {
|
||||||
let mut engine = create(EngineServices::default()).expect("engine");
|
let mut engine = create(EngineServices::default()).expect("engine");
|
||||||
let before = step_headless(&mut engine, InputSnapshot).expect("step");
|
let before = step_headless(&mut engine, InputSnapshot, 0).expect("step");
|
||||||
let err = load_mission(
|
let err = load_mission(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
MissionRequest {
|
MissionRequest {
|
||||||
@@ -5180,7 +5241,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.expect_err("missing VFS");
|
.expect_err("missing VFS");
|
||||||
assert!(matches!(err, EngineError::MissingVfs));
|
assert!(matches!(err, EngineError::MissingVfs));
|
||||||
let after = step_headless(&mut engine, InputSnapshot).expect("step");
|
let after = step_headless(&mut engine, InputSnapshot, 0).expect("step");
|
||||||
assert_eq!(before.snapshot.objects, after.snapshot.objects);
|
assert_eq!(before.snapshot.objects, after.snapshot.objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6909,6 +6970,116 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires licensed corpus"]
|
||||||
|
fn headless_step_runs_resource_schedule_and_rolls_back_both_error_paths() {
|
||||||
|
let mut engine = create(EngineServices::new(Arc::new(DirectoryVfs::new(
|
||||||
|
licensed_root("IS"),
|
||||||
|
))))
|
||||||
|
.expect("engine");
|
||||||
|
load_mission(
|
||||||
|
&mut engine,
|
||||||
|
MissionRequest {
|
||||||
|
key: "MISSIONS/Autodemo.00/data.tma".into(),
|
||||||
|
mode: MissionMode::AutoDemo,
|
||||||
|
},
|
||||||
|
100_000,
|
||||||
|
)
|
||||||
|
.expect("licensed mission");
|
||||||
|
let schedules = |engine: &Engine| {
|
||||||
|
loaded_mission_script_bundles(engine)
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|bundle| bundle.behavior_warm_start)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
let initial = schedules(&engine);
|
||||||
|
frame(&mut engine, 100_200).expect("deadline equality");
|
||||||
|
assert_eq!(schedules(&engine), initial);
|
||||||
|
let result = step_headless(&mut engine, InputSnapshot, 100_201).expect("due clan 1");
|
||||||
|
assert_eq!(result.snapshot.tick.0, 2);
|
||||||
|
let after = schedules(&engine);
|
||||||
|
assert_eq!(after[0], initial[0]);
|
||||||
|
let updated = after[1].expect("clan 1 schedule");
|
||||||
|
assert_eq!(updated.first_time_ms, 100_201);
|
||||||
|
assert!(f32::from_bits(updated.cleared_words[0]) > 0.0);
|
||||||
|
|
||||||
|
let resources = |engine: &Engine| {
|
||||||
|
let state = engine.loaded.as_ref().unwrap();
|
||||||
|
(
|
||||||
|
state.behavior_distributors.clone(),
|
||||||
|
state.behavior_ore_states.clone(),
|
||||||
|
state.control_device_tables.clone(),
|
||||||
|
schedules(engine),
|
||||||
|
engine.behavior_rng_state,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let saved_controls = engine
|
||||||
|
.loaded
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.control_device_tables
|
||||||
|
.clone();
|
||||||
|
let plant = engine
|
||||||
|
.loaded
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.behavior_distributors
|
||||||
|
.iter()
|
||||||
|
.find(|distributor| distributor.clan_index == 1)
|
||||||
|
.unwrap()
|
||||||
|
.registered_objects
|
||||||
|
.iter()
|
||||||
|
.find(|object| object.class_flags == 0x8000_0010)
|
||||||
|
.unwrap()
|
||||||
|
.handle;
|
||||||
|
engine
|
||||||
|
.loaded
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.control_device_tables
|
||||||
|
.retain(|table| table.handle != plant);
|
||||||
|
let before = resources(&engine);
|
||||||
|
let world_before = fparkan_world::canonical_state_hash(&engine.world);
|
||||||
|
assert!(matches!(
|
||||||
|
step_headless(&mut engine, InputSnapshot, 100_500),
|
||||||
|
Err(EngineError::BehaviorDistributorTick { clan_index: 1, .. })
|
||||||
|
));
|
||||||
|
assert_eq!(resources(&engine), before);
|
||||||
|
assert_eq!(
|
||||||
|
fparkan_world::canonical_state_hash(&engine.world),
|
||||||
|
world_before
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.loaded.as_mut().unwrap().control_device_tables = saved_controls;
|
||||||
|
let stale = construct_object(&mut engine.world, ObjectDraft { original_id: None }).unwrap();
|
||||||
|
register_object(&mut engine.world, stale).unwrap();
|
||||||
|
fparkan_world::enqueue(
|
||||||
|
&mut engine.world,
|
||||||
|
fparkan_world::WorldCommand {
|
||||||
|
sequence: 0,
|
||||||
|
target: Some(stale),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fparkan_world::request_delete(&mut engine.world, stale).unwrap();
|
||||||
|
let before = resources(&engine);
|
||||||
|
let world_before = fparkan_world::canonical_state_hash(&engine.world);
|
||||||
|
assert!(matches!(
|
||||||
|
step_headless(&mut engine, InputSnapshot, 100_500),
|
||||||
|
Err(EngineError::World(_))
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
resources(&engine),
|
||||||
|
before,
|
||||||
|
"a failed world step must not publish staged resource ticks"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fparkan_world::canonical_state_hash(&engine.world),
|
||||||
|
world_before
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires licensed corpus"]
|
#[ignore = "requires licensed corpus"]
|
||||||
fn licensed_autodemo_resource_ticks_use_queue_time_and_real_generator_profile() {
|
fn licensed_autodemo_resource_ticks_use_queue_time_and_real_generator_profile() {
|
||||||
@@ -7714,7 +7885,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.expect("static preview does not require Behavior profile parsing");
|
.expect("static preview does not require Behavior profile parsing");
|
||||||
let previous_mission = loaded_mission(&engine).expect("loaded preview").clone();
|
let previous_mission = loaded_mission(&engine).expect("loaded preview").clone();
|
||||||
let before = step_headless(&mut engine, InputSnapshot)
|
let before = step_headless(&mut engine, InputSnapshot, 0)
|
||||||
.expect("step preview world")
|
.expect("step preview world")
|
||||||
.snapshot
|
.snapshot
|
||||||
.objects;
|
.objects;
|
||||||
@@ -7726,7 +7897,7 @@ mod tests {
|
|||||||
EngineError::Nres { ref path, .. } if path == "behpsp.res"
|
EngineError::Nres { ref path, .. } if path == "behpsp.res"
|
||||||
));
|
));
|
||||||
assert_eq!(loaded_mission(&engine), Some(&previous_mission));
|
assert_eq!(loaded_mission(&engine), Some(&previous_mission));
|
||||||
let after = step_headless(&mut engine, InputSnapshot)
|
let after = step_headless(&mut engine, InputSnapshot, 0)
|
||||||
.expect("step retained preview world")
|
.expect("step retained preview world")
|
||||||
.snapshot
|
.snapshot
|
||||||
.objects;
|
.objects;
|
||||||
@@ -7921,7 +8092,7 @@ mod tests {
|
|||||||
denied,
|
denied,
|
||||||
});
|
});
|
||||||
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
||||||
let before = step_headless(&mut engine, InputSnapshot).expect("before");
|
let before = step_headless(&mut engine, InputSnapshot, 0).expect("before");
|
||||||
let err = load_mission(&mut engine, mission, 0).expect_err("load error");
|
let err = load_mission(&mut engine, mission, 0).expect_err("load error");
|
||||||
match denied {
|
match denied {
|
||||||
DenyRule::Suffix("Land.map") => assert!(matches!(err, EngineError::Vfs { .. })),
|
DenyRule::Suffix("Land.map") => assert!(matches!(err, EngineError::Vfs { .. })),
|
||||||
@@ -7931,7 +8102,7 @@ mod tests {
|
|||||||
DenyRule::Suffix(unexpected) => panic!("unexpected deny rule {unexpected}"),
|
DenyRule::Suffix(unexpected) => panic!("unexpected deny rule {unexpected}"),
|
||||||
}
|
}
|
||||||
assert!(loaded_mission(&engine).is_none());
|
assert!(loaded_mission(&engine).is_none());
|
||||||
let after = step_headless(&mut engine, InputSnapshot).expect("after");
|
let after = step_headless(&mut engine, InputSnapshot, 0).expect("after");
|
||||||
assert_eq!(before.snapshot.objects, after.snapshot.objects);
|
assert_eq!(before.snapshot.objects, after.snapshot.objects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7942,7 +8113,7 @@ mod tests {
|
|||||||
let root = licensed_root("IS");
|
let root = licensed_root("IS");
|
||||||
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
||||||
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
||||||
let before = step_headless(&mut engine, InputSnapshot).expect("before");
|
let before = step_headless(&mut engine, InputSnapshot, 0).expect("before");
|
||||||
|
|
||||||
let err = load_mission_with_options(
|
let err = load_mission_with_options(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
@@ -7967,7 +8138,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
));
|
));
|
||||||
assert!(loaded_mission(&engine).is_none());
|
assert!(loaded_mission(&engine).is_none());
|
||||||
let after = step_headless(&mut engine, InputSnapshot).expect("after");
|
let after = step_headless(&mut engine, InputSnapshot, 0).expect("after");
|
||||||
assert_eq!(before.snapshot.objects, after.snapshot.objects);
|
assert_eq!(before.snapshot.objects, after.snapshot.objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8011,7 +8182,7 @@ mod tests {
|
|||||||
.expect("previous Behavior distributors")
|
.expect("previous Behavior distributors")
|
||||||
.to_vec();
|
.to_vec();
|
||||||
let previous_rng = engine.behavior_rng_state;
|
let previous_rng = engine.behavior_rng_state;
|
||||||
let before = step_headless(&mut engine, InputSnapshot)
|
let before = step_headless(&mut engine, InputSnapshot, 0)
|
||||||
.expect("step previous world")
|
.expect("step previous world")
|
||||||
.snapshot
|
.snapshot
|
||||||
.objects;
|
.objects;
|
||||||
@@ -8038,7 +8209,7 @@ mod tests {
|
|||||||
Some(previous_distributors.as_slice())
|
Some(previous_distributors.as_slice())
|
||||||
);
|
);
|
||||||
assert_eq!(engine.behavior_rng_state, previous_rng);
|
assert_eq!(engine.behavior_rng_state, previous_rng);
|
||||||
let after = step_headless(&mut engine, InputSnapshot)
|
let after = step_headless(&mut engine, InputSnapshot, 0)
|
||||||
.expect("step retained world")
|
.expect("step retained world")
|
||||||
.snapshot
|
.snapshot
|
||||||
.objects;
|
.objects;
|
||||||
@@ -8217,7 +8388,7 @@ mod tests {
|
|||||||
let mut snapshot = None;
|
let mut snapshot = None;
|
||||||
for _ in 0..10_000 {
|
for _ in 0..10_000 {
|
||||||
snapshot = Some(
|
snapshot = Some(
|
||||||
step_headless(&mut engine, InputSnapshot)
|
step_headless(&mut engine, InputSnapshot, 0)
|
||||||
.expect("selected mission deterministic tick")
|
.expect("selected mission deterministic tick")
|
||||||
.snapshot,
|
.snapshot,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -380,11 +380,14 @@ RVA `0xbcf0` для остальных. Это подтверждает стат
|
|||||||
|
|
||||||
Отдельный API `advance_loaded_mission_resources` принимает явное время очереди
|
Отдельный API `advance_loaded_mission_resources` принимает явное время очереди
|
||||||
и обновляет представленные поля `CurrentOre`, строк распределителя и Control
|
и обновляет представленные поля `CurrentOre`, строк распределителя и Control
|
||||||
selector `1`. API не подключён к игровому циклу и не обновляет Control amount,
|
selector `1`. Тот же расчёт вызывают `step_headless` и `frame` с явным
|
||||||
профили, special object-state gate или жизненный цикл объектов. Если обязательный
|
clock очереди; подготовка ресурсов и мировой шаг завершаются атомарно.
|
||||||
|
Оконное приложение пока использует static preview. Этот путь не обновляет
|
||||||
|
Control amount, профили или special object-state gate. Если обязательный
|
||||||
запрос позднего объекта не поддержан, транзакция возвращает ошибку без применения
|
запрос позднего объекта не поддержан, транзакция возвращает ошибку без применения
|
||||||
подготовленных изменений. AutoDemo-тест подтверждает связь настоящих профилей с
|
подготовленных изменений. AutoDemo-тест подтверждает связь настоящих профилей с
|
||||||
этим шагом, но не полный паритет миссии; отдельный aggregate getter Behavior RVA
|
этим шагом и проверяет откат при ошибке ресурсов или мировой очереди,
|
||||||
|
но не полный паритет миссии; отдельный aggregate getter Behavior RVA
|
||||||
`0xa490` в oracle не исполнен.
|
`0xa490` в oracle не исполнен.
|
||||||
|
|
||||||
Behavior selectors `0x1002` и `0x1005` используют значения выбранного
|
Behavior selectors `0x1002` и `0x1005` используют значения выбранного
|
||||||
|
|||||||
@@ -408,9 +408,11 @@ bundle, но остальные потребители исходного гло
|
|||||||
В Rust также нет значения аргумента в `AI+0x3fc`, recursion guard `AI+0x78`
|
В Rust также нет значения аргумента в `AI+0x3fc`, recursion guard `AI+0x78`
|
||||||
или одноразового latch `AI+0x8a8`. Поэтому это связанный начальный поднабор, а
|
или одноразового latch `AI+0x8a8`. Поэтому это связанный начальный поднабор, а
|
||||||
не полная активация SuperAI. После полной загрузки узкая Rust-реализация
|
не полная активация SuperAI. После полной загрузки узкая Rust-реализация
|
||||||
вызывается явно через `advance_loaded_mission_resources(engine, queue_time_ms)`;
|
вызывается из `step_headless(engine, input, queue_time_ms)` и
|
||||||
runtime не читает системную очередь, а приложение пока не вызывает API из
|
`frame(engine, queue_time_ms)`. Отдельный
|
||||||
игрового цикла. Шаг использует разрешённые handles и представленные snapshots
|
`advance_loaded_mission_resources(engine, queue_time_ms)` остаётся доступен
|
||||||
|
для прямого ресурсного шага. Runtime не читает системную очередь, а оконное
|
||||||
|
приложение пока использует static preview. Шаг использует разрешённые handles и представленные snapshots
|
||||||
профилей и Control, поэтому не воспроизводит отсутствующие в них callbacks. При
|
профилей и Control, поэтому не воспроизводит отсутствующие в них callbacks. При
|
||||||
ошибке ограниченного `Init` Rust отклоняет полную загрузку и сохраняет прежний
|
ошибке ограниченного `Init` Rust отклоняет полную загрузку и сохраняет прежний
|
||||||
мир и состояние генератора; такая политика отката принадлежит Rust loader-у.
|
мир и состояние генератора; такая политика отката принадлежит Rust loader-у.
|
||||||
@@ -479,8 +481,14 @@ selector `1` на интерфейсе получателя становится
|
|||||||
только для полного режима; preview остаётся статическим. Явный API
|
только для полного режима; preview остаётся статическим. Явный API
|
||||||
`advance_loaded_mission_resources` использует очередь времени, расписание,
|
`advance_loaded_mission_resources` использует очередь времени, расписание,
|
||||||
профили и представленные Control-таблицы, чтобы исполнить этот ограниченный
|
профили и представленные Control-таблицы, чтобы исполнить этот ограниченный
|
||||||
шаг над загруженным состоянием. `step_headless` по-прежнему шагает только
|
шаг над загруженным состоянием. `step_headless` и `frame` требуют явный
|
||||||
`World`.
|
`u32` clock и используют ту же подготовку: ресурсные изменения, расписания и
|
||||||
|
RNG остаются временными до успешного `World::step`. Ошибка ресурсов не
|
||||||
|
продвигает мир; ошибка мировой очереди не публикует ресурсные изменения.
|
||||||
|
Игнорируемый тест `headless_step_runs_resource_schedule_and_rolls_back_both_error_paths`
|
||||||
|
на AutoDemo проверяет равенство срока, исполнение следующего шага и обе ветви
|
||||||
|
отказа, включая отсутствие Control-таблицы у позднего клана и устаревший handle
|
||||||
|
в очереди. Это не запускает отдельный clock Control или полную миссионную VM.
|
||||||
|
|
||||||
### Добавление и нормализация долей
|
### Добавление и нормализация долей
|
||||||
|
|
||||||
|
|||||||
@@ -363,6 +363,13 @@ CollObject, Control и PhysicalModel, пропуская отсутствующ
|
|||||||
Статическая цепочка ещё не доказывает исполнение активного Control update на
|
Статическая цепочка ещё не доказывает исполнение активного Control update на
|
||||||
миссии. Rust пока сохраняет начальные provider-снимки и не изменяет их этим путём.
|
миссии. Rust пока сохраняет начальные provider-снимки и не изменяет их этим путём.
|
||||||
|
|
||||||
|
В текущем runtime `step_headless` и `frame` получают время очереди явным
|
||||||
|
аргументом `u32`. По нему запускается представленный ресурсный планировщик
|
||||||
|
Behavior, после чего обрабатываются очередь мира и отложенные удаления.
|
||||||
|
Ресурсные изменения публикуются после успешного мирового шага, поэтому ошибка
|
||||||
|
не оставляет мир и ресурсы на разных шагах. Это начало расчётного цикла;
|
||||||
|
отдельное обновление Control, общая VM и движение ещё требуют подключения.
|
||||||
|
|
||||||
## World3D
|
## World3D
|
||||||
|
|
||||||
World3D связывает игровые объекты, события, время, ввод, камеру, сетевые
|
World3D связывает игровые объекты, события, время, ввод, камеру, сетевые
|
||||||
|
|||||||
@@ -202,17 +202,17 @@ SuperAI для записей кланов, затем обновляет Behavi
|
|||||||
|
|
||||||
Это полезное начало сценария, но ещё не целая игровая жизнь AI. Текущий runtime
|
Это полезное начало сценария, но ещё не целая игровая жизнь AI. Текущий runtime
|
||||||
не воспроизводит аргумент обратного вызова в `AI+0x3fc`, защиту от повторного
|
не воспроизводит аргумент обратного вызова в `AI+0x3fc`, защиту от повторного
|
||||||
входа `AI+0x78`, одноразовую метку `AI+0x8a8` или обновления Behavior на игровых
|
входа `AI+0x78`, одноразовую метку `AI+0x8a8` или полное обновление Behavior на игровых
|
||||||
тиках. При этом один конкретный шаг Behavior — распределение ресурсов — уже
|
тиках. Распределение ресурсов исполнено на исходной DLL с искусственными
|
||||||
исполнен на исходной DLL с искусственными объектами. Rust пока не вызывает
|
объектами и подключено к Rust `step_headless`: явное время очереди определяет,
|
||||||
регулярный шаг: игровой мир ещё не предоставляет нужные ресурсные интерфейсы.
|
какой клан должен выполнить очередной ресурсный шаг.
|
||||||
При полной загрузке Rust уже восстанавливает членство объектов в клановых
|
При полной загрузке Rust уже восстанавливает членство объектов в клановых
|
||||||
списках Behavior и задаёт их начальные доли. Остальные слова записи хранятся
|
списках Behavior и задаёт их начальные доли. Остальные слова записи хранятся
|
||||||
сырыми: семантика только первого из пяти слов хвоста установлена для
|
сырыми: семантика только первого из пяти слов хвоста установлена для
|
||||||
нормализации.
|
нормализации.
|
||||||
Runtime также не подменяет очередь оригинала: вызывающая сторона сама передаёт
|
Runtime также не подменяет очередь оригинала: вызывающая сторона сама передаёт
|
||||||
время, а локальный генератор Rust пока учитывает только случайные числа
|
время, а локальный генератор Rust пока учитывает только случайные числа
|
||||||
восстановленного стартового шага.
|
восстановленного стартового шага и ресурсного планировщика.
|
||||||
|
|
||||||
### Откуда берутся объекты распределителя
|
### Откуда берутся объекты распределителя
|
||||||
|
|
||||||
@@ -355,9 +355,16 @@ Control selector `1`; неподдержанное свойство, запро
|
|||||||
арифметическое значение возвращает ошибку. Обновления нескольких кланов, их
|
арифметическое значение возвращает ошибку. Обновления нескольких кланов, их
|
||||||
расписания и RNG фиксируются одной транзакцией: если обязательный запрос
|
расписания и RNG фиксируются одной транзакцией: если обязательный запрос
|
||||||
позднего клана не разрешён, ранее подготовленные изменения остаются
|
позднего клана не разрешён, ранее подготовленные изменения остаются
|
||||||
неприменёнными. Приложение пока не вызывает
|
неприменёнными. Обычные `step_headless(engine, input, queue_time_ms)` и
|
||||||
этот API из игрового цикла, а динамические callbacks, которые переносят
|
`frame(engine, queue_time_ms)` запускают этот расчёт до обработки очереди мира.
|
||||||
количество между Control provider-строками, ещё не моделируются. Поэтому это
|
Они сначала готовят ресурсные изменения, затем выполняют шаг мира и публикуют
|
||||||
|
ресурсное состояние после его успеха. Ошибка любой стадии сохраняет прежние
|
||||||
|
ресурсы; ошибка подготовки также не продвигает мировой tick. До срока
|
||||||
|
планировщика большие таблицы ресурсов не копируются.
|
||||||
|
|
||||||
|
Оконное приложение пока запускает static preview и не вызывает этот расчётный
|
||||||
|
путь. Динамические callbacks, которые переносят количество между Control
|
||||||
|
provider-строками, ещё не моделируются. Поэтому это
|
||||||
исполняемый загруженный resource step, а не полный симулятор миссии.
|
исполняемый загруженный resource step, а не полный симулятор миссии.
|
||||||
|
|
||||||
Для настоящих ресурсов AutoDemo тест проходит несколько временных точек:
|
Для настоящих ресурсов AutoDemo тест проходит несколько временных точек:
|
||||||
|
|||||||
Reference in new issue
Block a user