refactor a bunch of things to get ready for rust world sync

This commit is contained in:
bgkillas 2025-07-13 21:06:05 -04:00
parent 4ff165b815
commit 067570672d
32 changed files with 540 additions and 3126 deletions

16
noita-proxy/Cargo.lock generated
View file

@ -890,9 +890,9 @@ dependencies = [
[[package]]
name = "crc32fast"
version = "1.4.2"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@ -2625,9 +2625,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "memmap2"
version = "0.9.5"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28"
dependencies = [
"libc",
]
@ -4041,9 +4041,9 @@ dependencies = [
[[package]]
name = "rgb"
version = "0.8.51"
version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a457e416a0f90d246a4c3288bd7a25b2304ca727f253f95be383dd17af56be8f"
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
dependencies = [
"bytemuck",
]
@ -6410,9 +6410,9 @@ dependencies = [
[[package]]
name = "winresource"
version = "0.1.22"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a179ac8923651ff1d15efbee760b4dd3679fd85fa5a8b2bb1109b7248f80e30f"
checksum = "edcacf11b6f48dd21b9ba002f991bdd5de29b2da8cc2800412f4b80f677e4957"
dependencies = [
"toml",
"version_check",

View file

@ -55,13 +55,13 @@ use util::{args::Args, steam_helper::LobbyExtraData};
mod bookkeeping;
use crate::net::messages::NetMsg;
use crate::net::omni::OmniPeerId;
use crate::net::world::world_model::ChunkCoord;
use crate::player_cosmetics::{
display_player_skin, get_player_skin, player_path, player_select_current_color_slot,
player_skin_display_color_picker, shift_hue,
};
pub use bookkeeping::{mod_manager, releases, self_update};
use shared::WorldPos;
use shared::world_sync::ChunkCoord;
mod lobby_code;
pub mod net;
mod player_cosmetics;
@ -126,7 +126,7 @@ pub enum LocalHealthMode {
#[serde(default)]
pub struct GameSettings {
seed: u64,
world_num: u16,
world_num: u8,
debug_mode: Option<bool>,
use_constant_seed: bool,
duplicate: Option<bool>,
@ -1660,7 +1660,7 @@ impl App {
fn connect_screen(&mut self, ctx: &Context) {
egui::CentralPanel::default().show(ctx, |ui| {
if self.app_saved_state.times_started % 20 == 0 {
if self.app_saved_state.times_started.is_multiple_of(20) {
let image = egui::Image::new(egui::include_image!("../assets/longleg.png"))
.texture_options(TextureOptions::NEAREST);
image.paint_at(ui, ctx.screen_rect());

View file

@ -25,12 +25,12 @@ use std::{
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use world::{NoitaWorldUpdate, WorldManager};
use world::WorldManager;
use crate::lobby_code::LobbyKind;
use crate::mod_manager::{ModmanagerSettings, get_mods};
use crate::net::world::world_model::ChunkData;
use crate::net::world::world_model::chunk::{Pixel, PixelFlags};
use crate::net::world::world_model::{ChunkCoord, ChunkData};
use crate::player_cosmetics::{PlayerPngDesc, create_player_png, get_player_skin};
use crate::steam_helper::LobbyExtraData;
use crate::{
@ -38,6 +38,7 @@ use crate::{
bookkeeping::save_state::{SaveState, SaveStateEntry},
};
use shared::des::ProxyToDes;
use shared::world_sync::{ChunkCoord, ProxyToWorldSync};
use tangled::Reliability;
use tracing::{error, info, warn};
mod audio;
@ -535,12 +536,10 @@ impl NetManager {
for msg in state.world.get_emitted_msgs() {
self.do_message_request(msg)
}
state.world.update();
let updates = state.world.get_noita_updates();
for update in updates {
state.try_ms_write(&ws_encode_proxy_bin(0, &update));
}
let updates = state.world.update();
state.try_ms_write(&NoitaInbound::ProxyToWorldSync(ProxyToWorldSync::Updates(
updates,
)));
if state.had_a_disconnect {
self.broadcast(&NetMsg::NoitaDisconnected, Reliability::Reliable);
@ -718,6 +717,10 @@ impl NetManager {
sendm: &Sender<FxHashMap<u16, u32>>,
) {
match net_msg {
NetMsg::ForwardWorldSyncToProxy(msg) => state.world.handle_noita_msg(src, msg),
NetMsg::ForwardProxyToWorldSync(msg) => {
state.try_ms_write(&NoitaInbound::ProxyToWorldSync(msg));
}
NetMsg::AudioData(data, global, tx, ty, vol) => {
if !self.is_cess.load(Ordering::Relaxed) {
let audio = self.audio.lock().unwrap().clone();
@ -1144,8 +1147,6 @@ impl NetManager {
},
);
}
// Binary message to proxy
3 => self.handle_bin_message_to_proxy(&raw_msg[1..], state),
0 => {
let flags = String::from_utf8_lossy(&raw_msg[1..]).into();
let msg = NetMsg::Flags(flags);
@ -1167,6 +1168,19 @@ impl NetManager {
);
}
}
NoitaOutbound::WorldSyncToProxy(world_sync_msg) => {
if self.is_host() {
state
.world
.handle_noita_msg(self.peer.my_id(), world_sync_msg)
} else {
self.send(
self.peer.host_id(),
&NetMsg::ForwardWorldSyncToProxy(world_sync_msg),
Reliability::Reliable,
);
}
}
NoitaOutbound::RemoteMessage {
reliable,
destination,
@ -1432,29 +1446,6 @@ impl NetManager {
}
}
fn handle_bin_message_to_proxy(&self, msg: &[u8], state: &mut NetInnerState) {
let key = msg[0];
let data = &msg[1..];
match key {
// world frame
0 => {
let update = NoitaWorldUpdate::load(data);
state.world.add_update(update);
}
// world end
1 => {
let pos = data[1..]
.split(|b| *b == b':')
.map(|s| String::from_utf8_lossy(s).parse::<i32>().unwrap_or(0))
.collect::<Vec<i32>>();
state.world.add_end(data[0], &pos);
}
key => {
error!("Unknown bin msg from mod: {:?}", key)
}
}
}
fn end_run(&self, state: &mut NetInnerState) {
self.init_settings.save_state.reset();
{
@ -1468,7 +1459,7 @@ impl NetManager {
.modmanager_settings
.get_progress()
.unwrap_or_default();
if settings.world_num == u16::MAX {
if settings.world_num == u8::MAX {
settings.world_num = 0
} else {
settings.world_num += 1

View file

@ -1,9 +1,9 @@
use super::{omni::OmniPeerId, world::WorldNetMessage};
use crate::net::world::world_model::{ChunkCoord, ChunkData};
use crate::net::world::world_model::ChunkData;
use crate::{GameSettings, player_cosmetics::PlayerPngDesc};
use bitcode::{Decode, Encode};
use rustc_hash::FxHashMap;
use shared::world_sync::ChunkCoord;
pub(crate) type Destination = shared::Destination<OmniPeerId>;
pub(crate) struct MessageRequest<T> {
@ -27,7 +27,9 @@ pub(crate) enum NetMsg {
PlayerColor(PlayerPngDesc, bool, Option<OmniPeerId>, String),
RemoteMsg(shared::RemoteMessage),
ForwardDesToProxy(shared::des::DesToProxy),
ForwardWorldSyncToProxy(shared::world_sync::WorldSyncToProxy),
ForwardProxyToDes(shared::des::ProxyToDes),
ForwardProxyToWorldSync(shared::world_sync::ProxyToWorldSync),
NoitaDisconnected,
Flags(String),
RespondFlagNormal(String, bool),

View file

@ -4,22 +4,19 @@ use rand::{Rng, rng};
use rayon::iter::IntoParallelIterator;
use rayon::iter::ParallelIterator;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::f32::consts::TAU;
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::time::Duration;
use std::{cmp, env, mem, thread};
use std::{cmp, mem, thread};
use tracing::{debug, info, warn};
use wide::f32x8;
use world_model::{
CHUNK_SIZE, ChunkCoord, ChunkData, ChunkDelta, WorldModel,
ChunkData, ChunkDelta, WorldModel,
chunk::{Chunk, Pixel},
};
pub use world_model::encoding::NoitaWorldUpdate;
use crate::bookkeeping::save_state::{SaveState, SaveStateEntry};
use super::{
@ -30,12 +27,6 @@ use super::{
pub mod world_model;
#[derive(Debug, Serialize, Deserialize)]
pub enum WorldUpdateKind {
Update(NoitaWorldUpdate),
End,
}
#[derive(Debug, Decode, Encode, Clone)]
pub(crate) enum WorldNetMessage {
// Authority request
@ -75,13 +66,13 @@ pub(crate) enum WorldNetMessage {
RelinquishAuthority {
chunk: ChunkCoord,
chunk_data: Option<ChunkData>,
world_num: i32,
world_num: u8,
},
// Ttell how to update a chunk storage
// Tell how to update a chunk storage
UpdateStorage {
chunk: ChunkCoord,
chunk_data: Option<ChunkData>,
world_num: i32,
world_num: u8,
priority: Option<u8>,
},
// When listening
@ -200,7 +191,7 @@ pub(crate) struct WorldManager {
chunk_last_update: FxHashMap<ChunkCoord, u64>,
/// Stores last priority we used for that chunk, in case transfer fails and we'll need to request authority normally.
last_request_priority: FxHashMap<ChunkCoord, u8>,
world_num: i32,
world_num: u8,
pub materials: FxHashMap<u16, (u32, u32, CellType, u32)>,
is_storage_recent: FxHashSet<ChunkCoord>,
explosion_pointer: FxHashMap<ChunkCoord, Vec<usize>>,
@ -330,64 +321,23 @@ impl WorldManager {
self.chunk_storage.clone()
}
pub(crate) fn add_update(&mut self, update: NoitaWorldUpdate) {
self.outbound_model
.apply_noita_update(&update, &mut self.is_storage_recent);
}
pub(crate) fn add_end(&mut self, priority: u8, pos: &[i32]) {
let updated_chunks = self
.outbound_model
.updated_chunks()
.iter()
.copied()
.collect::<Vec<_>>();
self.current_update += 1;
let chunks_to_send: Vec<Vec<(OmniPeerId, u8)>> = updated_chunks
.iter()
.map(|chunk| self.chunk_updated_locally(*chunk, priority, pos))
.collect();
let mut chunk_packet: HashMap<OmniPeerId, Vec<(ChunkDelta, u8)>> = HashMap::new();
for (chunk, who_sending) in updated_chunks.iter().zip(chunks_to_send.iter()) {
let Some(delta) = self.outbound_model.get_chunk_delta(*chunk, false) else {
continue;
};
for (peer, pri) in who_sending {
chunk_packet
.entry(*peer)
.or_default()
.push((delta.clone(), *pri));
}
}
let mut emit_queue = Vec::new();
for (peer, chunkpacket) in chunk_packet {
emit_queue.push((
Destination::Peer(peer),
WorldNetMessage::ChunkPacket { chunkpacket },
));
}
for (dst, msg) in emit_queue {
self.emit_msg(dst, msg)
}
self.outbound_model.reset_change_tracking();
}
fn chunk_updated_locally(
&mut self,
chunk: ChunkCoord,
priority: u8,
pos: &[i32],
pos: Option<(i32, i32, i32, i32, bool)>,
world_num: u8,
) -> Vec<(OmniPeerId, u8)> {
if pos.len() == 6 {
self.my_pos = (pos[0], pos[1]);
self.cam_pos = (pos[2], pos[3]);
self.is_notplayer = pos[4] == 1;
if self.world_num != pos[5] {
self.world_num = pos[5];
if let Some((px, py, cx, cy, is_not)) = pos {
self.my_pos = (px, py);
self.cam_pos = (cx, cy);
self.is_notplayer = is_not;
if self.world_num != world_num {
self.world_num = world_num;
self.reset();
}
} else if self.world_num != pos[0] {
self.world_num = pos[0];
} else if self.world_num != world_num {
self.world_num = world_num;
self.reset();
}
let entry = self.chunk_state.entry(chunk).or_insert_with(|| {
@ -508,7 +458,7 @@ impl WorldManager {
chunks_to_send
}
pub(crate) fn update(&mut self) {
pub(crate) fn update(&mut self) -> Vec<NoitaWorldUpdate> {
fn should_kill(
my_pos: (i32, i32),
cam_pos: (i32, i32),
@ -638,18 +588,11 @@ impl WorldManager {
}
retain
});
self.get_noita_updates()
}
pub(crate) fn get_noita_updates(&mut self) -> Vec<Vec<u8>> {
// Sends random data to noita to check if it crashes.
if env::var_os("NP_WORLD_SYNC_TEST").is_some() && self.current_update % 10 == 0 {
let chunk_data = ChunkData::make_random();
self.inbound_model
.apply_chunk_data(ChunkCoord(0, 0), &chunk_data)
}
let updates = self.inbound_model.get_all_noita_updates();
self.inbound_model.reset_change_tracking();
updates
pub(crate) fn get_noita_updates(&mut self) -> Vec<NoitaWorldUpdate> {
self.inbound_model.get_all_noita_updates()
}
pub(crate) fn reset(&mut self) {
@ -3137,6 +3080,7 @@ use crate::net::world::world_model::chunk::PixelFlags;
use rand::seq::SliceRandom;
#[cfg(test)]
use serial_test::serial;
use shared::world_sync::{CHUNK_SIZE, ChunkCoord, NoitaWorldUpdate, WorldSyncToProxy};
#[cfg(test)]
#[test]
#[serial]
@ -3406,3 +3350,52 @@ fn test_cut_perf() {
}
println!("total micros: {}", total / iters);
}
impl WorldManager {
pub fn handle_noita_msg(&mut self, _: OmniPeerId, msg: WorldSyncToProxy) {
match msg {
WorldSyncToProxy::Updates(updates) => {
for update in updates {
self.outbound_model
.apply_noita_update(update, &mut self.is_storage_recent)
}
}
WorldSyncToProxy::End(pos, priority, world_num) => {
let updated_chunks = self
.outbound_model
.updated_chunks()
.iter()
.copied()
.collect::<Vec<_>>();
self.current_update += 1;
let chunks_to_send: Vec<Vec<(OmniPeerId, u8)>> = updated_chunks
.iter()
.map(|chunk| self.chunk_updated_locally(*chunk, priority, pos, world_num))
.collect();
let mut chunk_packet: HashMap<OmniPeerId, Vec<(ChunkDelta, u8)>> = HashMap::new();
for (chunk, who_sending) in updated_chunks.iter().zip(chunks_to_send.iter()) {
let Some(delta) = self.outbound_model.get_chunk_delta(*chunk, false) else {
continue;
};
for (peer, pri) in who_sending {
chunk_packet
.entry(*peer)
.or_default()
.push((delta.clone(), *pri));
}
}
let mut emit_queue = Vec::new();
for (peer, chunkpacket) in chunk_packet {
emit_queue.push((
Destination::Peer(peer),
WorldNetMessage::ChunkPacket { chunkpacket },
));
}
for (dst, msg) in emit_queue {
self.emit_msg(dst, msg)
}
self.outbound_model.reset_change_tracking();
}
}
}
}

View file

@ -3,18 +3,13 @@ use std::sync::Arc;
use bitcode::{Decode, Encode};
use chunk::{Chunk, CompactPixel, Pixel, PixelFlags};
use encoding::{NoitaWorldUpdate, PixelRun, PixelRunner};
use encoding::PixelRunner;
use rustc_hash::{FxHashMap, FxHashSet};
use shared::world_sync::{CHUNK_SIZE, ChunkCoord, NoitaWorldUpdate, PixelRun};
use tracing::info;
pub(crate) mod chunk;
pub mod encoding;
pub(crate) const CHUNK_SIZE: usize = 128;
#[derive(Debug, Encode, Decode, Clone, Copy, Hash, PartialEq, Eq)]
pub struct ChunkCoord(pub i32, pub i32);
#[derive(Default)]
pub(crate) struct WorldModel {
chunks: FxHashMap<ChunkCoord, Chunk>,
@ -38,7 +33,7 @@ pub(crate) struct ChunkDelta {
}
impl ChunkData {
pub(crate) fn make_random() -> Self {
/*pub(crate) fn make_random() -> Self {
let mut runner = PixelRunner::new();
for i in 0..CHUNK_SIZE * CHUNK_SIZE {
runner.put_pixel(
@ -51,7 +46,7 @@ impl ChunkData {
}
let runs = runner.build();
ChunkData { runs }
}
}*/
#[cfg(test)]
pub(crate) fn new(mat: u16) -> Self {
@ -123,17 +118,17 @@ impl WorldModel {
self.updated_chunks.insert(chunk_coord);
}*/
fn get_pixel(&self, x: i32, y: i32) -> Pixel {
/*fn get_pixel(&self, x: i32, y: i32) -> Pixel {
let (chunk_coord, offset) = Self::get_chunk_coords(x, y);
self.chunks
.get(&chunk_coord)
.map(|chunk| chunk.pixel(offset))
.unwrap_or_default()
}
}*/
pub fn apply_noita_update(
&mut self,
update: &NoitaWorldUpdate,
update: NoitaWorldUpdate,
changed: &mut FxHashSet<ChunkCoord>,
) {
fn set_pixel(pixel: Pixel, chunk: &mut Chunk, offset: usize) -> bool {
@ -145,21 +140,23 @@ impl WorldModel {
false
}
}
let header = &update.header;
let runs = &update.runs;
let mut x = 0;
let mut y = 0;
let (mut chunk_coord, _) = Self::get_chunk_coords(header.x, header.y);
let mut chunk = self.chunks.entry(chunk_coord).or_default();
for run in runs {
let (start_x, start_y) = (
update.coord.0 * CHUNK_SIZE as i32,
update.coord.1 * CHUNK_SIZE as i32,
);
let mut chunk_coord = update.coord;
let mut chunk = self.chunks.entry(update.coord).or_default();
for run in update.runs {
let flags = if run.data.flags > 0 {
PixelFlags::Fluid
} else {
PixelFlags::Normal
};
for _ in 0..run.length {
let xs = header.x + x;
let ys = header.y + y;
let xs = start_x + x;
let ys = start_y + y;
let (new_chunk_coord, offset) = Self::get_chunk_coords(xs, ys);
if chunk_coord != new_chunk_coord {
chunk_coord = new_chunk_coord;
@ -178,37 +175,32 @@ impl WorldModel {
changed.remove(&chunk_coord);
}
}
x += 1;
if x == i32::from(header.w) + 1 {
if x == CHUNK_SIZE as i32 {
x = 0;
y += 1;
} else {
x += 1;
}
}
}
}
pub fn get_noita_update(&self, x: i32, y: i32, w: u32, h: u32) -> NoitaWorldUpdate {
assert!(w <= 256);
assert!(h <= 256);
let mut runner = PixelRunner::new();
for j in 0..(h as i32) {
for i in 0..(w as i32) {
runner.put_pixel(self.get_pixel(x + i, y + j).to_raw())
}
}
runner.into_noita_update(x, y, (w - 1) as u8, (h - 1) as u8)
}
pub fn get_all_noita_updates(&self) -> Vec<Vec<u8>> {
pub fn get_all_noita_updates(&mut self) -> Vec<NoitaWorldUpdate> {
let mut updates = Vec::new();
for chunk_coord in &self.updated_chunks {
let update = self.get_noita_update(
chunk_coord.0 * (CHUNK_SIZE as i32),
chunk_coord.1 * (CHUNK_SIZE as i32),
CHUNK_SIZE as u32,
CHUNK_SIZE as u32,
);
updates.push(update.save());
for coord in self.updated_chunks.drain() {
if let Some(chunk) = self.chunks.get_mut(&coord) {
chunk.clear_changed();
let mut runner = PixelRunner::new();
for j in 0..CHUNK_SIZE {
for i in 0..CHUNK_SIZE {
runner.put_pixel(chunk.pixel(i + j * CHUNK_SIZE).to_raw())
}
}
updates.push(NoitaWorldUpdate {
coord,
runs: runner.build(),
});
}
}
updates
}

View file

@ -1,12 +1,9 @@
use std::num::NonZeroU16;
use super::{ChunkData, encoding::PixelRunner};
use bitcode::{Decode, Encode};
use crossbeam::atomic::AtomicCell;
use super::{
CHUNK_SIZE, ChunkData,
encoding::{PixelRunner, RawPixel},
};
use shared::world_sync::{CHUNK_SIZE, RawPixel};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Encode, Decode)]
pub enum PixelFlags {

View file

@ -1,46 +1,12 @@
use bitcode::{Decode, Encode};
use bytemuck::{AnyBitPattern, NoUninit, bytes_of, pod_read_unaligned};
use serde::{Deserialize, Serialize};
use std::mem::size_of;
#[derive(Debug, Clone, Copy, AnyBitPattern, NoUninit, Serialize, Deserialize, PartialEq, Eq)]
#[repr(C)]
pub(crate) struct Header {
pub x: i32,
pub y: i32,
pub w: u8,
pub h: u8,
pub run_count: u16,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct NoitaWorldUpdate {
pub(crate) header: Header,
pub(crate) runs: Vec<PixelRun<RawPixel>>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
pub(crate) struct RawPixel {
pub material: u16,
pub flags: u8,
}
struct ByteParser<'a> {
use shared::world_sync::PixelRun;
/*struct ByteParser<'a> {
data: &'a [u8],
}
/// Stores a run of pixels.
/// Not specific to Noita side - length is an actual length
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Encode, Decode)]
pub struct PixelRun<Pixel> {
pub length: u32,
pub data: Pixel,
}
}*/
/// Converts a normal sequence of pixels to a run-length-encoded one.
pub struct PixelRunner<Pixel> {
current_pixel: Option<Pixel>,
current_run_len: u32,
current_run_len: u16,
runs: Vec<PixelRun<Pixel>>,
}
@ -86,24 +52,7 @@ impl<Pixel: Eq + Copy> PixelRunner<Pixel> {
}
}
impl PixelRunner<RawPixel> {
/// Note: w/h are actualy width/height -1
pub fn into_noita_update(self, x: i32, y: i32, w: u8, h: u8) -> NoitaWorldUpdate {
let runs = self.build();
NoitaWorldUpdate {
header: Header {
x,
y,
w,
h,
run_count: runs.len() as u16,
},
runs,
}
}
}
impl<'a> ByteParser<'a> {
/*impl<'a> ByteParser<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data }
}
@ -117,7 +66,7 @@ impl<'a> ByteParser<'a> {
fn next_run(&mut self) -> PixelRun<RawPixel> {
PixelRun {
length: u32::from(self.next::<u16>()) + 1,
length: self.next::<u16>() + 1,
data: RawPixel {
material: self.next(),
flags: self.next(),
@ -125,37 +74,4 @@ impl<'a> ByteParser<'a> {
}
}
}
impl NoitaWorldUpdate {
pub fn load(data: &[u8]) -> Self {
let mut parser = ByteParser::new(data);
let header: Header = parser.next();
let mut runs = Vec::with_capacity(header.run_count.into());
for _ in 0..header.run_count {
runs.push(parser.next_run());
}
assert!(parser.data.is_empty());
Self { header, runs }
}
pub fn save(&self) -> Vec<u8> {
let header = Header {
run_count: self.runs.len() as u16,
..self.header
};
let mut buf = Vec::new();
buf.extend_from_slice(bytes_of(&header));
for run in &self.runs {
let len = u16::try_from(run.length - 1).unwrap();
buf.extend_from_slice(bytes_of(&len));
buf.extend_from_slice(bytes_of(&run.data.material));
buf.extend_from_slice(bytes_of(&run.data.flags));
}
buf
}
}
*/