package main import ( "fmt" "log" "net" "os" "time" "github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" "tea.melonie54.xyz/sean/go-susapp/src/enum" "tea.melonie54.xyz/sean/go-susapp/src/gamedata" "tea.melonie54.xyz/sean/go-susapp/src/packets" "tea.melonie54.xyz/sean/go-susapp/src/protocol" "tea.melonie54.xyz/sean/go-susapp/src/util" ) const appID = "xyz.melonie54.susapp" func main() { fmt.Println("Booting Sus App") sus := &SusApp{} sus.startApp() fmt.Println("Goodbye :(") } type SusApp struct { state *State application *gtk.Application appWindow *gtk.ApplicationWindow renderer *Renderer keyInput *KeyInputHandler mouseInput *MouseInputHandler packetProcessor *PacketProcessor movementProcessor *MovementProcessor networkHandler *protocol.PacketHandler } func (sus *SusApp) startApp() { // Create Gtk Application, change appID to your application domain name reversed. var err error sus.application, err = gtk.ApplicationNew(appID, glib.APPLICATION_NON_UNIQUE) // Check to make sure no errors when creating Gtk Application if err != nil { log.Fatal("Could not create application.", err) } renderTicker := time.NewTicker(100 * time.Millisecond) renderTickerDone := make(chan bool) defer func() { fmt.Println("Stopping render ticks") renderTicker.Stop() renderTickerDone <- true }() movementTicker := time.NewTicker(50 * time.Millisecond) movementTickerDone := make(chan bool) defer func() { fmt.Println("Stopping movement ticks") movementTicker.Stop() movementTickerDone <- true }() sus.state = CreateAppState(GenerateVersionNumber(2021, 6, 30, 0)) if sus.state == nil { fmt.Println("Error loading new app state") return } sus.renderer = NewRenderer(sus.state) sus.keyInput = NewKeyInputHandler(sus.state) sus.mouseInput = NewMouseInputHandler(sus.state, sus.renderer, sus.playGame, sus.openSettings, sus.disconnectFromGame) sus.packetProcessor = NewPacketProcessor(sus) sus.movementProcessor = &MovementProcessor{sus: sus} fmt.Printf("Starting Sus App %s\n", GetVersionFormatted(sus.state.Version)) sus.application.Connect("activate", func() { w := 800 h := 450 // Create ApplicationWindow sus.appWindow, err = gtk.ApplicationWindowNew(sus.application) if err != nil { log.Fatal("Could not create application window.", err) } // Set ApplicationWindow Properties sus.appWindow.SetTitle("Sus App") sus.appWindow.SetDefaultSize(w, h) layout, _ := gtk.LayoutNew(nil, nil) sus.appWindow.Add(layout) da, _ := gtk.DrawingAreaNew() da.SetHAlign(gtk.ALIGN_FILL) da.SetVAlign(gtk.ALIGN_FILL) da.SetHExpand(true) da.SetVExpand(true) da.SetSizeRequest(w, h) layout.Add(da) // Listen for ticks from renderTicker go func() { for { select { case <-renderTickerDone: return case <-renderTicker.C: da.QueueDraw() } } }() // Listen for ticks from movementTicker go func() { z := time.Now().UnixMilli() for { select { case <-movementTickerDone: return case a := <-movementTicker.C: y := a.UnixMilli() - z z = a.UnixMilli() sus.movementProcessor.Tick(y) } } }() r := sus.renderer da.Connect("draw", func(da *gtk.DrawingArea, cr *cairo.Context) { r.Draw(da, cr) }) da.AddEvents(int(gdk.KEY_RELEASE_MASK) | int(gdk.KEY_PRESS_MASK) | int(gdk.EVENT_CONFIGURE) | int(gdk.EVENT_BUTTON_PRESS)) da.Connect("event", sus.mouseInput.motionEvent) sus.appWindow.Connect("button-press-event", sus.mouseInput.buttonPressEvent) sus.appWindow.Connect("key-press-event", sus.keyInput.keyPressEvent) sus.appWindow.Connect("key-release-event", sus.keyInput.keyReleaseEvent) sus.appWindow.Connect("configure-event", func(window *gtk.ApplicationWindow, ev *gdk.Event) { e := gdk.EventConfigureNewFromEvent(ev) da.SetSizeRequest(e.Width(), e.Height()) }) sus.appWindow.Connect("destroy", func(window *gtk.ApplicationWindow) bool { fmt.Println("Closing Sus App") sus.disconnectFromGame() sus.application.Quit() return true }) sus.appWindow.ShowAll() }) sus.application.Run(os.Args) } func (sus *SusApp) setupPacketHandler(ip *net.IP, port int, firstPacket func(ph *protocol.PacketHandler)) { addr := net.UDPAddr{ Port: port, IP: *ip, } ser, err := net.DialUDP("udp", nil, &addr) if err != nil { fmt.Printf("Some error %v\n", err) return } defer ser.Close() fmt.Printf("Starting connection: %v => %v\n", ser.LocalAddr().String(), ser.RemoteAddr().String()) nethandler := protocol.NewPacketHandler(ser, &addr) nethandler.HandleNormalPacket = sus.handleNormalPacket nethandler.HandleReliablePacket = sus.handleReliablePacket nethandler.HandleForceDisconnectPacket = sus.handleForceDisconnectPacket nethandler.HandleNormalDisconnectPacket = sus.handleNormalDisconnectPacket nethandler.HandleAcknowledgementPacket = sus.handleAcknowledgementPacket nethandler.HandlePingPacket = sus.handlePingPacket sus.networkHandler = nethandler p := make([]byte, 4096) firstPacket(nethandler) for { n, _, err := ser.ReadFromUDP(p) if n != 0 { if err != nil { fmt.Printf("Some error %v\n", err) continue } go nethandler.ReadPacket(p[:n]) } } } func (sus *SusApp) playGame() { sus.state.Screen = enum.SCREEN_WAITING sus.state.IsTypingReady = false addrstr := fmt.Sprintf("%v:%v", sus.state.Settings.ServerAddress, sus.state.Settings.ServerPort) raddr, err := net.ResolveUDPAddr("udp", addrstr) if err != nil { fmt.Printf("Failed to resolve address: %v\n", addrstr) return } go sus.setupPacketHandler(&raddr.IP, raddr.Port, func(ph *protocol.PacketHandler) { fmt.Println("Sending first packet") ph.SendHelloPacket(ph.GetNonce(), 0, sus.state.Version, sus.state.Settings.PlayerName, 0, sus.state.Settings.Language, sus.state.Settings.QuickChatMode) var y int32 if sus.state.TypedGameID == "" { sus.state.TypedGameID = util.CodeFromGameID(0x20000000) } y, _ = util.CodeToGameID(sus.state.TypedGameID) var z protocol.HazelPayload = &packets.JoinGameC2S{GameID: y} ph.SendReliablePacket(ph.GetNonce(), []*protocol.Hazel{ protocol.NewHazelFromPayload(&z), }) }) } func (sus *SusApp) openSettings() { openSettingsWindow(sus.application, nil, sus.state) } func (sus *SusApp) disconnectFromGame() { sus.state.IsLobbyMenuOpen = false if sus.networkHandler != nil { sus.networkHandler.SendNormalDisconnectPacket() } } // TODO: implement all these functions func (sus *SusApp) handleNormalPacket(net *protocol.PacketHandler, h []*protocol.Hazel) { sus.packetProcessor.receiveNormalPacket(net, h) } func (sus *SusApp) handleReliablePacket(net *protocol.PacketHandler, nonce uint16, h []*protocol.Hazel) { sus.packetProcessor.receiveReliablePacket(net, nonce, h) } func (sus *SusApp) handleForceDisconnectPacket(net *protocol.PacketHandler, h *protocol.Hazel) { net.SendNormalDisconnectPacket() p := h.GetUnderlyingPacket() sus.state.DisconnectReason = p.Read() if p.Remaining() > 0 { sus.state.DisconnectStringReason = p.ReadString() sus.state.DisconnectReason = 0xfe } sus.state.Screen = enum.SCREEN_ERROR sus.state.IsLobbyMenuOpen = false } func (sus *SusApp) handleNormalDisconnectPacket(net *protocol.PacketHandler) { net.SendNormalDisconnectPacket() sus.state.DisconnectReason = 0xff sus.state.Screen = enum.SCREEN_TITLE sus.state.IsLobbyMenuOpen = false } func (sus *SusApp) handleAcknowledgementPacket(net *protocol.PacketHandler, nonce uint16, missing byte) { if sus.state.CurrentGame != nil && nonce != 0 { if nonce == sus.state.CurrentGame.CheckNameNonce && sus.state.CurrentGame.CheckedNameAndColor&0b01 == 0 { sus.state.CurrentGame.CheckedNameAndColor |= 0b01 if sus.state.CurrentGame.CheckedNameAndColor == 0b11 && sus.state.CurrentGame.CheckedNameAndColor&0b10 == 0 { sus.sendPetHatAndSkin(net) } } else if nonce == sus.state.CurrentGame.CheckColorNonce { sus.state.CurrentGame.CheckedNameAndColor |= 0b10 if sus.state.CurrentGame.CheckedNameAndColor == 0b11 { sus.sendPetHatAndSkin(net) } } } } func (sus *SusApp) handlePingPacket(net *protocol.PacketHandler, nonce uint16) { // TODO: setup acknowledgement tracking and remove hard coded 0xff net.SendAcknowledgementPacket(nonce, 0xff) } func (sus *SusApp) sendPetHatAndSkin(net *protocol.PacketHandler) { // Send RPC SetPet, SetHat, SetSkin sus.state.CurrentGame.CheckPetHatAndSkinNonce = net.GetNonce() var f1a gamedata.RpcSub = &gamedata.RpcSetPet{PetID: enum.PET_ELLIE_ROSE + 1} var f2a packets.GameDataSubpacket = gamedata.NewRpcFromRpcSub(sus.state.CurrentGame.ClientPlayerNetID, &f1a) var f1b gamedata.RpcSub = &gamedata.RpcSetHat{HatID: enum.HAT_HEART_PIN + 1} var f2b packets.GameDataSubpacket = gamedata.NewRpcFromRpcSub(sus.state.CurrentGame.ClientPlayerNetID, &f1b) var f1c gamedata.RpcSub = &gamedata.RpcSetSkin{SkinID: enum.SKIN_RIGHT_HAND_MAN_REBORN} var f2c packets.GameDataSubpacket = gamedata.NewRpcFromRpcSub(sus.state.CurrentGame.ClientPlayerNetID, &f1c) var f3 protocol.HazelPayload = &packets.GameDataAll{GameID: sus.state.CurrentGame.GameID, Subpackets: []*protocol.Hazel{ f2a.AsHazel(), f2b.AsHazel(), f2c.AsHazel(), }} net.SendReliablePacket(sus.state.CurrentGame.CheckPetHatAndSkinNonce, []*protocol.Hazel{protocol.NewHazelFromPayload(&f3)}) }