99 Commits

Author SHA1 Message Date
Gent Semaj
e7e4667620 Merge d8b63a043e into c636c538eb 2023-09-02 20:50:38 -07:00
gsemaj
d8b63a043e Fix conflicts 2023-08-23 19:22:38 -07:00
gsemaj
f2447cd940 Clear player buffs on death if not revived 2023-08-20 18:16:14 -07:00
gsemaj
97dca62fd0 Fix egg skill handling 2023-08-20 18:07:23 -07:00
gsemaj
0eff236512 Fix corruption reflection 2023-08-19 16:19:36 -07:00
gsemaj
0fcf41cbfe Fix guard FOR REAL 2023-08-19 15:03:16 -07:00
gsemaj
efb3887e3e Fix corruption attack rebound 2023-08-19 14:54:18 -07:00
gsemaj
874bdefb13 Fix passive powers 2023-08-19 14:46:35 -07:00
gsemaj
94b08659d2 Fix battery drain block 2023-08-19 14:21:04 -07:00
gsemaj
d936d0b621 Updater contributer guide 2023-08-19 20:46:41 +00:00
gsemaj
249e5b081f Move corruption block to new system 2023-08-19 20:46:41 +00:00
gsemaj
52c1ed4480 Return home heal to new system 2023-08-19 20:46:41 +00:00
gsemaj
522b9ab7b8 Passive mob buffs to new system 2023-08-19 20:46:41 +00:00
gsemaj
c0a9ec6b7c Move mob active skills to new system 2023-08-19 20:46:41 +00:00
gsemaj
d416763ae0 Move eruption attacks to new system 2023-08-19 20:46:41 +00:00
gsemaj
ff74c8ede1 Implement leech 2023-08-19 20:46:41 +00:00
gsemaj
e64bcf91a5 Make clearBuffs memory-safe
improperly erasing during iteration oops
2023-08-19 20:46:41 +00:00
gsemaj
b04c292272 Break if player dies from buff combat tick 2023-08-19 20:46:41 +00:00
gsemaj
36b6aeeb00 Fix segv: Halt buff iteration if mob is dead 2023-08-19 20:46:41 +00:00
gsemaj
93ec6380c1 Tighten parameter for removeBuff 2023-08-19 20:46:41 +00:00
gsemaj
c1aff8d3c3 Move drain to new system
TODO
- There is a seg fault that happens when drain kills the mob
- leech unimplemented
- mob skills unimplemented
2023-08-19 20:46:41 +00:00
gsemaj
1cf2be9968 reorder some stuff 2023-08-19 20:46:41 +00:00
gsemaj
dd4063e416 Fix icon not disappearing for debuffs 2023-08-19 20:46:41 +00:00
gsemaj
464d18820b Tick buffs for mobs 2023-08-19 20:46:41 +00:00
gsemaj
288a4a3da5 Fix infinite slowdown with snare 2023-08-19 20:46:41 +00:00
gsemaj
807e182407 Implement buff handling for CombatNPC 2023-08-19 20:46:41 +00:00
gsemaj
0dd628717d Move buffs from Mob to CombatNPC 2023-08-19 20:46:41 +00:00
gsemaj
c70205a15b Implement buffs for mobs 2023-08-19 20:46:41 +00:00
gsemaj
8062e52c55 Damage n debuff handler 2023-08-19 20:46:41 +00:00
gsemaj
3b5f6c0fe7 Fix trailing structs
The change to allow flexible trailing struct sizes broke
`attachSkillResults` oops
2023-08-19 20:46:41 +00:00
gsemaj
e9cd5db8a2 Get rid of cbf 2023-08-19 20:46:41 +00:00
gsemaj
e7450b974c Add clearBuffs 2023-08-19 20:46:41 +00:00
gsemaj
13e71de785 Make skill result size check an assertion 2023-08-19 20:46:41 +00:00
gsemaj
e3c3da87b2 Oops 2023-08-19 20:46:41 +00:00
gsemaj
4bc53b2e7e Change SkillResult size validation
Since leech uses trailing structs of two different sizes, just use the max SkillResult size in validation/zeroing and then check for overflow in a couple extra places
2023-08-19 20:46:41 +00:00
gsemaj
03abb6f830 Reorder abilities to match client handling 2023-08-19 20:46:41 +00:00
gsemaj
bd1abcef72 Don't add buff if player dead 2023-08-19 20:46:41 +00:00
gsemaj
49fa6d65c4 Fix seg fault with egg powers 2023-08-19 20:46:41 +00:00
gsemaj
773e4b36e1 Reuse useNanoSkill codepath for passive powers 2023-08-19 20:46:41 +00:00
gsemaj
945599f93c Fix recall 2023-08-19 20:46:41 +00:00
gsemaj
78c15e2899 Make SkillType an enum class 2023-08-19 20:46:41 +00:00
gsemaj
eaebe3ba4c Fix self recall 2023-08-19 20:46:41 +00:00
gsemaj
9312706524 [WIP] Fix targeting for groups 2023-08-19 20:46:41 +00:00
gsemaj
80dd6b5479 [WIP] Active power handling
TODO:
- recall (self and group) is broken
- revive (only group) is broken
- damage + debuff is unimplemented
2023-08-19 20:46:41 +00:00
gsemaj
751fd4fd9d Sync with master 2023-08-19 20:46:41 +00:00
gsemaj
ec71fd8f46 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-08-19 20:46:41 +00:00
gsemaj
f0bb90b547 Move some stuff from playerTick to player combat step 2023-08-19 20:46:41 +00:00
gsemaj
0dfcc928a9 Refactor group handling 2023-08-19 20:46:41 +00:00
gsemaj
dc6386131a Port egg buffs over to new system 2023-08-19 20:46:41 +00:00
gsemaj
9977907842 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-08-19 20:46:41 +00:00
gsemaj
723e455b1d Passive nano powers 2023-08-19 20:46:41 +00:00
gsemaj
ab4b763f00 YET ANOTHER ITERATION of the new ability system
I am very tired
2023-08-19 20:46:41 +00:00
gsemaj
8b94bcd5ca Passive nano powers pt 1 2023-08-19 20:46:41 +00:00
gsemaj
1637b8e789 Passive nano powers boilerplate 2023-08-19 20:46:41 +00:00
gsemaj
da38bbec29 Fix timed out buffs not calling onExpire 2023-08-19 20:46:41 +00:00
gsemaj
a07f36e379 Buff framework tweaks + polish 2023-08-19 20:46:41 +00:00
gsemaj
afc48b7676 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-08-19 20:46:41 +00:00
gsemaj
15b6cd2fb4 oops 2023-08-19 20:46:41 +00:00
gsemaj
34669eb7b9 CRLF purge in Buffs.cpp 2023-08-19 20:46:41 +00:00
gsemaj
ded7462677 egg prep 2023-08-19 20:46:41 +00:00
gsemaj
704d6a2452 Move Buff implementation to Buffs.cpp 2023-08-19 20:46:41 +00:00
gsemaj
98634d5aa2 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-08-19 20:46:41 +00:00
gsemaj
306a75f469 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-08-19 20:46:41 +00:00
gsemaj
e4e4a421f4 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-08-19 20:46:41 +00:00
gsemaj
2f612ce0e1 Handle case where cmake is invoked outside root 2023-08-19 20:46:41 +00:00
gsemaj
760170af94 Start moving passive power processing to playerTick 2023-08-19 20:46:41 +00:00
gsemaj
2f4c8cdd60 Groundwork for new buff system 2023-08-19 20:46:41 +00:00
gsemaj
c8f5aab929 Active power classification 2023-08-19 20:46:41 +00:00
gsemaj
9f74b7decb some struct reorg 2023-08-19 20:46:41 +00:00
gsemaj
eea4107665 Replace group filter operator with function 2023-08-19 20:46:41 +00:00
gsemaj
ad53ec82af Ignore .bak files
for my local backups lol
2023-08-19 20:46:41 +00:00
gsemaj
22c93ac854 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-08-19 20:46:41 +00:00
gsemaj
710300c04c (WIP) EXPERIMENTAL GROUP CHANGES 2023-08-19 20:46:41 +00:00
gsemaj
04221f1c5f (WIP) TODO ABILITIES 2023-08-19 20:46:41 +00:00
gsemaj
828f49cd62 Move mob aggro logic into takeDamage override
God that feels good
2023-08-19 20:46:41 +00:00
gsemaj
4f49bcea87 (WIP) Move away from rigid states/transitions to allow custom behavior 2023-08-19 20:46:41 +00:00
gsemaj
f6094fde58 EntityType -> EntityKind 2023-08-19 20:46:41 +00:00
gsemaj
595dcda1b7 (WIP) onRoamStart hook implementation 2023-08-19 20:46:41 +00:00
gsemaj
b6f15824f1 (WIP) Remove BaseNPC::barkerType to save space 2023-08-19 20:46:41 +00:00
gsemaj
35e938b8c6 ope 2023-08-19 20:46:41 +00:00
gsemaj
345c9cd3b2 (WIP) onCombatStart hook implementation 2023-08-19 20:46:41 +00:00
gsemaj
68d53feea3 (WIP) onDeath hook implementation 2023-08-19 20:46:41 +00:00
gsemaj
45742e90a2 (WIP) Add src param to transition + certain hooks
Should all hooks have src? I think not
2023-08-19 20:46:41 +00:00
gsemaj
69a478b777 (WIP) Transitions + hook definitions + onRetreat hook implementation 2023-08-19 20:46:41 +00:00
gsemaj
c32e5b2d5e (WIP) Point 2: Generalization 2023-08-19 20:46:41 +00:00
gsemaj
3d572432b3 (WIP) Point 1: step functions 2023-08-19 20:46:41 +00:00
gsemaj
0c4cdaeabf (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-08-19 20:46:41 +00:00
gsemaj
ed866fbee4 (WIP) Move ICombatant functions around a bit 2023-08-19 20:46:41 +00:00
gsemaj
5ab0112298 (WIP) Initial ICombatant draft 2023-08-19 20:46:41 +00:00
4494ba5932 [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-08-19 20:46:41 +00:00
803073213e [refactor] Replace a few uses of magic numbers with enums 2023-08-19 20:46:41 +00:00
4153d5cd30 [refactor] Cosmetic cleanup in Fuse fight functions 2023-08-19 20:46:41 +00:00
71f1f6edb9 [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-08-19 20:46:41 +00:00
gsemaj
32fad56d38 [WIP] Stub power handler 2023-08-19 20:46:41 +00:00
gsemaj
af7b99195f [WIP] Use EntityRef instead of CNSocket in ability handler 2023-08-19 20:46:41 +00:00
gsemaj
efc00e63b3 [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-08-19 20:46:41 +00:00
gsemaj
7ab01b098d [WIP] Rename Entity.type -> Entity.kind 2023-08-19 20:46:41 +00:00
gsemaj
32db574700 [WIP] Fix Nanos -> Abilities namespace calls 2023-08-19 20:46:41 +00:00
gsemaj
c965024d1c [WIP] Initial merge of ability namespaces & features 2023-08-19 20:46:41 +00:00
18 changed files with 138 additions and 161 deletions

View File

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

View File

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

View File

@@ -76,20 +76,12 @@ static SkillResult handleSkillDamageNDebuff(SkillData* skill, int power, ICombat
}
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.iID = target->getID();
result.bProtected = blocked;
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);
}
@@ -120,8 +112,7 @@ static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* sour
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
skill->drainType == SkillDrainType::PASSIVE ? 1 : (duration * 100) / MS_PER_COMBAT_TICK, // ticks
strength, // value
source->getRef(), // source
source == target ? BuffClass::NANO : BuffClass::GROUP_NANO, // buff class
@@ -129,10 +120,9 @@ static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* sour
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) {
if(buff->id == ECSB_BOUNDINGBALL) {
// drain
ICombatant* combatant = dynamic_cast<ICombatant*>(self.getEntity());
combatant->takeDamage(buff->getLastSource(), 0); // aggro
@@ -141,13 +131,11 @@ static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* sour
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++;
[](EntityRef self, Buff* buff, time_t currTime) {
if(buff->id == ECSB_BOUNDINGBALL)
Buffs::tickDrain(self, buff); // drain
},
&passiveBuff)) return SkillResult();
&passiveBuff)) return SkillResult(); // no result if already buffed
sSkillResult_Buff result{};
result.eCT = target->getCharType();

