From 3fad3812e117c6bc16b5007076803f498538e4c4 Mon Sep 17 00:00:00 2001 From: Mica White Date: Mon, 8 Dec 2025 19:54:36 -0500 Subject: First commit --- lib/console.dart | 157 ++++++++++++++++++++++++++++++++ lib/logs.dart | 242 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/main.dart | 254 ++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/profiler.dart | 76 ++++++++++++++++ lib/project.dart | 174 +++++++++++++++++++++++++++++++++++ lib/serializer.dart | 93 +++++++++++++++++++ lib/settings.dart | 23 +++++ 7 files changed, 1019 insertions(+) create mode 100755 lib/console.dart create mode 100755 lib/logs.dart create mode 100755 lib/main.dart create mode 100755 lib/profiler.dart create mode 100755 lib/project.dart create mode 100755 lib/serializer.dart create mode 100755 lib/settings.dart (limited to 'lib') diff --git a/lib/console.dart b/lib/console.dart new file mode 100755 index 0000000..e5bbc8b --- /dev/null +++ b/lib/console.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; + +class ConsoleEntry { + final String text; + final bool generatedByRuntime; + final DateTime timeGenerated; + + ConsoleEntry({ + required this.text, + required this.generatedByRuntime, + required this.timeGenerated, + }); +} + +class _ConsoleFilter extends StatelessWidget { + const _ConsoleFilter(this.filter, {required this.onChanged}); + + final String filter; + final Function(String filter) onChanged; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 3), + child: TextFormField( + autocorrect: false, + enableSuggestions: false, + onChanged: onChanged, + style: const TextStyle(fontSize: 12), + decoration: const InputDecoration( + contentPadding: EdgeInsets.all(6), + labelText: "Filter: ", + ), + ), + ), + ), + const SizedBox(width: 16), + const Text("Console entries are only kept for several seconds"), + ], + ); + } +} + +class _ConsoleLine extends StatelessWidget { + const _ConsoleLine( + this.text, { + required this.runtime, + required this.time, + }); + + final String text; + final bool runtime; + final DateTime time; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 3), + child: DecoratedBox( + decoration: BoxDecoration( + color: this.runtime + ? Colors.blue.shade900.withAlpha(128) + : Colors.purple.shade900.withAlpha(128), + borderRadius: const BorderRadius.all(Radius.circular(4)), + border: Border.all( + color: this.runtime ? Colors.blue.shade700 : Colors.purple.shade700, + width: 3, + )), + child: Padding( + padding: const EdgeInsets.all(6), + child: Row( + children: [ + Text(this.runtime ? "Runtime" : "Editor"), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + this.text, + style: const TextStyle(fontFamily: 'Consolas'), + ), + ), + ), + Text( + "${this.time.hour.toString().padLeft(2, '0')}:${this.time.minute.toString().padLeft(2, '0')}") + ], + ), + ), + ), + ); + } +} + +class ConsolePage extends StatefulWidget { + final List entries; + final void Function(String) messageFn; + + const ConsolePage(this.entries, this.messageFn, {super.key}); + + @override + State createState() => _ConsolePageState(); +} + +class _ConsolePageState extends State { + String filter = ""; + + @override + Widget build(BuildContext context) { + final controller = TextEditingController(); + final filteredMessages = + this.widget.entries.where((e) => e.text.contains(this.filter)).toList(); + + return Center( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column(children: [ + _ConsoleFilter( + filter, + onChanged: (newFilter) => this.setState(() => this.filter = newFilter), + ), + Expanded( + child: ListView.builder( + reverse: true, + itemCount: filteredMessages.length, + prototypeItem: _ConsoleLine( + "Loading...", + runtime: false, + time: DateTime(1970), + ), + itemBuilder: (context, index) { + final entry = filteredMessages[filteredMessages.length - index - 1]; + return _ConsoleLine( + entry.text, + runtime: entry.generatedByRuntime, + time: entry.timeGenerated, + ); + }, + ), + ), + TextField( + autocorrect: false, + enableSuggestions: false, + style: const TextStyle(fontFamily: 'Consolas'), + controller: controller, + decoration: const InputDecoration(hintText: "Send a console message to the runtime"), + onSubmitted: (message) { + this.widget.messageFn(message); + controller.clear(); + }, + ), + ]), + ), + ); + } +} diff --git a/lib/logs.dart b/lib/logs.dart new file mode 100755 index 0000000..78f43c2 --- /dev/null +++ b/lib/logs.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:multiselect/multiselect.dart'; + +enum LogType { + error, + warning, + info, + debug, + trace, +} + +extension LogTypeExtension on LogType { + static LogType parse(String logType) { + final logTypeS = logType.toLowerCase().trim(); + switch (logTypeS) { + case 'error': + return LogType.error; + case 'warning': + return LogType.warning; + case 'info': + return LogType.info; + case 'debug': + return LogType.debug; + case 'trace': + return LogType.trace; + default: + // there's no particular reason why this should be the default + // but hopefully this line is never needed + return LogType.error; + } + } + + String name() { + switch (this) { + case LogType.error: + return "Error"; + case LogType.warning: + return "Warning"; + case LogType.info: + return "Info"; + case LogType.debug: + return "Debug"; + case LogType.trace: + return "Trace"; + } + } +} + +class LogEntry { + final bool scriptLog; + final LogType logType; + final String? file; + final int? lineNumber; + final String message; + final DateTime time; + + LogEntry( + this.scriptLog, + this.logType, + this.file, + this.lineNumber, + this.message, + ) : this.time = DateTime.now(); +} + +class _LogFilter extends StatelessWidget { + const _LogFilter(this.label, this.onChanged, this.selectedValues); + + final String label; + final Function(List) onChanged; + final List selectedValues; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + this.label, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(width: 10), + SizedBox( + width: 200, + height: 40, + child: DropDownMultiSelect( + options: LogType.values.map((e) => e.name()).toList(), + selectedValues: this.selectedValues.map((e) => e.name()).toList(), + whenEmpty: "Filter Script Logs", + isDense: true, + onChanged: this.onChanged, + ), + ), + ], + ); + } +} + +class _LogLine extends StatelessWidget { + const _LogLine(this.entry); + + final LogEntry entry; + + @override + Widget build(BuildContext context) { + Map icons = { + LogType.error: Icon( + Icons.error, + color: Colors.red[300], + size: 24, + ), + LogType.warning: Icon( + Icons.warning, + color: Colors.yellow[300], + size: 24, + ), + LogType.info: Icon( + Icons.info, + color: Colors.blue[300], + size: 24, + ), + LogType.debug: Icon( + Icons.bug_report, + color: Colors.green[300], + size: 24, + ), + LogType.trace: Icon( + Icons.message, + color: Colors.grey[300], + size: 24, + ), + }; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 3), + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.purple.shade900.withAlpha(128), + border: Border.all(color: Colors.purple.shade800, width: 3), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + child: Padding( + padding: const EdgeInsets.all(6), + child: Row( + children: [ + icons[this.entry.logType]!, + const SizedBox(width: 5), + SizedBox(width: 70, child: Text(this.entry.logType.name())), + SizedBox(width: 54, child: Text(this.entry.scriptLog ? 'Script' : 'Runtime')), + const SizedBox(width: 20), + Expanded( + child: Text( + this.entry.message, + style: const TextStyle(fontFamily: 'Consolas'), + ), + ), + const SizedBox(width: 20), + Text("${this.entry.file}:${this.entry.lineNumber}"), + const SizedBox(width: 10), + Text( + "${this.entry.time.hour.toString().padLeft(2, '0')}:${this.entry.time.minute.toString().padLeft(2, '0')}") + ], + ), + ), + ), + ); + } +} + +class LogPage extends StatefulWidget { + const LogPage(this.logEntries, {super.key}); + + final List logEntries; + + @override + State createState() => _LogPageState(); +} + +class _LogPageState extends State { + Set runtimeLogsFilter = {LogType.error, LogType.warning}; + Set scriptLogsFilter = Set.from(LogType.values); + String searchFilter = ''; + + Function(List) _generateOnFilter(Set filter) { + return (values) { + setState(() { + filter.clear(); + filter.addAll(values.map((e) => LogTypeExtension.parse(e))); + }); + }; + } + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column(children: [ + Row(children: [ + Expanded( + child: SizedBox( + height: 40, + child: TextField( + autocorrect: false, + onChanged: (search) => setState(() => this.searchFilter = search), + decoration: + const InputDecoration(border: OutlineInputBorder(), labelText: 'Search'), + ), + ), + ), + const SizedBox(width: 80), + _LogFilter( + "Runtime Logs Filter:", + _generateOnFilter(this.runtimeLogsFilter), + runtimeLogsFilter.toList(), + ), + const SizedBox(width: 80), + _LogFilter( + "Script Logs Filter:", + _generateOnFilter(this.scriptLogsFilter), + scriptLogsFilter.toList(), + ), + ]), + Expanded( + child: SingleChildScrollView( + reverse: true, + child: Column( + children: this + .widget + .logEntries + .where((e) => + ((e.scriptLog && this.scriptLogsFilter.contains(e.logType)) || + (!e.scriptLog && this.runtimeLogsFilter.contains(e.logType))) && + (this.searchFilter.isEmpty || e.message.contains(this.searchFilter))) + .map((e) => _LogLine(e)) + .toList()), + ), + ), + ]), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100755 index 0000000..270eb34 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,254 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'console.dart'; +import 'logs.dart'; +import 'project.dart'; +import 'settings.dart'; +import 'profiler.dart'; +import 'serializer.dart'; + +const maxConsoleEntries = 15000; +const maxProfileFrames = 15000; + +void main() { + runApp(MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => ProjectConfig()), + ], + child: const AlligatorApp(), + )); +} + +class AlligatorApp extends StatelessWidget { + const AlligatorApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Alligator Editor', + locale: const Locale('en', 'US'), + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + brightness: Brightness.dark, + seedColor: Colors.green, + ), + useMaterial3: true, + ), + home: const MyHomePage(), + ); + } +} + +class RunButtons extends StatelessWidget { + final bool isRunning; + final Function(BuildContext) onStart; + final Function(BuildContext) onStop; + + const RunButtons(this.isRunning, {required this.onStart, required this.onStop, super.key}); + + @override + Widget build(BuildContext context) { + if (!this.isRunning) { + return TextButton.icon( + onPressed: () => this.onStart(context), + icon: const Icon(Icons.play_arrow), + label: const Text('Run'), + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + side: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 3, + ), + ), + ), + ); + } else { + return TextButton.icon( + onPressed: () => this.onStop(context), + icon: const Icon(Icons.stop), + label: const Text('Stop'), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + side: BorderSide( + color: Theme.of(context).colorScheme.error, + width: 3, + ), + ), + ), + ); + } + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final List _consoleEntries = []; + + static const List tabs = [ + Tab(text: 'Project'), + Tab(text: 'Log'), + Tab(text: 'Profiler'), + Tab(text: 'Console'), + Tab(text: 'Settings'), + ]; + + Process? _runningGame; + final StringBuffer _buffer = StringBuffer(); + + final List _logEntries = []; + + DateTime? _previousFrameTime; + int _nextFrameId = 0; + final List _frames = []; + + void _sendMessage(String msg) { + this._runningGame?.stdin.writeln(msg); + setState(() { + _consoleEntries.add(ConsoleEntry( + text: msg, + generatedByRuntime: false, + timeGenerated: DateTime.now(), + )); + }); + } + + void _parseMessage(String message) { + final args = message.split(' '); + if (args[0] == 'runtimelog') { + final logType = LogTypeExtension.parse(args[1]); + final [fileName, lineNumberS] = args[2].split(':'); + final lineNumber = int.parse(lineNumberS); + final logMsg = args.sublist(3).join(' '); + setState(() => this._logEntries.add(LogEntry(false, logType, fileName, lineNumber, logMsg))); + } else if (args[0] == 'scriptlog') { + final logType = LogTypeExtension.parse(args[1]); + final [fileName, lineNumberS] = args[2].split(':'); + final lineNumber = int.parse(lineNumberS); + final logMsg = args.sublist(3).join(' '); + setState(() => this._logEntries.add(LogEntry(true, logType, fileName, lineNumber, logMsg))); + } else if (args[0] == 'frametime') { + if (_previousFrameTime == null) { + _previousFrameTime = DateTime.fromMicrosecondsSinceEpoch(int.parse(args[1])); + return; + } + + final id = _nextFrameId++; + final start = _previousFrameTime!; + final end = DateTime.fromMicrosecondsSinceEpoch(int.parse(args[1])); + _previousFrameTime = end; + + setState(() { + _frames.add(ProfileFrame(id, start, end, [])); + if (_frames.length >= maxProfileFrames) { + _frames.removeRange(0, _frames.length - maxProfileFrames); + } + }); + } + } + + void _startGame(BuildContext cx) async { + ProjectConfig projectConfig = Provider.of(cx, listen: false); + final gameConfig = AlligatorGame.fromConfig(projectConfig: projectConfig).toJson(); + + this._runningGame?.kill(); + + this._buffer.clear(); + _consoleEntries.clear(); + this._logEntries.clear(); + this._previousFrameTime = null; + this._nextFrameId = 0; + this._frames.clear(); + + this._runningGame = await Process.start("alligator", ["--config", gameConfig, "--debug"]); + this._runningGame!.exitCode.then((value) => this._runningGame = null); + this._runningGame!.stdout.listen( + (event) async { + await Future.delayed(const Duration(milliseconds: 16)); + for (final code in event) { + final char = String.fromCharCode(code); + + if (char == "\n") { + final message = this._buffer.toString(); + setState( + () => _consoleEntries.add(ConsoleEntry( + text: message, + generatedByRuntime: true, + timeGenerated: DateTime.now(), + )), + ); + + this._buffer.clear(); + _parseMessage(message); + } else { + this._buffer.write(char); + } + + setState(() { + if (_consoleEntries.length > maxConsoleEntries) { + _consoleEntries.removeRange(0, _consoleEntries.length - maxConsoleEntries); + } + }); + } + }, + onDone: () { + this.setState(() => this._runningGame = null); + }, + ); + } + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return DefaultTabController( + length: tabs.length, + child: Scaffold( + appBar: AppBar( + title: const Text("Alligator Editor"), + bottom: const TabBar( + tabs: tabs, + ), + actions: [ + RunButtons( + this._runningGame != null, + onStart: this._startGame, + onStop: (_) => this._runningGame!.kill(), + ), + const SizedBox(width: 20), + ], + ), + body: TabBarView( + children: [ + const ProjectPage(), + LogPage(this._logEntries), + ProfilerPage(_frames), + ConsolePage(_consoleEntries, this._sendMessage), + const SettingsPage(), + ], + ), + ), + ); + } +} diff --git a/lib/profiler.dart b/lib/profiler.dart new file mode 100755 index 0000000..495dc48 --- /dev/null +++ b/lib/profiler.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +class ProfileScope { + final String name; + final DateTime start; + final DateTime end; + + const ProfileScope(this.name, this.start, this.end); +} + +class ProfileFrame { + final int id; + final DateTime start; + final DateTime end; + final List scopes; + + const ProfileFrame(this.id, this.start, this.end, this.scopes); +} + +extension DurationExt on Duration { + double get toSeconds => this.inMicroseconds.toDouble() / 1000000.0; +} + +class ProfilerPage extends StatelessWidget { + final List frames; + + const ProfilerPage(this.frames, {super.key}); + + @override + Widget build(BuildContext context) { + if (frames.isEmpty) { + return const Center( + child: Text( + "Run the project to see profiling information", + style: TextStyle(fontSize: 36), + ), + ); + } + + final frameTimes = this.frames.map((frame) => frame.end.difference(frame.start)).toList(); + final totalFrameTime = frameTimes.reduce((a, b) => a + b); + final averageFrameTime = totalFrameTime.inMicroseconds / this.frames.length; + final averageFps = this.frames.length / totalFrameTime.toSeconds; + + Duration totalFrameTimeForPreviousSecond = const Duration(); + final List frameTimesForPreviousSecond = []; + for (int i = frameTimes.length - 1; i >= 0; i--) { + final currentFrameTime = frameTimes[i]; + totalFrameTimeForPreviousSecond += currentFrameTime; + frameTimesForPreviousSecond.add(currentFrameTime); + + if (totalFrameTimeForPreviousSecond.toSeconds >= 1.0) { + break; + } + } + frameTimesForPreviousSecond.sort((a, b) => b.compareTo(a)); + final fpsLow = 1 / + frameTimesForPreviousSecond[(frameTimesForPreviousSecond.length / 20).floor()].toSeconds; + + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Average Frame Time: ${averageFrameTime.round()} μs"), + Text("Average FPS: ${averageFps.round()} fps"), + Text("Low 5% FPS: ${fpsLow.round()}"), + ], + ), + ], + ), + ); + } +} diff --git a/lib/project.dart b/lib/project.dart new file mode 100755 index 0000000..1f9b0ad --- /dev/null +++ b/lib/project.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +enum WindowMode { + windowed, + borderlessFullscreen; + + String get displayString { + switch (this) { + case WindowMode.windowed: + return "Windowed"; + case WindowMode.borderlessFullscreen: + return "Borderless Fullscreen"; + } + } + + String get pascalCase { + switch (this) { + case WindowMode.windowed: + return "Windowed"; + case WindowMode.borderlessFullscreen: + return "BorderlessFullscreen"; + } + } + + static WindowMode fromPascalCase(String pascalCase) { + return WindowMode.values.firstWhere((mode) => mode.pascalCase == pascalCase); + } +} + +class ProjectConfig extends ChangeNotifier { + final TextEditingController _gameName; + final TextEditingController _defaultWindowWidth; + final TextEditingController _defaultWindowHeight; + + bool _defaultVsync; + WindowMode _defaultWindowMode; + + String get gameName => this._gameName.text; + set gameName(String gameName) { + this._gameName.text = gameName; + } + + int get defaultWindowWidth => int.parse(this._defaultWindowWidth.text); + + set defaultWindowWidth(int defaultWindowWidth) { + this._defaultWindowWidth.text = defaultWindowWidth.toString(); + } + + int get defaultWindowHeight => int.parse(this._defaultWindowHeight.text); + + set defaultWindowHeight(int? defaultWindowHeight) { + this._defaultWindowHeight.text = defaultWindowHeight.toString(); + } + + bool get defaultVsync => this._defaultVsync; + set defaultVsync(bool defaultVsync) { + this._defaultVsync = defaultVsync; + notifyListeners(); + } + + WindowMode get defaultWindowMode => this._defaultWindowMode; + set defaultWindowMode(WindowMode defaultWindowMode) { + this._defaultWindowMode = defaultWindowMode; + notifyListeners(); + } + + ProjectConfig({ + String gameName = "Black Screen", + int defaultWindowWidth = 1280, + int defaultWindowHeight = 720, + bool defaultVsync = true, + WindowMode defaultWindowMode = WindowMode.windowed, + }) : this._gameName = TextEditingController(text: gameName), + this._defaultWindowWidth = TextEditingController(text: defaultWindowWidth.toString()), + this._defaultWindowHeight = TextEditingController(text: defaultWindowHeight.toString()), + this._defaultVsync = defaultVsync, + this._defaultWindowMode = defaultWindowMode; +} + +class ProjectPage extends StatelessWidget { + const ProjectPage({super.key}); + + int getWidth(ProjectConfig config) => int.tryParse(config._defaultWindowWidth.text) ?? 1280; + int getHeight(ProjectConfig config) => int.tryParse(config._defaultWindowHeight.text) ?? 720; + + @override + Widget build(BuildContext context) { + var config = Provider.of(context, listen: false); + + return Padding( + padding: const EdgeInsets.all(20), + child: Form( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 600, + child: TextFormField( + decoration: const InputDecoration( + labelText: "Project Name", + //helperText: "This will appear as the game window's title", + border: OutlineInputBorder(), + ), + controller: config._gameName, + ), + ), + const SizedBox(height: 20), + DropdownMenu( + width: 600, + label: const Text("Default Window Mode"), + initialSelection: config.defaultWindowMode, + onSelected: (v) => config.defaultWindowMode = v!, + dropdownMenuEntries: WindowMode.values + .map(((mode) => DropdownMenuEntry(value: mode, label: mode.displayString))) + .toList(), + ), + const SizedBox(height: 20), + Consumer( + builder: (context, config, child) => Row( + children: [ + SizedBox( + width: 300, + child: TextFormField( + decoration: const InputDecoration( + labelText: "Default Resolution Width", + border: OutlineInputBorder(), + ), + controller: config._defaultWindowWidth, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Consumer( + builder: (context, config, child) => Row( + children: [ + SizedBox( + width: 300, + child: TextFormField( + decoration: const InputDecoration( + labelText: "Default Resolution Height", + border: OutlineInputBorder(), + ), + controller: config._defaultWindowHeight, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + SizedBox( + width: 250, + child: Consumer( + builder: (context, config, child) => CheckboxListTile( + value: config.defaultVsync, + onChanged: (v) => config.defaultVsync = v!, + title: const Text("Default Vsync", style: TextStyle(fontSize: 20)), + controlAffinity: ListTileControlAffinity.leading, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/serializer.dart b/lib/serializer.dart new file mode 100755 index 0000000..d9d5dbe --- /dev/null +++ b/lib/serializer.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:alligator_editor/project.dart'; + +const currentAlligatorVersion = "0.1"; +const currentRuntimeVersion = "0.1"; + +class AlligatorProject { + String version; + ProjectConfig projectConfig; + + AlligatorProject({ + this.version = currentAlligatorVersion, + required this.projectConfig, + }); + + factory AlligatorProject.fromJson(String json) { + Map jsonMap = jsonDecode(json); + String version = jsonMap['version']; + ProjectConfig projectConfig = ProjectConfig( + gameName: jsonMap['project']['name'], + defaultWindowWidth: jsonMap['project']['defaultWidth'], + defaultWindowHeight: jsonMap['project']['defaultHeight'], + defaultWindowMode: WindowMode.fromPascalCase(jsonMap['project']['defaultWindowMode']), + defaultVsync: jsonMap['project']['vsync'], + ); + + return AlligatorProject(version: version, projectConfig: projectConfig); + } + + String toJson() { + Map json = { + 'version': this.version, + 'project': { + 'name': this.projectConfig.gameName, + 'defaultWidth': this.projectConfig.defaultWindowWidth, + 'defaultHeight': this.projectConfig.defaultWindowHeight, + 'defaultWindowMode': this.projectConfig.defaultWindowMode, + 'vsync': this.projectConfig.defaultVsync, + } + }; + + return jsonEncode(json); + } +} + +class AlligatorGame { + final String alligatorVersion; + final int? defaultWindowWidth; + final int? defaultWindowHeight; + final WindowMode defaultWindowMode; + final String windowTitle; + final bool vsync; + + const AlligatorGame({ + this.alligatorVersion = currentRuntimeVersion, + this.defaultWindowWidth = 1280, + this.defaultWindowHeight = 720, + this.defaultWindowMode = WindowMode.windowed, + this.windowTitle = "Alligator Game", + this.vsync = true, + }); + + AlligatorGame.fromConfig({ + String alligatorVersion = currentRuntimeVersion, + required ProjectConfig projectConfig, + }) : this( + alligatorVersion: alligatorVersion, + defaultWindowWidth: projectConfig.defaultWindowWidth, + defaultWindowHeight: projectConfig.defaultWindowHeight, + defaultWindowMode: projectConfig.defaultWindowMode, + windowTitle: projectConfig.gameName, + vsync: projectConfig.defaultVsync, + ); + + String toJson() { + Map json = { + 'alligator_version': this.alligatorVersion, + 'scenes': {}, + 'textures': {}, + 'scripts': {}, + 'default_scene': "", + 'sprite_manager_capacity': 0, + 'default_window_width': this.defaultWindowWidth, + 'default_window_height': this.defaultWindowHeight, + 'default_window_mode': this.defaultWindowMode.pascalCase, + 'window_title': this.windowTitle, + 'vsync': this.vsync + }; + + return jsonEncode(json); + } +} diff --git a/lib/settings.dart b/lib/settings.dart new file mode 100755 index 0000000..b968502 --- /dev/null +++ b/lib/settings.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class Settings { + int tabPagePadding = 20; + double logRadius = 4; + double logBorderThickness = 2; + double logPadding = 6; + double logMessagePadding = 16; + String logMessageFont = 'Consolas'; + + Color consoleRuntimeEntry = Colors.blue.shade900; + Color consoleEditorEntry = Colors.purple.shade900; + Color consoleEntryBorderColor = Colors.white; +} + +class SettingsPage extends StatelessWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} -- cgit v1.2.3