Expand raw save company-chairman analysis surfaces
This commit is contained in:
parent
a58680481f
commit
1525703cd1
5 changed files with 219 additions and 14 deletions
|
|
@ -38,7 +38,8 @@ company debt, and company track-laying capacity are grounded directly from save
|
|||
broader company finance/governance scalars and controller-kind reconstruction still remain
|
||||
conservative defaults until their raw lanes are pinned more strongly. The offline runtime analysis
|
||||
surface also now exposes `runtime inspect-save-company-chairman <save.gms>` for those remaining raw
|
||||
company/chairman scalar candidates. A checked-in
|
||||
company/chairman scalar candidates, including fixed-world chairman slot / role-gate context,
|
||||
explicit company dword candidate windows, and richer chairman qword cache views. A checked-in
|
||||
`EventEffects` export now exists too in
|
||||
`artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now
|
||||
exists beside it in `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`. Recovered
|
||||
|
|
|
|||
|
|
@ -83,9 +83,10 @@ pub use smp::{
|
|||
SmpRuntimeAnchorCycleBlock, SmpRuntimePostSpanHeaderCandidate, SmpRuntimePostSpanProbe,
|
||||
SmpRuntimeTrailerBlock, SmpSaveAnchorRunBlock, SmpSaveBootstrapBlock,
|
||||
SmpSaveChairmanRecordAnalysisEntry, SmpSaveCompanyChairmanAnalysisReport,
|
||||
SmpSaveCompanyRecordAnalysisEntry, SmpSaveLoadCandidateTableSummary, SmpSaveLoadSummary,
|
||||
SmpSaveScalarCandidate, SmpSaveTaggedCollectionHeaderProbe, SmpSecondaryVariantProbe,
|
||||
SmpSharedHeader, SmpSpecialConditionEntry, SmpSpecialConditionsProbe,
|
||||
SmpSaveCompanyRecordAnalysisEntry, SmpSaveDwordCandidate, SmpSaveLoadCandidateTableSummary,
|
||||
SmpSaveLoadSummary, SmpSaveScalarCandidate, SmpSaveTaggedCollectionHeaderProbe,
|
||||
SmpSaveWorldSelectionRoleAnalysis, SmpSaveWorldSelectionRoleAnalysisEntry,
|
||||
SmpSecondaryVariantProbe, SmpSharedHeader, SmpSpecialConditionEntry, SmpSpecialConditionsProbe,
|
||||
inspect_save_company_and_chairman_analysis_bytes,
|
||||
inspect_save_company_and_chairman_analysis_file, inspect_smp_bytes, inspect_smp_file,
|
||||
load_save_slice_file, load_save_slice_from_report,
|
||||
|
|
|
|||
|
|
@ -2185,9 +2185,41 @@ pub struct SmpLoadedChairmanProfileTable {
|
|||
pub struct SmpSaveScalarCandidate {
|
||||
pub relative_offset: usize,
|
||||
pub relative_offset_hex: String,
|
||||
pub raw_u64: u64,
|
||||
pub raw_u64_hex: String,
|
||||
pub value_i64: i64,
|
||||
pub value_f64: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SmpSaveDwordCandidate {
|
||||
pub label: String,
|
||||
pub relative_offset: usize,
|
||||
pub relative_offset_hex: String,
|
||||
pub raw_u32: u32,
|
||||
pub raw_u32_hex: String,
|
||||
pub value_i32: i32,
|
||||
pub value_f32: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SmpSaveWorldSelectionRoleAnalysisEntry {
|
||||
pub slot_index: usize,
|
||||
pub selector_byte: u8,
|
||||
pub selector_byte_hex: String,
|
||||
pub role_gate_byte: u8,
|
||||
pub role_gate_byte_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SmpSaveWorldSelectionRoleAnalysis {
|
||||
pub selected_company_id: u32,
|
||||
pub selected_chairman_profile_id: u32,
|
||||
pub campaign_override_flag: u8,
|
||||
pub campaign_override_flag_hex: String,
|
||||
pub chairman_slots: Vec<SmpSaveWorldSelectionRoleAnalysisEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SmpSaveCompanyRecordAnalysisEntry {
|
||||
pub company_id: u32,
|
||||
|
|
@ -2211,6 +2243,10 @@ pub struct SmpSaveCompanyRecordAnalysisEntry {
|
|||
pub linked_transit_latch: bool,
|
||||
pub merger_cooldown_year: u32,
|
||||
pub takeover_cooldown_year: u32,
|
||||
#[serde(default)]
|
||||
pub scalar_dword_candidates: Vec<SmpSaveDwordCandidate>,
|
||||
#[serde(default)]
|
||||
pub post_capacity_dword_candidates: Vec<SmpSaveDwordCandidate>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -2235,6 +2271,8 @@ pub struct SmpSaveCompanyChairmanAnalysisReport {
|
|||
#[serde(default)]
|
||||
pub selected_chairman_profile_id: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub world_selection_context: Option<SmpSaveWorldSelectionRoleAnalysis>,
|
||||
#[serde(default)]
|
||||
pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>,
|
||||
#[serde(default)]
|
||||
pub chairman_entries: Vec<SmpSaveChairmanRecordAnalysisEntry>,
|
||||
|
|
@ -2758,6 +2796,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
|
|||
report: &SmpInspectionReport,
|
||||
) -> Option<SmpSaveCompanyChairmanAnalysisReport> {
|
||||
let selection_probe = report.save_world_selection_context_probe.as_ref();
|
||||
let world_selection_context = selection_probe.map(build_save_world_selection_role_analysis);
|
||||
let company_header_probe = report.save_company_collection_header_probe.as_ref();
|
||||
let chairman_header_probe = report
|
||||
.save_chairman_profile_collection_header_probe
|
||||
|
|
@ -2840,6 +2879,18 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
|
|||
&bytes,
|
||||
record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET,
|
||||
)?;
|
||||
let scalar_dword_candidates = SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS
|
||||
.iter()
|
||||
.map(|(label, relative_offset)| {
|
||||
build_save_dword_candidate(&bytes, record_offset, label, *relative_offset)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
let post_capacity_dword_candidates = SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS
|
||||
.iter()
|
||||
.map(|(label, relative_offset)| {
|
||||
build_save_dword_candidate(&bytes, record_offset, label, *relative_offset)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
entries.push(SmpSaveCompanyRecordAnalysisEntry {
|
||||
company_id,
|
||||
name,
|
||||
|
|
@ -2860,6 +2911,8 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
|
|||
linked_transit_latch,
|
||||
merger_cooldown_year,
|
||||
takeover_cooldown_year,
|
||||
scalar_dword_candidates,
|
||||
post_capacity_dword_candidates,
|
||||
});
|
||||
}
|
||||
entries
|
||||
|
|
@ -2907,12 +2960,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
|
|||
let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS
|
||||
.iter()
|
||||
.map(|relative_offset| {
|
||||
let value_f64 = read_f64_at(&bytes, record_offset + relative_offset)?;
|
||||
Some(SmpSaveScalarCandidate {
|
||||
relative_offset: *relative_offset,
|
||||
relative_offset_hex: format!("0x{relative_offset:x}"),
|
||||
value_f64,
|
||||
})
|
||||
build_save_qword_candidate(&bytes, record_offset, *relative_offset)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
entries.push(SmpSaveChairmanRecordAnalysisEntry {
|
||||
|
|
@ -2931,6 +2979,12 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
|
|||
};
|
||||
|
||||
let mut notes = Vec::new();
|
||||
if world_selection_context.is_some() {
|
||||
notes.push(
|
||||
"World selection context now exports the grounded chairman-slot selector bytes and per-slot role-gate bytes from the fixed save-side 0x32c8 world block."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if !company_entries.is_empty() {
|
||||
notes.push(
|
||||
"Company debt is derived from the grounded bond table at [company+0x5b/+0x5f] by summing live principal slots.".to_string(),
|
||||
|
|
@ -2938,10 +2992,13 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
|
|||
notes.push(
|
||||
"Company available_track_laying_capacity is derived from the grounded tail dword [company+0x7680], with negative values treated as the unlimited sentinel.".to_string(),
|
||||
);
|
||||
notes.push(
|
||||
"Company scalar_dword_candidates expose the current checked-in raw save windows around support/share-price/calendar lanes, and post_capacity_dword_candidates expose the immediate dwords after [company+0x7680] for deeper track-count and record-tail analysis.".to_string(),
|
||||
);
|
||||
}
|
||||
if !chairman_entries.is_empty() {
|
||||
notes.push(
|
||||
"Chairman cached_scalar_candidates expose the adjacent qword band rooted at [profile+0x1e9] for further purchasing-power analysis.".to_string(),
|
||||
"Chairman cached_scalar_candidates expose the adjacent qword band rooted at [profile+0x1e9], now including raw qword hex and signed/f64 views for further purchasing-power analysis.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2954,6 +3011,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
|
|||
selected_company_id: selection_probe.map(|probe| probe.selected_company_id),
|
||||
selected_chairman_profile_id: selection_probe
|
||||
.map(|probe| probe.selected_chairman_profile_id),
|
||||
world_selection_context,
|
||||
company_entries,
|
||||
chairman_entries,
|
||||
notes,
|
||||
|
|
@ -3089,6 +3147,23 @@ const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET: usize = 0x0d59;
|
|||
const SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56;
|
||||
const SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET: usize = 0x0d7b;
|
||||
const SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET: usize = 0x7680;
|
||||
const SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS: [(&str, usize); 7] = [
|
||||
("mutable_support_scalar", 0x4f),
|
||||
("young_company_support_scalar", 0x57),
|
||||
("support_progress_word", 0x0d07),
|
||||
("recent_per_share_subscore", 0x0d19),
|
||||
("cached_share_price", 0x0d7b),
|
||||
("current_issue_calendar_word", 0x16b),
|
||||
("prior_issue_calendar_word", 0x173),
|
||||
];
|
||||
const SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS: [(&str, usize); 6] = [
|
||||
("post_capacity_word_1", 0x7684),
|
||||
("post_capacity_word_2", 0x7688),
|
||||
("post_capacity_word_3", 0x768c),
|
||||
("post_capacity_word_4", 0x7690),
|
||||
("post_capacity_word_5", 0x7694),
|
||||
("post_capacity_word_6", 0x7698),
|
||||
];
|
||||
const SAVE_COMPANY_RECORD_START_SCAN_LIMIT: usize = 0x120;
|
||||
|
||||
const SAVE_CHAIRMAN_RECORD_NAME_OFFSET: usize = 0x08;
|
||||
|
|
@ -3296,6 +3371,69 @@ fn parse_save_company_available_track_laying_capacity(
|
|||
}
|
||||
}
|
||||
|
||||
fn build_save_dword_candidate(
|
||||
bytes: &[u8],
|
||||
record_offset: usize,
|
||||
label: &str,
|
||||
relative_offset: usize,
|
||||
) -> Option<SmpSaveDwordCandidate> {
|
||||
let raw_u32 = read_u32_at(bytes, record_offset + relative_offset)?;
|
||||
Some(SmpSaveDwordCandidate {
|
||||
label: label.to_string(),
|
||||
relative_offset,
|
||||
relative_offset_hex: format!("0x{relative_offset:x}"),
|
||||
raw_u32,
|
||||
raw_u32_hex: format!("0x{raw_u32:08x}"),
|
||||
value_i32: raw_u32 as i32,
|
||||
value_f32: f32::from_bits(raw_u32),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_save_qword_candidate(
|
||||
bytes: &[u8],
|
||||
record_offset: usize,
|
||||
relative_offset: usize,
|
||||
) -> Option<SmpSaveScalarCandidate> {
|
||||
let raw_u64 = read_u64_at(bytes, record_offset + relative_offset)?;
|
||||
Some(SmpSaveScalarCandidate {
|
||||
relative_offset,
|
||||
relative_offset_hex: format!("0x{relative_offset:x}"),
|
||||
raw_u64,
|
||||
raw_u64_hex: format!("0x{raw_u64:016x}"),
|
||||
value_i64: raw_u64 as i64,
|
||||
value_f64: f64::from_bits(raw_u64),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_save_world_selection_role_analysis(
|
||||
probe: &SmpSaveWorldSelectionContextProbe,
|
||||
) -> SmpSaveWorldSelectionRoleAnalysis {
|
||||
let chairman_slots = probe
|
||||
.chairman_slot_selectors
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(probe.chairman_role_gate_bytes.iter().copied())
|
||||
.enumerate()
|
||||
.map(|(slot_index, (selector_byte, role_gate_byte))| {
|
||||
SmpSaveWorldSelectionRoleAnalysisEntry {
|
||||
slot_index,
|
||||
selector_byte,
|
||||
selector_byte_hex: format!("0x{selector_byte:02x}"),
|
||||
role_gate_byte,
|
||||
role_gate_byte_hex: format!("0x{role_gate_byte:02x}"),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
SmpSaveWorldSelectionRoleAnalysis {
|
||||
selected_company_id: probe.selected_company_id,
|
||||
selected_chairman_profile_id: probe.selected_chairman_profile_id,
|
||||
campaign_override_flag: probe.campaign_override_flag,
|
||||
campaign_override_flag_hex: probe.campaign_override_flag_hex.clone(),
|
||||
chairman_slots,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_save_chairman_profile_table_probe(
|
||||
bytes: &[u8],
|
||||
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
||||
|
|
@ -9986,6 +10124,13 @@ fn read_i64_at(bytes: &[u8], offset: usize) -> Option<i64> {
|
|||
]))
|
||||
}
|
||||
|
||||
fn read_u64_at(bytes: &[u8], offset: usize) -> Option<u64> {
|
||||
let chunk = bytes.get(offset..offset + 8)?;
|
||||
Some(u64::from_le_bytes([
|
||||
chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
|
||||
]))
|
||||
}
|
||||
|
||||
fn read_f32_at(bytes: &[u8], offset: usize) -> Option<f32> {
|
||||
let chunk = bytes.get(offset..offset + 4)?;
|
||||
Some(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
||||
|
|
@ -14949,6 +15094,61 @@ mod tests {
|
|||
assert_eq!(table.entries[1].holdings_value_total, 822000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_save_world_selection_role_analysis_from_probe() {
|
||||
let probe = SmpSaveWorldSelectionContextProbe {
|
||||
profile_family: "rt3-105-save-container-v1".to_string(),
|
||||
source_kind: "save-direct-world-block".to_string(),
|
||||
semantic_family: "scenario-selected-company-and-chairman-context".to_string(),
|
||||
chunk_tag_offset: 0,
|
||||
payload_offset: 0,
|
||||
payload_len: 0x4f2c,
|
||||
payload_len_hex: "0x4f2c".to_string(),
|
||||
selected_company_id_offset: 0x21,
|
||||
selected_company_id: 3,
|
||||
selected_company_id_hex: "0x00000003".to_string(),
|
||||
selected_chairman_profile_id_offset: 0x25,
|
||||
selected_chairman_profile_id: 7,
|
||||
selected_chairman_profile_id_hex: "0x00000007".to_string(),
|
||||
chairman_slot_selector_offset: 0x87,
|
||||
chairman_slot_selectors: vec![1, 0, 2, 0],
|
||||
campaign_override_flag_offset: 0xc5,
|
||||
campaign_override_flag: 1,
|
||||
campaign_override_flag_hex: "0x01".to_string(),
|
||||
chairman_role_gate_offset: 0x0bc3,
|
||||
chairman_role_gate_bytes: vec![2, 0, 1, 0],
|
||||
evidence: vec![],
|
||||
};
|
||||
|
||||
let analysis = build_save_world_selection_role_analysis(&probe);
|
||||
|
||||
assert_eq!(analysis.selected_company_id, 3);
|
||||
assert_eq!(analysis.selected_chairman_profile_id, 7);
|
||||
assert_eq!(analysis.campaign_override_flag_hex, "0x01");
|
||||
assert_eq!(analysis.chairman_slots.len(), 4);
|
||||
assert_eq!(analysis.chairman_slots[0].selector_byte_hex, "0x01");
|
||||
assert_eq!(analysis.chairman_slots[2].role_gate_byte_hex, "0x01");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_save_candidate_views_with_raw_bits() {
|
||||
let mut bytes = vec![0u8; 0x40];
|
||||
bytes[0x08..0x0c].copy_from_slice(&0x3f800000u32.to_le_bytes());
|
||||
bytes[0x10..0x18].copy_from_slice(&(-2458.0f64).to_le_bytes());
|
||||
|
||||
let dword = build_save_dword_candidate(&bytes, 0, "unit_float", 0x08)
|
||||
.expect("dword candidate should build");
|
||||
let qword =
|
||||
build_save_qword_candidate(&bytes, 0, 0x10).expect("qword candidate should build");
|
||||
|
||||
assert_eq!(dword.raw_u32_hex, "0x3f800000");
|
||||
assert_eq!(dword.value_i32, 1_065_353_216);
|
||||
assert_eq!(dword.value_f32, 1.0);
|
||||
assert_eq!(qword.raw_u64, (-2458.0f64).to_bits());
|
||||
assert_eq!(qword.value_i64, (-2458.0f64).to_bits() as i64);
|
||||
assert_eq!(qword.value_f64, -2458.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_rt3_105_post_span_bridge_variants() {
|
||||
let base_trailer = SmpRuntimeTrailerBlock {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ The highest-value next passes are now:
|
|||
finance/governance scalar lanes plus controller-kind reconstruction still remain conservative
|
||||
defaults until their raw offsets are pinned more strongly; the offline analysis command
|
||||
`runtime inspect-save-company-chairman <save.gms>` now dumps those remaining raw record
|
||||
candidates directly from the rehosted parser
|
||||
candidates directly from the rehosted parser, including fixed-world chairman slot / role-gate
|
||||
context, company dword candidate windows, and richer chairman qword cache views
|
||||
- a checked-in `EventEffects` export now exists at
|
||||
`artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer
|
||||
now exists at `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`
|
||||
|
|
|
|||
|
|
@ -65,8 +65,10 @@ Implemented today:
|
|||
collections now provide save-native roster entries and `observed_entry_count`; raw company debt
|
||||
from the bond table and raw company track-laying capacity from the record tail are grounded too,
|
||||
and `runtime inspect-save-company-chairman <save.gms>` now exposes the remaining raw
|
||||
company/chairman scalar candidates directly from the rehosted parser; the remaining raw-save
|
||||
boundary is company-finance/governance scalar depth plus controller-kind closure, not roster absence
|
||||
company/chairman scalar candidates directly from the rehosted parser, including fixed-world
|
||||
chairman slot / role-gate context, company dword candidate windows, and richer chairman qword
|
||||
cache views; the remaining raw-save boundary is company-finance/governance scalar depth plus
|
||||
controller-kind closure, not roster absence
|
||||
- a checked-in `EventEffects` export now exists too at
|
||||
`artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer
|
||||
now exists at `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue