58 Commits

Author SHA1 Message Date
CakeLancelot
fa8c1e73d1 Fix a few compiler warnings and formatting for DB startup message (#272)
Note that the warning in TableData.cpp only seems to occur on clang (deprecated-declarations)

Database account/player count message:
Before: [INFO] Database in operation : Found 1 account(s) and 2 player(s)s
After: [INFO] Database in operation: Found 1 account and 2 players
2024-03-31 13:02:39 -05:00
CakeLancelot
aeac57ebf7 Remove duplicate headers from Makefile 2024-03-31 02:48:18 -05:00
CakeLancelot
632406e93b Bump nlohmann JSON to 3.11.3 (#271)
I've checked all the changelogs from the version we were using (3.9.1) to the latest, and there should not be any breaking changes or deprecated functionality.
The server also compiled fine and I did not encounter any issues after a brief play session.
2024-03-29 21:50:55 -05:00
837f109752 Add missing mutex 2024-02-03 03:13:43 -05:00
c11cfebdb1 Fix account and player counts on startup 2024-02-03 03:13:24 -05:00
20367d77f0 Bump copyright date 2024-02-02 22:01:38 -05:00
8d04f31c61 Default PROTOCOL_VERSION to 104 2024-02-02 21:58:37 -05:00
FinnHornhoover
44560a46b7 Rank Calculation Out-of-Bounds Fix (#268) 2023-12-24 02:49:51 +01:00
gsemaj
21d280147c Shard crash fixes 2023-12-19 13:23:58 -05:00
FinnHornhoover
b765821552 added option to disable score capping 2023-12-19 13:18:43 -05:00
e61682dfb2 fix: out-of-bounds index in itemDeleteHandler() 2023-12-18 04:41:53 -06:00
FinnHornhoover
d9ebb4e3ef only allow barkers of simple npcs
Co-authored-by: Gent Semaj <gsemaj@proton.me>
2023-12-12 06:01:30 -08:00
FinnHornhoover
73c610b471 randomized npcs for mission barkers 2023-12-12 06:01:30 -08:00
FinnHornhoover
3e6bfea3fe fixed unexpected randomization of barkers 2023-12-12 06:01:30 -08:00
FinnHornhoover
cd265af8e0 Original Racing Scores Functionality (#257) 2023-10-12 04:42:57 +02:00
CakeLancelot
38c68f351b Update README.md to fix some inaccurate info, move commands to wiki page 2023-10-12 04:00:16 +02:00
edfbe4d005 Make winTerminate() a WINAPI function
This macro makes it use the stdcall calling convention when compiling
for 32-bit Windows. Its absence was technically a bug, but it's a no-op
on 64-bit Windows.
2023-10-12 02:02:23 +02:00
96c430c994 Restore rapid-fire anti-cheat for projectiles
For a while we had made the temporary rapid-fire anti-cheat optional on
master, but had removed it entirely on the refactor branch. The
modification on master was acidentally only applied to the regular
(non-projectile) combat handler, while the removal on refactor removed
both that and the projectile check.

When the refactor branch was merged, that resulted in the removal of
only the projectile rapid-fire check, while the conditional regular
combat rapid-fire check was kept.

This change restores the projectile rapid-fire check such that it is
conditional, just like for regular combat.
2023-10-12 02:02:23 +02:00
gsemaj
4592fc42af CI/CD improvements
- PR builds will now run even if they weren't opened as drafts
- PR builds will now re-run when they get new commits
- Builds can now be manually triggered from GitHub
2023-10-10 14:35:07 -04:00
FinnHornhoover
70a27afad1 Pimpleback IZ Pod Fix (#259) 2023-10-08 17:02:32 -04:00
gsemaj
6cfb3bf532 Merge branch 'refactor' 2023-10-08 16:54:42 -04:00
9b2a65f8fd [refactor] More buff + skill fixes 2023-10-08 16:43:11 -04:00
6a69388822 Refactor and generalize NPCEvent logic 2023-10-08 16:43:11 -04:00
2924a27eb4 Rename coord args in summonNPC() and constructors to clarify purpose
This makes it clearer that the real coords aren't set until the first
call to updateNPCPosition().
2023-10-08 16:43:11 -04:00
ba20f5a401 Quick fix for Fuse boss fight NPCEvent logic
Will be replaced with a proper rework immediately.
2023-10-08 16:43:11 -04:00
gsemaj
eb88fa05cb reduce drain tickrate 2023-10-08 16:43:11 -04:00
gsemaj
0b73cef187 Clear player buffs on death if not revived 2023-10-08 16:43:11 -04:00
gsemaj
7af39b3d04 [refactor] Buff + skill bugfixes 2023-10-08 16:43:11 -04:00
gsemaj
33206b1207 Updater contributer guide 2023-10-08 16:43:11 -04:00
gsemaj
e325f7a40b Implement buff handling for CombatNPC 2023-10-08 16:43:11 -04:00
gsemaj
82bee2051a [refactor] Active power handling 2023-10-08 16:43:11 -04:00
gsemaj
4ece1bb89b [refactor] Sync with master
This was initially a merge commit that got ironed out by rebase cleanup.
Notable changes are:
- Copying EntityRef instead of referencing it
- Some changes to includes
2023-10-08 16:41:51 -04:00
gsemaj
31677e2638 [refactor] New buff framework (player implementation)
Get rid of `iConditionBitFlag` in favor of a system of individual buff
objects that get composited to a bitflag on-the-fly.
Buff objects can have callbacks for application, expiration, and tick,
making them pretty flexible. Scripting languages can eventually use
these for custom behavior, too.

TODO:
- Get rid of bitflag in BaseNPC
- Apply buffs from passive nano powers
- Apply buffs from active nano powers
- Move eggs to new system
- ???
2023-10-08 16:34:43 -04:00
gsemaj
d32827b692 The great re-#include
Was getting frustrated by the inconsistency in our include statements,
which were causing me problems. As a result, I went through and manually
re-organized every include statement in non-core files.

I'm just gonna copy my rant from Discord:
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
2023-10-08 16:34:18 -04:00
gsemaj
13c009b448 Get rid of player fire rate suspicion
This was super primitive & jank, and caused false positives.
Will replace with a polished system later on.
2023-10-08 16:34:18 -04:00
gsemaj
a032497bed Handle case where cmake is invoked outside root 2023-10-08 16:34:18 -04:00
gsemaj
3b6b61d087 [refactor] SkillData additions for buffs 2023-10-08 16:34:18 -04:00
gsemaj
6d760f5bce Replace group filter operator with function 2023-10-08 16:33:54 -04:00
gsemaj
2a622f901c Ignore .bak files
for my local backups lol
2023-10-08 16:33:53 -04:00
gsemaj
03d28bf4e4 [refactor] Refactor groups 2023-10-08 16:33:53 -04:00
gsemaj
4b834579c5 [refactor] Remaining ICombatant implementation 2023-10-08 16:33:34 -04:00
gsemaj
07fe8ca367 (WIP) Remove BaseNPC::barkerType to save space 2023-10-08 16:32:49 -04:00
gsemaj
2f3f8a3951 [refactor] Initial ICombatant draft 2023-10-08 16:32:49 -04:00
4f890a9c07 [refactor] Get rid of NPC.hpp
This file was already obsoleted at the start of the refactor, but seems
to have escaped notice until now.
2023-10-08 16:31:53 -04:00
8517e0c7de [refactor] Replace a few uses of magic numbers with enums 2023-10-08 16:31:53 -04:00
5fb0cbbcf7 [refactor] Cosmetic cleanup in Fuse fight functions 2023-10-08 16:31:53 -04:00
55e9f6531d [refactor] Remove redundant coord args from most entity constructors
Mobs and CombatNPCs still need theirs in order to properly set their
roaming and spawn coords. Assignment of the latter has been moved to the
CombatNPC constructor, where it should have already been.
2023-10-08 16:31:53 -04:00
gsemaj
7726357fbe [WIP] Stub power handler 2023-10-08 16:31:53 -04:00
gsemaj
564c275d51 [WIP] Use EntityRef instead of CNSocket in ability handler 2023-10-08 16:31:53 -04:00
gsemaj
3ce9ae5f77 [WIP] Replace appearance data with individual fields
Storing certain things in appearance data and others in their own fields
was gross. Now everything is stored on the same level and functions have
been added to generate appearance data when it's needed by the client.
2023-10-08 16:31:53 -04:00
gsemaj
7c5b9a8105 [WIP] Rename Entity.type -> Entity.kind 2023-10-08 16:31:53 -04:00
gsemaj
258ff35e20 [WIP] Initial merge of ability namespaces & features 2023-10-08 16:31:53 -04:00
CakeLancelot
ab480d88f1 Update version numbers to 1.5 2023-10-07 18:20:04 -05:00
CakeLancelot
89772d763b CI: specify Ubuntu runner version and fix artifact zip name
We were technically already using 22.04 for a bit, it got updated without us noticing since the version was set to `ubuntu-latest`.
Affix the version to 22.04 so that it doesn't get unexpectedly updated again, and update the artifact's zip to reflect the change.
2023-10-07 17:13:13 -05:00
bd0cc3c212 Fix regression with /speed and /jump after previous change
Also changed the case values to use the client definitions.
2023-09-13 04:37:49 +02:00
c636c538eb Fix minor visual bug in setValuePlayer() 2023-09-02 20:59:34 +02:00
d3bef95a7f Fix /npcr
Also removed a redundant invocation of NPCManager::updateNPCPosition()
and simplified the surrounding code.
2023-08-20 05:06:16 +02:00
gsemaj
650f947451 Add .dockerignore file
Fixes an issue where if you had a version.h file generated from cmake,
it would be used in the Dockerfile even though the container uses make.
2023-08-19 18:22:21 +00:00
46 changed files with 7940 additions and 8278 deletions

1
.dockerignore Normal file
View File

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

View File

@@ -9,12 +9,13 @@ on:
- CMakeLists.txt - CMakeLists.txt
- Makefile - Makefile
pull_request: pull_request:
types: ready_for_review types: [opened, reopened, synchronize, ready_for_review]
paths: paths:
- src/** - src/**
- vendor/** - vendor/**
- CMakeLists.txt - CMakeLists.txt
- Makefile - Makefile
workflow_dispatch:
jobs: jobs:
ubuntu-build: ubuntu-build:
@@ -53,7 +54,7 @@ jobs:
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: 'ubuntu20_04-bin-x64-${{ env.SHORT_SHA }}' name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
path: bin path: bin
windows-build: windows-build:
@@ -112,7 +113,7 @@ jobs:
copy-artifacts: copy-artifacts:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest runs-on: ubuntu-22.04
needs: [windows-build, ubuntu-build] needs: [windows-build, ubuntu-build]
env: env:
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }} BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}

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. 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. Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge.
These are generally either: 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. 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. 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. 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). 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). 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. 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: 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. 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`) (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. 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`. Create a work branch just for your changeset with `git checkout -b work`.
@@ -81,3 +81,34 @@ 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`. 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. 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,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020-2023 OpenFusion Contributors Copyright (c) 2020-2024 OpenFusion Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -95,8 +95,6 @@ CXXHDR=\
vendor/bcrypt/BCrypt.hpp\ vendor/bcrypt/BCrypt.hpp\
vendor/INIReader.hpp\ vendor/INIReader.hpp\
vendor/JSON.hpp\ vendor/JSON.hpp\
vendor/INIReader.hpp\
vendor/JSON.hpp\
src/Buffs.hpp\ src/Buffs.hpp\
src/Chat.hpp\ src/Chat.hpp\
src/CustomCommands.hpp\ src/CustomCommands.hpp\

View File

@@ -13,13 +13,13 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
### Getting Started ### Getting Started
#### Method A: Installer (Easiest) #### Method A: Installer (Easiest)
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. 1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5/OpenFusionClient-1.5-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. 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. 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. 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 #### Method B: Standalone .zip file
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4.zip). 1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5/OpenFusionClient-1.5.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. 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. 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. 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,8 +28,7 @@ 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). 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 ### 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).
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. 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: 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. 1. For Description, enter anything you want. This is what will show up in the server list.
@@ -54,10 +53,7 @@ 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 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 MIME type `application/vnd.unity`, where the `src` param is the address of the game's `.unity3d` entrypoint. 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.
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. 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. This will potentially become relevant later, as people start experimenting and mixing and matching versions.
@@ -66,7 +62,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"). `/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! 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 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!). It instead loads the web pages locally using the `file://` schema, and fetches the game's assets from a standard web server.
`/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. `/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.
@@ -102,26 +98,13 @@ If you'd like to contribute to this project, please read [CONTRIBUTING.md](CONTR
## Gameplay ## Gameplay
The goal of the project is to faithfully recreate the game as it was at the time of the targeted build. The goal of the project is to faithfully recreate the game as it was at the time of the targeted build.
The server is not yet complete, however, and some functionality is still missing. While most features are implemented and the game is playable start to finish, there may be missing functionality or bugs present.
Because the server is still in development, ordinary players are allowed access to a few admin commands: Depending on the server configuration, you'll have access to certain commands.
![](res/sane_upsell.png) 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).
### Movement commands When hosting a local server, you will have access to all commands by default (account level 1).
* 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.).
### Item commands For a list of available commands, see [this wiki page](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).
* `/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

@@ -66,6 +66,9 @@ motd=Welcome to OpenFusion!
# location of the database # location of the database
#dbpath=database.db #dbpath=database.db
# should there be a score cap for infected zone races?
#izracescorecapped=true
# should tutorial flags be disabled off the bat? # should tutorial flags be disabled off the bat?
disablefirstuseflag=true disablefirstuseflag=true

View File

@@ -1,11 +1,12 @@
#include "Abilities.hpp" #include "Abilities.hpp"
#include "servers/CNShardServer.hpp"
#include "NPCManager.hpp" #include "NPCManager.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Buffs.hpp" #include "Buffs.hpp"
#include "Nanos.hpp" #include "Nanos.hpp"
#include "MobAI.hpp"
#include <assert.h>
using namespace Abilities; using namespace Abilities;
@@ -47,16 +48,107 @@ static SkillResult handleSkillHealHP(SkillData* skill, int power, ICombatant* so
} }
static SkillResult handleSkillDamageNDebuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) { static SkillResult handleSkillDamageNDebuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
// TODO abilities // take aggro
target->takeDamage(source->getRef(), 0);
int duration = 0;
int strength = 0;
bool blocked = target->hasBuff(ECSB_FREEDOM);
if(!blocked) {
duration = skill->durationTime[power];
strength = skill->values[0][power];
BuffStack debuff = {
(duration * 100) / MS_PER_COMBAT_TICK, // ticks
strength, // value
source->getRef(), // source
BuffClass::NANO, // buff class
};
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
target->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) {
Buffs::timeBuffTick(self, buff);
},
&debuff);
}
sSkillResult_Damage_N_Debuff result{}; sSkillResult_Damage_N_Debuff result{};
result.iDamage = duration / 10; // we use the duration as the damage number (why?)
result.iHP = target->getCurrentHP();
result.eCT = target->getCharType(); result.eCT = target->getCharType();
result.iID = target->getID(); result.iID = target->getID();
result.bProtected = false; result.bProtected = blocked;
result.iConditionBitFlag = target->getCompositeCondition(); result.iConditionBitFlag = target->getCompositeCondition();
// for player targets, make sure to update Nano stamina
if (target->getCharType() == 1) {
Player *plr = dynamic_cast<Player*>(target);
result.iStamina = plr->getActiveNano()->iStamina;
}
return SkillResult(sizeof(sSkillResult_Damage_N_Debuff), &result); return SkillResult(sizeof(sSkillResult_Damage_N_Debuff), &result);
} }
static SkillResult handleSkillLeech(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
EntityRef sourceRef = source->getRef();
int heal = skill->values[0][power];
int healed = source->heal(sourceRef, heal);
int damage = heal * 2;
int dealt = target->takeDamage(sourceRef, damage);
sSkillResult_Leech result{};
result.Damage.eCT = target->getCharType();
result.Damage.iID = target->getID();
result.Damage.bProtected = dealt <= 0;
result.Damage.iDamage = dealt;
result.Damage.iHP = target->getCurrentHP();
result.Heal.eCT = result.Damage.eCT;
result.Heal.iID = result.Damage.iID;
result.Heal.iHealHP = healed;
result.Heal.iHP = source->getCurrentHP();
return SkillResult(sizeof(sSkillResult_Leech), &result);
}
static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) { static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
int duration = skill->durationTime[power];
int strength = skill->values[0][power];
BuffStack passiveBuff = {
// if the duration is 0, it needs to be recast every tick
duration == 0 ? 1 : (duration * 100) / MS_PER_COMBAT_TICK, // ticks
strength, // value
source->getRef(), // source
source == target ? BuffClass::NANO : BuffClass::GROUP_NANO, // buff class
};
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
SkillDrainType drainType = skill->drainType;
int combatLifetime = 0;
if(!target->addBuff(timeBuffId,
[drainType](EntityRef self, Buff* buff, int status, BuffStack* stack) {
if(buff->id == ECSB_BOUNDINGBALL && status == ETBU_ADD) {
// drain
ICombatant* combatant = dynamic_cast<ICombatant*>(self.getEntity());
combatant->takeDamage(buff->getLastSource(), 0); // aggro
}
Buffs::timeBuffUpdate(self, buff, status, stack);
if(drainType == SkillDrainType::ACTIVE && status == ETBU_DEL)
Buffs::timeBuffTimeout(self);
},
[combatLifetime](EntityRef self, Buff* buff, time_t currTime) mutable {
if(buff->id == ECSB_BOUNDINGBALL &&
combatLifetime % COMBAT_TICKS_PER_DRAIN_PROC == 0)
Buffs::tickDrain(self, buff, COMBAT_TICKS_PER_DRAIN_PROC); // drain
combatLifetime++;
},
&passiveBuff)) return SkillResult();
sSkillResult_Buff result{}; sSkillResult_Buff result{};
result.eCT = target->getCharType(); result.eCT = target->getCharType();
result.iID = target->getID(); result.iID = target->getID();
@@ -71,19 +163,24 @@ static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombata
Player* plr = dynamic_cast<Player*>(target); Player* plr = dynamic_cast<Player*>(target);
const double scalingFactor = (18 + source->getLevel()) / 36.0; const double scalingFactor = (18 + source->getLevel()) / 36.0;
const bool blocked = target->hasBuff(ECSB_PROTECT_BATTERY);
int boostDrain = (int)(skill->values[0][power] * scalingFactor); int boostDrain = 0;
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW; int potionDrain = 0;
plr->batteryW -= boostDrain; if(!blocked) {
boostDrain = (int)(skill->values[0][power] * scalingFactor);
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
plr->batteryW -= boostDrain;
int potionDrain = (int)(skill->values[1][power] * scalingFactor); potionDrain = (int)(skill->values[1][power] * scalingFactor);
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN; if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
plr->batteryN -= potionDrain; plr->batteryN -= potionDrain;
}
sSkillResult_BatteryDrain result{}; sSkillResult_BatteryDrain result{};
result.eCT = target->getCharType(); result.eCT = target->getCharType();
result.iID = target->getID(); result.iID = target->getID();
result.bProtected = target->hasBuff(ECSB_PROTECT_BATTERY); result.bProtected = blocked;
result.iDrainW = boostDrain; result.iDrainW = boostDrain;
result.iBatteryW = plr->batteryW; result.iBatteryW = plr->batteryW;
result.iDrainN = potionDrain; result.iDrainN = potionDrain;
@@ -97,8 +194,13 @@ static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombata
static SkillResult handleSkillMove(SkillData* skill, int power, ICombatant* source, ICombatant* target) { static SkillResult handleSkillMove(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
if(source->getCharType() != 1) if(source->getCharType() != 1)
return SkillResult(); // only Players are valid sources for recall return SkillResult(); // only Players are valid sources for recall
Player* plr = dynamic_cast<Player*>(source); Player* plr = dynamic_cast<Player*>(source);
PlayerManager::sendPlayerTo(source->getRef().sock, plr->recallX, plr->recallY, plr->recallZ, plr->recallInstance); if(source == target) {
// no trailing struct for self
PlayerManager::sendPlayerTo(target->getRef().sock, plr->recallX, plr->recallY, plr->recallZ, plr->recallInstance);
return SkillResult();
}
sSkillResult_Move result{}; sSkillResult_Move result{};
result.eCT = target->getCharType(); result.eCT = target->getCharType();
@@ -120,56 +222,66 @@ static SkillResult handleSkillResurrect(SkillData* skill, int power, ICombatant*
#pragma endregion #pragma endregion
static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombatant* src, std::vector<ICombatant*> targets) { static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombatant* src, std::vector<ICombatant*> targets) {
size_t resultSize = 0;
SkillResult (*skillHandler)(SkillData*, int, ICombatant*, ICombatant*) = nullptr; SkillResult (*skillHandler)(SkillData*, int, ICombatant*, ICombatant*) = nullptr;
std::vector<SkillResult> results; std::vector<SkillResult> results;
switch(skill->skillType) switch(skill->skillType)
{ {
case EST_DAMAGE: case SkillType::CORRUPTIONATTACK:
resultSize = sizeof(sSkillResult_Damage); case SkillType::CORRUPTIONATTACKLOSE:
case SkillType::CORRUPTIONATTACKWIN:
// skillHandler = handleSkillCorruptionReflect;
// break;
case SkillType::DAMAGE:
skillHandler = handleSkillDamage; skillHandler = handleSkillDamage;
break; break;
case EST_HEAL_HP: case SkillType::HEAL_HP:
case EST_RETURNHOMEHEAL: case SkillType::RETURNHOMEHEAL:
resultSize = sizeof(sSkillResult_Heal_HP);
skillHandler = handleSkillHealHP; skillHandler = handleSkillHealHP;
break; break;
case EST_JUMP: case SkillType::KNOCKDOWN:
case EST_RUN: case SkillType::SLEEP:
case EST_FREEDOM: case SkillType::SNARE:
case EST_PHOENIX: case SkillType::STUN:
case EST_INVULNERABLE: skillHandler = handleSkillDamageNDebuff;
case EST_MINIMAPENEMY: break;
case EST_MINIMAPTRESURE: case SkillType::JUMP:
case EST_NANOSTIMPAK: case SkillType::RUN:
case EST_PROTECTBATTERY: case SkillType::STEALTH:
case EST_PROTECTINFECTION: case SkillType::MINIMAPENEMY:
case EST_REWARDBLOB: case SkillType::MINIMAPTRESURE:
case EST_REWARDCASH: case SkillType::PHOENIX:
case EST_STAMINA_SELF: case SkillType::PROTECTBATTERY:
case EST_STEALTH: case SkillType::PROTECTINFECTION:
resultSize = sizeof(sSkillResult_Buff); case SkillType::REWARDBLOB:
case SkillType::REWARDCASH:
// case SkillType::INFECTIONDAMAGE:
case SkillType::FREEDOM:
case SkillType::BOUNDINGBALL:
case SkillType::INVULNERABLE:
case SkillType::STAMINA_SELF:
case SkillType::NANOSTIMPAK:
case SkillType::BUFFHEAL:
skillHandler = handleSkillBuff; skillHandler = handleSkillBuff;
break; break;
case EST_BATTERYDRAIN: case SkillType::BLOODSUCKING:
resultSize = sizeof(sSkillResult_BatteryDrain); skillHandler = handleSkillLeech;
skillHandler = handleSkillBatteryDrain;
break; break;
case EST_RECALL: // still soft lock case SkillType::RETROROCKET_SELF:
case EST_RECALL_GROUP: // works for player who uses it
resultSize = sizeof(sSkillResult_Move);
skillHandler = handleSkillMove;
break;
case EST_PHOENIX_GROUP: // broken
resultSize = sizeof(sSkillResult_Resurrect);
skillHandler = handleSkillResurrect;
break;
case EST_RETROROCKET_SELF:
// no-op // no-op
return results; return results;
case SkillType::PHOENIX_GROUP:
skillHandler = handleSkillResurrect;
break;
case SkillType::RECALL:
case SkillType::RECALL_GROUP:
skillHandler = handleSkillMove;
break;
case SkillType::BATTERYDRAIN:
skillHandler = handleSkillBatteryDrain;
break;
default: default:
std::cout << "[WARN] Unhandled skill type " << skill->skillType << std::endl; std::cout << "[WARN] Unhandled skill type " << (int)skill->skillType << std::endl;
return results; return results;
} }
@@ -177,8 +289,8 @@ static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombat
assert(target != nullptr); assert(target != nullptr);
SkillResult result = skillHandler(skill, power, src != nullptr ? src : target, target); SkillResult result = skillHandler(skill, power, src != nullptr ? src : target, target);
if(result.size == 0) continue; // skill not applicable if(result.size == 0) continue; // skill not applicable
if(result.size != resultSize) { if(result.size > MAX_SKILLRESULT_SIZE) {
std::cout << "[WARN] bad skill result size for " << skill->skillType << " from " << (void*)handleSkillBuff << std::endl; std::cout << "[WARN] bad skill result size for " << (int)skill->skillType << " from " << (void*)handleSkillBuff << std::endl;
continue; continue;
} }
results.push_back(result); results.push_back(result);
@@ -186,36 +298,42 @@ static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombat
return results; return results;
} }
static void attachSkillResults(std::vector<SkillResult> results, size_t resultSize, uint8_t* pivot) { static void attachSkillResults(std::vector<SkillResult> results, uint8_t* pivot) {
for(SkillResult& result : results) { for(SkillResult& result : results) {
memcpy(pivot, result.payload, resultSize); size_t sz = result.size;
pivot += resultSize; memcpy(pivot, result.payload, sz);
pivot += sz;
} }
} }
void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std::vector<ICombatant*> affected) { void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std::vector<ICombatant*> affected) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
ICombatant* combatant = dynamic_cast<ICombatant*>(plr);
int boost = 0; int boost = 0;
if (Nanos::getNanoBoost(plr)) if (Nanos::getNanoBoost(plr))
boost = 3; boost = 3;
nano.iStamina -= skill->batteryUse[boost]; if(skill->drainType == SkillDrainType::ACTIVE) {
if (nano.iStamina < 0) nano.iStamina -= skill->batteryUse[boost];
nano.iStamina = 0; if (nano.iStamina <= 0)
nano.iStamina = 0;
}
std::vector<SkillResult> results = handleSkill(skill, boost, plr, affected); std::vector<SkillResult> results = handleSkill(skill, boost, combatant, affected);
size_t resultSize = 0; // guaranteed to be the same for every item if(results.empty()) return; // no effect; no need for confirmation packets
if (!results.empty()) resultSize = results.back().size;
if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), results.size(), resultSize)) { // lazy validation since skill results might be different sizes
if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), results.size(), MAX_SKILLRESULT_SIZE)) {
std::cout << "[WARN] bad sP_FE2CL_NANO_SKILL_USE_SUCC packet size\n"; std::cout << "[WARN] bad sP_FE2CL_NANO_SKILL_USE_SUCC packet size\n";
return; return;
} }
// initialize response struct // initialize response struct
size_t resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) + results.size() * resultSize; size_t resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC);
for(SkillResult& sr : results)
resplen += sr.size;
uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen); memset(respbuf, 0, resplen);
@@ -225,15 +343,17 @@ void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std:
pkt->iSkillID = nano.iSkillID; pkt->iSkillID = nano.iSkillID;
pkt->iNanoStamina = nano.iStamina; pkt->iNanoStamina = nano.iStamina;
pkt->bNanoDeactive = nano.iStamina <= 0; pkt->bNanoDeactive = nano.iStamina <= 0;
pkt->eST = skill->skillType; pkt->eST = (int32_t)skill->skillType;
pkt->iTargetCnt = (int32_t)results.size(); pkt->iTargetCnt = (int32_t)results.size();
attachSkillResults(results, resultSize, (uint8_t*)(pkt + 1)); attachSkillResults(results, (uint8_t*)(pkt + 1));
sock->sendPacket(pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen); sock->sendPacket(pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen);
PlayerManager::sendToViewable(sock, pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen);
if (nano.iStamina <= 0) if(skill->skillType == SkillType::RECALL_GROUP)
Nanos::summonNano(sock, -1); // group recall packet is sent only to group members
PlayerManager::sendToGroup(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen);
else
PlayerManager::sendToViewable(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen);
} }
void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*> affected) { void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*> affected) {
@@ -248,25 +368,34 @@ void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*>
SkillData* skill = &SkillTable[skillID]; SkillData* skill = &SkillTable[skillID];
std::vector<SkillResult> results = handleSkill(skill, 0, src, affected); std::vector<SkillResult> results = handleSkill(skill, 0, src, affected);
size_t resultSize = results.back().size; // guaranteed to be the same for every item if(results.empty()) return; // no effect; no need for confirmation packets
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), resultSize)) { // lazy validation since skill results might be different sizes
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), MAX_SKILLRESULT_SIZE)) {
std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_HIT packet size\n"; std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_HIT packet size\n";
return; return;
} }
// initialize response struct // initialize response struct
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + results.size() * resultSize; size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT);
for(SkillResult& sr : results)
resplen += sr.size;
uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen); memset(respbuf, 0, resplen);
sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf; sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
pkt->iNPC_ID = npc.id; pkt->iNPC_ID = npc.id;
pkt->iSkillID = skillID; pkt->iSkillID = skillID;
pkt->eST = skill->skillType; pkt->eST = (int32_t)skill->skillType;
pkt->iTargetCnt = (int32_t)results.size(); pkt->iTargetCnt = (int32_t)results.size();
if(npc.kind == EntityKind::MOB) {
Mob* mob = dynamic_cast<Mob*>(entity);
pkt->iValue1 = mob->hitX;
pkt->iValue2 = mob->hitY;
pkt->iValue3 = mob->hitZ;
}
attachSkillResults(results, resultSize, (uint8_t*)(pkt + 1)); attachSkillResults(results, (uint8_t*)(pkt + 1));
NPCManager::sendToViewable(entity, pkt, P_FE2CL_NPC_SKILL_HIT, resplen); NPCManager::sendToViewable(entity, pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
} }
@@ -283,18 +412,13 @@ static std::vector<ICombatant*> entityRefsToCombatants(std::vector<EntityRef> re
std::vector<ICombatant*> Abilities::matchTargets(ICombatant* src, SkillData* skill, int count, int32_t *ids) { std::vector<ICombatant*> Abilities::matchTargets(ICombatant* src, SkillData* skill, int count, int32_t *ids) {
if(skill->targetType == SkillTargetType::GROUP)
return entityRefsToCombatants(src->getGroupMembers());
// this check *has* to happen after the group check above due to cases like group recall that use both
if(skill->effectTarget == SkillEffectTarget::SELF) if(skill->effectTarget == SkillEffectTarget::SELF)
return {src}; // client sends 0 targets for certain self-targeting skills (recall) return {src}; // client sends 0 targets for certain self-targeting skills (recall)
if(skill->targetType == SkillTargetType::GROUP) {
// group
if(count != 1 || ids[0] != src->getID()) {
std::cout << "[WARN] skill: bad group targeting (id " << ids[0] << ")\n";
return {};
}
return entityRefsToCombatants(src->getGroupMembers());
}
// individuals // individuals
std::vector<ICombatant*> targets; std::vector<ICombatant*> targets;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -324,63 +448,69 @@ std::vector<ICombatant*> Abilities::matchTargets(ICombatant* src, SkillData* ski
} }
/* ripped from client (enums emplaced) */ /* ripped from client (enums emplaced) */
int Abilities::getCSTBFromST(int eSkillType) { int Abilities::getCSTBFromST(SkillType skillType) {
int result = 0; int result = 0;
switch (eSkillType) switch (skillType)
{ {
case EST_RUN: case SkillType::RUN:
result = ECSB_UP_MOVE_SPEED; result = ECSB_UP_MOVE_SPEED;
break; break;
case EST_JUMP: case SkillType::JUMP:
result = ECSB_UP_JUMP_HEIGHT; result = ECSB_UP_JUMP_HEIGHT;
break; break;
case EST_STEALTH: case SkillType::STEALTH:
result = ECSB_UP_STEALTH; result = ECSB_UP_STEALTH;
break; break;
case EST_PHOENIX: case SkillType::PHOENIX:
result = ECSB_PHOENIX; result = ECSB_PHOENIX;
break; break;
case EST_PROTECTBATTERY: case SkillType::PROTECTBATTERY:
result = ECSB_PROTECT_BATTERY; result = ECSB_PROTECT_BATTERY;
break; break;
case EST_PROTECTINFECTION: case SkillType::PROTECTINFECTION:
result = ECSB_PROTECT_INFECTION; result = ECSB_PROTECT_INFECTION;
break; break;
case EST_SNARE: case SkillType::MINIMAPENEMY:
result = ECSB_DN_MOVE_SPEED;
break;
case EST_SLEEP:
result = ECSB_MEZ;
break;
case EST_MINIMAPENEMY:
result = ECSB_MINIMAP_ENEMY; result = ECSB_MINIMAP_ENEMY;
break; break;
case EST_MINIMAPTRESURE: case SkillType::MINIMAPTRESURE:
result = ECSB_MINIMAP_TRESURE; result = ECSB_MINIMAP_TRESURE;
break; break;
case EST_REWARDBLOB: case SkillType::REWARDBLOB:
result = ECSB_REWARD_BLOB; result = ECSB_REWARD_BLOB;
break; break;
case EST_REWARDCASH: case SkillType::REWARDCASH:
result = ECSB_REWARD_CASH; result = ECSB_REWARD_CASH;
break; break;
case EST_INFECTIONDAMAGE: case SkillType::FREEDOM:
result = ECSB_INFECTION;
break;
case EST_FREEDOM:
result = ECSB_FREEDOM; result = ECSB_FREEDOM;
break; break;
case EST_BOUNDINGBALL: case SkillType::INVULNERABLE:
result = ECSB_BOUNDINGBALL;
break;
case EST_INVULNERABLE:
result = ECSB_INVULNERABLE; result = ECSB_INVULNERABLE;
break; break;
case EST_BUFFHEAL: case SkillType::BUFFHEAL:
result = ECSB_HEAL; result = ECSB_HEAL;
break; break;
case EST_NANOSTIMPAK: case SkillType::NANOSTIMPAK:
result = ECSB_STIMPAKSLOT1; result = ECSB_STIMPAKSLOT1;
// shift as necessary
break;
case SkillType::SNARE:
result = ECSB_DN_MOVE_SPEED;
break;
case SkillType::STUN:
result = ECSB_STUN;
break;
case SkillType::SLEEP:
result = ECSB_MEZ;
break;
case SkillType::INFECTIONDAMAGE:
result = ECSB_INFECTION;
break;
case SkillType::BOUNDINGBALL:
result = ECSB_BOUNDINGBALL;
break;
default:
break; break;
} }
return result; return result;

View File

@@ -7,9 +7,52 @@
#include <map> #include <map>
#include <vector> #include <vector>
#include <assert.h>
const int COMBAT_TICKS_PER_DRAIN_PROC = 2;
constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain); constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain);
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,
};
enum class SkillEffectTarget { enum class SkillEffectTarget {
POINT = 1, POINT = 1,
SELF = 2, SELF = 2,
@@ -34,6 +77,7 @@ struct SkillResult {
size_t size; size_t size;
uint8_t payload[MAX_SKILLRESULT_SIZE]; uint8_t payload[MAX_SKILLRESULT_SIZE];
SkillResult(size_t len, void* dat) { SkillResult(size_t len, void* dat) {
assert(len <= MAX_SKILLRESULT_SIZE);
size = len; size = len;
memcpy(payload, dat, len); memcpy(payload, dat, len);
} }
@@ -43,7 +87,7 @@ struct SkillResult {
}; };
struct SkillData { struct SkillData {
int skillType; // eST SkillType skillType; // eST
SkillEffectTarget effectTarget; SkillEffectTarget effectTarget;
int effectType; // always 1? int effectType; // always 1?
SkillTargetType targetType; SkillTargetType targetType;
@@ -64,5 +108,5 @@ namespace Abilities {
void useNPCSkill(EntityRef, int skillID, std::vector<ICombatant*>); void useNPCSkill(EntityRef, int skillID, std::vector<ICombatant*>);
std::vector<ICombatant*> matchTargets(ICombatant*, SkillData*, int, int32_t*); std::vector<ICombatant*> matchTargets(ICombatant*, SkillData*, int, int32_t*);
int getCSTBFromST(int eSkillType); int getCSTBFromST(SkillType skillType);
} }

View File

@@ -95,6 +95,12 @@ int Buff::getValue(BuffValueSelector selector) {
return value; return value;
} }
EntityRef Buff::getLastSource() {
if(stacks.empty())
return self;
return stacks.back().source;
}
bool Buff::isStale() { bool Buff::isStale() {
return stacks.empty(); return stacks.empty();
} }
@@ -137,15 +143,56 @@ void Buffs::timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* st
self.sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE)); 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) { void Buffs::timeBuffTimeout(EntityRef self) {
if(self.kind != EntityKind::PLAYER && self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB) if(self.kind != EntityKind::PLAYER && self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not a combatant return; // not a combatant
Entity* entity = self.getEntity(); Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity); ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt); // send a buff timeout to other players INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt); // send a buff timeout to other players
pkt.eCT = combatant->getCharType(); int32_t eCharType = combatant->getCharType();
pkt.eCT = eCharType == 4 ? 2 : eCharType; // convention not followed by client here
pkt.iID = combatant->getID(); pkt.iID = combatant->getID();
pkt.iConditionBitFlag = combatant->getCompositeCondition(); pkt.iConditionBitFlag = combatant->getCompositeCondition();
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); 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 #pragma endregion

View File

@@ -66,6 +66,7 @@ public:
BuffClass maxClass(); BuffClass maxClass();
int getValue(BuffValueSelector selector); int getValue(BuffValueSelector selector);
EntityRef getLastSource();
/* /*
* In general, a Buff object won't exist * In general, a Buff object won't exist
@@ -86,5 +87,7 @@ public:
namespace Buffs { namespace Buffs {
void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack); void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack);
void timeBuffTick(EntityRef self, Buff* buff);
void timeBuffTimeout(EntityRef self); void timeBuffTimeout(EntityRef self);
void tickDrain(EntityRef self, Buff* buff, int mult);
} }

View File

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

View File

@@ -21,13 +21,14 @@ std::map<int32_t, std::map<int8_t, Bullet>> Combat::Bullets;
#pragma region Player #pragma region Player
bool Player::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) { bool Player::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) {
EntityRef self = PlayerManager::getSockFromID(iID); if(!isAlive())
return false;
if(!hasBuff(buffId)) { if(!hasBuff(buffId)) {
buffs[buffId] = new Buff(buffId, self, onUpdate, onTick, stack); buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack);
return true; return true;
} }
buffs[buffId]->updateCallbacks(onUpdate, onTick); buffs[buffId]->updateCallbacks(onUpdate, onTick);
buffs[buffId]->addStack(stack); buffs[buffId]->addStack(stack);
return false; return false;
@@ -48,9 +49,10 @@ void Player::removeBuff(int buffId) {
} }
} }
void Player::removeBuff(int buffId, int buffClass) { void Player::removeBuff(int buffId, BuffClass buffClass) {
if(hasBuff(buffId)) { if(hasBuff(buffId)) {
buffs[buffId]->clear((BuffClass)buffClass); buffs[buffId]->clear(buffClass);
// buff might not be stale since another buff class might remain
if(buffs[buffId]->isStale()) { if(buffs[buffId]->isStale()) {
delete buffs[buffId]; delete buffs[buffId];
buffs.erase(buffId); buffs.erase(buffId);
@@ -58,6 +60,16 @@ void Player::removeBuff(int buffId, int buffClass) {
} }
} }
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) { bool Player::hasBuff(int buffId) {
auto buff = buffs.find(buffId); auto buff = buffs.find(buffId);
return buff != buffs.end() && !buff->second->isStale(); return buff != buffs.end() && !buff->second->isStale();
@@ -156,29 +168,78 @@ void Player::step(time_t currTime) {
// buffs // buffs
for(auto buffEntry : buffs) { for(auto buffEntry : buffs) {
buffEntry.second->combatTick(currTime); buffEntry.second->combatTick(currTime);
if(!isAlive())
break; // unsafe to keep ticking if we're dead
} }
} }
#pragma endregion #pragma endregion
#pragma region CombatNPC #pragma region CombatNPC
bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) { /* stubbed */ 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; return false;
} }
Buff* CombatNPC::getBuff(int buffId) { /* stubbed */ Buff* CombatNPC::getBuff(int buffId) {
if(hasBuff(buffId)) {
return buffs[buffId];
}
return nullptr; return nullptr;
} }
void CombatNPC::removeBuff(int buffId) { /* stubbed */ } void CombatNPC::removeBuff(int buffId) {
if(hasBuff(buffId)) {
void CombatNPC::removeBuff(int buffId, int buffClass) { /* stubbed */ } buffs[buffId]->clear();
delete buffs[buffId];
bool CombatNPC::hasBuff(int buffId) { /* stubbed */ buffs.erase(buffId);
return false; }
} }
int CombatNPC::getCompositeCondition() { /* stubbed */ void CombatNPC::removeBuff(int buffId, BuffClass buffClass) {
return 0; 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 CombatNPC::takeDamage(EntityRef src, int amt) {
@@ -249,19 +310,19 @@ void CombatNPC::step(time_t currTime) {
} }
void CombatNPC::transition(AIState newState, EntityRef src) { void CombatNPC::transition(AIState newState, EntityRef src) {
state = newState; state = newState;
if (transitionHandlers.find(newState) != transitionHandlers.end()) if (transitionHandlers.find(newState) != transitionHandlers.end())
transitionHandlers[newState](this, src); transitionHandlers[newState](this, src);
else { else {
std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl; std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl;
transition(AIState::INACTIVE, id); transition(AIState::INACTIVE, id);
} }
/* TODO: fire any triggered events
// trigger special NPCEvents, if applicable
for (NPCEvent& event : NPCManager::NPCEvents) for (NPCEvent& event : NPCManager::NPCEvents)
if (event.trigger == ON_KILLED && event.npcType == type) if (event.triggerState == newState && event.npcType == type)
event.handler(src, this); event.handler(this);
*/
} }
#pragma endregion #pragma endregion
@@ -304,7 +365,7 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
return ret; return ret;
} }
static bool checkRapidFire(CNSocket *sock, int targetCount) { static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTargets) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
time_t currTime = getTime(); time_t currTime = getTime();
@@ -316,7 +377,7 @@ static bool checkRapidFire(CNSocket *sock, int targetCount) {
plr->lastShot = currTime; plr->lastShot = currTime;
// 3+ targets should never be possible // 3+ targets should never be possible
if (targetCount > 3) if (!allowManyTargets && targetCount > 3)
plr->suspicionRating += 10001; plr->suspicionRating += 10001;
// kill the socket when the player is too suspicious // kill the socket when the player is too suspicious
@@ -335,7 +396,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
auto targets = (int32_t*)data->trailers; auto targets = (int32_t*)data->trailers;
// kick the player if firing too rapidly // kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt)) if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt, false))
return; return;
/* /*
@@ -776,6 +837,10 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
return; return;
} }
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iTargetCnt, true))
return;
/* /*
* initialize response struct * initialize response struct
* rocket style hit doesn't work properly, so we're always sending this one * rocket style hit doesn't work properly, so we're always sending this one
@@ -874,9 +939,12 @@ static void playerTick(CNServer *serv, time_t currTime) {
if (Abilities::SkillTable.find(nano->iSkillID) != Abilities::SkillTable.end()) { if (Abilities::SkillTable.find(nano->iSkillID) != Abilities::SkillTable.end()) {
// nano has skill data // nano has skill data
SkillData* skill = &Abilities::SkillTable[nano->iSkillID]; SkillData* skill = &Abilities::SkillTable[nano->iSkillID];
if (skill->drainType == SkillDrainType::PASSIVE) if (skill->drainType == SkillDrainType::PASSIVE) {
Nanos::applyNanoBuff(skill, plr); ICombatant* src = dynamic_cast<ICombatant*>(plr);
// ^ composite condition calculation is separate from combat for responsiveness int32_t targets[] = { plr->iID };
std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets);
Abilities::useNanoSkill(sock, skill, *nano, affectedCombatants);
}
} }
} }
@@ -896,6 +964,7 @@ static void playerTick(CNServer *serv, time_t currTime) {
auto it = plr->buffs.begin(); auto it = plr->buffs.begin();
while(it != plr->buffs.end()) { while(it != plr->buffs.end()) {
Buff* buff = (*it).second; Buff* buff = (*it).second;
//buff->combatTick() gets called in Player::step
buff->tick(currTime); buff->tick(currTime);
if(buff->isStale()) { if(buff->isStale()) {
// garbage collect // garbage collect

View File

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

View File

@@ -26,6 +26,7 @@ void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
return; return;
} }
SkillResult result = SkillResult();
SkillData* skill = &Abilities::SkillTable[skillId]; SkillData* skill = &Abilities::SkillTable[skillId];
if(skill->drainType == SkillDrainType::PASSIVE) { if(skill->drainType == SkillDrainType::PASSIVE) {
// apply buff // apply buff
@@ -50,12 +51,57 @@ void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
// no-op // no-op
}, },
&eggBuff); &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);
} 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;
}
} }
// use skill // initialize response struct
std::vector<ICombatant*> targets; size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + result.size;
targets.push_back(dynamic_cast<ICombatant*>(plr)); uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
Abilities::useNPCSkill(src, skillId, targets); memset(respbuf, 0, resplen);
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);
}
NPCManager::sendToViewable(src.getEntity(), pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
} }
static void eggStep(CNServer* serv, time_t currTime) { static void eggStep(CNServer* serv, time_t currTime) {

View File

@@ -16,8 +16,9 @@ EntityRef::EntityRef(CNSocket *s) {
EntityRef::EntityRef(int32_t i) { EntityRef::EntityRef(int32_t i) {
id = i; id = i;
assert(NPCManager::NPCs.find(id) != NPCManager::NPCs.end()); kind = EntityKind::INVALID;
kind = NPCManager::NPCs[id]->kind; if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end())
kind = NPCManager::NPCs[id]->kind;
} }
bool EntityRef::isValid() const { bool EntityRef::isValid() const {
@@ -40,7 +41,7 @@ sNPCAppearanceData BaseNPC::getAppearanceData() {
sNPCAppearanceData data = {}; sNPCAppearanceData data = {};
data.iAngle = angle; data.iAngle = angle;
data.iBarkerType = 0; // unused? data.iBarkerType = 0; // unused?
data.iConditionBitFlag = cbf; data.iConditionBitFlag = 0;
data.iHP = hp; data.iHP = hp;
data.iNPCType = type; data.iNPCType = type;
data.iNPC_ID = id; data.iNPC_ID = id;
@@ -50,6 +51,12 @@ sNPCAppearanceData BaseNPC::getAppearanceData() {
return data; return data;
} }
sNPCAppearanceData CombatNPC::getAppearanceData() {
sNPCAppearanceData data = BaseNPC::getAppearanceData();
data.iConditionBitFlag = getCompositeCondition();
return data;
}
/* /*
* Entity coming into view. * Entity coming into view.
*/ */

View File

@@ -47,7 +47,8 @@ public:
virtual bool addBuff(int, BuffCallback<int, BuffStack*>, BuffCallback<time_t>, BuffStack*) = 0; virtual bool addBuff(int, BuffCallback<int, BuffStack*>, BuffCallback<time_t>, BuffStack*) = 0;
virtual Buff* getBuff(int) = 0; virtual Buff* getBuff(int) = 0;
virtual void removeBuff(int) = 0; virtual void removeBuff(int) = 0;
virtual void removeBuff(int, int) = 0; virtual void removeBuff(int, BuffClass) = 0;
virtual void clearBuffs(bool) = 0;
virtual bool hasBuff(int) = 0; virtual bool hasBuff(int) = 0;
virtual int getCompositeCondition() = 0; virtual int getCompositeCondition() = 0;
virtual int takeDamage(EntityRef, int) = 0; virtual int takeDamage(EntityRef, int) = 0;
@@ -72,7 +73,6 @@ public:
int type; int type;
int hp; int hp;
int angle; int angle;
int cbf;
bool loopingPath = false; bool loopingPath = false;
BaseNPC(int _A, uint64_t iID, int t, int _id) { BaseNPC(int _A, uint64_t iID, int t, int _id) {
@@ -80,7 +80,6 @@ public:
type = t; type = t;
hp = 400; hp = 400;
angle = _A; angle = _A;
cbf = 0;
id = _id; id = _id;
instanceID = iID; instanceID = iID;
}; };
@@ -88,7 +87,7 @@ public:
virtual void enterIntoViewOf(CNSocket *sock) override; virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override;
sNPCAppearanceData getAppearanceData(); virtual sNPCAppearanceData getAppearanceData();
}; };
struct CombatNPC : public BaseNPC, public ICombatant { struct CombatNPC : public BaseNPC, public ICombatant {
@@ -105,11 +104,13 @@ struct CombatNPC : public BaseNPC, public ICombatant {
std::map<AIState, void (*)(CombatNPC*, time_t)> stateHandlers; std::map<AIState, void (*)(CombatNPC*, time_t)> stateHandlers;
std::map<AIState, void (*)(CombatNPC*, EntityRef)> transitionHandlers; std::map<AIState, void (*)(CombatNPC*, EntityRef)> transitionHandlers;
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP) std::unordered_map<int, Buff*> buffs = {};
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) { : BaseNPC(angle, iID, t, id), maxHealth(maxHP) {
spawnX = x; this->spawnX = spawnX;
spawnY = y; this->spawnY = spawnY;
spawnZ = z; this->spawnZ = spawnZ;
kind = EntityKind::COMBAT_NPC; kind = EntityKind::COMBAT_NPC;
@@ -117,12 +118,15 @@ struct CombatNPC : public BaseNPC, public ICombatant {
transitionHandlers[AIState::INACTIVE] = {}; transitionHandlers[AIState::INACTIVE] = {};
} }
virtual sNPCAppearanceData getAppearanceData() override;
virtual bool isExtant() override { return hp > 0; } virtual bool isExtant() override { return hp > 0; }
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override; virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
virtual Buff* getBuff(int buffId) override; virtual Buff* getBuff(int buffId) override;
virtual void removeBuff(int buffId) override; virtual void removeBuff(int buffId) override;
virtual void removeBuff(int buffId, int buffClass) override; virtual void removeBuff(int buffId, BuffClass buffClass) override;
virtual void clearBuffs(bool force) override;
virtual bool hasBuff(int buffId) override; virtual bool hasBuff(int buffId) override;
virtual int getCompositeCondition() override; virtual int getCompositeCondition() override;
virtual int takeDamage(EntityRef src, int amt) override; virtual int takeDamage(EntityRef src, int amt) override;

View File

@@ -64,6 +64,9 @@ static void attachGroupData(std::vector<EntityRef>& pcs, std::vector<EntityRef>&
} }
void Groups::addToGroup(Group* group, EntityRef member) { void Groups::addToGroup(Group* group, EntityRef member) {
if (group == nullptr)
return;
if (member.kind == EntityKind::PLAYER) { if (member.kind == EntityKind::PLAYER) {
Player* plr = PlayerManager::getPlayer(member.sock); Player* plr = PlayerManager::getPlayer(member.sock);
plr->group = group; plr->group = group;
@@ -109,6 +112,9 @@ void Groups::addToGroup(Group* group, EntityRef member) {
} }
bool Groups::removeFromGroup(Group* group, EntityRef member) { bool Groups::removeFromGroup(Group* group, EntityRef member) {
if (group == nullptr)
return false;
if (member.kind == EntityKind::PLAYER) { if (member.kind == EntityKind::PLAYER) {
Player* plr = PlayerManager::getPlayer(member.sock); Player* plr = PlayerManager::getPlayer(member.sock);
plr->group = nullptr; // no dangling pointers here muahaahahah plr->group = nullptr; // no dangling pointers here muahaahahah
@@ -168,6 +174,9 @@ bool Groups::removeFromGroup(Group* group, EntityRef member) {
} }
void Groups::disbandGroup(Group* group) { void Groups::disbandGroup(Group* group) {
if (group == nullptr)
return;
// remove everyone from the group!! // remove everyone from the group!!
bool done = false; bool done = false;
while(!done) { while(!done) {
@@ -252,6 +261,9 @@ static void leaveGroup(CNSocket* sock, CNPacketData* data) {
} }
void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) { void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) {
if (group == nullptr)
return;
auto players = group->filter(EntityKind::PLAYER); auto players = group->filter(EntityKind::PLAYER);
for (EntityRef ref : players) { for (EntityRef ref : players) {
ref.sock->sendPacket(buf, type, size); ref.sock->sendPacket(buf, type, size);
@@ -259,6 +271,9 @@ void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) {
} }
void Groups::sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size) { void Groups::sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size) {
if (group == nullptr)
return;
auto players = group->filter(EntityKind::PLAYER); auto players = group->filter(EntityKind::PLAYER);
for (EntityRef ref : players) { for (EntityRef ref : players) {
if(ref != excluded) ref.sock->sendPacket(buf, type, size); if(ref != excluded) ref.sock->sendPacket(buf, type, size);
@@ -294,6 +309,9 @@ void Groups::groupTickInfo(CNSocket* sock) {
void Groups::groupKick(Group* group, EntityRef ref) { void Groups::groupKick(Group* group, EntityRef ref) {
if (group == nullptr)
return;
// if you are the group leader, destroy your own group and kick everybody // if you are the group leader, destroy your own group and kick everybody
if (group->members[0] == ref) { if (group->members[0] == ref) {
disbandGroup(group); disbandGroup(group);

View File

@@ -3,6 +3,7 @@
#include "EntityRef.hpp" #include "EntityRef.hpp"
#include <vector> #include <vector>
#include <assert.h>
struct Group { struct Group {
std::vector<EntityRef> members; std::vector<EntityRef> members;
@@ -14,6 +15,10 @@ struct Group {
}); });
return filtered; return filtered;
} }
EntityRef getLeader() {
assert(members.size() > 0);
return members[0];
}
Group(EntityRef leader); Group(EntityRef leader);
}; };

View File

@@ -416,6 +416,9 @@ static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
if (itemdel->iSlotNum < 0 || itemdel->iSlotNum >= AINVEN_COUNT)
return; // sanity check
resp.eIL = itemdel->eIL; resp.eIL = itemdel->eIL;
resp.iSlotNum = itemdel->iSlotNum; resp.iSlotNum = itemdel->iSlotNum;
@@ -482,7 +485,7 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
resp->iSlotNum = request->iSlotNum; resp->iSlotNum = request->iSlotNum;
resp->RemainItem = gumball; resp->RemainItem = gumball;
resp->iTargetCnt = 1; resp->iTargetCnt = 1;
resp->eST = EST_NANOSTIMPAK; resp->eST = (int32_t)SkillType::NANOSTIMPAK;
resp->iSkillID = 144; resp->iSkillID = 144;
int eCSB = ECSB_STIMPAKSLOT1 + request->iNanoSlot; int eCSB = ECSB_STIMPAKSLOT1 + request->iNanoSlot;

View File

@@ -386,6 +386,9 @@ static void taskStart(CNSocket* sock, CNPacketData* data) {
static void taskEnd(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; 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]; TaskData* task = Missions::Tasks[missionData->iTaskNum];
// handle timed mission failure // handle timed mission failure

View File

@@ -51,13 +51,13 @@ int Mob::takeDamage(EntityRef src, int amt) {
} }
// wake up sleeping monster // wake up sleeping monster
if (cbf & CSB_BIT_MEZ) { if (hasBuff(ECSB_MEZ)) {
cbf &= ~CSB_BIT_MEZ; removeBuff(ECSB_MEZ);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2; pkt1.eCT = 2;
pkt1.iID = id; pkt1.iID = id;
pkt1.iConditionBitFlag = cbf; pkt1.iConditionBitFlag = getCompositeCondition();
NPCManager::sendToViewable(this, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); NPCManager::sendToViewable(this, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
} }
@@ -96,13 +96,12 @@ static std::pair<int,int> lerp(int x1, int y1, int x2, int y2, int speed) {
void MobAI::clearDebuff(Mob *mob) { void MobAI::clearDebuff(Mob *mob) {
mob->skillStyle = -1; mob->skillStyle = -1;
mob->cbf = 0; mob->clearBuffs(false);
mob->unbuffTimes.clear();
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2; pkt1.eCT = 2;
pkt1.iID = mob->id; pkt1.iID = mob->id;
pkt1.iConditionBitFlag = mob->cbf; pkt1.iConditionBitFlag = mob->getCompositeCondition();
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
} }
@@ -228,7 +227,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
return false; return false;
} }
static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int style) { static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int mobStyle) {
Player *plr = PlayerManager::getPlayer(mob->target); Player *plr = PlayerManager::getPlayer(mob->target);
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT) + targetData[0] * sizeof(sCAttackResult); size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT) + targetData[0] * sizeof(sCAttackResult);
@@ -247,7 +246,7 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
resp->iNPC_ID = mob->id; resp->iNPC_ID = mob->id;
resp->iSkillID = skillID; resp->iSkillID = skillID;
resp->iStyle = style; resp->iStyle = mobStyle;
resp->iValue1 = plr->x; resp->iValue1 = plr->x;
resp->iValue2 = plr->y; resp->iValue2 = plr->y;
resp->iValue3 = plr->z; resp->iValue3 = plr->z;
@@ -281,26 +280,38 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
respdata[i].iActiveNanoSlotNum = n; respdata[i].iActiveNanoSlotNum = n;
respdata[i].iNanoID = plr->activeNano; respdata[i].iNanoID = plr->activeNano;
int style2 = Nanos::nanoStyle(plr->activeNano); int nanoStyle = Nanos::nanoStyle(plr->activeNano);
if (style2 == -1) { // no nano if (nanoStyle == -1) { // no nano
respdata[i].iHitFlag = HF_BIT_STYLE_TIE; respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
} else if (style == style2) { } else if (mobStyle == nanoStyle) {
respdata[i].iHitFlag = HF_BIT_STYLE_TIE; respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
respdata[i].iDamage = 0; respdata[i].iDamage = 0;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina; respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
} else if (style - style2 == 1 || style2 - style == 2) { } else if (mobStyle - nanoStyle == 1 || nanoStyle - mobStyle == 2) {
respdata[i].iHitFlag = HF_BIT_STYLE_WIN; respdata[i].iHitFlag = HF_BIT_STYLE_WIN;
respdata[i].iDamage = 0; respdata[i].iDamage = 0;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45; respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45;
if (plr->Nanos[plr->activeNano].iStamina > 150) if (plr->Nanos[plr->activeNano].iStamina > 150)
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 150; respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 150;
// fire damage power disguised as a corruption attack back at the enemy // fire damage power disguised as a corruption attack back at the enemy
// TODO ABILITIES SkillData skill = {
/*std::vector<int> targetData2 = {1, mob->id, 0, 0, 0}; SkillType::DAMAGE, // skillType
for (auto& pwr : Abilities::Powers) SkillEffectTarget::POINT, // effectTarget
if (pwr.skillType == EST_DAMAGE) 1, // effectType
pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200);*/ SkillTargetType::MOBS, // targetType
SkillDrainType::ACTIVE, // drainType
0, // effectArea
{0, 0, 0, 0}, // batteryUse
{0, 0, 0, 0}, // durationTime
{0, 0, 0}, // valueTypes (unused)
{
{200, 200, 200, 200},
{200, 200, 200, 200},
{200, 200, 200, 200},
}
};
Abilities::useNanoSkill(sock, &skill, *plr->getActiveNano(), { mob });
} else { } else {
respdata[i].iHitFlag = HF_BIT_STYLE_LOSE; respdata[i].iHitFlag = HF_BIT_STYLE_LOSE;
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
@@ -327,12 +338,6 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
} }
static void useAbilities(Mob *mob, time_t currTime) { static void useAbilities(Mob *mob, time_t currTime) {
/*
* targetData approach
* first integer is the count
* second to fifth integers are IDs, these can be either player iID or mob's iID
* whether the skill targets players or mobs is determined by the skill packet being fired
*/
Player *plr = PlayerManager::getPlayer(mob->target); Player *plr = PlayerManager::getPlayer(mob->target);
if (mob->skillStyle >= 0) { // corruption hit if (mob->skillStyle >= 0) { // corruption hit
@@ -347,7 +352,7 @@ static void useAbilities(Mob *mob, time_t currTime) {
if (mob->skillStyle == -2) { // eruption hit if (mob->skillStyle == -2) { // eruption hit
int skillID = (int)mob->data["m_iMegaType"]; int skillID = (int)mob->data["m_iMegaType"];
std::vector<int> targetData = {0, 0, 0, 0, 0}; std::vector<ICombatant*> targets{};
// find the players within range of eruption // find the players within range of eruption
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) { for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
@@ -357,26 +362,22 @@ static void useAbilities(Mob *mob, time_t currTime) {
if (ref.kind != EntityKind::PLAYER) if (ref.kind != EntityKind::PLAYER)
continue; continue;
CNSocket *s= ref.sock; CNSocket *s = ref.sock;
Player *plr = PlayerManager::getPlayer(s); Player *plr = PlayerManager::getPlayer(s);
if (plr->HP <= 0) if (!plr->isAlive())
continue; continue;
int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y); int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y);
if (distance < Abilities::SkillTable[skillID].effectArea) { if (distance < Abilities::SkillTable[skillID].effectArea) {
targetData[0] += 1; targets.push_back(plr);
targetData[targetData[0]] = plr->iID; if (targets.size() > 3) // make sure not to have more than 4
if (targetData[0] > 3) // make sure not to have more than 4
break; break;
} }
} }
} }
// TODO ABILITIES Abilities::useNPCSkill(mob->id, skillID, targets);
/*for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);*/
mob->skillStyle = -3; // eruption cooldown mob->skillStyle = -3; // eruption cooldown
mob->nextAttack = currTime + 1000; mob->nextAttack = currTime + 1000;
return; return;
@@ -394,14 +395,11 @@ static void useAbilities(Mob *mob, time_t currTime) {
if (random < prob1) { // active skill hit if (random < prob1) { // active skill hit
int skillID = (int)mob->data["m_iActiveSkill1"]; int skillID = (int)mob->data["m_iActiveSkill1"];
// TODO ABILITIES SkillData* skill = &Abilities::SkillTable[skillID];
//std::vector<int> targetData = {1, plr->iID, 0, 0, 0}; int debuffID = Abilities::getCSTBFromST(skill->skillType);
//for (auto& pwr : Abilities::Powers) if(plr->hasBuff(debuffID))
// if (pwr.skillType == Abilities::SkillTable[skillID].skillType) { return; // prevent debuffing a player twice
// if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag)) Abilities::useNPCSkill(mob->getRef(), skillID, { plr });
// return; // prevent debuffing a player twice
// pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);
// }
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100; mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
return; return;
} }
@@ -442,31 +440,6 @@ static void useAbilities(Mob *mob, time_t currTime) {
return; return;
} }
static void drainMobHP(Mob *mob, int amount) {
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
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;
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
pkt->iID = mob->id;
pkt->eCT = 4; // mob
pkt->iTB_ID = ECSB_BOUNDINGBALL;
drain->eCT = 4;
drain->iID = mob->id;
drain->iDamage = amount;
drain->iHP = mob->hp -= amount;
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
if (mob->hp <= 0)
mob->transition(AIState::DEAD, mob->target);
}
void MobAI::incNextMovement(Mob* mob, time_t currTime) { void MobAI::incNextMovement(Mob* mob, time_t currTime) {
if (currTime == 0) if (currTime == 0)
currTime = getTime(); currTime = getTime();
@@ -553,38 +526,27 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
return; return;
} }
// drain // tick buffs
if (self->skillStyle < 0 && (self->lastDrainTime == 0 || currTime - self->lastDrainTime >= 1000) auto it = npc->buffs.begin();
&& self->cbf & CSB_BIT_BOUNDINGBALL) { while(it != npc->buffs.end()) {
drainMobHP(self, self->maxHealth / 20); // lose 5% every second Buff* buff = (*it).second;
self->lastDrainTime = currTime; buff->combatTick(currTime);
}
// if drain killed the mob, return early // if mob state changed, end the step
if (self->hp <= 0) if(self->state != AIState::COMBAT)
return; return;
// unbuffing buff->tick(currTime);
std::unordered_map<int32_t, time_t>::iterator it = self->unbuffTimes.begin(); if(buff->isStale()) {
while (it != self->unbuffTimes.end()) { // garbage collect
it = npc->buffs.erase(it);
if (currTime >= it->second) { delete buff;
self->cbf &= ~it->first;
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = self->id;
pkt1.iConditionBitFlag = self->cbf;
NPCManager::sendToViewable(self, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
it = self->unbuffTimes.erase(it);
} else {
it++;
} }
else it++;
} }
// skip attack if stunned or asleep // skip attack if stunned or asleep
if (self->cbf & (CSB_BIT_STUN|CSB_BIT_MEZ)) { if (self->hasBuff(ECSB_STUN) || self->hasBuff(ECSB_MEZ)) {
self->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up. self->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
return; return;
} }
@@ -600,6 +562,7 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
} }
int distanceToTravel = INT_MAX; int distanceToTravel = INT_MAX;
int speed = self->speed;
// movement logic: move when out of range but don't move while casting a skill // movement logic: move when out of range but don't move while casting a skill
if (distance > mobRange && self->skillStyle == -1) { if (distance > mobRange && self->skillStyle == -1) {
if (self->nextMovement != 0 && currTime < self->nextMovement) if (self->nextMovement != 0 && currTime < self->nextMovement)
@@ -609,8 +572,8 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
self->nextAttack = 0; self->nextAttack = 0;
// halve movement speed if snared // halve movement speed if snared
if (self->cbf & CSB_BIT_DN_MOVE_SPEED) if (self->hasBuff(ECSB_DN_MOVE_SPEED))
self->speed /= 2; speed /= 2;
int targetX = plr->x; int targetX = plr->x;
int targetY = plr->y; int targetY = plr->y;
@@ -619,9 +582,9 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
targetY += self->offsetY*distance/(self->idleRange + 1); targetY += self->offsetY*distance/(self->idleRange + 1);
} }
distanceToTravel = std::min(distance-mobRange+1, self->speed*2/5); distanceToTravel = std::min(distance-mobRange+1, speed*2/5);
auto targ = lerp(self->x, self->y, targetX, targetY, distanceToTravel); auto targ = lerp(self->x, self->y, targetX, targetY, distanceToTravel);
if (distanceToTravel < self->speed*2/5 && currTime >= self->nextAttack) if (distanceToTravel < speed*2/5 && currTime >= self->nextAttack)
self->nextAttack = 0; self->nextAttack = 0;
NPCManager::updateNPCPosition(self->id, targ.first, targ.second, self->z, self->instanceID, self->angle); NPCManager::updateNPCPosition(self->id, targ.first, targ.second, self->z, self->instanceID, self->angle);
@@ -629,7 +592,7 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt); INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
pkt.iNPC_ID = self->id; pkt.iNPC_ID = self->id;
pkt.iSpeed = self->speed; pkt.iSpeed = speed;
pkt.iToX = self->x = targ.first; pkt.iToX = self->x = targ.first;
pkt.iToY = self->y = targ.second; pkt.iToY = self->y = targ.second;
pkt.iToZ = plr->z; pkt.iToZ = plr->z;
@@ -715,7 +678,7 @@ void MobAI::roamingStep(CombatNPC* npc, time_t currTime) {
farY = std::clamp(farY, yStart, yStart + self->idleRange); farY = std::clamp(farY, yStart, yStart + self->idleRange);
// halve movement speed if snared // halve movement speed if snared
if (self->cbf & CSB_BIT_DN_MOVE_SPEED) if (self->hasBuff(ECSB_DN_MOVE_SPEED))
self->speed /= 2; self->speed /= 2;
std::queue<Vec3> queue; std::queue<Vec3> queue;
@@ -788,14 +751,10 @@ void MobAI::onRoamStart(CombatNPC* npc, EntityRef src) {
self->hp = self->maxHealth; self->hp = self->maxHealth;
self->killedTime = 0; self->killedTime = 0;
self->nextAttack = 0; self->nextAttack = 0;
self->cbf = 0;
// cast a return home heal spell, this is the right way(tm) // cast a return home heal spell, this is the right way(tm)
// TODO ABILITIES Abilities::useNPCSkill(npc->getRef(), 110, { npc });
/*std::vector<int> targetData = { 1, 0, 0, 0, 0 };
for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[110].skillType)
pwr.handle(self->id, targetData, 110, Abilities::SkillTable[110].durationTime[0], Abilities::SkillTable[110].powerIntensity[0]);*/
// clear outlying debuffs // clear outlying debuffs
clearDebuff(self); clearDebuff(self);
} }
@@ -812,12 +771,9 @@ void MobAI::onCombatStart(CombatNPC* npc, EntityRef src) {
self->roamY = self->y; self->roamY = self->y;
self->roamZ = self->z; self->roamZ = self->z;
int skillID = (int)self->data["m_iPassiveBuff"]; // cast passive int skillID = (int)self->data["m_iPassiveBuff"];
// TODO ABILITIES if(skillID != 0) // cast passive
/*std::vector<int> targetData = { 1, self->id, 0, 0, 0 }; Abilities::useNPCSkill(npc->getRef(), skillID, { npc });
for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
pwr.handle(self->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);*/
} }
void MobAI::onRetreat(CombatNPC* npc, EntityRef src) { void MobAI::onRetreat(CombatNPC* npc, EntityRef src) {
@@ -833,9 +789,8 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
Mob* self = (Mob*)npc; Mob* self = (Mob*)npc;
self->target = nullptr; self->target = nullptr;
self->cbf = 0;
self->skillStyle = -1; self->skillStyle = -1;
self->unbuffTimes.clear(); self->clearBuffs(true);
self->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step? self->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
// check for the edge case where hitting the mob did not aggro it // check for the edge case where hitting the mob did not aggro it

View File

@@ -21,8 +21,6 @@ namespace MobAI {
} }
struct Mob : public CombatNPC { struct Mob : public CombatNPC {
// general
std::unordered_map<int32_t,time_t> unbuffTimes = {};
// dead // dead
time_t killedTime = 0; time_t killedTime = 0;
@@ -52,8 +50,8 @@ struct Mob : public CombatNPC {
// temporary; until we're sure what's what // temporary; until we're sure what's what
nlohmann::json data = {}; nlohmann::json data = {};
Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id) Mob(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]), : CombatNPC(spawnX, spawnY, spawnZ, angle, iID, t, id, d["m_iHP"]),
sightRange(d["m_iSightRange"]) { sightRange(d["m_iSightRange"]) {
state = AIState::ROAMING; state = AIState::ROAMING;
@@ -64,15 +62,13 @@ struct Mob : public CombatNPC {
idleRange = (int)data["m_iIdleRange"]; idleRange = (int)data["m_iIdleRange"];
level = data["m_iNpcLevel"]; level = data["m_iNpcLevel"];
roamX = x; roamX = spawnX;
roamY = y; roamY = spawnY;
roamZ = z; roamZ = spawnZ;
offsetX = 0; offsetX = 0;
offsetY = 0; offsetY = 0;
cbf = 0;
// NOTE: there appear to be discrepancies in the dump // NOTE: there appear to be discrepancies in the dump
hp = maxHealth; hp = maxHealth;

View File

@@ -94,20 +94,49 @@ void NPCManager::sendToViewable(Entity *npc, void *buf, uint32_t type, size_t si
static void npcBarkHandler(CNSocket* sock, CNPacketData* data) { static void npcBarkHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf; sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf;
// get bark IDs from task data int taskID = req->iMissionTaskID;
TaskData* td = Missions::Tasks[req->iMissionTaskID]; // ignore req->iNPC_ID as it is often fixated on a single npc in the region
std::vector<int> barks;
for (int i = 0; i < 4; i++) { if (Missions::Tasks.find(taskID) == Missions::Tasks.end()) {
if (td->task["m_iHBarkerTextID"][i] != 0) // non-zeroes only std::cout << "mission task not found: " << taskID << std::endl;
barks.push_back(td->task["m_iHBarkerTextID"][i]); return;
} }
if (barks.empty()) TaskData* td = Missions::Tasks[taskID];
return; // no barks 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())];
INITSTRUCT(sP_FE2CL_REP_BARKER, resp); INITSTRUCT(sP_FE2CL_REP_BARKER, resp);
resp.iNPC_ID = req->iNPC_ID; resp.iNPC_ID = npcID;
resp.iMissionStringID = barks[Rand::rand(barks.size())]; resp.iMissionStringID = missionStringID;
sock->sendPacket(resp, P_FE2CL_REP_BARKER); sock->sendPacket(resp, P_FE2CL_REP_BARKER);
} }
@@ -122,16 +151,15 @@ static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
} }
// type must already be checked and updateNPCPosition() must be called on the result // type must already be checked and updateNPCPosition() must be called on the result
BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) { BaseNPC *NPCManager::summonNPC(int spawnX, int spawnY, int spawnZ, uint64_t instance, int type, bool respawn, bool baseInstance) {
uint64_t inst = baseInstance ? MAPNUM(instance) : instance; uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
//assert(nextId < INT32_MAX);
int id = nextId--; int id = nextId--;
int team = NPCData[type]["m_iTeam"]; int team = NPCData[type]["m_iTeam"];
BaseNPC *npc = nullptr; BaseNPC *npc = nullptr;
if (team == 2) { if (team == 2) {
npc = new Mob(x, y, z, inst, type, NPCData[type], id); npc = new Mob(spawnX, spawnY, spawnZ, inst, type, NPCData[type], id);
// re-enable respawning, if desired // re-enable respawning, if desired
((Mob*)npc)->summoned = !respawn; ((Mob*)npc)->summoned = !respawn;
@@ -294,57 +322,55 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
return npc; return npc;
} }
// TODO: Move this to MobAI, possibly // TODO: Move this to separate file in ai/ subdir when implementing more events
#pragma region NPCEvents #pragma region NPCEvents
// summon right arm and stage 2 body // summon right arm and stage 2 body
static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) { static void lordFuseStageTwo(CombatNPC *npc) {
Mob *oldbody = (Mob*)npc; // adaptium, stun Mob *oldbody = (Mob*)npc; // adaptium, stun
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage two" << std::endl; std::cout << "Lord Fuse stage two" << std::endl;
// Fuse doesn't move // Fuse doesn't move
// Blastons, Heal // Blastons, Heal
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2467); Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2467);
newbody->angle = oldbody->angle; newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z, NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
plr->instanceID, oldbody->angle); oldbody->instanceID, oldbody->angle);
// right arm, Adaptium, Stun // right arm, Adaptium, Stun
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, plr->instanceID, 2469); Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, oldbody->instanceID, 2469);
arm->angle = oldbody->angle; arm->angle = oldbody->angle;
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z, NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
plr->instanceID, oldbody->angle); oldbody->instanceID, oldbody->angle);
} }
// summon left arm and stage 3 body // summon left arm and stage 3 body
static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) { static void lordFuseStageThree(CombatNPC *npc) {
Mob *oldbody = (Mob*)npc; Mob *oldbody = (Mob*)npc;
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage three" << std::endl; std::cout << "Lord Fuse stage three" << std::endl;
// Cosmix, Damage Point // Cosmix, Damage Point
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2468); Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2468);
newbody->angle = oldbody->angle; newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z, NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
plr->instanceID, oldbody->angle); newbody->instanceID, oldbody->angle);
// Blastons, Heal // Blastons, Heal
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, plr->instanceID, 2470); Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, oldbody->instanceID, 2470);
arm->angle = oldbody->angle; arm->angle = oldbody->angle;
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z, NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
plr->instanceID, oldbody->angle); arm->instanceID, oldbody->angle);
} }
std::vector<NPCEvent> NPCManager::NPCEvents = { std::vector<NPCEvent> NPCManager::NPCEvents = {
NPCEvent(2466, ON_KILLED, lordFuseStageTwo), NPCEvent(2466, AIState::DEAD, lordFuseStageTwo),
NPCEvent(2467, ON_KILLED, lordFuseStageThree), NPCEvent(2467, AIState::DEAD, lordFuseStageThree),
}; };
#pragma endregion NPCEvents #pragma endregion NPCEvents
@@ -376,5 +402,5 @@ void NPCManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler);
REGISTER_SHARD_TIMER(step, 200); REGISTER_SHARD_TIMER(step, MS_PER_COMBAT_TICK);
} }

