Lorebooks2MediaWiki: Difference between revisions
m M4tsuri moved page Lorebooks2Mediawiki to Lorebooks2MediaWiki |
No edit summary |
||
Line 1: | Line 1: | ||
= From Lorebooks to MediaWiki: A Demonstration with Semantic MediaWiki (SMW) and Scribunto = | = From Lorebooks to MediaWiki: A Demonstration with Semantic MediaWiki (SMW) and Scribunto = | ||
This article shows, with concrete, runnable examples, how common Lorebook concepts (World Info, PLists, Ali:Chat, keys, placement, recursion, and filters) map cleanly to MediaWiki using Semantic MediaWiki (SMW) and Scribunto (Lua) | '''This article is generated by GenAI and slightly modified by the author.''' | ||
This article shows, with concrete, runnable examples, how common Lorebook concepts (World Info, PLists, Ali:Chat, keys, placement, recursion, and filters) map cleanly to MediaWiki using Semantic MediaWiki (SMW) and Scribunto (Lua). | |||
References for context: | References for context: |
Revision as of 16:48, 17 October 2025
From Lorebooks to MediaWiki: A Demonstration with Semantic MediaWiki (SMW) and Scribunto
This article is generated by GenAI and slightly modified by the author.
This article shows, with concrete, runnable examples, how common Lorebook concepts (World Info, PLists, Ali:Chat, keys, placement, recursion, and filters) map cleanly to MediaWiki using Semantic MediaWiki (SMW) and Scribunto (Lua).
References for context: - World Info & Lorebook concepts: https://rentry.co/world-info-encyclopedia - Character formatting (PLists + Ali:Chat): https://wikia.schneedc.com/bot-creation/trappu/creation
What we will build
We’ll model a small universe called "Farlandia" with: - Places (Environment): town, landmark, etc. - Entities (Lore): monsters like "slime" with both PList-like traits and Ali:Chat-like example reactions. - Keys and specificity (primary/secondary keys, AND/NOT), trigger rules and placement. - Recursive scanning (parent→child activation), constant vs conditional entries. - Exporters to JSON for use as a Lorebook, and transclusion for in-wiki pages.
Technologies used: - Semantic MediaWiki (SMW) for structured data: properties, categories, queries. - Scribunto (Lua) for lightweight logic: key matching, AND/NOT filters, recursion, and JSON export.
Data Model (SMW)
We use pages for concepts and SMW properties for fields. Minimal property set: - PageProperty "Has type" is a declarative property and can only be used on a property or category page. properties:
* Property:Belongs to world — parent world (e.g., Farlandia) * Property:Parent concept — parent concept to support recursion (e.g., Monsters → Slimes)
- TextProperty "Has type" is a declarative property and can only be used on a property or category page. properties:
* Property:Plist — serialized PList-like content (single-line or semicolon-separated) * Property:AliChat — example dialogues (Ali:Chat-style text) * Property:Primary keys — comma-separated keys (e.g., slime,slimes) * Property:Secondary keys — comma-separated keys (e.g., enemy,gelatin) * Property:Key mode — one of: constant | conditional | disabled * Property:Logic — one of: ANY | AND | NOT (maps to Lorebook specificity) * Property:Placement — hints like after-character | before-character | depth:5 * Property:Non-recursable — yes/no (to opt out from recursion)
Tip: Keep properties predictable, single purpose, and text-based for simple editing. You can always add more typed properties later (e.g., Has type::Boolean for Non-recursable).
Minimal Page Templates
Create simple templates to standardize data entry. Below are wikitext examples; adapt names as you like.
Template:World
Purpose: model a world (like a global Lorebook/WI scope) and group entries.
<includeonly>[[Category:World]] ; Name: {{{name|{{PAGENAME}}}}} ; Description: {{{description|}}} </includeonly><noinclude> Usage: {{World|name=Farlandia|description=A fantasy setting used for examples.}} </noinclude>
Template:Concept
Purpose: model an Environment/Lore entry (place, monster, item, etc.).
<includeonly>[[Category:Concept]] [[Belongs to world::{{{world|}}}]] {{#if:{{{parent|}}}|[[Parent concept::{{{parent}}}]]|}} {{#if:{{{plist|}}}|[[Plist::{{{plist}}}]]|}} {{#if:{{{alichat|}}}|[[AliChat::{{{alichat}}}]]|}} {{#if:{{{primary|}}}|[[Primary keys::{{{primary}}}]]|}} {{#if:{{{secondary|}}}|[[Secondary keys::{{{secondary}}}]]|}} {{#if:{{{logic|}}}|[[Logic::{{{logic}}}]]|}} {{#if:{{{mode|}}}|[[Key mode::{{{mode}}}]]|}} {{#if:{{{placement|}}}|[[Placement::{{{placement}}}]]|}} {{#if:{{{nonrecursable|}}}|[[Non-recursable::{{{nonrecursable}}}]]|}} </includeonly><noinclude> Usage: {{Concept | world=Farlandia | parent=Farlandia:Monsters | plist=[slime: enemy, slimeball, made of gelatin, bounces to move, annoyance] | alichat={{user}}: Slime? {{char}}: "Oh... Those things." She looks down and slightly blushes. | primary=slime,slimes | secondary= | logic=ANY | mode=conditional | placement=depth:5 | nonrecursable=no }} </noinclude>
Example Pages
Farlandia (world)
Create page: "Farlandia" with:
{{World|description=A fantasy setting used for examples.}}
Farlandia:Monsters (category concept)
Create page: "Farlandia:Monsters" with:
{{Concept | world=Farlandia | plist=[Farlandia's monsters: slimes, dragons] | primary=monsters | logic=ANY | mode=constant | placement=after-character | nonrecursable=no }}
This acts like a parent that mentions child keys (slimes, dragons), enabling recursive discovery.
Slime (concept)
Create page: "Slime" with:
{{Concept | world=Farlandia | parent=Farlandia:Monsters | plist=[slime: enemy, slimeball, made of gelatin, bounces to move, annoyance] | alichat={{user}}: Slime? {{char}}: "Oh... Those things." She looks down and slightly blushes in embarrassment. "I remember first fighting those things. They're so quick that it's hard to get a good strike on them." Shizuru brushes away the memory. "Anyways, let's keep going. There's more places to explore." | primary=slime,slimes | logic=ANY | mode=conditional | placement=depth:5 | nonrecursable=no }}
Mossford (place/environment)
Create page: "Mossford" with:
{{Concept | world=Farlandia | plist=[Mossford(The town of Moss): town, mossy buildings, moss used for(magic, power), has(tavern, bank, inn, castle), kind people, wealthy] | primary=Mossford,town,moss | logic=ANY | mode=conditional | placement=before-character | nonrecursable=yes }}
Notes: - Mode=constant means always injected; conditional means key-triggered; disabled means off. - Logic=AND/NOT works with secondary keys:
- AND example: Primary: house,home; Secondary: your,yours,she,her — both must match. - NOT example: Secondary keys negate injection if present.
- Non-recursable=yes prevents child-follows-parent injection for this concept (mimics Non-Recursable in lorebooks, often used for Ali:Chat-only entries).
Querying with SMW
You can list concepts for a world or find concepts by key. Examples:
List all concepts in Farlandia:
{{#ask: [[Category:Concept]] [[Belongs to world::Farlandia]] |?Plist |?AliChat |?Primary keys |?Secondary keys |format=table |limit=50 }}
Find concepts that mention a particular key string in Primary keys (simple contains):
{{#ask: [[Category:Concept]] [[Primary keys::~*slime*]] [[Belongs to world::Farlandia]] |?Plist |?AliChat |format=table |limit=20 }}
Scribunto (Lua) Module: Key Matching, Logic, Recursion, Export
We’ll create a Lua module to: - Parse a "query context" (the current prompt text) and decide which concepts trigger. - Implement Logic ANY/AND/NOT with primary/secondary keys. - Walk parent->child recursion unless Non-recursable=yes. - Produce a plain text injection block and/or JSON export compatible with Lorebook formats.
Create a module page: "Module:Lorebook" with this Lua code:
local p = {} local function splitCSV(s) if not s or s == '' then return {} end local t = {} for token in mw.text.gsplit(s, ',', true) do token = mw.text.trim(token) if token ~= '' then table.insert(t, token:lower()) end end return t end local function containsAny(haystack, needles) local s = ' ' .. haystack:lower() .. ' ' for _, n in ipairs(needles) do if n ~= '' and s:find('%f[%w]' .. mw.ustring.gsub(mw.text.nowiki(n), '([^%w])', '%%%1') .. '%f[%w]') then return true end end return false end local function andMatch(haystack, primary, secondary) return containsAny(haystack, primary) and containsAny(haystack, secondary) end local function notMatch(haystack, primary, secondary) return containsAny(haystack, primary) and not containsAny(haystack, secondary) end local function anyMatch(haystack, primary, secondary) -- secondary unused for ANY return containsAny(haystack, primary) end local logicDispatch = { ANY = anyMatch, AND = andMatch, NOT = notMatch, } local function getProp(page, prop) local data = mw.ext.semwiki.getProperty(page, prop) -- requires Semantic MediaWiki if type(data) == 'table' then return data[1] end return data end local function getAllConcepts(world) local query = string.format('[[Category:Concept]][[Belongs to world::%s]]', world) local res = mw.ext.semwiki.ask(query, { mainlabel = 'page', ['?Plist'] = 'plist', ['?AliChat'] = 'alichat', ['?Primary keys'] = 'primary', ['?Secondary keys'] = 'secondary', ['?Logic'] = 'logic', ['?Key mode'] = 'mode', ['?Parent concept'] = 'parent', ['?Non-recursable'] = 'nonrec', ['?Placement'] = 'placement', limit = 999 }) return res or {} end local function indexByTitle(rows) local idx = {} for _, r in ipairs(rows) do idx[r.page] = r end return idx end local function shouldTrigger(row, haystack) local mode = (row.mode or 'conditional'):upper() if mode == 'DISABLED' then return false end if mode == 'CONSTANT' then return true end local logic = (row.logic or 'ANY'):upper() local f = logicDispatch[logic] or anyMatch local primary = splitCSV(row.primary) local secondary = splitCSV(row.secondary) return f(haystack, primary, secondary) end local function walkRecursion(rows, idx, startPages) -- Build children map from Parent concept local children = {} for _, r in ipairs(rows) do local parent = r.parent if parent and parent ~= '' then children[parent] = children[parent] or {} table.insert(children[parent], r.page) end end local visited = {} local stack = {} for _, ptitle in ipairs(startPages) do table.insert(stack, ptitle) end local ordered = {} while #stack > 0 do local cur = table.remove(stack) if not visited[cur] then visited[cur] = true table.insert(ordered, cur) local row = idx[cur] if row and (row.nonrec or ''):lower() ~= 'yes' then for _, c in ipairs(children[cur] or {}) do table.insert(stack, c) end end end end return ordered end local function buildInjection(rows, triggered) -- Simple: PList first (joined), then Ali:Chat snippets local plistChunks, chatChunks = {}, {} for _, title in ipairs(triggered) do local r for _, row in ipairs(rows) do if row.page == title then r = row break end end if r then if r.plist and r.plist ~= '' then table.insert(plistChunks, r.plist) end if r.alichat and r.alichat ~= '' then table.insert(chatChunks, r.alichat) end end end local text = '' if #plistChunks > 0 then text = text .. table.concat(plistChunks, '\n') .. '\n' end if #chatChunks > 0 then text = text .. table.concat(chatChunks, '\n') end return text end function p.inject(frame) local world = frame.args.world or 'Farlandia' local haystack = frame.args.context or '' local rows = getAllConcepts(world) local idx = indexByTitle(rows) local start = {} for _, r in ipairs(rows) do if shouldTrigger(r, haystack) then table.insert(start, r.page) end end local triggered = walkRecursion(rows, idx, start) return buildInjection(rows, triggered) end function p.export(frame) -- Export JSON array resembling a lorebook entries list local world = frame.args.world or 'Farlandia' local rows = getAllConcepts(world) local entries = {} for _, r in ipairs(rows) do table.insert(entries, { title = r.page, plist = r.plist or '', alichat = r.alichat or '', primary = r.primary or '', secondary = r.secondary or '', logic = (r.logic or 'ANY'), mode = (r.mode or 'conditional'), parent = r.parent or '', nonrecursable = r.nonrec or '', placement = r.placement or '' }) end return mw.text.jsonEncode(entries) end return p
Notes: - This module uses Semantic MediaWiki querying via mw.ext.semwiki.ask/getProperty. If your SMW exposes a different API wrapper (e.g., mw.ext.smw), adjust the calls accordingly:
- Replace mw.ext.semwiki.ask with mw.ext.smw.ask - Replace mw.ext.semwiki.getProperty with mw.ext.smw.getQueryResult or mw.smw.getProperty per your setup.
- The matching in containsAny is deliberately lightweight. You can expand to support quoted keys or regex-based matching.
Using the Module
Inject text for a given world and a chat/context string:
{{#invoke:Lorebook|inject|world=Farlandia|context=We entered the town of Mossford where slimes attack.}}
This will collect all triggered concepts (e.g., Mossford and Slime), apply recursion (e.g., parent Farlandia:Monsters), and produce a concatenated block of PList+Ali:Chat snippets.
Export the current world as JSON (to save or feed a frontend):
{{#invoke:Lorebook|export|world=Farlandia}}
You can put the export on a page like "Farlandia:Export" and copy the JSON.
Mapping Lorebook Concepts → MediaWiki
- World Info vs Lorebook: Use pages and categories. A page like "Farlandia" groups concepts through Farlandia. Character-specific lore can use another property Name. - PList: Store as text in ... exactly as you'd author it in a Lorebook. You may also split categories into separate properties later. - Ali:Chat: Store in ... verbatim (use nowiki or pipes carefully when needed). - Keys: ... and ... as comma-separated strings. Logic AND. - Placement: Record as a hint in .... Consumers (Lua or exporters) decide how to map to a UI’s positions. - Constant/Conditional/Disabled: ... toggles inclusion. - Recursion: Use ... edges; Lua decides to follow unless yes. - Stacking and filters: Add ... and ... if you need per-character scoping. Extend Lua to honor them.
Advanced: PList Base World
You can implement the "PList base world" pattern by creating bracket border concepts with insertion orders. In wiki, model insertion order as Property:Order (number) and update the Lua builder to wrap all PList between order 2 and 998. Then omit [] in child PList and end lines with semicolons, matching the guide’s algorithm.
Quality and Maintenance
- Pages are collaboratively editable and diffable. - SMW queries provide dynamic tables and navigation. - Lua module localizes decision logic; exporting keeps compatibility with external tools.
Try it checklist
1) Create properties (SMW): Belongs to world, Parent concept, Plist, AliChat, Primary keys, Secondary keys, Key mode, Logic, Placement, Non-recursable (and optionally Order). 2) Create templates: World and Concept. 3) Create pages: Farlandia, Farlandia:Monsters, Slime, Mossford using examples above.
4) Create Module:Lorebook with the Lua code and verify
[Farlandia's monsters: slimes, dragons] [slime: enemy, slimeball, made of gelatin, bounces to move, annoyance] {{user}}: Slime? {{char}}: "Oh... Those things." She blushes, remembering their first battle.
works.
5) Optionally add an Export page calling [{"nonrecursable":"","parent":"","primary":"monsters","alichat":"","title":"Farlandia:Monsters","placement":"after-character","mode":"constant","plist":"[Farlandia's monsters: slimes, dragons]","logic":"ANY","secondary":""},{"nonrecursable":"yes","parent":"","primary":"Mossford,town,moss","alichat":"","title":"Mossford","placement":"before-character","mode":"conditional","plist":"[Mossford(The town of Moss): town, mossy buildings, moss used for(magic, power), has(tavern, bank, inn, castle), kind people, wealthy]","logic":"ANY","secondary":""},{"nonrecursable":"","parent":"Farlandia:Monsters","primary":"slime,slimes","alichat":"{{user}}: Slime?\n{{char}}: \"Oh... Those things.\" She blushes, remembering their first battle.","title":"Slime","placement":"depth:5","mode":"conditional","plist":"[slime: enemy, slimeball, made of gelatin, bounces to move, annoyance]","logic":"ANY","secondary":""}] and copy JSON into a lorebook-compatible frontend.
If you’d like, I can auto-create the pages and a minimal Module:Lorebook on your wiki via the API and save a ready-to-import XML export in this repo.