61 Commits

Author SHA1 Message Date
Gent Semaj
0b325f6f9f Merge 9394825d41 into b12aecad63 2023-07-12 21:43:15 +00:00
gsemaj
9394825d41 Fix recall 2023-07-12 17:43:05 -04:00
gsemaj
6e0d55c4b7 Make SkillType an enum class 2023-07-12 17:37:05 -04:00
gsemaj
33396f8d28 Fix self recall 2023-07-11 20:21:37 -04:00
gsemaj
cd908666af [WIP] Fix targeting for groups 2023-07-11 17:42:08 -04:00
gsemaj
74588f2c77 [WIP] Active power handling
TODO:
- recall (self and group) is broken
- revive (only group) is broken
- damage + debuff is unimplemented
2023-07-11 13:52:59 -04:00
gsemaj
829f75112c Sync with master 2023-07-11 13:52:59 -04:00
gsemaj
343668fbcd Add overload to remove specific class of buff
I initially added this because, despite the higher tickrate for
composite condition calculations thanks to the last commit, there is
still a slight status icon delay when rapidly switching nanos. I
attempted to use this to make that problem go away and for whatever
reason it wasn't effective, but I figure it would be useful to have
anyway so I'm keeping it.
2023-07-11 13:52:59 -04:00
gsemaj
2e572169c0 Move some stuff from playerTick to player combat step 2023-07-11 13:52:59 -04:00
gsemaj
b3e28ddea3 Refactor group handling 2023-07-11 13:52:58 -04:00
gsemaj
1670dfd830 Port egg buffs over to new system 2023-07-11 13:52:58 -04:00
gsemaj
e768ebcabe More skill handlers
Note: need to revisit these when active powers are implemented to make
sure they are correct. DamageNDebuff isn't even implemented yet.
2023-07-11 13:52:58 -04:00
gsemaj
9bc7e8de62 Passive nano powers 2023-07-11 13:52:58 -04:00
gsemaj
f249599ab5 YET ANOTHER ITERATION of the new ability system
I am very tired
2023-07-11 13:52:57 -04:00
gsemaj
2901f5f285 Passive nano powers pt 1 2023-07-11 13:52:57 -04:00
gsemaj
36f329c302 Passive nano powers boilerplate 2023-07-11 13:52:57 -04:00
gsemaj
eb7daf8eaa Fix timed out buffs not calling onExpire 2023-07-11 13:52:57 -04:00
gsemaj
0c5a9400ce Buff framework tweaks + polish 2023-07-11 13:52:56 -04:00
gsemaj
f150595f70 Rework buff callbacks
The first implementation was way too complicated and prone to bugs.
This is much more simple flexible; first off, std::function is now used
instead of a raw function pointer, so lambdas and binds are fair game
which is great for scripting. Second, callbacks for all stacks are
executed. It is up to the callback target to ensure correct behavior.
2023-07-11 13:52:56 -04:00
gsemaj
c6528eb2ac oops 2023-07-11 13:52:56 -04:00
gsemaj
d631ca1aa1 CRLF purge in Buffs.cpp 2023-07-11 13:52:56 -04:00
gsemaj
a94fb0ed6d egg prep 2023-07-11 13:52:55 -04:00
gsemaj
d48aa21135 Move Buff implementation to Buffs.cpp 2023-07-11 13:52:55 -04:00
gsemaj
c60c4dac38 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-07-11 13:52:55 -04:00
gsemaj
215da0130d 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-07-11 13:52:54 -04:00
gsemaj
0e8a4742eb 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-07-11 13:52:54 -04:00
gsemaj
85bb4d163e Handle case where cmake is invoked outside root 2023-07-11 13:52:54 -04:00
gsemaj
09b74a5711 Start moving passive power processing to playerTick 2023-07-11 13:52:53 -04:00
gsemaj
90819bea8e Groundwork for new buff system 2023-07-11 13:52:53 -04:00
gsemaj
3cc5c09a91 Active power classification 2023-07-11 13:52:53 -04:00
gsemaj
536d5fbcfa some struct reorg 2023-07-11 13:52:53 -04:00
gsemaj
4ec3a3acb7 Replace group filter operator with function 2023-07-11 13:52:52 -04:00
gsemaj
89ed0b99a3 Ignore .bak files
for my local backups lol
2023-07-11 13:52:52 -04:00
gsemaj
db73b85bc8 Refactor player groups
Group structures are used now. Adds more checks in some places but simplifies things overall.
We can expand this system to entities as well now pretty trivially.
2023-07-11 13:52:52 -04:00
gsemaj
6cab203401 (WIP) EXPERIMENTAL GROUP CHANGES 2023-07-11 13:52:52 -04:00
gsemaj
92846e0eac (WIP) TODO ABILITIES 2023-07-11 13:52:51 -04:00
gsemaj
5963ea06be Move mob aggro logic into takeDamage override
God that feels good
2023-07-11 13:52:51 -04:00
gsemaj
c28970f2e1 (WIP) Move away from rigid states/transitions to allow custom behavior 2023-07-11 13:52:51 -04:00
gsemaj
8be853c2dc EntityType -> EntityKind 2023-07-11 13:52:51 -04:00
gsemaj
a811e73fed (WIP) onRoamStart hook implementation 2023-07-11 13:52:50 -04:00
gsemaj
a58971c270 (WIP) Remove BaseNPC::barkerType to save space 2023-07-11 13:52:50 -04:00
gsemaj
07429a0e51 ope 2023-07-11 13:52:50 -04:00
gsemaj
7c9038cf10 (WIP) onCombatStart hook implementation 2023-07-11 13:52:50 -04:00
gsemaj
c1e391d86a (WIP) onDeath hook implementation 2023-07-11 13:52:49 -04:00
gsemaj
e23af08838 (WIP) Add src param to transition + certain hooks
Should all hooks have src? I think not
2023-07-11 13:52:49 -04:00
gsemaj
8a26ae2f01 (WIP) Transitions + hook definitions + onRetreat hook implementation 2023-07-11 13:52:49 -04:00
gsemaj
d17694e12e (WIP) Point 2: Generalization 2023-07-11 13:52:48 -04:00
gsemaj
4b612f35d2 (WIP) Point 1: step functions 2023-07-11 13:52:48 -04:00
gsemaj
d9e0a4a281 (WIP) Start implementing ICombatant
Start by replacing `hitMob` with `takeDamage` interface function.
Simplify `pcAttackChars` a little by utilizing the new interface, then add more interface functions as needed.

