2024-10-22 18:49:14 -06:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2024-10-26 19:34:56 -06:00
|
|
|
|
"bufio"
|
2024-10-25 22:20:55 -06:00
|
|
|
|
"encoding/json"
|
2024-10-22 18:49:14 -06:00
|
|
|
|
"fmt"
|
2024-10-25 22:20:55 -06:00
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
netclient "hudly/client"
|
2024-10-22 18:49:14 -06:00
|
|
|
|
"hudly/hypixel"
|
|
|
|
|
"hudly/mcfetch"
|
|
|
|
|
"log"
|
|
|
|
|
"os"
|
2024-10-26 19:34:56 -06:00
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"runtime"
|
|
|
|
|
"strings"
|
2024-10-22 18:49:14 -06:00
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
type PlayerWrapper struct {
|
|
|
|
|
Player hypixel.Player `json:"player"`
|
|
|
|
|
}
|
2024-10-22 18:49:14 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
func replaceCorruptedRune(msg string) string {
|
|
|
|
|
runes := []rune(msg)
|
|
|
|
|
for i, r := range runes {
|
|
|
|
|
if r == '<27>' {
|
|
|
|
|
runes[i] = '§'
|
|
|
|
|
}
|
2024-10-22 18:49:14 -06:00
|
|
|
|
}
|
2024-10-26 19:34:56 -06:00
|
|
|
|
return string(runes)
|
|
|
|
|
}
|
2024-10-22 18:49:14 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
func clearTerminal() {
|
|
|
|
|
var cmd *exec.Cmd
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
cmd = exec.Command("cmd", "/c", "cls")
|
|
|
|
|
} else {
|
|
|
|
|
cmd = exec.Command("clear")
|
|
|
|
|
}
|
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
|
cmd.Run()
|
|
|
|
|
}
|
2024-10-22 18:49:14 -06:00
|
|
|
|
|
2024-10-26 20:59:03 -06:00
|
|
|
|
func calcRatio(numerator, denominator int) float64 {
|
2024-10-26 19:34:56 -06:00
|
|
|
|
if denominator == 0 {
|
|
|
|
|
return float64(numerator)
|
|
|
|
|
}
|
|
|
|
|
return float64(numerator) / float64(denominator)
|
|
|
|
|
}
|
2024-10-22 18:49:14 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
type DemoApp struct {
|
|
|
|
|
Client *netclient.Client
|
|
|
|
|
API *hypixel.HypixelApi
|
|
|
|
|
MemCache *mcfetch.MemoryCache
|
|
|
|
|
LogBuf *LogBuffer
|
|
|
|
|
PartyBuilder []map[string]interface{}
|
|
|
|
|
}
|
2024-10-22 18:49:14 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
func NewDemoApp(key string) *DemoApp {
|
|
|
|
|
var api_key = hypixel.NewAPIKey(key)
|
|
|
|
|
app := &DemoApp{
|
|
|
|
|
API: hypixel.NewAPI(*api_key),
|
|
|
|
|
MemCache: &mcfetch.MemoryCache{},
|
|
|
|
|
LogBuf: NewLogBuffer(10),
|
|
|
|
|
PartyBuilder: []map[string]interface{}{},
|
|
|
|
|
}
|
|
|
|
|
app.MemCache.Init()
|
|
|
|
|
return app
|
|
|
|
|
}
|
2024-10-22 18:49:14 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
func (app *DemoApp) FetchMCPlayer(name string) (*mcfetch.FetchedPlayerResult, error) {
|
|
|
|
|
asyncFetcher := mcfetch.NewPlayerFetcher(
|
|
|
|
|
name,
|
|
|
|
|
app.MemCache,
|
|
|
|
|
2,
|
|
|
|
|
2*time.Second,
|
|
|
|
|
5*time.Second,
|
2024-10-22 18:49:14 -06:00
|
|
|
|
)
|
2024-10-26 19:34:56 -06:00
|
|
|
|
data, err := asyncFetcher.FetchPlayerData()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return data, nil
|
|
|
|
|
}
|
2024-10-22 18:49:14 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
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)
|
|
|
|
|
}
|
2024-10-22 18:49:14 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
OnlinePrefix := "[CHAT] ONLINE: "
|
|
|
|
|
PartyListSeparatorLinePrefix := "[CHAT] -----------------------------------------------------"
|
|
|
|
|
//PartyMemberCountPrefix := "[CHAT] Party Members ("
|
|
|
|
|
PartyLeaderPrefix := "[CHAT] Party Leader: "
|
|
|
|
|
PartyListMembersPrefix := "[CHAT] Party Members: "
|
2024-10-22 18:49:14 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
if strings.HasPrefix(submsg, OnlinePrefix) { // Online Message
|
|
|
|
|
newsubmsg := strings.TrimPrefix(submsg, OnlinePrefix)
|
|
|
|
|
players := strings.Split(newsubmsg, ",")
|
2024-10-26 20:59:03 -06:00
|
|
|
|
var online []mcfetch.CacheResult
|
2024-10-26 19:34:56 -06:00
|
|
|
|
for _, player := range players {
|
|
|
|
|
playerName := strings.TrimSpace(player)
|
|
|
|
|
plr, err := app.FetchMCPlayer(playerName)
|
|
|
|
|
res_name := plr.Name
|
|
|
|
|
res_uuid := plr.UUID
|
|
|
|
|
if err != nil {
|
2024-10-26 20:59:03 -06:00
|
|
|
|
fmt.Println(fmt.Sprintf("Error fetching UUID: %v", err))
|
|
|
|
|
continue
|
2024-10-26 19:34:56 -06:00
|
|
|
|
}
|
|
|
|
|
fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid)
|
2024-10-26 20:59:03 -06:00
|
|
|
|
res_player := mcfetch.CacheResult{
|
|
|
|
|
UUID: plr.UUID,
|
|
|
|
|
Name: plr.Name,
|
|
|
|
|
}
|
|
|
|
|
online = append(online, res_player)
|
2024-10-26 19:34:56 -06:00
|
|
|
|
//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)
|
2024-10-22 18:49:14 -06:00
|
|
|
|
}
|
2024-10-26 20:59:03 -06:00
|
|
|
|
app.sendPartyList(online)
|
2024-10-22 18:49:14 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
} 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
|
2024-10-22 18:49:14 -06:00
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
println(submsg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *DemoApp) tailFile(path string, lineCh chan<- string) {
|
|
|
|
|
file, err := os.Open(path)
|
2024-10-22 18:49:14 -06:00
|
|
|
|
if err != nil {
|
2024-10-26 19:34:56 -06:00
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 20:59:03 -06:00
|
|
|
|
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)
|
2024-10-26 19:34:56 -06:00
|
|
|
|
|
|
|
|
|
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!")
|
2024-10-22 18:49:14 -06:00
|
|
|
|
}
|
2024-10-26 20:59:03 -06:00
|
|
|
|
|
|
|
|
|
// Clear buffer so it only sends the current party list, not previous party lists
|
|
|
|
|
app.PartyBuilder = []map[string]interface{}{}
|
2024-10-22 18:49:14 -06:00
|
|
|
|
}
|
2024-10-25 22:20:55 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
func (app *DemoApp) Start() {
|
2024-10-25 22:20:55 -06:00
|
|
|
|
var cmd string
|
|
|
|
|
fmt.Print(" | CREATE\n | JOIN\nEnter Choice:\n>")
|
|
|
|
|
fmt.Scanln(&cmd)
|
|
|
|
|
|
|
|
|
|
if cmd != "CREATE" && cmd != "JOIN" {
|
|
|
|
|
fmt.Println("Invalid command.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
var err error
|
2024-10-26 20:59:03 -06:00
|
|
|
|
app.Client, err = netclient.NewClient("chat.itzilly.com", uuid.New().String())
|
2024-10-25 22:20:55 -06:00
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Failed to create client: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
if cmd == "CREATE" {
|
|
|
|
|
app.CreateRoom()
|
2024-10-25 22:20:55 -06:00
|
|
|
|
} else if cmd == "JOIN" {
|
2024-10-26 19:34:56 -06:00
|
|
|
|
err := app.JoinRoom()
|
2024-10-25 22:20:55 -06:00
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-26 19:34:56 -06:00
|
|
|
|
fmt.Printf("[DEV] Joined Branches\n")
|
2024-10-25 22:20:55 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
app.Client.ListenForData()
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case data, ok := <-app.Client.DataChannel:
|
|
|
|
|
if !ok {
|
|
|
|
|
fmt.Println("Data channel closed, exiting...")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
app.HandleData(data)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-25 22:20:55 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
fmt.Println("Closing app")
|
|
|
|
|
}
|
2024-10-25 22:20:55 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
func (app *DemoApp) CreateRoom() {
|
|
|
|
|
var err error
|
|
|
|
|
code, err := app.Client.CreateRoom("")
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
fmt.Println("Created room:", code)
|
2024-10-25 22:20:55 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
err = app.Client.JoinRoom(code, "password")
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
println("Connected to room")
|
2024-10-25 22:20:55 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
path := os.Getenv("USERPROFILE")
|
|
|
|
|
logPath := filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log")
|
2024-10-25 22:20:55 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
fmt.Println("Reading log file from:", logPath)
|
2024-10-25 22:20:55 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
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))
|
2024-10-25 22:20:55 -06:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-26 19:34:56 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *DemoApp) JoinRoom() error {
|
|
|
|
|
var code string
|
|
|
|
|
var password string
|
|
|
|
|
fmt.Print("Enter Room Code:\n>")
|
|
|
|
|
fmt.Scanln(&code)
|
2024-10-25 22:20:55 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
fmt.Print("Enter Room Password:\n>")
|
|
|
|
|
fmt.Scanln(&password)
|
|
|
|
|
|
|
|
|
|
err := app.Client.JoinRoom(code, password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
return err
|
2024-10-25 22:20:55 -06:00
|
|
|
|
}
|
2024-10-26 19:34:56 -06:00
|
|
|
|
fmt.Println("Joined room:", code)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2024-10-25 22:20:55 -06:00
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
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)
|
2024-10-25 22:20:55 -06:00
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 19:34:56 -06:00
|
|
|
|
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 {
|
2024-10-26 20:59:03 -06:00
|
|
|
|
fmt.Printf("| %-20s | %-10d | %10.3f |\n",
|
2024-10-26 19:34:56 -06:00
|
|
|
|
player.DisplayName,
|
|
|
|
|
player.Achievements.BedwarsLevel,
|
|
|
|
|
player.Stats.Bedwars.FKDR)
|
2024-10-25 22:20:55 -06:00
|
|
|
|
}
|
2024-10-26 19:34:56 -06:00
|
|
|
|
|
|
|
|
|
fmt.Println("|----------------------|------------|------------|")
|
2024-10-25 22:20:55 -06:00
|
|
|
|
}
|