feat: Refactor admin UI pages to support embedded mode and improve navigation

- Updated PlayerLogs, Players, Reports, Rooms, Settings, SignupCodes, and other pages to accept an optional `embedded` prop for better integration in various contexts.
- Introduced a new Activity page consolidating audit logs and player request logs with tab navigation.
- Added Content page for community board and loading tips with tab navigation.
- Created Moderation page to unify player directory, bans, and reports under a single interface.
- Implemented SettingsHome page to manage server settings and signup codes with tab navigation.
- Enhanced the Join page to include password creation for new accounts.
- Added global friends feature in Settings to allow all accounts to be friends without database entries.
- Introduced RelationshipQueries service to manage effective friend resolution based on global friends toggle.
- Added WireDates for consistent date handling in serialized data.
- Created reusable Tabs component for consistent tab navigation across various pages.
This commit is contained in:
Alexa
2026-06-06 16:39:19 +03:00
parent 34506e8294
commit 955246c6e4
41 changed files with 4129 additions and 2057 deletions
+219 -1
View File
@@ -70,6 +70,13 @@ public class Mod : MelonMod
public static bool EnableTlsTrustBypass = true; public static bool EnableTlsTrustBypass = true;
// Photon Custom Auth injector was parked 2026-05-28; see // Photon Custom Auth injector was parked 2026-05-28; see
// attic/AuthValuesInjector.cs.attic for the code + restore notes. // attic/AuthValuesInjector.cs.attic for the code + restore notes.
// Force-enable the client's built-in RecRoom.Debugging.DebugConsole
// (normally dev-gated) and silence CheatManager's tamper detectors
// so movement/time commands (SetTimeScale, Fly, Teleport, …) don't
// drop you to the dorm. Default OFF — purely opt-in. The toggle key
// is a UnityEngine.KeyCode name; BackQuote is the `~` key.
public static bool EnableDebugConsole = false;
public static string DebugConsoleToggleKey = "BackQuote";
} }
public override void OnInitializeMelon() public override void OnInitializeMelon()
@@ -183,6 +190,17 @@ public class Mod : MelonMod
"RpcChatEmote", "RpcChatEmote",
args: new[] { typeof(string) }, args: new[] { typeof(string) },
prefix: nameof(ChatPatches.RpcChatEmote_Prefix)); prefix: nameof(ChatPatches.RpcChatEmote_Prefix));
// Debug console: skip CheatManager's tamper detectors so the
// console's movement/time commands don't trip a dorm-drop. The
// console UI toggle itself is driven from OnUpdate (hotkey) and
// resolved lazily — only the anti-cheat skip needs a patch.
if (Cfg.EnableDebugConsole)
{
foreach (var m in new[] { "OnTimeCheatDetected", "OnObscuredTypeCheatDetected", "OnHeightCheatDetected", "OnAdvancedMovementCheatDetected" })
TryPatchByName("CheatManager", m, prefix: nameof(DebugConsolePatches.SuppressCheatDetected_Prefix));
Log.Msg($"[debugconsole] enabled — CheatManager detectors silenced; press '{Cfg.DebugConsoleToggleKey}' to toggle the console.");
}
RegisterDiagnostics(); RegisterDiagnostics();
Log.Msg("=== Client patches registered ==="); Log.Msg("=== Client patches registered ===");
} }
@@ -252,6 +270,8 @@ public class Mod : MelonMod
if (!PhotonPatches.OverrideApplied && (_photonOverridePollFrame++ % 6) == 0) if (!PhotonPatches.OverrideApplied && (_photonOverridePollFrame++ % 6) == 0)
PhotonPatches.TryApplyOverride(reason: "update-poll"); PhotonPatches.TryApplyOverride(reason: "update-poll");
if (Cfg.EnableDebugConsole) DebugConsolePatches.PollToggleKey();
if (_diagnosticGameComplete) return; if (_diagnosticGameComplete) return;
if (_diagnosticRetryFrame++ > 3600) return; if (_diagnosticRetryFrame++ > 3600) return;
if ((_diagnosticRetryFrame % 60) != 0) return; if ((_diagnosticRetryFrame % 60) != 0) return;
@@ -299,6 +319,8 @@ public class Mod : MelonMod
if (r.TryGetProperty("PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId; if (r.TryGetProperty("PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
if (r.TryGetProperty("PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion; if (r.TryGetProperty("PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
if (r.TryGetProperty("EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean(); if (r.TryGetProperty("EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
if (r.TryGetProperty("EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
if (r.TryGetProperty("DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
// "InjectAuthValues" key in the template is now ignored — // "InjectAuthValues" key in the template is now ignored —
// see attic/AuthValuesInjector.cs.attic. // see attic/AuthValuesInjector.cs.attic.
Log.Msg($"[config] loaded: ServerHost={Cfg.ServerHost}, SingleOrigin={(string.IsNullOrEmpty(Cfg.SingleOriginBaseUrl) ? "<off>" : Cfg.SingleOriginBaseUrl)}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "<unset>" : "<set>")}, " + Log.Msg($"[config] loaded: ServerHost={Cfg.ServerHost}, SingleOrigin={(string.IsNullOrEmpty(Cfg.SingleOriginBaseUrl) ? "<off>" : Cfg.SingleOriginBaseUrl)}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "<unset>" : "<set>")}, " +
@@ -443,7 +465,7 @@ public class Mod : MelonMod
{ {
// Look in all three patch holder classes — small enough that a // Look in all three patch holder classes — small enough that a
// linear scan is cheaper than per-class lookups. // linear scan is cheaper than per-class lookups.
foreach (var holder in new[] { typeof(UriPatches), typeof(PhotonPatches), typeof(TlsPatches), typeof(DiagnosticPatches), typeof(SavePatches), typeof(ChatPatches) }) foreach (var holder in new[] { typeof(UriPatches), typeof(PhotonPatches), typeof(TlsPatches), typeof(DiagnosticPatches), typeof(SavePatches), typeof(ChatPatches), typeof(DebugConsolePatches) })
{ {
var m = holder.GetMethod(name, BindingFlags.Public | BindingFlags.Static); var m = holder.GetMethod(name, BindingFlags.Public | BindingFlags.Static);
if (m is not null) return m; if (m is not null) return m;
@@ -525,6 +547,202 @@ public class Mod : MelonMod
// Harmony invokes. Kept separate from Mod for readability and so the // Harmony invokes. Kept separate from Mod for readability and so the
// reflection lookup in GetPatchMethod stays simple. // reflection lookup in GetPatchMethod stays simple.
// ── Debug console access ───────────────────────────────────────────────
// The 2020 client ships RecRoom.Debugging.DebugConsole — a real in-game
// dev console (text input + a DebugConsoleCommandConfig command table:
// SetTimeScale, Fly, Teleport, GoToRoom, KillAllEnemies, DebugCompleteDaily,
// …). Retail gates it behind a developer flag. This holder force-toggles
// its UI on a hotkey and (via the OnLateInitializeMelon patch above) skips
// CheatManager's tamper detectors so movement/time commands don't drop you
// to the dorm.
//
// Everything is resolved by reflection at runtime so the SAME code works on
// both client builds despite their differing obfuscation:
// * The show/hide toggle is the one non-compiler-generated private
// instance void(bool) on DebugConsole — ShowInputField on 2020.03,
// MCNBJFECHLJ on 2020.12. We try the readable name, then the obfuscated
// one, then fall back to that structural match.
// * The live component is found via UnityEngine.Object.FindObjectOfType
// (name-stable) rather than the SingletonMonoBehaviour<T> accessor.
// * Execute(string) keeps its readable name on both builds (kept here as
// a scripted-command entry point even though the hotkey drives the UI).
internal static class DebugConsolePatches
{
private static bool _consoleVisible;
private static bool _pollErrorLogged;
private static int _probeFrame;
private static bool _probeDone;
private static bool _inputResolved;
private static MethodInfo? _getKeyDown; // UnityEngine.Input.GetKeyDown(KeyCode)
private static object? _toggleKeyValue; // boxed KeyCode enum value
private static bool _consoleResolved;
private static Type? _consoleType;
private static MethodInfo? _toggleMethod; // ShowInputField(bool) / MCNBJFECHLJ(bool)
private static MethodInfo? _findObjectOfType; // UnityEngine.Object.FindObjectOfType(Type)
// Harmony prefix returning false → skips the original CheatManager
// detector, so a tripped heuristic never runs its punish/drop path.
public static bool SuppressCheatDetected_Prefix() => false;
// Polled every frame from Mod.OnUpdate while EnableDebugConsole is set.
public static void PollToggleKey()
{
try
{
if (!_inputResolved) ResolveInput();
// One-shot probe ~3s in (≈180 frames @60fps): log whether the
// console object actually exists, so we learn that even if the
// hotkey never fires.
if (!_probeDone && _probeFrame++ > 180)
{
_probeDone = true;
ProbeConsole();
}
if (_getKeyDown is null || _toggleKeyValue is null) return;
if (_getKeyDown.Invoke(null, new[] { _toggleKeyValue }) is true)
{
Mod.Log.Msg($"[debugconsole] toggle key '{Mod.Cfg.DebugConsoleToggleKey}' detected");
Toggle();
}
}
catch (Exception ex)
{
if (!_pollErrorLogged) { _pollErrorLogged = true; Mod.Log.Warning($"[debugconsole] poll failed (logged once): {ex}"); }
}
}
// Diagnostic: reports whether a live DebugConsole component exists and
// whether the show/hide method resolved — without needing a keypress.
private static void ProbeConsole()
{
if (!_consoleResolved) ResolveConsole();
if (_consoleType is null) { Mod.Log.Warning("[debugconsole] probe: DebugConsole type NOT resolved"); return; }
try
{
var active = _findObjectOfType?.Invoke(null, new object[] { _consoleType });
Mod.Log.Msg($"[debugconsole] probe: type={_consoleType.FullName}, toggleMethod={(_toggleMethod?.Name ?? "<none>")}, activeInstance={(active is null ? "null" : "found")}");
}
catch (Exception ex) { Mod.Log.Warning($"[debugconsole] probe failed: {ex.Message}"); }
}
// Runs DebugConsole.Execute(command) directly — bypasses the UI/gate
// entirely. Handy for scripted one-shots; the hotkey path uses the UI.
public static void ExecuteCommand(string command)
{
if (!_consoleResolved) ResolveConsole();
if (_consoleType is null) return;
try
{
var execute = AccessTools.Method(_consoleType, "Execute", new[] { typeof(string) });
if (execute is null) { Mod.Log.Warning("[debugconsole] Execute(string) not found"); return; }
execute.Invoke(null, new object[] { command });
Mod.Log.Msg($"[debugconsole] executed: {command}");
}
catch (Exception ex) { Mod.Log.Warning($"[debugconsole] execute '{command}' failed: {ex.Message}"); }
}
private static void ResolveInput()
{
_inputResolved = true;
var keyCodeType = Mod.ResolveType("UnityEngine.KeyCode");
var inputType = Mod.ResolveType("UnityEngine.Input");
if (keyCodeType is null || inputType is null)
{
Mod.Log.Warning("[debugconsole] UnityEngine.Input/KeyCode not found — hotkey disabled");
return;
}
try { _toggleKeyValue = Enum.Parse(keyCodeType, Mod.Cfg.DebugConsoleToggleKey, ignoreCase: true); }
catch
{
Mod.Log.Warning($"[debugconsole] '{Mod.Cfg.DebugConsoleToggleKey}' is not a KeyCode; defaulting to BackQuote");
_toggleKeyValue = Enum.Parse(keyCodeType, "BackQuote");
}
_getKeyDown = AccessTools.Method(inputType, "GetKeyDown", new[] { keyCodeType });
if (_getKeyDown is null) Mod.Log.Warning("[debugconsole] Input.GetKeyDown(KeyCode) not found");
else Mod.Log.Msg($"[debugconsole] hotkey armed: '{Mod.Cfg.DebugConsoleToggleKey}' (poll live)");
}
private static void ResolveConsole()
{
_consoleResolved = true;
_consoleType = Mod.ResolveType("RecRoom.Debugging.DebugConsole");
if (_consoleType is null) { Mod.Log.Warning("[debugconsole] DebugConsole type not found"); return; }
// Prefer the readable 2020.03 name, then the 2020.12 obfuscated
// name, then a structural fallback that fits both builds.
_toggleMethod = AccessTools.Method(_consoleType, "ShowInputField", new[] { typeof(bool) })
?? AccessTools.Method(_consoleType, "MCNBJFECHLJ", new[] { typeof(bool) })
?? FindToggleByShape(_consoleType);
if (_toggleMethod is null) Mod.Log.Warning("[debugconsole] show/hide method not found");
var objType = Mod.ResolveType("UnityEngine.Object");
_findObjectOfType = objType is null ? null
: AccessTools.Method(objType, "FindObjectOfType", new[] { typeof(Type) });
if (_findObjectOfType is null) Mod.Log.Warning("[debugconsole] Object.FindObjectOfType(Type) not found");
}
// The console's show/hide is the single private, instance, void method
// taking exactly one bool, excluding compiler-generated members (the
// property backing setter on 2020.12 is also void(bool) but is marked
// [CompilerGenerated]).
private static MethodInfo? FindToggleByShape(Type t)
{
MethodInfo? found = null;
foreach (var m in t.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
{
if (m.ReturnType != typeof(void)) continue;
var ps = m.GetParameters();
if (ps.Length != 1 || ps[0].ParameterType != typeof(bool)) continue;
if (m.Name.StartsWith("set_", StringComparison.Ordinal)) continue;
if (m.IsDefined(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), false)) continue;
if (found is not null) return null; // ambiguous — don't guess
found = m;
}
return found;
}
public static void Toggle()
{
if (!_consoleResolved) ResolveConsole();
if (_consoleType is null || _toggleMethod is null || _findObjectOfType is null) return;
try
{
var instance = _findObjectOfType.Invoke(null, new object[] { _consoleType });
if (instance is null)
{
Mod.Log.Warning("[debugconsole] no live DebugConsole in scene — it may not be instantiated on this build");
return;
}
// Best-effort: flip the static "show button" enable so the
// game's own Update keeps the console available instead of
// re-hiding it. Failures are ignored (obfuscated builds).
EnableShowButton();
_consoleVisible = !_consoleVisible;
_toggleMethod.Invoke(instance, new object[] { _consoleVisible });
Mod.Log.Msg($"[debugconsole] {(_consoleVisible ? "opened" : "closed")}");
}
catch (Exception ex) { Mod.Log.Warning($"[debugconsole] toggle failed: {ex.Message}"); }
}
private static void EnableShowButton()
{
if (_consoleType is null) return;
try
{
var prop = _consoleType.GetProperty("ShowButton", BindingFlags.Public | BindingFlags.Static);
if (prop is not null && prop.CanWrite) { prop.SetValue(null, true); return; }
foreach (var name in new[] { "showButton", "IEGCNLHPKFH" })
{
var f = _consoleType.GetField(name, BindingFlags.NonPublic | BindingFlags.Static);
if (f is not null && f.FieldType == typeof(bool)) { f.SetValue(null, true); return; }
}
}
catch { /* best-effort — the toggle still works without it */ }
}
}
internal static class UriPatches internal static class UriPatches
{ {
public static void HttpManagerSendRequestString_Prefix(ref string url) public static void HttpManagerSendRequestString_Prefix(ref string url)
@@ -284,7 +284,10 @@ public class ImagesController(
[HttpGet("api/images/v1/slideshow")] [HttpGet("api/images/v1/slideshow")]
public IActionResult SlideshowInfo() => Ok(new public IActionResult SlideshowInfo() => Ok(new
{ {
ValidTill = new DateTime(9999, 12, 31, 0, 0, 0, DateTimeKind.Utc), // WireDates.FarFuture (2099), NOT year 9999 — a 9999 ValidTill
// overflows the watch's local-timezone date parse in some DST
// timezones → "malformed RecNet response". See WireDates.
ValidTill = WireDates.FarFuture,
Images = Array.Empty<object>(), Images = Array.Empty<object>(),
}); });
@@ -23,6 +23,7 @@ public class MessagesController(
NotificationService notifications, NotificationService notifications,
PrivateInstanceService privateInstances, PrivateInstanceService privateInstances,
OnlinePresenceService onlinePresence, OnlinePresenceService onlinePresence,
ServerSettingsService serverSettings,
ILogger<MessagesController> logger) : ControllerBase ILogger<MessagesController> logger) : ControllerBase
{ {
private long Me => this.RequireCurrentPlayerId(); private long Me => this.RequireCurrentPlayerId();
@@ -471,11 +472,8 @@ public class MessagesController(
[HttpGet("/api/messages/v1/favoriteFriendOnlineStatus")] [HttpGet("/api/messages/v1/favoriteFriendOnlineStatus")]
public async Task<ActionResult> FavoriteFriendOnlineStatus() public async Task<ActionResult> FavoriteFriendOnlineStatus()
{ {
var friendIds = await db.Relationships // Honors the global-friends toggle (everyone is a friend when on).
.Where(r => r.Status == RelationshipStatus.Friend && var friendIds = await RelationshipQueries.EffectiveFriendIdsAsync(db, serverSettings, Me);
(r.RequesterId == Me || r.TargetId == Me))
.Select(r => r.RequesterId == Me ? r.TargetId : r.RequesterId)
.ToListAsync();
var online = onlinePresence.OnlinePlayerIds().ToHashSet(); var online = onlinePresence.OnlinePlayerIds().ToHashSet();
return Ok(friendIds.Select(id => new return Ok(friendIds.Select(id => new
{ {
@@ -48,7 +48,7 @@ namespace DorkNet.Server.Controllers.API.Relationships.V2;
/// </summary> /// </summary>
[ApiController] [ApiController]
[Authorize] [Authorize]
public class RelationshipsController(DorkNetDbContext db, NotificationService notifications) : ControllerBase public class RelationshipsController(DorkNetDbContext db, NotificationService notifications, ServerSettingsService settings) : ControllerBase
{ {
// RelationshipType — int values match Relationship.RelationshipType // RelationshipType — int values match Relationship.RelationshipType
// in the patched dump.cs. // in the patched dump.cs.
@@ -72,13 +72,84 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
public async Task<ActionResult> GetMyRelationships() public async Task<ActionResult> GetMyRelationships()
{ {
var me = CurrentPlayerId; var me = CurrentPlayerId;
// Global-friends mode (admin toggle): synthesize a Friend row for
// every other account WITHOUT persisting anything. Real rows are
// overlaid so mute/favorite still apply and Blocked stays non-friend.
if (await settings.IsGlobalFriendsEnabledAsync())
return Ok(await BuildGlobalFriendListAsync(me));
var rels = await db.Relationships var rels = await db.Relationships
.Where(r => r.RequesterId == me || r.TargetId == me) .Where(r => r.RequesterId == me || r.TargetId == me)
.ToListAsync(); .ToListAsync();
var wire = rels.Select(r => BuildRelationship(r, me)).ToList(); // Merge the (up to two) rows per pair so a favorite/mute/ignore the
// caller set on a friendship the OTHER player initiated isn't dropped
// by the watch (which keys its list by PlayerID — duplicate ids lose).
var wire = rels
.GroupBy(r => r.RequesterId == me ? r.TargetId : r.RequesterId)
.Select(g => BuildMerged(me, g.Key, g.ToList()))
.ToList();
return Ok(wire); return Ok(wire);
} }
/// <summary>Synthesize the caller's friend list as "everyone" for the
/// global-friends toggle. One Friend entry per other account; real rows
/// are overlaid so a Block stays None and mute/favorite carry through.</summary>
private async Task<List<Dictionary<string, object>>> BuildGlobalFriendListAsync(long me)
{
var realRows = await db.Relationships
.Where(r => r.RequesterId == me || r.TargetId == me)
.ToListAsync();
// Group both directional rows per pair so the caller's favorite/mute/
// ignore (stored on the row where they're the requester) survives even
// when the friendship row itself was created by the other player.
var realByOther = realRows
.GroupBy(r => r.RequesterId == me ? r.TargetId : r.RequesterId)
.ToDictionary(g => g.Key, g => g.ToList());
var others = await db.Players
.Where(p => p.Id != me && p.Id != RelationshipQueries.SystemAccountId)
.Select(p => new { p.Id, p.Username })
.ToListAsync();
var wire = new List<Dictionary<string, object>>(others.Count);
foreach (var other in others)
{
var otherId = other.Id;
if (realByOther.TryGetValue(otherId, out var pair))
{
// A real block wins — that pair is genuinely not friends.
if (pair.Any(r => r.Status == EntityStatus.Blocked)) { wire.Add(EmptyRelationship(otherId)); continue; }
// Carry the caller's own preference bits from their row.
var mineRow = pair.FirstOrDefault(r => r.RequesterId == me);
wire.Add(SynthFriend(otherId, mineRow, me));
}
else
{
// Hide auto-generated Player_NNN placeholders from the
// synthesized list (they never set a real username).
if (RelationshipQueries.IsPlaceholderUsername(other.Username)) continue;
wire.Add(SynthFriend(otherId, null, me));
}
}
return wire;
}
/// <summary>A synthetic Friend relationship for global-friends mode,
/// carrying mute/ignore/favorite from the caller's real row when present.</summary>
private static Dictionary<string, object> SynthFriend(long other, RelationshipEntity? real, long me)
{
var mineDir = real is not null && real.RequesterId == me;
return new()
{
["PlayerID"] = (int)other,
["RelationshipType"] = RT_Friend,
["Muted"] = mineDir && real!.Muted ? 1 : 0,
["Ignored"] = mineDir && real!.Ignored ? 1 : 0,
["Favorited"] = mineDir && real!.Favorited ? 1 : 0,
};
}
/// <summary>GET <c>api/relationships/v2/personaldetails/{playerId}</c> /// <summary>GET <c>api/relationships/v2/personaldetails/{playerId}</c>
/// — single relationship row from the caller's perspective. Used /// — single relationship row from the caller's perspective. Used
/// by the watch's profile-card view to determine whether to show /// by the watch's profile-card view to determine whether to show
@@ -89,8 +160,27 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
{ {
var me = CurrentPlayerId; var me = CurrentPlayerId;
if (playerId <= 0 || playerId == me) return Ok(EmptyRelationship(playerId)); if (playerId <= 0 || playerId == me) return Ok(EmptyRelationship(playerId));
var row = await FindAsync(me, playerId); var pair = await db.Relationships
return Ok(row is null ? EmptyRelationship(playerId) : BuildRelationship(row, me)); .Where(r => (r.RequesterId == me && r.TargetId == playerId) ||
(r.RequesterId == playerId && r.TargetId == me))
.ToListAsync();
var blocked = pair.Any(r => r.Status == EntityStatus.Blocked);
var mineRow = pair.FirstOrDefault(r => r.RequesterId == me);
// Global-friends mode: report Friend for any real account that isn't
// the system account and isn't explicitly blocked. Placeholder
// Player_NNN accounts are hidden unless there's a genuine row.
if (playerId != RelationshipQueries.SystemAccountId
&& !blocked
&& await settings.IsGlobalFriendsEnabledAsync())
{
var isPlaceholder = pair.Count == 0 && RelationshipQueries.IsPlaceholderUsername(
await db.Players.Where(p => p.Id == playerId)
.Select(p => p.Username).FirstOrDefaultAsync());
if (!isPlaceholder) return Ok(SynthFriend(playerId, mineRow, me));
}
return Ok(pair.Count == 0 ? EmptyRelationship(playerId) : BuildMerged(me, playerId, pair));
} }
// ── /api/relationships/v2/sendfriendrequest?id={N} ─────────────── // ── /api/relationships/v2/sendfriendrequest?id={N} ───────────────
@@ -106,17 +196,34 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
if (id <= 0 || id == me) if (id <= 0 || id == me)
return Ok(EmptyRelationship(id)); return Ok(EmptyRelationship(id));
var existing = await FindAsync(me, id); var pair = await db.Relationships
if (existing is not null) .Where(r => (r.RequesterId == me && r.TargetId == id) ||
return Ok(BuildRelationship(existing, me)); (r.RequesterId == id && r.TargetId == me))
.ToListAsync();
var row = new RelationshipEntity // Anything that already defines a status (friend/pending/block) wins —
// a None row is preference-only and doesn't count as a relationship.
if (pair.Any(r => r.Status != EntityStatus.None))
return Ok(BuildMerged(me, id, pair));
// Promote the caller's own preference-only row if present (keeping its
// favorite/mute/ignore bits) rather than spawning a second row.
var row = pair.FirstOrDefault(r => r.RequesterId == me);
if (row is not null)
{ {
RequesterId = me, row.Status = EntityStatus.PendingSent;
TargetId = id, row.UpdatedAt = DateTime.UtcNow;
Status = EntityStatus.PendingSent, }
}; else
db.Relationships.Add(row); {
row = new RelationshipEntity
{
RequesterId = me,
TargetId = id,
Status = EntityStatus.PendingSent,
};
db.Relationships.Add(row);
}
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await notifications.FriendRequestReceived(id, me); await notifications.FriendRequestReceived(id, me);
@@ -162,16 +269,26 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
if (id <= 0) if (id <= 0)
return Ok(EmptyRelationship(id)); return Ok(EmptyRelationship(id));
var rel = await FindAsync(me, id); var pair = await db.Relationships
if (rel is null) .Where(r => (r.RequesterId == me && r.TargetId == id) ||
(r.RequesterId == id && r.TargetId == me))
.ToListAsync();
if (pair.Count == 0)
return Ok(EmptyRelationship(id)); return Ok(EmptyRelationship(id));
if (rel.Status == EntityStatus.Blocked)
return Ok(BuildRelationship(rel, me));
db.Relationships.Remove(rel); // Drop every non-block row for the pair (friendship row + any stray
// preference row), leaving a block intact — the watch unblocks
// through a separate path.
var removable = pair.Where(r => r.Status != EntityStatus.Blocked).ToList();
if (removable.Count == 0)
return Ok(BuildMerged(me, id, pair));
db.Relationships.RemoveRange(removable);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await notifications.FriendRemoved(id, me); await notifications.FriendRemoved(id, me);
return Ok(EmptyRelationship(id));
var remaining = pair.Except(removable).ToList();
return Ok(remaining.Count == 0 ? EmptyRelationship(id) : BuildMerged(me, id, remaining));
} }
// ── /api/relationships/v2/addfriend?id={N} ─────────────────────── // ── /api/relationships/v2/addfriend?id={N} ───────────────────────
@@ -186,7 +303,20 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
if (id <= 0 || id == me) if (id <= 0 || id == me)
return Ok(EmptyRelationship(id)); return Ok(EmptyRelationship(id));
var existing = await FindAsync(me, id); var pair = await db.Relationships
.Where(r => (r.RequesterId == me && r.TargetId == id) ||
(r.RequesterId == id && r.TargetId == me))
.ToListAsync();
// Don't override a block, and don't spawn a duplicate when a
// friendship row already exists (in either direction).
if (pair.Any(r => r.Status == EntityStatus.Blocked) ||
pair.Any(r => r.Status == EntityStatus.Friend))
return Ok(BuildMerged(me, id, pair));
// Promote the caller's own row (preference-only or pending) to Friend,
// or create one if none exists.
var existing = pair.FirstOrDefault(r => r.RequesterId == me);
if (existing is null) if (existing is null)
{ {
existing = new RelationshipEntity existing = new RelationshipEntity
@@ -197,7 +327,7 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
}; };
db.Relationships.Add(existing); db.Relationships.Add(existing);
} }
else if (existing.Status != EntityStatus.Blocked) else
{ {
existing.Status = EntityStatus.Friend; existing.Status = EntityStatus.Friend;
existing.UpdatedAt = DateTime.UtcNow; existing.UpdatedAt = DateTime.UtcNow;
@@ -211,8 +341,8 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
// "api/relationships/v1/unfavorite?id={N}". These return a // "api/relationships/v1/unfavorite?id={N}". These return a
// boolean-ish RecNetResult per the FavoritePlayer/UnfavoritePlayer // boolean-ish RecNetResult per the FavoritePlayer/UnfavoritePlayer
// disassembly (Core.Post … ExpectHttpStatusSuccess), not a // disassembly (Core.Post … ExpectHttpStatusSuccess), not a
// Relationship — they don't surface any state change in the // Relationship. The flag is persisted on the caller's directional row
// 2020 storage model so we just acknowledge. // and surfaced back through v2/get + personaldetails (see BuildMerged).
[HttpPost("api/relationships/v1/favorite")] [HttpPost("api/relationships/v1/favorite")]
public Task<IActionResult> Favorite([FromQuery] long id) => SetPreference(id, favorited: true); public Task<IActionResult> Favorite([FromQuery] long id) => SetPreference(id, favorited: true);
@@ -249,11 +379,16 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
r.RequesterId == me && r.TargetId == otherId); r.RequesterId == me && r.TargetId == otherId);
if (row is null) if (row is null)
{ {
// A preference (favorite/mute/ignore) is the caller's own bit and
// lives on their own directional row. Status None — favorite/mute/
// ignore must NOT fabricate a friendship; the real friendship row
// (this direction or the other) carries the actual status and the
// read path merges the two.
row = new RelationshipEntity row = new RelationshipEntity
{ {
RequesterId = me, RequesterId = me,
TargetId = otherId, TargetId = otherId,
Status = EntityStatus.Friend, // placeholder; favorite/mute/ignore don't require friend status Status = EntityStatus.None,
}; };
db.Relationships.Add(row); db.Relationships.Add(row);
} }
@@ -304,11 +439,6 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
// ── helpers ────────────────────────────────────────────────────── // ── helpers ──────────────────────────────────────────────────────
private Task<RelationshipEntity?> FindAsync(long me, long other) =>
db.Relationships.FirstOrDefaultAsync(r =>
(r.RequesterId == me && r.TargetId == other) ||
(r.RequesterId == other && r.TargetId == me));
/// <summary>Map a storage row to the watch's wire RelationshipType /// <summary>Map a storage row to the watch's wire RelationshipType
/// from <paramref name="me"/>'s perspective. PendingSent flips /// from <paramref name="me"/>'s perspective. PendingSent flips
/// direction depending on which side originated.</summary> /// direction depending on which side originated.</summary>
@@ -353,6 +483,32 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no
}; };
} }
/// <summary>Merge all rows for one pair (up to two — one per direction)
/// into a single wire object from <paramref name="me"/>'s view. The
/// relationship type comes from whichever row defines it (Blocked &gt;
/// Friend &gt; Pending); the favorite/mute/ignore bits are the caller's
/// own, read from the row where the caller is the requester. Without this
/// a favorite the caller set on a friendship the OTHER player initiated
/// lands on a separate row and is dropped by the watch (its list is keyed
/// by PlayerID, so a duplicate id loses).</summary>
private static Dictionary<string, object> BuildMerged(long me, long other, List<RelationshipEntity> pair)
{
var statusRow =
pair.FirstOrDefault(r => r.Status == EntityStatus.Blocked)
?? pair.FirstOrDefault(r => r.Status == EntityStatus.Friend)
?? pair.FirstOrDefault(r => r.Status is EntityStatus.PendingSent or EntityStatus.PendingReceived)
?? pair[0];
var mineRow = pair.FirstOrDefault(r => r.RequesterId == me);
return new()
{
["PlayerID"] = (int)other,
["RelationshipType"] = MapType(statusRow, me),
["Muted"] = mineRow is { Muted: true } ? 1 : 0,
["Ignored"] = mineRow is { Ignored: true } ? 1 : 0,
["Favorited"] = mineRow is { Favorited: true } ? 1 : 0,
};
}
/// <summary>"None" relationship for a target the caller has nothing /// <summary>"None" relationship for a target the caller has nothing
/// on file with — used as a no-op success response so the watch's /// on file with — used as a no-op success response so the watch's
/// Relationship.Deserialize stays satisfied.</summary> /// Relationship.Deserialize stays satisfied.</summary>
@@ -1168,12 +1168,41 @@ public class AdminController(
return Ok(new return Ok(new
{ {
row.SignupsDisabled, row.SignupsDisabled,
row.GlobalFriendsEnabled,
row.WeeklyChallengesCompletedRequired, row.WeeklyChallengesCompletedRequired,
row.UpdatedAt, row.UpdatedAt,
}); });
} }
public sealed record SignupsToggleRequest(bool Disabled); public sealed record SignupsToggleRequest(bool Disabled);
public sealed record GlobalFriendsToggleRequest(bool Enabled);
/// <summary>POST <c>api/admin/v1/settings/global-friends</c> — flip the
/// "everyone is friends" toggle. No relationship rows are written; the
/// friend graph is synthesized at read time (see
/// <see cref="RelationshipQueries"/>). After flipping, broadcast
/// <c>RelationshipsInvalid</c> to every connected watch so it re-fetches
/// <c>api/relationships/v2/get</c> and shows the new state live — no
/// relog (the 2020 watch handles that push via
/// <c>Relationships.RefreshList</c>).</summary>
[HttpPost("settings/global-friends")]
public async Task<ActionResult> SetGlobalFriends([FromBody] GlobalFriendsToggleRequest body)
{
var row = await serverSettings.SetGlobalFriendsEnabledAsync(body.Enabled);
await LogAsync(body.Enabled ? "global_friends_enabled" : "global_friends_disabled", "system", 0, "");
await db.SaveChangesAsync();
// Live refresh: every connected watch re-pulls its relationship list.
await notifications.BroadcastAsync(DorkNet.Models.Notification.PushNotificationId.RelationshipsInvalid);
return Ok(new
{
row.SignupsDisabled,
row.GlobalFriendsEnabled,
row.WeeklyChallengesCompletedRequired,
row.UpdatedAt,
});
}
/// <summary>POST <c>api/admin/v1/settings/signups</c> — flip the /// <summary>POST <c>api/admin/v1/settings/signups</c> — flip the
/// server-wide account-creation kill switch. While disabled, both /// server-wide account-creation kill switch. While disabled, both
@@ -1190,6 +1219,7 @@ public class AdminController(
return Ok(new return Ok(new
{ {
row.SignupsDisabled, row.SignupsDisabled,
row.GlobalFriendsEnabled,
row.WeeklyChallengesCompletedRequired, row.WeeklyChallengesCompletedRequired,
row.UpdatedAt, row.UpdatedAt,
}); });
@@ -1777,32 +1807,59 @@ public class AdminController(
// ── Avatar catalog (gifting picker source) ─────────────────────────── // ── Avatar catalog (gifting picker source) ───────────────────────────
/// <summary>GET <c>api/admin/v1/avatar-items</c> — flat list of every /// <summary>One giftable avatar item for the Gift picker. <c>Desc</c> is
/// renderable avatar item, keyed by the GUID the inventory grant /// the exact RecNet item string to gift — a bare base GUID for the
/// endpoint expects. Backs the Gift page's item picker so admins can /// default colour, or a <c>{guid},{swatch},{mask},</c> string for a
/// browse by friendly name instead of pasting GUIDs by hand. Read-only. /// colour variant. <c>Slot</c> is the real OutfitType (head/torso/…),
/// Catalog source is <c>data/avatar_item_lookup.json</c>, parsed once /// not the always-Outfit AvatarItemType.</summary>
/// at startup by <see cref="DorkNet.Server.Controllers.API.Avatar.V4.AvatarItemsController"/>.</summary> private sealed record GiftItemRow(
string Desc, string Guid, int Slot, string FriendlyName, string? Color, bool IsVariant);
/// <summary>GET <c>api/admin/v1/avatar-items</c> — every giftable avatar
/// item: each base item (default colour) PLUS every colour variant the
/// store seeds from <c>data/color_variants.json</c>. Backs the Gift
/// page's item picker so admins can browse by friendly name + colour
/// instead of pasting descs by hand. Read-only. Base catalog source is
/// <c>data/avatar_item_lookup.json</c>, parsed once at startup by
/// <see cref="DorkNet.Server.Controllers.API.Avatar.V4.AvatarItemsController"/>;
/// variants come from <see cref="DorkNet.Server.Services.StoreService.ColorVariantCatalog"/>.</summary>
[HttpGet("avatar-items")] [HttpGet("avatar-items")]
public ActionResult ListAvatarItems() public ActionResult ListAvatarItems()
{ {
var catalog = DorkNet.Server.Controllers.API.Avatar.V4.AvatarItemsController.Catalog; var catalog = DorkNet.Server.Controllers.API.Avatar.V4.AvatarItemsController.Catalog;
var safe = DorkNet.Server.Controllers.API.Avatar.V4.AvatarItemsController.SafeGuids; var safe = DorkNet.Server.Controllers.API.Avatar.V4.AvatarItemsController.SafeGuids;
var rows = catalog var rows = new List<GiftItemRow>(catalog.Count + DorkNet.Server.Services.StoreService.ColorVariantCatalog.Count);
.Select(kv => new
{ // Base items (default colour). Real slot from the bundled lookup —
Guid = kv.Key, // the DTO's AvatarItemType is always Outfit, which is useless as a
Slot = kv.Value.AvatarItemType, // slot filter, so resolve the actual OutfitType per GUID.
FriendlyName = kv.Value.FriendlyName, foreach (var kv in catalog)
Tooltip = kv.Value.Tooltip, {
Rarity = kv.Value.Rarity, if (!safe.Contains(kv.Key)) continue; // GUIDs the watch can't render
Safe = safe.Contains(kv.Key), var slot = DorkNet.Server.Controllers.API.Avatar.V4.AvatarItemsController
}) .TryGetOutfitType(kv.Key, out var s) ? s : 0;
.Where(r => r.Safe) // hide GUIDs that would crash the watch's parser rows.Add(new GiftItemRow(kv.Key, kv.Key, slot, kv.Value.FriendlyName, null, false));
}
// Colour variants — the "dozens of headphones" the admin sees in the
// store but not here. Each gifts as "{item},{swatch},{mask}," so the
// recipient gets the exact swatch, not the default colour.
foreach (var v in DorkNet.Server.Services.StoreService.ColorVariantCatalog.Values)
{
if (string.IsNullOrEmpty(v.item_guid) || string.IsNullOrEmpty(v.swatch_guid)) continue;
if (!safe.Contains(v.item_guid)) continue; // base must be renderable
var baseName = catalog.TryGetValue(v.item_guid, out var bd) && !string.IsNullOrEmpty(bd.FriendlyName)
? bd.FriendlyName
: v.item_name;
var desc = $"{v.item_guid},{v.swatch_guid},{v.mask_guid},";
rows.Add(new GiftItemRow(desc, v.item_guid,
v.item_outfit_type ?? 0, $"{baseName} — {v.color}", v.color, true));
}
return Ok(rows
.OrderBy(r => r.Slot) .OrderBy(r => r.Slot)
.ThenBy(r => r.FriendlyName) .ThenBy(r => r.FriendlyName, StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList());
return Ok(rows);
} }
// ── Send gift package (in-game gift box popup) ──────────────────────── // ── Send gift package (in-game gift box popup) ────────────────────────
@@ -36,6 +36,7 @@ public class GoToController(
RoomService rooms, RoomService rooms,
DorkNet.Server.Data.DorkNetDbContext db, DorkNet.Server.Data.DorkNetDbContext db,
NotificationService notifications, NotificationService notifications,
ServerSettingsService serverSettings,
ILogger<GoToController> logger) : ControllerBase ILogger<GoToController> logger) : ControllerBase
{ {
[HttpPost("/goto/room/{roomName}")] [HttpPost("/goto/room/{roomName}")]
@@ -634,11 +635,9 @@ public class GoToController(
private async Task NotifyFriendsOfMoveAsync(long playerId, RoomInstanceDto room) private async Task NotifyFriendsOfMoveAsync(long playerId, RoomInstanceDto room)
{ {
var friendIds = await db.Relationships // Honors the global-friends toggle: with it on, every other player is
.Where(r => r.Status == DorkNet.Server.Data.Entities.RelationshipStatus.Friend && // a "friend" and gets the presence ping.
(r.RequesterId == playerId || r.TargetId == playerId)) var friendIds = await RelationshipQueries.EffectiveFriendIdsAsync(db, serverSettings, playerId);
.Select(r => r.RequesterId == playerId ? r.TargetId : r.RequesterId)
.ToListAsync();
logger.LogInformation( logger.LogInformation(
"[goto-fanout] player={PlayerId} moved to room={RoomId}/{RoomName} → notifying {FriendCount} friend(s)", "[goto-fanout] player={PlayerId} moved to room={RoomId}/{RoomName} → notifying {FriendCount} friend(s)",
playerId, room.RoomId, room.Name, friendIds.Count); playerId, room.RoomId, room.Name, friendIds.Count);
@@ -267,16 +267,15 @@ public class PublicSiteController(
}).ToList()); }).ToList());
} }
public sealed record JoinRedeemRequest(string? Code, string? Username, string? DeviceId); public sealed record JoinRedeemRequest(string? Code, string? Username, string? Password);
/// <summary>POST /api/site/v1/join/redeem — redeem a signup code, /// <summary>POST /api/site/v1/join/redeem — redeem a signup code,
/// minting an account bound to the supplied device id. On success the /// minting a username/password account. Returns {ok, error?,
/// player relaunches the game and the watch's device-id login finds /// username?}.</summary>
/// the new account. Returns {ok, error?, username?}.</summary>
[HttpPost("join/redeem")] [HttpPost("join/redeem")]
public async Task<IActionResult> JoinRedeem([FromBody] JoinRedeemRequest body) public async Task<IActionResult> JoinRedeem([FromBody] JoinRedeemRequest body)
{ {
var result = await signupCodes.RedeemAsync(body.Code, body.Username, body.DeviceId); var result = await signupCodes.RedeemAsync(body.Code, body.Username, body.Password);
if (!result.Ok) if (!result.Ok)
return BadRequest(new { ok = false, error = result.Error }); return BadRequest(new { ok = false, error = result.Error });
return Ok(new { ok = true, username = result.Username, playerId = result.PlayerId }); return Ok(new { ok = true, username = result.Username, playerId = result.PlayerId });
@@ -9,6 +9,12 @@ namespace DorkNet.Server.Data.Entities;
/// </summary> /// </summary>
public enum RelationshipStatus public enum RelationshipStatus
{ {
/// <summary>No friendship/block — the row exists only to hold the
/// requester's own favorite/mute/ignore preference about the target.
/// The actual relationship status (if any) lives on the paired row in
/// the other direction; the read path merges the two.</summary>
None = 0,
/// <summary>Mutual friend (both directions accepted).</summary> /// <summary>Mutual friend (both directions accepted).</summary>
Friend = 1, Friend = 1,
@@ -22,6 +22,16 @@ public class ServerSettingsEntity
/// brand-new account requests are blocked.</summary> /// brand-new account requests are blocked.</summary>
public bool SignupsDisabled { get; set; } public bool SignupsDisabled { get; set; }
/// <summary>When true, every account on the server is treated as a
/// friend of every other account — the relationships list, friend
/// online-status HUD, and room-move fan-out all behave as if everyone
/// is mutually friended. Built for small private servers where
/// searching + manually friend-requesting each other is friction; no
/// rows are written, so flipping it off instantly reverts to the real
/// relationship graph. Blocked relationships still suppress the pairing,
/// and the system/coach account (Id=1) is excluded.</summary>
public bool GlobalFriendsEnabled { get; set; }
/// <summary>When true, the watch's weekly-challenge map reports /// <summary>When true, the watch's weekly-challenge map reports
/// <c>CompletedRequired</c> — the player must finish every listed /// <c>CompletedRequired</c> — the player must finish every listed
/// challenge before the gift unlocks. Mirrors /// challenge before the gift unlocks. Mirrors
+2869 -1622
View File
@@ -9,87 +9,6 @@
"item_outfit_type": 200, "item_outfit_type": 200,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Windbreaker_Blue_Swatch",
"swatch_guid": "EbVDqEJQOk6j0UvX3t-8rQ",
"color": "Blue",
"item_name": "(Wrist_80sWindbreaker)",
"item_guid": "52VsI_lKlkSUAOJlfDnyrQ",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "Windbreaker_Yellow_Swatch",
"swatch_guid": "C3ll7uPfUkeZ1QLX80_BpQ",
"color": "Yellow",
"item_name": "(Wrist_80sWindbreaker)",
"item_guid": "52VsI_lKlkSUAOJlfDnyrQ",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "Windbreaker_Salmon_Swatch",
"swatch_guid": "JS7aa4QSUUOpUYPqRZEb5g",
"color": "Salmon",
"item_name": "(Wrist_80sWindbreaker)",
"item_guid": "52VsI_lKlkSUAOJlfDnyrQ",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "Knight_SRank_Swatch",
"swatch_guid": "ITH2OpzjP022vZ5JgxV_iQ",
"color": "SRank",
"item_name": "(Shirt_KnightTorso)",
"item_guid": "739fd42a-8a8c-44a5-afa3-e0974802c6ae",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Knight_Blue_Swatch",
"swatch_guid": "ed134bee-d199-43eb-98bd-206571603f40",
"color": "Blue",
"item_name": "(Shirt_KnightTorso)",
"item_guid": "739fd42a-8a8c-44a5-afa3-e0974802c6ae",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Knight_Red_Swatch",
"swatch_guid": "59866578-d4e0-417b-9483-233ab108b1ac",
"color": "Red",
"item_name": "(Shirt_KnightTorso)",
"item_guid": "739fd42a-8a8c-44a5-afa3-e0974802c6ae",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Knight_Black_Swatch",
"swatch_guid": "e1f49b74-b071-4bd1-9c4b-af36d24d45c5",
"color": "Black",
"item_name": "(Shirt_KnightTorso)",
"item_guid": "739fd42a-8a8c-44a5-afa3-e0974802c6ae",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Knight_Green_Swatch",
"swatch_guid": "PMa9370LPEig_7pKwCceEQ",
"color": "Green",
"item_name": "(Shirt_KnightTorso)",
"item_guid": "739fd42a-8a8c-44a5-afa3-e0974802c6ae",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Knight_White_Swatch",
"swatch_guid": "22dc463a-a89b-46ad-9a0d-557c742b4a80",
"color": "White",
"item_name": "(Shirt_KnightTorso)",
"item_guid": "739fd42a-8a8c-44a5-afa3-e0974802c6ae",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "LumberJack_Blue_Swatch", "swatch_name": "LumberJack_Blue_Swatch",
"swatch_guid": "cKHAetNcgUOxFfDkAPPF9w", "swatch_guid": "cKHAetNcgUOxFfDkAPPF9w",
@@ -99,15 +18,6 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "LumberJack_Green_Swatch",
"swatch_guid": "7amHTgEv-UOJSGl0ofo4mw",
"color": "Green",
"item_name": "(Shirt_Lumberjack)",
"item_guid": "350c5c1e-318b-4233-a7e5-1f49909d8e65",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "SpaceMarine_Red_Swatch", "swatch_name": "SpaceMarine_Red_Swatch",
"swatch_guid": "d78643e4-c0d2-4836-bf22-b5a0e2d65b28", "swatch_guid": "d78643e4-c0d2-4836-bf22-b5a0e2d65b28",
@@ -115,52 +25,7 @@
"item_name": "(Shirt_SpaceMarine)", "item_name": "(Shirt_SpaceMarine)",
"item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338", "item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "7ac81d60-05b8-4a85-982c-f69213a93818"
},
{
"swatch_name": "SpaceMarine_Pink_Swatch",
"swatch_guid": "a36d252b-16ac-4a82-bece-de145147e7d9",
"color": "Pink",
"item_name": "(Shirt_SpaceMarine)",
"item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "SpaceMarine_Cyan_Swatch",
"swatch_guid": "c8cc9589-4831-4e73-a865-ef8c139a2ba7",
"color": "Cyan",
"item_name": "(Shirt_SpaceMarine)",
"item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "SpaceMarine_Light_Swatch",
"swatch_guid": "68273c9e-96d6-4ddb-aaab-d99229e864d5",
"color": "Light",
"item_name": "(Shirt_SpaceMarine)",
"item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "SpaceMarine_Dark_Swatch",
"swatch_guid": "a089bcc6-798d-4777-98e2-7afd290dc6b6",
"color": "Dark",
"item_name": "(Shirt_SpaceMarine)",
"item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "SpaceMarine_SRank_Swatch",
"swatch_guid": "4ElTihrMA0ipcgeb9UG01Q",
"color": "SRank",
"item_name": "(Shirt_SpaceMarine)",
"item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 101,
"mask_guid": ""
}, },
{ {
"swatch_name": "Hibiscus_Yellow_Swatch", "swatch_name": "Hibiscus_Yellow_Swatch",
@@ -171,15 +36,6 @@
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Hibiscus_White_Swatch",
"swatch_guid": "ce465d53-3807-423f-baa0-bdba2a75ceb3",
"color": "White",
"item_name": "(Hat_Hibiscus)",
"item_guid": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01",
"item_outfit_type": 0,
"mask_guid": ""
},
{ {
"swatch_name": "Blazer_CandyCane_Swatch", "swatch_name": "Blazer_CandyCane_Swatch",
"swatch_guid": "_ycDQ6RcyEGAntVlSDpPeA", "swatch_guid": "_ycDQ6RcyEGAntVlSDpPeA",
@@ -189,19 +45,10 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54" "mask_guid": "be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54"
}, },
{
"swatch_name": "Blazer_Shamrock_Swatch",
"swatch_guid": "7WrSbK4Ml0K2z-ig5HIUCw",
"color": "Shamrock",
"item_name": "(Blazer)",
"item_guid": "de0ac50d-2adb-4114-bd2e-68953b13d706",
"item_outfit_type": 101,
"mask_guid": "be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54"
},
{ {
"swatch_name": "Raincoat_Shirt_Blue_Swatch", "swatch_name": "Raincoat_Shirt_Blue_Swatch",
"swatch_guid": "GH9q8_0O9EqjGdz7lGWD2g", "swatch_guid": "GH9q8_0O9EqjGdz7lGWD2g",
"color": "Blue", "color": "Shirt_Blue",
"item_name": "(Shirt_Raincoat)", "item_name": "(Shirt_Raincoat)",
"item_guid": "9e6397dd-b7b0-4d32-8c86-1910106a8478", "item_guid": "9e6397dd-b7b0-4d32-8c86-1910106a8478",
"item_outfit_type": 101, "item_outfit_type": 101,
@@ -225,6 +72,15 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Winter_Gloves_Tree_Green_Swatch",
"swatch_guid": "tyzdtSkR9k-YCnjNvPAT6w",
"color": "Gloves_Tree_Green",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": ""
},
{ {
"swatch_name": "Headphones_Red_Swatch", "swatch_name": "Headphones_Red_Swatch",
"swatch_guid": "viL4CPch2ECwG-RNaWcyZw", "swatch_guid": "viL4CPch2ECwG-RNaWcyZw",
@@ -232,129 +88,12 @@
"item_name": "(Hat_Headphones)", "item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw", "item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "Headphones_EmissiveBlack_Swatch",
"swatch_guid": "Uz2hrptCa0erjqyPoiZxew",
"color": "EmissiveBlack",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_EmissiveRed_Swatch",
"swatch_guid": "I1J-xY4YNU2dB_od4P_7Dg",
"color": "EmissiveRed",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_TwoRed_Swatch",
"swatch_guid": "hf0gkFVS8ESvceRsC3LAXQ",
"color": "TwoRed",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_EmissiveBlue_Swatch",
"swatch_guid": "2sf-FU4WckGBDWUkVz4z1g",
"color": "EmissiveBlue",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_Purple_Swatch",
"swatch_guid": "a5nUvJZgTEGBUvdHx0Y6Ng",
"color": "Purple",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_EmissiveOrange_Swatch",
"swatch_guid": "d9mwowRelE2lDKEA0UQMZQ",
"color": "EmissiveOrange",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_Orange_Swatch",
"swatch_guid": "JWBK5v78Y0uRVEXlD-Qk6Q",
"color": "Orange",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_Blue_Swatch",
"swatch_guid": "8ANbOhNnO0yLQOCIBsDwfg",
"color": "Blue",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_TwoBlue_Swatch",
"swatch_guid": "4scG_UBlzE-5DWRhSrtm6g",
"color": "TwoBlue",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_TwoOrange_Swatch",
"swatch_guid": "nX7krD8e2km_ZlOLWcoxdQ",
"color": "TwoOrange",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_TwoPurple_Swatch",
"swatch_guid": "OqsbZ8Xls0i8uA6ha7H3qQ",
"color": "TwoPurple",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_EmissivePurple_Swatch",
"swatch_guid": "3PfP661Fg0CruzRxis9EiA",
"color": "EmissivePurple",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_TwoBlack_Swatch",
"swatch_guid": "ns5tIRishUOp-nYqLedcwA",
"color": "TwoBlack",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": ""
}, },
{ {
"swatch_name": "JackfrostHair_Hat_Lilac_Swatch", "swatch_name": "JackfrostHair_Hat_Lilac_Swatch",
"swatch_guid": "KwhdQ1deNE6Y6jzO9vaAnQ", "swatch_guid": "KwhdQ1deNE6Y6jzO9vaAnQ",
"color": "Lilac", "color": "Hat_Lilac",
"item_name": "(Hat_JackfrostHair)", "item_name": "(Hat_JackfrostHair)",
"item_guid": "e6216087-fb37-46f1-bda4-a88c4640f339", "item_guid": "e6216087-fb37-46f1-bda4-a88c4640f339",
"item_outfit_type": 0, "item_outfit_type": 0,
@@ -369,15 +108,6 @@
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Baseball_Green_Swatch",
"swatch_guid": "e5de2e45-207c-46af-9a5e-eb01a0c621fe",
"color": "Green",
"item_name": "(Hat_Baseball)",
"item_guid": "14ef6b00-debf-4a85-9755-b4d37df496d3",
"item_outfit_type": 0,
"mask_guid": ""
},
{ {
"swatch_name": "ButtonTop_Orange_Swatch", "swatch_name": "ButtonTop_Orange_Swatch",
"swatch_guid": "e0397982-c2c2-4733-9a40-46e18675b5af", "swatch_guid": "e0397982-c2c2-4733-9a40-46e18675b5af",
@@ -388,12 +118,21 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "ButtonTop_Tan_Swatch", "swatch_name": "CowboyCherry_Swatch",
"swatch_guid": "49f5864f-9d40-497c-88c8-e87f64d41d74", "swatch_guid": "624c85e5-095a-48b6-85fb-c9bbade19a8d",
"color": "Tan", "color": "Cherry",
"item_name": "(ButtonTop)", "item_name": "(Hat_Cowboy)",
"item_guid": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352", "item_guid": "ad0bdec5-f767-42e7-99e0-221473464a85",
"item_outfit_type": 101, "item_outfit_type": 0,
"mask_guid": "54778d93-2cff-48db-b25b-65c08b0e5be8"
},
{
"swatch_name": "Mobster_HerringBone_Tan_Swatch",
"swatch_guid": "531b4297-b38b-4362-9854-4f2139ea43be",
"color": "HerringBone_Tan",
"item_name": "(Wrist_Mobster)",
"item_guid": "08d56627-4500-4724-8692-a23c96ddb3ec",
"item_outfit_type": 200,
"mask_guid": "" "mask_guid": ""
}, },
{ {
@@ -405,33 +144,6 @@
"item_outfit_type": 200, "item_outfit_type": 200,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "MagicalGirl_Red_Swatch",
"swatch_guid": "FE7_gQNBt0CXzMN6di5HWA",
"color": "Red",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "MagicalGirl_Green_Swatch",
"swatch_guid": "kTP3xKhzBkuQF3SqvmYFKA",
"color": "Green",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "MagicalGirl_Orange_Swatch",
"swatch_guid": "hYu9itGe1Ee5Neanhj9VtQ",
"color": "Orange",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": ""
},
{ {
"swatch_name": "Wizard_SRank_Swatch", "swatch_name": "Wizard_SRank_Swatch",
"swatch_guid": "RKmRHk2N_U2gd91nybo6vg", "swatch_guid": "RKmRHk2N_U2gd91nybo6vg",
@@ -441,51 +153,6 @@
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Wizard_Purple_Swatch",
"swatch_guid": "d3KmqVuMQkCYK6sVTMOTOA",
"color": "Purple",
"item_name": "(Hat_Wizard)",
"item_guid": "01689baf-405e-4b38-9f09-b624e03865b8",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Wizard_White_Swatch",
"swatch_guid": "d3a9b576-5433-4305-bd96-b08820c09212",
"color": "White",
"item_name": "(Hat_Wizard)",
"item_guid": "01689baf-405e-4b38-9f09-b624e03865b8",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Wizard_Green_Swatch",
"swatch_guid": "357eb160-360b-4a63-840d-65d11cc8ffc2",
"color": "Green",
"item_name": "(Hat_Wizard)",
"item_guid": "01689baf-405e-4b38-9f09-b624e03865b8",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Wizard_Black_Swatch",
"swatch_guid": "7afe1bb0-5ca9-4263-8f85-49d3a18353c7",
"color": "Black",
"item_name": "(Hat_Wizard)",
"item_guid": "01689baf-405e-4b38-9f09-b624e03865b8",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Wizard_Red_Swatch",
"swatch_guid": "a38254c0-00e3-429b-9d5d-b9e6a9812e69",
"color": "Red",
"item_name": "(Hat_Wizard)",
"item_guid": "01689baf-405e-4b38-9f09-b624e03865b8",
"item_outfit_type": 0,
"mask_guid": ""
},
{ {
"swatch_name": "Punk_Pink_Swatch", "swatch_name": "Punk_Pink_Swatch",
"swatch_guid": "4169bfed-8403-4590-a29d-4941275ac0b6", "swatch_guid": "4169bfed-8403-4590-a29d-4941275ac0b6",
@@ -493,43 +160,16 @@
"item_name": "(Wrist_Punk)", "item_name": "(Wrist_Punk)",
"item_guid": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", "item_guid": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7",
"item_outfit_type": 200, "item_outfit_type": 200,
"mask_guid": "" "mask_guid": "dd7472ed-bdcb-4811-81ba-5ed682fb7675"
}, },
{ {
"swatch_name": "Punk_WristPink_Swatch", "swatch_name": "Countess_Wrist_Purple_Swatch",
"swatch_guid": "f3b2330a-b981-4c7e-902f-1f57382f2388", "swatch_guid": "jwEB6LyEaEekqtPNRc6TVA",
"color": "WristPink", "color": "Wrist_Purple",
"item_name": "(Wrist_Punk)", "item_name": "(Shirt_Countess)",
"item_guid": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7", "item_guid": "QCtqxbvYOkan79YFugH_Cw",
"item_outfit_type": 200, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "8kdJkmwNM0WQmMVrb8fK8A"
},
{
"swatch_name": "Punk_WristRed_Swatch",
"swatch_guid": "baffdefe-de21-4551-9330-b50873965bdd",
"color": "WristRed",
"item_name": "(Wrist_Punk)",
"item_guid": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "Punk_Green_Swatch",
"swatch_guid": "0724b166-9ace-46ff-a191-b13f135b00a8",
"color": "Green",
"item_name": "(Wrist_Punk)",
"item_guid": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "Punk_WristGreen_Swatch",
"swatch_guid": "a6796264-9c7e-4605-830c-06db8d0dc780",
"color": "WristGreen",
"item_name": "(Wrist_Punk)",
"item_guid": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7",
"item_outfit_type": 200,
"mask_guid": ""
}, },
{ {
"swatch_name": "Hat_GraduationCap_Green_Swatch", "swatch_name": "Hat_GraduationCap_Green_Swatch",
@@ -547,16 +187,16 @@
"item_name": "(EquestrianJacket)", "item_name": "(EquestrianJacket)",
"item_guid": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", "item_guid": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "d344b8cc-85a8-4ace-9f92-38c84f396e99"
}, },
{ {
"swatch_name": "EquestrianJacket_Gray_Swatch", "swatch_name": "Headphones_EmissiveBlack_Swatch",
"swatch_guid": "4828b50c-95b6-466a-bb25-514891d78202", "swatch_guid": "Uz2hrptCa0erjqyPoiZxew",
"color": "Gray", "color": "EmissiveBlack",
"item_name": "(EquestrianJacket)", "item_name": "(Hat_Headphones)",
"item_guid": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5", "item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 101, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
}, },
{ {
"swatch_name": "Shirt_VampireHunter_White_Swatch", "swatch_name": "Shirt_VampireHunter_White_Swatch",
@@ -568,22 +208,76 @@
"mask_guid": "x10FeRBsQU2DIIpG8XIlwg" "mask_guid": "x10FeRBsQU2DIIpG8XIlwg"
}, },
{ {
"swatch_name": "Shirt_VampireHunter_Midnight_Swatch", "swatch_name": "FighterPilot_Gloves_Orange_Swatch",
"swatch_guid": "CavRDqIlmUeTZBtCLn1hZQ", "swatch_guid": "18b52db5-0261-46a0-9214-ff510d455ed9",
"color": "Midnight", "color": "Gloves_Orange",
"item_name": "(Shirt_VampireHunter)", "item_name": "(Wrist_FighterPilot)",
"item_guid": "JWfs9IiQs0Weu8MmyGpqLQ", "item_guid": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8",
"item_outfit_type": 101, "item_outfit_type": 200,
"mask_guid": "x10FeRBsQU2DIIpG8XIlwg" "mask_guid": ""
}, },
{ {
"swatch_name": "Shirt_VampireHunter_Leather_Swatch", "swatch_name": "Aviator_Gloves_Tan_Swatch",
"swatch_guid": "Z8Bfk0MTb0evZehvvgzuBA", "swatch_guid": "7fb8496f-0e6c-4e48-b8fa-5709ebbf4820",
"color": "Leather", "color": "Gloves_Tan",
"item_name": "(Shirt_VampireHunter)", "item_name": "(Wrist_Aviator)",
"item_guid": "JWfs9IiQs0Weu8MmyGpqLQ", "item_guid": "125a3a62-1005-47dc-9fd8-ee09ea803e9f",
"item_outfit_type": 200,
"mask_guid": "5659ce2c-9cb0-49da-8c39-4b7df0f8deec"
},
{
"swatch_name": "Astronaut_Glove_Orange_Swatch",
"swatch_guid": "y_gTbnuYS0GKJmnCs2retw",
"color": "e_Orange",
"item_name": "(Wrist_AstronautGlove)",
"item_guid": "88739d79-aeb9-471a-b25b-776f46bba9f6",
"item_outfit_type": 200,
"mask_guid": "hjk9OO8CW0KNGR7A6_p84A"
},
{
"swatch_name": "Winter_Hat_Tree_Swatch",
"swatch_guid": "xVwzV9-m6E-8PVpkg4D-AQ",
"color": "Hat_Tree",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": "nalaCGqtL0SFA4Itwqmp8g"
},
{
"swatch_name": "Harness_Hat_Teal_Swatch",
"swatch_guid": "lsyYlxY9o0a3TbUrHkxeyQ",
"color": "Hat_Teal",
"item_name": "(Shirt_Harness)",
"item_guid": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "x10FeRBsQU2DIIpG8XIlwg" "mask_guid": "PZHohDVX0U2frX13N4KYDQ"
},
{
"swatch_name": "Sherlock_Torso_Gray_Swatch",
"swatch_guid": "msOFHoFN9UORkeXXtdqBsw",
"color": "Torso_Gray",
"item_name": "(Hat_Sherlock)",
"item_guid": "5bde836f-9343-4934-b01a-606f707020ef",
"item_outfit_type": 0,
"mask_guid": "9ZP617DQk0mZ10J3d5q9oA"
},
{
"swatch_name": "BowlingShirt_CenterStripe_Burgandy_Swatch",
"swatch_guid": "ghM-uFq5-0Ci38AOHa9zZA",
"color": "Shirt_CenterStripe_Burgandy",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "IWg9wuda4kaCilJk8JQpTQ"
},
{
"swatch_name": "Scuba_Neck_Blue_Swatch",
"swatch_guid": "V_duN2oWaUmmao1xHhzCJA",
"color": "Neck_Blue",
"item_name": "(Shirt_Scuba)",
"item_guid": "57552652-d902-43b7-a68e-5c7da87582bc",
"item_outfit_type": 101,
"mask_guid": ""
}, },
{ {
"swatch_name": "Scallywag_Bracelet_SRank_Swatch", "swatch_name": "Scallywag_Bracelet_SRank_Swatch",
@@ -595,21 +289,30 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Scallywag_Bracelet_Red_Swatch", "swatch_name": "ArtistHat_Red_Swatch",
"swatch_guid": "62bd5dda-08da-4528-83c1-f0ca3812dc6e", "swatch_guid": "5c8f36c5-07da-49f4-af02-d23207a43216",
"color": "Red", "color": "Hat_Red",
"item_name": "(Wrist_Scallywag_Bracelet)", "item_name": "(Shirt_Artist)",
"item_guid": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", "item_guid": "02077915-57d3-4e26-b4fe-8e0399cbb293",
"item_outfit_type": 200, "item_outfit_type": 101,
"mask_guid": "3485a30d-27f8-450b-b5aa-1ff3cd227d19"
},
{
"swatch_name": "ArcherBlack_Quiver_Swatch",
"swatch_guid": "74b009f4-e128-4c59-8a6a-4c0ba6119bde",
"color": "Black_Quiver",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Scallywag_Bracelet_Blue_Swatch", "swatch_name": "ArcherYellow_Swatch",
"swatch_guid": "cdca593a-8840-4140-9145-fe962732040c", "swatch_guid": "6e152cc7-e93a-4408-8092-236db420b680",
"color": "Blue", "color": "Yellow",
"item_name": "(Wrist_Scallywag_Bracelet)", "item_name": "(Shirt_Archer)",
"item_guid": "b9e0f03b-2609-4911-82e9-1e324dbbe8be", "item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 200, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
@@ -621,24 +324,6 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "3ba69a39-a4e7-4505-bae7-8db31360ec1c" "mask_guid": "3ba69a39-a4e7-4505-bae7-8db31360ec1c"
}, },
{
"swatch_name": "PurpleJacket_Blue_Swatch",
"swatch_guid": "b42d903b-b393-4f49-af5e-38d0fb6c42b0",
"color": "Blue",
"item_name": "(PurpleJacket)",
"item_guid": "745c5110-104e-4b37-aabf-7c690fd54811",
"item_outfit_type": 101,
"mask_guid": "3ba69a39-a4e7-4505-bae7-8db31360ec1c"
},
{
"swatch_name": "PurpleJacket_Black_Swatch",
"swatch_guid": "8cab8cc6-9a2d-434c-8b72-e66edbdd223c",
"color": "Black",
"item_name": "(PurpleJacket)",
"item_guid": "745c5110-104e-4b37-aabf-7c690fd54811",
"item_outfit_type": 101,
"mask_guid": "3ba69a39-a4e7-4505-bae7-8db31360ec1c"
},
{ {
"swatch_name": "FighterPilot_Orange_Swatch", "swatch_name": "FighterPilot_Orange_Swatch",
"swatch_guid": "cea53c7f-24a9-4e3b-80b9-fbf95738f973", "swatch_guid": "cea53c7f-24a9-4e3b-80b9-fbf95738f973",
@@ -648,24 +333,6 @@
"item_outfit_type": 200, "item_outfit_type": 200,
"mask_guid": "435eba0d-d8f9-43cc-91aa-2959d63c6fa7" "mask_guid": "435eba0d-d8f9-43cc-91aa-2959d63c6fa7"
}, },
{
"swatch_name": "FighterPilot_Black_Swatch",
"swatch_guid": "8c74ee27-3d56-401b-8a8c-7868a80770e8",
"color": "Black",
"item_name": "(Wrist_FighterPilot)",
"item_guid": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8",
"item_outfit_type": 200,
"mask_guid": "435eba0d-d8f9-43cc-91aa-2959d63c6fa7"
},
{
"swatch_name": "FighterPilot_Blue_Swatch",
"swatch_guid": "e37b07d1-6b9d-49b9-94ca-1c728e228cfe",
"color": "Blue",
"item_name": "(Wrist_FighterPilot)",
"item_guid": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8",
"item_outfit_type": 200,
"mask_guid": "435eba0d-d8f9-43cc-91aa-2959d63c6fa7"
},
{ {
"swatch_name": "Neck_Grillmaster_Denim_Swatch", "swatch_name": "Neck_Grillmaster_Denim_Swatch",
"swatch_guid": "eV3Nv6SWS0yl3f-fgjPb-w", "swatch_guid": "eV3Nv6SWS0yl3f-fgjPb-w",
@@ -675,15 +342,6 @@
"item_outfit_type": 100, "item_outfit_type": 100,
"mask_guid": "t-ZEWJCgFEyGP2PjNblQMA" "mask_guid": "t-ZEWJCgFEyGP2PjNblQMA"
}, },
{
"swatch_name": "Neck_Grillmaster_White_Swatch",
"swatch_guid": "Lxx3q6JRnUW_cPG_kGG4ag",
"color": "White",
"item_name": "(Neck_Grillmaster)",
"item_guid": "tTKv7eRODka2KC_hsY2O3Q",
"item_outfit_type": 100,
"mask_guid": "t-ZEWJCgFEyGP2PjNblQMA"
},
{ {
"swatch_name": "BabydollDress_Green_Swatch", "swatch_name": "BabydollDress_Green_Swatch",
"swatch_guid": "c9589974-c261-485e-b571-cb2f34fb178f", "swatch_guid": "c9589974-c261-485e-b571-cb2f34fb178f",
@@ -694,22 +352,22 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "BabydollDress_Black_Swatch", "swatch_name": "Hat_SeaCaptainDarkDarkGold_Swatch",
"swatch_guid": "e13cf33d-7588-4e2e-955e-f0a29d6380b7", "swatch_guid": "70112887-dc8d-49d5-96ed-0845204e0b58",
"color": "Black", "color": "DarkDarkGold",
"item_name": "(BabydollDress)", "item_name": "(Hat_SeaCaptain)",
"item_guid": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", "item_guid": "f5c9743f-e77c-42f2-ba63-9843342cbf8e",
"item_outfit_type": 101, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
}, },
{ {
"swatch_name": "BabydollDress_Pink_Swatch", "swatch_name": "Letterman_Wrist_Red_Swatch",
"swatch_guid": "da6d0ced-3d43-4878-a868-925f2ed6fd5b", "swatch_guid": "HWc25-d8dUmNN2F3n8mV5A",
"color": "Pink", "color": "Wrist_Red",
"item_name": "(BabydollDress)", "item_name": "(Wrist_Letterman)",
"item_guid": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf", "item_guid": "2dFMy4zc7ky_Fd5fzfgInw",
"item_outfit_type": 101, "item_outfit_type": 200,
"mask_guid": "" "mask_guid": "4EHIyZkOOEqnM-PuD_a5xw"
}, },
{ {
"swatch_name": "WitchHunter_Purple_Swatch", "swatch_name": "WitchHunter_Purple_Swatch",
@@ -721,49 +379,13 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "WitchHunter_Green_Swatch", "swatch_name": "Beekeeper_Shirt_Teal_Swatch",
"swatch_guid": "64545dcc-a8eb-4c69-a539-78ee73a2d327", "swatch_guid": "oZ8k0Qv3SkG-DkY0_dP7UA",
"color": "Green", "color": "Shirt_Teal",
"item_name": "(Shirt_WitchHunter)", "item_name": "(Shirt_Beekeeper)",
"item_guid": "418db0e0-c7af-40c2-9cac-d461ce41e210", "item_guid": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "afd88a79-7f03-40f6-bcb4-c247dd86edc4"
},
{
"swatch_name": "WitchHunter_Blue_Swatch",
"swatch_guid": "aBQ3R9boek-mYeSYSNvDbQ",
"color": "Blue",
"item_name": "(Shirt_WitchHunter)",
"item_guid": "418db0e0-c7af-40c2-9cac-d461ce41e210",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "WitchHunter_SRank_Swatch",
"swatch_guid": "Tbrc2MlSlE6mKua8qSoluQ",
"color": "SRank",
"item_name": "(Shirt_WitchHunter)",
"item_guid": "418db0e0-c7af-40c2-9cac-d461ce41e210",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "WitchHunter_White_Swatch",
"swatch_guid": "e02838f8-4029-455a-b35c-dd40f972b05b",
"color": "White",
"item_name": "(Shirt_WitchHunter)",
"item_guid": "418db0e0-c7af-40c2-9cac-d461ce41e210",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "WitchHunter_Red_Swatch",
"swatch_guid": "909ed249-ce09-4304-8b02-56794fa7567d",
"color": "Red",
"item_name": "(Shirt_WitchHunter)",
"item_guid": "418db0e0-c7af-40c2-9cac-d461ce41e210",
"item_outfit_type": 101,
"mask_guid": ""
}, },
{ {
"swatch_name": "Smuggler_Green_Swatch", "swatch_name": "Smuggler_Green_Swatch",
@@ -772,52 +394,7 @@
"item_name": "(Shirt_Smuggler)", "item_name": "(Shirt_Smuggler)",
"item_guid": "25d4c652-1730-4925-925e-31a290a2272a", "item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "970a8c7e-d091-4719-bb9c-e3be8d46ee68"
},
{
"swatch_name": "Smuggler_SRank_Swatch",
"swatch_guid": "bzYdPb8GWUS-CgT4wrziIA",
"color": "SRank",
"item_name": "(Shirt_Smuggler)",
"item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Smuggler_Grey_Swatch",
"swatch_guid": "90869e86-cbc5-4024-9d08-fcdc41c64eb5",
"color": "Grey",
"item_name": "(Shirt_Smuggler)",
"item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Smuggler_Dark_Swatch",
"swatch_guid": "e821aa74-2d47-4388-bcfb-1893b8a83a52",
"color": "Dark",
"item_name": "(Shirt_Smuggler)",
"item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Smuggler_Cherry_Swatch",
"swatch_guid": "fed760b2-15e2-46b2-b1e3-cc3c8d780d5d",
"color": "Cherry",
"item_name": "(Shirt_Smuggler)",
"item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Smuggler_Yellow_Swatch",
"swatch_guid": "002b07bb-4256-44ce-8ca4-25510e70b6b5",
"color": "Yellow",
"item_name": "(Shirt_Smuggler)",
"item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 101,
"mask_guid": ""
}, },
{ {
"swatch_name": "SodaJerk_Green_Swatch", "swatch_name": "SodaJerk_Green_Swatch",
@@ -829,22 +406,13 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "SodaJerk_Mustard_Swatch", "swatch_name": "Scarecrow_Hat_Contest_Swatch",
"swatch_guid": "MxnAq_XOw0uTzQ8IIxqb_Q", "swatch_guid": "TZ3oQBp_3kWY4Vzscgv5Wg",
"color": "Mustard", "color": "Hat_Contest",
"item_name": "(Shirt_SodaJerk)", "item_name": "(Shirt_Scarecrow)",
"item_guid": "iai6P7dDTUq8S4hqQmY1YA", "item_guid": "1fcaf4fd-9012-407e-b691-fbc3ccf22421",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "xCpdHx1iM06Gg0tYoGv0DA"
},
{
"swatch_name": "SodaJerk_Blue_Swatch",
"swatch_guid": "XFCpIdeDXUqeYCIgh4fGtg",
"color": "Blue",
"item_name": "(Shirt_SodaJerk)",
"item_guid": "iai6P7dDTUq8S4hqQmY1YA",
"item_outfit_type": 101,
"mask_guid": ""
}, },
{ {
"swatch_name": "Hat_TutorialCap_2_Swatch", "swatch_name": "Hat_TutorialCap_2_Swatch",
@@ -856,13 +424,31 @@
"mask_guid": "GV8z2dlqGEqIAYaGrF1ZYQ" "mask_guid": "GV8z2dlqGEqIAYaGrF1ZYQ"
}, },
{ {
"swatch_name": "Hat_TutorialCap_3_Swatch", "swatch_name": "Hat_Baseball_ThreeColor_BlueRed_Swatch",
"swatch_guid": "wHJnbDOo8ESmUbQDr0k31w", "swatch_guid": "nZor-clxlkm8EO9Gco0Otg",
"color": "3", "color": "ThreeColor_BlueRed",
"item_name": "(Hat_TutorialCap)", "item_name": "(Hat_Baseball)",
"item_guid": "M9weaekvIUimiJSVJqZl-w", "item_guid": "14ef6b00-debf-4a85-9755-b4d37df496d3",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "GV8z2dlqGEqIAYaGrF1ZYQ" "mask_guid": "r_GLekt6mkWxXw8zPPR8Mg"
},
{
"swatch_name": "PirateCaptain_Hat_Black_Swatch",
"swatch_guid": "f9aaa7c5-59bc-486c-9a6e-27a8ac618f84",
"color": "Hat_Black",
"item_name": "(Neck_PirateCaptain)",
"item_guid": "38b38e24-d636-41c6-b80e-a285ae3106dc",
"item_outfit_type": 102,
"mask_guid": ""
},
{
"swatch_name": "Wizard_Purple_Swatch",
"swatch_guid": "d3KmqVuMQkCYK6sVTMOTOA",
"color": "Purple",
"item_name": "(Hat_Wizard)",
"item_guid": "01689baf-405e-4b38-9f09-b624e03865b8",
"item_outfit_type": 0,
"mask_guid": ""
}, },
{ {
"swatch_name": "BaseballHelmet_Red_Swatch", "swatch_name": "BaseballHelmet_Red_Swatch",
@@ -871,16 +457,7 @@
"item_name": "(Hat_BaseballHelmet)", "item_name": "(Hat_BaseballHelmet)",
"item_guid": "293e7ad3-01c9-43af-96e3-06c34145ff12", "item_guid": "293e7ad3-01c9-43af-96e3-06c34145ff12",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "FVdDQFOMQ06teQm2ANDPng"
},
{
"swatch_name": "BaseballHelmet_Green_Swatch",
"swatch_guid": "9967ce54-c2b7-437a-946e-11f8ee6d6fdd",
"color": "Green",
"item_name": "(Hat_BaseballHelmet)",
"item_guid": "293e7ad3-01c9-43af-96e3-06c34145ff12",
"item_outfit_type": 0,
"mask_guid": ""
}, },
{ {
"swatch_name": "TruckerOveralls_Mustard_Swatch", "swatch_name": "TruckerOveralls_Mustard_Swatch",
@@ -891,24 +468,6 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "TruckerOveralls_Red_Swatch",
"swatch_guid": "iokdlBfjuEuAwqIMOQwxdA",
"color": "Red",
"item_name": "(Shirt_TruckerOveralls)",
"item_guid": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "TruckerOveralls_Green_Swatch",
"swatch_guid": "EiGgN7uVukqoRE7UJcMLTw",
"color": "Green",
"item_name": "(Shirt_TruckerOveralls)",
"item_guid": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "PirateCaptain_Black_Swatch", "swatch_name": "PirateCaptain_Black_Swatch",
"swatch_guid": "d81d0b62-e052-46c2-9252-4bc5fb219f53", "swatch_guid": "d81d0b62-e052-46c2-9252-4bc5fb219f53",
@@ -919,21 +478,48 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "PirateCaptain_SRank_Swatch", "swatch_name": "BlazerGreyDark_Swatch",
"swatch_guid": "xGmnUGF-Ck-jP43r8tVVjg", "swatch_guid": "9374bf66-2ee5-493b-8439-efce4b201904",
"color": "SRank", "color": "GreyDark",
"item_name": "(Neck_PirateCaptain)", "item_name": "(Blazer)",
"item_guid": "38b38e24-d636-41c6-b80e-a285ae3106dc", "item_guid": "de0ac50d-2adb-4114-bd2e-68953b13d706",
"item_outfit_type": 102, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54"
}, },
{ {
"swatch_name": "PirateCaptain_Red_Swatch", "swatch_name": "MagicalGirl_Wrist_Red_Swatch",
"swatch_guid": "556141f7-6fbc-4065-9faa-8d8240a93d7f", "swatch_guid": "N9CBuYaMtUOrCks5UcOycg",
"color": "Red", "color": "Wrist_Red",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "ZtCqMnFTQUWv2h0MfZ4m6A"
},
{
"swatch_name": "PirateCaptain_Belt_Black_Swatch",
"swatch_guid": "f4ad4b9e-dd42-4711-9437-5beae51966f6",
"color": "Belt_Black",
"item_name": "(Neck_PirateCaptain)", "item_name": "(Neck_PirateCaptain)",
"item_guid": "38b38e24-d636-41c6-b80e-a285ae3106dc", "item_guid": "38b38e24-d636-41c6-b80e-a285ae3106dc",
"item_outfit_type": 102, "item_outfit_type": 102,
"mask_guid": "9b2102f0-e16b-464f-9f1c-f068de52bb89"
},
{
"swatch_name": "BowlingShirt_Flames_Red_Swatch",
"swatch_guid": "8xjU4GQ4OkaqGyad-ypwtw",
"color": "Shirt_Flames_Red",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "5RUG6i2Mx0-55IO-IlhUdQ"
},
{
"swatch_name": "BabydollDress_Black_Swatch",
"swatch_guid": "e13cf33d-7588-4e2e-955e-f0a29d6380b7",
"color": "Black",
"item_name": "(BabydollDress)",
"item_guid": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf",
"item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
@@ -952,17 +538,53 @@
"item_name": "(Wrist_FlapperGlove)", "item_name": "(Wrist_FlapperGlove)",
"item_guid": "473c672d-2b79-48a2-a49d-fd68160c5bde", "item_guid": "473c672d-2b79-48a2-a49d-fd68160c5bde",
"item_outfit_type": 200, "item_outfit_type": 200,
"mask_guid": "0e42108e-3b08-49b4-86c0-7984719e8380"
},
{
"swatch_name": "ButtonTop_Tan_Swatch",
"swatch_guid": "49f5864f-9d40-497c-88c8-e87f64d41d74",
"color": "Tan",
"item_name": "(ButtonTop)",
"item_guid": "9c8fc7f0-8f99-4aad-a34f-8d979f6ae352",
"item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "FlapperGlove_Gold_Swatch", "swatch_name": "Windbreaker_Blue_Swatch",
"swatch_guid": "2cf2b644-3fa6-449c-9fe8-d0ab917f3106", "swatch_guid": "EbVDqEJQOk6j0UvX3t-8rQ",
"color": "Gold", "color": "Blue",
"item_name": "(Wrist_FlapperGlove)", "item_name": "(Wrist_80sWindbreaker)",
"item_guid": "473c672d-2b79-48a2-a49d-fd68160c5bde", "item_guid": "52VsI_lKlkSUAOJlfDnyrQ",
"item_outfit_type": 200, "item_outfit_type": 200,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Aviator_Goggles_Gray_Swatch",
"swatch_guid": "23428ca9-be75-4146-b400-df01d1dadac9",
"color": "s_Gray",
"item_name": "(Eye_AviatorGoggles)",
"item_guid": "47629ed1-5378-427c-ad61-4b44d2fe90cc",
"item_outfit_type": 10,
"mask_guid": "556f876a-70da-4104-b792-fca8ad67be0c"
},
{
"swatch_name": "SafariTorso_Green_Swatch",
"swatch_guid": "49bbd11a-e5c9-47ff-b04d-72eb321e8a57",
"color": "Torso_Green",
"item_name": "(Shirt_Safari)",
"item_guid": "1041d745-c5e9-4218-a3e1-198f5c9c418c",
"item_outfit_type": 101,
"mask_guid": "84c016ad-133f-45e3-a412-cac35612540b"
},
{
"swatch_name": "Scallywag_Hat_Blue_Swatch",
"swatch_guid": "53dd9e44-7096-438e-a7c3-11d7c307426f",
"color": "Hat_Blue",
"item_name": "(Shirt_Scallywag)",
"item_guid": "664485e9-a159-4fcb-b32c-2b114b4a1218",
"item_outfit_type": 101,
"mask_guid": "a4903da3-249d-4f0a-a746-b547fda371ff"
},
{ {
"swatch_name": "BusinessTie_Valentine_Swatch", "swatch_name": "BusinessTie_Valentine_Swatch",
"swatch_guid": "31jelNWFyk6FlWt4HyPAlw", "swatch_guid": "31jelNWFyk6FlWt4HyPAlw",
@@ -973,12 +595,21 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "BusinessTie_CandyCane_Swatch", "swatch_name": "Astronaut_Helmet_Orange_Swatch",
"swatch_guid": "W4a144so90eRzCojUnDowA", "swatch_guid": "JFalLDUvm0aMm3wh1UVfVg",
"color": "CandyCane", "color": "t_Orange",
"item_name": "(BusinessTie)", "item_name": "(Hat_AstronautHelmet)",
"item_guid": "c6c08eb5-381a-4193-9722-80da95d62abe", "item_guid": "f218e2de-46fd-45b9-9e0e-670b6bb905e7",
"item_outfit_type": 100, "item_outfit_type": 0,
"mask_guid": "iQjkS1xYP0S63ltvKpkz5g"
},
{
"swatch_name": "ArcherGray_Quiver_Swatch",
"swatch_guid": "imDGL6dhrka-KliYlfpdgw",
"color": "Gray_Quiver",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
@@ -997,61 +628,43 @@
"item_name": "(Hat_HeadBackBow)", "item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew", "item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "3DdSzSqV1UG5AtPr8ro3Kg"
},
{
"swatch_name": "Headphones_EmissiveRed_Swatch",
"swatch_guid": "I1J-xY4YNU2dB_od4P_7Dg",
"color": "EmissiveRed",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "EquestrianJacket_Gray_Swatch",
"swatch_guid": "4828b50c-95b6-466a-bb25-514891d78202",
"color": "Gray",
"item_name": "(EquestrianJacket)",
"item_guid": "896c2491-2f96-4986-9cbd-b3b31ef5d8c5",
"item_outfit_type": 101,
"mask_guid": "d344b8cc-85a8-4ace-9f92-38c84f396e99"
},
{
"swatch_name": "Hoodie_Skeleton_NoGlow_Swatch",
"swatch_guid": "s6DQxJ3gY0CG8L7JmcmRTQ",
"color": "Skeleton_NoGlow",
"item_name": "(Shirt_Hoodie)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA",
"item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "HeadBackBow_Red_Swatch", "swatch_name": "AnglerHat_Blue_Swatch",
"swatch_guid": "ZNKGkou_gU-1cxBFy7pDKw", "swatch_guid": "d7f0ffd3-ccf9-465e-8afe-4d7efb78dd2c",
"color": "Red", "color": "Hat_Blue",
"item_name": "(Hat_HeadBackBow)", "item_name": "(Hat_Angler)",
"item_guid": "JTrfBXhIT02o2-ssP94zew", "item_guid": "03f8c394-28fa-4087-978b-8d108f0bd969",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "efeb07ab-6253-436d-bb66-991ddad72e58"
},
{
"swatch_name": "HeadBackBow_White_Swatch",
"swatch_guid": "-Iqmonry4EKoqAULCKzRPA",
"color": "White",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "HeadBackBow_SolidBlack_Swatch",
"swatch_guid": "f_c50saStkaGJuhLjJ9lfA",
"color": "SolidBlack",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "HeadBackBow_Hearts_Swatch",
"swatch_guid": "D-SngK7hhkOpoZ2aMWv9EA",
"color": "Hearts",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "HeadBackBow_SolidBlue_Swatch",
"swatch_guid": "8iZO162KNUiMC8l_z3OC_A",
"color": "SolidBlue",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "HeadBackBow_SolidPink_Swatch",
"swatch_guid": "FUP4iRnoc0ikNmjrFD06lg",
"color": "SolidPink",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": ""
}, },
{ {
"swatch_name": "FurCoat_Black_Swatch", "swatch_name": "FurCoat_Black_Swatch",
@@ -1063,13 +676,22 @@
"mask_guid": "2c0d912d-c184-4eab-8ee4-73b90ba7b0a6" "mask_guid": "2c0d912d-c184-4eab-8ee4-73b90ba7b0a6"
}, },
{ {
"swatch_name": "FurCoat_White_Swatch", "swatch_name": "Scuba_eye_Contest_Swatch",
"swatch_guid": "643b5f2d-0666-4869-b699-03742de34d16", "swatch_guid": "ZSOuWuKVCkqaIKc5d23G8Q",
"color": "White", "color": "eye_Contest",
"item_name": "(FurCoat)", "item_name": "(Shirt_Scuba)",
"item_guid": "77c37481-a185-47ff-af55-65f9fdfcf9e5", "item_guid": "57552652-d902-43b7-a68e-5c7da87582bc",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "2c0d912d-c184-4eab-8ee4-73b90ba7b0a6" "mask_guid": ""
},
{
"swatch_name": "BowlingShirt_OneStripe_Green_Swatch",
"swatch_guid": "kLyM-6otSUy2ql5SEmnF9Q",
"color": "Shirt_OneStripe_Green",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "b-IM3eYMJUa40395O5XeLg"
}, },
{ {
"swatch_name": "FlapperHeadband_Gold_Swatch", "swatch_name": "FlapperHeadband_Gold_Swatch",
@@ -1078,16 +700,16 @@
"item_name": "(Hat_FlapperHeadband)", "item_name": "(Hat_FlapperHeadband)",
"item_guid": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", "item_guid": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "d9be3a98-9da9-419e-9dce-fe58a3ad9c4e"
}, },
{ {
"swatch_name": "FlapperHeadband_Black_Swatch", "swatch_name": "Golfer_Shirt_Blue_Swatch",
"swatch_guid": "565c17e0-496d-45bd-a2c3-564e118c06cc", "swatch_guid": "96e2b29e-e553-40f1-9eb6-8a9f7e64da39",
"color": "Black", "color": "Shirt_Blue",
"item_name": "(Hat_FlapperHeadband)", "item_name": "(Hat_Golfer)",
"item_guid": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c", "item_guid": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "6ce21c65-f4e9-41ac-be94-8eb1a08d5c76"
}, },
{ {
"swatch_name": "Wrist_VampireHunter_Red_Swatch", "swatch_name": "Wrist_VampireHunter_Red_Swatch",
@@ -1099,22 +721,31 @@
"mask_guid": "N5wN1l0tYEau5np-9PoIiA" "mask_guid": "N5wN1l0tYEau5np-9PoIiA"
}, },
{ {
"swatch_name": "Wrist_VampireHunter_Black_Swatch", "swatch_name": "Hibiscus_White_Swatch",
"swatch_guid": "Vxq6v55a_kGPODzwB5MC1g", "swatch_guid": "ce465d53-3807-423f-baa0-bdba2a75ceb3",
"color": "Black", "color": "White",
"item_name": "(Wrist_VampireHunter)", "item_name": "(Hat_Hibiscus)",
"item_guid": "_OWVy3z6iU-M3-zbQgSLig", "item_guid": "a61e86cd-f7e7-4ac7-a6e8-df269bc5cb01",
"item_outfit_type": 200, "item_outfit_type": 0,
"mask_guid": "N5wN1l0tYEau5np-9PoIiA" "mask_guid": ""
}, },
{ {
"swatch_name": "Wrist_VampireHunter_Leather_Swatch", "swatch_name": "FlapperHeadband_Black_Swatch",
"swatch_guid": "D0tP58wC30mJ00vNxw8hqA", "swatch_guid": "565c17e0-496d-45bd-a2c3-564e118c06cc",
"color": "Leather", "color": "Black",
"item_name": "(Wrist_VampireHunter)", "item_name": "(Hat_FlapperHeadband)",
"item_guid": "_OWVy3z6iU-M3-zbQgSLig", "item_guid": "18feb3c2-049f-4f91-9f4e-6b5fcb4ab70c",
"item_outfit_type": 200, "item_outfit_type": 0,
"mask_guid": "N5wN1l0tYEau5np-9PoIiA" "mask_guid": "d9be3a98-9da9-419e-9dce-fe58a3ad9c4e"
},
{
"swatch_name": "Shirt_TutorialGown3_Black_Swatch",
"swatch_guid": "GK3yolCNlEm3B_5DtHkE8w",
"color": "Black",
"item_name": "(Shirt_TutorialGown_3)",
"item_guid": "0yFzgVESokC3gFd3hrS8bg",
"item_outfit_type": 101,
"mask_guid": "yKb2A8M_m0GQDhFWCRUHTQ"
}, },
{ {
"swatch_name": "PeaCoat_Purple_Swatch", "swatch_name": "PeaCoat_Purple_Swatch",
@@ -1123,32 +754,86 @@
"item_name": "(Shirt_Peacoat)", "item_name": "(Shirt_Peacoat)",
"item_guid": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", "item_guid": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "AZnPWCN41kmNwFwDqWGFWQ"
},
{
"swatch_name": "Winter_Scarf_Tree_Green_Swatch",
"swatch_guid": "-tDd0gdxQkuvL2id10AweQ",
"color": "Scarf_Tree_Green",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "PeaCoat_Navy_Swatch", "swatch_name": "Headphones_TwoRed_Swatch",
"swatch_guid": "fvoaMN4M10WS6jiA6XlOJQ", "swatch_guid": "hf0gkFVS8ESvceRsC3LAXQ",
"color": "Navy", "color": "TwoRed",
"item_name": "(Shirt_Peacoat)", "item_name": "(Hat_Headphones)",
"item_guid": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", "item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "Vest_HoundsTooth_Black_Swatch",
"swatch_guid": "m4W5D0Oa9kaWa-8I96YYzA",
"color": "HoundsTooth_Black",
"item_name": "(Shirt_Vest)",
"item_guid": "3d9d52cb-6a3d-436b-90e7-e76f238329ee",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "PeaCoat_Tan_Swatch", "swatch_name": "Headphones_EmissiveBlue_Swatch",
"swatch_guid": "PwsLxCnzOkSW-RGz3tNb1w", "swatch_guid": "2sf-FU4WckGBDWUkVz4z1g",
"color": "Tan", "color": "EmissiveBlue",
"item_name": "(Shirt_Peacoat)", "item_name": "(Hat_Headphones)",
"item_guid": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", "item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "Headphones_Purple_Swatch",
"swatch_guid": "a5nUvJZgTEGBUvdHx0Y6Ng",
"color": "Purple",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "Winter_Scarf_Red_Swatch",
"swatch_guid": "2lD-mjl3pk2wWv24RY2tbA",
"color": "Scarf_Red",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": "dUu7b-JFOky9c2lIVCNmcg"
},
{
"swatch_name": "Headphones_EmissiveOrange_Swatch",
"swatch_guid": "d9mwowRelE2lDKEA0UQMZQ",
"color": "EmissiveOrange",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "Scuba_eye_Blue_Swatch",
"swatch_guid": "DRS8xu3AqUOy2L0RKF3f7g",
"color": "eye_Blue",
"item_name": "(Shirt_Scuba)",
"item_guid": "57552652-d902-43b7-a68e-5c7da87582bc",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "PeaCoat_Red_Swatch", "swatch_name": "ArcherGray_Swatch",
"swatch_guid": "jXBi28cRV0CEQgKIrpD0_A", "swatch_guid": "61VZkFjB402brzWm4UsftA",
"color": "Red", "color": "Gray",
"item_name": "(Shirt_Peacoat)", "item_name": "(Shirt_Archer)",
"item_guid": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5", "item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
@@ -1171,22 +856,31 @@
"mask_guid": "1e3195ab-f7f6-41f4-a5ec-378d09ecdf69" "mask_guid": "1e3195ab-f7f6-41f4-a5ec-378d09ecdf69"
}, },
{ {
"swatch_name": "Hat_CatEars_White_Swatch", "swatch_name": "Rainhat_Hat_Red_Swatch",
"swatch_guid": "c1e430c5-fd8d-4032-9e3f-8d87cc8784da", "swatch_guid": "v3af60p1Q02NpdCqIRO9lw",
"color": "White", "color": "Hat_Red",
"item_name": "(Hat_CatEars)", "item_name": "(Hat_Rainhat)",
"item_guid": "3d5184e1-0201-4476-bf4b-4eb28190de62", "item_guid": "2bd53190-46f8-4956-9388-87c5a04efc4f",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "1e3195ab-f7f6-41f4-a5ec-378d09ecdf69" "mask_guid": "1hVOI7IuY0mKvtr2PZXLww"
}, },
{ {
"swatch_name": "Hat_CatEars_Gold_Swatch", "swatch_name": "WitchHunter_Green_Swatch",
"swatch_guid": "37f8378b-b98c-477a-b8c9-9340772800b5", "swatch_guid": "64545dcc-a8eb-4c69-a539-78ee73a2d327",
"color": "Gold", "color": "Green",
"item_name": "(Hat_CatEars)", "item_name": "(Shirt_WitchHunter)",
"item_guid": "3d5184e1-0201-4476-bf4b-4eb28190de62", "item_guid": "418db0e0-c7af-40c2-9cac-d461ce41e210",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Winter_Gloves_Red_Swatch",
"swatch_guid": "SWvJWvMDpE-it5PFoPgERg",
"color": "Gloves_Red",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "1e3195ab-f7f6-41f4-a5ec-378d09ecdf69" "mask_guid": ""
}, },
{ {
"swatch_name": "TruckerJacket_White_Swatch", "swatch_name": "TruckerJacket_White_Swatch",
@@ -1197,42 +891,6 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "TruckerJacket_Tan_Swatch",
"swatch_guid": "o2WfRyjJy0CKoUg8YIxNjQ",
"color": "Tan",
"item_name": "(Shirt_TruckerJacket)",
"item_guid": "8b065e2a-b106-4f11-b415-4c47ce87993e",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "TruckerJacket_RebelRose_Swatch",
"swatch_guid": "4Eck_dNL1UKlIZWsA1YppQ",
"color": "RebelRose",
"item_name": "(Shirt_TruckerJacket)",
"item_guid": "8b065e2a-b106-4f11-b415-4c47ce87993e",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "TruckerJacket_Black_Swatch",
"swatch_guid": "P9bBSL0YLEyjN8PxMfgbKQ",
"color": "Black",
"item_name": "(Shirt_TruckerJacket)",
"item_guid": "8b065e2a-b106-4f11-b415-4c47ce87993e",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "TruckerJacket_Olive_Swatch",
"swatch_guid": "bgRKH1wfqkWVHcuSnFYnlQ",
"color": "Olive",
"item_name": "(Shirt_TruckerJacket)",
"item_guid": "8b065e2a-b106-4f11-b415-4c47ce87993e",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "80sSpandex_White_Swatch", "swatch_name": "80sSpandex_White_Swatch",
"swatch_guid": "zu0Jk24WTkan6XWSwGU1Uw", "swatch_guid": "zu0Jk24WTkan6XWSwGU1Uw",
@@ -1240,43 +898,16 @@
"item_name": "(Shirt_80sSpandex)", "item_name": "(Shirt_80sSpandex)",
"item_guid": "17236d76-e3b6-42f6-9179-6fcf0479436f", "item_guid": "17236d76-e3b6-42f6-9179-6fcf0479436f",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "B-sIpWNOfEigMeQF-WVR1g"
}, },
{ {
"swatch_name": "80sSpandex_Red_Swatch", "swatch_name": "Scout_Sash_Blue_Swatch",
"swatch_guid": "6t2rCb6WWEW6pJbcjsAFCw", "swatch_guid": "67490e3b-0f9d-4bf8-84fb-54957b981c8c",
"color": "Red", "color": "h_Blue",
"item_name": "(Shirt_80sSpandex)", "item_name": "(Neck_ScoutSash)",
"item_guid": "17236d76-e3b6-42f6-9179-6fcf0479436f", "item_guid": "bf9dc196-5727-4752-8126-7ad5fcd96d55",
"item_outfit_type": 101, "item_outfit_type": 100,
"mask_guid": "" "mask_guid": "66cc536d-8cb5-4262-b7f0-49a33b97ea10"
},
{
"swatch_name": "80sSpandex_Green_Swatch",
"swatch_guid": "3frH6KxZ9kGUYkjeCDpylQ",
"color": "Green",
"item_name": "(Shirt_80sSpandex)",
"item_guid": "17236d76-e3b6-42f6-9179-6fcf0479436f",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "80sSpandex_Blue_Swatch",
"swatch_guid": "Lf4FjROSAEqXedFxpXFQfg",
"color": "Blue",
"item_name": "(Shirt_80sSpandex)",
"item_guid": "17236d76-e3b6-42f6-9179-6fcf0479436f",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "80sSpandex_Yellow_Swatch",
"swatch_guid": "i1XbjBqTu0CILIuJClWimA",
"color": "Yellow",
"item_name": "(Shirt_80sSpandex)",
"item_guid": "17236d76-e3b6-42f6-9179-6fcf0479436f",
"item_outfit_type": 101,
"mask_guid": ""
}, },
{ {
"swatch_name": "WaitressApron_Red_Swatch", "swatch_name": "WaitressApron_Red_Swatch",
@@ -1288,22 +919,31 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "WaitressApron_Mustard_Swatch", "swatch_name": "SpaceMarine_Pink_Swatch",
"swatch_guid": "b7oobDcDtE-69fhM-5g5AQ", "swatch_guid": "a36d252b-16ac-4a82-bece-de145147e7d9",
"color": "Mustard", "color": "Pink",
"item_name": "(Belt_WaitressApron)", "item_name": "(Shirt_SpaceMarine)",
"item_guid": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", "item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 102, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "7ac81d60-05b8-4a85-982c-f69213a93818"
}, },
{ {
"swatch_name": "WaitressApron_Green_Swatch", "swatch_name": "Mobster_Hat_Brown_Swatch",
"swatch_guid": "Blev_VFjRUysY_nymGWAVg", "swatch_guid": "1421caa9-9a0d-42c2-b847-1500ba2e483a",
"color": "Green", "color": "Hat_Brown",
"item_name": "(Belt_WaitressApron)", "item_name": "(Wrist_Mobster)",
"item_guid": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7", "item_guid": "08d56627-4500-4724-8692-a23c96ddb3ec",
"item_outfit_type": 102, "item_outfit_type": 200,
"mask_guid": "" "mask_guid": "d759c88d-a159-4c26-86dd-b054bb05b3ed"
},
{
"swatch_name": "ElectricianTorso_Gray_Swatch",
"swatch_guid": "b995b419-1897-4c1d-98be-83d551f7ba71",
"color": "Torso_Gray",
"item_name": "(Hat_Electrician)",
"item_guid": "34654303-9760-447f-8614-1c26506db994",
"item_outfit_type": 0,
"mask_guid": "daa38a31-fb7b-4e26-a2f5-728d6dcec81a"
}, },
{ {
"swatch_name": "Shirt_MartyRecFly_Hipster_Swatch", "swatch_name": "Shirt_MartyRecFly_Hipster_Swatch",
@@ -1314,6 +954,15 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Smuggler_SRank_Swatch",
"swatch_guid": "bzYdPb8GWUS-CgT4wrziIA",
"color": "SRank",
"item_name": "(Shirt_Smuggler)",
"item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 101,
"mask_guid": "970a8c7e-d091-4719-bb9c-e3be8d46ee68"
},
{ {
"swatch_name": "Hat_SkiBuddy_White_Swatch", "swatch_name": "Hat_SkiBuddy_White_Swatch",
"swatch_guid": "KwCEye0nsESFs-yj4h9sjQ", "swatch_guid": "KwCEye0nsESFs-yj4h9sjQ",
@@ -1323,6 +972,15 @@
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "c_t9XDbn-Eqml1O-wC5zJQ" "mask_guid": "c_t9XDbn-Eqml1O-wC5zJQ"
}, },
{
"swatch_name": "PeaCoat_Navy_Swatch",
"swatch_guid": "fvoaMN4M10WS6jiA6XlOJQ",
"color": "Navy",
"item_name": "(Shirt_Peacoat)",
"item_guid": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5",
"item_outfit_type": 101,
"mask_guid": "AZnPWCN41kmNwFwDqWGFWQ"
},
{ {
"swatch_name": "Wrist_Grillmaster_Blue_Swatch", "swatch_name": "Wrist_Grillmaster_Blue_Swatch",
"swatch_guid": "90qQe0AR3kOou0MsuoTmPA", "swatch_guid": "90qQe0AR3kOou0MsuoTmPA",
@@ -1333,13 +991,22 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Wrist_Grillmaster_Black_Swatch", "swatch_name": "FootballHelmet_Orange_Swatch",
"swatch_guid": "hZKPr4sg10SRj9DLuEgOrw", "swatch_guid": "d8d490aa-7088-4163-9305-1441367f490c",
"color": "Black", "color": "Helmet_Orange",
"item_name": "(Wrist_Grillmaster)", "item_name": "(Hat_FootBall)",
"item_guid": "q7pSsRMNHkqEJmYHuhhG6g", "item_guid": "5a38f864-7d1c-4989-8558-40b9668f78ef",
"item_outfit_type": 200, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "7660e11a-7ef0-43d3-bc02-76b3a4aab064"
},
{
"swatch_name": "HeadBackBow_Red_Swatch",
"swatch_guid": "ZNKGkou_gU-1cxBFy7pDKw",
"color": "Red",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": "3DdSzSqV1UG5AtPr8ro3Kg"
}, },
{ {
"swatch_name": "Scientist_Blue_Swatch", "swatch_name": "Scientist_Blue_Swatch",
@@ -1360,13 +1027,112 @@
"mask_guid": "2d7361d2-31a2-404b-90da-10e89114e1f2" "mask_guid": "2d7361d2-31a2-404b-90da-10e89114e1f2"
}, },
{ {
"swatch_name": "RedFlowerDress_Yellow_Swatch", "swatch_name": "Aviator_Jacket_Tan_Swatch",
"swatch_guid": "e98c3f98-7844-4d6e-a21c-05027033c18c", "swatch_guid": "bea795cc-aabd-4229-84dd-49ab4f91a445",
"color": "Yellow", "color": "Jacket_Tan",
"item_name": "(RedFlowerDress)", "item_name": "(Wrist_Aviator)",
"item_guid": "515dfc36-7f7b-47bc-abee-380d68f41be6", "item_guid": "125a3a62-1005-47dc-9fd8-ee09ea803e9f",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "Mobster_Hat_Purple_Swatch",
"swatch_guid": "PZx7Iae0pkiDaxT9t3-vgg",
"color": "Hat_Purple",
"item_name": "(Wrist_Mobster)",
"item_guid": "08d56627-4500-4724-8692-a23c96ddb3ec",
"item_outfit_type": 200,
"mask_guid": "d759c88d-a159-4c26-86dd-b054bb05b3ed"
},
{
"swatch_name": "LumberJack_Green_Swatch",
"swatch_guid": "7amHTgEv-UOJSGl0ofo4mw",
"color": "Green",
"item_name": "(Shirt_Lumberjack)",
"item_guid": "350c5c1e-318b-4233-a7e5-1f49909d8e65",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "2d7361d2-31a2-404b-90da-10e89114e1f2" "mask_guid": ""
},
{
"swatch_name": "Harness_Hat_Yellow_Swatch",
"swatch_guid": "L2iFQcL170OajqfW7t6sVw",
"color": "Hat_Yellow",
"item_name": "(Shirt_Harness)",
"item_guid": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af",
"item_outfit_type": 101,
"mask_guid": "PZHohDVX0U2frX13N4KYDQ"
},
{
"swatch_name": "BowlingShirt_CenterStripe_Black_Swatch",
"swatch_guid": "Gtq8yRlt1UeFW8U-sc7FiA",
"color": "Shirt_CenterStripe_Black",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "IWg9wuda4kaCilJk8JQpTQ"
},
{
"swatch_name": "Punk_WristPink_Swatch",
"swatch_guid": "f3b2330a-b981-4c7e-902f-1f57382f2388",
"color": "WristPink",
"item_name": "(Wrist_Punk)",
"item_guid": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7",
"item_outfit_type": 200,
"mask_guid": "dd7472ed-bdcb-4811-81ba-5ed682fb7675"
},
{
"swatch_name": "MagicalGirl_Wrist_Orange_Swatch",
"swatch_guid": "xpkEQ6yjhU-WfFPSjvPdHg",
"color": "Wrist_Orange",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "ZtCqMnFTQUWv2h0MfZ4m6A"
},
{
"swatch_name": "FighterPilot_Black_Swatch",
"swatch_guid": "8c74ee27-3d56-401b-8a8c-7868a80770e8",
"color": "Black",
"item_name": "(Wrist_FighterPilot)",
"item_guid": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8",
"item_outfit_type": 200,
"mask_guid": "435eba0d-d8f9-43cc-91aa-2959d63c6fa7"
},
{
"swatch_name": "MagicalGirl_Hat_Blue_Swatch",
"swatch_guid": "lhfOKGDAgEKWucerCmH7Fg",
"color": "Hat_Blue",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "mG60EPIQIkaJNgOkonVmZg"
},
{
"swatch_name": "Windbreaker_Yellow_Swatch",
"swatch_guid": "C3ll7uPfUkeZ1QLX80_BpQ",
"color": "Yellow",
"item_name": "(Wrist_80sWindbreaker)",
"item_guid": "52VsI_lKlkSUAOJlfDnyrQ",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "80sSpandex_Red_Swatch",
"swatch_guid": "6t2rCb6WWEW6pJbcjsAFCw",
"color": "Red",
"item_name": "(Shirt_80sSpandex)",
"item_guid": "17236d76-e3b6-42f6-9179-6fcf0479436f",
"item_outfit_type": 101,
"mask_guid": "B-sIpWNOfEigMeQF-WVR1g"
},
{
"swatch_name": "Hat_Baseball_Logo_Ramen_Swatch",
"swatch_guid": "cpTKHXjj0kulivZCKVQqiA",
"color": "Logo_Ramen",
"item_name": "(Hat_Baseball)",
"item_guid": "14ef6b00-debf-4a85-9755-b4d37df496d3",
"item_outfit_type": 0,
"mask_guid": ""
}, },
{ {
"swatch_name": "YellowBlueDress_White_Swatch", "swatch_name": "YellowBlueDress_White_Swatch",
@@ -1378,13 +1144,13 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "YellowBlueDress_Purple_Swatch", "swatch_name": "Headphones_Orange_Swatch",
"swatch_guid": "830be2fa-60a5-48cc-931f-34b670eae4bd", "swatch_guid": "JWBK5v78Y0uRVEXlD-Qk6Q",
"color": "Purple", "color": "Orange",
"item_name": "(YellowBlueDress)", "item_name": "(Hat_Headphones)",
"item_guid": "2296ed0d-df56-4d46-b33a-aae9230a47fc", "item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 101, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
}, },
{ {
"swatch_name": "TennisSkirt_Yellow_Swatch", "swatch_name": "TennisSkirt_Yellow_Swatch",
@@ -1396,13 +1162,40 @@
"mask_guid": "b75ef67d-00c3-4ac1-9b72-212032460294" "mask_guid": "b75ef67d-00c3-4ac1-9b72-212032460294"
}, },
{ {
"swatch_name": "TennisSkirt_Red_Swatch", "swatch_name": "MagicalGirl_Red_Swatch",
"swatch_guid": "758752bd-db2f-43d2-b580-55b3e1efffd5", "swatch_guid": "FE7_gQNBt0CXzMN6di5HWA",
"color": "Red", "color": "Red",
"item_name": "(TennisSkirt)", "item_name": "(Wrist_MagicalGirl)",
"item_guid": "21caa68e-c3fa-474c-af5e-af1e742b7a60", "item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "BowlingShirt_Flames_Green_Swatch",
"swatch_guid": "M4765UHzsUeNTwH-Pcxp1g",
"color": "Shirt_Flames_Green",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "b75ef67d-00c3-4ac1-9b72-212032460294" "mask_guid": "5RUG6i2Mx0-55IO-IlhUdQ"
},
{
"swatch_name": "Smuggler_Grey_Swatch",
"swatch_guid": "90869e86-cbc5-4024-9d08-fcdc41c64eb5",
"color": "Grey",
"item_name": "(Shirt_Smuggler)",
"item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 101,
"mask_guid": "970a8c7e-d091-4719-bb9c-e3be8d46ee68"
},
{
"swatch_name": "ArcherBlack_Swatch",
"swatch_guid": "9ad37173-e640-4b9e-a474-716d482b73d2",
"color": "Black",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
}, },
{ {
"swatch_name": "LaserTag_Cyan_Swatch", "swatch_name": "LaserTag_Cyan_Swatch",
@@ -1414,22 +1207,22 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "LaserTag_Green_Swatch", "swatch_name": "BlazerDark_Swatch",
"swatch_guid": "2Z_FxSPiyUODtlFZWF9DFQ", "swatch_guid": "05ac07e1-67f0-486c-abf5-a62866475abb",
"color": "Green", "color": "Dark",
"item_name": "(Hat_LaserTag)", "item_name": "(Blazer)",
"item_guid": "123de433-a005-4994-8a11-d2d84ad66b11", "item_guid": "de0ac50d-2adb-4114-bd2e-68953b13d706",
"item_outfit_type": 0, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54"
}, },
{ {
"swatch_name": "LaserTag_Orange_Swatch", "swatch_name": "Letterman_Wrist_Purple_Swatch",
"swatch_guid": "jkuw9EhCpEKn-2lKJXWd3Q", "swatch_guid": "MS7EPNLJEEyMLC6_yHQYtA",
"color": "Orange", "color": "Wrist_Purple",
"item_name": "(Hat_LaserTag)", "item_name": "(Wrist_Letterman)",
"item_guid": "123de433-a005-4994-8a11-d2d84ad66b11", "item_guid": "2dFMy4zc7ky_Fd5fzfgInw",
"item_outfit_type": 0, "item_outfit_type": 200,
"mask_guid": "" "mask_guid": "4EHIyZkOOEqnM-PuD_a5xw"
}, },
{ {
"swatch_name": "Letterman_Red_Swatch", "swatch_name": "Letterman_Red_Swatch",
@@ -1440,33 +1233,6 @@
"item_outfit_type": 200, "item_outfit_type": 200,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Letterman_Purple_Swatch",
"swatch_guid": "iufpnteVCEiiZu_m-yPovQ",
"color": "Purple",
"item_name": "(Wrist_Letterman)",
"item_guid": "2dFMy4zc7ky_Fd5fzfgInw",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "Letterman_Black_Swatch",
"swatch_guid": "Z1F9rjKqCE-Hd2-Ip5g3gA",
"color": "Black",
"item_name": "(Wrist_Letterman)",
"item_guid": "2dFMy4zc7ky_Fd5fzfgInw",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "Letterman_Green_Swatch",
"swatch_guid": "apSQiQ1eO0mEzRsbugeu9w",
"color": "Green",
"item_name": "(Wrist_Letterman)",
"item_guid": "2dFMy4zc7ky_Fd5fzfgInw",
"item_outfit_type": 200,
"mask_guid": ""
},
{ {
"swatch_name": "TrackSuit_Pink_Swatch", "swatch_name": "TrackSuit_Pink_Swatch",
"swatch_guid": "026ca50e-3a81-43cf-87f5-950687b7bbdd", "swatch_guid": "026ca50e-3a81-43cf-87f5-950687b7bbdd",
@@ -1476,6 +1242,15 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "adae82eb-a4b9-4541-acf2-76cea839a593" "mask_guid": "adae82eb-a4b9-4541-acf2-76cea839a593"
}, },
{
"swatch_name": "Witch_Contest_Hat_Swatch",
"swatch_guid": "PcUZrdKXhk2XHa5Gy0YnnA",
"color": "Contest_Hat",
"item_name": "(Shirt_Witch)",
"item_guid": "7LSm-xCYakmgcAr--Fn-JA",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "QuiltedCoat_Orange_Swatch", "swatch_name": "QuiltedCoat_Orange_Swatch",
"swatch_guid": "-HYrY2A5Tk2UWMi4_hmYbg", "swatch_guid": "-HYrY2A5Tk2UWMi4_hmYbg",
@@ -1486,21 +1261,48 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "QuiltedCoat_Green_Swatch", "swatch_name": "CowboyWhite_Swatch",
"swatch_guid": "QBglgj80zkuvUkk55Y5Feg", "swatch_guid": "teVKVbVEsEyqOSuRdqHQzA",
"color": "Green", "color": "White",
"item_name": "(Shirt_Quiltedcoat)", "item_name": "(Hat_Cowboy)",
"item_guid": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", "item_guid": "ad0bdec5-f767-42e7-99e0-221473464a85",
"item_outfit_type": 101, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "54778d93-2cff-48db-b25b-65c08b0e5be8"
}, },
{ {
"swatch_name": "QuiltedCoat_Plum_Swatch", "swatch_name": "Aviator_Hat_Tan_Swatch",
"swatch_guid": "IjRMopsYlUqSE6327I9TIQ", "swatch_guid": "c6cf096c-f0cb-4500-80a0-bc2916b5a426",
"color": "Plum", "color": "Hat_Tan",
"item_name": "(Shirt_Quiltedcoat)", "item_name": "(Wrist_Aviator)",
"item_guid": "0ab5790f-a537-4306-ba66-5eb87c02ec7d", "item_guid": "125a3a62-1005-47dc-9fd8-ee09ea803e9f",
"item_outfit_type": 200,
"mask_guid": "ff5ae41e-e446-4a96-8c11-49d85556fa81"
},
{
"swatch_name": "Beekeeper_Shirt_Lavender_Swatch",
"swatch_guid": "lBe3yxuE40eBmTrnEHBqQQ",
"color": "Shirt_Lavender",
"item_name": "(Shirt_Beekeeper)",
"item_guid": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "afd88a79-7f03-40f6-bcb4-c247dd86edc4"
},
{
"swatch_name": "PeaCoat_Tan_Swatch",
"swatch_guid": "PwsLxCnzOkSW-RGz3tNb1w",
"color": "Tan",
"item_name": "(Shirt_Peacoat)",
"item_guid": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5",
"item_outfit_type": 101,
"mask_guid": "AZnPWCN41kmNwFwDqWGFWQ"
},
{
"swatch_name": "Baseball_Green_Swatch",
"swatch_guid": "e5de2e45-207c-46af-9a5e-eb01a0c621fe",
"color": "Green",
"item_name": "(Hat_Baseball)",
"item_guid": "14ef6b00-debf-4a85-9755-b4d37df496d3",
"item_outfit_type": 0,
"mask_guid": "" "mask_guid": ""
}, },
{ {
@@ -1512,6 +1314,15 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "HQQWccdyakaEmntPhvC6CQ" "mask_guid": "HQQWccdyakaEmntPhvC6CQ"
}, },
{
"swatch_name": "BowlingShirt_Diamonds_Purple_Swatch",
"swatch_guid": "yrQCLXiqRUSwnuQRYX2Rsw",
"color": "Shirt_Diamonds_Purple",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "VO_3cgqQqUOTz30TVUxrFA"
},
{ {
"swatch_name": "Dodgeball_Gray_Swatch", "swatch_name": "Dodgeball_Gray_Swatch",
"swatch_guid": "0ba92db2-690a-44ce-92f4-130e9503b6c1", "swatch_guid": "0ba92db2-690a-44ce-92f4-130e9503b6c1",
@@ -1522,13 +1333,13 @@
"mask_guid": "944d934d-5e21-42d7-8196-fff9a81ee469" "mask_guid": "944d934d-5e21-42d7-8196-fff9a81ee469"
}, },
{ {
"swatch_name": "Dodgeball_Black_Swatch", "swatch_name": "Aviator_Gloves_Black_Swatch",
"swatch_guid": "4a85c181-92fe-47a5-925e-05f0ccb25006", "swatch_guid": "97bbb384-b37e-4ea1-902c-88fbd6854ab5",
"color": "Black", "color": "Gloves_Black",
"item_name": "(Shirt_Dodgeball)", "item_name": "(Wrist_Aviator)",
"item_guid": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59", "item_guid": "125a3a62-1005-47dc-9fd8-ee09ea803e9f",
"item_outfit_type": 101, "item_outfit_type": 200,
"mask_guid": "944d934d-5e21-42d7-8196-fff9a81ee469" "mask_guid": "5659ce2c-9cb0-49da-8c39-4b7df0f8deec"
}, },
{ {
"swatch_name": "Hoodie_Gray_Swatch", "swatch_name": "Hoodie_Gray_Swatch",
@@ -1539,6 +1350,60 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Headphones_Blue_Swatch",
"swatch_guid": "8ANbOhNnO0yLQOCIBsDwfg",
"color": "Blue",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "Scallywag_Eye_SRank_Swatch",
"swatch_guid": "clFrpYWFQEmDkE14f3o_Ig",
"color": "Eye_SRank",
"item_name": "(Shirt_Scallywag)",
"item_guid": "664485e9-a159-4fcb-b32c-2b114b4a1218",
"item_outfit_type": 101,
"mask_guid": "DE1C-RJkuEWIvpR0q1i42g"
},
{
"swatch_name": "Winter_Hat_Red_Swatch",
"swatch_guid": "6vjA_vHq70Gclypll_GS_A",
"color": "Hat_Red",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": "nalaCGqtL0SFA4Itwqmp8g"
},
{
"swatch_name": "DeepSeaDiver_Black_Swatch",
"swatch_guid": "CahBzgN3D0y46AQsvGC14A",
"color": "Black",
"item_name": "(Hat_DeepSeaDiver)",
"item_guid": "4fcb5b84-13a9-43e6-9680-72b17874ae53",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Wizard_White_Swatch",
"swatch_guid": "d3a9b576-5433-4305-bd96-b08820c09212",
"color": "White",
"item_name": "(Hat_Wizard)",
"item_guid": "01689baf-405e-4b38-9f09-b624e03865b8",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Wrist_Grillmaster_Black_Swatch",
"swatch_guid": "hZKPr4sg10SRj9DLuEgOrw",
"color": "Black",
"item_name": "(Wrist_Grillmaster)",
"item_guid": "q7pSsRMNHkqEJmYHuhhG6g",
"item_outfit_type": 200,
"mask_guid": ""
},
{ {
"swatch_name": "Hoodie_Green_Swatch", "swatch_name": "Hoodie_Green_Swatch",
"swatch_guid": "ytmSzjhCr0Sm58MSNbwJUA", "swatch_guid": "ytmSzjhCr0Sm58MSNbwJUA",
@@ -1548,6 +1413,15 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Harness_Hat_White_Swatch",
"swatch_guid": "zlqLrGEa8E2WW9ey_szREA",
"color": "Hat_White",
"item_name": "(Shirt_Harness)",
"item_guid": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af",
"item_outfit_type": 101,
"mask_guid": "PZHohDVX0U2frX13N4KYDQ"
},
{ {
"swatch_name": "Hoodie_Blue_Swatch", "swatch_name": "Hoodie_Blue_Swatch",
"swatch_guid": "u_Gzj9Nro0WS48xKnH9few", "swatch_guid": "u_Gzj9Nro0WS48xKnH9few",
@@ -1558,67 +1432,31 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Hoodie_Pink_Swatch", "swatch_name": "Ranger_Shirt_Green_Swatch",
"swatch_guid": "TEgugZRs80iJbhL87RdYwg", "swatch_guid": "b415fe60-3e9d-4f4d-a53a-ca15eac6af7d",
"color": "Pink", "color": "Shirt_Green",
"item_name": "(Shirt_Hoodie)", "item_name": "(Hat_Ranger)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA", "item_guid": "f277af6a-0807-4716-8948-1bee236ffb74",
"item_outfit_type": 101, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Hoodie_Orange_Swatch", "swatch_name": "Shirt_TutorialGown2_Orange_Swatch",
"swatch_guid": "rNhOQoP3XkOBHV28ze2NpA", "swatch_guid": "L333w3dfIEWDbtt1Yn1gBg",
"color": "Orange", "color": "Orange",
"item_name": "(Shirt_Hoodie)", "item_name": "(Shirt_TutorialGown_2)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA", "item_guid": "MJQiQ3LATEOymxCC8wfSjw",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "lSDrKR69rkSFULy9GA__1w"
}, },
{ {
"swatch_name": "Hoodie_Purple_Swatch", "swatch_name": "ElectricianTorso_Yellow_Swatch",
"swatch_guid": "mvMeyStxGUGlNLSYNeSubQ", "swatch_guid": "b539777f-48b2-44a2-9d63-17ac703e4d57",
"color": "Purple", "color": "Torso_Yellow",
"item_name": "(Shirt_Hoodie)", "item_name": "(Hat_Electrician)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA", "item_guid": "34654303-9760-447f-8614-1c26506db994",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Hoodie_Red_Swatch",
"swatch_guid": "yM5xRrd9OkmDGyQddwOP6g",
"color": "Red",
"item_name": "(Shirt_Hoodie)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Hoodie_White_Swatch",
"swatch_guid": "-Nf7WM2TeEKujB637h3tRA",
"color": "White",
"item_name": "(Shirt_Hoodie)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "DeepSeaDiver_Black_Swatch",
"swatch_guid": "CahBzgN3D0y46AQsvGC14A",
"color": "Black",
"item_name": "(Hat_DeepSeaDiver)",
"item_guid": "4fcb5b84-13a9-43e6-9680-72b17874ae53",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "daa38a31-fb7b-4e26-a2f5-728d6dcec81a"
},
{
"swatch_name": "DeepSeaDiver_Beige_Swatch",
"swatch_guid": "Su7kiW24d0KapNns96JoFQ",
"color": "Beige",
"item_name": "(Hat_DeepSeaDiver)",
"item_guid": "4fcb5b84-13a9-43e6-9680-72b17874ae53",
"item_outfit_type": 0,
"mask_guid": ""
}, },
{ {
"swatch_name": "QueenCrown_Black_Swatch", "swatch_name": "QueenCrown_Black_Swatch",
@@ -1627,16 +1465,16 @@
"item_name": "(Hat_QueenCrown)", "item_name": "(Hat_QueenCrown)",
"item_guid": "9d395a84-4342-4b1c-940c-213ec2eb56f9", "item_guid": "9d395a84-4342-4b1c-940c-213ec2eb56f9",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "XFuSiGupZ02Fut1Dxum_Iw"
}, },
{ {
"swatch_name": "QueenCrown_White_Swatch", "swatch_name": "Shirt_VampireHunter_Midnight_Swatch",
"swatch_guid": "Zljmtj5WqUaRvB3XQTgJXg", "swatch_guid": "CavRDqIlmUeTZBtCLn1hZQ",
"color": "White", "color": "Midnight",
"item_name": "(Hat_QueenCrown)", "item_name": "(Shirt_VampireHunter)",
"item_guid": "9d395a84-4342-4b1c-940c-213ec2eb56f9", "item_guid": "JWfs9IiQs0Weu8MmyGpqLQ",
"item_outfit_type": 0, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "x10FeRBsQU2DIIpG8XIlwg"
}, },
{ {
"swatch_name": "Jazzercise_Black_Swatch", "swatch_name": "Jazzercise_Black_Swatch",
@@ -1647,15 +1485,6 @@
"item_outfit_type": 2, "item_outfit_type": 2,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Jazzercise_Yellow_Swatch",
"swatch_guid": "78eb0969-f246-4cac-acd6-295df7c092e4",
"color": "Yellow",
"item_name": "(Hair_Jazzercise)",
"item_guid": "49397e16-1027-4286-9bc4-b59eae565ff0",
"item_outfit_type": 2,
"mask_guid": ""
},
{ {
"swatch_name": "FieldHockey_Blue_Swatch", "swatch_name": "FieldHockey_Blue_Swatch",
"swatch_guid": "7b0d6661-e856-4ec1-ab90-d89c28a70826", "swatch_guid": "7b0d6661-e856-4ec1-ab90-d89c28a70826",
@@ -1665,6 +1494,15 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "1fede91a-434e-49a5-882e-e6db107112a8" "mask_guid": "1fede91a-434e-49a5-882e-e6db107112a8"
}, },
{
"swatch_name": "ArcherTan_Glove_Swatch",
"swatch_guid": "d5987fee-e3cc-4336-853a-5df57f73df05",
"color": "Tan_Glove",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "FlowerSwimsuit_Green_Swatch", "swatch_name": "FlowerSwimsuit_Green_Swatch",
"swatch_guid": "cf515c87-3c0a-4443-9f63-5ea1bdbaa61a", "swatch_guid": "cf515c87-3c0a-4443-9f63-5ea1bdbaa61a",
@@ -1675,22 +1513,22 @@
"mask_guid": "3248a3ec-50d1-4c0a-96ae-25eb9d8ed622" "mask_guid": "3248a3ec-50d1-4c0a-96ae-25eb9d8ed622"
}, },
{ {
"swatch_name": "FlowerSwimsuit_Pink_Swatch", "swatch_name": "Letterman_Purple_Swatch",
"swatch_guid": "27680277-aec1-41be-a476-9f51cf775580", "swatch_guid": "iufpnteVCEiiZu_m-yPovQ",
"color": "Pink", "color": "Purple",
"item_name": "(FlowerSwimsuit)", "item_name": "(Wrist_Letterman)",
"item_guid": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", "item_guid": "2dFMy4zc7ky_Fd5fzfgInw",
"item_outfit_type": 101, "item_outfit_type": 200,
"mask_guid": "3248a3ec-50d1-4c0a-96ae-25eb9d8ed622" "mask_guid": ""
}, },
{ {
"swatch_name": "FlowerSwimsuit_Gray_Swatch", "swatch_name": "Beekeeper_Shirt_Gold_Swatch",
"swatch_guid": "5a96ac4d-03c3-4214-9dcb-c2044ff28f2e", "swatch_guid": "1919d319-6cce-4500-8766-15fed6a13fa8",
"color": "Gray", "color": "Shirt_Gold",
"item_name": "(FlowerSwimsuit)", "item_name": "(Shirt_Beekeeper)",
"item_guid": "f41653f9-ae02-443c-aaf4-1c6f4e49035d", "item_guid": "1ebbfdfc-f72b-4d70-99d4-4d527f896aa7",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "3248a3ec-50d1-4c0a-96ae-25eb9d8ed622" "mask_guid": "afd88a79-7f03-40f6-bcb4-c247dd86edc4"
}, },
{ {
"swatch_name": "Angler_Blue_Swatch", "swatch_name": "Angler_Blue_Swatch",
@@ -1702,22 +1540,31 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Angler_Tan_Swatch", "swatch_name": "Hat_CatEars_White_Swatch",
"swatch_guid": "cac27853-e2df-4d77-b3fd-af3bc0813f01", "swatch_guid": "c1e430c5-fd8d-4032-9e3f-8d87cc8784da",
"color": "Tan", "color": "White",
"item_name": "(Hat_Angler)", "item_name": "(Hat_CatEars)",
"item_guid": "03f8c394-28fa-4087-978b-8d108f0bd969", "item_guid": "3d5184e1-0201-4476-bf4b-4eb28190de62",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "1e3195ab-f7f6-41f4-a5ec-378d09ecdf69"
}, },
{ {
"swatch_name": "Angler_Gray_Swatch", "swatch_name": "Smuggler_Dark_Swatch",
"swatch_guid": "97938f4c-52b8-4c85-8575-856654666316", "swatch_guid": "e821aa74-2d47-4388-bcfb-1893b8a83a52",
"color": "Gray", "color": "Dark",
"item_name": "(Hat_Angler)", "item_name": "(Shirt_Smuggler)",
"item_guid": "03f8c394-28fa-4087-978b-8d108f0bd969", "item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 0, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "970a8c7e-d091-4719-bb9c-e3be8d46ee68"
},
{
"swatch_name": "ScoutSkirt_Shirt_Red_Swatch",
"swatch_guid": "2bb65868-ee12-4871-b50d-64bbc9066f0b",
"color": "Shirt_Red",
"item_name": "(Shirt_ScoutSkirt)",
"item_guid": "35f60d27-22cf-4e17-8f39-c9e3a9fa2291",
"item_outfit_type": 101,
"mask_guid": "e65530f7-543c-43e9-bf12-b6cda2bc755c"
}, },
{ {
"swatch_name": "Artist_Blue_Swatch", "swatch_name": "Artist_Blue_Swatch",
@@ -1729,11 +1576,74 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Artist_Red_Swatch", "swatch_name": "Hat_CatEars_Gold_Swatch",
"swatch_guid": "17b16982-871c-476a-860f-f6953c89ee8c", "swatch_guid": "37f8378b-b98c-477a-b8c9-9340772800b5",
"color": "Red", "color": "Gold",
"item_name": "(Shirt_Artist)", "item_name": "(Hat_CatEars)",
"item_guid": "02077915-57d3-4e26-b4fe-8e0399cbb293", "item_guid": "3d5184e1-0201-4476-bf4b-4eb28190de62",
"item_outfit_type": 0,
"mask_guid": "1e3195ab-f7f6-41f4-a5ec-378d09ecdf69"
},
{
"swatch_name": "LaserTagVisor_Green_Swatch",
"swatch_guid": "-e6bmXnwZU2EVwZ8RupKbg",
"color": "Visor_Green",
"item_name": "(Hat_LaserTag)",
"item_guid": "123de433-a005-4994-8a11-d2d84ad66b11",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Headphones_TwoBlue_Swatch",
"swatch_guid": "4scG_UBlzE-5DWRhSrtm6g",
"color": "TwoBlue",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "CollarShirt_Torso_Red_Swatch",
"swatch_guid": "FAviMCQ_EE2Mpt6QPo5OEw",
"color": "Torso_Red",
"item_name": "(CollarShirt)",
"item_guid": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Pearls_Neck_Black_Swatch",
"swatch_guid": "sqPMH0LhLkeG-4XkxpU-sw",
"color": "Neck_Black",
"item_name": "(Neck_Pearls)",
"item_guid": "7be2eafc-21ea-4266-ae64-8888c06798d6",
"item_outfit_type": 103,
"mask_guid": ""
},
{
"swatch_name": "ArcherYellow_Glove_Swatch",
"swatch_guid": "47da86b9-4e6e-423c-b8c4-ea6bf8472509",
"color": "Yellow_Glove",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "MagicalGirl_Neck_Orange_Swatch",
"swatch_guid": "S9uymuvraUCOM8gmB7bnEA",
"color": "Neck_Orange",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "M5z2wk-GDkOAjvZzqsfcNQ"
},
{
"swatch_name": "TruckerJacket_Tan_Swatch",
"swatch_guid": "o2WfRyjJy0CKoUg8YIxNjQ",
"color": "Tan",
"item_name": "(Shirt_TruckerJacket)",
"item_guid": "8b065e2a-b106-4f11-b415-4c47ce87993e",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
@@ -1746,24 +1656,6 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "e5635772-4e84-4b30-b77f-4a1196cb599b" "mask_guid": "e5635772-4e84-4b30-b77f-4a1196cb599b"
}, },
{
"swatch_name": "HockeyShirt_Teal_Swatch",
"swatch_guid": "fcbdfbfd-254a-4538-b119-43e23404b8b5",
"color": "Teal",
"item_name": "(HockeyShirt)",
"item_guid": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c",
"item_outfit_type": 101,
"mask_guid": "e5635772-4e84-4b30-b77f-4a1196cb599b"
},
{
"swatch_name": "HockeyShirt_Purple_Swatch",
"swatch_guid": "d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace",
"color": "Purple",
"item_name": "(HockeyShirt)",
"item_guid": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c",
"item_outfit_type": 101,
"mask_guid": "e5635772-4e84-4b30-b77f-4a1196cb599b"
},
{ {
"swatch_name": "Hat_SeaCaptain_Contest_Swatch", "swatch_name": "Hat_SeaCaptain_Contest_Swatch",
"swatch_guid": "hhs9GY3XZkWb-8TXODvo6A", "swatch_guid": "hhs9GY3XZkWb-8TXODvo6A",
@@ -1774,18 +1666,45 @@
"mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9" "mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
}, },
{ {
"swatch_name": "LaserTagGlove_Orange_Swatch", "swatch_name": "YellowBlueDress_Purple_Swatch",
"swatch_guid": "EC0ttvk6eU2HEMacLAx2Iw", "swatch_guid": "830be2fa-60a5-48cc-931f-34b670eae4bd",
"color": "Orange", "color": "Purple",
"item_name": "(Wrist_LaserTagGlove)", "item_name": "(YellowBlueDress)",
"item_guid": "3044adce-90c0-4f66-81f5-39c4fca86fdf", "item_guid": "2296ed0d-df56-4d46-b33a-aae9230a47fc",
"item_outfit_type": 200, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "LaserTagGlove_Green_Swatch", "swatch_name": "80sSpandex_Green_Swatch",
"swatch_guid": "mhsglVEtREmElF5wuLw3NQ", "swatch_guid": "3frH6KxZ9kGUYkjeCDpylQ",
"color": "Green", "color": "Green",
"item_name": "(Shirt_80sSpandex)",
"item_guid": "17236d76-e3b6-42f6-9179-6fcf0479436f",
"item_outfit_type": 101,
"mask_guid": "B-sIpWNOfEigMeQF-WVR1g"
},
{
"swatch_name": "Hat_Baseball_Logo_Bucketeers_Swatch",
"swatch_guid": "5iCrYb8MBkqN6IBEe3IipQ",
"color": "Logo_Bucketeers",
"item_name": "(Hat_Baseball)",
"item_guid": "14ef6b00-debf-4a85-9755-b4d37df496d3",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "BowlingShirt_OneStripe_Pink_Swatch",
"swatch_guid": "-k9_-2onrEG407or8t1RFw",
"color": "Shirt_OneStripe_Pink",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "b-IM3eYMJUa40395O5XeLg"
},
{
"swatch_name": "LaserTagGlove_Orange_Swatch",
"swatch_guid": "EC0ttvk6eU2HEMacLAx2Iw",
"color": "Orange",
"item_name": "(Wrist_LaserTagGlove)", "item_name": "(Wrist_LaserTagGlove)",
"item_guid": "3044adce-90c0-4f66-81f5-39c4fca86fdf", "item_guid": "3044adce-90c0-4f66-81f5-39c4fca86fdf",
"item_outfit_type": 200, "item_outfit_type": 200,
@@ -1798,61 +1717,16 @@
"item_name": "(Shirt_50sDress)", "item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", "item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "Rk18_7icF0Srgh7M1_G-hA"
}, },
{ {
"swatch_name": "50sDress_Pink_Swatch", "swatch_name": "Firefighter_Hat_Yellow_Swatch",
"swatch_guid": "eb3SmJTIh0628LrZUkI4KQ", "swatch_guid": "4c26d1e7-2382-4478-b9c4-a068ed01a607",
"color": "Pink", "color": "Hat_Yellow",
"item_name": "(Shirt_50sDress)", "item_name": "(Hat_Firefighter)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee", "item_guid": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1",
"item_outfit_type": 101, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "c8ccb85e-52cd-4b3b-aadf-289819067125"
},
{
"swatch_name": "50sDress_Teal_Swatch",
"swatch_guid": "QzjakcO8tUaPeiI5KBLKtg",
"color": "Teal",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "50sDress_White_Swatch",
"swatch_guid": "BLgkgLeS-EyazgPQxGcavg",
"color": "White",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "50sDress_CandyCane_Swatch",
"swatch_guid": "PEGqjoxP6E2yRx_A-HphrQ",
"color": "CandyCane",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "50sDress_Halloween_Swatch",
"swatch_guid": "ZvPidQ57rUS50Qz89ERmLw",
"color": "Halloween",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "50sDress_Shamrock_Swatch",
"swatch_guid": "K17dIz_TRUq5D50NBfLjyw",
"color": "Shamrock",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": ""
}, },
{ {
"swatch_name": "TopHat_White_Swatch", "swatch_name": "TopHat_White_Swatch",
@@ -1861,8 +1735,44 @@
"item_name": "(Hat_TopHat)", "item_name": "(Hat_TopHat)",
"item_guid": "102c625b-b988-4bf8-a2aa-a31ad7029cdc", "item_guid": "102c625b-b988-4bf8-a2aa-a31ad7029cdc",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "ccf1ccc1-e229-4157-bb74-f2cdef01e547"
},
{
"swatch_name": "DeepSeaDiver_Beige_Swatch",
"swatch_guid": "Su7kiW24d0KapNns96JoFQ",
"color": "Beige",
"item_name": "(Hat_DeepSeaDiver)",
"item_guid": "4fcb5b84-13a9-43e6-9680-72b17874ae53",
"item_outfit_type": 0,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Scallywag_Bracelet_Red_Swatch",
"swatch_guid": "62bd5dda-08da-4528-83c1-f0ca3812dc6e",
"color": "Red",
"item_name": "(Wrist_Scallywag_Bracelet)",
"item_guid": "b9e0f03b-2609-4911-82e9-1e324dbbe8be",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "QuiltedCoat_Green_Swatch",
"swatch_guid": "QBglgj80zkuvUkk55Y5Feg",
"color": "Green",
"item_name": "(Shirt_Quiltedcoat)",
"item_guid": "0ab5790f-a537-4306-ba66-5eb87c02ec7d",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Scout_RaccoonHat_Gray_Swatch",
"swatch_guid": "926ae943-76cb-46dd-9c66-a6ee44ea9194",
"color": "nHat_Gray",
"item_name": "(Hat_ScoutRaccoon)",
"item_guid": "7c1552a6-bc36-46e6-884c-1de6b8111a92",
"item_outfit_type": 0,
"mask_guid": "f1cd10fb-3d0b-436f-a173-65bc0d88d8bf"
},
{ {
"swatch_name": "Shirt_GraduationGown_Green_Swatch", "swatch_name": "Shirt_GraduationGown_Green_Swatch",
"swatch_guid": "iZy7RmfAz0603oMO-jD-WA", "swatch_guid": "iZy7RmfAz0603oMO-jD-WA",
@@ -1872,6 +1782,15 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "ica0LZyM_kOox8gxW7CaMw" "mask_guid": "ica0LZyM_kOox8gxW7CaMw"
}, },
{
"swatch_name": "WaitressApron_Mustard_Swatch",
"swatch_guid": "b7oobDcDtE-69fhM-5g5AQ",
"color": "Mustard",
"item_name": "(Belt_WaitressApron)",
"item_guid": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7",
"item_outfit_type": 102,
"mask_guid": ""
},
{ {
"swatch_name": "DrumMajor_Orange_Swatch", "swatch_name": "DrumMajor_Orange_Swatch",
"swatch_guid": "82de1ca3-67d6-454d-84fb-0eeece71fcbc", "swatch_guid": "82de1ca3-67d6-454d-84fb-0eeece71fcbc",
@@ -1882,22 +1801,31 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "DrumMajor_Green_Swatch", "swatch_name": "JackfrostWings_Neck_Lilac_Swatch",
"swatch_guid": "fe5288bc-0e02-4ea3-b17d-dee0e576979d", "swatch_guid": "H6fcuLxeBUSY3oERFc-plA",
"color": "Green", "color": "Neck_Lilac",
"item_name": "(Hat_DrumMajor)", "item_name": "(Neck_JackFrostWings)",
"item_guid": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", "item_guid": "022ce375-ed02-479d-b87d-9d72d126bbd4",
"item_outfit_type": 0, "item_outfit_type": 100,
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "DrumMajor_Purple_Swatch", "swatch_name": "PurpleJacket_Blue_Swatch",
"swatch_guid": "54d3c589-30ba-44c1-a77c-8f9e16eb0b20", "swatch_guid": "b42d903b-b393-4f49-af5e-38d0fb6c42b0",
"color": "Purple", "color": "Blue",
"item_name": "(Hat_DrumMajor)", "item_name": "(PurpleJacket)",
"item_guid": "1a028850-64ee-40ab-b547-ebbcfa1b04b3", "item_guid": "745c5110-104e-4b37-aabf-7c690fd54811",
"item_outfit_type": 101,
"mask_guid": "3ba69a39-a4e7-4505-bae7-8db31360ec1c"
},
{
"swatch_name": "CowboyBlack_Swatch",
"swatch_guid": "K5vPqD3BrEuODRDzFdC6Hw",
"color": "Black",
"item_name": "(Hat_Cowboy)",
"item_guid": "ad0bdec5-f767-42e7-99e0-221473464a85",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "54778d93-2cff-48db-b25b-65c08b0e5be8"
}, },
{ {
"swatch_name": "Harness_Teal_Swatch", "swatch_name": "Harness_Teal_Swatch",
@@ -1908,6 +1836,123 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Angler_Tan_Swatch",
"swatch_guid": "cac27853-e2df-4d77-b3fd-af3bc0813f01",
"color": "Tan",
"item_name": "(Hat_Angler)",
"item_guid": "03f8c394-28fa-4087-978b-8d108f0bd969",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "50sDress_Pink_Swatch",
"swatch_guid": "eb3SmJTIh0628LrZUkI4KQ",
"color": "Pink",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": "Rk18_7icF0Srgh7M1_G-hA"
},
{
"swatch_name": "Scout_Sash_Brown_Swatch",
"swatch_guid": "639f5e2c-6fbd-4ab4-aa10-7772e0932798",
"color": "h_Brown",
"item_name": "(Neck_ScoutSash)",
"item_guid": "bf9dc196-5727-4752-8126-7ad5fcd96d55",
"item_outfit_type": 100,
"mask_guid": "66cc536d-8cb5-4262-b7f0-49a33b97ea10"
},
{
"swatch_name": "Unicorn_Pastel_Swatch",
"swatch_guid": "FNe9LkWJMUeC3frYJMyEaQ",
"color": "Pastel",
"item_name": "(Hat_Unicorn)",
"item_guid": "2caHvVQ1vECrPv-YcdxdyA",
"item_outfit_type": 0,
"mask_guid": "nW_fBsuZA0OfIjTMXm6ZCA"
},
{
"swatch_name": "Countess_Black_Swatch",
"swatch_guid": "i5RKLo-RbEqisnGII1wpaQ",
"color": "Black",
"item_name": "(Shirt_Countess)",
"item_guid": "QCtqxbvYOkan79YFugH_Cw",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Smuggler_Cherry_Swatch",
"swatch_guid": "fed760b2-15e2-46b2-b1e3-cc3c8d780d5d",
"color": "Cherry",
"item_name": "(Shirt_Smuggler)",
"item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 101,
"mask_guid": "970a8c7e-d091-4719-bb9c-e3be8d46ee68"
},
{
"swatch_name": "Astronaut_Torso_Orange_Swatch",
"swatch_guid": "9GhnX4Pj4E-wxbPlD8GB-g",
"color": "o_Orange",
"item_name": "(AstronautTorso)",
"item_guid": "ec5255d5-edaf-4888-9fe1-e89ac0e0d300",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "AnglerHat_Gray_Swatch",
"swatch_guid": "225209dc-3afc-40cf-858e-89f7afa00c0d",
"color": "Hat_Gray",
"item_name": "(Hat_Angler)",
"item_guid": "03f8c394-28fa-4087-978b-8d108f0bd969",
"item_outfit_type": 0,
"mask_guid": "efeb07ab-6253-436d-bb66-991ddad72e58"
},
{
"swatch_name": "BaseballHelmet_Green_Swatch",
"swatch_guid": "9967ce54-c2b7-437a-946e-11f8ee6d6fdd",
"color": "Green",
"item_name": "(Hat_BaseballHelmet)",
"item_guid": "293e7ad3-01c9-43af-96e3-06c34145ff12",
"item_outfit_type": 0,
"mask_guid": "FVdDQFOMQ06teQm2ANDPng"
},
{
"swatch_name": "Neck_Grillmaster_White_Swatch",
"swatch_guid": "Lxx3q6JRnUW_cPG_kGG4ag",
"color": "White",
"item_name": "(Neck_Grillmaster)",
"item_guid": "tTKv7eRODka2KC_hsY2O3Q",
"item_outfit_type": 100,
"mask_guid": "t-ZEWJCgFEyGP2PjNblQMA"
},
{
"swatch_name": "50sDress_Teal_Swatch",
"swatch_guid": "QzjakcO8tUaPeiI5KBLKtg",
"color": "Teal",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": "Rk18_7icF0Srgh7M1_G-hA"
},
{
"swatch_name": "BowlingShirt_Diamonds_Teal_Swatch",
"swatch_guid": "yZR5aX3gQ0WpRUT6cfK1cA",
"color": "Shirt_Diamonds_Teal",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "VO_3cgqQqUOTz30TVUxrFA"
},
{
"swatch_name": "Scallywag_Bracelet_Blue_Swatch",
"swatch_guid": "cdca593a-8840-4140-9145-fe962732040c",
"color": "Blue",
"item_name": "(Wrist_Scallywag_Bracelet)",
"item_guid": "b9e0f03b-2609-4911-82e9-1e324dbbe8be",
"item_outfit_type": 200,
"mask_guid": ""
},
{ {
"swatch_name": "Harness_Black_Swatch", "swatch_name": "Harness_Black_Swatch",
"swatch_guid": "AC3zfm38JEyBZWuXDh4Ukg", "swatch_guid": "AC3zfm38JEyBZWuXDh4Ukg",
@@ -1917,6 +1962,150 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "CowboyDark_Swatch",
"swatch_guid": "192ba73d-6990-42da-8fe0-914c2a7181eb",
"color": "Dark",
"item_name": "(Hat_Cowboy)",
"item_guid": "ad0bdec5-f767-42e7-99e0-221473464a85",
"item_outfit_type": 0,
"mask_guid": "54778d93-2cff-48db-b25b-65c08b0e5be8"
},
{
"swatch_name": "SodaJerk_Mustard_Swatch",
"swatch_guid": "MxnAq_XOw0uTzQ8IIxqb_Q",
"color": "Mustard",
"item_name": "(Shirt_SodaJerk)",
"item_guid": "iai6P7dDTUq8S4hqQmY1YA",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "FlowerSwimsuit_Pink_Swatch",
"swatch_guid": "27680277-aec1-41be-a476-9f51cf775580",
"color": "Pink",
"item_name": "(FlowerSwimsuit)",
"item_guid": "f41653f9-ae02-443c-aaf4-1c6f4e49035d",
"item_outfit_type": 101,
"mask_guid": "3248a3ec-50d1-4c0a-96ae-25eb9d8ed622"
},
{
"swatch_name": "Artist_Red_Swatch",
"swatch_guid": "17b16982-871c-476a-860f-f6953c89ee8c",
"color": "Red",
"item_name": "(Shirt_Artist)",
"item_guid": "02077915-57d3-4e26-b4fe-8e0399cbb293",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Hoodie_Pink_Swatch",
"swatch_guid": "TEgugZRs80iJbhL87RdYwg",
"color": "Pink",
"item_name": "(Shirt_Hoodie)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "50sDress_White_Swatch",
"swatch_guid": "BLgkgLeS-EyazgPQxGcavg",
"color": "White",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": "Rk18_7icF0Srgh7M1_G-hA"
},
{
"swatch_name": "Hat_TutorialCap_3_Swatch",
"swatch_guid": "wHJnbDOo8ESmUbQDr0k31w",
"color": "3",
"item_name": "(Hat_TutorialCap)",
"item_guid": "M9weaekvIUimiJSVJqZl-w",
"item_outfit_type": 0,
"mask_guid": "GV8z2dlqGEqIAYaGrF1ZYQ"
},
{
"swatch_name": "Scout_Sash_Red_Swatch",
"swatch_guid": "b520f3bc-599a-498c-8a4f-97cc25aa741f",
"color": "h_Red",
"item_name": "(Neck_ScoutSash)",
"item_guid": "bf9dc196-5727-4752-8126-7ad5fcd96d55",
"item_outfit_type": 100,
"mask_guid": "66cc536d-8cb5-4262-b7f0-49a33b97ea10"
},
{
"swatch_name": "DrumMajor_Green_Swatch",
"swatch_guid": "fe5288bc-0e02-4ea3-b17d-dee0e576979d",
"color": "Green",
"item_name": "(Hat_DrumMajor)",
"item_guid": "1a028850-64ee-40ab-b547-ebbcfa1b04b3",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "PirateCaptain_Hat_SRank_Swatch",
"swatch_guid": "jQwNBXyoU0Gk4FyNw6jFxg",
"color": "Hat_SRank",
"item_name": "(Neck_PirateCaptain)",
"item_guid": "38b38e24-d636-41c6-b80e-a285ae3106dc",
"item_outfit_type": 102,
"mask_guid": ""
},
{
"swatch_name": "50sDress_CandyCane_Swatch",
"swatch_guid": "PEGqjoxP6E2yRx_A-HphrQ",
"color": "CandyCane",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": "Rk18_7icF0Srgh7M1_G-hA"
},
{
"swatch_name": "SpaceMarine_Cyan_Swatch",
"swatch_guid": "c8cc9589-4831-4e73-a865-ef8c139a2ba7",
"color": "Cyan",
"item_name": "(Shirt_SpaceMarine)",
"item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 101,
"mask_guid": "7ac81d60-05b8-4a85-982c-f69213a93818"
},
{
"swatch_name": "MagicalGirl_Green_Swatch",
"swatch_guid": "kTP3xKhzBkuQF3SqvmYFKA",
"color": "Green",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "FootballPads_Orange_Swatch",
"swatch_guid": "32dde6dc-e3dc-43ee-9b7c-987bc6778c17",
"color": "Pads_Orange",
"item_name": "(Hat_FootBall)",
"item_guid": "5a38f864-7d1c-4989-8558-40b9668f78ef",
"item_outfit_type": 0,
"mask_guid": "e17481fc-b2b6-47a5-91e5-7205f45a3247"
},
{
"swatch_name": "HeadBackBow_White_Swatch",
"swatch_guid": "-Iqmonry4EKoqAULCKzRPA",
"color": "White",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": "3DdSzSqV1UG5AtPr8ro3Kg"
},
{
"swatch_name": "Hoodie_Orange_Swatch",
"swatch_guid": "rNhOQoP3XkOBHV28ze2NpA",
"color": "Orange",
"item_name": "(Shirt_Hoodie)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "Harness_Yellow_Swatch", "swatch_name": "Harness_Yellow_Swatch",
"swatch_guid": "dyEaPloE8kazWC7Zblv8uQ", "swatch_guid": "dyEaPloE8kazWC7Zblv8uQ",
@@ -1927,85 +2116,31 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Harness_Blue_Swatch", "swatch_name": "PirateCaptain_Hat_Red_Swatch",
"swatch_guid": "RfZNDPUok0WyQEa3Ko08ow", "swatch_guid": "17040a88-87f1-4613-b439-3316e33dbd48",
"color": "Hat_Red",
"item_name": "(Neck_PirateCaptain)",
"item_guid": "38b38e24-d636-41c6-b80e-a285ae3106dc",
"item_outfit_type": 102,
"mask_guid": ""
},
{
"swatch_name": "80sSpandex_Blue_Swatch",
"swatch_guid": "Lf4FjROSAEqXedFxpXFQfg",
"color": "Blue", "color": "Blue",
"item_name": "(Shirt_Harness)", "item_name": "(Shirt_80sSpandex)",
"item_guid": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af", "item_guid": "17236d76-e3b6-42f6-9179-6fcf0479436f",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "B-sIpWNOfEigMeQF-WVR1g"
}, },
{ {
"swatch_name": "Harness_Orange_Swatch", "swatch_name": "PurpleJacket_Black_Swatch",
"swatch_guid": "sX0ZZDcCUkyFSJISfvq0vg", "swatch_guid": "8cab8cc6-9a2d-434c-8b72-e66edbdd223c",
"color": "Orange",
"item_name": "(Shirt_Harness)",
"item_guid": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Unicorn_Pastel_Swatch",
"swatch_guid": "FNe9LkWJMUeC3frYJMyEaQ",
"color": "Pastel",
"item_name": "(Hat_Unicorn)",
"item_guid": "2caHvVQ1vECrPv-YcdxdyA",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Unicorn_Glowing_Swatch",
"swatch_guid": "mudt-Z5-5USdnIq-xaDgOw",
"color": "Glowing",
"item_name": "(Hat_Unicorn)",
"item_guid": "2caHvVQ1vECrPv-YcdxdyA",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Countess_Black_Swatch",
"swatch_guid": "i5RKLo-RbEqisnGII1wpaQ",
"color": "Black", "color": "Black",
"item_name": "(Shirt_Countess)", "item_name": "(PurpleJacket)",
"item_guid": "QCtqxbvYOkan79YFugH_Cw", "item_guid": "745c5110-104e-4b37-aabf-7c690fd54811",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "3ba69a39-a4e7-4505-bae7-8db31360ec1c"
},
{
"swatch_name": "Countess_Purple_Swatch",
"swatch_guid": "IzkFARlv6EenmrfcNkcOuw",
"color": "Purple",
"item_name": "(Shirt_Countess)",
"item_guid": "QCtqxbvYOkan79YFugH_Cw",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Countess_Green_Swatch",
"swatch_guid": "LyVf9lnzdku1F4iPBGqk-g",
"color": "Green",
"item_name": "(Shirt_Countess)",
"item_guid": "QCtqxbvYOkan79YFugH_Cw",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "CheerDress_Blue_Swatch",
"swatch_guid": "faedf215-289b-40f7-90cc-f5b98517d133",
"color": "Blue",
"item_name": "(FemaleCheerTorso)",
"item_guid": "657efe89-620a-46a2-8b58-cafd855c7cd5",
"item_outfit_type": 101,
"mask_guid": "93987d32-7a6e-45df-af56-76f912969ce7"
},
{
"swatch_name": "CheerDress_Red_Swatch",
"swatch_guid": "1773d5d8-4714-4721-b30c-97e25d6f2a5a",
"color": "Red",
"item_name": "Cheerful Dress",
"item_guid": "b02053b3-013f-42b0-8881-85f5be2b8cda",
"item_outfit_type": 101,
"mask_guid": "93987d32-7a6e-45df-af56-76f912969ce7"
}, },
{ {
"swatch_name": "Mobster_Valentine_Swatch", "swatch_name": "Mobster_Valentine_Swatch",
@@ -2017,13 +2152,13 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Mobster_WhiteHatHacker_Swatch", "swatch_name": "Hat_SeaCaptainFushia_Swatch",
"swatch_guid": "40283127-2ffc-4989-82f6-29c3381bf9e2", "swatch_guid": "2738ab5f-8c94-46b3-bac6-4a8111c41c21",
"color": "WhiteHatHacker", "color": "Fushia",
"item_name": "(Wrist_Mobster)", "item_name": "(Hat_SeaCaptain)",
"item_guid": "08d56627-4500-4724-8692-a23c96ddb3ec", "item_guid": "f5c9743f-e77c-42f2-ba63-9843342cbf8e",
"item_outfit_type": 200, "item_outfit_type": 0,
"mask_guid": "" "mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
}, },
{ {
"swatch_name": "Luchador_Red_Swatch", "swatch_name": "Luchador_Red_Swatch",
@@ -2035,13 +2170,22 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Luchador_Blue_Swatch", "swatch_name": "Scarecrow_Wrist_Contest_Swatch",
"swatch_guid": "0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250", "swatch_guid": "dro7VLV8BkGjqpPAbJheYA",
"color": "Blue", "color": "Wrist_Contest",
"item_name": "(Hat_Luchador)", "item_name": "(Shirt_Scarecrow)",
"item_guid": "1596c113-aa3a-4f5c-b398-e5c3575548e7", "item_guid": "1fcaf4fd-9012-407e-b691-fbc3ccf22421",
"item_outfit_type": 0, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": "bNA44H2wzUy-FkWgyOq1Kw"
},
{
"swatch_name": "FlapperGlove_Gold_Swatch",
"swatch_guid": "2cf2b644-3fa6-449c-9fe8-d0ab917f3106",
"color": "Gold",
"item_name": "(Wrist_FlapperGlove)",
"item_guid": "473c672d-2b79-48a2-a49d-fd68160c5bde",
"item_outfit_type": 200,
"mask_guid": "0e42108e-3b08-49b4-86c0-7984719e8380"
}, },
{ {
"swatch_name": "BlueJacket_Mint_Swatch", "swatch_name": "BlueJacket_Mint_Swatch",
@@ -2052,6 +2196,150 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "HeadBackBow_SolidBlack_Swatch",
"swatch_guid": "f_c50saStkaGJuhLjJ9lfA",
"color": "SolidBlack",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": "3DdSzSqV1UG5AtPr8ro3Kg"
},
{
"swatch_name": "FootballPads_Dark_Swatch",
"swatch_guid": "88c3ed05-ce41-4a9d-a4a7-8a428d240b3f",
"color": "Pads_Dark",
"item_name": "(Hat_FootBall)",
"item_guid": "5a38f864-7d1c-4989-8558-40b9668f78ef",
"item_outfit_type": 0,
"mask_guid": "e17481fc-b2b6-47a5-91e5-7205f45a3247"
},
{
"swatch_name": "MagicalGirl_Neck_Red_Swatch",
"swatch_guid": "_uh3kj6hKUyBKOSR3R8dog",
"color": "Neck_Red",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "M5z2wk-GDkOAjvZzqsfcNQ"
},
{
"swatch_name": "Aviator_Hat_Black_Swatch",
"swatch_guid": "a8de80d3-286a-4fdc-bed2-75d52f8c0964",
"color": "Hat_Black",
"item_name": "(Wrist_Aviator)",
"item_guid": "125a3a62-1005-47dc-9fd8-ee09ea803e9f",
"item_outfit_type": 200,
"mask_guid": "ff5ae41e-e446-4a96-8c11-49d85556fa81"
},
{
"swatch_name": "Aviator_Goggles_Black_Swatch",
"swatch_guid": "7b82c3c5-fd1f-45ec-b83f-e32668aa8dce",
"color": "s_Black",
"item_name": "(Eye_AviatorGoggles)",
"item_guid": "47629ed1-5378-427c-ad61-4b44d2fe90cc",
"item_outfit_type": 10,
"mask_guid": "556f876a-70da-4104-b792-fca8ad67be0c"
},
{
"swatch_name": "Mobster_Fashion_Gold_Swatch",
"swatch_guid": "46970903-9799-4d25-b90a-7162c1f5dbef",
"color": "Fashion_Gold",
"item_name": "(Wrist_Mobster)",
"item_guid": "08d56627-4500-4724-8692-a23c96ddb3ec",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "ArcherSRank_Glove_Swatch",
"swatch_guid": "1VGZ1QCvcU6rsKVkUDBNjQ",
"color": "SRank_Glove",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Golfer_Shirt_Grey_Swatch",
"swatch_guid": "4c845dbb-26c3-48a1-b562-e28a7416b154",
"color": "Shirt_Grey",
"item_name": "(Hat_Golfer)",
"item_guid": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54",
"item_outfit_type": 0,
"mask_guid": "6ce21c65-f4e9-41ac-be94-8eb1a08d5c76"
},
{
"swatch_name": "Hat_SeaCaptainDarkGold_Swatch",
"swatch_guid": "f5403c89-9769-479b-8a05-c71607f9b189",
"color": "DarkGold",
"item_name": "(Hat_SeaCaptain)",
"item_guid": "f5c9743f-e77c-42f2-ba63-9843342cbf8e",
"item_outfit_type": 0,
"mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
},
{
"swatch_name": "CollarShirt_Torso_Blue_Swatch",
"swatch_guid": "0iSsaY-HgkmLaRHCn5vEdw",
"color": "Torso_Blue",
"item_name": "(CollarShirt)",
"item_guid": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Letterman_Black_Swatch",
"swatch_guid": "Z1F9rjKqCE-Hd2-Ip5g3gA",
"color": "Black",
"item_name": "(Wrist_Letterman)",
"item_guid": "2dFMy4zc7ky_Fd5fzfgInw",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "DrumMajor_Purple_Swatch",
"swatch_guid": "54d3c589-30ba-44c1-a77c-8f9e16eb0b20",
"color": "Purple",
"item_name": "(Hat_DrumMajor)",
"item_guid": "1a028850-64ee-40ab-b547-ebbcfa1b04b3",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "PirateCaptain_Belt_Red_Swatch",
"swatch_guid": "0f71b9a2-dd88-435c-a571-4503d12dcf2d",
"color": "Belt_Red",
"item_name": "(Neck_PirateCaptain)",
"item_guid": "38b38e24-d636-41c6-b80e-a285ae3106dc",
"item_outfit_type": 102,
"mask_guid": "9b2102f0-e16b-464f-9f1c-f068de52bb89"
},
{
"swatch_name": "ArcherBlack_Glove_Swatch",
"swatch_guid": "2be2f040-68e4-44dd-9099-29f11110f3a4",
"color": "Black_Glove",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "WitchHunter_Blue_Swatch",
"swatch_guid": "aBQ3R9boek-mYeSYSNvDbQ",
"color": "Blue",
"item_name": "(Shirt_WitchHunter)",
"item_guid": "418db0e0-c7af-40c2-9cac-d461ce41e210",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Sherlock_Hat_Gray_Swatch",
"swatch_guid": "MQOxmUk-mUG-Ee9m9nX31Q",
"color": "Hat_Gray",
"item_name": "(Hat_Sherlock)",
"item_guid": "5bde836f-9343-4934-b01a-606f707020ef",
"item_outfit_type": 0,
"mask_guid": "axPBGoqorEe77Oqqjfzb5w"
},
{ {
"swatch_name": "Steampunk_Contest_Swatch", "swatch_name": "Steampunk_Contest_Swatch",
"swatch_guid": "fD5vtKxBWECH54Jj4FbnUg", "swatch_guid": "fD5vtKxBWECH54Jj4FbnUg",
@@ -2061,6 +2349,51 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Hoodie_Purple_Swatch",
"swatch_guid": "mvMeyStxGUGlNLSYNeSubQ",
"color": "Purple",
"item_name": "(Shirt_Hoodie)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "FighterPilot_Blue_Swatch",
"swatch_guid": "e37b07d1-6b9d-49b9-94ca-1c728e228cfe",
"color": "Blue",
"item_name": "(Wrist_FighterPilot)",
"item_guid": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8",
"item_outfit_type": 200,
"mask_guid": "435eba0d-d8f9-43cc-91aa-2959d63c6fa7"
},
{
"swatch_name": "AnglerHat_Tan_Swatch",
"swatch_guid": "3f6c8bd2-2c1d-4c8c-a9de-16600fee9645",
"color": "Hat_Tan",
"item_name": "(Hat_Angler)",
"item_guid": "03f8c394-28fa-4087-978b-8d108f0bd969",
"item_outfit_type": 0,
"mask_guid": "efeb07ab-6253-436d-bb66-991ddad72e58"
},
{
"swatch_name": "Golfer_Glove_Blue_Swatch",
"swatch_guid": "32a5e42d-2b60-40b1-a145-ed96fc91af8e",
"color": "Glove_Blue",
"item_name": "(Hat_Golfer)",
"item_guid": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54",
"item_outfit_type": 0,
"mask_guid": "f4cea991-fc3d-4715-bf82-8a8fb60e8799"
},
{
"swatch_name": "Steampunk_Femme_Contest_Hat_Swatch",
"swatch_guid": "vuSwk04zykCe3ANKN838OA",
"color": "e_Contest_Hat",
"item_name": "(Hat_SteampunkFemme)",
"item_guid": "JWanxvOHyESg9F_HlmTOfA",
"item_outfit_type": 0,
"mask_guid": ""
},
{ {
"swatch_name": "GoldMedal_Bronze_Swatch", "swatch_name": "GoldMedal_Bronze_Swatch",
"swatch_guid": "iaDL2im4lkCXUo0NWM-F3g", "swatch_guid": "iaDL2im4lkCXUo0NWM-F3g",
@@ -2070,6 +2403,69 @@
"item_outfit_type": 100, "item_outfit_type": 100,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "DrumMajorGlove_Purple_Swatch",
"swatch_guid": "8053b5e5-0f67-4329-a45d-1ee9fc830820",
"color": "Purple",
"item_name": "(Wrist_DrumMajor_Glove)",
"item_guid": "4d533b3a-b5fa-446a-842d-92798ad5b493",
"item_outfit_type": 200,
"mask_guid": "6b38ab79-142b-446e-978c-ad829ade8823"
},
{
"swatch_name": "50sDress_Halloween_Swatch",
"swatch_guid": "ZvPidQ57rUS50Qz89ERmLw",
"color": "Halloween",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": "Rk18_7icF0Srgh7M1_G-hA"
},
{
"swatch_name": "TruckerOveralls_Red_Swatch",
"swatch_guid": "iokdlBfjuEuAwqIMOQwxdA",
"color": "Red",
"item_name": "(Shirt_TruckerOveralls)",
"item_guid": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "ElectricianTorso_Green_Swatch",
"swatch_guid": "67fc9269-8dd8-4d6f-b594-c77e716f2482",
"color": "Torso_Green",
"item_name": "(Hat_Electrician)",
"item_guid": "34654303-9760-447f-8614-1c26506db994",
"item_outfit_type": 0,
"mask_guid": "daa38a31-fb7b-4e26-a2f5-728d6dcec81a"
},
{
"swatch_name": "QuiltedCoat_Plum_Swatch",
"swatch_guid": "IjRMopsYlUqSE6327I9TIQ",
"color": "Plum",
"item_name": "(Shirt_Quiltedcoat)",
"item_guid": "0ab5790f-a537-4306-ba66-5eb87c02ec7d",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "WitchHunter_SRank_Swatch",
"swatch_guid": "Tbrc2MlSlE6mKua8qSoluQ",
"color": "SRank",
"item_name": "(Shirt_WitchHunter)",
"item_guid": "418db0e0-c7af-40c2-9cac-d461ce41e210",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "TruckerJacket_RebelRose_Swatch",
"swatch_guid": "4Eck_dNL1UKlIZWsA1YppQ",
"color": "RebelRose",
"item_name": "(Shirt_TruckerJacket)",
"item_guid": "8b065e2a-b106-4f11-b415-4c47ce87993e",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "GoldMedal_Silver_Swatch", "swatch_name": "GoldMedal_Silver_Swatch",
"swatch_guid": "ERnUPsyT6kuEnIRZF8zPwg", "swatch_guid": "ERnUPsyT6kuEnIRZF8zPwg",
@@ -2088,6 +2484,294 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Sherlock_Torso_Black_Swatch",
"swatch_guid": "Z1Ib9r_fkUqgoZk0uid_Xw",
"color": "Torso_Black",
"item_name": "(Hat_Sherlock)",
"item_guid": "5bde836f-9343-4934-b01a-606f707020ef",
"item_outfit_type": 0,
"mask_guid": "9ZP617DQk0mZ10J3d5q9oA"
},
{
"swatch_name": "DrumMajorGlove_Orange_Swatch",
"swatch_guid": "b444ed0a-eab5-4bd6-af34-d080de74a149",
"color": "Orange",
"item_name": "(Wrist_DrumMajor_Glove)",
"item_guid": "4d533b3a-b5fa-446a-842d-92798ad5b493",
"item_outfit_type": 200,
"mask_guid": "6b38ab79-142b-446e-978c-ad829ade8823"
},
{
"swatch_name": "LaserTagVisor_Orange_Swatch",
"swatch_guid": "oT1vLrHW6UC2hWLIT3f_SQ",
"color": "Visor_Orange",
"item_name": "(Hat_LaserTag)",
"item_guid": "123de433-a005-4994-8a11-d2d84ad66b11",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Punk_WristRed_Swatch",
"swatch_guid": "baffdefe-de21-4551-9330-b50873965bdd",
"color": "WristRed",
"item_name": "(Wrist_Punk)",
"item_guid": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7",
"item_outfit_type": 200,
"mask_guid": "dd7472ed-bdcb-4811-81ba-5ed682fb7675"
},
{
"swatch_name": "Winter_Gloves_Tree_Swatch",
"swatch_guid": "afWY5VGiF0isGn6qpo4KeQ",
"color": "Gloves_Tree",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Punk_Green_Swatch",
"swatch_guid": "0724b166-9ace-46ff-a191-b13f135b00a8",
"color": "Green",
"item_name": "(Wrist_Punk)",
"item_guid": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7",
"item_outfit_type": 200,
"mask_guid": "dd7472ed-bdcb-4811-81ba-5ed682fb7675"
},
{
"swatch_name": "ArcherRed_Glove_Swatch",
"swatch_guid": "0c433086-9501-48c4-8d0e-4474eb420c61",
"color": "Red_Glove",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Lacrosse_Stripe_Purple_Swatch",
"swatch_guid": "e3454bc7-c88d-4958-8b81-db3fe5472a1e",
"color": "Stripe_Purple",
"item_name": "(Shirt_Lacrosse)",
"item_guid": "39278302-b52a-48a8-a566-ed756a5fa1a9",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Hat_Baseball_Logo_Goblins_Swatch",
"swatch_guid": "pqLkLbri9Eu48eryQIFGMg",
"color": "Logo_Goblins",
"item_name": "(Hat_Baseball)",
"item_guid": "14ef6b00-debf-4a85-9755-b4d37df496d3",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Letterman_Wrist_Green_Swatch",
"swatch_guid": "UUyjVU4mvECPhI9xTMYR1Q",
"color": "Wrist_Green",
"item_name": "(Wrist_Letterman)",
"item_guid": "2dFMy4zc7ky_Fd5fzfgInw",
"item_outfit_type": 200,
"mask_guid": "4EHIyZkOOEqnM-PuD_a5xw"
},
{
"swatch_name": "Winter_Torso_Tree_Swatch",
"swatch_guid": "XOBIPrKCr02xa4tKj8wN9g",
"color": "Torso_Tree",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": "Too5jpb1w0iQenkjRRS-QA"
},
{
"swatch_name": "ArcherSRank_Quiver_Swatch",
"swatch_guid": "29EZ6r2SsUakWjzuevxeoA",
"color": "SRank_Quiver",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Headphones_TwoOrange_Swatch",
"swatch_guid": "nX7krD8e2km_ZlOLWcoxdQ",
"color": "TwoOrange",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "Headphones_TwoPurple_Swatch",
"swatch_guid": "OqsbZ8Xls0i8uA6ha7H3qQ",
"color": "TwoPurple",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "Wizard_Green_Swatch",
"swatch_guid": "357eb160-360b-4a63-840d-65d11cc8ffc2",
"color": "Green",
"item_name": "(Hat_Wizard)",
"item_guid": "01689baf-405e-4b38-9f09-b624e03865b8",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "MagicalGirl_Wrist_Green_Swatch",
"swatch_guid": "1FcyrVGyGEC9dmOq_51nFQ",
"color": "Wrist_Green",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "ZtCqMnFTQUWv2h0MfZ4m6A"
},
{
"swatch_name": "Scuba_Shirt_Contest_Swatch",
"swatch_guid": "J-K34UX_B0yObjQI9tn0vQ",
"color": "Shirt_Contest",
"item_name": "(Shirt_Scuba)",
"item_guid": "57552652-d902-43b7-a68e-5c7da87582bc",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "PeaCoat_Red_Swatch",
"swatch_guid": "jXBi28cRV0CEQgKIrpD0_A",
"color": "Red",
"item_name": "(Shirt_Peacoat)",
"item_guid": "ab15ef15-a97c-4ab1-896b-bd4d7e1cd9f5",
"item_outfit_type": 101,
"mask_guid": "AZnPWCN41kmNwFwDqWGFWQ"
},
{
"swatch_name": "Angler_Gray_Swatch",
"swatch_guid": "97938f4c-52b8-4c85-8575-856654666316",
"color": "Gray",
"item_name": "(Hat_Angler)",
"item_guid": "03f8c394-28fa-4087-978b-8d108f0bd969",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "80sSpandex_Yellow_Swatch",
"swatch_guid": "i1XbjBqTu0CILIuJClWimA",
"color": "Yellow",
"item_name": "(Shirt_80sSpandex)",
"item_guid": "17236d76-e3b6-42f6-9179-6fcf0479436f",
"item_outfit_type": 101,
"mask_guid": "B-sIpWNOfEigMeQF-WVR1g"
},
{
"swatch_name": "FootballHelmet_Dark_Swatch",
"swatch_guid": "61ff7a02-50ff-49ea-b95e-c31b95a30c8e",
"color": "Helmet_Dark",
"item_name": "(Hat_FootBall)",
"item_guid": "5a38f864-7d1c-4989-8558-40b9668f78ef",
"item_outfit_type": 0,
"mask_guid": "7660e11a-7ef0-43d3-bc02-76b3a4aab064"
},
{
"swatch_name": "LaserTagVisor_Cyan_Swatch",
"swatch_guid": "426d0592-f8e4-4bc6-9fe1-a62b91fd57d5",
"color": "Visor_Cyan",
"item_name": "(Hat_LaserTag)",
"item_guid": "123de433-a005-4994-8a11-d2d84ad66b11",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "TruckerOveralls_Green_Swatch",
"swatch_guid": "EiGgN7uVukqoRE7UJcMLTw",
"color": "Green",
"item_name": "(Shirt_TruckerOveralls)",
"item_guid": "b080e2f4-c699-4cca-bb9e-7cfc4bafcbfc",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Winter_Hat_Tree_Green_Swatch",
"swatch_guid": "mraJrTA4V0eARb_Kq7yYVw",
"color": "Hat_Tree_Green",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Shirt_VampireHunter_Leather_Swatch",
"swatch_guid": "Z8Bfk0MTb0evZehvvgzuBA",
"color": "Leather",
"item_name": "(Shirt_VampireHunter)",
"item_guid": "JWfs9IiQs0Weu8MmyGpqLQ",
"item_outfit_type": 101,
"mask_guid": "x10FeRBsQU2DIIpG8XIlwg"
},
{
"swatch_name": "CowboyLight_Swatch",
"swatch_guid": "5e00a832-3f25-4dd3-84d2-b6ed6a4b4c37",
"color": "Light",
"item_name": "(Hat_Cowboy)",
"item_guid": "ad0bdec5-f767-42e7-99e0-221473464a85",
"item_outfit_type": 0,
"mask_guid": "54778d93-2cff-48db-b25b-65c08b0e5be8"
},
{
"swatch_name": "Hoodie_Red_Swatch",
"swatch_guid": "yM5xRrd9OkmDGyQddwOP6g",
"color": "Red",
"item_name": "(Shirt_Hoodie)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Winter_Torso_Tree_Green_Swatch",
"swatch_guid": "i33RmJJCwUem-ADkOwMNQQ",
"color": "Torso_Tree_Green",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Smuggler_Yellow_Swatch",
"swatch_guid": "002b07bb-4256-44ce-8ca4-25510e70b6b5",
"color": "Yellow",
"item_name": "(Shirt_Smuggler)",
"item_guid": "25d4c652-1730-4925-925e-31a290a2272a",
"item_outfit_type": 101,
"mask_guid": "970a8c7e-d091-4719-bb9c-e3be8d46ee68"
},
{
"swatch_name": "BlazerMidnightBlue_Swatch",
"swatch_guid": "6f2e74bf-1e95-463d-97db-d5d1a53b2c28",
"color": "MidnightBlue",
"item_name": "(Blazer)",
"item_guid": "de0ac50d-2adb-4114-bd2e-68953b13d706",
"item_outfit_type": 101,
"mask_guid": "be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54"
},
{
"swatch_name": "MagicalGirl_Wrist_Blue_Swatch",
"swatch_guid": "zaSTWOUy_k6cnoVVYzypWg",
"color": "Wrist_Blue",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "ZtCqMnFTQUWv2h0MfZ4m6A"
},
{
"swatch_name": "Hoodie_White_Swatch",
"swatch_guid": "-Nf7WM2TeEKujB637h3tRA",
"color": "White",
"item_name": "(Shirt_Hoodie)",
"item_guid": "b6rLwzD4NkKV7xKn9ZYVkA",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "Turtleneck_Yellow_Swatch", "swatch_name": "Turtleneck_Yellow_Swatch",
"swatch_guid": "G4HIPUjekUigiU0-Kr9mdA", "swatch_guid": "G4HIPUjekUigiU0-Kr9mdA",
@@ -2097,6 +2781,177 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Mobster_WhiteHatHacker_Swatch",
"swatch_guid": "40283127-2ffc-4989-82f6-29c3381bf9e2",
"color": "WhiteHatHacker",
"item_name": "(Wrist_Mobster)",
"item_guid": "08d56627-4500-4724-8692-a23c96ddb3ec",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "QueenCrown_White_Swatch",
"swatch_guid": "Zljmtj5WqUaRvB3XQTgJXg",
"color": "White",
"item_name": "(Hat_QueenCrown)",
"item_guid": "9d395a84-4342-4b1c-940c-213ec2eb56f9",
"item_outfit_type": 0,
"mask_guid": "XFuSiGupZ02Fut1Dxum_Iw"
},
{
"swatch_name": "Mobster_Hat_Gray_Swatch",
"swatch_guid": "390a476a-5545-48eb-955c-1bf1a16012bf",
"color": "Hat_Gray",
"item_name": "(Wrist_Mobster)",
"item_guid": "08d56627-4500-4724-8692-a23c96ddb3ec",
"item_outfit_type": 200,
"mask_guid": "d759c88d-a159-4c26-86dd-b054bb05b3ed"
},
{
"swatch_name": "SpaceMarine_Light_Swatch",
"swatch_guid": "68273c9e-96d6-4ddb-aaab-d99229e864d5",
"color": "Light",
"item_name": "(Shirt_SpaceMarine)",
"item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 101,
"mask_guid": "7ac81d60-05b8-4a85-982c-f69213a93818"
},
{
"swatch_name": "50sDress_Shamrock_Swatch",
"swatch_guid": "K17dIz_TRUq5D50NBfLjyw",
"color": "Shamrock",
"item_name": "(Shirt_50sDress)",
"item_guid": "439d1e24-d67f-4b8a-8fb3-8bf589d5a8ee",
"item_outfit_type": 101,
"mask_guid": "Rk18_7icF0Srgh7M1_G-hA"
},
{
"swatch_name": "MagicalGirl_Hat_Green_Swatch",
"swatch_guid": "kLv_wsbbGUmlfIUO1QLYLw",
"color": "Hat_Green",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "mG60EPIQIkaJNgOkonVmZg"
},
{
"swatch_name": "Aviator_Jacket_Black_Swatch",
"swatch_guid": "da185fbb-ad2b-4f30-87a2-491f24b78ebb",
"color": "Jacket_Black",
"item_name": "(Wrist_Aviator)",
"item_guid": "125a3a62-1005-47dc-9fd8-ee09ea803e9f",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "RedFlowerDress_Yellow_Swatch",
"swatch_guid": "e98c3f98-7844-4d6e-a21c-05027033c18c",
"color": "Yellow",
"item_name": "(RedFlowerDress)",
"item_guid": "515dfc36-7f7b-47bc-abee-380d68f41be6",
"item_outfit_type": 101,
"mask_guid": "2d7361d2-31a2-404b-90da-10e89114e1f2"
},
{
"swatch_name": "CollarShirt_Torso_Pink_Swatch",
"swatch_guid": "kmj5zOjcwku_WWKroCeiVQ",
"color": "Torso_Pink",
"item_name": "(CollarShirt)",
"item_guid": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "MagicalGirl_Neck_Green_Swatch",
"swatch_guid": "i3rD_hnQG0a-5f6Ve4AfWg",
"color": "Neck_Green",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "M5z2wk-GDkOAjvZzqsfcNQ"
},
{
"swatch_name": "Winter_Torso_Red_Swatch",
"swatch_guid": "LDqcPkvNXE2gFXxbpNkfhA",
"color": "Torso_Red",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": "Too5jpb1w0iQenkjRRS-QA"
},
{
"swatch_name": "Wizard_Black_Swatch",
"swatch_guid": "7afe1bb0-5ca9-4263-8f85-49d3a18353c7",
"color": "Black",
"item_name": "(Hat_Wizard)",
"item_guid": "01689baf-405e-4b38-9f09-b624e03865b8",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "TruckerJacket_Black_Swatch",
"swatch_guid": "P9bBSL0YLEyjN8PxMfgbKQ",
"color": "Black",
"item_name": "(Shirt_TruckerJacket)",
"item_guid": "8b065e2a-b106-4f11-b415-4c47ce87993e",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "FlowerSwimsuit_Gray_Swatch",
"swatch_guid": "5a96ac4d-03c3-4214-9dcb-c2044ff28f2e",
"color": "Gray",
"item_name": "(FlowerSwimsuit)",
"item_guid": "f41653f9-ae02-443c-aaf4-1c6f4e49035d",
"item_outfit_type": 101,
"mask_guid": "3248a3ec-50d1-4c0a-96ae-25eb9d8ed622"
},
{
"swatch_name": "PirateCaptain_SRank_Swatch",
"swatch_guid": "xGmnUGF-Ck-jP43r8tVVjg",
"color": "SRank",
"item_name": "(Neck_PirateCaptain)",
"item_guid": "38b38e24-d636-41c6-b80e-a285ae3106dc",
"item_outfit_type": 102,
"mask_guid": ""
},
{
"swatch_name": "Mobster_PinStripe_Purple_Swatch",
"swatch_guid": "newGzX_zA0eGUX3Iayq5Ag",
"color": "PinStripe_Purple",
"item_name": "(Wrist_Mobster)",
"item_guid": "08d56627-4500-4724-8692-a23c96ddb3ec",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "BabydollDress_Pink_Swatch",
"swatch_guid": "da6d0ced-3d43-4878-a868-925f2ed6fd5b",
"color": "Pink",
"item_name": "(BabydollDress)",
"item_guid": "da55bfc3-47b7-46f6-b548-7fb2c2a5e0bf",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Soccer_Shirt_Blue_Swatch",
"swatch_guid": "OnQeVxdstkS2zGxLBAuM3Q",
"color": "Shirt_Blue",
"item_name": "(Wrist_Soccer)",
"item_guid": "94eb7ef3-528f-4f59-92a0-48b0ec287527",
"item_outfit_type": 200,
"mask_guid": "i8bvbGAKREmAEbjKO-geOQ"
},
{
"swatch_name": "CollarShirt_Torso_GoldYellow_Swatch",
"swatch_guid": "jGj28vhq8EGwP2RuM074aQ",
"color": "Torso_GoldYellow",
"item_name": "(CollarShirt)",
"item_guid": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "Turtleneck_Green_Swatch", "swatch_name": "Turtleneck_Green_Swatch",
"swatch_guid": "Jt0QqNrAp0qzUR_RQoivVg", "swatch_guid": "Jt0QqNrAp0qzUR_RQoivVg",
@@ -2106,6 +2961,123 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "SodaJerk_Blue_Swatch",
"swatch_guid": "XFCpIdeDXUqeYCIgh4fGtg",
"color": "Blue",
"item_name": "(Shirt_SodaJerk)",
"item_guid": "iai6P7dDTUq8S4hqQmY1YA",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "PirateCaptain_Red_Swatch",
"swatch_guid": "556141f7-6fbc-4065-9faa-8d8240a93d7f",
"color": "Red",
"item_name": "(Neck_PirateCaptain)",
"item_guid": "38b38e24-d636-41c6-b80e-a285ae3106dc",
"item_outfit_type": 102,
"mask_guid": ""
},
{
"swatch_name": "Steampunk_Femme_Contest_Swatch",
"swatch_guid": "A6VnGtpY9Ey0atf3zaLo3g",
"color": "e_Contest",
"item_name": "(Hat_SteampunkFemme)",
"item_guid": "JWanxvOHyESg9F_HlmTOfA",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "BowlingShirt_Flames_Pink_Swatch",
"swatch_guid": "FC8ZFBPt4EyTe7IYbv2Tpg",
"color": "Shirt_Flames_Pink",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "5RUG6i2Mx0-55IO-IlhUdQ"
},
{
"swatch_name": "Luchador_Blue_Swatch",
"swatch_guid": "0217fd2e-ff9d-49f7-9bc8-8d0d6b8bf250",
"color": "Blue",
"item_name": "(Hat_Luchador)",
"item_guid": "1596c113-aa3a-4f5c-b398-e5c3575548e7",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Jazzercise_Yellow_Swatch",
"swatch_guid": "78eb0969-f246-4cac-acd6-295df7c092e4",
"color": "Yellow",
"item_name": "(Hair_Jazzercise)",
"item_guid": "49397e16-1027-4286-9bc4-b59eae565ff0",
"item_outfit_type": 2,
"mask_guid": ""
},
{
"swatch_name": "Countess_Purple_Swatch",
"swatch_guid": "IzkFARlv6EenmrfcNkcOuw",
"color": "Purple",
"item_name": "(Shirt_Countess)",
"item_guid": "QCtqxbvYOkan79YFugH_Cw",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Harness_Hat_Orange_Swatch",
"swatch_guid": "Euf1h1qsZ02evDsyoHYbPQ",
"color": "Hat_Orange",
"item_name": "(Shirt_Harness)",
"item_guid": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af",
"item_outfit_type": 101,
"mask_guid": "PZHohDVX0U2frX13N4KYDQ"
},
{
"swatch_name": "LaserTag_Green_Swatch",
"swatch_guid": "2Z_FxSPiyUODtlFZWF9DFQ",
"color": "Green",
"item_name": "(Hat_LaserTag)",
"item_guid": "123de433-a005-4994-8a11-d2d84ad66b11",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Golfer_Glove_Grey_Swatch",
"swatch_guid": "72a010c0-bddd-4e5d-bca6-9821d82de9a6",
"color": "Glove_Grey",
"item_name": "(Hat_Golfer)",
"item_guid": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54",
"item_outfit_type": 0,
"mask_guid": "f4cea991-fc3d-4715-bf82-8a8fb60e8799"
},
{
"swatch_name": "HeadBackBow_Hearts_Swatch",
"swatch_guid": "D-SngK7hhkOpoZ2aMWv9EA",
"color": "Hearts",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": "3DdSzSqV1UG5AtPr8ro3Kg"
},
{
"swatch_name": "TruckerJacket_Olive_Swatch",
"swatch_guid": "bgRKH1wfqkWVHcuSnFYnlQ",
"color": "Olive",
"item_name": "(Shirt_TruckerJacket)",
"item_guid": "8b065e2a-b106-4f11-b415-4c47ce87993e",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "BlazerSeaDark_Swatch",
"swatch_guid": "0ffad843-d6c9-425a-8686-7217009c867e",
"color": "SeaDark",
"item_name": "(Blazer)",
"item_guid": "de0ac50d-2adb-4114-bd2e-68953b13d706",
"item_outfit_type": 101,
"mask_guid": "be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54"
},
{ {
"swatch_name": "Turtleneck_Red_Swatch", "swatch_name": "Turtleneck_Red_Swatch",
"swatch_guid": "lBePHNL9LkyaAjEFyFE8Sw", "swatch_guid": "lBePHNL9LkyaAjEFyFE8Sw",
@@ -2116,21 +3088,273 @@
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "Turtleneck_Orange_Swatch", "swatch_name": "HockeyShirt_Teal_Swatch",
"swatch_guid": "9b9t8_aKH0-gQXMhZycaOA", "swatch_guid": "fcbdfbfd-254a-4538-b119-43e23404b8b5",
"color": "Teal",
"item_name": "(HockeyShirt)",
"item_guid": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c",
"item_outfit_type": 101,
"mask_guid": "e5635772-4e84-4b30-b77f-4a1196cb599b"
},
{
"swatch_name": "Unicorn_Glowing_Swatch",
"swatch_guid": "mudt-Z5-5USdnIq-xaDgOw",
"color": "Glowing",
"item_name": "(Hat_Unicorn)",
"item_guid": "2caHvVQ1vECrPv-YcdxdyA",
"item_outfit_type": 0,
"mask_guid": "nW_fBsuZA0OfIjTMXm6ZCA"
},
{
"swatch_name": "MagicalGirl_Orange_Swatch",
"swatch_guid": "hYu9itGe1Ee5Neanhj9VtQ",
"color": "Orange", "color": "Orange",
"item_name": "(Shirt_Turtleneck)", "item_name": "(Wrist_MagicalGirl)",
"item_guid": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285", "item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "ArcherTan_Quiver_Swatch",
"swatch_guid": "d1eb3573-672c-4bb4-9ec5-7c81e6a38d3d",
"color": "Tan_Quiver",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
"swatch_name": "PB_Trucker_Red_Swatch", "swatch_name": "Punk_WristGreen_Swatch",
"swatch_guid": "lDXambcP60CeY2C8qeGDFw", "swatch_guid": "a6796264-9c7e-4605-830c-06db8d0dc780",
"color": "Red", "color": "WristGreen",
"item_name": "(Hat_PB_TruckerHat)", "item_name": "(Wrist_Punk)",
"item_guid": "0c537a84-9095-406f-8c87-1464f3a774ba", "item_guid": "28374ebc-4050-4ea0-b3b6-41ebe75cedf7",
"item_outfit_type": 200,
"mask_guid": "dd7472ed-bdcb-4811-81ba-5ed682fb7675"
},
{
"swatch_name": "Headphones_EmissivePurple_Swatch",
"swatch_guid": "3PfP661Fg0CruzRxis9EiA",
"color": "EmissivePurple",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0, "item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "Harness_Hat_Black_Swatch",
"swatch_guid": "JVZHZm6nGEqE2zEEPoHbwA",
"color": "Hat_Black",
"item_name": "(Shirt_Harness)",
"item_guid": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af",
"item_outfit_type": 101,
"mask_guid": "PZHohDVX0U2frX13N4KYDQ"
},
{
"swatch_name": "HeadBackBow_SolidBlue_Swatch",
"swatch_guid": "8iZO162KNUiMC8l_z3OC_A",
"color": "SolidBlue",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": "3DdSzSqV1UG5AtPr8ro3Kg"
},
{
"swatch_name": "Firefighter_Hat_White_Swatch",
"swatch_guid": "891d902e-3257-4a8c-9001-858046007997",
"color": "Hat_White",
"item_name": "(Hat_Firefighter)",
"item_guid": "bb54e0e9-5d90-41f3-94c0-8e82f1791af1",
"item_outfit_type": 0,
"mask_guid": "c8ccb85e-52cd-4b3b-aadf-289819067125"
},
{
"swatch_name": "Scallywag_Hat_Red_Swatch",
"swatch_guid": "c3c9f89c-580c-43c4-b38b-07394044e0e1",
"color": "Hat_Red",
"item_name": "(Shirt_Scallywag)",
"item_guid": "664485e9-a159-4fcb-b32c-2b114b4a1218",
"item_outfit_type": 101,
"mask_guid": "a4903da3-249d-4f0a-a746-b547fda371ff"
},
{
"swatch_name": "HeadBackBow_SolidPink_Swatch",
"swatch_guid": "FUP4iRnoc0ikNmjrFD06lg",
"color": "SolidPink",
"item_name": "(Hat_HeadBackBow)",
"item_guid": "JTrfBXhIT02o2-ssP94zew",
"item_outfit_type": 0,
"mask_guid": "3DdSzSqV1UG5AtPr8ro3Kg"
},
{
"swatch_name": "SpaceMarine_Dark_Swatch",
"swatch_guid": "a089bcc6-798d-4777-98e2-7afd290dc6b6",
"color": "Dark",
"item_name": "(Shirt_SpaceMarine)",
"item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 101,
"mask_guid": "7ac81d60-05b8-4a85-982c-f69213a93818"
},
{
"swatch_name": "Headphones_TwoBlack_Swatch",
"swatch_guid": "ns5tIRishUOp-nYqLedcwA",
"color": "TwoBlack",
"item_name": "(Hat_Headphones)",
"item_guid": "UcPo9SsF3EGTCBB2J-Efdw",
"item_outfit_type": 0,
"mask_guid": "_Yq00H-NWUyQNvGxRunHbg"
},
{
"swatch_name": "Wrist_VampireHunter_Black_Swatch",
"swatch_guid": "Vxq6v55a_kGPODzwB5MC1g",
"color": "Black",
"item_name": "(Wrist_VampireHunter)",
"item_guid": "_OWVy3z6iU-M3-zbQgSLig",
"item_outfit_type": 200,
"mask_guid": "N5wN1l0tYEau5np-9PoIiA"
},
{
"swatch_name": "Blazer_Shamrock_Swatch",
"swatch_guid": "7WrSbK4Ml0K2z-ig5HIUCw",
"color": "Shamrock",
"item_name": "(Blazer)",
"item_guid": "de0ac50d-2adb-4114-bd2e-68953b13d706",
"item_outfit_type": 101,
"mask_guid": "be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54"
},
{
"swatch_name": "MagicalGirl_Hat_Red_Swatch",
"swatch_guid": "EDLxuGjEVEGdOPXDqyN-xA",
"color": "Hat_Red",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "mG60EPIQIkaJNgOkonVmZg"
},
{
"swatch_name": "DrumMajorHat_Green_Swatch",
"swatch_guid": "5a614f97-ef7f-4b70-afff-9ed837a86407",
"color": "Hat_Green",
"item_name": "(Hat_DrumMajor)",
"item_guid": "1a028850-64ee-40ab-b547-ebbcfa1b04b3",
"item_outfit_type": 0,
"mask_guid": "a8a91216-0fcc-461e-a682-95ee82509cce"
},
{
"swatch_name": "Golfer_Hat_Blue_Swatch",
"swatch_guid": "6584dada-499e-4d7c-958d-466955b20640",
"color": "Hat_Blue",
"item_name": "(Hat_Golfer)",
"item_guid": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54",
"item_outfit_type": 0,
"mask_guid": "ddf429fd-a883-48a5-869b-bc12e44d30bf"
},
{
"swatch_name": "Harness_Blue_Swatch",
"swatch_guid": "RfZNDPUok0WyQEa3Ko08ow",
"color": "Blue",
"item_name": "(Shirt_Harness)",
"item_guid": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "ArcherSRank_Swatch",
"swatch_guid": "lgqLGi01o02ZFC9Xs5Ofdg",
"color": "SRank",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "ArcherRed_Swatch",
"swatch_guid": "5b5281c9-b795-4bb2-ba7b-ba52047ebe3f",
"color": "Red",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Wrist_VampireHunter_Leather_Swatch",
"swatch_guid": "D0tP58wC30mJ00vNxw8hqA",
"color": "Leather",
"item_name": "(Wrist_VampireHunter)",
"item_guid": "_OWVy3z6iU-M3-zbQgSLig",
"item_outfit_type": 200,
"mask_guid": "N5wN1l0tYEau5np-9PoIiA"
},
{
"swatch_name": "SpaceMarine_SRank_Swatch",
"swatch_guid": "4ElTihrMA0ipcgeb9UG01Q",
"color": "SRank",
"item_name": "(Shirt_SpaceMarine)",
"item_guid": "4ce7a70f-15a0-4cad-98ee-5cbb40827338",
"item_outfit_type": 101,
"mask_guid": "7ac81d60-05b8-4a85-982c-f69213a93818"
},
{
"swatch_name": "Jackfrost_Shirt_Lilac_Swatch",
"swatch_guid": "prEZHWvOM0ysaNfAe06ywA",
"color": "Shirt_Lilac",
"item_name": "(Shirt_JackFrost)",
"item_guid": "105a5100-cb57-410f-a26b-1ee21002f067",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "BusinessTie_CandyCane_Swatch",
"swatch_guid": "W4a144so90eRzCojUnDowA",
"color": "CandyCane",
"item_name": "(BusinessTie)",
"item_guid": "c6c08eb5-381a-4193-9722-80da95d62abe",
"item_outfit_type": 100,
"mask_guid": ""
},
{
"swatch_name": "MagicalGirl_Hat_Orange_Swatch",
"swatch_guid": "f0pjsV0XMUWoE_z2kqOlSA",
"color": "Hat_Orange",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "mG60EPIQIkaJNgOkonVmZg"
},
{
"swatch_name": "BowlingShirt_CenterStripe_Yellow_Swatch",
"swatch_guid": "XrSL-v2MbkK0md-FMOweOA",
"color": "Shirt_CenterStripe_Yellow",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "IWg9wuda4kaCilJk8JQpTQ"
},
{
"swatch_name": "CollarShirt_Torso_Plaid_Red_Swatch",
"swatch_guid": "ojvIFCTpJkeegestF065wA",
"color": "Torso_Plaid_Red",
"item_name": "(CollarShirt)",
"item_guid": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343",
"item_outfit_type": 101,
"mask_guid": "10fYSIBytk-yUjveDa407w"
},
{
"swatch_name": "Witch_Contest_Wrist_Swatch",
"swatch_guid": "Hk6svQ5Sr0GDKLxOyktk9w",
"color": "Contest_Wrist",
"item_name": "(Shirt_Witch)",
"item_guid": "7LSm-xCYakmgcAr--Fn-JA",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "WitchHunter_White_Swatch",
"swatch_guid": "e02838f8-4029-455a-b35c-dd40f972b05b",
"color": "White",
"item_name": "(Shirt_WitchHunter)",
"item_guid": "418db0e0-c7af-40c2-9cac-d461ce41e210",
"item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{ {
@@ -2142,6 +3366,51 @@
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "" "mask_guid": ""
}, },
{
"swatch_name": "Countess_Green_Wrist_Swatch",
"swatch_guid": "Wj7I2YiVoE6zp8J_HYs8tw",
"color": "Green_Wrist",
"item_name": "(Shirt_Countess)",
"item_guid": "QCtqxbvYOkan79YFugH_Cw",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "CollarShirt_Torso_GreenFlower_Swatch",
"swatch_guid": "EfdMcnfHt0mr0PQ_maaYOg",
"color": "Torso_GreenFlower",
"item_name": "(CollarShirt)",
"item_guid": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "ArcherRed_Quiver_Swatch",
"swatch_guid": "7221132e-053a-4c58-b719-ed3c8558907d",
"color": "Red_Quiver",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Winter_Scarf_Tree_Swatch",
"swatch_guid": "OrqqGMhK1UKGowFxpJi2mw",
"color": "Scarf_Tree",
"item_name": "(Hat_Winter)",
"item_guid": "24c4404a-839f-46bd-98b6-6848d747a98b",
"item_outfit_type": 0,
"mask_guid": "dUu7b-JFOky9c2lIVCNmcg"
},
{
"swatch_name": "ArcherGray_Glove_Swatch",
"swatch_guid": "8hUBfmq28EixYjv92Xcrgg",
"color": "Gray_Glove",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{ {
"swatch_name": "Tennis_Pink_Swatch", "swatch_name": "Tennis_Pink_Swatch",
"swatch_guid": "304e5cf1-5c94-481f-8e49-e6a943e6b300", "swatch_guid": "304e5cf1-5c94-481f-8e49-e6a943e6b300",
@@ -2150,497 +3419,475 @@
"item_guid": "74b91f2b-d4d3-4e41-8946-b1296ab7a773", "item_guid": "74b91f2b-d4d3-4e41-8946-b1296ab7a773",
"item_outfit_type": 101, "item_outfit_type": 101,
"mask_guid": "90e0da8b-1941-4ca8-b7e5-b4e31921873c" "mask_guid": "90e0da8b-1941-4ca8-b7e5-b4e31921873c"
},
{
"swatch_name": "BowlingShirt_Diamonds_Blue_Swatch",
"swatch_guid": "KqjQ1xRpokGLKp9GVN1EFA",
"color": "Shirt_Diamonds_Blue",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "VO_3cgqQqUOTz30TVUxrFA"
},
{
"swatch_name": "Scarecrow_Shirt_Contest_Swatch",
"swatch_guid": "S0AKRB_m2k-I7snHnUigtw",
"color": "Shirt_Contest",
"item_name": "(Shirt_Scarecrow)",
"item_guid": "1fcaf4fd-9012-407e-b691-fbc3ccf22421",
"item_outfit_type": 101,
"mask_guid": "uTkbswCedUGyuZcaX0W6Ww"
},
{
"swatch_name": "Sherlock_Hat_Black_Swatch",
"swatch_guid": "fEPwvuJCWkGBe4IekXpmLw",
"color": "Hat_Black",
"item_name": "(Hat_Sherlock)",
"item_guid": "5bde836f-9343-4934-b01a-606f707020ef",
"item_outfit_type": 0,
"mask_guid": "axPBGoqorEe77Oqqjfzb5w"
},
{
"swatch_name": "ElectricianTorso_Beige_Swatch",
"swatch_guid": "5fb2e235-8c67-4335-b432-edb10db2b139",
"color": "Torso_Beige",
"item_name": "(Hat_Electrician)",
"item_guid": "34654303-9760-447f-8614-1c26506db994",
"item_outfit_type": 0,
"mask_guid": "daa38a31-fb7b-4e26-a2f5-728d6dcec81a"
},
{
"swatch_name": "Letterman_Wrist_Black_Swatch",
"swatch_guid": "Y0e7o0_BKUm7XJSLediN5w",
"color": "Wrist_Black",
"item_name": "(Wrist_Letterman)",
"item_guid": "2dFMy4zc7ky_Fd5fzfgInw",
"item_outfit_type": 200,
"mask_guid": "4EHIyZkOOEqnM-PuD_a5xw"
},
{
"swatch_name": "LaserTag_Orange_Swatch",
"swatch_guid": "jkuw9EhCpEKn-2lKJXWd3Q",
"color": "Orange",
"item_name": "(Hat_LaserTag)",
"item_guid": "123de433-a005-4994-8a11-d2d84ad66b11",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "Golfer_Hat_Grey_Swatch",
"swatch_guid": "48c18418-01dd-40ff-9dfe-c4cf5f4e1245",
"color": "Hat_Grey",
"item_name": "(Hat_Golfer)",
"item_guid": "2faa26b6-f7c3-4b0d-8f70-1f9454e64a54",
"item_outfit_type": 0,
"mask_guid": "ddf429fd-a883-48a5-869b-bc12e44d30bf"
},
{
"swatch_name": "Soccer_Glove_Blue_Swatch",
"swatch_guid": "whEHo6Rhr0CxsDBsBD32WA",
"color": "Glove_Blue",
"item_name": "(Wrist_Soccer)",
"item_guid": "94eb7ef3-528f-4f59-92a0-48b0ec287527",
"item_outfit_type": 200,
"mask_guid": "av4PwLz6YkmpBRImQZgn4Q"
},
{
"swatch_name": "FurCoat_White_Swatch",
"swatch_guid": "643b5f2d-0666-4869-b699-03742de34d16",
"color": "White",
"item_name": "(FurCoat)",
"item_guid": "77c37481-a185-47ff-af55-65f9fdfcf9e5",
"item_outfit_type": 101,
"mask_guid": "2c0d912d-c184-4eab-8ee4-73b90ba7b0a6"
},
{
"swatch_name": "Hat_SeaCaptainDarkSilver_Swatch",
"swatch_guid": "36e63797-5a42-4779-a5cc-ef5c2e7d17d4",
"color": "DarkSilver",
"item_name": "(Hat_SeaCaptain)",
"item_guid": "f5c9743f-e77c-42f2-ba63-9843342cbf8e",
"item_outfit_type": 0,
"mask_guid": "a4e51b6b-fdfd-47dc-9d0e-f26f383e03e9"
},
{
"swatch_name": "HockeyShirt_Purple_Swatch",
"swatch_guid": "d5efaf80-7f34-4c9a-bc3a-b80b1ac70ace",
"color": "Purple",
"item_name": "(HockeyShirt)",
"item_guid": "d6b0a1e7-e918-43e5-8191-3a8d9d01df7c",
"item_outfit_type": 101,
"mask_guid": "e5635772-4e84-4b30-b77f-4a1196cb599b"
},
{
"swatch_name": "Steampunk_Contest_Hat_Swatch",
"swatch_guid": "AAbGHOOitUu9enweOXbrkw",
"color": "Contest_Hat",
"item_name": "(Shirt_SteamPunk)",
"item_guid": "E9VTiIVFPEa-OuQRto0e_Q",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "ArcherYellow_Quiver_Swatch",
"swatch_guid": "e548124c-d7f3-45de-aaa0-f66130eca576",
"color": "Yellow_Quiver",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "LaserTagGlove_Green_Swatch",
"swatch_guid": "mhsglVEtREmElF5wuLw3NQ",
"color": "Green",
"item_name": "(Wrist_LaserTagGlove)",
"item_guid": "3044adce-90c0-4f66-81f5-39c4fca86fdf",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "ArcherTan_Swatch",
"swatch_guid": "9b35ae52-febe-486a-8557-ec7190a9756e",
"color": "Tan",
"item_name": "(Shirt_Archer)",
"item_guid": "c0e59f27-a13d-4bd9-b3b8-a38a360f649b",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "BowlingShirt_Flames_Blue_Swatch",
"swatch_guid": "032pE8s9Ik-2CImFVDrSqg",
"color": "Shirt_Flames_Blue",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "5RUG6i2Mx0-55IO-IlhUdQ"
},
{
"swatch_name": "Turtleneck_Orange_Swatch",
"swatch_guid": "9b9t8_aKH0-gQXMhZycaOA",
"color": "Orange",
"item_name": "(Shirt_Turtleneck)",
"item_guid": "7e22a4bd-3e87-4d50-8843-9e2e3cf45285",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "MagicalGirl_Neck_Blue_Swatch",
"swatch_guid": "Ot9EG-FfKEauT0u6pSZuWg",
"color": "Neck_Blue",
"item_name": "(Wrist_MagicalGirl)",
"item_guid": "eS_or4AnMka9M_bD7YN28g",
"item_outfit_type": 200,
"mask_guid": "M5z2wk-GDkOAjvZzqsfcNQ"
},
{
"swatch_name": "WitchHunter_Red_Swatch",
"swatch_guid": "909ed249-ce09-4304-8b02-56794fa7567d",
"color": "Red",
"item_name": "(Shirt_WitchHunter)",
"item_guid": "418db0e0-c7af-40c2-9cac-d461ce41e210",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Countess_Green_Swatch",
"swatch_guid": "LyVf9lnzdku1F4iPBGqk-g",
"color": "Green",
"item_name": "(Shirt_Countess)",
"item_guid": "QCtqxbvYOkan79YFugH_Cw",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Shirt_TutorialGown1_Purple_Swatch",
"swatch_guid": "U2ZbHn8WJEySUXVpzAY6ig",
"color": "Purple",
"item_name": "(Shirt_TutorialGown_1)",
"item_guid": "ASHnWS0h9kOJhbOUwxWKFQ",
"item_outfit_type": 101,
"mask_guid": "6nmHu9zj8k-tEz0tBHblnQ"
},
{
"swatch_name": "BowlingShirt_OneStripe_Blue_Swatch",
"swatch_guid": "GDvaUU10-EKh3pKUQ9tnag",
"color": "Shirt_OneStripe_Blue",
"item_name": "(Shirt_Bowling)",
"item_guid": "CQuKKNpan0O7rhR1fr14-Q",
"item_outfit_type": 101,
"mask_guid": "b-IM3eYMJUa40395O5XeLg"
},
{
"swatch_name": "Scuba_Neck_Contest_Swatch",
"swatch_guid": "GWi1rEebqkCV_TS2WzQhcA",
"color": "Neck_Contest",
"item_name": "(Shirt_Scuba)",
"item_guid": "57552652-d902-43b7-a68e-5c7da87582bc",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Dodgeball_Black_Swatch",
"swatch_guid": "4a85c181-92fe-47a5-925e-05f0ccb25006",
"color": "Black",
"item_name": "(Shirt_Dodgeball)",
"item_guid": "d3d69bee-60bc-44a3-ad7c-65b3b69d1e59",
"item_outfit_type": 101,
"mask_guid": "944d934d-5e21-42d7-8196-fff9a81ee469"
},
{
"swatch_name": "BlazerGrey_Swatch",
"swatch_guid": "272fe8eb-5061-4729-a7a8-414ff667a82f",
"color": "Grey",
"item_name": "(Blazer)",
"item_guid": "de0ac50d-2adb-4114-bd2e-68953b13d706",
"item_outfit_type": 101,
"mask_guid": "be2b9293-1d3c-4b1c-b4c5-fad3ab16cf54"
},
{
"swatch_name": "TennisSkirt_Red_Swatch",
"swatch_guid": "758752bd-db2f-43d2-b580-55b3e1efffd5",
"color": "Red",
"item_name": "(TennisSkirt)",
"item_guid": "21caa68e-c3fa-474c-af5e-af1e742b7a60",
"item_outfit_type": 101,
"mask_guid": "b75ef67d-00c3-4ac1-9b72-212032460294"
},
{
"swatch_name": "Windbreaker_Salmon_Swatch",
"swatch_guid": "JS7aa4QSUUOpUYPqRZEb5g",
"color": "Salmon",
"item_name": "(Wrist_80sWindbreaker)",
"item_guid": "52VsI_lKlkSUAOJlfDnyrQ",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "Wizard_Red_Swatch",
"swatch_guid": "a38254c0-00e3-429b-9d5d-b9e6a9812e69",
"color": "Red",
"item_name": "(Hat_Wizard)",
"item_guid": "01689baf-405e-4b38-9f09-b624e03865b8",
"item_outfit_type": 0,
"mask_guid": ""
},
{
"swatch_name": "FighterPilot_Gloves_Blue_Swatch",
"swatch_guid": "55b4e661-43e1-4708-82d5-2dd2bebd4f45",
"color": "Gloves_Blue",
"item_name": "(Wrist_FighterPilot)",
"item_guid": "5800d2b5-75b8-442f-8b10-63bd3d73a1b8",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "Letterman_Green_Swatch",
"swatch_guid": "apSQiQ1eO0mEzRsbugeu9w",
"color": "Green",
"item_name": "(Wrist_Letterman)",
"item_guid": "2dFMy4zc7ky_Fd5fzfgInw",
"item_outfit_type": 200,
"mask_guid": ""
},
{
"swatch_name": "ArtistHat_Blue_Swatch",
"swatch_guid": "14b43bf9-5ba0-484e-ae5d-55cfeb09a70d",
"color": "Hat_Blue",
"item_name": "(Shirt_Artist)",
"item_guid": "02077915-57d3-4e26-b4fe-8e0399cbb293",
"item_outfit_type": 101,
"mask_guid": "3485a30d-27f8-450b-b5aa-1ff3cd227d19"
},
{
"swatch_name": "WaitressApron_Green_Swatch",
"swatch_guid": "Blev_VFjRUysY_nymGWAVg",
"color": "Green",
"item_name": "(Belt_WaitressApron)",
"item_guid": "204bc384-e6d2-41b8-8ce9-0b26be5d85b7",
"item_outfit_type": 102,
"mask_guid": ""
},
{
"swatch_name": "CollarShirt_Torso_White_Swatch",
"swatch_guid": "MFrcSQ1DYUm8imvy4ypgvw",
"color": "Torso_White",
"item_name": "(CollarShirt)",
"item_guid": "2e59d8d0-91a0-4449-bfdc-a5d663fd9343",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Ranger_Hat_Green_Swatch",
"swatch_guid": "d0bb468c-b78f-4e80-916d-65fec3ec0010",
"color": "Hat_Green",
"item_name": "(Hat_Ranger)",
"item_guid": "f277af6a-0807-4716-8948-1bee236ffb74",
"item_outfit_type": 0,
"mask_guid": "0272eb19-fc0e-4ad6-8db9-cc0cda2528b0"
},
{
"swatch_name": "Scuba_Shirt_Blue_Swatch",
"swatch_guid": "VqtOYBiKRkaSW4xAlBxQgg",
"color": "Shirt_Blue",
"item_name": "(Shirt_Scuba)",
"item_guid": "57552652-d902-43b7-a68e-5c7da87582bc",
"item_outfit_type": 101,
"mask_guid": ""
},
{
"swatch_name": "Harness_Orange_Swatch",
"swatch_guid": "sX0ZZDcCUkyFSJISfvq0vg",
"color": "Orange",
"item_name": "(Shirt_Harness)",
"item_guid": "ca7a3ccb-2bc9-4c93-a6a8-6f22b38526af",
"item_outfit_type": 101,
"mask_guid": ""
} }
], ],
"unmatched": [ "unmatched": [
"Knight_SRank_Swatch",
"TShirt_SquadGoals_Swatch", "TShirt_SquadGoals_Swatch",
"TShirt_HangInThere_Swatch",
"TShirt_Orange_Swatch",
"ZebraWhite_Swatch", "ZebraWhite_Swatch",
"Winter_Gloves_Tree_Green_Swatch",
"Cyber_Trash_Yellow_Swatch", "Cyber_Trash_Yellow_Swatch",
"Cyber_Trash_Black_Swatch",
"PoofyDress_Purple_Swatch", "PoofyDress_Purple_Swatch",
"PoofyDress_Orange_Swatch",
"CowboyCherry_Swatch",
"Mobster_HerringBone_Tan_Swatch",
"Laser_Graffiti_Swatch", "Laser_Graffiti_Swatch",
"Laser_Ham_Swatch",
"Shuttershades_Salmon_Swatch", "Shuttershades_Salmon_Swatch",
"Shuttershades_Red_Swatch",
"Shuttershades_Blue_Swatch",
"Shuttershades_Yellow_Swatch",
"Countess_Wrist_Purple_Swatch",
"Class_Of_2018_Swatch", "Class_Of_2018_Swatch",
"FighterPilot_Gloves_Orange_Swatch",
"FighterPilot_Gloves_Blue_Swatch",
"Aviator_Gloves_Tan_Swatch",
"Aviator_Gloves_Black_Swatch",
"Plaid_LightBlue_Swatch", "Plaid_LightBlue_Swatch",
"Plaid_Red_Swatch",
"Astronaut_Glove_Orange_Swatch",
"Fatigue_Swatch", "Fatigue_Swatch",
"Wrist_CowboyWhite_Swatch", "Wrist_CowboyWhite_Swatch",
"Wrist_CowboyCherry_Swatch",
"Wrist_CowboyBlack_Swatch",
"Wrist_CowboyLight_Swatch",
"Wrist_CowboyDark_Swatch",
"Winter_Hat_Tree_Swatch",
"Winter_Hat_Red_Swatch",
"Harness_Hat_Teal_Swatch",
"Harness_Hat_Yellow_Swatch",
"Harness_Hat_White_Swatch",
"Harness_Hat_Orange_Swatch",
"Harness_Hat_Black_Swatch",
"Sherlock_Torso_Gray_Swatch",
"Sherlock_Torso_Black_Swatch",
"BowlingShirt_CenterStripe_Burgandy_Swatch",
"BowlingShirt_CenterStripe_Black_Swatch",
"BowlingShirt_CenterStripe_Yellow_Swatch",
"Scuba_Neck_Blue_Swatch",
"Scuba_Neck_Contest_Swatch",
"Cream_Swatch", "Cream_Swatch",
"ArtistHat_Red_Swatch", "Shuttershades_Red_Swatch",
"ArtistHat_Blue_Swatch",
"ArcherBlack_Quiver_Swatch",
"ArcherBlack_Swatch",
"ArcherBlack_Glove_Swatch",
"ArcherYellow_Swatch",
"ArcherYellow_Glove_Swatch",
"ArcherYellow_Quiver_Swatch",
"Hat_SeaCaptainDarkDarkGold_Swatch",
"Hat_SeaCaptainFushia_Swatch",
"Hat_SeaCaptainDarkGold_Swatch",
"Hat_SeaCaptainDarkSilver_Swatch",
"PolkaBlue_Swatch", "PolkaBlue_Swatch",
"Letterman_Wrist_Red_Swatch", "Wrist_CowboyCherry_Swatch",
"Letterman_Wrist_Purple_Swatch",
"Letterman_Wrist_Green_Swatch",
"Letterman_Wrist_Black_Swatch",
"Yellow_Swatch", "Yellow_Swatch",
"Windbreaker_Wrist_Yellow_Swatch", "Windbreaker_Wrist_Yellow_Swatch",
"Windbreaker_Wrist_Red_Swatch",
"Windbreaker_Wrist_Blue_Swatch",
"Windbreaker_Wrist_Salmon_Swatch",
"Beekeeper_Shirt_Teal_Swatch",
"Beekeeper_Shirt_Lavender_Swatch",
"Beekeeper_Shirt_Gold_Swatch",
"2D_Contest_Winner_Swatch", "2D_Contest_Winner_Swatch",
"Scarecrow_Hat_Contest_Swatch",
"Red_Swatch", "Red_Swatch",
"Hat_Baseball_ThreeColor_BlueRed_Swatch",
"PirateCaptain_Hat_Black_Swatch",
"PirateCaptain_Hat_SRank_Swatch",
"PirateCaptain_Hat_Red_Swatch",
"RoyalCape_Green_Swatch", "RoyalCape_Green_Swatch",
"RoyalCape_Blue_Swatch",
"RoyalCape_Purple_Swatch",
"Grey_Swatch", "Grey_Swatch",
"BlazerGreyDark_Swatch",
"MagicalGirl_Wrist_Red_Swatch",
"MagicalGirl_Wrist_Orange_Swatch",
"MagicalGirl_Wrist_Green_Swatch",
"MagicalGirl_Wrist_Blue_Swatch",
"PirateCaptain_Belt_Black_Swatch",
"PirateCaptain_Belt_Red_Swatch",
"BowlingShirt_Flames_Red_Swatch",
"BowlingShirt_Flames_Green_Swatch",
"BowlingShirt_Flames_Pink_Swatch",
"BowlingShirt_Flames_Blue_Swatch",
"RoyalCape_Contest_White_Swatch", "RoyalCape_Contest_White_Swatch",
"Barber_Torso_Formal_Swatch", "Barber_Torso_Formal_Swatch",
"Barber_Torso_Green_Swatch", "Knight_Blue_Swatch",
"Aviator_Goggles_Gray_Swatch",
"Aviator_Goggles_Black_Swatch",
"Neck_TutorialGown3_Black_Swatch", "Neck_TutorialGown3_Black_Swatch",
"SafariTorso_Green_Swatch",
"Scallywag_Hat_Blue_Swatch",
"Scallywag_Hat_Red_Swatch",
"TShirt_Laser_Unicorn_Purple_Swatch", "TShirt_Laser_Unicorn_Purple_Swatch",
"Astronaut_Helmet_Orange_Swatch",
"ArcherGray_Quiver_Swatch",
"ArcherGray_Swatch",
"ArcherGray_Glove_Swatch",
"Hoodie_Skeleton_NoGlow_Swatch",
"AnglerHat_Blue_Swatch",
"AnglerHat_Gray_Swatch",
"AnglerHat_Tan_Swatch",
"Scuba_eye_Contest_Swatch",
"Scuba_eye_Blue_Swatch",
"BowlingShirt_OneStripe_Green_Swatch",
"BowlingShirt_OneStripe_Pink_Swatch",
"BowlingShirt_OneStripe_Blue_Swatch",
"Golfer_Shirt_Blue_Swatch",
"Golfer_Shirt_Grey_Swatch",
"PaintBallHat_Red_Swatch", "PaintBallHat_Red_Swatch",
"PaintBallHat_Green_Swatch",
"PaintBallHat_Mustard_Swatch",
"Shirt_TutorialGown3_Black_Swatch",
"Winter_Scarf_Tree_Green_Swatch",
"Blue_Swatch", "Blue_Swatch",
"Barber_Hat_Formal_Swatch", "Barber_Hat_Formal_Swatch",
"Barber_Hat_Green_Swatch", "RoyalCape_Blue_Swatch",
"Vest_HoundsTooth_Black_Swatch",
"Neck_VampiteHunter_Opal_Swatch", "Neck_VampiteHunter_Opal_Swatch",
"Neck_VampiteHunter_Ruby_Swatch", "TShirt_HangInThere_Swatch",
"Neck_VampiteHunter_Amethyst_Swatch", "Knight_Red_Swatch",
"Neck_VampiteHunter_Diamond_Swatch",
"Neck_VampiteHunter_Emerald_Swatch",
"Neck_VampiteHunter_Amber_Swatch",
"Neck_VampiteHunter_Topaz_Swatch",
"Winter_Scarf_Red_Swatch",
"Winter_Scarf_Tree_Swatch",
"Floatie_Donut_Swatch", "Floatie_Donut_Swatch",
"Floatie_Dinosaur_Swatch",
"Rainhat_Hat_Red_Swatch",
"Winter_Gloves_Red_Swatch",
"Winter_Gloves_Tree_Swatch",
"Scout_Sash_Blue_Swatch",
"Scout_Sash_Brown_Swatch",
"Scout_Sash_Red_Swatch",
"RoyalLady_Green_Swatch", "RoyalLady_Green_Swatch",
"RoyalLady_Black_Swatch", "TShirt_Orange_Swatch",
"RoyalLady_Burgandy_Swatch",
"Mobster_Hat_Brown_Swatch",
"Mobster_Hat_Purple_Swatch",
"Mobster_Hat_Gray_Swatch",
"ElectricianTorso_Gray_Swatch",
"ElectricianTorso_Yellow_Swatch",
"ElectricianTorso_Green_Swatch",
"ElectricianTorso_Beige_Swatch",
"White_Swatch", "White_Swatch",
"RoyalCape_Purple_Swatch",
"BeltDress_Green_Swatch", "BeltDress_Green_Swatch",
"BeltDress_Black_Swatch", "Shuttershades_Blue_Swatch",
"BeltDress_Yellow_Swatch", "Neck_VampiteHunter_Ruby_Swatch",
"FootballHelmet_Orange_Swatch",
"FootballHelmet_Dark_Swatch",
"Aviator_Jacket_Tan_Swatch",
"Aviator_Jacket_Black_Swatch",
"RoyalCrown_Contest_Swatch", "RoyalCrown_Contest_Swatch",
"RoyalCrown_White_Swatch", "BeltDress_Black_Swatch",
"RoyalCrown_Black_Swatch", "Wrist_CowboyBlack_Swatch",
"MagicalGirl_Hat_Blue_Swatch",
"MagicalGirl_Hat_Green_Swatch",
"MagicalGirl_Hat_Red_Swatch",
"MagicalGirl_Hat_Orange_Swatch",
"TShirt_KO_Skull_Swatch", "TShirt_KO_Skull_Swatch",
"RoyalLord_Black_Swatch", "RoyalLord_Black_Swatch",
"RoyalLord_Burgandy_Swatch",
"RoyalLord_Green_Swatch",
"Hat_Baseball_Logo_Ramen_Swatch",
"Hat_Baseball_Logo_Bucketeers_Swatch",
"Hat_Baseball_Logo_Goblins_Swatch",
"TShirt_Orange_Video_Contest_Generic_Swatch", "TShirt_Orange_Video_Contest_Generic_Swatch",
"TShirt_Scifi_Face_BLue_Swatch", "TShirt_Scifi_Face_BLue_Swatch",
"TShirt_Scifi_Face_Pink_Swatch",
"Green_Swatch", "Green_Swatch",
"BlazerDark_Swatch",
"Witch_Contest_Hat_Swatch",
"Witch_Contest_Wrist_Swatch",
"CowboyWhite_Swatch",
"Aviator_Hat_Tan_Swatch",
"Aviator_Hat_Black_Swatch",
"GreenFlower_Swatch", "GreenFlower_Swatch",
"BowlingShirt_Diamonds_Purple_Swatch",
"BowlingShirt_Diamonds_Teal_Swatch",
"BowlingShirt_Diamonds_Blue_Swatch",
"FormalDress_Black_Swatch", "FormalDress_Black_Swatch",
"Birthday01_Swatch", "Birthday01_Swatch",
"Scallywag_Eye_SRank_Swatch", "Neck_VampiteHunter_Amethyst_Swatch",
"Ranger_Shirt_Green_Swatch", "Cyber_Trash_Black_Swatch",
"SciFiVisor_Peach_Swatch", "SciFiVisor_Peach_Swatch",
"SciFiVisor_Blue_Swatch",
"SciFiVisor_Orange_Swatch",
"Shirt_TutorialGown2_Orange_Swatch",
"Scifi_Carpet_Swatch", "Scifi_Carpet_Swatch",
"Teal_Swatch", "Teal_Swatch",
"Charcoal_Swatch", "Charcoal_Swatch",
"Paintball_Red_Swatch", "Paintball_Red_Swatch",
"Paintball_Blue_Swatch",
"ArcherTan_Glove_Swatch",
"ArcherTan_Quiver_Swatch",
"ArcherTan_Swatch",
"BicycleJacket_Black_Swatch", "BicycleJacket_Black_Swatch",
"BicycleJacket_DarkBlue_Swatch", "TShirt_Scifi_Face_Pink_Swatch",
"BicycleJacket_Blue_Swatch",
"Argyle_Brown_Swatch", "Argyle_Brown_Swatch",
"Argyle_Red_Swatch", "Paintball_Blue_Swatch",
"BasketballShirt_Purple_Swatch", "BasketballShirt_Purple_Swatch",
"BasketballShirt_Blue_Swatch",
"Purple_Swatch", "Purple_Swatch",
"ScoutSkirt_Shirt_Red_Swatch", "Knight_Black_Swatch",
"LaserTagVisor_Green_Swatch",
"LaserTagVisor_Orange_Swatch",
"LaserTagVisor_Cyan_Swatch",
"TShirt_KO_Skull_Pink_Swatch", "TShirt_KO_Skull_Pink_Swatch",
"CollarShirt_Torso_Red_Swatch", "BeltDress_Yellow_Swatch",
"CollarShirt_Torso_Blue_Swatch", "Shuttershades_Yellow_Swatch",
"CollarShirt_Torso_Pink_Swatch", "PaintBallHat_Green_Swatch",
"CollarShirt_Torso_GoldYellow_Swatch",
"CollarShirt_Torso_GreenFlower_Swatch",
"CollarShirt_Torso_White_Swatch",
"Pearls_Neck_Black_Swatch",
"MagicalGirl_Neck_Orange_Swatch",
"MagicalGirl_Neck_Red_Swatch",
"MagicalGirl_Neck_Green_Swatch",
"MagicalGirl_Neck_Blue_Swatch",
"Cyber_Junk_City_Swatch", "Cyber_Junk_City_Swatch",
"Firefighter_Hat_Yellow_Swatch", "BicycleJacket_DarkBlue_Swatch",
"Firefighter_Hat_White_Swatch",
"Scout_RaccoonHat_Gray_Swatch",
"JackfrostWings_Neck_Lilac_Swatch",
"CowboyBlack_Swatch",
"Torso_SeaCaptainBlueWhite_Swatch", "Torso_SeaCaptainBlueWhite_Swatch",
"Torso_SeaCaptainBlueDark2_Swatch", "CheerDress_Blue_Swatch",
"Torso_SeaCaptainDark_Swatch",
"Torso_SeaCaptainWhiteDark_Swatch",
"Torso_SeaCaptainBlackDark_Swatch",
"Torso_SeaCaptainBlueDark_Swatch",
"Torso_SeaCaptainBlue_Swatch",
"Astronaut_Torso_Orange_Swatch",
"CowboyDark_Swatch",
"Denim_Swatch", "Denim_Swatch",
"FootballPads_Orange_Swatch",
"FootballPads_Dark_Swatch",
"RoyalLady_Contest_White_Swatch", "RoyalLady_Contest_White_Swatch",
"Neck_VampiteHunter_Diamond_Swatch",
"Floatie_Dinosaur_Swatch",
"Torso_SeaCaptainBlueDark2_Swatch",
"WaitressDress_Mustard_Swatch", "WaitressDress_Mustard_Swatch",
"WaitressDress_Green_Swatch",
"WaitressDress_Red_Swatch",
"CollarDress_Pink_Swatch", "CollarDress_Pink_Swatch",
"CollarDress_LightBlue_Swatch", "Torso_SeaCaptainDark_Swatch",
"Scarecrow_Wrist_Contest_Swatch", "SciFiVisor_Blue_Swatch",
"RoyalCrown_White_Swatch",
"Pink_Swatch", "Pink_Swatch",
"Mobster_Fashion_Gold_Swatch",
"ArcherSRank_Glove_Swatch",
"ArcherSRank_Quiver_Swatch",
"ArcherSRank_Swatch",
"Royal_Swatch", "Royal_Swatch",
"GoldYellow_Swatch", "GoldYellow_Swatch",
"Sherlock_Hat_Gray_Swatch",
"Sherlock_Hat_Black_Swatch",
"Happy_Tree_Purple_Swatch", "Happy_Tree_Purple_Swatch",
"Golfer_Glove_Blue_Swatch", "Barber_Torso_Green_Swatch",
"Golfer_Glove_Grey_Swatch", "CheerDress_Red_Swatch",
"Steampunk_Femme_Contest_Hat_Swatch",
"TShirt_RECS_Logo_Blue_Swatch", "TShirt_RECS_Logo_Blue_Swatch",
"TShirt_RECS_Logo_Green_Swatch",
"Scout_Shirt_Blue_Swatch", "Scout_Shirt_Blue_Swatch",
"DrumMajorGlove_Purple_Swatch", "Windbreaker_Wrist_Red_Swatch",
"DrumMajorGlove_Orange_Swatch", "Neck_VampiteHunter_Emerald_Swatch",
"ArcherRed_Glove_Swatch", "TShirt_RECS_Logo_Green_Swatch",
"ArcherRed_Swatch", "Neck_VampiteHunter_Amber_Swatch",
"ArcherRed_Quiver_Swatch", "RoyalCrown_Black_Swatch",
"Lacrosse_Stripe_Purple_Swatch", "RoyalLord_Burgandy_Swatch",
"Winter_Torso_Tree_Swatch", "Knight_Green_Swatch",
"Winter_Torso_Red_Swatch", "Torso_SeaCaptainWhiteDark_Swatch",
"Scuba_Shirt_Contest_Swatch",
"Scuba_Shirt_Blue_Swatch",
"LightBlue_Swatch", "LightBlue_Swatch",
"Winter_Hat_Tree_Green_Swatch",
"CowboyLight_Swatch",
"Happy_Tree_Swatch", "Happy_Tree_Swatch",
"Winter_Torso_Tree_Green_Swatch", "BicycleJacket_Blue_Swatch",
"BlazerMidnightBlue_Swatch", "Torso_SeaCaptainBlackDark_Swatch",
"PaintBallHat_Mustard_Swatch",
"Scifi_Carpet_TopHat_Swatch", "Scifi_Carpet_TopHat_Swatch",
"Windbreaker_Wrist_Blue_Swatch",
"Neck_VampiteHunter_Topaz_Swatch",
"Plaid_Red_Swatch",
"Laser_Ham_Swatch",
"Rebel_Rose_Swatch", "Rebel_Rose_Swatch",
"Mobster_PinStripe_Purple_Swatch", "Wrist_CowboyLight_Swatch",
"Soccer_Shirt_Blue_Swatch",
"Barber_Wrist_Formal_Swatch", "Barber_Wrist_Formal_Swatch",
"Steampunk_Femme_Contest_Swatch", "SciFiVisor_Orange_Swatch",
"RoyalLady_Black_Swatch",
"WaitressDress_Green_Swatch",
"StollDress_Green_Swatch", "StollDress_Green_Swatch",
"StollDress_Blue_Swatch", "Torso_SeaCaptainBlueDark_Swatch",
"BlazerSeaDark_Swatch", "Torso_SeaCaptainBlue_Swatch",
"Orange_Swatch", "Orange_Swatch",
"DrumMajorHat_Green_Swatch", "PB_Trucker_Red_Swatch",
"Golfer_Hat_Blue_Swatch", "Barber_Hat_Green_Swatch",
"Golfer_Hat_Grey_Swatch",
"RoyalLord_Contest_White_Swatch", "RoyalLord_Contest_White_Swatch",
"Black_Swatch", "Black_Swatch",
"Jackfrost_Shirt_Lilac_Swatch", "RoyalLord_Green_Swatch",
"CollarShirt_Torso_Plaid_Red_Swatch", "CollarDress_LightBlue_Swatch",
"StollDress_Blue_Swatch",
"Argyle_Red_Swatch",
"RoyalLady_Burgandy_Swatch",
"TShirt_LaserTag_Alpha_Swatch", "TShirt_LaserTag_Alpha_Swatch",
"Countess_Green_Wrist_Swatch",
"Hyper_Noodle_Ramen_Swatch", "Hyper_Noodle_Ramen_Swatch",
"Scarecrow_Shirt_Contest_Swatch", "PoofyDress_Orange_Swatch",
"Knight_White_Swatch",
"BicycleHelmet_Yellow_Swatch", "BicycleHelmet_Yellow_Swatch",
"Soccer_Glove_Blue_Swatch", "BasketballShirt_Blue_Swatch",
"Steampunk_Contest_Hat_Swatch", "Wrist_CowboyDark_Swatch",
"Windbreaker_Wrist_Salmon_Swatch",
"Emerald_Syndicate_Swatch", "Emerald_Syndicate_Swatch",
"Neck_TutorialGown2_Orange_Swatch", "Neck_TutorialGown2_Orange_Swatch",
"Shirt_TutorialGown1_Purple_Swatch",
"2D_Contest_Winner_Yellow_Swatch", "2D_Contest_Winner_Yellow_Swatch",
"BlazerGrey_Swatch", "WaitressDress_Red_Swatch",
"Stripe3_Swatch", "Stripe3_Swatch"
"Ranger_Hat_Green_Swatch"
], ],
"ambiguous": [ "ambiguous": []
{
"prefix": "windbreaker",
"candidates": [
"52VsI_lKlkSUAOJlfDnyrQ",
"AIpF1QUeMUWytpHYBDBAkg"
]
},
{
"prefix": "lumberjack",
"candidates": [
"350c5c1e-318b-4233-a7e5-1f49909d8e65",
"61ba3c90-c81e-4deb-bc79-50c0f1fe3e83",
"b537c30c-faef-4649-9a1f-be3985936f93"
]
},
{
"prefix": "magicalgirl",
"candidates": [
"eS_or4AnMka9M_bD7YN28g",
"hT7nSD7Ts06F_1SNlOMLwg",
"MJkDYDH510izAab13ZfXQw",
"WuShlrJnHUKuscn8e6Rt2g"
]
},
{
"prefix": "wizard",
"candidates": [
"01689baf-405e-4b38-9f09-b624e03865b8",
"22ef1089-d073-4fe8-b038-6f34adf0b85f",
"97eae086-cfea-45ff-9b77-1d184256d493"
]
},
{
"prefix": "punk",
"candidates": [
"28374ebc-4050-4ea0-b3b6-41ebe75cedf7",
"ce250e13-8137-4f62-9e52-2b1cebe7d0ad",
"e4ba395e-f291-4e7b-8193-45f4422f41ce"
]
},
{
"prefix": "fighterpilot",
"candidates": [
"5800d2b5-75b8-442f-8b10-63bd3d73a1b8",
"ec836d2a-b1b8-4566-b8eb-123ae2845956",
"ff7e6d41-5c27-4516-930c-ad0e9a23cce2"
]
},
{
"prefix": "witchhunter",
"candidates": [
"418db0e0-c7af-40c2-9cac-d461ce41e210",
"6d36091e-7083-4e3d-8729-94612602c8cb"
]
},
{
"prefix": "smuggler",
"candidates": [
"25d4c652-1730-4925-925e-31a290a2272a",
"3dec4865-cf23-4c19-a460-dd2db80ee851"
]
},
{
"prefix": "sodajerk",
"candidates": [
"iai6P7dDTUq8S4hqQmY1YA",
"Wua43ED71UqL71ATtyS0iQ"
]
},
{
"prefix": "piratecaptain",
"candidates": [
"38b38e24-d636-41c6-b80e-a285ae3106dc",
"3b00c71f-79c2-4a04-9fc0-a23b4c85562f",
"3df394de-9c3e-4141-aba4-12eac4742102",
"eceb7d49-be41-4228-86a9-2973d62e7135"
]
},
{
"prefix": "truckerjacket",
"candidates": [
"8b065e2a-b106-4f11-b415-4c47ce87993e",
"973d84ee-5b88-475c-b579-a76c34a7c533"
]
},
{
"prefix": "scientist",
"candidates": [
"f17a2f9e-1eab-44e8-b10a-1f53c5d0778c",
"fe34adaf-47d3-435b-b2fd-b2fee05fceae"
]
},
{
"prefix": "lasertag",
"candidates": [
"123de433-a005-4994-8a11-d2d84ad66b11",
"24d94673-f394-42e6-a3c8-276f8b8aa722"
]
},
{
"prefix": "letterman",
"candidates": [
"2dFMy4zc7ky_Fd5fzfgInw",
"C4uuztTiYUe20PACMARksA"
]
},
{
"prefix": "deepseadiver",
"candidates": [
"4fcb5b84-13a9-43e6-9680-72b17874ae53",
"8d97303c-5138-4da1-a74d-378da19d11cb",
"b1c3ccc8-91d7-434e-b9cc-bd632fffc01b",
"c3745cee-d87e-4a23-b6e2-64ebf05c8b11"
]
},
{
"prefix": "jazzercise",
"candidates": [
"49397e16-1027-4286-9bc4-b59eae565ff0",
"5f492f8f-ab61-484a-9587-747132b54309"
]
},
{
"prefix": "angler",
"candidates": [
"03f8c394-28fa-4087-978b-8d108f0bd969",
"b1bfe0b4-1ff6-420f-acfc-2331d34246dc"
]
},
{
"prefix": "drummajor",
"candidates": [
"1a028850-64ee-40ab-b547-ebbcfa1b04b3",
"a6f1825e-1647-4d06-85d7-ac2d139612c0"
]
},
{
"prefix": "countess",
"candidates": [
"QCtqxbvYOkan79YFugH_Cw",
"t9co2516nkuJO4LIJA1bJA"
]
},
{
"prefix": "mobster",
"candidates": [
"08d56627-4500-4724-8692-a23c96ddb3ec",
"4308da04-af84-40bb-845a-7a9e1a2726ec",
"76369aef-aeda-46d2-996a-cd00594d0543"
]
},
{
"prefix": "steampunk",
"candidates": [
"E9VTiIVFPEa-OuQRto0e_Q",
"gwLiwmqBIk6glFxvaTzxeA",
"qxovV6Vym0ufL2cTCyKjOQ",
"sOREgAGs-06CeGGsG-WIkw"
]
},
{
"prefix": "witch",
"candidates": [
"7LSm-xCYakmgcAr--Fn-JA",
"s6mUIqiPCk6e57UYCx3SZA",
"wy2cfgb8mUqjMrIOwHH2ew"
]
}
]
} }
+8
View File
@@ -746,6 +746,11 @@ using (var scope = app.Services.CreateScope())
await RunPatchAsync("ServerSettings.WeeklyChallengeRewardJson column", await RunPatchAsync("ServerSettings.WeeklyChallengeRewardJson column",
@"ALTER TABLE ""ServerSettings"" @"ALTER TABLE ""ServerSettings""
ADD COLUMN IF NOT EXISTS ""WeeklyChallengeRewardJson"" text NOT NULL DEFAULT '';"); ADD COLUMN IF NOT EXISTS ""WeeklyChallengeRewardJson"" text NOT NULL DEFAULT '';");
// 2026-06-04 — "everyone is friends" admin toggle (synthesized at
// read time; see ServerSettingsEntity.GlobalFriendsEnabled).
await RunPatchAsync("ServerSettings.GlobalFriendsEnabled column",
@"ALTER TABLE ""ServerSettings""
ADD COLUMN IF NOT EXISTS ""GlobalFriendsEnabled"" boolean NOT NULL DEFAULT false;");
// 2026-05-19 — Players.IsCommunityTeam column. Backs the // 2026-05-19 — Players.IsCommunityTeam column. Backs the
// overhead-badge admin toggle alongside IsDeveloper; the // overhead-badge admin toggle alongside IsDeveloper; the
@@ -1319,6 +1324,9 @@ static void ApplySqliteCompatibilityPatches(DorkNetDbContext db)
@"""WeeklyChallengesJson"" TEXT NOT NULL DEFAULT ''"); @"""WeeklyChallengesJson"" TEXT NOT NULL DEFAULT ''");
AddSqliteColumnIfMissing(db, "ServerSettings", "WeeklyChallengeRewardJson", AddSqliteColumnIfMissing(db, "ServerSettings", "WeeklyChallengeRewardJson",
@"""WeeklyChallengeRewardJson"" TEXT NOT NULL DEFAULT ''"); @"""WeeklyChallengeRewardJson"" TEXT NOT NULL DEFAULT ''");
// 2026-06-04 — "everyone is friends" admin toggle.
AddSqliteColumnIfMissing(db, "ServerSettings", "GlobalFriendsEnabled",
@"""GlobalFriendsEnabled"" INTEGER NOT NULL DEFAULT 0");
} }
static void AddSqliteColumnIfMissing(DorkNetDbContext db, string table, string column, string definition) static void AddSqliteColumnIfMissing(DorkNetDbContext db, string table, string column, string definition)
+14 -3
View File
@@ -174,13 +174,21 @@ public class PlayerService(DorkNetDbContext db, RoomService rooms, ILogger<Playe
// Self-heal: if the row exists but the bookkeeping fields // Self-heal: if the row exists but the bookkeeping fields
// drift (e.g. someone manually edited Username), restore them // drift (e.g. someone manually edited Username), restore them
// so admin tools always show "Coach" as the owner. // so admin tools always show "Coach" as the owner.
if (existing.Username != SystemAccountUsername || existing.DisplayName != SystemAccountUsername) var nameDrift = existing.Username != SystemAccountUsername
|| existing.DisplayName != SystemAccountUsername;
// Heal a legacy year-9999 BannedUntil written before WireDates:
// such a value corrupts the watch's local-timezone date parse if it
// ever reaches a client (e.g. via /api/playersbanned). See WireDates.
var banDrift = existing.BannedUntil.HasValue
&& existing.BannedUntil.Value > WireDates.FarFuture;
if (nameDrift || banDrift)
{ {
await db.Players await db.Players
.Where(p => p.Id == SystemAccountId) .Where(p => p.Id == SystemAccountId)
.ExecuteUpdateAsync(s => s .ExecuteUpdateAsync(s => s
.SetProperty(p => p.Username, SystemAccountUsername) .SetProperty(p => p.Username, SystemAccountUsername)
.SetProperty(p => p.DisplayName, SystemAccountUsername)); .SetProperty(p => p.DisplayName, SystemAccountUsername)
.SetProperty(p => p.BannedUntil, WireDates.FarFuture));
} }
return; return;
} }
@@ -204,7 +212,10 @@ public class PlayerService(DorkNetDbContext db, RoomService rooms, ILogger<Playe
// Far-future ban as a hard authorization backstop in case a // Far-future ban as a hard authorization backstop in case a
// forged JWT ever names player 1 as the subject. BanCheckMiddleware // forged JWT ever names player 1 as the subject. BanCheckMiddleware
// refuses any authenticated request when BannedUntil > now. // refuses any authenticated request when BannedUntil > now.
BannedUntil = new DateTime(9999, 12, 31, 0, 0, 0, DateTimeKind.Utc), // WireDates.FarFuture (2099), NOT year 9999 — a 9999 BannedUntil
// serialised to the watch overflows its local-timezone date parse
// in some DST timezones and corrupts the response. See WireDates.
BannedUntil = WireDates.FarFuture,
PasswordHash = null, PasswordHash = null,
CreatedAt = new DateTime(2020, 3, 6, 0, 0, 0, DateTimeKind.Utc), CreatedAt = new DateTime(2020, 3, 6, 0, 0, 0, DateTimeKind.Utc),
LastSeenAt = new DateTime(2020, 3, 6, 0, 0, 0, DateTimeKind.Utc), LastSeenAt = new DateTime(2020, 3, 6, 0, 0, 0, DateTimeKind.Utc),
@@ -0,0 +1,94 @@
using DorkNet.Server.Data;
using DorkNet.Server.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace DorkNet.Server.Services;
/// <summary>
/// Effective-friend resolution that honors the
/// <see cref="ServerSettingsEntity.GlobalFriendsEnabled"/> toggle.
///
/// When the toggle is ON, every account on the server is treated as a
/// friend of every other account WITHOUT writing any relationship rows —
/// the friend graph is synthesized at read time. This keeps a small private
/// server frictionless (no searching + manual friend requests) and reverts
/// instantly when the toggle flips off, since nothing was persisted.
///
/// Blocked relationships still suppress the pairing, and the system/coach
/// account (<see cref="SystemAccountId"/>) is never offered as a friend.
///
/// Live propagation: flipping the toggle broadcasts
/// <c>PushNotificationId.RelationshipsInvalid</c>, which the 2020 watch
/// handles by calling <c>Relationships.RefreshList</c> — a full re-fetch of
/// <c>api/relationships/v2/get</c> — so connected players see the change
/// without relogging (verified against the readable March decompile:
/// <c>RecNet/Relationships.txt → OnRelationshipsInvalid → RefreshList</c>).
/// </summary>
public static class RelationshipQueries
{
/// <summary>Coach/system account seeded at startup (Player.Id = 1).
/// Excluded from the global-friends set so it never shows up as a
/// friend in anyone's list.</summary>
public const long SystemAccountId = 1;
/// <summary>True when <paramref name="username"/> is still an
/// auto-generated placeholder name (<c>Player_</c> followed by digits
/// only — see <c>PlayerService.GenerateUsername</c>). These are accounts
/// that never set a real username; the global-friends toggle hides them
/// from the synthesized "everyone" list so they don't clutter it. A
/// genuine Friend row still surfaces them — only the synthetic pairing is
/// suppressed.</summary>
public static bool IsPlaceholderUsername(string? username)
{
const string prefix = "Player_";
if (string.IsNullOrEmpty(username) ||
!username.StartsWith(prefix, StringComparison.Ordinal)) return false;
var suffix = username.AsSpan(prefix.Length);
if (suffix.IsEmpty) return false;
foreach (var c in suffix)
if (!char.IsAsciiDigit(c)) return false;
return true;
}
/// <summary>The effective friend ids for <paramref name="me"/>. With the
/// global-friends toggle off this is the real Friend graph; with it on
/// it's every other (non-system, non-blocked) account, excluding
/// placeholder <c>Player_NNN</c> accounts that aren't already real
/// friends.</summary>
public static async Task<List<long>> EffectiveFriendIdsAsync(
DorkNetDbContext db, ServerSettingsService settings, long me)
{
if (!await settings.IsGlobalFriendsEnabledAsync())
{
return await db.Relationships
.Where(r => r.Status == RelationshipStatus.Friend &&
(r.RequesterId == me || r.TargetId == me))
.Select(r => r.RequesterId == me ? r.TargetId : r.RequesterId)
.ToListAsync();
}
// Global friends: everyone except myself, the system account, and
// anyone in a Blocked relationship with me (either direction).
var blocked = await db.Relationships
.Where(r => r.Status == RelationshipStatus.Blocked &&
(r.RequesterId == me || r.TargetId == me))
.Select(r => r.RequesterId == me ? r.TargetId : r.RequesterId)
.ToListAsync();
var realFriendSet = (await db.Relationships
.Where(r => r.Status == RelationshipStatus.Friend &&
(r.RequesterId == me || r.TargetId == me))
.Select(r => r.RequesterId == me ? r.TargetId : r.RequesterId)
.ToListAsync()).ToHashSet();
var candidates = await db.Players
.Where(p => p.Id != me && p.Id != SystemAccountId && !blocked.Contains(p.Id))
.Select(p => new { p.Id, p.Username })
.ToListAsync();
// Suppress placeholder accounts unless they're an actual friend.
return candidates
.Where(p => realFriendSet.Contains(p.Id) || !IsPlaceholderUsername(p.Username))
.Select(p => p.Id)
.ToList();
}
}
@@ -48,6 +48,26 @@ public class ServerSettingsService(DorkNetDbContext db)
return existing; return existing;
} }
/// <summary>When true, every account is treated as a friend of every
/// other account (synthesized at read time — no relationship rows are
/// written). See <see cref="ServerSettingsEntity.GlobalFriendsEnabled"/>
/// and <see cref="RelationshipQueries"/>.</summary>
public async Task<bool> IsGlobalFriendsEnabledAsync()
{
var row = await db.ServerSettings.AsNoTracking()
.FirstOrDefaultAsync(s => s.Id == RowId);
return row?.GlobalFriendsEnabled ?? false;
}
public async Task<ServerSettingsEntity> SetGlobalFriendsEnabledAsync(bool enabled)
{
var existing = await GetTrackedRowAsync();
existing.GlobalFriendsEnabled = enabled;
existing.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return existing;
}
public async Task<WeeklyChallengeSettings> GetWeeklyChallengesAsync() public async Task<WeeklyChallengeSettings> GetWeeklyChallengesAsync()
{ {
var row = await GetAsync(); var row = await GetAsync();
+12 -26
View File
@@ -10,10 +10,8 @@ namespace DorkNet.Server.Services;
/// Backs the admin-issued signup-code flow. While the server has signups /// Backs the admin-issued signup-code flow. While the server has signups
/// disabled, the only path to a new account is redeeming one of these on /// disabled, the only path to a new account is redeeming one of these on
/// the public <c>/join</c> page. Codes are single-use, carry an admin /// the public <c>/join</c> page. Codes are single-use, carry an admin
/// descriptor, and can have an optional expiry. Redemption mints an /// descriptor, and can have an optional expiry. Redemption mints a normal
/// account bound to the device id the player's game client reported (see /// username/password account.
/// <see cref="PendingDeviceEntity"/>), so the next launch logs straight
/// in via the watch's device-id login.
/// </summary> /// </summary>
public class SignupCodeService(DorkNetDbContext db, PlayerService players) public class SignupCodeService(DorkNetDbContext db, PlayerService players)
{ {
@@ -61,10 +59,10 @@ public class SignupCodeService(DorkNetDbContext db, PlayerService players)
public sealed record RedeemResult(bool Ok, string? Error, long? PlayerId, string? Username); public sealed record RedeemResult(bool Ok, string? Error, long? PlayerId, string? Username);
/// <summary>Redeem a code: validate it, then mint an account bound to /// <summary>Redeem a code: validate it, then mint a password account
/// <paramref name="deviceId"/> with the requested username. The code /// with the requested username. The code is consumed atomically with
/// is consumed atomically with account creation.</summary> /// account creation.</summary>
public async Task<RedeemResult> RedeemAsync(string? rawCode, string? rawUsername, string? deviceId) public async Task<RedeemResult> RedeemAsync(string? rawCode, string? rawUsername, string? rawPassword)
{ {
var code = NormalizeCode(rawCode); var code = NormalizeCode(rawCode);
if (string.IsNullOrEmpty(code)) return new(false, "missing_code", null, null); if (string.IsNullOrEmpty(code)) return new(false, "missing_code", null, null);
@@ -72,7 +70,9 @@ public class SignupCodeService(DorkNetDbContext db, PlayerService players)
var username = (rawUsername ?? string.Empty).Trim(); var username = (rawUsername ?? string.Empty).Trim();
if (!IsValidUsername(username)) return new(false, "invalid_username", null, null); if (!IsValidUsername(username)) return new(false, "invalid_username", null, null);
if (string.IsNullOrWhiteSpace(deviceId)) return new(false, "missing_device", null, null); var password = rawPassword ?? string.Empty;
if (string.IsNullOrEmpty(password)) return new(false, "missing_password", null, null);
if (password.Length < 8) return new(false, "password_too_short", null, null);
var entity = await db.SignupCodes.FirstOrDefaultAsync(c => c.Code == code); var entity = await db.SignupCodes.FirstOrDefaultAsync(c => c.Code == code);
if (entity is null) return new(false, "invalid_code", null, null); if (entity is null) return new(false, "invalid_code", null, null);
@@ -83,31 +83,17 @@ public class SignupCodeService(DorkNetDbContext db, PlayerService players)
if (await db.Players.AnyAsync(p => p.Username == username)) if (await db.Players.AnyAsync(p => p.Username == username))
return new(false, "username_taken", null, null); return new(false, "username_taken", null, null);
// The device must not already own an account (otherwise the var player = await players.CreateNewAccountAsync(
// player would launch into the existing one, not this fresh deviceId: $"join-{Guid.NewGuid():N}",
// account). Guides them to a different device / to just log in.
if (await players.GetByDeviceAsync(deviceId) is not null)
return new(false, "device_in_use", null, null);
var player = await players.GetOrCreateByDeviceAsync(
deviceId: deviceId,
platform: 0, platform: 0,
platformId: null, platformId: null,
displayName: username); displayName: username);
player.PasswordHash = BCrypt.Net.BCrypt.HashPassword(password, workFactor: 11);
entity.RedeemedByPlayerId = player.Id; entity.RedeemedByPlayerId = player.Id;
entity.RedeemedAt = DateTime.UtcNow; entity.RedeemedAt = DateTime.UtcNow;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
// The device is now bound to a real account — drop any pending
// refusal row so it stops showing on the /join picker.
var pending = await db.PendingDevices.FirstOrDefaultAsync(d => d.DeviceId == deviceId);
if (pending is not null)
{
db.PendingDevices.Remove(pending);
await db.SaveChangesAsync();
}
return new(true, null, player.Id, player.Username); return new(true, null, player.Id, player.Username);
} }
+5 -2
View File
@@ -778,8 +778,11 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
{ {
StorefrontType = storefrontType, StorefrontType = storefrontType,
StorefrontName = name, StorefrontName = name,
NextUpdate = "9999-12-31T00:00:00Z", // NOT year 9999: that overflows the watch's local-timezone date
NewUntil = "9999-12-31T00:00:00Z", // parse in some DST timezones → "malformed RecNet response" and a
// broken store for that player only. See WireDates.
NextUpdate = WireDates.FarFutureIso,
NewUntil = WireDates.FarFutureIso,
StoreItems = storeItems, StoreItems = storeItems,
}; };
} }
+32
View File
@@ -0,0 +1,32 @@
namespace DorkNet.Server;
/// <summary>
/// Canonical "far future / never expires" sentinel dates for anything we
/// serialise back to the watch.
///
/// DO NOT use <c>DateTime.MaxValue</c> or year 9999 in client-facing wire
/// data. The 2020 watch parses ISO-8601 timestamps and adjusts them to the
/// player's LOCAL timezone (DateTimeParse.ParseISO8601 → AdjustTimeZoneToLocal
/// → TimeZoneInfo DST resolution). For a player whose timezone has DST
/// adjustment rules, resolving the DST transition for a year-9999 instant
/// pushes the computed transition point past <c>DateTime.MaxValue</c> (year
/// 10000) and throws <c>ArgumentOutOfRangeException: Year, Month, and Day
/// parameters describe an un-representable DateTime</c>. That blows up the
/// WHOLE response deserialization → "Received malformed RecNet response" → the
/// store / gift drops / promo board silently break, but ONLY for players in
/// the affected timezones (everyone else parses the same payload fine, which
/// makes it look like a per-player gremlin).
///
/// 2099 is effectively "forever" for a 2020-era client and is nowhere near the
/// overflow boundary in any timezone.
/// </summary>
public static class WireDates
{
/// <summary>Far-future sentinel as a <see cref="DateTime"/> (UTC).</summary>
public static readonly DateTime FarFuture =
new(2099, 12, 31, 0, 0, 0, DateTimeKind.Utc);
/// <summary>Far-future sentinel pre-rendered as the ISO-8601 Z string the
/// watch expects (used where the DTO field is a raw string).</summary>
public const string FarFutureIso = "2099-12-31T00:00:00Z";
}
+31 -26
View File
@@ -4,25 +4,16 @@ import { RequireAuth } from './components/RequireAuth';
import { ToastProvider } from './components/Toast'; import { ToastProvider } from './components/Toast';
import { Login } from './pages/Login'; import { Login } from './pages/Login';
import { Dashboard } from './pages/Dashboard'; import { Dashboard } from './pages/Dashboard';
import { Players } from './pages/Players'; import { Moderation } from './pages/Moderation';
import { Bans } from './pages/Bans'; import { Activity } from './pages/Activity';
import { Reports } from './pages/Reports';
import { Audit } from './pages/Audit';
import { PlayerLogs } from './pages/PlayerLogs';
import { Rooms } from './pages/Rooms'; import { Rooms } from './pages/Rooms';
import { Room } from './pages/Room'; import { Room } from './pages/Room';
import { ImportRoom } from './pages/ImportRoom'; import { ImportRoom } from './pages/ImportRoom';
import { LegacyImportRoom } from './pages/LegacyImportRoom'; import { LegacyImportRoom } from './pages/LegacyImportRoom';
import { Store } from './pages/Store';
import { Broadcast } from './pages/Broadcast'; import { Broadcast } from './pages/Broadcast';
import { CommunityBoard } from './pages/CommunityBoard'; import { Content } from './pages/Content';
import { LoadingTips } from './pages/LoadingTips';
import { Gift } from './pages/Gift';
import { Passwords } from './pages/Passwords';
import { Grants } from './pages/Grants';
import { Storage } from './pages/Storage'; import { Storage } from './pages/Storage';
import { Settings } from './pages/Settings'; import { SettingsHome } from './pages/SettingsHome';
import { SignupCodes } from './pages/SignupCodes';
export default function App() { export default function App() {
return ( return (
@@ -31,11 +22,23 @@ export default function App() {
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route element={<RequireAuth><Layout /></RequireAuth>}> <Route element={<RequireAuth><Layout /></RequireAuth>}>
<Route index element={<Dashboard />} /> <Route index element={<Dashboard />} />
<Route path="players" element={<Players />} />
<Route path="bans" element={<Bans />} /> {/* Players hub — directory + bans + reports as tabs. The old
<Route path="reports" element={<Reports />} /> standalone routes redirect in with the matching sub-tab
<Route path="audit" element={<Audit />} /> preselected. Per-player ban / grant / gift / password
<Route path="logs" element={<PlayerLogs />} /> actions all live in the row's detail modal. */}
<Route path="players" element={<Moderation />} />
<Route path="bans" element={<Navigate to="/players?tab=bans" replace />} />
<Route path="reports" element={<Navigate to="/players?tab=reports" replace />} />
<Route path="gift" element={<Navigate to="/players" replace />} />
<Route path="passwords" element={<Navigate to="/players" replace />} />
<Route path="grants" element={<Navigate to="/players" replace />} />
{/* Activity hub — audit trail + per-player request logs. */}
<Route path="activity" element={<Activity />} />
<Route path="audit" element={<Navigate to="/activity?tab=audit" replace />} />
<Route path="logs" element={<Navigate to="/activity?tab=logs" replace />} />
<Route path="rooms" element={<Rooms />} /> <Route path="rooms" element={<Rooms />} />
<Route path="rooms/:id" element={<Room />} /> <Route path="rooms/:id" element={<Room />} />
{/* Back-compat: the merge left RR-Originals / Instances / {/* Back-compat: the merge left RR-Originals / Instances /
@@ -48,16 +51,18 @@ export default function App() {
<Route path="leaderboards" element={<Navigate to="/rooms" replace />} /> <Route path="leaderboards" element={<Navigate to="/rooms" replace />} />
<Route path="import-room" element={<ImportRoom />} /> <Route path="import-room" element={<ImportRoom />} />
<Route path="import-room-legacy" element={<LegacyImportRoom />} /> <Route path="import-room-legacy" element={<LegacyImportRoom />} />
<Route path="store" element={<Store />} />
<Route path="community" element={<CommunityBoard />} /> {/* Content hub — community board + loading-screen tips. */}
<Route path="loading-tips" element={<LoadingTips />} /> <Route path="content" element={<Content />} />
<Route path="community" element={<Navigate to="/content?tab=community" replace />} />
<Route path="loading-tips" element={<Navigate to="/content?tab=tips" replace />} />
<Route path="broadcast" element={<Broadcast />} /> <Route path="broadcast" element={<Broadcast />} />
<Route path="gift" element={<Gift />} />
<Route path="passwords" element={<Passwords />} />
<Route path="grants" element={<Grants />} />
<Route path="storage" element={<Storage />} /> <Route path="storage" element={<Storage />} />
<Route path="settings" element={<Settings />} />
<Route path="signup-codes" element={<SignupCodes />} /> {/* Settings hub — server toggles + signup codes. */}
<Route path="settings" element={<SettingsHome />} />
<Route path="signup-codes" element={<Navigate to="/settings?tab=signup" replace />} />
</Route> </Route>
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
@@ -1,10 +1,16 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { import {
LayoutDashboard, Users, Building2, Ban, Flag, ScrollText, Activity, LayoutDashboard, Users, Building2, Activity,
Megaphone, Upload, ShoppingBag, Gift, KeyRound, Wrench, MessageSquare, HardDrive, Settings, X, Megaphone, MessageSquare, HardDrive, Settings, X,
} from './Icons'; } from './Icons';
// Consolidated navigation. Per-player moderation (bans, grants, gifts,
// password resets) lives in the player detail modal; the old Bans /
// Reports / Audit / Player-logs / Community / Loading-tips / Gift /
// Passwords / Signup-codes pages are now tabs under Players, Activity,
// Content, and Settings. Import room is a header action on the Rooms
// page. Old routes still resolve via redirects in App.tsx.
const groups: Array<{ title: string; items: Array<{ to: string; label: string; icon: React.ReactNode; new?: boolean }>}> = [ const groups: Array<{ title: string; items: Array<{ to: string; label: string; icon: React.ReactNode; new?: boolean }>}> = [
{ {
title: 'Overview', title: 'Overview',
@@ -13,39 +19,25 @@ const groups: Array<{ title: string; items: Array<{ to: string; label: string; i
], ],
}, },
{ {
title: 'People', title: 'Moderation',
items: [ items: [
{ to: '/players', label: 'Players', icon: <Users /> }, { to: '/players', label: 'Players', icon: <Users /> },
{ to: '/bans', label: 'Bans', icon: <Ban /> }, { to: '/activity', label: 'Activity', icon: <Activity /> },
{ to: '/reports', label: 'Reports', icon: <Flag /> },
{ to: '/audit', label: 'Audit log', icon: <ScrollText /> },
{ to: '/logs', label: 'Player logs', icon: <Activity /> },
], ],
}, },
{ {
title: 'Content', title: 'Content',
items: [ items: [
// Rooms is the unified view — clicking a row opens /rooms/:id { to: '/rooms', label: 'Rooms', icon: <Building2 /> },
// which carries the old RR-Originals edit modal, live Photon { to: '/content', label: 'Content', icon: <MessageSquare /> },
// instance list, recent visitors, ownership management, and
// per-room leaderboard channels in one tabbed page.
{ to: '/rooms', label: 'Rooms', icon: <Building2 /> },
{ to: '/import-room', label: 'Import room', icon: <Upload /> },
{ to: '/store', label: 'Store catalog', icon: <ShoppingBag /> },
{ to: '/community', label: 'Community board', icon: <MessageSquare /> },
{ to: '/loading-tips', label: 'Loading screen tips', icon: <MessageSquare /> },
], ],
}, },
{ {
title: 'Tools', title: 'Operations',
items: [ items: [
{ to: '/broadcast', label: 'Broadcast', icon: <Megaphone /> }, { to: '/broadcast', label: 'Broadcast', icon: <Megaphone /> },
{ to: '/gift', label: 'Gift items', icon: <Gift />, new: true }, { to: '/storage', label: 'Storage', icon: <HardDrive /> },
{ to: '/passwords', label: 'Reset passwords', icon: <KeyRound />, new: true }, { to: '/settings', label: 'Settings', icon: <Settings /> },
{ to: '/grants', label: 'Grant maker pen', icon: <Wrench />, new: true },
{ to: '/storage', label: 'Storage backfill', icon: <HardDrive />, new: true },
{ to: '/signup-codes', label: 'Signup codes', icon: <KeyRound />, new: true },
{ to: '/settings', label: 'Server settings', icon: <Settings />, new: true },
], ],
}, },
]; ];
@@ -0,0 +1,50 @@
import { useSearchParams } from 'react-router-dom';
// Shared horizontal tab strip — the same underlined-button style the
// per-player modal and the old Bans page already used inline, pulled
// out so the consolidated hub pages (Players / Activity / Content /
// Settings) all render their sub-tabs identically.
export function Tabs<T extends string>({
tabs,
active,
onChange,
}: {
tabs: ReadonlyArray<{ key: T; label: string }>;
active: T;
onChange: (key: T) => void;
}) {
return (
<div className="flex border-b border-ink-800 text-sm mb-4 overflow-x-auto">
{tabs.map(t => (
<button
key={t.key}
onClick={() => onChange(t.key)}
className={`px-4 py-2 -mb-px border-b-2 whitespace-nowrap ${
active === t.key
? 'border-brand-500 text-ink-50'
: 'border-transparent text-ink-300 hover:text-ink-100'
}`}
>
{t.label}
</button>
))}
</div>
);
}
// Binds the active hub tab to a `?tab=` query param so the back-compat
// redirects (e.g. /bans → /players?tab=bans) land on the right sub-tab
// and the choice survives a refresh / shared link. The default tab
// drops the param entirely to keep URLs clean.
export function useTabParam<T extends string>(def: T, valid: readonly T[]): [T, (t: T) => void] {
const [params, setParams] = useSearchParams();
const raw = params.get('tab');
const active = (valid as readonly string[]).includes(raw ?? '') ? (raw as T) : def;
const set = (t: T) => {
const next = new URLSearchParams(params);
if (t === def) next.delete('tab');
else next.set('tab', t);
setParams(next, { replace: true });
};
return [active, set];
}
@@ -0,0 +1,25 @@
import { Audit } from './Audit';
import { PlayerLogs } from './PlayerLogs';
import { PageHeader } from '../components/PageHeader';
import { Tabs, useTabParam } from '../components/Tabs';
// Both log views in one place: the admin audit trail and the per-player
// HTTP request ring buffer. Old /audit and /logs routes redirect here.
export function Activity() {
const [tab, setTab] = useTabParam<'audit' | 'logs'>('audit', ['audit', 'logs']);
return (
<div>
<PageHeader title="Activity" blurb="Admin audit trail and per-player HTTP request logs." />
<Tabs
tabs={[
{ key: 'audit', label: 'Audit log' },
{ key: 'logs', label: 'Player requests' },
]}
active={tab}
onChange={setTab}
/>
{tab === 'audit' && <Audit embedded />}
{tab === 'logs' && <PlayerLogs embedded />}
</div>
);
}
+23 -15
View File
@@ -6,26 +6,34 @@ import { Empty } from '../components/Empty';
import { absoluteTime, clip, relativeTime } from '../lib/format'; import { absoluteTime, clip, relativeTime } from '../lib/format';
import { RefreshCw } from '../components/Icons'; import { RefreshCw } from '../components/Icons';
export function Audit() { export function Audit({ embedded }: { embedded?: boolean } = {}) {
const [take, setTake] = useState(200); const [take, setTake] = useState(200);
const { data, loading, error, refresh } = useApi<AuditEntry[]>(`/audit?take=${take}`); const { data, loading, error, refresh } = useApi<AuditEntry[]>(`/audit?take=${take}`);
const controls = (
<>
<select value={take} onChange={e => setTake(parseInt(e.target.value))} className="input w-24 text-xs !py-1.5">
<option value={100}>last 100</option>
<option value={200}>last 200</option>
<option value={500}>last 500</option>
</select>
<button onClick={refresh} className="btn-secondary text-xs" disabled={loading}>
<RefreshCw className={loading ? 'animate-spin' : ''} /> Refresh
</button>
</>
);
return ( return (
<div> <div>
<PageHeader {embedded ? (
title="Audit log" <div className="flex justify-end gap-2 mb-3">{controls}</div>
blurb="Every admin action with actor, target, and reason. Newest first." ) : (
actions={<> <PageHeader
<select value={take} onChange={e => setTake(parseInt(e.target.value))} className="input w-24 text-xs !py-1.5"> title="Audit log"
<option value={100}>last 100</option> blurb="Every admin action with actor, target, and reason. Newest first."
<option value={200}>last 200</option> actions={controls}
<option value={500}>last 500</option> />
</select> )}
<button onClick={refresh} className="btn-secondary text-xs" disabled={loading}>
<RefreshCw className={loading ? 'animate-spin' : ''} /> Refresh
</button>
</>}
/>
{error && <div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger">{error}</div>} {error && <div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger">{error}</div>}
+7 -5
View File
@@ -8,14 +8,16 @@ import { Empty } from '../components/Empty';
import { absoluteTime, relativeTime } from '../lib/format'; import { absoluteTime, relativeTime } from '../lib/format';
import { RefreshCw, Trash } from '../components/Icons'; import { RefreshCw, Trash } from '../components/Icons';
export function Bans() { export function Bans({ embedded }: { embedded?: boolean } = {}) {
const [tab, setTab] = useState<'players' | 'ip'>('players'); const [tab, setTab] = useState<'players' | 'ip'>('players');
return ( return (
<div> <div>
<PageHeader {!embedded && (
title="Bans" <PageHeader
blurb="Player bans and IP-level bans, all in one place." title="Bans"
/> blurb="Player bans and IP-level bans, all in one place."
/>
)}
<div className="flex border-b border-ink-800 text-sm mb-4"> <div className="flex border-b border-ink-800 text-sm mb-4">
{(['players', 'ip'] as const).map(t => ( {(['players', 'ip'] as const).map(t => (
<button <button
@@ -6,7 +6,7 @@ import { PageHeader } from '../components/PageHeader';
import { useToast } from '../components/Toast'; import { useToast } from '../components/Toast';
import { Plus, Trash } from '../components/Icons'; import { Plus, Trash } from '../components/Icons';
export function CommunityBoard() { export function CommunityBoard({ embedded }: { embedded?: boolean } = {}) {
const { data: roomList } = useApi<Room[]>('/rooms'); const { data: roomList } = useApi<Room[]>('/rooms');
const { data: players } = useApi<Player[]>('/players?take=500'); const { data: players } = useApi<Player[]>('/players?take=500');
const [state, setState] = useState<CommunityBoardState | null>(null); const [state, setState] = useState<CommunityBoardState | null>(null);
@@ -37,13 +37,19 @@ export function CommunityBoard() {
} }
}; };
const saveBtn = <button onClick={save} disabled={busy} className="btn-primary text-xs">{busy ? 'Saving…' : 'Save all'}</button>;
return ( return (
<div> <div>
<PageHeader {embedded ? (
title="Community board" <div className="flex justify-end mb-3">{saveBtn}</div>
blurb="The dorm community panel the watch fetches from /api/communityboard/v1/current. Edits go live instantly." ) : (
actions={<button onClick={save} disabled={busy} className="btn-primary text-xs">{busy ? 'Saving…' : 'Save all'}</button>} <PageHeader
/> title="Community board"
blurb="The dorm community panel the watch fetches from /api/communityboard/v1/current. Edits go live instantly."
actions={saveBtn}
/>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Announcement state={state} setState={setState} /> <Announcement state={state} setState={setState} />
@@ -0,0 +1,26 @@
import { CommunityBoard } from './CommunityBoard';
import { LoadingTips } from './LoadingTips';
import { PageHeader } from '../components/PageHeader';
import { Tabs, useTabParam } from '../components/Tabs';
// The two player-facing content surfaces the watch pulls on dorm load —
// the community board and the loading-screen tips — share one page.
// Old /community and /loading-tips routes redirect here.
export function Content() {
const [tab, setTab] = useTabParam<'community' | 'tips'>('community', ['community', 'tips']);
return (
<div>
<PageHeader title="Content" blurb="Player-facing content shown on dorm load: the community board and loading-screen tips." />
<Tabs
tabs={[
{ key: 'community', label: 'Community board' },
{ key: 'tips', label: 'Loading tips' },
]}
active={tab}
onChange={setTab}
/>
{tab === 'community' && <CommunityBoard embedded />}
{tab === 'tips' && <LoadingTips embedded />}
</div>
);
}
+36 -36
View File
@@ -1,8 +1,6 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { api, get } from '../lib/api'; import { api, get } from '../lib/api';
import type { Player } from '../lib/types';
import { useApi } from '../lib/useApi'; import { useApi } from '../lib/useApi';
import { PageHeader } from '../components/PageHeader';
import { useToast } from '../components/Toast'; import { useToast } from '../components/Toast';
import { Confirm } from '../components/Confirm'; import { Confirm } from '../components/Confirm';
import { RefreshCw, Search, Trash } from '../components/Icons'; import { RefreshCw, Search, Trash } from '../components/Icons';
@@ -27,12 +25,14 @@ const RARITY_OPTIONS: Array<{ value: number; label: string }> = [
]; ];
interface AvatarItem { interface AvatarItem {
// The exact RecNet desc to gift: a bare base GUID (default colour) or a
// "{guid},{swatch},{mask}," string for a colour variant.
desc: string;
guid: string; guid: string;
slot: number; slot: number;
friendlyName: string; friendlyName: string;
tooltip: string; color: string | null;
rarity: number; isVariant: boolean;
safe: boolean;
} }
// Gifting fires the in-game gift-box popup by inserting a GiftPackageEntity // Gifting fires the in-game gift-box popup by inserting a GiftPackageEntity
@@ -41,10 +41,11 @@ interface AvatarItem {
// and the consume endpoint writes the avatar item into their inventory. // and the consume endpoint writes the avatar item into their inventory.
// This is the only flow that triggers the popup — directly poking // This is the only flow that triggers the popup — directly poking
// InventoryJson skips it entirely. // InventoryJson skips it entirely.
export function Gift() { // Per-player gift composer, rendered as the "Gift" tab of the player
const { data: players } = useApi<Player[]>('/players?take=500'); // detail modal. The recipient is fixed to the open player, so there's
const [playerId, setPlayerId] = useState<number | null>(null); // no picker and no page chrome — just the reward builder, presentation
// options, and that player's pending-gift inbox.
export function GiftPanel({ playerId }: { playerId: number }) {
const [includeItem, setIncludeItem] = useState(true); const [includeItem, setIncludeItem] = useState(true);
const [includeCurrency, setIncludeCurrency] = useState(false); const [includeCurrency, setIncludeCurrency] = useState(false);
const [includeXp, setIncludeXp] = useState(false); const [includeXp, setIncludeXp] = useState(false);
@@ -66,12 +67,14 @@ export function Gift() {
const toast = useToast(); const toast = useToast();
const send = async () => { const send = async () => {
if (!playerId) return toast.push('Pick a recipient first', 'error');
if (!includeItem && !includeCurrency && !includeXp) { if (!includeItem && !includeCurrency && !includeXp) {
return toast.push('Pick at least one reward (item / currency / xp)', 'error'); return toast.push('Pick at least one reward (item / currency / xp)', 'error');
} }
const guid = includeItem ? (manualGuid.trim() || item?.guid) : null; // The picked item carries the full desc (colour variants included);
if (includeItem && !guid) { // a manually pasted value may be a bare GUID or a full desc — both are
// normalised server-side.
const desc = includeItem ? (manualGuid.trim() || item?.desc) : null;
if (includeItem && !desc) {
return toast.push('Pick an avatar item from the list, or paste a GUID', 'error'); return toast.push('Pick an avatar item from the list, or paste a GUID', 'error');
} }
setBusy(true); setBusy(true);
@@ -79,7 +82,7 @@ export function Gift() {
const res = await api<{ id: number }>(`/players/${playerId}/gift`, { const res = await api<{ id: number }>(`/players/${playerId}/gift`, {
method: 'POST', method: 'POST',
body: { body: {
AvatarItemGuid: includeItem ? guid : null, AvatarItemGuid: includeItem ? desc : null,
AvatarItemType: includeItem ? avatarItemType : null, AvatarItemType: includeItem ? avatarItemType : null,
CurrencyType: includeCurrency ? currencyType : null, CurrencyType: includeCurrency ? currencyType : null,
Currency: includeCurrency ? currencyAmount : null, Currency: includeCurrency ? currencyAmount : null,
@@ -101,23 +104,14 @@ export function Gift() {
return ( return (
<div> <div>
<PageHeader <p className="text-xs text-ink-400 mb-4">
title="Send a gift" Drops a wrapped gift box on this player's HUD — they tap to open it and the rewards land in their inventory / wallet.
blurb="Drops a wrapped gift box on the recipient's HUD — they tap to open it and the rewards land in their inventory / wallet." </p>
/>
{playerId !== null && <PendingGifts playerId={playerId} refreshKey={pendingRefreshKey} />} <PendingGifts playerId={playerId} refreshKey={pendingRefreshKey} />
<div className="grid grid-cols-1 lg:grid-cols-[1fr,360px] gap-4 max-w-5xl"> <div className="grid grid-cols-1 lg:grid-cols-[1fr,360px] gap-4">
<div className="card !p-5 space-y-4"> <div className="card !p-5 space-y-4">
<label className="flex flex-col gap-1">
<span className="label">Recipient</span>
<select value={playerId ?? ''} onChange={e => setPlayerId(e.target.value ? parseInt(e.target.value) : null)} className="input">
<option value="">— pick a player —</option>
{(players ?? []).map(p => <option key={p.id} value={p.id}>{p.displayName || p.username} · @{p.username} · #{p.id}</option>)}
</select>
</label>
<div className="space-y-2"> <div className="space-y-2">
<label className="flex items-center gap-2 text-sm text-ink-100 font-medium"> <label className="flex items-center gap-2 text-sm text-ink-100 font-medium">
<input type="checkbox" checked={includeItem} onChange={e => setIncludeItem(e.target.checked)} className="size-4 accent-brand-500" /> <input type="checkbox" checked={includeItem} onChange={e => setIncludeItem(e.target.checked)} className="size-4 accent-brand-500" />
@@ -198,7 +192,7 @@ export function Gift() {
<span className="label">Message (shown on the open card)</span> <span className="label">Message (shown on the open card)</span>
<textarea value={message} onChange={e => setMessage(e.target.value)} rows={3} className="input" /> <textarea value={message} onChange={e => setMessage(e.target.value)} rows={3} className="input" />
</label> </label>
<button onClick={send} disabled={busy || !playerId} className="btn-primary w-full text-xs"> <button onClick={send} disabled={busy} className="btn-primary w-full text-xs">
{busy ? 'Sending…' : 'Send gift'} {busy ? 'Sending…' : 'Send gift'}
</button> </button>
<div className="text-[11px] text-ink-500"> <div className="text-[11px] text-ink-500">
@@ -219,7 +213,10 @@ function AvatarItemPicker({ selected, onSelect }: { selected: AvatarItem | null;
const term = search.trim().toLowerCase(); const term = search.trim().toLowerCase();
return (catalog ?? []).filter(it => { return (catalog ?? []).filter(it => {
if (slotFilter !== 'all' && it.slot !== slotFilter) return false; if (slotFilter !== 'all' && it.slot !== slotFilter) return false;
if (term && !it.friendlyName.toLowerCase().includes(term) && !it.guid.toLowerCase().includes(term)) return false; if (term
&& !it.friendlyName.toLowerCase().includes(term)
&& !(it.color ?? '').toLowerCase().includes(term)
&& !it.guid.toLowerCase().includes(term)) return false;
return true; return true;
}); });
}, [catalog, search, slotFilter]); }, [catalog, search, slotFilter]);
@@ -244,21 +241,24 @@ function AvatarItemPicker({ selected, onSelect }: { selected: AvatarItem | null;
{filtered.length > 0 && ( {filtered.length > 0 && (
<div className="table-scroll"><table className="w-full text-sm min-w-[560px]"> <div className="table-scroll"><table className="w-full text-sm min-w-[560px]">
<tbody className="divide-y divide-ink-800"> <tbody className="divide-y divide-ink-800">
{filtered.slice(0, 200).map(it => ( {filtered.slice(0, 500).map(it => (
<tr <tr
key={it.guid} key={it.desc}
onClick={() => onSelect(it)} onClick={() => onSelect(it)}
className={`cursor-pointer table-row-hover ${selected?.guid === it.guid ? 'bg-brand-500/10' : ''}`} className={`cursor-pointer table-row-hover ${selected?.desc === it.desc ? 'bg-brand-500/10' : ''}`}
> >
<td className="px-3 py-1.5"> <td className="px-3 py-1.5">
<div className="text-ink-100 text-sm">{it.friendlyName || <span className="text-ink-500">unnamed</span>}</div> <div className="text-ink-100 text-sm flex items-center gap-1.5">
<div className="font-mono text-[10px] text-ink-500">{it.guid}</div> {it.friendlyName || <span className="text-ink-500">unnamed</span>}
{it.isVariant && <span className="badge-neutral text-[10px]">{it.color}</span>}
</div>
<div className="font-mono text-[10px] text-ink-500">{it.desc}</div>
</td> </td>
<td className="px-3 py-1.5 text-xs text-ink-300 whitespace-nowrap">{SLOT_NAMES[it.slot] ?? `slot ${it.slot}`}</td> <td className="px-3 py-1.5 text-xs text-ink-300 whitespace-nowrap">{SLOT_NAMES[it.slot] ?? `slot ${it.slot}`}</td>
</tr> </tr>
))} ))}
{filtered.length > 200 && ( {filtered.length > 500 && (
<tr><td colSpan={2} className="p-3 text-center text-xs text-ink-500">+ {filtered.length - 200} more — refine the search.</td></tr> <tr><td colSpan={2} className="p-3 text-center text-xs text-ink-500">+ {filtered.length - 500} more — refine the search.</td></tr>
)} )}
</tbody> </tbody>
</table></div> </table></div>
@@ -1,36 +0,0 @@
import { PageHeader } from '../components/PageHeader';
// Maker-pen-anywhere needs server-side integration with CdnController
// (substitute the room blob for flagged players so they get the
// all-permissions PersistedRoomData) AND a PlayerEntity.HasGlobalMakerPen
// column with an EF migration. Wiring is non-trivial enough to keep this
// page as a clearly-labeled placeholder until that lands — saves shipping
// a button that does nothing.
export function Grants() {
return (
<div>
<PageHeader
title="Grant maker pen"
blurb="Flag players who should receive maker-pen permissions in every room they load."
/>
<div className="card !p-6 max-w-2xl space-y-3 text-sm text-ink-300">
<div className="rounded-lg border border-warn/30 bg-warn/10 px-3 py-2 text-xs text-warn">
<strong className="text-warn">Not yet wired up.</strong> The endpoint and CDN integration land in a follow-up commit.
</div>
<p>
The plan: add <span className="font-mono text-xs text-ink-100">HasGlobalMakerPen</span> to
<span className="font-mono text-xs text-ink-100"> PlayerEntity</span>, intercept room blob
fetches in <span className="font-mono text-xs text-ink-100">CdnController</span> for flagged players,
and substitute the all-permissions default blob that
<span className="font-mono text-xs text-ink-100"> RoomDataBlobService</span> already produces.
Result: the player loads any room with full Maker Pen / invention spawn permissions, regardless of the
room's owner-defined roles.
</p>
<p className="text-xs text-ink-400">
Until then, use the Player → Grants tab to bestow per-item inventory grants (Maker Pen is granted via
inventory in some flows).
</p>
</div>
</div>
);
}
@@ -29,7 +29,7 @@ function blankTip(): LoadingScreenTip {
}; };
} }
export function LoadingTips() { export function LoadingTips({ embedded }: { embedded?: boolean } = {}) {
const [tips, setTips] = useState<LoadingScreenTip[] | null>(null); const [tips, setTips] = useState<LoadingScreenTip[] | null>(null);
const [err, setErr] = useState<string | null>(null); const [err, setErr] = useState<string | null>(null);
const toast = useToast(); const toast = useToast();
@@ -100,13 +100,19 @@ export function LoadingTips() {
} }
}; };
const addBtn = <button onClick={addNew} className="btn-primary text-xs"><Plus /> Add tip</button>;
return ( return (
<div> <div>
<PageHeader {embedded ? (
title="Loading screen tips" <div className="flex justify-end mb-3">{addBtn}</div>
blurb="The list the watch fetches from cdn.localhost/config/LoadingScreenTipData on every dorm load. Edit, upload an image, or scope a tip to specific rooms — saves are live immediately." ) : (
actions={<button onClick={addNew} className="btn-primary text-xs"><Plus /> Add tip</button>} <PageHeader
/> title="Loading screen tips"
blurb="The list the watch fetches from cdn.localhost/config/LoadingScreenTipData on every dorm load. Edit, upload an image, or scope a tip to specific rooms — saves are live immediately."
actions={addBtn}
/>
)}
{err && ( {err && (
<div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger mb-4"> <div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger mb-4">
@@ -0,0 +1,31 @@
import { Players } from './Players';
import { Bans } from './Bans';
import { Reports } from './Reports';
import { PageHeader } from '../components/PageHeader';
import { Tabs, useTabParam } from '../components/Tabs';
// Unified moderation hub. The account directory (with its per-player
// detail / ban / grant / gift / password modal), the active-ban list,
// and the open-report queue were three separate sidebar entries — they
// describe one workflow, so they're tabs of one page now. The old
// /bans and /reports routes redirect here with ?tab= preselected.
export function Moderation() {
const [tab, setTab] = useTabParam<'directory' | 'bans' | 'reports'>('directory', ['directory', 'bans', 'reports']);
return (
<div>
<PageHeader title="Players" blurb="Search and moderate accounts, review active bans, and triage open reports." />
<Tabs
tabs={[
{ key: 'directory', label: 'Directory' },
{ key: 'bans', label: 'Bans' },
{ key: 'reports', label: 'Reports' },
]}
active={tab}
onChange={setTab}
/>
{tab === 'directory' && <Players embedded />}
{tab === 'bans' && <Bans embedded />}
{tab === 'reports' && <Reports embedded />}
</div>
);
}
+42 -58
View File
@@ -1,13 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { api } from '../lib/api'; import { api } from '../lib/api';
import type { Player } from '../lib/types';
import { useApi } from '../lib/useApi';
import { PageHeader } from '../components/PageHeader';
import { useToast } from '../components/Toast'; import { useToast } from '../components/Toast';
export function Passwords() { // Admin password reset, rendered as a card inside the player detail
const { data: players } = useApi<Player[]>('/players?take=500'); // modal's Profile tab. The target account is the open player, so there's
const [playerId, setPlayerId] = useState<number | null>(null); // no picker — just set-and-confirm. Issues a Logout push so any active
// sessions on the old password get dropped.
export function PasswordResetCard({ playerId, username }: { playerId: number; username: string }) {
const [pw, setPw] = useState(''); const [pw, setPw] = useState('');
const [pw2, setPw2] = useState(''); const [pw2, setPw2] = useState('');
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
@@ -27,11 +26,9 @@ export function Passwords() {
const submit = async (e: React.FormEvent) => { const submit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!playerId) return;
if (pw !== pw2) return toast.push("Passwords don't match", 'error'); if (pw !== pw2) return toast.push("Passwords don't match", 'error');
if (pw.length < 8) return toast.push('Password must be at least 8 characters', 'error'); if (pw.length < 8) return toast.push('Password must be at least 8 characters', 'error');
const player = players?.find(p => p.id === playerId); if (!confirm(`Reset password for @${username}? This forces a logout on every active session.`)) return;
if (!confirm(`Reset password for @${player?.username ?? `#${playerId}`}? This forces a logout on every active session.`)) return;
setBusy(true); setBusy(true);
try { try {
await api(`/players/${playerId}/password`, { await api(`/players/${playerId}/password`, {
@@ -48,62 +45,49 @@ export function Passwords() {
}; };
return ( return (
<div> <form onSubmit={submit} className="card !p-4 md:col-span-2 space-y-3">
<PageHeader <div>
title="Reset passwords" <h3 className="text-sm font-semibold text-ink-50 mb-1">Reset password</h3>
blurb="Admin-set a new password for any account. Issues a Logout push so any active sessions on the old password get dropped." <p className="text-xs text-ink-400">
/> Admin-set a new password. The plaintext is hashed with BCrypt before write — we never store it, and the audit log
records the action but not the password. Issues a Logout push that drops any session still on the old password.
</p>
</div>
<form onSubmit={submit} className="card !p-5 max-w-xl space-y-4"> <label className="flex flex-col gap-1">
<label className="flex flex-col gap-1"> <span className="label">New password</span>
<span className="label">Account</span> <div className="flex gap-2">
<select value={playerId ?? ''} onChange={e => setPlayerId(e.target.value ? parseInt(e.target.value) : null)} required className="input">
<option value="">— pick an account —</option>
{(players ?? []).map(p => <option key={p.id} value={p.id}>{p.displayName || p.username} · @{p.username} · #{p.id}</option>)}
</select>
</label>
<label className="flex flex-col gap-1">
<span className="label">New password</span>
<div className="flex gap-2">
<input
type={show ? 'text' : 'password'}
value={pw}
onChange={e => setPw(e.target.value)}
minLength={8}
required
autoComplete="new-password"
className="input flex-1 font-mono"
/>
<button type="button" onClick={() => setShow(s => !s)} className="btn-ghost text-xs">{show ? 'Hide' : 'Show'}</button>
<button type="button" onClick={random} className="btn-secondary text-xs">Random</button>
</div>
</label>
<label className="flex flex-col gap-1">
<span className="label">Confirm new password</span>
<input <input
type={show ? 'text' : 'password'} type={show ? 'text' : 'password'}
value={pw2} value={pw}
onChange={e => setPw2(e.target.value)} onChange={e => setPw(e.target.value)}
minLength={8} minLength={8}
required
autoComplete="new-password" autoComplete="new-password"
className="input font-mono" className="input flex-1 font-mono"
/> />
</label> <button type="button" onClick={() => setShow(s => !s)} className="btn-ghost text-xs">{show ? 'Hide' : 'Show'}</button>
<button type="button" onClick={random} className="btn-secondary text-xs">Random</button>
<div className="rounded-lg border border-warn/30 bg-warn/10 px-3 py-2 text-xs text-warn">
The plaintext is hashed with BCrypt before write — we never store it. The audit log records the action but not the password.
</div> </div>
</label>
<div className="flex justify-end gap-2"> <label className="flex flex-col gap-1">
<button type="button" onClick={() => { setPw(''); setPw2(''); setShow(false); }} className="btn-ghost text-xs" disabled={busy}>Clear</button> <span className="label">Confirm new password</span>
<button className="btn-primary text-xs" disabled={busy || !playerId || pw.length < 8 || pw !== pw2}> <input
{busy ? 'Updating…' : 'Update password'} type={show ? 'text' : 'password'}
</button> value={pw2}
</div> onChange={e => setPw2(e.target.value)}
</form> minLength={8}
</div> autoComplete="new-password"
className="input font-mono"
/>
</label>
<div className="flex justify-end gap-2">
<button type="button" onClick={() => { setPw(''); setPw2(''); setShow(false); }} className="btn-ghost text-xs" disabled={busy}>Clear</button>
<button className="btn-primary text-xs" disabled={busy || pw.length < 8 || pw !== pw2}>
{busy ? 'Updating…' : 'Update password'}
</button>
</div>
</form>
); );
} }
@@ -7,7 +7,7 @@ import { Empty } from '../components/Empty';
import { absoluteTime, clip, relativeTime } from '../lib/format'; import { absoluteTime, clip, relativeTime } from '../lib/format';
import { RefreshCw } from '../components/Icons'; import { RefreshCw } from '../components/Icons';
export function PlayerLogs() { export function PlayerLogs({ embedded }: { embedded?: boolean } = {}) {
const { data: players } = useApi<Player[]>('/players?take=500'); const { data: players } = useApi<Player[]>('/players?take=500');
const [pid, setPid] = useState<number | null>(null); const [pid, setPid] = useState<number | null>(null);
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
@@ -49,10 +49,12 @@ export function PlayerLogs() {
return ( return (
<div> <div>
<PageHeader {!embedded && (
title="Player request logs" <PageHeader
blurb="Last few hundred HTTP calls per player, pulled from the Redis ring buffer. Useful when a player reports something weird." title="Player request logs"
/> blurb="Last few hundred HTTP calls per player, pulled from the Redis ring buffer. Useful when a player reports something weird."
/>
)}
<div className="card !p-4 mb-4 grid grid-cols-1 md:grid-cols-[2fr,1fr,120px,auto,auto] gap-2 items-end"> <div className="card !p-4 mb-4 grid grid-cols-1 md:grid-cols-[2fr,1fr,120px,auto,auto] gap-2 items-end">
<label className="flex flex-col gap-1"> <label className="flex flex-col gap-1">
+24 -13
View File
@@ -9,8 +9,10 @@ import { useToast } from '../components/Toast';
import { Empty } from '../components/Empty'; import { Empty } from '../components/Empty';
import { PlayerAvatar } from '../components/PlayerAvatar'; import { PlayerAvatar } from '../components/PlayerAvatar';
import { RefreshCw, Search } from '../components/Icons'; import { RefreshCw, Search } from '../components/Icons';
import { GiftPanel } from './Gift';
import { PasswordResetCard } from './Passwords';
export function Players() { export function Players({ embedded }: { embedded?: boolean } = {}) {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [rows, setRows] = useState<Player[] | null>(null); const [rows, setRows] = useState<Player[] | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -38,18 +40,24 @@ export function Players() {
useEffect(() => { search(query); }, [query, search]); useEffect(() => { search(query); }, [query, search]);
const refreshBtn = (
<button onClick={() => search(query)} className="btn-secondary text-xs" disabled={loading}>
<RefreshCw className={loading ? 'animate-spin' : ''} />
Refresh
</button>
);
return ( return (
<div> <div>
<PageHeader {embedded ? (
title="Players" <div className="flex justify-end mb-3">{refreshBtn}</div>
blurb="Search and moderate every account in the database." ) : (
actions={ <PageHeader
<button onClick={() => search(query)} className="btn-secondary text-xs" disabled={loading}> title="Players"
<RefreshCw className={loading ? 'animate-spin' : ''} /> blurb="Search and moderate every account in the database."
Refresh actions={refreshBtn}
</button> />
} )}
/>
<div className="card overflow-hidden"> <div className="card overflow-hidden">
<div className="border-b border-ink-800 p-3 flex items-center gap-2"> <div className="border-b border-ink-800 p-3 flex items-center gap-2">
@@ -146,7 +154,7 @@ export function Players() {
function PlayerDetail({ id, onClose, onChanged }: { id: number; onClose: () => void; onChanged: () => void }) { function PlayerDetail({ id, onClose, onChanged }: { id: number; onClose: () => void; onChanged: () => void }) {
const [data, setData] = useState<PlayerDetail | null>(null); const [data, setData] = useState<PlayerDetail | null>(null);
const [err, setErr] = useState<string | null>(null); const [err, setErr] = useState<string | null>(null);
const [tab, setTab] = useState<'overview' | 'mod' | 'grants' | 'profile'>('overview'); const [tab, setTab] = useState<'overview' | 'mod' | 'grants' | 'gift' | 'profile'>('overview');
const reload = () => { const reload = () => {
setErr(null); setErr(null);
@@ -189,7 +197,7 @@ function PlayerDetail({ id, onClose, onChanged }: { id: number; onClose: () => v
</div> </div>
<div className="flex border-b border-ink-800 text-sm"> <div className="flex border-b border-ink-800 text-sm">
{(['overview', 'mod', 'grants', 'profile'] as const).map(t => ( {(['overview', 'mod', 'grants', 'gift', 'profile'] as const).map(t => (
<button <button
key={t} key={t}
onClick={() => setTab(t)} onClick={() => setTab(t)}
@@ -203,6 +211,7 @@ function PlayerDetail({ id, onClose, onChanged }: { id: number; onClose: () => v
{tab === 'overview' && <OverviewTab data={data} />} {tab === 'overview' && <OverviewTab data={data} />}
{tab === 'mod' && <ModerationTab data={data} onChanged={onChanged} />} {tab === 'mod' && <ModerationTab data={data} onChanged={onChanged} />}
{tab === 'grants' && <GrantsTab data={data} onChanged={reload} />} {tab === 'grants' && <GrantsTab data={data} onChanged={reload} />}
{tab === 'gift' && <GiftPanel playerId={data.id} />}
{tab === 'profile' && <ProfileTab data={data} onChanged={reload} />} {tab === 'profile' && <ProfileTab data={data} onChanged={reload} />}
</div> </div>
)} )}
@@ -537,6 +546,8 @@ function ProfileTab({ data, onChanged }: { data: PlayerDetail; onChanged: () =>
className="btn-danger text-xs" className="btn-danger text-xs"
>Reset to starter</button> >Reset to starter</button>
</div> </div>
<PasswordResetCard playerId={data.id} username={data.username} />
</div> </div>
); );
} }
+16 -10
View File
@@ -9,21 +9,27 @@ import { useToast } from '../components/Toast';
import { absoluteTime, relativeTime } from '../lib/format'; import { absoluteTime, relativeTime } from '../lib/format';
import { RefreshCw } from '../components/Icons'; import { RefreshCw } from '../components/Icons';
export function Reports() { export function Reports({ embedded }: { embedded?: boolean } = {}) {
const { data, loading, error, refresh } = useApi<Report[]>('/reports?take=100'); const { data, loading, error, refresh } = useApi<Report[]>('/reports?take=100');
const [resolving, setResolving] = useState<Report | null>(null); const [resolving, setResolving] = useState<Report | null>(null);
const refreshBtn = (
<button onClick={refresh} className="btn-secondary text-xs" disabled={loading}>
<RefreshCw className={loading ? 'animate-spin' : ''} /> Refresh
</button>
);
return ( return (
<div> <div>
<PageHeader {embedded ? (
title="Open reports" <div className="flex justify-end mb-3">{refreshBtn}</div>
blurb="Oldest first. Resolving a report only closes the ticket — issue the actual moderation action separately." ) : (
actions={ <PageHeader
<button onClick={refresh} className="btn-secondary text-xs" disabled={loading}> title="Open reports"
<RefreshCw className={loading ? 'animate-spin' : ''} /> Refresh blurb="Oldest first. Resolving a report only closes the ticket — issue the actual moderation action separately."
</button> actions={refreshBtn}
} />
/> )}
{error && <div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger">{error}</div>} {error && <div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger">{error}</div>}
+4 -1
View File
@@ -8,7 +8,7 @@ import { Empty } from '../components/Empty';
import { Modal } from '../components/Modal'; import { Modal } from '../components/Modal';
import { useToast } from '../components/Toast'; import { useToast } from '../components/Toast';
import { Confirm } from '../components/Confirm'; import { Confirm } from '../components/Confirm';
import { RefreshCw, Trash } from '../components/Icons'; import { RefreshCw, Trash, Upload } from '../components/Icons';
export function Rooms() { export function Rooms() {
const { data, loading, error, refresh } = useApi<Room[]>('/rooms'); const { data, loading, error, refresh } = useApi<Room[]>('/rooms');
@@ -55,6 +55,9 @@ export function Rooms() {
title="Rooms" title="Rooms"
blurb="Every room in the DB. Archive (trash icon) is soft and reversible. Purge is a hard wipe — drops scene rows + per-room blobs + thumbnail and requires typing the room name twice." blurb="Every room in the DB. Archive (trash icon) is soft and reversible. Purge is a hard wipe — drops scene rows + per-room blobs + thumbnail and requires typing the room name twice."
actions={<> actions={<>
<Link to="/import-room" className="btn-primary text-xs">
<Upload /> Import room
</Link>
<button onClick={refresh} className="btn-secondary text-xs" disabled={loading}> <button onClick={refresh} className="btn-secondary text-xs" disabled={loading}>
<RefreshCw className={loading ? 'animate-spin' : ''} /> Refresh <RefreshCw className={loading ? 'animate-spin' : ''} /> Refresh
</button> </button>
+74 -11
View File
@@ -6,6 +6,7 @@ import { Plus, RefreshCw, Trash } from '../components/Icons';
interface ServerSettings { interface ServerSettings {
signupsDisabled: boolean; signupsDisabled: boolean;
globalFriendsEnabled: boolean;
weeklyChallengesCompletedRequired: boolean; weeklyChallengesCompletedRequired: boolean;
updatedAt: string; updatedAt: string;
} }
@@ -58,7 +59,7 @@ interface RewardOptions {
consumables: RewardOption[]; consumables: RewardOption[];
} }
export function Settings() { export function Settings({ embedded }: { embedded?: boolean } = {}) {
const [settings, setSettings] = useState<ServerSettings | null>(null); const [settings, setSettings] = useState<ServerSettings | null>(null);
const [weekly, setWeekly] = useState<WeeklyChallengeSettings | null>(null); const [weekly, setWeekly] = useState<WeeklyChallengeSettings | null>(null);
const [rewardOptions, setRewardOptions] = useState<RewardOptions>({ const [rewardOptions, setRewardOptions] = useState<RewardOptions>({
@@ -108,6 +109,30 @@ export function Settings() {
} }
}; };
const toggleGlobalFriends = async () => {
if (!settings) return;
const next = !settings.globalFriendsEnabled;
const verb = next ? 'make everyone friends' : 'turn off all-friends';
if (!confirm(
next
? 'Make every account friends with every other account? No friend rows are written — it\'s synthesized live, and connected players refresh instantly. Toggle off to revert.'
: `Really ${verb}? Players will revert to their real friends list.`,
)) return;
setBusy(true);
try {
const updated = await api<ServerSettings>('/settings/global-friends', {
method: 'POST',
body: { Enabled: next },
});
setSettings(updated);
toast.push(next ? 'Everyone is now friends — players refreshing live.' : 'All-friends turned off.', 'success');
} catch (e) {
toast.push((e as Error).message, 'error');
} finally {
setBusy(false);
}
};
const updateWeeklyChallenge = (index: number, patch: Partial<WeeklyChallenge>) => { const updateWeeklyChallenge = (index: number, patch: Partial<WeeklyChallenge>) => {
setWeekly((current) => { setWeekly((current) => {
if (!current) return current; if (!current) return current;
@@ -253,18 +278,24 @@ export function Settings() {
} }
}; };
const refreshBtn = (
<button onClick={load} className="btn-secondary text-xs" disabled={busy}>
<RefreshCw className={busy ? 'animate-spin' : ''} />
Refresh
</button>
);
return ( return (
<div> <div>
<PageHeader {embedded ? (
title="Server settings" <div className="flex justify-end mb-3">{refreshBtn}</div>
blurb="Runtime toggles applied across every replica without a redeploy." ) : (
actions={ <PageHeader
<button onClick={load} className="btn-secondary text-xs" disabled={busy}> title="Server settings"
<RefreshCw className={busy ? 'animate-spin' : ''} /> blurb="Runtime toggles applied across every replica without a redeploy."
Refresh actions={refreshBtn}
</button> />
} )}
/>
{err && ( {err && (
<div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger mb-4">{err}</div> <div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger mb-4">{err}</div>
@@ -303,6 +334,38 @@ export function Settings() {
</div> </div>
)} )}
{settings && (
<div className="card !p-5 max-w-2xl mt-5">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<h2 className="text-sm font-semibold text-ink-50">Everyone is friends</h2>
<p className="mt-1 text-xs text-ink-400">
When on, every account is treated as a friend of every other account, so players
don't have to search and friend-request each other on a small server. Nothing is
written to the database — it's synthesized live, and connected players get a refresh
signal so their friends list updates without relogging. Turn it off to instantly
revert to real friend lists. Blocks are still respected.
</p>
<p className="mt-2 text-[11px] text-ink-500">
Last changed {new Date(settings.updatedAt).toLocaleString()}.
</p>
</div>
<button
onClick={toggleGlobalFriends}
disabled={busy}
className={(settings.globalFriendsEnabled ? 'btn-danger' : 'btn-primary') + ' text-xs shrink-0'}
>
{busy ? 'Working…' : settings.globalFriendsEnabled ? 'Turn off' : 'Make everyone friends'}
</button>
</div>
<div className="mt-4 text-xs">
{settings.globalFriendsEnabled
? <span className="badge-online">Everyone is friends</span>
: <span className="badge-neutral">Normal friend lists</span>}
</div>
</div>
)}
{weekly && ( {weekly && (
<div className="card !p-5 max-w-5xl mt-5"> <div className="card !p-5 max-w-5xl mt-5">
<div className="flex flex-wrap items-start justify-between gap-4"> <div className="flex flex-wrap items-start justify-between gap-4">
@@ -0,0 +1,26 @@
import { Settings } from './Settings';
import { SignupCodes } from './SignupCodes';
import { PageHeader } from '../components/PageHeader';
import { Tabs, useTabParam } from '../components/Tabs';
// Server-wide configuration: runtime toggles plus the single-use signup
// invite codes (an access-control setting in its own right). Old
// /signup-codes route redirects here with ?tab=signup.
export function SettingsHome() {
const [tab, setTab] = useTabParam<'server' | 'signup'>('server', ['server', 'signup']);
return (
<div>
<PageHeader title="Settings" blurb="Runtime server toggles and single-use signup invite codes." />
<Tabs
tabs={[
{ key: 'server', label: 'Server' },
{ key: 'signup', label: 'Signup codes' },
]}
active={tab}
onChange={setTab}
/>
{tab === 'server' && <Settings embedded />}
{tab === 'signup' && <SignupCodes embedded />}
</div>
);
}
@@ -24,7 +24,7 @@ const STATUS_BADGE: Record<SignupCode['status'], string> = {
expired: 'badge-neutral', expired: 'badge-neutral',
}; };
export function SignupCodes() { export function SignupCodes({ embedded }: { embedded?: boolean } = {}) {
const [codes, setCodes] = useState<SignupCode[] | null>(null); const [codes, setCodes] = useState<SignupCode[] | null>(null);
const [err, setErr] = useState<string | null>(null); const [err, setErr] = useState<string | null>(null);
const [descriptor, setDescriptor] = useState(''); const [descriptor, setDescriptor] = useState('');
@@ -84,18 +84,24 @@ export function SignupCodes() {
} }
}; };
const refreshBtn = (
<button onClick={load} className="btn-secondary text-xs">
<RefreshCw />
Refresh
</button>
);
return ( return (
<div> <div>
<PageHeader {embedded ? (
title="Signup codes" <div className="flex justify-end mb-3">{refreshBtn}</div>
blurb="Single-use invite codes. Hand one to a player; they redeem it on the site's /join page to create an account while signups are disabled." ) : (
actions={ <PageHeader
<button onClick={load} className="btn-secondary text-xs"> title="Signup codes"
<RefreshCw /> blurb="Single-use invite codes. Hand one to a player; they redeem it on the site's /join page to create an account while signups are disabled."
Refresh actions={refreshBtn}
</button> />
} )}
/>
{err && ( {err && (
<div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger mb-4">{err}</div> <div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger mb-4">{err}</div>
+40 -71
View File
@@ -1,11 +1,5 @@
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { get, post } from '../lib/api'; import { post } from '../lib/api';
interface PendingDevice {
deviceId: string;
platform: number;
lastSeenAt: string;
}
// Server error code -> friendly copy. // Server error code -> friendly copy.
const ERRORS: Record<string, string> = { const ERRORS: Record<string, string> = {
@@ -16,40 +10,34 @@ const ERRORS: Record<string, string> = {
code_expired: 'That code has expired. Ask for a new one.', code_expired: 'That code has expired. Ask for a new one.',
invalid_username: 'Pick a username 2–24 characters long (letters, numbers, _ or -).', invalid_username: 'Pick a username 2–24 characters long (letters, numbers, _ or -).',
username_taken: 'That username is taken — try another.', username_taken: 'That username is taken — try another.',
missing_device: 'Pick or paste the device id from your game client.', missing_password: 'Enter a password.',
device_in_use: 'That device already has an account — just launch the game to log in.', password_too_short: 'Use at least 8 characters for your password.',
password_mismatch: 'The passwords do not match.',
}; };
export function Join() { export function Join() {
const [code, setCode] = useState(''); const [code, setCode] = useState('');
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [deviceId, setDeviceId] = useState(''); const [password, setPassword] = useState('');
const [pending, setPending] = useState<PendingDevice[]>([]); const [confirmPassword, setConfirmPassword] = useState('');
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [err, setErr] = useState<string | null>(null); const [err, setErr] = useState<string | null>(null);
const [doneUsername, setDoneUsername] = useState<string | null>(null); const [doneUsername, setDoneUsername] = useState<string | null>(null);
// Pull the devices our caller's IP was recently refused from, so the
// player can pick the one their own game client reported instead of
// hunting for the Unity device id by hand.
useEffect(() => {
get<PendingDevice[]>('/join/pending-devices')
.then((rows) => {
setPending(rows);
if (rows.length > 0) setDeviceId((cur) => cur || rows[0].deviceId);
})
.catch(() => { /* picker is a convenience; manual paste still works */ });
}, []);
const submit = async (e: React.FormEvent) => { const submit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setBusy(true); setBusy(true);
setErr(null); setErr(null);
if (password !== confirmPassword) {
setErr(ERRORS.password_mismatch);
setBusy(false);
return;
}
try { try {
const res = await post<{ ok: boolean; username: string }>('/join/redeem', { const res = await post<{ ok: boolean; username: string }>('/join/redeem', {
Code: code.trim(), Code: code.trim(),
Username: username.trim(), Username: username.trim(),
DeviceId: deviceId.trim(), Password: password,
}); });
setDoneUsername(res.username); setDoneUsername(res.username);
} catch (e) { } catch (e) {
@@ -66,8 +54,8 @@ export function Join() {
<div className="card !p-6 text-center"> <div className="card !p-6 text-center">
<h1 className="text-2xl font-semibold text-ink-50">You're in, {doneUsername}! 🎉</h1> <h1 className="text-2xl font-semibold text-ink-50">You're in, {doneUsername}! 🎉</h1>
<p className="mt-3 text-sm text-ink-300"> <p className="mt-3 text-sm text-ink-300">
Your account is ready. Launch the game through the DorkNet launcher on this Your account is ready. Launch the game through the DorkNet launcher and sign in
same device and you'll be logged straight in — no signup step needed. with your username and password.
</p> </p>
</div> </div>
</div> </div>
@@ -79,7 +67,7 @@ export function Join() {
<div> <div>
<h1 className="text-2xl font-semibold text-ink-50">Join the server</h1> <h1 className="text-2xl font-semibold text-ink-50">Join the server</h1>
<p className="text-sm text-ink-400"> <p className="text-sm text-ink-400">
Got a signup code? Redeem it here to create your account, then launch the game. Got a signup code? Redeem it here to create your account, then sign in from the game.
</p> </p>
</div> </div>
@@ -102,52 +90,33 @@ export function Join() {
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
placeholder="2–24 chars: letters, numbers, _ or -" placeholder="2–24 chars: letters, numbers, _ or -"
autoComplete="username"
/> />
</label> </label>
<div className="block"> <label className="block">
<span className="label">Your device</span> <span className="label">Password</span>
{pending.length > 0 ? ( <input
<> className="input mt-1"
<select type="password"
className="input mt-1" value={password}
value={pending.some((d) => d.deviceId === deviceId) ? deviceId : '__manual__'} onChange={(e) => setPassword(e.target.value)}
onChange={(e) => setDeviceId(e.target.value === '__manual__' ? '' : e.target.value)} placeholder="at least 8 characters"
> autoComplete="new-password"
{pending.map((d) => ( />
<option key={d.deviceId} value={d.deviceId}> </label>
{d.deviceId.slice(0, 16)}… · last seen {new Date(d.lastSeenAt).toLocaleTimeString()}
</option> <label className="block">
))} <span className="label">Confirm password</span>
<option value="__manual__">Enter device id manually…</option> <input
</select> className="input mt-1"
{!pending.some((d) => d.deviceId === deviceId) && ( type="password"
<input value={confirmPassword}
className="input mt-2 font-mono text-xs" onChange={(e) => setConfirmPassword(e.target.value)}
value={deviceId} placeholder="type it again"
onChange={(e) => setDeviceId(e.target.value)} autoComplete="new-password"
placeholder="paste your device id" />
/> </label>
)}
<p className="mt-1 text-[11px] text-ink-500">
These are devices that just tried to sign in from your network. Pick yours.
</p>
</>
) : (
<>
<input
className="input mt-1 font-mono text-xs"
value={deviceId}
onChange={(e) => setDeviceId(e.target.value)}
placeholder="paste your device id"
/>
<p className="mt-1 text-[11px] text-ink-500">
Launch the game once first — when it says signups are disabled, come back here
and your device should appear automatically.
</p>
</>
)}
</div>
{err && ( {err && (
<div className="rounded-lg border border-danger/30 bg-danger/5 px-3 py-2 text-sm text-danger">{err}</div> <div className="rounded-lg border border-danger/30 bg-danger/5 px-3 py-2 text-sm text-danger">{err}</div>
+6
View File
@@ -20,6 +20,12 @@ export default defineConfig({
emptyOutDir: true, emptyOutDir: true,
target: 'es2022', target: 'es2022',
sourcemap: true, sourcemap: true,
rollupOptions: {
output: {
entryFileNames: 'assets/[name]-join-password-[hash].js',
chunkFileNames: 'assets/[name]-join-password-[hash].js',
},
},
}, },
server: { server: {
port: 5174, port: 5174,
+31 -3
View File
@@ -33,7 +33,7 @@ Subsystems **NOT** present on this branch (these landed in December):
- Late-2020 store catalog additions - Late-2020 store catalog additions
Weekly challenges **are** present on this branch: the admin SPA's Weekly challenges **are** present on this branch: the admin SPA's
**Server settings** page edits the weekly slate, the `CompletedRequired` **Settings → Server** tab edits the weekly slate, the `CompletedRequired`
flag, and the gift (XP + tokens, plus an optional store skin/consumable flag, and the gift (XP + tokens, plus an optional store skin/consumable
that's granted straight to the player's inventory when the week's that's granted straight to the player's inventory when the week's
challenges complete). challenges complete).
@@ -104,6 +104,17 @@ AppIds, and bypasses TLS verification on the client's HTTPS calls so
self-signed certs work. (First run prints "launch the game once"; do self-signed certs work. (First run prints "launch the game once"; do
that, then re-run with `-ResumeBuild`.) that, then re-run with `-ResumeBuild`.)
### Debug console (opt-in)
The 2020 client ships a built-in dev console
(`RecRoom.Debugging.DebugConsole`) with commands like `SetTimeScale`,
`Fly`, `Teleport`, `GoToRoom`, and `KillAllEnemies` — normally locked to
developer accounts. Set `"EnableDebugConsole": true` in
`dorknet-clientmod.json` and relaunch: the mod force-toggles the console
UI on a hotkey (`"DebugConsoleToggleKey"`, default `BackQuote` = the `~`
key) and silences `CheatManager` so the movement/time commands don't drop
you to the dorm. Both default off.
If MelonLoader isn't installable on the user's machine, the legacy If MelonLoader isn't installable on the user's machine, the legacy
`tools/install-legacy-client.ps1` does the same patches via direct `tools/install-legacy-client.ps1` does the same patches via direct
byte-edits of `resources.assets` + `GameAssembly.dll`. Slower setup, byte-edits of `resources.assets` + `GameAssembly.dll`. Slower setup,
@@ -127,9 +138,9 @@ and the multi-step migrations haven't been consolidated.
### Signup codes ### Signup codes
When account creation is disabled (admin **Server settings**), the only When account creation is disabled (admin **Settings → Server**), the only
way in is an admin-issued single-use **signup code**: generate one in the way in is an admin-issued single-use **signup code**: generate one in the
admin panel's **Signup codes** page (with a descriptor + optional expiry), admin panel's **Settings → Signup codes** tab (with a descriptor + optional expiry),
hand it to the player, and they redeem it on the site's **`/join`** page — hand it to the player, and they redeem it on the site's **`/join`** page —
which creates their account bound to the device their game client which creates their account bound to the device their game client
reported, so the next launch logs straight in. The `SignupCodes` / reported, so the next launch logs straight in. The `SignupCodes` /
@@ -137,6 +148,23 @@ reported, so the next launch logs straight in. The `SignupCodes` /
an idempotent `CREATE TABLE IF NOT EXISTS` step at boot (Program.cs), so an idempotent `CREATE TABLE IF NOT EXISTS` step at boot (Program.cs), so
existing SQLite/Postgres DBs pick them up without a new migration. existing SQLite/Postgres DBs pick them up without a new migration.
### Everyone-is-friends toggle
For small servers where searching + friend-requesting each other is
friction, admin **Settings → Server → "Everyone is friends"** makes every
account a friend of every other account. It writes **no** relationship
rows — the friend graph is synthesized at read time
(`RelationshipQueries.EffectiveFriendIdsAsync`, gated on
`ServerSettings.GlobalFriendsEnabled`), so flipping it off reverts
instantly. Blocks are still honored and the system/coach account is
excluded, as are auto-generated `Player_NNN` placeholder accounts that
never set a real username (they only appear if there's a genuine friend
row). Flipping it broadcasts `RelationshipsInvalid` to every connected
watch, which calls `Relationships.RefreshList` and re-pulls
`api/relationships/v2/get` — so players see everyone appear (or disappear)
**without relogging**. It covers the friends list, the friends-online HUD,
and room-move presence fan-out.
--- ---
## Contributing ## Contributing
+6
View File
@@ -440,6 +440,12 @@ $cfg = [ordered]@{
PhotonCloudRegion = $PhotonCloudRegion PhotonCloudRegion = $PhotonCloudRegion
InjectAuthValues = $true InjectAuthValues = $true
EnableTlsTrustBypass = $true EnableTlsTrustBypass = $true
# Opt-in: force-enable the client's built-in debug console (dev gate
# bypassed) + silence CheatManager so SetTimeScale / Fly / Teleport etc.
# don't drop you to the dorm. Flip to $true and relaunch; press the
# toggle key (a UnityEngine.KeyCode name; BackQuote = the `~` key).
EnableDebugConsole = $false
DebugConsoleToggleKey = 'BackQuote'
} }
Write-Step "Writing mod config -> $cfgPath" Write-Step "Writing mod config -> $cfgPath"
[System.IO.File]::WriteAllText($cfgPath, ($cfg | ConvertTo-Json -Depth 4)) [System.IO.File]::WriteAllText($cfgPath, ($cfg | ConvertTo-Json -Depth 4))