4 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
88 changed files with 10391 additions and 10599 deletions

1
.dockerignore Normal file
View File

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

View File

@@ -9,13 +9,12 @@ on:
- CMakeLists.txt
- Makefile
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
types: ready_for_review
paths:
- src/**
- vendor/**
- CMakeLists.txt
- Makefile
workflow_dispatch:
jobs:
ubuntu-build:
@@ -52,9 +51,9 @@ jobs:
Copy-Item -Path "config.ini" -Destination "bin"
shell: pwsh
- name: Upload build artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
name: 'ubuntu20_04-bin-x64-${{ env.SHORT_SHA }}'
path: bin
windows-build:
@@ -106,14 +105,14 @@ jobs:
}
shell: pwsh
- name: Upload build artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: 'windows-vs2019-bin-x64-${{ env.SHORT_SHA }}'
path: bin
copy-artifacts:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: [windows-build, ubuntu-build]
env:
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}
@@ -127,7 +126,7 @@ jobs:
GITDESC=$(git describe --tags)
mkdir $GITDESC
echo "ARTDIR=$GITDESC" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
with:
path: ${{ env.ARTDIR }}
- name: Upload artifacts

View File

@@ -1,38 +0,0 @@
name: Push Docker Image
on:
release:
types: [published]
jobs:
push-docker-image:
name: Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
platforms:
- linux/amd64
- linux/arm64
steps:
- uses: actions/checkout@v4
- name: Retrieve major version
uses: winterjung/split@v2
id: split
with:
msg: ${{ github.ref_name }}
separator: .
- name: Log in to registry
uses: docker/login-action@v3
with:
password: ${{ secrets.DOCKERHUB_TOKEN }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
- name: Build and push the Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: ${{ matrix.platforms }}
push: true
tags: ${{ secrets.DOCKERHUB_REPOSITORY }}:${{ github.ref_name }},${{ secrets.DOCKERHUB_REPOSITORY }}:${{ steps.split.outputs._0 }},${{ secrets.DOCKERHUB_REPOSITORY }}:latest

2
.gitignore vendored
View File

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

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

View File

@@ -26,7 +26,7 @@ Both are pretty short reads and following them will get you up to speed with bra
I will now cover a few examples of the complications people have encountered contributing to this project, how to understand them, overcome them and henceforth avoid them.
### Dirty pull requests
## Dirty pull requests
Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge.
These are generally either:
@@ -44,7 +44,7 @@ If you read the above links, you'll note that this isn't exactly a perfect solut
The obvious issue, then, is that the people submitting dirty PRs are the exact people who don't *know* how to rebase their fork to continue submitting their work cleanly.
So they end up creating countless merge commits when pulling upstream on top of their own incompatible histories, and then submitting those merge commits in their PRs and the cycle continues.
### The details
## The details
A git commit is uniquely identified by its SHA1 hash.
Its hash is generated from its contents, as well as the hash(es) of its parent commit(s).
@@ -52,7 +52,7 @@ Its hash is generated from its contents, as well as the hash(es) of its parent c
That means that even if two commits are exactly the same in terms of content, they're not the same commit if their parent commits differ (ex. if they've been rebased).
So if you keep issuing `git pull`s after upstream has merged a rebased version of your PR, you will never re-synchronize with it, and will instead construct an alternate history polluted by pointless merge commits.
### The solution
## The solution
If you already have a messed-up fork and you have no changes on it that you're afraid to lose, the solution is simple:
@@ -67,7 +67,7 @@ If you do have some committed changes that haven't yet been merged upstream, you
If you do end up messing something up, don't worry, it most likely isn't really lost and `git reflog` is your friend.
(You can checkout an arbitrary commit, and make it into its own branch with `git checkout -b BRANCH` or set a pre-exisitng branch to it with `git reset --hard COMMIT`)
### Avoiding the problem
## Avoiding the problem
When working on a changeset you want to submit back upstream, don't do it on the main branch.
Create a work branch just for your changeset with `git checkout -b work`.
@@ -81,34 +81,3 @@ That way you can always keep master in sync with upstream with `git pull --ff-on
Creating new branches for the rebase isn't strictly necessary since you can always return a branch to its previous state with `git reflog`.
For moving uncommited changes around between branches, `git stash` is a real blessing.
## Code guidelines
Alright, you're up to speed on Git and ready to go. Here are a few specific code guidelines to try and follow:
### Match the styling
Pretty straightforward, make sure your code looks similar to the code around it. Match whitespacing, bracket styling, variable naming conventions, etc.
### Prefer short-circuiting
To minimize branching complexity (as this makes the code hard to read), we prefer to keep the number of `if-else` statements as low as possible. One easy way to achieve this is by doing an early return after branching into an `if` block and then writing the code for the other path outside the block entirely. You can find examples of this in practically every source file. Note that in a few select situations, this might actually make your code less elegant, which is why this isn't a strict rule. Lean towards short-circuiting and use your better judgement.
### Follow the include convention
This one matters a lot as it can cause cyclic dependencies and other code-breaking issues.
FOR HEADER FILES (.hpp):
- everything you use IN THE HEADER must be EXPLICITLY INCLUDED with the exception of things that fall under Core.hpp
- you may NOT include ANYTHING ELSE
FOR SOURCE FILES (.cpp):
- you can #include whatever you want as long as the partner header is included first
- anything that gets included by another include is fair game
- redundant includes are ok because they'll be harmless AS LONG AS our header files stay lean.
The point of this is NOT to optimize the number of includes used all around or make things more efficient necessarily. it's to improve readability & coherence and make it easier to avoid cyclical issues.
## When in doubt, ask
If you still have questions that were not answered here, feel free to ping a dev in the Discord server or on GitHub.

View File

@@ -1,37 +1,21 @@
# build
FROM alpine:3 as build
FROM debian:latest
WORKDIR /usr/src/app
RUN apk update && apk upgrade && apk add \
linux-headers \
RUN apt-get -y update && apt-get install -y \
git \
clang18 \
clang \
make \
sqlite-dev
libsqlite3-dev
COPY src ./src
COPY vendor ./vendor
COPY .git ./.git
COPY Makefile CMakeLists.txt version.h.in ./
RUN sed -i 's/^CC=clang$/&-18/' Makefile
RUN sed -i 's/^CXX=clang++$/&-18/' Makefile
COPY . ./
RUN make -j8
# prod
FROM alpine:3
# tabledata should be copied from the host;
# clone it there before building the container
#RUN git submodule update --init --recursive
WORKDIR /usr/src/app
CMD ["./bin/fusion"]
RUN apk update && apk upgrade && apk add \
libstdc++ \
sqlite-dev
COPY --from=build /usr/src/app/bin/fusion /bin/fusion
COPY sql ./sql
CMD ["/bin/fusion"]
LABEL Name=openfusion Version=0.0.2
LABEL Name=openfusion Version=0.0.1

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2024 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\
@@ -95,7 +94,8 @@ CXXHDR=\
vendor/bcrypt/BCrypt.hpp\
vendor/INIReader.hpp\
vendor/JSON.hpp\
src/Buffs.hpp\
vendor/INIReader.hpp\
vendor/JSON.hpp\
src/Chat.hpp\
src/CustomCommands.hpp\
src/Entities.hpp\

View File

@@ -13,13 +13,13 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
### Getting Started
#### Method A: Installer (Easiest)
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5.2/OpenFusionClient-1.5.2-Installer.exe) - choose to run the file.
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4-Installer.exe) - choose to run the file.
2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
3. 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.
4. 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 3.
#### Method B: Standalone .zip file
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5.2/OpenFusionClient-1.5.2.zip).
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4.zip).
2. Extract it to a folder of your choice. Note: if you are upgrading from an older version, it is preferable to start with a fresh folder rather than overwriting a previous install.
3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
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.
@@ -28,7 +28,8 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
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.5-Original.zip` or `OpenFusionServer-1.5-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.5.2).
1. Grab `OpenFusionServer-1.4-original.zip` or `OpenFusionServer-1.4-academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.4).
2. Extract it to a folder of your choice, then run `winfusion.exe` (Windows) or `fusion` (Linux) to start the server.
3. Add a new server to the client's list:
1. For Description, enter anything you want. This is what will show up in the server list.
@@ -53,7 +54,10 @@ FusionFall consists of the following components:
The original game made use of the player's actual web browser to launch the game, but since then the NPAPI plugin interface the game relied on has been deprecated and is no longer available in most modern browsers. Both Retro and OpenFusion get around this issue by distributing an older version of Electron, a software package that is essentially a specialized web browser.
The browser/Electron client opens a web page with an `<embed>` tag of the appropriate MIME type, where the `src` param is the address of the game's `.unity3d` entrypoint. This triggers the browser to load an NPAPI plugin that handles said MIME type, in this case the Unity Web Player.
The browser/Electron client opens a web page with an `<embed>` tag of MIME type `application/vnd.unity`, where the `src` param is the address of the game's `.unity3d` entrypoint.
This triggers the browser to load an NPAPI plugin that handles this MIME type, the Unity Web Player, which the browser looks for in `C:\Users\%USERNAME%\AppData\LocalLow\Unity\WebPlayer`.
The Web Player was previously copied there by `installUnity.bat`.
Note that the version of the web player distributed with OpenFusion expects a standard `UnityWeb` magic number for all assets, instead of Retro's modified `streamed` magic number.
This will potentially become relevant later, as people start experimenting and mixing and matching versions.
@@ -62,7 +66,7 @@ The web player will execute the game code, which will request the following file
`/assetInfo.php` contains the address from which to fetch the rest of the game's assets (the "dongresources").
Normally those would be hosted on the same web server as the gateway, but the OpenFusion distribution (in it's default configuration) doesn't use a web server at all!
It instead loads the web pages locally using the `file://` schema, and fetches the game's assets from a standard web server.
It loads the web pages locally using the `file://` schema, and fetches the game's assets from Turner's CDN (which is still hosting them to this day!).
`/loginInfo.php` contains the IP:port pair of the FusionFall login server, which the client will connect to. This login server drives the client while it's in the Character Selection menu, as well as Character Creation and the Tutorial.
@@ -98,13 +102,26 @@ If you'd like to contribute to this project, please read [CONTRIBUTING.md](CONTR
## Gameplay
The goal of the project is to faithfully recreate the game as it was at the time of the targeted build.
While most features are implemented and the game is playable start to finish, there may be missing functionality or bugs present.
The server is not yet complete, however, and some functionality is still missing.
Depending on the server configuration, you'll have access to certain commands.
Because the server is still in development, ordinary players are allowed access to a few admin commands:
For the public servers: Original has item spawning, the ability to set player speed/jump height, and teleportation enabled (default account level 50).
Meanwhile the Academy server is more meant for legitimate playthroughs (default account level 99).
![](res/sane_upsell.png)
When hosting a local server, you will have access to all commands by default (account level 1).
### Movement commands
* A `/speed` of around 2400 or 3000 is nice.
* A `/jump` of about 50 will send you soaring
* [This map](res/dong_number_map.png) (credit to Danny O) is useful for `/warp` coordinates.
* `/goto` is useful for more precise teleportation (ie. for getting into Infected Zones, etc.).
For a list of available commands, see [this wiki page](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).
### Item commands
* `/itemN [type] [itemId] [amount]`
(Refer to the [item list](https://docs.google.com/spreadsheets/d/1mpoJ9iTHl_xLI4wQ_9UvIDYNcsDYscdkyaGizs43TCg/))
### Nano commands
* `/nano [id] (1-36)`
* `/nano_equip [id] (1-36) [slot] (0-2)`
* `/nano_unequip [slot] (0-2)`
* `/nano_active [slot] (0-2)`
### A full list of commands can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).

View File

@@ -17,10 +17,6 @@ acceptallcustomnames=true
# should attempts to log into non-existent accounts
# automatically create them?
autocreateaccounts=true
# list of supported authentication methods (comma-separated)
# password = allow login type 1 with plaintext passwords
# cookie = allow login type 2 with one-shot auth cookies
authmethods=password
# how often should everything be flushed to the database?
# the default is 4 minutes
dbsaveinterval=240
@@ -70,9 +66,6 @@ motd=Welcome to OpenFusion!
# location of the database
#dbpath=database.db
# should there be a score cap for infected zone races?
#izracescorecapped=true
# should tutorial flags be disabled off the bat?
disablefirstuseflag=true

View File

@@ -6,10 +6,6 @@ services:
build:
context: .
dockerfile: ./Dockerfile
volumes:
- ./config.ini:/usr/src/app/config.ini
- ./database.db:/usr/src/app/database.db
- ./tdata:/usr/src/app/tdata
ports:
- "23000:23000"
- "23001:23001"

View File

@@ -1,19 +0,0 @@
/*
It is recommended in the SQLite manual to turn off
foreign keys when making schema changes that involve them
*/
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
-- New table to store auth cookies
CREATE TABLE Auth (
AccountID INTEGER NOT NULL,
Cookie TEXT NOT NULL,
Expires INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID)
);
-- Update DB Version
UPDATE Meta SET Value = 5 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -143,7 +143,7 @@ CREATE TABLE IF NOT EXISTS EmailItems (
UNIQUE (PlayerID, MsgIndex, Slot)
);
CREATE TABLE IF NOT EXISTS RaceResults (
CREATE TABLE IF NOT EXISTS RaceResults(
EPID INTEGER NOT NULL,
PlayerID INTEGER NOT NULL,
Score INTEGER NOT NULL,
@@ -153,17 +153,9 @@ CREATE TABLE IF NOT EXISTS RaceResults (
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS RedeemedCodes (
CREATE TABLE IF NOT EXISTS RedeemedCodes(
PlayerID INTEGER NOT NULL,
Code TEXT NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Code)
);
CREATE TABLE IF NOT EXISTS Auth (
AccountID INTEGER NOT NULL,
Cookie TEXT NOT NULL,
Expires INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID)
);
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +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>
#include <assert.h>
struct NanoPower {
int16_t skillType;
int32_t bitFlag;
int16_t timeBuffID;
PowerHandler handler;
const int COMBAT_TICKS_PER_DRAIN_PROC = 2;
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 SkillType {
DAMAGE = 1,
HEAL_HP = 2,
KNOCKDOWN = 3, // uses DamageNDebuff
SLEEP = 4, // uses DamageNDebuff
SNARE = 5, // uses DamageNDebuff
HEAL_STAMINA = 6,
STAMINA_SELF = 7,
STUN = 8, // uses DamageNDebuff
WEAPONSLOW = 9,
JUMP = 10,
RUN = 11,
STEALTH = 12,
SWIM = 13,
MINIMAPENEMY = 14,
MINIMAPTRESURE = 15,
PHOENIX = 16,
PROTECTBATTERY = 17,
PROTECTINFECTION = 18,
REWARDBLOB = 19,
REWARDCASH = 20,
BATTERYDRAIN = 21,
CORRUPTIONATTACK = 22,
INFECTIONDAMAGE = 23,
KNOCKBACK = 24,
FREEDOM = 25,
PHOENIX_GROUP = 26,
RECALL = 27,
RECALL_GROUP = 28,
RETROROCKET_SELF = 29,
BLOODSUCKING = 30,
BOUNDINGBALL = 31,
INVULNERABLE = 32,
NANOSTIMPAK = 33,
RETURNHOMEHEAL = 34,
BUFFHEAL = 35,
EXTRABANK = 36,
CORRUPTIONATTACKWIN = 38,
CORRUPTIONATTACKLOSE = 39,
};
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 SkillEffectTarget {
POINT = 1,
SELF = 2,
CONE = 3,
WEAPON = 4,
AREA_SELF = 5,
AREA_TARGET = 6
};
enum class SkillTargetType {
MOBS = 1,
PLAYERS = 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) {
assert(len <= MAX_SKILLRESULT_SIZE);
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 {
SkillType 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(ICombatant*, SkillData*, int, int32_t*);
int getCSTBFromST(SkillType skillType);
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;
@@ -30,7 +35,7 @@ static bool playerHasBuddyWithID(Player* plr, int buddyID) {
#pragma endregion
// Refresh buddy list
void Buddies::sendBuddyList(CNSocket* sock) {
void Buddies::refreshBuddyList(CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
int buddyCnt = Database::getNumBuddies(plr);
@@ -277,6 +282,15 @@ static void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) {
// Getting buddy state
static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
/*
* If the buddy list wasn't synced a second time yet, sync it.
* Not sure why we have to do it again for the client not to trip up.
*/
if (!plr->buddiesSynced) {
refreshBuddyList(sock);
plr->buddiesSynced = true;
}
INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);

View File

@@ -1,10 +1,12 @@
#pragma once
#include "Player.hpp"
#include "core/Core.hpp"
#include "core/Core.hpp"
namespace Buddies {
void init();
// Buddy list
void sendBuddyList(CNSocket* sock);
void refreshBuddyList(CNSocket* sock);
}

View File

@@ -1,198 +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;
}
EntityRef Buff::getLastSource() {
if(stacks.empty())
return self;
return stacks.back().source;
}
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::timeBuffTick(EntityRef self, Buff* buff) {
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not implemented
Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK, pkt);
pkt.eCT = combatant->getCharType();
pkt.iID = combatant->getID();
pkt.iTB_ID = buff->id;
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
}
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
int32_t eCharType = combatant->getCharType();
pkt.eCT = eCharType == 4 ? 2 : eCharType; // convention not followed by client here
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));
}
void Buffs::tickDrain(EntityRef self, Buff* buff, int mult) {
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not implemented
Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
int damage = combatant->getMaxHP() / 100 * mult;
int dealt = combatant->takeDamage(buff->getLastSource(), damage);
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
pkt->iID = self.id;
pkt->eCT = combatant->getCharType();
pkt->iTB_ID = ECSB_BOUNDINGBALL;
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
drain->iDamage = dealt;
drain->iHP = combatant->getCurrentHP();
drain->eCT = pkt->eCT;
drain->iID = pkt->iID;
NPCManager::sendToViewable(self.getEntity(), (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
}
#pragma endregion

View File

@@ -1,93 +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);
EntityRef getLastSource();
/*
* 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 timeBuffTick(EntityRef self, Buff* buff);
void timeBuffTimeout(EntityRef self);
void tickDrain(EntityRef self, Buff* buff, int mult);
}

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) {
@@ -260,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));
@@ -282,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,331 +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) {
if(!isAlive())
return false;
if(!hasBuff(buffId)) {
buffs[buffId] = new Buff(buffId, getRef(), 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, BuffClass buffClass) {
if(hasBuff(buffId)) {
buffs[buffId]->clear(buffClass);
// buff might not be stale since another buff class might remain
if(buffs[buffId]->isStale()) {
delete buffs[buffId];
buffs.erase(buffId);
}
}
}
void Player::clearBuffs(bool force) {
auto it = buffs.begin();
while(it != buffs.end()) {
Buff* buff = (*it).second;
if(!force) buff->clear();
delete buff;
it = buffs.erase(it);
}
}
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);
if(!isAlive())
break; // unsafe to keep ticking if we're dead
}
}
#pragma endregion
#pragma region CombatNPC
bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) {
if(!isAlive())
return false;
if (this->state != AIState::COMBAT && this->state != AIState::ROAMING)
return false;
if(!hasBuff(buffId)) {
buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack);
return true;
}
buffs[buffId]->updateCallbacks(onUpdate, onTick);
buffs[buffId]->addStack(stack);
return false;
}
Buff* CombatNPC::getBuff(int buffId) {
if(hasBuff(buffId)) {
return buffs[buffId];
}
return nullptr;
}
void CombatNPC::removeBuff(int buffId) {
if(hasBuff(buffId)) {
buffs[buffId]->clear();
delete buffs[buffId];
buffs.erase(buffId);
}
}
void CombatNPC::removeBuff(int buffId, BuffClass buffClass) {
if(hasBuff(buffId)) {
buffs[buffId]->clear(buffClass);
// buff might not be stale since another buff class might remain
if(buffs[buffId]->isStale()) {
delete buffs[buffId];
buffs.erase(buffId);
}
}
}
void CombatNPC::clearBuffs(bool force) {
auto it = buffs.begin();
while(it != buffs.end()) {
Buff* buff = (*it).second;
if(!force) buff->clear();
delete buff;
it = buffs.erase(it);
}
}
bool CombatNPC::hasBuff(int buffId) {
auto buff = buffs.find(buffId);
return buff != buffs.end() && !buff->second->isStale();
}
int CombatNPC::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 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);
}
// trigger special NPCEvents, if applicable
for (NPCEvent& event : NPCManager::NPCEvents)
if (event.triggerState == newState && event.npcType == type)
event.handler(this);
}
#pragma endregion
static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shouldCrit,
bool batteryBoost, int attackerStyle,
int defenderStyle, int difficulty) {
@@ -365,9 +56,9 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
return ret;
}
static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTargets) {
static bool checkRapidFire(CNSocket *sock, int targetCount) {
Player *plr = PlayerManager::getPlayer(sock);
time_t currTime = getTime();
int64_t currTime = getTime();
if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
@@ -377,7 +68,7 @@ static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTarget
plr->lastShot = currTime;
// 3+ targets should never be possible
if (!allowManyTargets && targetCount > 3)
if (targetCount > 3)
plr->suspicionRating += 10001;
// kill the socket when the player is too suspicious
@@ -396,7 +87,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
auto targets = (int32_t*)data->trailers;
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt, false))
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt))
return;
/*
@@ -423,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;
}
@@ -446,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
}
@@ -467,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);
@@ -477,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;
@@ -489,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
@@ -501,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);
@@ -533,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;
@@ -577,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);
@@ -619,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";
@@ -643,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;
}
@@ -667,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);
@@ -837,9 +653,20 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
return;
}
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iTargetCnt, true))
// 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
@@ -869,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;
}
@@ -882,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;
}
@@ -899,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;
@@ -909,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) {
@@ -927,24 +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) {
ICombatant* src = dynamic_cast<ICombatant*>(plr);
int32_t targets[] = { plr->iID };
std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets);
Abilities::useNanoSkill(sock, skill, *nano, affectedCombatants);
}
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;
}
}
@@ -960,20 +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->combatTick() gets called in Player::step
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);
@@ -988,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);
@@ -83,77 +82,7 @@ static void helpCommand(std::string full, std::vector<std::string>& args, CNSock
}
static void accessCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
if (args.size() < 2) {
Chat::sendServerMessage(sock, "Usage: /access <id> [new_level]");
Chat::sendServerMessage(sock, "Use . for id to select yourself");
return;
}
char *tmp;
Player* self = PlayerManager::getPlayer(sock);
int selfAccess = self->accountLevel;
Player* player;
if (args[1].compare(".") == 0) {
player = self;
} else {
int id = std::strtol(args[1].c_str(), &tmp, 10);
if (*tmp) {
Chat::sendServerMessage(sock, "Invalid player ID " + args[1]);
return;
}
player = PlayerManager::getPlayerFromID(id);
if (player == nullptr) {
Chat::sendServerMessage(sock, "Could not find player with ID " + std::to_string(id));
return;
}
// Messing with other players requires a baseline access of 30
if (player != self && selfAccess > 30) {
Chat::sendServerMessage(sock, "Can't check or change other players access levels (insufficient privileges)");
return;
}
}
std::string playerName = PlayerManager::getPlayerName(player);
int currentAccess = player->accountLevel;
if (args.size() < 3) {
// just check
Chat::sendServerMessage(sock, playerName + " has access level " + std::to_string(currentAccess));
return;
}
// Can't change the access level of someone with stronger privileges
// N.B. lower value = stronger privileges
if (currentAccess <= selfAccess) {
Chat::sendServerMessage(sock, "Can't change this player's access level (insufficient privileges)");
return;
}
int newAccess = std::strtol(args[2].c_str(), &tmp, 10);
if (*tmp) {
Chat::sendServerMessage(sock, "Invalid access level " + args[2]);
return;
}
// Can only assign an access level weaker than yours
if (newAccess <= selfAccess) {
Chat::sendServerMessage(sock, "Can only assign privileges weaker than your own");
return;
}
player->accountLevel = newAccess;
// Save to database
int accountId = Database::getAccountIdForPlayer(player->iID);
Database::updateAccountLevel(accountId, newAccess);
std::string msg = "Changed access level for " + playerName + " from " + std::to_string(currentAccess) + " to " + std::to_string(newAccess);
if (newAccess <= 50 && currentAccess > 50)
msg += " (they must log out and back in for some commands to be enabled)";
Chat::sendServerMessage(sock, msg);
Chat::sendServerMessage(sock, "Your access level is " + std::to_string(PlayerManager::getPlayer(sock)->accountLevel));
}
static void populationCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -314,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) {
@@ -340,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];
@@ -365,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;
}
@@ -379,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) {
@@ -395,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();
@@ -427,18 +356,18 @@ 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);
bool isGruntworkNpc = true;
// add a rotation entry to the gruntwork file, unless it's already a gruntwork NPC
if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end()) {
TableData::RunningNPCRotations[npc->id] = angle;
if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) == TableData::RunningMobs.end()) {
TableData::RunningNPCRotations[npc->appearanceData.iNPC_ID] = angle;
isGruntworkNpc = false;
}
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) +
" for " + (isGruntworkNpc ? "gruntwork " : "") + "NPC " + std::to_string(npc->id));
" 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);
@@ -510,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) {
@@ -568,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) {
@@ -602,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);
@@ -679,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;
}
}
@@ -724,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) {
@@ -741,14 +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] 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)));
@@ -763,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();
@@ -774,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;
@@ -1046,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;
}
@@ -1073,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);
@@ -1088,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");
@@ -1127,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);
@@ -1145,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");
@@ -1155,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
@@ -1167,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;
}
@@ -1195,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;
@@ -1214,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
@@ -1241,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);
@@ -1253,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");
@@ -1270,7 +1186,7 @@ static void registerCommand(std::string cmd, int requiredLevel, CommandHandler h
void CustomCommands::init() {
registerCommand("help", 100, helpCommand, "list all unlocked server-side commands");
registerCommand("access", 100, accessCommand, "check or change access levels");
registerCommand("access", 100, accessCommand, "print your access level");
registerCommand("instance", 30, instanceCommand, "print or change your current instance");
registerCommand("mss", 30, mssCommand, "edit Monkey Skyway routes");
registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs");

View File

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

View File

@@ -1,124 +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);
SkillResult result = SkillResult();
SkillData* skill = &Abilities::SkillTable[skillId];
if(skill->drainType == SkillDrainType::PASSIVE) {
// apply buff
if(skill->targetType != SkillTargetType::PLAYERS) {
std::cout << "[WARN] weird skill type for egg " << eggId << " with skill " << skillId << ", should be " << (int)skill->targetType << std::endl;
}
size_t resplen;
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);
sSkillResult_Buff resultBuff{};
resultBuff.eCT = plr->getCharType();
resultBuff.iID = plr->getID();
resultBuff.bProtected = false;
resultBuff.iConditionBitFlag = plr->getCompositeCondition();
result = SkillResult(sizeof(sSkillResult_Buff), &resultBuff);
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 {
int value = plr->getMaxHP() * skill->values[0][0] / 1000;
sSkillResult_Damage resultDamage{};
sSkillResult_Heal_HP resultHeal{};
switch(skill->skillType)
{
case SkillType::DAMAGE:
resultDamage.bProtected = false;
resultDamage.eCT = plr->getCharType();
resultDamage.iID = plr->getID();
resultDamage.iDamage = plr->takeDamage(src, value);
resultDamage.iHP = plr->getCurrentHP();
result = SkillResult(sizeof(sSkillResult_Damage), &resultDamage);
break;
case SkillType::HEAL_HP:
resultHeal.eCT = plr->getCharType();
resultHeal.iID = plr->getID();
resultHeal.iHealHP = plr->heal(src, value);
resultHeal.iHP = plr->getCurrentHP();
result = SkillResult(sizeof(sSkillResult_Heal_HP), &resultHeal);
break;
default:
std::cout << "[WARN] oops, egg with active skill type " << (int)skill->skillType << " unhandled";
return;
}
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
// initialize response struct
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + result.size;
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
auto skillUse = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
pkt->iNPC_ID = eggId;
pkt->iSkillID = skillId;
pkt->eST = (int32_t)skill->skillType;
pkt->iTargetCnt = 1;
if(result.size > 0) {
void* attached = (void*)(pkt + 1);
memcpy(attached, result.payload, result.size);
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;
}
NPCManager::sendToViewable(src.getEntity(), pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
skillUse->iNPC_ID = eggId;
skillUse->iSkillID = skillId;
skillUse->eST = Nanos::SkillTable[skillId].skillType;
skillUse->iTargetCnt = 1;
sock->sendPacket((void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
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});
}
@@ -146,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;
}
@@ -163,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;
@@ -171,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;
@@ -234,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"
@@ -325,13 +325,6 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
std::string logEmail = "[Email] " + PlayerManager::getPlayerName(plr, true) + " (to " + PlayerManager::getPlayerName(&otherPlr, true) + "): <" + email.SubjectLine + ">\n" + email.MsgBody;
std::cout << logEmail << std::endl;
dump.push_back(logEmail);
// notification to recipient if online
CNSocket* recipient = PlayerManager::getSockFromID(pkt->iTo_PCUID);
if (recipient != nullptr)
{
emailUpdateCheck(recipient, nullptr);
}
}
void Email::init() {

View File

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

View File

@@ -1,28 +1,30 @@
#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;
}
EntityRef::EntityRef(int32_t i) {
id = i;
kind = EntityKind::INVALID;
if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end())
kind = NPCManager::NPCs[id]->kind;
assert(NPCManager::NPCs.find(id) != NPCManager::NPCs.end());
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();
@@ -31,38 +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 = 0;
data.iHP = hp;
data.iNPCType = type;
data.iNPC_ID = id;
data.iX = x;
data.iY = y;
data.iZ = z;
return data;
}
sNPCAppearanceData CombatNPC::getAppearanceData() {
sNPCAppearanceData data = BaseNPC::getAppearanceData();
data.iConditionBitFlag = getCompositeCondition();
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);
}
@@ -71,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
};
@@ -81,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);
}
@@ -123,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,39 +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, BuffClass) = 0;
virtual void clearBuffs(bool) = 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;
}
};
/*
@@ -69,104 +74,75 @@ public:
*/
class BaseNPC : public Entity {
public:
int id;
int type;
int hp;
int angle;
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;
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;
virtual 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;
std::unordered_map<int, Buff*> buffs = {};
// 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) {}
CombatNPC(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, int id, int maxHP)
: BaseNPC(angle, iID, t, id), maxHealth(maxHP) {
this->spawnX = spawnX;
this->spawnY = spawnY;
this->spawnZ = spawnZ;
kind = EntityKind::COMBAT_NPC;
stateHandlers[AIState::INACTIVE] = {};
transitionHandlers[AIState::INACTIVE] = {};
virtual void stepAI(int64_t currTime) {
if (_stepAI != nullptr)
_stepAI(this, currTime);
}
virtual sNPCAppearanceData getAppearanceData() override;
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, BuffClass buffClass) override;
virtual void clearBuffs(bool force) 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,186 +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 (group == nullptr)
return;
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 (group == nullptr)
return false;
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) {
if (group == nullptr)
return;
// 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;
@@ -236,89 +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) {
if (group == nullptr)
return;
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]);
auto players = group->filter(EntityKind::PLAYER);
for (EntityRef ref : players) {
ref.sock->sendPacket(buf, type, size);
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) {
if (group == nullptr)
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;
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();
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + plr->groupCnt * sizeof(sPCGroupMemberInfo);
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;
memset(respbuf, 0, resplen);
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));
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::groupKick(Group* group, EntityRef ref) {
if (group == nullptr)
return;
void Groups::groupKickPlayer(Player* plr) {
// if you are the group leader, destroy your own group and kick everybody
if (group->members[0] == ref) {
disbandGroup(group);
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;
}
removeFromGroup(group, ref);
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,37 +1,17 @@
#pragma once
#include "EntityRef.hpp"
#include "Player.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include <vector>
#include <assert.h>
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;
}
EntityRef getLeader() {
assert(members.size() > 0);
return members[0];
}
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;
@@ -416,9 +413,6 @@ static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
if (itemdel->iSlotNum < 0 || itemdel->iSlotNum >= AINVEN_COUNT)
return; // sanity check
resp.eIL = itemdel->eIL;
resp.iSlotNum = itemdel->iSlotNum;
@@ -485,34 +479,30 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
resp->iSlotNum = request->iSlotNum;
resp->RemainItem = gumball;
resp->iTargetCnt = 1;
resp->eST = (int32_t)SkillType::NANOSTIMPAK;
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) {
@@ -765,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;
@@ -780,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;
@@ -832,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;
}
}
@@ -386,9 +387,6 @@ static void taskStart(CNSocket* sock, CNPacketData* data) {
static void taskEnd(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TASK_END* missionData = (sP_CL2FE_REQ_PC_TASK_END*)data->buf;
if (Missions::Tasks.find(missionData->iTaskNum) == Missions::Tasks.end())
return;
TaskData* task = Missions::Tasks[missionData->iTaskNum];
// handle timed mission failure
@@ -401,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;

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +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
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
@@ -47,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 spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(spawnX, spawnY, spawnZ, angle, iID, t, id, d["m_iHP"]),
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;
@@ -62,28 +65,20 @@ struct Mob : public CombatNPC {
idleRange = (int)data["m_iIdleRange"];
level = data["m_iNpcLevel"];
roamX = spawnX;
roamY = spawnY;
roamZ = spawnZ;
roamX = spawnX = x;
roamY = spawnY = y;
roamZ = spawnZ = z;
offsetX = 0;
offsetY = 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
@@ -94,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];
}
@@ -106,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);
}
}
@@ -94,49 +92,20 @@ void NPCManager::sendToViewable(Entity *npc, void *buf, uint32_t type, size_t si
static void npcBarkHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf;
int taskID = req->iMissionTaskID;
// ignore req->iNPC_ID as it is often fixated on a single npc in the region
if (Missions::Tasks.find(taskID) == Missions::Tasks.end()) {
std::cout << "mission task not found: " << taskID << std::endl;
return;
// get bark IDs from task data
TaskData* td = Missions::Tasks[req->iMissionTaskID];
std::vector<int> barks;
for (int i = 0; i < 4; i++) {
if (td->task["m_iHBarkerTextID"][i] != 0) // non-zeroes only
barks.push_back(td->task["m_iHBarkerTextID"][i]);
}
TaskData* td = Missions::Tasks[taskID];
auto& barks = td->task["m_iHBarkerTextID"];
Player* plr = PlayerManager::getPlayer(sock);
std::vector<std::pair<int32_t, int32_t>> npcLines;
for (Chunk* chunk : plr->viewableChunks) {
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
if (ent->kind != EntityKind::SIMPLE_NPC)
continue;
BaseNPC* npc = (BaseNPC*)ent->getEntity();
if (npc->type < 0 || npc->type >= NPCData.size())
continue; // npc unknown ?!
int barkType = NPCData[npc->type]["m_iBarkerType"];
if (barkType < 1 || barkType > 4)
continue; // no barks
int barkID = barks[barkType - 1];
if (barkID == 0)
continue; // no barks
npcLines.push_back(std::make_pair(npc->id, barkID));
}
}
if (npcLines.size() == 0)
return; // totally no barks
auto& [npcID, missionStringID] = npcLines[Rand::rand(npcLines.size())];
if (barks.empty())
return; // no barks
INITSTRUCT(sP_FE2CL_REP_BARKER, resp);
resp.iNPC_ID = npcID;
resp.iMissionStringID = missionStringID;
resp.iNPC_ID = req->iNPC_ID;
resp.iMissionStringID = barks[Rand::rand(barks.size())];
sock->sendPacket(resp, P_FE2CL_REP_BARKER);
}
@@ -151,20 +120,22 @@ static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
}
// type must already be checked and updateNPCPosition() must be called on the result
BaseNPC *NPCManager::summonNPC(int spawnX, int spawnY, int spawnZ, uint64_t instance, int type, bool respawn, bool baseInstance) {
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--;
int team = NPCData[type]["m_iTeam"];
BaseNPC *npc = nullptr;
if (team == 2) {
npc = new Mob(spawnX, spawnY, spawnZ, 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;
@@ -183,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);
}
}
@@ -212,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
@@ -227,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;
@@ -307,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();
@@ -322,55 +291,57 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
return npc;
}
// TODO: Move this to separate file in ai/ subdir when implementing more events
// TODO: Move this to MobAI, possibly
#pragma region NPCEvents
// summon right arm and stage 2 body
static void lordFuseStageTwo(CombatNPC *npc) {
static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc; // adaptium, stun
Player *plr = PlayerManager::getPlayer(sock);
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, oldbody->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->spawnX, newbody->spawnY, newbody->spawnZ,
oldbody->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, oldbody->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->spawnX, arm->spawnY, arm->spawnZ,
oldbody->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
static void lordFuseStageThree(CombatNPC *npc) {
static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc;
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage three" << std::endl;
// Cosmix, Damage Point
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->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->spawnX, newbody->spawnY, newbody->spawnZ,
newbody->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, oldbody->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->spawnX, arm->spawnY, arm->spawnZ,
arm->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 = {
NPCEvent(2466, AIState::DEAD, lordFuseStageTwo),
NPCEvent(2467, AIState::DEAD, lordFuseStageThree),
NPCEvent(2466, ON_KILLED, lordFuseStageTwo),
NPCEvent(2467, ON_KILLED, lordFuseStageThree),
};
#pragma endregion NPCEvents
@@ -379,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
@@ -402,5 +373,5 @@ void NPCManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler);
REGISTER_SHARD_TIMER(step, MS_PER_COMBAT_TICK);
REGISTER_SHARD_TIMER(step, 200);
}

