mirror of
https://github.com/PaperMC/Paper.git
synced 2025-08-15 20:23:53 -07:00
[Bleeding] Added a Metadata framework for Entities, Blocks, and Worlds
This metadata implementation has the following features: - All metadata is lazy. Metadata values are not actually computed until another plugin requests them. Memory and CPU are conserved by not computing and storing unnecessary metadata values. - All metadata is cached. Once a metadata value is computed its value is cached in the metadata store to prevent further unnecessary computation. An invalidation mechanism is provided to flush the cache and force recompilation of metadata values. - All metadata is stored in basic data types. Convenience methods in the MetadataValue class allow for the conversion of metadata data types when possible. Restricting metadata to basic data types prevents the accidental linking of large object graphs into metadata. Metadata is persistent across the lifetime of the application and adding large object graphs would damage garbage collector performance. - Metadata access is thread safe. Care has been taken to protect the internal data structures and access them in a thread safe manner. - Metadata is exposed for all objects that descend from Entity, Block, and World. All Entity and World metadata is stored at the Server level and all Block metadata is stored at the World level. - Metadata is NOT keyed on references to original objects - instead metadata is keyed off of unique fields within those objects. Doing this allows metadata to exist for blocks that are in chunks not currently in memory. Additionally, Player objects are keyed off of player name so that Player metadata remains consistent between logins. - Metadata convenience methods have been added to all Entities, Players, Blocks, BlockStates, and World allowing direct access to an individual instance's metadata. - Players and OfflinePlayers share a single metadata store, allowing player metadata to be manipulated regardless of the player's current online status. By: rmichela <deltahat@gmail.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package org.bukkit.metadata;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* A FixedMetadataValue is a special case metadata item that contains the same value forever after initialization.
|
||||
* Invalidating a FixedMetadataValue has no affect.
|
||||
*/
|
||||
public class FixedMetadataValue extends LazyMetadataValue {
|
||||
/**
|
||||
* Initializes a FixedMetadataValue with an Object
|
||||
*
|
||||
* @param owningPlugin the {@link Plugin} that created this metadata value.
|
||||
* @param value the value assigned to this metadata value.
|
||||
*/
|
||||
public FixedMetadataValue(Plugin owningPlugin, final Object value) {
|
||||
super(owningPlugin, CacheStrategy.CACHE_ETERNALLY, new Callable<Object>() {
|
||||
public Object call() throws Exception {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,158 @@
|
||||
package org.bukkit.metadata;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.apache.commons.lang.Validate;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.util.NumberConversions;
|
||||
|
||||
/**
|
||||
* The LazyMetadataValue class implements a type of metadata that is not computed until another plugin asks for it.
|
||||
* By making metadata values lazy, no computation is done by the providing plugin until absolutely necessary (if ever).
|
||||
* Additionally, LazyMetadataValue objects cache their values internally unless overridden by a {@link CacheStrategy}
|
||||
* or invalidated at the individual or plugin level. Once invalidated, the LazyMetadataValue will recompute its value
|
||||
* when asked.
|
||||
*/
|
||||
public class LazyMetadataValue implements MetadataValue {
|
||||
private Callable<Object> lazyValue;
|
||||
private CacheStrategy cacheStrategy;
|
||||
private SoftReference<Object> internalValue = new SoftReference<Object>(null);
|
||||
private Plugin owningPlugin;
|
||||
private static final Object ACTUALLY_NULL = new Object();
|
||||
|
||||
/**
|
||||
* Initialized a LazyMetadataValue object with the default CACHE_AFTER_FIRST_EVAL cache strategy.
|
||||
*
|
||||
* @param owningPlugin the {@link Plugin} that created this metadata value.
|
||||
* @param lazyValue the lazy value assigned to this metadata value.
|
||||
*/
|
||||
public LazyMetadataValue(Plugin owningPlugin, Callable<Object> lazyValue) {
|
||||
this(owningPlugin, CacheStrategy.CACHE_AFTER_FIRST_EVAL, lazyValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a LazyMetadataValue object with a specific cache strategy.
|
||||
*
|
||||
* @param owningPlugin the {@link Plugin} that created this metadata value.
|
||||
* @param cacheStrategy determines the rules for caching this metadata value.
|
||||
* @param lazyValue the lazy value assigned to this metadata value.
|
||||
*/
|
||||
public LazyMetadataValue(Plugin owningPlugin, CacheStrategy cacheStrategy, Callable<Object> lazyValue) {
|
||||
Validate.notNull(owningPlugin, "owningPlugin cannot be null");
|
||||
Validate.notNull(cacheStrategy, "cacheStrategy cannot be null");
|
||||
Validate.notNull(lazyValue, "lazyValue cannot be null");
|
||||
|
||||
this.lazyValue = lazyValue;
|
||||
this.owningPlugin = owningPlugin;
|
||||
this.cacheStrategy = cacheStrategy;
|
||||
}
|
||||
|
||||
public Plugin getOwningPlugin() {
|
||||
return owningPlugin;
|
||||
}
|
||||
|
||||
public Object value() {
|
||||
eval();
|
||||
Object value = internalValue.get();
|
||||
if (value == ACTUALLY_NULL) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public int asInt() {
|
||||
return NumberConversions.toInt(value());
|
||||
}
|
||||
|
||||
public float asFloat() {
|
||||
return NumberConversions.toFloat(value());
|
||||
}
|
||||
|
||||
public double asDouble() {
|
||||
return NumberConversions.toDouble(value());
|
||||
}
|
||||
|
||||
public long asLong() {
|
||||
return NumberConversions.toLong(value());
|
||||
}
|
||||
|
||||
public short asShort() {
|
||||
return NumberConversions.toShort(value());
|
||||
}
|
||||
|
||||
public byte asByte() {
|
||||
return NumberConversions.toByte(value());
|
||||
}
|
||||
|
||||
public boolean asBoolean() {
|
||||
Object value = value();
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue() != 0;
|
||||
}
|
||||
|
||||
if (value instanceof String) {
|
||||
return Boolean.parseBoolean((String) value);
|
||||
}
|
||||
|
||||
return value != null;
|
||||
}
|
||||
|
||||
public String asString() {
|
||||
Object value = value();
|
||||
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily evaluates the value of this metadata item.
|
||||
*
|
||||
* @throws MetadataEvaluationException if computing the metadata value fails.
|
||||
*/
|
||||
private synchronized void eval() throws MetadataEvaluationException {
|
||||
if (cacheStrategy == CacheStrategy.NEVER_CACHE || internalValue.get() == null) {
|
||||
try {
|
||||
Object value = lazyValue.call();
|
||||
if (value == null) {
|
||||
value = ACTUALLY_NULL;
|
||||
}
|
||||
internalValue = new SoftReference<Object>(value);
|
||||
} catch (Exception e) {
|
||||
throw new MetadataEvaluationException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void invalidate() {
|
||||
if (cacheStrategy != CacheStrategy.CACHE_ETERNALLY) {
|
||||
internalValue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes possible caching strategies for metadata.
|
||||
*/
|
||||
public enum CacheStrategy {
|
||||
/**
|
||||
* Once the metadata value has been evaluated, do not re-evaluate the value until it is manually invalidated.
|
||||
*/
|
||||
CACHE_AFTER_FIRST_EVAL,
|
||||
|
||||
/**
|
||||
* Re-evaluate the metadata item every time it is requested
|
||||
*/
|
||||
NEVER_CACHE,
|
||||
|
||||
/**
|
||||
* Once the metadata value has been evaluated, do not re-evaluate the value in spite of manual invalidation.
|
||||
*/
|
||||
CACHE_ETERNALLY
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package org.bukkit.metadata;
|
||||
|
||||
/**
|
||||
* A MetadataConversionException is thrown any time a {@link LazyMetadataValue} attempts to convert a metadata value
|
||||
* to an inappropriate data type.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class MetadataConversionException extends RuntimeException {
|
||||
MetadataConversionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package org.bukkit.metadata;
|
||||
|
||||
/**
|
||||
* A MetadataEvaluationException is thrown any time a {@link LazyMetadataValue} fails to evaluate its value due to
|
||||
* an exception. The originating exception will be included as this exception's cause.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class MetadataEvaluationException extends RuntimeException {
|
||||
MetadataEvaluationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
package org.bukkit.metadata;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface MetadataStore<T> {
|
||||
/**
|
||||
* Adds a metadata value to an object.
|
||||
*
|
||||
* @param subject The object receiving the metadata.
|
||||
* @param metadataKey A unique key to identify this metadata.
|
||||
* @param newMetadataValue The metadata value to apply.
|
||||
*/
|
||||
public void setMetadata(T subject, String metadataKey, MetadataValue newMetadataValue);
|
||||
|
||||
/**
|
||||
* Returns all metadata values attached to an object. If multiple plugins have attached metadata, each will value
|
||||
* will be included.
|
||||
*
|
||||
* @param subject the object being interrogated.
|
||||
* @param metadataKey the unique metadata key being sought.
|
||||
* @return A list of values, one for each plugin that has set the requested value.
|
||||
*/
|
||||
public List<MetadataValue> getMetadata(T subject, String metadataKey);
|
||||
|
||||
/**
|
||||
* Tests to see if a metadata attribute has been set on an object.
|
||||
*
|
||||
* @param subject the object upon which the has-metadata test is performed.
|
||||
* @param metadataKey the unique metadata key being queried.
|
||||
* @return the existence of the metadataKey within subject.
|
||||
*/
|
||||
public boolean hasMetadata(T subject, String metadataKey);
|
||||
|
||||
/**
|
||||
* Removes a metadata item owned by a plugin from a subject.
|
||||
*
|
||||
* @param subject the object to remove the metadata from.
|
||||
* @param metadataKey the unique metadata key identifying the metadata to remove.
|
||||
* @param owningPlugin the plugin attempting to remove a metadata item.
|
||||
*/
|
||||
public void removeMetadata(T subject, String metadataKey, Plugin owningPlugin);
|
||||
|
||||
/**
|
||||
* Invalidates all metadata in the metadata store that originates from the given plugin. Doing this will force
|
||||
* each invalidated metadata item to be recalculated the next time it is accessed.
|
||||
*
|
||||
* @param owningPlugin the plugin requesting the invalidation.
|
||||
*/
|
||||
public void invalidateAll(Plugin owningPlugin);
|
||||
}
|
@@ -0,0 +1,143 @@
|
||||
package org.bukkit.metadata;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public abstract class MetadataStoreBase<T> {
|
||||
private Map<String, List<MetadataValue>> metadataMap = new HashMap<String, List<MetadataValue>>();
|
||||
private WeakHashMap<T, String> disambiguationCache = new WeakHashMap<T, String>();
|
||||
|
||||
/**
|
||||
* Adds a metadata value to an object. Each metadata value is owned by a specific{@link Plugin}.
|
||||
* If a plugin has already added a metadata value to an object, that value
|
||||
* will be replaced with the value of {@code newMetadataValue}. Multiple plugins can set independent values for
|
||||
* the same {@code metadataKey} without conflict.
|
||||
*
|
||||
* Implementation note: I considered using a {@link java.util.concurrent.locks.ReadWriteLock} for controlling
|
||||
* access to {@code metadataMap}, but decided that the added overhead wasn't worth the finer grained access control.
|
||||
* Bukkit is almost entirely single threaded so locking overhead shouldn't pose a problem.
|
||||
*
|
||||
* @see MetadataStore#setMetadata(Object, String, MetadataValue)
|
||||
* @param subject The object receiving the metadata.
|
||||
* @param metadataKey A unique key to identify this metadata.
|
||||
* @param newMetadataValue The metadata value to apply.
|
||||
*/
|
||||
public synchronized void setMetadata(T subject, String metadataKey, MetadataValue newMetadataValue) {
|
||||
String key = cachedDisambiguate(subject, metadataKey);
|
||||
if (!metadataMap.containsKey(key)) {
|
||||
metadataMap.put(key, new ArrayList<MetadataValue>());
|
||||
}
|
||||
// we now have a list of subject's metadata for the given metadata key. If newMetadataValue's owningPlugin
|
||||
// is found in this list, replace the value rather than add a new one.
|
||||
List<MetadataValue> metadataList = metadataMap.get(key);
|
||||
for (int i = 0; i < metadataList.size(); i++) {
|
||||
if (metadataList.get(i).getOwningPlugin().equals(newMetadataValue.getOwningPlugin())) {
|
||||
metadataList.set(i, newMetadataValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// we didn't find a duplicate...add the new metadata value
|
||||
metadataList.add(newMetadataValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all metadata values attached to an object. If multiple plugins have attached metadata, each will value
|
||||
* will be included.
|
||||
*
|
||||
* @see MetadataStore#getMetadata(Object, String)
|
||||
* @param subject the object being interrogated.
|
||||
* @param metadataKey the unique metadata key being sought.
|
||||
* @return A list of values, one for each plugin that has set the requested value.
|
||||
*/
|
||||
public synchronized List<MetadataValue> getMetadata(T subject, String metadataKey) {
|
||||
String key = cachedDisambiguate(subject, metadataKey);
|
||||
if (metadataMap.containsKey(key)) {
|
||||
return Collections.unmodifiableList(metadataMap.get(key));
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests to see if a metadata attribute has been set on an object.
|
||||
*
|
||||
* @param subject the object upon which the has-metadata test is performed.
|
||||
* @param metadataKey the unique metadata key being queried.
|
||||
* @return the existence of the metadataKey within subject.
|
||||
*/
|
||||
public synchronized boolean hasMetadata(T subject, String metadataKey) {
|
||||
String key = cachedDisambiguate(subject, metadataKey);
|
||||
return metadataMap.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a metadata item owned by a plugin from a subject.
|
||||
*
|
||||
* @see MetadataStore#removeMetadata(Object, String, org.bukkit.plugin.Plugin)
|
||||
* @param subject the object to remove the metadata from.
|
||||
* @param metadataKey the unique metadata key identifying the metadata to remove.
|
||||
* @param owningPlugin the plugin attempting to remove a metadata item.
|
||||
*/
|
||||
public synchronized void removeMetadata(T subject, String metadataKey, Plugin owningPlugin) {
|
||||
String key = cachedDisambiguate(subject, metadataKey);
|
||||
List<MetadataValue> metadataList = metadataMap.get(key);
|
||||
for (int i = 0; i < metadataList.size(); i++) {
|
||||
if (metadataList.get(i).getOwningPlugin().equals(owningPlugin)) {
|
||||
metadataList.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates all metadata in the metadata store that originates from the given plugin. Doing this will force
|
||||
* each invalidated metadata item to be recalculated the next time it is accessed.
|
||||
*
|
||||
* @see MetadataStore#invalidateAll(org.bukkit.plugin.Plugin)
|
||||
* @param owningPlugin the plugin requesting the invalidation.
|
||||
*/
|
||||
public synchronized void invalidateAll(Plugin owningPlugin) {
|
||||
if(owningPlugin == null) {
|
||||
throw new IllegalArgumentException("owningPlugin cannot be null");
|
||||
}
|
||||
|
||||
for (List<MetadataValue> values : metadataMap.values()) {
|
||||
for (MetadataValue value : values) {
|
||||
if (value.getOwningPlugin().equals(owningPlugin)) {
|
||||
value.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the results of calls to {@link MetadataStoreBase#disambiguate(Object, String)} in a {@link WeakHashMap}. Doing so maintains a
|
||||
* <a href="http://www.codeinstructions.com/2008/09/weakhashmap-is-not-cache-understanding.html">canonical list</a>
|
||||
* of disambiguation strings for objects in memory. When those objects are garbage collected, the disambiguation string
|
||||
* in the list is aggressively garbage collected as well.
|
||||
* @param subject The object for which this key is being generated.
|
||||
* @param metadataKey The name identifying the metadata value.
|
||||
* @return a unique metadata key for the given subject.
|
||||
*/
|
||||
private String cachedDisambiguate(T subject, String metadataKey) {
|
||||
if (disambiguationCache.containsKey(subject)) {
|
||||
return disambiguationCache.get(subject);
|
||||
} else {
|
||||
String disambiguation = disambiguate(subject, metadataKey);
|
||||
disambiguationCache.put(subject, disambiguation);
|
||||
return disambiguation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique name for the object receiving metadata by combining unique data from the subject with a metadataKey.
|
||||
* The name created must be globally unique for the given object and any two equivalent objects must generate the
|
||||
* same unique name. For example, two Player objects must generate the same string if they represent the same player,
|
||||
* even if the objects would fail a reference equality test.
|
||||
*
|
||||
* @param subject The object for which this key is being generated.
|
||||
* @param metadataKey The name identifying the metadata value.
|
||||
* @return a unique metadata key for the given subject.
|
||||
*/
|
||||
protected abstract String disambiguate(T subject, String metadataKey);
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
package org.bukkit.metadata;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
public interface MetadataValue {
|
||||
|
||||
/**
|
||||
* Fetches the value of this metadata item.
|
||||
*
|
||||
* @return the metadata value.
|
||||
*/
|
||||
public Object value();
|
||||
|
||||
/**
|
||||
* Attempts to convert the value of this metadata item into an int.
|
||||
* @return the value as an int.
|
||||
*/
|
||||
public int asInt();
|
||||
|
||||
/**
|
||||
* Attempts to convert the value of this metadata item into a float.
|
||||
* @return the value as a float.
|
||||
*/
|
||||
public float asFloat();
|
||||
|
||||
/**
|
||||
* Attempts to convert the value of this metadata item into a double.
|
||||
* @return the value as a double.
|
||||
*/
|
||||
public double asDouble();
|
||||
|
||||
/**
|
||||
* Attempts to convert the value of this metadata item into a long.
|
||||
* @return the value as a long.
|
||||
*/
|
||||
public long asLong();
|
||||
|
||||
/**
|
||||
* Attempts to convert the value of this metadata item into a short.
|
||||
* @return the value as a short.
|
||||
*/
|
||||
public short asShort();
|
||||
|
||||
/**
|
||||
* Attempts to convert the value of this metadata item into a byte.
|
||||
* @return the value as a byte.
|
||||
*/
|
||||
public byte asByte();
|
||||
|
||||
/**
|
||||
* Attempts to convert the value of this metadata item into a boolean.
|
||||
* @return the value as a boolean.
|
||||
*/
|
||||
public boolean asBoolean();
|
||||
|
||||
/**
|
||||
* Attempts to convert the value of this metadata item into a string.
|
||||
* @return the value as a string.
|
||||
*/
|
||||
public String asString();
|
||||
|
||||
/**
|
||||
* Returns the {@link Plugin} that created this metadata item.
|
||||
*
|
||||
* @return the plugin that owns this metadata value.
|
||||
*/
|
||||
public Plugin getOwningPlugin();
|
||||
|
||||
/**
|
||||
* Invalidates this metadata item, forcing it to recompute when next accessed.
|
||||
*/
|
||||
public void invalidate();
|
||||
}
|
42
paper-api/src/main/java/org/bukkit/metadata/Metadatable.java
Normal file
42
paper-api/src/main/java/org/bukkit/metadata/Metadatable.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package org.bukkit.metadata;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This interface is implemented by all objects that can provide metadata about themselves.
|
||||
*/
|
||||
public interface Metadatable {
|
||||
/**
|
||||
* Sets a metadata value in the implementing object's metadata store.
|
||||
*
|
||||
* @param metadataKey A unique key to identify this metadata.
|
||||
* @param newMetadataValue The metadata value to apply.
|
||||
*/
|
||||
public void setMetadata(String metadataKey, MetadataValue newMetadataValue);
|
||||
|
||||
/**
|
||||
* Returns a list of previously set metadata values from the implementing object's metadata store.
|
||||
*
|
||||
* @param metadataKey the unique metadata key being sought.
|
||||
* @return A list of values, one for each plugin that has set the requested value.
|
||||
*/
|
||||
public List<MetadataValue> getMetadata(String metadataKey);
|
||||
|
||||
/**
|
||||
* Tests to see whether the implementing object contains the given metadata value in its metadata store.
|
||||
*
|
||||
* @param metadataKey the unique metadata key being queried.
|
||||
* @return the existence of the metadataKey within subject.
|
||||
*/
|
||||
public boolean hasMetadata(String metadataKey);
|
||||
|
||||
/**
|
||||
* Removes the given metadata value from the implementing object's metadata store.
|
||||
*
|
||||
* @param metadataKey the unique metadata key identifying the metadata to remove.
|
||||
* @param owningPlugin This plugin's metadata value will be removed. All other values will be left untouched.
|
||||
*/
|
||||
public void removeMetadata(String metadataKey, Plugin owningPlugin);
|
||||
}
|
Reference in New Issue
Block a user