Browse Source

Implement techblox game save deserialization for all blocks available in-game

tags/v0.5.1
NGnius (Graham) 2 years ago
parent
commit
51a7710e9f
15 changed files with 494 additions and 7 deletions
  1. +1
    -1
      .gitignore
  2. +39
    -0
      src/techblox/blocks/engine.rs
  3. +24
    -0
      src/techblox/blocks/joint.rs
  4. +86
    -2
      src/techblox/blocks/lookup_tables.rs
  5. +15
    -0
      src/techblox/blocks/mod.rs
  6. +28
    -0
      src/techblox/blocks/passenger_seat.rs
  7. +36
    -0
      src/techblox/blocks/pilot_seat.rs
  8. +90
    -0
      src/techblox/blocks/spring.rs
  9. +24
    -0
      src/techblox/blocks/tyre.rs
  10. +74
    -0
      src/techblox/blocks/wheel_rig.rs
  11. +12
    -1
      src/techblox/entity_header.rs
  12. +2
    -0
      src/techblox/gamesave.rs
  13. +1
    -0
      test.sh
  14. BIN
      tests/All.Techblox
  15. +62
    -3
      tests/techblox_parsing.rs

+ 1
- 1
.gitignore View File

@@ -3,4 +3,4 @@ Cargo.lock
/.idea
/parsable_macro_derive/target
/tests/test-*.obj
/tests/GameSave2.Techblox
/tests/*.out.Techblox

+ 39
- 0
src/techblox/blocks/engine.rs View File

@@ -0,0 +1,39 @@
use crate::techblox::{SerializedEntityDescriptor, Parsable, SerializedEntityComponent,
blocks::{BlockEntity}};
use libfj_parsable_macro_derive::*;

/// Engine entity descriptor
#[derive(Copy, Clone, Parsable)]
pub struct EngineBlockEntity {
/// parent block entity
pub block: BlockEntity,
/// Engine tweakables component
pub tweak_component: EngineBlockTweakableComponent,
}

impl SerializedEntityDescriptor for EngineBlockEntity {
fn serialized_components() -> u8 {
BlockEntity::serialized_components() + 1
}

fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> {
let mut c = self.block.components();
c.push(&self.tweak_component);
return c;
}

fn hash_name(&self) -> u32 {
Self::hash("EngineBlockEntityDescriptor") // 1757314505
}
}

/// Engine settings entity component.
#[derive(Copy, Clone, Parsable)]
pub struct EngineBlockTweakableComponent {
/// Engine power (percent?)
pub power: f32,
/// Is the engine's transmission automatic? (bool)
pub automatic_gears: u32, // why is this not stored as u8 like the other bools?
}

impl SerializedEntityComponent for EngineBlockTweakableComponent {}

+ 24
- 0
src/techblox/blocks/joint.rs View File

@@ -0,0 +1,24 @@
use crate::techblox::{SerializedEntityDescriptor, Parsable, SerializedEntityComponent,
blocks::{BlockEntity}};
use libfj_parsable_macro_derive::*;

/// Joint block entity descriptor
#[derive(Copy, Clone, Parsable)]
pub struct JointBlockEntity {
/// parent block entity
pub block: BlockEntity,
}

impl SerializedEntityDescriptor for JointBlockEntity {
fn serialized_components() -> u8 {
BlockEntity::serialized_components()
}

fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> {
self.block.components()
}

fn hash_name(&self) -> u32 {
Self::hash("JointBlockEntityDescriptorV3") // 3586818581
}
}

+ 86
- 2
src/techblox/blocks/lookup_tables.rs View File

@@ -1,16 +1,100 @@
use std::io::Read;

use crate::techblox::{Parsable, SerializedEntityDescriptor};
#[cfg(debug_assertions)]
use crate::techblox::blocks::*;

const HASHNAMES: &[&str] = &[
// Block group info entities
"BlockGroupEntityDescriptorV0",
// Block entities
"StandardBlockEntityDescriptorV4",
"BatteryEntityDescriptorV4",
"MotorEntityDescriptorV7",
"LeverEntityDescriptorV7",
"ButtonEntityDescriptorV6",
"JointBlockEntityDescriptorV3",
"ServoEntityDescriptorV7",
"PistonEntityDescriptorV6",
"DampedSpringEntityDescriptorV5",
"DampedAngularSpringEntityDescriptorV4",
"SpawnPointEntityDescriptorV6",
"BuildingSpawnPointEntityDescriptorV4",
"TriggerEntityDescriptorV6",
"PilotSeatEntityDescriptorV4",
"PilotSeatEntityDescriptorV3",
"TextBlockEntityDescriptorV4",
"PassengerSeatEntityDescriptorV4",
"PassengerSeatEntityDescriptorV3",
"LogicBlockEntityDescriptorV1",
"TyreEntityDescriptorV1",
"ObjectIDEntityDescriptorV1",
"MoverEntityDescriptorV1",
"RotatorEntityDescriptorV1",
"DamperEntityDescriptorV1",
"AdvancedDamperEntityDescriptorV1",
"CoMEntityDescriptor",
"FilterBlockEntityDescriptorV1",
"ConstrainerEntityDescriptorV1",
"NumberToTextBlockEntityDescriptorV1",
"CentreHudBlockEntityDescriptorV1",
"ObjectiveHudBlockEntityDescriptorV1",
"GameStatsHudBlockEntityDescriptorV1",
"GameOverHudBlockEntityDescriptorV1",
"TimerBlockEntityDescriptorV1",
"BitBlockEntityDescriptorV2",
"ConstantBlockEntityDescriptor",
"CounterBlockEntityDescriptorV1",
"SimpleSfxEntityDescriptorV1",
"LoopedSfxEntityDescriptorV1",
"MusicBlockEntityDescriptorV1",
"ProjectileBlockEntityDescriptorV1",
"DamagingSurfaceEntityDescriptorV1",
"DestructionManagerEntityDescriptorV1",
"ChunkDestructionBlockEntityDescriptorV1",
"ClusterDestructionBlockEntityDescriptorV1",
"PickupBlockEntityDescriptorV1",
"PointLightEntityDescriptorV1",
"SpotLightEntityDescriptorV1",
"SunLightEntityDescriptorV1",
"AmbientLightEntityDescriptorV1",
"FogEntityDescriptorV1",
"SkyEntityDescriptorV1",
"SynchronizedWireBlockEntityDescriptor",
"WheelRigEntityDescriptor",
"WheelRigSteerableEntityDescriptor",
"EngineBlockEntityDescriptor",
// Other Non-block entities (stored after blocks in game saves)
"WireEntityDescriptorMock",
"GlobalWireSettingsEntityDescriptor",
"FlyCamEntityDescriptorV0",
"CharacterCameraEntityDescriptorV1",
];

pub fn lookup_hashname(hash: u32, data: &mut dyn Read) -> std::io::Result<Box<dyn SerializedEntityDescriptor>> {
Ok(match hash {
1357220432 /*StandardBlockEntityDescriptorV4*/ => Box::new(BlockEntity::parse(data)?),
2281299333 /*PilotSeatEntityDescriptorV4*/ => Box::new(PilotSeatEntity::parse(data)?),
1360086092 /*PassengerSeatEntityDescriptorV4*/ => Box::new(PassengerSeatEntity::parse(data)?),
1757314505 /*EngineBlockEntityDescriptor*/ => Box::new(EngineBlockEntity::parse(data)?),
3586818581 /*JointBlockEntityDescriptorV3*/ => Box::new(JointBlockEntity::parse(data)?),
3789998433 /*DampedAngularSpringEntityDescriptorV4*/ => Box::new(DampedAngularSpringEntity::parse(data)?),
2892049599 /*DampedSpringEntityDescriptorV5*/ => Box::new(DampedSpringEntity::parse(data)?),
1156723746 /*WheelRigEntityDescriptor*/ => Box::new(WheelRigEntity::parse(data)?),
1864425618 /*WheelRigSteerableEntityDescriptor*/ => Box::new(WheelRigSteerableEntity::parse(data)?),
1517625162 /*TyreEntityDescriptorV1*/ => Box::new(TyreEntity::parse(data)?),
_ => {
#[cfg(debug_assertions)]
println!("Unknown hash ID {}", hash);
println!("Unknown hash ID {} (missing entry for {})", hash, lookup_name_by_hash(hash).unwrap_or("<Unknown>"));
return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Unrecognised hash {}", hash)))
}
})
}

