1
0
mirror of https://github.com/p2r3/bareiron.git synced 2025-10-01 23:25:09 +02:00

59 Commits
latest ... main

Author SHA1 Message Date
7e23cabfce getrandomg 2025-09-27 22:07:55 +02:00
João Lucas
d02608c69d perf: Optimize main loop to eliminate idle CPU usage 2025-09-27 22:07:51 +02:00
7e7a98bd41 "deferred block updates": falling sand and flowing water 2025-09-27 22:07:26 +02:00
90a7a8b48e FIX: MAX_RECV_BUFFER_LEN not used in header 2025-09-27 22:06:44 +02:00
9fb3e870bf clang tooling helpers 2025-09-27 22:06:39 +02:00
p2r3
d272e63dd7 fix compilation error when targeting esp-idf 2025-09-27 00:51:40 +03:00
p2r3
182a180c2e update contribution guidelines 2025-09-23 13:25:45 +03:00
Floof
450bef9e6c validate block change buffer boundaries for chests 2025-09-22 04:19:30 +03:00
p2r3
516a00f122 fix various compilation issues when targeting esp-idf 2025-09-19 02:24:36 +03:00
p2r3
a631de77b5 fix wrong expression order in mob movement 2025-09-19 02:12:00 +03:00
Tyler Zars
3bde692976 implement recv_buffer size limit and input validation
* Implement recv_buffer size limit and input validation

* Add readLengthPrefixedData helper and refactor some read flows

* Change error to be more accurate

* Add newline to error message

* fix long chat messages kicking clients

* style nitpicks

---------

Co-authored-by: p2r3 <p2r3@p2r3.com>
Co-authored-by: p2r3 <41925384+p2r3@users.noreply.github.com>
2025-09-19 02:00:02 +03:00
M6a5x98
b23e19ecd4 implement !msg and !help chat commands
* Added the !msg and !help command

* Started fixing issues

* Rewrote !msg processing in chat system

* cleanup

---------

Co-authored-by: p2r3 <p2r3@p2r3.com>
2025-09-19 01:20:50 +03:00
SDFTDusername
ba86dfd927 implement sheep shearing
* Added shearing sheeps

* Added empty line

* Changed comment in MobData to say what the middle 1 bit is used for

* Replaced if statements with a switch statement and early returns

* Send mob metadata to players that join

* Fixed mob metadata getting freed when exiting the switch statement

* remove currently unnecessary check

* use bitwise and in place of modulo

* style nitpicks

---------

Co-authored-by: p2r3 <p2r3@p2r3.com>
2025-09-16 13:36:42 +03:00
p2r3
244c98552f add note about dev tools in contribution guidelines 2025-09-16 11:37:45 +03:00
p2r3
e88bf47d5b create issue templates 2025-09-15 22:41:06 +03:00
SDFTDusername
c5e8408052 implement player poses (sneak/sprint)
* Added sending player metadata with poses to everyone

* Renamed sendPlayerMetadataToAll to broadcastPlayerMetadata

* Made entity metadata system flexible

* Made broadcastPlayerMetadata create new metadata instead of using a global one

* Moved writeEntityData, sizeEntityData and sizeEntityMetadata to procedures

* style nitpicks

---------

Co-authored-by: p2r3 <p2r3@p2r3.com>
2025-09-15 22:01:22 +03:00
Henry Wandover
e5dfe53d14 implement cactus damage
* Added cactus damage for players and issues the respective death message.

Halts if condition is reached

* style nitpicks

---------

Co-authored-by: p2r3 <p2r3@p2r3.com>
2025-09-15 21:30:33 +03:00
Un1q32
ff89519340 add missing static statement to inline tools 2025-09-15 15:11:35 +03:00
vil02
200106bb46 Close file on error paths to prevent resource leak 2025-09-14 21:19:54 +03:00
jcfb
6a65b8acba support 32-bit compilation on windows
* Add support for 32 bit cross compile in mingw64 and windows 98 support

* add check to error out on linux

* update text

* add instructions for 32-bit compilation on windows

---------

Co-authored-by: p2r3 <41925384+p2r3@users.noreply.github.com>
2025-09-14 18:56:17 +03:00
jcfb
26f068bc09 support native windows binary compilation
* add winsock2 to globals.c

* add winsocket2 compatibility to main function

* add winsocket2 to packets.c

* and winsocket2 to procedures.c

* add winsocket 2 compatibility to tools.c

* add winsocket2 to varnum.c

* add mingw64 linker options to build.sh

* fix build

* style nitpicks

* remove old_fd

* update compilation instructions for windows

---------

Co-authored-by: p2r3 <41925384+p2r3@users.noreply.github.com>
2025-09-14 17:11:56 +03:00
SDFTDusername
f6333429eb implement arm swinging animation
* Added packets to show players swinging their arms

* fix misleading comment

---------

Co-authored-by: p2r3 <p2r3@p2r3.com>
2025-09-14 16:59:25 +03:00
p2r3
a34134918a implement mob "panic" behavior 2025-09-14 15:37:05 +03:00
p2r3
e7e9d307e6 fix chunks sometimes not loading after unclean shutdown 2025-09-14 14:59:13 +03:00
p2r3
925b841e95 refactor mob movement
Fixes #64
2025-09-14 14:58:31 +03:00
p2r3
1f9aa50573 fix delayed fall damage from swimming
Closes #61
2025-09-14 03:23:50 +03:00
p2r3
39f5c69bc3 fix crash when overwriting terrain with a chest
Closes #48
2025-09-14 02:08:16 +03:00
p2r3
e81e75a6d7 ensure mob uuid uniqueness on login
Closes #51
2025-09-14 01:30:42 +03:00
p2r3
61eb38a83d fix block changes not syncing on interval 2025-09-13 21:58:03 +03:00
p2r3
bdb4e4b72c remove trailing whitespace 2025-09-13 21:55:01 +03:00
p2r3
e3589a02f0 fix player entities desyncing on join sometimes 2025-09-13 20:06:25 +03:00
p2r3
bad337a032 fix compilation error when targeting esp-idf 2025-09-13 18:26:31 +03:00
breakgimme
6f260383cd correct windows build documentation 2025-09-13 16:45:46 +03:00
p2r3
7e80e12b26 add bareiron.exe to gitignore 2025-09-13 14:24:25 +03:00
Bradlee Barnes
969f7d3974 send server brand during login
* send server brand to client

* send brand regardless of `0x02` being recieved by server or not

* send brand regardless of `0x02` being recieved by server or not
+ if statement to match previous lines

* ifdef check for brand sending

* commit main.c for ifdef check (i forgot)

* commit main.c for ifdef check (i forgot)

* change sc_pluginMessage to sc_sendPluginMessage

- added params to it for use in other cases
- other changes to fit PR reviews

* revert .gitignore

* revert unrelated changes

* send byte instead of varint for constant packet id

* gate declaration of brand string behind ifdef

* make logging consistent with codebase

---------

Co-authored-by: p2r3 <p2r3@p2r3.com>
2025-09-13 14:22:06 +03:00
p2r3
2a9e443a8d fix compilation on windows 2025-09-13 14:09:13 +03:00
anmol-patankar
8d75d0a75f fix path handling in build.bat 2025-09-13 09:01:52 +03:00
p2r3
70b3b048db improve esp compilation instructions in readme 2025-09-13 00:36:40 +03:00
fox3000foxy
0f1cadd341 add warning about instability when using modded clients
* Added Fabric Notice

* rephrase fabric warning

---------

Co-authored-by: p2r3 <41925384+p2r3@users.noreply.github.com>
2025-09-13 00:18:16 +03:00
p2r3
efac4c125d fix compilation error on label syntax quirk 2025-09-13 00:10:33 +03:00
KornineQ
fddef798e3 Don't take fall damage in creative or spectator 2025-09-12 23:08:13 +03:00
Mefiresu
808bbb26a2 Remove BlockChange from packed structs
pragma pack (push, 1) forces alignment to 1-byte.
While reordering the fields does help with memory accesses, it only
works if the compiler can ensure 2-byte alignments for this struct.
2025-09-12 23:05:10 +03:00
anmol-patankar
ba03d92ad0 fix mobs spawning in water
* Fixed underwater under lava mob spawn issue #17

* made comment changes

* minor fix

* method name change

* method change

* fixed
2025-09-12 19:53:08 +03:00
p2r3
dabb202c13 mention testing in contribution guidelines 2025-09-12 19:39:33 +03:00
CodeAsm
eef1020ad8 automate vanilla registry extraction on linux
* Adding a simple check if someone has read the readme

* semi automate the registeries generation. still requires manually downloading server.jar for eula purposes

* The mods have spoken: no spelling mistakes allowed.

* mention extract_registries script in readme

---------

Co-authored-by: p2r3 <41925384+p2r3@users.noreply.github.com>
2025-09-12 19:31:03 +03:00
fox3000foxy
81865cb7ac add string bounds check when copying player name 2025-09-12 19:04:36 +03:00
Anmol
7206dd1b11 create windows build script
* Create build.bat

* Update build.bat

* added error handling

* changed error to match build.sh

* update readme to mention windows build script

---------

Co-authored-by: zyriu1 <Suvkq@hotmail.com>
Co-authored-by: p2r3 <41925384+p2r3@users.noreply.github.com>
2025-09-12 18:41:01 +03:00
breakgimme
3d02dc02bf use libc htonll if found 2025-09-12 15:27:49 +03:00
p2r3
5b8360708c clarify memory footprint of VISITED_HISTORY 2025-09-12 15:12:46 +03:00
p2r3
5a100bcd23 disable chunk generation logging by default 2025-09-12 15:10:44 +03:00
p2r3
46a808152a document important configuration options for real servers 2025-09-12 15:09:36 +03:00
p2r3
ba11d121cf reorder coordinates in BlockChange struct 2025-09-12 14:52:49 +03:00
p2r3
a83acbda67 add option to tie movement updates to tickrate 2025-09-11 22:00:32 +03:00
p2r3
e4c0c6b6e9 fix armor not equipping from right click 2025-09-10 20:11:59 +03:00
p2r3
55da6c7d4c fix features generating when they shouldn't 2025-09-09 21:20:40 +03:00
p2r3
5c571df947 clarify details in readme 2025-09-09 19:33:52 +03:00
p2r3
125203980b add current game and protocol version to readme 2025-09-09 14:01:34 +03:00
p2r3
362f9e925b document latest build binary in readme 2025-09-09 13:59:39 +03:00
p2r3
fdb6f6dd15 add release body to build workflow 2025-09-09 13:51:00 +03:00
23 changed files with 1451 additions and 352 deletions

2
.clang-format-ignore Normal file
View File

@@ -0,0 +1,2 @@
src/
include/

40
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,40 @@
---
name: Bug report
about: Report a gameplay bug. Compilation issues do not count.
title: 'Bug report: '
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Write clear step-by-step instructions on how to reproduce the bug from a new world:
1.
2.
3.
**Expected behavior**
A clear and concise description of what you expected to happen, compared to what actually happened.
**Screenshots**
If applicable, add screenshots or videos to help explain your problem.
**Minecraft client details:**
- Version:
- Mods:
**Platform (if desktop):**
- Operating System:
- CPU:
**Platform (if ESP variant):**
- Chip:
- Development board:
- Power supply:
- Internet properties:
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,16 @@
---
name: Feature request
about: Suggest a gameplay feature or enhancement. Development tooling does not count.
title: 'Feature request:'
labels: feature request
assignees: ''
---
**Describe the feature:**
A clear and concise description of the feature you wish to see implemented.
**Explain why this feature is necessary:**
A clear and concise explanation of why this feature is important. Consider what compromises are made to memory/performance and weigh your arguments against those.
**Are you planning on implementing this yourself? If so, explain the approach you intend to take:**

View File

@@ -0,0 +1,16 @@
---
name: Optimization idea
about: Suggest a potential memory or performance optimization.
title: 'Optimization: '
labels: optimization
assignees: ''
---
**Describe the optimization:**
A clear and concise description of the optimization you wish to implement or see implemented.
**Explain why this feature is necessary:**
A clear and concise explanation of the effects of this optimization, and why it is important. Please remember that this project's priorities are **memory first, performance second**. If your suggestion requires compromising memory usage for performance, make a case for why the tradeoff is worth it.
**Are you planning on implementing this yourself? If so, explain the approach you intend to take:**

View File

@@ -54,8 +54,8 @@ jobs:
with:
tag_name: latest
name: Latest build
body: "This is a rolling release of bareiron, compiled from the latest commit on the main branch.\n\nThe executable (`bareiron.exe`) is a Cosmopolitan-built polyglot, and should run on Windows and Linux (and Mac?), despite the file extension."
files: bareiron.exe
overwrite: true
prerelease: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

6
.gitignore vendored
View File

@@ -1,6 +1,12 @@
a.out
*.exe
*.o
*.a
core
.vscode
notchian
bareiron
bareiron.exe
src/registries.c
include/registries.h
*.bin

View File

