mirror of
https://github.com/horizon-rust/horizon-mods.git
synced 2026-11-07 11:05:58 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a086c90431 |
No files matched your search
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "horizon-mod-smp"
|
||||
version = "0.30.1"
|
||||
version = "0.31.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "horizon-mod-smp"
|
||||
version = "0.30.1"
|
||||
version = "0.31.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user