test(harness): stage 7 spike, rnt.spike7 (D299) #11
@@ -2246,6 +2246,241 @@ namespace Oxide.Plugins
|
||||
Check("s6.examples.items", itemCount == 6, $"rnpc_juggernaut carries {itemCount} of its 6 items (a short name this Rust lacks would be left out)");
|
||||
}
|
||||
|
||||
// ---- stage 7's spike (D299): the corpse's loot, crates, the locked crate's timer, removing a corpse ----
|
||||
|
||||
private int _corpsePopulated;
|
||||
private NPCPlayerCorpse _lastCorpse;
|
||||
|
||||
/// <summary>Counts the framework hook for an NPC corpse being filled, for our NPC (stage 7 spike).</summary>
|
||||
private void OnCorpsePopulate(BasePlayer npc, NPCPlayerCorpse corpse)
|
||||
{
|
||||
if (npc == null || npc.GetType().Name != "RunicNpcPlayer") return;
|
||||
_corpsePopulated++;
|
||||
_lastCorpse = corpse;
|
||||
}
|
||||
|
||||
private static string Contents(ItemContainer c) =>
|
||||
c == null ? "-" : string.Join(", ", c.itemList.Select(i => $"{i.info.shortname}×{i.amount}").ToArray());
|
||||
|
||||
private static string CorpseContents(LootableCorpse c) =>
|
||||
c == null || c.containers == null ? "-" : string.Join(" | ", c.containers.Select(Contents).ToArray());
|
||||
|
||||
/// <summary><c>rnt.spike7 [part,...]</c>: <c>corpse</c>, <c>crates</c>, <c>locked</c>, <c>removal</c>, or <c>all</c>.</summary>
|
||||
[ConsoleCommand("rnt.spike7")]
|
||||
private void CmdSpike7(ConsoleSystem.Arg arg)
|
||||
{
|
||||
if (arg.Connection != null && !arg.IsAdmin) return;
|
||||
string part = arg.GetString(0, "all");
|
||||
if (_running != null) ServerMgr.Instance.StopCoroutine(_running);
|
||||
_running = ServerMgr.Instance.StartCoroutine(Spike7(part));
|
||||
arg.ReplyWith($"rnt: spike7 {part}");
|
||||
}
|
||||
|
||||
private IEnumerator Spike7(string part)
|
||||
{
|
||||
_pass = _fail = 0;
|
||||
Note($"spike7 {part} on {(IsCarbon() ? "carbon" : "oxide")}, map {World.Size}/{World.Seed}");
|
||||
SetTestProfiles();
|
||||
Vector3 f;
|
||||
if (!FindField(300f, 2027, out f))
|
||||
{
|
||||
Check("s7.setup.field", false, "no open field on the navmesh");
|
||||
yield break;
|
||||
}
|
||||
|
||||
string[] parts = part == "all" ? new[] { "corpse", "crates", "locked", "removal" } : part.Split(',');
|
||||
foreach (string p in parts)
|
||||
{
|
||||
StandIn(f + new Vector3(0, 0, 60), 50);
|
||||
IEnumerator inner = p == "corpse" ? CorpseSpike(f)
|
||||
: p == "crates" ? CratesSpike(f)
|
||||
: p == "locked" ? LockedSpike(f)
|
||||
: p == "removal" ? RemovalSpike(f)
|
||||
: null;
|
||||
if (inner == null) { Check("s7.setup.part", false, "no part " + p); continue; }
|
||||
while (inner.MoveNext()) yield return inner.Current;
|
||||
Api<int>("RunicNpc_DespawnOwner", Owner);
|
||||
ClearWorld();
|
||||
yield return new WaitForSeconds(1f);
|
||||
}
|
||||
|
||||
Note($"done spike7 {part}: {_pass} pass, {_fail} fail");
|
||||
_running = null;
|
||||
}
|
||||
|
||||
/// <summary>Kills one t_roam as a stand-in and returns its corpse (found near where it died).</summary>
|
||||
private IEnumerator KillForCorpse(Vector3 at, Action<BasePlayer> before, Action<NPCPlayerCorpse, double> got)
|
||||
{
|
||||
BasePlayer npc = Spawn(at, "t_roam");
|
||||
yield return new WaitForSeconds(1f);
|
||||
if (npc == null) { got(null, 0); yield break; }
|
||||
before?.Invoke(npc);
|
||||
BasePlayer shooter = StandIn(OnMesh(at + new Vector3(0, 0, 20)), 80);
|
||||
_lastCorpse = null;
|
||||
Vector3 where = npc.transform.position;
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
npc.Hurt(new HitInfo(shooter, npc, Rust.DamageType.Bullet, 5000f));
|
||||
double ms = sw.Elapsed.TotalMilliseconds;
|
||||
yield return new WaitForSeconds(1f);
|
||||
NPCPlayerCorpse corpse = _lastCorpse;
|
||||
if (corpse == null)
|
||||
{
|
||||
var near = new List<NPCPlayerCorpse>();
|
||||
Vis.Entities(where, 5f, near);
|
||||
corpse = near.FirstOrDefault();
|
||||
}
|
||||
|
||||
got(corpse, ms);
|
||||
}
|
||||
|
||||
private IEnumerator CorpseSpike(Vector3 f)
|
||||
{
|
||||
var applyLoot = typeof(HumanNPC).GetMethod("ApplyLoot", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
var createCorpse = typeof(NPCPlayer).GetMethod("CreateCorpse", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
var copyField = typeof(NPCPlayer).GetField("CopyInventoryToCorpse", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
?? typeof(HumanNPC).GetField("CopyInventoryToCorpse", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
var copyProp = copyField == null ? typeof(NPCPlayer).GetProperty("CopyInventoryToCorpse", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) : null;
|
||||
Note($"s7.corpse ApplyLoot: {(applyLoot == null ? "none" : (applyLoot.IsVirtual ? "virtual" : "not virtual") + " " + (applyLoot.IsFamily ? "protected" : applyLoot.IsPublic ? "public" : "private"))}; " +
|
||||
$"CreateCorpse virtual: {createCorpse?.IsVirtual}; CopyInventoryToCorpse: {(copyField != null ? "field" : copyProp != null ? "property, settable " + (copyProp.GetSetMethod(true) != null) + ", virtual get " + copyProp.GetGetMethod(true)?.IsVirtual : "none")}");
|
||||
Check("s7.corpse.applyLootOverridable", applyLoot != null && applyLoot.IsVirtual, "a subclass can fill its own corpse");
|
||||
|
||||
// 1. As today: Rust's scientist loot.
|
||||
NPCPlayerCorpse c1 = null;
|
||||
double ms1 = 0;
|
||||
_corpsePopulated = 0;
|
||||
IEnumerator k = KillForCorpse(OnMesh(f + new Vector3(-40, 0, 0)), null, (c, ms) => { c1 = c; ms1 = ms; });
|
||||
while (k.MoveNext()) yield return k.Current;
|
||||
Check("s7.corpse.hook", _corpsePopulated > 0, $"OnCorpsePopulate raised {_corpsePopulated} time(s) for our NPC");
|
||||
Check("s7.corpse.default", c1 != null, $"Rust's scientist loot: {CorpseContents(c1)}; removal in {(c1 != null ? c1.GetRemovalTime() : -1):0} s; death took {ms1:0.0} ms");
|
||||
|
||||
// 2. The kit: Rust copies the inventory when CopyInventoryToCorpse is true.
|
||||
NPCPlayerCorpse c2 = null;
|
||||
k = KillForCorpse(OnMesh(f + new Vector3(-30, 0, 0)), npc =>
|
||||
{
|
||||
if (copyField != null) copyField.SetValue(npc, true);
|
||||
else if (copyProp != null && copyProp.GetSetMethod(true) != null) copyProp.SetValue(npc, true, null);
|
||||
}, (c, ms) => c2 = c);
|
||||
while (k.MoveNext()) yield return k.Current;
|
||||
bool kit = c2 != null && c2.containers != null && c2.containers.Any(x => x.itemList.Any(i => i.info.shortname == "pistol.revolver"));
|
||||
Check("s7.corpse.kit", kit, $"with CopyInventoryToCorpse on: {CorpseContents(c2)}");
|
||||
|
||||
// 3. Nothing, then a table added: emptied in the hook's place, then filled.
|
||||
NPCPlayerCorpse c3 = null;
|
||||
k = KillForCorpse(OnMesh(f + new Vector3(-20, 0, 0)), null, (c, ms) => c3 = c);
|
||||
while (k.MoveNext()) yield return k.Current;
|
||||
if (c3 != null)
|
||||
{
|
||||
foreach (ItemContainer cont in c3.containers) cont.Clear();
|
||||
ItemManager.DoRemoves();
|
||||
Item scrap = ItemManager.CreateByName("scrap", 150);
|
||||
bool moved = scrap != null && scrap.MoveToContainer(c3.containers[0]);
|
||||
Check("s7.corpse.emptyThenFill", moved && c3.containers[0].itemList.Count == 1, $"emptied, then the table's scrap: {CorpseContents(c3)}");
|
||||
}
|
||||
else Check("s7.corpse.emptyThenFill", false, "no corpse");
|
||||
}
|
||||
|
||||
private static readonly string[][] Crates =
|
||||
{
|
||||
new[] { "wooden box", "assets/bundled/prefabs/radtown/crate_normal_2.prefab" },
|
||||
new[] { "military crate", "assets/bundled/prefabs/radtown/crate_normal.prefab" },
|
||||
new[] { "elite crate", "assets/bundled/prefabs/radtown/crate_elite.prefab" },
|
||||
new[] { "locked crate", "assets/prefabs/deployable/chinooklockedcrate/codelockedhackablecrate.prefab" },
|
||||
};
|
||||
|
||||
private IEnumerator CratesSpike(Vector3 f)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (string[] c in Crates)
|
||||
{
|
||||
Vector3 at = OnMesh(f + new Vector3(-40 + i * 6, 0, 10));
|
||||
i++;
|
||||
BaseEntity made = GameManager.server.CreateEntity(c[1], at);
|
||||
LootContainer box = made as LootContainer;
|
||||
if (box == null)
|
||||
{
|
||||
Check("s7.crate." + c[0].Replace(" ", ""), false, made == null ? "no entity from " + c[1] : made.GetType().Name + " is not a LootContainer");
|
||||
if (made != null) made.Kill();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Before it spawns: no loot of Rust's, never refilled.
|
||||
box.initialLootSpawn = false;
|
||||
box.minSecondsBetweenRefresh = -1f;
|
||||
box.maxSecondsBetweenRefresh = -1f;
|
||||
box.enableSaving = false;
|
||||
box.Spawn();
|
||||
_built.Add(box);
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
int before = box.inventory?.itemList.Count ?? -1;
|
||||
Item scrap = ItemManager.CreateByName("scrap", 200);
|
||||
bool moved = scrap != null && scrap.MoveToContainer(box.inventory);
|
||||
yield return new WaitForSeconds(1f);
|
||||
Check("s7.crate." + c[0].Replace(" ", ""), before == 0 && moved && box.inventory.itemList.Count == 1,
|
||||
$"{box.GetType().Name} '{box.ShortPrefabName}': {before} item(s) of Rust's at spawn, then {Contents(box.inventory)}; capacity {box.inventory?.capacity}, refreshes {box.shouldRefreshContents}, destroyOnEmpty {box.destroyOnEmpty}");
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly FieldInfo HackSecondsField = typeof(HackableLockedCrate).GetField("hackSeconds", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
|
||||
private IEnumerator LockedSpike(Vector3 f)
|
||||
{
|
||||
Note($"s7.locked server-wide: requiredHackSeconds {HackableLockedCrate.requiredHackSeconds}, decaySeconds {HackableLockedCrate.decaySeconds}; hackSeconds field {(HackSecondsField != null ? "found" : "MISSING")}");
|
||||
foreach (float want in new[] { 10f, 20f })
|
||||
{
|
||||
Vector3 at = OnMesh(f + new Vector3(20 + want, 0, 10));
|
||||
var crate = GameManager.server.CreateEntity(Crates[3][1], at) as HackableLockedCrate;
|
||||
if (crate == null || HackSecondsField == null) { Check("s7.locked." + want, false, "no crate or no field"); yield break; }
|
||||
crate.initialLootSpawn = false;
|
||||
crate.enableSaving = false;
|
||||
crate.Spawn();
|
||||
_built.Add(crate);
|
||||
// A per-crate timer: start its count where `want` seconds remain of the server-wide total.
|
||||
HackSecondsField.SetValue(crate, HackableLockedCrate.requiredHackSeconds - want);
|
||||
crate.StartHacking();
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
while (sw.Elapsed.TotalSeconds < want + 15 && !crate.IsFullyHacked())
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
Check("s7.locked." + want, crate.IsFullyHacked() && Mathf.Abs((float)sw.Elapsed.TotalSeconds - want) < 3f,
|
||||
$"asked {want} s: unlocked after {sw.Elapsed.TotalSeconds:0.0} s, lootable {crate.IsFullyHacked()}");
|
||||
}
|
||||
|
||||
// Longer than Rust's own: the count starts below zero. Read where it stands after 3 s.
|
||||
var slow = GameManager.server.CreateEntity(Crates[3][1], OnMesh(f + new Vector3(60, 0, 10))) as HackableLockedCrate;
|
||||
if (slow != null)
|
||||
{
|
||||
slow.initialLootSpawn = false;
|
||||
slow.enableSaving = false;
|
||||
slow.Spawn();
|
||||
_built.Add(slow);
|
||||
HackSecondsField.SetValue(slow, HackableLockedCrate.requiredHackSeconds - 1800f);
|
||||
slow.StartHacking();
|
||||
yield return new WaitForSeconds(3f);
|
||||
float now = (float)HackSecondsField.GetValue(slow);
|
||||
Check("s7.locked.longer", !slow.IsFullyHacked() && now < 0f, $"asked 1800 s: count at {now:0} after 3 s (counts up to {HackableLockedCrate.requiredHackSeconds}); the client is sent this count, so its timer shows from a negative start");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator RemovalSpike(Vector3 f)
|
||||
{
|
||||
NPCPlayerCorpse c = null;
|
||||
IEnumerator k = KillForCorpse(OnMesh(f + new Vector3(-10, 0, 0)), null, (x, ms) => c = x);
|
||||
while (k.MoveNext()) yield return k.Current;
|
||||
if (c == null) { Check("s7.removal.timed", false, "no corpse"); yield break; }
|
||||
float rust = c.GetRemovalTime();
|
||||
c.ResetRemovalTime(5f);
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
while (sw.Elapsed.TotalSeconds < 15 && c != null && !c.IsDestroyed)
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
Check("s7.removal.timed", c == null || c.IsDestroyed, $"Rust's own time {rust:0} s; set to 5 s: gone after {sw.Elapsed.TotalSeconds:0.0} s");
|
||||
|
||||
NPCPlayerCorpse c2 = null;
|
||||
k = KillForCorpse(OnMesh(f + new Vector3(0, 0, 0)), null, (x, ms) => c2 = x);
|
||||
while (k.MoveNext()) yield return k.Current;
|
||||
if (c2 != null) c2.Kill();
|
||||
yield return null;
|
||||
Check("s7.removal.none", c2 == null || c2.IsDestroyed, "a corpse killed at once is gone");
|
||||
}
|
||||
|
||||
// ---- stage 6's spike (D282): press E, the health bar, Kits' data file, a kit swap mid-fight, adds ----
|
||||
|
||||
private const string SwapKit = "rnt6smg";
|
||||
|
||||
Reference in New Issue
Block a user