Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data Docs 2.0 #331

Closed
wants to merge 11 commits into from
111 changes: 111 additions & 0 deletions source/plugin/data/datamanipulators.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
=================
Data Manipulators
=================

Accessing and modifying data
============================

A data manipulator represents a certain component and all of its data. It stores a representation of that data and can
be offered to or created from data holders which possess a matching component. Again, let's use an example. And again
try to heal someone (or something).

**Code example: Healing with data manipulators**

.. code-block:: java

public static DataTransactionResult heal(DataHolder target) {
Optional<HealthData> healthOptional = target.getOrCreate(HealthData.class);
if (healthOptional.isPresent()) {
HealthData healthData = healthOptional.get();

double maxHealth = healthData.maxHealth().get();
MutableBoundedValue<Double> currentHealth = healthData.health();
currentHealth.set(maxHealth);
healthData.set(currentHealth);

target.offer(healthData);
}
}

First we need to check if our target has health data. We do so by first asking it to provide us with its health
data by passing its class to the ``getOrCreate()`` method. We get an ``Optional`` which we can use for our check.
If the target does not support health data, it will be absent. But if the health data is present, it now contains
a mutable copy of the data present on the data holder. We make our alterations and finally offer the changed data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to include that if the data is "supported" but not available, it is still created. (the whole point of getOrCreate() versus get()).

back to our target, where it is accepted (again, ``offer`` will return a ``DataTransactionResult`` which we will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"offer" --> "offer()"

just discard here).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not discard, but rather, disregarding. Even though, sometimes it should be checked for any possible errors.


As you can see, the results for ``health()`` and ``maxHealth()`` are again value containers we obtain from the
``DataHolder``. As the ``MutableBoundedValue`` we receive from calling ``health()`` again just contains a copy of
the data, we first need to apply our changes back to the ``DataManipulator`` before we can offer the
``healthData`` back to our target.

.. tip::

Rule #1 of the Data API: Everything you receive is a copy. So whenever you change something, make sure that
your change is propagated back to where the original value came from.


DataManipulator vs. Keys
========================

If you compared both of our healing examples, you may wonder 'Why bother with data manipulators anyway, keys are
so much easier' and be right - for getting and setting single values. But the true merit of a data manipulator is
that it contains *all* data pertaining to a certain component. Let us take a look at another example.

**Code Example: Swapping two data holders' health**

.. code-block:: java

public void swapHealth(DataHolder targetA, DataHolder targetB) {
if (targetA.supports(HealthData.class) && targetB.supports(HealthData.class)) {
HealthData healthA = targetA.get(HealthData.class).get();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, emphasis on the difference between get() and getOrCreate().
nvm, saw the explanation below.

HealthData healthB = targetB.get(HealthData.class).get();
targetA.offer(healthB);
targetB.offer(healthA);
}
}

First we check if both targets support HealthData. If they do, we save the health of both in one variable each. We
don't need to bother with ``Optional`` this time since we verified that ``HealthData`` is supported and the
``getOrCreate()`` method ensures that even if no data is present, default values are generated.

Then we just offer the saved health data to the *other* target, thus switching their health status with each other.

This example done with ``Keys`` would be a bit longer and more complicated since we'd have to take care of each
individual key by ourself. And if instead of health we swapped another data manipulator containing even more data
(maybe ``InvisibilityData`` which even contains a list), we'd have a lot more work to do. But since the data
holder itself takes care of all data pertaining to it, we could even modify the above function to swap arbitrary
data between two holders.

**Code Example: Swapping any data manipulator**

.. code-block:: java

public <T extends DataManipulator<?,?>> void swapData(DataHolder targetA, DataHolder targetB, Class<T> dataClass) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this will compile with generics, have you checked?

if (targetA.supports(dataClass) && targetB.supports(dataClass)) {
T dataA = targetA.getOrCreate(dataClass).get();
T dataB = targetB.getOrCreate(dataClass).get();
targetA.offer(dataB);
targetB.offer(dataA);
}
}

The ability to write a function that can just swap any data on a data holder with the same data on another data
holder demonstrates the core design goal of the Data API: Maximum compatibility across the API.