View File

@@ -14,20 +14,15 @@
#define RESURRECT_HEIGHT 400 #define RESURRECT_HEIGHT 400
enum Trigger { typedef void (*NPCEventHandler)(CombatNPC*);
ON_KILLED,
ON_COMBAT
};
typedef void (*NPCEventHandler)(CNSocket*, BaseNPC*);
struct NPCEvent { struct NPCEvent {
int32_t npcType; int32_t npcType;
int trigger; AIState triggerState;
NPCEventHandler handler; NPCEventHandler handler;
NPCEvent(int32_t t, int tr, NPCEventHandler hndlr) NPCEvent(int32_t t, AIState tr, NPCEventHandler hndlr)
: npcType(t), trigger(tr), handler(hndlr) {} : npcType(t), triggerState(tr), handler(hndlr) {}
}; };
namespace NPCManager { namespace NPCManager {

View File

@@ -69,43 +69,6 @@ void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm)
PlayerManager::sendToViewable(sock, resp2, P_FE2CL_REP_PC_CHANGE_LEVEL); PlayerManager::sendToViewable(sock, resp2, P_FE2CL_REP_PC_CHANGE_LEVEL);
} }
std::vector<ICombatant*> Nanos::applyNanoBuff(SkillData* skill, Player* plr) {
assert(skill->drainType == SkillDrainType::PASSIVE);
EntityRef self = PlayerManager::getSockFromID(plr->iID);
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
int boost = Nanos::getNanoBoost(plr) ? 3 : 0;
int value = skill->values[0][boost];
BuffStack passiveBuff = {
1, // passive nano buffs refreshed every tick
value,
self,
BuffClass::NONE, // overwritten per target
};
// for passive skills, using just the player as a target is fine
// this is because the group skill type will ignore the count,
// and the other option is single-target
std::vector<ICombatant*> targets = Abilities::matchTargets(dynamic_cast<ICombatant*>(plr), skill, 1, &plr->iID);
std::vector<ICombatant*> affected;
for (ICombatant* target : targets) {
passiveBuff.buffStackClass = target == plr ? BuffClass::NANO : BuffClass::GROUP_NANO;
if(target->addBuff(timeBuffId,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
},
[](EntityRef self, Buff* buff, time_t currTime) {
// no-op
},
&passiveBuff)) affected.push_back(target);
}
return affected;
}
void Nanos::summonNano(CNSocket *sock, int slot, bool silent) { void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
INITSTRUCT(sP_FE2CL_REP_NANO_ACTIVE_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_NANO_ACTIVE_SUCC, resp);
resp.iActiveNanoSlotNum = slot; resp.iActiveNanoSlotNum = slot;
@@ -130,8 +93,10 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
if (slot != -1 && skill != nullptr && skill->drainType == SkillDrainType::PASSIVE) { if (slot != -1 && skill != nullptr && skill->drainType == SkillDrainType::PASSIVE) {
// passive buff effect // passive buff effect
resp.eCSTB___Add = 1; resp.eCSTB___Add = 1;
std::vector<ICombatant*> affectedCombatants = applyNanoBuff(skill, plr); ICombatant* src = dynamic_cast<ICombatant*>(plr);
if(!affectedCombatants.empty()) Abilities::useNanoSkill(sock, skill, nano, affectedCombatants); int32_t targets[] = { plr->iID };
std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets);
Abilities::useNanoSkill(sock, skill, nano, affectedCombatants);
} }
if (!silent) // silent nano death but only for the summoning player if (!silent) // silent nano death but only for the summoning player
@@ -145,7 +110,7 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
} }
static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) { static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
if (skill->iNanoID >= NANO_COUNT) if (skill == nullptr || skill->iNanoID >= NANO_COUNT || skill->iNanoID < 0)
return; return;
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
@@ -312,7 +277,7 @@ static void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) {
std::vector<ICombatant*> targetData = Abilities::matchTargets(plrCombatant, skillData, pkt->iTargetCnt, (int32_t*)(pkt + 1)); std::vector<ICombatant*> targetData = Abilities::matchTargets(plrCombatant, skillData, pkt->iTargetCnt, (int32_t*)(pkt + 1));
Abilities::useNanoSkill(sock, skillData, nano, targetData); Abilities::useNanoSkill(sock, skillData, nano, targetData);
if (plr->Nanos[plr->activeNano].iStamina < 0) if (plr->Nanos[plr->activeNano].iStamina <= 0)
summonNano(sock, -1); summonNano(sock, -1);
} }