View File

@@ -1,30 +1,36 @@
#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
typedef void (*NPCEventHandler)(CombatNPC*);
enum Trigger {
ON_KILLED,
ON_COMBAT
};
typedef void (*NPCEventHandler)(CNSocket*, BaseNPC*);
struct NPCEvent {
int32_t npcType;
AIState triggerState;
int trigger;
NPCEventHandler handler;
NPCEvent(int32_t t, AIState tr, NPCEventHandler hndlr)
: npcType(t), triggerState(tr), handler(hndlr) {}
NPCEvent(int32_t t, int tr, NPCEventHandler hndlr)
: 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;
@@ -38,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>
@@ -82,21 +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;
ICombatant* src = dynamic_cast<ICombatant*>(plr);
int32_t targets[] = { plr->iID };
std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets);
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
@@ -105,12 +124,12 @@ 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);
}
static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
if (skill == nullptr || skill->iNanoID >= NANO_COUNT || skill->iNanoID < 0)
if (skill->iNanoID >= NANO_COUNT)
return;
Player *plr = PlayerManager::getPlayer(sock);
@@ -192,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;
}
@@ -216,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);
@@ -258,26 +289,28 @@ 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;
)
ICombatant* plrCombatant = dynamic_cast<ICombatant*>(plr);
std::vector<ICombatant*> targetData = Abilities::matchTargets(plrCombatant, skillData, pkt->iTargetCnt, (int32_t*)(pkt + 1));
Abilities::useNanoSkill(sock, skillData, nano, targetData);
std::vector<int> targetData = findTargets(plr, skillID, data);
if (plr->Nanos[plr->activeNano].iStamina <= 0)
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;

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,48 +65,28 @@ 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;
bool unwarpable = false;
bool initialLoadDone = false;
bool buddiesSynced = false;
int64_t buddyIDs[50] = {};
bool isBuddyBlocked[50] = {};
uint64_t iFirstUseFlag[2] = {};
time_t lastHeartbeat = 0;
int64_t lastHeartbeat = 0;
int suspicionRating = 0;
time_t lastShot = 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, BuffClass buffClass) override;
virtual void clearBuffs(bool force) 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>
@@ -36,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);
@@ -66,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;
}
@@ -77,10 +86,7 @@ void PlayerManager::updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, ui
plr->x = X;
plr->y = Y;
plr->z = Z;
if (plr->instanceID != I) {
plr->instanceID = I;
plr->recallInstance = INSTANCE_OVERWORLD;
}
plr->instanceID = I;
if (oldChunk == newChunk)
return; // didn't change chunks
Chunking::updateEntityChunk({sock}, oldChunk, newChunk);
@@ -126,6 +132,24 @@ void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC);
}
if (I != INSTANCE_OVERWORLD) {
INITSTRUCT(sP_FE2CL_INSTANCE_MAP_INFO, pkt);
pkt.iInstanceMapNum = (int32_t)MAPNUM(I); // lower 32 bits are mapnum
if (I != fromInstance // do not retransmit MAP_INFO on recall
&& Racing::EPData.find(pkt.iInstanceMapNum) != Racing::EPData.end()) {
EPInfo* ep = &Racing::EPData[pkt.iInstanceMapNum];
pkt.iEP_ID = ep->EPID;
pkt.iMapCoordX_Min = ep->zoneX * 51200;
pkt.iMapCoordX_Max = (ep->zoneX + 1) * 51200;
pkt.iMapCoordY_Min = ep->zoneY * 51200;
pkt.iMapCoordY_Max = (ep->zoneY + 1) * 51200;
pkt.iMapCoordZ_Min = INT32_MIN;
pkt.iMapCoordZ_Max = INT32_MAX;
}
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
}
INITSTRUCT(sP_FE2CL_REP_PC_GOTO_SUCC, pkt2);
pkt2.iX = X;
pkt2.iY = Y;
@@ -155,21 +179,16 @@ void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z) {
* Nanos the player hasn't unlocked will (and should) be greyed out. Thus, all nanos should be accounted
* for in these packets, even if the player hasn't unlocked them.
*/
static void sendNanoBook(CNSocket *sock, Player *plr, bool resizeOnly) {
static void sendNanoBookSubset(CNSocket *sock) {
#ifdef ACADEMY
Player *plr = getPlayer(sock);
int16_t id = 0;
INITSTRUCT(sP_FE2CL_REP_NANO_BOOK_SUBSET, pkt);
pkt.PCUID = plr->iID;
pkt.bookSize = NANO_COUNT;
if (resizeOnly) {
// triggers nano array resizing without
// actually sending nanos
sock->sendPacket(pkt, P_FE2CL_REP_NANO_BOOK_SUBSET);
return;
}
while (id < NANO_COUNT) {
pkt.elementOffset = id;
@@ -213,11 +232,11 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
Database::getPlayer(plr, lm->playerId);
}
plr->group = nullptr;
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;
@@ -300,21 +319,27 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
sock->setFEKey(lm->FEKey);
sock->setActiveKey(SOCKETKEY_FE); // send all packets using the FE key from now on
// Academy builds receive nanos in a separate packet. An initial one with the size of the
// nano book needs to be sent before PC_ENTER_SUCC so the client can resize its nano arrays,
// and then proper packets with the nanos included must be sent after, while the game is loading.
sendNanoBook(sock, plr, true);
sock->sendPacket(response, P_FE2CL_REP_PC_ENTER_SUCC);
sendNanoBook(sock, plr, false);
// transmit MOTD after entering the game, so the client hopefully changes modes on time
Chat::sendServerMessage(sock, settings::MOTDSTRING);
// 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, plr);
// set player equip stats
Items::setItemStats(plr);
Missions::failInstancedMissions(sock);
sendNanoBookSubset(sock);
// initial buddy sync
Buddies::refreshBuddyList(sock);
for (auto& pair : players)
if (pair.second->notify)
Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(plr) + " has joined.");
@@ -323,20 +348,12 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
delete lm;
}
void PlayerManager::sendToGroup(CNSocket* sock, void* buf, uint32_t type, size_t size) {
Player* plr = getPlayer(sock);
if (plr->group == nullptr)
return;
for(const EntityRef& ref : plr->group->filter(EntityKind::PLAYER))
ref.sock->sendPacket(buf, type, size);
}
void PlayerManager::sendToViewable(CNSocket* sock, void* buf, uint32_t type, size_t size) {
Player* plr = getPlayer(sock);
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);
@@ -359,35 +376,6 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) {
updatePlayerPosition(sock, plr->x, plr->y, plr->z, plr->instanceID, plr->angle);
sock->sendPacket(response, P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC);
if (plr->instanceID != INSTANCE_OVERWORLD) {
INITSTRUCT(sP_FE2CL_INSTANCE_MAP_INFO, pkt);
pkt.iInstanceMapNum = (int32_t)MAPNUM(plr->instanceID); // lower 32 bits are mapnum
if (pkt.iInstanceMapNum != plr->recallInstance // do not retransmit MAP_INFO on recall
&& Racing::EPData.find(pkt.iInstanceMapNum) != Racing::EPData.end()) {
EPInfo* ep = &Racing::EPData[pkt.iInstanceMapNum];
pkt.iEP_ID = ep->EPID;
pkt.iMapCoordX_Min = ep->zoneX * 51200;
pkt.iMapCoordX_Max = (ep->zoneX + 1) * 51200;
pkt.iMapCoordY_Min = ep->zoneY * 51200;
pkt.iMapCoordY_Max = (ep->zoneY + 1) * 51200;
pkt.iMapCoordZ_Min = INT32_MIN;
pkt.iMapCoordZ_Max = INT32_MAX;
}
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
}
if (!plr->initialLoadDone) {
// these should be called only once, but not until after
// first load-in or else the client may ignore the packets
Chat::sendServerMessage(sock, settings::MOTDSTRING); // MOTD
Missions::failInstancedMissions(sock); // auto-fail missions
Buddies::sendBuddyList(sock); // buddy list
Items::checkItemExpire(sock, plr); // vehicle expiration
plr->initialLoadDone = true;
}
}
static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
@@ -418,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;
// 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;
plr->clearBuffs(false);
// 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++) {
@@ -486,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];
@@ -591,7 +579,7 @@ static void setFirstUseFlag(CNSocket* sock, CNPacketData* data) {
std::cout << "[WARN] Client submitted invalid first use flag number?!" << std::endl;
return;
}
if (flag->iFlagCode <= 64)
plr->iFirstUseFlag[0] |= (1ULL << (flag->iFlagCode - 1));
else
@@ -679,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();
@@ -33,7 +34,6 @@ namespace PlayerManager {
CNSocket *getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname);
WarpLocation *getRespawnPoint(Player *plr);
void sendToGroup(CNSocket *sock, void* buf, uint32_t type, size_t size);
void sendToViewable(CNSocket *sock, void* buf, uint32_t type, size_t size);
// TODO: unify this under the new Entity system
@@ -43,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;
@@ -66,7 +64,7 @@ static void racingCancel(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
/*
/*
* This request packet is used for both cancelling the race via the
* NPC at the start, *and* failing the race by running out of time.
* If the latter is to happen, the client disables movement until it
@@ -99,43 +97,35 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
return; // IZ not found
EPInfo& epInfo = EPData[mapNum];
EPRace& epRace = EPRaces[sock];
uint64_t now = getTime() / 1000;
int timeDiff = now - epRace.startTime;
int podsCollected = epRace.collectedRings.size();
int score = std::exp(
(epInfo.podFactor * podsCollected) / epInfo.maxPods
- (epInfo.timeFactor * timeDiff) / epInfo.maxTime
+ epInfo.scaleFactor);
score = (settings::IZRACESCORECAPPED && score > epInfo.maxScore) ? epInfo.maxScore : score;
int fm = (1.0 + std::exp(epInfo.scaleFactor - 1.0) * epInfo.podFactor * podsCollected) / epInfo.maxPods;
int timeDiff = now - EPRaces[sock].startTime;
int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff;
if (score < 0) score = 0; // lol
int fm = score * plr->level * (1.0f / 36) * 0.3f;
// we submit the ranking first...
Database::RaceRanking postRanking = {};
postRanking.EPID = epInfo.EPID;
postRanking.EPID = EPData[mapNum].EPID;
postRanking.PlayerID = plr->iID;
postRanking.RingCount = podsCollected;
postRanking.RingCount = EPRaces[sock].collectedRings.size();
postRanking.Score = score;
postRanking.Time = timeDiff;
postRanking.Timestamp = getTimestamp();
Database::postRaceRanking(postRanking);
// ...then we get the top ranking, which may or may not be what we just submitted
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(epInfo.EPID, plr->iID);
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(EPData[mapNum].EPID, plr->iID);
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
// get rank scores and rewards
std::vector<int>* rankScores = &EPRewards[epInfo.EPID].first;
std::vector<int>* rankRewards = &EPRewards[epInfo.EPID].second;
std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first;
std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second;
// top ranking
int maxRank = rankScores->size() - 1;
int topRank = 0;
while (topRank < maxRank && rankScores->at(topRank) > topRankingPlayer.Score)
while (rankScores->at(topRank) > topRankingPlayer.Score)
topRank++;
resp.iEPTopRank = topRank + 1;
@@ -145,7 +135,7 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
// this ranking
int rank = 0;
while (rank < maxRank && rankScores->at(rank) > postRanking.Score)
while (rankScores->at(rank) > postRanking.Score)
rank++;
resp.iEPRank = rank + 1;

View File

@@ -1,23 +1,17 @@
#pragma once
#include "core/Core.hpp"
#include <map>
#include <vector>
#include <set>
#include "servers/CNShardServer.hpp"
struct EPInfo {
// available through XDT (maxScore may be updated by drops)
int zoneX, zoneY, EPID, maxScore;
// available through drops
int maxTime, maxPods;
double scaleFactor, podFactor, timeFactor;
int zoneX, zoneY, EPID, maxScore, maxTime;
};
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>
@@ -227,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"];
@@ -326,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();
@@ -375,7 +363,7 @@ static void loadPaths(json& pathData, int32_t* nextId) {
Transport::NPCPaths.push_back(pathTemplate);
}
std::cout << "[INFO] Loaded " << Transport::NPCPaths.size() << " NPC paths" << std::endl;
}
catch (const std::exception& err) {
std::cerr << "[FATAL] Malformed paths.json file! Reason:" << err.what() << std::endl;
@@ -584,17 +572,8 @@ static void loadDrops(json& dropData) {
continue;
}
EPInfo& epInfo = Racing::EPData[EPMap];
// max score is specified in the XDT, but can be updated if specified in the drops JSON
epInfo.maxScore = (int)race["ScoreCap"];
// time limit and total pods are not stored in the XDT, so we include it in the drops JSON
epInfo.maxTime = (int)race["TimeLimit"];
epInfo.maxPods = (int)race["TotalPods"];
// IZ-specific calculated constants included in the drops JSON
epInfo.scaleFactor = (double)race["ScaleFactor"];
epInfo.podFactor = (double)race["PodFactor"];
epInfo.timeFactor = (double)race["TimeFactor"];
// time limit isn't stored in the XDT, so we include it in the reward table instead
Racing::EPData[EPMap].maxTime = race["TimeLimit"];
// score cutoffs
std::vector<int> rankScores;
@@ -610,7 +589,7 @@ static void loadDrops(json& dropData) {
if (rankScores.size() != 5 || rankScores.size() != rankRewards.size()) {
char buff[255];
snprintf(buff, 255, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID);
sprintf(buff, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID);
throw TableException(std::string(buff));
}
@@ -680,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);
@@ -695,7 +674,7 @@ static void loadEggs(json& eggData, int32_t* nextId) {
}
}
/*
/*
* Load gruntwork output, if it exists
*/
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
@@ -776,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;
}
@@ -789,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;
}
@@ -812,12 +791,12 @@ 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
@@ -861,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)--;
@@ -871,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"];
@@ -880,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;
@@ -914,7 +893,7 @@ static void loadNPCs(json& npcData) {
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"]);
@@ -1024,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)--;
@@ -1247,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;
@@ -1259,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);
@@ -1280,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;
}
@@ -1306,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) {
@@ -1321,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;
@@ -1346,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);
}
@@ -1370,7 +1349,7 @@ void TableData::flush() {
targetIDs.push_back(tID);
for (int32_t tType : path.targetTypes)
targetTypes.push_back(tType);
pathObj["iBaseSpeed"] = path.speed;
pathObj["iTaskID"] = path.escortTaskID;
pathObj["bRelative"] = path.isRelative;

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,7 +1,4 @@
#include "Trading.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "db/Database.hpp"
@@ -186,36 +183,6 @@ static void tradeOfferRefusal(CNSocket* sock, CNPacketData* data) {
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
}
static void tradeOfferCancel(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL));
}
static void tradeOfferAbort(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT));
}
static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf;
@@ -460,8 +427,6 @@ void Trading::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, tradeOffer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT, tradeOfferAccept);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, tradeOfferRefusal);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL, tradeOfferCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ABORT, tradeOfferAbort);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM, tradeConfirm);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem);

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,9 +1,4 @@
#include "Vendors.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Rand.hpp"
// 7 days

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