Mutable vs. Immutable Data Manipulators
=======================================

To every data manipulator, there is a matching ``ImmutableDataHolder``. For instance both ``HealthData`` and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comma after "instance"

``ImmutableHealthData`` contain the same data, only the latter does not provide any means to make alterations to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"only" --> "but" (just sounds better to me, you may disagree)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"only the latter returns new instances when requesting modified data."

the data.

Conversion between mutable and immutable data manipulators is done via the ``asImmutable()`` and ``asMutable()``
methods, which each will return a copy of the data. Since the only way to obtain an immutable data manipulator
from a data holder is obtaining a mutable one and then using ``asImmutable()`` in terms of processing power it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comma after asImmutable()

might be cheaper to only use immutable data holders if it is to be passed around.

A possible use case for this would be a custom event fired when someone is healed. It should provide copies of
the health data before and after, but event listeners should not be able to change them. Therefore we can write
our event to only provide ``ImmutableHealthData`` instances. That way, even if third party code gets to interact
with our data, we can rest assured that it will not be changed.
78 changes: 78 additions & 0 deletions source/plugin/data/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
============
The Data API
============

Overview
========

The unified Data API aims to provide a consistent way of accessing and modifying data of any kind. Where other
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brain dump on defining "data":

Data

Any type of data that is consistently synchronized from the game server to the clients. If it can be changed and is synchronized to the connected clients, that is Data that the Data API should cover. Properties on the other hand, are data that can not be synchronized from the server to the client throughout the lifetime of the game, without requiring modifications to both the server and the client. Properties are explained in "insert article on properties here".

approaches define the available data using interface and inheritance (like a ``LivingEntity`` interface providing
getter and setter functions for current and maximum health), in Sponge every entity, block etc. is completely
oblivious to what data it holds. While this may appear less straightforward than direct accessor methods, it is
foremost far more extensible. And thanks to the addition of ``Key``\ s, simply accessing specific values is no
less straightforward.

.. warning::

As of writing, the Data API is not yet fully implemented. Refer to the `Implementation Tracker
<https://github.com/SpongePowered/SpongeCommon/issues/8>`_, ask in the ``#spongedev`` IRC channel or on the
`Forums <https://forums.spongepowered.org>`_ to find out if the data you need to work with is available yet.

Concepts
========

On first glance at the API docs, the data API threatens to overwhelm you with lots of interfaces and packages. But
to simply use the data API, you will not have to deal with many of them, as most interfaces found there are just
specific data manipulators.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should likely also include the notion of ValueContainers that are the backend of Keys.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they are the backend, should they really get included into the basic section? I tried to include only those which a plugin developer will directly interact with. Are ValueContainers relevant for basic users or should they rather be spared until documentation for advanced users follows?

DataHolder
~~~~~~~~~~

A data holder is just that - something that holds data. It provides methods to retrieve and offer back data. The
interface itself is completely oblivious to the type of data held. Since only the implementations will possess
this knowledge, it is possible to ask a ``DataHolder`` to provide data it does not have or to accept data it
cannot use. In those cases, the return values of the methods will provide the information that data is not
available (via ``Optional.absent()``) or not accepted.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or DataTransactionResult.Type.FAILURE.


Property
~~~~~~~~

A property is a read-only point of data. It provides information that is directly derived from its holders type,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"holders" --> "holder's"

but, unlike other data, unchangeable (except for core mods). Example for this are the harvesting ablities on tools
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add "it is" before unchangeable

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Example for this are" --> "Examples of this include"

(represented as ``HarvestingProperty``) or the damage absorption of an equippable armor item.

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

A data manipulator represents points of cohesive data that describes a certain component of its holder. For
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 - as sheep, stained glass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting with "... - as sheep..." is worded a bit weirdly... Maybe put a period after "methods" and say "For example, sheep, stained blocks, and leather armor..."

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
similar to direct getter/setter methods. All keys used within Sponge are listed as constants in the
``org.spongepowered.api.data.key.Keys`` utility class.

Value
~~~~~

