28 Commits

Author SHA1 Message Date
630e7a69b1 Make the bcrypt.c changes portable 2023-09-13 23:19:24 +02:00
eb72dc5f2e Forgot to commit this one 2023-09-13 23:08:09 +02:00
a7993bfc10 Replace all instances of time_t with int64_t
This solves all of our problems with 32-bit time_t on 32-bit Windows.
Only had to revert a single occurance in timeStampToStruct() in
src/main.cpp to match the args of localtime().

Invocation:
find src/ -type f | xargs sed -i 's/time_t/int64_t/g'
2023-09-13 22:15:14 +02:00
a716883971 Builds and runs mostly fine 2023-09-13 21:57:19 +02:00
bd0cc3c212 Fix regression with /speed and /jump after previous change
Also changed the case values to use the client definitions.
2023-09-13 04:37:49 +02:00
c636c538eb Fix minor visual bug in setValuePlayer() 2023-09-02 20:59:34 +02:00
d3bef95a7f Fix /npcr
Also removed a redundant invocation of NPCManager::updateNPCPosition()
and simplified the surrounding code.
2023-08-20 05:06:16 +02:00
gsemaj
650f947451 Add .dockerignore file
Fixes an issue where if you had a version.h file generated from cmake,
it would be used in the Dockerfile even though the container uses make.
2023-08-19 18:22:21 +00:00
gsemaj
b12aecad63 Fix vscode launch configs for Windows 2023-07-11 13:52:22 -04:00
gsemaj
5bf0c8f3ea Add launch configurations for vscode 2023-07-11 12:29:47 -04:00
gsemaj
2ddc956c9b Fix sqlite casing and syntax error in cmakelists 2023-07-11 12:29:08 -04:00
gsemaj
4f0ae027a5 Add Dockerfile and docker-compose 2023-06-30 03:31:45 -04:00
23ab908366 Refuse to start if there are invalid NPC types in the JSONs
This fixes an issue where there server would start up fine even if NPC
types from a later build were found in the gruntwork file. Because of
the semantics of the C++ array indexing operator, the index into NPCData
in loadGruntworkPost() would silently create extra entries in the
nlohmann::json array, which would break future NPC type limit checks and
subsequently crash the server at the next invocation of /summonW, and
likely other places.

We fix this by refusing to start the server if any invalid NPC types are
found, because simply skipping them in the gruntwork file would silently
omit them from further writes to the gruntwork file, which would be
undesirable data loss.
2023-06-22 02:43:26 +02:00
be6a4c0a5d Enforce minimum supported libsqlite version
The server now checks the libsqlite both at compile time and on server
startup. The version the executable was built with and the one it's
running with may be different, so long as they're both at or above the
minimum supported version. One or both version numbers are printed on
startup, depending on if they're identical or not.

The compile-time ("Built with") version depends on the sqlite3.h header
used during compilation, while the runtime ("Using") version depends on
either:

* The sqlite3.c version used during compilation, if statically linked.
  (Which may be different from the header version and still compile and run
  fine.)
* The version of the libsqlite3.so or sqlite3.dll that the server
  loaded, if dynamically linked. Version mismatches here are normal,
  especially on Unix systems with their own system libraries.

The current minimum version is 3.33.0, from 2020-08-14, as that's the
one that introduced the UPDATE-FROM syntax used during login by
Database::updateSelectedByPlayerId().

Also rearranged the prints and initialization calls in main() slightly.
2023-03-19 01:41:07 +01:00
8eb1af20c8 Clean up tdata file loading logic slightly
The earlier addition of empty file checks made it just a bit too cumbersome.
2023-03-13 21:42:59 +01:00
e73daa0865 Skip loadGruntworkPre() if there's no gruntwork
Previously, only loadGruntworkPost() would be skipped if the gruntwork
was null, but it was never null at that point because loadGruntworkPre()
would inadvertently create gruntwork["paths"] when it indexes it to
iterate through it.

Now, the gruntwork loading messages will no longer be misleadingly
printed to stdout when there isn't a gruntwork file.
2023-03-13 05:58:49 +01:00
743a39c125 Tolerate empty gruntwork file
This prevents the server from failing to start if a gruntwork file
exists, but happens to be empty.
2023-03-13 05:18:27 +01:00
a9af8713bc Reject network messages too small for the packet size field 2023-03-12 01:45:18 +01:00
4825267537 Use memcpy() instead of casting to load keys
UBSAN complains about the casting approach because it loads a 64-bit
integer from the defaultKeys string which isn't guaranteed to be 64-bit
aligned, which is undefined behavior.
2023-03-11 23:16:09 +01:00
a92cfaff25 Differentiate new connection messages on the login and shard ports 2023-03-11 21:54:56 +01:00
abcfa3445b Move dead socket cleanup out of the poll() loop
This fixes a potential desync between the fds vector and the connections
map that could happen due to file descriptor number reuse.
2023-03-11 03:24:48 +01:00
2bf14200f7 Make CNSocket::kill() idempotent
CNSocket::kill() will now no longer call close() on already closed sockets.

close() should never be called on already closed file descriptors, yet
CNSocket::kill() was lacking any protection against that, despite its
use as both a high-level way of killing player connections and as a
means of ensuring that closing connections have been properly terminated
in the poll() loop.

This was causing close() to be erroneously called on each socket at least
one extra time. It was also introducing a race condition where the login
and shard threads could close each other's newly opened sockets due to
file descriptor reuse when a connection was accept()ed after the first
call to close(), but before the second one. See the close(2) manpage for
details.
2023-03-11 02:59:05 +01:00
876a9c82cd Bump copyright year to 2023 2023-03-06 21:08:45 +01:00
fb5b0eeeb9 Make socket connection state mismatch into a fatal error
These problems are usually not ephemeral, and cause persistent console
spam, so they should immediately crash the server so they can be
investigated.
2023-03-06 02:21:54 +01:00
7aabc507e7 Stop handling the current packet if the server is shutting down
Previously, terminating a running server from the terminal would
sometimes print a benign warning message if the server was currently
handling an incoming packet. This happened because CNServer::step()
would continue handling the packet after CNServer::kill() released the
activeCrit mutex. Now it first re-checks if active has been set to false
in the mean time after acquiring the mutex.
2023-03-06 02:21:54 +01:00
2914b95cff Combat: 3+ targets should automatically kick the connection 2023-03-01 11:18:41 -06:00
dbd2ec2270 Email: update the item slots via a ITEM_MOVE_SUCC packet 2023-02-28 15:15:57 -06:00
50e00a6772 Email: fix issue #186 2023-02-28 15:14:04 -06:00
41 changed files with 326 additions and 142 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
version.h

26
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug (Linux)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/fusion",
"cwd": "${workspaceFolder}"
},
{
"name": "Debug (Windows)",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/Debug/winfusion.exe",
"cwd": "${workspaceFolder}"
},
{
"name": "Release (Windows)",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/Release/winfusion.exe",
"cwd": "${workspaceFolder}"
}
]
}

View File

