package main import ( "errors" "flag" "fmt" "image" "image/color" "log" "math" "net" "os" "strings" "sync" "gioui.org/app" "gioui.org/f32" "gioui.org/font/gofont" "gioui.org/io/system" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" "gioui.org/text" "gioui.org/widget" "gioui.org/widget/material" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/chunk" "github.com/df-mc/dragonfly/server/world/mcdb" "github.com/sandertv/gophertunnel/minecraft" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) const ( state_not_connected = iota state_working state_saving state_done ) var G_window *app.Window var G_state int = state_working var theme = material.NewTheme(gofont.Collection()) var finish_button widget.Clickable // the state used for drawing and saving type WorldState struct { Dimension int Dimensions map[int]*mcdb.Provider Entities map[int][]world.SaveableEntity ChunkCount int PlayerPos packet.MovePlayer img image.NRGBA _mutex sync.Mutex } var world_state *WorldState = &WorldState{ Dimensions: make(map[int]*mcdb.Provider), Entities: make(map[int][]world.SaveableEntity), PlayerPos: packet.MovePlayer{}, _mutex: sync.Mutex{}, } func init() { register_command("world", "Launch world downloading proxy", world_main) } func world_main(args []string) error { var target string var help bool flag.StringVar(&target, "target", "", "target server") flag.BoolVar(&help, "help", false, "show help") fmt.Printf("%v\n", args) flag.CommandLine.Parse(args) if help { flag.Usage() return nil } if target == "" { target = input_server() } if len(strings.Split(target, ":")) == 1 { target += ":19132" } _status := minecraft.NewStatusProvider("Server") listener, err := minecraft.ListenConfig{ StatusProvider: _status, }.Listen("raknet", ":19132") if err != nil { return err } defer listener.Close() go func() { for { c, err := listener.Accept() if err != nil { log.Fatal(err) } // not a goroutine, only 1 client at a time handleConn(c.(*minecraft.Conn), listener, target) } }() go func() { G_window = app.NewWindow() if err := run_gui(target); err != nil { log.Fatal(err) } os.Exit(0) }() app.Main() return nil } func ProcessSubChunk(sub_chunk *packet.SubChunk) { } func draw_chunk(pos protocol.ChunkPos, ch *chunk.Chunk) { // TODO } func ProcessChunk(pk *packet.LevelChunk) { ch, err := chunk.NetworkDecode(uint32(pk.HighestSubChunk), pk.RawPayload, int(pk.SubChunkCount), cube.Range{-64, 320}) if err != nil { log.Fatal(err) } world_state.Dimensions[world_state.Dimension].SaveChunk(world.ChunkPos(pk.Position), ch) world_state.ChunkCount++ draw_chunk(pk.Position, ch) G_window.Invalidate() } func ProcessActor(actor *packet.AddActor) { // TODO } func ProcessBlockUpdate(update *packet.UpdateBlock) { // TODO } func ProcessMove(player *packet.MovePlayer) { world_state.PlayerPos = *player G_window.Invalidate() } func SetState(state int) { G_state = state G_window.Invalidate() } func handleConn(conn *minecraft.Conn, listener *minecraft.Listener, target string) { var packet_func func(header packet.Header, payload []byte, src, dst net.Addr) = nil if G_debug { packet_func = PacketLogger } serverConn, err := minecraft.Dialer{ TokenSource: G_src, ClientData: conn.ClientData(), PacketFunc: packet_func, }.Dial("raknet", target) if err != nil { panic(err) } var g sync.WaitGroup g.Add(2) go func() { if err := conn.StartGame(serverConn.GameData()); err != nil { panic(err) } g.Done() }() go func() { if err := serverConn.DoSpawn(); err != nil { panic(err) } g.Done() }() g.Wait() SetState(state_working) go func() { // client loop defer listener.Disconnect(conn, "connection lost") defer serverConn.Close() defer finish_button.Click() for { pk, err := conn.ReadPacket() if err != nil { return } switch _pk := pk.(type) { case *packet.RequestChunkRadius: pk = &packet.RequestChunkRadius{ // rewrite packet to send a bigger radius ChunkRadius: 32, } case *packet.MovePlayer: ProcessMove(_pk) } if err := serverConn.WritePacket(pk); err != nil { if disconnect, ok := errors.Unwrap(err).(minecraft.DisconnectError); ok { _ = listener.Disconnect(conn, disconnect.Error()) } return } } }() go func() { // server loop defer serverConn.Close() defer listener.Disconnect(conn, "connection lost") defer finish_button.Click() for { pk, err := serverConn.ReadPacket() if err != nil { if disconnect, ok := errors.Unwrap(err).(minecraft.DisconnectError); ok { _ = listener.Disconnect(conn, disconnect.Error()) } return } switch pk := pk.(type) { case *packet.ChangeDimension: world_state.Dimension = int(pk.Dimension) world_state.Entities[world_state.Dimension] = nil world_state.Dimensions[world_state.Dimension], err = mcdb.New(fmt.Sprintf("worlds/%d", world_state.Dimension), world.Overworld) if err != nil { panic(err) } case *packet.LevelChunk: ProcessChunk(pk) case *packet.SubChunk: ProcessSubChunk(pk) case *packet.AddActor: ProcessActor(pk) case *packet.UpdateBlock: ProcessBlockUpdate(pk) case *packet.ChunkRadiusUpdated: fmt.Printf("ChunkRadiusUpdated: %d\n", pk.ChunkRadius) } if err := conn.WritePacket(pk); err != nil { return } } }() } func draw_rect(gtx layout.Context, rect image.Rectangle, col color.NRGBA) { cl := clip.Rect{Min: rect.Min, Max: rect.Max}.Push(gtx.Ops) paint.ColorOp{Color: col}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) cl.Pop() } func layout_chunks(gtx layout.Context) layout.Dimensions { world_state._mutex.Lock() draw_player_icon(gtx) world_state._mutex.Unlock() return layout.Dimensions{Size: image.Point{X: 100, Y: 100}} } func draw_player_icon(gtx layout.Context) { player := world_state.PlayerPos op.Affine(f32.Affine2D{}.Rotate(f32.Pt(5, 5), player.HeadYaw*(math.Pi/180))).Add(gtx.Ops) // rotate and offset relative to first chunk draw_rect(gtx, image.Rectangle{image.Point{X: 0, Y: 0}, image.Point{X: 10, Y: 10}}, color.NRGBA{255, 180, 0, 255}) } func draw_working(gtx layout.Context) { layout.Stack{ Alignment: layout.Center, }.Layout(gtx, layout.Stacked(func(gtx layout.Context) layout.Dimensions { return layout.Flex{ Axis: layout.Vertical, }.Layout(gtx, layout.Flexed(0.1, func(gtx layout.Context) layout.Dimensions { // top text title := material.H2(theme, fmt.Sprintf("Chunks: %d\n", world_state.ChunkCount)) title.Alignment = text.Middle title.Color = color.NRGBA{R: 127, G: 0, B: 0, A: 255} return title.Layout(gtx) }), layout.Flexed(0.9, func(gtx layout.Context) layout.Dimensions { // centered chunk view return layout.Center.Layout(gtx, layout_chunks) }), ) }), layout.Stacked(func(gtx layout.Context) layout.Dimensions { b := material.Button(theme, &finish_button, "Finish") b.Color = color.NRGBA{R: 255, G: 255, B: 255, A: 255} return b.Layout(gtx) }), ) if finish_button.Clicked() { SetState(state_saving) go begin_save_world(world_state) } } func begin_save_world(world *WorldState) { for i, dim := range world.Dimensions { dim.Close() world.Dimensions[i] = nil } } func draw_saving(gtx layout.Context) { } func run_gui(target string) error { th := material.NewTheme(gofont.Collection()) var ops op.Ops for { e := <-G_window.Events() switch e := e.(type) { case system.DestroyEvent: return e.Err case system.FrameEvent: gtx := layout.NewContext(&ops, e) switch G_state { case state_not_connected: title := material.H1(th, fmt.Sprintf("Connect to %s to start", "thelocalserverip")) title.Alignment = text.Middle title.Color = color.NRGBA{R: 127, G: 0, B: 0, A: 255} title.Layout(gtx) case state_working: draw_working(gtx) case state_saving: draw_saving(gtx) } e.Frame(gtx.Ops) } } }