pub fn lookup_name_by_hash(hash: u32) -> Option<&'static str> {
for name in HASHNAMES {
if crate::techblox::hashname(name) == hash {
return Some(name);
}
}
None
}

+ 15
- 0
src/techblox/blocks/mod.rs View File

@@ -2,7 +2,14 @@

mod block_entity;
mod common_components;
mod engine;
mod joint;
mod lookup_tables;
mod pilot_seat;
mod passenger_seat;
mod spring;
mod tyre;
mod wheel_rig;
mod wire_entity;

pub use block_entity::{BlockEntity};
@@ -10,5 +17,13 @@ pub use common_components::{DBEntityStruct, PositionEntityStruct, ScalingEntityS
SkewComponent, GridRotationStruct, SerializedGridConnectionsEntityStruct, SerializedBlockPlacementInfoStruct,
SerializedCubeMaterialStruct, SerializedUniformBlockScaleEntityStruct, SerializedColourParameterEntityStruct,
BlockGroupEntityComponent};
pub use engine::{EngineBlockEntity, EngineBlockTweakableComponent};
pub use joint::{JointBlockEntity};
pub use pilot_seat::{PilotSeatEntity, SeatFollowCamComponent};
pub use passenger_seat::PassengerSeatEntity;
pub(crate) use lookup_tables::*;
pub use spring::{DampedAngularSpringEntity, TweakableJointDampingComponent, DampedAngularSpringROStruct,
DampedSpringEntity, DampedSpringROStruct};
pub use tyre::{TyreEntity};
pub use wheel_rig::{WheelRigEntity, WheelRigTweakableStruct, WheelRigSteerableEntity, WheelRigSteerableTweakableStruct};
pub use wire_entity::{SerializedWireEntity, WireSaveDataStruct, SerializedGlobalWireSettingsEntity, GlobalWireSettingsEntityStruct};