@@ -44,7 +44,7 @@ add_executable(openfusion ${SOURCES})
set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME}) set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME})
# find sqlite3 and use it # find sqlite3 and use it
find_package(sqlite3 REQUIRED) find_package(SQLite3 REQUIRED)
target_include_directories(openfusion PRIVATE ${SQLite3_INCLUDE_DIRS}) target_include_directories(openfusion PRIVATE ${SQLite3_INCLUDE_DIRS})
target_link_libraries(openfusion PRIVATE ${SQLite3_LIBRARIES}) target_link_libraries(openfusion PRIVATE ${SQLite3_LIBRARIES})
@@ -56,5 +56,5 @@ set_property(TARGET openfusion PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_S
# It's not something you should do, but it's there if you need it... # It's not something you should do, but it's there if you need it...
if (NOT CMAKE_GENERATOR MATCHES "Visual Studio" AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND NOT CMAKE_GENERATOR MATCHES "MinGW Makefiles") if (NOT CMAKE_GENERATOR MATCHES "Visual Studio" AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND NOT CMAKE_GENERATOR MATCHES "MinGW Makefiles")
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
target_link_libraries(openfusion pthread) target_link_libraries(openfusion PRIVATE pthread)
endif() endif()

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM debian:latest
WORKDIR /usr/src/app
RUN apt-get -y update && apt-get install -y \
git \
clang \
make \
libsqlite3-dev
COPY . ./
RUN make -j8
# tabledata should be copied from the host;
# clone it there before building the container
#RUN git submodule update --init --recursive
CMD ["./bin/fusion"]
LABEL Name=openfusion Version=0.0.1

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020-2022 OpenFusion Contributors Copyright (c) 2020-2023 OpenFusion Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -83,7 +83,7 @@ This just works if you're all under the same LAN, but if you want to play over t
## Compiling ## Compiling
OpenFusion has one external dependency: SQLite. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [read this guide on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/Installing-SQLite-on-Windows-using-vcpkg). OpenFusion has one external dependency: SQLite. The oldest compatible version is `3.33.0`. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [read this guide on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/Installing-SQLite-on-Windows-using-vcpkg).
You have two choices for compiling OpenFusion: the included Makefile and the included CMakeLists file. You have two choices for compiling OpenFusion: the included Makefile and the included CMakeLists file.

12
docker-compose.yml Normal file
View File

@@ -0,0 +1,12 @@
version: '3.4'
services:
openfusion:
image: openfusion
build:
context: .
dockerfile: ./Dockerfile
ports:
- "23000:23000"
- "23001:23001"
- "8003:8003"

View File

@@ -546,7 +546,7 @@ bool doDamageNDebuff(Mob *mob, sSkillResult_Damage_N_Debuff *respdata, int i, in
respdata[i].bProtected = 0; respdata[i].bProtected = 0;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, bitFlag); std::pair<CNSocket*, int32_t> key = std::make_pair(sock, bitFlag);
time_t until = getTime() + (time_t)duration * 100; int64_t until = getTime() + (int64_t)duration * 100;
Eggs::EggBuffs[key] = until; Eggs::EggBuffs[key] = until;
} }
respdata[i].iConditionBitFlag = plr->iConditionBitFlag; respdata[i].iConditionBitFlag = plr->iConditionBitFlag;

View File

@@ -69,31 +69,41 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
// Handle serverside value-changes // Handle serverside value-changes
switch (setData->iSetValueType) { switch (setData->iSetValueType) {
case 1: case CN_GM_SET_VALUE_TYPE__HP:
plr->HP = setData->iSetValue; response.iSetValue = plr->HP = setData->iSetValue;
break; break;
case 2: case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY :
plr->batteryW = setData->iSetValue; plr->batteryW = setData->iSetValue;
// caps // caps
if (plr->batteryW > 9999) if (plr->batteryW > 9999)
plr->batteryW = 9999; plr->batteryW = 9999;
response.iSetValue = plr->batteryW;
break; break;
case 3: case CN_GM_SET_VALUE_TYPE__NANO_BATTERY:
plr->batteryN = setData->iSetValue; plr->batteryN = setData->iSetValue;
// caps // caps
if (plr->batteryN > 9999) if (plr->batteryN > 9999)
plr->batteryN = 9999; plr->batteryN = 9999;
response.iSetValue = plr->batteryN;
break; break;
case 4: case CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter); Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
response.iSetValue = plr->fusionmatter;
break; break;
case 5: case CN_GM_SET_VALUE_TYPE__CANDY:
plr->money = setData->iSetValue; response.iSetValue = plr->money = setData->iSetValue;
break;
case CN_GM_SET_VALUE_TYPE__SPEED:
case CN_GM_SET_VALUE_TYPE__JUMP:
response.iSetValue = setData->iSetValue;
break; break;
} }
response.iPC_ID = setData->iPC_ID; response.iPC_ID = setData->iPC_ID;
response.iSetValue = setData->iSetValue;
response.iSetValueType = setData->iSetValueType; response.iSetValueType = setData->iSetValueType;
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE); sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);

View File

