mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2025-10-22 21:20:08 +00:00
Compare commits
40 Commits
refactor
...
02c1c681dd
Author | SHA1 | Date | |
---|---|---|---|
02c1c681dd | |||
![]() |
225515ec21 | ||
![]() |
f436108d24 | ||
![]() |
1781ac6b03 | ||
![]() |
b0e2391f24 | ||
![]() |
6f88613ede | ||
![]() |
78e4779db6 | ||
![]() |
47dbc6d35e | ||
![]() |
b780f5ee60 | ||
![]() |
003186d97a | ||
![]() |
6d2f120305 | ||
![]() |
51615db230 | ||
![]() |
233d21ecd7 | ||
![]() |
54327b0c23 | ||
![]() |
fa8c1e73d1 | ||
![]() |
aeac57ebf7 | ||
![]() |
632406e93b | ||
837f109752
|
|||
c11cfebdb1
|
|||
20367d77f0
|
|||
8d04f31c61
|
|||
![]() |
44560a46b7 | ||
![]() |
21d280147c | ||
![]() |
b765821552 | ||
e61682dfb2 | |||
![]() |
d9ebb4e3ef | ||
![]() |
73c610b471 | ||
![]() |
3e6bfea3fe | ||
![]() |
cd265af8e0 | ||
![]() |
38c68f351b | ||
edfbe4d005 | |||
96c430c994 | |||
![]() |
4592fc42af | ||
![]() |
70a27afad1 | ||
![]() |
6cfb3bf532 | ||
![]() |
ab480d88f1 | ||
![]() |
89772d763b | ||
bd0cc3c212 | |||
c636c538eb | |||
d3bef95a7f |
@@ -1 +0,0 @@
|
||||
version.h
|
7
.github/workflows/check-builds.yaml
vendored
7
.github/workflows/check-builds.yaml
vendored
@@ -9,12 +9,13 @@ on:
|
||||
- CMakeLists.txt
|
||||
- Makefile
|
||||
pull_request:
|
||||
types: ready_for_review
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- src/**
|
||||
- vendor/**
|
||||
- CMakeLists.txt
|
||||
- Makefile
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ubuntu-build:
|
||||
@@ -53,7 +54,7 @@ jobs:
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'ubuntu20_04-bin-x64-${{ env.SHORT_SHA }}'
|
||||
name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
|
||||
path: bin
|
||||
|
||||
windows-build:
|
||||
@@ -112,7 +113,7 @@ jobs:
|
||||
|
||||
copy-artifacts:
|
||||
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]
|
||||
env:
|
||||
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}
|
||||
|
38
.github/workflows/push-docker-image.yml
vendored
Normal file
38
.github/workflows/push-docker-image.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Push Docker Image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push-docker-image:
|
||||
name: Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix:
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Retrieve major version
|
||||
uses: winterjung/split@v2
|
||||
id: split
|
||||
with:
|
||||
msg: ${{ github.ref_name }}
|
||||
separator: .
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
- name: Build and push the Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ matrix.platforms }}
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_REPOSITORY }}:${{ github.ref_name }},${{ secrets.DOCKERHUB_REPOSITORY }}:${{ steps.split.outputs._0 }},${{ secrets.DOCKERHUB_REPOSITORY }}:latest
|
25
Dockerfile
25
Dockerfile
@@ -1,4 +1,5 @@
|
||||
FROM debian:latest
|
||||
# build
|
||||
FROM debian:stable-slim as build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -8,14 +9,24 @@ clang \
|
||||
make \
|
||||
libsqlite3-dev
|
||||
|
||||
COPY . ./
|
||||
COPY src ./src
|
||||
COPY vendor ./vendor
|
||||
COPY .git ./.git
|
||||
COPY Makefile CMakeLists.txt version.h.in ./
|
||||
|
||||
RUN make -j8
|
||||
|
||||
# tabledata should be copied from the host;
|
||||
# clone it there before building the container
|
||||
#RUN git submodule update --init --recursive
|
||||
# prod
|
||||
FROM debian:stable-slim
|
||||
|
||||
CMD ["./bin/fusion"]
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
LABEL Name=openfusion Version=0.0.1
|
||||
RUN apt-get -y update && apt-get install -y \
|
||||
libsqlite3-dev
|
||||
|
||||
COPY --from=build /usr/src/app/bin/fusion /bin/fusion
|
||||
COPY sql ./sql
|
||||
|
||||
CMD ["/bin/fusion"]
|
||||
|
||||
LABEL Name=openfusion Version=0.0.2
|
||||
|
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2023 OpenFusion Contributors
|
||||
Copyright (c) 2020-2024 OpenFusion Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
2
Makefile
2
Makefile
@@ -95,8 +95,6 @@ CXXHDR=\
|
||||
vendor/bcrypt/BCrypt.hpp\
|
||||
vendor/INIReader.hpp\
|
||||
vendor/JSON.hpp\
|
||||
vendor/INIReader.hpp\
|
||||
vendor/JSON.hpp\
|
||||
src/Buffs.hpp\
|
||||
src/Chat.hpp\
|
||||
src/CustomCommands.hpp\
|
||||
|
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.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.
|
||||
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.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.
|
||||
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,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).
|
||||
|
||||
### 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.
|
||||
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.
|
||||
@@ -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 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`.
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -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").
|
||||
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.
|
||||
|
||||
@@ -102,26 +98,13 @@ 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.
|
||||
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.
|
||||
|
||||

|
||||
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
|
||||
* 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.).
|
||||
When hosting a local server, you will have access to all commands by default (account level 1).
|
||||
|
||||
### 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).
|
||||
For a list of available commands, see [this wiki page](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).
|
||||
|
@@ -66,6 +66,9 @@ motd=Welcome to OpenFusion!
|
||||
# location of the database
|
||||
#dbpath=database.db
|
||||
|
||||
# should there be a score cap for infected zone races?
|
||||
#izracescorecapped=true
|
||||
|
||||
# should tutorial flags be disabled off the bat?
|
||||
disablefirstuseflag=true
|
||||
|
||||
|
@@ -6,6 +6,10 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
volumes:
|
||||
- ./config.ini:/usr/src/app/config.ini
|
||||
- ./database.db:/usr/src/app/database.db
|
||||
- ./tdata:/usr/src/app/tdata
|
||||
ports:
|
||||
- "23000:23000"
|
||||
- "23001:23001"
|
||||
|
@@ -30,7 +30,7 @@ static bool playerHasBuddyWithID(Player* plr, int buddyID) {
|
||||
#pragma endregion
|
||||
|
||||
// Refresh buddy list
|
||||
void Buddies::refreshBuddyList(CNSocket* sock) {
|
||||
void Buddies::sendBuddyList(CNSocket* sock) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
int buddyCnt = Database::getNumBuddies(plr);
|
||||
|
||||
@@ -277,15 +277,6 @@ static void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) {
|
||||
// Getting buddy state
|
||||
static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
/*
|
||||
* If the buddy list wasn't synced a second time yet, sync it.
|
||||
* Not sure why we have to do it again for the client not to trip up.
|
||||
*/
|
||||
if (!plr->buddiesSynced) {
|
||||
refreshBuddyList(sock);
|
||||
plr->buddiesSynced = true;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);
|
||||
|
||||
|
@@ -6,5 +6,5 @@ namespace Buddies {
|
||||
void init();
|
||||
|
||||
// Buddy list
|
||||
void refreshBuddyList(CNSocket* sock);
|
||||
void sendBuddyList(CNSocket* sock);
|
||||
}
|
||||
|
@@ -72,31 +72,41 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
|
||||
|
||||
// Handle serverside value-changes
|
||||
switch (setData->iSetValueType) {
|
||||
case 1:
|
||||
plr->HP = setData->iSetValue;
|
||||
case CN_GM_SET_VALUE_TYPE__HP:
|
||||
response.iSetValue = plr->HP = setData->iSetValue;
|
||||
break;
|
||||
case 2:
|
||||
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY :
|
||||
plr->batteryW = setData->iSetValue;
|
||||
|
||||
// caps
|
||||
if (plr->batteryW > 9999)
|
||||
plr->batteryW = 9999;
|
||||
|
||||
response.iSetValue = plr->batteryW;
|
||||
break;
|
||||
case 3:
|
||||
case CN_GM_SET_VALUE_TYPE__NANO_BATTERY:
|
||||
plr->batteryN = setData->iSetValue;
|
||||
|
||||
// caps
|
||||
if (plr->batteryN > 9999)
|
||||
plr->batteryN = 9999;
|
||||
|
||||
response.iSetValue = plr->batteryN;
|
||||
break;
|
||||
case 4:
|
||||
case CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
|
||||
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
|
||||
response.iSetValue = plr->fusionmatter;
|
||||
break;
|
||||
case 5:
|
||||
plr->money = setData->iSetValue;
|
||||
case CN_GM_SET_VALUE_TYPE__CANDY:
|
||||
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;
|
||||
response.iSetValue = setData->iSetValue;
|
||||
response.iSetValueType = setData->iSetValueType;
|
||||
|
||||
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);
|
||||
|
@@ -365,7 +365,7 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool checkRapidFire(CNSocket *sock, int targetCount) {
|
||||
static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTargets) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
time_t currTime = getTime();
|
||||
|
||||
@@ -377,7 +377,7 @@ static bool checkRapidFire(CNSocket *sock, int targetCount) {
|
||||
plr->lastShot = currTime;
|
||||
|
||||
// 3+ targets should never be possible
|
||||
if (targetCount > 3)
|
||||
if (!allowManyTargets && targetCount > 3)
|
||||
plr->suspicionRating += 10001;
|
||||
|
||||
// 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;
|
||||
|
||||
// kick the player if firing too rapidly
|
||||
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt))
|
||||
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt, false))
|
||||
return;
|
||||
|
||||
/*
|
||||
@@ -837,6 +837,10 @@ 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
|
||||
|
@@ -83,7 +83,77 @@ static void helpCommand(std::string full, std::vector<std::string>& args, CNSock
|
||||
}
|
||||
|
||||
static void accessCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
|
||||
Chat::sendServerMessage(sock, "Your access level is " + std::to_string(PlayerManager::getPlayer(sock)->accountLevel));
|
||||
if (args.size() < 2) {
|
||||
Chat::sendServerMessage(sock, "Usage: /access <id> [new_level]");
|
||||
Chat::sendServerMessage(sock, "Use . for id to select yourself");
|
||||
return;
|
||||
}
|
||||
|
||||
char *tmp;
|
||||
|
||||
Player* self = PlayerManager::getPlayer(sock);
|
||||
int selfAccess = self->accountLevel;
|
||||
|
||||
Player* player;
|
||||
if (args[1].compare(".") == 0) {
|
||||
player = self;
|
||||
} else {
|
||||
int id = std::strtol(args[1].c_str(), &tmp, 10);
|
||||
if (*tmp) {
|
||||
Chat::sendServerMessage(sock, "Invalid player ID " + args[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
player = PlayerManager::getPlayerFromID(id);
|
||||
if (player == nullptr) {
|
||||
Chat::sendServerMessage(sock, "Could not find player with ID " + std::to_string(id));
|
||||
return;
|
||||
}
|
||||
|
||||
// Messing with other players requires a baseline access of 30
|
||||
if (player != self && selfAccess > 30) {
|
||||
Chat::sendServerMessage(sock, "Can't check or change other players access levels (insufficient privileges)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::string playerName = PlayerManager::getPlayerName(player);
|
||||
int currentAccess = player->accountLevel;
|
||||
if (args.size() < 3) {
|
||||
// just check
|
||||
Chat::sendServerMessage(sock, playerName + " has access level " + std::to_string(currentAccess));
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't change the access level of someone with stronger privileges
|
||||
// N.B. lower value = stronger privileges
|
||||
if (currentAccess <= selfAccess) {
|
||||
Chat::sendServerMessage(sock, "Can't change this player's access level (insufficient privileges)");
|
||||
return;
|
||||
}
|
||||
|
||||
int newAccess = std::strtol(args[2].c_str(), &tmp, 10);
|
||||
if (*tmp) {
|
||||
Chat::sendServerMessage(sock, "Invalid access level " + args[2]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Can only assign an access level weaker than yours
|
||||
if (newAccess <= selfAccess) {
|
||||
Chat::sendServerMessage(sock, "Can only assign privileges weaker than your own");
|
||||
return;
|
||||
}
|
||||
|
||||
player->accountLevel = newAccess;
|
||||
|
||||
// Save to database
|
||||
int accountId = Database::getAccountIdForPlayer(player->iID);
|
||||
Database::updateAccountLevel(accountId, newAccess);
|
||||
|
||||
std::string msg = "Changed access level for " + playerName + " from " + std::to_string(currentAccess) + " to " + std::to_string(newAccess);
|
||||
if (newAccess <= 50 && currentAccess > 50)
|
||||
msg += " (they must log out and back in for some commands to be enabled)";
|
||||
Chat::sendServerMessage(sock, msg);
|
||||
}
|
||||
|
||||
static void populationCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
|
||||
@@ -359,23 +429,19 @@ static void npcRotateCommand(std::string full, std::vector<std::string>& args, C
|
||||
int angle = (plr->angle + 180) % 360;
|
||||
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle);
|
||||
|
||||
// if it's a gruntwork NPC, rotate in-place
|
||||
if (TableData::RunningMobs.find(npc->id) != TableData::RunningMobs.end()) {
|
||||
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle);
|
||||
bool isGruntworkNpc = true;
|
||||
|
||||
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for gruntwork NPC "
|
||||
+ std::to_string(npc->id));
|
||||
} else {
|
||||
// add a rotation entry to the gruntwork file, unless it's already a gruntwork NPC
|
||||
if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end()) {
|
||||
TableData::RunningNPCRotations[npc->id] = angle;
|
||||
|
||||
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for NPC "
|
||||
+ std::to_string(npc->id));
|
||||
isGruntworkNpc = false;
|
||||
}
|
||||
|
||||
// update rotation clientside
|
||||
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
|
||||
pkt.NPCAppearanceData = npc->getAppearanceData();
|
||||
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
|
||||
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) +
|
||||
" for " + (isGruntworkNpc ? "gruntwork " : "") + "NPC " + std::to_string(npc->id));
|
||||
|
||||
// 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) {
|
||||
@@ -1204,7 +1270,7 @@ static void registerCommand(std::string cmd, int requiredLevel, CommandHandler h
|
||||
|
||||
void CustomCommands::init() {
|
||||
registerCommand("help", 100, helpCommand, "list all unlocked server-side commands");
|
||||
registerCommand("access", 100, accessCommand, "print your access level");
|
||||
registerCommand("access", 100, accessCommand, "check or change access levels");
|
||||
registerCommand("instance", 30, instanceCommand, "print or change your current instance");
|
||||
registerCommand("mss", 30, mssCommand, "edit Monkey Skyway routes");
|
||||
registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs");
|
||||
|
@@ -325,6 +325,13 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
|
||||
std::string logEmail = "[Email] " + PlayerManager::getPlayerName(plr, true) + " (to " + PlayerManager::getPlayerName(&otherPlr, true) + "): <" + email.SubjectLine + ">\n" + email.MsgBody;
|
||||
std::cout << logEmail << std::endl;
|
||||
dump.push_back(logEmail);
|
||||
|
||||
// notification to recipient if online
|
||||
CNSocket* recipient = PlayerManager::getSockFromID(pkt->iTo_PCUID);
|
||||
if (recipient != nullptr)
|
||||
{
|
||||
emailUpdateCheck(recipient, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void Email::init() {
|
||||
|
@@ -16,8 +16,9 @@ EntityRef::EntityRef(CNSocket *s) {
|
||||
EntityRef::EntityRef(int32_t i) {
|
||||
id = i;
|
||||
|
||||
assert(NPCManager::NPCs.find(id) != NPCManager::NPCs.end());
|
||||
kind = NPCManager::NPCs[id]->kind;
|
||||
kind = EntityKind::INVALID;
|
||||
if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end())
|
||||
kind = NPCManager::NPCs[id]->kind;
|
||||
}
|
||||
|
||||
bool EntityRef::isValid() const {
|
||||
|
@@ -64,6 +64,9 @@ static void attachGroupData(std::vector<EntityRef>& pcs, std::vector<EntityRef>&
|
||||
}
|
||||
|
||||
void Groups::addToGroup(Group* group, EntityRef member) {
|
||||
if (group == nullptr)
|
||||
return;
|
||||
|
||||
if (member.kind == EntityKind::PLAYER) {
|
||||
Player* plr = PlayerManager::getPlayer(member.sock);
|
||||
plr->group = group;
|
||||
@@ -109,6 +112,9 @@ void Groups::addToGroup(Group* group, EntityRef member) {
|
||||
}
|
||||
|
||||
bool Groups::removeFromGroup(Group* group, EntityRef member) {
|
||||
if (group == nullptr)
|
||||
return false;
|
||||
|
||||
if (member.kind == EntityKind::PLAYER) {
|
||||
Player* plr = PlayerManager::getPlayer(member.sock);
|
||||
plr->group = nullptr; // no dangling pointers here muahaahahah
|
||||
@@ -168,6 +174,9 @@ bool Groups::removeFromGroup(Group* group, EntityRef member) {
|
||||
}
|
||||
|
||||
void Groups::disbandGroup(Group* group) {
|
||||
if (group == nullptr)
|
||||
return;
|
||||
|
||||
// remove everyone from the group!!
|
||||
bool done = false;
|
||||
while(!done) {
|
||||
@@ -252,6 +261,9 @@ static void leaveGroup(CNSocket* sock, CNPacketData* data) {
|
||||
}
|
||||
|
||||
void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) {
|
||||
if (group == nullptr)
|
||||
return;
|
||||
|
||||
auto players = group->filter(EntityKind::PLAYER);
|
||||
for (EntityRef ref : players) {
|
||||
ref.sock->sendPacket(buf, type, size);
|
||||
@@ -259,6 +271,9 @@ void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) {
|
||||
}
|
||||
|
||||
void Groups::sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size) {
|
||||
if (group == nullptr)
|
||||
return;
|
||||
|
||||
auto players = group->filter(EntityKind::PLAYER);
|
||||
for (EntityRef ref : players) {
|
||||
if(ref != excluded) ref.sock->sendPacket(buf, type, size);
|
||||
@@ -294,6 +309,9 @@ void Groups::groupTickInfo(CNSocket* sock) {
|
||||
|
||||
void Groups::groupKick(Group* group, EntityRef ref) {
|
||||
|
||||
if (group == nullptr)
|
||||
return;
|
||||
|
||||
// if you are the group leader, destroy your own group and kick everybody
|
||||
if (group->members[0] == ref) {
|
||||
disbandGroup(group);
|
||||
|
@@ -416,6 +416,9 @@ static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (itemdel->iSlotNum < 0 || itemdel->iSlotNum >= AINVEN_COUNT)
|
||||
return; // sanity check
|
||||
|
||||
resp.eIL = itemdel->eIL;
|
||||
resp.iSlotNum = itemdel->iSlotNum;
|
||||
|
||||
|
@@ -386,6 +386,9 @@ static void taskStart(CNSocket* sock, CNPacketData* data) {
|
||||
static void taskEnd(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TASK_END* missionData = (sP_CL2FE_REQ_PC_TASK_END*)data->buf;
|
||||
|
||||
if (Missions::Tasks.find(missionData->iTaskNum) == Missions::Tasks.end())
|
||||
return;
|
||||
|
||||
TaskData* task = Missions::Tasks[missionData->iTaskNum];
|
||||
|
||||
// handle timed mission failure
|
||||
|
@@ -94,20 +94,49 @@ void NPCManager::sendToViewable(Entity *npc, void *buf, uint32_t type, size_t si
|
||||
static void npcBarkHandler(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf;
|
||||
|
||||
// get bark IDs from task data
|
||||
TaskData* td = Missions::Tasks[req->iMissionTaskID];
|
||||
std::vector<int> barks;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (td->task["m_iHBarkerTextID"][i] != 0) // non-zeroes only
|
||||
barks.push_back(td->task["m_iHBarkerTextID"][i]);
|
||||
int taskID = req->iMissionTaskID;
|
||||
// ignore req->iNPC_ID as it is often fixated on a single npc in the region
|
||||
|
||||
if (Missions::Tasks.find(taskID) == Missions::Tasks.end()) {
|
||||
std::cout << "mission task not found: " << taskID << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (barks.empty())
|
||||
return; // no barks
|
||||
TaskData* td = Missions::Tasks[taskID];
|
||||
auto& barks = td->task["m_iHBarkerTextID"];
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
std::vector<std::pair<int32_t, int32_t>> npcLines;
|
||||
|
||||
for (Chunk* chunk : plr->viewableChunks) {
|
||||
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
|
||||
if (ent->kind != EntityKind::SIMPLE_NPC)
|
||||
continue;
|
||||
|
||||
BaseNPC* npc = (BaseNPC*)ent->getEntity();
|
||||
if (npc->type < 0 || npc->type >= NPCData.size())
|
||||
continue; // npc unknown ?!
|
||||
|
||||
int barkType = NPCData[npc->type]["m_iBarkerType"];
|
||||
if (barkType < 1 || barkType > 4)
|
||||
continue; // no barks
|
||||
|
||||
int barkID = barks[barkType - 1];
|
||||
if (barkID == 0)
|
||||
continue; // no barks
|
||||
|
||||
npcLines.push_back(std::make_pair(npc->id, barkID));
|
||||
}
|
||||
}
|
||||
|
||||
if (npcLines.size() == 0)
|
||||
return; // totally no barks
|
||||
|
||||
auto& [npcID, missionStringID] = npcLines[Rand::rand(npcLines.size())];
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_BARKER, resp);
|
||||
resp.iNPC_ID = req->iNPC_ID;
|
||||
resp.iMissionStringID = barks[Rand::rand(barks.size())];
|
||||
resp.iNPC_ID = npcID;
|
||||
resp.iMissionStringID = missionStringID;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_BARKER);
|
||||
}
|
||||
|
||||
|
@@ -110,7 +110,7 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
|
||||
}
|
||||
|
||||
static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
|
||||
if (skill->iNanoID >= NANO_COUNT)
|
||||
if (skill == nullptr || skill->iNanoID >= NANO_COUNT || skill->iNanoID < 0)
|
||||
return;
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
@@ -72,8 +72,8 @@ struct Player : public Entity, public ICombatant {
|
||||
bool notify = false;
|
||||
bool hidden = false;
|
||||
bool unwarpable = false;
|
||||
bool initialLoadDone = false;
|
||||
|
||||
bool buddiesSynced = false;
|
||||
int64_t buddyIDs[50] = {};
|
||||
bool isBuddyBlocked[50] = {};
|
||||
|
||||
|
@@ -77,7 +77,10 @@ void PlayerManager::updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, ui
|
||||
plr->x = X;
|
||||
plr->y = Y;
|
||||
plr->z = Z;
|
||||
plr->instanceID = I;
|
||||
if (plr->instanceID != I) {
|
||||
plr->instanceID = I;
|
||||
plr->recallInstance = INSTANCE_OVERWORLD;
|
||||
}
|
||||
if (oldChunk == newChunk)
|
||||
return; // didn't change chunks
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -170,16 +155,21 @@ void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z) {
|
||||
* Nanos the player hasn't unlocked will (and should) be greyed out. Thus, all nanos should be accounted
|
||||
* for in these packets, even if the player hasn't unlocked them.
|
||||
*/
|
||||
static void sendNanoBookSubset(CNSocket *sock) {
|
||||
static void sendNanoBook(CNSocket *sock, Player *plr, bool resizeOnly) {
|
||||
#ifdef ACADEMY
|
||||
Player *plr = getPlayer(sock);
|
||||
|
||||
int16_t id = 0;
|
||||
INITSTRUCT(sP_FE2CL_REP_NANO_BOOK_SUBSET, pkt);
|
||||
|
||||
pkt.PCUID = plr->iID;
|
||||
pkt.bookSize = NANO_COUNT;
|
||||
|
||||
if (resizeOnly) {
|
||||
// triggers nano array resizing without
|
||||
// actually sending nanos
|
||||
sock->sendPacket(pkt, P_FE2CL_REP_NANO_BOOK_SUBSET);
|
||||
return;
|
||||
}
|
||||
|
||||
while (id < NANO_COUNT) {
|
||||
pkt.elementOffset = id;
|
||||
|
||||
@@ -227,6 +217,7 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
|
||||
response.iID = plr->iID;
|
||||
response.uiSvrTime = getTime();
|
||||
|
||||
response.PCLoadData2CL.iUserLevel = plr->accountLevel;
|
||||
response.PCLoadData2CL.iHP = plr->HP;
|
||||
response.PCLoadData2CL.iLevel = plr->level;
|
||||
@@ -309,27 +300,21 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
sock->setFEKey(lm->FEKey);
|
||||
sock->setActiveKey(SOCKETKEY_FE); // send all packets using the FE key from now on
|
||||
|
||||
// Academy builds receive nanos in a separate packet. An initial one with the size of the
|
||||
// nano book needs to be sent before PC_ENTER_SUCC so the client can resize its nano arrays,
|
||||
// and then proper packets with the nanos included must be sent after, while the game is loading.
|
||||
sendNanoBook(sock, plr, true);
|
||||
|
||||
sock->sendPacket(response, P_FE2CL_REP_PC_ENTER_SUCC);
|
||||
|
||||
// transmit MOTD after entering the game, so the client hopefully changes modes on time
|
||||
Chat::sendServerMessage(sock, settings::MOTDSTRING);
|
||||
sendNanoBook(sock, plr, false);
|
||||
|
||||
// transfer ownership of Player object into the shard (still valid in this function though)
|
||||
addPlayer(sock, plr);
|
||||
|
||||
// check if there is an expiring vehicle
|
||||
Items::checkItemExpire(sock, plr);
|
||||
|
||||
// set player equip stats
|
||||
Items::setItemStats(plr);
|
||||
|
||||
Missions::failInstancedMissions(sock);
|
||||
|
||||
sendNanoBookSubset(sock);
|
||||
|
||||
// initial buddy sync
|
||||
Buddies::refreshBuddyList(sock);
|
||||
|
||||
for (auto& pair : players)
|
||||
if (pair.second->notify)
|
||||
Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(plr) + " has joined.");
|
||||
@@ -374,6 +359,35 @@ 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);
|
||||
}
|
||||
|
||||
if (!plr->initialLoadDone) {
|
||||
// these should be called only once, but not until after
|
||||
// first load-in or else the client may ignore the packets
|
||||
Chat::sendServerMessage(sock, settings::MOTDSTRING); // MOTD
|
||||
Missions::failInstancedMissions(sock); // auto-fail missions
|
||||
Buddies::sendBuddyList(sock); // buddy list
|
||||
Items::checkItemExpire(sock, plr); // vehicle expiration
|
||||
|
||||
plr->initialLoadDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
@@ -577,7 +591,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
|
||||
|
@@ -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,35 +99,43 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
|
||||
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
|
||||
return; // IZ not found
|
||||
|
||||
uint64_t now = getTime() / 1000;
|
||||
EPInfo& epInfo = EPData[mapNum];
|
||||
EPRace& epRace = EPRaces[sock];
|
||||
|
||||
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;
|
||||
uint64_t now = getTime() / 1000;
|
||||
int timeDiff = now - epRace.startTime;
|
||||
int podsCollected = epRace.collectedRings.size();
|
||||
|
||||
int score = std::exp(
|
||||
(epInfo.podFactor * podsCollected) / epInfo.maxPods
|
||||
- (epInfo.timeFactor * timeDiff) / epInfo.maxTime
|
||||
+ epInfo.scaleFactor);
|
||||
score = (settings::IZRACESCORECAPPED && score > epInfo.maxScore) ? epInfo.maxScore : score;
|
||||
int fm = (1.0 + std::exp(epInfo.scaleFactor - 1.0) * epInfo.podFactor * podsCollected) / epInfo.maxPods;
|
||||
|
||||
// we submit the ranking first...
|
||||
Database::RaceRanking postRanking = {};
|
||||
postRanking.EPID = EPData[mapNum].EPID;
|
||||
postRanking.EPID = epInfo.EPID;
|
||||
postRanking.PlayerID = plr->iID;
|
||||
postRanking.RingCount = EPRaces[sock].collectedRings.size();
|
||||
postRanking.RingCount = podsCollected;
|
||||
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(EPData[mapNum].EPID, plr->iID);
|
||||
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(epInfo.EPID, plr->iID);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
|
||||
|
||||
// get rank scores and rewards
|
||||
std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first;
|
||||
std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second;
|
||||
std::vector<int>* rankScores = &EPRewards[epInfo.EPID].first;
|
||||
std::vector<int>* rankRewards = &EPRewards[epInfo.EPID].second;
|
||||
|
||||
// top ranking
|
||||
int maxRank = rankScores->size() - 1;
|
||||
int topRank = 0;
|
||||
while (rankScores->at(topRank) > topRankingPlayer.Score)
|
||||
while (topRank < maxRank && rankScores->at(topRank) > topRankingPlayer.Score)
|
||||
topRank++;
|
||||
|
||||
resp.iEPTopRank = topRank + 1;
|
||||
@@ -137,7 +145,7 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
|
||||
|
||||
// this ranking
|
||||
int rank = 0;
|
||||
while (rankScores->at(rank) > postRanking.Score)
|
||||
while (rank < maxRank && rankScores->at(rank) > postRanking.Score)
|
||||
rank++;
|
||||
|
||||
resp.iEPRank = rank + 1;
|
||||
|
@@ -7,7 +7,11 @@
|
||||
#include <set>
|
||||
|
||||
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 {
|
||||
|
@@ -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,8 +584,17 @@ static void loadDrops(json& dropData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// time limit isn't stored in the XDT, so we include it in the reward table instead
|
||||
Racing::EPData[EPMap].maxTime = race["TimeLimit"];
|
||||
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"];
|
||||
|
||||
// score cutoffs
|
||||
std::vector<int> rankScores;
|
||||
@@ -601,7 +610,7 @@ static void loadDrops(json& dropData) {
|
||||
|
||||
if (rankScores.size() != 5 || rankScores.size() != rankRewards.size()) {
|
||||
char buff[255];
|
||||
sprintf(buff, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID);
|
||||
snprintf(buff, 255, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID);
|
||||
throw TableException(std::string(buff));
|
||||
}
|
||||
|
||||
@@ -686,7 +695,7 @@ static void loadEggs(json& eggData, int32_t* nextId) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* Load gruntwork output, if it exists
|
||||
*/
|
||||
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
|
||||
@@ -1361,7 +1370,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;
|
||||
|
@@ -186,6 +186,36 @@ static void tradeOfferRefusal(CNSocket* sock, CNPacketData* data) {
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
|
||||
}
|
||||
|
||||
static void tradeOfferCancel(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL));
|
||||
}
|
||||
|
||||
static void tradeOfferAbort(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT));
|
||||
}
|
||||
|
||||
static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf;
|
||||
|
||||
@@ -430,6 +460,8 @@ void Trading::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, tradeOffer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT, tradeOfferAccept);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, tradeOfferRefusal);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL, tradeOfferCancel);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ABORT, tradeOfferAbort);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM, tradeConfirm);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem);
|
||||
|
@@ -50,13 +50,15 @@ time_t getTime();
|
||||
time_t getTimestamp();
|
||||
void terminate(int);
|
||||
|
||||
// The PROTOCOL_VERSION definition is defined by the build system.
|
||||
// The PROTOCOL_VERSION definition can be defined by the build system.
|
||||
#if !defined(PROTOCOL_VERSION)
|
||||
#define PROTOCOL_VERSION 104
|
||||
#endif
|
||||
|
||||
#if PROTOCOL_VERSION == 104
|
||||
#include "structs/0104.hpp"
|
||||
#elif PROTOCOL_VERSION == 728
|
||||
#include "structs/0728.hpp"
|
||||
#elif PROTOCOL_VERSION == 104
|
||||
#include "structs/0104.hpp"
|
||||
#elif PROTOCOL_VERSION == 1013
|
||||
#include "structs/1013.hpp"
|
||||
#else
|
||||
|
@@ -46,9 +46,13 @@ namespace Database {
|
||||
void close();
|
||||
|
||||
void findAccount(Account* account, std::string login);
|
||||
// returns ID, 0 if something failed
|
||||
|
||||
// return ID, 0 if something failed
|
||||
int getAccountIdForPlayer(int playerId);
|
||||
int addAccount(std::string login, std::string password);
|
||||
|
||||
void updateAccountLevel(int accountId, int accountLevel);
|
||||
|
||||
// interface for the /ban command
|
||||
bool banPlayer(int playerId, std::string& reason);
|
||||
bool unbanPlayer(int playerId);
|
||||
|
@@ -226,10 +226,11 @@ static void createTables() {
|
||||
static int getTableSize(std::string tableName) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit); // XXX
|
||||
|
||||
const char* sql = "SELECT COUNT(*) FROM ?";
|
||||
// you aren't allowed to bind the table name
|
||||
const char* sql = "SELECT COUNT(*) FROM ";
|
||||
tableName.insert(0, sql);
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_text(stmt, 1, tableName.c_str(), -1, NULL);
|
||||
sqlite3_prepare_v2(db, tableName.c_str(), -1, &stmt, NULL);
|
||||
sqlite3_step(stmt);
|
||||
int result = sqlite3_column_int(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
@@ -266,17 +267,17 @@ void Database::open() {
|
||||
checkMetaTable();
|
||||
createTables();
|
||||
|
||||
std::cout << "[INFO] Database in operation ";
|
||||
std::cout << "[INFO] Database in operation";
|
||||
int accounts = getTableSize("Accounts");
|
||||
int players = getTableSize("Players");
|
||||
std::string message = "";
|
||||
if (accounts > 0) {
|
||||
message += ": Found " + std::to_string(accounts) + " Account";
|
||||
message += ": Found " + std::to_string(accounts) + " account";
|
||||
if (accounts > 1)
|
||||
message += "s";
|
||||
}
|
||||
if (players > 0) {
|
||||
message += " and " + std::to_string(players) + " Player Character";
|
||||
message += " and " + std::to_string(players) + " player";
|
||||
if (players > 1)
|
||||
message += "s";
|
||||
}
|
||||
|
@@ -27,6 +27,32 @@ void Database::findAccount(Account* account, std::string login) {
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
int Database::getAccountIdForPlayer(int playerId) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT AccountID
|
||||
FROM Players
|
||||
WHERE PlayerID = ?
|
||||
LIMIT 1;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, playerId);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_ROW) {
|
||||
std::cout << "[WARN] Database: couldn't get account id for player " << playerId << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int accountId = sqlite3_column_int(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return accountId;
|
||||
}
|
||||
|
||||
int Database::addAccount(std::string login, std::string password) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
@@ -52,6 +78,26 @@ int Database::addAccount(std::string login, std::string password) {
|
||||
return sqlite3_last_insert_rowid(db);
|
||||
}
|
||||
|
||||
void Database::updateAccountLevel(int accountId, int accountLevel) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
UPDATE Accounts SET
|
||||
AccountLevel = ?
|
||||
WHERE AccountID = ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, accountLevel);
|
||||
sqlite3_bind_int(stmt, 2, accountId);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE)
|
||||
std::cout << "[WARN] Database fail on updateAccountLevel(): " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void Database::updateSelected(int accountId, int slot) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
|
@@ -208,6 +208,8 @@ void Database::addBlock(int playerId, int blockedPlayerId) {
|
||||
}
|
||||
|
||||
void Database::removeBlock(int playerId, int blockedPlayerId) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
DELETE FROM Blocks
|
||||
WHERE PlayerID = ? AND BlockedPlayerID = ?;
|
||||
|
@@ -64,7 +64,7 @@ void terminate(int arg) {
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static BOOL winTerminate(DWORD arg) {
|
||||
static BOOL WINAPI winTerminate(DWORD arg) {
|
||||
terminate(0);
|
||||
return FALSE;
|
||||
}
|
||||
|
@@ -67,6 +67,9 @@ int settings::MONITORINTERVAL = 5000;
|
||||
// event mode settings
|
||||
int settings::EVENTMODE = 0;
|
||||
|
||||
// race settings
|
||||
bool settings::IZRACESCORECAPPED = true;
|
||||
|
||||
void settings::init() {
|
||||
INIReader reader("config.ini");
|
||||
|
||||
@@ -111,6 +114,7 @@ void settings::init() {
|
||||
EVENTMODE = reader.GetInteger("shard", "eventmode", EVENTMODE);
|
||||
DISABLEFIRSTUSEFLAG = reader.GetBoolean("shard", "disablefirstuseflag", DISABLEFIRSTUSEFLAG);
|
||||
ANTICHEAT = reader.GetBoolean("shard", "anticheat", ANTICHEAT);
|
||||
IZRACESCORECAPPED = reader.GetBoolean("shard", "izracescorecapped", IZRACESCORECAPPED);
|
||||
MONITORENABLED = reader.GetBoolean("monitor", "enabled", MONITORENABLED);
|
||||
MONITORPORT = reader.GetInteger("monitor", "port", MONITORPORT);
|
||||
MONITORINTERVAL = reader.GetInteger("monitor", "interval", MONITORINTERVAL);
|
||||
|
@@ -38,6 +38,7 @@ namespace settings {
|
||||
extern int MONITORPORT;
|
||||
extern int MONITORINTERVAL;
|
||||
extern bool DISABLEFIRSTUSEFLAG;
|
||||
extern bool IZRACESCORECAPPED;
|
||||
|
||||
void init();
|
||||
}
|
||||
|
2
tdata
2
tdata
Submodule tdata updated: cc65dbb402...8c98c83682
14838
vendor/JSON.hpp
vendored
14838
vendor/JSON.hpp
vendored
File diff suppressed because it is too large
Load Diff
8
vendor/bcrypt/bcrypt.c
vendored
8
vendor/bcrypt/bcrypt.c
vendored
@@ -22,9 +22,13 @@
|
||||
#endif
|
||||
#include <errno.h>
|
||||
|
||||
#ifdef _WIN32 || _WIN64
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
// On windows we need to generate random bytes differently.
|
||||
#if defined(_WIN32) && !defined(_WIN64)
|
||||
typedef __int32 ssize_t;
|
||||
#elif defined(_WIN32) && defined(_WIN64)
|
||||
typedef __int64 ssize_t;
|
||||
#endif
|
||||
#define BCRYPT_HASHSIZE 60
|
||||
|
||||
#include "bcrypt.h"
|
||||
@@ -117,7 +121,7 @@ int bcrypt_gensalt(int factor, char salt[BCRYPT_HASHSIZE])
|
||||
char *aux;
|
||||
|
||||
// Note: Windows does not have /dev/urandom sadly.
|
||||
#ifdef _WIN32 || _WIN64
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
HCRYPTPROV p;
|
||||
ULONG i;
|
||||
|
||||
|
2
vendor/bcrypt/crypt_blowfish.c
vendored
2
vendor/bcrypt/crypt_blowfish.c
vendored
@@ -51,7 +51,7 @@
|
||||
#endif
|
||||
|
||||
/* Just to make sure the prototypes match the actual definitions */
|
||||
#ifdef _WIN32 || _WIN64
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#include "crypt_blowfish.h"
|
||||
#else
|
||||
#include "crypt_blowfish.h"
|
||||
|
4
vendor/bcrypt/wrapper.c
vendored
4
vendor/bcrypt/wrapper.c
vendored
@@ -41,7 +41,7 @@
|
||||
#define __SKIP_GNU
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32 | _WIN64
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#include "ow-crypt.h"
|
||||
|
||||
#include "crypt_blowfish.h"
|
||||
@@ -251,7 +251,7 @@ char *__crypt_gensalt_ra(const char *prefix, unsigned long count,
|
||||
input, size, output, sizeof(output));
|
||||
|
||||
if (retval) {
|
||||
#ifdef _WIN32 | _WIN64
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
retval = _strdup(retval);
|
||||
#else
|
||||
retval = strdup(retval);
|
||||
|
Reference in New Issue
Block a user