@@ -7,13 +7,34 @@ SUBSYSTEM_DEF(codex)
7
7
var /regex /trailingLinebreakRegexStart
8
8
var /regex /trailingLinebreakRegexEnd
9
9
10
+ // / All entries. Unkeyed.
10
11
var /list /all_entries = list ()
12
+ // / All STATIC entries, By path. Does not include dynamic entries.
11
13
var /list /entries_by_path = list ()
14
+ // / All entries, by name.
12
15
var /list /entries_by_string = list ()
16
+ // / The same as above, but sorted (?)
13
17
var /list /index_file = list ()
18
+ // / Search result cache, so we don't need to hit the DB every time.
14
19
var /list /search_cache = list ()
20
+ // / All categories.
15
21
var /list /codex_categories = list ()
16
22
23
+ // / Codex Database Connection
24
+ var /database /codex_index
25
+
26
+ / datum / controller/ subsystem/ codex/ vv_get_dropdown()
27
+ . = .. ()
28
+ VV_DROPDOWN_OPTION (" " , " ---" )
29
+ VV_DROPDOWN_OPTION (VV_HK_REGENERATE_CODEX , " Regenerate Search Database" )
30
+
31
+ / datum / controller/ subsystem/ codex/ vv_do_topic(href_list)
32
+ . = .. ()
33
+ if (href_list[VV_HK_REGENERATE_CODEX ])
34
+ if (tgui_alert(usr , " Are you sure you want to regenerate the search index? This will almost certainly cause lag." , " Regenerate Index" , list (" Yes" , " No" )) == " Yes" )
35
+ prepare_search_database (TRUE )
36
+
37
+
17
38
/ datum / controller/ subsystem/ codex/ Initialize()
18
39
// Codex link syntax is such:
19
40
// <l>keyword</l> when keyword is mentioned verbatim,
@@ -44,6 +65,9 @@ SUBSYSTEM_DEF(codex)
44
65
for (var /datum /codex_entry/entry as anything in all_entries)
45
66
index_file[entry. name] = entry
46
67
index_file = sortTim(index_file, GLOBAL_PROC_REF (cmp_text_asc))
68
+
69
+ // Prepare the search database.
70
+ prepare_search_database ()
47
71
. = .. ()
48
72
49
73
/ datum / controller/ subsystem/ codex/ proc / parse_links(string, viewer)
@@ -99,7 +123,7 @@ SUBSYSTEM_DEF(codex)
99
123
codex_data += " <h3><b>[ entries. len] matches</b> [ search_query ? " for '[ search_query] '" : " " ] :</h3>"
100
124
101
125
if (LAZYLEN (entries) > CODEX_ENTRY_LIMIT )
102
- codex_data += " Showing first <b>[ CODEX_ENTRY_LIMIT ] </b> entries. <b> [ entries. len - 5 ] result \s </b> omitted.</br>"
126
+ codex_data += " Showing first <b>[ CODEX_ENTRY_LIMIT ] </b> entries. <b> [ entries. len - CODEX_ENTRY_LIMIT ] result \s </b> omitted.</br>"
103
127
codex_data += " <table width = 100%>"
104
128
105
129
for (var /i = 1 to min (entries. len, CODEX_ENTRY_LIMIT ))
@@ -111,24 +135,73 @@ SUBSYSTEM_DEF(codex)
111
135
popup. set_content(codex_data. Join())
112
136
popup. open()
113
137
114
- #undef CODEX_ENTRY_LIMIT
138
+
115
139
/ datum / controller/ subsystem/ codex/ proc / get_guide(category)
116
140
var /datum /codex_category/cat = codex_categories[category]
117
141
. = cat?. guide_html
118
142
143
+ // / Perform a full-text search through all codex entries. Entries matching the query by name will be shown first.
144
+ // / Results are cached. Relies on the index database.
119
145
/ datum / controller/ subsystem/ codex/ proc / retrieve_entries_for_string(searching)
120
146
121
- if (! initialized)
122
- return list ()
123
-
124
147
searching = codex_sanitize(searching)
125
-
126
- if (! searching)
127
- return list ()
128
-
129
148
. = search_cache[searching]
130
149
if (. )
131
150
return .
151
+ if (! searching || ! initialized)
152
+ return list ()
153
+ if (! codex_index) // No codex DB loaded. Use the fallback search.
154
+ return text_search_no_db(searching)
155
+
156
+
157
+ var /search_string = " %[ searching] %"
158
+ // Search by name to build the priority entries first
159
+ var /database/query /cursor = new (
160
+ {" SELECT name FROM codex_entries
161
+ WHERE name LIKE ?
162
+ ORDER BY name asc
163
+ LIMIT [ CODEX_ENTRY_LIMIT ] "} ,
164
+ search_string
165
+ )
166
+ // Execute the query, returning us a list of types we can retrieve from the list indexes.
167
+ cursor. Execute(codex_index)
168
+
169
+ // God this sucks.
170
+ var /list /datum/codex_entry/priority_results = list ()
171
+ while (cursor. NextRow())
172
+ var /row = cursor. GetRowData()
173
+ priority_results += index_file[row[" name" ]]
174
+ CHECK_TICK
175
+
176
+ // Now the awful slow ones.
177
+ cursor. Add(
178
+ {" SELECT name FROM codex_entries
179
+ WHERE lore_text LIKE ?
180
+ AND mechanics_text LIKE ?
181
+ AND antag_text LIKE ?
182
+ ORDER BY name asc
183
+ LIMIT [ CODEX_ENTRY_LIMIT ] "} ,
184
+ search_string,
185
+ search_string,
186
+ search_string
187
+ )
188
+ // Execute the query, returning us a list of types we can retrieve from the list indexes.
189
+ cursor. Execute(codex_index)
190
+ var /list /datum/codex_entry/fulltext_results = list ()
191
+ while (cursor. NextRow())
192
+ var /row = cursor. GetRowData()
193
+ fulltext_results += index_file[row[" name" ]]
194
+ CHECK_TICK
195
+
196
+ priority_results += fulltext_results
197
+ . = search_cache[searching] = priority_results
198
+
199
+ // / Straight-DM implimentation of full text search. Objectively garbage.
200
+ // / Does not use the DB. Used when database loading is skipped.
201
+ // / Argument has already been sanitized.
202
+ // / Safety checks have already been done. Cache has already been checked.
203
+ / datum / controller/ subsystem/ codex/ proc / text_search_no_db(searching)
204
+ PRIVATE_PROC (TRUE )
132
205
133
206
var /list /results = list ()
134
207
var /list /priority_results = list ()
@@ -168,3 +241,125 @@ SUBSYSTEM_DEF(codex)
168
241
if (entry)
169
242
present_codex_entry (showing_mob, entry)
170
243
return TRUE
244
+
245
+ #define CODEX_SEARCH_INDEX_FILE " codex.db"
246
+ #define CODEX_SERIAL_ALWAYS_VALID " I_DOWNLOADED_A_ZIP_INSTEAD_OF_USING_GIT"
247
+ // / Prepare the search database.
248
+ / datum / controller/ subsystem/ codex/ proc / prepare_search_database(drop_existing = FALSE )
249
+ if (GLOB . is_debug_server && ! FORCE_CODEX_DATABASE )
250
+ to_chat (world , span_debug(" Codex: Debug server detected. DB operation disabled." ))
251
+ log_world (" Codex: Codex DB generation Skipped" )
252
+ return
253
+ if (drop_existing)
254
+ to_chat (world , span_debug(" Codex: Deleting old index..." ))
255
+ // Check if we've already opened one this round, if so, get rid of it.
256
+ if (codex_index)
257
+ del (codex_index)
258
+ fdel (CODEX_SEARCH_INDEX_FILE )
259
+ else
260
+ to_chat (world , span_debug(" Codex: Preparing Search Database" ))
261
+
262
+
263
+ if (! rustg_file_exists(CODEX_SEARCH_INDEX_FILE ))
264
+ if (! drop_existing)
265
+ to_chat (world , span_debug(" Codex: Database missing, building..." ))
266
+ create_db ()
267
+ build_db_index ()
268
+
269
+ if (! codex_index) // If we didn't just create it, we need to load it.
270
+ codex_index = new (CODEX_SEARCH_INDEX_FILE )
271
+
272
+ var /database/query /cursor = new (" SELECT * FROM _info" )
273
+ if (! cursor. Execute(codex_index))
274
+ to_chat (world , span_debug(" Codex: ABORTING! Database error: [ cursor. Error()] | [ cursor. ErrorMsg()] " ))
275
+ return
276
+
277
+ cursor. NextRow()
278
+ var /list /revline = cursor. GetRowData()
279
+ var /db_serial = revline[" revision" ]
280
+ if (db_serial != GLOB . revdata. commit)
281
+ if (db_serial == CODEX_SERIAL_ALWAYS_VALID )
282
+ to_chat (world , span_debug(" Codex: Special Database Serial detected. Data may be inaccurate or out of date." ))
283
+ else
284
+ to_chat (world , span_debug(" Codex: Database out of date, Rebuilding..." ))
285
+ prepare_search_database (TRUE ) // recursiveness funny,,
286
+ return
287
+
288
+ if (drop_existing)
289
+ to_chat (world , span_debug(" Codex: Collation complete.\n Codex: Index ready." ))
290
+ return
291
+ to_chat (world , span_debug(" Codex: Database Serial validated.\n Codex: Loading complete." ))
292
+
293
+ / datum / controller/ subsystem/ codex/ proc / create_db()
294
+ // No index? Make one.
295
+
296
+ to_chat (world , span_debug(" Codex: Writing new database file..." ))
297
+ // We explicitly store the DB in the root directory, so that TGS builds wipe it.
298
+ codex_index = new (CODEX_SEARCH_INDEX_FILE )
299
+
300
+ // / Holds the revision the index was compiled for. If it's different then live, we need to regenerate the index.
301
+ var /static /create_info_schema = {"
302
+ CREATE TABLE "_info" (
303
+ "revision" TEXT
304
+ );"}
305
+
306
+ // Create the initial schema
307
+ var /database/query /init_cursor = new (create_info_schema)
308
+
309
+ if (! init_cursor. Execute(codex_index))
310
+ to_chat (world , span_debug(" Codex: ABORTING! Database error: [ init_cursor. Error()] | [ init_cursor. ErrorMsg()] " ))
311
+ return
312
+
313
+ // Holds all codex entries to enable accelerated text search.
314
+ var /static /create_codex_schema = {"
315
+ CREATE TABLE "codex_entries" (
316
+ "name" TEXT NOT NULL,
317
+ "lore_text" TEXT,
318
+ "mechanics_text" TEXT,
319
+ "antag_text" TEXT,
320
+ PRIMARY KEY("name")
321
+ );"}
322
+
323
+ init_cursor. Add(create_codex_schema)
324
+ if (! init_cursor. Execute(codex_index))
325
+ to_chat (world , span_debug(" Codex: ABORTING! Database error: [ init_cursor. Error()] | [ init_cursor. ErrorMsg()] " ))
326
+ return
327
+
328
+ var /revid = GLOB . revdata. commit
329
+ if (! revid) // zip download, you're on your own pissboy, The serial will always be considered valid.
330
+ revid = CODEX_SERIAL_ALWAYS_VALID
331
+
332
+ // Insert the revision header.
333
+ init_cursor. Add(" INSERT INTO _info (revision) VALUES (?)" , revid)
334
+ if (! init_cursor. Execute(codex_index))
335
+ to_chat (world , span_debug(" Codex: ABORTING! Database error: [ init_cursor. Error()] | [ init_cursor. ErrorMsg()] " ))
336
+ return
337
+
338
+ / datum / controller/ subsystem/ codex/ proc / build_db_index()
339
+ to_chat (world , span_debug(" Codex: Building search index." ))
340
+
341
+ var /database/query /cursor = new
342
+ var /total_entries = length(all_entries)
343
+ to_chat (world , span_debug(" \t Codex: Collating [ total_entries] records..." ))
344
+ var /record_id = 0 // Counter for debugging.
345
+ for (var /datum /codex_entry/entry as anything in all_entries)
346
+ cursor. Add(
347
+ " INSERT INTO codex_entries (name, lore_text, mechanics_text, antag_text) VALUES (?,?,?,?)" ,
348
+ entry. name,
349
+ entry. lore_text,
350
+ entry. mechanics_text,
351
+ entry. antag_text
352
+ )
353
+
354
+ if (! cursor. Execute(codex_index))
355
+ to_chat (world , span_debug(" Codex: ABORTING! Database error: [ cursor. Error()] | [ cursor. ErrorMsg()] " ))
356
+ return
357
+
358
+ record_id++
359
+ if ((! (record_id % 100 )) || (record_id == total_entries))
360
+ to_chat (world , span_debug(" \t Codex: [ record_id] / [ total_entries] ..." ))
361
+
362
+ CHECK_TICK // We'd deadlock the server otherwise.
363
+
364
+ #undef CODEX_SEARCH_INDEX_FILE
365
+ #undef CODEX_ENTRY_LIMIT
0 commit comments