Pastebin

Untitled

text 8 views Created: October 28, 2026 at 22:06:44 EDT
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
// COMPLETE REPLACEMENT: copy this entire file into a new PlayFab CloudScript revision.
// Contains the original handlers supplied in this conversation plus OoglyClaimDailyReward.
//
// REVISION 18 (2026-10-15) = revision 17 with the security lockdown (Phase 0 of
// SECURITY-PLAN-2026-10-15.md):
// - Removed GrantItemToPlayer, GivePlayerCurrency, RemovePlayerCurrency, ConsoleGiveCurrency,
//   ConsoleGiveCosmetic and ValidateOculusPurchaseAndGrant. Any logged-in client could call them to
//   mint currency or items for anyone, or drain anyone. No game code, Rule or scheduled task calls them.
// - GrantCatalogItemToCurrentPlayer, GrantEventReward and RepairIAPItems only grant items from the
//   server tables in the OB SECURITY section at the end. RepairIAPItems and the daily VIP check ask
//   Meta whether the player really owns the SKU, instead of trusting client-written OwnedIAP.
// - The Be The Monster code sits at top level again. In revision 17 it had been pasted inside
//   obDailyReply after its return, so OoglyBeMonsterRoundReward was never registered.

function getWebhookUrl() {
    var data = server.GetTitleInternalData({ Keys: ["DiscordWebhookURL"] });
    return data.Data["DiscordWebhookURL"] || "";
}

// LOGIN & VALIDATION

handlers.NewLogin = function(args, context) {
    if (context.playStreamEvent.CustomTags['userProof'] != undefined && context.playStreamEvent.CustomTags['userId'] != undefined) {
        var url = 'https://graph.oculus.com/user_nonce_validate?nonce=' + context.playStreamEvent.CustomTags['userProof'] + '&user_id=' + context.playStreamEvent.CustomTags['userId'] + '&access_token=OC|6755960357813543|dceda3c54c45b49a8502d52b320b7e6e';
        var responseString = http.request(url, 'post', '', 'application/json', {});
        if (JSON.parse(responseString).is_valid != undefined) {
            if (!JSON.parse(responseString).is_valid) {
                server.BanUsers({
                    'DurationInHours': 64,
                    'PlayFabId': currentPlayerId,
                    'Reason': 'Validation was unsuccessful',
                    'IPAddress': context.playStreamEvent.IPV4Address
                });
                return { error: 'Unable To Login' };
            }
        } else {
            server.BanUsers({
                'DurationInHours': 64,
                'PlayFabId': currentPlayerId,
                'Reason': 'Custom tags are null',
                'IPAddress': context.playStreamEvent.IPV4Address
            });
            return { error: 'Custom tags were null' };
        }
    } else {
        server.BanUsers({
            'DurationInHours': 64,
            'PlayFabId': currentPlayerId,
            'Reason': 'Custom tags are null',
            'IPAddress': context.playStreamEvent.IPV4Address
        });
        return { error: 'Custom tags were null' };
    }
};

handlers.BetterAuthenticator = function(args) {
    var getUserInforesult = server.GetUserAccountInfo({PlayFabId: currentPlayerId}).UserInfo;
    if (!getUserInforesult.ServerCustomIdInfo) {
        var contentBody = {
            "content": "**INVALID PLAYER ATTEMPTING TO AUTHENTICATE**: " + currentPlayerId + "\nINVALID LOGIN TYPE :x:" + "\nCUSTOM ID: N/A"
        };
        var webhookURL = getWebhookUrl();
        http.request(webhookURL, "post", JSON.stringify(contentBody), "application/json", {});
        server.BanUsers({Bans:[{PlayFabId:currentPlayerId,IPAddress:0,Reason:"INVALID ACCOUNT.",DurationInHours: 672}]});
        server.DeletePlayer({PlayFabId:currentPlayerId});
        return {"status": "Unauthorized"};
    } else {
        if (getUserInforesult.ServerCustomIdInfo.CustomId.startsWith("OCULUS")) {
            if (getUserInforesult.ServerCustomIdInfo.CustomId.substring(6).length == 16 || getUserInforesult.ServerCustomIdInfo.CustomId.substring(6).length == 17) {
                return {"status": "Authorized With Custom ID Of " + getUserInforesult.ServerCustomIdInfo.CustomId};
            } else {
                var contentBody = {
                    "content": "**INVALID PLAYER ATTEMPTING TO AUTHENTICATE**: " + currentPlayerId + "\nINVALID ORG SCOPED ID :x:" + "\nCUSTOM ID: " + getUserInforesult.ServerCustomIdInfo.CustomId
                };
                var webhookURL = getWebhookUrl();
                http.request(webhookURL, "post", JSON.stringify(contentBody), "application/json", {});
                server.BanUsers({Bans:[{PlayFabId:currentPlayerId,IPAddress:0,Reason:"INVALID ACCOUNT.",DurationInHours: 672}]});
                server.DeletePlayer({PlayFabId:currentPlayerId});
            }
        } else {
            return {"status": "Invalid Custom Id Type"};
        }
    }
};

