Skip to content

Commit

Permalink
API 8: Data (#981)
Browse files Browse the repository at this point in the history
* Data!!!

* Fixed spelling mistakes

* Missed one

* Inscrutable changes
mosemister authored Mar 29, 2024
1 parent 0d8b4df commit 75bd738
Showing 12 changed files with 326 additions and 909 deletions.
4 changes: 2 additions & 2 deletions source/plugin/blocks/accessing.rst
Original file line number Diff line number Diff line change
@@ -90,7 +90,7 @@ First, we need to know which ``DataManipulator`` ``Key`` we need. We can then pa
``ServerLocation`` which will return an ``Optional``. We then have to check if our ``DataManipulator`` actually
exists for our block by checking ``ifPresent()``. If it exists, then we can use it.

More on ``DataManipulator``\s can be found in the :doc:`data documentation <../data/datamanipulators>`.
More on ``DataManipulator``\s can be found in the :doc:`data documentation <../data/customdata>`.

.. tip::

@@ -124,7 +124,7 @@ use of this is getting an :javadoc:`DataManipulator.Immutable`, as shown below:
}
More information on mutable and immutable ``DataManipulator``\s can be found in the :doc:`data documentation
<../data/datamanipulators>`.
<../data/customdata>`.

Block State Properties
~~~~~~~~~~~~~~~~~~~~~~
8 changes: 0 additions & 8 deletions source/plugin/data/custom/dataholders.rst

This file was deleted.

362 changes: 0 additions & 362 deletions source/plugin/data/custom/datamanipulators.rst

This file was deleted.

38 changes: 0 additions & 38 deletions source/plugin/data/custom/index.rst

This file was deleted.

181 changes: 0 additions & 181 deletions source/plugin/data/custom/serialization.rst

This file was deleted.

297 changes: 297 additions & 0 deletions source/plugin/data/customdata.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
===========
Custom Data
===========

.. javadoc-import::
org.spongepowered.api.data.DataHolder
org.spongepowered.api.data.DataManager
org.spongepowered.api.data.DataRegistration
org.spongepowered.api.data.Key
org.spongepowered.api.data.persistence.DataContentUpdater
org.spongepowered.api.data.persistence.DataContainer
org.spongepowered.api.data.persistence.DataBuilder
org.spongepowered.api.data.persistence.DataQuery
org.spongepowered.api.data.persistence.DataSerializable
org.spongepowered.api.data.persistence.DataStore
org.spongepowered.api.data.value.MapValue
org.spongepowered.api.data.value.Value
org.spongepowered.api.entity.Entity
org.spongepowered.api.event.lifecycle.RegisterDataEvent

Object Custom Data
==================

The core part of custom data is the :javadoc:`DataSerializable`. To implement it, you must first decide if you want to
create a separate API for your custom data. Generally speaking it's best to separate the API from the implementation
(as SpongeAPI does), but if it won't be seen by other developers then you can just put both in the same class.

First, create a class and define the data you wish to store. In the following example we will use the idea of storing a
players last attacker, therefore we will have only the data of the last attackers UUID.

.. code-block:: java
import java.util.UUID;
public class LastAttackerDataSerilizable {
private UUID lastAttackerId;
}
.. note::

Any data you wish to store must be able to be serialized into java primitives and/or strings


From here you will want to implement the javadoc:`DataSerializable` which will give you two methods to implement. The
first is ``contentVersion`` which is for the version of your data manipulator. The other method (``toContainer``) is
used for serializing your data to the dataholder it belongs to. To do this you need to create a new :javadoc:`DataContainer`
then set your value(s) to the newly created ``DataContainer``


.. code-block:: java
import org.spongepowered.api.data.persistence.DataSerializable;
import org.spongepowered.api.data.persistence.DataContainer;
import org.spongepowered.api.data.persistence.DataQuery;
public class LastAttackerDataSerilizable implements DataSerializable {
private UUID lastAttackerId;
public static final DataQuery UUID_PATH = DataQuery.of("attack", "last");
@Override
public int contentVersion(){
return 1;
}
@Override
public DataContainer toContainer(){
return DataContainer.createNew()
.set(LastAttackerDataSerilizable.UUID_PATH, lastAttackerId.toString());
}
}
After that you will want to create a class that can build the data from a ``DataContainer`` this is known as
the :javadoc:`DataBuilder` which can be implemented as follows:

.. code-block:: java
import org.spongepowered.api.data.persistence.InvalidDataException;
public class LastAttackerDataBuilder implements DataBuilder<LastAttackerDataSerilizable> {
@Override
public Optional<LastAttackerDataSerilizable> build(DataView container) throws InvalidDataException {
Optional<String> lastAttackerAsStringId container.getString(LastAttackerDataSerilizable.UUID_PATH);
if(lastAttackerAsStringId.isPresent()){
UUID lastAttacker = UUID.fromString(lastAttackerAsStringId.get());
return Optional.of(new LastAttackerDataSerilizable(lastAttacker));
}
return Optional.empty();
}
}
Registration
============

Registering your ``DataSerializable`` allows it to be accessible by Sponge and by other plugins in a generic way. The
game/plugin can create copies of your data and serialize/deserialize your data without referencing any of your classes
directly.

To register a ``DataSerializable`` Sponge has the :javadoc:`RegisterDataEvent` event. This will allow you to register
your data with the appropriate ``DataHolder``

Simple Custom Data
==================

All of above is a lot of work if you just wanting to register a java primitive or ``String`` to
a ``DataHolder``. Thankfully there is a much shorter way to do all of that.

.. code-block:: java
Key<? extends Value<String>> key = Key.from(pluginContainer, "my_simple_data", String.class);
DataRegistration myData = DataRegistration.of(key, ServerPlayer.class);
event.register(myData);
Registration Key
================

When it comes to registering your data, you are required to register it with a :javadoc:`Key` which will allow you and
other developers access to your data manipulator.


.. code-block:: java
import org.spongepowered.api.ResourceKey;
import org.spongepowered.api.data.Key;
import org.spongepowered.api.data.value.Value;
ResourceKey resourceKey = ResourceKey(pluginContainer, "last_attacker_manipulator");
Key<? extends Value<LastAttackerDataSerilizable>> key = Key
.builder()
.key(resourceKey)
.elementType(LastAttackerDataSerilizable.class)
.build();
.. warning::

Be sure to store your ``Key`` somewhere global so you can access it later.

.. tip::

You can register a key for a specific element within a DataSerializable

Data Store
==========

The :javadoc:`DataStore` is used to register your ``Key`` with the appropriate ``DataHolder`` and also register
any other keys you may have accessing your ``DataSerializable``. In the example below, it creates a ``DataStore``
and makes it appliciable to only the :javadoc:`Entity` ``DataHolder``.

.. code-block:: java
import org.spongepowered.api.data.persistence.DataStore;
DataStore datastore = DataStore
.builder()
.pluginData(resourceKey)
.holder(Entity.class)
.key(key)
.build();
Simple Data Store
=================

The above code is a lot for such a simple DataStore, so thankfully Sponge allows a quick way to create a ``DataStore``
for a single key. The following example does the same as the above example.

.. code-block:: java
DataStore datastore = DataStore.of(key, DataQuery.of(), Entity.class);
Multi-Key Data Store
====================

If you are registering multiple keys onto a single ``DataStore`` then the first approach should be used, however the
other keys should be specified with the original key, such as the following example.

.. code-block:: java
import org.spongepowered.api.entity.Entity;
DataStore datastore = DataStore
.builder()
.pluginData(resourceKey)
.holder(Entity.class)
.key(key)
.key(innerKey, DataQuery.of("inner_data"))
.build();
Data Provider
=============

For data that requires more code to be used whenever the getter, setter, deleter are used will require the use of
a ``DataProvider``. With a ``dataProvider`` a plugin is able to manipulate how its data should be received, set, and
deleted automatically.

In the following example, we will be getting the UUID from the last attacker but if there is no last attacker, then
return the player's UUID instead.

.. code-block:: java
import org.spongepowered.api.data.DataProvider;
DataProvider<Value<UUID>, UUID> provider = DataProvider.mutableBuilder()
.dataKey(innerKey)
.dataHolder(ServerPlayer.class)
.get(this::myCustomGetter)
.build();
public UUID myCustomGetter(ServerPlayer player){
return player.get(key).orElse(player.uniqueId());
}
.. note::

Data Providers are completely optional, if your data does not require one then don't use one

.. tip::

Data Providers are great if you wish to have your data be synced with a database


Data Registration
=================

The final object you will need to register your data is the :javadoc:`DataRegistration` which combines
your ``Key``, ``DataStore`` and ``DataProvider`` together into a single package that you can register.

.. code-block:: java
import org.spongepowered.api.data.DataRegistration;
DataRegistration myData = DataRegistration.builder()
.key(key)
.store(datastore)
.provider(provider)
.build();
event.register(myData);
Data Builder Register
=====================

The final part of your custom data registration is registering the data builder so your data can be
constructed upon reboot. This is registered though the :javadoc:`DataManager`, although it is recommended
that you register it within the ``RegisterDataEvent``.

.. code-block:: java
Sponge.dataManager().registerBuilder(LastAttackerDataSerilizable.class, new LastAttackerDataBuilder());
Updating Data Containers
========================

You may wish to update the data found within a DataHolder to a new and improved ``DataSerializable``.
This can be done with the use of the :javadoc:`DataContentUpdater` interface. In the example below
we will be adding a field of the nanosecond the attack occurred, with the update value being ``LocalDateTime.MIN``.

.. code-block:: java
import org.spongepowered.api.data.persistence.DataContentUpdater;
public class LastAttackerUpdater implements DataContentUpdater {
@Override
public int inputVersion(){
return 1;
}
@Override
public int outputVersion(){
return 2;
}
@Override
public DataView update(DataView view){
view.set(DataQuery.of("attack", "occurred"), LocalDateTime.MIN.getNano());
return view;
}
}
This can then be registered with your ``DataStore``, whereby specifying a version number
on the ``pluginData`` function will allow you to register your ``DataContentUpdater``.

.. code-block:: java
DataStore.builder()
.pluginData(resourceKey, 1)
.updater(new LastAttackerUpdater())
//continue with the normal registeration
167 changes: 0 additions & 167 deletions source/plugin/data/datamanipulators.rst

This file was deleted.

25 changes: 8 additions & 17 deletions source/plugin/data/index.rst
Original file line number Diff line number Diff line change
@@ -2,20 +2,12 @@
The Data API
============

.. warning::
These docs were written for SpongeAPI 7 and are likely out of date.
`If you feel like you can help update them, please submit a PR! <https://github.com/SpongePowered/SpongeDocs>`__

