base timer off of game state event

pull/1384/head
ggurdin 1 year ago
parent e8bb166fae
commit 7f731e1406
No known key found for this signature in database
GPG Key ID: A01CB41737CBB478

@ -15,10 +15,12 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart';
import 'package:fluffychat/pages/chat/recording_dialog.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart';
import 'package:fluffychat/pangea/models/game_state_model.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/pages/games/story_game/round_model.dart';
@ -26,7 +28,6 @@ import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/utils/report_message.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/widgets/chat/round_timer.dart';
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
@ -115,17 +116,9 @@ class ChatController extends State<ChatPageWithRoom>
// #Pangea
final PangeaController pangeaController = MatrixState.pangeaController;
late Choreographer choreographer = Choreographer(pangeaController, this);
final GlobalKey<RoundTimerState> roundTimerStateKey =
GlobalKey<RoundTimerState>();
RoundTimer? timer;
final List<GameRoundModel> gameRounds = [];
List<String> get completedRoundEventIds => gameRounds
.where((round) => round.isCompleted)
.map((round) => round.userMessageIDs)
.expand((x) => x)
.toList();
/// Model of the current story game round
GameRoundModel? currentRound;
// Pangea#
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
@ -308,12 +301,22 @@ class ChatController extends State<ChatPageWithRoom>
}
// #Pangea
void addRound() {
debugPrint("ADDING A ROUND. Rounds so far: ${gameRounds.length}");
final newRound = GameRoundModel(controller: this, timer: timer!);
gameRounds.add(newRound);
newRound.roundCompleter.future.then((_) {
if (mounted) addRound();
/// Recursive function that sets the current round, waits for it to
/// finish, sets it, etc. until the chat view is no longer mounted.
void setRound() {
currentRound?.dispose();
currentRound = GameRoundModel(room: room);
room.client.onRoomState.stream.firstWhere((update) {
if (update.roomId != roomId) return false;
if (update.state is! Event) return false;
if ((update.state as Event).type != PangeaEventTypes.storyGame) {
return false;
}
final game = GameModel.fromJson((update.state as Event).content);
return game.previousRoundEndTime != null;
}).then((_) {
if (mounted) setRound();
});
}
// Pangea#
@ -333,8 +336,7 @@ class ChatController extends State<ChatPageWithRoom>
sendingClient = Matrix.of(context).client;
WidgetsBinding.instance.addObserver(this);
// #Pangea
timer = RoundTimer(key: roundTimerStateKey);
addRound();
setRound();
if (!mounted) return;
Future.delayed(const Duration(seconds: 1), () async {
if (!mounted) return;
@ -421,8 +423,7 @@ class ChatController extends State<ChatPageWithRoom>
List<Event> get visibleEvents =>
timeline?.events
.where(
(x) =>
x.isVisibleInGui && !completedRoundEventIds.contains(x.eventId),
(x) => x.isVisibleInGui,
)
.toList() ??
<Event>[];
@ -560,6 +561,7 @@ class ChatController extends State<ChatPageWithRoom>
//#Pangea
choreographer.stateListener.close();
choreographer.dispose();
currentRound?.dispose();
//Pangea#
super.dispose();
}

@ -6,6 +6,7 @@ import 'package:fluffychat/pages/chat/typing_indicators.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/widgets/chat/locked_chat_message.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
@ -32,7 +33,13 @@ class ChatEventList extends StatelessWidget {
event.isVisibleInGui
// #Pangea
&&
!controller.completedRoundEventIds.contains(event.eventId)
// In story game, hide messages sent by non-bot users in previous round
(event.type != EventTypes.Message ||
event.senderId == BotName.byEnvironment ||
controller.currentRound?.previousRoundEnd == null ||
event.originServerTs.isAfter(
controller.currentRound!.previousRoundEnd!,
))
// Pangea#
,
)

@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart';
import 'package:fluffychat/pangea/widgets/chat/round_timer.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/widgets/connection_status_header.dart';
@ -119,7 +120,7 @@ class ChatView extends StatelessWidget {
// #Pangea
} else {
return [
controller.timer ?? const SizedBox(),
RoundTimer(controller: controller),
const SizedBox(
width: 10,
),

@ -0,0 +1,3 @@
class GameConstants {
static const int timerMaxSeconds = 120;
}

@ -119,4 +119,8 @@ class ModelKey {
static const String prevEventId = "prev_event_id";
static const String prevLastUpdated = "prev_last_updated";
static const String gameState = "game_state";
static const String currentRoundStartTime = "start_time";
static const String previousRoundEndTime = "message_visible_from";
}

@ -35,4 +35,6 @@ class PangeaEventTypes {
/// A record of completion of an activity. There
/// can be one per user per activity.
static const activityRecord = "pangea.activity_completion";
static const storyGame = "p.game.story";
}

@ -0,0 +1,48 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix_api_lite/generated/model.dart';
class GameModel {
DateTime? currentRoundStartTime;
DateTime? previousRoundEndTime;
GameModel({
this.currentRoundStartTime,
this.previousRoundEndTime,
});
factory GameModel.fromJson(json) {
return GameModel(
currentRoundStartTime: json[ModelKey.currentRoundStartTime] != null
? DateTime.parse(json[ModelKey.currentRoundStartTime])
: null,
previousRoundEndTime: json[ModelKey.previousRoundEndTime] != null
? DateTime.parse(json[ModelKey.previousRoundEndTime])
: null,
);
}
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
try {
data[ModelKey.currentRoundStartTime] =
currentRoundStartTime?.toIso8601String();
data[ModelKey.previousRoundEndTime] =
previousRoundEndTime?.toIso8601String();
return data;
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: s);
return data;
}
}
StateEvent get toStateEvent => StateEvent(
content: toJson(),
type: PangeaEventTypes.storyGame,
);
}

@ -1,62 +1,84 @@
import 'dart:async';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/constants/game_constants.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/sync_update_extension.dart';
import 'package:fluffychat/pangea/models/game_state_model.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/widgets/chat/round_timer.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
enum RoundState { notStarted, inProgress, completed }
/// A model of a game round. Manages the round's state and duration.
class GameRoundModel {
static const int timerMaxSeconds = 180;
final String adminName = BotName.byEnvironment;
final Duration roundDuration = const Duration(
seconds: GameConstants.timerMaxSeconds,
);
final Room room;
final ChatController controller;
final Completer<void> roundCompleter = Completer<void>();
// All the below state variables are used for sending and managing
// round start and end times. Once the bot starts doing that, they should be removed.
late DateTime createdAt;
RoundTimer timer;
DateTime? startTime;
DateTime? endTime;
RoundState state = RoundState.notStarted;
Timer? timer;
StreamSubscription? syncSubscription;
final List<String> userMessageIDs = [];
final List<String> botMessageIDs = [];
GameRoundModel({
required this.controller,
required this.timer,
required this.room,
}) {
createdAt = DateTime.now();
syncSubscription ??= client.onSync.stream.listen(_handleSync);
// if, on creation, the current round is already ongoing,
// start the timer (or reset it if the round went over)
if (currentRoundStart != null) {
final currentRoundDuration = DateTime.now().difference(
currentRoundStart!,
);
final roundFinished = currentRoundDuration > roundDuration;
if (roundFinished) {
endRound();
}
}
// listen to syncs for new bot messages to start and stop rounds
syncSubscription ??= room.client.onSync.stream.listen(_handleSync);
}
GameModel get gameState => GameModel.fromJson(
room.getState(PangeaEventTypes.storyGame)?.content ?? {},
);
DateTime? get currentRoundStart => gameState.currentRoundStartTime;
DateTime? get previousRoundEnd => gameState.previousRoundEndTime;
void _handleSync(SyncUpdate update) {
final newMessages = update
.messages(controller.room)
.messages(room)
.where((msg) => msg.originServerTs.isAfter(createdAt))
.toList();
final botMessages =
newMessages.where((msg) => msg.senderId == adminName).toList();
final userMessages =
newMessages.where((msg) => msg.senderId != adminName).toList();
final botMessages = newMessages
.where((msg) => msg.senderId == BotName.byEnvironment)
.toList();
final userMessages = newMessages
.where((msg) => msg.senderId != BotName.byEnvironment)
.toList();
final hasNewBotMessage = botMessages.any(
(msg) => !botMessageIDs.contains(msg.eventId),
);
if (hasNewBotMessage) {
if (state == RoundState.notStarted) {
if (currentRoundStart == null) {
startRound();
} else if (state == RoundState.inProgress) {
} else {
endRound();
return;
}
}
if (state == RoundState.inProgress) {
if (currentRoundStart != null) {
for (final message in botMessages) {
if (!botMessageIDs.contains(message.eventId)) {
botMessageIDs.add(message.eventId);
@ -71,32 +93,53 @@ class GameRoundModel {
}
}
Client get client => controller.pangeaController.matrixState.client;
/// Set the start and end times of the current and previous rounds.
Future<void> setRoundTimes({
DateTime? currentRoundStart,
DateTime? previousRoundEnd,
}) async {
final game = GameModel.fromJson(
room.getState(PangeaEventTypes.storyGame)?.content ?? {},
);
bool get isCompleted => roundCompleter.isCompleted;
game.currentRoundStartTime = currentRoundStart;
game.previousRoundEndTime = previousRoundEnd;
void startRound() {
debugPrint("starting round");
state = RoundState.inProgress;
startTime = DateTime.now();
controller.roundTimerStateKey.currentState?.resetTimer(
roundLength: timerMaxSeconds,
await room.client.setRoomStateWithKey(
room.id,
PangeaEventTypes.storyGame,
'',
game.toJson(),
);
controller.roundTimerStateKey.currentState?.startTimer();
}
/// Start a new round.
void startRound() {
setRoundTimes(
currentRoundStart: DateTime.now(),
previousRoundEnd: null,
).then((_) => timer = Timer(roundDuration, endRound));
}
/// End and cleanup after the current round.
void endRound() {
debugPrint(
"ending round, user message IDs: $userMessageIDs, bot message IDs: $botMessageIDs",
);
endTime = DateTime.now();
state = RoundState.completed;
controller.roundTimerStateKey.currentState?.resetTimer();
syncSubscription?.cancel();
roundCompleter.complete();
syncSubscription = null;
timer?.cancel();
timer = null;
setRoundTimes(
currentRoundStart: null,
previousRoundEnd: DateTime.now(),
);
}
void dispose() {
syncSubscription?.cancel();
syncSubscription = null;
timer?.cancel();
timer = null;
}
}

@ -1,17 +1,20 @@
import 'dart:async';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/constants/game_constants.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/models/game_state_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
/// Create a timer that counts down to the given time
/// Default duration is 180 seconds
class RoundTimer extends StatefulWidget {
final int timerMaxSeconds;
final Duration roundDuration;
final ChatController controller;
const RoundTimer({
super.key,
this.timerMaxSeconds = 180,
this.roundDuration = const Duration(seconds: 1),
required this.controller,
});
@override
@ -20,90 +23,112 @@ class RoundTimer extends StatefulWidget {
class RoundTimerState extends State<RoundTimer> {
int currentSeconds = 0;
Timer? _timer;
bool isTiming = false;
Duration? duration;
int timerMaxSeconds = 180;
void resetTimer({Duration? roundDuration, int? roundLength}) {
if (_timer != null) {
_timer!.cancel();
isTiming = false;
}
if (roundDuration != null) {
duration = roundDuration;
}
if (roundLength != null) {
timerMaxSeconds = roundLength;
}
setState(() {
currentSeconds = 0;
});
}
Timer? timer;
StreamSubscription? stateSubscription;
int get remainingTime => timerMaxSeconds - currentSeconds;
@override
void initState() {
super.initState();
String get timerText =>
'${(remainingTime ~/ 60).toString().padLeft(2, '0')}: ${(remainingTime % 60).toString().padLeft(2, '0')}';
final roundStartTime = widget.controller.currentRound?.currentRoundStart;
if (roundStartTime != null) {
final roundDuration = DateTime.now().difference(roundStartTime).inSeconds;
if (roundDuration > GameConstants.timerMaxSeconds) return;
startTimer() {
_timer = Timer.periodic(duration ?? widget.roundDuration, (timer) {
setState(() {
currentSeconds = roundDuration;
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
currentSeconds++;
if (currentSeconds >= timerMaxSeconds) timer.cancel();
if (currentSeconds >= GameConstants.timerMaxSeconds) {
t.cancel();
}
setState(() {});
});
});
setState(() {
isTiming = true;
});
}
stateSubscription = Matrix.of(context)
.client
.onRoomState
.stream
.where(isRoundUpdate)
.listen(onRoundUpdate);
}
stopTimer() {
if (_timer != null) {
_timer!.cancel();
}
setState(() {
isTiming = false;
});
bool isRoundUpdate(update) {
return update.roomId == widget.controller.room.id &&
update.state is Event &&
(update.state as Event).type == PangeaEventTypes.storyGame;
}
@override
void initState() {
duration = widget.roundDuration;
timerMaxSeconds = widget.timerMaxSeconds;
super.initState();
void onRoundUpdate(update) {
final GameModel gameState = GameModel.fromJson(
(update.state as Event).content,
);
final startTime = gameState.currentRoundStartTime;
final endTime = gameState.previousRoundEndTime;
if (startTime == null && endTime == null) return;
timer?.cancel();
timer = null;
// if this update is the start of a round
if (startTime != null) {
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
currentSeconds++;
if (currentSeconds >= GameConstants.timerMaxSeconds) {
t.cancel();
}
setState(() {});
});
return;
}
// if this update is the end of a round
currentSeconds = 0;
setState(() {});
}
@override
void dispose() {
if (_timer != null) {
_timer!.cancel();
}
super.dispose();
stateSubscription?.cancel();
stateSubscription = null;
timer?.cancel();
timer = null;
}
int get remainingTime => GameConstants.timerMaxSeconds - currentSeconds;
String get timerText =>
'${(remainingTime ~/ 60).toString().padLeft(2, '0')}: ${(remainingTime % 60).toString().padLeft(2, '0')}';
@override
Widget build(BuildContext context) {
return Material(
color: const Color.fromARGB(255, 126, 22, 14),
child: Padding(
padding: const EdgeInsets.all(
5,
),
padding: const EdgeInsets.all(5),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(timerText),
// Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// IconButton(
// onPressed: isTiming ? stopTimeout : startTimeout,
// icon: Icon(isTiming ? Icons.pause_circle : Icons.play_circle),
// ),
// ],
// ),
const Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// IconButton(
// onPressed: widget.currentRound.timer == null
// ? widget.currentRound.startRound
// : null,
// icon: Icon(
// widget.currentRound.timer != null
// ? Icons.pause_circle
// : Icons.play_circle,
// ),
// ),
],
),
],
),
),

Loading…
Cancel
Save