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(), ], ), ), ); } }