68 Commits

Author SHA1 Message Date
CakeLancelot
06d1ccc17e Update tdata reference 2026-03-25 12:11:07 -05:00
FinnHornhoover
113bc0bc1b Nanocom Boosters and Authentic FM-Taro Scale Logic (#315)
* Groundwork for nanocom boosters

* The item use handler now has a switch for multiple item types (currently gumballs, and a stub for boosters)
* All item types are now checked for expiration, not just vehicles

* implement nanocom booster helpers, save and expiry

* implement authentic taro and fm modfication

* magic number and code refactor

* make sure only close by group members are counted

* add safe taro fm handling, rate command, race and mission booster logic

* add config option to disable authentic group scaling

* rename for consistency

* make rates percentages, fix chat message, add config options

* add config option to the ini file

* add index guard for hasBoost functions

* reorder config ini options

* add bank item expiry option

* fix trade oversight

---------

Co-authored-by: CakeLancelot <CakeLancelot@users.noreply.github.com>
2026-03-25 12:09:40 -05:00
9a62ec61c9 Set TCP_NODELAY on clients 2026-03-19 22:00:29 -07:00
FinnHornhoover
51d3cfbb3b Add config option to include drop fixes (#313)
* add config option to include drop fixes easier

* amend comment and rename fix directories

* check for 1013 before applying 1013 fixes
2026-02-07 17:14:44 -06:00
CakeLancelot
8df263a64d Update tdata reference - Mob Overhaul 2.0 2026-02-07 17:14:05 -06:00
572df90645 Fix email attachment collisions causing email send failure (#312) 2025-12-07 16:38:46 -08:00
FinnHornhoover
8d0587dd45 increase mob group offset to avoid collisions (#311) 2025-12-06 16:31:27 -08:00
Erik
0078be8e9a Update Windows Github Actions runner from 2019 to 2022 (#309)
* Attempt to update Windows action runner to 2022

* Fix VS path
2025-07-24 01:27:27 -05:00
b617456aa1 Include tabledata in CI builds 2025-02-11 17:30:35 -08:00
935ee1bf6f CI fixes 2025-02-11 17:19:27 -08:00
43a2504357 Update Dockerfile, license, and readme 2025-02-10 18:43:01 -08:00
ca196bf620 Add windows dev setup script 2025-01-30 21:33:05 -08:00
6b9ae4c325 Validate name wheel names 2025-01-06 23:43:37 -05:00
d06c324aa3 Send namereq event on name change as well 2025-01-05 22:30:55 -05:00
052196d1cd Treat bcast areas 1 and 2 as global 2025-01-02 18:26:00 -05:00
e84f6505b8 Name request monitor events 2025-01-02 18:06:06 -05:00
b483bf7190 Respect name check flag in getPlayerName 2025-01-02 17:44:37 -05:00
6ff51685a8 Move event buffers to Monitor 2025-01-02 17:39:16 -05:00
b4ed31d4fb Dedicated bcast monitor event 2025-01-02 17:18:53 -05:00
36e0667ed2 Add Email and LastPasswordReset columns to Account table (#299)
* Add Email column to Account table

* Add LastPasswordReset timestamp column, missing DB version bump
2024-11-28 23:01:29 -05:00
ed9fe61faf Auth cookie refresh on PC_EXIT 2024-11-23 11:29:55 -08:00
55cf3f7102 Refactor login packet handler for more flexible auth (#298)
This PR enables auth cookies to be used simultaneously with plaintext paasswords sent in the cookie authID field.

* Hoist a bunch of checks from the login packet handler into helper functions.
* Rename the LoginType enum to AuthMethod and distinguish it from the iLoginType packet field (see comment in code for why these should be decoupled).
* If the provided token does not pass the cookie check and password auth is enabled, treat it as a plaintext password and authenticate if it is correct.
2024-11-17 05:21:37 +01:00
1543dac4e0 BUILD - Optimize Dockerfile by using alpine as base image (#277) 2024-10-28 21:26:25 -07:00
ae327cc104 Use FE2CL_..._AROUND, _AROUND_DEL packets (#295)
* Use FE2CL_..._AROUND, _AROUND_DEL packets
* Use increased buffer size for 728 and 1013 protocols
2024-10-28 20:49:34 -07:00
6ffde9bb44 Replace most usages of CN_PACKET_BUFFER_SIZE with usable body size 2024-10-28 20:39:25 -07:00
8568fd1c46 Restore the check that makes sure mob paths start from their spawn point
This was added in 599bbedd and accidentally removed during the TableData
refactor in c960b062.
2024-10-19 04:26:09 +02:00
05a5303522 Fix one-off mobs respawning if their regenTime is 0 2024-10-19 04:25:08 +02:00
3365cb53b7 Only listen for monitor connections on localhost by default
This is to prevent accidental exposure of the monitor port to the public
internet if a server admin enables the monitor port without it being
properly firewalled. There is now a config option that lets you override
the address to bind to, so that it can still be made available to other
machines over private networks such as Wireguard.
2024-10-17 01:04:50 +02:00
5e92a58134 Print server types when starting servers
Should have done this back when I added serverType.
2024-10-17 01:04:50 +02:00
94064e1865 [sandbox] Print error message on seccomp sandbox violation
Co-authored-by: cpunch <sethtstubbs@gmail.com>
2024-10-17 01:04:50 +02:00
5e73ff272d [sandbox] Add make target for building without Landlock 2024-10-17 01:04:47 +02:00
197ccad0eb [sandbox] Landlock support
* Support disabling Landlock at compile time or runtime if unsupported,
  without disabling seccomp
* Support older Landlock ABI versions
* Support an extra arbitrary RW path, inteded for the coredump dir
* Support database locations other than the working directory
2024-10-17 01:03:06 +02:00
CakeLancelot
68b56e7c25 Docker: disable sandbox to fix crashes and update Dockerfile/compose.yml (#294)
Additionally:
* Add EXPOSE hints to Dockerfile
* as -> AS in Dockerfile to resolve warning
* Point docker-compose to our docker hub image
* Remove version property in docker-compose.yml as it was deprecated
2024-10-15 01:00:37 -05:00
CakeLancelot
cada1bcfd8 Update check-builds.yaml
* Install SQLite3 headers as they arent included in the Ubuntu 24.04 image (only includes CLI currently): https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md
* Change copy-artifacts task to also use ubuntu-latest
2024-10-14 22:48:41 -05:00
Juansecu
ca43a2996a BUILD - Fix build of Docker image for different archs (#293) 2024-10-14 17:22:45 -05:00
7c66041a6f Add make target for building without the sandbox 2024-10-12 15:42:26 +02:00
2c822e210b [bcrypt] Fix missing include on Windows
Co-authored-by: Jade Shrinemaiden <jadeshrinemaiden@gmail.com>
2024-10-12 15:15:36 +02:00
Juansecu
352fa8a133 BUILD - Expose ports used by the application server in Dockerfile 2024-10-09 19:13:54 -05:00
CakeLancelot
c116794c83 README updates
Create dockerhub pull badge
Change links from wiki to website
2024-10-08 22:00:45 -05:00
CakeLancelot
4ebda6066c Docker: add manual workflow dispatch and auto install QEMU 2024-10-08 21:52:05 -05:00
6de21277d6 Fix eruption attacks cancelling when no targets are in range 2024-10-08 19:18:00 -07:00
CakeLancelot
397700e909 Update version numbers in README 2024-10-06 23:26:36 -05:00
d9b6aedd5b Fix nested ifdefs for windows 2024-10-06 20:58:20 -07:00
CakeLancelot
145113062b Update tdata reference to fix invalid label 2024-10-05 16:51:39 -05:00
d717c5d74d Update tdata reference for the car paths 2024-09-22 04:28:28 +02:00
a6eb0e2349 Auth Cookie Support (#285)
* Auth cookie support

* Add config option for auth cookie support

* Safe handling of TEGid/auth_id strings

* Fix bad size calculation due to pointer cast

* Expiration timestamp instead of valid bit

* Change setting to "allowed auth methods"

This allows plaintext password auth to be disabled altogether

* PR feedback
2024-09-17 20:41:48 -07:00
52833f7fb3 Fix CI/CD 2024-09-05 13:43:57 -04:00
CakeLancelot
3aed24de26 Update download links in Getting Started 2024-07-14 18:52:38 -05:00
Gent Semaj
17362b2ea6 Client synchronization improvements (#283)
* Client synchronization improvements

* Remove bad comment

* Remove guard on PC_TICK

* Fix delayed loading of nano skill icons

We actually don't need to wait for post-load to do the second nano book send.
That adds unnecessary delay. Moving it to right after `P_FE2CL_REP_PC_ENTER_SUCC`
does the trick and gives the client plenty of time to fetch the icons before
loading in-game.

* Don't send unnecessary nano book subsets pre-enter

* Fix comment
2024-06-27 08:19:54 -05:00
gsemaj
47dbc6d35e Notify if player must log out and back in for access change 2024-06-23 18:41:43 -07:00
Gent Semaj
b780f5ee60 Enable account level changing at runtime (#282)
* Enable account level change at runtime

* PR feedback
2024-06-23 20:25:46 -05:00
Gent Semaj
003186d97a Immediate email notifications (#281) 2024-06-23 10:32:22 -05:00
Gent Semaj
6d2f120305 Add missing trade offer packets (#280) 2024-06-22 18:20:59 -05:00
Juansecu
2096c3c3cc BUILD - Optimize Dockerfile by using alpine as base image 2024-06-08 21:55:03 -05:00
Juansecu
51615db230 CD - Add GitHub Action to push Docker image to Docker Hub (#275)
* CD - Add GitHub Action to push Docker image to Docker Hub

* BUILD - Reduce size of image by using slim version of Debian

* CI - Modify Push Docker Image action to build and push Docker image to linux/amd64 and linux/arm64 architectures
2024-05-15 10:17:33 -05:00
CakeLancelot
233d21ecd7 Fix copy/paste error in docker-compose.yml 2024-05-07 23:01:09 -05:00
Gent Semaj
54327b0c23 Docker improvements (#274) 2024-05-07 22:11:14 -05:00
CakeLancelot
fa8c1e73d1 Fix a few compiler warnings and formatting for DB startup message (#272)
Note that the warning in TableData.cpp only seems to occur on clang (deprecated-declarations)

Database account/player count message:
Before: [INFO] Database in operation : Found 1 account(s) and 2 player(s)s
After: [INFO] Database in operation: Found 1 account and 2 players
2024-03-31 13:02:39 -05:00
CakeLancelot
aeac57ebf7 Remove duplicate headers from Makefile 2024-03-31 02:48:18 -05:00
CakeLancelot
632406e93b Bump nlohmann JSON to 3.11.3 (#271)
I've checked all the changelogs from the version we were using (3.9.1) to the latest, and there should not be any breaking changes or deprecated functionality.
The server also compiled fine and I did not encounter any issues after a brief play session.
2024-03-29 21:50:55 -05:00
837f109752 Add missing mutex 2024-02-03 03:13:43 -05:00
c11cfebdb1 Fix account and player counts on startup 2024-02-03 03:13:24 -05:00
20367d77f0 Bump copyright date 2024-02-02 22:01:38 -05:00
8d04f31c61 Default PROTOCOL_VERSION to 104 2024-02-02 21:58:37 -05:00
FinnHornhoover
44560a46b7 Rank Calculation Out-of-Bounds Fix (#268) 2023-12-24 02:49:51 +01:00
gsemaj
21d280147c Shard crash fixes 2023-12-19 13:23:58 -05:00
FinnHornhoover
b765821552 added option to disable score capping 2023-12-19 13:18:43 -05:00
e61682dfb2 fix: out-of-bounds index in itemDeleteHandler() 2023-12-18 04:41:53 -06:00
80 changed files with 9314 additions and 8409 deletions

View File

@@ -1 +0,0 @@
version.h

View File

@@ -8,6 +8,7 @@ on:
- .github/workflows/check-builds.yaml
- CMakeLists.txt
- Makefile
- tdata
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
paths:
@@ -19,7 +20,7 @@ on:
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
@@ -29,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"
@@ -48,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: '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
@@ -73,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
@@ -100,20 +102,21 @@ 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'
runs-on: ubuntu-22.04
if: github.event_name != 'pull_request' && (github.ref_type == 'tag' || github.ref_name == 'master')
runs-on: ubuntu-latest
needs: [windows-build, ubuntu-build]
env:
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}
@@ -121,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
View 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

3
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "tdata"]
path = tdata
url = https://github.com/OpenFusionProject/tabledata.git
[submodule "vendor/Lua"]
path = vendor/Lua
url = https://github.com/walterschell/Lua.git

View File

@@ -34,28 +34,20 @@ else()
set(BIN_NAME fusion)
endif()
# add lua
option(LUA_BUILD_COMPILER OFF)
option(LUA_ENABLE_SHARED OFF)
option(LUA_ENABLE_TESTING OFF)
add_subdirectory(vendor/Lua)
include_directories(src vendor)
file(GLOB_RECURSE VENDOR_SOURCES vendor/bcrypt/**.[ch] vendor/mingw/**.h vendor/INIReader.hpp vendor/JSON.hpp)
file(GLOB_RECURSE SOURCES src/**.[ch]pp version.h)
file(GLOB_RECURSE SOURCES src/**.[ch]pp vendor/**.[ch]pp vendor/**.[ch] version.h)
configure_file(version.h.in ${CMAKE_SOURCE_DIR}/version.h @ONLY)
add_executable(openfusion ${SOURCES} ${VENDOR_SOURCES})
add_executable(openfusion ${SOURCES})
set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME})
# find sqlite3 and use it
find_package(SQLite3 REQUIRED)
target_include_directories(openfusion PRIVATE ${SQLite3_INCLUDE_DIRS})
target_link_libraries(openfusion PRIVATE lua_static ${SQLite3_LIBRARIES})
target_link_libraries(openfusion PRIVATE ${SQLite3_LIBRARIES})
# Makes it so config, tdata, etc. get picked up when starting via the debugger in VS
set_property(TARGET openfusion PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,34 +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.5/OpenFusionClient-1.5-Installer.exe) - choose to run the file.
2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
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.5/OpenFusionClient-1.5.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.5-Original.zip` or `OpenFusionServer-1.5-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.5).
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.
@@ -79,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
@@ -107,4 +108,4 @@ Meanwhile the Academy server is more meant for legitimate playthroughs (default
When hosting a local server, you will have access to all commands by default (account level 1).
For a list of available commands, see [this wiki page](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/).

View File

@@ -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
@@ -37,6 +43,33 @@ simulatemobs=true
# little message players see when they enter the game
motd=Welcome to OpenFusion!
# Should drop fixes be enabled?
# This will add drops to (mostly Academy-specific) mobs that don't have drops
# and rearrange drop tables that are either unassigned or stranded in difficult to reach mobs
# e.g. Hyper Fusionfly and Fusion Numbuh Four drops will become more accessible.
# This is a polish option that is slightly inauthentic to the original game.
#dropfixesenabled=true
# Should groups have to divide up gained Taros / FM among themselves?
# Taros is divided up, FM gets diminished per group member, roughly -12.5% per group member
# Original game worked like this. Uncomment below to disable this behavior.
#lesstarofmingroupdisabled=true
# General reward percentages
# You can change the rate of taro and fusion matter gains for all players.
# The numbers are in percentages, i.e. 1000 is 1000%. You should only use whole numbers, no decimals.
# Uncomment and change below to your desired rate. Defaults are 100%, regular gain rates.
#tarorate=100
#fusionmatterrate=100
# Should expired items in the bank disappear automatically?
# Original game let you kep expired items in the bank until you take them out.
# Uncomment below to enable this behavior.
#removeexpireditemsfrombank=true
# Should there be a score cap for infected zone races?
#izracescorecapped=true
# The following are the default locations of the JSON files the server
# requires to run. You can override them by changing their values and
# uncommenting them (removing the leading # character from that line).
@@ -95,5 +128,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

View File

@@ -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
View 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
View 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;

View File

@@ -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)
);

View File

@@ -170,11 +170,11 @@ static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombata
if(!blocked) {
boostDrain = (int)(skill->values[0][power] * scalingFactor);
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
plr->batteryW -= boostDrain;
plr->subtractCapped(CappedValueType::BATTERY_W, boostDrain);
potionDrain = (int)(skill->values[1][power] * scalingFactor);
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
plr->batteryN -= potionDrain;
plr->subtractCapped(CappedValueType::BATTERY_N, potionDrain);
}
sSkillResult_BatteryDrain result{};
@@ -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
View 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;
}
};

View File

@@ -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));
@@ -278,15 +278,6 @@ static void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) {
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);
for (int slot = 0; slot < 50; slot++) {

View File

@@ -6,5 +6,5 @@ namespace Buddies {
void init();
// Buddy list
void refreshBuddyList(CNSocket* sock);
void sendBuddyList(CNSocket* sock);
}

View File

@@ -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;

View File

@@ -75,30 +75,22 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
case CN_GM_SET_VALUE_TYPE__HP:
response.iSetValue = plr->HP = setData->iSetValue;
break;
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY :
plr->batteryW = setData->iSetValue;
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY:
plr->setCapped(CappedValueType::BATTERY_W, setData->iSetValue);
response.iSetValue = plr->batteryW;
break;
case CN_GM_SET_VALUE_TYPE__NANO_BATTERY:
plr->batteryN = setData->iSetValue;
// caps
if (plr->batteryN > 9999)
plr->batteryN = 9999;
plr->setCapped(CappedValueType::BATTERY_N, setData->iSetValue);
response.iSetValue = plr->batteryN;
break;
case CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
plr->setCapped(CappedValueType::FUSIONMATTER, setData->iSetValue);
Missions::updateFusionMatter(sock);
response.iSetValue = plr->fusionmatter;
break;
case CN_GM_SET_VALUE_TYPE__CANDY:
response.iSetValue = plr->money = setData->iSetValue;
plr->setCapped(CappedValueType::TAROS, setData->iSetValue);
response.iSetValue = plr->money;
break;
case CN_GM_SET_VALUE_TYPE__SPEED:
case CN_GM_SET_VALUE_TYPE__JUMP:
@@ -148,6 +140,60 @@ static void setGMSpecialOnOff(CNSocket *sock, CNPacketData *data) {
// this is only used for muting players, so no need to update the client since that logic is server-side
}
static void setGMRewardRate(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
// access check
if (plr->accountLevel > 30)
return;
auto req = (sP_CL2FE_GM_REQ_REWARD_RATE*)data->buf;
if (req->iGetSet != 0) {
double *rate = nullptr;
switch (req->iRewardType) {
case REWARD_TYPE_TAROS:
rate = plr->rateT;
break;
case REWARD_TYPE_FUSIONMATTER:
rate = plr->rateF;
break;
}
if (rate == nullptr) {
std::cout << "[WARN] Invalid reward type for setGMRewardRate(): " << req->iRewardType << std::endl;
return;
}
if (req->iSetRateValue < 0 || req->iSetRateValue > 1000) {
std::cout << "[WARN] Invalid rate value for setGMRewardRate(): " << req->iSetRateValue << " (must be between 0 and 1000)" << std::endl;
return;
}
switch (req->iRewardRateIndex) {
case RATE_SLOT_ALL:
for (int i = 0; i < 5; i++)
rate[i] = req->iSetRateValue / 100.0;
break;
case RATE_SLOT_COMBAT:
case RATE_SLOT_MISSION:
case RATE_SLOT_EGG:
case RATE_SLOT_RACING:
rate[req->iRewardRateIndex] = req->iSetRateValue / 100.0;
break;
}
}
INITSTRUCT(sP_FE2CL_GM_REP_REWARD_RATE_SUCC, resp);
for (int i = 0; i < 5; i++) {
// double to float
resp.afRewardRate_Taros[i] = plr->rateT[i];
resp.afRewardRate_FusionMatter[i] = plr->rateF[i];
}
sock->sendPacket(resp, P_FE2CL_GM_REP_REWARD_RATE_SUCC);
}
static void locatePlayer(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
@@ -371,6 +417,7 @@ void BuiltinCommands::init() {
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH, setGMSpecialSwitchPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF, setGMSpecialOnOff);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_REWARD_RATE, setGMRewardRate);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_LOCATION, locatePlayer);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_KICK_PLAYER, kickPlayer);

View File

@@ -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);

View File

@@ -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

View File

@@ -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) {

View File

@@ -441,11 +441,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
if (plr->batteryW >= 6 + difficulty)
plr->batteryW -= 6 + difficulty;
else
plr->batteryW = 0;
plr->subtractCapped(CappedValueType::BATTERY_W, 6 + difficulty);
damage.first = mob->takeDamage(sock, damage.first);
respdata[i].iID = mob->id;
@@ -539,9 +535,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 +629,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));
@@ -690,10 +686,7 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
}
if (plr->batteryW >= 6 + plr->level)
plr->batteryW -= 6 + plr->level;
else
plr->batteryW = 0;
plr->subtractCapped(CappedValueType::BATTERY_W, 6 + plr->level);
damage.first = target->takeDamage(sock, damage.first);
@@ -742,7 +735,7 @@ static int8_t addBullet(Player* plr, bool isGrenade) {
toAdd.weaponBoost = plr->batteryW > 0;
if (toAdd.weaponBoost) {
int boostCost = Rand::rand(11) + 20;
plr->batteryW = boostCost > plr->batteryW ? 0 : plr->batteryW - boostCost;
plr->subtractCapped(CappedValueType::BATTERY_W, boostCost);
}
Bullets[plr->iID][findId] = toAdd;
@@ -847,9 +840,9 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
*/
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));

View File

@@ -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) {
@@ -1200,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");

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
@@ -76,7 +75,7 @@ static void emailReceiveTaros(CNSocket* sock, CNPacketData* data) {
Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex);
// money transfer
plr->money += email.Taros;
plr->addCapped(CappedValueType::TAROS, email.Taros);
email.Taros = 0;
// update Taros in email
Database::updateEmailContent(&email);
@@ -275,7 +274,7 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
}
int cost = pkt->iCash + 50 + 20 * attachments.size(); // attached taros + postage
plr->money -= cost;
plr->subtractCapped(CappedValueType::TAROS, cost);
Database::EmailData email = {
(int)pkt->iTo_PCUID, // PlayerId
Database::getNextEmailIndex(pkt->iTo_PCUID), // MsgIndex
@@ -292,7 +291,7 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
};
if (!Database::sendEmail(&email, attachments, plr)) {
plr->money += cost; // give money back
plr->addCapped(CappedValueType::TAROS, cost); // give money back
// give items back
while (!attachments.empty()) {
sItemBase attachment = attachments.back();
@@ -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() {

View File

@@ -4,7 +4,5 @@
#include <string>
namespace Email {
extern std::vector<std::string> dump;
void init();
}

View File

@@ -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() {
@@ -110,6 +117,121 @@ sPCAppearanceData Player::getAppearanceData() {
return data;
}
bool Player::hasQuestBoost() const {
if (AEQUIP_COUNT < AEQUIP_COUNT_WITH_BOOSTERS)
return false;
const sItemBase& booster = Equip[10];
return booster.iID == 153 && booster.iOpt > 0;
}
bool Player::hasHunterBoost() const {
if (AEQUIP_COUNT < AEQUIP_COUNT_WITH_BOOSTERS)
return false;
const sItemBase& booster = Equip[11];
return booster.iID == 154 && booster.iOpt > 0;
}
bool Player::hasRacerBoost() const {
if (AEQUIP_COUNT < AEQUIP_COUNT_WITH_BOOSTERS)
return false;
const sItemBase& booster = Equip[9];
return booster.iID == 155 && booster.iOpt > 0;
}
bool Player::hasSuperBoost() const {
return Player::hasQuestBoost() && Player::hasHunterBoost() && Player::hasRacerBoost();
}
static int32_t getCap(CappedValueType type) {
switch (type) {
case CappedValueType::TAROS:
return PC_CANDY_MAX;
case CappedValueType::FUSIONMATTER:
return PC_FUSIONMATTER_MAX;
case CappedValueType::BATTERY_W:
return PC_BATTERY_MAX;
case CappedValueType::BATTERY_N:
return PC_BATTERY_MAX;
case CappedValueType::TAROS_IN_TRADE:
return PC_CANDY_MAX;
default:
return INT32_MAX;
}
}
static int32_t *getCappedValue(Player *player, CappedValueType type) {
switch (type) {
case CappedValueType::TAROS:
return &player->money;
case CappedValueType::FUSIONMATTER:
return &player->fusionmatter;
case CappedValueType::BATTERY_W:
return &player->batteryW;
case CappedValueType::BATTERY_N:
return &player->batteryN;
case CappedValueType::TAROS_IN_TRADE:
return &player->moneyInTrade;
default:
return nullptr;
}
}
void Player::addCapped(CappedValueType type, int32_t diff) {
if (diff <= 0)
return;
int32_t max = getCap(type);
int32_t *value = getCappedValue(this, type);
if (value == nullptr)
return;
if (diff > max)
diff = max;
if (*value + diff > max)
*value = max;
else
*value += diff;
}
void Player::subtractCapped(CappedValueType type, int32_t diff) {
if (diff <= 0)
return;
int32_t max = getCap(type);
int32_t *value = getCappedValue(this, type);
if (value == nullptr)
return;
if (diff > max)
diff = max;
if (*value - diff < 0)
*value = 0;
else
*value -= diff;
}
void Player::setCapped(CappedValueType type, int32_t value) {
int32_t max = getCap(type);
int32_t *valToSet = getCappedValue(this, type);
if (valToSet == nullptr)
return;
if (value < 0)
value = 0;
else if (value > max)
value = max;
*valToSet = value;
}
// TODO: this is less effiecient than it was, because of memset()
void Player::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_PC_NEW, pkt);

View File

@@ -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();
};

View File

@@ -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);

View File

@@ -33,6 +33,19 @@ std::map<int32_t, int32_t> Items::EventToDropMap;
std::map<int32_t, int32_t> Items::MobToDropMap;
std::map<int32_t, ItemSet> Items::ItemSets;
// 1 week
#define NANOCOM_BOOSTER_DURATION 604800
// known general item ids
#define GENERALITEM_GUMBALL_ADAPTIUM 119
#define GENERALITEM_GUMBALL_BLASTONS 120
#define GENERALITEM_GUMBALL_COSMIX 121
#define GENERALITEM_FUSION_HUNTER_BOOSTER 153
#define GENERALITEM_IZ_RACER_BOOSTER 154
#define GENERALITEM_QUESTER_BOOSTER 155
#define GENERALITEM_SUPER_BOOSTER_DX 156
#ifdef ACADEMY
std::map<int32_t, int32_t> Items::NanoCapsules; // crate id -> nano id
@@ -46,7 +59,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
@@ -322,14 +335,14 @@ static void itemMoveHandler(CNSocket* sock, CNPacketData* data) {
// if equipping an item, validate that it's of the correct type for the slot
if ((SlotType)itemmove->eTo == SlotType::EQUIP) {
if (fromItem->iType == 10 && itemmove->iToSlotNum != 8)
if (fromItem->iType == 10 && itemmove->iToSlotNum != EQUIP_SLOT_VEHICLE)
return; // vehicle in wrong slot
else if (fromItem->iType != 10
&& !(fromItem->iType == 0 && itemmove->iToSlotNum == 7)
&& fromItem->iType != itemmove->iToSlotNum)
return; // something other than a vehicle or a weapon in a non-matching slot
else if (itemmove->iToSlotNum >= AEQUIP_COUNT) // TODO: reject slots >= 9?
return; // invalid slot
else if (itemmove->iToSlotNum >= AEQUIP_COUNT_MINUS_BOOSTERS)
return; // boosters can't be equipped via move packet
}
// save items to response
@@ -386,7 +399,7 @@ static void itemMoveHandler(CNSocket* sock, CNPacketData* data) {
}
// unequip vehicle if equip slot 8 is 0
if (plr->Equip[8].iID == 0 && plr->iPCState & 8) {
if (plr->Equip[EQUIP_SLOT_VEHICLE].iID == 0 && plr->iPCState & 8) {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC);
@@ -416,6 +429,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;
@@ -427,30 +443,19 @@ static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_DELETE_SUCC);
}
static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
static void useGumball(CNSocket* sock, CNPacketData* data) {
auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf;
Player* player = PlayerManager::getPlayer(sock);
if (request->iSlotNum < 0 || request->iSlotNum >= AINVEN_COUNT)
return; // sanity check
// gumball can only be used from inventory, so we ignore eIL
sItemBase gumball = player->Inven[request->iSlotNum];
sNano nano = player->Nanos[player->equippedNanos[request->iNanoSlot]];
// sanity check, check if gumball exists
if (!(gumball.iOpt > 0 && gumball.iType == 7 && gumball.iID>=119 && gumball.iID<=121)) {
std::cout << "[WARN] Gumball not found" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
return;
}
// sanity check, check if gumball type matches nano style
int nanoStyle = Nanos::nanoStyle(nano.iID);
if (!((gumball.iID == 119 && nanoStyle == 0) ||
( gumball.iID == 120 && nanoStyle == 1) ||
( gumball.iID == 121 && nanoStyle == 2))) {
if (!((gumball.iID == GENERALITEM_GUMBALL_ADAPTIUM && nanoStyle == 0) ||
( gumball.iID == GENERALITEM_GUMBALL_BLASTONS && nanoStyle == 1) ||
( gumball.iID == GENERALITEM_GUMBALL_COSMIX && nanoStyle == 2))) {
std::cout << "[WARN] Gumball type doesn't match nano type" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
@@ -469,9 +474,6 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
return;
}
if (gumball.iOpt == 0)
gumball = {};
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
@@ -512,6 +514,128 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
player->Inven[resp->iSlotNum] = resp->RemainItem;
}
static void useNanocomBooster(CNSocket* sock, CNPacketData* data) {
// Guard against using nanocom boosters in before and including 0104
// either path should be optimized by the compiler, effectively a no-op
if (AEQUIP_COUNT < AEQUIP_COUNT_WITH_BOOSTERS) {
std::cout << "[WARN] Nanocom Booster use not supported in this version" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, respFail);
sock->sendPacket(respFail, P_FE2CL_REP_PC_ITEM_USE_FAIL);
return;
}
auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf;
Player* player = PlayerManager::getPlayer(sock);
sItemBase item = player->Inven[request->iSlotNum];
// decide on the booster to activate
std::vector<int16_t> boosterIDs;
switch(item.iID) {
case GENERALITEM_FUSION_HUNTER_BOOSTER:
case GENERALITEM_IZ_RACER_BOOSTER:
case GENERALITEM_QUESTER_BOOSTER:
boosterIDs.push_back(item.iID);
break;
case GENERALITEM_SUPER_BOOSTER_DX:
boosterIDs.push_back(GENERALITEM_FUSION_HUNTER_BOOSTER);
boosterIDs.push_back(GENERALITEM_IZ_RACER_BOOSTER);
boosterIDs.push_back(GENERALITEM_QUESTER_BOOSTER);
break;
}
// consume item
item.iOpt -= 1;
if (item.iOpt == 0)
item = {};
// client wants to subtract server time in seconds from the time limit for display purposes
int32_t timeLimitDisplayed = (getTime() / 1000UL) + NANOCOM_BOOSTER_DURATION;
// in actuality we will use the timestamp of booster activation to the item time limit similar to vehicles
// and this is how it will be saved to the database
int32_t timeLimit = getTimestamp() + NANOCOM_BOOSTER_DURATION;
// give item(s) to inv slots
for (int16_t itemID : boosterIDs) {
sItemBase boosterItem = { 7, itemID, 1, timeLimitDisplayed };
// quester 155 -> 9, hunter 153 -> 10, racer 154 -> 11
int slot = 9 + ((itemID - GENERALITEM_FUSION_HUNTER_BOOSTER + 1) % 3);
// give item to the equip slot
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp);
resp.eIL = (int)SlotType::EQUIP;
resp.iSlotNum = slot;
resp.Item = boosterItem;
sock->sendPacket(resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
// inform client of equip change (non visible so it's okay to just send to the player)
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, equipChange);
equipChange.iPC_ID = player->iID;
equipChange.iEquipSlotNum = slot;
equipChange.EquipSlotItem = boosterItem;
sock->sendPacket(equipChange, P_FE2CL_PC_EQUIP_CHANGE);
boosterItem.iTimeLimit = timeLimit;
// should replace existing booster in slot if it exists, i.e. you can refresh your boosters
player->Equip[slot] = boosterItem;
}
// send item use success packet
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_SUCC, respUse);
respUse.iPC_ID = player->iID;
respUse.eIL = (int)SlotType::INVENTORY;
respUse.iSlotNum = request->iSlotNum;
respUse.RemainItem = item;
sock->sendPacket(respUse, P_FE2CL_REP_PC_ITEM_USE_SUCC);
// update inventory serverside
player->Inven[request->iSlotNum] = item;
}
static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf;
Player* player = PlayerManager::getPlayer(sock);
if (request->iSlotNum < 0 || request->iSlotNum >= AINVEN_COUNT)
return; // sanity check
sItemBase item = player->Inven[request->iSlotNum];
// sanity check, check the item exists and has correct iType
if (!(item.iOpt > 0 && item.iType == 7)) {
std::cout << "[WARN] General item not found" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
return;
}
/*
* TODO: In the XDT, there are subtypes for general-use items
* (m_pGeneralItemTable -> m_pItemData-> m_iItemType) that
* determine their behavior. It would be better to load these
* and use them in this switch, rather than hardcoding by IDs.
*/
switch(item.iID) {
case GENERALITEM_GUMBALL_ADAPTIUM:
case GENERALITEM_GUMBALL_BLASTONS:
case GENERALITEM_GUMBALL_COSMIX:
useGumball(sock, data);
break;
case GENERALITEM_FUSION_HUNTER_BOOSTER:
case GENERALITEM_IZ_RACER_BOOSTER:
case GENERALITEM_QUESTER_BOOSTER:
case GENERALITEM_SUPER_BOOSTER_DX:
useNanocomBooster(sock, data);
break;
default:
std::cout << "[INFO] General item "<< item.iID << " is unimplemented." << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
break;
}
}
static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
@@ -553,7 +677,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
@@ -595,7 +719,7 @@ static void chestOpenHandler(CNSocket *sock, CNPacketData *data) {
// if we failed to open a crate, at least give the player a gumball (suggested by Jade)
if (failing) {
item->sItem.iType = 7;
item->sItem.iID = 119 + Rand::rand(3);
item->sItem.iID = GENERALITEM_GUMBALL_ADAPTIUM + Rand::rand(3);
item->sItem.iOpt = 1;
std::cout << "[WARN] Crate open failed, giving a Gumball..." << std::endl;
@@ -629,39 +753,89 @@ Item* Items::getItemData(int32_t id, int32_t type) {
return nullptr;
}
void Items::checkItemExpire(CNSocket* sock, Player* player) {
if (player->toRemoveVehicle.eIL == 0 && player->toRemoveVehicle.iSlotNum == 0)
return;
size_t Items::checkAndRemoveExpiredItems(CNSocket* sock, Player* player) {
int32_t currentTime = getTimestamp();
/* prepare packet
* yes, this is a varadic packet, however analyzing client behavior and code
* it only checks takes the first item sent into account
* yes, this is very stupid
* therefore, we delete all but 1 expired vehicle while loading player
* to delete the last one here so player gets a notification
*/
// if there are expired items in bank just remove them silently
if (settings::REMOVEEXPIREDITEMSFROMBANK) {
for (int i = 0; i < ABANK_COUNT; i++) {
if (player->Bank[i].iTimeLimit < currentTime && player->Bank[i].iTimeLimit != 0) {
memset(&player->Bank[i], 0, sizeof(sItemBase));
}
}
}
// collect items to remove and data for the packet
std::vector<sItemBase*> toRemove;
std::vector<sTimeLimitItemDeleteInfo2CL> itemData;
// equipped items
for (int i = 0; i < AEQUIP_COUNT; i++) {
if (player->Equip[i].iOpt > 0 && player->Equip[i].iTimeLimit < currentTime && player->Equip[i].iTimeLimit != 0) {
toRemove.push_back(&player->Equip[i]);
itemData.push_back({ (int)SlotType::EQUIP, i });
}
}
// inventory
for (int i = 0; i < AINVEN_COUNT; i++) {
if (player->Inven[i].iTimeLimit < currentTime && player->Inven[i].iTimeLimit != 0) {
toRemove.push_back(&player->Inven[i]);
itemData.push_back({ (int)SlotType::INVENTORY, i });
}
}
if (itemData.empty())
return 0;
// prepare packet containing all expired items to delete
// this is expected for academy
// pre-academy only checks the first item in the packet
const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL) * itemData.size();
// 8 bytes * 262 items = 2096 bytes, in total this shouldn't exceed 2500 bytes
assert(resplen < CN_PACKET_BODY_SIZE);
uint8_t respbuf[CN_PACKET_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
// 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;
sTimeLimitItemDeleteInfo2CL* itemData = (sTimeLimitItemDeleteInfo2CL*)(respbuf + sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM));
memset(respbuf, 0, resplen);
packet->iItemListCount = 1;
itemData->eIL = player->toRemoveVehicle.eIL;
itemData->iSlotNum = player->toRemoveVehicle.iSlotNum;
for (size_t i = 0; i < itemData.size(); i++) {
auto itemToDeletePtr = (sTimeLimitItemDeleteInfo2CL*)(
respbuf + sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL) * i
);
itemToDeletePtr->eIL = itemData[i].eIL;
itemToDeletePtr->iSlotNum = itemData[i].iSlotNum;
packet->iItemListCount++;
}
sock->sendPacket((void*)&respbuf, P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, resplen);
// delete serverside
if (player->toRemoveVehicle.eIL == 0)
memset(&player->Equip[8], 0, sizeof(sItemBase));
else
memset(&player->Inven[player->toRemoveVehicle.iSlotNum], 0, sizeof(sItemBase));
// delete items serverside and send unequip packets
for (size_t i = 0; i < itemData.size(); i++) {
sItemBase* item = toRemove[i];
memset(item, 0, sizeof(sItemBase));
player->toRemoveVehicle.eIL = 0;
player->toRemoveVehicle.iSlotNum = 0;
// send item delete success packet
// required for pre-academy builds
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_DELETE_SUCC, itemDelete);
itemDelete.eIL = itemData[i].eIL;
itemDelete.iSlotNum = itemData[i].iSlotNum;
sock->sendPacket(itemDelete, P_FE2CL_REP_PC_ITEM_DELETE_SUCC);
// also update item equips if needed
if (itemData[i].eIL == (int)SlotType::EQUIP) {
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, equipChange);
equipChange.iPC_ID = player->iID;
equipChange.iEquipSlotNum = itemData[i].iSlotNum;
sock->sendPacket(equipChange, P_FE2CL_PC_EQUIP_CHANGE);
}
}
// exit vehicle if player no longer has one equipped (function checks pcstyle)
if (player->Equip[EQUIP_SLOT_VEHICLE].iID == 0)
PlayerManager::exitPlayerVehicle(sock, nullptr);
return itemData.size();
}
void Items::setItemStats(Player* plr) {
@@ -708,11 +882,73 @@ static void getMobDrop(sItemBase* reward, const std::vector<int>& weights, const
reward->iID = crateIds[chosenIndex];
}
static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRoll& rolled) {
static int32_t calculateTaroReward(Player* plr, int baseAmount, int groupSize) {
double bonus = plr->hasBuff(ECSB_REWARD_CASH) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0;
double groupEffect = settings::LESSTAROFMINGROUPDISABLED ? 1.0 : 1.0 / groupSize;
return baseAmount * plr->rateT[RATE_SLOT_COMBAT] * bonus * groupEffect;
}
static int32_t calculateFMReward(Player* plr, int baseAmount, int levelDiff, int groupSize) {
double scavenge = plr->hasBuff(ECSB_REWARD_BLOB) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0;
double boosterEffect = plr->hasHunterBoost() ? (plr->hasQuestBoost() && plr->hasRacerBoost() ? 1.75 : 1.5) : 1.0;
// if player is within 1 level of the mob, FM is untouched
double levelEffect = 1.0;
// otherwise, follow the table below
switch (std::clamp(levelDiff, -3, 6)) {
case 6:
// if player is 6 or more levels above mob, no FM is dropped
levelEffect = 0.0;
break;
case 5:
levelEffect = 0.25;
break;
case 4:
levelEffect = 0.5;
break;
case 3:
levelEffect = 0.75;
break;
case 2:
levelEffect = 0.899;
break;
case -2:
levelEffect = 1.1;
break;
case -3:
// if player is 3 or more levels below mob, FM is 1.2x
levelEffect = 1.2;
break;
}
// if no group, FM is untouched
double groupEffect = 1.0;
// otherwise, follow the table below
if (!settings::LESSTAROFMINGROUPDISABLED) {
switch (groupSize) {
case 2:
groupEffect = 0.875;
break;
case 3:
groupEffect = 0.75;
break;
case 4:
// this case is more lenient
groupEffect = 0.688;
break;
}
}
int32_t amount = baseAmount * plr->rateF[RATE_SLOT_COMBAT] * scavenge * levelEffect * groupEffect;
amount *= boosterEffect;
return amount;
}
static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRoll& rolled, int groupSize) {
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
@@ -760,43 +996,20 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
MiscDropType& miscDropType = Items::MiscDropTypes[drop.miscDropTypeId];
if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) {
plr->money += miscDropType.taroAmount;
// money nano boost
if (plr->hasBuff(ECSB_REWARD_CASH)) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
plr->money += miscDropType.taroAmount * (5 + boost) / 25;
}
int32_t taros = calculateTaroReward(plr, miscDropType.taroAmount, groupSize);
plr->addCapped(CappedValueType::TAROS, taros);
}
if (rolled.fm % miscDropChance.fmDropChanceTotal < miscDropChance.fmDropChance) {
// formula for scaling FM with player/mob level difference
// TODO: adjust this better
int levelDifference = plr->level - mob->level;
int fm = miscDropType.fmAmount;
if (levelDifference > 0)
fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0;
// scavenger nano boost
if (plr->hasBuff(ECSB_REWARD_BLOB)) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
fm += fm * (5 + boost) / 25;
}
Missions::updateFusionMatter(sock, fm);
int32_t fm = calculateFMReward(plr, miscDropType.fmAmount, levelDifference, groupSize);
plr->addCapped(CappedValueType::FUSIONMATTER, fm);
Missions::updateFusionMatter(sock);
}
if (rolled.potions % miscDropChance.potionDropChanceTotal < miscDropChance.potionDropChance)
plr->batteryN += miscDropType.potionAmount;
plr->addCapped(CappedValueType::BATTERY_N, miscDropType.potionAmount);
if (rolled.boosts % miscDropChance.boostDropChanceTotal < miscDropChance.boostDropChance)
plr->batteryW += miscDropType.boostAmount;
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
if (plr->batteryN > 9999)
plr->batteryN = 9999;
plr->addCapped(CappedValueType::BATTERY_W, miscDropType.boostAmount);
// simple rewards
reward->m_iCandy = plr->money;
@@ -827,7 +1040,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
}
}
void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled) {
void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled, int groupSize) {
// sanity check
if (Items::MobToDropMap.find(mob->type) == Items::MobToDropMap.end()) {
std::cout << "[WARN] Mob ID " << mob->type << " has no drops assigned" << std::endl;
@@ -836,7 +1049,7 @@ void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const
// find mob drop id
int mobDropId = Items::MobToDropMap[mob->type];
giveSingleDrop(sock, mob, mobDropId, rolled);
giveSingleDrop(sock, mob, mobDropId, rolled, groupSize);
if (settings::EVENTMODE != 0) {
// sanity check
@@ -847,14 +1060,13 @@ void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const
// find mob drop id
int eventMobDropId = Items::EventToDropMap[settings::EVENTMODE];
giveSingleDrop(sock, mob, eventMobDropId, eventRolled);
giveSingleDrop(sock, mob, eventMobDropId, eventRolled, groupSize);
}
}
void Items::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler);
// this one is for gumballs
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_USE, itemUseHandler);
// Bank
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BANK_OPEN, itemBankOpenHandler);

View File

@@ -113,11 +113,11 @@ namespace Items {
void init();
// mob drops
void giveMobDrop(CNSocket *sock, Mob *mob, const DropRoll& rolled, const DropRoll& eventRolled);
void giveMobDrop(CNSocket *sock, Mob *mob, const DropRoll& rolled, const DropRoll& eventRolled, int groupSize);
int findFreeSlot(Player *plr);
Item* getItemData(int32_t id, int32_t type);
void checkItemExpire(CNSocket* sock, Player* player);
size_t checkAndRemoveExpiredItems(CNSocket* sock, Player* player);
void setItemStats(Player* plr);
void updateEquips(CNSocket* sock, Player* plr);

View File

@@ -12,6 +12,20 @@ std::map<int32_t, Reward*> Missions::Rewards;
std::map<int32_t, TaskData*> Missions::Tasks;
nlohmann::json Missions::AvatarGrowth[37];
static int32_t calculateTaroReward(Player* plr, int32_t baseAmount) {
double bonus = plr->hasBuff(ECSB_REWARD_CASH) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0;
return baseAmount * plr->rateT[RATE_SLOT_MISSION] * bonus;
}
static int32_t calculateFMReward(Player* plr, int32_t baseAmount) {
double scavenge = plr->hasBuff(ECSB_REWARD_BLOB) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0;
double missionBoost = plr->hasQuestBoost() ? (plr->hasHunterBoost() && plr->hasRacerBoost() ? 1.75 : 1.5) : 1.0;
int32_t reward = baseAmount * plr->rateF[RATE_SLOT_MISSION] * scavenge;
reward *= missionBoost;
return reward;
}
static void saveMission(Player* player, int missionId) {
// sanity check missionID so we don't get exceptions
if (missionId < 0 || missionId > 1023) {
@@ -64,7 +78,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,31 +166,22 @@ 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;
if (plr->hasBuff(ECSB_REWARD_CASH)) { // nano boost for taros
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
plr->money += reward->money * (5 + boost) / 25;
}
int32_t money = calculateTaroReward(plr, reward->money);
plr->addCapped(CappedValueType::TAROS, money);
if (plr->hasBuff(ECSB_REWARD_BLOB)) { // nano boost for fm
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
updateFusionMatter(sock, reward->fusionmatter * (30 + boost) / 25);
} else
updateFusionMatter(sock, reward->fusionmatter);
int32_t fusionMatter = calculateFMReward(plr, reward->fusionmatter);
plr->addCapped(CappedValueType::FUSIONMATTER, fusionMatter);
Missions::updateFusionMatter(sock);
// simple rewards
resp->m_iCandy = plr->money;
@@ -386,6 +391,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
@@ -508,17 +516,13 @@ void Missions::quitTask(CNSocket* sock, int32_t taskNum, bool manual) {
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_STOP_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_STOP_SUCC));
}
void Missions::updateFusionMatter(CNSocket* sock, int fusion) {
void Missions::updateFusionMatter(CNSocket* sock) {
Player *plr = PlayerManager::getPlayer(sock);
plr->fusionmatter += fusion;
// there's a much lower FM cap in the Future
int fmCap = AvatarGrowth[plr->level]["m_iFMLimit"];
if (plr->fusionmatter > fmCap)
plr->fusionmatter = fmCap;
else if (plr->fusionmatter < 0) // if somehow lowered too far
plr->fusionmatter = 0;
// don't run nano mission logic at level 36
if (plr->level >= 36)
@@ -548,7 +552,7 @@ void Missions::updateFusionMatter(CNSocket* sock, int fusion) {
response.iTaskNum = AvatarGrowth[plr->level]["m_iNanoQuestTaskID"];
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
#else
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"]);
plr->level++;
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response);

View File

@@ -47,7 +47,7 @@ namespace Missions {
bool startTask(Player* plr, int TaskID);
// checks if player doesn't have n/n quest items
void updateFusionMatter(CNSocket* sock, int fusion);
void updateFusionMatter(CNSocket* sock);
void mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls);

View File

@@ -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;
@@ -800,18 +808,17 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
Items::DropRoll rolled;
Items::DropRoll eventRolled;
std::map<int, int> qitemRolls;
std::vector<EntityRef> playersInRange;
std::vector<Player*> playerRefs;
if (plr->group == nullptr) {
playerRefs.push_back(plr);
Combat::genQItemRolls(playerRefs, qitemRolls);
Items::giveMobDrop(src.sock, self, rolled, eventRolled);
Items::giveMobDrop(src.sock, self, rolled, eventRolled, 1);
Missions::mobKilled(src.sock, self->type, qitemRolls);
}
else {
auto players = plr->group->filter(EntityKind::PLAYER);
for (EntityRef pRef : players) playerRefs.push_back(PlayerManager::getPlayer(pRef.sock));
Combat::genQItemRolls(playerRefs, qitemRolls);
for (int i = 0; i < players.size(); i++) {
CNSocket* sockTo = players[i].sock;
Player* otherPlr = PlayerManager::getPlayer(sockTo);
@@ -821,7 +828,14 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
if (dist > 5000)
continue;
Items::giveMobDrop(sockTo, self, rolled, eventRolled);
playersInRange.push_back(players[i]);
}
for (EntityRef pRef : playersInRange) playerRefs.push_back(PlayerManager::getPlayer(pRef.sock));
Combat::genQItemRolls(playerRefs, qitemRolls);
for (int i = 0; i < playersInRange.size(); i++) {
CNSocket* sockTo = playersInRange[i].sock;
Items::giveMobDrop(sockTo, self, rolled, eventRolled, playersInRange.size());
Missions::mobKilled(sockTo, self->type, qitemRolls);
}
}

View File

@@ -33,8 +33,10 @@ void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm)
*/
plr->level = level;
if (spendfm)
Missions::updateFusionMatter(sock, -(int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]);
if (spendfm) {
plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]);
Missions::updateFusionMatter(sock);
}
#endif
// Send to client
@@ -110,7 +112,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);
@@ -143,7 +145,7 @@ static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
return;
#endif
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"];
plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"]);
int reqItemCount = NanoTunings[skill->iTuneID].reqItemCount;
int reqItemID = NanoTunings[skill->iTuneID].reqItems;
@@ -355,7 +357,7 @@ static void nanoPotionHandler(CNSocket* sock, CNPacketData* data) {
sock->sendPacket(response, P_FE2CL_REP_CHARGE_NANO_STAMINA);
// now update serverside
player->batteryN -= difference;
player->subtractCapped(CappedValueType::BATTERY_N, difference);
player->Nanos[nano.iID].iStamina += difference;
}

View File

@@ -15,6 +15,14 @@ struct BuffStack;
#define PC_MAXHEALTH(level) (925 + 75 * (level))
enum class CappedValueType {
TAROS,
FUSIONMATTER,
BATTERY_W,
BATTERY_N,
TAROS_IN_TRADE,
};
struct Player : public Entity, public ICombatant {
int accountId = 0;
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
@@ -65,15 +73,13 @@ struct Player : public Entity, public ICombatant {
sItemBase QInven[AQINVEN_COUNT] = {};
int32_t CurrentMissionID = 0;
sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {};
Group* group = nullptr;
bool notify = false;
bool hidden = false;
bool unwarpable = false;
bool initialLoadDone = false;
bool buddiesSynced = false;
int64_t buddyIDs[50] = {};
bool isBuddyBlocked[50] = {};
@@ -84,7 +90,14 @@ struct Player : public Entity, public ICombatant {
time_t lastShot = 0;
std::vector<sItemBase> buyback = {};
Player() { kind = EntityKind::PLAYER; }
double rateF[5] = { 1.0, 1.0, 1.0, 1.0, 1.0 };
double rateT[5] = { 1.0, 1.0, 1.0, 1.0, 1.0 };
Player() {
kind = EntityKind::PLAYER;
std::fill_n(rateF, 5, (double)settings::FUSIONMATTERRATE / 100.0);
std::fill_n(rateT, 5, (double)settings::TARORATE / 100.0);
}
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
@@ -111,4 +124,11 @@ struct Player : public Entity, public ICombatant {
sNano* getActiveNano();
sPCAppearanceData getAppearanceData();
bool hasQuestBoost() const;
bool hasHunterBoost() const;
bool hasRacerBoost() const;
bool hasSuperBoost() const;
void addCapped(CappedValueType type, int32_t diff);
void subtractCapped(CappedValueType type, int32_t diff);
void setCapped(CappedValueType type, int32_t value);
};

View File

@@ -155,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;
@@ -212,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;
@@ -237,9 +243,19 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
// client doesnt read this, it gets it from charinfo
// response.PCLoadData2CL.PCStyle2 = plr->PCStyle2;
// inventory
for (int i = 0; i < AEQUIP_COUNT; i++)
// equipment (except nanocom boosters)
for (int i = 0; i < AEQUIP_COUNT_MINUS_BOOSTERS; i++)
response.PCLoadData2CL.aEquip[i] = plr->Equip[i];
// equipment (nanocom boosters, loop only runs if boosters are available)
int32_t serverTime = getTime() / 1000UL;
int32_t timestamp = getTimestamp();
for (int i = AEQUIP_COUNT_MINUS_BOOSTERS; i < AEQUIP_COUNT; i++) {
response.PCLoadData2CL.aEquip[i] = plr->Equip[i];
// client subtracts server time, then adds local timestamp to the item to print expiration time
response.PCLoadData2CL.aEquip[i].iTimeLimit = std::max(0, plr->Equip[i].iTimeLimit - timestamp + serverTime);
}
// inventory
for (int i = 0; i < AINVEN_COUNT; i++)
response.PCLoadData2CL.aInven[i] = plr->Inven[i];
// quest inventory
@@ -294,27 +310,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.");
@@ -377,6 +387,17 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) {
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::checkAndRemoveExpiredItems(sock, plr); // vehicle and booster expiration
plr->initialLoadDone = true;
}
}
static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
@@ -385,6 +406,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;
@@ -476,7 +505,6 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
resp2.PCRegenDataForOtherPC.iAngle = plr->angle;
if (plr->group != nullptr) {
resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->getCompositeCondition();
resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState;
resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState;
@@ -498,9 +526,7 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
if (plr->instanceID != 0)
return;
bool expired = plr->Equip[8].iTimeLimit < getTimestamp() && plr->Equip[8].iTimeLimit != 0;
if (plr->Equip[8].iID > 0 && !expired) {
if (plr->Equip[EQUIP_SLOT_VEHICLE].iID > 0) {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_SUCC, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_SUCC);
@@ -514,30 +540,6 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
} else {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_FAIL, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_FAIL);
// check if vehicle didn't expire
if (expired) {
plr->toRemoveVehicle.eIL = 0;
plr->toRemoveVehicle.iSlotNum = 8;
Items::checkItemExpire(sock, plr);
}
}
}
static void exitPlayerVehicle(CNSocket* sock, CNPacketData* data) {
Player* plr = getPlayer(sock);
if (plr->iPCState & 8) {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC);
// send to other players
plr->iPCState &= ~8;
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, response2);
response2.iPC_ID = plr->iID;
response2.iState = plr->iPCState;
sendToViewable(sock, response2, P_FE2CL_PC_STATE_CHANGE);
}
}
@@ -566,7 +568,7 @@ static void changePlayerGuide(CNSocket *sock, CNPacketData *data) {
}
// start Blossom nano mission if applicable
Missions::updateFusionMatter(sock, 0);
Missions::updateFusionMatter(sock);
}
// save it on player
plr->mentor = pkt->iMentor;
@@ -588,6 +590,23 @@ static void setFirstUseFlag(CNSocket* sock, CNPacketData* data) {
}
#pragma region Helper methods
void PlayerManager::exitPlayerVehicle(CNSocket* sock, CNPacketData* data) {
Player* plr = getPlayer(sock);
if (plr->iPCState & 8) {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC);
// send to other players
plr->iPCState &= ~8;
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, response2);
response2.iPC_ID = plr->iID;
response2.iState = plr->iPCState;
sendToViewable(sock, response2, P_FE2CL_PC_STATE_CHANGE);
}
}
Player *PlayerManager::getPlayer(CNSocket* key) {
if (players.find(key) != players.end())
return players[key];
@@ -601,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) ";

View File

@@ -14,6 +14,8 @@ namespace PlayerManager {
extern std::map<CNSocket*, Player*> players;
void init();
void exitPlayerVehicle(CNSocket* sock, CNPacketData* data);
void removePlayer(CNSocket* key);
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle);

View File

@@ -7,6 +7,7 @@
#include "PlayerManager.hpp"
#include "Missions.hpp"
#include "Items.hpp"
#include "Nanos.hpp"
using namespace Racing;
@@ -14,6 +15,15 @@ std::map<int32_t, EPInfo> Racing::EPData;
std::map<CNSocket*, EPRace> Racing::EPRaces;
std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> Racing::EPRewards;
static int32_t calculateFMReward(Player* plr, int32_t baseAmount) {
double scavenge = plr->hasBuff(ECSB_REWARD_BLOB) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0;
double raceBoost = plr->hasRacerBoost() ? (plr->hasHunterBoost() && plr->hasQuestBoost() ? 1.75 : 1.5) : 1.0;
int32_t reward = baseAmount * plr->rateF[RATE_SLOT_RACING] * scavenge;
reward *= raceBoost;
return reward;
}
static void racingStart(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_EP_RACE_START*)data->buf;
@@ -106,10 +116,11 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
int timeDiff = now - epRace.startTime;
int podsCollected = epRace.collectedRings.size();
int score = std::min(epInfo.maxScore, (int)std::exp(
int score = std::exp(
(epInfo.podFactor * podsCollected) / epInfo.maxPods
- (epInfo.timeFactor * timeDiff) / epInfo.maxTime
+ epInfo.scaleFactor));
+ 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...
@@ -132,8 +143,9 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
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;
@@ -143,7 +155,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;
@@ -153,7 +165,9 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
resp.iEPRaceMode = EPRaces[sock].mode;
resp.iEPRewardFM = fm;
Missions::updateFusionMatter(sock, resp.iEPRewardFM);
int32_t fmReward = calculateFMReward(plr, resp.iEPRewardFM);
plr->addCapped(CappedValueType::FUSIONMATTER, fmReward);
Missions::updateFusionMatter(sock);
resp.iFusionMatter = plr->fusionmatter;
resp.iFatigue = 50;

View File

@@ -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"];
@@ -610,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));
}

