Skip to content

Commit 58114eb

Browse files
committed
Event promise and thenable added to new gameplay utility library
Some changes to the readme, advances to the tests Tests and linting done Change files Removing change log files, not sure if correct or not Removed last references to math
1 parent 6974f7b commit 58114eb

16 files changed

+712
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "major",
3+
"comment": "Event promise and thenable added to new gameplay utility library",
4+
"packageName": "@minecraft/gameplay-utilities",
5+
"email": "agriffin@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Minecraft Gameplay Utilities
2+
3+
A set of utilities and functions for common gameplay operations. Major pieces are covered below.
4+
5+
## Thenable
6+
7+
A promise-like object which allows for cancellation through external resolution with it's `fulfill` and `reject` functions.
8+
9+
## EventThenable
10+
11+
This object provides a "wait for next event" utility. A wrapper around the `Thenable` object which is designed to be used with Minecraft script event signals. Provide the constructor with a signal and it will resolve the promise when the next event for the provided signal is raised. Also provides a `cancel` function to unregister the event and fulfill the promise with `undefined`.
12+
13+
### Can be awaited to receive the event
14+
15+
```ts
16+
const event = await new EventThenable(world.afterEvents.buttonPush);
17+
```
18+
19+
### Can be used like a promise
20+
21+
```ts
22+
new EventThenable(world.afterEvents.leverAction).then(
23+
(event) => {
24+
// do something with the event
25+
}).finally(() => {
26+
// something else to do
27+
});
28+
```
29+
30+
### Optionally provide filters for the signal and use helper function
31+
32+
```ts
33+
const creeperDeathEvent = await waitForNextEvent(world.afterEvents.entityDie, { entityTypes: ['minecraft:creeper'] });
34+
```
35+
36+
## How to use @minecraft/gameplay-utilities in your project
37+
38+
@minecraft/gameplay-utilities is published to NPM and follows standard semver semantics. To use it in your project,
39+
40+
- Download `@minecraft/gameplay-utilities` from NPM by doing `npm install @minecraft/gameplay-utilities` within your scripts pack. By using `@minecraft/gameplay-utilities`, you will need to do some sort of bundling to merge the library into your packs code. We recommend using [esbuild](https://esbuild.github.io/getting-started/#your-first-bundle) for simplicity.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Config file for API Extractor. For more info, please visit: https://api-extractor.com
3+
*/
4+
{
5+
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
6+
"extends": "@minecraft/api-extractor-base/api-extractor-base.json"
7+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
## API Report File for "@minecraft/gameplay-utilities"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
// @public
8+
export interface EventSignal<T, U> {
9+
// (undocumented)
10+
subscribe(closure: (event: T) => void, filter?: U): (event: T) => void;
11+
// (undocumented)
12+
unsubscribe(closure: (event: T) => void): void;
13+
}
14+
15+
// @public
16+
export class EventThenable<T, U = undefined> extends Thenable<T | undefined> {
17+
constructor(signal: EventSignal<T, U>, filter?: U);
18+
cancel(): void;
19+
}
20+
21+
// @public
22+
export enum PromiseState {
23+
// (undocumented)
24+
FULFILLED = "fulfilled",
25+
// (undocumented)
26+
PENDING = "pending",
27+
// (undocumented)
28+
REJECTED = "rejected"
29+
}
30+
31+
// @public
32+
export class Thenable<T> {
33+
constructor(callback: (fulfill: (value: T) => void, reject: (reason: unknown) => void) => void);
34+
catch(onRejected: (reason: unknown) => unknown): Thenable<unknown>;
35+
finally(onFinally: () => void): Thenable<T>;
36+
fulfill(value: T | Thenable<unknown>): void;
37+
reject(error: unknown): void;
38+
state(): PromiseState;
39+
then<U>(onFulfilled?: (val: T) => U, onRejected?: (reason: unknown) => unknown): Thenable<U>;
40+
}
41+
42+
// @public
43+
export function waitForNextEvent<T, U>(signal: EventSignal<T, U>, filter?: U): EventThenable<T, U>;
44+
45+
// (No @packageDocumentation comment for this package)
46+
47+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import configMinecraftScripting from 'eslint-config-minecraft-scripting';
5+
6+
export default [...configMinecraftScripting];
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { execSync } from 'child_process';
5+
import { argv, series, task, tscTask } from 'just-scripts';
6+
import {
7+
DEFAULT_CLEAN_DIRECTORIES,
8+
apiExtractorTask,
9+
cleanTask,
10+
coreLint,
11+
publishReleaseTask,
12+
vitestTask,
13+
} from '@minecraft/core-build-tasks';
14+
import { copyFileSync, readFileSync } from 'node:fs';
15+
import { resolve } from 'node:path';
16+
17+
const isOnlyBuild = argv()._.findIndex(arg => arg === 'test') === -1;
18+
19+
// Lint
20+
task('lint', coreLint(['src/**/*.ts'], argv().fix));
21+
22+
// Build
23+
task('typescript', tscTask());
24+
task('api-extractor-local', apiExtractorTask('./api-extractor.json', isOnlyBuild /* localBuild */));
25+
task('bundle', () => {
26+
execSync(
27+
'npx esbuild ./lib/index.js --bundle --outfile=dist/minecraft-gameplay-utilities.js --format=esm --sourcemap --external:@minecraft/server'
28+
);
29+
// Copy over type definitions and rename
30+
const officialTypes = JSON.parse(readFileSync('./package.json', 'utf-8'))['types'];
31+
if (!officialTypes) {
32+
// Has the package.json been restructured?
33+
throw new Error('The package.json file does not contain a "types" field. Unable to copy types to bundle.');
34+
}
35+
const officialTypesPath = resolve(officialTypes);
36+
copyFileSync(officialTypesPath, './dist/minecraft-gameplay-utilities.d.ts');
37+
});
38+
task('build', series('typescript', 'api-extractor-local', 'bundle'));
39+
40+
// Test
41+
task('api-extractor-validate', apiExtractorTask('./api-extractor.json', isOnlyBuild /* localBuild */));
42+
task('vitest', vitestTask({ test: argv().test, update: argv().update }));
43+
task('test', series('api-extractor-validate', 'vitest'));
44+
45+
// Clean
46+
task('clean', cleanTask(DEFAULT_CLEAN_DIRECTORIES));
47+
48+
// Post-publish
49+
task('postpublish', () => {
50+
return publishReleaseTask({
51+
repoOwner: 'Mojang',
52+
repoName: 'minecraft-scripting-libraries',
53+
message: 'See attached zip for pre-built minecraft-gameplay-utilities bundle with type declarations.',
54+
});
55+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "@minecraft/gameplay-utilities",
3+
"version": "0.1.0",
4+
"author": "Raphael Landaverde (rlanda@microsoft.com)",
5+
"contributors": [
6+
{
7+
"name": "Jake Shirley",
8+
"email": "jashir@mojang.com"
9+
}
10+
],
11+
"description": "Gameplay utilities for use with minecraft scripting modules",
12+
"exports": {
13+
"import": "./lib/index.js",
14+
"types": "./lib/types/gameplay-utilities-public.d.ts"
15+
},
16+
"type": "module",
17+
"types": "./lib/types/gameplay-utilities-public.d.ts",
18+
"repository": {
19+
"type": "git",
20+
"url": "https://github.com/Mojang/minecraft-scripting-libraries.git",
21+
"directory": "libraries/gameplay-utilities"
22+
},
23+
"scripts": {
24+
"build": "just build",
25+
"lint": "just lint",
26+
"test": "just test",
27+
"clean": "just clean",
28+
"postpublish": "just postpublish"
29+
},
30+
"license": "MIT",
31+
"files": [
32+
"dist",
33+
"lib",
34+
"api-report"
35+
],
36+
"peerDependencies": {
37+
"@minecraft/server": "^1.15.0 || ^2.0.0"
38+
},
39+
"devDependencies": {
40+
"@minecraft/core-build-tasks": "*",
41+
"@minecraft/server": "^2.0.0",
42+
"@minecraft/tsconfig": "*",
43+
"just-scripts": "^2.4.1",
44+
"prettier": "^3.5.3",
45+
"vitest": "^3.0.8"
46+
}
47+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { assert, describe, expect, it } from 'vitest';
5+
import { EventThenable } from './eventThenable.js';
6+
import { PromiseState } from '../thenable.js';
7+
8+
describe('EventThenable', () => {
9+
class Event {
10+
num: number = 0;
11+
}
12+
13+
describe('Non-Filtering Signals', () => {
14+
class Signal {
15+
public closure_: ((e: Event) => void) | undefined = undefined;
16+
17+
public sendEvent(e: Event) {
18+
if (this.closure_ !== undefined) {
19+
this.closure_(e);
20+
}
21+
}
22+
23+
public subscribe(closure: (e: Event) => void) {
24+
this.closure_ = closure;
25+
return closure;
26+
}
27+
28+
public unsubscribe(_: (e: Event) => void) {
29+
this.closure_ = undefined;
30+
}
31+
}
32+
const signal = new Signal();
33+
34+
it('successfully resolve event', () => {
35+
const e = new EventThenable(signal);
36+
assert(signal.closure_ !== undefined);
37+
signal.sendEvent({ num: 4 });
38+
expect(e.state()).toBe(PromiseState.FULFILLED);
39+
});
40+
41+
it('successfully use then on an EventThenable', () => {
42+
new EventThenable(signal).then((event?: Event) => {
43+
assert(event !== undefined);
44+
expect(event.num).toBe(5);
45+
});
46+
signal.sendEvent({ num: 5 });
47+
});
48+
49+
it('successfully cancel an EventThenable', () => {
50+
const e = new EventThenable(signal);
51+
e.then((event?: Event) => {
52+
assert(event === undefined);
53+
});
54+
e.cancel();
55+
expect(e.state()).toBe(PromiseState.FULFILLED);
56+
});
57+
});
58+
59+
describe('Filterable Signals', () => {
60+
class EventFilters {
61+
public someFilterValue: number = 0;
62+
}
63+
64+
class Signal {
65+
public closure_: ((e: Event) => void) | undefined = undefined;
66+
public filters_: EventFilters | undefined = undefined;
67+
68+
public sendEvent(e: Event) {
69+
if (this.closure_ !== undefined) {
70+
this.closure_(e);
71+
}
72+
}
73+
74+
public subscribe(closure: (e: Event) => void, options?: EventFilters) {
75+
this.closure_ = closure;
76+
this.filters_ = options;
77+
return closure;
78+
}
79+
80+
public unsubscribe(_: (e: Event) => void) {
81+
this.closure_ = undefined;
82+
}
83+
}
84+
const signal = new Signal();
85+
86+
// checking that the filters are being passed through to the signal properly
87+
it('successfully create EventThenable with filtered signal with filter', () => {
88+
const e = new EventThenable(signal, { someFilterValue: 18 });
89+
assert(signal.filters_ !== undefined);
90+
expect(signal.filters_.someFilterValue).toBe(18);
91+
signal.sendEvent({ num: 4 });
92+
expect(e.state()).toBe(PromiseState.FULFILLED);
93+
});
94+
95+
it('successfully create EventThenable with filtered signal with no filter', () => {
96+
const e = new EventThenable(signal);
97+
assert(signal.filters_ === undefined);
98+
signal.sendEvent({ num: 4 });
99+
expect(e.state()).toBe(PromiseState.FULFILLED);
100+
});
101+
});
102+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { Thenable } from '../thenable.js';
5+
6+
/**
7+
* Interface representing the functions required to subscribe and unsubscribe to events.
8+
*
9+
* @public
10+
*/
11+
export interface EventSignal<T, U> {
12+
subscribe(closure: (event: T) => void, filter?: U): (event: T) => void;
13+
unsubscribe(closure: (event: T) => void): void;
14+
}
15+
16+
/**
17+
* Helper to create a new EventThenable from an event signal.
18+
*
19+
* @public
20+
*/
21+
export function waitForNextEvent<T, U>(signal: EventSignal<T, U>, filter?: U) {
22+
return new EventThenable(signal, filter);
23+
}
24+
25+
/**
26+
* A promise wrapper utility which returns a new promise that will resolve when the next
27+
* event is raised.
28+
*
29+
* @public
30+
*/
31+
export class EventThenable<T, U = undefined> extends Thenable<T | undefined> {
32+
private onCancel?: () => void;
33+
34+
constructor(signal: EventSignal<T, U>, filter?: U) {
35+
let cancelFn: (() => void) | undefined = undefined;
36+
super((resolve, _) => {
37+
let sub: (event: T) => void;
38+
if (filter === undefined) {
39+
sub = signal.subscribe(event => {
40+
this.onCancel = undefined;
41+
signal.unsubscribe(sub);
42+
resolve(event);
43+
});
44+
} else {
45+
sub = signal.subscribe(event => {
46+
this.onCancel = undefined;
47+
signal.unsubscribe(sub);
48+
resolve(event);
49+
}, filter);
50+
}
51+
52+
cancelFn = () => {
53+
signal.unsubscribe(sub);
54+
resolve(undefined);
55+
};
56+
});
57+
this.onCancel = cancelFn;
58+
}
59+
60+
/**
61+
* Cancels the promise by resolving it with undefined and unsubscribing from the event signal.
62+
*/
63+
cancel() {
64+
if (this.onCancel) {
65+
this.onCancel();
66+
}
67+
}
68+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
export * from './eventThenable.js';

0 commit comments

Comments
 (0)