+ 28
- 0
src/techblox/blocks/passenger_seat.rs View File

@@ -0,0 +1,28 @@
use crate::techblox::{SerializedEntityDescriptor, Parsable, SerializedEntityComponent,
blocks::{BlockEntity, SeatFollowCamComponent}};
use libfj_parsable_macro_derive::*;

/// Passenger seat entity descriptor (V4)
#[derive(Copy, Clone, Parsable)]
pub struct PassengerSeatEntity {
/// parent block entity
pub block: BlockEntity,
/// Seat following camera component
pub cam_component: SeatFollowCamComponent,
}

impl SerializedEntityDescriptor for PassengerSeatEntity {
fn serialized_components() -> u8 {
BlockEntity::serialized_components() + 1
}

fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> {
let mut c = self.block.components();
c.push(&self.cam_component);
return c;
}

fn hash_name(&self) -> u32 {
Self::hash("PassengerSeatEntityDescriptorV4") // 1360086092
}
}

+ 36
- 0
src/techblox/blocks/pilot_seat.rs View File

@@ -0,0 +1,36 @@
use crate::techblox::{SerializedEntityDescriptor, Parsable, SerializedEntityComponent, blocks::BlockEntity};
use libfj_parsable_macro_derive::*;

/// Pilot seat entity descriptor (V4)
#[derive(Copy, Clone, Parsable)]
pub struct PilotSeatEntity {
/// parent block entity
pub block: BlockEntity,
/// Seat following camera component
pub cam_component: SeatFollowCamComponent,
}

impl SerializedEntityDescriptor for PilotSeatEntity {
fn serialized_components() -> u8 {
BlockEntity::serialized_components() + 1
}

fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> {
let mut c = self.block.components();
c.push(&self.cam_component);
return c;
}

fn hash_name(&self) -> u32 {
Self::hash("PilotSeatEntityDescriptorV4") // 2281299333
}
}

/// Seat settings entity component.
#[derive(Copy, Clone, Parsable)]
pub struct SeatFollowCamComponent {
/// Should the camera follow the seat? (bool)
pub follow: u8,
}

impl SerializedEntityComponent for SeatFollowCamComponent {}

+ 90
- 0
src/techblox/blocks/spring.rs View File