// BAN MANAGEMENT

handlers.banPlayer = function(args) {
    var duration = args.duration;
    var reason = args.reason;
    server.BanUsers({Bans:[{DurationInHours:duration,PlayFabId:currentPlayerId,Reason:reason}]});
};

handlers.IncrementBanCount = function(args, context) {
    var dateKey = "BanCount_" + (new Date()).toISOString().slice(0,10);
    var dataResult = server.GetTitleInternalData({ Keys: [dateKey] });
    var currentCountStr = dataResult.Data[dateKey];
    var count = currentCountStr ? parseInt(currentCountStr) : 0;
    count++;
    server.SetTitleInternalData({ Key: dateKey, Value: count.toString() });
    return { newCount: count };
};

handlers.GetBanCount = function(args, context) {
    var dateKey = "BanCount" + (new Date()).toISOString().slice(0, 10);
    var dataResult = server.GetTitleInternalData({ Keys: [dateKey] });
    var count = dataResult.Data[dateKey] ? parseInt(dataResult.Data[dateKey]) : 0;
    return { count: count };
};

// ANTI CHEAT

handlers.AntiLemonLoader = function(args, context) {
    var result = server.GetUserAccountInfo({ PlayFabId: currentPlayerId });
    var id = context.playerProfile.PlayerId;
    var cidd = result.UserInfo.CustomIdInfo ? result.UserInfo.CustomIdInfo.CustomId : "N/A";
    var playerData = server.GetUserReadOnlyData({
        PlayFabId: currentPlayerId,
        Keys: ["androidDataPath", "PlayerName"]
    });
    var androidDataPath = playerData.Data["androidDataPath"] ? playerData.Data["androidDataPath"].Value : null;
    var lemonloader = ["/villanstudios.com/files/mods", "/villanstudios.com/files/Mods"];
    var banReason = "Mods";
    var deletePlayer = false;
    if (androidDataPath && lemonloader.includes(androidDataPath)) {
        banReason = "CHEATING. USING A PRIVATE APPLAB.";
        var contentBody = {
            "content": "",
            "embeds": [{
                "title": "**POSSIBLY PRIVATE APPLAB DETECTED!**",
                "color": 16711680,
                "fields": [{
                    "name": "PLAYER DETAILS",
                    "value": "\nCUSTOM ID: " + cidd + "\nPLAYER ID: " + id
                }]
            }],
            "attachments": []
        };
        http.request(getWebhookUrl(), "post", JSON.stringify(contentBody), "application/json", {});
        deletePlayer = true;
    }
    if (deletePlayer) {
        server.BanUsers({
            Bans: [{DurationInHours: 24, IPAddress: 0, PlayFabId: currentPlayerId, Reason: banReason}]
        });
    } else {
        log.debug("Valid package name: " + androidDataPath);
    }
};

