World replays

This commit is contained in:
IQuant 2024-06-03 20:13:16 +03:00
parent 67ebe63fa3
commit a7f52e42d1
5 changed files with 146 additions and 25 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
target
mat_colors.txt
mat_data.txt

View file

@ -33,7 +33,7 @@ struct ByteParser<'a> {
}
pub struct WorldManager {
// writer: BufWriter<File>,
writer: BufWriter<File>,
}
impl<'a> ByteParser<'a> {
@ -81,16 +81,16 @@ pub enum WorldUpdateKind {
impl WorldManager {
pub fn new() -> Self {
Self {
// writer: BufWriter::new(File::create("worldlog.bin").unwrap()),
writer: BufWriter::new(File::create("worldlog.bin").unwrap()),
}
}
pub fn add_update(&mut self, update: RunLengthUpdate) {
// bincode::serialize_into(&mut self.writer, &WorldUpdateKind::Update(update)).unwrap();
bincode::serialize_into(&mut self.writer, &WorldUpdateKind::Update(update)).unwrap();
}
pub fn add_end(&mut self) {
// bincode::serialize_into(&mut self.writer, &WorldUpdateKind::End).unwrap();
bincode::serialize_into(&mut self.writer, &WorldUpdateKind::End).unwrap();
}
}
@ -110,16 +110,25 @@ mod test {
model.apply_update(&entry);
// println!("{:?}", entry.header)
entry_id += 1;
if entry_id > 1000000 {
break;
}
// if entry_id > 1000000 {
// break;
// }
if entry_id % 10000 == 0 {
println!("{}", model.pixels.len());
let (x, y) = model.get_start();
let img = model.gen_image(x, y, 2048, 2048);
img.save(format!("/tmp/img_{}.png", entry_id)).unwrap();
}
}
}
let (x, y) = model.get_start();
let img = model.gen_image(x, y, 2048 * 2, 2048 * 2);
img.save(format!("/tmp/img_{}.png", entry_id)).unwrap();
let mut mats = model.mats.iter().copied().collect::<Vec<_>>();
mats.sort();
for mat in mats {
println!("{}", mat)
}
}
}

View file

@ -1,28 +1,88 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use image::{Rgb, RgbImage};
use super::RunLengthUpdate;
const CHUNK_SIZE: usize = 256;
#[derive(Clone, Copy, Default)]
pub struct Pixel {
material: i16,
}
struct Chunk {
pixels: [Pixel; CHUNK_SIZE * CHUNK_SIZE],
}
pub struct WorldModel {
pub pixels: HashMap<(i32, i32), Pixel>,
chunks: HashMap<(i32, i32), Chunk>,
pub mats: HashSet<i16>,
palette: MatPalette,
}
struct MatPalette {
colors: Vec<Rgb<u8>>,
}
impl Default for Chunk {
fn default() -> Self {
Self {
pixels: [Pixel::default(); CHUNK_SIZE * CHUNK_SIZE],
}
}
}
impl MatPalette {
fn new() -> Self {
let raw_data = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/mat_colors.txt"
));
let mut colors = Vec::new();
for line in raw_data.split_ascii_whitespace() {
let num: u32 = line.parse().unwrap();
let color = Rgb::from([(num >> 16) as u8, (num >> 8) as u8, num as u8]);
colors.push(color);
}
Self { colors }
}
}
impl WorldModel {
fn to_chunk_coords(x: i32, y: i32) -> ((i32, i32), usize) {
let chunk_x = x.div_euclid(CHUNK_SIZE as i32);
let chunk_y = y.div_euclid(CHUNK_SIZE as i32);
let x = x.rem_euclid(CHUNK_SIZE as i32) as usize;
let y = y.rem_euclid(CHUNK_SIZE as i32) as usize;
let offset = x + y * CHUNK_SIZE;
((chunk_x, chunk_y), offset)
}
fn set_pixel(&mut self, x: i32, y: i32, pixel: Pixel) {
self.pixels.insert((x, y), pixel);
self.mats.insert(pixel.material);
let (chunk_coord, offset) = Self::to_chunk_coords(x, y);
let chunk = self
.chunks
.entry(chunk_coord)
.or_insert_with(Default::default);
chunk.pixels[offset] = pixel;
}
fn get_pixel(&self, x: i32, y: i32) -> Pixel {
self.pixels.get(&(x, y)).copied().unwrap_or_default()
let (chunk_coord, offset) = Self::to_chunk_coords(x, y);
self.chunks
.get(&chunk_coord)
.map(|chunk| chunk.pixels[offset])
.unwrap_or_default()
}
pub fn new() -> Self {
Self {
pixels: Default::default(),
chunks: Default::default(),
mats: Default::default(),
palette: MatPalette::new(),
}
}
@ -52,16 +112,21 @@ impl WorldModel {
}
pub fn get_start(&self) -> (i32, i32) {
let x = self.pixels.keys().map(|v| v.0).min().unwrap_or_default();
let y = self.pixels.keys().map(|v| v.1).min().unwrap_or_default();
(x, y)
let cx = self.chunks.keys().map(|v| v.0).min().unwrap_or_default();
let cy = self.chunks.keys().map(|v| v.1).min().unwrap_or_default();
(cx * CHUNK_SIZE as i32, cy * CHUNK_SIZE as i32)
}
pub fn gen_image(&self, x: i32, y: i32, w: u32, h: u32) -> RgbImage {
RgbImage::from_fn(w, h, |lx, ly| {
let pixel = self.get_pixel(x + lx as i32, y + ly as i32);
let b = if pixel.material != 0 { 255 } else { 0 };
Rgb([pixel.material as u8, (pixel.material >> 8) as u8, b])
// let b = if pixel.material != 0 { 255 } else { 0 };
// Rgb([pixel.material as u8, (pixel.material >> 8) as u8, b])
self.palette
.colors
.get(usize::try_from(pixel.material).unwrap_or_default())
.copied()
.unwrap_or(Rgb([0, 0, 0]))
})
}
}

