multyx-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ var _a;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const utils_1 = require("../utils");
5
+ const object_1 = require("./object");
6
+ class MultyxClientList extends object_1.default {
7
+ get value() {
8
+ const parsed = [];
9
+ for (let i = 0; i < this.length; i++)
10
+ parsed[i] = this.get(i).value;
11
+ return parsed;
12
+ }
13
+ constructor(multyx, list, propertyPath = [], editable) {
14
+ super(multyx, {}, propertyPath, editable);
15
+ this.toString = () => this.value.toString();
16
+ this.valueOf = () => this.value;
17
+ this[_a] = () => this.value;
18
+ this.length = 0;
19
+ this.push(...(list instanceof utils_1.EditWrapper ? list.value.map(x => new utils_1.EditWrapper(x)) : list));
20
+ return new Proxy(this, {
21
+ has: (o, p) => {
22
+ if (p in o)
23
+ return true;
24
+ return o.has(p);
25
+ },
26
+ get: (o, p) => {
27
+ if (p in o)
28
+ return o[p];
29
+ return o.get(p);
30
+ },
31
+ set: (o, p, v) => {
32
+ if (p in o) {
33
+ o[p] = v;
34
+ return true;
35
+ }
36
+ return o.set(p, v);
37
+ },
38
+ deleteProperty: (o, p) => {
39
+ return o.delete(p, false);
40
+ }
41
+ });
42
+ }
43
+ set(index, value) {
44
+ if (typeof index == 'string')
45
+ index = parseInt(index);
46
+ if (value === undefined)
47
+ return this.delete(index, false);
48
+ if (value instanceof utils_1.EditWrapper && value.value === undefined)
49
+ return this.delete(index, true);
50
+ const result = super.set(index, value);
51
+ if (result && index >= this.length)
52
+ this.length = index + 1;
53
+ return result;
54
+ }
55
+ delete(index, native = false) {
56
+ if (typeof index == 'string')
57
+ index = parseInt(index);
58
+ const res = super.delete(index, native);
59
+ if (res)
60
+ this.length = this.reduce((a, c, i) => c !== undefined ? i + 1 : a, 0);
61
+ return res;
62
+ }
63
+ /**
64
+ * Create a callback function that gets called for any current or future element in list
65
+ * @param callbackfn Function to call for every element
66
+ */
67
+ forAll(callbackfn) {
68
+ for (let i = 0; i < this.length; i++) {
69
+ callbackfn(this.get(i), i);
70
+ }
71
+ super.forAll((key, value) => callbackfn(value, key));
72
+ }
73
+ /* All general array methods */
74
+ push(...items) {
75
+ for (const item of items)
76
+ this.set(this.length, item);
77
+ return this.length;
78
+ }
79
+ pop() {
80
+ if (this.length === 0)
81
+ return null;
82
+ const res = this.get(this.length);
83
+ this.delete(this.length);
84
+ return res;
85
+ }
86
+ unshift(...items) {
87
+ for (let i = this.length - 1; i >= 0; i--) {
88
+ if (i >= items.length) {
89
+ this.set(i, this.get(i - items.length));
90
+ }
91
+ else {
92
+ this.set(i, items[i]);
93
+ }
94
+ }
95
+ return this.length;
96
+ }
97
+ shift() {
98
+ if (this.length == 0)
99
+ return undefined;
100
+ this.length--;
101
+ const res = this.get(0);
102
+ for (let i = 0; i < this.length; i++) {
103
+ this.set(i, this.get(i + 1));
104
+ }
105
+ return res;
106
+ }
107
+ splice(start, deleteCount, ...items) {
108
+ if (deleteCount === undefined) {
109
+ deleteCount = this.length - start;
110
+ }
111
+ // Move elements in front of splice forward or backward
112
+ let move = items.length - deleteCount;
113
+ if (move > 0) {
114
+ for (let i = this.length - 1; i >= start + deleteCount; i--) {
115
+ this.set(i + move, this.get(i));
116
+ }
117
+ }
118
+ else if (move < 0) {
119
+ for (let i = start + deleteCount; i < this.length; i++) {
120
+ this.set(i + move, this.get(i));
121
+ }
122
+ // Delete elements past end of new list
123
+ const originalLength = this.length;
124
+ for (let i = originalLength + move; i < originalLength; i++) {
125
+ this.set(i, undefined);
126
+ }
127
+ }
128
+ // Insert new elements starting at start
129
+ for (let i = start; i < items.length; i++) {
130
+ this.set(i, items[i]);
131
+ }
132
+ }
133
+ filter(predicate) {
134
+ const keep = [];
135
+ for (let i = 0; i < this.length; i++) {
136
+ keep.push(predicate(this.get(i), i, this));
137
+ }
138
+ let negativeOffset = 0;
139
+ for (let i = 0; i < keep.length; i++) {
140
+ if (keep[i] && negativeOffset)
141
+ this.set(i - negativeOffset, this.get(i));
142
+ if (!keep[i])
143
+ negativeOffset--;
144
+ }
145
+ }
146
+ map(callbackfn) {
147
+ for (let i = 0; i < this.length; i++) {
148
+ this.set(i, callbackfn(this.get(i), i, this));
149
+ }
150
+ }
151
+ flat() {
152
+ for (let i = 0; i < this.length; i++) {
153
+ const item = this.get(i);
154
+ if (item instanceof MultyxClientList) {
155
+ for (let j = 0; j < item.length; j++) {
156
+ i++;
157
+ this.set(i, item[j]);
158
+ }
159
+ }
160
+ }
161
+ }
162
+ reduce(callbackfn, startingAccumulator) {
163
+ for (let i = 0; i < this.length; i++) {
164
+ startingAccumulator = callbackfn(startingAccumulator, this.get(i), i, this);
165
+ }
166
+ return startingAccumulator;
167
+ }
168
+ reduceRight(callbackfn, startingAccumulator) {
169
+ for (let i = this.length - 1; i >= 0; i--) {
170
+ startingAccumulator = callbackfn(startingAccumulator, this.get(i), i, this);
171
+ }
172
+ return startingAccumulator;
173
+ }
174
+ reverse() {
175
+ let right = this.length - 1;
176
+ for (let left = 0; left < right; left++) {
177
+ const a = this.get(left);
178
+ const b = this.get(right);
179
+ this.set(left, b);
180
+ this.set(right, a);
181
+ }
182
+ return this;
183
+ }
184
+ forEach(callbackfn) {
185
+ for (let i = 0; i < this.length; i++) {
186
+ callbackfn(this.get(i), i, this);
187
+ }
188
+ }
189
+ every(predicate) {
190
+ for (let i = 0; i < this.length; i++) {
191
+ if (!predicate(this.get(i), i, this))
192
+ return false;
193
+ }
194
+ return true;
195
+ }
196
+ some(predicate) {
197
+ for (let i = 0; i < this.length; i++) {
198
+ if (predicate(this.get(i), i, this))
199
+ return true;
200
+ }
201
+ return false;
202
+ }
203
+ find(predicate) {
204
+ for (let i = 0; i < this.length; i++) {
205
+ if (predicate(this.get(i), i, this))
206
+ return this.get(i);
207
+ }
208
+ return undefined;
209
+ }
210
+ findIndex(predicate) {
211
+ for (let i = 0; i < this.length; i++) {
212
+ if (predicate(this.get(i), i, this))
213
+ return i;
214
+ }
215
+ return -1;
216
+ }
217
+ deorder() {
218
+ const values = [];
219
+ for (const index in this.object) {
220
+ values.push(this.get(index));
221
+ }
222
+ return values;
223
+ }
224
+ deorderEntries() {
225
+ const values = [];
226
+ for (const index in this.object) {
227
+ values.push([parseInt(index), this.get(index)]);
228
+ }
229
+ return values;
230
+ }
231
+ entries() {
232
+ const entryList = [];
233
+ for (let i = 0; i < this.length; i++) {
234
+ entryList.push([this.get(i), i]);
235
+ }
236
+ return entryList;
237
+ }
238
+ keys() {
239
+ return Array(this.length).fill(0).map((_, i) => i);
240
+ }
241
+ /* Native methods to allow MultyxClientList to be treated as array */
242
+ [Symbol.iterator]() {
243
+ const values = [];
244
+ for (let i = 0; i < this.length; i++)
245
+ values[i] = this.get(i);
246
+ return values[Symbol.iterator]();
247
+ }
248
+ }
249
+ _a = Symbol.toPrimitive;
250
+ exports.default = MultyxClientList;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const message_1 = require("../message");
4
+ const utils_1 = require("../utils");
5
+ const router_1 = require("./router");
6
+ const value_1 = require("./value");
7
+ class MultyxClientObject {
8
+ get value() {
9
+ const parsed = {};
10
+ for (const prop in this.object)
11
+ parsed[prop] = this.object[prop];
12
+ return parsed;
13
+ }
14
+ constructor(multyx, object, propertyPath = [], editable) {
15
+ this.object = {};
16
+ this.propertyPath = propertyPath;
17
+ this.multyx = multyx;
18
+ this.editable = editable;
19
+ this.setterListeners = [];
20
+ if (object instanceof MultyxClientObject)
21
+ object = object.value;
22
+ for (const prop in (object instanceof utils_1.EditWrapper ? object.value : object)) {
23
+ this.set(prop, object instanceof utils_1.EditWrapper
24
+ ? new utils_1.EditWrapper(object.value[prop])
25
+ : object[prop]);
26
+ }
27
+ if (this.constructor !== MultyxClientObject)
28
+ return;
29
+ return new Proxy(this, {
30
+ has: (o, p) => {
31
+ return o.has(p);
32
+ },
33
+ get: (o, p) => {
34
+ if (p in o)
35
+ return o[p];
36
+ return o.get(p);
37
+ },
38
+ set: (o, p, v) => {
39
+ if (p in o) {
40
+ o[p] = v;
41
+ return true;
42
+ }
43
+ return o.set(p, v);
44
+ },
45
+ deleteProperty: (o, p) => {
46
+ return o.delete(p, false);
47
+ }
48
+ });
49
+ }
50
+ has(property) {
51
+ return property in this.object;
52
+ }
53
+ get(property) {
54
+ return this.object[property];
55
+ }
56
+ set(property, value) {
57
+ if (value === undefined)
58
+ return this.delete(property);
59
+ // Only create new MultyxClientItem when needed
60
+ if (this.object[property] instanceof value_1.default)
61
+ return this.object[property].set(value);
62
+ // If value was deleted by the server
63
+ if (value instanceof utils_1.EditWrapper && value.value === undefined)
64
+ return this.delete(property, true);
65
+ // Attempting to edit property not editable to client
66
+ if (!(value instanceof utils_1.EditWrapper) && !this.editable) {
67
+ if (this.multyx.options.verbose) {
68
+ console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + property}' to ${value}`);
69
+ }
70
+ return false;
71
+ }
72
+ // Creating a new value
73
+ this.object[property] = new ((0, router_1.default)(value instanceof utils_1.EditWrapper ? value.value : value))(this.multyx, value, [...this.propertyPath, property], this.editable);
74
+ // We have to push into queue, since object may not be fully created
75
+ // and there may still be more updates to parse
76
+ for (const listener of this.setterListeners) {
77
+ this.multyx[utils_1.Add](() => {
78
+ if (this.has(property))
79
+ listener(property, this.get(property));
80
+ });
81
+ }
82
+ // Relay change to server if not edit wrapped
83
+ if (!(value instanceof utils_1.EditWrapper))
84
+ this.multyx.ws.send(message_1.Message.Native({
85
+ instruction: 'edit',
86
+ path: this.propertyPath,
87
+ value: this.object[property].value
88
+ }));
89
+ return true;
90
+ }
91
+ delete(property, native = false) {
92
+ // Attempting to edit property not editable by client
93
+ if (!this.editable && !native) {
94
+ if (this.multyx.options.verbose) {
95
+ console.error(`Attempting to delete property that is not editable. Deleting '${this.propertyPath.join('.') + '.' + property}'`);
96
+ }
97
+ return false;
98
+ }
99
+ delete this.object[property];
100
+ if (!native) {
101
+ this.multyx.ws.send(message_1.Message.Native({
102
+ instruction: 'edit',
103
+ path: [...this.propertyPath, property],
104
+ value: undefined
105
+ }));
106
+ }
107
+ return true;
108
+ }
109
+ /**
110
+ * Create a callback function that gets called for any current or future property in object
111
+ * @param callbackfn Function to call for every property
112
+ */
113
+ forAll(callbackfn) {
114
+ for (let prop in this.object) {
115
+ callbackfn(prop, this.get(prop));
116
+ }
117
+ this.setterListeners.push(callbackfn);
118
+ }
119
+ keys() {
120
+ return Object.keys(this.object);
121
+ }
122
+ values() {
123
+ return Object.values(this.object);
124
+ }
125
+ entries() {
126
+ const entryList = [];
127
+ for (let prop in this.object) {
128
+ entryList.push([prop, this.get(prop)]);
129
+ }
130
+ return entryList;
131
+ }
132
+ /**
133
+ * Unpack constraints from server
134
+ * @param constraints Packed constraints object mirroring MultyxClientObject shape
135
+ */
136
+ [utils_1.Unpack](constraints) {
137
+ for (const prop in constraints) {
138
+ this.object[prop][utils_1.Unpack](constraints[prop]);
139
+ }
140
+ }
141
+ }
142
+ exports.default = MultyxClientObject;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = MultyxClientItemRouter;
4
+ function MultyxClientItemRouter(data) {
5
+ return Array.isArray(data) ? require('./list').default
6
+ : typeof data == 'object' ? require('./object').default
7
+ : require('./value').default;
8
+ }
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var _a;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const message_1 = require("../message");
5
+ const utils_1 = require("../utils");
6
+ class MultyxClientValue {
7
+ get value() {
8
+ if (this.interpolator)
9
+ return this.interpolator.get();
10
+ return this._value;
11
+ }
12
+ set value(v) {
13
+ this._value = v;
14
+ if (this.interpolator)
15
+ this.interpolator.set();
16
+ }
17
+ constructor(multyx, value, propertyPath = [], editable) {
18
+ /* Native methods to allow MultyxValue to be treated as primitive */
19
+ this.toString = () => this.value.toString();
20
+ this.valueOf = () => this.value;
21
+ this[_a] = () => this.value;
22
+ this.propertyPath = propertyPath;
23
+ this.editable = editable;
24
+ this.multyx = multyx;
25
+ this.constraints = {};
26
+ this.set(value);
27
+ }
28
+ set(value) {
29
+ if (value instanceof utils_1.EditWrapper) {
30
+ this.value = value.value;
31
+ return true;
32
+ }
33
+ // Attempting to edit property not editable to client
34
+ if (!this.editable) {
35
+ if (this.multyx.options.verbose) {
36
+ console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.')}' to ${value}`);
37
+ }
38
+ return false;
39
+ }
40
+ let nv = value;
41
+ for (const constraint in this.constraints) {
42
+ const fn = this.constraints[constraint];
43
+ nv = fn(nv);
44
+ if (nv === null) {
45
+ if (this.multyx.options.verbose) {
46
+ console.error(`Attempting to set property that failed on constraint. Setting '${this.propertyPath.join('.')}' to ${value}, stopped by constraint '${constraint}'`);
47
+ }
48
+ return false;
49
+ }
50
+ }
51
+ if (this.value === nv) {
52
+ this.value = nv;
53
+ return true;
54
+ }
55
+ this.value = nv;
56
+ this.multyx.ws.send(message_1.Message.Native({
57
+ instruction: 'edit',
58
+ path: this.propertyPath,
59
+ value: nv
60
+ }));
61
+ return true;
62
+ }
63
+ /**
64
+ * Unpack constraints sent from server and store
65
+ * @param constraints Packed constraints from server
66
+ */
67
+ [utils_1.Unpack](constraints) {
68
+ for (const [cname, args] of Object.entries(constraints)) {
69
+ const constraint = (0, utils_1.BuildConstraint)(cname, args);
70
+ if (!constraint)
71
+ continue;
72
+ this.constraints[cname] = constraint;
73
+ }
74
+ }
75
+ /**
76
+ * Linearly interpolate value across frames
77
+ * Will run 1 frame behind on average
78
+ */
79
+ Lerp() {
80
+ this.interpolator = {
81
+ history: [
82
+ { value: this._value, time: Date.now() },
83
+ { value: this._value, time: Date.now() }
84
+ ],
85
+ get: () => {
86
+ const [e, s] = this.interpolator.history;
87
+ const ratio = Math.min(1, (Date.now() - e.time) / Math.min(250, e.time - s.time));
88
+ if (Number.isNaN(ratio) || typeof e.value != 'number' || typeof s.value != 'number')
89
+ return e.value;
90
+ return e.value * ratio + s.value * (1 - ratio);
91
+ },
92
+ set: () => {
93
+ this.interpolator.history.pop();
94
+ this.interpolator.history.unshift({
95
+ value: this._value,
96
+ time: Date.now()
97
+ });
98
+ }
99
+ };
100
+ }
101
+ PredictiveLerp() {
102
+ this.interpolator = {
103
+ history: [
104
+ { value: this._value, time: Date.now() },
105
+ { value: this._value, time: Date.now() },
106
+ { value: this._value, time: Date.now() }
107
+ ],
108
+ get: () => {
109
+ const [e, s, p] = this.interpolator.history;
110
+ const ratio = Math.min(1, (Date.now() - e.time) / (e.time - s.time));
111
+ if (Number.isNaN(ratio) || typeof p.value != 'number')
112
+ return e.value;
113
+ if (typeof e.value != 'number' || typeof s.value != 'number')
114
+ return e.value;
115
+ // Speed changed too fast, don't interpolate, return new value
116
+ if (Math.abs((e.value - s.value) / (s.value - p.value) - 1) > 0.2)
117
+ return e.value;
118
+ return e.value * (1 + ratio) - s.value * ratio;
119
+ },
120
+ set: () => {
121
+ this.interpolator.history.pop();
122
+ this.interpolator.history.unshift({
123
+ value: this._value,
124
+ time: Date.now()
125
+ });
126
+ }
127
+ };
128
+ }
129
+ }
130
+ _a = Symbol.toPrimitive;
131
+ exports.default = MultyxClientValue;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Message = void 0;
4
+ class Message {
5
+ constructor(name, data, native = false) {
6
+ this.name = name;
7
+ this.data = data;
8
+ this.time = Date.now();
9
+ this.native = native;
10
+ }
11
+ static BundleOperations(deltaTime, operations) {
12
+ if (!Array.isArray(operations))
13
+ operations = [operations];
14
+ return JSON.stringify(new Message('_', { operations, deltaTime }));
15
+ }
16
+ static Native(update) {
17
+ return JSON.stringify(new Message('_', update, true));
18
+ }
19
+ static Parse(str) {
20
+ const parsed = JSON.parse(str);
21
+ if (parsed.name[0] == '_')
22
+ parsed.name = parsed.name.slice(1);
23
+ return new Message(parsed.name, parsed.data, parsed.name == '');
24
+ }
25
+ static Create(name, data) {
26
+ if (name.length == 0)
27
+ throw new Error('Multyx message cannot have empty name');
28
+ if (name[0] == '_')
29
+ name = '_' + name;
30
+ if (typeof data === 'function') {
31
+ throw new Error('Multyx data must be JSON storable');
32
+ }
33
+ return JSON.stringify(new Message(name, data));
34
+ }
35
+ }
36
+ exports.Message = Message;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DefaultOptions = void 0;
4
+ exports.DefaultOptions = {
5
+ port: 443,
6
+ secure: false,
7
+ uri: 'localhost',
8
+ verbose: false,
9
+ logUpdateFrame: false,
10
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/utils.js ADDED
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EditWrapper = exports.Add = exports.Done = exports.Unpack = void 0;
4
+ exports.Interpolate = Interpolate;
5
+ exports.BuildConstraint = BuildConstraint;
6
+ exports.Unpack = Symbol("unpack");
7
+ exports.Done = Symbol("done");
8
+ exports.Add = Symbol("add");
9
+ class EditWrapper {
10
+ constructor(value) {
11
+ this.value = value;
12
+ }
13
+ }
14
+ exports.EditWrapper = EditWrapper;
15
+ /**
16
+ * Set a customized interpolation curve for values to follow
17
+ * @param values Slices to interpolate through. Time must be between 0 and 1, while progress is the percentage between the old value and new value at the respective time, where 0 represents old value and 1 represents new value
18
+ * @example
19
+ * ```js
20
+ * car.get('speed').interpolate([
21
+ * { time: 0, progress: 0 },
22
+ * { time: 0.2, progress: 0.6 },
23
+ * { time: 0.4, progress: 1.2 },
24
+ * { time: 0.6, progress: 1.4 },
25
+ * { time: 0.8, progress: 1.2 },
26
+ * { time: 1, progress: 1 }
27
+ * ]);
28
+ * ```
29
+ */
30
+ function Interpolate(object, property, interpolationCurve) {
31
+ let start = { value: object[property], time: Date.now() };
32
+ let end = { value: object[property], time: Date.now() };
33
+ Object.defineProperty(object, property, {
34
+ get: () => {
35
+ const time = end.time - start.time;
36
+ let lower = interpolationCurve[0];
37
+ let upper = interpolationCurve[0];
38
+ for (const slice of interpolationCurve) {
39
+ if (time > slice.time && slice.time > lower.time)
40
+ lower = slice;
41
+ if (time < slice.time && slice.time < upper.time)
42
+ upper = slice;
43
+ }
44
+ const sliceTime = (time - lower.time) / (upper.time - lower.time);
45
+ const ratio = lower.progress + sliceTime * (upper.progress - lower.progress);
46
+ if (Number.isNaN(ratio))
47
+ return start.value;
48
+ return end.value * ratio + start.value * (1 - ratio);
49
+ },
50
+ set: (value) => {
51
+ // Don't lerp between edit requests sent in same frame
52
+ if (Date.now() - end.time < 10) {
53
+ end.value = value;
54
+ return true;
55
+ }
56
+ start = Object.assign({}, end);
57
+ end = { value, time: Date.now() };
58
+ return true;
59
+ }
60
+ });
61
+ }
62
+ function BuildConstraint(name, args) {
63
+ if (name == 'min')
64
+ return n => n >= args[0] ? n : args[0];
65
+ if (name == 'max')
66
+ return n => n <= args[0] ? n : args[0];
67
+ if (name == 'int')
68
+ return n => Math.floor(n);
69
+ if (name == 'ban')
70
+ return n => args.includes(n) ? null : n;
71
+ if (name == 'disabled')
72
+ return n => args[0] ? null : n;
73
+ return I => I;
74
+ }