View File

@@ -26,5 +26,4 @@ namespace Nanos {
void summonNano(CNSocket* sock, int slot, bool silent = false); void summonNano(CNSocket* sock, int slot, bool silent = false);
int nanoStyle(int nanoID); int nanoStyle(int nanoID);
bool getNanoBoost(Player* plr); bool getNanoBoost(Player* plr);
std::vector<ICombatant*> applyNanoBuff(SkillData* skill, Player* plr);
} }

View File

@@ -92,7 +92,8 @@ struct Player : public Entity, public ICombatant {
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override; virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
virtual Buff* getBuff(int buffId) override; virtual Buff* getBuff(int buffId) override;
virtual void removeBuff(int buffId) override; virtual void removeBuff(int buffId) override;
virtual void removeBuff(int buffId, int buffClass) override; virtual void removeBuff(int buffId, BuffClass buffClass) override;
virtual void clearBuffs(bool force) override;
virtual bool hasBuff(int buffId) override; virtual bool hasBuff(int buffId) override;
virtual int getCompositeCondition() override; virtual int getCompositeCondition() override;
virtual int takeDamage(EntityRef src, int amt) override; virtual int takeDamage(EntityRef src, int amt) override;

View File

@@ -77,7 +77,10 @@ void PlayerManager::updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, ui
plr->x = X; plr->x = X;
plr->y = Y; plr->y = Y;
plr->z = Z; plr->z = Z;
plr->instanceID = I; if (plr->instanceID != I) {
plr->instanceID = I;
plr->recallInstance = INSTANCE_OVERWORLD;
}
if (oldChunk == newChunk) if (oldChunk == newChunk)
return; // didn't change chunks return; // didn't change chunks
Chunking::updateEntityChunk({sock}, oldChunk, newChunk); Chunking::updateEntityChunk({sock}, oldChunk, newChunk);
@@ -123,24 +126,6 @@ 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); 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); INITSTRUCT(sP_FE2CL_REP_PC_GOTO_SUCC, pkt2);
pkt2.iX = X; pkt2.iX = X;
pkt2.iY = Y; pkt2.iY = Y;
@@ -338,6 +323,14 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
delete lm; 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) { void PlayerManager::sendToViewable(CNSocket* sock, void* buf, uint32_t type, size_t size) {
Player* plr = getPlayer(sock); Player* plr = getPlayer(sock);
for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) { for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) {
@@ -366,6 +359,24 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) {
updatePlayerPosition(sock, plr->x, plr->y, plr->z, plr->instanceID, plr->angle); updatePlayerPosition(sock, plr->x, plr->y, plr->z, plr->instanceID, plr->angle);
sock->sendPacket(response, P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC); 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);
}
} }
static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) { static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
@@ -408,6 +419,7 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
default: // plain respawn default: // plain respawn
plr->HP = PC_MAXHEALTH(plr->level) / 2; plr->HP = PC_MAXHEALTH(plr->level) / 2;
plr->clearBuffs(false);
// fallthrough // fallthrough
case ePCRegenType::Unstick: // warp away case ePCRegenType::Unstick: // warp away
move = true; move = true;
@@ -568,7 +580,7 @@ static void setFirstUseFlag(CNSocket* sock, CNPacketData* data) {
std::cout << "[WARN] Client submitted invalid first use flag number?!" << std::endl; std::cout << "[WARN] Client submitted invalid first use flag number?!" << std::endl;
return; return;
} }
if (flag->iFlagCode <= 64) if (flag->iFlagCode <= 64)
plr->iFirstUseFlag[0] |= (1ULL << (flag->iFlagCode - 1)); plr->iFirstUseFlag[0] |= (1ULL << (flag->iFlagCode - 1));
else else

