feat(runtime): project factory-2 selector 6 metadata
This commit is contained in:
7 files changed
+1856
-29
No files matched your search
Generated
+1
@@ -639,6 +639,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"fparkan-animation",
|
"fparkan-animation",
|
||||||
"fparkan-assets",
|
"fparkan-assets",
|
||||||
|
"fparkan-fx",
|
||||||
"fparkan-msh",
|
"fparkan-msh",
|
||||||
"fparkan-path",
|
"fparkan-path",
|
||||||
"fparkan-platform",
|
"fparkan-platform",
|
||||||
|
|||||||
@@ -2112,6 +2112,33 @@ impl<R: ResourceRepository> AssetManager<R> {
|
|||||||
) -> Result<MissionAssetPlan, AssetError> {
|
) -> Result<MissionAssetPlan, AssetError> {
|
||||||
build_mission_asset_plan_with_repository(&self.repository, prototypes)
|
build_mission_asset_plan_with_repository(&self.repository, prototypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads raw opcodes from the validated initial Control effect group.
|
||||||
|
///
|
||||||
|
/// A complete no-effect section or an out-of-range raw initial-group
|
||||||
|
/// selector returns `Some(Vec::new())`; unsupported shapes return `None`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`AssetError`] when the Control resource is malformed.
|
||||||
|
pub fn initial_control_effect_opcodes(
|
||||||
|
&self,
|
||||||
|
source: &ResourceKey,
|
||||||
|
model_node_count: usize,
|
||||||
|
prototype_index: usize,
|
||||||
|
) -> Result<Option<Vec<u32>>, AssetError> {
|
||||||
|
let Some(payload) = read_optional_key(&self.repository, source, Some("component control"))?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let (_, opcodes) = decode_component_control_payload_with_opcodes(
|
||||||
|
&payload,
|
||||||
|
source.clone(),
|
||||||
|
model_node_count,
|
||||||
|
prototype_index,
|
||||||
|
)?;
|
||||||
|
Ok(opcodes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produces a count-only plan from a prototype graph.
|
/// Produces a count-only plan from a prototype graph.
|
||||||
@@ -3881,6 +3908,7 @@ const CONTROL_EFFECT_ROW_BYTES: usize = 100;
|
|||||||
struct DecodedComponentEffectSection {
|
struct DecodedComponentEffectSection {
|
||||||
section: PreparedControlEffectSection,
|
section: PreparedControlEffectSection,
|
||||||
initial_effect_group: Option<usize>,
|
initial_effect_group: Option<usize>,
|
||||||
|
initial_group_opcodes: Option<Vec<u32>>,
|
||||||
typed_records: Vec<PreparedControlTypedRecord>,
|
typed_records: Vec<PreparedControlTypedRecord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3905,6 +3933,7 @@ where
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
initial_effect_group: None,
|
initial_effect_group: None,
|
||||||
|
initial_group_opcodes: (cursor == payload.len()).then(Vec::new),
|
||||||
typed_records: Vec::new(),
|
typed_records: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3926,6 +3955,7 @@ where
|
|||||||
typed_record_tag: Some(tag),
|
typed_record_tag: Some(tag),
|
||||||
},
|
},
|
||||||
initial_effect_group: None,
|
initial_effect_group: None,
|
||||||
|
initial_group_opcodes: None,
|
||||||
typed_records,
|
typed_records,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4015,8 +4045,9 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
if effect_group_count == 0 {
|
if effect_group_count == 0 {
|
||||||
|
let complete = cursor == payload.len();
|
||||||
return Ok(DecodedComponentEffectSection {
|
return Ok(DecodedComponentEffectSection {
|
||||||
section: if cursor == payload.len() {
|
section: if complete {
|
||||||
PreparedControlEffectSection::Parsed(Vec::new())
|
PreparedControlEffectSection::Parsed(Vec::new())
|
||||||
} else {
|
} else {
|
||||||
PreparedControlEffectSection::Unsupported {
|
PreparedControlEffectSection::Unsupported {
|
||||||
@@ -4025,6 +4056,7 @@ where
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
initial_effect_group: None,
|
initial_effect_group: None,
|
||||||
|
initial_group_opcodes: complete.then(Vec::new),
|
||||||
typed_records,
|
typed_records,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4043,6 +4075,7 @@ where
|
|||||||
.filter(|group| *group < effect_group_count);
|
.filter(|group| *group < effect_group_count);
|
||||||
cursor = group_header_end;
|
cursor = group_header_end;
|
||||||
|
|
||||||
|
let mut initial_group_opcodes = Some(Vec::new());
|
||||||
let mut effect_rows = Vec::new();
|
let mut effect_rows = Vec::new();
|
||||||
for table_index in 0..effect_group_count {
|
for table_index in 0..effect_group_count {
|
||||||
let row_count = usize::try_from(
|
let row_count = usize::try_from(
|
||||||
@@ -4069,7 +4102,14 @@ where
|
|||||||
for row_index in 0..row_count {
|
for row_index in 0..row_count {
|
||||||
let row_start = rows_start + row_index * CONTROL_EFFECT_ROW_BYTES;
|
let row_start = rows_start + row_index * CONTROL_EFFECT_ROW_BYTES;
|
||||||
let row = &payload[row_start..row_start + CONTROL_EFFECT_ROW_BYTES];
|
let row = &payload[row_start..row_start + CONTROL_EFFECT_ROW_BYTES];
|
||||||
if control_u32_at(row, 0x0C) != Some(4) {
|
let opcode = control_u32_at(row, 0x0C).expect("fixed effect row has opcode");
|
||||||
|
if initial_effect_group == Some(table_index) {
|
||||||
|
initial_group_opcodes
|
||||||
|
.as_mut()
|
||||||
|
.expect("valid initial group has an opcode list")
|
||||||
|
.push(opcode);
|
||||||
|
}
|
||||||
|
if opcode != 4 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let raw_condition = [
|
let raw_condition = [
|
||||||
@@ -4108,12 +4148,14 @@ where
|
|||||||
typed_record_tag: None,
|
typed_record_tag: None,
|
||||||
},
|
},
|
||||||
initial_effect_group: None,
|
initial_effect_group: None,
|
||||||
|
initial_group_opcodes: None,
|
||||||
typed_records,
|
typed_records,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(DecodedComponentEffectSection {
|
Ok(DecodedComponentEffectSection {
|
||||||
section: PreparedControlEffectSection::Parsed(effect_rows),
|
section: PreparedControlEffectSection::Parsed(effect_rows),
|
||||||
initial_effect_group,
|
initial_effect_group,
|
||||||
|
initial_group_opcodes,
|
||||||
typed_records,
|
typed_records,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -4130,6 +4172,21 @@ fn decode_component_control_payload(
|
|||||||
model_node_count: usize,
|
model_node_count: usize,
|
||||||
prototype_index: usize,
|
prototype_index: usize,
|
||||||
) -> Result<PreparedComponentControl, AssetError> {
|
) -> Result<PreparedComponentControl, AssetError> {
|
||||||
|
decode_component_control_payload_with_opcodes(
|
||||||
|
payload,
|
||||||
|
source,
|
||||||
|
model_node_count,
|
||||||
|
prototype_index,
|
||||||
|
)
|
||||||
|
.map(|(control, _)| control)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_component_control_payload_with_opcodes(
|
||||||
|
payload: &[u8],
|
||||||
|
source: ResourceKey,
|
||||||
|
model_node_count: usize,
|
||||||
|
prototype_index: usize,
|
||||||
|
) -> Result<(PreparedComponentControl, Option<Vec<u32>>), AssetError> {
|
||||||
const HEADER_BYTES: usize = 5 * 4;
|
const HEADER_BYTES: usize = 5 * 4;
|
||||||
const FIXED_SECTION_BYTES: usize = 108;
|
const FIXED_SECTION_BYTES: usize = 108;
|
||||||
const SECTION_FIXED_BYTES: usize = 156;
|
const SECTION_FIXED_BYTES: usize = 156;
|
||||||
@@ -4267,18 +4324,22 @@ fn decode_component_control_payload(
|
|||||||
&invalid,
|
&invalid,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(PreparedComponentControl {
|
let initial_group_opcodes = decoded_effect.initial_group_opcodes;
|
||||||
source,
|
Ok((
|
||||||
native_profile_words,
|
PreparedComponentControl {
|
||||||
model_node_count,
|
source,
|
||||||
bindings,
|
native_profile_words,
|
||||||
timeline_rows,
|
model_node_count,
|
||||||
typed_records: decoded_effect.typed_records,
|
bindings,
|
||||||
node_data: None,
|
timeline_rows,
|
||||||
effect_section: decoded_effect.section,
|
typed_records: decoded_effect.typed_records,
|
||||||
initial_effect_group: decoded_effect.initial_effect_group,
|
node_data: None,
|
||||||
point_list: None,
|
effect_section: decoded_effect.section,
|
||||||
})
|
initial_effect_group: decoded_effect.initial_effect_group,
|
||||||
|
point_list: None,
|
||||||
|
},
|
||||||
|
initial_group_opcodes,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sibling_name(key: &ResourceKey, extension: &str) -> Result<ResourceName, AssetError> {
|
fn sibling_name(key: &ResourceKey, extension: &str) -> Result<ResourceName, AssetError> {
|
||||||
@@ -6803,6 +6864,138 @@ mod tests {
|
|||||||
assert_eq!(out_of_range.initial_effect_group, None);
|
assert_eq!(out_of_range.initial_effect_group, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_control_effect_opcodes_keep_raw_selected_rows_in_order() {
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").expect("archive"),
|
||||||
|
name: resource_name(b"parts.ctl"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let mut payload = component_control_payload(&[]);
|
||||||
|
payload[16..20].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
let group_header = payload.len();
|
||||||
|
payload.resize(group_header + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0);
|
||||||
|
let opcodes = [1, 4, 4, 12];
|
||||||
|
append_effect_table(
|
||||||
|
&mut payload,
|
||||||
|
&opcodes.map(|opcode| effect_row(opcode, [0; 3], [0; 3], 0, "", "")),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (control, decoded_opcodes) =
|
||||||
|
decode_component_control_payload_with_opcodes(&payload, source.clone(), 2, 0)
|
||||||
|
.expect("complete selected group");
|
||||||
|
assert_eq!(decoded_opcodes, Some(opcodes.to_vec()));
|
||||||
|
let PreparedControlEffectSection::Parsed(rows) = control.effect_section else {
|
||||||
|
panic!("opcode-4 projection should remain parsed");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
rows.iter().map(|row| row.row_index).collect::<Vec<_>>(),
|
||||||
|
[1, 2]
|
||||||
|
);
|
||||||
|
|
||||||
|
let repository = repository_with_archives(&[("controls.rlb", &[(b"parts.ctl", &payload)])]);
|
||||||
|
let manager = AssetManager::new(repository);
|
||||||
|
assert_eq!(
|
||||||
|
manager
|
||||||
|
.initial_control_effect_opcodes(&source, 2, 0)
|
||||||
|
.expect("public raw-opcode scanner"),
|
||||||
|
Some(opcodes.to_vec())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_control_effect_opcodes_distinguish_complete_empty_from_unknown_shapes() {
|
||||||
|
let source = |name: &[u8]| ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").expect("archive"),
|
||||||
|
name: resource_name(name),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let empty = component_control_payload(&[]);
|
||||||
|
|
||||||
|
let mut selected_noop = component_control_payload(&[]);
|
||||||
|
selected_noop[16..20].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
let noop_header = selected_noop.len();
|
||||||
|
selected_noop.resize(noop_header + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0);
|
||||||
|
append_effect_table(&mut selected_noop, &[]);
|
||||||
|
|
||||||
|
let mut unsupported = component_control_payload(&[]);
|
||||||
|
unsupported[12..16].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
append_typed_record(&mut unsupported, 99, 0);
|
||||||
|
|
||||||
|
let mut trailing_data = component_control_payload(&[]);
|
||||||
|
trailing_data.push(0xAA);
|
||||||
|
let repository = repository_with_archives(&[(
|
||||||
|
"controls.rlb",
|
||||||
|
&[
|
||||||
|
(b"empty.ctl", &empty),
|
||||||
|
(b"noop.ctl", &selected_noop),
|
||||||
|
(b"unsupported.ctl", &unsupported),
|
||||||
|
(b"trailing.ctl", &trailing_data),
|
||||||
|
],
|
||||||
|
)]);
|
||||||
|
let manager = AssetManager::new(repository);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
manager
|
||||||
|
.initial_control_effect_opcodes(&source(b"empty.ctl"), 2, 0)
|
||||||
|
.expect("complete empty CTLD"),
|
||||||
|
Some(Vec::new())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
manager
|
||||||
|
.initial_control_effect_opcodes(&source(b"noop.ctl"), 2, 0)
|
||||||
|
.expect("complete no-op selected group"),
|
||||||
|
Some(Vec::new())
|
||||||
|
);
|
||||||
|
for name in [b"unsupported.ctl".as_slice(), b"trailing.ctl".as_slice()] {
|
||||||
|
assert_eq!(
|
||||||
|
manager
|
||||||
|
.initial_control_effect_opcodes(&source(name), 2, 0)
|
||||||
|
.expect("unsupported but bounded shape"),
|
||||||
|
None,
|
||||||
|
"{} remains unavailable",
|
||||||
|
String::from_utf8_lossy(name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_control_effect_opcodes_treat_out_of_range_initial_indices_as_known_empty() {
|
||||||
|
let make_payload = |raw_initial_group: u32| {
|
||||||
|
let mut payload = component_control_payload(&[]);
|
||||||
|
payload[16..20].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
let group_header = payload.len();
|
||||||
|
payload.resize(group_header + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0);
|
||||||
|
payload[group_header..group_header + 4]
|
||||||
|
.copy_from_slice(&raw_initial_group.to_le_bytes());
|
||||||
|
append_effect_table(&mut payload, &[effect_row(1, [0; 3], [0; 3], 0, "", "")]);
|
||||||
|
payload
|
||||||
|
};
|
||||||
|
let sentinel = make_payload(u32::MAX);
|
||||||
|
let exact_count = make_payload(1);
|
||||||
|
let repository = repository_with_archives(&[(
|
||||||
|
"controls.rlb",
|
||||||
|
&[
|
||||||
|
(b"sentinel.ctl", &sentinel),
|
||||||
|
(b"exact-count.ctl", &exact_count),
|
||||||
|
],
|
||||||
|
)]);
|
||||||
|
let manager = AssetManager::new(repository);
|
||||||
|
let sources =
|
||||||
|
[b"sentinel.ctl".as_slice(), b"exact-count.ctl".as_slice()].map(|name| ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").expect("archive"),
|
||||||
|
name: resource_name(name),
|
||||||
|
type_id: None,
|
||||||
|
});
|
||||||
|
let results = sources.map(|source| {
|
||||||
|
manager
|
||||||
|
.initial_control_effect_opcodes(&source, 2, 0)
|
||||||
|
.expect("complete CTLD frames")
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(results, [Some(Vec::new()), Some(Vec::new())]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tag21_gate_provider_comes_from_validated_explicit_ndp() {
|
fn tag21_gate_provider_comes_from_validated_explicit_ndp() {
|
||||||
let source = ResourceKey {
|
let source = ResourceKey {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-assets = { path = "../fparkan-assets" }
|
fparkan-assets = { path = "../fparkan-assets" }
|
||||||
|
fparkan-fx = { path = "../fparkan-fx" }
|
||||||
fparkan-msh = { path = "../fparkan-msh" }
|
fparkan-msh = { path = "../fparkan-msh" }
|
||||||
fparkan-path = { path = "../fparkan-path" }
|
fparkan-path = { path = "../fparkan-path" }
|
||||||
fparkan-platform = { path = "../fparkan-platform" }
|
fparkan-platform = { path = "../fparkan-platform" }
|
||||||
|
|||||||
+1525
-14
File diff suppressed because it is too large.
Load diff
@@ -536,6 +536,41 @@ MSH-часть проверки подтверждает начальный сн
|
|||||||
остаётся статическим выводом по RVA `AniMesh.dll+0xa460`, а последующие
|
остаётся статическим выводом по RVA `AniMesh.dll+0xa460`, а последующие
|
||||||
изменения в полном Control/animation tick-е не проверялись.
|
изменения в полном Control/animation tick-е не проверялись.
|
||||||
|
|
||||||
|
Устройство фабрики `2` через selector `6` читает `+0x174` только при текущем descriptor tag `2`,
|
||||||
|
затем проверяет, что устройство создано фабрикой `2` и поле доступно. Значение
|
||||||
|
должно быть конечным нормальным числом или нулём любого знака. Строки в
|
||||||
|
`[27..35)` и `[35..43)` читаются до первого NUL: хвост игнорируется,
|
||||||
|
ASCII-регистр не важен. После изменения пустая строка оставляет старые биты;
|
||||||
|
пустая пара конструктора уже совпадает и не вызывает пересчёт. Проверка не
|
||||||
|
использует список имён: нужен один корневой mesh `BULL` из `ObjectsRegistry`,
|
||||||
|
без дочерних компонентов и записей timeline; все типизированные записи должны
|
||||||
|
иметь tag `17`, а эффекты и таблица NDP — разбираться полностью. Число строк
|
||||||
|
NDP произвольно, включая пустую таблицу.
|
||||||
|
|
||||||
|
Сканер возвращает коды выбранной начальной группы в исходном порядке. Допустимы
|
||||||
|
`1/4/12`. `Some([])` означает, что по полному разбору данных команд нет: группа
|
||||||
|
отсутствует, пуста или сырое беззнаковое значение индекса не меньше числа
|
||||||
|
групп. `None` означает неподдержанный либо неполный разбор; повреждённые данные
|
||||||
|
дают ошибку. Для выбранного opcode `4` нужны FXID-типы `2/3/4/5/7/8/9` и
|
||||||
|
допустимые CPT-смещения внутри компонента; типы `1/6/10` остаются недоступными.
|
||||||
|
Непредставленный ключ или число не превращаются в ноль. Если распознанный
|
||||||
|
ресурс повреждён, новая миссия завершается ошибкой до публикации, сохраняя
|
||||||
|
прежние мир и RNG.
|
||||||
|
|
||||||
|
Загрузка лицензированной AutoDemo и Rust-запрос selector `6` охватили 12
|
||||||
|
устройств и семь пар с битовыми значениями:
|
||||||
|
`bb_m_01=43f98000`, `bm_m_01=44228000`, `bl_l_01=436a0000`,
|
||||||
|
`br_b_01=44c80000`, `bf_b_01=443b8000`, `bl_m_01=443b4000`,
|
||||||
|
`bm_l_01=43340000`. Все семь значений совпали с замороженными арифметическими
|
||||||
|
векторами. Пятнадцать изолированных вызовов selector `35` оригинальной DLL
|
||||||
|
(включая семь однострочных BULL-таблиц) проверили арифметику при x87 control
|
||||||
|
word `0x007f`, но не создавали полный native World. Таблица Control-устройств
|
||||||
|
Rust по-прежнему строится при полной загрузке
|
||||||
|
Unit DAT. CPU-проекция не воспроизводит временное создание объектов оригинала и
|
||||||
|
его побочные эффекты FX, звука и RNG, размещение в World или следующий Control
|
||||||
|
tick. Полное размещение объектов, игровой tick, Handler 43 и другие
|
||||||
|
неподдержанные форматы остаются открытыми.
|
||||||
|
|
||||||
Для provider-строки Rust хранит amount `+0x08`, накопитель `+0x10`, обработанное
|
Для provider-строки Rust хранит amount `+0x08`, накопитель `+0x10`, обработанное
|
||||||
количество `+0x0c`, долю `+0x14`, стадию `+0x18` и flags `+0x28` раздельно.
|
количество `+0x0c`, долю `+0x14`, стадию `+0x18` и flags `+0x28` раздельно.
|
||||||
Инициализатор `Control.dll+0xf940` кладёт результат selector `3` в текущее и
|
Инициализатор `Control.dll+0xf940` кладёт результат selector `3` в текущее и
|
||||||
|
|||||||
@@ -1502,7 +1502,69 @@ RVA `0x2bb9a` загружает `device+0x174` как `f32`, сохраняет
|
|||||||
буфер и возвращает `AL=1`/`RET 8`; другие текущие теги возвращают false.
|
буфер и возвращает `AL=1`/`RET 8`; другие текущие теги возвращают false.
|
||||||
Наличие поля `+0x174` подтверждено для экземпляра фабрики `2`. INTO сохраняет
|
Наличие поля `+0x174` подтверждено для экземпляра фабрики `2`. INTO сохраняет
|
||||||
исходный тип экземпляра, поэтому проверка тега descriptor отдельно не доказывает
|
исходный тип экземпляра, поэтому проверка тега descriptor отдельно не доказывает
|
||||||
наличие этого поля у другой фабрики. В Rust selector `6` пока не поддержан.
|
наличие этого поля у другой фабрики. Rust selector `6` сначала проверяет
|
||||||
|
текущий descriptor tag: тег не `2` даёт `Ok(None)`; затем требуется известная
|
||||||
|
фабрика `2` и конечное нормальное число либо ноль любого знака в `+0x174`. В
|
||||||
|
`Control.dll` (SHA-256
|
||||||
|
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) поле
|
||||||
|
получает ноль в конструкторе по RVA `0x294c0`; чтение, сравнение пары строк и
|
||||||
|
пересчёт находятся по RVA `0x2bb40`, `0x29940` и `0x296f0`.
|
||||||
|
|
||||||
|
Typed record хранит две ASCII C-строки в words `[27..35)` и `[35..43)`. Строка
|
||||||
|
заканчивается на первом NUL: байты за ним игнорируются, а регистр при сравнении
|
||||||
|
не учитывается. Если новая, изменённая пара содержит пустую строку, native
|
||||||
|
recalc завершается до `CreateObject` и selector `35`, поэтому сохраняются
|
||||||
|
прежние биты. Пустая пара конструктора уже совпадает с текущей и вообще не
|
||||||
|
запускает recalc. Иначе append и первая активная запись `INTO` применяют
|
||||||
|
результат в исходном порядке. Для нераспознанной формы поле остаётся
|
||||||
|
недоступным; ошибка разбора распознанного, но повреждённого ресурса прерывает
|
||||||
|
новую загрузку до публикации.
|
||||||
|
|
||||||
|
В общем случае native selector 35 не равен сумме сырых Max:
|
||||||
|
`Control.dll` getter `0x0dc40` (selector 3) читает Max по `descriptor+4`, где
|
||||||
|
указатель descriptor лежит в начале runtime provider row, затем умножает его
|
||||||
|
на масштабы владельца `+0x660` и `+0x548`. Функция `0x13620` складывает выходы
|
||||||
|
в порядке строк с округлением `f32`; при `row+0x24 != 0xffffffff` добавляется
|
||||||
|
индексированный вклад. Конструктор `0xb240` задаёт runtime `row+0x24`
|
||||||
|
служебное значение `0xffffffff`. Только в свежей форме поддержанного BULL оба
|
||||||
|
масштаба равны `1`, а все `row+0x24` равны
|
||||||
|
`0xffffffff`, поэтому формула сводится к NDP Max сумме.
|
||||||
|
|
||||||
|
Пересчёт создаёт временный объект запросом `9` через `World3D.dll+0x7cb0`.
|
||||||
|
Загрузчик `Control.dll+0x32280` формирует для его корня входной контекст с
|
||||||
|
полем `+0xc8 = 0xffffffff`. Для разрешённых opcode `1` и `12`, а
|
||||||
|
также `4` только после FXID/CPT-проверки, подтверждённые callbacks сохраняют
|
||||||
|
входы суммы: `Control.dll` RVA `0x43d0` меняет только локальные слова; в
|
||||||
|
`AniMesh.dll` (`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`)
|
||||||
|
RVA `0x5500/0x93c0` меняют узлы и список модели, а `0x5320` не пишет её
|
||||||
|
масштабы. Это статическая граница свежего пути, не снимок живого native World
|
||||||
|
после всех creation callbacks.
|
||||||
|
|
||||||
|
Проверка opcode `4` читает только выбранные строки и принимает FXID-типы
|
||||||
|
`2/3/4/5/7/8/9` с распознанными ASCII-ссылками и CPT offsets внутри компонента;
|
||||||
|
типы `1/6/10`, отсутствующие ресурсы и непроверенные offsets оставляют поле
|
||||||
|
недоступным. В `Effect.dll` (SHA-256
|
||||||
|
`386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`) проверены
|
||||||
|
инициализаторы типов `2/3/4/5/7/8/9` по RVA `0x12d10/0x103b0/0x108c0/0x28a0/
|
||||||
|
0x1720/0x11230`; тип `9` разделяет путь типа `3`.
|
||||||
|
|
||||||
|
`AssetManager::initial_control_effect_opcodes` собирает исходный порядок кодов
|
||||||
|
выбранной начальной группы до фильтра opcode `4`. `Some([])` означает, что
|
||||||
|
по структуре данных команд нет: группа отсутствует, пуста или её беззнаковый
|
||||||
|
индекс не меньше числа групп. Условия отдельных строк этот читатель не
|
||||||
|
вычисляет. `None` означает неподдержанный либо
|
||||||
|
неполный формат, а повреждённая структура возвращает ошибку. Такая разница не
|
||||||
|
позволяет принять неизвестный разбор за пустую сумму.
|
||||||
|
|
||||||
|
Ресурсные callbacks в этой цепочке привязаны также к следующим образам.
|
||||||
|
Записи палитры и звуковые либо графические буферы имеют отдельное хранение;
|
||||||
|
проверенные начальные вызовы не меняют входы суммы NDP.
|
||||||
|
|
||||||
|
| Модуль | SHA-256 | Существенные RVA |
|
||||||
|
|---|---|---|
|
||||||
|
| `World3D.dll` | `17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e` | `0x7cb0` — создание; `0x4b10/0x4cb0` — текстуры и lightmap |
|
||||||
|
| `Ngi32.dll` | `bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5` | `0x11f80` — чтение ресурса; `0x11410` — счётчик палитры; `0xd620/0xe900` — звуковые буферы |
|
||||||
|
| `Terrain.dll` | `af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` | `0x166a0/0x17ac0` — получение указателя на числовое поле World |
|
||||||
|
|
||||||
FCA0 передаёт в owner query порядковый номер `i`; query `0x2bb40` находит
|
FCA0 передаёт в owner query порядковый номер `i`; query `0x2bb40` находит
|
||||||
устройство `i` и вызывает его reader. Уже reader `FUN_10021d00` получает provider
|
устройство `i` и вызывает его reader. Уже reader `FUN_10021d00` получает provider
|
||||||
|
|||||||
@@ -1442,6 +1442,30 @@ descriptor через `INTO`. Так разные части Control могут
|
|||||||
загруженной таблицы; обновление пока не подключено к игровым часам и не
|
загруженной таблицы; обновление пока не подключено к игровым часам и не
|
||||||
заменяет полный Control tick.
|
заменяет полный Control tick.
|
||||||
|
|
||||||
|
Устройство фабрики `2` имеет поле selector `6`, `+0x174`; конструктор задаёт ему
|
||||||
|
ноль. Позднее Control может пересчитать его по паре строк записи и отдельному
|
||||||
|
прототипу `BULL`. Только для поддержанной свежей формы одного корневого mesh
|
||||||
|
`BULL` из `objects.rlb`, без дочерних компонентов, его NDP-строки сводятся к
|
||||||
|
сумме Max; например, `[1.5, 2.25]`
|
||||||
|
превращается в `3.75` с округлением `f32` после каждого шага:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut total = 0.0_f32;
|
||||||
|
for row in ndp_rows {
|
||||||
|
total += row.max_volume;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Пустая таблица даёт ноль; физическую единицу этому числу не приписываем. Пара
|
||||||
|
сравнивается как две C-строки до первого NUL: хвост не учитывается, регистр не
|
||||||
|
важен. Если после изменения любая строка пуста, старое `+0x174` сохраняется;
|
||||||
|
пустая пара конструктора не вызывает пересчёт, потому что уже совпадает. Новые
|
||||||
|
значения применяются при добавлении и первой активной записи `INTO` в исходном
|
||||||
|
порядке. Непредставленные входы оставляют селектор недоступным, а ошибка чтения
|
||||||
|
узнаваемого, но повреждённого ресурса отменяет новую загрузку и сохраняет
|
||||||
|
прежние мир и RNG. Общая формула selector 35 и условия свежей формы разобраны в
|
||||||
|
[техническом приложении](../appendices/script-vm.md).
|
||||||
|
|
||||||
Отдельный путь объекта класса `4` умножает интервал на контактный коэффициент и
|
Отдельный путь объекта класса `4` умножает интервал на контактный коэффициент и
|
||||||
передаёт изменение строке. В проверенном примере коэффициент `1`, интервал `2`,
|
передаёт изменение строке. В проверенном примере коэффициент `1`, интервал `2`,
|
||||||
и значение строки уменьшается на `2`; этот коэффициент не является ставкой
|
и значение строки уменьшается на `2`; этот коэффициент не является ставкой
|
||||||
|
|||||||
Reference in new issue
Block a user