feat(bridge)!: Protocol 3.0 cutover — world.ruleset, points.board, vendor.listing #6
@@ -48,6 +48,25 @@ PresenceSweepSeconds=30
|
|||||||
# house.remove (owner, region, location, decay). Houses change slowly; a few minutes is fine.
|
# house.remove (owner, region, location, decay). Houses change slowly; a few minutes is fine.
|
||||||
HousingSweepSeconds=300
|
HousingSweepSeconds=300
|
||||||
|
|
||||||
|
# Shard ruleset (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v3.md §5). One world.ruleset frame — expansion, which
|
||||||
|
# systems are on, skill/stat caps, account and house limits, champion scroll rules —
|
||||||
|
# emitted on every sidecar connect (and on [bridge reload), so the website's rules page
|
||||||
|
# cannot drift from the server. Not a sweep: it changes only when you edit a .cfg.
|
||||||
|
#
|
||||||
|
# The frame is built from an explicit allowlist of keys in BridgeRuleset.cs. Server.cfg,
|
||||||
|
# Staff.cfg, Email.cfg, DataPath.cfg, Bridge.cfg, Compiler.cfg, Reports.cfg and Client.cfg
|
||||||
|
# are never read.
|
||||||
|
RulesetEnabled=true
|
||||||
|
|
||||||
|
# The one connection detail the bridge will publish, e.g. play.myshard.com,2593. Blank
|
||||||
|
# (the default) omits it entirely. Server.cfg's Address/Listen/Port are NEVER published —
|
||||||
|
# if you want a connect string on the site, put it here deliberately.
|
||||||
|
PublicConnectAddress=
|
||||||
|
|
||||||
|
# Include the save/restart schedule (AutoSave frequency, AutoRestart hour) in the frame.
|
||||||
|
# Turn off if you would rather not advertise a predictable restart window.
|
||||||
|
RulesetIncludeSchedule=true
|
||||||
|
|
||||||
# Shown to a player when they run [link. The website page where they enter the code.
|
# Shown to a player when they run [link. The website page where they enter the code.
|
||||||
LinkUrl=https://yoursite/link
|
LinkUrl=https://yoursite/link
|
||||||
|
|
||||||
|
|||||||
@@ -165,8 +165,11 @@ namespace Server.Custom.Bridge
|
|||||||
BridgeGovernance.Rearm();
|
BridgeGovernance.Rearm();
|
||||||
BridgePresence.Rearm();
|
BridgePresence.Rearm();
|
||||||
BridgeHousing.Rearm();
|
BridgeHousing.Rearm();
|
||||||
|
// Not a sweep, so it has nothing to re-arm — but an operator who just edited a
|
||||||
|
// .cfg wants the change on the site now, not after a shard restart.
|
||||||
|
BridgeRuleset.Emit();
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
|
||||||
e.Mobile.SendMessage("Bridge: sweeps re-armed; endpoint changes take effect on reconnect.");
|
e.Mobile.SendMessage("Bridge: sweeps re-armed; ruleset re-emitted; endpoint changes take effect on reconnect.");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ping":
|
case "ping":
|
||||||
@@ -203,6 +206,7 @@ namespace Server.Custom.Bridge
|
|||||||
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeHousing.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeHousing.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgePages.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgePages.Status());
|
||||||
|
e.Mobile.SendMessage("Bridge: {0}", BridgeRuleset.Status());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ namespace Server.Custom.Bridge
|
|||||||
public static int PresenceSweepSeconds { get; private set; }
|
public static int PresenceSweepSeconds { get; private set; }
|
||||||
public static int HousingSweepSeconds { get; private set; }
|
public static int HousingSweepSeconds { get; private set; }
|
||||||
|
|
||||||
|
// ---- shard ruleset (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v3.md §5) ----
|
||||||
|
public static bool RulesetEnabled { get; private set; }
|
||||||
|
public static string PublicConnectAddress { get; private set; }
|
||||||
|
public static bool RulesetIncludeSchedule { get; private set; }
|
||||||
|
|
||||||
public static string LinkUrl { get; private set; }
|
public static string LinkUrl { get; private set; }
|
||||||
|
|
||||||
public static int TownCrierMaxLines { get; private set; }
|
public static int TownCrierMaxLines { get; private set; }
|
||||||
@@ -107,6 +112,14 @@ namespace Server.Custom.Bridge
|
|||||||
if (HousingSweepSeconds < 1)
|
if (HousingSweepSeconds < 1)
|
||||||
HousingSweepSeconds = 1;
|
HousingSweepSeconds = 1;
|
||||||
|
|
||||||
|
// The ruleset frame is not a sweep — it is emitted once per sidecar connect (and on
|
||||||
|
// `[bridge reload`), so it has no interval. PublicConnectAddress is the ONE connection
|
||||||
|
// detail the bridge will publish, and only because an operator typed it here for that
|
||||||
|
// purpose; Server.cfg's Address/Port are never read (see BridgeRuleset's allowlist note).
|
||||||
|
RulesetEnabled = Config.Get("Bridge.RulesetEnabled", true);
|
||||||
|
PublicConnectAddress = Config.Get("Bridge.PublicConnectAddress", "");
|
||||||
|
RulesetIncludeSchedule = Config.Get("Bridge.RulesetIncludeSchedule", true);
|
||||||
|
|
||||||
LinkUrl = Config.Get("Bridge.LinkUrl", "https://yoursite/link");
|
LinkUrl = Config.Get("Bridge.LinkUrl", "https://yoursite/link");
|
||||||
|
|
||||||
TownCrierMaxLines = Config.Get("Bridge.TownCrierMaxLines", 6);
|
TownCrierMaxLines = Config.Get("Bridge.TownCrierMaxLines", 6);
|
||||||
|
|||||||
355
overlay/Scripts/Custom/Bridge/BridgeRuleset.cs
Normal file
355
overlay/Scripts/Custom/Bridge/BridgeRuleset.cs
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using Server.Engines.CityLoyalty;
|
||||||
|
using Server.Engines.VvV;
|
||||||
|
using Server.Multis;
|
||||||
|
|
||||||
|
namespace Server.Custom.Bridge
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The shard ruleset (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v3.md §5). One `world.ruleset` frame describing how
|
||||||
|
/// this shard is actually configured: expansion, which systems are on, skill/stat caps, house
|
||||||
|
/// and account limits, champion scroll rules, and the save/restart schedule. It is what turns
|
||||||
|
/// the website's "Rules" page from hand-maintained prose into something that cannot drift from
|
||||||
|
/// the server.
|
||||||
|
///
|
||||||
|
/// Modelled on <see cref="BridgeBoot.EmitHello"/>, NOT on the diff sweeps: the ruleset changes
|
||||||
|
/// only when an operator edits Config/*.cfg and restarts (or runs `[bridge reload`), so there is
|
||||||
|
/// nothing to poll. It subscribes Connected_Core so a sidecar that comes up second still learns
|
||||||
|
/// the ruleset, exactly as server.hello does.
|
||||||
|
///
|
||||||
|
/// **The one hard rule: this is an explicit allowlist of Config.Get calls.** Never enumerate
|
||||||
|
/// Config.Entries (Server/Config.cs) — that would sweep in every key on the server, secrets
|
||||||
|
/// included. Files deliberately never read here, in addition to anything not named below:
|
||||||
|
///
|
||||||
|
/// Server.cfg — Address / Listen / Port. Only Bridge.PublicConnectAddress is published,
|
||||||
|
/// and only because an operator typed it there for exactly this purpose.
|
||||||
|
/// Staff.cfg — staff account names.
|
||||||
|
/// Email.cfg — SMTP credentials.
|
||||||
|
/// DataPath.cfg — filesystem layout.
|
||||||
|
/// Bridge.cfg — the sidecar host/port and our own caps.
|
||||||
|
/// Compiler.cfg — build flags.
|
||||||
|
/// Reports.cfg — report upload credentials.
|
||||||
|
/// Client.cfg — client-version enforcement (not player-facing rules).
|
||||||
|
///
|
||||||
|
/// `rev` is an FNV-1a hash of the emitted body, so a reconnect that carries an unchanged ruleset
|
||||||
|
/// is a no-op site-side. String.GetHashCode() is deliberately NOT used: it is randomized per
|
||||||
|
/// process on modern .NET, so it would change on every shard restart and defeat the whole point.
|
||||||
|
/// </summary>
|
||||||
|
public static class BridgeRuleset
|
||||||
|
{
|
||||||
|
private static long _emitted;
|
||||||
|
private static string _rev = "";
|
||||||
|
private static int _bytes;
|
||||||
|
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
if (!BridgeConfig.Enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
BridgeLink.Connected_Core += Emit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Status()
|
||||||
|
{
|
||||||
|
return String.Format("ruleset(enabled={0} emitted={1} rev={2} bytes={3})",
|
||||||
|
BridgeConfig.RulesetEnabled, _emitted, _rev.Length == 0 ? "-" : _rev, _bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Core thread. Builds and queues one `world.ruleset` frame. Called on every sidecar
|
||||||
|
/// connect and by `[bridge reload` (an operator who just edited a .cfg wants to see the
|
||||||
|
/// change on the site without restarting the shard).
|
||||||
|
/// </summary>
|
||||||
|
public static void Emit()
|
||||||
|
{
|
||||||
|
if (!BridgeConfig.RulesetEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var body = BuildBody();
|
||||||
|
|
||||||
|
_rev = Fnv1a(body);
|
||||||
|
_bytes = body.Length;
|
||||||
|
_emitted++;
|
||||||
|
|
||||||
|
// rev goes first so a reader can short-circuit on an unchanged frame before parsing
|
||||||
|
// the rest of it.
|
||||||
|
BridgeLink.Emit(BridgeJson.Begin("world.ruleset")
|
||||||
|
.Str("rev", _rev)
|
||||||
|
.Append(body)
|
||||||
|
.End());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Bridge] ruleset emit threw: {0}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The allowlist. Every block is optional and omitted when its system is off, so a shard
|
||||||
|
/// that does not run (say) VvV publishes no `vvv` block rather than a block of zeroes.
|
||||||
|
/// </summary>
|
||||||
|
private static string BuildBody()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(2048);
|
||||||
|
|
||||||
|
sb.Str("shard", Server.Misc.ServerList.ServerName);
|
||||||
|
sb.Str("expansion", Core.Expansion.ToString());
|
||||||
|
|
||||||
|
// The ONLY thing published from a connection-address setting, and only because the
|
||||||
|
// operator put it in Bridge.cfg specifically to be shown. Server.cfg is never read.
|
||||||
|
var connect = BridgeConfig.PublicConnectAddress;
|
||||||
|
if (!String.IsNullOrEmpty(connect))
|
||||||
|
sb.Str("connect", connect);
|
||||||
|
|
||||||
|
WriteSystems(sb);
|
||||||
|
WriteCaps(sb);
|
||||||
|
|
||||||
|
sb.Append(",\"housing\":{\"accountHouseLimit\":")
|
||||||
|
.Append(BaseHouse.AccountHouseLimit).Append('}');
|
||||||
|
|
||||||
|
WriteAccounts(sb);
|
||||||
|
WriteVetRewards(sb);
|
||||||
|
WriteLoot(sb);
|
||||||
|
WriteVendors(sb);
|
||||||
|
WriteChampions(sb);
|
||||||
|
WriteTreasureMaps(sb);
|
||||||
|
WriteVvV(sb);
|
||||||
|
WriteStore(sb);
|
||||||
|
|
||||||
|
if (BridgeConfig.RulesetIncludeSchedule)
|
||||||
|
WriteSchedule(sb);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Which optional systems this shard runs. Read from each system's own static rather than
|
||||||
|
/// re-parsing its .cfg, so a system that derives its state (Factions is on exactly when VvV
|
||||||
|
/// is off — Services/Factions/Core/Faction.cs) is reported the way the server actually sees
|
||||||
|
/// it. This block subsumes the `world.systems` capability frame PROTOCOL_2.md §10.4
|
||||||
|
/// sketched but never implemented.
|
||||||
|
/// </summary>
|
||||||
|
private static void WriteSystems(StringBuilder sb)
|
||||||
|
{
|
||||||
|
sb.Append(",\"systems\":{");
|
||||||
|
sb.Append("\"cityLoyalty\":").Append(Json(CityLoyaltySystem.Enabled));
|
||||||
|
sb.Append(",\"vvv\":").Append(Json(ViceVsVirtueSystem.Enabled));
|
||||||
|
sb.Append(",\"factions\":").Append(Json(Server.Factions.Settings.Enabled));
|
||||||
|
sb.Append(",\"siege\":").Append(Json(Siege.SiegeShard));
|
||||||
|
sb.Append(",\"chat\":").Append(Json(Config.Get("Chat.Enabled", true)));
|
||||||
|
sb.Append(",\"store\":").Append(Json(Config.Get("Store.Enabled", true)));
|
||||||
|
sb.Append(",\"dailyRares\":").Append(Json(Config.Get("DailyRares.Enabled", true)));
|
||||||
|
sb.Append(",\"honesty\":").Append(Json(Config.Get("Honesty.Enabled", true)));
|
||||||
|
sb.Append(",\"shadowguard\":").Append(Json(Core.TOL));
|
||||||
|
sb.Append(",\"treasureMaps\":").Append(Json(Config.Get("TreasureMaps.Enabled", true)));
|
||||||
|
sb.Append(",\"vetRewards\":").Append(Json(Config.Get("VetRewards.Enabled", true)));
|
||||||
|
sb.Append(",\"testCenter\":").Append(Json(Config.Get("TestCenter.Enabled", false)));
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Skill and stat caps — the single most-asked "what are the rules here?" question, and the
|
||||||
|
/// one most often wrong on a hand-written page. SkillCap is in tenths (1000 = 100.0).
|
||||||
|
/// </summary>
|
||||||
|
private static void WriteCaps(StringBuilder sb)
|
||||||
|
{
|
||||||
|
sb.Append(",\"caps\":{");
|
||||||
|
sb.Append("\"skill\":").Append(Config.Get("PlayerCaps.SkillCap", 1000));
|
||||||
|
sb.Append(",\"totalSkill\":").Append(Config.Get("PlayerCaps.TotalSkillCap", 7000));
|
||||||
|
sb.Append(",\"stat\":").Append(Config.Get("PlayerCaps.TotalStatCap", 225));
|
||||||
|
sb.Append(",\"str\":").Append(Config.Get("PlayerCaps.StrCap", 125));
|
||||||
|
sb.Append(",\"dex\":").Append(Config.Get("PlayerCaps.DexCap", 125));
|
||||||
|
sb.Append(",\"int\":").Append(Config.Get("PlayerCaps.IntCap", 125));
|
||||||
|
sb.Append(",\"strMax\":").Append(Config.Get("PlayerCaps.StrMaxCap", 150));
|
||||||
|
sb.Append(",\"dexMax\":").Append(Config.Get("PlayerCaps.DexMaxCap", 150));
|
||||||
|
sb.Append(",\"intMax\":").Append(Config.Get("PlayerCaps.IntMaxCap", 150));
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Account limits. `autoCreate` is the in-game first-login auto-create switch, which pairs
|
||||||
|
/// with the bridge's own SignupMode (BridgeConfig.WarnOnSignupMismatch) — publishing it
|
||||||
|
/// lets the site's signup page tell a visitor the truth about how to get an account.
|
||||||
|
/// Character slots come from Siege.cfg, which is where ServUO keeps them regardless of
|
||||||
|
/// whether the shard is actually Siege.
|
||||||
|
/// </summary>
|
||||||
|
private static void WriteAccounts(StringBuilder sb)
|
||||||
|
{
|
||||||
|
sb.Append(",\"accounts\":{");
|
||||||
|
sb.Append("\"perIp\":").Append(Config.Get("Accounts.AccountsPerIp", 1));
|
||||||
|
sb.Append(",\"charSlots\":").Append(Siege.CharacterSlots);
|
||||||
|
sb.Append(",\"autoCreate\":").Append(Json(Config.Get("Accounts.AutoCreateAccounts", true)));
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteVetRewards(StringBuilder sb)
|
||||||
|
{
|
||||||
|
var enabled = Config.Get("VetRewards.Enabled", true);
|
||||||
|
|
||||||
|
sb.Append(",\"vetRewards\":{\"enabled\":").Append(Json(enabled));
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
var interval = Config.Get("VetRewards.RewardInterval", TimeSpan.FromDays(30.0));
|
||||||
|
sb.Append(",\"rewardIntervalDays\":").Append((int)interval.TotalDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The Felucca risk-vs-reward numbers — the reason players choose a facet.</summary>
|
||||||
|
private static void WriteLoot(StringBuilder sb)
|
||||||
|
{
|
||||||
|
sb.Append(",\"loot\":{");
|
||||||
|
sb.Append("\"feluccaLuckBonus\":").Append(Config.Get("Loot.FeluccaLuckBonus", 0));
|
||||||
|
sb.Append(",\"feluccaBudgetBonus\":").Append(Config.Get("Loot.FeluccaBudgetBonus", 0));
|
||||||
|
sb.Append(",\"feluccaMaxProps\":").Append(Config.Get("Loot.MaxProps", 5));
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteVendors(StringBuilder sb)
|
||||||
|
{
|
||||||
|
sb.Append(",\"vendors\":{");
|
||||||
|
sb.Append("\"restockDelayMinutes\":").Append(Config.Get("Vendors.RestockDelay", 60));
|
||||||
|
sb.Append(",\"maxSell\":").Append(Config.Get("Vendors.MaxSell", 500));
|
||||||
|
sb.Append(",\"economyStockAmount\":").Append(Config.Get("Vendors.EconomyStockAmount", 500));
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Champion spawn rewards. `rankThresholds` is the red-skull count at which each rank is
|
||||||
|
/// reached, which is what a player actually wants to know before committing to a spawn.
|
||||||
|
/// </summary>
|
||||||
|
private static void WriteChampions(StringBuilder sb)
|
||||||
|
{
|
||||||
|
if (!Config.Get("Champions.Enabled", true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
sb.Append(",\"champions\":{");
|
||||||
|
sb.Append("\"powerScrolls\":").Append(Config.Get("Champions.PowerScrolls", 6));
|
||||||
|
sb.Append(",\"statScrolls\":").Append(Config.Get("Champions.StatScrolls", 16));
|
||||||
|
sb.Append(",\"scrollChance\":").Append(Json(Config.Get("Champions.ScrollChance", 0.1)));
|
||||||
|
sb.Append(",\"transcendenceChance\":")
|
||||||
|
.Append(Json(Config.Get("Champions.TranscendenceChance", 50.0)));
|
||||||
|
sb.Append(",\"rankThresholds\":[")
|
||||||
|
.Append(Config.Get("Champions.Rank2RedSkulls", 5)).Append(',')
|
||||||
|
.Append(Config.Get("Champions.Rank3RedSkulls", 10)).Append(',')
|
||||||
|
.Append(Config.Get("Champions.Rank4RedSkulls", 13))
|
||||||
|
.Append(']');
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteTreasureMaps(StringBuilder sb)
|
||||||
|
{
|
||||||
|
var enabled = Config.Get("TreasureMaps.Enabled", true);
|
||||||
|
|
||||||
|
sb.Append(",\"treasureMaps\":{\"enabled\":").Append(Json(enabled));
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
sb.Append(",\"lootChance\":").Append(Json(Config.Get("TreasureMaps.LootChance", 0.01)));
|
||||||
|
sb.Append(",\"resetDays\":").Append(Json(Config.Get("TreasureMaps.ResetTime", 30.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteVvV(StringBuilder sb)
|
||||||
|
{
|
||||||
|
if (!ViceVsVirtueSystem.Enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
sb.Append(",\"vvv\":{");
|
||||||
|
sb.Append("\"enabled\":true");
|
||||||
|
sb.Append(",\"startSilver\":").Append(ViceVsVirtueSystem.StartSilver);
|
||||||
|
sb.Append(",\"enhancedRules\":").Append(Json(ViceVsVirtueSystem.EnhancedRules));
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Ultima Store. Only `enabled` and the currency's display name — never the store's
|
||||||
|
/// price table or any payment configuration, neither of which lives in Config anyway.
|
||||||
|
/// </summary>
|
||||||
|
private static void WriteStore(StringBuilder sb)
|
||||||
|
{
|
||||||
|
var enabled = Config.Get("Store.Enabled", true);
|
||||||
|
|
||||||
|
sb.Append(",\"store\":{\"enabled\":").Append(Json(enabled));
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
sb.Str("currencyName", Config.Get("Store.CurrencyName", "Sovereigns"));
|
||||||
|
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save and restart schedule — "when does the shard hiccup?", the other question a live
|
||||||
|
/// status page is asked. Off behind RulesetIncludeSchedule for an operator who would rather
|
||||||
|
/// not advertise a predictable restart window.
|
||||||
|
/// </summary>
|
||||||
|
private static void WriteSchedule(StringBuilder sb)
|
||||||
|
{
|
||||||
|
sb.Append(",\"schedule\":{");
|
||||||
|
|
||||||
|
var saves = Config.Get("AutoSave.Enabled", true);
|
||||||
|
sb.Append("\"autoSaveEnabled\":").Append(Json(saves));
|
||||||
|
|
||||||
|
if (saves)
|
||||||
|
{
|
||||||
|
var freq = Config.Get("AutoSave.Frequency", TimeSpan.FromMinutes(5.0));
|
||||||
|
sb.Append(",\"autoSaveFrequencyMinutes\":").Append((int)freq.TotalMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var restart = Config.Get("AutoRestart.Enabled", false);
|
||||||
|
sb.Append(",\"autoRestartEnabled\":").Append(Json(restart));
|
||||||
|
|
||||||
|
if (restart)
|
||||||
|
{
|
||||||
|
sb.Append(",\"autoRestartHour\":").Append(Config.Get("AutoRestart.Hour", 12));
|
||||||
|
sb.Append(",\"autoRestartMinute\":").Append(Config.Get("AutoRestart.Minute", 0));
|
||||||
|
sb.Append(",\"autoRestartFrequencyHours\":").Append(Config.Get("AutoRestart.Frequency", 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers ----
|
||||||
|
|
||||||
|
private static string Json(bool value)
|
||||||
|
{
|
||||||
|
return value ? "true" : "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Json(double value)
|
||||||
|
{
|
||||||
|
return value.ToString("R", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// FNV-1a over the UTF-16 code units of the body, as 8 lowercase hex digits. Any stable
|
||||||
|
/// hash would do; what matters is that it is stable ACROSS PROCESSES, which
|
||||||
|
/// String.GetHashCode() is not (it is seeded randomly per process), so using that would
|
||||||
|
/// produce a different rev after every restart and make the whole diff pointless.
|
||||||
|
/// </summary>
|
||||||
|
private static string Fnv1a(string s)
|
||||||
|
{
|
||||||
|
const uint offset = 2166136261;
|
||||||
|
const uint prime = 16777619;
|
||||||
|
|
||||||
|
uint hash = offset;
|
||||||
|
|
||||||
|
for (int i = 0; i < s.Length; i++)
|
||||||
|
{
|
||||||
|
char c = s[i];
|
||||||
|
hash = (hash ^ (byte)(c & 0xFF)) * prime;
|
||||||
|
hash = (hash ^ (byte)(c >> 8)) * prime;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.ToString("x8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user