Within the Data API, a value referred to by a ``Key`` is encoded in a container object. For this documentation, it
is referred to as 'value container' to avoid confusion with the actual value, but note that the class name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should likely find a different word to really avoid confusion as to which ValueContainer you're referring to. Maybe a "keyed value" to represent a Value.

``ValueContainer`` has another meaning in the API. A value container encapsulates the actual data value (if it is
present), a default value (to be used if no direct value is present) and the Key by which the value is identified.

Contents
========

.. toctree::
:maxdepth: 2
:titlesonly:

keys
datamanipulators
transactions
109 changes: 109 additions & 0 deletions source/plugin/data/keys.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
==========
Using Keys
==========

Getting and offering data using a key
=====================================

Since accessing even a single point of data going from ``DataHolder`` to the correct ``DataManipulator`` tended to
be quite tedious and bloat both the code and the import list, a more direct way of accessing values via ``Key``\ s
was devised. Let's just start out with an example.

**Code Example: Healing a data holder, if possible**

.. code-block:: java

import org.spongepowered.api.data.DataHolder;
import org.spongepowered.api.data.key.Keys;

public void heal(DataHolder target) {
if (target.supports(Keys.HEALTH)) {
double maxHealth = target.getOrNull(Keys.MAX_HEALTH);
target.offer(Keys.HEALTH, maxHealth);
}
}

Now for the details of the above function.

The first line checks if our given data holder possesses health. Only if he does, he can be healed after all.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"he" --> "it"

Since a data holder can not have health without having a maximum health and vice versa, a check for one of the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that this logic makes sense... Maybe "Since any particular data holder is not aware of it's data types, check if it the key is applicable to the data holder using supports()."

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, since HEALTH and MAX_HEALTH are treated by the same data processor (as they are part of the same data manipulator), checking if one of them is supported implies the other is too.
Are you saying I should check for both keys, and for the sake of this docs page act like all keys are independent from each other?

keys using the ``supports()`` method suffices.

The second line uses the ``getOrNull()`` function to ask the data holder for its maximum health. Besides
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean get() since getOrNull() was not used.

``getOrNull()``, the methods ``get()`` and ``getOrElse()`` exist, all of which accept a ``Key`` as their first
parameter. Generally, ``get()`` should be used. It will return an ``Optional`` of the data requested or
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Generally, get() should be used. It..." --> "Generally, get() should be used, which will..."

``Optional.absent()`` if the data holder does not support the supplied key. The ``getOrNull()`` method we used is
no more than a shortcut for ``get(key).orNull()``. In our example it is safe to use ``getOrNull()`` since we
already verified that the value will be present in the first line and therefore ``getOrNull()`` will just relieve
us of the ``Optional``. The third possibility would be the ``getOrElse()``, which accepts a default value as a
second parameter to be returned if the value is not present on the data holder.

In the third line, we offer data back to the data holder. We provide a ``Key`` denoting the current health and the
before acquired maximum health, thus healing the data holder to full health. There are a variety of ``offer()``
methods accepting different parameter sets, all of which return a ``DataTransactionResult`` containing information
if the offer was accepted. For now, we'll use the one accepting a ``Key`` and a corresponding value, but we will
encounter more in the next pages. Since we already know that our offer of current health is accepted (as the data
holder supports it), we can silently discard the result.

Transforming Data
=================

Other than getting a value, modify it and offer it back, there is another way of modifying data. Using a data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Other than getting a value, modify it and offer it back" --> "Other than getting, modifying, and offering a value"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"modifiying" --> "interacting with"

holders ``transform()`` method we can pass a ``Key`` and a ``Function``. Internally, the value for the key will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"holders" --> "holder's"

retrieved and the given function applied to it. The result is then stored under the key and the ``transform()``
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"retrieved" --> "be retrieved"

method will return a ``DataTransactionResult`` accordingly.

Now, as an example, imagine we want to buff a data holder by doubling his maximum health.

.. code-block:: java

public void buff(DataHolder target) {
target.transform(Keys.MAX_HEALTH, new Function<Double,Double>() {
@Override
public Double apply(Double input) {
return (input == null) ? 0 : input * 2;
}
});
}

