39 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
7471bcbf38 Fix vehicle rental periods not showing up in vendor listings
Fixes #250.
2022-12-28 17:57:37 +01:00
100b4605ec Fix early CNShared timeout
Revisiting this again; the issue was that the comparison operator was
facing the wrong way, so connections were being pruned every 30 seconds
or less. This was effectively a race condition kicking an unlucky player
every so often when pruning happened exactly during an attempt to enter
the game.

Now that the proper timeout is being enforced, I've reduced it to 5
minutes, down from 15, since it really doesn't need to be that long.
2022-12-11 19:46:29 +01:00
741b898230 Remove redundant copy of Player object when added to the shard
Since the Player object is loaded up in loadPlayer() now, it's pretty
apparent that there's no more reason to copy it around at any point.
2022-12-06 02:11:31 +01:00
3f44f53f97 On login, load Player from DB in shard thread, not in login thread
This avoids some needless data shuffling and fixes a rare desync.
2022-12-06 01:07:21 +01:00
d92b407349 Fix sanity check in emailReceiveItemSingle() 2022-12-05 22:30:02 +01:00
9b3e856a05 Sync player data to DB when trading and sending emails 2022-12-05 22:29:23 +01:00
eb8e54c1f0 Do not evaluate timers if the server is shutting down
This should fix issues with segfaults when the server is being
terminated that sometimes occur because things like NPC path traversal
keep running while the process is executing the signal handler.
2022-11-27 22:33:55 +01:00
1ba0f5e14a Mention Linux instructions in README.md
Also clarify the line about CI binaries.
2022-11-26 20:30:07 +01:00
12dde394c0 Add undocumented config option to disable rapid fire anticheat
This quick hack has been around for a while, so we might as well make it
configurable.

Also updated tdata reference.
2022-11-26 19:36:10 +01:00
b1eea6d4fe [seccomp] Whitelist rseq syscall
Used by glibc 2.35 and later.
2022-11-15 02:30:20 +01:00
f126b88781 [seccomp] Whitelist newfstatat and fix a few #ifdefs
Some newer versions of either glibc or libsqlite3 seem to require this
syscall for the server to terminate properly.
2022-09-04 20:53:17 +02:00
85 changed files with 2842 additions and 2733 deletions

1
.dockerignore Normal file
View File

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

2
.gitignore vendored
View File

@@ -16,5 +16,3 @@ build/
version.h
infer-out
gmon.out
*.bak

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

@@ -3,8 +3,7 @@ project(OpenFusion)
set(CMAKE_CXX_STANDARD 17)
execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
# OpenFusion supports multiple packet/struct versions
# 104 is the default version to build which can be changed
@@ -45,7 +44,7 @@ add_executable(openfusion ${SOURCES})
set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME})
# find sqlite3 and use it
find_package(sqlite3 REQUIRED)
find_package(SQLite3 REQUIRED)
target_include_directories(openfusion PRIVATE ${SQLite3_INCLUDE_DIRS})
target_link_libraries(openfusion PRIVATE ${SQLite3_LIBRARIES})
@@ -57,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...
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)
target_link_libraries(openfusion pthread)
target_link_libraries(openfusion PRIVATE pthread)
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
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
of this software and associated documentation files (the "Software"), to deal

View File

@@ -50,7 +50,6 @@ CXXSRC=\
src/db/email.cpp\
src/sandbox/seccomp.cpp\
src/sandbox/openbsd.cpp\
src/Buffs.cpp\
src/Chat.cpp\
src/CustomCommands.cpp\
src/Entities.cpp\
@@ -97,7 +96,6 @@ CXXHDR=\
vendor/JSON.hpp\
vendor/INIReader.hpp\
vendor/JSON.hpp\
src/Buffs.hpp\
src/Chat.hpp\
src/CustomCommands.hpp\
src/Entities.hpp\

View File

