9 Commits

Author SHA1 Message Date
FinnHornhoover
50df74212b Merge f4b36b8f73 into 70a27afad1 2023-10-08 17:02:43 -04:00
FinnHornhoover
70a27afad1 Pimpleback IZ Pod Fix (#259) 2023-10-08 17:02:32 -04:00
gsemaj
6cfb3bf532 Merge branch 'refactor' 2023-10-08 16:54:42 -04:00
CakeLancelot
ab480d88f1 Update version numbers to 1.5 2023-10-07 18:20:04 -05:00
CakeLancelot
89772d763b CI: specify Ubuntu runner version and fix artifact zip name
We were technically already using 22.04 for a bit, it got updated without us noticing since the version was set to `ubuntu-latest`.
Affix the version to 22.04 so that it doesn't get unexpectedly updated again, and update the artifact's zip to reflect the change.
2023-10-07 17:13:13 -05:00
bd0cc3c212 Fix regression with /speed and /jump after previous change
Also changed the case values to use the client definitions.
2023-09-13 04:37:49 +02:00
c636c538eb Fix minor visual bug in setValuePlayer() 2023-09-02 20:59:34 +02:00
d3bef95a7f Fix /npcr
Also removed a redundant invocation of NPCManager::updateNPCPosition()
and simplified the surrounding code.
2023-08-20 05:06:16 +02:00
FinnHornhoover
f4b36b8f73 added alternate racing score funcitonality 2023-07-24 00:33:19 +03:00
8 changed files with 118 additions and 62 deletions

View File

@@ -53,7 +53,7 @@ jobs:
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: 'ubuntu20_04-bin-x64-${{ env.SHORT_SHA }}' name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
path: bin path: bin
windows-build: windows-build:
@@ -112,7 +112,7 @@ jobs:
copy-artifacts: copy-artifacts:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest runs-on: ubuntu-22.04
needs: [windows-build, ubuntu-build] needs: [windows-build, ubuntu-build]
env: env:
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }} BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}

View File

@@ -13,13 +13,13 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
### Getting Started ### Getting Started
#### Method A: Installer (Easiest) #### Method A: Installer (Easiest)
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4-Installer.exe) - choose to run the file. 1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5/OpenFusionClient-1.5-Installer.exe) - choose to run the file.
2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect. 2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
3. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen. 3. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
4. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 3. 4. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 3.
#### Method B: Standalone .zip file #### Method B: Standalone .zip file
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4.zip). 1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5/OpenFusionClient-1.5.zip).
2. Extract it to a folder of your choice. Note: if you are upgrading from an older version, it is preferable to start with a fresh folder rather than overwriting a previous install. 2. Extract it to a folder of your choice. Note: if you are upgrading from an older version, it is preferable to start with a fresh folder rather than overwriting a previous install.
3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect. 3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen. 4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
@@ -29,7 +29,7 @@ Instructions for getting the client to run on Linux through Wine can be found [h
### Hosting a server ### Hosting a server
1. Grab `OpenFusionServer-1.4-original.zip` or `OpenFusionServer-1.4-academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.4). 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.

View File

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

View File

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

View File

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

View File

@@ -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,47 @@ 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];
// if there are no divide-by-zero dangers, and at least one factor has been specified
// we switch over to OG scoring
bool useOGScoring = (epInfo.maxPods > 0) && (epInfo.maxTime > 0) && (
(epInfo.scaleFactor > 0.0) || (epInfo.podFactor > 0.0) || (epInfo.timeFactor > 0.0));
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 = 0, fm = 0;
if (useOGScoring) {
score = std::min(epInfo.maxScore, (int)std::exp(
(epInfo.podFactor * podsCollected) / epInfo.maxPods
- (epInfo.timeFactor * timeDiff) / epInfo.maxTime
+ epInfo.scaleFactor));
fm = (1.0 + std::exp(epInfo.scaleFactor - 1.0) * epInfo.podFactor * podsCollected) / epInfo.maxPods;
} else {
score = std::max(0, 500 * podsCollected - 10 * timeDiff);
fm = score * plr->level * (1.0f / 36) * 0.3f;
}
// 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;
// (maybe) available through drops
int maxTime = 0, maxPods = 0;
double scaleFactor = 0.0, podFactor = 0.0, timeFactor = 0.0;
}; };
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,35 @@ 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 // time limit isn't stored in the XDT, so we include it in the reward table instead
Racing::EPData[EPMap].maxTime = race["TimeLimit"]; epInfo.maxTime = (int)race["TimeLimit"];
// update max score (if present)
if (race.find("ScoreCap") != race.end()) {
epInfo.maxScore = (int)race["ScoreCap"];
}
// update max pods (if present)
if (race.find("TotalPods") != race.end()) {
epInfo.maxPods = (int)race["TotalPods"];
}
// update scale factor (if present)
if (race.find("ScaleFactor") != race.end()) {
epInfo.scaleFactor = (double)race["ScaleFactor"];
}
// update pod factor (if present)
if (race.find("PodFactor") != race.end()) {
epInfo.podFactor = (double)race["PodFactor"];
}
// update time factor (if present)
if (race.find("TimeFactor") != race.end()) {
epInfo.timeFactor = (double)race["TimeFactor"];
}
// score cutoffs // score cutoffs
std::vector<int> rankScores; std::vector<int> rankScores;
@@ -686,7 +713,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 +1388,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;