@@ -0,0 +1,90 @@
use crate::techblox::{SerializedEntityDescriptor, Parsable, SerializedEntityComponent,
blocks::{BlockEntity}};
use libfj_parsable_macro_derive::*;

/// Damped angular spring entity descriptor
#[derive(Copy, Clone, Parsable)]
pub struct DampedAngularSpringEntity {
/// parent block entity
pub block: BlockEntity,
/// Joint tweakables component
pub tweak_component: TweakableJointDampingComponent,
/// Spring tweakables component
pub spring_component: DampedAngularSpringROStruct,
}

impl SerializedEntityDescriptor for DampedAngularSpringEntity {
fn serialized_components() -> u8 {
BlockEntity::serialized_components() + 2
}

fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> {
let mut c = self.block.components();
c.push(&self.tweak_component);
c.push(&self.spring_component);
return c;
}

fn hash_name(&self) -> u32 {
Self::hash("DampedAngularSpringEntityDescriptorV4") // 3789998433
}
}

/// Damped spring entity descriptor
#[derive(Copy, Clone, Parsable)]
pub struct DampedSpringEntity {
/// parent block entity
pub block: BlockEntity,
/// Joint tweakables component
pub tweak_component: TweakableJointDampingComponent,
/// Spring tweakables component
pub spring_component: DampedSpringROStruct,
}

impl SerializedEntityDescriptor for DampedSpringEntity {
fn serialized_components() -> u8 {
BlockEntity::serialized_components() + 2
}

fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> {
let mut c = self.block.components();
c.push(&self.tweak_component);
c.push(&self.spring_component);
return c;
}

fn hash_name(&self) -> u32 {
Self::hash("DampedSpringEntityDescriptorV5") // 2892049599
}
}

/// Joint settings entity component.
#[derive(Copy, Clone, Parsable)]
pub struct TweakableJointDampingComponent {
/// Joint stiffness (percent?)
pub stiffness: f32,
/// Force damping (percent?)
pub damping: f32,
}

impl SerializedEntityComponent for TweakableJointDampingComponent {}

/// Damped angular spring settings entity component.
#[derive(Copy, Clone, Parsable)]
pub struct DampedSpringROStruct {
/// Maximum spring extension
pub max_extension: f32,
}

impl SerializedEntityComponent for DampedSpringROStruct {}

/// Damped angular spring settings entity component.
#[derive(Copy, Clone, Parsable)]
pub struct DampedAngularSpringROStruct {
/// Minimum sprint extension
pub joint_min: f32,
/// Maximum sprint extension
pub joint_max: f32,
}

impl SerializedEntityComponent for DampedAngularSpringROStruct {}

+ 24
- 0
src/techblox/blocks/tyre.rs View File

@@ -0,0 +1,24 @@
use crate::techblox::{SerializedEntityDescriptor, Parsable, SerializedEntityComponent,
blocks::{BlockEntity}};
use libfj_parsable_macro_derive::*;

/// Tire entity descriptor
#[derive(Copy, Clone, Parsable)]
pub struct TyreEntity {
/// parent block entity
pub block: BlockEntity,
}

impl SerializedEntityDescriptor for TyreEntity {
fn serialized_components() -> u8 {
BlockEntity::serialized_components()
}

fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> {
self.block.components()
}

fn hash_name(&self) -> u32 {
Self::hash("TyreEntityDescriptorV1") // 1517625162
}
}

+ 74
- 0
src/techblox/blocks/wheel_rig.rs View File

@@ -0,0 +1,74 @@
use crate::techblox::{SerializedEntityDescriptor, Parsable, SerializedEntityComponent,
blocks::{BlockEntity, TweakableJointDampingComponent}};
use libfj_parsable_macro_derive::*;

/// Wheel rig entity descriptor
#[derive(Copy, Clone, Parsable)]
pub struct WheelRigEntity {
/// parent block entity
pub block: BlockEntity,
/// Wheel tweakables component
pub tweak_component: WheelRigTweakableStruct,
/// Joint tweakables component
pub joint_component: TweakableJointDampingComponent,
}