handlers.AntiDll = function(args) {
    var cheatPaths = [
        "/storage/emulated/0/MelonLoader/villanstudios.com/files/mods",
        "/storage/emulated/0/Android/data/villanstudios.com/files/Mods",
        "/sdcard/Android/data/villanstudios.com/files/Mods"
    ];
    var playerData = server.GetUserAccountInfo({ PlayFabId: currentPlayerId }).UserInfo;
    var customId = playerData.CustomIdInfo && playerData.CustomIdInfo.CustomId;
    var readOnlyData = server.GetUserReadOnlyData({PlayFabId: currentPlayerId, Keys: ["androidDataPath"]});
    var androidDataPath = readOnlyData.Data["androidDataPath"] ? readOnlyData.Data["androidDataPath"].Value : null;
    var hasCheatFolder = androidDataPath && cheatPaths.indexOf(androidDataPath) !== -1;
    if (hasCheatFolder) {
        server.BanUsers({
            Bans: [{PlayFabId: currentPlayerId, DurationInHours: 1385, Reason: "CHEATING - Mod folder detected"}]
        });
        var contentBody = {
            "content": null,
            "embeds": [{
                "title": "Modding Detected",
                "description": "**Custom ID:** " + customId + "\n**Player ID:** " + currentPlayerId + "\n**Path:** " + androidDataPath,
                "color": 16711680,
                "author": {"name": "Anti-Cheat"}
            }],
            "attachments": []
        };
        http.request(getWebhookUrl(), "POST", JSON.stringify(contentBody), "application/json", {});
        return { message: "User banned for 1385 hours due to cheating." };
    } else {
        return { message: "Player is clean." };
    }
};

handlers.ValidatePlayerAction = function(args, context) {
    var playerId = context.playerId;
    if (args.cheatToolDetected) {
        var logMessage = "Player " + playerId + " reported using cheat tool: " + args.cheatToolDetected;
        server.WriteLog(logMessage);
        return { success: false, message: "Cheat tool detected. Action denied." };
    }
    if (args.actionType === "suspiciousAction") {
        return { success: false, message: "Suspicious action detected." };
    }
    return { success: true, message: "Action validated." };
};

// IAP & CURRENCY

// GrantItemToPlayer, GivePlayerCurrency and RemovePlayerCurrency were removed in revision 18 (see top).

handlers.CompleteIAPPurchase = function(args, context) {
    var url = 'https://graph.oculus.com/5409297455797322/consume_entitlement?nonce=' + args.UserProof + '&user_id=' + args.MetaId + '&sku=' + args.SKU + '&access_token=OC|6755960357813543|dceda3c54c45b49a8502d52b320b7e6e';
    var responseString = http.request(url, 'post', '', 'application/json', {});
    if (JSON.parse(responseString).success != undefined) {
        if (JSON.parse(responseString).success) {
            var recordTransactionRequest = {
                PlayFabId: currentPlayerId,
                TitleId: titleId,
                PurchaseId: args.SKU,
                PurchaseAmount: args.PurchaseAmount
            };
            server.UpdateUserInternalData(recordTransactionRequest);
            return true;
        }
    }
    return false;
};

// ValidateOculusPurchaseAndGrant was removed in revision 18: it granted whatever itemId the client
// named for any receipt, and nothing in the game calls it.

// PHOTON

handlers.GetPhotonAuth = function(args, context) {
    var authData = "446a5b3e-3d7c-4a99-9eae-21e7b90d8a91";
    return authData;
};

