|
|
|
|
@@ -62,7 +62,8 @@ namespace Oxide.Plugins
|
|
|
|
|
/// one-time reward messages <c>perm.sync</c> carries as <c>notices</c> (D333); and
|
|
|
|
|
/// Economics (§20.3, PLAN_REDESIGNS.md §11.2): <c>economics.pay</c>,
|
|
|
|
|
/// <c>economics.balances</c>, <c>economics.adjust</c>, the tally's <c>balance</c> and the
|
|
|
|
|
/// <c>economics.wiped</c> event.
|
|
|
|
|
/// <c>economics.wiped</c> event; and Backpacks (§20.4, PLAN_REDESIGNS.md §11.3):
|
|
|
|
|
/// <c>backpacks.read</c> and hello's <c>integrations.backpacks.sizes</c>.
|
|
|
|
|
/// </para>
|
|
|
|
|
/// </summary>
|
|
|
|
|
private const int ProtocolVersion = 14;
|
|
|
|
|
@@ -1074,6 +1075,12 @@ namespace Oxide.Plugins
|
|
|
|
|
HandleEconomicsAdjust(cmd, reqId);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "backpacks.read":
|
|
|
|
|
// Protocol 14 (PROTOCOL.md §20.4, the module's PLAN_REDESIGNS.md §11.3). One
|
|
|
|
|
// player's backpack, read and never written (R2).
|
|
|
|
|
HandleBackpacksRead(cmd, reqId);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "chat.say":
|
|
|
|
|
HandleChatSay(cmd, reqId);
|
|
|
|
|
break;
|
|
|
|
|
@@ -5156,8 +5163,9 @@ namespace Oxide.Plugins
|
|
|
|
|
|
|
|
|
|
// Hello carries ZoneManager's flag list and both helpers' states, so a change to any
|
|
|
|
|
// of the four is said again rather than waiting for the next connect (§3.1, §3.2).
|
|
|
|
|
// Economics too: hello carries its starting balance, which the richest column needs.
|
|
|
|
|
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin))
|
|
|
|
|
// Economics too: hello carries its starting balance, which the richest column needs;
|
|
|
|
|
// and Backpacks, whose sizes are what a Bigger backpack step offers.
|
|
|
|
|
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin))
|
|
|
|
|
NextTick(ResendHello);
|
|
|
|
|
|
|
|
|
|
// Economics loaded after players connected: what they hold now is not otherwise known
|
|
|
|
|
@@ -5187,7 +5195,7 @@ namespace Oxide.Plugins
|
|
|
|
|
if (plugin != null && plugin.Name == "BetterChat")
|
|
|
|
|
_titlesRegistered = false;
|
|
|
|
|
|
|
|
|
|
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin))
|
|
|
|
|
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin))
|
|
|
|
|
NextTick(ResendHello);
|
|
|
|
|
|
|
|
|
|
ConfigWriteJob job = _configJob;
|
|
|
|
|
@@ -12660,6 +12668,283 @@ namespace Oxide.Plugins
|
|
|
|
|
ForgetOldPayments();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Backpacks (protocol 14, PROTOCOL.md §20.4; the module's PLAN_REDESIGNS.md §11.3) ────
|
|
|
|
|
//
|
|
|
|
|
// WhiteThunder's Backpacks, optional like the other mods (D199). **Read, never write (R2):**
|
|
|
|
|
// `backpacks.read` is the only verb, and nothing here calls a Backpacks method that changes
|
|
|
|
|
// a backpack or a capacity. The extra space an event gives is a `backpacks.size.N`
|
|
|
|
|
// permission the site leases through `perm.sync` like a kit reward (D337), so it needs no
|
|
|
|
|
// code here at all. What the spike found (§11.3.1) shapes the read:
|
|
|
|
|
//
|
|
|
|
|
// * `API_ReadBackpackContentsAsJson` answers Backpacks' own item data — item ids, slots
|
|
|
|
|
// that run across pages, nested contents — and null for a player who never had one;
|
|
|
|
|
// * a capacity read while the backpack is not loaded is cached by Backpacks and pins it
|
|
|
|
|
// until the player reconnects, so the capacity is read only after the contents read
|
|
|
|
|
// has loaded the backpack.
|
|
|
|
|
|
|
|
|
|
private const string BackpacksSizePrefix = "backpacks.size.";
|
|
|
|
|
|
|
|
|
|
/// <summary>Backpacks' own bound on a page, and its default "Max size per page".</summary>
|
|
|
|
|
private const int BackpacksPageMax = 48;
|
|
|
|
|
|
|
|
|
|
private int _backpacksPageSize = BackpacksPageMax;
|
|
|
|
|
private DateTime _backpacksConfigAt;
|
|
|
|
|
|
|
|
|
|
private Plugin BackpacksPlugin()
|
|
|
|
|
{
|
|
|
|
|
return plugins.Find("Backpacks");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsBackpacks(Plugin plugin)
|
|
|
|
|
{
|
|
|
|
|
return plugin != null && plugin != this && plugin.Name == "Backpacks";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Hello's <c>integrations.backpacks</c>: loaded and version like every optional mod, and
|
|
|
|
|
/// <c>sizes</c>, the N of every registered <c>backpacks.size.N</c>, ascending. They are
|
|
|
|
|
/// the operator's own <i>Permission sizes</i>, and what the site may lease (D337).
|
|
|
|
|
/// </summary>
|
|
|
|
|
private Dictionary<string, object> BackpacksBody()
|
|
|
|
|
{
|
|
|
|
|
Plugin backpacks = BackpacksPlugin();
|
|
|
|
|
Dictionary<string, object> body = IntegrationBody(backpacks);
|
|
|
|
|
|
|
|
|
|
if (backpacks != null)
|
|
|
|
|
body["sizes"] = BackpackSizes();
|
|
|
|
|
|
|
|
|
|
return body;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private List<int> BackpackSizes()
|
|
|
|
|
{
|
|
|
|
|
var sizes = new SortedSet<int>();
|
|
|
|
|
|
|
|
|
|
foreach (string name in permission.GetPermissions() ?? new string[0])
|
|
|
|
|
{
|
|
|
|
|
if (name == null || !name.StartsWith(BackpacksSizePrefix, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
int n;
|
|
|
|
|
|
|
|
|
|
// `backpacks.size.profile.6-48` is a dynamic-size profile, not a size.
|
|
|
|
|
if (int.TryParse(name.Substring(BackpacksSizePrefix.Length), System.Globalization.NumberStyles.None,
|
|
|
|
|
System.Globalization.CultureInfo.InvariantCulture, out n) && n > 0)
|
|
|
|
|
sizes.Add(n);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new List<int>(sizes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Backpacks' "Max size per page" from its config, read again only when the file
|
|
|
|
|
/// changes; 48, its default, when the file does not say. The directory comes from the
|
|
|
|
|
/// framework, as Economics' does (R19).
|
|
|
|
|
/// </summary>
|
|
|
|
|
private int BackpacksPageSize()
|
|
|
|
|
{
|
|
|
|
|
string path = Path.Combine(Interface.Oxide.ConfigDirectory, "Backpacks.json");
|
|
|
|
|
DateTime at;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (!File.Exists(path))
|
|
|
|
|
return BackpacksPageMax;
|
|
|
|
|
|
|
|
|
|
at = File.GetLastWriteTimeUtc(path);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
return BackpacksPageMax;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (at == _backpacksConfigAt)
|
|
|
|
|
return _backpacksPageSize;
|
|
|
|
|
|
|
|
|
|
int size = BackpacksPageMax;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var config = Newtonsoft.Json.Linq.JObject.Parse(File.ReadAllText(path));
|
|
|
|
|
var section = config["Backpack size"] as Newtonsoft.Json.Linq.JObject;
|
|
|
|
|
Newtonsoft.Json.Linq.JToken page = section != null ? section["Max size per page"] : null;
|
|
|
|
|
|
|
|
|
|
// Clamped as Backpacks clamps it.
|
|
|
|
|
if (page != null && page.Type == Newtonsoft.Json.Linq.JTokenType.Integer)
|
|
|
|
|
size = Math.Max(1, Math.Min(BackpacksPageMax, (int)page));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Puts($"could not read Backpacks' config: {Flatten(ex.Message)}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_backpacksPageSize = size;
|
|
|
|
|
_backpacksConfigAt = at;
|
|
|
|
|
return size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Main thread. One player's backpack, for a staff view or the player's own page. Live:
|
|
|
|
|
/// nothing is kept here, and the site stores nothing either.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void HandleBackpacksRead(Dictionary<string, object> cmd, string reqId)
|
|
|
|
|
{
|
|
|
|
|
Plugin backpacks = BackpacksPlugin();
|
|
|
|
|
|
|
|
|
|
if (backpacks == null)
|
|
|
|
|
{
|
|
|
|
|
EmitBackpacksError(reqId, "backpacks-missing", "Backpacks is not installed on this server");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string raw = StringField(cmd, "steamId");
|
|
|
|
|
ulong id;
|
|
|
|
|
|
|
|
|
|
if (raw == null || !TryParseSteamId(raw, out id))
|
|
|
|
|
{
|
|
|
|
|
EmitBackpacksError(reqId, "bad-id", $"'{Flatten(raw ?? "")}' is not a Steam id");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Dictionary<string, object> reply = Frame("backpacks.contents", "reply");
|
|
|
|
|
reply["steamId"] = raw;
|
|
|
|
|
|
|
|
|
|
// Loads an offline player's backpack from disk; null when they have never had one.
|
|
|
|
|
string json = backpacks.Call("API_ReadBackpackContentsAsJson", id) as string;
|
|
|
|
|
|
|
|
|
|
if (json == null)
|
|
|
|
|
{
|
|
|
|
|
reply["exists"] = false;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
List<Dictionary<string, object>> items;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
items = BackpackStacks(Newtonsoft.Json.Linq.JArray.Parse(json), true);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
EmitBackpacksError(reqId, "unreadable", $"Backpacks answered contents this bridge could not read: {Flatten(ex.Message)}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only now, through the backpack the read just loaded, so the capacity follows a
|
|
|
|
|
// size granted later (§11.3.1's trap).
|
|
|
|
|
int capacity = CallInt(backpacks, "API_GetBackpackCapacityById", id, raw);
|
|
|
|
|
|
|
|
|
|
// Past the capacity is what waits for the owner's next open to be moved (D338).
|
|
|
|
|
foreach (Dictionary<string, object> stack in items)
|
|
|
|
|
{
|
|
|
|
|
if ((int)stack["slot"] >= capacity)
|
|
|
|
|
stack["overflow"] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply["exists"] = true;
|
|
|
|
|
reply["capacity"] = capacity;
|
|
|
|
|
reply["pageSize"] = BackpacksPageSize();
|
|
|
|
|
reply["items"] = items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (reqId != null)
|
|
|
|
|
reply["reqId"] = reqId;
|
|
|
|
|
|
|
|
|
|
EmitFrame(reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Backpacks' item data reduced to what a grid shows, by slot. <paramref name="nest"/>
|
|
|
|
|
/// keeps one level of <c>contents</c> — a weapon's attachments, a container's items —
|
|
|
|
|
/// and no deeper.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static List<Dictionary<string, object>> BackpackStacks(Newtonsoft.Json.Linq.JArray data, bool nest)
|
|
|
|
|
{
|
|
|
|
|
var stacks = new List<Dictionary<string, object>>();
|
|
|
|
|
|
|
|
|
|
foreach (Newtonsoft.Json.Linq.JToken token in data)
|
|
|
|
|
{
|
|
|
|
|
var item = token as Newtonsoft.Json.Linq.JObject;
|
|
|
|
|
|
|
|
|
|
if (item != null)
|
|
|
|
|
stacks.Add(BackpackStack(item, nest));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stacks.Sort((a, b) => ((int)a["slot"]).CompareTo((int)b["slot"]));
|
|
|
|
|
return stacks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// One stack: slot, shortname, the game's English name (D340, so the site needs no item
|
|
|
|
|
/// catalogue), amount, and skin and name when set. An id no item definition knows is sent
|
|
|
|
|
/// with a null shortname and its <c>itemId</c>. A blueprint names what it is for (D341):
|
|
|
|
|
/// <c>blueprint</c> is the target's shortname and <c>display</c> the target's name.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static Dictionary<string, object> BackpackStack(Newtonsoft.Json.Linq.JObject item, bool nest)
|
|
|
|
|
{
|
|
|
|
|
int itemId = JsonInt(item["ID"]);
|
|
|
|
|
ItemDefinition def = ItemManager.FindItemDefinition(itemId);
|
|
|
|
|
|
|
|
|
|
var stack = new Dictionary<string, object>
|
|
|
|
|
{
|
|
|
|
|
{ "slot", JsonInt(item["Position"]) },
|
|
|
|
|
{ "shortname", def != null ? def.shortname : null },
|
|
|
|
|
{ "amount", JsonInt(item["Amount"]) }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (def == null)
|
|
|
|
|
stack["itemId"] = itemId;
|
|
|
|
|
else
|
|
|
|
|
stack["display"] = def.displayName != null ? def.displayName.english : def.shortname;
|
|
|
|
|
|
|
|
|
|
Newtonsoft.Json.Linq.JToken blueprint = item["IsBlueprint"];
|
|
|
|
|
|
|
|
|
|
if (blueprint != null && blueprint.Type == Newtonsoft.Json.Linq.JTokenType.Boolean && (bool)blueprint)
|
|
|
|
|
{
|
|
|
|
|
ItemDefinition target = ItemManager.FindItemDefinition(JsonInt(item["BlueprintTarget"]));
|
|
|
|
|
|
|
|
|
|
if (target != null)
|
|
|
|
|
{
|
|
|
|
|
stack["blueprint"] = target.shortname;
|
|
|
|
|
stack["display"] = target.displayName != null ? target.displayName.english : target.shortname;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Newtonsoft.Json.Linq.JToken skin = item["Skin"];
|
|
|
|
|
|
|
|
|
|
if (skin != null && skin.Type == Newtonsoft.Json.Linq.JTokenType.Integer && (ulong)skin != 0UL)
|
|
|
|
|
stack["skin"] = (ulong)skin;
|
|
|
|
|
|
|
|
|
|
Newtonsoft.Json.Linq.JToken name = item["Name"];
|
|
|
|
|
|
|
|
|
|
if (name != null && name.Type == Newtonsoft.Json.Linq.JTokenType.String && ((string)name).Length > 0)
|
|
|
|
|
stack["name"] = (string)name;
|
|
|
|
|
|
|
|
|
|
var contents = item["Contents"] as Newtonsoft.Json.Linq.JArray;
|
|
|
|
|
|
|
|
|
|
if (nest && contents != null && contents.Count > 0)
|
|
|
|
|
stack["contents"] = BackpackStacks(contents, false);
|
|
|
|
|
|
|
|
|
|
return stack;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int JsonInt(Newtonsoft.Json.Linq.JToken token)
|
|
|
|
|
{
|
|
|
|
|
return token != null && token.Type == Newtonsoft.Json.Linq.JTokenType.Integer ? (int)token : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EmitBackpacksError(string reqId, string reason, string message)
|
|
|
|
|
{
|
|
|
|
|
Dictionary<string, object> frame = Frame("backpacks.error", "reply");
|
|
|
|
|
frame["reason"] = reason;
|
|
|
|
|
frame["message"] = message;
|
|
|
|
|
|
|
|
|
|
if (reqId != null)
|
|
|
|
|
frame["reqId"] = reqId;
|
|
|
|
|
|
|
|
|
|
EmitFrame(frame);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Chat ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
@@ -13098,7 +13383,8 @@ namespace Oxide.Plugins
|
|
|
|
|
{ "popupNotifications", IntegrationBody(PopupPlugin()) },
|
|
|
|
|
{ "zoneDomes", ZoneDomesBody() },
|
|
|
|
|
{ "runicNpc", RunicNpcBody() },
|
|
|
|
|
{ "economics", EconomicsBody() }
|
|
|
|
|
{ "economics", EconomicsBody() },
|
|
|
|
|
{ "backpacks", BackpacksBody() }
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|