impl SerializedEntityDescriptor for WheelRigEntity {
fn serialized_components() -> u8 {
BlockEntity::serialized_components() + 2
}

fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> {
let mut c = self.block.components();
c.push(&self.tweak_component);
c.push(&self.joint_component);
return c;
}

fn hash_name(&self) -> u32 {
Self::hash("WheelRigEntityDescriptor") // 1156723746
}
}

/// Wheel rig entity descriptor
#[derive(Copy, Clone, Parsable)]
pub struct WheelRigSteerableEntity {
/// parent wheel rig entity
pub block: WheelRigEntity,
/// Steering tweakables component
pub tweak_component: WheelRigSteerableTweakableStruct,
}

impl SerializedEntityDescriptor for WheelRigSteerableEntity {
fn serialized_components() -> u8 {
WheelRigEntity::serialized_components() + 1
}

fn components<'a>(&'a self) -> Vec<&'a dyn SerializedEntityComponent> {
let mut c = self.block.components();
c.push(&self.tweak_component);
return c;
}

fn hash_name(&self) -> u32 {
Self::hash("WheelRigSteerableEntityDescriptor") // 1864425618
}
}

/// Wheel rig settings entity component.
#[derive(Copy, Clone, Parsable)]
pub struct WheelRigTweakableStruct {
/// Brake force (percent?)
pub braking_strength: f32,
}

impl SerializedEntityComponent for WheelRigTweakableStruct {}

/// Steering wheel rig settings entity component.
#[derive(Copy, Clone, Parsable)]
pub struct WheelRigSteerableTweakableStruct {
/// Wheel steering angle (max?)
pub steer_angle: f32,
}

impl SerializedEntityComponent for WheelRigSteerableTweakableStruct {}

+ 12
- 1
src/techblox/entity_header.rs View File

@@ -1,4 +1,4 @@
use crate::techblox::{hashname, brute_force, Parsable};
use crate::techblox::{hashname, brute_force, Parsable, blocks::lookup_name_by_hash};
use libfj_parsable_macro_derive::*;

/// An entity's header information.
@@ -24,6 +24,17 @@ impl EntityHeader {
brute_force(self.hash)
}

/// Lookup the name from the header's hash from a list of known entity names.
///
/// This is much faster than guess_name() and is guaranteed to return a correct result if one exists.
/// If the hash has no known correct name, None is returned instead.
pub fn lookup_name(&self) -> Option<String> {
if let Some(name) = lookup_name_by_hash(self.hash) {
return Some(name.to_string());
}
None
}

