Database
oxmysql is required and is the only library this resource needs. The almanac lives in the database — that is what makes pages save live instead of sitting in a config file.
Nothing to do
The three tables are created on first start if they are missing, and a welcome chapter is written so the almanac is not empty the first time someone opens it.
| Table | Holds |
|---|---|
rm_guidebook_chapters | Chapters |
rm_guidebook_entries | Entries and their text |
rm_guidebook_signposts | Signposts in the world |
If you would rather see the schema before it lands, or your database user cannot create tables at runtime, run install/schema.sql yourself first.
It only ever creates
Every statement is CREATE TABLE IF NOT EXISTS. It never drops and never alters, so running it again after an update is safe.
rm_guidebook_chapters
CREATE TABLE IF NOT EXISTS `rm_guidebook_chapters` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`slug` VARCHAR(48) NOT NULL,
`title` VARCHAR(120) NOT NULL,
`rank` INT NOT NULL DEFAULT 1,
`shown` TINYINT(1) NOT NULL DEFAULT 1,
`unfurled` TINYINT(1) NOT NULL DEFAULT 1,
`access` LONGTEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_rm_guidebook_chapters_slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;access holds a JSON blob:
{"on":false,"jobs":[{"job":"police","grade":1}]}rm_guidebook_entries
Same shape, plus three columns that matter:
| Column | |
|---|---|
chapter_slug | The chapter it belongs to. Indexed |
body | The HTML the press wrote. Up to 512 KiB |
revision | Goes up by one on every write. Stale editors are refused |
revision is what produces "Someone else wrote this leaf just now" — a save naming an older revision is rejected rather than silently overwriting whoever got there first.
slug is unique across the whole table, not per chapter. Two chapters cannot both hold an entry called rules.
rm_guidebook_signposts
| Column | |
|---|---|
pos | {"x":0,"y":0,"z":0} |
text | Title size, font, colour and reach |
blip | Enabled, sprite, tint, scale |
marker | Enabled, kind, size, tint, spin, angle, reach |
entry_slug | The entry it opens — or NULL when it carries its own body |
body | Its own text, when entry_slug is null |
routable | Whether /almanac_route will route to it |
A signpost opens an entry or carries a body, never both.
Why the JSON columns are LONGTEXT
Not an oversight.
MariaDB before 10.2 has no JSON type
And the resource never asks the database to look inside those columns — every blob is decoded and validated in Lua on the way out. Storing them as LONGTEXT costs nothing and works on every version anyone actually runs.
Backing it up
Three small tables. Even a large almanac is well under a megabyte unless someone has pasted a book into an entry.
mysqldump --single-transaction DBNAME \
rm_guidebook_chapters rm_guidebook_entries rm_guidebook_signposts \
-u DBUSER -p > rm_guidebook_backup.sqlRestore all three together — a signpost pointing at an entry that no longer exists is not fatal, but it is a prompt that opens nothing.
There is no foreign key between entries and chapters
The link is chapter_slug, enforced in Lua when a record is written rather than by the database. An entry whose chapter was removed by hand becomes an orphan — /almanac_resync will report it and drop it from the shelf rather than crashing.
Editing rows by hand
You can, and then you have to tell the resource:
/almanac_resyncIt reads the almanac back from the database and pushes it to everyone. Without it, the in-memory shelf still holds what was there before — the resource does not poll.
You need this whenever something changed the tables behind its back: a row edited in a SQL client, a backup restored, or a second server writing to the same database.
Useful queries
What the almanac actually holds:
SELECT
(SELECT COUNT(*) FROM rm_guidebook_chapters) AS chapters,
(SELECT COUNT(*) FROM rm_guidebook_entries) AS entries,
(SELECT COUNT(*) FROM rm_guidebook_signposts) AS signposts;Everything currently gated, which is the query to run when someone says a page vanished:
SELECT 'chapter' AS kind, slug, title, access FROM rm_guidebook_chapters
WHERE access IS NOT NULL AND access <> '' AND access NOT LIKE '%"on":false%'
UNION ALL
SELECT 'entry', slug, title, access FROM rm_guidebook_entries
WHERE access IS NOT NULL AND access <> '' AND access NOT LIKE '%"on":false%';Orphaned entries — a chapter removed by hand leaves these behind:
SELECT e.slug, e.title, e.chapter_slug
FROM rm_guidebook_entries e
LEFT JOIN rm_guidebook_chapters c ON c.slug = e.chapter_slug
WHERE c.slug IS NULL;Signposts pointing at an entry that no longer exists:
SELECT s.slug, s.title, s.entry_slug
FROM rm_guidebook_signposts s
LEFT JOIN rm_guidebook_entries e ON e.slug = s.entry_slug
WHERE s.entry_slug IS NOT NULL AND e.slug IS NULL;Migrating a legacy guidebook
If this server previously ran the FiveM guidebook resource whose tables are named rcore_guidebook_*, and those tables are still in the same database:
/almanac_migrateIt copies categories, pages and points across, reshaping them into chapters, entries and signposts.
It never overwrites
A slug already in the almanac is left alone, so running it twice is safe. You can run it, look at the result, strike what you do not want, and run it again.
Two things do not survive, because they have no RedM equivalent:
| Why | |
|---|---|
| Blip sprites | The old resource stored FiveM sprite numbers; RedM addresses blips by name. Everything becomes blip_poi |
| Marker types | Same reason. Everything becomes a cylinder |
Titles, order, hidden flags, job permissions, page text, positions, colours, sizes, draw distances and which page a point opens all come across as written.
The seed
seed = { demo = true },Writes a welcome chapter and entry the first time the resource starts against an empty chapters table. Harmless once anything exists — it checks for emptiness, not for a marker.
Set it false before the first start if you would rather begin with nothing.
For something more substantial to look at, /almanac_demo writes six chapters, fifteen entries and seven signposts, and /almanac_demo clear removes them.