A lot of the combat logic is tied to the `Mob` class. Need to start moving stuff over to CombatNPC.
2023-07-11 13:52:48 -04:00
gsemaj
dd9891f668 (WIP) Move ICombatant functions around a bit 2023-07-11 13:52:48 -04:00
gsemaj
962141e54f (WIP) Initial ICombatant draft 2023-07-11 13:52:47 -04:00
bea41132b4 [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-07-11 13:52:47 -04:00
f305d7252d [refactor] Replace a few uses of magic numbers with enums 2023-07-11 13:52:47 -04:00
ff62129eec [refactor] Cosmetic cleanup in Fuse fight functions 2023-07-11 13:52:47 -04:00
55f3ab8bad [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-07-11 13:52:46 -04:00
gsemaj
a732cde117 [WIP] Stub power handler 2023-07-11 13:52:46 -04:00
gsemaj
703eaff2b4 [WIP] Use EntityRef instead of CNSocket in ability handler 2023-07-11 13:52:46 -04:00
gsemaj
f4f5f2e0bd [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-07-11 13:52:45 -04:00
gsemaj
1858938280 [WIP] Rename Entity.type -> Entity.kind 2023-07-11 13:52:45 -04:00
gsemaj
0af8f7e91d [WIP] Fix Nanos -> Abilities namespace calls 2023-07-11 13:52:45 -04:00
gsemaj
168a85e8ff [WIP] Initial merge of ability namespaces & features 2023-07-11 13:52:45 -04:00
56 changed files with 8279 additions and 8284 deletions

View File

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

View File

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

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,34 +81,3 @@ That way you can always keep master in sync with upstream with `git pull --ff-on
Creating new branches for the rebase isn't strictly necessary since you can always return a branch to its previous state with `git reflog`. 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,5 +1,4 @@
# build FROM debian:latest
FROM debian:stable-slim as build
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -9,24 +8,14 @@ clang \
make \ make \
libsqlite3-dev libsqlite3-dev
COPY src ./src COPY . ./
COPY vendor ./vendor
COPY .git ./.git
COPY Makefile CMakeLists.txt version.h.in ./
RUN make -j8 RUN make -j8
# prod # tabledata should be copied from the host;
FROM debian:stable-slim # clone it there before building the container
#RUN git submodule update --init --recursive
WORKDIR /usr/src/app CMD ["./bin/fusion"]
RUN apt-get -y update && apt-get install -y \ LABEL Name=openfusion Version=0.0.1
libsqlite3-dev
COPY --from=build /usr/src/app/bin/fusion /bin/fusion
COPY sql ./sql
CMD ["/bin/fusion"]
LABEL Name=openfusion Version=0.0.2

View File

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

View File

@@ -17,10 +17,6 @@ acceptallcustomnames=true
# should attempts to log into non-existent accounts # should attempts to log into non-existent accounts
# automatically create them? # automatically create them?
autocreateaccounts=true autocreateaccounts=true
# list of supported authentication methods (comma-separated)
# password = allow login type 1 with plaintext passwords
# cookie = allow login type 2 with one-shot auth cookies
authmethods=password
# how often should everything be flushed to the database? # how often should everything be flushed to the database?
# the default is 4 minutes # the default is 4 minutes
dbsaveinterval=240 dbsaveinterval=240
@@ -70,9 +66,6 @@ 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

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

View File

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

View File

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

View File

@@ -1,12 +1,11 @@
#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;
@@ -48,107 +47,16 @@ 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) {
// take aggro // TODO abilities
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 = blocked; result.bProtected = false;
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();
@@ -163,24 +71,19 @@ 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 = 0; int boostDrain = (int)(skill->values[0][power] * scalingFactor);
int potionDrain = 0; if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
if(!blocked) { plr->batteryW -= boostDrain;
boostDrain = (int)(skill->values[0][power] * scalingFactor);
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
plr->batteryW -= boostDrain;
potionDrain = (int)(skill->values[1][power] * scalingFactor); int 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 = blocked; result.bProtected = target->hasBuff(ECSB_PROTECT_BATTERY);
result.iDrainW = boostDrain; result.iDrainW = boostDrain;
result.iBatteryW = plr->batteryW; result.iBatteryW = plr->batteryW;
result.iDrainN = potionDrain; result.iDrainN = potionDrain;
@@ -222,64 +125,54 @@ 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 SkillType::CORRUPTIONATTACK:
case SkillType::CORRUPTIONATTACKLOSE:
case SkillType::CORRUPTIONATTACKWIN:
// skillHandler = handleSkillCorruptionReflect;
// break;
case SkillType::DAMAGE: case SkillType::DAMAGE:
resultSize = sizeof(sSkillResult_Damage);
skillHandler = handleSkillDamage; skillHandler = handleSkillDamage;
break; break;
case SkillType::HEAL_HP: case SkillType::HEAL_HP:
case SkillType::RETURNHOMEHEAL: case SkillType::RETURNHOMEHEAL:
resultSize = sizeof(sSkillResult_Heal_HP);
skillHandler = handleSkillHealHP; skillHandler = handleSkillHealHP;
break; break;
case SkillType::KNOCKDOWN:
case SkillType::SLEEP:
case SkillType::SNARE:
case SkillType::STUN:
skillHandler = handleSkillDamageNDebuff;
break;
case SkillType::JUMP: case SkillType::JUMP:
case SkillType::RUN: case SkillType::RUN:
case SkillType::STEALTH: case SkillType::FREEDOM:
case SkillType::PHOENIX:
case SkillType::INVULNERABLE:
case SkillType::MINIMAPENEMY: case SkillType::MINIMAPENEMY:
case SkillType::MINIMAPTRESURE: case SkillType::MINIMAPTRESURE:
case SkillType::PHOENIX: case SkillType::NANOSTIMPAK:
case SkillType::PROTECTBATTERY: case SkillType::PROTECTBATTERY:
case SkillType::PROTECTINFECTION: case SkillType::PROTECTINFECTION:
case SkillType::REWARDBLOB: case SkillType::REWARDBLOB:
case SkillType::REWARDCASH: case SkillType::REWARDCASH:
// case SkillType::INFECTIONDAMAGE:
case SkillType::FREEDOM:
case SkillType::BOUNDINGBALL:
case SkillType::INVULNERABLE:
case SkillType::STAMINA_SELF: case SkillType::STAMINA_SELF:
case SkillType::NANOSTIMPAK: case SkillType::STEALTH:
case SkillType::BUFFHEAL: resultSize = sizeof(sSkillResult_Buff);
skillHandler = handleSkillBuff; skillHandler = handleSkillBuff;
break; break;
case SkillType::BLOODSUCKING: case SkillType::BATTERYDRAIN:
skillHandler = handleSkillLeech; resultSize = sizeof(sSkillResult_BatteryDrain);
skillHandler = handleSkillBatteryDrain;
break;
case SkillType::RECALL: // still soft lock
case SkillType::RECALL_GROUP: // works for player who uses it
resultSize = sizeof(sSkillResult_Move);
skillHandler = handleSkillMove;
break;
case SkillType::PHOENIX_GROUP: // broken
resultSize = sizeof(sSkillResult_Resurrect);
skillHandler = handleSkillResurrect;
break; break;
case SkillType::RETROROCKET_SELF: case SkillType::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 " << (int)skill->skillType << std::endl; std::cout << "[WARN] Unhandled skill type " << (int)skill->skillType << std::endl;
return results; return results;
@@ -289,7 +182,7 @@ 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 > MAX_SKILLRESULT_SIZE) { if(result.size != resultSize) {
std::cout << "[WARN] bad skill result size for " << (int)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;
} }
@@ -298,42 +191,36 @@ static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombat
return results; return results;
} }
static void attachSkillResults(std::vector<SkillResult> results, uint8_t* pivot) { static void attachSkillResults(std::vector<SkillResult> results, size_t resultSize, uint8_t* pivot) {
for(SkillResult& result : results) { for(SkillResult& result : results) {
size_t sz = result.size; memcpy(pivot, result.payload, resultSize);
memcpy(pivot, result.payload, sz); pivot += resultSize;
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;
if(skill->drainType == SkillDrainType::ACTIVE) { nano.iStamina -= skill->batteryUse[boost];
nano.iStamina -= skill->batteryUse[boost]; if (nano.iStamina < 0)
if (nano.iStamina <= 0) nano.iStamina = 0;
nano.iStamina = 0;
}
std::vector<SkillResult> results = handleSkill(skill, boost, combatant, affected); std::vector<SkillResult> results = handleSkill(skill, boost, plr, affected);
if(results.empty()) return; // no effect; no need for confirmation packets size_t resultSize = 0; // guaranteed to be the same for every item
if (!results.empty()) resultSize = results.back().size;
// lazy validation since skill results might be different sizes if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), results.size(), resultSize)) {
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); size_t resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) + results.size() * resultSize;
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);
@@ -346,7 +233,7 @@ void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std:
pkt->eST = (int32_t)skill->skillType; pkt->eST = (int32_t)skill->skillType;
pkt->iTargetCnt = (int32_t)results.size(); pkt->iTargetCnt = (int32_t)results.size();
attachSkillResults(results, (uint8_t*)(pkt + 1)); attachSkillResults(results, resultSize, (uint8_t*)(pkt + 1));
sock->sendPacket(pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen); sock->sendPacket(pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen);
if(skill->skillType == SkillType::RECALL_GROUP) if(skill->skillType == SkillType::RECALL_GROUP)
@@ -354,6 +241,9 @@ void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std:
PlayerManager::sendToGroup(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen); PlayerManager::sendToGroup(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen);
else else
PlayerManager::sendToViewable(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen); PlayerManager::sendToViewable(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen);
if (nano.iStamina <= 0)
Nanos::summonNano(sock, -1);
} }
void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*> affected) { void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*> affected) {
@@ -368,18 +258,15 @@ 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);
if(results.empty()) return; // no effect; no need for confirmation packets size_t resultSize = results.back().size; // guaranteed to be the same for every item
// lazy validation since skill results might be different sizes if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), resultSize)) {
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); size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + results.size() * resultSize;
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);
@@ -388,14 +275,8 @@ void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*>
pkt->iSkillID = skillID; pkt->iSkillID = skillID;
pkt->eST = (int32_t)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, (uint8_t*)(pkt + 1)); attachSkillResults(results, resultSize, (uint8_t*)(pkt + 1));
NPCManager::sendToViewable(entity, pkt, P_FE2CL_NPC_SKILL_HIT, resplen); NPCManager::sendToViewable(entity, pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
} }
@@ -412,8 +293,14 @@ 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) 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()); return entityRefsToCombatants(src->getGroupMembers());
}
// this check *has* to happen after the group check above due to cases like group recall that use both // 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)
@@ -470,6 +357,12 @@ int Abilities::getCSTBFromST(SkillType skillType) {
case SkillType::PROTECTINFECTION: case SkillType::PROTECTINFECTION:
result = ECSB_PROTECT_INFECTION; result = ECSB_PROTECT_INFECTION;
break; break;
case SkillType::SNARE:
result = ECSB_DN_MOVE_SPEED;
break;
case SkillType::SLEEP:
result = ECSB_MEZ;
break;
case SkillType::MINIMAPENEMY: case SkillType::MINIMAPENEMY:
result = ECSB_MINIMAP_ENEMY; result = ECSB_MINIMAP_ENEMY;
break; break;
@@ -482,9 +375,15 @@ int Abilities::getCSTBFromST(SkillType skillType) {
case SkillType::REWARDCASH: case SkillType::REWARDCASH:
result = ECSB_REWARD_CASH; result = ECSB_REWARD_CASH;
break; break;
case SkillType::INFECTIONDAMAGE:
result = ECSB_INFECTION;
break;
case SkillType::FREEDOM: case SkillType::FREEDOM:
result = ECSB_FREEDOM; result = ECSB_FREEDOM;
break; break;
case SkillType::BOUNDINGBALL:
result = ECSB_BOUNDINGBALL;
break;
case SkillType::INVULNERABLE: case SkillType::INVULNERABLE:
result = ECSB_INVULNERABLE; result = ECSB_INVULNERABLE;
break; break;
@@ -493,24 +392,6 @@ int Abilities::getCSTBFromST(SkillType skillType) {
break; break;
case SkillType::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,20 +7,18 @@
#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 { enum class SkillType {
DAMAGE = 1, DAMAGE = 1,
HEAL_HP = 2, HEAL_HP = 2,
KNOCKDOWN = 3, // uses DamageNDebuff KNOCKDOWN = 3,
SLEEP = 4, // uses DamageNDebuff SLEEP = 4,
SNARE = 5, // uses DamageNDebuff SNARE = 5,
HEAL_STAMINA = 6, HEAL_STAMINA = 6,
STAMINA_SELF = 7, STAMINA_SELF = 7,
STUN = 8, // uses DamageNDebuff STUN = 8,
WEAPONSLOW = 9, WEAPONSLOW = 9,
JUMP = 10, JUMP = 10,
RUN = 11, RUN = 11,
@@ -77,7 +75,6 @@ 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);
} }

View File

@@ -30,7 +30,7 @@ static bool playerHasBuddyWithID(Player* plr, int buddyID) {
#pragma endregion #pragma endregion
// Refresh buddy list // Refresh buddy list
void Buddies::sendBuddyList(CNSocket* sock) { void Buddies::refreshBuddyList(CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
int buddyCnt = Database::getNumBuddies(plr); int buddyCnt = Database::getNumBuddies(plr);
@@ -277,6 +277,15 @@ static void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) {
// Getting buddy state // Getting buddy state
static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) { static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
/*
* If the buddy list wasn't synced a second time yet, sync it.
* Not sure why we have to do it again for the client not to trip up.
*/
if (!plr->buddiesSynced) {
refreshBuddyList(sock);
plr->buddiesSynced = true;
}
INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);

View File

@@ -6,5 +6,5 @@ namespace Buddies {
void init(); void init();
// Buddy list // Buddy list
void sendBuddyList(CNSocket* sock); void refreshBuddyList(CNSocket* sock);
} }

View File

@@ -95,12 +95,6 @@ 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();
} }
@@ -143,56 +137,15 @@ 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
int32_t eCharType = combatant->getCharType(); pkt.eCT = 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,7 +66,6 @@ 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
@@ -87,7 +86,5 @@ 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,41 +72,31 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
// Handle serverside value-changes // Handle serverside value-changes
switch (setData->iSetValueType) { switch (setData->iSetValueType) {
case CN_GM_SET_VALUE_TYPE__HP: case 1:
response.iSetValue = plr->HP = setData->iSetValue; plr->HP = setData->iSetValue;
break; break;
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY : case 2:
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 CN_GM_SET_VALUE_TYPE__NANO_BATTERY: case 3:
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 CN_GM_SET_VALUE_TYPE__FUSION_MATTER: case 4:
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter); Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
response.iSetValue = plr->fusionmatter;
break; break;
case CN_GM_SET_VALUE_TYPE__CANDY: case 5:
response.iSetValue = plr->money = setData->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,14 +21,13 @@ 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) {
if(!isAlive()) EntityRef self = PlayerManager::getSockFromID(iID);
return false;
if(!hasBuff(buffId)) { if(!hasBuff(buffId)) {
buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack); buffs[buffId] = new Buff(buffId, self, 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;
@@ -49,10 +48,9 @@ void Player::removeBuff(int buffId) {
} }
} }
void Player::removeBuff(int buffId, BuffClass buffClass) { void Player::removeBuff(int buffId, int buffClass) {
if(hasBuff(buffId)) { if(hasBuff(buffId)) {
buffs[buffId]->clear(buffClass); buffs[buffId]->clear((BuffClass)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);
@@ -60,16 +58,6 @@ void Player::removeBuff(int buffId, BuffClass 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();
@@ -168,78 +156,29 @@ 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) { bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) { /* stubbed */
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) { Buff* CombatNPC::getBuff(int buffId) { /* stubbed */
if(hasBuff(buffId)) {
return buffs[buffId];
}
return nullptr; return nullptr;
} }
void CombatNPC::removeBuff(int buffId) { void CombatNPC::removeBuff(int buffId) { /* stubbed */ }
if(hasBuff(buffId)) {
buffs[buffId]->clear(); void CombatNPC::removeBuff(int buffId, int buffClass) { /* stubbed */ }
delete buffs[buffId];
buffs.erase(buffId); bool CombatNPC::hasBuff(int buffId) { /* stubbed */
} return false;
} }
void CombatNPC::removeBuff(int buffId, BuffClass buffClass) { int CombatNPC::getCompositeCondition() { /* stubbed */
if(hasBuff(buffId)) { return 0;
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) {
@@ -310,19 +249,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.triggerState == newState && event.npcType == type) if (event.trigger == ON_KILLED && event.npcType == type)
event.handler(this); event.handler(src, this);
*/
} }
#pragma endregion #pragma endregion
@@ -365,7 +304,7 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
return ret; return ret;
} }
static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTargets) { static bool checkRapidFire(CNSocket *sock, int targetCount) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
time_t currTime = getTime(); time_t currTime = getTime();
@@ -377,7 +316,7 @@ static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTarget
plr->lastShot = currTime; plr->lastShot = currTime;
// 3+ targets should never be possible // 3+ targets should never be possible
if (!allowManyTargets && targetCount > 3) if (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
@@ -396,7 +335,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, false)) if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt))
return; return;
/* /*
@@ -837,10 +776,6 @@ 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
@@ -939,12 +874,9 @@ 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)
ICombatant* src = dynamic_cast<ICombatant*>(plr); Nanos::applyNanoBuff(skill, plr);
int32_t targets[] = { plr->iID }; // ^ composite condition calculation is separate from combat for responsiveness
std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets);
Abilities::useNanoSkill(sock, skill, *nano, affectedCombatants);
}
} }
} }
@@ -964,7 +896,6 @@ 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

@@ -83,77 +83,7 @@ static void helpCommand(std::string full, std::vector<std::string>& args, CNSock
} }
static void accessCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void accessCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
if (args.size() < 2) { Chat::sendServerMessage(sock, "Your access level is " + std::to_string(PlayerManager::getPlayer(sock)->accountLevel));
Chat::sendServerMessage(sock, "Usage: /access <id> [new_level]");
Chat::sendServerMessage(sock, "Use . for id to select yourself");
return;
}
char *tmp;
Player* self = PlayerManager::getPlayer(sock);
int selfAccess = self->accountLevel;
Player* player;
if (args[1].compare(".") == 0) {
player = self;
} else {
int id = std::strtol(args[1].c_str(), &tmp, 10);
if (*tmp) {
Chat::sendServerMessage(sock, "Invalid player ID " + args[1]);
return;
}
player = PlayerManager::getPlayerFromID(id);
if (player == nullptr) {
Chat::sendServerMessage(sock, "Could not find player with ID " + std::to_string(id));
return;
}
// Messing with other players requires a baseline access of 30
if (player != self && selfAccess > 30) {
Chat::sendServerMessage(sock, "Can't check or change other players access levels (insufficient privileges)");
return;
}
}
std::string playerName = PlayerManager::getPlayerName(player);
int currentAccess = player->accountLevel;
if (args.size() < 3) {
// just check
Chat::sendServerMessage(sock, playerName + " has access level " + std::to_string(currentAccess));
return;
}
// Can't change the access level of someone with stronger privileges
// N.B. lower value = stronger privileges
if (currentAccess <= selfAccess) {
Chat::sendServerMessage(sock, "Can't change this player's access level (insufficient privileges)");
return;
}
int newAccess = std::strtol(args[2].c_str(), &tmp, 10);
if (*tmp) {
Chat::sendServerMessage(sock, "Invalid access level " + args[2]);
return;
}
// Can only assign an access level weaker than yours
if (newAccess <= selfAccess) {
Chat::sendServerMessage(sock, "Can only assign privileges weaker than your own");
return;
}
player->accountLevel = newAccess;
// Save to database
int accountId = Database::getAccountIdForPlayer(player->iID);
Database::updateAccountLevel(accountId, newAccess);
std::string msg = "Changed access level for " + playerName + " from " + std::to_string(currentAccess) + " to " + std::to_string(newAccess);
if (newAccess <= 50 && currentAccess > 50)
msg += " (they must log out and back in for some commands to be enabled)";
Chat::sendServerMessage(sock, msg);
} }
static void populationCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void populationCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -429,19 +359,23 @@ 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);
bool isGruntworkNpc = true; // if it's a gruntwork NPC, rotate in-place
if (TableData::RunningMobs.find(npc->id) != TableData::RunningMobs.end()) {
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle);
// add a rotation entry to the gruntwork file, unless it's already a gruntwork NPC Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for gruntwork NPC "
if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end()) { + std::to_string(npc->id));
} 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));
} }
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + // update rotation clientside
" for " + (isGruntworkNpc ? "gruntwork " : "") + "NPC " + std::to_string(npc->id)); INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
pkt.NPCAppearanceData = npc->getAppearanceData();
// update rotation clientside by refreshing the player's chunks (same as the /refresh command) sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
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) {
@@ -744,6 +678,7 @@ 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));
@@ -1270,7 +1205,7 @@ static void registerCommand(std::string cmd, int requiredLevel, CommandHandler h
void CustomCommands::init() { void CustomCommands::init() {
registerCommand("help", 100, helpCommand, "list all unlocked server-side commands"); registerCommand("help", 100, helpCommand, "list all unlocked server-side commands");
registerCommand("access", 100, accessCommand, "check or change access levels"); registerCommand("access", 100, accessCommand, "print your access level");
registerCommand("instance", 30, instanceCommand, "print or change your current instance"); registerCommand("instance", 30, instanceCommand, "print or change your current instance");
registerCommand("mss", 30, mssCommand, "edit Monkey Skyway routes"); registerCommand("mss", 30, mssCommand, "edit Monkey Skyway routes");
registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs"); registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs");

View File

@@ -26,7 +26,6 @@ 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
@@ -51,57 +50,12 @@ 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;
}
} }
// initialize response struct // use skill
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + result.size; std::vector<ICombatant*> targets;
uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; targets.push_back(dynamic_cast<ICombatant*>(plr));
memset(respbuf, 0, resplen); Abilities::useNPCSkill(src, skillId, targets);
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

@@ -325,13 +325,6 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
std::string logEmail = "[Email] " + PlayerManager::getPlayerName(plr, true) + " (to " + PlayerManager::getPlayerName(&otherPlr, true) + "): <" + email.SubjectLine + ">\n" + email.MsgBody; std::string logEmail = "[Email] " + PlayerManager::getPlayerName(plr, true) + " (to " + PlayerManager::getPlayerName(&otherPlr, true) + "): <" + email.SubjectLine + ">\n" + email.MsgBody;
std::cout << logEmail << std::endl; std::cout << logEmail << std::endl;
dump.push_back(logEmail); dump.push_back(logEmail);
// notification to recipient if online
CNSocket* recipient = PlayerManager::getSockFromID(pkt->iTo_PCUID);
if (recipient != nullptr)
{
emailUpdateCheck(recipient, nullptr);
}
} }
void Email::init() { void Email::init() {

View File

@@ -16,9 +16,8 @@ EntityRef::EntityRef(CNSocket *s) {
EntityRef::EntityRef(int32_t i) { EntityRef::EntityRef(int32_t i) {
id = i; id = i;
kind = EntityKind::INVALID; assert(NPCManager::NPCs.find(id) != NPCManager::NPCs.end());
if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end()) kind = NPCManager::NPCs[id]->kind;
kind = NPCManager::NPCs[id]->kind;
} }
bool EntityRef::isValid() const { bool EntityRef::isValid() const {
@@ -41,7 +40,7 @@ sNPCAppearanceData BaseNPC::getAppearanceData() {
sNPCAppearanceData data = {}; sNPCAppearanceData data = {};
data.iAngle = angle; data.iAngle = angle;
data.iBarkerType = 0; // unused? data.iBarkerType = 0; // unused?
data.iConditionBitFlag = 0; data.iConditionBitFlag = cbf;
data.iHP = hp; data.iHP = hp;
data.iNPCType = type; data.iNPCType = type;
data.iNPC_ID = id; data.iNPC_ID = id;
@@ -51,12 +50,6 @@ 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,8 +47,7 @@ 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, BuffClass) = 0; virtual void removeBuff(int, int) = 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;
@@ -73,6 +72,7 @@ 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,6 +80,7 @@ public:
type = t; type = t;
hp = 400; hp = 400;
angle = _A; angle = _A;
cbf = 0;
id = _id; id = _id;
instanceID = iID; instanceID = iID;
}; };
@@ -87,7 +88,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;
virtual sNPCAppearanceData getAppearanceData(); sNPCAppearanceData getAppearanceData();
}; };
struct CombatNPC : public BaseNPC, public ICombatant { struct CombatNPC : public BaseNPC, public ICombatant {
@@ -104,13 +105,11 @@ 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;
std::unordered_map<int, Buff*> buffs = {}; CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP)
CombatNPC(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, int id, int maxHP)
: BaseNPC(angle, iID, t, id), maxHealth(maxHP) { : BaseNPC(angle, iID, t, id), maxHealth(maxHP) {
this->spawnX = spawnX; spawnX = x;
this->spawnY = spawnY; spawnY = y;
this->spawnZ = spawnZ; spawnZ = z;
kind = EntityKind::COMBAT_NPC; kind = EntityKind::COMBAT_NPC;
@@ -118,15 +117,12 @@ 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, BuffClass buffClass) override; virtual void removeBuff(int buffId, int 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,9 +64,6 @@ 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;
@@ -112,9 +109,6 @@ 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
@@ -174,9 +168,6 @@ 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) {
@@ -261,9 +252,6 @@ 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);
@@ -271,9 +259,6 @@ 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);
@@ -309,9 +294,6 @@ 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,7 +3,6 @@
#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;
@@ -15,10 +14,6 @@ struct Group {
}); });
return filtered; return filtered;
} }
EntityRef getLeader() {
assert(members.size() > 0);
return members[0];
}
Group(EntityRef leader); Group(EntityRef leader);
}; };

View File

@@ -416,9 +416,6 @@ 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;

View File

@@ -386,9 +386,6 @@ 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 (hasBuff(ECSB_MEZ)) { if (cbf & CSB_BIT_MEZ) {
removeBuff(ECSB_MEZ); cbf &= ~CSB_BIT_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 = getCompositeCondition(); pkt1.iConditionBitFlag = cbf;
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,12 +96,13 @@ 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->clearBuffs(false); mob->cbf = 0;
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->getCompositeCondition(); pkt1.iConditionBitFlag = mob->cbf;
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));
} }
@@ -227,7 +228,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 mobStyle) { static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int style) {
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);
@@ -246,7 +247,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 = mobStyle; resp->iStyle = style;
resp->iValue1 = plr->x; resp->iValue1 = plr->x;
resp->iValue2 = plr->y; resp->iValue2 = plr->y;
resp->iValue3 = plr->z; resp->iValue3 = plr->z;
@@ -280,38 +281,26 @@ 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 nanoStyle = Nanos::nanoStyle(plr->activeNano); int style2 = Nanos::nanoStyle(plr->activeNano);
if (nanoStyle == -1) { // no nano if (style2 == -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 (mobStyle == nanoStyle) { } else if (style == style2) {
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 (mobStyle - nanoStyle == 1 || nanoStyle - mobStyle == 2) { } else if (style - style2 == 1 || style2 - style == 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
SkillData skill = { // TODO ABILITIES
SkillType::DAMAGE, // skillType /*std::vector<int> targetData2 = {1, mob->id, 0, 0, 0};
SkillEffectTarget::POINT, // effectTarget for (auto& pwr : Abilities::Powers)
1, // effectType if (pwr.skillType == EST_DAMAGE)
SkillTargetType::MOBS, // targetType pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200);*/
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;
@@ -338,6 +327,12 @@ 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
@@ -352,7 +347,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<ICombatant*> targets{}; std::vector<int> targetData = {0, 0, 0, 0, 0};
// 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++) {
@@ -362,22 +357,26 @@ 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->isAlive()) if (plr->HP <= 0)
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) {
targets.push_back(plr); targetData[0] += 1;
if (targets.size() > 3) // make sure not to have more than 4 targetData[targetData[0]] = plr->iID;
if (targetData[0] > 3) // make sure not to have more than 4
break; break;
} }
} }
} }
Abilities::useNPCSkill(mob->id, skillID, targets); // TODO ABILITIES
/*for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);*/
mob->skillStyle = -3; // eruption cooldown mob->skillStyle = -3; // eruption cooldown
mob->nextAttack = currTime + 1000; mob->nextAttack = currTime + 1000;
return; return;
@@ -395,11 +394,14 @@ 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"];
SkillData* skill = &Abilities::SkillTable[skillID]; // TODO ABILITIES
int debuffID = Abilities::getCSTBFromST(skill->skillType); //std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
if(plr->hasBuff(debuffID)) //for (auto& pwr : Abilities::Powers)
return; // prevent debuffing a player twice // if (pwr.skillType == Abilities::SkillTable[skillID].skillType) {
Abilities::useNPCSkill(mob->getRef(), skillID, { plr }); // if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag))
// return; // prevent debuffing a player twice
// pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);
// }
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100; mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
return; return;
} }
@@ -440,6 +442,31 @@ 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();
@@ -526,27 +553,38 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
return; return;
} }
// tick buffs // drain
auto it = npc->buffs.begin(); if (self->skillStyle < 0 && (self->lastDrainTime == 0 || currTime - self->lastDrainTime >= 1000)
while(it != npc->buffs.end()) { && self->cbf & CSB_BIT_BOUNDINGBALL) {
Buff* buff = (*it).second; drainMobHP(self, self->maxHealth / 20); // lose 5% every second
buff->combatTick(currTime); self->lastDrainTime = currTime;
}
// if mob state changed, end the step // if drain killed the mob, return early
if(self->state != AIState::COMBAT) if (self->hp <= 0)
return; return;
buff->tick(currTime); // unbuffing
if(buff->isStale()) { std::unordered_map<int32_t, time_t>::iterator it = self->unbuffTimes.begin();
// garbage collect while (it != self->unbuffTimes.end()) {
it = npc->buffs.erase(it);
delete buff; if (currTime >= it->second) {
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->hasBuff(ECSB_STUN) || self->hasBuff(ECSB_MEZ)) { if (self->cbf & (CSB_BIT_STUN|CSB_BIT_MEZ)) {
self->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up. self->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
return; return;
} }
@@ -562,7 +600,6 @@ 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)
@@ -572,8 +609,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->hasBuff(ECSB_DN_MOVE_SPEED)) if (self->cbf & CSB_BIT_DN_MOVE_SPEED)
speed /= 2; self->speed /= 2;
int targetX = plr->x; int targetX = plr->x;
int targetY = plr->y; int targetY = plr->y;
@@ -582,9 +619,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, speed*2/5); distanceToTravel = std::min(distance-mobRange+1, self->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 < speed*2/5 && currTime >= self->nextAttack) if (distanceToTravel < self->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);
@@ -592,7 +629,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 = speed; pkt.iSpeed = self->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;
@@ -678,7 +715,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->hasBuff(ECSB_DN_MOVE_SPEED)) if (self->cbf & CSB_BIT_DN_MOVE_SPEED)
self->speed /= 2; self->speed /= 2;
std::queue<Vec3> queue; std::queue<Vec3> queue;
@@ -751,10 +788,14 @@ 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)
Abilities::useNPCSkill(npc->getRef(), 110, { npc }); // TODO ABILITIES
/*std::vector<int> targetData = { 1, 0, 0, 0, 0 };
for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[110].skillType)
pwr.handle(self->id, targetData, 110, Abilities::SkillTable[110].durationTime[0], Abilities::SkillTable[110].powerIntensity[0]);*/
// clear outlying debuffs // clear outlying debuffs
clearDebuff(self); clearDebuff(self);
} }
@@ -771,9 +812,12 @@ 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"]; int skillID = (int)self->data["m_iPassiveBuff"]; // cast passive
if(skillID != 0) // cast passive // TODO ABILITIES
Abilities::useNPCSkill(npc->getRef(), skillID, { npc }); /*std::vector<int> targetData = { 1, self->id, 0, 0, 0 };
for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
pwr.handle(self->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);*/
} }
void MobAI::onRetreat(CombatNPC* npc, EntityRef src) { void MobAI::onRetreat(CombatNPC* npc, EntityRef src) {
@@ -789,8 +833,9 @@ 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->clearBuffs(true); self->unbuffTimes.clear();
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,6 +21,8 @@ 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;
@@ -50,8 +52,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 spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id) Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(spawnX, spawnY, spawnZ, angle, iID, t, id, d["m_iHP"]), : CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]),
sightRange(d["m_iSightRange"]) { sightRange(d["m_iSightRange"]) {
state = AIState::ROAMING; state = AIState::ROAMING;
@@ -62,13 +64,15 @@ struct Mob : public CombatNPC {
idleRange = (int)data["m_iIdleRange"]; idleRange = (int)data["m_iIdleRange"];
level = data["m_iNpcLevel"]; level = data["m_iNpcLevel"];
roamX = spawnX; roamX = x;
roamY = spawnY; roamY = y;
roamZ = spawnZ; roamZ = z;
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,49 +94,20 @@ 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;
int taskID = req->iMissionTaskID; // get bark IDs from task data
// ignore req->iNPC_ID as it is often fixated on a single npc in the region TaskData* td = Missions::Tasks[req->iMissionTaskID];
std::vector<int> barks;
if (Missions::Tasks.find(taskID) == Missions::Tasks.end()) { for (int i = 0; i < 4; i++) {
std::cout << "mission task not found: " << taskID << std::endl; if (td->task["m_iHBarkerTextID"][i] != 0) // non-zeroes only
return; barks.push_back(td->task["m_iHBarkerTextID"][i]);
} }
TaskData* td = Missions::Tasks[taskID]; if (barks.empty())
auto& barks = td->task["m_iHBarkerTextID"]; return; // no barks
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 = npcID; resp.iNPC_ID = req->iNPC_ID;
resp.iMissionStringID = missionStringID; resp.iMissionStringID = barks[Rand::rand(barks.size())];
sock->sendPacket(resp, P_FE2CL_REP_BARKER); sock->sendPacket(resp, P_FE2CL_REP_BARKER);
} }
@@ -151,15 +122,16 @@ 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 spawnX, int spawnY, int spawnZ, uint64_t instance, int type, bool respawn, bool baseInstance) { BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) {
uint64_t inst = baseInstance ? MAPNUM(instance) : instance; 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(spawnX, spawnY, spawnZ, inst, type, NPCData[type], id); npc = new Mob(x, y, z, inst, type, NPCData[type], id);
// re-enable respawning, if desired // re-enable respawning, if desired
((Mob*)npc)->summoned = !respawn; ((Mob*)npc)->summoned = !respawn;
@@ -322,55 +294,57 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
return npc; return npc;
} }
// TODO: Move this to separate file in ai/ subdir when implementing more events // TODO: Move this to MobAI, possibly
#pragma region NPCEvents #pragma region NPCEvents
// summon right arm and stage 2 body // summon right arm and stage 2 body
static void lordFuseStageTwo(CombatNPC *npc) { static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc; // adaptium, stun 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, oldbody->instanceID, 2467); Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2467);
newbody->angle = oldbody->angle; newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ, NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
oldbody->instanceID, oldbody->angle); plr->instanceID, oldbody->angle);
// right arm, Adaptium, Stun // right arm, Adaptium, Stun
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, oldbody->instanceID, 2469); Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, plr->instanceID, 2469);
arm->angle = oldbody->angle; arm->angle = oldbody->angle;
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ, NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
oldbody->instanceID, oldbody->angle); plr->instanceID, oldbody->angle);
} }
// summon left arm and stage 3 body // summon left arm and stage 3 body
static void lordFuseStageThree(CombatNPC *npc) { static void lordFuseStageThree(CNSocket *sock, BaseNPC *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, oldbody->instanceID, 2468); Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2468);
newbody->angle = oldbody->angle; newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ, NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
newbody->instanceID, oldbody->angle); plr->instanceID, oldbody->angle);
// Blastons, Heal // Blastons, Heal
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, oldbody->instanceID, 2470); Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, plr->instanceID, 2470);
arm->angle = oldbody->angle; arm->angle = oldbody->angle;
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ, NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
arm->instanceID, oldbody->angle); plr->instanceID, oldbody->angle);
} }
std::vector<NPCEvent> NPCManager::NPCEvents = { std::vector<NPCEvent> NPCManager::NPCEvents = {
NPCEvent(2466, AIState::DEAD, lordFuseStageTwo), NPCEvent(2466, ON_KILLED, lordFuseStageTwo),
NPCEvent(2467, AIState::DEAD, lordFuseStageThree), NPCEvent(2467, ON_KILLED, lordFuseStageThree),
}; };
#pragma endregion NPCEvents #pragma endregion NPCEvents
@@ -402,5 +376,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, MS_PER_COMBAT_TICK); REGISTER_SHARD_TIMER(step, 200);
} }

View File

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

View File

@@ -69,6 +69,43 @@ 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;
@@ -93,10 +130,8 @@ 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;
ICombatant* src = dynamic_cast<ICombatant*>(plr); std::vector<ICombatant*> affectedCombatants = applyNanoBuff(skill, plr);
int32_t targets[] = { plr->iID }; if(!affectedCombatants.empty()) Abilities::useNanoSkill(sock, skill, nano, affectedCombatants);
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
@@ -110,7 +145,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 == nullptr || skill->iNanoID >= NANO_COUNT || skill->iNanoID < 0) if (skill->iNanoID >= NANO_COUNT)
return; return;
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
@@ -277,7 +312,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,4 +26,5 @@ 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

@@ -72,8 +72,8 @@ struct Player : public Entity, public ICombatant {
bool notify = false; bool notify = false;
bool hidden = false; bool hidden = false;
bool unwarpable = false; bool unwarpable = false;
bool initialLoadDone = false;
bool buddiesSynced = false;
int64_t buddyIDs[50] = {}; int64_t buddyIDs[50] = {};
bool isBuddyBlocked[50] = {}; bool isBuddyBlocked[50] = {};
@@ -92,8 +92,7 @@ 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, BuffClass buffClass) override; virtual void removeBuff(int buffId, int 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,10 +77,7 @@ 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;
if (plr->instanceID != I) { 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);
@@ -126,6 +123,24 @@ void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC); 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;
@@ -155,21 +170,16 @@ void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z) {
* Nanos the player hasn't unlocked will (and should) be greyed out. Thus, all nanos should be accounted * Nanos the player hasn't unlocked will (and should) be greyed out. Thus, all nanos should be accounted
* for in these packets, even if the player hasn't unlocked them. * for in these packets, even if the player hasn't unlocked them.
*/ */
static void sendNanoBook(CNSocket *sock, Player *plr, bool resizeOnly) { static void sendNanoBookSubset(CNSocket *sock) {
#ifdef ACADEMY #ifdef ACADEMY
Player *plr = getPlayer(sock);
int16_t id = 0; int16_t id = 0;
INITSTRUCT(sP_FE2CL_REP_NANO_BOOK_SUBSET, pkt); INITSTRUCT(sP_FE2CL_REP_NANO_BOOK_SUBSET, pkt);
pkt.PCUID = plr->iID; pkt.PCUID = plr->iID;
pkt.bookSize = NANO_COUNT; pkt.bookSize = NANO_COUNT;
if (resizeOnly) {
// triggers nano array resizing without
// actually sending nanos
sock->sendPacket(pkt, P_FE2CL_REP_NANO_BOOK_SUBSET);
return;
}
while (id < NANO_COUNT) { while (id < NANO_COUNT) {
pkt.elementOffset = id; pkt.elementOffset = id;
@@ -217,7 +227,6 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
response.iID = plr->iID; response.iID = plr->iID;
response.uiSvrTime = getTime(); response.uiSvrTime = getTime();
response.PCLoadData2CL.iUserLevel = plr->accountLevel; response.PCLoadData2CL.iUserLevel = plr->accountLevel;
response.PCLoadData2CL.iHP = plr->HP; response.PCLoadData2CL.iHP = plr->HP;
response.PCLoadData2CL.iLevel = plr->level; response.PCLoadData2CL.iLevel = plr->level;
@@ -300,21 +309,27 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
sock->setFEKey(lm->FEKey); sock->setFEKey(lm->FEKey);
sock->setActiveKey(SOCKETKEY_FE); // send all packets using the FE key from now on sock->setActiveKey(SOCKETKEY_FE); // send all packets using the FE key from now on
// Academy builds receive nanos in a separate packet. An initial one with the size of the
// nano book needs to be sent before PC_ENTER_SUCC so the client can resize its nano arrays,
// and then proper packets with the nanos included must be sent after, while the game is loading.
sendNanoBook(sock, plr, true);
sock->sendPacket(response, P_FE2CL_REP_PC_ENTER_SUCC); sock->sendPacket(response, P_FE2CL_REP_PC_ENTER_SUCC);
sendNanoBook(sock, plr, false); // transmit MOTD after entering the game, so the client hopefully changes modes on time
Chat::sendServerMessage(sock, settings::MOTDSTRING);
// transfer ownership of Player object into the shard (still valid in this function though) // transfer ownership of Player object into the shard (still valid in this function though)
addPlayer(sock, plr); addPlayer(sock, plr);
// check if there is an expiring vehicle
Items::checkItemExpire(sock, plr);
// set player equip stats // set player equip stats
Items::setItemStats(plr); Items::setItemStats(plr);
Missions::failInstancedMissions(sock);
sendNanoBookSubset(sock);
// initial buddy sync
Buddies::refreshBuddyList(sock);
for (auto& pair : players) for (auto& pair : players)
if (pair.second->notify) if (pair.second->notify)
Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(plr) + " has joined."); Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(plr) + " has joined.");
@@ -359,35 +374,6 @@ 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);
}
if (!plr->initialLoadDone) {
// these should be called only once, but not until after
// first load-in or else the client may ignore the packets
Chat::sendServerMessage(sock, settings::MOTDSTRING); // MOTD
Missions::failInstancedMissions(sock); // auto-fail missions
Buddies::sendBuddyList(sock); // buddy list
Items::checkItemExpire(sock, plr); // vehicle expiration
plr->initialLoadDone = true;
}
} }
static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) { static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
@@ -430,7 +416,6 @@ 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;
@@ -591,7 +576,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

@@ -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,43 +99,35 @@ 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
EPInfo& epInfo = EPData[mapNum];
EPRace& epRace = EPRaces[sock];
uint64_t now = getTime() / 1000; uint64_t now = getTime() / 1000;
int timeDiff = now - epRace.startTime;
int podsCollected = epRace.collectedRings.size();
int score = std::exp( int timeDiff = now - EPRaces[sock].startTime;
(epInfo.podFactor * podsCollected) / epInfo.maxPods int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff;
- (epInfo.timeFactor * timeDiff) / epInfo.maxTime if (score < 0) score = 0; // lol
+ epInfo.scaleFactor); int fm = score * plr->level * (1.0f / 36) * 0.3f;
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 = epInfo.EPID; postRanking.EPID = EPData[mapNum].EPID;
postRanking.PlayerID = plr->iID; postRanking.PlayerID = plr->iID;
postRanking.RingCount = podsCollected; postRanking.RingCount = EPRaces[sock].collectedRings.size();
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(epInfo.EPID, plr->iID); Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(EPData[mapNum].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[epInfo.EPID].first; std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first;
std::vector<int>* rankRewards = &EPRewards[epInfo.EPID].second; std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second;
// top ranking // top ranking
int maxRank = rankScores->size() - 1;
int topRank = 0; int topRank = 0;
while (topRank < maxRank && rankScores->at(topRank) > topRankingPlayer.Score) while (rankScores->at(topRank) > topRankingPlayer.Score)
topRank++; topRank++;
resp.iEPTopRank = topRank + 1; resp.iEPTopRank = topRank + 1;
@@ -145,7 +137,7 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
// this ranking // this ranking
int rank = 0; int rank = 0;
while (rank < maxRank && rankScores->at(rank) > postRanking.Score) while (rankScores->at(rank) > postRanking.Score)
rank++; rank++;
resp.iEPRank = rank + 1; resp.iEPRank = rank + 1;

View File

@@ -7,11 +7,7 @@
#include <set> #include <set>
struct EPInfo { struct EPInfo {
// available through XDT (maxScore may be updated by drops) int zoneX, zoneY, EPID, maxScore, maxTime;
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,17 +584,8 @@ static void loadDrops(json& dropData) {
continue; continue;
} }
EPInfo& epInfo = Racing::EPData[EPMap]; // time limit isn't stored in the XDT, so we include it in the reward table instead
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;
@@ -610,7 +601,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];
snprintf(buff, 255, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID); sprintf(buff, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID);
throw TableException(std::string(buff)); throw TableException(std::string(buff));
} }
@@ -695,7 +686,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) {
@@ -1370,7 +1361,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

@@ -186,36 +186,6 @@ static void tradeOfferRefusal(CNSocket* sock, CNPacketData* data) {
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL)); otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
} }
static void tradeOfferCancel(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL));
}
static void tradeOfferAbort(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT));
}
static void tradeConfirm(CNSocket* sock, CNPacketData* data) { static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf; sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf;
@@ -460,8 +430,6 @@ void Trading::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, tradeOffer); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, tradeOffer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT, tradeOfferAccept); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT, tradeOfferAccept);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, tradeOfferRefusal); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, tradeOfferRefusal);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL, tradeOfferCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ABORT, tradeOfferAbort);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM, tradeConfirm); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM, tradeConfirm);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem);

View File

@@ -40,7 +40,6 @@
// wrapper for U16toU8 // wrapper for U16toU8
#define ARRLEN(x) (sizeof(x)/sizeof(*x)) #define ARRLEN(x) (sizeof(x)/sizeof(*x))
#define AUTOU8(x) std::string((char*)x, ARRLEN(x))
#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x)) #define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt // TODO: rewrite U16toU8 & U8toU16 to not use codecvt
@@ -51,15 +50,13 @@ time_t getTime();
time_t getTimestamp(); time_t getTimestamp();
void terminate(int); void terminate(int);
// The PROTOCOL_VERSION definition can be defined by the build system. // The PROTOCOL_VERSION definition is defined by the build system.
#if !defined(PROTOCOL_VERSION) #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

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

View File

@@ -226,11 +226,10 @@ 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
// you aren't allowed to bind the table name const char* sql = "SELECT COUNT(*) FROM ?";
const char* sql = "SELECT COUNT(*) FROM ";
tableName.insert(0, sql);
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, tableName.c_str(), -1, &stmt, NULL); sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, tableName.c_str(), -1, NULL);
sqlite3_step(stmt); sqlite3_step(stmt);
int result = sqlite3_column_int(stmt, 0); int result = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
@@ -267,17 +266,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"; message += " and " + std::to_string(players) + " Player Character";
if (players > 1) if (players > 1)
message += "s"; message += "s";
} }

View File

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

View File

@@ -208,8 +208,6 @@ void Database::addBlock(int playerId, int blockedPlayerId) {
} }
void Database::removeBlock(int playerId, int blockedPlayerId) { 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 WINAPI winTerminate(DWORD arg) { static BOOL winTerminate(DWORD arg) {
terminate(0); terminate(0);
return FALSE; return FALSE;
} }

View File

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

View File

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

View File

@@ -8,7 +8,6 @@
#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

@@ -13,7 +13,6 @@ bool settings::SANDBOX = true;
int settings::LOGINPORT = 23000; int settings::LOGINPORT = 23000;
bool settings::APPROVEALLNAMES = true; bool settings::APPROVEALLNAMES = true;
bool settings::AUTOCREATEACCOUNTS = true; bool settings::AUTOCREATEACCOUNTS = true;
std::string settings::AUTHMETHODS = "password";
int settings::DBSAVEINTERVAL = 240; int settings::DBSAVEINTERVAL = 240;
int settings::SHARDPORT = 23001; int settings::SHARDPORT = 23001;
@@ -68,9 +67,6 @@ 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");
@@ -88,7 +84,6 @@ void settings::init() {
LOGINPORT = reader.GetInteger("login", "port", LOGINPORT); LOGINPORT = reader.GetInteger("login", "port", LOGINPORT);
APPROVEALLNAMES = reader.GetBoolean("login", "acceptallcustomnames", APPROVEALLNAMES); APPROVEALLNAMES = reader.GetBoolean("login", "acceptallcustomnames", APPROVEALLNAMES);
AUTOCREATEACCOUNTS = reader.GetBoolean("login", "autocreateaccounts", AUTOCREATEACCOUNTS); AUTOCREATEACCOUNTS = reader.GetBoolean("login", "autocreateaccounts", AUTOCREATEACCOUNTS);
AUTHMETHODS = reader.Get("login", "authmethods", AUTHMETHODS);
DBSAVEINTERVAL = reader.GetInteger("login", "dbsaveinterval", DBSAVEINTERVAL); DBSAVEINTERVAL = reader.GetInteger("login", "dbsaveinterval", DBSAVEINTERVAL);
SHARDPORT = reader.GetInteger("shard", "port", SHARDPORT); SHARDPORT = reader.GetInteger("shard", "port", SHARDPORT);
SHARDSERVERIP = reader.Get("shard", "ip", SHARDSERVERIP); SHARDSERVERIP = reader.Get("shard", "ip", SHARDSERVERIP);
@@ -116,7 +111,6 @@ 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

@@ -8,7 +8,6 @@ namespace settings {
extern int LOGINPORT; extern int LOGINPORT;
extern bool APPROVEALLNAMES; extern bool APPROVEALLNAMES;
extern bool AUTOCREATEACCOUNTS; extern bool AUTOCREATEACCOUNTS;
extern std::string AUTHMETHODS;
extern int DBSAVEINTERVAL; extern int DBSAVEINTERVAL;
extern int SHARDPORT; extern int SHARDPORT;
extern std::string SHARDSERVERIP; extern std::string SHARDSERVERIP;
@@ -39,7 +38,6 @@ 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: bdb611b092...cc65dbb402

14842
vendor/JSON.hpp vendored

File diff suppressed because it is too large Load Diff

View File

@@ -22,14 +22,9 @@
#endif #endif
#include <errno.h> #include <errno.h>
#if defined(_WIN32) && !defined(_WIN64) #ifdef _WIN32 || _WIN64
typedef __int32 ssize_t;
#elif defined(_WIN32) && defined(_WIN64)
typedef __int64 ssize_t;
#endif
#if defined(_WIN32) || defined(_WIN64)
// On windows we need to generate random bytes differently. // On windows we need to generate random bytes differently.
typedef __int64 ssize_t;
#define BCRYPT_HASHSIZE 60 #define BCRYPT_HASHSIZE 60
#include "bcrypt.h" #include "bcrypt.h"
@@ -122,7 +117,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.
#if defined(_WIN32) || defined(_WIN64) #ifdef _WIN32 || _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 */
#if defined(_WIN32) || defined(_WIN64) #ifdef _WIN32 || _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
#if defined(_WIN32) || defined(_WIN64) #ifdef _WIN32 | _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) {
#if defined(_WIN32) || defined(_WIN64) #ifdef _WIN32 | _WIN64
retval = _strdup(retval); retval = _strdup(retval);
#else #else
retval = strdup(retval); retval = strdup(retval);