/// Create an entity header using the hash of `name`.
pub fn from_name(name: &str, entity_id: u32, group_id: u32, component_count: u8) -> Self {
Self {


+ 2
- 0
src/techblox/gamesave.rs View File

@@ -93,6 +93,8 @@ impl Parsable for GameSave {
for _i in 0..cube_count {
let header = EntityHeader::parse(data)?;
let hash = header.hash;
#[cfg(debug_assertions)]
println!("Handling block {} (hash: {} id:{}/{} components: {})", cubes_h.len(), hash, header.entity_id, header.group_id, header.component_count);
cubes_h.push(header);
cubes_e.push(lookup_hashname(hash, data)?);
}


+ 1
- 0
test.sh View File

@@ -1,4 +1,5 @@
#!/bin/bash
RUST_BACKTRACE=1 cargo test --all-features -- --nocapture
# RUST_BACKTRACE=1 cargo test --release --all-features -- --nocapture
# RUST_BACKTRACE=1 cargo test --features techblox -- --nocapture
exit $?

BIN
tests/All.Techblox View File


+ 62
- 3
tests/techblox_parsing.rs View File

@@ -10,7 +10,11 @@ use std::fs::{File, OpenOptions};
#[cfg(feature = "techblox")]
const GAMESAVE_PATH: &str = "tests/GameSave.Techblox";
#[cfg(feature = "techblox")]
const GAMESAVE_PATH2: &str = "tests/GameSave2.Techblox";
const GAMESAVE_PATH_OUT: &str = "tests/GameSave.out.Techblox";
#[cfg(feature = "techblox")]
const GAMESAVE_PATH_ALL: &str = "tests/All.Techblox";
#[cfg(feature = "techblox")]
const GAMESAVE_PATH_ALL_OUT: &str = "tests/All.out.Techblox";

#[cfg(feature = "techblox")]
const HASHNAMES: &[&str] = &[
@@ -148,15 +152,70 @@ fn hash_tb_name() {
#[cfg(feature = "techblox")]
#[test]
fn techblox_gamesave_perfect_parse() -> Result<(), ()> {
let mut in_file = File::open(GAMESAVE_PATH).map_err(|_| ())?;
let mut in_file = File::open(GAMESAVE_PATH_ALL).map_err(|_| ())?;
let mut buf = Vec::new();
in_file.read_to_end(&mut buf).map_err(|_| ())?;
let gs = techblox::GameSave::parse(&mut buf.as_slice()).map_err(|_| ())?;
let mut out_file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(GAMESAVE_PATH_OUT)
.map_err(|_| ())?;
gs.dump(&mut out_file).map_err(|_| ())?;
assert_eq!(in_file.stream_position().unwrap(), out_file.stream_position().unwrap());
Ok(())
}

#[cfg(feature = "techblox")]
#[test]
fn techblox_gamesave_parse_all() -> Result<(), ()> {
let mut in_file = File::open(GAMESAVE_PATH_ALL).map_err(|_| ())?;
let mut buf = Vec::new();
in_file.read_to_end(&mut buf).map_err(|_| ())?;
let gs = techblox::GameSave::parse(&mut buf.as_slice()).map_err(|_| ())?;

// verify
for i in 1..(gs.group_len as usize) {
assert_eq!(gs.group_headers[i-1].hash, gs.group_headers[i].hash);
//println!("#{} count {} vs {}", i, gs.group_headers[i-1].component_count, gs.group_headers[i].component_count);
assert_eq!(gs.group_headers[i-1].component_count, gs.group_headers[i].component_count);
}
for i in 0..(gs.group_len as usize) {
assert_eq!(gs.group_headers[i].component_count, techblox::BlockGroupEntity::serialized_components());
assert_eq!(gs.group_headers[i].hash, gs.cube_groups[i].hash_name());
}
for i in 1..(gs.cube_len as usize) {
//assert_eq!(gs.cube_headers[i-1].hash, gs.cube_headers[i].hash);
//println!("#{} count {} vs {}", i, gs.cube_headers[i-1].component_count, gs.cube_headers[i].component_count);
if gs.cube_headers[i-1].hash == gs.cube_headers[i].hash {
assert_eq!(gs.group_headers[i-1].component_count, gs.group_headers[i].component_count);
}
}
for i in 0..(gs.cube_len as usize) {
assert!(gs.cube_headers[i].component_count >= blocks::BlockEntity::serialized_components());
//println!("#{} components: {}", i, gs.cube_headers[i].component_count);
assert_eq!(gs.cube_headers[i].hash, gs.cube_entities[i].hash_name());
}

//println!("Parsed wire settings hash: {} obsolete? {}", gs.wire_settings_header.hash, gs.wire_settings_entity.settings_component.obsolete != 0);
assert_eq!(gs.wire_settings_header.hash, EntityHeader::from_name("GlobalWireSettingsEntityDescriptor", 0, 0, 0).hash);
assert_eq!(gs.wire_settings_header.hash, gs.wire_settings_entity.hash_name());

//println!("Parsed Flycam hash: {}", gs.flycam_header.hash);
assert_eq!(gs.flycam_header.hash, EntityHeader::from_name("FlyCamEntityDescriptorV0", 0, 0, 0).hash);
assert_eq!(gs.flycam_header.hash, gs.flycam_entity.hash_name());

//println!("Parsed Phycam hash: {}", gs.phycam_header.hash);
assert_eq!(gs.phycam_header.hash, EntityHeader::from_name("CharacterCameraEntityDescriptorV1", 0, 0, 0).hash);
assert_eq!(gs.phycam_header.hash, gs.phycam_entity.hash_name());

// write out
let mut out_file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(GAMESAVE_PATH2)
.open(GAMESAVE_PATH_ALL_OUT)
.map_err(|_| ())?;
gs.dump(&mut out_file).map_err(|_| ())?;
assert_eq!(in_file.stream_position().unwrap(), out_file.stream_position().unwrap());


Loading…
Cancel
Save