View File

@@ -9,18 +9,17 @@
#include <vector>
#include <assert.h>
const int COMBAT_TICKS_PER_DRAIN_PROC = 2;
constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain);
enum class SkillType {
DAMAGE = 1,
HEAL_HP = 2,
KNOCKDOWN = 3, // uses DamageNDebuff
SLEEP = 4, // uses DamageNDebuff
SNARE = 5, // uses DamageNDebuff
KNOCKDOWN = 3, // dnd
SLEEP = 4, // dnd
SNARE = 5, // dnd
HEAL_STAMINA = 6,
STAMINA_SELF = 7,
STUN = 8, // uses DamageNDebuff
STUN = 8, // dnd
WEAPONSLOW = 9,
JUMP = 10,
RUN = 11,

View File

@@ -169,13 +169,12 @@ void Buffs::timeBuffTimeout(EntityRef self) {
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) {
void Buffs::tickDrain(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);
int damage = combatant->getMaxHP() / 100 * mult;
int dealt = combatant->takeDamage(buff->getLastSource(), damage);
int damage = combatant->takeDamage(buff->getLastSource(), combatant->getMaxHP() / 100);
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
@@ -188,7 +187,7 @@ void Buffs::tickDrain(EntityRef self, Buff* buff, int mult) {
pkt->iTB_ID = ECSB_BOUNDINGBALL;
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
drain->iDamage = dealt;
drain->iDamage = damage;
drain->iHP = combatant->getCurrentHP();
drain->eCT = pkt->eCT;
drain->iID = pkt->iID;

View File

@@ -89,5 +89,5 @@ namespace Buffs {
void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack);
void timeBuffTick(EntityRef self, Buff* buff);
void timeBuffTimeout(EntityRef self);
void tickDrain(EntityRef self, Buff* buff, int mult);
void tickDrain(EntityRef self, Buff* buff);
}

View File

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

View File

@@ -175,13 +175,10 @@ void Player::step(time_t currTime) {
#pragma endregion
#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;
@@ -192,7 +189,7 @@ bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, Buff
return false;
}
Buff* CombatNPC::getBuff(int buffId) {
Buff* CombatNPC::getBuff(int buffId) { /* stubbed */
if(hasBuff(buffId)) {
return buffs[buffId];
}
@@ -310,19 +307,19 @@ void CombatNPC::step(time_t currTime) {
}
void CombatNPC::transition(AIState newState, EntityRef src) {
state = newState;
state = newState;
if (transitionHandlers.find(newState) != transitionHandlers.end())
transitionHandlers[newState](this, src);
else {
std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl;
transition(AIState::INACTIVE, id);
}
// trigger special NPCEvents, if applicable
/* TODO: fire any triggered events
for (NPCEvent& event : NPCManager::NPCEvents)
if (event.triggerState == newState && event.npcType == type)
event.handler(this);
if (event.trigger == ON_KILLED && event.npcType == type)
event.handler(src, this);
*/
}
#pragma endregion
@@ -365,7 +362,7 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
return ret;
}
static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTargets) {
static bool checkRapidFire(CNSocket *sock, int targetCount) {
Player *plr = PlayerManager::getPlayer(sock);
time_t currTime = getTime();
@@ -377,7 +374,7 @@ static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTarget
plr->lastShot = currTime;
// 3+ targets should never be possible
if (!allowManyTargets && targetCount > 3)
if (targetCount > 3)
plr->suspicionRating += 10001;
// kill the socket when the player is too suspicious
@@ -396,7 +393,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
auto targets = (int32_t*)data->trailers;
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt, false))
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt))
return;
/*
@@ -837,10 +834,6 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
return;
}
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iTargetCnt, true))
return;
/*
* initialize response struct
* rocket style hit doesn't work properly, so we're always sending this one

View File

@@ -106,11 +106,11 @@ struct CombatNPC : public BaseNPC, public ICombatant {
std::unordered_map<int, Buff*> buffs = {};
CombatNPC(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, int id, int maxHP)
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP)
: BaseNPC(angle, iID, t, id), maxHealth(maxHP) {
this->spawnX = spawnX;
this->spawnY = spawnY;
this->spawnZ = spawnZ;
spawnX = x;
spawnY = y;
spawnZ = z;
kind = EntityKind::COMBAT_NPC;

View File

@@ -50,8 +50,8 @@ struct Mob : public CombatNPC {
// temporary; until we're sure what's what
nlohmann::json data = {};
Mob(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(spawnX, spawnY, spawnZ, angle, iID, t, id, d["m_iHP"]),
Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]),
sightRange(d["m_iSightRange"]) {
state = AIState::ROAMING;
@@ -62,9 +62,9 @@ struct Mob : public CombatNPC {
idleRange = (int)data["m_iIdleRange"];
level = data["m_iNpcLevel"];
roamX = spawnX;
roamY = spawnY;
roamZ = spawnZ;
roamX = x;
roamY = y;
roamZ = z;
offsetX = 0;
offsetY = 0;

View File

@@ -122,15 +122,16 @@ static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
}
// type must already be checked and updateNPCPosition() must be called on the result
BaseNPC *NPCManager::summonNPC(int spawnX, int spawnY, int spawnZ, uint64_t instance, int type, bool respawn, bool baseInstance) {
BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) {
uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
//assert(nextId < INT32_MAX);
int id = nextId--;
int team = NPCData[type]["m_iTeam"];
BaseNPC *npc = nullptr;
if (team == 2) {
npc = new Mob(spawnX, spawnY, spawnZ, inst, type, NPCData[type], id);
npc = new Mob(x, y, z, inst, type, NPCData[type], id);
// re-enable respawning, if desired
((Mob*)npc)->summoned = !respawn;
@@ -293,55 +294,57 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
return npc;
}
// TODO: Move this to separate file in ai/ subdir when implementing more events
// TODO: Move this to MobAI, possibly
#pragma region NPCEvents
// summon right arm and stage 2 body
static void lordFuseStageTwo(CombatNPC *npc) {
static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc; // adaptium, stun
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage two" << std::endl;
// Fuse doesn't move
// 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;
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
oldbody->instanceID, oldbody->angle);
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
plr->instanceID, oldbody->angle);
// 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;
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
oldbody->instanceID, oldbody->angle);
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
plr->instanceID, oldbody->angle);
}
// summon left arm and stage 3 body
static void lordFuseStageThree(CombatNPC *npc) {
static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc;
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage three" << std::endl;
// Cosmix, Damage Point
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2468);
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2468);
newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
newbody->instanceID, oldbody->angle);
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
plr->instanceID, oldbody->angle);
// 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;
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
arm->instanceID, oldbody->angle);
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
plr->instanceID, oldbody->angle);
}
std::vector<NPCEvent> NPCManager::NPCEvents = {
NPCEvent(2466, AIState::DEAD, lordFuseStageTwo),
NPCEvent(2467, AIState::DEAD, lordFuseStageThree),
NPCEvent(2466, ON_KILLED, lordFuseStageTwo),
NPCEvent(2467, ON_KILLED, lordFuseStageThree),
};
#pragma endregion NPCEvents

View File

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

View File

@@ -77,10 +77,7 @@ void PlayerManager::updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, ui
plr->x = X;
plr->y = Y;
plr->z = Z;
if (plr->instanceID != I) {
plr->instanceID = I;
plr->recallInstance = INSTANCE_OVERWORLD;
}
plr->instanceID = I;
if (oldChunk == newChunk)
return; // didn't change chunks
Chunking::updateEntityChunk({sock}, oldChunk, newChunk);
@@ -126,6 +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);
}
if (I != INSTANCE_OVERWORLD) {
INITSTRUCT(sP_FE2CL_INSTANCE_MAP_INFO, pkt);
pkt.iInstanceMapNum = (int32_t)MAPNUM(I); // lower 32 bits are mapnum
if (I != fromInstance // do not retransmit MAP_INFO on recall
&& Racing::EPData.find(pkt.iInstanceMapNum) != Racing::EPData.end()) {
EPInfo* ep = &Racing::EPData[pkt.iInstanceMapNum];
pkt.iEP_ID = ep->EPID;
pkt.iMapCoordX_Min = ep->zoneX * 51200;
pkt.iMapCoordX_Max = (ep->zoneX + 1) * 51200;
pkt.iMapCoordY_Min = ep->zoneY * 51200;
pkt.iMapCoordY_Max = (ep->zoneY + 1) * 51200;
pkt.iMapCoordZ_Min = INT32_MIN;
pkt.iMapCoordZ_Max = INT32_MAX;
}
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
}
INITSTRUCT(sP_FE2CL_REP_PC_GOTO_SUCC, pkt2);
pkt2.iX = X;
pkt2.iY = Y;
@@ -359,24 +374,6 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) {
updatePlayerPosition(sock, plr->x, plr->y, plr->z, plr->instanceID, plr->angle);
sock->sendPacket(response, P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC);
if (plr->instanceID != INSTANCE_OVERWORLD) {
INITSTRUCT(sP_FE2CL_INSTANCE_MAP_INFO, pkt);
pkt.iInstanceMapNum = (int32_t)MAPNUM(plr->instanceID); // lower 32 bits are mapnum
if (pkt.iInstanceMapNum != plr->recallInstance // do not retransmit MAP_INFO on recall
&& Racing::EPData.find(pkt.iInstanceMapNum) != Racing::EPData.end()) {
EPInfo* ep = &Racing::EPData[pkt.iInstanceMapNum];
pkt.iEP_ID = ep->EPID;
pkt.iMapCoordX_Min = ep->zoneX * 51200;
pkt.iMapCoordX_Max = (ep->zoneX + 1) * 51200;
pkt.iMapCoordY_Min = ep->zoneY * 51200;
pkt.iMapCoordY_Max = (ep->zoneY + 1) * 51200;
pkt.iMapCoordZ_Min = INT32_MIN;
pkt.iMapCoordZ_Max = INT32_MAX;
}
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
}
}
static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
@@ -580,7 +577,7 @@ static void setFirstUseFlag(CNSocket* sock, CNPacketData* data) {
std::cout << "[WARN] Client submitted invalid first use flag number?!" << std::endl;
return;
}
if (flag->iFlagCode <= 64)
plr->iFirstUseFlag[0] |= (1ULL << (flag->iFlagCode - 1));
else

View File

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

View File

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

View File

@@ -375,7 +375,7 @@ static void loadPaths(json& pathData, int32_t* nextId) {
Transport::NPCPaths.push_back(pathTemplate);
}
std::cout << "[INFO] Loaded " << Transport::NPCPaths.size() << " NPC paths" << std::endl;
}
catch (const std::exception& err) {
std::cerr << "[FATAL] Malformed paths.json file! Reason:" << err.what() << std::endl;
@@ -584,17 +584,8 @@ static void loadDrops(json& dropData) {
continue;
}
EPInfo& epInfo = Racing::EPData[EPMap];
// max score is specified in the XDT, but can be updated if specified in the drops JSON
epInfo.maxScore = (int)race["ScoreCap"];
// time limit and total pods are not stored in the XDT, so we include it in the drops JSON
epInfo.maxTime = (int)race["TimeLimit"];
epInfo.maxPods = (int)race["TotalPods"];
// IZ-specific calculated constants included in the drops JSON
epInfo.scaleFactor = (double)race["ScaleFactor"];
epInfo.podFactor = (double)race["PodFactor"];
epInfo.timeFactor = (double)race["TimeFactor"];
// time limit isn't stored in the XDT, so we include it in the reward table instead
Racing::EPData[EPMap].maxTime = race["TimeLimit"];
// score cutoffs
std::vector<int> rankScores;
@@ -695,7 +686,7 @@ static void loadEggs(json& eggData, int32_t* nextId) {
}
}
/*
/*
* Load gruntwork output, if it exists
*/
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
@@ -1370,7 +1361,7 @@ void TableData::flush() {
targetIDs.push_back(tID);
for (int32_t tType : path.targetTypes)
targetTypes.push_back(tType);
pathObj["iBaseSpeed"] = path.speed;
pathObj["iTaskID"] = path.escortTaskID;
pathObj["bRelative"] = path.isRelative;

View File

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

2
tdata

Submodule tdata updated: 8c98c83682...cc65dbb402