.. javadoc-import::

org.spongepowered.api.block.BlockType
org.spongepowered.api.data.DataHolder
org.spongepowered.api.data.key.Key
org.spongepowered.api.data.key.Keys
org.spongepowered.api.data.manipulator.mutable.DyeableData
org.spongepowered.api.data.manipulator.mutable.entity.HealthData
org.spongepowered.api.data.property.item.DamageAbsorptionProperty
org.spongepowered.api.data.property.item.HarvestingProperty
org.spongepowered.api.data.Key
org.spongepowered.api.data.Keys
org.spongepowered.api.entity.EntityType
org.spongepowered.api.item.ItemType

@@ -59,23 +51,23 @@ Property
A property too is data, but not synchronized between server and clients. Therefore, it can only be
changed by modifications present on both client and server. Since Sponge is not intended to require a
client-side counterpart, properties are not modifiable.
Examples of properties are the harvesting abilities on tools (represented as :javadoc:`HarvestingProperty` or the damage
absorption of an equipable armor item (represented as :javadoc:`DamageAbsorptionProperty`).
Examples of properties are the applicable potion effects on tools (represented as :javadoc:`Keys#APPLICABLE_POTION_EFFECTS` or the damage
absorption of an equipable armor item (represented as :javadoc:`Keys#ABSORPTION`).

DataManipulator
~~~~~~~~~~~~~~~

A data manipulator represents points of cohesive data that describes a certain component of its holder. For
example :javadoc:`HealthData`, which contains both current and maximum health. If a data holder has ``HealthData``, it
example HealthData, which contains both current and maximum health. If a data holder has ``HealthData``, it
has health that can somehow be depleted and replenished and can die if that health is depleted. This allows for the
re-use of such components over the API and prevents duplication of accessor methods. For example, sheep, stained glass
blocks and leather armor all can share the :javadoc:`DyeableData` holding the color they are dyed in.
blocks and leather armor all can share the DyeableData holding the color they are dyed in.

Key
~~~

A ``Key`` is a unique identifier for a single point of data and can be used to directly read or set that point of
data without worrying about data manipulators. It was designed to provide a convenient way of accessing data
data without worrying about data manipulators. It was designed to provide a way of accessing data
similar to direct getter/setter methods. All keys used within Sponge are listed as constants in the
:javadoc:`Keys` utility class.

@@ -94,8 +86,7 @@ Contents
:maxdepth: 2
:titlesonly:

custom/index
keys
datamanipulators
transactions
serialization
customdata
30 changes: 7 additions & 23 deletions source/plugin/data/keys.rst
Original file line number Diff line number Diff line change
@@ -2,17 +2,13 @@
Using Keys
==========

.. warning::
These docs were written for SpongeAPI 7 and are likely out of date.
`If you feel like you can help update them, please submit a PR! <https://github.com/SpongePowered/SpongeDocs>`__

.. javadoc-import::
org.spongepowered.api.data.DataHolder
org.spongepowered.api.data.DataTransactionResult
org.spongepowered.api.data.key.Key
org.spongepowered.api.data.key.Keys
org.spongepowered.api.data.value.BaseValue
org.spongepowered.api.data.value.mutable.MutableBoundedValue
org.spongepowered.api.data.value.WeightedCollectionValue

Getting and offering data using a key
=====================================
@@ -106,29 +102,17 @@ Keyed Values

There are cases where you may care about not only the direct value for a Key, but the keyed value
encapsulating it. In that case, use the ``getValue(key)`` method instead of ``get(key)``. You will receive an
object inheriting from :javadoc:`BaseValue` which contains a copy of the original value. Since we know that current
health is a :javadoc:`MutableBoundedValue`, we can find out the minimum possible value and set our target's health just
a tiny bit above that.
object inheriting from :javadoc:`Value` which contains a copy of the original value. As ``Keys#SPAWNABLE_ENTITIES``
is a :javadoc:`WeightedCollectionValue`, we can get a list of potentional entity that could spawn using the ``Value``
of the key.

**Code Example: Bring a target to the brink of death**

.. code-block:: java
import org.spongepowered.api.data.value.mutable.MutableBoundedValue;
public void scare(DataHolder target) {
if (target.supports(Keys.HEALTH)) {
MutableBoundedValue<Double> health = target.getValue(Keys.HEALTH).get();
double nearDeath = health.getMinValue() + 1;
health.set(nearDeath);
target.offer(health);
if (target.supports(Keys.NEXT_ENTITY_TO_SPAWN)) {
WeightedCollectionValue value = target.getValue(Keys.NEXT_ENTITY_TO_SPAWN).get();
List<Entity> entities = value.get(new Random());
}
}
Again, we check if our target supports the health key and then obtain the keyed value. A
``MutableBoundedValue`` contains a ``getMinValue()`` method, so we obtain the minimal value, add 1 and then set
it to our data container. Internally, the ``set()`` method performs a check if our supplied value is valid and
silently fails if it is not. Calling ``health.set(-2)`` would not change the value within ``health`` since it
would fail the validity checks. To finally apply our changes to the target, we need to offer the keyed value
back to it. As a keyed value also contains the ``Key`` used to identify it, calling ``target.offer(health)``
is equivalent to ``target.offer(health.getKey(), health.get())``.
106 changes: 6 additions & 100 deletions source/plugin/data/serialization.rst
Original file line number Diff line number Diff line change
@@ -15,19 +15,15 @@ Serializing Data
org.spongepowered.api.data.DataQuery
org.spongepowered.api.data.DataSerializable
org.spongepowered.api.data.DataView
org.spongepowered.api.data.MemoryDataContainer
org.spongepowered.api.data.MemoryDataView
org.spongepowered.api.data.manipulator.DataManipulator
org.spongepowered.api.data.manipulator.ImmutableDataManipulator
org.spongepowered.api.data.manipulator.mutable.entity.HealthData
org.spongepowered.api.data.DataManipulator
org.spongepowered.api.data.DataManipualator.Immutable
org.spongepowered.api.data.persistence.DataBuilder
org.spongepowered.api.data.persistence.DataTranslator
org.spongepowered.api.data.persistence.DataTranslators
org.spongepowered.api.data.persistence.InvalidDataException
org.spongepowered.api.data.persistence.DataFormat
org.spongepowered.api.data.persistence.DataFormats

While an :javadoc:`ImmutableDataManipulator` is a good way to store data while the server is running, it will not
While an :javadoc:`DataManipulator#Immutable` is a good way to store data while the server is running, it will not
persist over a restart. However, every :javadoc:`DataManipulator` implements the :javadoc:`DataSerializable` interface
and thus can be serialized to a :javadoc:`DataContainer` and deserialized by a :javadoc:`DataBuilder`.

@@ -41,102 +37,12 @@ A :javadoc:`DataView` is a general-purpose structure for holding any kind of dat
nested ``DataView``\ s as a value, thus allowing for a tree-like structure. Every value is identified by a
:javadoc:`DataQuery`. A ``DataContainer`` is a root ``DataView``.

Every ``DataSerializable`` provides a ``toContainer()`` method which will create and return a ``DataContainer``.
As an example, calling ``toContainer()`` on a :javadoc:`HealthData` instance will yield a ``DataContainer`` containing
two values, one for the current and one for the maximum health, each identified by the ``DataQuery`` of the respective
``Key``.

.. code-block:: java
import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.key.Keys;
DataContainer serializedHealth = healthData.toContainer();
double currentHealth = serializedHealth.getDouble(Keys.HEALTH.getQuery()).get();
currentHealth == healthData.health().get(); // true
Converting this container back into a ``HealthData`` instance is done by the corresponding ``DataBuilder``. Those are
registered and managed by the :javadoc:`DataManager`. It can either be obtained from a valid :javadoc:`Game` instance
or using the :javadoc:`Sponge` utility class. The ``DataManager`` provides a method to get the appropriate
``DataBuilder`` to deserialize a given class and additionally a shorthand method to get the ``DataBuilder`` and have it
do the deserialization in one step. Both of the following code examples are functionally equivalent.

**Code Example: Deserialization, the long way**

.. code-block:: java
import org.spongepowered.api.data.DataView;
import org.spongepowered.api.data.manipulator.mutable.entity.HealthData;
import org.spongepowered.api.util.persistence.DataBuilder;
import java.util.Optional;
public Optional<HealthData> deserializeHealth(DataView container) {
final Optional<DataBuilder<HealthData>> builder = Sponge.getDataManager().getBuilder(HealthData.class);
if (builder.isPresent()) {
return builder.get().build(container);
}
return Optional.empty();
}
**Code Example: Deserialization, the short way**

.. code-block:: java
import org.spongepowered.api.data.manipulator.mutable.entity.HealthData;
public Optional<HealthData> deserializeHealth(DataView container) {
return Sponge.getDataManager().deserialize(HealthData.class, container);
}
The ``deserializeHealth`` function will return ``Optional.empty()`` if there is no ``DataBuilder`` registered for
``HealthData`` or the supplied ``DataContainer`` is empty. If invalid data is present in the ``DataContainer``, an
:javadoc:`InvalidDataException` will be thrown.

DataTranslator
==============

In Sponge, generally the implementations :javadoc:`MemoryDataView` and :javadoc:`MemoryDataContainer` are used, which
reside in memory only and thus will not persist over a server restart. In order to persistently store a
``DataContainer``, it first has to be converted into a storable representation.

Using the :javadoc:`DataTranslators#CONFIGURATION_NODE` implementation of :javadoc:`DataTranslator`, we can convert a
``DataView`` to a :javadoc:`ConfigurationNode` and vice versa. ``ConfigurationNode``\ s can then be written to and read
from persistent files using the :doc:`Configurate Library <../configuration/index>`.

**Code Example: Serializing a HealthData instance to Configurate**

.. code-block:: java
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.api.data.persistence.DataTranslator;
import org.spongepowered.api.data.persistence.DataTranslators;
public ConfigurationNode translateToConfig(HealthData data) {
final DataTranslator<ConfigurationNode> translator = DataTranslators.CONFIGURATION_NODE;
final DataView container = data.toContainer();
return translator.translate(container);
}
**Code Example: Deserializing a HealthData instance from Configurate**

.. code-block:: java
import java.util.Optional;
public Optional<HealthData> translateFromConfig(ConfigurationNode node) {
final DataTranslator<ConfigurationNode> translator = DataTranslators.CONFIGURATION_NODE;
final DataView container = translator.translate(node);
return deserializeHealth(container);
}
DataFormat
==========

An alternative to using a ``DataTranslator`` is to use :javadoc:`DataFormat`, which allows you to store a
``DataContainer`` in HOCON, JSON or NBT format. You can also recreate DataContainers using ``DataFormats``. Sponge
provided ``DataFormat`` implementations are available in the :javadoc:`DataFormats` class.
:javadoc:`DataFormat` allows you to store a ``DataContainer`` in HOCON, JSON or NBT format.
You can also recreate DataContainers using ``DataFormats``. Sponge provided ``DataFormat``
implementations are available in the :javadoc:`DataFormats` class.

For example, we can use the :javadoc:`DataFormats#JSON` ``DataFormat`` which allows us to create a JSON representation
of a ``DataContainer``. The output JSON could then easily be stored in a database. We can then use the same
15 changes: 5 additions & 10 deletions source/plugin/data/transactions.rst
Original file line number Diff line number Diff line change
@@ -2,14 +2,9 @@
Transactions
============

.. warning::
These docs were written for SpongeAPI 7 and are likely out of date.
`If you feel like you can help update them, please submit a PR! <https://github.com/SpongePowered/SpongeDocs>`__

.. javadoc-import::
org.spongepowered.api.data.DataTransactionResult
org.spongepowered.api.data.DataTransactionResult.Type
org.spongepowered.api.data.manipulator.mutable.entity.HealthData
org.spongepowered.api.world.Location

Reading the Result
@@ -43,11 +38,11 @@ The result also provides a couple of immutable lists containing immutable value
the data that was involved in the transaction.

+-------------------------+---------------------------------------------------------------+
| ``getSuccessfulData()`` | contains all data that was successfully set |
| ``successfulData()`` | contains all data that was successfully set |
+-------------------------+---------------------------------------------------------------+
| ``getReplacedData()`` | contains all data that got replaced by successfully set data |
| ``replacedData()`` | contains all data that got replaced by successfully set data |
+-------------------------+---------------------------------------------------------------+
| ``getRejectedData()`` | contains all data that could not be set |
| ``rejectedData()`` | contains all data that could not be set |
+-------------------------+---------------------------------------------------------------+

Examples
@@ -64,8 +59,8 @@ Surely you remember the healing example in the :doc:`keys` page. Imagine a playe
- ``getReplacedData()`` would contain one value container for the ``Keys.HEALTH`` key with a value of 1.0
- ``getSuccessfulData()`` would contain one value container for the ``Keys.HEALTH`` key with a value of 20.0

Now what would be different if we used the healing example from the :doc:`datamanipulators` page instead? Since the
:javadoc:`HealthData` data manipulator contains values for both the current and the maximum health, in addition to the
Now what would be different if we used the healing example from the :doc:`customdata` page instead? Since the
``HealthData`` data manipulator contains values for both the current and the maximum health, in addition to the
above result, both the ``getReplacedData()`` list and the ``getSuccessfulData()`` list would contain one more element:
A value container for the ``Keys.MAX_HEALTH`` key with a value of 20.0.

2 changes: 1 addition & 1 deletion source/plugin/entities/modifying.rst
Original file line number Diff line number Diff line change
@@ -88,5 +88,5 @@ Another, shorter way to do this is by just using :javadoc:`Keys` on our ``Entity
creeper.offer(Keys.DISPLAY_NAME, Component.text("Inscrutable", NamedTextColor.DARK_AQUA));
}
This would neaten our code and is easier to perform. See the :doc:`data documentation <../data/datamanipulators>` on
This would neaten our code and is easier to perform. See the :doc:`data documentation <../data/customdata>` on
the specific benefits of using either ``DataManipulator``\ s or just ``Keys``.

0 comments on commit 75bd738

Please sign in to comment.