using Raylib_cs; using Sand.Core; using System.Numerics; using System.Diagnostics; namespace Sand.App; internal static partial class SandApp { private const int ScreenWidth = 1280; private const int ScreenHeight = 800; private const int SidebarWidth = 220; private const int SettingsWidth = 320; private const int ParticleSize = 4; public static void Run() { var state = CreateState(); Raylib.SetConfigFlags(ConfigFlags.ResizableWindow); Raylib.InitWindow(ScreenWidth, ScreenHeight, $"Sand - C# Port ({state.BackendName})"); Raylib.SetTargetFPS(120); var frameImage = Raylib.GenImageColor(state.SimWidth, state.SimHeight, Color.Black); state.FrameTexture = Raylib.LoadTextureFromImage(frameImage); Raylib.UnloadImage(frameImage); while (!Raylib.WindowShouldClose()) { var frameStart = Stopwatch.GetTimestamp(); Update(state); Draw(state); var frameEnd = Stopwatch.GetTimestamp(); state.LastAppFrameTimeMicroseconds = ToMicroseconds(frameStart, frameEnd); state.LastAppOtherTimeMicroseconds = Math.Max( 0, state.LastAppFrameTimeMicroseconds - state.LastUpdateTimeMicroseconds - state.LastFrameBuildCallTimeMicroseconds - state.LastTextureUploadTimeMicroseconds - state.LastDrawTimeMicroseconds); } Raylib.UnloadTexture(state.FrameTexture); Raylib.CloseWindow(); } private static AppState CreateState() { var contentRoot = Path.Combine(AppContext.BaseDirectory, "Content", "part"); var library = ParticleLibraryLoader.LoadFromDirectory(contentRoot); var settings = new SimulationSettings { StorageMode = SimulationStorageMode.Dense, PauseSim = false, EnableDebug = true, EnableFps = true, EnableCursor = true, EnableGasEffect = true, }; var simWidth = (ScreenWidth - SidebarWidth) / ParticleSize; var simHeight = ScreenHeight / ParticleSize; var simulation = CreateSimulationBackend(simWidth, simHeight, library, settings); var categories = BuildCategories(library, simulation.BackendName); var currentCategory = "Solids"; return new AppState { Library = library, Settings = settings, Simulation = simulation, Categories = categories, CurrentCategory = currentCategory, CurrentParticle = categories[currentCategory][0].Id, BackendName = simulation.BackendName, BrushRadius = 3, SimWidth = simWidth, SimHeight = simHeight, UploadBuffer = new byte[simWidth * simHeight * 4], LastWindDirection = new Vector2(1f, 0f), SettingItems = [ new SettingItem("Cursor", () => settings.EnableCursor, () => settings.EnableCursor = !settings.EnableCursor), new SettingItem("Glow", () => settings.EnableGlow, () => settings.EnableGlow = !settings.EnableGlow), new SettingItem("Gas FX", () => settings.EnableGasEffect, () => settings.EnableGasEffect = !settings.EnableGasEffect), new SettingItem("Wind FX", () => settings.EnableWindVisuals, () => settings.EnableWindVisuals = !settings.EnableWindVisuals), new SettingItem("Pressure FX", () => settings.EnablePressureVisuals, () => settings.EnablePressureVisuals = !settings.EnablePressureVisuals), new SettingItem("Debug", () => settings.EnableDebug, () => settings.EnableDebug = !settings.EnableDebug), new SettingItem("FPS", () => settings.EnableFps, () => settings.EnableFps = !settings.EnableFps), new SettingItem("Temp FX", () => settings.EnableTempVisuals, () => settings.EnableTempVisuals = !settings.EnableTempVisuals), new SettingItem("Wrap", () => settings.WrapParticles, () => settings.WrapParticles = !settings.WrapParticles), new SettingItem("Outer Wall", () => settings.OuterWall, () => settings.OuterWall = !settings.OuterWall), ], }; } private static ISimulationBackend CreateSimulationBackend(int simWidth, int simHeight, IParticleLibrary library, SimulationSettings settings) { var backend = Environment.GetEnvironmentVariable("SAND_BACKEND"); var storageMode = settings.StorageMode; if (string.Equals(backend, "chunk", StringComparison.OrdinalIgnoreCase)) { storageMode = SimulationStorageMode.ChunkPrototype; } else if (string.Equals(backend, "dense", StringComparison.OrdinalIgnoreCase)) { storageMode = SimulationStorageMode.Dense; } if (storageMode == SimulationStorageMode.ChunkPrototype) { return new ChunkPrototypeSimulationBackend(simWidth, simHeight, ParticleSize, library, settings); } return new CoreSimulationBackend(new SandSimulation(simWidth, simHeight, ParticleSize, library, settings)); } private static Dictionary> BuildCategories(IParticleLibrary library, string backendName) { if (string.Equals(backendName, "chunk", StringComparison.OrdinalIgnoreCase)) { return BuildChunkPrototypeCategories(library); } var special = library.Definitions.Where(static d => d.IsSpecial).OrderBy(static d => d.Name).ToList(); if (!special.Any(static d => d.Id == "air")) { special.Add(new ParticleDef { Id = "air", Name = "Air", Kind = ParticleKind.Gas, IsSpecial = true, Color = new Rgb24(160, 210, 255), }); } special = special.OrderBy(static d => d.Name).ToList(); return new Dictionary> { ["Solids"] = library.Definitions.Where(static d => d.Kind == ParticleKind.Solid && !d.IsSpecial).OrderBy(static d => d.Name).ToList(), ["Liquids"] = library.Definitions.Where(static d => d.Kind == ParticleKind.Liquid).OrderBy(static d => d.Name).ToList(), ["Gases"] = library.Definitions.Where(static d => d.Kind == ParticleKind.Gas).OrderBy(static d => d.Name).ToList(), ["Special"] = special, }; } private static Dictionary> BuildChunkPrototypeCategories(IParticleLibrary library) { var special = library.Definitions.Where(static d => d.IsSpecial).OrderBy(static d => d.Name).ToList(); if (!special.Any(static d => d.Id == "air")) { special.Add(new ParticleDef { Id = "air", Name = "Air", Kind = ParticleKind.Gas, IsSpecial = true, Color = new Rgb24(160, 210, 255), }); } special = special.OrderBy(static d => d.Name).ToList(); return new Dictionary> { ["Solids"] = library.Definitions.Where(static d => d.Kind == ParticleKind.Solid && !d.IsSpecial).OrderBy(static d => d.Name).ToList(), ["Liquids"] = library.Definitions.Where(static d => d.Kind == ParticleKind.Liquid).OrderBy(static d => d.Name).ToList(), ["Gases"] = library.Definitions.Where(static d => d.Kind == ParticleKind.Gas).OrderBy(static d => d.Name).ToList(), ["Special"] = special, }; } private static long ToMicroseconds(long startTimestamp, long endTimestamp) { return (long)((endTimestamp - startTimestamp) * 1_000_000.0 / Stopwatch.Frequency); } }