package server import ( "io" "os" "fmt" "time" "regexp" "slices" "strings" "git.citrons.xyz/metronode/phony" "git.citrons.xyz/metronode/classic" ) type player struct { phony.Inbox state playerState client *client server *Server name string extensions map[string]bool level *level levelLoaded bool lastMovementUpdate time.Time currentMessage string } type authLevel int type playerState struct { LevelId levelId Pos entityPos Facing entityFacing Auth authLevel } const ( defaultAuth = 0 cheatAuth = 100 moderateAuth = 150 opAuth = 200 ConsoleAuth = 900 ) var playerNameRegex = regexp.MustCompile("^[.-_a-zA-Z0-9]*$") func loadPlayerData( from phony.Actor, name string, loaded func(playerState, bool)) { loadDataFile(from, "player/" + name, loaded) } func savePlayerData( from phony.Actor, name string, state playerState, done func()) { saveDataFile(from, "player/" + name, state) if done != nil { dataManager.Act(nil, done) } } func newPlayer( s *Server, cl *client, name string, ext map[string]bool) *player { pl := &player { client: cl, server: s, name: name, extensions: ext, } loadPlayerData(pl, name, func(state playerState, ok bool) { if ok { pl.state = state pl.ChangeLevel(pl, state.LevelId, state.Pos) } else { s.SendToSpawn(pl, pl) } }) return pl } func (p *player) save(done func()) { os.Mkdir("world/player", 0777) savePlayerData(p, p.name, p.state, done) } func (p *player) kick(reason string) { p.save(nil) p.client.Disconnect(p, reason) if p.level != nil { p.level.OnRemovePlayer(p) p.level = nil } p.server.OnLeave(p, p.name) } func (p *player) sendAuthInfo() { var userType, hacks byte if p.state.Auth >= cheatAuth { userType = classic.OpUser hacks = 1 } p.client.SendPacket(p, &classic.UpdateUserType { Type: userType, }) if p.extensions["HackControl"] { p.client.SendPacket(p, &classic.HackControl { Flying: hacks, NoClip: hacks, Speeding: hacks, SpawnControl: hacks, ThirdPersonView: 1, JumpHeight: 40, }) } if p.extensions["BlockPermissions"] { p.client.SendPackets(p, getBlockPermissionPackets(p.state.Auth)) } } func (p *player) handlePacket(packet classic.Packet) { if p.level == nil || !p.levelLoaded { return } switch pck := packet.(type) { case *classic.SetPosFacingExt: newPos := entityPos { entityCoord(pck.X), entityCoord(pck.Y), entityCoord(pck.Z), } newFacing := entityFacing {pck.Yaw, pck.Pitch} if newPos == p.state.Pos && newFacing == p.state.Facing { if time.Since(p.lastMovementUpdate) < time.Second { break } } p.lastMovementUpdate = time.Now() p.state.Pos = newPos p.state.Facing = newFacing p.level.OnMovePlayer(p, p.state.Pos, p.state.Facing) case *classic.ClientSetBlock: block := blockType(pck.Block) if pck.Mode == classic.BlockDestroyed { block = 0 } pos := blockPos { blockCoord(pck.X), blockCoord(pck.Y), blockCoord(pck.Z), } p.level.SetBlock(p, pos, block) case *classic.Message: incomplete := pck.PlayerId == 1 && p.extensions["LongerMessages"] message := p.currentMessage + classic.UnpadString(pck.Message) if !incomplete { p.handleChat(message) p.currentMessage = "" } else { p.currentMessage += string(pck.Message[:]) // preserve whitespace } } } func (p *player) handleChat(message string) { isCmd, text := isCommand(message) if strings.TrimSpace(text) == "" { return } if !isCmd { message = fmt.Sprintf("&7<&b%s&7>&f %s", p.name, message) p.server.Chat.Send(p, p, message) } else { p.server.ExecuteCommand(p, p.state.Auth, text) } } func (p *player) Save(from phony.Actor, done func()) { p.Act(from, func() { p.save(func() { if done != nil { from.Act(nil, done) } }) }) } func (p *player) Kick(from phony.Actor, reason string) { p.Act(from, func() { p.kick(reason) }) } func (p *player) SetAuthLevel(from phony.Actor, auth authLevel) { p.Act(from, func() { p.state.Auth = auth p.sendAuthInfo() }) } func (p *player) OnPacket(from phony.Actor, packet classic.Packet) { p.Act(from, func() { p.handlePacket(packet) }) } func (p *player) joinLevel(id levelId, lvl *level, pos entityPos) { p.state.LevelId = id p.state.Pos = pos p.level = lvl p.levelLoaded = false lvl.OnAddPlayer(p, p.name, pos) } func (p *player) ChangeLevel(from phony.Actor, lvl levelId, pos entityPos) { p.Act(from, func() { if p.level != nil { p.level.OnRemovePlayer(p) } p.server.GetLevel(p, lvl, func(l *level) { p.joinLevel(lvl, l, pos) }) }) } func (p *player) MovePlayer( from phony.Actor, pos entityPos, facing entityFacing) { p.Act(from, func() { p.state.Pos = pos p.state.Facing = facing p.client.SendPacket(p, &classic.SetPosFacingExt { PlayerId: -1, X: classic.Fixed(pos.X), Y: classic.Fixed(pos.Y), Z: classic.Fixed(pos.Z), Yaw: facing.Yaw, Pitch: facing.Pitch, }) }) } func (p *player) GetInfo(from phony.Actor, reply func(name string, state playerState)) { p.Act(from, func() { name := p.name state := p.state from.Act(nil, func() {reply(name, state)}) }) } func (p *player) SendMessage(from phony.Actor, message string) { p.Act(from, func() { p.client.SendPackets(p, slices.Values(processChatMessage(message))) }) } func (p *player) OnChatMessage(from *gameChat, sender any, message string) { p.SendMessage(from, message) } func (p *player) OnCommandOutput(from *Server, output string) { p.SendMessage(from, "&e" + output) } func (p *player) OnCommandError(from *Server, err string) { p.SendMessage(from, "&c" + err) } func (p *player) OnLevelData(from *level, info levelInfo, data io.ReadCloser) { p.Act(from, func() { if from != p.level { return } packets := func(yield func(classic.Packet) bool) { defer data.Close() for { var packet classic.LevelDataChunk n, err := io.ReadFull(data, packet.Data[:]) if err == io.EOF || err == io.ErrUnexpectedEOF { if n == 0 { break } } else if err != nil { panic(err) } packet.Length = int16(n) if !yield(&packet) { return } } } p.client.SendPackets(p, packets) p.client.SendPacket(p, &classic.LevelFinalize { Width: int16(info.Size.X), Height: int16(info.Size.Y), Length: int16(info.Size.Z), }) p.client.SendPacket(p, &classic.AddEntity2 { EntityId: 255, InGameName: classic.PadString(p.name), SkinName: classic.PadString(p.name), X: classic.Fixed(p.state.Pos.X), Y: classic.Fixed(p.state.Pos.Y), Z: classic.Fixed(p.state.Pos.Z), Yaw: p.state.Facing.Yaw, Pitch: p.state.Facing.Pitch, }) p.sendAuthInfo() p.levelLoaded = true }) } func (p *player) OnLevelError(from *level, message string, info levelInfo) { p.SendMessage(from, "&cCannot join level: " + message) if !info.IsSpawn { p.Act(from, func() { p.server.SendToSpawn(p, p) }) } else { p.Kick(from, "Error: " + message) } } func (p *player) OnPlayer( from *level, id levelPlayerId, name string, pos entityPos) { p.Act(from, func() { p.client.SendPacket(p, &classic.AddEntity2 { EntityId: byte(id), InGameName: classic.PadString(name), SkinName: classic.PadString(name), X: classic.Fixed(pos.X), Y: classic.Fixed(pos.Y), Z: classic.Fixed(pos.Z), }) }) } func (p *player) OnRemovePlayer(from *level, id levelPlayerId) { p.Act(from, func() { p.client.SendPacket(p, &classic.DespawnPlayer {int8(id)}) }) } func (p *player) OnAddListEntry(from *Server, entry listEntry) { p.client.SendPacket(p, &classic.AddPlayerName { NameId: int16(entry.id), PlayerName: classic.PadString(entry.playerName), ListName: classic.PadString(entry.listName), }) } func (p *player) OnRemoveListEntry(from *Server, id listId) { p.client.SendPacket(p, &classic.RemovePlayerName { NameId: int16(id), }) } func (p *player) OnMovePlayer( from *level, id levelPlayerId, pos entityPos, facing entityFacing) { p.Act(from, func() { p.client.SendPacket(p, &classic.SetPosFacingExt { PlayerId: int8(id), X: classic.Fixed(pos.X), Y: classic.Fixed(pos.Y), Z: classic.Fixed(pos.Z), Yaw: facing.Yaw, Pitch: facing.Pitch, }) }) } func (p *player) OnSetBlock(from *level, pos blockPos, block blockType) { p.client.SendPacket(p, &classic.SetBlock { X: int16(pos.X), Y: int16(pos.Y), Z: int16(pos.Z), Block: byte(block), }) }