Merge pull request 'feat(bridge): the faction table and an event's escort, ally and tether (runicnpc stage 5)' (#23) from feat/runicnpc-stage5 into edge
Reviewed-on: #23
This commit is contained in:
@@ -80,7 +80,7 @@ requires_plugins = ["Kits", "ZoneManager"]
|
||||
|
||||
# ── The RunicNPC API the bridge needs ───────────────────────────────────────
|
||||
#
|
||||
# RunicNPC (docs/runicnpc/PLAN.md stage 4): the bridge's npc.* commands call
|
||||
# RunicNPC (docs/runicnpc/PLAN.md stages 4 and 5): the bridge's npc.* commands call
|
||||
# RunicNPC's API and need at least this version of it. It MUST equal
|
||||
# `RunicNpcApiNeeded` in RunicGateway.cs; `node scripts/checkPlugin.js` holds the
|
||||
# two equal. The release copies it into manifest.json, and the installer's
|
||||
@@ -88,4 +88,4 @@ requires_plugins = ["Kits", "ZoneManager"]
|
||||
#
|
||||
# RunicNPC is optional to the bridge until its stage 9: without it every npc.*
|
||||
# command answers `runicnpc-missing`, and events place Rust's own scientists.
|
||||
runicnpc_api = 3
|
||||
runicnpc_api = 4
|
||||
|
||||
@@ -10367,8 +10367,11 @@ namespace Oxide.Plugins
|
||||
// stays true. It is optional until RunicNPC's stage 9; without it every `npc.*` command
|
||||
// answers `runicnpc-missing`, and events keep Rust's own scientists (D243).
|
||||
|
||||
/// <summary>The RunicNPC API these commands call: 3 added placements from a website (D249).</summary>
|
||||
private const int RunicNpcApiNeeded = 3;
|
||||
/// <summary>
|
||||
/// The RunicNPC API these commands call: 3 added placements from a website (D249), 4 the
|
||||
/// faction table and an event's escort, ally and tether (runicnpc stage 5).
|
||||
/// </summary>
|
||||
private const int RunicNpcApiNeeded = 4;
|
||||
|
||||
private Plugin RunicNpcPlugin()
|
||||
{
|
||||
@@ -10462,6 +10465,9 @@ namespace Oxide.Plugins
|
||||
reply["profiles"] = all["profiles"] ?? new Newtonsoft.Json.Linq.JObject();
|
||||
reply["refused"] = all["refused"] ?? new Newtonsoft.Json.Linq.JObject();
|
||||
|
||||
// Runicnpc stage 5 (D254): the faction table, read with the profiles so the site can adopt it.
|
||||
reply["factions"] = all["factions"] ?? new Newtonsoft.Json.Linq.JArray();
|
||||
|
||||
if (reqId != null)
|
||||
reply["reqId"] = reqId;
|
||||
|
||||
@@ -10489,13 +10495,23 @@ namespace Oxide.Plugins
|
||||
return;
|
||||
}
|
||||
|
||||
// Runicnpc stage 5 (D254): the faction table travels with the profiles. It is set first, so the
|
||||
// profiles are checked against the table they will be used with; a refused table is reported
|
||||
// and the old one kept, and the profiles are pushed either way.
|
||||
var factions = cmd.TryGetValue("factions", out value) ? value as Newtonsoft.Json.Linq.JArray : null;
|
||||
string factionsRefused = factions != null ? npc.Call("RunicNpc_SetFactions", factions) as string : null;
|
||||
|
||||
var refused = npc.Call("RunicNpc_SetProfiles", profiles) as Dictionary<string, string>;
|
||||
_lastNpcSummary = $"profiles pushed: {profiles.Count}, refused {(refused != null ? refused.Count : 0)}";
|
||||
_lastNpcSummary = $"profiles pushed: {profiles.Count}, refused {(refused != null ? refused.Count : 0)}" +
|
||||
(factions != null ? $", factions {factions.Count}{(factionsRefused != null ? " REFUSED" : "")}" : "");
|
||||
Puts($"runicnpc: {_lastNpcSummary}");
|
||||
|
||||
Dictionary<string, object> reply = Frame("npc.ok", "reply");
|
||||
reply["refused"] = refused ?? new Dictionary<string, string>();
|
||||
|
||||
if (factionsRefused != null)
|
||||
reply["factionsRefused"] = factionsRefused;
|
||||
|
||||
if (reqId != null)
|
||||
reply["reqId"] = reqId;
|
||||
|
||||
@@ -10892,6 +10908,14 @@ namespace Oxide.Plugins
|
||||
return;
|
||||
}
|
||||
|
||||
NpcOrders orders;
|
||||
|
||||
if (!ReadNpcOrders(cmd, runId, center, out orders, out reason, out message))
|
||||
{
|
||||
EmitWorldError(reqId, reason, message);
|
||||
return;
|
||||
}
|
||||
|
||||
string owner = "run:" + runId;
|
||||
var made = new List<WorldEntry>();
|
||||
|
||||
@@ -10925,6 +10949,19 @@ namespace Oxide.Plugins
|
||||
return;
|
||||
}
|
||||
|
||||
string orderRefusal = ApplyNpcOrders(npc, spawned, orders);
|
||||
|
||||
if (orderRefusal != null)
|
||||
{
|
||||
npc.Call("RunicNpc_Despawn", spawned.net.ID.Value);
|
||||
|
||||
foreach (WorldEntry e in made)
|
||||
npc.Call("RunicNpc_Despawn", ulong.Parse(e.Id, System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
EmitWorldError(reqId, "refused", $"RunicNPC would not give '{profile}' its orders: {orderRefusal}");
|
||||
return;
|
||||
}
|
||||
|
||||
UnityEngine.Vector3 put = spawned.transform.position;
|
||||
made.Add(new WorldEntry
|
||||
{
|
||||
@@ -10955,6 +10992,144 @@ namespace Oxide.Plugins
|
||||
/// <summary>A registry entry's prefab for a RunicNPC profile: <c>runicnpc:<profile></c>.</summary>
|
||||
private const string RunicNpcPrefix = "runicnpc:";
|
||||
|
||||
// ---- an event's NPCs: escort, ally and tether (runicnpc stage 5, D269, D270, D272) ----
|
||||
|
||||
/// <summary>What a placing step tells its NPCs to do besides their profile. Every part is optional.</summary>
|
||||
private sealed class NpcOrders
|
||||
{
|
||||
/// <summary>D269: the player they escort.</summary>
|
||||
public BasePlayer Escort;
|
||||
|
||||
/// <summary>D270: <c>clan</c> (the game's clan id) or <c>player</c> (a Steam id: that player and their team).</summary>
|
||||
public string AllyKind;
|
||||
public string AllyId;
|
||||
|
||||
/// <summary>D272: the zone of this run that holds the step's point.</summary>
|
||||
public string Tether;
|
||||
|
||||
public bool Any => Escort != null || AllyKind != null || Tether != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. Reads <c>escort</c> (a Steam id), <c>ally</c> (<c>{kind: clan|player, id}</c>) and
|
||||
/// <c>tether</c> (true: the run's own zone around the point) from a <c>world.place</c>. False with
|
||||
/// the reason when one cannot be honoured, so the step is refused before anything spawns.
|
||||
/// </summary>
|
||||
private bool ReadNpcOrders(Dictionary<string, object> cmd, string runId, UnityEngine.Vector3 center,
|
||||
out NpcOrders orders, out string reason, out string message)
|
||||
{
|
||||
orders = new NpcOrders();
|
||||
reason = null;
|
||||
message = null;
|
||||
|
||||
string escort = StringField(cmd, "escort");
|
||||
|
||||
if (!string.IsNullOrEmpty(escort))
|
||||
{
|
||||
ulong steamId;
|
||||
|
||||
if (!ulong.TryParse(escort, out steamId))
|
||||
{
|
||||
reason = "malformed";
|
||||
message = $"an escort is a player's Steam id, not '{escort}'";
|
||||
return false;
|
||||
}
|
||||
|
||||
BasePlayer player = BasePlayer.FindByID(steamId);
|
||||
|
||||
if (player == null || !player.IsConnected || player.IsDead())
|
||||
{
|
||||
reason = "escort-offline";
|
||||
message = $"the player {steamId} is not on the server, so there is nobody to escort";
|
||||
return false;
|
||||
}
|
||||
|
||||
orders.Escort = player;
|
||||
}
|
||||
|
||||
object allyValue;
|
||||
var ally = cmd.TryGetValue("ally", out allyValue) ? allyValue as Newtonsoft.Json.Linq.JObject : null;
|
||||
|
||||
if (ally != null)
|
||||
{
|
||||
orders.AllyKind = (string)ally["kind"];
|
||||
orders.AllyId = (string)ally["id"];
|
||||
|
||||
if ((orders.AllyKind != "clan" && orders.AllyKind != "player") || string.IsNullOrEmpty(orders.AllyId))
|
||||
{
|
||||
reason = "malformed";
|
||||
message = "an ally is {kind: clan|player, id}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
object tetherValue;
|
||||
|
||||
if (cmd.TryGetValue("tether", out tetherValue) && tetherValue is bool && (bool)tetherValue)
|
||||
{
|
||||
orders.Tether = RunZoneAt(runId, center);
|
||||
|
||||
if (orders.Tether == null)
|
||||
{
|
||||
reason = "no-zone";
|
||||
message = "no zone this event made holds that point, so there is nothing to keep the NPCs inside";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (orders.Any && RunicNpcApi() < 4)
|
||||
{
|
||||
reason = "runicnpc-old";
|
||||
message = $"escort, ally and tether need RunicNPC's API 4, and it answers {RunicNpcApi()}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Main thread. The zone a run made that holds a point, or null.</summary>
|
||||
private string RunZoneAt(string runId, UnityEngine.Vector3 at)
|
||||
{
|
||||
Plugin zm = plugins.Find("ZoneManager");
|
||||
|
||||
if (zm == null)
|
||||
return null;
|
||||
|
||||
foreach (WorldEntry e in _world.Values)
|
||||
{
|
||||
if (e.Kind != WorldKindZone || e.RunId != runId || ZoneExpired(e))
|
||||
continue;
|
||||
|
||||
object inside = zm.Call("IsPositionInZone", e.Id, at);
|
||||
|
||||
if (inside is bool && (bool)inside)
|
||||
return e.Id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Main thread. Gives one spawned NPC its orders; null, or RunicNPC's reason for refusing one.</summary>
|
||||
private static string ApplyNpcOrders(Plugin npc, BasePlayer spawned, NpcOrders orders)
|
||||
{
|
||||
if (!orders.Any)
|
||||
return null;
|
||||
|
||||
ulong netId = spawned.net.ID.Value;
|
||||
string refusal = null;
|
||||
|
||||
if (orders.Escort != null)
|
||||
refusal = npc.Call("RunicNpc_Escort", netId, (BaseEntity)orders.Escort) as string;
|
||||
|
||||
if (refusal == null && orders.AllyKind != null)
|
||||
refusal = npc.Call("RunicNpc_Ally", netId, orders.AllyKind, orders.AllyId) as string;
|
||||
|
||||
if (refusal == null && orders.Tether != null)
|
||||
refusal = npc.Call("RunicNpc_Tether", netId, orders.Tether) as string;
|
||||
|
||||
return refusal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. Removes a run's RunicNPC NPC through RunicNPC, so it hears of it (and raises
|
||||
/// its own despawn hook); anything else, or RunicNPC gone, is killed as any entity is.
|
||||
|
||||
Reference in New Issue
Block a user