Commit cc2a64eef93c59d3230cbd1ba67a82d47de3eabe
1 parent
3fb6dc86
Add CloudConfig convenience class for mods to interact with webprefs
Showing
1 changed file
with
573 additions
and
0 deletions
src/client/java/com/mumfrey/liteloader/modconfig/CloudConfig.java
0 → 100644
1 | +/* | |
2 | + * This file is part of LiteLoader. | |
3 | + * Copyright (C) 2012-16 Adam Mummery-Smith | |
4 | + * All Rights Reserved. | |
5 | + */ | |
6 | +package com.mumfrey.liteloader.modconfig; | |
7 | + | |
8 | +import java.io.File; | |
9 | +import java.lang.reflect.Field; | |
10 | +import java.lang.reflect.Modifier; | |
11 | +import java.util.ArrayList; | |
12 | +import java.util.Iterator; | |
13 | +import java.util.List; | |
14 | +import java.util.UUID; | |
15 | +import java.util.regex.Pattern; | |
16 | + | |
17 | +import com.mojang.authlib.GameProfile; | |
18 | +import com.mumfrey.liteloader.InitCompleteListener; | |
19 | +import com.mumfrey.liteloader.core.LiteLoader; | |
20 | +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; | |
21 | +import com.mumfrey.webprefs.WebPreferencesManager; | |
22 | +import com.mumfrey.webprefs.interfaces.IWebPreferences; | |
23 | + | |
24 | +import net.minecraft.client.Minecraft; | |
25 | +import net.minecraft.entity.player.EntityPlayer; | |
26 | + | |
27 | +/** | |
28 | + * Convenience class for mods to work with cloud-based config. This class | |
29 | + * encapsulates a typical lifecycle for working with the webprefs framework. | |
30 | + * | |
31 | + * <p>To use this class, simply create a subclass with fields you wish to | |
32 | + * synchronise. Fields can be {@link String}, <tt>int</tt>, <tt>float</tt> or | |
33 | + * <tt>boolean</tt>, <tt>transient</tt> and <tt>static</tt> fields are ignored. | |
34 | + * </p> | |
35 | + * | |
36 | + * <p>Since fields are accessed via reflection, an update interval is defined in | |
37 | + * order to reduce the overhead thus incurred. The default interval is 5 seconds | |
38 | + * which provides a good balance between reliable replication and reduction of | |
39 | + * overhead. Calling {@link #poll} temporarily reduces the update interval to 1 | |
40 | + * second until a response is received from the server. The interval is restored | |
41 | + * to the specified update interval once a value is received, or after a minute | |
42 | + * elapses with no new data from the server.</p> | |
43 | + */ | |
44 | +public abstract class CloudConfig | |
45 | +{ | |
46 | + /** | |
47 | + * Minecraft TPS | |
48 | + */ | |
49 | + public static final int TICKS_PER_SECOND = 20; | |
50 | + | |
51 | + /** | |
52 | + * 5 seconds | |
53 | + */ | |
54 | + public static final int DEFAULT_UPDATE_INTERVAL = 5 * CloudConfig.TICKS_PER_SECOND; | |
55 | + | |
56 | + /** | |
57 | + * Reduced update interval to use following a call to {@link #poll} | |
58 | + */ | |
59 | + private static final int POLL_UPDATE_INTERVAL = 1 * CloudConfig.TICKS_PER_SECOND; | |
60 | + | |
61 | + /** | |
62 | + * Amount of time to wait following a call to {@link #poll} before we revert | |
63 | + * to normal polling interval | |
64 | + */ | |
65 | + private static final int POLL_RESET_INTERVAL = 60 * CloudConfig.TICKS_PER_SECOND; | |
66 | + | |
67 | + /** | |
68 | + * Same as normal key pattern except that $ gets translated to . for key | |
69 | + */ | |
70 | + private static final Pattern keyPattern = Pattern.compile("^[a-z0-9_\\-\\$]{1,32}$"); | |
71 | + | |
72 | + /** | |
73 | + * A field value tracker | |
74 | + */ | |
75 | + abstract class TrackedField | |
76 | + { | |
77 | + protected final String name; | |
78 | + | |
79 | + private final Field handle; | |
80 | + | |
81 | + private boolean dirty; | |
82 | + | |
83 | + protected boolean initialUpdate = true; | |
84 | + | |
85 | + TrackedField(Field handle) | |
86 | + { | |
87 | + this.handle = handle; | |
88 | + this.name = handle.getName().replace("$", "."); | |
89 | + } | |
90 | + | |
91 | + @SuppressWarnings("unchecked") | |
92 | + protected <T> T getValue() throws IllegalAccessException | |
93 | + { | |
94 | + return (T)this.handle.get(CloudConfig.this); | |
95 | + } | |
96 | + | |
97 | + protected <T> void setValue(T value) throws IllegalAccessException | |
98 | + { | |
99 | + this.handle.set(CloudConfig.this, value); | |
100 | + this.dirty = true; | |
101 | + } | |
102 | + | |
103 | + @Override | |
104 | + public final String toString() | |
105 | + { | |
106 | + return this.name; | |
107 | + } | |
108 | + | |
109 | + final boolean isDirty() | |
110 | + { | |
111 | + boolean dirty = this.dirty; | |
112 | + this.dirty = false; | |
113 | + return dirty; | |
114 | + } | |
115 | + | |
116 | + abstract void sync() throws IllegalAccessException; | |
117 | + } | |
118 | + | |
119 | + /** | |
120 | + * String field tracker | |
121 | + */ | |
122 | + class TrackedStringField extends TrackedField | |
123 | + { | |
124 | + private String localValue; | |
125 | + | |
126 | + TrackedStringField(Field handle) throws IllegalAccessException | |
127 | + { | |
128 | + super(handle); | |
129 | + this.localValue = this.<String>getValue(); | |
130 | + } | |
131 | + | |
132 | + @Override | |
133 | + void sync() throws IllegalAccessException | |
134 | + { | |
135 | + String value = this.<String>getValue(); | |
136 | + if (this.initialUpdate) | |
137 | + { | |
138 | + this.initialUpdate = false; | |
139 | + } | |
140 | + else if (value != this.localValue && value != null) | |
141 | + { | |
142 | + CloudConfig.this.preferences.set(this.name, value); | |
143 | + } | |
144 | + else if (CloudConfig.this.preferences.has(this.name)) | |
145 | + { | |
146 | + String remoteValue = CloudConfig.this.preferences.get(this.name); | |
147 | + if (value != remoteValue) | |
148 | + { | |
149 | + value = remoteValue; | |
150 | + this.setValue(value); | |
151 | + } | |
152 | + } | |
153 | + | |
154 | + this.localValue = value; | |
155 | + } | |
156 | + } | |
157 | + | |
158 | + /** | |
159 | + * Integer field tracker | |
160 | + */ | |
161 | + class TrackedIntegerField extends TrackedField | |
162 | + { | |
163 | + private int localValue; | |
164 | + | |
165 | + TrackedIntegerField(Field handle) | |
166 | + { | |
167 | + super(handle); | |
168 | + } | |
169 | + | |
170 | + @Override | |
171 | + void sync() throws IllegalAccessException | |
172 | + { | |
173 | + int value = this.<Integer>getValue().intValue(); | |
174 | + if (this.initialUpdate) | |
175 | + { | |
176 | + this.initialUpdate = false; | |
177 | + } | |
178 | + else if (value != this.localValue) | |
179 | + { | |
180 | + CloudConfig.this.preferences.set(this.name, String.valueOf(value)); | |
181 | + } | |
182 | + else if (CloudConfig.this.preferences.has(this.name)) | |
183 | + { | |
184 | + int remoteValue = this.tryParse(CloudConfig.this.preferences.get(this.name), value); | |
185 | + if (value != remoteValue) | |
186 | + { | |
187 | + value = remoteValue; | |
188 | + this.setValue(value); | |
189 | + } | |
190 | + } | |
191 | + | |
192 | + this.localValue = value; | |
193 | + } | |
194 | + | |
195 | + private int tryParse(String string, int defaultValue) | |
196 | + { | |
197 | + try | |
198 | + { | |
199 | + return Integer.parseInt(string); | |
200 | + } | |
201 | + catch (NumberFormatException ex) | |
202 | + { | |
203 | + return defaultValue; | |
204 | + } | |
205 | + } | |
206 | + } | |
207 | + | |
208 | + /** | |
209 | + * Float field tracker | |
210 | + */ | |
211 | + class TrackedFloatField extends TrackedField | |
212 | + { | |
213 | + private float localValue; | |
214 | + | |
215 | + TrackedFloatField(Field handle) | |
216 | + { | |
217 | + super(handle); | |
218 | + } | |
219 | + | |
220 | + @Override | |
221 | + void sync() throws IllegalAccessException | |
222 | + { | |
223 | + float value = this.<Float>getValue().floatValue(); | |
224 | + if (this.initialUpdate) | |
225 | + { | |
226 | + this.initialUpdate = false; | |
227 | + } | |
228 | + else if (value != this.localValue) | |
229 | + { | |
230 | + CloudConfig.this.preferences.set(this.name, String.valueOf(value)); | |
231 | + } | |
232 | + else if (CloudConfig.this.preferences.has(this.name)) | |
233 | + { | |
234 | + float remoteValue = this.tryParse(CloudConfig.this.preferences.get(this.name), value); | |
235 | + if (value != remoteValue) | |
236 | + { | |
237 | + value = remoteValue; | |
238 | + this.setValue(value); | |
239 | + } | |
240 | + } | |
241 | + | |
242 | + this.localValue = value; | |
243 | + } | |
244 | + | |
245 | + private float tryParse(String string, float defaultValue) | |
246 | + { | |
247 | + try | |
248 | + { | |
249 | + return Float.parseFloat(string); | |
250 | + } | |
251 | + catch (NumberFormatException ex) | |
252 | + { | |
253 | + return defaultValue; | |
254 | + } | |
255 | + } | |
256 | + } | |
257 | + | |
258 | + /** | |
259 | + * Bool field tracker | |
260 | + */ | |
261 | + class TrackedBooleanField extends TrackedField | |
262 | + { | |
263 | + private boolean localValue; | |
264 | + | |
265 | + TrackedBooleanField(Field handle) throws IllegalAccessException | |
266 | + { | |
267 | + super(handle); | |
268 | + this.localValue = this.<Boolean>getValue().booleanValue(); | |
269 | + } | |
270 | + | |
271 | + @Override | |
272 | + void sync() throws IllegalAccessException | |
273 | + { | |
274 | + boolean value = this.<Boolean>getValue().booleanValue(); | |
275 | + if (this.initialUpdate) | |
276 | + { | |
277 | + this.initialUpdate = false; | |
278 | + } | |
279 | + else if (value != this.localValue) | |
280 | + { | |
281 | + CloudConfig.this.preferences.set(this.name, String.valueOf(value)); | |
282 | + } | |
283 | + else if (CloudConfig.this.preferences.has(this.name)) | |
284 | + { | |
285 | + boolean remoteValue = "true".equals(CloudConfig.this.preferences.get(this.name)); | |
286 | + if (value != remoteValue) | |
287 | + { | |
288 | + value = remoteValue; | |
289 | + this.setValue(value); | |
290 | + } | |
291 | + } | |
292 | + | |
293 | + this.localValue = value; | |
294 | + } | |
295 | + } | |
296 | + | |
297 | + static final class UpdateTicker implements InitCompleteListener | |
298 | + { | |
299 | + private static UpdateTicker instance; | |
300 | + | |
301 | + private final List<CloudConfig> prefs = new ArrayList<CloudConfig>(); | |
302 | + | |
303 | + static UpdateTicker getInstance() | |
304 | + { | |
305 | + if (UpdateTicker.instance == null) | |
306 | + { | |
307 | + UpdateTicker.instance = new UpdateTicker(); | |
308 | + LiteLoader.getInterfaceManager().registerListener(UpdateTicker.instance); | |
309 | + } | |
310 | + | |
311 | + return UpdateTicker.instance; | |
312 | + } | |
313 | + | |
314 | + private UpdateTicker() | |
315 | + { | |
316 | + } | |
317 | + | |
318 | + void register(CloudConfig pref) | |
319 | + { | |
320 | + this.prefs.add(pref); | |
321 | + } | |
322 | + | |
323 | + @Override | |
324 | + public void onTick(Minecraft minecraft, float partialTicks, boolean inGame, boolean clock) | |
325 | + { | |
326 | + for (CloudConfig pref : this.prefs) | |
327 | + { | |
328 | + if (clock) | |
329 | + { | |
330 | + pref.onTick(); | |
331 | + } | |
332 | + } | |
333 | + } | |
334 | + | |
335 | + @Override | |
336 | + public void onInitCompleted(Minecraft minecraft, LiteLoader loader) | |
337 | + { | |
338 | + for (CloudConfig pref : this.prefs) | |
339 | + { | |
340 | + pref.poll(); | |
341 | + } | |
342 | + } | |
343 | + | |
344 | + @Override | |
345 | + public final String getVersion() | |
346 | + { | |
347 | + return "N/A"; | |
348 | + } | |
349 | + | |
350 | + @Override | |
351 | + public final void init(File configPath) | |
352 | + { | |
353 | + } | |
354 | + | |
355 | + @Override | |
356 | + public final void upgradeSettings(String version, File configPath, File oldConfigPath) | |
357 | + { | |
358 | + } | |
359 | + | |
360 | + @Override | |
361 | + public final String getName() | |
362 | + { | |
363 | + return "UpdateTicker"; | |
364 | + } | |
365 | + } | |
366 | + | |
367 | + /** | |
368 | + * Web preferences manager (service in use) | |
369 | + */ | |
370 | + protected final WebPreferencesManager manager; | |
371 | + | |
372 | + /** | |
373 | + * Preferences instance | |
374 | + */ | |
375 | + protected final IWebPreferences preferences; | |
376 | + | |
377 | + /** | |
378 | + * Field trackers | |
379 | + */ | |
380 | + private final List<TrackedField> fields = new ArrayList<TrackedField>(); | |
381 | + | |
382 | + /** | |
383 | + * Update interval specified | |
384 | + */ | |
385 | + private final int desiredUpdateInterval; | |
386 | + | |
387 | + /** | |
388 | + * Current update interval, reduced following a call to {@link poll} in | |
389 | + * order to apply updates as soon as they arrive. | |
390 | + */ | |
391 | + private int updateInterval; | |
392 | + | |
393 | + /** | |
394 | + * Ticks since thing | |
395 | + */ | |
396 | + private int updateCounter, pendingResetCounter; | |
397 | + | |
398 | + protected CloudConfig(boolean privatePrefs) | |
399 | + { | |
400 | + this(WebPreferencesManager.getDefault(), privatePrefs, CloudConfig.DEFAULT_UPDATE_INTERVAL); | |
401 | + } | |
402 | + | |
403 | + protected CloudConfig(WebPreferencesManager manager, boolean privatePrefs) | |
404 | + { | |
405 | + this(manager, privatePrefs, CloudConfig.DEFAULT_UPDATE_INTERVAL); | |
406 | + } | |
407 | + | |
408 | + protected CloudConfig(WebPreferencesManager manager, boolean privatePrefs, int updateInterval) | |
409 | + { | |
410 | + this(manager, manager.getLocalPreferences(privatePrefs), updateInterval); | |
411 | + } | |
412 | + | |
413 | + protected CloudConfig(WebPreferencesManager manager, EntityPlayer player, boolean privatePrefs) | |
414 | + { | |
415 | + this(manager, player, privatePrefs, CloudConfig.DEFAULT_UPDATE_INTERVAL); | |
416 | + } | |
417 | + | |
418 | + protected CloudConfig(WebPreferencesManager manager, EntityPlayer player, boolean privatePrefs, int updateInterval) | |
419 | + { | |
420 | + this(manager, manager.getPreferences(player, privatePrefs), updateInterval); | |
421 | + } | |
422 | + | |
423 | + protected CloudConfig(WebPreferencesManager manager, GameProfile profile, boolean privatePrefs) | |
424 | + { | |
425 | + this(manager, profile, privatePrefs, CloudConfig.DEFAULT_UPDATE_INTERVAL); | |
426 | + } | |
427 | + | |
428 | + protected CloudConfig(WebPreferencesManager manager, GameProfile profile, boolean privatePrefs, int updateInterval) | |
429 | + { | |
430 | + this(manager, manager.getPreferences(profile, privatePrefs), updateInterval); | |
431 | + } | |
432 | + | |
433 | + protected CloudConfig(WebPreferencesManager manager, UUID uuid, boolean privatePrefs) | |
434 | + { | |
435 | + this(manager, uuid, privatePrefs, CloudConfig.DEFAULT_UPDATE_INTERVAL); | |
436 | + } | |
437 | + | |
438 | + protected CloudConfig(WebPreferencesManager manager, UUID uuid, boolean privatePrefs, int updateInterval) | |
439 | + { | |
440 | + this(manager, manager.getPreferences(uuid, privatePrefs), updateInterval); | |
441 | + } | |
442 | + | |
443 | + private CloudConfig(WebPreferencesManager manager, IWebPreferences preferences, int updateInterval) | |
444 | + { | |
445 | + this.manager = manager; | |
446 | + this.preferences = preferences; | |
447 | + this.updateInterval = this.desiredUpdateInterval = updateInterval; | |
448 | + this.initFields(); | |
449 | + } | |
450 | + | |
451 | + /** | |
452 | + * Force an update request to be sent to the server | |
453 | + */ | |
454 | + public final void poll() | |
455 | + { | |
456 | + this.preferences.poll(); | |
457 | + this.updateInterval = Math.min(this.desiredUpdateInterval, CloudConfig.POLL_UPDATE_INTERVAL); | |
458 | + this.pendingResetCounter = CloudConfig.POLL_RESET_INTERVAL; | |
459 | + } | |
460 | + | |
461 | + /** | |
462 | + * Force local changes to be pushed to the server | |
463 | + */ | |
464 | + public final void commit() | |
465 | + { | |
466 | + this.preferences.commit(false); | |
467 | + } | |
468 | + | |
469 | + private void initFields() | |
470 | + { | |
471 | + for (Field field : this.getClass().getDeclaredFields()) | |
472 | + { | |
473 | + int modifiers = field.getModifiers(); | |
474 | + if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers)) | |
475 | + { | |
476 | + LiteLoaderLogger.debug("Skipping transient field %s in %s", field.getName(), this.getClass().getName()); | |
477 | + } | |
478 | + | |
479 | + if (!CloudConfig.keyPattern.matcher(field.getName()).matches()) | |
480 | + { | |
481 | + LiteLoaderLogger.warning("Skipping field with invalid name %s in %s", field.getName(), this.getClass().getName()); | |
482 | + } | |
483 | + | |
484 | + Class<?> type = field.getType(); | |
485 | + try | |
486 | + { | |
487 | + field.setAccessible(true); | |
488 | + if (type == String.class) | |
489 | + { | |
490 | + this.fields.add(new TrackedStringField(field)); | |
491 | + continue; | |
492 | + } | |
493 | + if (type == Integer.TYPE) | |
494 | + { | |
495 | + this.fields.add(new TrackedIntegerField(field)); | |
496 | + continue; | |
497 | + } | |
498 | + if (type == Float.TYPE) | |
499 | + { | |
500 | + this.fields.add(new TrackedFloatField(field)); | |
501 | + continue; | |
502 | + } | |
503 | + if (type == Boolean.TYPE) | |
504 | + { | |
505 | + this.fields.add(new TrackedBooleanField(field)); | |
506 | + continue; | |
507 | + } | |
508 | + } | |
509 | + catch (IllegalAccessException ex) | |
510 | + { | |
511 | + LiteLoaderLogger.warning("Skipping inaccessible field %s in %s", field.getName(), this.getClass().getName()); | |
512 | + ex.printStackTrace(); | |
513 | + } | |
514 | + | |
515 | + LiteLoaderLogger.warning("Skipping field %s with unsupported type %s in %s", field.getName(), type, this.getClass().getName()); | |
516 | + } | |
517 | + | |
518 | + if (this.fields.size() > 0) | |
519 | + { | |
520 | + UpdateTicker.getInstance().register(this); | |
521 | + } | |
522 | + } | |
523 | + | |
524 | + final void onTick() | |
525 | + { | |
526 | + // Reset to desired interval if we don't receive an update following poll | |
527 | + if (this.pendingResetCounter > 0) | |
528 | + { | |
529 | + this.pendingResetCounter--; | |
530 | + if (this.pendingResetCounter == 0) | |
531 | + { | |
532 | + this.updateInterval = this.desiredUpdateInterval; | |
533 | + } | |
534 | + } | |
535 | + | |
536 | + if (++this.updateCounter > this.updateInterval) | |
537 | + { | |
538 | + this.updateCounter = 0; | |
539 | + | |
540 | + boolean dirty = false; | |
541 | + for (Iterator<TrackedField> iter = this.fields.iterator(); iter.hasNext();) | |
542 | + { | |
543 | + TrackedField field = iter.next(); | |
544 | + try | |
545 | + { | |
546 | + field.sync(); | |
547 | + dirty |= field.isDirty(); | |
548 | + } | |
549 | + catch (IllegalAccessException ex) | |
550 | + { | |
551 | + LiteLoaderLogger.warning("Removing invalid field %s in %s", field, this.getClass().getName()); | |
552 | + ex.printStackTrace(); | |
553 | + iter.remove(); | |
554 | + } | |
555 | + } | |
556 | + | |
557 | + if (dirty) | |
558 | + { | |
559 | + this.updateInterval = this.desiredUpdateInterval; | |
560 | + this.pendingResetCounter = 0; | |
561 | + this.onUpdated(); | |
562 | + } | |
563 | + } | |
564 | + } | |
565 | + | |
566 | + /** | |
567 | + * Stub for subclasses, called when any value change is received from the | |
568 | + * server | |
569 | + */ | |
570 | + protected void onUpdated() | |
571 | + { | |
572 | + } | |
573 | +} | ... | ... |