Skip to content

Commit 63cd3ad

Browse files
committed
Start adding multi world support to server
1 parent c449aea commit 63cd3ad

File tree

12 files changed

+249
-147
lines changed

12 files changed

+249
-147
lines changed

app/lib/bloc/world/bloc.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ class WorldBloc extends Bloc<PlayableWorldEvent, ClientWorldState> {
4545
onProcess: (p0, p1, [force = false]) {
4646
process(p1);
4747
},
48-
onSendEvent: (p0, p1) {
49-
_processEvent(p1);
48+
onSendEvent: (packet, worldName) {
49+
_processEvent(packet);
5050
},
5151
playersGetter: () => state.multiplayer.clients.toList(),
5252
stateGetter: () => state.world,

docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@astrojs/starlight": "^0.33.0",
1616
"@phosphor-icons/react": "^2.1.7",
1717
"@types/react": "^19.1.0",
18-
"@types/react-dom": "^19.1.1",
18+
"@types/react-dom": "^19.1.2",
1919
"astro": "^5.6.1",
2020
"react": "^19.1.0",
2121
"react-dom": "^19.1.0",

docs/pnpm-lock.yaml

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

metadata/en-US/changelogs/5.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
* Add importing system for editor
2-
* Add launch app link for android
2+
* Add launch app link for android ([#37](https://github.com/LinwoodDev/Setonix/issues/37))
33
* Add option for server to send link information
44
* Improve multiplayer ui
55
* Add swamp multiplayer support

plugin/lib/src/events/model.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,25 @@ import 'package:setonix_api/setonix_api.dart';
66

77
part 'model.mapper.dart';
88

9+
const defaultWorldName = '';
10+
911
base class Event<T extends WorldEvent> {
1012
final T clientEvent;
1113
final Channel source;
14+
final String worldName;
1215
ServerWorldEvent serverEvent;
1316
Channel target;
1417
bool cancelled = false;
1518
Set<Channel>? needsUpdate;
1619

17-
Event(this.serverEvent, this.target, this.clientEvent, this.source,
18-
this.needsUpdate);
20+
Event({
21+
required this.serverEvent,
22+
required this.target,
23+
required this.clientEvent,
24+
required this.source,
25+
this.worldName = defaultWorldName,
26+
this.needsUpdate,
27+
});
1928

2029
Event<C> castEvent<C extends WorldEvent>() {
2130
return _LinkedEvent<C>(this);
@@ -65,6 +74,9 @@ final class _LinkedEvent<T extends WorldEvent> implements Event<T> {
6574

6675
@override
6776
set needsUpdate(Set<Channel>? value) => parent.needsUpdate = value;
77+
78+
@override
79+
String get worldName => parent.worldName;
6880
}
6981

7082
final class ServerPing {

plugin/lib/src/plugin.dart

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ import 'package:setonix_plugin/src/rust/frb_generated.dart';
1010

1111
typedef PluginProcessCallback = void Function(String, WorldEvent, [bool force]);
1212
typedef PluginSendEventCallback = void Function(
13-
String, NetworkerPacket<PlayableWorldEvent>);
13+
NetworkerPacket<PlayableWorldEvent> packet, String? worldName);
1414

1515
final class PluginSystem {
1616
final Map<
1717
String,
1818
(
1919
SetonixPlugin,
2020
StreamSubscription<ProcessMessage>,
21-
StreamSubscription<NetworkerPacket<PlayableWorldEvent>>
21+
StreamSubscription<SentEvent>
2222
)> _plugins = {};
2323
final PluginProcessCallback _onProcess;
2424
final PluginSendEventCallback _onSendEvent;
@@ -36,8 +36,8 @@ final class PluginSystem {
3636
void registerPlugin(String name, SetonixPlugin plugin) {
3737
final processSub = plugin.onProcess
3838
.listen((message) => _onProcess(name, message.event, message.force));
39-
final sendSub =
40-
plugin.onSendEvent.listen((event) => _onSendEvent(name, event));
39+
final sendSub = plugin.onSendEvent
40+
.listen((event) => _onSendEvent(event.event, event.worldName));
4141
_plugins[name] = (plugin, processSub, sendSub);
4242
}
4343

@@ -102,22 +102,30 @@ final class ProcessMessage {
102102
ProcessMessage(this.event, this.force);
103103
}
104104

105+
final class SentEvent {
106+
final NetworkerPacket<PlayableWorldEvent> event;
107+
final String? worldName;
108+
109+
SentEvent(this.event, [this.worldName]);
110+
}
111+
105112
class SetonixPlugin {
106113
final EventSystem eventSystem = EventSystem();
107114
final StreamController<ProcessMessage> _onProcessController =
108115
StreamController.broadcast();
109-
final StreamController<NetworkerPacket<PlayableWorldEvent>>
110-
_onSendEventController = StreamController.broadcast();
116+
final StreamController<SentEvent> _onSendEventController =
117+
StreamController.broadcast();
111118

112119
Stream<ProcessMessage> get onProcess => _onProcessController.stream;
113-
Stream<NetworkerPacket<PlayableWorldEvent>> get onSendEvent =>
114-
_onSendEventController.stream;
120+
Stream<SentEvent> get onSendEvent => _onSendEventController.stream;
115121

116122
void process(WorldEvent event, {bool force = false}) =>
117123
_onProcessController.add(ProcessMessage(event, force));
118124

119-
void sendEvent(PlayableWorldEvent event, [Channel target = kAnyChannel]) =>
120-
_onSendEventController.add(NetworkerPacket(event, target));
125+
void sendEvent(PlayableWorldEvent event,
126+
{Channel target = kAnyChannel, String? worldName}) =>
127+
_onSendEventController
128+
.add(SentEvent(NetworkerPacket(event, target), worldName));
121129

122130
void dispose() {
123131
eventSystem.dispose();
@@ -148,7 +156,7 @@ final class RustSetonixPlugin extends SetonixPlugin {
148156
});
149157
callback.changeSendEvent(sendEvent: (eventSerizalized, target) {
150158
final event = PlayableWorldEventMapper.fromJson(eventSerizalized);
151-
instance.sendEvent(event, target ?? kAnyChannel);
159+
instance.sendEvent(event, target: target ?? kAnyChannel);
152160
});
153161
callback.changeStateFieldAccess(stateFieldAccess: (field) {
154162
final state = pluginSystem.stateGetter();

server/lib/setonix_server.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ library;
44
export 'package:setonix_api/setonix_api.dart';
55
export 'package:setonix_plugin/setonix_plugin.dart';
66
export 'src/asset.dart';
7+
export 'src/bloc.dart';
78
export 'src/main.dart';
89
export 'src/server.dart';
910
export 'src/programs/kick.dart';

server/lib/src/bloc.dart

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import 'dart:io';
2+
import 'dart:isolate';
3+
4+
import 'package:bloc/bloc.dart';
5+
import 'package:bloc_concurrency/bloc_concurrency.dart';
6+
import 'package:consoler/consoler.dart';
7+
import 'package:setonix_server/setonix_server.dart';
8+
9+
Future<ServerProcessed> _computeEvent(ServerWorldEvent event, WorldState state,
10+
List<SignatureMetadata> signature) {
11+
return Isolate.run(
12+
() => processServerEvent(event, state, signature: signature));
13+
}
14+
15+
class WorldBloc extends Bloc<PlayableWorldEvent, WorldState> {
16+
final SetonixServer server;
17+
final String worldName;
18+
19+
ServerAssetManager get assetManager => server.assetManager;
20+
21+
bool get autosave => server.autosave;
22+
23+
WorldBloc(SetonixData data, this.server, this.worldName)
24+
: super(WorldState(
25+
data: data,
26+
table: data.getTableOrDefault(),
27+
metadata: data.getMetadataOrDefault(),
28+
info: data.getInfoOrDefault(),
29+
)) {
30+
on<ServerWorldEvent>((event, emit) async {
31+
final signature = assetManager.createSignature();
32+
final processed =
33+
await _computeEvent(event, state, signature.values.toList());
34+
final newState = processed.state;
35+
processed.responses.forEach(process);
36+
if (event is WorldInitialized) {
37+
server.log(
38+
"World initialized${(event.info?.script != null) ? " with script ${event.info?.script}" : ""}",
39+
level: LogLevel.info);
40+
await _loadScript((newState ?? state).info.script);
41+
}
42+
if (newState == null) return;
43+
emit(newState);
44+
return save();
45+
}, transformer: sequential());
46+
on<ImagesUpdated>((event, emit) {
47+
emit(state.copyWith(images: event.images));
48+
});
49+
}
50+
51+
Future<void> _loadScript(String? script) async {
52+
try {
53+
if (script == null) return;
54+
server.pluginSystem.loadLuaPlugin(assetManager, script);
55+
} catch (e) {
56+
server.log('Error loading script: $e', level: LogLevel.error);
57+
}
58+
}
59+
60+
Future<void> init() async {
61+
await _loadScript(state.info.script);
62+
}
63+
64+
Future<void> resetWorld([ItemLocation? mode]) async {
65+
process(ModeChangeRequest(mode));
66+
await stream.first;
67+
}
68+
69+
Future<void> save({bool force = false}) async {
70+
var file = File(worldName == defaultWorldName
71+
? 'world.stnx'
72+
: 'worlds/$worldName.stnx');
73+
if (!await file.exists()) {
74+
await file.create(recursive: true);
75+
}
76+
if (!force && autosave) return;
77+
final bytes = state.save().exportAsBytes();
78+
await file.writeAsBytes(bytes);
79+
}
80+
81+
void onClientEvent(NetworkerPacket<WorldEvent> packet,
82+
{bool force = false}) async {
83+
final data = packet.data;
84+
ServerResponse? process;
85+
try {
86+
process = processClientEvent(
87+
data is UserJoined ? null : data,
88+
packet.channel,
89+
state,
90+
assetManager: assetManager,
91+
allowServerEvents: packet.isServer,
92+
);
93+
} catch (e) {
94+
server.log('Error processing event: $e', level: LogLevel.error);
95+
}
96+
if (process == null) return;
97+
final event = Event(
98+
serverEvent: process.main.data,
99+
target: process.main.channel,
100+
clientEvent: data,
101+
source: packet.channel,
102+
needsUpdate: process.needsUpdate,
103+
worldName: worldName,
104+
);
105+
if (!force) {
106+
server.eventSystem.fire(event);
107+
if (event.cancelled) return;
108+
server.log(
109+
'Processing event by ${event.source}: ${limitOutput(event.clientEvent)}, answered with ${limitOutput(event.serverEvent)}',
110+
level: LogLevel.verbose);
111+
}
112+
switch (packet.data) {
113+
case MessageRequest data:
114+
server.log("Message by ${packet.channel}: ${data.message}",
115+
level: LogLevel.info);
116+
default:
117+
}
118+
server.sendEvent(event.serverEvent,
119+
target: event.target, worldName: worldName);
120+
final updatePackets = process.buildUpdatePacketsFor(
121+
state, server.channels, event.needsUpdate);
122+
for (final packet in updatePackets) {
123+
sendEvent(packet.data, target: packet.channel);
124+
}
125+
}
126+
127+
void process(WorldEvent event, [bool force = true]) {
128+
onClientEvent(NetworkerPacket(event, kAuthorityChannel), force: force);
129+
}
130+
131+
void sendEvent(PlayableWorldEvent event, {required Channel target}) {
132+
server.sendEvent(event, target: target, worldName: worldName);
133+
}
134+
}

server/lib/src/main.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ ArgParser buildParser() {
4141
abbr: 'a', help: "Disable saving of the world automatically")
4242
..addOption('max-players',
4343
abbr: 'm', help: "Maximum number of players", defaultsTo: '10')
44-
..addOption('room-mode',
45-
abbr: 'r', help: "Enable room mode", defaultsTo: 'false');
44+
..addFlag('multi-world',
45+
abbr: 'w',
46+
negatable: false,
47+
help: "Enable multi-world support",
48+
defaultsTo: false);
4649
}
4750

4851
void printUsage(ArgParser argParser) {
@@ -62,7 +65,7 @@ Future<void> runServer(List<String> arguments, [ServerLoader? onLoad]) async {
6265
final ArgParser argParser = buildParser();
6366
try {
6467
final ArgResults results = argParser.parse(arguments);
65-
bool verbose = false, autosave = false, roomMode = false;
68+
bool verbose = false, autosave = false, multiWorld = false;
6669
int maxPlayers = 10;
6770

6871
// Process the parsed arguments.
@@ -80,12 +83,12 @@ Future<void> runServer(List<String> arguments, [ServerLoader? onLoad]) async {
8083
if (results.wasParsed('autosave')) {
8184
autosave = true;
8285
}
86+
if (results.wasParsed('multi-world')) {
87+
multiWorld = true;
88+
}
8389
if (results.wasParsed('max-players')) {
8490
maxPlayers = int.tryParse(results['max-players'] ?? '') ?? maxPlayers;
8591
}
86-
if (results.wasParsed('room-mode')) {
87-
roomMode = true;
88-
}
8992
String description = '';
9093
if (results.wasParsed('description')) {
9194
description = results['description'];
@@ -97,7 +100,7 @@ Future<void> runServer(List<String> arguments, [ServerLoader? onLoad]) async {
97100
autosave: autosave,
98101
description: description,
99102
maxPlayers: maxPlayers,
100-
roomMode: roomMode,
103+
multiWorld: multiWorld,
101104
);
102105
await onLoad?.call(server);
103106
await server.run();

0 commit comments

Comments
 (0)