handlers.GrantCatalogItemToCurrentPlayer = function (args, context) {
    log.info("GrantCatalogItemToCurrentPlayer called.");
    if (!args) throw "Missing args";
    if (!args.ItemId || args.ItemId === "") throw "Missing ItemId";
    if (!args.CatalogVersion || args.CatalogVersion === "") throw "Missing CatalogVersion";
    var itemId = args.ItemId;
    var catalogVersion = args.CatalogVersion;
    // Only the free-claim items listed on the server; anything else used to be a free item of choice.
    if (!obIsFreeClaim(catalogVersion, itemId)) {
        log.error("[GrantCatalogItemToCurrentPlayer] refused, not a free-claim item: " + catalogVersion + "/" + itemId);
        return { success: false, alreadyOwned: false, granted: false, message: "Item is not claimable.", itemId: itemId, playFabId: currentPlayerId };
    }
    log.info("CurrentPlayerId: " + currentPlayerId);
    log.info("CatalogVersion: " + catalogVersion);
    log.info("ItemId: " + itemId);
    var inventoryResult = server.GetUserInventory({PlayFabId: currentPlayerId});
    if (inventoryResult && inventoryResult.Inventory) {
        for (var i = 0; i < inventoryResult.Inventory.length; i++) {
            if (inventoryResult.Inventory[i].ItemId == itemId) {
                log.info("Player already owns item: " + itemId);
                return {
                    success: true, alreadyOwned: true, granted: false,
                    message: "Player already owns this item.", itemId: itemId, playFabId: currentPlayerId
                };
            }
        }
    }
    var grantResult = server.GrantItemsToUser({
        PlayFabId: currentPlayerId,
        CatalogVersion: catalogVersion,
        ItemIds: [itemId],
        Annotation: "Claimed from Unity button"
    });
    log.info("GrantItemsToUser completed.");
    return {
        success: true, alreadyOwned: false, granted: true,
        message: "Item granted successfully.", itemId: itemId,
        playFabId: currentPlayerId, grantResult: grantResult
    };
};

handlers.GrantEventReward = function (args, context) {
    var eventCode = args && args.eventCode ? args.eventCode : null;
    if (!eventCode) throw "Missing eventCode";
    // The reward list comes from the server table, never from the client's args.
    var cosmeticItemIds = obEventRewards(eventCode);
    if (!cosmeticItemIds || cosmeticItemIds.length === 0) throw "No reward configured for event: " + eventCode;
    var dataResult = server.GetUserData({PlayFabId: currentPlayerId, Keys: [eventCode]});
    var progress = null;
    if (dataResult.Data && dataResult.Data[eventCode] && dataResult.Data[eventCode].Value) {
        progress = JSON.parse(dataResult.Data[eventCode].Value);
    } else {
        throw "No event progress found for key: " + eventCode;
    }
    if (!progress.collected || progress.collected.length !== 9) throw "Collected array is invalid";
    var allCollected = true;
    for (var i = 0; i < progress.collected.length; i++) {
        if (!progress.collected[i]) { allCollected = false; break; }
    }
    if (!allCollected) {
        return {success: false, message: "Not all collectibles are collected yet.", eventCode: eventCode};
    }
    if (progress.rewardClaimed === true) {
        return {success: true, message: "Reward already claimed.", eventCode: eventCode, rewardClaimed: true};
    }
    var inventory = server.GetUserInventory({PlayFabId: currentPlayerId});
    var ownedMap = {};
    if (inventory && inventory.Inventory) {
        for (var j = 0; j < inventory.Inventory.length; j++) ownedMap[inventory.Inventory[j].ItemId] = true;
    }
    var grantedItems = [];
    for (var k = 0; k < cosmeticItemIds.length; k++) {
        var itemId = cosmeticItemIds[k];
        if (!itemId) continue;
        if (!ownedMap[itemId]) {
            server.GrantItemsToUser({PlayFabId: currentPlayerId, ItemIds: [itemId], CatalogVersion: "Johnsons"});
            grantedItems.push(itemId);
        }
    }
    progress.rewardClaimed = true;
    server.UpdateUserData({PlayFabId: currentPlayerId, Data: {[eventCode]: JSON.stringify(progress)}, Permission: "Public"});
    return {success: true, eventCode: eventCode, rewardClaimed: true, grantedItems: grantedItems};
};

// ConsoleGiveCurrency and ConsoleGiveCosmetic were removed in revision 18. They checked IsMod in
// normal player data, which any client can write, so anyone could grant anything to anyone; the
// console script that called them (OoglyConsoleCloudScript) is not used by the game. If a mod console
// comes back, read IsMod with server.GetUserReadOnlyData, which only the server or an admin can set.