View File

@@ -12,7 +12,7 @@
// these are added to the NPC's static key to avoid collisions
const int NPC_ID_OFFSET = 1;
const int MOB_ID_OFFSET = 10000;
const int MOB_GROUP_ID_OFFSET = 20000;
const int MOB_GROUP_ID_OFFSET = 30000;
// typedef for JSON object because I don't want to type nlohmann::json every time
typedef nlohmann::json json;

View File

@@ -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;
@@ -243,14 +273,16 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
resp2.iID_Request = pacdat->iID_Request;
resp2.iID_From = pacdat->iID_From;
resp2.iID_To = pacdat->iID_To;
plr->money = plr->money + plr2->moneyInTrade - plr->moneyInTrade;
plr->subtractCapped(CappedValueType::TAROS, plr->moneyInTrade);
plr->addCapped(CappedValueType::TAROS, plr2->moneyInTrade);
resp2.iCandy = plr->money;
memcpy(resp2.Item, plr2->Trade, sizeof(plr2->Trade));
memcpy(resp2.ItemStay, plr->Trade, sizeof(plr->Trade));
sock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
plr2->money = plr2->money + plr->moneyInTrade - plr2->moneyInTrade;
plr2->subtractCapped(CappedValueType::TAROS, plr2->moneyInTrade);
plr2->addCapped(CappedValueType::TAROS, plr->moneyInTrade);
resp2.iCandy = plr2->money;
memcpy(resp2.Item, plr->Trade, sizeof(plr->Trade));
memcpy(resp2.ItemStay, plr2->Trade, sizeof(plr2->Trade));
@@ -421,7 +453,7 @@ static void tradeRegisterCash(CNSocket* sock, CNPacketData* data) {
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
plr->moneyInTrade = pacdat->iCandy;
plr->setCapped(CappedValueType::TAROS_IN_TRADE, pacdat->iCandy);
plr->isTradeConfirm = false;
}
@@ -430,6 +462,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);

