diff --git a/api/lib/models.dart b/api/lib/models.dart index d2510fda..409b27ba 100644 --- a/api/lib/models.dart +++ b/api/lib/models.dart @@ -14,3 +14,4 @@ export 'src/models/table.dart'; export 'src/models/translation.dart'; export 'src/models/vector.dart'; export 'src/models/visual.dart'; +export 'src/models/waypoint.dart'; diff --git a/api/lib/src/event/event.mapper.dart b/api/lib/src/event/event.mapper.dart index 25448de3..dc48847d 100644 --- a/api/lib/src/event/event.mapper.dart +++ b/api/lib/src/event/event.mapper.dart @@ -4198,6 +4198,7 @@ class HybridWorldEventMapper extends SubClassMapperBase { if (_instance == null) { MapperContainer.globals.use(_instance = HybridWorldEventMapper._()); WorldEventMapper.ensureInitialized().addSubMapper(_instance!); + CellSwitchedMapper.ensureInitialized(); BackgroundChangedMapper.ensureInitialized(); ObjectsSpawnedMapper.ensureInitialized(); ObjectsMovedMapper.ensureInitialized(); @@ -4211,6 +4212,8 @@ class HybridWorldEventMapper extends SubClassMapperBase { TableRemovedMapper.ensureInitialized(); NoteChangedMapper.ensureInitialized(); NoteRemovedMapper.ensureInitialized(); + WaypointChangedMapper.ensureInitialized(); + WaypointRemovedMapper.ensureInitialized(); } return _instance!; } @@ -4267,6 +4270,165 @@ abstract class HybridWorldEventCopyWith<$R, $In extends HybridWorldEvent, $Out> ); } +class CellSwitchedMapper extends SubClassMapperBase { + CellSwitchedMapper._(); + + static CellSwitchedMapper? _instance; + static CellSwitchedMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = CellSwitchedMapper._()); + HybridWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + VectorDefinitionMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'CellSwitched'; + + static VectorDefinition? _$cell(CellSwitched v) => v.cell; + static const Field _f$cell = Field( + 'cell', + _$cell, + ); + static bool _$selected(CellSwitched v) => v.selected; + static const Field _f$selected = Field( + 'selected', + _$selected, + opt: true, + def: true, + ); + static bool _$teleport(CellSwitched v) => v.teleport; + static const Field _f$teleport = Field( + 'teleport', + _$teleport, + opt: true, + def: false, + ); + + @override + final MappableFields fields = const { + #cell: _f$cell, + #selected: _f$selected, + #teleport: _f$teleport, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'CellSwitched'; + @override + late final ClassMapperBase superMapper = + HybridWorldEventMapper.ensureInitialized(); + + static CellSwitched _instantiate(DecodingData data) { + return CellSwitched( + data.dec(_f$cell), + selected: data.dec(_f$selected), + teleport: data.dec(_f$teleport), + ); + } + + @override + final Function instantiate = _instantiate; + + static CellSwitched fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static CellSwitched fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin CellSwitchedMappable { + String toJson() { + return CellSwitchedMapper.ensureInitialized().encodeJson( + this as CellSwitched, + ); + } + + Map toMap() { + return CellSwitchedMapper.ensureInitialized().encodeMap( + this as CellSwitched, + ); + } + + CellSwitchedCopyWith get copyWith => + _CellSwitchedCopyWithImpl( + this as CellSwitched, + $identity, + $identity, + ); + @override + String toString() { + return CellSwitchedMapper.ensureInitialized().stringifyValue( + this as CellSwitched, + ); + } + + @override + bool operator ==(Object other) { + return CellSwitchedMapper.ensureInitialized().equalsValue( + this as CellSwitched, + other, + ); + } + + @override + int get hashCode { + return CellSwitchedMapper.ensureInitialized().hashValue( + this as CellSwitched, + ); + } +} + +extension CellSwitchedValueCopy<$R, $Out> + on ObjectCopyWith<$R, CellSwitched, $Out> { + CellSwitchedCopyWith<$R, CellSwitched, $Out> get $asCellSwitched => + $base.as((v, t, t2) => _CellSwitchedCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class CellSwitchedCopyWith<$R, $In extends CellSwitched, $Out> + implements HybridWorldEventCopyWith<$R, $In, $Out> { + VectorDefinitionCopyWith<$R, VectorDefinition, VectorDefinition>? get cell; + @override + $R call({VectorDefinition? cell, bool? selected, bool? teleport}); + CellSwitchedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class _CellSwitchedCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, CellSwitched, $Out> + implements CellSwitchedCopyWith<$R, CellSwitched, $Out> { + _CellSwitchedCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + CellSwitchedMapper.ensureInitialized(); + @override + VectorDefinitionCopyWith<$R, VectorDefinition, VectorDefinition>? get cell => + $value.cell?.copyWith.$chain((v) => call(cell: v)); + @override + $R call({Object? cell = $none, bool? selected, bool? teleport}) => $apply( + FieldCopyWithData({ + if (cell != $none) #cell: cell, + if (selected != null) #selected: selected, + if (teleport != null) #teleport: teleport, + }), + ); + @override + CellSwitched $make(CopyWithData data) => CellSwitched( + data.get(#cell, or: $value.cell), + selected: data.get(#selected, or: $value.selected), + teleport: data.get(#teleport, or: $value.teleport), + ); + + @override + CellSwitchedCopyWith<$R2, CellSwitched, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ) => _CellSwitchedCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + class BackgroundChangedMapper extends SubClassMapperBase { BackgroundChangedMapper._(); @@ -6163,6 +6325,304 @@ class _NoteRemovedCopyWithImpl<$R, $Out> ) => _NoteRemovedCopyWithImpl<$R2, $Out2>($value, $cast, t); } +class WaypointChangedMapper extends SubClassMapperBase { + WaypointChangedMapper._(); + + static WaypointChangedMapper? _instance; + static WaypointChangedMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = WaypointChangedMapper._()); + HybridWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + WaypointMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'WaypointChanged'; + + static String? _$team(WaypointChanged v) => v.team; + static const Field _f$team = Field( + 'team', + _$team, + opt: true, + ); + static Waypoint _$waypoint(WaypointChanged v) => v.waypoint; + static const Field _f$waypoint = Field( + 'waypoint', + _$waypoint, + ); + static String? _$name(WaypointChanged v) => v.name; + static const Field _f$name = Field( + 'name', + _$name, + opt: true, + ); + + @override + final MappableFields fields = const { + #team: _f$team, + #waypoint: _f$waypoint, + #name: _f$name, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'WaypointChanged'; + @override + late final ClassMapperBase superMapper = + HybridWorldEventMapper.ensureInitialized(); + + static WaypointChanged _instantiate(DecodingData data) { + return WaypointChanged( + team: data.dec(_f$team), + waypoint: data.dec(_f$waypoint), + name: data.dec(_f$name), + ); + } + + @override + final Function instantiate = _instantiate; + + static WaypointChanged fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static WaypointChanged fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin WaypointChangedMappable { + String toJson() { + return WaypointChangedMapper.ensureInitialized() + .encodeJson(this as WaypointChanged); + } + + Map toMap() { + return WaypointChangedMapper.ensureInitialized().encodeMap( + this as WaypointChanged, + ); + } + + WaypointChangedCopyWith + get copyWith => + _WaypointChangedCopyWithImpl( + this as WaypointChanged, + $identity, + $identity, + ); + @override + String toString() { + return WaypointChangedMapper.ensureInitialized().stringifyValue( + this as WaypointChanged, + ); + } + + @override + bool operator ==(Object other) { + return WaypointChangedMapper.ensureInitialized().equalsValue( + this as WaypointChanged, + other, + ); + } + + @override + int get hashCode { + return WaypointChangedMapper.ensureInitialized().hashValue( + this as WaypointChanged, + ); + } +} + +extension WaypointChangedValueCopy<$R, $Out> + on ObjectCopyWith<$R, WaypointChanged, $Out> { + WaypointChangedCopyWith<$R, WaypointChanged, $Out> get $asWaypointChanged => + $base.as((v, t, t2) => _WaypointChangedCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class WaypointChangedCopyWith<$R, $In extends WaypointChanged, $Out> + implements HybridWorldEventCopyWith<$R, $In, $Out> { + WaypointCopyWith<$R, Waypoint, Waypoint> get waypoint; + @override + $R call({String? team, Waypoint? waypoint, String? name}); + WaypointChangedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + +class _WaypointChangedCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, WaypointChanged, $Out> + implements WaypointChangedCopyWith<$R, WaypointChanged, $Out> { + _WaypointChangedCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + WaypointChangedMapper.ensureInitialized(); + @override + WaypointCopyWith<$R, Waypoint, Waypoint> get waypoint => + $value.waypoint.copyWith.$chain((v) => call(waypoint: v)); + @override + $R call({Object? team = $none, Waypoint? waypoint, Object? name = $none}) => + $apply( + FieldCopyWithData({ + if (team != $none) #team: team, + if (waypoint != null) #waypoint: waypoint, + if (name != $none) #name: name, + }), + ); + @override + WaypointChanged $make(CopyWithData data) => WaypointChanged( + team: data.get(#team, or: $value.team), + waypoint: data.get(#waypoint, or: $value.waypoint), + name: data.get(#name, or: $value.name), + ); + + @override + WaypointChangedCopyWith<$R2, WaypointChanged, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ) => _WaypointChangedCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + +class WaypointRemovedMapper extends SubClassMapperBase { + WaypointRemovedMapper._(); + + static WaypointRemovedMapper? _instance; + static WaypointRemovedMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = WaypointRemovedMapper._()); + HybridWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + } + return _instance!; + } + + @override + final String id = 'WaypointRemoved'; + + static String? _$team(WaypointRemoved v) => v.team; + static const Field _f$team = Field( + 'team', + _$team, + opt: true, + ); + static String _$name(WaypointRemoved v) => v.name; + static const Field _f$name = Field('name', _$name); + + @override + final MappableFields fields = const { + #team: _f$team, + #name: _f$name, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'WaypointRemoved'; + @override + late final ClassMapperBase superMapper = + HybridWorldEventMapper.ensureInitialized(); + + static WaypointRemoved _instantiate(DecodingData data) { + return WaypointRemoved(team: data.dec(_f$team), name: data.dec(_f$name)); + } + + @override + final Function instantiate = _instantiate; + + static WaypointRemoved fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static WaypointRemoved fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin WaypointRemovedMappable { + String toJson() { + return WaypointRemovedMapper.ensureInitialized() + .encodeJson(this as WaypointRemoved); + } + + Map toMap() { + return WaypointRemovedMapper.ensureInitialized().encodeMap( + this as WaypointRemoved, + ); + } + + WaypointRemovedCopyWith + get copyWith => + _WaypointRemovedCopyWithImpl( + this as WaypointRemoved, + $identity, + $identity, + ); + @override + String toString() { + return WaypointRemovedMapper.ensureInitialized().stringifyValue( + this as WaypointRemoved, + ); + } + + @override + bool operator ==(Object other) { + return WaypointRemovedMapper.ensureInitialized().equalsValue( + this as WaypointRemoved, + other, + ); + } + + @override + int get hashCode { + return WaypointRemovedMapper.ensureInitialized().hashValue( + this as WaypointRemoved, + ); + } +} + +extension WaypointRemovedValueCopy<$R, $Out> + on ObjectCopyWith<$R, WaypointRemoved, $Out> { + WaypointRemovedCopyWith<$R, WaypointRemoved, $Out> get $asWaypointRemoved => + $base.as((v, t, t2) => _WaypointRemovedCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class WaypointRemovedCopyWith<$R, $In extends WaypointRemoved, $Out> + implements HybridWorldEventCopyWith<$R, $In, $Out> { + @override + $R call({String? team, String? name}); + WaypointRemovedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + +class _WaypointRemovedCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, WaypointRemoved, $Out> + implements WaypointRemovedCopyWith<$R, WaypointRemoved, $Out> { + _WaypointRemovedCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + WaypointRemovedMapper.ensureInitialized(); + @override + $R call({Object? team = $none, String? name}) => $apply( + FieldCopyWithData({ + if (team != $none) #team: team, + if (name != null) #name: name, + }), + ); + @override + WaypointRemoved $make(CopyWithData data) => WaypointRemoved( + team: data.get(#team, or: $value.team), + name: data.get(#name, or: $value.name), + ); + + @override + WaypointRemovedCopyWith<$R2, WaypointRemoved, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ) => _WaypointRemovedCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + class LocalWorldEventMapper extends SubClassMapperBase { LocalWorldEventMapper._(); diff --git a/api/lib/src/event/hybrid.dart b/api/lib/src/event/hybrid.dart index 883c7948..591ede6d 100644 --- a/api/lib/src/event/hybrid.dart +++ b/api/lib/src/event/hybrid.dart @@ -5,6 +5,14 @@ sealed class HybridWorldEvent extends WorldEvent with HybridWorldEventMappable implements ClientWorldEvent, ServerWorldEvent {} +@MappableClass() +final class CellSwitched extends HybridWorldEvent with CellSwitchedMappable { + final VectorDefinition? cell; + final bool selected, teleport; + + CellSwitched(this.cell, {this.selected = true, this.teleport = false}); +} + @MappableClass() final class BackgroundChanged extends HybridWorldEvent with BackgroundChangedMappable { @@ -224,3 +232,22 @@ final class NoteRemoved extends HybridWorldEvent with NoteRemovedMappable { NoteRemoved(this.name); } + +@MappableClass() +final class WaypointChanged extends HybridWorldEvent + with WaypointChangedMappable { + final String? team; + final Waypoint waypoint; + final String? name; + + WaypointChanged({this.team, required this.waypoint, this.name}); +} + +@MappableClass() +final class WaypointRemoved extends HybridWorldEvent + with WaypointRemovedMappable { + final String? team; + final String name; + + WaypointRemoved({this.team, required this.name}); +} diff --git a/api/lib/src/event/process/client.dart b/api/lib/src/event/process/client.dart index d89433a5..b86bcf35 100644 --- a/api/lib/src/event/process/client.dart +++ b/api/lib/src/event/process/client.dart @@ -362,9 +362,7 @@ Future processClientEvent( ); case ModeChangeRequest(): final location = event.location; - final mode = location == null - ? null - : assetManager.getPack(location.namespace)?.getMode(location.id); + final mode = location == null ? null : assetManager.getModeItem(location); return UpdateServerResponse.builder( WorldInitialized.fromMode(mode, state), channel, diff --git a/api/lib/src/event/process/server.dart b/api/lib/src/event/process/server.dart index d6bc8ab8..3a1455d6 100644 --- a/api/lib/src/event/process/server.dart +++ b/api/lib/src/event/process/server.dart @@ -346,6 +346,21 @@ ServerProcessed processServerEvent( state.copyWith( tableName: state.tableName == event.name ? '' : state.tableName, data: state.data.removeTable(event.name), + info: state.info.copyWith( + teams: state.info.teams.map( + (k, v) => MapEntry( + k, + v.copyWith( + claimedCells: v.claimedCells + .where((e) => e.table != event.name) + .toSet(), + waypoints: v.waypoints + .where((e) => e.position.table != event.name) + .toList(), + ), + ), + ), + ), ), ); case NoteChanged(): @@ -418,5 +433,52 @@ ServerProcessed processServerEvent( return ServerProcessed(state.copyWith(serverState: event.state)); case AuthenticatedRequested(): return ServerProcessed(state.copyWith(authRequest: event)); + case WaypointChanged(): + var info = state.info; + final team = event.team; + final waypoints = List.from( + team == null ? info.waypoints : (info.teams[team]?.waypoints ?? []), + ); + final index = waypoints.indexWhere( + (e) => e.name == (event.name ?? event.waypoint.name), + ); + if (index != -1) { + waypoints[index] = event.waypoint; + } else { + waypoints.add(event.waypoint); + } + if (team == null) { + info = info.copyWith(waypoints: waypoints); + } else { + final gameTeam = info.teams[event.team]; + if (gameTeam != null) { + info = info.copyWith.teams.put( + team, + gameTeam.copyWith(waypoints: waypoints), + ); + } + } + return ServerProcessed(state.copyWith(info: info)); + case WaypointRemoved(): + var info = state.info; + final team = event.team; + final waypoints = team == null + ? List.from(info.waypoints) + : List.from(info.teams[team]?.waypoints ?? []); + waypoints.removeWhere((e) => e.name == event.name); + if (team == null) { + info = info.copyWith(waypoints: waypoints); + } else { + final gameTeam = info.teams[event.team]; + if (gameTeam != null) { + info = info.copyWith.teams.put( + team, + gameTeam.copyWith(waypoints: waypoints), + ); + } + } + return ServerProcessed(state.copyWith(info: info)); + case CellSwitched(): + return ServerProcessed(null); } } diff --git a/api/lib/src/event/server.dart b/api/lib/src/event/server.dart index 2e1dc806..6252602a 100644 --- a/api/lib/src/event/server.dart +++ b/api/lib/src/event/server.dart @@ -27,16 +27,18 @@ final class WorldInitialized extends ServerWorldEvent this.clearUserInterface = false, }); - factory WorldInitialized.fromMode(GameMode? mode, WorldState state) => - WorldInitialized( - clearUserInterface: true, - info: state.info.copyWith( - teams: mode?.teams ?? {}, - script: mode?.script, - ), - table: mode?.tables[state.tableName] ?? GameTable(), - teamMembers: const {}, - ); + factory WorldInitialized.fromMode( + PackItem? mode, + WorldState state, + ) => WorldInitialized( + clearUserInterface: true, + info: state.info.copyWith( + teams: mode?.item.teams ?? {}, + gameMode: mode?.location, + ), + table: mode?.item.tables[state.tableName] ?? GameTable(), + teamMembers: const {}, + ); } @MappableClass() diff --git a/api/lib/src/models/config.dart b/api/lib/src/models/config.dart index b792f710..bc5c5fea 100644 --- a/api/lib/src/models/config.dart +++ b/api/lib/src/models/config.dart @@ -41,6 +41,9 @@ final class SetonixConfig with SetonixConfigMappable { final String? endpointSecret; static const String defaultEndpointSecret = ''; static const String envEndpointSecret = 'SETONIX_ENDPOINT_SECRET'; + final String? gameMode; + static const String defaultGameMode = ''; + static const String envGameMode = 'SETONIX_GAME_MODE'; const SetonixConfig({ this.host, @@ -55,6 +58,7 @@ final class SetonixConfig with SetonixConfigMappable { this.accountRequired, this.apiEndpoint, this.endpointSecret, + this.gameMode, }); static const defaultConfig = SetonixConfig( @@ -70,6 +74,7 @@ final class SetonixConfig with SetonixConfigMappable { accountRequired: defaultAccountRequired, apiEndpoint: defaultApiEndpoint, endpointSecret: defaultEndpointSecret, + gameMode: defaultGameMode, ); static SetonixConfig fromEnvironment() { @@ -128,6 +133,9 @@ final class SetonixConfig with SetonixConfigMappable { defaultValue: defaultEndpointSecret, ) : null, + gameMode: bool.hasEnvironment(envGameMode) + ? String.fromEnvironment(envGameMode, defaultValue: defaultGameMode) + : null, ); } @@ -144,5 +152,6 @@ final class SetonixConfig with SetonixConfigMappable { whitelistEnabled: other.whitelistEnabled ?? whitelistEnabled, apiEndpoint: other.apiEndpoint ?? apiEndpoint, endpointSecret: other.endpointSecret ?? endpointSecret, + gameMode: other.gameMode ?? gameMode, ); } diff --git a/api/lib/src/models/config.mapper.dart b/api/lib/src/models/config.mapper.dart index 23b21fc7..6e4d12c6 100644 --- a/api/lib/src/models/config.mapper.dart +++ b/api/lib/src/models/config.mapper.dart @@ -93,6 +93,12 @@ class SetonixConfigMapper extends ClassMapperBase { _$endpointSecret, opt: true, ); + static String? _$gameMode(SetonixConfig v) => v.gameMode; + static const Field _f$gameMode = Field( + 'gameMode', + _$gameMode, + opt: true, + ); @override final MappableFields fields = const { @@ -108,6 +114,7 @@ class SetonixConfigMapper extends ClassMapperBase { #accountRequired: _f$accountRequired, #apiEndpoint: _f$apiEndpoint, #endpointSecret: _f$endpointSecret, + #gameMode: _f$gameMode, }; static SetonixConfig _instantiate(DecodingData data) { @@ -124,6 +131,7 @@ class SetonixConfigMapper extends ClassMapperBase { accountRequired: data.dec(_f$accountRequired), apiEndpoint: data.dec(_f$apiEndpoint), endpointSecret: data.dec(_f$endpointSecret), + gameMode: data.dec(_f$gameMode), ); } @@ -202,6 +210,7 @@ abstract class SetonixConfigCopyWith<$R, $In extends SetonixConfig, $Out> bool? accountRequired, String? apiEndpoint, String? endpointSecret, + String? gameMode, }); SetonixConfigCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); } @@ -228,6 +237,7 @@ class _SetonixConfigCopyWithImpl<$R, $Out> Object? accountRequired = $none, Object? apiEndpoint = $none, Object? endpointSecret = $none, + Object? gameMode = $none, }) => $apply( FieldCopyWithData({ if (host != $none) #host: host, @@ -242,6 +252,7 @@ class _SetonixConfigCopyWithImpl<$R, $Out> if (accountRequired != $none) #accountRequired: accountRequired, if (apiEndpoint != $none) #apiEndpoint: apiEndpoint, if (endpointSecret != $none) #endpointSecret: endpointSecret, + if (gameMode != $none) #gameMode: gameMode, }), ); @override @@ -258,6 +269,7 @@ class _SetonixConfigCopyWithImpl<$R, $Out> accountRequired: data.get(#accountRequired, or: $value.accountRequired), apiEndpoint: data.get(#apiEndpoint, or: $value.apiEndpoint), endpointSecret: data.get(#endpointSecret, or: $value.endpointSecret), + gameMode: data.get(#gameMode, or: $value.gameMode), ); @override diff --git a/api/lib/src/models/data.dart b/api/lib/src/models/data.dart index ca093ab9..07a3067e 100644 --- a/api/lib/src/models/data.dart +++ b/api/lib/src/models/data.dart @@ -38,6 +38,24 @@ class SetonixData extends ArchiveData { : identifier = identifier ?? createPackIdentifier(data), super.fromBytes(); + factory SetonixData.fromMode( + PackItem? mode, { + Set packs = const {}, + }) { + if (mode == null) return SetonixData.empty(); + var data = SetonixData.empty().setInfo( + GameInfo( + packs: {...packs, mode.namespace}.toList(), + gameMode: mode.location, + teams: mode.item.teams, + ), + ); + for (final entry in mode.item.tables.entries) { + data = data.setTable(entry.value, entry.key); + } + return data; + } + GameTable? getTable([String name = '']) { final data = getAsset('$kGameTablePath/$name.json'); if (data == null) return null; @@ -305,6 +323,14 @@ class SetonixData extends ArchiveData { } } + PackItem? getModeItem(String id, [String namespace = '']) => + PackItem.wrap( + pack: this, + namespace: namespace, + id: id, + item: getMode(id), + ); + Map getModesData() => Map.fromEntries( getModes().map((e) { final mode = getMode(e); @@ -389,6 +415,6 @@ final class PackItem { String get namespace => location.namespace; String get id => location.id; - PackItem withItem(E backgroundTranslation) => - PackItem(pack: pack, location: location, item: backgroundTranslation); + PackItem withItem(E item) => + PackItem(pack: pack, location: location, item: item); } diff --git a/api/lib/src/models/info.dart b/api/lib/src/models/info.dart index c503aa9b..da318e67 100644 --- a/api/lib/src/models/info.dart +++ b/api/lib/src/models/info.dart @@ -1,6 +1,7 @@ import 'package:dart_mappable/dart_mappable.dart'; import 'table.dart'; +import 'waypoint.dart'; part 'info.mapper.dart'; @@ -8,9 +9,15 @@ part 'info.mapper.dart'; class GameInfo with GameInfoMappable { final Map teams; final List packs; - final String? script; + final ItemLocation? gameMode; + final List waypoints; - const GameInfo({this.teams = const {}, this.packs = const [], this.script}); + const GameInfo({ + this.teams = const {}, + this.packs = const [], + this.gameMode, + this.waypoints = const [], + }); } @MappableEnum() @@ -33,6 +40,12 @@ class GameTeam with GameTeamMappable { final String description; final TeamColor? color; final Set claimedCells; + final List waypoints; - GameTeam({this.description = '', this.color, this.claimedCells = const {}}); + GameTeam({ + this.description = '', + this.color, + this.claimedCells = const {}, + this.waypoints = const [], + }); } diff --git a/api/lib/src/models/info.mapper.dart b/api/lib/src/models/info.mapper.dart index a02c5e12..12f56c75 100644 --- a/api/lib/src/models/info.mapper.dart +++ b/api/lib/src/models/info.mapper.dart @@ -97,6 +97,8 @@ class GameInfoMapper extends ClassMapperBase { if (_instance == null) { MapperContainer.globals.use(_instance = GameInfoMapper._()); GameTeamMapper.ensureInitialized(); + ItemLocationMapper.ensureInitialized(); + WaypointMapper.ensureInitialized(); } return _instance!; } @@ -118,25 +120,34 @@ class GameInfoMapper extends ClassMapperBase { opt: true, def: const [], ); - static String? _$script(GameInfo v) => v.script; - static const Field _f$script = Field( - 'script', - _$script, + static ItemLocation? _$gameMode(GameInfo v) => v.gameMode; + static const Field _f$gameMode = Field( + 'gameMode', + _$gameMode, opt: true, ); + static List _$waypoints(GameInfo v) => v.waypoints; + static const Field> _f$waypoints = Field( + 'waypoints', + _$waypoints, + opt: true, + def: const [], + ); @override final MappableFields fields = const { #teams: _f$teams, #packs: _f$packs, - #script: _f$script, + #gameMode: _f$gameMode, + #waypoints: _f$waypoints, }; static GameInfo _instantiate(DecodingData data) { return GameInfo( teams: data.dec(_f$teams), packs: data.dec(_f$packs), - script: data.dec(_f$script), + gameMode: data.dec(_f$gameMode), + waypoints: data.dec(_f$waypoints), ); } @@ -200,7 +211,15 @@ abstract class GameInfoCopyWith<$R, $In extends GameInfo, $Out> MapCopyWith<$R, String, GameTeam, GameTeamCopyWith<$R, GameTeam, GameTeam>> get teams; ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get packs; - $R call({Map? teams, List? packs, String? script}); + ItemLocationCopyWith<$R, ItemLocation, ItemLocation>? get gameMode; + ListCopyWith<$R, Waypoint, WaypointCopyWith<$R, Waypoint, Waypoint>> + get waypoints; + $R call({ + Map? teams, + List? packs, + ItemLocation? gameMode, + List? waypoints, + }); GameInfoCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); } @@ -227,22 +246,35 @@ class _GameInfoCopyWithImpl<$R, $Out> (v) => call(packs: v), ); @override + ItemLocationCopyWith<$R, ItemLocation, ItemLocation>? get gameMode => + $value.gameMode?.copyWith.$chain((v) => call(gameMode: v)); + @override + ListCopyWith<$R, Waypoint, WaypointCopyWith<$R, Waypoint, Waypoint>> + get waypoints => ListCopyWith( + $value.waypoints, + (v, t) => v.copyWith.$chain(t), + (v) => call(waypoints: v), + ); + @override $R call({ Map? teams, List? packs, - Object? script = $none, + Object? gameMode = $none, + List? waypoints, }) => $apply( FieldCopyWithData({ if (teams != null) #teams: teams, if (packs != null) #packs: packs, - if (script != $none) #script: script, + if (gameMode != $none) #gameMode: gameMode, + if (waypoints != null) #waypoints: waypoints, }), ); @override GameInfo $make(CopyWithData data) => GameInfo( teams: data.get(#teams, or: $value.teams), packs: data.get(#packs, or: $value.packs), - script: data.get(#script, or: $value.script), + gameMode: data.get(#gameMode, or: $value.gameMode), + waypoints: data.get(#waypoints, or: $value.waypoints), ); @override @@ -260,6 +292,7 @@ class GameTeamMapper extends ClassMapperBase { MapperContainer.globals.use(_instance = GameTeamMapper._()); TeamColorMapper.ensureInitialized(); GlobalVectorDefinitionMapper.ensureInitialized(); + WaypointMapper.ensureInitialized(); } return _instance!; } @@ -284,12 +317,20 @@ class GameTeamMapper extends ClassMapperBase { v.claimedCells; static const Field> _f$claimedCells = Field('claimedCells', _$claimedCells, opt: true, def: const {}); + static List _$waypoints(GameTeam v) => v.waypoints; + static const Field> _f$waypoints = Field( + 'waypoints', + _$waypoints, + opt: true, + def: const [], + ); @override final MappableFields fields = const { #description: _f$description, #color: _f$color, #claimedCells: _f$claimedCells, + #waypoints: _f$waypoints, }; static GameTeam _instantiate(DecodingData data) { @@ -297,6 +338,7 @@ class GameTeamMapper extends ClassMapperBase { description: data.dec(_f$description), color: data.dec(_f$color), claimedCells: data.dec(_f$claimedCells), + waypoints: data.dec(_f$waypoints), ); } @@ -357,10 +399,13 @@ extension GameTeamValueCopy<$R, $Out> on ObjectCopyWith<$R, GameTeam, $Out> { abstract class GameTeamCopyWith<$R, $In extends GameTeam, $Out> implements ClassCopyWith<$R, $In, $Out> { + ListCopyWith<$R, Waypoint, WaypointCopyWith<$R, Waypoint, Waypoint>> + get waypoints; $R call({ String? description, TeamColor? color, Set? claimedCells, + List? waypoints, }); GameTeamCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); } @@ -374,15 +419,24 @@ class _GameTeamCopyWithImpl<$R, $Out> late final ClassMapperBase $mapper = GameTeamMapper.ensureInitialized(); @override + ListCopyWith<$R, Waypoint, WaypointCopyWith<$R, Waypoint, Waypoint>> + get waypoints => ListCopyWith( + $value.waypoints, + (v, t) => v.copyWith.$chain(t), + (v) => call(waypoints: v), + ); + @override $R call({ String? description, Object? color = $none, Set? claimedCells, + List? waypoints, }) => $apply( FieldCopyWithData({ if (description != null) #description: description, if (color != $none) #color: color, if (claimedCells != null) #claimedCells: claimedCells, + if (waypoints != null) #waypoints: waypoints, }), ); @override @@ -390,6 +444,7 @@ class _GameTeamCopyWithImpl<$R, $Out> description: data.get(#description, or: $value.description), color: data.get(#color, or: $value.color), claimedCells: data.get(#claimedCells, or: $value.claimedCells), + waypoints: data.get(#waypoints, or: $value.waypoints), ); @override diff --git a/api/lib/src/models/kick.dart b/api/lib/src/models/kick.dart index a7c49a4b..7ee30ea6 100644 --- a/api/lib/src/models/kick.dart +++ b/api/lib/src/models/kick.dart @@ -2,6 +2,7 @@ import 'package:dart_mappable/dart_mappable.dart'; part 'kick.mapper.dart'; +@MappableEnum() enum KickReason { kick, ban, diff --git a/api/lib/src/models/kick.mapper.dart b/api/lib/src/models/kick.mapper.dart index fdba3663..c25d4f1c 100644 --- a/api/lib/src/models/kick.mapper.dart +++ b/api/lib/src/models/kick.mapper.dart @@ -7,6 +7,68 @@ part of 'kick.dart'; +class KickReasonMapper extends EnumMapper { + KickReasonMapper._(); + + static KickReasonMapper? _instance; + static KickReasonMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = KickReasonMapper._()); + } + return _instance!; + } + + static KickReason fromValue(dynamic value) { + ensureInitialized(); + return MapperContainer.globals.fromValue(value); + } + + @override + KickReason decode(dynamic value) { + switch (value) { + case r'kick': + return KickReason.kick; + case r'ban': + return KickReason.ban; + case r'notWhitelisted': + return KickReason.notWhitelisted; + case r'notRegistered': + return KickReason.notRegistered; + case r'challengeFailed': + return KickReason.challengeFailed; + case r'pleaseLink': + return KickReason.pleaseLink; + default: + throw MapperException.unknownEnumValue(value); + } + } + + @override + dynamic encode(KickReason self) { + switch (self) { + case KickReason.kick: + return r'kick'; + case KickReason.ban: + return r'ban'; + case KickReason.notWhitelisted: + return r'notWhitelisted'; + case KickReason.notRegistered: + return r'notRegistered'; + case KickReason.challengeFailed: + return r'challengeFailed'; + case KickReason.pleaseLink: + return r'pleaseLink'; + } + } +} + +extension KickReasonMapperExtension on KickReason { + String toValue() { + KickReasonMapper.ensureInitialized(); + return MapperContainer.globals.toValue(this) as String; + } +} + class KickMessageMapper extends ClassMapperBase { KickMessageMapper._(); @@ -14,6 +76,7 @@ class KickMessageMapper extends ClassMapperBase { static KickMessageMapper ensureInitialized() { if (_instance == null) { MapperContainer.globals.use(_instance = KickMessageMapper._()); + KickReasonMapper.ensureInitialized(); } return _instance!; } diff --git a/api/lib/src/models/mode.dart b/api/lib/src/models/mode.dart index fec61785..d84c75d4 100644 --- a/api/lib/src/models/mode.dart +++ b/api/lib/src/models/mode.dart @@ -10,13 +10,11 @@ final class GameMode with GameModeMappable { final String? script; final Map tables; - final String tableName; final Map teams; GameMode({ required this.script, this.tables = const {}, - this.tableName = '', this.teams = const {}, }); } diff --git a/api/lib/src/models/mode.mapper.dart b/api/lib/src/models/mode.mapper.dart index 4aaed89a..e6af0e9a 100644 --- a/api/lib/src/models/mode.mapper.dart +++ b/api/lib/src/models/mode.mapper.dart @@ -32,13 +32,6 @@ class GameModeMapper extends ClassMapperBase { opt: true, def: const {}, ); - static String _$tableName(GameMode v) => v.tableName; - static const Field _f$tableName = Field( - 'tableName', - _$tableName, - opt: true, - def: '', - ); static Map _$teams(GameMode v) => v.teams; static const Field> _f$teams = Field( 'teams', @@ -51,7 +44,6 @@ class GameModeMapper extends ClassMapperBase { final MappableFields fields = const { #script: _f$script, #tables: _f$tables, - #tableName: _f$tableName, #teams: _f$teams, }; @@ -59,7 +51,6 @@ class GameModeMapper extends ClassMapperBase { return GameMode( script: data.dec(_f$script), tables: data.dec(_f$tables), - tableName: data.dec(_f$tableName), teams: data.dec(_f$teams), ); } @@ -133,7 +124,6 @@ abstract class GameModeCopyWith<$R, $In extends GameMode, $Out> $R call({ String? script, Map? tables, - String? tableName, Map? teams, }); GameModeCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); @@ -170,13 +160,11 @@ class _GameModeCopyWithImpl<$R, $Out> $R call({ Object? script = $none, Map? tables, - String? tableName, Map? teams, }) => $apply( FieldCopyWithData({ if (script != $none) #script: script, if (tables != null) #tables: tables, - if (tableName != null) #tableName: tableName, if (teams != null) #teams: teams, }), ); @@ -184,7 +172,6 @@ class _GameModeCopyWithImpl<$R, $Out> GameMode $make(CopyWithData data) => GameMode( script: data.get(#script, or: $value.script), tables: data.get(#tables, or: $value.tables), - tableName: data.get(#tableName, or: $value.tableName), teams: data.get(#teams, or: $value.teams), ); diff --git a/api/lib/src/models/table.dart b/api/lib/src/models/table.dart index 0fd71dce..827ca9b2 100644 --- a/api/lib/src/models/table.dart +++ b/api/lib/src/models/table.dart @@ -82,13 +82,6 @@ class GlobalVectorDefinition with GlobalVectorDefinitionMappable { int get y => position.y; } -@MappableClass() -class GameSeat with GameSeatMappable { - final int? color; - - GameSeat({this.color}); -} - @MappableClass() class TableCell with TableCellMappable { final List objects; @@ -116,7 +109,7 @@ class BoardTile with BoardTileMappable { BoardTile(this.asset, this.tile); } -@MappableClass() +@MappableClass(hook: ItemLocationHook()) class ItemLocation with ItemLocationMappable { final String namespace, id; @@ -130,6 +123,32 @@ class ItemLocation with ItemLocationMappable { return ItemLocation(splitted[0], splitted[1]); } + bool get isEmpty => namespace.isEmpty && id.isEmpty; + @override String toString() => namespace.isEmpty ? id : '$namespace:$id'; } + +class ItemLocationHook extends MappingHook { + final bool nullOnEmpty; + + const ItemLocationHook({this.nullOnEmpty = true}); + + @override + Object? beforeDecode(Object? value) { + if (value is String) { + return ItemLocation.fromString(value).toMap(); + } + return value; + } + + @override + Object? afterEncode(Object? value) { + if (value is ItemLocation) { + if (value.isEmpty && nullOnEmpty) { + return null; + } + } + return value; + } +} diff --git a/api/lib/src/models/table.mapper.dart b/api/lib/src/models/table.mapper.dart index 6932e720..125d7ecf 100644 --- a/api/lib/src/models/table.mapper.dart +++ b/api/lib/src/models/table.mapper.dart @@ -610,6 +610,8 @@ class ItemLocationMapper extends ClassMapperBase { #id: _f$id, }; + @override + final MappingHook hook = const ItemLocationHook(); static ItemLocation _instantiate(DecodingData data) { return ItemLocation(data.dec(_f$namespace), data.dec(_f$id)); } @@ -992,113 +994,3 @@ class _GlobalVectorDefinitionCopyWithImpl<$R, $Out> _GlobalVectorDefinitionCopyWithImpl<$R2, $Out2>($value, $cast, t); } -class GameSeatMapper extends ClassMapperBase { - GameSeatMapper._(); - - static GameSeatMapper? _instance; - static GameSeatMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = GameSeatMapper._()); - } - return _instance!; - } - - @override - final String id = 'GameSeat'; - - static int? _$color(GameSeat v) => v.color; - static const Field _f$color = Field( - 'color', - _$color, - opt: true, - ); - - @override - final MappableFields fields = const {#color: _f$color}; - - static GameSeat _instantiate(DecodingData data) { - return GameSeat(color: data.dec(_f$color)); - } - - @override - final Function instantiate = _instantiate; - - static GameSeat fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static GameSeat fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin GameSeatMappable { - String toJson() { - return GameSeatMapper.ensureInitialized().encodeJson( - this as GameSeat, - ); - } - - Map toMap() { - return GameSeatMapper.ensureInitialized().encodeMap( - this as GameSeat, - ); - } - - GameSeatCopyWith get copyWith => - _GameSeatCopyWithImpl( - this as GameSeat, - $identity, - $identity, - ); - @override - String toString() { - return GameSeatMapper.ensureInitialized().stringifyValue(this as GameSeat); - } - - @override - bool operator ==(Object other) { - return GameSeatMapper.ensureInitialized().equalsValue( - this as GameSeat, - other, - ); - } - - @override - int get hashCode { - return GameSeatMapper.ensureInitialized().hashValue(this as GameSeat); - } -} - -extension GameSeatValueCopy<$R, $Out> on ObjectCopyWith<$R, GameSeat, $Out> { - GameSeatCopyWith<$R, GameSeat, $Out> get $asGameSeat => - $base.as((v, t, t2) => _GameSeatCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class GameSeatCopyWith<$R, $In extends GameSeat, $Out> - implements ClassCopyWith<$R, $In, $Out> { - $R call({int? color}); - GameSeatCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _GameSeatCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, GameSeat, $Out> - implements GameSeatCopyWith<$R, GameSeat, $Out> { - _GameSeatCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - GameSeatMapper.ensureInitialized(); - @override - $R call({Object? color = $none}) => - $apply(FieldCopyWithData({if (color != $none) #color: color})); - @override - GameSeat $make(CopyWithData data) => - GameSeat(color: data.get(#color, or: $value.color)); - - @override - GameSeatCopyWith<$R2, GameSeat, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _GameSeatCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - diff --git a/api/lib/src/models/waypoint.dart b/api/lib/src/models/waypoint.dart new file mode 100644 index 00000000..b19920e8 --- /dev/null +++ b/api/lib/src/models/waypoint.dart @@ -0,0 +1,13 @@ +import 'package:dart_mappable/dart_mappable.dart'; + +import 'table.dart'; + +part 'waypoint.mapper.dart'; + +@MappableClass() +final class Waypoint with WaypointMappable { + final String name; + final GlobalVectorDefinition position; + + Waypoint({required this.name, required this.position}); +} diff --git a/api/lib/src/models/waypoint.mapper.dart b/api/lib/src/models/waypoint.mapper.dart new file mode 100644 index 00000000..4de1b350 --- /dev/null +++ b/api/lib/src/models/waypoint.mapper.dart @@ -0,0 +1,143 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format off +// ignore_for_file: type=lint +// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member +// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter + +part of 'waypoint.dart'; + +class WaypointMapper extends ClassMapperBase { + WaypointMapper._(); + + static WaypointMapper? _instance; + static WaypointMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = WaypointMapper._()); + GlobalVectorDefinitionMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'Waypoint'; + + static String _$name(Waypoint v) => v.name; + static const Field _f$name = Field('name', _$name); + static GlobalVectorDefinition _$position(Waypoint v) => v.position; + static const Field _f$position = Field( + 'position', + _$position, + ); + + @override + final MappableFields fields = const { + #name: _f$name, + #position: _f$position, + }; + + static Waypoint _instantiate(DecodingData data) { + return Waypoint(name: data.dec(_f$name), position: data.dec(_f$position)); + } + + @override + final Function instantiate = _instantiate; + + static Waypoint fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static Waypoint fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin WaypointMappable { + String toJson() { + return WaypointMapper.ensureInitialized().encodeJson( + this as Waypoint, + ); + } + + Map toMap() { + return WaypointMapper.ensureInitialized().encodeMap( + this as Waypoint, + ); + } + + WaypointCopyWith get copyWith => + _WaypointCopyWithImpl( + this as Waypoint, + $identity, + $identity, + ); + @override + String toString() { + return WaypointMapper.ensureInitialized().stringifyValue(this as Waypoint); + } + + @override + bool operator ==(Object other) { + return WaypointMapper.ensureInitialized().equalsValue( + this as Waypoint, + other, + ); + } + + @override + int get hashCode { + return WaypointMapper.ensureInitialized().hashValue(this as Waypoint); + } +} + +extension WaypointValueCopy<$R, $Out> on ObjectCopyWith<$R, Waypoint, $Out> { + WaypointCopyWith<$R, Waypoint, $Out> get $asWaypoint => + $base.as((v, t, t2) => _WaypointCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class WaypointCopyWith<$R, $In extends Waypoint, $Out> + implements ClassCopyWith<$R, $In, $Out> { + GlobalVectorDefinitionCopyWith< + $R, + GlobalVectorDefinition, + GlobalVectorDefinition + > + get position; + $R call({String? name, GlobalVectorDefinition? position}); + WaypointCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class _WaypointCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, Waypoint, $Out> + implements WaypointCopyWith<$R, Waypoint, $Out> { + _WaypointCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + WaypointMapper.ensureInitialized(); + @override + GlobalVectorDefinitionCopyWith< + $R, + GlobalVectorDefinition, + GlobalVectorDefinition + > + get position => $value.position.copyWith.$chain((v) => call(position: v)); + @override + $R call({String? name, GlobalVectorDefinition? position}) => $apply( + FieldCopyWithData({ + if (name != null) #name: name, + if (position != null) #position: position, + }), + ); + @override + Waypoint $make(CopyWithData data) => Waypoint( + name: data.get(#name, or: $value.name), + position: data.get(#position, or: $value.position), + ); + + @override + WaypointCopyWith<$R2, Waypoint, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ) => _WaypointCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + diff --git a/api/lib/src/services/asset.dart b/api/lib/src/services/asset.dart index 07bda9aa..1332ab1d 100644 --- a/api/lib/src/services/asset.dart +++ b/api/lib/src/services/asset.dart @@ -53,4 +53,7 @@ abstract class AssetManager { PackItem? getDeckItem(ItemLocation location) => getPack(location.namespace)?.getDeckItem(location.id, location.namespace); + + PackItem? getModeItem(ItemLocation location) => + getPack(location.namespace)?.getModeItem(location.id, location.namespace); } diff --git a/app/lib/bloc/world/bloc.dart b/app/lib/bloc/world/bloc.dart index ab453b51..47d53761 100644 --- a/app/lib/bloc/world/bloc.dart +++ b/app/lib/bloc/world/bloc.dart @@ -36,6 +36,11 @@ class _WorldServerInterfaceImpl implements ServerInterface { bloc._processEvent(NetworkerPacket(event, target)); } + @override + void print(String message, [String? plugin]) { + // TODO: implement better logging + } + @override WorldState get state => bloc.state.world; @@ -82,6 +87,15 @@ class WorldBloc extends Bloc { }) ..serverEvents.listen(_processEvent); + on((event, emit) { + emit( + state.copyWith( + selectedCell: event.selected ? event.cell : state.selectedCell, + selectedDeck: null, + showHand: true, + ), + ); + }); on((event, emit) async { try { final signature = state.assetManager.createSignature(); @@ -116,20 +130,12 @@ class WorldBloc extends Bloc { ), ); }); - on((event, emit) { - emit( - state.copyWith( - selectedCell: event.toggle && state.selectedCell == event.cell - ? null - : event.cell, - selectedDeck: null, - showHand: true, - ), - ); - }); on((event, emit) { emit(state.copyWith(switchCellOnMove: event.value)); }); + on((event, emit) { + emit(state.copyWith(showWaypoints: event.value)); + }); on((event, emit) { emit( state.copyWith.world( @@ -154,7 +160,8 @@ class WorldBloc extends Bloc { ); }); if (!state.multiplayer.isClient) { - _loadScript(state.world.info.script); + final mode = state.world.info.gameMode; + if (mode != null) _loadGameMode(mode); } } @@ -222,10 +229,20 @@ class WorldBloc extends Bloc { } } - Future _loadScript(String? script) async { + Future _loadGameMode(ItemLocation? location) async { try { - if (script == null) return; - pluginSystem.loadLuaPlugin(state.assetManager, script); + if (location == null) return; + final gameMode = state.assetManager + .getPack(location.namespace) + ?.getMode(location.id); + if (gameMode == null) return; + final script = gameMode.script; + if (script != null && script.isNotEmpty) { + pluginSystem.loadLuaPluginFromLocation( + state.assetManager, + ItemLocation(location.namespace, script), + ); + } // ignore: empty_catches } catch (e) {} } diff --git a/app/lib/bloc/world/local.dart b/app/lib/bloc/world/local.dart index bebcfb1e..dd18bdc3 100644 --- a/app/lib/bloc/world/local.dart +++ b/app/lib/bloc/world/local.dart @@ -15,14 +15,6 @@ final class HandChanged extends LocalWorldEvent with HandChangedMappable { HandChanged.toggle({this.deck}) : show = null; } -@MappableClass() -final class CellSwitched extends LocalWorldEvent with CellSwitchedMappable { - final VectorDefinition? cell; - final bool toggle; - - CellSwitched(this.cell, {this.toggle = false}); -} - @MappableClass() final class ColorSchemeChanged extends LocalWorldEvent with ColorSchemeChangedMappable { @@ -39,6 +31,14 @@ final class SwitchCellOnMoveChanged extends LocalWorldEvent SwitchCellOnMoveChanged(this.value); } +@MappableClass() +final class WaypointVisibilityChanged extends LocalWorldEvent + with WaypointVisibilityChangedMappable { + final bool value; + + WaypointVisibilityChanged(this.value); +} + @MappableClass() final class TableSwitched extends LocalWorldEvent with TableSwitchedMappable { final String name; diff --git a/app/lib/bloc/world/local.mapper.dart b/app/lib/bloc/world/local.mapper.dart index 2bf60e8c..3ac397e9 100644 --- a/app/lib/bloc/world/local.mapper.dart +++ b/app/lib/bloc/world/local.mapper.dart @@ -151,151 +151,6 @@ class _HandChangedCopyWithImpl<$R, $Out> ) => _HandChangedCopyWithImpl<$R2, $Out2>($value, $cast, t); } -class CellSwitchedMapper extends SubClassMapperBase { - CellSwitchedMapper._(); - - static CellSwitchedMapper? _instance; - static CellSwitchedMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = CellSwitchedMapper._()); - LocalWorldEventMapper.ensureInitialized().addSubMapper(_instance!); - VectorDefinitionMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'CellSwitched'; - - static VectorDefinition? _$cell(CellSwitched v) => v.cell; - static const Field _f$cell = Field( - 'cell', - _$cell, - ); - static bool _$toggle(CellSwitched v) => v.toggle; - static const Field _f$toggle = Field( - 'toggle', - _$toggle, - opt: true, - def: false, - ); - - @override - final MappableFields fields = const { - #cell: _f$cell, - #toggle: _f$toggle, - }; - - @override - final String discriminatorKey = 'type'; - @override - final dynamic discriminatorValue = 'CellSwitched'; - @override - late final ClassMapperBase superMapper = - LocalWorldEventMapper.ensureInitialized(); - - static CellSwitched _instantiate(DecodingData data) { - return CellSwitched(data.dec(_f$cell), toggle: data.dec(_f$toggle)); - } - - @override - final Function instantiate = _instantiate; - - static CellSwitched fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static CellSwitched fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin CellSwitchedMappable { - String toJson() { - return CellSwitchedMapper.ensureInitialized().encodeJson( - this as CellSwitched, - ); - } - - Map toMap() { - return CellSwitchedMapper.ensureInitialized().encodeMap( - this as CellSwitched, - ); - } - - CellSwitchedCopyWith get copyWith => - _CellSwitchedCopyWithImpl( - this as CellSwitched, - $identity, - $identity, - ); - @override - String toString() { - return CellSwitchedMapper.ensureInitialized().stringifyValue( - this as CellSwitched, - ); - } - - @override - bool operator ==(Object other) { - return CellSwitchedMapper.ensureInitialized().equalsValue( - this as CellSwitched, - other, - ); - } - - @override - int get hashCode { - return CellSwitchedMapper.ensureInitialized().hashValue( - this as CellSwitched, - ); - } -} - -extension CellSwitchedValueCopy<$R, $Out> - on ObjectCopyWith<$R, CellSwitched, $Out> { - CellSwitchedCopyWith<$R, CellSwitched, $Out> get $asCellSwitched => - $base.as((v, t, t2) => _CellSwitchedCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class CellSwitchedCopyWith<$R, $In extends CellSwitched, $Out> - implements LocalWorldEventCopyWith<$R, $In, $Out> { - VectorDefinitionCopyWith<$R, VectorDefinition, VectorDefinition>? get cell; - @override - $R call({VectorDefinition? cell, bool? toggle}); - CellSwitchedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _CellSwitchedCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, CellSwitched, $Out> - implements CellSwitchedCopyWith<$R, CellSwitched, $Out> { - _CellSwitchedCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - CellSwitchedMapper.ensureInitialized(); - @override - VectorDefinitionCopyWith<$R, VectorDefinition, VectorDefinition>? get cell => - $value.cell?.copyWith.$chain((v) => call(cell: v)); - @override - $R call({Object? cell = $none, bool? toggle}) => $apply( - FieldCopyWithData({ - if (cell != $none) #cell: cell, - if (toggle != null) #toggle: toggle, - }), - ); - @override - CellSwitched $make(CopyWithData data) => CellSwitched( - data.get(#cell, or: $value.cell), - toggle: data.get(#toggle, or: $value.toggle), - ); - - @override - CellSwitchedCopyWith<$R2, CellSwitched, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _CellSwitchedCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - class ColorSchemeChangedMapper extends SubClassMapperBase { ColorSchemeChangedMapper._(); @@ -574,6 +429,150 @@ class _SwitchCellOnMoveChangedCopyWithImpl<$R, $Out> _SwitchCellOnMoveChangedCopyWithImpl<$R2, $Out2>($value, $cast, t); } +class WaypointVisibilityChangedMapper + extends SubClassMapperBase { + WaypointVisibilityChangedMapper._(); + + static WaypointVisibilityChangedMapper? _instance; + static WaypointVisibilityChangedMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use( + _instance = WaypointVisibilityChangedMapper._(), + ); + LocalWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + } + return _instance!; + } + + @override + final String id = 'WaypointVisibilityChanged'; + + static bool _$value(WaypointVisibilityChanged v) => v.value; + static const Field _f$value = Field( + 'value', + _$value, + ); + + @override + final MappableFields fields = const { + #value: _f$value, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'WaypointVisibilityChanged'; + @override + late final ClassMapperBase superMapper = + LocalWorldEventMapper.ensureInitialized(); + + static WaypointVisibilityChanged _instantiate(DecodingData data) { + return WaypointVisibilityChanged(data.dec(_f$value)); + } + + @override + final Function instantiate = _instantiate; + + static WaypointVisibilityChanged fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static WaypointVisibilityChanged fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin WaypointVisibilityChangedMappable { + String toJson() { + return WaypointVisibilityChangedMapper.ensureInitialized() + .encodeJson( + this as WaypointVisibilityChanged, + ); + } + + Map toMap() { + return WaypointVisibilityChangedMapper.ensureInitialized() + .encodeMap( + this as WaypointVisibilityChanged, + ); + } + + WaypointVisibilityChangedCopyWith< + WaypointVisibilityChanged, + WaypointVisibilityChanged, + WaypointVisibilityChanged + > + get copyWith => + _WaypointVisibilityChangedCopyWithImpl< + WaypointVisibilityChanged, + WaypointVisibilityChanged + >(this as WaypointVisibilityChanged, $identity, $identity); + @override + String toString() { + return WaypointVisibilityChangedMapper.ensureInitialized().stringifyValue( + this as WaypointVisibilityChanged, + ); + } + + @override + bool operator ==(Object other) { + return WaypointVisibilityChangedMapper.ensureInitialized().equalsValue( + this as WaypointVisibilityChanged, + other, + ); + } + + @override + int get hashCode { + return WaypointVisibilityChangedMapper.ensureInitialized().hashValue( + this as WaypointVisibilityChanged, + ); + } +} + +extension WaypointVisibilityChangedValueCopy<$R, $Out> + on ObjectCopyWith<$R, WaypointVisibilityChanged, $Out> { + WaypointVisibilityChangedCopyWith<$R, WaypointVisibilityChanged, $Out> + get $asWaypointVisibilityChanged => $base.as( + (v, t, t2) => _WaypointVisibilityChangedCopyWithImpl<$R, $Out>(v, t, t2), + ); +} + +abstract class WaypointVisibilityChangedCopyWith< + $R, + $In extends WaypointVisibilityChanged, + $Out +> + implements LocalWorldEventCopyWith<$R, $In, $Out> { + @override + $R call({bool? value}); + WaypointVisibilityChangedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t, + ); +} + +class _WaypointVisibilityChangedCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, WaypointVisibilityChanged, $Out> + implements + WaypointVisibilityChangedCopyWith<$R, WaypointVisibilityChanged, $Out> { + _WaypointVisibilityChangedCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + WaypointVisibilityChangedMapper.ensureInitialized(); + @override + $R call({bool? value}) => + $apply(FieldCopyWithData({if (value != null) #value: value})); + @override + WaypointVisibilityChanged $make(CopyWithData data) => + WaypointVisibilityChanged(data.get(#value, or: $value.value)); + + @override + WaypointVisibilityChangedCopyWith<$R2, WaypointVisibilityChanged, $Out2> + $chain<$R2, $Out2>(Then<$Out2, $R2> t) => + _WaypointVisibilityChangedCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + class TableSwitchedMapper extends SubClassMapperBase { TableSwitchedMapper._(); diff --git a/app/lib/bloc/world/state.dart b/app/lib/bloc/world/state.dart index d7ec52ba..9cdbe338 100644 --- a/app/lib/bloc/world/state.dart +++ b/app/lib/bloc/world/state.dart @@ -22,7 +22,7 @@ final class ClientWorldState with ClientWorldStateMappable { final ColorScheme colorScheme; final VectorDefinition? selectedCell; final ItemLocation? selectedDeck; - final bool showHand, switchCellOnMove; + final bool showHand, switchCellOnMove, showWaypoints; final DrawerView drawerView; final String searchTerm; final bool showDuplicates; @@ -36,6 +36,7 @@ final class ClientWorldState with ClientWorldStateMappable { this.selectedDeck, this.showHand = false, this.switchCellOnMove = false, + this.showWaypoints = false, this.drawerView = DrawerView.chat, this.searchTerm = '', this.showDuplicates = false, diff --git a/app/lib/bloc/world/state.mapper.dart b/app/lib/bloc/world/state.mapper.dart index 6bf98b7d..1ea8f43e 100644 --- a/app/lib/bloc/world/state.mapper.dart +++ b/app/lib/bloc/world/state.mapper.dart @@ -158,6 +158,13 @@ class ClientWorldStateMapper extends ClassMapperBase { opt: true, def: false, ); + static bool _$showWaypoints(ClientWorldState v) => v.showWaypoints; + static const Field _f$showWaypoints = Field( + 'showWaypoints', + _$showWaypoints, + opt: true, + def: false, + ); static DrawerView _$drawerView(ClientWorldState v) => v.drawerView; static const Field _f$drawerView = Field( 'drawerView', @@ -190,6 +197,7 @@ class ClientWorldStateMapper extends ClassMapperBase { #selectedDeck: _f$selectedDeck, #showHand: _f$showHand, #switchCellOnMove: _f$switchCellOnMove, + #showWaypoints: _f$showWaypoints, #drawerView: _f$drawerView, #searchTerm: _f$searchTerm, #showDuplicates: _f$showDuplicates, @@ -205,6 +213,7 @@ class ClientWorldStateMapper extends ClassMapperBase { selectedDeck: data.dec(_f$selectedDeck), showHand: data.dec(_f$showHand), switchCellOnMove: data.dec(_f$switchCellOnMove), + showWaypoints: data.dec(_f$showWaypoints), drawerView: data.dec(_f$drawerView), searchTerm: data.dec(_f$searchTerm), showDuplicates: data.dec(_f$showDuplicates), @@ -286,6 +295,7 @@ abstract class ClientWorldStateCopyWith<$R, $In extends ClientWorldState, $Out> ItemLocation? selectedDeck, bool? showHand, bool? switchCellOnMove, + bool? showWaypoints, DrawerView? drawerView, String? searchTerm, bool? showDuplicates, @@ -323,6 +333,7 @@ class _ClientWorldStateCopyWithImpl<$R, $Out> Object? selectedDeck = $none, bool? showHand, bool? switchCellOnMove, + bool? showWaypoints, DrawerView? drawerView, String? searchTerm, bool? showDuplicates, @@ -336,6 +347,7 @@ class _ClientWorldStateCopyWithImpl<$R, $Out> if (selectedDeck != $none) #selectedDeck: selectedDeck, if (showHand != null) #showHand: showHand, if (switchCellOnMove != null) #switchCellOnMove: switchCellOnMove, + if (showWaypoints != null) #showWaypoints: showWaypoints, if (drawerView != null) #drawerView: drawerView, if (searchTerm != null) #searchTerm: searchTerm, if (showDuplicates != null) #showDuplicates: showDuplicates, @@ -351,6 +363,7 @@ class _ClientWorldStateCopyWithImpl<$R, $Out> selectedDeck: data.get(#selectedDeck, or: $value.selectedDeck), showHand: data.get(#showHand, or: $value.showHand), switchCellOnMove: data.get(#switchCellOnMove, or: $value.switchCellOnMove), + showWaypoints: data.get(#showWaypoints, or: $value.showWaypoints), drawerView: data.get(#drawerView, or: $value.drawerView), searchTerm: data.get(#searchTerm, or: $value.searchTerm), showDuplicates: data.get(#showDuplicates, or: $value.showDuplicates), diff --git a/app/lib/board/cell.dart b/app/lib/board/cell.dart index da98625d..b875129d 100644 --- a/app/lib/board/cell.dart +++ b/app/lib/board/cell.dart @@ -4,9 +4,11 @@ import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/events.dart'; +import 'package:flame/text.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:setonix/pages/game/waypoint.dart'; import 'package:setonix/src/generated/i18n/app_localizations.dart'; import 'package:material_leap/material_leap.dart'; import 'package:setonix/bloc/world/bloc.dart'; @@ -37,6 +39,7 @@ class GameCell extends PositionComponent ScrollCallbacks { late final SpriteComponent _selectionComponent; SpriteComponent? _cardComponent, _boardComponent; + TextElementComponent? _waypointComponent; late final BoardGrid grid; List? _effects; @@ -60,6 +63,66 @@ class GameCell extends PositionComponent } } + void _buildWaypointComponent(ClientWorldState state) { + final visible = state.showWaypoints; + _waypointComponent?.removeFromParent(); + _waypointComponent = null; + if (!visible) { + return; + } + final global = toGlobalDefinition(state); + final globalWaypoints = state.info.waypoints + .where((waypoint) => waypoint.position == global) + .map((e) => PlainTextNode(e.name)) + .toList(); + final teamWaypoints = state.world.getTeams().expand((name) { + final team = state.info.teams[name]; + if (team == null) return Iterable.empty(); + return team.waypoints + .where((waypoint) => waypoint.position == global) + .map( + (e) => CustomInlineTextNode( + PlainTextNode(e.name), + styleName: 'team-$name', + ), + ); + }).toList(); + if (globalWaypoints.isEmpty && teamWaypoints.isEmpty) { + return; + } + final blocks = [ + ParagraphNode.group(globalWaypoints), + ParagraphNode.group(teamWaypoints), + ]; + final document = DocumentRoot(blocks); + final component = _waypointComponent = TextElementComponent.fromDocument( + document: document, + size: size, + priority: 2, + style: DocumentStyle( + paragraph: BlockStyle(textAlign: TextAlign.center), + customStyles: { + for (final entry in state.world.getTeams()) + 'team-$entry': InlineTextStyle( + color: + state.info.teams[entry]?.color?.color ?? + state.colorScheme.primary, + ), + }, + text: InlineTextStyle( + shadows: [ + Shadow( + color: Colors.black, + offset: const Offset(0, 0), + blurRadius: 2, + ), + ], + ), + ), + ); + add(component); + } + @override void onLoad() { super.onLoad(); @@ -81,7 +144,8 @@ class GameCell extends PositionComponent previousState.table.cells[definition] != newState.table.cells[definition] || previousState.teamMembers != newState.teamMembers || - previousState.colorScheme != newState.colorScheme; + previousState.colorScheme != newState.colorScheme || + previousState.showWaypoints != newState.showWaypoints; } bool get isSelected => isMounted && bloc.state.selectedCell == toDefinition(); @@ -120,7 +184,7 @@ class GameCell extends PositionComponent if (isSelected) { bloc.process(HandChanged.hide()); } else { - bloc.process(CellSwitched(toDefinition(), toggle: true)); + bloc.process(CellSwitched(isSelected ? null : toDefinition())); } } @@ -133,6 +197,7 @@ class GameCell extends PositionComponent @override void onInitialState(ClientWorldState state) { if (state.selectedCell != toDefinition()) _selectionComponent.opacity = 0; + _buildWaypointComponent(state); } bool isClaimed(ClientWorldState state) => state.info.teams.entries.any( @@ -177,6 +242,7 @@ class GameCell extends PositionComponent ), ]); } + _buildWaypointComponent(state); } Future _updateTop() async { @@ -291,6 +357,21 @@ class GameCell extends PositionComponent onClose(); }, ), + ContextMenuButtonItem( + label: AppLocalizations.of(context).addWaypoint, + onPressed: () { + onClose(); + showDialog( + context: context, + builder: (context) => BlocProvider.value( + value: bloc, + child: WaypointDialog( + position: toGlobalDefinition(bloc.state), + ), + ), + ); + }, + ), ContextMenuButtonItem( label: AppLocalizations.of(context).teams, onPressed: () { diff --git a/app/lib/board/game.dart b/app/lib/board/game.dart index 16f8f472..eea6b77a 100644 --- a/app/lib/board/game.dart +++ b/app/lib/board/game.dart @@ -14,6 +14,8 @@ import 'package:setonix/board/grid.dart'; import 'package:setonix/board/hand/view.dart'; import 'package:setonix/helpers/scroll.dart'; import 'package:setonix/helpers/secondary.dart'; +import 'package:setonix/helpers/vector.dart'; +import 'package:setonix_api/setonix_api.dart'; class BoardGame extends FlameGame with @@ -172,4 +174,15 @@ class BoardGame extends FlameGame contextMenuBuilder(context, contextMenuController.remove), ); } + + void teleport(GlobalVectorDefinition position) { + final table = position.table; + if (table != bloc.state.world.tableName) { + bloc.add(TableSwitched(table)); + } + final cellSize = grid.cellSize; + camera.moveTo( + (position.position.toVector()..multiply(cellSize)) + cellSize / 2, + ); + } } diff --git a/app/lib/board/hand/figure.dart b/app/lib/board/hand/figure.dart index 120d0188..5a900dac 100644 --- a/app/lib/board/hand/figure.dart +++ b/app/lib/board/hand/figure.dart @@ -1,5 +1,4 @@ import 'package:flame/widgets.dart'; -import 'package:setonix/bloc/world/local.dart'; import 'package:setonix/bloc/world/state.dart'; import 'package:setonix/board/cell.dart'; import 'package:setonix/board/hand/item.dart'; diff --git a/app/lib/board/hand/object.dart b/app/lib/board/hand/object.dart index 6c84f914..003aef47 100644 --- a/app/lib/board/hand/object.dart +++ b/app/lib/board/hand/object.dart @@ -1,7 +1,6 @@ import 'package:flame/components.dart'; import 'package:flutter/widgets.dart'; import 'package:setonix/src/generated/i18n/app_localizations.dart'; -import 'package:setonix/bloc/world/local.dart'; import 'package:setonix/bloc/world/state.dart'; import 'package:setonix/board/cell.dart'; import 'package:setonix/board/hand/item.dart'; diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index d6b46d53..8b4f2df9 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -280,5 +280,11 @@ "singleplayer": "Singleplayer", "recentGames": "Recent games", "noRecentGames": "There are no recent games available", - "copy": "Copy" + "copy": "Copy", + "waypoints": "Waypoints", + "addWaypoint": "Add waypoint", + "editWaypoint": "Edit waypoint", + "team": "Team", + "public": "Public", + "noWaypoints": "There are no waypoints available" } diff --git a/app/lib/main.dart b/app/lib/main.dart index a4382bf5..8af47619 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -22,7 +22,6 @@ import 'package:setonix/theme.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:setonix_plugin/setonix_plugin.dart'; import 'bloc/settings.dart'; import 'pages/settings/home.dart'; @@ -49,7 +48,7 @@ Future main(List args) async { await setup(settingsCubit); - await initPluginSystem(); + //await initPluginSystem(); runApp( MultiBlocProvider( providers: [ diff --git a/app/lib/pages/game/drawer.dart b/app/lib/pages/game/drawer.dart index 4a38c846..39ff998b 100644 --- a/app/lib/pages/game/drawer.dart +++ b/app/lib/pages/game/drawer.dart @@ -2,7 +2,9 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:setonix/board/game.dart'; import 'package:setonix/pages/game/multiplayer/dialog.dart'; +import 'package:setonix/pages/game/waypoint.dart'; import 'package:setonix/src/generated/i18n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:material_leap/material_leap.dart'; @@ -20,7 +22,9 @@ import 'package:setonix/pages/packs/dialog.dart'; import 'package:setonix_api/setonix_api.dart'; class GameDrawer extends StatelessWidget { - const GameDrawer({super.key}); + final BoardGame game; + + const GameDrawer({super.key, required this.game}); @override Widget build(BuildContext context) { @@ -181,6 +185,24 @@ class GameDrawer extends StatelessWidget { ); }, ), + BlocBuilder( + buildWhen: (previous, current) => + previous.showWaypoints != current.showWaypoints, + builder: (context, state) { + return Padding( + padding: EdgeInsets.only(right: 24), + child: AdvancedSwitchListTile( + title: Text(AppLocalizations.of(context).waypoints), + leading: const Icon(PhosphorIconsLight.mapPin), + value: state.showWaypoints, + onChanged: (value) => context.read().process( + WaypointVisibilityChanged(value), + ), + onTap: () => _showWaypointsDialog(context), + ), + ); + }, + ), BlocBuilder( buildWhen: (previous, current) => previous.zoom != current.zoom, builder: (context, state) => ListTile( @@ -646,4 +668,121 @@ class GameDrawer extends StatelessWidget { ], ); } + + void _showWaypointsDialog(BuildContext context) { + final bloc = context.read(); + Widget buildWaypointTile(Waypoint waypoint, {String? team}) { + final gameTeam = bloc.state.info.teams[team]; + return ContextRegion( + builder: (ctx, button, controller) => ListTile( + title: Text(waypoint.name), + leading: Icon( + team != null ? PhosphorIconsLight.users : PhosphorIconsLight.mapPin, + color: gameTeam?.color?.color, + ), + trailing: button, + onTap: () { + Navigator.of(ctx).pop(); + game.teleport(waypoint.position); + Scaffold.of(context).closeDrawer(); + }, + ), + menuChildren: [ + MenuItemButton( + leadingIcon: const Icon(PhosphorIconsLight.pencil), + child: Text(AppLocalizations.of(context).edit), + onPressed: () => showDialog( + context: context, + builder: (context) => BlocProvider.value( + value: bloc, + child: WaypointDialog(waypoint: waypoint, team: team), + ), + ), + ), + MenuItemButton( + leadingIcon: const Icon(PhosphorIconsLight.trash), + child: Text(AppLocalizations.of(context).delete), + onPressed: () { + bloc.add(WaypointRemoved(name: waypoint.name, team: team)); + }, + ), + ], + ); + } + + bool showPublic = true, showTeams = true; + + showLeapBottomSheet( + context: context, + titleBuilder: (context) => Text(AppLocalizations.of(context).waypoints), + childrenBuilder: (context) => [ + StatefulBuilder( + builder: (context, setLocalState) { + return BlocBuilder( + bloc: bloc, + buildWhen: (previous, current) => + previous.info.waypoints != current.info.waypoints || + previous.info.teams != current.info.teams || + previous.teamMembers != current.teamMembers, + builder: (context, state) { + final List waypointTiles = []; + + if (showTeams) { + waypointTiles.addAll( + state.world.getTeams().expand( + (e) => + state.info.teams[e]?.waypoints.map( + (waypoint) => buildWaypointTile(waypoint, team: e), + ) ?? + [], + ), + ); + } + + if (showPublic) { + waypointTiles.addAll( + state.info.waypoints.map((e) => buildWaypointTile(e)), + ); + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Wrap( + spacing: 8, + runSpacing: 4, + children: [ + FilterChip( + label: Text(AppLocalizations.of(context).teams), + selected: showTeams, + onSelected: (v) => setLocalState(() => showTeams = v), + ), + FilterChip( + label: const Text('Public'), + selected: showPublic, + onSelected: (v) => + setLocalState(() => showPublic = v), + ), + ], + ), + const SizedBox(height: 8), + if (waypointTiles.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Center( + child: Text(AppLocalizations.of(context).noWaypoints), + ), + ) + else + ...waypointTiles, + ], + ); + }, + ); + }, + ), + ], + ); + } } diff --git a/app/lib/pages/game/page.dart b/app/lib/pages/game/page.dart index 29ebe160..9973168c 100644 --- a/app/lib/pages/game/page.dart +++ b/app/lib/pages/game/page.dart @@ -147,6 +147,12 @@ class _GamePageState extends State { onReconnect: () async => (await _bloc)?.$1.reconnect(), ); } + final game = BoardGame( + bloc: context.read(), + settingsCubit: context.read(), + contextMenuController: _contextMenuController, + onEscape: () => Scaffold.of(context).openDrawer(), + ); return Scaffold( appBar: WindowTitleBar( title: Text(AppLocalizations.of(context).game), @@ -181,7 +187,7 @@ class _GamePageState extends State { ), ], ), - drawer: const GameDrawer(), + drawer: GameDrawer(game: game), endDrawer: BlocBuilder( buildWhen: (previous, current) => previous.drawerView != current.drawerView, @@ -258,13 +264,7 @@ class _GamePageState extends State { ) else GameWidget( - game: BoardGame( - bloc: context.read(), - settingsCubit: context.read(), - contextMenuController: _contextMenuController, - onEscape: () => - Scaffold.of(context).openDrawer(), - ), + game: game, focusNode: _focusNode, initialActiveOverlays: ['dialogs', 'filter'], overlayBuilderMap: { diff --git a/app/lib/pages/game/waypoint.dart b/app/lib/pages/game/waypoint.dart new file mode 100644 index 00000000..6599d71d --- /dev/null +++ b/app/lib/pages/game/waypoint.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:setonix/bloc/world/bloc.dart'; +import 'package:setonix/src/generated/i18n/app_localizations.dart'; +import 'package:setonix_api/setonix_api.dart'; + +class WaypointDialog extends StatelessWidget { + final String? team; + final Waypoint? waypoint; + final GlobalVectorDefinition? position; + + const WaypointDialog({super.key, this.team, this.waypoint, this.position}); + + @override + Widget build(BuildContext context) { + final isCreated = position != null; + final loc = AppLocalizations.of(context); + var waypoint = + this.waypoint ?? + Waypoint( + name: '', + position: position ?? GlobalVectorDefinition('', 0, 0), + ); + var team = this.team; + final bloc = context.read(); + return ResponsiveAlertDialog( + title: Text(isCreated ? loc.addWaypoint : loc.editWaypoint), + constraints: const BoxConstraints(maxWidth: LeapBreakpoints.compact), + content: ListView( + shrinkWrap: true, + children: [ + TextFormField( + decoration: InputDecoration(labelText: loc.name, filled: true), + initialValue: waypoint.name, + onChanged: (value) { + waypoint = waypoint.copyWith(name: value); + }, + ), + const SizedBox(height: 8), + DropdownMenu( + expandedInsets: EdgeInsets.symmetric(horizontal: 0, vertical: 12), + dropdownMenuEntries: [ + DropdownMenuEntry(value: null, label: loc.public), + ...bloc.state.world.getTeams().map( + (e) => DropdownMenuEntry(value: e, label: e), + ), + ], + label: Text(loc.team), + initialSelection: team, + onSelected: (value) { + team = value; + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(loc.cancel), + ), + ElevatedButton( + onPressed: () { + bloc.add( + WaypointChanged( + waypoint: waypoint, + team: team, + name: this.waypoint?.name, + ), + ); + Navigator.of(context).pop(); + }, + child: Text(isCreated ? loc.add : loc.save), + ), + ], + ); + } +} diff --git a/app/pack/modes/hello.json b/app/pack/modes/hello.json new file mode 100644 index 00000000..45cc5bae --- /dev/null +++ b/app/pack/modes/hello.json @@ -0,0 +1,3 @@ +{ + "script": "hello.lua" +} \ No newline at end of file diff --git a/app/pack/scripts/hello.lua b/app/pack/scripts/hello.lua new file mode 100644 index 00000000..4d51f376 --- /dev/null +++ b/app/pack/scripts/hello.lua @@ -0,0 +1,5 @@ +print("Hello from Lua script!") + +Events.CellHideChanged.Connect(function() + print("cell hide changed") +end) diff --git a/plugin/lib/setonix_plugin.dart b/plugin/lib/setonix_plugin.dart index 2fa283f5..12aacb89 100644 --- a/plugin/lib/setonix_plugin.dart +++ b/plugin/lib/setonix_plugin.dart @@ -5,10 +5,16 @@ export 'src/rust/api/plugin.dart'; export 'src/rust/api/luau.dart'; export 'events.dart'; +bool _isInitialized = false; + +bool get isPluginSystemInitialized => _isInitialized; + Future initPluginSystem() { - return Future.value(); + _isInitialized = true; + return RustLib.init(); } void disposePluginSystem() { + _isInitialized = false; RustLib.dispose(); } diff --git a/plugin/lib/src/events/model.dart b/plugin/lib/src/events/model.dart index df5009e6..5a79190c 100644 --- a/plugin/lib/src/events/model.dart +++ b/plugin/lib/src/events/model.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:dart_mappable/dart_mappable.dart'; import 'package:networker/networker.dart'; @@ -93,7 +95,45 @@ final class UserLeaveCallback { UserLeaveCallback({required this.channel, required this.info}); } -@MappableClass() +class _ConvertedConnectionInfo extends ConnectionInfo { + final Uri address; + + _ConvertedConnectionInfo(this.address); + + @override + FutureOr close() {} + + @override + bool get isClosed => true; + + @override + FutureOr sendMessage(Uint8List data) {} +} + +class ConnectionInfoMapper extends SimpleMapper { + const ConnectionInfoMapper(); + + @override + ConnectionInfo decode(Object value) { + if (value is Map) { + return _ConvertedConnectionInfo( + Uri.parse(value['address'] as String? ?? 'http://localhost'), + ); + } + return _ConvertedConnectionInfo(Uri.parse('http://localhost')); + } + + @override + Object? encode(ConnectionInfo self) { + return { + 'address': self is _ConvertedConnectionInfo + ? self.address.toString() + : '', + }; + } +} + +@MappableClass(includeCustomMappers: [ConnectionInfoMapper()]) final class UserJoined extends LocalWorldEvent with UserJoinedMappable { final Channel channel; final ConnectionInfo info; diff --git a/plugin/lib/src/events/model.mapper.dart b/plugin/lib/src/events/model.mapper.dart index 8c3bc4d2..5d3e45b4 100644 --- a/plugin/lib/src/events/model.mapper.dart +++ b/plugin/lib/src/events/model.mapper.dart @@ -1,5 +1,6 @@ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format off // ignore_for_file: type=lint // ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member // ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter @@ -14,6 +15,7 @@ class UserJoinedMapper extends SubClassMapperBase { if (_instance == null) { MapperContainer.globals.use(_instance = UserJoinedMapper._()); LocalWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + MapperContainer.globals.useAll([ConnectionInfoMapper()]); } return _instance!; } @@ -138,3 +140,4 @@ class _UserJoinedCopyWithImpl<$R, $Out> Then<$Out2, $R2> t, ) => _UserJoinedCopyWithImpl<$R2, $Out2>($value, $cast, t); } + diff --git a/plugin/lib/src/plugin.dart b/plugin/lib/src/plugin.dart index cd02ca90..55953734 100644 --- a/plugin/lib/src/plugin.dart +++ b/plugin/lib/src/plugin.dart @@ -22,6 +22,8 @@ mixin ServerInterface { required String plugin, }); + void print(String message, [String? plugin]); + WorldState get state; List get players; } @@ -32,26 +34,24 @@ final class PluginSystem { PluginSystem({required this.server}); - SetonixPlugin registerPlugin( + Future registerPlugin( String name, - SetonixPlugin Function(PluginServerInterface) pluginBuilder, - ) { + FutureOr Function(PluginServerInterface) pluginBuilder, + ) async { + unregisterPlugin(name); final pluginServer = _PluginServerInterfaceImpl(server, name); - final plugin = pluginBuilder(pluginServer); + final plugin = await pluginBuilder(pluginServer); return _plugins[name] = plugin; } - SetonixPlugin registerLuauPlugin( - String name, - String code, { - void Function(String)? onPrint, - }) { + Future registerLuauPlugin(String name, String code) { if (!_nativeEnabled) throw Exception('Native not enabled'); return registerPlugin( name, (pluginServer) => RustSetonixPlugin.build( (c) => LuauPlugin(code: code, callback: c), pluginServer, + onPrint: (e) => server.print(e, name), ), ); } @@ -60,13 +60,11 @@ final class PluginSystem { _plugins.remove(name); } - void loadLuaPlugin( + void loadLuaPluginFromLocation( AssetManager assetManager, - String script, [ + ItemLocation location, [ String name = 'game', ]) { - unregisterPlugin(name); - final location = ItemLocation.fromString(script); final data = assetManager .getPack(location.namespace) ?.getScript(location.id); @@ -76,11 +74,17 @@ final class PluginSystem { bool get _nativeEnabled => RustLib.instance.initialized; + Iterable get plugins => _plugins.keys; + void dispose([bool disposeNative = true]) { - List.from(_plugins.keys).forEach(unregisterPlugin); + unregisterAll(); if (disposeNative) disposePluginSystem(); } + void unregisterAll() { + List.from(_plugins.keys).forEach(unregisterPlugin); + } + void fire(Event event) { for (final plugin in _plugins.values) { plugin.eventSystem.fire(event); @@ -155,14 +159,12 @@ final class RustSetonixPlugin extends SetonixPlugin { RustSetonixPlugin._(super.server, this.plugin); - factory RustSetonixPlugin.build( + static Future build( RustPlugin Function(PluginCallback) builder, PluginServerInterface server, { void Function(String)? onPrint, - }) { + }) async { final callback = PluginCallback.default_(); - final plugin = builder(callback); - final instance = RustSetonixPlugin._(server, plugin); if (onPrint != null) { callback.changeOnPrint(onPrint: onPrint); } @@ -183,13 +185,17 @@ final class RustSetonixPlugin extends SetonixPlugin { final state = server.state; return switch (field) { StateFieldAccess.info => state.info.toJson(), - StateFieldAccess.table => state.table.toJson(), + StateFieldAccess.tables => jsonEncode( + state.data.getTables().toList(), + ), StateFieldAccess.tableName => jsonEncode(state.tableName), StateFieldAccess.players => jsonEncode(server.players), StateFieldAccess.teamMembers => jsonEncode(state.teamMembers), }; }, ); + final plugin = builder(callback); + final instance = RustSetonixPlugin._(server, plugin); instance.eventSystem.on((e) { instance.plugin.runEvent( eventType: e.clientEvent.runtimeType.toString(), @@ -199,6 +205,7 @@ final class RustSetonixPlugin extends SetonixPlugin { source: e.source, ); }); + await instance.plugin.run(); return instance; } diff --git a/plugin/lib/src/rust/api/plugin.dart b/plugin/lib/src/rust/api/plugin.dart index 324ca3bd..0321f48e 100644 --- a/plugin/lib/src/rust/api/plugin.dart +++ b/plugin/lib/src/rust/api/plugin.dart @@ -27,6 +27,10 @@ abstract class PluginCallback implements RustOpaqueInterface { required FutureOr Function(StateFieldAccess) stateFieldAccess, }); + void changeTableAccess({ + required FutureOr Function(String?) tableAccess, + }); + static PluginCallback default_() => RustLib.instance.api.crateApiPluginPluginCallbackDefault(); } @@ -64,4 +68,4 @@ class EventResult { needsUpdate == other.needsUpdate; } -enum StateFieldAccess { table, tableName, info, players, teamMembers } +enum StateFieldAccess { tableName, tables, info, players, teamMembers } diff --git a/plugin/lib/src/rust/frb_generated.dart b/plugin/lib/src/rust/frb_generated.dart index 7f16b3bb..0d00d8a4 100644 --- a/plugin/lib/src/rust/frb_generated.dart +++ b/plugin/lib/src/rust/frb_generated.dart @@ -66,7 +66,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => 2139481266; + int get rustContentHash => -1139025024; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -113,6 +113,11 @@ abstract class RustLibApi extends BaseApi { required FutureOr Function(StateFieldAccess) stateFieldAccess, }); + void crateApiPluginPluginCallbackChangeTableAccess({ + required PluginCallback that, + required FutureOr Function(String?) tableAccess, + }); + PluginCallback crateApiPluginPluginCallbackDefault(); Future crateApiSimpleSimpleAdderTwinNormal({ @@ -409,13 +414,49 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - PluginCallback crateApiPluginPluginCallbackDefault() { + void crateApiPluginPluginCallbackChangeTableAccess({ + required PluginCallback that, + required FutureOr Function(String?) tableAccess, + }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPluginCallback( + that, + serializer, + ); + sse_encode_DartFn_Inputs_opt_String_Output_String_AnyhowException( + tableAccess, + serializer, + ); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiPluginPluginCallbackChangeTableAccessConstMeta, + argValues: [that, tableAccess], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiPluginPluginCallbackChangeTableAccessConstMeta => + const TaskConstMeta( + debugName: "PluginCallback_change_table_access", + argNames: ["that", "tableAccess"], + ); + + @override + PluginCallback crateApiPluginPluginCallbackDefault() { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; + }, codec: SseCodec( decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPluginCallback, @@ -445,7 +486,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 11, + funcId: 12, port: port_, ); }, @@ -573,6 +614,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { }; } + Future Function(int, dynamic) + encode_DartFn_Inputs_opt_String_Output_String_AnyhowException( + FutureOr Function(String?) raw, + ) { + return (callId, rawArg0) async { + final arg0 = dco_decode_opt_String(rawArg0); + + Box? rawOutput; + Box? rawError; + try { + rawOutput = Box(await raw(arg0)); + } catch (e, s) { + rawError = Box(AnyhowException("$e\n\n$s")); + } + + final serializer = SseSerializer(generalizedFrbRustBinding); + assert((rawOutput != null) ^ (rawError != null)); + if (rawOutput != null) { + serializer.buffer.putUint8(0); + sse_encode_String(rawOutput.value, serializer); + } else { + serializer.buffer.putUint8(1); + sse_encode_AnyhowException(rawError!.value, serializer); + } + final output = serializer.intoRaw(); + + generalizedFrbRustBinding.dartFnDeliverOutput( + callId: callId, + ptr: output.ptr, + rustVecLen: output.rustVecLen, + dataLen: output.dataLen, + ); + }; + } + Future Function(int, dynamic) encode_DartFn_Inputs_state_field_access_Output_String_AnyhowException( FutureOr Function(StateFieldAccess) raw, @@ -691,6 +767,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { throw UnimplementedError(''); } + @protected + FutureOr Function(String?) + dco_decode_DartFn_Inputs_opt_String_Output_String_AnyhowException( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(''); + } + @protected FutureOr Function(StateFieldAccess) dco_decode_DartFn_Inputs_state_field_access_Output_String_AnyhowException( @@ -1185,6 +1270,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void sse_encode_DartFn_Inputs_opt_String_Output_String_AnyhowException( + FutureOr Function(String?) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_DartOpaque( + encode_DartFn_Inputs_opt_String_Output_String_AnyhowException(self), + serializer, + ); + } + @protected void sse_encode_DartFn_Inputs_state_field_access_Output_String_AnyhowException( @@ -1470,4 +1567,11 @@ class PluginCallbackImpl extends RustOpaque implements PluginCallback { that: this, stateFieldAccess: stateFieldAccess, ); + + void changeTableAccess({ + required FutureOr Function(String?) tableAccess, + }) => RustLib.instance.api.crateApiPluginPluginCallbackChangeTableAccess( + that: this, + tableAccess: tableAccess, + ); } diff --git a/plugin/lib/src/rust/frb_generated.io.dart b/plugin/lib/src/rust/frb_generated.io.dart index 368100d8..25946210 100644 --- a/plugin/lib/src/rust/frb_generated.io.dart +++ b/plugin/lib/src/rust/frb_generated.io.dart @@ -71,6 +71,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + FutureOr Function(String?) + dco_decode_DartFn_Inputs_opt_String_Output_String_AnyhowException( + dynamic raw, + ); + @protected FutureOr Function(StateFieldAccess) dco_decode_DartFn_Inputs_state_field_access_Output_String_AnyhowException( @@ -305,6 +311,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_DartFn_Inputs_opt_String_Output_String_AnyhowException( + FutureOr Function(String?) self, + SseSerializer serializer, + ); + @protected void sse_encode_DartFn_Inputs_state_field_access_Output_String_AnyhowException( diff --git a/plugin/lib/src/rust/frb_generated.web.dart b/plugin/lib/src/rust/frb_generated.web.dart index 34fedc43..d4f352e2 100644 --- a/plugin/lib/src/rust/frb_generated.web.dart +++ b/plugin/lib/src/rust/frb_generated.web.dart @@ -73,6 +73,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + FutureOr Function(String?) + dco_decode_DartFn_Inputs_opt_String_Output_String_AnyhowException( + dynamic raw, + ); + @protected FutureOr Function(StateFieldAccess) dco_decode_DartFn_Inputs_state_field_access_Output_String_AnyhowException( @@ -307,6 +313,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_DartFn_Inputs_opt_String_Output_String_AnyhowException( + FutureOr Function(String?) self, + SseSerializer serializer, + ); + @protected void sse_encode_DartFn_Inputs_state_field_access_Output_String_AnyhowException( diff --git a/plugin/rust/src/api/luau/state.rs b/plugin/rust/src/api/luau/state.rs index 69dd52c0..cc600f9e 100644 --- a/plugin/rust/src/api/luau/state.rs +++ b/plugin/rust/src/api/luau/state.rs @@ -19,5 +19,22 @@ impl LuaUserData for LuauStateUserData { Ok(serialized) }); } + fields.add_field_method_get("Table", move |lua, this: &LuauStateUserData| { + let callback = this.0.table_access.clone(); + let result = block_on(callback(None)); + let result = serde_json::from_str::(&result).unwrap(); + let serialized = lua.to_value(&result).unwrap(); + Ok(serialized) + }); + } + + fn add_methods>(methods: &mut M) { + methods.add_method("GetTable", |lua, this: &LuauStateUserData, table_name: Option| { + let callback = this.0.table_access.clone(); + let result = block_on(callback(table_name)); + let result = serde_json::from_str::(&result).unwrap(); + let serialized = lua.to_value(&result).unwrap(); + Ok(serialized) + }); } } diff --git a/plugin/rust/src/api/plugin.rs b/plugin/rust/src/api/plugin.rs index e320f628..a8c2b4dc 100644 --- a/plugin/rust/src/api/plugin.rs +++ b/plugin/rust/src/api/plugin.rs @@ -13,8 +13,8 @@ pub type DartCallback2 = Arc DartFnFuture<()> + Send + Syn #[derive(strum::Display, strum::EnumIter, Clone, Copy)] pub enum StateFieldAccess { - Table, TableName, + Tables, Info, Players, TeamMembers, @@ -26,6 +26,7 @@ pub struct PluginCallback { pub(crate) process_event: DartCallback2>, pub(crate) send_event: DartCallback2>, pub(crate) state_field_access: Arc DartFnFuture + Send + Sync>, + pub(crate) table_access: Arc) -> DartFnFuture + Send + Sync>, } impl Default for PluginCallback { @@ -40,6 +41,7 @@ impl Default for PluginCallback { process_event: Arc::new(|_, _| Box::pin(async {})), send_event: Arc::new(|_, _| Box::pin(async {})), state_field_access: Arc::new(|_| Box::pin(async { "".to_string() })), + table_access: Arc::new(|_| Box::pin(async { "".to_string() })), } } } @@ -63,6 +65,11 @@ impl PluginCallback { pub fn change_state_field_access(&mut self, state_field_access: impl Fn(StateFieldAccess) -> DartFnFuture + 'static + Send + Sync) { self.state_field_access = Arc::new(Box::new(state_field_access)); // or sth like that } + + #[frb(sync)] + pub fn change_table_access(&mut self, table_access: impl Fn(Option) -> DartFnFuture + 'static + Send + Sync) { + self.table_access = Arc::new(Box::new(table_access)); // or sth like that + } } pub type Channel = i16; diff --git a/plugin/rust/src/frb_generated.rs b/plugin/rust/src/frb_generated.rs index 7da3bb05..6e1c2881 100644 --- a/plugin/rust/src/frb_generated.rs +++ b/plugin/rust/src/frb_generated.rs @@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 2139481266; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1139025024; // Section: executor @@ -411,6 +411,60 @@ fn wire__crate__api__plugin__PluginCallback_change_state_field_access_impl( }, ) } +fn wire__crate__api__plugin__PluginCallback_change_table_access_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "PluginCallback_change_table_access", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_table_access = decode_DartFn_Inputs_opt_String_Output_String_AnyhowException( + ::sse_decode(&mut deserializer), + ); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, true, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref_mut()), + _ => unreachable!(), + } + } + let mut api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::api::plugin::PluginCallback::change_table_access( + &mut *api_that_guard, + api_table_access, + ); + })?; + Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__plugin__PluginCallback_default_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -591,6 +645,38 @@ fn decode_DartFn_Inputs_String_opt_box_autoadd_i_16_Output_unit_AnyhowException( )) } } +fn decode_DartFn_Inputs_opt_String_Output_String_AnyhowException( + dart_opaque: flutter_rust_bridge::DartOpaque, +) -> impl Fn(Option) -> flutter_rust_bridge::DartFnFuture { + use flutter_rust_bridge::IntoDart; + + async fn body(dart_opaque: flutter_rust_bridge::DartOpaque, arg0: Option) -> String { + let args = vec![arg0.into_into_dart().into_dart()]; + let message = FLUTTER_RUST_BRIDGE_HANDLER + .dart_fn_invoke(dart_opaque, args) + .await; + + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let action = deserializer.cursor.read_u8().unwrap(); + let ans = match action { + 0 => std::result::Result::Ok(::sse_decode(&mut deserializer)), + 1 => std::result::Result::Err( + ::sse_decode(&mut deserializer), + ), + _ => unreachable!(), + }; + deserializer.end(); + let ans = ans.expect("Dart throws exception but Rust side assume it is not failable"); + ans + } + + move |arg0: Option| { + flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( + dart_opaque.clone(), + arg0, + )) + } +} fn decode_DartFn_Inputs_state_field_access_Output_String_AnyhowException( dart_opaque: flutter_rust_bridge::DartOpaque, ) -> impl Fn(crate::api::plugin::StateFieldAccess) -> flutter_rust_bridge::DartFnFuture { @@ -823,8 +909,8 @@ impl SseDecode for crate::api::plugin::StateFieldAccess { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { let mut inner = ::sse_decode(deserializer); return match inner { - 0 => crate::api::plugin::StateFieldAccess::Table, - 1 => crate::api::plugin::StateFieldAccess::TableName, + 0 => crate::api::plugin::StateFieldAccess::TableName, + 1 => crate::api::plugin::StateFieldAccess::Tables, 2 => crate::api::plugin::StateFieldAccess::Info, 3 => crate::api::plugin::StateFieldAccess::Players, 4 => crate::api::plugin::StateFieldAccess::TeamMembers, @@ -863,7 +949,7 @@ fn pde_ffi_dispatcher_primary_impl( match func_id { 2 => wire__crate__api__luau__LuauPlugin_run_impl(port, ptr, rust_vec_len, data_len), 3 => wire__crate__api__luau__LuauPlugin_run_event_impl(port, ptr, rust_vec_len, data_len), - 11 => wire__crate__api__simple__simple_adder_twin_normal_impl( + 12 => wire__crate__api__simple__simple_adder_twin_normal_impl( port, ptr, rust_vec_len, @@ -902,7 +988,12 @@ fn pde_ffi_dispatcher_sync_impl( rust_vec_len, data_len, ), - 8 => wire__crate__api__plugin__PluginCallback_default_impl(ptr, rust_vec_len, data_len), + 8 => wire__crate__api__plugin__PluginCallback_change_table_access_impl( + ptr, + rust_vec_len, + data_len, + ), + 9 => wire__crate__api__plugin__PluginCallback_default_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -965,8 +1056,8 @@ impl flutter_rust_bridge::IntoIntoDart impl flutter_rust_bridge::IntoDart for crate::api::plugin::StateFieldAccess { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self { - Self::Table => 0.into_dart(), - Self::TableName => 1.into_dart(), + Self::TableName => 0.into_dart(), + Self::Tables => 1.into_dart(), Self::Info => 2.into_dart(), Self::Players => 3.into_dart(), Self::TeamMembers => 4.into_dart(), @@ -1155,8 +1246,8 @@ impl SseEncode for crate::api::plugin::StateFieldAccess { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { ::sse_encode( match self { - crate::api::plugin::StateFieldAccess::Table => 0, - crate::api::plugin::StateFieldAccess::TableName => 1, + crate::api::plugin::StateFieldAccess::TableName => 0, + crate::api::plugin::StateFieldAccess::Tables => 1, crate::api::plugin::StateFieldAccess::Info => 2, crate::api::plugin::StateFieldAccess::Players => 3, crate::api::plugin::StateFieldAccess::TeamMembers => 4, diff --git a/server/.gitignore b/server/.gitignore index 16cd4faf..7e280e4f 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -6,6 +6,7 @@ doc/ # Runtime files world.stnx packs/ +scripts/ setonix.db config.json diff --git a/server/lib/src/bloc.dart b/server/lib/src/bloc.dart index a71a1601..e490b193 100644 --- a/server/lib/src/bloc.dart +++ b/server/lib/src/bloc.dart @@ -16,6 +16,8 @@ Future _computeEvent( ); } +const scriptSuffix = '.lua'; + class WorldBloc extends Bloc with ServerInterface { final SetonixServer server; @@ -41,7 +43,6 @@ class WorldBloc extends Bloc ), ) { _pluginSystem = PluginSystem(server: this); - _serverPlugin = _pluginSystem.registerPlugin('', SetonixPlugin.new); on((event, emit) async { final signature = assetManager.createSignature(); final processed = await _computeEvent( @@ -53,10 +54,14 @@ class WorldBloc extends Bloc processed.responses.forEach(process); if (event is WorldInitialized) { server.log( - "World initialized${(event.info?.script != null) ? " with script ${event.info?.script}" : ""}", + "World initialized${(event.info?.gameMode != null) ? " with script ${event.info?.gameMode}" : ""}", level: LogLevel.info, ); - await _loadScript((newState ?? state).info.script); + _serverPlugin = await _pluginSystem.registerPlugin( + '', + SetonixPlugin.new, + ); + await _loadScripts((newState ?? state).info.gameMode); } if (newState == null) return; emit(newState); @@ -67,17 +72,62 @@ class WorldBloc extends Bloc }); } - Future _loadScript(String? script) async { + @override + void print(String message, [String? plugin]) { + if (plugin != null && plugin.isNotEmpty) { + server.log("[$plugin] $message", level: LogLevel.info); + } else { + server.log(message, level: LogLevel.info); + } + } + + Future _loadGameMode(ItemLocation location) async { + final mode = assetManager.getPack(location.namespace)?.getMode(location.id); + if (mode == null) return; + final script = mode.script; + if (script == null) return; + final scriptLocation = ItemLocation.fromString(script, location.namespace); + pluginSystem.loadLuaPluginFromLocation(assetManager, scriptLocation); + } + + Future _loadScripts(ItemLocation? mode) async { + pluginSystem.unregisterAll(); try { - if (script == null) return; - pluginSystem.loadLuaPlugin(assetManager, script); + if (mode != null) await _loadGameMode(mode); } catch (e) { server.log('Error loading script: $e', level: LogLevel.error); } + + final scriptsFolder = Directory('scripts'); + if (!await scriptsFolder.exists()) { + await scriptsFolder.create(recursive: true); + } + final scriptFiles = (await scriptsFolder.list().toList()) + .whereType() + .where((file) => file.path.endsWith(scriptSuffix)); + server.log( + "Found ${scriptFiles.length} script file(s)", + level: LogLevel.info, + ); + for (final file in scriptFiles) { + try { + final code = await file.readAsString(); + final relativePath = file.path.substring( + scriptsFolder.path.length + 1, + file.path.length - scriptSuffix.length, + ); + await pluginSystem.registerLuauPlugin(relativePath, code); + } catch (e) { + server.log( + 'Error loading script from ${file.path}: $e', + level: LogLevel.warning, + ); + } + } } Future init() async { - await _loadScript(state.info.script); + await _loadScripts(state.info.gameMode); } Future resetWorld([ItemLocation? mode]) async { @@ -87,7 +137,7 @@ class WorldBloc extends Bloc Future save({bool force = false}) async { var file = File( - worldName == defaultWorldName ? 'world.stnx' : 'worlds/$worldName.stnx', + '${worldName == defaultWorldName ? SetonixServer.defaultWorldName : '${SetonixServer.worldDirectory}/$worldName'}${SetonixServer.worldSuffix}', ); if (!await file.exists()) { await file.create(recursive: true); @@ -128,7 +178,7 @@ class WorldBloc extends Bloc worldName: worldName, ); if (!force) { - server.defaultEventSystem.fire(event); + server.defaultWorld.pluginSystem.fire(event); if (event.cancelled) return; server.log( 'Processing event by ${event.source}: ${limitOutput(event.clientEvent)}, answered with ${limitOutput(event.serverEvent)}', diff --git a/server/lib/src/config.dart b/server/lib/src/config.dart index 818119f7..ac3f9350 100644 --- a/server/lib/src/config.dart +++ b/server/lib/src/config.dart @@ -10,7 +10,9 @@ class ConfigManager { SetonixConfig _argsConfig = SetonixConfig(); ConfigManager({SetonixConfig? argsConfig, SetonixConfig? envConfig}) - : _envConfig = envConfig ?? SetonixConfig.fromEnvironment(); + : _envConfig = (envConfig ?? SetonixConfig.fromEnvironment()).merge( + argsConfig ?? SetonixConfig(), + ); SetonixConfig _mergeConfig() { return _config.merge(_envConfig).merge(_argsConfig); @@ -67,4 +69,10 @@ class ConfigManager { String get endpointSecret => _mergedConfig.endpointSecret ?? SetonixConfig.defaultEndpointSecret; + + ItemLocation? get gameMode { + final data = _mergedConfig.gameMode ?? SetonixConfig.defaultGameMode; + if (data.isEmpty) return null; + return ItemLocation.fromString(data); + } } diff --git a/server/lib/src/main.dart b/server/lib/src/main.dart index 2c6d1bb8..a22b01d9 100644 --- a/server/lib/src/main.dart +++ b/server/lib/src/main.dart @@ -60,6 +60,11 @@ ArgParser buildParser() { negatable: false, help: "Enable multi-world support", defaultsTo: false, + ) + ..addOption( + 'game-mode', + abbr: 'g', + help: 'The game mode to load. Otherwise it is a sandbox.', ); } @@ -81,6 +86,7 @@ Future runServer(List arguments, [ServerLoader? onLoad]) async { try { final ArgResults results = argParser.parse(arguments); bool verbose = false, autosave = false, multiWorld = false; + String? gameMode; int maxPlayers = 10; // Process the parsed arguments. @@ -112,6 +118,9 @@ Future runServer(List arguments, [ServerLoader? onLoad]) async { if (results.wasParsed('host')) { host = results['host']; } + if (results.wasParsed('game-mode')) { + gameMode = results['game-mode']; + } final server = await SetonixServer.load( argsConfig: SetonixConfig( host: host, @@ -120,6 +129,7 @@ Future runServer(List arguments, [ServerLoader? onLoad]) async { description: description, maxPlayers: maxPlayers, multiWorld: multiWorld, + gameMode: gameMode, ), ); await server.init(verbose: verbose); diff --git a/server/lib/src/programs/scripts.dart b/server/lib/src/programs/scripts.dart new file mode 100644 index 00000000..4a577ea2 --- /dev/null +++ b/server/lib/src/programs/scripts.dart @@ -0,0 +1,28 @@ +import 'package:consoler/consoler.dart'; +import 'package:setonix_server/setonix_server.dart'; + +class ScriptsProgram extends ConsoleProgram { + final SetonixServer server; + + ScriptsProgram(this.server); + + @override + String getDescription() => "Show all loaded scripts"; + + @override + String getUsage() => "[world]"; + + @override + void run(String label, List args) { + String world = defaultWorldName; + if (args.length > 1) return print("Usage: $label [world]"); + if (args.length == 1) world = args[0]; + print("-----"); + final scripts = server.getWorld(world)?.pluginSystem.plugins.toList() ?? []; + print("Loaded ${scripts.length} script(s)."); + for (final script in scripts) { + print("> $script"); + } + print("-----"); + } +} diff --git a/server/lib/src/server.dart b/server/lib/src/server.dart index dcee2ed3..3289861d 100644 --- a/server/lib/src/server.dart +++ b/server/lib/src/server.dart @@ -14,6 +14,7 @@ import 'package:setonix_server/src/programs/players.dart'; import 'package:setonix_server/src/programs/reset.dart'; import 'package:setonix_server/src/programs/save.dart'; import 'package:setonix_server/src/programs/say.dart'; +import 'package:setonix_server/src/programs/scripts.dart'; import 'package:setonix_server/src/programs/stop.dart'; import 'package:setonix_plugin/setonix_plugin.dart'; import 'package:setonix_server/src/programs/whitelist.dart'; @@ -29,6 +30,10 @@ String limitOutput(Object? value, [int limit = 500]) { } final class SetonixServer { + static const String defaultWorldName = 'world'; + static const String worldDirectory = 'worlds'; + static const String worldSuffix = '.stnx'; + final Consoler consoler; final ConfigManager configManager; final ServerAssetManager assetManager; @@ -51,8 +56,16 @@ final class SetonixServer { )); SetonixData _buildDefaultWorld() { - final data = SetonixData.empty().setInfo( - GameInfo(packs: assetManager.getPackIds().toList()), + final location = configManager.gameMode; + PackItem? gameMode; + if (location != null) { + gameMode = assetManager + .getPack(location.namespace) + ?.getModeItem(location.id, location.namespace); + } + final data = SetonixData.fromMode( + gameMode, + packs: assetManager.getPackIds().toSet(), ); return data; } @@ -125,8 +138,6 @@ final class SetonixServer { void log(Object? message, {LogLevel? level}) => consoler.print(message, level: level); - static final String defaultWorldFile = 'world.stnx'; - Map get players => Map.fromEntries( (_server?.clientConnections ?? {}).map( (e) => MapEntry(e, _server!.getConnectionInfo(e)!), @@ -209,8 +220,10 @@ final class SetonixServer { 'reset': ResetProgram(this), 'kick': KickProgram(this), 'whitelist': WhitelistProgram(this), + 'scripts': ScriptsProgram(this), null: UnknownProgram(), }); + await loadWorlds(); } void _onClientEvent( @@ -275,6 +288,53 @@ final class SetonixServer { challengeManager?.removeChallenge(user); } + Future loadWorlds() async { + Map worlds = {}; + final defaultFile = File('$worldDirectory/$defaultWorldName$worldSuffix'); + if (await defaultFile.exists()) { + try { + final bytes = await defaultFile.readAsBytes(); + worlds[defaultWorldName] = SetonixData.fromData(bytes); + } catch (e) { + log( + 'Error loading default world from ${defaultFile.path}: $e', + level: LogLevel.warning, + ); + } + } else { + worlds[defaultWorldName] = _buildDefaultWorld(); + log('No default world found, creating new one', level: LogLevel.info); + } + final dir = Directory(worldDirectory); + if (await dir.exists()) { + await for (final file in dir.list(recursive: false)) { + if (file is! File || !file.path.endsWith(worldSuffix)) continue; + final name = file.path.substring( + dir.path.length + 1, + file.path.length - worldSuffix.length, + ); + if (name == defaultWorldName) continue; + try { + final bytes = await file.readAsBytes(); + worlds[name] = SetonixData.fromData(bytes); + } catch (e) { + log( + 'Error loading world $name from ${file.path}: $e', + level: LogLevel.warning, + ); + } + } + } + for (final entry in worlds.entries) { + final name = entry.key; + final data = entry.value; + final bloc = WorldBloc(data, this, name); + _worlds[name] = bloc; + await bloc.init(); + log('Loaded world $name', level: LogLevel.info); + } + } + Future saveAll({bool force = false}) async { await Future.wait(_worlds.values.map((e) => e.save(force: force))); } diff --git a/tools/build_server.dart b/tools/build_server.dart deleted file mode 100644 index 8b137891..00000000 --- a/tools/build_server.dart +++ /dev/null @@ -1 +0,0 @@ -