Skip to content

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.

TableHolds
rm_guidebook_chaptersChapters
rm_guidebook_entriesEntries and their text
rm_guidebook_signpostsSignposts 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 ​

sql
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:

json
{"on":false,"jobs":[{"job":"police","grade":1}]}

rm_guidebook_entries ​

Same shape, plus three columns that matter:

Column
chapter_slugThe chapter it belongs to. Indexed
bodyThe HTML the press wrote. Up to 512 KiB
revisionGoes 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}
textTitle size, font, colour and reach
blipEnabled, sprite, tint, scale
markerEnabled, kind, size, tint, spin, angle, reach
entry_slugThe entry it opens — or NULL when it carries its own body
bodyIts own text, when entry_slug is null
routableWhether /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.

bash
mysqldump --single-transaction DBNAME \
  rm_guidebook_chapters rm_guidebook_entries rm_guidebook_signposts \
  -u DBUSER -p > rm_guidebook_backup.sql

Restore 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_resync

It 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:

sql
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:

sql
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:

sql
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:

sql
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_migrate

It 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 spritesThe old resource stored FiveM sprite numbers; RedM addresses blips by name. Everything becomes blip_poi
Marker typesSame 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 ​

lua
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.

Documentation for RedMorrow. Scripts are licensed per server — redistribution is not permitted.