data: one trie for maps and dictionaries, one loader
The hash map served a single exact-match lookup that the trie already answers, at the price of a second container, a second file loader with its own drift, and a Str-to-UTF-8 conversion on every chain probe. The files are small (kanji.dict is 7.5k lines), so the trie holds both. trieopen validates keys and each space-separated value word against Str and reports path:line; trielookup takes the Str every caller holds and treats a nil trie as an unloaded map; trienew/trieput exist for tests. The overflow guards on growth, Trie.root (always 0) and the per-language init loop written twice are gone.
This commit is contained in:
@@ -28,11 +28,11 @@ SMOKE = ibus_live_test ibus_client_smoke gtk_live_test xim_live_test \
|
||||
ipc_live_test
|
||||
FAULT = daemon_collision_test daemon_failure_test daemon_restart_test
|
||||
LIVE = $(SMOKE) $(FAULT)
|
||||
TESTSRC = test_util.c str_test.c hash_test.c trie_test.c \
|
||||
TESTSRC = test_util.c str_test.c trie_test.c \
|
||||
ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \
|
||||
popup_test.c font_test.c ibus_test.c server_test.c xim_adapter_test.c
|
||||
TESTOBJ = $(TESTSRC:.c=.o)
|
||||
PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \
|
||||
PARENTSRC = str.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \
|
||||
font.c
|
||||
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
|
||||
XIMOBJ = unit_xim_keymap.o unit_ximtext.o
|
||||
|
||||
@@ -3,21 +3,20 @@
|
||||
void
|
||||
dictionary_candidates(struct ct *t)
|
||||
{
|
||||
static char malformed[] = { 'o', 'k', ' ', (char)0x80 };
|
||||
char many[512], item[8];
|
||||
char *p;
|
||||
Dictreq req;
|
||||
Dictres res;
|
||||
Hmap *saved;
|
||||
Trie *saved;
|
||||
Lang *lang;
|
||||
Str key;
|
||||
int i;
|
||||
|
||||
lang = getlang(LangJP);
|
||||
saved = lang->dict;
|
||||
lang->dict = hmapalloc(1);
|
||||
lang->dict = trienew();
|
||||
key = mkstr("かな");
|
||||
hmapset(&lang->dict, &key, " 候補1 かな 候補2 ",
|
||||
trieput(lang->dict, "かな", strlen("かな"), " 候補1 かな 候補2 ",
|
||||
strlen(" 候補1 かな 候補2 "));
|
||||
memset(&req, 0, sizeof req);
|
||||
req.key = key;
|
||||
@@ -42,7 +41,7 @@ dictionary_candidates(struct ct *t)
|
||||
p += strlen(item);
|
||||
}
|
||||
*p = '\0';
|
||||
hmapset(&lang->dict, &key, many, strlen(many));
|
||||
trieput(lang->dict, "key", 3, many, strlen(many));
|
||||
req.key = key;
|
||||
req.pre = mkstr("preedit-two");
|
||||
req.seq = 29;
|
||||
@@ -54,13 +53,8 @@ dictionary_candidates(struct ct *t)
|
||||
CT_EQ_INT(t, 0, scmp(&req.pre, &res.key));
|
||||
checkstr(t, "first capped candidate", "c00", &res.kouho[0]);
|
||||
checkstr(t, "last capped candidate", "c31", &res.kouho[31]);
|
||||
key = mkstr("malformed");
|
||||
hmapset(&lang->dict, &key, malformed, sizeof malformed);
|
||||
req.key = key;
|
||||
dictlookup(&req, &res);
|
||||
CT_EQ_INT(t, 0, res.nkouho);
|
||||
cleanup:
|
||||
hmapfree(lang->dict);
|
||||
trieclose(lang->dict);
|
||||
lang->dict = saved;
|
||||
}
|
||||
|
||||
@@ -77,13 +71,13 @@ dictionary_misses_clear_result(struct ct *t)
|
||||
};
|
||||
Dictreq req;
|
||||
Dictres res;
|
||||
Hmap *saved;
|
||||
Trie *saved;
|
||||
Lang *lang;
|
||||
int i;
|
||||
|
||||
lang = getlang(LangJP);
|
||||
saved = lang->dict;
|
||||
lang->dict = hmapalloc(1);
|
||||
lang->dict = trienew();
|
||||
for(i = 0; i < nelem(cases); i++){
|
||||
memset(&res, 0xa5, sizeof res);
|
||||
req.key = mkstr(cases[i].key);
|
||||
@@ -98,7 +92,7 @@ dictionary_misses_clear_result(struct ct *t)
|
||||
CT_EQ_UINT(t, 0xf00d0000U + i, res.seq);
|
||||
checkstr(t, cases[i].name, cases[i].pre, &res.key);
|
||||
}
|
||||
hmapfree(lang->dict);
|
||||
trieclose(lang->dict);
|
||||
lang->dict = saved;
|
||||
}
|
||||
|
||||
@@ -107,15 +101,15 @@ dictionary_emoji_identity(struct ct *t)
|
||||
{
|
||||
Dictreq req;
|
||||
Dictres res;
|
||||
Hmap *saved;
|
||||
Trie *saved;
|
||||
Lang *lang;
|
||||
Str key;
|
||||
|
||||
lang = getlang(LangEMOJI);
|
||||
saved = lang->dict;
|
||||
lang->dict = hmapalloc(2);
|
||||
lang->dict = trienew();
|
||||
key = mkstr("é");
|
||||
hmapset(&lang->dict, &key, "é", strlen("é"));
|
||||
trieput(lang->dict, "é", strlen("é"), "é", strlen("é"));
|
||||
memset(&req, 0, sizeof req);
|
||||
req.key = key;
|
||||
req.pre = key;
|
||||
@@ -124,6 +118,6 @@ dictionary_emoji_identity(struct ct *t)
|
||||
dictlookup(&req, &res);
|
||||
if(CT_EQ_INT(t, 1, res.nkouho))
|
||||
checkstr(t, "emoji identity candidate", "é", &res.kouho[0]);
|
||||
hmapfree(lang->dict);
|
||||
trieclose(lang->dict);
|
||||
lang->dict = saved;
|
||||
}
|
||||
|
||||
@@ -311,14 +311,14 @@ engine_rejects_stale_dictionary_results(struct ct *t)
|
||||
{
|
||||
Dictreq a, b;
|
||||
Dictres res;
|
||||
Hmap *saved;
|
||||
Trie *saved;
|
||||
Lang *jp;
|
||||
char one, two;
|
||||
int n;
|
||||
|
||||
jp = getlang(LangJP);
|
||||
saved = jp->dict;
|
||||
jp->dict = hmapalloc(1);
|
||||
jp->dict = trienew();
|
||||
init();
|
||||
im.l = jp;
|
||||
while(channbrecv(dictreqc, &a) > 0)
|
||||
@@ -362,7 +362,7 @@ engine_rejects_stale_dictionary_results(struct ct *t)
|
||||
cleanup:
|
||||
while(channbrecv(dictreqc, &a) > 0)
|
||||
;
|
||||
hmapfree(jp->dict);
|
||||
trieclose(jp->dict);
|
||||
jp->dict = saved;
|
||||
}
|
||||
|
||||
@@ -720,7 +720,7 @@ struct Searchfix
|
||||
Im im;
|
||||
Search search;
|
||||
Lang *dictlang;
|
||||
Hmap *dict;
|
||||
Trie *dict;
|
||||
int activecap;
|
||||
int visible;
|
||||
int candidatechosen;
|
||||
@@ -939,12 +939,9 @@ engine_candidate_completion(struct ct *t)
|
||||
}
|
||||
|
||||
static void
|
||||
setdict(Hmap **dict, char *key, char *val)
|
||||
setdict(Trie *dict, char *key, char *val)
|
||||
{
|
||||
Str s;
|
||||
|
||||
s = mkstr(key);
|
||||
hmapset(dict, &s, val, strlen(val));
|
||||
trieput(dict, key, strlen(key), val, strlen(val));
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -964,22 +961,22 @@ static void
|
||||
emojibegin(Searchfix *f, int showpre)
|
||||
{
|
||||
searchsave(f, LangEMOJI);
|
||||
f->dictlang->dict = hmapalloc(32);
|
||||
setdict(&f->dictlang->dict, "a", "A B");
|
||||
setdict(&f->dictlang->dict, "ㅁ", "B C");
|
||||
setdict(&f->dictlang->dict, "alpha", "α");
|
||||
setdict(&f->dictlang->dict, "smil", "😀 😄");
|
||||
setdict(&f->dictlang->dict, "smile", "😀 😄");
|
||||
setdict(&f->dictlang->dict, "웃음", "😀 😄 😂");
|
||||
setdict(&f->dictlang->dict, "えがお", "😀 😄 😊");
|
||||
setdict(&f->dictlang->dict, "エガオ", "😀 😄 😊");
|
||||
setdict(&f->dictlang->dict, "heart", "❤️");
|
||||
setdict(&f->dictlang->dict, "coffee", "☕️");
|
||||
setdict(&f->dictlang->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽");
|
||||
setdict(&f->dictlang->dict, "^0", "⁰");
|
||||
setdict(&f->dictlang->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍");
|
||||
setdict(&f->dictlang->dict, "<", "← ≤ ♥ ≠");
|
||||
setdict(&f->dictlang->dict, "many",
|
||||
f->dictlang->dict = trienew();
|
||||
setdict(f->dictlang->dict, "a", "A B");
|
||||
setdict(f->dictlang->dict, "ㅁ", "B C");
|
||||
setdict(f->dictlang->dict, "alpha", "α");
|
||||
setdict(f->dictlang->dict, "smil", "😀 😄");
|
||||
setdict(f->dictlang->dict, "smile", "😀 😄");
|
||||
setdict(f->dictlang->dict, "웃음", "😀 😄 😂");
|
||||
setdict(f->dictlang->dict, "えがお", "😀 😄 😊");
|
||||
setdict(f->dictlang->dict, "エガオ", "😀 😄 😊");
|
||||
setdict(f->dictlang->dict, "heart", "❤️");
|
||||
setdict(f->dictlang->dict, "coffee", "☕️");
|
||||
setdict(f->dictlang->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽");
|
||||
setdict(f->dictlang->dict, "^0", "⁰");
|
||||
setdict(f->dictlang->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍");
|
||||
setdict(f->dictlang->dict, "<", "← ≤ ♥ ≠");
|
||||
setdict(f->dictlang->dict, "many",
|
||||
"c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12");
|
||||
init();
|
||||
im.l = getlang(LangKO);
|
||||
@@ -989,11 +986,11 @@ emojibegin(Searchfix *f, int showpre)
|
||||
static void
|
||||
searchend(Searchfix *f)
|
||||
{
|
||||
Hmap *dict;
|
||||
Trie *dict;
|
||||
|
||||
dict = f->dictlang->dict;
|
||||
f->dictlang->dict = f->dict;
|
||||
hmapfree(dict);
|
||||
trieclose(dict);
|
||||
draindraw(nil);
|
||||
im = f->im;
|
||||
search = f->search;
|
||||
@@ -1079,12 +1076,12 @@ static void
|
||||
hanjabegin(Searchfix *f, int lang)
|
||||
{
|
||||
searchsave(f, LangHANJA);
|
||||
f->dictlang->dict = hmapalloc(8);
|
||||
setdict(&f->dictlang->dict, "한", "漢 韓");
|
||||
setdict(&f->dictlang->dict, "가", "家");
|
||||
setdict(&f->dictlang->dict, "ㄱ", "假");
|
||||
setdict(&f->dictlang->dict, "한글", "文");
|
||||
setdict(&f->dictlang->dict, "か", "仮");
|
||||
f->dictlang->dict = trienew();
|
||||
setdict(f->dictlang->dict, "한", "漢 韓");
|
||||
setdict(f->dictlang->dict, "가", "家");
|
||||
setdict(f->dictlang->dict, "ㄱ", "假");
|
||||
setdict(f->dictlang->dict, "한글", "文");
|
||||
setdict(f->dictlang->dict, "か", "仮");
|
||||
init();
|
||||
im.l = getlang(lang);
|
||||
}
|
||||
@@ -1156,15 +1153,14 @@ engine_japanese_candidates(struct ct *t)
|
||||
{
|
||||
Dictreq req;
|
||||
Dictres res;
|
||||
Hmap *saved;
|
||||
Trie *saved;
|
||||
Lang *jp;
|
||||
Str com, key, shown;
|
||||
Str com, shown;
|
||||
|
||||
jp = getlang(LangJP);
|
||||
saved = jp->dict;
|
||||
jp->dict = hmapalloc(8);
|
||||
key = mkstr("かんじ");
|
||||
hmapset(&jp->dict, &key, "漢字 幹事", strlen("漢字 幹事"));
|
||||
jp->dict = trienew();
|
||||
setdict(jp->dict, "かんじ", "漢字 幹事");
|
||||
init();
|
||||
im.l = jp;
|
||||
sclear(&com);
|
||||
@@ -1189,7 +1185,7 @@ engine_japanese_candidates(struct ct *t)
|
||||
checkstr(t, "selected Kanji", "幹事", &com);
|
||||
cleanup:
|
||||
newestrequest(&req);
|
||||
hmapfree(jp->dict);
|
||||
trieclose(jp->dict);
|
||||
jp->dict = saved;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
#include "test.h"
|
||||
|
||||
void
|
||||
hmap_set_replace_and_grow(struct ct *t)
|
||||
{
|
||||
char keybuf[16], valbuf[16], source[] = "copied";
|
||||
Hmap *h;
|
||||
Hnode *n;
|
||||
Str key;
|
||||
int i;
|
||||
|
||||
h = hmapalloc(1);
|
||||
for(i = 0; i < 4; i++){
|
||||
snprint(keybuf, sizeof keybuf, "key%d", i);
|
||||
snprint(valbuf, sizeof valbuf, "value%d", i);
|
||||
key = mkstr(keybuf);
|
||||
hmapset(&h, &key, valbuf, strlen(valbuf));
|
||||
}
|
||||
for(i = 0; i < 4; i++){
|
||||
snprint(keybuf, sizeof keybuf, "key%d", i);
|
||||
snprint(valbuf, sizeof valbuf, "value%d", i);
|
||||
key = mkstr(keybuf);
|
||||
n = hmapget(h, &key);
|
||||
if(n == nil || strcmp(n->val, valbuf) != 0)
|
||||
CT_ERRORF(t, "%s: collision chain lost value", keybuf);
|
||||
}
|
||||
key = mkstr("key1");
|
||||
hmapset(&h, &key, source, strlen(source));
|
||||
source[0] = 'X';
|
||||
n = hmapget(h, &key);
|
||||
if(!CT_CHECK(t, n != nil))
|
||||
goto cleanup;
|
||||
CT_EQ_STR(t, "copied", n->val);
|
||||
key = mkstr("key2");
|
||||
n = hmapget(h, &key);
|
||||
if(!CT_CHECK(t, n != nil))
|
||||
goto cleanup;
|
||||
CT_EQ_STR(t, "value2", n->val);
|
||||
key = mkstr("key1");
|
||||
hmapset(&h, &key, nil, 0);
|
||||
n = hmapget(h, &key);
|
||||
if(!CT_CHECK(t, n != nil))
|
||||
goto cleanup;
|
||||
CT_EQ_INT(t, 0, n->vlen);
|
||||
CT_EQ_PTR(t, nil, n->val);
|
||||
key = mkstr("missing");
|
||||
CT_EQ_PTR(t, nil, hmapget(h, &key));
|
||||
cleanup:
|
||||
hmapfree(h);
|
||||
}
|
||||
|
||||
void
|
||||
hmap_long_utf8_keys(struct ct *t)
|
||||
{
|
||||
Hmap *h;
|
||||
Hnode *n;
|
||||
Str a, b;
|
||||
int i;
|
||||
|
||||
for(i = 0; i < Maxrunes-1; i++)
|
||||
a.r[i] = b.r[i] = 0x1f600;
|
||||
a.r[Maxrunes-1] = 0x1f601;
|
||||
b.r[Maxrunes-1] = 0x1f602;
|
||||
a.n = b.n = Maxrunes;
|
||||
h = hmapalloc(1);
|
||||
hmapset(&h, &a, "first", 5);
|
||||
hmapset(&h, &b, "second", 6);
|
||||
n = hmapget(h, &a);
|
||||
if(!CT_CHECK(t, n != nil))
|
||||
goto cleanup;
|
||||
CT_EQ_STR(t, "first", n != nil ? n->val : nil);
|
||||
n = hmapget(h, &b);
|
||||
if(!CT_CHECK(t, n != nil))
|
||||
goto cleanup;
|
||||
CT_EQ_STR(t, "second", n != nil ? n->val : nil);
|
||||
cleanup:
|
||||
hmapfree(h);
|
||||
}
|
||||
|
||||
void
|
||||
hmap_binary_keys_and_invalid_lengths(struct ct *t)
|
||||
{
|
||||
Hmap *h;
|
||||
Hnode *n;
|
||||
Str key, other;
|
||||
|
||||
h = hmapalloc(1);
|
||||
CT_CHECK(t, h != nil);
|
||||
key.n = 3;
|
||||
key.r[0] = 'a';
|
||||
key.r[1] = 0;
|
||||
key.r[2] = 'b';
|
||||
other = key;
|
||||
other.r[2] = 'c';
|
||||
hmapset(&h, &key, "one", 3);
|
||||
hmapset(&h, &other, "two", 3);
|
||||
n = hmapget(h, &key);
|
||||
if(CT_CHECK(t, n != nil)){
|
||||
CT_EQ_INT(t, 3, n->klen);
|
||||
CT_EQ_MEM(t, "one", n->val, n->vlen);
|
||||
}
|
||||
n = hmapget(h, &other);
|
||||
if(CT_CHECK(t, n != nil))
|
||||
CT_EQ_MEM(t, "two", n->val, n->vlen);
|
||||
hmapset(&h, &key, "bad", -1);
|
||||
hmapset(&h, &key, nil, 1);
|
||||
n = hmapget(h, &key);
|
||||
if(CT_CHECK(t, n != nil))
|
||||
CT_EQ_MEM(t, "one", n->val, n->vlen);
|
||||
CT_EQ_PTR(t, nil, hmapalloc(0));
|
||||
hmapfree(h);
|
||||
}
|
||||
@@ -12,11 +12,8 @@ void str_init_utf8(struct ct*);
|
||||
void str_edit_and_alias(struct ct*);
|
||||
void str_utf8_capacity(struct ct*);
|
||||
void str_invalid_and_full_appends(struct ct*);
|
||||
void hmap_set_replace_and_grow(struct ct*);
|
||||
void hmap_long_utf8_keys(struct ct*);
|
||||
void hmap_binary_keys_and_invalid_lengths(struct ct*);
|
||||
void trie_exact_prefix_and_duplicate(struct ct*);
|
||||
void trie_optional_outputs_and_invalid_lengths(struct ct*);
|
||||
void trie_put_and_unloaded(struct ct*);
|
||||
void popup_layout(struct ct*);
|
||||
void font_render(struct ct*);
|
||||
void production_maps_load(struct ct*);
|
||||
|
||||
@@ -5,26 +5,26 @@ trie_exact_prefix_and_duplicate(struct ct *t)
|
||||
{
|
||||
static const struct {
|
||||
char *key;
|
||||
int klen;
|
||||
int match;
|
||||
char *want;
|
||||
} cases[] = {
|
||||
{ "a", 1, TrieExact, "alpha" },
|
||||
{ "dupli", 5, TriePrefix, nil },
|
||||
{ "duplicate", 9, TrieExact, "second" },
|
||||
{ "missing", 7, TrieMiss, nil },
|
||||
{ nil, 1, TrieMiss, nil },
|
||||
{ "a", -1, TrieMiss, nil },
|
||||
{ "a", TrieExact, "alpha" },
|
||||
{ "ab", TrieExact, "beta" },
|
||||
{ "한", TrieExact, "값" },
|
||||
{ "dupli", TriePrefix, nil },
|
||||
{ "duplicate", TrieExact, "second" },
|
||||
{ "missing", TrieMiss, nil },
|
||||
{ "", TriePrefix, nil },
|
||||
};
|
||||
char *v;
|
||||
Trie *trie;
|
||||
Str key;
|
||||
int i, match, n;
|
||||
|
||||
trie = trieopen("data/trie.map");
|
||||
for(i = 0; i < nelem(cases); i++){
|
||||
v = "unchanged";
|
||||
n = 77;
|
||||
match = trielookup(trie, cases[i].key, cases[i].klen, &v, &n);
|
||||
key = mkstr(cases[i].key);
|
||||
match = trielookup(trie, &key, &v, &n);
|
||||
if(match != cases[i].match){
|
||||
CT_ERRORF(t, "case %d: want match %d, got %d",
|
||||
i, cases[i].match, match);
|
||||
@@ -42,22 +42,28 @@ trie_exact_prefix_and_duplicate(struct ct *t)
|
||||
}
|
||||
|
||||
void
|
||||
trie_optional_outputs_and_invalid_lengths(struct ct *t)
|
||||
trie_put_and_unloaded(struct ct *t)
|
||||
{
|
||||
char *v;
|
||||
Trie *trie;
|
||||
Str key;
|
||||
int n;
|
||||
|
||||
trie = trieopen("data/trie.map");
|
||||
CT_EQ_INT(t, TrieExact, trielookup(trie, "a", 1, &v, nil));
|
||||
CT_CHECK(t, v != nil);
|
||||
CT_EQ_INT(t, TrieExact, trielookup(trie, "a", 1, nil, &n));
|
||||
CT_EQ_INT(t, 5, n);
|
||||
CT_EQ_INT(t, TriePrefix,
|
||||
trielookup(trie, "dupli", 5, nil, nil));
|
||||
CT_EQ_INT(t, TriePrefix, trielookup(trie, nil, 0, &v, &n));
|
||||
CT_EQ_PTR(t, nil, v);
|
||||
CT_EQ_INT(t, 0, n);
|
||||
key = mkstr("k");
|
||||
CT_EQ_INT(t, TrieMiss, trielookup(nil, &key, &v, &n));
|
||||
trie = trienew();
|
||||
CT_EQ_INT(t, TrieMiss, trielookup(trie, &key, &v, &n));
|
||||
trieput(trie, "k", 1, "one two", 7);
|
||||
trieput(trie, "ka", 2, "", 0);
|
||||
if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
|
||||
CT_EQ_MEM(t, "one two", v, 7);
|
||||
key = mkstr("ka");
|
||||
if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
|
||||
CT_EQ_INT(t, 0, n);
|
||||
trieput(trie, "k", 1, "three", 5);
|
||||
key = mkstr("k");
|
||||
if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
|
||||
CT_EQ_MEM(t, "three", v, 5);
|
||||
trieclose(trie);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,11 +67,8 @@ static const struct ct_test tests[] = {
|
||||
{ "str/edit-and-alias", str_edit_and_alias },
|
||||
{ "str/utf8-capacity", str_utf8_capacity },
|
||||
{ "str/invalid-full-appends", str_invalid_and_full_appends },
|
||||
{ "hmap/set-replace-grow", hmap_set_replace_and_grow },
|
||||
{ "hmap/long-utf8-keys", hmap_long_utf8_keys },
|
||||
{ "hmap/binary-invalid-lengths", hmap_binary_keys_and_invalid_lengths },
|
||||
{ "trie/exact-prefix-duplicate", trie_exact_prefix_and_duplicate },
|
||||
{ "trie/optional-invalid-lengths", trie_optional_outputs_and_invalid_lengths },
|
||||
{ "trie/put-and-unloaded", trie_put_and_unloaded },
|
||||
{ "popup/layout", popup_layout },
|
||||
{ "font/render", font_render },
|
||||
{ "map/production-lifecycle", production_maps_load },
|
||||
|
||||
Reference in New Issue
Block a user