14 Commits

Author SHA1 Message Date
CakeLancelot
9af3b60d4d Fix compilation with MinGW on Windows
Prereqs: mingw-w64-x86_64-luajit, pkg-config
2021-05-12 20:55:40 -05:00
gsemaj
d5fc36f877 Fix bad null check and dynamic ID calculation 2021-05-09 09:36:50 -04:00
aa3dacc84b Fixed stack issue with lEvents 2021-05-09 08:40:39 -04:00
a29de394ee Updated NPCWrapper to use Vec3 2021-05-09 08:40:39 -04:00
ca299e0a5b Added NPC.speed setters & getters 2021-05-09 08:40:39 -04:00
794b881c4d Added NPCWrapper
- NPC.new, NPC:moveTo() & NPC.maxHealth have all been added
2021-05-09 08:40:39 -04:00
e8659962a5 Switched makefile to use pkg-config 2021-05-09 08:40:39 -04:00
2090062c75 Added Player:message() 2021-05-09 08:40:39 -04:00
b25b229eb2 Add Player.instance & Player:teleport() 2021-05-09 08:40:39 -04:00
c2853d9271 Added "/rscripts" command to reload all script states 2021-05-09 08:40:39 -04:00
cb85f7b7c9 Added Player.onChat event & Player:kick() 2021-05-09 08:40:39 -04:00
80f0ff7479 Added World, Entity & Player wrappers
- LuaWrapper.cpp/lua_autopush was updated to support CNSocket*
- Added basic getters added for Entity, (x, y, z & type)
- Added basic getter for Player, (name)
- Added bases getter for World, (version)
- Added World.onPlayerAdded & World.onPlayerRemoved events
2021-05-09 08:40:39 -04:00
2aa23a83de Added Lua Events
- LuaWrapper creates a simple event layer for lua states. Allowing events to have registered callbacks & be waited on until fired
- EventWrapper exposes this api to scripts
2021-05-09 08:40:39 -04:00
43aa4eaeb8 Added base lua manager
- CMakeLists.txt, Makefile, and appveyor now use luajit
- lua/LuaManager.[ch]pp has been added which includes a basic script loader & thread scheduler
- SCRIPTSDIR has been added to settings.[ch]pp, this specifies what directory lua scripts should be loaded from
- Makefile has been updated to include lua/LuaManager.[ch]pp
2021-05-09 08:40:39 -04:00
116 changed files with 12799 additions and 13807 deletions

View File

@@ -1,149 +0,0 @@
name: Check Builds
on:
push:
paths:
- src/**
- vendor/**
- .github/workflows/check-builds.yaml
- CMakeLists.txt
- Makefile
- tdata
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
paths:
- src/**
- vendor/**
- CMakeLists.txt
- Makefile
workflow_dispatch:
jobs:
ubuntu-build:
runs-on: ubuntu-22.04
steps:
- name: Set environment
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
shell: bash
- uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Install dependencies
run: sudo apt install clang cmake snap libsqlite3-dev -y && sudo snap install powershell --classic
- name: Check compilation
run: |
$versions = "104", "728", "1013"
foreach ($version in $versions) {
Write-Output "Cleaning old output"
Invoke-Expression "make clean"
if ($LASTEXITCODE -ne "0") {
Write-Error "make clean failed for version $version" -ErrorAction Stop
}
Write-Output "Building version $version"
Invoke-Expression "make -j8 PROTOCOL_VERSION=$version"
if ($LASTEXITCODE -ne "0") {
Write-Error "make failed for version $version" -ErrorAction Stop
}
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@v4
with:
name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
path: bin
windows-build:
runs-on: windows-2022
steps:
- name: Set environment
run: $s = $env:GITHUB_SHA.subString(0, 7); echo "SHORT_SHA=$s" >> $env:GITHUB_ENV
shell: pwsh
- uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Check compilation
run: |
$versions = "104", "728", "1013"
$configurations = "Release"
# "Debug" builds are disabled, since we don't really need them
$vsPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
Import-Module "$vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation
Invoke-Expression "vcpkg install sqlite3:x64-windows"
Invoke-Expression "vcpkg integrate install"
foreach ($version in $versions) {
if (Test-Path -LiteralPath "build") {
Remove-Item "build" -Recurse
Write-Output "Deleted existing build folder"
}
Invoke-Expression "cmake -B build -DPROTOCOL_VERSION=$version -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake"
if ($LASTEXITCODE -ne "0") {
Write-Error "cmake generation failed for version $version" -ErrorAction Stop
}
Write-Output "Generated build files for version $version"
foreach ($configuration in $configurations) {
Write-Output "Building version $version $configuration"
Invoke-Expression "msbuild build\OpenFusion.sln /maxcpucount:8 /p:BuildInParallel=true /p:CL_MPCount=8 /p:UseMultiToolTask=true /p:Configuration=$configuration"
if ($LASTEXITCODE -ne "0") {
Write-Error "msbuild build failed for version $version" -ErrorAction Stop
}
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@v4
with:
name: 'windows-vs2022-bin-x64-${{ env.SHORT_SHA }}'
path: bin
copy-artifacts:
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 }}
ENDPOINT: ${{ secrets.ENDPOINT }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: |
GITDESC=$(git describe --tags)
mkdir $GITDESC
echo "ARTDIR=$GITDESC" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
with:
path: ${{ env.ARTDIR }}
- name: Upload artifacts
shell: bash
run: |
sudo apt install zip -y
cd $ARTDIR
for build in *; do
cd $build
zip -r ../$build.zip *
cd ..
rm -r $build
done
cd ..
umask 077
printf %s "$BOT_SSH_KEY" > cdn_key
scp -i cdn_key -o StrictHostKeyChecking=no -r $ARTDIR $ENDPOINT

View File

@@ -1,38 +0,0 @@
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

2
.gitignore vendored
View File

@@ -16,5 +16,3 @@ build/
version.h version.h
infer-out infer-out
gmon.out gmon.out
*.bak

26
.vscode/launch.json vendored
View File

@@ -1,26 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug (Linux)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/fusion",
"cwd": "${workspaceFolder}"
},
{
"name": "Debug (Windows)",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/Debug/winfusion.exe",
"cwd": "${workspaceFolder}"
},
{
"name": "Release (Windows)",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/Release/winfusion.exe",
"cwd": "${workspaceFolder}"
}
]
}

View File

@@ -3,8 +3,7 @@ project(OpenFusion)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# OpenFusion supports multiple packet/struct versions # OpenFusion supports multiple packet/struct versions
# 104 is the default version to build which can be changed # 104 is the default version to build which can be changed
@@ -44,10 +43,8 @@ add_executable(openfusion ${SOURCES})
set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME}) set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME})
# find sqlite3 and use it target_link_libraries(openfusion sqlite3)
find_package(SQLite3 REQUIRED) target_link_libraries(openfusion lua51)
target_include_directories(openfusion PRIVATE ${SQLite3_INCLUDE_DIRS})
target_link_libraries(openfusion PRIVATE ${SQLite3_LIBRARIES})
# Makes it so config, tdata, etc. get picked up when starting via the debugger in VS # 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}") set_property(TARGET openfusion PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
@@ -57,5 +54,5 @@ set_property(TARGET openfusion PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_S
# It's not something you should do, but it's there if you need it... # It's not something you should do, but it's there if you need it...
if (NOT CMAKE_GENERATOR MATCHES "Visual Studio" AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND NOT CMAKE_GENERATOR MATCHES "MinGW Makefiles") if (NOT CMAKE_GENERATOR MATCHES "Visual Studio" AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND NOT CMAKE_GENERATOR MATCHES "MinGW Makefiles")
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
target_link_libraries(openfusion PRIVATE pthread) target_link_libraries(openfusion pthread)
endif() endif()

View File

@@ -26,7 +26,7 @@ Both are pretty short reads and following them will get you up to speed with bra
I will now cover a few examples of the complications people have encountered contributing to this project, how to understand them, overcome them and henceforth avoid them. I will now cover a few examples of the complications people have encountered contributing to this project, how to understand them, overcome them and henceforth avoid them.
### Dirty pull requests ## Dirty pull requests
Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge. Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge.
These are generally either: These are generally either:
@@ -44,7 +44,7 @@ If you read the above links, you'll note that this isn't exactly a perfect solut
The obvious issue, then, is that the people submitting dirty PRs are the exact people who don't *know* how to rebase their fork to continue submitting their work cleanly. The obvious issue, then, is that the people submitting dirty PRs are the exact people who don't *know* how to rebase their fork to continue submitting their work cleanly.
So they end up creating countless merge commits when pulling upstream on top of their own incompatible histories, and then submitting those merge commits in their PRs and the cycle continues. So they end up creating countless merge commits when pulling upstream on top of their own incompatible histories, and then submitting those merge commits in their PRs and the cycle continues.
### The details ## The details
A git commit is uniquely identified by its SHA1 hash. A git commit is uniquely identified by its SHA1 hash.
Its hash is generated from its contents, as well as the hash(es) of its parent commit(s). Its hash is generated from its contents, as well as the hash(es) of its parent commit(s).
@@ -52,7 +52,7 @@ Its hash is generated from its contents, as well as the hash(es) of its parent c
That means that even if two commits are exactly the same in terms of content, they're not the same commit if their parent commits differ (ex. if they've been rebased). That means that even if two commits are exactly the same in terms of content, they're not the same commit if their parent commits differ (ex. if they've been rebased).
So if you keep issuing `git pull`s after upstream has merged a rebased version of your PR, you will never re-synchronize with it, and will instead construct an alternate history polluted by pointless merge commits. So if you keep issuing `git pull`s after upstream has merged a rebased version of your PR, you will never re-synchronize with it, and will instead construct an alternate history polluted by pointless merge commits.
### The solution ## The solution
If you already have a messed-up fork and you have no changes on it that you're afraid to lose, the solution is simple: If you already have a messed-up fork and you have no changes on it that you're afraid to lose, the solution is simple:
@@ -67,7 +67,7 @@ If you do have some committed changes that haven't yet been merged upstream, you
If you do end up messing something up, don't worry, it most likely isn't really lost and `git reflog` is your friend. If you do end up messing something up, don't worry, it most likely isn't really lost and `git reflog` is your friend.
(You can checkout an arbitrary commit, and make it into its own branch with `git checkout -b BRANCH` or set a pre-exisitng branch to it with `git reset --hard COMMIT`) (You can checkout an arbitrary commit, and make it into its own branch with `git checkout -b BRANCH` or set a pre-exisitng branch to it with `git reset --hard COMMIT`)
### Avoiding the problem ## Avoiding the problem
When working on a changeset you want to submit back upstream, don't do it on the main branch. When working on a changeset you want to submit back upstream, don't do it on the main branch.
Create a work branch just for your changeset with `git checkout -b work`. Create a work branch just for your changeset with `git checkout -b work`.
@@ -81,34 +81,3 @@ That way you can always keep master in sync with upstream with `git pull --ff-on
Creating new branches for the rebase isn't strictly necessary since you can always return a branch to its previous state with `git reflog`. Creating new branches for the rebase isn't strictly necessary since you can always return a branch to its previous state with `git reflog`.
For moving uncommited changes around between branches, `git stash` is a real blessing. For moving uncommited changes around between branches, `git stash` is a real blessing.
## Code guidelines
Alright, you're up to speed on Git and ready to go. Here are a few specific code guidelines to try and follow:
### Match the styling
Pretty straightforward, make sure your code looks similar to the code around it. Match whitespacing, bracket styling, variable naming conventions, etc.
### Prefer short-circuiting
To minimize branching complexity (as this makes the code hard to read), we prefer to keep the number of `if-else` statements as low as possible. One easy way to achieve this is by doing an early return after branching into an `if` block and then writing the code for the other path outside the block entirely. You can find examples of this in practically every source file. Note that in a few select situations, this might actually make your code less elegant, which is why this isn't a strict rule. Lean towards short-circuiting and use your better judgement.
### Follow the include convention
This one matters a lot as it can cause cyclic dependencies and other code-breaking issues.
FOR HEADER FILES (.hpp):
- everything you use IN THE HEADER must be EXPLICITLY INCLUDED with the exception of things that fall under Core.hpp
- you may NOT include ANYTHING ELSE
FOR SOURCE FILES (.cpp):
- you can #include whatever you want as long as the partner header is included first
- anything that gets included by another include is fair game
- redundant includes are ok because they'll be harmless AS LONG AS our header files stay lean.
The point of this is NOT to optimize the number of includes used all around or make things more efficient necessarily. it's to improve readability & coherence and make it easier to avoid cyclical issues.
## When in doubt, ask
If you still have questions that were not answered here, feel free to ping a dev in the Discord server or on GitHub.

View File

@@ -1,41 +0,0 @@
# build
FROM alpine:3 as build
WORKDIR /usr/src/app
RUN apk update && apk upgrade && apk add \
linux-headers \
git \
clang18 \
make \
sqlite-dev
COPY src ./src
COPY vendor ./vendor
COPY .git ./.git
COPY Makefile CMakeLists.txt version.h.in ./
RUN sed -i 's/^CC=clang$/&-18/' Makefile
RUN sed -i 's/^CXX=clang++$/&-18/' Makefile
RUN make nosandbox -j$(nproc)
# prod
FROM alpine:3
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 MIT License
Copyright (c) 2020-2025 OpenFusion Contributors Copyright (c) 2020 Seth Stubbs
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -4,9 +4,11 @@ CC=clang
CXX=clang++ CXX=clang++
# -w suppresses all warnings (the part that's commented out helps me find memory leaks, it ruins performance though!) # -w suppresses all warnings (the part that's commented out helps me find memory leaks, it ruins performance though!)
# If compiling with ASAN, invoke like this: $ LSAN_OPTIONS=suppressions=suppr.txt bin/fusion # If compiling with ASAN, invoke like this: $ LSAN_OPTIONS=suppressions=suppr.txt bin/fusion
CFLAGS=-Wall -Wno-unknown-pragmas -O2 -fPIE -D_FORTIFY_SOURCE=1 -fstack-protector #-g3 -fsanitize=address CFLAGS=-O3 #-g3 -fsanitize=address
CXXFLAGS=$(CFLAGS) -std=c++17 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor LCFLAGS=`pkg-config --cflags luajit`
LDFLAGS=-lpthread -lsqlite3 -pie -Wl,-z,relro -Wl,-z,now #-g3 -fsanitize=address LLIBFLAGS=`pkg-config --libs luajit`
CXXFLAGS=-Wall -Wno-unknown-pragmas -std=c++17 -O2 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor $(LCFLAGS) #-g3 -fsanitize=address
LDFLAGS=-lpthread -lsqlite3 $(LLIBFLAGS) -lstdc++fs #-g3 -fsanitize=address
# specifies the name of our exectuable # specifies the name of our exectuable
SERVER=bin/fusion SERVER=bin/fusion
@@ -17,9 +19,9 @@ PROTOCOL_VERSION?=104
# Windows-specific # Windows-specific
WIN_CC=x86_64-w64-mingw32-gcc WIN_CC=x86_64-w64-mingw32-gcc
WIN_CXX=x86_64-w64-mingw32-g++ WIN_CXX=x86_64-w64-mingw32-g++
WIN_CFLAGS=-O2 -D_WIN32_WINNT=0x0601 -Wall -Wno-unknown-pragmas WIN_CFLAGS=-O3 #-g3 -fsanitize=address
WIN_CXXFLAGS=$(WIN_CFLAGS) -std=c++17 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor WIN_CXXFLAGS=-D_WIN32_WINNT=0x0601 -Wall -Wno-unknown-pragmas -std=c++17 -O3 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor $(LCFLAGS) #-g3 -fsanitize=address
WIN_LDFLAGS=-static -lws2_32 -lwsock32 -lsqlite3 WIN_LDFLAGS=-static -lws2_32 -lwsock32 -lsqlite3 $(LLIBFLAGS) -lstdc++fs #-g3 -fsanitize=address
WIN_SERVER=bin/winfusion.exe WIN_SERVER=bin/winfusion.exe
# C code; currently exclusively from vendored libraries # C code; currently exclusively from vendored libraries
@@ -43,14 +45,17 @@ CXXSRC=\
src/servers/CNLoginServer.cpp\ src/servers/CNLoginServer.cpp\
src/servers/CNShardServer.cpp\ src/servers/CNShardServer.cpp\
src/servers/Monitor.cpp\ src/servers/Monitor.cpp\
src/lua/LuaManager.cpp\
src/lua/EventWrapper.cpp\
src/lua/WorldWrapper.cpp\
src/lua/EntityWrapper.cpp\
src/lua/PlayerWrapper.cpp\
src/lua/NPCWrapper.cpp\
src/db/init.cpp\ src/db/init.cpp\
src/db/login.cpp\ src/db/login.cpp\
src/db/shard.cpp\ src/db/shard.cpp\
src/db/player.cpp\ src/db/player.cpp\
src/db/email.cpp\ src/db/email.cpp\
src/sandbox/seccomp.cpp\
src/sandbox/openbsd.cpp\
src/Buffs.cpp\
src/Chat.cpp\ src/Chat.cpp\
src/CustomCommands.cpp\ src/CustomCommands.cpp\
src/Entities.cpp\ src/Entities.cpp\
@@ -89,13 +94,20 @@ CXXHDR=\
src/servers/CNLoginServer.hpp\ src/servers/CNLoginServer.hpp\
src/servers/CNShardServer.hpp\ src/servers/CNShardServer.hpp\
src/servers/Monitor.hpp\ src/servers/Monitor.hpp\
src/lua/LuaManager.hpp\
src/lua/LuaWrapper.hpp\
src/lua/EventWrapper.hpp\
src/lua/WorldWrapper.hpp\
src/lua/EntityWrapper.hpp\
src/lua/PlayerWrapper.hpp\
src/lua/NPCWrapper.hpp\
src/db/Database.hpp\ src/db/Database.hpp\
src/db/internal.hpp\ src/db/internal.hpp\
src/sandbox/Sandbox.hpp\
vendor/bcrypt/BCrypt.hpp\ vendor/bcrypt/BCrypt.hpp\
vendor/INIReader.hpp\ vendor/INIReader.hpp\
vendor/JSON.hpp\ vendor/JSON.hpp\
src/Buffs.hpp\ vendor/INIReader.hpp\
vendor/JSON.hpp\
src/Chat.hpp\ src/Chat.hpp\
src/CustomCommands.hpp\ src/CustomCommands.hpp\
src/Entities.hpp\ src/Entities.hpp\
@@ -115,7 +127,6 @@ CXXHDR=\
src/settings.hpp\ src/settings.hpp\
src/Transport.hpp\ src/Transport.hpp\
src/TableData.hpp\ src/TableData.hpp\
src/Bucket.hpp\
src/Chunking.hpp\ src/Chunking.hpp\
src/Buddies.hpp\ src/Buddies.hpp\
src/Groups.hpp\ src/Groups.hpp\
@@ -134,8 +145,6 @@ HDR=$(CHDR) $(CXXHDR)
all: $(SERVER) all: $(SERVER)
windows: $(SERVER) windows: $(SERVER)
nosandbox: $(SERVER)
nolandlock: $(SERVER)
# assign Windows-specific values if targeting Windows # assign Windows-specific values if targeting Windows
windows : CC=$(WIN_CC) windows : CC=$(WIN_CC)
@@ -145,10 +154,7 @@ windows : CXXFLAGS=$(WIN_CXXFLAGS)
windows : LDFLAGS=$(WIN_LDFLAGS) windows : LDFLAGS=$(WIN_LDFLAGS)
windows : SERVER=$(WIN_SERVER) windows : SERVER=$(WIN_SERVER)
nosandbox : CFLAGS+=-DCONFIG_NOSANDBOX=1 .SUFFIX: .o .c .cpp .h .hpp
nolandlock : CFLAGS+=-DCONFIG_NOLANDLOCK=1
.SUFFIXES: .o .c .cpp .h .hpp
.c.o: .c.o:
$(CC) -c $(CFLAGS) -o $@ $< $(CC) -c $(CFLAGS) -o $@ $<
@@ -157,7 +163,7 @@ nolandlock : CFLAGS+=-DCONFIG_NOLANDLOCK=1
$(CXX) -c $(CXXFLAGS) -o $@ $< $(CXX) -c $(CXXFLAGS) -o $@ $<
# header timestamps are a prerequisite for OF object files # header timestamps are a prerequisite for OF object files
$(CXXOBJ): $(HDR) $(CXXOBJ): $(CXXHDR)
$(SERVER): $(OBJ) $(CHDR) $(CXXHDR) $(SERVER): $(OBJ) $(CHDR) $(CXXHDR)
mkdir -p bin mkdir -p bin
@@ -169,7 +175,7 @@ version.h:
src/main.o: version.h src/main.o: version.h
.PHONY: all windows nosandbox nolandlock clean nuke .PHONY: all windows clean nuke
# only gets rid of OpenFusion objects, so we don't need to # only gets rid of OpenFusion objects, so we don't need to
# recompile the libs every time # recompile the libs every time

View File

@@ -1,43 +1,32 @@
<p align="center"><img width="640" src="res/openfusion-hero.png" alt="OpenFusion Logo"></p> <p align="center"><img width="640" src="res/openfusion-hero.png" alt=""></p>
<p align="center"> <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/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://ci.appveyor.com/project/OpenFusionProject/openfusion"><img src="https://ci.appveyor.com/api/projects/status/github/OpenFusionProject/OpenFusion?svg=true" alt="AppVeyor"></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://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> <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> </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://openfusion.dev/docs/reference/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://github.com/OpenFusionProject/OpenFusion/wiki/FusionFall-Version-Support) for others.
## Usage ## Usage
### Getting Started ### Getting Started
#### Method A: Installer (Easiest)
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.3/OpenFusionClient-1.3.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. 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 OpenFusionLauncher.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect. 3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen. 4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
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. 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://openfusion.dev/docs/guides/running-on-linux/).
### Hosting a server ### Hosting a server
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.
2. For Server IP, enter the IP address and port of the login server. If you're hosting and playing on the same PC, this would be `127.0.0.1:23000`.
3. Lastly Game Version - select `beta-20100104` if you downloaded the original zip, or `beta-20111013` if you downloaded the academy zip.
5. Once you've added the server to the list, connect to it and log in. If you're having trouble with this, refer to steps 4 and 5 from the previous section.
If you want to run the latest development builds of the server, [compiled binaries (artifacts) for each functional commit can be found here.](http://cdn.dexlabs.systems/of-builds/) 1. Grab `OpenFusionServer-1.3-original.zip` or `OpenFusionServer-1.3-academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.3).
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: the default port is 23000, so the full IP with default settings would be 127.0.0.1:23000.
4. Once you've added the server to the list, connect to it and log in. If you're having trouble with this, refer to steps 4 and 5 from the previous section.
If you want, [compiled binaries (artifacts) for each new commit can be found on AppVeyor.](https://ci.appveyor.com/project/OpenFusionProject/openfusion)
For a more detailed overview of the game's architecture and how to configure it, read the following sections. For a more detailed overview of the game's architecture and how to configure it, read the following sections.
@@ -54,7 +43,10 @@ FusionFall consists of the following components:
The original game made use of the player's actual web browser to launch the game, but since then the NPAPI plugin interface the game relied on has been deprecated and is no longer available in most modern browsers. Both Retro and OpenFusion get around this issue by distributing an older version of Electron, a software package that is essentially a specialized web browser. The original game made use of the player's actual web browser to launch the game, but since then the NPAPI plugin interface the game relied on has been deprecated and is no longer available in most modern browsers. Both Retro and OpenFusion get around this issue by distributing an older version of Electron, a software package that is essentially a specialized web browser.
The browser/Electron client opens a web page with an `<embed>` tag of the appropriate MIME type, where the `src` param is the address of the game's `.unity3d` entrypoint. This triggers the browser to load an NPAPI plugin that handles said MIME type, in this case the Unity Web Player. The browser/Electron client opens a web page with an `<embed>` tag of MIME type `application/vnd.unity`, where the `src` param is the address of the game's `.unity3d` entrypoint.
This triggers the browser to load an NPAPI plugin that handles this MIME type, the Unity Web Player, which the browser looks for in `C:\Users\%USERNAME%\AppData\LocalLow\Unity\WebPlayer`.
The Web Player was previously copied there by `installUnity.bat`.
Note that the version of the web player distributed with OpenFusion expects a standard `UnityWeb` magic number for all assets, instead of Retro's modified `streamed` magic number. Note that the version of the web player distributed with OpenFusion expects a standard `UnityWeb` magic number for all assets, instead of Retro's modified `streamed` magic number.
This will potentially become relevant later, as people start experimenting and mixing and matching versions. This will potentially become relevant later, as people start experimenting and mixing and matching versions.
@@ -63,7 +55,7 @@ The web player will execute the game code, which will request the following file
`/assetInfo.php` contains the address from which to fetch the rest of the game's assets (the "dongresources"). `/assetInfo.php` contains the address from which to fetch the rest of the game's assets (the "dongresources").
Normally those would be hosted on the same web server as the gateway, but the OpenFusion distribution (in it's default configuration) doesn't use a web server at all! Normally those would be hosted on the same web server as the gateway, but the OpenFusion distribution (in it's default configuration) doesn't use a web server at all!
It instead loads the web pages locally using the `file://` schema, and fetches the game's assets from a standard web server. It loads the web pages locally using the `file://` schema, and fetches the game's assets from Turner's CDN (which is still hosting them to this day!).
`/loginInfo.php` contains the IP:port pair of the FusionFall login server, which the client will connect to. This login server drives the client while it's in the Character Selection menu, as well as Character Creation and the Tutorial. `/loginInfo.php` contains the IP:port pair of the FusionFall login server, which the client will connect to. This login server drives the client while it's in the Character Selection menu, as well as Character Creation and the Tutorial.
@@ -80,17 +72,17 @@ This just works if you're all under the same LAN, but if you want to play over t
## Compiling ## 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](https://openfusion.dev/docs/development/installing-sqlite-on-windows-using-vcpkg/). OpenFusion has one external dependency: SQLite. 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).
You have two choices for compiling OpenFusion: the included Makefile and the included CMakeLists file. You have two choices for compiling OpenFusion: the included Makefile and the included CMakeLists file.
### Makefile ### Makefile
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). 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).
### CMake ### CMake
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` 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`
## Contributing ## Contributing
@@ -99,13 +91,26 @@ If you'd like to contribute to this project, please read [CONTRIBUTING.md](CONTR
## Gameplay ## Gameplay
The goal of the project is to faithfully recreate the game as it was at the time of the targeted build. The goal of the project is to faithfully recreate the game as it was at the time of the targeted build.
While most features are implemented and the game is playable start to finish, there may be missing functionality or bugs present. The server is not yet complete, however, and some functionality is still missing.
Depending on the server configuration, you'll have access to certain commands. Because the server is still in development, ordinary players are allowed access to a few admin commands:
For the public servers: Original has item spawning, the ability to set player speed/jump height, and teleportation enabled (default account level 50). ![](res/sane_upsell.png)
Meanwhile the Academy server is more meant for legitimate playthroughs (default account level 99).
When hosting a local server, you will have access to all commands by default (account level 1). ### Movement commands
* A `/speed` of around 2400 or 3000 is nice.
* A `/jump` of about 50 will send you soaring
* [This map](res/dong_number_map.png) (credit to Danny O) is useful for `/warp` coordinates.
* `/goto` is useful for more precise teleportation (ie. for getting into Infected Zones, etc.).
For a list of available commands, see [this page](https://openfusion.dev/docs/reference/ingame-command-list/). ### Item commands
* `/itemN [type] [itemId] [amount]`
(Refer to the [item list](https://docs.google.com/spreadsheets/d/1mpoJ9iTHl_xLI4wQ_9UvIDYNcsDYscdkyaGizs43TCg/))
### Nano commands
* `/nano [id] (1-36)`
* `/nano_equip [id] (1-36) [slot] (0-2)`
* `/nano_unequip [slot] (0-2)`
* `/nano_active [slot] (0-2)`
### A full list of commands can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).

93
appveyor.yml Normal file
View File

@@ -0,0 +1,93 @@
version: 'openfusion-{branch}-{build}'
build_cloud: GCE us-east1-b n2-standard-8
skip_branch_with_pr: true
image:
- GCP-Windows-VS2019
- GCP-Linux-Ubuntu2004
platform:
- x64
configuration:
- Release
for:
-
matrix:
only:
- image: GCP-Linux-Ubuntu2004
install:
- sh: sudo apt install libluajit-5.1-dev -y
build_script:
- ps: |
$versions = "104", "728", "1013"
foreach ($version in $versions) {
Write-Output "Cleaning old output"
Invoke-Expression "make clean"
if ($LASTEXITCODE -ne "0") {
Write-Error "make clean failed for version $version" -ErrorAction Stop
}
Write-Output "Building version $version"
Invoke-Expression "make -j8 PROTOCOL_VERSION=$version"
if ($LASTEXITCODE -ne "0") {
Write-Error "make failed for version $version" -ErrorAction Stop
}
Rename-Item -Path "bin/fusion" -newName "$version-fusion"
Write-Output "Built version $version"
}
Copy-Item -Path "sql" -Destination "bin/sql" -Recurse
Copy-Item -Path "config.ini" -Destination "bin"
artifacts:
- path: bin
name: ubuntu20_04-bin-x64
type: zip
-
matrix:
only:
- image: GCP-Windows-VS2019
install:
- cmd: vcpkg install sqlite3:x64-windows
- cmd: vcpkg install luajit:x64-windows
- cmd: vcpkg integrate install
build_script:
- ps: |
$versions = "104", "728", "1013"
$configurations = "Release"
# "Debug" builds are disabled, since we don't really need them
# AppVeyor uses VS2019 Community
$vsPath = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community"
Import-Module "$vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation
foreach ($version in $versions) {
if (Test-Path -LiteralPath "build") {
Remove-Item "build" -Recurse
Write-Output "Deleted existing build folder"
}
Invoke-Expression "cmake -B build -DPROTOCOL_VERSION=$version"
if ($LASTEXITCODE -ne "0") {
Write-Error "cmake generation failed for version $version" -ErrorAction Stop
}
Write-Output "Generated build files for version $version"
foreach ($configuration in $configurations) {
Write-Output "Building version $version $configuration"
Invoke-Expression "msbuild build\OpenFusion.sln /maxcpucount:8 /p:BuildInParallel=true /p:CL_MPCount=8 /p:UseMultiToolTask=true /p:Configuration=$configuration"
if ($LASTEXITCODE -ne "0") {
Write-Error "msbuild build failed for version $version" -ErrorAction Stop
}
Rename-Item -Path "bin/$configuration" -newName "$version-$configuration"
Write-Output "Built version $version $configuration"
Copy-Item -Path "sql" -Destination "bin/$version-$configuration/sql" -Recurse
Copy-Item -Path "config.ini" -Destination "bin/$version-$configuration"
}
}
artifacts:
- path: bin
name: windows-vs2019-bin-x64
type: zip

View File

@@ -5,24 +5,12 @@
# 3 = print all packets # 3 = print all packets
verbosity=1 verbosity=1
# sandbox the process on supported platforms
sandbox=true
# Login Server configuration # Login Server configuration
[login] [login]
# must be kept in sync with loginInfo.php # must be kept in sync with loginInfo.php
port=23000 port=23000
# will all name wheel names be approved instantly?
acceptallwheelnames=true
# will all custom names be approved instantly? # will all custom names be approved instantly?
acceptallcustomnames=true 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? # how often should everything be flushed to the database?
# the default is 4 minutes # the default is 4 minutes
dbsaveinterval=240 dbsaveinterval=240
@@ -43,33 +31,6 @@ simulatemobs=true
# little message players see when they enter the game # little message players see when they enter the game
motd=Welcome to OpenFusion! 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 # The following are the default locations of the JSON files the server
# requires to run. You can override them by changing their values and # requires to run. You can override them by changing their values and
# uncommenting them (removing the leading # character from that line). # uncommenting them (removing the leading # character from that line).
@@ -128,8 +89,5 @@ eventmode=0
enabled=false enabled=false
# the port to listen for connections on # the port to listen for connections on
port=8003 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) # how often the listeners should be updated (in milliseconds)
interval=5000 interval=5000

View File

@@ -1,14 +0,0 @@
services:
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"
- "8003:8003"

28
scripts/test.lua Normal file
View File

@@ -0,0 +1,28 @@
print("Hello wtf!")
function onJoin(plr)
print(plr.type .. " " .. plr.name .. " joined from LUA!!")
plr.onChat:listen(function(msg)
print(plr.name .. " said : \'" .. msg .. "\'")
if msg == "kickme" then
plr:kick()
elseif msg == "hi" then
print("hello " .. plr.name)
elseif msg == "pet" then
local dog = NPC.new(plr.x, plr.y, plr.z, 3054)
while wait(2) and plr:exists() do
dog:moveTo(plr.x + math.random(-500, 500), plr.y + math.random(-500, 500), plr.z)
end
end
end)
end
World.onPlayerAdded:listen(onJoin)
for i, plr in ipairs(World.players) do
onJoin(plr)
end
wait(2)
print("Hello world ~2 seconds later! running protcol version " .. World.version)

View File

@@ -1,28 +0,0 @@
/*
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;
-- Change username column (Login) to be case-insensitive
CREATE TABLE Temp (
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,
PRIMARY KEY(AccountID AUTOINCREMENT)
);
INSERT INTO Temp SELECT * FROM Accounts;
DROP TABLE Accounts;
ALTER TABLE Temp RENAME TO Accounts;
-- Update DB Version
UPDATE Meta SET Value = 4 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,19 +0,0 @@
/*
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;

View File

@@ -1,8 +0,0 @@
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,16 +1,14 @@
CREATE TABLE IF NOT EXISTS Accounts ( CREATE TABLE IF NOT EXISTS Accounts (
AccountID INTEGER NOT NULL, AccountID INTEGER NOT NULL,
Login TEXT NOT NULL UNIQUE COLLATE NOCASE, Login TEXT NOT NULL UNIQUE,
Password TEXT NOT NULL, Password TEXT NOT NULL,
Selected INTEGER DEFAULT 1 NOT NULL, Selected INTEGER DEFAULT 1 NOT NULL,
AccountLevel INTEGER NOT NULL, AccountLevel INTEGER NOT NULL,
Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
BannedUntil INTEGER DEFAULT 0 NOT NULL, BannedUntil INTEGER DEFAULT 0 NOT NULL,
BannedSince INTEGER DEFAULT 0 NOT NULL, BannedSince INTEGER DEFAULT 0 NOT NULL,
BanReason TEXT DEFAULT '' NOT NULL, BanReason TEXT DEFAULT '' NOT NULL,
Email TEXT DEFAULT '' NOT NULL,
LastPasswordReset INTEGER DEFAULT 0 NOT NULL,
PRIMARY KEY(AccountID AUTOINCREMENT) PRIMARY KEY(AccountID AUTOINCREMENT)
); );
@@ -145,7 +143,7 @@ CREATE TABLE IF NOT EXISTS EmailItems (
UNIQUE (PlayerID, MsgIndex, Slot) UNIQUE (PlayerID, MsgIndex, Slot)
); );
CREATE TABLE IF NOT EXISTS RaceResults ( CREATE TABLE IF NOT EXISTS RaceResults(
EPID INTEGER NOT NULL, EPID INTEGER NOT NULL,
PlayerID INTEGER NOT NULL, PlayerID INTEGER NOT NULL,
Score INTEGER NOT NULL, Score INTEGER NOT NULL,
@@ -155,17 +153,9 @@ CREATE TABLE IF NOT EXISTS RaceResults (
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE 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, PlayerID INTEGER NOT NULL,
Code TEXT NOT NULL, Code TEXT NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE, FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Code) 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)
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +1,64 @@
#pragma once #pragma once
#include "core/Core.hpp" #include "core/Core.hpp"
#include "Combat.hpp"
#include "Entities.hpp" typedef void (*PowerHandler)(CNSocket*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
#include "Player.hpp"
#include <map> struct NanoPower {
#include <vector> int16_t skillType;
#include <assert.h> int32_t bitFlag;
int16_t timeBuffID;
PowerHandler handler;
const int COMBAT_TICKS_PER_DRAIN_PROC = 2; NanoPower(int16_t s, int32_t b, int16_t t, PowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain);
enum class SkillType { void handle(CNSocket *sock, std::vector<int> targetData, int16_t nanoID, int16_t skillID, int16_t duration, int16_t amount) {
DAMAGE = 1, if (handler == nullptr)
HEAL_HP = 2, return;
KNOCKDOWN = 3, // uses DamageNDebuff
SLEEP = 4, // uses DamageNDebuff
SNARE = 5, // uses DamageNDebuff
HEAL_STAMINA = 6,
STAMINA_SELF = 7,
STUN = 8, // uses DamageNDebuff
WEAPONSLOW = 9,
JUMP = 10,
RUN = 11,
STEALTH = 12,
SWIM = 13,
MINIMAPENEMY = 14,
MINIMAPTRESURE = 15,
PHOENIX = 16,
PROTECTBATTERY = 17,
PROTECTINFECTION = 18,
REWARDBLOB = 19,
REWARDCASH = 20,
BATTERYDRAIN = 21,
CORRUPTIONATTACK = 22,
INFECTIONDAMAGE = 23,
KNOCKBACK = 24,
FREEDOM = 25,
PHOENIX_GROUP = 26,
RECALL = 27,
RECALL_GROUP = 28,
RETROROCKET_SELF = 29,
BLOODSUCKING = 30,
BOUNDINGBALL = 31,
INVULNERABLE = 32,
NANOSTIMPAK = 33,
RETURNHOMEHEAL = 34,
BUFFHEAL = 35,
EXTRABANK = 36,
CORRUPTIONATTACKWIN = 38,
CORRUPTIONATTACKLOSE = 39,
};
enum class SkillEffectTarget { handler(sock, targetData, nanoID, skillID, duration, amount, skillType, bitFlag, timeBuffID);
POINT = 1,
SELF = 2,
CONE = 3,
WEAPON = 4,
AREA_SELF = 5,
AREA_TARGET = 6
};
enum class SkillTargetType {
MOBS = 1,
PLAYERS = 2,
GROUP = 3
};
enum class SkillDrainType {
ACTIVE = 1,
PASSIVE = 2
};
struct SkillResult {
size_t size;
uint8_t payload[MAX_SKILLRESULT_SIZE];
SkillResult(size_t len, void* dat) {
assert(len <= MAX_SKILLRESULT_SIZE);
size = len;
memcpy(payload, dat, len);
} }
SkillResult() { };
size = 0;
typedef void (*MobPowerHandler)(Mob*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
struct MobPower {
int16_t skillType;
int32_t bitFlag;
int16_t timeBuffID;
MobPowerHandler handler;
MobPower(int16_t s, int32_t b, int16_t t, MobPowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
void handle(Mob *mob, std::vector<int> targetData, int16_t skillID, int16_t duration, int16_t amount) {
if (handler == nullptr)
return;
handler(mob, targetData, skillID, duration, amount, skillType, bitFlag, timeBuffID);
} }
}; };
struct SkillData { struct SkillData {
SkillType skillType; // eST int skillType;
SkillEffectTarget effectTarget; int targetType;
int effectType; // always 1? int drainType;
SkillTargetType targetType;
SkillDrainType drainType;
int effectArea; int effectArea;
int batteryUse[4]; int batteryUse[4];
int durationTime[4]; int durationTime[4];
int powerIntensity[4];
int valueTypes[3];
int values[3][4];
}; };
namespace Abilities { namespace Nanos {
extern std::vector<NanoPower> NanoPowers;
extern std::map<int32_t, SkillData> SkillTable; extern std::map<int32_t, SkillData> SkillTable;
void useNanoSkill(CNSocket*, SkillData*, sNano&, std::vector<ICombatant*>); void nanoUnbuff(CNSocket* sock, std::vector<int> targetData, int32_t bitFlag, int16_t timeBuffID, int16_t amount, bool groupPower);
void useNPCSkill(EntityRef, int skillID, std::vector<ICombatant*>); int applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags);
std::vector<ICombatant*> matchTargets(ICombatant*, SkillData*, int, int32_t*); std::vector<int> findTargets(Player* plr, int skillID, CNPacketData* data = nullptr);
int getCSTBFromST(SkillType skillType); }
namespace Combat {
extern std::vector<MobPower> MobPowers;
} }

View File

@@ -1,40 +0,0 @@
#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

@@ -1,10 +1,15 @@
#include "Buddies.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp" #include "servers/CNShardServer.hpp"
#include "Buddies.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Buddies.hpp"
#include "db/Database.hpp"
#include "Items.hpp"
#include "db/Database.hpp"
#include <iostream>
#include <chrono>
#include <algorithm>
#include <thread>
using namespace Buddies; using namespace Buddies;
@@ -30,7 +35,7 @@ static bool playerHasBuddyWithID(Player* plr, int buddyID) {
#pragma endregion #pragma endregion
// Refresh buddy list // Refresh buddy list
void Buddies::sendBuddyList(CNSocket* sock) { void Buddies::refreshBuddyList(CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
int buddyCnt = Database::getNumBuddies(plr); int buddyCnt = Database::getNumBuddies(plr);
@@ -41,9 +46,9 @@ void Buddies::sendBuddyList(CNSocket* sock) {
// initialize response struct // initialize response struct
size_t resplen = sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC) + buddyCnt * sizeof(sBuddyBaseInfo); size_t resplen = sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC) + buddyCnt * sizeof(sBuddyBaseInfo);
uint8_t respbuf[CN_PACKET_BODY_SIZE]; uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE); memset(respbuf, 0, resplen);
sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC* resp = (sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC*)respbuf; 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)); sBuddyBaseInfo* respdata = (sBuddyBaseInfo*)(respbuf + sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC));
@@ -277,6 +282,15 @@ static void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) {
// Getting buddy state // Getting buddy state
static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) { static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); 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); INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);

View File

@@ -1,10 +1,12 @@
#pragma once #pragma once
#include "Player.hpp"
#include "core/Core.hpp"
#include "core/Core.hpp" #include "core/Core.hpp"
namespace Buddies { namespace Buddies {
void init(); void init();
// Buddy list // Buddy list
void sendBuddyList(CNSocket* sock); void refreshBuddyList(CNSocket* sock);
} }

View File

@@ -1,198 +0,0 @@
#include "Buffs.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
using namespace Buffs;
void Buff::tick(time_t currTime) {
auto it = stacks.begin();
while(it != stacks.end()) {
BuffStack& stack = *it;
//if(onTick) onTick(self, this, currTime);
if(stack.durationTicks == 0) {
BuffStack deadStack = stack;
it = stacks.erase(it);
if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack);
} else {
if(stack.durationTicks > 0) stack.durationTicks--;
it++;
}
}
}
void Buff::combatTick(time_t currTime) {
if(onCombatTick) onCombatTick(self, this, currTime);
}
void Buff::clear() {
while(!stacks.empty()) {
BuffStack stack = stacks.back();
stacks.pop_back();
if(onUpdate) onUpdate(self, this, ETBU_DEL, &stack);
}
}
void Buff::clear(BuffClass buffClass) {
auto it = stacks.begin();
while(it != stacks.end()) {
BuffStack& stack = *it;
if(stack.buffStackClass == buffClass) {
BuffStack deadStack = stack;
it = stacks.erase(it);
if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack);
} else it++;
}
}
void Buff::addStack(BuffStack* stack) {
stacks.push_back(*stack);
if(onUpdate) onUpdate(self, this, ETBU_ADD, &stacks.back());
}
bool Buff::hasClass(BuffClass buffClass) {
for(BuffStack& stack : stacks) {
if(stack.buffStackClass == buffClass)
return true;
}
return false;
}
BuffClass Buff::maxClass() {
BuffClass buffClass = BuffClass::NONE;
for(BuffStack& stack : stacks) {
if(stack.buffStackClass > buffClass)
buffClass = stack.buffStackClass;
}
return buffClass;
}
int Buff::getValue(BuffValueSelector selector) {
if(isStale()) return 0;
int value = selector == BuffValueSelector::NET_TOTAL ? 0 : stacks.front().value;
for(BuffStack& stack : stacks) {
switch(selector)
{
case BuffValueSelector::NET_TOTAL:
value += stack.value;
break;
case BuffValueSelector::MIN_VALUE:
if(stack.value < value) value = stack.value;
break;
case BuffValueSelector::MAX_VALUE:
if(stack.value > value) value = stack.value;
break;
case BuffValueSelector::MIN_MAGNITUDE:
if(abs(stack.value) < abs(value)) value = stack.value;
break;
case BuffValueSelector::MAX_MAGNITUDE:
default:
if(abs(stack.value) > abs(value)) value = stack.value;
}
}
return value;
}
EntityRef Buff::getLastSource() {
if(stacks.empty())
return self;
return stacks.back().source;
}
bool Buff::isStale() {
return stacks.empty();
}
/* This will practically never do anything important, but it's here just in case */
void Buff::updateCallbacks(BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fOnCombatTick) {
if(!onUpdate) onUpdate = fOnUpdate;
if(!onCombatTick) onCombatTick = fOnCombatTick;
}
#pragma region Handlers
void Buffs::timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack) {
if(self.kind != EntityKind::PLAYER)
return; // not implemented
Player* plr = (Player*)self.getEntity();
if(plr == nullptr)
return; // sanity check
if(status == ETBU_DEL && !buff->isStale())
return; // no premature effect deletion
int cbf = plr->getCompositeCondition();
sTimeBuff payload{};
if(status == ETBU_ADD) {
payload.iValue = buff->getValue(BuffValueSelector::MAX_MAGNITUDE);
// we need to explicitly add the ECSB for this buff,
// in case this is the first stack in and the entry
// in the buff map doesn't yet exist
if(buff->id > 0) cbf |= CSB_FROM_ECSB(buff->id);
}
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = buff->id; // eCharStatusTimeBuffID
pkt.eTBU = status; // eTimeBuffUpdate
pkt.eTBT = (int)stack->buffStackClass;
pkt.iConditionBitFlag = cbf;
pkt.TimeBuff = payload;
self.sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
void Buffs::timeBuffTick(EntityRef self, Buff* buff) {
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not implemented
Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK, pkt);
pkt.eCT = combatant->getCharType();
pkt.iID = combatant->getID();
pkt.iTB_ID = buff->id;
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
}
void Buffs::timeBuffTimeout(EntityRef self) {
if(self.kind != EntityKind::PLAYER && self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not a combatant
Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt); // send a buff timeout to other players
int32_t eCharType = combatant->getCharType();
pkt.eCT = eCharType == 4 ? 2 : eCharType; // convention not followed by client here
pkt.iID = combatant->getID();
pkt.iConditionBitFlag = combatant->getCompositeCondition();
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
void Buffs::tickDrain(EntityRef self, Buff* buff, int mult) {
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not implemented
Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
int damage = combatant->getMaxHP() / 100 * 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_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;
pkt->eCT = combatant->getCharType();
pkt->iTB_ID = ECSB_BOUNDINGBALL;
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
drain->iDamage = dealt;
drain->iHP = combatant->getCurrentHP();
drain->eCT = pkt->eCT;
drain->iID = pkt->iID;
NPCManager::sendToViewable(self.getEntity(), (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
}
#pragma endregion

View File

@@ -1,93 +0,0 @@
#pragma once
#include "core/Core.hpp"
#include "EntityRef.hpp"
#include <vector>
#include <functional>
/* forward declaration(s) */
class Buff;
template<class... Types>
using BuffCallback = std::function<void(EntityRef, Buff*, Types...)>;
#define CSB_FROM_ECSB(x) (1 << (x - 1))
enum class BuffClass {
NONE = ETBT_NONE,
NANO = ETBT_NANO,
GROUP_NANO = ETBT_GROUPNANO,
EGG = ETBT_SHINY,
ENVIRONMENT = ETBT_LANDEFFECT,
ITEM = ETBT_ITEM,
CASH_ITEM = ETBT_CASHITEM
};
enum class BuffValueSelector {
MAX_VALUE,
MIN_VALUE,
MAX_MAGNITUDE,
MIN_MAGNITUDE,
NET_TOTAL
};
struct BuffStack {
int durationTicks;
int value;
EntityRef source;
BuffClass buffStackClass;
};
class Buff {
private:
EntityRef self;
std::vector<BuffStack> stacks;
public:
int id;
/* called just after a stack is added or removed */
BuffCallback<int, BuffStack*> onUpdate;
/* called when the buff is combat-ticked */
BuffCallback<time_t> onCombatTick;
void tick(time_t);
void combatTick(time_t);
void clear();
void clear(BuffClass buffClass);
void addStack(BuffStack* stack);
/*
* Sometimes we need to determine if a buff
* is covered by a certain class, ex: nano
* vs. coco egg in the case of infection protection
*/
bool hasClass(BuffClass buffClass);
BuffClass maxClass();
int getValue(BuffValueSelector selector);
EntityRef getLastSource();
/*
* In general, a Buff object won't exist
* unless it has stacks. However, when
* popping stacks during iteration (onExpire),
* stacks will be empty for a brief moment
* when the last stack is popped.
*/
bool isStale();
void updateCallbacks(BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fonTick);
Buff(int iid, EntityRef pSelf, BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fOnCombatTick, BuffStack* firstStack)
: self(pSelf), id(iid), onUpdate(fOnUpdate), onCombatTick(fOnCombatTick) {
addStack(firstStack);
}
};
namespace Buffs {
void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack);
void timeBuffTick(EntityRef self, Buff* buff);
void timeBuffTimeout(EntityRef self);
void tickDrain(EntityRef self, Buff* buff, int mult);
}

View File

@@ -1,13 +1,10 @@
#include "BuiltinCommands.hpp" #include "BuiltinCommands.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Chat.hpp"
#include "Items.hpp" #include "Items.hpp"
#include "Missions.hpp" #include "Missions.hpp"
#include "Chat.hpp"
#include "Nanos.hpp" #include "Nanos.hpp"
#include "Rand.hpp"
// helper function, not a packet handler // helper function, not a packet handler
void BuiltinCommands::setSpecialState(CNSocket* sock, CNPacketData* data) { void BuiltinCommands::setSpecialState(CNSocket* sock, CNPacketData* data) {
@@ -72,33 +69,31 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
// Handle serverside value-changes // Handle serverside value-changes
switch (setData->iSetValueType) { switch (setData->iSetValueType) {
case CN_GM_SET_VALUE_TYPE__HP: case 1:
response.iSetValue = plr->HP = setData->iSetValue; plr->HP = setData->iSetValue;
break; break;
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY: case 2:
plr->setCapped(CappedValueType::BATTERY_W, setData->iSetValue); plr->batteryW = setData->iSetValue;
response.iSetValue = plr->batteryW; // caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
break; break;
case CN_GM_SET_VALUE_TYPE__NANO_BATTERY: case 3:
plr->setCapped(CappedValueType::BATTERY_N, setData->iSetValue); plr->batteryN = setData->iSetValue;
response.iSetValue = plr->batteryN; // caps
if (plr->batteryN > 9999)
plr->batteryN = 9999;
break; break;
case CN_GM_SET_VALUE_TYPE__FUSION_MATTER: case 4:
plr->setCapped(CappedValueType::FUSIONMATTER, setData->iSetValue); Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
Missions::updateFusionMatter(sock);
response.iSetValue = plr->fusionmatter;
break; break;
case CN_GM_SET_VALUE_TYPE__CANDY: case 5:
plr->setCapped(CappedValueType::TAROS, setData->iSetValue); plr->money = setData->iSetValue;
response.iSetValue = plr->money;
break;
case CN_GM_SET_VALUE_TYPE__SPEED:
case CN_GM_SET_VALUE_TYPE__JUMP:
response.iSetValue = setData->iSetValue;
break; break;
} }
response.iPC_ID = setData->iPC_ID; response.iPC_ID = setData->iPC_ID;
response.iSetValue = setData->iSetValue;
response.iSetValueType = setData->iSetValueType; response.iSetValueType = setData->iSetValueType;
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE); sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);
@@ -140,60 +135,6 @@ 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 // 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) { static void locatePlayer(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
@@ -306,17 +247,17 @@ static void teleportPlayer(CNSocket *sock, CNPacketData *data) {
uint64_t instance = plr->instanceID; uint64_t instance = plr->instanceID;
const int unstickRange = 400; const int unstickRange = 400;
switch ((eCN_GM_TeleportType)req->eTeleportType) { switch (req->eTeleportType) {
case eCN_GM_TeleportType::MyLocation: case eCN_GM_TeleportMapType__MyLocation:
PlayerManager::sendPlayerTo(targetSock, plr->x, plr->y, plr->z, instance); PlayerManager::sendPlayerTo(targetSock, plr->x, plr->y, plr->z, instance);
break; break;
case eCN_GM_TeleportType::MapXYZ: case eCN_GM_TeleportMapType__MapXYZ:
instance = req->iToMap; instance = req->iToMap;
// fallthrough // fallthrough
case eCN_GM_TeleportType::XYZ: case eCN_GM_TeleportMapType__XYZ:
PlayerManager::sendPlayerTo(targetSock, req->iToX, req->iToY, req->iToZ, instance); PlayerManager::sendPlayerTo(targetSock, req->iToX, req->iToY, req->iToZ, instance);
break; break;
case eCN_GM_TeleportType::SomeoneLocation: case eCN_GM_TeleportMapType__SomeoneLocation:
// player to teleport to // player to teleport to
goalSock = PlayerManager::getSockFromAny(req->eGoalPCSearchBy, req->iGoalPC_ID, req->iGoalPC_UID, goalSock = PlayerManager::getSockFromAny(req->eGoalPCSearchBy, req->iGoalPC_ID, req->iGoalPC_UID,
AUTOU16TOU8(req->szGoalPC_FirstName), AUTOU16TOU8(req->szGoalPC_LastName)); AUTOU16TOU8(req->szGoalPC_FirstName), AUTOU16TOU8(req->szGoalPC_LastName));
@@ -328,7 +269,7 @@ static void teleportPlayer(CNSocket *sock, CNPacketData *data) {
PlayerManager::sendPlayerTo(targetSock, goalPlr->x, goalPlr->y, goalPlr->z, goalPlr->instanceID); PlayerManager::sendPlayerTo(targetSock, goalPlr->x, goalPlr->y, goalPlr->z, goalPlr->instanceID);
break; break;
case eCN_GM_TeleportType::Unstick: case eCN_GM_TeleportMapType__Unstick:
targetPlr = PlayerManager::getPlayer(targetSock); targetPlr = PlayerManager::getPlayer(targetSock);
PlayerManager::sendPlayerTo(targetSock, targetPlr->x - unstickRange/2 + Rand::rand(unstickRange), PlayerManager::sendPlayerTo(targetSock, targetPlr->x - unstickRange/2 + Rand::rand(unstickRange),
@@ -342,56 +283,35 @@ static void itemGMGiveHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
if (plr->accountLevel > 50) { if (plr->accountLevel > 50) {
// TODO: send fail packet // TODO: send fail packet
return; return;
} }
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp); if (itemreq->eIL == 2) {
// Quest item, not a real item, handle this later, stubbed for now
} else if (itemreq->eIL == 1 && itemreq->Item.iType >= 0 && itemreq->Item.iType <= 10) {
if (itemreq->eIL == 1) { if (Items::ItemData.find(std::pair<int32_t, int32_t>(itemreq->Item.iID, itemreq->Item.iType)) == Items::ItemData.end()) {
if (Items::ItemData.find(std::pair<int32_t, int32_t>(itemreq->Item.iID, itemreq->Item.iType)) == Items::ItemData.end()
|| itemreq->Item.iType < 0 || itemreq->Item.iType > 10) {
// invalid item // invalid item
std::cout << "[WARN] Item id " << itemreq->Item.iID << " with type " << itemreq->Item.iType << " is invalid (give item)" << std::endl; std::cout << "[WARN] Item id " << itemreq->Item.iID << " with type " << itemreq->Item.iType << " is invalid (give item)" << std::endl;
return; return;
} }
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp);
resp.eIL = itemreq->eIL;
resp.iSlotNum = itemreq->iSlotNum;
if (itemreq->Item.iType == 10) { if (itemreq->Item.iType == 10) {
// item is vehicle, set expiration date // item is vehicle, set expiration date
// set time limit: current time + 7days // set time limit: current time + 7days
itemreq->Item.iTimeLimit = getTimestamp() + 604800; itemreq->Item.iTimeLimit = getTimestamp() + 604800;
} }
resp.Item = itemreq->Item;
plr->Inven[itemreq->iSlotNum] = itemreq->Item; plr->Inven[itemreq->iSlotNum] = itemreq->Item;
} else if (itemreq->eIL == 2) {
int id = itemreq->Item.iID;
int slot = Missions::findQSlot(plr, id);
if (slot == -1) { sock->sendPacket(resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
std::cout << "[WARN] Player has no room for quest items" << std::endl;
return;
}
if (id != 0)
std::cout << "new qitem in slot " << slot << std::endl;
// update player
if (id != 0) {
plr->QInven[slot].iType = 8;
plr->QInven[slot].iID = id;
plr->QInven[slot].iOpt += itemreq->Item.iOpt;
// destroy the item if its 0
if (plr->QInven[slot].iOpt == 0)
memset(&plr->QInven[slot], 0, sizeof(sItemBase));
}
std::cout << "Item id " << id << " is in slot " << slot << " of count " << plr->QInven[slot].iOpt << std::endl;
} }
resp.eIL = itemreq->eIL;
resp.iSlotNum = itemreq->iSlotNum;
resp.Item = itemreq->Item;
sock->sendPacket(resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
} }
static void nanoGMGiveHandler(CNSocket* sock, CNPacketData* data) { static void nanoGMGiveHandler(CNSocket* sock, CNPacketData* data) {
@@ -417,7 +337,6 @@ void BuiltinCommands::init() {
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH, setGMSpecialSwitchPlayer); 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_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_PC_LOCATION, locatePlayer);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_KICK_PLAYER, kickPlayer); REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_KICK_PLAYER, kickPlayer);

View File

@@ -1,14 +1,12 @@
#include "Chat.hpp" #include "Chat.hpp"
#include "servers/CNShardServer.hpp"
#include "servers/Monitor.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Groups.hpp"
#include "CustomCommands.hpp" #include "CustomCommands.hpp"
#include <assert.h> #include <assert.h>
std::vector<std::string> Chat::dump;
using namespace Chat; using namespace Chat;
static void chatHandler(CNSocket* sock, CNPacketData* data) { static void chatHandler(CNSocket* sock, CNPacketData* data) {
@@ -21,13 +19,17 @@ static void chatHandler(CNSocket* sock, CNPacketData* data) {
return; return;
} }
// if the player has an onChat Lua event registered, call it
if (plr->onChat != nullptr)
plr->onChat->call(fullChat.c_str());
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT) if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT)
return; return;
std::string logLine = "[FreeChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat; std::string logLine = "[FreeChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
std::cout << logLine << std::endl; std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine); dump.push_back(logLine);
// send to client // send to client
INITSTRUCT(sP_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC, resp);
@@ -50,7 +52,7 @@ static void menuChatHandler(CNSocket* sock, CNPacketData* data) {
std::string logLine = "[MenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat; std::string logLine = "[MenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
std::cout << logLine << std::endl; std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine); dump.push_back(logLine);
// send to client // send to client
INITSTRUCT(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, resp);
@@ -102,14 +104,14 @@ static void announcementHandler(CNSocket* sock, CNPacketData* data) {
memcpy(msg.szAnnounceMsg, announcement->szAnnounceMsg, sizeof(msg.szAnnounceMsg)); memcpy(msg.szAnnounceMsg, announcement->szAnnounceMsg, sizeof(msg.szAnnounceMsg));
std::map<CNSocket*, Player*>::iterator it; 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) { switch (announcement->iAreaType) {
case 0: // area (all players in viewable chunks) case 0: // area (all players in viewable chunks)
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE); sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
PlayerManager::sendToViewable(sock, msg, P_FE2CL_GM_REP_PC_ANNOUNCE); PlayerManager::sendToViewable(sock, msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
break; break;
case 1: // channel case 1: // shard
case 2: // shard case 2: // world
break; // not applicable to OpenFusion
case 3: // global (all players) case 3: // global (all players)
for (it = PlayerManager::players.begin(); it != PlayerManager::players.end(); it++) { for (it = PlayerManager::players.begin(); it != PlayerManager::players.end(); it++) {
CNSocket* allSock = it->first; CNSocket* allSock = it->first;
@@ -119,12 +121,9 @@ static void announcementHandler(CNSocket* sock, CNPacketData* data) {
break; break;
} }
std::string logLine = std::to_string(announcement->iAreaType) + " " std::string logLine = "[Bcast " + std::to_string(announcement->iAreaType) + "] " + PlayerManager::getPlayerName(plr, false) + ": " + AUTOU16TOU8(msg.szAnnounceMsg);
+ std::to_string(announcement->iAnnounceType) + " " std::cout << logLine << std::endl;
+ std::to_string(announcement->iDuringTime) + " " dump.push_back("**" + logLine + "**");
+ PlayerManager::getPlayerName(plr, false) + ": " + AUTOU16TOU8(msg.szAnnounceMsg);
std::cout << "Broadcast " << logLine << std::endl;
Monitor::bcasts.push_back(logLine);
} }
// Buddy freechatting // Buddy freechatting
@@ -157,7 +156,7 @@ static void buddyChatHandler(CNSocket* sock, CNPacketData* data) {
std::string logLine = "[BuddyChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat; std::string logLine = "[BuddyChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
std::cout << logLine << std::endl; std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine); dump.push_back(logLine);
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat)); U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
@@ -187,7 +186,7 @@ static void buddyMenuChatHandler(CNSocket* sock, CNPacketData* data) {
std::string logLine = "[BuddyMenuChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat; std::string logLine = "[BuddyMenuChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
std::cout << logLine << std::endl; std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine); dump.push_back(logLine);
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat)); U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
@@ -220,7 +219,7 @@ static void tradeChatHandler(CNSocket* sock, CNPacketData* data) {
std::string logLine = "[TradeChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat; std::string logLine = "[TradeChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
std::cout << logLine << std::endl; std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine); dump.push_back(logLine);
resp.iEmoteCode = pacdat->iEmoteCode; resp.iEmoteCode = pacdat->iEmoteCode;
sock->sendPacket(resp, P_FE2CL_REP_PC_TRADE_EMOTES_CHAT); sock->sendPacket(resp, P_FE2CL_REP_PC_TRADE_EMOTES_CHAT);
@@ -230,6 +229,10 @@ static void tradeChatHandler(CNSocket* sock, CNPacketData* data) {
static void groupChatHandler(CNSocket* sock, CNPacketData* data) { static void groupChatHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE*)data->buf; sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat)); std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
@@ -243,7 +246,7 @@ static void groupChatHandler(CNSocket* sock, CNPacketData* data) {
std::string logLine = "[GroupChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat; std::string logLine = "[GroupChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
std::cout << logLine << std::endl; std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine); dump.push_back(logLine);
// send to client // send to client
INITSTRUCT(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, resp);
@@ -252,21 +255,22 @@ static void groupChatHandler(CNSocket* sock, CNPacketData* data) {
resp.iSendPCID = plr->iID; resp.iSendPCID = plr->iID;
resp.iEmoteCode = chat->iEmoteCode; resp.iEmoteCode = chat->iEmoteCode;
if (plr->group == nullptr) Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
else
Groups::sendToGroup(plr->group, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
} }
static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) { static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE*)data->buf; sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat)); std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
std::string logLine = "[GroupMenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat; std::string logLine = "[GroupMenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
std::cout << logLine << std::endl; std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine); dump.push_back(logLine);
// send to client // send to client
INITSTRUCT(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, resp);
@@ -275,9 +279,7 @@ static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) {
resp.iSendPCID = plr->iID; resp.iSendPCID = plr->iID;
resp.iEmoteCode = chat->iEmoteCode; resp.iEmoteCode = chat->iEmoteCode;
if (plr->group == nullptr) Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
Groups::sendToGroup(plr->group, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
} }
// we only allow plain ascii, at least for now // we only allow plain ascii, at least for now

View File

@@ -2,12 +2,10 @@
#define CMD_PREFIX '/' #define CMD_PREFIX '/'
#include "core/Core.hpp" #include "servers/CNShardServer.hpp"
#include <string>
#include <vector>
namespace Chat { namespace Chat {
extern std::vector<std::string> dump;
void init(); void init();
void sendServerMessage(CNSocket* sock, std::string msg); // uses MOTD void sendServerMessage(CNSocket* sock, std::string msg); // uses MOTD

View File

@@ -1,30 +1,17 @@
#include "Chunking.hpp" #include "Chunking.hpp"
#include "PlayerManager.hpp"
#include "Player.hpp"
#include "MobAI.hpp"
#include "NPCManager.hpp" #include "NPCManager.hpp"
#include "Bucket.hpp" #include "settings.hpp"
#include "Combat.hpp"
#include <assert.h> #include "Eggs.hpp"
using namespace Chunking; 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; std::map<ChunkPos, Chunk*> Chunking::chunks;
static void newChunk(ChunkPos pos) { static void newChunk(ChunkPos pos) {
if (chunkExists(pos)) { if (chunkExists(pos)) {
std::cout << "[WARN] Tried to create a chunk that already exists" << std::endl; std::cout << "[WARN] Tried to create a chunk that already exists\n";
return; return;
} }
@@ -34,13 +21,13 @@ static void newChunk(ChunkPos pos) {
// add the chunk to the cache of all players and NPCs in the surrounding chunks // add the chunk to the cache of all players and NPCs in the surrounding chunks
std::set<Chunk*> surroundings = getViewableChunks(pos); std::set<Chunk*> surroundings = getViewableChunks(pos);
for (Chunk* c : surroundings) for (Chunk* c : surroundings)
for (const EntityRef ref : c->entities) for (const EntityRef& ref : c->entities)
ref.getEntity()->viewableChunks.insert(chunk); ref.getEntity()->viewableChunks.insert(chunk);
} }
static void deleteChunk(ChunkPos pos) { static void deleteChunk(ChunkPos pos) {
if (!chunkExists(pos)) { if (!chunkExists(pos)) {
std::cout << "[WARN] Tried to delete a chunk that doesn't exist" << std::endl; std::cout << "[WARN] Tried to delete a chunk that doesn't exist\n";
return; return;
} }
@@ -49,24 +36,24 @@ static void deleteChunk(ChunkPos pos) {
// remove the chunk from the cache of all players and NPCs in the surrounding chunks // remove the chunk from the cache of all players and NPCs in the surrounding chunks
std::set<Chunk*> surroundings = getViewableChunks(pos); std::set<Chunk*> surroundings = getViewableChunks(pos);
for(Chunk* c : surroundings) for(Chunk* c : surroundings)
for (const EntityRef ref : c->entities) for (const EntityRef& ref : c->entities)
ref.getEntity()->viewableChunks.erase(chunk); ref.getEntity()->viewableChunks.erase(chunk);
chunks.erase(pos); // remove from map chunks.erase(pos); // remove from map
delete chunk; // free from memory delete chunk; // free from memory
} }
void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef ref) { void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef& ref) {
if (!chunkExists(chunkPos)) if (!chunkExists(chunkPos))
return; // shouldn't happen return; // shouldn't happen
chunks[chunkPos]->entities.insert(ref); chunks[chunkPos]->entities.insert(ref);
if (ref.kind == EntityKind::PLAYER) if (ref.type == EntityType::PLAYER)
chunks[chunkPos]->nplayers++; chunks[chunkPos]->nplayers++;
} }
void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) { void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef& ref) {
if (!chunkExists(chunkPos)) if (!chunkExists(chunkPos))
return; // do nothing if chunk doesn't even exist return; // do nothing if chunk doesn't even exist
@@ -74,7 +61,7 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) {
chunk->entities.erase(ref); // gone chunk->entities.erase(ref); // gone
if (ref.kind == EntityKind::PLAYER) if (ref.type == EntityType::PLAYER)
chunks[chunkPos]->nplayers--; chunks[chunkPos]->nplayers--;
assert(chunks[chunkPos]->nplayers >= 0); assert(chunks[chunkPos]->nplayers >= 0);
@@ -83,82 +70,13 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) {
deleteChunk(chunkPos); deleteChunk(chunkPos);
} }
template<class T, size_t N> void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
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(); Entity *ent = ref.getEntity();
bool alive = ent->isExtant(); bool alive = ent->isAlive();
std::vector<Bucket<sPCAppearanceData, MAX_PC_PER_AROUND>> pcAppearances; // TODO: maybe optimize this, potentially using AROUND packets?
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 (Chunk *chunk : chnks) {
for (const EntityRef otherRef : chunk->entities) { for (const EntityRef& otherRef : chunk->entities) {
// skip oneself // skip oneself
if (ref == otherRef) if (ref == otherRef)
continue; continue;
@@ -166,76 +84,31 @@ void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
Entity *other = otherRef.getEntity(); Entity *other = otherRef.getEntity();
// notify all visible players of the existence of this Entity // notify all visible players of the existence of this Entity
if (alive && otherRef.kind == EntityKind::PLAYER) { if (alive && otherRef.type == EntityType::PLAYER) {
ent->enterIntoViewOf(otherRef.sock); ent->enterIntoViewOf(otherRef.sock);
} }
// notify this *player* of the existence of all visible Entities // notify this *player* of the existence of all visible Entities
if (ref.kind == EntityKind::PLAYER && other->isExtant()) { if (ref.type == EntityType::PLAYER && other->isAlive()) {
sPCAppearanceData pcData; other->enterIntoViewOf(ref.sock);
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 // for mobs, increment playersInView
if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER) if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER)
((Mob*)ent)->playersInView++; ((Mob*)ent)->playersInView++;
if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER) if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER)
((Mob*)other)->playersInView++; ((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) { void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
Entity *ent = ref.getEntity(); Entity *ent = ref.getEntity();
bool alive = ent->isExtant(); bool alive = ent->isAlive();
std::vector<Bucket<int32_t, MAX_IDS_PER_AROUND_DEL>> pcDisappearances; // TODO: same as above
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 (Chunk *chunk : chnks) {
for (const EntityRef otherRef : chunk->entities) { for (const EntityRef& otherRef : chunk->entities) {
// skip oneself // skip oneself
if (ref == otherRef) if (ref == otherRef)
continue; continue;
@@ -243,55 +116,22 @@ void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef re
Entity *other = otherRef.getEntity(); Entity *other = otherRef.getEntity();
// notify all visible players of the departure of this Entity // notify all visible players of the departure of this Entity
if (alive && otherRef.kind == EntityKind::PLAYER) { if (alive && otherRef.type == EntityType::PLAYER) {
ent->disappearFromViewOf(otherRef.sock); ent->disappearFromViewOf(otherRef.sock);
} }
// notify this *player* of the departure of all visible Entities // notify this *player* of the departure of all visible Entities
if (ref.kind == EntityKind::PLAYER && other->isExtant()) { if (ref.type == EntityType::PLAYER && other->isAlive()) {
int32_t id; other->disappearFromViewOf(ref.sock);
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 // for mobs, decrement playersInView
if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER) if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER)
((Mob*)ent)->playersInView--; ((Mob*)ent)->playersInView--;
if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER) if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER)
((Mob*)other)->playersInView--; ((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) { static void emptyChunk(ChunkPos chunkPos) {
@@ -309,8 +149,8 @@ static void emptyChunk(ChunkPos chunkPos) {
// unspawn all of the mobs/npcs // unspawn all of the mobs/npcs
std::set refs(chunk->entities); std::set refs(chunk->entities);
for (const EntityRef ref : refs) { for (const EntityRef& ref : refs) {
if (ref.kind == EntityKind::PLAYER) if (ref.type == EntityType::PLAYER)
assert(0); assert(0);
// every call of this will check if the chunk is empty and delete it if so // every call of this will check if the chunk is empty and delete it if so
@@ -318,7 +158,7 @@ static void emptyChunk(ChunkPos chunkPos) {
} }
} }
void Chunking::updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to) { void Chunking::updateEntityChunk(const EntityRef& ref, ChunkPos from, ChunkPos to) {
Entity* ent = ref.getEntity(); Entity* ent = ref.getEntity();
// move to other chunk's player set // move to other chunk's player set
@@ -360,7 +200,7 @@ bool Chunking::chunkExists(ChunkPos chunk) {
} }
ChunkPos Chunking::chunkPosAt(int posX, int posY, uint64_t instanceID) { ChunkPos Chunking::chunkPosAt(int posX, int posY, uint64_t instanceID) {
return ChunkPos(posX / (settings::VIEWDISTANCE / 3), posY / (settings::VIEWDISTANCE / 3), instanceID); return std::make_tuple(posX / (settings::VIEWDISTANCE / 3), posY / (settings::VIEWDISTANCE / 3), instanceID);
} }
std::set<Chunk*> Chunking::getViewableChunks(ChunkPos chunk) { std::set<Chunk*> Chunking::getViewableChunks(ChunkPos chunk) {
@@ -373,7 +213,7 @@ std::set<Chunk*> Chunking::getViewableChunks(ChunkPos chunk) {
// grabs surrounding chunks if they exist // grabs surrounding chunks if they exist
for (int i = -1; i < 2; i++) { for (int i = -1; i < 2; i++) {
for (int z = -1; z < 2; z++) { for (int z = -1; z < 2; z++) {
ChunkPos pos = ChunkPos(x+i, y+z, inst); ChunkPos pos = std::make_tuple(x+i, y+z, inst);
// if chunk exists, add it to the set // if chunk exists, add it to the set
if (chunkExists(pos)) if (chunkExists(pos))
@@ -422,53 +262,54 @@ void Chunking::createInstance(uint64_t instanceID) {
std::cout << "Creating instance " << instanceID << std::endl; std::cout << "Creating instance " << instanceID << std::endl;
for (ChunkPos &coords : templateChunks) { for (ChunkPos &coords : templateChunks) {
for (const EntityRef ref : chunks[coords]->entities) { for (const EntityRef& ref : chunks[coords]->entities) {
if (ref.kind == EntityKind::PLAYER) if (ref.type == EntityType::PLAYER)
continue; continue;
int npcID = ref.id; int npcID = ref.id;
BaseNPC* baseNPC = (BaseNPC*)ref.getEntity(); BaseNPC* baseNPC = (BaseNPC*)ref.getEntity();
// make a copy of each NPC in the template chunks and put them in the new instance // make a copy of each NPC in the template chunks and put them in the new instance
if (baseNPC->kind == EntityKind::MOB) { if (baseNPC->type == EntityType::MOB) {
if (((Mob*)baseNPC)->groupLeader != 0 && ((Mob*)baseNPC)->groupLeader != npcID) if (((Mob*)baseNPC)->groupLeader != 0 && ((Mob*)baseNPC)->groupLeader != npcID)
continue; // follower; don't copy individually continue; // follower; don't copy individually
Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->angle, Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle,
instanceID, baseNPC->type, NPCManager::NPCData[baseNPC->type], NPCManager::nextId--); instanceID, baseNPC->appearanceData.iNPCType, NPCManager::NPCData[baseNPC->appearanceData.iNPCType], NPCManager::nextId--);
NPCManager::NPCs[newMob->id] = newMob; NPCManager::NPCs[newMob->appearanceData.iNPC_ID] = newMob;
// if in a group, copy over group members as well // if in a group, copy over group members as well
if (((Mob*)baseNPC)->groupLeader != 0) { if (((Mob*)baseNPC)->groupLeader != 0) {
newMob->groupLeader = newMob->id; // set leader ID for new leader newMob->groupLeader = newMob->appearanceData.iNPC_ID; // set leader ID for new leader
Mob* mobData = (Mob*)baseNPC; Mob* mobData = (Mob*)baseNPC;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (mobData->groupMember[i] != 0) { if (mobData->groupMember[i] != 0) {
int followerID = NPCManager::nextId--; // id for follower int followerID = NPCManager::nextId--; // id for follower
BaseNPC* baseFollower = NPCManager::NPCs[mobData->groupMember[i]]; // follower from template BaseNPC* baseFollower = NPCManager::NPCs[mobData->groupMember[i]]; // follower from template
// new follower instance // new follower instance
Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->angle, Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->appearanceData.iAngle,
instanceID, baseFollower->type, NPCManager::NPCData[baseFollower->type], followerID); instanceID, baseFollower->appearanceData.iNPCType, NPCManager::NPCData[baseFollower->appearanceData.iNPCType], followerID);
// add follower to NPC maps // add follower to NPC maps
NPCManager::NPCs[followerID] = newMobFollower; NPCManager::NPCs[followerID] = newMobFollower;
// set follower-specific properties // set follower-specific properties
newMobFollower->groupLeader = newMob->id; newMobFollower->groupLeader = newMob->appearanceData.iNPC_ID;
newMobFollower->offsetX = ((Mob*)baseFollower)->offsetX; newMobFollower->offsetX = ((Mob*)baseFollower)->offsetX;
newMobFollower->offsetY = ((Mob*)baseFollower)->offsetY; newMobFollower->offsetY = ((Mob*)baseFollower)->offsetY;
// add follower copy to leader copy // add follower copy to leader copy
newMob->groupMember[i] = followerID; newMob->groupMember[i] = followerID;
NPCManager::updateNPCPosition(followerID, baseFollower->x, baseFollower->y, baseFollower->z, NPCManager::updateNPCPosition(followerID, baseFollower->x, baseFollower->y, baseFollower->z,
instanceID, baseFollower->angle); instanceID, baseFollower->appearanceData.iAngle);
} }
} }
} }
NPCManager::updateNPCPosition(newMob->id, baseNPC->x, baseNPC->y, baseNPC->z, NPCManager::updateNPCPosition(newMob->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->angle); instanceID, baseNPC->appearanceData.iAngle);
} else { } else {
BaseNPC* newNPC = new BaseNPC(baseNPC->angle, instanceID, baseNPC->type, NPCManager::nextId--); BaseNPC* newNPC = new BaseNPC(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle,
NPCManager::NPCs[newNPC->id] = newNPC; instanceID, baseNPC->appearanceData.iNPCType, NPCManager::nextId--);
NPCManager::updateNPCPosition(newNPC->id, baseNPC->x, baseNPC->y, baseNPC->z, NPCManager::NPCs[newNPC->appearanceData.iNPC_ID] = newNPC;
instanceID, baseNPC->angle); NPCManager::updateNPCPosition(newNPC->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->appearanceData.iAngle);
} }
} }
} }

View File

@@ -1,26 +1,22 @@
#pragma once #pragma once
#include "EntityRef.hpp" #include "core/Core.hpp"
#include "Entities.hpp"
#include <utility>
#include <set> #include <set>
#include <map> #include <map>
#include <vector> #include <tuple>
#include <algorithm>
class Chunk { class Chunk {
public: public:
//std::set<CNSocket*> players;
//std::set<int32_t> NPCs;
std::set<EntityRef> entities; std::set<EntityRef> entities;
int nplayers = 0; int nplayers = 0;
}; };
// to help the readability of ChunkPos
typedef std::tuple<int, int, uint64_t> _ChunkPos;
class ChunkPos : public _ChunkPos {
public:
ChunkPos() : _ChunkPos(0, 0, (uint64_t) -1) {}
ChunkPos(int x, int y, uint64_t inst) : _ChunkPos(x, y, inst) {}
};
enum { enum {
INSTANCE_OVERWORLD, // default instance every player starts in INSTANCE_OVERWORLD, // default instance every player starts in
INSTANCE_IZ, // these aren't actually used INSTANCE_IZ, // these aren't actually used
@@ -30,15 +26,13 @@ enum {
namespace Chunking { namespace Chunking {
extern std::map<ChunkPos, Chunk*> chunks; extern std::map<ChunkPos, Chunk*> chunks;
extern const ChunkPos INVALID_CHUNK; void updateEntityChunk(const EntityRef& ref, ChunkPos from, ChunkPos to);
void updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to); void trackEntity(ChunkPos chunkPos, const EntityRef& ref);
void untrackEntity(ChunkPos chunkPos, const EntityRef& ref);
void trackEntity(ChunkPos chunkPos, const EntityRef ref); void addEntityToChunks(std::set<Chunk*> chnks, const EntityRef& ref);
void untrackEntity(ChunkPos chunkPos, const EntityRef ref); void removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef& ref);
void addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref);
void removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref);
bool chunkExists(ChunkPos chunk); bool chunkExists(ChunkPos chunk);
ChunkPos chunkPosAt(int posX, int posY, uint64_t instanceID); ChunkPos chunkPosAt(int posX, int posY, uint64_t instanceID);

View File

@@ -1,331 +1,22 @@
#include "Combat.hpp" #include "Combat.hpp"
#include "servers/CNShardServer.hpp"
#include "Rand.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Nanos.hpp" #include "Nanos.hpp"
#include "NPCManager.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Groups.hpp"
#include "Transport.hpp"
#include "Racing.hpp"
#include "Abilities.hpp" #include "Abilities.hpp"
#include "Buffs.hpp" #include "Rand.hpp"
#include <assert.h> #include <assert.h>
#include <iostream>
#include <functional>
using namespace Combat; using namespace Combat;
/// Player Id -> Bullet Id -> Bullet /// Player Id -> Bullet Id -> Bullet
std::map<int32_t, std::map<int8_t, Bullet>> Combat::Bullets; std::map<int32_t, std::map<int8_t, Bullet>> Combat::Bullets;
#pragma region Player
bool Player::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) {
if(!isAlive())
return false;
if(!hasBuff(buffId)) {
buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack);
return true;
}
buffs[buffId]->updateCallbacks(onUpdate, onTick);
buffs[buffId]->addStack(stack);
return false;
}
Buff* Player::getBuff(int buffId) {
if(hasBuff(buffId)) {
return buffs[buffId];
}
return nullptr;
}
void Player::removeBuff(int buffId) {
if(hasBuff(buffId)) {
buffs[buffId]->clear();
delete buffs[buffId];
buffs.erase(buffId);
}
}
void Player::removeBuff(int buffId, BuffClass buffClass) {
if(hasBuff(buffId)) {
buffs[buffId]->clear(buffClass);
// buff might not be stale since another buff class might remain
if(buffs[buffId]->isStale()) {
delete buffs[buffId];
buffs.erase(buffId);
}
}
}
void Player::clearBuffs(bool force) {
auto it = buffs.begin();
while(it != buffs.end()) {
Buff* buff = (*it).second;
if(!force) buff->clear();
delete buff;
it = buffs.erase(it);
}
}
bool Player::hasBuff(int buffId) {
auto buff = buffs.find(buffId);
return buff != buffs.end() && !buff->second->isStale();
}
int Player::getCompositeCondition() {
int conditionBitFlag = 0;
for(auto buff : buffs) {
if(!buff.second->isStale() && buff.second->id > 0)
conditionBitFlag |= CSB_FROM_ECSB(buff.first);
}
return conditionBitFlag;
}
int Player::takeDamage(EntityRef src, int amt) {
int dmg = amt;
if(HP - dmg < 0) dmg = HP;
HP -= dmg;
return dmg;
}
int Player::heal(EntityRef src, int amt) {
int heal = amt;
if(HP + heal > getMaxHP()) heal = getMaxHP() - HP;
HP += heal;
return heal;
}
bool Player::isAlive() {
return HP > 0;
}
int Player::getCurrentHP() {
return HP;
}
int Player::getMaxHP() {
return PC_MAXHEALTH(level);
}
int Player::getLevel() {
return level;
}
std::vector<EntityRef> Player::getGroupMembers() {
std::vector<EntityRef> members;
if(group != nullptr)
members = group->members;
else
members.push_back(PlayerManager::getSockFromID(iID));
return members;
}
int32_t Player::getCharType() {
return 1; // eCharType (eCT_PC)
}
int32_t Player::getID() {
return iID;
}
EntityRef Player::getRef() {
return EntityRef(PlayerManager::getSockFromID(iID));
}
void Player::step(time_t currTime) {
CNSocket* sock = getRef().sock;
// nanos
for (int i = 0; i < 3; i++) {
if (activeNano != 0 && equippedNanos[i] == activeNano) { // tick active nano
sNano& nano = Nanos[activeNano];
int drainRate = 0;
if (Abilities::SkillTable.find(nano.iSkillID) != Abilities::SkillTable.end()) {
// nano has skill data
SkillData* skill = &Abilities::SkillTable[nano.iSkillID];
int boost = Nanos::getNanoBoost(this);
if (skill->drainType == SkillDrainType::PASSIVE)
drainRate = skill->batteryUse[boost * 3];
}
nano.iStamina -= 1 + drainRate / 5;
if (nano.iStamina <= 0)
Nanos::summonNano(sock, -1, true); // unsummon nano silently
} else if (Nanos[equippedNanos[i]].iStamina < 150) { // tick resting nano
sNano& nano = Nanos[equippedNanos[i]];
if (nano.iStamina < 150)
nano.iStamina += 1;
}
}
// buffs
for(auto buffEntry : buffs) {
buffEntry.second->combatTick(currTime);
if(!isAlive())
break; // unsafe to keep ticking if we're dead
}
}
#pragma endregion
#pragma region CombatNPC
bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) {
if(!isAlive())
return false;
if (this->state != AIState::COMBAT && this->state != AIState::ROAMING)
return false;
if(!hasBuff(buffId)) {
buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack);
return true;
}
buffs[buffId]->updateCallbacks(onUpdate, onTick);
buffs[buffId]->addStack(stack);
return false;
}
Buff* CombatNPC::getBuff(int buffId) {
if(hasBuff(buffId)) {
return buffs[buffId];
}
return nullptr;
}
void CombatNPC::removeBuff(int buffId) {
if(hasBuff(buffId)) {
buffs[buffId]->clear();
delete buffs[buffId];
buffs.erase(buffId);
}
}
void CombatNPC::removeBuff(int buffId, BuffClass buffClass) {
if(hasBuff(buffId)) {
buffs[buffId]->clear(buffClass);
// buff might not be stale since another buff class might remain
if(buffs[buffId]->isStale()) {
delete buffs[buffId];
buffs.erase(buffId);
}
}
}
void CombatNPC::clearBuffs(bool force) {
auto it = buffs.begin();
while(it != buffs.end()) {
Buff* buff = (*it).second;
if(!force) buff->clear();
delete buff;
it = buffs.erase(it);
}
}
bool CombatNPC::hasBuff(int buffId) {
auto buff = buffs.find(buffId);
return buff != buffs.end() && !buff->second->isStale();
}
int CombatNPC::getCompositeCondition() {
int conditionBitFlag = 0;
for(auto buff : buffs) {
if(!buff.second->isStale() && buff.second->id > 0)
conditionBitFlag |= CSB_FROM_ECSB(buff.first);
}
return conditionBitFlag;
}
int CombatNPC::takeDamage(EntityRef src, int amt) {
int dmg = amt;
if(hp - dmg < 0) dmg = hp;
hp -= dmg;
if(hp <= 0) transition(AIState::DEAD, src);
return dmg;
}
int CombatNPC::heal(EntityRef src, int amt) {
int heal = amt;
if(hp + heal > getMaxHP()) heal = getMaxHP() - hp;
hp += heal;
return heal;
}
bool CombatNPC::isAlive() {
return hp > 0;
}
int CombatNPC::getCurrentHP() {
return hp;
}
int CombatNPC::getMaxHP() {
return maxHealth;
}
int CombatNPC::getLevel() {
return level;
}
std::vector<EntityRef> CombatNPC::getGroupMembers() {
std::vector<EntityRef> members;
if(group != nullptr)
members = group->members;
else
members.push_back(id);
return members;
}
int32_t CombatNPC::getCharType() {
if(kind == EntityKind::MOB)
return 4; // eCharType (eCT_MOB)
return 2; // eCharType (eCT_NPC)
}
int32_t CombatNPC::getID() {
return id;
}
EntityRef CombatNPC::getRef() {
return EntityRef(id);
}
void CombatNPC::step(time_t currTime) {
if(stateHandlers.find(state) != stateHandlers.end())
stateHandlers[state](this, currTime);
else {
std::cout << "[WARN] State " << (int)state << " has no handler; going inactive" << std::endl;
transition(AIState::INACTIVE, id);
}
}
void CombatNPC::transition(AIState newState, EntityRef src) {
state = newState;
if (transitionHandlers.find(newState) != transitionHandlers.end())
transitionHandlers[newState](this, src);
else {
std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl;
transition(AIState::INACTIVE, id);
}
// trigger special NPCEvents, if applicable
for (NPCEvent& event : NPCManager::NPCEvents)
if (event.triggerState == newState && event.npcType == type)
event.handler(this);
}
#pragma endregion
static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shouldCrit, static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shouldCrit,
bool batteryBoost, int attackerStyle, bool batteryBoost, int attackerStyle,
int defenderStyle, int difficulty) { int defenderStyle, int difficulty) {
@@ -365,10 +56,14 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
return ret; return ret;
} }
static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTargets) { static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
auto pkt = (sP_CL2FE_REQ_PC_ATTACK_NPCs*)data->buf;
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
time_t currTime = getTime(); auto targets = (int32_t*)data->trailers;
// rapid fire anti-cheat
// TODO: move this out of here, when generalizing packet frequency validation
time_t currTime = getTime();
if (currTime - plr->lastShot < plr->fireRate * 80) if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0) else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
@@ -376,28 +71,11 @@ static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTarget
plr->lastShot = currTime; plr->lastShot = currTime;
// 3+ targets should never be possible if (pkt->iNPCCnt > 3) // 3+ targets should never be possible
if (!allowManyTargets && targetCount > 3) plr->suspicionRating += 10000;
plr->suspicionRating += 10001;
// kill the socket when the player is too suspicious if (plr->suspicionRating > 10000) // kill the socket when the player is too suspicious
if (plr->suspicionRating > 10000) {
sock->kill(); sock->kill();
CNShardServer::_killConnection(sock);
return true;
}
return false;
}
static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
auto pkt = (sP_CL2FE_REQ_PC_ATTACK_NPCs*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
auto targets = (int32_t*)data->trailers;
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt, false))
return;
/* /*
* IMPORTANT: This validates memory safety in addition to preventing * IMPORTANT: This validates memory safety in addition to preventing
@@ -423,7 +101,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
BaseNPC* npc = NPCManager::NPCs[targets[i]]; BaseNPC* npc = NPCManager::NPCs[targets[i]];
if (npc->kind != EntityKind::MOB) { if (npc->type != EntityType::MOB) {
std::cout << "[WARN] pcAttackNpcs: NPC is not a mob" << std::endl; std::cout << "[WARN] pcAttackNpcs: NPC is not a mob" << std::endl;
return; return;
} }
@@ -441,12 +119,16 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty), damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
plr->subtractCapped(CappedValueType::BATTERY_W, 6 + difficulty); if (plr->batteryW >= 6 + difficulty)
damage.first = mob->takeDamage(sock, damage.first); plr->batteryW -= 6 + difficulty;
else
plr->batteryW = 0;
respdata[i].iID = mob->id; damage.first = hitMob(sock, mob, damage.first);
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iDamage = damage.first; respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->hp; respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
} }
@@ -468,12 +150,12 @@ void Combat::npcAttackPc(Mob *mob, time_t currTime) {
INITVARPACKET(respbuf, sP_FE2CL_NPC_ATTACK_PCs, pkt, sAttackResult, atk); INITVARPACKET(respbuf, sP_FE2CL_NPC_ATTACK_PCs, pkt, sAttackResult, atk);
auto damage = getDamage(450 + (int)mob->data["m_iPower"], plr->defense, true, false, -1, -1, 0); auto damage = getDamage(450 + (int)mob->data["m_iPower"], plr->defense, false, false, -1, -1, 0);
if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
plr->HP -= damage.first; plr->HP -= damage.first;
pkt->iNPC_ID = mob->id; pkt->iNPC_ID = mob->appearanceData.iNPC_ID;
pkt->iPCCnt = 1; pkt->iPCCnt = 1;
atk->iID = plr->iID; atk->iID = plr->iID;
@@ -485,25 +167,122 @@ void Combat::npcAttackPc(Mob *mob, time_t currTime) {
PlayerManager::sendToViewable(mob->target, respbuf, P_FE2CL_NPC_ATTACK_PCs); PlayerManager::sendToViewable(mob->target, respbuf, P_FE2CL_NPC_ATTACK_PCs);
if (plr->HP <= 0) { if (plr->HP <= 0) {
if (!MobAI::aggroCheck(mob, getTime())) mob->target = nullptr;
mob->transition(AIState::RETREAT, mob->target); mob->state = MobState::RETREAT;
if (!MobAI::aggroCheck(mob, currTime)) {
MobAI::clearDebuff(mob);
if (mob->groupLeader != 0)
MobAI::groupRetreat(mob);
}
} }
} }
/* int Combat::hitMob(CNSocket *sock, Mob *mob, int damage) {
* When a group of players is doing missions together, we want them to all get // cannot kill mobs multiple times; cannot harm retreating mobs
* quest items at the same time, but we don't want the odds of quest item if (mob->state != MobState::ROAMING && mob->state != MobState::COMBAT) {
* drops from different missions to be linked together. That's why we use a return 0; // no damage
* single RNG roll per mission task, and every group member shares that same }
* set of rolls.
*/
void Combat::genQItemRolls(std::vector<Player*> players, std::map<int, int>& rolls) {
for (int i = 0; i < players.size(); i++) {
Player* member = players[i]; if (mob->skillStyle >= 0)
for (int j = 0; j < ACTIVE_MISSION_COUNT; j++) return 0; // don't hurt a mob casting corruption
if (member->tasks[j] != 0)
rolls[member->tasks[j]] = Rand::rand(); if (mob->state == MobState::ROAMING) {
assert(mob->target == nullptr);
MobAI::enterCombat(sock, mob);
if (mob->groupLeader != 0)
MobAI::followToCombat(mob);
}
mob->appearanceData.iHP -= damage;
// wake up sleeping monster
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_MEZ) {
mob->appearanceData.iConditionBitFlag &= ~CSB_BIT_MEZ;
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = mob->appearanceData.iNPC_ID;
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
if (mob->appearanceData.iHP <= 0)
killMob(mob->target, mob);
return damage;
}
void Combat::killMob(CNSocket *sock, Mob *mob) {
mob->state = MobState::DEAD;
mob->target = nullptr;
mob->appearanceData.iConditionBitFlag = 0;
mob->skillStyle = -1;
mob->unbuffTimes.clear();
mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
// check for the edge case where hitting the mob did not aggro it
if (sock != nullptr) {
Player* plr = PlayerManager::getPlayer(sock);
Items::DropRoll rolled;
Items::DropRoll eventRolled;
int rolledQItem = Rand::rand();
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
Items::giveMobDrop(sock, mob, rolled, eventRolled);
Missions::mobKilled(sock, mob->appearanceData.iNPCType, rolledQItem);
} else {
Player* otherPlayer = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlayer == nullptr)
return;
for (int i = 0; i < otherPlayer->groupCnt; i++) {
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlayer->groupIDs[i]);
if (sockTo == nullptr)
continue;
Player *otherPlr = PlayerManager::getPlayer(sockTo);
// only contribute to group members' kills if they're close enough
int dist = std::hypot(plr->x - otherPlr->x + 1, plr->y - otherPlr->y + 1);
if (dist > 5000)
continue;
Items::giveMobDrop(sockTo, mob, rolled, eventRolled);
Missions::mobKilled(sockTo, mob->appearanceData.iNPCType, rolledQItem);
}
}
}
// delay the despawn animation
mob->despawned = false;
// fire any triggered events
for (NPCEvent& event : NPCManager::NPCEvents)
if (event.trigger == ON_KILLED && event.npcType == mob->appearanceData.iNPCType)
event.handler(sock, mob);
auto it = Transport::NPCQueues.find(mob->appearanceData.iNPC_ID);
if (it == Transport::NPCQueues.end() || it->second.empty())
return;
// rewind or empty the movement queue
if (mob->staticPath) {
/*
* This is inelegant, but we wind forward in the path until we find the point that
* corresponds with the Mob's spawn point.
*
* IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever.
*/
auto& queue = it->second;
for (auto point = queue.front(); point.x != mob->spawnX || point.y != mob->spawnY; point = queue.front()) {
queue.pop();
queue.push(point);
}
} else {
Transport::NPCQueues.erase(mob->appearanceData.iNPC_ID);
} }
} }
@@ -529,27 +308,40 @@ static void combatEnd(CNSocket *sock, CNPacketData *data) {
plr->healCooldown = 4000; plr->healCooldown = 4000;
} }
static void dealGooDamage(CNSocket *sock) { static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf;
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
if(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)
return; // ignore completely
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION) != (bool)pkt->iFlag)
plr->iConditionBitFlag ^= CSB_BIT_INFECTION;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt1);
pkt1.eCSTB = ECSB_INFECTION; // eCharStatusTimeBuffID
pkt1.eTBU = 1; // eTimeBuffUpdate
pkt1.eTBT = 0; // eTimeBuffType 1 means nano
pkt1.iConditionBitFlag = plr->iConditionBitFlag;
sock->sendPacket((void*)&pkt1, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
static void dealGooDamage(CNSocket *sock, int amount) {
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_DotDamage); size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_DotDamage);
assert(resplen < CN_PACKET_BODY_SIZE); assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
uint8_t respbuf[CN_PACKET_BODY_SIZE]; uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE); Player *plr = PlayerManager::getPlayer(sock);
memset(respbuf, 0, resplen);
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf; 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)); sSkillResult_DotDamage *dmg = (sSkillResult_DotDamage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
int amount = PC_MAXHEALTH(plr->level) * 3 / 20; if (plr->iConditionBitFlag & CSB_BIT_PROTECT_INFECTION) {
Buff* protectionBuff = plr->getBuff(ECSB_PROTECT_INFECTION);
if (protectionBuff != nullptr) {
amount = -2; // -2 is the magic number for "Protected" to appear as the damage number amount = -2; // -2 is the magic number for "Protected" to appear as the damage number
dmg->bProtected = 1; dmg->bProtected = 1;
// eggs allow protection without nanos // eggs allow protection without nanos
if (protectionBuff->maxClass() <= BuffClass::NANO && plr->activeNano != -1) if (plr->activeNano != -1 && (plr->iSelfConditionBitFlag & CSB_BIT_PROTECT_INFECTION))
plr->Nanos[plr->activeNano].iStamina -= 3; plr->Nanos[plr->activeNano].iStamina -= 3;
} else { } else {
plr->HP -= amount; plr->HP -= amount;
@@ -573,54 +365,27 @@ static void dealGooDamage(CNSocket *sock) {
dmg->iID = plr->iID; dmg->iID = plr->iID;
dmg->iDamage = amount; dmg->iDamage = amount;
dmg->iHP = plr->HP; dmg->iHP = plr->HP;
dmg->iConditionBitFlag = plr->getCompositeCondition(); dmg->iConditionBitFlag = plr->iConditionBitFlag;
sock->sendPacket((void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); sock->sendPacket((void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
} }
static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
// infection debuff toggles as the client asks it to,
// so we add and remove a permanent debuff
if (pkt->iFlag && !plr->hasBuff(ECSB_INFECTION)) {
BuffStack infection = {
-1, // infinite
0, // no value
sock, // self-inflicted
BuffClass::ENVIRONMENT
};
plr->addBuff(ECSB_INFECTION,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
},
[](EntityRef self, Buff* buff, time_t currTime) {
if(self.kind == EntityKind::PLAYER)
dealGooDamage(self.sock);
},
&infection);
} else if(!pkt->iFlag && plr->hasBuff(ECSB_INFECTION)) {
plr->removeBuff(ECSB_INFECTION);
}
}
static void pcAttackChars(CNSocket *sock, CNPacketData *data) { static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_REQ_PC_ATTACK_CHARs* pkt = (sP_CL2FE_REQ_PC_ATTACK_CHARs*)data->buf; sP_CL2FE_REQ_PC_ATTACK_CHARs* pkt = (sP_CL2FE_REQ_PC_ATTACK_CHARs*)data->buf;
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
// only GMs can use this variant // only GMs can use this this variant
if (plr->accountLevel > 30) if (plr->accountLevel > 30)
return; return;
// Unlike the attack mob packet, attacking players packet has an 8-byte trail (Instead of 4 bytes). // Unlike the attack mob packet, attacking players packet has an 8-byte trail (Instead of 4 bytes).
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(sGM_PVPTarget), data->size)) { if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(int32_t) * 2, data->size)) {
std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_CHARs packet size\n"; std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_CHARs packet size\n";
return; return;
} }
sGM_PVPTarget* pktdata = (sGM_PVPTarget*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs)); int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs));
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC), pkt->iTargetCnt, sizeof(sAttackResult))) { if (!validOutVarPacket(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC), pkt->iTargetCnt, sizeof(sAttackResult))) {
std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_CHARs_SUCC packet size\n"; std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_CHARs_SUCC packet size\n";
@@ -629,9 +394,9 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
// initialize response struct // initialize response struct
size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) + pkt->iTargetCnt * sizeof(sAttackResult); size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) + pkt->iTargetCnt * sizeof(sAttackResult);
uint8_t respbuf[CN_PACKET_BODY_SIZE]; uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE); memset(respbuf, 0, resplen);
sP_FE2CL_PC_ATTACK_CHARs_SUCC *resp = (sP_FE2CL_PC_ATTACK_CHARs_SUCC*)respbuf; 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)); sAttackResult *respdata = (sAttackResult*)(respbuf+sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC));
@@ -639,19 +404,11 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
resp->iTargetCnt = pkt->iTargetCnt; resp->iTargetCnt = pkt->iTargetCnt;
for (int i = 0; i < pkt->iTargetCnt; i++) { for (int i = 0; i < pkt->iTargetCnt; i++) {
if (pktdata[i*2+1] == 1) { // eCT == 1; attack player
ICombatant* target = nullptr; Player *target = nullptr;
std::pair<int, int> damage;
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
if (pktdata[i].eCT == 1) { // eCT == 1; attack player
for (auto& pair : PlayerManager::players) { for (auto& pair : PlayerManager::players) {
if (pair.second->iID == pktdata[i].iID) { if (pair.second->iID == pktdata[i*2]) {
target = pair.second; target = pair.second;
break; break;
} }
@@ -663,38 +420,67 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
return; return;
} }
damage = getDamage(damage.first, ((Player*)target)->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0); std::pair<int,int> damage;
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
damage = getDamage(damage.first, target->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0);
if (plr->batteryW >= 6 + plr->level)
plr->batteryW -= 6 + plr->level;
else
plr->batteryW = 0;
target->HP -= damage.first;
respdata[i].eCT = pktdata[i*2+1];
respdata[i].iID = target->iID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = target->HP;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
} else { // eCT == 4; attack mob } else { // eCT == 4; attack mob
if (NPCManager::NPCs.find(pktdata[i*2]) == NPCManager::NPCs.end()) {
if (NPCManager::NPCs.find(pktdata[i].iID) == NPCManager::NPCs.end()) {
// not sure how to best handle this // not sure how to best handle this
std::cout << "[WARN] pcAttackChars: NPC ID not found" << std::endl; std::cout << "[WARN] pcAttackChars: NPC ID not found" << std::endl;
return; return;
} }
BaseNPC* npc = NPCManager::NPCs[pktdata[i].iID]; BaseNPC* npc = NPCManager::NPCs[pktdata[i * 2]];
if (npc->kind != EntityKind::MOB) { if (npc->type != EntityType::MOB) {
std::cout << "[WARN] pcAttackChars: NPC is not a mob" << std::endl; std::cout << "[WARN] pcAttackChars: NPC is not a mob" << std::endl;
return; return;
} }
Mob* mob = (Mob*)npc; Mob* mob = (Mob*)npc;
target = mob;
std::pair<int,int> damage;
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
int difficulty = (int)mob->data["m_iNpcLevel"]; int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty), damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
if (plr->batteryW >= 6 + difficulty)
plr->batteryW -= 6 + difficulty;
else
plr->batteryW = 0;
damage.first = hitMob(sock, mob, damage.first);
respdata[i].eCT = pktdata[i*2+1];
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
} }
plr->subtractCapped(CappedValueType::BATTERY_W, 6 + plr->level);
damage.first = target->takeDamage(sock, damage.first);
respdata[i].eCT = pktdata[i].eCT;
respdata[i].iID = target->getID();
respdata[i].iDamage = damage.first;
respdata[i].iHP = target->getCurrentHP();
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
} }
sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_CHARs_SUCC, resplen); sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_CHARs_SUCC, resplen);
@@ -735,7 +521,7 @@ static int8_t addBullet(Player* plr, bool isGrenade) {
toAdd.weaponBoost = plr->batteryW > 0; toAdd.weaponBoost = plr->batteryW > 0;
if (toAdd.weaponBoost) { if (toAdd.weaponBoost) {
int boostCost = Rand::rand(11) + 20; int boostCost = Rand::rand(11) + 20;
plr->subtractCapped(CappedValueType::BATTERY_W, boostCost); plr->batteryW = boostCost > plr->batteryW ? 0 : plr->batteryW - boostCost;
} }
Bullets[plr->iID][findId] = toAdd; Bullets[plr->iID][findId] = toAdd;
@@ -830,9 +616,17 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
return; return;
} }
// kick the player if firing too rapidly // rapid fire anti-cheat
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iTargetCnt, true)) time_t currTime = getTime();
return; if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing
plr->lastShot = currTime;
if (plr->suspicionRating > 10000) // kill the socket when the player is too suspicious
sock->kill();
/* /*
* initialize response struct * initialize response struct
@@ -840,9 +634,9 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
*/ */
size_t resplen = sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT) + pkt->iTargetCnt * sizeof(sAttackResult); size_t resplen = sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT) + pkt->iTargetCnt * sizeof(sAttackResult);
uint8_t respbuf[CN_PACKET_BODY_SIZE]; uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE); memset(respbuf, 0, resplen);
sP_FE2CL_PC_GRENADE_STYLE_HIT* resp = (sP_FE2CL_PC_GRENADE_STYLE_HIT*)respbuf; 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)); sAttackResult* respdata = (sAttackResult*)(respbuf + sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT));
@@ -862,7 +656,7 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
} }
BaseNPC* npc = NPCManager::NPCs[pktdata[i]]; BaseNPC* npc = NPCManager::NPCs[pktdata[i]];
if (npc->kind != EntityKind::MOB) { if (npc->type != EntityType::MOB) {
std::cout << "[WARN] projectileHit: NPC is not a mob" << std::endl; std::cout << "[WARN] projectileHit: NPC is not a mob" << std::endl;
return; return;
} }
@@ -875,11 +669,11 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
int difficulty = (int)mob->data["m_iNpcLevel"]; int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
damage.first = mob->takeDamage(sock, damage.first); damage.first = hitMob(sock, mob, damage.first);
respdata[i].iID = mob->id; respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iDamage = damage.first; respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->hp; respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHitFlag = damage.second; respdata[i].iHitFlag = damage.second;
} }
@@ -894,7 +688,6 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
static void playerTick(CNServer *serv, time_t currTime) { static void playerTick(CNServer *serv, time_t currTime) {
static time_t lastHealTime = 0; static time_t lastHealTime = 0;
static time_t lastCombatTIme = 0;
for (auto& pair : PlayerManager::players) { for (auto& pair : PlayerManager::players) {
CNSocket *sock = pair.first; CNSocket *sock = pair.first;
@@ -902,13 +695,18 @@ static void playerTick(CNServer *serv, time_t currTime) {
bool transmit = false; bool transmit = false;
// group ticks // group ticks
if (plr->group != nullptr) if (plr->groupCnt > 1)
Groups::groupTickInfo(sock); Groups::groupTickInfo(plr);
// do not tick dead players // do not tick dead players
if (plr->HP <= 0) if (plr->HP <= 0)
continue; continue;
// fm patch/lake damage
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION)
&& !(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
dealGooDamage(sock, PC_MAXHEALTH(plr->level) * 3 / 20);
// heal // heal
if (currTime - lastHealTime >= 4000 && !plr->inCombat && plr->HP < PC_MAXHEALTH(plr->level)) { if (currTime - lastHealTime >= 4000 && !plr->inCombat && plr->HP < PC_MAXHEALTH(plr->level)) {
if (currTime - lastHealTime - plr->healCooldown >= 4000) { if (currTime - lastHealTime - plr->healCooldown >= 4000) {
@@ -920,24 +718,22 @@ static void playerTick(CNServer *serv, time_t currTime) {
plr->healCooldown -= 4000; plr->healCooldown -= 4000;
} }
// combat tick for (int i = 0; i < 3; i++) {
if(currTime - lastCombatTIme >= 2000) { if (plr->activeNano != 0 && plr->equippedNanos[i] == plr->activeNano) { // spend stamina
plr->step(currTime); plr->Nanos[plr->activeNano].iStamina -= 1 + plr->nanoDrainRate / 5;
transmit = true;
}
// nanos if (plr->Nanos[plr->activeNano].iStamina <= 0)
if (plr->activeNano != 0) { // tick active nano Nanos::summonNano(sock, -1, true); // unsummon nano silently
sNano* nano = plr->getActiveNano();
if (Abilities::SkillTable.find(nano->iSkillID) != Abilities::SkillTable.end()) { transmit = true;
// nano has skill data } else if (plr->Nanos[plr->equippedNanos[i]].iStamina < 150) { // regain stamina
SkillData* skill = &Abilities::SkillTable[nano->iSkillID]; sNano& nano = plr->Nanos[plr->equippedNanos[i]];
if (skill->drainType == SkillDrainType::PASSIVE) { nano.iStamina += 1;
ICombatant* src = dynamic_cast<ICombatant*>(plr);
int32_t targets[] = { plr->iID }; if (nano.iStamina > 150)
std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets); nano.iStamina = 150;
Abilities::useNanoSkill(sock, skill, *nano, affectedCombatants);
} transmit = true;
} }
} }
@@ -953,20 +749,6 @@ static void playerTick(CNServer *serv, time_t currTime) {
PlayerManager::sendToViewable(sock, (void*)&dead, P_FE2CL_PC_SUDDEN_DEAD, sizeof(sP_FE2CL_PC_SUDDEN_DEAD)); PlayerManager::sendToViewable(sock, (void*)&dead, P_FE2CL_PC_SUDDEN_DEAD, sizeof(sP_FE2CL_PC_SUDDEN_DEAD));
} }
// process buffsets
auto it = plr->buffs.begin();
while(it != plr->buffs.end()) {
Buff* buff = (*it).second;
//buff->combatTick() gets called in Player::step
buff->tick(currTime);
if(buff->isStale()) {
// garbage collect
it = plr->buffs.erase(it);
delete buff;
}
else it++;
}
if (transmit) { if (transmit) {
INITSTRUCT(sP_FE2CL_REP_PC_TICK, pkt); INITSTRUCT(sP_FE2CL_REP_PC_TICK, pkt);
@@ -981,15 +763,13 @@ static void playerTick(CNServer *serv, time_t currTime) {
} }
} }
// if this was a heal/combat tick, update the counters outside of the loop // if this was a heal tick, update the counter outside of the loop
if (currTime - lastHealTime >= 4000) if (currTime - lastHealTime >= 4000)
lastHealTime = currTime; lastHealTime = currTime;
if(currTime - lastCombatTIme >= 2000)
lastCombatTIme = currTime;
} }
void Combat::init() { void Combat::init() {
REGISTER_SHARD_TIMER(playerTick, MS_PER_PLAYER_TICK); REGISTER_SHARD_TIMER(playerTick, 2000);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, pcAttackNpcs); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, pcAttackNpcs);

View File

@@ -1,10 +1,15 @@
#pragma once #pragma once
#include "Player.hpp" #include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "NPC.hpp"
#include "MobAI.hpp" #include "MobAI.hpp"
#include "JSON.hpp"
#include <map> #include <map>
#include <vector> #include <unordered_map>
#include <queue>
struct Bullet { struct Bullet {
int pointDamage; int pointDamage;
@@ -19,5 +24,6 @@ namespace Combat {
void init(); void init();
void npcAttackPc(Mob *mob, time_t currTime); void npcAttackPc(Mob *mob, time_t currTime);
void genQItemRolls(std::vector<Player*> players, std::map<int, int>& rolls); int hitMob(CNSocket *sock, Mob *mob, int damage);
void killMob(CNSocket *sock, Mob *mob);
} }

View File

@@ -1,20 +1,20 @@
#include "CustomCommands.hpp" #include "CustomCommands.hpp"
#include "db/Database.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Chat.hpp" #include "Chat.hpp"
#include "PlayerManager.hpp"
#include "TableData.hpp" #include "TableData.hpp"
#include "NPCManager.hpp" #include "NPCManager.hpp"
#include "MobAI.hpp"
#include "Missions.hpp"
#include "Eggs.hpp" #include "Eggs.hpp"
#include "MobAI.hpp"
#include "Items.hpp" #include "Items.hpp"
#include "Abilities.hpp" #include "db/Database.hpp"
#include "Transport.hpp"
#include "Missions.hpp"
#include "lua/LuaManager.hpp"
#include <sstream> #include <sstream>
#include <limits.h> #include <iterator>
#include <math.h>
typedef void (*CommandHandler)(std::string fullString, std::vector<std::string>& args, CNSocket* sock); typedef void (*CommandHandler)(std::string fullString, std::vector<std::string>& args, CNSocket* sock);
@@ -83,77 +83,7 @@ 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) { static void accessCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
if (args.size() < 2) { Chat::sendServerMessage(sock, "Your access level is " + std::to_string(PlayerManager::getPlayer(sock)->accountLevel));
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) { static void populationCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -314,20 +244,20 @@ static void summonWCommand(std::string full, std::vector<std::string>& args, CNS
BaseNPC *npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true); BaseNPC *npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true);
// update angle // update angle
npc->angle = (plr->angle + 180) % 360; npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, plr->instanceID, npc->angle); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, npc->appearanceData.iAngle);
// if we're in a lair, we need to spawn the NPC in both the private instance and the template // if we're in a lair, we need to spawn the NPC in both the private instance and the template
if (PLAYERID(plr->instanceID) != 0) { if (PLAYERID(plr->instanceID) != 0) {
npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true, true); npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true, true);
npc->angle = (plr->angle + 180) % 360; npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, npc->instanceID, npc->angle); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, npc->instanceID, npc->appearanceData.iAngle);
} }
Chat::sendServerMessage(sock, "/summonW: placed mob with type: " + std::to_string(type) + Chat::sendServerMessage(sock, "/summonW: placed mob with type: " + std::to_string(type) +
", id: " + std::to_string(npc->id)); ", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningMobs[npc->id] = npc; // only record the one in the template TableData::RunningMobs[npc->appearanceData.iNPC_ID] = npc; // only record the one in the template
} }
static void unsummonWCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void unsummonWCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -340,24 +270,24 @@ static void unsummonWCommand(std::string full, std::vector<std::string>& args, C
return; return;
} }
if (TableData::RunningEggs.find(npc->id) != TableData::RunningEggs.end()) { if (TableData::RunningEggs.find(npc->appearanceData.iNPC_ID) != TableData::RunningEggs.end()) {
Chat::sendServerMessage(sock, "/unsummonW: removed egg with type: " + std::to_string(npc->type) + Chat::sendServerMessage(sock, "/unsummonW: removed egg with type: " + std::to_string(npc->appearanceData.iNPCType) +
", id: " + std::to_string(npc->id)); ", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningEggs.erase(npc->id); TableData::RunningEggs.erase(npc->appearanceData.iNPC_ID);
NPCManager::destroyNPC(npc->id); NPCManager::destroyNPC(npc->appearanceData.iNPC_ID);
return; return;
} }
if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end() if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) == TableData::RunningMobs.end()
&& TableData::RunningGroups.find(npc->id) == TableData::RunningGroups.end()) { && TableData::RunningGroups.find(npc->appearanceData.iNPC_ID) == TableData::RunningGroups.end()) {
Chat::sendServerMessage(sock, "/unsummonW: Closest NPC is not a gruntwork mob."); Chat::sendServerMessage(sock, "/unsummonW: Closest NPC is not a gruntwork mob.");
return; return;
} }
if (NPCManager::NPCs.find(npc->id) != NPCManager::NPCs.end() && NPCManager::NPCs[npc->id]->kind == EntityKind::MOB) { if (NPCManager::NPCs.find(npc->appearanceData.iNPC_ID) != NPCManager::NPCs.end() && NPCManager::NPCs[npc->appearanceData.iNPC_ID]->type == EntityType::MOB) {
int leadId = ((Mob*)npc)->groupLeader; int leadId = ((Mob*)npc)->groupLeader;
if (leadId != 0) { if (leadId != 0) {
if (NPCManager::NPCs.find(leadId) == NPCManager::NPCs.end() || NPCManager::NPCs[leadId]->kind != EntityKind::MOB) { if (NPCManager::NPCs.find(leadId) == NPCManager::NPCs.end() || NPCManager::NPCs[leadId]->type != EntityType::MOB) {
std::cout << "[WARN] unsummonW: leader not found!" << std::endl; std::cout << "[WARN] unsummonW: leader not found!" << std::endl;
} }
Mob* leadNpc = (Mob*)NPCManager::NPCs[leadId]; Mob* leadNpc = (Mob*)NPCManager::NPCs[leadId];
@@ -365,7 +295,7 @@ static void unsummonWCommand(std::string full, std::vector<std::string>& args, C
if (leadNpc->groupMember[i] == 0) if (leadNpc->groupMember[i] == 0)
break; break;
if (NPCManager::NPCs.find(leadNpc->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadNpc->groupMember[i]]->kind != EntityKind::MOB) { if (NPCManager::NPCs.find(leadNpc->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadNpc->groupMember[i]]->type != EntityType::MOB) {
std::cout << "[WARN] unsommonW: leader can't find a group member!" << std::endl; std::cout << "[WARN] unsommonW: leader can't find a group member!" << std::endl;
continue; continue;
} }
@@ -379,12 +309,12 @@ static void unsummonWCommand(std::string full, std::vector<std::string>& args, C
} }
} }
Chat::sendServerMessage(sock, "/unsummonW: removed mob with type: " + std::to_string(npc->type) + Chat::sendServerMessage(sock, "/unsummonW: removed mob with type: " + std::to_string(npc->appearanceData.iNPCType) +
", id: " + std::to_string(npc->id)); ", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningMobs.erase(npc->id); TableData::RunningMobs.erase(npc->appearanceData.iNPC_ID);
NPCManager::destroyNPC(npc->id); NPCManager::destroyNPC(npc->appearanceData.iNPC_ID);
} }
static void toggleAiCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void toggleAiCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -395,11 +325,11 @@ static void toggleAiCommand(std::string full, std::vector<std::string>& args, CN
// return all mobs to their spawn points // return all mobs to their spawn points
for (auto& pair : NPCManager::NPCs) { for (auto& pair : NPCManager::NPCs) {
if (pair.second->kind != EntityKind::MOB) if (pair.second->type != EntityType::MOB)
continue; continue;
Mob* mob = (Mob*)pair.second; Mob* mob = (Mob*)pair.second;
mob->state = AIState::RETREAT; mob->state = MobState::RETREAT;
mob->target = nullptr; mob->target = nullptr;
mob->nextMovement = getTime(); mob->nextMovement = getTime();
@@ -427,27 +357,34 @@ static void npcRotateCommand(std::string full, std::vector<std::string>& args, C
} }
int angle = (plr->angle + 180) % 360; int angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, npc->instanceID, angle);
bool isGruntworkNpc = true; // if it's a gruntwork NPC, rotate in-place
if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) != TableData::RunningMobs.end()) {
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, npc->instanceID, angle);
// add a rotation entry to the gruntwork file, unless it's already a gruntwork NPC Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for gruntwork NPC "
if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end()) { + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningNPCRotations[npc->id] = angle; } else {
isGruntworkNpc = false; TableData::RunningNPCRotations[npc->appearanceData.iNPC_ID] = angle;
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for NPC "
+ std::to_string(npc->appearanceData.iNPC_ID));
} }
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + // update rotation clientside
" for " + (isGruntworkNpc ? "gruntwork " : "") + "NPC " + std::to_string(npc->id)); INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
pkt.NPCAppearanceData = npc->appearanceData;
// update rotation clientside by refreshing the player's chunks (same as the /refresh command) sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
PlayerManager::updatePlayerPositionForWarp(sock, plr->x, plr->y, plr->z, plr->instanceID);
} }
static void refreshCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void refreshCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player *plr = PlayerManager::getPlayer(sock); EntityRef ref = {sock};
Entity* plr = ref.getEntity();
PlayerManager::updatePlayerPositionForWarp(sock, plr->x, plr->y, plr->z, plr->instanceID); ChunkPos currentChunk = plr->chunkPos;
ChunkPos nullChunk = std::make_tuple(0, 0, 0);
Chunking::updateEntityChunk(ref, currentChunk, nullChunk);
Chunking::updateEntityChunk(ref, nullChunk, currentChunk);
} }
static void instanceCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void instanceCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -510,9 +447,9 @@ static void npcInstanceCommand(std::string full, std::vector<std::string>& args,
return; return;
} }
Chat::sendServerMessage(sock, "[NPCI] Moving NPC with ID " + std::to_string(npc->id) + " to instance " + std::to_string(instance)); Chat::sendServerMessage(sock, "[NPCI] Moving NPC with ID " + std::to_string(npc->appearanceData.iNPC_ID) + " to instance " + std::to_string(instance));
TableData::RunningNPCMapNumbers[npc->id] = instance; TableData::RunningNPCMapNumbers[npc->appearanceData.iNPC_ID] = instance;
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, instance, npc->angle); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, instance, npc->appearanceData.iAngle);
} }
static void minfoCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void minfoCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -568,12 +505,9 @@ static void buffCommand(std::string full, std::vector<std::string>& args, CNSock
if (*tmp) if (*tmp)
return; return;
if (Abilities::SkillTable.count(skillId) == 0) { if (Eggs::eggBuffPlayer(sock, skillId, 0, duration)<0)
Chat::sendServerMessage(sock, "/buff: unknown skill Id"); Chat::sendServerMessage(sock, "/buff: unknown skill Id");
return;
}
Eggs::eggBuffPlayer(sock, skillId, 0, duration);
} }
static void eggCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void eggCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -602,7 +536,7 @@ static void eggCommand(std::string full, std::vector<std::string>& args, CNSocke
int addX = 0; //-500.0f * sin(plr->angle / 180.0f * M_PI); int addX = 0; //-500.0f * sin(plr->angle / 180.0f * M_PI);
int addY = 0; //-500.0f * cos(plr->angle / 180.0f * M_PI); int addY = 0; //-500.0f * cos(plr->angle / 180.0f * M_PI);
Egg* egg = new Egg(plr->instanceID, eggType, id, false); // change last arg to true after gruntwork Egg* egg = new Egg(plr->x + addX, plr->y + addY, plr->z, plr->instanceID, eggType, id, false); // change last arg to true after gruntwork
NPCManager::NPCs[id] = egg; NPCManager::NPCs[id] = egg;
NPCManager::updateNPCPosition(id, plr->x + addX, plr->y + addY, plr->z, plr->instanceID, plr->angle); NPCManager::updateNPCPosition(id, plr->x + addX, plr->y + addY, plr->z, plr->instanceID, plr->angle);
@@ -679,40 +613,40 @@ static void summonGroupCommand(std::string full, std::vector<std::string>& args,
} }
BaseNPC *npc = NPCManager::summonNPC(x, y, z, plr->instanceID, type, wCommand); BaseNPC *npc = NPCManager::summonNPC(x, y, z, plr->instanceID, type, wCommand);
if (team == 2 && i > 0 && npc->kind == EntityKind::MOB) { if (team == 2 && i > 0 && npc->type == EntityType::MOB) {
leadNpc->groupMember[i-1] = npc->id; leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID;
Mob* mob = (Mob*)NPCManager::NPCs[npc->id]; Mob* mob = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID];
mob->groupLeader = leadNpc->id; mob->groupLeader = leadNpc->appearanceData.iNPC_ID;
mob->offsetX = x - plr->x; mob->offsetX = x - plr->x;
mob->offsetY = y - plr->y; mob->offsetY = y - plr->y;
} }
npc->angle = (plr->angle + 180) % 360; npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, x, y, z, plr->instanceID, npc->angle); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, x, y, z, plr->instanceID, npc->appearanceData.iAngle);
// if we're in a lair, we need to spawn the NPC in both the private instance and the template // if we're in a lair, we need to spawn the NPC in both the private instance and the template
if (PLAYERID(plr->instanceID) != 0) { if (PLAYERID(plr->instanceID) != 0) {
npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, wCommand, true); npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, wCommand, true);
if (team == 2 && i > 0 && npc->kind == EntityKind::MOB) { if (team == 2 && i > 0 && npc->type == EntityType::MOB) {
leadNpc->groupMember[i-1] = npc->id; leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID;
Mob* mob = (Mob*)NPCManager::NPCs[npc->id]; Mob* mob = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID];
mob->groupLeader = leadNpc->id; mob->groupLeader = leadNpc->appearanceData.iNPC_ID;
mob->offsetX = x - plr->x; mob->offsetX = x - plr->x;
mob->offsetY = y - plr->y; mob->offsetY = y - plr->y;
} }
npc->angle = (plr->angle + 180) % 360; npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, x, y, z, plr->instanceID, npc->angle); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, x, y, z, plr->instanceID, npc->appearanceData.iAngle);
} }
Chat::sendServerMessage(sock, "/summonGroup(W): placed mob with type: " + std::to_string(type) + Chat::sendServerMessage(sock, "/summonGroup(W): placed mob with type: " + std::to_string(type) +
", id: " + std::to_string(npc->id)); ", id: " + std::to_string(npc->appearanceData.iNPC_ID));
if (i == 0 && team == 2 && npc->kind == EntityKind::MOB) { if (i == 0 && team == 2 && npc->type == EntityType::MOB) {
type = type2; type = type2;
leadNpc = (Mob*)NPCManager::NPCs[npc->id]; leadNpc = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID];
leadNpc->groupLeader = leadNpc->id; leadNpc->groupLeader = leadNpc->appearanceData.iNPC_ID;
} }
} }
@@ -724,7 +658,7 @@ static void summonGroupCommand(std::string full, std::vector<std::string>& args,
return; return;
} }
TableData::RunningGroups[leadNpc->id] = leadNpc; // only record the leader TableData::RunningGroups[leadNpc->appearanceData.iNPC_ID] = leadNpc; // only record the leader
} }
static void flushCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void flushCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -741,14 +675,15 @@ static void whoisCommand(std::string full, std::vector<std::string>& args, CNSoc
return; return;
} }
Chat::sendServerMessage(sock, "[WHOIS] ID: " + std::to_string(npc->id)); Chat::sendServerMessage(sock, "[WHOIS] ID: " + std::to_string(npc->appearanceData.iNPC_ID));
Chat::sendServerMessage(sock, "[WHOIS] Type: " + std::to_string(npc->type)); Chat::sendServerMessage(sock, "[WHOIS] Type: " + std::to_string(npc->appearanceData.iNPCType));
Chat::sendServerMessage(sock, "[WHOIS] HP: " + std::to_string(npc->hp)); Chat::sendServerMessage(sock, "[WHOIS] HP: " + std::to_string(npc->appearanceData.iHP));
Chat::sendServerMessage(sock, "[WHOIS] EntityType: " + std::to_string((int)npc->kind)); Chat::sendServerMessage(sock, "[WHOIS] CBF: " + std::to_string(npc->appearanceData.iConditionBitFlag));
Chat::sendServerMessage(sock, "[WHOIS] EntityType: " + std::to_string((int)npc->type));
Chat::sendServerMessage(sock, "[WHOIS] X: " + std::to_string(npc->x)); Chat::sendServerMessage(sock, "[WHOIS] X: " + std::to_string(npc->x));
Chat::sendServerMessage(sock, "[WHOIS] Y: " + std::to_string(npc->y)); Chat::sendServerMessage(sock, "[WHOIS] Y: " + std::to_string(npc->y));
Chat::sendServerMessage(sock, "[WHOIS] Z: " + std::to_string(npc->z)); Chat::sendServerMessage(sock, "[WHOIS] Z: " + std::to_string(npc->z));
Chat::sendServerMessage(sock, "[WHOIS] Angle: " + std::to_string(npc->angle)); Chat::sendServerMessage(sock, "[WHOIS] Angle: " + std::to_string(npc->appearanceData.iAngle));
std::string chunkPosition = std::to_string(std::get<0>(npc->chunkPos)) + ", " + std::to_string(std::get<1>(npc->chunkPos)) + ", " + std::to_string(std::get<2>(npc->chunkPos)); std::string chunkPosition = std::to_string(std::get<0>(npc->chunkPos)) + ", " + std::to_string(std::get<1>(npc->chunkPos)) + ", " + std::to_string(std::get<2>(npc->chunkPos));
Chat::sendServerMessage(sock, "[WHOIS] Chunk: {" + chunkPosition + "}"); Chat::sendServerMessage(sock, "[WHOIS] Chunk: {" + chunkPosition + "}");
Chat::sendServerMessage(sock, "[WHOIS] MapNum: " + std::to_string(MAPNUM(npc->instanceID))); Chat::sendServerMessage(sock, "[WHOIS] MapNum: " + std::to_string(MAPNUM(npc->instanceID)));
@@ -757,35 +692,39 @@ static void whoisCommand(std::string full, std::vector<std::string>& args, CNSoc
static void lairUnlockCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) { static void lairUnlockCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
if (!Chunking::chunkExists(plr->chunkPos))
return;
Chunk* chnk = Chunking::chunks[plr->chunkPos];
int taskID = -1; int taskID = -1;
int missionID = -1; int missionID = -1;
int lastDist = INT_MAX; int found = 0;
for (Chunk *chnk : Chunking::getViewableChunks(plr->chunkPos)) { for (const EntityRef& ref : chnk->entities) {
for (const EntityRef& ref : chnk->entities) { if (ref.type == EntityType::PLAYER)
if (ref.kind == EntityKind::PLAYER) continue;
continue;
BaseNPC* npc = (BaseNPC*)ref.getEntity(); int32_t id = ref.id;
if (NPCManager::NPCs.find(id) == NPCManager::NPCs.end())
continue;
int distXY = std::hypot(plr->x - npc->x, plr->y - npc->y); BaseNPC* npc = NPCManager::NPCs[id];
int dist = std::hypot(distXY, plr->z - npc->z); for (auto it = NPCManager::Warps.begin(); it != NPCManager::Warps.end(); it++) {
if (dist >= lastDist) if ((*it).second.npcID == npc->appearanceData.iNPCType) {
continue; taskID = (*it).second.limitTaskID;
missionID = Missions::Tasks[taskID]->task["m_iHMissionID"];
for (auto it = NPCManager::Warps.begin(); it != NPCManager::Warps.end(); it++) { found++;
if (it->second.npcID == npc->type) { break;
taskID = it->second.limitTaskID;
missionID = Missions::Tasks[taskID]->task["m_iHMissionID"];
lastDist = dist;
break;
}
} }
} }
} }
if (missionID == -1 || taskID == -1) { if (missionID == -1 || taskID == -1) {
Chat::sendServerMessage(sock, "No nearby Lair portals found."); Chat::sendServerMessage(sock, "You are NOT standing near a lair portal; move around and try again!");
return;
}
if (found > 1) {
Chat::sendServerMessage(sock, "More than one lair found; decrease chunk size and try again!");
return; return;
} }
@@ -1046,17 +985,11 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// add first point at NPC's current location // add first point at NPC's current location
std::vector<BaseNPC*> pathPoints; std::vector<BaseNPC*> pathPoints;
BaseNPC* marker = new BaseNPC(0, plr->instanceID, 1386, NPCManager::nextId--); BaseNPC* marker = new BaseNPC(npc->x, npc->y, npc->z, 0, plr->instanceID, 1386, NPCManager::nextId--);
// assign coords manually, since we aren't actually adding markers to the world
marker->x = npc->x;
marker->y = npc->y;
marker->z = npc->z;
pathPoints.push_back(marker); pathPoints.push_back(marker);
// map from player // map from player
TableData::RunningNPCPaths[plr->iID] = std::make_pair(npc, pathPoints); TableData::RunningNPCPaths[plr->iID] = std::make_pair(npc, pathPoints);
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is now following you"); Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is now following you");
updatePathMarkers(sock); updatePathMarkers(sock);
return; return;
} }
@@ -1073,12 +1006,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// /path kf // /path kf
if (args[1] == "kf") { if (args[1] == "kf") {
BaseNPC* marker = new BaseNPC(0, plr->instanceID, 1386, NPCManager::nextId--); BaseNPC* marker = new BaseNPC(npc->x, npc->y, npc->z, 0, plr->instanceID, 1386, NPCManager::nextId--);
marker->x = npc->x;
marker->y = npc->y;
marker->z = npc->z;
entry->second.push_back(marker); entry->second.push_back(marker);
Chat::sendServerMessage(sock, "[PATH] Added keyframe"); Chat::sendServerMessage(sock, "[PATH] Added keyframe");
updatePathMarkers(sock); updatePathMarkers(sock);
@@ -1088,8 +1016,8 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// /path here // /path here
if (args[1] == "here") { if (args[1] == "here") {
// bring the NPC to where the player is standing // bring the NPC to where the player is standing
Transport::NPCQueues.erase(npc->id); // delete transport queue Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, npc->instanceID, 0); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock); npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock); npc->enterIntoViewOf(sock);
Chat::sendServerMessage(sock, "[PATH] Come here"); Chat::sendServerMessage(sock, "[PATH] Come here");
@@ -1127,9 +1055,9 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
speed = speedArg; speed = speedArg;
} }
// return NPC to home // return NPC to home
Transport::NPCQueues.erase(npc->id); // delete transport queue Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
BaseNPC* home = entry->second[0]; BaseNPC* home = entry->second[0];
NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock); npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock); npc->enterIntoViewOf(sock);
@@ -1145,7 +1073,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
Transport::lerp(&keyframes, from, to, speed); // lerp from A to B Transport::lerp(&keyframes, from, to, speed); // lerp from A to B
from = to; // update point A from = to; // update point A
} }
Transport::NPCQueues[npc->id] = keyframes; Transport::NPCQueues[npc->appearanceData.iNPC_ID] = keyframes;
entry->second.pop_back(); // remove temp end point entry->second.pop_back(); // remove temp end point
Chat::sendServerMessage(sock, "[PATH] Testing NPC path"); Chat::sendServerMessage(sock, "[PATH] Testing NPC path");
@@ -1155,9 +1083,9 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// /path cancel // /path cancel
if (args[1] == "cancel") { if (args[1] == "cancel") {
// return NPC to home // return NPC to home
Transport::NPCQueues.erase(npc->id); // delete transport queue Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
BaseNPC* home = entry->second[0]; BaseNPC* home = entry->second[0];
NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock); npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock); npc->enterIntoViewOf(sock);
// deallocate markers // deallocate markers
@@ -1167,7 +1095,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
} }
// unmap // unmap
TableData::RunningNPCPaths.erase(plr->iID); TableData::RunningNPCPaths.erase(plr->iID);
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is no longer following you"); Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is no longer following you");
return; return;
} }
@@ -1195,9 +1123,9 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
} }
// return NPC to home and set path to repeat // return NPC to home and set path to repeat
Transport::NPCQueues.erase(npc->id); // delete transport queue Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
BaseNPC* home = entry->second[0]; BaseNPC* home = entry->second[0];
NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock); npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock); npc->enterIntoViewOf(sock);
npc->loopingPath = true; npc->loopingPath = true;
@@ -1214,7 +1142,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
Transport::lerp(&keyframes, from, to, speed); // lerp from A to B Transport::lerp(&keyframes, from, to, speed); // lerp from A to B
from = to; // update point A from = to; // update point A
} }
Transport::NPCQueues[npc->id] = keyframes; Transport::NPCQueues[npc->appearanceData.iNPC_ID] = keyframes;
entry->second.pop_back(); // remove temp end point entry->second.pop_back(); // remove temp end point
// save to gruntwork // save to gruntwork
@@ -1241,7 +1169,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
finishedPath.isLoop = true; finishedPath.isLoop = true;
finishedPath.speed = speed; finishedPath.speed = speed;
finishedPath.points = finalPoints; finishedPath.points = finalPoints;
finishedPath.targetIDs.push_back(npc->id); finishedPath.targetIDs.push_back(npc->appearanceData.iNPC_ID);
TableData::FinishedNPCPaths.push_back(finishedPath); TableData::FinishedNPCPaths.push_back(finishedPath);
@@ -1253,7 +1181,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// unmap // unmap
TableData::RunningNPCPaths.erase(plr->iID); TableData::RunningNPCPaths.erase(plr->iID);
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is no longer following you"); Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is no longer following you");
TableData::flush(); TableData::flush();
Chat::sendServerMessage(sock, "[PATH] Path saved to gruntwork"); Chat::sendServerMessage(sock, "[PATH] Path saved to gruntwork");
@@ -1264,13 +1192,19 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
Chat::sendServerMessage(sock, "[PATH] Unknown argument '" + args[1] + "'"); Chat::sendServerMessage(sock, "[PATH] Unknown argument '" + args[1] + "'");
} }
static void reloadScriptsCommand(std::string full, std::vector<std::string>& args, CNSocket *sock) {
// reloads all scripts
LuaManager::stopScripts();
LuaManager::loadScripts();
}
static void registerCommand(std::string cmd, int requiredLevel, CommandHandler handlr, std::string help) { static void registerCommand(std::string cmd, int requiredLevel, CommandHandler handlr, std::string help) {
commands[cmd] = ChatCommand(requiredLevel, handlr, help); commands[cmd] = ChatCommand(requiredLevel, handlr, help);
} }
void CustomCommands::init() { void CustomCommands::init() {
registerCommand("help", 100, helpCommand, "list all unlocked server-side commands"); registerCommand("help", 100, helpCommand, "list all unlocked server-side commands");
registerCommand("access", 100, accessCommand, "check or change access levels"); registerCommand("access", 100, accessCommand, "print your access level");
registerCommand("instance", 30, instanceCommand, "print or change your current instance"); registerCommand("instance", 30, instanceCommand, "print or change your current instance");
registerCommand("mss", 30, mssCommand, "edit Monkey Skyway routes"); registerCommand("mss", 30, mssCommand, "edit Monkey Skyway routes");
registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs"); registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs");
@@ -1303,4 +1237,5 @@ void CustomCommands::init() {
registerCommand("unregisterall", 50, unregisterallCommand, "clear all SCAMPER and MSS destinations"); registerCommand("unregisterall", 50, unregisterallCommand, "clear all SCAMPER and MSS destinations");
registerCommand("redeem", 100, redeemCommand, "redeem a code item"); registerCommand("redeem", 100, redeemCommand, "redeem a code item");
registerCommand("path", 30, pathCommand, "edit NPC paths"); registerCommand("path", 30, pathCommand, "edit NPC paths");
registerCommand("rscripts", 30, reloadScriptsCommand, "stops all script states and reloads all scripts");
} }

View File

@@ -2,8 +2,6 @@
#include "core/Core.hpp" #include "core/Core.hpp"
#include <string>
namespace CustomCommands { namespace CustomCommands {
void init(); void init();

View File

@@ -1,124 +1,141 @@
#include "core/Core.hpp"
#include "Eggs.hpp" #include "Eggs.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Abilities.hpp"
#include "NPCManager.hpp"
#include "Entities.hpp"
#include "Items.hpp" #include "Items.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include "Groups.hpp"
#include <assert.h> #include <assert.h>
using namespace Eggs; using namespace Eggs;
/// sock, CBFlag -> until
std::map<std::pair<CNSocket*, int32_t>, time_t> Eggs::EggBuffs;
std::unordered_map<int, EggType> Eggs::EggTypes; std::unordered_map<int, EggType> Eggs::EggTypes;
void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) { int Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
// eggId might be 0 if the buff is made by the /buff command int bitFlag = Groups::getGroupFlags(otherPlr);
EntityRef src = eggId == 0 ? sock : EntityRef(eggId); int CBFlag = Nanos::applyBuff(sock, skillId, 1, 3, bitFlag);
if(Abilities::SkillTable.count(skillId) == 0) {
std::cout << "[WARN] egg " << eggId << " has skill ID " << skillId << " which doesn't exist" << std::endl;
return;
}
SkillResult result = SkillResult(); size_t resplen;
SkillData* skill = &Abilities::SkillTable[skillId];
if(skill->drainType == SkillDrainType::PASSIVE) {
// apply buff
if(skill->targetType != SkillTargetType::PLAYERS) {
std::cout << "[WARN] weird skill type for egg " << eggId << " with skill " << skillId << ", should be " << (int)skill->targetType << std::endl;
}
int timeBuffId = Abilities::getCSTBFromST(skill->skillType); if (skillId == 183) {
int value = skill->values[0][0]; resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Damage);
BuffStack eggBuff = { } else if (skillId == 150) {
duration * 1000 / MS_PER_PLAYER_TICK, resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Heal_HP);
value,
src,
BuffClass::EGG
};
plr->addBuff(timeBuffId,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
if(status == ETBU_DEL) Buffs::timeBuffTimeout(self);
},
[](EntityRef self, Buff* buff, time_t currTime) {
// no-op
},
&eggBuff);
sSkillResult_Buff resultBuff{};
resultBuff.eCT = plr->getCharType();
resultBuff.iID = plr->getID();
resultBuff.bProtected = false;
resultBuff.iConditionBitFlag = plr->getCompositeCondition();
result = SkillResult(sizeof(sSkillResult_Buff), &resultBuff);
} else { } else {
int value = plr->getMaxHP() * skill->values[0][0] / 1000; resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Buff);
sSkillResult_Damage resultDamage{}; }
sSkillResult_Heal_HP resultHeal{}; assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
switch(skill->skillType) // we know it's only one trailing struct, so we can skip full validation
{
case SkillType::DAMAGE: uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
resultDamage.bProtected = false; auto skillUse = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
resultDamage.eCT = plr->getCharType();
resultDamage.iID = plr->getID(); if (skillId == 183) { // damage egg
resultDamage.iDamage = plr->takeDamage(src, value); auto skill = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
resultDamage.iHP = plr->getCurrentHP(); memset(respbuf, 0, resplen);
result = SkillResult(sizeof(sSkillResult_Damage), &resultDamage); skill->eCT = 1;
break; skill->iID = plr->iID;
case SkillType::HEAL_HP: skill->iDamage = PC_MAXHEALTH(plr->level) * Nanos::SkillTable[skillId].powerIntensity[0] / 1000;
resultHeal.eCT = plr->getCharType(); plr->HP -= skill->iDamage;
resultHeal.iID = plr->getID(); if (plr->HP < 0)
resultHeal.iHealHP = plr->heal(src, value); plr->HP = 0;
resultHeal.iHP = plr->getCurrentHP(); skill->iHP = plr->HP;
result = SkillResult(sizeof(sSkillResult_Heal_HP), &resultHeal); } else if (skillId == 150) { // heal egg
break; auto skill = (sSkillResult_Heal_HP*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
default: memset(respbuf, 0, resplen);
std::cout << "[WARN] oops, egg with active skill type " << (int)skill->skillType << " unhandled"; skill->eCT = 1;
return; skill->iID = plr->iID;
} skill->iHealHP = PC_MAXHEALTH(plr->level) * Nanos::SkillTable[skillId].powerIntensity[0] / 1000;
plr->HP += skill->iHealHP;
if (plr->HP > PC_MAXHEALTH(plr->level))
plr->HP = PC_MAXHEALTH(plr->level);
skill->iHP = plr->HP;
} else { // regular buff egg
auto skill = (sSkillResult_Buff*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iConditionBitFlag = plr->iConditionBitFlag;
} }
// initialize response struct skillUse->iNPC_ID = eggId;
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + result.size; skillUse->iSkillID = skillId;
uint8_t respbuf[CN_PACKET_BODY_SIZE]; skillUse->eST = Nanos::SkillTable[skillId].skillType;
memset(respbuf, 0, CN_PACKET_BODY_SIZE); skillUse->iTargetCnt = 1;
sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf; sock->sendPacket((void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
pkt->iNPC_ID = eggId; PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
pkt->iSkillID = skillId;
pkt->eST = (int32_t)skill->skillType;
pkt->iTargetCnt = 1;
if(result.size > 0) { if (CBFlag == 0)
void* attached = (void*)(pkt + 1); return -1;
memcpy(attached, result.payload, result.size);
} std::pair<CNSocket*, int32_t> key = std::make_pair(sock, CBFlag);
NPCManager::sendToViewable(src.getEntity(), pkt, P_FE2CL_NPC_SKILL_HIT, resplen); // save the buff serverside;
// if you get the same buff again, new duration will override the previous one
time_t until = getTime() + (time_t)duration * 1000;
EggBuffs[key] = until;
return 0;
} }
static void eggStep(CNServer* serv, time_t currTime) { static void eggStep(CNServer* serv, time_t currTime) {
// tick buffs
time_t timeStamp = currTime;
auto it = EggBuffs.begin();
while (it != EggBuffs.end()) {
// check remaining time
if (it->second > timeStamp) {
it++;
} else { // if time reached 0
CNSocket* sock = it->first.first;
int32_t CBFlag = it->first.second;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
int groupFlags = Groups::getGroupFlags(otherPlr);
for (auto& pwr : Nanos::NanoPowers) {
if (pwr.bitFlag == CBFlag) { // pick the power with the right flag and unbuff
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp);
resp.eCSTB = pwr.timeBuffID;
resp.eTBU = 2;
resp.eTBT = 3; // for egg buffs
plr->iConditionBitFlag &= ~CBFlag;
resp.iConditionBitFlag = plr->iConditionBitFlag |= groupFlags | plr->iSelfConditionBitFlag;
sock->sendPacket(resp, P_FE2CL_PC_BUFF_UPDATE);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, resp2); // send a buff timeout to other players
resp2.eCT = 1;
resp2.iID = plr->iID;
resp2.iConditionBitFlag = plr->iConditionBitFlag;
PlayerManager::sendToViewable(sock, resp2, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT);
}
}
// remove buff from the map
it = EggBuffs.erase(it);
}
}
// check dead eggs and eggs in inactive chunks // check dead eggs and eggs in inactive chunks
for (auto npc : NPCManager::NPCs) { for (auto npc : NPCManager::NPCs) {
if (npc.second->kind != EntityKind::EGG) if (npc.second->type != EntityType::EGG)
continue; continue;
auto egg = (Egg*)npc.second; auto egg = (Egg*)npc.second;
if (!egg->dead || !Chunking::inPopulatedChunks(&egg->viewableChunks)) if (!egg->dead || !Chunking::inPopulatedChunks(&egg->viewableChunks))
continue; continue;
if (egg->deadUntil <= currTime) { if (egg->deadUntil <= timeStamp) {
// respawn it // respawn it
egg->dead = false; egg->dead = false;
egg->deadUntil = 0; egg->deadUntil = 0;
egg->hp = 400; egg->appearanceData.iHP = 400;
Chunking::addEntityToChunks(Chunking::getViewableChunks(egg->chunkPos), {npc.first}); Chunking::addEntityToChunks(Chunking::getViewableChunks(egg->chunkPos), {npc.first});
} }
@@ -126,6 +143,15 @@ 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) { static void eggPickup(CNSocket* sock, CNPacketData* data) {
auto pickup = (sP_CL2FE_REQ_SHINY_PICKUP*)data->buf; auto pickup = (sP_CL2FE_REQ_SHINY_PICKUP*)data->buf;
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
@@ -137,7 +163,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
return; return;
} }
auto egg = (Egg*)eggRef.getEntity(); auto egg = (Egg*)eggRef.getEntity();
if (egg->kind != EntityKind::EGG) { if (egg->type != EntityType::EGG) {
std::cout << "[WARN] Player tried to open something other than an?!" << std::endl; std::cout << "[WARN] Player tried to open something other than an?!" << std::endl;
return; return;
} }
@@ -154,7 +180,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
} }
*/ */
int typeId = egg->type; int typeId = egg->appearanceData.iNPCType;
if (EggTypes.find(typeId) == EggTypes.end()) { if (EggTypes.find(typeId) == EggTypes.end()) {
std::cout << "[WARN] Egg Type " << typeId << " not found!" << std::endl; std::cout << "[WARN] Egg Type " << typeId << " not found!" << std::endl;
return; return;
@@ -162,13 +188,16 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
EggType* type = &EggTypes[typeId]; EggType* type = &EggTypes[typeId];
// buff the player
if (type->effectId != 0)
eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration);
/* /*
* SHINY_PICKUP_SUCC is only causing a GUI effect in the client * SHINY_PICKUP_SUCC is only causing a GUI effect in the client
* (buff icon pops up in the bottom of the screen) * (buff icon pops up in the bottom of the screen)
* so we don't send it for non-effect * so we don't send it for non-effect
*/ */
if (type->effectId != 0) { if (type->effectId != 0) {
eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration);
INITSTRUCT(sP_FE2CL_REP_SHINY_PICKUP_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_SHINY_PICKUP_SUCC, resp);
resp.iSkillID = type->effectId; resp.iSkillID = type->effectId;
@@ -183,7 +212,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
// drop // drop
if (type->dropCrateId != 0) { if (type->dropCrateId != 0) {
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward); const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE); assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
// we know it's only one trailing struct, so we can skip full validation // 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 uint8_t respbuf[resplen]; // not a variable length array, don't worry
@@ -226,7 +255,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
Chunking::removeEntityFromChunks(Chunking::getViewableChunks(egg->chunkPos), eggRef); Chunking::removeEntityFromChunks(Chunking::getViewableChunks(egg->chunkPos), eggRef);
egg->dead = true; egg->dead = true;
egg->deadUntil = getTime() + (time_t)type->regen * 1000; egg->deadUntil = getTime() + (time_t)type->regen * 1000;
egg->hp = 0; egg->appearanceData.iHP = 0;
} }
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "core/Core.hpp" #include "core/Core.hpp"
#include "Entities.hpp"
struct EggType { struct EggType {
int dropCrateId; int dropCrateId;
@@ -10,9 +11,12 @@ struct EggType {
}; };
namespace Eggs { namespace Eggs {
extern std::map<std::pair<CNSocket*, int32_t>, time_t> EggBuffs;
extern std::unordered_map<int, EggType> EggTypes; extern std::unordered_map<int, EggType> EggTypes;
void init(); void init();
void eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration); /// returns -1 on fail
int eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration);
void npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg);
} }

View File

@@ -1,10 +1,9 @@
#include "Email.hpp" #include "Email.hpp"
#include "core/Core.hpp" #include "core/Core.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp" #include "servers/CNShardServer.hpp"
#include "servers/Monitor.hpp"
#include "db/Database.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Items.hpp" #include "Items.hpp"
#include "Chat.hpp" #include "Chat.hpp"
@@ -75,7 +74,7 @@ static void emailReceiveTaros(CNSocket* sock, CNPacketData* data) {
Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex); Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex);
// money transfer // money transfer
plr->addCapped(CappedValueType::TAROS, email.Taros); plr->money += email.Taros;
email.Taros = 0; email.Taros = 0;
// update Taros in email // update Taros in email
Database::updateEmailContent(&email); Database::updateEmailContent(&email);
@@ -92,7 +91,7 @@ static void emailReceiveItemSingle(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM*)data->buf; auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM*)data->buf;
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
if (pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT || pkt->iEmailItemSlot < 1 || pkt->iEmailItemSlot > 4) if (pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT || pkt->iSlotNum < 1 || pkt->iSlotNum > 4)
return; // sanity check return; // sanity check
// get email item from db and delete it // get email item from db and delete it
@@ -227,10 +226,10 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_SUCC, resp);
Player otherPlr = {};
Database::getPlayer(&otherPlr, pkt->iTo_PCUID);
if (pkt->iCash || pkt->aItem[0].ItemInven.iID) { if (pkt->iCash || pkt->aItem[0].ItemInven.iID) {
// if there are item or taro attachments // if there are item or taro attachments
Player otherPlr = {};
Database::getPlayer(&otherPlr, pkt->iTo_PCUID);
if (otherPlr.iID != 0 && plr->PCStyle2.iPayzoneFlag != otherPlr.PCStyle2.iPayzoneFlag) { if (otherPlr.iID != 0 && plr->PCStyle2.iPayzoneFlag != otherPlr.PCStyle2.iPayzoneFlag) {
// if the players are not in the same time period // if the players are not in the same time period
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_FAIL, resp); INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_FAIL, resp);
@@ -251,30 +250,15 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
if (attachment.ItemInven.iID == 0) if (attachment.ItemInven.iID == 0)
continue; continue;
sItemBase* item = &pkt->aItem[i].ItemInven;
sItemBase* real = &plr->Inven[attachment.iSlotNum];
resp.aItem[i] = attachment; resp.aItem[i] = attachment;
attachments.push_back(attachment.ItemInven); attachments.push_back(attachment.ItemInven);
attSlots.push_back(attachment.iSlotNum); attSlots.push_back(attachment.iSlotNum);
if (real->iOpt <= item->iOpt) // delete item (if they attached the whole stack) // delete item
*real = { 0, 0, 0, 0 }; plr->Inven[attachment.iSlotNum] = { 0, 0, 0, 0 };
else // otherwise, decrement the item
real->iOpt -= item->iOpt;
// HACK: update the slot
INITSTRUCT(sP_FE2CL_PC_ITEM_MOVE_SUCC, itemResp);
itemResp.iFromSlotNum = attachment.iSlotNum;
itemResp.iToSlotNum = attachment.iSlotNum;
itemResp.FromSlotItem = *real;
itemResp.ToSlotItem = *real;
itemResp.eFrom = (int32_t)Items::SlotType::INVENTORY;
itemResp.eTo = (int32_t)Items::SlotType::INVENTORY;
sock->sendPacket(itemResp, P_FE2CL_PC_ITEM_MOVE_SUCC);
} }
int cost = pkt->iCash + 50 + 20 * attachments.size(); // attached taros + postage int cost = pkt->iCash + 50 + 20 * attachments.size(); // attached taros + postage
plr->subtractCapped(CappedValueType::TAROS, cost); plr->money -= cost;
Database::EmailData email = { Database::EmailData email = {
(int)pkt->iTo_PCUID, // PlayerId (int)pkt->iTo_PCUID, // PlayerId
Database::getNextEmailIndex(pkt->iTo_PCUID), // MsgIndex Database::getNextEmailIndex(pkt->iTo_PCUID), // MsgIndex
@@ -290,8 +274,8 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
0 // DeleteTime (unimplemented) 0 // DeleteTime (unimplemented)
}; };
if (!Database::sendEmail(&email, attachments, plr)) { if (!Database::sendEmail(&email, attachments)) {
plr->addCapped(CappedValueType::TAROS, cost); // give money back plr->money += cost; // give money back
// give items back // give items back
while (!attachments.empty()) { while (!attachments.empty()) {
sItemBase attachment = attachments.back(); sItemBase attachment = attachments.back();
@@ -320,17 +304,6 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
resp.iTo_PCUID = pkt->iTo_PCUID; resp.iTo_PCUID = pkt->iTo_PCUID;
sock->sendPacket(resp, P_FE2CL_REP_PC_SEND_EMAIL_SUCC); sock->sendPacket(resp, P_FE2CL_REP_PC_SEND_EMAIL_SUCC);
std::string logEmail = "[Email] " + PlayerManager::getPlayerName(plr, true) + " (to " + PlayerManager::getPlayerName(&otherPlr, true) + "): <" + email.SubjectLine + ">\n" + email.MsgBody;
std::cout << logEmail << std::endl;
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() { void Email::init() {

View File

@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <vector>
#include <string>
namespace Email { namespace Email {
void init(); void init();
} }

View File

@@ -1,28 +1,30 @@
#include "core/Core.hpp"
#include "Entities.hpp" #include "Entities.hpp"
#include "Chunking.hpp"
#include "NPCManager.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Eggs.hpp"
#include "MobAI.hpp"
#include <assert.h> #include <type_traits>
static_assert(std::is_standard_layout<EntityRef>::value); static_assert(std::is_standard_layout<EntityRef>::value);
static_assert(std::is_trivially_copyable<EntityRef>::value); static_assert(std::is_trivially_copyable<EntityRef>::value);
EntityRef::EntityRef(CNSocket *s) { EntityRef::EntityRef(CNSocket *s) {
kind = EntityKind::PLAYER; type = EntityType::PLAYER;
sock = s; sock = s;
} }
EntityRef::EntityRef(int32_t i) { EntityRef::EntityRef(int32_t i) {
id = i; id = i;
kind = EntityKind::INVALID; assert(NPCManager::NPCs.find(id) != NPCManager::NPCs.end());
if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end()) type = NPCManager::NPCs[id]->type;
kind = NPCManager::NPCs[id]->kind;
} }
bool EntityRef::isValid() const { bool EntityRef::isValid() const {
if (kind == EntityKind::PLAYER) if (type == EntityType::PLAYER)
return PlayerManager::players.find(sock) != PlayerManager::players.end(); return PlayerManager::players.find(sock) != PlayerManager::players.end();
return NPCManager::NPCs.find(id) != NPCManager::NPCs.end(); return NPCManager::NPCs.find(id) != NPCManager::NPCs.end();
@@ -31,38 +33,21 @@ bool EntityRef::isValid() const {
Entity *EntityRef::getEntity() const { Entity *EntityRef::getEntity() const {
assert(isValid()); assert(isValid());
if (kind == EntityKind::PLAYER) if (type == EntityType::PLAYER)
return PlayerManager::getPlayer(sock); return PlayerManager::getPlayer(sock);
return NPCManager::NPCs[id]; return NPCManager::NPCs[id];
} }
sNPCAppearanceData BaseNPC::getAppearanceData() {
sNPCAppearanceData data = {};
data.iAngle = angle;
data.iBarkerType = 0; // unused?
data.iConditionBitFlag = 0;
data.iHP = hp;
data.iNPCType = type;
data.iNPC_ID = id;
data.iX = x;
data.iY = y;
data.iZ = z;
return data;
}
sNPCAppearanceData CombatNPC::getAppearanceData() {
sNPCAppearanceData data = BaseNPC::getAppearanceData();
data.iConditionBitFlag = getCompositeCondition();
return data;
}
/* /*
* Entity coming into view. * Entity coming into view.
*/ */
void BaseNPC::enterIntoViewOf(CNSocket *sock) { void BaseNPC::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt); INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
pkt.NPCAppearanceData = getAppearanceData(); pkt.NPCAppearanceData = appearanceData;
pkt.NPCAppearanceData.iX = x;
pkt.NPCAppearanceData.iY = y;
pkt.NPCAppearanceData.iZ = z;
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER); sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
} }
@@ -70,172 +55,39 @@ void Bus::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_TRANSPORTATION_ENTER, pkt); INITSTRUCT(sP_FE2CL_TRANSPORTATION_ENTER, pkt);
// TODO: Potentially decouple this from BaseNPC? // TODO: Potentially decouple this from BaseNPC?
pkt.AppearanceData = getTransportationAppearanceData(); pkt.AppearanceData = {
3, appearanceData.iNPC_ID, appearanceData.iNPCType,
x, y, z
};
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_ENTER); sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_ENTER);
} }
void Egg::enterIntoViewOf(CNSocket *sock) { void Egg::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt); INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt);
// TODO: Potentially decouple this from BaseNPC? Eggs::npcDataToEggData(x, y, z, &appearanceData, &pkt.ShinyAppearanceData);
pkt.ShinyAppearanceData = getShinyAppearanceData();
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER); 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
};
}
sNano* Player::getActiveNano() {
return &Nanos[activeNano];
}
sPCAppearanceData Player::getAppearanceData() {
sPCAppearanceData data = {};
data.iID = iID;
data.iHP = HP;
data.iLv = level;
data.iX = x;
data.iY = y;
data.iZ = z;
data.iAngle = angle;
data.PCStyle = PCStyle;
data.Nano = Nanos[activeNano];
data.iPCState = iPCState;
data.iSpecialState = iSpecialState;
memcpy(data.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT);
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() // TODO: this is less effiecient than it was, because of memset()
void Player::enterIntoViewOf(CNSocket *sock) { void Player::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_PC_NEW, pkt); INITSTRUCT(sP_FE2CL_PC_NEW, pkt);
pkt.PCAppearanceData = getAppearanceData();
pkt.PCAppearanceData.iID = iID;
pkt.PCAppearanceData.iHP = HP;
pkt.PCAppearanceData.iLv = level;
pkt.PCAppearanceData.iX = x;
pkt.PCAppearanceData.iY = y;
pkt.PCAppearanceData.iZ = z;
pkt.PCAppearanceData.iAngle = angle;
pkt.PCAppearanceData.PCStyle = PCStyle;
pkt.PCAppearanceData.Nano = Nanos[activeNano];
pkt.PCAppearanceData.iPCState = iPCState;
pkt.PCAppearanceData.iSpecialState = iSpecialState;
memcpy(pkt.PCAppearanceData.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT);
sock->sendPacket(pkt, P_FE2CL_PC_NEW); sock->sendPacket(pkt, P_FE2CL_PC_NEW);
} }
@@ -244,20 +96,20 @@ void Player::enterIntoViewOf(CNSocket *sock) {
*/ */
void BaseNPC::disappearFromViewOf(CNSocket *sock) { void BaseNPC::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt); INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
pkt.iNPC_ID = id; pkt.iNPC_ID = appearanceData.iNPC_ID;
sock->sendPacket(pkt, P_FE2CL_NPC_EXIT); sock->sendPacket(pkt, P_FE2CL_NPC_EXIT);
} }
void Bus::disappearFromViewOf(CNSocket *sock) { void Bus::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_TRANSPORTATION_EXIT, pkt); INITSTRUCT(sP_FE2CL_TRANSPORTATION_EXIT, pkt);
pkt.eTT = 3; pkt.eTT = 3;
pkt.iT_ID = id; pkt.iT_ID = appearanceData.iNPC_ID;
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_EXIT); sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_EXIT);
} }
void Egg::disappearFromViewOf(CNSocket *sock) { void Egg::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_EXIT, pkt); INITSTRUCT(sP_FE2CL_SHINY_EXIT, pkt);
pkt.iShinyID = id; pkt.iShinyID = appearanceData.iNPC_ID;
sock->sendPacket(pkt, P_FE2CL_SHINY_EXIT); sock->sendPacket(pkt, P_FE2CL_SHINY_EXIT);
} }

View File

@@ -2,25 +2,23 @@
#include "core/Core.hpp" #include "core/Core.hpp"
#include "EntityRef.hpp" #include <stdint.h>
#include "Buffs.hpp"
#include "Chunking.hpp"
#include "Groups.hpp"
#include <set> #include <set>
#include <map>
#include <functional>
enum class AIState { enum class EntityType : uint8_t {
INACTIVE, INVALID,
ROAMING, PLAYER,
COMBAT, SIMPLE_NPC,
RETREAT, COMBAT_NPC,
DEAD MOB,
EGG,
BUS
}; };
class Chunk;
struct Entity { struct Entity {
EntityKind kind = EntityKind::INVALID; EntityType type = EntityType::INVALID;
int x = 0, y = 0, z = 0; int x = 0, y = 0, z = 0;
uint64_t instanceID = 0; uint64_t instanceID = 0;
ChunkPos chunkPos = {}; ChunkPos chunkPos = {};
@@ -29,39 +27,47 @@ struct Entity {
// destructor must be virtual, apparently // destructor must be virtual, apparently
virtual ~Entity() {} virtual ~Entity() {}
virtual bool isExtant() { return true; } virtual bool isAlive() { return true; }
// stubs // stubs
virtual void enterIntoViewOf(CNSocket *sock) = 0; virtual void enterIntoViewOf(CNSocket *sock) = 0;
virtual void disappearFromViewOf(CNSocket *sock) = 0; virtual void disappearFromViewOf(CNSocket *sock) = 0;
}; };
/* struct EntityRef {
* Interfaces EntityType type;
*/ union {
class ICombatant { CNSocket *sock;
public: int32_t id;
ICombatant() {} };
virtual ~ICombatant() {}
virtual bool addBuff(int, BuffCallback<int, BuffStack*>, BuffCallback<time_t>, BuffStack*) = 0; EntityRef(CNSocket *s);
virtual Buff* getBuff(int) = 0; EntityRef(int32_t i);
virtual void removeBuff(int) = 0;
virtual void removeBuff(int, BuffClass) = 0; bool isValid() const;
virtual void clearBuffs(bool) = 0; Entity *getEntity() const;
virtual bool hasBuff(int) = 0;
virtual int getCompositeCondition() = 0; bool operator==(const EntityRef& other) const {
virtual int takeDamage(EntityRef, int) = 0; if (type != other.type)
virtual int heal(EntityRef, int) = 0; return false;
virtual bool isAlive() = 0;
virtual int getCurrentHP() = 0; if (type == EntityType::PLAYER)
virtual int getMaxHP() = 0; return sock == other.sock;
virtual int getLevel() = 0;
virtual std::vector<EntityRef> getGroupMembers() = 0; return id == other.id;
virtual int32_t getCharType() = 0; }
virtual int32_t getID() = 0;
virtual EntityRef getRef() = 0; // arbitrary ordering
virtual void step(time_t currTime) = 0; bool operator<(const EntityRef& other) const {
if (type == other.type) {
if (type == EntityType::PLAYER)
return sock < other.sock;
else
return id < other.id;
}
return type < other.type;
}
}; };
/* /*
@@ -69,111 +75,77 @@ public:
*/ */
class BaseNPC : public Entity { class BaseNPC : public Entity {
public: public:
int id; sNPCAppearanceData appearanceData = {};
int type;
int hp;
int angle;
bool loopingPath = false; bool loopingPath = false;
BaseNPC(int _A, uint64_t iID, int t, int _id) { BaseNPC(int _X, int _Y, int _Z, int angle, uint64_t iID, int t, int id) { // XXX
kind = EntityKind::SIMPLE_NPC; x = _X;
type = t; y = _Y;
hp = 400; z = _Z;
angle = _A; appearanceData.iNPCType = t;
id = _id; appearanceData.iHP = 400;
appearanceData.iAngle = angle;
appearanceData.iConditionBitFlag = 0;
appearanceData.iBarkerType = 0;
appearanceData.iNPC_ID = id;
instanceID = iID; instanceID = iID;
}; };
virtual void enterIntoViewOf(CNSocket *sock) override; virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override;
virtual sNPCAppearanceData getAppearanceData();
}; };
struct CombatNPC : public BaseNPC, public ICombatant { struct CombatNPC : public BaseNPC {
int maxHealth = 0; int maxHealth = 0;
int spawnX = 0; int spawnX = 0;
int spawnY = 0; int spawnY = 0;
int spawnZ = 0; int spawnZ = 0;
int level = 0; int level = 0;
int speed = 300; int speed = 300;
AIState state = AIState::INACTIVE;
Group* group = nullptr;
int playersInView = 0; // for optimizing away AI in empty chunks
std::map<AIState, void (*)(CombatNPC*, time_t)> stateHandlers; void (*_stepAI)(CombatNPC*, time_t) = nullptr;
std::map<AIState, void (*)(CombatNPC*, EntityRef)> transitionHandlers;
std::unordered_map<int, Buff*> buffs = {}; // XXX
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP) :
BaseNPC(x, y, z, angle, iID, t, id),
maxHealth(maxHP) {}
CombatNPC(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, int id, int maxHP) virtual void stepAI(time_t currTime) {
: BaseNPC(angle, iID, t, id), maxHealth(maxHP) { if (_stepAI != nullptr)
this->spawnX = spawnX; _stepAI(this, currTime);
this->spawnY = spawnY;
this->spawnZ = spawnZ;
kind = EntityKind::COMBAT_NPC;
stateHandlers[AIState::INACTIVE] = {};
transitionHandlers[AIState::INACTIVE] = {};
} }
virtual sNPCAppearanceData getAppearanceData() override; virtual bool isAlive() override { return appearanceData.iHP > 0; }
virtual bool isExtant() override { return hp > 0; }
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
virtual Buff* getBuff(int buffId) override;
virtual void removeBuff(int buffId) override;
virtual void removeBuff(int buffId, BuffClass buffClass) override;
virtual void clearBuffs(bool force) override;
virtual bool hasBuff(int buffId) override;
virtual int getCompositeCondition() override;
virtual int takeDamage(EntityRef src, int amt) override;
virtual int heal(EntityRef src, int amt) override;
virtual bool isAlive() override;
virtual int getCurrentHP() override;
virtual int getMaxHP() override;
virtual int getLevel() override;
virtual std::vector<EntityRef> getGroupMembers() override;
virtual int32_t getCharType() override;
virtual int32_t getID() override;
virtual EntityRef getRef() override;
virtual void step(time_t currTime) override;
virtual void transition(AIState newState, EntityRef src);
}; };
// Mob is in MobAI.hpp, Player is in Player.hpp // Mob is in MobAI.hpp, Player is in Player.hpp
// TODO: decouple from BaseNPC
struct Egg : public BaseNPC { struct Egg : public BaseNPC {
bool summoned = false; bool summoned = false;
bool dead = false; bool dead = false;
time_t deadUntil; time_t deadUntil;
Egg(uint64_t iID, int t, int32_t id, bool summon) Egg(int x, int y, int z, uint64_t iID, int t, int32_t id, bool summon)
: BaseNPC(0, iID, t, id) { : BaseNPC(x, y, z, 0, iID, t, id) {
summoned = summon; summoned = summon;
kind = EntityKind::EGG; type = EntityType::EGG;
} }
virtual bool isExtant() override { return !dead; } virtual bool isAlive() override { return !dead; }
virtual void enterIntoViewOf(CNSocket *sock) override; virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override;
sShinyAppearanceData getShinyAppearanceData();
}; };
// TODO: decouple from BaseNPC
struct Bus : public BaseNPC { struct Bus : public BaseNPC {
Bus(int angle, uint64_t iID, int t, int id) : Bus(int x, int y, int z, int angle, uint64_t iID, int t, int id) :
BaseNPC(angle, iID, t, id) { BaseNPC(x, y, z, angle, iID, t, id) {
kind = EntityKind::BUS; type = EntityType::BUS;
loopingPath = true;
} }
virtual void enterIntoViewOf(CNSocket *sock) override; virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override;
sTransportationAppearanceData getTransportationAppearanceData();
}; };

View File

@@ -1,56 +0,0 @@
#pragma once
#include "core/Core.hpp"
/* forward declaration(s) */
struct Entity;
enum EntityKind {
INVALID,
PLAYER,
SIMPLE_NPC,
COMBAT_NPC,
MOB,
EGG,
BUS
};
struct EntityRef {
EntityKind kind;
union {
CNSocket *sock;
int32_t id;
};
EntityRef(CNSocket *s);
EntityRef(int32_t i);
bool isValid() const;
Entity *getEntity() const;
bool operator==(const EntityRef& other) const {
if (kind != other.kind)
return false;
if (kind == EntityKind::PLAYER)
return sock == other.sock;
return id == other.id;
}
bool operator!=(const EntityRef& other) const {
return !(*this == other);
}
// arbitrary ordering
bool operator<(const EntityRef& other) const {
if (kind == other.kind) {
if (kind == EntityKind::PLAYER)
return sock < other.sock;
else
return id < other.id;
}
return kind < other.kind;
}
};

View File

@@ -1,10 +1,13 @@
#include "Groups.hpp"
#include "servers/CNShardServer.hpp" #include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Entities.hpp" #include "Groups.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include <iostream>
#include <chrono>
#include <algorithm>
#include <thread>
/* /*
* NOTE: Variadic response packets that list group members are technically * NOTE: Variadic response packets that list group members are technically
@@ -16,186 +19,22 @@
using namespace Groups; using namespace Groups;
Group::Group(EntityRef leader) {
addToGroup(this, leader);
}
static void attachGroupData(std::vector<EntityRef>& pcs, std::vector<EntityRef>& npcs, uint8_t* pivot) {
for(EntityRef pcRef : pcs) {
sPCGroupMemberInfo* info = (sPCGroupMemberInfo*)pivot;
Player* plr = PlayerManager::getPlayer(pcRef.sock);
info->iPC_ID = plr->iID;
info->iPCUID = plr->PCStyle.iPC_UID;
info->iNameCheck = plr->PCStyle.iNameCheck;
memcpy(info->szFirstName, plr->PCStyle.szFirstName, sizeof(plr->PCStyle.szFirstName));
memcpy(info->szLastName, plr->PCStyle.szLastName, sizeof(plr->PCStyle.szLastName));
info->iSpecialState = plr->iSpecialState;
info->iLv = plr->level;
info->iHP = plr->HP;
info->iMaxHP = PC_MAXHEALTH(plr->level);
// info->iMapType = 0;
// info->iMapNum = 0;
info->iX = plr->x;
info->iY = plr->y;
info->iZ = plr->z;
if(plr->activeNano > 0) {
info->Nano = *plr->getActiveNano();
info->bNano = true;
}
pivot = (uint8_t*)(info + 1);
}
for(EntityRef npcRef : npcs) {
sNPCGroupMemberInfo* info = (sNPCGroupMemberInfo*)pivot;
// probably should not assume that the combatant is an
// entity, but it works for now
BaseNPC* npc = (BaseNPC*)npcRef.getEntity();
info->iNPC_ID = npcRef.id;
info->iNPC_Type = npc->type;
info->iHP = npc->hp;
info->iX = npc->x;
info->iY = npc->y;
info->iZ = npc->z;
pivot = (uint8_t*)(info + 1);
}
}
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;
}
else if (member.kind == EntityKind::COMBAT_NPC) {
CombatNPC* npc = (CombatNPC*)member.getEntity();
npc->group = group;
}
else {
std::cout << "[WARN] Adding a weird entity type to a group" << std::endl;
}
group->members.push_back(member);
if(member.kind == EntityKind::PLAYER) {
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
size_t pcCount = pcs.size();
size_t npcCount = npcs.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;
pkt->iMemberPCCnt = (int32_t)pcCount;
pkt->iMemberNPCCnt = (int32_t)npcCount;
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), pcCount, sizeof(sPCGroupMemberInfo))
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size" << std::endl;
} else {
uint8_t* pivot = (uint8_t*)(pkt + 1);
attachGroupData(pcs, npcs, pivot);
// PC_GROUP_JOIN_SUCC and PC_GROUP_JOIN carry identical payloads but have different IDs
// (and the client does care!) so we need to send one to the new member
// and the other to the rest
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo);
member.sock->sendPacket(respbuf, P_FE2CL_PC_GROUP_JOIN_SUCC, resplen);
sendToGroup(group, member, respbuf, P_FE2CL_PC_GROUP_JOIN, resplen);
}
}
}
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
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, leavePkt);
member.sock->sendPacket(leavePkt, P_FE2CL_PC_GROUP_LEAVE_SUCC);
}
else if (member.kind == EntityKind::COMBAT_NPC) {
CombatNPC* npc = (CombatNPC*)member.getEntity();
npc->group = nullptr;
}
else {
std::cout << "[WARN] Removing a weird entity type from a group" << std::endl;
}
auto it = std::find(group->members.begin(), group->members.end(), member);
if (it == group->members.end()) {
std::cout << "[WARN] Tried to remove a member that isn't in the group" << std::endl;
} else {
group->members.erase(it);
}
if(member.kind == EntityKind::PLAYER) {
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
size_t pcCount = pcs.size();
size_t npcCount = npcs.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;
pkt->iMemberPCCnt = (int32_t)pcCount;
pkt->iMemberNPCCnt = (int32_t)npcCount;
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), pcCount, sizeof(sPCGroupMemberInfo))
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size" << std::endl;
} else {
uint8_t* pivot = (uint8_t*)(pkt + 1);
attachGroupData(pcs, npcs, pivot);
sendToGroup(group, respbuf, P_FE2CL_PC_GROUP_LEAVE,
sizeof(sP_FE2CL_PC_GROUP_LEAVE) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo));
}
}
if (group->members.size() == 1) {
return removeFromGroup(group, group->members.back());
}
if (group->members.empty()) {
delete group; // cleanup memory
return true;
}
return false;
}
void Groups::disbandGroup(Group* group) {
if (group == nullptr)
return;
// remove everyone from the group!!
bool done = false;
while(!done) {
EntityRef back = group->members.back();
done = removeFromGroup(group, back);
}
}
static void requestGroup(CNSocket* sock, CNPacketData* data) { static void requestGroup(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_GROUP_INVITE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE*)data->buf; sP_CL2FE_REQ_PC_GROUP_INVITE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_To); Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_To);
if (otherPlr == nullptr)
return;
otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup);
if (otherPlr == nullptr) if (otherPlr == nullptr)
return; return;
// fail if the group is full or the other player is already in a group // fail if the group is full or the other player is already in a group
if ((plr->group != nullptr && plr->group->filter(EntityKind::PLAYER).size() >= 4) || otherPlr->group != nullptr) { if (plr->groupCnt >= 4 || otherPlr->iIDGroup != otherPlr->iID || otherPlr->groupCnt > 1) {
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_FAIL, resp); INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_FAIL, resp);
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_FAIL, sizeof(sP_FE2CL_PC_GROUP_INVITE_FAIL)); sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_FAIL, sizeof(sP_FE2CL_PC_GROUP_INVITE_FAIL));
return; return;
@@ -236,89 +75,255 @@ static void joinGroup(CNSocket* sock, CNPacketData* data) {
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_From); Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_From);
if (otherPlr == nullptr) if (otherPlr == nullptr)
return; // disconnect or something return;
int size = otherPlr->group == nullptr ? 1 : otherPlr->group->filter(EntityKind::PLAYER).size(); otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup);
if (otherPlr == nullptr)
return;
// fail if the group is full or the other player is already in a group // fail if the group is full or the other player is already in a group
if (plr->group != nullptr || size + 1 > 4) { if (plr->groupCnt > 1 || plr->iIDGroup != plr->iID || otherPlr->groupCnt >= 4) {
INITSTRUCT(sP_FE2CL_PC_GROUP_JOIN_FAIL, resp); INITSTRUCT(sP_FE2CL_PC_GROUP_JOIN_FAIL, resp);
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_JOIN_FAIL, sizeof(sP_FE2CL_PC_GROUP_JOIN_FAIL)); sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_JOIN_FAIL, sizeof(sP_FE2CL_PC_GROUP_JOIN_FAIL));
return; return;
} }
if (otherPlr->group == nullptr) { if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), otherPlr->groupCnt + 1, sizeof(sPCGroupMemberInfo))) {
// create group std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
EntityRef otherPlrRef = PlayerManager::getSockFromID(recv->iID_From); return;
otherPlr->group = new Group(otherPlrRef);
} }
addToGroup(otherPlr->group, sock);
plr->iIDGroup = otherPlr->iID;
otherPlr->groupCnt += 1;
otherPlr->groupIDs[otherPlr->groupCnt-1] = plr->iID;
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + otherPlr->groupCnt * sizeof(sPCGroupMemberInfo);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_JOIN *resp = (sP_FE2CL_PC_GROUP_JOIN*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_JOIN));
resp->iID_NewMember = plr->iID;
resp->iMemberPCCnt = otherPlr->groupCnt;
int bitFlag = getGroupFlags(otherPlr);
for (int i = 0; i < otherPlr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]);
if (varPlr == nullptr || sockTo == nullptr)
continue;
respdata[i].iPC_ID = varPlr->iID;
respdata[i].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i].iSpecialState = varPlr->iSpecialState;
respdata[i].iLv = varPlr->level;
respdata[i].iHP = varPlr->HP;
respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level);
//respdata[i].iMapType = 0;
//respdata[i].iMapNum = 0;
respdata[i].iX = varPlr->x;
respdata[i].iY = varPlr->y;
respdata[i].iZ = varPlr->z;
// client doesnt read nano data here
if (varPlr != plr) { // apply the new member's buffs to the group and the group's buffs to the new member
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 1, 1, bitFlag);
if (Nanos::SkillTable[plr->Nanos[plr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 1, 1, bitFlag);
}
}
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_JOIN, resplen);
} }
static void leaveGroup(CNSocket* sock, CNPacketData* data) { static void leaveGroup(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
groupKick(plr->group, sock); groupKickPlayer(plr);
} }
void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) { void Groups::sendToGroup(Player* plr, void* buf, uint32_t type, size_t size) {
if (group == nullptr) for (int i = 0; i < plr->groupCnt; i++) {
return; CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[i]);
auto players = group->filter(EntityKind::PLAYER); if (sock == nullptr)
for (EntityRef ref : players) { continue;
ref.sock->sendPacket(buf, type, size);
if (type == P_FE2CL_PC_GROUP_LEAVE_SUCC) {
Player* leavingPlr = PlayerManager::getPlayer(sock);
leavingPlr->iIDGroup = leavingPlr->iID;
}
sock->sendPacket(buf, type, size);
} }
} }
void Groups::sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size) { void Groups::groupTickInfo(Player* plr) {
if (group == nullptr) if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), plr->groupCnt, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
return; return;
}
auto players = group->filter(EntityKind::PLAYER); size_t resplen = sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + plr->groupCnt * sizeof(sPCGroupMemberInfo);
for (EntityRef ref : players) { uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
if(ref != excluded) ref.sock->sendPacket(buf, type, size);
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_MEMBER_INFO *resp = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO));
resp->iID = plr->iID;
resp->iMemberPCCnt = plr->groupCnt;
for (int i = 0; i < plr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
if (varPlr == nullptr)
continue;
respdata[i].iPC_ID = varPlr->iID;
respdata[i].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i].iSpecialState = varPlr->iSpecialState;
respdata[i].iLv = varPlr->level;
respdata[i].iHP = varPlr->HP;
respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level);
//respdata[i].iMapType = 0;
//respdata[i].iMapNum = 0;
respdata[i].iX = varPlr->x;
respdata[i].iY = varPlr->y;
respdata[i].iZ = varPlr->z;
if (varPlr->activeNano > 0) {
respdata[i].bNano = 1;
respdata[i].Nano = varPlr->Nanos[varPlr->activeNano];
}
}
sendToGroup(plr, (void*)&respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO, resplen);
}
static void groupUnbuff(Player* plr) {
for (int i = 0; i < plr->groupCnt; i++) {
for (int n = 0; n < plr->groupCnt; n++) {
if (i == n)
continue;
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[n]);
Nanos::applyBuff(sock, otherPlr->Nanos[otherPlr->activeNano].iSkillID, 2, 1, 0);
}
} }
} }
void Groups::groupTickInfo(CNSocket* sock) { void Groups::groupKickPlayer(Player* plr) {
Player* plr = PlayerManager::getPlayer(sock);
Group* group = plr->group;
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
size_t pcCount = pcs.size();
size_t npcCount = npcs.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;
pkt->iMemberPCCnt = (int32_t)pcCount;
pkt->iMemberNPCCnt = (int32_t)npcCount;
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), pcCount, sizeof(sPCGroupMemberInfo))
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_MEMBER_INFO packet size" << std::endl;
} else {
uint8_t* pivot = (uint8_t*)(pkt + 1);
attachGroupData(pcs, npcs, pivot);
sock->sendPacket(respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO,
sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo));
}
}
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 you are the group leader, destroy your own group and kick everybody
if (group->members[0] == ref) { if (plr->iID == plr->iIDGroup) {
disbandGroup(group); groupUnbuff(plr);
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1);
sendToGroup(plr, (void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
plr->groupCnt = 1;
return; return;
} }
removeFromGroup(group, ref); Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), otherPlr->groupCnt - 1, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size\n";
return;
}
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_LEAVE) + (otherPlr->groupCnt - 1) * sizeof(sPCGroupMemberInfo);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_LEAVE *resp = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_LEAVE));
resp->iID_LeaveMember = plr->iID;
resp->iMemberPCCnt = otherPlr->groupCnt - 1;
int bitFlag = getGroupFlags(otherPlr) & ~plr->iGroupConditionBitFlag;
int moveDown = 0;
CNSocket* sock = PlayerManager::getSockFromID(plr->iID);
if (sock == nullptr)
return;
for (int i = 0; i < otherPlr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]);
if (varPlr == nullptr || sockTo == nullptr)
continue;
if (moveDown == 1)
otherPlr->groupIDs[i-1] = otherPlr->groupIDs[i];
respdata[i-moveDown].iPC_ID = varPlr->iID;
respdata[i-moveDown].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i-moveDown].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i-moveDown].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i-moveDown].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i-moveDown].iSpecialState = varPlr->iSpecialState;
respdata[i-moveDown].iLv = varPlr->level;
respdata[i-moveDown].iHP = varPlr->HP;
respdata[i-moveDown].iMaxHP = PC_MAXHEALTH(varPlr->level);
// respdata[i-moveDown]].iMapType = 0;
// respdata[i-moveDown]].iMapNum = 0;
respdata[i-moveDown].iX = varPlr->x;
respdata[i-moveDown].iY = varPlr->y;
respdata[i-moveDown].iZ = varPlr->z;
// client doesnt read nano data here
if (varPlr == plr) {
moveDown = 1;
otherPlr->groupIDs[i] = 0;
} else { // remove the leaving member's buffs from the group and remove the group buffs from the leaving member.
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 2, 1, 0);
if (Nanos::SkillTable[plr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 2, 1, bitFlag);
}
}
plr->iIDGroup = plr->iID;
otherPlr->groupCnt -= 1;
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_LEAVE, resplen);
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1);
sock->sendPacket((void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
}
int Groups::getGroupFlags(Player* plr) {
int bitFlag = 0;
for (int i = 0; i < plr->groupCnt; i++) {
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
if (otherPlr == nullptr)
continue;
bitFlag |= otherPlr->iGroupConditionBitFlag;
}
return bitFlag;
} }
void Groups::init() { void Groups::init() {

View File

@@ -1,37 +1,17 @@
#pragma once #pragma once
#include "EntityRef.hpp" #include "Player.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include <vector> #include <map>
#include <assert.h> #include <list>
struct Group {
std::vector<EntityRef> members;
std::vector<EntityRef> filter(EntityKind kind) {
std::vector<EntityRef> filtered;
std::copy_if(members.begin(), members.end(), std::back_inserter(filtered), [kind](EntityRef e) {
return e.kind == kind;
});
return filtered;
}
EntityRef getLeader() {
assert(members.size() > 0);
return members[0];
}
Group(EntityRef leader);
};
namespace Groups { namespace Groups {
void init(); void init();
void sendToGroup(Group* group, void* buf, uint32_t type, size_t size); void sendToGroup(Player* plr, void* buf, uint32_t type, size_t size);
void sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size); void groupTickInfo(Player* plr);
void groupTickInfo(CNSocket* sock); void groupKickPlayer(Player* plr);
int getGroupFlags(Player* plr);
void groupKick(Group* group, EntityRef ref);
void addToGroup(Group* group, EntityRef member);
bool removeFromGroup(Group* group, EntityRef member); // true iff group deleted
void disbandGroup(Group* group);
} }

View File

@@ -1,19 +1,16 @@
#include "Items.hpp"
#include "servers/CNShardServer.hpp" #include "servers/CNShardServer.hpp"
#include "Items.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Nanos.hpp" #include "Nanos.hpp"
#include "NPCManager.hpp"
#include "Player.hpp"
#include "Abilities.hpp" #include "Abilities.hpp"
#include "Eggs.hpp"
#include "MobAI.hpp"
#include "Missions.hpp" #include "Missions.hpp"
#include "Buffs.hpp" #include "Eggs.hpp"
#include "Rand.hpp"
#include <string.h> // for memset() #include <string.h> // for memset()
#include <assert.h> #include <assert.h>
#include <numeric>
using namespace Items; using namespace Items;
@@ -33,19 +30,6 @@ std::map<int32_t, int32_t> Items::EventToDropMap;
std::map<int32_t, int32_t> Items::MobToDropMap; std::map<int32_t, int32_t> Items::MobToDropMap;
std::map<int32_t, ItemSet> Items::ItemSets; 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 #ifdef ACADEMY
std::map<int32_t, int32_t> Items::NanoCapsules; // crate id -> nano id std::map<int32_t, int32_t> Items::NanoCapsules; // crate id -> nano id
@@ -59,7 +43,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 // 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); const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE); assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
// we know it's only one trailing struct, so we can skip full validation // 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 uint8_t respbuf[resplen]; // not a variable length array, don't worry
@@ -215,7 +199,7 @@ static int getCrateItem(sItemBase* result, int itemSetId, int rarity, int player
return -1; return -1;
} }
// initialize all weights as the default weight for all item slots // initialize all weights as the default weight for all item slots
std::vector<int> itemWeights(validItems.size(), itemSet.defaultItemWeight); std::vector<int> itemWeights(validItems.size(), itemSet.defaultItemWeight);
if (!itemSet.alterItemWeightMap.empty()) { if (!itemSet.alterItemWeightMap.empty()) {
@@ -335,14 +319,14 @@ static void itemMoveHandler(CNSocket* sock, CNPacketData* data) {
// if equipping an item, validate that it's of the correct type for the slot // if equipping an item, validate that it's of the correct type for the slot
if ((SlotType)itemmove->eTo == SlotType::EQUIP) { if ((SlotType)itemmove->eTo == SlotType::EQUIP) {
if (fromItem->iType == 10 && itemmove->iToSlotNum != EQUIP_SLOT_VEHICLE) if (fromItem->iType == 10 && itemmove->iToSlotNum != 8)
return; // vehicle in wrong slot return; // vehicle in wrong slot
else if (fromItem->iType != 10 else if (fromItem->iType != 10
&& !(fromItem->iType == 0 && itemmove->iToSlotNum == 7) && !(fromItem->iType == 0 && itemmove->iToSlotNum == 7)
&& fromItem->iType != itemmove->iToSlotNum) && fromItem->iType != itemmove->iToSlotNum)
return; // something other than a vehicle or a weapon in a non-matching slot return; // something other than a vehicle or a weapon in a non-matching slot
else if (itemmove->iToSlotNum >= AEQUIP_COUNT_MINUS_BOOSTERS) else if (itemmove->iToSlotNum >= AEQUIP_COUNT) // TODO: reject slots >= 9?
return; // boosters can't be equipped via move packet return; // invalid slot
} }
// save items to response // save items to response
@@ -399,7 +383,7 @@ static void itemMoveHandler(CNSocket* sock, CNPacketData* data) {
} }
// unequip vehicle if equip slot 8 is 0 // unequip vehicle if equip slot 8 is 0
if (plr->Equip[EQUIP_SLOT_VEHICLE].iID == 0 && plr->iPCState & 8) { if (plr->Equip[8].iID == 0 && plr->iPCState & 8) {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response); INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC); sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC);
@@ -429,9 +413,6 @@ static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
if (itemdel->iSlotNum < 0 || itemdel->iSlotNum >= AINVEN_COUNT)
return; // sanity check
resp.eIL = itemdel->eIL; resp.eIL = itemdel->eIL;
resp.iSlotNum = itemdel->iSlotNum; resp.iSlotNum = itemdel->iSlotNum;
@@ -443,19 +424,30 @@ static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_DELETE_SUCC); sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_DELETE_SUCC);
} }
static void useGumball(CNSocket* sock, CNPacketData* data) { static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf; auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf;
Player* player = PlayerManager::getPlayer(sock); 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 // gumball can only be used from inventory, so we ignore eIL
sItemBase gumball = player->Inven[request->iSlotNum]; sItemBase gumball = player->Inven[request->iSlotNum];
sNano nano = player->Nanos[player->equippedNanos[request->iNanoSlot]]; 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 // sanity check, check if gumball type matches nano style
int nanoStyle = Nanos::nanoStyle(nano.iID); int nanoStyle = Nanos::nanoStyle(nano.iID);
if (!((gumball.iID == GENERALITEM_GUMBALL_ADAPTIUM && nanoStyle == 0) || if (!((gumball.iID == 119 && nanoStyle == 0) ||
( gumball.iID == GENERALITEM_GUMBALL_BLASTONS && nanoStyle == 1) || ( gumball.iID == 120 && nanoStyle == 1) ||
( gumball.iID == GENERALITEM_GUMBALL_COSMIX && nanoStyle == 2))) { ( gumball.iID == 121 && nanoStyle == 2))) {
std::cout << "[WARN] Gumball type doesn't match nano type" << std::endl; std::cout << "[WARN] Gumball type doesn't match nano type" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response); INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL); sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
@@ -474,6 +466,9 @@ static void useGumball(CNSocket* sock, CNPacketData* data) {
return; return;
} }
if (gumball.iOpt == 0)
gumball = {};
uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen); memset(respbuf, 0, resplen);
@@ -484,156 +479,30 @@ static void useGumball(CNSocket* sock, CNPacketData* data) {
resp->iSlotNum = request->iSlotNum; resp->iSlotNum = request->iSlotNum;
resp->RemainItem = gumball; resp->RemainItem = gumball;
resp->iTargetCnt = 1; resp->iTargetCnt = 1;
resp->eST = (int32_t)SkillType::NANOSTIMPAK; resp->eST = EST_NANOSTIMPAK;
resp->iSkillID = 144; resp->iSkillID = 144;
int eCSB = ECSB_STIMPAKSLOT1 + request->iNanoSlot; int value1 = CSB_BIT_STIMPAKSLOT1 << request->iNanoSlot;
int value2 = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
respdata->eCT = 1; respdata->eCT = 1;
respdata->iID = player->iID; respdata->iID = player->iID;
respdata->iConditionBitFlag = CSB_FROM_ECSB(eCSB); respdata->iConditionBitFlag = value1;
int durationMilliseconds = Abilities::SkillTable[144].durationTime[0] * 100; INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
BuffStack gumballBuff = { pkt.eCSTB = value2; // eCharStatusTimeBuffID
durationMilliseconds / MS_PER_PLAYER_TICK, pkt.eTBU = 1; // eTimeBuffUpdate
0, pkt.eTBT = 1; // eTimeBuffType 1 means nano
sock, pkt.iConditionBitFlag = player->iConditionBitFlag |= value1;
BuffClass::CASH_ITEM // or BuffClass::ITEM? sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
};
player->addBuff(eCSB,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
},
[](EntityRef self, Buff* buff, time_t currTime) {
// no-op
},
&gumballBuff);
sock->sendPacket((void*)&respbuf, P_FE2CL_REP_PC_ITEM_USE_SUCC, resplen); sock->sendPacket((void*)&respbuf, P_FE2CL_REP_PC_ITEM_USE_SUCC, resplen);
// update inventory serverside // update inventory serverside
player->Inven[resp->iSlotNum] = resp->RemainItem; player->Inven[resp->iSlotNum] = resp->RemainItem;
}
static void useNanocomBooster(CNSocket* sock, CNPacketData* data) { std::pair<CNSocket*, int32_t> key = std::make_pair(sock, value1);
// Guard against using nanocom boosters in before and including 0104 time_t until = getTime() + (time_t)Nanos::SkillTable[144].durationTime[0] * 100;
// either path should be optimized by the compiler, effectively a no-op Eggs::EggBuffs[key] = until;
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) { static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) {
@@ -677,7 +546,7 @@ static void chestOpenHandler(CNSocket *sock, CNPacketData *data) {
// item giving packet // item giving packet
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward); const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE); assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
// we know it's only one trailing struct, so we can skip full validation // 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 uint8_t respbuf[resplen]; // not a variable length array, don't worry
@@ -719,7 +588,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 we failed to open a crate, at least give the player a gumball (suggested by Jade)
if (failing) { if (failing) {
item->sItem.iType = 7; item->sItem.iType = 7;
item->sItem.iID = GENERALITEM_GUMBALL_ADAPTIUM + Rand::rand(3); item->sItem.iID = 119 + Rand::rand(3);
item->sItem.iOpt = 1; item->sItem.iOpt = 1;
std::cout << "[WARN] Crate open failed, giving a Gumball..." << std::endl; std::cout << "[WARN] Crate open failed, giving a Gumball..." << std::endl;
@@ -753,89 +622,39 @@ Item* Items::getItemData(int32_t id, int32_t type) {
return nullptr; return nullptr;
} }
size_t Items::checkAndRemoveExpiredItems(CNSocket* sock, Player* player) { void Items::checkItemExpire(CNSocket* sock, Player* player) {
int32_t currentTime = getTimestamp(); if (player->toRemoveVehicle.eIL == 0 && player->toRemoveVehicle.iSlotNum == 0)
return;
// if there are expired items in bank just remove them silently /* prepare packet
if (settings::REMOVEEXPIREDITEMSFROMBANK) { * yes, this is a varadic packet, however analyzing client behavior and code
for (int i = 0; i < ABANK_COUNT; i++) { * it only checks takes the first item sent into account
if (player->Bank[i].iTimeLimit < currentTime && player->Bank[i].iTimeLimit != 0) { * yes, this is very stupid
memset(&player->Bank[i], 0, sizeof(sItemBase)); * therefore, we delete all but 1 expired vehicle while loading player
} * to delete the last one here so player gets a notification
} */
}
// 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; 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);
for (size_t i = 0; i < itemData.size(); i++) { packet->iItemListCount = 1;
auto itemToDeletePtr = (sTimeLimitItemDeleteInfo2CL*)( itemData->eIL = player->toRemoveVehicle.eIL;
respbuf + sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL) * i itemData->iSlotNum = player->toRemoveVehicle.iSlotNum;
);
itemToDeletePtr->eIL = itemData[i].eIL;
itemToDeletePtr->iSlotNum = itemData[i].iSlotNum;
packet->iItemListCount++;
}
sock->sendPacket((void*)&respbuf, P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, resplen); sock->sendPacket((void*)&respbuf, P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, resplen);
// delete items serverside and send unequip packets // delete serverside
for (size_t i = 0; i < itemData.size(); i++) { if (player->toRemoveVehicle.eIL == 0)
sItemBase* item = toRemove[i]; memset(&player->Equip[8], 0, sizeof(sItemBase));
memset(item, 0, sizeof(sItemBase)); else
memset(&player->Inven[player->toRemoveVehicle.iSlotNum], 0, sizeof(sItemBase));
// send item delete success packet player->toRemoveVehicle.eIL = 0;
// required for pre-academy builds player->toRemoveVehicle.iSlotNum = 0;
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) { void Items::setItemStats(Player* plr) {
@@ -882,73 +701,11 @@ static void getMobDrop(sItemBase* reward, const std::vector<int>& weights, const
reward->iID = crateIds[chosenIndex]; reward->iID = crateIds[chosenIndex];
} }
static int32_t calculateTaroReward(Player* plr, int baseAmount, int groupSize) { static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRoll& rolled) {
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); Player *plr = PlayerManager::getPlayer(sock);
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward); const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE); assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
// we know it's only one trailing struct, so we can skip full validation // 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 uint8_t respbuf[resplen]; // not a variable length array, don't worry
@@ -996,20 +753,43 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
MiscDropType& miscDropType = Items::MiscDropTypes[drop.miscDropTypeId]; MiscDropType& miscDropType = Items::MiscDropTypes[drop.miscDropTypeId];
if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) { if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) {
int32_t taros = calculateTaroReward(plr, miscDropType.taroAmount, groupSize); plr->money += miscDropType.taroAmount;
plr->addCapped(CappedValueType::TAROS, taros); // money nano boost
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
plr->money += miscDropType.taroAmount * (5 + boost) / 25;
}
} }
if (rolled.fm % miscDropChance.fmDropChanceTotal < miscDropChance.fmDropChance) { 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 levelDifference = plr->level - mob->level;
int32_t fm = calculateFMReward(plr, miscDropType.fmAmount, levelDifference, groupSize); int fm = miscDropType.fmAmount;
plr->addCapped(CappedValueType::FUSIONMATTER, fm); if (levelDifference > 0)
Missions::updateFusionMatter(sock); fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0;
// scavenger nano boost
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
fm += fm * (5 + boost) / 25;
}
Missions::updateFusionMatter(sock, fm);
} }
if (rolled.potions % miscDropChance.potionDropChanceTotal < miscDropChance.potionDropChance) if (rolled.potions % miscDropChance.potionDropChanceTotal < miscDropChance.potionDropChance)
plr->addCapped(CappedValueType::BATTERY_N, miscDropType.potionAmount); plr->batteryN += miscDropType.potionAmount;
if (rolled.boosts % miscDropChance.boostDropChanceTotal < miscDropChance.boostDropChance) if (rolled.boosts % miscDropChance.boostDropChanceTotal < miscDropChance.boostDropChance)
plr->addCapped(CappedValueType::BATTERY_W, miscDropType.boostAmount); plr->batteryW += miscDropType.boostAmount;
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
if (plr->batteryN > 9999)
plr->batteryN = 9999;
// simple rewards // simple rewards
reward->m_iCandy = plr->money; reward->m_iCandy = plr->money;
@@ -1040,16 +820,16 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
} }
} }
void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled, int groupSize) { void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled) {
// sanity check // sanity check
if (Items::MobToDropMap.find(mob->type) == Items::MobToDropMap.end()) { if (Items::MobToDropMap.find(mob->appearanceData.iNPCType) == Items::MobToDropMap.end()) {
std::cout << "[WARN] Mob ID " << mob->type << " has no drops assigned" << std::endl; std::cout << "[WARN] Mob ID " << mob->appearanceData.iNPCType << " has no drops assigned" << std::endl;
return; return;
} }
// find mob drop id // find mob drop id
int mobDropId = Items::MobToDropMap[mob->type]; int mobDropId = Items::MobToDropMap[mob->appearanceData.iNPCType];
giveSingleDrop(sock, mob, mobDropId, rolled, groupSize); giveSingleDrop(sock, mob, mobDropId, rolled);
if (settings::EVENTMODE != 0) { if (settings::EVENTMODE != 0) {
// sanity check // sanity check
@@ -1060,13 +840,14 @@ void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const
// find mob drop id // find mob drop id
int eventMobDropId = Items::EventToDropMap[settings::EVENTMODE]; int eventMobDropId = Items::EventToDropMap[settings::EVENTMODE];
giveSingleDrop(sock, mob, eventMobDropId, eventRolled, groupSize); giveSingleDrop(sock, mob, eventMobDropId, eventRolled);
} }
} }
void Items::init() { void Items::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler);
// this one is for gumballs
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_USE, itemUseHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_USE, itemUseHandler);
// Bank // Bank
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BANK_OPEN, itemBankOpenHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BANK_OPEN, itemBankOpenHandler);

View File

@@ -1,13 +1,9 @@
#pragma once #pragma once
#include "core/Core.hpp" #include "servers/CNShardServer.hpp"
#include "Player.hpp" #include "Player.hpp"
#include "Rand.hpp"
#include "MobAI.hpp" #include "MobAI.hpp"
#include "Rand.hpp"
#include <vector>
#include <map>
struct CrocPotEntry { struct CrocPotEntry {
int multStats, multLooks; int multStats, multLooks;
@@ -113,11 +109,11 @@ namespace Items {
void init(); void init();
// mob drops // mob drops
void giveMobDrop(CNSocket *sock, Mob *mob, const DropRoll& rolled, const DropRoll& eventRolled, int groupSize); void giveMobDrop(CNSocket *sock, Mob *mob, const DropRoll& rolled, const DropRoll& eventRolled);
int findFreeSlot(Player *plr); int findFreeSlot(Player *plr);
Item* getItemData(int32_t id, int32_t type); Item* getItemData(int32_t id, int32_t type);
size_t checkAndRemoveExpiredItems(CNSocket* sock, Player* player); void checkItemExpire(CNSocket* sock, Player* player);
void setItemStats(Player* plr); void setItemStats(Player* plr);
void updateEquips(CNSocket* sock, Player* plr); void updateEquips(CNSocket* sock, Player* plr);

View File

@@ -1,10 +1,11 @@
#include "Missions.hpp"
#include "servers/CNShardServer.hpp" #include "servers/CNShardServer.hpp"
#include "Missions.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Items.hpp"
#include "Nanos.hpp" #include "Nanos.hpp"
#include "Items.hpp"
#include "Transport.hpp"
#include "string.h"
using namespace Missions; using namespace Missions;
@@ -12,20 +13,6 @@ std::map<int32_t, Reward*> Missions::Rewards;
std::map<int32_t, TaskData*> Missions::Tasks; std::map<int32_t, TaskData*> Missions::Tasks;
nlohmann::json Missions::AvatarGrowth[37]; 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) { static void saveMission(Player* player, int missionId) {
// sanity check missionID so we don't get exceptions // sanity check missionID so we don't get exceptions
if (missionId < 0 || missionId > 1023) { if (missionId < 0 || missionId > 1023) {
@@ -40,12 +27,12 @@ static void saveMission(Player* player, int missionId) {
} }
static bool isMissionCompleted(Player* player, int missionId) { static bool isMissionCompleted(Player* player, int missionId) {
int row = missionId / 64; int row = missionId / 64;
int column = missionId % 64; int column = missionId % 64;
return player->aQuestFlag[row] & (1ULL << column); return player->aQuestFlag[row] & (1ULL << column);
} }
int Missions::findQSlot(Player *plr, int id) { static int findQSlot(Player *plr, int id) {
int i; int i;
// two passes. we mustn't fail to find an existing stack. // two passes. we mustn't fail to find an existing stack.
@@ -65,7 +52,7 @@ int Missions::findQSlot(Player *plr, int id) {
static bool isQuestItemFull(CNSocket* sock, int itemId, int itemCount) { static bool isQuestItemFull(CNSocket* sock, int itemId, int itemCount) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
int slot = Missions::findQSlot(plr, itemId); int slot = findQSlot(plr, itemId);
if (slot == -1) { if (slot == -1) {
// this should never happen // this should never happen
std::cout << "[WARN] Player has no room for quest item!?" << std::endl; std::cout << "[WARN] Player has no room for quest item!?" << std::endl;
@@ -78,7 +65,7 @@ static bool isQuestItemFull(CNSocket* sock, int itemId, int itemCount) {
static void dropQuestItem(CNSocket *sock, int task, int count, int id, int mobid) { 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; 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); const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE); assert(resplen < CN_PACKET_BUFFER_SIZE);
// we know it's only one trailing struct, so we can skip full validation // we know it's only one trailing struct, so we can skip full validation
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
@@ -91,7 +78,7 @@ static void dropQuestItem(CNSocket *sock, int task, int count, int id, int mobid
memset(respbuf, 0, resplen); memset(respbuf, 0, resplen);
// find free quest item slot // find free quest item slot
int slot = Missions::findQSlot(plr, id); int slot = findQSlot(plr, id);
if (slot == -1) { if (slot == -1) {
// this should never happen // this should never happen
std::cout << "[WARN] Player has no room for quest item!?" << std::endl; std::cout << "[WARN] Player has no room for quest item!?" << std::endl;
@@ -162,26 +149,35 @@ static int giveMissionReward(CNSocket *sock, int task, int choice=0) {
} }
return -1; return -1;
} }
plr->Inven[slots[i]] = { 999, 999, 999, 0 }; // temp item; overwritten later plr->Inven[slots[i]] = { 999, 999, 999, 0 }; // temp item; overwritten later
} }
uint8_t respbuf[CN_PACKET_BODY_SIZE]; uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + nrewards * sizeof(sItemReward); size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + nrewards * sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE); assert(resplen < CN_PACKET_BUFFER_SIZE);
sP_FE2CL_REP_REWARD_ITEM *resp = (sP_FE2CL_REP_REWARD_ITEM *)respbuf; sP_FE2CL_REP_REWARD_ITEM *resp = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM)); sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
// don't forget to zero the buffer! // don't forget to zero the buffer!
memset(respbuf, 0, CN_PACKET_BODY_SIZE); memset(respbuf, 0, resplen);
// update player // update player
int32_t money = calculateTaroReward(plr, reward->money); plr->money += reward->money;
plr->addCapped(CappedValueType::TAROS, money); if (plr->iConditionBitFlag & CSB_BIT_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 fusionMatter = calculateFMReward(plr, reward->fusionmatter); if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) { // nano boost for fm
plr->addCapped(CappedValueType::FUSIONMATTER, fusionMatter); int boost = 0;
Missions::updateFusionMatter(sock); if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
updateFusionMatter(sock, reward->fusionmatter * (30 + boost) / 25);
} else
updateFusionMatter(sock, reward->fusionmatter);
// simple rewards // simple rewards
resp->m_iCandy = plr->money; resp->m_iCandy = plr->money;
@@ -223,18 +219,28 @@ static bool endTask(CNSocket *sock, int32_t taskNum, int choice=0) {
// ugly pointer/reference juggling for the sake of operator overloading... // ugly pointer/reference juggling for the sake of operator overloading...
TaskData& task = *Tasks[taskNum]; TaskData& task = *Tasks[taskNum];
// sanity check // update player
int i; int i;
bool found = false; bool found = false;
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) { for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == taskNum) { if (plr->tasks[i] == taskNum) {
found = true; found = true;
break; plr->tasks[i] = 0;
for (int j = 0; j < 3; j++) {
plr->RemainingNPCCount[i][j] = 0;
}
} }
} }
if (!found) if (!found) {
std::cout << "[WARN] Player tried to end task that isn't in journal?" << std::endl;
return false;
}
if (i == ACTIVE_MISSION_COUNT - 1 && plr->tasks[i] != 0) {
std::cout << "[WARN] Player completed non-active mission!?" << std::endl;
return false; return false;
}
// mission rewards // mission rewards
if (Rewards.find(taskNum) != Rewards.end()) { if (Rewards.find(taskNum) != Rewards.end()) {
@@ -243,23 +249,6 @@ static bool endTask(CNSocket *sock, int32_t taskNum, int choice=0) {
} }
// don't take away quest items if we haven't finished the quest // don't take away quest items if we haven't finished the quest
/*
* Update player's active mission data.
*
* This must be done after all early returns have passed, otherwise we
* risk introducing non-atomic changes. For example, failing to finish
* a mission due to not having any inventory space could delete the
* mission server-side; leading to a desync.
*/
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == taskNum) {
plr->tasks[i] = 0;
for (int j = 0; j < 3; j++) {
plr->RemainingNPCCount[i][j] = 0;
}
}
}
/* /*
* Give (or take away) quest items * Give (or take away) quest items
* *
@@ -302,13 +291,13 @@ bool Missions::startTask(Player* plr, int TaskID) {
TaskData& task = *Missions::Tasks[TaskID]; TaskData& task = *Missions::Tasks[TaskID];
if (task["m_iCTRReqLvMin"] > plr->level) { if (task["m_iCTRReqLvMin"] > plr->level) {
std::cout << "[WARN] Player tried to start a task above their level" << std::endl; std::cout << "[WARN] Player tried to start a task below their level" << std::endl;
return false; return false;
} }
if (isMissionCompleted(plr, (int)(task["m_iHMissionID"]) - 1)) { if (isMissionCompleted(plr, (int)(task["m_iHMissionID"]) - 1)) {
std::cout << "[WARN] Player tried to start an already completed mission" << std::endl; std::cout << "[WARN] Player tried to start an already completed mission" << std::endl;
return false; return false;
} }
// client freaks out if nano mission isn't sent first after relogging, so it's easiest to set it here // client freaks out if nano mission isn't sent first after relogging, so it's easiest to set it here
@@ -371,15 +360,15 @@ static void taskStart(CNSocket* sock, CNPacketData* data) {
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC)); sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
// if escort task, assign matching paths to all nearby NPCs // if escort task, assign matching paths to all nearby NPCs
if (task["m_iHTaskType"] == (int)eTaskTypeProperty::EscortDefence) { if (task["m_iHTaskType"] == 6) {
for (ChunkPos& chunkPos : Chunking::getChunksInMap(plr->instanceID)) { // check all NPCs in the instance for (ChunkPos& chunkPos : Chunking::getChunksInMap(plr->instanceID)) { // check all NPCs in the instance
Chunk* chunk = Chunking::chunks[chunkPos]; Chunk* chunk = Chunking::chunks[chunkPos];
for (EntityRef ref : chunk->entities) { for (EntityRef ref : chunk->entities) {
if (ref.kind != EntityKind::PLAYER) { if (ref.type != EntityType::PLAYER) {
BaseNPC* npc = (BaseNPC*)ref.getEntity(); BaseNPC* npc = (BaseNPC*)ref.getEntity();
NPCPath* path = Transport::findApplicablePath(npc->id, npc->type, missionData->iTaskNum); NPCPath* path = Transport::findApplicablePath(npc->appearanceData.iNPC_ID, npc->appearanceData.iNPCType, missionData->iTaskNum);
if (path != nullptr) { if (path != nullptr) {
Transport::constructPathNPC(npc->id, path); Transport::constructPathNPC(npc->appearanceData.iNPC_ID, path);
return; return;
} }
} }
@@ -391,44 +380,42 @@ static void taskStart(CNSocket* sock, CNPacketData* data) {
static void taskEnd(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; sP_CL2FE_REQ_PC_TASK_END* missionData = (sP_CL2FE_REQ_PC_TASK_END*)data->buf;
if (Missions::Tasks.find(missionData->iTaskNum) == Missions::Tasks.end()) // failed timed missions give an iNPC_ID of 0
return; if (missionData->iNPC_ID == 0) {
TaskData* task = Missions::Tasks[missionData->iTaskNum];
TaskData* task = Missions::Tasks[missionData->iTaskNum]; if (task->task["m_iSTGrantTimer"] > 0) { // its a timed mission
Player* plr = PlayerManager::getPlayer(sock);
// handle timed mission failure /*
if (task->task["m_iSTGrantTimer"] > 0 && missionData->iNPC_ID == 0) { * Enemy killing missions
Player* plr = PlayerManager::getPlayer(sock); * this is gross and should be cleaned up later
* once we comb over mission logic more throughly
/* */
* Enemy killing missions bool mobsAreKilled = false;
* this is gross and should be cleaned up later if (task->task["m_iHTaskType"] == 5) {
* once we comb over mission logic more throughly mobsAreKilled = true;
*/ for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
bool mobsAreKilled = false; if (plr->tasks[i] == missionData->iTaskNum) {
if (task->task["m_iHTaskType"] == (int)eTaskTypeProperty::Defeat) { for (int j = 0; j < 3; j++) {
mobsAreKilled = true; if (plr->RemainingNPCCount[i][j] > 0) {
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) { mobsAreKilled = false;
if (plr->tasks[i] == missionData->iTaskNum) { break;
for (int j = 0; j < 3; j++) { }
if (plr->RemainingNPCCount[i][j] > 0) {
mobsAreKilled = false;
break;
} }
} }
} }
} }
}
if (!mobsAreKilled) { if (!mobsAreKilled) {
int failTaskID = task->task["m_iFOutgoingTask"];
if (failTaskID != 0) { int failTaskID = task->task["m_iFOutgoingTask"];
Missions::quitTask(sock, missionData->iTaskNum, false); if (failTaskID != 0) {
Missions::quitTask(sock, missionData->iTaskNum, false);
for (int i = 0; i < 6; i++)
if (plr->tasks[i] == missionData->iTaskNum) for (int i = 0; i < 6; i++)
plr->tasks[i] = failTaskID; if (plr->tasks[i] == missionData->iTaskNum)
return; plr->tasks[i] = failTaskID;
return;
}
} }
} }
} }
@@ -499,12 +486,6 @@ void Missions::quitTask(CNSocket* sock, int32_t taskNum, bool manual) {
memset(&plr->QInven[j], 0, sizeof(sItemBase)); memset(&plr->QInven[j], 0, sizeof(sItemBase));
} }
} else { } else {
for (i = 0; i < 3; i++) {
if (task["m_iFItemID"][i] == 0)
continue;
dropQuestItem(sock, taskNum, task["m_iFItemNumNeeded"][i], task["m_iFItemID"][i], 0);
}
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_FAIL, failResp); INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_FAIL, failResp);
failResp.iErrorCode = 1; failResp.iErrorCode = 1;
failResp.iTaskNum = taskNum; failResp.iTaskNum = taskNum;
@@ -516,13 +497,17 @@ 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)); sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_STOP_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_STOP_SUCC));
} }
void Missions::updateFusionMatter(CNSocket* sock) { void Missions::updateFusionMatter(CNSocket* sock, int fusion) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
plr->fusionmatter += fusion;
// there's a much lower FM cap in the Future // there's a much lower FM cap in the Future
int fmCap = AvatarGrowth[plr->level]["m_iFMLimit"]; int fmCap = AvatarGrowth[plr->level]["m_iFMLimit"];
if (plr->fusionmatter > fmCap) if (plr->fusionmatter > fmCap)
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 // don't run nano mission logic at level 36
if (plr->level >= 36) if (plr->level >= 36)
@@ -552,7 +537,10 @@ void Missions::updateFusionMatter(CNSocket* sock) {
response.iTaskNum = AvatarGrowth[plr->level]["m_iNanoQuestTaskID"]; 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)); sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
#else #else
plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"]); if (plr->level >= 36)
return;
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
plr->level++; plr->level++;
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response); INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response);
@@ -570,7 +558,7 @@ void Missions::updateFusionMatter(CNSocket* sock) {
PlayerManager::sendToViewable(sock, (void*)&bcast, P_FE2CL_PC_EVENT, sizeof(sP_FE2CL_PC_EVENT)); PlayerManager::sendToViewable(sock, (void*)&bcast, P_FE2CL_PC_EVENT, sizeof(sP_FE2CL_PC_EVENT));
} }
void Missions::mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls) { void Missions::mobKilled(CNSocket *sock, int mobid, int rolledQItem) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
bool missionmob = false; bool missionmob = false;
@@ -593,29 +581,12 @@ void Missions::mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls) {
plr->RemainingNPCCount[i][j]--; plr->RemainingNPCCount[i][j]--;
} }
} }
// drop quest item // drop quest item
if (task["m_iCSUItemNumNeeded"][j] != 0 && !isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j]) ) { if (task["m_iCSUItemNumNeeded"][j] != 0 && !isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j]) ) {
bool drop = rolls[plr->tasks[i]] % 100 < task["m_iSTItemDropRate"][j]; bool drop = rolledQItem % 100 < task["m_iSTItemDropRate"][j];
if (drop) { if (drop) {
// XXX: are CSUItemID and CSTItemID the same?
dropQuestItem(sock, plr->tasks[i], 1, task["m_iCSUItemID"][j], mobid); dropQuestItem(sock, plr->tasks[i], 1, task["m_iCSUItemID"][j], mobid);
/*
* Workaround: The client has a bug where it only sends a TASK_END request
* for the first task of multiple that met their quest item requirements
* at the same time. We deal with this by sending TASK_END response packets
* proactively and then silently ignoring the extra TASK_END requests it
* sends afterwards.
*/
if (isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j])) {
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, end);
end.iTaskNum = plr->tasks[i];
if (!endTask(sock, plr->tasks[i]))
continue;
sock->sendPacket(end, P_FE2CL_REP_PC_TASK_END_SUCC);
}
} else { } else {
// fail to drop (itemID == 0) // fail to drop (itemID == 0)
dropQuestItem(sock, plr->tasks[i], 1, 0, mobid); dropQuestItem(sock, plr->tasks[i], 1, 0, mobid);

View File

@@ -1,11 +1,9 @@
#pragma once #pragma once
#include "core/Core.hpp" #include "servers/CNShardServer.hpp"
#include "JSON.hpp"
#include "Player.hpp" #include "Player.hpp"
#include <map> #include "JSON.hpp"
struct Reward { struct Reward {
int32_t id; int32_t id;
@@ -42,14 +40,13 @@ namespace Missions {
extern std::map<int32_t, TaskData*> Tasks; extern std::map<int32_t, TaskData*> Tasks;
extern nlohmann::json AvatarGrowth[37]; extern nlohmann::json AvatarGrowth[37];
void init(); void init();
int findQSlot(Player *plr, int id);
bool startTask(Player* plr, int TaskID); bool startTask(Player* plr, int TaskID);
// checks if player doesn't have n/n quest items // checks if player doesn't have n/n quest items
void updateFusionMatter(CNSocket* sock); void updateFusionMatter(CNSocket* sock, int fusion);
void mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls); void mobKilled(CNSocket *sock, int mobid, int rolledQItem);
void quitTask(CNSocket* sock, int32_t taskNum, bool manual); void quitTask(CNSocket* sock, int32_t taskNum, bool manual);

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,26 @@
#pragma once #pragma once
#include "core/Core.hpp" #include "core/Core.hpp"
#include "JSON.hpp" #include "NPCManager.hpp"
#include "Entities.hpp" enum class MobState {
INACTIVE,
#include <unordered_map> ROAMING,
#include <string> COMBAT,
RETREAT,
DEAD
};
namespace MobAI { namespace MobAI {
void deadStep(CombatNPC* self, time_t currTime); // needs to be declared before Mob's constructor
void combatStep(CombatNPC* self, time_t currTime); void step(CombatNPC*, time_t);
void roamingStep(CombatNPC* self, time_t currTime); };
void retreatStep(CombatNPC* self, time_t currTime);
void onRoamStart(CombatNPC* self, EntityRef src);
void onCombatStart(CombatNPC* self, EntityRef src);
void onRetreat(CombatNPC* self, EntityRef src);
void onDeath(CombatNPC* self, EntityRef src);
}
struct Mob : public CombatNPC { struct Mob : public CombatNPC {
// general
MobState state = MobState::INACTIVE;
std::unordered_map<int32_t,time_t> unbuffTimes = {};
// dead // dead
time_t killedTime = 0; time_t killedTime = 0;
@@ -47,13 +47,16 @@ struct Mob : public CombatNPC {
int offsetX = 0, offsetY = 0; int offsetX = 0, offsetY = 0;
int groupMember[4] = {}; int groupMember[4] = {};
// for optimizing away AI in empty chunks
int playersInView = 0;
// temporary; until we're sure what's what // temporary; until we're sure what's what
nlohmann::json data = {}; nlohmann::json data = {};
Mob(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id) Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(spawnX, spawnY, spawnZ, angle, iID, t, id, d["m_iHP"]), : CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]),
sightRange(d["m_iSightRange"]) { sightRange(d["m_iSightRange"]) {
state = AIState::ROAMING; state = MobState::ROAMING;
data = d; data = d;
@@ -62,28 +65,20 @@ struct Mob : public CombatNPC {
idleRange = (int)data["m_iIdleRange"]; idleRange = (int)data["m_iIdleRange"];
level = data["m_iNpcLevel"]; level = data["m_iNpcLevel"];
roamX = spawnX; roamX = spawnX = x;
roamY = spawnY; roamY = spawnY = y;
roamZ = spawnZ; roamZ = spawnZ = z;
offsetX = 0; offsetX = 0;
offsetY = 0; offsetY = 0;
appearanceData.iConditionBitFlag = 0;
// NOTE: there appear to be discrepancies in the dump // NOTE: there appear to be discrepancies in the dump
hp = maxHealth; appearanceData.iHP = maxHealth;
kind = EntityKind::MOB; type = EntityType::MOB;
_stepAI = MobAI::step;
// AI
stateHandlers[AIState::DEAD] = MobAI::deadStep;
stateHandlers[AIState::COMBAT] = MobAI::combatStep;
stateHandlers[AIState::ROAMING] = MobAI::roamingStep;
stateHandlers[AIState::RETREAT] = MobAI::retreatStep;
transitionHandlers[AIState::DEAD] = MobAI::onDeath;
transitionHandlers[AIState::COMBAT] = MobAI::onCombatStart;
transitionHandlers[AIState::ROAMING] = MobAI::onRoamStart;
transitionHandlers[AIState::RETREAT] = MobAI::onRetreat;
} }
// constructor for /summon // constructor for /summon
@@ -94,9 +89,6 @@ struct Mob : public CombatNPC {
~Mob() {} ~Mob() {}
virtual int takeDamage(EntityRef src, int amt) override;
virtual void step(time_t currTime) override;
auto operator[](std::string s) { auto operator[](std::string s) {
return data[s]; return data[s];
} }
@@ -111,4 +103,5 @@ namespace MobAI {
void clearDebuff(Mob *mob); void clearDebuff(Mob *mob);
void followToCombat(Mob *mob); void followToCombat(Mob *mob);
void groupRetreat(Mob *mob); void groupRetreat(Mob *mob);
void enterCombat(CNSocket *sock, Mob *mob);
} }

4
src/NPC.hpp Normal file
View File

@@ -0,0 +1,4 @@
#pragma once
#include "Chunking.hpp"
#include "Entities.hpp"

View File

@@ -1,8 +1,4 @@
#include "NPCManager.hpp" #include "NPCManager.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp" #include "Items.hpp"
#include "settings.hpp" #include "settings.hpp"
#include "Combat.hpp" #include "Combat.hpp"
@@ -24,6 +20,8 @@
#include <assert.h> #include <assert.h>
#include <limits.h> #include <limits.h>
#include "JSON.hpp"
using namespace NPCManager; using namespace NPCManager;
std::unordered_map<int32_t, BaseNPC*> NPCManager::NPCs; std::unordered_map<int32_t, BaseNPC*> NPCManager::NPCs;
@@ -69,7 +67,7 @@ void NPCManager::destroyNPC(int32_t id) {
void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I, int angle) { void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I, int angle) {
BaseNPC* npc = NPCs[id]; BaseNPC* npc = NPCs[id];
npc->angle = angle; npc->appearanceData.iAngle = angle;
ChunkPos oldChunk = npc->chunkPos; ChunkPos oldChunk = npc->chunkPos;
ChunkPos newChunk = Chunking::chunkPosAt(X, Y, I); ChunkPos newChunk = Chunking::chunkPosAt(X, Y, I);
npc->x = X; npc->x = X;
@@ -81,11 +79,11 @@ void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I,
Chunking::updateEntityChunk({id}, oldChunk, newChunk); Chunking::updateEntityChunk({id}, oldChunk, newChunk);
} }
void NPCManager::sendToViewable(Entity *npc, void *buf, uint32_t type, size_t size) { void NPCManager::sendToViewable(BaseNPC *npc, void *buf, uint32_t type, size_t size) {
for (auto it = npc->viewableChunks.begin(); it != npc->viewableChunks.end(); it++) { for (auto it = npc->viewableChunks.begin(); it != npc->viewableChunks.end(); it++) {
Chunk* chunk = *it; Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) { for (const EntityRef& ref : chunk->entities) {
if (ref.kind == EntityKind::PLAYER) if (ref.type == EntityType::PLAYER)
ref.sock->sendPacket(buf, type, size); ref.sock->sendPacket(buf, type, size);
} }
} }
@@ -94,49 +92,20 @@ void NPCManager::sendToViewable(Entity *npc, void *buf, uint32_t type, size_t si
static void npcBarkHandler(CNSocket* sock, CNPacketData* data) { static void npcBarkHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf; sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf;
int taskID = req->iMissionTaskID; // get bark IDs from task data
// ignore req->iNPC_ID as it is often fixated on a single npc in the region TaskData* td = Missions::Tasks[req->iMissionTaskID];
std::vector<int> barks;
if (Missions::Tasks.find(taskID) == Missions::Tasks.end()) { for (int i = 0; i < 4; i++) {
std::cout << "mission task not found: " << taskID << std::endl; if (td->task["m_iHBarkerTextID"][i] != 0) // non-zeroes only
return; barks.push_back(td->task["m_iHBarkerTextID"][i]);
} }
TaskData* td = Missions::Tasks[taskID]; if (barks.empty())
auto& barks = td->task["m_iHBarkerTextID"]; return; // no barks
Player* plr = PlayerManager::getPlayer(sock);
std::vector<std::pair<int32_t, int32_t>> npcLines;
for (Chunk* chunk : plr->viewableChunks) {
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
if (ent->kind != EntityKind::SIMPLE_NPC)
continue;
BaseNPC* npc = (BaseNPC*)ent->getEntity();
if (npc->type < 0 || npc->type >= NPCData.size())
continue; // npc unknown ?!
int barkType = NPCData[npc->type]["m_iBarkerType"];
if (barkType < 1 || barkType > 4)
continue; // no barks
int barkID = barks[barkType - 1];
if (barkID == 0)
continue; // no barks
npcLines.push_back(std::make_pair(npc->id, barkID));
}
}
if (npcLines.size() == 0)
return; // totally no barks
auto& [npcID, missionStringID] = npcLines[Rand::rand(npcLines.size())];
INITSTRUCT(sP_FE2CL_REP_BARKER, resp); INITSTRUCT(sP_FE2CL_REP_BARKER, resp);
resp.iNPC_ID = npcID; resp.iNPC_ID = req->iNPC_ID;
resp.iMissionStringID = missionStringID; resp.iMissionStringID = barks[Rand::rand(barks.size())];
sock->sendPacket(resp, P_FE2CL_REP_BARKER); sock->sendPacket(resp, P_FE2CL_REP_BARKER);
} }
@@ -151,20 +120,22 @@ static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
} }
// type must already be checked and updateNPCPosition() must be called on the result // type must already be checked and updateNPCPosition() must be called on the result
BaseNPC *NPCManager::summonNPC(int spawnX, int spawnY, int spawnZ, uint64_t instance, int type, bool respawn, bool baseInstance) { BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) {
uint64_t inst = baseInstance ? MAPNUM(instance) : instance; uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
#define EXTRA_HEIGHT 0
//assert(nextId < INT32_MAX);
int id = nextId--; int id = nextId--;
int team = NPCData[type]["m_iTeam"]; int team = NPCData[type]["m_iTeam"];
BaseNPC *npc = nullptr; BaseNPC *npc = nullptr;
if (team == 2) { if (team == 2) {
npc = new Mob(spawnX, spawnY, spawnZ, inst, type, NPCData[type], id); npc = new Mob(x, y, z + EXTRA_HEIGHT, inst, type, NPCData[type], id);
// re-enable respawning, if desired // re-enable respawning, if desired
((Mob*)npc)->summoned = !respawn; ((Mob*)npc)->summoned = !respawn;
} else } else
npc = new BaseNPC(0, inst, type, id); npc = new BaseNPC(x, y, z + EXTRA_HEIGHT, 0, inst, type, id);
NPCs[id] = npc; NPCs[id] = npc;
@@ -183,7 +154,7 @@ static void npcSummonHandler(CNSocket* sock, CNPacketData* data) {
for (int i = 0; i < req->iNPCCnt; i++) { for (int i = 0; i < req->iNPCCnt; i++) {
BaseNPC *npc = summonNPC(plr->x, plr->y, plr->z, plr->instanceID, req->iNPCType); BaseNPC *npc = summonNPC(plr->x, plr->y, plr->z, plr->instanceID, req->iNPCType);
updateNPCPosition(npc->id, plr->x, plr->y, plr->z, plr->instanceID, 0); updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, 0);
} }
} }
@@ -212,12 +183,9 @@ static void handleWarp(CNSocket* sock, int32_t warpId) {
if (Warps[warpId].isInstance) { if (Warps[warpId].isInstance) {
uint64_t instanceID = Warps[warpId].instanceID; uint64_t instanceID = Warps[warpId].instanceID;
Player* leader = plr;
if (plr->group != nullptr) leader = PlayerManager::getPlayer(plr->group->filter(EntityKind::PLAYER)[0].sock);
// if warp requires you to be on a mission, it's gotta be a unique instance // if warp requires you to be on a mission, it's gotta be a unique instance
if (Warps[warpId].limitTaskID != 0 || instanceID == 14) { // 14 is a special case for the Time Lab if (Warps[warpId].limitTaskID != 0 || instanceID == 14) { // 14 is a special case for the Time Lab
instanceID += ((uint64_t)leader->iID << 32); // upper 32 bits are leader ID instanceID += ((uint64_t)plr->iIDGroup << 32); // upper 32 bits are leader ID
Chunking::createInstance(instanceID); Chunking::createInstance(instanceID);
// save Lair entrance coords as a pseudo-Resurrect 'Em // save Lair entrance coords as a pseudo-Resurrect 'Em
@@ -227,13 +195,14 @@ static void handleWarp(CNSocket* sock, int32_t warpId) {
plr->recallInstance = instanceID; plr->recallInstance = instanceID;
} }
if (plr->group == nullptr) if (plr->iID == plr->iIDGroup && plr->groupCnt == 1)
PlayerManager::sendPlayerTo(sock, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID); PlayerManager::sendPlayerTo(sock, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
else { else {
auto players = plr->group->filter(EntityKind::PLAYER); Player* leaderPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
for (int i = 0; i < players.size(); i++) {
CNSocket* sockTo = players[i].sock; for (int i = 0; i < leaderPlr->groupCnt; i++) {
Player* otherPlr = PlayerManager::getPlayer(sockTo); Player* otherPlr = PlayerManager::getPlayerFromID(leaderPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(leaderPlr->groupIDs[i]);
if (otherPlr == nullptr || sockTo == nullptr) if (otherPlr == nullptr || sockTo == nullptr)
continue; continue;
@@ -277,7 +246,8 @@ static void handleWarp(CNSocket* sock, int32_t warpId) {
Missions::failInstancedMissions(sock); // fail any instanced missions Missions::failInstancedMissions(sock); // fail any instanced missions
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC); sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC);
PlayerManager::updatePlayerPositionForWarp(sock, resp.iX, resp.iY, resp.iZ, INSTANCE_OVERWORLD); Chunking::updateEntityChunk({sock}, plr->chunkPos, std::make_tuple(0, 0, 0)); // force player to reload chunks
PlayerManager::updatePlayerPosition(sock, resp.iX, resp.iY, resp.iZ, INSTANCE_OVERWORLD, plr->angle);
// remove the player's ongoing race, if any // remove the player's ongoing race, if any
if (Racing::EPRaces.find(sock) != Racing::EPRaces.end()) if (Racing::EPRaces.find(sock) != Racing::EPRaces.end())
@@ -307,7 +277,7 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
for (auto c = chunks->begin(); c != chunks->end(); c++) { // haha get it for (auto c = chunks->begin(); c != chunks->end(); c++) { // haha get it
Chunk* chunk = *c; Chunk* chunk = *c;
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) { for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
if (ent->kind == EntityKind::PLAYER) if (ent->type == EntityType::PLAYER)
continue; continue;
BaseNPC* npcTemp = (BaseNPC*)ent->getEntity(); BaseNPC* npcTemp = (BaseNPC*)ent->getEntity();
@@ -322,55 +292,57 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
return npc; return npc;
} }
// TODO: Move this to separate file in ai/ subdir when implementing more events // TODO: Move this to MobAI, possibly
#pragma region NPCEvents #pragma region NPCEvents
// summon right arm and stage 2 body // summon right arm and stage 2 body
static void lordFuseStageTwo(CombatNPC *npc) { static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc; // adaptium, stun Mob *oldbody = (Mob*)npc; // adaptium, stun
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage two" << std::endl; std::cout << "Lord Fuse stage two" << std::endl;
// Fuse doesn't move // Fuse doesn't move; spawnX, etc. is shorter to write than *appearanceData*
// Blastons, Heal // Blastons, Heal
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2467); Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2467);
newbody->angle = oldbody->angle; newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ, NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
oldbody->instanceID, oldbody->angle); plr->instanceID, oldbody->appearanceData.iAngle);
// right arm, Adaptium, Stun // right arm, Adaptium, Stun
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, oldbody->instanceID, 2469); Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX - 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2469);
arm->angle = oldbody->angle; arm->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ, NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ,
oldbody->instanceID, oldbody->angle); plr->instanceID, oldbody->appearanceData.iAngle);
} }
// summon left arm and stage 3 body // summon left arm and stage 3 body
static void lordFuseStageThree(CombatNPC *npc) { static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc; Mob *oldbody = (Mob*)npc;
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage three" << std::endl; std::cout << "Lord Fuse stage three" << std::endl;
// Cosmix, Damage Point // Cosmix, Damage Point
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2468); Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2468);
newbody->angle = oldbody->angle; newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ, NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
newbody->instanceID, oldbody->angle); plr->instanceID, oldbody->appearanceData.iAngle);
// Blastons, Heal // Blastons, Heal
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, oldbody->instanceID, 2470); Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX + 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2470);
arm->angle = oldbody->angle; arm->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ, NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ,
arm->instanceID, oldbody->angle); plr->instanceID, oldbody->appearanceData.iAngle);
} }
std::vector<NPCEvent> NPCManager::NPCEvents = { std::vector<NPCEvent> NPCManager::NPCEvents = {
NPCEvent(2466, AIState::DEAD, lordFuseStageTwo), NPCEvent(2466, ON_KILLED, lordFuseStageTwo),
NPCEvent(2467, AIState::DEAD, lordFuseStageThree), NPCEvent(2467, ON_KILLED, lordFuseStageThree),
}; };
#pragma endregion NPCEvents #pragma endregion NPCEvents
@@ -381,11 +353,11 @@ void NPCManager::queueNPCRemoval(int32_t id) {
static void step(CNServer *serv, time_t currTime) { static void step(CNServer *serv, time_t currTime) {
for (auto& pair : NPCs) { for (auto& pair : NPCs) {
if (pair.second->kind != EntityKind::COMBAT_NPC && pair.second->kind != EntityKind::MOB) if (pair.second->type != EntityType::COMBAT_NPC && pair.second->type != EntityType::MOB)
continue; continue;
auto npc = (CombatNPC*)pair.second; auto npc = (CombatNPC*)pair.second;
npc->step(currTime); npc->stepAI(currTime);
} }
// deallocate all NPCs queued for removal // deallocate all NPCs queued for removal
@@ -402,5 +374,5 @@ void NPCManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler);
REGISTER_SHARD_TIMER(step, MS_PER_COMBAT_TICK); REGISTER_SHARD_TIMER(step, 200);
} }

View File

@@ -1,30 +1,36 @@
#pragma once #pragma once
#include "core/Core.hpp" #include "core/Core.hpp"
#include "JSON.hpp" #include "PlayerManager.hpp"
#include "NPC.hpp"
#include "Transport.hpp" #include "Transport.hpp"
#include "Chunking.hpp"
#include "Entities.hpp" #include "JSON.hpp"
#include <map> #include <map>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <set>
#define RESURRECT_HEIGHT 400 #define RESURRECT_HEIGHT 400
typedef void (*NPCEventHandler)(CombatNPC*); enum Trigger {
ON_KILLED,
ON_COMBAT
};
typedef void (*NPCEventHandler)(CNSocket*, BaseNPC*);
struct NPCEvent { struct NPCEvent {
int32_t npcType; int32_t npcType;
AIState triggerState; int trigger;
NPCEventHandler handler; NPCEventHandler handler;
NPCEvent(int32_t t, AIState tr, NPCEventHandler hndlr) NPCEvent(int32_t t, int tr, NPCEventHandler hndlr)
: npcType(t), triggerState(tr), handler(hndlr) {} : npcType(t), trigger(tr), handler(hndlr) {}
}; };
struct WarpLocation;
namespace NPCManager { namespace NPCManager {
extern std::unordered_map<int32_t, BaseNPC*> NPCs; extern std::unordered_map<int32_t, BaseNPC*> NPCs;
extern std::map<int32_t, WarpLocation> Warps; extern std::map<int32_t, WarpLocation> Warps;
@@ -38,7 +44,7 @@ namespace NPCManager {
void destroyNPC(int32_t); void destroyNPC(int32_t);
void updateNPCPosition(int32_t, int X, int Y, int Z, uint64_t I, int angle); void updateNPCPosition(int32_t, int X, int Y, int Z, uint64_t I, int angle);
void sendToViewable(Entity* npc, void* buf, uint32_t type, size_t size); void sendToViewable(BaseNPC* npc, void* buf, uint32_t type, size_t size);
BaseNPC *summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn=false, bool baseInstance=false); BaseNPC *summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn=false, bool baseInstance=false);

View File

@@ -1,11 +1,11 @@
#include "Nanos.hpp"
#include "servers/CNShardServer.hpp" #include "servers/CNShardServer.hpp"
#include "Nanos.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Missions.hpp"
#include "Abilities.hpp"
#include "NPCManager.hpp" #include "NPCManager.hpp"
#include "Combat.hpp"
#include "Missions.hpp"
#include "Groups.hpp"
#include "Abilities.hpp"
#include <cmath> #include <cmath>
@@ -33,10 +33,8 @@ void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm)
*/ */
plr->level = level; plr->level = level;
if (spendfm) { if (spendfm)
plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]); Missions::updateFusionMatter(sock, -(int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]);
Missions::updateFusionMatter(sock);
}
#endif #endif
// Send to client // Send to client
@@ -84,21 +82,40 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
if (slot != -1 && plr->Nanos[nanoID].iSkillID == 0) if (slot != -1 && plr->Nanos[nanoID].iSkillID == 0)
return; // prevent powerless nanos from summoning return; // prevent powerless nanos from summoning
plr->nanoDrainRate = 0;
int16_t skillID = plr->Nanos[plr->activeNano].iSkillID;
// passive nano unbuffing
if (SkillTable[skillID].drainType == 2) {
std::vector<int> targetData = findTargets(plr, skillID);
for (auto& pwr : NanoPowers)
if (pwr.skillType == SkillTable[skillID].skillType)
nanoUnbuff(sock, targetData, pwr.bitFlag, pwr.timeBuffID, 0,(SkillTable[skillID].targetType == 3));
}
if (nanoID >= NANO_COUNT || nanoID < 0) if (nanoID >= NANO_COUNT || nanoID < 0)
return; // sanity check return; // sanity check
plr->activeNano = nanoID; plr->activeNano = nanoID;
sNano& nano = plr->Nanos[nanoID]; skillID = plr->Nanos[nanoID].iSkillID;
SkillData* skill = Abilities::SkillTable.count(nano.iSkillID) > 0 // passive nano buffing
? &Abilities::SkillTable[nano.iSkillID] : nullptr; if (SkillTable[skillID].drainType == 2) {
if (slot != -1 && skill != nullptr && skill->drainType == SkillDrainType::PASSIVE) { std::vector<int> targetData = findTargets(plr, skillID);
// passive buff effect
resp.eCSTB___Add = 1; int boost = 0;
ICombatant* src = dynamic_cast<ICombatant*>(plr); if (getNanoBoost(plr))
int32_t targets[] = { plr->iID }; boost = 1;
std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets);
Abilities::useNanoSkill(sock, skill, nano, affectedCombatants); for (auto& pwr : NanoPowers) {
if (pwr.skillType == SkillTable[skillID].skillType) {
resp.eCSTB___Add = 1; // the part that makes nano go ZOOMAZOOM
plr->nanoDrainRate = SkillTable[skillID].batteryUse[boost*3];
pwr.handle(sock, targetData, nanoID, skillID, 0, SkillTable[skillID].powerIntensity[boost]);
}
}
} }
if (!silent) // silent nano death but only for the summoning player if (!silent) // silent nano death but only for the summoning player
@@ -107,12 +124,12 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
// Send to other players, these players can't handle silent nano deaths so this packet needs to be sent. // Send to other players, these players can't handle silent nano deaths so this packet needs to be sent.
INITSTRUCT(sP_FE2CL_NANO_ACTIVE, pkt1); INITSTRUCT(sP_FE2CL_NANO_ACTIVE, pkt1);
pkt1.iPC_ID = plr->iID; pkt1.iPC_ID = plr->iID;
pkt1.Nano = nano; pkt1.Nano = plr->Nanos[nanoID];
PlayerManager::sendToViewable(sock, pkt1, P_FE2CL_NANO_ACTIVE); PlayerManager::sendToViewable(sock, pkt1, P_FE2CL_NANO_ACTIVE);
} }
static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) { static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
if (skill == nullptr || skill->iNanoID >= NANO_COUNT || skill->iNanoID < 0) if (skill->iNanoID >= NANO_COUNT)
return; return;
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
@@ -145,7 +162,7 @@ static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
return; return;
#endif #endif
plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"]); plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"];
int reqItemCount = NanoTunings[skill->iTuneID].reqItemCount; int reqItemCount = NanoTunings[skill->iTuneID].reqItemCount;
int reqItemID = NanoTunings[skill->iTuneID].reqItems; int reqItemID = NanoTunings[skill->iTuneID].reqItems;
@@ -192,9 +209,9 @@ int Nanos::nanoStyle(int nanoID) {
} }
bool Nanos::getNanoBoost(Player* plr) { bool Nanos::getNanoBoost(Player* plr) {
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
if (plr->equippedNanos[i] == plr->activeNano) if (plr->equippedNanos[i] == plr->activeNano)
if (plr->hasBuff(ECSB_STIMPAKSLOT1 + i)) if (plr->iConditionBitFlag & (CSB_BIT_STIMPAKSLOT1 << i))
return true; return true;
return false; return false;
} }
@@ -209,15 +226,24 @@ static void nanoEquipHandler(CNSocket* sock, CNPacketData* data) {
if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0) if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0)
return; return;
if (nano->iNanoID < 0 || nano->iNanoID >= NANO_COUNT)
return;
resp.iNanoID = nano->iNanoID; resp.iNanoID = nano->iNanoID;
resp.iNanoSlotNum = nano->iNanoSlotNum; resp.iNanoSlotNum = nano->iNanoSlotNum;
// Update player // Update player
plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID; plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID;
// Unbuff gumballs
int value1 = CSB_BIT_STIMPAKSLOT1 << nano->iNanoSlotNum;
if (plr->iConditionBitFlag & value1) {
int value2 = ECSB_STIMPAKSLOT1 + nano->iNanoSlotNum;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = value2; // eCharStatusTimeBuffID
pkt.eTBU = 2; // eTimeBuffUpdate
pkt.eTBT = 1; // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = plr->iConditionBitFlag &= ~value1;
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
}
// unsummon nano if replaced // unsummon nano if replaced
if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum]) if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum])
summonNano(sock, -1); summonNano(sock, -1);
@@ -260,26 +286,28 @@ static void nanoSummonHandler(CNSocket* sock, CNPacketData* data) {
static void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) { static void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
// validate request check int16_t nanoID = plr->activeNano;
sP_CL2FE_REQ_NANO_SKILL_USE* pkt = (sP_CL2FE_REQ_NANO_SKILL_USE*)data->buf; int16_t skillID = plr->Nanos[nanoID].iSkillID;
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_NANO_SKILL_USE), pkt->iTargetCnt, sizeof(int32_t), data->size)) {
std::cout << "[WARN] bad sP_CL2FE_REQ_NANO_SKILL_USE packet size" << std::endl;
return;
}
sNano& nano = plr->Nanos[plr->activeNano];
int16_t skillID = nano.iSkillID;
SkillData* skillData = &Abilities::SkillTable[skillID];
DEBUGLOG( DEBUGLOG(
std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano skill " << std::endl; std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano skill " << std::endl;
) )
ICombatant* plrCombatant = dynamic_cast<ICombatant*>(plr); std::vector<int> targetData = findTargets(plr, skillID, data);
std::vector<ICombatant*> targetData = Abilities::matchTargets(plrCombatant, skillData, pkt->iTargetCnt, (int32_t*)(pkt + 1));
Abilities::useNanoSkill(sock, skillData, nano, targetData);
if (plr->Nanos[plr->activeNano].iStamina <= 0) int boost = 0;
if (getNanoBoost(plr))
boost = 1;
plr->Nanos[plr->activeNano].iStamina -= SkillTable[skillID].batteryUse[boost*3];
if (plr->Nanos[plr->activeNano].iStamina < 0)
plr->Nanos[plr->activeNano].iStamina = 0;
for (auto& pwr : NanoPowers)
if (pwr.skillType == SkillTable[skillID].skillType)
pwr.handle(sock, targetData, nanoID, skillID, SkillTable[skillID].durationTime[boost], SkillTable[skillID].powerIntensity[boost]);
if (plr->Nanos[plr->activeNano].iStamina < 0)
summonNano(sock, -1); summonNano(sock, -1);
} }
@@ -357,7 +385,7 @@ static void nanoPotionHandler(CNSocket* sock, CNPacketData* data) {
sock->sendPacket(response, P_FE2CL_REP_CHARGE_NANO_STAMINA); sock->sendPacket(response, P_FE2CL_REP_CHARGE_NANO_STAMINA);
// now update serverside // now update serverside
player->subtractCapped(CappedValueType::BATTERY_N, difference); player->batteryN -= difference;
player->Nanos[nano.iID].iStamina += difference; player->Nanos[nano.iID].iStamina += difference;
} }

View File

@@ -1,11 +1,10 @@
#pragma once #pragma once
#include "core/Core.hpp" #include <set>
#include <vector>
#include "Player.hpp" #include "Player.hpp"
#include "Abilities.hpp" #include "servers/CNShardServer.hpp"
#include <map>
struct NanoData { struct NanoData {
int style; int style;

View File

@@ -1,32 +1,24 @@
#pragma once #pragma once
#include <string>
#include <cstring>
#include "core/Core.hpp" #include "core/Core.hpp"
#include "Chunking.hpp"
#include "Entities.hpp" #include "Entities.hpp"
#include "Groups.hpp"
#include <vector> #include "lua/LuaWrapper.hpp"
/* forward declaration(s) */
class Buff;
struct BuffStack;
#define ACTIVE_MISSION_COUNT 6 #define ACTIVE_MISSION_COUNT 6
#define PC_MAXHEALTH(level) (925 + 75 * (level)) #define PC_MAXHEALTH(level) (925 + 75 * (level))
enum class CappedValueType { struct Player : public Entity {
TAROS,
FUSIONMATTER,
BATTERY_W,
BATTERY_N,
TAROS_IN_TRADE,
};
struct Player : public Entity, public ICombatant {
int accountId = 0; int accountId = 0;
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums) int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
int64_t SerialKey = 0;
int32_t iID = 0; int32_t iID = 0;
uint64_t FEKey = 0;
int level = 0; int level = 0;
int HP = 0; int HP = 0;
@@ -44,8 +36,9 @@ struct Player : public Entity, public ICombatant {
int8_t iPCState = 0; int8_t iPCState = 0;
int32_t iWarpLocationFlag = 0; int32_t iWarpLocationFlag = 0;
int64_t aSkywayLocationFlag[2] = {}; int64_t aSkywayLocationFlag[2] = {};
int32_t iConditionBitFlag = 0;
int32_t iSelfConditionBitFlag = 0;
int8_t iSpecialState = 0; int8_t iSpecialState = 0;
std::unordered_map<int, Buff*> buffs = {};
int angle = 0; int angle = 0;
int lastX = 0, lastY = 0, lastZ = 0, lastAngle = 0; int lastX = 0, lastY = 0, lastZ = 0, lastAngle = 0;
@@ -60,6 +53,7 @@ struct Player : public Entity, public ICombatant {
bool inCombat = false; bool inCombat = false;
bool onMonkey = false; bool onMonkey = false;
int nanoDrainRate = 0;
int healCooldown = 0; int healCooldown = 0;
int pointDamage = 0; int pointDamage = 0;
@@ -73,13 +67,18 @@ struct Player : public Entity, public ICombatant {
sItemBase QInven[AQINVEN_COUNT] = {}; sItemBase QInven[AQINVEN_COUNT] = {};
int32_t CurrentMissionID = 0; int32_t CurrentMissionID = 0;
Group* group = nullptr; sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {};
int32_t iIDGroup = 0;
int groupCnt = 0;
int32_t groupIDs[4] = {};
int32_t iGroupConditionBitFlag = 0;
bool notify = false; bool notify = false;
bool hidden = false; bool hidden = false;
bool unwarpable = false; bool unwarpable = false;
bool initialLoadDone = false;
bool buddiesSynced = false;
int64_t buddyIDs[50] = {}; int64_t buddyIDs[50] = {};
bool isBuddyBlocked[50] = {}; bool isBuddyBlocked[50] = {};
@@ -90,45 +89,16 @@ struct Player : public Entity, public ICombatant {
time_t lastShot = 0; time_t lastShot = 0;
std::vector<sItemBase> buyback = {}; std::vector<sItemBase> buyback = {};
double rateF[5] = { 1.0, 1.0, 1.0, 1.0, 1.0 }; // lua events
double rateT[5] = { 1.0, 1.0, 1.0, 1.0, 1.0 }; lEvent *onChat = nullptr;
Player() { Player() { type = EntityType::PLAYER; }
kind = EntityKind::PLAYER; ~Player() {
std::fill_n(rateF, 5, (double)settings::FUSIONMATTERRATE / 100.0); // if an event was registered, free it
std::fill_n(rateT, 5, (double)settings::TARORATE / 100.0); if (onChat != nullptr)
delete onChat;
} }
virtual void enterIntoViewOf(CNSocket *sock) override; virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override;
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
virtual Buff* getBuff(int buffId) override;
virtual void removeBuff(int buffId) override;
virtual void removeBuff(int buffId, BuffClass buffClass) override;
virtual void clearBuffs(bool force) override;
virtual bool hasBuff(int buffId) override;
virtual int getCompositeCondition() override;
virtual int takeDamage(EntityRef src, int amt) override;
virtual int heal(EntityRef src, int amt) override;
virtual bool isAlive() override;
virtual int getCurrentHP() override;
virtual int getMaxHP() override;
virtual int getLevel() override;
virtual std::vector<EntityRef> getGroupMembers() override;
virtual int32_t getCharType() override;
virtual int32_t getID() override;
virtual EntityRef getRef() override;
virtual void step(time_t currTime) override;
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

@@ -1,20 +1,27 @@
#include "PlayerManager.hpp" #include "core/Core.hpp"
#include "db/Database.hpp"
#include "core/CNShared.hpp" #include "core/CNShared.hpp"
#include "servers/CNShardServer.hpp" #include "servers/CNShardServer.hpp"
#include "db/Database.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp" #include "NPCManager.hpp"
#include "Missions.hpp"
#include "Items.hpp"
#include "Nanos.hpp"
#include "Groups.hpp"
#include "Chat.hpp"
#include "Buddies.hpp"
#include "Combat.hpp" #include "Combat.hpp"
#include "Racing.hpp" #include "Racing.hpp"
#include "Eggs.hpp"
#include "Missions.hpp"
#include "Chat.hpp"
#include "Items.hpp"
#include "Buddies.hpp"
#include "BuiltinCommands.hpp" #include "BuiltinCommands.hpp"
#include "Abilities.hpp"
#include "Eggs.hpp"
#include "settings.hpp"
#include "lua/LuaManager.hpp"
#include <assert.h> #include <assert.h>
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>
#include <cmath> #include <cmath>
@@ -23,26 +30,31 @@ using namespace PlayerManager;
std::map<CNSocket*, Player*> PlayerManager::players; std::map<CNSocket*, Player*> PlayerManager::players;
static void addPlayer(CNSocket* key, Player *plr) { static void addPlayer(CNSocket* key, Player plr) {
players[key] = plr; Player *p = new Player();
plr->chunkPos = Chunking::INVALID_CHUNK;
plr->lastHeartbeat = 0;
std::cout << getPlayerName(plr) << " has joined!" << std::endl; // copy object into heap memory
*p = plr;
players[key] = p;
p->chunkPos = std::make_tuple(0, 0, 0); // TODO: maybe replace with specialized "no chunk" value
p->lastHeartbeat = 0;
std::cout << getPlayerName(p) << " has joined!" << std::endl;
std::cout << players.size() << " players" << std::endl; std::cout << players.size() << " players" << std::endl;
// call events
LuaManager::playerAdded(key);
} }
void PlayerManager::removePlayer(CNSocket* key) { void PlayerManager::removePlayer(CNSocket* key) {
Player* plr = getPlayer(key); Player* plr = getPlayer(key);
uint64_t fromInstance = plr->instanceID; uint64_t fromInstance = plr->instanceID;
// free buff memory // call events
for(auto buffEntry : plr->buffs) LuaManager::playerRemoved(key);
delete buffEntry.second;
// leave group Groups::groupKickPlayer(plr);
if(plr->group != nullptr)
Groups::groupKick(plr->group, key);
// remove player's bullets // remove player's bullets
Combat::Bullets.erase(plr->iID); Combat::Bullets.erase(plr->iID);
@@ -66,6 +78,16 @@ void PlayerManager::removePlayer(CNSocket* key) {
// if the player was in a lair, clean it up // if the player was in a lair, clean it up
Chunking::destroyInstanceIfEmpty(fromInstance); Chunking::destroyInstanceIfEmpty(fromInstance);
// remove player's buffs from the server
auto it = Eggs::EggBuffs.begin();
while (it != Eggs::EggBuffs.end()) {
if (it->first.first == key) {
it = Eggs::EggBuffs.erase(it);
}
else
it++;
}
std::cout << players.size() << " players" << std::endl; std::cout << players.size() << " players" << std::endl;
} }
@@ -77,28 +99,12 @@ void PlayerManager::updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, ui
plr->x = X; plr->x = X;
plr->y = Y; plr->y = Y;
plr->z = Z; plr->z = Z;
if (plr->instanceID != I) { plr->instanceID = I;
plr->instanceID = I;
plr->recallInstance = INSTANCE_OVERWORLD;
}
if (oldChunk == newChunk) if (oldChunk == newChunk)
return; // didn't change chunks return; // didn't change chunks
Chunking::updateEntityChunk({sock}, oldChunk, newChunk); Chunking::updateEntityChunk({sock}, oldChunk, newChunk);
} }
/*
* Low-level helper function for correctly updating chunks when teleporting players.
*
* Use PlayerManager::sendPlayerTo() to actually teleport players.
*/
void PlayerManager::updatePlayerPositionForWarp(CNSocket* sock, int X, int Y, int Z, uint64_t inst) {
Player *plr = getPlayer(sock);
// force player to reload chunks
Chunking::updateEntityChunk({sock}, plr->chunkPos, Chunking::INVALID_CHUNK);
updatePlayerPosition(sock, X, Y, Z, inst, plr->angle);
}
void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I) { void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I) {
Player* plr = getPlayer(sock); Player* plr = getPlayer(sock);
plr->onMonkey = false; plr->onMonkey = false;
@@ -126,13 +132,32 @@ void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC); sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC);
} }
if (I != INSTANCE_OVERWORLD) {
INITSTRUCT(sP_FE2CL_INSTANCE_MAP_INFO, pkt);
pkt.iInstanceMapNum = (int32_t)MAPNUM(I); // lower 32 bits are mapnum
if (I != fromInstance // do not retransmit MAP_INFO on recall
&& Racing::EPData.find(pkt.iInstanceMapNum) != Racing::EPData.end()) {
EPInfo* ep = &Racing::EPData[pkt.iInstanceMapNum];
pkt.iEP_ID = ep->EPID;
pkt.iMapCoordX_Min = ep->zoneX * 51200;
pkt.iMapCoordX_Max = (ep->zoneX + 1) * 51200;
pkt.iMapCoordY_Min = ep->zoneY * 51200;
pkt.iMapCoordY_Max = (ep->zoneY + 1) * 51200;
pkt.iMapCoordZ_Min = INT32_MIN;
pkt.iMapCoordZ_Max = INT32_MAX;
}
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
}
INITSTRUCT(sP_FE2CL_REP_PC_GOTO_SUCC, pkt2); INITSTRUCT(sP_FE2CL_REP_PC_GOTO_SUCC, pkt2);
pkt2.iX = X; pkt2.iX = X;
pkt2.iY = Y; pkt2.iY = Y;
pkt2.iZ = Z; pkt2.iZ = Z;
sock->sendPacket(pkt2, P_FE2CL_REP_PC_GOTO_SUCC); sock->sendPacket(pkt2, P_FE2CL_REP_PC_GOTO_SUCC);
updatePlayerPositionForWarp(sock, X, Y, Z, I); Chunking::updateEntityChunk({sock}, plr->chunkPos, std::make_tuple(0, 0, 0)); // force player to reload chunks
updatePlayerPosition(sock, X, Y, Z, I, plr->angle);
// post-warp: check if the source instance has no more players in it and delete it if so // post-warp: check if the source instance has no more players in it and delete it if so
Chunking::destroyInstanceIfEmpty(fromInstance); Chunking::destroyInstanceIfEmpty(fromInstance);
@@ -155,21 +180,16 @@ 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 * 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. * for in these packets, even if the player hasn't unlocked them.
*/ */
static void sendNanoBook(CNSocket *sock, Player *plr, bool resizeOnly) { static void sendNanoBookSubset(CNSocket *sock) {
#ifdef ACADEMY #ifdef ACADEMY
Player *plr = getPlayer(sock);
int16_t id = 0; int16_t id = 0;
INITSTRUCT(sP_FE2CL_REP_NANO_BOOK_SUBSET, pkt); INITSTRUCT(sP_FE2CL_REP_NANO_BOOK_SUBSET, pkt);
pkt.PCUID = plr->iID; pkt.PCUID = plr->iID;
pkt.bookSize = NANO_COUNT; 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) { while (id < NANO_COUNT) {
pkt.elementOffset = id; pkt.elementOffset = id;
@@ -185,98 +205,78 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
auto enter = (sP_CL2FE_REQ_PC_ENTER*)data->buf; auto enter = (sP_CL2FE_REQ_PC_ENTER*)data->buf;
INITSTRUCT(sP_FE2CL_REP_PC_ENTER_SUCC, response); INITSTRUCT(sP_FE2CL_REP_PC_ENTER_SUCC, response);
LoginMetadata *lm = CNShared::getLoginMetadata(enter->iEnterSerialKey); // TODO: check if serialkey exists, if it doesn't send sP_FE2CL_REP_PC_ENTER_FAIL
if (lm == nullptr) { Player plr = CNSharedData::getPlayer(enter->iEnterSerialKey);
std::cout << "[WARN] Refusing invalid REQ_PC_ENTER" << std::endl;
// send failure packet plr.groupCnt = 1;
INITSTRUCT(sP_FE2CL_REP_PC_ENTER_FAIL, fail); plr.iIDGroup = plr.groupIDs[0] = plr.iID;
sock->sendPacket(fail, P_FE2CL_REP_PC_ENTER_FAIL);
// kill the connection DEBUGLOG(
sock->kill(); std::cout << "P_CL2FE_REQ_PC_ENTER:" << std::endl;
// no need to call _killConnection(); Player isn't in shard yet std::cout << "\tID: " << AUTOU16TOU8(enter->szID) << std::endl;
std::cout << "\tSerial: " << enter->iEnterSerialKey << std::endl;
return; std::cout << "\tTemp: " << enter->iTempValue << std::endl;
} std::cout << "\tPC_UID: " << plr.PCStyle.iPC_UID << std::endl;
)
Player *plr = new Player();
Database::getPlayer(plr, lm->playerId);
// check if account is already in use // check if account is already in use
if (isAccountInUse(plr->accountId)) { if (isAccountInUse(plr.accountId)) {
// kick the other player // kick the other player
exitDuplicate(plr->accountId); exitDuplicate(plr.accountId);
// re-read the player from disk, in case it was just flushed
*plr = {};
Database::getPlayer(plr, lm->playerId);
} }
plr->group = nullptr; response.iID = plr.iID;
response.iID = plr->iID;
response.uiSvrTime = getTime(); response.uiSvrTime = getTime();
response.PCLoadData2CL.iUserLevel = plr.accountLevel;
response.PCLoadData2CL.iUserLevel = plr->accountLevel; response.PCLoadData2CL.iHP = plr.HP;
response.PCLoadData2CL.iHP = plr->HP; response.PCLoadData2CL.iLevel = plr.level;
response.PCLoadData2CL.iLevel = plr->level; response.PCLoadData2CL.iCandy = plr.money;
response.PCLoadData2CL.iCandy = plr->money; response.PCLoadData2CL.iFusionMatter = plr.fusionmatter;
response.PCLoadData2CL.iFusionMatter = plr->fusionmatter; response.PCLoadData2CL.iMentor = plr.mentor;
response.PCLoadData2CL.iMentor = plr->mentor;
response.PCLoadData2CL.iMentorCount = 1; // how many guides the player has had response.PCLoadData2CL.iMentorCount = 1; // how many guides the player has had
response.PCLoadData2CL.iX = plr->x; response.PCLoadData2CL.iX = plr.x;
response.PCLoadData2CL.iY = plr->y; response.PCLoadData2CL.iY = plr.y;
response.PCLoadData2CL.iZ = plr->z; response.PCLoadData2CL.iZ = plr.z;
response.PCLoadData2CL.iAngle = plr->angle; response.PCLoadData2CL.iAngle = plr.angle;
response.PCLoadData2CL.iBatteryN = plr->batteryN; response.PCLoadData2CL.iBatteryN = plr.batteryN;
response.PCLoadData2CL.iBatteryW = plr->batteryW; response.PCLoadData2CL.iBatteryW = plr.batteryW;
response.PCLoadData2CL.iBuddyWarpTime = 60; // sets 60s warp cooldown on login response.PCLoadData2CL.iBuddyWarpTime = 60; // sets 60s warp cooldown on login
response.PCLoadData2CL.iWarpLocationFlag = plr->iWarpLocationFlag; response.PCLoadData2CL.iWarpLocationFlag = plr.iWarpLocationFlag;
response.PCLoadData2CL.aWyvernLocationFlag[0] = plr->aSkywayLocationFlag[0]; response.PCLoadData2CL.aWyvernLocationFlag[0] = plr.aSkywayLocationFlag[0];
response.PCLoadData2CL.aWyvernLocationFlag[1] = plr->aSkywayLocationFlag[1]; response.PCLoadData2CL.aWyvernLocationFlag[1] = plr.aSkywayLocationFlag[1];
response.PCLoadData2CL.iActiveNanoSlotNum = -1; response.PCLoadData2CL.iActiveNanoSlotNum = -1;
response.PCLoadData2CL.iFatigue = 50; response.PCLoadData2CL.iFatigue = 50;
response.PCLoadData2CL.PCStyle = plr->PCStyle; response.PCLoadData2CL.PCStyle = plr.PCStyle;
// client doesnt read this, it gets it from charinfo // client doesnt read this, it gets it from charinfo
// response.PCLoadData2CL.PCStyle2 = plr->PCStyle2; // response.PCLoadData2CL.PCStyle2 = plr.PCStyle2;
// 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 // inventory
for (int i = 0; i < AEQUIP_COUNT; i++)
response.PCLoadData2CL.aEquip[i] = plr.Equip[i];
for (int i = 0; i < AINVEN_COUNT; i++) for (int i = 0; i < AINVEN_COUNT; i++)
response.PCLoadData2CL.aInven[i] = plr->Inven[i]; response.PCLoadData2CL.aInven[i] = plr.Inven[i];
// quest inventory // quest inventory
for (int i = 0; i < AQINVEN_COUNT; i++) for (int i = 0; i < AQINVEN_COUNT; i++)
response.PCLoadData2CL.aQInven[i] = plr->QInven[i]; response.PCLoadData2CL.aQInven[i] = plr.QInven[i];
// nanos // nanos
for (int i = 1; i < SIZEOF_NANO_BANK_SLOT; i++) { for (int i = 1; i < SIZEOF_NANO_BANK_SLOT; i++) {
response.PCLoadData2CL.aNanoBank[i] = plr->Nanos[i]; response.PCLoadData2CL.aNanoBank[i] = plr.Nanos[i];
//response.PCLoadData2CL.aNanoBank[i] = plr.Nanos[i] = {0};
} }
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
response.PCLoadData2CL.aNanoSlots[i] = plr->equippedNanos[i]; response.PCLoadData2CL.aNanoSlots[i] = plr.equippedNanos[i];
} }
// missions in progress // missions in progress
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) { for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == 0) if (plr.tasks[i] == 0)
break; break;
response.PCLoadData2CL.aRunningQuest[i].m_aCurrTaskID = plr->tasks[i]; response.PCLoadData2CL.aRunningQuest[i].m_aCurrTaskID = plr.tasks[i];
TaskData &task = *Missions::Tasks[plr->tasks[i]]; TaskData &task = *Missions::Tasks[plr.tasks[i]];
for (int j = 0; j < 3; j++) { for (int j = 0; j < 3; j++) {
response.PCLoadData2CL.aRunningQuest[i].m_aKillNPCID[j] = (int)task["m_iCSUEnemyID"][j]; response.PCLoadData2CL.aRunningQuest[i].m_aKillNPCID[j] = (int)task["m_iCSUEnemyID"][j];
response.PCLoadData2CL.aRunningQuest[i].m_aKillNPCCount[j] = plr->RemainingNPCCount[i][j]; response.PCLoadData2CL.aRunningQuest[i].m_aKillNPCCount[j] = plr.RemainingNPCCount[i][j];
/* /*
* client doesn't care about NeededItem ID and Count, * client doesn't care about NeededItem ID and Count,
* it gets Count from Quest Inventory * it gets Count from Quest Inventory
@@ -286,12 +286,12 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
*/ */
} }
} }
response.PCLoadData2CL.iCurrentMissionID = plr->CurrentMissionID; response.PCLoadData2CL.iCurrentMissionID = plr.CurrentMissionID;
// completed missions // completed missions
// the packet requires 32 items, but the client only checks the first 16 (shrug) // the packet requires 32 items, but the client only checks the first 16 (shrug)
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
response.PCLoadData2CL.aQuestFlag[i] = plr->aQuestFlag[i]; response.PCLoadData2CL.aQuestFlag[i] = plr.aQuestFlag[i];
} }
// Computress tips // Computress tips
@@ -300,45 +300,39 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
response.PCLoadData2CL.iFirstUseFlag2 = UINT64_MAX; response.PCLoadData2CL.iFirstUseFlag2 = UINT64_MAX;
} }
else { else {
response.PCLoadData2CL.iFirstUseFlag1 = plr->iFirstUseFlag[0]; response.PCLoadData2CL.iFirstUseFlag1 = plr.iFirstUseFlag[0];
response.PCLoadData2CL.iFirstUseFlag2 = plr->iFirstUseFlag[1]; response.PCLoadData2CL.iFirstUseFlag2 = plr.iFirstUseFlag[1];
} }
plr->instanceID = INSTANCE_OVERWORLD; // the player should never be in an instance on enter plr.SerialKey = enter->iEnterSerialKey;
plr.instanceID = INSTANCE_OVERWORLD; // the player should never be in an instance on enter
sock->setEKey(CNSocketEncryption::createNewKey(response.uiSvrTime, response.iID + 1, response.PCLoadData2CL.iFusionMatter + 1)); sock->setEKey(CNSocketEncryption::createNewKey(response.uiSvrTime, response.iID + 1, response.PCLoadData2CL.iFusionMatter + 1));
sock->setFEKey(lm->FEKey); sock->setFEKey(plr.FEKey);
sock->setActiveKey(SOCKETKEY_FE); // send all packets using the FE key from now on 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); sock->sendPacket(response, P_FE2CL_REP_PC_ENTER_SUCC);
sendNanoBook(sock, plr, false); // transmit MOTD after entering the game, so the client hopefully changes modes on time
Chat::sendServerMessage(sock, settings::MOTDSTRING);
// transfer ownership of Player object into the shard (still valid in this function though)
addPlayer(sock, plr); addPlayer(sock, plr);
// check if there is an expiring vehicle
Items::checkItemExpire(sock, getPlayer(sock));
// set player equip stats // set player equip stats
Items::setItemStats(plr); Items::setItemStats(getPlayer(sock));
Missions::failInstancedMissions(sock);
sendNanoBookSubset(sock);
// initial buddy sync
Buddies::refreshBuddyList(sock);
for (auto& pair : players) for (auto& pair : players)
if (pair.second->notify) if (pair.second->notify)
Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(plr) + " has joined."); Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(&plr) + " has joined.");
// deallocate lm
delete lm;
}
void PlayerManager::sendToGroup(CNSocket* sock, void* buf, uint32_t type, size_t size) {
Player* plr = getPlayer(sock);
if (plr->group == nullptr)
return;
for(const EntityRef& ref : plr->group->filter(EntityKind::PLAYER))
ref.sock->sendPacket(buf, type, size);
} }
void PlayerManager::sendToViewable(CNSocket* sock, void* buf, uint32_t type, size_t size) { void PlayerManager::sendToViewable(CNSocket* sock, void* buf, uint32_t type, size_t size) {
@@ -346,7 +340,7 @@ void PlayerManager::sendToViewable(CNSocket* sock, void* buf, uint32_t type, siz
for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) { for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) {
Chunk* chunk = *it; Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) { for (const EntityRef& ref : chunk->entities) {
if (ref.kind != EntityKind::PLAYER || ref.sock == sock) if (ref.type != EntityType::PLAYER || ref.sock == sock)
continue; continue;
ref.sock->sendPacket(buf, type, size); ref.sock->sendPacket(buf, type, size);
@@ -369,35 +363,6 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) {
updatePlayerPosition(sock, plr->x, plr->y, plr->z, plr->instanceID, plr->angle); updatePlayerPosition(sock, plr->x, plr->y, plr->z, plr->instanceID, plr->angle);
sock->sendPacket(response, P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC); sock->sendPacket(response, P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC);
if (plr->instanceID != INSTANCE_OVERWORLD) {
INITSTRUCT(sP_FE2CL_INSTANCE_MAP_INFO, pkt);
pkt.iInstanceMapNum = (int32_t)MAPNUM(plr->instanceID); // lower 32 bits are mapnum
if (pkt.iInstanceMapNum != plr->recallInstance // do not retransmit MAP_INFO on recall
&& Racing::EPData.find(pkt.iInstanceMapNum) != Racing::EPData.end()) {
EPInfo* ep = &Racing::EPData[pkt.iInstanceMapNum];
pkt.iEP_ID = ep->EPID;
pkt.iMapCoordX_Min = ep->zoneX * 51200;
pkt.iMapCoordX_Max = (ep->zoneX + 1) * 51200;
pkt.iMapCoordY_Min = ep->zoneY * 51200;
pkt.iMapCoordY_Max = (ep->zoneY + 1) * 51200;
pkt.iMapCoordZ_Min = INT32_MIN;
pkt.iMapCoordZ_Max = INT32_MAX;
}
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
}
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) { static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
@@ -406,23 +371,12 @@ static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
static void exitGame(CNSocket* sock, CNPacketData* data) { static void exitGame(CNSocket* sock, CNPacketData* data) {
auto exitData = (sP_CL2FE_REQ_PC_EXIT*)data->buf; 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); INITSTRUCT(sP_FE2CL_REP_PC_EXIT_SUCC, response);
response.iID = exitData->iID; response.iID = exitData->iID;
response.iExitCode = 1; response.iExitCode = 1;
sock->sendPacket(response, P_FE2CL_REP_PC_EXIT_SUCC); sock->sendPacket(response, P_FE2CL_REP_PC_EXIT_SUCC);
sock->kill();
CNShardServer::_killConnection(sock);
} }
static void revivePlayer(CNSocket* sock, CNPacketData* data) { static void revivePlayer(CNSocket* sock, CNPacketData* data) {
@@ -436,23 +390,17 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
int activeSlot = -1; int activeSlot = -1;
bool move = false; bool move = false;
switch ((ePCRegenType)reviveData->iRegenType) { if (reviveData->iRegenType == 3 && plr->iConditionBitFlag & CSB_BIT_PHOENIX) {
case ePCRegenType::HereByPhoenix: // nano revive // nano revive
if (!(plr->hasBuff(ECSB_PHOENIX)))
return; // sanity check
plr->Nanos[plr->activeNano].iStamina = 0; plr->Nanos[plr->activeNano].iStamina = 0;
// fallthrough plr->HP = PC_MAXHEALTH(plr->level);
case ePCRegenType::HereByPhoenixGroup: // revived by group member's nano Nanos::applyBuff(sock, plr->Nanos[plr->activeNano].iSkillID, 2, 1, 0);
plr->HP = PC_MAXHEALTH(plr->level) / 2; } else if (reviveData->iRegenType == 4) {
break; plr->HP = PC_MAXHEALTH(plr->level);
} else {
default: // plain respawn
plr->HP = PC_MAXHEALTH(plr->level) / 2;
plr->clearBuffs(false);
// fallthrough
case ePCRegenType::Unstick: // warp away
move = true; move = true;
break; if (reviveData->iRegenType != 5)
plr->HP = PC_MAXHEALTH(plr->level);
} }
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
@@ -504,8 +452,11 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
resp2.PCRegenDataForOtherPC.iHP = plr->HP; resp2.PCRegenDataForOtherPC.iHP = plr->HP;
resp2.PCRegenDataForOtherPC.iAngle = plr->angle; resp2.PCRegenDataForOtherPC.iAngle = plr->angle;
if (plr->group != nullptr) { Player *otherPlr = getPlayerFromID(plr->iIDGroup);
resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->getCompositeCondition(); if (otherPlr != nullptr) {
int bitFlag = Groups::getGroupFlags(otherPlr);
resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->iConditionBitFlag = plr->iSelfConditionBitFlag | bitFlag;
resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState; resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState;
resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState; resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState;
resp2.PCRegenDataForOtherPC.Nano = plr->Nanos[plr->activeNano]; resp2.PCRegenDataForOtherPC.Nano = plr->Nanos[plr->activeNano];
@@ -516,7 +467,8 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
if (!move) if (!move)
return; return;
updatePlayerPositionForWarp(sock, x, y, z, plr->instanceID); Chunking::updateEntityChunk({sock}, plr->chunkPos, std::make_tuple(0, 0, 0)); // force player to reload chunks
updatePlayerPosition(sock, x, y, z, plr->instanceID, plr->angle);
} }
static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) { static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
@@ -526,7 +478,9 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
if (plr->instanceID != 0) if (plr->instanceID != 0)
return; return;
if (plr->Equip[EQUIP_SLOT_VEHICLE].iID > 0) { bool expired = plr->Equip[8].iTimeLimit < getTimestamp() && plr->Equip[8].iTimeLimit != 0;
if (plr->Equip[8].iID > 0 && !expired) {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_SUCC, response); INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_SUCC, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_SUCC); sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_SUCC);
@@ -540,6 +494,30 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
} else { } else {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_FAIL, response); INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_FAIL, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_FAIL); 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);
} }
} }
@@ -568,7 +546,7 @@ static void changePlayerGuide(CNSocket *sock, CNPacketData *data) {
} }
// start Blossom nano mission if applicable // start Blossom nano mission if applicable
Missions::updateFusionMatter(sock); Missions::updateFusionMatter(sock, 0);
} }
// save it on player // save it on player
plr->mentor = pkt->iMentor; plr->mentor = pkt->iMentor;
@@ -582,7 +560,7 @@ static void setFirstUseFlag(CNSocket* sock, CNPacketData* data) {
std::cout << "[WARN] Client submitted invalid first use flag number?!" << std::endl; std::cout << "[WARN] Client submitted invalid first use flag number?!" << std::endl;
return; return;
} }
if (flag->iFlagCode <= 64) if (flag->iFlagCode <= 64)
plr->iFirstUseFlag[0] |= (1ULL << (flag->iFlagCode - 1)); plr->iFirstUseFlag[0] |= (1ULL << (flag->iFlagCode - 1));
else else
@@ -590,23 +568,6 @@ static void setFirstUseFlag(CNSocket* sock, CNPacketData* data) {
} }
#pragma region Helper methods #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) { Player *PlayerManager::getPlayer(CNSocket* key) {
if (players.find(key) != players.end()) if (players.find(key) != players.end())
return players[key]; return players[key];
@@ -620,10 +581,6 @@ std::string PlayerManager::getPlayerName(Player *plr, bool id) {
if (plr == nullptr) if (plr == nullptr)
return "NOT IN GAME"; return "NOT IN GAME";
if (plr->PCStyle.iNameCheck != 1) {
return "Player " + std::to_string(plr->iID);
}
std::string ret = ""; std::string ret = "";
if (id && plr->accountLevel <= 30) if (id && plr->accountLevel <= 30)
ret += "(GM) "; ret += "(GM) ";
@@ -691,16 +648,16 @@ CNSocket *PlayerManager::getSockFromName(std::string firstname, std::string last
} }
CNSocket *PlayerManager::getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname) { CNSocket *PlayerManager::getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname) {
switch ((eCN_GM_TargetSearchBy)by) { switch (by) {
case eCN_GM_TargetSearchBy::PC_ID: case eCN_GM_TargetSearchBy__PC_ID:
assert(id != 0); assert(id != 0);
return getSockFromID(id); return getSockFromID(id);
case eCN_GM_TargetSearchBy::PC_UID: // account id; not player id case eCN_GM_TargetSearchBy__PC_UID: // account id; not player id
assert(uid != 0); assert(uid != 0);
for (auto& pair : players) for (auto& pair : players)
if (pair.second->accountId == uid) if (pair.second->accountId == uid)
return pair.first; return pair.first;
case eCN_GM_TargetSearchBy::PC_Name: case eCN_GM_TargetSearchBy__PC_Name:
assert(firstname != "" && lastname != ""); // XXX: remove this if we start messing around with edited names? assert(firstname != "" && lastname != ""); // XXX: remove this if we start messing around with edited names?
return getSockFromName(firstname, lastname); return getSockFromName(firstname, lastname);
} }
@@ -738,6 +695,4 @@ void PlayerManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VEHICLE_OFF, exitPlayerVehicle); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VEHICLE_OFF, exitPlayerVehicle);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_CHANGE_MENTOR, changePlayerGuide); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_CHANGE_MENTOR, changePlayerGuide);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_FIRST_USE_FLAG_SET, setFirstUseFlag); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_FIRST_USE_FLAG_SET, setFirstUseFlag);
REGISTER_SHARD_TIMER(CNShared::pruneLoginMetadata, CNSHARED_PERIOD);
} }

View File

@@ -1,25 +1,23 @@
#pragma once #pragma once
#include "core/Core.hpp"
#include "Player.hpp" #include "Player.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "Chunking.hpp" #include "Chunking.hpp"
#include "Transport.hpp"
#include "Entities.hpp"
#include <utility>
#include <map> #include <map>
#include <list> #include <list>
struct WarpLocation;
namespace PlayerManager { namespace PlayerManager {
extern std::map<CNSocket*, Player*> players; extern std::map<CNSocket*, Player*> players;
void init(); void init();
void exitPlayerVehicle(CNSocket* sock, CNPacketData* data);
void removePlayer(CNSocket* key); void removePlayer(CNSocket* key);
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle); void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle);
void updatePlayerPositionForWarp(CNSocket* sock, int X, int Y, int Z, uint64_t inst);
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I); void sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I);
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z); void sendPlayerTo(CNSocket* sock, int X, int Y, int Z);
@@ -35,7 +33,6 @@ namespace PlayerManager {
CNSocket *getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname); CNSocket *getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname);
WarpLocation *getRespawnPoint(Player *plr); WarpLocation *getRespawnPoint(Player *plr);
void sendToGroup(CNSocket *sock, void* buf, uint32_t type, size_t size);
void sendToViewable(CNSocket *sock, void* buf, uint32_t type, size_t size); void sendToViewable(CNSocket *sock, void* buf, uint32_t type, size_t size);
// TODO: unify this under the new Entity system // TODO: unify this under the new Entity system
@@ -45,7 +42,7 @@ namespace PlayerManager {
for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) { for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) {
Chunk* chunk = *it; Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) { for (const EntityRef& ref : chunk->entities) {
if (ref.kind != EntityKind::PLAYER || ref.sock == sock) if (ref.type != EntityType::PLAYER || ref.sock == sock)
continue; continue;
ref.sock->sendPacket(pkt, type); ref.sock->sendPacket(pkt, type);

View File

@@ -1,7 +1,4 @@
#include "PlayerMovement.hpp" #include "PlayerMovement.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "TableData.hpp" #include "TableData.hpp"
#include "core/Core.hpp" #include "core/Core.hpp"
@@ -36,7 +33,7 @@ static void movePlayer(CNSocket* sock, CNPacketData* data) {
// [gruntwork] check if player has a follower and move it // [gruntwork] check if player has a follower and move it
if (TableData::RunningNPCPaths.find(plr->iID) != TableData::RunningNPCPaths.end()) { if (TableData::RunningNPCPaths.find(plr->iID) != TableData::RunningNPCPaths.end()) {
BaseNPC* follower = TableData::RunningNPCPaths[plr->iID].first; BaseNPC* follower = TableData::RunningNPCPaths[plr->iID].first;
Transport::NPCQueues.erase(follower->id); // erase existing points Transport::NPCQueues.erase(follower->appearanceData.iNPC_ID); // erase existing points
std::queue<Vec3> queue; std::queue<Vec3> queue;
Vec3 from = { follower->x, follower->y, follower->z }; Vec3 from = { follower->x, follower->y, follower->z };
float drag = 0.95f; // this ensures that they don't bump into the player float drag = 0.95f; // this ensures that they don't bump into the player
@@ -48,7 +45,7 @@ static void movePlayer(CNSocket* sock, CNPacketData* data) {
// add a route to the queue; to be processed in Transport::stepNPCPathing() // add a route to the queue; to be processed in Transport::stepNPCPathing()
Transport::lerp(&queue, from, to, NPC_DEFAULT_SPEED * 1.5); // little faster than typical Transport::lerp(&queue, from, to, NPC_DEFAULT_SPEED * 1.5); // little faster than typical
Transport::NPCQueues[follower->id] = queue; Transport::NPCQueues[follower->appearanceData.iNPC_ID] = queue;
} }
} }

View File

@@ -1,13 +1,10 @@
#include "Racing.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp" #include "servers/CNShardServer.hpp"
#include "Racing.hpp"
#include "NPCManager.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Missions.hpp" #include "Missions.hpp"
#include "Items.hpp" #include "Items.hpp"
#include "Nanos.hpp" #include "db/Database.hpp"
#include "NPCManager.hpp"
using namespace Racing; using namespace Racing;
@@ -15,15 +12,6 @@ std::map<int32_t, EPInfo> Racing::EPData;
std::map<CNSocket*, EPRace> Racing::EPRaces; std::map<CNSocket*, EPRace> Racing::EPRaces;
std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> Racing::EPRewards; 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) { static void racingStart(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_EP_RACE_START*)data->buf; auto req = (sP_CL2FE_REQ_EP_RACE_START*)data->buf;
@@ -76,23 +64,9 @@ static void racingCancel(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC); sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
/* // we have to teleport the player back after this, otherwise they are unable to move
* This request packet is used for both cancelling the race via the
* NPC at the start, *and* failing the race by running out of time.
* If the latter is to happen, the client disables movement until it
* receives a packet from the server that re-enables it.
*
* So, in order to prevent a potential softlock we respawn the player.
*/
WarpLocation* respawnLoc = PlayerManager::getRespawnPoint(plr); WarpLocation* respawnLoc = PlayerManager::getRespawnPoint(plr);
PlayerManager::sendPlayerTo(sock, respawnLoc->x, respawnLoc->y, respawnLoc->z, respawnLoc->instanceID);
if (respawnLoc != nullptr) {
PlayerManager::sendPlayerTo(sock, respawnLoc->x, respawnLoc->y, respawnLoc->z, respawnLoc->instanceID);
} else {
// fallback, just respawn the player in-place if no suitable point is found
PlayerManager::sendPlayerTo(sock, plr->x, plr->y, plr->z, plr->instanceID);
}
} }
static void racingEnd(CNSocket* sock, CNPacketData* data) { static void racingEnd(CNSocket* sock, CNPacketData* data) {
@@ -109,43 +83,35 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0) if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
return; // IZ not found return; // IZ not found
EPInfo& epInfo = EPData[mapNum];
EPRace& epRace = EPRaces[sock];
uint64_t now = getTime() / 1000; uint64_t now = getTime() / 1000;
int timeDiff = now - epRace.startTime;
int podsCollected = epRace.collectedRings.size();
int score = std::exp( int timeDiff = now - EPRaces[sock].startTime;
(epInfo.podFactor * podsCollected) / epInfo.maxPods int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff;
- (epInfo.timeFactor * timeDiff) / epInfo.maxTime if (score < 0) score = 0; // lol
+ epInfo.scaleFactor); int fm = score * plr->level * (1.0f / 36) * 0.3f;
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... // we submit the ranking first...
Database::RaceRanking postRanking = {}; Database::RaceRanking postRanking = {};
postRanking.EPID = epInfo.EPID; postRanking.EPID = EPData[mapNum].EPID;
postRanking.PlayerID = plr->iID; postRanking.PlayerID = plr->iID;
postRanking.RingCount = podsCollected; postRanking.RingCount = EPRaces[sock].collectedRings.size();
postRanking.Score = score; postRanking.Score = score;
postRanking.Time = timeDiff; postRanking.Time = timeDiff;
postRanking.Timestamp = getTimestamp(); postRanking.Timestamp = getTimestamp();
Database::postRaceRanking(postRanking); Database::postRaceRanking(postRanking);
// ...then we get the top ranking, which may or may not be what we just submitted // ...then we get the top ranking, which may or may not be what we just submitted
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(epInfo.EPID, plr->iID); Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(EPData[mapNum].EPID, plr->iID);
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
// get rank scores and rewards // get rank scores and rewards
std::vector<int>* rankScores = &EPRewards[epInfo.EPID].first; std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first;
std::vector<int>* rankRewards = &EPRewards[epInfo.EPID].second; std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second;
// top ranking // top ranking
int maxRank = rankScores->size() - 1;
int topRank = 0; int topRank = 0;
while (topRank < maxRank && rankScores->at(topRank) > topRankingPlayer.Score) while (rankScores->at(topRank) > topRankingPlayer.Score)
topRank++; topRank++;
resp.iEPTopRank = topRank + 1; resp.iEPTopRank = topRank + 1;
@@ -155,7 +121,7 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
// this ranking // this ranking
int rank = 0; int rank = 0;
while (rank < maxRank && rankScores->at(rank) > postRanking.Score) while (rankScores->at(rank) > postRanking.Score)
rank++; rank++;
resp.iEPRank = rank + 1; resp.iEPRank = rank + 1;
@@ -165,9 +131,7 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
resp.iEPRaceMode = EPRaces[sock].mode; resp.iEPRaceMode = EPRaces[sock].mode;
resp.iEPRewardFM = fm; resp.iEPRewardFM = fm;
int32_t fmReward = calculateFMReward(plr, resp.iEPRewardFM); Missions::updateFusionMatter(sock, resp.iEPRewardFM);
plr->addCapped(CappedValueType::FUSIONMATTER, fmReward);
Missions::updateFusionMatter(sock);
resp.iFusionMatter = plr->fusionmatter; resp.iFusionMatter = plr->fusionmatter;
resp.iFatigue = 50; resp.iFatigue = 50;

View File

@@ -1,21 +1,15 @@
#pragma once #pragma once
#include "core/Core.hpp"
#include <map>
#include <vector>
#include <set> #include <set>
#include "servers/CNShardServer.hpp"
struct EPInfo { struct EPInfo {
// available through XDT (maxScore may be updated by drops) int zoneX, zoneY, EPID, maxScore, maxTime;
int zoneX, zoneY, EPID, maxScore;
// available through drops
int maxTime, maxPods;
double scaleFactor, podFactor, timeFactor;
}; };
struct EPRace { struct EPRace {
std::set<int> collectedRings; std::set<int> collectedRings;
int mode, ticketSlot; int mode, ticketSlot;
time_t startTime; time_t startTime;
}; };

View File

@@ -1,5 +1,4 @@
#include "Rand.hpp" #include "Rand.hpp"
#include "core/Core.hpp"
std::unique_ptr<std::mt19937> Rand::generator; std::unique_ptr<std::mt19937> Rand::generator;
@@ -34,58 +33,6 @@ float Rand::randFloat() {
return Rand::randFloat(0.0f, 1.0f); return Rand::randFloat(0.0f, 1.0f);
} }
#define RANDBYTES 8
/*
* Cryptographically secure RNG. Borrowed from bcrypt_gensalt().
*/
uint64_t Rand::cryptoRand() {
uint8_t buf[RANDBYTES];
#ifdef _WIN32
HCRYPTPROV p;
// Acquire a crypt context for generating random bytes.
if (CryptAcquireContext(&p, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT) == FALSE) {
goto fail;
}
if (CryptGenRandom(p, RANDBYTES, (BYTE*)buf) == FALSE) {
goto fail;
}
if (CryptReleaseContext(p, 0) == FALSE) {
goto fail;
}
#else
int fd;
// Get random bytes on Unix/Linux.
fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) {
perror("open");
goto fail;
}
if (read(fd, buf, RANDBYTES) < RANDBYTES) {
perror("read");
close(fd);
goto fail;
}
close(fd);
#endif
return *(uint64_t*)buf;
fail:
std::cout << "[FATAL] Failed to generate cryptographic random number" << std::endl;
terminate(0);
/* not reached */
return 0;
}
void Rand::init(uint64_t seed) { void Rand::init(uint64_t seed) {
Rand::generator = std::make_unique<std::mt19937>(std::mt19937(seed)); Rand::generator = std::make_unique<std::mt19937>(std::mt19937(seed));
} }

View File

@@ -2,7 +2,6 @@
#include <random> #include <random>
#include <memory> #include <memory>
#include <vector>
namespace Rand { namespace Rand {
extern std::unique_ptr<std::mt19937> generator; extern std::unique_ptr<std::mt19937> generator;
@@ -15,8 +14,6 @@ namespace Rand {
int32_t randWeighted(const std::vector<int32_t>& weights); int32_t randWeighted(const std::vector<int32_t>& weights);
uint64_t cryptoRand();
float randFloat(float startInclusive, float endExclusive); float randFloat(float startInclusive, float endExclusive);
float randFloat(float endExclusive); float randFloat(float endExclusive);
float randFloat(); float randFloat();

View File

@@ -1,16 +1,18 @@
#include "TableData.hpp" #include "TableData.hpp"
#include "servers/CNLoginServer.hpp"
#include "NPCManager.hpp" #include "NPCManager.hpp"
#include "Missions.hpp" #include "Transport.hpp"
#include "Items.hpp" #include "Items.hpp"
#include "Vendors.hpp" #include "settings.hpp"
#include "Racing.hpp" #include "Missions.hpp"
#include "Chunking.hpp"
#include "Nanos.hpp" #include "Nanos.hpp"
#include "Racing.hpp"
#include "Vendors.hpp"
#include "Abilities.hpp" #include "Abilities.hpp"
#include "Eggs.hpp" #include "Eggs.hpp"
#include "JSON.hpp"
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <cmath> #include <cmath>
@@ -35,22 +37,6 @@ public:
const char *what() const throw() { return msg.c_str(); } const char *what() const throw() { return msg.c_str(); }
}; };
/*
* We must refuse to run if an invalid NPC type is found in the JSONs, especially
* the gruntwork file. If we were to just skip loading invalid NPCs, they would get
* silently dropped from the gruntwork file, which would be confusing in situations
* where a gruntwork file for the wrong game build was accidentally loaded.
*/
static void ensureValidNPCType(int type, std::string filename) {
// last known NPC type
int npcLimit = NPCManager::NPCData.back()["m_iNpcNumber"];
if (type > npcLimit) {
std::cout << "[FATAL] " << filename << " contains an invalid NPC type: " << type << std::endl;
exit(1);
}
}
/* /*
* Create a full and properly-paced path by interpolating between keyframes. * Create a full and properly-paced path by interpolating between keyframes.
*/ */
@@ -81,25 +67,6 @@ static void loadXDT(json& xdtData) {
NPCManager::NPCData = xdtData["m_pNpcTable"]["m_pNpcData"]; NPCManager::NPCData = xdtData["m_pNpcTable"]["m_pNpcData"];
try { 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 // load warps
json warpData = xdtData["m_pInstanceTable"]["m_pWarpData"]; json warpData = xdtData["m_pInstanceTable"]["m_pWarpData"];
@@ -248,34 +215,18 @@ static void loadXDT(json& xdtData) {
// load nano powers // load nano powers
json skills = xdtData["m_pSkillTable"]["m_pSkillData"]; json skills = xdtData["m_pSkillTable"]["m_pSkillData"];
for (json::iterator _skill = skills.begin(); _skill != skills.end(); _skill++) { for (json::iterator _skills = skills.begin(); _skills != skills.end(); _skills++) {
auto skill = _skill.value(); auto skills = _skills.value();
SkillData skillData = { SkillData skillData = { skills["m_iSkillType"], skills["m_iTargetType"], skills["m_iBatteryDrainType"], skills["m_iEffectArea"] };
skill["m_iSkillType"],
skill["m_iEffectTarget"],
skill["m_iEffectType"],
skill["m_iTargetType"],
skill["m_iBatteryDrainType"],
skill["m_iEffectArea"]
};
skillData.valueTypes[0] = skill["m_iValueA_Type"];
skillData.valueTypes[1] = skill["m_iValueB_Type"];
skillData.valueTypes[2] = skill["m_iValueC_Type"];
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
skillData.batteryUse[i] = skill["m_iBatteryDrainUse"][i]; skillData.batteryUse[i] = skills["m_iBatteryDrainUse"][i];
skillData.durationTime[i] = skill["m_iDurationTime"][i]; skillData.durationTime[i] = skills["m_iDurationTime"][i];
skillData.powerIntensity[i] = skills["m_iValueA"][i];
skillData.values[0][i] = skill["m_iValueA"][i];
skillData.values[1][i] = skill["m_iValueB"][i];
skillData.values[2][i] = skill["m_iValueC"][i];
} }
Nanos::SkillTable[skills["m_iSkillNumber"]] = skillData;
Abilities::SkillTable[skill["m_iSkillNumber"]] = skillData;
} }
std::cout << "[INFO] Loaded " << Abilities::SkillTable.size() << " nano skills" << std::endl; std::cout << "[INFO] Loaded " << Nanos::SkillTable.size() << " nano skills" << std::endl;
// load EP data // load EP data
json instances = xdtData["m_pInstanceTable"]["m_pInstanceData"]; json instances = xdtData["m_pInstanceTable"]["m_pInstanceData"];
@@ -347,10 +298,10 @@ static void loadPaths(json& pathData, int32_t* nextId) {
if (passedDistance >= SLIDER_GAP_SIZE) { // space them out uniformaly if (passedDistance >= SLIDER_GAP_SIZE) { // space them out uniformaly
passedDistance -= SLIDER_GAP_SIZE; // step down passedDistance -= SLIDER_GAP_SIZE; // step down
// spawn a slider // spawn a slider
Bus* slider = new Bus(0, INSTANCE_OVERWORLD, 1, (*nextId)--); Bus* slider = new Bus(point.x, point.y, point.z, 0, INSTANCE_OVERWORLD, 1, (*nextId)--);
NPCManager::NPCs[slider->id] = slider; NPCManager::NPCs[slider->appearanceData.iNPC_ID] = slider;
NPCManager::updateNPCPosition(slider->id, point.x, point.y, point.z, INSTANCE_OVERWORLD, 0); NPCManager::updateNPCPosition(slider->appearanceData.iNPC_ID, slider->x, slider->y, slider->z, INSTANCE_OVERWORLD, 0);
Transport::NPCQueues[slider->id] = route; Transport::NPCQueues[slider->appearanceData.iNPC_ID] = route;
} }
// rotate // rotate
route.pop(); route.pop();
@@ -396,7 +347,7 @@ static void loadPaths(json& pathData, int32_t* nextId) {
Transport::NPCPaths.push_back(pathTemplate); Transport::NPCPaths.push_back(pathTemplate);
} }
std::cout << "[INFO] Loaded " << Transport::NPCPaths.size() << " NPC paths" << std::endl; std::cout << "[INFO] Loaded " << Transport::NPCPaths.size() << " NPC paths" << std::endl;
} }
catch (const std::exception& err) { catch (const std::exception& err) {
std::cerr << "[FATAL] Malformed paths.json file! Reason:" << err.what() << std::endl; std::cerr << "[FATAL] Malformed paths.json file! Reason:" << err.what() << std::endl;
@@ -605,17 +556,8 @@ static void loadDrops(json& dropData) {
continue; continue;
} }
EPInfo& epInfo = Racing::EPData[EPMap]; // time limit isn't stored in the XDT, so we include it in the reward table instead
Racing::EPData[EPMap].maxTime = race["TimeLimit"];
// max score is specified in the XDT, but can be updated if specified in the drops JSON
epInfo.maxScore = (int)race["ScoreCap"];
// time limit and total pods are not stored in the XDT, so we include it in the drops JSON
epInfo.maxTime = (int)race["TimeLimit"];
epInfo.maxPods = (int)race["TotalPods"];
// IZ-specific calculated constants included in the drops JSON
epInfo.scaleFactor = (double)race["ScaleFactor"];
epInfo.podFactor = (double)race["PodFactor"];
epInfo.timeFactor = (double)race["TimeFactor"];
// score cutoffs // score cutoffs
std::vector<int> rankScores; std::vector<int> rankScores;
@@ -631,7 +573,7 @@ static void loadDrops(json& dropData) {
if (rankScores.size() != 5 || rankScores.size() != rankRewards.size()) { if (rankScores.size() != 5 || rankScores.size() != rankRewards.size()) {
char buff[255]; char buff[255];
snprintf(buff, 255, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID); sprintf(buff, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID);
throw TableException(std::string(buff)); throw TableException(std::string(buff));
} }
@@ -701,7 +643,7 @@ static void loadEggs(json& eggData, int32_t* nextId) {
int id = (*nextId)--; int id = (*nextId)--;
uint64_t instanceID = egg.find("iMapNum") == egg.end() ? INSTANCE_OVERWORLD : (int)egg["iMapNum"]; uint64_t instanceID = egg.find("iMapNum") == egg.end() ? INSTANCE_OVERWORLD : (int)egg["iMapNum"];
Egg* addEgg = new Egg(instanceID, (int)egg["iType"], id, false); Egg* addEgg = new Egg((int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, (int)egg["iType"], id, false);
NPCManager::NPCs[id] = addEgg; NPCManager::NPCs[id] = addEgg;
eggCount++; eggCount++;
NPCManager::updateNPCPosition(id, (int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, 0); NPCManager::updateNPCPosition(id, (int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, 0);
@@ -716,12 +658,10 @@ static void loadEggs(json& eggData, int32_t* nextId) {
} }
} }
/* /*
* Load gruntwork output, if it exists * Load gruntwork output, if it exists
*/ */
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) { static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
if (gruntwork.is_null())
return;
try { try {
auto paths = gruntwork["paths"]; auto paths = gruntwork["paths"];
@@ -731,10 +671,10 @@ static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
std::vector<int32_t> targetIDs; std::vector<int32_t> targetIDs;
std::vector<int32_t> targetTypes; // target types are not exportable from gw, but load them anyway std::vector<int32_t> targetTypes; // target types are not exportable from gw, but load them anyway
std::vector<Vec3> pathPoints; std::vector<Vec3> pathPoints;
int speed = path.find("iBaseSpeed") == path.end() ? NPC_DEFAULT_SPEED : (int)path["iBaseSpeed"]; int speed = (int)path["iBaseSpeed"];
int taskID = path.find("iTaskID") == path.end() ? -1 : (int)path["iTaskID"]; int taskID = (int)path["iTaskID"];
bool relative = path.find("bRelative") == path.end() ? false : (bool)path["bRelative"]; bool relative = (bool)path["bRelative"];
bool loop = path.find("bLoop") == path.end() ? true : (bool)path["bLoop"]; // loop by default bool loop = (bool)path["bLoop"];
// target IDs // target IDs
for (json::iterator _tID = path["aNPCIDs"].begin(); _tID != path["aNPCIDs"].end(); _tID++) for (json::iterator _tID = path["aNPCIDs"].begin(); _tID != path["aNPCIDs"].end(); _tID++)
@@ -771,8 +711,8 @@ static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
} }
static void loadGruntworkPost(json& gruntwork, int32_t* nextId) { static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
if (gruntwork.is_null())
return; if (gruntwork.is_null()) return;
try { try {
// skyway paths // skyway paths
@@ -797,7 +737,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
if (NPCManager::NPCs.find(npcID) == NPCManager::NPCs.end()) if (NPCManager::NPCs.find(npcID) == NPCManager::NPCs.end())
continue; // NPC not found continue; // NPC not found
BaseNPC* npc = NPCManager::NPCs[npcID]; BaseNPC* npc = NPCManager::NPCs[npcID];
npc->angle = angle; npc->appearanceData.iAngle = angle;
RunningNPCRotations[npcID] = angle; RunningNPCRotations[npcID] = angle;
} }
@@ -810,8 +750,8 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
if (NPCManager::NPCs.find(npcID) == NPCManager::NPCs.end()) if (NPCManager::NPCs.find(npcID) == NPCManager::NPCs.end())
continue; // NPC not found continue; // NPC not found
BaseNPC* npc = NPCManager::NPCs[npcID]; BaseNPC* npc = NPCManager::NPCs[npcID];
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y,
npc->z, instanceID, npc->angle); npc->z, instanceID, npc->appearanceData.iAngle);
RunningNPCMapNumbers[npcID] = instanceID; RunningNPCMapNumbers[npcID] = instanceID;
} }
@@ -824,8 +764,6 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int id = (*nextId)--; int id = (*nextId)--;
uint64_t instanceID = mob.find("iMapNum") == mob.end() ? INSTANCE_OVERWORLD : (int)mob["iMapNum"]; uint64_t instanceID = mob.find("iMapNum") == mob.end() ? INSTANCE_OVERWORLD : (int)mob["iMapNum"];
ensureValidNPCType((int)mob["iNPCType"], settings::GRUNTWORKJSON);
if (NPCManager::NPCData[(int)mob["iNPCType"]]["m_iTeam"] == 2) { if (NPCManager::NPCData[(int)mob["iNPCType"]]["m_iTeam"] == 2) {
npc = new Mob(mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iNPCType"], npc = new Mob(mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iNPCType"],
NPCManager::NPCData[(int)mob["iNPCType"]], id); NPCManager::NPCData[(int)mob["iNPCType"]], id);
@@ -833,21 +771,18 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
// re-enable respawning // re-enable respawning
((Mob*)npc)->summoned = false; ((Mob*)npc)->summoned = false;
} else { } else {
npc = new BaseNPC(mob["iAngle"], instanceID, mob["iNPCType"], id); npc = new BaseNPC(mob["iX"], mob["iY"], mob["iZ"], mob["iAngle"], instanceID, mob["iNPCType"], id);
} }
NPCManager::NPCs[npc->id] = npc; NPCManager::NPCs[npc->appearanceData.iNPC_ID] = npc;
RunningMobs[npc->id] = npc; RunningMobs[npc->appearanceData.iNPC_ID] = npc;
NPCManager::updateNPCPosition(npc->id, mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iAngle"]); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iAngle"]);
} }
// mob groups // mob groups
auto groups = gruntwork["groups"]; auto groups = gruntwork["groups"];
for (auto _group = groups.begin(); _group != groups.end(); _group++) { for (auto _group = groups.begin(); _group != groups.end(); _group++) {
auto leader = _group.value(); auto leader = _group.value();
ensureValidNPCType((int)leader["iNPCType"], settings::GRUNTWORKJSON);
auto td = NPCManager::NPCData[(int)leader["iNPCType"]]; auto td = NPCManager::NPCData[(int)leader["iNPCType"]];
uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"]; uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"];
@@ -868,9 +803,6 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int followerCount = 0; int followerCount = 0;
for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) { for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) {
auto follower = _fol.value(); auto follower = _fol.value();
ensureValidNPCType((int)follower["iNPCType"], settings::GRUNTWORKJSON);
auto tdFol = NPCManager::NPCData[(int)follower["iNPCType"]]; auto tdFol = NPCManager::NPCData[(int)follower["iNPCType"]];
Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, *nextId); Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, *nextId);
@@ -882,7 +814,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"]; tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"];
tmpFol->offsetY = follower.find("iOffsetY") == follower.end() ? 0 : (int)follower["iOffsetY"]; tmpFol->offsetY = follower.find("iOffsetY") == follower.end() ? 0 : (int)follower["iOffsetY"];
tmpFol->groupLeader = tmp->id; tmpFol->groupLeader = tmp->appearanceData.iNPC_ID;
tmp->groupMember[followerCount++] = *nextId; tmp->groupMember[followerCount++] = *nextId;
(*nextId)--; (*nextId)--;
@@ -892,7 +824,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
std::cout << "[WARN] Mob group leader with ID " << *nextId << " has too many followers (" << followers.size() << ")\n"; std::cout << "[WARN] Mob group leader with ID " << *nextId << " has too many followers (" << followers.size() << ")\n";
} }
RunningGroups[tmp->id] = tmp; // store as running RunningGroups[tmp->appearanceData.iNPC_ID] = tmp; // store as running
} }
auto eggs = gruntwork["eggs"]; auto eggs = gruntwork["eggs"];
@@ -901,7 +833,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int id = (*nextId)--; int id = (*nextId)--;
uint64_t instanceID = egg.find("iMapNum") == egg.end() ? INSTANCE_OVERWORLD : (int)egg["iMapNum"]; uint64_t instanceID = egg.find("iMapNum") == egg.end() ? INSTANCE_OVERWORLD : (int)egg["iMapNum"];
Egg* addEgg = new Egg(instanceID, (int)egg["iType"], id, false); Egg* addEgg = new Egg((int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, (int)egg["iType"], id, false);
NPCManager::NPCs[id] = addEgg; NPCManager::NPCs[id] = addEgg;
NPCManager::updateNPCPosition(id, (int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, 0); NPCManager::updateNPCPosition(id, (int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, 0);
RunningEggs[id] = addEgg; RunningEggs[id] = addEgg;
@@ -927,15 +859,16 @@ static void loadNPCs(json& npcData) {
npcID += NPC_ID_OFFSET; npcID += NPC_ID_OFFSET;
int instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"]; int instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"];
int type = (int)npc["iNPCType"]; int type = (int)npc["iNPCType"];
if (NPCManager::NPCData[type].is_null()) {
ensureValidNPCType(type, settings::NPCJSON); std::cout << "[WARN] NPC type " << type << " not found; skipping (json#" << _npc.key() << ")" << std::endl;
continue;
}
#ifdef ACADEMY #ifdef ACADEMY
// do not spawn NPCs in the future // do not spawn NPCs in the future
if (npc["iX"] > 512000 && npc["iY"] < 256000) if (npc["iX"] > 512000 && npc["iY"] < 256000)
continue; continue;
#endif #endif
BaseNPC* tmp = new BaseNPC(npc["iAngle"], instanceID, type, npcID); BaseNPC* tmp = new BaseNPC(npc["iX"], npc["iY"], npc["iZ"], npc["iAngle"], instanceID, type, npcID);
NPCManager::NPCs[npcID] = tmp; NPCManager::NPCs[npcID] = tmp;
NPCManager::updateNPCPosition(npcID, npc["iX"], npc["iY"], npc["iZ"], instanceID, npc["iAngle"]); NPCManager::updateNPCPosition(npcID, npc["iX"], npc["iY"], npc["iZ"], instanceID, npc["iAngle"]);
@@ -971,9 +904,10 @@ static void loadMobs(json& npcData, int32_t* nextId) {
int npcID = std::strtol(_npc.key().c_str(), nullptr, 10); // parse ID string to integer int npcID = std::strtol(_npc.key().c_str(), nullptr, 10); // parse ID string to integer
npcID += MOB_ID_OFFSET; npcID += MOB_ID_OFFSET;
int type = (int)npc["iNPCType"]; int type = (int)npc["iNPCType"];
if (NPCManager::NPCData[type].is_null()) {
ensureValidNPCType(type, settings::MOBJSON); std::cout << "[WARN] NPC type " << type << " not found; skipping (json#" << _npc.key() << ")" << std::endl;
continue;
}
auto td = NPCManager::NPCData[type]; auto td = NPCManager::NPCData[type];
uint64_t instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"]; uint64_t instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"];
@@ -1001,10 +935,7 @@ static void loadMobs(json& npcData, int32_t* nextId) {
for (json::iterator _group = groupData.begin(); _group != groupData.end(); _group++) { for (json::iterator _group = groupData.begin(); _group != groupData.end(); _group++) {
auto leader = _group.value(); auto leader = _group.value();
int leadID = std::strtol(_group.key().c_str(), nullptr, 10); // parse ID string to integer int leadID = std::strtol(_group.key().c_str(), nullptr, 10); // parse ID string to integer
leadID += MOB_GROUP_ID_OFFSET; leadID += MOB_GROUP_ID_OFFSET;
ensureValidNPCType(leader["iNPCType"], settings::MOBJSON);
auto td = NPCManager::NPCData[(int)leader["iNPCType"]]; auto td = NPCManager::NPCData[(int)leader["iNPCType"]];
uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"]; uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"];
auto followers = leader["aFollowers"]; auto followers = leader["aFollowers"];
@@ -1034,9 +965,6 @@ static void loadMobs(json& npcData, int32_t* nextId) {
int followerCount = 0; int followerCount = 0;
for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) { for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) {
auto follower = _fol.value(); auto follower = _fol.value();
ensureValidNPCType(follower["iNPCType"], settings::MOBJSON);
auto tdFol = NPCManager::NPCData[(int)follower["iNPCType"]]; auto tdFol = NPCManager::NPCData[(int)follower["iNPCType"]];
Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, *nextId); Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, *nextId);
@@ -1045,7 +973,7 @@ static void loadMobs(json& npcData, int32_t* nextId) {
tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"]; tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"];
tmpFol->offsetY = follower.find("iOffsetY") == follower.end() ? 0 : (int)follower["iOffsetY"]; tmpFol->offsetY = follower.find("iOffsetY") == follower.end() ? 0 : (int)follower["iOffsetY"];
tmpFol->groupLeader = tmp->id; tmpFol->groupLeader = tmp->appearanceData.iNPC_ID;
tmp->groupMember[followerCount++] = *nextId; tmp->groupMember[followerCount++] = *nextId;
(*nextId)--; (*nextId)--;
@@ -1142,39 +1070,19 @@ void TableData::init() {
}; };
// load JSON data into tables // load JSON data into tables
std::ifstream fstream;
for (int i = 0; i < 7; i++) { for (int i = 0; i < 7; i++) {
std::pair<json*, std::string>& table = tables[i]; std::pair<json*, std::string>& table = tables[i];
fstream.open(settings::TDATADIR + table.second); // open file
// scope for fstream if (!fstream.fail()) {
{ fstream >> *table.first; // load file contents into table
std::ifstream fstream; } else {
fstream.open(settings::TDATADIR + "/" + table.second); // open file if (table.first != &gruntwork) { // gruntwork isn't critical
std::cerr << "[FATAL] Critical tdata file missing: " << settings::TDATADIR << table.second << std::endl;
// did we fail to open the file?
if (fstream.fail()) {
// gruntwork isn't critical
if (table.first == &gruntwork)
continue;
std::cerr << "[FATAL] Critical tdata file missing: " << table.second << std::endl;
exit(1); exit(1);
} }
// is the file empty?
if (fstream.peek() == std::ifstream::traits_type::eof()) {
// tolerate empty gruntwork file
if (table.first == &gruntwork) {
std::cout << "[WARN] The gruntwork file is empty" << std::endl;
continue;
}
std::cerr << "[FATAL] Critical tdata file is empty: " << table.second << std::endl;
exit(1);
}
// load file contents into table
fstream >> *table.first;
} }
fstream.close();
// patching: load each patch directory specified in the config file // patching: load each patch directory specified in the config file
@@ -1189,11 +1097,11 @@ void TableData::init() {
std::string patchModuleName = *it; std::string patchModuleName = *it;
std::string patchFile = settings::PATCHDIR + patchModuleName + "/" + table.second; std::string patchFile = settings::PATCHDIR + patchModuleName + "/" + table.second;
try { try {
std::ifstream fstream;
fstream.open(patchFile); fstream.open(patchFile);
fstream >> patch; // load into temporary json object fstream >> patch; // load into temporary json object
std::cout << "[INFO] Patching " << patchFile << std::endl; std::cout << "[INFO] Patching " << patchFile << std::endl;
patchJSON(table.first, &patch); // patch patchJSON(table.first, &patch); // patch
fstream.close();
} catch (const std::exception& err) { } catch (const std::exception& err) {
// no-op // no-op
} }
@@ -1219,7 +1127,7 @@ void TableData::init() {
* Write gruntwork output to file * Write gruntwork output to file
*/ */
void TableData::flush() { void TableData::flush() {
std::ofstream file(settings::TDATADIR + "/" + settings::GRUNTWORKJSON); std::ofstream file(settings::TDATADIR + settings::GRUNTWORKJSON);
json gruntwork; json gruntwork;
for (auto& pair : RunningSkywayRoutes) { for (auto& pair : RunningSkywayRoutes) {
@@ -1268,7 +1176,7 @@ void TableData::flush() {
continue; continue;
int x, y, z; int x, y, z;
if (npc->kind == EntityKind::MOB) { if (npc->type == EntityType::MOB) {
Mob *m = (Mob*)npc; Mob *m = (Mob*)npc;
x = m->spawnX; x = m->spawnX;
y = m->spawnY; y = m->spawnY;
@@ -1280,13 +1188,13 @@ void TableData::flush() {
} }
// NOTE: this format deviates slightly from the one in mobs.json // NOTE: this format deviates slightly from the one in mobs.json
mob["iNPCType"] = (int)npc->type; mob["iNPCType"] = (int)npc->appearanceData.iNPCType;
mob["iX"] = x; mob["iX"] = x;
mob["iY"] = y; mob["iY"] = y;
mob["iZ"] = z; mob["iZ"] = z;
mob["iMapNum"] = MAPNUM(npc->instanceID); mob["iMapNum"] = MAPNUM(npc->instanceID);
// this is a bit imperfect, since this is a live angle, not a spawn angle so it'll change often, but eh // this is a bit imperfect, since this is a live angle, not a spawn angle so it'll change often, but eh
mob["iAngle"] = npc->angle; mob["iAngle"] = npc->appearanceData.iAngle;
// it's called mobs, but really it's everything // it's called mobs, but really it's everything
gruntwork["mobs"].push_back(mob); gruntwork["mobs"].push_back(mob);
@@ -1301,19 +1209,19 @@ void TableData::flush() {
int x, y, z; int x, y, z;
std::vector<Mob*> followers; std::vector<Mob*> followers;
if (npc->kind == EntityKind::MOB) { if (npc->type == EntityType::MOB) {
Mob* m = (Mob*)npc; Mob* m = (Mob*)npc;
x = m->spawnX; x = m->spawnX;
y = m->spawnY; y = m->spawnY;
z = m->spawnZ; z = m->spawnZ;
if (m->groupLeader != m->id) { // make sure this is a leader if (m->groupLeader != m->appearanceData.iNPC_ID) { // make sure this is a leader
std::cout << "[WARN] Non-leader mob found in running groups; ignoring\n"; std::cout << "[WARN] Non-leader mob found in running groups; ignoring\n";
continue; continue;
} }
// add follower data to vector; go until OOB or until follower ID is 0 // add follower data to vector; go until OOB or until follower ID is 0
for (int i = 0; i < 4 && m->groupMember[i] > 0; i++) { for (int i = 0; i < 4 && m->groupMember[i] > 0; i++) {
if (NPCManager::NPCs.find(m->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[m->groupMember[i]]->kind != EntityKind::MOB) { if (NPCManager::NPCs.find(m->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[m->groupMember[i]]->type != EntityType::MOB) {
std::cout << "[WARN] Follower with ID " << m->groupMember[i] << " not found; skipping\n"; std::cout << "[WARN] Follower with ID " << m->groupMember[i] << " not found; skipping\n";
continue; continue;
} }
@@ -1327,13 +1235,13 @@ void TableData::flush() {
} }
// NOTE: this format deviates slightly from the one in mobs.json // NOTE: this format deviates slightly from the one in mobs.json
mob["iNPCType"] = (int)npc->type; mob["iNPCType"] = (int)npc->appearanceData.iNPCType;
mob["iX"] = x; mob["iX"] = x;
mob["iY"] = y; mob["iY"] = y;
mob["iZ"] = z; mob["iZ"] = z;
mob["iMapNum"] = MAPNUM(npc->instanceID); mob["iMapNum"] = MAPNUM(npc->instanceID);
// this is a bit imperfect, since this is a live angle, not a spawn angle so it'll change often, but eh // this is a bit imperfect, since this is a live angle, not a spawn angle so it'll change often, but eh
mob["iAngle"] = npc->angle; mob["iAngle"] = npc->appearanceData.iAngle;
// followers // followers
while (followers.size() > 0) { while (followers.size() > 0) {
@@ -1342,7 +1250,7 @@ void TableData::flush() {
// populate JSON entry // populate JSON entry
json fol; json fol;
fol["iNPCType"] = follower->type; fol["iNPCType"] = follower->appearanceData.iNPCType;
fol["iOffsetX"] = follower->offsetX; fol["iOffsetX"] = follower->offsetX;
fol["iOffsetY"] = follower->offsetY; fol["iOffsetY"] = follower->offsetY;
@@ -1367,7 +1275,7 @@ void TableData::flush() {
int mapnum = MAPNUM(npc->instanceID); int mapnum = MAPNUM(npc->instanceID);
if (mapnum != 0) if (mapnum != 0)
egg["iMapNum"] = mapnum; egg["iMapNum"] = mapnum;
egg["iType"] = npc->type; egg["iType"] = npc->appearanceData.iNPCType;
gruntwork["eggs"].push_back(egg); gruntwork["eggs"].push_back(egg);
} }
@@ -1391,7 +1299,7 @@ void TableData::flush() {
targetIDs.push_back(tID); targetIDs.push_back(tID);
for (int32_t tType : path.targetTypes) for (int32_t tType : path.targetTypes)
targetTypes.push_back(tType); targetTypes.push_back(tType);
pathObj["iBaseSpeed"] = path.speed; pathObj["iBaseSpeed"] = path.speed;
pathObj["iTaskID"] = path.escortTaskID; pathObj["iTaskID"] = path.escortTaskID;
pathObj["bRelative"] = path.isRelative; pathObj["bRelative"] = path.isRelative;

View File

@@ -1,18 +1,13 @@
#pragma once #pragma once
#include "JSON.hpp"
#include "Entities.hpp"
#include "Transport.hpp"
#include <map> #include <map>
#include <unordered_map>
#include <vector> #include "NPCManager.hpp"
// these are added to the NPC's static key to avoid collisions // these are added to the NPC's static key to avoid collisions
const int NPC_ID_OFFSET = 1; const int NPC_ID_OFFSET = 1;
const int MOB_ID_OFFSET = 10000; const int MOB_ID_OFFSET = 10000;
const int MOB_GROUP_ID_OFFSET = 30000; const int MOB_GROUP_ID_OFFSET = 20000;
// typedef for JSON object because I don't want to type nlohmann::json every time // typedef for JSON object because I don't want to type nlohmann::json every time
typedef nlohmann::json json; typedef nlohmann::json json;

View File

@@ -1,9 +1,5 @@
#include "Trading.hpp" #include "Trading.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "db/Database.hpp"
using namespace Trading; using namespace Trading;
@@ -186,36 +182,6 @@ 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)); 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) { static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf; sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf;
@@ -230,7 +196,7 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
Player* plr2 = PlayerManager::getPlayer(otherSock); Player* plr2 = PlayerManager::getPlayer(otherSock);
if (!(plr->isTrading && plr2->isTrading)) { // both players must be trading if (!(plr->isTrading && plr2->isTrading)) { // both players must be trading
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp); INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp);
resp.iID_Request = plr2->iID; resp.iID_Request = plr2->iID;
@@ -239,13 +205,6 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT)); sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
resp.iID_Request = plr->iID; resp.iID_Request = plr->iID;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT)); otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
// both players are no longer trading
plr->isTrading = false;
plr2->isTrading = false;
plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
return; return;
} }
@@ -273,16 +232,14 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
resp2.iID_Request = pacdat->iID_Request; resp2.iID_Request = pacdat->iID_Request;
resp2.iID_From = pacdat->iID_From; resp2.iID_From = pacdat->iID_From;
resp2.iID_To = pacdat->iID_To; resp2.iID_To = pacdat->iID_To;
plr->subtractCapped(CappedValueType::TAROS, plr->moneyInTrade); plr->money = plr->money + plr2->moneyInTrade - plr->moneyInTrade;
plr->addCapped(CappedValueType::TAROS, plr2->moneyInTrade);
resp2.iCandy = plr->money; resp2.iCandy = plr->money;
memcpy(resp2.Item, plr2->Trade, sizeof(plr2->Trade)); memcpy(resp2.Item, plr2->Trade, sizeof(plr2->Trade));
memcpy(resp2.ItemStay, plr->Trade, sizeof(plr->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)); sock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
plr2->subtractCapped(CappedValueType::TAROS, plr2->moneyInTrade); plr2->money = plr2->money + plr->moneyInTrade - plr2->moneyInTrade;
plr2->addCapped(CappedValueType::TAROS, plr->moneyInTrade);
resp2.iCandy = plr2->money; resp2.iCandy = plr2->money;
memcpy(resp2.Item, plr->Trade, sizeof(plr->Trade)); memcpy(resp2.Item, plr->Trade, sizeof(plr->Trade));
memcpy(resp2.ItemStay, plr2->Trade, sizeof(plr2->Trade)); memcpy(resp2.ItemStay, plr2->Trade, sizeof(plr2->Trade));
@@ -290,23 +247,14 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
otherSock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC)); otherSock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
} else { } else {
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp); INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp);
resp.iID_Request = plr->iID; resp.iID_Request = plr2->iID;
resp.iID_From = pacdat->iID_From; resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To; resp.iID_To = pacdat->iID_To;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT)); sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
resp.iID_Request = plr2->iID; resp.iID_Request = plr->iID;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT)); otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Trade Failed";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 3;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
otherSock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
return; return;
} }
Database::commitTrade(plr, plr2);
} }
static void tradeConfirmCancel(CNSocket* sock, CNPacketData* data) { static void tradeConfirmCancel(CNSocket* sock, CNPacketData* data) {
@@ -353,14 +301,12 @@ static void tradeRegisterItem(CNSocket* sock, CNPacketData* data) {
return; return;
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
Player* plr2 = PlayerManager::getPlayer(otherSock);
plr->Trade[pacdat->Item.iSlotNum] = pacdat->Item; plr->Trade[pacdat->Item.iSlotNum] = pacdat->Item;
plr->isTradeConfirm = false; plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
// since you can spread items like gumballs over multiple slots, we need to count them all // since you can spread items like gumballs over multiple slots, we need to count them all
// to make sure the inventory shows the right value during trade. // to make sure the inventory shows the right value during trade.
int count = 0; int count = 0;
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
if (plr->Trade[i].iInvenNum == pacdat->Item.iInvenNum) if (plr->Trade[i].iInvenNum == pacdat->Item.iInvenNum)
count += plr->Trade[i].iOpt; count += plr->Trade[i].iOpt;
@@ -397,9 +343,7 @@ static void tradeUnregisterItem(CNSocket* sock, CNPacketData* data) {
return; return;
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
Player* plr2 = PlayerManager::getPlayer(otherSock);
plr->isTradeConfirm = false; plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, resp);
resp.iID_Request = pacdat->iID_Request; resp.iID_Request = pacdat->iID_Request;
@@ -412,7 +356,7 @@ static void tradeUnregisterItem(CNSocket* sock, CNPacketData* data) {
// since you can spread items like gumballs over multiple slots, we need to count them all // since you can spread items like gumballs over multiple slots, we need to count them all
// to make sure the inventory shows the right value during trade. // to make sure the inventory shows the right value during trade.
int count = 0; int count = 0;
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
if (plr->Trade[i].iInvenNum == resp.InvenItem.iInvenNum) if (plr->Trade[i].iInvenNum == resp.InvenItem.iInvenNum)
count += plr->Trade[i].iOpt; count += plr->Trade[i].iOpt;
@@ -430,7 +374,7 @@ static void tradeRegisterCash(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
if (pacdat->iCandy < 0 || pacdat->iCandy > plr->money) if (pacdat->iCandy < 0 || pacdat->iCandy > plr->money)
return; // famous glitch, begone return; // famous glitch, begone
CNSocket* otherSock; // weird flip flop because we need to know who the other player is CNSocket* otherSock; // weird flip flop because we need to know who the other player is
if (pacdat->iID_Request == pacdat->iID_From) if (pacdat->iID_Request == pacdat->iID_From)
@@ -441,10 +385,6 @@ static void tradeRegisterCash(CNSocket* sock, CNPacketData* data) {
if (otherSock == nullptr) if (otherSock == nullptr)
return; return;
Player* plr2 = PlayerManager::getPlayer(otherSock);
plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, resp);
resp.iID_Request = pacdat->iID_Request; resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From; resp.iID_From = pacdat->iID_From;
@@ -453,7 +393,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)); 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)); otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
plr->setCapped(CappedValueType::TAROS_IN_TRADE, pacdat->iCandy); plr->moneyInTrade = pacdat->iCandy;
plr->isTradeConfirm = false; plr->isTradeConfirm = false;
} }
@@ -462,8 +402,6 @@ void Trading::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, tradeOffer); 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_ACCEPT, tradeOfferAccept);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, tradeOfferRefusal); 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, tradeConfirm);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem);

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include "Items.hpp"
namespace Trading { namespace Trading {
void init(); void init();
} }

View File

@@ -1,12 +1,9 @@
#include "Transport.hpp"
#include "servers/CNShardServer.hpp" #include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Nanos.hpp" #include "Nanos.hpp"
#include "Transport.hpp"
#include "TableData.hpp" #include "TableData.hpp"
#include "Entities.hpp" #include "Combat.hpp"
#include "NPCManager.hpp"
#include "MobAI.hpp" #include "MobAI.hpp"
#include <unordered_map> #include <unordered_map>
@@ -117,7 +114,7 @@ static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
} }
TransportRoute route = Routes[req->iTransporationID]; TransportRoute route = Routes[req->iTransporationID];
plr->subtractCapped(CappedValueType::TAROS, route.cost); plr->money -= route.cost;
TransportLocation* target = nullptr; TransportLocation* target = nullptr;
switch (route.type) { switch (route.type) {
@@ -143,7 +140,7 @@ static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
} }
// refund and send alert packet // refund and send alert packet
plr->addCapped(CappedValueType::TAROS, route.cost); plr->money += route.cost;
INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert); INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert);
alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog
alert.iDuringTime = 3; alert.iDuringTime = 3;
@@ -168,9 +165,9 @@ static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
if (target == nullptr) if (target == nullptr)
return; return;
// we warped; update position and chunks // we warped; update position and chunks
PlayerManager::updatePlayerPositionForWarp(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD); Chunking::updateEntityChunk({sock}, plr->chunkPos, std::make_tuple(0, 0, 0)); // force player to reload chunks
PlayerManager::updatePlayerPosition(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD, plr->angle);
} }
void Transport::testMssRoute(CNSocket *sock, std::vector<Vec3>* route) { void Transport::testMssRoute(CNSocket *sock, std::vector<Vec3>* route) {
@@ -260,13 +257,13 @@ static void stepNPCPathing() {
} }
// skip if not simulating mobs // skip if not simulating mobs
if (npc->kind == EntityKind::MOB && !MobAI::simulateMobs) { if (npc->type == EntityType::MOB && !MobAI::simulateMobs) {
it++; it++;
continue; continue;
} }
// do not roam if not roaming // do not roam if not roaming
if (npc->kind == EntityKind::MOB && ((Mob*)npc)->state != AIState::ROAMING) { if (npc->type == EntityType::MOB && ((Mob*)npc)->state != MobState::ROAMING) {
it++; it++;
continue; continue;
} }
@@ -279,15 +276,15 @@ static void stepNPCPathing() {
int distanceBetween = hypot(dXY, point.z - npc->z); // total distance int distanceBetween = hypot(dXY, point.z - npc->z); // total distance
// update NPC location to update viewables // update NPC location to update viewables
NPCManager::updateNPCPosition(npc->id, point.x, point.y, point.z, npc->instanceID, npc->angle); NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, point.x, point.y, point.z, npc->instanceID, npc->appearanceData.iAngle);
// TODO: move walking logic into Entity stack // TODO: move walking logic into Entity stack
switch (npc->kind) { switch (npc->type) {
case EntityKind::BUS: case EntityType::BUS:
INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove); INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove);
busMove.eTT = 3; busMove.eTT = 3;
busMove.iT_ID = npc->id; busMove.iT_ID = npc->appearanceData.iNPC_ID;
busMove.iMoveStyle = 0; // ??? busMove.iMoveStyle = 0; // ???
busMove.iToX = point.x; busMove.iToX = point.x;
busMove.iToY = point.y; busMove.iToY = point.y;
@@ -296,12 +293,12 @@ static void stepNPCPathing() {
NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE)); NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE));
break; break;
case EntityKind::MOB: case EntityType::MOB:
MobAI::incNextMovement((Mob*)npc); MobAI::incNextMovement((Mob*)npc);
/* fallthrough */ /* fallthrough */
default: default:
INITSTRUCT(sP_FE2CL_NPC_MOVE, move); INITSTRUCT(sP_FE2CL_NPC_MOVE, move);
move.iNPC_ID = npc->id; move.iNPC_ID = npc->appearanceData.iNPC_ID;
move.iMoveStyle = 0; // ??? move.iMoveStyle = 0; // ???
move.iToX = point.x; move.iToX = point.x;
move.iToY = point.y; move.iToY = point.y;
@@ -388,21 +385,8 @@ NPCPath* Transport::findApplicablePath(int32_t id, int32_t type, int taskID) {
void Transport::constructPathNPC(int32_t id, NPCPath* path) { void Transport::constructPathNPC(int32_t id, NPCPath* path) {
BaseNPC* npc = NPCManager::NPCs[id]; BaseNPC* npc = NPCManager::NPCs[id];
if (npc->type == EntityType::MOB)
if (npc->kind == EntityKind::MOB) { ((Mob*)(npc))->staticPath = true;
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; npc->loopingPath = path->isLoop;
// Interpolate // Interpolate

View File

@@ -1,11 +1,8 @@
#pragma once #pragma once
#include "core/Core.hpp" #include "servers/CNShardServer.hpp"
#include <unordered_map> #include <unordered_map>
#include <map>
#include <vector>
#include <queue>
const int SLIDER_SPEED = 1200; const int SLIDER_SPEED = 1200;
const int SLIDER_STOP_TICKS = 16; const int SLIDER_STOP_TICKS = 16;

View File

@@ -1,14 +1,6 @@
#include "Vendors.hpp" #include "Vendors.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Rand.hpp" #include "Rand.hpp"
// 7 days
#define VEHICLE_EXPIRY_DURATION 604800
using namespace Vendors; using namespace Vendors;
std::map<int32_t, std::vector<VendorListing>> Vendors::VendorTables; std::map<int32_t, std::vector<VendorListing>> Vendors::VendorTables;
@@ -21,25 +13,8 @@ static void vendorBuy(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp);
failResp.iErrorCode = 0; failResp.iErrorCode = 0;
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end()) {
std::cout << "[WARN] Vendor with ID " << req->iVendorID << " mismatched or not found (buy)" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
return;
}
std::vector<VendorListing>* listings = &Vendors::VendorTables[req->iVendorID];
VendorListing reqItem;
reqItem.id = req->Item.iID;
reqItem.type = req->Item.iType;
reqItem.sort = 0; // just to be safe
if (std::find(listings->begin(), listings->end(), reqItem) == listings->end()) { // item not found in listing
std::cout << "[WARN] Player " << PlayerManager::getPlayerName(plr) << " tried to buy an item that wasn't on sale" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
return;
}
Items::Item* itemDat = Items::getItemData(req->Item.iID, req->Item.iType); Items::Item* itemDat = Items::getItemData(req->Item.iID, req->Item.iType);
if (itemDat == nullptr) { if (itemDat == nullptr) {
std::cout << "[WARN] Item id " << req->Item.iID << " with type " << req->Item.iType << " not found (buy)" << std::endl; std::cout << "[WARN] Item id " << req->Item.iID << " with type " << req->Item.iType << " not found (buy)" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL); sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
@@ -61,8 +36,8 @@ static void vendorBuy(CNSocket* sock, CNPacketData* data) {
// if vehicle // if vehicle
if (req->Item.iType == 10) { if (req->Item.iType == 10) {
// set time limit: current time + expiry duration // set time limit: current time + 7days
req->Item.iTimeLimit = getTimestamp() + VEHICLE_EXPIRY_DURATION; req->Item.iTimeLimit = getTimestamp() + 604800;
} }
if (slot != req->iInvenSlotNum) { if (slot != req->iInvenSlotNum) {
@@ -72,7 +47,7 @@ static void vendorBuy(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC, resp);
plr->subtractCapped(CappedValueType::TAROS, itemCost); plr->money = plr->money - itemCost;
plr->Inven[slot] = req->Item; plr->Inven[slot] = req->Item;
resp.iCandy = plr->money; resp.iCandy = plr->money;
@@ -117,7 +92,7 @@ static void vendorSell(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, resp);
// increment taros // increment taros
plr->addCapped(CappedValueType::TAROS, itemData->sellPrice * req->iItemCnt); plr->money += itemData->sellPrice * req->iItemCnt;
// modify item // modify item
if (item->iOpt - req->iItemCnt > 0) { // selling part of a stack if (item->iOpt - req->iItemCnt > 0) { // selling part of a stack
@@ -209,7 +184,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; std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl;
} }
plr->subtractCapped(CappedValueType::TAROS, itemCost); plr->money = plr->money - itemCost;
plr->Inven[slot] = item; plr->Inven[slot] = item;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp);
@@ -227,25 +202,17 @@ static void vendorTable(CNSocket* sock, CNPacketData* data) {
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end()) if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end())
return; return;
std::vector<VendorListing>& listings = Vendors::VendorTables[req->iVendorID]; std::vector<VendorListing> listings = Vendors::VendorTables[req->iVendorID];
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, resp);
for (int i = 0; i < (int)listings.size() && i < 20; i++) { // 20 is the max for (int i = 0; i < (int)listings.size() && i < 20; i++) { // 20 is the max
sItemBase base = {}; sItemBase base;
base.iID = listings[i].id; base.iID = listings[i].iID;
base.iOpt = 0;
base.iTimeLimit = 0;
base.iType = listings[i].type; base.iType = listings[i].type;
/*
* Set vehicle expiry value.
*
* Note: sItemBase.iTimeLimit in the context of vendor listings contains
* a duration, unlike in most other contexts where it contains the
* expiration timestamp.
*/
if (listings[i].type == 10)
base.iTimeLimit = VEHICLE_EXPIRY_DURATION;
sItemVendor vItem; sItemVendor vItem;
vItem.item = base; vItem.item = base;
vItem.iSortNum = listings[i].sort; vItem.iSortNum = listings[i].sort;
@@ -273,7 +240,7 @@ static void vendorBuyBattery(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
int cost = req->Item.iOpt * 100; int cost = req->Item.iOpt * 100;
if ((req->Item.iID == 3 ? (plr->batteryW >= PC_BATTERY_MAX) : (plr->batteryN >= PC_BATTERY_MAX)) || plr->money < cost || req->Item.iOpt < 0) { // sanity check if ((req->Item.iID == 3 ? (plr->batteryW >= 9999) : (plr->batteryN >= 9999)) || plr->money < cost || req->Item.iOpt < 0) { // sanity check
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp);
failResp.iErrorCode = 0; failResp.iErrorCode = 0;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL); sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL);
@@ -281,11 +248,17 @@ static void vendorBuyBattery(CNSocket* sock, CNPacketData* data) {
} }
cost = plr->batteryW + plr->batteryN; cost = plr->batteryW + plr->batteryN;
plr->addCapped(CappedValueType::BATTERY_W, req->Item.iID == 3 ? req->Item.iOpt * 100 : 0); plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 100 : 0;
plr->addCapped(CappedValueType::BATTERY_N, req->Item.iID == 4 ? 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;
cost = plr->batteryW + plr->batteryN - cost; cost = plr->batteryW + plr->batteryN - cost;
plr->subtractCapped(CappedValueType::TAROS, cost); plr->money -= cost;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp);
@@ -358,7 +331,7 @@ static void vendorCombineItems(CNSocket* sock, CNPacketData* data) {
float rolled = Rand::randFloat(100.0f); // success chance out of 100 float rolled = Rand::randFloat(100.0f); // success chance out of 100
//std::cout << rolled << " vs " << successChance << std::endl; //std::cout << rolled << " vs " << successChance << std::endl;
plr->subtractCapped(CappedValueType::TAROS, cost); plr->money -= cost;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, resp);

View File

@@ -1,17 +1,13 @@
#pragma once #pragma once
#include "core/Core.hpp" #include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include <vector> #include "Items.hpp"
#include <map> #include "PlayerManager.hpp"
struct VendorListing { struct VendorListing {
int sort, type, id; int sort, type, iID;
// when validating a listing, we don't really care about the sorting index
bool operator==(const VendorListing& other) const {
return type == other.type && id == other.id;
}
}; };
namespace Vendors { namespace Vendors {

View File

@@ -41,8 +41,7 @@ int CNSocketEncryption::xorData(uint8_t* buffer, uint8_t* key, int size) {
uint64_t CNSocketEncryption::createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2) { uint64_t CNSocketEncryption::createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2) {
uint64_t num = (uint64_t)(iv1 + 1); uint64_t num = (uint64_t)(iv1 + 1);
uint64_t num2 = (uint64_t)(iv2 + 1); uint64_t num2 = (uint64_t)(iv2 + 1);
uint64_t dEKey; uint64_t dEKey = (uint64_t)(*(uint64_t*)&defaultKey[0]);
memcpy(&dEKey, defaultKey, sizeof(dEKey));
return dEKey * (uTime * num * num2); return dEKey * (uTime * num * num2);
} }
@@ -66,7 +65,7 @@ CNPacketData::CNPacketData(void *b, uint32_t t, int l, int trnum, void *trs):
// ========================================================[[ CNSocket ]]======================================================== // ========================================================[[ CNSocket ]]========================================================
CNSocket::CNSocket(SOCKET s, struct sockaddr_in &addr, PacketHandler ph): sock(s), sockaddr(addr), pHandler(ph) { CNSocket::CNSocket(SOCKET s, struct sockaddr_in &addr, PacketHandler ph): sock(s), sockaddr(addr), pHandler(ph) {
memcpy(&EKey, CNSocketEncryption::defaultKey, sizeof(EKey)); EKey = (uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]);
} }
bool CNSocket::sendData(uint8_t* data, int size) { bool CNSocket::sendData(uint8_t* data, int size) {
@@ -110,11 +109,7 @@ bool CNSocket::isAlive() {
} }
void CNSocket::kill() { void CNSocket::kill() {
if (!alive)
return;
alive = false; alive = false;
#ifdef _WIN32 #ifdef _WIN32
shutdown(sock, SD_BOTH); shutdown(sock, SD_BOTH);
closesocket(sock); closesocket(sock);
@@ -246,10 +241,9 @@ void CNSocket::step() {
if (readSize <= 0) { if (readSize <= 0) {
// we aren't reading a packet yet, try to start looking for one // we aren't reading a packet yet, try to start looking for one
int recved = recv(sock, (buffer_t*)readBuffer, sizeof(int32_t), 0); int recved = recv(sock, (buffer_t*)readBuffer, sizeof(int32_t), 0);
if (recved >= 0 && recved < sizeof(int32_t)) { if (recved == 0) {
// too little data for readSize or the socket was closed normally (when 0 bytes were read) // the socket was closed normally
kill(); kill();
return;
} else if (!SOCKETERROR(recved)) { } else if (!SOCKETERROR(recved)) {
// we got our packet size!!!! // we got our packet size!!!!
readSize = *((int32_t*)readBuffer); readSize = *((int32_t*)readBuffer);
@@ -270,12 +264,11 @@ void CNSocket::step() {
} }
if (readSize > 0 && readBufferIndex < readSize) { if (readSize > 0 && readBufferIndex < readSize) {
// read until the end of the packet (or at least try to) // read until the end of the packet! (or at least try too)
int recved = recv(sock, (buffer_t*)(readBuffer + readBufferIndex), readSize - readBufferIndex, 0); int recved = recv(sock, (buffer_t*)(readBuffer + readBufferIndex), readSize - readBufferIndex, 0);
if (recved == 0) { if (recved == 0) {
// the socket was closed normally // the socket was closed normally
kill(); kill();
return;
} else if (!SOCKETERROR(recved)) } else if (!SOCKETERROR(recved))
readBufferIndex += recved; readBufferIndex += recved;
else if (OF_ERRNO != OF_EWOULD) { else if (OF_ERRNO != OF_EWOULD) {
@@ -365,8 +358,14 @@ void CNServer::init() {
} }
// attach socket to the port // attach socket to the port
if (!setSocketOption(sock, SOL_SOCKET, SO_REUSEADDR, 1)) { 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
std::cerr << "[FATAL] OpenFusion: setsockopt failed" << std::endl; std::cerr << "[FATAL] OpenFusion: setsockopt failed" << std::endl;
printSocketError("setsockopt");
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
address.sin_family = AF_INET; address.sin_family = AF_INET;
@@ -408,25 +407,13 @@ void CNServer::init() {
CNServer::CNServer() {}; CNServer::CNServer() {};
CNServer::CNServer(uint16_t p): port(p) {} 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) { void CNServer::addPollFD(SOCKET s) {
fds.push_back({s, POLLIN}); fds.push_back({s, POLLIN});
} }
void CNServer::removePollFD(int fd) { void CNServer::removePollFD(int i) {
auto it = fds.begin(); auto it = fds.begin();
while (it != fds.end() && it->fd != fd) while (it != fds.end() && it->fd != fds[i].fd)
it++; it++;
assert(it != fds.end()); assert(it != fds.end());
@@ -434,7 +421,7 @@ void CNServer::removePollFD(int fd) {
} }
void CNServer::start() { void CNServer::start() {
std::cout << "Starting " << serverType << " server at *:" << port << std::endl; std::cout << "Starting server at *:" << port << std::endl;
while (active) { while (active) {
// the timeout is to ensure shard timers are ticking // the timeout is to ensure shard timers are ticking
int n = poll(fds.data(), fds.size(), 50); int n = poll(fds.data(), fds.size(), 50);
@@ -468,13 +455,10 @@ void CNServer::start() {
continue; 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)) if (!setSockNonblocking(sock, newConnectionSocket))
continue; continue;
std::cout << "New " << serverType << " connection! " << inet_ntoa(address.sin_addr) << std::endl; std::cout << "New connection! " << inet_ntoa(address.sin_addr) << std::endl;
addPollFD(newConnectionSocket); addPollFD(newConnectionSocket);
@@ -489,15 +473,10 @@ void CNServer::start() {
} else { } else {
std::lock_guard<std::mutex> lock(activeCrit); // protect operations on connections std::lock_guard<std::mutex> lock(activeCrit); // protect operations on connections
// halt packet handling if server is shutting down
if (!active)
return;
// player sockets // player sockets
if (connections.find(fds[i].fd) == connections.end()) { if (connections.find(fds[i].fd) == connections.end()) {
std::cout << "[FATAL] Event on non-existent socket: " << fds[i].fd << std::endl; std::cout << "[WARN] Event on non-existant socket?" << std::endl;
assert(0); continue; // just to be safe
/* not reached */
} }
CNSocket* cSock = connections[fds[i].fd]; CNSocket* cSock = connections[fds[i].fd];
@@ -506,29 +485,22 @@ void CNServer::start() {
if (fds[i].revents & ~POLLIN) if (fds[i].revents & ~POLLIN)
cSock->kill(); cSock->kill();
if (cSock->isAlive()) if (cSock->isAlive()) {
cSock->step(); cSock->step();
} else {
killConnection(cSock);
connections.erase(fds[i].fd);
delete cSock;
removePollFD(i);
// a new entry was moved to this position, so we check it again
i--;
}
} }
} }
onStep(); onStep();
// clean up dead connection sockets
auto it = connections.begin();
while (it != connections.end()) {
CNSocket *cSock = it->second;
if (!cSock->isAlive()) {
killConnection(cSock);
it = connections.erase(it);
removePollFD(cSock->sock);
delete cSock;
} else {
it++;
}
}
} }
} }

View File

@@ -28,7 +28,6 @@
// posix platform // posix platform
#include <sys/socket.h> #include <sys/socket.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <poll.h> #include <poll.h>
#include <unistd.h> #include <unistd.h>
@@ -94,16 +93,16 @@ inline constexpr bool isOutboundPacketID(uint32_t id) {
// overflow-safe validation of variable-length packets // overflow-safe validation of variable-length packets
// for outbound packets // for outbound packets
inline constexpr bool validOutVarPacket(size_t base, size_t npayloads, size_t plsize) { inline constexpr bool validOutVarPacket(size_t base, int32_t npayloads, size_t plsize) {
// check for multiplication overflow // check for multiplication overflow
if (npayloads > 0 && (CN_PACKET_BODY_SIZE) / (size_t)npayloads < plsize) if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
return false; return false;
// it's safe to multiply // it's safe to multiply
size_t trailing = npayloads * plsize; size_t trailing = npayloads * plsize;
// does it fit in a packet? // does it fit in a packet?
if (base + trailing > CN_PACKET_BODY_SIZE) if (base + trailing > CN_PACKET_BUFFER_SIZE - 8)
return false; return false;
// everything is a-ok! // everything is a-ok!
@@ -111,16 +110,16 @@ inline constexpr bool validOutVarPacket(size_t base, size_t npayloads, size_t pl
} }
// for inbound packets // for inbound packets
inline constexpr bool validInVarPacket(size_t base, size_t npayloads, size_t plsize, size_t datasize) { inline constexpr bool validInVarPacket(size_t base, int32_t npayloads, size_t plsize, size_t datasize) {
// check for multiplication overflow // check for multiplication overflow
if (npayloads > 0 && CN_PACKET_BODY_SIZE / (size_t)npayloads < plsize) if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
return false; return false;
// it's safe to multiply // it's safe to multiply
size_t trailing = npayloads * plsize; size_t trailing = npayloads * plsize;
// make sure size is exact // make sure size is exact
// datasize has already been validated against CN_PACKET_BODY_SIZE // datasize has already been validated against CN_PACKET_BUFFER_SIZE
if (datasize != base + trailing) if (datasize != base + trailing)
return false; return false;
@@ -231,7 +230,6 @@ protected:
const size_t STARTFDSCOUNT = 8; // number of initial PollFD slots const size_t STARTFDSCOUNT = 8; // number of initial PollFD slots
std::vector<PollFD> fds; std::vector<PollFD> fds;
std::string serverType = "invalid";
SOCKET sock; SOCKET sock;
uint16_t port; uint16_t port;
socklen_t addressSize; socklen_t addressSize;
@@ -240,7 +238,6 @@ protected:
bool active = true; bool active = true;
bool setSocketOption(SOCKET s, int level, int option, int value);
void addPollFD(SOCKET s); void addPollFD(SOCKET s);
void removePollFD(int i); void removePollFD(int i);

View File

@@ -1,45 +1,27 @@
#include "core/CNShared.hpp" #include "core/CNShared.hpp"
static std::unordered_map<int64_t, LoginMetadata*> logins; #if defined(__MINGW32__) && !defined(_GLIBCXX_HAS_GTHREADS)
static std::mutex mtx; #include "mingw/mingw.mutex.h"
#else
#include <mutex>
#endif
std::map<int64_t, Player> CNSharedData::players;
std::mutex playerCrit;
void CNShared::storeLoginMetadata(int64_t sk, LoginMetadata *lm) { void CNSharedData::setPlayer(int64_t sk, Player& plr) {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
// take ownership of connection data players[sk] = plr;
logins[sk] = lm;
} }
LoginMetadata* CNShared::getLoginMetadata(int64_t sk) { Player CNSharedData::getPlayer(int64_t sk) {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
// fail if the key isn't found return players[sk];
if (logins.find(sk) == logins.end())
return nullptr;
// transfer ownership of connection data to shard
LoginMetadata *lm = logins[sk];
logins.erase(sk);
return lm;
} }
void CNShared::pruneLoginMetadata(CNServer *serv, time_t currTime) { void CNSharedData::erasePlayer(int64_t sk) {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
auto it = logins.begin(); players.erase(sk);
while (it != logins.end()) {
auto& sk = it->first;
auto& lm = it->second;
if (currTime > lm->timestamp + CNSHARED_TIMEOUT) {
std::cout << "[WARN] Pruning hung connection attempt" << std::endl;
// deallocate object and remove map entry
delete logins[sk];
it = logins.erase(it); // skip the invalidated iterator
} else {
it++;
}
}
} }

View File

@@ -10,20 +10,11 @@
#include "Player.hpp" #include "Player.hpp"
/* namespace CNSharedData {
* Connecions time out after 5 minutes, checked every 30 seconds. // serialkey corresponds to player data
*/ extern std::map<int64_t, Player> players;
#define CNSHARED_TIMEOUT 300000
#define CNSHARED_PERIOD 30000
struct LoginMetadata { void setPlayer(int64_t sk, Player& plr);
uint64_t FEKey; Player getPlayer(int64_t sk);
int32_t playerId; void erasePlayer(int64_t sk);
time_t timestamp;
};
namespace CNShared {
void storeLoginMetadata(int64_t sk, LoginMetadata *lm);
LoginMetadata* getLoginMetadata(int64_t sk);
void pruneLoginMetadata(CNServer *serv, time_t currTime);
} }

View File

@@ -23,14 +23,13 @@
#include <string> #include <string>
#include <locale> #include <locale>
#include <codecvt> #include <codecvt>
#include <tuple>
// yes this is ugly, but this is needed to zero out the memory so we don't have random stackdata in our structs. // yes this is ugly, but this is needed to zero out the memory so we don't have random stackdata in our structs.
#define INITSTRUCT(T, x) T x; \ #define INITSTRUCT(T, x) T x; \
memset(&x, 0, sizeof(T)); memset(&x, 0, sizeof(T));
#define INITVARPACKET(_buf, _Pkt, _pkt, _Trailer, _trailer) uint8_t _buf[CN_PACKET_BODY_SIZE]; \ #define INITVARPACKET(_buf, _Pkt, _pkt, _Trailer, _trailer) uint8_t _buf[CN_PACKET_BUFFER_SIZE]; \
memset(&_buf, 0, CN_PACKET_BODY_SIZE); \ memset(&_buf, 0, CN_PACKET_BUFFER_SIZE); \
auto _pkt = (_Pkt*)_buf; \ auto _pkt = (_Pkt*)_buf; \
auto _trailer = (_Trailer*)(_pkt + 1); auto _trailer = (_Trailer*)(_pkt + 1);
@@ -40,8 +39,10 @@
// wrapper for U16toU8 // wrapper for U16toU8
#define ARRLEN(x) (sizeof(x)/sizeof(*x)) #define ARRLEN(x) (sizeof(x)/sizeof(*x))
#define AUTOU8(x) std::string((char*)x, ARRLEN(x)) #define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))
#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))
// typedef for chunk position tuple
typedef std::tuple<int, int, uint64_t> ChunkPos;
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt // TODO: rewrite U16toU8 & U8toU16 to not use codecvt
@@ -49,25 +50,19 @@ 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 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 getTime();
time_t getTimestamp(); time_t getTimestamp();
int timingSafeStrcmp(const char* a, const char* b);
void terminate(int); void terminate(int);
// The PROTOCOL_VERSION definition can be defined by the build system. // The PROTOCOL_VERSION definition is defined by the build system.
#if !defined(PROTOCOL_VERSION) #if !defined(PROTOCOL_VERSION)
#define PROTOCOL_VERSION 104
#endif
#if PROTOCOL_VERSION == 104
#include "structs/0104.hpp" #include "structs/0104.hpp"
#elif PROTOCOL_VERSION == 728 #elif PROTOCOL_VERSION == 728
#include "structs/0728.hpp" #include "structs/0728.hpp"
#elif PROTOCOL_VERSION == 104
#include "structs/0104.hpp"
#elif PROTOCOL_VERSION == 1013 #elif PROTOCOL_VERSION == 1013
#include "structs/1013.hpp" #include "structs/1013.hpp"
#else #else
#error Invalid PROTOCOL_VERSION #error Invalid PROTOCOL_VERSION
#endif #endif
#define AEQUIP_COUNT_MINUS_BOOSTERS 9
#define AEQUIP_COUNT_WITH_BOOSTERS 12
sSYSTEMTIME timeStampToStruct(uint64_t time); sSYSTEMTIME timeStampToStruct(uint64_t time);

View File

@@ -1,8 +1,6 @@
/* enum definitions from the client */ /* enum definitions from the client */
#pragma once #pragma once
#include "core/CNStructs.hpp"
// floats // floats
const float VALUE_BATTERY_EMPTY_PENALTY = 0.5f; const float VALUE_BATTERY_EMPTY_PENALTY = 0.5f;
const float CN_EP_RANK_1 = 0.8f; const float CN_EP_RANK_1 = 0.8f;
@@ -12,44 +10,63 @@ const float CN_EP_RANK_4 = 0.3f;
const float CN_EP_RANK_5 = 0.29f; const float CN_EP_RANK_5 = 0.29f;
// methods of finding players for GM commands // methods of finding players for GM commands
enum class eCN_GM_TargetSearchBy { enum eCN_GM_TargetSearchBy {
PC_ID, // player id eCN_GM_TargetSearchBy__PC_ID, // player id
PC_Name, // firstname, lastname eCN_GM_TargetSearchBy__PC_Name, // firstname, lastname
PC_UID // account id eCN_GM_TargetSearchBy__PC_UID // account id
}; };
enum class eCN_GM_TeleportType { enum eCN_GM_TeleportType {
XYZ, eCN_GM_TeleportMapType__XYZ,
MapXYZ, eCN_GM_TeleportMapType__MapXYZ,
MyLocation, eCN_GM_TeleportMapType__MyLocation,
SomeoneLocation, eCN_GM_TeleportMapType__SomeoneLocation,
Unstick eCN_GM_TeleportMapType__Unstick
}; };
enum class eTaskTypeProperty { // nano powers
None = -1,
Talk = 1,
GotoLocation = 2,
UseItems = 3,
Delivery = 4,
Defeat = 5,
EscortDefence = 6,
Max = 7
};
enum class ePCRegenType {
None,
Xcom,
Here,
HereByPhoenix,
HereByPhoenixGroup,
Unstick,
HereByPhoenixItem,
End
};
// nano power flags
enum { enum {
EST_NONE = 0,
EST_DAMAGE = 1,
EST_HEAL_HP = 2,
EST_KNOCKDOWN = 3,
EST_SLEEP = 4,
EST_SNARE = 5,
EST_HEAL_STAMINA = 6,
EST_STAMINA_SELF = 7,
EST_STUN = 8,
EST_WEAPONSLOW = 9,
EST_JUMP = 10,
EST_RUN = 11,
EST_STEALTH = 12,
EST_SWIM = 13,
EST_MINIMAPENEMY = 14,
EST_MINIMAPTRESURE = 15,
EST_PHOENIX = 16,
EST_PROTECTBATTERY = 17,
EST_PROTECTINFECTION = 18,
EST_REWARDBLOB = 19,
EST_REWARDCASH = 20,
EST_BATTERYDRAIN = 21,
EST_CORRUPTIONATTACK = 22,
EST_INFECTIONDAMAGE = 23,
EST_KNOCKBACK = 24,
EST_FREEDOM = 25,
EST_PHOENIX_GROUP = 26,
EST_RECALL = 27,
EST_RECALL_GROUP = 28,
EST_RETROROCKET_SELF = 29,
EST_BLOODSUCKING = 30,
EST_BOUNDINGBALL = 31,
EST_INVULNERABLE = 32,
EST_NANOSTIMPAK = 33,
EST_RETURNHOMEHEAL = 34,
EST_BUFFHEAL = 35,
EST_EXTRABANK = 36,
EST__END = 37,
EST_CORRUPTIONATTACKWIN = 38,
EST_CORRUPTIONATTACKLOSE = 39,
ECSB_NONE = 0, ECSB_NONE = 0,
ECSB_UP_MOVE_SPEED = 1, ECSB_UP_MOVE_SPEED = 1,
ECSB_UP_SWIM_SPEED = 2, ECSB_UP_SWIM_SPEED = 2,
@@ -79,27 +96,6 @@ enum {
ECSTB__END = 26, ECSTB__END = 26,
}; };
enum {
ETBU_NONE = 0,
ETBU_ADD = 1,
ETBU_DEL = 2,
ETBU_CHANGE = 3,
ETBU__END = 4,
};
enum {
ETBT_NONE = 0,
ETBT_NANO = 1,
ETBT_GROUPNANO = 2,
ETBT_SHINY = 3,
ETBT_LANDEFFECT = 4,
ETBT_ITEM = 5,
ETBT_CASHITEM = 6,
ETBT__END = 7,
ETBT_SKILL = 1,
ETBT_GROUPSKILL = 2
};
enum { enum {
SUCC = 1, SUCC = 1,
FAIL = 0, FAIL = 0,
@@ -225,15 +221,6 @@ enum {
SIZEOF_NANO_TUNE_NEED_ITEM_SLOT = 10, SIZEOF_NANO_TUNE_NEED_ITEM_SLOT = 10,
VALUE_ATTACK_MISS = 1, 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_ONLINE = 1,
MSG_BUSY = 2, MSG_BUSY = 2,
MSG_OFFLINE = 0, MSG_OFFLINE = 0,
@@ -421,13 +408,7 @@ enum {
SEND_ANYCAST_NEW = 3, SEND_ANYCAST_NEW = 3,
SEND_BROADCAST = 4, 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, CN_PACKET_BUFFER_SIZE = 4096,
#endif
P_CL2LS_REQ_LOGIN = 0x12000001, // 301989889 P_CL2LS_REQ_LOGIN = 0x12000001, // 301989889
P_CL2LS_REQ_CHECK_CHAR_NAME = 0x12000002, // 301989890 P_CL2LS_REQ_CHECK_CHAR_NAME = 0x12000002, // 301989890
@@ -944,15 +925,10 @@ enum {
* Each is the last packet - the upper bits + 1 * Each is the last packet - the upper bits + 1
*/ */
enum { enum {
N_CL2LS = 0xf, N_CL2LS = 0xf,
N_CL2FE = 0xa5, N_CL2FE = 0xa5,
N_FE2CL = 0x12f, N_FE2CL = 0x12f,
N_LS2CL = 0x1a, N_LS2CL = 0x1a,
N_PACKETS = N_CL2LS + N_CL2FE + N_FE2CL + N_LS2CL 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_FAIL),
PACKET(P_FE2CL_REP_PC_EXIT_SUCC), PACKET(P_FE2CL_REP_PC_EXIT_SUCC),
PACKET(P_FE2CL_PC_EXIT), PACKET(P_FE2CL_PC_EXIT),
VAR_PACKET(P_FE2CL_PC_AROUND, iPCCnt, sPCAppearanceData), PACKET(P_FE2CL_PC_AROUND),
PACKET(P_FE2CL_PC_MOVE), PACKET(P_FE2CL_PC_MOVE),
PACKET(P_FE2CL_PC_STOP), PACKET(P_FE2CL_PC_STOP),
PACKET(P_FE2CL_PC_JUMP), 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_EXIT),
PACKET(P_FE2CL_NPC_MOVE), PACKET(P_FE2CL_NPC_MOVE),
PACKET(P_FE2CL_NPC_NEW), PACKET(P_FE2CL_NPC_NEW),
VAR_PACKET(P_FE2CL_NPC_AROUND, iNPCCnt, sNPCAppearanceData), PACKET(P_FE2CL_NPC_AROUND),
VAR_PACKET(P_FE2CL_AROUND_DEL_PC, iPCCnt, int32_t), PACKET(P_FE2CL_AROUND_DEL_PC),
VAR_PACKET(P_FE2CL_AROUND_DEL_NPC, iNPCCnt, int32_t), PACKET(P_FE2CL_AROUND_DEL_NPC),
PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC), PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC),
PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_FAIL), PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_FAIL),
VAR_PACKET(P_FE2CL_PC_ATTACK_NPCs_SUCC, iNPCCnt, sAttackResult), 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_EXIT),
PACKET(P_FE2CL_TRANSPORTATION_MOVE), PACKET(P_FE2CL_TRANSPORTATION_MOVE),
PACKET(P_FE2CL_TRANSPORTATION_NEW), PACKET(P_FE2CL_TRANSPORTATION_NEW),
VAR_PACKET(P_FE2CL_TRANSPORTATION_AROUND, iCnt, sTransportationAppearanceData), PACKET(P_FE2CL_TRANSPORTATION_AROUND),
VAR_PACKET(P_FE2CL_AROUND_DEL_TRANSPORTATION, iCnt, int32_t), PACKET(P_FE2CL_AROUND_DEL_TRANSPORTATION),
PACKET(P_FE2CL_REP_EP_RANK_LIST), PACKET(P_FE2CL_REP_EP_RANK_LIST),
PACKET(P_FE2CL_REP_EP_RANK_DETAIL), PACKET(P_FE2CL_REP_EP_RANK_DETAIL),
PACKET(P_FE2CL_REP_EP_RANK_PC_INFO), 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_ENTER),
PACKET(P_FE2CL_SHINY_EXIT), PACKET(P_FE2CL_SHINY_EXIT),
PACKET(P_FE2CL_SHINY_NEW), PACKET(P_FE2CL_SHINY_NEW),
VAR_PACKET(P_FE2CL_SHINY_AROUND, iShinyCnt, sShinyAppearanceData), PACKET(P_FE2CL_SHINY_AROUND),
VAR_PACKET(P_FE2CL_AROUND_DEL_SHINY, iShinyCnt, int32_t), PACKET(P_FE2CL_AROUND_DEL_SHINY),
PACKET(P_FE2CL_REP_SHINY_PICKUP_FAIL), PACKET(P_FE2CL_REP_SHINY_PICKUP_FAIL),
PACKET(P_FE2CL_REP_SHINY_PICKUP_SUCC), PACKET(P_FE2CL_REP_SHINY_PICKUP_SUCC),
PACKET(P_FE2CL_PC_MOVETRANSPORTATION), PACKET(P_FE2CL_PC_MOVETRANSPORTATION),

View File

@@ -47,8 +47,8 @@ struct PacketDesc {
* really should. * really should.
*/ */
struct sGM_PVPTarget { struct sGM_PVPTarget {
uint32_t iID;
uint32_t eCT; uint32_t eCT;
uint32_t iID;
}; };
struct sSkillResult_Leech { struct sSkillResult_Leech {

View File

@@ -5,7 +5,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#define DATABASE_VERSION 6 #define DATABASE_VERSION 3
namespace Database { namespace Database {
@@ -41,35 +41,24 @@ namespace Database {
uint64_t Timestamp; uint64_t Timestamp;
}; };
void init();
void open(); void open();
void close(); void close();
void findAccount(Account* account, std::string login); 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); 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 // interface for the /ban command
bool banPlayer(int playerId, std::string& reason); bool banPlayer(int playerId, std::string& reason);
bool unbanPlayer(int playerId); bool unbanPlayer(int playerId);
void updateSelected(int accountId, int slot); void updateSelected(int accountId, int playerId);
void updateSelectedByPlayerId(int accountId, int playerId);
bool validateCharacter(int characterID, int userID); bool validateCharacter(int characterID, int userID);
bool isNameFree(std::string firstName, std::string lastName); bool isNameFree(std::string firstName, std::string lastName);
bool isSlotFree(int accountId, int slotNum); bool isSlotFree(int accountId, int slotNum);
/// returns ID, 0 if something failed /// returns ID, 0 if something failed
int createCharacter(int slot, int accountId, const char* firstName, const char* lastName, int nameCheck); int createCharacter(sP_CL2LS_REQ_SAVE_CHAR_NAME* save, int AccountID);
/// returns true if query succeeded /// returns true if query succeeded
bool finishCharacter(sP_CL2LS_REQ_CHAR_CREATE* character, int accountId); bool finishCharacter(sP_CL2LS_REQ_CHAR_CREATE* character, int accountId);
/// returns true if query succeeded /// returns true if query succeeded
@@ -85,13 +74,11 @@ namespace Database {
}; };
void evaluateCustomName(int characterID, CustomName decision); void evaluateCustomName(int characterID, CustomName decision);
/// returns true if query succeeded /// returns true if query succeeded
bool changeName(int playerId, int accountId, const char* firstName, const char* lastName, int nameCheck); bool changeName(sP_CL2LS_REQ_CHANGE_CHAR_NAME* save, int accountId);
// getting players // getting players
void getPlayer(Player* plr, int id); void getPlayer(Player* plr, int id);
bool _updatePlayer(Player *player);
void updatePlayer(Player *player); void updatePlayer(Player *player);
void commitTrade(Player *plr1, Player *plr2);
// buddies // buddies
int getNumBuddies(Player* player); int getNumBuddies(Player* player);
@@ -111,7 +98,7 @@ namespace Database {
void deleteEmailAttachments(int playerID, int index, int slot); void deleteEmailAttachments(int playerID, int index, int slot);
void deleteEmails(int playerID, int64_t* indices); void deleteEmails(int playerID, int64_t* indices);
int getNextEmailIndex(int playerID); int getNextEmailIndex(int playerID);
bool sendEmail(EmailData* data, std::vector<sItemBase> attachments, Player *sender); bool sendEmail(EmailData* data, std::vector<sItemBase> attachments);
// racing // racing
RaceRanking getTopRaceRanking(int epID, int playerID); RaceRanking getTopRaceRanking(int epID, int playerID);

View File

@@ -152,8 +152,7 @@ void Database::updateEmailContent(EmailData* data) {
sqlite3_step(stmt); sqlite3_step(stmt);
int attachmentsCount = sqlite3_column_int(stmt, 0); int attachmentsCount = sqlite3_column_int(stmt, 0);
// set attachment flag dynamically data->ItemFlag = (data->Taros > 0 || attachmentsCount > 0) ? 1 : 0; // set attachment flag dynamically
data->ItemFlag = (data->Taros > 0 || attachmentsCount > 0) ? 1 : 0;
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
@@ -196,16 +195,18 @@ void Database::updateEmailContent(EmailData* data) {
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
} }
static void _deleteEmailAttachments(int playerID, int index, int slot) { void Database::deleteEmailAttachments(int playerID, int index, int slot) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
std::string sql(R"( std::string sql(R"(
DELETE FROM EmailItems DELETE FROM EmailItems
WHERE PlayerID = ? AND MsgIndex = ? WHERE PlayerID = ? AND MsgIndex = ?;
)"); )");
if (slot != -1) if (slot != -1)
sql += " AND \"Slot\" = ?"; sql += " AND \"Slot\" = ? ";
sql += ";"; sql += ";";
sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, NULL); sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, NULL);
@@ -219,11 +220,6 @@ static void _deleteEmailAttachments(int playerID, int index, int slot) {
sqlite3_finalize(stmt); 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) { void Database::deleteEmails(int playerID, int64_t* indices) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);
@@ -237,15 +233,12 @@ void Database::deleteEmails(int playerID, int64_t* indices) {
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
int64_t msgIndex = indices[i];
sqlite3_bind_int(stmt, 1, playerID); sqlite3_bind_int(stmt, 1, playerID);
sqlite3_bind_int64(stmt, 2, msgIndex); sqlite3_bind_int64(stmt, 2, indices[i]);
if (sqlite3_step(stmt) != SQLITE_DONE) { if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to delete an email: " << sqlite3_errmsg(db) << std::endl; std::cout << "[WARN] Database: Failed to delete an email: " << sqlite3_errmsg(db) << std::endl;
} }
sqlite3_reset(stmt); sqlite3_reset(stmt);
// delete all attachments
_deleteEmailAttachments(playerID, msgIndex, -1);
} }
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
@@ -272,7 +265,7 @@ int Database::getNextEmailIndex(int playerID) {
return (index > 0 ? index + 1 : 1); return (index > 0 ? index + 1 : 1);
} }
bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments, Player *sender) { bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL); sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
@@ -329,32 +322,14 @@ bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments, Pl
sqlite3_bind_int(stmt, 7, item.iTimeLimit); sqlite3_bind_int(stmt, 7, item.iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) { if (sqlite3_step(stmt) != SQLITE_DONE) {
// very likely the UNIQUE constraint failing due to std::cout << "[WARN] Database: Failed to send email: " << sqlite3_errmsg(db) << std::endl;
// orphaned attachments from an old email. sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
// try deleting them first sqlite3_finalize(stmt);
_deleteEmailAttachments(data->PlayerId, data->MsgIndex, -1); return false;
// 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_reset(stmt);
sqlite3_clear_bindings(stmt);
} }
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
if (!_updatePlayer(sender)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return false;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL); sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
return true; return true;
} }

View File

@@ -9,37 +9,6 @@
std::mutex dbCrit; std::mutex dbCrit;
sqlite3 *db; sqlite3 *db;
/*
* When migrating from DB version 3 to 4, we change the username column
* to be case-insensitive. This function ensures there aren't any
* duplicates, e.g. username and USERNAME, before doing the migration.
* I handled this in the code itself rather than the migration file just so
* we can have a more detailed error message than what SQLite provides.
*/
static void checkCaseSensitiveDupes() {
const char* sql = "SELECT Login, COUNT(*) FROM Accounts GROUP BY LOWER(Login) HAVING COUNT(*) > 1;";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
int stat = sqlite3_step(stmt);
if (stat == SQLITE_DONE) {
// no rows returned, so we're good
sqlite3_finalize(stmt);
return;
} else if (stat != SQLITE_ROW) {
std::cout << "[FATAL] Failed to check for duplicate accounts: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
exit(1);
}
std::cout << "[FATAL] Case-sensitive duplicates detected in the Login column." << std::endl;
std::cout << "Either manually delete/rename the offending accounts, or run the pruning script:" << std::endl;
std::cout << "https://github.com/OpenFusionProject/scripts/tree/main/db_migration/caseinsens.py" << std::endl;
sqlite3_finalize(stmt);
exit(1);
}
static void createMetaTable() { static void createMetaTable() {
std::lock_guard<std::mutex> lock(dbCrit); // XXX std::lock_guard<std::mutex> lock(dbCrit); // XXX
@@ -99,7 +68,7 @@ static void checkMetaTable() {
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (sqlite3_step(stmt) != SQLITE_ROW) { if (sqlite3_step(stmt) != SQLITE_ROW) {
std::cout << "[FATAL] Failed to check meta table: " << sqlite3_errmsg(db) << std::endl; std::cout << "[FATAL] Failed to check meta table" << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
exit(1); exit(1);
} }
@@ -174,10 +143,6 @@ static void checkMetaTable() {
} }
while (dbVersion != DATABASE_VERSION) { while (dbVersion != DATABASE_VERSION) {
// need to run this before we do any migration logic
if (dbVersion == 3)
checkCaseSensitiveDupes();
// db migrations // db migrations
std::cout << "[INFO] Migrating Database to Version " << dbVersion + 1 << std::endl; std::cout << "[INFO] Migrating Database to Version " << dbVersion + 1 << std::endl;
@@ -226,31 +191,17 @@ static void createTables() {
static int getTableSize(std::string tableName) { static int getTableSize(std::string tableName) {
std::lock_guard<std::mutex> lock(dbCrit); // XXX std::lock_guard<std::mutex> lock(dbCrit); // XXX
// you aren't allowed to bind the table name const char* sql = "SELECT COUNT(*) FROM ?";
const char* sql = "SELECT COUNT(*) FROM ";
tableName.insert(0, sql);
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, tableName.c_str(), -1, &stmt, NULL); sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, tableName.c_str(), -1, NULL);
sqlite3_step(stmt); sqlite3_step(stmt);
int result = sqlite3_column_int(stmt, 0); int result = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return result; return result;
} }
void Database::init() {
std::cout << "[INFO] Built with libsqlite " SQLITE_VERSION << std::endl;
if (sqlite3_libversion_number() != SQLITE_VERSION_NUMBER)
std::cout << "[INFO] Using libsqlite " << std::string(sqlite3_libversion()) << std::endl;
if (sqlite3_libversion_number() < MIN_SUPPORTED_SQLITE_NUMBER) {
std::cerr << "[FATAL] Runtime sqlite version too old. Minimum compatible version: " MIN_SUPPORTED_SQLITE << std::endl;
exit(1);
}
}
void Database::open() { void Database::open() {
// XXX: move locks here // XXX: move locks here
int rc = sqlite3_open(settings::DBPATH.c_str(), &db); int rc = sqlite3_open(settings::DBPATH.c_str(), &db);
if (rc != SQLITE_OK) { if (rc != SQLITE_OK) {
@@ -267,17 +218,17 @@ void Database::open() {
checkMetaTable(); checkMetaTable();
createTables(); createTables();
std::cout << "[INFO] Database in operation"; std::cout << "[INFO] Database in operation ";
int accounts = getTableSize("Accounts"); int accounts = getTableSize("Accounts");
int players = getTableSize("Players"); int players = getTableSize("Players");
std::string message = ""; std::string message = "";
if (accounts > 0) { if (accounts > 0) {
message += ": Found " + std::to_string(accounts) + " account"; message += ": Found " + std::to_string(accounts) + " Account";
if (accounts > 1) if (accounts > 1)
message += "s"; message += "s";
} }
if (players > 0) { if (players > 0) {
message += " and " + std::to_string(players) + " player"; message += " and " + std::to_string(players) + " Player Character";
if (players > 1) if (players > 1)
message += "s"; message += "s";
} }

View File

@@ -3,13 +3,10 @@
#include "db/Database.hpp" #include "db/Database.hpp"
#include <sqlite3.h> #include <sqlite3.h>
#define MIN_SUPPORTED_SQLITE_NUMBER 3033000 #if defined(__MINGW32__) && !defined(_GLIBCXX_HAS_GTHREADS)
#define MIN_SUPPORTED_SQLITE "3.33.0" #include "mingw/mingw.mutex.h"
// we can't use this in #error, since it doesn't expand macros #else
#include <mutex>
// Compile-time libsqlite version check
#if SQLITE_VERSION_NUMBER < MIN_SUPPORTED_SQLITE_NUMBER
#error libsqlite version too old. Minimum compatible version: 3.33.0
#endif #endif
extern std::mutex dbCrit; extern std::mutex dbCrit;

View File

@@ -1,9 +1,5 @@
#include "core/CNStructs.hpp"
#include "db/internal.hpp" #include "db/internal.hpp"
#include "servers/CNLoginServer.hpp"
#include "bcrypt/BCrypt.hpp" #include "bcrypt/BCrypt.hpp"
void Database::findAccount(Account* account, std::string login) { void Database::findAccount(Account* account, std::string login) {
@@ -31,32 +27,6 @@ void Database::findAccount(Account* account, std::string login) {
sqlite3_finalize(stmt); 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) { int Database::addAccount(std::string login, std::string password) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);
@@ -82,98 +52,6 @@ int Database::addAccount(std::string login, std::string password) {
return sqlite3_last_insert_rowid(db); 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) { void Database::updateSelected(int accountId, int slot) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);
@@ -201,29 +79,6 @@ void Database::updateSelected(int accountId, int slot) {
std::cout << "[WARN] Database fail on updateSelected(): " << sqlite3_errmsg(db) << std::endl; std::cout << "[WARN] Database fail on updateSelected(): " << sqlite3_errmsg(db) << std::endl;
} }
void Database::updateSelectedByPlayerId(int accountId, int32_t playerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
UPDATE Accounts SET
Selected = p.Slot,
LastLogin = (strftime('%s', 'now'))
FROM (SELECT Slot From Players WHERE PlayerId = ?) AS p
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
sqlite3_bind_int(stmt, 2, accountId);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on updateSelectedByPlayerId(): " << sqlite3_errmsg(db) << std::endl;
}
bool Database::validateCharacter(int characterID, int userID) { bool Database::validateCharacter(int characterID, int userID) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);
@@ -293,7 +148,7 @@ bool Database::isSlotFree(int accountId, int slotNum) {
return result; return result;
} }
int Database::createCharacter(int slot, int accountId, const char* firstName, const char* lastName, int nameCheck) { int Database::createCharacter(sP_CL2LS_REQ_SAVE_CHAR_NAME* save, int AccountID) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL); sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
@@ -306,17 +161,22 @@ int Database::createCharacter(int slot, int accountId, const char* firstName, co
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
)"; )";
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
std::string firstName = AUTOU16TOU8(save->szFirstName);
std::string lastName = AUTOU16TOU8(save->szLastName);
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId); sqlite3_bind_int(stmt, 1, AccountID);
sqlite3_bind_int(stmt, 2, slot); sqlite3_bind_int(stmt, 2, save->iSlotNum);
sqlite3_bind_text(stmt, 3, firstName, -1, NULL); sqlite3_bind_text(stmt, 3, firstName.c_str(), -1, NULL);
sqlite3_bind_text(stmt, 4, lastName, -1, NULL); sqlite3_bind_text(stmt, 4, lastName.c_str(), -1, NULL);
sqlite3_bind_int(stmt, 5, settings::SPAWN_X); sqlite3_bind_int(stmt, 5, settings::SPAWN_X);
sqlite3_bind_int(stmt, 6, settings::SPAWN_Y); sqlite3_bind_int(stmt, 6, settings::SPAWN_Y);
sqlite3_bind_int(stmt, 7, settings::SPAWN_Z); sqlite3_bind_int(stmt, 7, settings::SPAWN_Z);
sqlite3_bind_int(stmt, 8, settings::SPAWN_ANGLE); sqlite3_bind_int(stmt, 8, settings::SPAWN_ANGLE);
sqlite3_bind_int(stmt, 9, PC_MAXHEALTH(1)); 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); sqlite3_bind_int(stmt, 10, nameCheck);
// blobs // blobs
@@ -645,7 +505,7 @@ void Database::evaluateCustomName(int characterID, CustomName decision) {
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
} }
bool Database::changeName(int playerId, int accountId, const char* firstName, const char* lastName, int nameCheck) { bool Database::changeName(sP_CL2LS_REQ_CHANGE_CHAR_NAME* save, int accountId) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"( const char* sql = R"(
@@ -659,10 +519,15 @@ bool Database::changeName(int playerId, int accountId, const char* firstName, co
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, firstName, -1, NULL); std::string firstName = AUTOU16TOU8(save->szFirstName);
sqlite3_bind_text(stmt, 2, lastName, -1, NULL); 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_int(stmt, 3, nameCheck); sqlite3_bind_int(stmt, 3, nameCheck);
sqlite3_bind_int(stmt, 4, playerId); sqlite3_bind_int(stmt, 4, save->iPCUID);
sqlite3_bind_int(stmt, 5, accountId); sqlite3_bind_int(stmt, 5, accountId);
int rc = sqlite3_step(stmt); int rc = sqlite3_step(stmt);

View File

@@ -2,6 +2,40 @@
// Loading and saving players to/from the DB // 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) { void Database::getPlayer(Player* plr, int id) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);
@@ -59,13 +93,13 @@ void Database::getPlayer(Player* plr, int id) {
plr->angle = sqlite3_column_int(stmt, 15); plr->angle = sqlite3_column_int(stmt, 15);
plr->HP = sqlite3_column_int(stmt, 16); plr->HP = sqlite3_column_int(stmt, 16);
plr->accountLevel = sqlite3_column_int(stmt, 17); plr->accountLevel = sqlite3_column_int(stmt, 17);
plr->setCapped(CappedValueType::FUSIONMATTER, sqlite3_column_int(stmt, 18)); plr->fusionmatter = sqlite3_column_int(stmt, 18);
plr->setCapped(CappedValueType::TAROS, sqlite3_column_int(stmt, 19)); plr->money = sqlite3_column_int(stmt, 19);
memcpy(plr->aQuestFlag, sqlite3_column_blob(stmt, 20), sizeof(plr->aQuestFlag)); memcpy(plr->aQuestFlag, sqlite3_column_blob(stmt, 20), sizeof(plr->aQuestFlag));
plr->setCapped(CappedValueType::BATTERY_W, sqlite3_column_int(stmt, 21)); plr->batteryW = sqlite3_column_int(stmt, 21);
plr->setCapped(CappedValueType::BATTERY_N, sqlite3_column_int(stmt, 22)); plr->batteryN = sqlite3_column_int(stmt, 22);
plr->mentor = sqlite3_column_int(stmt, 23); plr->mentor = sqlite3_column_int(stmt, 23);
plr->iWarpLocationFlag = sqlite3_column_int(stmt, 24); plr->iWarpLocationFlag = sqlite3_column_int(stmt, 24);
@@ -126,6 +160,8 @@ void Database::getPlayer(Player* plr, int id) {
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
removeExpiredVehicles(plr);
// get quest inventory // get quest inventory
sql = R"( sql = R"(
SELECT Slot, ID, Opt SELECT Slot, ID, Opt
@@ -249,13 +285,11 @@ void Database::getPlayer(Player* plr, int id) {
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
} }
/* void Database::updatePlayer(Player *player) {
* Low-level function to save a player to DB. std::lock_guard<std::mutex> lock(dbCrit);
* Must be run in a SQL transaction and with dbCrit locked.
* The caller manages the transacstion, so if this function returns false, sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
* the caller must roll it back.
*/
bool Database::_updatePlayer(Player *player) {
const char* sql = R"( const char* sql = R"(
UPDATE Players UPDATE Players
SET SET
@@ -302,8 +336,10 @@ bool Database::_updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 21, player->iID); sqlite3_bind_int(stmt, 21, player->iID);
if (sqlite3_step(stmt) != SQLITE_DONE) { if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return false; sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
} }
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
@@ -339,8 +375,10 @@ bool Database::_updatePlayer(Player *player) {
rc = sqlite3_step(stmt); rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) { if (rc != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return false; return;
} }
sqlite3_reset(stmt); sqlite3_reset(stmt);
} }
@@ -357,8 +395,10 @@ bool Database::_updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 6, player->Inven[i].iTimeLimit); sqlite3_bind_int(stmt, 6, player->Inven[i].iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) { if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return false; return;
} }
sqlite3_reset(stmt); sqlite3_reset(stmt);
} }
@@ -375,8 +415,10 @@ bool Database::_updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 6, player->Bank[i].iTimeLimit); sqlite3_bind_int(stmt, 6, player->Bank[i].iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) { if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return false; return;
} }
sqlite3_reset(stmt); sqlite3_reset(stmt);
} }
@@ -409,8 +451,10 @@ bool Database::_updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 4, player->QInven[i].iID); sqlite3_bind_int(stmt, 4, player->QInven[i].iID);
if (sqlite3_step(stmt) != SQLITE_DONE) { if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return false; return;
} }
sqlite3_reset(stmt); sqlite3_reset(stmt);
} }
@@ -443,8 +487,10 @@ bool Database::_updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 4, player->Nanos[i].iStamina); sqlite3_bind_int(stmt, 4, player->Nanos[i].iStamina);
if (sqlite3_step(stmt) != SQLITE_DONE) { if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return false; return;
} }
sqlite3_reset(stmt); sqlite3_reset(stmt);
} }
@@ -478,47 +524,14 @@ bool Database::_updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 5, player->RemainingNPCCount[i][2]); sqlite3_bind_int(stmt, 5, player->RemainingNPCCount[i][2]);
if (sqlite3_step(stmt) != SQLITE_DONE) { if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return false; return;
} }
sqlite3_reset(stmt); sqlite3_reset(stmt);
} }
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return true;
}
void Database::updatePlayer(Player *player) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
if (!_updatePlayer(player)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
}
void Database::commitTrade(Player *plr1, Player *plr2) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
if (!_updatePlayer(plr1)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
if (!_updatePlayer(plr2)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
} }

View File

@@ -208,8 +208,6 @@ void Database::addBlock(int playerId, int blockedPlayerId) {
} }
void Database::removeBlock(int playerId, int blockedPlayerId) { void Database::removeBlock(int playerId, int blockedPlayerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"( const char* sql = R"(
DELETE FROM Blocks DELETE FROM Blocks
WHERE PlayerID = ? AND BlockedPlayerID = ?; WHERE PlayerID = ? AND BlockedPlayerID = ?;

174
src/lua/EntityWrapper.cpp Normal file
View File

@@ -0,0 +1,174 @@
#include "lua/LuaWrapper.hpp"
#include "lua/LuaManager.hpp"
#include "lua/EntityWrapper.hpp"
#include "Entities.hpp"
#define LIBNAME "Entity"
#define SUPERTBL "__entSUPERCLASSES"
#define ENTYGONESTR "Entity doesn't exist anymore!"
EntityRef* grabBaseEntityRef(lua_State *state, int indx) {
// first, make sure its a userdata
luaL_checktype(state, indx, LUA_TUSERDATA);
// grab the super class table
lua_getfield(state, LUA_REGISTRYINDEX, SUPERTBL);
// grab the userdata
EntityRef *data = (EntityRef*)lua_touserdata(state, indx);
// check if it doesn't have a metatable
if (!lua_getmetatable(state, indx)) {
luaL_typerror(state, indx, LIBNAME);
return NULL;
}
// index the super class table
lua_gettable(state, -2);
// if the index was nil, it doesn't exist
if (lua_isnil(state, -1)) {
lua_pop(state, 1);
luaL_typerror(state, indx, LIBNAME);
return NULL;
}
// it's good :)
lua_pop(state, 1);
return data;
}
// check at index
EntityRef* grabEntityRef(lua_State *state, int indx) {
EntityRef *ref = grabBaseEntityRef(state, indx);
if (ref == NULL || !ref->isValid())
return NULL;
return ref;
}
// =============================================== [[ GETTERS ]] ===============================================
static int ent_getX(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
if (ref == NULL)
return 0;
lua_pushnumber(state, ref->getEntity()->x);
return 1;
}
static int ent_getY(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
if (ref == NULL)
return 0;
lua_pushnumber(state, ref->getEntity()->y);
return 1;
}
static int ent_getZ(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
if (ref == NULL)
return 0;
lua_pushnumber(state, ref->getEntity()->z);
return 1;
}
static int ent_getType(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
if (ref == NULL)
return 0;
// push the type string
switch(ref->type) {
case EntityType::PLAYER:
lua_pushstring(state, "Player");
break;
case EntityType::MOB:
lua_pushstring(state, "Mob");
break;
case EntityType::EGG:
lua_pushstring(state, "Egg");
break;
case EntityType::BUS:
lua_pushstring(state, "Bus");
break;
default: // INVALID, COMBAT_NPC, SIMPLE_NPC
lua_pushstring(state, "Entity");
break;
}
return 1;
}
static luaL_Reg ent_getters[] = {
{"x", ent_getX},
{"y", ent_getY},
{"z", ent_getZ},
{"type", ent_getType},
{0, 0}
};
// =============================================== [[ METHODS ]] ===============================================
static int ent_exists(lua_State *state) {
EntityRef *data = (EntityRef*)grabBaseEntityRef(state, 1);
lua_pushboolean(state, !(data == NULL || !data->isValid()));
return 1;
}
static luaL_Reg ent_methods[] = {
{"exists", ent_exists},
{0, 0}
};
void LuaManager::Entity::init(lua_State *state) {
// register our library as a global (and leave it on the stack)
luaL_register(state, LIBNAME, ent_methods);
lua_pop(state, 1); // pop library table
// will hold our super classes
lua_pushstring(state, SUPERTBL);
lua_newtable(state);
lua_rawset(state, LUA_REGISTRYINDEX);
}
void LuaManager::Entity::registerSuper(lua_State *state, const char *tname) {
// grab the super class table
lua_getfield(state, LUA_REGISTRYINDEX, SUPERTBL);
// grab the metatable
lua_getfield(state, LUA_REGISTRYINDEX, tname);
lua_pushboolean(state, 1);
// finally, set the index
lua_rawset(state, -3);
lua_pop(state, 1); // pop the super class table
}
void LuaManager::Entity::addGetters(lua_State *state) {
luaL_register(state, NULL, ent_getters);
}
void LuaManager::Entity::addMethods(lua_State *state) {
luaL_register(state, NULL, ent_methods);
}
void LuaManager::Entity::push(lua_State *state, EntityRef ref, const char *tname) {
// creates the udata and copies the reference to the udata
EntityRef *ent = (EntityRef*)lua_newuserdata(state, sizeof(EntityRef));
*ent = ref;
// attaches our metatable from the registry to the udata
luaL_getmetatable(state, tname);
lua_setmetatable(state, -2);
}

19
src/lua/EntityWrapper.hpp Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "lua/LuaWrapper.hpp"
#include "lua/LuaManager.hpp"
#include "Entities.hpp"
namespace LuaManager {
namespace Entity {
void init(lua_State *state);
void registerSuper(lua_State *state, const char *tname);
void addGetters(lua_State *state);
void addMethods(lua_State *state);
void push(lua_State *state, EntityRef ref, const char *tname);
}
}

190
src/lua/EventWrapper.cpp Normal file
View File

@@ -0,0 +1,190 @@
/*
This loads the Event library, basically a wrapper for lEvents, allows the lua script to register callbacks or wait for an event to be fired
*/
#include "lua/EventWrapper.hpp"
#include "lua/LuaWrapper.hpp"
#include <unordered_set>
#define LIBNAME "Event"
#define LISTNR "Listener"
typedef lEvent* eventData;
std::unordered_set<lEvent*> activeEvents;
struct lstnrData {
lEvent *event;
uint32_t rawListener;
};
static void pushListener(lua_State *state, lEvent *event, uint32_t listener) {
lstnrData *lstnr = (lstnrData*)lua_newuserdata(state, sizeof(lstnrData));
lstnr->event = event;
lstnr->rawListener = listener;
// attaches our metatable from the registry to the udata
luaL_getmetatable(state, LISTNR);
lua_setmetatable(state, -2);
}
static void pushEvent(lua_State *state, lEvent *event) {
eventData *eData = (eventData*)lua_newuserdata(state, sizeof(eventData));
*eData = event;
// attaches our metatable from the registry to the udata
luaL_getmetatable(state, LIBNAME);
lua_setmetatable(state, -2);
}
static lEvent *grabEvent(lua_State *state, int indx) {
// first, make sure its a userdata
luaL_checktype(state, indx, LUA_TUSERDATA);
// now, check and make sure its our libraries metatable attached to this userdata
eventData *event = (eventData*)luaL_checkudata(state, indx, LIBNAME);
if (event == NULL) {
luaL_typerror(state, indx, LIBNAME);
return NULL;
}
// if the event is not active return NULL and don't throw an error (this is an easily forgivable user-error)
if (activeEvents.find(*event) == activeEvents.end())
return NULL;
// return the pointer to the lEvent
return *event;
}
static lstnrData *grabListener(lua_State *state, int indx) {
// first, make sure its a userdata
luaL_checktype(state, indx, LUA_TUSERDATA);
// now, check and make sure its our libraries metatable attached to this userdata
lstnrData *lstnr = (lstnrData*)luaL_checkudata(state, indx, LISTNR);
if (lstnr == NULL) {
luaL_typerror(state, indx, LISTNR);
return NULL;
}
// if the event is not active return NULL and don't throw an error (this is an easily forgivable user-error)
if (activeEvents.find(lstnr->event) == activeEvents.end())
return NULL;
// return the pointer to the lEvent
return lstnr;
}
// connects a callback to an event
static int evnt_listen(lua_State *state) {
int nargs = lua_gettop(state);
lEvent *event = grabEvent(state, 1);
// sanity check
if (event == NULL)
return 0;
// for each argument passed, check that it's a function and add it to the event
for (int i = 2; i <= nargs; i++) {
luaL_checktype(state, i, LUA_TFUNCTION);
lua_pushvalue(state, i);
pushListener(state, event, event->addCallback(state, luaL_ref(state, LUA_REGISTRYINDEX)));
}
// we return this many listeners
return nargs - 1;
}
// yields the thread until the event is triggered
static int evnt_wait(lua_State *state) {
lEvent *event = grabEvent(state, 1);
// sanity check
if (event == NULL)
return 0;
event->addWait(state);
return lua_yield(state, 0);
}
static int lstnr_disconnect(lua_State *state) {
lstnrData *lstnr = grabListener(state, 1);
// sanity check
if (lstnr == NULL)
return 0;
// disconnect the event
lstnr->event->disconnectEvent(lstnr->rawListener);
return 0;
}
static int lstnr_reconnect(lua_State *state) {
lstnrData *lstnr = grabListener(state, 1);
// sanity check
if (lstnr == NULL)
return 0;
// disconnect the event
lstnr->event->reconnectEvent(lstnr->rawListener);
return 0;
}
static int lstnr_gc(lua_State *state) {
lstnrData *lstnr = grabListener(state, 1);
// sanity check
if (lstnr == NULL)
return 0;
// if the listener is disabled, clear it
if (lstnr->event->isDisabled(lstnr->rawListener))
lstnr->event->clear(lstnr->rawListener);
return 0;
}
static luaL_Reg evnt_methods[] = {
{"listen", evnt_listen},
{"wait", evnt_wait},
{0, 0}
};
static luaL_Reg lstnr_methods[] = {
{"disconnect", lstnr_disconnect},
{"reconnect", lstnr_reconnect},
{0, 0}
};
void LuaManager::Event::init(lua_State *state) {
// register the library & pop it
luaL_register(state, LIBNAME, evnt_methods);
// create the event metatable
luaL_newmetatable(state, LIBNAME);
lua_pushstring(state, "__index");
lua_pushvalue(state, -3);
lua_rawset(state, -3);
// create the listener metatable
luaL_newmetatable(state, LISTNR);
lua_pushstring(state, "__index");
lua_newtable(state);
luaL_register(state, NULL, lstnr_methods);
lua_rawset(state, -3);
lua_pushstring(state, "__gc");
lua_pushcfunction(state, lstnr_gc);
lua_rawset(state, -3);
// pop the tables off the stack
lua_pop(state, 3);
activeEvents = std::unordered_set<lEvent*>();
}
// just a wrapper for pushEvent()
void LuaManager::Event::push(lua_State *state, lEvent *event) {
pushEvent(state, event);
}

10
src/lua/EventWrapper.hpp Normal file
View File

@@ -0,0 +1,10 @@
#include "lua/LuaManager.hpp"
#include "lua/LuaWrapper.hpp"
namespace LuaManager {
namespace Event {
void init(lua_State *state);
void push(lua_State *state, lEvent *event);
}
}

185
src/lua/LuaManager.cpp Normal file
View File

@@ -0,0 +1,185 @@
#include "lua/LuaManager.hpp"
#include "lua/LuaWrapper.hpp"
#include "lua/EventWrapper.hpp"
#include "lua/WorldWrapper.hpp"
#include "lua/PlayerWrapper.hpp"
#include "lua/NPCWrapper.hpp"
#include "PlayerManager.hpp"
#include "servers/CNShardServer.hpp"
#include "settings.hpp"
#include <filesystem>
#include <vector>
time_t getTime();
class Script;
// our "main" state, holds our environment
lua_State *LuaManager::global;
std::map<lua_State*, Script*> activeScripts;
/*
Basically each script is treated as a coroutine, when wait() is called it gets yielded and is pushed onto the scheduler queue to be resumed.
*/
class Script {
private:
lua_State *thread;
lRegistry threadRef; // we'll need to unref this when closing this state
public:
Script(std::string source) {
// make the thread & register it in the registry
thread = lua_newthread(LuaManager::global);
threadRef = luaL_ref(LuaManager::global, LUA_REGISTRYINDEX);
// add this script to the map
activeScripts[thread] = this;
// compile & run the script, if it error'd, print the error
int _retCode;
if (luaL_loadfile(thread, source.c_str()) || ((_retCode = lua_resume(thread, 0)) != 0 && (_retCode != LUA_YIELD))) {
std::cout << "[LUA ERROR]: " << lua_tostring(thread, -1) << std::endl;
}
}
// unregister all of our events from the wrappers
~Script() {
LuaManager::clearState(thread);
// remove it from the global registry
luaL_unref(LuaManager::global, LUA_REGISTRYINDEX, threadRef);
}
// c++ moment....
lua_State* getState() {
return thread;
}
};
struct scheduledThread {
lRegistry ref; // ref that should be unref'd before resuming
time_t time;
};
std::map<lua_State*, scheduledThread> scheduleQueue;
// pauses the script for x seconds, not very accurate but should be
// called within ~60ms or less of when it was schedueled
// will also return the time the thread was paused
int OF_wait(lua_State *state) {
double seconds = luaL_checknumber(state, 1);
// register the thread in the global registry so we don't get GC'd
lua_pushthread(state);
lRegistry ref = luaL_ref(state, LUA_REGISTRYINDEX); // threads decentant of a global state all share the global registry
// yield the state and push the state onto our scheduler queue
scheduleQueue[state] = {ref, (int)(seconds*1000) + getTime()};
return lua_yield(state, 0);
}
void luaScheduler(CNServer *serv, time_t currtime) {
for (auto iter = scheduleQueue.begin(); iter != scheduleQueue.end();) {
time_t event = (*iter).second.time;
lRegistry ref = (*iter).second.ref;
lua_State *thread = (*iter).first;
// is it time to run the event?
if (event <= currtime) {
// remove from the scheduler queue
scheduleQueue.erase(iter++);
// unregister the thread
luaL_unref(thread, LUA_REGISTRYINDEX, ref);
// resume the state, (wait() returns the delta time since call)
lua_pushnumber(thread, ((double)currtime - event)/10);
yieldCall(thread, 1);
} else // go to the next iteration
++iter;
}
}
void LuaManager::printError(std::string err) {
std::cerr << "[LUA ERR]: " << err << std::endl;
}
void LuaManager::init() {
// allocate our state
global = luaL_newstate();
// open lua's base libraries (excluding the IO for now)
luaopen_base(global);
luaopen_table(global);
luaopen_string(global);
luaopen_math(global);
luaopen_debug(global);
// add wait()
lua_register(global, "wait", OF_wait);
// register our libraries
Event::init(global);
World::init(global);
Entity::init(global);
Player::init(global);
NPC::init(global);
activeScripts = std::map<lua_State*, Script*>();
// we want to be called after every poll(), so our timer delta is set to 0
REGISTER_SHARD_TIMER(luaScheduler, 0);
// load our scripts
loadScripts();
}
void LuaManager::runScript(std::string filename) {
new Script(filename);
}
void LuaManager::stopScripts() {
// clear the scheduler queue
scheduleQueue.clear();
// free all the scripts, they'll take care of everything for us :)
for (auto as : activeScripts) {
delete as.second;
}
// finally clear the map
activeScripts.clear();
// walk through each player and unregister each event
for (auto pair: PlayerManager::players) {
if (pair.second->onChat != nullptr) {
delete pair.second->onChat;
pair.second->onChat = nullptr;
}
}
}
void LuaManager::loadScripts() {
if (!std::filesystem::exists(settings::SCRIPTSDIR)) {
std::cout << "[WARN] scripts directory \"" << settings::SCRIPTSDIR << "\" doesn't exist!" << std::endl;
return;
}
// for each file in the scripts director, load the script
std::filesystem::path dir(settings::SCRIPTSDIR);
for (auto &d : std::filesystem::directory_iterator(dir)) {
if (d.path().extension().u8string() == ".lua")
runScript(d.path().u8string());
}
}
void LuaManager::clearState(lua_State *state) {
// TODO
}
void LuaManager::playerAdded(CNSocket *sock) {
World::playerAdded(sock);
}
void LuaManager::playerRemoved(CNSocket *sock) {
World::playerRemoved(sock);
}

28
src/lua/LuaManager.hpp Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include "core/CNProtocol.hpp"
#include <string>
#ifdef _MSC_VER
#include <luajit/lua.hpp>
#else
#include <lua.hpp>
#endif
typedef int lRegistry;
namespace LuaManager {
extern lua_State *global;
void init();
void printError(std::string err);
// runs the script in the passed file
void runScript(std::string filename);
void stopScripts();
void loadScripts();
// unregisters the events tied to this state with all wrappers
void clearState(lua_State *state);
void playerAdded(CNSocket *sock);
void playerRemoved(CNSocket *sock);
}

223
src/lua/LuaWrapper.hpp Normal file
View File

@@ -0,0 +1,223 @@
#pragma once
/*
This is a very simple header that adds a small "event" layer, where scripts can register callbacks & "waits" on events. The actual
Event metatables & library is in EventWrapper.[hc]pp
*/
#include <string>
#include <map>
#include <unordered_set>
#include <cassert>
#include "lua/LuaManager.hpp"
#include "lua/PlayerWrapper.hpp"
#define yieldCall(state, nargs) \
int _retCode = lua_resume(state, nargs); \
if (_retCode != 0 && _retCode != LUA_YIELD) \
LuaManager::printError(lua_tostring(state, -1)); \
inline static int lua_autoPush(lua_State* state, int nargs) {
// return the number of pushed arguments :)
return nargs;
}
/*
This function will automatically push all of the passed arguments onto the stack and returns the # of pushed values
Supported datatypes are:
double or int : LUA_TNUMBER
char* or const char* : LUA_TSTRING
bool : LUA_TBOOLEAN
lRegistry : grabs the object from the lua registry and pushes it onto the stack
CNSocket* : Pushes the Player Entity
*/
template<typename T, class... Rest>
inline static int lua_autoPush(lua_State* state, int nargs, T arg, Rest... rest) {
// pick which branch to compile based on the type of arg
if constexpr(std::is_same<T, int>::value || std::is_same<T, double>::value) {
lua_pushnumber(state, (lua_Number)arg);
} else if constexpr(std::is_same<T, char*>::value || std::is_same<T, const char*>::value) {
lua_pushstring(state, (const char*)arg);
} else if constexpr(std::is_same<T, lRegistry>::value) {
// grab the value from the registry
lua_rawgeti(state, LUA_REGISTRYINDEX, (int)arg);
} else if constexpr(std::is_same<T, CNSocket*>::value) { // pushes a Player Entity
LuaManager::Player::push(state, arg);
} else if constexpr(std::is_same<T, bool>::value) {
lua_pushboolean(state, arg);
}
// recursively call, expanding rest and pushing the left-most rvalue into arg
return lua_autoPush(state, ++nargs, rest...);
}
enum eventType {
EVENT_CALLBACK, // standard callback
EVENT_WAIT // state needs to be resumed with the arguments
};
class lEvent;
extern std::unordered_set<lEvent*> activeEvents;
class lEvent {
private:
struct rawEvent {
lua_State *state;
eventType type;
lRegistry ref;
bool disabled;
};
uint32_t nextId; // next ID
std::map<uint32_t, rawEvent> events;
uint32_t registerEvent(lua_State *state, lRegistry ref, eventType type) {
uint32_t id = nextId++;
assert(nextId < UINT32_MAX);
events[id] = {state, type, ref, false};
return id;
}
void freeEvent(rawEvent *event) {
// the registry of all states is the same!
luaL_unref(event->state, LUA_REGISTRYINDEX, event->ref);
}
public:
lEvent() {
events = std::map<uint32_t, rawEvent>();
nextId = 0; // start at 0
activeEvents.insert(this);
}
~lEvent() {
// remove from the active set and disable all existing callbacks
activeEvents.erase(this);
clear();
}
uint32_t addCallback(lua_State *state, lRegistry ref) {
return registerEvent(state, ref, EVENT_CALLBACK);
}
// yields the thread until the event is called
uint32_t addWait(lua_State *state) {
// push the thread onto the stack and register it in the global registry
lua_pushthread(state);
lRegistry ref = luaL_ref(state, LUA_REGISTRYINDEX);
return registerEvent(state, ref, EVENT_WAIT);
}
// walks through the events and unregister them from the state
void clear() {
for (auto &pair : events) {
freeEvent(&pair.second);
}
events.clear();
}
// free the event based on id
void clear(uint32_t id) {
auto iter = events.find(id);
// sanity check
if (iter == events.end())
return;
// free the event
freeEvent(&(*iter).second);
events.erase(iter);
}
// frees all events to this state
void clear(lua_State *state) {
for (auto iter = events.begin(); iter != events.end();) {
rawEvent *event = &(*iter).second;
// if this is our state, free this event and erase it from the map
if (event->state == state) {
freeEvent(event);
events.erase(iter++);
} else
++iter;
}
}
void disconnectEvent(uint32_t id) {
auto iter = events.find(id);
// sanity check
if (iter == events.end())
return;
(*iter).second.disabled = true;
}
void reconnectEvent(uint32_t id) {
auto iter = events.find(id);
// sanity check
if (iter == events.end())
return;
(*iter).second.disabled = false;
}
bool isDisabled(uint32_t id) {
auto iter = events.find(id);
// sanity check
if (iter == events.end())
return true;
return (*iter).second.disabled;
}
template<class... Args> inline void call(Args... args) {
auto eventsClone = events; // so if a callback is added, we don't iterate into undefined behavior
for (auto &pair : eventsClone) {
rawEvent *event = &pair.second;
// if the event is disabled, skip it
if (event->disabled)
continue;
switch (event->type) {
case EVENT_CALLBACK: {
// make thread for this callback
if (!lua_checkstack(event->state, 1)) {
std::cout << "[FATAL] Failed to create new lua thread! out of memory!" << std::endl;
terminate(0);
}
lua_State *nThread = lua_newthread(event->state);
// push the callable first, the push all the arguments
lua_rawgeti(nThread, LUA_REGISTRYINDEX, (int)event->ref);
int nargs = lua_autoPush(nThread, 0, args...);
// then call it :)
yieldCall(nThread, nargs);
break;
}
case EVENT_WAIT: {
// erase this event
freeEvent(&pair.second);
events.erase(pair.first);
// the :wait() will return the passed arguments
int nargs = lua_autoPush(event->state, 0, args...);
yieldCall(event->state, nargs);
break;
}
default:
std::cout << "[WARN] INVALID EVENT TYPE : " << event->type << std::endl;
break;
}
}
}
};

266
src/lua/NPCWrapper.cpp Normal file
View File

@@ -0,0 +1,266 @@
#include "lua/LuaWrapper.hpp"
#include "lua/NPCWrapper.hpp"
#include "NPC.hpp"
#include "NPCManager.hpp"
#include "Transport.hpp"
#define LIBNAME "NPC"
#define NPCGONESTR "NPC was destoryed and no longer exists!"
#define GETTERTBL "__npcGETTERS"
#define SETTERTBL "__npcSETTERS"
#define METHODTBL "__npcMETHODS"
static EntityRef* grabEntityRef(lua_State *state, int indx) {
// first, make sure its a userdata
luaL_checktype(state, indx, LUA_TUSERDATA);
// now, check and make sure its our library's metatable attached to this userdata
EntityRef *ref = (EntityRef*)luaL_checkudata(state, indx, LIBNAME);
if (ref == NULL) {
luaL_typerror(state, indx, LIBNAME);
return NULL;
}
// check if the npc exists still & return NULL if it doesn't
if (!ref->isValid()) {
luaL_argerror(state, indx, NPCGONESTR);
return NULL;
}
return ref;
}
static CombatNPC* grabNPC(lua_State *state, int indx) {
EntityRef *ref = grabEntityRef(state, indx);
// if grabEntityRef failed, return error result
return (ref == NULL) ? NULL : (CombatNPC*)ref->getEntity();
}
static int32_t grabID(lua_State *state, int indx) {
EntityRef *ref = grabEntityRef(state, indx);
// if grabEntityRef failed, return error result
return (ref == NULL) ? -1 : ref->id;
}
// =============================================== [[ GETTERS ]] ===============================================
static int npc_getMaxHealth(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
// sanity check
if (npc == NULL)
return 0;
lua_pushinteger(state, npc->maxHealth);
return 1;
}
static int npc_getSpeed(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
// sanity check
if (npc == NULL)
return 0;
lua_pushinteger(state, npc->speed);
return 1;
}
static const luaL_Reg npc_getters[] = {
{"maxHealth", npc_getMaxHealth},
{"speed", npc_getSpeed},
{0, 0}
};
// =============================================== [[ SETTERS ]] ===============================================
static int npc_setMaxHealth(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
int newMH = luaL_checkint(state, 2);
// sanity check
if (npc != NULL)
npc->maxHealth = newMH;
return 0;
}
static int npc_setSpeed(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
int newSpeed = luaL_checkint(state, 2);
// sanity check
if (npc != NULL)
npc->speed = newSpeed;
return 0;
}
static const luaL_Reg npc_setters[] = {
{"maxHealth", npc_setMaxHealth},
{"speed", npc_setSpeed},
{0, 0}
};
// =============================================== [[ METHODS ]] ===============================================
static int npc_moveto(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
int X = luaL_checkint(state, 2);
int Y = luaL_checkint(state, 3);
int Z = luaL_checkint(state, 4);
// sanity check
if (npc == NULL)
return 0;
std::queue<Vec3> queue;
Vec3 from = { npc->x, npc->y, npc->z };
Vec3 to = { X, Y, Z };
// add a route to the queue; to be processed in Transport::stepNPCPathing()
Transport::lerp(&queue, from, to, npc->speed);
Transport::NPCQueues[npc->appearanceData.iNPC_ID] = queue;
return 0;
}
static int npc_say(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
std::string msg = std::string(luaL_checkstring(state, 2));
// sanity check
if (npc != NULL) {
INITSTRUCT(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, res);
U8toU16(msg, (char16_t*)&res.szFreeChat, sizeof(res.szFreeChat));
res.iPC_ID = npc->appearanceData.iNPC_ID;
res.iEmoteCode = 421;
// send the packet
NPCManager::sendToViewable(npc, &res, P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC));
}
return 0;
}
static const luaL_Reg npc_methods[] = {
{"moveTo", npc_moveto},
{"say", npc_say},
{0, 0}
};
// in charge of calling the correct getter method
static int plr_index(lua_State *state) {
// grab the function from the getters lookup table
lua_pushstring(state, GETTERTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// if it's not nil, call it and run the getter method
if (!lua_isnil(state, -1)) {
// push userdata & call the function
lua_pushvalue(state, 1);
lua_call(state, 1, 1);
// return # of results
return 1;
}
// grab the function from the methods lookup table
lua_pop(state, 1);
lua_pushstring(state, METHODTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// return result
return 1;
}
// in charge of calling the correct setter method
static int plr_newindex(lua_State *state) {
// grab the function from the getters lookup table
lua_pushstring(state, SETTERTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// if it's nil return
if (lua_isnil(state, -1))
return 0;
// push userdata & call the function
lua_pushvalue(state, 1);
lua_call(state, 1, 0);
// return # of results
return 0;
}
static int npc_new(lua_State *state) {
int X = luaL_checkint(state, 1);
int Y = luaL_checkint(state, 2);
int Z = luaL_checkint(state, 3);
int type = luaL_checkint(state, 4);
int id = NPCManager::nextId++;
// create & initalize the NPC
CombatNPC *NPC = new CombatNPC(X, Y, Z, 0, INSTANCE_OVERWORLD, type, id, 1000);
NPCManager::NPCs[id] = NPC;
NPCManager::updateNPCPosition(id, X, Y, Z, INSTANCE_OVERWORLD, 0);
// push it to the lua stack & return
LuaManager::NPC::push(state, NPC);
return 1;
}
void LuaManager::NPC::init(lua_State *state) {
// register our library as a global (and leave it on the stack)
luaL_register(state, LIBNAME, npc_methods);
// sets NPC.new
lua_pushstring(state, "new");
lua_pushcfunction(state, npc_new);
lua_rawset(state, -3);
// create the meta table and populate it with our functions
luaL_newmetatable(state, LIBNAME);
lua_pushstring(state, "__index");
lua_pushcfunction(state, plr_index);
lua_rawset(state, -3); // sets meta.__index = plr_index
lua_pushstring(state, "__newindex");
lua_pushcfunction(state, plr_newindex);
lua_rawset(state, -3); // sets meta.__newindex = plr_newindex
lua_pop(state, 2); // pop meta & library table
// create the methods table
lua_pushstring(state, METHODTBL);
lua_newtable(state);
Entity::addMethods(state); // register the base Entity methods
luaL_register(state, NULL, npc_methods);
lua_rawset(state, LUA_REGISTRYINDEX);
// create the getters table
lua_pushstring(state, GETTERTBL);
lua_newtable(state);
Entity::addGetters(state); // register the base Entity getters
luaL_register(state, NULL, npc_getters);
lua_rawset(state, LUA_REGISTRYINDEX);
// create the setters table
lua_pushstring(state, SETTERTBL);
lua_newtable(state);
luaL_register(state, NULL, npc_setters);
lua_rawset(state, LUA_REGISTRYINDEX);
LuaManager::Entity::registerSuper(state, LIBNAME);
}
void LuaManager::NPC::push(lua_State *state, CombatNPC *npc) {
Entity::push(state, EntityRef(npc->appearanceData.iNPC_ID), LIBNAME);
}

11
src/lua/NPCWrapper.hpp Normal file
View File

@@ -0,0 +1,11 @@
#include "lua/LuaWrapper.hpp"
#include "lua/LuaManager.hpp"
#include "lua/EntityWrapper.hpp"
namespace LuaManager {
namespace NPC {
void init(lua_State *state);
void push(lua_State *state, CombatNPC *npc);
}
}

278
src/lua/PlayerWrapper.cpp Normal file
View File

@@ -0,0 +1,278 @@
#include "lua/EntityWrapper.hpp"
#include "lua/EventWrapper.hpp"
#include "lua/PlayerWrapper.hpp"
#include "core/CNProtocol.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Chat.hpp"
#define LIBNAME "Player"
#define PLRGONESTR "Player doesn't exist anymore, they left!"
#define GETTERTBL "__plrGETTERS"
#define SETTERTBL "__plrSETTERS"
#define METHODTBL "__plrMETHODS"
static EntityRef* grabEntityRef(lua_State *state, int indx) {
// first, make sure its a userdata
luaL_checktype(state, indx, LUA_TUSERDATA);
// now, check and make sure its our library's metatable attached to this userdata
EntityRef *ref = (EntityRef*)luaL_checkudata(state, indx, LIBNAME);
if (ref == NULL) {
luaL_typerror(state, indx, LIBNAME);
return NULL;
}
// check if the player exists still & return NULL if it doesn't
if (!ref->isValid()) {
luaL_argerror(state, indx, PLRGONESTR);
return NULL;
}
return ref;
}
static Player* grabPlayer(lua_State *state, int indx) {
EntityRef *ref = grabEntityRef(state, indx);
if (ref == NULL)
return NULL;
return (Player*)ref->getEntity();
}
static CNSocket* grabSock(lua_State *state, int indx) {
EntityRef *ref = grabEntityRef(state, indx);
if (ref == NULL)
return NULL;
return ref->sock;
}
// =============================================== [[ GETTERS ]] ===============================================
static int plr_getName(lua_State *state) {
Player *plr = grabPlayer(state, 1);
if (plr == NULL)
return 0;
lua_pushstring(state, PlayerManager::getPlayerName(plr).c_str());
return 1;
}
static int plr_getInstance(lua_State *state) {
Player *plr = grabPlayer(state, 1);
if (plr == NULL)
return 0;
lua_pushnumber(state, plr->instanceID);
return 1;
}
static int plr_getChatted(lua_State *state) {
Player *plr = grabPlayer(state, 1);
if (plr == NULL)
return 0;
// the Player* entity doesn't actually have an lEvent setup until a lua script asks for it, so
// if Player->onChat is nullptr, create the lEvent and then push it :D
if (plr->onChat == nullptr)
plr->onChat = new lEvent();
LuaManager::Event::push(state, plr->onChat);
return 1;
}
static const luaL_Reg plr_getters[] = {
{"name", plr_getName},
{"instance", plr_getInstance},
{"onChat", plr_getChatted},
{0, 0}
};
// =============================================== [[ SETTERS ]] ===============================================
static int plr_setInstance(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
int newInst = luaL_checkint(state, 2);
Player *plr;
CNSocket *sock;
// sanity check
if (ref == NULL)
return 0;
plr = (Player*)ref->getEntity();
sock = ref->sock;
PlayerManager::sendPlayerTo(sock, plr->x, plr->y, plr->z, newInst);
return 0;
}
static const luaL_Reg plr_setters[] = {
{"instance", plr_setInstance},
{0, 0}
};
// =============================================== [[ METHODS ]] ===============================================
static int plr_kick(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
Player *plr;
CNSocket *sock;
// sanity check
if (ref == NULL)
return 0;
plr = (Player*)ref->getEntity();
sock = ref->sock;
// construct packet
INITSTRUCT(sP_FE2CL_REP_PC_EXIT_SUCC, response);
response.iID = plr->iID;
response.iExitCode = 3; // "a GM has terminated your connection"
// send to target player
sock->sendPacket(response, P_FE2CL_REP_PC_EXIT_SUCC);
// ensure that the connection has terminated
sock->kill();
return 0;
}
static int plr_teleport(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
Player *plr;
CNSocket *sock;
// sanity check
if (ref == NULL)
return 0;
plr = (Player*)ref->getEntity();
sock = ref->sock;
int X = luaL_checkint(state, 2);
int Y = luaL_checkint(state, 3);
int Z = luaL_checkint(state, 4);
int inst = luaL_optint(state, 5, plr->instanceID);
PlayerManager::sendPlayerTo(sock, X, Y, Z, inst);
return 0;
}
static int plr_msg(lua_State *state) {
CNSocket *sock = grabSock(state, 1);
const char *msg = luaL_checkstring(state, 2);
// sanity check
if (sock != NULL)
Chat::sendServerMessage(sock, std::string(msg));
return 0;
}
static const luaL_Reg plr_methods[] = {
{"kick", plr_kick},
{"teleport", plr_teleport},
{"message", plr_msg},
{0, 0}
};
// in charge of calling the correct getter method
static int plr_index(lua_State *state) {
// grab the function from the getters lookup table
lua_pushstring(state, GETTERTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// if it's not nil, call it and run the getter method
if (!lua_isnil(state, -1)) {
// push userdata & call the function
lua_pushvalue(state, 1);
lua_call(state, 1, 1);
// return # of results
return 1;
}
// grab the function from the methods lookup table
lua_pop(state, 1);
lua_pushstring(state, METHODTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// return result
return 1;
}
// in charge of calling the correct setter method
static int plr_newindex(lua_State *state) {
// grab the function from the getters lookup table
lua_pushstring(state, SETTERTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// if it's nil return
if (lua_isnil(state, -1))
return 0;
// push userdata & call the function
lua_pushvalue(state, 1);
lua_call(state, 1, 0);
// return # of results
return 0;
}
void LuaManager::Player::init(lua_State *state) {
// register our library as a global (and leave it on the stack)
luaL_register(state, LIBNAME, plr_methods);
// create the meta table and populate it with our functions
luaL_newmetatable(state, LIBNAME);
lua_pushstring(state, "__index");
lua_pushcfunction(state, plr_index);
lua_rawset(state, -3); // sets meta.__index = plr_index
lua_pushstring(state, "__newindex");
lua_pushcfunction(state, plr_newindex);
lua_rawset(state, -3); // sets meta.__newindex = plr_newindex
lua_pop(state, 2); // pop meta & library table
// create the methods table
lua_pushstring(state, METHODTBL);
lua_newtable(state);
Entity::addMethods(state); // register the base Entity methods
luaL_register(state, NULL, plr_methods);
lua_rawset(state, LUA_REGISTRYINDEX);
// create the getters table
lua_pushstring(state, GETTERTBL);
lua_newtable(state);
Entity::addGetters(state); // register the base Entity getters
luaL_register(state, NULL, plr_getters);
lua_rawset(state, LUA_REGISTRYINDEX);
// create the setters table
lua_pushstring(state, SETTERTBL);
lua_newtable(state);
luaL_register(state, NULL, plr_setters);
lua_rawset(state, LUA_REGISTRYINDEX);
LuaManager::Entity::registerSuper(state, LIBNAME);
}
void LuaManager::Player::push(lua_State *state, CNSocket *sock) {
Entity::push(state, EntityRef(sock), LIBNAME);
}

11
src/lua/PlayerWrapper.hpp Normal file
View File

@@ -0,0 +1,11 @@
#include "lua/LuaWrapper.hpp"
#include "lua/LuaManager.hpp"
#include "lua/EntityWrapper.hpp"
namespace LuaManager {
namespace Player {
void init(lua_State *state);
void push(lua_State *state, CNSocket *sock);
}
}

130
src/lua/WorldWrapper.cpp Normal file
View File

@@ -0,0 +1,130 @@
#include "lua/LuaManager.hpp"
#include "lua/LuaWrapper.hpp"
#include "lua/EventWrapper.hpp"
#include "lua/WorldWrapper.hpp"
#include "core/CNStructs.hpp"
#include "PlayerManager.hpp"
static lEvent *addedEvent;
static lEvent *removedEvent;
#define LIBNAME "World"
#define GETTERTBL "__wrldGETTERS"
#define METHODTBL "__wrldMETHODS"
// =============================================== [[ GETTERS ]] ===============================================
int wrld_getPlrAdded(lua_State *state) {
LuaManager::Event::push(state, addedEvent);
return 1;
}
int wrld_getPlrRemoved(lua_State *state) {
LuaManager::Event::push(state, removedEvent);
return 1;
}
int wrld_getVersion(lua_State *state) {
lua_pushnumber(state, PROTOCOL_VERSION);
return 1;
}
int wrld_getPlayers(lua_State *state) {
// create a new lua table and push it onto the stack
int entries = 0;
lua_newtable(state);
// walk through the current list of players and add them to the table
for (auto pair : PlayerManager::players) {
lua_pushinteger(state, ++entries);
LuaManager::Player::push(state, pair.first);
lua_rawset(state, -3);
}
// returns the player table :)
return 1;
}
// =============================================== [[ METHODS ]] ===============================================
int wrld_index(lua_State *state) {
// grab the function from the getters lookup table
lua_pushstring(state, GETTERTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// if it's not nil, call it and run the getter method
if (!lua_isnil(state, -1)) {
// push userdata & call the function
lua_pushvalue(state, 1);
lua_call(state, 1, 1);
// return # of results
return 1;
}
// grab the function from the methods lookup table
lua_pop(state, 1);
lua_pushstring(state, METHODTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// return result
return 1;
}
static const luaL_Reg getters[] {
{"onPlayerAdded", wrld_getPlrAdded},
{"onPlayerRemoved", wrld_getPlrRemoved},
{"players", wrld_getPlayers},
{"version", wrld_getVersion},
{0, 0}
};
// TODO
static const luaL_Reg methods[] = {
//{"getNearbyPlayers", wrld_getNPlrs},
{0, 0}
};
void LuaManager::World::init(lua_State *state) {
lua_newtable(state);
luaL_newmetatable(state, LIBNAME);
lua_pushstring(state, "__index");
lua_pushcfunction(state, wrld_index);
lua_rawset(state, -3); // sets meta.__index = wrld_index
lua_setmetatable(state, -2); // sets world.__metatable = meta
lua_setglobal(state, LIBNAME);
// setup the __wrldGETTERS table in the registry
lua_pushstring(state, GETTERTBL);
lua_newtable(state);
luaL_register(state, NULL, getters);
lua_rawset(state, LUA_REGISTRYINDEX);
// setup the __wrldMETHODS table in the registry
lua_pushstring(state, METHODTBL);
lua_newtable(state);
luaL_register(state, NULL, methods);
lua_rawset(state, LUA_REGISTRYINDEX);
addedEvent = new lEvent();
removedEvent = new lEvent();
}
void LuaManager::World::clearState(lua_State *state) {
addedEvent->clear(state);
removedEvent->clear(state);
}
void LuaManager::World::playerAdded(CNSocket *sock) {
addedEvent->call(sock);
}
void LuaManager::World::playerRemoved(CNSocket *sock) {
removedEvent->call(sock);
}

13
src/lua/WorldWrapper.hpp Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include "lua/LuaManager.hpp"
namespace LuaManager {
namespace World {
void init(lua_State *state);
void clearState(lua_State *state);
void playerAdded(CNSocket *sock);
void playerRemoved(CNSocket *sock);
}
}

View File

@@ -1,5 +1,6 @@
#include "servers/CNLoginServer.hpp" #include "servers/CNLoginServer.hpp"
#include "servers/CNShardServer.hpp" #include "servers/CNShardServer.hpp"
#include "lua/LuaManager.hpp"
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "PlayerMovement.hpp" #include "PlayerMovement.hpp"
#include "BuiltinCommands.hpp" #include "BuiltinCommands.hpp"
@@ -25,7 +26,6 @@
#include "Rand.hpp" #include "Rand.hpp"
#include "settings.hpp" #include "settings.hpp"
#include "sandbox/Sandbox.hpp"
#include "../version.h" #include "../version.h"
@@ -49,7 +49,6 @@ CNShardServer *shardServer = nullptr;
std::thread *shardThread = nullptr; std::thread *shardThread = nullptr;
void startShard(CNShardServer* server) { void startShard(CNShardServer* server) {
sandbox_thread_start();
server->start(); server->start();
} }
@@ -64,20 +63,8 @@ void terminate(int arg) {
exit(0); exit(0);
} }
#ifdef _WIN32 #ifndef _WIN32
static BOOL WINAPI winTerminate(DWORD arg) {
terminate(0);
return FALSE;
}
#endif
void initsignals() { void initsignals() {
#ifdef _WIN32
if (!SetConsoleCtrlHandler(winTerminate, TRUE)) {
std::cerr << "[FATAL] Failed to set control handler" << std::endl;
exit(1);
}
#else
struct sigaction act; struct sigaction act;
memset((void*)&act, 0, sizeof(act)); memset((void*)&act, 0, sizeof(act));
@@ -95,29 +82,25 @@ void initsignals() {
perror("sigaction"); perror("sigaction");
exit(1); exit(1);
} }
#endif
} }
#endif
int main() { int main() {
std::cout << "[INFO] OpenFusion v" GIT_VERSION << std::endl;
std::cout << "[INFO] Protocol version: " << PROTOCOL_VERSION << std::endl;
#ifdef _WIN32 #ifdef _WIN32
WSADATA wsaData; WSADATA wsaData;
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) { if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
std::cerr << "OpenFusion: WSAStartup failed" << std::endl; std::cerr << "OpenFusion: WSAStartup failed" << std::endl;
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
#endif #else
initsignals(); initsignals();
settings::init(); #endif
Database::init();
Rand::init(getTime()); Rand::init(getTime());
TableData::init(); settings::init();
std::cout << "[INFO] OpenFusion v" GIT_VERSION << std::endl;
std::cout << "[INFO] Protocol version: " << PROTOCOL_VERSION << std::endl;
std::cout << "[INFO] Intializing Packet Managers..." << std::endl; std::cout << "[INFO] Intializing Packet Managers..." << std::endl;
TableData::init();
PlayerManager::init(); PlayerManager::init();
PlayerMovement::init(); PlayerMovement::init();
BuiltinCommands::init(); BuiltinCommands::init();
@@ -136,9 +119,9 @@ int main() {
Email::init(); Email::init();
Groups::init(); Groups::init();
Racing::init(); Racing::init();
Trading::init();
Database::open(); Database::open();
Trading::init();
LuaManager::init();
switch (settings::EVENTMODE) { switch (settings::EVENTMODE) {
case 0: break; // no event case 0: break; // no event
@@ -151,17 +134,12 @@ int main() {
/* not reached */ /* not reached */
} }
sandbox_init();
std::cout << "[INFO] Starting Server Threads..." << std::endl; std::cout << "[INFO] Starting Server Threads..." << std::endl;
CNLoginServer loginServer(settings::LOGINPORT); CNLoginServer loginServer(settings::LOGINPORT);
shardServer = new CNShardServer(settings::SHARDPORT); shardServer = new CNShardServer(settings::SHARDPORT);
shardThread = new std::thread(startShard, (CNShardServer*)shardServer); shardThread = new std::thread(startShard, (CNShardServer*)shardServer);
sandbox_start();
sandbox_thread_start();
loginServer.start(); loginServer.start();
shardServer->kill(); shardServer->kill();
@@ -176,15 +154,10 @@ int main() {
// helper functions // helper functions
std::string U16toU8(char16_t* src, size_t max) { std::string U16toU8(char16_t* src, size_t max) {
src[max-1] = '\0'; // force a NULL terminator src[max-1] = '\0'; // force a NULL terminatorstd::string U16toU8(char16_t* src) {
try { try {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert; std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
std::string ret = convert.to_bytes(src); return convert.to_bytes(src);
if (ret.size() >= max)
ret.resize(max-2);
return ret;
} catch(const std::exception& e) { } catch(const std::exception& e) {
return ""; return "";
} }
@@ -208,7 +181,7 @@ size_t U8toU16(std::string src, char16_t* des, size_t max) {
time_t getTime() { time_t getTime() {
using namespace std::chrono; using namespace std::chrono;
milliseconds value = duration_cast<milliseconds>((time_point_cast<milliseconds>(steady_clock::now())).time_since_epoch()); milliseconds value = duration_cast<milliseconds>((time_point_cast<milliseconds>(high_resolution_clock::now())).time_since_epoch());
return (time_t)value.count(); return (time_t)value.count();
} }
@@ -222,17 +195,6 @@ time_t getTimestamp() {
return (time_t)value.count(); 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 // convert integer timestamp (in s) to FF systime struct
sSYSTEMTIME timeStampToStruct(uint64_t time) { sSYSTEMTIME timeStampToStruct(uint64_t time) {

View File

@@ -1,28 +0,0 @@
#pragma once
// use the sandbox on supported platforms, unless disabled
#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;
}
# endif // CONFIG_NOSANDBOX
#else
// stub for unsupported platforms
inline void sandbox_init() {}
inline void sandbox_start() {}
inline void sandbox_thread_start() {}
#endif

Some files were not shown because too many files have changed in this diff Show More