mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2025-10-14 18:00:32 +00:00
Compare commits
99 Commits
1.5.1
...
e7e4667620
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e7e4667620 | ||
![]() |
d8b63a043e | ||
![]() |
f2447cd940 | ||
![]() |
97dca62fd0 | ||
![]() |
0eff236512 | ||
![]() |
0fcf41cbfe | ||
![]() |
efb3887e3e | ||
![]() |
874bdefb13 | ||
![]() |
94b08659d2 | ||
![]() |
d936d0b621 | ||
![]() |
249e5b081f | ||
![]() |
52c1ed4480 | ||
![]() |
522b9ab7b8 | ||
![]() |
c0a9ec6b7c | ||
![]() |
d416763ae0 | ||
![]() |
ff74c8ede1 | ||
![]() |
e64bcf91a5 | ||
![]() |
b04c292272 | ||
![]() |
36b6aeeb00 | ||
![]() |
93ec6380c1 | ||
![]() |
c1aff8d3c3 | ||
![]() |
1cf2be9968 | ||
![]() |
dd4063e416 | ||
![]() |
464d18820b | ||
![]() |
288a4a3da5 | ||
![]() |
807e182407 | ||
![]() |
0dd628717d | ||
![]() |
c70205a15b | ||
![]() |
8062e52c55 | ||
![]() |
3b5f6c0fe7 | ||
![]() |
e9cd5db8a2 | ||
![]() |
e7450b974c | ||
![]() |
13e71de785 | ||
![]() |
e3c3da87b2 | ||
![]() |
4bc53b2e7e | ||
![]() |
03abb6f830 | ||
![]() |
bd1abcef72 | ||
![]() |
49fa6d65c4 | ||
![]() |
773e4b36e1 | ||
![]() |
945599f93c | ||
![]() |
78c15e2899 | ||
![]() |
eaebe3ba4c | ||
![]() |
9312706524 | ||
![]() |
80dd6b5479 | ||
![]() |
751fd4fd9d | ||
![]() |
ec71fd8f46 | ||
![]() |
f0bb90b547 | ||
![]() |
0dfcc928a9 | ||
![]() |
dc6386131a | ||
![]() |
9977907842 | ||
![]() |
723e455b1d | ||
![]() |
ab4b763f00 | ||
![]() |
8b94bcd5ca | ||
![]() |
1637b8e789 | ||
![]() |
da38bbec29 | ||
![]() |
a07f36e379 | ||
![]() |
afc48b7676 | ||
![]() |
15b6cd2fb4 | ||
![]() |
34669eb7b9 | ||
![]() |
ded7462677 | ||
![]() |
704d6a2452 | ||
![]() |
98634d5aa2 | ||
![]() |
306a75f469 | ||
![]() |
e4e4a421f4 | ||
![]() |
2f612ce0e1 | ||
![]() |
760170af94 | ||
![]() |
2f4c8cdd60 | ||
![]() |
c8f5aab929 | ||
![]() |
9f74b7decb | ||
![]() |
eea4107665 | ||
![]() |
ad53ec82af | ||
![]() |
22c93ac854 | ||
![]() |
710300c04c | ||
![]() |
04221f1c5f | ||
![]() |
828f49cd62 | ||
![]() |
4f49bcea87 | ||
![]() |
f6094fde58 | ||
![]() |
595dcda1b7 | ||
![]() |
b6f15824f1 | ||
![]() |
35e938b8c6 | ||
![]() |
345c9cd3b2 | ||
![]() |
68d53feea3 | ||
![]() |
45742e90a2 | ||
![]() |
69a478b777 | ||
![]() |
c32e5b2d5e | ||
![]() |
3d572432b3 | ||
![]() |
0c4cdaeabf | ||
![]() |
ed866fbee4 | ||
![]() |
5ab0112298 | ||
4494ba5932 | |||
803073213e | |||
4153d5cd30 | |||
71f1f6edb9 | |||
![]() |
32fad56d38 | ||
![]() |
af7b99195f | ||
![]() |
efc00e63b3 | ||
![]() |
7ab01b098d | ||
![]() |
32db574700 | ||
![]() |
c965024d1c |
7
.github/workflows/check-builds.yaml
vendored
7
.github/workflows/check-builds.yaml
vendored
@@ -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 }}
|
||||
|
39
README.md
39
README.md
@@ -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).
|
||||

|
||||
|
||||
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).
|
||||
|
@@ -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();
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
}
|
||||
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) {
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
@@ -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
2
tdata
Submodule tdata updated: 8c98c83682...cc65dbb402
Reference in New Issue
Block a user