@@ -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;
}
};

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();

View File

@@ -19,11 +19,11 @@
struct LoginMetadata {
uint64_t FEKey;
int32_t playerId;
time_t timestamp;
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; \
@@ -40,26 +39,23 @@
// wrapper for U16toU8
#define ARRLEN(x) (sizeof(x)/sizeof(*x))
#define AUTOU8(x) std::string((char*)x, ARRLEN(x))
#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt
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 can be defined by the build system.
// The PROTOCOL_VERSION definition is defined by the build system.
#if !defined(PROTOCOL_VERSION)
#define PROTOCOL_VERSION 104
#endif
#if PROTOCOL_VERSION == 104
#include "structs/0104.hpp"
#elif PROTOCOL_VERSION == 728
#include "structs/0728.hpp"
#elif PROTOCOL_VERSION == 104
#include "structs/0104.hpp"
#elif PROTOCOL_VERSION == 1013
#include "structs/1013.hpp"
#else

View File

@@ -10,44 +10,63 @@ 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 eCN_GM_TeleportType {
eCN_GM_TeleportMapType__XYZ,
eCN_GM_TeleportMapType__MapXYZ,
eCN_GM_TeleportMapType__MyLocation,
eCN_GM_TeleportMapType__SomeoneLocation,
eCN_GM_TeleportMapType__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
};
// nano power flags
// nano powers
enum {
EST_NONE = 0,
EST_DAMAGE = 1,
EST_HEAL_HP = 2,
EST_KNOCKDOWN = 3,
EST_SLEEP = 4,
EST_SNARE = 5,
EST_HEAL_STAMINA = 6,
EST_STAMINA_SELF = 7,
EST_STUN = 8,
EST_WEAPONSLOW = 9,
EST_JUMP = 10,
EST_RUN = 11,
EST_STEALTH = 12,
EST_SWIM = 13,
EST_MINIMAPENEMY = 14,
EST_MINIMAPTRESURE = 15,
EST_PHOENIX = 16,
EST_PROTECTBATTERY = 17,
EST_PROTECTINFECTION = 18,
EST_REWARDBLOB = 19,
EST_REWARDCASH = 20,
EST_BATTERYDRAIN = 21,
EST_CORRUPTIONATTACK = 22,
EST_INFECTIONDAMAGE = 23,
EST_KNOCKBACK = 24,
EST_FREEDOM = 25,
EST_PHOENIX_GROUP = 26,
EST_RECALL = 27,
EST_RECALL_GROUP = 28,
EST_RETROROCKET_SELF = 29,
EST_BLOODSUCKING = 30,
EST_BOUNDINGBALL = 31,
EST_INVULNERABLE = 32,
EST_NANOSTIMPAK = 33,
EST_RETURNHOMEHEAL = 34,
EST_BUFFHEAL = 35,
EST_EXTRABANK = 36,
EST__END = 37,
EST_CORRUPTIONATTACKWIN = 38,
EST_CORRUPTIONATTACKLOSE = 39,
ECSB_NONE = 0,
ECSB_UP_MOVE_SPEED = 1,
ECSB_UP_SWIM_SPEED = 2,
@@ -77,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

@@ -5,7 +5,7 @@
#include <string>
#include <vector>
#define DATABASE_VERSION 5
#define DATABASE_VERSION 4
namespace Database {
@@ -13,7 +13,7 @@ namespace Database {
int AccountID;
std::string Password;
int Selected;
time_t BannedUntil;
int64_t BannedUntil;
std::string BanReason;
};
@@ -46,17 +46,9 @@ namespace Database {
void close();
void findAccount(Account* account, std::string login);
// return ID, 0 if something failed
int getAccountIdForPlayer(int playerId);
// returns ID, 0 if something failed
int addAccount(std::string login, std::string password);
void updateAccountLevel(int accountId, int accountLevel);
// return true if cookie is valid for the account.
// invalidates the stored cookie afterwards
bool checkCookie(int accountId, const char *cookie);
// interface for the /ban command
bool banPlayer(int playerId, std::string& reason);
bool unbanPlayer(int playerId);

View File

@@ -226,11 +226,10 @@ static void createTables() {
static int getTableSize(std::string tableName) {
std::lock_guard<std::mutex> lock(dbCrit); // XXX
// you aren't allowed to bind the table name
const char* sql = "SELECT COUNT(*) FROM ";
tableName.insert(0, sql);
const char* sql = "SELECT COUNT(*) FROM ?";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, tableName.c_str(), -1, &stmt, NULL);
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, tableName.c_str(), -1, NULL);
sqlite3_step(stmt);
int result = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
@@ -267,17 +266,17 @@ void Database::open() {
checkMetaTable();
createTables();
std::cout << "[INFO] Database in operation";
std::cout << "[INFO] Database in operation ";
int accounts = getTableSize("Accounts");
int players = getTableSize("Players");
std::string message = "";
if (accounts > 0) {
message += ": Found " + std::to_string(accounts) + " account";
message += ": Found " + std::to_string(accounts) + " Account";
if (accounts > 1)
message += "s";
}
if (players > 0) {
message += " and " + std::to_string(players) + " player";
message += " and " + std::to_string(players) + " Player Character";
if (players > 1)
message += "s";
}

View File

@@ -27,32 +27,6 @@ void Database::findAccount(Account* account, std::string login) {
sqlite3_finalize(stmt);
}
int Database::getAccountIdForPlayer(int playerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT AccountID
FROM Players
WHERE PlayerID = ?
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
std::cout << "[WARN] Database: couldn't get account id for player " << playerId << std::endl;
sqlite3_finalize(stmt);
return 0;
}
int accountId = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return accountId;
}
int Database::addAccount(std::string login, std::string password) {
std::lock_guard<std::mutex> lock(dbCrit);
@@ -78,75 +52,6 @@ int Database::addAccount(std::string login, std::string password) {
return sqlite3_last_insert_rowid(db);
}
void Database::updateAccountLevel(int accountId, int accountLevel) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
UPDATE Accounts SET
AccountLevel = ?
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountLevel);
sqlite3_bind_int(stmt, 2, accountId);
int rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on updateAccountLevel(): " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
}
bool Database::checkCookie(int accountId, const char *tryCookie) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql_get = R"(
SELECT Cookie
FROM Auth
WHERE AccountID = ? AND Expires > ?;
)";
const char* sql_invalidate = R"(
UPDATE Auth
SET Expires = 0
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql_get, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
sqlite3_bind_int(stmt, 2, getTimestamp());
int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return false;
}
const char *cookie = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (strlen(cookie) != strlen(tryCookie)) {
sqlite3_finalize(stmt);
return false;
}
/*
* since cookies are immediately invalidated, we don't need to be concerned about
* timing-related side channel attacks, so strcmp is fine here
*/
bool match = (strcmp(cookie, tryCookie) == 0);
sqlite3_finalize(stmt);
sqlite3_prepare_v2(db, sql_invalidate, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on checkCookie(): " << sqlite3_errmsg(db) << std::endl;
return match;
}
void Database::updateSelected(int accountId, int slot) {
std::lock_guard<std::mutex> lock(dbCrit);

View File

@@ -208,8 +208,6 @@ void Database::addBlock(int playerId, int blockedPlayerId) {
}
void Database::removeBlock(int playerId, int blockedPlayerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
DELETE FROM Blocks
WHERE PlayerID = ? AND BlockedPlayerID = ?;

View File

@@ -201,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

@@ -1,14 +1,12 @@
#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;
@@ -105,95 +103,57 @@ void loginFail(LoginError errorCode, std::string userLogin, CNSocket* sock) {
void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
auto login = (sP_CL2LS_REQ_LOGIN*)data->buf;
std::string userLogin;
std::string userToken; // could be password or auth cookie
// TODO: implement better way of sending credentials
std::string userLogin((char*)login->szCookie_TEGid);
std::string userPassword((char*)login->szCookie_authid);
/*
* The std::string -> char* -> std::string maneuver should remove any
* trailing garbage after the null terminator.
* Sometimes the client sends garbage cookie data.
* Validate it as normal credentials instead of using a length check before falling back.
*/
if (login->iLoginType == (int32_t)LoginType::COOKIE) {
userLogin = std::string(AUTOU8(login->szCookie_TEGid).c_str());
userToken = std::string(AUTOU8(login->szCookie_authid).c_str());
} else {
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
/*
* The std::string -> char* -> std::string maneuver should remove any
* trailing garbage after the null terminator.
*/
userLogin = std::string(AUTOU16TOU8(login->szID).c_str());
userToken = std::string(AUTOU16TOU8(login->szPassword).c_str());
userPassword = std::string(AUTOU16TOU8(login->szPassword).c_str());
}
// check username regex
if (!CNLoginServer::isUsernameGood(userLogin)) {
// the client inserts a "\n" in the password if you press enter key in the middle of the password
// (not at the start or the end of the password field)
if (int(userPassword.find("\n")) > 0)
userPassword.erase(userPassword.find("\n"), 1);
// check regex
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid login\n";
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore";
std::string text = "Invalid login or password\n";
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore\n";
text += "Password has to be 8 - 32 characters long";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 10;
msg.iDuringTime = 15;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
// we only interpret the token as a cookie if cookie login was used and it's allowed.
// otherwise we interpret it as a password, and this maintains compatibility with
// the auto-login trick used on older clients
bool isCookieAuth = login->iLoginType == (int32_t)LoginType::COOKIE
&& CNLoginServer::isLoginTypeAllowed(LoginType::COOKIE);
// password login checks
if (!isCookieAuth) {
// bail if password auth isn't allowed
if (!CNLoginServer::isLoginTypeAllowed(LoginType::PASSWORD)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Password login disabled\n";
text += "This server has disabled logging in with plaintext passwords.\n";
text += "Please contact an admin for assistance.";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 12;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
// check regex
if (!CNLoginServer::isPasswordGood(userToken)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid password\n";
text += "Password has to be 8 - 32 characters long";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
}
Database::Account findUser = {};
Database::findAccount(&findUser, userLogin);
// account was not found
if (findUser.AccountID == 0) {
// don't auto-create an account if it's a cookie auth for whatever reason
if (settings::AUTOCREATEACCOUNTS && !isCookieAuth)
return newAccount(sock, userLogin, userToken, login->iClientVerC);
if (settings::AUTOCREATEACCOUNTS)
return newAccount(sock, userLogin, userPassword, login->iClientVerC);
return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock);
}
if (isCookieAuth) {
const char *cookie = userToken.c_str();
if (!Database::checkCookie(findUser.AccountID, cookie))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
} else {
// simple password check
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userToken))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
}
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userPassword))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
// is the account banned
if (findUser.BannedUntil > getTimestamp()) {
@@ -614,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;
@@ -659,14 +619,11 @@ bool CNLoginServer::exitDuplicate(int accountId) {
return false;
}
bool CNLoginServer::isUsernameGood(std::string login) {
const std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
return (std::regex_match(login, loginRegex));
}
bool CNLoginServer::isLoginDataGood(std::string login, std::string password) {
std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
bool CNLoginServer::isPasswordGood(std::string password) {
const std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
return (std::regex_match(password, passwordRegex));
return (std::regex_match(login, loginRegex) && std::regex_match(password, passwordRegex));
}
bool CNLoginServer::isPasswordCorrect(std::string actualPassword, std::string tryPassword) {
@@ -679,17 +636,4 @@ bool CNLoginServer::isCharacterNameGood(std::string Firstname, std::string Lastn
std::regex lastnamecheck(R"(((?! )(?!\.)[a-zA-Z0-9]*\.{0,1}(?!\.+ +)[a-zA-Z0-9]* {0,1}(?! +))*$)");
return (std::regex_match(Firstname, firstnamecheck) && std::regex_match(Lastname, lastnamecheck));
}
bool CNLoginServer::isLoginTypeAllowed(LoginType loginType) {
// the config file specifies "comma-separated" but tbh we don't care
switch (loginType) {
case LoginType::PASSWORD:
return settings::AUTHMETHODS.find("password") != std::string::npos;
case LoginType::COOKIE:
return settings::AUTHMETHODS.find("cookie") != std::string::npos;
default:
break;
}
return false;
}
#pragma endregion

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 {
@@ -23,11 +22,6 @@ enum class LoginError {
UPDATED_EUALA_REQUIRED = 9
};
enum class LoginType {
PASSWORD = 1,
COOKIE = 2
};
// WARNING: THERE CAN ONLY BE ONE OF THESE SERVERS AT A TIME!!!!!! TODO: change loginSessions & packet handlers to be non-static
class CNLoginServer : public CNServer {
private:
@@ -44,12 +38,10 @@ private:
static void changeName(CNSocket* sock, CNPacketData* data);
static void duplicateExit(CNSocket* sock, CNPacketData* data);
static bool isUsernameGood(std::string login);
static bool isPasswordGood(std::string password);
static bool isLoginDataGood(std::string login, std::string password);
static bool isPasswordCorrect(std::string actualPassword, std::string tryPassword);
static bool isAccountInUse(int accountId);
static bool isCharacterNameGood(std::string Firstname, std::string Lastname);
static bool isLoginTypeAllowed(LoginType loginType);
static void newAccount(CNSocket* sock, std::string userLogin, std::string userPassword, int32_t clientVerC);
// returns true if success
static bool exitDuplicate(int accountId);

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()
@@ -61,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
@@ -74,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;
@@ -116,7 +114,7 @@ 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)

View File

@@ -3,19 +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
#define MS_PER_COMBAT_TICK 200
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;
@@ -13,13 +12,12 @@ bool settings::SANDBOX = true;
int settings::LOGINPORT = 23000;
bool settings::APPROVEALLNAMES = true;
bool settings::AUTOCREATEACCOUNTS = true;
std::string settings::AUTHMETHODS = "password";
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;
@@ -68,9 +66,6 @@ int settings::MONITORINTERVAL = 5000;
// event mode settings
int settings::EVENTMODE = 0;
// race settings
bool settings::IZRACESCORECAPPED = true;
void settings::init() {
INIReader reader("config.ini");
@@ -88,7 +83,6 @@ void settings::init() {
LOGINPORT = reader.GetInteger("login", "port", LOGINPORT);
APPROVEALLNAMES = reader.GetBoolean("login", "acceptallcustomnames", APPROVEALLNAMES);
AUTOCREATEACCOUNTS = reader.GetBoolean("login", "autocreateaccounts", AUTOCREATEACCOUNTS);
AUTHMETHODS = reader.Get("login", "authmethods", AUTHMETHODS);
DBSAVEINTERVAL = reader.GetInteger("login", "dbsaveinterval", DBSAVEINTERVAL);
SHARDPORT = reader.GetInteger("shard", "port", SHARDPORT);
SHARDSERVERIP = reader.Get("shard", "ip", SHARDSERVERIP);
@@ -116,7 +110,6 @@ void settings::init() {
EVENTMODE = reader.GetInteger("shard", "eventmode", EVENTMODE);
DISABLEFIRSTUSEFLAG = reader.GetBoolean("shard", "disablefirstuseflag", DISABLEFIRSTUSEFLAG);
ANTICHEAT = reader.GetBoolean("shard", "anticheat", ANTICHEAT);
IZRACESCORECAPPED = reader.GetBoolean("shard", "izracescorecapped", IZRACESCORECAPPED);
MONITORENABLED = reader.GetBoolean("monitor", "enabled", MONITORENABLED);
MONITORPORT = reader.GetInteger("monitor", "port", MONITORPORT);
MONITORINTERVAL = reader.GetInteger("monitor", "interval", MONITORINTERVAL);

View File

@@ -1,15 +1,11 @@
#pragma once
#include <stdint.h>
#include <string>
namespace settings {
extern int VERBOSITY;
extern bool SANDBOX;
extern int LOGINPORT;
extern bool APPROVEALLNAMES;
extern bool AUTOCREATEACCOUNTS;
extern std::string AUTHMETHODS;
extern int DBSAVEINTERVAL;
extern int SHARDPORT;
extern std::string SHARDSERVERIP;
@@ -40,7 +36,6 @@ namespace settings {
extern int MONITORPORT;
extern int MONITORINTERVAL;
extern bool DISABLEFIRSTUSEFLAG;
extern bool IZRACESCORECAPPED;
void init();
}

2
tdata

Submodule tdata updated: bdb611b092...cc65dbb402

14840
vendor/JSON.hpp vendored

File diff suppressed because it is too large Load Diff

View File

@@ -22,13 +22,8 @@
#endif
#include <errno.h>
#if defined(_WIN32) || defined(_WIN64)
#ifdef _WIN32 || _WIN64
// On windows we need to generate random bytes differently.
#if defined(_WIN32) && !defined(_WIN64)
typedef __int32 ssize_t;
#elif defined(_WIN32) && defined(_WIN64)
typedef __int64 ssize_t;
#endif
#define BCRYPT_HASHSIZE 60
#include "bcrypt.h"
@@ -55,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;
@@ -79,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
@@ -121,7 +118,7 @@ int bcrypt_gensalt(int factor, char salt[BCRYPT_HASHSIZE])
char *aux;
// Note: Windows does not have /dev/urandom sadly.
#if defined(_WIN32) || defined(_WIN64)
#ifdef _WIN32 || _WIN64
HCRYPTPROV p;
ULONG i;

View File

@@ -51,7 +51,7 @@
#endif
/* Just to make sure the prototypes match the actual definitions */
#if defined(_WIN32) || defined(_WIN64)
#ifdef _WIN32 || _WIN64
#include "crypt_blowfish.h"
#else
#include "crypt_blowfish.h"

View File

@@ -41,7 +41,7 @@
#define __SKIP_GNU
#endif
#if defined(_WIN32) || defined(_WIN64)
#ifdef _WIN32 | _WIN64
#include "ow-crypt.h"
#include "crypt_blowfish.h"
@@ -251,7 +251,7 @@ char *__crypt_gensalt_ra(const char *prefix, unsigned long count,
input, size, output, sizeof(output));
if (retval) {
#if defined(_WIN32) || defined(_WIN64)
#ifdef _WIN32 | _WIN64
retval = _strdup(retval);
#else
retval = strdup(retval);