handlers.RepairIAPItems = function (args, context) {
    var playFabId = currentPlayerId;
    try {
        log.info("[RepairIAPItems] START PlayFabId=" + playFabId);
        log.info("[RepairIAPItems] Raw args=" + JSON.stringify(args));
        if (!args) return Fail("Missing args.");
        if (!args.sku) return Fail("Missing sku.");
        var sku = String(args.sku).trim();
        // The bundle's catalog and items come from the server table (OB_IAP_BUNDLES), never from the
        // client's args: the client could otherwise name any item.
        var bundle = obIapBundle(sku);
        if (!bundle) return Fail("Unknown sku.");
        var catalogVersion = bundle.catalog;
        var itemNames = CleanItemList(bundle.items);
        log.info("[RepairIAPItems] sku=[" + sku + "]");
        log.info("[RepairIAPItems] catalogVersion=[" + catalogVersion + "]");
        log.info("[RepairIAPItems] itemNames=[" + itemNames.join(",") + "]");
        if (sku.length === 0) return Fail("Sku was empty.");
        if (catalogVersion.length === 0) return Fail("CatalogVersion was empty.");
        if (itemNames.length === 0) return Fail("No valid item names were sent.");
        var keysToCheck = ["Owned IAP", "OwnedIAP", "OwenedIAP", "Owened IAP"];
        var normalData = server.GetUserData({PlayFabId: playFabId, Keys: keysToCheck});
        var readOnlyData = server.GetUserReadOnlyData({PlayFabId: playFabId, Keys: keysToCheck});
        var foundKey = "";
        var foundSource = "";
        var ownedIAPRaw = "";
        for (var k = 0; k < keysToCheck.length; k++) {
            var normalValue = GetDataValue(normalData, keysToCheck[k]);
            var readOnlyValue = GetDataValue(readOnlyData, keysToCheck[k]);
            log.info("[RepairIAPItems] NormalData key=[" + keysToCheck[k] + "] value=[" + normalValue + "]");
            log.info("[RepairIAPItems] ReadOnlyData key=[" + keysToCheck[k] + "] value=[" + readOnlyValue + "]");
            if (ownedIAPRaw.length === 0 && normalValue.length > 0) {
                ownedIAPRaw = normalValue; foundKey = keysToCheck[k]; foundSource = "PlayerData";
            }
            if (ownedIAPRaw.length === 0 && readOnlyValue.length > 0) {
                ownedIAPRaw = readOnlyValue; foundKey = keysToCheck[k]; foundSource = "ReadOnlyData";
            }
        }
        var ownedIAPs = ParseCsv(ownedIAPRaw);
        // OwnedIAP is written by the client, so it only decides whether Meta is asked at all (every
        // shop stand runs this at each login). Ownership itself is Meta's answer.
        var claimsIAP = ContainsCaseInsensitive(ownedIAPs, sku);
        var ownsIAP = claimsIAP && obOwnsSku(sku);
        log.info("[RepairIAPItems] foundSource=[" + foundSource + "]");
        log.info("[RepairIAPItems] foundKey=[" + foundKey + "]");
        log.info("[RepairIAPItems] ownedIAPRaw=[" + ownedIAPRaw + "]");
        log.info("[RepairIAPItems] parsedOwnedIAPs=[" + ownedIAPs.join(",") + "]");
        log.info("[RepairIAPItems] claimsIAP=[" + claimsIAP + "] ownsIAP=[" + ownsIAP + "]");
        if (!ownsIAP) {
            return {
                success: true, error: "",
                stoppedReason: claimsIAP ? "Meta does not confirm the player owns this sku." : "Player does not own sku in any checked IAP key.",
                ownsIAP: false, sku: sku, catalogVersion: catalogVersion,
                foundSource: foundSource, foundKey: foundKey, ownedIAPRaw: ownedIAPRaw,
                parsedOwnedIAPs: ownedIAPs, requiredItems: itemNames,
                ownedItems: [], missingItems: [], grantedItems: []
            };
        }
        var inventory = server.GetUserInventory({PlayFabId: playFabId});
        var ownedItems = [];
        if (inventory.Inventory) {
            for (var i = 0; i < inventory.Inventory.length; i++) {
                if (inventory.Inventory[i] && inventory.Inventory[i].ItemId) ownedItems.push(inventory.Inventory[i].ItemId);
            }
        }
        log.info("[RepairIAPItems] inventoryCount=[" + ownedItems.length + "]");
        log.info("[RepairIAPItems] ownedItems=[" + ownedItems.join(",") + "]");
        var missingItems = [];
        for (var r = 0; r < itemNames.length; r++) {
            if (!ContainsExact(ownedItems, itemNames[r])) missingItems.push(itemNames[r]);
        }
        log.info("[RepairIAPItems] missingCount=[" + missingItems.length + "]");
        log.info("[RepairIAPItems] missingItems=[" + missingItems.join(",") + "]");
        var grantedItems = [];
        var grantResultRaw = "";
        if (missingItems.length > 0) {
            log.info("[RepairIAPItems] Calling GrantItemsToUser. catalogVersion=[" + catalogVersion + "] items=[" + missingItems.join(",") + "]");
            var grantResult = server.GrantItemsToUser({
                PlayFabId: playFabId, CatalogVersion: catalogVersion,
                ItemIds: missingItems, Annotation: "IAP repair grant for " + sku
            });
            grantResultRaw = JSON.stringify(grantResult);
            log.info("[RepairIAPItems] grantResult=[" + grantResultRaw + "]");
            if (grantResult && grantResult.ItemGrantResults) {
                for (var g = 0; g < grantResult.ItemGrantResults.length; g++) {
                    if (grantResult.ItemGrantResults[g] && grantResult.ItemGrantResults[g].ItemId) grantedItems.push(grantResult.ItemGrantResults[g].ItemId);
                }
            }
        }
        log.info("[RepairIAPItems] grantedCount=[" + grantedItems.length + "]");
        log.info("[RepairIAPItems] grantedItems=[" + grantedItems.join(",") + "]");
        log.info("[RepairIAPItems] END SUCCESS");
        return {
            success: true, error: "", stoppedReason: "", ownsIAP: true,
            sku: sku, catalogVersion: catalogVersion, foundSource: foundSource, foundKey: foundKey,
            ownedIAPRaw: ownedIAPRaw, parsedOwnedIAPs: ownedIAPs, requiredItems: itemNames,
            ownedItems: ownedItems, missingItems: missingItems, grantedItems: grantedItems, grantResultRaw: grantResultRaw
        };
    } catch (e) {
        var message = e && e.message ? e.message : String(e);
        log.error("[RepairIAPItems] EXCEPTION [" + message + "]");
        return {
            success: false, error: message, exception: true, ownsIAP: false,
            requiredItems: [], ownedItems: [], missingItems: [], grantedItems: []
        };
    }
};

