chore(tools): dress a seeded world so it can be screenshotted
BridgeSeeder builds a world at realistic scale, which is what the bridge needed; it never needed the world to look like anything. So a vendor is `seed vendor` trading as `Seed Shop 810`, a character is `Seed004A` and a house sign reads `Seed House 12` — and every one of those strings travels the whole bridge and lands on the marketplace, the guild roster and the housing pages of the website. That is fine for a protocol test and wrong for the marketing site's screenshots (runicgateway.com PLAN.md §13 phase 9, D42/D46). BridgeDemoDress renames them in place and seeds nothing: prices, listing counts, decay stages, fame and skills stay exactly as the seeder left them, so the data keeps its provenance and only the strings a human reads change. Names come from fixed tables hashed off each object's serial, so a re-run reproduces the same world and screenshots can be retaken later and still match. It also does two things the screenshots needed and nothing else provides: - stages a few condemned houses back into the last decay levels, because decay is a live process and "Houses in danger" is empty by the time anyone looks - sets a known password on seed_000, because logging a character in is the only way to make the online roster non-empty and the seeder assigns a random GUID. The password is read from Bridge.cfg, never compiled in. Test scaffolding, in tools/, never deployed — deploy.ps1 copies overlay/ only. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
402
tools/scaffolding/BridgeDemoDress.cs
Normal file
402
tools/scaffolding/BridgeDemoDress.cs
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using Server.Accounting;
|
||||||
|
using Server.Commands;
|
||||||
|
using Server.Mobiles;
|
||||||
|
using Server.Multis;
|
||||||
|
|
||||||
|
namespace Server.Custom
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gives a BridgeSeeder world presentable names, so a shard standing behind a public
|
||||||
|
/// screenshot does not read as test data.
|
||||||
|
///
|
||||||
|
/// Test scaffolding. Not part of the bridge. Never deployed — see tools/README.md.
|
||||||
|
///
|
||||||
|
/// WHY THIS EXISTS
|
||||||
|
/// ---------------
|
||||||
|
/// BridgeSeeder builds a world at realistic SCALE, which is what the bridge needed:
|
||||||
|
/// 50 accounts, 150 characters, 30 houses, 30 vendors, 1,200 listings. It never needed
|
||||||
|
/// the world to look like anything, so a vendor is "seed vendor" trading as
|
||||||
|
/// "Seed Shop 810" and a character is "Seed004A". Every one of those names travels the
|
||||||
|
/// whole bridge — plugin, sidecar, website — and lands on the marketplace, the guild
|
||||||
|
/// roster and the housing pages, which are exactly the pages a screenshot wants.
|
||||||
|
///
|
||||||
|
/// This pass renames what is already there rather than seeding anything new. That
|
||||||
|
/// matters: the data keeps its provenance. The prices, the listing counts, the decay
|
||||||
|
/// stages, the fame and the skill sheets are all still whatever BridgeSeeder produced
|
||||||
|
/// and whatever the shard has done to them since — only the strings a human reads are
|
||||||
|
/// replaced. Nothing here invents shard state that the game did not produce.
|
||||||
|
///
|
||||||
|
/// IDEMPOTENT, AND DETERMINISTIC
|
||||||
|
/// -----------------------------
|
||||||
|
/// Names come from fixed tables indexed by the object's own serial, so the same vendor
|
||||||
|
/// draws the same shop name on every run against the same save — screenshots retaken
|
||||||
|
/// later still match. A second run is therefore a no-op, and a world half-dressed by an
|
||||||
|
/// interrupted run finishes cleanly.
|
||||||
|
///
|
||||||
|
/// Shop and house names are also re-dressed when they are names THIS pass produced, so
|
||||||
|
/// a change to the tables or to the hash can be applied to a world that has already been
|
||||||
|
/// through here once. Character names are not: a person's name is an ordinary string
|
||||||
|
/// with no closed set to recognise it by, so once dressed it is left alone.
|
||||||
|
///
|
||||||
|
/// WHAT IT ALSO DOES, AND WHY EACH IS HERE
|
||||||
|
/// ---------------------------------------
|
||||||
|
/// - Stages houses back into IDOC. The seeder condemned eighteen of them, but decay is
|
||||||
|
/// a live process: by the time anybody looks, the stages have moved on and the
|
||||||
|
/// "Houses in danger" page is empty. Empty is a true state and a poor screenshot, so
|
||||||
|
/// this puts a handful back.
|
||||||
|
/// - Sets a known password on one seeded account. Logging a character in is the only
|
||||||
|
/// way to make the online roster non-empty, and it needs a client, and a client needs
|
||||||
|
/// a password. The seeder gives every account a random GUID nobody kept.
|
||||||
|
///
|
||||||
|
/// Flag: `DemoDressOnStart=True` in Config/Bridge.cfg. In game: `[demodress`.
|
||||||
|
/// </summary>
|
||||||
|
public static class BridgeDemoDress
|
||||||
|
{
|
||||||
|
private const string Prefix = "seed_";
|
||||||
|
|
||||||
|
/// <summary>The account whose password is set, so a character can be logged in.</summary>
|
||||||
|
private const string LoginAccount = "seed_000";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read from Config/Bridge.cfg (`DemoDressPassword`) so a password never lands in
|
||||||
|
/// source control. Absent means the account is left alone.
|
||||||
|
/// </summary>
|
||||||
|
private static string LoginPassword
|
||||||
|
{
|
||||||
|
get { return Config.Get("Bridge.DemoDressPassword", default(string)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>How many condemned houses to put back into the last two decay stages.</summary>
|
||||||
|
private const int IdocHouses = 4;
|
||||||
|
|
||||||
|
// ── Name tables ────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Ordinary fantasy given names and English trade-sign nouns. Deliberately dull: the
|
||||||
|
// point is that a reader's eye passes over them, which is what a real roster does.
|
||||||
|
|
||||||
|
private static readonly string[] Given =
|
||||||
|
{
|
||||||
|
"Alaric", "Bess", "Corwin", "Dagna", "Edric", "Fenna", "Garrick", "Halle",
|
||||||
|
"Ivo", "Jessa", "Kellen", "Lira", "Marek", "Nessa", "Orrin", "Perrin",
|
||||||
|
"Quill", "Rowan", "Sera", "Tamsin", "Ulric", "Vera", "Wendel", "Xanthe",
|
||||||
|
"Yorick", "Zara", "Bram", "Caitrin", "Doran", "Elspeth"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] Family =
|
||||||
|
{
|
||||||
|
"Ashdown", "Bellweather", "Crowe", "Dunmore", "Eastgate", "Fairbourne",
|
||||||
|
"Grimsby", "Hollowell", "Ironwood", "Larkspur", "Mosswick", "Thornbury"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] ShopFirst =
|
||||||
|
{
|
||||||
|
"The Copper", "The Silver", "The Gilded", "The Iron", "The Rusted", "The Amber",
|
||||||
|
"The Quiet", "The Crooked", "The Old", "The Wandering", "The Salted", "The Ember"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] ShopSecond =
|
||||||
|
{
|
||||||
|
"Anvil", "Kettle", "Lantern", "Compass", "Bellows", "Flask", "Ledger",
|
||||||
|
"Wagon", "Tankard", "Whetstone", "Sextant", "Coffer"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] HouseNames =
|
||||||
|
{
|
||||||
|
"Ashwood Cottage", "Bramblegate", "Candlewick House", "Dovecote",
|
||||||
|
"Eastmarch", "Fernhollow", "Greywater", "Hearthstone",
|
||||||
|
"Ivyfall", "Kestrel Lodge", "Longmeadow", "Millrace",
|
||||||
|
"Northrest", "Oakenshaw", "Pinefall", "Quarrystone",
|
||||||
|
"Riverwatch", "Stonebrook", "Thistledown", "Umberley",
|
||||||
|
"Vinesend", "Westbarrow", "Yewcross", "Almsgate",
|
||||||
|
"Brightmoor", "Coldspring", "Duskvale", "Elmshade",
|
||||||
|
"Foxhollow", "Gravensward"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
CommandSystem.Register("demodress", AccessLevel.Administrator, Dress_OnCommand);
|
||||||
|
|
||||||
|
if (Config.Get("Bridge.DemoDressOnStart", false))
|
||||||
|
EventSink.ServerStarted += () => Run(null, save: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Usage("demodress")]
|
||||||
|
[Description("Renames BridgeSeeder's synthetic world so it is presentable in screenshots.")]
|
||||||
|
private static void Dress_OnCommand(CommandEventArgs e)
|
||||||
|
{
|
||||||
|
Run(e.Mobile, save: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Report(Mobile to, string text)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[BridgeDemoDress] " + text);
|
||||||
|
|
||||||
|
if (to != null)
|
||||||
|
to.SendMessage(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Run(Mobile to, bool save)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var start = DateTime.UtcNow;
|
||||||
|
|
||||||
|
int chars = DressCharacters();
|
||||||
|
int vendors = DressVendors();
|
||||||
|
int houses = DressHouses();
|
||||||
|
int idoc = StageIdoc();
|
||||||
|
bool password = SetLoginPassword(to);
|
||||||
|
|
||||||
|
Report(to, String.Format(
|
||||||
|
"Dressed {0} characters, {1} vendors, {2} house signs; staged {3} IDOC; " +
|
||||||
|
"login password {4}. ({5:F1}s)",
|
||||||
|
chars, vendors, houses, idoc, password ? "set" : "skipped",
|
||||||
|
(DateTime.UtcNow - start).TotalSeconds));
|
||||||
|
|
||||||
|
if (save)
|
||||||
|
{
|
||||||
|
Report(to, "Saving world...");
|
||||||
|
World.Save();
|
||||||
|
Report(to, "Save complete.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Report(to, "FAILED: " + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A stable index for a world object, salted so that two names drawn for the SAME
|
||||||
|
/// object land in unrelated places in their tables.
|
||||||
|
///
|
||||||
|
/// Serial is the only identifier that survives a save and is identical on every
|
||||||
|
/// load, which is what makes the naming reproducible. But serials are dense and
|
||||||
|
/// sequential, so a weak mix hands neighbouring objects neighbouring names. The
|
||||||
|
/// first attempt derived the second word from `serial / 5`, which is constant
|
||||||
|
/// across five consecutive serials — twenty-seven vendors came out as four
|
||||||
|
/// Flasks, four Lanterns and three Anvils in a row. Salting and re-mixing per
|
||||||
|
/// draw is what fixes that: each word is an independent hash of the pair.
|
||||||
|
/// </summary>
|
||||||
|
private static int Pick(int serial, int salt, int modulus)
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
uint h = (uint)serial ^ ((uint)salt * 0x9E3779B1u);
|
||||||
|
h ^= h >> 15;
|
||||||
|
h *= 2246822519u;
|
||||||
|
h ^= h >> 13;
|
||||||
|
h *= 3266489917u;
|
||||||
|
h ^= h >> 16;
|
||||||
|
return (int)(h % (uint)modulus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PersonName(int serial)
|
||||||
|
{
|
||||||
|
return Given[Pick(serial, 1, Given.Length)] + " " + Family[Pick(serial, 2, Family.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ShopSign(int serial)
|
||||||
|
{
|
||||||
|
return ShopFirst[Pick(serial, 3, ShopFirst.Length)] + " " +
|
||||||
|
ShopSecond[Pick(serial, 4, ShopSecond.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksSeeded(string name, string marker)
|
||||||
|
{
|
||||||
|
return name != null && name.StartsWith(marker, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when a name is one this pass could have produced.
|
||||||
|
///
|
||||||
|
/// Dressing has to be re-runnable in both directions: a first pass renames what the
|
||||||
|
/// seeder left, and a later pass — after the tables or the hash change — has to be
|
||||||
|
/// able to rename its own earlier output. A name is recognised by MEMBERSHIP of the
|
||||||
|
/// closed tables rather than by a marker on the object, because the object is a
|
||||||
|
/// PlayerVendor whose name is a plain string with nowhere to hide a flag, and a
|
||||||
|
/// name that is not in the tables was set by a person and is left alone.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsOurs(string name, string[] first, string[] second)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrEmpty(name))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach (var a in first)
|
||||||
|
{
|
||||||
|
if (!name.StartsWith(a + " ", StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var rest = name.Substring(a.Length + 1);
|
||||||
|
|
||||||
|
foreach (var b in second)
|
||||||
|
{
|
||||||
|
if (rest == b)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsOurHouseName(string name)
|
||||||
|
{
|
||||||
|
foreach (var h in HouseNames)
|
||||||
|
{
|
||||||
|
if (h == name)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DressCharacters()
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
foreach (Account acct in Accounts.GetAccounts())
|
||||||
|
{
|
||||||
|
if (!acct.Username.StartsWith(Prefix, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (int i = 0; i < acct.Length; i++)
|
||||||
|
{
|
||||||
|
var m = acct[i];
|
||||||
|
|
||||||
|
if (m == null || !LooksSeeded(m.Name, "Seed"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Offset by the slot so an account's three characters are three people
|
||||||
|
// rather than three spellings of one.
|
||||||
|
m.Name = PersonName(m.Serial.Value + i * 101);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DressVendors()
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
if (PlayerVendor.PlayerVendors == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// PlayerVendors is a live collection; the rename does not add or remove members,
|
||||||
|
// but copy anyway so an unrelated vendor placement mid-pass cannot invalidate it.
|
||||||
|
var vendors = new List<PlayerVendor>(PlayerVendor.PlayerVendors);
|
||||||
|
|
||||||
|
foreach (var vendor in vendors)
|
||||||
|
{
|
||||||
|
bool touched = false;
|
||||||
|
|
||||||
|
if (LooksSeeded(vendor.ShopName, "Seed Shop") ||
|
||||||
|
IsOurs(vendor.ShopName, ShopFirst, ShopSecond))
|
||||||
|
{
|
||||||
|
var sign = ShopSign(vendor.Serial.Value);
|
||||||
|
|
||||||
|
if (sign != vendor.ShopName)
|
||||||
|
{
|
||||||
|
vendor.ShopName = sign;
|
||||||
|
touched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LooksSeeded(vendor.Name, "seed vendor"))
|
||||||
|
{
|
||||||
|
vendor.Name = PersonName(vendor.Serial.Value + 7919);
|
||||||
|
touched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touched)
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DressHouses()
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
foreach (var house in BaseHouse.AllHouses)
|
||||||
|
{
|
||||||
|
if (house.Sign == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!LooksSeeded(house.Sign.Name, "Seed House") && !IsOurHouseName(house.Sign.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var name = HouseNames[Pick(house.Serial.Value, 5, HouseNames.Length)];
|
||||||
|
|
||||||
|
if (name == house.Sign.Name)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
house.Sign.Name = name;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Puts a few condemned houses back into the final decay stages.
|
||||||
|
///
|
||||||
|
/// Only houses that CAN decay are touched — an active owner's AutoRefresh house is
|
||||||
|
/// left alone, because forcing one into IDOC would be inventing a state the game
|
||||||
|
/// would never produce and the next refresh would undo it anyway.
|
||||||
|
/// </summary>
|
||||||
|
private static int StageIdoc()
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
foreach (var house in BaseHouse.AllHouses)
|
||||||
|
{
|
||||||
|
if (n >= IdocHouses)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!house.CanDecay || house.DecayLevel == DecayLevel.IDOC)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
house.SetDynamicDecay(n % 2 == 0 ? DecayLevel.IDOC : DecayLevel.Greatly);
|
||||||
|
house.NextDecayStage = DateTime.UtcNow + TimeSpan.FromHours(6);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a known password on one seeded account so a character can be logged in with
|
||||||
|
/// a real client. The seeder assigns a random GUID, which nobody kept.
|
||||||
|
/// </summary>
|
||||||
|
private static bool SetLoginPassword(Mobile to)
|
||||||
|
{
|
||||||
|
var password = LoginPassword;
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(password))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var acct = Accounts.GetAccount(LoginAccount) as Account;
|
||||||
|
|
||||||
|
if (acct == null)
|
||||||
|
{
|
||||||
|
Report(to, "No account " + LoginAccount + " — password not set.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
acct.SetPassword(password);
|
||||||
|
|
||||||
|
// The seeder backdates some accounts past InactiveDuration to condemn their
|
||||||
|
// houses. This one has to be able to log in, so bring it back to the present.
|
||||||
|
acct.LastLogin = DateTime.UtcNow;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ These two scripts produced the measured budget in [PLAN.md](https://gitea.whitlo
|
|||||||
| `BridgeLinkProbe.cs` | `Scripts/Custom/BridgeLinkProbe.cs` | Triggers `[link` for seed_001 without a client, then saves so the `WebsiteUserId` tag reaches `accounts.xml`. Flag: `LinkProbeOnStart`. Pair with a sidecar that reads the code and sends `link.confirm`. |
|
| `BridgeLinkProbe.cs` | `Scripts/Custom/BridgeLinkProbe.cs` | Triggers `[link` for seed_001 without a client, then saves so the `WebsiteUserId` tag reaches `accounts.xml`. Flag: `LinkProbeOnStart`. Pair with a sidecar that reads the code and sends `link.confirm`. |
|
||||||
| `BridgeCrierProbe.cs` | `Scripts/Custom/BridgeCrierProbe.cs` | Logs the global town-crier entry list every 3s so `towncrier.add` / `remove` can be seen landing in game state. Flag: `CrierProbeOnStart`. |
|
| `BridgeCrierProbe.cs` | `Scripts/Custom/BridgeCrierProbe.cs` | Logs the global town-crier entry list every 3s so `towncrier.add` / `remove` can be seen landing in game state. Flag: `CrierProbeOnStart`. |
|
||||||
| `BridgeVendorSaleProbe.cs` | `Scripts/Custom/BridgeVendorSaleProbe.cs` | Fires `PlayerVendorSale` (Phase 7) with real seeded-vendor data so `vendor.sale` can be verified without a live buy. Requires the Phase 7 patches applied. Flag: `VendorSaleProbeOnStart`. |
|
| `BridgeVendorSaleProbe.cs` | `Scripts/Custom/BridgeVendorSaleProbe.cs` | Fires `PlayerVendorSale` (Phase 7) with real seeded-vendor data so `vendor.sale` can be verified without a live buy. Requires the Phase 7 patches applied. Flag: `VendorSaleProbeOnStart`. |
|
||||||
|
| `BridgeDemoDress.cs` | `Scripts/Custom/BridgeDemoDress.cs` | Renames a seeded world so it is presentable in a screenshot: shop signs, vendor and character names, house signs. Also stages a few condemned houses back into IDOC, and sets a known password on `seed_000` so a character can be logged in. Flags: `DemoDressOnStart`, `DemoDressPassword`. In game: `[demodress`. |
|
||||||
|
|
||||||
## Deploy overwrites Bridge.cfg
|
## Deploy overwrites Bridge.cfg
|
||||||
|
|
||||||
@@ -34,6 +35,41 @@ Because `Config.Get` returns `false` for a missing key, a server whose `Bridge.c
|
|||||||
|
|
||||||
In-game, `[seedworld` and `[unseedworld` (Administrator) do the same work on a live shard.
|
In-game, `[seedworld` and `[unseedworld` (Administrator) do the same work on a live shard.
|
||||||
|
|
||||||
|
## Dressing a seeded world for screenshots
|
||||||
|
|
||||||
|
`BridgeSeeder` builds a world at realistic **scale**, which is all the bridge ever needed. It does not
|
||||||
|
build one that looks like anything: a vendor is `seed vendor` trading as `Seed Shop 810`, a character
|
||||||
|
is `Seed004A`, a house sign says `Seed House 12`. Those strings travel the whole bridge and land on
|
||||||
|
the marketplace, the guild roster and the housing pages of the website — fine for a protocol test,
|
||||||
|
wrong for a screenshot.
|
||||||
|
|
||||||
|
`BridgeDemoDress.cs` renames them in place. It seeds nothing: prices, listing counts, decay stages,
|
||||||
|
fame and skills stay exactly as the seeder left them and as the shard has moved them since, so the
|
||||||
|
data keeps its provenance and only the strings a human reads change. Names are drawn from fixed
|
||||||
|
tables by a hash of each object's serial, so a re-run reproduces the same world, and shop and house
|
||||||
|
names are re-dressed when they are names the pass itself produced — so a change to the tables can be
|
||||||
|
applied to a world that has already been through here.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
DemoDressOnStart=True
|
||||||
|
DemoDressPassword=<a password you choose>
|
||||||
|
```
|
||||||
|
|
||||||
|
Boot once, then set `DemoDressOnStart=False`. The password is written to `seed_000` so a real client
|
||||||
|
can log a character in — the only way to make the website's online roster non-empty — and it is read
|
||||||
|
from the config rather than compiled in, so it never lands in source control.
|
||||||
|
|
||||||
|
**It dresses seeded objects only, which means your own characters keep their names.** That is the
|
||||||
|
right behaviour for a test shard and a thing to remember before pointing a camera at one: a dev
|
||||||
|
world usually also holds the accounts, characters, guilds and houses of whoever built it, and those
|
||||||
|
are real identifiers on a page that may end up public.
|
||||||
|
|
||||||
|
**The sidecar's board is cached, so the website lags a rename.** A shop name reaches the site on the
|
||||||
|
next market sweep, and a sweep advances `MarketSweepBatch` vendors per tick — 27 vendors at the
|
||||||
|
defaults is two ticks. Allow a couple of minutes before concluding that a rename failed. This cost a
|
||||||
|
debugging detour once: the shard had the new names all along and the sidecar was still serving the
|
||||||
|
previous ones.
|
||||||
|
|
||||||
## Back up `Saves/` first
|
## Back up `Saves/` first
|
||||||
|
|
||||||
`[seedworld` and `SeedOnStart` **write to the live world**. Copy `Saves/` somewhere outside the repo before running either. `Backups/Automatic` is rotated by `AutoSave.cs` and `Backups/Temp` is deleted outright, so neither is a safe destination.
|
`[seedworld` and `SeedOnStart` **write to the live world**. Copy `Saves/` somewhere outside the repo before running either. `Backups/Automatic` is rotated by `AutoSave.cs` and `Backups/Temp` is deleted outright, so neither is a safe destination.
|
||||||
|
|||||||
Reference in New Issue
Block a user