Core concepts
A few ideas run through the whole API. Read this page once, and the rest of the API reads the way you expect.
The person and the actor
Section titled “The person and the actor”Every edit names two things:
- The person is who is behind the edit: the player who acted, or the player who started a chain that the world carried on.
- The actor is the world actor that carried the edit, such as
Actor.LAVA. It is empty for an edit a person made directly.
Take a player who pours lava. The lava flows down a hill and sets a forest on fire. Oasis logs each block the lava and the fire change with the player as the person, and with Actor.LAVA or Actor.FIRE as the actor. So a rollback of that player puts the whole forest back, and a lookup still tells you that lava did it.
When nobody is behind an edit, such as lava that flows out of a natural lava lake, the person is the id of the Actor that acted. A world actor, such as Actor.LAVA, is then the actor of the edit too: edit.person() is Actor.LAVA.id(), and edit.actor() holds Actor.LAVA. Actor.of(edit.person()) turns a person id back into an actor, and is empty for a player.
The logger and the reader use the same two words. You log the lava above as:
oasis.logger().by(player).via(Actor.LAVA).block(Cause.FLOW, at, air, lava);And you read it back with edit.person(), which is the player’s id, and edit.actor(), which holds Actor.LAVA. A Selection.all().by(player) chooses the edits of that person, the lava and the fire included. A Selection.all().by(Actor.LAVA) chooses all lava: the lava with nobody behind it, and the lava from any player’s bucket.
Selections and requests are immutable
Section titled “Selections and requests are immutable”A Selection is a filter you build from Selection.all(). Every method returns a new selection and changes one part of it. It never changes the selection you call it on.
Selection base = Selection.all().within(Duration.ofHours(2));Selection ofSteve = base.by(steve); // base still selects every playerSelection ofAlex = base.by(alex); // Alex alone: ofSteve did not change baseEach method replaces its part. by(steve).by(alex) selects Alex only. To select both, name them in one call: by(steve, alex). A part you never set does not narrow the selection. not() leaves edits out with the same verbs, and replaces its part the same way.
An OperationRequest works the same way. You can keep one in a field, share it between threads, and start it as often as you like. See Selecting edits and Running operations.
The thread rule
Section titled “The thread rule”Three rules cover every call in the API:
- Every call returns at once. No method waits for the disk. A method that needs the store returns a
CompletableFuture. - Every future and every callback completes on the main thread. That covers pages, estimates, progress callbacks and results. You may touch the world, send messages and open inventories in any of them, with no scheduler call of your own.
- The logger takes edits from any thread. Some of its methods read a Paper object on the thread that calls. Logging edits says which.
A call fails in one of three ways:
- A call you get wrong throws
IllegalArgumentExceptionat once, such as a selection that cannot match anything. See Selecting edits. - A read completes exceptionally with an
OasisException. A lookup, aPage.next(), a history, a kept list, an estimate or a purge count can fail, for example when the store is still opening, holds too many requests, or when a selection names a world that does not exist. The exception carries aFailure. Handle it withexceptionallyorwhenComplete. See When a read fails. - An operation or a purge always completes its result, with the failure inside it: a failed operation ends with
Outcome.FAILEDand aFailure, and a failed purge ends the same way with aPurgeResult.
When Oasis disables, for example while the server stops, it runs no more ticks. A future that has not completed by then never completes, and the logger drops the edits it gets.
Pages carry no totals
Section titled “Pages carry no totals”A Page<T> holds its items(), hasNext(), and next(). It has no total, no page count and no page number.
That is on purpose. To count a selection, the store must read every edit in it, and on a busy server that can be hundreds of millions of rows. A page reads its own edits and one more, so the first page costs the same however much history the server holds. next() reads on from the last edit of the page, so the tenth page costs what the first page costs.
Progress works the same way. An operation counts the chunks it writes, not the edits it will write. When you need a size before an operation starts, ask for it by name with estimate(), which reads the selection once and changes nothing.
Nullness
Section titled “Nullness”Both packages, com.oasis.api and com.oasis.api.event, are @NullMarked with JSpecify. No method takes or returns null. Your IDE and a null checker read the annotation and warn you.
A value that can be absent is an Optional or an OptionalLong: Edit.actor(), Edit.item(), Edit.operation(), Operation.id(), OperationEndEvent.operation(), Result.failure(), Result.incomplete(), Result.forceKept() and the getters of a Selection. No id or count uses 0 or -1 to mean “none”.
The text fields of an edit, before(), after() and text(), are an empty string when the edit has none.
What the API acts for
Section titled “What the API acts for”Oasis.api(this) returns an API that acts for your plugin. Oasis gives your plugin a person of its own, with the key #plugin: followed by your plugin’s name, such as #plugin:GriefRevert.
- An operation you start names your plugin as the one who asked for it, until you name someone else with
requestedBy. Staff see#plugin:GriefRevertin a lookup, and/undoof a staff member never reaches your plugin’s operations. oasis.id()is the id of that person. Oasis makes it from the key, so it is the same on every server.oasis.key(person)is the key staff type afteruserfor a person that is not a player. It is an actor key such as#lava, or#plugin:Namefor a plugin that asked for the API since the server started. It is empty for a player, and for an id Oasis does not know. Ask Bukkit for a player’s name.oasis.operations().active()lists the operations your plugin started that still run. It does not list another plugin’s operations, or the ones staff typed.
String who = oasis.key(event.requester()) .orElseGet(() -> String.valueOf(Bukkit.getOfflinePlayer(event.requester()).getName()));This turns any requester into a name: a plugin, an actor such as #console, or a player.