function Fail(error) {
    log.error("[RepairIAPItems] FAIL [" + error + "]");
    return {success: false, error: error, stoppedReason: error, ownsIAP: false,
        requiredItems: [], ownedItems: [], missingItems: [], grantedItems: []};
}

function GetDataValue(result, key) {
    if (!result || !result.Data || !result.Data[key] || result.Data[key].Value === null || result.Data[key].Value === undefined) return "";
    return String(result.Data[key].Value).trim();
}

function CleanItemList(rawList) {
    var result = [];
    for (var i = 0; i < rawList.length; i++) {
        if (rawList[i] === null || rawList[i] === undefined) continue;
        var cleaned = String(rawList[i]).trim();
        if (cleaned.length <= 0) continue;
        if (!ContainsExact(result, cleaned)) result.push(cleaned);
    }
    return result;
}

function ParseCsv(value) {
    var result = [];
    if (!value) return result;
    var split = String(value).split(",");
    for (var i = 0; i < split.length; i++) {
        var cleaned = String(split[i]).trim();
        if (cleaned.length > 0) result.push(cleaned);
    }
    return result;
}

function ContainsExact(list, value) {
    for (var i = 0; i < list.length; i++) {
        if (String(list[i]) === String(value)) return true;
    }
    return false;
}

function ContainsCaseInsensitive(list, value) {
    var wanted = String(value).trim().toLowerCase();
    for (var i = 0; i < list.length; i++) {
        if (String(list[i]).trim().toLowerCase() === wanted) return true;
    }
    return false;
}

Embed this paste

Install this app

  1. Tap the Share button
  2. Scroll down and tap "Add to Home Screen"
  3. Tap "Add" in the top right

Please confirm