overall improvements + keep up with querymanager v0.2

This commit is contained in:
fusion32 2025-10-18 20:37:00 -03:00
parent cd93a7c2bf
commit 48375acd7d
10 changed files with 104 additions and 17 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ bin
build build
local local
*.log *.log
config.cfg

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"unicode" "unicode"
"unicode/utf8"
) )
// TReadBuffer // TReadBuffer
@ -83,7 +84,11 @@ func (ReadBuffer *TReadBuffer) ReadString() string {
Result := "" Result := ""
if ReadBuffer.CanRead(Length) { if ReadBuffer.CanRead(Length) {
Result = string(ReadBuffer.Buffer[ReadBuffer.Position:][:Length]) // IMPORTANT(fusion): The game server uses LATIN1 encoding, which forces
// the query manager to use LATIN1 encoding for text, at least on the
// protocol level.
Input := ReadBuffer.Buffer[ReadBuffer.Position:][:Length]
Result = string(Latin1ToUTF8(Input))
} }
ReadBuffer.Position += Length ReadBuffer.Position += Length
return Result return Result
@ -157,7 +162,11 @@ func (WriteBuffer *TWriteBuffer) Write32BE(Value uint32) {
} }
func (WriteBuffer *TWriteBuffer) WriteString(String string) { func (WriteBuffer *TWriteBuffer) WriteString(String string) {
Length := len(String) // IMPORTANT(fusion): The game server uses LATIN1 encoding, which forces
// the query manager to use LATIN1 encoding for text, at least on the
// protocol level.
Output := UTF8ToLatin1([]byte(String))
Length := len(Output)
if Length < 0xFFFF { if Length < 0xFFFF {
WriteBuffer.Write16(uint16(Length)) WriteBuffer.Write16(uint16(Length))
} else { } else {
@ -166,7 +175,7 @@ func (WriteBuffer *TWriteBuffer) WriteString(String string) {
} }
if WriteBuffer.CanWrite(Length) { if WriteBuffer.CanWrite(Length) {
copy(WriteBuffer.Buffer[WriteBuffer.Position:], String) copy(WriteBuffer.Buffer[WriteBuffer.Position:], Output)
} }
WriteBuffer.Position += Length WriteBuffer.Position += Length
} }
@ -370,7 +379,7 @@ func WorldTypeString(Type int) string {
} }
} }
func TimestampString(Timestamp int) string { func FormatTimestamp(Timestamp int) string {
String := "Never" String := "Never"
if Timestamp > 0 { if Timestamp > 0 {
Time := time.Unix(int64(Timestamp), 0) Time := time.Unix(int64(Timestamp), 0)
@ -378,3 +387,53 @@ func TimestampString(Timestamp int) string {
} }
return String return String
} }
func FormatDurationSince(Timestamp int) string {
String := "N/A"
if Timestamp > 0{
Duration := time.Since(time.Unix(int64(Timestamp), 0))
String = Duration.Truncate(time.Second).String()
}
return String
}
func UTF8FindNextLeadingByte(Buffer []byte) int {
Offset := 0
for Offset < len(Buffer) {
// NOTE(fusion): Allow the first byte to be a leading byte, in case we
// just want to advance from one leading byte to another.
if(Offset > 0 && utf8.RuneStart(Buffer[Offset])){
break
}
Offset += 1
}
return Offset
}
func UTF8ToLatin1(Buffer []byte) []byte {
ReadPos := 0
Result := []byte{}
for ReadPos < len(Buffer) {
Codepoint, Size := utf8.DecodeRune(Buffer[ReadPos:])
if Codepoint != utf8.RuneError {
ReadPos += Size
}else{
ReadPos += UTF8FindNextLeadingByte(Buffer[ReadPos:])
}
if Codepoint >= 0 && Codepoint <= 0xFF {
Result = append(Result, byte(Codepoint))
}else{
Result = append(Result, '?')
}
}
return Result
}
func Latin1ToUTF8(Buffer []byte) []byte {
Result := []byte{}
for ReadPos := range Buffer {
Result = utf8.AppendRune(Result, rune(Buffer[ReadPos]))
}
return Result
}

View File

@ -563,7 +563,7 @@ func HandleWorld(Context *THttpRequestContext) {
} }
func main() { func main() {
g_Log.Print("Tibia Web Server v0.1") g_Log.Print("Tibia Web Server v0.2")
if !ReadConfig("config.cfg", WebKVCallback) { if !ReadConfig("config.cfg", WebKVCallback) {
return return
} }

View File

@ -45,8 +45,10 @@ type (
Type string Type string
NumPlayers int NumPlayers int
MaxPlayers int MaxPlayers int
OnlineRecord int OnlinePeak int
OnlineRecordTime string OnlinePeakTimestamp int
LastStartup int
LastShutdown int
} }
TAccountSummary struct { TAccountSummary struct {
@ -77,7 +79,7 @@ type (
Level int Level int
Profession string Profession string
Residence string Residence string
LastLogin string LastLogin int
PremiumDays int PremiumDays int
Online bool Online bool
Deleted bool Deleted bool
@ -389,7 +391,7 @@ func (Connection *TQueryManagerConnection) GetCharacterProfile(CharacterName str
Character.Level = int(ReadBuffer.Read16()) Character.Level = int(ReadBuffer.Read16())
Character.Profession = ReadBuffer.ReadString() Character.Profession = ReadBuffer.ReadString()
Character.Residence = ReadBuffer.ReadString() Character.Residence = ReadBuffer.ReadString()
Character.LastLogin = TimestampString(int(ReadBuffer.Read32())) Character.LastLogin = int(ReadBuffer.Read32())
Character.PremiumDays = int(ReadBuffer.Read16()) Character.PremiumDays = int(ReadBuffer.Read16())
Character.Online = ReadBuffer.ReadFlag() Character.Online = ReadBuffer.ReadFlag()
Character.Deleted = ReadBuffer.ReadFlag() Character.Deleted = ReadBuffer.ReadFlag()
@ -422,8 +424,10 @@ func (Connection *TQueryManagerConnection) GetWorlds() (Result int, Worlds []TWo
Worlds[Index].Type = WorldTypeString(int(ReadBuffer.Read8())) Worlds[Index].Type = WorldTypeString(int(ReadBuffer.Read8()))
Worlds[Index].NumPlayers = int(ReadBuffer.Read16()) Worlds[Index].NumPlayers = int(ReadBuffer.Read16())
Worlds[Index].MaxPlayers = int(ReadBuffer.Read16()) Worlds[Index].MaxPlayers = int(ReadBuffer.Read16())
Worlds[Index].OnlineRecord = int(ReadBuffer.Read16()) Worlds[Index].OnlinePeak = int(ReadBuffer.Read16())
Worlds[Index].OnlineRecordTime = TimestampString(int(ReadBuffer.Read32())) Worlds[Index].OnlinePeakTimestamp = int(ReadBuffer.Read32())
Worlds[Index].LastStartup = int(ReadBuffer.Read32())
Worlds[Index].LastShutdown = int(ReadBuffer.Read32())
} }
} }
default: default:

View File

@ -58,7 +58,13 @@ var (
func InitTemplates() bool { func InitTemplates() bool {
var Err error var Err error
g_Templates, Err = template.ParseGlob("templates/*.tmpl")
CustomFuncs := template.FuncMap{
"FormatTimestamp": FormatTimestamp,
"FormatDurationSince": FormatDurationSince,
}
g_Templates, Err = template.New("").Funcs(CustomFuncs).ParseGlob("templates/*.tmpl")
if Err != nil { if Err != nil {
g_LogErr.Printf("Failed to parse templates: %v", Err) g_LogErr.Printf("Failed to parse templates: %v", Err)
return false return false

View File

@ -1,6 +1,6 @@
{{template "_header.tmpl" .Common}} {{template "_header.tmpl" .Common}}
<form class="box" action="/character/create" method="POST"> <form class="box" action="/character/create" method="POST">
<h1>Create Account</h1> <h1>Create Character</h1>
<label for="character_name">NAME</label> <label for="character_name">NAME</label>
<input id="character_name" type="text" name="name"/> <input id="character_name" type="text" name="name"/>

View File

@ -35,7 +35,7 @@
</tr> </tr>
<tr> <tr>
<th>Last Login:</th> <th>Last Login:</th>
<td>{{.LastLogin}}</td> <td>{{FormatTimestamp .LastLogin}}</td>
</tr> </tr>
<tr> <tr>
<th>Account Status:</th> <th>Account Status:</th>

View File

@ -16,13 +16,24 @@
<td>{{.NumPlayers}}</td> <td>{{.NumPlayers}}</td>
</tr> </tr>
<tr> <tr>
<th>Online Record:</th> <th>Online Peak:</th>
{{if .OnlineRecord}} {{if eq .OnlinePeak 1}}
<td>{{.OnlineRecord}} players (on {{.OnlineRecordTime}})</td> <td>{{.OnlinePeak}} player (on {{FormatTimestamp .OnlinePeakTimestamp}})</td>
{{else if gt .OnlinePeak 1}}
<td>{{.OnlinePeak}} players (on {{FormatTimestamp .OnlinePeakTimestamp}})</td>
{{else}} {{else}}
<td>None</td> <td>None</td>
{{end}} {{end}}
</tr> </tr>
<tr>
{{if gt .LastStartup .LastShutdown}}
<th>Uptime:</th>
<td>{{FormatDurationSince .LastStartup}}</td>
{{else}}
<th>Downtime:</th>
<td style="color: #A11;">{{FormatDurationSince .LastShutdown}}</td>
{{end}}
</tr>
</table> </table>
<a class="button" href="/killstatistics?world={{.Name}}">Kill Statistics</a> <a class="button" href="/killstatistics?world={{.Name}}">Kill Statistics</a>
{{else}} {{else}}

View File

@ -7,12 +7,18 @@
<th>Name</th> <th>Name</th>
<th>Type</th> <th>Type</th>
<th>Players Online</th> <th>Players Online</th>
<th>Status</th>
</tr> </tr>
{{range .Worlds}} {{range .Worlds}}
<tr> <tr>
<td><a href="/world?name={{.Name}}">{{.Name}}</a></td> <td><a href="/world?name={{.Name}}">{{.Name}}</a></td>
<td>{{.Type}}</td> <td>{{.Type}}</td>
<td>{{.NumPlayers}}</td> <td>{{.NumPlayers}}</td>
{{if gt .LastStartup .LastShutdown}}
<td style="color: #1A1;">Online</td>
{{else}}
<td style="color: #A11;">Offline</td>
{{end}}
</tr> </tr>
{{end}} {{end}}
</table> </table>