mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2025-09-26 01:50:23 +00:00
Compare commits
76 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0078be8e9a | ||
b617456aa1
|
|||
935ee1bf6f
|
|||
43a2504357
|
|||
ca196bf620
|
|||
6b9ae4c325
|
|||
d06c324aa3
|
|||
052196d1cd
|
|||
e84f6505b8
|
|||
b483bf7190
|
|||
6ff51685a8
|
|||
b4ed31d4fb
|
|||
36e0667ed2 | |||
ed9fe61faf
|
|||
55cf3f7102 | |||
1543dac4e0
|
|||
ae327cc104 | |||
6ffde9bb44
|
|||
8568fd1c46 | |||
05a5303522 | |||
3365cb53b7 | |||
5e92a58134 | |||
94064e1865 | |||
5e73ff272d | |||
197ccad0eb | |||
![]() |
68b56e7c25 | ||
![]() |
cada1bcfd8 | ||
![]() |
ca43a2996a | ||
7c66041a6f | |||
2c822e210b | |||
![]() |
352fa8a133 | ||
![]() |
c116794c83 | ||
![]() |
4ebda6066c | ||
6de21277d6
|
|||
![]() |
397700e909 | ||
d9b6aedd5b
|
|||
![]() |
145113062b | ||
d717c5d74d | |||
a6eb0e2349 | |||
52833f7fb3
|
|||
![]() |
3aed24de26 | ||
![]() |
17362b2ea6 | ||
![]() |
47dbc6d35e | ||
![]() |
b780f5ee60 | ||
![]() |
003186d97a | ||
![]() |
6d2f120305 | ||
![]() |
2096c3c3cc | ||
![]() |
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
|
27
.github/workflows/check-builds.yaml
vendored
27
.github/workflows/check-builds.yaml
vendored
@@ -8,17 +8,19 @@ on:
|
||||
- .github/workflows/check-builds.yaml
|
||||
- CMakeLists.txt
|
||||
- Makefile
|
||||
- tdata
|
||||
pull_request:
|
||||
types: ready_for_review
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- src/**
|
||||
- vendor/**
|
||||
- CMakeLists.txt
|
||||
- Makefile
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ubuntu-build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Set environment
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
@@ -28,7 +30,7 @@ jobs:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
run: sudo apt install clang cmake snap -y && sudo snap install powershell --classic
|
||||
run: sudo apt install clang cmake snap libsqlite3-dev -y && sudo snap install powershell --classic
|
||||
- name: Check compilation
|
||||
run: |
|
||||
$versions = "104", "728", "1013"
|
||||
@@ -47,17 +49,18 @@ jobs:
|
||||
Rename-Item -Path "bin/fusion" -newName "$version-fusion"
|
||||
Write-Output "Built version $version"
|
||||
}
|
||||
Copy-Item -Path "tdata" -Destination "bin/tdata" -Recurse
|
||||
Copy-Item -Path "sql" -Destination "bin/sql" -Recurse
|
||||
Copy-Item -Path "config.ini" -Destination "bin"
|
||||
shell: pwsh
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'ubuntu20_04-bin-x64-${{ env.SHORT_SHA }}'
|
||||
name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
|
||||
path: bin
|
||||
|
||||
windows-build:
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- name: Set environment
|
||||
run: $s = $env:GITHUB_SHA.subString(0, 7); echo "SHORT_SHA=$s" >> $env:GITHUB_ENV
|
||||
@@ -72,7 +75,7 @@ jobs:
|
||||
$configurations = "Release"
|
||||
# "Debug" builds are disabled, since we don't really need them
|
||||
|
||||
$vsPath = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise"
|
||||
$vsPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
|
||||
|
||||
Import-Module "$vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
|
||||
Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation
|
||||
@@ -99,19 +102,20 @@ jobs:
|
||||
}
|
||||
Rename-Item -Path "bin/$configuration" -newName "$version-$configuration"
|
||||
Write-Output "Built version $version $configuration"
|
||||
Copy-Item -Path "tdata" -Destination "bin/$version-$configuration/tdata" -Recurse
|
||||
Copy-Item -Path "sql" -Destination "bin/$version-$configuration/sql" -Recurse
|
||||
Copy-Item -Path "config.ini" -Destination "bin/$version-$configuration"
|
||||
}
|
||||
}
|
||||
shell: pwsh
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'windows-vs2019-bin-x64-${{ env.SHORT_SHA }}'
|
||||
name: 'windows-vs2022-bin-x64-${{ env.SHORT_SHA }}'
|
||||
path: bin
|
||||
|
||||
copy-artifacts:
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
|
||||
if: github.event_name != 'pull_request' && (github.ref_type == 'tag' || github.ref_name == 'master')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [windows-build, ubuntu-build]
|
||||
env:
|
||||
@@ -120,13 +124,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- run: |
|
||||
GITDESC=$(git describe --tags)
|
||||
mkdir $GITDESC
|
||||
echo "ARTDIR=$GITDESC" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ env.ARTDIR }}
|
||||
- name: Upload artifacts
|
||||
|
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]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push-docker-image:
|
||||
name: Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
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: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build and push the Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_REPOSITORY }}:${{ github.ref_name }},${{ secrets.DOCKERHUB_REPOSITORY }}:${{ steps.split.outputs._0 }},${{ secrets.DOCKERHUB_REPOSITORY }}:latest
|
42
Dockerfile
42
Dockerfile
@@ -1,21 +1,41 @@
|
||||
FROM debian:latest
|
||||
# build
|
||||
FROM alpine:3 as build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get -y update && apt-get install -y \
|
||||
RUN apk update && apk upgrade && apk add \
|
||||
linux-headers \
|
||||
git \
|
||||
clang \
|
||||
clang18 \
|
||||
make \
|
||||
libsqlite3-dev
|
||||
sqlite-dev
|
||||
|
||||
COPY . ./
|
||||
COPY src ./src
|
||||
COPY vendor ./vendor
|
||||
COPY .git ./.git
|
||||
COPY Makefile CMakeLists.txt version.h.in ./
|
||||
|
||||
RUN make -j8
|
||||
RUN sed -i 's/^CC=clang$/&-18/' Makefile
|
||||
RUN sed -i 's/^CXX=clang++$/&-18/' Makefile
|
||||
|
||||
# tabledata should be copied from the host;
|
||||
# clone it there before building the container
|
||||
#RUN git submodule update --init --recursive
|
||||
RUN make nosandbox -j$(nproc)
|
||||
|
||||
CMD ["./bin/fusion"]
|
||||
# prod
|
||||
FROM alpine:3
|
||||
|
||||
LABEL Name=openfusion Version=0.0.1
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk update && apk upgrade && apk add \
|
||||
libstdc++ \
|
||||
sqlite-dev
|
||||
|
||||
COPY --from=build /usr/src/app/bin/fusion /bin/fusion
|
||||
COPY sql ./sql
|
||||
|
||||
CMD ["/bin/fusion"]
|
||||
|
||||
EXPOSE 23000/tcp
|
||||
EXPOSE 23001/tcp
|
||||
EXPOSE 8003/tcp
|
||||
|
||||
LABEL Name=openfusion Version=2.0.0
|
||||
|
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2023 OpenFusion Contributors
|
||||
Copyright (c) 2020-2025 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
|
||||
|
10
Makefile
10
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\
|
||||
@@ -117,6 +115,7 @@ CXXHDR=\
|
||||
src/settings.hpp\
|
||||
src/Transport.hpp\
|
||||
src/TableData.hpp\
|
||||
src/Bucket.hpp\
|
||||
src/Chunking.hpp\
|
||||
src/Buddies.hpp\
|
||||
src/Groups.hpp\
|
||||
@@ -135,6 +134,8 @@ HDR=$(CHDR) $(CXXHDR)
|
||||
all: $(SERVER)
|
||||
|
||||
windows: $(SERVER)
|
||||
nosandbox: $(SERVER)
|
||||
nolandlock: $(SERVER)
|
||||
|
||||
# assign Windows-specific values if targeting Windows
|
||||
windows : CC=$(WIN_CC)
|
||||
@@ -144,6 +145,9 @@ windows : CXXFLAGS=$(WIN_CXXFLAGS)
|
||||
windows : LDFLAGS=$(WIN_LDFLAGS)
|
||||
windows : SERVER=$(WIN_SERVER)
|
||||
|
||||
nosandbox : CFLAGS+=-DCONFIG_NOSANDBOX=1
|
||||
nolandlock : CFLAGS+=-DCONFIG_NOLANDLOCK=1
|
||||
|
||||
.SUFFIXES: .o .c .cpp .h .hpp
|
||||
|
||||
.c.o:
|
||||
@@ -165,7 +169,7 @@ version.h:
|
||||
|
||||
src/main.o: version.h
|
||||
|
||||
.PHONY: all windows clean nuke
|
||||
.PHONY: all windows nosandbox nolandlock clean nuke
|
||||
|
||||
# only gets rid of OpenFusion objects, so we don't need to
|
||||
# recompile the libs every time
|
||||
|
56
README.md
56
README.md
@@ -1,35 +1,35 @@
|
||||
<p align="center"><img width="640" src="res/openfusion-hero.png" alt=""></p>
|
||||
<p align="center"><img width="640" src="res/openfusion-hero.png" alt="OpenFusion Logo"></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/OpenFusionProject/OpenFusion/releases/latest"><img src="https://img.shields.io/github/v/release/OpenFusionProject/OpenFusion" alt="Current Release"></a>
|
||||
<a href="https://github.com/OpenFusionProject/OpenFusion/actions/workflows/check-builds.yaml"><img src="https://github.com/OpenFusionProject/OpenFusion/actions/workflows/check-builds.yaml/badge.svg" alt="Workflow"></a>
|
||||
<a href="https://hub.docker.com/repository/docker/openfusion/openfusion/"><img src="https://badgen.net/docker/pulls/openfusion/openfusion?icon=docker&label=pulls"></a>
|
||||
<a href="https://discord.gg/DYavckB"><img src="https://img.shields.io/badge/chat-on%20discord-7289da.svg?logo=discord" alt="Discord"></a>
|
||||
<a href="https://github.com/OpenFusionProject/OpenFusion/blob/master/LICENSE.md"><img src="https://img.shields.io/github/license/OpenFusionProject/OpenFusion" alt="License"></a>
|
||||
</p>
|
||||
|
||||
OpenFusion is a reverse-engineered server for FusionFall. It primarily targets versions `beta-20100104` and `beta-20111013` of the original game, with [limited support](https://github.com/OpenFusionProject/OpenFusion/wiki/FusionFall-Version-Support) for others.
|
||||
OpenFusion is a reverse-engineered server for FusionFall. It primarily targets versions `beta-20100104` and `beta-20111013` of the original game, with [limited support](https://openfusion.dev/docs/reference/fusionfall-version-support/) for others.
|
||||
|
||||
## Usage
|
||||
|
||||
### 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.
|
||||
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.
|
||||
1. Download the launcher installer by clicking [here](https://github.com/OpenFusionProject/OpenFusionLauncher/releases/latest/download/OpenFusionLauncher-Windows-Installer.exe) - choose to run the file.
|
||||
2. After a few moments, the launcher 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 launcher from [here](https://github.com/OpenFusionProject/OpenFusionLauncher/releases/latest/download/OpenFusionLauncher-Windows-Portable.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.
|
||||
3. Run OpenFusionLauncher.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.
|
||||
5. 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 4.
|
||||
|
||||
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://openfusion.dev/docs/guides/running-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-Windows-Original.zip` or `OpenFusionServer-Windows-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/latest).
|
||||
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 +54,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 +63,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.
|
||||
|
||||
@@ -83,17 +80,17 @@ This just works if you're all under the same LAN, but if you want to play over t
|
||||
|
||||
## Compiling
|
||||
|
||||
OpenFusion has one external dependency: SQLite. The oldest compatible version is `3.33.0`. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [read this guide on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/Installing-SQLite-on-Windows-using-vcpkg).
|
||||
OpenFusion has one external dependency: SQLite. The oldest compatible version is `3.33.0`. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [read this guide](https://openfusion.dev/docs/development/installing-sqlite-on-windows-using-vcpkg/).
|
||||
|
||||
You have two choices for compiling OpenFusion: the included Makefile and the included CMakeLists file.
|
||||
|
||||
### Makefile
|
||||
|
||||
A detailed compilation guide is available for Windows users in the wiki [using MinGW-w64 and MSYS2](https://github.com/OpenFusionProject/OpenFusion/wiki/Compilation-on-Windows). Otherwise, to compile it for the current platform you're on, just run `make` with the correct build tools installed (currently make and clang).
|
||||
A detailed compilation guide is available for Windows users on the website [using MinGW-w64 and MSYS2](https://openfusion.dev/docs/development/compilation-on-windows-msys2-mingw/). Otherwise, to compile it for the current platform you're on, just run `make` with the correct build tools installed (currently make and clang).
|
||||
|
||||
### CMake
|
||||
|
||||
A detailed guide is available [on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/Compilation-with-CMake-or-Visual-Studio) for people using regular old CMake or the version of CMake that comes with Visual Studio. tl;dr: `cmake -B build`
|
||||
A detailed guide is available [in our documentation](https://openfusion.dev/docs/development/compilation-with-cmake-or-visual-studio/) for people using regular old CMake or the version of CMake that comes with Visual Studio. TL;DR: `cmake -B build`
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -102,26 +99,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 page](https://openfusion.dev/docs/reference/ingame-command-list/).
|
||||
|
12
config.ini
12
config.ini
@@ -12,11 +12,17 @@ sandbox=true
|
||||
[login]
|
||||
# must be kept in sync with loginInfo.php
|
||||
port=23000
|
||||
# will all name wheel names be approved instantly?
|
||||
acceptallwheelnames=true
|
||||
# will all custom names be approved instantly?
|
||||
acceptallcustomnames=true
|
||||
# should attempts to log into non-existent accounts
|
||||
# automatically create them?
|
||||
autocreateaccounts=true
|
||||
# list of supported authentication methods (comma-separated)
|
||||
# password = allow logging in with plaintext passwords
|
||||
# cookie = allow logging in with one-shot auth cookies
|
||||
authmethods=password
|
||||
# how often should everything be flushed to the database?
|
||||
# the default is 4 minutes
|
||||
dbsaveinterval=240
|
||||
@@ -66,6 +72,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
|
||||
|
||||
@@ -95,5 +104,8 @@ eventmode=0
|
||||
enabled=false
|
||||
# the port to listen for connections on
|
||||
port=8003
|
||||
# The local IP to listen on.
|
||||
# Do not change this unless you know what you're doing.
|
||||
listenip=127.0.0.1
|
||||
# how often the listeners should be updated (in milliseconds)
|
||||
interval=5000
|
||||
|
@@ -1,11 +1,13 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
openfusion:
|
||||
image: openfusion
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
image: openfusion/openfusion:latest
|
||||
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"
|
||||
|
19
sql/migration4.sql
Normal file
19
sql/migration4.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
It is recommended in the SQLite manual to turn off
|
||||
foreign keys when making schema changes that involve them
|
||||
*/
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN TRANSACTION;
|
||||
-- New table to store auth cookies
|
||||
CREATE TABLE Auth (
|
||||
AccountID INTEGER NOT NULL,
|
||||
Cookie TEXT NOT NULL,
|
||||
Expires INTEGER DEFAULT 0 NOT NULL,
|
||||
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
|
||||
UNIQUE (AccountID)
|
||||
);
|
||||
-- Update DB Version
|
||||
UPDATE Meta SET Value = 5 WHERE Key = 'DatabaseVersion';
|
||||
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
8
sql/migration5.sql
Normal file
8
sql/migration5.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
BEGIN TRANSACTION;
|
||||
-- New Columns
|
||||
ALTER TABLE Accounts ADD Email TEXT DEFAULT '' NOT NULL;
|
||||
ALTER TABLE Accounts ADD LastPasswordReset INTEGER DEFAULT 0 NOT NULL;
|
||||
-- Update DB Version
|
||||
UPDATE Meta SET Value = 6 WHERE Key = 'DatabaseVersion';
|
||||
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
|
||||
COMMIT;
|
@@ -1,14 +1,16 @@
|
||||
CREATE TABLE IF NOT EXISTS Accounts (
|
||||
AccountID INTEGER NOT NULL,
|
||||
Login TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
Password TEXT NOT NULL,
|
||||
Selected INTEGER DEFAULT 1 NOT NULL,
|
||||
AccountLevel INTEGER NOT NULL,
|
||||
Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
|
||||
LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
|
||||
BannedUntil INTEGER DEFAULT 0 NOT NULL,
|
||||
BannedSince INTEGER DEFAULT 0 NOT NULL,
|
||||
BanReason TEXT DEFAULT '' NOT NULL,
|
||||
AccountID INTEGER NOT NULL,
|
||||
Login TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
Password TEXT NOT NULL,
|
||||
Selected INTEGER DEFAULT 1 NOT NULL,
|
||||
AccountLevel INTEGER NOT NULL,
|
||||
Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
|
||||
LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
|
||||
BannedUntil INTEGER DEFAULT 0 NOT NULL,
|
||||
BannedSince INTEGER DEFAULT 0 NOT NULL,
|
||||
BanReason TEXT DEFAULT '' NOT NULL,
|
||||
Email TEXT DEFAULT '' NOT NULL,
|
||||
LastPasswordReset INTEGER DEFAULT 0 NOT NULL,
|
||||
PRIMARY KEY(AccountID AUTOINCREMENT)
|
||||
);
|
||||
|
||||
@@ -143,7 +145,7 @@ CREATE TABLE IF NOT EXISTS EmailItems (
|
||||
UNIQUE (PlayerID, MsgIndex, Slot)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS RaceResults(
|
||||
CREATE TABLE IF NOT EXISTS RaceResults (
|
||||
EPID INTEGER NOT NULL,
|
||||
PlayerID INTEGER NOT NULL,
|
||||
Score INTEGER NOT NULL,
|
||||
@@ -153,9 +155,17 @@ CREATE TABLE IF NOT EXISTS RaceResults(
|
||||
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS RedeemedCodes(
|
||||
CREATE TABLE IF NOT EXISTS RedeemedCodes (
|
||||
PlayerID INTEGER NOT NULL,
|
||||
Code TEXT NOT NULL,
|
||||
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
|
||||
UNIQUE (PlayerID, Code)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Auth (
|
||||
AccountID INTEGER NOT NULL,
|
||||
Cookie TEXT NOT NULL,
|
||||
Expires INTEGER DEFAULT 0 NOT NULL,
|
||||
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
|
||||
UNIQUE (AccountID)
|
||||
);
|
||||
|
@@ -334,8 +334,8 @@ void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std:
|
||||
size_t resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC);
|
||||
for(SkillResult& sr : results)
|
||||
resplen += sr.size;
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
sP_FE2CL_NANO_SKILL_USE_SUCC* pkt = (sP_FE2CL_NANO_SKILL_USE_SUCC*)respbuf;
|
||||
pkt->iPC_ID = plr->iID;
|
||||
@@ -368,7 +368,6 @@ void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*>
|
||||
SkillData* skill = &SkillTable[skillID];
|
||||
|
||||
std::vector<SkillResult> results = handleSkill(skill, 0, src, affected);
|
||||
if(results.empty()) return; // no effect; no need for confirmation packets
|
||||
|
||||
// lazy validation since skill results might be different sizes
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), MAX_SKILLRESULT_SIZE)) {
|
||||
@@ -380,8 +379,8 @@ void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*>
|
||||
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT);
|
||||
for(SkillResult& sr : results)
|
||||
resplen += sr.size;
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
|
||||
pkt->iNPC_ID = npc.id;
|
||||
|
40
src/Bucket.hpp
Normal file
40
src/Bucket.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
template<class T, size_t N>
|
||||
class Bucket {
|
||||
std::array<T, N> buf;
|
||||
size_t sz;
|
||||
public:
|
||||
Bucket() {
|
||||
sz = 0;
|
||||
}
|
||||
|
||||
void add(const T& item) {
|
||||
assert(sz < N);
|
||||
buf[sz++] = item;
|
||||
}
|
||||
|
||||
std::optional<T> get(size_t idx) const {
|
||||
if (idx < sz) {
|
||||
return buf[idx];
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t size() const {
|
||||
return sz;
|
||||
}
|
||||
|
||||
bool isFull() const {
|
||||
return sz == N;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
sz = 0;
|
||||
}
|
||||
};
|
@@ -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);
|
||||
|
||||
@@ -41,9 +41,9 @@ void Buddies::refreshBuddyList(CNSocket* sock) {
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC) + buddyCnt * sizeof(sBuddyBaseInfo);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC* resp = (sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC*)respbuf;
|
||||
sBuddyBaseInfo* respdata = (sBuddyBaseInfo*)(respbuf + sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC));
|
||||
@@ -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);
|
||||
}
|
||||
|
@@ -178,9 +178,9 @@ void Buffs::tickDrain(EntityRef self, Buff* buff, int mult) {
|
||||
int dealt = combatant->takeDamage(buff->getLastSource(), damage);
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
assert(resplen < CN_PACKET_BODY_SIZE);
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
|
||||
pkt->iID = self.id;
|
||||
|
@@ -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);
|
||||
|
32
src/Chat.cpp
32
src/Chat.cpp
@@ -1,6 +1,7 @@
|
||||
#include "Chat.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "servers/Monitor.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
@@ -8,8 +9,6 @@
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
std::vector<std::string> Chat::dump;
|
||||
|
||||
using namespace Chat;
|
||||
|
||||
static void chatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
@@ -28,7 +27,7 @@ static void chatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
std::string logLine = "[FreeChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
Monitor::chats.push_back(logLine);
|
||||
|
||||
// send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC, resp);
|
||||
@@ -51,7 +50,7 @@ static void menuChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
std::string logLine = "[MenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
Monitor::chats.push_back(logLine);
|
||||
|
||||
// send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, resp);
|
||||
@@ -103,14 +102,14 @@ static void announcementHandler(CNSocket* sock, CNPacketData* data) {
|
||||
memcpy(msg.szAnnounceMsg, announcement->szAnnounceMsg, sizeof(msg.szAnnounceMsg));
|
||||
std::map<CNSocket*, Player*>::iterator it;
|
||||
|
||||
// This value is completely arbitrary, but these make the most sense when you consider the architecture of the game
|
||||
switch (announcement->iAreaType) {
|
||||
case 0: // area (all players in viewable chunks)
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
PlayerManager::sendToViewable(sock, msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
break;
|
||||
case 1: // shard
|
||||
case 2: // world
|
||||
break; // not applicable to OpenFusion
|
||||
case 1: // channel
|
||||
case 2: // shard
|
||||
case 3: // global (all players)
|
||||
for (it = PlayerManager::players.begin(); it != PlayerManager::players.end(); it++) {
|
||||
CNSocket* allSock = it->first;
|
||||
@@ -120,9 +119,12 @@ static void announcementHandler(CNSocket* sock, CNPacketData* data) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::string logLine = "[Bcast " + std::to_string(announcement->iAreaType) + "] " + PlayerManager::getPlayerName(plr, false) + ": " + AUTOU16TOU8(msg.szAnnounceMsg);
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back("**" + logLine + "**");
|
||||
std::string logLine = std::to_string(announcement->iAreaType) + " "
|
||||
+ std::to_string(announcement->iAnnounceType) + " "
|
||||
+ std::to_string(announcement->iDuringTime) + " "
|
||||
+ PlayerManager::getPlayerName(plr, false) + ": " + AUTOU16TOU8(msg.szAnnounceMsg);
|
||||
std::cout << "Broadcast " << logLine << std::endl;
|
||||
Monitor::bcasts.push_back(logLine);
|
||||
}
|
||||
|
||||
// Buddy freechatting
|
||||
@@ -155,7 +157,7 @@ static void buddyChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
|
||||
std::string logLine = "[BuddyChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
Monitor::chats.push_back(logLine);
|
||||
|
||||
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
|
||||
|
||||
@@ -185,7 +187,7 @@ static void buddyMenuChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
std::string logLine = "[BuddyMenuChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
Monitor::chats.push_back(logLine);
|
||||
|
||||
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
|
||||
|
||||
@@ -218,7 +220,7 @@ static void tradeChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
std::string logLine = "[TradeChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
Monitor::chats.push_back(logLine);
|
||||
|
||||
resp.iEmoteCode = pacdat->iEmoteCode;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_TRADE_EMOTES_CHAT);
|
||||
@@ -241,7 +243,7 @@ static void groupChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
|
||||
std::string logLine = "[GroupChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
Monitor::chats.push_back(logLine);
|
||||
|
||||
// send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, resp);
|
||||
@@ -264,7 +266,7 @@ static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
std::string logLine = "[GroupMenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
Monitor::chats.push_back(logLine);
|
||||
|
||||
// send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, resp);
|
||||
|
@@ -8,7 +8,6 @@
|
||||
#include <vector>
|
||||
|
||||
namespace Chat {
|
||||
extern std::vector<std::string> dump;
|
||||
void init();
|
||||
|
||||
void sendServerMessage(CNSocket* sock, std::string msg); // uses MOTD
|
||||
|
163
src/Chunking.cpp
163
src/Chunking.cpp
@@ -1,7 +1,9 @@
|
||||
#include "Chunking.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "MobAI.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
#include "Bucket.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
@@ -11,6 +13,12 @@ using namespace Chunking;
|
||||
* The initial chunkPos value before a player is placed into the world.
|
||||
*/
|
||||
const ChunkPos Chunking::INVALID_CHUNK = {};
|
||||
constexpr size_t MAX_PC_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sPCAppearanceData);
|
||||
constexpr size_t MAX_NPC_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sNPCAppearanceData);
|
||||
constexpr size_t MAX_SHINY_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sShinyAppearanceData);
|
||||
constexpr size_t MAX_TRANSPORTATION_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sTransportationAppearanceData);
|
||||
constexpr size_t MAX_IDS_PER_AROUND_DEL = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(int32_t);
|
||||
constexpr size_t MAX_TRANSPORTATION_IDS_PER_AROUND_DEL = MAX_IDS_PER_AROUND_DEL - 1; // 1 less for eTT
|
||||
|
||||
std::map<ChunkPos, Chunk*> Chunking::chunks;
|
||||
|
||||
@@ -75,11 +83,80 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) {
|
||||
deleteChunk(chunkPos);
|
||||
}
|
||||
|
||||
template<class T, size_t N>
|
||||
static void sendAroundPackets(const EntityRef recipient, std::vector<Bucket<T, N>>& buckets, uint32_t packetId) {
|
||||
assert(recipient.kind == EntityKind::PLAYER);
|
||||
|
||||
uint8_t pktBuf[CN_PACKET_BODY_SIZE];
|
||||
for (const auto& bucket : buckets) {
|
||||
memset(pktBuf, 0, CN_PACKET_BODY_SIZE);
|
||||
int count = bucket.size();
|
||||
*((int32_t*)pktBuf) = count;
|
||||
T* data = (T*)(pktBuf + sizeof(int32_t));
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
data[i] = bucket.get(i).value();
|
||||
}
|
||||
recipient.sock->sendPacket(pktBuf, packetId, sizeof(int32_t) + (count * sizeof(T)));
|
||||
}
|
||||
}
|
||||
|
||||
template<size_t N>
|
||||
static void sendAroundDelPackets(const EntityRef recipient, std::vector<Bucket<int32_t, N>>& buckets, uint32_t packetId) {
|
||||
assert(recipient.kind == EntityKind::PLAYER);
|
||||
|
||||
uint8_t pktBuf[CN_PACKET_BODY_SIZE];
|
||||
for (const auto& bucket : buckets) {
|
||||
memset(pktBuf, 0, CN_PACKET_BODY_SIZE);
|
||||
int count = bucket.size();
|
||||
assert(count <= N);
|
||||
|
||||
size_t baseSize;
|
||||
if (packetId == P_FE2CL_AROUND_DEL_TRANSPORTATION) {
|
||||
sP_FE2CL_AROUND_DEL_TRANSPORTATION* pkt = (sP_FE2CL_AROUND_DEL_TRANSPORTATION*)pktBuf;
|
||||
pkt->eTT = 3;
|
||||
pkt->iCnt = count;
|
||||
baseSize = sizeof(sP_FE2CL_AROUND_DEL_TRANSPORTATION);
|
||||
} else {
|
||||
*((int32_t*)pktBuf) = count;
|
||||
baseSize = sizeof(int32_t);
|
||||
}
|
||||
int32_t* ids = (int32_t*)(pktBuf + baseSize);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
ids[i] = bucket.get(i).value();
|
||||
}
|
||||
recipient.sock->sendPacket(pktBuf, packetId, baseSize + (count * sizeof(int32_t)));
|
||||
}
|
||||
}
|
||||
|
||||
template<class T, size_t N>
|
||||
static void bufferAppearanceData(std::vector<Bucket<T, N>>& buckets, const T& data) {
|
||||
if (buckets.empty())
|
||||
buckets.push_back({});
|
||||
auto& bucket = buckets[buckets.size() - 1];
|
||||
bucket.add(data);
|
||||
if (bucket.isFull())
|
||||
buckets.push_back({});
|
||||
}
|
||||
|
||||
template<size_t N>
|
||||
static void bufferIdForDisappearance(std::vector<Bucket<int32_t, N>>& buckets, int32_t id) {
|
||||
if (buckets.empty())
|
||||
buckets.push_back({});
|
||||
auto& bucket = buckets[buckets.size() - 1];
|
||||
bucket.add(id);
|
||||
if (bucket.isFull())
|
||||
buckets.push_back({});
|
||||
}
|
||||
|
||||
void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
|
||||
Entity *ent = ref.getEntity();
|
||||
bool alive = ent->isExtant();
|
||||
|
||||
// TODO: maybe optimize this, potentially using AROUND packets?
|
||||
std::vector<Bucket<sPCAppearanceData, MAX_PC_PER_AROUND>> pcAppearances;
|
||||
std::vector<Bucket<sNPCAppearanceData, MAX_NPC_PER_AROUND>> npcAppearances;
|
||||
std::vector<Bucket<sShinyAppearanceData, MAX_SHINY_PER_AROUND>> shinyAppearances;
|
||||
std::vector<Bucket<sTransportationAppearanceData, MAX_TRANSPORTATION_PER_AROUND>> transportationAppearances;
|
||||
for (Chunk *chunk : chnks) {
|
||||
for (const EntityRef otherRef : chunk->entities) {
|
||||
// skip oneself
|
||||
@@ -95,7 +172,38 @@ void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
|
||||
|
||||
// notify this *player* of the existence of all visible Entities
|
||||
if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
|
||||
other->enterIntoViewOf(ref.sock);
|
||||
sPCAppearanceData pcData;
|
||||
sNPCAppearanceData npcData;
|
||||
sShinyAppearanceData eggData;
|
||||
sTransportationAppearanceData busData;
|
||||
switch(otherRef.kind) {
|
||||
case EntityKind::PLAYER:
|
||||
pcData = dynamic_cast<Player*>(other)->getAppearanceData();
|
||||
bufferAppearanceData(pcAppearances, pcData);
|
||||
break;
|
||||
case EntityKind::SIMPLE_NPC:
|
||||
npcData = dynamic_cast<BaseNPC*>(other)->getAppearanceData();
|
||||
bufferAppearanceData(npcAppearances, npcData);
|
||||
break;
|
||||
case EntityKind::COMBAT_NPC:
|
||||
npcData = dynamic_cast<CombatNPC*>(other)->getAppearanceData();
|
||||
bufferAppearanceData(npcAppearances, npcData);
|
||||
break;
|
||||
case EntityKind::MOB:
|
||||
npcData = dynamic_cast<Mob*>(other)->getAppearanceData();
|
||||
bufferAppearanceData(npcAppearances, npcData);
|
||||
break;
|
||||
case EntityKind::EGG:
|
||||
eggData = dynamic_cast<Egg*>(other)->getShinyAppearanceData();
|
||||
bufferAppearanceData(shinyAppearances, eggData);
|
||||
break;
|
||||
case EntityKind::BUS:
|
||||
busData = dynamic_cast<Bus*>(other)->getTransportationAppearanceData();
|
||||
bufferAppearanceData(transportationAppearances, busData);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// for mobs, increment playersInView
|
||||
@@ -105,13 +213,27 @@ void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
|
||||
((Mob*)other)->playersInView++;
|
||||
}
|
||||
}
|
||||
|
||||
if (ref.kind == EntityKind::PLAYER) {
|
||||
if (!pcAppearances.empty())
|
||||
sendAroundPackets(ref, pcAppearances, P_FE2CL_PC_AROUND);
|
||||
if (!npcAppearances.empty())
|
||||
sendAroundPackets(ref, npcAppearances, P_FE2CL_NPC_AROUND);
|
||||
if (!shinyAppearances.empty())
|
||||
sendAroundPackets(ref, shinyAppearances, P_FE2CL_SHINY_AROUND);
|
||||
if (!transportationAppearances.empty())
|
||||
sendAroundPackets(ref, transportationAppearances, P_FE2CL_TRANSPORTATION_AROUND);
|
||||
}
|
||||
}
|
||||
|
||||
void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref) {
|
||||
Entity *ent = ref.getEntity();
|
||||
bool alive = ent->isExtant();
|
||||
|
||||
// TODO: same as above
|
||||
std::vector<Bucket<int32_t, MAX_IDS_PER_AROUND_DEL>> pcDisappearances;
|
||||
std::vector<Bucket<int32_t, MAX_IDS_PER_AROUND_DEL>> npcDisappearances;
|
||||
std::vector<Bucket<int32_t, MAX_IDS_PER_AROUND_DEL>> shinyDisappearances;
|
||||
std::vector<Bucket<int32_t, MAX_TRANSPORTATION_IDS_PER_AROUND_DEL>> transportationDisappearances;
|
||||
for (Chunk *chunk : chnks) {
|
||||
for (const EntityRef otherRef : chunk->entities) {
|
||||
// skip oneself
|
||||
@@ -127,7 +249,29 @@ void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef re
|
||||
|
||||
// notify this *player* of the departure of all visible Entities
|
||||
if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
|
||||
other->disappearFromViewOf(ref.sock);
|
||||
int32_t id;
|
||||
switch(otherRef.kind) {
|
||||
case EntityKind::PLAYER:
|
||||
id = dynamic_cast<Player*>(other)->iID;
|
||||
bufferIdForDisappearance(pcDisappearances, id);
|
||||
break;
|
||||
case EntityKind::SIMPLE_NPC:
|
||||
case EntityKind::COMBAT_NPC:
|
||||
case EntityKind::MOB:
|
||||
id = dynamic_cast<BaseNPC*>(other)->id;
|
||||
bufferIdForDisappearance(npcDisappearances, id);
|
||||
break;
|
||||
case EntityKind::EGG:
|
||||
id = dynamic_cast<Egg*>(other)->id;
|
||||
bufferIdForDisappearance(shinyDisappearances, id);
|
||||
break;
|
||||
case EntityKind::BUS:
|
||||
id = dynamic_cast<Bus*>(other)->id;
|
||||
bufferIdForDisappearance(transportationDisappearances, id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// for mobs, decrement playersInView
|
||||
@@ -137,6 +281,17 @@ void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef re
|
||||
((Mob*)other)->playersInView--;
|
||||
}
|
||||
}
|
||||
|
||||
if (ref.kind == EntityKind::PLAYER) {
|
||||
if (!pcDisappearances.empty())
|
||||
sendAroundDelPackets(ref, pcDisappearances, P_FE2CL_AROUND_DEL_PC);
|
||||
if (!npcDisappearances.empty())
|
||||
sendAroundDelPackets(ref, npcDisappearances, P_FE2CL_AROUND_DEL_NPC);
|
||||
if (!shinyDisappearances.empty())
|
||||
sendAroundDelPackets(ref, shinyDisappearances, P_FE2CL_AROUND_DEL_SHINY);
|
||||
if (!transportationDisappearances.empty())
|
||||
sendAroundDelPackets(ref, transportationDisappearances, P_FE2CL_AROUND_DEL_TRANSPORTATION);
|
||||
}
|
||||
}
|
||||
|
||||
static void emptyChunk(ChunkPos chunkPos) {
|
||||
|
@@ -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;
|
||||
|
||||
/*
|
||||
@@ -539,9 +539,9 @@ static void dealGooDamage(CNSocket *sock) {
|
||||
return; // ignore completely
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_DotDamage);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
assert(resplen < CN_PACKET_BODY_SIZE);
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
|
||||
sSkillResult_DotDamage *dmg = (sSkillResult_DotDamage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
||||
@@ -633,9 +633,9 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) + pkt->iTargetCnt * sizeof(sAttackResult);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
sP_FE2CL_PC_ATTACK_CHARs_SUCC *resp = (sP_FE2CL_PC_ATTACK_CHARs_SUCC*)respbuf;
|
||||
sAttackResult *respdata = (sAttackResult*)(respbuf+sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC));
|
||||
@@ -837,15 +837,19 @@ 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
|
||||
*/
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT) + pkt->iTargetCnt * sizeof(sAttackResult);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
sP_FE2CL_PC_GRENADE_STYLE_HIT* resp = (sP_FE2CL_PC_GRENADE_STYLE_HIT*)respbuf;
|
||||
sAttackResult* respdata = (sAttackResult*)(respbuf + sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT));
|
||||
|
@@ -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");
|
||||
|
15
src/Eggs.cpp
15
src/Eggs.cpp
@@ -87,8 +87,8 @@ void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + result.size;
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
|
||||
pkt->iNPC_ID = eggId;
|
||||
@@ -126,15 +126,6 @@ static void eggStep(CNServer* serv, time_t currTime) {
|
||||
|
||||
}
|
||||
|
||||
void Eggs::npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg) {
|
||||
egg->iX = x;
|
||||
egg->iY = y;
|
||||
egg->iZ = z;
|
||||
// client doesn't care about egg->iMapNum
|
||||
egg->iShinyType = npc->iNPCType;
|
||||
egg->iShiny_ID = npc->iNPC_ID;
|
||||
}
|
||||
|
||||
static void eggPickup(CNSocket* sock, CNPacketData* data) {
|
||||
auto pickup = (sP_CL2FE_REQ_SHINY_PICKUP*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
@@ -192,7 +183,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
|
||||
// drop
|
||||
if (type->dropCrateId != 0) {
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
assert(resplen < CN_PACKET_BODY_SIZE);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
|
@@ -15,5 +15,4 @@ namespace Eggs {
|
||||
void init();
|
||||
|
||||
void eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration);
|
||||
void npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg);
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
#include "core/Core.hpp"
|
||||
#include "db/Database.hpp"
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "servers/Monitor.hpp"
|
||||
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Items.hpp"
|
||||
@@ -10,8 +11,6 @@
|
||||
|
||||
using namespace Email;
|
||||
|
||||
std::vector<std::string> Email::dump;
|
||||
|
||||
// New email notification
|
||||
static void emailUpdateCheck(CNSocket* sock, CNPacketData* data) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_NEW_EMAIL, resp);
|
||||
@@ -324,7 +323,14 @@ 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);
|
||||
Monitor::emails.push_back(logEmail);
|
||||
|
||||
// notification to recipient if online
|
||||
CNSocket* recipient = PlayerManager::getSockFromID(pkt->iTo_PCUID);
|
||||
if (recipient != nullptr)
|
||||
{
|
||||
emailUpdateCheck(recipient, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void Email::init() {
|
||||
|
@@ -4,7 +4,5 @@
|
||||
#include <string>
|
||||
|
||||
namespace Email {
|
||||
extern std::vector<std::string> dump;
|
||||
|
||||
void 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 {
|
||||
@@ -69,11 +70,7 @@ void Bus::enterIntoViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_TRANSPORTATION_ENTER, pkt);
|
||||
|
||||
// TODO: Potentially decouple this from BaseNPC?
|
||||
pkt.AppearanceData = {
|
||||
3, id, type,
|
||||
x, y, z
|
||||
};
|
||||
|
||||
pkt.AppearanceData = getTransportationAppearanceData();
|
||||
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_ENTER);
|
||||
}
|
||||
|
||||
@@ -81,12 +78,22 @@ void Egg::enterIntoViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt);
|
||||
|
||||
// TODO: Potentially decouple this from BaseNPC?
|
||||
pkt.ShinyAppearanceData = {
|
||||
pkt.ShinyAppearanceData = getShinyAppearanceData();
|
||||
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER);
|
||||
}
|
||||
|
||||
sTransportationAppearanceData Bus::getTransportationAppearanceData() {
|
||||
return sTransportationAppearanceData {
|
||||
3, id, type,
|
||||
x, y, z
|
||||
};
|
||||
}
|
||||
|
||||
sShinyAppearanceData Egg::getShinyAppearanceData() {
|
||||
return sShinyAppearanceData {
|
||||
id, type, 0, // client doesn't care about map num
|
||||
x, y, z
|
||||
};
|
||||
|
||||
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER);
|
||||
}
|
||||
|
||||
sNano* Player::getActiveNano() {
|
||||
|
@@ -161,6 +161,8 @@ struct Egg : public BaseNPC {
|
||||
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
|
||||
sShinyAppearanceData getShinyAppearanceData();
|
||||
};
|
||||
|
||||
struct Bus : public BaseNPC {
|
||||
@@ -172,4 +174,6 @@ struct Bus : public BaseNPC {
|
||||
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
|
||||
sTransportationAppearanceData getTransportationAppearanceData();
|
||||
};
|
||||
|
@@ -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;
|
||||
@@ -84,8 +87,8 @@ void Groups::addToGroup(Group* group, EntityRef member) {
|
||||
size_t pcCount = pcs.size();
|
||||
size_t npcCount = npcs.size();
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
sP_FE2CL_PC_GROUP_JOIN* pkt = (sP_FE2CL_PC_GROUP_JOIN*)respbuf;
|
||||
|
||||
pkt->iID_NewMember = PlayerManager::getPlayer(member.sock)->iID;
|
||||
@@ -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
|
||||
@@ -137,8 +143,8 @@ bool Groups::removeFromGroup(Group* group, EntityRef member) {
|
||||
size_t pcCount = pcs.size();
|
||||
size_t npcCount = npcs.size();
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
sP_FE2CL_PC_GROUP_LEAVE* pkt = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf;
|
||||
|
||||
pkt->iID_LeaveMember = PlayerManager::getPlayer(member.sock)->iID;
|
||||
@@ -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);
|
||||
@@ -273,8 +288,8 @@ void Groups::groupTickInfo(CNSocket* sock) {
|
||||
size_t pcCount = pcs.size();
|
||||
size_t npcCount = npcs.size();
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
sP_FE2CL_PC_GROUP_MEMBER_INFO* pkt = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf;
|
||||
|
||||
pkt->iID = plr->iID;
|
||||
@@ -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);
|
||||
|
@@ -46,7 +46,7 @@ static void nanoCapsuleHandler(CNSocket* sock, int slot, sItemBase *chest) {
|
||||
|
||||
// in order to remove capsule form inventory, we have to send item reward packet with empty item
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
assert(resplen < CN_PACKET_BODY_SIZE);
|
||||
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
@@ -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;
|
||||
|
||||
@@ -472,8 +475,8 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
|
||||
if (gumball.iOpt == 0)
|
||||
gumball = {};
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
sP_FE2CL_REP_PC_ITEM_USE_SUCC *resp = (sP_FE2CL_REP_PC_ITEM_USE_SUCC*)respbuf;
|
||||
sSkillResult_Buff *respdata = (sSkillResult_Buff*)(respbuf+sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC));
|
||||
@@ -553,7 +556,7 @@ static void chestOpenHandler(CNSocket *sock, CNPacketData *data) {
|
||||
|
||||
// item giving packet
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
assert(resplen < CN_PACKET_BODY_SIZE);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
@@ -642,7 +645,7 @@ void Items::checkItemExpire(CNSocket* sock, Player* player) {
|
||||
*/
|
||||
|
||||
const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
assert(resplen < CN_PACKET_BODY_SIZE);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
auto packet = (sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM*)respbuf;
|
||||
@@ -712,7 +715,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
assert(resplen < CN_PACKET_BODY_SIZE);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
|
@@ -64,7 +64,7 @@ static bool isQuestItemFull(CNSocket* sock, int itemId, int itemCount) {
|
||||
static void dropQuestItem(CNSocket *sock, int task, int count, int id, int mobid) {
|
||||
std::cout << "Altered item id " << id << " by " << count << " for task id " << task << std::endl;
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE);
|
||||
assert(resplen < CN_PACKET_BODY_SIZE);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
@@ -152,14 +152,14 @@ static int giveMissionReward(CNSocket *sock, int task, int choice=0) {
|
||||
plr->Inven[slots[i]] = { 999, 999, 999, 0 }; // temp item; overwritten later
|
||||
}
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + nrewards * sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE);
|
||||
assert(resplen < CN_PACKET_BODY_SIZE);
|
||||
sP_FE2CL_REP_REWARD_ITEM *resp = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
|
||||
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||
|
||||
// don't forget to zero the buffer!
|
||||
memset(respbuf, 0, resplen);
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
// update player
|
||||
plr->money += reward->money;
|
||||
@@ -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
|
||||
|
@@ -238,8 +238,8 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
uint8_t respbuf[CN_PACKET_BODY_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
|
||||
|
||||
sP_FE2CL_NPC_SKILL_CORRUPTION_HIT *resp = (sP_FE2CL_NPC_SKILL_CORRUPTION_HIT*)respbuf;
|
||||
sCAttackResult *respdata = (sCAttackResult*)(respbuf+sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT));
|
||||
@@ -478,6 +478,14 @@ void MobAI::deadStep(CombatNPC* npc, time_t currTime) {
|
||||
if (self->groupLeader == self->id)
|
||||
roamingStep(self, currTime);
|
||||
|
||||
/*
|
||||
* If the mob hasn't fully despanwed yet, don't try to respawn it. This protects
|
||||
* against the edge case where mobs with a very short regenTime would try to respawn
|
||||
* before they've faded away; and would respawn even if they were meant to be removed.
|
||||
*/
|
||||
if (!self->despawned)
|
||||
return;
|
||||
|
||||
if (self->killedTime != 0 && currTime - self->killedTime < self->regenTime * 100)
|
||||
return;
|
||||
|
||||
|
@@ -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) {
|
||||
@@ -382,6 +396,14 @@ static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
|
||||
static void exitGame(CNSocket* sock, CNPacketData* data) {
|
||||
auto exitData = (sP_CL2FE_REQ_PC_EXIT*)data->buf;
|
||||
|
||||
// Refresh any auth cookie, in case "change character" was used
|
||||
Player* plr = getPlayer(sock);
|
||||
if (plr != nullptr) {
|
||||
// 5 seconds should be enough to log in again
|
||||
Database::refreshCookie(plr->accountId, 5);
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_EXIT_SUCC, response);
|
||||
|
||||
response.iID = exitData->iID;
|
||||
@@ -577,7 +599,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
|
||||
@@ -598,6 +620,10 @@ std::string PlayerManager::getPlayerName(Player *plr, bool id) {
|
||||
if (plr == nullptr)
|
||||
return "NOT IN GAME";
|
||||
|
||||
if (plr->PCStyle.iNameCheck != 1) {
|
||||
return "Player " + std::to_string(plr->iID);
|
||||
}
|
||||
|
||||
std::string ret = "";
|
||||
if (id && plr->accountLevel <= 30)
|
||||
ret += "(GM) ";
|
||||
|
@@ -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 {
|
||||
|
@@ -1,5 +1,7 @@
|
||||
#include "TableData.hpp"
|
||||
|
||||
#include "servers/CNLoginServer.hpp"
|
||||
|
||||
#include "NPCManager.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Items.hpp"
|
||||
@@ -79,6 +81,25 @@ static void loadXDT(json& xdtData) {
|
||||
NPCManager::NPCData = xdtData["m_pNpcTable"]["m_pNpcData"];
|
||||
|
||||
try {
|
||||
// load name wheel names
|
||||
json firstNameData = xdtData["m_pNameTable"]["m_pFirstName"];
|
||||
for (json::iterator _name = firstNameData.begin(); _name != firstNameData.end(); _name++) {
|
||||
auto name = _name.value();
|
||||
LoginServer::WheelFirstNames.push_back(name["m_pstrNameString"]);
|
||||
}
|
||||
|
||||
json middleNameData = xdtData["m_pNameTable"]["m_pMiddleName"];
|
||||
for (json::iterator _name = middleNameData.begin(); _name != middleNameData.end(); _name++) {
|
||||
auto name = _name.value();
|
||||
LoginServer::WheelMiddleNames.push_back(name["m_pstrNameString"]);
|
||||
}
|
||||
|
||||
json lastNameData = xdtData["m_pNameTable"]["m_pLastName"];
|
||||
for (json::iterator _name = lastNameData.begin(); _name != lastNameData.end(); _name++) {
|
||||
auto name = _name.value();
|
||||
LoginServer::WheelLastNames.push_back(name["m_pstrNameString"]);
|
||||
}
|
||||
|
||||
// load warps
|
||||
json warpData = xdtData["m_pInstanceTable"]["m_pWarpData"];
|
||||
|
||||
@@ -375,7 +396,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 +605,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 +631,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 +716,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 +1391,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);
|
||||
|
@@ -388,8 +388,21 @@ NPCPath* Transport::findApplicablePath(int32_t id, int32_t type, int taskID) {
|
||||
|
||||
void Transport::constructPathNPC(int32_t id, NPCPath* path) {
|
||||
BaseNPC* npc = NPCManager::NPCs[id];
|
||||
if (npc->kind == EntityKind::MOB)
|
||||
((Mob*)(npc))->staticPath = true;
|
||||
|
||||
if (npc->kind == EntityKind::MOB) {
|
||||
auto mob = (Mob*)npc;
|
||||
mob->staticPath = true;
|
||||
|
||||
Vec3 firstPoint = path->points.front();
|
||||
|
||||
// Ensure that the first point coincides with the mob's spawn point.
|
||||
if (mob->spawnX != firstPoint.x || mob->spawnY != firstPoint.y) {
|
||||
std::cout << "[FATAL] The first point of the route for mob " << mob->id << " (type " << mob->type
|
||||
<< ") does not correspond with its spawn point." << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
npc->loopingPath = path->isLoop;
|
||||
|
||||
// Interpolate
|
||||
|
@@ -428,7 +428,7 @@ void CNServer::removePollFD(int fd) {
|
||||
}
|
||||
|
||||
void CNServer::start() {
|
||||
std::cout << "Starting server at *:" << port << std::endl;
|
||||
std::cout << "Starting " << serverType << " server at *:" << port << std::endl;
|
||||
while (active) {
|
||||
// the timeout is to ensure shard timers are ticking
|
||||
int n = poll(fds.data(), fds.size(), 50);
|
||||
|
@@ -95,14 +95,14 @@ inline constexpr bool isOutboundPacketID(uint32_t id) {
|
||||
// for outbound packets
|
||||
inline constexpr bool validOutVarPacket(size_t base, size_t npayloads, size_t plsize) {
|
||||
// check for multiplication overflow
|
||||
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
|
||||
if (npayloads > 0 && (CN_PACKET_BODY_SIZE) / (size_t)npayloads < plsize)
|
||||
return false;
|
||||
|
||||
// it's safe to multiply
|
||||
size_t trailing = npayloads * plsize;
|
||||
|
||||
// does it fit in a packet?
|
||||
if (base + trailing > CN_PACKET_BUFFER_SIZE - 8)
|
||||
if (base + trailing > CN_PACKET_BODY_SIZE)
|
||||
return false;
|
||||
|
||||
// everything is a-ok!
|
||||
@@ -112,14 +112,14 @@ inline constexpr bool validOutVarPacket(size_t base, size_t npayloads, size_t pl
|
||||
// for inbound packets
|
||||
inline constexpr bool validInVarPacket(size_t base, size_t npayloads, size_t plsize, size_t datasize) {
|
||||
// check for multiplication overflow
|
||||
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
|
||||
if (npayloads > 0 && CN_PACKET_BODY_SIZE / (size_t)npayloads < plsize)
|
||||
return false;
|
||||
|
||||
// it's safe to multiply
|
||||
size_t trailing = npayloads * plsize;
|
||||
|
||||
// make sure size is exact
|
||||
// datasize has already been validated against CN_PACKET_BUFFER_SIZE
|
||||
// datasize has already been validated against CN_PACKET_BODY_SIZE
|
||||
if (datasize != base + trailing)
|
||||
return false;
|
||||
|
||||
|
@@ -29,8 +29,8 @@
|
||||
#define INITSTRUCT(T, x) T x; \
|
||||
memset(&x, 0, sizeof(T));
|
||||
|
||||
#define INITVARPACKET(_buf, _Pkt, _pkt, _Trailer, _trailer) uint8_t _buf[CN_PACKET_BUFFER_SIZE]; \
|
||||
memset(&_buf, 0, CN_PACKET_BUFFER_SIZE); \
|
||||
#define INITVARPACKET(_buf, _Pkt, _pkt, _Trailer, _trailer) uint8_t _buf[CN_PACKET_BODY_SIZE]; \
|
||||
memset(&_buf, 0, CN_PACKET_BODY_SIZE); \
|
||||
auto _pkt = (_Pkt*)_buf; \
|
||||
auto _trailer = (_Trailer*)(_pkt + 1);
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
|
||||
// wrapper for U16toU8
|
||||
#define ARRLEN(x) (sizeof(x)/sizeof(*x))
|
||||
#define AUTOU8(x) std::string((char*)x, ARRLEN(x))
|
||||
#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))
|
||||
|
||||
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt
|
||||
@@ -48,15 +49,18 @@ std::string U16toU8(char16_t* src, size_t max);
|
||||
size_t U8toU16(std::string src, char16_t* des, size_t max); // returns number of char16_t that was written at des
|
||||
time_t getTime();
|
||||
time_t getTimestamp();
|
||||
int timingSafeStrcmp(const char* a, const char* b);
|
||||
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
|
||||
|
@@ -1,6 +1,8 @@
|
||||
/* enum definitions from the client */
|
||||
#pragma once
|
||||
|
||||
#include "core/CNStructs.hpp"
|
||||
|
||||
// floats
|
||||
const float VALUE_BATTERY_EMPTY_PENALTY = 0.5f;
|
||||
const float CN_EP_RANK_1 = 0.8f;
|
||||
@@ -410,7 +412,13 @@ enum {
|
||||
SEND_ANYCAST_NEW = 3,
|
||||
SEND_BROADCAST = 4,
|
||||
|
||||
#if PROTOCOL_VERSION == 728
|
||||
CN_PACKET_BUFFER_SIZE = 8192,
|
||||
#elif PROTOCOL_VERSION == 1013
|
||||
CN_PACKET_BUFFER_SIZE = 8192,
|
||||
#else
|
||||
CN_PACKET_BUFFER_SIZE = 4096,
|
||||
#endif
|
||||
|
||||
P_CL2LS_REQ_LOGIN = 0x12000001, // 301989889
|
||||
P_CL2LS_REQ_CHECK_CHAR_NAME = 0x12000002, // 301989890
|
||||
@@ -934,3 +942,8 @@ enum {
|
||||
|
||||
N_PACKETS = N_CL2LS + N_CL2FE + N_FE2CL + N_LS2CL
|
||||
};
|
||||
|
||||
/*
|
||||
* Usable space in the packet buffer = CN_PACKET_BUFFER_SIZE - type - size
|
||||
*/
|
||||
constexpr size_t CN_PACKET_BODY_SIZE = CN_PACKET_BUFFER_SIZE - 2 * sizeof(int32_t);
|
||||
|
@@ -235,7 +235,7 @@ std::map<uint32_t, PacketDesc> Packets::packets = {
|
||||
PACKET(P_FE2CL_REP_PC_EXIT_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_EXIT_SUCC),
|
||||
PACKET(P_FE2CL_PC_EXIT),
|
||||
PACKET(P_FE2CL_PC_AROUND),
|
||||
VAR_PACKET(P_FE2CL_PC_AROUND, iPCCnt, sPCAppearanceData),
|
||||
PACKET(P_FE2CL_PC_MOVE),
|
||||
PACKET(P_FE2CL_PC_STOP),
|
||||
PACKET(P_FE2CL_PC_JUMP),
|
||||
@@ -243,9 +243,9 @@ std::map<uint32_t, PacketDesc> Packets::packets = {
|
||||
PACKET(P_FE2CL_NPC_EXIT),
|
||||
PACKET(P_FE2CL_NPC_MOVE),
|
||||
PACKET(P_FE2CL_NPC_NEW),
|
||||
PACKET(P_FE2CL_NPC_AROUND),
|
||||
PACKET(P_FE2CL_AROUND_DEL_PC),
|
||||
PACKET(P_FE2CL_AROUND_DEL_NPC),
|
||||
VAR_PACKET(P_FE2CL_NPC_AROUND, iNPCCnt, sNPCAppearanceData),
|
||||
VAR_PACKET(P_FE2CL_AROUND_DEL_PC, iPCCnt, int32_t),
|
||||
VAR_PACKET(P_FE2CL_AROUND_DEL_NPC, iNPCCnt, int32_t),
|
||||
PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC),
|
||||
PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_FAIL),
|
||||
VAR_PACKET(P_FE2CL_PC_ATTACK_NPCs_SUCC, iNPCCnt, sAttackResult),
|
||||
@@ -387,8 +387,8 @@ std::map<uint32_t, PacketDesc> Packets::packets = {
|
||||
PACKET(P_FE2CL_TRANSPORTATION_EXIT),
|
||||
PACKET(P_FE2CL_TRANSPORTATION_MOVE),
|
||||
PACKET(P_FE2CL_TRANSPORTATION_NEW),
|
||||
PACKET(P_FE2CL_TRANSPORTATION_AROUND),
|
||||
PACKET(P_FE2CL_AROUND_DEL_TRANSPORTATION),
|
||||
VAR_PACKET(P_FE2CL_TRANSPORTATION_AROUND, iCnt, sTransportationAppearanceData),
|
||||
VAR_PACKET(P_FE2CL_AROUND_DEL_TRANSPORTATION, iCnt, int32_t),
|
||||
PACKET(P_FE2CL_REP_EP_RANK_LIST),
|
||||
PACKET(P_FE2CL_REP_EP_RANK_DETAIL),
|
||||
PACKET(P_FE2CL_REP_EP_RANK_PC_INFO),
|
||||
@@ -404,8 +404,8 @@ std::map<uint32_t, PacketDesc> Packets::packets = {
|
||||
PACKET(P_FE2CL_SHINY_ENTER),
|
||||
PACKET(P_FE2CL_SHINY_EXIT),
|
||||
PACKET(P_FE2CL_SHINY_NEW),
|
||||
PACKET(P_FE2CL_SHINY_AROUND),
|
||||
PACKET(P_FE2CL_AROUND_DEL_SHINY),
|
||||
VAR_PACKET(P_FE2CL_SHINY_AROUND, iShinyCnt, sShinyAppearanceData),
|
||||
VAR_PACKET(P_FE2CL_AROUND_DEL_SHINY, iShinyCnt, int32_t),
|
||||
PACKET(P_FE2CL_REP_SHINY_PICKUP_FAIL),
|
||||
PACKET(P_FE2CL_REP_SHINY_PICKUP_SUCC),
|
||||
PACKET(P_FE2CL_PC_MOVETRANSPORTATION),
|
||||
|
@@ -5,7 +5,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define DATABASE_VERSION 4
|
||||
#define DATABASE_VERSION 6
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -46,9 +46,18 @@ 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);
|
||||
|
||||
// return true if cookie is valid for the account.
|
||||
// invalidates the stored cookie afterwards
|
||||
bool checkCookie(int accountId, const char *cookie);
|
||||
void refreshCookie(int accountId, int durationSec);
|
||||
|
||||
// interface for the /ban command
|
||||
bool banPlayer(int playerId, std::string& reason);
|
||||
bool unbanPlayer(int playerId);
|
||||
@@ -60,7 +69,7 @@ namespace Database {
|
||||
bool isNameFree(std::string firstName, std::string lastName);
|
||||
bool isSlotFree(int accountId, int slotNum);
|
||||
/// returns ID, 0 if something failed
|
||||
int createCharacter(sP_CL2LS_REQ_SAVE_CHAR_NAME* save, int AccountID);
|
||||
int createCharacter(int slot, int accountId, const char* firstName, const char* lastName, int nameCheck);
|
||||
/// returns true if query succeeded
|
||||
bool finishCharacter(sP_CL2LS_REQ_CHAR_CREATE* character, int accountId);
|
||||
/// returns true if query succeeded
|
||||
@@ -76,7 +85,7 @@ namespace Database {
|
||||
};
|
||||
void evaluateCustomName(int characterID, CustomName decision);
|
||||
/// returns true if query succeeded
|
||||
bool changeName(sP_CL2LS_REQ_CHANGE_CHAR_NAME* save, int accountId);
|
||||
bool changeName(int playerId, int accountId, const char* firstName, const char* lastName, int nameCheck);
|
||||
|
||||
// getting players
|
||||
void getPlayer(Player* plr, int id);
|
||||
|
@@ -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";
|
||||
}
|
||||
|
150
src/db/login.cpp
150
src/db/login.cpp
@@ -1,5 +1,9 @@
|
||||
#include "core/CNStructs.hpp"
|
||||
|
||||
#include "db/internal.hpp"
|
||||
|
||||
#include "servers/CNLoginServer.hpp"
|
||||
|
||||
#include "bcrypt/BCrypt.hpp"
|
||||
|
||||
void Database::findAccount(Account* account, std::string login) {
|
||||
@@ -27,6 +31,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 +82,98 @@ 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);
|
||||
}
|
||||
|
||||
bool Database::checkCookie(int accountId, const char *tryCookie) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql_get = R"(
|
||||
SELECT Cookie
|
||||
FROM Auth
|
||||
WHERE AccountID = ? AND Expires > ?;
|
||||
)";
|
||||
|
||||
const char* sql_invalidate = R"(
|
||||
UPDATE Auth
|
||||
SET Expires = 0
|
||||
WHERE AccountID = ?;
|
||||
)";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql_get, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, accountId);
|
||||
sqlite3_bind_int(stmt, 2, getTimestamp());
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *cookie = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
||||
if (strlen(cookie) != strlen(tryCookie)) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool match = (timingSafeStrcmp(cookie, tryCookie) == 0);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
/*
|
||||
* Only invalidate the cookie if it was correct. This prevents
|
||||
* replay attacks without enabling DOS attacks on accounts.
|
||||
*/
|
||||
if (match) {
|
||||
sqlite3_prepare_v2(db, sql_invalidate, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, accountId);
|
||||
rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE)
|
||||
std::cout << "[WARN] Database fail on checkCookie(): " << sqlite3_errmsg(db) << std::endl;
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
void Database::refreshCookie(int accountId, int durationSec) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
UPDATE Auth
|
||||
SET Expires = ?
|
||||
WHERE AccountID = ?;
|
||||
)";
|
||||
|
||||
int expires = getTimestamp() + durationSec;
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, expires);
|
||||
sqlite3_bind_int(stmt, 2, accountId);
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE)
|
||||
std::cout << "[WARN] Database fail on refreshCookie(): " << sqlite3_errmsg(db) << std::endl;
|
||||
}
|
||||
|
||||
void Database::updateSelected(int accountId, int slot) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
@@ -171,7 +293,7 @@ bool Database::isSlotFree(int accountId, int slotNum) {
|
||||
return result;
|
||||
}
|
||||
|
||||
int Database::createCharacter(sP_CL2LS_REQ_SAVE_CHAR_NAME* save, int AccountID) {
|
||||
int Database::createCharacter(int slot, int accountId, const char* firstName, const char* lastName, int nameCheck) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
||||
@@ -184,22 +306,17 @@ int Database::createCharacter(sP_CL2LS_REQ_SAVE_CHAR_NAME* save, int AccountID)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
std::string firstName = AUTOU16TOU8(save->szFirstName);
|
||||
std::string lastName = AUTOU16TOU8(save->szLastName);
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, AccountID);
|
||||
sqlite3_bind_int(stmt, 2, save->iSlotNum);
|
||||
sqlite3_bind_text(stmt, 3, firstName.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 4, lastName.c_str(), -1, NULL);
|
||||
sqlite3_bind_int(stmt, 1, accountId);
|
||||
sqlite3_bind_int(stmt, 2, slot);
|
||||
sqlite3_bind_text(stmt, 3, firstName, -1, NULL);
|
||||
sqlite3_bind_text(stmt, 4, lastName, -1, NULL);
|
||||
sqlite3_bind_int(stmt, 5, settings::SPAWN_X);
|
||||
sqlite3_bind_int(stmt, 6, settings::SPAWN_Y);
|
||||
sqlite3_bind_int(stmt, 7, settings::SPAWN_Z);
|
||||
sqlite3_bind_int(stmt, 8, settings::SPAWN_ANGLE);
|
||||
sqlite3_bind_int(stmt, 9, PC_MAXHEALTH(1));
|
||||
|
||||
// if FNCode isn't 0, it's a wheel name
|
||||
int nameCheck = (settings::APPROVEALLNAMES || save->iFNCode) ? 1 : 0;
|
||||
sqlite3_bind_int(stmt, 10, nameCheck);
|
||||
|
||||
// blobs
|
||||
@@ -528,7 +645,7 @@ void Database::evaluateCustomName(int characterID, CustomName decision) {
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
bool Database::changeName(sP_CL2LS_REQ_CHANGE_CHAR_NAME* save, int accountId) {
|
||||
bool Database::changeName(int playerId, int accountId, const char* firstName, const char* lastName, int nameCheck) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
@@ -542,15 +659,10 @@ bool Database::changeName(sP_CL2LS_REQ_CHANGE_CHAR_NAME* save, int accountId) {
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
std::string firstName = AUTOU16TOU8(save->szFirstName);
|
||||
std::string lastName = AUTOU16TOU8(save->szLastName);
|
||||
|
||||
sqlite3_bind_text(stmt, 1, firstName.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 2, lastName.c_str(), -1, NULL);
|
||||
// if FNCode isn't 0, it's a wheel name
|
||||
int nameCheck = (settings::APPROVEALLNAMES || save->iFNCode) ? 1 : 0;
|
||||
sqlite3_bind_text(stmt, 1, firstName, -1, NULL);
|
||||
sqlite3_bind_text(stmt, 2, lastName, -1, NULL);
|
||||
sqlite3_bind_int(stmt, 3, nameCheck);
|
||||
sqlite3_bind_int(stmt, 4, save->iPCUID);
|
||||
sqlite3_bind_int(stmt, 4, playerId);
|
||||
sqlite3_bind_int(stmt, 5, accountId);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
|
@@ -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 = ?;
|
||||
|
17
src/main.cpp
17
src/main.cpp
@@ -49,6 +49,7 @@ CNShardServer *shardServer = nullptr;
|
||||
std::thread *shardThread = nullptr;
|
||||
|
||||
void startShard(CNShardServer* server) {
|
||||
sandbox_thread_start();
|
||||
server->start();
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ void terminate(int arg) {
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static BOOL winTerminate(DWORD arg) {
|
||||
static BOOL WINAPI winTerminate(DWORD arg) {
|
||||
terminate(0);
|
||||
return FALSE;
|
||||
}
|
||||
@@ -150,6 +151,8 @@ int main() {
|
||||
/* not reached */
|
||||
}
|
||||
|
||||
sandbox_init();
|
||||
|
||||
std::cout << "[INFO] Starting Server Threads..." << std::endl;
|
||||
CNLoginServer loginServer(settings::LOGINPORT);
|
||||
shardServer = new CNShardServer(settings::SHARDPORT);
|
||||
@@ -157,6 +160,7 @@ int main() {
|
||||
shardThread = new std::thread(startShard, (CNShardServer*)shardServer);
|
||||
|
||||
sandbox_start();
|
||||
sandbox_thread_start();
|
||||
|
||||
loginServer.start();
|
||||
|
||||
@@ -218,6 +222,17 @@ time_t getTimestamp() {
|
||||
return (time_t)value.count();
|
||||
}
|
||||
|
||||
// timing safe strcmp implementation for e.g. cookie validation
|
||||
int timingSafeStrcmp(const char* a, const char* b) {
|
||||
int diff = 0;
|
||||
while (*a && *b) {
|
||||
diff |= *a++ ^ *b++;
|
||||
}
|
||||
diff |= *a;
|
||||
diff |= *b;
|
||||
return diff;
|
||||
}
|
||||
|
||||
// convert integer timestamp (in s) to FF systime struct
|
||||
sSYSTEMTIME timeStampToStruct(uint64_t time) {
|
||||
|
||||
|
@@ -4,11 +4,16 @@
|
||||
#if defined(__linux__) || defined(__OpenBSD__)
|
||||
|
||||
# if !defined(CONFIG_NOSANDBOX)
|
||||
void sandbox_init();
|
||||
void sandbox_start();
|
||||
void sandbox_thread_start();
|
||||
# else
|
||||
|
||||
#include <iostream>
|
||||
|
||||
inline void sandbox_init() {}
|
||||
inline void sandbox_thread_start() {}
|
||||
|
||||
inline void sandbox_start() {
|
||||
std::cout << "[WARN] Built without a sandbox" << std::endl;
|
||||
}
|
||||
@@ -17,5 +22,7 @@ inline void sandbox_start() {
|
||||
|
||||
#else
|
||||
// stub for unsupported platforms
|
||||
inline void sandbox_init() {}
|
||||
inline void sandbox_start() {}
|
||||
inline void sandbox_thread_start() {}
|
||||
#endif
|
||||
|
@@ -13,6 +13,9 @@ static void eunveil(const char *path, const char *permissions) {
|
||||
err(1, "unveil");
|
||||
}
|
||||
|
||||
void sandbox_init() {}
|
||||
void sandbox_thread_start() {}
|
||||
|
||||
void sandbox_start() {
|
||||
/*
|
||||
* There shouldn't ever be a reason to disable this one, but might as well
|
||||
|
@@ -4,6 +4,9 @@
|
||||
#include "settings.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/ptrace.h>
|
||||
@@ -17,6 +20,10 @@
|
||||
#include <linux/audit.h>
|
||||
#include <linux/net.h> // for socketcall() args
|
||||
|
||||
#ifndef CONFIG_NOLANDLOCK
|
||||
#include <linux/landlock.h>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Macros adapted from https://outflux.net/teach-seccomp/
|
||||
* Relevant license:
|
||||
@@ -54,7 +61,7 @@
|
||||
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ERRNO|(_errno))
|
||||
|
||||
#define KILL_PROCESS \
|
||||
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL_PROCESS)
|
||||
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_TRAP)
|
||||
|
||||
/*
|
||||
* Macros adapted from openssh's sandbox-seccomp-filter.c
|
||||
@@ -297,25 +304,201 @@ static sock_fprog prog = {
|
||||
ARRLEN(filter), filter
|
||||
};
|
||||
|
||||
// our own wrapper for the seccomp() syscall
|
||||
// Our own wrapper for the seccomp() syscall.
|
||||
int seccomp(unsigned int operation, unsigned int flags, void *args) {
|
||||
return syscall(__NR_seccomp, operation, flags, args);
|
||||
}
|
||||
|
||||
void sandbox_start() {
|
||||
#ifndef CONFIG_NOLANDLOCK
|
||||
|
||||
// Support compilation on systems that only have older Landlock headers.
|
||||
#ifndef LANDLOCK_ACCESS_FS_REFER
|
||||
#define LANDLOCK_ACCESS_FS_REFER 0
|
||||
#endif
|
||||
#ifndef LANDLOCK_ACCESS_FS_TRUNCATE
|
||||
#define LANDLOCK_ACCESS_FS_TRUNCATE 0
|
||||
#endif
|
||||
|
||||
struct landlock_ruleset_attr ruleset_attr = {
|
||||
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE
|
||||
| LANDLOCK_ACCESS_FS_WRITE_FILE
|
||||
| LANDLOCK_ACCESS_FS_READ_DIR
|
||||
| LANDLOCK_ACCESS_FS_MAKE_REG
|
||||
| LANDLOCK_ACCESS_FS_MAKE_DIR
|
||||
| LANDLOCK_ACCESS_FS_MAKE_SYM
|
||||
| LANDLOCK_ACCESS_FS_MAKE_SOCK
|
||||
| LANDLOCK_ACCESS_FS_MAKE_FIFO
|
||||
| LANDLOCK_ACCESS_FS_MAKE_BLOCK
|
||||
| LANDLOCK_ACCESS_FS_REMOVE_FILE
|
||||
| LANDLOCK_ACCESS_FS_REMOVE_DIR
|
||||
| LANDLOCK_ACCESS_FS_TRUNCATE
|
||||
| LANDLOCK_ACCESS_FS_REFER
|
||||
};
|
||||
|
||||
uint64_t landlock_perms = LANDLOCK_ACCESS_FS_READ_FILE
|
||||
| LANDLOCK_ACCESS_FS_WRITE_FILE
|
||||
| LANDLOCK_ACCESS_FS_TRUNCATE
|
||||
| LANDLOCK_ACCESS_FS_MAKE_REG
|
||||
| LANDLOCK_ACCESS_FS_REMOVE_FILE;
|
||||
|
||||
int landlock_fd;
|
||||
bool landlock_supported;
|
||||
|
||||
/*
|
||||
* Our own wrappers for Landlock syscalls.
|
||||
*/
|
||||
|
||||
int landlock_create_ruleset(const struct landlock_ruleset_attr *attr, size_t size, uint32_t flags) {
|
||||
return syscall(__NR_landlock_create_ruleset, attr, size, flags);
|
||||
}
|
||||
|
||||
int landlock_add_rule(int ruleset_fd, enum landlock_rule_type rule_type, const void *rule_attr, uint32_t flags) {
|
||||
return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags);
|
||||
}
|
||||
|
||||
int landlock_restrict_self(int ruleset_fd, uint32_t flags) {
|
||||
return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
|
||||
}
|
||||
|
||||
static void landlock_path(std::string path, uint32_t perms) {
|
||||
struct landlock_path_beneath_attr path_beneath = {
|
||||
.allowed_access = perms
|
||||
};
|
||||
|
||||
path_beneath.parent_fd = open(path.c_str(), O_PATH|O_CLOEXEC);
|
||||
if (path_beneath.parent_fd < 0) {
|
||||
perror(path.c_str());
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (landlock_add_rule(landlock_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0)) {
|
||||
perror("landlock_add_rule");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
close(path_beneath.parent_fd);
|
||||
}
|
||||
|
||||
static bool landlock_detect() {
|
||||
int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
|
||||
|
||||
if (abi < 0) {
|
||||
if (errno == ENOSYS || errno == EOPNOTSUPP) {
|
||||
std::cout << "[WARN] No Landlock support on this system" << std::endl;
|
||||
return false;
|
||||
}
|
||||
perror("landlock_create_ruleset");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
std::cout << "[INFO] Detected Landlock ABI version: " << abi << std::endl;
|
||||
|
||||
switch (abi) {
|
||||
case 1:
|
||||
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
|
||||
landlock_perms &= ~LANDLOCK_ACCESS_FS_REFER;
|
||||
// fallthrough
|
||||
case 2:
|
||||
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE;
|
||||
landlock_perms &= ~LANDLOCK_ACCESS_FS_TRUNCATE;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void landlock_init() {
|
||||
std::cout << "[INFO] Setting up Landlock sandbox..." << std::endl;
|
||||
|
||||
landlock_supported = landlock_detect();
|
||||
|
||||
if (!landlock_supported)
|
||||
return;
|
||||
|
||||
landlock_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
|
||||
if (landlock_fd < 0) {
|
||||
perror("landlock_create_ruleset");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
std::string dbdir = std::filesystem::path(settings::DBPATH).parent_path();
|
||||
|
||||
// for the DB files (we can't rely on them being in the working directory)
|
||||
landlock_path(dbdir == "" ? "." : dbdir, landlock_perms);
|
||||
// for writing the gruntwork file
|
||||
landlock_path(settings::TDATADIR, landlock_perms);
|
||||
// for passowrd salting during account creation
|
||||
landlock_path("/dev/urandom", LANDLOCK_ACCESS_FS_READ_FILE);
|
||||
// for core dumps, optionally
|
||||
if (settings::SANDBOXEXTRAPATH != "")
|
||||
landlock_path(settings::SANDBOXEXTRAPATH, landlock_perms);
|
||||
}
|
||||
|
||||
#endif // !CONFIG_NOLANDLOCK
|
||||
|
||||
static void sigsys_handler(int signo, siginfo_t *info, void *context) {
|
||||
// report the unhandled syscall
|
||||
std::cout << "[FATAL] Unhandled syscall " << info->si_syscall
|
||||
<< " at " << std::hex << info->si_call_addr << " on arch " << info->si_arch << std::endl;
|
||||
|
||||
std::cout << "If you're unsure why this is happening, please read https://openfusion.dev/docs/development/the-sandbox/" << std::endl
|
||||
<< "for more information and possibly open an issue at https://github.com/OpenFusionProject/OpenFusion/issues to report"
|
||||
<< " needed changes in our seccomp filter." << std::endl;
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
void sandbox_init() {
|
||||
if (!settings::SANDBOX) {
|
||||
std::cout << "[WARN] Running without a sandbox" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// listen to SIGSYS to report unhandled syscalls
|
||||
struct sigaction sa = {};
|
||||
|
||||
sa.sa_flags = SA_SIGINFO;
|
||||
sa.sa_sigaction = sigsys_handler;
|
||||
|
||||
if (sigaction(SIGSYS, &sa, NULL) < 0) {
|
||||
perror("sigaction");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
#ifndef CONFIG_NOLANDLOCK
|
||||
landlock_init();
|
||||
#else
|
||||
std::cout << "[WARN] Built without Landlock" << std::endl;
|
||||
#endif
|
||||
}
|
||||
|
||||
void sandbox_start() {
|
||||
if (!settings::SANDBOX)
|
||||
return;
|
||||
|
||||
std::cout << "[INFO] Starting seccomp-bpf sandbox..." << std::endl;
|
||||
|
||||
// Sandboxing starts in sandbox_thread_start().
|
||||
}
|
||||
|
||||
void sandbox_thread_start() {
|
||||
if (!settings::SANDBOX)
|
||||
return;
|
||||
|
||||
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
|
||||
perror("prctl");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, &prog) < 0) {
|
||||
#ifndef CONFIG_NOLANDLOCK
|
||||
if (landlock_supported) {
|
||||
if (landlock_restrict_self(landlock_fd, 0)) {
|
||||
perror("landlock_restrict_self");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog) < 0) {
|
||||
perror("seccomp");
|
||||
exit(1);
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
#include "servers/CNLoginServer.hpp"
|
||||
#include "servers/Monitor.hpp"
|
||||
|
||||
#include "core/CNShared.hpp"
|
||||
#include "db/Database.hpp"
|
||||
@@ -12,6 +13,12 @@
|
||||
|
||||
std::map<CNSocket*, CNLoginData> CNLoginServer::loginSessions;
|
||||
|
||||
namespace LoginServer {
|
||||
std::vector<std::string> WheelFirstNames;
|
||||
std::vector<std::string> WheelMiddleNames;
|
||||
std::vector<std::string> WheelLastNames;
|
||||
}
|
||||
|
||||
CNLoginServer::CNLoginServer(uint16_t p) {
|
||||
serverType = "login";
|
||||
port = p;
|
||||
@@ -105,77 +112,63 @@ void loginFail(LoginError errorCode, std::string userLogin, CNSocket* sock) {
|
||||
|
||||
void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
|
||||
auto login = (sP_CL2LS_REQ_LOGIN*)data->buf;
|
||||
// TODO: implement better way of sending credentials
|
||||
std::string userLogin((char*)login->szCookie_TEGid);
|
||||
std::string userPassword((char*)login->szCookie_authid);
|
||||
|
||||
std::string userLogin;
|
||||
std::string userToken; // could be password or auth cookie
|
||||
|
||||
/*
|
||||
* Sometimes the client sends garbage cookie data.
|
||||
* Validate it as normal credentials instead of using a length check before falling back.
|
||||
* In this context, "cookie auth" just means the credentials were sent
|
||||
* in the szCookie fields instead of szID and szPassword.
|
||||
*/
|
||||
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
|
||||
/*
|
||||
* The std::string -> char* -> std::string maneuver should remove any
|
||||
* trailing garbage after the null terminator.
|
||||
*/
|
||||
bool isCookieAuth = login->iLoginType == USE_COOKIE_FIELDS;
|
||||
|
||||
/*
|
||||
* The std::string -> char* -> std::string maneuver should remove any
|
||||
* trailing garbage after the null terminator.
|
||||
*/
|
||||
if (isCookieAuth) {
|
||||
userLogin = std::string(AUTOU8(login->szCookie_TEGid).c_str());
|
||||
userToken = std::string(AUTOU8(login->szCookie_authid).c_str());
|
||||
} else {
|
||||
userLogin = std::string(AUTOU16TOU8(login->szID).c_str());
|
||||
userPassword = std::string(AUTOU16TOU8(login->szPassword).c_str());
|
||||
userToken = std::string(AUTOU16TOU8(login->szPassword).c_str());
|
||||
}
|
||||
|
||||
// the client inserts a "\n" in the password if you press enter key in the middle of the password
|
||||
// (not at the start or the end of the password field)
|
||||
if (int(userPassword.find("\n")) > 0)
|
||||
userPassword.erase(userPassword.find("\n"), 1);
|
||||
|
||||
// check regex
|
||||
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
|
||||
// send a custom error message
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
|
||||
std::string text = "Invalid login or password\n";
|
||||
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore\n";
|
||||
text += "Password has to be 8 - 32 characters long";
|
||||
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
|
||||
msg.iDuringTime = 15;
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
|
||||
// we still have to send login fail to prevent softlock
|
||||
if (!CNLoginServer::checkUsername(sock, userLogin)) {
|
||||
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
|
||||
}
|
||||
|
||||
|
||||
if (!isCookieAuth) {
|
||||
// password was sent in plaintext
|
||||
if (!CNLoginServer::checkPassword(sock, userToken)) {
|
||||
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
|
||||
}
|
||||
}
|
||||
|
||||
Database::Account findUser = {};
|
||||
Database::findAccount(&findUser, userLogin);
|
||||
|
||||
|
||||
// account was not found
|
||||
if (findUser.AccountID == 0) {
|
||||
if (settings::AUTOCREATEACCOUNTS)
|
||||
return newAccount(sock, userLogin, userPassword, login->iClientVerC);
|
||||
/*
|
||||
* Don't auto-create accounts if it's a cookie login.
|
||||
* It'll either be a bad cookie or a plaintext password sent by auto-login;
|
||||
* either way, we only want to allow auto-creation if the user explicitly entered their credentials.
|
||||
*/
|
||||
if (settings::AUTOCREATEACCOUNTS && !isCookieAuth) {
|
||||
return newAccount(sock, userLogin, userToken, login->iClientVerC);
|
||||
}
|
||||
|
||||
return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock);
|
||||
}
|
||||
|
||||
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userPassword))
|
||||
// make sure either a valid cookie or password was sent
|
||||
if (!CNLoginServer::checkToken(sock, findUser, userToken, isCookieAuth)) {
|
||||
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
|
||||
}
|
||||
|
||||
// is the account banned
|
||||
if (findUser.BannedUntil > getTimestamp()) {
|
||||
// send a custom error message
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
|
||||
|
||||
// ceiling devision
|
||||
int64_t remainingDays = (findUser.BannedUntil-getTimestamp()) / 86400 + ((findUser.BannedUntil - getTimestamp()) % 86400 != 0);
|
||||
|
||||
std::string text = "Your account has been banned. \nReason: ";
|
||||
text += findUser.BanReason;
|
||||
text += "\nBan expires in " + std::to_string(remainingDays) + " day";
|
||||
if (remainingDays > 1)
|
||||
text += "s";
|
||||
|
||||
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
|
||||
msg.iDuringTime = 99999999;
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
// don't send fail packet
|
||||
return;
|
||||
if (CNLoginServer::checkBan(sock, findUser)) {
|
||||
return; // don't send fail packet
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -299,10 +292,29 @@ void CNLoginServer::nameSave(CNSocket* sock, CNPacketData* data) {
|
||||
INITSTRUCT(sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC, resp);
|
||||
|
||||
int errorCode = 0;
|
||||
if (!CNLoginServer::isCharacterNameGood(AUTOU16TOU8(save->szFirstName), AUTOU16TOU8(save->szLastName))) {
|
||||
errorCode = 4;
|
||||
} else if (!Database::isNameFree(AUTOU16TOU8(save->szFirstName), AUTOU16TOU8(save->szLastName))) {
|
||||
errorCode = 1;
|
||||
|
||||
std::string firstName = AUTOU16TOU8(save->szFirstName);
|
||||
std::string lastName = AUTOU16TOU8(save->szLastName);
|
||||
int nameCheck = 0;
|
||||
|
||||
// if FNCode isn't 0, it's a wheel name
|
||||
if (save->iFNCode != 0) {
|
||||
if (!CNLoginServer::isNameWheelNameGood(save->iFNCode, save->iMNCode, save->iLNCode, firstName, lastName)) {
|
||||
errorCode = 4;
|
||||
} else {
|
||||
nameCheck = settings:: APPROVEWHEELNAMES ? 1 : 0;
|
||||
}
|
||||
} else {
|
||||
// custom name
|
||||
nameCheck = settings::APPROVECUSTOMNAMES ? 1 : 0;
|
||||
}
|
||||
|
||||
if (errorCode == 0) {
|
||||
if (!CNLoginServer::isCharacterNameGood(firstName, lastName)) {
|
||||
errorCode = 4;
|
||||
} else if (!Database::isNameFree(firstName, lastName)) {
|
||||
errorCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCode != 0) {
|
||||
@@ -320,12 +332,19 @@ void CNLoginServer::nameSave(CNSocket* sock, CNPacketData* data) {
|
||||
if (!Database::isSlotFree(loginSessions[sock].userID, save->iSlotNum))
|
||||
return invalidCharacter(sock);
|
||||
|
||||
resp.iPC_UID = Database::createCharacter(save, loginSessions[sock].userID);
|
||||
resp.iPC_UID = Database::createCharacter(save->iSlotNum, loginSessions[sock].userID, firstName.c_str(), lastName.c_str(), nameCheck);
|
||||
// if query somehow failed
|
||||
if (resp.iPC_UID == 0) {
|
||||
std::cout << "[WARN] Login Server: Database failed to create new character!" << std::endl;
|
||||
return invalidCharacter(sock);
|
||||
}
|
||||
|
||||
// fire name check event if needed
|
||||
if (nameCheck != 1) {
|
||||
std::string namereq = std::to_string(resp.iPC_UID) + " " + firstName + " " + lastName;
|
||||
Monitor::namereqs.push_back(namereq);
|
||||
}
|
||||
|
||||
resp.iSlotNum = save->iSlotNum;
|
||||
resp.iGender = save->iGender;
|
||||
|
||||
@@ -341,7 +360,9 @@ void CNLoginServer::nameSave(CNSocket* sock, CNPacketData* data) {
|
||||
DEBUGLOG(
|
||||
std::cout << "Login Server: new character created" << std::endl;
|
||||
std::cout << "\tSlot: " << (int)save->iSlotNum << std::endl;
|
||||
std::cout << "\tName: " << AUTOU16TOU8(save->szFirstName) << " " << AUTOU16TOU8(save->szLastName) << std::endl;
|
||||
std::cout << "\tName: " << firstName << " " << lastName;
|
||||
if (nameCheck != 1) std::cout << " (pending approval)";
|
||||
std::cout << std::endl;
|
||||
)
|
||||
}
|
||||
|
||||
@@ -508,11 +529,30 @@ void CNLoginServer::changeName(CNSocket* sock, CNPacketData* data) {
|
||||
auto save = (sP_CL2LS_REQ_CHANGE_CHAR_NAME*)data->buf;
|
||||
|
||||
int errorCode = 0;
|
||||
if (!CNLoginServer::isCharacterNameGood(AUTOU16TOU8(save->szFirstName), AUTOU16TOU8(save->szLastName))) {
|
||||
errorCode = 4;
|
||||
|
||||
std::string firstName = AUTOU16TOU8(save->szFirstName);
|
||||
std::string lastName = AUTOU16TOU8(save->szLastName);
|
||||
int nameCheck = 0;
|
||||
|
||||
// if FNCode isn't 0, it's a wheel name
|
||||
if (save->iFNCode != 0) {
|
||||
if (!CNLoginServer::isNameWheelNameGood(save->iFNCode, save->iMNCode, save->iLNCode, firstName, lastName)) {
|
||||
errorCode = 4;
|
||||
} else {
|
||||
nameCheck = settings::APPROVEWHEELNAMES ? 1 : 0;
|
||||
}
|
||||
} else {
|
||||
// custom name
|
||||
nameCheck = settings::APPROVECUSTOMNAMES ? 1 : 0;
|
||||
}
|
||||
else if (!Database::isNameFree(AUTOU16TOU8(save->szFirstName), AUTOU16TOU8(save->szLastName))) {
|
||||
errorCode = 1;
|
||||
|
||||
if (errorCode == 0) {
|
||||
if (!CNLoginServer::isCharacterNameGood(firstName, lastName)) {
|
||||
errorCode = 4;
|
||||
}
|
||||
else if (!Database::isNameFree(firstName, lastName)) {
|
||||
errorCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCode != 0) {
|
||||
@@ -527,9 +567,15 @@ void CNLoginServer::changeName(CNSocket* sock, CNPacketData* data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Database::changeName(save, loginSessions[sock].userID))
|
||||
if (!Database::changeName(save->iPCUID, loginSessions[sock].userID, firstName.c_str(), lastName.c_str(), nameCheck))
|
||||
return invalidCharacter(sock);
|
||||
|
||||
// fire name check event if needed
|
||||
if (nameCheck != 1) {
|
||||
std::string namereq = std::to_string(save->iPCUID) + " " + firstName + " " + lastName;
|
||||
Monitor::namereqs.push_back(namereq);
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_LS2CL_REP_CHANGE_CHAR_NAME_SUCC, resp);
|
||||
resp.iPC_UID = save->iPCUID;
|
||||
memcpy(resp.szFirstName, save->szFirstName, sizeof(resp.szFirstName));
|
||||
@@ -541,8 +587,10 @@ void CNLoginServer::changeName(CNSocket* sock, CNPacketData* data) {
|
||||
sock->sendPacket(resp, P_LS2CL_REP_CHANGE_CHAR_NAME_SUCC);
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "Login Server: Name check success for character [" << save->iPCUID << "]" << std::endl;
|
||||
std::cout << "\tNew name: " << AUTOU16TOU8(save->szFirstName) << " " << AUTOU16TOU8(save->szLastName) << std::endl;
|
||||
std::cout << "Login Server: Name change request for character [" << save->iPCUID << "]" << std::endl;
|
||||
std::cout << "\tNew name: " << firstName << " " << lastName;
|
||||
if (nameCheck != 1) std::cout << " (pending approval)";
|
||||
std::cout << std::endl;
|
||||
)
|
||||
}
|
||||
|
||||
@@ -621,21 +669,164 @@ bool CNLoginServer::exitDuplicate(int accountId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CNLoginServer::isLoginDataGood(std::string login, std::string password) {
|
||||
std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
|
||||
std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
|
||||
bool CNLoginServer::isUsernameGood(std::string& login) {
|
||||
const std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
|
||||
return (std::regex_match(login, loginRegex));
|
||||
}
|
||||
|
||||
return (std::regex_match(login, loginRegex) && std::regex_match(password, passwordRegex));
|
||||
bool CNLoginServer::isPasswordGood(std::string& password) {
|
||||
const std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
|
||||
return (std::regex_match(password, passwordRegex));
|
||||
}
|
||||
|
||||
bool CNLoginServer::isPasswordCorrect(std::string actualPassword, std::string tryPassword) {
|
||||
return BCrypt::validatePassword(tryPassword, actualPassword);
|
||||
}
|
||||
|
||||
bool CNLoginServer::isNameWheelNameGood(int fnCode, int mnCode, int lnCode, std::string& firstName, std::string& lastName) {
|
||||
if (fnCode >= LoginServer::WheelFirstNames.size()
|
||||
|| mnCode >= LoginServer::WheelMiddleNames.size()
|
||||
|| lnCode >= LoginServer::WheelLastNames.size()) {
|
||||
std::cout << "[WARN] Login Server: Invalid name codes received: " << fnCode << " " << mnCode << " " << lnCode << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// client sends 1 if not selected for these. they point to a single blank space. why.
|
||||
// just change them to 0, which points to an empty string; keeps the code much cleaner
|
||||
if (mnCode == 1) mnCode = 0;
|
||||
if (lnCode == 1) lnCode = 0;
|
||||
|
||||
std::string firstNameFromWheel = LoginServer::WheelFirstNames[fnCode];
|
||||
|
||||
std::string middleNamePart = LoginServer::WheelMiddleNames[mnCode];
|
||||
std::string lastNamePart = LoginServer::WheelLastNames[lnCode];
|
||||
if (mnCode != 0 && middleNamePart[middleNamePart.size() - 1] != ' ') {
|
||||
// If there's a middle name, we need to lowercase the last name
|
||||
std::transform(lastNamePart.begin(), lastNamePart.end(), lastNamePart.begin(), ::tolower);
|
||||
}
|
||||
std::string lastNameFromWheel = middleNamePart + lastNamePart;
|
||||
|
||||
if (firstNameFromWheel.empty() || lastNameFromWheel.empty()) {
|
||||
std::cout << "[WARN] Login Server: Invalid wheel name combo: " << fnCode << " " << mnCode << " " << lnCode << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstName != firstNameFromWheel || lastName != lastNameFromWheel) {
|
||||
std::cout << "[WARN] Login Server: Name wheel mismatch. Expected " << firstNameFromWheel << " " << lastNameFromWheel << ", got " << firstName << " " << lastName << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CNLoginServer::isCharacterNameGood(std::string Firstname, std::string Lastname) {
|
||||
//Allow alphanumeric and dot characters in names(disallows dot and space characters at the beginning of a name)
|
||||
std::regex firstnamecheck(R"(((?! )(?!\.)[a-zA-Z0-9]*\.{0,1}(?!\.+ +)[a-zA-Z0-9]* {0,1}(?! +))*$)");
|
||||
std::regex lastnamecheck(R"(((?! )(?!\.)[a-zA-Z0-9]*\.{0,1}(?!\.+ +)[a-zA-Z0-9]* {0,1}(?! +))*$)");
|
||||
return (std::regex_match(Firstname, firstnamecheck) && std::regex_match(Lastname, lastnamecheck));
|
||||
}
|
||||
|
||||
bool CNLoginServer::isAuthMethodAllowed(AuthMethod authMethod) {
|
||||
// the config file specifies "comma-separated" but tbh we don't care
|
||||
switch (authMethod) {
|
||||
case AuthMethod::PASSWORD:
|
||||
return settings::AUTHMETHODS.find("password") != std::string::npos;
|
||||
case AuthMethod::COOKIE:
|
||||
return settings::AUTHMETHODS.find("cookie") != std::string::npos;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CNLoginServer::checkPassword(CNSocket* sock, std::string& password) {
|
||||
// check password auth allowed
|
||||
if (!CNLoginServer::isAuthMethodAllowed(AuthMethod::PASSWORD)) {
|
||||
// send a custom error message
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
|
||||
std::string text = "Password login disabled\n";
|
||||
text += "This server has disabled logging in with plaintext passwords.\n";
|
||||
text += "Please contact an admin for assistance.";
|
||||
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
|
||||
msg.iDuringTime = 12;
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// check regex
|
||||
if (!CNLoginServer::isPasswordGood(password)) {
|
||||
// send a custom error message
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
|
||||
std::string text = "Invalid password\n";
|
||||
text += "Password has to be 8 - 32 characters long";
|
||||
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
|
||||
msg.iDuringTime = 10;
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CNLoginServer::checkUsername(CNSocket* sock, std::string& username) {
|
||||
// check username regex
|
||||
if (!CNLoginServer::isUsernameGood(username)) {
|
||||
// send a custom error message
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
|
||||
std::string text = "Invalid login\n";
|
||||
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore";
|
||||
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
|
||||
msg.iDuringTime = 10;
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CNLoginServer::checkToken(CNSocket* sock, Database::Account& account, std::string& token, bool isCookieAuth) {
|
||||
// check for valid cookie first
|
||||
if (isCookieAuth && CNLoginServer::isAuthMethodAllowed(AuthMethod::COOKIE)) {
|
||||
const char *cookie = token.c_str();
|
||||
if (Database::checkCookie(account.AccountID, cookie)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// cookie check disabled or failed; check to see if it's a plaintext password
|
||||
if (CNLoginServer::isAuthMethodAllowed(AuthMethod::PASSWORD)
|
||||
&& CNLoginServer::isPasswordCorrect(account.Password, token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CNLoginServer::checkBan(CNSocket* sock, Database::Account& account) {
|
||||
// check if the account is banned
|
||||
if (account.BannedUntil > getTimestamp()) {
|
||||
// send a custom error message
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
|
||||
|
||||
// ceiling devision
|
||||
int64_t remainingDays = (account.BannedUntil-getTimestamp()) / 86400 + ((account.BannedUntil - getTimestamp()) % 86400 != 0);
|
||||
|
||||
std::string text = "Your account has been banned. \nReason: ";
|
||||
text += account.BanReason;
|
||||
text += "\nBan expires in " + std::to_string(remainingDays) + " day";
|
||||
if (remainingDays > 1)
|
||||
text += "s";
|
||||
|
||||
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
|
||||
msg.iDuringTime = 99999999;
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#pragma endregion
|
||||
|
@@ -1,11 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "db/Database.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace LoginServer {
|
||||
extern std::vector<std::string> WheelFirstNames;
|
||||
extern std::vector<std::string> WheelMiddleNames;
|
||||
extern std::vector<std::string> WheelLastNames;
|
||||
}
|
||||
|
||||
struct CNLoginData {
|
||||
int userID;
|
||||
time_t lastHeartbeat;
|
||||
@@ -23,6 +30,13 @@ enum class LoginError {
|
||||
UPDATED_EUALA_REQUIRED = 9
|
||||
};
|
||||
|
||||
#define USE_COOKIE_FIELDS 2
|
||||
|
||||
enum class AuthMethod {
|
||||
PASSWORD = 1,
|
||||
COOKIE = 2
|
||||
};
|
||||
|
||||
// WARNING: THERE CAN ONLY BE ONE OF THESE SERVERS AT A TIME!!!!!! TODO: change loginSessions & packet handlers to be non-static
|
||||
class CNLoginServer : public CNServer {
|
||||
private:
|
||||
@@ -39,10 +53,18 @@ private:
|
||||
static void changeName(CNSocket* sock, CNPacketData* data);
|
||||
static void duplicateExit(CNSocket* sock, CNPacketData* data);
|
||||
|
||||
static bool isLoginDataGood(std::string login, std::string password);
|
||||
static bool isUsernameGood(std::string& login);
|
||||
static bool isPasswordGood(std::string& password);
|
||||
static bool isPasswordCorrect(std::string actualPassword, std::string tryPassword);
|
||||
static bool isAccountInUse(int accountId);
|
||||
static bool isNameWheelNameGood(int fnCode, int mnCode, int lnCode, std::string& firstName, std::string& lastName);
|
||||
static bool isCharacterNameGood(std::string Firstname, std::string Lastname);
|
||||
static bool isAuthMethodAllowed(AuthMethod authMethod);
|
||||
static bool checkUsername(CNSocket* sock, std::string& username);
|
||||
static bool checkPassword(CNSocket* sock, std::string& password);
|
||||
static bool checkToken(CNSocket* sock, Database::Account& account, std::string& token, bool isCookieAuth);
|
||||
static bool checkBan(CNSocket* sock, Database::Account& account);
|
||||
|
||||
static void newAccount(CNSocket* sock, std::string userLogin, std::string userPassword, int32_t clientVerC);
|
||||
// returns true if success
|
||||
static bool exitDuplicate(int accountId);
|
||||
|
@@ -14,6 +14,13 @@ static std::mutex sockLock; // guards socket list
|
||||
static std::list<SOCKET> sockets;
|
||||
static sockaddr_in address;
|
||||
|
||||
std::vector<std::string> Monitor::chats;
|
||||
std::vector<std::string> Monitor::bcasts;
|
||||
std::vector<std::string> Monitor::emails;
|
||||
std::vector<std::string> Monitor::namereqs;
|
||||
|
||||
using namespace Monitor;
|
||||
|
||||
static bool transmit(std::list<SOCKET>::iterator& it, char *buff, int len) {
|
||||
int n = 0;
|
||||
int sock = *it;
|
||||
@@ -99,15 +106,23 @@ outer:
|
||||
}
|
||||
|
||||
// chat
|
||||
for (auto& str : Chat::dump) {
|
||||
for (auto& str : chats) {
|
||||
n = std::snprintf(buff, sizeof(buff), "chat %s\n", str.c_str());
|
||||
|
||||
if (!transmit(it, buff, n))
|
||||
goto outer;
|
||||
}
|
||||
|
||||
// announcements
|
||||
for (auto& str : bcasts) {
|
||||
n = std::snprintf(buff, sizeof(buff), "bcast %s\n", str.c_str());
|
||||
|
||||
if (!transmit(it, buff, n))
|
||||
goto outer;
|
||||
}
|
||||
|
||||
// emails
|
||||
for (auto& str : Email::dump) {
|
||||
for (auto& str : emails) {
|
||||
n = process_email(buff, str);
|
||||
|
||||
if (!transmit(it, buff, n))
|
||||
@@ -117,14 +132,24 @@ outer:
|
||||
goto outer;
|
||||
}
|
||||
|
||||
// name requests
|
||||
for (auto& str : namereqs) {
|
||||
n = std::snprintf(buff, sizeof(buff), "namereq %s\n", str.c_str());
|
||||
|
||||
if (!transmit(it, buff, n))
|
||||
goto outer;
|
||||
}
|
||||
|
||||
if (!transmit(it, (char*)"end\n", 4))
|
||||
continue;
|
||||
|
||||
it++;
|
||||
}
|
||||
|
||||
Chat::dump.clear();
|
||||
Email::dump.clear();
|
||||
chats.clear();
|
||||
bcasts.clear();
|
||||
emails.clear();
|
||||
namereqs.clear();
|
||||
}
|
||||
|
||||
bool Monitor::acceptConnection(SOCKET fd, uint16_t revents) {
|
||||
@@ -180,9 +205,14 @@ SOCKET Monitor::init() {
|
||||
}
|
||||
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
address.sin_port = htons(settings::MONITORPORT);
|
||||
|
||||
if (!inet_pton(AF_INET, settings::MONITORLISTENIP.c_str(), &address.sin_addr)) {
|
||||
std::cout << "Failed to set monitor listen address" << std::endl;
|
||||
printSocketError("inet_pton");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (SOCKETERROR(bind(listener, (struct sockaddr*)&address, sizeof(address)))) {
|
||||
std::cout << "Failed to bind to monitor port" << std::endl;
|
||||
printSocketError("bind");
|
||||
@@ -206,7 +236,7 @@ SOCKET Monitor::init() {
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
std::cout << "Monitor listening on *:" << settings::MONITORPORT << std::endl;
|
||||
std::cout << "Monitor listening on " << settings::MONITORLISTENIP << ":" << settings::MONITORPORT << std::endl;
|
||||
|
||||
REGISTER_SHARD_TIMER(tick, settings::MONITORINTERVAL);
|
||||
|
||||
|
@@ -3,6 +3,11 @@
|
||||
#include "core/Core.hpp"
|
||||
|
||||
namespace Monitor {
|
||||
extern std::vector<std::string> chats;
|
||||
extern std::vector<std::string> bcasts;
|
||||
extern std::vector<std::string> emails;
|
||||
extern std::vector<std::string> namereqs;
|
||||
|
||||
SOCKET init();
|
||||
bool acceptConnection(SOCKET, uint16_t);
|
||||
};
|
||||
|
@@ -9,10 +9,13 @@
|
||||
// defaults :)
|
||||
int settings::VERBOSITY = 1;
|
||||
bool settings::SANDBOX = true;
|
||||
std::string settings::SANDBOXEXTRAPATH = "";
|
||||
|
||||
int settings::LOGINPORT = 23000;
|
||||
bool settings::APPROVEALLNAMES = true;
|
||||
bool settings::APPROVEWHEELNAMES = true;
|
||||
bool settings::APPROVECUSTOMNAMES = true;
|
||||
bool settings::AUTOCREATEACCOUNTS = true;
|
||||
std::string settings::AUTHMETHODS = "password";
|
||||
int settings::DBSAVEINTERVAL = 240;
|
||||
|
||||
int settings::SHARDPORT = 23001;
|
||||
@@ -62,11 +65,15 @@ bool settings::DISABLEFIRSTUSEFLAG = true;
|
||||
// monitor settings
|
||||
bool settings::MONITORENABLED = false;
|
||||
int settings::MONITORPORT = 8003;
|
||||
std::string settings::MONITORLISTENIP = "127.0.0.1";
|
||||
int settings::MONITORINTERVAL = 5000;
|
||||
|
||||
// event mode settings
|
||||
int settings::EVENTMODE = 0;
|
||||
|
||||
// race settings
|
||||
bool settings::IZRACESCORECAPPED = true;
|
||||
|
||||
void settings::init() {
|
||||
INIReader reader("config.ini");
|
||||
|
||||
@@ -81,9 +88,12 @@ void settings::init() {
|
||||
|
||||
VERBOSITY = reader.GetInteger("", "verbosity", VERBOSITY);
|
||||
SANDBOX = reader.GetBoolean("", "sandbox", SANDBOX);
|
||||
SANDBOXEXTRAPATH = reader.Get("", "sandboxextrapath", SANDBOXEXTRAPATH);
|
||||
LOGINPORT = reader.GetInteger("login", "port", LOGINPORT);
|
||||
APPROVEALLNAMES = reader.GetBoolean("login", "acceptallcustomnames", APPROVEALLNAMES);
|
||||
APPROVEWHEELNAMES = reader.GetBoolean("login", "acceptallwheelnames", APPROVEWHEELNAMES);
|
||||
APPROVECUSTOMNAMES = reader.GetBoolean("login", "acceptallcustomnames", APPROVECUSTOMNAMES);
|
||||
AUTOCREATEACCOUNTS = reader.GetBoolean("login", "autocreateaccounts", AUTOCREATEACCOUNTS);
|
||||
AUTHMETHODS = reader.Get("login", "authmethods", AUTHMETHODS);
|
||||
DBSAVEINTERVAL = reader.GetInteger("login", "dbsaveinterval", DBSAVEINTERVAL);
|
||||
SHARDPORT = reader.GetInteger("shard", "port", SHARDPORT);
|
||||
SHARDSERVERIP = reader.Get("shard", "ip", SHARDSERVERIP);
|
||||
@@ -111,7 +121,9 @@ 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);
|
||||
MONITORLISTENIP = reader.Get("monitor", "listenip", MONITORLISTENIP);
|
||||
MONITORINTERVAL = reader.GetInteger("monitor", "interval", MONITORINTERVAL);
|
||||
}
|
||||
|
@@ -1,13 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <time.h>
|
||||
|
||||
namespace settings {
|
||||
extern int VERBOSITY;
|
||||
extern bool SANDBOX;
|
||||
extern std::string SANDBOXEXTRAPATH;
|
||||
extern int LOGINPORT;
|
||||
extern bool APPROVEALLNAMES;
|
||||
extern bool APPROVEWHEELNAMES;
|
||||
extern bool APPROVECUSTOMNAMES;
|
||||
extern bool AUTOCREATEACCOUNTS;
|
||||
extern std::string AUTHMETHODS;
|
||||
extern int DBSAVEINTERVAL;
|
||||
extern int SHARDPORT;
|
||||
extern std::string SHARDSERVERIP;
|
||||
@@ -36,8 +41,10 @@ namespace settings {
|
||||
extern int EVENTMODE;
|
||||
extern bool MONITORENABLED;
|
||||
extern int MONITORPORT;
|
||||
extern std::string MONITORLISTENIP;
|
||||
extern int MONITORINTERVAL;
|
||||
extern bool DISABLEFIRSTUSEFLAG;
|
||||
extern bool IZRACESCORECAPPED;
|
||||
|
||||
void init();
|
||||
}
|
||||
|
2
tdata
2
tdata
Submodule tdata updated: cc65dbb402...bdb611b092
14838
vendor/JSON.hpp
vendored
14838
vendor/JSON.hpp
vendored
File diff suppressed because it is too large
Load Diff
14
vendor/bcrypt/bcrypt.c
vendored
14
vendor/bcrypt/bcrypt.c
vendored
@@ -22,9 +22,14 @@
|
||||
#endif
|
||||
#include <errno.h>
|
||||
|
||||
#ifdef _WIN32 || _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
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
// On windows we need to generate random bytes differently.
|
||||
#define BCRYPT_HASHSIZE 60
|
||||
|
||||
#include "bcrypt.h"
|
||||
@@ -33,9 +38,10 @@ typedef __int64 ssize_t;
|
||||
#include <wincrypt.h> /* CryptAcquireContext, CryptGenRandom */
|
||||
#else
|
||||
#include "bcrypt.h"
|
||||
#include "ow-crypt.h"
|
||||
#endif
|
||||
|
||||
#include "ow-crypt.h"
|
||||
|
||||
#define RANDBYTES (16)
|
||||
|
||||
static int try_close(int fd)
|
||||
@@ -117,7 +123,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);
|
||||
|
52
win-setup.ps1
Normal file
52
win-setup.ps1
Normal file
@@ -0,0 +1,52 @@
|
||||
# Run this first if needed:
|
||||
# Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
|
||||
param (
|
||||
# height of largest column without top bar
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$protocolVersion
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# check for vscmd
|
||||
if ([string]::IsNullOrEmpty($env:VSCMD_VER)) {
|
||||
Write-Host 'Must be run inside of VS Developer Powershell'
|
||||
exit 1
|
||||
}
|
||||
|
||||
# check for git
|
||||
try {
|
||||
$git_version = git --version
|
||||
} catch {
|
||||
Write-Host 'git not installed'
|
||||
exit 1
|
||||
}
|
||||
|
||||
# setup vcpkg
|
||||
if (Test-Path -Path 'vcpkg\') {
|
||||
Write-Host 'vcpkg already setup'
|
||||
} else {
|
||||
Write-Host 'Setting up vcpkg...'
|
||||
git clone "https://github.com/microsoft/vcpkg.git"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path 'vcpkg\vcpkg.exe')) {
|
||||
Write-Host 'Bootstrapping vcpkg...'
|
||||
Start-Process -Wait -NoNewWindow -FilePath 'vcpkg\bootstrap-vcpkg.bat' -ArgumentList '-disableMetrics'
|
||||
}
|
||||
|
||||
$env:VCPKG_ROOT='' # ignore msvc's vcpkg root, it doesn't work
|
||||
$vcpkg = (Resolve-Path -Path '.\vcpkg')
|
||||
Write-Host "vcpkg installed to $vcpkg"
|
||||
Start-Process -Wait -NoNewWindow -FilePath 'vcpkg\vcpkg.exe' -ArgumentList 'install sqlite3:x64-windows'
|
||||
|
||||
# setup cmake project
|
||||
if (Test-Path -Path 'build\') {
|
||||
Write-Host 'cmake project already setup';
|
||||
} else {
|
||||
Write-Host 'Setting up cmake project...'
|
||||
Start-Process -Wait -NoNewWindow -FilePath 'cmake' -ArgumentList "-B build -DPROTOCOL_VERSION=$protocolVersion -DCMAKE_TOOLCHAIN_FILE=$vcpkg\scripts\buildsystems\vcpkg.cmake"
|
||||
}
|
||||
|
||||
Write-Host 'Done!'
|
||||
$sln = (Resolve-Path -Path '.\build\OpenFusion.sln')
|
||||
Write-Host "Solution file is at $sln"
|
Reference in New Issue
Block a user