Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full TypeScript rewrite. Best type defs #651

Open
wants to merge 25 commits into
base: master
Choose a base branch
from

Conversation

zardoy
Copy link
Contributor

@zardoy zardoy commented Jan 17, 2024

closes #323

Still a lot of commits to pick, but decided to open early to run a few checks (like jsdoc description & remove docs, generator). awaiting feedback

I have published module for testing so you can see the final result if interested (including docs URL): http://npmjs.com/package/@zardoy/flying-squid

@zardoy zardoy marked this pull request as draft January 17, 2024 16:18
@@ -210,7 +210,7 @@ module.exports.server = function (serv, { version }) {

const count = opt.count !== undefined
? opt.count
: (type === 'all' || type === 'entity' ? serv.entities.length : 1)
: (type === 'all' || type === 'entity' ? Object.keys(serv.entities).length : 1)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐛

src/lib/modules/communication.ts Outdated Show resolved Hide resolved
src/lib/modules/communication.ts Outdated Show resolved Hide resolved
"_writeArray": (packetName: any, packetFields: any, players: any) => any
"_writeNearby": (packetName: any, packetFields: any, loc: any) => any
"getNearby": ({ world, position, radius }: { world: any; position: any; radius?: number | undefined }) => any
"getNearbyEntities": ({ world, position, radius }: { world: any; position: any; radius?: number | undefined }) => any[]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to remove destructure from types.

entity.effects = {}
for (let i = 1; i <= 23; i++) { // 23 in 1.8, 27 in 1.9
entity.effects[i] = null // Just so we know it's a real potion and not undefined/not existant
}

entity.sendEffect = (effectId, { amplifier = 0, duration = 30 * 20, particles = true, whitelist, blacklist = [] } = {}) => {
if (!whitelist) whitelist = serv.getNearby(entity)
if (entity.type === 'player' && [1].indexOf(effectId) !== -1) entity.sendAbilities()
if (entity.type === 'player' && [1].indexOf(effectId) !== -1) (entity as Player).sendAbilities()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could turn entity into union of "common entity" and "player" so checks like entity.type === player would narrow the type to Player. But it's not possible since we re using interfaces for per-module extension and not types. So it would be easier to just mention this trick

src/lib/modules/effects.ts Outdated Show resolved Hide resolved
src/lib/modules/login.ts Outdated Show resolved Hide resolved
src/lib/modules/moderation.ts Outdated Show resolved Hide resolved
const registry = require('prismarine-registry')(version)
const blocks = registry.blocks

player._client.on('block_place', async ({ direction, location, cursorY } = {}) => {
const referencePosition = new Vec3(location.x, location.y, location.z)
const block = await player.world.getBlock(referencePosition)
block.position = referencePosition
//@ts-ignore TODO
block.direction = direction
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this pattern is widely used across prismarine projects, imo it shouldn't as it makes debugging hard

src/lib/modules/chat.ts Outdated Show resolved Hide resolved
remove almost all require's, make mcData typed (expose), make types ready for publishing!

(cherry picked from commit e4be9d8)
add block renames

(cherry picked from commit b9a2813)
(cherry picked from commit f324dc4)
…rnal

remove api.md for now
fix gentypes

(cherry picked from commit dd3253e)
(cherry picked from commit ba37e8d)
(cherry picked from commit b81e264)
(cherry picked from commit 692f062)
Copy link

socket-security bot commented Jan 17, 2024

No top level dependency changes detected. Learn more about Socket for GitHub ↗︎

@zardoy
Copy link
Contributor Author

zardoy commented Jan 17, 2024

almost done, I picked everything. will review it once again and mark it as ready.

I tried ts-standard instead of standard. While it uses some useful rules like @typescript-eslint/no-floating-promises it also includes too strict rules like @typescript-eslint/explicit-function-return-type that require you to write more code. Overall ts-standard ruleset looks good to me so if you want I can fix all errors.

@rom1504
Copy link
Member

rom1504 commented Jan 17, 2024

Please fix tests

@rom1504
Copy link
Member

rom1504 commented Jan 17, 2024

I see you deleted the doc. Can you show what a generated doc would look like ?

@rom1504
Copy link
Member

rom1504 commented Jan 17, 2024

Overall doesn't look worse than before.

But also it's not super clear to me what we gain here. Just adds a bunch of syntactic complexity and maybe a small amount of type safeness.

What do you think @extremeheat ?

@rom1504
Copy link
Member

rom1504 commented Jan 17, 2024

https://github.com/zardoy/space-squid/tree/ts-master/src/lib this is the part that may maybe benefit from TS but somehow you didn't convert it. Any reason?

@zardoy
Copy link
Contributor Author

zardoy commented Jan 17, 2024

I see you deleted the doc. Can you show what a generated doc would look like ?

Yes give me a moment. Also let's discuss it in the last step, not the biggest issue here imo.

Just adds a bunch of syntactic complexity

Let's revert things you don't like here. I just want basic IntelliSense for serv/entity/player (and typings!).

small amount of type safeness.

Yes I fixed a small amount of bugs that types helped me to catch. But in the current state the codebase is almost fully typed so referencing would allow you to prototype faster. Or did you mean something else?

zardoy/space-squid@ts-master/src/lib

can convert them as well if needed, I think this is just 5% of the codebase. I was really focused on plugins, so if plugins code look good...

Copy link
Contributor Author

@zardoy zardoy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also let's remove duplicating code const registry = require('prismarine-registry')(version) and instead set it to server like serv.resitry = ...

} finally {
process.stdout = tmp
}
// let tmp
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to revert i guess

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what was the purpose of that? I remember it was broking output in some way

Copy link
Contributor Author

@zardoy zardoy Jan 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, got the issue, it worked before because the code was not in strict mode (which is not good)

@@ -66,8 +71,15 @@ module.exports.player = async function (player, serv, settings) {

function updateInventory () {
playerData.inventory.forEach((item) => {
const itemName = item.id.value.slice(10) // skip game brand prefix
const itemValue: string | number = item.id.value
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better inventory decode

@@ -187,35 +186,35 @@ module.exports.server = function (serv, settings) {
serv.banUUID(username, reason)
.then(result => {
if (result) {
serv.emit('banned', ctx.player ? ctx.player : { username: '[@]' }, username, reason)
if (ctx.player) ctx.player.chat(username + ' was banned')
serv.emit('banned', (ctx.player != null) ? ctx.player : { username: '[@]' }, username, reason)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its standard-ts changed al these checks

@zardoy
Copy link
Contributor Author

zardoy commented Jan 17, 2024

I generated docs:
typedoc (without plugins) https://zardoy.github.io/space-squid/functions/createMCServer.html
real time generated docs from npm: https://paka.dev/npm/@zardoy/flying-squid

yes, need to clean up a few things like position: any or getSpawnPacket. But look at props like bannedPlayers that can reference other code. Imo all this looks useful.
But I'm just curious which generator do you think looks cleaner?

EDIT: there is a lot of noise on paka because it also included internal API, gonna update in a second

const generationModule = generations[generation.name] ? generations[generation.name] : require(generation.name)
serv.overworld = new World(generationModule(generation.options), regionFolder === undefined ? null : new Anvil(regionFolder))
serv.netherworld = new World(generations.nether(generation.options))
const generationOptions = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the code behaves the same, but it doesn't mutate generationOptions

const player = function (player: Player, serv: Server, settings: Options) {
const registry = RegistryLoader(settings.version)

player.flying = 0
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add player.flying

@@ -214,14 +244,6 @@ module.exports.player = function (player, serv, settings) {
})
}

function spiral (arr) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reuse spiral package (both still incorrect though)

@@ -252,21 +274,15 @@ module.exports.player = function (player, serv, settings) {
}

player.sendMap = () => {
return player.sendNearbyChunks(Math.min(3, settings['view-distance']))
.catch((err) => setTimeout(() => { throw err }), 0)
return player.sendNearbyChunks(settings['view-distance'])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the logic was hardcoded, I want to send more chunks

parse (str) {
return str.match(/^(((.* )?~?-?\d* ~?-?\d* ~?-?\d*)|(.+ .+))$/) ? str.split(' ') : false
},
action (args, ctx) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewrite tp implementation (fixed it)

player._client.write('tab_complete', {
matches: matches.filter((match) => match.startsWith(existingContent))
matches: !registry.supportFeature('tabCompleteHasAToolTip')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix crash on completions send in newer versions

serv.playSound(sound, player.world, null, { ...opt, whitelist: player })
}

// player.on('placeBlock_cancel', async ({ reference }, cancel) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented since events are not implemented in the code

player.prevGameMode = 255
player.gameMode = serv.gameMode
player.findSpawnPoint = async () => {
player.spawnPoint = await serv.getSpawnPoint(player.world)
}
player._client.on('settings', ({ viewDistance }) => {
player.view = viewDistance
player.sendRestMap()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bugfix, chunks with new view distance should be sended

@@ -74,7 +76,7 @@ module.exports.server = function (serv) {
if (arr.length === 0) throw new UserError('Could not find player')
arr.forEach(entity => {
entity.takeDamage({ damage: 20 })
serv.info(`Killed ${colors.bold(entity.type === 'player' ? entity.username : entity.name)}`)
serv.info(`Killed ${colors.bold(entity.type === 'player' ? (entity as Player).username : entity.name ?? '<unknown>')}`)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entity can be without name in some cases

function attackEntity (entityId) {
const attackedEntity = serv.entities[entityId]
if (!attackedEntity || (attackedEntity.gameMode !== 0 && attackedEntity.type === 'player')) return
const attackedPlayer = attackedEntity.type === 'player' ? attackedEntity as Player : undefined
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes a bit more verbose, but doesn't it look cleaner?

@@ -105,16 +105,16 @@ module.exports.server = function (serv, { version }) {
usage: '/give <player> <item> [count]',
tab: ['player', 'item', 'number'],
op: true,
parse (args, ctx) {
args = args.split(' ')
parse (_args, ctx) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ts pattern.

player.on('disconnected', () => serv.info(player.username + ' disconnected'))
player.on('chat', ({ message }) => serv.info('<' + player.username + '>' + ' ' + message))
player.on('disconnected', (reason) => serv.info(player.username + ' disconnected. Reason: ' + reason))
// player.on('chat', ({ message }) => serv.info('<' + player.username + '>' + ' ' + message))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chat event not implemented yet

} else {
// esbuild custom plugin
const files = require(/* webpackIgnore: true */ 'esbuild-import-glob(path:.,skipFiles:index.js,external.js)')
module.exports.builtinPlugins = Object.values(files)
const files = require(/* webpackIgnore: true */ 'esbuild-import-glob(path:.,skipFiles:index.js,external.js,index.ts,external.ts)')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we now have build step I think it would be better to remove these workarounds and instead create index.js with all plugins included. So we don't need to handle different platforms

@@ -180,7 +181,7 @@ module.exports.player = function (player, serv, { version }) {
player.changeBlock(data.position, 0, 0)
const aboveBlock = await player.world.getBlock(data.position.offset(0, 1, 0))
if (aboveBlock.material === 'plant') {
await player.setBlock(data.position.offset(0, 1, 0), 0, 0)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setblock doesn't have 3rd arg, could lead to potential bug

@@ -280,22 +283,22 @@ module.exports.server = function (serv, { version }) {
return !fail
})

if (type === 'near') sample.sort((a, b) => a.position.distanceTo(opt.pos) > b.position.distanceTo(opt.pos))
if (type === 'near') sample.sort((a, b) => a.position.distanceTo(opt.pos) - b.position.distanceTo(opt.pos))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this sorting was incorrect 😄 at least that one was caught by ts

@@ -251,6 +253,7 @@ module.exports.server = function (serv, { version }) {
})

sample = sample.filter(s => {
changeType<Player & Entity & { team, scores }>(s) // TODO implement team & scores
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an easy way to change any type without writing a bunch of unnecessary TS stuff

const data = parseInt(params[5] || 0, 10)
const stateId = registry.supportFeature('theFlattening') ? (blocks[id].minStateId + data) : (id << 4 | data)
const data = parseInt(params[5] || '0', 10)
const stateId = postFlatenning
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setblock was setting incorrect default state, fixed

player.changeBlock = async (position, blockType, blockData) => {
serv.players
.filter(p => p.world === player.world && player !== p)
.forEach(p => p.sendBlock(position, blockType, blockData))
.forEach(p => p.sendBlock(position, blockType/* , blockData */)) // todo
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was handled incorrectly anyway

const UserError = require('./user_error')
import UserError from './user_error'

type Ctx<P extends boolean> = P extends true ? {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i tried to not add a lot of generics, but this one here is just super useful for other plugins

Copy link
Contributor Author

@zardoy zardoy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

found a few unrelated to js->ts migration, they are not big, but i can remove them if they are blocking this pr

@zardoy
Copy link
Contributor Author

zardoy commented Jan 18, 2024

yes, I agree, typedoc is not the best generator here and its default look is really bad, but at least everything is structured. Anyway, I'm happy with making it all work, now its up to you whether to use it or not. If you still don't want to have anything like this, we can simply removing old docs (but I wouldn't be happy to keeping them up to date)

@zardoy
Copy link
Contributor Author

zardoy commented Jan 18, 2024

the current build time is not ok, if everything else is ok, I will add esbuild-based pipeline for faster dev builds.

also I'm curious why !src/plugins/README.md is in .npmignore? whats the point of publishing it?

@zardoy zardoy marked this pull request as ready for review January 18, 2024 16:23
@rom1504 rom1504 added this to Needs triage in PR Triage Jan 21, 2024
@rom1504 rom1504 moved this from Needs triage to Waiting for user input in PR Triage Jan 21, 2024
@zardoy zardoy changed the title TS 2 Full TypeScript rewrite. Best type defs Apr 18, 2024
@Pandapip1
Copy link
Contributor

+1 to using typescript. Having even a small amount of type safety saves so much debugging time.

@zardoy
Copy link
Contributor Author

zardoy commented Apr 22, 2024

saves so much debugging time.

You are right. If you write new modules or plugins you can catch bugs with the right types earlier.
But I'm doing it mostly for better DX and a way to pick definitions faster. This all just makes a huge difference for us and unfortunately, we simply can't work without types there. Also, unlike in mineflayer I can guarantee these types are correct since the source code reuses them. Btw can look at http://npmjs.com/package/@zardoy/flying-squid for a real demo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
PR Triage
  
Waiting for user input
Development

Successfully merging this pull request may close these issues.

Flow/TypeScript
3 participants