View File

@@ -117,7 +117,7 @@ static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
}
TransportRoute route = Routes[req->iTransporationID];
plr->money -= route.cost;
plr->subtractCapped(CappedValueType::TAROS, route.cost);
TransportLocation* target = nullptr;
switch (route.type) {
@@ -143,7 +143,7 @@ static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
}
// refund and send alert packet
plr->money += route.cost;
plr->addCapped(CappedValueType::TAROS, route.cost);
INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert);
alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog
alert.iDuringTime = 3;
@@ -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

View File

@@ -72,7 +72,7 @@ static void vendorBuy(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC, resp);
plr->money = plr->money - itemCost;
plr->subtractCapped(CappedValueType::TAROS, itemCost);
plr->Inven[slot] = req->Item;
resp.iCandy = plr->money;
@@ -117,7 +117,7 @@ static void vendorSell(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, resp);
// increment taros
plr->money += itemData->sellPrice * req->iItemCnt;
plr->addCapped(CappedValueType::TAROS, itemData->sellPrice * req->iItemCnt);
// modify item
if (item->iOpt - req->iItemCnt > 0) { // selling part of a stack
@@ -209,7 +209,7 @@ static void vendorBuyback(CNSocket* sock, CNPacketData* data) {
std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl;
}
plr->money = plr->money - itemCost;
plr->subtractCapped(CappedValueType::TAROS, itemCost);
plr->Inven[slot] = item;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp);
@@ -273,7 +273,7 @@ static void vendorBuyBattery(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
int cost = req->Item.iOpt * 100;
if ((req->Item.iID == 3 ? (plr->batteryW >= 9999) : (plr->batteryN >= 9999)) || plr->money < cost || req->Item.iOpt < 0) { // sanity check
if ((req->Item.iID == 3 ? (plr->batteryW >= PC_BATTERY_MAX) : (plr->batteryN >= PC_BATTERY_MAX)) || plr->money < cost || req->Item.iOpt < 0) { // sanity check
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL);
@@ -281,17 +281,11 @@ static void vendorBuyBattery(CNSocket* sock, CNPacketData* data) {
}
cost = plr->batteryW + plr->batteryN;
plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 100 : 0;
plr->batteryN += req->Item.iID == 4 ? req->Item.iOpt * 100 : 0;
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
if (plr->batteryN > 9999)
plr->batteryN = 9999;
plr->addCapped(CappedValueType::BATTERY_W, req->Item.iID == 3 ? req->Item.iOpt * 100 : 0);
plr->addCapped(CappedValueType::BATTERY_N, req->Item.iID == 4 ? req->Item.iOpt * 100 : 0);
cost = plr->batteryW + plr->batteryN - cost;
plr->money -= cost;
plr->subtractCapped(CappedValueType::TAROS, cost);
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp);
@@ -364,7 +358,7 @@ static void vendorCombineItems(CNSocket* sock, CNPacketData* data) {
float rolled = Rand::randFloat(100.0f); // success chance out of 100
//std::cout << rolled << " vs " << successChance << std::endl;
plr->money -= cost;
plr->subtractCapped(CappedValueType::TAROS, cost);
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, resp);

View File

@@ -365,14 +365,8 @@ void CNServer::init() {
}
// attach socket to the port
int opt = 1;
#ifdef _WIN32
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)) != 0) {
#else
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) {
#endif
if (!setSocketOption(sock, SOL_SOCKET, SO_REUSEADDR, 1)) {
std::cerr << "[FATAL] OpenFusion: setsockopt failed" << std::endl;
printSocketError("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
@@ -414,6 +408,18 @@ void CNServer::init() {
CNServer::CNServer() {};
CNServer::CNServer(uint16_t p): port(p) {}
bool CNServer::setSocketOption(SOCKET s, int level, int option, int value) {
#ifdef _WIN32
if (setsockopt(s, level, option, (const char*)&value, sizeof(value)) != 0) {
#else
if (setsockopt(s, level, option, &value, sizeof(value)) != 0) {
#endif
printSocketError("setsockopt");
return false;
}
return true;
}
void CNServer::addPollFD(SOCKET s) {
fds.push_back({s, POLLIN});
}
@@ -428,7 +434,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);
@@ -462,6 +468,9 @@ void CNServer::start() {
continue;
}
if (!setSocketOption(newConnectionSocket, IPPROTO_TCP, TCP_NODELAY, 1))
std::cout << "[WARN] OpenFusion: failed to set TCP_NODELAY on new connection" << std::endl;
if (!setSockNonblocking(sock, newConnectionSocket))
continue;

View File

@@ -28,6 +28,7 @@
// posix platform
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <poll.h>
#include <unistd.h>
@@ -95,14 +96,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 +113,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;
@@ -239,6 +240,7 @@ protected:
bool active = true;
bool setSocketOption(SOCKET s, int level, int option, int value);
void addPollFD(SOCKET s);
void removePollFD(int i);

View File

@@ -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,19 +49,25 @@ 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
#error Invalid PROTOCOL_VERSION
#endif
#define AEQUIP_COUNT_MINUS_BOOSTERS 9
#define AEQUIP_COUNT_WITH_BOOSTERS 12
sSYSTEMTIME timeStampToStruct(uint64_t time);

View File

@@ -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;
@@ -223,6 +225,15 @@ enum {
SIZEOF_NANO_TUNE_NEED_ITEM_SLOT = 10,
VALUE_ATTACK_MISS = 1,
REWARD_TYPE_TAROS = 0,
REWARD_TYPE_FUSIONMATTER = 1,
RATE_SLOT_ALL = 0,
RATE_SLOT_COMBAT = 1,
RATE_SLOT_MISSION = 2,
RATE_SLOT_EGG = 3,
RATE_SLOT_RACING = 4,
MSG_ONLINE = 1,
MSG_BUSY = 2,
MSG_OFFLINE = 0,
@@ -410,7 +421,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 +951,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);

View File

@@ -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),

View File

@@ -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);

View File

@@ -196,18 +196,16 @@ void Database::updateEmailContent(EmailData* data) {
sqlite3_finalize(stmt);
}
void Database::deleteEmailAttachments(int playerID, int index, int slot) {
std::lock_guard<std::mutex> lock(dbCrit);
static void _deleteEmailAttachments(int playerID, int index, int slot) {
sqlite3_stmt* stmt;
std::string sql(R"(
DELETE FROM EmailItems
WHERE PlayerID = ? AND MsgIndex = ?;
WHERE PlayerID = ? AND MsgIndex = ?
)");
if (slot != -1)
sql += " AND \"Slot\" = ? ";
sql += " AND \"Slot\" = ?";
sql += ";";
sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, NULL);
@@ -221,6 +219,11 @@ void Database::deleteEmailAttachments(int playerID, int index, int slot) {
sqlite3_finalize(stmt);
}
void Database::deleteEmailAttachments(int playerID, int index, int slot) {
std::lock_guard<std::mutex> lock(dbCrit);
_deleteEmailAttachments(playerID, index, slot);
}
void Database::deleteEmails(int playerID, int64_t* indices) {
std::lock_guard<std::mutex> lock(dbCrit);
@@ -234,12 +237,15 @@ void Database::deleteEmails(int playerID, int64_t* indices) {
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
for (int i = 0; i < 5; i++) {
int64_t msgIndex = indices[i];
sqlite3_bind_int(stmt, 1, playerID);
sqlite3_bind_int64(stmt, 2, indices[i]);
sqlite3_bind_int64(stmt, 2, msgIndex);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to delete an email: " << sqlite3_errmsg(db) << std::endl;
}
sqlite3_reset(stmt);
// delete all attachments
_deleteEmailAttachments(playerID, msgIndex, -1);
}
sqlite3_finalize(stmt);
@@ -323,12 +329,23 @@ bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments, Pl
sqlite3_bind_int(stmt, 7, item.iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to send email: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return false;
// very likely the UNIQUE constraint failing due to
// orphaned attachments from an old email.
// try deleting them first
_deleteEmailAttachments(data->PlayerId, data->MsgIndex, -1);
// try again
sqlite3_reset(stmt);
if (sqlite3_step(stmt) != SQLITE_DONE) {
// different error, give up
std::cout << "[WARN] Database: Failed to send email: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return false;
}
}
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
}
sqlite3_finalize(stmt);

View File

@@ -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";
}

View File

@@ -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);

View File

@@ -2,40 +2,6 @@
// Loading and saving players to/from the DB
static void removeExpiredVehicles(Player* player) {
int32_t currentTime = getTimestamp();
// if there are expired vehicles in bank just remove them silently
for (int i = 0; i < ABANK_COUNT; i++) {
if (player->Bank[i].iType == 10 && player->Bank[i].iTimeLimit < currentTime && player->Bank[i].iTimeLimit != 0) {
memset(&player->Bank[i], 0, sizeof(sItemBase));
}
}
// we want to leave only 1 expired vehicle on player to delete it with the client packet
std::vector<sItemBase*> toRemove;
// equipped vehicle
if (player->Equip[8].iOpt > 0 && player->Equip[8].iTimeLimit < currentTime && player->Equip[8].iTimeLimit != 0) {
toRemove.push_back(&player->Equip[8]);
player->toRemoveVehicle.eIL = 0;
player->toRemoveVehicle.iSlotNum = 8;
}
// inventory
for (int i = 0; i < AINVEN_COUNT; i++) {
if (player->Inven[i].iType == 10 && player->Inven[i].iTimeLimit < currentTime && player->Inven[i].iTimeLimit != 0) {
toRemove.push_back(&player->Inven[i]);
player->toRemoveVehicle.eIL = 1;
player->toRemoveVehicle.iSlotNum = i;
}
}
// delete all but one vehicles, leave last one for ceremonial deletion
for (int i = 0; i < (int)toRemove.size()-1; i++) {
memset(toRemove[i], 0, sizeof(sItemBase));
}
}
void Database::getPlayer(Player* plr, int id) {
std::lock_guard<std::mutex> lock(dbCrit);
@@ -93,13 +59,13 @@ void Database::getPlayer(Player* plr, int id) {
plr->angle = sqlite3_column_int(stmt, 15);
plr->HP = sqlite3_column_int(stmt, 16);
plr->accountLevel = sqlite3_column_int(stmt, 17);
plr->fusionmatter = sqlite3_column_int(stmt, 18);
plr->money = sqlite3_column_int(stmt, 19);
plr->setCapped(CappedValueType::FUSIONMATTER, sqlite3_column_int(stmt, 18));
plr->setCapped(CappedValueType::TAROS, sqlite3_column_int(stmt, 19));
memcpy(plr->aQuestFlag, sqlite3_column_blob(stmt, 20), sizeof(plr->aQuestFlag));
plr->batteryW = sqlite3_column_int(stmt, 21);
plr->batteryN = sqlite3_column_int(stmt, 22);
plr->setCapped(CappedValueType::BATTERY_W, sqlite3_column_int(stmt, 21));
plr->setCapped(CappedValueType::BATTERY_N, sqlite3_column_int(stmt, 22));
plr->mentor = sqlite3_column_int(stmt, 23);
plr->iWarpLocationFlag = sqlite3_column_int(stmt, 24);
@@ -160,8 +126,6 @@ void Database::getPlayer(Player* plr, int id) {
sqlite3_finalize(stmt);
removeExpiredVehicles(plr);
// get quest inventory
sql = R"(
SELECT Slot, ID, Opt

View File

@@ -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 = ?;

View File

@@ -1,30 +0,0 @@
#include "lua/Manager.hpp"
#include <iostream>
using namespace LuaManager;
static lua_State *globalState = nullptr;
void LuaManager::init() {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
static LuaThread *startScript(const char *script) {
lua_State *L = lua_newthread(globalState);
LuaThread *T = new LuaThread(L, luaL_ref(L, LUA_REGISTRYINDEX));
if (luaL_dostring(L, script)) {
std::cout << "Lua error: " << lua_tostring(L, -1) << std::endl;
lua_pop(L, 1);
}
return T;
}
void LuaManager::init() {
globalState = luaL_newstate();
luaL_openlibs(globalState);
delete startScript("print(\"Hello from Lua!\")");
}

View File

@@ -1,7 +0,0 @@
#pragma once
#include "lua/Thread.hpp"
namespace LuaManager {
void init();
}

View File

@@ -1,17 +0,0 @@
#pragma once
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
struct LuaThread {
lua_State *L;
int ref;
LuaThread(lua_State *L, int ref) : L(L), ref(ref) {}
~LuaThread() {
luaL_unref(L, LUA_REGISTRYINDEX, ref);
}
};

View File

@@ -24,8 +24,6 @@
#include "Eggs.hpp"
#include "Rand.hpp"
#include "lua/Manager.hpp"
#include "settings.hpp"
#include "sandbox/Sandbox.hpp"
@@ -51,6 +49,7 @@ CNShardServer *shardServer = nullptr;
std::thread *shardThread = nullptr;
void startShard(CNShardServer* server) {
sandbox_thread_start();
server->start();
}
@@ -140,7 +139,6 @@ int main() {
Trading::init();
Database::open();
LuaManager::init();
switch (settings::EVENTMODE) {
case 0: break; // no event
@@ -153,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);
@@ -160,6 +160,7 @@ int main() {
shardThread = new std::thread(startShard, (CNShardServer*)shardServer);
sandbox_start();
sandbox_thread_start();
loginServer.start();
@@ -221,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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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);

View File

@@ -9,6 +9,7 @@
#include "MobAI.hpp"
#include "settings.hpp"
#include "TableData.hpp" // for flush()
#include "Items.hpp" // for checkAndRemoveExpiredItems()
#include <iostream>
#include <sstream>
@@ -23,6 +24,7 @@ CNShardServer::CNShardServer(uint16_t p) {
pHandler = &CNShardServer::handlePacket;
REGISTER_SHARD_TIMER(keepAliveTimer, 4000);
REGISTER_SHARD_TIMER(periodicSaveTimer, settings::DBSAVEINTERVAL*1000);
REGISTER_SHARD_TIMER(periodicItemExpireTimer, 60000);
init();
if (settings::MONITORENABLED)
@@ -88,6 +90,22 @@ void CNShardServer::periodicSaveTimer(CNServer* serv, time_t currTime) {
std::cout << "[INFO] Done." << std::endl;
}
void CNShardServer::periodicItemExpireTimer(CNServer* serv, time_t currTime) {
size_t playersWithExpiredItems = 0;
size_t itemsRemoved = 0;
for (const auto& [sock, player] : PlayerManager::players) {
// check and remove expired items
size_t removed = Items::checkAndRemoveExpiredItems(sock, player);
itemsRemoved += removed;
playersWithExpiredItems += (removed == 0 ? 0 : 1);
}
if (playersWithExpiredItems > 0) {
std::cout << "[INFO] Removed " << itemsRemoved << " expired items from " << playersWithExpiredItems << " players." << std::endl;
}
}
bool CNShardServer::checkExtraSockets(int i) {
return Monitor::acceptConnection(fds[i].fd, fds[i].revents);
}

View File

@@ -16,6 +16,7 @@ private:
static void keepAliveTimer(CNServer*, time_t);
static void periodicSaveTimer(CNServer* serv, time_t currTime);
static void periodicItemExpireTimer(CNServer* serv, time_t currTime);
public:
static std::map<uint32_t, PacketHandler> ShardPackets;

View File

@@ -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);

View File

@@ -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);
};

View File

@@ -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,28 @@ 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;
// drop fixes enabled
bool settings::DROPFIXESENABLED = false;
// less taro / fm while in a group
bool settings::LESSTAROFMINGROUPDISABLED = false;
// general reward percentages
int settings::TARORATE = 100;
int settings::FUSIONMATTERRATE = 100;
// should expired items in the bank disappear automatically?
bool settings::REMOVEEXPIREDITEMSFROMBANK = false;
void settings::init() {
INIReader reader("config.ini");
@@ -81,9 +101,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);
@@ -107,11 +130,30 @@ void settings::init() {
TDATADIR = reader.Get("shard", "tdatadir", TDATADIR);
PATCHDIR = reader.Get("shard", "patchdir", PATCHDIR);
ENABLEDPATCHES = reader.Get("shard", "enabledpatches", ENABLEDPATCHES);
DROPFIXESENABLED = reader.GetBoolean("shard", "dropfixesenabled", DROPFIXESENABLED);
LESSTAROFMINGROUPDISABLED = reader.GetBoolean("shard", "lesstarofmingroupdisabled", LESSTAROFMINGROUPDISABLED);
TARORATE = reader.GetInteger("shard", "tarorate", TARORATE);
FUSIONMATTERRATE = reader.GetInteger("shard", "fusionmatterrate", FUSIONMATTERRATE);
ACCLEVEL = reader.GetInteger("shard", "accountlevel", ACCLEVEL);
EVENTMODE = reader.GetInteger("shard", "eventmode", EVENTMODE);
DISABLEFIRSTUSEFLAG = reader.GetBoolean("shard", "disablefirstuseflag", DISABLEFIRSTUSEFLAG);
ANTICHEAT = reader.GetBoolean("shard", "anticheat", ANTICHEAT);
IZRACESCORECAPPED = reader.GetBoolean("shard", "izracescorecapped", IZRACESCORECAPPED);
REMOVEEXPIREDITEMSFROMBANK = reader.GetBoolean("shard", "removeexpireditemsfrombank", REMOVEEXPIREDITEMSFROMBANK);
MONITORENABLED = reader.GetBoolean("monitor", "enabled", MONITORENABLED);
MONITORPORT = reader.GetInteger("monitor", "port", MONITORPORT);
MONITORLISTENIP = reader.Get("monitor", "listenip", MONITORLISTENIP);
MONITORINTERVAL = reader.GetInteger("monitor", "interval", MONITORINTERVAL);
if (DROPFIXESENABLED) {
std::cout << "[INFO] Drop fixes enabled" << std::endl;
if (ENABLEDPATCHES.empty()) {
ENABLEDPATCHES = "0104-fixes";
} else {
ENABLEDPATCHES += " 0104-fixes";
if (ENABLEDPATCHES.find("1013") != std::string::npos) {
ENABLEDPATCHES += " 1013-fixes";
}
}
}
}

View File

@@ -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,15 @@ namespace settings {
extern int EVENTMODE;
extern bool MONITORENABLED;
extern int MONITORPORT;
extern std::string MONITORLISTENIP;
extern int MONITORINTERVAL;
extern bool DISABLEFIRSTUSEFLAG;
extern bool IZRACESCORECAPPED;
extern bool DROPFIXESENABLED;
extern bool LESSTAROFMINGROUPDISABLED;
extern int TARORATE;
extern int FUSIONMATTERRATE;
extern bool REMOVEEXPIREDITEMSFROMBANK;
void init();
}

2
tdata

Submodule tdata updated: 8c98c83682...fed031b972

14836
vendor/JSON.hpp vendored

File diff suppressed because it is too large Load Diff

1
vendor/Lua vendored

Submodule vendor/Lua deleted from 88246d621a

View File

@@ -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;

View File

@@ -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"

View File

@@ -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
View 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"