View file

@ -18,6 +18,9 @@ local bandwidth_bucket_max = 29000
local KEY_WORLD_FRAME = 0
local KEY_WORLD_END = 1
local initialized_chunks = {}
local CHUNK_SIZE = 256
function world_sync.on_world_update_host()
local grid_world = world_ffi.get_grid_world()
@ -28,7 +31,6 @@ function world_sync.on_world_update_host()
local end_ = begin + thread_impl.chunk_update_count
local count = thread_impl.chunk_update_count
-- GamePrint("w update "..count)
for i = 0, count - 1 do
local it = begin[i]
@ -42,11 +44,8 @@ function world_sync.on_world_update_host()
end_x = end_x + 1
end_y = end_y + 2
-- if i < 9 then
-- GamePrint(start_x.." "..start_y.." "..end_x.." "..end_y)
-- end
local rectangle = rect.Rectangle(start_x, start_y, end_x, end_y)
rect_optimiser:submit(rectangle)
-- rect_optimiser:submit(rectangle)
end
for i = 0, tonumber(thread_impl.world_update_params_count) - 1 do
local wup = thread_impl.world_update_params.begin[i]
@ -56,11 +55,28 @@ function world_sync.on_world_update_host()
local end_y = wup.update_region.bottom_right.y
local rectangle = rect.Rectangle(start_x, start_y, end_x, end_y)
rect_optimiser:submit(rectangle)
-- rect_optimiser:submit(rectangle)
end
local px, py = EntityGetTransform(ctx.my_player.entity)
local ocx, ocy = math.floor(px / CHUNK_SIZE), math.floor(py / CHUNK_SIZE)
for cx = ocx-1,ocx+1 do
for cy = ocy-1,ocy+1 do
local chunk_id = cx.." "..cy
if initialized_chunks[chunk_id] == nil then
local crect = rect.Rectangle(cx * CHUNK_SIZE, cy * CHUNK_SIZE, (cx+1) * CHUNK_SIZE, (cy+1) * CHUNK_SIZE)
if DoesWorldExistAt(crect.left, crect.top, crect.right, crect.bottom) then
GamePrint("Sending chunk "..chunk_id)
initialized_chunks[chunk_id] = true
rect_optimiser:submit(crect)
end
end
end
end
if GameGetFrameNum() % 1 == 0 then
rect_optimiser:scan()
for crect in rect.parts(rect_optimiser:iterate(), 256) do
local area = nil
-- Make sure we don't send chunks that aren't loaded yet, like holy mountains before host got there.

29
scripts/mat_colors.py Normal file
View file

@ -0,0 +1,29 @@
mats_path = "/home/quant/.local/share/Steam/steamapps/compatdata/881100/pfx/dosdevices/c:/users/steamuser/AppData/LocalLow/Nolla_Games_Noita/data/materials.xml"
import xml.etree.ElementTree as ET
tree = ET.parse(mats_path)
root = tree.getroot()
colors = []
mat_data = open("mat_data.txt", "w")
for i, child in enumerate(root):
if child.tag not in ["CellData", "CellDataChild"]:
continue
name = child.get("name")
print(i, name, file=mat_data)
graphics = child.find("Graphics")
if graphics is not None:
color = graphics.get("color")
if color is not None:
colors.append(int(color[2:], 16))
else:
colors.append(0)
else:
colors.append(0)
with open("mat_colors.txt", "w") as f:
print(*colors, sep=" ", file=f)