Skip to content

Commit

Permalink
Enforce packs on client
Browse files Browse the repository at this point in the history
  • Loading branch information
CodeDoctorDE committed Aug 26, 2024
1 parent 015b702 commit 8fc1b30
Show file tree
Hide file tree
Showing 16 changed files with 160 additions and 94 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,9 @@ jobs:
steps:
- name: ⬆️ Checkout
uses: actions/checkout@v4
- name: Install yq
if: ${{ matrix.os.name == 'windows-2022' }}
run: choco install yq
- uses: subosito/[email protected]
with:
flutter-version-file: app/pubspec.yaml
Expand Down
22 changes: 21 additions & 1 deletion api/lib/src/event/process/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,30 @@ bool isValidServerEvent(ServerWorldEvent event, WorldState state) =>
_ => true,
};

WorldState? processServerEvent(ServerWorldEvent event, WorldState state) {
sealed class FatalServerEventError {}

final class InvalidPacksError extends FatalServerEventError {
final Map<String, String> signature;

InvalidPacksError({required this.signature});

@override
String toString() =>
'Server requested packs, that are not available on the client: $signature';
}

WorldState? processServerEvent(
ServerWorldEvent event,
WorldState state, {
required AssetManager assetManager,
}) {
if (!isValidServerEvent(event, state)) return null;
switch (event) {
case WorldInitialized event:
final supported = assetManager.isServerSupported(event.packsSignature);
if (!supported) {
throw InvalidPacksError(signature: event.packsSignature);
}
return state.copyWith(
table: event.table,
id: event.id,
Expand Down
2 changes: 2 additions & 0 deletions api/lib/src/event/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ final class WorldInitialized extends ServerWorldEvent
final GameTable table;
final Map<String, Set<Channel>> teamMembers;
final Channel id;
final Map<String, String> packsSignature;

WorldInitialized({
required this.table,
this.teamMembers = const {},
this.id = kAuthorityChannel,
this.packsSignature = const {},
});
}

Expand Down
11 changes: 11 additions & 0 deletions api/lib/src/services/asset.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,15 @@ import 'package:quokka_api/quokka_api.dart';

abstract class AssetManager {
QuokkaData? getPack(String key);

bool isServerSupported(Map<String, String> signature) {
if (signature.isEmpty) return false;
for (final entry in signature.entries) {
final pack = getPack(entry.key);
if (pack == null || pack.getChecksum().toString() != entry.value) {
return false;
}
}
return true;
}
}
7 changes: 7 additions & 0 deletions app/lib/bloc/multiplayer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,11 @@ class MultiplayerCubit extends Cubit<MultiplayerState> {
emit(MultiplayerDisconnectedState(error: e));
}
}

Future<void> raiseError(FatalServerEventError e) async {
final state = this.state;
if (state is! MultiplayerConnectedState) return;
await state.networker.close();
emit(MultiplayerDisconnectedState(error: e, oldState: state));
}
}
13 changes: 9 additions & 4 deletions app/lib/bloc/world/bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@ class WorldBloc extends Bloc<PlayableWorldEvent, ClientWorldState> {
..serverEvents.listen(_processEvent);

on<ServerWorldEvent>((event, emit) {
final newState = processServerEvent(event, state);
if (newState is! ClientWorldState) return null;
emit(newState);
return save();
try {
final newState =
processServerEvent(event, state, assetManager: assetManager);
if (newState is! ClientWorldState) return null;
emit(newState);
return save();
} on FatalServerEventError catch (e) {
state.multiplayer.raiseError(e);
}
});
on<ColorSchemeChanged>((event, emit) {
emit(state.copyWith(colorScheme: event.colorScheme));
Expand Down
11 changes: 0 additions & 11 deletions app/lib/helpers/asset.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,6 @@ class GameAssetManager extends AssetManager {
..forEach((_, v) => v.then((e) => e.dispose()))
..clear();

bool isServerSupported(Map<String, String> signature) {
if (signature.isEmpty) return false;
for (final entry in signature.entries) {
final pack = _loadedPacks[entry.key];
if (pack == null || pack.getChecksum().toString() != entry.value) {
return false;
}
}
return true;
}

void setAllowedPacks(Iterable<String> packs) {
_allowedPacks.clear();
_allowedPacks.addAll(packs);
Expand Down
3 changes: 2 additions & 1 deletion app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,6 @@
"importGameDescription": "Are you sure you want to import the game?",
"importTemplateDescription": "Are you sure you want to import the template?",
"export": "Export",
"editInfo": "Edit information"
"editInfo": "Edit information",
"invalidPacks": "The server requested packs that doesn't exist on this client"
}
137 changes: 77 additions & 60 deletions app/lib/pages/game/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ class _GamePageState extends State<GamePage> {

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return FutureBuilder<Blocs>(
future: _bloc,
builder: (context, snapshot) {
Expand All @@ -128,65 +127,9 @@ class _GamePageState extends State<GamePage> {
return const Center(child: CircularProgressIndicator());
}
if (state is MultiplayerDisconnectedState) {
return Scaffold(
body: Stack(
alignment: Alignment.center,
children: [
const DotsBackground(),
Card.filled(
child: Container(
constraints: const BoxConstraints(
maxWidth: LeapBreakpoints.large,
),
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalizations.of(context)
.disconnected,
textAlign: TextAlign.center,
style: theme.textTheme.headlineSmall,
),
Text(
AppLocalizations.of(context)
.disconnectedMessage,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton(
onPressed: () async =>
(await _bloc)?.$1.reconnect(),
child: Text(
AppLocalizations.of(context)
.reconnect),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () =>
GoRouter.of(context).go('/'),
child: Text(
AppLocalizations.of(context)
.home),
),
],
),
if (state.error != null) ...[
const SizedBox(height: 16),
Text(state.error.toString()),
],
],
),
),
),
),
],
),
return _GameErrorView(
state: state,
onReconnect: () async => (await _bloc)?.$1.reconnect(),
);
}
return Scaffold(
Expand Down Expand Up @@ -235,3 +178,77 @@ class _GamePageState extends State<GamePage> {
});
}
}

class _GameErrorView extends StatelessWidget {
final MultiplayerDisconnectedState state;
final VoidCallback onReconnect;

const _GameErrorView({
required this.state,
required this.onReconnect,
});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final error = state.error;
var message = AppLocalizations.of(context).disconnectedMessage;
if (error is FatalServerEventError) {
message = switch (error) {
InvalidPacksError() => AppLocalizations.of(context).invalidPacks,
};
}
return Scaffold(
body: Stack(
alignment: Alignment.center,
children: [
const DotsBackground(),
Card.filled(
child: Container(
constraints: const BoxConstraints(
maxWidth: LeapBreakpoints.large,
),
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalizations.of(context).disconnected,
textAlign: TextAlign.center,
style: theme.textTheme.headlineSmall,
),
Text(
message,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton(
onPressed: onReconnect,
child: Text(AppLocalizations.of(context).reconnect),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => GoRouter.of(context).go('/'),
child: Text(AppLocalizations.of(context).home),
),
],
),
if (state.error != null) ...[
const SizedBox(height: 16),
Text(state.error.toString()),
],
],
),
),
),
),
],
),
);
}
}
8 changes: 4 additions & 4 deletions app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -724,10 +724,10 @@ packages:
dependency: transitive
description:
name: mime
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
version: "1.0.6"
nested:
dependency: transitive
description:
Expand Down Expand Up @@ -989,10 +989,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974
sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
shared_preferences_foundation:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"sharp": "^0.33.5",
"typescript": "^5.5.4"
},
"packageManager": "pnpm@9.8.0",
"packageManager": "pnpm@9.9.0",
"devDependencies": {
"sass": "^1.77.8"
}
Expand Down
19 changes: 14 additions & 5 deletions server/lib/asset.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:quokka_server/console.dart';

