mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 10:39:46 -10:00
Native trade hook: each trade's own offer snapshot; xtask stub; docs
Event::Traded now carries the offer as notifyTrade left it, so a shift-click whose trades level the villager up reports each trade's own prices. xtask's mod API check gets a MerchantOffer stand-in. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0dd9865328
commit
60191c6b70
4 files changed
+37
-86
No files matched your search
@@ -423,7 +423,7 @@ impl Game<'_> {
|
||||
) -> (Box<merchant::Session>, Vec<MerchantOffer>) {
|
||||
let mut session = session;
|
||||
let events = std::mem::take(&mut session.events);
|
||||
let traded = traded_offers(&events, &session.offers);
|
||||
let mut traded = Vec::new();
|
||||
let (villager, origin) = (session.villager, session.origin);
|
||||
if let Some(m) = mob.and_then(|id| self.mobs.iter_mut().find(|m| m.id == id)) {
|
||||
m.random = session.random.clone();
|
||||
@@ -451,7 +451,7 @@ impl Game<'_> {
|
||||
};
|
||||
self.audio.play(sound, Some(origin), 1.0, pitch);
|
||||
}
|
||||
merchant::Event::Traded(_) => {}
|
||||
merchant::Event::Traded(_, offer) => traded.push(*offer),
|
||||
merchant::Event::TradeSound => {
|
||||
let sound = if villager {
|
||||
"minecraft:entity.villager.yes"
|
||||
@@ -464,10 +464,10 @@ impl Game<'_> {
|
||||
}
|
||||
(session, traded)
|
||||
}
|
||||
/// `MerchantResultSlot.onTake` for each trade made (`offers`, as they
|
||||
/// are after it): `AbstractVillager.notifyTrade`'s `villager_trade`
|
||||
/// trigger, the statistic, then the mods' `native_trade_with_villager`
|
||||
/// for a server's player.
|
||||
/// `MerchantResultSlot.onTake` for each trade made (`offers`, as each
|
||||
/// was right after its trade): the statistic, `AbstractVillager.notifyTrade`'s
|
||||
/// `villager_trade` trigger, then the mods' `native_trade_with_villager`
|
||||
/// for a server's player (when the trader is still in the level).
|
||||
pub(super) fn merchant_traded(&mut self, mob: Option<u32>, offers: &[MerchantOffer]) {
|
||||
for offer in offers {
|
||||
self.stats.custom("traded_with_villager", 1);
|
||||
@@ -935,33 +935,6 @@ fn drag_scroll(y: f32, offers: usize) -> usize {
|
||||
((offs * max as f32 + 0.5) as i32).clamp(0, max) as usize
|
||||
}
|
||||
|
||||
/// Each trade a click made, as its offer was right after it: `offers` are
|
||||
/// as they are after all of the click's trades (a shift-click trades
|
||||
/// several times), so each copy counts only the uses up to its own trade.
|
||||
fn traded_offers(events: &[merchant::Event], offers: &[MerchantOffer]) -> Vec<MerchantOffer> {
|
||||
let mut later = vec![0; offers.len()];
|
||||
for e in events {
|
||||
if let merchant::Event::Traded(index) = e {
|
||||
if let Some(n) = later.get_mut(*index) {
|
||||
*n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut traded = Vec::new();
|
||||
for e in events {
|
||||
if let merchant::Event::Traded(index) = *e {
|
||||
if let (Some(offer), Some(n)) = (offers.get(index), later.get_mut(index)) {
|
||||
*n -= 1;
|
||||
traded.push(MerchantOffer {
|
||||
uses: offer.uses - *n,
|
||||
..offer.clone()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
traded
|
||||
}
|
||||
|
||||
/// `MerchantScreen.mouseScrolled`: `Mth.clamp((int)(scrollOff - scrollY), 0, max)`,
|
||||
/// so a fractional wheel step that truncates to zero does not move the list.
|
||||
fn scrolled(scroll: usize, max: usize, d: f32) -> usize {
|
||||
@@ -1007,52 +980,4 @@ mod tests {
|
||||
assert_eq!(drag_scroll(31.5 + 112.0, 10), 3);
|
||||
assert_eq!(drag_scroll(500.0, 10), 3);
|
||||
}
|
||||
|
||||
fn offer(result: &str, uses: i32) -> MerchantOffer {
|
||||
MerchantOffer {
|
||||
base_cost_a: merchant::ItemCost {
|
||||
stack: ItemStack::new("minecraft:emerald", 1),
|
||||
},
|
||||
cost_b: None,
|
||||
result: ItemStack::new(result, 1),
|
||||
uses,
|
||||
max_uses: 12,
|
||||
reward_exp: true,
|
||||
special_price_diff: 0,
|
||||
demand: 0,
|
||||
price_multiplier: 0.05,
|
||||
xp: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// A shift-click that trades offer 1 three times and offer 0 once: each
|
||||
/// trade is heard with its offer's uses as they were right after it
|
||||
/// (MerchantResultSlot.onTake runs per trade), not the click's final count.
|
||||
#[test]
|
||||
fn each_trade_of_a_click_sees_its_own_use_count() {
|
||||
use merchant::Event::{Orb, TradeSound, Traded};
|
||||
// After the click: offer 0 was at 4 uses, offer 1 at 2.
|
||||
let offers = [offer("minecraft:bread", 5), offer("minecraft:arrow", 5)];
|
||||
let events = [
|
||||
Traded(1),
|
||||
Orb(3),
|
||||
Traded(1),
|
||||
TradeSound,
|
||||
Traded(0),
|
||||
Traded(1),
|
||||
];
|
||||
let traded = traded_offers(&events, &offers);
|
||||
let seen: Vec<_> = traded.iter().map(|o| (&*o.result.item, o.uses)).collect();
|
||||
assert_eq!(
|
||||
seen,
|
||||
[
|
||||
("minecraft:arrow", 3),
|
||||
("minecraft:arrow", 4),
|
||||
("minecraft:bread", 5),
|
||||
("minecraft:arrow", 5),
|
||||
]
|
||||
);
|
||||
// A trade of an offer that is gone is not heard.
|
||||
assert!(traded_offers(&[Traded(7)], &offers).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -278,8 +278,9 @@ pub enum Event {
|
||||
TradeUpdated(bool, f32),
|
||||
/// `MerchantMenu.playTradeSound` (a shift-clicked trade).
|
||||
TradeSound,
|
||||
/// A trade of this offer was made (`notifyTrade`).
|
||||
Traded(usize),
|
||||
/// A trade of this offer was made (`notifyTrade`), with the offer as
|
||||
/// it was right after that trade's use (before a level-up's prices).
|
||||
Traded(usize, Box<MerchantOffer>),
|
||||
}
|
||||
/// What the world lends a trading session for offers made while it is open.
|
||||
pub struct TradeEnv<'a> {
|
||||
@@ -373,7 +374,8 @@ impl Session {
|
||||
offer.uses += 1;
|
||||
let offer = offer.clone();
|
||||
self.ambient_sound_time = -80;
|
||||
self.events.push(Event::Traded(index));
|
||||
self.events
|
||||
.push(Event::Traded(index, Box::new(offer.clone())));
|
||||
let mut prices = false;
|
||||
if self.villager {
|
||||
let mut pop = 3 + self.random.next_int_bound(4).unwrap_or(0);
|
||||
@@ -1016,6 +1018,28 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A shift-click trades several times in one session: each `Traded`
|
||||
/// event keeps the offer as it was right after its own trade, which is
|
||||
/// what the mods hear (`TradeWithVillagerEvent` per `onTake`).
|
||||
#[test]
|
||||
fn each_trade_event_keeps_its_own_offer() {
|
||||
let items = items();
|
||||
let mut s = Session::new(vec![offer(4, None), offer(2, None)], false);
|
||||
s.notify_trade(1, None, &items);
|
||||
s.notify_trade(1, None, &items);
|
||||
s.notify_trade(0, None, &items);
|
||||
let seen: Vec<_> = s
|
||||
.events
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
Event::Traded(i, o) => Some((*i, o.uses, o.base_cost_a.stack.count)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(seen, [(1, 1, 2), (1, 2, 2), (0, 1, 4)]);
|
||||
assert_eq!((s.offers[0].uses, s.offers[1].uses), (1, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn demand_raises_prices() {
|
||||
let items = items();
|
||||
|
||||
+3
-2
@@ -1490,8 +1490,9 @@ For mods that run where the world is simulated: a dedicated server, a LAN host,
|
||||
- **`native_trade_with_villager(merchant, kind, offer)`**: NeoForge's `TradeWithVillagerEvent`, heard once per trade a
|
||||
server player makes with a villager or a wandering trader (a shift-click that trades several times is heard for each),
|
||||
after the trade's statistic and `villager_trade` trigger. `merchant` is the trader's entity id, `kind` its type
|
||||
(`minecraft:villager`), and `offer` the `MerchantOffer` as it is after the trade: its costs, `result`, `uses` (this
|
||||
trade counted), `max_uses` and `xp`. Listen only. The player's state is swapped in, so `native_acting_player` names
|
||||
(`minecraft:villager` or `minecraft:wandering_trader`), and `offer` the `MerchantOffer` as it was right after the
|
||||
trade: its costs, `result`, `uses` (this trade counted), `max_uses` and `xp`. Listen only, heard once the whole click
|
||||
is done (its payments taken; NeoForge fires during each take). The player's state is swapped in, so `native_acting_player` names
|
||||
them. Unlike `native_player_trade`, which is heard once per click with no offer.
|
||||
- **`native_player_shield_disable(player, name, ticks)`**: a blocked melee hit disabled the server player’s shield for `ticks` ticks. It fires just before the cooldown is set; guest lookup helpers may not find the guest during packet handling.
|
||||
- **`native_player_loom_pattern_select(player, name, pattern)`**: a server player selected loom pattern button `pattern`, after the menu accepted it. Guest lookup helpers may not find the guest during packet handling.
|
||||
|
||||
@@ -1045,6 +1045,7 @@ mod tests {
|
||||
pub(super) struct NativeScreen;
|
||||
pub(super) struct ModBlockEntity;
|
||||
pub(super) struct ModContainer;
|
||||
pub(super) struct MerchantOffer;
|
||||
pub(super) struct ModFluidTanks;
|
||||
pub(super) struct ModEnergyBuffer;
|
||||
pub(super) struct LootStack;
|
||||
|
||||
Reference in new issue
Block a user