-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathwebapp.cpp
More file actions
1746 lines (1454 loc) · 59.4 KB
/
Copy pathwebapp.cpp
File metadata and controls
1746 lines (1454 loc) · 59.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include <cassert>
#include <cstdarg>
#include <cstdint>
#include <optional>
#include <memory>
#include <span>
#include <string>
#include <utility>
#include <vector>
#include <emscripten.h>
#include <emscripten/bind.h>
// EM_ASM doesn't work properly, because we share generated JS between puzzles.
// (But EM_JS works just fine.)
#undef EM_ASM
#undef EM_ASM_INT
#undef EM_ASM_DOUBLE
#undef EM_ASM_PTR
extern "C" {
#include "puzzles.h"
}
using namespace emscripten;
EM_JS(void, throw_js_error, (const char* message), {
throw new Error(UTF8ToString(message));
});
std::string slugify(const std::string& text) {
std::string slug;
slug.reserve(text.length());
bool last_was_delimiter = false;
for (const unsigned char c : text) {
if (c > 127) {
fatal("slugify: non-ASCII character: 0x%02X", c);
}
if (c == '(' && !slug.empty()) {
// Slugify "Size (s*s)" as "size" not "size-s-s"
break;
}
if (std::isalnum(c) || c == '%') {
if (last_was_delimiter && !slug.empty()) {
slug += '-';
}
if (c == '%') {
// For Bridges (e.g.,) slugify "Expansion factor (%age)" to
// "expansion-factor-percentage" not "expansion-factor-age"
slug.append("percent");
} else {
slug += static_cast<char>(std::tolower(c));
}
last_was_delimiter = false;
} else {
last_was_delimiter = true;
}
}
return slug;
}
/*
* Smart pointers for C data types
*/
/**
* @brief A base utility class for managing pointers with custom deleters.
* (This is essentially std::unique_ptr<T, decltype(deleter)>,
* but ensures the default deleter is never accidentally used.)
* @tparam T The type of the pointer to manage (e.g., char).
* @tparam deleter The deleter function for the pointer (e.g., sfree).
*/
template <typename T, auto& deleter>
class allocated_ptr_base {
protected:
std::unique_ptr<T, decltype(deleter)> m_ptr;
public:
explicit allocated_ptr_base(T* raw_ptr)
: m_ptr(raw_ptr, deleter) {}
// Non-copyable
allocated_ptr_base(const allocated_ptr_base&) = delete;
allocated_ptr_base& operator=(const allocated_ptr_base&) = delete;
// Movable
allocated_ptr_base(allocated_ptr_base&& other) noexcept = default;
allocated_ptr_base& operator=(allocated_ptr_base&& other) noexcept = default;
/**
* @brief Provides access to the raw managed pointer.
* @warning Use with caution. The caller must not free this pointer directly.
*/
[[nodiscard]] T* get() const { return m_ptr.get(); }
/**
* @brief Checks if the object is managing a valid (non-nullptr) pointer.
*/
explicit operator bool() const noexcept { return static_cast<bool>(m_ptr); }
};
/**
* @brief A mixin class providing string conversion methods for string-like types.
* @tparam Derived The derived class that inherits from this mixin.
* It must define a `get()` method that returns a type that
* can be passed to std::string() (e.g., `char *`).
*/
template <typename Derived>
class string_mixins {
[[nodiscard]] const Derived* derived() const {
return static_cast<const Derived*>(this);
}
public:
[[nodiscard]] std::string as_string() const {
if (const auto value = derived()->get()) {
return std::string(value);
}
return {};
}
[[nodiscard]] std::optional<std::string> as_optional_string() const {
if (const auto value = derived()->get()) {
return std::make_optional(std::string(value));
}
return std::nullopt;
}
};
/**
* A char* allocated in C that must be freed with sfree().
* Supports as_string() and as_optional_string().
*/
class allocated_char_ptr:
public allocated_ptr_base<char, sfree>,
public string_mixins<allocated_char_ptr>
{
public:
explicit allocated_char_ptr(char* raw_ptr) : allocated_ptr_base(raw_ptr) {}
};
/**
* A static const char* from C that must not be freed.
* (All midend error messages are static.)
* Supports as_string() and as_optional_string().
*/
class static_char_ptr: public string_mixins<static_char_ptr> {
const char* m_ptr;
public:
explicit static_char_ptr(const char* raw_ptr) : m_ptr(raw_ptr) {}
// Non-owning, so default copy/move are fine
static_char_ptr(const static_char_ptr&) = default;
static_char_ptr& operator=(const static_char_ptr&) = default;
static_char_ptr(static_char_ptr&&) noexcept = default;
static_char_ptr& operator=(static_char_ptr&&) noexcept = default;
[[nodiscard]] const char* get() const { return m_ptr; }
explicit operator bool() const noexcept { return m_ptr != nullptr; }
};
class allocated_float_ptr: public allocated_ptr_base<float, sfree> {
public:
explicit allocated_float_ptr(float* raw_ptr) : allocated_ptr_base(raw_ptr) {}
};
// Converting std::vector to JS Array:
//
// We'd like to write `typedef std::vector<Foo> FooList`, and have FooList
// turn into TypeScript `Foo[]` (assuming Foo is also embindable). Unfortunately:
// - `register_vector<Foo>("FooList")` results in a custom JS Vector class,
// which isn't iterable or indexable or usable like an ordinary JS Array.
// - Implicit bindings from vector to array via custom marshaling would be ideal
// (https://github.com/emscripten-core/emscripten/issues/11070#issuecomment-717675128)
// but that specific implementation causes compliation errors in current Emscripten.
//
// Instead, declare a custom emscripten::val type and convert manually:
// EMSCRIPTEN_DECLARE_VAL_TYPE(FooList);
// EMSCRIPTEN_BINDINGS(...) { register_type<FooList>("Foo[]"); }
// Conversion (in C++):
// FooList fooArray = val::array(foo_vector).as<FooList>();
// std::vector<Foo> foo_vector = val::vecFromJSArray<Foo>(fooArray);
/*
* Embind value objects
* (Default constructors are required for embind)
*/
// [r, g, b] array: matches the layout of the midend_colours return value.
typedef std::array<float, 3> Colour;
EMSCRIPTEN_DECLARE_VAL_TYPE(ColourList);
// JS-ified options for DrawingAPI.drawText
EMSCRIPTEN_DECLARE_VAL_TYPE(TextAlign); // "left" | "center" | "right"
EMSCRIPTEN_DECLARE_VAL_TYPE(TextBaseline); // "alphabetic" | "mathematical"
EMSCRIPTEN_DECLARE_VAL_TYPE(FontType); // "fixed" | "variable"
struct DrawTextOptions {
TextAlign align;
TextBaseline baseline;
FontType fontType;
int size;
DrawTextOptions() : align(to_halign(ALIGN_HLEFT)),
baseline(to_valign(ALIGN_VNORMAL)),
fontType(to_fontType(FONT_VARIABLE)),
size(12) {}
// From drawing_api draw_text params:
DrawTextOptions(
int _fonttype, int _fontsize, int _align
) : align(to_halign(_align)),
baseline(to_valign(_align)),
fontType(to_fontType(_fonttype)),
size(_fontsize) {}
private:
static TextAlign to_halign(int align) {
static constexpr int ALIGN_HMASK = ALIGN_HLEFT | ALIGN_HCENTRE | ALIGN_HRIGHT;
if ((align & ALIGN_HMASK) == ALIGN_HLEFT)
return val("left").as<TextAlign>();
if ((align & ALIGN_HMASK) == ALIGN_HCENTRE)
return val("center").as<TextAlign>();
return val("right").as<TextAlign>();
}
static TextBaseline to_valign(int align) {
static constexpr int ALIGN_VMASK = ALIGN_VCENTRE | ALIGN_VNORMAL;
if ((align & ALIGN_VMASK) == ALIGN_VCENTRE)
return val("mathematical").as<TextBaseline>();
return val("alphabetic").as<TextBaseline>();
}
static FontType to_fontType(int fonttype) {
if (fonttype == FONT_FIXED)
return val("fixed").as<FontType>();
return val("variable").as<FontType>();
}
};
struct KeyLabel {
std::string label;
int button = 0;
KeyLabel() = default;
explicit KeyLabel(const key_label &_key) :
label(std::string(_key.label)),
button(_key.button) {}
};
EMSCRIPTEN_DECLARE_VAL_TYPE(KeyLabelList);
// Although most drawing API functions use int coords,
// draw_thick_line uses float. Since both map to JS number,
// use floats here to avoid having two different `Point` objects in JS.
struct Point {
float x, y;
Point() : x(0), y(0) {}
Point(const float _x, const float _y) : x(_x), y(_y) {}
Point(const int _x, const int _y)
: x(static_cast<float>(_x)), y(static_cast<float>(_y)) {}
// IntPoint is useful for draw_polygon argument coercion.
typedef struct {
int x, y;
} IntPoint;
explicit Point(const IntPoint &_p) : Point(_p.x, _p.y) {}
};
EMSCRIPTEN_DECLARE_VAL_TYPE(PointList);
struct Rect {
int x, y, w, h;
Rect() : x(0), y(0), w(0), h(0) {}
Rect(int _x, int _y, int _w, int _h) : x(_x), y(_y), w(_w), h(_h) {}
};
typedef std::optional<Rect> OptionalRect;
struct Size {
int w, h;
Size() : w(0), h(0) {}
Size(int _w, int _h) : w(_w), h(_h) {}
};
EMSCRIPTEN_DECLARE_VAL_TYPE(StringList);
EMSCRIPTEN_BINDINGS(utilities) {
value_array<Colour>("Colour")
.element(emscripten::index<0>())
.element(emscripten::index<1>())
.element(emscripten::index<2>());
register_type<ColourList>("Colour[]");
// Would like to use lib.dom.d.ts CanvasTextAlign and CanvasTextBaseline,
// but tsgen throws `BindingError: emval::as has unknown type 15CanvasTextAlign`.
register_type<TextAlign>(R"("left" | "center" | "right")");
register_type<TextBaseline>(R"("alphabetic" | "mathematical")");
register_type<FontType>(R"("fixed" | "variable")");
value_object<DrawTextOptions>("DrawTextOptions")
.field("align", &DrawTextOptions::align)
.field("baseline", &DrawTextOptions::baseline)
.field("fontType", &DrawTextOptions::fontType)
.field("size", &DrawTextOptions::size);
value_object<KeyLabel>("KeyLabel")
.field("label", &KeyLabel::label)
.field("button", &KeyLabel::button);
register_type<KeyLabelList>("KeyLabel[]");
value_object<Point>("Point").field("x", &Point::x).field("y", &Point::y);
register_type<PointList>("Point[]");
value_object<Rect>("Rect")
.field("x", &Rect::x)
.field("y", &Rect::y)
.field("w", &Rect::w)
.field("h", &Rect::h);
register_optional<Rect>();
value_object<Size>("Size").field("w", &Size::w).field("h", &Size::h);
register_optional<int>();
register_optional<std::string>();
register_type<StringList>("string[]");
}
/*
* Drawing class -- implemented in JS
*/
// This allows implementing the drawing_api in JS code, (mostly) with type
// checking on both sides, and using embind's generated ClassHandle glue
// for interoperatbility between C and JS object instances.
//
// Embind's mechanism for a JS implementation requires listing each function
// three times:
// 1. In `class Drawing`, an abstract base class that declares a pure virtual
// method for each function. This is neccessary for embind to allow JS
// overrides of the functions. (We use camelCase to match JS norms.)
// 2. In `class DrawingWrapper`, a concrete implementation of `class Drawing`
// that calls out to JS methods. This layer also maps C types to and from
// embind `val` (native JS) types to simplify the JS code.
// 3. In EMSCRIPTEN_BINDINGS, to declare the DrawingWrapper methods
// available for implementation/use in JS.
//
// Compiling generates a .d.ts file that exports TypeScript interfaces for
// `DrawingWrapper` (the functions that must be implemented in JS), `Drawing`
// (the object type that must be passed to `Frontend.setDrawing`), and a module property
// `Drawing` that is used to bind an instance of the JS DrawingWrapper's
// implementation to C code, by calling `module.Drawing.implement(instance)`.
EMSCRIPTEN_DECLARE_VAL_TYPE(Blitter);
constexpr float default_line_thickness = 1.0f;
class Drawing {
public:
virtual ~Drawing() = default;
virtual void drawText(
const Point &origin, const DrawTextOptions &options, int colour,
const std::string &text
) = 0;
virtual void drawRect(const Rect &rect, int colour) = 0;
virtual void drawLine(
const Point &start, const Point &end, int colour, float thickness
) = 0;
void drawLine(const Point &start, const Point &end, int colour) {
return drawLine(start, end, colour, default_line_thickness);
}
virtual void drawPolygon(
const PointList &coords, int fillcolour, int outlinecolour
) = 0;
virtual void drawCircle(
const Point &origin, int radius, int fillcolour, int outlinecolour
) = 0;
virtual void drawUpdate(const Rect &rect) = 0;
virtual void clip(const Rect &rect) = 0;
virtual void unclip() = 0;
virtual void startDraw() = 0;
virtual void endDraw() = 0;
virtual Blitter blitterNew(const Size &size) = 0;
virtual void blitterFree(const Blitter &bl) = 0;
virtual void blitterSave(const Blitter &bl, const Point &origin) = 0;
virtual void blitterLoad(const Blitter &bl, const Point &origin) = 0;
};
class DrawingWrapper : public wrapper<Drawing> {
public:
EMSCRIPTEN_WRAPPER(explicit DrawingWrapper);
void drawText(
const Point &origin, const DrawTextOptions &options, int colour,
const std::string &text
) override {
return call<void>("drawText", origin, options, colour, text);
}
void drawRect(const Rect &rect, int colour) override {
return call<void>("drawRect", rect, colour);
}
void drawLine(
const Point &start, const Point &end, int colour, float thickness
) override {
// This combines drawing_api's draw_line and draw_thick_line.
return call<void>("drawLine", start, end, colour, thickness);
}
void drawPolygon(
const PointList &coords, int fillcolour, int outlinecolour
) override {
return call<void>("drawPolygon", coords, fillcolour, outlinecolour);
}
void drawCircle(
const Point &origin, int radius, int fillcolour, int outlinecolour
) override {
return call<void>("drawCircle", origin, radius, fillcolour, outlinecolour);
}
void drawUpdate(const Rect &rect) override {
return call<void>("drawUpdate", rect);
}
void clip(const Rect &rect) override { return call<void>("clip", rect); }
void unclip() override { return call<void>("unclip"); }
void startDraw() override { return call<void>("startDraw"); }
void endDraw() override { return call<void>("endDraw"); }
Blitter blitterNew(const Size &size) override {
return call<Blitter>("blitterNew", size).as<Blitter>();
}
void blitterFree(const Blitter &bl) override {
return call<void>("blitterFree", bl);
}
void blitterSave(const Blitter &bl, const Point &origin) override {
return call<void>("blitterSave", bl, origin);
}
void blitterLoad(const Blitter &bl, const Point &origin) override {
return call<void>("blitterLoad", bl, origin);
}
// (Printing API not implemented)
};
EMSCRIPTEN_BINDINGS(drawing) {
register_type<Blitter>("unknown");
// ReSharper disable once CppExpressionWithoutSideEffects
class_<Drawing>("Drawing")
.smart_ptr<std::shared_ptr<Drawing> >("Drawing")
.function("drawText(origin, options, colour, text)", &DrawingWrapper::drawText)
.function("drawRect(rect, colour)", &DrawingWrapper::drawRect)
.function("drawLine(p1, p2, colour, thickness)", &DrawingWrapper::drawLine)
.function(
"drawPolygon(coords, fillcolour, outlinecolour)",
&DrawingWrapper::drawPolygon
)
.function(
"drawCircle(centre, radius, fillcolour, outlinecolour)",
&DrawingWrapper::drawCircle
)
.function("drawUpdate(rect)", &DrawingWrapper::drawUpdate)
.function("clip(rect)", &DrawingWrapper::clip)
.function("unclip", &DrawingWrapper::unclip)
.function("startDraw", &DrawingWrapper::startDraw)
.function("endDraw", &DrawingWrapper::endDraw)
.function("blitterNew(size)", &DrawingWrapper::blitterNew)
.function("blitterFree(blitter)", &DrawingWrapper::blitterFree)
.function("blitterSave(blitter, origin)", &DrawingWrapper::blitterSave)
.function("blitterLoad(blitter, origin)", &DrawingWrapper::blitterLoad)
.allow_subclass<DrawingWrapper>("DrawingWrapper");
}
/*
* Drawing API
*/
Drawing *DRAWING(const drawing *dr);
struct blitter {
// an emscripten::val -- any JS object or value
const Blitter js_value;
explicit blitter(Blitter _value) : js_value(std::move(_value)) {}
};
void js_draw_text(
drawing *dr, int x, int y, int fonttype, int fontsize, int align,
int colour, const char *text
) {
const auto options = DrawTextOptions(fonttype, fontsize, align);
DRAWING(dr)->drawText(Point(x, y), options, colour, std::string(text));
}
void js_draw_rect(drawing *dr, int x, int y, int w, int h, int colour) {
DRAWING(dr)->drawRect(Rect(x, y, w, h), colour);
}
void js_draw_line(drawing *dr, int x1, int y1, int x2, int y2, int colour) {
DRAWING(dr)->drawLine(Point(x1, y1), Point(x2, y2), colour);
}
void js_draw_polygon(
drawing *dr, const int *coords, int npoints, int fillcolour,
int outlinecolour
) {
static_assert(
sizeof(Point::IntPoint) == 2 * sizeof(*coords),
"_IntPoint doesn't match draw_polygon coords layout"
);
auto points = reinterpret_cast<const Point::IntPoint *>(coords);
auto points_vec = std::vector<Point>();
points_vec.reserve(npoints);
for (const auto point_ptr: std::span(points, npoints))
points_vec.emplace_back(point_ptr);
auto point_list = val::array(points_vec).as<PointList>();
DRAWING(dr)->drawPolygon(point_list, fillcolour, outlinecolour);
}
void js_draw_circle(
drawing *dr, int cx, int cy, int radius, int fillcolour,
int outlinecolour
) {
DRAWING(dr)->drawCircle(Point(cx, cy), radius, fillcolour, outlinecolour);
}
void js_draw_update(drawing *dr, int x, int y, int w, int h) {
DRAWING(dr)->drawUpdate(Rect(x, y, w, h));
}
void js_clip(drawing *dr, int x, int y, int w, int h) {
DRAWING(dr)->clip(Rect(x, y, w, h));
}
void js_unclip(drawing *dr) { DRAWING(dr)->unclip(); }
void js_start_draw(drawing *dr) { DRAWING(dr)->startDraw(); }
void js_end_draw(drawing *dr) { DRAWING(dr)->endDraw(); }
blitter *js_blitter_new(drawing *dr, int w, int h) {
Blitter js_value = DRAWING(dr)->blitterNew(Size(w, h));
return new blitter(js_value);
}
void js_blitter_free(drawing *dr, blitter *bl) {
DRAWING(dr)->blitterFree(bl->js_value);
delete bl;
}
void js_blitter_save(drawing *dr, blitter *bl, int x, int y) {
DRAWING(dr)->blitterSave(bl->js_value, Point(x, y));
}
void js_blitter_load(drawing *dr, blitter *bl, int x, int y) {
DRAWING(dr)->blitterLoad(bl->js_value, Point(x, y));
}
void js_draw_thick_line(
drawing *dr, float thickness, float x1, float y1, float x2,
float y2, int colour
) {
DRAWING(dr)->drawLine(Point(x1, y1), Point(x2, y2), colour, thickness);
}
/*
* Notifications -- from the Frontend to JS
*/
// All of this should result in emitting TypeScript declarations equivalent to:
// type Notification = NotifyGameIdChange | NotifyGameStateChange | ...;
// type NotifyCallbackFunc = (message: Notification) => void;
// with `ChangeNotification` being a discriminated union of all the Notify types.
#define VAL_CONSTANT(type, name, value) \
inline type name() { \
static const auto constant = val::u8string(value).as<type>(); \
return constant; \
}
EMSCRIPTEN_DECLARE_VAL_TYPE(NotifyGameIdChangeType);
VAL_CONSTANT(NotifyGameIdChangeType, GAME_ID_CHANGE, "game-id-change")
struct NotifyGameIdChange {
NotifyGameIdChangeType type = GAME_ID_CHANGE();
std::string currentGameId;
std::optional<std::string> randomSeed = std::nullopt;
NotifyGameIdChange() = default;
explicit NotifyGameIdChange(midend *me)
: currentGameId(allocated_char_ptr(midend_get_game_id(me)).as_string()),
randomSeed(allocated_char_ptr(midend_get_random_seed(me)).as_optional_string())
{}
};
EMSCRIPTEN_DECLARE_VAL_TYPE(GameStatus);
VAL_CONSTANT(GameStatus, STATUS_ONGOING, "ongoing")
VAL_CONSTANT(GameStatus, STATUS_SOLVED, "solved")
// VAL_CONSTANT(GameStatus, STATUS_SOLVED_WITH_HELP, "solved-with-help")
VAL_CONSTANT(GameStatus, STATUS_LOST, "lost")
EMSCRIPTEN_DECLARE_VAL_TYPE(NotifyGameStateChangeType);
VAL_CONSTANT(NotifyGameStateChangeType, GAME_STATE_CHANGE, "game-state-change")
struct NotifyGameStateChange {
NotifyGameStateChangeType type = GAME_STATE_CHANGE();
GameStatus status = STATUS_ONGOING();
int currentMove = 0;
int totalMoves = 0;
bool canUndo = false;
bool canRedo = false;
NotifyGameStateChange() = default;
explicit NotifyGameStateChange(midend *me)
: canUndo(midend_can_undo(me)),
canRedo(midend_can_redo(me)) {
midend_get_move_count(me, ¤tMove, &totalMoves);
auto const _status = midend_status(me);
if (_status < 0) {
status = STATUS_LOST();
} else if (_status > 0) {
// TODO: separate midend status for STATUS_SOLVED_WITH_HELP()
status = STATUS_SOLVED();
} else {
status = STATUS_ONGOING();
}
}
};
EMSCRIPTEN_DECLARE_VAL_TYPE(NotifyParamsChangeType);
VAL_CONSTANT(NotifyParamsChangeType, PARAMS_CHANGE, "params-change")
struct NotifyParamsChange {
NotifyParamsChangeType type = PARAMS_CHANGE();
std::string params;
NotifyParamsChange() = default;
explicit NotifyParamsChange(midend *me)
: params(allocated_char_ptr(midend_get_encoded_params(me)).as_string())
{}
};
EMSCRIPTEN_DECLARE_VAL_TYPE(NotifyStatusBarChangeType);
VAL_CONSTANT(NotifyStatusBarChangeType, STATUS_BAR_CHANGE, "status-bar-change")
struct NotifyStatusBarChange {
NotifyStatusBarChangeType type = STATUS_BAR_CHANGE();
std::string statusBarText;
NotifyStatusBarChange() = default;
explicit NotifyStatusBarChange(std::string text): statusBarText(std::move(text)) {}
};
EMSCRIPTEN_DECLARE_VAL_TYPE(NotifyCallbackFunc);
EMSCRIPTEN_BINDINGS(notifiations) {
register_type<NotifyGameIdChangeType>("\"game-id-change\"");
value_object<NotifyGameIdChange>("NotifyGameIdChange")
.field("type", &NotifyGameIdChange::type)
.field("currentGameId", &NotifyGameIdChange::currentGameId)
.field("randomSeed", &NotifyGameIdChange::randomSeed);
register_type<GameStatus>(R"("ongoing" | "solved" | "solved-with-help" | "lost")");
register_type<NotifyGameStateChangeType>("\"game-state-change\"");
value_object<NotifyGameStateChange>("NotifyGameStateChange")
.field("type", &NotifyGameStateChange::type)
.field("status", &NotifyGameStateChange::status)
.field("currentMove", &NotifyGameStateChange::currentMove)
.field("totalMoves", &NotifyGameStateChange::totalMoves)
.field("canUndo", &NotifyGameStateChange::canUndo)
.field("canRedo", &NotifyGameStateChange::canRedo);
register_type<NotifyParamsChangeType>("\"params-change\"");
value_object<NotifyParamsChange>("NotifyParamsChange")
.field("type", &NotifyParamsChange::type)
.field("params", &NotifyParamsChange::params);
register_type<NotifyStatusBarChangeType>("\"status-bar-change\"");
value_object<NotifyStatusBarChange>("NotifyStatusBarChange")
.field("type", &NotifyStatusBarChange::type)
.field("statusBarText", &NotifyStatusBarChange::statusBarText);
// (Must inline the Notification union to get Emscripten to emit it.)
register_type<NotifyCallbackFunc>(R"(
(message:
| NotifyGameIdChange
| NotifyGameStateChange
| NotifyParamsChange
| NotifyStatusBarChange
) => void
)");
};
/*
* Serialization and deserialization buffers
*/
EMSCRIPTEN_DECLARE_VAL_TYPE(Uint8Array);
class WriteBuffer {
val buffer;
size_t position = 0;
public:
explicit WriteBuffer(size_t initial_size = 4096) {
buffer = val::global("Uint8Array").new_(initial_size);
}
void append(const void *data, size_t len) {
const size_t new_position = position + len;
const auto current_size = buffer["length"].as<size_t>();
// Grow if needed
if (new_position > current_size) {
size_t new_size = (std::max)(current_size * 2, new_position);
const auto new_buffer = val::global("Uint8Array").new_(new_size);
new_buffer.call<void>("set", buffer);
buffer = new_buffer;
}
// Copy data directly
const auto view = val::global("Uint8Array").new_(
buffer["buffer"], position, len
);
view.call<void>(
"set", typed_memory_view(len, static_cast<const uint8_t *>(data))
);
position = new_position;
}
Uint8Array finalize() {
// Return exactly-sized buffer
return val::global("Uint8Array").new_(buffer["buffer"], 0, position).as<
Uint8Array>();
}
static void write_callback(void *ctx, const void *buf, int len) {
static_cast<WriteBuffer *>(ctx)->append(buf, len);
}
};
// JS-side helper for copying from (non-heap) ArrayBuffer into heap buffer
EM_JS(void, copy_from_js_buffer, (
EM_VAL js_buffer,
size_t js_position,
size_t length,
uint8_t* dest_ptr
), {
const sourceBuffer = Emval.toValue(js_buffer);
const sourceView = new Uint8Array(
sourceBuffer.buffer,
sourceBuffer.byteOffset + js_position,
length
);
const destView = new Uint8Array(HEAPU8.buffer, dest_ptr, length);
destView.set(sourceView);
});
class ReadBuffer {
Uint8Array buffer;
size_t position = 0;
size_t total_size;
public:
explicit ReadBuffer(const Uint8Array &uint8_array)
: buffer(uint8_array), total_size(uint8_array["length"].as<size_t>()) {}
bool read(void *buf, size_t len) {
if (position + len > total_size) {
return false; // Not enough data
}
copy_from_js_buffer(
buffer.as_handle(),
position,
len,
static_cast<uint8_t *>(buf)
);
position += len;
return true;
}
static bool read_callback(void *ctx, void *buf, int len) {
return static_cast<ReadBuffer *>(ctx)->read(buf, len);
}
};
/*
* Wrappers for config_item lists and game_params lists,
* adding RAII memory management and other conveniences.
*/
class allocated_config_item_ptr: public allocated_ptr_base<config_item, free_cfg> {
public:
explicit allocated_config_item_ptr(config_item *raw_ptr) : allocated_ptr_base(raw_ptr) {}
};
class wrapped_config_items {
public:
const int which;
// ReSharper disable once CppDFANotInitializedField: false positive on static builder
const allocated_char_ptr title;
const allocated_config_item_ptr items;
explicit wrapped_config_items(const int _which, config_item *_items, char *_title)
: which(_which), title(_title), items(_items) {}
static wrapped_config_items get(midend *me, const int which) {
// (It's not possible to call midend_get_config without title.)
char *title;
config_item *items = midend_get_config(me, which, &title);
return wrapped_config_items(which, items, title);
}
[[nodiscard]] static_char_ptr set(midend *me) const {
return static_char_ptr(midend_set_config(me, which, items.get()));
}
// Non-copyable
wrapped_config_items(const wrapped_config_items&) = delete;
wrapped_config_items& operator=(const wrapped_config_items&) = delete;
// Non-movable (it could be made movable, but we don't need it)
wrapped_config_items(wrapped_config_items&&) = delete;
wrapped_config_items& operator=(wrapped_config_items&&) = delete;
};
class wrapped_game_params {
const game *m_game;
game_params *m_params;
public:
explicit wrapped_game_params(const game *game, game_params *params)
: m_game(game), m_params(params) {}
// Default params for game
explicit wrapped_game_params(const game *game)
: m_game(game), m_params(game->default_params()) {}
// Custom params for config items
explicit wrapped_game_params(const game *game, const config_item *items)
: m_game(game), m_params(game->custom_params(items)) {}
explicit wrapped_game_params(const game *game, const allocated_config_item_ptr &items)
: m_game(game), m_params(game->custom_params(items.get())) {}
// Custom params from encoded params string
explicit wrapped_game_params(const game *game, const char *encoded_params)
: m_game(game), m_params(game->default_params()) {
m_game->decode_params(m_params, encoded_params);
}
// Params from midend_get_params
// (the params used for new games, not necessarily the current game's params)
explicit wrapped_game_params(midend *me)
: m_game(midend_which_game(me)), m_params(midend_get_params(me)) {}
~wrapped_game_params() {
if (m_params && m_game) {
m_game->free_params(m_params);
}
}
[[nodiscard]] const game_params *get() const { return m_params; }
[[nodiscard]] static_char_ptr validate(const bool full=true) const {
return static_char_ptr(m_game->validate_params(m_params, full));
}
[[nodiscard]] allocated_char_ptr as_encoded_string(const bool full=true) const {
return allocated_char_ptr(m_game->encode_params(m_params, full));
}
[[nodiscard]] allocated_config_item_ptr as_config_items() const {
return allocated_config_item_ptr(m_game->configure(m_params));
}
void set_to_midend(midend* me) const {
midend_set_params(me, m_params);
}
// Non-copyable
wrapped_game_params(const wrapped_game_params&) = delete;
wrapped_game_params& operator=(const wrapped_game_params&) = delete;
// Non-movable (it could be made movable, but we don't need it)
wrapped_game_params(wrapped_game_params&&) = delete;
wrapped_game_params& operator=(wrapped_game_params&&) = delete;
};
/*
* frontend -- exported to JS as Frontend.
* Wraps midend functions for use by JS.
* Provides frontend functions required by midend.
*/
EMSCRIPTEN_DECLARE_VAL_TYPE(PresetMenuEntryList);
typedef std::optional<PresetMenuEntryList> OptionalPresetMenuEntryList;
struct PresetMenuEntry {
// TODO: these fields really should be const, but embind value_object doesn't like that
std::string title;
std::string params;
OptionalPresetMenuEntryList submenu = std::nullopt;
PresetMenuEntry() = default;
explicit PresetMenuEntry(midend *me, const preset_menu_entry &preset) :
title(preset.title),
params(midend_get_encoded_params_for_preset(me, preset.id)),
submenu(
preset.submenu == nullptr
? std::nullopt
: OptionalPresetMenuEntryList(build_menu(me, preset.submenu))
) {}
static PresetMenuEntryList build_menu(midend *me, const preset_menu *menu) {
auto entries = std::span(menu->entries, menu->n_entries);
auto menu_vec = std::vector<PresetMenuEntry>();
for (auto &entry: entries) {
menu_vec.emplace_back(me, entry);
}
return val::array(menu_vec).as<PresetMenuEntryList>();
}
};
EMSCRIPTEN_DECLARE_VAL_TYPE(ConfigDescription);
EMSCRIPTEN_DECLARE_VAL_TYPE(ConfigValues);
EMSCRIPTEN_DECLARE_VAL_TYPE(ConfigValuesIn);
EMSCRIPTEN_DECLARE_VAL_TYPE(ConfigValuesOrErrorString);
EMSCRIPTEN_DECLARE_VAL_TYPE(ActivateTimerFunc);
EMSCRIPTEN_DECLARE_VAL_TYPE(DeactivateTimerFunc);
EMSCRIPTEN_DECLARE_VAL_TYPE(TextFallbackFunc);
struct FrontendConstructorArgs {
ActivateTimerFunc activateTimer = val::undefined().as<ActivateTimerFunc>();
DeactivateTimerFunc deactivateTimer = val::undefined().as<DeactivateTimerFunc>();
TextFallbackFunc textFallback = val::undefined().as<TextFallbackFunc>();
NotifyCallbackFunc notifyChange = val::undefined().as<NotifyCallbackFunc>();
FrontendConstructorArgs() = default;
};
const drawing_api *get_js_drawing_api();
struct frontend {
private:
std::unique_ptr<midend, decltype(&midend_free)> me_ptr;
[[nodiscard]] midend* me() const { return me_ptr.get(); }
std::string statusbarText;
// Used by getColourPalette / frontend_default_colour
bool defaultBackgroundIsValid = false;
Colour defaultBackground;
// Callbacks into JS
ActivateTimerFunc activateTimer;
DeactivateTimerFunc deactivateTimer;
TextFallbackFunc textFallback;
NotifyCallbackFunc notifyChange;
public:
// Allow late binding of JS Drawing, by passing myself as the drhandle.
// (Unwound in DRAWING() accessor below.)
Drawing *drawing = nullptr;
explicit frontend(const FrontendConstructorArgs &args)
: me_ptr(
// For midend purposes, the frontend is also the drhandle.
midend_new(this, &thegame, get_js_drawing_api(), this),
midend_free
),
activateTimer(args.activateTimer),
deactivateTimer(args.deactivateTimer),