diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-01-10 17:16:03 +0100 |
|---|---|---|
| committer | tpart <tpart120@proton.me> | 2026-02-26 20:48:35 +0100 |
| commit | b634bad931f530ee0a207e1461ffc5e52ebb83e3 (patch) | |
| tree | 90a45e8c67e6a7c66e49c7409f0c37881eade96e | |
| parent | 0ed9e8387a66b3af78412feea62fdc8b9804f793 (diff) | |
| download | hurrycurry-b634bad931f530ee0a207e1461ffc5e52ebb83e3.tar hurrycurry-b634bad931f530ee0a207e1461ffc5e52ebb83e3.tar.bz2 hurrycurry-b634bad931f530ee0a207e1461ffc5e52ebb83e3.tar.zst | |
compiles with tile stacks
| -rw-r--r-- | data/maps/lobby.yaml | 80 | ||||
| -rw-r--r-- | server/bot/src/algos/customer.rs | 25 | ||||
| -rw-r--r-- | server/bot/src/algos/simple.rs | 33 | ||||
| -rw-r--r-- | server/data/src/lib.rs | 8 | ||||
| -rw-r--r-- | server/data/src/registry.rs | 14 | ||||
| -rw-r--r-- | server/editor/src/main.rs | 12 | ||||
| -rw-r--r-- | server/game-core/src/gamedata_index.rs | 2 | ||||
| -rw-r--r-- | server/game-core/src/interaction.rs | 30 | ||||
| -rw-r--r-- | server/game-core/src/lib.rs | 85 | ||||
| -rw-r--r-- | server/protocol/src/helpers.rs | 22 | ||||
| -rw-r--r-- | server/protocol/src/lib.rs | 8 | ||||
| -rw-r--r-- | server/registry/src/lobby.rs | 26 | ||||
| -rw-r--r-- | server/replaytool/src/replay.rs | 2 | ||||
| -rw-r--r-- | server/src/entity/campaign.rs | 2 | ||||
| -rw-r--r-- | server/src/entity/customers.rs | 14 | ||||
| -rw-r--r-- | server/src/entity/player_portal_pair.rs | 20 | ||||
| -rw-r--r-- | server/src/entity/tag_minigame.rs | 6 | ||||
| -rw-r--r-- | server/src/entity/tutorial.rs | 49 | ||||
| -rw-r--r-- | server/src/lib.rs | 2 | ||||
| -rw-r--r-- | server/src/server.rs | 40 | ||||
| -rw-r--r-- | server/tools/src/map_linter.rs | 8 |
21 files changed, 265 insertions, 223 deletions
diff --git a/data/maps/lobby.yaml b/data/maps/lobby.yaml index defab5f1..b8a4529d 100644 --- a/data/maps/lobby.yaml +++ b/data/maps/lobby.yaml @@ -40,46 +40,46 @@ tiles: # "6": table -i=dirty-plate # "7": table -i=plate:pizza:sliced-mushroom,sliced-cheese,tomato-juice # "8": table -i=plate:mushroom-soup - "~": floor -w --chef-spawn - ".": floor -w --customer-spawn - "'": grass -w - "g": cutting-board - "h": rolling-board - "e": stove -i=pot - "f": stove -i=pan - "x": book --book - "o": oven - "a": counter -i=foodprocessor - "Z": freezer -x - "z": counter -i=plate - "k": counter -i=glass - "i": deep-fryer -xi=basket - "S": screen + "~": [floor, --chef-spawn] + ".": [floor, --customer-spawn] + "'": [grass] + "g": [counter, cutting-board] + "h": [counter, rolling-board] + "e": [stove, -i=pot] + "f": [stove, -i=pan] + "x": [book, --book] + "o": [oven] + "a": [counter, -i=foodprocessor] + "Z": [freezer] + "z": [counter, -i=plate] + "k": [counter, -i=glass] + "i": [counter, deep-fryer, -i=basket] + "S": [screen] - "0": crate:rice -x - "1": crate:steak -x - "2": crate:tomato -x - "3": crate:leek -x - "4": crate:coconut -x - "5": crate:strawberry -x - "6": crate:flour -x - "7": crate:fish -x - "8": crate:cheese -x - "9": crate:lettuce -x - "¹": crate:mushroom -x - "²": crate:potato -x - "³": crate:bun -x - "¼": crate:noodles -x - "°": crate:tomato -x + "0": [crate:rice] + "1": [crate:steak] + "2": [crate:tomato] + "3": [crate:leek] + "4": [crate:coconut] + "5": [crate:strawberry] + "6": [crate:flour] + "7": [crate:fish] + "8": [crate:cheese] + "9": [crate:lettuce] + "¹": [crate:mushroom] + "²": [crate:potato] + "³": [crate:bun] + "¼": [crate:noodles] + "°": [crate:tomato] - "p": crate:plate -x - "G": crate:glass -x - "½": trash -x - "*": tree -c - "⌷": counter - "█": wall -c + "p": [crate:plate] + "G": [crate:glass] + "½": [trash] + "*": [tree] + "⌷": [counter] + "█": [wall] - "<": conveyor --conveyor=-1,0 - ">": conveyor --conveyor=1,0 - "‹": counter-window-conveyor --demand-sink - "›": counter-window-conveyor --demand-sink + "<": [conveyor, "--conveyor=-1,0"] + ">": [conveyor, "--conveyor=1,0"] + "‹": [counter-window-conveyor, --demand-sink] + "›": [counter-window-conveyor, --demand-sink] diff --git a/server/bot/src/algos/customer.rs b/server/bot/src/algos/customer.rs index ebc6e64a..0ed1ba9a 100644 --- a/server/bot/src/algos/customer.rs +++ b/server/bot/src/algos/customer.rs @@ -22,7 +22,7 @@ use crate::{ }; use hurrycurry_game_core::Game; use hurrycurry_protocol::{ - DemandIndex, Hand, ItemLocation, Message, PacketS, PlayerClass, PlayerID, Score, + DemandIndex, Hand, ItemLocation, Message, PacketS, PlayerClass, PlayerID, Score, TileIndex, glam::{IVec2, Vec2}, }; use log::debug; @@ -101,12 +101,14 @@ impl CustomerState { match self { CustomerState::New => { if !game.data.demands.is_empty() { + let chair_ti = game.data.get_tile_by_name("chair").unwrap_or(TileIndex(0)); // TODO let chairs = game - .tiles - .iter() - .filter(|(_, t)| game.data.tile_name(t.kind) == "chair") - .map(|(p, _)| *p) - .collect::<Vec<_>>(); + .tile_index + .get(&chair_ti) + .into_iter() + .flatten() + .copied() + .collect::<Vec<_>>(); //? maybe opt alloc if let Some(&chair) = chairs.get(random::<usize>(..) % chairs.len().max(1)) && let Some(path) = find_path(game, pos.as_ivec2(), chair) { @@ -135,12 +137,11 @@ impl CustomerState { let timeout = 90. + random_float() * 60.; let mut facing = Vec2::ZERO; for off in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] { - if game.tiles.get(&(off + *chair)).is_some_and(|t| { - game.data - .tile_placeable_items - .get(&t.kind) - .is_none_or(|p| p.contains(&requested_item)) - }) { + if game + .tiles + .get(&(off + *chair)) + .is_some_and(|t| game.data.can_place_item(&t.parts, requested_item)) + { facing = off.as_vec2(); } } diff --git a/server/bot/src/algos/simple.rs b/server/bot/src/algos/simple.rs index ba617707..5d19ed58 100644 --- a/server/bot/src/algos/simple.rs +++ b/server/bot/src/algos/simple.rs @@ -99,13 +99,10 @@ impl<S> Context<'_, S> { [IVec2::X, IVec2::Y, -IVec2::X, -IVec2::Y] .into_iter() .find(|off| { - self.game.tiles.get(&(pos + *off)).is_some_and(|t| { - self.game - .data - .tile_placeable_items - .get(&t.kind) - .is_none_or(|placable| placable.contains(item)) - }) + self.game + .tiles + .get(&(pos + *off)) + .is_some_and(|t| self.game.data.can_place_item(&t.parts, *item)) }) .map(|off| pos + off) .map(|pos| (*item, pos, timeout.remaining)) @@ -134,7 +131,7 @@ impl<S> Context<'_, S> { self.game .tiles .iter() - .find(|(_, t)| t.kind == tile) + .find(|(_, t)| t.parts.contains(&tile)) // TODO opt use tile index .map(|(p, _)| *p) } pub fn find_occupied_table_or_floor(&self) -> Option<IVec2> { @@ -143,10 +140,9 @@ impl<S> Context<'_, S> { .iter() .find(|(_, t)| { t.item.is_some() - && matches!( - self.game.data.tile_names[t.kind.0].as_str(), - "table" | "floor" - ) + && t.parts.iter().any(|p| { + matches!(self.game.data.tile_names[p.0].as_str(), "table" | "floor") + }) }) .map(|(p, _)| *p) } @@ -155,9 +151,10 @@ impl<S> Context<'_, S> { .tiles .iter() .find(|(_, t)| { - !self.game.data.tile_placeable_items.contains_key(&t.kind) - && t.item.is_none() - && self.game.data.tile_names[t.kind.0] == name + t.item.is_none() + && t.parts + .iter() + .any(|i| self.game.data.tile_names[i.0] == name) // TODO use index }) .map(|(p, _)| *p) } @@ -178,11 +175,7 @@ impl<S: State> Context<'_, S> { } } warn!("all counters filled up"); - self.game - .tiles - .iter() - .find(|(_, t)| !self.game.data.tile_placeable_items.contains_key(&t.kind)) // TODO filter by placable item - .map(|(p, _)| *p) + None } pub fn clear_tile(&mut self, pos: IVec2) -> LogicRes { debug!("clear tile {pos}"); diff --git a/server/data/src/lib.rs b/server/data/src/lib.rs index ef0f9bb6..80b2f75b 100644 --- a/server/data/src/lib.rs +++ b/server/data/src/lib.rs @@ -115,7 +115,7 @@ pub struct DemandDecl { #[derive(Debug, Clone, Default)] #[rustfmt::skip] pub struct Serverdata { - pub initial_map: HashMap<IVec2, (TileIndex, Option<ItemIndex>)>, + pub initial_map: HashMap<IVec2, (Vec<TileIndex>, Option<ItemIndex>)>, pub chef_spawn: Vec2, pub customer_spawn: Option<Vec2>, pub score_baseline: i64, @@ -152,6 +152,7 @@ fn build_data( let mut tile_walkable = HashSet::new(); let mut exclusive_tiles = BTreeMap::<TileIndex, HashSet<ItemIndex>>::new(); let mut tile_placeable_items = BTreeMap::new(); + let mut tile_placeable_any = HashSet::new(); let mut tile_interactable_empty = HashSet::new(); for (y, line) in map_in.map.iter().enumerate() { for (x, char) in line.chars().enumerate() { @@ -168,7 +169,7 @@ fn build_data( tiles_used.insert(tile); let item = tile_spec.item.clone().map(|i| reg.register_item(i)); items_used.extend(item); - initial_map.insert(pos, (tile, item)); + initial_map.insert(pos, (vec![tile], item)); if tile_spec.chef_spawn { chef_spawn = Some(pos.as_vec2() + Vec2::splat(0.5)); @@ -285,8 +286,9 @@ fn build_data( let mut data = Gamedata { current_map: map_name, maps, - tile_walkable, + tile_collide: tile_walkable, tile_placeable_items, + tile_placeable_any, tile_interactable_empty, flags: map_in.flags, recipes, diff --git a/server/data/src/registry.rs b/server/data/src/registry.rs index 14a06b93..4de672d0 100644 --- a/server/data/src/registry.rs +++ b/server/data/src/registry.rs @@ -74,8 +74,8 @@ pub(crate) fn filter_unused_tiles_and_items(data: &mut Gamedata, serverdata: &mu for rg in serverdata.recipe_groups.values() { used_items.extend(rg); } - for &(tile, item) in serverdata.initial_map.values() { - used_tiles.insert(tile); + for (tile, item) in serverdata.initial_map.values() { + used_tiles.extend(tile); used_items.extend(item); } for e in &serverdata.entity_decls { @@ -185,8 +185,10 @@ pub(crate) fn filter_unused_tiles_and_items(data: &mut Gamedata, serverdata: &mu for rg in serverdata.recipe_groups.values_mut() { *rg = rg.clone().into_iter().map(|e| item_map[&e]).collect(); } - for (tile, item) in serverdata.initial_map.values_mut() { - *tile = tile_map[tile]; + for (tiles, item) in serverdata.initial_map.values_mut() { + for tile in tiles { + *tile = tile_map[tile]; + } if let Some(item) = item { *item = item_map[item] } @@ -218,8 +220,8 @@ pub(crate) fn filter_unused_tiles_and_items(data: &mut Gamedata, serverdata: &mu _ => (), }; } - data.tile_walkable = data - .tile_walkable + data.tile_collide = data + .tile_collide .clone() .into_iter() .map(|e| tile_map[&e]) diff --git a/server/editor/src/main.rs b/server/editor/src/main.rs index 86cb4b40..1751b1f5 100644 --- a/server/editor/src/main.rs +++ b/server/editor/src/main.rs @@ -143,7 +143,7 @@ async fn handle_conn( }); state.out.push(PacketC::Data { data: Box::new(Gamedata { - tile_walkable: (0..TILES.len()).map(TileIndex).collect(), + tile_collide: (0..TILES.len()).map(TileIndex).collect(), tile_placeable_items: BTreeMap::new(), tile_names: TILES.iter().map(|(name, _, _)| name.to_string()).collect(), current_map: "editor".to_owned(), @@ -235,17 +235,9 @@ impl State { if !self.dirty_tiles.is_empty() { for p in self.dirty_tiles.drain() { self.out.push(PacketC::UpdateMap { - tile: p, - kind: self.tiles.get(&p).copied(), - neighbors: [ - self.tiles.get(&(p + IVec2::NEG_Y)).copied(), - self.tiles.get(&(p + IVec2::NEG_X)).copied(), - self.tiles.get(&(p + IVec2::Y)).copied(), - self.tiles.get(&(p + IVec2::X)).copied(), - ], + changes: vec![(p, self.tiles.get(&p).copied().into_iter().collect())], }) } - self.out.push(PacketC::FlushMap); } } pub fn build_start_platform(&mut self) { diff --git a/server/game-core/src/gamedata_index.rs b/server/game-core/src/gamedata_index.rs index 8aa5e4af..d4bbcce1 100644 --- a/server/game-core/src/gamedata_index.rs +++ b/server/game-core/src/gamedata_index.rs @@ -30,7 +30,7 @@ impl GamedataIndex { self.tile_collide.clear(); for tile in (0..data.tile_names.len()).map(TileIndex) { - self.tile_collide.push(!data.tile_walkable.contains(&tile)); + self.tile_collide.push(!data.tile_collide.contains(&tile)); } for (ri, r) in data.recipes() { diff --git a/server/game-core/src/interaction.rs b/server/game-core/src/interaction.rs index 275e87d1..28efc43e 100644 --- a/server/game-core/src/interaction.rs +++ b/server/game-core/src/interaction.rs @@ -33,9 +33,13 @@ impl Game { ItemLocation::Player(pid, _) => Some(pid), _ => None, }; - let this_tile_kind = match this_loc { - ItemLocation::Tile(t) => self.tiles.get(&t).map(|t| t.kind), - _ => None, + let this_tile_parts = match this_loc { + ItemLocation::Tile(t) => self + .tiles + .get(&t) + .map(|t| t.parts.clone()) + .unwrap_or_default(), + _ => Vec::new(), }; let (this_slot, other_slot) = match (this_loc, other_loc) { @@ -94,7 +98,7 @@ impl Game { && let Some(inv) = &mut item.active { let recipe = &self.data.recipe(inv.recipe); - if recipe.supports_tile(this_tile_kind) + if this_tile_parts.iter().any(|p| recipe.supports_tile(*p)) && let Recipe::Active { outputs, speed, .. } = recipe { if edge { @@ -142,7 +146,7 @@ impl Game { return Ok(()); } for (ri, recipe) in self.data.recipes() { - if !recipe.supports_tile(this_tile_kind) { + if !this_tile_parts.iter().any(|p| recipe.supports_tile(*p)) { continue; } match recipe { @@ -238,7 +242,7 @@ impl Game { } let can_place = automated - || this_tile_kind.is_none_or(|tile| { + || this_tile_parts.iter().any(|tile| { other_slot.as_ref().is_some_and(|other| { self.data .tile_placeable_items @@ -276,9 +280,13 @@ impl Game { } pub fn tick_slot(&mut self, loc: ItemLocation, dt: f32) -> Result<(), TrError> { - let tile = match loc { - ItemLocation::Tile(t) => self.tiles.get(&t).map(|t| t.kind), - _ => None, + let parts = match loc { + ItemLocation::Tile(t) => self + .tiles + .get(&t) + .map(|t| t.parts.clone()) + .unwrap_or_default(), + _ => Vec::new(), }; let slot = match loc { ItemLocation::Tile(p) => { @@ -303,7 +311,7 @@ impl Game { let r = &self.data.recipe(a.recipe); let prev_speed = a.speed; - if r.supports_tile(tile) { + if parts.iter().any(|p| r.supports_tile(*p)) { if a.speed <= 0. && let Recipe::Passive { speed, .. } = &self.data.recipe(a.recipe) { @@ -355,7 +363,7 @@ impl Game { } else if let Some(recipes) = self.data_index.recipe_passive_by_input.get(&item.kind) { for &ri in recipes { let recipe = self.data.recipe(ri); - if recipe.supports_tile(tile) + if parts.iter().any(|p| recipe.supports_tile(*p)) && let Recipe::Passive { input, warn, speed, .. } = recipe diff --git a/server/game-core/src/lib.rs b/server/game-core/src/lib.rs index a0d4d05d..bee97652 100644 --- a/server/game-core/src/lib.rs +++ b/server/game-core/src/lib.rs @@ -47,7 +47,7 @@ pub struct Item { } pub struct Tile { - pub kind: TileIndex, + pub parts: Vec<TileIndex>, pub item: Option<Item>, } @@ -82,6 +82,7 @@ pub struct Game { pub item_locations_index: HashSet<ItemLocation>, pub events: VecDeque<PacketC>, + pub map_changes: HashSet<IVec2>, } impl Game { @@ -165,12 +166,10 @@ impl Game { }); } } - PacketC::UpdateMap { - tile, - kind, - neighbors: _, - } => { - self.set_tile(tile, kind); + PacketC::UpdateMap { changes } => { + for (pos, tiles) in changes { + self.set_tile(pos, tiles); + } } PacketC::Communicate { player, @@ -196,32 +195,58 @@ impl Game { } } - pub fn set_tile(&mut self, pos: IVec2, kind: Option<TileIndex>) { + pub fn set_tile(&mut self, pos: IVec2, parts: Vec<TileIndex>) { self.tiles.remove(&pos); self.walkable.remove(&pos); - if let Some(prev) = self.tiles.get(&pos) - && let Some(set) = self.tile_index.get_mut(&prev.kind) - { - set.remove(&pos); - } - if let Some(kind) = kind { - self.tiles.insert(pos, Tile { kind, item: None }); - if !self.data_index.tile_collide[kind.0] { - self.walkable.insert(pos); + if let Some(prev) = self.tiles.get(&pos) { + for &part in &prev.parts { + self.tile_index.entry(part).or_default().remove(&pos); } - self.tile_index.entry(kind).or_default().insert(pos); } - self.events.push_back(PacketC::UpdateMap { - tile: pos, - kind, - neighbors: [ - self.tiles.get(&(pos + IVec2::NEG_Y)).map(|e| e.kind), - self.tiles.get(&(pos + IVec2::NEG_X)).map(|e| e.kind), - self.tiles.get(&(pos + IVec2::Y)).map(|e| e.kind), - self.tiles.get(&(pos + IVec2::X)).map(|e| e.kind), - ], + if self.data.walkable(&parts) { + self.walkable.insert(pos); + } + for &part in &parts { + self.tile_index.entry(part).or_default().insert(pos); + } + if !parts.is_empty() { + self.tiles.insert(pos, Tile { parts, item: None }); + } + self.map_changes.insert(pos); + } + pub fn add_tile_part(&mut self, pos: IVec2, part: TileIndex) { + self.map_changes.insert(pos); + let tile = self.tiles.entry(pos).or_insert(Tile { + item: None, + parts: vec![], }); + if tile.parts.contains(&part) { + return; + } + tile.parts.push(part); + self.tile_index.entry(part).or_default().insert(pos); + self.walkable.remove(&pos); + if self.data.walkable(&tile.parts) { + self.walkable.insert(pos); + } } + pub fn remove_tile_part(&mut self, pos: IVec2, part: TileIndex) { + let Some(tile) = self.tiles.get_mut(&pos) else { + return; + }; + let lb = tile.parts.len(); + tile.parts.retain(|&p| p != part); + if tile.parts.len() == lb { + return; + } + self.map_changes.insert(pos); + self.tile_index.entry(part).or_default().remove(&pos); + self.walkable.remove(&pos); + if self.data.walkable(&tile.parts) { + self.walkable.insert(pos); + } + } + pub fn set_item(&mut self, pos: IVec2, kind: Option<ItemIndex>) { let Some(tile) = self.tiles.get_mut(&pos) else { return; @@ -292,9 +317,3 @@ impl Game { PlayerID(self.player_id_counter) } } - -impl From<TileIndex> for Tile { - fn from(kind: TileIndex) -> Self { - Self { kind, item: None } - } -} diff --git a/server/protocol/src/helpers.rs b/server/protocol/src/helpers.rs index 679e6301..1be37a36 100644 --- a/server/protocol/src/helpers.rs +++ b/server/protocol/src/helpers.rs @@ -45,6 +45,20 @@ impl Gamedata { .enumerate() .map(|(i, e)| (RecipeIndex(i), e)) } + pub fn can_place_item(&self, tiles: &[TileIndex], item: ItemIndex) -> bool { + for t in tiles.iter().rev() { + if let Some(whitelist) = self.tile_placeable_items.get(t) { + return whitelist.contains(&item); + } + if self.tile_placeable_any.contains(t) { + return true; + } + } + false + } + pub fn walkable(&self, tiles: &[TileIndex]) -> bool { + tiles.iter().any(|t| self.tile_collide.contains(t)) + } } impl Recipe { @@ -92,13 +106,9 @@ impl Recipe { a.iter().chain(b.iter()).copied() } - pub fn supports_tile(&self, tile: Option<TileIndex>) -> bool { + pub fn supports_tile(&self, tile: TileIndex) -> bool { if let Some(tile_constraint) = self.tile() { - if let Some(tile) = tile { - tile == tile_constraint - } else { - false - } + tile == tile_constraint } else { true } diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs index d7cd2cd9..8a6e282e 100644 --- a/server/protocol/src/lib.rs +++ b/server/protocol/src/lib.rs @@ -87,9 +87,10 @@ pub struct Gamedata { pub current_map: String, pub item_names: Vec<String>, pub tile_names: Vec<String>, - pub tile_walkable: HashSet<TileIndex>, + pub tile_collide: HashSet<TileIndex>, #[serde(deserialize_with = "deser_tile_index_map")] pub tile_placeable_items: BTreeMap<TileIndex, HashSet<ItemIndex>>, + pub tile_placeable_any: HashSet<TileIndex>, pub tile_interactable_empty: HashSet<TileIndex>, pub maps: Vec<(String, MapMetadata)>, pub bot_algos: Vec<String>, @@ -257,11 +258,8 @@ pub enum PacketC { warn: bool, }, UpdateMap { - tile: IVec2, - kind: Option<TileIndex>, - neighbors: [Option<TileIndex>; 4], + changes: Vec<(IVec2, Vec<TileIndex>)>, }, - FlushMap, Communicate { player: PlayerID, message: Option<Message>, diff --git a/server/registry/src/lobby.rs b/server/registry/src/lobby.rs index 8492fac6..3a3880f6 100644 --- a/server/registry/src/lobby.rs +++ b/server/registry/src/lobby.rs @@ -56,15 +56,15 @@ async fn handle_conn(sock: TcpStream, addr: SocketAddr, entries: &[Entry]) -> Re vec2(t.sin() * radius, t.cos() * radius).round().as_ivec2() }; - let mut tiles = HashMap::<IVec2, TileIndex>::new(); + let mut tiles = HashMap::<IVec2, Vec<TileIndex>>::new(); let bounds = (radius + 5.).ceil() as i32; for x in -bounds..bounds { for y in -bounds..bounds { - tiles.insert(ivec2(x, y), TileIndex(0)); + tiles.insert(ivec2(x, y), vec![TileIndex(0)]); } } for (i, _) in entries.iter().enumerate() { - tiles.insert(portal_location(i), TileIndex(1)); + tiles.insert(portal_location(i), vec![TileIndex(1)]); } let mut out = Vec::new(); @@ -75,22 +75,22 @@ async fn handle_conn(sock: TcpStream, addr: SocketAddr, entries: &[Entry]) -> Re }); out.push(PacketC::Data { data: Box::new(Gamedata { - tile_walkable: (0..TILES.len()).map(TileIndex).collect(), + tile_collide: (0..TILES.len()).map(TileIndex).collect(), tile_placeable_items: BTreeMap::new(), tile_names: TILES.iter().map(|(s, _)| s.to_string()).collect(), current_map: "registry".to_owned(), ..Default::default() }), }); - let walkable = HashSet::from_iter(tiles.iter().filter(|(_, v)| !TILES[v.0].1).map(|(k, _)| *k)); - for (&tile, &kind) in &tiles { - out.push(PacketC::UpdateMap { - tile, - kind: Some(kind), - neighbors: [None, None, None, None], - }); - } - out.push(PacketC::FlushMap); + let walkable = HashSet::from_iter( + tiles + .iter() + .filter(|(_, v)| !v.iter().any(|p| TILES[p.0].1)) + .map(|(k, _)| *k), + ); + out.push(PacketC::UpdateMap { + changes: tiles.clone().into_iter().collect(), + }); out.push(PacketC::SetIngame { state: true, lobby: false, // very ironic diff --git a/server/replaytool/src/replay.rs b/server/replaytool/src/replay.rs index 09c7e639..0f465c54 100644 --- a/server/replaytool/src/replay.rs +++ b/server/replaytool/src/replay.rs @@ -109,7 +109,7 @@ pub async fn replay(ws_listener: &TcpListener, input: &Path) -> Result<()> { send(PacketC::ReplayStop).await?; next_event = Event { ts: f64::INFINITY, - packet: PacketC::FlushMap, + packet: PacketC::ReplayStop, }; continue; }; diff --git a/server/src/entity/campaign.rs b/server/src/entity/campaign.rs index b7d7d8db..af6de391 100644 --- a/server/src/entity/campaign.rs +++ b/server/src/entity/campaign.rs @@ -60,7 +60,7 @@ impl Entity for Gate { self.active = false; self.unlocked = self.condition.check(c.scoreboard); if !self.unlocked { - c.game.set_tile(self.pos, Some(self.blocker_tile)); + c.game.set_tile(self.pos, vec![self.blocker_tile]); } } Ok(()) diff --git a/server/src/entity/customers.rs b/server/src/entity/customers.rs index c1bfb525..2e2f82af 100644 --- a/server/src/entity/customers.rs +++ b/server/src/entity/customers.rs @@ -20,7 +20,7 @@ use crate::random_float; use anyhow::Result; use hurrycurry_bot::algos::Customer; use hurrycurry_locale::TrError; -use hurrycurry_protocol::{Character, PlayerClass}; +use hurrycurry_protocol::{Character, PlayerClass, TileIndex}; use std::random::random; pub struct Customers { @@ -47,10 +47,14 @@ impl Entity for Customers { fn tick(&mut self, mut c: EntityContext) -> Result<(), TrError> { let chairs = *self.chair_count.get_or_insert_with(|| { c.game - .tiles - .values() - .filter(|t| c.game.data.tile_name(t.kind) == "chair") - .count() + .tile_index + .get( + &c.game + .data + .get_tile_by_name("chair") + .unwrap_or(TileIndex(0)), + ) + .map_or(0, |c| c.len()) }); let max_count = (self.scaling_factor * chairs as f32).max(1.) as usize; diff --git a/server/src/entity/player_portal_pair.rs b/server/src/entity/player_portal_pair.rs index fd28b774..b525f4ca 100644 --- a/server/src/entity/player_portal_pair.rs +++ b/server/src/entity/player_portal_pair.rs @@ -76,23 +76,31 @@ impl Entity for PlayerPortalPair { if (self.state == 1 && near_b.is_none() && in_a.is_none()) || (self.state == -1 && near_a.is_none() && in_b.is_none()) { - c.game.set_tile(self.b_i, Some(self.neutral_tile)); - c.game.set_tile(self.a_i, Some(self.neutral_tile)); + c.game.remove_tile_part(self.a_i, self.in_tile); + c.game.remove_tile_part(self.a_i, self.out_tile); + c.game.add_tile_part(self.a_i, self.neutral_tile); + c.game.remove_tile_part(self.b_i, self.in_tile); + c.game.remove_tile_part(self.b_i, self.out_tile); + c.game.add_tile_part(self.b_i, self.neutral_tile); self.state = 0; return Ok(()); } if self.state == 0 && in_a.is_some() { - c.game.set_tile(self.a_i, Some(self.in_tile)); - c.game.set_tile(self.b_i, Some(self.out_tile)); + c.game.remove_tile_part(self.a_i, self.neutral_tile); + c.game.remove_tile_part(self.b_i, self.neutral_tile); + c.game.add_tile_part(self.a_i, self.in_tile); + c.game.add_tile_part(self.b_i, self.out_tile); self.state = 1; self.delay = 0.2; return Ok(()); } if self.state == 0 && in_b.is_some() { - c.game.set_tile(self.b_i, Some(self.in_tile)); - c.game.set_tile(self.a_i, Some(self.out_tile)); + c.game.remove_tile_part(self.b_i, self.neutral_tile); + c.game.remove_tile_part(self.a_i, self.neutral_tile); + c.game.add_tile_part(self.b_i, self.in_tile); + c.game.add_tile_part(self.a_i, self.out_tile); self.state = -1; self.delay = 0.2; return Ok(()); diff --git a/server/src/entity/tag_minigame.rs b/server/src/entity/tag_minigame.rs index e45422b7..48558b41 100644 --- a/server/src/entity/tag_minigame.rs +++ b/server/src/entity/tag_minigame.rs @@ -100,7 +100,7 @@ impl Entity for TagMinigame { .filter(|(_, v)| **v <= 0.) .for_each(|(&pos, _)| { self.wall_remove_timeouts.insert(pos, 3.); - c.game.set_tile(pos, Some(self.blocker_tile)); + c.game.add_tile_part(pos, self.blocker_tile); }); self.wall_place_delays.retain(|_, v| *v > 0.); @@ -111,9 +111,7 @@ impl Entity for TagMinigame { .iter() .filter(|(_, v)| **v <= 0.) .for_each(|(&pos, _)| { - if let Some(&(tile, _)) = c.serverdata.initial_map.get(&pos) { - c.game.set_tile(pos, Some(tile)); - } + c.game.remove_tile_part(pos, self.blocker_tile); }); self.wall_remove_timeouts.retain(|_, v| *v > 0.); diff --git a/server/src/entity/tutorial.rs b/server/src/entity/tutorial.rs index d1390b20..87896a09 100644 --- a/server/src/entity/tutorial.rs +++ b/server/src/entity/tutorial.rs @@ -169,10 +169,9 @@ impl StepContext<'_> { fn find_tile(&self, tile: TileIndex) -> Option<IVec2> { self.ent .game - .tiles - .iter() - .find(|(_, t)| t.kind == tile) - .map(|(p, _)| *p) + .tile_index + .get(&tile) + .and_then(|s| s.iter().next().copied()) } fn aquire_placed_item(&mut self, item: ItemIndex) -> Result<IVec2, (Option<IVec2>, Message)> { debug!( @@ -193,9 +192,14 @@ impl StepContext<'_> { }) { Err(( Some(*pos), - match self.ent.game.data.tile_name(tile.kind) { - "stove" | "oven" => trm!("s.tutorial.prevent_burning"), - _ => trm!("s.tutorial.take_now"), + if tile + .parts + .iter() + .any(|p| matches!(self.ent.game.data.tile_name(*p), "stove" | "oven")) + { + trm!("s.tutorial.prevent_burning") + } else { + trm!("s.tutorial.take_now") }, )) } else { @@ -288,11 +292,16 @@ impl StepContext<'_> { speed, .. } => { - for (pos, tile) in self.ent.game.tiles.iter().filter(|(_, t)| t.kind == *tile) { - if let Some(item) = &tile.item - && item.kind == *input - { - return Err((Some(*pos), trm!("s.tutorial.hold_interact"))); + if let Some(tiles) = self.ent.game.tile_index.get(tile) { + for pos in tiles { + let Some(tile) = self.ent.game.tiles.get(pos) else { + continue; + }; + if let Some(item) = &tile.item + && item.kind == *input + { + return Err((Some(*pos), trm!("s.tutorial.hold_interact"))); + } } } if let Some(pos) = self.find_tile(*tile) { @@ -312,12 +321,16 @@ impl StepContext<'_> { input, .. } => { - for (_pos, tile) in self.ent.game.tiles.iter().filter(|(_, t)| t.kind == *tile) - { - if let Some(item) = &tile.item - && item.kind == *input - { - return Err((None, trm!("s.tutorial.wait_finish"))); + if let Some(tiles) = self.ent.game.tile_index.get(tile) { + for pos in tiles { + let Some(tile) = self.ent.game.tiles.get(pos) else { + continue; + }; + if let Some(item) = &tile.item + && item.kind == *input + { + return Err((None, trm!("s.tutorial.wait_finish"))); + } } } if let Some(pos) = self.find_tile(*tile) { diff --git a/server/src/lib.rs b/server/src/lib.rs index e5b78644..c166e665 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -16,13 +16,13 @@ */ #![feature(if_let_guard, iterator_try_collect, stmt_expr_attributes, random)] +pub mod benchmark; pub mod commands; pub mod entity; pub mod network; pub mod scoreboard; pub mod server; pub mod state; -pub mod benchmark; use hurrycurry_protocol::glam::Vec2; use std::{fmt::Display, random::random}; diff --git a/server/src/server.rs b/server/src/server.rs index 49b9cbd7..bd1d0f86 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -32,7 +32,7 @@ use hurrycurry_locale::{ use hurrycurry_protocol::{ Character, Gamedata, Hand, ItemLocation, Menu, MessageTimeout, PacketC, PacketS, PlayerClass, PlayerID, Score, - glam::{IVec2, Vec2}, + glam::Vec2, movement::MovementBase, }; use log::{info, warn}; @@ -155,13 +155,9 @@ impl GameServerExt for Game { self.players_spatial_index.remove_entry(id); packet_out.push_back(PacketC::RemovePlayer { id }) } - for (pos, _) in self.tiles.drain() { - packet_out.push_back(PacketC::UpdateMap { - tile: pos, - kind: None, - neighbors: [None, None, None, None], - }) - } + packet_out.push_back(PacketC::UpdateMap { + changes: self.tiles.keys().map(|&p| (p, Vec::new())).collect(), + }); self.score = Score::default(); self.environment_effects.clear(); self.walkable.clear(); @@ -193,21 +189,23 @@ impl GameServerExt for Game { ..Default::default() }; - for (&p, (tile, item)) in &serverdata.initial_map { + for (&p, (tiles, item)) in &serverdata.initial_map { self.tiles.insert( p, Tile { - kind: *tile, + parts: tiles.to_vec(), item: item.map(|i| Item { kind: i, active: None, }), }, ); - if !self.data_index.tile_collide[tile.0] { + if !self.data.walkable(tiles) { self.walkable.insert(p); } - self.tile_index.entry(*tile).or_default().insert(p); + for tile in tiles { + self.tile_index.entry(*tile).or_default().insert(p); + } if item.is_some() { self.item_locations_index.insert(ItemLocation::Tile(p)); } @@ -272,17 +270,14 @@ impl GameServerExt for Game { }) } } + out.push(PacketC::UpdateMap { + changes: self + .tiles + .iter() + .map(|(pos, tile)| (*pos, tile.parts.clone())) + .collect(), + }); for (&tile, tdata) in &self.tiles { - out.push(PacketC::UpdateMap { - tile, - neighbors: [ - self.tiles.get(&(tile + IVec2::NEG_Y)).map(|e| e.kind), - self.tiles.get(&(tile + IVec2::NEG_X)).map(|e| e.kind), - self.tiles.get(&(tile + IVec2::Y)).map(|e| e.kind), - self.tiles.get(&(tile + IVec2::X)).map(|e| e.kind), - ], - kind: Some(tdata.kind), - }); if let Some(item) = &tdata.item { out.push(PacketC::SetItem { location: ItemLocation::Tile(tile), @@ -306,7 +301,6 @@ impl GameServerExt for Game { } } } - out.push(PacketC::FlushMap); out.push(PacketC::Score(self.score.clone())); out.push(PacketC::SetIngame { state: true, diff --git a/server/tools/src/map_linter.rs b/server/tools/src/map_linter.rs index 0f5e3364..41f1e925 100644 --- a/server/tools/src/map_linter.rs +++ b/server/tools/src/map_linter.rs @@ -191,7 +191,7 @@ pub fn check_map(map: &str) -> Result<()> { match mode { Normal => { - if data.tile_walkable.contains(&tile) { + if data.tile_collide.contains(&tile) { warnings.push(trm!("s.tool.map_linter.walkable", t = tile)); } if data.tile_placeable_items.contains_key(&tile) { @@ -216,7 +216,7 @@ pub fn check_map(map: &str) -> Result<()> { } } Walkable => { - if !data.tile_walkable.contains(&tile) { + if !data.tile_collide.contains(&tile) { warnings.push(trm!("s.tool.map_linter.not_walkable", t = tile)); } } @@ -245,7 +245,7 @@ pub fn check_map(map: &str) -> Result<()> { )); } if NEED_EASY_ACCESS.contains(&tile_name) - && !has_neighbour(&serverdata, pos, |t| data.tile_walkable.contains(&t)) + && !has_neighbour(&serverdata, pos, |t| data.tile_collide.contains(&t)) { warnings.push(trm!( "s.tool.map_linter.no_easy_access", @@ -349,7 +349,7 @@ fn fill_walkable(d: &Gamedata, sd: &Serverdata, start: IVec2) -> HashSet<IVec2> if sd .initial_map .get(&npos) - .is_some_and(|(x, _)| d.tile_walkable.contains(x)) + .is_some_and(|(x, _)| d.tile_collide.contains(x)) && !visited.contains(&npos) { open.insert(npos); |