@@ -58,7 +58,7 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
static bool checkRapidFire(CNSocket *sock, int targetCount) { static bool checkRapidFire(CNSocket *sock, int targetCount) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
time_t currTime = getTime(); int64_t currTime = getTime();
if (currTime - plr->lastShot < plr->fireRate * 80) if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
@@ -69,7 +69,7 @@ static bool checkRapidFire(CNSocket *sock, int targetCount) {
// 3+ targets should never be possible // 3+ targets should never be possible
if (targetCount > 3) if (targetCount > 3)
plr->suspicionRating += 10000; plr->suspicionRating += 10001;
// kill the socket when the player is too suspicious // kill the socket when the player is too suspicious
if (plr->suspicionRating > 10000) { if (plr->suspicionRating > 10000) {
@@ -158,7 +158,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
PlayerManager::sendToViewable(sock, respbuf, P_FE2CL_PC_ATTACK_NPCs); PlayerManager::sendToViewable(sock, respbuf, P_FE2CL_PC_ATTACK_NPCs);
} }
void Combat::npcAttackPc(Mob *mob, time_t currTime) { void Combat::npcAttackPc(Mob *mob, int64_t currTime) {
Player *plr = PlayerManager::getPlayer(mob->target); Player *plr = PlayerManager::getPlayer(mob->target);
INITVARPACKET(respbuf, sP_FE2CL_NPC_ATTACK_PCs, pkt, sAttackResult, atk); INITVARPACKET(respbuf, sP_FE2CL_NPC_ATTACK_PCs, pkt, sAttackResult, atk);
@@ -654,7 +654,7 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
} }
// rapid fire anti-cheat // rapid fire anti-cheat
time_t currTime = getTime(); int64_t currTime = getTime();
if (currTime - plr->lastShot < plr->fireRate * 80) if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0) else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
@@ -726,8 +726,8 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
Bullets[plr->iID].erase(resp->iBulletID); Bullets[plr->iID].erase(resp->iBulletID);
} }
static void playerTick(CNServer *serv, time_t currTime) { static void playerTick(CNServer *serv, int64_t currTime) {
static time_t lastHealTime = 0; static int64_t lastHealTime = 0;
for (auto& pair : PlayerManager::players) { for (auto& pair : PlayerManager::players) {
CNSocket *sock = pair.first; CNSocket *sock = pair.first;

View File

@@ -23,7 +23,7 @@ namespace Combat {
void init(); void init();
void npcAttackPc(Mob *mob, time_t currTime); void npcAttackPc(Mob *mob, int64_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);
} }

View File

@@ -358,23 +358,19 @@ static void npcRotateCommand(std::string full, std::vector<std::string>& args, C
int angle = (plr->angle + 180) % 360; int angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, npc->instanceID, angle); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, npc->instanceID, angle);
// if it's a gruntwork NPC, rotate in-place bool isGruntworkNpc = true;
if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) != TableData::RunningMobs.end()) {
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, npc->instanceID, angle);
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for gruntwork NPC " // add a rotation entry to the gruntwork file, unless it's already a gruntwork NPC
+ std::to_string(npc->appearanceData.iNPC_ID)); if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) == TableData::RunningMobs.end()) {
} else {
TableData::RunningNPCRotations[npc->appearanceData.iNPC_ID] = angle; TableData::RunningNPCRotations[npc->appearanceData.iNPC_ID] = angle;
isGruntworkNpc = false;
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for NPC "
+ std::to_string(npc->appearanceData.iNPC_ID));
} }
// update rotation clientside Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) +
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt); " for " + (isGruntworkNpc ? "gruntwork " : "") + "NPC " + std::to_string(npc->appearanceData.iNPC_ID));
pkt.NPCAppearanceData = npc->appearanceData;
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER); // update rotation clientside by refreshing the player's chunks (same as the /refresh command)
PlayerManager::updatePlayerPositionForWarp(sock, plr->x, plr->y, plr->z, plr->instanceID);
} }
static void refreshCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void refreshCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {

View File

@@ -11,7 +11,7 @@
using namespace Eggs; using namespace Eggs;
/// sock, CBFlag -> until /// sock, CBFlag -> until
std::map<std::pair<CNSocket*, int32_t>, time_t> Eggs::EggBuffs; std::map<std::pair<CNSocket*, int32_t>, int64_t> Eggs::EggBuffs;
std::unordered_map<int, EggType> Eggs::EggTypes; std::unordered_map<int, EggType> Eggs::EggTypes;
int Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) { int Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
@@ -79,15 +79,15 @@ int Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
// save the buff serverside; // save the buff serverside;
// if you get the same buff again, new duration will override the previous one // if you get the same buff again, new duration will override the previous one
time_t until = getTime() + (time_t)duration * 1000; int64_t until = getTime() + (int64_t)duration * 1000;
EggBuffs[key] = until; EggBuffs[key] = until;
return 0; return 0;
} }
static void eggStep(CNServer* serv, time_t currTime) { static void eggStep(CNServer* serv, int64_t currTime) {
// tick buffs // tick buffs
time_t timeStamp = currTime; int64_t timeStamp = currTime;
auto it = EggBuffs.begin(); auto it = EggBuffs.begin();
while (it != EggBuffs.end()) { while (it != EggBuffs.end()) {
// check remaining time // check remaining time
@@ -254,7 +254,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
else { else {
Chunking::removeEntityFromChunks(Chunking::getViewableChunks(egg->chunkPos), eggRef); Chunking::removeEntityFromChunks(Chunking::getViewableChunks(egg->chunkPos), eggRef);
egg->dead = true; egg->dead = true;
egg->deadUntil = getTime() + (time_t)type->regen * 1000; egg->deadUntil = getTime() + (int64_t)type->regen * 1000;
egg->appearanceData.iHP = 0; egg->appearanceData.iHP = 0;
} }
} }

View File

@@ -11,7 +11,7 @@ struct EggType {
}; };
namespace Eggs { namespace Eggs {
extern std::map<std::pair<CNSocket*, int32_t>, time_t> EggBuffs; extern std::map<std::pair<CNSocket*, int32_t>, int64_t> EggBuffs;
extern std::unordered_map<int, EggType> EggTypes; extern std::unordered_map<int, EggType> EggTypes;
void init(); void init();

View File

@@ -252,11 +252,26 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
if (attachment.ItemInven.iID == 0) if (attachment.ItemInven.iID == 0)
continue; continue;
sItemBase* item = &pkt->aItem[i].ItemInven;
sItemBase* real = &plr->Inven[attachment.iSlotNum];
resp.aItem[i] = attachment; resp.aItem[i] = attachment;
attachments.push_back(attachment.ItemInven); attachments.push_back(attachment.ItemInven);
attSlots.push_back(attachment.iSlotNum); attSlots.push_back(attachment.iSlotNum);
// delete item if (real->iOpt <= item->iOpt) // delete item (if they attached the whole stack)
plr->Inven[attachment.iSlotNum] = { 0, 0, 0, 0 }; *real = { 0, 0, 0, 0 };
else // otherwise, decrement the item
real->iOpt -= item->iOpt;
// HACK: update the slot
INITSTRUCT(sP_FE2CL_PC_ITEM_MOVE_SUCC, itemResp);
itemResp.iFromSlotNum = attachment.iSlotNum;
itemResp.iToSlotNum = attachment.iSlotNum;
itemResp.FromSlotItem = *real;
itemResp.ToSlotItem = *real;
itemResp.eFrom = (int32_t)Items::SlotType::INVENTORY;
itemResp.eTo = (int32_t)Items::SlotType::INVENTORY;
sock->sendPacket(itemResp, P_FE2CL_PC_ITEM_MOVE_SUCC);
} }
int cost = pkt->iCash + 50 + 20 * attachments.size(); // attached taros + postage int cost = pkt->iCash + 50 + 20 * attachments.size(); // attached taros + postage

View File

@@ -103,14 +103,14 @@ struct CombatNPC : public BaseNPC {
int level = 0; int level = 0;
int speed = 300; int speed = 300;
void (*_stepAI)(CombatNPC*, time_t) = nullptr; void (*_stepAI)(CombatNPC*, int64_t) = nullptr;
// XXX // XXX
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP) : CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP) :
BaseNPC(x, y, z, angle, iID, t, id), BaseNPC(x, y, z, angle, iID, t, id),
maxHealth(maxHP) {} maxHealth(maxHP) {}
virtual void stepAI(time_t currTime) { virtual void stepAI(int64_t currTime) {
if (_stepAI != nullptr) if (_stepAI != nullptr)
_stepAI(this, currTime); _stepAI(this, currTime);
} }
@@ -124,7 +124,7 @@ struct CombatNPC : public BaseNPC {
struct Egg : public BaseNPC { struct Egg : public BaseNPC {
bool summoned = false; bool summoned = false;
bool dead = false; bool dead = false;
time_t deadUntil; int64_t deadUntil;
Egg(int x, int y, int z, uint64_t iID, int t, int32_t id, bool summon) Egg(int x, int y, int z, uint64_t iID, int t, int32_t id, bool summon)
: BaseNPC(x, y, z, 0, iID, t, id) { : BaseNPC(x, y, z, 0, iID, t, id) {

View File

@@ -501,7 +501,7 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
player->Inven[resp->iSlotNum] = resp->RemainItem; player->Inven[resp->iSlotNum] = resp->RemainItem;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, value1); std::pair<CNSocket*, int32_t> key = std::make_pair(sock, value1);
time_t until = getTime() + (time_t)Nanos::SkillTable[144].durationTime[0] * 100; int64_t until = getTime() + (int64_t)Nanos::SkillTable[144].durationTime[0] * 100;
Eggs::EggBuffs[key] = until; Eggs::EggBuffs[key] = until;
} }

View File

@@ -14,7 +14,7 @@ using namespace MobAI;
bool MobAI::simulateMobs = settings::SIMULATEMOBS; bool MobAI::simulateMobs = settings::SIMULATEMOBS;
static void roamingStep(Mob *mob, time_t currTime); static void roamingStep(Mob *mob, int64_t currTime);
/* /*
* Dynamic lerp; distinct from Transport::lerp(). This one doesn't care about height and * Dynamic lerp; distinct from Transport::lerp(). This one doesn't care about height and
@@ -119,7 +119,7 @@ void MobAI::groupRetreat(Mob *mob) {
* Even if they're in range, we can't assume they're all in the same one chunk * Even if they're in range, we can't assume they're all in the same one chunk
* as the mob, since it might be near a chunk boundary. * as the mob, since it might be near a chunk boundary.
*/ */
bool MobAI::aggroCheck(Mob *mob, time_t currTime) { bool MobAI::aggroCheck(Mob *mob, int64_t currTime) {
CNSocket *closest = nullptr; CNSocket *closest = nullptr;
int closestDistance = INT_MAX; int closestDistance = INT_MAX;
@@ -281,7 +281,7 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_CORRUPTION_HIT, resplen); NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_CORRUPTION_HIT, resplen);
} }
static void useAbilities(Mob *mob, time_t currTime) { static void useAbilities(Mob *mob, int64_t currTime) {
/* /*
* targetData approach * targetData approach
* first integer is the count * first integer is the count
@@ -441,7 +441,7 @@ static void drainMobHP(Mob *mob, int amount) {
Combat::killMob(mob->target, mob); Combat::killMob(mob->target, mob);
} }
static void deadStep(Mob *mob, time_t currTime) { static void deadStep(Mob *mob, int64_t currTime) {
// despawn the mob after a short delay // despawn the mob after a short delay
if (mob->killedTime != 0 && !mob->despawned && currTime - mob->killedTime > 2000) { if (mob->killedTime != 0 && !mob->despawned && currTime - mob->killedTime > 2000) {
mob->despawned = true; mob->despawned = true;
@@ -500,7 +500,7 @@ static void deadStep(Mob *mob, time_t currTime) {
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW)); NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
} }
static void combatStep(Mob *mob, time_t currTime) { static void combatStep(Mob *mob, int64_t currTime) {
assert(mob->target != nullptr); assert(mob->target != nullptr);
// lose aggro if the player lost connection // lose aggro if the player lost connection
@@ -542,7 +542,7 @@ static void combatStep(Mob *mob, time_t currTime) {
return; return;
// unbuffing // unbuffing
std::unordered_map<int32_t, time_t>::iterator it = mob->unbuffTimes.begin(); std::unordered_map<int32_t, int64_t>::iterator it = mob->unbuffTimes.begin();
while (it != mob->unbuffTimes.end()) { while (it != mob->unbuffTimes.end()) {
if (currTime >= it->second) { if (currTime >= it->second) {
@@ -640,7 +640,7 @@ static void combatStep(Mob *mob, time_t currTime) {
} }
} }
void MobAI::incNextMovement(Mob *mob, time_t currTime) { void MobAI::incNextMovement(Mob *mob, int64_t currTime) {
if (currTime == 0) if (currTime == 0)
currTime = getTime(); currTime = getTime();
@@ -648,7 +648,7 @@ void MobAI::incNextMovement(Mob *mob, time_t currTime) {
mob->nextMovement = currTime + delay/2 + Rand::rand(delay/2); mob->nextMovement = currTime + delay/2 + Rand::rand(delay/2);
} }
static void roamingStep(Mob *mob, time_t currTime) { static void roamingStep(Mob *mob, int64_t currTime) {
/* /*
* We reuse nextAttack to avoid scanning for players all the time, but to still * We reuse nextAttack to avoid scanning for players all the time, but to still
* do so more often than if we waited for nextMovement (which is way too slow). * do so more often than if we waited for nextMovement (which is way too slow).
@@ -736,7 +736,7 @@ static void roamingStep(Mob *mob, time_t currTime) {
} }
} }
static void retreatStep(Mob *mob, time_t currTime) { static void retreatStep(Mob *mob, int64_t currTime) {
if (mob->nextMovement != 0 && currTime < mob->nextMovement) if (mob->nextMovement != 0 && currTime < mob->nextMovement)
return; return;
@@ -781,7 +781,7 @@ static void retreatStep(Mob *mob, time_t currTime) {
} }
} }
void MobAI::step(CombatNPC *npc, time_t currTime) { void MobAI::step(CombatNPC *npc, int64_t currTime) {
assert(npc->type == EntityType::MOB); assert(npc->type == EntityType::MOB);
auto mob = (Mob*)npc; auto mob = (Mob*)npc;

View File

@@ -13,32 +13,32 @@ enum class MobState {
namespace MobAI { namespace MobAI {
// needs to be declared before Mob's constructor // needs to be declared before Mob's constructor
void step(CombatNPC*, time_t); void step(CombatNPC*, int64_t);
}; };
struct Mob : public CombatNPC { struct Mob : public CombatNPC {
// general // general
MobState state = MobState::INACTIVE; MobState state = MobState::INACTIVE;
std::unordered_map<int32_t,time_t> unbuffTimes = {}; std::unordered_map<int32_t,int64_t> unbuffTimes = {};
// dead // dead
time_t killedTime = 0; int64_t killedTime = 0;
time_t regenTime = 0; int64_t regenTime = 0;
bool summoned = false; bool summoned = false;
bool despawned = false; // for the sake of death animations bool despawned = false; // for the sake of death animations
// roaming // roaming
int idleRange = 0; int idleRange = 0;
const int sightRange = 0; const int sightRange = 0;
time_t nextMovement = 0; int64_t nextMovement = 0;
bool staticPath = false; bool staticPath = false;
int roamX = 0, roamY = 0, roamZ = 0; int roamX = 0, roamY = 0, roamZ = 0;
// combat // combat
CNSocket *target = nullptr; CNSocket *target = nullptr;
time_t nextAttack = 0; int64_t nextAttack = 0;
time_t lastDrainTime = 0; int64_t lastDrainTime = 0;
int skillStyle = -1; // -1 for nothing, 0-2 for corruption, -2 for eruption int skillStyle = -1; // -1 for nothing, 0-2 for corruption, -2 for eruption
int hitX = 0, hitY = 0, hitZ = 0; // for use in ability targeting int hitX = 0, hitY = 0, hitZ = 0; // for use in ability targeting
@@ -98,8 +98,8 @@ namespace MobAI {
extern bool simulateMobs; extern bool simulateMobs;
// TODO: make this internal later // TODO: make this internal later
void incNextMovement(Mob *mob, time_t currTime=0); void incNextMovement(Mob *mob, int64_t currTime=0);
bool aggroCheck(Mob *mob, time_t currTime); bool aggroCheck(Mob *mob, int64_t currTime);
void clearDebuff(Mob *mob); void clearDebuff(Mob *mob);
void followToCombat(Mob *mob); void followToCombat(Mob *mob);
void groupRetreat(Mob *mob); void groupRetreat(Mob *mob);

View File

@@ -350,7 +350,7 @@ void NPCManager::queueNPCRemoval(int32_t id) {
RemovalQueue.push(id); RemovalQueue.push(id);
} }
static void step(CNServer *serv, time_t currTime) { static void step(CNServer *serv, int64_t currTime) {
for (auto& pair : NPCs) { for (auto& pair : NPCs) {
if (pair.second->type != EntityType::COMBAT_NPC && pair.second->type != EntityType::MOB) if (pair.second->type != EntityType::COMBAT_NPC && pair.second->type != EntityType::MOB)
continue; continue;

View File

@@ -79,10 +79,10 @@ struct Player : public Entity {
bool isBuddyBlocked[50] = {}; bool isBuddyBlocked[50] = {};
uint64_t iFirstUseFlag[2] = {}; uint64_t iFirstUseFlag[2] = {};
time_t lastHeartbeat = 0; int64_t lastHeartbeat = 0;
int suspicionRating = 0; int suspicionRating = 0;
time_t lastShot = 0; int64_t lastShot = 0;
std::vector<sItemBase> buyback = {}; std::vector<sItemBase> buyback = {};
Player() { type = EntityType::PLAYER; } Player() { type = EntityType::PLAYER; }

View File

@@ -11,7 +11,7 @@ struct EPInfo {
struct EPRace { struct EPRace {
std::set<int> collectedRings; std::set<int> collectedRings;
int mode, ticketSlot; int mode, ticketSlot;
time_t startTime; int64_t startTime;
}; };
namespace Racing { namespace Racing {

View File

@@ -37,6 +37,22 @@ public:
const char *what() const throw() { return msg.c_str(); } const char *what() const throw() { return msg.c_str(); }
}; };
/*
* We must refuse to run if an invalid NPC type is found in the JSONs, especially
* the gruntwork file. If we were to just skip loading invalid NPCs, they would get
* silently dropped from the gruntwork file, which would be confusing in situations
* where a gruntwork file for the wrong game build was accidentally loaded.
*/
static void ensureValidNPCType(int type, std::string filename) {
// last known NPC type
int npcLimit = NPCManager::NPCData.back()["m_iNpcNumber"];
if (type > npcLimit) {
std::cout << "[FATAL] " << filename << " contains an invalid NPC type: " << type << std::endl;
exit(1);
}
}
/* /*
* Create a full and properly-paced path by interpolating between keyframes. * Create a full and properly-paced path by interpolating between keyframes.
*/ */
@@ -662,6 +678,8 @@ static void loadEggs(json& eggData, int32_t* nextId) {
* Load gruntwork output, if it exists * Load gruntwork output, if it exists
*/ */
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) { static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
if (gruntwork.is_null())
return;
try { try {
auto paths = gruntwork["paths"]; auto paths = gruntwork["paths"];
@@ -711,8 +729,8 @@ static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
} }
static void loadGruntworkPost(json& gruntwork, int32_t* nextId) { static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
if (gruntwork.is_null())
if (gruntwork.is_null()) return; return;
try { try {
// skyway paths // skyway paths
@@ -764,6 +782,8 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int id = (*nextId)--; int id = (*nextId)--;
uint64_t instanceID = mob.find("iMapNum") == mob.end() ? INSTANCE_OVERWORLD : (int)mob["iMapNum"]; uint64_t instanceID = mob.find("iMapNum") == mob.end() ? INSTANCE_OVERWORLD : (int)mob["iMapNum"];
ensureValidNPCType((int)mob["iNPCType"], settings::GRUNTWORKJSON);
if (NPCManager::NPCData[(int)mob["iNPCType"]]["m_iTeam"] == 2) { if (NPCManager::NPCData[(int)mob["iNPCType"]]["m_iTeam"] == 2) {
npc = new Mob(mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iNPCType"], npc = new Mob(mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iNPCType"],
NPCManager::NPCData[(int)mob["iNPCType"]], id); NPCManager::NPCData[(int)mob["iNPCType"]], id);
@@ -783,6 +803,9 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
auto groups = gruntwork["groups"]; auto groups = gruntwork["groups"];
for (auto _group = groups.begin(); _group != groups.end(); _group++) { for (auto _group = groups.begin(); _group != groups.end(); _group++) {
auto leader = _group.value(); auto leader = _group.value();
ensureValidNPCType((int)leader["iNPCType"], settings::GRUNTWORKJSON);
auto td = NPCManager::NPCData[(int)leader["iNPCType"]]; auto td = NPCManager::NPCData[(int)leader["iNPCType"]];
uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"]; uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"];
@@ -803,6 +826,9 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int followerCount = 0; int followerCount = 0;
for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) { for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) {
auto follower = _fol.value(); auto follower = _fol.value();
ensureValidNPCType((int)follower["iNPCType"], settings::GRUNTWORKJSON);
auto tdFol = NPCManager::NPCData[(int)follower["iNPCType"]]; auto tdFol = NPCManager::NPCData[(int)follower["iNPCType"]];
Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, *nextId); Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, *nextId);
@@ -859,10 +885,9 @@ static void loadNPCs(json& npcData) {
npcID += NPC_ID_OFFSET; npcID += NPC_ID_OFFSET;
int instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"]; int instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"];
int type = (int)npc["iNPCType"]; int type = (int)npc["iNPCType"];
if (NPCManager::NPCData[type].is_null()) {
std::cout << "[WARN] NPC type " << type << " not found; skipping (json#" << _npc.key() << ")" << std::endl; ensureValidNPCType(type, settings::NPCJSON);
continue;
}
#ifdef ACADEMY #ifdef ACADEMY
// do not spawn NPCs in the future // do not spawn NPCs in the future
if (npc["iX"] > 512000 && npc["iY"] < 256000) if (npc["iX"] > 512000 && npc["iY"] < 256000)
@@ -904,10 +929,9 @@ static void loadMobs(json& npcData, int32_t* nextId) {
int npcID = std::strtol(_npc.key().c_str(), nullptr, 10); // parse ID string to integer int npcID = std::strtol(_npc.key().c_str(), nullptr, 10); // parse ID string to integer
npcID += MOB_ID_OFFSET; npcID += MOB_ID_OFFSET;
int type = (int)npc["iNPCType"]; int type = (int)npc["iNPCType"];
if (NPCManager::NPCData[type].is_null()) {
std::cout << "[WARN] NPC type " << type << " not found; skipping (json#" << _npc.key() << ")" << std::endl; ensureValidNPCType(type, settings::MOBJSON);
continue;
}
auto td = NPCManager::NPCData[type]; auto td = NPCManager::NPCData[type];
uint64_t instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"]; uint64_t instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"];
@@ -935,7 +959,10 @@ static void loadMobs(json& npcData, int32_t* nextId) {
for (json::iterator _group = groupData.begin(); _group != groupData.end(); _group++) { for (json::iterator _group = groupData.begin(); _group != groupData.end(); _group++) {
auto leader = _group.value(); auto leader = _group.value();
int leadID = std::strtol(_group.key().c_str(), nullptr, 10); // parse ID string to integer int leadID = std::strtol(_group.key().c_str(), nullptr, 10); // parse ID string to integer
leadID += MOB_GROUP_ID_OFFSET; leadID += MOB_GROUP_ID_OFFSET;
ensureValidNPCType(leader["iNPCType"], settings::MOBJSON);
auto td = NPCManager::NPCData[(int)leader["iNPCType"]]; auto td = NPCManager::NPCData[(int)leader["iNPCType"]];
uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"]; uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"];
auto followers = leader["aFollowers"]; auto followers = leader["aFollowers"];
@@ -965,6 +992,9 @@ static void loadMobs(json& npcData, int32_t* nextId) {
int followerCount = 0; int followerCount = 0;
for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) { for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) {
auto follower = _fol.value(); auto follower = _fol.value();
ensureValidNPCType(follower["iNPCType"], settings::MOBJSON);
auto tdFol = NPCManager::NPCData[(int)follower["iNPCType"]]; auto tdFol = NPCManager::NPCData[(int)follower["iNPCType"]];
Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, *nextId); Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, *nextId);
@@ -1070,19 +1100,39 @@ void TableData::init() {
}; };
// load JSON data into tables // load JSON data into tables
std::ifstream fstream;
for (int i = 0; i < 7; i++) { for (int i = 0; i < 7; i++) {
std::pair<json*, std::string>& table = tables[i]; std::pair<json*, std::string>& table = tables[i];
fstream.open(settings::TDATADIR + "/" + table.second); // open file
if (!fstream.fail()) { // scope for fstream
fstream >> *table.first; // load file contents into table {
} else { std::ifstream fstream;
if (table.first != &gruntwork) { // gruntwork isn't critical fstream.open(settings::TDATADIR + "/" + table.second); // open file
// did we fail to open the file?
if (fstream.fail()) {
// gruntwork isn't critical
if (table.first == &gruntwork)
continue;
std::cerr << "[FATAL] Critical tdata file missing: " << table.second << std::endl; std::cerr << "[FATAL] Critical tdata file missing: " << table.second << std::endl;
exit(1); exit(1);
} }
// is the file empty?
if (fstream.peek() == std::ifstream::traits_type::eof()) {
// tolerate empty gruntwork file
if (table.first == &gruntwork) {
std::cout << "[WARN] The gruntwork file is empty" << std::endl;
continue;
}
std::cerr << "[FATAL] Critical tdata file is empty: " << table.second << std::endl;
exit(1);
}
// load file contents into table
fstream >> *table.first;
} }
fstream.close();
// patching: load each patch directory specified in the config file // patching: load each patch directory specified in the config file
@@ -1097,11 +1147,11 @@ void TableData::init() {
std::string patchModuleName = *it; std::string patchModuleName = *it;
std::string patchFile = settings::PATCHDIR + patchModuleName + "/" + table.second; std::string patchFile = settings::PATCHDIR + patchModuleName + "/" + table.second;
try { try {
std::ifstream fstream;
fstream.open(patchFile); fstream.open(patchFile);
fstream >> patch; // load into temporary json object fstream >> patch; // load into temporary json object
std::cout << "[INFO] Patching " << patchFile << std::endl; std::cout << "[INFO] Patching " << patchFile << std::endl;
patchJSON(table.first, &patch); // patch patchJSON(table.first, &patch); // patch
fstream.close();
} catch (const std::exception& err) { } catch (const std::exception& err) {
// no-op // no-op
} }

View File

@@ -319,7 +319,7 @@ static void stepNPCPathing() {
} }
} }
static void tickTransportationSystem(CNServer* serv, time_t currTime) { static void tickTransportationSystem(CNServer* serv, int64_t currTime) {
stepNPCPathing(); stepNPCPathing();
stepSkywaySystem(); stepSkywaySystem();
} }

View File

@@ -41,7 +41,8 @@ int CNSocketEncryption::xorData(uint8_t* buffer, uint8_t* key, int size) {
uint64_t CNSocketEncryption::createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2) { uint64_t CNSocketEncryption::createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2) {
uint64_t num = (uint64_t)(iv1 + 1); uint64_t num = (uint64_t)(iv1 + 1);
uint64_t num2 = (uint64_t)(iv2 + 1); uint64_t num2 = (uint64_t)(iv2 + 1);
uint64_t dEKey = (uint64_t)(*(uint64_t*)&defaultKey[0]); uint64_t dEKey;
memcpy(&dEKey, defaultKey, sizeof(dEKey));
return dEKey * (uTime * num * num2); return dEKey * (uTime * num * num2);
} }
@@ -65,7 +66,7 @@ CNPacketData::CNPacketData(void *b, uint32_t t, int l, int trnum, void *trs):
// ========================================================[[ CNSocket ]]======================================================== // ========================================================[[ CNSocket ]]========================================================
CNSocket::CNSocket(SOCKET s, struct sockaddr_in &addr, PacketHandler ph): sock(s), sockaddr(addr), pHandler(ph) { CNSocket::CNSocket(SOCKET s, struct sockaddr_in &addr, PacketHandler ph): sock(s), sockaddr(addr), pHandler(ph) {
EKey = (uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]); memcpy(&EKey, CNSocketEncryption::defaultKey, sizeof(EKey));
} }
bool CNSocket::sendData(uint8_t* data, int size) { bool CNSocket::sendData(uint8_t* data, int size) {
@@ -109,7 +110,11 @@ bool CNSocket::isAlive() {
} }
void CNSocket::kill() { void CNSocket::kill() {
if (!alive)
return;
alive = false; alive = false;
#ifdef _WIN32 #ifdef _WIN32
shutdown(sock, SD_BOTH); shutdown(sock, SD_BOTH);
closesocket(sock); closesocket(sock);
@@ -241,9 +246,10 @@ void CNSocket::step() {
if (readSize <= 0) { if (readSize <= 0) {
// we aren't reading a packet yet, try to start looking for one // we aren't reading a packet yet, try to start looking for one
int recved = recv(sock, (buffer_t*)readBuffer, sizeof(int32_t), 0); int recved = recv(sock, (buffer_t*)readBuffer, sizeof(int32_t), 0);
if (recved == 0) { if (recved >= 0 && recved < sizeof(int32_t)) {
// the socket was closed normally // too little data for readSize or the socket was closed normally (when 0 bytes were read)
kill(); kill();
return;
} else if (!SOCKETERROR(recved)) { } else if (!SOCKETERROR(recved)) {
// we got our packet size!!!! // we got our packet size!!!!
readSize = *((int32_t*)readBuffer); readSize = *((int32_t*)readBuffer);
@@ -264,11 +270,12 @@ void CNSocket::step() {
} }
if (readSize > 0 && readBufferIndex < readSize) { if (readSize > 0 && readBufferIndex < readSize) {
// read until the end of the packet! (or at least try too) // read until the end of the packet (or at least try to)
int recved = recv(sock, (buffer_t*)(readBuffer + readBufferIndex), readSize - readBufferIndex, 0); int recved = recv(sock, (buffer_t*)(readBuffer + readBufferIndex), readSize - readBufferIndex, 0);
if (recved == 0) { if (recved == 0) {
// the socket was closed normally // the socket was closed normally
kill(); kill();
return;
} else if (!SOCKETERROR(recved)) } else if (!SOCKETERROR(recved))
readBufferIndex += recved; readBufferIndex += recved;
else if (OF_ERRNO != OF_EWOULD) { else if (OF_ERRNO != OF_EWOULD) {
@@ -411,9 +418,9 @@ void CNServer::addPollFD(SOCKET s) {
fds.push_back({s, POLLIN}); fds.push_back({s, POLLIN});
} }
void CNServer::removePollFD(int i) { void CNServer::removePollFD(int fd) {
auto it = fds.begin(); auto it = fds.begin();
while (it != fds.end() && it->fd != fds[i].fd) while (it != fds.end() && it->fd != fd)
it++; it++;
assert(it != fds.end()); assert(it != fds.end());
@@ -458,7 +465,7 @@ void CNServer::start() {
if (!setSockNonblocking(sock, newConnectionSocket)) if (!setSockNonblocking(sock, newConnectionSocket))
continue; continue;
std::cout << "New connection! " << inet_ntoa(address.sin_addr) << std::endl; std::cout << "New " << serverType << " connection! " << inet_ntoa(address.sin_addr) << std::endl;
addPollFD(newConnectionSocket); addPollFD(newConnectionSocket);
@@ -473,10 +480,15 @@ void CNServer::start() {
} else { } else {
std::lock_guard<std::mutex> lock(activeCrit); // protect operations on connections std::lock_guard<std::mutex> lock(activeCrit); // protect operations on connections
// halt packet handling if server is shutting down
if (!active)
return;
// player sockets // player sockets
if (connections.find(fds[i].fd) == connections.end()) { if (connections.find(fds[i].fd) == connections.end()) {
std::cout << "[WARN] Event on non-existant socket?" << std::endl; std::cout << "[FATAL] Event on non-existent socket: " << fds[i].fd << std::endl;
continue; // just to be safe assert(0);
/* not reached */
} }
CNSocket* cSock = connections[fds[i].fd]; CNSocket* cSock = connections[fds[i].fd];
@@ -485,22 +497,29 @@ void CNServer::start() {
if (fds[i].revents & ~POLLIN) if (fds[i].revents & ~POLLIN)
cSock->kill(); cSock->kill();
if (cSock->isAlive()) { if (cSock->isAlive())
cSock->step(); cSock->step();
} else {
killConnection(cSock);
connections.erase(fds[i].fd);
delete cSock;
removePollFD(i);
// a new entry was moved to this position, so we check it again
i--;
}
} }
} }
onStep(); onStep();
// clean up dead connection sockets
auto it = connections.begin();
while (it != connections.end()) {
CNSocket *cSock = it->second;
if (!cSock->isAlive()) {
killConnection(cSock);
it = connections.erase(it);
removePollFD(cSock->sock);
delete cSock;
} else {
it++;
}
}
} }
} }

View File

@@ -208,15 +208,15 @@ public:
}; };
class CNServer; class CNServer;
typedef void (*TimerHandler)(CNServer* serv, time_t time); typedef void (*TimerHandler)(CNServer* serv, int64_t time);
// timer struct // timer struct
struct TimerEvent { struct TimerEvent {
TimerHandler handlr; TimerHandler handlr;
time_t delta; // time to be added to the current time on reset int64_t delta; // time to be added to the current time on reset
time_t scheduledEvent; // time to call handlr() int64_t scheduledEvent; // time to call handlr()
TimerEvent(TimerHandler h, time_t d): handlr(h), delta(d) { TimerEvent(TimerHandler h, int64_t d): handlr(h), delta(d) {
scheduledEvent = 0; scheduledEvent = 0;
} }
}; };
@@ -230,6 +230,7 @@ protected:
const size_t STARTFDSCOUNT = 8; // number of initial PollFD slots const size_t STARTFDSCOUNT = 8; // number of initial PollFD slots
std::vector<PollFD> fds; std::vector<PollFD> fds;
std::string serverType = "invalid";
SOCKET sock; SOCKET sock;
uint16_t port; uint16_t port;
socklen_t addressSize; socklen_t addressSize;

View File

@@ -24,7 +24,7 @@ LoginMetadata* CNShared::getLoginMetadata(int64_t sk) {
return lm; return lm;
} }
void CNShared::pruneLoginMetadata(CNServer *serv, time_t currTime) { void CNShared::pruneLoginMetadata(CNServer *serv, int64_t currTime) {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(mtx);
auto it = logins.begin(); auto it = logins.begin();

View File

@@ -19,11 +19,11 @@
struct LoginMetadata { struct LoginMetadata {
uint64_t FEKey; uint64_t FEKey;
int32_t playerId; int32_t playerId;
time_t timestamp; int64_t timestamp;
}; };
namespace CNShared { namespace CNShared {
void storeLoginMetadata(int64_t sk, LoginMetadata *lm); void storeLoginMetadata(int64_t sk, LoginMetadata *lm);
LoginMetadata* getLoginMetadata(int64_t sk); LoginMetadata* getLoginMetadata(int64_t sk);
void pruneLoginMetadata(CNServer *serv, time_t currTime); void pruneLoginMetadata(CNServer *serv, int64_t currTime);
} }

View File

@@ -45,8 +45,8 @@
std::string U16toU8(char16_t* src, size_t max); std::string U16toU8(char16_t* src, size_t max);
size_t U8toU16(std::string src, char16_t* des, size_t max); // returns number of char16_t that was written at des size_t U8toU16(std::string src, char16_t* des, size_t max); // returns number of char16_t that was written at des
time_t getTime(); int64_t getTime();
time_t getTimestamp(); int64_t getTimestamp();
void terminate(int); void terminate(int);
// The PROTOCOL_VERSION definition is defined by the build system. // The PROTOCOL_VERSION definition is defined by the build system.

View File

@@ -13,7 +13,7 @@ namespace Database {
int AccountID; int AccountID;
std::string Password; std::string Password;
int Selected; int Selected;
time_t BannedUntil; int64_t BannedUntil;
std::string BanReason; std::string BanReason;
}; };
@@ -41,6 +41,7 @@ namespace Database {
uint64_t Timestamp; uint64_t Timestamp;
}; };
void init();
void open(); void open();
void close(); void close();

View File

@@ -236,7 +236,20 @@ static int getTableSize(std::string tableName) {
return result; return result;
} }
void Database::init() {
std::cout << "[INFO] Built with libsqlite " SQLITE_VERSION << std::endl;
if (sqlite3_libversion_number() != SQLITE_VERSION_NUMBER)
std::cout << "[INFO] Using libsqlite " << std::string(sqlite3_libversion()) << std::endl;
if (sqlite3_libversion_number() < MIN_SUPPORTED_SQLITE_NUMBER) {
std::cerr << "[FATAL] Runtime sqlite version too old. Minimum compatible version: " MIN_SUPPORTED_SQLITE << std::endl;
exit(1);
}
}
void Database::open() { void Database::open() {
// XXX: move locks here // XXX: move locks here
int rc = sqlite3_open(settings::DBPATH.c_str(), &db); int rc = sqlite3_open(settings::DBPATH.c_str(), &db);
if (rc != SQLITE_OK) { if (rc != SQLITE_OK) {

View File

@@ -3,6 +3,15 @@
#include "db/Database.hpp" #include "db/Database.hpp"
#include <sqlite3.h> #include <sqlite3.h>
#define MIN_SUPPORTED_SQLITE_NUMBER 3033000
#define MIN_SUPPORTED_SQLITE "3.33.0"
// we can't use this in #error, since it doesn't expand macros
// Compile-time libsqlite version check
#if SQLITE_VERSION_NUMBER < MIN_SUPPORTED_SQLITE_NUMBER
#error libsqlite version too old. Minimum compatible version: 3.33.0
#endif
extern std::mutex dbCrit; extern std::mutex dbCrit;
extern sqlite3 *db; extern sqlite3 *db;

View File

@@ -64,7 +64,7 @@ void terminate(int arg) {
} }
#ifdef _WIN32 #ifdef _WIN32
static BOOL winTerminate(DWORD arg) { static BOOL WINAPI winTerminate(DWORD arg) {
terminate(0); terminate(0);
return FALSE; return FALSE;
} }
@@ -98,6 +98,9 @@ void initsignals() {
} }
int main() { int main() {
std::cout << "[INFO] OpenFusion v" GIT_VERSION << std::endl;
std::cout << "[INFO] Protocol version: " << PROTOCOL_VERSION << std::endl;
#ifdef _WIN32 #ifdef _WIN32
WSADATA wsaData; WSADATA wsaData;
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) { if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
@@ -105,15 +108,15 @@ int main() {
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
#endif #endif
initsignals(); initsignals();
settings::init(); settings::init();
Database::init();
std::cout << "[INFO] OpenFusion v" GIT_VERSION << std::endl;
std::cout << "[INFO] Protocol version: " << PROTOCOL_VERSION << std::endl;
std::cout << "[INFO] Intializing Packet Managers..." << std::endl;
Rand::init(getTime()); Rand::init(getTime());
TableData::init(); TableData::init();
std::cout << "[INFO] Intializing Packet Managers..." << std::endl;
PlayerManager::init(); PlayerManager::init();
PlayerMovement::init(); PlayerMovement::init();
BuiltinCommands::init(); BuiltinCommands::init();
@@ -132,9 +135,10 @@ int main() {
Email::init(); Email::init();
Groups::init(); Groups::init();
Racing::init(); Racing::init();
Database::open();
Trading::init(); Trading::init();
Database::open();
switch (settings::EVENTMODE) { switch (settings::EVENTMODE) {
case 0: break; // no event case 0: break; // no event
case 1: std::cout << "[INFO] Event active. Hey, Hey It's Knishmas!" << std::endl; break; case 1: std::cout << "[INFO] Event active. Hey, Hey It's Knishmas!" << std::endl; break;
@@ -197,21 +201,21 @@ size_t U8toU16(std::string src, char16_t* des, size_t max) {
return tmp.length(); return tmp.length();
} }
time_t getTime() { int64_t getTime() {
using namespace std::chrono; using namespace std::chrono;
milliseconds value = duration_cast<milliseconds>((time_point_cast<milliseconds>(steady_clock::now())).time_since_epoch()); milliseconds value = duration_cast<milliseconds>((time_point_cast<milliseconds>(steady_clock::now())).time_since_epoch());
return (time_t)value.count(); return (int64_t)value.count();
} }
// returns system time in seconds // returns system time in seconds
time_t getTimestamp() { int64_t getTimestamp() {
using namespace std::chrono; using namespace std::chrono;
seconds value = duration_cast<seconds>((time_point_cast<seconds>(system_clock::now())).time_since_epoch()); seconds value = duration_cast<seconds>((time_point_cast<seconds>(system_clock::now())).time_since_epoch());
return (time_t)value.count(); return (int64_t)value.count();
} }
// convert integer timestamp (in s) to FF systime struct // convert integer timestamp (in s) to FF systime struct

View File

@@ -11,6 +11,7 @@
std::map<CNSocket*, CNLoginData> CNLoginServer::loginSessions; std::map<CNSocket*, CNLoginData> CNLoginServer::loginSessions;
CNLoginServer::CNLoginServer(uint16_t p) { CNLoginServer::CNLoginServer(uint16_t p) {
serverType = "login";
port = p; port = p;
pHandler = &CNLoginServer::handlePacket; pHandler = &CNLoginServer::handlePacket;
init(); init();
@@ -208,9 +209,12 @@ void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
// send the resp in with original key // send the resp in with original key
sock->sendPacket(resp, P_LS2CL_REP_LOGIN_SUCC); sock->sendPacket(resp, P_LS2CL_REP_LOGIN_SUCC);
uint64_t defaultKey;
memcpy(&defaultKey, CNSocketEncryption::defaultKey, sizeof(defaultKey));
// update keys // update keys
sock->setEKey(CNSocketEncryption::createNewKey(resp.uiSvrTime, resp.iCharCount + 1, resp.iSlotNum + 1)); sock->setEKey(CNSocketEncryption::createNewKey(resp.uiSvrTime, resp.iCharCount + 1, resp.iSlotNum + 1));
sock->setFEKey(CNSocketEncryption::createNewKey((uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]), login->iClientVerC, 1)); sock->setFEKey(CNSocketEncryption::createNewKey(defaultKey, login->iClientVerC, 1));
DEBUGLOG( DEBUGLOG(
std::cout << "Login Server: Login success. Welcome " << userLogin << " [" << loginSessions[sock].userID << "]" << std::endl; std::cout << "Login Server: Login success. Welcome " << userLogin << " [" << loginSessions[sock].userID << "]" << std::endl;
@@ -570,8 +574,8 @@ void CNLoginServer::killConnection(CNSocket* cns) {
} }
void CNLoginServer::onStep() { void CNLoginServer::onStep() {
time_t currTime = getTime(); int64_t currTime = getTime();
static time_t lastCheck = 0; static int64_t lastCheck = 0;
if (currTime - lastCheck < 16000) if (currTime - lastCheck < 16000)
return; return;

View File

@@ -7,7 +7,7 @@
struct CNLoginData { struct CNLoginData {
int userID; int userID;
time_t lastHeartbeat; int64_t lastHeartbeat;
}; };
enum class LoginError { enum class LoginError {

View File

@@ -16,6 +16,7 @@ std::map<uint32_t, PacketHandler> CNShardServer::ShardPackets;
std::list<TimerEvent> CNShardServer::Timers; std::list<TimerEvent> CNShardServer::Timers;
CNShardServer::CNShardServer(uint16_t p) { CNShardServer::CNShardServer(uint16_t p) {
serverType = "shard";
port = p; port = p;
pHandler = &CNShardServer::handlePacket; pHandler = &CNShardServer::handlePacket;
REGISTER_SHARD_TIMER(keepAliveTimer, 4000); REGISTER_SHARD_TIMER(keepAliveTimer, 4000);
@@ -58,7 +59,7 @@ void CNShardServer::handlePacket(CNSocket* sock, CNPacketData* data) {
PlayerManager::players[sock]->lastHeartbeat = getTime(); PlayerManager::players[sock]->lastHeartbeat = getTime();
} }
void CNShardServer::keepAliveTimer(CNServer* serv, time_t currTime) { void CNShardServer::keepAliveTimer(CNServer* serv, int64_t currTime) {
for (auto& pair : PlayerManager::players) { for (auto& pair : PlayerManager::players) {
if (pair.second->lastHeartbeat != 0 && currTime - pair.second->lastHeartbeat > settings::TIMEOUT) { if (pair.second->lastHeartbeat != 0 && currTime - pair.second->lastHeartbeat > settings::TIMEOUT) {
// if the client hasn't responded in 60 seconds, its a dead connection so throw it out // if the client hasn't responded in 60 seconds, its a dead connection so throw it out
@@ -71,7 +72,7 @@ void CNShardServer::keepAliveTimer(CNServer* serv, time_t currTime) {
} }
} }
void CNShardServer::periodicSaveTimer(CNServer* serv, time_t currTime) { void CNShardServer::periodicSaveTimer(CNServer* serv, int64_t currTime) {
if (PlayerManager::players.empty()) if (PlayerManager::players.empty())
return; return;
@@ -113,7 +114,7 @@ void CNShardServer::kill() {
} }
void CNShardServer::onStep() { void CNShardServer::onStep() {
time_t currTime = getTime(); int64_t currTime = getTime();
// do not evaluate timers if the server is shutting down // do not evaluate timers if the server is shutting down
if (!active) if (!active)

View File

@@ -11,8 +11,8 @@ class CNShardServer : public CNServer {
private: private:
static void handlePacket(CNSocket* sock, CNPacketData* data); static void handlePacket(CNSocket* sock, CNPacketData* data);
static void keepAliveTimer(CNServer*, time_t); static void keepAliveTimer(CNServer*, int64_t);
static void periodicSaveTimer(CNServer* serv, time_t currTime); static void periodicSaveTimer(CNServer* serv, int64_t currTime);
public: public:
static std::map<uint32_t, PacketHandler> ShardPackets; static std::map<uint32_t, PacketHandler> ShardPackets;

View File

@@ -72,7 +72,7 @@ static int process_email(char *buff, std::string email) {
return i; return i;
} }
static void tick(CNServer *serv, time_t delta) { static void tick(CNServer *serv, int64_t delta) {
std::lock_guard<std::mutex> lock(sockLock); std::lock_guard<std::mutex> lock(sockLock);
char buff[BUFSIZE]; char buff[BUFSIZE];
int n; int n;

View File

@@ -17,7 +17,7 @@ int settings::DBSAVEINTERVAL = 240;
int settings::SHARDPORT = 23001; int settings::SHARDPORT = 23001;
std::string settings::SHARDSERVERIP = "127.0.0.1"; std::string settings::SHARDSERVERIP = "127.0.0.1";
bool settings::LOCALHOSTWORKAROUND = true; bool settings::LOCALHOSTWORKAROUND = true;
time_t settings::TIMEOUT = 60000; int64_t settings::TIMEOUT = 60000;
int settings::VIEWDISTANCE = 25600; int settings::VIEWDISTANCE = 25600;
bool settings::SIMULATEMOBS = true; bool settings::SIMULATEMOBS = true;
bool settings::ANTICHEAT = true; bool settings::ANTICHEAT = true;

View File

@@ -11,7 +11,7 @@ namespace settings {
extern std::string SHARDSERVERIP; extern std::string SHARDSERVERIP;
extern bool LOCALHOSTWORKAROUND; extern bool LOCALHOSTWORKAROUND;
extern bool ANTICHEAT; extern bool ANTICHEAT;
extern time_t TIMEOUT; extern int64_t TIMEOUT;
extern int VIEWDISTANCE; extern int VIEWDISTANCE;
extern bool SIMULATEMOBS; extern bool SIMULATEMOBS;
extern int SPAWN_X; extern int SPAWN_X;

View File

@@ -24,7 +24,6 @@
#ifdef _WIN32 || _WIN64 #ifdef _WIN32 || _WIN64
// On windows we need to generate random bytes differently. // On windows we need to generate random bytes differently.
typedef __int64 ssize_t;
#define BCRYPT_HASHSIZE 60 #define BCRYPT_HASHSIZE 60
#include "bcrypt.h" #include "bcrypt.h"
@@ -51,6 +50,7 @@ static int try_close(int fd)
return ret; return ret;
} }
#ifndef _WIN32
static int try_read(int fd, char *out, size_t count) static int try_read(int fd, char *out, size_t count)
{ {
size_t total; size_t total;
@@ -75,6 +75,7 @@ static int try_read(int fd, char *out, size_t count)
return 0; return 0;
} }
#endif
/* /*
* This is a best effort implementation. Nothing prevents a compiler from * This is a best effort implementation. Nothing prevents a compiler from