mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2024-11-22 13:30:06 +00:00
functional crates (no more plungers) (#133)
* FM, Taros and Boosts awards from killing mobs should be pretty accurate now. A temporary formula for adjusting player/mob level gap is implemented, but it will probably need to be adjusted in the future * Mobs now drop correct crates * Crates can be opened and give you correct items This includes regular mob crates, world boss crates, mission crates, IZ race crates, E.G.G.E.R.s, golden Eggs, and Event Crates. Keep in mind that neither IZ races or golden Eggs are implemented, but if you spawn such a crate it can be opened. * All data is read from a json file, for which I'm going to release a tool soon so it's easily adjustable * There is a new setting for enabling events, which enables dropping extra event crates These are Knishmas, Halloween and Easter
This commit is contained in:
parent
ab5857e7e2
commit
dd54668697
@ -11,6 +11,13 @@
|
|||||||
std::map<std::pair<int32_t, int32_t>, Item> ItemManager::ItemData;
|
std::map<std::pair<int32_t, int32_t>, Item> ItemManager::ItemData;
|
||||||
std::map<int32_t, std::vector<VendorListing>> ItemManager::VendorTables;
|
std::map<int32_t, std::vector<VendorListing>> ItemManager::VendorTables;
|
||||||
std::map<int32_t, CrocPotEntry> ItemManager::CrocPotTable;
|
std::map<int32_t, CrocPotEntry> ItemManager::CrocPotTable;
|
||||||
|
std::map<int32_t, std::vector<int>> ItemManager::RarityRatios;
|
||||||
|
std::map<int32_t, Crate> ItemManager::Crates;
|
||||||
|
/// pair Itemset, Rarity -> vector of pointers (map iterators) to records in ItemData
|
||||||
|
std::map<std::pair<int32_t, int32_t>, std::vector<std::map<std::pair<int32_t, int32_t>, Item>::iterator>> ItemManager::CrateItems;
|
||||||
|
|
||||||
|
// buffer for error messages
|
||||||
|
char buffer[255];
|
||||||
|
|
||||||
void ItemManager::init() {
|
void ItemManager::init() {
|
||||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler);
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler);
|
||||||
@ -801,12 +808,12 @@ void ItemManager::chestOpenHandler(CNSocket *sock, CNPacketData *data) {
|
|||||||
if (data->size != sizeof(sP_CL2FE_REQ_ITEM_CHEST_OPEN))
|
if (data->size != sizeof(sP_CL2FE_REQ_ITEM_CHEST_OPEN))
|
||||||
return; // ignore the malformed packet
|
return; // ignore the malformed packet
|
||||||
|
|
||||||
sP_CL2FE_REQ_ITEM_CHEST_OPEN *pkt = (sP_CL2FE_REQ_ITEM_CHEST_OPEN *)data->buf;
|
sP_CL2FE_REQ_ITEM_CHEST_OPEN *chest = (sP_CL2FE_REQ_ITEM_CHEST_OPEN *)data->buf;
|
||||||
Player *plr = PlayerManager::getPlayer(sock);
|
Player *player = PlayerManager::getPlayer(sock);
|
||||||
|
|
||||||
if (plr == nullptr)
|
if (player == nullptr)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// item giving packet
|
// item giving packet
|
||||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||||
@ -820,21 +827,23 @@ void ItemManager::chestOpenHandler(CNSocket *sock, CNPacketData *data) {
|
|||||||
memset(respbuf, 0, resplen);
|
memset(respbuf, 0, resplen);
|
||||||
|
|
||||||
// maintain stats
|
// maintain stats
|
||||||
reward->m_iCandy = plr->money;
|
reward->m_iCandy = player->money;
|
||||||
reward->m_iFusionMatter = plr->fusionmatter;
|
reward->m_iFusionMatter = player->fusionmatter;
|
||||||
reward->iFatigue = 100; // prevents warning message
|
reward->iFatigue = 100; // prevents warning message
|
||||||
reward->iFatigue_Level = 1;
|
reward->iFatigue_Level = 1;
|
||||||
reward->iItemCnt = 1; // remember to update resplen if you change this
|
reward->iItemCnt = 1; // remember to update resplen if you change this
|
||||||
|
|
||||||
// item reward
|
// item reward
|
||||||
item->sItem.iType = 0;
|
if (chest->ChestItem.iType != 9)
|
||||||
item->sItem.iID = 96;
|
std::cout << "[WARN] Player tried to open a crate with incorrect iType ?!" << std::endl;
|
||||||
item->sItem.iOpt = 1;
|
else
|
||||||
item->iSlotNum = pkt->iSlotNum;
|
item->sItem = openCrate(chest->ChestItem.iID, player->PCStyle.iGender);
|
||||||
item->eIL = pkt->eIL;
|
|
||||||
|
item->iSlotNum = chest->iSlotNum;
|
||||||
|
item->eIL = chest->eIL;
|
||||||
|
|
||||||
// update player
|
// update player
|
||||||
plr->Inven[pkt->iSlotNum] = item->sItem;
|
player->Inven[chest->iSlotNum] = item->sItem;
|
||||||
|
|
||||||
// transmit item
|
// transmit item
|
||||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||||
@ -842,12 +851,119 @@ void ItemManager::chestOpenHandler(CNSocket *sock, CNPacketData *data) {
|
|||||||
// chest opening acknowledgement packet
|
// chest opening acknowledgement packet
|
||||||
INITSTRUCT(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, resp);
|
INITSTRUCT(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, resp);
|
||||||
|
|
||||||
resp.iSlotNum = pkt->iSlotNum;
|
resp.iSlotNum = chest->iSlotNum;
|
||||||
|
|
||||||
std::cout << "opening chest..." << std::endl;
|
std::cout << "opening chest..." << std::endl;
|
||||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, sizeof(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC));
|
sock->sendPacket((void*)&resp, P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, sizeof(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sItemBase ItemManager::openCrate(int crateId, int playerGender) {
|
||||||
|
sItemBase reward = {};
|
||||||
|
try {
|
||||||
|
Crate crate = getCrate(crateId);
|
||||||
|
int itemSetId = getItemSetId(crate, crateId);
|
||||||
|
int rarity = getRarity(crate, itemSetId);
|
||||||
|
reward = getCrateItem(itemSetId, rarity, playerGender);
|
||||||
|
}
|
||||||
|
catch (const std::exception& err) {
|
||||||
|
std::cerr << "[WARN] An error has occured while trying to open a crate. Error description: \n" << err.what() << std::endl;
|
||||||
|
// if we failed to open a crate, at least give the player a gumball (suggested by Jade)
|
||||||
|
reward.iType = 7;
|
||||||
|
reward.iID = 119 + (rand() % 3);
|
||||||
|
reward.iOpt = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reward;
|
||||||
|
}
|
||||||
|
|
||||||
|
Crate ItemManager::getCrate(int crateId) {
|
||||||
|
if (Crates.find(crateId) == Crates.end())
|
||||||
|
throwError(sprintf(buffer, "Crate %d was not found!", crateId));
|
||||||
|
return Crates[crateId];
|
||||||
|
}
|
||||||
|
|
||||||
|
int ItemManager::getItemSetId(Crate crate, int crateId) {
|
||||||
|
int itemSetsCount = crate.itemSets.size();
|
||||||
|
if (itemSetsCount == 0)
|
||||||
|
throwError(sprintf(buffer, "Crate %d has no item sets assigned?!", crateId));
|
||||||
|
|
||||||
|
// if crate points to multiple itemSets, choose a random one
|
||||||
|
int itemSetIndex = rand() % itemSetsCount;
|
||||||
|
return crate.itemSets[itemSetIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
int ItemManager::getRarity(Crate crate , int itemSetId) {
|
||||||
|
// find rarity ratio
|
||||||
|
if (RarityRatios.find(crate.rarityRatioId) == RarityRatios.end())
|
||||||
|
throwError(sprintf(buffer, "Rarity Ratio %d not found!", crate.rarityRatioId));
|
||||||
|
|
||||||
|
std::vector<int> rarityRatio = RarityRatios[crate.rarityRatioId];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* First we have to check if specified item set contains items with all specified rarities,
|
||||||
|
* and if not eliminate them from the draw
|
||||||
|
* it is simpler to do here than to fix individually in the file
|
||||||
|
*/
|
||||||
|
|
||||||
|
// remember that rarities start from 1 !
|
||||||
|
for (int i = 0; i < rarityRatio.size(); i++){
|
||||||
|
if (CrateItems.find(std::make_pair(itemSetId, i+1)) == CrateItems.end())
|
||||||
|
rarityRatio[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = 0;
|
||||||
|
for (int value : rarityRatio)
|
||||||
|
total += value;
|
||||||
|
|
||||||
|
// if we didn't find any items, throw exception
|
||||||
|
if (total == 0)
|
||||||
|
throwError(sprintf(buffer, "Item Set %d has no items assigned?!", itemSetId));
|
||||||
|
|
||||||
|
// now return a random rarity number
|
||||||
|
int randomNum = rand() % total;
|
||||||
|
int rarity = 0;
|
||||||
|
int sum = 0;
|
||||||
|
do {
|
||||||
|
sum += rarityRatio[rarity];
|
||||||
|
rarity++;
|
||||||
|
} while (sum <= randomNum);
|
||||||
|
|
||||||
|
return rarity;
|
||||||
|
}
|
||||||
|
|
||||||
|
sItemBase ItemManager::getCrateItem(int itemSetId, int rarity, int playerGender) {
|
||||||
|
std::pair key = std::make_pair(itemSetId, rarity);
|
||||||
|
|
||||||
|
if (CrateItems.find(key) == CrateItems.end())
|
||||||
|
throwError(sprintf(buffer, "Item Set ID %d Rarity %d items have not been found", itemSetId, rarity));
|
||||||
|
|
||||||
|
// only take into account items that have correct gender
|
||||||
|
std::vector<std::map<std::pair<int32_t, int32_t>, Item>::iterator> items;
|
||||||
|
for (auto crateitem : CrateItems[key])
|
||||||
|
{
|
||||||
|
int gender = crateitem->second.gender;
|
||||||
|
// if gender is incorrect, exclude item
|
||||||
|
if (gender != 0 && gender != playerGender)
|
||||||
|
continue;
|
||||||
|
items.push_back(crateitem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.size() == 0)
|
||||||
|
throwError(sprintf(buffer, "Gender inequality! Set ID %d Rarity %d contains only %s items?!", itemSetId, rarity, playerGender==2 ? "boys" : "girls"));
|
||||||
|
auto item = items[rand() % items.size()];
|
||||||
|
sItemBase result = {};
|
||||||
|
result.iID = item->first.first;
|
||||||
|
result.iType = item->first.second;
|
||||||
|
result.iOpt = 1;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// argument is here only so we can call sprintf in brackets
|
||||||
|
void ItemManager::throwError(int ignore) {
|
||||||
|
throw buffer;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: use this in cleaned up ItemManager
|
// TODO: use this in cleaned up ItemManager
|
||||||
int ItemManager::findFreeSlot(Player *plr) {
|
int ItemManager::findFreeSlot(Player *plr) {
|
||||||
int i;
|
int i;
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
struct Item {
|
struct Item {
|
||||||
bool tradeable, sellable;
|
bool tradeable, sellable;
|
||||||
int buyPrice, sellPrice, stackSize, level, rarity, pointDamage, groupDamage, defense; // TODO: implement more as needed
|
int buyPrice, sellPrice, stackSize, level, rarity, pointDamage, groupDamage, defense, gender; // TODO: implement more as needed
|
||||||
};
|
};
|
||||||
struct VendorListing {
|
struct VendorListing {
|
||||||
int sort, type, iID;
|
int sort, type, iID;
|
||||||
@ -14,6 +14,10 @@ struct CrocPotEntry {
|
|||||||
int multStats, multLooks;
|
int multStats, multLooks;
|
||||||
float base, rd0, rd1, rd2, rd3;
|
float base, rd0, rd1, rd2, rd3;
|
||||||
};
|
};
|
||||||
|
struct Crate {
|
||||||
|
int rarityRatioId;
|
||||||
|
std::vector<int> itemSets;
|
||||||
|
};
|
||||||
|
|
||||||
namespace ItemManager {
|
namespace ItemManager {
|
||||||
enum class SlotType {
|
enum class SlotType {
|
||||||
@ -25,6 +29,11 @@ namespace ItemManager {
|
|||||||
extern std::map<std::pair<int32_t, int32_t>, Item> ItemData; // <id, type> -> data
|
extern std::map<std::pair<int32_t, int32_t>, Item> ItemData; // <id, type> -> data
|
||||||
extern std::map<int32_t, std::vector<VendorListing>> VendorTables;
|
extern std::map<int32_t, std::vector<VendorListing>> VendorTables;
|
||||||
extern std::map<int32_t, CrocPotEntry> CrocPotTable; // level gap -> entry
|
extern std::map<int32_t, CrocPotEntry> CrocPotTable; // level gap -> entry
|
||||||
|
extern std::map<int32_t, std::vector<int>> RarityRatios;
|
||||||
|
extern std::map<int32_t, Crate> Crates;
|
||||||
|
// pair <Itemset, Rarity> -> vector of pointers (map iterators) to records in ItemData (it looks a lot scarier than it is)
|
||||||
|
extern std::map<std::pair<int32_t, int32_t>,
|
||||||
|
std::vector<std::map<std::pair<int32_t, int32_t>, Item>::iterator>> CrateItems;
|
||||||
|
|
||||||
void init();
|
void init();
|
||||||
|
|
||||||
@ -46,6 +55,14 @@ namespace ItemManager {
|
|||||||
void itemTradeChatHandler(CNSocket* sock, CNPacketData* data);
|
void itemTradeChatHandler(CNSocket* sock, CNPacketData* data);
|
||||||
void chestOpenHandler(CNSocket* sock, CNPacketData* data);
|
void chestOpenHandler(CNSocket* sock, CNPacketData* data);
|
||||||
|
|
||||||
|
// crate opening logic with all helper functions
|
||||||
|
sItemBase openCrate(int crateId, int playerGender);
|
||||||
|
Crate getCrate(int crateId);
|
||||||
|
int getItemSetId(Crate crate, int crateId);
|
||||||
|
int getRarity(Crate crate, int itemSetId);
|
||||||
|
sItemBase getCrateItem(int itemSetId, int rarity, int playerGender);
|
||||||
|
void throwError(int ignore);
|
||||||
|
|
||||||
int findFreeSlot(Player *plr);
|
int findFreeSlot(Player *plr);
|
||||||
Item* getItemData(int32_t id, int32_t type);
|
Item* getItemData(int32_t id, int32_t type);
|
||||||
void checkItemExpire(CNSocket* sock, Player* player);
|
void checkItemExpire(CNSocket* sock, Player* player);
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
|
||||||
std::map<int32_t, Mob*> MobManager::Mobs;
|
std::map<int32_t, Mob*> MobManager::Mobs;
|
||||||
|
std::map<int32_t, MobDropChance> MobManager::MobDropChances;
|
||||||
|
std::map<int32_t, MobDrop> MobManager::MobDrops;
|
||||||
std::queue<int32_t> MobManager::RemovalQueue;
|
std::queue<int32_t> MobManager::RemovalQueue;
|
||||||
|
|
||||||
bool MobManager::simulateMobs;
|
bool MobManager::simulateMobs;
|
||||||
@ -143,7 +145,7 @@ void MobManager::npcAttackPc(Mob *mob, time_t currTime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MobManager::giveReward(CNSocket *sock) {
|
void MobManager::giveReward(CNSocket *sock, Mob* mob) {
|
||||||
Player *plr = PlayerManager::getPlayer(sock);
|
Player *plr = PlayerManager::getPlayer(sock);
|
||||||
|
|
||||||
if (plr == nullptr)
|
if (plr == nullptr)
|
||||||
@ -160,9 +162,31 @@ void MobManager::giveReward(CNSocket *sock) {
|
|||||||
// don't forget to zero the buffer!
|
// don't forget to zero the buffer!
|
||||||
memset(respbuf, 0, resplen);
|
memset(respbuf, 0, resplen);
|
||||||
|
|
||||||
// NOTE: these will need to be scaled according to the player/mob level difference
|
// sanity check
|
||||||
plr->money += (int)MissionManager::AvatarGrowth[plr->level]["m_iMobFM"]; // this one's innacurate, but close enough for now
|
if (MobDrops.find(mob->dropType) == MobDrops.end()) {
|
||||||
MissionManager::updateFusionMatter(sock, MissionManager::AvatarGrowth[plr->level]["m_iMobFM"]);
|
std::cout << "[WARN] Drop Type " << mob->dropType << " was not found" << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// find correct mob drop
|
||||||
|
MobDrop drop = MobDrops[mob->dropType];
|
||||||
|
|
||||||
|
plr->money += drop.taros;
|
||||||
|
// formula for scaling FM with player/mob level difference
|
||||||
|
// TODO: adjust this better
|
||||||
|
int levelDifference = plr->level - mob->level;
|
||||||
|
int fm = drop.fm;
|
||||||
|
if (levelDifference > 0)
|
||||||
|
fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0;
|
||||||
|
|
||||||
|
MissionManager::updateFusionMatter(sock, fm);
|
||||||
|
|
||||||
|
// give boosts 1 in 3 times
|
||||||
|
if (drop.boosts > 0) {
|
||||||
|
if (rand() % 3 == 0)
|
||||||
|
plr->batteryN += drop.boosts;
|
||||||
|
if (rand() % 3 == 0)
|
||||||
|
plr->batteryW += drop.boosts;
|
||||||
|
}
|
||||||
|
|
||||||
// simple rewards
|
// simple rewards
|
||||||
reward->m_iCandy = plr->money;
|
reward->m_iCandy = plr->money;
|
||||||
@ -173,20 +197,29 @@ void MobManager::giveReward(CNSocket *sock) {
|
|||||||
reward->iFatigue_Level = 1;
|
reward->iFatigue_Level = 1;
|
||||||
reward->iItemCnt = 1; // remember to update resplen if you change this
|
reward->iItemCnt = 1; // remember to update resplen if you change this
|
||||||
|
|
||||||
#if 0
|
|
||||||
int slot = ItemManager::findFreeSlot(plr);
|
int slot = ItemManager::findFreeSlot(plr);
|
||||||
if (slot == -1) {
|
|
||||||
#else
|
bool awardDrop = false;
|
||||||
int slot = -1;
|
MobDropChance chance;
|
||||||
if (true) {
|
// sanity check
|
||||||
#endif
|
if (MobDropChances.find(drop.dropChanceType) == MobDropChances.end())
|
||||||
|
std::cout << "[WARN] Unknown Drop Chance Type: " << drop.dropChanceType << std::endl;
|
||||||
|
else {
|
||||||
|
chance = MobDropChances[drop.dropChanceType];
|
||||||
|
awardDrop = (rand() % 1000 < chance.dropChance);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// no drop
|
||||||
|
if (slot == -1 || !awardDrop) {
|
||||||
|
|
||||||
// no room for an item, but you still get FM and taros
|
// no room for an item, but you still get FM and taros
|
||||||
reward->iItemCnt = 0;
|
reward->iItemCnt = 0;
|
||||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||||
} else {
|
} else {
|
||||||
// item reward
|
// item reward
|
||||||
item->sItem.iType = 9;
|
item->sItem = getReward(&drop, &chance);
|
||||||
item->sItem.iID = 1;
|
|
||||||
item->iSlotNum = slot;
|
item->iSlotNum = slot;
|
||||||
item->eIL = 1; // Inventory Location. 1 means player inventory.
|
item->eIL = 1; // Inventory Location. 1 means player inventory.
|
||||||
|
|
||||||
@ -194,8 +227,91 @@ void MobManager::giveReward(CNSocket *sock) {
|
|||||||
plr->Inven[slot] = item->sItem;
|
plr->Inven[slot] = item->sItem;
|
||||||
|
|
||||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// event crates
|
||||||
|
if (settings::EVENTMODE != 0)
|
||||||
|
giveEventReward(sock, plr);
|
||||||
|
}
|
||||||
|
|
||||||
|
sItemBase MobManager::getReward(MobDrop* drop, MobDropChance* chance) {
|
||||||
|
sItemBase reward = {};
|
||||||
|
reward.iType = 9;
|
||||||
|
reward.iOpt = 1;
|
||||||
|
|
||||||
|
int total = 0;
|
||||||
|
for (int ratio : chance->cratesRatio)
|
||||||
|
total += ratio;
|
||||||
|
|
||||||
|
// randomizing a crate
|
||||||
|
int randomNum = rand() % total;
|
||||||
|
int i = 0;
|
||||||
|
int sum = 0;
|
||||||
|
do {
|
||||||
|
reward.iID = drop->crateIDs[i];
|
||||||
|
sum += chance->cratesRatio[i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
while (sum<=randomNum);
|
||||||
|
return reward;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MobManager::giveEventReward(CNSocket* sock, Player* player) {
|
||||||
|
|
||||||
|
// random drop chance
|
||||||
|
if (rand() % 100 > settings::EVENTCRATECHANCE)
|
||||||
|
return;
|
||||||
|
// no slot = no award
|
||||||
|
int slot = ItemManager::findFreeSlot(player);
|
||||||
|
if (slot == -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||||
|
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||||
|
// we know it's only one trailing struct, so we can skip full validation
|
||||||
|
|
||||||
|
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||||
|
sP_FE2CL_REP_REWARD_ITEM* reward = (sP_FE2CL_REP_REWARD_ITEM*)respbuf;
|
||||||
|
sItemReward* item = (sItemReward*)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||||
|
|
||||||
|
// don't forget to zero the buffer!
|
||||||
|
memset(respbuf, 0, resplen);
|
||||||
|
|
||||||
|
// leave everything here as it is
|
||||||
|
reward->m_iCandy = player->money;
|
||||||
|
reward->m_iFusionMatter = player->fusionmatter;
|
||||||
|
reward->m_iBatteryN = player->batteryN;
|
||||||
|
reward->m_iBatteryW = player->batteryW;
|
||||||
|
reward->iFatigue = 100; // prevents warning message
|
||||||
|
reward->iFatigue_Level = 1;
|
||||||
|
reward->iItemCnt = 1; // remember to update resplen if you change this
|
||||||
|
|
||||||
|
// which crate to drop
|
||||||
|
int crateId;
|
||||||
|
switch (settings::EVENTMODE)
|
||||||
|
{
|
||||||
|
// knishmas
|
||||||
|
case 1: crateId = 1187; break;
|
||||||
|
// halloween
|
||||||
|
case 2: crateId = 1181; break;
|
||||||
|
// spring
|
||||||
|
case 3: crateId = 1126; break;
|
||||||
|
// what
|
||||||
|
default:
|
||||||
|
std::cout << "[WARN] Unknown event Id " << settings::EVENTMODE << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item->sItem.iType = 9;
|
||||||
|
item->sItem.iID = crateId;
|
||||||
|
item->sItem.iOpt = 1;
|
||||||
|
item->iSlotNum = slot;
|
||||||
|
item->eIL = 1; // Inventory Location. 1 means player inventory.
|
||||||
|
|
||||||
|
// update player
|
||||||
|
player->Inven[slot] = item->sItem;
|
||||||
|
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||||
}
|
}
|
||||||
|
|
||||||
int MobManager::hitMob(CNSocket *sock, Mob *mob, int damage) {
|
int MobManager::hitMob(CNSocket *sock, Mob *mob, int damage) {
|
||||||
@ -233,7 +349,7 @@ void MobManager::killMob(CNSocket *sock, Mob *mob) {
|
|||||||
mob->target = nullptr;
|
mob->target = nullptr;
|
||||||
mob->appearanceData.iConditionBitFlag = 0;
|
mob->appearanceData.iConditionBitFlag = 0;
|
||||||
mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
|
mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
|
||||||
|
|
||||||
// check for the edge case where hitting the mob did not aggro it
|
// check for the edge case where hitting the mob did not aggro it
|
||||||
if (sock != nullptr) {
|
if (sock != nullptr) {
|
||||||
Player* plr = PlayerManager::getPlayer(sock);
|
Player* plr = PlayerManager::getPlayer(sock);
|
||||||
@ -242,7 +358,7 @@ void MobManager::killMob(CNSocket *sock, Mob *mob) {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
|
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
|
||||||
giveReward(sock);
|
giveReward(sock, mob);
|
||||||
MissionManager::mobKilled(sock, mob->appearanceData.iNPCType);
|
MissionManager::mobKilled(sock, mob->appearanceData.iNPCType);
|
||||||
} else {
|
} else {
|
||||||
plr = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
plr = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
||||||
@ -252,7 +368,7 @@ void MobManager::killMob(CNSocket *sock, Mob *mob) {
|
|||||||
|
|
||||||
for (int i = 0; i < plr->groupCnt; i++) {
|
for (int i = 0; i < plr->groupCnt; i++) {
|
||||||
CNSocket* sockTo = PlayerManager::getSockFromID(plr->groupIDs[i]);
|
CNSocket* sockTo = PlayerManager::getSockFromID(plr->groupIDs[i]);
|
||||||
giveReward(sockTo);
|
giveReward(sockTo, mob);
|
||||||
MissionManager::mobKilled(sockTo, mob->appearanceData.iNPCType);
|
MissionManager::mobKilled(sockTo, mob->appearanceData.iNPCType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ struct Mob : public BaseNPC {
|
|||||||
int spawnX;
|
int spawnX;
|
||||||
int spawnY;
|
int spawnY;
|
||||||
int spawnZ;
|
int spawnZ;
|
||||||
|
int level;
|
||||||
|
|
||||||
// dead
|
// dead
|
||||||
time_t killedTime = 0;
|
time_t killedTime = 0;
|
||||||
@ -42,6 +43,9 @@ struct Mob : public BaseNPC {
|
|||||||
time_t nextAttack = 0;
|
time_t nextAttack = 0;
|
||||||
int roamX, roamY, roamZ;
|
int roamX, roamY, roamZ;
|
||||||
|
|
||||||
|
//drop
|
||||||
|
int dropType;
|
||||||
|
|
||||||
// temporary; until we're sure what's what
|
// temporary; until we're sure what's what
|
||||||
nlohmann::json data;
|
nlohmann::json data;
|
||||||
|
|
||||||
@ -53,6 +57,8 @@ struct Mob : public BaseNPC {
|
|||||||
|
|
||||||
regenTime = data["m_iRegenTime"];
|
regenTime = data["m_iRegenTime"];
|
||||||
idleRange = (int)data["m_iIdleRange"] * 2; // TODO: tuning?
|
idleRange = (int)data["m_iIdleRange"] * 2; // TODO: tuning?
|
||||||
|
dropType = data["m_iDropType"];
|
||||||
|
level = data["m_iNpcLevel"];
|
||||||
|
|
||||||
// XXX: temporarily force respawns for Fusions until we implement instancing
|
// XXX: temporarily force respawns for Fusions until we implement instancing
|
||||||
//if (regenTime >= 300000000)
|
//if (regenTime >= 300000000)
|
||||||
@ -84,9 +90,24 @@ struct Mob : public BaseNPC {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct MobDropChance {
|
||||||
|
int dropChance;
|
||||||
|
std::vector<int> cratesRatio;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MobDrop {
|
||||||
|
std::vector<int> crateIDs;
|
||||||
|
int dropChanceType;
|
||||||
|
int taros;
|
||||||
|
int fm;
|
||||||
|
int boosts;
|
||||||
|
};
|
||||||
|
|
||||||
namespace MobManager {
|
namespace MobManager {
|
||||||
extern std::map<int32_t, Mob*> Mobs;
|
extern std::map<int32_t, Mob*> Mobs;
|
||||||
extern std::queue<int32_t> RemovalQueue;
|
extern std::queue<int32_t> RemovalQueue;
|
||||||
|
extern std::map<int32_t, MobDropChance> MobDropChances;
|
||||||
|
extern std::map<int32_t, MobDrop> MobDrops;
|
||||||
extern bool simulateMobs;
|
extern bool simulateMobs;
|
||||||
|
|
||||||
void init();
|
void init();
|
||||||
@ -107,7 +128,10 @@ namespace MobManager {
|
|||||||
void npcAttackPc(Mob *mob, time_t currTime);
|
void npcAttackPc(Mob *mob, time_t currTime);
|
||||||
int hitMob(CNSocket *sock, Mob *mob, int damage);
|
int hitMob(CNSocket *sock, Mob *mob, int damage);
|
||||||
void killMob(CNSocket *sock, Mob *mob);
|
void killMob(CNSocket *sock, Mob *mob);
|
||||||
void giveReward(CNSocket *sock);
|
void giveReward(CNSocket *sock, Mob *mob);
|
||||||
|
sItemBase getReward(MobDrop *drop, MobDropChance *chance);
|
||||||
|
void giveEventReward(CNSocket* sock, Player* player);
|
||||||
|
|
||||||
std::pair<int,int> lerp(int, int, int, int, int);
|
std::pair<int,int> lerp(int, int, int, int, int);
|
||||||
std::pair<int,int> getDamage(int, int, bool, bool, int, int, int);
|
std::pair<int,int> getDamage(int, int, bool, bool, int, int, int);
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ void TableData::init() {
|
|||||||
auto item = _item.value();
|
auto item = _item.value();
|
||||||
int typeOverride = getItemType(i); // used for special cases where iEquipLoc doesn't indicate item type
|
int typeOverride = getItemType(i); // used for special cases where iEquipLoc doesn't indicate item type
|
||||||
ItemManager::ItemData[std::pair<int32_t, int32_t>(item["m_iItemNumber"], typeOverride != -1 ? typeOverride : (int)item["m_iEquipLoc"])]
|
ItemManager::ItemData[std::pair<int32_t, int32_t>(item["m_iItemNumber"], typeOverride != -1 ? typeOverride : (int)item["m_iEquipLoc"])]
|
||||||
= { item["m_iTradeAble"] == 1, item["m_iSellAble"] == 1, item["m_iItemPrice"], item["m_iItemSellPrice"], item["m_iStackNumber"], i > 9 ? 0 : (int)item["m_iMinReqLev"], i > 9 ? 1 : (int)item["m_iRarity"], i > 9 ? 0 : (int)item["m_iPointRat"], i > 9 ? 0 : (int)item["m_iGroupRat"], i > 9 ? 0 : (int)item["m_iDefenseRat"] };
|
= { item["m_iTradeAble"] == 1, item["m_iSellAble"] == 1, item["m_iItemPrice"], item["m_iItemSellPrice"], item["m_iStackNumber"], i > 9 ? 0 : (int)item["m_iMinReqLev"], i > 9 ? 1 : (int)item["m_iRarity"], i > 9 ? 0 : (int)item["m_iPointRat"], i > 9 ? 0 : (int)item["m_iGroupRat"], i > 9 ? 0 : (int)item["m_iDefenseRat"], i > 9 ? 0 : (int)item["m_iReqSex"] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +197,8 @@ void TableData::init() {
|
|||||||
std::cerr << "[WARN] Malformed mobs.json file! Reason:" << err.what() << std::endl;
|
std::cerr << "[WARN] Malformed mobs.json file! Reason:" << err.what() << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadDrops();
|
||||||
|
|
||||||
loadPaths(&nextId); // load paths
|
loadPaths(&nextId); // load paths
|
||||||
|
|
||||||
loadGruntwork(&nextId);
|
loadGruntwork(&nextId);
|
||||||
@ -293,6 +295,115 @@ void TableData::loadPaths(int* nextId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load drops data from JSON.
|
||||||
|
* This has to be called after reading xdt because it reffers to ItemData!!!
|
||||||
|
*/
|
||||||
|
void TableData::loadDrops() {
|
||||||
|
try {
|
||||||
|
std::ifstream inFile(settings::DROPSJSON);
|
||||||
|
nlohmann::json dropData;
|
||||||
|
|
||||||
|
// read file into json
|
||||||
|
inFile >> dropData;
|
||||||
|
|
||||||
|
// MobDropChances
|
||||||
|
nlohmann::json mobDropChances = dropData["MobDropChances"];
|
||||||
|
for (nlohmann::json::iterator _dropChance = mobDropChances.begin(); _dropChance != mobDropChances.end(); _dropChance++) {
|
||||||
|
auto dropChance = _dropChance.value();
|
||||||
|
MobDropChance toAdd = {};
|
||||||
|
toAdd.dropChance = (int)dropChance["DropChance"];
|
||||||
|
for (nlohmann::json::iterator _cratesRatio = dropChance["CratesRatio"].begin(); _cratesRatio != dropChance["CratesRatio"].end(); _cratesRatio++) {
|
||||||
|
toAdd.cratesRatio.push_back((int)_cratesRatio.value());
|
||||||
|
}
|
||||||
|
MobManager::MobDropChances[(int)dropChance["Type"]] = toAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MobDrops
|
||||||
|
nlohmann::json mobDrops = dropData["MobDrops"];
|
||||||
|
for (nlohmann::json::iterator _drop = mobDrops.begin(); _drop != mobDrops.end(); _drop++) {
|
||||||
|
auto drop = _drop.value();
|
||||||
|
MobDrop toAdd = {};
|
||||||
|
for (nlohmann::json::iterator _crates = drop["CrateIDs"].begin(); _crates != drop["CrateIDs"].end(); _crates++) {
|
||||||
|
toAdd.crateIDs.push_back((int)_crates.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
toAdd.dropChanceType = (int)drop["DropChance"];
|
||||||
|
// Check if DropChance exists
|
||||||
|
if (MobManager::MobDropChances.find(toAdd.dropChanceType) == MobManager::MobDropChances.end())
|
||||||
|
{
|
||||||
|
std::string errorMessage = " MobDropChance not found: " + std::to_string((toAdd.dropChanceType));
|
||||||
|
throw (std::exception((errorMessage).c_str()));
|
||||||
|
}
|
||||||
|
// Check if number of crates is correct
|
||||||
|
if (!(MobManager::MobDropChances[(int)drop["DropChance"]].cratesRatio.size() == toAdd.crateIDs.size()))
|
||||||
|
{
|
||||||
|
std::string errorMessage = " DropType " + std::to_string((int)drop["DropType"]) + " contains invalid number of crates";
|
||||||
|
throw (std::exception((errorMessage).c_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
toAdd.taros = (int)drop["Taros"];
|
||||||
|
toAdd.fm = (int)drop["FM"];
|
||||||
|
toAdd.boosts = (int)drop["Boosts"];
|
||||||
|
MobManager::MobDrops[(int)drop["DropType"]] = toAdd;
|
||||||
|
}
|
||||||
|
std::cout << "[INFO] Loaded " << MobManager::MobDrops.size() << " Mob Drop Types"<< std::endl;
|
||||||
|
// Rarity Ratios
|
||||||
|
nlohmann::json rarities = dropData["RarityRatios"];
|
||||||
|
for (nlohmann::json::iterator _rarity = rarities.begin(); _rarity != rarities.end(); _rarity++) {
|
||||||
|
auto rarity = _rarity.value();
|
||||||
|
std::vector<int> toAdd;
|
||||||
|
for (nlohmann::json::iterator _ratio = rarity["Ratio"].begin(); _ratio != rarity["Ratio"].end(); _ratio++){
|
||||||
|
toAdd.push_back((int)_ratio.value());
|
||||||
|
}
|
||||||
|
ItemManager::RarityRatios[(int)rarity["Type"]] = toAdd;
|
||||||
|
}
|
||||||
|
// Crates
|
||||||
|
nlohmann::json crates = dropData["Crates"];
|
||||||
|
for (nlohmann::json::iterator _crate = crates.begin(); _crate != crates.end(); _crate++) {
|
||||||
|
auto crate = _crate.value();
|
||||||
|
Crate toAdd;
|
||||||
|
toAdd.rarityRatioId = (int)crate["RarityRatio"];
|
||||||
|
for (nlohmann::json::iterator _itemSet = crate["ItemSets"].begin(); _itemSet != crate["ItemSets"].end(); _itemSet++) {
|
||||||
|
toAdd.itemSets.push_back((int)_itemSet.value());
|
||||||
|
}
|
||||||
|
ItemManager::Crates[(int)crate["Id"]] = toAdd;
|
||||||
|
}
|
||||||
|
// Crate Items
|
||||||
|
nlohmann::json items = dropData["Items"];
|
||||||
|
int itemCount = 0;
|
||||||
|
for (nlohmann::json::iterator _item = items.begin(); _item != items.end(); _item++) {
|
||||||
|
auto item = _item.value();
|
||||||
|
std::pair<int32_t, int32_t> itemSetkey = std::make_pair((int)item["ItemSet"], (int)item["Rarity"]);
|
||||||
|
std::pair<int32_t, int32_t> itemDataKey = std::make_pair((int)item["Id"], (int)item["Type"]);
|
||||||
|
if (ItemManager::ItemData.find(itemDataKey) == ItemManager::ItemData.end())
|
||||||
|
{
|
||||||
|
char buff[255];
|
||||||
|
sprintf(buff, "Unknown item with Id %d and Type %d", (int)item["Id"], (int)item["Type"]);
|
||||||
|
throw (std::exception(buff));
|
||||||
|
}
|
||||||
|
std::map<std::pair<int32_t, int32_t>, Item>::iterator toAdd = ItemManager::ItemData.find(itemDataKey);
|
||||||
|
// if item collection doesn't exist, start a new one
|
||||||
|
if (ItemManager::CrateItems.find(itemSetkey) == ItemManager::CrateItems.end()) {
|
||||||
|
std::vector<std::map<std::pair<int32_t, int32_t>, Item>::iterator> vector;
|
||||||
|
vector.push_back(toAdd);
|
||||||
|
ItemManager::CrateItems[itemSetkey] = vector;
|
||||||
|
}
|
||||||
|
// else add a new element to existing collection
|
||||||
|
else
|
||||||
|
ItemManager::CrateItems[itemSetkey].push_back(toAdd);
|
||||||
|
itemCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[INFO] Loaded " << ItemManager::Crates.size() << " Crates containing "
|
||||||
|
<<itemCount<<" items" << std::endl;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (const std::exception& err) {
|
||||||
|
std::cerr << "[WARN] Malformed drops.json file! Reason:" << err.what() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Create a full and properly-paced path by interpolating between keyframes.
|
* Create a full and properly-paced path by interpolating between keyframes.
|
||||||
*/
|
*/
|
||||||
|
@ -17,6 +17,7 @@ namespace TableData {
|
|||||||
|
|
||||||
int getItemType(int);
|
int getItemType(int);
|
||||||
void loadPaths(int*);
|
void loadPaths(int*);
|
||||||
|
void loadDrops();
|
||||||
void constructPathSkyway(nlohmann::json::iterator);
|
void constructPathSkyway(nlohmann::json::iterator);
|
||||||
void constructPathSlider(nlohmann::json, int, int);
|
void constructPathSlider(nlohmann::json, int, int);
|
||||||
void constructPathNPC(nlohmann::json::iterator, int id=0);
|
void constructPathNPC(nlohmann::json::iterator, int id=0);
|
||||||
|
@ -101,9 +101,15 @@ int main() {
|
|||||||
TransportManager::init();
|
TransportManager::init();
|
||||||
// BuddyManager::init(); // stubbed until we have database integration + lots of bug fixes
|
// BuddyManager::init(); // stubbed until we have database integration + lots of bug fixes
|
||||||
GroupManager::init();
|
GroupManager::init();
|
||||||
|
|
||||||
Database::open();
|
Database::open();
|
||||||
|
|
||||||
|
switch (settings::EVENTMODE)
|
||||||
|
{
|
||||||
|
case 1: std::cout << "[INFO] Event active. Hey, Hey It's Knishmas!" << std::endl; break;
|
||||||
|
case 2: std::cout << "[INFO] Event active. Wishing you a spook-tacular Halloween!" << std::endl; break;
|
||||||
|
case 3: std::cout << "[INFO] Event active. Have a very hoppy Easter!" << std::endl; break;
|
||||||
|
}
|
||||||
|
|
||||||
std::cout << "[INFO] Starting Server Threads..." << std::endl;
|
std::cout << "[INFO] Starting Server Threads..." << std::endl;
|
||||||
CNLoginServer loginServer(settings::LOGINPORT);
|
CNLoginServer loginServer(settings::LOGINPORT);
|
||||||
shardServer = new CNShardServer(settings::SHARDPORT);
|
shardServer = new CNShardServer(settings::SHARDPORT);
|
||||||
|
@ -24,10 +24,15 @@ std::string settings::NPCJSON = "tdata/NPCs.json";
|
|||||||
std::string settings::XDTJSON = "tdata/xdt.json";
|
std::string settings::XDTJSON = "tdata/xdt.json";
|
||||||
std::string settings::MOBJSON = "tdata/mobs.json";
|
std::string settings::MOBJSON = "tdata/mobs.json";
|
||||||
std::string settings::PATHJSON = "tdata/paths.json";
|
std::string settings::PATHJSON = "tdata/paths.json";
|
||||||
|
std::string settings::DROPSJSON = "tdata/drops.json";
|
||||||
std::string settings::GRUNTWORKJSON = "tdata/gruntwork.json";
|
std::string settings::GRUNTWORKJSON = "tdata/gruntwork.json";
|
||||||
std::string settings::MOTDSTRING = "Welcome to OpenFusion!";
|
std::string settings::MOTDSTRING = "Welcome to OpenFusion!";
|
||||||
int settings::ACCLEVEL = 1;
|
int settings::ACCLEVEL = 1;
|
||||||
|
|
||||||
|
// event mode settings
|
||||||
|
int settings::EVENTMODE = 0;
|
||||||
|
int settings::EVENTCRATECHANCE = 10;
|
||||||
|
|
||||||
void settings::init() {
|
void settings::init() {
|
||||||
INIReader reader("config.ini");
|
INIReader reader("config.ini");
|
||||||
|
|
||||||
@ -56,8 +61,11 @@ void settings::init() {
|
|||||||
NPCJSON = reader.Get("shard", "npcdata", NPCJSON);
|
NPCJSON = reader.Get("shard", "npcdata", NPCJSON);
|
||||||
XDTJSON = reader.Get("shard", "xdtdata", XDTJSON);
|
XDTJSON = reader.Get("shard", "xdtdata", XDTJSON);
|
||||||
MOBJSON = reader.Get("shard", "mobdata", MOBJSON);
|
MOBJSON = reader.Get("shard", "mobdata", MOBJSON);
|
||||||
|
DROPSJSON = reader.Get("shard", "dropdata", DROPSJSON);
|
||||||
PATHJSON = reader.Get("shard", "pathdata", PATHJSON);
|
PATHJSON = reader.Get("shard", "pathdata", PATHJSON);
|
||||||
GRUNTWORKJSON = reader.Get("shard", "gruntwork", GRUNTWORKJSON);
|
GRUNTWORKJSON = reader.Get("shard", "gruntwork", GRUNTWORKJSON);
|
||||||
MOTDSTRING = reader.Get("shard", "motd", MOTDSTRING);
|
MOTDSTRING = reader.Get("shard", "motd", MOTDSTRING);
|
||||||
ACCLEVEL = reader.GetInteger("shard", "accountlevel", ACCLEVEL);
|
ACCLEVEL = reader.GetInteger("shard", "accountlevel", ACCLEVEL);
|
||||||
|
EVENTMODE = reader.GetInteger("shard", "eventmode", EVENTMODE);
|
||||||
|
EVENTCRATECHANCE = reader.GetInteger("shard", "eventcratechance", EVENTCRATECHANCE);
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,10 @@ namespace settings {
|
|||||||
extern std::string XDTJSON;
|
extern std::string XDTJSON;
|
||||||
extern std::string MOBJSON;
|
extern std::string MOBJSON;
|
||||||
extern std::string PATHJSON;
|
extern std::string PATHJSON;
|
||||||
|
extern std::string DROPSJSON;
|
||||||
extern std::string GRUNTWORKJSON;
|
extern std::string GRUNTWORKJSON;
|
||||||
|
extern int EVENTMODE;
|
||||||
|
extern int EVENTCRATECHANCE;
|
||||||
|
|
||||||
void init();
|
void init();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user