mirror of
https://github.com/Detanup01/gbe_fork.git
synced 2024-12-24 09:24:15 +08:00
Merge branch 'issue_#5' of https://gitlab.com/Nemirtingas/goldberg_emulator into inventory_pr
This commit is contained in:
commit
f56503fcd0
12
README.md
12
README.md
@ -16,6 +16,18 @@ If your game has an original steam_api(64).dll or libsteam_api.so older than may
|
||||
|
||||
For more information see: [The Release Readme](Readme_release.txt)
|
||||
|
||||
## How to add items to your steam inventory
|
||||
|
||||
Create a folder named steam_settings right beside steam_api.dll if there isn't one already. In that folder, create a file named items.json which will contain every item you want to have in your game.
|
||||
|
||||
An example can be found in steam_settings.EXAMPLE that works with Killing Floor 2.
|
||||
|
||||
The items.json syntax is simple, you SHOULD validate your .json file before trying to run your game or you won't have any item in your inventory. Just look for "online json validator" on your web brower to valide your file.
|
||||
|
||||
You can use https://steamdb.info/ to list items and attributes they have and put them into your .json.
|
||||
|
||||
Keep in mind that some item are not valid to have in your inventory. For example, in PayDay2 all items below item_id 50000 will make your game crash.
|
||||
|
||||
## Download Binaries
|
||||
|
||||
You can download the latest git builds for Linux and Windows on [the Gitlab pages website](https://mr_goldberg.gitlab.io/goldberg_emulator/) and the stable releases in the [release section](https://gitlab.com/Mr_Goldberg/goldberg_emulator/releases) of this repo.
|
||||
|
60
dll/item_db_loader.cpp
Normal file
60
dll/item_db_loader.cpp
Normal file
@ -0,0 +1,60 @@
|
||||
/* Copyright (C) 2019 Nemirtingas (Maxime P)
|
||||
This file is part of the Goldberg Emulator
|
||||
|
||||
The Goldberg Emulator is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3 of the License, or (at your option) any later version.
|
||||
|
||||
The Goldberg Emulator is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with the Goldberg Emulator; if not, see
|
||||
<http://www.gnu.org/licenses/>. */
|
||||
#include "item_db_loader.h"
|
||||
|
||||
#include <fstream>
|
||||
#include "../json/json.hpp"
|
||||
|
||||
void read_items_db(std::string items_db, std::map<SteamItemDef_t, std::map<std::string, std::string>> *items, std::atomic_bool *is_loadedb)
|
||||
{
|
||||
std::ifstream items_file(items_db);
|
||||
// If there is a file and we opened it
|
||||
if( items_file )
|
||||
{
|
||||
items_file.seekg(0, std::ios::end);
|
||||
size_t size = items_file.tellg();
|
||||
std::string buffer(size, '\0');
|
||||
items_file.seekg(0);
|
||||
// Read it entirely, if the .json file gets too big,
|
||||
// I should look into this and split reads into smaller parts.
|
||||
items_file.read(&buffer[0], size);
|
||||
items_file.close();
|
||||
|
||||
try
|
||||
{
|
||||
std::map<SteamItemDef_t, std::map<std::string, std::string>> tmp;
|
||||
nlohmann::json json = nlohmann::json::parse(buffer);
|
||||
|
||||
for (auto& i : json.items())
|
||||
{
|
||||
SteamItemDef_t key = std::stoi((*i).key());
|
||||
nlohmann::json& value = (*i).value();
|
||||
for (auto& j : value.items())
|
||||
{
|
||||
tmp[key][(*j).key()] = (*j).value();
|
||||
}
|
||||
}
|
||||
|
||||
items->swap(tmp);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
PRINT_DEBUG("Error while parsing json: %s", e.what());
|
||||
}
|
||||
}
|
||||
*is_loadedb = true;
|
||||
}
|
24
dll/item_db_loader.h
Normal file
24
dll/item_db_loader.h
Normal file
@ -0,0 +1,24 @@
|
||||
/* Copyright (C) 2019 Nemirtingas (Maxime P)
|
||||
This file is part of the Goldberg Emulator
|
||||
|
||||
The Goldberg Emulator is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3 of the License, or (at your option) any later version.
|
||||
|
||||
The Goldberg Emulator is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with the Goldberg Emulator;*/
|
||||
#ifndef __ITEM_DB_LOADER_INCLUDED__
|
||||
#define __ITEM_DB_LOADER_INCLUDED__
|
||||
|
||||
#include "base.h" // For SteamItemDef_t
|
||||
|
||||
#include <atomic>
|
||||
void read_items_db(std::string items_db, std::map<SteamItemDef_t, std::map<std::string, std::string>> *items, std::atomic_bool *is_loaded);
|
||||
|
||||
#endif//__ITEM_DB_LOADER_INCLUDED__
|
5
dll/steam_inventory.cpp
Normal file
5
dll/steam_inventory.cpp
Normal file
@ -0,0 +1,5 @@
|
||||
#include "steam_inventory.h"
|
||||
|
||||
std::once_flag Steam_Inventory::items_loading;
|
||||
std::atomic_bool Steam_Inventory::items_loaded(false);
|
||||
std::map<SteamItemDef_t, std::map<std::string, std::string>> Steam_Inventory::items;
|
@ -15,7 +15,8 @@
|
||||
License along with the Goldberg Emulator; if not, see
|
||||
<http://www.gnu.org/licenses/>. */
|
||||
|
||||
#include "base.h"
|
||||
#include "item_db_loader.h"
|
||||
#include <thread>
|
||||
|
||||
struct Steam_Inventory_Requests {
|
||||
double timeout = 0.1;
|
||||
@ -34,11 +35,10 @@ struct Steam_Inventory_Requests {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class Steam_Inventory :
|
||||
public ISteamInventory001,
|
||||
public ISteamInventory002,
|
||||
public ISteamInventory
|
||||
public ISteamInventory001,
|
||||
public ISteamInventory002,
|
||||
public ISteamInventory
|
||||
{
|
||||
class Settings *settings;
|
||||
class SteamCallResults *callback_results;
|
||||
@ -46,7 +46,21 @@ public ISteamInventory
|
||||
|
||||
std::vector<struct Steam_Inventory_Requests> inventory_requests;
|
||||
|
||||
struct Steam_Inventory_Requests *new_inventory_result(const SteamItemInstanceID_t *pInstanceIDs=NULL, uint32 unCountInstanceIDs=0)
|
||||
static std::once_flag items_loading;
|
||||
static std::atomic_bool items_loaded;
|
||||
static std::map<SteamItemDef_t, std::map<std::string, std::string>> items;
|
||||
// Like typedefs
|
||||
using item_iterator = std::map<SteamItemDef_t, std::map<std::string, std::string>>::iterator;
|
||||
using attr_iterator = std::map<std::string, std::string>::iterator;
|
||||
|
||||
// Set this to false when we have cached everything,
|
||||
// reset to true if something changed in the item db.
|
||||
// Could use inotify on linux
|
||||
// Could use FindFirstChangeNotificationA + WaitForSingleObject + FindNextChangeNotification on Windows to monitor the db file
|
||||
// Or find a server somewhere to hold the data for us then cache on local settings.
|
||||
bool need_load_definitions = true;
|
||||
|
||||
struct Steam_Inventory_Requests* new_inventory_result(const SteamItemInstanceID_t* pInstanceIDs = NULL, uint32 unCountInstanceIDs = 0)
|
||||
{
|
||||
static SteamInventoryResult_t result;
|
||||
++result;
|
||||
@ -77,6 +91,14 @@ public:
|
||||
|
||||
Steam_Inventory(class Settings *settings, class SteamCallResults *callback_results, class SteamCallBacks *callbacks)
|
||||
{
|
||||
std::call_once(items_loading, [&]()
|
||||
{
|
||||
std::string items_db_file(Local_Storage::get_game_settings_path() + "items.json");
|
||||
PRINT_DEBUG("Items file path: %s\n", items_db_file.c_str());
|
||||
std::thread items_load_thread(read_items_db, items_db_file, &items, &items_loaded);
|
||||
items_load_thread.detach();
|
||||
});
|
||||
|
||||
this->settings = settings;
|
||||
this->callbacks = callbacks;
|
||||
this->callback_results = callback_results;
|
||||
@ -122,7 +144,25 @@ bool GetResultItems( SteamInventoryResult_t resultHandle,
|
||||
if (!request) return false;
|
||||
if (!request->result_done()) return false;
|
||||
|
||||
if (punOutItemsArraySize) *punOutItemsArraySize = 0;
|
||||
if (pOutItemsArray != nullptr)
|
||||
{
|
||||
uint32 max_items = *punOutItemsArraySize;
|
||||
// We end if we reached the end of items or the end of buffer
|
||||
for( auto i = items.begin(); i != items.end() && max_items; ++i, --max_items )
|
||||
{
|
||||
pOutItemsArray->m_iDefinition = i->first;
|
||||
pOutItemsArray->m_itemId = i->first;
|
||||
pOutItemsArray->m_unQuantity = 1;
|
||||
pOutItemsArray->m_unFlags = k_ESteamItemNoTrade;
|
||||
++pOutItemsArray;
|
||||
}
|
||||
*punOutItemsArraySize = std::min(*punOutItemsArraySize, static_cast<uint32>(items.size()));
|
||||
}
|
||||
else if (punOutItemsArraySize != nullptr)
|
||||
{
|
||||
*punOutItemsArraySize = items.size();
|
||||
}
|
||||
|
||||
PRINT_DEBUG("GetResultItems good\n");
|
||||
return true;
|
||||
}
|
||||
@ -206,8 +246,21 @@ bool GetAllItems( SteamInventoryResult_t *pResultHandle )
|
||||
{
|
||||
PRINT_DEBUG("GetAllItems\n");
|
||||
std::lock_guard<std::recursive_mutex> lock(global_mutex);
|
||||
if (pResultHandle) {
|
||||
struct Steam_Inventory_Requests *request = new_inventory_result();
|
||||
struct Steam_Inventory_Requests* request = new_inventory_result();
|
||||
|
||||
// Can't call LoadItemDefinitions because it sends a SteamInventoryResultReady_t.
|
||||
if( need_load_definitions )
|
||||
{
|
||||
if (items_loaded)
|
||||
{
|
||||
need_load_definitions = false;
|
||||
SteamInventoryDefinitionUpdate_t data = {};
|
||||
callbacks->addCBResult(data.k_iCallback, &data, sizeof(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!need_load_definitions)
|
||||
{
|
||||
{
|
||||
// SteamInventoryFullUpdate_t callbacks are triggered when GetAllItems
|
||||
// successfully returns a result which is newer / fresher than the last
|
||||
@ -224,11 +277,21 @@ bool GetAllItems( SteamInventoryResult_t *pResultHandle )
|
||||
callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), request->timeout);
|
||||
}
|
||||
|
||||
*pResultHandle = request->inventory_result;
|
||||
return true;
|
||||
if (pResultHandle != nullptr)
|
||||
*pResultHandle = request->inventory_result;
|
||||
}
|
||||
else
|
||||
{
|
||||
struct SteamInventoryResultReady_t data;
|
||||
data.m_handle = request->inventory_result;
|
||||
data.m_result = k_EResultPending;
|
||||
callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), request->timeout);
|
||||
|
||||
if (pResultHandle != nullptr)
|
||||
*pResultHandle = request->inventory_result;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -488,6 +551,32 @@ STEAM_METHOD_DESC(LoadItemDefinitions triggers the automatic load and refresh of
|
||||
bool LoadItemDefinitions()
|
||||
{
|
||||
PRINT_DEBUG("LoadItemDefinitions\n");
|
||||
|
||||
if (need_load_definitions)
|
||||
{
|
||||
if (!items_loaded)
|
||||
{
|
||||
SteamInventoryResultReady_t data;
|
||||
data.m_result = k_EResultPending;
|
||||
data.m_handle = new_inventory_result()->inventory_result;
|
||||
callbacks->addCBResult(data.k_iCallback, &data, sizeof(data));
|
||||
}
|
||||
else
|
||||
{
|
||||
need_load_definitions = false;
|
||||
{
|
||||
SteamInventoryDefinitionUpdate_t data = {};
|
||||
callbacks->addCBResult(data.k_iCallback, &data, sizeof(data));
|
||||
}
|
||||
{
|
||||
SteamInventoryResultReady_t data = {};
|
||||
data.m_result = k_EResultOK;
|
||||
data.m_handle = new_inventory_result()->inventory_result;
|
||||
callbacks->addCBResult(data.k_iCallback, &data, sizeof(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -502,18 +591,24 @@ bool GetItemDefinitionIDs(
|
||||
STEAM_DESC(Size of array is passed in and actual size used is returned in this param) uint32 *punItemDefIDsArraySize )
|
||||
{
|
||||
PRINT_DEBUG("GetItemDefinitionIDs\n");
|
||||
if (!punItemDefIDsArraySize) {
|
||||
if (!punItemDefIDsArraySize)
|
||||
return false;
|
||||
}
|
||||
|
||||
PRINT_DEBUG("array_size %u\n", *punItemDefIDsArraySize);
|
||||
/*
|
||||
if (pItemDefIDs) {
|
||||
*pItemDefIDs = 0;
|
||||
|
||||
if (pItemDefIDs == nullptr)
|
||||
{
|
||||
*punItemDefIDsArraySize = items.size();
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
//*punItemDefIDsArraySize = 0;
|
||||
return false;
|
||||
|
||||
if (*punItemDefIDsArraySize < items.size())
|
||||
return false;
|
||||
|
||||
for (auto& i : items)
|
||||
*pItemDefIDs++ = i.first;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -530,6 +625,75 @@ bool GetItemDefinitionProperty( SteamItemDef_t iDefinition, const char *pchPrope
|
||||
STEAM_OUT_STRING_COUNT(punValueBufferSizeOut) char *pchValueBuffer, uint32 *punValueBufferSizeOut )
|
||||
{
|
||||
PRINT_DEBUG("GetItemDefinitionProperty\n");
|
||||
|
||||
item_iterator item;
|
||||
if ((item = items.find(iDefinition)) != items.end())
|
||||
{
|
||||
attr_iterator attr;
|
||||
if (pchPropertyName != nullptr)
|
||||
{
|
||||
// Should I check for punValueBufferSizeOut == nullptr ?
|
||||
// Try to get the property
|
||||
if ((attr = item->second.find(pchPropertyName)) != items[iDefinition].end())
|
||||
{
|
||||
std::string const& val = attr->second;
|
||||
if (pchValueBuffer != nullptr)
|
||||
{
|
||||
// copy what we can
|
||||
strncpy(pchValueBuffer, val.c_str(), *punValueBufferSizeOut);
|
||||
}
|
||||
|
||||
// Set punValueBufferSizeOut to the property size
|
||||
*punValueBufferSizeOut = std::min(static_cast<uint32>(val.length() + 1), *punValueBufferSizeOut);
|
||||
|
||||
if (pchValueBuffer != nullptr)
|
||||
{
|
||||
// Make sure we have a null terminator
|
||||
pchValueBuffer[*punValueBufferSizeOut-1] = '\0';
|
||||
}
|
||||
}
|
||||
// Property not found
|
||||
else
|
||||
{
|
||||
*punValueBufferSizeOut = 0;
|
||||
PRINT_DEBUG("Attr %s not found for item %d\n", pchPropertyName, iDefinition);
|
||||
}
|
||||
}
|
||||
else // Pass a NULL pointer for pchPropertyName to get a comma - separated list of available property names.
|
||||
{
|
||||
// If pchValueBuffer is NULL, *punValueBufferSize will contain the suggested buffer size
|
||||
if (pchValueBuffer == nullptr)
|
||||
{
|
||||
// Should I check for punValueBufferSizeOut == nullptr ?
|
||||
*punValueBufferSizeOut = 0;
|
||||
for (auto& i : item->second)
|
||||
*punValueBufferSizeOut += i.first.length() + 1; // Size of key + comma, and the last is not a comma but null char
|
||||
}
|
||||
else
|
||||
{
|
||||
// strncat always add the null terminator, so remove 1 to the string length
|
||||
uint32_t len = *punValueBufferSizeOut-1;
|
||||
*punValueBufferSizeOut = 0;
|
||||
memset(pchValueBuffer, 0, len);
|
||||
for( auto i = item->second.begin(); i != item->second.end() && len > 0; ++i )
|
||||
{
|
||||
strncat(pchValueBuffer, i->first.c_str(), len);
|
||||
// Count how many chars we copied
|
||||
// Either the string length or the buffer size if its too small
|
||||
uint32 x = std::min(len, static_cast<uint32>(i->first.length()));
|
||||
*punValueBufferSizeOut += x;
|
||||
len -= x;
|
||||
|
||||
if (len && std::distance(i, item->second.end()) != 1) // If this is not the last item, add a comma
|
||||
strncat(pchValueBuffer, ",", len--);
|
||||
|
||||
// Always add 1, its a comma or the null terminator
|
||||
++*punValueBufferSizeOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -653,4 +817,4 @@ bool SubmitUpdateProperties( SteamInventoryUpdateHandle_t handle, SteamInventory
|
||||
PRINT_DEBUG("SubmitUpdateProperties\n");
|
||||
}
|
||||
|
||||
};
|
||||
};
|
95
files_example/steam_settings.EXAMPLE/items.EXAMPLE.json
Normal file
95
files_example/steam_settings.EXAMPLE/items.EXAMPLE.json
Normal file
@ -0,0 +1,95 @@
|
||||
{
|
||||
"2001": {
|
||||
"Timestamp": "2018-01-09T19:30:03Z",
|
||||
"modified": "20180109T193003Z",
|
||||
"date_created": "20180109T193003Z",
|
||||
"type": "bundle",
|
||||
"display_type": "Bundle",
|
||||
"name": "Foster Classic Bundle",
|
||||
"bundle": "2011x1;2012x1;3358x1",
|
||||
"description": "Comes with Foster's Classic Suit Uniform (includes 9 skin styles), Classic Tie Accessory (includes 8 skin styles) and Bow Tie Accessory. Not tradeable or marketable.",
|
||||
"background_color": "000000",
|
||||
"icon_url": "http://art.tripwirecdn.com/TestItemIcons/Bundle_ClassicFoster_96.png",
|
||||
"icon_url_large": "http://art.tripwirecdn.com/TestItemIcons/Bundle_ClassicFoster_360.png",
|
||||
"name_color": "7a0000",
|
||||
"tradable": "false",
|
||||
"marketable": "false",
|
||||
"commodity": "false",
|
||||
"drop_interval": "0",
|
||||
"drop_max_per_window": "0",
|
||||
"workshopid": "0",
|
||||
"tw_unique_to_own": "true",
|
||||
"item_quality": "0",
|
||||
"tw_price": "$4.99",
|
||||
"tw_type": "skc",
|
||||
"tw_client_visible": "1",
|
||||
"tw_icon_small": "CHR_MrFoster_Item_TEX.ClassicSuit.UniformBundle_FostersSuit",
|
||||
"tw_icon_large": "CHR_MrFoster_Item_TEX.ClassicSuit.UniformBundle_FostersSuit",
|
||||
"tw_description": "<FosterClassicBundleDescription:Bundles>",
|
||||
"tw_client_name": "<FosterClassicBundleName:Bundles>",
|
||||
"tw_client_type": "<BundleType:Bundles>",
|
||||
"tw_rarity": "crate"
|
||||
},
|
||||
"2002": {
|
||||
"Timestamp": "2018-01-09T19:30:03Z",
|
||||
"modified": "20180109T193003Z",
|
||||
"date_created": "20180109T193003Z",
|
||||
"type": "bundle",
|
||||
"display_type": "Bundle",
|
||||
"name": "Briar's Bobby Bundle",
|
||||
"bundle": "2021x1;2022x1",
|
||||
"description": "Comes with Briar's London Uniform (includes 5 skin styles), and Custodian Helmet Cosmetic Accessory (includes 3 skin styles) Not tradeable or marketable.",
|
||||
"background_color": "000000",
|
||||
"icon_url": "http://art.tripwirecdn.com/TestItemIcons/Bundle_BriarBobby_96.png",
|
||||
"icon_url_large": "http://art.tripwirecdn.com/TestItemIcons/Bundle_BriarBobby_360.png",
|
||||
"name_color": "7a0000",
|
||||
"tradable": "false",
|
||||
"marketable": "false",
|
||||
"commodity": "false",
|
||||
"drop_interval": "0",
|
||||
"drop_max_per_window": "0",
|
||||
"workshopid": "0",
|
||||
"tw_unique_to_own": "true",
|
||||
"item_quality": "0",
|
||||
"tw_price": "$4.99",
|
||||
"tw_type": "skc",
|
||||
"tw_client_visible": "1",
|
||||
"tw_icon_small": "CHR_Briar_Item_TEX.BobbyUniform.UniformBundle_BriarBobby",
|
||||
"tw_icon_large": "CHR_Briar_Item_TEX.BobbyUniform.UniformBundle_BriarBobby",
|
||||
"tw_description": "<BriarsBobbyBundleDescription:Bundles>",
|
||||
"tw_client_name": "<BriarsBobbyBundleName:Bundles>",
|
||||
"tw_client_type": "<BundleType:Bundles>",
|
||||
"tw_rarity": "crate"
|
||||
},
|
||||
"2003": {
|
||||
"Timestamp": "2018-01-09T19:30:03Z",
|
||||
"modified": "20180109T193003Z",
|
||||
"date_created": "20180109T193003Z",
|
||||
"type": "bundle",
|
||||
"display_type": "Bundle",
|
||||
"name": "Tanaka's Biker Bundle",
|
||||
"bundle": "2031x1;2032x1",
|
||||
"description": "Comes with Tanaka's Motorcycle Uniform (includes 7 skin styles) and Helmet Cosmetic Accessory (includes 7 skin styles) Not tradeable or marketable.",
|
||||
"background_color": "000000",
|
||||
"icon_url": "http://art.tripwirecdn.com/TestItemIcons/Bundle_BikerTanaka_96.png",
|
||||
"icon_url_large": "http://art.tripwirecdn.com/TestItemIcons/Bundle_BikerTanaka_360.png",
|
||||
"name_color": "7a0000",
|
||||
"tradable": "false",
|
||||
"marketable": "false",
|
||||
"commodity": "false",
|
||||
"drop_interval": "0",
|
||||
"drop_max_per_window": "0",
|
||||
"workshopid": "0",
|
||||
"tw_unique_to_own": "true",
|
||||
"item_quality": "0",
|
||||
"tw_price": "$4.99",
|
||||
"tw_type": "skc",
|
||||
"tw_client_visible": "1",
|
||||
"tw_icon_small": "CHR_Tanaka_01_Item_TEX.BikerUniform.UniformBundle_TanakaBiker",
|
||||
"tw_icon_large": "CHR_Tanaka_01_Item_TEX.BikerUniform.UniformBundle_TanakaBiker",
|
||||
"tw_description": "<TanakasBikerBundleDescription:Bundles>",
|
||||
"tw_client_name": "<TanakasBikerBundleName:Bundles>",
|
||||
"tw_client_type": "<BundleType:Bundles>",
|
||||
"tw_rarity": "crate"
|
||||
}
|
||||
}
|
20919
json/json.hpp
Normal file
20919
json/json.hpp
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user