Add configuration files and update project documentation
- Introduced .editorconfig for consistent coding styles across the project. - Added .golangci.yml for Go linting configuration. - Updated AGENTS.md to clarify project structure and components. - Enhanced CONTRIBUTING.md with Makefile usage for common tasks. - Updated Dockerfiles to use Go 1.24 and improved build instructions. - Refined README.md and deployment documentation for clarity. - Added testing documentation in testing.md for backend and frontend tests. - Introduced Makefile for streamlined development commands and tasks.
This commit is contained in:
@@ -11,6 +11,26 @@ import (
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const (
|
||||
AUTH_ADMIN = "admin"
|
||||
AUTH_MAP = "map"
|
||||
AUTH_MARKERS = "markers"
|
||||
AUTH_UPLOAD = "upload"
|
||||
|
||||
CharCleanupInterval = 10 * time.Second
|
||||
CharStaleThreshold = 10 * time.Second
|
||||
TileUpdateInterval = 30 * time.Minute
|
||||
MaxZoomLevel = 5
|
||||
GridSize = 100
|
||||
SessionMaxAge = 7 * 24 * 3600 // 1 week in seconds
|
||||
MultipartMaxMemory = 100 << 20 // 100 MB
|
||||
MergeMaxMemory = 500 << 20 // 500 MB
|
||||
ClientVersion = "4"
|
||||
SSETickInterval = 5 * time.Second
|
||||
SSETileChannelSize = 1000
|
||||
SSEMergeChannelSize = 5
|
||||
)
|
||||
|
||||
// App is the main application (map server) state.
|
||||
type App struct {
|
||||
gridStorage string
|
||||
@@ -24,22 +44,29 @@ type App struct {
|
||||
mergeUpdates Topic[Merge]
|
||||
}
|
||||
|
||||
// GridStorage returns the grid storage path.
|
||||
func (a *App) GridStorage() string {
|
||||
return a.gridStorage
|
||||
// NewApp creates an App with the given storage paths and database.
|
||||
func NewApp(gridStorage, frontendRoot string, db *bbolt.DB) (*App, error) {
|
||||
return &App{
|
||||
gridStorage: gridStorage,
|
||||
frontendRoot: frontendRoot,
|
||||
db: db,
|
||||
characters: make(map[string]Character),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GridUpdates returns the tile updates topic for MapService.
|
||||
func (a *App) GridUpdates() *Topic[TileData] {
|
||||
return &a.gridUpdates
|
||||
}
|
||||
// GridStorage returns the path to the grid storage directory.
|
||||
func (a *App) GridStorage() string { return a.gridStorage }
|
||||
|
||||
// MergeUpdates returns the merge updates topic for MapService.
|
||||
func (a *App) MergeUpdates() *Topic[Merge] {
|
||||
return &a.mergeUpdates
|
||||
}
|
||||
// GridUpdates returns the tile update pub/sub topic.
|
||||
func (a *App) GridUpdates() *Topic[TileData] { return &a.gridUpdates }
|
||||
|
||||
// GetCharacters returns a copy of all characters (for MapService).
|
||||
// MergeUpdates returns the merge event pub/sub topic.
|
||||
func (a *App) MergeUpdates() *Topic[Merge] { return &a.mergeUpdates }
|
||||
|
||||
// DB returns the underlying bbolt database.
|
||||
func (a *App) DB() *bbolt.DB { return a.db }
|
||||
|
||||
// GetCharacters returns a copy of all characters.
|
||||
func (a *App) GetCharacters() []Character {
|
||||
a.chmu.RLock()
|
||||
defer a.chmu.RUnlock()
|
||||
@@ -50,128 +77,30 @@ func (a *App) GetCharacters() []Character {
|
||||
return chars
|
||||
}
|
||||
|
||||
// NewApp creates an App with the given storage paths and database.
|
||||
// frontendRoot is the directory for the map SPA (e.g. "frontend").
|
||||
func NewApp(gridStorage, frontendRoot string, db *bbolt.DB) (*App, error) {
|
||||
return &App{
|
||||
gridStorage: gridStorage,
|
||||
frontendRoot: frontendRoot,
|
||||
db: db,
|
||||
characters: make(map[string]Character),
|
||||
}, nil
|
||||
// WithCharacters provides locked mutable access to the character map.
|
||||
func (a *App) WithCharacters(fn func(chars map[string]Character)) {
|
||||
a.chmu.Lock()
|
||||
defer a.chmu.Unlock()
|
||||
fn(a.characters)
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
ID string
|
||||
Username string
|
||||
Auths Auths `json:"-"`
|
||||
TempAdmin bool
|
||||
}
|
||||
|
||||
type Character struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Map int `json:"map"`
|
||||
Position Position `json:"position"`
|
||||
Type string `json:"type"`
|
||||
updated time.Time
|
||||
}
|
||||
|
||||
type Marker struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
GridID string `json:"gridID"`
|
||||
Position Position `json:"position"`
|
||||
Image string `json:"image"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
type FrontendMarker struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Map int `json:"map"`
|
||||
Position Position `json:"position"`
|
||||
Image string `json:"image"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
type MapInfo struct {
|
||||
ID int
|
||||
Name string
|
||||
Hidden bool
|
||||
Priority bool
|
||||
}
|
||||
|
||||
type GridData struct {
|
||||
ID string
|
||||
Coord Coord
|
||||
NextUpdate time.Time
|
||||
Map int
|
||||
}
|
||||
|
||||
type Coord struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
func (c Coord) Name() string {
|
||||
return fmt.Sprintf("%d_%d", c.X, c.Y)
|
||||
}
|
||||
|
||||
func (c Coord) Parent() Coord {
|
||||
if c.X < 0 {
|
||||
c.X--
|
||||
}
|
||||
if c.Y < 0 {
|
||||
c.Y--
|
||||
}
|
||||
return Coord{
|
||||
X: c.X / 2,
|
||||
Y: c.Y / 2,
|
||||
}
|
||||
}
|
||||
|
||||
type Auths []string
|
||||
|
||||
func (a Auths) Has(auth string) bool {
|
||||
for _, v := range a {
|
||||
if v == auth {
|
||||
return true
|
||||
// CleanChars runs a background loop that removes stale character entries.
|
||||
func (a *App) CleanChars() {
|
||||
for range time.Tick(CharCleanupInterval) {
|
||||
a.chmu.Lock()
|
||||
for n, c := range a.characters {
|
||||
if c.Updated.Before(time.Now().Add(-CharStaleThreshold)) {
|
||||
delete(a.characters, n)
|
||||
}
|
||||
}
|
||||
a.chmu.Unlock()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
AUTH_ADMIN = "admin"
|
||||
AUTH_MAP = "map"
|
||||
AUTH_MARKERS = "markers"
|
||||
AUTH_UPLOAD = "upload"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Pass []byte
|
||||
Auths Auths
|
||||
Tokens []string
|
||||
// OAuth: provider -> subject (unique ID from provider)
|
||||
OAuthLinks map[string]string `json:"oauth_links,omitempty"` // e.g. "google" -> "123456789"
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// serveSPARoot serves the map SPA from root: static files from frontend, fallback to index.html for client-side routes.
|
||||
// Handles redirects from old /map/* URLs for backward compatibility.
|
||||
func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) {
|
||||
// serveSPARoot serves the map SPA: static files from frontend, fallback to index.html.
|
||||
func (a *App) ServeSPARoot(rw http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
|
||||
// Redirect old /map/* URLs to flat routes
|
||||
if path == "/map" || path == "/map/" {
|
||||
http.Redirect(rw, req, "/", http.StatusFound)
|
||||
return
|
||||
@@ -200,7 +129,6 @@ func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// File serving: path relative to frontend root (with baseURL /, files are at root)
|
||||
filePath := strings.TrimPrefix(path, "/")
|
||||
if filePath == "" {
|
||||
filePath = "index.html"
|
||||
@@ -210,14 +138,12 @@ func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) {
|
||||
http.NotFound(rw, req)
|
||||
return
|
||||
}
|
||||
// Try both root and map/ for backward compatibility with old builds
|
||||
tryPaths := []string{filePath, filepath.Join("map", filePath)}
|
||||
var f http.File
|
||||
for _, p := range tryPaths {
|
||||
var err error
|
||||
f, err = http.Dir(a.frontendRoot).Open(p)
|
||||
if err == nil {
|
||||
filePath = p
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -234,28 +160,130 @@ func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) {
|
||||
http.ServeContent(rw, req, stat.Name(), stat.ModTime(), f)
|
||||
}
|
||||
|
||||
// CleanChars runs a background loop that removes stale character entries. Call once as a goroutine.
|
||||
func (a *App) CleanChars() {
|
||||
for range time.Tick(time.Second * 10) {
|
||||
a.chmu.Lock()
|
||||
for n, c := range a.characters {
|
||||
if c.updated.Before(time.Now().Add(-10 * time.Second)) {
|
||||
delete(a.characters, n)
|
||||
}
|
||||
}
|
||||
a.chmu.Unlock()
|
||||
// --- Domain types ---
|
||||
|
||||
// Session represents an authenticated user session.
|
||||
type Session struct {
|
||||
ID string
|
||||
Username string
|
||||
Auths Auths `json:"-"`
|
||||
TempAdmin bool
|
||||
}
|
||||
|
||||
// Character represents a game character on the map.
|
||||
type Character struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Map int `json:"map"`
|
||||
Position Position `json:"position"`
|
||||
Type string `json:"type"`
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
// Marker represents a map marker stored per grid.
|
||||
type Marker struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
GridID string `json:"gridID"`
|
||||
Position Position `json:"position"`
|
||||
Image string `json:"image"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
// FrontendMarker is a marker with map-level coordinates for the frontend.
|
||||
type FrontendMarker struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Map int `json:"map"`
|
||||
Position Position `json:"position"`
|
||||
Image string `json:"image"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
// MapInfo describes a map instance.
|
||||
type MapInfo struct {
|
||||
ID int
|
||||
Name string
|
||||
Hidden bool
|
||||
Priority bool
|
||||
}
|
||||
|
||||
// GridData represents a grid tile with its coordinates and map assignment.
|
||||
type GridData struct {
|
||||
ID string
|
||||
Coord Coord
|
||||
NextUpdate time.Time
|
||||
Map int
|
||||
}
|
||||
|
||||
// Coord represents a grid coordinate pair.
|
||||
type Coord struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
// Position represents a pixel position within a grid.
|
||||
type Position struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
// Name returns the string representation "X_Y" used as bucket keys.
|
||||
func (c Coord) Name() string {
|
||||
return fmt.Sprintf("%d_%d", c.X, c.Y)
|
||||
}
|
||||
|
||||
// Parent returns the parent coordinate at the next zoom level.
|
||||
func (c Coord) Parent() Coord {
|
||||
if c.X < 0 {
|
||||
c.X--
|
||||
}
|
||||
if c.Y < 0 {
|
||||
c.Y--
|
||||
}
|
||||
return Coord{
|
||||
X: c.X / 2,
|
||||
Y: c.Y / 2,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all HTTP handlers for the app.
|
||||
func (a *App) RegisterRoutes() {
|
||||
http.HandleFunc("/client/", a.client)
|
||||
http.HandleFunc("/logout", a.redirectLogout)
|
||||
// Auths is a list of permission strings (e.g. "admin", "map", "upload").
|
||||
type Auths []string
|
||||
|
||||
http.HandleFunc("/map/api/", a.apiRouter)
|
||||
http.HandleFunc("/map/updates", a.watchGridUpdates)
|
||||
http.HandleFunc("/map/grids/", a.gridTile)
|
||||
|
||||
// SPA catch-all: must be last
|
||||
http.HandleFunc("/", a.serveSPARoot)
|
||||
// Has returns true if the list contains the given permission.
|
||||
func (a Auths) Has(auth string) bool {
|
||||
for _, v := range a {
|
||||
if v == auth {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// User represents a stored user account.
|
||||
type User struct {
|
||||
Pass []byte
|
||||
Auths Auths
|
||||
Tokens []string
|
||||
OAuthLinks map[string]string `json:"oauth_links,omitempty"`
|
||||
}
|
||||
|
||||
// Page holds page metadata for rendering.
|
||||
type Page struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// Config holds the application config sent to the frontend.
|
||||
type Config struct {
|
||||
Title string `json:"title"`
|
||||
Auths []string `json:"auths"`
|
||||
}
|
||||
|
||||
// TileData represents a tile image entry in the database.
|
||||
type TileData struct {
|
||||
MapID int
|
||||
Coord Coord
|
||||
Zoom int
|
||||
File string
|
||||
Cache int64
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user