class ServerAssetManager extends AssetManager {
final Map<String, QuokkaData> _packs = {};

static const _qkaExtension = 'qka';
Iterable<MapEntry<String, QuokkaData>> get packs => _packs.entries;

Future<void> init(
Expand All @@ -21,18 +21,27 @@ class ServerAssetManager extends AssetManager {
await for (final file in directory.list()) {
if (file is File) {
final data = QuokkaData.fromData(await file.readAsBytes());
final fileName = p.basenameWithoutExtension(file.path);
_packs[fileName] = data;
final fileName = p.basename(file.path);
final extension = fileName.split('.').last;
if (extension != _qkaExtension) {
console.print(
'WARNING: Invalid pack file extension: $fileName. Skipping.',
level: LogLevel.warning);
continue;
}
final name =
fileName.substring(0, fileName.length - _qkaExtension.length - 1);
_packs[name] = data;
}
}
final coreIncluded = _packs.containsKey('');
console.print(
'Loaded ${_packs.length} packs. ${coreIncluded ? '(with core pack)' : '(without core pack)'}',
'Loaded ${_packs.length} pack(s). ${coreIncluded ? '(with core pack)' : '(without core pack)'}',
level: LogLevel.info);
if (_packs.isEmpty) {
console.print('No packs loaded.', level: LogLevel.warning);
} else {
console.print('Loaded packs: ${_packs.keys.join(', ')}',
console.print('Loaded pack(s): ${_packs.keys.join(', ')}',
level: LogLevel.verbose);
}
}
Expand Down
3 changes: 2 additions & 1 deletion server/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ final class QuokkaServer extends Bloc<ServerWorldEvent, WorldState> {
metadata: data.getMetadataOrDefault(),
)) {
on<ServerWorldEvent>((event, emit) {
final newState = processServerEvent(event, state);
final newState =
processServerEvent(event, state, assetManager: assetManager);
if (newState == null) return null;
emit(newState);
return save();
Expand Down
5 changes: 3 additions & 2 deletions server/lib/programs/packs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ class PacksProgram extends ConsoleProgram {
void run(List<String> args) {
print("-----");
final packs = server.assetManager.packs.toList();
print("Loaded ${packs.length} packs.");
print("Loaded ${packs.length} pack(s).");
for (final pack in packs) {
final checksum = pack.value.getChecksum();
if (pack.key.isEmpty) {
print("| Core pack ($checksum)");
} else {
print("> ${pack.key} ($checksum)");
}
print("> ${pack.key} ($checksum)");
}
print("-----");
}
Expand Down
Loading

0 comments on commit 8fc1b30

Please sign in to comment.