Move test scaffolding out of the deploy path
BridgeSeeder and BridgeProbe are not part of the bridge, but living under overlay/Scripts/Custom/ meant deploy.ps1 would copy them onto any server we deployed to. Move them to tools/scaffolding/, which nothing syncs. overlay/Config/Bridge.cfg now carries the bridge's own settings (host, port, queue cap, sweep intervals) rather than the scaffolding flags. Config.Get returns false for a missing key, so a deployed server never runs the seeder or probe even if their .cs files are present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,405 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
using Server.Accounting;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Multis;
|
||||
|
||||
namespace Server.Custom
|
||||
{
|
||||
/// <summary>
|
||||
/// Measures the main-thread cost of every read the bridge plugin would perform, against
|
||||
/// whatever world is currently loaded. Read-only. Test scaffolding, not part of the bridge.
|
||||
///
|
||||
/// Everything here runs on the Core thread, which is exactly where the real plugin's
|
||||
/// reads must run, so these timings are the ones that matter for frame budget.
|
||||
/// </summary>
|
||||
public static class BridgeProbe
|
||||
{
|
||||
private const int Iterations = 20;
|
||||
|
||||
private static readonly AosAttribute[] AllAttrs =
|
||||
(AosAttribute[])Enum.GetValues(typeof(AosAttribute));
|
||||
|
||||
private static readonly AosWeaponAttribute[] AllWeaponAttrs =
|
||||
(AosWeaponAttribute[])Enum.GetValues(typeof(AosWeaponAttribute));
|
||||
|
||||
private static readonly AosArmorAttribute[] AllArmorAttrs =
|
||||
(AosArmorAttribute[])Enum.GetValues(typeof(AosArmorAttribute));
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
if (Config.Get("Bridge.ProbeOnStart", false))
|
||||
EventSink.ServerStarted += () => Timer.DelayCall(TimeSpan.FromSeconds(2.0), Run);
|
||||
}
|
||||
|
||||
private static void Log(string fmt, params object[] args)
|
||||
{
|
||||
Console.WriteLine("[BridgeProbe] " + String.Format(fmt, args));
|
||||
}
|
||||
|
||||
private static void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
var players = CollectSeededChars();
|
||||
var houses = BaseHouse.AllHouses;
|
||||
var vendors = PlayerVendor.PlayerVendors ?? new List<PlayerVendor>();
|
||||
|
||||
Log("world: {0} seeded chars, {1} houses, {2} vendors, {3} accounts",
|
||||
players.Count, houses.Count, vendors.Count, Accounting.Accounts.Count);
|
||||
Log("thread: {0} (id {1})",
|
||||
System.Threading.Thread.CurrentThread.Name,
|
||||
System.Threading.Thread.CurrentThread.ManagedThreadId);
|
||||
Log("");
|
||||
|
||||
// ---- one full character profile, the heaviest single read ----
|
||||
var sb = new StringBuilder(8192);
|
||||
double perProfile = Time(() =>
|
||||
{
|
||||
for (int i = 0; i < players.Count; i++)
|
||||
{
|
||||
sb.Clear();
|
||||
WriteProfile(sb, players[i]);
|
||||
}
|
||||
}) / Math.Max(1, players.Count);
|
||||
|
||||
sb.Clear();
|
||||
if (players.Count > 0)
|
||||
WriteProfile(sb, players[0]);
|
||||
|
||||
int profileBytes = sb.Length;
|
||||
|
||||
Log("char.profile {0,8:F3} ms/char {1,6} bytes json -> {2:F1} ms for all {3}",
|
||||
perProfile, profileBytes, perProfile * players.Count, players.Count);
|
||||
|
||||
// ---- vitals: the 30s sweep the doc proposes ----
|
||||
double vitals = Time(() =>
|
||||
{
|
||||
var b = new StringBuilder(256);
|
||||
for (int i = 0; i < players.Count; i++)
|
||||
{
|
||||
b.Clear();
|
||||
WriteVitals(b, players[i]);
|
||||
}
|
||||
});
|
||||
|
||||
Log("vitals sweep {0,8:F3} ms for {1} chars ({2:F4} ms/char)",
|
||||
vitals, players.Count, vitals / Math.Max(1, players.Count));
|
||||
|
||||
// ---- house decay sweep (III.3) ----
|
||||
double decay = Time(() =>
|
||||
{
|
||||
for (int i = 0; i < houses.Count; i++)
|
||||
{
|
||||
var lvl = houses[i].DecayLevel;
|
||||
GC.KeepAlive(lvl);
|
||||
}
|
||||
});
|
||||
|
||||
Log("decay sweep {0,8:F3} ms for {1} houses ({2:F4} ms/house)",
|
||||
decay, houses.Count, decay / Math.Max(1, houses.Count));
|
||||
|
||||
// ---- economy: money supply snapshot ----
|
||||
double econ = 0;
|
||||
double total = 0;
|
||||
econ = Time(() =>
|
||||
{
|
||||
total = 0;
|
||||
foreach (Account a in Accounting.Accounts.GetAccounts())
|
||||
total += a.TotalCurrency;
|
||||
});
|
||||
|
||||
Log("economy sweep {0,8:F3} ms for {1} accounts (supply {2:N0} gold)",
|
||||
econ, Accounting.Accounts.Count, total * Account.CurrencyThreshold);
|
||||
|
||||
// ---- player vendor snapshot ----
|
||||
int listings = 0;
|
||||
double vend = Time(() =>
|
||||
{
|
||||
listings = 0;
|
||||
var b = new StringBuilder(4096);
|
||||
for (int i = 0; i < vendors.Count; i++)
|
||||
{
|
||||
b.Clear();
|
||||
listings += WriteVendor(b, vendors[i]);
|
||||
}
|
||||
});
|
||||
|
||||
Log("vendor snap {0,8:F3} ms for {1} vendors ({2} listings)",
|
||||
vend, vendors.Count, listings);
|
||||
|
||||
Log("");
|
||||
Log("--- extrapolation (linear, same gear complexity) ---");
|
||||
Log(" vitals sweep @ 200 online: {0,7:F2} ms", vitals / Math.Max(1, players.Count) * 200);
|
||||
Log(" vitals sweep @ 1000 online: {0,7:F2} ms", vitals / Math.Max(1, players.Count) * 1000);
|
||||
Log(" profiles for 1000 chars : {0,7:F1} ms <-- never do this in a sweep",
|
||||
perProfile * 1000);
|
||||
Log(" decay sweep @ 2000 houses: {0,7:F2} ms", decay / Math.Max(1, houses.Count) * 2000);
|
||||
Log(" economy @ 5000 accts : {0,7:F2} ms", econ / Math.Max(1, Accounting.Accounts.Count) * 5000);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("FAILED: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Best-of-N: the minimum is the least noisy estimate of true cost.</summary>
|
||||
private static double Time(Action action)
|
||||
{
|
||||
action(); // warm up JIT and caches
|
||||
|
||||
double best = double.MaxValue;
|
||||
var sw = new Stopwatch();
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
sw.Restart();
|
||||
action();
|
||||
sw.Stop();
|
||||
|
||||
double ms = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
if (ms < best)
|
||||
best = ms;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static List<PlayerMobile> CollectSeededChars()
|
||||
{
|
||||
var list = new List<PlayerMobile>();
|
||||
|
||||
foreach (Account a in Accounting.Accounts.GetAccounts())
|
||||
{
|
||||
if (!a.Username.StartsWith("seed_", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
var pm = a[i] as PlayerMobile;
|
||||
|
||||
if (pm != null)
|
||||
list.Add(pm);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static void WriteVitals(StringBuilder sb, PlayerMobile m)
|
||||
{
|
||||
sb.Append("{\"kind\":\"char.vitals\",\"serial\":\"0x");
|
||||
sb.Append(m.Serial.Value.ToString("X"));
|
||||
sb.Append("\",\"hits\":").Append(m.Hits);
|
||||
sb.Append(",\"hitsMax\":").Append(m.HitsMax);
|
||||
sb.Append(",\"mana\":").Append(m.Mana);
|
||||
sb.Append(",\"stam\":").Append(m.Stam);
|
||||
sb.Append(",\"str\":").Append(m.Str);
|
||||
sb.Append(",\"dex\":").Append(m.Dex);
|
||||
sb.Append(",\"int\":").Append(m.Int);
|
||||
sb.Append(",\"x\":").Append(m.X);
|
||||
sb.Append(",\"y\":").Append(m.Y);
|
||||
sb.Append(",\"online\":").Append(m.NetState != null ? "true" : "false");
|
||||
sb.Append('}');
|
||||
}
|
||||
|
||||
private static void WriteProfile(StringBuilder sb, PlayerMobile m)
|
||||
{
|
||||
sb.Append("{\"kind\":\"char.profile\",\"serial\":\"0x");
|
||||
sb.Append(m.Serial.Value.ToString("X"));
|
||||
sb.Append("\",\"name\":\"").Append(m.Name).Append('"');
|
||||
sb.Append(",\"body\":").Append(m.Body.BodyID);
|
||||
sb.Append(",\"online\":").Append(m.NetState != null ? "true" : "false");
|
||||
|
||||
sb.Append(",\"stats\":{\"str\":").Append(m.Str);
|
||||
sb.Append(",\"dex\":").Append(m.Dex);
|
||||
sb.Append(",\"int\":").Append(m.Int);
|
||||
sb.Append(",\"hits\":").Append(m.Hits).Append(",\"hitsMax\":").Append(m.HitsMax);
|
||||
sb.Append(",\"mana\":").Append(m.Mana).Append(",\"manaMax\":").Append(m.ManaMax);
|
||||
sb.Append(",\"stam\":").Append(m.Stam).Append(",\"stamMax\":").Append(m.StamMax);
|
||||
sb.Append(",\"fame\":").Append(m.Fame).Append(",\"karma\":").Append(m.Karma);
|
||||
sb.Append(",\"luck\":").Append(m.Luck);
|
||||
sb.Append(",\"resist\":{\"phys\":").Append(m.PhysicalResistance);
|
||||
sb.Append(",\"fire\":").Append(m.FireResistance);
|
||||
sb.Append(",\"cold\":").Append(m.ColdResistance);
|
||||
sb.Append(",\"pois\":").Append(m.PoisonResistance);
|
||||
sb.Append(",\"energy\":").Append(m.EnergyResistance).Append("}}");
|
||||
|
||||
sb.Append(",\"skills\":[");
|
||||
bool first = true;
|
||||
for (int i = 0; i < m.Skills.Length; i++)
|
||||
{
|
||||
var s = m.Skills[i];
|
||||
|
||||
if (s.Base <= 0.0)
|
||||
continue; // untrained: the bridge should not ship ~50 zeroes per char
|
||||
|
||||
if (!first)
|
||||
sb.Append(',');
|
||||
first = false;
|
||||
|
||||
sb.Append("{\"n\":\"").Append(s.SkillName).Append('"');
|
||||
sb.Append(",\"base\":").Append(s.Base.ToString("F1"));
|
||||
sb.Append(",\"value\":").Append(s.Value.ToString("F1"));
|
||||
sb.Append(",\"cap\":").Append(s.Cap.ToString("F1"));
|
||||
sb.Append(",\"lock\":\"").Append(s.Lock).Append("\"}");
|
||||
}
|
||||
sb.Append(']');
|
||||
|
||||
sb.Append(",\"equipment\":[");
|
||||
first = true;
|
||||
foreach (var item in m.Items)
|
||||
{
|
||||
if (item.Layer == Layer.Backpack || item.Layer == Layer.Bank ||
|
||||
item.Layer == Layer.Hair || item.Layer == Layer.FacialHair ||
|
||||
item.Layer == Layer.Mount)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
sb.Append(',');
|
||||
first = false;
|
||||
|
||||
sb.Append("{\"serial\":\"0x").Append(item.Serial.Value.ToString("X"));
|
||||
sb.Append("\",\"layer\":\"").Append(item.Layer).Append('"');
|
||||
sb.Append(",\"itemId\":").Append(item.ItemID);
|
||||
sb.Append(",\"hue\":").Append(item.Hue);
|
||||
sb.Append(",\"cliloc\":").Append(item.LabelNumber);
|
||||
|
||||
if (item.Name != null)
|
||||
sb.Append(",\"name\":\"").Append(item.Name).Append('"');
|
||||
|
||||
sb.Append(",\"mods\":{");
|
||||
bool m1 = true;
|
||||
|
||||
var weapon = item as BaseWeapon;
|
||||
var armor = item as BaseArmor;
|
||||
|
||||
if (weapon != null)
|
||||
{
|
||||
sb.Append("\"minDamage\":").Append(weapon.MinDamage);
|
||||
sb.Append(",\"maxDamage\":").Append(weapon.MaxDamage);
|
||||
m1 = false;
|
||||
|
||||
WriteAttrs(sb, weapon.Attributes, ref m1);
|
||||
WriteWeaponAttrs(sb, weapon.WeaponAttributes, ref m1);
|
||||
}
|
||||
else if (armor != null)
|
||||
{
|
||||
sb.Append("\"baseRating\":").Append(armor.BaseArmorRating);
|
||||
m1 = false;
|
||||
|
||||
WriteAttrs(sb, armor.Attributes, ref m1);
|
||||
WriteArmorAttrs(sb, armor.ArmorAttributes, ref m1);
|
||||
}
|
||||
|
||||
sb.Append("}}");
|
||||
}
|
||||
sb.Append(']');
|
||||
sb.Append('}');
|
||||
}
|
||||
|
||||
private static void WriteAttrs(StringBuilder sb, AosAttributes a, ref bool first)
|
||||
{
|
||||
if (a == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < AllAttrs.Length; i++)
|
||||
{
|
||||
int v = a[AllAttrs[i]];
|
||||
|
||||
if (v == 0)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
sb.Append(',');
|
||||
first = false;
|
||||
|
||||
sb.Append('"').Append(AllAttrs[i]).Append("\":").Append(v);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteWeaponAttrs(StringBuilder sb, AosWeaponAttributes a, ref bool first)
|
||||
{
|
||||
if (a == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < AllWeaponAttrs.Length; i++)
|
||||
{
|
||||
int v = a[AllWeaponAttrs[i]];
|
||||
|
||||
if (v == 0)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
sb.Append(',');
|
||||
first = false;
|
||||
|
||||
sb.Append('"').Append(AllWeaponAttrs[i]).Append("\":").Append(v);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteArmorAttrs(StringBuilder sb, AosArmorAttributes a, ref bool first)
|
||||
{
|
||||
if (a == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < AllArmorAttrs.Length; i++)
|
||||
{
|
||||
int v = a[AllArmorAttrs[i]];
|
||||
|
||||
if (v == 0)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
sb.Append(',');
|
||||
first = false;
|
||||
|
||||
sb.Append('"').Append(AllArmorAttrs[i]).Append("\":").Append(v);
|
||||
}
|
||||
}
|
||||
|
||||
private static int WriteVendor(StringBuilder sb, PlayerVendor v)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
sb.Append("{\"kind\":\"vendor.snapshot\",\"serial\":\"0x");
|
||||
sb.Append(v.Serial.Value.ToString("X"));
|
||||
sb.Append("\",\"holdGold\":").Append(v.HoldGold);
|
||||
sb.Append(",\"listings\":[");
|
||||
|
||||
var pack = v.Backpack;
|
||||
|
||||
if (pack != null)
|
||||
{
|
||||
bool first = true;
|
||||
|
||||
foreach (var item in pack.Items)
|
||||
{
|
||||
var vi = v.GetVendorItem(item);
|
||||
|
||||
if (vi == null)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
sb.Append(',');
|
||||
first = false;
|
||||
count++;
|
||||
|
||||
sb.Append("{\"serial\":\"0x").Append(item.Serial.Value.ToString("X"));
|
||||
sb.Append("\",\"itemId\":").Append(item.ItemID);
|
||||
sb.Append(",\"price\":").Append(vi.Price);
|
||||
sb.Append(",\"forSale\":").Append(vi.IsForSale ? "true" : "false");
|
||||
sb.Append('}');
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("]}");
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
using Server.Accounting;
|
||||
using Server.Commands;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Multis;
|
||||
|
||||
namespace Server.Custom
|
||||
{
|
||||
/// <summary>
|
||||
/// Populates a test shard with synthetic accounts, characters, houses and player
|
||||
/// vendors so the ServUO/sidecar bridge can be exercised at a realistic scale.
|
||||
///
|
||||
/// Test scaffolding. Not part of the bridge. Remove before production use.
|
||||
///
|
||||
/// A house only reaches IDOC when BaseHouse.CanDecay is true, and CanDecay is true
|
||||
/// only for DecayType.Condemned or DecayType.ManualRefresh. An active owner's newest
|
||||
/// house is AutoRefresh, which never decays. So the decaying houses below are given
|
||||
/// to accounts whose LastLogin is backdated past Account.InactiveDuration (180 days),
|
||||
/// which makes them Condemned.
|
||||
///
|
||||
/// Decay stage is then forced with SetDynamicDecay rather than by backdating
|
||||
/// LastRefreshed: this shard is EJ, so Core.ML is true, so DynamicDecay.Enabled is
|
||||
/// true and BaseHouse.GetOldDecayLevel (the percentage-of-DecayPeriod model) is never
|
||||
/// reached.
|
||||
/// </summary>
|
||||
public static class BridgeSeeder
|
||||
{
|
||||
private const string Prefix = "seed_";
|
||||
|
||||
private const int Accounts = 50;
|
||||
private const int CharsPerAccount = 3;
|
||||
|
||||
private const int ActiveHouses = 12; // healthy, owned by active accounts
|
||||
private const int DecayingHouses = 18; // owned by inactive accounts, staged below
|
||||
private const int VendorHouses = 15;
|
||||
private const int VendorsPerHouse = 2;
|
||||
private const int ItemsPerVendor = 40;
|
||||
|
||||
private static readonly Point3D HouseOrigin = new Point3D(1400, 1600, 0);
|
||||
private const int HouseSpacing = 40;
|
||||
private const int HousesPerRow = 6;
|
||||
|
||||
// Spread the decaying houses across stages so a transition sweep sees variety.
|
||||
private static readonly DecayLevel[] DecayStages =
|
||||
{
|
||||
DecayLevel.Slightly, DecayLevel.Somewhat, DecayLevel.Fairly,
|
||||
DecayLevel.Greatly, DecayLevel.IDOC, DecayLevel.IDOC
|
||||
};
|
||||
|
||||
private static readonly Random Rng = new Random(20260710);
|
||||
|
||||
// VendorItem.Price is get-only and PlayerVendor.SetVendorItem is private, so a seeded
|
||||
// vendor would otherwise be stuck at the 999 default that OnSubItemAdded assigns.
|
||||
private static readonly MethodInfo SetVendorItemMethod = typeof(PlayerVendor).GetMethod(
|
||||
"SetVendorItem",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic,
|
||||
null,
|
||||
new[] { typeof(Item), typeof(int), typeof(string) },
|
||||
null);
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
CommandSystem.Register("seedworld", AccessLevel.Administrator, Seed_OnCommand);
|
||||
CommandSystem.Register("unseedworld", AccessLevel.Administrator, Unseed_OnCommand);
|
||||
|
||||
if (Config.Get("Bridge.SeedOnStart", false))
|
||||
EventSink.ServerStarted += () => Run(null, save: true);
|
||||
|
||||
if (Config.Get("Bridge.CensusOnStart", false))
|
||||
EventSink.ServerStarted += Census;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports what the seeded world actually contains after a load, rather than what
|
||||
/// the seeder intended to create. Read-only.
|
||||
/// </summary>
|
||||
private static void Census()
|
||||
{
|
||||
try
|
||||
{
|
||||
var byLevel = new Dictionary<DecayLevel, int>();
|
||||
|
||||
foreach (var house in BaseHouse.AllHouses)
|
||||
{
|
||||
var level = house.DecayLevel;
|
||||
int n;
|
||||
byLevel.TryGetValue(level, out n);
|
||||
byLevel[level] = n + 1;
|
||||
}
|
||||
|
||||
Console.WriteLine("[BridgeSeeder] houses={0}", BaseHouse.AllHouses.Count);
|
||||
|
||||
foreach (var kv in byLevel)
|
||||
Console.WriteLine("[BridgeSeeder] decay {0,-18} {1}", kv.Key, kv.Value);
|
||||
|
||||
int chars = 0, totalEquipped = 0, naked = 0;
|
||||
|
||||
foreach (Account a in Accounting.Accounts.GetAccounts())
|
||||
{
|
||||
if (!a.Username.StartsWith(Prefix, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
var m = a[i];
|
||||
|
||||
if (m == null)
|
||||
continue;
|
||||
|
||||
chars++;
|
||||
|
||||
int worn = 0;
|
||||
|
||||
foreach (var item in m.Items)
|
||||
{
|
||||
if (item.Layer != Layer.Backpack && item.Layer != Layer.Bank &&
|
||||
item.Layer != Layer.Hair && item.Layer != Layer.FacialHair)
|
||||
worn++;
|
||||
}
|
||||
|
||||
totalEquipped += worn;
|
||||
|
||||
if (worn == 0)
|
||||
naked++;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("[BridgeSeeder] seeded chars={0} avgEquipped={1:F2} naked={2}",
|
||||
chars, chars == 0 ? 0.0 : (double)totalEquipped / chars, naked);
|
||||
|
||||
Console.WriteLine("[BridgeSeeder] playervendors={0}",
|
||||
PlayerVendor.PlayerVendors == null ? 0 : PlayerVendor.PlayerVendors.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[BridgeSeeder] census failed: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
[Usage("seedworld")]
|
||||
[Description("Populates the shard with synthetic bridge-test accounts, houses and vendors.")]
|
||||
private static void Seed_OnCommand(CommandEventArgs e)
|
||||
{
|
||||
Run(e.Mobile, save: false);
|
||||
}
|
||||
|
||||
[Usage("unseedworld")]
|
||||
[Description("Deletes everything created by [seedworld.")]
|
||||
private static void Unseed_OnCommand(CommandEventArgs e)
|
||||
{
|
||||
Unseed(e.Mobile);
|
||||
}
|
||||
|
||||
private static void Report(Mobile to, string text)
|
||||
{
|
||||
Console.WriteLine("[BridgeSeeder] " + text);
|
||||
|
||||
if (to != null)
|
||||
to.SendMessage(text);
|
||||
}
|
||||
|
||||
private static void Run(Mobile to, bool save)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Accounting.Accounts.GetAccount(Prefix + "000") != null)
|
||||
{
|
||||
Report(to, "Seed data already present. Run [unseedworld first.");
|
||||
return;
|
||||
}
|
||||
|
||||
var start = DateTime.UtcNow;
|
||||
var seeded = Seed();
|
||||
var elapsed = DateTime.UtcNow - start;
|
||||
|
||||
Report(to, String.Format(
|
||||
"Seeded {0} accounts, {1} chars, {2} houses, {3} vendors in {4:F1}s.",
|
||||
seeded.AccountCount, seeded.CharCount, seeded.HouseCount,
|
||||
seeded.VendorCount, elapsed.TotalSeconds));
|
||||
|
||||
if (save)
|
||||
{
|
||||
Report(to, "Saving world...");
|
||||
World.Save();
|
||||
Report(to, "Save complete.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Report(to, "FAILED: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
private class Counts
|
||||
{
|
||||
public int AccountCount, CharCount, HouseCount, VendorCount;
|
||||
}
|
||||
|
||||
private static Counts Seed()
|
||||
{
|
||||
var counts = new Counts();
|
||||
|
||||
var accounts = new List<Account>();
|
||||
var owners = new List<PlayerMobile>();
|
||||
|
||||
for (int i = 0; i < Accounts; i++)
|
||||
{
|
||||
var acct = new Account(String.Format("{0}{1:000}", Prefix, i), Guid.NewGuid().ToString("N"));
|
||||
acct.DepositGold(Utility.RandomMinMax(5000, 4000000));
|
||||
|
||||
accounts.Add(acct);
|
||||
counts.AccountCount++;
|
||||
|
||||
for (int c = 0; c < CharsPerAccount; c++)
|
||||
{
|
||||
var pm = CreateChar(acct, c);
|
||||
acct[c] = pm;
|
||||
counts.CharCount++;
|
||||
|
||||
if (c == 0)
|
||||
owners.Add(pm);
|
||||
}
|
||||
}
|
||||
|
||||
// Houses. The first ActiveHouses go to accounts left active; the remainder go to
|
||||
// accounts backdated into inactivity so their houses are Condemned and will decay.
|
||||
int placed = 0;
|
||||
|
||||
for (int i = 0; i < ActiveHouses && i < owners.Count; i++, placed++)
|
||||
{
|
||||
PlaceHouse(owners[i], placed);
|
||||
counts.HouseCount++;
|
||||
}
|
||||
|
||||
for (int i = 0; i < DecayingHouses && (ActiveHouses + i) < owners.Count; i++, placed++)
|
||||
{
|
||||
var owner = owners[ActiveHouses + i];
|
||||
var house = PlaceHouse(owner, placed);
|
||||
counts.HouseCount++;
|
||||
|
||||
// Condemn the account: LastLogin older than Account.InactiveDuration (180d).
|
||||
var acct = (Account)owner.Account;
|
||||
acct.LastLogin = DateTime.UtcNow - TimeSpan.FromDays(200 + i);
|
||||
|
||||
// Force the stage directly; DynamicDecay owns the model on this shard.
|
||||
var stage = DecayStages[i % DecayStages.Length];
|
||||
house.SetDynamicDecay(stage);
|
||||
house.NextDecayStage = DateTime.UtcNow + TimeSpan.FromHours(6);
|
||||
}
|
||||
|
||||
// Vendors on the first VendorHouses placed.
|
||||
var allHouses = new List<BaseHouse>();
|
||||
|
||||
foreach (var pm in owners)
|
||||
allHouses.AddRange(BaseHouse.GetHouses(pm));
|
||||
|
||||
for (int h = 0; h < VendorHouses && h < allHouses.Count; h++)
|
||||
{
|
||||
var house = allHouses[h];
|
||||
|
||||
for (int v = 0; v < VendorsPerHouse; v++)
|
||||
{
|
||||
CreateVendor(house);
|
||||
counts.VendorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
private static PlayerMobile CreateChar(Account acct, int slot)
|
||||
{
|
||||
var pm = new PlayerMobile
|
||||
{
|
||||
Player = true,
|
||||
AccessLevel = AccessLevel.Player,
|
||||
Name = String.Format("Seed{0}{1}", acct.Username.Substring(Prefix.Length), (char)('A' + slot)),
|
||||
Female = Utility.RandomBool(),
|
||||
Hue = Utility.RandomSkinHue(),
|
||||
Fame = Utility.RandomMinMax(0, 15000),
|
||||
Karma = Utility.RandomMinMax(-15000, 15000)
|
||||
};
|
||||
|
||||
pm.Body = pm.Female ? 401 : 400;
|
||||
|
||||
// Str must clear the plate requirements in EquipGear (PlateChest needs 95), or
|
||||
// BaseArmor.CanEquip refuses and the gear is left parentless for Cleanup to sweep.
|
||||
pm.RawStr = Utility.RandomMinMax(100, 125);
|
||||
pm.RawDex = Utility.RandomMinMax(25, 125);
|
||||
pm.RawInt = Utility.RandomMinMax(25, 125);
|
||||
|
||||
pm.Hits = pm.HitsMax;
|
||||
pm.Mana = pm.ManaMax;
|
||||
pm.Stam = pm.StamMax;
|
||||
|
||||
// Full skill sheet: the bridge's profile export reads every skill.
|
||||
for (int i = 0; i < pm.Skills.Length; i++)
|
||||
{
|
||||
pm.Skills[i].Base = Rng.Next(100) < 20 ? Utility.RandomMinMax(60, 120) : 0.0;
|
||||
pm.Skills[i].Cap = 120.0;
|
||||
}
|
||||
|
||||
var pack = new Backpack { Movable = false };
|
||||
pm.AddItem(pack);
|
||||
|
||||
EquipGear(pm);
|
||||
|
||||
pm.MoveToWorld(
|
||||
new Point3D(HouseOrigin.X + Utility.RandomMinMax(-50, 50),
|
||||
HouseOrigin.Y + Utility.RandomMinMax(-50, 50), 0),
|
||||
Map.Felucca);
|
||||
|
||||
return pm;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gear carries AOS attribute bags. The profile exporter has to walk these, so a
|
||||
/// seeded character must have them populated or the cost measurement is meaningless.
|
||||
/// </summary>
|
||||
private static void EquipGear(PlayerMobile pm)
|
||||
{
|
||||
Equip(pm, MakeWeapon());
|
||||
|
||||
Equip(pm, Decorate(new PlateChest()));
|
||||
Equip(pm, Decorate(new PlateLegs()));
|
||||
Equip(pm, Decorate(new PlateHelm()));
|
||||
Equip(pm, Decorate(new PlateArms()));
|
||||
Equip(pm, Decorate(new PlateGloves()));
|
||||
Equip(pm, new Boots(Utility.RandomNeutralHue()));
|
||||
Equip(pm, new Cloak(Utility.RandomNeutralHue()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A rejected EquipItem leaves the item in World.Items with no parent, which the
|
||||
/// Cleanup pass later deletes en masse. Drop it immediately instead.
|
||||
/// </summary>
|
||||
private static void Equip(PlayerMobile pm, Item item)
|
||||
{
|
||||
if (!pm.EquipItem(item))
|
||||
{
|
||||
Console.WriteLine("[BridgeSeeder] equip rejected: {0} on {1}", item.GetType().Name, pm.Name);
|
||||
item.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static BaseWeapon MakeWeapon()
|
||||
{
|
||||
BaseWeapon w;
|
||||
|
||||
switch (Utility.Random(3))
|
||||
{
|
||||
case 0: w = new Longsword(); break;
|
||||
case 1: w = new Katana(); break;
|
||||
default: w = new Broadsword(); break;
|
||||
}
|
||||
|
||||
w.Hue = Utility.RandomNeutralHue();
|
||||
w.Attributes.WeaponDamage = Utility.RandomMinMax(10, 50);
|
||||
w.Attributes.AttackChance = Utility.RandomMinMax(5, 15);
|
||||
w.Attributes.DefendChance = Utility.RandomMinMax(5, 15);
|
||||
w.Attributes.BonusHits = Utility.RandomMinMax(1, 8);
|
||||
w.WeaponAttributes.HitLightning = Utility.RandomMinMax(10, 50);
|
||||
w.WeaponAttributes.HitLeechHits = Utility.RandomMinMax(10, 40);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
private static BaseArmor Decorate(BaseArmor a)
|
||||
{
|
||||
a.Hue = Utility.RandomNeutralHue();
|
||||
a.Attributes.BonusHits = Utility.RandomMinMax(1, 6);
|
||||
a.Attributes.LowerManaCost = Utility.RandomMinMax(1, 8);
|
||||
a.ArmorAttributes.SelfRepair = Utility.RandomMinMax(1, 5);
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
private static BaseHouse PlaceHouse(PlayerMobile owner, int index)
|
||||
{
|
||||
int x = HouseOrigin.X + (index % HousesPerRow) * HouseSpacing;
|
||||
int y = HouseOrigin.Y + (index / HousesPerRow) * HouseSpacing;
|
||||
|
||||
// Placement validation (HousePlacement.Check) is deliberately bypassed: the bridge
|
||||
// only reads serial/owner/coords/decay off these, never their terrain validity.
|
||||
var house = new SmallOldHouse(owner, 0x64);
|
||||
house.MoveToWorld(new Point3D(x, y, 0), Map.Felucca);
|
||||
|
||||
house.BuiltOn = DateTime.UtcNow - TimeSpan.FromDays(Utility.RandomMinMax(10, 400));
|
||||
house.LastRefreshed = house.BuiltOn;
|
||||
|
||||
if (house.Sign != null)
|
||||
house.Sign.Name = String.Format("Seed House {0}", index);
|
||||
|
||||
return house;
|
||||
}
|
||||
|
||||
private static void CreateVendor(BaseHouse house)
|
||||
{
|
||||
var owner = house.Owner;
|
||||
|
||||
var vendor = new PlayerVendor(owner, house)
|
||||
{
|
||||
Name = "seed vendor",
|
||||
ShopName = String.Format("Seed Shop {0}", Utility.Random(1000))
|
||||
};
|
||||
|
||||
vendor.MoveToWorld(
|
||||
new Point3D(house.X + Utility.RandomMinMax(-2, 2),
|
||||
house.Y + Utility.RandomMinMax(-2, 2), house.Z),
|
||||
house.Map);
|
||||
|
||||
vendor.HoldGold = Utility.RandomMinMax(1000, 250000);
|
||||
|
||||
for (int i = 0; i < ItemsPerVendor; i++)
|
||||
{
|
||||
Item item = MakeWeapon();
|
||||
|
||||
// Dropping into the pack fires OnSubItemAdded, which registers a VendorItem
|
||||
// at the default price of 999; then correct the price.
|
||||
vendor.Backpack.DropItem(item);
|
||||
|
||||
if (SetVendorItemMethod != null)
|
||||
SetVendorItemMethod.Invoke(vendor, new object[] { item, Utility.RandomMinMax(50, 75000), "" });
|
||||
}
|
||||
}
|
||||
|
||||
private static void Unseed(Mobile to)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doomed = new List<Account>();
|
||||
|
||||
foreach (Account a in Accounting.Accounts.GetAccounts())
|
||||
{
|
||||
if (a.Username.StartsWith(Prefix, StringComparison.Ordinal))
|
||||
doomed.Add(a);
|
||||
}
|
||||
|
||||
// Account.Delete also deletes the account's characters and their houses.
|
||||
foreach (var a in doomed)
|
||||
a.Delete();
|
||||
|
||||
Report(to, String.Format("Removed {0} seed accounts (chars and houses included).", doomed.Count));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Report(to, "FAILED: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user