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 | +} |