mirror of
https://github.com/horizon-rust/horizon-mods.git
synced 2026-11-07 11:05:58 -10:00
SMP 0.24.0: /pay received counts toward the daily selling limit
Selling up to the cap on alts and paying it all to a main bypassed the daily sell limit (reported by pointofpressure). A /pay now checks the receiver's remaining daily_sell_coins and is refused past it; a payment made counts toward it. Trades and auction sales are unaffected. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9ce6a3f9a0
commit
964325b819
6 files changed
+96
-5
No files matched your search
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "horizon-mod-smp"
|
||||
version = "0.23.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "horizon-mod-smp"
|
||||
version = "0.23.0"
|
||||
version = "0.24.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
@@ -73,6 +73,9 @@ money and the room before anything moves.
|
||||
copper, diamond, emerald, coal, redstone, lapis, wheat, wood and food chains at each start.
|
||||
- Each player sells at most `daily_sell_coins` (2,000 coins) and `daily_sell_items` (640 of each item) a UTC day,
|
||||
`/worth` and the shop show what is left, and `market.json` keeps the day's count across restarts.
|
||||
- Money received with `/pay` counts toward the receiver's same `daily_sell_coins`: selling up to the limit on alts
|
||||
and paying it all to a main is refused once the main's limit is reached ("can only receive N more today").
|
||||
Trades (`/trade`) and auction house sales are not counted.
|
||||
- Amounts are checked: no price above 1,000,000,000,000, no sum that overflows.
|
||||
- The coinflip and the lottery give the winner the stakes (less `fee_percent`, 0 by default): they move money
|
||||
between players and never make any.
|
||||
|
||||
@@ -41,7 +41,7 @@ use std::{path::PathBuf, sync::Mutex};
|
||||
/// The mod's id, name and version.
|
||||
pub const ID: &str = "smp";
|
||||
pub const NAME: &str = "SMP";
|
||||
pub const VERSION: &str = "0.23.0";
|
||||
pub const VERSION: &str = "0.24.0";
|
||||
|
||||
/// `config.json` in `world/horizon-smp/`, written with its defaults the
|
||||
/// first time, and again with the keys a newer version adds (values kept).
|
||||
@@ -434,6 +434,20 @@ const COMMANDS: [&str; 19] = [
|
||||
"lottery", "treasury", "taxes", "ah", "bank", "jobs", "casino",
|
||||
];
|
||||
|
||||
/// `/pay <player> <amount>`'s receiver (account, name) and amount, as
|
||||
/// `Economy::pay` reads them; None for another command.
|
||||
fn pay_target(e: &Economy, line: &str) -> Option<(u128, String, i64)> {
|
||||
let [cmd, who, amount] = line.split_whitespace().collect::<Vec<_>>()[..] else {
|
||||
return None;
|
||||
};
|
||||
if cmd != "pay" {
|
||||
return None;
|
||||
}
|
||||
let amount = smp_economy::parse_amount(amount)?;
|
||||
let (to, account) = e.find(who)?;
|
||||
Some((to, account.name.clone(), amount))
|
||||
}
|
||||
|
||||
/// The market's (sell, buy) for one item, as the contracts read it.
|
||||
fn market_price(market: &Market, id: &str) -> Option<(i64, i64)> {
|
||||
let p = market.price(id);
|
||||
@@ -670,7 +684,27 @@ pub(in crate::game) fn perform(
|
||||
let price = |id: &str| market_price(&smp.market, id);
|
||||
smp.jobs.as_mut()?.command(e, &me, trades, &mut bag, &price, line)
|
||||
})
|
||||
.or_else(|| e.command(&me, &on, line))
|
||||
.or_else(|| {
|
||||
// A payment counts toward the receiver's daily selling
|
||||
// limit (alts can't sell for a main past it).
|
||||
let paid = pay_target(e, line);
|
||||
if let Some((to, name, amount)) = &paid
|
||||
&& let Err(left) = smp.market.may_receive(*to, *amount, now)
|
||||
{
|
||||
return Some(vec![Out::Error(format!(
|
||||
"{name} can only receive {} more today: payments count toward the same daily limit as selling to the market.",
|
||||
e.money(left)
|
||||
))]);
|
||||
}
|
||||
let before = paid.as_ref().map(|(to, ..)| e.balance(*to));
|
||||
let out = e.command(&me, &on, line);
|
||||
if let (Some((to, _, amount)), Some(b)) = (paid, before)
|
||||
&& e.balance(to) == b + amount
|
||||
{
|
||||
smp.market.count_received(to, now, amount);
|
||||
}
|
||||
out
|
||||
})
|
||||
.or_else(|| smp.market.command(e, &me, trades, &mut bag, line))
|
||||
.or_else(|| {
|
||||
let (out, a) = smp.trades.command(e, &me, trades, &online, now, line)?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[mod]
|
||||
id = "smp"
|
||||
version = "0.23.0"
|
||||
version = "0.24.0"
|
||||
priority = 100
|
||||
|
||||
# The commit of horizon main it was tested on. It uses only the native hook points (hooks 62 in
|
||||
|
||||
@@ -358,6 +358,32 @@ impl Market {
|
||||
.then(|| cap.saturating_sub(self.sold(uuid, now).items.get(id).copied().unwrap_or(0)))
|
||||
}
|
||||
|
||||
/// Whether `uuid` may be paid `amount` by another player now (`/pay`):
|
||||
/// money received counts toward the same daily limit as selling to the
|
||||
/// market, so selling on alts and paying it all to a main doesn't get
|
||||
/// past it. Err: what they may still receive today.
|
||||
pub fn may_receive(&self, uuid: u128, amount: i64, now: i64) -> Result<(), i64> {
|
||||
match self.coins_left(uuid, now) {
|
||||
Some(left) if amount > left => Err(left),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts `coins` paid to `uuid` (`/pay`) against today's limit.
|
||||
pub fn count_received(&mut self, uuid: u128, now: i64, coins: i64) {
|
||||
let day = now.div_euclid(DAY_MS);
|
||||
let s = self.ledger.sold.entry(uuid_string(uuid)).or_default();
|
||||
if s.day != day {
|
||||
*s = Sold {
|
||||
day,
|
||||
..Sold::default()
|
||||
};
|
||||
}
|
||||
s.coins = s.coins.saturating_add(coins.max(0));
|
||||
self.ledger.sold.retain(|_, s| s.day == day);
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Counts a sale against today's limits.
|
||||
fn count_sale(&mut self, uuid: u128, now: i64, id: &str, n: u32, coins: i64) {
|
||||
let day = now.div_euclid(DAY_MS);
|
||||
@@ -739,6 +765,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn money_received_by_pay_counts_toward_the_daily_selling_limit() {
|
||||
let config = Config {
|
||||
daily_sell_coins: 250,
|
||||
..Config::default()
|
||||
};
|
||||
let (_e, mut m, _me, _dir) = setup_with("received", config);
|
||||
let now = 1_793_577_600_000;
|
||||
// An alt pays the main 200: fine, and it counts.
|
||||
assert_eq!(m.may_receive(ALEX, 200, now), Ok(()));
|
||||
m.count_received(ALEX, now, 200);
|
||||
assert_eq!(m.coins_left(ALEX, now), Some(50));
|
||||
// A second alt's 100 is refused: 50 left today, for sales or payments.
|
||||
assert_eq!(m.may_receive(ALEX, 100, now), Err(50));
|
||||
assert_eq!(m.may_receive(ALEX, 50, now), Ok(()));
|
||||
// The next day the limit is whole again.
|
||||
assert_eq!(m.may_receive(ALEX, 250, now + DAY_MS), Ok(()));
|
||||
// No limit configured: anything goes.
|
||||
let (_e, m, _me, _dir) = setup_with(
|
||||
"unlimited",
|
||||
Config {
|
||||
daily_sell_coins: 0,
|
||||
..Config::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(m.may_receive(ALEX, 1_000_000, now), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selling_takes_the_items_then_pays_and_logs_both() {
|
||||
let (mut e, mut m, me, dir) = setup("sell");
|
||||
|
||||
Reference in new issue
Block a user