Merge pull request 'Merge demo into main branch' (#2) from dev/demo into master
Reviewed-on: http://localhost:3001/illyum/hudly/pulls/2
This commit is contained in:
commit
bc6c2c5988
76
README.md
Normal file
76
README.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//[07:41:31] [Client thread/INFO]: [CHAT] {"server":"mini208R","gametype":"HOUSING","mode":"dynamic","map":"Base"}
|
||||||
|
|
||||||
|
https://playerdb.co/api/player/minecraft/illyum
|
||||||
|
https://playerdb.co/
|
||||||
|
|
||||||
|
|
||||||
|
[CHAT] ONLINE: angryhacks
|
||||||
|
|
||||||
|
### When to look up stats
|
||||||
|
- on private message / dm
|
||||||
|
- Party Invite
|
||||||
|
- Guild Invite
|
||||||
|
- Friend Invite
|
||||||
|
- When you invite someone to party
|
||||||
|
- When party leader/other member invites party
|
||||||
|
- When party joins (/stream open)
|
||||||
|
- Duel Request (that specific duel's stats)
|
||||||
|
|
||||||
|
### Trackers
|
||||||
|
- Tips?
|
||||||
|
- WDR Reports?
|
||||||
|
|
||||||
|
### Other Stuff To Add
|
||||||
|
If you screenshot a leaderboard, it will check the screenshots folder and try to detect if a screenshot has a leaderboard in it, then it will load the leaderboard in and check for those people's stats and display them off to the side (daily only since other lb are on api)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Building:
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
```bash
|
||||||
|
set CGO_ENABLED=0
|
||||||
|
go build -ldflags="-s -w"
|
||||||
|
upx --best --lzma hudly.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mac (intel x86_64)
|
||||||
|
```bash
|
||||||
|
set GOOS=darwin
|
||||||
|
set GOARCH=amd64
|
||||||
|
set CGO_ENABLED=0
|
||||||
|
go build -ldflags="-s -w"
|
||||||
|
upx --best --lzma --force-macos hudly
|
||||||
|
```
|
||||||
|
NOTE: macOS is currently not supported
|
||||||
|
|
||||||
|
#### Mac (apple silicon ARM)
|
||||||
|
```bash
|
||||||
|
set GOOS=darwin
|
||||||
|
set GOARCH=arm64
|
||||||
|
set CGO_ENABLED=0
|
||||||
|
go build -ldflags="-s -w"
|
||||||
|
upx --best --lzma --force-macos hudly
|
||||||
|
```
|
||||||
|
NOTE: macOS is currently not supported
|
||||||
|
|
||||||
|
|
||||||
|
# TODOS/Limitation
|
||||||
|
- (api_key) Incorrect structure (api headers don't exist if key is invalid) so you get the wrong error code
|
||||||
|
- (build) Requires google's UUID library (too big for my liking)
|
||||||
|
- (client) No keep-alive implemented
|
||||||
|
- (client) No room closure detection
|
||||||
|
- (client) You can't see and send data (sender needs to have 2 clients, 1 to host and 1 to read)
|
||||||
|
- (client/config) Hard coded ip address / port
|
||||||
|
- (config) No Config (hard code key)
|
||||||
|
- (demo) Only in-memory uuid cache
|
||||||
|
- (demo) Lunar Client ONLY (default log location only)
|
||||||
|
- (demo) Requires working key to function
|
||||||
|
- (demo) Windows client sender ONLY (not correct log path locator)
|
||||||
|
- (demo) does NOT show nicked players (doesn't crash)
|
||||||
|
- (gui) Just terminal for now
|
||||||
|
- (hypixel_api) No cache
|
||||||
|
- (player) Only bedwars stats
|
||||||
|
- (server) Terrible status messages
|
||||||
|
- (server/server-config) Hard coded port
|
||||||
|
- (uuid cache) no lifetime (probably isn't needed but still)
|
1647
SINGLEFILE.go
Normal file
1647
SINGLEFILE.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
|||||||
package main
|
|
8
app/demo.go
Normal file
8
app/demo.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
var key = "f6999283-43ba-413e-a04d-32dbde98f423"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var demoApp = NewDemoApp(key)
|
||||||
|
demoApp.Start()
|
||||||
|
}
|
50
app/logbuf.go
Normal file
50
app/logbuf.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type LogBuffer struct {
|
||||||
|
strings []string
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogBuffer(size int) *LogBuffer {
|
||||||
|
return &LogBuffer{
|
||||||
|
strings: make([]string, 0, size),
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogBuffer) Add(s string) {
|
||||||
|
if len(l.strings) == l.size {
|
||||||
|
l.strings = l.strings[1:]
|
||||||
|
}
|
||||||
|
l.strings = append(l.strings, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogBuffer) Get() []string {
|
||||||
|
return l.strings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogBuffer) GetLast() (string, error) {
|
||||||
|
if len(l.strings) == 0 {
|
||||||
|
return "", fmt.Errorf("log buffer is empty")
|
||||||
|
}
|
||||||
|
return l.strings[len(l.strings)-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogBuffer) GetSecondToLast() (string, error) {
|
||||||
|
if len(l.strings) < 2 {
|
||||||
|
return "", fmt.Errorf("log buffer does not have enough lines")
|
||||||
|
}
|
||||||
|
return l.strings[len(l.strings)-2], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogBuffer) GetLineStepsBack(x int) (string, error) {
|
||||||
|
if x < 0 || x >= len(l.strings) {
|
||||||
|
return "", fmt.Errorf("log buffer does not have enough lines to step back %d times", x)
|
||||||
|
}
|
||||||
|
return l.strings[len(l.strings)-1-x], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//var LogBuf = NewLogBuffer(10)
|
60
app/logger.go
Normal file
60
app/logger.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppLogger = NewCustomLogger()
|
||||||
|
|
||||||
|
// CustomLogger wraps the standard logger
|
||||||
|
type CustomLogger struct {
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCustomLogger initializes the custom logger
|
||||||
|
func NewCustomLogger() *CustomLogger {
|
||||||
|
// Create a logger that writes to stdout with no flags
|
||||||
|
return &CustomLogger{
|
||||||
|
logger: log.New(os.Stdout, "", 0), // we will handle formatting manually
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logFormat retrieves the function name, file, and line number
|
||||||
|
func logFormat() string {
|
||||||
|
// Use the runtime.Caller to retrieve caller info
|
||||||
|
pc, file, line, ok := runtime.Caller(2)
|
||||||
|
if !ok {
|
||||||
|
file = "unknown"
|
||||||
|
line = 0
|
||||||
|
}
|
||||||
|
// Get function name
|
||||||
|
funcName := runtime.FuncForPC(pc).Name()
|
||||||
|
|
||||||
|
// Format timestamp
|
||||||
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
// Return formatted log prefix (timestamp, file, line, and function name)
|
||||||
|
return fmt.Sprintf("%s - %s:%d - %s: ", timestamp, file, line, funcName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info logs informational messages
|
||||||
|
func (c *CustomLogger) Info(msg string) {
|
||||||
|
c.logger.Println(logFormat() + "INFO: " + msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs error messages
|
||||||
|
func (c *CustomLogger) Error(msg string) {
|
||||||
|
c.logger.Println(logFormat() + "ERROR: " + msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage of the custom logger
|
||||||
|
func showcase() {
|
||||||
|
logger := NewCustomLogger()
|
||||||
|
|
||||||
|
logger.Info("This is an info message")
|
||||||
|
logger.Error("This is an error message")
|
||||||
|
}
|
455
app/main.go
455
app/main.go
@ -1,75 +1,404 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
netclient "hudly/client"
|
||||||
"hudly/hypixel"
|
"hudly/hypixel"
|
||||||
"hudly/mcfetch"
|
"hudly/mcfetch"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var key = "9634ea92-80f0-482f-aebd-b082c6ed6f19"
|
type PlayerWrapper struct {
|
||||||
var uuid = "5328930e-d411-49cb-90ad-4e5c7b27dd86"
|
Player hypixel.Player `json:"player"`
|
||||||
|
}
|
||||||
func demo() {
|
|
||||||
// Ensure a username is provided as a command-line argument
|
func replaceCorruptedRune(msg string) string {
|
||||||
if len(os.Args) < 2 {
|
runes := []rune(msg)
|
||||||
log.Fatal("Please provide a Minecraft username as a command-line argument.")
|
for i, r := range runes {
|
||||||
}
|
if r == '<27>' {
|
||||||
|
runes[i] = '§'
|
||||||
// Get the username from the command-line arguments
|
}
|
||||||
username := os.Args[1]
|
}
|
||||||
|
return string(runes)
|
||||||
thing := hypixel.NewAPIKey(key)
|
}
|
||||||
api := hypixel.NewAPI(*thing)
|
|
||||||
thing.UsesLeft = 11
|
func clearTerminal() {
|
||||||
|
var cmd *exec.Cmd
|
||||||
// Create a MemoryCache instance
|
if runtime.GOOS == "windows" {
|
||||||
memCache := &mcfetch.MemoryCache{}
|
cmd = exec.Command("cmd", "/c", "cls")
|
||||||
memCache.Init()
|
} else {
|
||||||
|
cmd = exec.Command("clear")
|
||||||
// Create a channel to receive the result
|
}
|
||||||
resultChan := make(chan map[string]interface{})
|
cmd.Stdout = os.Stdout
|
||||||
errorChan := make(chan error)
|
cmd.Run()
|
||||||
|
}
|
||||||
// Create an AsyncPlayerFetcher for asynchronous data fetching with MemoryCache
|
|
||||||
asyncFetcher := mcfetch.NewAsyncPlayerFetcher(
|
func calcRatio(numerator, denominator int) float64 {
|
||||||
username, // Minecraft username or UUID
|
if denominator == 0 {
|
||||||
memCache, // Pass the memory cache instance
|
return float64(numerator)
|
||||||
2, // Number of retries
|
}
|
||||||
2*time.Second, // Retry delay
|
return float64(numerator) / float64(denominator)
|
||||||
5*time.Second, // Request timeout
|
}
|
||||||
)
|
|
||||||
|
type DemoApp struct {
|
||||||
// Start asynchronous data fetching
|
Client *netclient.Client
|
||||||
asyncFetcher.FetchPlayerData(resultChan, errorChan)
|
API *hypixel.HypixelApi
|
||||||
|
MemCache *mcfetch.MemoryCache
|
||||||
// Non-blocking code execution (do something else while waiting)
|
LogBuf *LogBuffer
|
||||||
fmt.Println("Fetching data asynchronously...")
|
PartyBuilder []map[string]interface{}
|
||||||
|
}
|
||||||
var userID string
|
|
||||||
// Block until we receive data or an error
|
func NewDemoApp(key string) *DemoApp {
|
||||||
select {
|
var api_key = hypixel.NewAPIKey(key)
|
||||||
case data := <-resultChan:
|
app := &DemoApp{
|
||||||
fmt.Printf("Player data: %+v\n", data)
|
API: hypixel.NewAPI(*api_key),
|
||||||
|
MemCache: &mcfetch.MemoryCache{},
|
||||||
// Check if "uuid" exists and is not nil
|
LogBuf: NewLogBuffer(10),
|
||||||
if uuid, ok := data["id"].(string); ok {
|
PartyBuilder: []map[string]interface{}{},
|
||||||
userID = uuid
|
}
|
||||||
} else {
|
app.MemCache.Init()
|
||||||
fmt.Println(fmt.Sprintf("%+v", data))
|
return app
|
||||||
log.Fatal("UUID not found or invalid for player")
|
}
|
||||||
}
|
|
||||||
|
func (app *DemoApp) FetchMCPlayer(name string) (*mcfetch.FetchedPlayerResult, error) {
|
||||||
case err := <-errorChan:
|
asyncFetcher := mcfetch.NewPlayerFetcher(
|
||||||
log.Fatal(err)
|
name,
|
||||||
}
|
app.MemCache,
|
||||||
|
2,
|
||||||
// Use the Hypixel API to get additional player data
|
2*time.Second,
|
||||||
res, err := api.GetPlayerResponse(userID)
|
5*time.Second,
|
||||||
if err != nil {
|
)
|
||||||
panic(err)
|
data, err := asyncFetcher.FetchPlayerData()
|
||||||
}
|
if err != nil {
|
||||||
fmt.Println(fmt.Sprintf("%+v", res))
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DemoApp) onFileEmit(line string) {
|
||||||
|
msg := strings.TrimSpace(line)
|
||||||
|
if len(msg) < 34 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submsg := msg[33:]
|
||||||
|
if len(submsg) != 0 {
|
||||||
|
app.LogBuf.Add(submsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
OnlinePrefix := "[CHAT] ONLINE: "
|
||||||
|
PartyListSeparatorLinePrefix := "[CHAT] -----------------------------------------------------"
|
||||||
|
//PartyMemberCountPrefix := "[CHAT] Party Members ("
|
||||||
|
PartyLeaderPrefix := "[CHAT] Party Leader: "
|
||||||
|
PartyListMembersPrefix := "[CHAT] Party Members: "
|
||||||
|
|
||||||
|
if strings.HasPrefix(submsg, OnlinePrefix) { // Online Message
|
||||||
|
newsubmsg := strings.TrimPrefix(submsg, OnlinePrefix)
|
||||||
|
players := strings.Split(newsubmsg, ",")
|
||||||
|
var online []mcfetch.CacheResult
|
||||||
|
for _, player := range players {
|
||||||
|
playerName := strings.TrimSpace(player)
|
||||||
|
plr, err := app.FetchMCPlayer(playerName)
|
||||||
|
res_name := plr.Name
|
||||||
|
res_uuid := plr.UUID
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(fmt.Sprintf("Error fetching UUID: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid)
|
||||||
|
res_player := mcfetch.CacheResult{
|
||||||
|
UUID: plr.UUID,
|
||||||
|
Name: plr.Name,
|
||||||
|
}
|
||||||
|
online = append(online, res_player)
|
||||||
|
//names, err := GetNameFromUUID(playerUUID)
|
||||||
|
//if err != nil {
|
||||||
|
// log.Fatalf("Error fetching names from UUID: %v", err)
|
||||||
|
//}
|
||||||
|
//fmt.Printf("Name history for UUID %s: %v\n", playerUUID, names)
|
||||||
|
}
|
||||||
|
app.sendPartyList(online)
|
||||||
|
|
||||||
|
} else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List
|
||||||
|
last, _ := app.LogBuf.GetSecondToLast()
|
||||||
|
// TODO: Check if moderators
|
||||||
|
if !strings.HasPrefix(last, PartyListMembersPrefix) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix)
|
||||||
|
var ppl []mcfetch.CacheResult
|
||||||
|
for _, player := range strings.Split(PartyMembersMsg, ",") {
|
||||||
|
playerName := strings.TrimSpace(strings.TrimSuffix(player, " ?"))
|
||||||
|
if strings.HasPrefix(playerName, "[") {
|
||||||
|
playerName = strings.Split(playerName, " ")[1]
|
||||||
|
}
|
||||||
|
plr, err := app.FetchMCPlayer(playerName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error fetching Player: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res_name := plr.Name
|
||||||
|
res_uuid := plr.UUID
|
||||||
|
|
||||||
|
res_player := mcfetch.CacheResult{
|
||||||
|
UUID: plr.UUID,
|
||||||
|
Name: plr.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid)
|
||||||
|
ppl = append(ppl, res_player)
|
||||||
|
|
||||||
|
//playerName := strings.TrimSpace(player)
|
||||||
|
//playerUUID, err := GetUUIDFromName(playerName)
|
||||||
|
//if err != nil {
|
||||||
|
// log.Fatalf("Error fetching UUID: %v", err)
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
//fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID)
|
||||||
|
//cachedPlayer := CachedUuid{playerUUID, playerName, playerName, time.Now()}
|
||||||
|
//UuidCache.Add(&cachedPlayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Party Leader
|
||||||
|
leaders_msg, err := app.LogBuf.GetLineStepsBack(2)
|
||||||
|
if err != nil {
|
||||||
|
println("Unable to find party leader message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PartyLeaderMsg := strings.TrimPrefix(leaders_msg, PartyLeaderPrefix)
|
||||||
|
playerName := strings.TrimSpace(strings.TrimSuffix(PartyLeaderMsg, " ?"))
|
||||||
|
if strings.HasPrefix(playerName, "[") {
|
||||||
|
playerName = strings.Split(playerName, " ")[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
plr, err := app.FetchMCPlayer(playerName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error fetching Player: %v", err)
|
||||||
|
}
|
||||||
|
res_name := plr.Name
|
||||||
|
res_uuid := plr.UUID
|
||||||
|
|
||||||
|
res_player := mcfetch.CacheResult{
|
||||||
|
UUID: plr.UUID,
|
||||||
|
Name: plr.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid)
|
||||||
|
ppl = append(ppl, res_player)
|
||||||
|
|
||||||
|
// Parse Party Count
|
||||||
|
//party_count_msg, err := LogBuf.GetLineStepsBack(4)
|
||||||
|
//if err != nil {
|
||||||
|
// println("Unable to find party count message")
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
//PartyCountMsg := strings.TrimPrefix(party_count_msg, PartyMemberCountPrefix)
|
||||||
|
//count_str := strings.TrimSuffix(PartyCountMsg, ")")
|
||||||
|
//count, err := strconv.Atoi(count_str)
|
||||||
|
//if err != nil {
|
||||||
|
// println("Unable to parse party count message - Invalid number used")
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
//print("Expected ")
|
||||||
|
//print(count)
|
||||||
|
//print(" party members\n")
|
||||||
|
app.sendPartyList(ppl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
println(submsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DemoApp) tailFile(path string, lineCh chan<- string) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to open file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
file.Seek(0, 2)
|
||||||
|
|
||||||
|
reader := bufio.NewReader(file)
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lineCh <- line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DemoApp) sendPartyList(ppl []mcfetch.CacheResult) {
|
||||||
|
for _, user := range ppl {
|
||||||
|
res, err := app.API.GetPlayerResponse(user.UUID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get player data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Player.Stats.Bedwars.WLR = calcRatio(res.Player.Stats.Bedwars.Wins, res.Player.Stats.Bedwars.Losses)
|
||||||
|
res.Player.Stats.Bedwars.KDR = calcRatio(res.Player.Stats.Bedwars.Kills, res.Player.Stats.Bedwars.Deaths)
|
||||||
|
res.Player.Stats.Bedwars.FKDR = calcRatio(res.Player.Stats.Bedwars.FinalKills, res.Player.Stats.Bedwars.FinalDeaths)
|
||||||
|
res.Player.Stats.Bedwars.BBLR = calcRatio(res.Player.Stats.Bedwars.BedsBroken, res.Player.Stats.Bedwars.BedsLost)
|
||||||
|
|
||||||
|
playerJSON, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to marshal player data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerMap map[string]interface{}
|
||||||
|
json.Unmarshal(playerJSON, &playerMap)
|
||||||
|
app.PartyBuilder = append(app.PartyBuilder, playerMap)
|
||||||
|
|
||||||
|
message, err := json.Marshal(app.PartyBuilder)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to marshal stuff: %v", err)
|
||||||
|
}
|
||||||
|
err = app.Client.SendData(string(message))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error sending data: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Sent stuff:", app.PartyBuilder)
|
||||||
|
println("Sending Done!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear buffer so it only sends the current party list, not previous party lists
|
||||||
|
app.PartyBuilder = []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DemoApp) Start() {
|
||||||
|
var cmd string
|
||||||
|
fmt.Print(" | CREATE\n | JOIN\nEnter Choice:\n>")
|
||||||
|
fmt.Scanln(&cmd)
|
||||||
|
|
||||||
|
if cmd != "CREATE" && cmd != "JOIN" {
|
||||||
|
fmt.Println("Invalid command.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
app.Client, err = netclient.NewClient("chat.itzilly.com", uuid.New().String())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create client: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd == "CREATE" {
|
||||||
|
app.CreateRoom()
|
||||||
|
} else if cmd == "JOIN" {
|
||||||
|
err := app.JoinRoom()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("[DEV] Joined Branches\n")
|
||||||
|
|
||||||
|
app.Client.ListenForData()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case data, ok := <-app.Client.DataChannel:
|
||||||
|
if !ok {
|
||||||
|
fmt.Println("Data channel closed, exiting...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.HandleData(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Closing app")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DemoApp) CreateRoom() {
|
||||||
|
var err error
|
||||||
|
code, err := app.Client.CreateRoom("")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Created room:", code)
|
||||||
|
|
||||||
|
err = app.Client.JoinRoom(code, "password")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
println("Connected to room")
|
||||||
|
|
||||||
|
path := os.Getenv("USERPROFILE")
|
||||||
|
logPath := filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log")
|
||||||
|
|
||||||
|
fmt.Println("Reading log file from:", logPath)
|
||||||
|
|
||||||
|
lineCh := make(chan string)
|
||||||
|
go app.tailFile(logPath, lineCh)
|
||||||
|
|
||||||
|
// TODO: Do this in a different goroutine so that you can still listen to data from your own client
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case line := <-lineCh:
|
||||||
|
app.onFileEmit(replaceCorruptedRune(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DemoApp) JoinRoom() error {
|
||||||
|
var code string
|
||||||
|
var password string
|
||||||
|
fmt.Print("Enter Room Code:\n>")
|
||||||
|
fmt.Scanln(&code)
|
||||||
|
|
||||||
|
fmt.Print("Enter Room Password:\n>")
|
||||||
|
fmt.Scanln(&password)
|
||||||
|
|
||||||
|
err := app.Client.JoinRoom(code, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Joined room:", code)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DemoApp) HandleData(data string) {
|
||||||
|
var playerWrappers []PlayerWrapper
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(data), &playerWrappers)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error unmarshalling data:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var players []*hypixel.Player
|
||||||
|
for _, wrapper := range playerWrappers {
|
||||||
|
players = append(players, &wrapper.Player)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.DisplayPlayers(players)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DemoApp) DisplayPlayers(players []*hypixel.Player) {
|
||||||
|
clearTerminal()
|
||||||
|
|
||||||
|
fmt.Printf("| %-20s | %-10s | %-10s |\n", "Player Name", "Bedwars Level", "FKDR")
|
||||||
|
fmt.Println("|----------------------|------------|------------|")
|
||||||
|
|
||||||
|
for _, player := range players {
|
||||||
|
fmt.Printf("| %-20s | %-10d | %10.3f |\n",
|
||||||
|
player.DisplayName,
|
||||||
|
player.Achievements.BedwarsLevel,
|
||||||
|
player.Stats.Bedwars.FKDR)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("|----------------------|------------|------------|")
|
||||||
}
|
}
|
||||||
|
270
client/client.go
270
client/client.go
@ -1,75 +1,239 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Connect to the chat server
|
const PORT = ":5518"
|
||||||
func connectToServer(address string) (net.Conn, error) {
|
|
||||||
conn, err := net.Dial("tcp", address)
|
type PacketID int
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not connect to server: %v", err)
|
const (
|
||||||
}
|
CONNECT_REQUEST PacketID = iota
|
||||||
return conn, nil
|
CONNECT_RESPONSE
|
||||||
|
JOIN_REQUEST
|
||||||
|
JOIN_RESPONSE
|
||||||
|
CREATE_REQUEST
|
||||||
|
CREATE_RESPONSE
|
||||||
|
READ_DATA
|
||||||
|
SEND_DATA_REQUEST
|
||||||
|
SEND_DATA_RESPONSE
|
||||||
|
LEAVE_ROOM
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client represents a client connection to the server.
|
||||||
|
type Client struct {
|
||||||
|
Conn net.Conn
|
||||||
|
ClientID string
|
||||||
|
RoomCode string
|
||||||
|
DataChannel chan string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read input from the terminal and send it to the server
|
// NewClient creates a new client and connects to the server.
|
||||||
func readInputAndSend(conn net.Conn) {
|
func NewClient(serverAddr, clientID string) (*Client, error) {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
conn, err := net.Dial("tcp", serverAddr+PORT)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to server: %w", err)
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
Conn: conn,
|
||||||
|
ClientID: clientID,
|
||||||
|
DataChannel: make(chan string),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRoom creates a new room on the server.
|
||||||
|
func (c *Client) CreateRoom(password string) (string, error) {
|
||||||
|
request := CreateRoomRequestPacket{
|
||||||
|
UserID: c.ClientID,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.sendPacket(CREATE_REQUEST, request); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response CreateRoomResponsePacket
|
||||||
|
if err := c.receivePacket(CREATE_RESPONSE, &response); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.Success {
|
||||||
|
return "", errors.New(response.Reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.RoomCode = response.RoomCode
|
||||||
|
return response.RoomCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinRoom joins an existing room on the server.
|
||||||
|
func (c *Client) JoinRoom(roomCode, password string) error {
|
||||||
|
request := JoinRequestPacket{
|
||||||
|
UserID: c.ClientID,
|
||||||
|
RoomCode: roomCode,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.sendPacket(JOIN_REQUEST, request); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response JoinRequestResponsePacket
|
||||||
|
if err := c.receivePacket(JOIN_RESPONSE, &response); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.Success {
|
||||||
|
return errors.New(response.Reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.RoomCode = roomCode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListenForData() {
|
||||||
|
go func() {
|
||||||
for {
|
for {
|
||||||
fmt.Print("> ")
|
var dataPacket SendDataRequestPacket
|
||||||
text, _ := reader.ReadString('\n')
|
err := c.receivePacket(READ_DATA, &dataPacket)
|
||||||
text = strings.TrimSpace(text)
|
|
||||||
|
|
||||||
// Send the command to the server
|
|
||||||
_, err := conn.Write([]byte(text + "\n"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error sending message:", err)
|
if err == io.EOF {
|
||||||
|
fmt.Println("Connection closed by the server")
|
||||||
|
close(c.DataChannel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Error receiving data: %v\n", err)
|
||||||
|
close(c.DataChannel)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user types 'quit', exit the program
|
// Send the received data to the channel
|
||||||
if text == "quit" {
|
c.DataChannel <- dataPacket.Data
|
||||||
fmt.Println("Goodbye!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for incoming messages from the server
|
// SendData sends a message to all other clients in the room.
|
||||||
func listenForMessages(conn net.Conn) {
|
func (c *Client) SendData(data string) error {
|
||||||
reader := bufio.NewReader(conn)
|
request := SendDataRequestPacket{
|
||||||
for {
|
UserID: c.ClientID,
|
||||||
message, err := reader.ReadString('\n')
|
Data: data,
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Disconnected from server.")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
fmt.Print(message)
|
|
||||||
|
if err := c.sendPacket(SEND_DATA_REQUEST, request); err != nil {
|
||||||
|
return fmt.Errorf("failed to send data packet: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var response SendDataResponsePacket
|
||||||
|
if err := c.receivePacket(SEND_DATA_RESPONSE, &response); err != nil {
|
||||||
|
return fmt.Errorf("failed to receive response for sent data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.Success {
|
||||||
|
return errors.New("server failed to process the data request: " + response.Reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
// LeaveRoom disconnects the client from the current room.
|
||||||
if len(os.Args) < 2 {
|
func (c *Client) LeaveRoom() error {
|
||||||
fmt.Println("Usage: go run client.go <server-address>")
|
return c.sendPacket(LEAVE_ROOM, nil)
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
// Close closes the connection to the server.
|
||||||
serverAddress := os.Args[1]
|
func (c *Client) Close() {
|
||||||
conn, err := connectToServer(serverAddress)
|
c.Conn.Close()
|
||||||
if err != nil {
|
}
|
||||||
fmt.Println(err)
|
|
||||||
return
|
// sendPacket sends a packet with a given ID and data to the server.
|
||||||
}
|
func (c *Client) sendPacket(packetID PacketID, data interface{}) error {
|
||||||
defer conn.Close()
|
packetData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
// Start a goroutine to listen for incoming messages from the server
|
return fmt.Errorf("failed to encode packet: %w", err)
|
||||||
go listenForMessages(conn)
|
}
|
||||||
|
return writePacket(c.Conn, byte(packetID), packetData)
|
||||||
// Read input from the terminal and send it to the server
|
}
|
||||||
readInputAndSend(conn)
|
|
||||||
|
// receivePacket reads a response from the server for a given packet ID.
|
||||||
|
func (c *Client) receivePacket(expected PacketID, v interface{}) error {
|
||||||
|
packetID, data, err := readPacket(c.Conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if packetID != expected {
|
||||||
|
return fmt.Errorf("unexpected packet ID: got %v, want %v", packetID, expected)
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readPacket reads a packet from the connection.
|
||||||
|
func readPacket(conn net.Conn) (PacketID, []byte, error) {
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageType byte
|
||||||
|
if err := binary.Read(conn, binary.BigEndian, &messageType); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, length-5)
|
||||||
|
if _, err := io.ReadFull(conn, data); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return PacketID(messageType), data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePacket writes a packet to the connection.
|
||||||
|
func writePacket(conn net.Conn, messageType byte, data []byte) error {
|
||||||
|
length := uint32(5 + len(data))
|
||||||
|
if err := binary.Write(conn, binary.BigEndian, length); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := binary.Write(conn, binary.BigEndian, messageType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := conn.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateRoomRequestPacket struct {
|
||||||
|
UserID string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateRoomResponsePacket struct {
|
||||||
|
Success bool
|
||||||
|
Reason string
|
||||||
|
RoomCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinRequestPacket struct {
|
||||||
|
UserID string
|
||||||
|
RoomCode string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinRequestResponsePacket struct {
|
||||||
|
Success bool
|
||||||
|
Reason string
|
||||||
|
CurrentData string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendDataRequestPacket struct {
|
||||||
|
UserID string
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendDataResponsePacket struct {
|
||||||
|
Success bool
|
||||||
|
Reason string
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -67,3 +70,45 @@ func GetDefaultConfig() *Config {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveConfig saves the given config struct to the file in JSON format
|
||||||
|
func SaveConfig(config *Config, filePath string) error {
|
||||||
|
// Convert the config struct to JSON
|
||||||
|
data, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to serialize config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the JSON data to a file
|
||||||
|
err = ioutil.WriteFile(filePath, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write config to file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig loads the config from the given file path
|
||||||
|
func LoadConfig(filePath string) (*Config, error) {
|
||||||
|
// Check if the file exists
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("config file does not exist: %s", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the file content
|
||||||
|
data, err := ioutil.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Config object
|
||||||
|
config := &Config{}
|
||||||
|
|
||||||
|
// Deserialize the JSON data into the Config object
|
||||||
|
err = json.Unmarshal(data, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to deserialize config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
7
go.mod
7
go.mod
@ -1 +1,8 @@
|
|||||||
module hudly
|
module hudly
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
package hypixel
|
|
||||||
|
|
||||||
func DoThing() int {
|
|
||||||
return 1
|
|
||||||
}
|
|
@ -28,24 +28,27 @@ func NewAsyncPlayerFetcher(playerName string, cache ICache, retries int, retryDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchPlayerData fetches the player data asynchronously
|
// FetchPlayerData fetches the player data asynchronously using channels
|
||||||
func (pf *AsyncPlayerFetcher) FetchPlayerData(resultChan chan map[string]interface{}, errorChan chan error) {
|
func (pf *AsyncPlayerFetcher) FetchPlayerData(resultChan chan *FetchedPlayerResult, errorChan chan error) {
|
||||||
go func() {
|
go func() {
|
||||||
cachedData, found := pf.cache.Get(pf.playerName)
|
cachedData, found := pf.cache.Get(pf.playerName)
|
||||||
if found {
|
if found {
|
||||||
resultChan <- cachedData
|
resultChan <- (*FetchedPlayerResult)(cachedData)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var data map[string]interface{}
|
// If not in cache, make request to Mojang API
|
||||||
|
var player FetchedPlayerResult
|
||||||
for i := 0; i < pf.retries; i++ {
|
for i := 0; i < pf.retries; i++ {
|
||||||
resp, err := pf.makeRequest(pf.playerName)
|
resp, err := pf.makeRequest(pf.playerName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&data); err == nil {
|
// Decode the response into FetchedPlayerResult
|
||||||
pf.cache.Set(pf.playerName, data)
|
if err := json.NewDecoder(resp.Body).Decode(&player); err == nil {
|
||||||
|
// Store the result in the cache and return the data
|
||||||
|
pf.cache.Set(pf.playerName, (*CacheResult)(&player))
|
||||||
pf.cache.Sync()
|
pf.cache.Sync()
|
||||||
resultChan <- data
|
resultChan <- &player
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,16 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CacheResult struct {
|
||||||
|
UUID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
type ICache interface {
|
type ICache interface {
|
||||||
Init()
|
Init()
|
||||||
Load()
|
Load()
|
||||||
Get(key string) (map[string]interface{}, bool)
|
Get(key string) (*CacheResult, bool)
|
||||||
Set(key string, data map[string]interface{})
|
Set(key string, data *CacheResult)
|
||||||
Save()
|
Save()
|
||||||
Sync()
|
Sync()
|
||||||
Purge()
|
Purge()
|
||||||
@ -21,31 +26,31 @@ type ICache interface {
|
|||||||
|
|
||||||
// MemoryCache implementation
|
// MemoryCache implementation
|
||||||
type MemoryCache struct {
|
type MemoryCache struct {
|
||||||
cache map[string]interface{}
|
cache map[string]*CacheResult
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the cache (no-op for MemoryCache)
|
// Init initializes the cache (no-op for MemoryCache)
|
||||||
func (c *MemoryCache) Init() {
|
func (c *MemoryCache) Init() {
|
||||||
c.cache = make(map[string]interface{})
|
c.cache = make(map[string]*CacheResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads the cache (no-op for MemoryCache)
|
// Load loads the cache (no-op for MemoryCache)
|
||||||
func (c *MemoryCache) Load() {}
|
func (c *MemoryCache) Load() {}
|
||||||
|
|
||||||
// Get retrieves an item from the cache
|
// Get retrieves an item from the cache
|
||||||
func (c *MemoryCache) Get(key string) (map[string]interface{}, bool) {
|
func (c *MemoryCache) Get(key string) (*CacheResult, bool) {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
value, found := c.cache[key]
|
value, found := c.cache[key]
|
||||||
if !found {
|
if !found {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return value.(map[string]interface{}), true
|
return value, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set stores an item in the cache
|
// Set stores an item in the cache
|
||||||
func (c *MemoryCache) Set(key string, data map[string]interface{}) {
|
func (c *MemoryCache) Set(key string, data *CacheResult) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
c.cache[key] = data
|
c.cache[key] = data
|
||||||
@ -64,19 +69,19 @@ func (c *MemoryCache) Purge() {}
|
|||||||
func (c *MemoryCache) Clear() {
|
func (c *MemoryCache) Clear() {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
c.cache = make(map[string]interface{})
|
c.cache = make(map[string]*CacheResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JsonFileCache implementation
|
// JsonFileCache implementation
|
||||||
type JsonFileCache struct {
|
type JsonFileCache struct {
|
||||||
filename string
|
filename string
|
||||||
cache map[string]interface{}
|
cache map[CacheResult]interface{}
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the cache
|
// Init initializes the cache
|
||||||
func (c *JsonFileCache) Init() {
|
func (c *JsonFileCache) Init() {
|
||||||
c.cache = make(map[string]interface{})
|
c.cache = make(map[CacheResult]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads the cache from a JSON file
|
// Load loads the cache from a JSON file
|
||||||
@ -104,21 +109,22 @@ func (c *JsonFileCache) Load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves an item from the cache
|
// Get retrieves an item from the cache
|
||||||
func (c *JsonFileCache) Get(key string) (map[string]interface{}, bool) {
|
func (c *JsonFileCache) Get(key string) (*CacheResult, bool) {
|
||||||
c.mu.RLock()
|
//c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
//defer c.mu.RUnlock()
|
||||||
value, found := c.cache[key]
|
//value, found := c.cache[key]
|
||||||
if !found {
|
//if !found {
|
||||||
|
// return nil, false
|
||||||
|
//}
|
||||||
|
//return value, true
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
|
||||||
return value.(map[string]interface{}), true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set stores an item in the cache
|
// Set stores an item in the cache
|
||||||
func (c *JsonFileCache) Set(key string, data map[string]interface{}) {
|
func (c *JsonFileCache) Set(key string, data *CacheResult) {
|
||||||
c.mu.Lock()
|
//c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
//defer c.mu.Unlock()
|
||||||
c.cache[key] = data
|
//c.cache[key] = data
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves the cache to a JSON file
|
// Save saves the cache to a JSON file
|
||||||
@ -150,5 +156,5 @@ func (c *JsonFileCache) Purge() {}
|
|||||||
func (c *JsonFileCache) Clear() {
|
func (c *JsonFileCache) Clear() {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
c.cache = make(map[string]interface{})
|
c.cache = make(map[CacheResult]interface{})
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,11 @@ type PlayerFetcher struct {
|
|||||||
cache ICache
|
cache ICache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FetchedPlayerResult struct {
|
||||||
|
UUID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
// NewPlayerFetcher creates a new PlayerFetcher with an abstract cache (ICache)
|
// NewPlayerFetcher creates a new PlayerFetcher with an abstract cache (ICache)
|
||||||
func NewPlayerFetcher(playerName string, cache ICache, retries int, retryDelay time.Duration, timeout time.Duration) *PlayerFetcher {
|
func NewPlayerFetcher(playerName string, cache ICache, retries int, retryDelay time.Duration, timeout time.Duration) *PlayerFetcher {
|
||||||
cache.Init()
|
cache.Init()
|
||||||
@ -29,21 +34,21 @@ func NewPlayerFetcher(playerName string, cache ICache, retries int, retryDelay t
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FetchPlayerData fetches the player data synchronously
|
// FetchPlayerData fetches the player data synchronously
|
||||||
func (pf *PlayerFetcher) FetchPlayerData() (map[string]interface{}, error) {
|
func (pf *PlayerFetcher) FetchPlayerData() (*FetchedPlayerResult, error) {
|
||||||
cachedData, found := pf.cache.Get(pf.playerName)
|
//cachedData, found := pf.cache.Get(pf.playerName)
|
||||||
if found {
|
//if found {
|
||||||
return cachedData, nil
|
// return &FetchedPlayerResult{}, nil
|
||||||
}
|
//}
|
||||||
|
|
||||||
var data map[string]interface{}
|
var player FetchedPlayerResult
|
||||||
for i := 0; i < pf.retries; i++ {
|
for i := 0; i < pf.retries; i++ {
|
||||||
resp, err := pf.makeRequest(pf.playerName)
|
resp, err := pf.makeRequest(pf.playerName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&data); err == nil {
|
if err := json.NewDecoder(resp.Body).Decode(&player); err == nil {
|
||||||
pf.cache.Set(pf.playerName, data)
|
// pf.cache.Set(pf.playerName, player)
|
||||||
pf.cache.Sync()
|
// pf.cache.Sync()
|
||||||
return data, nil
|
return &player, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
time.Sleep(pf.retryDelay)
|
time.Sleep(pf.retryDelay)
|
||||||
|
573
server/server.go
573
server/server.go
@ -1,201 +1,99 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const PORT = ":5518"
|
||||||
|
|
||||||
|
type PacketID int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CONNECT_REQUEST PacketID = iota
|
||||||
|
CONNECT_RESPONSE
|
||||||
|
JOIN_REQUEST
|
||||||
|
JOIN_RESPONSE
|
||||||
|
CREATE_REQUEST
|
||||||
|
CREATE_RESPONSE
|
||||||
|
READ_DATA
|
||||||
|
SEND_DATA_REQUEST
|
||||||
|
SEND_DATA_RESPONSE
|
||||||
|
LEAVE_ROOM
|
||||||
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
conn net.Conn
|
Conn net.Conn
|
||||||
username string
|
ClientID string
|
||||||
room *Room
|
RoomCode string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Room struct {
|
type Room struct {
|
||||||
code string
|
Code string
|
||||||
password string
|
Password string
|
||||||
clients map[*Client]bool
|
Clients []*Client
|
||||||
lock sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var rooms = map[string]*Room{}
|
||||||
rooms = make(map[string]*Room)
|
var clients = map[string]*Client{}
|
||||||
mu sync.Mutex
|
var mu sync.Mutex
|
||||||
)
|
|
||||||
|
|
||||||
// Helper function to generate a 4-hexadecimal room code
|
func readPacket(conn net.Conn) (PacketID, []byte, error) {
|
||||||
func generateRoomCode() string {
|
// First, read the length of the packet (4 bytes)
|
||||||
rand.Seed(time.Now().UnixNano())
|
var length uint32
|
||||||
return fmt.Sprintf("%04x", rand.Intn(0xFFFF))
|
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then read the packet ID (1 byte)
|
||||||
|
var messageType byte
|
||||||
|
if err := binary.Read(conn, binary.BigEndian, &messageType); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the remaining data (length - 5 bytes, since 4 bytes for length and 1 byte for messageType)
|
||||||
|
data := make([]byte, length-5)
|
||||||
|
if _, err := io.ReadFull(conn, data); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return PacketID(int(messageType)), data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle client connection
|
func writePacket(conn net.Conn, messageType byte, data []byte) error {
|
||||||
func handleClient(client *Client) {
|
// Calculate the total length of the packet
|
||||||
defer client.conn.Close()
|
// 4 bytes for length, 1 byte for messageType
|
||||||
|
length := uint32(5 + len(data))
|
||||||
|
|
||||||
reader := bufio.NewReader(client.conn)
|
// Write the length and the message type
|
||||||
|
if err := binary.Write(conn, binary.BigEndian, length); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := binary.Write(conn, binary.BigEndian, messageType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
// Write the data
|
||||||
line, err := reader.ReadString('\n')
|
_, err := conn.Write(data)
|
||||||
if err != nil {
|
return err
|
||||||
if client.room != nil {
|
|
||||||
leaveRoom(client)
|
|
||||||
}
|
|
||||||
fmt.Println("Client disconnected:", client.conn.RemoteAddr())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
processCommand(client, strings.TrimSpace(line))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process client commands
|
func main() {
|
||||||
func processCommand(client *Client, input string) {
|
listener, err := net.Listen("tcp", PORT)
|
||||||
parts := strings.SplitN(input, " ", 2)
|
|
||||||
if len(parts) < 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := strings.ToUpper(parts[0])
|
|
||||||
args := ""
|
|
||||||
if len(parts) > 1 {
|
|
||||||
args = parts[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cmd {
|
|
||||||
case "CREATE":
|
|
||||||
createRoom(client, args)
|
|
||||||
case "JOIN":
|
|
||||||
joinRoom(client, args)
|
|
||||||
case "LEAVE":
|
|
||||||
leaveRoom(client)
|
|
||||||
case "MESSAGE":
|
|
||||||
sendMessage(client, args)
|
|
||||||
case "SYNC":
|
|
||||||
handleSync(client, args)
|
|
||||||
default:
|
|
||||||
client.conn.Write([]byte("Unknown command\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a room with an optional password
|
|
||||||
func createRoom(client *Client, args string) {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
password := ""
|
|
||||||
if args != "" {
|
|
||||||
password = args // Treat all input after CREATE as a password
|
|
||||||
}
|
|
||||||
|
|
||||||
roomCode := generateRoomCode() // Room code is generated automatically
|
|
||||||
room := &Room{
|
|
||||||
code: roomCode,
|
|
||||||
password: password,
|
|
||||||
clients: make(map[*Client]bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
rooms[roomCode] = room
|
|
||||||
client.conn.Write([]byte(fmt.Sprintf("Room created with code: %s\n", roomCode)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join a room using its 4-hexadecimal code and optional password
|
|
||||||
func joinRoom(client *Client, args string) {
|
|
||||||
parts := strings.SplitN(args, " ", 2)
|
|
||||||
roomCode := parts[0]
|
|
||||||
password := ""
|
|
||||||
if len(parts) == 2 {
|
|
||||||
password = parts[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
room, exists := rooms[roomCode]
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
client.conn.Write([]byte("Room not found\n"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if room.password != "" && room.password != password {
|
|
||||||
client.conn.Write([]byte("Incorrect password\n"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
room.lock.Lock()
|
|
||||||
room.clients[client] = true
|
|
||||||
client.room = room
|
|
||||||
room.lock.Unlock()
|
|
||||||
|
|
||||||
client.conn.Write([]byte(fmt.Sprintf("Joined room: %s\n", roomCode)))
|
|
||||||
broadcastMessage(client.room, fmt.Sprintf("%s has joined the room\n", client.username))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leave the current room
|
|
||||||
func leaveRoom(client *Client) {
|
|
||||||
if client.room == nil {
|
|
||||||
client.conn.Write([]byte("You are not in any room\n"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client.room.lock.Lock()
|
|
||||||
delete(client.room.clients, client)
|
|
||||||
client.room.lock.Unlock()
|
|
||||||
|
|
||||||
broadcastMessage(client.room, fmt.Sprintf("%s has left the room\n", client.username))
|
|
||||||
client.conn.Write([]byte("You have left the room\n"))
|
|
||||||
client.room = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a message to all clients in the current room
|
|
||||||
func sendMessage(client *Client, message string) {
|
|
||||||
if client.room == nil {
|
|
||||||
client.conn.Write([]byte("You are not in any room\n"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
formattedMessage := fmt.Sprintf("[%s] %s: %s\n", timestamp, client.username, message)
|
|
||||||
broadcastMessage(client.room, formattedMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast a message to all clients in the room
|
|
||||||
func broadcastMessage(room *Room, message string) {
|
|
||||||
room.lock.Lock()
|
|
||||||
defer room.lock.Unlock()
|
|
||||||
for client := range room.clients {
|
|
||||||
client.conn.Write([]byte(message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the SYNC command, expecting a JSON payload
|
|
||||||
func handleSync(client *Client, payload string) {
|
|
||||||
var data map[string]interface{}
|
|
||||||
err := json.Unmarshal([]byte(payload), &data)
|
|
||||||
if err != nil {
|
|
||||||
client.conn.Write([]byte("Invalid JSON\n"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can process the JSON payload here as needed
|
|
||||||
client.conn.Write([]byte("Sync received\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the server
|
|
||||||
func startServer() {
|
|
||||||
listener, err := net.Listen("tcp", ":5518")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error starting server:", err)
|
fmt.Println("Error starting server:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
fmt.Println("Server started on port 5518")
|
fmt.Println("Server started on port", PORT)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
@ -203,21 +101,338 @@ func startServer() {
|
|||||||
fmt.Println("Error accepting connection:", err)
|
fmt.Println("Error accepting connection:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
go handleClient(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
type ConnectionRequestPacket struct {
|
||||||
conn.Write([]byte("Enter your username: "))
|
UserID string
|
||||||
username, _ := bufio.NewReader(conn).ReadString('\n')
|
RoomCode string
|
||||||
username = strings.TrimSpace(username)
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectionResponsePacket struct {
|
||||||
|
Success bool
|
||||||
|
Reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateRoomRequestPacket struct {
|
||||||
|
UserID string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateRoomResponsePacket struct {
|
||||||
|
Success bool
|
||||||
|
Reason string
|
||||||
|
RoomCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinRequestPacket struct {
|
||||||
|
UserID string
|
||||||
|
Password string
|
||||||
|
RoomCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinRequestResponsePacket struct {
|
||||||
|
Success bool
|
||||||
|
Reason string
|
||||||
|
CurrentData string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendDataRequestPacket struct {
|
||||||
|
UserID string
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardDataPacket struct {
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendDataResponsePacket struct {
|
||||||
|
Success bool
|
||||||
|
Reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleClient(conn net.Conn) {
|
||||||
|
// defer conn.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
var packetId PacketID
|
||||||
|
packetId, data, err := readPacket(conn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error reading packet (handleclient) :", err)
|
||||||
|
handleLeaveRoom(conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch packetId {
|
||||||
|
case CONNECT_REQUEST:
|
||||||
|
var packet ConnectionRequestPacket
|
||||||
|
if err := json.Unmarshal(data, &packet); err != nil {
|
||||||
|
fmt.Println("Error decoding connection request:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleConnectRequest(conn, packet)
|
||||||
|
case CREATE_REQUEST:
|
||||||
|
var packet CreateRoomRequestPacket
|
||||||
|
if err := json.Unmarshal(data, &packet); err != nil {
|
||||||
|
fmt.Println("Error decoding connection request:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleCreateRoomRequest(conn, packet)
|
||||||
|
case JOIN_REQUEST:
|
||||||
|
var packet JoinRequestPacket
|
||||||
|
if err := json.Unmarshal(data, &packet); err != nil {
|
||||||
|
fmt.Println("Error decoding connection request:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleJoinRequest(conn, packet)
|
||||||
|
case SEND_DATA_REQUEST:
|
||||||
|
var packet SendDataRequestPacket
|
||||||
|
if err := json.Unmarshal(data, &packet); err != nil {
|
||||||
|
fmt.Println("Error decoding connection request:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleSendData(conn, packet)
|
||||||
|
case LEAVE_ROOM:
|
||||||
|
handleLeaveRoom(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleConnectRequest(conn net.Conn, packet ConnectionRequestPacket) {
|
||||||
|
fmt.Println("Incoming connection request")
|
||||||
|
room := findRoom(packet.RoomCode)
|
||||||
|
if room == nil {
|
||||||
|
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
|
||||||
|
Success: false,
|
||||||
|
Reason: "room does not exist",
|
||||||
|
}))
|
||||||
|
fmt.Println("Attempted room does not exist")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if room.Password == "" || packet.Password == room.Password {
|
||||||
|
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
|
||||||
|
Success: true,
|
||||||
|
Reason: "",
|
||||||
|
}))
|
||||||
|
fmt.Println("Invalid password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
|
||||||
|
Success: false,
|
||||||
|
Reason: "invalid password",
|
||||||
|
}))
|
||||||
|
fmt.Println("Connected user to room ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCreateRoomRequest(conn net.Conn, packet CreateRoomRequestPacket) {
|
||||||
|
if !isValidPassword(packet.Password) {
|
||||||
|
writePacket(conn, byte(CREATE_RESPONSE), encodeResponsePacket(CreateRoomResponsePacket{
|
||||||
|
Success: false,
|
||||||
|
Reason: "invalid password",
|
||||||
|
RoomCode: "",
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := generateRandomRoomCode()
|
||||||
|
|
||||||
|
room := &Room{
|
||||||
|
Code: code,
|
||||||
|
Password: packet.Password,
|
||||||
|
Clients: []*Client{},
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
rooms[code] = room
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
writePacket(conn, byte(CREATE_RESPONSE), encodeResponsePacket(CreateRoomResponsePacket{
|
||||||
|
Success: true,
|
||||||
|
Reason: "",
|
||||||
|
RoomCode: code,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleJoinRequest(conn net.Conn, packet JoinRequestPacket) {
|
||||||
|
room := findRoom(packet.RoomCode)
|
||||||
|
if room == nil {
|
||||||
|
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
|
||||||
|
Success: false,
|
||||||
|
Reason: "room not found",
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidPassword(packet.Password) {
|
||||||
|
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
|
||||||
|
Success: false,
|
||||||
|
Reason: "invalid password",
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if room.Password == "" || room.Password == packet.Password {
|
||||||
client := &Client{
|
client := &Client{
|
||||||
conn: conn,
|
Conn: conn,
|
||||||
username: username,
|
ClientID: packet.UserID,
|
||||||
|
RoomCode: packet.RoomCode,
|
||||||
}
|
}
|
||||||
conn.Write([]byte(fmt.Sprintf("Welcome %s!\n", username)))
|
|
||||||
handleClient(client)
|
mu.Lock()
|
||||||
}()
|
room.Clients = append(room.Clients, client)
|
||||||
|
clients[packet.UserID] = client
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
|
||||||
|
Success: true,
|
||||||
|
Reason: "",
|
||||||
|
CurrentData: "{}",
|
||||||
|
// TODO: Send current data
|
||||||
|
}))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
|
||||||
|
Success: false,
|
||||||
|
Reason: "invalid password",
|
||||||
|
}))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func handleSendData(conn net.Conn, packet SendDataRequestPacket) {
|
||||||
startServer()
|
fmt.Println("Incoming send data request")
|
||||||
|
mu.Lock()
|
||||||
|
client, exists := clients[packet.UserID]
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if !exists || client.RoomCode == "" {
|
||||||
|
writePacket(conn, byte(SEND_DATA_RESPONSE), encodeResponsePacket(SendDataResponsePacket{
|
||||||
|
Success: false,
|
||||||
|
Reason: "client not in a room",
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
room := findRoom(client.RoomCode)
|
||||||
|
if room == nil {
|
||||||
|
writePacket(conn, byte(SEND_DATA_RESPONSE), encodeResponsePacket(SendDataResponsePacket{
|
||||||
|
Success: false,
|
||||||
|
Reason: "room not found",
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataPacket := encodeResponsePacket(ForwardDataPacket{
|
||||||
|
Data: packet.Data,
|
||||||
|
})
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
for _, roomClient := range room.Clients {
|
||||||
|
// if roomClient.ClientID != packet.UserID {
|
||||||
|
// // Send data to all other clients except the sender
|
||||||
|
// writePacket(roomClient.Conn, byte(READ_DATA), dataPacket)
|
||||||
|
// }
|
||||||
|
writePacket(roomClient.Conn, byte(READ_DATA), dataPacket)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
writePacket(conn, byte(SEND_DATA_RESPONSE), encodeResponsePacket(SendDataResponsePacket{
|
||||||
|
Success: true,
|
||||||
|
Reason: "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLeaveRoom(conn net.Conn) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
var leavingClient *Client
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.Conn == conn {
|
||||||
|
leavingClient = client
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if leavingClient == nil {
|
||||||
|
fmt.Println("Client not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
room := rooms[leavingClient.RoomCode]
|
||||||
|
if room == nil {
|
||||||
|
fmt.Println("Room not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, client := range room.Clients {
|
||||||
|
if client == leavingClient {
|
||||||
|
room.Clients = append(room.Clients[:i], room.Clients[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the room if its empty
|
||||||
|
if len(room.Clients) == 0 {
|
||||||
|
delete(rooms, room.Code)
|
||||||
|
fmt.Println("Room", room.Code, "deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(clients, leavingClient.ClientID)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
leavingClient.Conn.Close()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
log.Fatalln("Timeout while closing leaving client's connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Client", leavingClient.ClientID, "left the room and connection closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func findRoom(code string) *Room {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
return rooms[code]
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeResponsePacket(packet interface{}) []byte {
|
||||||
|
data, err := json.Marshal(packet)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error encoding response packet:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomRoomCode() string {
|
||||||
|
validChars := "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
code := make([]byte, 4)
|
||||||
|
for i := range code {
|
||||||
|
code[i] = validChars[rand.Intn(len(validChars))]
|
||||||
|
}
|
||||||
|
return string(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidPassword(password string) bool {
|
||||||
|
validChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!()[]{}<>-_"
|
||||||
|
charMap := make(map[rune]bool)
|
||||||
|
for _, char := range validChars {
|
||||||
|
charMap[char] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, char := range password {
|
||||||
|
if !charMap[char] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user