Files
radiantwave-legacy/internal/application/application.go
T

455 lines
12 KiB
Go

package application
import (
"errors"
"os"
"path/filepath"
"sync/atomic"
"time"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
"gorm.io/gorm"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/graphics"
"rhemawavetech.com/rhemawave/internal/keybinds"
"rhemawavetech.com/rhemawave/internal/license"
"rhemawavetech.com/rhemawave/internal/logger"
"rhemawavetech.com/rhemawave/internal/mixer"
"rhemawavetech.com/rhemawave/internal/network"
"rhemawavetech.com/rhemawave/internal/page"
"rhemawavetech.com/rhemawave/internal/shaderManager"
"rhemawavetech.com/rhemawave/internal/sync"
)
const (
targetFPS = 60.0
volumeStepSize = 16
openGLMajorVersion = 3
openGLMinorVersion = 3
apiURL = "https://portal.rhemawavetech.com/api/v1"
)
type State int
const (
NEEDS_INTERNET State = iota
NEEDS_LICENSE_KEY
NEEDS_EMAIL_ADDRESS
READY
)
type Application struct {
Window *sdl.Window
License *license.Response
pageStack []page.Page
pendingAction func()
running bool
DB *gorm.DB
Config atomic.Pointer[db.AppConfig]
PageRouter *page.Router
state State
}
var lastNetworkCheckTime time.Time
var networkIsUp bool
// ----------------------------------------------------------------------------
// Main Entry Point
// ----------------------------------------------------------------------------
func Run() {
var err error
var homeDir string
var dataDir string
var DB *gorm.DB
var initialConfig db.AppConfig
var result *gorm.DB
networkIsUp = false
logger.Info("Starting Application")
homeDir, err = os.UserHomeDir()
if err != nil {
logger.Error("unable to fetch user $HOME directory: %v", err)
return
}
dataDir = filepath.Join(homeDir, ".local", "share", "rhemawave")
if err := os.MkdirAll(dataDir, 0755); err != nil {
logger.Error("unable to create data directory %s %v", dataDir, err)
return
}
testFile := filepath.Join(dataDir, ".access_test")
f, err := os.Create(testFile)
if err != nil {
logger.Error("data %s is not writeable: %v", dataDir, err)
return
}
f.Close()
os.Remove(testFile)
logger.AddFile(filepath.Join(dataDir, "log.txt"))
DB, err = db.Connect(filepath.Join(dataDir, "database.sqlite"))
if err != nil {
logger.Error("application failed to connect to the database: %v", err)
return
}
if err = db.Migrate(DB); err != nil {
logger.Error("application failed to perform database migrations: %v", err)
return
}
// Populate the initial Config cache OR create defaults
result = DB.First(&initialConfig)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
logger.Info("First run detected: Creating default configuration...")
initialConfig = db.AppConfig{
DataDir: dataDir,
AssetsDir: dataDir,
SystemLevel: "BASIC",
SelectedAudioDevice: "default",
SelectedAudio: "audio.wav",
InitialFontSize: 128,
StandardFontSize: 32,
LinePattern: "fibonacci",
AudioEnabled: true,
}
if err := DB.Create(&initialConfig).Error; err != nil {
logger.Error("failed to insert default config into database: %v", err)
return
}
} else {
logger.Error("error querying database for initial config: %v", result.Error)
return
}
}
logger.AddDatabase(DB)
app := &Application{
DB: DB,
}
app.refreshCache()
w, h := app.initializeSDLAndOpenGL()
defer app.cleanup()
app.initializeManagers()
app.initializeAudio()
app.PageRouter = page.NewRouter(page.PageCtx{
CfgPtr: app.Config.Load(),
DbPtr: app.DB,
W: w,
H: h,
RefreshConfig: app.refreshCache,
})
// Sync the proclamations from the API to the Database
sync.SyncProclamations(DB)
sync.SyncCategories(DB)
app.runEventLoop()
}
func (app *Application) checkState() {
if app.state == READY {
return
}
// app.state = 0 means that there is no state set
// Check for an internet connection first
if !networkIsUp {
if lastNetworkCheckTime.IsZero() || time.Since(lastNetworkCheckTime) >= time.Second {
networkManager, err := network.New()
if err != nil {
logger.Error("error obtaining a network manager %v", err)
return
}
lastNetworkCheckTime = time.Now()
status, err := networkManager.CheckInternetConnection()
if err != nil {
logger.Error("error checking for internet connection %v", err)
return
}
if status.StatusCode != network.StatusWifiConnectedInternetUp &&
status.StatusCode != network.StatusEthernetConnectedInternetUp {
if app.state != NEEDS_INTERNET {
app.PageRouter.NavigateTo(page.WIFI)
app.state = NEEDS_INTERNET
}
return
}
networkIsUp = true // ← internet confirmed, don't check again
} else {
return // throttled, and we don't yet know if network is up — can't proceed
}
}
// Then check for a license key
if app.Config.Load().LicenseKey == "" {
if app.state != NEEDS_LICENSE_KEY {
app.PageRouter.NavigateTo(page.LICENSE_KEY)
app.state = NEEDS_LICENSE_KEY
}
return
}
// Finally, check for an email address
if app.Config.Load().EmailAddress == "" {
if app.state != NEEDS_EMAIL_ADDRESS {
app.PageRouter.NavigateTo(page.EMAIL_ADDRESS)
app.state = NEEDS_EMAIL_ADDRESS
}
return
}
app.state = READY
app.PageRouter.NavigateTo(page.HOME)
}
func (app *Application) refreshCache() {
var freshConfig db.AppConfig
if result := app.DB.First(&freshConfig); result.Error != nil {
logger.Error("failed to fetch configuration from the database: %v", result.Error)
}
app.Config.Store(&freshConfig)
}
func (app *Application) initializeSDLAndOpenGL() (W, H int32) {
logger.Info("Initializing SDL")
if err := sdl.Init(sdl.INIT_VIDEO | sdl.INIT_AUDIO); err != nil {
logger.Fatal("SDL init failed: %v", err)
}
logger.Info("Initializing TTF")
if err := ttf.Init(); err != nil {
logger.Fatal("TTF init failed: %v", err)
}
sdl.GLSetAttribute(sdl.GL_CONTEXT_MAJOR_VERSION, openGLMajorVersion)
sdl.GLSetAttribute(sdl.GL_CONTEXT_MINOR_VERSION, openGLMinorVersion)
sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE)
sdl.ShowCursor(sdl.DISABLE)
sdl.SetRelativeMouseMode(true)
logger.Info("Querying display mode")
displayMode, err := sdl.GetDisplayMode(0, 0)
if err != nil {
logger.Fatal("Display mode query failed: %v", err)
}
if displayMode.W == 0 || displayMode.H == 0 {
logger.Fatal("Invalid display dimensions: %dx%d", displayMode.W, displayMode.H)
}
logger.Info("Display: %dx%d @ %dHz", displayMode.W, displayMode.H, displayMode.RefreshRate)
logger.Info("Creating window")
window, err := sdl.CreateWindow(
"Rhema Wave",
sdl.WINDOWPOS_UNDEFINED,
sdl.WINDOWPOS_UNDEFINED,
displayMode.W,
displayMode.H,
sdl.WINDOW_OPENGL|sdl.WINDOW_FULLSCREEN_DESKTOP|sdl.WINDOW_SHOWN,
)
if err != nil {
logger.Fatal("Window creation failed: %v", err)
}
app.Window = window
logger.Info("Creating OpenGL context")
if _, err := app.Window.GLCreateContext(); err != nil {
logger.Fatal("OpenGL context creation failed: %v", err)
}
logger.Info("Initializing go-gl")
if err := gl.InitWithProcAddrFunc(sdl.GLGetProcAddress); err != nil {
logger.Fatal("go-gl initialization failed: %v", err)
}
logger.Info("OpenGL version: %s", gl.GoStr(gl.GetString(gl.VERSION)))
logger.Info("OpenGL renderer: %s", gl.GoStr(gl.GetString(gl.RENDERER)))
if err := sdl.GLSetSwapInterval(1); err != nil {
logger.Warning("Failed to enable vsync: %v", err)
}
w, h := app.Window.GLGetDrawableSize()
logger.Info("Drawable size: %dx%d", w, h)
logger.Info("Initializing virtual framebuffer (%dx%d -> %dx%d)",
graphics.VirtualWidth, graphics.VirtualHeight, w, h)
if err := graphics.InitVirtualFramebuffer(w, h); err != nil {
logger.Fatal("Virtual framebuffer init failed: %v", err)
}
app.initKeybinds()
logger.Info("SDL and OpenGL initialization complete")
return graphics.VirtualWidth, graphics.VirtualHeight
}
func (app *Application) initializeManagers() {
logger.Info("Initializing ShaderManager")
shaderDir := filepath.Join(app.Config.Load().AssetsDir, "shaders")
logger.Info("Loading shaders from %s", shaderDir)
if err := shaderManager.InitShaderManager(shaderDir); err != nil {
logger.Fatal("ShaderManager initialization failed: %v", err)
}
logger.Info("Initializing FontManager")
if err := fontManager.InitFontManager(app.Config.Load()); err != nil {
logger.Fatal("FontManager initialization failed: %v", err)
}
}
func (app *Application) initializeAudio() {
logger.Info("Initializing audio")
if err := mixer.Init(app.Config.Load(), app.refreshCache); err != nil {
logger.Error("Unable to initialize audio device '%s': %v", app.Config.Load().SelectedAudioDevice, err)
logger.Error("Audio will be unavailable until a device is configured")
} else {
logger.Info("Audio initialized with device: '%s'", app.Config.Load().SelectedAudioDevice)
}
}
func (app *Application) cleanup() {
logger.Info("Cleaning up")
if app.PageRouter != nil && app.PageRouter.ActivePage != nil {
app.PageRouter.ActivePage.Destroy()
}
graphics.DestroyVirtualFramebuffer()
if app.Window != nil {
app.Window.Destroy()
}
ttf.Quit()
sdl.Quit()
logger.Info("Cleanup complete")
logger.Close()
}
// ----------------------------------------------------------------------------
// Event Loop
// ----------------------------------------------------------------------------
func (app *Application) runEventLoop() {
if app.PageRouter == nil || app.PageRouter.ActivePage == nil {
logger.Fatal("No initial page loaded, Router is empty")
}
const frameDelay = time.Second / targetFPS
app.running = true
lastTime := time.Now()
logger.Info("Entering event loop")
for app.running {
frameStart := time.Now()
app.checkState()
app.handleEvents()
if !app.running {
break
}
deltaTime := float32(time.Since(lastTime).Seconds())
lastTime = time.Now()
app.updateCurrentPage(deltaTime)
app.renderFrame()
frameTime := time.Since(frameStart)
if frameTime < frameDelay {
time.Sleep(frameDelay - frameTime)
}
}
logger.Info("Event loop exited")
}
func (app *Application) handleEvents() {
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
switch e := event.(type) {
case *sdl.QuitEvent:
app.running = false
return
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN && e.Repeat == 0 {
isCtrlPressed := (e.Keysym.Mod&sdl.KMOD_CTRL) != 0 || (e.Keysym.Mod&sdl.KMOD_GUI) != 0
if e.Keysym.Sym == sdl.K_c && isCtrlPressed {
continue
}
keybinds.PerformAction(e)
}
}
if currentPage := app.PageRouter.ActivePage; currentPage != nil {
if err := currentPage.HandleEvent(&event); err != nil {
logger.Error("Page event handler error: %v", err)
}
}
}
}
func (app *Application) updateCurrentPage(deltaTime float32) {
if currentPage := app.PageRouter.ActivePage; currentPage != nil {
currentPage.Update(deltaTime)
}
}
func (app *Application) renderFrame() {
graphics.BeginVirtualRender()
gl.ClearColor(0.0, 0.0, 0.0, 1.0)
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
if currentPage := app.PageRouter.ActivePage; currentPage != nil {
currentPage.Render()
}
graphics.BlitToScreen()
app.Window.GLSwap()
}
// ----------------------------------------------------------------------------
// Keybind Registration
// ----------------------------------------------------------------------------
func (app *Application) initKeybinds() {
keybinds.Register(sdl.K_F3, sdl.KMOD_NONE, func() {
app.PageRouter.NavigateTo(page.SETTINGS)
})
keybinds.Register(sdl.K_F2, sdl.KMOD_NONE, func() {
app.PageRouter.NavigateTo(page.SCROLLER)
})
keybinds.Register(sdl.K_F1, sdl.KMOD_NONE, func() {
app.PageRouter.NavigateTo(page.HOME)
})
keybinds.Register(sdl.K_q, sdl.KMOD_RCTRL, func() {
app.Stop()
})
}
func (app *Application) Stop() {
app.running = false
}
func (app *Application) Quit() {
logger.Info("User has quit the application")
app.running = false
}