mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Merge pull request #10383 from voidsipp/fix/10369-map-x-offscreen
Keep treasure map targets when carrying undecorated copies
This commit is contained in:
6 files changed
+153
-59
No files matched your search
@@ -356,7 +356,7 @@ mod tests {
|
||||
y,
|
||||
rot,
|
||||
name: None,
|
||||
key: Key::Item,
|
||||
key: Key::Remote,
|
||||
};
|
||||
// The centre, facing north: 8 map pixels wide, nudged by (-0.5, 0.5).
|
||||
close(
|
||||
|
||||
@@ -726,6 +726,12 @@ impl Game<'_> {
|
||||
);
|
||||
out.push('\n');
|
||||
}
|
||||
"map_carried" => {
|
||||
self.harness_owner(|g| {
|
||||
g.tick_maps();
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
"craft_menu_click" | "craft_menu_state" | "craft_menu_close" => {
|
||||
let owner = self.serving.as_ref().ok_or("no server")?.owner;
|
||||
let state = self.in_owner_level(|g| -> Result<Value> {
|
||||
@@ -824,10 +830,21 @@ impl Game<'_> {
|
||||
let id = op["id"].as_i64().ok_or("command harness: map id")? as i32;
|
||||
let value = self.harness_owner(|g| {
|
||||
Ok(match g.map(id) {
|
||||
Some(m) => format!(
|
||||
"scale={},locked={},center={},{}",
|
||||
m.scale, m.locked, m.center[0], m.center[1]
|
||||
),
|
||||
Some(m) => {
|
||||
let mut value = format!(
|
||||
"scale={},locked={},center={},{}",
|
||||
m.scale, m.locked, m.center[0], m.center[1]
|
||||
);
|
||||
if op["red_x"].as_bool().unwrap_or(false) {
|
||||
let count = m
|
||||
.decorations
|
||||
.iter()
|
||||
.filter(|d| d.sprite == "map/decorations/red_x")
|
||||
.count();
|
||||
value.push_str(&format!(",red_x={count}"));
|
||||
}
|
||||
value
|
||||
}
|
||||
None => "missing".to_owned(),
|
||||
})
|
||||
})?;
|
||||
|
||||
@@ -11,19 +11,20 @@ use horizon_client::{
|
||||
items::ItemStack,
|
||||
mesher::Vertex,
|
||||
};
|
||||
use horizon_world::nbt::Tag;
|
||||
use horizon_world::nbt::{JavaString, Tag};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// What a decoration is filed under in `MapItemSavedData.decorations` (a
|
||||
/// `LinkedHashMap<String, MapDecoration>`): a player's marker by their
|
||||
/// connection, the banner at a position (`MapBanner.getId`), a framed map's
|
||||
/// frame by its entity (`getFrameKey`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) enum Key {
|
||||
Player(u32),
|
||||
/// An item's `minecraft:map_decorations` (an explorer map's target), or a
|
||||
/// decoration a server sent (`addClientSideDecorations`).
|
||||
Item,
|
||||
/// A named entry of an item's `minecraft:map_decorations`.
|
||||
Item(JavaString),
|
||||
/// A decoration a server sent (`addClientSideDecorations`).
|
||||
Remote,
|
||||
Banner([i32; 3]),
|
||||
Frame(i32),
|
||||
}
|
||||
@@ -301,8 +302,8 @@ impl MapData {
|
||||
self.holders.retain(|h| h.holds(now));
|
||||
let holders = &self.holders;
|
||||
let before = self.decorations.len();
|
||||
self.decorations.retain(|d| match d.key {
|
||||
Key::Player(w) => holders.iter().any(|h| h.who == w),
|
||||
self.decorations.retain(|d| match &d.key {
|
||||
Key::Player(w) => holders.iter().any(|h| h.who == *w),
|
||||
_ => true,
|
||||
});
|
||||
if self.decorations.len() != before {
|
||||
@@ -365,7 +366,11 @@ impl MapData {
|
||||
name,
|
||||
key,
|
||||
};
|
||||
match self.decorations.iter().position(|d| d.key == key) {
|
||||
match self
|
||||
.decorations
|
||||
.iter()
|
||||
.position(|d| d.key == decoration.key)
|
||||
{
|
||||
Some(i) if self.decorations[i] == decoration => {}
|
||||
Some(i) => {
|
||||
self.decorations[i] = decoration;
|
||||
@@ -377,6 +382,24 @@ impl MapData {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `tickCarriedBy` adds only missing component keys. Another carried
|
||||
/// copy without those entries must not erase an explorer map's target.
|
||||
fn add_item_decorations(&mut self, targets: Vec<StaticDecoration>, game_time: i64) {
|
||||
for (name, sprite, x, z, rotation) in targets {
|
||||
let key = Key::Item(name);
|
||||
if !self.decorations.iter().any(|d| d.key == key) {
|
||||
self.add_decoration(
|
||||
sprite,
|
||||
Some(game_time),
|
||||
key,
|
||||
x,
|
||||
z,
|
||||
f64::from(rotation),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `MapItemSavedData.tickCarriedBy`'s check on every holder but `who`, the
|
||||
/// player ticking: one wearing map invisibility gear has no marker.
|
||||
fn hide_invisible(&mut self, who: u32) {
|
||||
@@ -785,9 +808,11 @@ fn decoration_type(sprite: &str) -> Option<&'static str> {
|
||||
.find(|(_, s)| *s == sprite)
|
||||
.map(|(name, _)| *name)
|
||||
}
|
||||
/// `minecraft:map_decorations` of a map item: (sprite, x, z, rotation), in
|
||||
/// the component's order.
|
||||
fn static_decorations(stack: &ItemStack) -> Vec<(&'static str, f64, f64, f32)> {
|
||||
/// A named `minecraft:map_decorations` entry: key, sprite, x, z, rotation.
|
||||
type StaticDecoration = (JavaString, &'static str, f64, f64, f32);
|
||||
|
||||
/// `minecraft:map_decorations` of a map item, in the component's order.
|
||||
fn static_decorations(stack: &ItemStack) -> Vec<StaticDecoration> {
|
||||
let Some(Tag::Compound(entries)) = stack.data().and_then(|d| {
|
||||
d.other
|
||||
.iter()
|
||||
@@ -803,13 +828,14 @@ fn static_decorations(stack: &ItemStack) -> Vec<(&'static str, f64, f64, f32)> {
|
||||
_ => None,
|
||||
};
|
||||
entries
|
||||
.values()
|
||||
.filter_map(|e| {
|
||||
.iter()
|
||||
.filter_map(|(key, e)| {
|
||||
let sprite = match e.get("type") {
|
||||
Some(Tag::String(s)) => decoration_sprite(&s.to_string_lossy())?,
|
||||
_ => return None,
|
||||
};
|
||||
Some((
|
||||
key.clone(),
|
||||
sprite,
|
||||
number(e.get("x"))?,
|
||||
number(e.get("z"))?,
|
||||
@@ -908,7 +934,7 @@ impl Game<'_> {
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|t| horizon_client::entity_nbt::name_text(t).plain()),
|
||||
key: Key::Item,
|
||||
key: Key::Remote,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -1337,7 +1363,7 @@ impl Game<'_> {
|
||||
.flatten()
|
||||
.filter_map(|i| self.stack_in(i).and_then(map_id))
|
||||
.collect();
|
||||
let carried: Vec<(i32, Vec<(&'static str, f64, f64, f32)>)> = self
|
||||
let carried: Vec<(i32, Vec<StaticDecoration>)> = self
|
||||
.inventory
|
||||
.iter()
|
||||
.chain(std::iter::once(&self.offhand))
|
||||
@@ -1345,7 +1371,7 @@ impl Game<'_> {
|
||||
.filter_map(|s| Some((map_id(s)?, static_decorations(s))))
|
||||
.collect();
|
||||
let (yaw, dim) = (self.yaw, self.dimension_id());
|
||||
let (nether, game_time) = (self.dim == super::Dim::Nether, self.world.game_time);
|
||||
let game_time = self.world.game_time;
|
||||
let invisible = self.map_invisibility_worn(&self.armor);
|
||||
// The guest whose turn it is, else the host.
|
||||
let me = self.lan_acting.unwrap_or(HOST);
|
||||
@@ -1370,39 +1396,7 @@ impl Game<'_> {
|
||||
m.remove_decoration(Key::Player(me));
|
||||
}
|
||||
m.hide_invisible(me);
|
||||
// The item's MAP_DECORATIONS (an explorer map's target), added
|
||||
// after the players as addDecoration would.
|
||||
let s = (1 << m.scale) as f32;
|
||||
let mut items: Vec<Decoration> = Vec::new();
|
||||
for (sprite, x, z, rotation) in targets {
|
||||
let dx = (x - f64::from(m.center[0])) as f32 / s;
|
||||
let dz = (z - f64::from(m.center[1])) as f32 / s;
|
||||
let inside = (-63.0..=63.0).contains(&dx) && (-63.0..=63.0).contains(&dz);
|
||||
if !inside && !m.unlimited {
|
||||
continue;
|
||||
}
|
||||
let d = Decoration {
|
||||
sprite,
|
||||
x: clamp_map_coordinate(dx),
|
||||
y: clamp_map_coordinate(dz),
|
||||
rot: decoration_rotation(nether, game_time, f64::from(rotation)),
|
||||
name: None,
|
||||
key: Key::Item,
|
||||
};
|
||||
if !items.contains(&d) {
|
||||
items.push(d);
|
||||
}
|
||||
}
|
||||
if !m
|
||||
.decorations
|
||||
.iter()
|
||||
.filter(|d| d.key == Key::Item)
|
||||
.eq(items.iter())
|
||||
{
|
||||
m.decorations.retain(|d| d.key != Key::Item);
|
||||
m.decorations.extend(items);
|
||||
m.decorations_changed();
|
||||
}
|
||||
m.add_item_decorations(targets, game_time);
|
||||
}
|
||||
for id in held {
|
||||
self.update_map(id);
|
||||
@@ -1965,7 +1959,7 @@ mod tests {
|
||||
y: -30,
|
||||
rot: 4,
|
||||
name: None,
|
||||
key: Key::Item,
|
||||
key: Key::Remote,
|
||||
};
|
||||
assert_eq!(
|
||||
decoration_pose(&vanilla, -0.02),
|
||||
@@ -2018,13 +2012,47 @@ mod tests {
|
||||
));
|
||||
assert_eq!(map_id(&stack), Some(3));
|
||||
let found = static_decorations(&stack);
|
||||
assert_eq!(found, vec![("map/decorations/red_x", 120.0, -300.0, 180.0)]);
|
||||
assert_eq!(
|
||||
found,
|
||||
vec![("+".into(), "map/decorations/red_x", 120.0, -300.0, 180.0)]
|
||||
);
|
||||
// calculateRotation(180): (byte) (188 * 16 / 360) = 8.
|
||||
assert_eq!(decoration_rotation(false, 0, 180.0), 8);
|
||||
assert_eq!(decoration_rotation(false, 0, -90.0), 12);
|
||||
assert_eq!(clamp_map_coordinate(-70.0), -128);
|
||||
assert_eq!(clamp_map_coordinate(10.25), 21);
|
||||
}
|
||||
#[test]
|
||||
fn an_undecorated_copy_does_not_erase_the_treasure_target() {
|
||||
let mut map = MapData::fresh(0.0, 0.0, 0, "minecraft:overworld");
|
||||
let mut treasure = ItemStack::new("minecraft:filled_map", 1);
|
||||
treasure.data_mut().map_id = Some(7);
|
||||
treasure.data_mut().other.push((
|
||||
"minecraft:map_decorations".into(),
|
||||
target_decoration("minecraft:red_x", [20, 40, 30]),
|
||||
));
|
||||
let mut copy = ItemStack::new("minecraft:filled_map", 1);
|
||||
copy.data_mut().map_id = Some(7);
|
||||
map.add_item_decorations(static_decorations(&treasure), 0);
|
||||
map.add_item_decorations(static_decorations(©), 0);
|
||||
assert_eq!(map.decorations.len(), 1);
|
||||
assert_eq!(map.decorations[0].sprite, "map/decorations/red_x");
|
||||
assert_eq!((map.decorations[0].x, map.decorations[0].y), (40, 60));
|
||||
}
|
||||
#[test]
|
||||
fn named_map_targets_are_added_once_without_replacing_other_targets() {
|
||||
let mut map = MapData::fresh(0.0, 0.0, 0, "minecraft:overworld");
|
||||
let target = |key: JavaString, x| (key, "map/decorations/red_x", x, 30.0, 180.0);
|
||||
let first_key = JavaString(vec![0xd800]);
|
||||
let second_key = JavaString(vec![0xd801]);
|
||||
map.add_item_decorations(vec![target(first_key.clone(), 20.0)], 0);
|
||||
map.add_item_decorations(vec![target(first_key, 40.0), target(second_key, 25.0)], 1);
|
||||
assert_eq!(map.decorations.len(), 2);
|
||||
assert_eq!((map.decorations[0].x, map.decorations[1].x), (40, 50));
|
||||
// Both entries are off the map: neither may remove an existing target.
|
||||
map.add_item_decorations(vec![target("absent".into(), 200.0)], 2);
|
||||
assert_eq!(map.decorations.len(), 2);
|
||||
}
|
||||
|
||||
/// `ItemStack.useOn` / `MapItem.useOn`: in adventure mode, a map whose
|
||||
/// `can_place_on` component includes the banner block toggles that banner's
|
||||
|
||||
@@ -925,6 +925,17 @@ public final class VanillaCommandExecOracle {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
case "map_carried" -> {
|
||||
// The harness has no player network listener; invoke the
|
||||
// real map update normally reached from its player tick.
|
||||
for (int i = 0; i < player.getInventory().getContainerSize(); i++) {
|
||||
var stack = player.getInventory().getItem(i);
|
||||
var id = stack.get(net.minecraft.core.component.DataComponents.MAP_ID);
|
||||
if (id == null) continue;
|
||||
var map = net.minecraft.world.item.MapItem.getSavedData(id, player.level());
|
||||
if (map != null) map.tickCarriedBy(player, stack, null);
|
||||
}
|
||||
}
|
||||
case "craft_menu_click" -> {
|
||||
if (!(player.containerMenu instanceof net.minecraft.world.inventory.CraftingMenu))
|
||||
throw new IllegalStateException("craft fixture: crafting menu not open");
|
||||
@@ -977,6 +988,12 @@ public final class VanillaCommandExecOracle {
|
||||
new net.minecraft.world.level.saveddata.maps.MapId(id), player.level());
|
||||
String value = map == null ? "missing" : "scale=" + map.scale + ",locked=" + map.locked
|
||||
+ ",center=" + map.centerX + "," + map.centerZ;
|
||||
if (map != null && op.has("red_x")) {
|
||||
int targets = 0;
|
||||
for (var decoration : map.getDecorations())
|
||||
if (decoration.type().is(net.minecraft.world.level.saveddata.maps.MapDecorationTypes.RED_X)) targets++;
|
||||
value += ",red_x=" + targets;
|
||||
}
|
||||
JsonObject record = new JsonObject();
|
||||
record.addProperty("cmd", "#map_state " + id);
|
||||
JsonArray messages = new JsonArray();
|
||||
|
||||
@@ -5855,7 +5855,7 @@ def command_zombie_reinforcements(once=False, raw=False, legacy=False):
|
||||
ISOLATED_REINFORCEMENTS = frozenset({'command zombie reinforcements', 'command reinforcement once'})
|
||||
# These overlays replace built-in recipes, never beside the corpus's fixed vanilla recipe-count controls.
|
||||
ISOLATED_RECIPES = frozenset({'crafting recipe priority', 'crafting base recipe control', 'transmute result patches'})
|
||||
ISOLATED_FRAME_MAPS = frozenset({'frame live map detachment'})
|
||||
ISOLATED_FRAME_MAPS = frozenset({'frame live map detachment', 'treasure map copies'})
|
||||
ISOLATED_MAP_CRAFTING = frozenset({'craft map takes'})
|
||||
ISOLATED_MACRO_TYPE = frozenset({'macro source type error'})
|
||||
ISOLATED_PICKUP_SAVE = frozenset({'mob saved pickup loot'})
|
||||
@@ -5882,7 +5882,7 @@ def groups():
|
||||
except ImportError:
|
||||
from mob_name_predicate_corpus import cases as mob_name_predicates
|
||||
try:
|
||||
from .frame_map_corpus import frame_maps
|
||||
from .frame_map_corpus import frame_maps, treasure_map_copies
|
||||
from .allay_damage_corpus import allay_player_damage
|
||||
from .wolf_anger_corpus import groups as wolf_anger_groups
|
||||
from .allay_healing_corpus import allay_healing
|
||||
@@ -5893,7 +5893,7 @@ def groups():
|
||||
from .gateway_passenger_corpus import gateway_passengers
|
||||
from .item_merge_corpus import stack_limit_merges
|
||||
except ImportError:
|
||||
from frame_map_corpus import frame_maps
|
||||
from frame_map_corpus import frame_maps, treasure_map_copies
|
||||
from allay_damage_corpus import allay_player_damage
|
||||
from wolf_anger_corpus import groups as wolf_anger_groups
|
||||
from allay_healing_corpus import allay_healing
|
||||
@@ -5913,6 +5913,7 @@ def groups():
|
||||
('mob effect predicate view', mob_effect_predicates(cmd, setup)),
|
||||
('mob custom name components', mob_name_predicates(cmd, setup)),
|
||||
('frame live map detachment', frame_maps(cmd, setup)),
|
||||
('treasure map copies', treasure_map_copies(cmd, setup)),
|
||||
('craft map takes', craft_map_takes(cmd, setup)),
|
||||
('allay player damage immunity', allay_player_damage(cmd, setup)),
|
||||
*wolf_anger_groups(cmd, setup),
|
||||
|
||||
@@ -147,3 +147,34 @@ def frame_maps(cmd, setup):
|
||||
return ops + setup('/kill @e[tag=frame_map_1]', '/kill @e[tag=frame_map_3]',
|
||||
'/scoreboard objectives remove frame_maps', '/clear @s', '/gamemode creative',
|
||||
'/forceload remove -32 -32 31 31', '/tick unfreeze')
|
||||
|
||||
|
||||
def treasure_map_copies(cmd, setup):
|
||||
"""A same-id copy without components must not remove a treasure map's X."""
|
||||
ops = []
|
||||
def run(command, result=1):
|
||||
ops.append(cmd(command, expect_result=result))
|
||||
def step():
|
||||
ops.append({'op': 'map_carried'})
|
||||
def state():
|
||||
ops.append({'op': 'map_state', 'id': 0, 'red_x': True,
|
||||
'expect_map': 'scale=0,locked=false,center=0,0,red_x=1'})
|
||||
run('/tick freeze')
|
||||
ops.extend(setup('/gamemode survival', '/clear @s', '/tp @s 4.5 100 4.5 0 0',
|
||||
'/forceload add 0 0 15 15'))
|
||||
ops.append({'op': 'settle', 'required_chunks': [[0, 0]]})
|
||||
run('/item replace entity @s weapon.mainhand with map')
|
||||
ops.append({'op': 'use_item'})
|
||||
run('/execute if items entity @s weapon.mainhand filled_map[map_id=0]')
|
||||
target = 'filled_map[map_id=0,map_decorations={"+":{type:"minecraft:red_x",x:20.0d,z:30.0d,rotation:180.0f}}]'
|
||||
run('/item replace entity @s weapon.mainhand with ' + target)
|
||||
step()
|
||||
state()
|
||||
run('/item replace entity @s hotbar.1 with filled_map[map_id=0]')
|
||||
step()
|
||||
state()
|
||||
# Retain the target even when only the undecorated copy remains.
|
||||
run('/item replace entity @s weapon.mainhand with air')
|
||||
step()
|
||||
state()
|
||||
return ops
|
||||
Reference in new issue
Block a user