initial commit
This commit is contained in:
@@ -0,0 +1,548 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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/download"
|
||||
"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
|
||||
openGLMajorVersion = 3
|
||||
openGLMinorVersion = 3
|
||||
// volumeStepSize = 16
|
||||
// apiURL = "https://portal.rhemawavetech.com/api/v1"
|
||||
)
|
||||
|
||||
type State int
|
||||
|
||||
const (
|
||||
FIRST_RUN State = iota
|
||||
NEEDS_INTERNET
|
||||
NEEDS_LICENSE_KEY
|
||||
NEEDS_EMAIL_ADDRESS
|
||||
DOWNLOADING_ASSETS
|
||||
READY
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
Window *sdl.Window
|
||||
License *license.Response
|
||||
pageStack []page.Page
|
||||
pendingAction func()
|
||||
running bool
|
||||
DB *gorm.DB
|
||||
PageRouter *page.Router
|
||||
state State
|
||||
downloadsComplete bool
|
||||
downloadFailed bool
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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"))
|
||||
logger.SetLevel = logger.LevelInfo
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
firstRun := false
|
||||
|
||||
// Populate the initial Config cache OR create defaults
|
||||
cfg, err := db.GetConfig(DB)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
firstRun = true
|
||||
cfg = &db.AppConfig{
|
||||
Model: gorm.Model{ID: 1},
|
||||
DataDir: dataDir,
|
||||
AssetsDir: dataDir,
|
||||
SystemLevel: "BASIC",
|
||||
SelectedAudioDevice: "default",
|
||||
InitialFontSize: 128,
|
||||
StandardFontSize: 32,
|
||||
LinePattern: "fibonacci",
|
||||
AudioEnabled: true,
|
||||
SavedVolume: 1.0,
|
||||
}
|
||||
|
||||
if err := DB.Create(cfg).Error; err != nil {
|
||||
logger.Error("failed to create config: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.AddDatabase(DB)
|
||||
|
||||
app := &Application{
|
||||
DB: DB,
|
||||
}
|
||||
|
||||
logger.Debug("==== Debug Statement ====")
|
||||
|
||||
if firstRun {
|
||||
app.state = FIRST_RUN
|
||||
}
|
||||
|
||||
w, h := app.initializeSDLAndOpenGL()
|
||||
defer app.cleanup()
|
||||
|
||||
app.initializeManagers()
|
||||
app.initializeAudio()
|
||||
// Create a buffered channel so log writes don't slow down downloads.
|
||||
downloadProgressChan := make(chan download.DownloadStatus, 1000)
|
||||
downloadDone := make(chan struct{})
|
||||
|
||||
app.PageRouter = page.NewRouter(page.PageCtx{
|
||||
DbPtr: app.DB,
|
||||
W: w,
|
||||
H: h,
|
||||
MessageChannel: downloadProgressChan,
|
||||
DownloadDone: downloadDone,
|
||||
})
|
||||
|
||||
// Sync the proclamations from the API to the Database
|
||||
sync.SyncProclamations(DB)
|
||||
sync.SyncCategories(DB)
|
||||
sync.SyncFrequencies(DB)
|
||||
|
||||
// audioFileNames := []string{
|
||||
// "OriginalAudio.wav",
|
||||
// "174Hz.wav",
|
||||
// "396Hz.wav",
|
||||
// "417Hz.wav",
|
||||
// "432Hz.wav",
|
||||
// "639Hz.wav",
|
||||
// "741Hz.wav",
|
||||
// "852Hz.wav",
|
||||
// "963Hz.wav",
|
||||
// "1111Hz.wav",
|
||||
// "14Hz_Binaural.wav",
|
||||
// "40Hz_Binaural.wav",
|
||||
// "AlphaWaves.wav",
|
||||
// "PinealGland.wav",
|
||||
// }
|
||||
|
||||
audioDir := filepath.Join(homeDir, ".radiantwave", "audio")
|
||||
|
||||
_, err = os.Stat(audioDir)
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(audioDir, 0755); err != nil {
|
||||
logger.Error("unable to create audio directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
go download.DownloadAudioFiles(app.PageRouter.Context.MessageChannel, audioDir, "https://files.radiantwavetech.com/audio/")
|
||||
|
||||
app.runEventLoop()
|
||||
}
|
||||
|
||||
// computeState determines what state the application should be in based on
|
||||
// current conditions, without performing any navigation. It returns the first
|
||||
// unmet prerequisite in priority order: network > email > license > downloads > ready.
|
||||
func (app *Application) computeState() State {
|
||||
if app.state == READY {
|
||||
return READY
|
||||
}
|
||||
|
||||
// Check if downloads have finished (non-blocking)
|
||||
if !app.downloadsComplete {
|
||||
select {
|
||||
case <-app.PageRouter.Context.DownloadDone:
|
||||
app.downloadsComplete = true
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is an internet connection
|
||||
if !networkIsUp {
|
||||
if lastNetworkCheckTime.IsZero() || time.Since(lastNetworkCheckTime) >= time.Second {
|
||||
lastNetworkCheckTime = time.Now()
|
||||
if !network.HasInternet() {
|
||||
return NEEDS_INTERNET
|
||||
}
|
||||
networkIsUp = true // internet confirmed, don't check again
|
||||
} else {
|
||||
return app.state // throttled, can't determine yet
|
||||
}
|
||||
}
|
||||
|
||||
// Check for email address
|
||||
config, err := db.GetConfig(app.DB)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// A rather annoying, insecure, but sometimes necessary "print the config" statement
|
||||
// data, err := json.MarshalIndent(config, "", " ")
|
||||
// if err != nil {
|
||||
// logger.Error("failed to marshal config: %v", err)
|
||||
// } else {
|
||||
// logger.Debug("AppConfig: %s", data)
|
||||
// }
|
||||
|
||||
if config.EmailAddress == "" {
|
||||
return NEEDS_EMAIL_ADDRESS
|
||||
}
|
||||
|
||||
// Check for license key
|
||||
config, err = db.GetConfig(app.DB)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if config.LicenseKey == "" {
|
||||
return NEEDS_LICENSE_KEY
|
||||
}
|
||||
|
||||
// All config prerequisites met -- now wait for downloads to finish
|
||||
if !app.downloadsComplete {
|
||||
return DOWNLOADING_ASSETS
|
||||
}
|
||||
|
||||
return READY
|
||||
}
|
||||
|
||||
// checkState keeps watch of the application state and sets pages
|
||||
// accordingly. This function runs once per frame.
|
||||
func (app *Application) checkState() {
|
||||
newState := app.computeState()
|
||||
|
||||
if newState == app.state {
|
||||
return
|
||||
}
|
||||
|
||||
oldState := app.state
|
||||
app.state = newState
|
||||
|
||||
// Both DOWNLOADING_ASSETS and READY map to the HOME page.
|
||||
// Skip re-navigation when transitioning between them to avoid
|
||||
// destroying and recreating the Home page (which would close
|
||||
// the already-closed DownloadDone channel).
|
||||
oldIsHome := oldState == DOWNLOADING_ASSETS || oldState == READY
|
||||
newIsHome := newState == DOWNLOADING_ASSETS || newState == READY
|
||||
if oldIsHome && newIsHome {
|
||||
return
|
||||
}
|
||||
|
||||
switch newState {
|
||||
case NEEDS_INTERNET:
|
||||
app.PageRouter.NavigateTo(page.WIFI)
|
||||
case NEEDS_EMAIL_ADDRESS:
|
||||
logger.Warning("No email address detected")
|
||||
app.PageRouter.NavigateTo(page.EMAIL_ADDRESS)
|
||||
case NEEDS_LICENSE_KEY:
|
||||
app.PageRouter.NavigateTo(page.LICENSE_KEY)
|
||||
case DOWNLOADING_ASSETS, READY:
|
||||
app.PageRouter.NavigateTo(page.HOME)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
config, err := db.GetConfig(app.DB)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
shaderDir := filepath.Join(config.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(config); err != nil {
|
||||
logger.Fatal("FontManager initialization failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) initializeAudio() {
|
||||
logger.Info("Initializing audio")
|
||||
config, err := db.GetConfig(app.DB)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := mixer.Init(config, app.refreshCache); err != nil {
|
||||
logger.Error("Unable to initialize audio device '%s': %v", config.SelectedAudioDevice, err)
|
||||
logger.Error("Audio will be unavailable until a device is configured")
|
||||
} else {
|
||||
logger.Info("Audio initialized with device: '%s'", config.SelectedAudioDevice)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) refreshCache() {}
|
||||
|
||||
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() {
|
||||
if app.state != READY && app.state != DOWNLOADING_ASSETS {
|
||||
return
|
||||
}
|
||||
app.PageRouter.NavigateTo(page.SETTINGS)
|
||||
})
|
||||
|
||||
keybinds.Register(sdl.K_F2, sdl.KMOD_NONE, func() {
|
||||
if app.state != READY && app.state != DOWNLOADING_ASSETS {
|
||||
return
|
||||
}
|
||||
app.PageRouter.NavigateTo(page.SCROLLER)
|
||||
})
|
||||
|
||||
keybinds.Register(sdl.K_F1, sdl.KMOD_NONE, func() {
|
||||
if app.state != READY && app.state != DOWNLOADING_ASSETS {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user