Compare commits

..

4 Commits

Author SHA1 Message Date
FinnHornhoover
cd265af8e0 Original Racing Scores Functionality (#257) 2023-10-12 04:42:57 +02:00
CakeLancelot
38c68f351b Update README.md to fix some inaccurate info, move commands to wiki page 2023-10-12 04:00:16 +02:00
edfbe4d005 Make winTerminate() a WINAPI function
This macro makes it use the stdcall calling convention when compiling
for 32-bit Windows. Its absence was technically a bug, but it's a no-op
on 64-bit Windows.
2023-10-12 02:02:23 +02:00
96c430c994 Restore rapid-fire anti-cheat for projectiles
For a while we had made the temporary rapid-fire anti-cheat optional on
master, but had removed it entirely on the refactor branch. The
modification on master was acidentally only applied to the regular
(non-projectile) combat handler, while the removal on refactor removed
both that and the projectile check.

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

This change restores the projectile rapid-fire check such that it is
conditional, just like for regular combat.
2023-10-12 02:02:23 +02:00
7 changed files with 54 additions and 48 deletions

View File

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

View File

@ -365,7 +365,7 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
return ret; return ret;
} }
static bool checkRapidFire(CNSocket *sock, int targetCount) { static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTargets) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
time_t currTime = getTime(); time_t currTime = getTime();
@ -377,7 +377,7 @@ static bool checkRapidFire(CNSocket *sock, int targetCount) {
plr->lastShot = currTime; plr->lastShot = currTime;
// 3+ targets should never be possible // 3+ targets should never be possible
if (targetCount > 3) if (!allowManyTargets && targetCount > 3)
plr->suspicionRating += 10001; plr->suspicionRating += 10001;
// kill the socket when the player is too suspicious // kill the socket when the player is too suspicious
@ -396,7 +396,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
auto targets = (int32_t*)data->trailers; auto targets = (int32_t*)data->trailers;
// kick the player if firing too rapidly // kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt)) if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt, false))
return; return;
/* /*
@ -837,6 +837,10 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
return; return;
} }
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iTargetCnt, true))
return;
/* /*
* initialize response struct * initialize response struct
* rocket style hit doesn't work properly, so we're always sending this one * rocket style hit doesn't work properly, so we're always sending this one

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,31 +99,37 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0) if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
return; // IZ not found return; // IZ not found
uint64_t now = getTime() / 1000; EPInfo& epInfo = EPData[mapNum];
EPRace& epRace = EPRaces[sock];
int timeDiff = now - EPRaces[sock].startTime; uint64_t now = getTime() / 1000;
int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff; int timeDiff = now - epRace.startTime;
if (score < 0) score = 0; // lol int podsCollected = epRace.collectedRings.size();
int fm = score * plr->level * (1.0f / 36) * 0.3f;
int score = std::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;
// we submit the ranking first... // we submit the ranking first...
Database::RaceRanking postRanking = {}; Database::RaceRanking postRanking = {};
postRanking.EPID = EPData[mapNum].EPID; postRanking.EPID = epInfo.EPID;
postRanking.PlayerID = plr->iID; postRanking.PlayerID = plr->iID;
postRanking.RingCount = EPRaces[sock].collectedRings.size(); postRanking.RingCount = podsCollected;
postRanking.Score = score; postRanking.Score = score;
postRanking.Time = timeDiff; postRanking.Time = timeDiff;
postRanking.Timestamp = getTimestamp(); postRanking.Timestamp = getTimestamp();
Database::postRaceRanking(postRanking); Database::postRaceRanking(postRanking);
// ...then we get the top ranking, which may or may not be what we just submitted // ...then we get the top ranking, which may or may not be what we just submitted
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(EPData[mapNum].EPID, plr->iID); Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(epInfo.EPID, plr->iID);
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
// get rank scores and rewards // get rank scores and rewards
std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first; std::vector<int>* rankScores = &EPRewards[epInfo.EPID].first;
std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second; std::vector<int>* rankRewards = &EPRewards[epInfo.EPID].second;
// top ranking // top ranking
int topRank = 0; int topRank = 0;

View File

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

View File

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

View File

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

2
tdata

@ -1 +1 @@
Subproject commit cc65dbb402b5baa2b604ed66132edd88cc82a52a Subproject commit 8c98c8368243a6e2a10cc5fe273236754f896e6a