View File

@@ -33,6 +33,7 @@ namespace PlayerManager {
CNSocket *getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname); CNSocket *getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname);
WarpLocation *getRespawnPoint(Player *plr); 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); void sendToViewable(CNSocket *sock, void* buf, uint32_t type, size_t size);
// TODO: unify this under the new Entity system // TODO: unify this under the new Entity system

View File

@@ -66,7 +66,7 @@ static void racingCancel(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC); sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
/* /*
* This request packet is used for both cancelling the race via the * 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. * 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 * If the latter is to happen, the client disables movement until it
@@ -99,35 +99,43 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0) if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
return; // IZ not found return; // IZ not found
uint64_t now = getTime() / 1000; EPInfo& epInfo = EPData[mapNum];
EPRace& epRace = EPRaces[sock];
int timeDiff = now - EPRaces[sock].startTime; uint64_t now = getTime() / 1000;
int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff; int timeDiff = now - epRace.startTime;
if (score < 0) score = 0; // lol int podsCollected = epRace.collectedRings.size();
int fm = score * plr->level * (1.0f / 36) * 0.3f;
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;
// we submit the ranking first... // we submit the ranking first...
Database::RaceRanking postRanking = {}; Database::RaceRanking postRanking = {};
postRanking.EPID = EPData[mapNum].EPID; postRanking.EPID = epInfo.EPID;
postRanking.PlayerID = plr->iID; postRanking.PlayerID = plr->iID;
postRanking.RingCount = EPRaces[sock].collectedRings.size(); postRanking.RingCount = podsCollected;
postRanking.Score = score; postRanking.Score = score;
postRanking.Time = timeDiff; postRanking.Time = timeDiff;
postRanking.Timestamp = getTimestamp(); postRanking.Timestamp = getTimestamp();
Database::postRaceRanking(postRanking); Database::postRaceRanking(postRanking);
// ...then we get the top ranking, which may or may not be what we just submitted // ...then we get the top ranking, which may or may not be what we just submitted
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(EPData[mapNum].EPID, plr->iID); Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(epInfo.EPID, plr->iID);
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
// get rank scores and rewards // get rank scores and rewards
std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first; std::vector<int>* rankScores = &EPRewards[epInfo.EPID].first;
std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second; std::vector<int>* rankRewards = &EPRewards[epInfo.EPID].second;
// top ranking // top ranking
int maxRank = rankScores->size() - 1;
int topRank = 0; int topRank = 0;
while (rankScores->at(topRank) > topRankingPlayer.Score) while (topRank < maxRank && rankScores->at(topRank) > topRankingPlayer.Score)
topRank++; topRank++;
resp.iEPTopRank = topRank + 1; resp.iEPTopRank = topRank + 1;
@@ -137,7 +145,7 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
// this ranking // this ranking
int rank = 0; int rank = 0;
while (rankScores->at(rank) > postRanking.Score) while (rank < maxRank && rankScores->at(rank) > postRanking.Score)
rank++; rank++;
resp.iEPRank = rank + 1; resp.iEPRank = rank + 1;

View File

@@ -7,7 +7,11 @@
#include <set> #include <set>
struct EPInfo { struct EPInfo {
int zoneX, zoneY, EPID, maxScore, maxTime; // available through XDT (maxScore may be updated by drops)
int zoneX, zoneY, EPID, maxScore;
// available through drops
int maxTime, maxPods;
double scaleFactor, podFactor, timeFactor;
}; };
struct EPRace { struct EPRace {

View File

@@ -375,7 +375,7 @@ static void loadPaths(json& pathData, int32_t* nextId) {
Transport::NPCPaths.push_back(pathTemplate); Transport::NPCPaths.push_back(pathTemplate);
} }
std::cout << "[INFO] Loaded " << Transport::NPCPaths.size() << " NPC paths" << std::endl; std::cout << "[INFO] Loaded " << Transport::NPCPaths.size() << " NPC paths" << std::endl;
} }
catch (const std::exception& err) { catch (const std::exception& err) {
std::cerr << "[FATAL] Malformed paths.json file! Reason:" << err.what() << std::endl; std::cerr << "[FATAL] Malformed paths.json file! Reason:" << err.what() << std::endl;
@@ -584,8 +584,17 @@ static void loadDrops(json& dropData) {
continue; continue;
} }
// time limit isn't stored in the XDT, so we include it in the reward table instead EPInfo& epInfo = Racing::EPData[EPMap];
Racing::EPData[EPMap].maxTime = race["TimeLimit"];
// 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"];
// score cutoffs // score cutoffs
std::vector<int> rankScores; std::vector<int> rankScores;
@@ -601,7 +610,7 @@ static void loadDrops(json& dropData) {
if (rankScores.size() != 5 || rankScores.size() != rankRewards.size()) { if (rankScores.size() != 5 || rankScores.size() != rankRewards.size()) {
char buff[255]; char buff[255];
sprintf(buff, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID); snprintf(buff, 255, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID);
throw TableException(std::string(buff)); throw TableException(std::string(buff));
} }
@@ -686,7 +695,7 @@ static void loadEggs(json& eggData, int32_t* nextId) {
} }
} }
/* /*
* Load gruntwork output, if it exists * Load gruntwork output, if it exists
*/ */
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) { static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
@@ -1361,7 +1370,7 @@ void TableData::flush() {
targetIDs.push_back(tID); targetIDs.push_back(tID);
for (int32_t tType : path.targetTypes) for (int32_t tType : path.targetTypes)
targetTypes.push_back(tType); targetTypes.push_back(tType);
pathObj["iBaseSpeed"] = path.speed; pathObj["iBaseSpeed"] = path.speed;
pathObj["iTaskID"] = path.escortTaskID; pathObj["iTaskID"] = path.escortTaskID;
pathObj["bRelative"] = path.isRelative; pathObj["bRelative"] = path.isRelative;

View File

@@ -50,13 +50,15 @@ time_t getTime();
time_t getTimestamp(); time_t getTimestamp();
void terminate(int); void terminate(int);
// The PROTOCOL_VERSION definition is defined by the build system. // The PROTOCOL_VERSION definition can be defined by the build system.
#if !defined(PROTOCOL_VERSION) #if !defined(PROTOCOL_VERSION)
#define PROTOCOL_VERSION 104
#endif
#if PROTOCOL_VERSION == 104
#include "structs/0104.hpp" #include "structs/0104.hpp"
#elif PROTOCOL_VERSION == 728 #elif PROTOCOL_VERSION == 728
#include "structs/0728.hpp" #include "structs/0728.hpp"
#elif PROTOCOL_VERSION == 104
#include "structs/0104.hpp"
#elif PROTOCOL_VERSION == 1013 #elif PROTOCOL_VERSION == 1013
#include "structs/1013.hpp" #include "structs/1013.hpp"
#else #else

View File

@@ -46,49 +46,8 @@ enum class ePCRegenType {
End End
}; };
// nano powers // nano power flags
enum { 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_NONE = 0,
ECSB_UP_MOVE_SPEED = 1, ECSB_UP_MOVE_SPEED = 1,
ECSB_UP_SWIM_SPEED = 2, ECSB_UP_SWIM_SPEED = 2,

View File

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

View File

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

View File

@@ -64,7 +64,7 @@ void terminate(int arg) {
} }
#ifdef _WIN32 #ifdef _WIN32
static BOOL winTerminate(DWORD arg) { static BOOL WINAPI winTerminate(DWORD arg) {
terminate(0); terminate(0);
return FALSE; return FALSE;
} }

View File

@@ -8,6 +8,7 @@
#define REGISTER_SHARD_PACKET(pactype, handlr) CNShardServer::ShardPackets[pactype] = handlr; #define REGISTER_SHARD_PACKET(pactype, handlr) CNShardServer::ShardPackets[pactype] = handlr;
#define REGISTER_SHARD_TIMER(handlr, delta) CNShardServer::Timers.push_back(TimerEvent(handlr, delta)); #define REGISTER_SHARD_TIMER(handlr, delta) CNShardServer::Timers.push_back(TimerEvent(handlr, delta));
#define MS_PER_PLAYER_TICK 500 #define MS_PER_PLAYER_TICK 500
#define MS_PER_COMBAT_TICK 200
class CNShardServer : public CNServer { class CNShardServer : public CNServer {
private: private:

View File

@@ -67,6 +67,9 @@ int settings::MONITORINTERVAL = 5000;
// event mode settings // event mode settings
int settings::EVENTMODE = 0; int settings::EVENTMODE = 0;
// race settings
bool settings::IZRACESCORECAPPED = true;
void settings::init() { void settings::init() {
INIReader reader("config.ini"); INIReader reader("config.ini");
@@ -111,6 +114,7 @@ void settings::init() {
EVENTMODE = reader.GetInteger("shard", "eventmode", EVENTMODE); EVENTMODE = reader.GetInteger("shard", "eventmode", EVENTMODE);
DISABLEFIRSTUSEFLAG = reader.GetBoolean("shard", "disablefirstuseflag", DISABLEFIRSTUSEFLAG); DISABLEFIRSTUSEFLAG = reader.GetBoolean("shard", "disablefirstuseflag", DISABLEFIRSTUSEFLAG);
ANTICHEAT = reader.GetBoolean("shard", "anticheat", ANTICHEAT); ANTICHEAT = reader.GetBoolean("shard", "anticheat", ANTICHEAT);
IZRACESCORECAPPED = reader.GetBoolean("shard", "izracescorecapped", IZRACESCORECAPPED);
MONITORENABLED = reader.GetBoolean("monitor", "enabled", MONITORENABLED); MONITORENABLED = reader.GetBoolean("monitor", "enabled", MONITORENABLED);
MONITORPORT = reader.GetInteger("monitor", "port", MONITORPORT); MONITORPORT = reader.GetInteger("monitor", "port", MONITORPORT);
MONITORINTERVAL = reader.GetInteger("monitor", "interval", MONITORINTERVAL); MONITORINTERVAL = reader.GetInteger("monitor", "interval", MONITORINTERVAL);

View File

@@ -38,6 +38,7 @@ namespace settings {
extern int MONITORPORT; extern int MONITORPORT;
extern int MONITORINTERVAL; extern int MONITORINTERVAL;
extern bool DISABLEFIRSTUSEFLAG; extern bool DISABLEFIRSTUSEFLAG;
extern bool IZRACESCORECAPPED;
void init(); void init();
} }

2
tdata

Submodule tdata updated: cc65dbb402...8c98c83682

14838
vendor/JSON.hpp vendored

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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