aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-01-10 17:16:03 +0100
committertpart <tpart120@proton.me>2026-02-26 20:48:35 +0100
commitb634bad931f530ee0a207e1461ffc5e52ebb83e3 (patch)
tree90a45e8c67e6a7c66e49c7409f0c37881eade96e
parent0ed9e8387a66b3af78412feea62fdc8b9804f793 (diff)
downloadhurrycurry-b634bad931f530ee0a207e1461ffc5e52ebb83e3.tar
hurrycurry-b634bad931f530ee0a207e1461ffc5e52ebb83e3.tar.bz2
hurrycurry-b634bad931f530ee0a207e1461ffc5e52ebb83e3.tar.zst
compiles with tile stacks
-rw-r--r--data/maps/lobby.yaml80
-rw-r--r--server/bot/src/algos/customer.rs25
-rw-r--r--server/bot/src/algos/simple.rs33
-rw-r--r--server/data/src/lib.rs8
-rw-r--r--server/data/src/registry.rs14
-rw-r--r--server/editor/src/main.rs12
-rw-r--r--server/game-core/src/gamedata_index.rs2
-rw-r--r--server/game-core/src/interaction.rs30
-rw-r--r--server/game-core/src/lib.rs85
-rw-r--r--server/protocol/src/helpers.rs22
-rw-r--r--server/protocol/src/lib.rs8
-rw-r--r--server/registry/src/lobby.rs26
-rw-r--r--server/replaytool/src/replay.rs2
-rw-r--r--server/src/entity/campaign.rs2
-rw-r--r--server/src/entity/customers.rs14
-rw-r--r--server/src/entity/player_portal_pair.rs20
-rw-r--r--server/src/entity/tag_minigame.rs6
-rw-r--r--server/src/entity/tutorial.rs49
-rw-r--r--server/src/lib.rs2
-rw-r--r--server/src/server.rs40
-rw-r--r--server/tools/src/map_linter.rs8
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);