@@ -3,14 +3,38 @@ Minimalist Minecraft server for memory-restrictive embedded systems.
The goal of this project is to enable hosting Minecraft servers on very weak devices, such as the ESP32. The project's priorities are, in order: **memory usage**, **performance**, and **features**. Because of this, compliance with vanilla Minecraft is not guaranteed, nor is it a goal of the project.
## Quick start
Before compiling, you'll need to dump registry data from a vanilla Minecraft server. Create a folder called `notchian` here, and put a Minecraft server JAR in it. Then, follow [this guide](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Data_Generators) to dump all of the registries. Finally, run `build_registries.js` with `node`, `bun`, or `deno`.
- Minecraft version: `1.21.8`
- Protocol version: `772`
- To target Linux, install `gcc` and run `build.sh`
- To target an ESP variant, set up a PlatformIO project and clone this repository on top of it.
> [!WARNING]
> Currently, only the vanilla client is officially supported. Issues have been reported when using Fabric or similar.
## Quick start
For PC x86_64 platforms, grab the [latest build binary](https://github.com/p2r3/bareiron/releases/download/latest/bareiron.exe) and run it. The file is a [Cosmopolitan polyglot](https://github.com/jart/cosmopolitan), which means it'll run on Windows, Linux, and possibly Mac, despite the file extension. Note that the server's default settings cannot be reconfigured without compiling from source.
For microcontrollers, see the section on **compilation** below.
## Compilation
Before compiling, you'll need to dump registry data from a vanilla Minecraft server. On Linux, this can be done automatically using the `extract_registries.sh` script. Otherwise, the manual process is as follows: create a folder called `notchian` here, and put a Minecraft server JAR in it. Then, follow [this guide](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Data_Generators) to dump all of the registries (use the _second_ command with the `--all` flag). Finally, run `build_registries.js` with either [bun](https://bun.sh/), [node](https://nodejs.org/en/download), or [deno](https://docs.deno.com/runtime/getting_started/installation/).
- To compile on Linux, install `gcc` and run `./build.sh`.
- For compiling on Windows, there are a few options:
- To compile a native Windows binary: install [MSYS2](https://www.msys2.org/) and open the "MSYS2 MINGW64" shell. From there, run `pacman -Sy mingw-w64-x86_64-gcc`, navigate to this project's directory, and run `./build.sh`.
- To compile a native 32-bit binary (compatible with Windows 95/98, but why would you ever want that), use the same steps above, except with `pacman -Sy mingw-w64-cross-gcc` and `./build.sh --9x`.
- To compile a MSYS2-linked binary: install [MSYS2](https://www.msys2.org/), and open the "MSYS2 MSYS" shell. From there, install `gcc` (run `pacman -Sy gcc`), navigate to this project's directory and run `./build.sh`.
- To compile and run a Linux binary from Windows: install WSL, and from there install `gcc` and run `./build.sh` in this project's directory.
- To target an ESP variant, set up a PlatformIO project (select the ESP-IDF framework, **not Arduino**) and clone this repository on top of it. See **Configuration** below for further steps. For better performance, consider changing the clock speed and enabling compiler optimizations. If you don't know how to do this, there are plenty of resources online.
## Configuration
Most user-friendly configuration options are available in `include/globals.h`, including WiFi credentials for embedded setups. Of course, many more things can be configured by editing the source code.
Configuring the server requires compiling it from its source code as described in the section above.
Most user-friendly configuration options are available in `include/globals.h`, including WiFi credentials for embedded setups. Some other details, like the MOTD or starting time of day, can be found in `src/globals.c`. For everything else, you'll have to dig through the code.
Here's a summary of some of the more important yet less trivial options for those who plan to use this on a real microcontroller with real players:
- Depending on the player count, the performance of the MCU, and the bandwidth of your network, player position broadcasting could potentially throttle your connection. If you find this to be the case, try commenting out `BROADCAST_ALL_MOVEMENT` and `SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT`. This will tie movement to the tickrate. If this change makes movement too choppy, you can decrease `TIME_BETWEEN_TICKS` at the cost of more compute.
- If you experience crashes or instability related to chests or water, those features can be disabled with `ALLOW_CHESTS` and `DO_FLUID_FLOW`, respectively.
- If you find frequent repeated chunk generation to choke the server, increasing `VISITED_HISTORY` might help. There isn't _that_ much of a memory footprint for this - increasing it to `64` for example would only take up 240 extra bytes per allocated player.
## Non-volatile storage (optional)
This section applies to those who target ESP variants and wish to persist world data after a shutdown. *This is not necessary on PC platforms*, as world and player data is written to `world.bin` by default.
@@ -22,5 +46,8 @@ If using an SD card module or other virtual file system, you'll have to implemen
Alternatively, if you can't set up a file system, you can dump and upload world data over TCP. This can be enabled by uncommenting `DEV_ENABLE_BEEF_DUMPS` in `globals.h`. *Note: this system implements no security or authentication.* With this option enabled, anyone with access to the server can upload arbitrary world data.
## Contribution
- Create issues and discuss with the maintainer(s) before making pull requests.
- Create issues and discuss with the maintainer(s) before making pull requests. Even for small changes.
- Follow the existing code style. Ensure that your changes fit in with the surrounding code, even if you disagree with the style. Pull requests with inconsistent style will be nitpicked.
- Test your code before creating a pull request or requesting a review, regardless of how "simple" your change is. It's a basic form of respect towards the maintainer and reviewer.
- Development tooling and compilation improvements _are not welcome,_ unless you've worked with the codebase long enough to have noticed practical shortcomings in that area. Adding a single compiler flag is not a meaningful first contribution.
- For information on the Minecraft server protocol, [refer to the wiki](https://minecraft.wiki/w/Java_Edition_protocol/Packets). For everything else, use a [search engine](https://google.com).

View File

@@ -1,3 +1,45 @@
rm bareiron
gcc src/*.c -O3 -Iinclude -o bareiron
./bareiron
#!/usr/bin/env bash
# Check for registries before attempting to compile, prevents confusion
if [ ! -f "include/registries.h" ]; then
echo "Error: 'include/registries.h' is missing."
echo "Please follow the 'Compilation' section of the README to generate it."
exit 1
fi
# Figure out executable suffix (for MSYS compilation)
case "$OSTYPE" in
msys*|cygwin*|win32*) exe=".exe" ;;
*) exe="" ;;
esac
# mingw64-specific linker options
windows_linker=""
unameOut="$(uname -s)"
case "$unameOut" in
MINGW64_NT*)
windows_linker="-static -lws2_32 -pthread"
;;
esac
# Default compiler
compiler="gcc"
# Handle arguments for windows 9x build
for arg in "$@"; do
case $arg in
--9x)
if [[ "$unameOut" == MINGW64_NT* ]]; then
compiler="/opt/bin/i686-w64-mingw32-gcc"
windows_linker="$windows_linker -Wl,--subsystem,console:4"
else
echo "Error: Compiling for Windows 9x is only supported when running under the MinGW64 shell."
exit 1
fi
;;
esac
done
rm -f "bareiron$exe"
$compiler src/*.c -O3 -Iinclude -o "bareiron$exe" $windows_linker
"./bareiron$exe"

1
compile_flags.txt Normal file
View File

@@ -0,0 +1 @@
-Iinclude

73
extract_registries.sh Executable file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail
REQUIRED_MAJOR=21
SERVER_JAR="${SERVER_JAR:-server.jar}"
NOTCHIAN_DIR="notchian"
JS_RUNTIME=""
get_java_version() {
java -version 2>&1 | awk -F[\".] '/version/ {print $2}'
}
check_java() {
if ! command -v java >/dev/null 2>&1; then
echo "Java not found in PATH."
exit 1
fi
local major
major="$(get_java_version)"
if (( major < REQUIRED_MAJOR )); then
echo "Java $REQUIRED_MAJOR or newer required, but found Java $major."
exit 1
fi
}
prepare_notchian_dir() {
if [[ ! -d "$NOTCHIAN_DIR" ]]; then
echo "Creating $NOTCHIAN_DIR directory..."
mkdir -p "$NOTCHIAN_DIR"
fi
cd "$NOTCHIAN_DIR"
}
dump_registries() {
if [[ ! -f "$SERVER_JAR" ]]; then
echo "No server.jar found (looked for $SERVER_JAR)."
echo "Please download the server.jar from https://www.minecraft.net/en-us/download/server"
echo "and place it in the notchian directory."
exit 1
fi
java -DbundlerMainClass="net.minecraft.data.Main" -jar "$SERVER_JAR" --all
}
detect_js_runtime() {
if command -v node >/dev/null 2>&1; then
JS_RUNTIME="node"
elif command -v bun >/dev/null 2>&1; then
JS_RUNTIME="bun"
elif command -v deno >/dev/null 2>&1; then
JS_RUNTIME="deno run"
else
echo "No JavaScript runtime found (Node.js, Bun, or Deno)."
exit 1
fi
}
run_js_script() {
local script="$1"
if [[ -z "$JS_RUNTIME" ]]; then
detect_js_runtime
fi
echo "Running $script with $JS_RUNTIME..."
$JS_RUNTIME "$script"
}
check_java
prepare_notchian_dir
dump_registries
run_js_script "../build_registries.js"
echo "Registry dump and processing complete."

View File

@@ -34,20 +34,12 @@
// Max render distance, determines how many chunks to send
#define VIEW_DISTANCE 2
// Time between server ticks in microseconds (default = 1s)
#define TIME_BETWEEN_TICKS 1000000
// Time between server ticks in microseconds (default = 0.05s)
#define TIME_BETWEEN_TICKS 50000
// Calculated from TIME_BETWEEN_TICKS
#define TICKS_PER_SECOND ((float)1000000 / TIME_BETWEEN_TICKS)
// Initial world generation seed, will be hashed on startup
// Used in generating terrain and biomes
#define INITIAL_WORLD_SEED 0xA103DE6C
// Initial general RNG seed, will be hashed on startup
// Used in random game events like item drops and mob behavior
#define INITIAL_RNG_SEED 0xE2B9419
// Size of each bilinearly interpolated area ("minichunk")
// For best performance, CHUNK_SIZE should be a power of 2
#define CHUNK_SIZE 8
@@ -74,6 +66,10 @@
// Determines the fixed amount of memory allocated to blocks
#define MAX_BLOCK_CHANGES 20000
// How many "deferred" (happen at a later time than triggered) block updates to store.
// Determines the fixed amount of memory allocated to that list
#define MAX_DEFERRED_BLOCK_UPDATES 64
// If defined, writes and reads world data to/from disk (or flash).
// This is a synchronous operation, and can cause performance issues if
// frequent random disk access is slow. Data is still stored in and
@@ -102,9 +98,26 @@
// clients from Keep Alive packets.
#define NETWORK_TIMEOUT_TIME 15000000
// Size of the receive buffer for incoming string data
#define MAX_RECV_BUF_LEN 256
// If defined, sends the server brand to clients. Doesn't do much, but will
// show up in the top-left of the F3/debug menu, in the Minecraft client.
// You can change the brand string in the "brand" variable in src/globals.c
#define SEND_BRAND
// If defined, rebroadcasts ALL incoming movement updates, disconnecting
// movement from the server's tickrate. This makes movement much smoother
// on very low tickrates, at the cost of potential network instability when
// hosting more than just a couple of players. When disabling this on low
// tickrates, consider disabling SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT too.
#define BROADCAST_ALL_MOVEMENT
// If defined, scales the frequency at which player movement updates are
// broadcast based on the amount of players, reducing overhead for higher
// player counts. For very many players, makes movement look jittery.
// It is not recommended to use this if BROADCAST_ALL_MOVEMENT is disabled
// on low tickrates, as that might drastically decrease the update rate.
#define SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT
// If defined, calculates fluid flow when blocks are updated near fluids
@@ -128,6 +141,9 @@
// every time a block is broken.
#define ENABLE_PICKUP_ANIMATION
// If defined, players are able to receive damage from nearby cacti.
#define ENABLE_CACTUS_DAMAGE
// If defined, logs unrecognized packet IDs
// #define DEV_LOG_UNKNOWN_PACKETS
@@ -135,7 +151,7 @@
#define DEV_LOG_LENGTH_DISCREPANCY
// If defined, log chunk generation events
#define DEV_LOG_CHUNK_GENERATION
// #define DEV_LOG_CHUNK_GENERATION
// If defined, allows dumping world data by sending 0xBEEF (big-endian),
// and uploading world data by sending 0xFEED, followed by the data buffer.
@@ -150,7 +166,7 @@
#define STATE_PLAY 5
extern ssize_t recv_count;
extern uint8_t recv_buffer[256];
extern uint8_t recv_buffer[MAX_RECV_BUF_LEN];
extern uint32_t world_seed;
extern uint32_t rng_seed;
@@ -161,17 +177,40 @@ extern uint32_t server_ticks;
extern char motd[];
extern uint8_t motd_len;
extern uint16_t client_count;
#ifdef SEND_BRAND
extern char brand[];
extern uint8_t brand_len;
#endif
#pragma pack(push, 1)
extern uint16_t client_count;
typedef struct {
short x;
uint8_t y;
short z;
uint8_t y;
uint8_t block;
} BlockChange;
#define UPDATE_BASIC (1 << 0)
// the sand at this position will be moved down immediately when this is processed
#define UPDATE_FALL_SAND (1 << 1)
#define UPDATE_FLOW_WATER (1 << 2)
// the sand below this block will fall soon
#define UPDATE_CHECK_SAND_FALL (1 << 3)
#define UPDATE_CHECK_WATER (1 << 4)
#define UPDATE_NOW (UPDATE_BASIC | UPDATE_CHECK_SAND_FALL | UPDATE_CHECK_WATER)
typedef struct {
short update_kind;
short x;
short z;
uint8_t y;
uint8_t await_ticks;
} DeferredBlockUpdate;
#pragma pack(push, 1)
typedef struct {
uint8_t uuid[16];
char name[16];
@@ -207,6 +246,7 @@ typedef struct {
// 0x08 - sprinting
// 0x10 - eating, makes flagval_16 act as eating timer
// 0x20 - client loading, uses flagval_16 as fallback timer
// 0x40 - movement update cooldown
uint8_t flags;
} PlayerData;
@@ -218,15 +258,33 @@ typedef struct {
uint8_t y;
short z;
// Lower 5 bits: health
// Upper 3 bits: reserved (?)
// Middle 1 bit: sheep sheared, unused for other mobs
// Upper 2 bits: panic timer
uint8_t data;
} MobData;
#pragma pack(pop)
union EntityDataValue {
uint8_t byte;
int pose;
};
typedef struct {
uint8_t index;
// 0 - Byte
// 21 - Pose
int type;
union EntityDataValue value;
} EntityData;
extern BlockChange block_changes[MAX_BLOCK_CHANGES];
extern int block_changes_count;
extern DeferredBlockUpdate deferred_block_updates[MAX_DEFERRED_BLOCK_UPDATES];
extern int deferred_block_updates_count;
extern uint8_t is_processing_deferred_block_updates;
extern PlayerData player_data[MAX_PLAYERS];
extern int player_data_count;

View File

@@ -14,6 +14,7 @@ int cs_setPlayerPosition (int client_fd, double *x, double *y, double *z, uint8_
int cs_setPlayerRotation (int client_fd, float *yaw, float *pitch, uint8_t *on_ground);
int cs_setPlayerMovementFlags (int client_fd, uint8_t *on_ground);
int cs_setHeldItem (int client_fd);
int cs_swingArm (int client_fd);
int cs_clickContainer (int client_fd);
int cs_closeContainer (int client_fd);
int cs_clientStatus (int client_fd);
@@ -21,11 +22,13 @@ int cs_chat (int client_fd);
int cs_interact (int client_fd);
int cs_playerInput (int client_fd);
int cs_playerCommand (int client_fd);
int cs_playerLoaded (int client_fd);
// Clientbound packets
int sc_statusResponse (int client_fd);
int sc_loginSuccess (int client_fd, uint8_t *uuid, char *name);
int sc_knownPacks (int client_fd);
int sc_sendPluginMessage (int client_fd, const char *channel, const uint8_t *data, size_t data_len);
int sc_finishConfiguration (int client_fd);
int sc_loginPlay (int client_fd);
int sc_synchronizePlayerPosition (int client_fd, double x, double y, double z, float yaw, float pitch);
@@ -45,6 +48,8 @@ int sc_acknowledgeBlockChange (int client_fd, int sequence);
int sc_playerInfoUpdateAddPlayer (int client_fd, PlayerData player);
int sc_spawnEntity (int client_fd, int id, uint8_t *uuid, int type, double x, double y, double z, uint8_t yaw, uint8_t pitch);
int sc_spawnEntityPlayer (int client_fd, PlayerData player);
int sc_setEntityMetadata (int client_fd, int id, EntityData *metadata, size_t length);
int sc_entityAnimation (int client_fd, int id, uint8_t animation);
int sc_teleportEntity (int client_fd, int id, double x, double y, double z, float yaw, float pitch);
int sc_setHeadRotation (int client_fd, int id, uint8_t yaw);
int sc_updateEntityRotation (int client_fd, int id, uint8_t yaw, uint8_t pitch);

View File

@@ -14,11 +14,16 @@ int getClientIndex (int client_fd);
void resetPlayerData (PlayerData *player);
int reservePlayerData (int client_fd, uint8_t *uuid, char* name);
int getPlayerData (int client_fd, PlayerData **output);
PlayerData *getPlayerByName (int start_offset, int end_offset, uint8_t *buffer);
void handlePlayerDisconnect (int client_fd);
void handlePlayerJoin (PlayerData* player);
void disconnectClient (int *client_fd, int cause);
int givePlayerItem (PlayerData *player, uint16_t item, uint8_t count);
void spawnPlayer (PlayerData *player);
void broadcastPlayerMetadata (PlayerData *player);
void broadcastMobMetadata (int client_fd, int entity_id);
uint8_t serverSlotToClientSlot (int window_id, uint8_t slot);
uint8_t clientSlotToServerSlot (int window_id, uint8_t slot);
@@ -27,8 +32,11 @@ uint8_t makeBlockChange (short x, uint8_t y, short z, uint8_t block);
uint8_t isInstantlyMined (PlayerData *player, uint8_t block);
uint8_t isColumnBlock (uint8_t block);
uint8_t isFallingBlock (uint8_t block);
uint8_t isPassableBlock (uint8_t block);
uint8_t isPassableSpawnBlock (uint8_t block);
uint8_t isReplaceableBlock (uint8_t block);
uint8_t getFluid (uint8_t block);
uint32_t isCompostItem (uint16_t item);
uint8_t getItemStackSize (uint16_t item);
@@ -37,12 +45,21 @@ void bumpToolDurability (PlayerData *player);
void handlePlayerAction (PlayerData *player, int action, short x, short y, short z);
void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t face);
void checkFluidUpdate (short x, uint8_t y, short z, uint8_t block);
void processBlockUpdate (short x, uint8_t y, short z, uint8_t block, short update_kind);
void updateXZNeighbors (short x, uint8_t y, short z, short update_kind);
void updateXYZNeighbors (short x, uint8_t y, short z, short update_kind);
void deferBlockUpdate (short x, uint8_t y, short z, uint8_t await_ticks, short update_kind);
void spawnMob (uint8_t type, short x, uint8_t y, short z, uint8_t health);
void interactEntity (int entity_id, int interactor_id);
void hurtEntity (int entity_id, int attacker_id, uint8_t damage_type, uint8_t damage);
void handleServerTick (int64_t time_since_last_tick);
void broadcastChestUpdate (int origin_fd, uint8_t *storage_ptr, uint16_t item, uint8_t count, uint8_t slot);
ssize_t writeEntityData (int client_fd, EntityData *data);
int sizeEntityData (EntityData *data);
int sizeEntityMetadata (EntityData *metadata, size_t length);
#endif

View File

@@ -8,11 +8,13 @@
void writeBlockChangesToDisk (int from, int to);
void writeChestChangesToDisk (uint8_t *storage_ptr, uint8_t slot);
void writePlayerDataToDisk ();
void writeDataToDiskOnInterval ();
#else
// Define no-op placeholders for when disk syncing isn't enabled
#define writeBlockChangesToDisk(a, b)
#define writeChestChangesToDisk(a, b)
#define writePlayerDataToDisk()
#define writeDataToDiskOnInterval()
#define initSerializer() 0
#endif

View File

@@ -5,10 +5,10 @@
#include "globals.h"
inline int mod_abs (int a, int b) {
static inline int mod_abs (int a, int b) {
return ((a % b) + b) % b;
}
inline int div_floor (int a, int b) {
static inline int div_floor (int a, int b) {
return a % b < 0 ? (a - b) / b : a / b;
}
@@ -32,7 +32,9 @@ int64_t readInt64 (int client_fd);
float readFloat (int client_fd);
double readDouble (int client_fd);
ssize_t readLengthPrefixedData (int client_fd);
void readString (int client_fd);
void readStringN (int client_fd, uint32_t max_length);
uint32_t fast_rand ();
uint64_t splitmix64 (uint64_t state);

View File

@@ -1,6 +1,11 @@
#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#endif
#include <unistd.h>
#include "globals.h"
@@ -24,10 +29,10 @@
#endif
ssize_t recv_count;
uint8_t recv_buffer[256] = {0};
uint8_t recv_buffer[MAX_RECV_BUF_LEN] = {0};
uint32_t world_seed = INITIAL_WORLD_SEED;
uint32_t rng_seed = INITIAL_RNG_SEED;
uint32_t world_seed;
uint32_t rng_seed;
uint16_t world_time = 0;
uint32_t server_ticks = 0;
@@ -35,11 +40,20 @@ uint32_t server_ticks = 0;
char motd[] = { "A bareiron server" };
uint8_t motd_len = sizeof(motd) - 1;
#ifdef SEND_BRAND
char brand[] = { "bareiron" };
uint8_t brand_len = sizeof(brand) - 1;
#endif
uint16_t client_count;
BlockChange block_changes[MAX_BLOCK_CHANGES];
int block_changes_count = 0;
DeferredBlockUpdate deferred_block_updates[MAX_DEFERRED_BLOCK_UPDATES];
int deferred_block_updates_count = 0;
uint8_t is_processing_deferred_block_updates = 0;
PlayerData player_data[MAX_PLAYERS];
int player_data_count = 0;

View File

@@ -19,9 +19,16 @@
#include "lwip/netdb.h"
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/random.h>
#endif
#include <unistd.h>
#include <time.h>
#endif
@@ -85,6 +92,10 @@ void handlePacket (int client_fd, int length, int packet_id, int state) {
if (cs_clientInformation(client_fd)) break;
if (sc_knownPacks(client_fd)) break;
if (sc_registries(client_fd)) break;
#ifdef SEND_BRAND
if (sc_sendPluginMessage(client_fd, "minecraft:brand", (uint8_t *)brand, brand_len)) break;
#endif
}
break;
@@ -122,34 +133,32 @@ void handlePacket (int client_fd, int length, int packet_id, int state) {
// Send full client spawn sequence
spawnPlayer(player);
// Prepare join message for broadcast
uint8_t player_name_len = strlen(player->name);
strcpy((char *)recv_buffer, player->name);
strcpy((char *)recv_buffer + player_name_len, " joined the game");
// Register all existing players and spawn their entities, and broadcast
// information about the joining player to all existing players.
// Register all existing players and spawn their entities
for (int i = 0; i < MAX_PLAYERS; i ++) {
if (player_data[i].client_fd == -1) continue;
if (player_data[i].flags & 0x20 && player_data[i].client_fd != client_fd) continue;
// Note that this will also filter out the joining player
if (player_data[i].flags & 0x20) continue;
sc_playerInfoUpdateAddPlayer(client_fd, player_data[i]);
sc_systemChat(player_data[i].client_fd, (char *)recv_buffer, 16 + player_name_len);
if (player_data[i].client_fd == client_fd) continue;
sc_playerInfoUpdateAddPlayer(player_data[i].client_fd, *player);
sc_spawnEntityPlayer(client_fd, player_data[i]);
sc_spawnEntityPlayer(player_data[i].client_fd, *player);
}
// Send information about all other entities (mobs)
// For more info on the arguments, see the spawnMob function
// Send information about all other entities (mobs):
// Use a random number for the first half of the UUID
uint8_t uuid[16];
uint32_t r = fast_rand();
memcpy(uuid, &r, 4);
// Send allocated living mobs, use ID for second half of UUID
for (int i = 0; i < MAX_MOBS; i ++) {
if (mob_data[i].type == 0) continue;
if ((mob_data[i].data & 31) == 0) continue;
memcpy(uuid + 4, &i, 4);
// For more info on the arguments here, see the spawnMob function
sc_spawnEntity(
client_fd, -2 - i, recv_buffer,
client_fd, -2 - i, uuid,
mob_data[i].type, mob_data[i].x, mob_data[i].y, mob_data[i].z,
0, 0
);
broadcastMobMetadata(client_fd, -2 - i);
}
}
@@ -212,16 +221,18 @@ void handlePacket (int client_fd, int length, int packet_id, int state) {
PlayerData *player;
if (getPlayerData(client_fd, &player)) break;
uint8_t block_feet = getBlockAt(player->x, player->y, player->z);
uint8_t swimming = block_feet >= B_water && block_feet < B_water + 8;
// Handle fall damage
if (on_ground) {
int16_t damage = player->grounded_y - player->y - 3;
if (damage > 0) {
uint8_t block_feet = getBlockAt(player->x, player->y, player->z);
if (block_feet < B_water || block_feet > B_water + 7) {
hurtEntity(client_fd, -1, D_fall, damage);
}
if (damage > 0 && (GAMEMODE == 0 || GAMEMODE == 2) && !swimming) {
hurtEntity(client_fd, -1, D_fall, damage);
}
player->grounded_y = player->y;
} else if (swimming) {
player->grounded_y = player->y;
}
// Don't continue if all we got were flags
@@ -233,17 +244,31 @@ void handlePacket (int client_fd, int length, int packet_id, int state) {
player->pitch = pitch / 90.0f * 127.0f;
}
// Broadcast player position to other players
// Whether to broadcast player position to other players
uint8_t should_broadcast = true;
#ifndef BROADCAST_ALL_MOVEMENT
// If applicable, tie movement updates to the tickrate by using
// a flag that gets reset on every tick. It might sound better
// to just make the tick handler broadcast position updates, but
// then we lose precision. While position is stored using integers,
// here the client gives us doubles and floats directly.
should_broadcast = !(player->flags & 0x40);
if (should_broadcast) player->flags |= 0x40;
#endif
#ifdef SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT
// If applicable, broadcast only every client_count-th movement update
uint8_t should_broadcast = false;
if (++player->packets_since_update >= client_count) {
should_broadcast = true;
if (++player->packets_since_update < client_count) {
should_broadcast = false;
} else {
// Note that this does not explicitly set should_broadcast to true
// This allows the above BROADCAST_ALL_MOVEMENT check to compound
// Whether that's ever favorable is up for debate
player->packets_since_update = 0;
}
#else
#define should_broadcast (client_count > 0)
#endif
if (should_broadcast) {
// If the packet had no rotation data, calculate it from player data
if (packet_id == 0x1D) {
@@ -341,10 +366,10 @@ void handlePacket (int client_fd, int length, int packet_id, int state) {
uint8_t b_mid = getBlockAt(mob_x, mob_y, mob_z);
uint8_t b_top = getBlockAt(mob_x, mob_y + 1, mob_z);
while (mob_y < 255) {
if ( // Solid block below, non-solid at feet and above
if ( // Solid block below, non-solid(spawnable) at feet and above
!isPassableBlock(b_low) &&
isPassableBlock(b_mid) &&
isPassableBlock(b_top)
isPassableSpawnBlock(b_mid) &&
isPassableSpawnBlock(b_top)
) break;
b_low = b_mid;
b_mid = b_top;
@@ -413,17 +438,17 @@ void handlePacket (int client_fd, int length, int packet_id, int state) {
if (state == STATE_PLAY) cs_playerInput(client_fd);
break;
case 0x2B: // Player Loaded
PlayerData *player;
if (getPlayerData(client_fd, &player)) break;
// Clear "client loading" flag and fallback timer
player->flags &= ~0x20;
player->flagval_16 = 0;
case 0x2B:
if (state == STATE_PLAY) cs_playerLoaded(client_fd);
break;
case 0x34:
if (state == STATE_PLAY) cs_setHeldItem(client_fd);
break;
case 0x3C:
if (state == STATE_PLAY) cs_swingArm(client_fd);
break;
case 0x28:
if (state == STATE_PLAY) cs_playerAction(client_fd);
@@ -474,16 +499,24 @@ void handlePacket (int client_fd, int length, int packet_id, int state) {
}
int main () {
#ifdef _WIN32 //initialize windows socket
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
fprintf(stderr, "WSAStartup failed\n");
exit(EXIT_FAILURE);
}
#endif
// Hash the seeds to ensure they're random enough
world_seed = splitmix64(world_seed);
printf("World seed (hashed): ");
if (0) { // TODO: manual seed
world_seed = splitmix64(world_seed);
} else {
getrandom(&world_seed, 4, GRND_RANDOM);
}
printf("World seed: ");
for (int i = 3; i >= 0; i --) printf("%X", (unsigned int)((world_seed >> (8 * i)) & 255));
printf("\n");
rng_seed = splitmix64(rng_seed);
printf("\nRNG seed (hashed): ");
for (int i = 3; i >= 0; i --) printf("%X", (unsigned int)((rng_seed >> (8 * i)) & 255));
printf("\n\n");
getrandom(&rng_seed, 4, GRND_RANDOM);
// Initialize block changes entries as unallocated
for (int i = 0; i < MAX_BLOCK_CHANGES; i ++) {
@@ -511,8 +544,12 @@ int main () {
perror("socket failed");
exit(EXIT_FAILURE);
}
#ifdef _WIN32
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR,
(const char*)&opt, sizeof(opt)) < 0) {
#else
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
#endif
perror("socket options failed");
exit(EXIT_FAILURE);
}
@@ -538,125 +575,190 @@ int main () {
// Make the socket non-blocking
// This is necessary to not starve the idle task during slow connections
#ifdef _WIN32
u_long mode = 1; // 1 = non-blocking
if (ioctlsocket(server_fd, FIONBIO, &mode) != 0) {
fprintf(stderr, "Failed to set non-blocking mode\n");
exit(EXIT_FAILURE);
}
#else
int flags = fcntl(server_fd, F_GETFL, 0);
fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);
#endif
// Track time of last server tick (in microseconds)
int64_t last_tick_time = get_program_time();
/**
* Cycles through all connected clients, handling one packet at a time
* from each player. With every iteration, attempts to accept a new
* client connection.
*/
// Main server loop: use select() to sleep until I/O or next tick
while (true) {
// Check if it's time to yield to the idle task
task_yield();
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(server_fd, &read_fds);
// Attempt to accept a new connection
for (int i = 0; i < MAX_PLAYERS; i ++) {
if (clients[i] != -1) continue;
clients[i] = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
// If the accept was successful, make the client non-blocking too
if (clients[i] != -1) {
printf("New client, fd: %d\n", clients[i]);
int flags = fcntl(clients[i], F_GETFL, 0);
fcntl(clients[i], F_SETFL, flags | O_NONBLOCK);
client_count ++;
}
break;
if (deferred_block_updates_count == MAX_DEFERRED_BLOCK_UPDATES) {
printf("WARNING: Deferred block update queue maxed out\n");
}
// Look for valid connected clients
client_index ++;
if (client_index == MAX_PLAYERS) client_index = 0;
if (clients[client_index] == -1) continue;
// Attempt to accept a new connection
int max_fd = server_fd;
for (int i = 0; i < MAX_PLAYERS; i ++) {
if (clients[i] != -1) {
FD_SET(clients[i], &read_fds);
if (clients[i] > max_fd) max_fd = clients[i];
}
}
// Handle periodic events (server ticks)
int64_t time_since_last_tick = get_program_time() - last_tick_time;
if (time_since_last_tick > TIME_BETWEEN_TICKS) {
handleServerTick(time_since_last_tick);
// Compute timeout until next server tick
struct timeval timeout;
int64_t now = get_program_time();
int64_t elapsed = now - last_tick_time;
int64_t to_next = TIME_BETWEEN_TICKS - elapsed;
if (to_next < 0) to_next = 0;
timeout.tv_sec = (time_t)(to_next / 1000000);
timeout.tv_usec = (suseconds_t)(to_next % 1000000);
int activity;
#ifdef _WIN32
activity = select(0, &read_fds, NULL, NULL, &timeout);
if (activity == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err == WSAEINTR) continue;
if (err == WSAETIMEDOUT) activity = 0;
}
#else
activity = select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
if (activity < 0) {
if (errno == EINTR) continue; // interrupted, retry
}
#endif
now = get_program_time();
elapsed = now - last_tick_time;
if (elapsed >= TIME_BETWEEN_TICKS) {
handleServerTick(elapsed);
last_tick_time = get_program_time();
}
// Handle this individual client
int client_fd = clients[client_index];
// Check if at least 2 bytes are available for reading
recv_count = recv(client_fd, &recv_buffer, 2, MSG_PEEK);
if (recv_count < 2) {
if (recv_count == 0 || (recv_count < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
disconnectClient(&clients[client_index], 1);
}
if (activity <= 0) {
// timeout or error already handled; continue loop
continue;
}
// Handle 0xBEEF and 0xFEED packets for dumping/uploading world data
#ifdef DEV_ENABLE_BEEF_DUMPS
// Received BEEF packet, dump world data and disconnect
if (recv_buffer[0] == 0xBE && recv_buffer[1] == 0xEF && getClientState(client_fd) == STATE_NONE) {
// Send block changes and player data back to back
// The client is expected to know (or calculate) the size of these buffers
send_all(client_fd, block_changes, sizeof(block_changes));
send_all(client_fd, player_data, sizeof(player_data));
// Flush the socket and receive everything left on the wire
shutdown(client_fd, SHUT_WR);
recv_all(client_fd, recv_buffer, sizeof(recv_buffer), false);
// Kick the client
disconnectClient(&clients[client_index], 6);
continue;
}
// Received FEED packet, load world data from socket and disconnect
if (recv_buffer[0] == 0xFE && recv_buffer[1] == 0xED && getClientState(client_fd) == STATE_NONE) {
// Consume 0xFEED bytes (previous read was just a peek)
recv_all(client_fd, recv_buffer, 2, false);
// Write full buffers straight into memory
recv_all(client_fd, block_changes, sizeof(block_changes), false);
recv_all(client_fd, player_data, sizeof(player_data), false);
// Recover block_changes_count
for (int i = 0; i < MAX_BLOCK_CHANGES; i ++) {
if (block_changes[i].block == 0xFF) continue;
if (block_changes[i].block == B_chest) i += 14;
if (i >= block_changes_count) block_changes_count = i + 1;
if (FD_ISSET(server_fd, &read_fds)) {
while (true) {
int slot = -1;
for (int i = 0; i < MAX_PLAYERS; i ++) {
if (clients[i] == -1) { slot = i; break; }
}
if (slot == -1) {
int tmp = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (tmp == -1) break;
#ifdef _WIN32
closesocket(tmp);
#else
close(tmp);
#endif
break;
}
clients[slot] = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (clients[slot] == -1) break; // no more pending
printf("New client, fd: %d\n", clients[slot]);
#ifdef _WIN32
u_long mode = 1;
ioctlsocket(clients[slot], FIONBIO, &mode);
#else
int flags = fcntl(clients[slot], F_GETFL, 0);
fcntl(clients[slot], F_SETFL, flags | O_NONBLOCK);
#endif
client_count ++;
}
// Update data on disk
writeBlockChangesToDisk(0, block_changes_count);
writePlayerDataToDisk();
// Kick the client
disconnectClient(&clients[client_index], 7);
continue;
}
for (int i = 0; i < MAX_PLAYERS; i ++) {
int client_fd = clients[i];
if (client_fd == -1) continue;
if (!FD_ISSET(client_fd, &read_fds)) continue;
#ifdef _WIN32
recv_count = recv(client_fd, recv_buffer, 2, MSG_PEEK);
if (recv_count == 0) {
disconnectClient(&clients[i], 1);
continue;
}
if (recv_count == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK) {
continue; // no data yet, keep client alive
} else {
disconnectClient(&clients[i], 1);
continue;
}
}
#else
recv_count = recv(client_fd, &recv_buffer, 2, MSG_PEEK);
if (recv_count < 2) {
if (recv_count == 0 || (recv_count < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
disconnectClient(&clients[i], 1);
}
continue;
}
#endif
// Read packet length
int length = readVarInt(client_fd);
if (length == VARNUM_ERROR) {
disconnectClient(&clients[client_index], 2);
continue;
}
// Read packet ID
int packet_id = readVarInt(client_fd);
if (packet_id == VARNUM_ERROR) {
disconnectClient(&clients[client_index], 3);
continue;
}
// Get client connection state
int state = getClientState(client_fd);
// Disconnect on legacy server list ping
if (state == STATE_NONE && length == 254 && packet_id == 122) {
disconnectClient(&clients[client_index], 5);
continue;
}
// Handle packet data
handlePacket(client_fd, length - sizeVarInt(packet_id), packet_id, state);
if (recv_count == 0 || (recv_count == -1 && errno != EAGAIN && errno != EWOULDBLOCK)) {
disconnectClient(&clients[client_index], 4);
continue;
}
#ifdef DEV_ENABLE_BEEF_DUMPS
if (recv_buffer[0] == 0xBE && recv_buffer[1] == 0xEF && getClientState(client_fd) == STATE_NONE) {
send_all(client_fd, block_changes, sizeof(block_changes));
send_all(client_fd, player_data, sizeof(player_data));
shutdown(client_fd, SHUT_WR);
recv_all(client_fd, recv_buffer, sizeof(recv_buffer), false);
disconnectClient(&clients[i], 6);
continue;
}
if (recv_buffer[0] == 0xFE && recv_buffer[1] == 0xED && getClientState(client_fd) == STATE_NONE) {
recv_all(client_fd, recv_buffer, 2, false);
recv_all(client_fd, block_changes, sizeof(block_changes), false);
recv_all(client_fd, player_data, sizeof(player_data), false);
for (int j = 0; j < MAX_BLOCK_CHANGES; j ++) {
if (block_changes[j].block == 0xFF) continue;
if (block_changes[j].block == B_chest) j += 14;
if (j >= block_changes_count) block_changes_count = j + 1;
}
writeBlockChangesToDisk(0, block_changes_count);
writePlayerDataToDisk();
disconnectClient(&clients[i], 7);
continue;
}
#endif
int length = readVarInt(client_fd);
if (length == VARNUM_ERROR) {
disconnectClient(&clients[i], 2);
continue;
}
int packet_id = readVarInt(client_fd);
if (packet_id == VARNUM_ERROR) {
disconnectClient(&clients[i], 3);
continue;
}
int state = getClientState(client_fd);
if (state == STATE_NONE && length == 254 && packet_id == 122) {
disconnectClient(&clients[i], 5);
continue;
}
handlePacket(client_fd, length - sizeVarInt(packet_id), packet_id, state);
if (recv_count == 0 || (recv_count == -1 && errno != EAGAIN && errno != EWOULDBLOCK)) {
disconnectClient(&clients[i], 4);
continue;
}
}
}
close(server_fd);
#ifdef _WIN32 //cleanup windows socket
WSACleanup();
#endif
printf("Server closed.\n");
}

View File

@@ -6,7 +6,12 @@
#include "lwip/netdb.h"
#include "esp_task_wdt.h"
#else
#include <arpa/inet.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#endif
#include <unistd.h>
#endif
@@ -63,7 +68,8 @@ int cs_loginStart (int client_fd, uint8_t *uuid, char *name) {
readString(client_fd);
if (recv_count == -1) return 1;
memcpy(name, recv_buffer, strlen((char *)recv_buffer) + 1);
strncpy(name, (char *)recv_buffer, 16 - 1);
name[16 - 1] = '\0';
printf(" Player name: %s\n", name);
recv_count = recv_all(client_fd, recv_buffer, 16, false);
if (recv_count == -1) return 1;
@@ -156,6 +162,23 @@ int cs_pluginMessage (int client_fd) {
return 0;
}
// S->C Clientbound Plugin Message
int sc_sendPluginMessage (int client_fd, const char *channel, const uint8_t *data, size_t data_len) {
printf("Sending Plugin Message\n\n");
int channel_len = (int)strlen(channel);
writeVarInt(client_fd, 1 + sizeVarInt(channel_len) + channel_len + sizeVarInt(data_len) + data_len);
writeByte(client_fd, 0x01);
writeVarInt(client_fd, channel_len);
send_all(client_fd, channel, channel_len);
writeVarInt(client_fd, data_len);
send_all(client_fd, data, data_len);
return 0;
}
// S->C Finish Configuration
int sc_finishConfiguration (int client_fd) {
writeVarInt(client_fd, 1);
@@ -635,10 +658,8 @@ int cs_clickContainer (int client_fd) {
count = (uint8_t)readVarInt(client_fd);
// ignore components
tmp = readVarInt(client_fd);
recv_all(client_fd, recv_buffer, tmp, false);
tmp = readVarInt(client_fd);
recv_all(client_fd, recv_buffer, tmp, false);
readLengthPrefixedData(client_fd);
readLengthPrefixedData(client_fd);
if (count > 0 && apply_changes) {
*p_item = item;
@@ -668,10 +689,8 @@ int cs_clickContainer (int client_fd) {
player->flagval_16 = readVarInt(client_fd);
player->flagval_8 = readVarInt(client_fd);
// ignore components
tmp = readVarInt(client_fd);
recv_all(client_fd, recv_buffer, tmp, false);
tmp = readVarInt(client_fd);
recv_all(client_fd, recv_buffer, tmp, false);
readLengthPrefixedData(client_fd);
readLengthPrefixedData(client_fd);
} else {
player->flagval_16 = 0;
player->flagval_8 = 0;
@@ -741,6 +760,47 @@ int cs_setPlayerMovementFlags (int client_fd, uint8_t *on_ground) {
*on_ground = readByte(client_fd) & 0x01;
PlayerData *player;
if (!getPlayerData(client_fd, &player))
broadcastPlayerMetadata(player);
return 0;
}
// C->S Swing Arm (serverbound)
int cs_swingArm (int client_fd) {
PlayerData *player;
if (getPlayerData(client_fd, &player)) return 1;
uint8_t hand = readVarInt(client_fd);
uint8_t animation = 255;
switch (hand) {
case 0: {
animation = 0;
break;
}
case 1: {
animation = 2;
break;
}
}
if (animation == 255)
return 1;
// Forward animation to all connected players
for (int i = 0; i < MAX_PLAYERS; i ++) {
PlayerData* other_player = &player_data[i];
if (other_player->client_fd == -1) continue;
if (other_player->client_fd == player->client_fd) continue;
if (other_player->flags & 0x20) continue;
sc_entityAnimation(other_player->client_fd, player->client_fd, animation);
}
return 0;
}
@@ -850,6 +910,26 @@ int sc_spawnEntity (
return 0;
}
// S->C Set Entity Metadata
int sc_setEntityMetadata (int client_fd, int id, EntityData *metadata, size_t length) {
int entity_metadata_size = sizeEntityMetadata(metadata, length);
if (entity_metadata_size == -1) return 1;
writeVarInt(client_fd, 2 + sizeVarInt(id) + entity_metadata_size);
writeByte(client_fd, 0x5C);
writeVarInt(client_fd, id); // Entity ID
for (size_t i = 0; i < length; i ++) {
EntityData *data = &metadata[i];
writeEntityData(client_fd, data);
}
writeByte(client_fd, 0xFF); // End
return 0;
}
// S->C Spawn Entity (from PlayerData)
int sc_spawnEntityPlayer (int client_fd, PlayerData player) {
return sc_spawnEntity(
@@ -862,6 +942,17 @@ int sc_spawnEntityPlayer (int client_fd, PlayerData player) {
);
}
// S->C Entity Animation
int sc_entityAnimation (int client_fd, int id, uint8_t animation) {
writeVarInt(client_fd, 2 + sizeVarInt(id));
writeByte(client_fd, 0x02);
writeVarInt(client_fd, id); // Entity ID
writeByte(client_fd, animation); // Animation
return 0;
}
// S->C Teleport Entity
int sc_teleportEntity (
int client_fd, int id,
@@ -1022,7 +1113,8 @@ int sc_systemChat (int client_fd, char* message, uint16_t len) {
// C->S Chat Message
int cs_chat (int client_fd) {
readString(client_fd);
// To be safe, cap messages to 32 bytes before the buffer length
readStringN(client_fd, 224);
PlayerData *player;
if (getPlayerData(client_fd, &player)) return 1;
@@ -1030,35 +1122,95 @@ int cs_chat (int client_fd) {
size_t message_len = strlen((char *)recv_buffer);
uint8_t name_len = strlen(player->name);
// To be safe, cap messages to 32 bytes before the buffer length
if (message_len > 224) {
recv_buffer[224] = '\0';
message_len = 224;
if (recv_buffer[0] != '!') { // Standard chat message
// Shift message contents forward to make space for player name tag
memmove(recv_buffer + name_len + 3, recv_buffer, message_len + 1);
// Copy player name to index 1
memcpy(recv_buffer + 1, player->name, name_len);
// Surround player name with brackets and a space
recv_buffer[0] = '<';
recv_buffer[name_len + 1] = '>';
recv_buffer[name_len + 2] = ' ';
// Forward message to all connected players
for (int i = 0; i < MAX_PLAYERS; i ++) {
if (player_data[i].client_fd == -1) continue;
if (player_data[i].flags & 0x20) continue;
sc_systemChat(player_data[i].client_fd, (char *)recv_buffer, message_len + name_len + 3);
}
goto cleanup;
}
// Shift message contents forward to make space for player name tag
memmove(recv_buffer + name_len + 3, recv_buffer, message_len + 1);
// Copy player name to index 1
memcpy(recv_buffer + 1, player->name, name_len);
// Surround player name with brackets and a space
recv_buffer[0] = '<';
recv_buffer[name_len + 1] = '>';
recv_buffer[name_len + 2] = ' ';
// Handle chat commands
// Forward message to all connected players
for (int i = 0; i < MAX_PLAYERS; i ++) {
if (player_data[i].client_fd == -1) continue;
if (player_data[i].flags & 0x20) continue;
sc_systemChat(player_data[i].client_fd, (char *)recv_buffer, message_len + name_len + 3);
if (!strncmp((char *)recv_buffer, "!msg", 4)) {
int target_offset = 5;
int target_end_offset = 0;
int text_offset = 0;
// Skip spaces after "!msg"
while (recv_buffer[target_offset] == ' ') target_offset++;
target_end_offset = target_offset;
// Extract target name
while (recv_buffer[target_end_offset] != ' ' && recv_buffer[target_end_offset] != '\0' && target_end_offset < 21) target_end_offset++;
text_offset = target_end_offset;
// Skip spaces before message
while (recv_buffer[text_offset] == ' ') text_offset++;
// Send usage guide if arguments are missing
if (target_offset == target_end_offset || target_end_offset == text_offset) {
sc_systemChat(client_fd, "§7Usage: !msg <player> <message>", 33);
goto cleanup;
}
// Query the target player
PlayerData *target = getPlayerByName(target_offset, target_end_offset, recv_buffer);
if (target == NULL) {
sc_systemChat(client_fd, "Player not found", 16);
goto cleanup;
}
// Format output as a vanilla whisper
int name_len = strlen(player->name);
int text_len = message_len - text_offset;
memmove(recv_buffer + name_len + 24, recv_buffer + text_offset, text_len);
snprintf((char *)recv_buffer, sizeof(recv_buffer), "§7§o%s whispers to you:", player->name);
recv_buffer[name_len + 23] = ' ';
// Send message to target player
sc_systemChat(target->client_fd, (char *)recv_buffer, (uint16_t)(name_len + 24 + text_len));
// Format output for sending player
int target_len = target_end_offset - target_offset;
memmove(recv_buffer + target_len + 23, recv_buffer + name_len + 24, text_len);
snprintf((char *)recv_buffer, sizeof(recv_buffer), "§7§oYou whisper to %s:", target->name);
recv_buffer[target_len + 22] = ' ';
// Report back to sending player
sc_systemChat(client_fd, (char *)recv_buffer, (uint16_t)(target_len + 23 + text_len));
goto cleanup;
}
if (!strncmp((char *)recv_buffer, "!help", 5)) {
// Send command guide
const char help_msg[] = "§7Commands:\n"
" !msg <player> <message> - Send a private message\n"
" !help - Show this help message";
sc_systemChat(client_fd, (char *)help_msg, (uint16_t)sizeof(help_msg) - 1);
goto cleanup;
}
// Handle fall-through case
sc_systemChat(client_fd, "§7Unknown command", 18);
cleanup:
readUint64(client_fd); // Ignore timestamp
readUint64(client_fd); // Ignore salt
// Ignore signature (if any)
uint8_t has_signature = readByte(client_fd);
if (has_signature) recv_all(client_fd, recv_buffer, 256, false);
readVarInt(client_fd); // Ignore message count
// Ignore acknowledgement bitmask and checksum
recv_all(client_fd, recv_buffer, 4, false);
@@ -1084,7 +1236,9 @@ int cs_interact (int client_fd) {
// Ignore sneaking flag
recv_all(client_fd, recv_buffer, 1, false);
if (type == 1) {
if (type == 0) { // Interact
interactEntity(entity_id, client_fd);
} else if (type == 1) { // Attack
hurtEntity(entity_id, client_fd, D_generic, 1);
}
@@ -1127,6 +1281,8 @@ int cs_playerInput (int client_fd) {
if (flags & 0x20) player->flags |= 0x04;
else player->flags &= ~0x04;
broadcastPlayerMetadata(player);
return 0;
}
@@ -1144,6 +1300,8 @@ int cs_playerCommand (int client_fd) {
if (action == 1) player->flags |= 0x08;
else if (action == 2) player->flags &= ~0x08;
broadcastPlayerMetadata(player);
return 0;
}
@@ -1160,6 +1318,18 @@ int sc_pickupItem (int client_fd, int collected, int collector, uint8_t count) {
return 0;
}
// C->S Player Loaded
int cs_playerLoaded (int client_fd) {
PlayerData *player;
if (getPlayerData(client_fd, &player)) return 1;
// Redirect handling to player join procedure
handlePlayerJoin(player);
return 0;
}
// S->C Registry Data (multiple packets) and Update Tags (configuration, multiple packets)
int sc_registries (int client_fd) {

View File

@@ -1,7 +1,11 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#ifdef _WIN32
#include <winsock2.h>
#endif
#include "globals.h"
#include "tools.h"
@@ -47,6 +51,7 @@ int getClientIndex (int client_fd) {
return -1;
}
// Restores player data to initial state (fresh spawn)
void resetPlayerData (PlayerData *player) {
player->health = 20;
player->hunger = 20;
@@ -66,16 +71,26 @@ void resetPlayerData (PlayerData *player) {
}
}
// Assigns the given data to a player_data entry
int reservePlayerData (int client_fd, uint8_t *uuid, char *name) {
for (int i = 0; i < MAX_PLAYERS; i ++) {
// Found existing player entry (UUID match)
if (memcmp(player_data[i].uuid, uuid, 16) == 0) {
// Set network file descriptor and username
player_data[i].client_fd = client_fd;
memcpy(player_data[i].name, name, 16);
// Flag player as loading
player_data[i].flags |= 0x20;
player_data[i].flagval_16 = 0;
memcpy(player_data[i].name, name, 16);
// Reset their recently visited chunk list
for (int j = 0; j < VISITED_HISTORY; j ++) {
player_data[i].visited_x[j] = 32767;
player_data[i].visited_z[j] = 32767;
}
return 0;
}
// Search for unallocated player slots
uint8_t empty = true;
for (uint8_t j = 0; j < 16; j ++) {
if (player_data[i].uuid[j] != 0) {
@@ -83,6 +98,7 @@ int reservePlayerData (int client_fd, uint8_t *uuid, char *name) {
break;
}
}
// Found free space for a player, initialize default parameters
if (empty) {
if (player_data_count >= MAX_PLAYERS) return 1;
player_data[i].client_fd = client_fd;
@@ -110,6 +126,22 @@ int getPlayerData (int client_fd, PlayerData **output) {
return 1;
}
// Returns the player with the given name, or NULL if not found
PlayerData *getPlayerByName (int start_offset, int end_offset, uint8_t *buffer) {
for (int i = 0; i < MAX_PLAYERS; i ++) {
if (player_data[i].client_fd == -1) continue;
int j;
for (j = start_offset; j < end_offset && j < 256 && buffer[j] != ' '; j++) {
if (player_data[i].name[j - start_offset] != buffer[j]) break;
}
if ((j == end_offset || buffer[j] == ' ') && j < 256) {
return &player_data[i];
}
}
return NULL;
}
// Marks a client as disconnected and cleans up player data
void handlePlayerDisconnect (int client_fd) {
// Search for a corresponding player in the player data array
@@ -117,11 +149,6 @@ void handlePlayerDisconnect (int client_fd) {
if (player_data[i].client_fd != client_fd) continue;
// Mark the player as being offline
player_data[i].client_fd = -1;
// Reset their recently visited chunk list
for (int j = 0; j < VISITED_HISTORY; j ++) {
player_data[i].visited_x[j] = 32767;
player_data[i].visited_z[j] = 32767;
}
// Prepare leave message for broadcast
uint8_t player_name_len = strlen(player_data[i].name);
strcpy((char *)recv_buffer, player_data[i].name);
@@ -146,14 +173,42 @@ void handlePlayerDisconnect (int client_fd) {
}
}
// Marks a client as connected and broadcasts their data to other players
void handlePlayerJoin (PlayerData* player) {
// Prepare join message for broadcast
uint8_t player_name_len = strlen(player->name);
strcpy((char *)recv_buffer, player->name);
strcpy((char *)recv_buffer + player_name_len, " joined the game");
// Inform other clients (and the joining client) of the player's name and entity
for (int i = 0; i < MAX_PLAYERS; i ++) {
sc_systemChat(player_data[i].client_fd, (char *)recv_buffer, 16 + player_name_len);
sc_playerInfoUpdateAddPlayer(player_data[i].client_fd, *player);
if (player_data[i].client_fd != player->client_fd) {
sc_spawnEntityPlayer(player_data[i].client_fd, *player);
}
}
// Clear "client loading" flag and fallback timer
player->flags &= ~0x20;
player->flagval_16 = 0;
}
void disconnectClient (int *client_fd, int cause) {
if (*client_fd == -1) return;
client_count --;
setClientState(*client_fd, STATE_NONE);
handlePlayerDisconnect(*client_fd);
#ifdef _WIN32
closesocket(*client_fd);
printf("Disconnected client %d, cause: %d, errno: %d\n", *client_fd, cause, WSAGetLastError());
#else
close(*client_fd);
*client_fd = -1;
printf("Disconnected client %d, cause: %d, errno: %d\n\n", *client_fd, cause, errno);
#endif
*client_fd = -1;
}
uint8_t serverSlotToClientSlot (int window_id, uint8_t slot) {
@@ -330,6 +385,87 @@ void spawnPlayer (PlayerData *player) {
}
// Broadcasts a player's entity metadata (sneak/sprint state) to other players
void broadcastPlayerMetadata (PlayerData *player) {
uint8_t sneaking = (player->flags & 0x04) != 0;
uint8_t sprinting = (player->flags & 0x08) != 0;
uint8_t entity_bit_mask = 0;
if (sneaking) entity_bit_mask |= 0x02;
if (sprinting) entity_bit_mask |= 0x08;
int pose = 0;
if (sneaking) pose = 5;
EntityData metadata[] = {
{
0, // Index (Entity Bit Mask)
0, // Type (Byte)
{ entity_bit_mask }, // Value
},
{
6, // Index (Pose),
21, // Type (Pose),
{ pose }, // Value (Standing)
}
};
for (int i = 0; i < MAX_PLAYERS; i ++) {
PlayerData* other_player = &player_data[i];
int client_fd = other_player->client_fd;
if (client_fd == -1) continue;
if (client_fd == player->client_fd) continue;
if (other_player->flags & 0x20) continue;
sc_setEntityMetadata(client_fd, player->client_fd, metadata, 2);
}
}
// Sends a mob's entity metadata to the given player.
// If client_fd is -1, broadcasts to all player
void broadcastMobMetadata (int client_fd, int entity_id) {
MobData *mob = &mob_data[-entity_id - 2];
EntityData *metadata;
size_t length;
switch (mob->type) {
case 106: // Sheep
if (!((mob->data >> 5) & 1)) // Don't send metadata if sheep isn't sheared
return;
metadata = malloc(sizeof *metadata);
metadata[0] = (EntityData){
17, // Index (Sheep Bit Mask),
0, // Type (Byte),
{ (uint8_t)0x10 }, // Value
};
length = 1;
break;
default: return;
}
if (client_fd == -1) {
for (int i = 0; i < MAX_PLAYERS; i ++) {
PlayerData* player = &player_data[i];
client_fd = player->client_fd;
if (client_fd == -1) continue;
if (player->flags & 0x20) continue;
sc_setEntityMetadata(client_fd, entity_id, metadata, length);
}
} else {
sc_setEntityMetadata(client_fd, entity_id, metadata, length);
}
free(metadata);
}
uint8_t getBlockChange (short x, uint8_t y, short z) {
for (int i = 0; i < block_changes_count; i ++) {
if (block_changes[i].block == 0xFF) continue;
@@ -411,7 +547,21 @@ uint8_t makeBlockChange (short x, uint8_t y, short z, uint8_t block) {
}
#endif
if (is_base_block) block_changes[i].block = 0xFF;
else block_changes[i].block = block;
else {
#ifdef ALLOW_CHESTS
// When placing chests, just unallocate the target block and fall
// through to the chest-specific routine below.
if (block == B_chest) {
block_changes[i].block = 0xFF;
if (first_gap > i) first_gap = i;
#ifndef DISK_SYNC_BLOCKS_ON_INTERVAL
writeBlockChangesToDisk(i, i);
#endif
break;
}
#endif
block_changes[i].block = block;
}
#ifndef DISK_SYNC_BLOCKS_ON_INTERVAL
writeBlockChangesToDisk(i, i);
#endif
@@ -430,6 +580,8 @@ uint8_t makeBlockChange (short x, uint8_t y, short z, uint8_t block) {
// which naturally appends the chest to the end if a gap isn't found.
int last_real_entry = first_gap - 1;
for (int i = first_gap; i <= block_changes_count + 15; i ++) {
if (i >= MAX_BLOCK_CHANGES) break; // No more space, trigger failBlockChange
if (block_changes[i].block != 0xFF) {
last_real_entry = i;
continue;
@@ -610,6 +762,13 @@ uint8_t isInstantlyMined (PlayerData *player, uint8_t block) {
}
// Checks whether the given block can fall down (like sand, anvils, ...)
uint8_t isFallingBlock (uint8_t block) {
return (
block == B_sand
);
}
// Checks whether the given block has to have something beneath it
uint8_t isColumnBlock (uint8_t block) {
return (
@@ -637,6 +796,14 @@ uint8_t isPassableBlock (uint8_t block) {
block == B_torch
);
}
// Checks whether the given block is non-solid and spawnable
uint8_t isPassableSpawnBlock (uint8_t block) {
if (getFluid(block))
{
return 0;
}
return isPassableBlock(block);
}
// Checks whether the given block can be replaced by another block
uint8_t isReplaceableBlock (uint8_t block) {
@@ -877,6 +1044,17 @@ void handleFluidMovement (short x, uint8_t y, short z, uint8_t fluid, uint8_t bl
// a higher fluid "level" means the fluid has traveled farther
uint8_t level = block - fluid;
uint8_t max_level;
switch (fluid) {
case B_lava:
max_level = 3;
break;
case B_water:
max_level = 7;
break;
}
// Query blocks adjacent to this fluid stream
uint8_t adjacent[4] = {
getBlockAt(x + 1, y, z),
@@ -898,10 +1076,7 @@ void handleFluidMovement (short x, uint8_t y, short z, uint8_t fluid, uint8_t bl
// If not connected, clear this block and recalculate surrounding flow
if (!connected) {
makeBlockChange(x, y, z, B_air);
checkFluidUpdate(x + 1, y, z, adjacent[0]);
checkFluidUpdate(x - 1, y, z, adjacent[1]);
checkFluidUpdate(x, y, z + 1, adjacent[2]);
checkFluidUpdate(x, y, z - 1, adjacent[3]);
updateXYZNeighbors(x, y, z, UPDATE_NOW);
return;
}
}
@@ -910,42 +1085,38 @@ void handleFluidMovement (short x, uint8_t y, short z, uint8_t fluid, uint8_t bl
uint8_t block_below = getBlockAt(x, y - 1, z);
if (isReplaceableBlock(block_below)) {
makeBlockChange(x, y - 1, z, fluid);
return handleFluidMovement(x, y - 1, z, fluid, fluid);
updateXYZNeighbors(x, y, z, UPDATE_NOW);
return;
}
// Stop flowing laterally at the maximum level
if (level == 3 && fluid == B_lava) return;
if (level == 7) return;
if (level == max_level) return;
// Handle lateral water flow, increasing level by 1
if (isReplaceableFluid(adjacent[0], level, fluid)) {
makeBlockChange(x + 1, y, z, block + 1);
handleFluidMovement(x + 1, y, z, fluid, block + 1);
updateXYZNeighbors(x, y, z, UPDATE_NOW);
}
if (isReplaceableFluid(adjacent[1], level, fluid)) {
makeBlockChange(x - 1, y, z, block + 1);
handleFluidMovement(x - 1, y, z, fluid, block + 1);
updateXYZNeighbors(x, y, z, UPDATE_NOW);
}
if (isReplaceableFluid(adjacent[2], level, fluid)) {
makeBlockChange(x, y, z + 1, block + 1);
handleFluidMovement(x, y, z + 1, fluid, block + 1);
updateXYZNeighbors(x, y, z, UPDATE_NOW);
}
if (isReplaceableFluid(adjacent[3], level, fluid)) {
makeBlockChange(x, y, z - 1, block + 1);
handleFluidMovement(x, y, z - 1, fluid, block + 1);
updateXYZNeighbors(x, y, z, UPDATE_NOW);
}
}
void checkFluidUpdate (short x, uint8_t y, short z, uint8_t block) {
uint8_t fluid;
if (block >= B_water && block < B_water + 8) fluid = B_water;
else if (block >= B_lava && block < B_lava + 4) fluid = B_lava;
else return;
handleFluidMovement(x, y, z, fluid, block);
uint8_t getFluid(uint8_t block) {
if (block >= B_water && block < B_water + 8)
return B_water;
else if (block >= B_lava && block < B_lava + 4)
return B_lava;
return 0;
}
#ifdef ENABLE_PICKUP_ANIMATION
@@ -983,6 +1154,111 @@ void playPickupAnimation (PlayerData *player, uint16_t item, double x, double y,
}
#endif
// can not be zero!!
#define SAND_FALL_T 15
#define SAND_FALL_PROPAGATE_T 2
void processBlockUpdate (short x, uint8_t y, short z, uint8_t block, short update_kind) {
if (update_kind & UPDATE_BASIC) {
// "normal" block update
}
if (update_kind & UPDATE_CHECK_WATER) {
uint8_t fluid = getFluid(block);
if (fluid) {
uint8_t flow_speed;
switch (fluid) {
case B_lava:
flow_speed = 30;
break;
case B_water:
flow_speed = 5;
break;
}
deferBlockUpdate(x, y, z, flow_speed, UPDATE_FLOW_WATER);
}
}
if (update_kind & UPDATE_FLOW_WATER) {
uint8_t fluid = getFluid(block);
if (fluid) {
handleFluidMovement(x, y, z, fluid, block);
}
}
if (update_kind & UPDATE_CHECK_SAND_FALL) {
// we have a separate UPDATE_CHECK_SAND_FALL,
// to make chains of falling sand fall as one,
// with less delay between each
if (isFallingBlock(block) && y) {
uint8_t below = getBlockAt(x, y - 1, z);
if (isReplaceableBlock(below)) {
// move the sand down in 15 ticks
deferBlockUpdate(x, y, z, SAND_FALL_T - 1, UPDATE_FALL_SAND);
if (y != 255 && isFallingBlock(getBlockAt(x, y + 1, z))) {
// also tell the block above that the sand will be gone soon
deferBlockUpdate(x, y + 1, z, SAND_FALL_PROPAGATE_T, UPDATE_CHECK_SAND_FALL);
}
}
}
}
if (update_kind & UPDATE_FALL_SAND) {
// make sure that is a fallable:tm: block, and we are not in the floor
if (isFallingBlock(block) && y) {
uint8_t below = getBlockAt(x, y - 1, z);
// TODO: what to do if below block breaks sand
if (isReplaceableBlock(below)) {
// TODO: drop item of below block
makeBlockChange(x, y, z, B_air);
makeBlockChange(x, y - 1, z, B_air);
makeBlockChange(x, y - 1, z, block);
// update this block after moved
processBlockUpdate (x, y - 1, z, block, UPDATE_NOW);
// also update the neighbors
updateXZNeighbors(x, y, z, UPDATE_NOW);
updateXZNeighbors(x, y - 1, z, UPDATE_NOW);
if (y != 255) {
processBlockUpdate(x, y + 1, z, getBlockAt(x, y + 1, z), UPDATE_NOW);
}
}
}
}
}
void updateXZNeighbors (short x, uint8_t y, short z, short update_kind) {
processBlockUpdate(x - 1, y, z, getBlockAt(x - 1, y, z), update_kind);
processBlockUpdate(x + 1, y, z, getBlockAt(x + 1, y, z), update_kind);
processBlockUpdate(x, y, z - 1, getBlockAt(x, y, z - 1), update_kind);
processBlockUpdate(x, y, z + 1, getBlockAt(x, y, z + 1), update_kind);
}
void updateXYZNeighbors (short x, uint8_t y, short z, short update_kind) {
processBlockUpdate(x - 1, y, z, getBlockAt(x - 1, y, z), update_kind);
processBlockUpdate(x + 1, y, z, getBlockAt(x + 1, y, z), update_kind);
processBlockUpdate(x, y, z - 1, getBlockAt(x, y, z - 1), update_kind);
processBlockUpdate(x, y, z + 1, getBlockAt(x, y, z + 1), update_kind);
processBlockUpdate(x, y - 1, z, getBlockAt(x, y - 1, z), update_kind);
processBlockUpdate(x, y + 1, z, getBlockAt(x, y + 1, z), update_kind);
}
void deferBlockUpdate (short x, uint8_t y, short z, uint8_t await_ticks, short update_kind) {
if (deferred_block_updates_count == MAX_DEFERRED_BLOCK_UPDATES) {
return;
}
DeferredBlockUpdate *u = &deferred_block_updates[deferred_block_updates_count ++];
u->x = x;
u->y = y;
u->z = z;
u->await_ticks = await_ticks + is_processing_deferred_block_updates;
u->update_kind = update_kind;
}
void handlePlayerAction (PlayerData *player, int action, short x, short y, short z) {
// Re-sync slot when player drops an item
@@ -1006,55 +1282,34 @@ void handlePlayerAction (PlayerData *player, int action, short x, short y, short
// Ignore further actions not pertaining to mining blocks
if (action != 0 && action != 2) return;
uint8_t block = getBlockAt(x, y, z);
// In creative, only the "start mining" action is sent
// No additional verification is performed, the block is simply removed
if (action == 0 && GAMEMODE == 1) {
makeBlockChange(x, y, z, 0);
return;
makeBlockChange(x, y, z, B_air);
}
else {
// If this is a "start mining" packet, the block must be instamine
if (action == 0 && !isInstantlyMined(player, block)) return;
// Don't continue if the block change failed
if (makeBlockChange(x, y, z, B_air)) return;
uint16_t held_item = player->inventory_items[player->hotbar];
uint16_t item = getMiningResult(held_item, block);
bumpToolDurability(player);
if (item) {
#ifdef ENABLE_PICKUP_ANIMATION
playPickupAnimation(player, item, x, y, z);
#endif
givePlayerItem(player, item, 1);
}
}
uint8_t block = getBlockAt(x, y, z);
// If this is a "start mining" packet, the block must be instamine
if (action == 0 && !isInstantlyMined(player, block)) return;
// Don't continue if the block change failed
if (makeBlockChange(x, y, z, 0)) return;
uint16_t held_item = player->inventory_items[player->hotbar];
uint16_t item = getMiningResult(held_item, block);
bumpToolDurability(player);
if (item) {
#ifdef ENABLE_PICKUP_ANIMATION
playPickupAnimation(player, item, x, y, z);
#endif
givePlayerItem(player, item, 1);
}
// Update nearby fluids
uint8_t block_above = getBlockAt(x, y + 1, z);
#ifdef DO_FLUID_FLOW
checkFluidUpdate(x, y + 1, z, block_above);
checkFluidUpdate(x - 1, y, z, getBlockAt(x - 1, y, z));
checkFluidUpdate(x + 1, y, z, getBlockAt(x + 1, y, z));
checkFluidUpdate(x, y, z - 1, getBlockAt(x, y, z - 1));
checkFluidUpdate(x, y, z + 1, getBlockAt(x, y, z + 1));
#endif
// Check if any blocks above this should break, and if so,
// iterate upward over all blocks in the column and break them
uint8_t y_offset = 1;
while (isColumnBlock(block_above)) {
// Destroy the next block
makeBlockChange(x, y + y_offset, z, 0);
// Check for item drops *without a tool*
uint16_t item = getMiningResult(0, block_above);
if (item) givePlayerItem(player, item, 1);
// Select the next block in the column
y_offset ++;
block_above = getBlockAt(x, y + y_offset, z);
}
// Update nearby blocks
updateXYZNeighbors(x, y, z, UPDATE_NOW);
}
void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t face) {
@@ -1151,7 +1406,7 @@ void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t
} else if (getItemDefensePoints(*item) != 0) {
// For some reason, this action is sent twice when looking at a block
// Ignore the variant that has coordinates
if (face == 255) return;
if (face != 255) return;
// Swap to held piece of armor
uint8_t slot = getArmorItemSlot(*item);
uint16_t prev_item = player->inventory_items[slot];
@@ -1190,8 +1445,7 @@ void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t
(y == player->y || y == player->y + 1) &&
z == player->z
) &&
isReplaceableBlock(getBlockAt(x, y, z)) &&
(!isColumnBlock(block) || getBlockAt(x, y - 1, z) != B_air)
isReplaceableBlock(getBlockAt(x, y, z))
) {
// Apply server-side block change
if (makeBlockChange(x, y, z, block)) return;
@@ -1199,14 +1453,9 @@ void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t
*count -= 1;
// Clear item id in slot if amount is zero
if (*count == 0) *item = 0;
// Calculate fluid flow
#ifdef DO_FLUID_FLOW
checkFluidUpdate(x, y + 1, z, getBlockAt(x, y + 1, z));
checkFluidUpdate(x - 1, y, z, getBlockAt(x - 1, y, z));
checkFluidUpdate(x + 1, y, z, getBlockAt(x + 1, y, z));
checkFluidUpdate(x, y, z - 1, getBlockAt(x, y, z - 1));
checkFluidUpdate(x, y, z + 1, getBlockAt(x, y, z + 1));
#endif
// Send updates
processBlockUpdate(x, y, z, block, UPDATE_NOW);
updateXYZNeighbors(x, y, z, UPDATE_NOW);
}
// Sync hotbar contents to player
@@ -1246,11 +1495,57 @@ void spawnMob (uint8_t type, short x, uint8_t y, short z, uint8_t health) {
);
}
// Freshly spawned mobs currently don't need metadata updates.
// If this changes, uncomment this line.
// broadcastMobMetadata(-1, i);
break;
}
}
void interactEntity (int entity_id, int interactor_id) {
PlayerData *player;
if (getPlayerData(interactor_id, &player)) return;
MobData *mob = &mob_data[-entity_id - 2];
switch (mob->type) {
case 106: // Sheep
if (player->inventory_items[player->hotbar] != I_shears)
return;
if ((mob->data >> 5) & 1) // Check if sheep has already been sheared
return;
mob->data |= 1 << 5; // Set sheared to true
bumpToolDurability(player);
#ifdef ENABLE_PICKUP_ANIMATION
playPickupAnimation(player, I_white_wool, mob->x, mob->y, mob->z);
#endif
uint8_t item_count = 1 + (fast_rand() & 1); // 1-2
givePlayerItem(player, I_white_wool, item_count);
for (int i = 0; i < MAX_PLAYERS; i ++) {
PlayerData* player = &player_data[i];
int client_fd = player->client_fd;
if (client_fd == -1) continue;
if (player->flags & 0x20) continue;
sc_entityAnimation(client_fd, interactor_id, 0);
}
broadcastMobMetadata(-1, entity_id);
break;
}
}
void hurtEntity (int entity_id, int attacker_id, uint8_t damage_type, uint8_t damage) {
if (attacker_id > 0) { // Attacker is a player
@@ -1328,10 +1623,14 @@ void hurtEntity (int entity_id, int attacker_id, uint8_t damage_type, uint8_t da
strcpy((char *)recv_buffer + player_name_len, " was slain by ");
strcpy((char *)recv_buffer + player_name_len + 14, attacker->name);
recv_buffer[player_name_len + 14 + strlen(attacker->name)] = '\0';
} else if (damage_type == D_cactus) {
// Killed by being near a cactus
strcpy((char *)recv_buffer + player_name_len, " was pricked to death");
recv_buffer[player_name_len + 21] = '\0';
} else {
// Unknown death reason
strcpy((char *)recv_buffer + player_name_len, " died");
recv_buffer[player_name_len + 4] = '\0';
recv_buffer[player_name_len + 5] = '\0';
}
} else player->health -= effective_damage;
@@ -1347,6 +1646,9 @@ void hurtEntity (int entity_id, int attacker_id, uint8_t damage_type, uint8_t da
// Don't continue if the mob is already dead
if (mob_health == 0) return;
// Set the mob's panic timer
mob->data |= (3 << 6);
// Process health change on the server
if (mob_health <= damage) {
@@ -1405,8 +1707,7 @@ void handleServerTick (int64_t time_since_last_tick) {
// If 3 seconds (60 vanilla ticks) have passed, assume player has loaded
player->flagval_16 ++;
if (player->flagval_16 > (uint16_t)(3 * TICKS_PER_SECOND)) {
player->flags &= ~0x20;
player->flagval_16 = 0;
handlePlayerJoin(player);
} else continue;
}
// Reset player attack cooldown
@@ -1424,6 +1725,11 @@ void handleServerTick (int64_t time_since_last_tick) {
player->flagval_16 = 0;
} else player->flagval_16 ++;
}
// Reset movement update cooldown if not broadcasting every update
// Effectively ties player movement updates to the tickrate
#ifndef BROADCAST_ALL_MOVEMENT
player->flags &= ~0x40;
#endif
// Below this, process events that happen once per second
if (server_ticks % (uint32_t)TICKS_PER_SECOND != 0) continue;
// Send Keep Alive and Update Time packets
@@ -1434,6 +1740,15 @@ void handleServerTick (int64_t time_since_last_tick) {
if (block >= B_lava && block < B_lava + 4) {
hurtEntity(player->client_fd, -1, D_lava, 8);
}
#ifdef ENABLE_CACTUS_DAMAGE
// Tick damage from a cactus block if one is under/inside or around the player.
if (block == B_cactus ||
getBlockAt(player->x + 1, player->y, player->z) == B_cactus ||
getBlockAt(player->x - 1, player->y, player->z) == B_cactus ||
getBlockAt(player->x, player->y, player->z + 1) == B_cactus ||
getBlockAt(player->x, player->y, player->z - 1) == B_cactus
) hurtEntity(player->client_fd, -1, D_cactus, 4);
#endif
// Heal from saturation if player is able and has enough food
if (player->health >= 20 || player->health == 0) continue;
if (player->hunger < 18) continue;
@@ -1447,11 +1762,8 @@ void handleServerTick (int64_t time_since_last_tick) {
sc_setHealth(player->client_fd, player->health, player->hunger, player->saturation);
}
// Write data to file (if applicable)
writePlayerDataToDisk();
#ifdef DISK_SYNC_BLOCKS_ON_INTERVAL
writeBlockChangesToDisk(0, block_changes_count);
#endif
// Perform regular checks for if it's time to write to disk
writeDataToDiskOnInterval();
/**
* If the RNG seed ever hits 0, it'll never generate anything
@@ -1462,6 +1774,22 @@ void handleServerTick (int64_t time_since_last_tick) {
*/
if (rng_seed == 0) rng_seed = world_seed;
// block updates might add more deferred block updates,
// so we temporarily make all new block updates add one more tick to the defer tick counter
is_processing_deferred_block_updates = 1;
int next_update_idx = 0;
for (int i = 0; i < deferred_block_updates_count; i ++) {
DeferredBlockUpdate *u = &deferred_block_updates[i];
if (u->await_ticks) {
u->await_ticks --;
deferred_block_updates[next_update_idx ++] = *u;
} else {
processBlockUpdate(u->x, u->y, u->z, getBlockAt(u->x, u->y, u->z), u->update_kind);
}
}
deferred_block_updates_count = next_update_idx;
is_processing_deferred_block_updates = 0;
// Tick mob behavior
for (int i = 0; i < MAX_MOBS; i ++) {
if (mob_data[i].type == 0) continue;
@@ -1490,6 +1818,9 @@ void handleServerTick (int64_t time_since_last_tick) {
mob_data[i].type == 95 || // Pig
mob_data[i].type == 106 // Sheep
);
// Mob "panic" timer, set to 3 after being hit
// Currently has no effect on hostile mobs
uint8_t panic = (mob_data[i].data >> 6) & 3;
// Burn hostile mobs if above ground during sunlight
if (!passive && (world_time < 13000 || world_time > 23460) && mob_data[i].y > 48) {
@@ -1499,8 +1830,21 @@ void handleServerTick (int64_t time_since_last_tick) {
uint32_t r = fast_rand();
if (passive) {
// Update passive mobs once per 4 seconds on average
if (r % (4 * (unsigned int)TICKS_PER_SECOND) != 0) continue;
if (panic) {
// If panicking, move randomly at up to 4 times per second
if (TICKS_PER_SECOND >= 4) {
uint32_t ticks_per_panic = (uint32_t)(TICKS_PER_SECOND / 4);
if (server_ticks % ticks_per_panic != 0) continue;
}
// Reset panic state after timer runs out
// Each panic timer tick takes one second
if (server_ticks % (uint32_t)TICKS_PER_SECOND == 0) {
mob_data[i].data -= (1 << 6);
}
} else {
// When not panicking, move idly once per 4 seconds on average
if (r % (4 * (unsigned int)TICKS_PER_SECOND) != 0) continue;
}
} else {
// Update hostile mobs once per second
if (server_ticks % (uint32_t)TICKS_PER_SECOND != 0) continue;
@@ -1527,8 +1871,11 @@ void handleServerTick (int64_t time_since_last_tick) {
continue;
}
short new_x = mob_data[i].x, new_z = mob_data[i].z;
uint8_t new_y = mob_data[i].y, yaw = 0;
short old_x = mob_data[i].x, old_z = mob_data[i].z;
uint8_t old_y = mob_data[i].y;
short new_x = old_x, new_z = old_z;
uint8_t new_y = old_y, yaw = 0;
if (passive) { // Passive mob movement handling
@@ -1545,48 +1892,98 @@ void handleServerTick (int64_t time_since_last_tick) {
} else { // Hostile mob movement handling
// If we're already next to the player, hurt them and skip movement
if (closest_dist < 3 && abs(mob_data[i].y - closest_player->y) < 2) {
if (closest_dist < 3 && abs(old_y - closest_player->y) < 2) {
hurtEntity(closest_player->client_fd, entity_id, D_generic, 6);
continue;
}
// Move towards the closest player on 8 axis
// The condition nesting ensures a correct yaw at 45 degree turns
if (closest_player->x < mob_data[i].x) {
if (closest_player->x < old_x) {
new_x -= 1; yaw = 64;
if (closest_player->z < mob_data[i].z) { new_z -= 1; yaw += 32; }
else if (closest_player->z > mob_data[i].z) { new_z += 1; yaw -= 32; }
if (closest_player->z < old_z) { new_z -= 1; yaw += 32; }
else if (closest_player->z > old_z) { new_z += 1; yaw -= 32; }
}
else if (closest_player->x > mob_data[i].x) {
else if (closest_player->x > old_x) {
new_x += 1; yaw = 192;
if (closest_player->z < mob_data[i].z) { new_z -= 1; yaw -= 32; }
else if (closest_player->z > mob_data[i].z) { new_z += 1; yaw += 32; }
if (closest_player->z < old_z) { new_z -= 1; yaw -= 32; }
else if (closest_player->z > old_z) { new_z += 1; yaw += 32; }
} else {
if (closest_player->z < mob_data[i].z) { new_z -= 1; yaw = 128; }
else if (closest_player->z > mob_data[i].z) { new_z += 1; yaw = 0; }
if (closest_player->z < old_z) { new_z -= 1; yaw = 128; }
else if (closest_player->z > old_z) { new_z += 1; yaw = 0; }
}
}
// Vary the yaw angle to look just a little less robotic
yaw += ((r >> 7) & 31) - 16;
// Check if the blocks we're moving into are passable:
// if yes, and the block below is solid, keep the same Y level;
// if yes, but the block below isn't solid, drop down one block;
// if not, go up by up to one block;
// if going up isn't possible, skip this iteration.
// Holds the block that the mob is moving into
uint8_t block = getBlockAt(new_x, new_y, new_z);
// Holds the block above the target block, i.e. the "head" block
uint8_t block_above = getBlockAt(new_x, new_y + 1, new_z);
if (!isPassableBlock(block) || !isPassableBlock(block_above)) {
block = block_above;
block_above = getBlockAt(new_x, new_y + 2, new_z);
if (isPassableBlock(block) && isPassableBlock(block_above)) new_y += 1;
else continue;
} else {
uint8_t block_below = getBlockAt(new_x, new_y - 1, new_z);
if (isPassableBlock(block_below) && block != B_water) new_y -= 1;
// Validate movement on X axis
if (new_x != old_x && (
!isPassableBlock(getBlockAt(new_x, new_y + 1, old_z)) ||
(
!isPassableBlock(getBlockAt(new_x, new_y, old_z)) &&
!isPassableBlock(getBlockAt(new_x, new_y + 2, old_z))
)
)) {
new_x = old_x;
block = getBlockAt(old_x, new_y, new_z);
block_above = getBlockAt(old_x, new_y + 1, new_z);
}
// Validate movement on Z axis
if (new_z != old_z && (
!isPassableBlock(getBlockAt(old_x, new_y + 1, new_z)) ||
(
!isPassableBlock(getBlockAt(old_x, new_y, new_z)) &&
!isPassableBlock(getBlockAt(old_x, new_y + 2, new_z))
)
)) {
new_z = old_z;
block = getBlockAt(new_x, new_y, old_z);
block_above = getBlockAt(new_x, new_y + 1, old_z);
}
// Validate diagonal movement
if (new_x != old_x && new_z != old_z && (
!isPassableBlock(block_above) ||
(
!isPassableBlock(block) &&
!isPassableBlock(getBlockAt(new_x, new_y + 2, new_z))
)
)) {
// We know that movement along just one axis is fine thanks to the
// checks above, pick one based on proximity.
int dist_x = abs(old_x - closest_player->x);
int dist_z = abs(old_z - closest_player->z);
if (dist_x < dist_z) new_z = old_z;
else new_x = old_x;
block = getBlockAt(new_x, new_y, new_z);
}
// Check if we're supposed to climb/drop one block
// The checks above already ensure that there's enough space to climb
if (!isPassableBlock(block)) new_y += 1;
else if (isPassableBlock(getBlockAt(new_x, new_y - 1, new_z))) new_y -= 1;
// Exit early if all movement was cancelled
if (new_x == mob_data[i].x && new_z == old_z && new_y == old_y) continue;
// Prevent collisions with other mobs
uint8_t colliding = false;
for (int j = 0; j < MAX_MOBS; j ++) {
if (j == i) continue;
if (mob_data[j].type == 0) continue;
if (
mob_data[j].x == new_x &&
mob_data[j].z == new_z &&
abs((int)mob_data[j].y - (int)new_y) < 2
) {
colliding = true;
break;
}
}
if (colliding) continue;
if ( // Hurt mobs that stumble into lava
(block >= B_lava && block < B_lava + 4) ||
@@ -1598,6 +1995,9 @@ void handleServerTick (int64_t time_since_last_tick) {
mob_data[i].y = new_y;
mob_data[i].z = new_z;
// Vary the yaw angle to look just a little less robotic
yaw += ((r >> 7) & 31) - 16;
// Broadcast relevant entity movement packets
for (int j = 0; j < MAX_PLAYERS; j ++) {
if (player_data[j].client_fd == -1) continue;
@@ -1633,3 +2033,47 @@ void broadcastChestUpdate (int origin_fd, uint8_t *storage_ptr, uint16_t item, u
}
#endif
ssize_t writeEntityData (int client_fd, EntityData *data) {
writeByte(client_fd, data->index);
writeVarInt(client_fd, data->type);
switch (data->type) {
case 0: // Byte
return writeByte(client_fd, data->value.byte);
case 21: // Pose
writeVarInt(client_fd, data->value.pose);
return 0;
default: return -1;
}
}
// Returns the networked size of an EntityData entry
int sizeEntityData (EntityData *data) {
int value_size;
switch (data->type) {
case 0: // Byte
value_size = 1;
break;
case 21: // Pose
value_size = sizeVarInt(data->value.pose);
break;
default: return -1;
}
return 1 + sizeVarInt(data->type) + value_size;
}
// Returns the networked size of an array of EntityData entries
int sizeEntityMetadata (EntityData *metadata, size_t length) {
int total_size = 0;
for (size_t i = 0; i < length; i ++) {
int size = sizeEntityData(&metadata[i]);
if (size == -1) return -1;
total_size += size;
}
return total_size;
}

View File

@@ -16,6 +16,8 @@
int64_t last_disk_sync_time = 0;
// TODO: store deferred block updates
// Restores world data from disk, or writes world file if it doesn't exist
int initSerializer () {
@@ -45,6 +47,7 @@ int initSerializer () {
size_t read = fread(block_changes, 1, sizeof(block_changes), file);
if (read != sizeof(block_changes)) {
printf("Read %u bytes from \"world.bin\", expected %u (block changes). Aborting.\n", read, sizeof(block_changes));
fclose(file);
return 1;
}
// Find the index of the last occupied entry to recover block_changes_count
@@ -56,6 +59,7 @@ int initSerializer () {
// Seek past block changes to start reading player data
if (fseek(file, sizeof(block_changes), SEEK_SET) != 0) {
perror("Failed to seek to player data in \"world.bin\". Aborting.");
fclose(file);
return 1;
}
// Read player data directly into memory
@@ -86,6 +90,7 @@ int initSerializer () {
"Failed to write initial block data to \"world.bin\".\n"
"Consider checking permissions or disabling SYNC_WORLD_TO_DISK in \"globals.h\"."
);
fclose(file);
return 1;
}
// Seek past written block changes to start writing player data
@@ -94,6 +99,7 @@ int initSerializer () {
"Failed to seek past block changes in \"world.bin\"."
"Consider checking permissions or disabling SYNC_WORLD_TO_DISK in \"globals.h\"."
);
fclose(file);
return 1;
}
// Write initial player data to disk (should be just nulls?)
@@ -115,12 +121,6 @@ int initSerializer () {
// Writes a range of block change entries to disk
void writeBlockChangesToDisk (int from, int to) {
#ifdef DISK_SYNC_BLOCKS_ON_INTERVAL
// Skip this write if enough time hasn't passed since the last one
if (get_program_time() - last_disk_sync_time < DISK_SYNC_INTERVAL) return;
last_disk_sync_time = get_program_time();
#endif
// Try to open the file in rw (without overwriting)
FILE *file = fopen(FILE_PATH, "r+b");
if (!file) {
@@ -149,10 +149,6 @@ void writeBlockChangesToDisk (int from, int to) {
// Writes all player data to disk
void writePlayerDataToDisk () {
// Skip this write if enough time hasn't passed since the last one
if (get_program_time() - last_disk_sync_time < DISK_SYNC_INTERVAL) return;
last_disk_sync_time = get_program_time();
// Try to open the file in rw (without overwriting)
FILE *file = fopen(FILE_PATH, "r+b");
if (!file) {
@@ -176,6 +172,21 @@ void writePlayerDataToDisk () {
fclose(file);
}
// Writes data queued for interval writes, but only if enough time has passed
void writeDataToDiskOnInterval () {
// Skip this write if enough time hasn't passed since the last one
if (get_program_time() - last_disk_sync_time < DISK_SYNC_INTERVAL) return;
last_disk_sync_time = get_program_time();
// Write full player data and block changes buffers
writePlayerDataToDisk();
#ifdef DISK_SYNC_BLOCKS_ON_INTERVAL
writeBlockChangesToDisk(0, block_changes_count);
#endif
}
#ifdef ALLOW_CHESTS
// Writes a chest slot change to disk
void writeChestChangesToDisk (uint8_t *storage_ptr, uint8_t slot) {

View File

@@ -7,8 +7,13 @@
#include "lwip/netdb.h"
#include "esp_timer.h"
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#endif
#include <unistd.h>
#include <time.h>
#ifndef CLOCK_MONOTONIC
@@ -21,14 +26,16 @@
#include "procedures.h"
#include "tools.h"
static uint64_t htonll (uint64_t value) {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
return ((uint64_t)htonl((uint32_t)(value >> 32))) |
((uint64_t)htonl((uint32_t)(value & 0xFFFFFFFF)) << 32);
#else
return value;
#ifndef htonll
static uint64_t htonll (uint64_t value) {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
return ((uint64_t)htonl((uint32_t)(value >> 32))) |
((uint64_t)htonl((uint32_t)(value & 0xFFFFFFFF)) << 32);
#else
return value;
#endif
}
#endif
}
// Keep track of the total amount of bytes received with recv_all
// Helps notice misread packets and clean up after errors
@@ -93,7 +100,11 @@ ssize_t send_all (int client_fd, const void *buf, ssize_t len) {
// Busy-wait (with task yielding) until all data has been sent
while (sent < len) {
ssize_t n = send(client_fd, p + sent, len - sent, MSG_NOSIGNAL);
#ifdef _WIN32
ssize_t n = send(client_fd, p + sent, len - sent, 0);
#else
ssize_t n = send(client_fd, p + sent, len - sent, MSG_NOSIGNAL);
#endif
if (n > 0) { // some data was sent, log it
sent += n;
last_update_time = get_program_time();
@@ -104,7 +115,12 @@ ssize_t send_all (int client_fd, const void *buf, ssize_t len) {
return -1;
}
// not yet ready to transmit, try again
#ifdef _WIN32 //handles windows socket timeout
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK || err == WSAEINTR) {
#else
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
#endif
// handle network timeout
if (get_program_time() - last_update_time > NETWORK_TIMEOUT_TIME) {
disconnectClient(&client_fd, -2);
@@ -201,12 +217,45 @@ double readDouble (int client_fd) {
return output;
}
// Receive length prefixed data with bounds checking
ssize_t readLengthPrefixedData (int client_fd) {
uint32_t length = readVarInt(client_fd);
if (length >= MAX_RECV_BUF_LEN) {
printf("ERROR: Received length (%lu) exceeds maximum (%u)\n", length, MAX_RECV_BUF_LEN);
disconnectClient(&client_fd, -1);
recv_count = 0;
return 0;
}
return recv_all(client_fd, recv_buffer, length, false);
}
// Reads a networked string into recv_buffer
void readString (int client_fd) {
uint32_t length = readVarInt(client_fd);
recv_count = recv_all(client_fd, recv_buffer, length, false);
recv_count = readLengthPrefixedData(client_fd);
recv_buffer[recv_count] = '\0';
}
// Reads a networked string of up to N bytes into recv_buffer
void readStringN (int client_fd, uint32_t max_length) {
// Forward to readString if max length is invalid
if (max_length >= MAX_RECV_BUF_LEN) {
readString(client_fd);
return;
}
// Attempt to read full string within maximum
uint32_t length = readVarInt(client_fd);
if (max_length > length) {
recv_count = recv_all(client_fd, recv_buffer, length, false);
recv_buffer[recv_count] = '\0';
return;
}
// Read string up to maximum, dump the rest
recv_count = recv_all(client_fd, recv_buffer, max_length, false);
recv_buffer[recv_count] = '\0';
uint8_t dummy;
for (uint32_t i = max_length; i < length; i ++) {
recv_all(client_fd, &dummy, 1, false);
}
}
uint32_t fast_rand () {
rng_seed ^= rng_seed << 13;

View File

@@ -1,5 +1,10 @@
#include <stdint.h>
#include <arpa/inet.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#endif
#include <unistd.h>
#include "varnum.h"

View File

@@ -172,9 +172,7 @@ uint8_t getHeightAt (int x, int z) {
uint8_t getTerrainAtFromCache (int x, int y, int z, int rx, int rz, ChunkAnchor anchor, ChunkFeature feature, uint8_t height) {
if (y < 64 || y < height) goto skip_feature;
switch (anchor.biome) {
if (y >= 64 && y >= height && feature.y != 255) switch (anchor.biome) {
case W_plains: { // Generate trees in the plains biome
// Don't generate trees underwater
@@ -250,7 +248,6 @@ uint8_t getTerrainAtFromCache (int x, int y, int z, int rx, int rz, ChunkAnchor
default: break;
}
skip_feature:
// Handle surface-level terrain (the very topmost blocks)
if (height >= 63) {
if (y == height) {