Compare commits

..

135 commits

Author SHA1 Message Date
f6abf37f41
items
Some checks failed
Node.js Build / build (20.x) (push) Failing after 6m35s
2024-10-29 00:47:11 +00:00
f156e21035 Update LICENSE year
All checks were successful
Node.js Build / build (20.x) (push) Successful in 5m29s
2024-10-28 09:46:55 +00:00
2761e6978c bump node version in workflow 2024-10-28 09:37:52 +00:00
25f52fc11d
add a few items 2024-10-27 13:00:20 +00:00
c521455ec4
improve shutdown procedure 2024-10-26 14:39:05 +01:00
78c894a94d
convert whole project to use default exports 2024-10-26 14:24:38 +01:00
7d4445112b
implement can place at 2024-10-18 21:47:03 +01:00
a6da71e6a2
make sugarcane break when no block under them 2024-10-17 21:20:56 +01:00
db9847634e
generate sugar cane on grass instead, forgot this only became a thing in beta 1.8+ 2024-10-17 10:02:43 +01:00
19453b7f16
generate sugar cane 2024-10-17 02:24:08 +01:00
9f93f14461
add all beta 1.7.3 blocks 2024-10-17 01:51:50 +01:00
45c08c0c5f
allow changing metadata directly without updating block id 2024-10-17 01:51:41 +01:00
5c60d1d758
WIP: New Overworld Generator
Some checks failed
Node.js Build / build (18.x) (push) Has been cancelled
2024-07-09 21:52:22 +01:00
f606d1547b
use funkyarray package 2024-07-08 09:56:03 +01:00
61586092d1
Add Item and Vector type implementations to MetadataWriter 2024-02-16 09:34:02 +00:00
9ae490bba3
make entity collision on the Y axis more reliable 2023-12-29 16:32:07 +00:00
b02ac676e2
Make dropped items properly work 2023-12-25 02:45:21 +00:00
328ddca458
Implement EntityLiving -> EntityLiving damage & block break sfx / effects 2023-12-24 17:47:20 +00:00
4afb4a0633
death and item drops n' stuff 2023-12-18 21:49:23 +00:00
797cd5db62
add TrackedProperty
This will be used for updating inventory slots when ItemStack properties change
2023-12-18 11:19:51 +00:00
f4733f50a3
Make item collection work 2023-12-18 11:18:15 +00:00
dc05cd4a2d
WIP: Item Collection 2023-12-18 01:23:52 +00:00
3ea5e47c57
Allow for entities to be killed. 2023-12-06 00:04:57 +00:00
91395db919
stop storing individual worlds in class vars 2023-12-05 20:36:50 +00:00
9231ce85d6
fix not having a seed crashing the server on startup 2023-12-05 20:07:32 +00:00
d3ebe1518d
WIP: Start trying to handle respawn. 2023-11-13 00:00:33 +00:00
a5910abd86
Only save player data on entity remove if the player is not dead. 2023-11-13 00:00:17 +00:00
1a08b5e991
Inform clients of entity destruction for all entity types 2023-11-12 23:59:54 +00:00
278c90f5f9
Make tall grass not drop 2023-11-11 00:14:27 +00:00
604bec9e89
Implement Entity / Player data saving 2023-11-09 21:59:45 +00:00
5a250cb601 WIP: Player Saving 2023-11-09 17:04:11 +00:00
e303cbc5df Remove AABB creation logging 2023-11-09 16:32:34 +00:00
63333a04aa Implement AABB collision and add EntityItem properly.
The items are fully functional apart from picking them up, they are commented out in the MPClient breakBlock function if you want to play with them.
2023-11-09 16:30:40 +00:00
6033c86247 start work on AABB 2023-11-08 15:45:25 +00:00
875318db04 improve initial generation logging 2023-11-08 15:44:46 +00:00
228602a54d remove time debug printing 2023-11-08 13:40:27 +00:00
64709abfb9 generate spawn chunks smarter 2023-11-08 13:36:22 +00:00
de4203d9f0
new save format 2023-11-07 20:46:17 +00:00
79f3ae1d4f
handle block breaking better 2023-11-07 01:50:51 +00:00
61b667ac49
Read multiple packets per chunk 2023-11-07 01:50:35 +00:00
0403ace046
zero the fall distance 2023-11-05 13:53:45 +00:00
617cebe2e0
Prevent fall damage if in water 2023-11-05 13:30:17 +00:00
f9d4c6b1f9
Merge branch 'typescript' of https://github.com/tgpholly/mc-beta-server into typescript 2023-11-05 10:58:52 +00:00
8054f06327
Send velocity for all entities 2023-11-05 10:58:47 +00:00
23a1d124c0
Send player equipment on join 2023-11-05 10:58:35 +00:00
6d7d1299ce
Use a better send method for position sending 2023-11-05 10:58:01 +00:00
d64f9469cb
Destroy entities on the client when they are removed from the world 2023-11-05 10:56:57 +00:00
81b5fb302d
Add isZero getter to Vec2 and Vec3 2023-11-05 10:56:28 +00:00
8fc6836b29
Fix EntityEquipment having the wrong byte length 2023-11-05 10:56:11 +00:00
1b2b1a091e
Add Packets DestroyEntity and EntityVelocity 2023-11-05 10:55:49 +00:00
bb733ff2af
Allow ItemStack(s) to be compared 2023-11-05 10:55:15 +00:00
0eb23adb09
Update README.md 2023-11-05 01:08:01 +00:00
83f37a0621
don't care 2023-11-05 00:58:00 +00:00
522916ecaa
Implement fall damage & fix damage in general 2023-11-05 00:55:23 +00:00
a3c865bd9f Artifacts attempt 2 2023-11-02 10:54:36 +00:00
2daec09426 Attempt to upload artifacts 2023-11-02 10:52:45 +00:00
0c0d8f7711 Swap smasher out for @vercel/ncc 2023-11-02 10:40:46 +00:00
f1c4dcda75
Block getting 2023-11-02 08:31:43 +00:00
d8d1eabcf4
WIP: Inventories 2023-10-29 05:08:26 +00:00
74374cb0d6
implement dimension switching 2023-09-04 23:42:38 +01:00
9feafc46c7
allow server to read chunks from disk again (oops) 2023-09-04 02:05:14 +01:00
479cc35d89
Update README to match current dev 2023-09-04 02:02:25 +01:00
a61d807966
update fileSmasher to support exporting default functions 2023-09-04 01:46:23 +01:00
690c7be16f
Update WorldSaveManager to support multiple dimensions and add the nether 2023-09-04 01:43:11 +01:00
5c25dd7436
ignore config file 2023-09-04 01:42:36 +01:00
19328d5500
remove config file 2023-09-04 01:42:04 +01:00
aea8c440d9
Fix issue preventing chunks from being loaded from disk 2023-08-20 16:31:23 +01:00
ad240d3598
Fix CI 2023-08-20 02:17:39 +01:00
30569c06ab
Update depends 2023-08-20 02:17:30 +01:00
5c478f461e
Store each entities current chunk inside of their object 2023-08-20 02:10:49 +01:00
41e1124c04
Flip the x and z on every other noise level 2023-08-20 02:10:30 +01:00
79b1f0c0eb
maintenance 2023-08-20 01:18:05 +01:00
3123dac2c2
Entity Status Packet 2023-08-20 01:17:37 +01:00
84bbe26454
Drowning and damage updates 2023-08-20 01:17:27 +01:00
310f1bd091
Block Behaviours 2023-08-20 01:17:05 +01:00
36ef685813
Update fileSmasher.ts 2023-08-15 10:32:44 +01:00
ad83454afa Switch to hsconsole package & add proper shutdown procedure 2023-08-15 10:00:12 +01:00
c80dcd2d4f
Allow blocks to update neighboring blocks 2023-08-12 22:06:16 +01:00
3351888f19 fix broken import 2023-07-28 12:40:07 +01:00
247ecb3484 Migrate to dyetty 2023-07-28 12:38:43 +01:00
c8b4a2f756 Update Depends 2023-07-28 10:36:56 +01:00
6ca1b7f052 CI Fix attempt 2023-07-28 10:36:50 +01:00
1f7d3652e1
oops 2023-07-13 16:49:45 +01:00
d387a6c299
don't need webpack anymore 2023-07-13 16:49:10 +01:00
e05f8d6c58
Update README.md 2023-06-26 16:50:51 +01:00
b038315b35 improve the readme 2023-06-26 16:47:39 +01:00
8bed6c7310 fileSmasher cleanup 2023-06-26 16:21:16 +01:00
a4ef4c6945 update README.md 2023-06-26 16:18:09 +01:00
ba1c4e04d8 general cleanup + add fileSmasher to replace webpack 2023-06-26 16:16:55 +01:00
3ca20743a3 clear off even more codefactor issues 2023-06-26 10:00:06 +01:00
59e12c0655 fix a bunch more codefactor issues 2023-06-26 09:57:40 +01:00
21a9f64a9e fix a bunch of codefactor issues 2023-06-26 09:53:45 +01:00
44f8568cdb sync package.json and package-lock.json so ci passes 2023-06-26 09:42:03 +01:00
055627bb63 Migrate to bufferstuff module 2023-06-22 12:43:28 +01:00
5fd4f50b1a migrate to bufferstuff npm module 2023-06-22 12:43:19 +01:00
3b208eb8db
lighting sort of works 2023-06-19 18:29:16 +01:00
5396b83f3f
Attempt to fix the workflow 2023-05-02 21:04:08 +01:00
a8d9ab18bb
Update depends 2023-05-02 20:59:12 +01:00
5fa7927402
Update submodules 2023-05-02 20:43:43 +01:00
15917f1533
Rework to use new bufferStuff submodule 2023-05-02 20:39:40 +01:00
ef1e7a278e
Merge branch 'typescript' of https://github.com/tgpethan/mc-beta-server into typescript 2023-05-02 08:51:13 +01:00
2aedd81baa
WIP: Lighting 2023-05-02 08:50:49 +01:00
ec24b6433e
Update README.md 2023-04-21 16:13:30 +01:00
93b4e701c6
Unfuck the workflow 2023-04-21 16:08:38 +01:00
c55c091872
Unfuck the workflow 2023-04-21 16:07:34 +01:00
3a84e84090
Attempt to unfuck the workflow 2023-04-21 16:06:14 +01:00
98060d67b7
Unfuck the workflow 2023-04-21 16:05:10 +01:00
acf4121727
Create node.js.yml 2023-04-21 16:04:00 +01:00
ad1c402d84
fix massive crators appearing, see: https://eusv.net/ZHMjQZTTNNM2nS 2023-04-17 02:15:51 +01:00
8b906bfe8f
allow chunks to be saved again. oops 2023-04-17 02:07:01 +01:00
e21bda6201
Add basic cave generation 2023-04-17 02:05:11 +01:00
0653da20b2
read config compression type as SaveCompressionType 2023-04-17 02:05:01 +01:00
b1a4933990
add logging to file 2023-04-17 02:04:05 +01:00
61133eb710
Update README.md 2023-04-14 00:01:40 +01:00
ae25686b10
Update README.md 2023-04-13 23:53:36 +01:00
686a694432
cross chunk block placement 2023-04-13 23:52:13 +01:00
Holly Stubbs
75d36b49db Update Depends 2023-04-13 11:06:00 +01:00
Holly Stubbs
35aa372d8e Force write chunks to disk on initial generation 2023-04-13 10:46:09 +01:00
e252fe255c
Update README.md 2023-04-13 00:30:13 +01:00
47f6d5cc37
Fix loading not actually working because of bogus instanceof checks 2023-04-13 00:26:01 +01:00
dd257acfe2
fix it for deflate as well oops 2023-04-12 23:38:27 +01:00
f8a52dfc98
Fix chunk loading 2023-04-12 23:37:51 +01:00
b474d35775
f 2023-04-12 23:01:23 +01:00
860c8f4866
wip saving to disk / chunk async 2023-04-11 07:47:56 +01:00
42cef0a838
WIP: Inventory support 2023-04-11 01:53:33 +01:00
b371521fda
Animations & Metadata 2023-04-10 21:52:30 +01:00
f93bb2605d
players!!!! 2023-04-10 14:42:14 +01:00
36d294a810
update package.json urls 2023-04-09 04:59:23 +01:00
e5ba93ce9c
fix more codefactor issues 2023-04-09 04:51:30 +01:00
16c7b5ddcc
fix a whole bunch of codefactor issues 2023-04-09 04:47:23 +01:00
fa27b476e7
Make CodeFactor image point to the typescript branch 2023-04-09 04:30:23 +01:00
122e0fa53a
Update README.md 2023-04-09 04:23:22 +01:00
5de6e74323
INFINITE TERRAIN!!!! 2023-04-09 04:19:10 +01:00
2503664723
Update LICENSE 2023-04-08 20:55:05 +01:00
3edcbc062f
Initial TypeScript commit 2023-04-08 20:52:47 +01:00
150 changed files with 9400 additions and 2537 deletions

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

37
.github/workflows/node.js.yml vendored Normal file
View file

