summaryrefslogtreecommitdiff
path: root/lib/main.dart
blob: 270eb34403ef38917dd9862d4544578951b2d07c (plain)
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<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final List<ConsoleEntry> _consoleEntries = [];

  static const List<Tab> tabs = <Tab>[
    Tab(text: 'Project'),
    Tab(text: 'Log'),
    Tab(text: 'Profiler'),
    Tab(text: 'Console'),
    Tab(text: 'Settings'),
  ];

  Process? _runningGame;
  final StringBuffer _buffer = StringBuffer();

  final List<LogEntry> _logEntries = [];

  DateTime? _previousFrameTime;
  int _nextFrameId = 0;
  final List<ProfileFrame> _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(),
          ],
        ),
      ),
    );
  }
}