Compare commits

...
Author SHA1 Message Date
rdbtCVS a086c90431 Remove SMP gameplay quotas and cooldowns for all worlds 2026-11-02 21:52:44 +00:00
8 changed files with 177 additions and 49 deletions

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "horizon-mod-smp"
version = "0.30.1"
version = "0.31.0"
dependencies = [
"serde",
"serde_json",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "horizon-mod-smp"
version = "0.30.1"
version = "0.31.0"
edition = "2024"
license = "MIT"
publish = false
+10
View File
@@ -8,6 +8,16 @@ Players install nothing: it speaks only what a stock Horizon client already show
lines, vanilla chest menus the server fills with ordinary items (the trade menu); later sounds, titles, the action
bar, boss bars and the sidebar).
**Unlimited gameplay (0.31).** On every world load, the SMP removes market daily sales and incoming-payment quotas,
market resale waiting periods and minute trade throttles, casino daily win/loss quotas and bet cooldowns,
coinflip cooldowns, lottery ticket quotas, auction listing quotas and percentage bid increments, home and claim
playtime quotas, teleport warmups/cooldowns, nickname playtime/cooldown requirements, and command rate limits.
Existing `config.json` limits are migrated automatically and saved. Bets accept any positive amount supported by
the economy. Home and claim counts use the platform's representable maximum; auction durations retain only a
safe timestamp ceiling. Permission checks, feature enable switches, inventory capacity, checked money arithmetic,
claim protection, nickname validation, market pricing curves and game rules still apply.
The limit settings and old defaults described below are retained for compatibility; the world-load policy overrides them.
This version: the economy, the auction house, the server market and its `/shop` menu, player trades, the coinflip, the lottery, places (spawn, homes, warps,
teleport requests, the rules), land claims, and the scoreboard (a sidebar with your balance, playtime and the richest players, the Tab list's
header and footer).
+6 -44
View File
@@ -10,14 +10,14 @@
use crate::game::{
Game,
smp_auction::{self, AuctionHouse},
smp_report::{self, Report},
smp_report::Report,
native_server::{NativeCommand, NativeServerPlayer, NativeSidebar},
smp_board::{self, Board, Line},
smp_casino::{self, Casino},
smp_casino::Casino,
smp_casino_glue,
smp_claims::{self, Claims},
smp_economy::{self, Economy, Out, Player},
smp_games::{self, Games},
smp_games::Games,
smp_lottery::Lottery,
smp_places::{self, Body, Place, Places},
smp_presence::{self, Presence},
@@ -39,48 +39,9 @@ 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.30.1";
pub const VERSION: &str = "0.31.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).
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Config {
#[serde(flatten)]
pub economy: smp_economy::Config,
pub market: smp_market::Config,
pub shop: smp_shop::Config,
pub gambling: smp_games::Config,
pub casino: smp_casino::Config,
pub places: smp_places::Config,
pub claims: smp_claims::Config,
pub board: smp_board::Config,
pub presence: smp_presence::Config,
pub nicks: smp_nick::Config,
pub auction: smp_auction::Config,
pub report: smp_report::Config,
pub web: WebPush,
}
/// `config.json`'s `web`: where the web pages (`web/market.json`,
/// `web/economy.json`) are sent for the website, `url` + `/<name>`, with
/// `token` as the bearer (off while empty). Only what changed is sent, at
/// most once a minute, gzipped, by the thread that writes the market page.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct WebPush {
pub url: String,
pub token: String,
}
impl Default for WebPush {
fn default() -> Self {
Self {
url: "https://kira-production-500e.up.railway.app/smp/ingest".into(),
token: String::new(),
}
}
}
pub use crate::game::smp_config::{Config, WebPush};
/// The SMP of the open world.
pub struct Smp {
@@ -162,6 +123,7 @@ fn open(dir: PathBuf) -> Smp {
}
};
let mut config = config;
let write = config.remove_limits() | write;
// Prices never edited: today's defaults (the balances are untouched).
let write = smp_market::migrate(&mut config.market) | write;
// 0.13's sidebar (the richest ten), never edited: 0.14's scoreboard.
+7 -1
View File
@@ -1,6 +1,6 @@
[mod]
id = "smp"
version = "0.30.1"
version = "0.31.0"
priority = 100
# The commit of horizon it was tested on: it needs horizon#6106 (merged at 975c07d): native_inventory,
@@ -315,3 +315,9 @@ bindings = [
{ name = "pos", ty = "[f64; 3]", value = "pos" },
{ name = "canceled", ty = "&mut bool", value = "&mut *canceled" },
]
[[module]]
id = "config"
file = "crates/horizon-client/src/game.rs"
name = "smp_config"
source = "smp_config.rs"
+130
View File
@@ -0,0 +1,130 @@
use super::{
smp_auction, smp_board, smp_casino, smp_claims, smp_economy, smp_games, smp_market, smp_nick,
smp_places, smp_presence, smp_report, smp_shop,
};
/// `config.json` in `world/horizon-smp/`, written with its defaults the
/// first time, and again with the keys a newer version adds (values kept).
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Config {
#[serde(flatten)]
pub economy: smp_economy::Config,
pub market: smp_market::Config,
pub shop: smp_shop::Config,
pub gambling: smp_games::Config,
pub casino: smp_casino::Config,
pub places: smp_places::Config,
pub claims: smp_claims::Config,
pub board: smp_board::Config,
pub presence: smp_presence::Config,
pub nicks: smp_nick::Config,
pub auction: smp_auction::Config,
pub report: smp_report::Config,
pub web: WebPush,
}
/// `config.json`'s `web`: where the web pages (`web/market.json`,
/// `web/economy.json`) are sent for the website, `url` + `/<name>`, with
/// `token` as the bearer (off while empty). Only what changed is sent, at
/// most once a minute, gzipped, by the thread that writes the market page.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct WebPush {
pub url: String,
pub token: String,
}
impl Default for WebPush {
fn default() -> Self {
Self {
url: "https://kira-production-500e.up.railway.app/smp/ingest".into(),
token: String::new(),
}
}
}
impl Default for Config {
fn default() -> Self {
let mut config = Self {
economy: Default::default(),
market: Default::default(),
shop: Default::default(),
gambling: Default::default(),
casino: Default::default(),
places: Default::default(),
claims: Default::default(),
board: Default::default(),
presence: Default::default(),
nicks: Default::default(),
auction: Default::default(),
report: Default::default(),
web: Default::default(),
};
config.remove_limits();
config
}
}
impl Config {
/// Apply the unlimited gameplay policy to both new and existing worlds.
/// Monetary and timestamp ceilings below are representation safeguards.
pub fn remove_limits(&mut self) -> bool {
let before = serde_json::to_value(&*self).expect("serializable config");
self.market.daily_sell_coins = 0;
self.market.daily_sell_items = 0;
self.market.dynamic.round_trip_hours = 0;
self.market.dynamic.max_minute_move_percent = 0.0;
self.gambling.min_bet = 1;
self.gambling.max_bet = smp_economy::MAX_AMOUNT;
self.gambling.max_tickets = smp_economy::MAX_AMOUNT;
self.gambling.cooldown_seconds = 0;
self.casino.min_bet = 1;
self.casino.max_bet = smp_economy::MAX_AMOUNT;
self.casino.daily_loss_cap = i64::MAX;
self.casino.daily_win_cap = i64::MAX;
self.casino.cooldown_ms = 0;
self.places.homes = vec![[0, i64::MAX]];
self.places.warmup_seconds = 0;
self.places.cooldown_seconds = 0;
self.claims.chunks = vec![[0, i64::MAX]];
self.presence.spam_commands = 0;
self.nicks.cooldown_seconds = 0;
self.nicks.min_playtime_minutes = 0;
self.auction.max_listings = usize::MAX;
// Keep enough timestamp headroom for the current date and extensions.
self.auction.auction_hours_max = i64::MAX / 7_200_000;
self.auction.bid_step_percent = 0;
before != serde_json::to_value(&*self).expect("serializable config")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn old_world_limits_are_removed_and_migration_is_idempotent() {
let mut c: Config = serde_json::from_value(serde_json::json!({
"market": {"daily_sell_coins": 2000, "daily_sell_items": 640,
"dynamic": {"round_trip_hours": 24, "max_minute_move_percent": 5}},
"places": {"homes": [[0, 1]], "cooldown_seconds": 10},
"gambling": {"enabled": true, "max_bet": 1000},
"casino": {"daily_loss_cap": 10000},
"nicks": {"ops_only": false}
}))
.unwrap();
assert!(c.remove_limits());
assert_eq!(c.market.daily_sell_coins, 0);
assert_eq!(c.market.daily_sell_items, 0);
assert_eq!(c.market.dynamic.round_trip_hours, 0);
assert_eq!(c.gambling.max_bet, smp_economy::MAX_AMOUNT);
assert_eq!(c.casino.daily_loss_cap, i64::MAX);
assert_eq!(c.places.homes, vec![[0, i64::MAX]]);
assert!(c.gambling.enabled);
assert!(!c.nicks.ops_only);
assert!(!c.remove_limits());
let roundtrip: Config = serde_json::from_value(serde_json::to_value(&c).unwrap()).unwrap();
assert_eq!(roundtrip.auction.max_listings, usize::MAX);
}
}
+19 -2
View File
@@ -41,7 +41,7 @@ pub struct Dynamic {
/// kept so older configs read the same.
pub max_trade_impact_percent: f64,
pub max_player_impact_percent: f64,
/// How far the price may move within one minute (either way).
/// How far the price may move within one minute (either way); 0: unlimited.
pub max_minute_move_percent: f64,
/// What a player bought from the market can't be sold back to it for
/// this long (that many of that item).
@@ -300,7 +300,7 @@ impl Book {
return (f64::from(n) * mid * s0.exp(), s0, n);
}
let mut done = n;
if edge < bound {
if band > 0.0 && edge < bound {
// The band comes first: as many as fit in it.
let fit = (edge.max(0.0) / delta + 1e-9).floor();
if fit < f64::from(n) {
@@ -677,6 +677,23 @@ mod tests {
assert_eq!(b.factor("minecraft:hay_block"), b.factor("minecraft:wheat"));
}
#[test]
fn unlimited_band_trades_every_item_and_preserves_round_trip_safety() {
let mut c = Dynamic::default();
c.max_minute_move_percent = 0.0;
c.validate();
for buy in [false, true] {
let mut b = book();
let (value, after, count) = b.fill(&c, "minecraft:diamond", 200.0, 0.01, 100_000, buy);
assert_eq!(count, 100_000);
b.track("minecraft:diamond").factor = after.exp();
let (returned, _, count) = b.fill(&c, "minecraft:diamond", 200.0, 0.01, count, !buy);
assert_eq!(count, 100_000);
let (paid, received) = if buy { (value, returned) } else { (returned, value) };
assert!(to_get(received) <= to_pay(paid));
}
}
#[test]
fn a_round_trip_earns_nothing_for_1_64_and_past_the_band() {
// 1, 64, and 2,000 (far past the minute's 5% band: only what fits
+3
View File
@@ -49,3 +49,6 @@ pub mod smp_shop;
pub mod smp_store;
#[path = "../mixins/smp/smp_trade.rs"]
pub mod smp_trade;
#[path = "../mixins/smp/smp_config.rs"]
pub mod smp_config;