@@ -25,6 +25,8 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
5. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 4.
Instructions for getting the client to run on Linux through Wine can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Running-the-game-client-on-Linux).
### Hosting a server
1. Grab `OpenFusionServer-1.4-original.zip` or `OpenFusionServer-1.4-academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.4).
@@ -35,7 +37,7 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
3. Lastly Game Version - select `beta-20100104` if you downloaded the original zip, or `beta-20111013` if you downloaded the academy zip.
5. Once you've added the server to the list, connect to it and log in. If you're having trouble with this, refer to steps 4 and 5 from the previous section.
If you want, [compiled binaries (artifacts) for each functional commit can be found here.](http://cdn.dexlabs.systems/of-builds/)
If you want to run the latest development builds of the server, [compiled binaries (artifacts) for each functional commit can be found here.](http://cdn.dexlabs.systems/of-builds/)
For a more detailed overview of the game's architecture and how to configure it, read the following sections.
@@ -81,7 +83,7 @@ This just works if you're all under the same LAN, but if you want to play over t
## 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.

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"

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +1,64 @@
#pragma once
#include "core/Core.hpp"
#include "Combat.hpp"
#include "Entities.hpp"
#include "Player.hpp"
typedef void (*PowerHandler)(CNSocket*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
#include <map>
#include <vector>
struct NanoPower {
int16_t skillType;
int32_t bitFlag;
int16_t timeBuffID;
PowerHandler handler;
constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain);
NanoPower(int16_t s, int32_t b, int16_t t, PowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
enum class SkillEffectTarget {
POINT = 1,
SELF = 2,
CONE = 3,
WEAPON = 4,
AREA_SELF = 5,
AREA_TARGET = 6
};
void handle(CNSocket *sock, std::vector<int> targetData, int16_t nanoID, int16_t skillID, int16_t duration, int16_t amount) {
if (handler == nullptr)
return;
enum class SkillTargetType {
MOBS = 1,
SELF = 2,
GROUP = 3
};
enum class SkillDrainType {
ACTIVE = 1,
PASSIVE = 2
};
struct SkillResult {
size_t size;
uint8_t payload[MAX_SKILLRESULT_SIZE];
SkillResult(size_t len, void* dat) {
size = len;
memcpy(payload, dat, len);
handler(sock, targetData, nanoID, skillID, duration, amount, skillType, bitFlag, timeBuffID);
}
SkillResult() {
size = 0;
};
typedef void (*MobPowerHandler)(Mob*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
struct MobPower {
int16_t skillType;
int32_t bitFlag;
int16_t timeBuffID;
MobPowerHandler handler;
MobPower(int16_t s, int32_t b, int16_t t, MobPowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
void handle(Mob *mob, std::vector<int> targetData, int16_t skillID, int16_t duration, int16_t amount) {
if (handler == nullptr)
return;
handler(mob, targetData, skillID, duration, amount, skillType, bitFlag, timeBuffID);
}
};
struct SkillData {
int skillType; // eST
SkillEffectTarget effectTarget;
int effectType; // always 1?
SkillTargetType targetType;
SkillDrainType drainType;
int skillType;
int targetType;
int drainType;
int effectArea;
int batteryUse[4];
int durationTime[4];
int valueTypes[3];
int values[3][4];
int powerIntensity[4];
};
namespace Abilities {
namespace Nanos {
extern std::vector<NanoPower> NanoPowers;
extern std::map<int32_t, SkillData> SkillTable;
void useNanoSkill(CNSocket*, SkillData*, sNano&, std::vector<ICombatant*>);
void useNPCSkill(EntityRef, int skillID, std::vector<ICombatant*>);
void nanoUnbuff(CNSocket* sock, std::vector<int> targetData, int32_t bitFlag, int16_t timeBuffID, int16_t amount, bool groupPower);
int applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags);
std::vector<ICombatant*> matchTargets(SkillData*, int, int32_t*);
int getCSTBFromST(int eSkillType);
std::vector<int> findTargets(Player* plr, int skillID, CNPacketData* data = nullptr);
}
namespace Combat {
extern std::vector<MobPower> MobPowers;
}

View File

@@ -1,10 +1,15 @@
#include "Buddies.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "Buddies.hpp"
#include "PlayerManager.hpp"
#include "Buddies.hpp"
#include "db/Database.hpp"
#include "Items.hpp"
#include "db/Database.hpp"
#include <iostream>
#include <chrono>
#include <algorithm>
#include <thread>
using namespace Buddies;

View File

@@ -1,5 +1,7 @@
#pragma once
#include "Player.hpp"
#include "core/Core.hpp"
#include "core/Core.hpp"
namespace Buddies {

View File

@@ -1,151 +0,0 @@
#include "Buffs.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
using namespace Buffs;
void Buff::tick(time_t currTime) {
auto it = stacks.begin();
while(it != stacks.end()) {
BuffStack& stack = *it;
//if(onTick) onTick(self, this, currTime);
if(stack.durationTicks == 0) {
BuffStack deadStack = stack;
it = stacks.erase(it);
if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack);
} else {
if(stack.durationTicks > 0) stack.durationTicks--;
it++;
}
}
}
void Buff::combatTick(time_t currTime) {
if(onCombatTick) onCombatTick(self, this, currTime);
}
void Buff::clear() {
while(!stacks.empty()) {
BuffStack stack = stacks.back();
stacks.pop_back();
if(onUpdate) onUpdate(self, this, ETBU_DEL, &stack);
}
}
void Buff::clear(BuffClass buffClass) {
auto it = stacks.begin();
while(it != stacks.end()) {
BuffStack& stack = *it;
if(stack.buffStackClass == buffClass) {
BuffStack deadStack = stack;
it = stacks.erase(it);
if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack);
} else it++;
}
}
void Buff::addStack(BuffStack* stack) {
stacks.push_back(*stack);
if(onUpdate) onUpdate(self, this, ETBU_ADD, &stacks.back());
}
bool Buff::hasClass(BuffClass buffClass) {
for(BuffStack& stack : stacks) {
if(stack.buffStackClass == buffClass)
return true;
}
return false;
}
BuffClass Buff::maxClass() {
BuffClass buffClass = BuffClass::NONE;
for(BuffStack& stack : stacks) {
if(stack.buffStackClass > buffClass)
buffClass = stack.buffStackClass;
}
return buffClass;
}
int Buff::getValue(BuffValueSelector selector) {
if(isStale()) return 0;
int value = selector == BuffValueSelector::NET_TOTAL ? 0 : stacks.front().value;
for(BuffStack& stack : stacks) {
switch(selector)
{
case BuffValueSelector::NET_TOTAL:
value += stack.value;
break;
case BuffValueSelector::MIN_VALUE:
if(stack.value < value) value = stack.value;
break;
case BuffValueSelector::MAX_VALUE:
if(stack.value > value) value = stack.value;
break;
case BuffValueSelector::MIN_MAGNITUDE:
if(abs(stack.value) < abs(value)) value = stack.value;
break;
case BuffValueSelector::MAX_MAGNITUDE:
default:
if(abs(stack.value) > abs(value)) value = stack.value;
}
}
return value;
}
bool Buff::isStale() {
return stacks.empty();
}
/* This will practically never do anything important, but it's here just in case */
void Buff::updateCallbacks(BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fOnCombatTick) {
if(!onUpdate) onUpdate = fOnUpdate;
if(!onCombatTick) onCombatTick = fOnCombatTick;
}
#pragma region Handlers
void Buffs::timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack) {
if(self.kind != EntityKind::PLAYER)
return; // not implemented
Player* plr = (Player*)self.getEntity();
if(plr == nullptr)
return; // sanity check
if(status == ETBU_DEL && !buff->isStale())
return; // no premature effect deletion
int cbf = plr->getCompositeCondition();
sTimeBuff payload{};
if(status == ETBU_ADD) {
payload.iValue = buff->getValue(BuffValueSelector::MAX_MAGNITUDE);
// we need to explicitly add the ECSB for this buff,
// in case this is the first stack in and the entry
// in the buff map doesn't yet exist
if(buff->id > 0) cbf |= CSB_FROM_ECSB(buff->id);
}
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = buff->id; // eCharStatusTimeBuffID
pkt.eTBU = status; // eTimeBuffUpdate
pkt.eTBT = (int)stack->buffStackClass;
pkt.iConditionBitFlag = cbf;
pkt.TimeBuff = payload;
self.sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
void Buffs::timeBuffTimeout(EntityRef self) {
if(self.kind != EntityKind::PLAYER && self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not a combatant
Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt); // send a buff timeout to other players
pkt.eCT = combatant->getCharType();
pkt.iID = combatant->getID();
pkt.iConditionBitFlag = combatant->getCompositeCondition();
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
#pragma endregion

View File

@@ -1,90 +0,0 @@
#pragma once
#include "core/Core.hpp"
#include "EntityRef.hpp"
#include <vector>
#include <functional>
/* forward declaration(s) */
class Buff;
template<class... Types>
using BuffCallback = std::function<void(EntityRef, Buff*, Types...)>;
#define CSB_FROM_ECSB(x) (1 << (x - 1))
enum class BuffClass {
NONE = ETBT_NONE,
NANO = ETBT_NANO,
GROUP_NANO = ETBT_GROUPNANO,
EGG = ETBT_SHINY,
ENVIRONMENT = ETBT_LANDEFFECT,
ITEM = ETBT_ITEM,
CASH_ITEM = ETBT_CASHITEM
};
enum class BuffValueSelector {
MAX_VALUE,
MIN_VALUE,
MAX_MAGNITUDE,
MIN_MAGNITUDE,
NET_TOTAL
};
struct BuffStack {
int durationTicks;
int value;
EntityRef source;
BuffClass buffStackClass;
};
class Buff {
private:
EntityRef self;
std::vector<BuffStack> stacks;
public:
int id;
/* called just after a stack is added or removed */
BuffCallback<int, BuffStack*> onUpdate;
/* called when the buff is combat-ticked */
BuffCallback<time_t> onCombatTick;
void tick(time_t);
void combatTick(time_t);
void clear();
void clear(BuffClass buffClass);
void addStack(BuffStack* stack);
/*
* Sometimes we need to determine if a buff
* is covered by a certain class, ex: nano
* vs. coco egg in the case of infection protection
*/
bool hasClass(BuffClass buffClass);
BuffClass maxClass();
int getValue(BuffValueSelector selector);
/*
* In general, a Buff object won't exist
* unless it has stacks. However, when
* popping stacks during iteration (onExpire),
* stacks will be empty for a brief moment
* when the last stack is popped.
*/
bool isStale();
void updateCallbacks(BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fonTick);
Buff(int iid, EntityRef pSelf, BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fOnCombatTick, BuffStack* firstStack)
: self(pSelf), id(iid), onUpdate(fOnUpdate), onCombatTick(fOnCombatTick) {
addStack(firstStack);
}
};
namespace Buffs {
void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack);
void timeBuffTimeout(EntityRef self);
}

View File

@@ -1,13 +1,10 @@
#include "BuiltinCommands.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Chat.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Chat.hpp"
#include "Nanos.hpp"
#include "Rand.hpp"
// helper function, not a packet handler
void BuiltinCommands::setSpecialState(CNSocket* sock, CNPacketData* data) {
@@ -72,31 +69,41 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
// Handle serverside value-changes
switch (setData->iSetValueType) {
case 1:
plr->HP = setData->iSetValue;
case CN_GM_SET_VALUE_TYPE__HP:
response.iSetValue = plr->HP = setData->iSetValue;
break;
case 2:
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY :
plr->batteryW = setData->iSetValue;
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
response.iSetValue = plr->batteryW;
break;
case 3:
case CN_GM_SET_VALUE_TYPE__NANO_BATTERY:
plr->batteryN = setData->iSetValue;
// caps
if (plr->batteryN > 9999)
plr->batteryN = 9999;
response.iSetValue = plr->batteryN;
break;
case 4:
case CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
response.iSetValue = plr->fusionmatter;
break;
case 5:
plr->money = setData->iSetValue;
case CN_GM_SET_VALUE_TYPE__CANDY:
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;
}
response.iPC_ID = setData->iPC_ID;
response.iSetValue = setData->iSetValue;
response.iSetValueType = setData->iSetValueType;
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);
@@ -250,17 +257,17 @@ static void teleportPlayer(CNSocket *sock, CNPacketData *data) {
uint64_t instance = plr->instanceID;
const int unstickRange = 400;
switch ((eCN_GM_TeleportType)req->eTeleportType) {
case eCN_GM_TeleportType::MyLocation:
switch (req->eTeleportType) {
case eCN_GM_TeleportMapType__MyLocation:
PlayerManager::sendPlayerTo(targetSock, plr->x, plr->y, plr->z, instance);
break;
case eCN_GM_TeleportType::MapXYZ:
case eCN_GM_TeleportMapType__MapXYZ:
instance = req->iToMap;
// fallthrough
case eCN_GM_TeleportType::XYZ:
case eCN_GM_TeleportMapType__XYZ:
PlayerManager::sendPlayerTo(targetSock, req->iToX, req->iToY, req->iToZ, instance);
break;
case eCN_GM_TeleportType::SomeoneLocation:
case eCN_GM_TeleportMapType__SomeoneLocation:
// player to teleport to
goalSock = PlayerManager::getSockFromAny(req->eGoalPCSearchBy, req->iGoalPC_ID, req->iGoalPC_UID,
AUTOU16TOU8(req->szGoalPC_FirstName), AUTOU16TOU8(req->szGoalPC_LastName));
@@ -272,7 +279,7 @@ static void teleportPlayer(CNSocket *sock, CNPacketData *data) {
PlayerManager::sendPlayerTo(targetSock, goalPlr->x, goalPlr->y, goalPlr->z, goalPlr->instanceID);
break;
case eCN_GM_TeleportType::Unstick:
case eCN_GM_TeleportMapType__Unstick:
targetPlr = PlayerManager::getPlayer(targetSock);
PlayerManager::sendPlayerTo(targetSock, targetPlr->x - unstickRange/2 + Rand::rand(unstickRange),

View File

@@ -1,9 +1,6 @@
#include "Chat.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Groups.hpp"
#include "CustomCommands.hpp"
#include <assert.h>
@@ -228,6 +225,10 @@ static void tradeChatHandler(CNSocket* sock, CNPacketData* data) {
static void groupChatHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
@@ -250,15 +251,16 @@ static void groupChatHandler(CNSocket* sock, CNPacketData* data) {
resp.iSendPCID = plr->iID;
resp.iEmoteCode = chat->iEmoteCode;
if (plr->group == nullptr)
sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
else
Groups::sendToGroup(plr->group, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
}
static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
std::string logLine = "[GroupMenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
@@ -273,9 +275,7 @@ static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) {
resp.iSendPCID = plr->iID;
resp.iEmoteCode = chat->iEmoteCode;
if (plr->group == nullptr)
sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
Groups::sendToGroup(plr->group, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
}
// we only allow plain ascii, at least for now

View File

@@ -2,10 +2,7 @@
#define CMD_PREFIX '/'
#include "core/Core.hpp"
#include <string>
#include <vector>
#include "servers/CNShardServer.hpp"
namespace Chat {
extern std::vector<std::string> dump;

View File

@@ -1,9 +1,9 @@
#include "Chunking.hpp"
#include "MobAI.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include <assert.h>
#include "settings.hpp"
#include "Combat.hpp"
#include "Eggs.hpp"
using namespace Chunking;
@@ -26,7 +26,7 @@ static void newChunk(ChunkPos pos) {
// add the chunk to the cache of all players and NPCs in the surrounding chunks
std::set<Chunk*> surroundings = getViewableChunks(pos);
for (Chunk* c : surroundings)
for (const EntityRef ref : c->entities)
for (const EntityRef& ref : c->entities)
ref.getEntity()->viewableChunks.insert(chunk);
}
@@ -41,24 +41,24 @@ static void deleteChunk(ChunkPos pos) {
// remove the chunk from the cache of all players and NPCs in the surrounding chunks
std::set<Chunk*> surroundings = getViewableChunks(pos);
for(Chunk* c : surroundings)
for (const EntityRef ref : c->entities)
for (const EntityRef& ref : c->entities)
ref.getEntity()->viewableChunks.erase(chunk);
chunks.erase(pos); // remove from map
delete chunk; // free from memory
}
void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef ref) {
void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef& ref) {
if (!chunkExists(chunkPos))
return; // shouldn't happen
chunks[chunkPos]->entities.insert(ref);
if (ref.kind == EntityKind::PLAYER)
if (ref.type == EntityType::PLAYER)
chunks[chunkPos]->nplayers++;
}
void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) {
void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef& ref) {
if (!chunkExists(chunkPos))
return; // do nothing if chunk doesn't even exist
@@ -66,7 +66,7 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) {
chunk->entities.erase(ref); // gone
if (ref.kind == EntityKind::PLAYER)
if (ref.type == EntityType::PLAYER)
chunks[chunkPos]->nplayers--;
assert(chunks[chunkPos]->nplayers >= 0);
@@ -75,13 +75,13 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) {
deleteChunk(chunkPos);
}
void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
Entity *ent = ref.getEntity();
bool alive = ent->isExtant();
bool alive = ent->isAlive();
// TODO: maybe optimize this, potentially using AROUND packets?
for (Chunk *chunk : chnks) {
for (const EntityRef otherRef : chunk->entities) {
for (const EntityRef& otherRef : chunk->entities) {
// skip oneself
if (ref == otherRef)
continue;
@@ -89,31 +89,31 @@ void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
Entity *other = otherRef.getEntity();
// notify all visible players of the existence of this Entity
if (alive && otherRef.kind == EntityKind::PLAYER) {
if (alive && otherRef.type == EntityType::PLAYER) {
ent->enterIntoViewOf(otherRef.sock);
}
// notify this *player* of the existence of all visible Entities
if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
if (ref.type == EntityType::PLAYER && other->isAlive()) {
other->enterIntoViewOf(ref.sock);
}
// for mobs, increment playersInView
if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER)
if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER)
((Mob*)ent)->playersInView++;
if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER)
if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER)
((Mob*)other)->playersInView++;
}
}
}
void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref) {
void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
Entity *ent = ref.getEntity();
bool alive = ent->isExtant();
bool alive = ent->isAlive();
// TODO: same as above
for (Chunk *chunk : chnks) {
for (const EntityRef otherRef : chunk->entities) {
for (const EntityRef& otherRef : chunk->entities) {
// skip oneself
if (ref == otherRef)
continue;
@@ -121,19 +121,19 @@ void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef re
Entity *other = otherRef.getEntity();
// notify all visible players of the departure of this Entity
if (alive && otherRef.kind == EntityKind::PLAYER) {
if (alive && otherRef.type == EntityType::PLAYER) {
ent->disappearFromViewOf(otherRef.sock);
}
// notify this *player* of the departure of all visible Entities
if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
if (ref.type == EntityType::PLAYER && other->isAlive()) {
other->disappearFromViewOf(ref.sock);
}
// for mobs, decrement playersInView
if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER)
if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER)
((Mob*)ent)->playersInView--;
if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER)
if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER)
((Mob*)other)->playersInView--;
}
}
@@ -154,8 +154,8 @@ static void emptyChunk(ChunkPos chunkPos) {
// unspawn all of the mobs/npcs
std::set refs(chunk->entities);
for (const EntityRef ref : refs) {
if (ref.kind == EntityKind::PLAYER)
for (const EntityRef& ref : refs) {
if (ref.type == EntityType::PLAYER)
assert(0);
// every call of this will check if the chunk is empty and delete it if so
@@ -163,7 +163,7 @@ static void emptyChunk(ChunkPos chunkPos) {
}
}
void Chunking::updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to) {
void Chunking::updateEntityChunk(const EntityRef& ref, ChunkPos from, ChunkPos to) {
Entity* ent = ref.getEntity();
// move to other chunk's player set
@@ -267,53 +267,54 @@ void Chunking::createInstance(uint64_t instanceID) {
std::cout << "Creating instance " << instanceID << std::endl;
for (ChunkPos &coords : templateChunks) {
for (const EntityRef ref : chunks[coords]->entities) {
if (ref.kind == EntityKind::PLAYER)
for (const EntityRef& ref : chunks[coords]->entities) {
if (ref.type == EntityType::PLAYER)
continue;
int npcID = ref.id;
BaseNPC* baseNPC = (BaseNPC*)ref.getEntity();
// make a copy of each NPC in the template chunks and put them in the new instance
if (baseNPC->kind == EntityKind::MOB) {
if (baseNPC->type == EntityType::MOB) {
if (((Mob*)baseNPC)->groupLeader != 0 && ((Mob*)baseNPC)->groupLeader != npcID)
continue; // follower; don't copy individually
Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->angle,
instanceID, baseNPC->type, NPCManager::NPCData[baseNPC->type], NPCManager::nextId--);
NPCManager::NPCs[newMob->id] = newMob;
Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle,
instanceID, baseNPC->appearanceData.iNPCType, NPCManager::NPCData[baseNPC->appearanceData.iNPCType], NPCManager::nextId--);
NPCManager::NPCs[newMob->appearanceData.iNPC_ID] = newMob;
// if in a group, copy over group members as well
if (((Mob*)baseNPC)->groupLeader != 0) {
newMob->groupLeader = newMob->id; // set leader ID for new leader
newMob->groupLeader = newMob->appearanceData.iNPC_ID; // set leader ID for new leader
Mob* mobData = (Mob*)baseNPC;
for (int i = 0; i < 4; i++) {
if (mobData->groupMember[i] != 0) {
int followerID = NPCManager::nextId--; // id for follower
BaseNPC* baseFollower = NPCManager::NPCs[mobData->groupMember[i]]; // follower from template
// new follower instance
Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->angle,
instanceID, baseFollower->type, NPCManager::NPCData[baseFollower->type], followerID);
Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->appearanceData.iAngle,
instanceID, baseFollower->appearanceData.iNPCType, NPCManager::NPCData[baseFollower->appearanceData.iNPCType], followerID);
// add follower to NPC maps
NPCManager::NPCs[followerID] = newMobFollower;
// set follower-specific properties
newMobFollower->groupLeader = newMob->id;
newMobFollower->groupLeader = newMob->appearanceData.iNPC_ID;
newMobFollower->offsetX = ((Mob*)baseFollower)->offsetX;
newMobFollower->offsetY = ((Mob*)baseFollower)->offsetY;
// add follower copy to leader copy
newMob->groupMember[i] = followerID;
NPCManager::updateNPCPosition(followerID, baseFollower->x, baseFollower->y, baseFollower->z,
instanceID, baseFollower->angle);
instanceID, baseFollower->appearanceData.iAngle);
}
}
}
NPCManager::updateNPCPosition(newMob->id, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->angle);
NPCManager::updateNPCPosition(newMob->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->appearanceData.iAngle);
} else {
BaseNPC* newNPC = new BaseNPC(baseNPC->angle, instanceID, baseNPC->type, NPCManager::nextId--);
NPCManager::NPCs[newNPC->id] = newNPC;
NPCManager::updateNPCPosition(newNPC->id, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->angle);
BaseNPC* newNPC = new BaseNPC(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle,
instanceID, baseNPC->appearanceData.iNPCType, NPCManager::nextId--);
NPCManager::NPCs[newNPC->appearanceData.iNPC_ID] = newNPC;
NPCManager::updateNPCPosition(newNPC->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->appearanceData.iAngle);
}
}
}

View File

@@ -1,10 +1,14 @@
#pragma once
#include "EntityRef.hpp"
#include "core/Core.hpp"
#include <utility>
#include <set>
#include <map>
#include <vector>
#include <tuple>
#include <algorithm>
struct EntityRef;
class Chunk {
public:
@@ -32,13 +36,13 @@ namespace Chunking {
extern const ChunkPos INVALID_CHUNK;
void updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to);
void updateEntityChunk(const EntityRef& ref, ChunkPos from, ChunkPos to);
void trackEntity(ChunkPos chunkPos, const EntityRef ref);
void untrackEntity(ChunkPos chunkPos, const EntityRef ref);
void trackEntity(ChunkPos chunkPos, const EntityRef& ref);
void untrackEntity(ChunkPos chunkPos, const EntityRef& ref);
void addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref);
void removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref);
void addEntityToChunks(std::set<Chunk*> chnks, const EntityRef& ref);
void removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef& ref);
bool chunkExists(ChunkPos chunk);
ChunkPos chunkPosAt(int posX, int posY, uint64_t instanceID);

View File

@@ -1,270 +1,22 @@
#include "Combat.hpp"
#include "servers/CNShardServer.hpp"
#include "Rand.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Nanos.hpp"
#include "NPCManager.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Groups.hpp"
#include "Transport.hpp"
#include "Racing.hpp"
#include "Abilities.hpp"
#include "Buffs.hpp"
#include "Rand.hpp"
#include <assert.h>
#include <iostream>
#include <functional>
using namespace Combat;
/// Player Id -> Bullet Id -> Bullet
std::map<int32_t, std::map<int8_t, Bullet>> Combat::Bullets;
#pragma region Player
bool Player::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) {
EntityRef self = PlayerManager::getSockFromID(iID);
if(!hasBuff(buffId)) {
buffs[buffId] = new Buff(buffId, self, onUpdate, onTick, stack);
return true;
}
buffs[buffId]->updateCallbacks(onUpdate, onTick);
buffs[buffId]->addStack(stack);
return false;
}
Buff* Player::getBuff(int buffId) {
if(hasBuff(buffId)) {
return buffs[buffId];
}
return nullptr;
}
void Player::removeBuff(int buffId) {
if(hasBuff(buffId)) {
buffs[buffId]->clear();
delete buffs[buffId];
buffs.erase(buffId);
}
}
void Player::removeBuff(int buffId, int buffClass) {
if(hasBuff(buffId)) {
buffs[buffId]->clear((BuffClass)buffClass);
if(buffs[buffId]->isStale()) {
delete buffs[buffId];
buffs.erase(buffId);
}
}
}
bool Player::hasBuff(int buffId) {
auto buff = buffs.find(buffId);
return buff != buffs.end() && !buff->second->isStale();
}
int Player::getCompositeCondition() {
int conditionBitFlag = 0;
for(auto buff : buffs) {
if(!buff.second->isStale() && buff.second->id > 0)
conditionBitFlag |= CSB_FROM_ECSB(buff.first);
}
return conditionBitFlag;
}
int Player::takeDamage(EntityRef src, int amt) {
int dmg = amt;
if(HP - dmg < 0) dmg = HP;
HP -= dmg;
return dmg;
}
int Player::heal(EntityRef src, int amt) {
int heal = amt;
if(HP + heal > getMaxHP()) heal = getMaxHP() - HP;
HP += heal;
return heal;
}
bool Player::isAlive() {
return HP > 0;
}
int Player::getCurrentHP() {
return HP;
}
int Player::getMaxHP() {
return PC_MAXHEALTH(level);
}
int Player::getLevel() {
return level;
}
std::vector<EntityRef> Player::getGroupMembers() {
std::vector<EntityRef> members;
if(group != nullptr)
members = group->members;
else
members.push_back(PlayerManager::getSockFromID(iID));
return members;
}
int32_t Player::getCharType() {
return 1; // eCharType (eCT_PC)
}
int32_t Player::getID() {
return iID;
}
EntityRef Player::getRef() {
return EntityRef(PlayerManager::getSockFromID(iID));
}
void Player::step(time_t currTime) {
CNSocket* sock = getRef().sock;
// nanos
for (int i = 0; i < 3; i++) {
if (activeNano != 0 && equippedNanos[i] == activeNano) { // tick active nano
sNano& nano = Nanos[activeNano];
int drainRate = 0;
if (Abilities::SkillTable.find(nano.iSkillID) != Abilities::SkillTable.end()) {
// nano has skill data
SkillData* skill = &Abilities::SkillTable[nano.iSkillID];
int boost = Nanos::getNanoBoost(this);
if (skill->drainType == SkillDrainType::PASSIVE)
drainRate = skill->batteryUse[boost * 3];
}
nano.iStamina -= 1 + drainRate / 5;
if (nano.iStamina <= 0)
Nanos::summonNano(sock, -1, true); // unsummon nano silently
} else if (Nanos[equippedNanos[i]].iStamina < 150) { // tick resting nano
sNano& nano = Nanos[equippedNanos[i]];
if (nano.iStamina < 150)
nano.iStamina += 1;
}
}
// buffs
for(auto buffEntry : buffs) {
buffEntry.second->combatTick(currTime);
}
}
#pragma endregion
#pragma region CombatNPC
bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) { /* stubbed */
return false;
}
Buff* CombatNPC::getBuff(int buffId) { /* stubbed */
return nullptr;
}
void CombatNPC::removeBuff(int buffId) { /* stubbed */ }
void CombatNPC::removeBuff(int buffId, int buffClass) { /* stubbed */ }
bool CombatNPC::hasBuff(int buffId) { /* stubbed */
return false;
}
int CombatNPC::getCompositeCondition() { /* stubbed */
return 0;
}
int CombatNPC::takeDamage(EntityRef src, int amt) {
int dmg = amt;
if(hp - dmg < 0) dmg = hp;
hp -= dmg;
if(hp <= 0) transition(AIState::DEAD, src);
return dmg;
}
int CombatNPC::heal(EntityRef src, int amt) {
int heal = amt;
if(hp + heal > getMaxHP()) heal = getMaxHP() - hp;
hp += heal;
return heal;
}
bool CombatNPC::isAlive() {
return hp > 0;
}
int CombatNPC::getCurrentHP() {
return hp;
}
int CombatNPC::getMaxHP() {
return maxHealth;
}
int CombatNPC::getLevel() {
return level;
}
std::vector<EntityRef> CombatNPC::getGroupMembers() {
std::vector<EntityRef> members;
if(group != nullptr)
members = group->members;
else
members.push_back(id);
return members;
}
int32_t CombatNPC::getCharType() {
if(kind == EntityKind::MOB)
return 4; // eCharType (eCT_MOB)
return 2; // eCharType (eCT_NPC)
}
int32_t CombatNPC::getID() {
return id;
}
EntityRef CombatNPC::getRef() {
return EntityRef(id);
}
void CombatNPC::step(time_t currTime) {
if(stateHandlers.find(state) != stateHandlers.end())
stateHandlers[state](this, currTime);
else {
std::cout << "[WARN] State " << (int)state << " has no handler; going inactive" << std::endl;
transition(AIState::INACTIVE, id);
}
}
void CombatNPC::transition(AIState newState, EntityRef src) {
state = newState;
if (transitionHandlers.find(newState) != transitionHandlers.end())
transitionHandlers[newState](this, src);
else {
std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl;
transition(AIState::INACTIVE, id);
}
/* TODO: fire any triggered events
for (NPCEvent& event : NPCManager::NPCEvents)
if (event.trigger == ON_KILLED && event.npcType == type)
event.handler(src, this);
*/
}
#pragma endregion
static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shouldCrit,
bool batteryBoost, int attackerStyle,
int defenderStyle, int difficulty) {
@@ -304,11 +56,40 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
return ret;
}
static bool checkRapidFire(CNSocket *sock, int targetCount) {
Player *plr = PlayerManager::getPlayer(sock);
int64_t currTime = getTime();
if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing
plr->lastShot = currTime;
// 3+ targets should never be possible
if (targetCount > 3)
plr->suspicionRating += 10001;
// kill the socket when the player is too suspicious
if (plr->suspicionRating > 10000) {
sock->kill();
CNShardServer::_killConnection(sock);
return true;
}
return false;
}
static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
auto pkt = (sP_CL2FE_REQ_PC_ATTACK_NPCs*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
auto targets = (int32_t*)data->trailers;
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt))
return;
/*
* IMPORTANT: This validates memory safety in addition to preventing
* ordinary cheating. If the client sends a very large number of trailing
@@ -333,7 +114,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
BaseNPC* npc = NPCManager::NPCs[targets[i]];
if (npc->kind != EntityKind::MOB) {
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] pcAttackNpcs: NPC is not a mob" << std::endl;
return;
}
@@ -356,11 +137,11 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
else
plr->batteryW = 0;
damage.first = mob->takeDamage(sock, damage.first);
damage.first = hitMob(sock, mob, damage.first);
respdata[i].iID = mob->id;
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->hp;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
}
@@ -377,7 +158,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
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);
INITVARPACKET(respbuf, sP_FE2CL_NPC_ATTACK_PCs, pkt, sAttackResult, atk);
@@ -387,7 +168,7 @@ void Combat::npcAttackPc(Mob *mob, time_t currTime) {
if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
plr->HP -= damage.first;
pkt->iNPC_ID = mob->id;
pkt->iNPC_ID = mob->appearanceData.iNPC_ID;
pkt->iPCCnt = 1;
atk->iID = plr->iID;
@@ -399,11 +180,52 @@ void Combat::npcAttackPc(Mob *mob, time_t currTime) {
PlayerManager::sendToViewable(mob->target, respbuf, P_FE2CL_NPC_ATTACK_PCs);
if (plr->HP <= 0) {
if (!MobAI::aggroCheck(mob, getTime()))
mob->transition(AIState::RETREAT, mob->target);
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!MobAI::aggroCheck(mob, currTime)) {
MobAI::clearDebuff(mob);
if (mob->groupLeader != 0)
MobAI::groupRetreat(mob);
}
}
}
int Combat::hitMob(CNSocket *sock, Mob *mob, int damage) {
// cannot kill mobs multiple times; cannot harm retreating mobs
if (mob->state != MobState::ROAMING && mob->state != MobState::COMBAT) {
return 0; // no damage
}
if (mob->skillStyle >= 0)
return 0; // don't hurt a mob casting corruption
if (mob->state == MobState::ROAMING) {
assert(mob->target == nullptr);
MobAI::enterCombat(sock, mob);
if (mob->groupLeader != 0)
MobAI::followToCombat(mob);
}
mob->appearanceData.iHP -= damage;
// wake up sleeping monster
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_MEZ) {
mob->appearanceData.iConditionBitFlag &= ~CSB_BIT_MEZ;
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = mob->appearanceData.iNPC_ID;
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
if (mob->appearanceData.iHP <= 0)
killMob(mob->target, mob);
return damage;
}
/*
* When a group of players is doing missions together, we want them to all get
* quest items at the same time, but we don't want the odds of quest item
@@ -411,16 +233,96 @@ void Combat::npcAttackPc(Mob *mob, time_t currTime) {
* single RNG roll per mission task, and every group member shares that same
* set of rolls.
*/
void Combat::genQItemRolls(std::vector<Player*> players, std::map<int, int>& rolls) {
for (int i = 0; i < players.size(); i++) {
static void genQItemRolls(Player *leader, std::map<int, int>& rolls) {
for (int i = 0; i < leader->groupCnt; i++) {
if (leader->groupIDs[i] == 0)
continue;
CNSocket *otherSock = PlayerManager::getSockFromID(leader->groupIDs[i]);
if (otherSock == nullptr)
continue;
Player *member = PlayerManager::getPlayer(otherSock);
Player* member = players[i];
for (int j = 0; j < ACTIVE_MISSION_COUNT; j++)
if (member->tasks[j] != 0)
rolls[member->tasks[j]] = Rand::rand();
}
}
void Combat::killMob(CNSocket *sock, Mob *mob) {
mob->state = MobState::DEAD;
mob->target = nullptr;
mob->appearanceData.iConditionBitFlag = 0;
mob->skillStyle = -1;
mob->unbuffTimes.clear();
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
if (sock != nullptr) {
Player* plr = PlayerManager::getPlayer(sock);
Items::DropRoll rolled;
Items::DropRoll eventRolled;
std::map<int, int> qitemRolls;
Player *leader = PlayerManager::getPlayerFromID(plr->iIDGroup);
assert(leader != nullptr); // should never happen
genQItemRolls(leader, qitemRolls);
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
Items::giveMobDrop(sock, mob, rolled, eventRolled);
Missions::mobKilled(sock, mob->appearanceData.iNPCType, qitemRolls);
} else {
for (int i = 0; i < leader->groupCnt; i++) {
CNSocket* sockTo = PlayerManager::getSockFromID(leader->groupIDs[i]);
if (sockTo == nullptr)
continue;
Player *otherPlr = PlayerManager::getPlayer(sockTo);
// only contribute to group members' kills if they're close enough
int dist = std::hypot(plr->x - otherPlr->x + 1, plr->y - otherPlr->y + 1);
if (dist > 5000)
continue;
Items::giveMobDrop(sockTo, mob, rolled, eventRolled);
Missions::mobKilled(sockTo, mob->appearanceData.iNPCType, qitemRolls);
}
}
}
// delay the despawn animation
mob->despawned = false;
// fire any triggered events
for (NPCEvent& event : NPCManager::NPCEvents)
if (event.trigger == ON_KILLED && event.npcType == mob->appearanceData.iNPCType)
event.handler(sock, mob);
auto it = Transport::NPCQueues.find(mob->appearanceData.iNPC_ID);
if (it == Transport::NPCQueues.end() || it->second.empty())
return;
// rewind or empty the movement queue
if (mob->staticPath) {
/*
* This is inelegant, but we wind forward in the path until we find the point that
* corresponds with the Mob's spawn point.
*
* IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever.
*/
auto& queue = it->second;
for (auto point = queue.front(); point.x != mob->spawnX || point.y != mob->spawnY; point = queue.front()) {
queue.pop();
queue.push(point);
}
} else {
Transport::NPCQueues.erase(mob->appearanceData.iNPC_ID);
}
}
static void combatBegin(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
@@ -443,27 +345,40 @@ static void combatEnd(CNSocket *sock, CNPacketData *data) {
plr->healCooldown = 4000;
}
static void dealGooDamage(CNSocket *sock) {
static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
if(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)
return; // ignore completely
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION) != (bool)pkt->iFlag)
plr->iConditionBitFlag ^= CSB_BIT_INFECTION;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt1);
pkt1.eCSTB = ECSB_INFECTION; // eCharStatusTimeBuffID
pkt1.eTBU = 1; // eTimeBuffUpdate
pkt1.eTBT = 0; // eTimeBuffType 1 means nano
pkt1.iConditionBitFlag = plr->iConditionBitFlag;
sock->sendPacket((void*)&pkt1, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
static void dealGooDamage(CNSocket *sock, int amount) {
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_DotDamage);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
Player *plr = PlayerManager::getPlayer(sock);
memset(respbuf, 0, resplen);
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
sSkillResult_DotDamage *dmg = (sSkillResult_DotDamage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
int amount = PC_MAXHEALTH(plr->level) * 3 / 20;
Buff* protectionBuff = plr->getBuff(ECSB_PROTECT_INFECTION);
if (protectionBuff != nullptr) {
if (plr->iConditionBitFlag & CSB_BIT_PROTECT_INFECTION) {
amount = -2; // -2 is the magic number for "Protected" to appear as the damage number
dmg->bProtected = 1;
// eggs allow protection without nanos
if (protectionBuff->maxClass() <= BuffClass::NANO && plr->activeNano != -1)
if (plr->activeNano != -1 && (plr->iSelfConditionBitFlag & CSB_BIT_PROTECT_INFECTION))
plr->Nanos[plr->activeNano].iStamina -= 3;
} else {
plr->HP -= amount;
@@ -487,39 +402,12 @@ static void dealGooDamage(CNSocket *sock) {
dmg->iID = plr->iID;
dmg->iDamage = amount;
dmg->iHP = plr->HP;
dmg->iConditionBitFlag = plr->getCompositeCondition();
dmg->iConditionBitFlag = plr->iConditionBitFlag;
sock->sendPacket((void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
}
static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
// infection debuff toggles as the client asks it to,
// so we add and remove a permanent debuff
if (pkt->iFlag && !plr->hasBuff(ECSB_INFECTION)) {
BuffStack infection = {
-1, // infinite
0, // no value
sock, // self-inflicted
BuffClass::ENVIRONMENT
};
plr->addBuff(ECSB_INFECTION,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
},
[](EntityRef self, Buff* buff, time_t currTime) {
if(self.kind == EntityKind::PLAYER)
dealGooDamage(self.sock);
},
&infection);
} else if(!pkt->iFlag && plr->hasBuff(ECSB_INFECTION)) {
plr->removeBuff(ECSB_INFECTION);
}
}
static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_REQ_PC_ATTACK_CHARs* pkt = (sP_CL2FE_REQ_PC_ATTACK_CHARs*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
@@ -529,12 +417,12 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
return;
// Unlike the attack mob packet, attacking players packet has an 8-byte trail (Instead of 4 bytes).
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(sGM_PVPTarget), data->size)) {
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(int32_t) * 2, data->size)) {
std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_CHARs packet size\n";
return;
}
sGM_PVPTarget* pktdata = (sGM_PVPTarget*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs));
int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs));
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC), pkt->iTargetCnt, sizeof(sAttackResult))) {
std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_CHARs_SUCC packet size\n";
@@ -553,19 +441,11 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
resp->iTargetCnt = pkt->iTargetCnt;
for (int i = 0; i < pkt->iTargetCnt; i++) {
ICombatant* target = nullptr;
std::pair<int, int> damage;
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
if (pktdata[i].eCT == 1) { // eCT == 1; attack player
if (pktdata[i*2+1] == 1) { // eCT == 1; attack player
Player *target = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == pktdata[i].iID) {
if (pair.second->iID == pktdata[i*2]) {
target = pair.second;
break;
}
@@ -577,41 +457,67 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
return;
}
damage = getDamage(damage.first, ((Player*)target)->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0);
std::pair<int,int> damage;
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
damage = getDamage(damage.first, target->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0);
if (plr->batteryW >= 6 + plr->level)
plr->batteryW -= 6 + plr->level;
else
plr->batteryW = 0;
target->HP -= damage.first;
respdata[i].eCT = pktdata[i*2+1];
respdata[i].iID = target->iID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = target->HP;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
} else { // eCT == 4; attack mob
if (NPCManager::NPCs.find(pktdata[i].iID) == NPCManager::NPCs.end()) {
if (NPCManager::NPCs.find(pktdata[i*2]) == NPCManager::NPCs.end()) {
// not sure how to best handle this
std::cout << "[WARN] pcAttackChars: NPC ID not found" << std::endl;
return;
}
BaseNPC* npc = NPCManager::NPCs[pktdata[i].iID];
if (npc->kind != EntityKind::MOB) {
BaseNPC* npc = NPCManager::NPCs[pktdata[i * 2]];
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] pcAttackChars: NPC is not a mob" << std::endl;
return;
}
Mob* mob = (Mob*)npc;
target = mob;
std::pair<int,int> damage;
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
if (plr->batteryW >= 6 + difficulty)
plr->batteryW -= 6 + difficulty;
else
plr->batteryW = 0;
damage.first = hitMob(sock, mob, damage.first);
respdata[i].eCT = pktdata[i*2+1];
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
}
if (plr->batteryW >= 6 + plr->level)
plr->batteryW -= 6 + plr->level;
else
plr->batteryW = 0;
damage.first = target->takeDamage(sock, damage.first);
respdata[i].eCT = pktdata[i].eCT;
respdata[i].iID = target->getID();
respdata[i].iDamage = damage.first;
respdata[i].iHP = target->getCurrentHP();
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
}
sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_CHARs_SUCC, resplen);
@@ -747,6 +653,21 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
return;
}
// rapid fire anti-cheat
int64_t currTime = getTime();
if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing
plr->lastShot = currTime;
if (plr->suspicionRating > 10000) { // kill the socket when the player is too suspicious
sock->kill();
CNShardServer::_killConnection(sock);
return;
}
/*
* initialize response struct
* rocket style hit doesn't work properly, so we're always sending this one
@@ -775,7 +696,7 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
}
BaseNPC* npc = NPCManager::NPCs[pktdata[i]];
if (npc->kind != EntityKind::MOB) {
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] projectileHit: NPC is not a mob" << std::endl;
return;
}
@@ -788,11 +709,11 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
damage.first = mob->takeDamage(sock, damage.first);
damage.first = hitMob(sock, mob, damage.first);
respdata[i].iID = mob->id;
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->hp;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHitFlag = damage.second;
}
@@ -805,9 +726,8 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
Bullets[plr->iID].erase(resp->iBulletID);
}
static void playerTick(CNServer *serv, time_t currTime) {
static time_t lastHealTime = 0;
static time_t lastCombatTIme = 0;
static void playerTick(CNServer *serv, int64_t currTime) {
static int64_t lastHealTime = 0;
for (auto& pair : PlayerManager::players) {
CNSocket *sock = pair.first;
@@ -815,13 +735,18 @@ static void playerTick(CNServer *serv, time_t currTime) {
bool transmit = false;
// group ticks
if (plr->group != nullptr)
Groups::groupTickInfo(sock);
if (plr->groupCnt > 1)
Groups::groupTickInfo(plr);
// do not tick dead players
if (plr->HP <= 0)
continue;
// fm patch/lake damage
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION)
&& !(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
dealGooDamage(sock, PC_MAXHEALTH(plr->level) * 3 / 20);
// heal
if (currTime - lastHealTime >= 4000 && !plr->inCombat && plr->HP < PC_MAXHEALTH(plr->level)) {
if (currTime - lastHealTime - plr->healCooldown >= 4000) {
@@ -833,21 +758,22 @@ static void playerTick(CNServer *serv, time_t currTime) {
plr->healCooldown -= 4000;
}
// combat tick
if(currTime - lastCombatTIme >= 2000) {
plr->step(currTime);
transmit = true;
}
for (int i = 0; i < 3; i++) {
if (plr->activeNano != 0 && plr->equippedNanos[i] == plr->activeNano) { // spend stamina
plr->Nanos[plr->activeNano].iStamina -= 1 + plr->nanoDrainRate / 5;
// nanos
if (plr->activeNano != 0) { // tick active nano
sNano* nano = plr->getActiveNano();
if (Abilities::SkillTable.find(nano->iSkillID) != Abilities::SkillTable.end()) {
// nano has skill data
SkillData* skill = &Abilities::SkillTable[nano->iSkillID];
if (skill->drainType == SkillDrainType::PASSIVE)
Nanos::applyNanoBuff(skill, plr);
// ^ composite condition calculation is separate from combat for responsiveness
if (plr->Nanos[plr->activeNano].iStamina <= 0)
Nanos::summonNano(sock, -1, true); // unsummon nano silently
transmit = true;
} else if (plr->Nanos[plr->equippedNanos[i]].iStamina < 150) { // regain stamina
sNano& nano = plr->Nanos[plr->equippedNanos[i]];
nano.iStamina += 1;
if (nano.iStamina > 150)
nano.iStamina = 150;
transmit = true;
}
}
@@ -863,19 +789,6 @@ static void playerTick(CNServer *serv, time_t currTime) {
PlayerManager::sendToViewable(sock, (void*)&dead, P_FE2CL_PC_SUDDEN_DEAD, sizeof(sP_FE2CL_PC_SUDDEN_DEAD));
}
// process buffsets
auto it = plr->buffs.begin();
while(it != plr->buffs.end()) {
Buff* buff = (*it).second;
buff->tick(currTime);
if(buff->isStale()) {
// garbage collect
it = plr->buffs.erase(it);
delete buff;
}
else it++;
}
if (transmit) {
INITSTRUCT(sP_FE2CL_REP_PC_TICK, pkt);
@@ -890,15 +803,13 @@ static void playerTick(CNServer *serv, time_t currTime) {
}
}
// if this was a heal/combat tick, update the counters outside of the loop
// if this was a heal tick, update the counter outside of the loop
if (currTime - lastHealTime >= 4000)
lastHealTime = currTime;
if(currTime - lastCombatTIme >= 2000)
lastCombatTIme = currTime;
}
void Combat::init() {
REGISTER_SHARD_TIMER(playerTick, MS_PER_PLAYER_TICK);
REGISTER_SHARD_TIMER(playerTick, 2000);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, pcAttackNpcs);

View File

@@ -1,10 +1,15 @@
#pragma once
#include "Player.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "NPC.hpp"
#include "MobAI.hpp"
#include "JSON.hpp"
#include <map>
#include <vector>
#include <unordered_map>
#include <queue>
struct Bullet {
int pointDamage;
@@ -18,6 +23,7 @@ namespace Combat {
void init();
void npcAttackPc(Mob *mob, time_t currTime);
void genQItemRolls(std::vector<Player*> players, std::map<int, int>& rolls);
void npcAttackPc(Mob *mob, int64_t currTime);
int hitMob(CNSocket *sock, Mob *mob, int damage);
void killMob(CNSocket *sock, Mob *mob);
}

View File

@@ -1,19 +1,18 @@
#include "CustomCommands.hpp"
#include "db/Database.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Chat.hpp"
#include "PlayerManager.hpp"
#include "TableData.hpp"
#include "NPCManager.hpp"
#include "MobAI.hpp"
#include "Missions.hpp"
#include "Eggs.hpp"
#include "MobAI.hpp"
#include "Items.hpp"
#include "Abilities.hpp"
#include "db/Database.hpp"
#include "Transport.hpp"
#include "Missions.hpp"
#include <sstream>
#include <iterator>
#include <math.h>
#include <limits.h>
typedef void (*CommandHandler)(std::string fullString, std::vector<std::string>& args, CNSocket* sock);
@@ -244,20 +243,20 @@ static void summonWCommand(std::string full, std::vector<std::string>& args, CNS
BaseNPC *npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true);
// update angle
npc->angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, plr->instanceID, npc->angle);
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, npc->appearanceData.iAngle);
// if we're in a lair, we need to spawn the NPC in both the private instance and the template
if (PLAYERID(plr->instanceID) != 0) {
npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true, true);
npc->angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, npc->instanceID, npc->angle);
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, npc->instanceID, npc->appearanceData.iAngle);
}
Chat::sendServerMessage(sock, "/summonW: placed mob with type: " + std::to_string(type) +
", id: " + std::to_string(npc->id));
TableData::RunningMobs[npc->id] = npc; // only record the one in the template
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningMobs[npc->appearanceData.iNPC_ID] = npc; // only record the one in the template
}
static void unsummonWCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -270,24 +269,24 @@ static void unsummonWCommand(std::string full, std::vector<std::string>& args, C
return;
}
if (TableData::RunningEggs.find(npc->id) != TableData::RunningEggs.end()) {
Chat::sendServerMessage(sock, "/unsummonW: removed egg with type: " + std::to_string(npc->type) +
", id: " + std::to_string(npc->id));
TableData::RunningEggs.erase(npc->id);
NPCManager::destroyNPC(npc->id);
if (TableData::RunningEggs.find(npc->appearanceData.iNPC_ID) != TableData::RunningEggs.end()) {
Chat::sendServerMessage(sock, "/unsummonW: removed egg with type: " + std::to_string(npc->appearanceData.iNPCType) +
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningEggs.erase(npc->appearanceData.iNPC_ID);
NPCManager::destroyNPC(npc->appearanceData.iNPC_ID);
return;
}
if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end()
&& TableData::RunningGroups.find(npc->id) == TableData::RunningGroups.end()) {
if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) == TableData::RunningMobs.end()
&& TableData::RunningGroups.find(npc->appearanceData.iNPC_ID) == TableData::RunningGroups.end()) {
Chat::sendServerMessage(sock, "/unsummonW: Closest NPC is not a gruntwork mob.");
return;
}
if (NPCManager::NPCs.find(npc->id) != NPCManager::NPCs.end() && NPCManager::NPCs[npc->id]->kind == EntityKind::MOB) {
if (NPCManager::NPCs.find(npc->appearanceData.iNPC_ID) != NPCManager::NPCs.end() && NPCManager::NPCs[npc->appearanceData.iNPC_ID]->type == EntityType::MOB) {
int leadId = ((Mob*)npc)->groupLeader;
if (leadId != 0) {
if (NPCManager::NPCs.find(leadId) == NPCManager::NPCs.end() || NPCManager::NPCs[leadId]->kind != EntityKind::MOB) {
if (NPCManager::NPCs.find(leadId) == NPCManager::NPCs.end() || NPCManager::NPCs[leadId]->type != EntityType::MOB) {
std::cout << "[WARN] unsummonW: leader not found!" << std::endl;
}
Mob* leadNpc = (Mob*)NPCManager::NPCs[leadId];
@@ -295,7 +294,7 @@ static void unsummonWCommand(std::string full, std::vector<std::string>& args, C
if (leadNpc->groupMember[i] == 0)
break;
if (NPCManager::NPCs.find(leadNpc->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadNpc->groupMember[i]]->kind != EntityKind::MOB) {
if (NPCManager::NPCs.find(leadNpc->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadNpc->groupMember[i]]->type != EntityType::MOB) {
std::cout << "[WARN] unsommonW: leader can't find a group member!" << std::endl;
continue;
}
@@ -309,12 +308,12 @@ static void unsummonWCommand(std::string full, std::vector<std::string>& args, C
}
}
Chat::sendServerMessage(sock, "/unsummonW: removed mob with type: " + std::to_string(npc->type) +
", id: " + std::to_string(npc->id));
Chat::sendServerMessage(sock, "/unsummonW: removed mob with type: " + std::to_string(npc->appearanceData.iNPCType) +
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningMobs.erase(npc->id);
TableData::RunningMobs.erase(npc->appearanceData.iNPC_ID);
NPCManager::destroyNPC(npc->id);
NPCManager::destroyNPC(npc->appearanceData.iNPC_ID);
}
static void toggleAiCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -325,11 +324,11 @@ static void toggleAiCommand(std::string full, std::vector<std::string>& args, CN
// return all mobs to their spawn points
for (auto& pair : NPCManager::NPCs) {
if (pair.second->kind != EntityKind::MOB)
if (pair.second->type != EntityType::MOB)
continue;
Mob* mob = (Mob*)pair.second;
mob->state = AIState::RETREAT;
mob->state = MobState::RETREAT;
mob->target = nullptr;
mob->nextMovement = getTime();
@@ -357,25 +356,21 @@ static void npcRotateCommand(std::string full, std::vector<std::string>& args, C
}
int angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->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
if (TableData::RunningMobs.find(npc->id) != TableData::RunningMobs.end()) {
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle);
bool isGruntworkNpc = true;
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for gruntwork NPC "
+ std::to_string(npc->id));
} else {
TableData::RunningNPCRotations[npc->id] = angle;
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for NPC "
+ std::to_string(npc->id));
// add a rotation entry to the gruntwork file, unless it's already a gruntwork NPC
if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) == TableData::RunningMobs.end()) {
TableData::RunningNPCRotations[npc->appearanceData.iNPC_ID] = angle;
isGruntworkNpc = false;
}
// update rotation clientside
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
pkt.NPCAppearanceData = npc->getAppearanceData();
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) +
" for " + (isGruntworkNpc ? "gruntwork " : "") + "NPC " + std::to_string(npc->appearanceData.iNPC_ID));
// 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) {
@@ -444,9 +439,9 @@ static void npcInstanceCommand(std::string full, std::vector<std::string>& args,
return;
}
Chat::sendServerMessage(sock, "[NPCI] Moving NPC with ID " + std::to_string(npc->id) + " to instance " + std::to_string(instance));
TableData::RunningNPCMapNumbers[npc->id] = instance;
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, instance, npc->angle);
Chat::sendServerMessage(sock, "[NPCI] Moving NPC with ID " + std::to_string(npc->appearanceData.iNPC_ID) + " to instance " + std::to_string(instance));
TableData::RunningNPCMapNumbers[npc->appearanceData.iNPC_ID] = instance;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, instance, npc->appearanceData.iAngle);
}
static void minfoCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -502,12 +497,9 @@ static void buffCommand(std::string full, std::vector<std::string>& args, CNSock
if (*tmp)
return;
if (Abilities::SkillTable.count(skillId) == 0) {
if (Eggs::eggBuffPlayer(sock, skillId, 0, duration)<0)
Chat::sendServerMessage(sock, "/buff: unknown skill Id");
return;
}
Eggs::eggBuffPlayer(sock, skillId, 0, duration);
}
static void eggCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -536,7 +528,7 @@ static void eggCommand(std::string full, std::vector<std::string>& args, CNSocke
int addX = 0; //-500.0f * sin(plr->angle / 180.0f * M_PI);
int addY = 0; //-500.0f * cos(plr->angle / 180.0f * M_PI);
Egg* egg = new Egg(plr->instanceID, eggType, id, false); // change last arg to true after gruntwork
Egg* egg = new Egg(plr->x + addX, plr->y + addY, plr->z, plr->instanceID, eggType, id, false); // change last arg to true after gruntwork
NPCManager::NPCs[id] = egg;
NPCManager::updateNPCPosition(id, plr->x + addX, plr->y + addY, plr->z, plr->instanceID, plr->angle);
@@ -613,40 +605,40 @@ static void summonGroupCommand(std::string full, std::vector<std::string>& args,
}
BaseNPC *npc = NPCManager::summonNPC(x, y, z, plr->instanceID, type, wCommand);
if (team == 2 && i > 0 && npc->kind == EntityKind::MOB) {
leadNpc->groupMember[i-1] = npc->id;
Mob* mob = (Mob*)NPCManager::NPCs[npc->id];
mob->groupLeader = leadNpc->id;
if (team == 2 && i > 0 && npc->type == EntityType::MOB) {
leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID;
Mob* mob = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID];
mob->groupLeader = leadNpc->appearanceData.iNPC_ID;
mob->offsetX = x - plr->x;
mob->offsetY = y - plr->y;
}
npc->angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, x, y, z, plr->instanceID, npc->angle);
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, x, y, z, plr->instanceID, npc->appearanceData.iAngle);
// if we're in a lair, we need to spawn the NPC in both the private instance and the template
if (PLAYERID(plr->instanceID) != 0) {
npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, wCommand, true);
if (team == 2 && i > 0 && npc->kind == EntityKind::MOB) {
leadNpc->groupMember[i-1] = npc->id;
Mob* mob = (Mob*)NPCManager::NPCs[npc->id];
mob->groupLeader = leadNpc->id;
if (team == 2 && i > 0 && npc->type == EntityType::MOB) {
leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID;
Mob* mob = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID];
mob->groupLeader = leadNpc->appearanceData.iNPC_ID;
mob->offsetX = x - plr->x;
mob->offsetY = y - plr->y;
}
npc->angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, x, y, z, plr->instanceID, npc->angle);
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, x, y, z, plr->instanceID, npc->appearanceData.iAngle);
}
Chat::sendServerMessage(sock, "/summonGroup(W): placed mob with type: " + std::to_string(type) +
", id: " + std::to_string(npc->id));
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
if (i == 0 && team == 2 && npc->kind == EntityKind::MOB) {
if (i == 0 && team == 2 && npc->type == EntityType::MOB) {
type = type2;
leadNpc = (Mob*)NPCManager::NPCs[npc->id];
leadNpc->groupLeader = leadNpc->id;
leadNpc = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID];
leadNpc->groupLeader = leadNpc->appearanceData.iNPC_ID;
}
}
@@ -658,7 +650,7 @@ static void summonGroupCommand(std::string full, std::vector<std::string>& args,
return;
}
TableData::RunningGroups[leadNpc->id] = leadNpc; // only record the leader
TableData::RunningGroups[leadNpc->appearanceData.iNPC_ID] = leadNpc; // only record the leader
}
static void flushCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -675,15 +667,15 @@ static void whoisCommand(std::string full, std::vector<std::string>& args, CNSoc
return;
}
Chat::sendServerMessage(sock, "[WHOIS] ID: " + std::to_string(npc->id));
Chat::sendServerMessage(sock, "[WHOIS] Type: " + std::to_string(npc->type));
Chat::sendServerMessage(sock, "[WHOIS] HP: " + std::to_string(npc->hp));
Chat::sendServerMessage(sock, "[WHOIS] CBF: " + std::to_string(npc->cbf));
Chat::sendServerMessage(sock, "[WHOIS] EntityType: " + std::to_string((int)npc->kind));
Chat::sendServerMessage(sock, "[WHOIS] ID: " + std::to_string(npc->appearanceData.iNPC_ID));
Chat::sendServerMessage(sock, "[WHOIS] Type: " + std::to_string(npc->appearanceData.iNPCType));
Chat::sendServerMessage(sock, "[WHOIS] HP: " + std::to_string(npc->appearanceData.iHP));
Chat::sendServerMessage(sock, "[WHOIS] CBF: " + std::to_string(npc->appearanceData.iConditionBitFlag));
Chat::sendServerMessage(sock, "[WHOIS] EntityType: " + std::to_string((int)npc->type));
Chat::sendServerMessage(sock, "[WHOIS] X: " + std::to_string(npc->x));
Chat::sendServerMessage(sock, "[WHOIS] Y: " + std::to_string(npc->y));
Chat::sendServerMessage(sock, "[WHOIS] Z: " + std::to_string(npc->z));
Chat::sendServerMessage(sock, "[WHOIS] Angle: " + std::to_string(npc->angle));
Chat::sendServerMessage(sock, "[WHOIS] Angle: " + std::to_string(npc->appearanceData.iAngle));
std::string chunkPosition = std::to_string(std::get<0>(npc->chunkPos)) + ", " + std::to_string(std::get<1>(npc->chunkPos)) + ", " + std::to_string(std::get<2>(npc->chunkPos));
Chat::sendServerMessage(sock, "[WHOIS] Chunk: {" + chunkPosition + "}");
Chat::sendServerMessage(sock, "[WHOIS] MapNum: " + std::to_string(MAPNUM(npc->instanceID)));
@@ -698,7 +690,7 @@ static void lairUnlockCommand(std::string full, std::vector<std::string>& args,
int lastDist = INT_MAX;
for (Chunk *chnk : Chunking::getViewableChunks(plr->chunkPos)) {
for (const EntityRef& ref : chnk->entities) {
if (ref.kind == EntityKind::PLAYER)
if (ref.type == EntityType::PLAYER)
continue;
BaseNPC* npc = (BaseNPC*)ref.getEntity();
@@ -709,7 +701,7 @@ static void lairUnlockCommand(std::string full, std::vector<std::string>& args,
continue;
for (auto it = NPCManager::Warps.begin(); it != NPCManager::Warps.end(); it++) {
if (it->second.npcID == npc->type) {
if (it->second.npcID == npc->appearanceData.iNPCType) {
taskID = it->second.limitTaskID;
missionID = Missions::Tasks[taskID]->task["m_iHMissionID"];
lastDist = dist;
@@ -981,17 +973,11 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// add first point at NPC's current location
std::vector<BaseNPC*> pathPoints;
BaseNPC* marker = new BaseNPC(0, plr->instanceID, 1386, NPCManager::nextId--);
// assign coords manually, since we aren't actually adding markers to the world
marker->x = npc->x;
marker->y = npc->y;
marker->z = npc->z;
BaseNPC* marker = new BaseNPC(npc->x, npc->y, npc->z, 0, plr->instanceID, 1386, NPCManager::nextId--);
pathPoints.push_back(marker);
// map from player
TableData::RunningNPCPaths[plr->iID] = std::make_pair(npc, pathPoints);
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is now following you");
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is now following you");
updatePathMarkers(sock);
return;
}
@@ -1008,12 +994,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// /path kf
if (args[1] == "kf") {
BaseNPC* marker = new BaseNPC(0, plr->instanceID, 1386, NPCManager::nextId--);
marker->x = npc->x;
marker->y = npc->y;
marker->z = npc->z;
BaseNPC* marker = new BaseNPC(npc->x, npc->y, npc->z, 0, plr->instanceID, 1386, NPCManager::nextId--);
entry->second.push_back(marker);
Chat::sendServerMessage(sock, "[PATH] Added keyframe");
updatePathMarkers(sock);
@@ -1023,8 +1004,8 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// /path here
if (args[1] == "here") {
// bring the NPC to where the player is standing
Transport::NPCQueues.erase(npc->id); // delete transport queue
NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, npc->instanceID, 0);
Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock);
Chat::sendServerMessage(sock, "[PATH] Come here");
@@ -1062,9 +1043,9 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
speed = speedArg;
}
// return NPC to home
Transport::NPCQueues.erase(npc->id); // delete transport queue
Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
BaseNPC* home = entry->second[0];
NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0);
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock);
@@ -1080,7 +1061,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
Transport::lerp(&keyframes, from, to, speed); // lerp from A to B
from = to; // update point A
}
Transport::NPCQueues[npc->id] = keyframes;
Transport::NPCQueues[npc->appearanceData.iNPC_ID] = keyframes;
entry->second.pop_back(); // remove temp end point
Chat::sendServerMessage(sock, "[PATH] Testing NPC path");
@@ -1090,9 +1071,9 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// /path cancel
if (args[1] == "cancel") {
// return NPC to home
Transport::NPCQueues.erase(npc->id); // delete transport queue
Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
BaseNPC* home = entry->second[0];
NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0);
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock);
// deallocate markers
@@ -1102,7 +1083,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
}
// unmap
TableData::RunningNPCPaths.erase(plr->iID);
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is no longer following you");
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is no longer following you");
return;
}
@@ -1130,9 +1111,9 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
}
// return NPC to home and set path to repeat
Transport::NPCQueues.erase(npc->id); // delete transport queue
Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
BaseNPC* home = entry->second[0];
NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0);
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock);
npc->loopingPath = true;
@@ -1149,7 +1130,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
Transport::lerp(&keyframes, from, to, speed); // lerp from A to B
from = to; // update point A
}
Transport::NPCQueues[npc->id] = keyframes;
Transport::NPCQueues[npc->appearanceData.iNPC_ID] = keyframes;
entry->second.pop_back(); // remove temp end point
// save to gruntwork
@@ -1176,7 +1157,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
finishedPath.isLoop = true;
finishedPath.speed = speed;
finishedPath.points = finalPoints;
finishedPath.targetIDs.push_back(npc->id);
finishedPath.targetIDs.push_back(npc->appearanceData.iNPC_ID);
TableData::FinishedNPCPaths.push_back(finishedPath);
@@ -1188,7 +1169,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// unmap
TableData::RunningNPCPaths.erase(plr->iID);
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is no longer following you");
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is no longer following you");
TableData::flush();
Chat::sendServerMessage(sock, "[PATH] Path saved to gruntwork");

View File

@@ -2,8 +2,6 @@
#include "core/Core.hpp"
#include <string>
namespace CustomCommands {
void init();

View File

@@ -1,78 +1,141 @@
#include "core/Core.hpp"
#include "Eggs.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Abilities.hpp"
#include "NPCManager.hpp"
#include "Entities.hpp"
#include "Items.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include "Groups.hpp"
#include <assert.h>
using namespace Eggs;
/// sock, CBFlag -> until
std::map<std::pair<CNSocket*, int32_t>, int64_t> Eggs::EggBuffs;
std::unordered_map<int, EggType> Eggs::EggTypes;
void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
int Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
// eggId might be 0 if the buff is made by the /buff command
EntityRef src = eggId == 0 ? sock : EntityRef(eggId);
if(Abilities::SkillTable.count(skillId) == 0) {
std::cout << "[WARN] egg " << eggId << " has skill ID " << skillId << " which doesn't exist" << std::endl;
return;
int bitFlag = Groups::getGroupFlags(otherPlr);
int CBFlag = Nanos::applyBuff(sock, skillId, 1, 3, bitFlag);
size_t resplen;
if (skillId == 183) {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Damage);
} else if (skillId == 150) {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Heal_HP);
} else {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Buff);
}
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
// we know it's only one trailing struct, so we can skip full validation
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
auto skillUse = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
if (skillId == 183) { // damage egg
auto skill = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iDamage = PC_MAXHEALTH(plr->level) * Nanos::SkillTable[skillId].powerIntensity[0] / 1000;
plr->HP -= skill->iDamage;
if (plr->HP < 0)
plr->HP = 0;
skill->iHP = plr->HP;
} else if (skillId == 150) { // heal egg
auto skill = (sSkillResult_Heal_HP*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iHealHP = PC_MAXHEALTH(plr->level) * Nanos::SkillTable[skillId].powerIntensity[0] / 1000;
plr->HP += skill->iHealHP;
if (plr->HP > PC_MAXHEALTH(plr->level))
plr->HP = PC_MAXHEALTH(plr->level);
skill->iHP = plr->HP;
} else { // regular buff egg
auto skill = (sSkillResult_Buff*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iConditionBitFlag = plr->iConditionBitFlag;
}
SkillData* skill = &Abilities::SkillTable[skillId];
if(skill->drainType == SkillDrainType::PASSIVE) {
// apply buff
if(skill->targetType != SkillTargetType::SELF) {
std::cout << "[WARN] weird skill type for egg " << eggId << " with skill " << skillId << ", should be " << (int)skill->targetType << std::endl;
}
skillUse->iNPC_ID = eggId;
skillUse->iSkillID = skillId;
skillUse->eST = Nanos::SkillTable[skillId].skillType;
skillUse->iTargetCnt = 1;
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
int value = skill->values[0][0];
BuffStack eggBuff = {
duration * 1000 / MS_PER_PLAYER_TICK,
value,
src,
BuffClass::EGG
};
plr->addBuff(timeBuffId,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
if(status == ETBU_DEL) Buffs::timeBuffTimeout(self);
},
[](EntityRef self, Buff* buff, time_t currTime) {
// no-op
},
&eggBuff);
}
sock->sendPacket((void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
// use skill
std::vector<ICombatant*> targets;
targets.push_back(dynamic_cast<ICombatant*>(plr));
Abilities::useNPCSkill(src, skillId, targets);
if (CBFlag == 0)
return -1;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, CBFlag);
// save the buff serverside;
// if you get the same buff again, new duration will override the previous one
int64_t until = getTime() + (int64_t)duration * 1000;
EggBuffs[key] = until;
return 0;
}
static void eggStep(CNServer* serv, time_t currTime) {
static void eggStep(CNServer* serv, int64_t currTime) {
// tick buffs
int64_t timeStamp = currTime;
auto it = EggBuffs.begin();
while (it != EggBuffs.end()) {
// check remaining time
if (it->second > timeStamp) {
it++;
} else { // if time reached 0
CNSocket* sock = it->first.first;
int32_t CBFlag = it->first.second;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
int groupFlags = Groups::getGroupFlags(otherPlr);
for (auto& pwr : Nanos::NanoPowers) {
if (pwr.bitFlag == CBFlag) { // pick the power with the right flag and unbuff
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp);
resp.eCSTB = pwr.timeBuffID;
resp.eTBU = 2;
resp.eTBT = 3; // for egg buffs
plr->iConditionBitFlag &= ~CBFlag;
resp.iConditionBitFlag = plr->iConditionBitFlag |= groupFlags | plr->iSelfConditionBitFlag;
sock->sendPacket(resp, P_FE2CL_PC_BUFF_UPDATE);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, resp2); // send a buff timeout to other players
resp2.eCT = 1;
resp2.iID = plr->iID;
resp2.iConditionBitFlag = plr->iConditionBitFlag;
PlayerManager::sendToViewable(sock, resp2, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT);
}
}
// remove buff from the map
it = EggBuffs.erase(it);
}
}
// check dead eggs and eggs in inactive chunks
for (auto npc : NPCManager::NPCs) {
if (npc.second->kind != EntityKind::EGG)
if (npc.second->type != EntityType::EGG)
continue;
auto egg = (Egg*)npc.second;
if (!egg->dead || !Chunking::inPopulatedChunks(&egg->viewableChunks))
continue;
if (egg->deadUntil <= currTime) {
if (egg->deadUntil <= timeStamp) {
// respawn it
egg->dead = false;
egg->deadUntil = 0;
egg->hp = 400;
egg->appearanceData.iHP = 400;
Chunking::addEntityToChunks(Chunking::getViewableChunks(egg->chunkPos), {npc.first});
}
@@ -100,7 +163,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
return;
}
auto egg = (Egg*)eggRef.getEntity();
if (egg->kind != EntityKind::EGG) {
if (egg->type != EntityType::EGG) {
std::cout << "[WARN] Player tried to open something other than an?!" << std::endl;
return;
}
@@ -117,7 +180,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
}
*/
int typeId = egg->type;
int typeId = egg->appearanceData.iNPCType;
if (EggTypes.find(typeId) == EggTypes.end()) {
std::cout << "[WARN] Egg Type " << typeId << " not found!" << std::endl;
return;
@@ -125,13 +188,16 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
EggType* type = &EggTypes[typeId];
// buff the player
if (type->effectId != 0)
eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration);
/*
* SHINY_PICKUP_SUCC is only causing a GUI effect in the client
* (buff icon pops up in the bottom of the screen)
* so we don't send it for non-effect
*/
if (type->effectId != 0) {
eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration);
INITSTRUCT(sP_FE2CL_REP_SHINY_PICKUP_SUCC, resp);
resp.iSkillID = type->effectId;
@@ -188,8 +254,8 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
else {
Chunking::removeEntityFromChunks(Chunking::getViewableChunks(egg->chunkPos), eggRef);
egg->dead = true;
egg->deadUntil = getTime() + (time_t)type->regen * 1000;
egg->hp = 0;
egg->deadUntil = getTime() + (int64_t)type->regen * 1000;
egg->appearanceData.iHP = 0;
}
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include "core/Core.hpp"
#include "Entities.hpp"
struct EggType {
int dropCrateId;
@@ -10,10 +11,12 @@ struct EggType {
};
namespace Eggs {
extern std::map<std::pair<CNSocket*, int32_t>, int64_t> EggBuffs;
extern std::unordered_map<int, EggType> EggTypes;
void init();
void eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration);
/// returns -1 on fail
int eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration);
void npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg);
}

View File

@@ -1,9 +1,9 @@
#include "Email.hpp"
#include "core/Core.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp"
#include "db/Database.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Chat.hpp"
@@ -93,7 +93,7 @@ static void emailReceiveItemSingle(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
if (pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT || pkt->iSlotNum < 1 || pkt->iSlotNum > 4)
if (pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT || pkt->iEmailItemSlot < 1 || pkt->iEmailItemSlot > 4)
return; // sanity check
// get email item from db and delete it
@@ -258,12 +258,12 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
resp.aItem[i] = attachment;
attachments.push_back(attachment.ItemInven);
attSlots.push_back(attachment.iSlotNum);
if (real->iOpt == item->iOpt) // delete item (if they attached the whole stack)
if (real->iOpt <= item->iOpt) // delete item (if they attached the whole stack)
*real = { 0, 0, 0, 0 };
else // otherwise, decrement the item
real->iOpt -= item->iOpt;
// update the slot
// HACK: update the slot
INITSTRUCT(sP_FE2CL_PC_ITEM_MOVE_SUCC, itemResp);
itemResp.iFromSlotNum = attachment.iSlotNum;
itemResp.iToSlotNum = attachment.iSlotNum;
@@ -291,7 +291,7 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
0 // DeleteTime (unimplemented)
};
if (!Database::sendEmail(&email, attachments)) {
if (!Database::sendEmail(&email, attachments, plr)) {
plr->money += cost; // give money back
// give items back
while (!attachments.empty()) {

View File

@@ -5,6 +5,6 @@
namespace Email {
extern std::vector<std::string> dump;
void init();
}

View File

@@ -1,15 +1,18 @@
#include "core/Core.hpp"
#include "Entities.hpp"
#include "NPCManager.hpp"
#include "Chunking.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Eggs.hpp"
#include "MobAI.hpp"
#include <assert.h>
#include <type_traits>
static_assert(std::is_standard_layout<EntityRef>::value);
static_assert(std::is_trivially_copyable<EntityRef>::value);
EntityRef::EntityRef(CNSocket *s) {
kind = EntityKind::PLAYER;
type = EntityType::PLAYER;
sock = s;
}
@@ -17,11 +20,11 @@ EntityRef::EntityRef(int32_t i) {
id = i;
assert(NPCManager::NPCs.find(id) != NPCManager::NPCs.end());
kind = NPCManager::NPCs[id]->kind;
type = NPCManager::NPCs[id]->type;
}
bool EntityRef::isValid() const {
if (kind == EntityKind::PLAYER)
if (type == EntityType::PLAYER)
return PlayerManager::players.find(sock) != PlayerManager::players.end();
return NPCManager::NPCs.find(id) != NPCManager::NPCs.end();
@@ -30,32 +33,21 @@ bool EntityRef::isValid() const {
Entity *EntityRef::getEntity() const {
assert(isValid());
if (kind == EntityKind::PLAYER)
if (type == EntityType::PLAYER)
return PlayerManager::getPlayer(sock);
return NPCManager::NPCs[id];
}
sNPCAppearanceData BaseNPC::getAppearanceData() {
sNPCAppearanceData data = {};
data.iAngle = angle;
data.iBarkerType = 0; // unused?
data.iConditionBitFlag = cbf;
data.iHP = hp;
data.iNPCType = type;
data.iNPC_ID = id;
data.iX = x;
data.iY = y;
data.iZ = z;
return data;
}
/*
* Entity coming into view.
*/
void BaseNPC::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
pkt.NPCAppearanceData = getAppearanceData();
pkt.NPCAppearanceData = appearanceData;
pkt.NPCAppearanceData.iX = x;
pkt.NPCAppearanceData.iY = y;
pkt.NPCAppearanceData.iZ = z;
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
}
@@ -64,7 +56,7 @@ void Bus::enterIntoViewOf(CNSocket *sock) {
// TODO: Potentially decouple this from BaseNPC?
pkt.AppearanceData = {
3, id, type,
3, appearanceData.iNPC_ID, appearanceData.iNPCType,
x, y, z
};
@@ -74,40 +66,28 @@ void Bus::enterIntoViewOf(CNSocket *sock) {
void Egg::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt);
// TODO: Potentially decouple this from BaseNPC?
pkt.ShinyAppearanceData = {
id, type, 0, // client doesn't care about map num
x, y, z
};
Eggs::npcDataToEggData(x, y, z, &appearanceData, &pkt.ShinyAppearanceData);
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER);
}
sNano* Player::getActiveNano() {
return &Nanos[activeNano];
}
sPCAppearanceData Player::getAppearanceData() {
sPCAppearanceData data = {};
data.iID = iID;
data.iHP = HP;
data.iLv = level;
data.iX = x;
data.iY = y;
data.iZ = z;
data.iAngle = angle;
data.PCStyle = PCStyle;
data.Nano = Nanos[activeNano];
data.iPCState = iPCState;
data.iSpecialState = iSpecialState;
memcpy(data.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT);
return data;
}
// TODO: this is less effiecient than it was, because of memset()
void Player::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_PC_NEW, pkt);
pkt.PCAppearanceData = getAppearanceData();
pkt.PCAppearanceData.iID = iID;
pkt.PCAppearanceData.iHP = HP;
pkt.PCAppearanceData.iLv = level;
pkt.PCAppearanceData.iX = x;
pkt.PCAppearanceData.iY = y;
pkt.PCAppearanceData.iZ = z;
pkt.PCAppearanceData.iAngle = angle;
pkt.PCAppearanceData.PCStyle = PCStyle;
pkt.PCAppearanceData.Nano = Nanos[activeNano];
pkt.PCAppearanceData.iPCState = iPCState;
pkt.PCAppearanceData.iSpecialState = iSpecialState;
memcpy(pkt.PCAppearanceData.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT);
sock->sendPacket(pkt, P_FE2CL_PC_NEW);
}
@@ -116,20 +96,20 @@ void Player::enterIntoViewOf(CNSocket *sock) {
*/
void BaseNPC::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
pkt.iNPC_ID = id;
pkt.iNPC_ID = appearanceData.iNPC_ID;
sock->sendPacket(pkt, P_FE2CL_NPC_EXIT);
}
void Bus::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_TRANSPORTATION_EXIT, pkt);
pkt.eTT = 3;
pkt.iT_ID = id;
pkt.iT_ID = appearanceData.iNPC_ID;
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_EXIT);
}
void Egg::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_EXIT, pkt);
pkt.iShinyID = id;
pkt.iShinyID = appearanceData.iNPC_ID;
sock->sendPacket(pkt, P_FE2CL_SHINY_EXIT);
}

View File

@@ -1,26 +1,23 @@
#pragma once
#include "core/Core.hpp"
#include "EntityRef.hpp"
#include "Buffs.hpp"
#include "Chunking.hpp"
#include "Groups.hpp"
#include <stdint.h>
#include <set>
#include <map>
#include <functional>
enum class AIState {
INACTIVE,
ROAMING,
COMBAT,
RETREAT,
DEAD
enum class EntityType : uint8_t {
INVALID,
PLAYER,
SIMPLE_NPC,
COMBAT_NPC,
MOB,
EGG,
BUS
};
struct Entity {
EntityKind kind = EntityKind::INVALID;
EntityType type = EntityType::INVALID;
int x = 0, y = 0, z = 0;
uint64_t instanceID = 0;
ChunkPos chunkPos = {};
@@ -29,38 +26,47 @@ struct Entity {
// destructor must be virtual, apparently
virtual ~Entity() {}
virtual bool isExtant() { return true; }
virtual bool isAlive() { return true; }
// stubs
virtual void enterIntoViewOf(CNSocket *sock) = 0;
virtual void disappearFromViewOf(CNSocket *sock) = 0;
};
/*
* Interfaces
*/
class ICombatant {
public:
ICombatant() {}
virtual ~ICombatant() {}
struct EntityRef {
EntityType type;
union {
CNSocket *sock;
int32_t id;
};
virtual bool addBuff(int, BuffCallback<int, BuffStack*>, BuffCallback<time_t>, BuffStack*) = 0;
virtual Buff* getBuff(int) = 0;
virtual void removeBuff(int) = 0;
virtual void removeBuff(int, int) = 0;
virtual bool hasBuff(int) = 0;
virtual int getCompositeCondition() = 0;
virtual int takeDamage(EntityRef, int) = 0;
virtual int heal(EntityRef, int) = 0;
virtual bool isAlive() = 0;
virtual int getCurrentHP() = 0;
virtual int getMaxHP() = 0;
virtual int getLevel() = 0;
virtual std::vector<EntityRef> getGroupMembers() = 0;
virtual int32_t getCharType() = 0;
virtual int32_t getID() = 0;
virtual EntityRef getRef() = 0;
virtual void step(time_t currTime) = 0;
EntityRef(CNSocket *s);
EntityRef(int32_t i);
bool isValid() const;
Entity *getEntity() const;
bool operator==(const EntityRef& other) const {
if (type != other.type)
return false;
if (type == EntityType::PLAYER)
return sock == other.sock;
return id == other.id;
}
// arbitrary ordering
bool operator<(const EntityRef& other) const {
if (type == other.type) {
if (type == EntityType::PLAYER)
return sock < other.sock;
else
return id < other.id;
}
return type < other.type;
}
};
/*
@@ -68,101 +74,75 @@ public:
*/
class BaseNPC : public Entity {
public:
int id;
int type;
int hp;
int angle;
int cbf;
sNPCAppearanceData appearanceData = {};
bool loopingPath = false;
BaseNPC(int _A, uint64_t iID, int t, int _id) {
kind = EntityKind::SIMPLE_NPC;
type = t;
hp = 400;
angle = _A;
cbf = 0;
id = _id;
BaseNPC(int _X, int _Y, int _Z, int angle, uint64_t iID, int t, int id) { // XXX
x = _X;
y = _Y;
z = _Z;
appearanceData.iNPCType = t;
appearanceData.iHP = 400;
appearanceData.iAngle = angle;
appearanceData.iConditionBitFlag = 0;
appearanceData.iBarkerType = 0;
appearanceData.iNPC_ID = id;
instanceID = iID;
};
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
sNPCAppearanceData getAppearanceData();
};
struct CombatNPC : public BaseNPC, public ICombatant {
struct CombatNPC : public BaseNPC {
int maxHealth = 0;
int spawnX = 0;
int spawnY = 0;
int spawnZ = 0;
int level = 0;
int speed = 300;
AIState state = AIState::INACTIVE;
Group* group = nullptr;
int playersInView = 0; // for optimizing away AI in empty chunks
std::map<AIState, void (*)(CombatNPC*, time_t)> stateHandlers;
std::map<AIState, void (*)(CombatNPC*, EntityRef)> transitionHandlers;
void (*_stepAI)(CombatNPC*, int64_t) = nullptr;
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP)
: BaseNPC(angle, iID, t, id), maxHealth(maxHP) {
spawnX = x;
spawnY = y;
spawnZ = z;
// XXX
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),
maxHealth(maxHP) {}
kind = EntityKind::COMBAT_NPC;
stateHandlers[AIState::INACTIVE] = {};
transitionHandlers[AIState::INACTIVE] = {};
virtual void stepAI(int64_t currTime) {
if (_stepAI != nullptr)
_stepAI(this, currTime);
}
virtual bool isExtant() override { return hp > 0; }
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
virtual Buff* getBuff(int buffId) override;
virtual void removeBuff(int buffId) override;
virtual void removeBuff(int buffId, int buffClass) override;
virtual bool hasBuff(int buffId) override;
virtual int getCompositeCondition() override;
virtual int takeDamage(EntityRef src, int amt) override;
virtual int heal(EntityRef src, int amt) override;
virtual bool isAlive() override;
virtual int getCurrentHP() override;
virtual int getMaxHP() override;
virtual int getLevel() override;
virtual std::vector<EntityRef> getGroupMembers() override;
virtual int32_t getCharType() override;
virtual int32_t getID() override;
virtual EntityRef getRef() override;
virtual void step(time_t currTime) override;
virtual void transition(AIState newState, EntityRef src);
virtual bool isAlive() override { return appearanceData.iHP > 0; }
};
// Mob is in MobAI.hpp, Player is in Player.hpp
// TODO: decouple from BaseNPC
struct Egg : public BaseNPC {
bool summoned = false;
bool dead = false;
time_t deadUntil;
int64_t deadUntil;
Egg(uint64_t iID, int t, int32_t id, bool summon)
: BaseNPC(0, iID, t, id) {
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) {
summoned = summon;
kind = EntityKind::EGG;
type = EntityType::EGG;
}
virtual bool isExtant() override { return !dead; }
virtual bool isAlive() override { return !dead; }
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
};
// TODO: decouple from BaseNPC
struct Bus : public BaseNPC {
Bus(int angle, uint64_t iID, int t, int id) :
BaseNPC(angle, iID, t, id) {
kind = EntityKind::BUS;
Bus(int x, int y, int z, int angle, uint64_t iID, int t, int id) :
BaseNPC(x, y, z, angle, iID, t, id) {
type = EntityType::BUS;
loopingPath = true;
}

View File

@@ -1,56 +0,0 @@
#pragma once
#include "core/Core.hpp"
/* forward declaration(s) */
struct Entity;
enum EntityKind {
INVALID,
PLAYER,
SIMPLE_NPC,
COMBAT_NPC,
MOB,
EGG,
BUS
};
struct EntityRef {
EntityKind kind;
union {
CNSocket *sock;
int32_t id;
};
EntityRef(CNSocket *s);
EntityRef(int32_t i);
bool isValid() const;
Entity *getEntity() const;
bool operator==(const EntityRef& other) const {
if (kind != other.kind)
return false;
if (kind == EntityKind::PLAYER)
return sock == other.sock;
return id == other.id;
}
bool operator!=(const EntityRef& other) const {
return !(*this == other);
}
// arbitrary ordering
bool operator<(const EntityRef& other) const {
if (kind == other.kind) {
if (kind == EntityKind::PLAYER)
return sock < other.sock;
else
return id < other.id;
}
return kind < other.kind;
}
};

View File

@@ -1,10 +1,13 @@
#include "Groups.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Entities.hpp"
#include "Groups.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include <iostream>
#include <chrono>
#include <algorithm>
#include <thread>
/*
* NOTE: Variadic response packets that list group members are technically
@@ -16,177 +19,22 @@
using namespace Groups;
Group::Group(EntityRef leader) {
addToGroup(this, leader);
}
static void attachGroupData(std::vector<EntityRef>& pcs, std::vector<EntityRef>& npcs, uint8_t* pivot) {
for(EntityRef pcRef : pcs) {
sPCGroupMemberInfo* info = (sPCGroupMemberInfo*)pivot;
Player* plr = PlayerManager::getPlayer(pcRef.sock);
info->iPC_ID = plr->iID;
info->iPCUID = plr->PCStyle.iPC_UID;
info->iNameCheck = plr->PCStyle.iNameCheck;
memcpy(info->szFirstName, plr->PCStyle.szFirstName, sizeof(plr->PCStyle.szFirstName));
memcpy(info->szLastName, plr->PCStyle.szLastName, sizeof(plr->PCStyle.szLastName));
info->iSpecialState = plr->iSpecialState;
info->iLv = plr->level;
info->iHP = plr->HP;
info->iMaxHP = PC_MAXHEALTH(plr->level);
// info->iMapType = 0;
// info->iMapNum = 0;
info->iX = plr->x;
info->iY = plr->y;
info->iZ = plr->z;
if(plr->activeNano > 0) {
info->Nano = *plr->getActiveNano();
info->bNano = true;
}
pivot = (uint8_t*)(info + 1);
}
for(EntityRef npcRef : npcs) {
sNPCGroupMemberInfo* info = (sNPCGroupMemberInfo*)pivot;
// probably should not assume that the combatant is an
// entity, but it works for now
BaseNPC* npc = (BaseNPC*)npcRef.getEntity();
info->iNPC_ID = npcRef.id;
info->iNPC_Type = npc->type;
info->iHP = npc->hp;
info->iX = npc->x;
info->iY = npc->y;
info->iZ = npc->z;
pivot = (uint8_t*)(info + 1);
}
}
void Groups::addToGroup(Group* group, EntityRef member) {
if (member.kind == EntityKind::PLAYER) {
Player* plr = PlayerManager::getPlayer(member.sock);
plr->group = group;
}
else if (member.kind == EntityKind::COMBAT_NPC) {
CombatNPC* npc = (CombatNPC*)member.getEntity();
npc->group = group;
}
else {
std::cout << "[WARN] Adding a weird entity type to a group" << std::endl;
}
group->members.push_back(member);
if(member.kind == EntityKind::PLAYER) {
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
size_t pcCount = pcs.size();
size_t npcCount = npcs.size();
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
sP_FE2CL_PC_GROUP_JOIN* pkt = (sP_FE2CL_PC_GROUP_JOIN*)respbuf;
pkt->iID_NewMember = PlayerManager::getPlayer(member.sock)->iID;
pkt->iMemberPCCnt = (int32_t)pcCount;
pkt->iMemberNPCCnt = (int32_t)npcCount;
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), pcCount, sizeof(sPCGroupMemberInfo))
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size" << std::endl;
} else {
uint8_t* pivot = (uint8_t*)(pkt + 1);
attachGroupData(pcs, npcs, pivot);
// PC_GROUP_JOIN_SUCC and PC_GROUP_JOIN carry identical payloads but have different IDs
// (and the client does care!) so we need to send one to the new member
// and the other to the rest
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo);
member.sock->sendPacket(respbuf, P_FE2CL_PC_GROUP_JOIN_SUCC, resplen);
sendToGroup(group, member, respbuf, P_FE2CL_PC_GROUP_JOIN, resplen);
}
}
}
bool Groups::removeFromGroup(Group* group, EntityRef member) {
if (member.kind == EntityKind::PLAYER) {
Player* plr = PlayerManager::getPlayer(member.sock);
plr->group = nullptr; // no dangling pointers here muahaahahah
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, leavePkt);
member.sock->sendPacket(leavePkt, P_FE2CL_PC_GROUP_LEAVE_SUCC);
}
else if (member.kind == EntityKind::COMBAT_NPC) {
CombatNPC* npc = (CombatNPC*)member.getEntity();
npc->group = nullptr;
}
else {
std::cout << "[WARN] Removing a weird entity type from a group" << std::endl;
}
auto it = std::find(group->members.begin(), group->members.end(), member);
if (it == group->members.end()) {
std::cout << "[WARN] Tried to remove a member that isn't in the group" << std::endl;
} else {
group->members.erase(it);
}
if(member.kind == EntityKind::PLAYER) {
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
size_t pcCount = pcs.size();
size_t npcCount = npcs.size();
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
sP_FE2CL_PC_GROUP_LEAVE* pkt = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf;
pkt->iID_LeaveMember = PlayerManager::getPlayer(member.sock)->iID;
pkt->iMemberPCCnt = (int32_t)pcCount;
pkt->iMemberNPCCnt = (int32_t)npcCount;
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), pcCount, sizeof(sPCGroupMemberInfo))
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size" << std::endl;
} else {
uint8_t* pivot = (uint8_t*)(pkt + 1);
attachGroupData(pcs, npcs, pivot);
sendToGroup(group, respbuf, P_FE2CL_PC_GROUP_LEAVE,
sizeof(sP_FE2CL_PC_GROUP_LEAVE) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo));
}
}
if (group->members.size() == 1) {
return removeFromGroup(group, group->members.back());
}
if (group->members.empty()) {
delete group; // cleanup memory
return true;
}
return false;
}
void Groups::disbandGroup(Group* group) {
// remove everyone from the group!!
bool done = false;
while(!done) {
EntityRef back = group->members.back();
done = removeFromGroup(group, back);
}
}
static void requestGroup(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_GROUP_INVITE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_To);
if (otherPlr == nullptr)
return;
otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup);
if (otherPlr == nullptr)
return;
// fail if the group is full or the other player is already in a group
if ((plr->group != nullptr && plr->group->filter(EntityKind::PLAYER).size() >= 4) || otherPlr->group != nullptr) {
if (plr->groupCnt >= 4 || otherPlr->iIDGroup != otherPlr->iID || otherPlr->groupCnt > 1) {
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_FAIL, resp);
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_FAIL, sizeof(sP_FE2CL_PC_GROUP_INVITE_FAIL));
return;
@@ -227,80 +75,255 @@ static void joinGroup(CNSocket* sock, CNPacketData* data) {
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_From);
if (otherPlr == nullptr)
return; // disconnect or something
return;
int size = otherPlr->group == nullptr ? 1 : otherPlr->group->filter(EntityKind::PLAYER).size();
otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup);
if (otherPlr == nullptr)
return;
// fail if the group is full or the other player is already in a group
if (plr->group != nullptr || size + 1 > 4) {
if (plr->groupCnt > 1 || plr->iIDGroup != plr->iID || otherPlr->groupCnt >= 4) {
INITSTRUCT(sP_FE2CL_PC_GROUP_JOIN_FAIL, resp);
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_JOIN_FAIL, sizeof(sP_FE2CL_PC_GROUP_JOIN_FAIL));
return;
}
if (otherPlr->group == nullptr) {
// create group
EntityRef otherPlrRef = PlayerManager::getSockFromID(recv->iID_From);
otherPlr->group = new Group(otherPlrRef);
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), otherPlr->groupCnt + 1, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
return;
}
addToGroup(otherPlr->group, sock);
plr->iIDGroup = otherPlr->iID;
otherPlr->groupCnt += 1;
otherPlr->groupIDs[otherPlr->groupCnt-1] = plr->iID;
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + otherPlr->groupCnt * sizeof(sPCGroupMemberInfo);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_JOIN *resp = (sP_FE2CL_PC_GROUP_JOIN*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_JOIN));
resp->iID_NewMember = plr->iID;
resp->iMemberPCCnt = otherPlr->groupCnt;
int bitFlag = getGroupFlags(otherPlr);
for (int i = 0; i < otherPlr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]);
if (varPlr == nullptr || sockTo == nullptr)
continue;
respdata[i].iPC_ID = varPlr->iID;
respdata[i].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i].iSpecialState = varPlr->iSpecialState;
respdata[i].iLv = varPlr->level;
respdata[i].iHP = varPlr->HP;
respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level);
//respdata[i].iMapType = 0;
//respdata[i].iMapNum = 0;
respdata[i].iX = varPlr->x;
respdata[i].iY = varPlr->y;
respdata[i].iZ = varPlr->z;
// client doesnt read nano data here
if (varPlr != plr) { // apply the new member's buffs to the group and the group's buffs to the new member
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 1, 1, bitFlag);
if (Nanos::SkillTable[plr->Nanos[plr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 1, 1, bitFlag);
}
}
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_JOIN, resplen);
}
static void leaveGroup(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
groupKick(plr->group, sock);
groupKickPlayer(plr);
}
void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) {
auto players = group->filter(EntityKind::PLAYER);
for (EntityRef ref : players) {
ref.sock->sendPacket(buf, type, size);
void Groups::sendToGroup(Player* plr, void* buf, uint32_t type, size_t size) {
for (int i = 0; i < plr->groupCnt; i++) {
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[i]);
if (sock == nullptr)
continue;
if (type == P_FE2CL_PC_GROUP_LEAVE_SUCC) {
Player* leavingPlr = PlayerManager::getPlayer(sock);
leavingPlr->iIDGroup = leavingPlr->iID;
}
sock->sendPacket(buf, type, size);
}
}
void Groups::sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size) {
auto players = group->filter(EntityKind::PLAYER);
for (EntityRef ref : players) {
if(ref != excluded) ref.sock->sendPacket(buf, type, size);
}
}
void Groups::groupTickInfo(CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
Group* group = plr->group;
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
size_t pcCount = pcs.size();
size_t npcCount = npcs.size();
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
sP_FE2CL_PC_GROUP_MEMBER_INFO* pkt = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf;
pkt->iID = plr->iID;
pkt->iMemberPCCnt = (int32_t)pcCount;
pkt->iMemberNPCCnt = (int32_t)npcCount;
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), pcCount, sizeof(sPCGroupMemberInfo))
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_MEMBER_INFO packet size" << std::endl;
} else {
uint8_t* pivot = (uint8_t*)(pkt + 1);
attachGroupData(pcs, npcs, pivot);
sock->sendPacket(respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO,
sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo));
}
}
void Groups::groupKick(Group* group, EntityRef ref) {
// if you are the group leader, destroy your own group and kick everybody
if (group->members[0] == ref) {
disbandGroup(group);
void Groups::groupTickInfo(Player* plr) {
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), plr->groupCnt, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
return;
}
removeFromGroup(group, ref);
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + plr->groupCnt * sizeof(sPCGroupMemberInfo);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_MEMBER_INFO *resp = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO));
resp->iID = plr->iID;
resp->iMemberPCCnt = plr->groupCnt;
for (int i = 0; i < plr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
if (varPlr == nullptr)
continue;
respdata[i].iPC_ID = varPlr->iID;
respdata[i].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i].iSpecialState = varPlr->iSpecialState;
respdata[i].iLv = varPlr->level;
respdata[i].iHP = varPlr->HP;
respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level);
//respdata[i].iMapType = 0;
//respdata[i].iMapNum = 0;
respdata[i].iX = varPlr->x;
respdata[i].iY = varPlr->y;
respdata[i].iZ = varPlr->z;
if (varPlr->activeNano > 0) {
respdata[i].bNano = 1;
respdata[i].Nano = varPlr->Nanos[varPlr->activeNano];
}
}
sendToGroup(plr, (void*)&respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO, resplen);
}
static void groupUnbuff(Player* plr) {
for (int i = 0; i < plr->groupCnt; i++) {
for (int n = 0; n < plr->groupCnt; n++) {
if (i == n)
continue;
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[n]);
Nanos::applyBuff(sock, otherPlr->Nanos[otherPlr->activeNano].iSkillID, 2, 1, 0);
}
}
}
void Groups::groupKickPlayer(Player* plr) {
// if you are the group leader, destroy your own group and kick everybody
if (plr->iID == plr->iIDGroup) {
groupUnbuff(plr);
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1);
sendToGroup(plr, (void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
plr->groupCnt = 1;
return;
}
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), otherPlr->groupCnt - 1, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size\n";
return;
}
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_LEAVE) + (otherPlr->groupCnt - 1) * sizeof(sPCGroupMemberInfo);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_LEAVE *resp = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_LEAVE));
resp->iID_LeaveMember = plr->iID;
resp->iMemberPCCnt = otherPlr->groupCnt - 1;
int bitFlag = getGroupFlags(otherPlr) & ~plr->iGroupConditionBitFlag;
int moveDown = 0;
CNSocket* sock = PlayerManager::getSockFromID(plr->iID);
if (sock == nullptr)
return;
for (int i = 0; i < otherPlr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]);
if (varPlr == nullptr || sockTo == nullptr)
continue;
if (moveDown == 1)
otherPlr->groupIDs[i-1] = otherPlr->groupIDs[i];
respdata[i-moveDown].iPC_ID = varPlr->iID;
respdata[i-moveDown].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i-moveDown].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i-moveDown].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i-moveDown].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i-moveDown].iSpecialState = varPlr->iSpecialState;
respdata[i-moveDown].iLv = varPlr->level;
respdata[i-moveDown].iHP = varPlr->HP;
respdata[i-moveDown].iMaxHP = PC_MAXHEALTH(varPlr->level);
// respdata[i-moveDown]].iMapType = 0;
// respdata[i-moveDown]].iMapNum = 0;
respdata[i-moveDown].iX = varPlr->x;
respdata[i-moveDown].iY = varPlr->y;
respdata[i-moveDown].iZ = varPlr->z;
// client doesnt read nano data here
if (varPlr == plr) {
moveDown = 1;
otherPlr->groupIDs[i] = 0;
} else { // remove the leaving member's buffs from the group and remove the group buffs from the leaving member.
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 2, 1, 0);
if (Nanos::SkillTable[plr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 2, 1, bitFlag);
}
}
plr->iIDGroup = plr->iID;
otherPlr->groupCnt -= 1;
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_LEAVE, resplen);
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1);
sock->sendPacket((void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
}
int Groups::getGroupFlags(Player* plr) {
int bitFlag = 0;
for (int i = 0; i < plr->groupCnt; i++) {
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
if (otherPlr == nullptr)
continue;
bitFlag |= otherPlr->iGroupConditionBitFlag;
}
return bitFlag;
}
void Groups::init() {

View File

@@ -1,32 +1,17 @@
#pragma once
#include "EntityRef.hpp"
#include "Player.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include <vector>
struct Group {
std::vector<EntityRef> members;
std::vector<EntityRef> filter(EntityKind kind) {
std::vector<EntityRef> filtered;
std::copy_if(members.begin(), members.end(), std::back_inserter(filtered), [kind](EntityRef e) {
return e.kind == kind;
});
return filtered;
}
Group(EntityRef leader);
};
#include <map>
#include <list>
namespace Groups {
void init();
void sendToGroup(Group* group, void* buf, uint32_t type, size_t size);
void sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size);
void groupTickInfo(CNSocket* sock);
void groupKick(Group* group, EntityRef ref);
void addToGroup(Group* group, EntityRef member);
bool removeFromGroup(Group* group, EntityRef member); // true iff group deleted
void disbandGroup(Group* group);
void sendToGroup(Player* plr, void* buf, uint32_t type, size_t size);
void groupTickInfo(Player* plr);
void groupKickPlayer(Player* plr);
int getGroupFlags(Player* plr);
}

View File

@@ -1,19 +1,16 @@
#include "Items.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "Items.hpp"
#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "NPCManager.hpp"
#include "Player.hpp"
#include "Abilities.hpp"
#include "Eggs.hpp"
#include "MobAI.hpp"
#include "Missions.hpp"
#include "Buffs.hpp"
#include "Eggs.hpp"
#include "Rand.hpp"
#include <string.h> // for memset()
#include <assert.h>
#include <numeric>
using namespace Items;
@@ -485,31 +482,27 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
resp->eST = EST_NANOSTIMPAK;
resp->iSkillID = 144;
int eCSB = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
int value1 = CSB_BIT_STIMPAKSLOT1 << request->iNanoSlot;
int value2 = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
respdata->eCT = 1;
respdata->iID = player->iID;
respdata->iConditionBitFlag = CSB_FROM_ECSB(eCSB);
respdata->iConditionBitFlag = value1;
int durationMilliseconds = Abilities::SkillTable[144].durationTime[0] * 100;
BuffStack gumballBuff = {
durationMilliseconds / MS_PER_PLAYER_TICK,
0,
sock,
BuffClass::CASH_ITEM // or BuffClass::ITEM?
};
player->addBuff(eCSB,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
},
[](EntityRef self, Buff* buff, time_t currTime) {
// no-op
},
&gumballBuff);
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = value2; // eCharStatusTimeBuffID
pkt.eTBU = 1; // eTimeBuffUpdate
pkt.eTBT = 1; // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = player->iConditionBitFlag |= value1;
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
sock->sendPacket((void*)&respbuf, P_FE2CL_REP_PC_ITEM_USE_SUCC, resplen);
// update inventory serverside
player->Inven[resp->iSlotNum] = resp->RemainItem;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, value1);
int64_t until = getTime() + (int64_t)Nanos::SkillTable[144].durationTime[0] * 100;
Eggs::EggBuffs[key] = until;
}
static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) {
@@ -762,7 +755,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) {
plr->money += miscDropType.taroAmount;
// money nano boost
if (plr->hasBuff(ECSB_REWARD_CASH)) {
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
@@ -777,7 +770,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
if (levelDifference > 0)
fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0;
// scavenger nano boost
if (plr->hasBuff(ECSB_REWARD_BLOB)) {
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
@@ -829,12 +822,12 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled) {
// sanity check
if (Items::MobToDropMap.find(mob->type) == Items::MobToDropMap.end()) {
std::cout << "[WARN] Mob ID " << mob->type << " has no drops assigned" << std::endl;
if (Items::MobToDropMap.find(mob->appearanceData.iNPCType) == Items::MobToDropMap.end()) {
std::cout << "[WARN] Mob ID " << mob->appearanceData.iNPCType << " has no drops assigned" << std::endl;
return;
}
// find mob drop id
int mobDropId = Items::MobToDropMap[mob->type];
int mobDropId = Items::MobToDropMap[mob->appearanceData.iNPCType];
giveSingleDrop(sock, mob, mobDropId, rolled);

View File

@@ -1,13 +1,9 @@
#pragma once
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "Rand.hpp"
#include "MobAI.hpp"
#include <vector>
#include <map>
#include "Rand.hpp"
struct CrocPotEntry {
int multStats, multLooks;

View File

@@ -1,10 +1,11 @@
#include "Missions.hpp"
#include "servers/CNShardServer.hpp"
#include "Missions.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Nanos.hpp"
#include "Items.hpp"
#include "Transport.hpp"
#include "string.h"
using namespace Missions;
@@ -163,14 +164,14 @@ static int giveMissionReward(CNSocket *sock, int task, int choice=0) {
// update player
plr->money += reward->money;
if (plr->hasBuff(ECSB_REWARD_CASH)) { // nano boost for taros
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) { // nano boost for taros
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
plr->money += reward->money * (5 + boost) / 25;
}
if (plr->hasBuff(ECSB_REWARD_BLOB)) { // nano boost for fm
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) { // nano boost for fm
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
@@ -366,15 +367,15 @@ static void taskStart(CNSocket* sock, CNPacketData* data) {
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
// if escort task, assign matching paths to all nearby NPCs
if (task["m_iHTaskType"] == (int)eTaskTypeProperty::EscortDefence) {
if (task["m_iHTaskType"] == 6) {
for (ChunkPos& chunkPos : Chunking::getChunksInMap(plr->instanceID)) { // check all NPCs in the instance
Chunk* chunk = Chunking::chunks[chunkPos];
for (EntityRef ref : chunk->entities) {
if (ref.kind != EntityKind::PLAYER) {
if (ref.type != EntityType::PLAYER) {
BaseNPC* npc = (BaseNPC*)ref.getEntity();
NPCPath* path = Transport::findApplicablePath(npc->id, npc->type, missionData->iTaskNum);
NPCPath* path = Transport::findApplicablePath(npc->appearanceData.iNPC_ID, npc->appearanceData.iNPCType, missionData->iTaskNum);
if (path != nullptr) {
Transport::constructPathNPC(npc->id, path);
Transport::constructPathNPC(npc->appearanceData.iNPC_ID, path);
return;
}
}
@@ -398,7 +399,7 @@ static void taskEnd(CNSocket* sock, CNPacketData* data) {
* once we comb over mission logic more throughly
*/
bool mobsAreKilled = false;
if (task->task["m_iHTaskType"] == (int)eTaskTypeProperty::Defeat) {
if (task->task["m_iHTaskType"] == 5) {
mobsAreKilled = true;
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == missionData->iTaskNum) {

View File

@@ -1,11 +1,9 @@
#pragma once
#include "core/Core.hpp"
#include "JSON.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include <map>
#include "JSON.hpp"
struct Reward {
int32_t id;

View File

@@ -1,15 +1,10 @@
#include "MobAI.hpp"
#include "Chunking.hpp"
#include "NPCManager.hpp"
#include "Entities.hpp"
#include "PlayerManager.hpp"
#include "Player.hpp"
#include "Racing.hpp"
#include "Transport.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include "Combat.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Abilities.hpp"
#include "Rand.hpp"
#include <cmath>
@@ -19,51 +14,7 @@ using namespace MobAI;
bool MobAI::simulateMobs = settings::SIMULATEMOBS;
void Mob::step(time_t currTime) {
if (playersInView < 0)
std::cout << "[WARN] Weird playerview value " << playersInView << std::endl;
// skip movement and combat if disabled or not in view
if ((!MobAI::simulateMobs || playersInView == 0) && state != AIState::DEAD
&& state != AIState::RETREAT)
return;
// call superclass step
CombatNPC::step(currTime);
}
int Mob::takeDamage(EntityRef src, int amt) {
// cannot kill mobs multiple times; cannot harm retreating mobs
if (state != AIState::ROAMING && state != AIState::COMBAT) {
return 0; // no damage
}
if (skillStyle >= 0)
return 0; // don't hurt a mob casting corruption
if (state == AIState::ROAMING) {
assert(target == nullptr && src.kind == EntityKind::PLAYER); // TODO: players only for now
transition(AIState::COMBAT, src);
if (groupLeader != 0)
MobAI::followToCombat(this);
}
// wake up sleeping monster
if (cbf & CSB_BIT_MEZ) {
cbf &= ~CSB_BIT_MEZ;
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = id;
pkt1.iConditionBitFlag = cbf;
NPCManager::sendToViewable(this, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
// call superclass takeDamage
return CombatNPC::takeDamage(src, amt);
}
static void roamingStep(Mob *mob, int64_t currTime);
/*
* Dynamic lerp; distinct from Transport::lerp(). This one doesn't care about height and
@@ -96,44 +47,44 @@ static std::pair<int,int> lerp(int x1, int y1, int x2, int y2, int speed) {
void MobAI::clearDebuff(Mob *mob) {
mob->skillStyle = -1;
mob->cbf = 0;
mob->appearanceData.iConditionBitFlag = 0;
mob->unbuffTimes.clear();
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = mob->id;
pkt1.iConditionBitFlag = mob->cbf;
pkt1.iID = mob->appearanceData.iNPC_ID;
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
void MobAI::followToCombat(Mob *mob) {
if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->kind == EntityKind::MOB) {
if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->type == EntityType::MOB) {
Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
for (int i = 0; i < 4; i++) {
if (leadMob->groupMember[i] == 0)
break;
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->kind != EntityKind::MOB) {
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->type != EntityType::MOB) {
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
continue;
}
Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]];
if (followerMob->state != AIState::ROAMING) // only roaming mobs should transition to combat
if (followerMob->state != MobState::ROAMING) // only roaming mobs should transition to combat
continue;
followerMob->transition(AIState::COMBAT, mob->target);
enterCombat(mob->target, followerMob);
}
if (leadMob->state != AIState::ROAMING)
if (leadMob->state != MobState::ROAMING)
return;
leadMob->transition(AIState::COMBAT, mob->target);
enterCombat(mob->target, leadMob);
}
}
void MobAI::groupRetreat(Mob *mob) {
if (NPCManager::NPCs.find(mob->groupLeader) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupLeader]->kind != EntityKind::MOB)
if (NPCManager::NPCs.find(mob->groupLeader) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupLeader]->type != EntityType::MOB)
return;
Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
@@ -141,25 +92,25 @@ void MobAI::groupRetreat(Mob *mob) {
if (leadMob->groupMember[i] == 0)
break;
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->kind != EntityKind::MOB) {
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->type != EntityType::MOB) {
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
continue;
}
Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]];
if (followerMob->state != AIState::COMBAT)
if (followerMob->state != MobState::COMBAT)
continue;
followerMob->target = nullptr;
followerMob->state = AIState::RETREAT;
followerMob->state = MobState::RETREAT;
clearDebuff(followerMob);
}
if (leadMob->state != AIState::COMBAT)
if (leadMob->state != MobState::COMBAT)
return;
leadMob->target = nullptr;
leadMob->state = AIState::RETREAT;
leadMob->state = MobState::RETREAT;
clearDebuff(leadMob);
}
@@ -168,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
* 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;
int closestDistance = INT_MAX;
@@ -176,7 +127,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
// TODO: support targetting other CombatNPCs
if (ref.kind != EntityKind::PLAYER)
if (ref.type != EntityType::PLAYER)
continue;
CNSocket *s = ref.sock;
@@ -187,7 +138,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
int mobRange = mob->sightRange;
if (plr->hasBuff(ECSB_UP_STEALTH)
if (plr->iConditionBitFlag & CSB_BIT_UP_STEALTH
|| Racing::EPRaces.find(s) != Racing::EPRaces.end())
mobRange /= 3;
@@ -196,7 +147,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
if (levelDifference > -10)
mobRange = levelDifference < 10 ? mobRange - (levelDifference * mobRange / 15) : mobRange / 3;
if (mob->state != AIState::ROAMING && plr->inCombat) // freshly out of aggro mobs
if (mob->state != MobState::ROAMING && plr->inCombat) // freshly out of aggro mobs
mobRange = mob->sightRange * 2; // should not be impacted by the above
if (plr->iSpecialState & (CN_SPECIAL_STATE_FLAG__INVISIBLE|CN_SPECIAL_STATE_FLAG__INVULNERABLE))
@@ -217,7 +168,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
if (closest != nullptr) {
// found closest player. engage.
mob->transition(AIState::COMBAT, closest);
enterCombat(closest, mob);
if (mob->groupLeader != 0)
followToCombat(mob);
@@ -245,7 +196,7 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
sP_FE2CL_NPC_SKILL_CORRUPTION_HIT *resp = (sP_FE2CL_NPC_SKILL_CORRUPTION_HIT*)respbuf;
sCAttackResult *respdata = (sCAttackResult*)(respbuf+sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT));
resp->iNPC_ID = mob->id;
resp->iNPC_ID = mob->appearanceData.iNPC_ID;
resp->iSkillID = skillID;
resp->iStyle = style;
resp->iValue1 = plr->x;
@@ -283,27 +234,26 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
int style2 = Nanos::nanoStyle(plr->activeNano);
if (style2 == -1) { // no nano
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
respdata[i].iHitFlag = 8;
respdata[i].iDamage = Nanos::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
} else if (style == style2) {
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
respdata[i].iHitFlag = 8; // tie
respdata[i].iDamage = 0;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
} else if (style - style2 == 1 || style2 - style == 2) {
respdata[i].iHitFlag = HF_BIT_STYLE_WIN;
respdata[i].iHitFlag = 4; // win
respdata[i].iDamage = 0;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45;
if (plr->Nanos[plr->activeNano].iStamina > 150)
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 150;
// fire damage power disguised as a corruption attack back at the enemy
// TODO ABILITIES
/*std::vector<int> targetData2 = {1, mob->id, 0, 0, 0};
for (auto& pwr : Abilities::Powers)
std::vector<int> targetData2 = {1, mob->appearanceData.iNPC_ID, 0, 0, 0};
for (auto& pwr : Nanos::NanoPowers)
if (pwr.skillType == EST_DAMAGE)
pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200);*/
pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200);
} else {
respdata[i].iHitFlag = HF_BIT_STYLE_LOSE;
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
respdata[i].iHitFlag = 16; // lose
respdata[i].iDamage = Nanos::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina -= 90;
if (plr->Nanos[plr->activeNano].iStamina < 0) {
respdata[i].bNanoDeactive = 1;
@@ -315,18 +265,23 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
plr->HP -= respdata[i].iDamage;
respdata[i].iHP = plr->HP;
respdata[i].iConditionBitFlag = plr->getCompositeCondition();
respdata[i].iConditionBitFlag = plr->iConditionBitFlag;
if (plr->HP <= 0) {
if (!MobAI::aggroCheck(mob, getTime()))
mob->transition(AIState::RETREAT, mob->target);
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!aggroCheck(mob, getTime())) {
clearDebuff(mob);
if (mob->groupLeader != 0)
groupRetreat(mob);
}
}
}
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
* first integer is the count
@@ -354,7 +309,7 @@ static void useAbilities(Mob *mob, time_t currTime) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
// TODO: see aggroCheck()
if (ref.kind != EntityKind::PLAYER)
if (ref.type != EntityType::PLAYER)
continue;
CNSocket *s= ref.sock;
@@ -364,7 +319,7 @@ static void useAbilities(Mob *mob, time_t currTime) {
continue;
int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y);
if (distance < Abilities::SkillTable[skillID].effectArea) {
if (distance < Nanos::SkillTable[skillID].effectArea) {
targetData[0] += 1;
targetData[targetData[0]] = plr->iID;
if (targetData[0] > 3) // make sure not to have more than 4
@@ -373,10 +328,9 @@ static void useAbilities(Mob *mob, time_t currTime) {
}
}
// TODO ABILITIES
/*for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);*/
for (auto& pwr : Combat::MobPowers)
if (pwr.skillType == Nanos::SkillTable[skillID].skillType)
pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]);
mob->skillStyle = -3; // eruption cooldown
mob->nextAttack = currTime + 1000;
return;
@@ -394,14 +348,13 @@ static void useAbilities(Mob *mob, time_t currTime) {
if (random < prob1) { // active skill hit
int skillID = (int)mob->data["m_iActiveSkill1"];
// TODO ABILITIES
//std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
//for (auto& pwr : Abilities::Powers)
// if (pwr.skillType == Abilities::SkillTable[skillID].skillType) {
// if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag))
// return; // prevent debuffing a player twice
// pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);
// }
std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
for (auto& pwr : Combat::MobPowers)
if (pwr.skillType == Nanos::SkillTable[skillID].skillType) {
if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag))
return; // prevent debuffing a player twice
pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]);
}
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
return;
}
@@ -409,7 +362,7 @@ static void useAbilities(Mob *mob, time_t currTime) {
if (random < prob1 + prob2) { // corruption windup
int skillID = (int)mob->data["m_iCorruptionType"];
INITSTRUCT(sP_FE2CL_NPC_SKILL_CORRUPTION_READY, pkt);
pkt.iNPC_ID = mob->id;
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
pkt.iSkillID = skillID;
pkt.iValue1 = plr->x;
pkt.iValue2 = plr->y;
@@ -428,7 +381,7 @@ static void useAbilities(Mob *mob, time_t currTime) {
if (random < prob1 + prob2 + prob3) { // eruption windup
int skillID = (int)mob->data["m_iMegaType"];
INITSTRUCT(sP_FE2CL_NPC_SKILL_READY, pkt);
pkt.iNPC_ID = mob->id;
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
pkt.iSkillID = skillID;
pkt.iValue1 = mob->hitX = plr->x;
pkt.iValue2 = mob->hitY = plr->y;
@@ -442,6 +395,27 @@ static void useAbilities(Mob *mob, time_t currTime) {
return;
}
void MobAI::enterCombat(CNSocket *sock, Mob *mob) {
mob->target = sock;
mob->state = MobState::COMBAT;
mob->nextMovement = getTime();
mob->nextAttack = 0;
mob->roamX = mob->x;
mob->roamY = mob->y;
mob->roamZ = mob->z;
int skillID = (int)mob->data["m_iPassiveBuff"]; // cast passive
std::vector<int> targetData = {1, mob->appearanceData.iNPC_ID, 0, 0, 0};
for (auto& pwr : Combat::MobPowers)
if (pwr.skillType == Nanos::SkillTable[skillID].skillType)
pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]);
for (NPCEvent& event : NPCManager::NPCEvents) // trigger an ON_COMBAT
if (event.trigger == ON_COMBAT && event.npcType == mob->appearanceData.iNPCType)
event.handler(sock, mob);
}
static void drainMobHP(Mob *mob, int amount) {
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
@@ -452,73 +426,64 @@ static void drainMobHP(Mob *mob, int amount) {
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
pkt->iID = mob->id;
pkt->iID = mob->appearanceData.iNPC_ID;
pkt->eCT = 4; // mob
pkt->iTB_ID = ECSB_BOUNDINGBALL;
drain->eCT = 4;
drain->iID = mob->id;
drain->iID = mob->appearanceData.iNPC_ID;
drain->iDamage = amount;
drain->iHP = mob->hp -= amount;
drain->iHP = mob->appearanceData.iHP -= amount;
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
if (mob->hp <= 0)
mob->transition(AIState::DEAD, mob->target);
if (mob->appearanceData.iHP <= 0)
Combat::killMob(mob->target, mob);
}
void MobAI::incNextMovement(Mob* mob, time_t currTime) {
if (currTime == 0)
currTime = getTime();
int delay = (int)mob->data["m_iDelayTime"] * 1000;
mob->nextMovement = currTime + delay / 2 + Rand::rand(delay / 2);
}
void MobAI::deadStep(CombatNPC* npc, time_t currTime) {
Mob* self = (Mob*)npc;
static void deadStep(Mob *mob, int64_t currTime) {
// despawn the mob after a short delay
if (self->killedTime != 0 && !self->despawned && currTime - self->killedTime > 2000) {
self->despawned = true;
if (mob->killedTime != 0 && !mob->despawned && currTime - mob->killedTime > 2000) {
mob->despawned = true;
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
pkt.iNPC_ID = self->id;
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
// if it was summoned, mark it for removal
if (self->summoned) {
if (mob->summoned) {
std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl;
NPCManager::queueNPCRemoval(self->id);
NPCManager::queueNPCRemoval(mob->appearanceData.iNPC_ID);
return;
}
// pre-set spawn coordinates if not marked for removal
self->x = self->spawnX;
self->y = self->spawnY;
self->z = self->spawnZ;
mob->x = mob->spawnX;
mob->y = mob->spawnY;
mob->z = mob->spawnZ;
}
// to guide their groupmates, group leaders still need to move despite being dead
if (self->groupLeader == self->id)
roamingStep(self, currTime);
if (mob->groupLeader == mob->appearanceData.iNPC_ID)
roamingStep(mob, currTime);
if (self->killedTime != 0 && currTime - self->killedTime < self->regenTime * 100)
if (mob->killedTime != 0 && currTime - mob->killedTime < mob->regenTime * 100)
return;
std::cout << "respawning mob " << self->id << " with HP = " << self->maxHealth << std::endl;
std::cout << "respawning mob " << mob->appearanceData.iNPC_ID << " with HP = " << mob->maxHealth << std::endl;
self->transition(AIState::ROAMING, self->id);
mob->appearanceData.iHP = mob->maxHealth;
mob->state = MobState::ROAMING;
// if mob is a group leader/follower, spawn where the group is.
if (self->groupLeader != 0) {
if (NPCManager::NPCs.find(self->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[self->groupLeader]->kind == EntityKind::MOB) {
Mob* leaderMob = (Mob*)NPCManager::NPCs[self->groupLeader];
self->x = leaderMob->x + self->offsetX;
self->y = leaderMob->y + self->offsetY;
self->z = leaderMob->z;
if (mob->groupLeader != 0) {
if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->type == EntityType::MOB) {
Mob* leaderMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
mob->x = leaderMob->x + mob->offsetX;
mob->y = leaderMob->y + mob->offsetY;
mob->z = leaderMob->z;
} else {
std::cout << "[WARN] deadStep: mob cannot find it's leader!" << std::endl;
}
@@ -526,157 +491,180 @@ void MobAI::deadStep(CombatNPC* npc, time_t currTime) {
INITSTRUCT(sP_FE2CL_NPC_NEW, pkt);
pkt.NPCAppearanceData = self->getAppearanceData();
pkt.NPCAppearanceData = mob->appearanceData;
pkt.NPCAppearanceData.iX = mob->x;
pkt.NPCAppearanceData.iY = mob->y;
pkt.NPCAppearanceData.iZ = mob->z;
// notify all nearby players
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
}
void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
Mob* self = (Mob*)npc;
assert(self->target != nullptr);
static void combatStep(Mob *mob, int64_t currTime) {
assert(mob->target != nullptr);
// lose aggro if the player lost connection
if (PlayerManager::players.find(self->target) == PlayerManager::players.end()) {
if (!MobAI::aggroCheck(self, getTime()))
self->transition(AIState::RETREAT, self->target);
if (PlayerManager::players.find(mob->target) == PlayerManager::players.end()) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!aggroCheck(mob, currTime)) {
clearDebuff(mob);
if (mob->groupLeader != 0)
groupRetreat(mob);
}
return;
}
Player *plr = PlayerManager::getPlayer(self->target);
Player *plr = PlayerManager::getPlayer(mob->target);
// lose aggro if the player became invulnerable or died
if (plr->HP <= 0
|| (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) {
if (!MobAI::aggroCheck(self, getTime()))
self->transition(AIState::RETREAT, self->target);
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!aggroCheck(mob, currTime)) {
clearDebuff(mob);
if (mob->groupLeader != 0)
groupRetreat(mob);
}
return;
}
// drain
if (self->skillStyle < 0 && (self->lastDrainTime == 0 || currTime - self->lastDrainTime >= 1000)
&& self->cbf & CSB_BIT_BOUNDINGBALL) {
drainMobHP(self, self->maxHealth / 20); // lose 5% every second
self->lastDrainTime = currTime;
if (mob->skillStyle < 0 && (mob->lastDrainTime == 0 || currTime - mob->lastDrainTime >= 1000)
&& mob->appearanceData.iConditionBitFlag & CSB_BIT_BOUNDINGBALL) {
drainMobHP(mob, mob->maxHealth / 20); // lose 5% every second
mob->lastDrainTime = currTime;
}
// if drain killed the mob, return early
if (self->hp <= 0)
if (mob->appearanceData.iHP <= 0)
return;
// unbuffing
std::unordered_map<int32_t, time_t>::iterator it = self->unbuffTimes.begin();
while (it != self->unbuffTimes.end()) {
std::unordered_map<int32_t, int64_t>::iterator it = mob->unbuffTimes.begin();
while (it != mob->unbuffTimes.end()) {
if (currTime >= it->second) {
self->cbf &= ~it->first;
mob->appearanceData.iConditionBitFlag &= ~it->first;
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = self->id;
pkt1.iConditionBitFlag = self->cbf;
NPCManager::sendToViewable(self, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
pkt1.iID = mob->appearanceData.iNPC_ID;
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
it = self->unbuffTimes.erase(it);
it = mob->unbuffTimes.erase(it);
} else {
it++;
}
}
// skip attack if stunned or asleep
if (self->cbf & (CSB_BIT_STUN|CSB_BIT_MEZ)) {
self->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
if (mob->appearanceData.iConditionBitFlag & (CSB_BIT_STUN|CSB_BIT_MEZ)) {
mob->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
return;
}
int distance = hypot(plr->x - self->x, plr->y - self->y);
int mobRange = (int)self->data["m_iAtkRange"] + (int)self->data["m_iRadius"];
int distance = hypot(plr->x - mob->x, plr->y - mob->y);
int mobRange = (int)mob->data["m_iAtkRange"] + (int)mob->data["m_iRadius"];
if (currTime >= self->nextAttack) {
if (self->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance.
useAbilities(self, currTime);
if (self->target == nullptr)
if (currTime >= mob->nextAttack) {
if (mob->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance.
useAbilities(mob, currTime);
if (mob->target == nullptr)
return;
}
int distanceToTravel = INT_MAX;
int speed = mob->speed;
// movement logic: move when out of range but don't move while casting a skill
if (distance > mobRange && self->skillStyle == -1) {
if (self->nextMovement != 0 && currTime < self->nextMovement)
if (distance > mobRange && mob->skillStyle == -1) {
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
return;
self->nextMovement = currTime + 400;
if (currTime >= self->nextAttack)
self->nextAttack = 0;
mob->nextMovement = currTime + 400;
if (currTime >= mob->nextAttack)
mob->nextAttack = 0;
// halve movement speed if snared
if (self->cbf & CSB_BIT_DN_MOVE_SPEED)
self->speed /= 2;
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED)
speed /= 2;
int targetX = plr->x;
int targetY = plr->y;
if (self->groupLeader != 0) {
targetX += self->offsetX*distance/(self->idleRange + 1);
targetY += self->offsetY*distance/(self->idleRange + 1);
if (mob->groupLeader != 0) {
targetX += mob->offsetX*distance/(mob->idleRange + 1);
targetY += mob->offsetY*distance/(mob->idleRange + 1);
}
distanceToTravel = std::min(distance-mobRange+1, self->speed*2/5);
auto targ = lerp(self->x, self->y, targetX, targetY, distanceToTravel);
if (distanceToTravel < self->speed*2/5 && currTime >= self->nextAttack)
self->nextAttack = 0;
distanceToTravel = std::min(distance-mobRange+1, speed*2/5);
auto targ = lerp(mob->x, mob->y, targetX, targetY, distanceToTravel);
if (distanceToTravel < speed*2/5 && currTime >= mob->nextAttack)
mob->nextAttack = 0;
NPCManager::updateNPCPosition(self->id, targ.first, targ.second, self->z, self->instanceID, self->angle);
NPCManager::updateNPCPosition(mob->appearanceData.iNPC_ID, targ.first, targ.second, mob->z, mob->instanceID, mob->appearanceData.iAngle);
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
pkt.iNPC_ID = self->id;
pkt.iSpeed = self->speed;
pkt.iToX = self->x = targ.first;
pkt.iToY = self->y = targ.second;
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
pkt.iSpeed = speed;
pkt.iToX = mob->x = targ.first;
pkt.iToY = mob->y = targ.second;
pkt.iToZ = plr->z;
pkt.iMoveStyle = 1;
// notify all nearby players
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
}
/* attack logic
* 2/5 represents 400 ms which is the time interval mobs use per movement logic step
* if the mob is one move interval away, we should just start attacking anyways.
*/
if (distance <= mobRange || distanceToTravel < self->speed*2/5) {
if (self->nextAttack == 0 || currTime >= self->nextAttack) {
self->nextAttack = currTime + (int)self->data["m_iDelayTime"] * 100;
Combat::npcAttackPc(self, currTime);
if (distance <= mobRange || distanceToTravel < speed*2/5) {
if (mob->nextAttack == 0 || currTime >= mob->nextAttack) {
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
Combat::npcAttackPc(mob, currTime);
}
}
// retreat if the player leaves combat range
int xyDistance = hypot(plr->x - self->roamX, plr->y - self->roamY);
distance = hypot(xyDistance, plr->z - self->roamZ);
if (distance >= self->data["m_iCombatRange"]) {
self->transition(AIState::RETREAT, self->target);
int xyDistance = hypot(plr->x - mob->roamX, plr->y - mob->roamY);
distance = hypot(xyDistance, plr->z - mob->roamZ);
if (distance >= mob->data["m_iCombatRange"]) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
clearDebuff(mob);
if (mob->groupLeader != 0)
groupRetreat(mob);
}
}
void MobAI::roamingStep(CombatNPC* npc, time_t currTime) {
Mob* self = (Mob*)npc;
void MobAI::incNextMovement(Mob *mob, int64_t currTime) {
if (currTime == 0)
currTime = getTime();
int delay = (int)mob->data["m_iDelayTime"] * 1000;
mob->nextMovement = currTime + delay/2 + Rand::rand(delay/2);
}
static void roamingStep(Mob *mob, int64_t currTime) {
/*
* 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).
* In the case of group leaders, this step will be called by dead mobs, so disable attack.
*/
if (self->state != AIState::DEAD && (self->nextAttack == 0 || currTime >= self->nextAttack)) {
self->nextAttack = currTime + 500;
if (aggroCheck(self, currTime))
if (mob->state != MobState::DEAD && (mob->nextAttack == 0 || currTime >= mob->nextAttack)) {
mob->nextAttack = currTime + 500;
if (aggroCheck(mob, currTime))
return;
}
// no random roaming if the mob already has a set path
if (self->staticPath)
if (mob->staticPath)
return;
if (self->groupLeader != 0 && self->groupLeader != self->id) // don't roam by yourself without group leader
if (mob->groupLeader != 0 && mob->groupLeader != mob->appearanceData.iNPC_ID) // don't roam by yourself without group leader
return;
/*
@@ -684,216 +672,142 @@ void MobAI::roamingStep(CombatNPC* npc, time_t currTime) {
* Transport::stepNPCPathing() (which ticks at a higher frequency than nextMovement),
* so we don't have to check if there's already entries in the queue since we know there won't be.
*/
if (self->nextMovement != 0 && currTime < self->nextMovement)
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
return;
incNextMovement(self, currTime);
incNextMovement(mob, currTime);
int xStart = self->spawnX - self->idleRange/2;
int yStart = self->spawnY - self->idleRange/2;
int xStart = mob->spawnX - mob->idleRange/2;
int yStart = mob->spawnY - mob->idleRange/2;
int speed = mob->speed;
// some mobs don't move (and we mustn't divide/modulus by zero)
if (self->idleRange == 0 || self->speed == 0)
if (mob->idleRange == 0 || speed == 0)
return;
int farX, farY, distance;
int minDistance = self->idleRange / 2;
int minDistance = mob->idleRange / 2;
// pick a random destination
farX = xStart + Rand::rand(self->idleRange);
farY = yStart + Rand::rand(self->idleRange);
farX = xStart + Rand::rand(mob->idleRange);
farY = yStart + Rand::rand(mob->idleRange);
distance = std::abs(std::max(farX - self->x, farY - self->y));
distance = std::abs(std::max(farX - mob->x, farY - mob->y));
if (distance == 0)
distance += 1; // hack to avoid FPE
// if it's too short a walk, go further in that direction
farX = self->x + (farX - self->x) * minDistance / distance;
farY = self->y + (farY - self->y) * minDistance / distance;
farX = mob->x + (farX - mob->x) * minDistance / distance;
farY = mob->y + (farY - mob->y) * minDistance / distance;
// but don't got out of bounds
farX = std::clamp(farX, xStart, xStart + self->idleRange);
farY = std::clamp(farY, yStart, yStart + self->idleRange);
farX = std::clamp(farX, xStart, xStart + mob->idleRange);
farY = std::clamp(farY, yStart, yStart + mob->idleRange);
// halve movement speed if snared
if (self->cbf & CSB_BIT_DN_MOVE_SPEED)
self->speed /= 2;
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED)
speed /= 2;
std::queue<Vec3> queue;
Vec3 from = { self->x, self->y, self->z };
Vec3 to = { farX, farY, self->z };
Vec3 from = { mob->x, mob->y, mob->z };
Vec3 to = { farX, farY, mob->z };
// add a route to the queue; to be processed in Transport::stepNPCPathing()
Transport::lerp(&queue, from, to, self->speed);
Transport::NPCQueues[self->id] = queue;
Transport::lerp(&queue, from, to, speed);
Transport::NPCQueues[mob->appearanceData.iNPC_ID] = queue;
if (self->groupLeader != 0 && self->groupLeader == self->id) {
if (mob->groupLeader != 0 && mob->groupLeader == mob->appearanceData.iNPC_ID) {
// make followers follow this npc.
for (int i = 0; i < 4; i++) {
if (self->groupMember[i] == 0)
if (mob->groupMember[i] == 0)
break;
if (NPCManager::NPCs.find(self->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[self->groupMember[i]]->kind != EntityKind::MOB) {
if (NPCManager::NPCs.find(mob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupMember[i]]->type != EntityType::MOB) {
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
continue;
}
std::queue<Vec3> queue2;
Mob* followerMob = (Mob*)NPCManager::NPCs[self->groupMember[i]];
Mob* followerMob = (Mob*)NPCManager::NPCs[mob->groupMember[i]];
from = { followerMob->x, followerMob->y, followerMob->z };
to = { farX + followerMob->offsetX, farY + followerMob->offsetY, followerMob->z };
Transport::lerp(&queue2, from, to, self->speed);
Transport::NPCQueues[followerMob->id] = queue2;
Transport::lerp(&queue2, from, to, speed);
Transport::NPCQueues[followerMob->appearanceData.iNPC_ID] = queue2;
}
}
}
void MobAI::retreatStep(CombatNPC* npc, time_t currTime) {
Mob* self = (Mob*)npc;
if (self->nextMovement != 0 && currTime < self->nextMovement)
static void retreatStep(Mob *mob, int64_t currTime) {
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
return;
self->nextMovement = currTime + 400;
mob->nextMovement = currTime + 400;
// distance between spawn point and current location
int distance = hypot(self->x - self->roamX, self->y - self->roamY);
int distance = hypot(mob->x - mob->roamX, mob->y - mob->roamY);
//if (distance > mob->data["m_iIdleRange"]) {
if (distance > 10) {
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
auto targ = lerp(self->x, self->y, self->roamX, self->roamY, (int)self->speed*4/5);
auto targ = lerp(mob->x, mob->y, mob->roamX, mob->roamY, (int)mob->speed*4/5);
pkt.iNPC_ID = self->id;
pkt.iSpeed = (int)self->speed * 2;
pkt.iToX = self->x = targ.first;
pkt.iToY = self->y = targ.second;
pkt.iToZ = self->z = self->spawnZ;
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
pkt.iSpeed = (int)mob->speed * 2;
pkt.iToX = mob->x = targ.first;
pkt.iToY = mob->y = targ.second;
pkt.iToZ = mob->z = mob->spawnZ;
pkt.iMoveStyle = 1;
// notify all nearby players
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
}
// if we got there
//if (distance <= mob->data["m_iIdleRange"]) {
if (distance <= 10) { // retreat back to the spawn point
self->transition(AIState::ROAMING, self->id);
mob->state = MobState::ROAMING;
mob->appearanceData.iHP = mob->maxHealth;
mob->killedTime = 0;
mob->nextAttack = 0;
mob->appearanceData.iConditionBitFlag = 0;
// cast a return home heal spell, this is the right way(tm)
std::vector<int> targetData = {1, 0, 0, 0, 0};
for (auto& pwr : Combat::MobPowers)
if (pwr.skillType == Nanos::SkillTable[110].skillType)
pwr.handle(mob, targetData, 110, Nanos::SkillTable[110].durationTime[0], Nanos::SkillTable[110].powerIntensity[0]);
// clear outlying debuffs
clearDebuff(mob);
}
}
void MobAI::onRoamStart(CombatNPC* npc, EntityRef src) {
Mob* self = (Mob*)npc;
void MobAI::step(CombatNPC *npc, int64_t currTime) {
assert(npc->type == EntityType::MOB);
auto mob = (Mob*)npc;
self->hp = self->maxHealth;
self->killedTime = 0;
self->nextAttack = 0;
self->cbf = 0;
if (mob->playersInView < 0)
std::cout << "[WARN] Weird playerview value " << mob->playersInView << std::endl;
// cast a return home heal spell, this is the right way(tm)
// TODO ABILITIES
/*std::vector<int> targetData = { 1, 0, 0, 0, 0 };
for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[110].skillType)
pwr.handle(self->id, targetData, 110, Abilities::SkillTable[110].durationTime[0], Abilities::SkillTable[110].powerIntensity[0]);*/
// clear outlying debuffs
clearDebuff(self);
}
void MobAI::onCombatStart(CombatNPC* npc, EntityRef src) {
Mob* self = (Mob*)npc;
assert(src.kind == EntityKind::PLAYER);
self->target = src.sock;
self->nextMovement = getTime();
self->nextAttack = 0;
self->roamX = self->x;
self->roamY = self->y;
self->roamZ = self->z;
int skillID = (int)self->data["m_iPassiveBuff"]; // cast passive
// TODO ABILITIES
/*std::vector<int> targetData = { 1, self->id, 0, 0, 0 };
for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
pwr.handle(self->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);*/
}
void MobAI::onRetreat(CombatNPC* npc, EntityRef src) {
Mob* self = (Mob*)npc;
self->target = nullptr;
MobAI::clearDebuff(self);
if (self->groupLeader != 0)
MobAI::groupRetreat(self);
}
void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
Mob* self = (Mob*)npc;
self->target = nullptr;
self->cbf = 0;
self->skillStyle = -1;
self->unbuffTimes.clear();
self->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
if (src.kind == EntityKind::PLAYER && src.isValid()) {
Player* plr = PlayerManager::getPlayer(src.sock);
Items::DropRoll rolled;
Items::DropRoll eventRolled;
std::map<int, int> qitemRolls;
std::vector<Player*> playerRefs;
if (plr->group == nullptr) {
playerRefs.push_back(plr);
Combat::genQItemRolls(playerRefs, qitemRolls);
Items::giveMobDrop(src.sock, self, rolled, eventRolled);
Missions::mobKilled(src.sock, self->type, qitemRolls);
}
else {
auto players = plr->group->filter(EntityKind::PLAYER);
for (EntityRef pRef : players) playerRefs.push_back(PlayerManager::getPlayer(pRef.sock));
Combat::genQItemRolls(playerRefs, qitemRolls);
for (int i = 0; i < players.size(); i++) {
CNSocket* sockTo = players[i].sock;
Player* otherPlr = PlayerManager::getPlayer(sockTo);
// only contribute to group members' kills if they're close enough
int dist = std::hypot(plr->x - otherPlr->x + 1, plr->y - otherPlr->y + 1);
if (dist > 5000)
continue;
Items::giveMobDrop(sockTo, self, rolled, eventRolled);
Missions::mobKilled(sockTo, self->type, qitemRolls);
}
}
}
// delay the despawn animation
self->despawned = false;
auto it = Transport::NPCQueues.find(self->id);
if (it == Transport::NPCQueues.end() || it->second.empty())
// skip mob movement and combat if disabled or not in view
if ((!simulateMobs || mob->playersInView == 0) && mob->state != MobState::DEAD
&& mob->state != MobState::RETREAT)
return;
// rewind or empty the movement queue
if (self->staticPath) {
/*
* This is inelegant, but we wind forward in the path until we find the point that
* corresponds with the Mob's spawn point.
*
* IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever.
*/
auto& queue = it->second;
for (auto point = queue.front(); point.x != self->spawnX || point.y != self->spawnY; point = queue.front()) {
queue.pop();
queue.push(point);
}
}
else {
Transport::NPCQueues.erase(self->id);
switch (mob->state) {
case MobState::INACTIVE:
// no-op
break;
case MobState::ROAMING:
roamingStep(mob, currTime);
break;
case MobState::COMBAT:
combatStep(mob, currTime);
break;
case MobState::RETREAT:
retreatStep(mob, currTime);
break;
case MobState::DEAD:
deadStep(mob, currTime);
break;
}
}

View File

@@ -1,46 +1,44 @@
#pragma once
#include "core/Core.hpp"
#include "JSON.hpp"
#include "NPCManager.hpp"
#include "Entities.hpp"
#include <unordered_map>
#include <string>
enum class MobState {
INACTIVE,
ROAMING,
COMBAT,
RETREAT,
DEAD
};
namespace MobAI {
void deadStep(CombatNPC* self, time_t currTime);
void combatStep(CombatNPC* self, time_t currTime);
void roamingStep(CombatNPC* self, time_t currTime);
void retreatStep(CombatNPC* self, time_t currTime);
void onRoamStart(CombatNPC* self, EntityRef src);
void onCombatStart(CombatNPC* self, EntityRef src);
void onRetreat(CombatNPC* self, EntityRef src);
void onDeath(CombatNPC* self, EntityRef src);
}
// needs to be declared before Mob's constructor
void step(CombatNPC*, int64_t);
};
struct Mob : public CombatNPC {
// general
std::unordered_map<int32_t,time_t> unbuffTimes = {};
MobState state = MobState::INACTIVE;
std::unordered_map<int32_t,int64_t> unbuffTimes = {};
// dead
time_t killedTime = 0;
time_t regenTime = 0;
int64_t killedTime = 0;
int64_t regenTime = 0;
bool summoned = false;
bool despawned = false; // for the sake of death animations
// roaming
int idleRange = 0;
const int sightRange = 0;
time_t nextMovement = 0;
int64_t nextMovement = 0;
bool staticPath = false;
int roamX = 0, roamY = 0, roamZ = 0;
// combat
CNSocket *target = nullptr;
time_t nextAttack = 0;
time_t lastDrainTime = 0;
int64_t nextAttack = 0;
int64_t lastDrainTime = 0;
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
@@ -49,13 +47,16 @@ struct Mob : public CombatNPC {
int offsetX = 0, offsetY = 0;
int groupMember[4] = {};
// for optimizing away AI in empty chunks
int playersInView = 0;
// temporary; until we're sure what's what
nlohmann::json data = {};
Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]),
sightRange(d["m_iSightRange"]) {
state = AIState::ROAMING;
state = MobState::ROAMING;
data = d;
@@ -64,30 +65,20 @@ struct Mob : public CombatNPC {
idleRange = (int)data["m_iIdleRange"];
level = data["m_iNpcLevel"];
roamX = x;
roamY = y;
roamZ = z;
roamX = spawnX = x;
roamY = spawnY = y;
roamZ = spawnZ = z;
offsetX = 0;
offsetY = 0;
cbf = 0;
appearanceData.iConditionBitFlag = 0;
// NOTE: there appear to be discrepancies in the dump
hp = maxHealth;
appearanceData.iHP = maxHealth;
kind = EntityKind::MOB;
// AI
stateHandlers[AIState::DEAD] = MobAI::deadStep;
stateHandlers[AIState::COMBAT] = MobAI::combatStep;
stateHandlers[AIState::ROAMING] = MobAI::roamingStep;
stateHandlers[AIState::RETREAT] = MobAI::retreatStep;
transitionHandlers[AIState::DEAD] = MobAI::onDeath;
transitionHandlers[AIState::COMBAT] = MobAI::onCombatStart;
transitionHandlers[AIState::ROAMING] = MobAI::onRoamStart;
transitionHandlers[AIState::RETREAT] = MobAI::onRetreat;
type = EntityType::MOB;
_stepAI = MobAI::step;
}
// constructor for /summon
@@ -98,9 +89,6 @@ struct Mob : public CombatNPC {
~Mob() {}
virtual int takeDamage(EntityRef src, int amt) override;
virtual void step(time_t currTime) override;
auto operator[](std::string s) {
return data[s];
}
@@ -110,9 +98,10 @@ namespace MobAI {
extern bool simulateMobs;
// TODO: make this internal later
void incNextMovement(Mob *mob, time_t currTime=0);
bool aggroCheck(Mob *mob, time_t currTime);
void incNextMovement(Mob *mob, int64_t currTime=0);
bool aggroCheck(Mob *mob, int64_t currTime);
void clearDebuff(Mob *mob);
void followToCombat(Mob *mob);
void groupRetreat(Mob *mob);
void enterCombat(CNSocket *sock, Mob *mob);
}

4
src/NPC.hpp Normal file
View File

@@ -0,0 +1,4 @@
#pragma once
#include "Chunking.hpp"
#include "Entities.hpp"

View File

@@ -1,8 +1,4 @@
#include "NPCManager.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "settings.hpp"
#include "Combat.hpp"
@@ -24,6 +20,8 @@
#include <assert.h>
#include <limits.h>
#include "JSON.hpp"
using namespace NPCManager;
std::unordered_map<int32_t, BaseNPC*> NPCManager::NPCs;
@@ -69,7 +67,7 @@ void NPCManager::destroyNPC(int32_t id) {
void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I, int angle) {
BaseNPC* npc = NPCs[id];
npc->angle = angle;
npc->appearanceData.iAngle = angle;
ChunkPos oldChunk = npc->chunkPos;
ChunkPos newChunk = Chunking::chunkPosAt(X, Y, I);
npc->x = X;
@@ -81,11 +79,11 @@ void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I,
Chunking::updateEntityChunk({id}, oldChunk, newChunk);
}
void NPCManager::sendToViewable(Entity *npc, void *buf, uint32_t type, size_t size) {
void NPCManager::sendToViewable(BaseNPC *npc, void *buf, uint32_t type, size_t size) {
for (auto it = npc->viewableChunks.begin(); it != npc->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
if (ref.kind == EntityKind::PLAYER)
if (ref.type == EntityType::PLAYER)
ref.sock->sendPacket(buf, type, size);
}
}
@@ -124,6 +122,7 @@ static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
// type must already be checked and updateNPCPosition() must be called on the result
BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) {
uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
#define EXTRA_HEIGHT 0
//assert(nextId < INT32_MAX);
int id = nextId--;
@@ -131,12 +130,12 @@ BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type,
BaseNPC *npc = nullptr;
if (team == 2) {
npc = new Mob(x, y, z, inst, type, NPCData[type], id);
npc = new Mob(x, y, z + EXTRA_HEIGHT, inst, type, NPCData[type], id);
// re-enable respawning, if desired
((Mob*)npc)->summoned = !respawn;
} else
npc = new BaseNPC(0, inst, type, id);
npc = new BaseNPC(x, y, z + EXTRA_HEIGHT, 0, inst, type, id);
NPCs[id] = npc;
@@ -155,7 +154,7 @@ static void npcSummonHandler(CNSocket* sock, CNPacketData* data) {
for (int i = 0; i < req->iNPCCnt; i++) {
BaseNPC *npc = summonNPC(plr->x, plr->y, plr->z, plr->instanceID, req->iNPCType);
updateNPCPosition(npc->id, plr->x, plr->y, plr->z, plr->instanceID, 0);
updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, 0);
}
}
@@ -184,12 +183,9 @@ static void handleWarp(CNSocket* sock, int32_t warpId) {
if (Warps[warpId].isInstance) {
uint64_t instanceID = Warps[warpId].instanceID;
Player* leader = plr;
if (plr->group != nullptr) leader = PlayerManager::getPlayer(plr->group->filter(EntityKind::PLAYER)[0].sock);
// if warp requires you to be on a mission, it's gotta be a unique instance
if (Warps[warpId].limitTaskID != 0 || instanceID == 14) { // 14 is a special case for the Time Lab
instanceID += ((uint64_t)leader->iID << 32); // upper 32 bits are leader ID
instanceID += ((uint64_t)plr->iIDGroup << 32); // upper 32 bits are leader ID
Chunking::createInstance(instanceID);
// save Lair entrance coords as a pseudo-Resurrect 'Em
@@ -199,13 +195,14 @@ static void handleWarp(CNSocket* sock, int32_t warpId) {
plr->recallInstance = instanceID;
}
if (plr->group == nullptr)
if (plr->iID == plr->iIDGroup && plr->groupCnt == 1)
PlayerManager::sendPlayerTo(sock, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
else {
auto players = plr->group->filter(EntityKind::PLAYER);
for (int i = 0; i < players.size(); i++) {
CNSocket* sockTo = players[i].sock;
Player* otherPlr = PlayerManager::getPlayer(sockTo);
Player* leaderPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
for (int i = 0; i < leaderPlr->groupCnt; i++) {
Player* otherPlr = PlayerManager::getPlayerFromID(leaderPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(leaderPlr->groupIDs[i]);
if (otherPlr == nullptr || sockTo == nullptr)
continue;
@@ -279,7 +276,7 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
for (auto c = chunks->begin(); c != chunks->end(); c++) { // haha get it
Chunk* chunk = *c;
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
if (ent->kind == EntityKind::PLAYER)
if (ent->type == EntityType::PLAYER)
continue;
BaseNPC* npcTemp = (BaseNPC*)ent->getEntity();
@@ -304,20 +301,20 @@ static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) {
std::cout << "Lord Fuse stage two" << std::endl;
// Fuse doesn't move
// Fuse doesn't move; spawnX, etc. is shorter to write than *appearanceData*
// Blastons, Heal
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2467);
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2467);
newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
plr->instanceID, oldbody->angle);
newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
// right arm, Adaptium, Stun
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, plr->instanceID, 2469);
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX - 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2469);
arm->angle = oldbody->angle;
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
plr->instanceID, oldbody->angle);
arm->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
}
// summon left arm and stage 3 body
@@ -328,18 +325,18 @@ static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
std::cout << "Lord Fuse stage three" << std::endl;
// Cosmix, Damage Point
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2468);
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2468);
newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
plr->instanceID, oldbody->angle);
newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
// Blastons, Heal
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, plr->instanceID, 2470);
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX + 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2470);
arm->angle = oldbody->angle;
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
plr->instanceID, oldbody->angle);
arm->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
}
std::vector<NPCEvent> NPCManager::NPCEvents = {
@@ -353,13 +350,13 @@ void NPCManager::queueNPCRemoval(int32_t id) {
RemovalQueue.push(id);
}
static void step(CNServer *serv, time_t currTime) {
static void step(CNServer *serv, int64_t currTime) {
for (auto& pair : NPCs) {
if (pair.second->kind != EntityKind::COMBAT_NPC && pair.second->kind != EntityKind::MOB)
if (pair.second->type != EntityType::COMBAT_NPC && pair.second->type != EntityType::MOB)
continue;
auto npc = (CombatNPC*)pair.second;
npc->step(currTime);
npc->stepAI(currTime);
}
// deallocate all NPCs queued for removal

View File

@@ -1,16 +1,15 @@
#pragma once
#include "core/Core.hpp"
#include "JSON.hpp"
#include "PlayerManager.hpp"
#include "NPC.hpp"
#include "Transport.hpp"
#include "Chunking.hpp"
#include "Entities.hpp"
#include "JSON.hpp"
#include <map>
#include <unordered_map>
#include <vector>
#include <set>
#define RESURRECT_HEIGHT 400
@@ -30,6 +29,8 @@ struct NPCEvent {
: npcType(t), trigger(tr), handler(hndlr) {}
};
struct WarpLocation;
namespace NPCManager {
extern std::unordered_map<int32_t, BaseNPC*> NPCs;
extern std::map<int32_t, WarpLocation> Warps;
@@ -43,7 +44,7 @@ namespace NPCManager {
void destroyNPC(int32_t);
void updateNPCPosition(int32_t, int X, int Y, int Z, uint64_t I, int angle);
void sendToViewable(Entity* npc, void* buf, uint32_t type, size_t size);
void sendToViewable(BaseNPC* npc, void* buf, uint32_t type, size_t size);
BaseNPC *summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn=false, bool baseInstance=false);

View File

@@ -1,11 +1,11 @@
#include "Nanos.hpp"
#include "servers/CNShardServer.hpp"
#include "Nanos.hpp"
#include "PlayerManager.hpp"
#include "Missions.hpp"
#include "Abilities.hpp"
#include "NPCManager.hpp"
#include "Combat.hpp"
#include "Missions.hpp"
#include "Groups.hpp"
#include "Abilities.hpp"
#include <cmath>
@@ -69,52 +69,6 @@ void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm)
PlayerManager::sendToViewable(sock, resp2, P_FE2CL_REP_PC_CHANGE_LEVEL);
}
std::vector<ICombatant*> Nanos::applyNanoBuff(SkillData* skill, Player* plr) {
assert(skill->drainType == SkillDrainType::PASSIVE);
EntityRef self = PlayerManager::getSockFromID(plr->iID);
std::vector<ICombatant*> affected;
std::vector<EntityRef> targets;
if (skill->targetType == SkillTargetType::GROUP) {
targets = plr->getGroupMembers(); // group
}
else if(skill->targetType == SkillTargetType::SELF) {
targets.push_back(self); // self
} else {
std::cout << "[WARN] Passive skill with type " << skill->skillType << " has target type MOB" << std::endl;
}
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
int boost = Nanos::getNanoBoost(plr) ? 3 : 0;
int value = skill->values[0][boost];
BuffStack passiveBuff = {
1, // passive nano buffs refreshed every tick
value,
self,
BuffClass::NONE, // overwritten per target
};
for (EntityRef target : targets) {
Entity* entity = target.getEntity();
if (entity->kind != PLAYER && entity->kind != COMBAT_NPC && entity->kind != MOB)
continue; // not a combatant
passiveBuff.buffStackClass = target == self ? BuffClass::NANO : BuffClass::GROUP_NANO;
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
if(combatant->addBuff(timeBuffId,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
},
[](EntityRef self, Buff* buff, time_t currTime) {
// no-op
},
&passiveBuff)) affected.push_back(combatant);
}
return affected;
}
void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
INITSTRUCT(sP_FE2CL_REP_NANO_ACTIVE_SUCC, resp);
resp.iActiveNanoSlotNum = slot;
@@ -128,19 +82,40 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
if (slot != -1 && plr->Nanos[nanoID].iSkillID == 0)
return; // prevent powerless nanos from summoning
plr->nanoDrainRate = 0;
int16_t skillID = plr->Nanos[plr->activeNano].iSkillID;
// passive nano unbuffing
if (SkillTable[skillID].drainType == 2) {
std::vector<int> targetData = findTargets(plr, skillID);
for (auto& pwr : NanoPowers)
if (pwr.skillType == SkillTable[skillID].skillType)
nanoUnbuff(sock, targetData, pwr.bitFlag, pwr.timeBuffID, 0,(SkillTable[skillID].targetType == 3));
}
if (nanoID >= NANO_COUNT || nanoID < 0)
return; // sanity check
plr->activeNano = nanoID;
sNano& nano = plr->Nanos[nanoID];
skillID = plr->Nanos[nanoID].iSkillID;
SkillData* skill = Abilities::SkillTable.count(nano.iSkillID) > 0
? &Abilities::SkillTable[nano.iSkillID] : nullptr;
if (slot != -1 && skill != nullptr && skill->drainType == SkillDrainType::PASSIVE) {
// passive buff effect
resp.eCSTB___Add = 1;
std::vector<ICombatant*> affectedCombatants = applyNanoBuff(skill, plr);
if(!affectedCombatants.empty()) Abilities::useNanoSkill(sock, skill, nano, affectedCombatants);
// passive nano buffing
if (SkillTable[skillID].drainType == 2) {
std::vector<int> targetData = findTargets(plr, skillID);
int boost = 0;
if (getNanoBoost(plr))
boost = 1;
for (auto& pwr : NanoPowers) {
if (pwr.skillType == SkillTable[skillID].skillType) {
resp.eCSTB___Add = 1; // the part that makes nano go ZOOMAZOOM
plr->nanoDrainRate = SkillTable[skillID].batteryUse[boost*3];
pwr.handle(sock, targetData, nanoID, skillID, 0, SkillTable[skillID].powerIntensity[boost]);
}
}
}
if (!silent) // silent nano death but only for the summoning player
@@ -149,7 +124,7 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
// Send to other players, these players can't handle silent nano deaths so this packet needs to be sent.
INITSTRUCT(sP_FE2CL_NANO_ACTIVE, pkt1);
pkt1.iPC_ID = plr->iID;
pkt1.Nano = nano;
pkt1.Nano = plr->Nanos[nanoID];
PlayerManager::sendToViewable(sock, pkt1, P_FE2CL_NANO_ACTIVE);
}
@@ -236,7 +211,7 @@ int Nanos::nanoStyle(int nanoID) {
bool Nanos::getNanoBoost(Player* plr) {
for (int i = 0; i < 3; i++)
if (plr->equippedNanos[i] == plr->activeNano)
if (plr->hasBuff(ECSB_STIMPAKSLOT1 + i))
if (plr->iConditionBitFlag & (CSB_BIT_STIMPAKSLOT1 << i))
return true;
return false;
}
@@ -260,6 +235,18 @@ static void nanoEquipHandler(CNSocket* sock, CNPacketData* data) {
// Update player
plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID;
// Unbuff gumballs
int value1 = CSB_BIT_STIMPAKSLOT1 << nano->iNanoSlotNum;
if (plr->iConditionBitFlag & value1) {
int value2 = ECSB_STIMPAKSLOT1 + nano->iNanoSlotNum;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = value2; // eCharStatusTimeBuffID
pkt.eTBU = 2; // eTimeBuffUpdate
pkt.eTBT = 1; // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = plr->iConditionBitFlag &= ~value1;
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
}
// unsummon nano if replaced
if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum])
summonNano(sock, -1);
@@ -302,24 +289,26 @@ static void nanoSummonHandler(CNSocket* sock, CNPacketData* data) {
static void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) {
Player *plr = PlayerManager::getPlayer(sock);
// validate request check
sP_CL2FE_REQ_NANO_SKILL_USE* pkt = (sP_CL2FE_REQ_NANO_SKILL_USE*)data->buf;
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_NANO_SKILL_USE), pkt->iTargetCnt, sizeof(int32_t), data->size)) {
std::cout << "[WARN] bad sP_CL2FE_REQ_NANO_SKILL_USE packet size" << std::endl;
return;
}
sNano& nano = plr->Nanos[plr->activeNano];
int16_t skillID = nano.iSkillID;
SkillData* skillData = &Abilities::SkillTable[skillID];
int16_t nanoID = plr->activeNano;
int16_t skillID = plr->Nanos[nanoID].iSkillID;
DEBUGLOG(
std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano skill " << std::endl;
)
// TODO ABILITIES
std::vector<ICombatant*> targetData = Abilities::matchTargets(skillData, pkt->iTargetCnt, (int32_t*)(pkt + 1));
Abilities::useNanoSkill(sock, skillData, nano, targetData);
std::vector<int> targetData = findTargets(plr, skillID, data);
int boost = 0;
if (getNanoBoost(plr))
boost = 1;
plr->Nanos[plr->activeNano].iStamina -= SkillTable[skillID].batteryUse[boost*3];
if (plr->Nanos[plr->activeNano].iStamina < 0)
plr->Nanos[plr->activeNano].iStamina = 0;
for (auto& pwr : NanoPowers)
if (pwr.skillType == SkillTable[skillID].skillType)
pwr.handle(sock, targetData, nanoID, skillID, SkillTable[skillID].durationTime[boost], SkillTable[skillID].powerIntensity[boost]);
if (plr->Nanos[plr->activeNano].iStamina < 0)
summonNano(sock, -1);

View File

@@ -1,11 +1,10 @@
#pragma once
#include "core/Core.hpp"
#include <set>
#include <vector>
#include "Player.hpp"
#include "Abilities.hpp"
#include <map>
#include "servers/CNShardServer.hpp"
struct NanoData {
int style;
@@ -26,5 +25,4 @@ namespace Nanos {
void summonNano(CNSocket* sock, int slot, bool silent = false);
int nanoStyle(int nanoID);
bool getNanoBoost(Player* plr);
std::vector<ICombatant*> applyNanoBuff(SkillData* skill, Player* plr);
}

View File

@@ -1,21 +1,17 @@
#pragma once
#include <string>
#include <cstring>
#include "core/Core.hpp"
#include "Chunking.hpp"
#include "Entities.hpp"
#include "Groups.hpp"
#include <vector>
/* forward declaration(s) */
class Buff;
struct BuffStack;
#define ACTIVE_MISSION_COUNT 6
#define PC_MAXHEALTH(level) (925 + 75 * (level))
struct Player : public Entity, public ICombatant {
struct Player : public Entity {
int accountId = 0;
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
int32_t iID = 0;
@@ -36,8 +32,9 @@ struct Player : public Entity, public ICombatant {
int8_t iPCState = 0;
int32_t iWarpLocationFlag = 0;
int64_t aSkywayLocationFlag[2] = {};
int32_t iConditionBitFlag = 0;
int32_t iSelfConditionBitFlag = 0;
int8_t iSpecialState = 0;
std::unordered_map<int, Buff*> buffs = {};
int angle = 0;
int lastX = 0, lastY = 0, lastZ = 0, lastAngle = 0;
@@ -52,6 +49,7 @@ struct Player : public Entity, public ICombatant {
bool inCombat = false;
bool onMonkey = false;
int nanoDrainRate = 0;
int healCooldown = 0;
int pointDamage = 0;
@@ -67,7 +65,10 @@ struct Player : public Entity, public ICombatant {
sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {};
Group* group = nullptr;
int32_t iIDGroup = 0;
int groupCnt = 0;
int32_t groupIDs[4] = {};
int32_t iGroupConditionBitFlag = 0;
bool notify = false;
bool hidden = false;
@@ -78,34 +79,14 @@ struct Player : public Entity, public ICombatant {
bool isBuddyBlocked[50] = {};
uint64_t iFirstUseFlag[2] = {};
time_t lastHeartbeat = 0;
int64_t lastHeartbeat = 0;
int suspicionRating = 0;
int64_t lastShot = 0;
std::vector<sItemBase> buyback = {};
Player() { kind = EntityKind::PLAYER; }
Player() { type = EntityType::PLAYER; }
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
virtual Buff* getBuff(int buffId) override;
virtual void removeBuff(int buffId) override;
virtual void removeBuff(int buffId, int buffClass) override;
virtual bool hasBuff(int buffId) override;
virtual int getCompositeCondition() override;
virtual int takeDamage(EntityRef src, int amt) override;
virtual int heal(EntityRef src, int amt) override;
virtual bool isAlive() override;
virtual int getCurrentHP() override;
virtual int getMaxHP() override;
virtual int getLevel() override;
virtual std::vector<EntityRef> getGroupMembers() override;
virtual int32_t getCharType() override;
virtual int32_t getID() override;
virtual EntityRef getRef() override;
virtual void step(time_t currTime) override;
sNano* getActiveNano();
sPCAppearanceData getAppearanceData();
};

View File

@@ -1,20 +1,25 @@
#include "PlayerManager.hpp"
#include "db/Database.hpp"
#include "core/Core.hpp"
#include "core/CNShared.hpp"
#include "servers/CNShardServer.hpp"
#include "db/Database.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Missions.hpp"
#include "Items.hpp"
#include "Nanos.hpp"
#include "Groups.hpp"
#include "Chat.hpp"
#include "Buddies.hpp"
#include "Combat.hpp"
#include "Racing.hpp"
#include "Eggs.hpp"
#include "Missions.hpp"
#include "Chat.hpp"
#include "Items.hpp"
#include "Buddies.hpp"
#include "BuiltinCommands.hpp"
#include "Abilities.hpp"
#include "Eggs.hpp"
#include "settings.hpp"
#include <assert.h>
#include <algorithm>
#include <vector>
#include <cmath>
@@ -23,17 +28,12 @@ using namespace PlayerManager;
std::map<CNSocket*, Player*> PlayerManager::players;
static void addPlayer(CNSocket* key, Player& plr) {
Player *p = new Player();
static void addPlayer(CNSocket* key, Player *plr) {
players[key] = plr;
plr->chunkPos = Chunking::INVALID_CHUNK;
plr->lastHeartbeat = 0;
// copy object into heap memory
*p = plr;
players[key] = p;
p->chunkPos = Chunking::INVALID_CHUNK;
p->lastHeartbeat = 0;
std::cout << getPlayerName(p) << " has joined!" << std::endl;
std::cout << getPlayerName(plr) << " has joined!" << std::endl;
std::cout << players.size() << " players" << std::endl;
}
@@ -41,13 +41,7 @@ void PlayerManager::removePlayer(CNSocket* key) {
Player* plr = getPlayer(key);
uint64_t fromInstance = plr->instanceID;
// free buff memory
for(auto buffEntry : plr->buffs)
delete buffEntry.second;
// leave group
if(plr->group != nullptr)
Groups::groupKick(plr->group, key);
Groups::groupKickPlayer(plr);
// remove player's bullets
Combat::Bullets.erase(plr->iID);
@@ -71,6 +65,16 @@ void PlayerManager::removePlayer(CNSocket* key) {
// if the player was in a lair, clean it up
Chunking::destroyInstanceIfEmpty(fromInstance);
// remove player's buffs from the server
auto it = Eggs::EggBuffs.begin();
while (it != Eggs::EggBuffs.end()) {
if (it->first.first == key) {
it = Eggs::EggBuffs.erase(it);
}
else
it++;
}
std::cout << players.size() << " players" << std::endl;
}
@@ -215,66 +219,73 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
return;
}
// for convenience
Player& plr = lm->plr;
Player *plr = new Player();
Database::getPlayer(plr, lm->playerId);
// check if account is already in use
if (isAccountInUse(plr.accountId)) {
if (isAccountInUse(plr->accountId)) {
// kick the other player
exitDuplicate(plr.accountId);
exitDuplicate(plr->accountId);
// re-read the player from disk, in case it was just flushed
*plr = {};
Database::getPlayer(plr, lm->playerId);
}
response.iID = plr.iID;
plr->groupCnt = 1;
plr->iIDGroup = plr->groupIDs[0] = plr->iID;
response.iID = plr->iID;
response.uiSvrTime = getTime();
response.PCLoadData2CL.iUserLevel = plr.accountLevel;
response.PCLoadData2CL.iHP = plr.HP;
response.PCLoadData2CL.iLevel = plr.level;
response.PCLoadData2CL.iCandy = plr.money;
response.PCLoadData2CL.iFusionMatter = plr.fusionmatter;
response.PCLoadData2CL.iMentor = plr.mentor;
response.PCLoadData2CL.iUserLevel = plr->accountLevel;
response.PCLoadData2CL.iHP = plr->HP;
response.PCLoadData2CL.iLevel = plr->level;
response.PCLoadData2CL.iCandy = plr->money;
response.PCLoadData2CL.iFusionMatter = plr->fusionmatter;
response.PCLoadData2CL.iMentor = plr->mentor;
response.PCLoadData2CL.iMentorCount = 1; // how many guides the player has had
response.PCLoadData2CL.iX = plr.x;
response.PCLoadData2CL.iY = plr.y;
response.PCLoadData2CL.iZ = plr.z;
response.PCLoadData2CL.iAngle = plr.angle;
response.PCLoadData2CL.iBatteryN = plr.batteryN;
response.PCLoadData2CL.iBatteryW = plr.batteryW;
response.PCLoadData2CL.iX = plr->x;
response.PCLoadData2CL.iY = plr->y;
response.PCLoadData2CL.iZ = plr->z;
response.PCLoadData2CL.iAngle = plr->angle;
response.PCLoadData2CL.iBatteryN = plr->batteryN;
response.PCLoadData2CL.iBatteryW = plr->batteryW;
response.PCLoadData2CL.iBuddyWarpTime = 60; // sets 60s warp cooldown on login
response.PCLoadData2CL.iWarpLocationFlag = plr.iWarpLocationFlag;
response.PCLoadData2CL.aWyvernLocationFlag[0] = plr.aSkywayLocationFlag[0];
response.PCLoadData2CL.aWyvernLocationFlag[1] = plr.aSkywayLocationFlag[1];
response.PCLoadData2CL.iWarpLocationFlag = plr->iWarpLocationFlag;
response.PCLoadData2CL.aWyvernLocationFlag[0] = plr->aSkywayLocationFlag[0];
response.PCLoadData2CL.aWyvernLocationFlag[1] = plr->aSkywayLocationFlag[1];
response.PCLoadData2CL.iActiveNanoSlotNum = -1;
response.PCLoadData2CL.iFatigue = 50;
response.PCLoadData2CL.PCStyle = plr.PCStyle;
response.PCLoadData2CL.PCStyle = plr->PCStyle;
// client doesnt read this, it gets it from charinfo
// response.PCLoadData2CL.PCStyle2 = plr.PCStyle2;
// response.PCLoadData2CL.PCStyle2 = plr->PCStyle2;
// inventory
for (int i = 0; i < AEQUIP_COUNT; i++)
response.PCLoadData2CL.aEquip[i] = plr.Equip[i];
response.PCLoadData2CL.aEquip[i] = plr->Equip[i];
for (int i = 0; i < AINVEN_COUNT; i++)
response.PCLoadData2CL.aInven[i] = plr.Inven[i];
response.PCLoadData2CL.aInven[i] = plr->Inven[i];
// quest inventory
for (int i = 0; i < AQINVEN_COUNT; i++)
response.PCLoadData2CL.aQInven[i] = plr.QInven[i];
response.PCLoadData2CL.aQInven[i] = plr->QInven[i];
// nanos
for (int i = 1; i < SIZEOF_NANO_BANK_SLOT; i++) {
response.PCLoadData2CL.aNanoBank[i] = plr.Nanos[i];
response.PCLoadData2CL.aNanoBank[i] = plr->Nanos[i];
}
for (int i = 0; i < 3; i++) {
response.PCLoadData2CL.aNanoSlots[i] = plr.equippedNanos[i];
response.PCLoadData2CL.aNanoSlots[i] = plr->equippedNanos[i];
}
// missions in progress
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr.tasks[i] == 0)
if (plr->tasks[i] == 0)
break;
response.PCLoadData2CL.aRunningQuest[i].m_aCurrTaskID = plr.tasks[i];
TaskData &task = *Missions::Tasks[plr.tasks[i]];
response.PCLoadData2CL.aRunningQuest[i].m_aCurrTaskID = plr->tasks[i];
TaskData &task = *Missions::Tasks[plr->tasks[i]];
for (int j = 0; j < 3; j++) {
response.PCLoadData2CL.aRunningQuest[i].m_aKillNPCID[j] = (int)task["m_iCSUEnemyID"][j];
response.PCLoadData2CL.aRunningQuest[i].m_aKillNPCCount[j] = plr.RemainingNPCCount[i][j];
response.PCLoadData2CL.aRunningQuest[i].m_aKillNPCCount[j] = plr->RemainingNPCCount[i][j];
/*
* client doesn't care about NeededItem ID and Count,
* it gets Count from Quest Inventory
@@ -284,12 +295,12 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
*/
}
}
response.PCLoadData2CL.iCurrentMissionID = plr.CurrentMissionID;
response.PCLoadData2CL.iCurrentMissionID = plr->CurrentMissionID;
// completed missions
// the packet requires 32 items, but the client only checks the first 16 (shrug)
for (int i = 0; i < 16; i++) {
response.PCLoadData2CL.aQuestFlag[i] = plr.aQuestFlag[i];
response.PCLoadData2CL.aQuestFlag[i] = plr->aQuestFlag[i];
}
// Computress tips
@@ -298,11 +309,11 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
response.PCLoadData2CL.iFirstUseFlag2 = UINT64_MAX;
}
else {
response.PCLoadData2CL.iFirstUseFlag1 = plr.iFirstUseFlag[0];
response.PCLoadData2CL.iFirstUseFlag2 = plr.iFirstUseFlag[1];
response.PCLoadData2CL.iFirstUseFlag1 = plr->iFirstUseFlag[0];
response.PCLoadData2CL.iFirstUseFlag2 = plr->iFirstUseFlag[1];
}
plr.instanceID = INSTANCE_OVERWORLD; // the player should never be in an instance on enter
plr->instanceID = INSTANCE_OVERWORLD; // the player should never be in an instance on enter
sock->setEKey(CNSocketEncryption::createNewKey(response.uiSvrTime, response.iID + 1, response.PCLoadData2CL.iFusionMatter + 1));
sock->setFEKey(lm->FEKey);
@@ -313,14 +324,14 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
// transmit MOTD after entering the game, so the client hopefully changes modes on time
Chat::sendServerMessage(sock, settings::MOTDSTRING);
// copy Player object into the shard
// transfer ownership of Player object into the shard (still valid in this function though)
addPlayer(sock, plr);
// check if there is an expiring vehicle
Items::checkItemExpire(sock, getPlayer(sock));
Items::checkItemExpire(sock, plr);
// set player equip stats
Items::setItemStats(getPlayer(sock));
Items::setItemStats(plr);
Missions::failInstancedMissions(sock);
@@ -331,9 +342,9 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
for (auto& pair : players)
if (pair.second->notify)
Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(&plr) + " has joined.");
Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(plr) + " has joined.");
// deallocate lm (and therefore the plr object)
// deallocate lm
delete lm;
}
@@ -342,7 +353,7 @@ void PlayerManager::sendToViewable(CNSocket* sock, void* buf, uint32_t type, siz
for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
if (ref.kind != EntityKind::PLAYER || ref.sock == sock)
if (ref.type != EntityType::PLAYER || ref.sock == sock)
continue;
ref.sock->sendPacket(buf, type, size);
@@ -395,23 +406,21 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
int activeSlot = -1;
bool move = false;
switch ((ePCRegenType)reviveData->iRegenType) {
case ePCRegenType::HereByPhoenix: // nano revive
if (!(plr->hasBuff(ECSB_PHOENIX)))
return; // sanity check
if (reviveData->iRegenType == 3 && plr->iConditionBitFlag & CSB_BIT_PHOENIX) {
// nano revive
plr->Nanos[plr->activeNano].iStamina = 0;
// TODO ABILITIES
//Abilities::applyBuff(sock, plr->Nanos[plr->activeNano].iSkillID, 2, 1, 0);
// fallthrough
case ePCRegenType::HereByPhoenixGroup: // revived by group member's nano
plr->HP = PC_MAXHEALTH(plr->level) / 2;
break;
default: // plain respawn
Nanos::applyBuff(sock, plr->Nanos[plr->activeNano].iSkillID, 2, 1, 0);
} else if (reviveData->iRegenType == 4) {
// revived by group member's nano
plr->HP = PC_MAXHEALTH(plr->level) / 2;
// fallthrough
case ePCRegenType::Unstick: // warp away
} else if (reviveData->iRegenType == 5) {
// warp away
move = true;
break;
} else {
// plain respawn
move = true;
plr->HP = PC_MAXHEALTH(plr->level) / 2;
}
for (int i = 0; i < 3; i++) {
@@ -463,9 +472,11 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
resp2.PCRegenDataForOtherPC.iHP = plr->HP;
resp2.PCRegenDataForOtherPC.iAngle = plr->angle;
if (plr->group != nullptr) {
resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->getCompositeCondition();
Player *otherPlr = getPlayerFromID(plr->iIDGroup);
if (otherPlr != nullptr) {
int bitFlag = Groups::getGroupFlags(otherPlr);
resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->iConditionBitFlag = plr->iSelfConditionBitFlag | bitFlag;
resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState;
resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState;
resp2.PCRegenDataForOtherPC.Nano = plr->Nanos[plr->activeNano];
@@ -656,16 +667,16 @@ CNSocket *PlayerManager::getSockFromName(std::string firstname, std::string last
}
CNSocket *PlayerManager::getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname) {
switch ((eCN_GM_TargetSearchBy)by) {
case eCN_GM_TargetSearchBy::PC_ID:
switch (by) {
case eCN_GM_TargetSearchBy__PC_ID:
assert(id != 0);
return getSockFromID(id);
case eCN_GM_TargetSearchBy::PC_UID: // account id; not player id
case eCN_GM_TargetSearchBy__PC_UID: // account id; not player id
assert(uid != 0);
for (auto& pair : players)
if (pair.second->accountId == uid)
return pair.first;
case eCN_GM_TargetSearchBy::PC_Name:
case eCN_GM_TargetSearchBy__PC_Name:
assert(firstname != "" && lastname != ""); // XXX: remove this if we start messing around with edited names?
return getSockFromName(firstname, lastname);
}

View File

@@ -1,15 +1,16 @@
#pragma once
#include "core/Core.hpp"
#include "Player.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "Chunking.hpp"
#include "Transport.hpp"
#include "Entities.hpp"
#include <utility>
#include <map>
#include <list>
struct WarpLocation;
namespace PlayerManager {
extern std::map<CNSocket*, Player*> players;
void init();
@@ -42,7 +43,7 @@ namespace PlayerManager {
for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
if (ref.kind != EntityKind::PLAYER || ref.sock == sock)
if (ref.type != EntityType::PLAYER || ref.sock == sock)
continue;
ref.sock->sendPacket(pkt, type);

View File

@@ -1,7 +1,4 @@
#include "PlayerMovement.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "TableData.hpp"
#include "core/Core.hpp"
@@ -36,7 +33,7 @@ static void movePlayer(CNSocket* sock, CNPacketData* data) {
// [gruntwork] check if player has a follower and move it
if (TableData::RunningNPCPaths.find(plr->iID) != TableData::RunningNPCPaths.end()) {
BaseNPC* follower = TableData::RunningNPCPaths[plr->iID].first;
Transport::NPCQueues.erase(follower->id); // erase existing points
Transport::NPCQueues.erase(follower->appearanceData.iNPC_ID); // erase existing points
std::queue<Vec3> queue;
Vec3 from = { follower->x, follower->y, follower->z };
float drag = 0.95f; // this ensures that they don't bump into the player
@@ -48,7 +45,7 @@ static void movePlayer(CNSocket* sock, CNPacketData* data) {
// add a route to the queue; to be processed in Transport::stepNPCPathing()
Transport::lerp(&queue, from, to, NPC_DEFAULT_SPEED * 1.5); // little faster than typical
Transport::NPCQueues[follower->id] = queue;
Transport::NPCQueues[follower->appearanceData.iNPC_ID] = queue;
}
}

View File

@@ -1,12 +1,10 @@
#include "Racing.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp"
#include "NPCManager.hpp"
#include "Racing.hpp"
#include "PlayerManager.hpp"
#include "Missions.hpp"
#include "Items.hpp"
#include "db/Database.hpp"
#include "NPCManager.hpp"
using namespace Racing;

View File

@@ -1,11 +1,9 @@
#pragma once
#include "core/Core.hpp"
#include <map>
#include <vector>
#include <set>
#include "servers/CNShardServer.hpp"
struct EPInfo {
int zoneX, zoneY, EPID, maxScore, maxTime;
};
@@ -13,7 +11,7 @@ struct EPInfo {
struct EPRace {
std::set<int> collectedRings;
int mode, ticketSlot;
time_t startTime;
int64_t startTime;
};
namespace Racing {

View File

@@ -2,7 +2,6 @@
#include <random>
#include <memory>
#include <vector>
namespace Rand {
extern std::unique_ptr<std::mt19937> generator;

View File

@@ -1,14 +1,18 @@
#include "TableData.hpp"
#include "NPCManager.hpp"
#include "Missions.hpp"
#include "Transport.hpp"
#include "Items.hpp"
#include "Vendors.hpp"
#include "Racing.hpp"
#include "settings.hpp"
#include "Missions.hpp"
#include "Chunking.hpp"
#include "Nanos.hpp"
#include "Racing.hpp"
#include "Vendors.hpp"
#include "Abilities.hpp"
#include "Eggs.hpp"
#include "JSON.hpp"
#include <fstream>
#include <sstream>
#include <cmath>
@@ -33,6 +37,22 @@ public:
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.
*/
@@ -211,34 +231,18 @@ static void loadXDT(json& xdtData) {
// load nano powers
json skills = xdtData["m_pSkillTable"]["m_pSkillData"];
for (json::iterator _skill = skills.begin(); _skill != skills.end(); _skill++) {
auto skill = _skill.value();
SkillData skillData = {
skill["m_iSkillType"],
skill["m_iEffectTarget"],
skill["m_iEffectType"],
skill["m_iTargetType"],
skill["m_iBatteryDrainType"],
skill["m_iEffectArea"]
};
skillData.valueTypes[0] = skill["m_iValueA_Type"];
skillData.valueTypes[1] = skill["m_iValueB_Type"];
skillData.valueTypes[2] = skill["m_iValueC_Type"];
for (json::iterator _skills = skills.begin(); _skills != skills.end(); _skills++) {
auto skills = _skills.value();
SkillData skillData = { skills["m_iSkillType"], skills["m_iTargetType"], skills["m_iBatteryDrainType"], skills["m_iEffectArea"] };
for (int i = 0; i < 4; i++) {
skillData.batteryUse[i] = skill["m_iBatteryDrainUse"][i];
skillData.durationTime[i] = skill["m_iDurationTime"][i];
skillData.values[0][i] = skill["m_iValueA"][i];
skillData.values[1][i] = skill["m_iValueB"][i];
skillData.values[2][i] = skill["m_iValueC"][i];
skillData.batteryUse[i] = skills["m_iBatteryDrainUse"][i];
skillData.durationTime[i] = skills["m_iDurationTime"][i];
skillData.powerIntensity[i] = skills["m_iValueA"][i];
}
Abilities::SkillTable[skill["m_iSkillNumber"]] = skillData;
Nanos::SkillTable[skills["m_iSkillNumber"]] = skillData;
}
std::cout << "[INFO] Loaded " << Abilities::SkillTable.size() << " nano skills" << std::endl;
std::cout << "[INFO] Loaded " << Nanos::SkillTable.size() << " nano skills" << std::endl;
// load EP data
json instances = xdtData["m_pInstanceTable"]["m_pInstanceData"];
@@ -310,10 +314,10 @@ static void loadPaths(json& pathData, int32_t* nextId) {
if (passedDistance >= SLIDER_GAP_SIZE) { // space them out uniformaly
passedDistance -= SLIDER_GAP_SIZE; // step down
// spawn a slider
Bus* slider = new Bus(0, INSTANCE_OVERWORLD, 1, (*nextId)--);
NPCManager::NPCs[slider->id] = slider;
NPCManager::updateNPCPosition(slider->id, point.x, point.y, point.z, INSTANCE_OVERWORLD, 0);
Transport::NPCQueues[slider->id] = route;
Bus* slider = new Bus(point.x, point.y, point.z, 0, INSTANCE_OVERWORLD, 1, (*nextId)--);
NPCManager::NPCs[slider->appearanceData.iNPC_ID] = slider;
NPCManager::updateNPCPosition(slider->appearanceData.iNPC_ID, slider->x, slider->y, slider->z, INSTANCE_OVERWORLD, 0);
Transport::NPCQueues[slider->appearanceData.iNPC_ID] = route;
}
// rotate
route.pop();
@@ -655,7 +659,7 @@ static void loadEggs(json& eggData, int32_t* nextId) {
int id = (*nextId)--;
uint64_t instanceID = egg.find("iMapNum") == egg.end() ? INSTANCE_OVERWORLD : (int)egg["iMapNum"];
Egg* addEgg = new Egg(instanceID, (int)egg["iType"], id, false);
Egg* addEgg = new Egg((int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, (int)egg["iType"], id, false);
NPCManager::NPCs[id] = addEgg;
eggCount++;
NPCManager::updateNPCPosition(id, (int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, 0);
@@ -674,6 +678,8 @@ static void loadEggs(json& eggData, int32_t* nextId) {
* Load gruntwork output, if it exists
*/
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
if (gruntwork.is_null())
return;
try {
auto paths = gruntwork["paths"];
@@ -723,8 +729,8 @@ static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
}
static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
if (gruntwork.is_null()) return;
if (gruntwork.is_null())
return;
try {
// skyway paths
@@ -749,7 +755,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
if (NPCManager::NPCs.find(npcID) == NPCManager::NPCs.end())
continue; // NPC not found
BaseNPC* npc = NPCManager::NPCs[npcID];
npc->angle = angle;
npc->appearanceData.iAngle = angle;
RunningNPCRotations[npcID] = angle;
}
@@ -762,8 +768,8 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
if (NPCManager::NPCs.find(npcID) == NPCManager::NPCs.end())
continue; // NPC not found
BaseNPC* npc = NPCManager::NPCs[npcID];
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y,
npc->z, instanceID, npc->angle);
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y,
npc->z, instanceID, npc->appearanceData.iAngle);
RunningNPCMapNumbers[npcID] = instanceID;
}
@@ -776,6 +782,8 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int id = (*nextId)--;
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) {
npc = new Mob(mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iNPCType"],
NPCManager::NPCData[(int)mob["iNPCType"]], id);
@@ -783,18 +791,21 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
// re-enable respawning
((Mob*)npc)->summoned = false;
} else {
npc = new BaseNPC(mob["iAngle"], instanceID, mob["iNPCType"], id);
npc = new BaseNPC(mob["iX"], mob["iY"], mob["iZ"], mob["iAngle"], instanceID, mob["iNPCType"], id);
}
NPCManager::NPCs[npc->id] = npc;
RunningMobs[npc->id] = npc;
NPCManager::updateNPCPosition(npc->id, mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iAngle"]);
NPCManager::NPCs[npc->appearanceData.iNPC_ID] = npc;
RunningMobs[npc->appearanceData.iNPC_ID] = npc;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iAngle"]);
}
// mob groups
auto groups = gruntwork["groups"];
for (auto _group = groups.begin(); _group != groups.end(); _group++) {
auto leader = _group.value();
ensureValidNPCType((int)leader["iNPCType"], settings::GRUNTWORKJSON);
auto td = NPCManager::NPCData[(int)leader["iNPCType"]];
uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"];
@@ -815,6 +826,9 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int followerCount = 0;
for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) {
auto follower = _fol.value();
ensureValidNPCType((int)follower["iNPCType"], settings::GRUNTWORKJSON);
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);
@@ -826,7 +840,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"];
tmpFol->offsetY = follower.find("iOffsetY") == follower.end() ? 0 : (int)follower["iOffsetY"];
tmpFol->groupLeader = tmp->id;
tmpFol->groupLeader = tmp->appearanceData.iNPC_ID;
tmp->groupMember[followerCount++] = *nextId;
(*nextId)--;
@@ -836,7 +850,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
std::cout << "[WARN] Mob group leader with ID " << *nextId << " has too many followers (" << followers.size() << ")\n";
}
RunningGroups[tmp->id] = tmp; // store as running
RunningGroups[tmp->appearanceData.iNPC_ID] = tmp; // store as running
}
auto eggs = gruntwork["eggs"];
@@ -845,7 +859,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int id = (*nextId)--;
uint64_t instanceID = egg.find("iMapNum") == egg.end() ? INSTANCE_OVERWORLD : (int)egg["iMapNum"];
Egg* addEgg = new Egg(instanceID, (int)egg["iType"], id, false);
Egg* addEgg = new Egg((int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, (int)egg["iType"], id, false);
NPCManager::NPCs[id] = addEgg;
NPCManager::updateNPCPosition(id, (int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, 0);
RunningEggs[id] = addEgg;
@@ -871,16 +885,15 @@ static void loadNPCs(json& npcData) {
npcID += NPC_ID_OFFSET;
int instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"];
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;
continue;
}
ensureValidNPCType(type, settings::NPCJSON);
#ifdef ACADEMY
// do not spawn NPCs in the future
if (npc["iX"] > 512000 && npc["iY"] < 256000)
continue;
#endif
BaseNPC* tmp = new BaseNPC(npc["iAngle"], instanceID, type, npcID);
BaseNPC* tmp = new BaseNPC(npc["iX"], npc["iY"], npc["iZ"], npc["iAngle"], instanceID, type, npcID);
NPCManager::NPCs[npcID] = tmp;
NPCManager::updateNPCPosition(npcID, npc["iX"], npc["iY"], npc["iZ"], instanceID, npc["iAngle"]);
@@ -916,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
npcID += MOB_ID_OFFSET;
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;
continue;
}
ensureValidNPCType(type, settings::MOBJSON);
auto td = NPCManager::NPCData[type];
uint64_t instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"];
@@ -947,7 +959,10 @@ static void loadMobs(json& npcData, int32_t* nextId) {
for (json::iterator _group = groupData.begin(); _group != groupData.end(); _group++) {
auto leader = _group.value();
int leadID = std::strtol(_group.key().c_str(), nullptr, 10); // parse ID string to integer
leadID += MOB_GROUP_ID_OFFSET;
ensureValidNPCType(leader["iNPCType"], settings::MOBJSON);
auto td = NPCManager::NPCData[(int)leader["iNPCType"]];
uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"];
auto followers = leader["aFollowers"];
@@ -977,6 +992,9 @@ static void loadMobs(json& npcData, int32_t* nextId) {
int followerCount = 0;
for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) {
auto follower = _fol.value();
ensureValidNPCType(follower["iNPCType"], settings::MOBJSON);
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);
@@ -985,7 +1003,7 @@ static void loadMobs(json& npcData, int32_t* nextId) {
tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"];
tmpFol->offsetY = follower.find("iOffsetY") == follower.end() ? 0 : (int)follower["iOffsetY"];
tmpFol->groupLeader = tmp->id;
tmpFol->groupLeader = tmp->appearanceData.iNPC_ID;
tmp->groupMember[followerCount++] = *nextId;
(*nextId)--;
@@ -1082,19 +1100,39 @@ void TableData::init() {
};
// load JSON data into tables
std::ifstream fstream;
for (int i = 0; i < 7; i++) {
std::pair<json*, std::string>& table = tables[i];
fstream.open(settings::TDATADIR + "/" + table.second); // open file
if (!fstream.fail()) {
fstream >> *table.first; // load file contents into table
} else {
if (table.first != &gruntwork) { // gruntwork isn't critical
// scope for fstream
{
std::ifstream fstream;
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;
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
@@ -1109,11 +1147,11 @@ void TableData::init() {
std::string patchModuleName = *it;
std::string patchFile = settings::PATCHDIR + patchModuleName + "/" + table.second;
try {
std::ifstream fstream;
fstream.open(patchFile);
fstream >> patch; // load into temporary json object
std::cout << "[INFO] Patching " << patchFile << std::endl;
patchJSON(table.first, &patch); // patch
fstream.close();
} catch (const std::exception& err) {
// no-op
}
@@ -1188,7 +1226,7 @@ void TableData::flush() {
continue;
int x, y, z;
if (npc->kind == EntityKind::MOB) {
if (npc->type == EntityType::MOB) {
Mob *m = (Mob*)npc;
x = m->spawnX;
y = m->spawnY;
@@ -1200,13 +1238,13 @@ void TableData::flush() {
}
// NOTE: this format deviates slightly from the one in mobs.json
mob["iNPCType"] = (int)npc->type;
mob["iNPCType"] = (int)npc->appearanceData.iNPCType;
mob["iX"] = x;
mob["iY"] = y;
mob["iZ"] = z;
mob["iMapNum"] = MAPNUM(npc->instanceID);
// this is a bit imperfect, since this is a live angle, not a spawn angle so it'll change often, but eh
mob["iAngle"] = npc->angle;
mob["iAngle"] = npc->appearanceData.iAngle;
// it's called mobs, but really it's everything
gruntwork["mobs"].push_back(mob);
@@ -1221,19 +1259,19 @@ void TableData::flush() {
int x, y, z;
std::vector<Mob*> followers;
if (npc->kind == EntityKind::MOB) {
if (npc->type == EntityType::MOB) {
Mob* m = (Mob*)npc;
x = m->spawnX;
y = m->spawnY;
z = m->spawnZ;
if (m->groupLeader != m->id) { // make sure this is a leader
if (m->groupLeader != m->appearanceData.iNPC_ID) { // make sure this is a leader
std::cout << "[WARN] Non-leader mob found in running groups; ignoring\n";
continue;
}
// add follower data to vector; go until OOB or until follower ID is 0
for (int i = 0; i < 4 && m->groupMember[i] > 0; i++) {
if (NPCManager::NPCs.find(m->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[m->groupMember[i]]->kind != EntityKind::MOB) {
if (NPCManager::NPCs.find(m->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[m->groupMember[i]]->type != EntityType::MOB) {
std::cout << "[WARN] Follower with ID " << m->groupMember[i] << " not found; skipping\n";
continue;
}
@@ -1247,13 +1285,13 @@ void TableData::flush() {
}
// NOTE: this format deviates slightly from the one in mobs.json
mob["iNPCType"] = (int)npc->type;
mob["iNPCType"] = (int)npc->appearanceData.iNPCType;
mob["iX"] = x;
mob["iY"] = y;
mob["iZ"] = z;
mob["iMapNum"] = MAPNUM(npc->instanceID);
// this is a bit imperfect, since this is a live angle, not a spawn angle so it'll change often, but eh
mob["iAngle"] = npc->angle;
mob["iAngle"] = npc->appearanceData.iAngle;
// followers
while (followers.size() > 0) {
@@ -1262,7 +1300,7 @@ void TableData::flush() {
// populate JSON entry
json fol;
fol["iNPCType"] = follower->type;
fol["iNPCType"] = follower->appearanceData.iNPCType;
fol["iOffsetX"] = follower->offsetX;
fol["iOffsetY"] = follower->offsetY;
@@ -1287,7 +1325,7 @@ void TableData::flush() {
int mapnum = MAPNUM(npc->instanceID);
if (mapnum != 0)
egg["iMapNum"] = mapnum;
egg["iType"] = npc->type;
egg["iType"] = npc->appearanceData.iNPCType;
gruntwork["eggs"].push_back(egg);
}

View File

@@ -1,13 +1,8 @@
#pragma once
#include "JSON.hpp"
#include "Entities.hpp"
#include "Transport.hpp"
#include <map>
#include <unordered_map>
#include <vector>
#include "NPCManager.hpp"
// these are added to the NPC's static key to avoid collisions
const int NPC_ID_OFFSET = 1;

View File

@@ -1,8 +1,6 @@
#include "Trading.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "db/Database.hpp"
using namespace Trading;
@@ -272,6 +270,8 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
otherSock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
return;
}
Database::commitTrade(plr, plr2);
}
static void tradeConfirmCancel(CNSocket* sock, CNPacketData* data) {

View File

@@ -1,5 +1,7 @@
#pragma once
#include "Items.hpp"
namespace Trading {
void init();
}

View File

@@ -1,12 +1,9 @@
#include "Transport.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "Transport.hpp"
#include "TableData.hpp"
#include "Entities.hpp"
#include "NPCManager.hpp"
#include "Combat.hpp"
#include "MobAI.hpp"
#include <unordered_map>
@@ -260,13 +257,13 @@ static void stepNPCPathing() {
}
// skip if not simulating mobs
if (npc->kind == EntityKind::MOB && !MobAI::simulateMobs) {
if (npc->type == EntityType::MOB && !MobAI::simulateMobs) {
it++;
continue;
}
// do not roam if not roaming
if (npc->kind == EntityKind::MOB && ((Mob*)npc)->state != AIState::ROAMING) {
if (npc->type == EntityType::MOB && ((Mob*)npc)->state != MobState::ROAMING) {
it++;
continue;
}
@@ -279,15 +276,15 @@ static void stepNPCPathing() {
int distanceBetween = hypot(dXY, point.z - npc->z); // total distance
// update NPC location to update viewables
NPCManager::updateNPCPosition(npc->id, point.x, point.y, point.z, npc->instanceID, npc->angle);
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, point.x, point.y, point.z, npc->instanceID, npc->appearanceData.iAngle);
// TODO: move walking logic into Entity stack
switch (npc->kind) {
case EntityKind::BUS:
switch (npc->type) {
case EntityType::BUS:
INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove);
busMove.eTT = 3;
busMove.iT_ID = npc->id;
busMove.iT_ID = npc->appearanceData.iNPC_ID;
busMove.iMoveStyle = 0; // ???
busMove.iToX = point.x;
busMove.iToY = point.y;
@@ -296,12 +293,12 @@ static void stepNPCPathing() {
NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE));
break;
case EntityKind::MOB:
case EntityType::MOB:
MobAI::incNextMovement((Mob*)npc);
/* fallthrough */
default:
INITSTRUCT(sP_FE2CL_NPC_MOVE, move);
move.iNPC_ID = npc->id;
move.iNPC_ID = npc->appearanceData.iNPC_ID;
move.iMoveStyle = 0; // ???
move.iToX = point.x;
move.iToY = point.y;
@@ -322,7 +319,7 @@ static void stepNPCPathing() {
}
}
static void tickTransportationSystem(CNServer* serv, time_t currTime) {
static void tickTransportationSystem(CNServer* serv, int64_t currTime) {
stepNPCPathing();
stepSkywaySystem();
}
@@ -388,7 +385,7 @@ NPCPath* Transport::findApplicablePath(int32_t id, int32_t type, int taskID) {
void Transport::constructPathNPC(int32_t id, NPCPath* path) {
BaseNPC* npc = NPCManager::NPCs[id];
if (npc->kind == EntityKind::MOB)
if (npc->type == EntityType::MOB)
((Mob*)(npc))->staticPath = true;
npc->loopingPath = path->isLoop;

View File

@@ -1,11 +1,8 @@
#pragma once
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include <unordered_map>
#include <map>
#include <vector>
#include <queue>
const int SLIDER_SPEED = 1200;
const int SLIDER_STOP_TICKS = 16;

View File

@@ -1,11 +1,9 @@
#include "Vendors.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Rand.hpp"
// 7 days
#define VEHICLE_EXPIRY_DURATION 604800
using namespace Vendors;
std::map<int32_t, std::vector<VendorListing>> Vendors::VendorTables;
@@ -58,8 +56,8 @@ static void vendorBuy(CNSocket* sock, CNPacketData* data) {
// if vehicle
if (req->Item.iType == 10) {
// set time limit: current time + 7days
req->Item.iTimeLimit = getTimestamp() + 604800;
// set time limit: current time + expiry duration
req->Item.iTimeLimit = getTimestamp() + VEHICLE_EXPIRY_DURATION;
}
if (slot != req->iInvenSlotNum) {
@@ -229,12 +227,20 @@ static void vendorTable(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, resp);
for (int i = 0; i < (int)listings.size() && i < 20; i++) { // 20 is the max
sItemBase base;
sItemBase base = {};
base.iID = listings[i].id;
base.iOpt = 0;
base.iTimeLimit = 0;
base.iType = listings[i].type;
/*
* Set vehicle expiry value.
*
* Note: sItemBase.iTimeLimit in the context of vendor listings contains
* a duration, unlike in most other contexts where it contains the
* expiration timestamp.
*/
if (listings[i].type == 10)
base.iTimeLimit = VEHICLE_EXPIRY_DURATION;
sItemVendor vItem;
vItem.item = base;
vItem.iSortNum = listings[i].sort;

View File

@@ -1,9 +1,10 @@
#pragma once
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include <vector>
#include <map>
#include "Items.hpp"
#include "PlayerManager.hpp"
struct VendorListing {
int sort, type, id;

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 num = (uint64_t)(iv1 + 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);
}
@@ -65,7 +66,7 @@ CNPacketData::CNPacketData(void *b, uint32_t t, int l, int trnum, void *trs):
// ========================================================[[ CNSocket ]]========================================================
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) {
@@ -109,7 +110,11 @@ bool CNSocket::isAlive() {
}
void CNSocket::kill() {
if (!alive)
return;
alive = false;
#ifdef _WIN32
shutdown(sock, SD_BOTH);
closesocket(sock);
@@ -241,9 +246,10 @@ void CNSocket::step() {
if (readSize <= 0) {
// we aren't reading a packet yet, try to start looking for one
int recved = recv(sock, (buffer_t*)readBuffer, sizeof(int32_t), 0);
if (recved == 0) {
// the socket was closed normally
if (recved >= 0 && recved < sizeof(int32_t)) {
// too little data for readSize or the socket was closed normally (when 0 bytes were read)
kill();
return;
} else if (!SOCKETERROR(recved)) {
// we got our packet size!!!!
readSize = *((int32_t*)readBuffer);
@@ -264,11 +270,12 @@ void CNSocket::step() {
}
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);
if (recved == 0) {
// the socket was closed normally
kill();
return;
} else if (!SOCKETERROR(recved))
readBufferIndex += recved;
else if (OF_ERRNO != OF_EWOULD) {
@@ -411,9 +418,9 @@ void CNServer::addPollFD(SOCKET s) {
fds.push_back({s, POLLIN});
}
void CNServer::removePollFD(int i) {
void CNServer::removePollFD(int fd) {
auto it = fds.begin();
while (it != fds.end() && it->fd != fds[i].fd)
while (it != fds.end() && it->fd != fd)
it++;
assert(it != fds.end());
@@ -458,7 +465,7 @@ void CNServer::start() {
if (!setSockNonblocking(sock, newConnectionSocket))
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);
@@ -473,10 +480,15 @@ void CNServer::start() {
} else {
std::lock_guard<std::mutex> lock(activeCrit); // protect operations on connections
// halt packet handling if server is shutting down
if (!active)
return;
// player sockets
if (connections.find(fds[i].fd) == connections.end()) {
std::cout << "[WARN] Event on non-existant socket?" << std::endl;
continue; // just to be safe
std::cout << "[FATAL] Event on non-existent socket: " << fds[i].fd << std::endl;
assert(0);
/* not reached */
}
CNSocket* cSock = connections[fds[i].fd];
@@ -485,22 +497,29 @@ void CNServer::start() {
if (fds[i].revents & ~POLLIN)
cSock->kill();
if (cSock->isAlive()) {
if (cSock->isAlive())
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();
// 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

@@ -93,7 +93,7 @@ inline constexpr bool isOutboundPacketID(uint32_t id) {
// overflow-safe validation of variable-length packets
// for outbound packets
inline constexpr bool validOutVarPacket(size_t base, size_t npayloads, size_t plsize) {
inline constexpr bool validOutVarPacket(size_t base, int32_t npayloads, size_t plsize) {
// check for multiplication overflow
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
return false;
@@ -110,7 +110,7 @@ inline constexpr bool validOutVarPacket(size_t base, size_t npayloads, size_t pl
}
// for inbound packets
inline constexpr bool validInVarPacket(size_t base, size_t npayloads, size_t plsize, size_t datasize) {
inline constexpr bool validInVarPacket(size_t base, int32_t npayloads, size_t plsize, size_t datasize) {
// check for multiplication overflow
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
return false;
@@ -208,15 +208,15 @@ public:
};
class CNServer;
typedef void (*TimerHandler)(CNServer* serv, time_t time);
typedef void (*TimerHandler)(CNServer* serv, int64_t time);
// timer struct
struct TimerEvent {
TimerHandler handlr;
time_t delta; // time to be added to the current time on reset
time_t scheduledEvent; // time to call handlr()
int64_t delta; // time to be added to the current time on reset
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;
}
};
@@ -230,6 +230,7 @@ protected:
const size_t STARTFDSCOUNT = 8; // number of initial PollFD slots
std::vector<PollFD> fds;
std::string serverType = "invalid";
SOCKET sock;
uint16_t port;
socklen_t addressSize;

View File

@@ -24,7 +24,7 @@ LoginMetadata* CNShared::getLoginMetadata(int64_t sk) {
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);
auto it = logins.begin();
@@ -32,7 +32,7 @@ void CNShared::pruneLoginMetadata(CNServer *serv, time_t currTime) {
auto& sk = it->first;
auto& lm = it->second;
if (lm->timestamp + CNSHARED_TIMEOUT > currTime) {
if (currTime > lm->timestamp + CNSHARED_TIMEOUT) {
std::cout << "[WARN] Pruning hung connection attempt" << std::endl;
// deallocate object and remove map entry

View File

@@ -11,19 +11,19 @@
#include "Player.hpp"
/*
* Connecions time out after 15 minutes, checked every 30 seconds.
* Connecions time out after 5 minutes, checked every 30 seconds.
*/
#define CNSHARED_TIMEOUT 900000
#define CNSHARED_TIMEOUT 300000
#define CNSHARED_PERIOD 30000
struct LoginMetadata {
uint64_t FEKey;
Player plr;
time_t timestamp;
int32_t playerId;
int64_t timestamp;
};
namespace CNShared {
void storeLoginMetadata(int64_t sk, LoginMetadata *lm);
LoginMetadata* getLoginMetadata(int64_t sk);
void pruneLoginMetadata(CNServer *serv, time_t currTime);
void pruneLoginMetadata(CNServer *serv, int64_t currTime);
}

View File

@@ -23,7 +23,6 @@
#include <string>
#include <locale>
#include <codecvt>
#include <tuple>
// yes this is ugly, but this is needed to zero out the memory so we don't have random stackdata in our structs.
#define INITSTRUCT(T, x) T x; \
@@ -46,8 +45,8 @@
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
time_t getTime();
time_t getTimestamp();
int64_t getTime();
int64_t getTimestamp();
void terminate(int);
// The PROTOCOL_VERSION definition is defined by the build system.

View File

@@ -10,40 +10,18 @@ const float CN_EP_RANK_4 = 0.3f;
const float CN_EP_RANK_5 = 0.29f;
// methods of finding players for GM commands
enum class eCN_GM_TargetSearchBy {
PC_ID, // player id
PC_Name, // firstname, lastname
PC_UID // account id
enum eCN_GM_TargetSearchBy {
eCN_GM_TargetSearchBy__PC_ID, // player id
eCN_GM_TargetSearchBy__PC_Name, // firstname, lastname
eCN_GM_TargetSearchBy__PC_UID // account id
};
enum class eCN_GM_TeleportType {
XYZ,
MapXYZ,
MyLocation,
SomeoneLocation,
Unstick
};
enum class eTaskTypeProperty {
None = -1,
Talk = 1,
GotoLocation = 2,
UseItems = 3,
Delivery = 4,
Defeat = 5,
EscortDefence = 6,
Max = 7
};
enum class ePCRegenType {
None,
Xcom,
Here,
HereByPhoenix,
HereByPhoenixGroup,
Unstick,
HereByPhoenixItem,
End
enum eCN_GM_TeleportType {
eCN_GM_TeleportMapType__XYZ,
eCN_GM_TeleportMapType__MapXYZ,
eCN_GM_TeleportMapType__MyLocation,
eCN_GM_TeleportMapType__SomeoneLocation,
eCN_GM_TeleportMapType__Unstick
};
// nano powers
@@ -118,27 +96,6 @@ enum {
ECSTB__END = 26,
};
enum {
ETBU_NONE = 0,
ETBU_ADD = 1,
ETBU_DEL = 2,
ETBU_CHANGE = 3,
ETBU__END = 4,
};
enum {
ETBT_NONE = 0,
ETBT_NANO = 1,
ETBT_GROUPNANO = 2,
ETBT_SHINY = 3,
ETBT_LANDEFFECT = 4,
ETBT_ITEM = 5,
ETBT_CASHITEM = 6,
ETBT__END = 7,
ETBT_SKILL = 1,
ETBT_GROUPSKILL = 2
};
enum {
SUCC = 1,
FAIL = 0,

View File

@@ -47,8 +47,8 @@ struct PacketDesc {
* really should.
*/
struct sGM_PVPTarget {
uint32_t iID;
uint32_t eCT;
uint32_t iID;
};
struct sSkillResult_Leech {

View File

@@ -13,7 +13,7 @@ namespace Database {
int AccountID;
std::string Password;
int Selected;
time_t BannedUntil;
int64_t BannedUntil;
std::string BanReason;
};
@@ -41,6 +41,7 @@ namespace Database {
uint64_t Timestamp;
};
void init();
void open();
void close();
@@ -52,7 +53,8 @@ namespace Database {
bool banPlayer(int playerId, std::string& reason);
bool unbanPlayer(int playerId);
void updateSelected(int accountId, int playerId);
void updateSelected(int accountId, int slot);
void updateSelectedByPlayerId(int accountId, int playerId);
bool validateCharacter(int characterID, int userID);
bool isNameFree(std::string firstName, std::string lastName);
@@ -78,7 +80,9 @@ namespace Database {
// getting players
void getPlayer(Player* plr, int id);
bool _updatePlayer(Player *player);
void updatePlayer(Player *player);
void commitTrade(Player *plr1, Player *plr2);
// buddies
int getNumBuddies(Player* player);
@@ -98,7 +102,7 @@ namespace Database {
void deleteEmailAttachments(int playerID, int index, int slot);
void deleteEmails(int playerID, int64_t* indices);
int getNextEmailIndex(int playerID);
bool sendEmail(EmailData* data, std::vector<sItemBase> attachments);
bool sendEmail(EmailData* data, std::vector<sItemBase> attachments, Player *sender);
// racing
RaceRanking getTopRaceRanking(int epID, int playerID);

View File

@@ -152,7 +152,8 @@ void Database::updateEmailContent(EmailData* data) {
sqlite3_step(stmt);
int attachmentsCount = sqlite3_column_int(stmt, 0);
data->ItemFlag = (data->Taros > 0 || attachmentsCount > 0) ? 1 : 0; // set attachment flag dynamically
// set attachment flag dynamically
data->ItemFlag = (data->Taros > 0 || attachmentsCount > 0) ? 1 : 0;
sqlite3_finalize(stmt);
@@ -265,7 +266,7 @@ int Database::getNextEmailIndex(int playerID) {
return (index > 0 ? index + 1 : 1);
}
bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments) {
bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments, Player *sender) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
@@ -330,6 +331,13 @@ bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments) {
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
if (!_updatePlayer(sender)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return false;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
return true;
}

View File

@@ -236,7 +236,20 @@ static int getTableSize(std::string tableName) {
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() {
// XXX: move locks here
int rc = sqlite3_open(settings::DBPATH.c_str(), &db);
if (rc != SQLITE_OK) {

View File

@@ -3,6 +3,15 @@
#include "db/Database.hpp"
#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 sqlite3 *db;

View File

@@ -79,6 +79,29 @@ void Database::updateSelected(int accountId, int slot) {
std::cout << "[WARN] Database fail on updateSelected(): " << sqlite3_errmsg(db) << std::endl;
}
void Database::updateSelectedByPlayerId(int accountId, int32_t playerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
UPDATE Accounts SET
Selected = p.Slot,
LastLogin = (strftime('%s', 'now'))
FROM (SELECT Slot From Players WHERE PlayerId = ?) AS p
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
sqlite3_bind_int(stmt, 2, accountId);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on updateSelectedByPlayerId(): " << sqlite3_errmsg(db) << std::endl;
}
bool Database::validateCharacter(int characterID, int userID) {
std::lock_guard<std::mutex> lock(dbCrit);

View File

@@ -285,11 +285,13 @@ void Database::getPlayer(Player* plr, int id) {
sqlite3_finalize(stmt);
}
void Database::updatePlayer(Player *player) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
/*
* Low-level function to save a player to DB.
* Must be run in a SQL transaction and with dbCrit locked.
* The caller manages the transacstion, so if this function returns false,
* the caller must roll it back.
*/
bool Database::_updatePlayer(Player *player) {
const char* sql = R"(
UPDATE Players
SET
@@ -336,10 +338,8 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 21, player->iID);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
return false;
}
sqlite3_finalize(stmt);
@@ -375,10 +375,8 @@ void Database::updatePlayer(Player *player) {
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
@@ -395,10 +393,8 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 6, player->Inven[i].iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
@@ -415,10 +411,8 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 6, player->Bank[i].iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
@@ -451,10 +445,8 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 4, player->QInven[i].iID);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
@@ -487,10 +479,8 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 4, player->Nanos[i].iStamina);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
@@ -524,14 +514,47 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 5, player->RemainingNPCCount[i][2]);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return true;
}
void Database::updatePlayer(Player *player) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
if (!_updatePlayer(player)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
}
void Database::commitTrade(Player *plr1, Player *plr2) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
if (!_updatePlayer(plr1)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
if (!_updatePlayer(plr2)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
}

View File

@@ -64,7 +64,7 @@ void terminate(int arg) {
}
#ifdef _WIN32
static BOOL winTerminate(DWORD arg) {
static BOOL WINAPI winTerminate(DWORD arg) {
terminate(0);
return FALSE;
}
@@ -98,6 +98,9 @@ void initsignals() {
}
int main() {
std::cout << "[INFO] OpenFusion v" GIT_VERSION << std::endl;
std::cout << "[INFO] Protocol version: " << PROTOCOL_VERSION << std::endl;
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
@@ -105,15 +108,15 @@ int main() {
exit(EXIT_FAILURE);
}
#endif
initsignals();
settings::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;
Database::init();
Rand::init(getTime());
TableData::init();
std::cout << "[INFO] Intializing Packet Managers..." << std::endl;
PlayerManager::init();
PlayerMovement::init();
BuiltinCommands::init();
@@ -132,9 +135,10 @@ int main() {
Email::init();
Groups::init();
Racing::init();
Database::open();
Trading::init();
Database::open();
switch (settings::EVENTMODE) {
case 0: break; // no event
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();
}
time_t getTime() {
int64_t getTime() {
using namespace std::chrono;
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
time_t getTimestamp() {
int64_t getTimestamp() {
using namespace std::chrono;
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

View File

@@ -153,16 +153,18 @@ static sock_filter filter[] = {
ALLOW_SYSCALL(read),
ALLOW_SYSCALL(write),
ALLOW_SYSCALL(close),
#if __NR_stat
#ifdef __NR_stat
ALLOW_SYSCALL(stat),
#endif
ALLOW_SYSCALL(fstat),
#ifdef __NR_newfstatat
ALLOW_SYSCALL(newfstatat),
#endif
ALLOW_SYSCALL(fsync), // maybe
#if __NR_creat
#ifdef __NR_creat
ALLOW_SYSCALL(creat), // maybe; for DB journal
#endif
#if __NR_unlink
#ifdef __NR_unlink
ALLOW_SYSCALL(unlink), // for DB journal
#endif
ALLOW_SYSCALL(lseek), // musl-libc; alt DB
@@ -193,6 +195,9 @@ static sock_filter filter[] = {
ALLOW_SYSCALL(exit_group),
ALLOW_SYSCALL(rt_sigprocmask), // musl-libc
ALLOW_SYSCALL(clock_nanosleep), // gets called very rarely
#ifdef __NR_rseq
ALLOW_SYSCALL(rseq),
#endif
// to crash properly on SIGSEGV
DENY_SYSCALL_ERRNO(tgkill, EPERM),
@@ -275,7 +280,7 @@ static sock_filter filter[] = {
#endif
// AArch64 (ARM64)
#if __NR_unlinkat
#ifdef __NR_unlinkat
ALLOW_SYSCALL(unlinkat),
#endif
#ifdef __NR_fstatat64

View File

@@ -1,18 +1,17 @@
#include "servers/CNLoginServer.hpp"
#include "core/CNShared.hpp"
#include "db/Database.hpp"
#include "bcrypt/BCrypt.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "settings.hpp"
#include <regex>
#include "bcrypt/BCrypt.hpp"
#include "settings.hpp"
std::map<CNSocket*, CNLoginData> CNLoginServer::loginSessions;
CNLoginServer::CNLoginServer(uint16_t p) {
serverType = "login";
port = p;
pHandler = &CNLoginServer::handlePacket;
init();
@@ -210,9 +209,12 @@ void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
// send the resp in with original key
sock->sendPacket(resp, P_LS2CL_REP_LOGIN_SUCC);
uint64_t defaultKey;
memcpy(&defaultKey, CNSocketEncryption::defaultKey, sizeof(defaultKey));
// update keys
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(
std::cout << "Login Server: Login success. Welcome " << userLogin << " [" << loginSessions[sock].userID << "]" << std::endl;
@@ -473,11 +475,7 @@ void CNLoginServer::characterSelect(CNSocket* sock, CNPacketData* data) {
LoginMetadata *lm = new LoginMetadata();
lm->FEKey = sock->getFEKey();
lm->timestamp = getTime();
Database::getPlayer(&lm->plr, selection->iPC_UID);
// this should never happen but for extra safety
if (lm->plr.iID == 0)
return invalidCharacter(sock);
lm->playerId = selection->iPC_UID;
resp.iEnterSerialKey = Rand::cryptoRand();
@@ -487,7 +485,7 @@ void CNLoginServer::characterSelect(CNSocket* sock, CNPacketData* data) {
sock->sendPacket(resp, P_LS2CL_REP_SHARD_SELECT_SUCC);
// update current slot in DB
Database::updateSelected(loginSessions[sock].userID, lm->plr.slot);
Database::updateSelectedByPlayerId(loginSessions[sock].userID, selection->iPC_UID);
}
void CNLoginServer::finishTutorial(CNSocket* sock, CNPacketData* data) {
@@ -576,8 +574,8 @@ void CNLoginServer::killConnection(CNSocket* cns) {
}
void CNLoginServer::onStep() {
time_t currTime = getTime();
static time_t lastCheck = 0;
int64_t currTime = getTime();
static int64_t lastCheck = 0;
if (currTime - lastCheck < 16000)
return;

View File

@@ -1,14 +1,13 @@
#pragma once
#include "core/Core.hpp"
#include "Player.hpp"
#include <map>
struct CNLoginData {
int userID;
time_t lastHeartbeat;
int64_t lastHeartbeat;
};
enum class LoginError {

View File

@@ -1,12 +1,10 @@
#include "servers/CNShardServer.hpp"
#include "core/Core.hpp"
#include "core/CNShared.hpp"
#include "db/Database.hpp"
#include "servers/Monitor.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "MobAI.hpp"
#include "core/CNShared.hpp"
#include "settings.hpp"
#include "TableData.hpp" // for flush()
@@ -18,6 +16,7 @@ std::map<uint32_t, PacketHandler> CNShardServer::ShardPackets;
std::list<TimerEvent> CNShardServer::Timers;
CNShardServer::CNShardServer(uint16_t p) {
serverType = "shard";
port = p;
pHandler = &CNShardServer::handlePacket;
REGISTER_SHARD_TIMER(keepAliveTimer, 4000);
@@ -60,7 +59,7 @@ void CNShardServer::handlePacket(CNSocket* sock, CNPacketData* data) {
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) {
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
@@ -73,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())
return;
@@ -115,7 +114,11 @@ void CNShardServer::kill() {
}
void CNShardServer::onStep() {
time_t currTime = getTime();
int64_t currTime = getTime();
// do not evaluate timers if the server is shutting down
if (!active)
return;
for (TimerEvent& event : Timers) {
if (event.scheduledEvent == 0) {

View File

@@ -3,18 +3,16 @@
#include "core/Core.hpp"
#include <map>
#include <list>
#define REGISTER_SHARD_PACKET(pactype, handlr) CNShardServer::ShardPackets[pactype] = handlr;
#define REGISTER_SHARD_TIMER(handlr, delta) CNShardServer::Timers.push_back(TimerEvent(handlr, delta));
#define MS_PER_PLAYER_TICK 500
class CNShardServer : public CNServer {
private:
static void handlePacket(CNSocket* sock, CNPacketData* data);
static void keepAliveTimer(CNServer*, time_t);
static void periodicSaveTimer(CNServer* serv, time_t currTime);
static void keepAliveTimer(CNServer*, int64_t);
static void periodicSaveTimer(CNServer* serv, int64_t currTime);
public:
static std::map<uint32_t, PacketHandler> ShardPackets;

View File

@@ -1,10 +1,8 @@
#include "servers/Monitor.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Chat.hpp"
#include "Email.hpp"
#include "servers/Monitor.hpp"
#include "settings.hpp"
#include <cstdio>
@@ -74,7 +72,7 @@ static int process_email(char *buff, std::string email) {
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);
char buff[BUFSIZE];
int n;

View File

@@ -2,6 +2,9 @@
#include "core/Core.hpp"
#include <list>
#include <mutex>
namespace Monitor {
SOCKET init();
bool acceptConnection(SOCKET, uint16_t);

View File

@@ -1,10 +1,9 @@
#include <iostream>
#include "settings.hpp"
#include "core/CNStructs.hpp" // so we get the ACADEMY definition
#include "INIReader.hpp"
#include <iostream>
// so we get the ACADEMY definition
#include "core/CNStructs.hpp"
// defaults :)
int settings::VERBOSITY = 1;
@@ -18,9 +17,10 @@ int settings::DBSAVEINTERVAL = 240;
int settings::SHARDPORT = 23001;
std::string settings::SHARDSERVERIP = "127.0.0.1";
bool settings::LOCALHOSTWORKAROUND = true;
time_t settings::TIMEOUT = 60000;
int64_t settings::TIMEOUT = 60000;
int settings::VIEWDISTANCE = 25600;
bool settings::SIMULATEMOBS = true;
bool settings::ANTICHEAT = true;
// default spawn point
#ifndef ACADEMY
@@ -109,6 +109,7 @@ void settings::init() {
ACCLEVEL = reader.GetInteger("shard", "accountlevel", ACCLEVEL);
EVENTMODE = reader.GetInteger("shard", "eventmode", EVENTMODE);
DISABLEFIRSTUSEFLAG = reader.GetBoolean("shard", "disablefirstuseflag", DISABLEFIRSTUSEFLAG);
ANTICHEAT = reader.GetBoolean("shard", "anticheat", ANTICHEAT);
MONITORENABLED = reader.GetBoolean("monitor", "enabled", MONITORENABLED);
MONITORPORT = reader.GetInteger("monitor", "port", MONITORPORT);
MONITORINTERVAL = reader.GetInteger("monitor", "interval", MONITORINTERVAL);

View File

@@ -1,7 +1,5 @@
#pragma once
#include <string>
namespace settings {
extern int VERBOSITY;
extern bool SANDBOX;
@@ -12,7 +10,8 @@ namespace settings {
extern int SHARDPORT;
extern std::string SHARDSERVERIP;
extern bool LOCALHOSTWORKAROUND;
extern time_t TIMEOUT;
extern bool ANTICHEAT;
extern int64_t TIMEOUT;
extern int VIEWDISTANCE;
extern bool SIMULATEMOBS;
extern int SPAWN_X;

2
tdata

Submodule tdata updated: 8230fb8649...cc65dbb402

2
vendor/JSON.hpp vendored
View File

@@ -27,8 +27,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#pragma once
#ifndef INCLUDE_NLOHMANN_JSON_HPP_
#define INCLUDE_NLOHMANN_JSON_HPP_

View File

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