Or, since we use Java 8 and are able to make use of its lambda expressions:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Or if you use Java 8, you're able to shorten the line with lambda expressions:"


.. code-block:: java

public void buff(DataHolder target) {
target.transform(Keys.MAX_HEALTH, d -> (d == null) ? 0 : 2*d);
}

Note that in both cases we need to make sure our passed function can handle ``null``. You will also notice that no
check has been performed if the target actually supports the ``MAX_HEALTH`` key. If a target does not support it,
the ``transform()`` function will fail and return a ``DataTransactionResult`` indicating so.

Value Containers
================

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

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

.. code-block:: java

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);
}
}

Again, we check if our target support the health key and then obtain the value container. 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 value container
back to it. As a value container also contains the ``Key`` used to identify it, calling ``target.offer(health)``
is equivalent to ``target.offer(health.getKey(), health.get())``.
90 changes: 90 additions & 0 deletions source/plugin/data/transactions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
============
Transactions
============

Reading the Result
==================

For everything you ``offer`` to a data holder, the ``offer`` method will yield a ``DataTransactionResult``. This
object will contain the following:

Type
~~~~

The ``Type`` indicates whether the transaction was completed successfully. The following values are possible:

+---------------+----------------------------------------------------------------------------+
| ``UNDEFINED`` | No clear result for the transaction - indicates that something went wrong |
+---------------+----------------------------------------------------------------------------+
| ``SUCCESS`` | Transaction was completed successfully |
+---------------+----------------------------------------------------------------------------+
| ``FAILURE`` | Transaction failed for expected reasons (e.g. incompatible data) |
+---------------+----------------------------------------------------------------------------+
| ``ERROR`` | Transaction failed for unexpected reasons |
+---------------+----------------------------------------------------------------------------+
| ``CANCELLED`` | An event for this transaction was cancelled |
+---------------+----------------------------------------------------------------------------+

A ton of immutable data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this heading is needed? Not sure what it's for.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually left it as a placeholder and then forgot to change it...

~~~~~~~~~~~~~~~~~~~~~~~

The result also provides a couple of lists containing immutable value containers

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

Examples
========

Healing a Player
~~~~~~~~~~~~~~~~

Surely you remember the healing example in the :doc:`keys` page. Imagine a player who is down to half a heart
(which equals 1 health) being healed that way. The ``DataTransactionResult`` in that case would look like this:

- ``getType()`` would return ``SUCCESS``
- ``getRejectedData()`` would be an empty list
- ``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 ``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.

Offering HealthData to a block of stone
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Now our above-mentioned examples are coded in a Way that they will fail silently rather than try to offer the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Way" --> "such a way"

incompatible data. But imagine we took a (fully healed) player's ``HealthData`` and tried to offer it to the
``Location`` of the stone block he's currently standing on. We can do this, since ``Location`` is also a data
holder. And if we do, it would reward us with a ``DataTransactionResult`` like this:

- ``getType()`` would return ``FAILURE``
- ``getRejectedData()`` would contain two value containers for the ``HEALTH`` and ``MAX_HEALTH`` keys, each with a value of 20.0
- ``getReplacedData()`` and ``getSuccessfulData()`` would be empty lists

Reverting Transactions
======================

Since everything about a transaction result is immutable, it can serve for documentation of data changes. And it
also allows for those changes it documents to be undone. For that, simply pass a transaction result to the data
holder's ``undo()`` method. This is particularly useful since some data offerings may be partially successful, so
that one or more values are successfully written to the data holder, yet one more value cannot be accepted. Since
you may wish to undo the partial successes.

**Code example: Reverting a transaction**

.. code-block:: java

public void safeOffer(DataHolder target, DataManipulator data) {
DataTransactionResult result = target.offer(data);
if (result.getType() != DataTransactionResult.Type.SUCCESS) {
target.undo(result);
}
}
1 change: 1 addition & 0 deletions source/plugin/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Contents
text/index
commands/index
events
data/index
blocks/index
entities/index
configuration/index
Expand Down