240 lines
5.5 KiB
Go

package control
import (
"encoding/json"
"fmt"
"os"
"sync"
)
var (
global *Control
mu sync.RWMutex
filename string
)
// Control represents the game control settings
type Control struct {
WorldSize int `json:"world_size"`
Open int `json:"open"`
AdminEmail string `json:"admin_email"`
EmailMode string `json:"email_mode"` // "file" or "smtp"
EmailFilePath string `json:"email_file_path"` // path for file mode
SMTPHost string `json:"smtp_host"`
SMTPPort string `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
}
// Init loads control settings from the specified JSON file
func Init(jsonFile string) error {
mu.Lock()
defer mu.Unlock()
filename = jsonFile
// Try to load from file
if data, err := os.ReadFile(filename); err == nil {
var ctrl Control
if err := json.Unmarshal(data, &ctrl); err != nil {
return fmt.Errorf("failed to parse JSON: %w", err)
}
// Apply defaults for any missing fields
defaults := New()
if ctrl.WorldSize == 0 {
ctrl.WorldSize = defaults.WorldSize
}
if ctrl.EmailMode == "" {
ctrl.EmailMode = defaults.EmailMode
}
if ctrl.EmailFilePath == "" {
ctrl.EmailFilePath = defaults.EmailFilePath
}
global = &ctrl
} else {
// Create default control settings if file doesn't exist
global = New()
if err := save(); err != nil {
return fmt.Errorf("failed to create default config file: %w", err)
}
}
return global.Validate()
}
// save writes the current control settings to the JSON file (internal use)
func save() error {
if filename == "" {
return fmt.Errorf("no filename set")
}
data, err := json.MarshalIndent(global, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
return os.WriteFile(filename, data, 0644)
}
// Save writes the current control settings to the JSON file (public)
func Save() error {
mu.RLock()
defer mu.RUnlock()
if global == nil {
return fmt.Errorf("control not initialized")
}
return save()
}
// New creates a new Control with sensible defaults
func New() *Control {
return &Control{
WorldSize: 200,
Open: 1,
AdminEmail: "",
EmailMode: "file",
EmailFilePath: "emails.txt",
SMTPHost: "",
SMTPPort: "587",
SMTPUsername: "",
SMTPPassword: "",
}
}
// Get returns the global control instance (thread-safe)
func Get() *Control {
mu.RLock()
defer mu.RUnlock()
if global == nil {
panic("control not initialized - call Init first")
}
return global
}
// Set updates the global control instance and saves to file (thread-safe)
func Set(control *Control) error {
mu.Lock()
defer mu.Unlock()
if err := control.Validate(); err != nil {
return err
}
global = control
return save()
}
// Update updates specific fields of the control settings and saves to file
func Update(updater func(*Control)) error {
mu.Lock()
defer mu.Unlock()
// Create a copy to work with
updated := *global
updater(&updated)
if err := updated.Validate(); err != nil {
return err
}
global = &updated
return save()
}
// UpdateNoSave updates specific fields without saving (useful for batch updates)
func UpdateNoSave(updater func(*Control)) error {
mu.Lock()
defer mu.Unlock()
// Create a copy to work with
updated := *global
updater(&updated)
if err := updated.Validate(); err != nil {
return err
}
global = &updated
return nil
}
// Validate checks if control settings have valid values
func (c *Control) Validate() error {
if c.WorldSize <= 0 || c.WorldSize > 10000 {
return fmt.Errorf("WorldSize must be between 1 and 10000")
}
if c.Open != 0 && c.Open != 1 {
return fmt.Errorf("Open must be 0 or 1")
}
if c.EmailMode != "file" && c.EmailMode != "smtp" {
return fmt.Errorf("EmailMode must be 'file' or 'smtp'")
}
if c.EmailMode == "smtp" {
if c.SMTPHost == "" {
return fmt.Errorf("SMTPHost required when EmailMode is 'smtp'")
}
if c.SMTPUsername == "" {
return fmt.Errorf("SMTPUsername required when EmailMode is 'smtp'")
}
if c.SMTPPassword == "" {
return fmt.Errorf("SMTPPassword required when EmailMode is 'smtp'")
}
}
return nil
}
// IsOpen returns true if the game world is open for new players
func (c *Control) IsOpen() bool {
return c.Open == 1
}
// SetOpen sets whether the game world is open for new players
func SetOpen(open bool) error {
return Update(func(c *Control) {
if open {
c.Open = 1
} else {
c.Open = 0
}
})
}
// HasAdminEmail returns true if an admin email is configured
func (c *Control) HasAdminEmail() bool {
return c.AdminEmail != ""
}
// IsEmailConfigured returns true if email is properly configured
func (c *Control) IsEmailConfigured() bool {
if c.EmailMode == "file" {
return c.EmailFilePath != ""
}
return c.SMTPHost != "" && c.SMTPUsername != "" && c.SMTPPassword != ""
}
// IsWorldSizeValid returns true if the world size is within reasonable bounds
func (c *Control) IsWorldSizeValid() bool {
return c.WorldSize > 0 && c.WorldSize <= 10000
}
// GetWorldRadius returns the world radius (half the world size)
func (c *Control) GetWorldRadius() int {
return c.WorldSize / 2
}
// IsWithinWorldBounds returns true if the given coordinates are within world bounds
func (c *Control) IsWithinWorldBounds(x, y int) bool {
radius := c.GetWorldRadius()
return x >= -radius && x <= radius && y >= -radius && y <= radius
}
// GetWorldBounds returns the minimum and maximum coordinates for the world
func (c *Control) GetWorldBounds() (minX, minY, maxX, maxY int) {
radius := c.GetWorldRadius()
return -radius, -radius, radius, radius
}