mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-11 23:55:37 -10:00
Honor custom durability on blocking items
Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
db87c15e7c
commit
3b34446f84
5 files changed
+144
-14
No files matched your search
@@ -190,6 +190,9 @@ impl Game<'_> {
|
||||
fn dispense_case(&mut self, case: &Value) -> Result<Vec<String>> {
|
||||
let seed = case["seed"].as_i64().unwrap_or(0);
|
||||
self.fixture_reset(seed);
|
||||
if let Some(op) = case.get("blocking_wear") {
|
||||
return self.fixture_blocking_wear(op);
|
||||
}
|
||||
// The oracle level answers one sky and one block light everywhere.
|
||||
let light = case["light"].as_array();
|
||||
let level = |i: usize, default: i64| {
|
||||
@@ -662,6 +665,39 @@ impl Game<'_> {
|
||||
/// source, the hurt with the weapon's armor effects, then `weapon_hit`
|
||||
/// (the mace's smash, post-attack enchantments, the wear), as
|
||||
/// `VanillaDispenserOracle.attack` prints them.
|
||||
fn fixture_blocking_wear(&mut self, op: &Value) -> Result<Vec<String>> {
|
||||
let stack = parse_stack(op["item"].as_str().ok_or("missing blocking stack")?)?
|
||||
.ok_or("empty blocking stack")?;
|
||||
// Reload the stack just as a player inventory does between sessions.
|
||||
let saved = horizon_client::item_nbt::item_to_nbt(&stack);
|
||||
self.inventory[self.selected] = horizon_client::item_nbt::item_from_nbt(&saved);
|
||||
self.creative = op["creative"].as_bool().unwrap_or(false);
|
||||
self.mode = if self.creative {
|
||||
GameMode::Creative
|
||||
} else {
|
||||
GameMode::Survival
|
||||
};
|
||||
self.combat = Default::default();
|
||||
self.using = None;
|
||||
self.shield_cooldown = 0;
|
||||
self.yaw = 0.0;
|
||||
self.player = Some(PlayerMotion::new(
|
||||
self.motion,
|
||||
horizon_core::geometry::Vec3::new(0.0, 64.0, 0.0),
|
||||
)?);
|
||||
if !self.raise_shield(self.selected) || self.using.is_none() {
|
||||
return Err("could not raise blocking stack".into());
|
||||
}
|
||||
let mut lines = Vec::new();
|
||||
for _ in 0..2 {
|
||||
self.hurt_player_from(4.0, Some([0.0, 64.0, 2.0]), "minecraft:arrow", Some(None));
|
||||
let stack = self.inventory[self.selected].as_ref();
|
||||
let (count, damage) = stack.map_or((0, 0), |stack| (stack.count, stack.damage));
|
||||
lines.push(format!("blocking wear {count} {damage}"));
|
||||
}
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
fn fixture_attack(&mut self, op: &Value, targets: &[Target]) -> Result<Vec<String>> {
|
||||
let stack = parse_stack(op["item"].as_str().unwrap_or("-"))?;
|
||||
self.mode = if op["creative"].as_bool().unwrap_or(false) {
|
||||
|
||||
@@ -151,21 +151,27 @@ impl Game<'_> {
|
||||
} else {
|
||||
(base + factor * outcome.blocked).floor() as i32
|
||||
};
|
||||
if item_damage > 0 && !self.creative {
|
||||
if item_damage > 0
|
||||
&& !self.creative
|
||||
&& let Some(max) = blocking_item_max_damage(
|
||||
&stack,
|
||||
self.items()
|
||||
.get(&stack.item)
|
||||
.map_or(0, |item| item.max_damage),
|
||||
)
|
||||
{
|
||||
let loss = self.wear(&stack, item_damage as u32);
|
||||
let max = self
|
||||
.res
|
||||
.items()
|
||||
.get(&stack.item)
|
||||
.map_or(0, |i| i.max_damage);
|
||||
let cell = self.stack_in_mut(slot);
|
||||
if let Some(s) = cell.as_mut()
|
||||
&& max > 0
|
||||
if loss > 0
|
||||
&& let Some(s) = cell.as_mut()
|
||||
{
|
||||
s.damage += loss;
|
||||
s.damage = s.damage.saturating_add(loss).min(max);
|
||||
if s.damage >= max {
|
||||
let broken = s.clone();
|
||||
*cell = None;
|
||||
s.count -= 1;
|
||||
if s.count == 0 {
|
||||
*cell = None;
|
||||
}
|
||||
self.using = None;
|
||||
// ItemStack.hurtAndBreak: onEquippedItemBroken for the hand it was used from.
|
||||
let hand = if slot == OFFHAND_SLOT {
|
||||
@@ -298,3 +304,28 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `ItemStack.isDamageableItem` and `getMaxDamage` for the blocking stack.
|
||||
fn blocking_item_max_damage(stack: &ItemStack, prototype_max: u32) -> Option<u32> {
|
||||
let data = stack.data();
|
||||
let component = |id| data.and_then(|data| data.other(id));
|
||||
if stack.count == 0
|
||||
|| component("minecraft:unbreakable").is_some()
|
||||
|| component("!minecraft:max_damage").is_some()
|
||||
|| component("!minecraft:damage").is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// A non-damageable prototype supplies neither MAX_DAMAGE nor DAMAGE.
|
||||
if prototype_max == 0
|
||||
&& stack.damage == 0
|
||||
&& !data.is_some_and(|data| data.explicit_zero_damage)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let max = match component("minecraft:max_damage") {
|
||||
Some(value) => horizon_world::datafix::dynamic::as_int(Some(value), 0).max(0) as u32,
|
||||
None => prototype_max,
|
||||
};
|
||||
(max > 0).then_some(max)
|
||||
}
|
||||
@@ -882,7 +882,12 @@ pub fn encode_components_with_items(stack: &ItemStack, items: &crate::items::Ite
|
||||
/// `encode_components` from the damage value and extra components.
|
||||
pub fn components_tag(damage: u32, data: Option<&StackData>) -> Option<Tag> {
|
||||
let mut entries: Vec<(&str, Tag)> = Vec::new();
|
||||
if damage > 0 {
|
||||
// A custom max damage can make zero DAMAGE a meaningful saved component.
|
||||
if damage > 0
|
||||
|| data.is_some_and(|data| {
|
||||
data.explicit_zero_damage && data.other("minecraft:max_damage").is_some()
|
||||
})
|
||||
{
|
||||
entries.push(("minecraft:damage", Tag::Int(damage as i32)));
|
||||
}
|
||||
if let Some(d) = data {
|
||||
|
||||
@@ -331,11 +331,44 @@ public final class VanillaDispenserOracle {
|
||||
return ent;
|
||||
}
|
||||
|
||||
/** Native BlocksAttacks.hurtBlockingItem, with a server player and live item components. */
|
||||
static void blockingWear(VanillaBlockUpdateOracle.World world, JsonObject op, List<String> result) throws Exception {
|
||||
var player = (BlockingWearer) VanillaBlockUpdateOracle.allocate.invoke(VanillaBlockUpdateOracle.unsafe, BlockingWearer.class);
|
||||
player.world = world;
|
||||
player.creative = op.has("creative") && op.get("creative").getAsBoolean();
|
||||
var advancements = (net.minecraft.server.PlayerAdvancements) VanillaBlockUpdateOracle.allocate.invoke(
|
||||
VanillaBlockUpdateOracle.unsafe, net.minecraft.server.PlayerAdvancements.class);
|
||||
VanillaBlockUpdateOracle.set(net.minecraft.server.PlayerAdvancements.class, advancements, "activeTriggers", new HashMap<>());
|
||||
VanillaBlockUpdateOracle.set(net.minecraft.server.level.ServerPlayer.class, player, "advancements", advancements);
|
||||
ItemStack item = stack(op.get("item").getAsString());
|
||||
var blocking = item.get(net.minecraft.core.component.DataComponents.BLOCKS_ATTACKS);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (!item.isEmpty()) {
|
||||
blocking.hurtBlockingItem(world, item, player, net.minecraft.world.InteractionHand.MAIN_HAND, 4);
|
||||
}
|
||||
result.add("blocking wear " + item.getCount() + " " + item.getDamageValue());
|
||||
}
|
||||
}
|
||||
|
||||
static final class BlockingWearer extends net.minecraft.server.level.ServerPlayer {
|
||||
ServerLevel world;
|
||||
boolean creative;
|
||||
BlockingWearer() { super(null, null, null, null); }
|
||||
@Override public ServerLevel level() { return world; }
|
||||
@Override public boolean hasInfiniteMaterials() { return creative; }
|
||||
@Override public void awardStat(net.minecraft.stats.Stat<?> stat, int count) {}
|
||||
@Override public void onEquippedItemBroken(ItemStack stack, EquipmentSlot slot) {}
|
||||
}
|
||||
|
||||
static void run(JsonObject f, List<String> result) throws Exception {
|
||||
VanillaBlockUpdateOracle.World w = VanillaBlockUpdateOracle.build(f);
|
||||
w.entities = new ArrayList<>();
|
||||
w.registries = ParityData.access;
|
||||
w.server.loot.delegate = ParityData.loot;
|
||||
if (f.has("blocking_wear")) {
|
||||
blockingWear(w, f.getAsJsonObject("blocking_wear"), result);
|
||||
return;
|
||||
}
|
||||
VanillaBlockUpdateOracle.set(Level.class, w, "damageSources", new net.minecraft.world.damagesource.DamageSources(ParityData.access));
|
||||
if (f.has("dim") && f.get("dim").getAsString().equals("nether")) {
|
||||
// The Nether's dimension type and attributes (respawn anchors work, beds explode).
|
||||
|
||||
@@ -15,6 +15,8 @@ import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
try:
|
||||
from . import block_update_harness as h, lab, test_rust_parity as base
|
||||
from .integration_env import run_game
|
||||
@@ -278,6 +280,22 @@ def vehicle_cases(states, seed, only):
|
||||
def cases(facts, count, seed, only):
|
||||
rng = random.Random(seed)
|
||||
states = States(facts)
|
||||
if not only or only in 'minecraft:stick':
|
||||
# A stick starts with neither durability component. Check explicit zero,
|
||||
# already worn stacks, missing components, unbreakable and creative.
|
||||
for patch, creative in [
|
||||
('"minecraft:max_damage":2,"minecraft:damage":0', False),
|
||||
('"minecraft:max_damage":2,"minecraft:damage":1', False),
|
||||
('"minecraft:max_damage":2', False),
|
||||
('"minecraft:damage":0', False),
|
||||
('"minecraft:max_damage":2,"minecraft:damage":0,"minecraft:unbreakable":{}', False),
|
||||
('"minecraft:max_damage":2,"minecraft:damage":0', True),
|
||||
]:
|
||||
blocking = '"minecraft:blocks_attacks":{item_damage:{threshold:0.0f,base:1.0f,factor:0.0f}}'
|
||||
item = '{id:"minecraft:stick",count:1,components:{' + ','.join(
|
||||
(blocking, '"minecraft:max_stack_size":1', patch)) + '}}'
|
||||
yield {'seed': seed, 'pos': [0, 0, 0], 'cells': [], 'items': [],
|
||||
'blocking_wear': {'item': item, 'creative': creative}}
|
||||
dragon_egg = plain('ender_dragon_spawn_egg')
|
||||
if not only or only in dragon_egg:
|
||||
# Both the unobstructed position and a fractional collision height must
|
||||
@@ -400,7 +418,7 @@ def voice(line):
|
||||
|
||||
|
||||
def run(command, **kwargs):
|
||||
runner = run_game if str(command[0]).endswith('/horizon-client') else subprocess.run
|
||||
runner = run_game if Path(command[0]).stem == 'horizon-client' else subprocess.run
|
||||
done = runner(command, capture_output=True, text=True, **kwargs)
|
||||
if done.returncode:
|
||||
print((done.stdout + done.stderr)[-4000:])
|
||||
@@ -468,8 +486,10 @@ def main():
|
||||
str(resources), str(work / 'cases.jsonl'), str(work / 'native.txt')])
|
||||
run(['cargo', 'build', '-q', '--profile', 'fast', '-p', 'horizon-client', '--bin', 'horizon-client'], cwd=lab.ROOT,
|
||||
env={**os.environ, 'CARGO_BUILD_JOBS': os.environ.get('CARGO_BUILD_JOBS', '3')})
|
||||
run([str(lab.cargo_target() / 'fast/horizon-client'), '--resources', str(resources)], cwd=work,
|
||||
env=lab.game_env(HORIZON_DISPENSE_FIXTURE=f'{work / "cases.jsonl"},{work / "rust.txt"}'))
|
||||
executable = lab.cargo_target() / ('fast/horizon-client.exe' if os.name == 'nt' else 'fast/horizon-client')
|
||||
with tempfile.TemporaryDirectory(prefix='game-', dir=work) as game_dir:
|
||||
run([str(executable), '--resources', str(resources), '--game-dir', game_dir], cwd=work,
|
||||
env={**os.environ, 'HORIZON_DISPENSE_FIXTURE': f'{work / "cases.jsonl"},{work / "rust.txt"}'})
|
||||
# Loot table names (D) are vanilla's bookkeeping; the drops themselves are compared.
|
||||
ignore = tuple(prefix + ' ' for prefix in args.ignore.split(',') if prefix) + ('D ',)
|
||||
native = h.split_cases((work / 'native.txt').read_text(), ignore)
|
||||
@@ -495,6 +515,9 @@ def main():
|
||||
a = [voice(line) for line in a if not line.startswith('rng ')]
|
||||
b = [voice(line) for line in b if not line.startswith('rng ')]
|
||||
if not same(a, b):
|
||||
if 'blocking_wear' in case:
|
||||
groups.setdefault('blocking wear', []).append((case, a, b))
|
||||
continue
|
||||
item = next(i[4] for i in case['items'] if i[:3] == [0, 0, 0])
|
||||
block = facts.states[case['cells'][0][3]][0].split(':')[1]
|
||||
groups.setdefault(f'{block} {item.split(chr(34))[1]}', []).append((case, a, b))
|
||||
@@ -515,6 +538,8 @@ def main():
|
||||
|
||||
|
||||
def describe(case, facts):
|
||||
if 'blocking_wear' in case:
|
||||
return json.dumps(case['blocking_wear'])
|
||||
cells = ' '.join(f'{c[0]},{c[1]},{c[2]}={facts.describe(c[3])}' for c in case['cells'])
|
||||
extra = ''
|
||||
if case.get('entities'):
|
||||
|
||||
Reference in new issue
Block a user