@ -0,0 +1,37 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js Build
on:
push:
branches: [ "typescript" ]
pull_request:
branches: [ "typescript" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
# Don't run updateCheck for now
#- run: npm run dev:updateCheck
- run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: 'mc-beta-server'
path: 'build/index.min.js'

114
.gitignore vendored
View file

@ -1,109 +1,7 @@
# Custom stuff
old/
#===- Default node gitignore -===#
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
build/
bundle/
world/
logs/
combined.ts
config.json

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 holly
Copyright (c) 2024 Holly Stubbs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,28 +1,77 @@
# mc-beta-server [![CodeFactor](https://www.codefactor.io/repository/github/tgpethan/mc-beta-server/badge)](https://www.codefactor.io/repository/github/tgpethan/mc-beta-server)
# mc-beta-server [![CodeFactor](https://www.codefactor.io/repository/github/tgpholly/mc-beta-server/badge/typescript)](https://www.codefactor.io/repository/github/tgpholly/mc-beta-server/overview/typescript) [![Node.js CI](https://github.com/tgpholly/mc-beta-server/actions/workflows/node.js.yml/badge.svg?branch=typescript)](https://github.com/tgpholly/mc-beta-server/actions/workflows/node.js.yml)
me trying to understand minecraft beta's protocol
- [Running the server](#running-the-server)
- ~~[Running from a release](#running-from-a-release)~~
- [Building yourself](#building-yourself)
- [Setting up for development](#setting-up-for-development)
<hr>
<img src="https://eusv.net/images/mc-beta-server-readme-1-overworld.webp" alt="Minecraft landscape with trees, caves, flowers, shrubs and an ocean.">
<img src="https://eusv.net/images/mc-beta-server-readme-1-nether.webp" alt="WIP Minecraft Nether with very simple terrain.">
**Goals:**
- Make this as fast as possible on a single thread.
- Only use thread pools if absolutely neccesary.
- Build the server in a way where it should be easy to multithread it.
**Implemented:**
- Basic flat terrain generation
- Terrain sending
- Block placement
- Basic (top to bottom, no spread) Lighting
- "Decent" terrain generation using OpenSimplex Noise
- Multi-World support
- Chunk management (loading / unloading)
- Cross chunk structure generation (trees, buildings, etc...)
- World/Chunk saving to disk
- Block breaking
- Entities:
- Players
- Player Inventory
**WIP:**
- Terrain generation using perlin noise
- Cross chunk structure generation (trees, buildings, etc...)
- Entities:
- Players
- Dimensions:
- Nether
- Inventories (containers, etc...)
**To Implement:**
- Block placement
- Entities:
- Items/Blocks
- Animals
- Items
- Mobs
- Inventories
- Terrain saving to disk
- Saving inventories
- Sleeping in beds
- Tile entities
- Probably a bunch more things that i'm forgetting
**Long Term:**
- Optimise the shit out of this
<hr>
## Running the server
**Heads Up!**
This server is under pretty heavy development with most likley breaking changes, I will try my best to keep compatibility in the save formats (or provide migration paths) with updates but cannot guarentee it.
To use the server either grab a [release](https://github.com/tgpholly/mc-beta-server/releases/latest) or build it yourself.
### Running from a release
~~When using a release all you need to do is run the executable you get from the download.~~
### Building yourself
To build the server yourself clone the repo and run
```
npm i
npm run build
```
This will automatically build the server and pack it into a single js file, you'll find your resulting js file in the `./build` folder.
## Setting up for development
To run the server simply clone the repo, and run:
```
npm i
```
Copy the [`config.example.json`](https://github.com/tgpholly/mc-beta-server/blob/typescript/config.example.json) to `config.json` and edit how you want it, then run:
```
npm run dev:run
```
You are now running a server locally!
When running the server with `npm run dev:run` it is running with nodemon, this means that the server will auto restart as you edit.

6
config.example.json Normal file
View file

@ -0,0 +1,6 @@
{
"port": 25565,
"maxPlayers": 20,
"saveCompression": "DEFLATE",
"worldName": "world"
}

View file

@ -1,4 +0,0 @@
{
"port": 25565,
"worldThreads": 4
}

9
config.ts Normal file
View file

@ -0,0 +1,9 @@
import SaveCompressionType from "./server/enums/SaveCompressionType";
export default interface Config {
port: number,
maxPlayers: number,
seed: number|string,
worldName: string,
saveCompression: SaveCompressionType
}

195
external/OpenSimplex2D.ts vendored Normal file
View file

@ -0,0 +1,195 @@
// This is free and unencumbered software released into the public domain
import shuffleSeed from "./shuffle_seed";
const NORM_2D = 1.0 / 47.0;
const SQUISH_2D = (Math.sqrt(2 + 1) - 1) / 2;
const STRETCH_2D = (1 / Math.sqrt(2 + 1) - 1) / 2;
export type Noise2D = (x: number, y: number) => number;
interface Contribution2D {
dx: number;
dy: number;
next?: Contribution2D;
xsb: number;
ysb: number;
}
function contribution2D(
multiplier: number,
xsb: number,
ysb: number,
): Contribution2D {
return {
dx: -xsb - multiplier * SQUISH_2D,
dy: -ysb - multiplier * SQUISH_2D,
xsb,
ysb,
};
}
export function makeNoise2D(clientSeed: number): Noise2D {
const contributions: Contribution2D[] = [];
for (let i = 0; i < p2D.length; i += 4) {
const baseSet = base2D[p2D[i]];
let previous: Contribution2D | null = null;
let current: Contribution2D | null = null;
for (let k = 0; k < baseSet.length; k += 3) {
current = contribution2D(baseSet[k], baseSet[k + 1], baseSet[k + 2]);
if (previous === null) contributions[i / 4] = current;
else previous.next = current;
previous = current;
}
current!.next = contribution2D(p2D[i + 1], p2D[i + 2], p2D[i + 3]);
}
const lookup: Contribution2D[] = [];
for (let i = 0; i < lookupPairs2D.length; i += 2) {
lookup[lookupPairs2D[i]] = contributions[lookupPairs2D[i + 1]];
}
const perm = new Uint8Array(256);
const perm2D = new Uint8Array(256);
const source = new Uint8Array(256);
for (let i = 0; i < 256; i++) source[i] = i;
let seed = new Uint32Array(1);
seed[0] = clientSeed;
seed = shuffleSeed(shuffleSeed(shuffleSeed(seed)));
for (let i = 255; i >= 0; i--) {
seed = shuffleSeed(seed);
const r = new Uint32Array(1);
r[0] = (seed[0] + 31) % (i + 1);
if (r[0] < 0) r[0] += i + 1;
perm[i] = source[r[0]];
perm2D[i] = perm[i] & 0x0e;
source[r[0]] = source[i];
}
return (x: number, y: number): number => {
const stretchOffset = (x + y) * STRETCH_2D;
const xs = x + stretchOffset;
const ys = y + stretchOffset;
const xsb = Math.floor(xs);
const ysb = Math.floor(ys);
const squishOffset = (xsb + ysb) * SQUISH_2D;
const dx0 = x - (xsb + squishOffset);
const dy0 = y - (ysb + squishOffset);
const xins = xs - xsb;
const yins = ys - ysb;
const inSum = xins + yins;
const hash = (xins - yins + 1) |
(inSum << 1) |
((inSum + yins) << 2) |
((inSum + xins) << 4);
let value = 0;
for (
let c: Contribution2D | undefined = lookup[hash];
c !== undefined;
c = c.next
) {
const dx = dx0 + c.dx;
const dy = dy0 + c.dy;
const attn = 2 - dx * dx - dy * dy;
if (attn > 0) {
const px = xsb + c.xsb;
const py = ysb + c.ysb;
const indexPartA = perm[px & 0xff];
const index = perm2D[(indexPartA + py) & 0xff];
const valuePart = gradients2D[index] * dx + gradients2D[index + 1] * dy;
value += attn * attn * attn * attn * valuePart;
}
}
return value * NORM_2D;
};
}
const base2D = [
[1, 1, 0, 1, 0, 1, 0, 0, 0],
[1, 1, 0, 1, 0, 1, 2, 1, 1],
];
const gradients2D = [
5,
2,
2,
5,
-5,
2,
-2,
5,
5,
-2,
2,
-5,
-5,
-2,
-2,
-5,
];
const lookupPairs2D = [
0,
1,
1,
0,
4,
1,
17,
0,
20,
2,
21,
2,
22,
5,
23,
5,
26,
4,
39,
3,
42,
4,
43,
3,
];
const p2D = [
0,
0,
1,
-1,
0,
0,
-1,
1,
0,
2,
1,
1,
1,
2,
2,
0,
1,
2,
0,
2,
1,
0,
0,
0,
];

596
external/OpenSimplex3D.ts vendored Normal file
View file

@ -0,0 +1,596 @@
// This is free and unencumbered software released into the public domain
import shuffleSeed from "./shuffle_seed";
const NORM_3D = 1.0 / 103.0;
const SQUISH_3D = (Math.sqrt(3 + 1) - 1) / 3;
const STRETCH_3D = (1 / Math.sqrt(3 + 1) - 1) / 3;
export type Noise3D = (x: number, y: number, z: number) => number;
interface Contribution3D {
dx: number;
dy: number;
dz: number;
next?: Contribution3D;
xsb: number;
ysb: number;
zsb: number;
}
function contribution3D(
multiplier: number,
xsb: number,
ysb: number,
zsb: number,
): Contribution3D {
return {
dx: -xsb - multiplier * SQUISH_3D,
dy: -ysb - multiplier * SQUISH_3D,
dz: -zsb - multiplier * SQUISH_3D,
xsb,
ysb,
zsb,
};
}
export function makeNoise3D(clientSeed: number): Noise3D {
const contributions: Contribution3D[] = [];
for (let i = 0; i < p3D.length; i += 9) {
const baseSet = base3D[p3D[i]];
let previous: Contribution3D | null = null;
let current: Contribution3D | null = null;
for (let k = 0; k < baseSet.length; k += 4) {
current = contribution3D(
baseSet[k],
baseSet[k + 1],
baseSet[k + 2],
baseSet[k + 3],
);
if (previous === null) contributions[i / 9] = current;
else previous.next = current;
previous = current;
}
current!.next = contribution3D(
p3D[i + 1],
p3D[i + 2],
p3D[i + 3],
p3D[i + 4],
);
current!.next.next = contribution3D(
p3D[i + 5],
p3D[i + 6],
p3D[i + 7],
p3D[i + 8],
);
}
const lookup: Contribution3D[] = [];
for (let i = 0; i < lookupPairs3D.length; i += 2) {
lookup[lookupPairs3D[i]] = contributions[lookupPairs3D[i + 1]];
}
const perm = new Uint8Array(256);
const perm3D = new Uint8Array(256);
const source = new Uint8Array(256);
for (let i = 0; i < 256; i++) source[i] = i;
let seed = new Uint32Array(1);
seed[0] = clientSeed;
seed = shuffleSeed(shuffleSeed(shuffleSeed(seed)));
for (let i = 255; i >= 0; i--) {
seed = shuffleSeed(seed);
const r = new Uint32Array(1);
r[0] = (seed[0] + 31) % (i + 1);
if (r[0] < 0) r[0] += i + 1;
perm[i] = source[r[0]];
perm3D[i] = (perm[i] % 24) * 3;
source[r[0]] = source[i];
}
return (x: number, y: number, z: number): number => {
const stretchOffset = (x + y + z) * STRETCH_3D;
const xs = x + stretchOffset;
const ys = y + stretchOffset;
const zs = z + stretchOffset;
const xsb = Math.floor(xs);
const ysb = Math.floor(ys);
const zsb = Math.floor(zs);
const squishOffset = (xsb + ysb + zsb) * SQUISH_3D;
const dx0 = x - (xsb + squishOffset);
const dy0 = y - (ysb + squishOffset);
const dz0 = z - (zsb + squishOffset);
const xins = xs - xsb;
const yins = ys - ysb;
const zins = zs - zsb;
const inSum = xins + yins + zins;
const hash = (yins - zins + 1) |
((xins - yins + 1) << 1) |
((xins - zins + 1) << 2) |
(inSum << 3) |
((inSum + zins) << 5) |
((inSum + yins) << 7) |
((inSum + xins) << 9);
let value = 0;
for (
let c: Contribution3D | undefined = lookup[hash];
c !== undefined;
c = c.next
) {
const dx = dx0 + c.dx;
const dy = dy0 + c.dy;
const dz = dz0 + c.dz;
const attn = 2 - dx * dx - dy * dy - dz * dz;
if (attn > 0) {
const px = xsb + c.xsb;
const py = ysb + c.ysb;
const pz = zsb + c.zsb;
const indexPartA = perm[px & 0xff];
const indexPartB = perm[(indexPartA + py) & 0xff];
const index = perm3D[(indexPartB + pz) & 0xff];
const valuePart = gradients3D[index] * dx +
gradients3D[index + 1] * dy +
gradients3D[index + 2] * dz;
value += attn * attn * attn * attn * valuePart;
}
}
return value * NORM_3D;
};
}
const base3D = [
[0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1],
[2, 1, 1, 0, 2, 1, 0, 1, 2, 0, 1, 1, 3, 1, 1, 1],
[1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 2, 1, 1, 0, 2, 1, 0, 1, 2, 0, 1, 1],
];
const gradients3D = [
-11,
4,
4,
-4,
11,
4,
-4,
4,
11,
11,
4,
4,
4,
11,
4,
4,
4,
11,
-11,
-4,
4,
-4,
-11,
4,
-4,
-4,
11,
11,
-4,
4,
4,
-11,
4,
4,
-4,
11,
-11,
4,
-4,
-4,
11,
-4,
-4,
4,
-11,
11,
4,
-4,
4,
11,
-4,
4,
4,
-11,
-11,
-4,
-4,
-4,
-11,
-4,
-4,
-4,
-11,
11,
-4,
-4,
4,
-11,
-4,
4,
-4,
-11,
];
const lookupPairs3D = [
0,
2,
1,
1,
2,
2,
5,
1,
6,
0,
7,
0,
32,
2,
34,
2,
129,
1,
133,
1,
160,
5,
161,
5,
518,
0,
519,
0,
546,
4,
550,
4,
645,
3,
647,
3,
672,
5,
673,
5,
674,
4,
677,
3,
678,
4,
679,
3,
680,
13,
681,
13,
682,
12,
685,
14,
686,
12,
687,
14,
712,
20,
714,
18,
809,
21,
813,
23,
840,
20,
841,
21,
1198,
19,
1199,
22,
1226,
18,
1230,
19,
1325,
23,
1327,
22,
1352,
15,
1353,
17,
1354,
15,
1357,
17,
1358,
16,
1359,
16,
1360,
11,
1361,
10,
1362,
11,
1365,
10,
1366,
9,
1367,
9,
1392,
11,
1394,
11,
1489,
10,
1493,
10,
1520,
8,
1521,
8,
1878,
9,
1879,
9,
1906,
7,
1910,
7,
2005,
6,
2007,
6,
2032,
8,
2033,
8,
2034,
7,
2037,
6,
2038,
7,
2039,
6,
];
const p3D = [
0,
0,
1,
-1,
0,
0,
1,
0,
-1,
0,
0,
-1,
1,
0,
0,
0,
1,
-1,
0,
0,
-1,
0,
1,
0,
0,
-1,
1,
0,
2,
1,
1,
0,
1,
1,
1,
-1,
0,
2,
1,
0,
1,
1,
1,
-1,
1,
0,
2,
0,
1,
1,
1,
-1,
1,
1,
1,
3,
2,
1,
0,
3,
1,
2,
0,
1,
3,
2,
0,
1,
3,
1,
0,
2,
1,
3,
0,
2,
1,
3,
0,
1,
2,
1,
1,
1,
0,
0,
2,
2,
0,
0,
1,
1,
0,
1,
0,
2,
0,
2,
0,
1,
1,
0,
0,
1,
2,
0,
0,
2,
2,
0,
0,
0,
0,
1,
1,
-1,
1,
2,
0,
0,
0,
0,
1,
-1,
1,
1,
2,
0,
0,
0,
0,
1,
1,
1,
-1,
2,
3,
1,
1,
1,
2,
0,
0,
2,
2,
3,
1,
1,
1,
2,
2,
0,
0,
2,
3,
1,
1,
1,
2,
0,
2,
0,
2,
1,
1,
-1,
1,
2,
0,
0,
2,
2,
1,
1,
-1,
1,
2,
2,
0,
0,
2,
1,
-1,
1,
1,
2,
0,
0,
2,
2,
1,
-1,
1,
1,
2,
0,
2,
0,
2,
1,
1,
1,
-1,
2,
2,
0,
0,
2,
1,
1,
1,
-1,
2,
0,
2,
0,
];

8
external/shuffle_seed.ts vendored Normal file
View file

@ -0,0 +1,8 @@
// This is free and unencumbered software released into the public domain
export default function shuffleSeed(seed: Uint32Array): Uint32Array {
const newSeed = new Uint32Array(1);
newSeed[0] = seed[0] * 1664525 + 1013904223;
return newSeed;
}

View file

@ -1,15 +0,0 @@
/*
==============- index.js -==============
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const server = new (require('net').Server)();
const config = require("./config.json");
const mcServer = require("./server/server.js");
server.listen(config.port, () => mcServer.init(config));
server.on('connection', mcServer.connection);

13
index.ts Normal file
View file

@ -0,0 +1,13 @@
import { readFileSync } from "fs";
import { Console } from "hsconsole";
import Config from "./config";
import MinecraftServer from "./server/MinecraftServer";
import SaveCompressionType from "./server/enums/SaveCompressionType";
const tempConfig = JSON.parse(readFileSync("./config.json").toString());
tempConfig.saveCompression = SaveCompressionType[tempConfig.saveCompression];
const config:Config = tempConfig as Config;
Console.customHeader(`MC Beta Server started at ${new Date()}`);
new MinecraftServer(config);

35
nibbleArray.ts Normal file
View file

@ -0,0 +1,35 @@
export default class NibbleArray {
private array:Uint8Array;
public constructor(size:number|ArrayBuffer|Uint8Array) {
if (size instanceof ArrayBuffer) {
this.array = new Uint8Array(size);
} else if (size instanceof Uint8Array) {
this.array = new Uint8Array(size);
} else {
this.array = new Uint8Array(size >> 1);
}
}
public get(index:number) {
const arrayIndex = index >> 1;
if ((index & 1) === 0) {
return this.array[arrayIndex] & 0xf;
} else {
return this.array[arrayIndex] >> 4 & 0xf;
}
}
public set(index:number, value:number) {
const arrayIndex = index >> 1;
if ((index & 1) === 0) {
this.array[arrayIndex] = this.array[arrayIndex] & 0xf0 | value & 0xf;
} else {
this.array[arrayIndex] = this.array[arrayIndex] & 0xf | (value & 0xf) << 4;
}
}
public toBuffer() {
return Buffer.from(this.array);
}
}

2777
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,20 +2,41 @@
"name": "mc-beta-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"main": "index.ts",
"scripts": {
"dev:updateCheck": "check-outdated",
"dev:run": "nodemon --watch './**/*.ts' index.ts",
"build": "npm-run-all build:*",
"build:build": "ncc build index.ts -o build",
"build:mangle": "ts-node ./tooling/mangle.ts",
"build:cleanup": "ts-node ./tooling/cleanup.ts",
"_clean": "tsc --build --clean"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tgpethan/mc-beta-server.git"
"url": "git+https://github.com/tgpholly/mc-beta-server.git"
},
"keywords": [],
"author": "",
"author": "tgpholly",
"license": "MIT",
"bugs": {
"url": "https://github.com/tgpethan/mc-beta-server/issues"
"url": "https://github.com/tgpholly/mc-beta-server/issues"
},
"homepage": "https://github.com/tgpethan/mc-beta-server#readme",
"homepage": "https://github.com/tgpholly/mc-beta-server#readme",
"dependencies": {
"net": "^1.0.2"
"bufferstuff": "^1.4.2",
"funky-array": "^1.0.0",
"hsconsole": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.8.10",
"@vercel/ncc": "^0.38.1",
"check-outdated": "^2.12.0",
"nodemon": "^3.0.1",
"npm-run-all": "^4.1.5",
"terser": "^5.24.0",
"ts-loader": "^9.5.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}

177
server/AABB.ts Normal file
View file

@ -0,0 +1,177 @@
import FunkyArray from "funky-array";
import Vec3 from "./Vec3";
// Based on this MDN article:
// https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection
export default class AABB {
private static readonly aabbPool:FunkyArray<string, AABB> = new FunkyArray<string, AABB>();
public readonly aabbPoolString:string;
public readonly pooled:boolean;
public initMin:Vec3;
public initMax:Vec3;
public prevMin:Vec3;
public prevMax:Vec3;
public min:Vec3;
public max:Vec3;
public constructor(minXOrMin:Vec3 | number, minYOrMax:Vec3 | number, minZ?:number, maxX?:number, maxY?:number, maxZ?:number, pooled:boolean = false) {
if (minXOrMin instanceof Vec3 && minYOrMax instanceof Vec3) {
this.min = minXOrMin;
this.max = minYOrMax;
} else if (typeof(minXOrMin) === "number" && typeof(minYOrMax) === "number" && typeof(minZ) === "number" && typeof(maxX) === "number" && typeof(maxY) === "number" && typeof(maxZ) === "number") {
this.min = new Vec3(minXOrMin, minYOrMax, minZ);
this.max = new Vec3(maxX, maxY, maxZ);
} else {
throw new Error("Invalid input parameters: AABB must be supplied with either two Vec3 with the min and max bounds or the raw bounds.");
}
this.initMin = new Vec3(this.min);
this.initMax = new Vec3(this.max);
this.prevMin = new Vec3(this.min);
this.prevMax = new Vec3(this.min);
this.pooled = pooled;
this.aabbPoolString = AABB.createAABBPoolString(this.min.x, this.min.y, this.min.z, this.max.x, this.max.y, this.max.z);
if (!AABB.aabbPool.has(this.aabbPoolString)) {
AABB.aabbPool.set(this.aabbPoolString, this);
}
}
public static createAABBPoolString(minX:number, minY:number, minZ:number, maxX:number, maxY:number, maxZ:number) {
return `m${minX}c${minY}a${minZ}a${maxX}b${maxY}b${maxZ}`;
}
public static getAABB(minX:number, minY:number, minZ:number, maxX:number, maxY:number, maxZ:number) {
const aabbPoolString = this.createAABBPoolString(minX, minY, minZ, maxX, maxY, maxZ);
if (AABB.aabbPool.has(aabbPoolString)) {
const aabb = AABB.aabbPool.get(aabbPoolString);
if (aabb === undefined) {
throw new Error(`Pooled AABB was ${typeof(aabb)}! This should be impossible.`);
}
return aabb;
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
public static intersects(a:AABB, b:AABB) {
return a.min.x <= b.max.x && a.max.x >= b.min.x && a.min.y <= b.max.y && a.max.y >= b.min.y && a.min.z <= b.max.z && a.max.z >= b.min.z;
}
public intersects(aabb:AABB) {
return this.min.x <= aabb.max.x && this.max.x >= aabb.min.x && this.min.y <= aabb.max.y && this.max.y >= aabb.min.y && this.min.z <= aabb.max.z && this.max.z >= aabb.min.z;
}
public static intersectionY(a: AABB, b: AABB) {
const minY = Math.max(a.min.y, b.min.y);
const maxY = Math.min(a.max.y, b.max.y);
return minY <= maxY ? maxY - minY : 0;
}
public intersectionY(aabb: AABB) {
const minY = Math.max(this.min.y, aabb.min.y);
const maxY = Math.min(this.max.y, aabb.max.y);
return minY <= maxY ? maxY - minY : 0;
}
public static intersectionX(a: AABB, b: AABB) {
const minX = Math.max(a.min.x, b.min.x);
const maxX = Math.min(a.max.x, b.max.x);
return minX <= maxX ? maxX - minX : 0;
}
public intersectionX(aabb: AABB) {
const minX = Math.max(this.min.x, aabb.min.x);
const maxX = Math.min(this.max.x, aabb.max.x);
return minX <= maxX ? maxX - minX : 0;
}
public static intersectionZ(a: AABB, b: AABB) {
const minZ = Math.max(a.min.z, b.min.z);
const maxZ = Math.min(a.max.z, b.max.z);
return minZ <= maxZ ? maxZ - minZ : 0;
}
public intersectionZ(aabb: AABB) {
const minZ = Math.max(this.min.z, aabb.min.z);
const maxZ = Math.min(this.max.z, aabb.max.z);
return minZ <= maxZ ? maxZ - minZ : 0;
}
public static copy(aabb:AABB) {
const newAABB = new AABB(new Vec3(aabb.min), new Vec3(aabb.max));
newAABB.min.set(aabb.min);
newAABB.max.set(aabb.max);
return newAABB;
}
public copy() {
const newAABB = new AABB(new Vec3(this.min), new Vec3(this.max));
newAABB.min.set(this.min);
newAABB.max.set(this.max);
return newAABB;
}
// Can only revert one step
public revert() {
this.min.set(this.prevMin);
this.max.set(this.prevMax);
}
public set(aabb:AABB) {
this.prevMin.set(this.min);
this.prevMax.set(this.max);
this.min.set(aabb.min);
this.max.set(aabb.min);
}
public move(xOrVec3:Vec3 | number, y?:number, z?:number) {
if (this.pooled) {
throw new Error(`Attempted to move a pooled AABB. This is not allowed!`);
}
this.prevMin.set(this.min);
this.prevMax.set(this.max);
this.min.set(this.initMin);
this.max.set(this.initMax);
if (xOrVec3 instanceof Vec3) {
//this.pos.set(xOrVec3);
this.min.add(xOrVec3);
this.max.add(xOrVec3);
} else if (typeof(xOrVec3) === "number" && typeof(y) === "number" && typeof(z) === "number") {
//this.pos.set(xOrVec3, y, z);
this.min.add(xOrVec3, y, z);
this.max.add(xOrVec3, y, z);
}
}
public offset(xOrVec3:Vec3 | number, y?:number, z?:number) {
if (this.pooled) {
throw new Error(`Attempted to offset a pooled AABB. This is not allowed!`);
}
this.prevMin.set(this.min);
this.prevMax.set(this.max);
if (xOrVec3 instanceof Vec3) {
this.min.add(xOrVec3);
this.max.add(xOrVec3);
} else if (typeof(xOrVec3) === "number" && typeof(y) === "number" && typeof(z) === "number") {
this.min.add(xOrVec3, y, z);
this.max.add(xOrVec3, y, z);
}
}
}

View file

@ -1,86 +0,0 @@
class Block {
static blocksList = [];
static blockOpaqueList = [];
static blockOpacity = [];
static blockGrassable = [];
static blockContainer = [];
constructor(blockID = 0) {
this.blockID = blockID;
this.blockHardness = 0;
this.blockResistance = 0;
this.blockName = "";
if (Block.blocksList[blockID] != null) {
throw `[Block] Slot ${blockID} is already occupied by ${Block.blocksList[blockID]} when adding ID:${blockID}`;
} else {
Block.blocksList[blockID] = this;
Block.blockOpaqueList[blockID] = false; // TODO: Block opaque
Block.blockOpacity[blockID] = 0; // TODO: Opacity
Block.blockGrassable[blockID] = false; // TODO: Get if block can be grass'd
Block.blockContainer[blockID] = false; // TODO: Containers
}
}
setHardness(hardness = 0) {
this.blockHardness = hardness;
return this;
}
setBlockUnbreakable() {
this.blockHardness = -1;
return this;
}
setResistance(resistance = 0) {
this.blockHardness = resistance;
return this;
}
setName(name = "") {
this.blockName = name;
return this;
}
static stone = new Block(1).setHardness(1.5).setResistance(10).setName("Stone");
static grass = new Block(2).setHardness(0.6).setName("Grass");
static dirt = new Block(3).setHardness(0.5).setName("Dirt");
static cobblestone = new Block(4).setHardness(2.0).setResistance(10).setName("Cobblestone");
static planks = new Block(5).setHardness(2).setResistance(5).setName("Planks");
static sapling = new Block(6).setName("Sapling");
static bedrock = new Block(7).setBlockUnbreakable().setResistance(6000000).setName("Bedrock");
static waterFlowing = new Block(8).setHardness(100).setName("Flowing Water");
static waterStill = new Block(9).setHardness(100).setName("Still Water");
static lavaMoving = new Block(10).setHardness(1.5).setResistance(10).setName("Flowing Lava");
static lavaStill = new Block(11).setHardness(1.5).setResistance(10).setName("Still Lava");
static sand = new Block(12).setHardness(1.5).setResistance(10).setName("Sand");
static gravel = new Block(13).setHardness(1.5).setResistance(10).setName("Gravel");
static goldOre = new Block(14).setHardness(1.5).setResistance(10).setName("Gold Ore");
static ironOre = new Block(15).setHardness(1.5).setResistance(10).setName("Iron Ore");
static coalOre = new Block(16).setHardness(1.5).setResistance(10).setName("Coal Ore");
static wood = new Block(17).setHardness(1.5).setResistance(10).setName("Wood");
static leaves = new Block(18).setHardness(1.5).setResistance(10).setName("Leaves");
static sponge = new Block(19).setHardness(1.5).setResistance(10).setName("Sponge");
static glass = new Block(20).setHardness(1.5).setResistance(10).setName("Glass");
static lapisOre = new Block(21).setHardness(1.5).setResistance(10).setName("Lapis Ore");
static lapisBlock = new Block(22).setHardness(1.5).setResistance(10).setName("Lapis Block");
static dispenser = new Block(23).setHardness(1.5).setResistance(10).setName("Dispenser");
static sandStone = new Block(24).setHardness(1.5).setResistance(10).setName("Sandstone");
static noteBlock = new Block(25).setHardness(1.5).setResistance(10).setName("Noteblock");
static blockBed = new Block(26).setHardness(1.5).setResistance(10).setName("Bed");
static poweredRail = new Block(27).setHardness(1.5).setResistance(10).setName("Powered Rail");
static detectorRail = new Block(28).setHardness(1.5).setResistance(10).setName("Detector Rail");
static stickyPisonBase = new Block(29).setHardness(1.5).setResistance(10).setName("Sticky Piston Base");
static cobweb = new Block(30).setHardness(4).setName("Cobweb");
static tallGrass = new Block(31).setName("Tall Grass");
static deadBush = new Block(32).setName("Dead Bush");
static pistonBase = new Block(33).setName("Piston Base");
static pistonExtension = new Block(34).setName("Piston Extension");
static wool = new Block(35).setHardness(0.8).setName("Wool");
static pistonMoving = new Block(36).setName("Piston Moving")
static dandilion = new Block(37).setName("Dandilion");
static rose = new Block(38).setName("Rose");
}
module.exports = Block;

190
server/Chunk.ts Normal file
View file

@ -0,0 +1,190 @@
import Block from "./blocks/Block";
import FunkyArray from "funky-array";
import NibbleArray from "../nibbleArray";
import Player from "./entities/Player";
import QueuedBlockUpdate from "./queuedUpdateTypes/BlockUpdate";
import World from "./World";
export default class Chunk {
private readonly MAX_HEIGHT:number = 128;
private readonly FULLBRIGHT = false;
public readonly world:World;
public readonly x:number;
public readonly z:number;
public readonly playersInChunk:FunkyArray<number, Player>;
public savingToDisk:boolean = false;
public forceLoaded:boolean = false;
private blocks:Uint8Array;
private metadata:NibbleArray;
public skyLight:NibbleArray;
public blockLight:NibbleArray;
public static CreateCoordPair(x:number, z:number) {
return (x >= 0 ? 0 : 2147483648) | (x & 0x7fff) << 16 | (z >= 0 ? 0 : 0x8000) | z & 0x7fff;
}
public constructor(world:World, x:number, z:number, generateOrBlockData?:boolean|Uint8Array, metadata?:Uint8Array, blockLight?:Uint8Array, skyLight?:Uint8Array) {
this.world = world;
this.x = x;
this.z = z;
this.playersInChunk = new FunkyArray<number, Player>();
if (generateOrBlockData instanceof Uint8Array && metadata instanceof Uint8Array && blockLight instanceof Uint8Array && skyLight instanceof Uint8Array) {
this.blocks = new Uint8Array(generateOrBlockData);
this.metadata = new NibbleArray(metadata);
this.skyLight = new NibbleArray(blockLight);
this.blockLight = new NibbleArray(skyLight);
} else if (generateOrBlockData instanceof Uint8Array && metadata instanceof Uint8Array && !(blockLight instanceof Uint8Array) && !(skyLight instanceof Uint8Array)) {
this.blocks = new Uint8Array(generateOrBlockData);
this.metadata = new NibbleArray(metadata);
this.skyLight = new NibbleArray(16 * 16 * this.MAX_HEIGHT);
this.blockLight = new NibbleArray(16 * 16 * this.MAX_HEIGHT);
this.calculateLighting();
} else {
this.blocks = new Uint8Array(16 * 16 * this.MAX_HEIGHT);
this.metadata = new NibbleArray(16 * 16 * this.MAX_HEIGHT);
this.skyLight = new NibbleArray(16 * 16 * this.MAX_HEIGHT);
this.blockLight = new NibbleArray(16 * 16 * this.MAX_HEIGHT);
if (typeof(generateOrBlockData) === "boolean" && generateOrBlockData) {
this.world.generator.generate(this);
this.calculateLighting();
}
}
}
public getTopBlockY(x:number, z:number) {
let castY = this.MAX_HEIGHT;
while (castY-- > 0) {
if (this.getBlockId(x >>> 0, castY, z >>> 0) !== 0) {
break;
}
}
return castY;
}
public calculateLighting() {
if (this.FULLBRIGHT) {
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
for (let y = this.MAX_HEIGHT - 1; y > 0; y--) {
this.setBlockLight(15, x, y, z);
this.setSkyLight(15, x, y, z);
}
}
}
return;
}
let blockId = 0;
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
let colLight = 255;
for (let y = this.MAX_HEIGHT - 1; y > 0; y--) {
blockId = this.getBlockId(x, y, z);
if (blockId == 0) {
if (colLight <= 0) {
this.setBlockLight(0, x, y, z);
this.setSkyLight(0, x, y, z);
} else {
this.setBlockLight(Math.round((colLight / 255) * 15), x, y, z);
this.setSkyLight(Math.round((colLight / 255) * 15), x, y, z);
}
} else {
if (colLight <= 0) {
this.setBlockLight(0, x, y, z);
} else {
this.setBlockLight(Math.round((colLight / 255) * 15), x, y, z);
colLight -= (255 - Block.blocks[blockId].lightPassage);
}
}
}
}
}
}
public queueBlockUpdateForOuterChunkBlock(blockId:number, metadata:number, x:number, y:number, z:number) {
const cPair = Chunk.CreateCoordPair(this.x + (x >> 4), this.z + (z >> 4));
if (this.world.chunks.keys.includes(cPair)) {
this.world.queuedUpdates.push(new QueuedBlockUpdate(cPair, x & 0xf, y, z & 0xf, blockId, metadata));
} else {
this.world.queuedChunkBlocks.push(new QueuedBlockUpdate(cPair, x & 0xf, y, z & 0xf, blockId, metadata));
}
}
public setBlock(blockId:number, x:number, y:number, z:number) {
if (x < 0 || x > 15 || y < 0 || y > 127 || z < 0 || z > 15) {
this.queueBlockUpdateForOuterChunkBlock(blockId, 0, x, y, z);
return;
}
this.blocks[x << 11 | z << 7 | y] = blockId;
}
public setBlockMetadata(metadata:number, x:number, y:number, z:number) {
const index = x << 11 | z << 7 | y;
if (x < 0 || x > 15 || y < 0 || y > 127 || z < 0 || z > 15) {
this.queueBlockUpdateForOuterChunkBlock(this.blocks[index], metadata, x, y, z);
return;
}
this.metadata.set(index, metadata);
}
public setBlockWithMetadata(blockId:number, metadata:number, x:number, y:number, z:number) {
if (x < 0 || x > 15 || y < 0 || y > 127 || z < 0 || z > 15) {
this.queueBlockUpdateForOuterChunkBlock(blockId, metadata, x, y, z);
return;
}
x = x << 11 | z << 7 | y;
this.blocks[x] = blockId;
this.metadata.set(x, metadata);
}
public getBlockId(x:number, y:number, z:number) {
return this.blocks[x << 11 | z << 7 | y];
}
public getBlockMetadata(x:number, y:number, z:number) {
return this.metadata.get(x << 11 | z << 7 | y);
}
public getBlockLight(x:number, y:number, z:number) {
return this.blockLight.get(x << 11 | z << 7 | y);
}
public setBlockLight(value:number, x:number, y:number, z:number) {
return this.blockLight.set(x << 11 | z << 7 | y, value);
}
public getSkyLight(x:number, y:number, z:number) {
return this.skyLight.get(x << 11 | z << 7 | y);
}
public setSkyLight(value:number, x:number, y:number, z:number) {
return this.skyLight.set(x << 11 | z << 7 | y, value);
}
public getBlockBuffer() {
return Buffer.from(this.blocks);
}
public getMetadataBuffer() {
return this.metadata.toBuffer();
}
public getBlockLightBuffer() {
return this.blockLight.toBuffer();
}
public getSkyLightBuffer() {
return this.skyLight.toBuffer();
}
public getBlockData() {
return this.blocks;
}
}

View file

@ -1,30 +0,0 @@
/*
============- Converter.js -============
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
module.exports.toAbsoluteInt = function(float = 0.0) {
return Math.floor(float * 32.0);
}
module.exports.to360Fraction = function(float = 0.0) {
if (float < 0) {
return Math.abs(Math.max(Math.floor((map(float, 0, -360, 360, 0) / 360) * 256) - 1, 0));
} else if (float > 0) {
return Math.max(Math.floor((float / 360) * 256) - 1, 0);
}
//return Math.max(Math.floor((float / 360) * 256), 0) - 1;
}
function map(input, inputMin, inputMax, outputMin, outputMax) {
const newv = (input - inputMin) / (inputMax - inputMin) * (outputMax - outputMin) + outputMin;
if (outputMin < outputMax) return constrain(newv, outputMin, outputMax);
else return constrain(newv, outputMax, outputMin);
}
function constrain(input, low, high) {
return Math.max(Math.min(input, high), low);
}

View file

@ -1,28 +0,0 @@
/*
==============- Entity.js -=============
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
class Entity {
constructor(EID = 0, x = 0, y = 0, z = 0, yaw = 0, pitch = 0) {
this.EID = EID;
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
this.motionX = 0;
this.motionY = 0;
this.motionZ = 0;
}
onTick() {
}
}
module.exports = Entity;

View file

@ -1,20 +0,0 @@
/*
============- EntityItem.js -===========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Entity = require("./Entity.js");
class EntityItem extends Entity {
constructor(itemStack, x = 0, y = 0, z = 0) {
super(global.fromIDPool(), x, y, z);
this.itemStack = itemStack;
this.motionX = (Math.random() * 0.2 - 0.1);
this.motionY = 0.2;
this.motionZ = (Math.random() * 0.2 - 0.1);
}
}

View file

@ -1,22 +0,0 @@
/*
==========- EntityLiving.js -===========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Entity = require("./Entity.js");
class EntityLiving extends Entity {
constructor(EID = 0, x = 0, y = 0, z = 0) {
super(EID, x, y, z);
}
onTick() {
super.onTick();
}
}
module.exports = EntityLiving;

View file

@ -1,73 +0,0 @@
/*
===========- EntityPlayer.js -==========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const EntityLiving = require("./EntityLiving.js");
const user = require("../user.js");
const Converter = require("../Converter.js");
const PacketMappingTable = require("../PacketMappingTable.js");
const NamedPackets = require("../NamedPackets.js");
class EntityPlayer extends EntityLiving {
constructor(parent = new user, x = 0, y = 0, z = 0) {
super(parent.id, x, y, z);
this.lastX = 0;
this.lastY = 0;
this.lastZ = 0;
this.absX = 0;
this.absY = 0;
this.absZ = 0;
this.absYaw = 0;
this.absPitch = 0;
this.lastYaw = 0;
this.lastPitch = 0;
this.allPacket = new PacketMappingTable[NamedPackets.EntityTeleport](this.EID, this.x, this.y, this.z, this.absYaw, this.absPitch);
this.allPacket.soup();
this.allPacket.writePacket();
this.parentPlayer = parent;
}
onTick() {
super.onTick();
this.absX = this.x.toFixed(2);
this.absY = this.y.toFixed(2);
this.absZ = this.z.toFixed(2);
this.absYaw = Math.floor(this.yaw);
this.absPitch = Math.floor(this.pitch);
if ((this.absX != this.lastX || this.absY != this.lastY || this.absZ != this.lastZ)) {
// all
this.allPacket.writer.offset = 5;
this.allPacket.writer.writeInt(Converter.toAbsoluteInt(this.x));
this.allPacket.writer.writeInt(Converter.toAbsoluteInt(this.y));
this.allPacket.writer.writeInt(Converter.toAbsoluteInt(this.z));
this.allPacket.writer.writeUByte(Converter.to360Fraction(this.absYaw));
this.allPacket.writer.writeUByte(Converter.to360Fraction(this.absPitch));
global.sendToAllPlayersButSelf(this.EID, this.allPacket.toBuffer());
} else if (this.absYaw != this.lastYaw || this.absPitch != this.lastPitch) {
// look only
global.sendToAllPlayersButSelf(this.EID, new PacketMappingTable[NamedPackets.EntityLook](this.EID, this.absYaw, this.absPitch).writePacket());
}
this.lastYaw = this.absYaw;
this.lastPitch = this.absPitch;
this.lastX = this.absX;
this.lastY = this.absY;
this.lastZ = this.absZ;
}
}
module.exports = EntityPlayer;

26
server/EntityMetadata.ts Normal file
View file

@ -0,0 +1,26 @@
import { MetadataEntry, MetadataWriter } from "./MetadataWriter";
import MetadataFieldType from "./enums/MetadataFieldType";
export default class EntityMetadata {
public onFire:boolean = false;
public crouched:boolean = false;
public ridingEntity:boolean = false;
private finalValue:number = 0;
private static readonly ENTITY_ON_FIRE = 1 << 0;
private static readonly ENTITY_CROUCHING = 1 << 1;
private static readonly ENTITY_RIDING = 1 << 2;
writeMetadata() {
const metadataWriter = new MetadataWriter();
this.finalValue =
(this.onFire ? EntityMetadata.ENTITY_ON_FIRE : 0) | // On Fire
(this.crouched ? EntityMetadata.ENTITY_CROUCHING : 0) | // Crouching
(this.ridingEntity ? EntityMetadata.ENTITY_CROUCHING : 0); // Riding entity
metadataWriter.addMetadataEntry(0, new MetadataEntry(MetadataFieldType.Byte, this.finalValue));
return metadataWriter.writeBuffer();
}
}

View file

@ -1,31 +0,0 @@
const Block = require("../Blocks/Block.js");
module.exports = function(x = 0, z = 0) {
// Create chunk
let chunk = {};
for (let y = 0; y < 128; y++) {
chunk[y] = {};
for (let x = 0; x < 16; x++) {
chunk[y][x] = {};
for (let z = 0; z < 16; z++) {
chunk[y][x][z] = [0, 0];
}
}
}
for (let y = 0; y < 128; y++) {
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
if (y == 64) {
chunk[y][x][z] = 2;
}
else if (y == 63 || y == 62) chunk[y][x][z][0] = Block.dirt.blockID;
else if (y == 0) chunk[y][x][z][0] = Block.bedrock.blockID;
else if (y < 62) chunk[y][x][z][0] = Block.stone.blockID;
else chunk[y][x][z][0] = 0;
}
}
}
return chunk;
}

View file

@ -1,187 +0,0 @@
const { perlin2D } = require("./perlin.js");
const Block = require("../Blocks/Block.js");
module.exports = function(cx = 0, cz = 0, seed = 0) {
// Create bare chunk
let chunk = {};
for (let y = 0; y < 128; y++) {
chunk[y] = {};
for (let x = 0; x < 16; x++) {
chunk[y][x] = {};
for (let z = 0; z < 16; z++) {
chunk[y][x][z] = [0, 0];
}
}
}
let stripTopCoord = {};
// History has shown it's better to alloc all at once
for (let x = 0; x < 16; x++) {
stripTopCoord[x] = {};
for (let z = 0; z < 16; z++) {
stripTopCoord[x][z] = 0;
}
}
// Generate top layer of grass
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
// NOTE: Because of the way this is, it is not random at all. The heightmap is simply offset so uhhh.
// TODO: Figure out a better way of dealing with this :)
const layer1 = (64 + (perlin2D(((cx << 4) + x) / 15, ((cz << 4) + z) / 15) * 10));
const layer2 = (64 + (perlin2D(((cx + (10 + seed) << 4) + x) / 15, ((cz + (4 + seed) << 4) + z) / 15) * 10));
const layer3_1 = (64 + (perlin2D(((cx + (-15 + seed) << 4) + x) / 15, ((cz + (-2 + seed) << 4) + z) / 15) * 23));
const layer3_2 = (64 + (perlin2D(((cx + (25 + seed) << 4) + x) / 15, ((cz + (-17 + seed) << 4) + z) / 15) * 40));
const layer3 = (layer3_1 + layer3_2) / 2;
const average = Math.floor((layer1 + layer2 + layer3) / 3);
stripTopCoord[x][z] = average;
chunk[average][x][z][0] = Block.grass.blockID;
}
}
// Generate down from the top layer
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
// Cache these otherwise we'll be doing more maths 128 times for each column and row
const topM1 = stripTopCoord[x][z] - 1,
topM2 = topM1 - 1;
for (let y = stripTopCoord[x][z]; y != -1; y--) {
if (y == topM1 || y == topM2) chunk[y][x][z][0] = Block.dirt.blockID;
else if (y == 0) chunk[y][x][z][0] = Block.bedrock.blockID;
else if (y < topM2) chunk[y][x][z][0] = Block.stone.blockID;
}
}
}
// 2nd pass
for (let y = 0; y < 128; y++) {
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
if (chunk[y][x][z][0] == 0 && y < 64) chunk[y][x][z][0] = Block.waterStill.blockID;
if (y < 127 && y > 0) if (chunk[y][x][z][0] == Block.waterStill.blockID && chunk[y - 1][x][z][0] == 2) chunk[y - 1][x][z][0] = Block.dirt.blockID;
//if (x == 0 && z == 0) chunk[y][x][z] = 57;
}
}
}
let treeBlocks = [];
const chunkX = cx << 4;
const chunkZ = cz << 4;
let topBlock = 0;
// 3rd pass???
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
topBlock = stripTopCoord[x][z];
if (chunk[topBlock][x][z][0] == Block.grass.blockID) {
if (Math.floor(Math.random() * 5) == 0) {
chunk[topBlock + 1][x][z][0] = Block.tallGrass.blockID;
chunk[topBlock + 1][x][z][1] = 1;
} else if (Math.floor(Math.random() * 150) == 0) {
chunk[topBlock + 1][x][z][0] = Block.rose.blockID;
} else if (Math.floor(Math.random() * 150) == 0) {
chunk[topBlock + 1][x][z][0] = Block.dandilion.blockID;
}
}
// Need a better way of doing this it currently takes a severely long time (gee I wonder why)
if (chunk[topBlock][x][z][0] == 2 && Math.floor(Math.random() * 200) == 0) {
chunk[topBlock][x][z][0] = 3;
// Logs
treeBlocks.push([(chunkX + x), topBlock + 1, (chunkZ + z), Block.wood.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 2, (chunkZ + z), Block.wood.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 3, (chunkZ + z), Block.wood.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 4, (chunkZ + z), Block.wood.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 5, (chunkZ + z), Block.wood.blockID]);
// Leaves
// Layer 1
treeBlocks.push([(chunkX + x) - 1, topBlock + 3, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 2, topBlock + 3, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 3, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 2, topBlock + 3, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 3, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 3, (chunkZ + z) - 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 3, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 3, (chunkZ + z) + 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 3, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 3, (chunkZ + z) - 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 3, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 3, (chunkZ + z) + 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 2, topBlock + 3, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 2, topBlock + 3, (chunkZ + z) - 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 2, topBlock + 3, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 2, topBlock + 3, (chunkZ + z) + 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 1, topBlock + 3, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 1, topBlock + 3, (chunkZ + z) - 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 1, topBlock + 3, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 1, topBlock + 3, (chunkZ + z) + 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 2, topBlock + 3, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 2, topBlock + 3, (chunkZ + z) - 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 2, topBlock + 3, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 2, topBlock + 3, (chunkZ + z) + 2, Block.leaves.blockID]);
// Layer 2
treeBlocks.push([(chunkX + x) - 1, topBlock + 4, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 2, topBlock + 4, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 4, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 2, topBlock + 4, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 4, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 4, (chunkZ + z) - 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 4, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 4, (chunkZ + z) + 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 4, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 4, (chunkZ + z) - 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 4, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 4, (chunkZ + z) + 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 2, topBlock + 4, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 2, topBlock + 4, (chunkZ + z) - 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 2, topBlock + 4, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 2, topBlock + 4, (chunkZ + z) + 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 1, topBlock + 4, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 1, topBlock + 4, (chunkZ + z) - 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 1, topBlock + 4, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 1, topBlock + 4, (chunkZ + z) + 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 2, topBlock + 4, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 2, topBlock + 4, (chunkZ + z) - 2, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 2, topBlock + 4, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 2, topBlock + 4, (chunkZ + z) + 2, Block.leaves.blockID]);
// Layer 3
treeBlocks.push([(chunkX + x) - 1, topBlock + 5, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 5, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 5, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 5, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 1, topBlock + 5, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 5, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) - 1, topBlock + 5, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 5, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 5, (chunkZ + z) - 1, Block.leaves.blockID]);
// Layer 4
treeBlocks.push([(chunkX + x) - 1, topBlock + 6, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x) + 1, topBlock + 6, (chunkZ + z), Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 6, (chunkZ + z) - 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 6, (chunkZ + z) + 1, Block.leaves.blockID]);
treeBlocks.push([(chunkX + x), topBlock + 6, (chunkZ + z), Block.leaves.blockID]);
}
}
}
return [chunk, treeBlocks];
}

View file

@ -1,307 +0,0 @@
/**
* A Perlin Noise library for JavaScript.
*
* Based on implementations by Ken Perlin (Java, C) & Stefan Gustavson (C).
*
* MIT License.
* Copyright (c) 2018 Leonardo de S.L.F.
* http://leodeslf.com/
*/
// Permutation table.
const p = [
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140,
36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234,
75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237,
149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48,
27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105,
92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73,
209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86,
164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38,
147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189,
28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153,
101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224,
232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144,
12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214,
31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150,
254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66,
215, 61, 156, 180
];
const P = [...p, ...p];
// Utility functions.
function fade(t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
function lerp(t, a, b) {
return a + t * (b - a);
}
// Helper functions to find gradients for each vertex.
function gradient1D(hash, x) {
return hash & 1 ? -x : x;
}
function gradient2D(hash, x, y) {
const H = hash & 3;
return (H < 2 ? x : -x) + (H === 0 || H === 2 ? y : -y);
}
function gradient3D(hash, x, y, z) {
const
H = hash & 15,
U = H < 8 ? x : y,
V = H < 4 ? y : H === 12 || H === 14 ? x : z;
return ((H & 1) ? -U : U) + ((H & 2) ? -V : V);
}
function gradient4D(hash, x, y, z, t) {
const
H = hash & 31,
U = H < 24 ? x : y,
V = H < 16 ? y : z,
W = H < 8 ? z : t;
return ((H & 1) ? -U : U) + ((H & 2) ? -V : V) + ((H & 4) ? -W : W);
}
// Perlin Noise functions.
/**
* Returns a one-dimensional noise value between -1 and 1.
* @param {number} x A numeric expression.
* @returns {number} Perlin Noise value.
*/
function perlin1D(x) {
const
FX = Math.floor(x),
X = FX & 255;
x = x - FX;
return lerp(
fade(x),
gradient1D(P[P[X]], x),
gradient1D(P[P[X + 1]], x - 1)
);
}
/**
* Returns a two-dimensional noise value between -1 and 1.
* @param {number} x A numeric expression.
* @param {number} y A numeric expression.
* @returns {number} Perlin Noise value.
*/
function perlin2D(x = 0, y = 0) {
// Hack to allow this to work with integer block coords
x += 0.2;
y += 0.2;
const FX = Math.floor(x),
FY = Math.floor(y),
X = FX & 255,
Y = FY & 255,
A = P[X] + Y,
B = P[X + 1] + Y;
x = x - FX;
y = y - FY;
const
FDX = fade(x),
x1 = x - 1,
y1 = y - 1;
return lerp(
fade(y),
lerp(
FDX,
gradient2D(P[A], x, y),
gradient2D(P[B], x1, y)
),
lerp(
FDX,
gradient2D(P[A + 1], x, y1),
gradient2D(P[B + 1], x1, y1)
)
);
}
/**
* Returns a three-dimensional noise value between -1 and 1.
* @param {number} x A numeric expression.
* @param {number} y A numeric expression.
* @param {number} z A numeric expression.
* @returns {number} Perlin Noise value.
*/
function perlin3D(x, y, z) {
const
FX = Math.floor(x),
FY = Math.floor(y),
FZ = Math.floor(z),
X = FX & 255,
Y = FY & 255,
Z = FZ & 255,
A = P[X] + Y,
B = P[X + 1] + Y,
AA = P[A] + Z,
BA = P[B] + Z,
AB = P[A + 1] + Z,
BB = P[B + 1] + Z;
x = x - FX;
y = y - FY;
z = z - FZ;
const
FDX = fade(x),
FDY = fade(y),
x1 = x - 1,
y1 = y - 1,
z1 = z - 1;
return lerp(
fade(z),
lerp(
FDY,
lerp(
FDX,
gradient3D(P[AA], x, y, z),
gradient3D(P[BA], x1, y, z)
),
lerp(
FDX,
gradient3D(P[AB], x, y1, z),
gradient3D(P[BB], x1, y1, z)
)
),
lerp(
FDY,
lerp(
FDX,
gradient3D(P[AA + 1], x, y, z1),
gradient3D(P[BA + 1], x1, y, z1)
),
lerp(
FDX,
gradient3D(P[AB + 1], x, y1, z1),
gradient3D(P[BB + 1], x1, y1, z1)
)
)
);
}
/**
* Returns a four-dimensional noise value between -1 and 1.
* @param {number} x A numeric expression.
* @param {number} y A numeric expression.
* @param {number} z A numeric expression.
* @param {number} t A numeric expression.
* @returns {number} Perlin Noise value.
*/
function perlin4D(x, y, z, t) {
const
FX = Math.floor(x),
FY = Math.floor(y),
FZ = Math.floor(z),
FT = Math.floor(t),
X = FX & 255,
Y = FY & 255,
Z = FZ & 255,
T = FT & 255,
A = P[X] + Y,
B = P[X + 1] + Y,
AA = P[A] + Z,
BA = P[B] + Z,
AB = P[A + 1] + Z,
BB = P[B + 1] + Z,
AAA = P[AA] + T,
BAA = P[BA] + T,
ABA = P[AB] + T,
BBA = P[BB] + T,
AAB = P[AA + 1] + T,
BAB = P[BA + 1] + T,
ABB = P[AB + 1] + T,
BBB = P[BB + 1] + T;
x = x - FX;
y = y - FY;
z = z - FZ;
t = t - FT;
const
FDX = fade(x),
FDY = fade(y),
FDZ = fade(z),
x1 = x - 1,
y1 = y - 1,
z1 = z - 1,
t1 = t - 1;
return lerp(
fade(t),
lerp(
FDZ,
lerp(
FDY,
lerp(
FDX,
gradient4D(P[AAA], x, y, z, t),
gradient4D(P[BAA], x1, y, z, t)
),
lerp(
FDX,
gradient4D(P[ABA], x, y1, z, t),
gradient4D(P[BBA], x1, y1, z, t)
)
),
lerp(
FDY,
lerp(
FDX,
gradient4D(P[AAB], x, y, z1, t),
gradient4D(P[BAB], x1, y, z1, t)
),
lerp(
FDX,
gradient4D(P[ABB], x, y1, z1, t),
gradient4D(P[BBB], x1, y1, z1, t)
)
)
),
lerp(
FDZ,
lerp(
FDY,
lerp(
FDX,
gradient4D(P[AAA + 1], x, y, z, t1),
gradient4D(P[BAA + 1], x1, y, z, t1)
),
lerp(
FDX,
gradient4D(P[ABA + 1], x, y1, z, t1),
gradient4D(P[BBA + 1], x1, y1, z, t1)
)
),
lerp(
FDY,
lerp(
FDX,
gradient4D(P[AAB + 1], x, y, z1, t1),
gradient4D(P[BAB + 1], x1, y, z1, t1)
),
lerp(
FDX,
gradient4D(P[ABB + 1], x, y1, z1, t1),
gradient4D(P[BBB + 1], x1, y1, z1, t1)
)
)
)
);
}
module.exports = { perlin1D, perlin2D, perlin3D, perlin4D };

View file

@ -1,23 +0,0 @@
const ItemStack = require("../ItemStack.js");
module.exports = class {
constructor() {
this.Slots = {};
}
saveData() {
}
loadData() {
}
getSlot(slot = 0) {
return this.Slots[slot]; // If the slot doesn't exist well sucks to be you I guess! Haha :Þ
}
setSlot(slot = 0, itemStack = new ItemStack) {
this.Slots[slot] = itemStack;
}
}

View file

@ -1,10 +0,0 @@
module.exports = class {
constructor(id, count) {
this.id = id;
this.count = count;
}
updateCount(count) {
this.count += count;
}
}

336
server/MPClient.ts Normal file
View file

@ -0,0 +1,336 @@
import { IReader } from "bufferstuff";
import { Console } from "hsconsole";
import { Socket } from "net";
import AABB from "./AABB";
import Block from "./blocks/Block";
import EntityLiving from "./entities/EntityLiving";
import EntityItem from "./entities/EntityItem";
import ItemStack from "./inventories/ItemStack";
import MinecraftServer from "./MinecraftServer";
import Packet from "./enums/Packet";
import PacketAnimation from "./packets/Animation";
import PacketChat from "./packets/Chat"
import PacketEntityAction from "./packets/EntityAction";
import PacketPlayer from "./packets/Player";
import PacketPlayerPosition from "./packets/PlayerPosition";
import PacketPlayerLook from "./packets/PlayerLook";
import PacketPlayerPositionLook from "./packets/PlayerPositionLook";
import PacketPlayerDigging from "./packets/PlayerDigging";
import PacketRespawn from "./packets/Respawn";
import PacketPlayerBlockPlacement from "./packets/PlayerBlockPlacement";
import PacketHoldingChange from "./packets/HoldingChange";
import PacketDisconnectKick from "./packets/DisconnectKick";
import PacketSoundEffect from "./packets/SoundEffect";
import PacketUseEntity from "./packets/UseEntity";
import Player from "./entities/Player";
import PlayerInventory from "./inventories/PlayerInventory";
import SoundEffects from "./enums/SoundEffects";
import Vec3 from "./Vec3";
export default class MPClient {
private readonly mcServer:MinecraftServer;
private readonly socket:Socket;
public entity:Player;
private inventory:PlayerInventory;
private dimension:number;
private holdingIndex:number = 36; // First hotbar slot.
private diggingAt:Vec3;
public constructor(mcServer:MinecraftServer, socket:Socket, entity:Player) {
this.mcServer = mcServer;
this.socket = socket;
this.entity = entity;
this.inventory = entity.inventory;
this.dimension = 0;
this.diggingAt = new Vec3();
}
private mapCoordsFromFace(pos:Vec3, face:number) {
switch (face) {
case 0:
pos.y--;
return pos;
case 1:
pos.y++;
return pos;
case 2:
pos.z--;
return pos;
case 3:
pos.z++;
return pos;
case 4:
pos.x--;
return pos;
case 5:
pos.x++;
return pos;
}
}
public handlePacket(reader:IReader) {
const packetId = reader.readUByte();
switch (packetId) {
case Packet.Chat: this.handleChat(new PacketChat().readData(reader)); break;
case Packet.UseEntity: this.handleUseEntity(new PacketUseEntity().readData(reader)); break;
case Packet.Respawn: this.handlePacketRespawn(new PacketRespawn().readData(reader)); break;
case Packet.Player: this.handlePacketPlayer(new PacketPlayer().readData(reader)); break;
case Packet.PlayerPosition: this.handlePacketPlayerPosition(new PacketPlayerPosition().readData(reader)); break;
case Packet.PlayerLook: this.handlePacketPlayerLook(new PacketPlayerLook().readData(reader)); break;
case Packet.PlayerPositionLook: this.handlePacketPlayerPositionLook(new PacketPlayerPositionLook().readData(reader)); break;
case Packet.PlayerDigging: this.handlePacketPlayerDigging(new PacketPlayerDigging().readData(reader)); break;
case Packet.PlayerBlockPlacement: this.handlePacketBlockPlacement(new PacketPlayerBlockPlacement().readData(reader)); break;
case Packet.HoldingChange: this.handlePacketHoldingChange(new PacketHoldingChange().readData(reader)); break;
//case Packets.UseBed: break;
case Packet.Animation: this.handlePacketAnimation(new PacketAnimation().readData(reader)); break;
case Packet.EntityAction: this.handlePacketEntityAction(new PacketEntityAction().readData(reader)); break;
case Packet.DisconnectKick: this.handleDisconnectKick(); break;
default: return Console.printWarn(`UNIMPLEMENTED PACKET: ${Packet[packetId]}`);
}
if (reader.readOffset < reader.length - 1) {
this.handlePacket(reader);
}
}
private handleUseEntity(packet:PacketUseEntity) {
const attacker = this.entity.world.entites.get(packet.userId);
const target = this.entity.world.entites.get(packet.targetId);
if (attacker && target && target instanceof EntityLiving) {
if (packet.leftClick) {
target.damageFrom(2, attacker);
}
}
}
private handleChat(packet:PacketChat) {
const message = packet.message.split(" ");
if (message[0].startsWith("/")) {
packet.message = "";
if (message[0] === "/tp") {
const x = this.entity.position.x = parseFloat(message[1]);
const y = this.entity.position.y = parseFloat(message[2]);
const z = this.entity.position.z = parseFloat(message[3]);
this.send(new PacketPlayerPositionLook(x, y, y + 0.62, z, 0, 0, false).writeData());
Console.printInfo(packet.message = `Teleported ${this.entity.username} to ${message[1]} ${message[2]} ${message[3]}`);
} else if (message[0] === "/csay") {
this.mcServer.sendChatMessage(`[CONSOLE] ${message.slice(1, message.length).join(" ")}`);
} else if (message[0] === "/top") {
packet.message = `Woosh!`;
const topBlock = this.entity.chunk.getTopBlockY(this.entity.position.x & 0xf, this.entity.position.z & 0xf);
this.send(new PacketPlayerPosition(this.entity.position.x, topBlock + 3, topBlock + 3.62, this.entity.position.z, false).writeData());
} else if (message[0] === "/tpx") {
const dimension = parseInt(message[1]);
if (this.mcServer.worlds.has(dimension)) {
packet.message = "\u00a76Switching dimensions...";
this.switchDimension(dimension);
} else {
packet.message = `\u00a7cNo dimension by id "${dimension}" exists!`;
}
}
if (packet.message !== "") {
this.send(packet.writeData());
}
return;
}
packet.message = `<${this.entity.username}> ${packet.message}`;
Console.printInfo(`[CHAT] ${packet.message}`);
this.mcServer.sendToAllClients(packet.writeData());
}
private handlePacketRespawn(packet:PacketRespawn) {
if (!this.entity.isDead && packet.dimension === this.entity.world.dimension) {
return;
}
const world = this.mcServer.worlds.get(this.entity.world.dimension);
if (world == undefined) {
return;
}
this.entity.world.removeEntity(this.entity);
const oldPlayerEntity = this.entity;
this.entity = new Player(this.mcServer, world, oldPlayerEntity.username);
this.entity.position.set(8, 70, 8);
world.addEntity(this.entity);
this.send(new PacketRespawn(world.dimension).writeData());
//this.send(new PacketSpawnPosition(8, 64, 8).writeData());
this.entity.position.set(this.entity.position.x, this.entity.position.y, this.entity.position.z);
this.send(new PacketPlayerPositionLook(this.entity.position.x, this.entity.position.y, this.entity.position.y + 0.62, this.entity.position.z, 0, 0, false).writeData());
this.entity.forceUpdatePlayerChunks();
}
private handlePacketPlayer(packet:PacketPlayer) {
this.entity.onGround = packet.onGround;
}
private handlePacketPlayerPosition(packet:PacketPlayerPosition) {
this.entity.onGround = packet.onGround;
this.entity.position.set(packet.x, packet.y, packet.z);
}
private handlePacketPlayerLook(packet:PacketPlayerLook) {
this.entity.onGround = packet.onGround;
this.entity.rotation.set(packet.yaw, packet.pitch);
}
private handlePacketPlayerPositionLook(packet:PacketPlayerPositionLook) {
this.entity.onGround = packet.onGround;
this.entity.position.set(packet.x, packet.y, packet.z);
this.entity.rotation.set(packet.yaw, packet.pitch);
}
private breakBlock(brokenBlockId:number, x:number, y:number, z:number) {
const metadata = this.entity.world.getBlockMetadata(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z);
this.entity.world.setBlockWithNotify(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z, 0);
//this.inventory.addItemStack(new ItemStack(Block.blockBehaviours[brokenBlockId].droppedItem(brokenBlockId), 1, metadata));
//this.send(new PacketWindowItems(0, this.inventory.getInventorySize(), this.inventory.constructInventoryPayload()).writeData());
const blockBehaviour = Block.blockBehaviours[brokenBlockId];
const itemId = blockBehaviour.droppedItem(brokenBlockId);
if (itemId !== -1) {
const itemCount = blockBehaviour.droppedCount(brokenBlockId);
this.entity.sendToNearby(new PacketSoundEffect(SoundEffects.BLOCK_BREAK, x, y, z, brokenBlockId).writeData());
for (let i = 0; i < ((itemCount - 1) >> 6) + 1; i++) {
const itemEntity = new EntityItem(this.entity.world, new ItemStack(itemId, Math.min(itemCount - 64 * i, 64), metadata));
itemEntity.position.set(x + 0.5, y + 0.5, z + 0.5);
itemEntity.pickupDelay = 10;
this.entity.world.addEntity(itemEntity);
}
}
}
// TODO: Cap how far away a player is able to break blocks
private handlePacketPlayerDigging(packet:PacketPlayerDigging) {
// Special drop item case
if (packet.status === 4) {
const itemStack = this.getHeldItemStack();
if (itemStack !== null && itemStack.size > 0) {
itemStack.size--;
const itemEntity = new EntityItem(this.entity.world, new ItemStack(itemStack.itemID, 1, itemStack.damage));
itemEntity.pickupDelay = 10;
itemEntity.position.set(this.entity.position.x, this.entity.position.y + 1.50, this.entity.position.z);
itemEntity.motion.set(
-Math.sin((this.entity.rotation.yaw / 180) * Math.PI) * Math.cos((this.entity.rotation.pitch / 180) * Math.PI) * 0.3,
-Math.sin((this.entity.rotation.pitch / 180) * Math.PI) * 0.3 + 0.1,
Math.cos((this.entity.rotation.yaw / 180) * Math.PI) * Math.cos((this.entity.rotation.pitch / 180) * Math.PI) * 0.3
);
// Add random motion vector
const twoPIRandomised = Math.random() * Math.PI * 2;
const rngMult = 0.02 * Math.random();
itemEntity.motion.add(
Math.cos(twoPIRandomised) * rngMult,
(Math.random() - Math.random()) * 0.1,
Math.sin(twoPIRandomised) * rngMult
);
this.entity.world.addEntity(itemEntity);
this.inventory.dropEmptyItemStacks();
this.inventory.sendUpdatedStacks([this.holdingIndex]);
}
return;
}
this.diggingAt.set(packet.x, packet.y, packet.z);
let brokenBlockId:number;
if (packet.status === 0) {
// Started digging
if ((brokenBlockId = this.entity.world.getBlockId(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z)) != 0 && Block.blocks[brokenBlockId].blockStrength() >= 1) {
this.breakBlock(brokenBlockId, this.diggingAt.x, this.diggingAt.y, this.diggingAt.z);
}
} else if (packet.status === 2) {
if ((brokenBlockId = this.entity.world.getBlockId(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z)) != 0) {
this.breakBlock(brokenBlockId, this.diggingAt.x, this.diggingAt.y, this.diggingAt.z);
}
}
}
public getHeldItemStack() {
return this.inventory.getSlotItemStack(this.holdingIndex);
}
private handlePacketBlockPlacement(packet:PacketPlayerBlockPlacement) {
this.diggingAt.set(packet.x, packet.y, packet.z);
this.mapCoordsFromFace(this.diggingAt, packet.face);
if (this.entity.entityAABB.intersects(AABB.getAABB(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z, this.diggingAt.x + 1, this.diggingAt.y + 1, this.diggingAt.z + 1))) {
return;
}
const itemStack = this.getHeldItemStack();
if (itemStack == null || itemStack.size == 0) {
return;
}
if (itemStack.isBlock && Block.blocks[itemStack.itemID].behaviour.canPlaceBlockAt(this.entity.world, this.diggingAt.x, this.diggingAt.y, this.diggingAt.z)) {
if (this.entity.world.getBlockId(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z) === 0) {
itemStack.size--;
this.entity.world.setBlockAndMetadataWithNotify(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z, itemStack.itemID, itemStack.damage);
this.inventory.dropEmptyItemStacks();
}
} else {
// TODO: Handle item usage
}
}
private handlePacketHoldingChange(packet:PacketHoldingChange) {
if (packet.slotId < 0 || packet.slotId > 8) {
this.send(new PacketDisconnectKick("Out of Bounds Holding Index!").writeData());
this.socket.end();
return;
}
this.holdingIndex = 36 + packet.slotId;
}
// Animation start
private handlePacketAnimation(packet:PacketAnimation) {
// Forward this packet to all nearby clients
this.entity.world.sendToNearbyClients(this.entity, packet.writeData());
}
private handlePacketEntityAction(packet:PacketEntityAction) {
// Forward this packet to all nearby clients
switch (packet.action) {
case 1: this.entity.crouching = true; break;
case 2: this.entity.crouching = false; break;
case 3: break; // TODO: Leave Bed
}
}
private switchDimension(dimension:number) {
const world = this.mcServer.worlds.get(dimension);
if (world == undefined) {
return;
}
this.entity.world.removeEntity(this.entity);
this.entity.world = world;
world.addEntity(this.entity);
this.send(new PacketRespawn(dimension).writeData());
//this.send(new PacketSpawnPosition(8, 64, 8).writeData());
this.entity.position.set(8, 60, 8);
this.send(new PacketPlayerPositionLook(8, 70, 70.62, 8, 0, 0, false).writeData());
this.entity.forceUpdatePlayerChunks();
}
private handleDisconnectKick() {
this.socket.end();
}
public send(buffer:Buffer) {
this.socket.write(buffer);
}
}

90
server/MetadataWriter.ts Normal file
View file

@ -0,0 +1,90 @@
import { createWriter, Endian } from "bufferstuff";
import FunkyArray from "funky-array";
import ItemStack from "./inventories/ItemStack";
import MetadataFieldType from "./enums/MetadataFieldType";
import Vec3 from "./Vec3";
export class MetadataEntry {
public type:MetadataFieldType;
public value:number|string|Vec3|ItemStack;
public constructor(type:MetadataFieldType, value:number|string|Vec3|ItemStack) {
this.type = type;
this.value = value;
}
}
export class MetadataWriter {
// https://wiki.vg/index.php?title=Protocol&oldid=488#Entity_Metadata_.280x28.29
private entries:FunkyArray<number, MetadataEntry>; // TODO: Extend with Item and Vector types
public constructor() {
this.entries = new FunkyArray<number, MetadataEntry>();
}
public addMetadataEntry(identifier:number, entry:MetadataEntry) {
this.entries.set(identifier, entry);
}
private calculateBufferSize() {
let size = this.entries.length + 1; // Type/Identifiers + Stream end magic
this.entries.forEach(entry => {
switch (entry.type) {
case MetadataFieldType.Byte: size += 1; break;
case MetadataFieldType.Short: size += 2; break;
case MetadataFieldType.Int: size += 4; break;
case MetadataFieldType.Float: size += 4; break;
case MetadataFieldType.String:
if (typeof(entry.value) === "string") {
size += 2 + entry.value.length * 2; break;
} else {
throw "Non-string value assigned to a String MetadataEntry";
}
case MetadataFieldType.Item:
if (entry.value instanceof ItemStack) {
size += 5;
} else {
throw "Non-ItemStack value assigned to an ItemStack MetadataEntry";
}
case MetadataFieldType.Vector:
if (entry.value instanceof Vec3) {
size += 12;
} else {
throw "Non-Vec3 value assigned to an Vec3 MetadataEntry";
}
}
})
return size;
}
public writeBuffer() {
const writer = createWriter(Endian.BE, this.calculateBufferSize());
for (const key of this.entries.keys) {
const entry = this.entries.get(key);
if (entry instanceof MetadataEntry) {
writer.writeByte((entry.type << 5 | key & 0x1f) & 0xff);
if (typeof(entry.value) === "number") {
switch (entry.type) {
case MetadataFieldType.Byte: writer.writeByte(entry.value); break;
case MetadataFieldType.Short: writer.writeShort(entry.value); break;
case MetadataFieldType.Int: writer.writeInt(entry.value); break;
case MetadataFieldType.Float: writer.writeFloat(entry.value); break;
}
} else if (typeof(entry.value) === "string") {
writer.writeString16(entry.value);
} else if (entry.value instanceof ItemStack) {
writer.writeShort(entry.value.itemID).writeByte(entry.value.size).writeShort(entry.value.damage);
} else if (entry.value instanceof Vec3) {
const absVec = entry.value.toAbs();
writer.writeInt(absVec.x).writeInt(absVec.y).writeInt(absVec.z);
}
}
}
// Metadata end magic
writer.writeByte(0x7F);
return writer.toBuffer();
}
}

296
server/MinecraftServer.ts Normal file
View file

@ -0,0 +1,296 @@
import { createReader, IReader, Endian } from "bufferstuff";
import { getRandomValues } from "crypto";
import { Console } from "hsconsole";
import { Server, Socket } from "net";
import Config from "../config";
import Chunk from "./Chunk";
import FunkyArray from "funky-array";
import HillyGenerator from "./generators/terrain/Hilly";
import MPClient from "./MPClient";
import NetherGenerator from "./generators/terrain/Nether";
import Packet from "./enums/Packet";
import PacketKeepAlive from "./packets/KeepAlive";
import PacketHandshake from "./packets/Handshake";
import PacketLoginRequest from "./packets/LoginRequest";
import PacketChat from "./packets/Chat";
import PacketSpawnPosition from "./packets/SpawnPosition";
import PacketPlayerPositionLook from "./packets/PlayerPositionLook";
import PacketNamedEntitySpawn from "./packets/NamedEntitySpawn";
import PacketDisconnectKick from "./packets/DisconnectKick";
import PacketTimeUpdate from "./packets/TimeUpdate";
import PacketWindowItems from "./packets/WindowItems";
import Player from "./entities/Player";
import SaveCompressionType from "./enums/SaveCompressionType";
import World from "./World";
import WorldSaveManager from "./WorldSaveManager";
const chunkFrom = -15;
const chunkTo = 15;
const chunkCount = Math.abs(chunkFrom * 2) * (chunkTo * 2);
function getRandomSeed() {
const arr = new Uint32Array(1);
return getRandomValues(arr)[0];
}
export default class MinecraftServer {
private static readonly PROTOCOL_VERSION = 14;
private static readonly TICK_RATE = 20;
private static readonly TICK_RATE_MS = 1000 / MinecraftServer.TICK_RATE;
private readonly keepalivePacket = new PacketKeepAlive().writeData();
private config:Config;
private server:Server;
private readonly serverClock:NodeJS.Timeout;
private tickCounter:number = 0;
private clients:FunkyArray<string, MPClient>;
public worlds:FunkyArray<number, World>;
public saveManager:WorldSaveManager;
// https://stackoverflow.com/a/7616484
// Good enough for the world seed.
private hashCode(string:string) : number {
let hash = 0, i, chr;
if (string.length === 0) {
return hash;
}
for (i = 0; i < string.length; i++) {
chr = string.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash;
}
public constructor(config:Config) {
this.config = config;
let shuttingDown = false;
process.on("SIGINT", async (signal) => {
if (shuttingDown) {
return;
}
shuttingDown = true;
Console.printInfo("Shutting down...");
// Stop the server timer
clearInterval(this.serverClock);
// Disconnect all players
const kickPacket = new PacketDisconnectKick("Server shutting down.").writeData();
this.sendToAllClients(kickPacket);
// Save chunks
Console.printInfo("Saving worlds...");
let savedWorldCount = 0;
let savedChunkCount = 0;
const worldsSaveStartTime = Date.now();
await this.worlds.forEach(async (world) => {
const worldSaveStartTime = Date.now();
if (world.chunks.length !== 0) {
await world.chunks.forEach(async (chunk) => {
await world.unloadChunk(Chunk.CreateCoordPair(chunk.x, chunk.z));
savedChunkCount++;
});
}
Console.printInfo(`Saved DIM${world.dimension} to disk. Took ${Date.now() - worldSaveStartTime}ms`);
savedWorldCount++;
});
Console.printInfo(`Saved ${savedChunkCount} chunks from ${savedWorldCount} world(s). Took ${Date.now() - worldsSaveStartTime}ms`);
// Flush final console log to disk and close all writers
Console.cleanup();
// hsconsole is gone now so we have to use built in.
console.log("Goodbye");
// Shut down the tcp server
this.server.close();
});
if (this.config.saveCompression === SaveCompressionType.NONE) {
Console.printWarn("=============- WARNING -=============");
Console.printWarn(" Chunk compression is disabled. This");
Console.printWarn(" will lead to large file sizes!");
Console.printWarn("=====================================");
}
this.clients = new FunkyArray<string, MPClient>();
// Convert seed if needed
let worldSeed = typeof(this.config.seed) === "string" ? this.hashCode(this.config.seed) : typeof(this.config.seed) === "number" ? this.config.seed : getRandomSeed();
// Init save manager and load seed from it if possible
this.saveManager = new WorldSaveManager(this.config, [0, -1], worldSeed);
if (this.saveManager.worldSeed !== Number.MIN_VALUE) {
worldSeed = this.saveManager.worldSeed;
}
this.worlds = new FunkyArray<number, World>();
//this.worlds.set(0, new World(this.saveManager, 0, worldSeed, new NewOverworld(worldSeed)));
this.worlds.set(0, new World(this.saveManager, 0, worldSeed, new HillyGenerator(worldSeed)));
this.worlds.set(-1, new World(this.saveManager, -1, worldSeed, new NetherGenerator(worldSeed)));
(async () => {
const generateStartTime = Date.now();
let timer = Date.now();
await this.worlds.forEach(async world => {
timer = Date.now();
let chunksGenerated = 0;
let chunksPerSecond = 0;
Console.printInfo(`Generating spawn area for DIM${world.dimension}...`);
for (let x = chunkFrom; x < chunkTo; x++) {
for (let z = chunkFrom; z < chunkTo; z++) {
const chunk = await world.getChunkSafe(x, z);
chunk.forceLoaded = true;
chunksGenerated++;
chunksPerSecond++;
if (Date.now() - timer >= 1000) {
Console.printInfo(`DIM${world.dimension} Progress [${chunksGenerated}/${chunkCount}] (${chunksPerSecond} chunks/s) ${((chunksGenerated / chunkCount) * 100).toFixed(2)}%`);
timer = Date.now();
chunksPerSecond = 0;
}
}
}
});
Console.printInfo(`Done! Took ${Date.now() - generateStartTime}ms`);
this.initServer();
}).bind(this)();
const timeUpdate = new PacketTimeUpdate(BigInt(this.tickCounter));
this.serverClock = setInterval(() => {
// Every 1 sec
if (this.tickCounter % MinecraftServer.TICK_RATE === 0) {
if (this.clients.length !== 0) {
timeUpdate.time = BigInt(this.tickCounter);
const timePacket = timeUpdate.writeData();
this.clients.forEach(client => {
// Keep the client happy
client.send(this.keepalivePacket);
client.send(timePacket);
});
}
// const memoryUsage = process.memoryUsage();
// console.log(`Memory Usage: ${(memoryUsage.heapUsed / 1024 / 1024).toFixed(1)}MB / ${(memoryUsage.heapTotal / 1024 / 1024).toFixed(1)}MB ArrayBuffers: ${(memoryUsage.arrayBuffers / 1024 / 1024).toFixed(1)}MB`);
}
this.worlds.forEach(world => {
world.tick();
});
this.tickCounter++;
}, MinecraftServer.TICK_RATE_MS);
this.server = new Server();
this.server.on("connection", this.onConnection.bind(this));
}
initServer() {
this.server.listen(this.config.port, () => Console.printInfo(`Minecraft server started at ${this.config.port}`));
}
sendToAllClients(buffer:Buffer) {
this.clients.forEach(client => {
client.send(buffer);
});
}
sendChatMessage(text:string) {
this.sendToAllClients(new PacketChat(text).writeData());
Console.printInfo(`[CHAT] ${text}`);
}
async handleLoginRequest(reader:IReader, socket:Socket, setMPClient:(mpclient:MPClient) => void) {
const loginPacket = new PacketLoginRequest().readData(reader);
if (loginPacket.protocolVersion !== MinecraftServer.PROTOCOL_VERSION) {
if (loginPacket.protocolVersion > MinecraftServer.PROTOCOL_VERSION) {
socket.write(new PacketDisconnectKick("Outdated server!").writeData());
} else {
socket.write(new PacketDisconnectKick("Outdated or modded client!").writeData());
}
return;
}
const dimension = 0;
const world = this.worlds.get(dimension);
if (world instanceof World) {
const clientEntity = new Player(this, world, loginPacket.username);
if (this.saveManager.playerDataOnDisk.includes(clientEntity.username)) {
clientEntity.fromSave(await this.saveManager.readPlayerDataFromDisk(clientEntity.username));
} else {
clientEntity.position.set(8, 70, 8);
}
world.addEntity(clientEntity);
const client = new MPClient(this, socket, clientEntity);
setMPClient(client);
clientEntity.mpClient = client;
this.clients.set(loginPacket.username, client);
this.sendChatMessage(`\u00a7e${loginPacket.username} joined the game`);
socket.write(new PacketLoginRequest(clientEntity.entityId, "", 0, dimension).writeData());
socket.write(new PacketSpawnPosition(8, 64, 8).writeData());
const thisPlayerSpawn = new PacketNamedEntitySpawn(clientEntity.entityId, clientEntity.username, clientEntity.absPosition.x, clientEntity.absPosition.y, clientEntity.absPosition.z, clientEntity.absRotation.yaw, clientEntity.absRotation.pitch, clientEntity.mpClient?.getHeldItemStack()?.itemID).writeData();
world.players.forEach(player => {
if (player.entityId !== clientEntity.entityId && clientEntity.distanceTo(player) < World.ENTITY_MAX_SEND_DISTANCE) {
// Inform the joining player of the players around them
socket.write(new PacketNamedEntitySpawn(player.entityId, player.username, player.absPosition.x, player.absPosition.y, player.absPosition.z, player.absRotation.yaw, player.absRotation.pitch, player.mpClient?.getHeldItemStack()?.itemID).writeData());
player.sendPlayerEquipment(clientEntity);
// Inform players around the joining player of the joined player
player.mpClient?.send(thisPlayerSpawn);
clientEntity.sendPlayerEquipment(player);
}
});
socket.write(new PacketPlayerPositionLook(clientEntity.position.x, clientEntity.position.y, clientEntity.position.y + 0.62, clientEntity.position.z, 0, 0, false).writeData());
const playerInventory = clientEntity.inventory;
socket.write(new PacketWindowItems(0, playerInventory.getInventorySize(), playerInventory.constructInventoryPayload()).writeData());
} else {
socket.write(new PacketDisconnectKick("Failed to find world to put player in.").writeData());
}
}
handleHandshake(reader:IReader, socket:Socket) {
const handshakePacket = new PacketHandshake().readData(reader);
socket.write(handshakePacket.writeData());
}
onConnection(socket:Socket) {
let mpClient:MPClient;
const setMPClient = (mpclient:MPClient) => {
mpClient = mpclient;
}
const playerDisconnect = (err:Error) => {
mpClient.entity.world.removeEntity(mpClient.entity);
this.clients.remove(mpClient.entity.username);
this.sendChatMessage(`\u00a7e${mpClient.entity.username} left the game`);
if (typeof(err) !== "boolean") {
Console.printError(`Client disconnected with error: ${err.message}`);
}
}
socket.on("close", playerDisconnect.bind(this));
socket.on("error", playerDisconnect.bind(this));
socket.on("data", chunk => {
const reader = createReader(Endian.BE, chunk);
// Let mpClient take over if it exists
if (mpClient instanceof MPClient) {
mpClient.handlePacket(reader);
return;
}
const packetId = reader.readUByte();
switch (packetId) {
// TODO: Handle timeouts at some point, idk.
case Packet.KeepAlive: break;
case Packet.LoginRequest: this.handleLoginRequest(reader, socket, setMPClient.bind(this)); break;
case Packet.Handshake: this.handleHandshake(reader, socket); break;
}
});
}
}

View file

@ -1,69 +0,0 @@
/*
===========- NamedPackets.js -==========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const namedPackets = {
"Disconnect": -1,
"KeepAlive": 0x00,
"LoginRequest": 0x01,
"Handshake": 0x02,
"ChatMessage": 0x03,
"TimeUpdate": 0x04,
"EntityEquipment": 0x05,
"SpawnPosition": 0x06,
"UseEntity": 0x07,
"UpdateHealth": 0x08,
"Respawn": 0x09,
"Player": 0x0A,
"PlayerPosition": 0x0B,
"PlayerLook": 0x0C,
"PlayerPositionAndLook": 0x0D,
"PlayerDigging": 0x0E,
"PlayerBlockPlacement": 0x0F,
"HoldingChange": 0x10,
"UseBed": 0x11,
"Animation": 0x12,
"EntityAction": 0x13,
"NamedEntitySpawn": 0x14,
"PickupSpawn": 0x15,
"CollectItem": 0x16,
"AddObjectOrVehicle": 0x17,
"MobSpawn": 0x18,
"EntityPainting": 0x19,
"StanceUpdate": 0x1B,
"EntityVelocity": 0x1C,
"DestroyEntity": 0x1D,
"Entity": 0x1E,
"EntityRelativeMove": 0x1F,
"EntityLook": 0x20,
"EntityLookAndRelativeMove": 0x21,
"EntityTeleport": 0x22,
"EntityStatus": 0x26,
"AttachEntity": 0x27,
"EntityMetadata": 0x28,
"PreChunk": 0x32,
"MapChunk": 0x33,
"MultiBlockChange": 0x34,
"BlockChange": 0x35,
"BlockAction": 0x36,
"Explosion": 0x3C,
"SoundEffect": 0x3D,
"NewOrInvalidState": 0x46,
"Thunderbolt": 0x47,
"OpenWindow": 0x64,
"CloseWindow": 0x65,
"WindowClick": 0x66,
"SetSlot": 0x67,
"WindowItems": 0x68,
"UpdateProgressBar": 0x69,
"Transaction": 0x6A,
"UpdateSign": 0x82,
"ItemData": 0x83,
"IncrementStatistic": 0xC8,
"DisconnectOrKick": 0xFF
};
module.exports = namedPackets;

View file

@ -1,42 +0,0 @@
/*
========- PacketMappingTable.js -=======
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet0KeepAlive = require("./Packets/Packet0KeepAlive.js"),
Packet1LoginRequest = require("./Packets/Packet1LoginRequest.js"),
Packet2Handshake = require("./Packets/Packet2Handshake.js"),
Packet3Chat = require("./Packets/Packet3Chat.js"),
Packet4TimeUpdate = require("./Packets/Packet4TimeUpdate"),
Packet6SpawnPosition = require("./Packets/Packet6SpawnPosition.js"),
Packet10Player = require("./Packets/Packet10Player.js"),
Packet13PlayerPositionAndLook = require("./Packets/Packet13PlayerPositionAndLook.js"),
Packet18Animation = require("./Packets/Packet18Animation.js"),
Packet20NamedEntitySpawn = require("./Packets/Packet20NamedEntitySpawn.js"),
Packet32EntityLook = require("./Packets/Packet32EntityLook.js"),
Packet34EntityTeleport = require("./Packets/Packet34EntityTeleport.js"),
Packet50PreChunk = require("./Packets/Packet50PreChunk.js"),
Packet53BlockChange = require("./Packets/Packet53BlockChange.js"),
Packet103SetSlot = require("./Packets/Packet103SetSlot.js");
const mappingTable = {
0x00: Packet0KeepAlive,
0x01: Packet1LoginRequest,
0x02: Packet2Handshake,
0x03: Packet3Chat,
0x04: Packet4TimeUpdate,
0x06: Packet6SpawnPosition,
0x0A: Packet10Player,
0x0D: Packet13PlayerPositionAndLook,
0x12: Packet18Animation,
0x14: Packet20NamedEntitySpawn,
0x20: Packet32EntityLook,
0x22: Packet34EntityTeleport,
0x32: Packet50PreChunk,
0x35: Packet53BlockChange,
0x67: Packet103SetSlot
};
module.exports = mappingTable;

View file

@ -1,30 +0,0 @@
/*
==============- Packet.js -=============
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const bufferStuff = require("../bufferStuff.js");
module.exports = class {
constructor(packetID = 0x00) {
this.id = packetID;
this.packetSize = 0;
this.writer = null;
}
writePacket() {
this.writer = new bufferStuff.Writer(this.packetSize);
this.writer.writeByte(this.id);
return this.writer;
}
toBuffer() {
return this.writer == null ? Buffer.alloc(0) : this.writer.buffer;
}
}

View file

@ -1,18 +0,0 @@
/*
========- Packet0KeepAlive.js -=========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet0KeepAlive extends Packet {
writePacket() {
super.writePacket();
return this.toBuffer();
}
}
module.exports = Packet0KeepAlive;

View file

@ -1,34 +0,0 @@
/*
========- Packet103SetSlot.js -=========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet103SetSlot extends Packet {
constructor(window_id = 0, slot = 0, item_id = -1, item_count = 0, item_uses = 0) {
super(0x67);
this.window_id = window_id;
this.slot = slot;
this.item_id = item_id;
this.item_count = item_count;
this.item_uses = item_uses;
}
writePacket() {
super.writePacket();
this.writer.writeByte(this.window_id);
this.writer.writeShort(this.slot);
this.writer.writeShort(this.item_id);
this.writer.writeByte(this.item_count);
if (this.item_id != -1) this.writer.writeShort(this.item_uses);
return this.toBuffer();
}
}
module.exports = Packet103SetSlot;

View file

@ -1,26 +0,0 @@
/*
=========- Packet10Player.js -==========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet10Player extends Packet {
constructor(onGround = true) {
super(0x0A);
this.onGround = onGround;
}
writePacket() {
super.writePacket();
this.writer.writeBool(this.onGround);
return this.toBuffer();
}
}
module.exports = Packet10Player;

View file

@ -1,38 +0,0 @@
/*
==- Packet13PlayerPositionAndLook.js -==
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet13PlayerPositionAndLook extends Packet {
constructor(x = 0, y = 65, stance = 67, z = 0, yaw = 0.0, pitch = 0.0, onGround = true) {
super(0x0D);
this.x = x;
this.y = y;
this.stance = stance;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
this.onGround = onGround;
}
writePacket() {
super.writePacket();
this.writer.writeDouble(this.x);
this.writer.writeDouble(this.y);
this.writer.writeDouble(this.stance);
this.writer.writeDouble(this.z);
this.writer.writeFloat(this.yaw);
this.writer.writeFloat(this.pitch);
this.writer.writeBool(this.onGround);
return this.toBuffer();
}
}
module.exports = Packet13PlayerPositionAndLook;

View file

@ -1,28 +0,0 @@
/*
========- Packet18Animation.js -========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet18Animation extends Packet {
constructor(EID = 0, animation = 0) {
super(0x12);
this.EID = EID;
this.animation = animation;
}
writePacket() {
super.writePacket();
this.writer.writeInt(this.EID);
this.writer.writeByte(this.animation);
return this.toBuffer();
}
}
module.exports = Packet18Animation;

View file

@ -1,36 +0,0 @@
/*
=======- Packet1LoginRequest.js -=======
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet1LoginRequest extends Packet {
constructor(protocol_version = 0, username = "", map_seed = BigInt(0), dimension = 0) {
super(0x01);
this.protocol_version = protocol_version;
this.username = username;
this.map_seed = map_seed;
this.dimension = dimension;
}
readPacket() {
}
writePacket(EID = 0) {
super.writePacket();
this.writer.writeInt(EID);
this.writer.writeString("");
this.writer.writeLong(971768181197178410);
this.writer.writeByte(0);
return this.toBuffer();
}
}
module.exports = Packet1LoginRequest;

View file

@ -1,41 +0,0 @@
/*
=====- Packet20NamedEntitySpawn.js -====
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
const Converter = require("../Converter.js");
class Packet20NamedEntitySpawn extends Packet {
constructor(EID = 0, entityName = "", x = 0.0, y = 0.0, z = 0.0, yaw = 0.0, pitch = 0.0, currentItem = 0) {
super(0x14);
this.EID = EID;
this.entityName = entityName;
this.absX = Converter.toAbsoluteInt(x);
this.absY = Converter.toAbsoluteInt(y);
this.absZ = Converter.toAbsoluteInt(z);
this.packedYaw = Converter.to360Fraction(yaw);
this.packedPitch = Converter.to360Fraction(pitch);
this.currentItem = currentItem;
}
writePacket() {
super.writePacket();
this.writer.writeInt(this.EID);
this.writer.writeString(this.entityName);
this.writer.writeInt(this.absX);
this.writer.writeInt(this.absY);
this.writer.writeInt(this.absZ);
this.writer.writeUByte(this.packedYaw);
this.writer.writeUByte(this.packedPitch);
this.writer.writeShort(this.currentItem);
return this.toBuffer();
}
}
module.exports = Packet20NamedEntitySpawn;

View file

@ -1,26 +0,0 @@
/*
=========- Packet2Handshake.js -========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet2Handshake extends Packet {
constructor(username = "") {
super(0x02);
this.username = username;
}
writePacket() {
super.writePacket();
this.writer.writeString("-"); // "-" == Offline mode
return this.toBuffer();
}
}
module.exports = Packet2Handshake;

View file

@ -1,33 +0,0 @@
/*
=======- Packet32EntityLook.js -========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
const Converter = require("../Converter.js");
class Packet32EntityLook extends Packet {
constructor(EID = 0, yaw = 0, pitch = 0) {
super(0x20);
this.packetSize = 7;
this.EID = EID;
this.packedYaw = Converter.to360Fraction(yaw);
this.packedPitch = Converter.to360Fraction(pitch);
}
writePacket() {
super.writePacket();
this.writer.writeInt(this.EID);
this.writer.writeUByte(this.packedYaw);
this.writer.writeUByte(this.packedPitch);
return this.toBuffer();
}
}
module.exports = Packet32EntityLook;

View file

@ -1,41 +0,0 @@
/*
======- Packet34EntityTeleport.js -=====
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
const Converter = require("../Converter.js");
class Packet34EntityTeleport extends Packet {
constructor(EID = 0, x = 0, y = 0, z = 0, yaw = 0, pitch = 0) {
super(0x22);
this.packetSize = 19;
this.EID = EID;
this.absX = Converter.toAbsoluteInt(x);
this.absY = Converter.toAbsoluteInt(y);
this.absZ = Converter.toAbsoluteInt(z);
this.packedYaw = Converter.to360Fraction(yaw);
this.packedPitch = Converter.to360Fraction(pitch);
}
soup() {
super.writePacket();
this.writer.writeInt(this.EID);
}
writePacket() {
this.writer.writeInt(this.absX);
this.writer.writeInt(this.absY);
this.writer.writeInt(this.absZ);
this.writer.writeUByte(this.packedYaw);
this.writer.writeUByte(this.packedPitch);
return this.toBuffer();
}
}
module.exports = Packet34EntityTeleport;

View file

@ -1,26 +0,0 @@
/*
===========- Packet3Chat.js -===========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet3Chat extends Packet {
constructor(message = "") {
super(0x03);
this.message = message;
}
writePacket() {
super.writePacket();
this.writer.writeString(this.message);
return this.toBuffer();
}
}
module.exports = Packet3Chat;

View file

@ -1,30 +0,0 @@
/*
========- Packet4TimeUpdate.js -========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet4TimeUpdate extends Packet {
constructor(time = BigInt(0)) {
super(0x04);
this.time = time;
}
readPacket() {
}
writePacket() {
super.writePacket();
this.writer.writeLong(this.time);
return this.toBuffer();
}
}
module.exports = Packet4TimeUpdate;

View file

@ -1,30 +0,0 @@
/*
========- Packet50PreChunk.js -=========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet50PreChunk extends Packet {
constructor(x = 0, z = 0, mode = true) {
super(0x32);
this.x = x;
this.z = z;
this.mode = mode;
}
writePacket() {
super.writePacket();
this.writer.writeInt(this.x);
this.writer.writeInt(this.z);
this.writer.writeBool(this.mode);
return this.toBuffer();
}
}
module.exports = Packet50PreChunk;

View file

@ -1,34 +0,0 @@
/*
=======- Packet53BlockChange.js -=======
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet53BlockChange extends Packet {
constructor(x = 0, y = 0, z = 0, block_type = 0, block_metadata = 0) {
super(0x35);
this.x = x;
this.y = y;
this.z = z;
this.block_type = block_type;
this.block_metadata = block_metadata;
}
writePacket() {
super.writePacket();
this.writer.writeInt(this.x);
this.writer.writeByte(this.y);
this.writer.writeInt(this.z);
this.writer.writeByte(this.block_type);
this.writer.writeByte(this.block_metadata);
return this.toBuffer();
}
}
module.exports = Packet53BlockChange;

View file

@ -1,30 +0,0 @@
/*
======- Packet6SpawnPosition.js -=======
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const Packet = require("./Packet.js");
class Packet6SpawnPosition extends Packet {
constructor(x = 8.5, y = 65.5, z = 8.5) {
super(0x06);
this.x = x;
this.y = y;
this.z = z;
}
writePacket() {
super.writePacket();
this.writer.writeInt(this.x);
this.writer.writeInt(this.y);
this.writer.writeInt(this.z);
return this.toBuffer();
}
}
module.exports = Packet6SpawnPosition;

17
server/Random.ts Normal file
View file

@ -0,0 +1,17 @@
import mulberry32 from "./mulberry32";
export default class Random {
private readonly mulberry32;
public constructor(seed:number = Date.now()) {
this.mulberry32 = mulberry32(seed);
}
public nextInt(bound = Number.MAX_SAFE_INTEGER) {
return Math.floor(this.mulberry32() * bound);
}
public nextFloat() {
return this.mulberry32();
}
}

19
server/Rotation.ts Normal file
View file

@ -0,0 +1,19 @@
import Vec2 from "./Vec2";
export default class Rotation extends Vec2 {
public get yaw() {
return this.x;
}
public set yaw(value:number) {
this.x = value;
}
public get pitch() {
return this.y;
}
public set pitch(value:number) {
this.y = value;
}
}

23
server/TrackedProperty.ts Normal file
View file

@ -0,0 +1,23 @@
export default class TrackedProperty<T> {
private trackedValue?:T;
private updateCallback?:(value:T) => void;
constructor(initialValue?:T) {
this.trackedValue = initialValue;
}
public set OnChange(value:() => void) {
this.updateCallback = value;
}
public get Value() {
return this.trackedValue;
}
public set Value(value) {
this.trackedValue = value;
if (this.updateCallback && value) {
this.updateCallback(value);
}
}
}

View file

@ -1,91 +0,0 @@
/*
===========- funkyArray.js -============
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const pRandom = require("./prettyRandom.js");
module.exports = class {
constructor(indexingMode = false) {
this.items = {};
this.itemKeys = Object.keys(this.items);
this.indexingMode = indexingMode;
this.index = 0;
this.iterableArray = [];
}
add(item, regenerate = true) {
let id;
if (this.indexingMode) {
this.items[id = this.index] = item;
this.index++;
} else {
this.items[id = pRandom()] = item;
}
if (regenerate) {
this.regenerateIterableArray();
this.itemKeys = Object.keys(this.items);
}
return this.items[id];
}
remove(id, regenerate = true) {
delete this.items[id];
if (regenerate) {
this.regenerateIterableArray();
this.itemKeys = Object.keys(this.items);
}
}
removeFirstItem(regenerate = true) {
delete this.items[this.itemKeys[0]];
if (regenerate) this.regenerateIterableArray();
this.itemKeys = Object.keys(this.items);
}
regenerateIterableArray() {
this.iterableArray = new Array();
for (let itemKey of this.itemKeys) {
this.iterableArray.push(this.items[itemKey]);
}
this.itemKeys = Object.keys(this.items);
}
getFirstItem() {
return this.items[this.itemKeys[0]];
}
getLength() {
return this.itemKeys.length;
}
getKeyById(id) {
return this.itemKeys[id];
}
getById(id) {
return this.items[this.itemKeys[id]];
}
getByKey(key) {
return this.items[key];
}
getKeys() {
return this.itemKeys;
}
getItems() {
return this.items;
}
getIterableItems() {
return this.iterableArray;
}
}

View file

@ -1,28 +0,0 @@
/*
===========- prettyRandom.js -==========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
let lastRandom = -1;
function pRandom(from = 0, to = 2147483647) {
let thisRandom = Math.floor(map(Math.random(), 0, 1, from, to));
if (thisRandom == lastRandom) thisRandom = pRandom(from, to);
return thisRandom;
}
function map(input, inputMin, inputMax, outputMin, outputMax) {
const newv = (input - inputMin) / (inputMax - inputMin) * (outputMax - outputMin) + outputMin;
if (outputMin < outputMax) return constrain(newv, outputMin, outputMax);
else return constrain(newv, outputMax, outputMin);
}
function constrain(input, low, high) {
return Math.max(Math.min(input, high), low);
}
module.exports = pRandom;

35
server/Vec2.ts Normal file
View file

@ -0,0 +1,35 @@
export default class Vec2 {
public x:number;
public y:number;
public constructor(x?:Vec2 | number, y?:number) {
if (typeof(x) === "number" && typeof(y) === "number") {
this.x = x;
this.y = y;
} else if (typeof(x) === "number" && typeof(y) !== "number") {
this.x = x;
this.y = x;
} else if (x instanceof Vec2) {
this.x = x.x;
this.y = x.y;
} else {
this.x = this.y = 0;
}
}
public get isZero() {
return this.x === 0 && this.y === 0;
}
public set(x?:Vec2 | number, y?:number) {
if (x instanceof Vec2) {
this.x = x.x;
this.y = x.y;
} else if (typeof(x) === "number" && typeof(y) === "number") {
this.x = x;
this.y = y;
} else {
this.x = this.y = 0;
}
}
}

75
server/Vec3.ts Normal file
View file

@ -0,0 +1,75 @@
export default class Vec3 {
public x:number;
public y:number;
public z:number;
public constructor(x?:Vec3 | number, y?:number, z?:number) {
if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") {
this.x = x;
this.y = y;
this.z = z;
} else if (typeof(x) === "number" && typeof(y) !== "number" && typeof(z) !== "number") {
this.x = x;
this.y = x;
this.z = x;
} else if (x instanceof Vec3) {
this.x = x.x;
this.y = x.y;
this.z = x.z;
} else {
this.x = this.y = this.z = 0;
}
}
public get isZero() {
return this.x === 0 && this.y === 0 && this.z === 0;
}
public set(x?:Vec3 | number, y?:number, z?:number) {
if (x instanceof Vec3) {
this.x = x.x;
this.y = x.y;
this.z = x.z;
} else if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") {
this.x = x;
this.y = y;
this.z = z;
} else {
throw new Error(`Invalid arguments for Vec3.set : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`);
}
}
add(x:Vec3 | number, y?:number, z?:number) {
if (x instanceof Vec3) {
this.set(this.x + x.x, this.y + x.y, this.z + x.z);
} else if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") {
this.set(this.x + x, this.y + y, this.z + z);
} else {
throw new Error(`Invalid arguments for Vec3.add : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`);
}
}
sub(x:Vec3 | number, y?:number, z?:number) {
if (x instanceof Vec3) {
this.set(this.x - x.x, this.y - x.y, this.z - x.z);
} else if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") {
this.set(this.x - x, this.y - y, this.z - z);
} else {
throw new Error(`Invalid arguments for Vec3.add : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`);
}
}
mult(x:Vec3 | number, y?:number, z?:number) {
if (x instanceof Vec3) {
this.set(this.x * x.x, this.y * x.y, this.z * x.z);
} else if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") {
this.set(this.x * x, this.y * y, this.z * z);
} else {
throw new Error(`Invalid arguments for Vec3.mult : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`);
}
}
toAbs() {
return new Vec3(Math.round(this.x * 32), Math.round(this.y * 32), Math.round(this.z * 32));
}
}

View file

@ -1,82 +0,0 @@
/*
===========- ChunkWorker.js -===========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const { parentPort } = require('worker_threads'),
{ deflateSync } = require("zlib");
const GeneratorFlat = require("../Generators/GeneratorFlat.js");
const GeneratorPerlin = require("../Generators/GeneratorPerlin.js");
const bufferStuff = require("../bufferStuff.js");
let busyInterval = null;
parentPort.on("message", (data) => {
// This stops the thread from stopping :)
if (busyInterval == null) busyInterval = setInterval(() => {}, 86400000); // Runs once a day
switch (data[0]) {
case "chunk":
parentPort.postMessage([data[0], doChunk(data[1]), data[2]]);
break;
case "generate":
const startTime = Date.now();
const chunkData = generateChunk(data[1], data[2], data[4]);
parentPort.postMessage([data[0], chunkData[0], data[1], data[2], data[3], chunkData[1]]);
console.log(`Chunk took ${Date.now() - startTime}ms`);
break;
}
});
function generateChunk(x = 0, z = 0, seed = 0) {
return GeneratorPerlin(x, z, seed);
}
function doChunk(chunk) {
const writer = new bufferStuff.Writer(18);
writer.writeByte(0x33); // Chunk
writer.writeInt(chunk[0] << 4); // Chunk X
writer.writeShort(0 << 7); // Chunk Y
writer.writeInt(chunk[1] << 4); // Chunk Z
writer.writeByte(15); // Size X
writer.writeByte(127); // Size Y
writer.writeByte(15); // Size Z
// pre-alloc since doing an alloc 98,304 times takes a while yknow.
const blocks = new bufferStuff.Writer(32768);
const metadata = new bufferStuff.Writer(32768);
const lighting = new bufferStuff.Writer(32768);
let blockMeta = false;
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
for (let y = 0; y < 128; y++) {
blocks.writeByte(chunk[2][y][x][z][0]);
if (blockMeta) {
metadata.writeNibble(chunk[2][y - 1][x][z][1], chunk[2][y][x][z][1]); // NOTE: This is sorta jank but it does work
// Light level 15 for 2 blocks (1111 1111)
lighting.writeNibble(15, 15); // TODO: Lighting (Client seems to do it's own (when a block update happens) so it's not top priority)
}
// Hack for nibble stuff
blockMeta = !blockMeta;
}
}
}
// These are hacks for the nibbles
blocks.writeBuffer(metadata.buffer);
blocks.writeBuffer(lighting.buffer); // Block lighting
//blocks.writeBuffer(lighting.buffer); // Sky lighting (Looks like this isn't needed???)
// We're on another thread we don't care if we halt
const deflated = deflateSync(blocks.buffer);
writer.writeInt(deflated.length); // Compressed Size
writer.writeBuffer(deflated); // Compressed chunk data
return writer.buffer;
}

333
server/World.ts Normal file
View file

@ -0,0 +1,333 @@
import { Endian, createWriter } from "bufferstuff";
import AABB from "./AABB";
import Block from "./blocks/Block";
import Chunk from "./Chunk";
import EntityItem from "./entities/EntityItem";
import FunkyArray from "funky-array";
import IEntity from "./entities/IEntity";
import IGenerator from "./generators/IGenerator";
import IQueuedUpdate from "./queuedUpdateTypes/IQueuedUpdate";
import PacketBlockChange from "./packets/BlockChange";
import PacketDestroyEntity from "./packets/DestroyEntity";
import PacketPickupSpawn from "./packets/PickupSpawn";
import Player from "./entities/Player";
import QueuedBlockUpdate from "./queuedUpdateTypes/BlockUpdate";
import Random from "./Random";
import WorldSaveManager from "./WorldSaveManager";
export default class World {
public static ENTITY_MAX_SEND_DISTANCE = 50;
private static READ_CHUNKS_FROM_DISK = true;
private readonly saveManager;
private readonly chunksOnDisk:Array<number>;
public chunks:FunkyArray<number, Chunk>;
public entites:FunkyArray<number, IEntity>;
public players:FunkyArray<number, Player>;
public playerHitboxes:FunkyArray<number, AABB>;
public queuedChunkBlocks:Array<IQueuedUpdate>;
public queuedUpdates:Array<IQueuedUpdate>;
public generator:IGenerator;
public random:Random = new Random();
public readonly dimension:number;
public constructor(saveManager:WorldSaveManager, dimension:number, seed:number, generator:IGenerator) {
this.dimension = dimension;
this.saveManager = saveManager;
this.chunksOnDisk = this.saveManager.chunksOnDisk.get(dimension) ?? new Array<number>;
this.chunks = new FunkyArray<number, Chunk>();
this.entites = new FunkyArray<number, IEntity>();
this.players = new FunkyArray<number, Player>();
this.playerHitboxes = new FunkyArray<number, AABB>();
this.queuedChunkBlocks = new Array<IQueuedUpdate>();
this.queuedUpdates = new Array<IQueuedUpdate>();
this.generator = generator;
}
public addEntity(entity:IEntity) {
this.entites.set(entity.entityId, entity);
this.playerHitboxes.set(entity.entityId, entity.entityAABB);
if (entity instanceof Player) {
this.players.set(entity.entityId, entity);
} else if (entity instanceof EntityItem) {
const packet = new PacketPickupSpawn(entity.entityId, entity.itemStack.itemID, entity.itemStack.size, entity.itemStack.damage, Math.round(entity.position.x * 32), Math.round(entity.position.y * 32), Math.round(entity.position.z * 32), 0, 0, 0).writeData();
entity.sendToNearby(packet);
}
}
// TODO: getChunkByCoordPair failed in here during removeEntity, figure out why.
public removeEntity(entity:IEntity) {
if (entity instanceof Player) {
for (const coordPair of entity.loadedChunks) {
if (this.chunkExists(coordPair)) {
const chunk = this.getChunkByCoordPair(coordPair);
chunk.playersInChunk.remove(entity.entityId);
if (!chunk.forceLoaded && chunk.playersInChunk.length === 0) {
this.unloadChunk(coordPair);
}
}
}
// Clear player chunk list (they may be switching dimensions)
entity.loadedChunks = new Array<number>();
entity.justUnloaded = new Array<number>();
this.playerHitboxes.remove(entity.entityId);
this.players.remove(entity.entityId);
if (!entity.isDead) {
const writer = createWriter(Endian.BE);
entity.toSave(writer);
this.saveManager.writePlayerSaveToDisk(entity.username, writer);
}
}
this.entites.remove(entity.entityId);
this.sendToNearbyClients(entity, new PacketDestroyEntity(entity.entityId).writeData());
}
public chunkExists(coordPairOrX:number, chunkZ?:number) {
if (typeof(coordPairOrX) === "number" && typeof(chunkZ) === "number") {
return this.chunks.has(Chunk.CreateCoordPair(coordPairOrX, chunkZ));
}
return this.chunks.has(coordPairOrX);
}
public getChunk(x:number, z:number) {
const coordPair = Chunk.CreateCoordPair(x, z);
const existingChunk = this.chunks.get(coordPair);
if (!(existingChunk instanceof Chunk)) {
throw new Error(`BADLOOKUP: Chunk [${x}, ${z}] does not exist.`);
}
return existingChunk;
}
public getChunkSafe(x:number, z:number) {
return new Promise<Chunk>((resolve) => {
const coordPair = Chunk.CreateCoordPair(x, z);
const existingChunk = this.chunks.get(coordPair);
if (!(existingChunk instanceof Chunk)) {
if (World.READ_CHUNKS_FROM_DISK && this.chunksOnDisk.includes(coordPair)) {
return this.saveManager.readChunkFromDisk(this, x, z)
.then(chunk => resolve(this.chunks.set(coordPair, chunk)));
} else {
resolve(this.chunks.set(coordPair, new Chunk(this, x, z, true)));
if (World.READ_CHUNKS_FROM_DISK) {
this.saveManager.writeChunkToDisk(this.getChunk(x, z));
}
return;
}
}
resolve(existingChunk);
});
}
public getChunkByCoordPair(coordPair:number) {
const existingChunk = this.chunks.get(coordPair);
if (!(existingChunk instanceof Chunk)) {
throw new Error(`BADLOOKUP: Chunk ${coordPair} does not exist.`);
}
return existingChunk;
}
public getBlockId(x:number, y:number, z:number) {
const chunkX = x >> 4,
chunkZ = z >> 4;
return this.getChunk(chunkX, chunkZ).getBlockId(x & 0xf, y, z & 0xf);
}
public getChunkBlockId(chunk:Chunk, x:number, y:number, z:number) {
return chunk.getBlockId(x & 0xf, y, z & 0xf);
}
public getBlockMetadata(x:number, y:number, z:number) {
const chunkX = x >> 4,
chunkZ = z >> 4;
return this.getChunk(chunkX, chunkZ).getBlockMetadata(x & 0xf, y, z & 0xf);
}
public getBlockLight(x:number, y:number, z:number) {
const chunkX = x >> 4,
chunkZ = z >> 4;
return this.getChunk(chunkX, chunkZ).getBlockLight(x & 0xf, y, z & 0xf);
}
public getSkyLight(x:number, y:number, z:number) {
const chunkX = x >> 4,
chunkZ = z >> 4;
return this.getChunk(chunkX, chunkZ).getSkyLight(x & 0xf, y, z & 0xf);
}
public setBlock(blockId:number, x:number, y:number, z:number, doBlockUpdate?:boolean) {
const chunkX = x >> 4,
chunkZ = z >> 4;
const chunk = this.getChunk(chunkX, chunkZ);
chunk.setBlockWithMetadata(blockId, 0, x & 0xf, y, z & 0xf);
if (doBlockUpdate) {
const blockUpdatePacket = new PacketBlockChange(x, y, z, blockId, 0).writeData();
// Send block update to all players that have this chunk loaded
chunk.playersInChunk.forEach(player => {
player.mpClient?.send(blockUpdatePacket);
});
}
}
public setBlockWithMetadata(blockId:number, metadata:number, x:number, y:number, z:number, doBlockUpdate?:boolean) {
const chunkX = x >> 4,
chunkZ = z >> 4;
const chunk = this.getChunk(chunkX, chunkZ);
chunk.setBlockWithMetadata(blockId, metadata, x & 0xf, y, z & 0xf);
if (doBlockUpdate) {
const blockUpdatePacket = new PacketBlockChange(x, y, z, blockId, metadata).writeData();
// Send block update to all players that have this chunk loaded
chunk.playersInChunk.forEach(player => {
player.mpClient?.send(blockUpdatePacket);
});
}
}
public setBlockMetadata(x:number, y:number, z:number, metadata:number, doBlockUpdate?:boolean) {
const chunkX = x >> 4, chunkZ = z >> 4;
const chunk = this.getChunk(chunkX, chunkZ);
const xc = x & 0xf, zc = z & 0xf;
chunk.setBlockMetadata(metadata, xc, y, zc);
if (doBlockUpdate) {
const blockId = chunk.getBlockId(xc, y, zc);
const blockUpdatePacket = new PacketBlockChange(x, y, z, blockId, metadata).writeData();
// Send block update to all players that have this chunk loaded
chunk.playersInChunk.forEach(player => {
player.mpClient?.send(blockUpdatePacket);
});
}
}
public setBlockWithNotify(x:number, y:number, z:number, blockId:number) {
this.setBlock(blockId, x, y, z, true);
this.notifyNeighborBlocksOfChange(x, y, z, blockId);
}
public setBlockMetadataWithNotify(x:number, y:number, z:number, metadata:number) {
this.setBlockMetadata(x, y, z, metadata, true);
this.notifyNeighborBlocksOfChange(x, y, z, this.getBlockId(x, y, z));
}
public setBlockAndMetadataWithNotify(x:number, y:number, z:number, blockId:number, metadata:number) {
this.setBlockWithMetadata(blockId, metadata, x, y, z, true);
this.notifyNeighborBlocksOfChange(x, y, z, blockId);
}
public notifyNeighborBlocksOfChange(x:number, y:number, z:number, blockId:number) {
this.notifyNeighborBlockOfChange(x - 1, y, z, blockId);
this.notifyNeighborBlockOfChange(x + 1, y, z, blockId);
this.notifyNeighborBlockOfChange(x, y - 1, z, blockId);
this.notifyNeighborBlockOfChange(x, y + 1, z, blockId);
this.notifyNeighborBlockOfChange(x, y, z - 1, blockId);
this.notifyNeighborBlockOfChange(x, y, z + 1, blockId);
}
private notifyNeighborBlockOfChange(x:number, y:number, z:number, blockId:number) {
const block = Block.blocks[this.getBlockId(x, y, z)];
if (block != null && block.blockId !== 0) {
block.neighborBlockChange(this, x, y, z, block.blockId);
}
}
public sendToNearbyClients(sentFrom:IEntity, buffer:Buffer) {
this.players.forEach(player => {
if (sentFrom.entityId !== player.entityId && Math.abs(sentFrom.distanceTo(player)) < World.ENTITY_MAX_SEND_DISTANCE) {
player.mpClient?.send(buffer);
}
});
}
public sendToNearbyAllNearbyClients(sentFrom:IEntity, buffer:Buffer) {
this.players.forEach(player => {
if (Math.abs(sentFrom.distanceTo(player)) < World.ENTITY_MAX_SEND_DISTANCE) {
player.mpClient?.send(buffer);
}
});
}
public async unloadChunk(coordPair:number) {
const chunk = this.getChunkByCoordPair(coordPair);
if (!chunk.savingToDisk) {
chunk.savingToDisk = true;
await this.saveManager.writeChunkToDisk(chunk);
if (chunk.playersInChunk.length === 0) {
this.chunks.remove(coordPair);
return;
}
// A player loaded the chunk while we were, flushing to disk.
// Keep it loaded.
chunk.savingToDisk = false;
}
}
public tick() {
if (this.queuedUpdates.length > 0) {
for (let i = this.queuedUpdates.length - 1; i >= 0; i--) {
const update = this.queuedUpdates[i];
if (update instanceof QueuedBlockUpdate) {
if (this.chunks.keys.includes(update.coordPair)) {
this.queuedUpdates.splice(i, 1);
const thatChunk = this.getChunkByCoordPair(update.coordPair);
thatChunk.setBlockWithMetadata(update.blockId, update.metadata, update.x, update.y, update.z);
if (thatChunk.playersInChunk.length > 0) {
const blockUpdate = new PacketBlockChange((thatChunk.x << 4) + update.x, update.y, (thatChunk.z << 4) + update.z, update.blockId, update.metadata).writeData()
thatChunk.playersInChunk.forEach(player => {
player.mpClient?.send(blockUpdate);
});
}
}
}
}
}
this.entites.forEach(entity => {
entity.onTick();
if (entity instanceof Player) {
if (entity.justUnloaded.length > 0) {
for (const coordPair of entity.justUnloaded) {
if (this.chunks.get(coordPair) != undefined) {
const chunkToUnload = this.getChunkByCoordPair(coordPair);
chunkToUnload.playersInChunk.remove(entity.entityId);
if (!chunkToUnload.forceLoaded && chunkToUnload.playersInChunk.length === 0) {
this.unloadChunk(coordPair);
}
}
}
entity.justUnloaded = new Array<number>();
}
}
if (entity.markedForDisposal) {
this.removeEntity(entity);
}
})
}
}

299
server/WorldSaveManager.ts Normal file
View file

@ -0,0 +1,299 @@
import { createWriter, createReader, Endian, IWriter, IReader } from "bufferstuff";
import { readFileSync, readFile, writeFile, existsSync, mkdirSync, writeFileSync, readdirSync, renameSync } from "fs";
import { Console } from "hsconsole";
import { deflate, inflate } from "zlib";
import Chunk from "./Chunk";
import Config from "../config";
import FunkyArray from "funky-array";
import SaveCompressionType from "./enums/SaveCompressionType";
import World from "./World";
enum FileMagic {
Chunk = 0xFC,
Info = 0xFD,
Player = 0xFE
}
export default class WorldSaveManager {
private readonly worldFolderPath;
private readonly globalDataPath;
private readonly worldPlayerDataFolderPath;
private readonly infoFilePath;
private readonly config:Config;
public worldCreationDate = new Date();
public worldLastLoadDate = new Date();
public worldSeed = Number.MIN_VALUE;
public chunksOnDisk:FunkyArray<number, Array<number>>;
public playerDataOnDisk:Array<string>;
public constructor(config:Config, dimensions:Array<number>, numericalSeed:number) {
this.chunksOnDisk = new FunkyArray<number, Array<number>>();
this.playerDataOnDisk = new Array<string>();
this.worldFolderPath = `./${config.worldName}`;
this.worldPlayerDataFolderPath = `${this.worldFolderPath}/playerdata`;
this.globalDataPath = `${this.worldFolderPath}/data`;
this.infoFilePath = `${this.worldFolderPath}/info.hwd`;
this.config = config;
// Create world folder if it doesn't exist
if (!existsSync(this.worldFolderPath)) {
mkdirSync(this.worldFolderPath);
mkdirSync(this.globalDataPath);
}
if (existsSync(this.infoFilePath)) {
this.readInfoFile();
} else {
// World info file does not exist
this.worldSeed = numericalSeed;
this.createInfoFile(numericalSeed);
}
for (const dimension of dimensions) {
const chunksArray = new Array<number>();
this.chunksOnDisk.set(dimension, chunksArray);
const dimensionFolderPath = `${this.worldFolderPath}/DIM${dimension}`
if (!existsSync(dimensionFolderPath)) {
mkdirSync(dimensionFolderPath);
mkdirSync(`${dimensionFolderPath}/chunks`);
mkdirSync(`${dimensionFolderPath}/data`);
} else {
const chunkFiles = readdirSync(`${dimensionFolderPath}/chunks`);
for (const file of chunkFiles) {
if (file.endsWith(".hwc")) {
const name = file.split(".")[0];
chunksArray.push(parseInt(name.startsWith("-") ? name.replace("-", "-0x") : `0x${name}`));
}
}
}
}
if (!existsSync(this.worldPlayerDataFolderPath)) {
mkdirSync(this.worldPlayerDataFolderPath);
}
const playerDataFiles = readdirSync(this.worldPlayerDataFolderPath);
for (const dataFile of playerDataFiles) {
if (dataFile.endsWith(".hpd")) {
this.playerDataOnDisk.push(dataFile.replace(".hpd", ""));
}
}
}
private createInfoFile(numericalSeed:number) {
const infoFileWriter = createWriter(Endian.BE, 26);
infoFileWriter.writeUByte(FileMagic.Info); // Info File Magic
infoFileWriter.writeUByte(2); // File Version
infoFileWriter.writeLong(this.worldCreationDate.getTime()); // World creation date
infoFileWriter.writeLong(this.worldLastLoadDate.getTime()); // Last load date
infoFileWriter.writeLong(numericalSeed);
writeFileSync(this.infoFilePath, infoFileWriter.toBuffer());
}
private readInfoFile() {
const infoFileReader = createReader(Endian.BE, readFileSync(this.infoFilePath));
const fileMagic = infoFileReader.readUByte();
if (fileMagic !== FileMagic.Info) {
throw new Error("World info file is invalid");
}
const fileVersion = infoFileReader.readByte();
// v0, v1 and v2 all contain the same data apart from version numbers
// All that changed between them was the folder format.
if (fileVersion === 0 || fileVersion === 1 || fileVersion === 2) {
this.worldCreationDate = new Date(Number(infoFileReader.readLong()));
infoFileReader.readLong(); // Last load time is currently ignored
this.worldSeed = Number(infoFileReader.readLong());
// Upgrade v0 to v1
if (fileVersion === 0) {
Console.printInfo("Upgrading world to format v1 from v0");
renameSync(`${this.worldFolderPath}/chunks`, `${this.worldFolderPath}/DIM0`);
this.createInfoFile(this.worldSeed);
}
// Upgrade v1 to v2
if (fileVersion === 1) {
Console.printInfo("Upgrading world to format v2 from v1");
const files = readdirSync(`${this.worldFolderPath}/`);
for (const file of files) {
if (file.startsWith("DIM")) {
renameSync(`${this.worldFolderPath}/${file}`, `${this.worldFolderPath}/OLD${file}`);
mkdirSync(`${this.worldFolderPath}/${file}`);
mkdirSync(`${this.worldFolderPath}/${file}/data`);
renameSync(`${this.worldFolderPath}/OLD${file}`, `${this.worldFolderPath}/${file}/chunks`);
}
}
this.createInfoFile(this.worldSeed);
}
}
}
public writeChunkToDisk(chunk:Chunk) {
return new Promise<boolean>((resolve, reject) => {
const saveType = this.config.saveCompression;
const chunkFileWriter = createWriter(Endian.BE, 10);
chunkFileWriter.writeUByte(FileMagic.Chunk); // Chunk File Magic
// TODO: Change to 1 when lighting actually works
chunkFileWriter.writeUByte(1); // File Version
chunkFileWriter.writeUByte(saveType); // Save compression type
chunkFileWriter.writeUByte(16); // Chunk X
chunkFileWriter.writeUByte(128); // Chunk Y
chunkFileWriter.writeUByte(16); // Chunk Z
const chunkData = createWriter(Endian.BE)
.writeBuffer(Buffer.from(chunk.getBlockData()))
.writeBuffer(chunk.getMetadataBuffer())
.writeBuffer(chunk.getBlockLightBuffer())
.writeBuffer(chunk.getSkyLightBuffer()).toBuffer();
const codArr = this.chunksOnDisk.get(chunk.world.dimension);
if (saveType === SaveCompressionType.NONE) {
chunkFileWriter.writeInt(chunkData.length); // Data length
chunkFileWriter.writeBuffer(chunkData); // Chunk data
writeFile(`${this.worldFolderPath}/DIM${chunk.world.dimension}/chunks/${Chunk.CreateCoordPair(chunk.x, chunk.z).toString(16)}.hwc`, chunkFileWriter.toBuffer(), () => {
const cPair = Chunk.CreateCoordPair(chunk.x, chunk.z);
if (!codArr?.includes(cPair)) {
codArr?.push(cPair);
}
resolve(true);
});
} else if (saveType === SaveCompressionType.DEFLATE) {
deflate(chunkData, (err, data) => {
if (err) {
return reject(err);
}
chunkFileWriter.writeInt(data.length);
chunkFileWriter.writeBuffer(data);
writeFile(`${this.worldFolderPath}/DIM${chunk.world.dimension}/chunks/${Chunk.CreateCoordPair(chunk.x, chunk.z).toString(16)}.hwc`, chunkFileWriter.toBuffer(), () => {
const cPair = Chunk.CreateCoordPair(chunk.x, chunk.z);
if (!codArr?.includes(cPair)) {
codArr?.push(cPair);
}
//console.log(`Wrote ${chunk.x},${chunk.z} to disk`);
resolve(true);
});
})
} else if (saveType === SaveCompressionType.XZ) {
// TODO: Implement XZ chunk saving
}
});
}
readChunkFromDisk(world:World, x:number, z:number) {
return new Promise<Chunk>((resolve, reject) => {
readFile(`${this.worldFolderPath}/DIM${world.dimension}/chunks/${Chunk.CreateCoordPair(x, z).toString(16)}.hwc`, (err, data) => {
if (err) {
return reject(err);
}
const chunkFileReader = createReader(Endian.BE, data);
// Check file validity
if (chunkFileReader.readUByte() !== FileMagic.Chunk) {
return reject(new Error("Chunk file is invalid"));
}
const fileVersion = chunkFileReader.readUByte();
if (fileVersion === 0) {
const saveCompressionType:SaveCompressionType = chunkFileReader.readUByte();
const chunkX = chunkFileReader.readUByte();
const chunkY = chunkFileReader.readUByte();
const chunkZ = chunkFileReader.readUByte();
const totalByteSize = chunkX * chunkZ * chunkY;
const contentLength = chunkFileReader.readInt();
if (saveCompressionType === SaveCompressionType.NONE) {
const chunkData = createReader(Endian.BE, chunkFileReader.readBuffer(contentLength));
const chunk = new Chunk(world, x, z, chunkData.readUint8Array(totalByteSize), chunkData.readUint8Array(totalByteSize / 2));
resolve(chunk);
} else if (saveCompressionType === SaveCompressionType.DEFLATE) {
inflate(chunkFileReader.readBuffer(contentLength), (err, data) => {
if (err) {
return reject(err);
}
const chunkData = createReader(Endian.BE, data);
const chunk = new Chunk(world, x, z, chunkData.readUint8Array(totalByteSize), chunkData.readUint8Array(totalByteSize / 2));
resolve(chunk);
});
}
} else if (fileVersion === 1) {
const saveCompressionType:SaveCompressionType = chunkFileReader.readUByte();
const chunkX = chunkFileReader.readUByte();
const chunkY = chunkFileReader.readUByte();
const chunkZ = chunkFileReader.readUByte();
const totalByteSize = chunkX * chunkZ * chunkY;
const contentLength = chunkFileReader.readInt();
if (saveCompressionType === SaveCompressionType.NONE) {
const chunkData = createReader(Endian.BE, chunkFileReader.readBuffer(contentLength));
const chunk = new Chunk(world, x, z, chunkData.readUint8Array(totalByteSize), chunkData.readUint8Array(totalByteSize / 2), chunkData.readUint8Array(totalByteSize / 2), chunkData.readUint8Array(totalByteSize / 2));
resolve(chunk);
} else if (saveCompressionType === SaveCompressionType.DEFLATE) {
inflate(chunkFileReader.readBuffer(contentLength), (err, data) => {
if (err) {
return reject(err);
}
const chunkData = createReader(Endian.BE, data);
const chunk = new Chunk(world, x, z, chunkData.readUint8Array(totalByteSize), chunkData.readUint8Array(totalByteSize / 2), chunkData.readUint8Array(totalByteSize / 2), chunkData.readUint8Array(totalByteSize / 2));
resolve(chunk);
});
}
}
});
});
}
writePlayerSaveToDisk(username:string, playerData:IWriter) {
return new Promise<boolean>((resolve, reject) => {
const playerDataWriter = createWriter(Endian.BE);
playerDataWriter.writeUByte(FileMagic.Player); // File magic
playerDataWriter.writeUByte(0); // File version
playerDataWriter.writeBuffer(playerData.toBuffer()); // Player data
writeFile(`${this.worldPlayerDataFolderPath}/${username}.hpd`, playerDataWriter.toBuffer(), (err) => {
if (err) {
return reject(err);
}
if (!this.playerDataOnDisk.includes(username)) {
this.playerDataOnDisk.push(username);
}
resolve(true);
})
});
}
readPlayerDataFromDisk(username:string) {
return new Promise<IReader>((resolve, reject) => {
readFile(`${this.worldPlayerDataFolderPath}/${username}.hpd`, (err, data) => {
if (err) {
return reject(err);
}
const reader = createReader(Endian.BE, data);
if (reader.readUByte() !== FileMagic.Player) {
return reject(new Error("Player data file is invalid"));
}
const fileVersion = reader.readUByte();
if (fileVersion === 0) {
resolve(reader);
}
});
});
}
}

256
server/blocks/Block.ts Normal file
View file

@ -0,0 +1,256 @@
import AABB from "../AABB";
import BlockBehaviour from "./BlockBehaviour";
import BlockBehaviourSapling from "./BlockBehaviorSapling";
import BlockBehaviourSugarcane from "./BlockBehaviorSugarcane";
import BlockBehaviourClay from "./BlockBehaviourClay";
import BlockBehaviourFlower from "./BlockBehaviourFlower";
import BlockBehaviourGrass from "./BlockBehaviourGrass";
import BlockBehaviourOre from "./BlockBehaviourOre";
import BlockBehaviourRedstoneOre from "./BlockBehaviourRedstoneOre";
import BlockBehaviourStone from "./BlockBehaviourStone";
import BlockBehaviourTallGrass from "./BlockBehaviourTallGrass";
import IBlockBehaviour from "./IBlockBehaviour";
import World from "../World";
abstract class Behaviour {
public static base = new BlockBehaviour();
public static stone = new BlockBehaviourStone();
public static grass = new BlockBehaviourGrass();
public static sapling = new BlockBehaviourSapling();
public static ore = new BlockBehaviourOre();
public static tallGrass = new BlockBehaviourTallGrass();
public static flower = new BlockBehaviourFlower();
public static redstoneOre = new BlockBehaviourRedstoneOre();
public static clay = new BlockBehaviourClay();
public static sugarcane = new BlockBehaviourSugarcane();
}
export default class Block {
public readonly blockId:number;
public static readonly blocks:Array<Block> = new Array<Block>();
public static readonly lightPassage:Array<number> = new Array<number>();
public static readonly lightEmission:Array<number> = new Array<number>();
public static readonly hardness:Array<number> = new Array<number>();
public static readonly blockAABBs:Array<AABB> = new Array<AABB>();
public static readonly blockBehaviours:Array<IBlockBehaviour> = new Array<IBlockBehaviour>();
public static readonly blockNames:Array<string> = new Array<string>();
public constructor(blockId:number) {
Block.blocks[blockId] = this;
Block.lightPassage[blockId] = 0;
Block.lightEmission[blockId] = 0;
Block.blockNames[blockId] = "";
Block.blockBehaviours[blockId] = Behaviour.base;
this.blockId = blockId;
}
public get lightPassage() {
return Block.lightPassage[this.blockId];
}
public set lightPassage(value:number) {
Block.lightPassage[this.blockId] = value;
}
public get lightEmission() {
return Block.lightEmission[this.blockId];
}
public set lightEmission(value:number) {
Block.lightEmission[this.blockId] = value;
}
public get hardness() {
return Block.hardness[this.blockId];
}
public set hardness(value:number) {
Block.hardness[this.blockId] = value;
}
private get blockAABB() {
return Block.blockAABBs[this.blockId];
}
private set blockAABB(value:AABB) {
Block.blockAABBs[this.blockId] = value;
}
public get blockName() {
return Block.blockNames[this.blockId];
}
public set blockName(value:string) {
Block.blockNames[this.blockId] = value;
}
public get behaviour() {
return Block.blockBehaviours[this.blockId];
}
public set behaviour(value:IBlockBehaviour) {
Block.blockBehaviours[this.blockId] = value;
}
public setBehaviour(value:IBlockBehaviour) {
value.block = this;
this.behaviour = value;
return this;
}
public setLightPassage(value:number) {
this.lightPassage = value;
return this;
}
public setLightEmission(value:number) {
this.lightEmission = value;
return this;
}
public setBlockName(value:string) {
this.blockName = value;
return this;
}
public neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number) {
this.behaviour.neighborBlockChange(world, x, y, z, blockId);
}
public droppedItem(blockId:number) {
this.behaviour.droppedItem(blockId);
}
public droppedCount(blockId:number) {
this.behaviour.droppedCount(blockId);
}
public getHardness() {
return this.hardness;
}
public setHardness(value:number) {
this.hardness = value;
return this;
}
public setUnbreakable() {
return this.setHardness(-1);
}
public blockStrength() {
if (this.hardness < 0) {
return 0;
}
// TODO: Check if we can actually harvest a block with current tool
// TODO: Have the 1 be based on current tool ig
return 1 / this.hardness / 100;
}
public getBoundingBox(x:number, y:number, z:number) {
return this.behaviour.getBoundingBox(x, y, z);
}
// Define statics here
static readonly stone = new Block(1).setHardness(1.5).setBehaviour(Behaviour.stone).setBlockName("Stone");
static readonly grass = new Block(2).setHardness(0.6).setBehaviour(Behaviour.grass).setBlockName("Grass");
static readonly dirt = new Block(3).setHardness(0.5).setBlockName("Dirt");
static readonly cobblestone = new Block(4).setHardness(2).setBlockName("Cobblestone");
static readonly planks = new Block(5).setHardness(2).setBlockName("Planks");
static readonly sapling = new Block(6).setHardness(0).setBehaviour(Behaviour.sapling).setBlockName("Sapling");
static readonly bedrock = new Block(7).setUnbreakable().setBlockName("Bedrock");
static readonly waterFlowing = new Block(8).setHardness(100).setLightPassage(128).setBlockName("Flowing Water"); // TODO: Behavior script
static readonly waterStill = new Block(9).setHardness(100).setLightPassage(255).setBlockName("Still Water"); // TODO: Behavior script
static readonly lavaFlowing = new Block(10).setHardness(0).setLightPassage(255).setBlockName("Flowing Lava"); // TODO: Behavior script
static readonly lavaStill = new Block(11).setHardness(100).setBlockName("Still Lava"); // TODO: Behavior script
static readonly sand = new Block(12).setHardness(0.5).setBlockName("Sand");
static readonly gravel = new Block(13).setHardness(0.6).setBlockName("Gravel"); // TODO: Behavior script
static readonly goldOre = new Block(14).setHardness(3).setBehaviour(Behaviour.ore).setBlockName("Gold Ore"); // TODO: Behavior script
static readonly ironOre = new Block(15).setHardness(3).setBehaviour(Behaviour.ore).setBlockName("Iron Ore"); // TODO: Behavior script
static readonly coalOre = new Block(16).setHardness(3).setBehaviour(Behaviour.ore).setBlockName("Coal Ore"); // TODO: Behavior script
static readonly wood = new Block(17).setHardness(2).setBlockName("Wood");
static readonly leaves = new Block(18).setHardness(0.2).setLightPassage(240).setBlockName("Leaves");
static readonly sponge = new Block(19).setHardness(0.6).setBlockName("Sponge");
static readonly glass = new Block(20).setHardness(0.3).setLightPassage(255).setBlockName("Glass"); // TODO: Behavior script
static readonly lapisOre = new Block(21).setHardness(3).setBehaviour(Behaviour.ore).setBlockName("Lapis Ore"); // TODO: Behavior script
static readonly lapisBlock = new Block(22).setHardness(3).setBlockName("Lapis Block");
static readonly dispenser = new Block(23).setHardness(3.5).setBlockName("Dispenser");
static readonly sandStone = new Block(24).setHardness(0.8).setBlockName("Sand Stone");
static readonly noteblock = new Block(25).setHardness(0.8).setBlockName("Noteblock");
static readonly bed = new Block(26).setHardness(0.2).setBlockName("Bed"); // TODO: Behavior script
static readonly poweredRail = new Block(27).setHardness(0.7).setBlockName("Powered Rail"); // TODO: Behavior script
static readonly detectorRail = new Block(28).setHardness(0.7).setBlockName("Detector Rail"); // TODO: Behavior script
static readonly stickyPistonBase = new Block(29).setBlockName("Sticky Piston Base"); // TODO: Behavior script
static readonly web = new Block(30).setHardness(4).setLightPassage(255).setBlockName("Web"); // TODO: Behavior script
static readonly tallGrass = new Block(31).setHardness(0).setLightPassage(255).setBehaviour(Behaviour.tallGrass).setBlockName("Tall Grass");
static readonly deadBush = new Block(32).setHardness(0).setLightPassage(255).setBehaviour(Behaviour.tallGrass).setBlockName("Dead Bush"); // TODO: Give it's own behavior script
static readonly pistonBase = new Block(33).setBlockName("Piston Base"); // TODO: Behavior script
static readonly pistonExtension = new Block(34).setBlockName("Piston Extension"); // TODO: Behavior script?
static readonly wool = new Block(35).setHardness(0.8).setBlockName("Wool"); // TODO: Behavior script?
static readonly pistonMoving = new Block(36).setBlockName("Piston Move Event Block Why Is This A Block"); // TODO: Behavior script
static readonly flowerDandelion = new Block(37).setHardness(0).setLightPassage(255).setBehaviour(Behaviour.flower).setBlockName("Dandelion");
static readonly flowerRose = new Block(38).setHardness(0).setLightPassage(255).setBehaviour(Behaviour.flower).setBlockName("Rose");
static readonly brownMushroom = new Block(39).setHardness(0).setLightEmission(0.125).setBlockName("Brown Mushroom"); // TODO: Behavior script
static readonly redMushroom = new Block(40).setHardness(0).setBlockName("Red Mushroom"); // TODO: Behavior script
static readonly goldBlock = new Block(41).setHardness(3).setBlockName("Gold Block");
static readonly ironBlock = new Block(42).setHardness(5).setBlockName("Iron Block");
static readonly doubleSlab = new Block(43).setHardness(2).setBlockName("Slab"); // TODO: Behavior script
static readonly singleSlab = new Block(44).setHardness(2).setBlockName("Slab"); // TODO: Behavior script
static readonly brick = new Block(45).setHardness(2).setBlockName("Brick");
static readonly tnt = new Block(46).setHardness(0).setBlockName("TNT"); // TODO: Behavior script
static readonly bookshelf = new Block(47).setHardness(1.5).setBlockName("Bookshelf"); // TODO: Behavior script
static readonly mossyCobblestone = new Block(48).setHardness(2).setBlockName("Mossy Cobblestone");
static readonly obsidian = new Block(49).setHardness(10).setBlockName("Obsidian"); // TODO: Behavior script?
static readonly torch = new Block(50).setHardness(0).setLightEmission(0.9).setBlockName("Torch"); // TODO: Behavior script
static readonly fire = new Block(51).setHardness(0).setLightEmission(1).setBlockName("Fire"); // TODO: Behavior script
static readonly mobSpawner = new Block(52).setHardness(5).setBlockName("Mob Spawner"); // TODO: Behavior script
static readonly woodenStairs = new Block(53).setBlockName("Wooden Stairs"); // TODO: Behavior script
static readonly chest = new Block(54).setHardness(2.5).setBlockName("Chest"); // TODO: Behavior script
static readonly redstoneDust = new Block(55).setHardness(0).setBlockName("Redstone Dust"); // TODO: Behavior script
static readonly diamondOre = new Block(56).setHardness(3).setBlockName("Diamond Ore"); // TODO: Behavior script
static readonly diamondBlock = new Block(57).setHardness(5).setBlockName("Diamond Block"); // TODO: Behavior script
static readonly craftingTable = new Block(58).setHardness(2.5).setBlockName("Crafting Table"); // TODO: Behavior script
static readonly wheatCrop = new Block(59).setHardness(0).setBlockName("Wheet Crop"); // TODO: Behavior script
static readonly farmland = new Block(60).setHardness(0.6).setBlockName("Farmland"); // TODO: Behavior script
static readonly furnaceIdle = new Block(61).setHardness(3.5).setBlockName("Furnace"); // TODO: Behavior script
static readonly furnaceActive = new Block(62).setHardness(3.5).setBlockName("Furnace"); // TODO: Behavior script
static readonly sign = new Block(63).setHardness(1).setBlockName("Sign"); // TODO: Behavior script
static readonly woodenDoor = new Block(64).setHardness(3).setBlockName("Wooden Door"); // TODO: Behavior script
static readonly ladder = new Block(65).setHardness(0.4).setBlockName("Ladder"); // TODO: Behavior script
static readonly rail = new Block(66).setHardness(0.7).setBlockName("Rail"); // TODO: Behavior script
static readonly cobblestoneStairs = new Block(67).setBlockName("Cobblestone Stairs"); // TODO: Behavior script
static readonly signWall = new Block(68).setHardness(1).setBlockName("Sign"); // TODO: Behavior script
static readonly lever = new Block(69).setHardness(0.5).setBlockName("Lever"); // TODO: Behavior script
static readonly stonePressurePlate = new Block(70).setHardness(0.5).setBlockName("Stone Pressure Plate"); // TODO: Behavior script
static readonly ironDoor = new Block(71).setHardness(5).setBlockName("Iron Door"); // TODO: Behavior script
static readonly woodenPressurePlate = new Block(72).setHardness(0.5).setBlockName("Wooden Pressure Plate"); // TODO: Behavior script
static readonly redstoneOre = new Block(73).setHardness(3).setBehaviour(Behaviour.redstoneOre).setBlockName("Redstone Ore"); // TODO: Behavior script
static readonly redstoneOreGlowing = new Block(74).setHardness(3).setLightEmission(0.625).setBehaviour(Behaviour.redstoneOre).setBlockName("Redstone Ore"); // TODO: Behavior script
static readonly redstoneTorchIdle = new Block(75).setHardness(0).setBlockName("Redstone Torch"); // TODO: Behavior script
static readonly redstoneTorchActive = new Block(76).setHardness(0).setLightEmission(0.5).setBlockName("Redstone Torch"); // TODO: Behavior script
static readonly button = new Block(77).setHardness(0.5).setBlockName("Button"); // TODO: Behavior script
static readonly snow = new Block(78).setHardness(0.1).setBlockName("Snow Layer"); // TODO: Behavior script
static readonly ice = new Block(79).setHardness(0.5).setLightPassage(128).setBlockName("Ice"); // TODO: Behavior script
static readonly snowBlock = new Block(80).setHardness(0.2).setBlockName("Snow"); // TODO: Behavior script
static readonly cactus = new Block(81).setHardness(0.4).setBlockName("Cactus"); // TODO: Behavior script
static readonly clay = new Block(82).setHardness(0.6).setBehaviour(Behaviour.clay).setBlockName("Clay");
static readonly sugarcane = new Block(83).setHardness(0).setLightPassage(255).setBehaviour(Behaviour.sugarcane).setBlockName("Sugar Cane"); // TODO: Behavior script
static readonly jukebox = new Block(84).setHardness(2).setBlockName("Jukebox"); // TODO: Behavior script
static readonly fence = new Block(85).setHardness(2).setBlockName("Fence"); // TODO: Behavior script
static readonly pumpkin = new Block(86).setHardness(1).setBlockName("Pumpkin");
static readonly netherrack = new Block(87).setHardness(0.4).setBlockName("Netherrack");
static readonly soulSand = new Block(88).setHardness(0.5).setBlockName("Soul Sand");
static readonly glowStone = new Block(89).setHardness(0.3).setBlockName("Glowstone"); // TODO: Behavior script
static readonly netherPortal = new Block(90).setUnbreakable().setLightEmission(0.75).setBlockName("Nether Portal"); // TODO: Behavior script
static readonly jackOLantern = new Block(91).setHardness(1).setLightEmission(1).setBlockName("Jack O' Lantern");
static readonly cake = new Block(92).setHardness(0.5).setBlockName("Cake"); // TODO: Behavior script
static readonly redstoneRepeaterIdle = new Block(93).setHardness(0).setBlockName("Redstone Repeater"); // TODO: Behavior script
static readonly redstoneRepeaterActive = new Block(94).setHardness(0).setLightEmission(0.625).setBlockName("Redstone Repeater"); // TODO: Behavior script
static readonly aprilFoolsLockedChest = new Block(95).setHardness(0).setLightEmission(1).setBlockName("Locked Chest");
static readonly trapdoor = new Block(96).setHardness(3).setBlockName("Trapdoor"); // TODO: Behavior script
}

View file

@ -0,0 +1,16 @@
import BlockBehaviour from "./BlockBehaviour";
import Random from "../Random";
import World from "../World";
export default class BlockBehaviourSapling extends BlockBehaviour {
public randomTick(world:World, x:number, y:number, z:number, random:Random) {
if (world.getBlockLight(x, y + 1, z) >= 9 && random.nextInt(30) === 0) {
const blockMetadata = world.getBlockMetadata(x, y, z);
if ((blockMetadata & 8) === 0) {
world.setBlockMetadataWithNotify(x, y, z, blockMetadata | 8);
} else {
console.log("UNIMPLEMENTED TREE GROW!!");
}
}
}
}

View file

@ -0,0 +1,46 @@
import AABB from "../AABB";
import Block from "./Block";
import BlockBehaviour from "./BlockBehaviour";
import World from "../World";
export default class BlockBehaviourSugarcane extends BlockBehaviour {
public neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number) {
const block = world.getBlockId(x, y - 1, z);
if (block === 0 || block !== Block.sugarcane.blockId) {
world.setBlockWithNotify(x, y, z, 0);
}
}
public canPlaceBlockAt(world: World, x: number, y: number, z: number) {
const blockBelow = world.getBlockId(x, y - 1, z);
if (blockBelow === this.block.blockId) {
return true;
}
// Check if right ground block
if (blockBelow !== Block.grass.blockId && blockBelow !== Block.dirt.blockId) {
return false;
}
// Check if water is around
if (world.getBlockId(x - 1, y - 1, z) === Block.waterStill.blockId) {
return true;
}
if (world.getBlockId(x + 1, y - 1, z) === Block.waterStill.blockId) {
return true;
}
if (world.getBlockId(x, y - 1, z - 1) === Block.waterStill.blockId) {
return true;
}
if (world.getBlockId(x, y - 1, z + 1) === Block.waterStill.blockId) {
return true;
}
return false;
}
public getBoundingBox() {
return AABB.getAABB(0, 0, 0, 0, 0, 0);
}
}

View file

@ -0,0 +1,16 @@
import AABB from "../AABB";
import Block from "./Block";
import IBlockBehaviour from "./IBlockBehaviour";
import Random from "../Random";
import World from "../World";
export default class BlockBehaviour implements IBlockBehaviour {
public block!:Block;
public neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number) {}
public droppedItem(blockId:number) { return blockId; }
public droppedCount(blockId:number) { return 1; }
public getBoundingBox(x:number, y:number, z:number) { return AABB.getAABB(0 + x, 0 + y, 0 + z, 1 + x, 1 + y, 1 + z); }
public randomTick(world:World, x:number, y:number, z:number, random:Random) {}
public canPlaceBlockAt(world:World, x:number, y:number, z:number) { return true; }
}

View file

@ -0,0 +1,12 @@
import BlockBehaviour from "./BlockBehaviour";
import Item from "../items/Item";
export default class BlockBehaviourClay extends BlockBehaviour {
public droppedItem(blockId:number) {
return Item.clay.shiftedItemID;
}
public droppedCount(blockId:number) {
return 4;
}
}

View file

@ -0,0 +1,17 @@
import AABB from "../AABB";
import Block from "./Block";
import BlockBehaviour from "./BlockBehaviour";
import World from "../World";
export default class BlockBehaviourFlower extends BlockBehaviour {
public neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number) {
const block = world.getBlockId(x, y - 1, z);
if (block !== Block.grass.blockId && block !== Block.dirt.blockId) {
world.setBlockWithNotify(x, y, z, 0);
}
}
public getBoundingBox() {
return AABB.getAABB(0, 0, 0, 0, 0, 0);
}
}

View file

@ -0,0 +1,8 @@
import Block from "./Block";
import BlockBehaviour from "./BlockBehaviour";
export default class BlockBehaviourGrass extends BlockBehaviour {
public droppedItem(blockId:number) {
return Block.dirt.blockId;
}
}

View file

@ -0,0 +1,12 @@
import BlockBehaviour from "./BlockBehaviour";
import Item from "../items/Item";
export default class BlockBehaviourOre extends BlockBehaviour {
public droppedItem(blockId:number) {
return Item.clay.shiftedItemID;
}
public droppedCount(blockId:number) {
return 4;
}
}

View file

@ -0,0 +1,12 @@
import BlockBehaviour from "./BlockBehaviour";
import Item from "../items/Item";
export default class BlockBehaviourRedstoneOre extends BlockBehaviour {
public droppedItem(blockId:number) {
return Item.clay.shiftedItemID;
}
public droppedCount(blockId:number) {
return 4;
}
}

View file

@ -0,0 +1,8 @@
import Block from "./Block";
import BlockBehaviour from "./BlockBehaviour";
export default class BlockBehaviourStone extends BlockBehaviour {
public droppedItem(blockId:number) {
return Block.cobblestone.blockId;
}
}

View file

@ -0,0 +1,21 @@
import AABB from "../AABB";
import Block from "./Block";
import BlockBehaviour from "./BlockBehaviour";
import World from "../World";
export default class BlockBehaviourTallGrass extends BlockBehaviour {
public neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number) {
const block = world.getBlockId(x, y - 1, z);
if (block !== Block.grass.blockId && block !== Block.dirt.blockId) {
world.setBlockWithNotify(x, y, z, 0);
}
}
public droppedItem(blockId:number) {
return -1;
}
public getBoundingBox() {
return AABB.getAABB(0, 0, 0, 0, 0, 0);
}
}

View file

@ -0,0 +1,15 @@
import AABB from "../AABB";
import Block from "./Block";
import Random from "../Random";
import World from "../World";
export default interface IBlockBehaviour {
block:Block,
neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number): void,
droppedItem: (blockId:number) => number,
droppedCount: (blockId:number) => number,
getBoundingBox: (x:number, y:number, z:number) => AABB,
randomTick: (world:World, x:number, y:number, z:number, random:Random) => void,
canPlaceBlockAt: (world:World, x:number, y:number, z:number) => boolean
}

View file

@ -1,225 +0,0 @@
/*
===========- bufferStuff.js -===========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
module.exports.Writer = class {
// bufferStuff supports pre-allocating memory for the buffer
// if you pass in a size of 0 then it will just dynamicly allocate at the
// cost of performance
// NOTE: In pre-allocation mode if you exceed the size of the buffer
// that you set it will cause a crash.
constructor(size = 0) {
this.buffer = Buffer.alloc(size);
this.offset = 0;
this.baseSize = size;
}
reset() {
this.buffer = Buffer.alloc(0);
}
writeBuffer(buff = Buffer.alloc(0)) {
this.buffer = Buffer.concat([this.buffer, buff], this.buffer.length + buff.length);
}
writeBool(data = false) {
this.writeByte(data ? 1 : 0);
}
// NOTE: Currently writing a nibble requires you to write both halves at the same time.
writeNibble(nibble1 = 0, nibble2 = 0) {
this.writeUByte(nibble1 | (nibble2 << 4));
}
writeByte(data = 0) {
if (this.baseSize == 0) {
const buff = Buffer.alloc(1);
buff.writeInt8(data, 0);
this.writeBuffer(buff);
} else {
this.buffer.writeInt8(data, this.offset);
this.offset += 1;
}
}
writeUByte(data = 0) {
if (this.baseSize == 0) {
const buff = Buffer.alloc(1);
buff.writeUInt8(data, 0);
this.writeBuffer(buff);
} else {
this.buffer.writeUInt8(data, this.offset);
this.offset += 1;
}
}
writeByteArray(data = [0]) {
if (this.baseSize == 0) {
const buff = Buffer.alloc(data.length);
for (let byte of data) {
buff.writeInt8(byte);
}
this.writeBuffer(buff);
} else {
for (let byte of data) {
this.buffer.writeInt8(byte);
this.offset += 1;
}
}
}
writeShort(data = 0) {
if (this.baseSize == 0) {
const buff = Buffer.alloc(2);
buff.writeIntBE(data, 0, 2);
this.writeBuffer(buff);
} else {
this.buffer.writeIntBE(data, this.offset, 2);
this.offset += 2;
}
}
writeShortArray(data = [0]) {
if (this.baseSize == 0) {
const buff = Buffer.alloc(data.length * 2);
let offset = 0;
for (let short of data) {
buff.writeIntBE(short, offset, 2);
offset += 2;
}
this.writeBuffer(buff);
} else {
for (let short of data) {
this.buffer.writeIntBE(short, this.offset, 2);
this.offset += 2;
}
}
}
writeInt(data = 0) {
if (this.baseSize == 0) {
const buff = Buffer.alloc(4);
buff.writeIntBE(data, 0, 4);
this.writeBuffer(buff);
} else {
this.buffer.writeIntBE(data, this.offset, 4);
this.offset += 4;
}
}
writeLong(data = 0) {
if (this.baseSize == 0) {
const buff = Buffer.alloc(8);
if (data instanceof BigInt) buff.writeBigInt64BE(data, 0);
else buff.writeBigInt64BE(BigInt(data), 0);
this.writeBuffer(buff);
} else {
if (data instanceof BigInt) this.buffer.writeBigInt64BE(data, this.offset);
else this.buffer.writeBigInt64BE(BigInt(data), this.offset);
this.offset += 8;
}
}
writeFloat(data = 0.0) {
if (this.baseSize == 0) {
const buff = Buffer.alloc(4);
buff.writeFloatBE(data, 0);
this.writeBuffer(buff);
} else {
this.buffer.writeFloatBE(data, this.offset);
this.offset += 4;
}
}
writeDouble(data = 0.0) {
if (this.baseSize == 0) {
const buff = Buffer.alloc(8);
buff.writeDoubleBE(data, 0);
this.writeBuffer(buff);
} else {
this.buffer.writeDoubleBE(data, this.offset);
this.offset += 8;
}
}
writeString(string = "") {
this.writeShort(string.length);
for (let i = 0; i < string.length; i++) {
this.writeShort(string.charCodeAt(i));
}
}
}
module.exports.Reader = class {
constructor(buffer = Buffer.alloc(0)) {
this.buffer = buffer;
this.offset = 0;
}
readBool() {
return this.readByte() == 0x01 ? true : false;
}
readByte() {
const data = this.buffer.readInt8(this.offset);
this.offset += 1;
return data;
}
readShort() {
const data = this.buffer.readIntBE(this.offset, 2);
this.offset += 2;
return data;
}
readInt() {
const data = this.buffer.readIntBE(this.offset, 4);
this.offset += 4;
return data;
}
readLong() {
const data = this.buffer.readBigInt64BE(this.offset);
this.offset += 8;
return data;
}
readFloat() {
const data = this.buffer.readFloatBE(this.offset);
this.offset += 4;
return data;
}
readDouble() {
const data = this.buffer.readDoubleBE(this.offset);
this.offset += 8;
return data;
}
readString() {
const length = this.readShort();
let data = "";
for (let i = 0; i < length; i++) {
data += String.fromCharCode(this.readShort());
}
return data;
}
}

View file

@ -1,130 +0,0 @@
/*
===========- chunkManager.js -==========
Created by Holly (tgpethan) (c) 2021
Licenced under MIT
========================================
*/
const { Worker } = require('worker_threads');
const FunkyArray = require("./Util/funkyArray.js");
const pRandom = require("./Util/prettyRandom.js");
const config = require("../config.json");
const workerPath = `${__dirname}/Workers/ChunkWorker.js`;
module.exports = class {
constructor() {
this.chunks = {};
this.queuedBlockUpdates = new FunkyArray();
global.generatingChunks = true;
this.threadPool = [];
this.workPool = new FunkyArray();
this.toRemove = [];
// TODO: Figure out a better way of doing this?
this.seed = pRandom(-2147483647, 2147483647) - new Date().getTime() * pRandom(1, 6);
// WoAh!!! Thread pool in js!?!??!???!11!?!?!
for (let i = 0; i < config.worldThreads; i++) {
const worker = new Worker(workerPath);
this.threadPool.push([false, worker]);
const myID = i;
worker.on("message", (data) => {
let user;
switch (data[0]) {
case "chunk":
user = global.getUserByKey(data[2]);
user.chunksToSend.add(Buffer.from(data[1]));
this.toRemove.push(data[1]);
this.threadPool[myID][0] = false;
break;
case "generate":
this.chunks[data[2]][data[3]] = data[1];
this.toRemove.push(data[4]);
const treeBlocksRef = data[5];
treeBlocksRef.forEach((block) => {
this.setBlock(block[0], block[1], block[2], block[3], 0, false);
});
this.queuedBlockUpdates.regenerateIterableArray();
this.threadPool[myID][0] = false;
break;
}
});
}
console.log("Created thread pool with " + this.threadPool.length + " threads");
setInterval(() => {
if (this.workPool.getLength() > 0) {
let limit = Math.min(this.workPool.getLength(), this.threadPool.length);
for (let i = 0; i < limit; i++) {
for (let i1 = 0; i1 < this.threadPool.length; i1++) {
let thread = this.threadPool[i1];
if (!thread[0]) {
const key = this.workPool.itemKeys[i];
const item = this.workPool.getByKey(key);
// Already being processed
if (item == null) break;
if (item[0] == true) {
limit += 1;
break;
}
item[0] = true;
item[1][3] = key;
thread[1].postMessage(item[1]);
thread[0] = true;
break;
}
}
}
for (let item of this.toRemove) {
//console.log("removing item " + this.workPool.getByKey(item)[0]);
this.workPool.remove(item);
}
this.toRemove = [];
}
}, 1000 / 60);
global.generatingChunks = false;
}
// TODO: Store metadata!
createChunk(cx = 0, cz = 0) {
if (this.chunks[cx] == null) this.chunks[cx] = {};
this.workPool.add([false, ["generate", cx, cz, null, this.seed]]);
}
chunkExists(cx = 0, cz = 0) {
if (this.chunks[cx] == null) return false;
if (this.chunks[cx][cz] == null) return false;
// In any other case the chunk exists
return true;
}
multiBlockChunk(chunkX = 0, chunkZ = 0, user) {
this.workPool.add([false, ["chunk", [chunkX, chunkZ, this.chunks[chunkX][chunkZ]], user.id, null]]);
}
setBlock(x = 0, y = 0, z = 0, id = 0, metadata = 0, regenerate = true) {
if (y < 0 || y > 127) return console.error("Tried to set a block outside of the world!");
const chunkX = x >> 4;
const chunkZ = z >> 4;
const blockX = x - (chunkX << 4);
const blockZ = z - (chunkZ << 4);
// Don't queue a block update if that block is already this block (wow those ifs)
if (this.chunks[chunkX] != null)
if (this.chunks[chunkX][chunkZ] != null)
if (this.chunks[chunkX][chunkZ][y][blockX][blockZ] == id) return;
this.queuedBlockUpdates.add([chunkX, chunkZ, y, blockX, blockZ, id, metadata], regenerate);
}
}

314
server/entities/Entity.ts Normal file
View file

@ -0,0 +1,314 @@
import { IReader, IWriter } from "bufferstuff";
import { MetadataEntry, MetadataWriter } from "../MetadataWriter";
import AABB from "../AABB";
import Block from "../blocks/Block";
import Chunk from "../Chunk";
import IEntity from "./IEntity";
import MetadataFieldType from "../enums/MetadataFieldType";
import PacketEntityLook from "../packets/EntityLook";
import PacketEntityLookRelativeMove from "../packets/EntityLookRelativeMove";
import PacketEntityMetadata from "../packets/EntityMetadata";
import PacketEntityRelativeMove from "../packets/EntityRelativeMove";
import PacketEntityTeleport from "../packets/EntityTeleport";
import PacketEntityVelocity from "../packets/EntityVelocity";
import Player from "./Player";
import Rotation from "../Rotation";
import Vec2 from "../Vec2";
import Vec3 from "../Vec3";
import World from "../World";
export default class Entity implements IEntity {
public static nextEntityId:number = 0;
public entityId:number;
public entitySize:Vec2;
public world:World;
public position:Vec3;
public lastPosition:Vec3;
public absPosition:Vec3;
public lastAbsPosition:Vec3;
public motion:Vec3;
private moveEntityBlockPosRel:Vec3;
public rotation:Rotation;
public lastRotation:Rotation;
public absRotation:Rotation;
public lastAbsRotation:Rotation;
public health:number;
public wasHurt:boolean;
public isDead:boolean;
public fire:number;
public fallDistance:number;
public onGround:boolean;
public chunk:Chunk;
public crouching:boolean;
private lastCrouchState:boolean;
private lastFireState:boolean;
public entityAABB:AABB;
public readonly isPlayer:boolean;
private queuedChunkUpdate:boolean;
public markedForDisposal:boolean = false;
public constructor(world:World, isPlayer:boolean = false) {
this.entityId = Entity.nextEntityId++;
this.isPlayer = isPlayer;
this.entitySize = new Vec2(0.6, 1.8);
this.entityAABB = new AABB(-this.entitySize.x / 2, 0, -this.entitySize.x / 2, this.entitySize.x / 2, this.entitySize.y, this.entitySize.x / 2);
this.fire = this.fallDistance = 0;
this.onGround = false;
this.world = world;
this.position = new Vec3();
this.lastPosition = new Vec3();
this.absPosition = new Vec3();
this.lastAbsPosition = new Vec3();
this.motion = new Vec3();
this.moveEntityBlockPosRel = new Vec3();
this.rotation = new Rotation();
this.lastRotation = new Rotation();
this.absRotation = new Rotation();
this.lastAbsRotation = new Rotation();
this.crouching = this.lastCrouchState = this.lastFireState = this.queuedChunkUpdate = false;
this.chunk = world.getChunk(this.position.x >> 4, this.position.z >> 4);
this.health = 20;
this.wasHurt = false;
this.isDead = false;
}
public fromSave(reader:IReader) {
this.position.set(reader.readDouble(), reader.readDouble(), reader.readDouble());
this.motion.set(reader.readFloat(), reader.readFloat(), reader.readFloat());
this.rotation.set(reader.readFloat(), reader.readFloat());
this.fire = reader.readShort();
this.fallDistance = reader.readFloat();
this.health = reader.readByte();
}
public toSave(writer:IWriter) {
writer.writeDouble(this.position.x).writeDouble(this.position.y).writeDouble(this.position.z) // Position
.writeFloat(this.motion.x).writeFloat(this.motion.y).writeFloat(this.motion.z) // Motion
.writeFloat(this.rotation.x).writeFloat(this.rotation.y) // Rotation
.writeShort(this.fire)
.writeFloat(this.fallDistance)
.writeByte(this.health);
}
async collidesWithPlayer(aabb:AABB) {
let collidedWith:Player | undefined;
await this.world.players.forEach(player => {
if (this.entityAABB.intersects(player.entityAABB) && collidedWith == undefined) {
collidedWith = player;
}
});
return collidedWith;
}
sendToNearby(buffer:Buffer) {
this.world.sendToNearbyClients(this, buffer);
}
sendToAllNearby(buffer:Buffer) {
this.world.sendToNearbyAllNearbyClients(this, buffer);
}
updateMetadata() {
const crouchStateChanged = this.crouching !== this.lastCrouchState;
const fireStateChanged = this.fire > 0 !== this.lastFireState;
if (crouchStateChanged || fireStateChanged) {
const metadata = new MetadataWriter();
// Flags:
// 1 = On Fire
// 2 = Player crouched
// 4 = Player on mount?
metadata.addMetadataEntry(0, new MetadataEntry(MetadataFieldType.Byte, Number(this.fire > 0) + Number(this.crouching) * 2));
this.sendToNearby(new PacketEntityMetadata(this.entityId, metadata.writeBuffer()).writeData());
this.lastCrouchState = this.crouching;
this.lastFireState = this.fire > 0;
}
}
distanceTo(entity:IEntity) {
const dX = entity.position.x - this.position.x,
dY = entity.position.y - this.position.y,
dZ = entity.position.z - this.position.z;
return Math.sqrt(Math.pow(dX, 2) + Math.pow(dY, 2) + Math.pow(dZ, 2));
}
damageFrom(damage:number, entity?:IEntity) {
if (this.health <= 0) {
return;
}
if (entity === undefined) {
this.health -= damage;
} else {
this.health -= damage;
}
this.wasHurt = true;
}
updateEntityChunk() {
const bitX = this.position.x >> 4;
const bitZ = this.position.z >> 4;
if (bitX != this.lastPosition.x >> 4 || bitZ != this.lastPosition.z >> 4 || this.queuedChunkUpdate) {
if (this.world.chunkExists(bitX, bitZ)) {
this.chunk = this.world.getChunk(bitX, bitZ);
this.queuedChunkUpdate = false;
} else {
this.queuedChunkUpdate = true;
}
}
}
private constrainRot(rot:number) {
return Math.min(Math.max(rot, -128), 127);
}
private sendPositionUpdate() {
this.absPosition.set(Math.floor(this.position.x * 32), Math.floor(this.position.y * 32), Math.floor(this.position.z * 32));
const yaw = this.rotation.yaw / 256 * 180;
const pitch = this.rotation.pitch / 256 * 180;
this.absRotation.set(
Math.floor(yaw - Math.floor((yaw + 128) / 256) * 256),
Math.floor(pitch - Math.floor((pitch + 128) / 256) * 256)
);
const diffX = this.absPosition.x - this.lastAbsPosition.x;
const diffY = this.absPosition.y - this.lastAbsPosition.y;
const diffZ = this.absPosition.z - this.lastAbsPosition.z;
const diffYaw = this.absRotation.yaw - this.lastAbsRotation.yaw;
const diffPitch = this.absRotation.pitch - this.lastAbsRotation.pitch;
const doRelativeMove = Math.abs(diffX) >= 4 || Math.abs(diffY) >= 4 || Math.abs(diffZ) >= 4;
const doLook = Math.abs(diffYaw) >= 4 || Math.abs(diffPitch) >= 4;
if (Math.abs(diffX) > 128 || Math.abs(diffY) > 128 || Math.abs(diffZ) > 128) {
this.sendToNearby(new PacketEntityTeleport(this.entityId, this.absPosition.x, this.absPosition.y, this.absPosition.z, this.absRotation.yaw, this.absRotation.pitch).writeData());
} else if (doRelativeMove && doLook) {
this.sendToNearby(new PacketEntityLookRelativeMove(this.entityId, diffX, diffY, diffZ, this.absRotation.yaw, this.absRotation.pitch).writeData());
} else if (doRelativeMove) {
this.sendToNearby(new PacketEntityRelativeMove(this.entityId, diffX, diffY, diffZ).writeData());
} else if (doLook) {
this.sendToNearby(new PacketEntityLook(this.entityId, this.absRotation.yaw, this.absRotation.pitch).writeData());
}
if (!this.motion.isZero) {
this.sendToNearby(new PacketEntityVelocity(this.entityId, this.motion.x, this.motion.y, this.motion.z).writeData());
}
if (doRelativeMove) {
this.lastAbsPosition.set(this.absPosition);
}
if (doLook) {
this.lastAbsRotation.set(this.absRotation);
}
}
fall(distance:number) {
// TODO: Entity falling mount transfer
}
kill() {
this.health = 0;
this.markedForDisposal = true;
}
updateFalling(distance:number) {
if (this.onGround) {
if (this.fallDistance > 0) {
this.fall(this.fallDistance);
this.fallDistance = 0;
}
} else if (distance < 0) {
this.fallDistance -= distance;
}
}
private getBlockAABBFor(x:number, y:number, z:number) {
const blockId = this.chunk.getBlockId(x, y, z);
const blockEntityIsTouching = blockId > 0 ? Block.blocks[blockId] : null;
if (blockEntityIsTouching != null) {
return blockEntityIsTouching.getBoundingBox(Math.floor(this.position.x), Math.floor(this.position.y), Math.floor(this.position.z));
}
return null;
}
moveEntity(motionX:number, motionY:number, motionZ:number) {
this.position.add(motionX, motionY, motionZ);
this.entityAABB.move(this.position);
let blockAABB = this.getBlockAABBFor(Math.floor(this.position.x) & 0xf, Math.floor(this.position.y), Math.floor(this.position.z) & 0xf);
if (blockAABB !== null) {
this.moveEntityBlockPosRel.set(this.position);
this.moveEntityBlockPosRel.sub(Math.floor(this.position.x), Math.floor(this.position.y), Math.floor(this.position.z));
// TODO: Handle X and Z collisions.
if (this.entityAABB.intersects(blockAABB)) {
const intersectionY = this.entityAABB.intersectionY(blockAABB);
if (this.moveEntityBlockPosRel.y > 0.5) {
motionY = intersectionY;
} else {
motionY = -intersectionY;
}
}
this.position.add(0, motionY, 0);
this.motion.y = 0;
this.onGround = true;
}
}
onTick() {
this.updateMetadata();
this.updateEntityChunk();
this.updateFalling(this.motion.y);
if (this.fire > 0) {
if (this.fire % 20 === 0) {
this.damageFrom(1);
}
this.fire--;
}
if (!this.isDead && this.health <= 0) {
this.isDead = true;
}
if (this.wasHurt) {
this.wasHurt = false;
}
this.sendPositionUpdate();
this.lastPosition.set(this.position);
}
}

View file

@ -0,0 +1,61 @@
import Entity from "./Entity";
import ItemStack from "../inventories/ItemStack";
import World from "../World";
export default class EntityItem extends Entity {
public age:number;
public itemStack:ItemStack;
public pickupDelay:number;
public constructor(world:World, itemStack:ItemStack) {
super(world);
this.itemStack = itemStack;
this.entitySize.set(0.2, 0.2);
this.pickupDelay = 0;
this.motion.set(Math.random() * 0.2 - 0.1, 0.2, Math.random() * 0.2 - 0.1);
this.age = 0;
this.health = 5;
}
async onTick() {
super.onTick();
if (this.pickupDelay > 0) {
this.pickupDelay--;
} else {
let playerCollided = await this.collidesWithPlayer(this.entityAABB);
if (playerCollided !== undefined) {
playerCollided.inventory.addItemStack(this.itemStack);
playerCollided.itemPickup(this, this.itemStack.size);
if (this.itemStack.size <= 0) {
this.kill();
}
}
}
this.motion.add(0, -0.04, 0);
this.moveEntity(this.motion.x, this.motion.y, this.motion.z);
let xyMult = 0.98;
if (this.onGround) {
xyMult = 0.59;
}
// TODO: Change the x and z based on the slipperiness of a block
this.motion.mult(xyMult, 0.98, xyMult);
if (this.onGround) {
this.motion.y *= -0.5;
}
this.age++;
if (this.age >= 6000) {
this.kill();
}
}
}

View file

@ -0,0 +1,84 @@
import { IReader, IWriter } from "bufferstuff";
import Block from "../blocks/Block";
import Entity from "./Entity";
import EntityStatus from "../enums/EntityStatus";
import IEntity from "./IEntity";
import PacketEntityStatus from "../packets/EntityStatus";
import World from "../World";
export default class EntityLiving extends Entity {
public timeInWater:number;
public headHeight:number;
public lastHealth:number;
public constructor(world:World, isPlayer:boolean = false) {
super(world, isPlayer);
this.timeInWater = 0;
this.headHeight = 1.62;
this.lastHealth = this.health;
}
public fromSave(reader:IReader) {
super.fromSave(reader);
this.timeInWater = reader.readShort();
}
public toSave(writer:IWriter) {
super.toSave(writer);
writer.writeShort(this.timeInWater == Number.MIN_SAFE_INTEGER ? 0 : this.timeInWater);
}
damageFrom(damage:number, entity?:IEntity) {
super.damageFrom(damage, entity);
if (this.health <= 0) {
this.isDead = true;
}
// Send Damage Animation packet or death packet
if (this.isDead) {
this.sendToAllNearby(new PacketEntityStatus(this.entityId, EntityStatus.Dead).writeData());
} else {
this.sendToAllNearby(new PacketEntityStatus(this.entityId, EntityStatus.Hurt).writeData());
}
}
isInWater(fromHead:boolean) {
return this.world.getChunkBlockId(this.chunk, this.position.x, this.position.y + (fromHead ? this.headHeight : 0), this.position.z) === Block.waterStill.blockId;
}
fall(distance:number) {
const adjustedFallDistance = Math.ceil(distance - 3);
if (adjustedFallDistance > 0) {
this.damageFrom(adjustedFallDistance);
}
}
onTick() {
super.onTick();
if (!this.isPlayer && !this.motion.isZero) {
this.moveEntity(this.motion.x, this.motion.y, this.motion.z);
}
// Drowning
if (this.isInWater(true)) {
if (this.timeInWater == Number.MIN_SAFE_INTEGER) {
this.timeInWater = 320;
}
if (this.timeInWater <= 0 && this.timeInWater % 20 === 0) {
this.damageFrom(1);
}
this.timeInWater--;
} else {
this.timeInWater = Number.MIN_SAFE_INTEGER;
}
if (this.isInWater(false)) {
this.fallDistance = 0;
}
}
}

View file

@ -0,0 +1,16 @@
import AABB from "../AABB"
import Vec3 from "../Vec3"
export default interface IEntity {
entityId:number,
position:Vec3,
motion:Vec3,
lastPosition:Vec3,
crouching:boolean,
isDead:boolean,
markedForDisposal:boolean,
entityAABB:AABB,
updateMetadata:() => void,
distanceTo:(entity:IEntity) => number,
onTick:() => void
}

201
server/entities/Player.ts Normal file
View file

@ -0,0 +1,201 @@
import { IReader, IWriter } from "bufferstuff";
import Block from "../blocks/Block";
import Chunk from "../Chunk";
import Entity from "./Entity";
import EntityItem from "./EntityItem";
import EntityLiving from "./EntityLiving";
import Item from "../items/Item";
import ItemStack from "../inventories/ItemStack";
import MinecraftServer from "../MinecraftServer";
import MPClient from "../MPClient";
import PacketCollectItem from "../packets/CollectItem";
import PacketEntityEquipment from "../packets/EntityEquipment";
import PacketMapChunk from "../packets/MapChunk";
import PacketPreChunk from "../packets/PreChunk";
import PacketUpdateHealth from "../packets/UpdateHealth";
import PlayerInventory from "../inventories/PlayerInventory";
import World from "../World";
const CHUNK_LOAD_RANGE = 15;
export default class Player extends EntityLiving {
public username:string;
private server:MinecraftServer;
private firstUpdate:boolean;
public loadedChunks:Array<number>;
public justUnloaded:Array<number>;
public mpClient?:MPClient;
public inventory:PlayerInventory;
public trackedEquipment:Array<ItemStack | null>;
public constructor(server:MinecraftServer, world:World, username:string) {
super(world, true);
this.server = server;
this.firstUpdate = true;
this.loadedChunks = new Array<number>();
this.justUnloaded = new Array<number>();
this.inventory = new PlayerInventory(this);
this.inventory.setSlotItemStack(36, new ItemStack(Item.ironSword, 1));
this.inventory.setSlotItemStack(37, new ItemStack(Item.ironPickaxe, 1));
this.inventory.setSlotItemStack(38, new ItemStack(Item.ironShovel, 1));
this.inventory.setSlotItemStack(39, new ItemStack(Item.ironAxe, 1));
this.inventory.setSlotItemStack(43, new ItemStack(Block.dirt, 32));
this.trackedEquipment = new Array<ItemStack | null>();
for (let i = 0; i < 5; i++) {
this.trackedEquipment.push(null);
}
this.username = username;
this.position.set(8, 64, 8);
}
public fromSave(reader:IReader) {
super.fromSave(reader);
this.inventory.fromSave(reader);
}
public toSave(writer:IWriter) {
super.toSave(writer);
this.inventory.toSave(writer);
}
// Forces a player chunk update *next tick*
public forceUpdatePlayerChunks() {
this.firstUpdate = true;
}
public itemPickup(entity:Entity, stackSize:number) {
if (!this.isDead) {
if (entity instanceof EntityItem) {
this.sendToAllNearby(new PacketCollectItem(entity.entityId, this.entityId).writeData());
}
}
}
dropAllItems() {
for (const itemStack of this.inventory.itemStacks) {
if (itemStack) {
const item = new EntityItem(this.world, itemStack);
item.position.set(this.position);
this.world.addEntity(item);
}
}
}
private async updatePlayerChunks() {
const bitX = this.position.x >> 4;
const bitZ = this.position.z >> 4;
if (bitX != this.lastPosition.x >> 4 || bitZ != this.lastPosition.z >> 4 || this.firstUpdate) {
if (this.firstUpdate) {
this.firstUpdate = false;
// TODO: Make this based on the player's initial coords
this.mpClient?.send(new PacketPreChunk(0, 0, true).writeData());
const chunk = await this.world.getChunkSafe(0, 0);
const chunkData = await (new PacketMapChunk(0, 0, 0, 15, 127, 15, chunk).writeData());
this.mpClient?.send(chunkData);
}
// Load or keep any chunks we need
const currentLoads = [];
for (let x = bitX - CHUNK_LOAD_RANGE; x < bitX + CHUNK_LOAD_RANGE; x++) {
for (let z = bitZ - CHUNK_LOAD_RANGE; z < bitZ + CHUNK_LOAD_RANGE; z++) {
const coordPair = Chunk.CreateCoordPair(x, z);
if (!this.loadedChunks.includes(coordPair)) {
const chunk = await this.world.getChunkSafe(x, z);
this.mpClient?.send(new PacketPreChunk(x, z, true).writeData());
this.loadedChunks.push(coordPair);
chunk.playersInChunk.set(this.entityId, this);
const chunkData = await (new PacketMapChunk(x, 0, z, 15, 127, 15, chunk).writeData());
this.mpClient?.send(chunkData);
}
currentLoads.push(coordPair);
}
}
// Mark any unaccounted chunks for unload
for (const coordPair of this.loadedChunks) {
if (!currentLoads.includes(coordPair) && this.world.chunkExists(coordPair)) {
this.justUnloaded.push(coordPair);
const chunkToUnload = this.world.getChunkByCoordPair(coordPair);
this.mpClient?.send(new PacketPreChunk(chunkToUnload.x, chunkToUnload.z, false).writeData());
}
}
// Overwrite loaded chunks
this.loadedChunks = currentLoads;
}
}
private getEquipmentForVirtualSlot(slot:number) {
if (slot === 0) {
return this.mpClient?.getHeldItemStack() ?? null;
} else {
this.inventory.getSlotItemStack(4 + slot); // 5 - 8
}
return null;
}
private sendEquipment(equipmentId:number, itemStack:ItemStack | null) {
this.sendToNearby(new PacketEntityEquipment(this.entityId, equipmentId, itemStack == null ? -1 : itemStack.itemID, itemStack == null ? 0 : itemStack.damage).writeData());
}
private sendEquipmentPlayer(mpClient:MPClient, equipmentId:number, itemStack:ItemStack | null) {
mpClient.send(new PacketEntityEquipment(this.entityId, equipmentId, itemStack == null ? -1 : itemStack.itemID, itemStack == null ? 0 : itemStack.damage).writeData());
}
// For login.
public sendPlayerEquipment(playerToSendTo:Player) {
const mpClient = playerToSendTo.mpClient;
if (mpClient == null) {
return;
}
for (let slotId = 0; slotId < 5; slotId++) {
const itemStack = this.getEquipmentForVirtualSlot(slotId);
const trackedEquipment = this.trackedEquipment[slotId];
if ((itemStack == null || trackedEquipment == null) || !itemStack.compare(trackedEquipment)) {
this.trackedEquipment[slotId] = itemStack;
this.sendEquipmentPlayer(mpClient, slotId, itemStack);
}
}
}
public onTick() {
this.updatePlayerChunks();
// Calculate player motion since we don't have it serverside.
this.motion.set(this.position.x - this.lastPosition.x, this.position.y - this.lastPosition.y, this.position.z - this.lastPosition.z);
if (!this.motion.isZero) {
this.entityAABB.move(this.position);
}
super.onTick();
for (let slotId = 0; slotId < 5; slotId++) {
const itemStack = this.getEquipmentForVirtualSlot(slotId);
const trackedEquipment = this.trackedEquipment[slotId];
if ((itemStack == null || trackedEquipment == null) || !itemStack.compare(trackedEquipment)) {
this.trackedEquipment[slotId] = itemStack;
this.sendEquipment(slotId, itemStack);
}
}
if (this.health != this.lastHealth) {
if (this.health <= 0 && this.isDead) {
this.dropAllItems();
}
this.lastHealth = this.health;
this.mpClient?.send(new PacketUpdateHealth(this.health).writeData());
}
}
}

11
server/enums/Animation.ts Normal file
View file

@ -0,0 +1,11 @@
enum Animation {
None = 0,
SwingArm = 1,
Damage = 2,
LeaveBed = 3,
Unknown102 = 102,
Crouch = 104,
Uncrouch = 105,
}
export default Animation;

View file

@ -0,0 +1,10 @@
enum EntityStatus {
Unknown0,
Unknown1,
Hurt,
Dead,
Unknown4,
Unknown5
}
export default EntityStatus;

9
server/enums/MaxUses.ts Normal file
View file

@ -0,0 +1,9 @@
enum MaxUses {
GOLD = 32,
WOOD = 59,
STONE = 131,
IRON = 250,
DIAMOND = 1561
}
export default MaxUses;

View file

@ -0,0 +1,11 @@
enum MetadataFieldType {
Byte = 0,
Short = 1,
Int = 2,
Float = 3,
String = 4,
Item = 5,
Vector = 6
}
export default MetadataFieldType;

54
server/enums/Packet.ts Normal file
View file

@ -0,0 +1,54 @@
// https://wiki.vg/index.php?title=Protocol&oldid=488
enum Packet {
KeepAlive = 0x00,
LoginRequest = 0x01,
Handshake = 0x02,
Chat = 0x03,
TimeUpdate = 0x04,
EntityEquipment = 0x05,
SpawnPosition = 0x06,
UseEntity = 0x07,
UpdateHealth = 0x08,
Respawn = 0x09,
Player = 0x0A,
PlayerPosition = 0x0B,
PlayerLook = 0x0C,
PlayerPositionLook = 0x0D,
PlayerDigging = 0x0E,
PlayerBlockPlacement = 0x0F,
HoldingChange = 0x10,
UseBed = 0x11,
Animation = 0x12,
EntityAction = 0x13,
NamedEntitySpawn = 0x14,
PickupSpawn = 0x15,
CollectItem = 0x16,
EntityVelocity = 0x1C,
DestroyEntity = 0x1D,
EntityStatus = 0x26,
EntityMetadata = 0x28,
PreChunk = 0x32,
MapChunk = 0x33,
MultiBlockChange = 0x34,
BlockChange = 0x035,
SoundEffect = 0x3D,
Entity = 0x1E,
EntityRelativeMove = 0x1F,
EntityLook = 0x20,
EntityLookRelativeMove = 0x21,
EntityTeleport = 0x22,
CloseWindow = 0x65,
WindowClick = 0x66,
SetSlot = 0x67,
WindowItems = 0x68,
DisconnectKick = 0xff
}
export default Packet;

View file

@ -0,0 +1,7 @@
enum SaveCompressionType {
NONE = 0,
DEFLATE = 1,
XZ = 2
}
export default SaveCompressionType;

View file

@ -0,0 +1,12 @@
enum SoundEffects {
CLICK2 = 1000,
CLICK1 = 1001,
BOW_FIRE = 1002,
DOOR_TOGGLE = 1003,
EXTINGUISH = 1004,
RECORD_PLAY = 1005,
SMOKE = 2000,
BLOCK_BREAK = 2001
}
export default SoundEffects;

View file

@ -0,0 +1,5 @@
import Chunk from "../Chunk";
export default interface IGenerator {
generate: (chunk:Chunk) => void
}

View file

@ -0,0 +1,23 @@
import Block from "../../blocks/Block";
import Chunk from "../../Chunk";
import IGenerator from "../IGenerator";
export default class FlatGenerator implements IGenerator {
public generate(chunk:Chunk) {
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
for (let y = 0; y < 128; y++) {
if (y === 63) {
chunk.setBlock(Block.grass.blockId, x, y, z);
} else if (y === 62 || y === 61) {
chunk.setBlock(Block.dirt.blockId, x, y, z);
} else if (y === 0) {
chunk.setBlock(Block.bedrock.blockId, x, y, z);
} else if (y < 61) {
chunk.setBlock(Block.stone.blockId, x, y, z);
}
}
}
}
}
}

View file

@ -0,0 +1,216 @@
import { Noise2D, makeNoise2D } from "../../../external/OpenSimplex2D";
import { Noise3D, makeNoise3D } from "../../../external/OpenSimplex3D";
import Block from "../../blocks/Block";
import Chunk from "../../Chunk";
import IGenerator from "../IGenerator";
import mulberry32 from "../../mulberry32";
import QueuedBlockUpdate from "../../queuedUpdateTypes/BlockUpdate";
export default class HillyGenerator implements IGenerator {
private seed:number;
seedGenerator:() => number;
private generator:Noise2D;
private generator1:Noise2D;
private generator2:Noise2D;
private generator3:Noise2D;
private generator4:Noise2D;
private generator5:Noise2D;
private generator6:Noise2D;
private oceanGenerator:Noise2D;
private hillGenerator:Noise2D;
private caveGenerator1:Noise3D;
private caveGenerator2:Noise3D;
private caveGenerator3:Noise3D;
private caveGenerator4:Noise3D;
private underwaterGravelGenerator:Noise2D;
private underwaterSandGenerator:Noise2D;
private underwaterClayGenerator:Noise2D;
private flowerGenerator:Noise2D;
public constructor(seed:number) {
this.seed = seed;
this.seedGenerator = mulberry32(this.seed);
this.generator = this.createGenerator2D();
this.generator1 = this.createGenerator2D();
this.generator2 = this.createGenerator2D();
this.generator3 = this.createGenerator2D();
this.generator4 = this.createGenerator2D();
this.generator5 = this.createGenerator2D();
this.generator6 = this.createGenerator2D();
this.oceanGenerator = this.createGenerator2D();
this.hillGenerator = this.createGenerator2D();
this.caveGenerator1 = this.createGenerator3D();
this.caveGenerator2 = this.createGenerator3D();
this.caveGenerator3 = this.createGenerator3D();
this.caveGenerator4 = this.createGenerator3D();
this.underwaterGravelGenerator = this.createGenerator2D();
this.underwaterSandGenerator = this.createGenerator2D();
this.underwaterClayGenerator = this.createGenerator2D();
this.flowerGenerator = this.createGenerator2D();
}
private createGenerator2D() {
return makeNoise2D(this.seedGenerator() * Number.MAX_SAFE_INTEGER);
}
private createGenerator3D() {
return makeNoise3D(this.seedGenerator() * Number.MAX_SAFE_INTEGER);
}
public generate(chunk:Chunk) {
const treeRNG = mulberry32(this.seed + chunk.x + chunk.z);
const sugarcaneRNG = mulberry32(this.seed + chunk.x + chunk.z);
const grassRNG = mulberry32(this.seed + chunk.x + chunk.z);
const flowerRNG = mulberry32(this.seed + chunk.x + chunk.z);
let colY = 0, colDirtMin = 0, colWaterY = 0, orgColY = 0;
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
const oceanValue = this.oceanGenerator((chunk.x * 16 + x) / 128, (chunk.z * 16 + z) / 128) * 100;
orgColY = colWaterY = colY = 60 + Math.round((
this.generator((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) * 16 +
this.generator1((chunk.z * 16 + z) / 16, (chunk.x * 16 + x) / 16) * 16 +
this.generator2((chunk.x * 16 + x) / 8, (chunk.z * 16 + z) / 8) * 8 +
this.generator3((chunk.z * 16 + z) / 4, (chunk.x * 16 + x) / 4) * 4 +
this.generator4((chunk.x * 16 + x) / 4, (chunk.z * 16 + z) / 4) * 4 +
this.generator5((chunk.z * 16 + z) / 10, (chunk.x * 16 + x) / 10) * 10 +
this.generator6((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) * 16 +
oceanValue +
(Math.max(this.hillGenerator((chunk.x * 16 + x) / 128, (chunk.z * 16 + z) / 128), 0) * 50 + Math.min(oceanValue, 0))
) / 9);
colDirtMin = colY - 2;
const sandNoise = this.underwaterSandGenerator((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16);
if (colY <= 60 && sandNoise > 0.5) {
chunk.setBlock(Block.sand.blockId, x, colY, z);
} else {
chunk.setBlock(Block.grass.blockId, x, colY, z);
}
let caveY = colY + 1;
while (colY-- > 0) {
if (colY >= colDirtMin) {
chunk.setBlock(Block.dirt.blockId, x, colY, z);
} else if (colY === 0) {
chunk.setBlock(Block.bedrock.blockId, x, colY, z);
} else {
chunk.setBlock(Block.stone.blockId, x, colY, z);
}
}
// Generate underwater blocks
if (colWaterY <= 58) {
if (this.underwaterGravelGenerator((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) > 0.3) {
chunk.setBlock(Block.gravel.blockId, x, colWaterY, z);
} else if (sandNoise > 0.4) {
chunk.setBlock(Block.sand.blockId, x, colWaterY, z);
} else if (this.underwaterClayGenerator((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) > 0.5) {
chunk.setBlock(Block.clay.blockId, x, colWaterY, z);
} else {
chunk.setBlock(Block.dirt.blockId, x, colWaterY, z);
}
}
while (colWaterY <= 58) {
colWaterY++;
chunk.setBlock(Block.waterStill.blockId, x, colWaterY, z);
}
while (caveY-- > 1) {
if (
((this.caveGenerator1((chunk.x * 16 + x) / 16, caveY / 16, (chunk.z * 16 + z) / 16) +
this.caveGenerator2((chunk.x * 16 + x) / 8, caveY / 8, (chunk.z * 16 + z) / 8)) / 2) > 0.45
|| this.caveGenerator3((chunk.x * 16 + x) / 16, caveY / 16, (chunk.z * 16 + z) / 16) > 0.6 ||
this.caveGenerator4((chunk.x * 16 + x) / 8, caveY / 8, (chunk.z * 16 + z) / 8) > 0.6
) {
if (caveY <= 3) {
chunk.setBlock(Block.lavaStill.blockId, x, caveY, z);
} else {
chunk.setBlock(0, x, caveY, z);
}
}
}
const queuedChunkBlocks = chunk.world.queuedChunkBlocks;
if (queuedChunkBlocks.length > 0) {
const thisCoordPair = Chunk.CreateCoordPair(chunk.x, chunk.z);
for (let i = queuedChunkBlocks.length - 1; i >= 0; i--) {
const blockUpdate = queuedChunkBlocks[i];
if (blockUpdate instanceof QueuedBlockUpdate && blockUpdate.coordPair === thisCoordPair) {
queuedChunkBlocks.splice(i, 1);
chunk.setBlockWithMetadata(blockUpdate.blockId, blockUpdate.metadata, blockUpdate.x, blockUpdate.y, blockUpdate.z);
}
}
}
// Grass and flowers
if (chunk.getBlockId(x, orgColY, z) === Block.grass.blockId) {
if (grassRNG() > 0.9) {
chunk.setBlockWithMetadata(Block.tallGrass.blockId, 1, x, orgColY + 1, z);
} else if (this.flowerGenerator((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) > 0.5 && flowerRNG() > 0.9) {
if (flowerRNG() > 0.4) {
chunk.setBlockWithMetadata(Block.flowerRose.blockId, 1, x, orgColY + 1, z);
} else {
chunk.setBlockWithMetadata(Block.flowerDandelion.blockId, 1, x, orgColY + 1, z);
}
}
}
if (
sugarcaneRNG() > 0.992 &&
chunk.getBlockId(x, orgColY + 1, z) !== Block.waterStill.blockId &&
chunk.getBlockId(x, orgColY, z) === Block.grass.blockId &&
(((x - 1) < 0 ? false : chunk.getBlockId(x - 1, orgColY, z) === Block.waterStill.blockId) ||
((x + 1) > 15 ? false : chunk.getBlockId(x + 1, orgColY, z) === Block.waterStill.blockId) ||
((z - 1) < 0 ? false : chunk.getBlockId(x, orgColY, z - 1) === Block.waterStill.blockId) ||
((z + 1) > 15 ? false : chunk.getBlockId(x, orgColY, z + 1) === Block.waterStill.blockId))
) {
let sugarcaneYHeight = 2 + Math.round(sugarcaneRNG() * 2.5);
while (sugarcaneYHeight > 0) {
chunk.setBlock(Block.sugarcane.blockId, x, orgColY + sugarcaneYHeight, z);
sugarcaneYHeight--;
}
}
// TODO: Move trees to it's own generator
if (chunk.getBlockId(x, orgColY + 1, z) !== Block.waterStill.blockId && chunk.getBlockId(x, orgColY, z) === Block.grass.blockId && treeRNG() > 0.995) {
const treeType = treeRNG() >= 0.5;
chunk.setBlock(Block.dirt.blockId, x, orgColY, z);
let tYT = 0, tY = tYT = orgColY + 4 + Math.round(treeRNG() - 0.2), tLY = 0;
while (tY > orgColY) {
chunk.setBlockWithMetadata(Block.wood.blockId, treeType ? 2 : 0, x, tY, z);
if (tLY !== 0 && tLY < 3) {
for (let tX = -2; tX <= 2; tX++) {
for (let tZ = -2; tZ <= 2; tZ++) {
if (tX === 0 && tZ === 0) {
continue;
}
chunk.setBlockWithMetadata(Block.leaves.blockId, treeType ? 2 : 0, x + tX, tY, z + tZ);
}
}
}
tY--;
tLY++;
}
tY = 0;
while (tY < 2) {
for (let tX = -1; tX < 2; tX++) {
for (let tZ = -1; tZ < 2; tZ++) {
if (tX === 0 && tZ === 0 && tY !== 1) {
continue;
}
chunk.setBlockWithMetadata(Block.leaves.blockId, treeType ? 2 : 0, x + tX, tYT + tY, z + tZ);
}
}
tY++;
}
}
}
}
}
}

View file

@ -0,0 +1,60 @@
import { Noise2D, makeNoise2D } from "../../../external/OpenSimplex2D";
import { Noise3D, makeNoise3D } from "../../../external/OpenSimplex3D";
import Block from "../../blocks/Block";
import Chunk from "../../Chunk";
import IGenerator from "../IGenerator";
import mulberry32 from "../../mulberry32";
export default class NetherGenerator implements IGenerator {
private seed:number;
seedGenerator:() => number;
private generator:Noise3D;
private generator1:Noise3D;
private generator2:Noise3D;
private generator3:Noise3D;
private generator4:Noise3D;
private generator5:Noise3D;
public constructor(seed:number) {
this.seed = seed;
this.seedGenerator = mulberry32(this.seed);
this.generator = this.createGenerator3D();
this.generator1 = this.createGenerator3D();
this.generator2 = this.createGenerator3D();
this.generator3 = this.createGenerator3D();
this.generator4 = this.createGenerator3D();
this.generator5 = this.createGenerator3D();
}
private createGenerator2D() {
return makeNoise2D(this.seedGenerator() * Number.MAX_SAFE_INTEGER);
}
private createGenerator3D() {
return makeNoise3D(this.seedGenerator() * Number.MAX_SAFE_INTEGER);
}
public generate(chunk:Chunk) {
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
for (let y = 0; y < 128; y++) {
if (y === 0) {
chunk.setBlock(Block.bedrock.blockId, x, y, z);
continue;
}
const layer1 = (this.generator((chunk.x * 16 + x) / 32, y / 32, (chunk.z * 16 + z) / 32) + this.generator1((chunk.x * 16 + x) / 32, y / 32, (chunk.z * 16 + z) / 32)) / 2;
const layer2 = (this.generator2((chunk.x * 16 + x) / 128, y / 128, (chunk.z * 16 + z) / 128) + this.generator3((chunk.x * 16 + x) / 128, y / 128, (chunk.z * 16 + z) / 128)) / 2;
const layer3 = (this.generator4((chunk.x * 16 + x) / 16, y / 16, (chunk.z * 16 + z) / 16) + this.generator5((chunk.x * 16 + x) / 16, y / 16, (chunk.z * 16 + z) / 16)) / 2;
if ((layer1 + layer2 + layer3) / 3 >= 0.1) {
chunk.setBlock(Block.netherrack.blockId, x, y, z);
} else if (y < 10) {
chunk.setBlock(Block.lavaStill.blockId, x, y, z);
}
}
}
}
}
}

View file

@ -0,0 +1,83 @@
import { makeNoise2D, Noise2D } from "../../../external/OpenSimplex2D";
import { makeNoise3D, Noise3D } from "../../../external/OpenSimplex3D";
import Block from "../../blocks/Block";
import Chunk from "../../Chunk";
import IGenerator from "../IGenerator";
import mulberry32 from "../../mulberry32";
export default class NewOverworld implements IGenerator {
private seed:number;
seedGenerator:() => number;
public layer1:Noise3D;
public layer2:Noise3D;
public mix:Noise3D;
public maxHeight:Noise2D;
private createGenerator2D() {
return makeNoise2D(this.seedGenerator() * Number.MAX_SAFE_INTEGER);
}
private createGenerator3D() {
return makeNoise3D(this.seedGenerator() * Number.MAX_SAFE_INTEGER);
}
public constructor(seed:number) {
this.seed = seed;
this.seedGenerator = mulberry32(this.seed);
this.layer1 = this.createGenerator3D();
this.layer2 = this.createGenerator3D();
this.mix = this.createGenerator3D();
this.maxHeight = this.createGenerator2D();
}
private noiseForCoord(chunk:Chunk, x:number, y:number, z:number) {
const mixValue = this.mix((chunk.x * 16 + x) / 16, y / 16, (chunk.z * 16 + z) / 16);
const layer1 = this.layer1((chunk.x * 16 + x) / 64, y / 64, (chunk.z * 16 + z) / 64) / 2048;
const layer2 = this.layer2((chunk.x * 16 + x) / 64, y / 64, (chunk.z * 16 + z) / 64) / 2048;
const maxHeightLayer = 80 + this.maxHeight((chunk.x * 16 + x) / 64, (chunk.z * 16 + z) / 64) * 32;
if (y > maxHeightLayer) {
return 0;
}
if (mixValue < 0) {
return layer1;
} else if (mixValue > 1) {
return layer2;
} else {
return layer1 + (layer2 - layer1) * mixValue;
}
}
public generate(chunk:Chunk) {
for (let y = 0; y < 128; y++) {
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
if (y === 0 || (y < 3 && this.layer1(x, y, z) > 0.1)) {
chunk.setBlock(Block.bedrock.blockId, x, y, z);
} else {
if (this.noiseForCoord(chunk, x, y, z) > 0) {
chunk.setBlock(Block.stone.blockId, x, y, z);
} else if (y < 64) {
chunk.setBlock(Block.waterStill.blockId, x, y, z);
}
}
}
}
}
for (let y = 0; y < 128; y++) {
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
if (y === 0 || (y < 3 && this.layer1(x, y, z) > 0.1)) {
chunk.setBlock(Block.bedrock.blockId, x, y, z);
} else {
}
}
}
}
}
}

View file

@ -0,0 +1,11 @@
import { IReader, IWriter } from "bufferstuff";
import ItemStack from "./ItemStack";
export default interface IInventory {
fromSave:(reader:IReader) => void,
toSave:(writer:IWriter) => void,
getInventoryName:() => string,
getInventorySize:() => number,
getSlotItemStack:(slotId:number) => ItemStack | null
setSlotItemStack:(slotId:number, itemStack:ItemStack | null) => IInventory
}

View file

@ -0,0 +1,122 @@
import { Endian, IReader, IWriter, createWriter } from "bufferstuff";
import IInventory from "./IInventory";
import ItemStack from "./ItemStack";
export default class Inventory implements IInventory {
public itemStacks:Array<ItemStack | null>;
private size:number;
private name:string;
public constructor(size:number, name:string) {
this.itemStacks = new Array<ItemStack | null>();
for (let i = 0; i < size; i++) {
this.itemStacks.push(null);
}
this.size = size;
this.name = name;
}
public fromSave(reader:IReader) {
const inventorySize = reader.readByte();
for (let i = 0; i < inventorySize; i++) {
this.itemStacks[i] = ItemStack.FromSave(reader);
}
}
public toSave(writer:IWriter) {
writer.writeByte(this.size);
for (const itemStack of this.itemStacks) {
if (itemStack === null) {
writer.writeShort(-1);
continue;
}
itemStack.toSave(writer);
}
}
addItemStack(itemStack:ItemStack) {
throw new Error("Adding items to non player inventories is unimplemented.");
// Check bottom inventory row (hotbar) first.
/*let workingItemStack:ItemStack | null;
for (let slotId = 9; slotId <= 35; slotId++) {
if (itemStack.size === 0) {
break;
}
if ((workingItemStack = this.itemStacks[slotId]) != null) {
workingItemStack.insert(itemStack);
}
}*/
}
getInventoryName() {
return this.name;
}
getInventorySize() {
return this.itemStacks.length;
}
getSlotItemStack(slotId:number) {
return this.itemStacks[slotId];
}
dropEmptyItemStacks() {
for (let i = 0; i < this.itemStacks.length; i++) {
const itemStack = this.itemStacks[i];
if (itemStack?.size === 0) {
this.itemStacks[i] = null;
}
}
}
setSlotItemStack(slotId:number, itemStack: ItemStack | null) {
if (slotId < 0 || slotId > this.size - 1) {
throw new Error(`Tried to set an Inventory ItemStack out of bounds! Requested slot: ${slotId}, Inventory Size: ${this.size}`);
}
this.itemStacks[slotId] = itemStack;
return this;
}
private calculateInventoryPayloadSize() {
let bufferSize = 0;
for (const stack of this.itemStacks) {
if (stack) {
bufferSize += 5; // short + byte + short
} else {
bufferSize += 2; // short
}
}
return bufferSize;
}
constructInventoryPayload() {
const writer = createWriter(Endian.BE, this.calculateInventoryPayloadSize());
for (const stack of this.itemStacks) {
writer.writeShort(stack == null ? -1 : stack.itemID);
if (stack != null) {
writer.writeByte(stack.size);
writer.writeShort(stack.damage);
}
}
return writer.toBuffer();
}
constructInventorySinglePayload(slotId:number) {
const stack = this.itemStacks[slotId];
const writer = createWriter(Endian.BE, stack == null ? 2 : 5);
writer.writeShort(stack == null ? -1 : stack.itemID);
if (stack != null) {
writer.writeByte(stack.size);
writer.writeShort(stack.damage);
}
return writer.toBuffer();
}
}

View file

@ -0,0 +1,134 @@
import { IReader, IWriter } from "bufferstuff";
import Block from "../blocks/Block";
import IEntity from "../entities/IEntity";
import Item from "../items/Item";
export default class ItemStack {
private static ITEMSTACK_ID_ADDER = 0;
private readonly itemStackId:number;
public readonly itemID:number;
public readonly isBlock:boolean;
public size:number;
public damage:number;
private readonly maxSize:number;
private readonly maxDamage:number;
private readonly canBeDamaged:boolean;
public constructor(blockOrItemOrItemID:Block|Item|number, size?:number, damage?:number) {
if (blockOrItemOrItemID instanceof Block && size === undefined && damage === undefined) {
this.itemID = blockOrItemOrItemID.blockId;
this.size = 0;
this.damage = 0;
} else if (blockOrItemOrItemID instanceof Block && typeof(size) === "number" && damage === undefined) {
this.itemID = blockOrItemOrItemID.blockId;
this.size = size;
this.damage = 0;
} else if (blockOrItemOrItemID instanceof Block && typeof(size) === "number" && typeof(damage) === "number") {
this.itemID = blockOrItemOrItemID.blockId;
this.size = size;
this.damage = damage;
} else if (blockOrItemOrItemID instanceof Item && size === undefined && damage === undefined) {
this.itemID = blockOrItemOrItemID.shiftedItemID;
this.size = 0;
this.damage = 0;
} else if (blockOrItemOrItemID instanceof Item && typeof(size) === "number" && damage === undefined) {
this.itemID = blockOrItemOrItemID.shiftedItemID;
this.size = size;
this.damage = 0;
} else if (blockOrItemOrItemID instanceof Item && typeof(size) === "number" && typeof(damage) === "number") {
this.itemID = blockOrItemOrItemID.shiftedItemID;
this.size = size;
this.damage = damage;
} else if (typeof(blockOrItemOrItemID) === "number" && typeof(size) === "number" && typeof(damage) === "number") {
this.itemID = blockOrItemOrItemID;
this.size = size;
this.damage = damage;
} else if (typeof(blockOrItemOrItemID) === "number" && typeof(size) === "number" && damage === undefined) {
this.itemID = blockOrItemOrItemID;
this.size = size;
this.damage = 0;
} else if (typeof(blockOrItemOrItemID) === "number" && size === undefined && damage === undefined) {
this.itemID = blockOrItemOrItemID;
this.size = 0;
this.damage = 0;
} else {
throw new Error(`ItemStack created with invalid properties (${typeof(blockOrItemOrItemID)}, ${typeof(size)}, ${typeof(damage)})`);
}
this.isBlock = this.itemID < 256;
this.maxSize = this.isBlock ? 64 : Item.getByShiftedItemId(this.itemID).maxStackSize;
this.maxDamage = this.isBlock ? 0 : Item.getByShiftedItemId(this.itemID).maxDamage;
this.canBeDamaged = this.maxDamage > 0;
this.itemStackId = ItemStack.ITEMSTACK_ID_ADDER++;
}
public static FromSave(reader:IReader) {
const itemId = reader.readShort();
if (itemId === -1) {
return null;
}
return new ItemStack(itemId, reader.readByte(), reader.readShort());
}
public toSave(writer:IWriter) {
writer.writeShort(this.itemID)
.writeByte(this.size)
.writeShort(this.damage);
}
public static Compare(itemStack1:ItemStack, itemStack2:ItemStack) {
return itemStack1.itemStackId === itemStack2.itemStackId;
}
public compare(itemStack:ItemStack) {
return this.itemStackId === itemStack.itemStackId;
}
public insert(itemStack:ItemStack) {
const remainingSpace = this.spaceAvaliable;
if (remainingSpace === 0) {
return false;
}
if (remainingSpace >= itemStack.size) {
this.size += itemStack.size;
itemStack.size = 0;
return true;
}
if (remainingSpace < itemStack.size) {
this.size += remainingSpace;
itemStack.size -= remainingSpace;
return true;
}
}
public damageItem(damageAmount:number, entity:IEntity) {
if (!this.canBeDamaged) {
return;
}
this.damage += damageAmount;
if (this.damage > this.maxDamage) {
this.size--;
if (this.size < 0) {
this.size = 0;
}
this.damage = 0;
}
}
public get spaceAvaliable() {
// Stack size check for Item(s) and Block(s).
return Math.max(this.maxSize - this.size, 0);
}
split(amount:number) {
this.size -= amount;
return new ItemStack(this.itemID, amount, this.damage);
}
}

View file

@ -0,0 +1,107 @@
import Inventory from "./Inventory";
import ItemStack from "./ItemStack";
import PacketSetSlot from "../packets/SetSlot";
import PacketWindowItems from "../packets/WindowItems";
import Player from "../entities/Player";
export default class PlayerInventory extends Inventory {
private player:Player;
public constructor(player:Player) {
super(44, "Player Inventory");
this.player = player;
}
sendUpdatedStacks(stackIdsChanged:Array<number>) {
let updateBuffer = Buffer.alloc(0);
for (const slotId of stackIdsChanged) {
const slotItem = this.itemStacks[slotId];
let buffer:Buffer;
if (slotItem == null) {
buffer = new PacketSetSlot(0, slotId, -1).writeData();
} else {
buffer = new PacketSetSlot(0, slotId, slotItem.itemID, slotItem.size, slotItem.damage).writeData();
}
updateBuffer = Buffer.concat([updateBuffer, buffer], updateBuffer.length + buffer.length);
}
if (updateBuffer.length > 0) {
this.player.mpClient?.send(updateBuffer);
}
}
addItemStack(itemStack:ItemStack) {
const itemStacksOfSameType:Array<ItemStack> = new Array<ItemStack>();
const itemStackIds:Array<number> = new Array<number>();
const stackIdsChanged:Array<number> = new Array<number>();
// Check bottom inventory row (hotbar) first.
let workingItemStack:ItemStack | null;
for (let slotId = 36; slotId <= 44; slotId++) {
if ((workingItemStack = this.itemStacks[slotId]) != null) {
itemStacksOfSameType.push(workingItemStack);
itemStackIds.push(slotId);
}
}
for (let slotId = 9; slotId <= 35; slotId++) {
if ((workingItemStack = this.itemStacks[slotId]) != null) {
itemStacksOfSameType.push(workingItemStack);
itemStackIds.push(slotId);
}
}
// Insert into existing stacks first.
for (let i = 0; i < itemStacksOfSameType.length; i++) {
const inventoryItemStack = itemStacksOfSameType[i];
// Exit early if we have nothing left
if (itemStack.size === 0) {
return this.sendUpdatedStacks(stackIdsChanged);
}
if (inventoryItemStack.itemID !== itemStack.itemID || inventoryItemStack.damage !== itemStack.damage) {
continue;
}
if (inventoryItemStack.insert(itemStack)) {
stackIdsChanged.push(itemStackIds[i]);
}
}
// Exit early if we have nothing left
if (itemStack.size === 0) {
return this.sendUpdatedStacks(stackIdsChanged);
}
for (let slotId = 36; slotId <= 44; slotId++) {
// Exit early if we have nothing left
if (itemStack.size === 0) {
return this.sendUpdatedStacks(stackIdsChanged);
}
if ((workingItemStack = this.itemStacks[slotId]) == null) {
const stack = this.itemStacks[slotId] = new ItemStack(itemStack.itemID, 0, itemStack.damage);
if (stack.insert(itemStack)) {
stackIdsChanged.push(slotId);
}
}
}
for (let slotId = 9; slotId <= 35; slotId++) {
// Exit early if we have nothing left
if (itemStack.size === 0) {
return this.sendUpdatedStacks(stackIdsChanged);
}
if ((workingItemStack = this.itemStacks[slotId]) == null) {
const stack = this.itemStacks[slotId] = new ItemStack(itemStack.itemID, 0, itemStack.damage);
if (stack.insert(itemStack)) {
stackIdsChanged.push(slotId);
}
}
}
this.sendUpdatedStacks(stackIdsChanged)
}
}

View file

@ -0,0 +1,6 @@
import EntityLiving from "../entities/EntityLiving";
import ItemStack from "../inventories/ItemStack";
export default interface IItemBehaviour {
}

137
server/items/Item.ts Normal file
View file

@ -0,0 +1,137 @@
import MaxUses from "../enums/MaxUses";
export default class Item {
public static items:Array<Item> = new Array<Item>();
public maxStackSize:number;
public maxDamage:number;
public shiftedItemID:number;
public name:string;
public constructor(itemID:number) {
this.shiftedItemID = 256 + itemID;
this.maxDamage = 0;
this.maxStackSize = 64;
this.name = "UNNAMED";
Item.items[itemID] = this;
}
public static getByShiftedItemId(shiftedItemID:number) {
return Item.items[shiftedItemID - 256];
}
public setMaxStackSize(stackSize:number) {
this.maxStackSize = stackSize;
return this;
}
public getMaxDamage() {
return this.maxDamage;
}
public setMaxDamage(value:number) {
this.maxDamage = value;
return this;
}
public setName(name:string) {
this.name = name;
return this;
}
public getName() {
return this.name;
}
// Define statics here
static ironShovel = new Item(0).setMaxDamage(MaxUses.IRON).setName("Iron Shovel");
static ironPickaxe = new Item(1).setMaxDamage(MaxUses.IRON).setName("Iron Pickaxe");
static ironAxe = new Item(2).setMaxDamage(MaxUses.IRON).setName("Iron Axe");
static flintAndSteel = new Item(3).setName("Flint and Steel");
static apple = new Item(4).setName("Apple");
static bow = new Item(5).setName("Bow");
static arrow = new Item(6).setName("Arrow");
static coal = new Item(7).setName("Coal");
static diamond = new Item(8).setName("Diamond");
static iron = new Item(9).setName("Iron Ingot");
static gold = new Item(10).setName("Gold Ingot");
static ironSword = new Item(11).setMaxDamage(MaxUses.IRON).setName("Iron Sword");
static woodenSword = new Item(12).setMaxDamage(MaxUses.WOOD).setName("Wooden Sword");
static woodenShovel = new Item(13).setMaxDamage(MaxUses.WOOD).setName("Wooden Shovel");
static woodenPickaxe = new Item(14).setMaxDamage(MaxUses.WOOD).setName("Wooden Pickaxe");
static woodenAxe = new Item(15).setMaxDamage(MaxUses.WOOD).setName("Wooden Axe");
static stoneSword = new Item(16).setMaxDamage(MaxUses.STONE).setName("Stone Sword");
static stoneShovel = new Item(17).setMaxDamage(MaxUses.STONE).setName("Stone Shovel");
static stonePickaxe = new Item(18).setMaxDamage(MaxUses.STONE).setName("Stone Pickaxe");
static stoneAxe = new Item(19).setMaxDamage(MaxUses.STONE).setName("Stone Axe");
static diamondSword = new Item(20).setMaxDamage(MaxUses.DIAMOND).setName("Diamond Sword");
static diamondShovel = new Item(21).setMaxDamage(MaxUses.DIAMOND).setName("Diamond Shovel");
static diamondPickaxe = new Item(22).setMaxDamage(MaxUses.DIAMOND).setName("Diamond Pickaxe");
static diamondAxe = new Item(23).setMaxDamage(MaxUses.DIAMOND).setName("Diamond Axe");
static stick = new Item(24).setName("Stick");
static bowl = new Item(25).setName("Bowl");
static mushroomStew = new Item(26).setName("Mushroom Stew");
static goldSword = new Item(27).setMaxDamage(MaxUses.GOLD).setName("Gold Sword");
static goldShovel = new Item(28).setMaxDamage(MaxUses.GOLD).setName("Gold Shovel");
static goldPickaxe = new Item(29).setMaxDamage(MaxUses.GOLD).setName("Gold Pickaxe");
static goldAxe = new Item(30).setMaxDamage(MaxUses.GOLD).setName("Gold Axe");
static string = new Item(31).setName("String");
static feather = new Item(32).setName("Feather");
static gunpowder = new Item(33).setName("Gunpowder");
static woodenHoe = new Item(34).setMaxDamage(MaxUses.WOOD).setName("Wooden Hoe");
static stoneHoe = new Item(35).setMaxDamage(MaxUses.WOOD).setName("Stone Hoe");
static ironHoe = new Item(36).setMaxDamage(MaxUses.WOOD).setName("Iron Hoe");
static diamondHoe = new Item(37).setMaxDamage(MaxUses.WOOD).setName("Diamond Hoe");
static goldHoe = new Item(38).setMaxDamage(MaxUses.WOOD).setName("Gold Hoe");
static seeds = new Item(39).setName("Seeds");
static wheat = new Item(40).setName("Wheat");
static bread = new Item(41).setName("Bread");
// TODO: Armor items
static flint = new Item(62).setName("Flint");
static rawPorkchop = new Item(63).setName("Raw Porkchop");
static cookedPorkchop = new Item(64).setName("Cooked Porkchop");
static painting = new Item(65).setName("Painting");
static goldenApple = new Item(66).setName("Golden Apple");
static sign = new Item(67).setName("Sign");
static woodenDoor = new Item(68).setName("Wooden Door");
static bucket = new Item(69).setName("Bucket");
static waterBucket = new Item(70).setName("Water Bucket");
static lavaBucket = new Item(71).setName("Lava Bucket");
static minecart = new Item(72).setName("Minecart");
static saddle = new Item(73).setName("Saddle");
static ironDoor = new Item(74).setName("Iron Door");
static redstone = new Item(75).setName("Redstone");
static snowball = new Item(76).setName("Snowball");
static boat = new Item(77).setName("Boat");
static leather = new Item(78).setName("Leather");
static milkBucket = new Item(79).setName("Milk Bucket");
static brick = new Item(80).setName("Brick");
static clay = new Item(81).setName("Clay");
static sugarcane = new Item(82).setName("Sugar Cane");
static paper = new Item(83).setName("Paper");
static book = new Item(84).setName("Book");
static slimeBall = new Item(85).setName("Slime Ball");
static minecartChest = new Item(86).setName("Minecart with Chest");
static minecartFurnace = new Item(87).setName("Minecart with Furnace");
static egg = new Item(88).setName("Egg");
static compass = new Item(89).setName("Compass");
static fishingRod = new Item(90).setName("Fishing Rod");
static clock = new Item(91).setName("Clock");
static glowstoneDust = new Item(92).setName("Glowstone Dust");
static rawFish = new Item(93).setName("Raw Fish");
static cookedFish = new Item(94).setName("Cooked Fish");
static dye = new Item(95).setName("Dye");
static bone = new Item(96).setName("Bone");
static sugar = new Item(97).setName("Sugar");
static cake = new Item(98).setName("Cake");
static bed = new Item(99).setName("Bed");
static redstoneRepeater = new Item(100).setName("Redstone Repeater");
static cooking = new Item(101).setName("Cookie");
static map = new Item(102).setName("Map");
static shears = new Item(103).setName("Shears");
static record13 = new Item(2000).setName("13");
static recordCat = new Item(2001).setName("Cat");
}

View file

@ -0,0 +1,7 @@
import EntityLiving from "../entities/EntityLiving";
import ItemStack from "../inventories/ItemStack";
import IItemBehaviour from "./IItemBehaviour";
export default class ItemBehaviour implements IItemBehaviour {
}

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