31
31
import org .checkerframework .checker .nullness .qual .NonNull ;
32
32
import org .geysermc .api .util .ApiVersion ;
33
33
import org .geysermc .geyser .GeyserImpl ;
34
+ import org .geysermc .geyser .GeyserLogger ;
34
35
import org .geysermc .geyser .api .GeyserApi ;
35
36
import org .geysermc .geyser .api .event .ExtensionEventBus ;
36
37
import org .geysermc .geyser .api .extension .Extension ;
42
43
import org .geysermc .geyser .api .extension .exception .InvalidExtensionException ;
43
44
import org .geysermc .geyser .extension .event .GeyserExtensionEventBus ;
44
45
import org .geysermc .geyser .text .GeyserLocale ;
46
+ import org .geysermc .geyser .util .ThrowingBiConsumer ;
45
47
46
48
import java .io .IOException ;
47
49
import java .io .Reader ;
51
53
import java .nio .file .NoSuchFileException ;
52
54
import java .nio .file .Path ;
53
55
import java .nio .file .StandardCopyOption ;
56
+ import java .util .ArrayList ;
54
57
import java .util .HashMap ;
55
58
import java .util .LinkedHashMap ;
56
59
import java .util .List ;
57
60
import java .util .Map ;
61
+ import java .util .function .BiConsumer ;
58
62
import java .util .regex .Pattern ;
59
63
60
64
@ RequiredArgsConstructor
@@ -155,6 +159,7 @@ void setClass(String name, final Class<?> clazz) {
155
159
156
160
@ Override
157
161
protected void loadAllExtensions (@ NonNull ExtensionManager extensionManager ) {
162
+ GeyserLogger logger = GeyserImpl .getInstance ().getLogger ();
158
163
try {
159
164
if (Files .notExists (extensionsDirectory )) {
160
165
Files .createDirectory (extensionsDirectory );
@@ -163,55 +168,68 @@ protected void loadAllExtensions(@NonNull ExtensionManager extensionManager) {
163
168
Map <String , Path > extensions = new LinkedHashMap <>();
164
169
Map <String , GeyserExtensionContainer > loadedExtensions = new LinkedHashMap <>();
165
170
166
- Pattern [] extensionFilters = this .extensionFilters ();
167
- List <Path > extensionPaths = Files .walk (extensionsDirectory ).toList ();
168
- extensionPaths .forEach (path -> {
169
- if (Files .isDirectory (path )) {
171
+ Path updateDirectory = extensionsDirectory .resolve ("update" );
172
+ if (Files .isDirectory (updateDirectory )) {
173
+ // Step 1: Collect the extension files that currently exist so they can be replaced
174
+ Map <String , List <Path >> extensionFiles = new HashMap <>();
175
+ this .processExtensionsFolder (extensionsDirectory , (path , description ) -> {
176
+ extensionFiles .computeIfAbsent (description .id (), k -> new ArrayList <>()).add (path );
177
+ }, (path , e ) -> {
178
+ // this file will throw again when we actually try to load extensions, and it will be handled there
179
+ });
180
+
181
+ // Step 2: Move the updated/new extensions
182
+ this .processExtensionsFolder (updateDirectory , (path , description ) -> {
183
+ // Remove the old extension files with the same ID if it exists
184
+ List <Path > oldExtensionFiles = extensionFiles .get (description .id ());
185
+ if (oldExtensionFiles != null ) {
186
+ for (Path oldExtensionFile : oldExtensionFiles ) {
187
+ Files .delete (oldExtensionFile );
188
+ }
189
+ }
190
+
191
+ // Overwrite the extension with the new jar
192
+ Files .move (path , extensionsDirectory .resolve (path .getFileName ()), StandardCopyOption .REPLACE_EXISTING );
193
+ }, (path , e ) -> {
194
+ logger .error (GeyserLocale .getLocaleStringLog ("geyser.extensions.update.failed" , path .getFileName ()), e );
195
+ });
196
+ }
197
+
198
+ // Step 3: Load the extensions
199
+ this .processExtensionsFolder (extensionsDirectory , (path , description ) -> {
200
+ String name = description .name ();
201
+ String id = description .id ();
202
+ if (extensions .containsKey (id ) || extensionManager .extension (id ) != null ) {
203
+ logger .warning (GeyserLocale .getLocaleStringLog ("geyser.extensions.load.duplicate" , name , path .toString ()));
170
204
return ;
171
205
}
172
206
173
- for (Pattern filter : extensionFilters ) {
174
- if (!filter .matcher (path .getFileName ().toString ()).matches ()) {
207
+ // Check whether an extensions' requested api version is compatible
208
+ ApiVersion .Compatibility compatibility = GeyserApi .api ().geyserApiVersion ().supportsRequestedVersion (
209
+ description .humanApiVersion (),
210
+ description .majorApiVersion (),
211
+ description .minorApiVersion ()
212
+ );
213
+
214
+ if (compatibility != ApiVersion .Compatibility .COMPATIBLE ) {
215
+ // Workaround for the switch to the Geyser API version instead of the Base API version in extensions
216
+ if (compatibility == ApiVersion .Compatibility .HUMAN_DIFFER && description .humanApiVersion () == 1 ) {
217
+ logger .warning ("The extension %s requested the Base API version %s, which is deprecated in favor of specifying the Geyser API version. Please update the extension, or contact its developer."
218
+ .formatted (name , description .apiVersion ()));
219
+ } else {
220
+ logger .error (GeyserLocale .getLocaleStringLog ("geyser.extensions.load.failed_api_version" , name , description .apiVersion ()));
175
221
return ;
176
222
}
177
223
}
178
224
179
- try {
180
- GeyserExtensionDescription description = this .extensionDescription (path );
181
-
182
- String name = description .name ();
183
- String id = description .id ();
184
- if (extensions .containsKey (id ) || extensionManager .extension (id ) != null ) {
185
- GeyserImpl .getInstance ().getLogger ().warning (GeyserLocale .getLocaleStringLog ("geyser.extensions.load.duplicate" , name , path .toString ()));
186
- return ;
187
- }
188
-
189
- // Check whether an extensions' requested api version is compatible
190
- ApiVersion .Compatibility compatibility = GeyserApi .api ().geyserApiVersion ().supportsRequestedVersion (
191
- description .humanApiVersion (),
192
- description .majorApiVersion (),
193
- description .minorApiVersion ()
194
- );
195
-
196
- if (compatibility != ApiVersion .Compatibility .COMPATIBLE ) {
197
- // Workaround for the switch to the Geyser API version instead of the Base API version in extensions
198
- if (compatibility == ApiVersion .Compatibility .HUMAN_DIFFER && description .humanApiVersion () == 1 ) {
199
- GeyserImpl .getInstance ().getLogger ().warning ("The extension %s requested the Base API version %s, which is deprecated in favor of specifying the Geyser API version. Please update the extension, or contact its developer."
200
- .formatted (name , description .apiVersion ()));
201
- } else {
202
- GeyserImpl .getInstance ().getLogger ().error (GeyserLocale .getLocaleStringLog ("geyser.extensions.load.failed_api_version" , name , description .apiVersion ()));
203
- return ;
204
- }
205
- }
206
-
207
- GeyserExtensionContainer container = this .loadExtension (path , description );
208
- extensions .put (id , path );
209
- loadedExtensions .put (id , container );
210
- } catch (Throwable e ) {
211
- GeyserImpl .getInstance ().getLogger ().error (GeyserLocale .getLocaleStringLog ("geyser.extensions.load.failed_with_name" , path .getFileName (), path .toAbsolutePath ()), e );
212
- }
225
+ GeyserExtensionContainer container = this .loadExtension (path , description );
226
+ extensions .put (id , path );
227
+ loadedExtensions .put (id , container );
228
+ }, (path , e ) -> {
229
+ logger .error (GeyserLocale .getLocaleStringLog ("geyser.extensions.load.failed_with_name" , path .getFileName (), path .toAbsolutePath ()), e );
213
230
});
214
231
232
+ // Step 4: Register the extensions
215
233
for (GeyserExtensionContainer container : loadedExtensions .values ()) {
216
234
this .extensionContainers .put (container .extension (), container );
217
235
this .register (container .extension (), extensionManager );
@@ -221,6 +239,40 @@ protected void loadAllExtensions(@NonNull ExtensionManager extensionManager) {
221
239
}
222
240
}
223
241
242
+ /**
243
+ * Process extension jars in a folder and call the accept or reject consumer based on the result
244
+ *
245
+ * @param directory the directory to process
246
+ * @param accept the consumer to call when an extension is accepted
247
+ * @param reject the consumer to call when an extension is rejected
248
+ * @throws IOException if an I/O error occurs
249
+ */
250
+ private void processExtensionsFolder (Path directory , ThrowingBiConsumer <Path , GeyserExtensionDescription > accept , BiConsumer <Path , Throwable > reject ) throws IOException {
251
+ List <Path > extensionPaths = Files .list (directory ).toList ();
252
+ Pattern [] extensionFilters = this .extensionFilters ();
253
+ extensionPaths .forEach (path -> {
254
+ if (Files .isDirectory (path )) {
255
+ return ;
256
+ }
257
+
258
+ // Only look at files that meet the extension filter
259
+ for (Pattern filter : extensionFilters ) {
260
+ if (!filter .matcher (path .getFileName ().toString ()).matches ()) {
261
+ return ;
262
+ }
263
+ }
264
+
265
+ try {
266
+ // Try load the description, so we know it's a valid extension
267
+ GeyserExtensionDescription description = this .extensionDescription (path );
268
+
269
+ accept .acceptThrows (path , description );
270
+ } catch (Throwable e ) {
271
+ reject .accept (path , e );
272
+ }
273
+ });
274
+ }
275
+
224
276
@ Override
225
277
protected boolean isEnabled (@ NonNull Extension extension ) {
226
278
return this .extensionContainers .get (extension ).enabled ;
0 commit comments