Jump to content

Lorebooks2MediaWiki: Difference between revisions

From example
No edit summary
No edit summary
Line 6: Line 6:


References for context:
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
* 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 ==
== What we will build ==
We’ll model a small universe called "Farlandia" with:
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.
* Places (Environment): town, landmark, etc.
- Keys and specificity (primary/secondary keys, AND/NOT), trigger rules and placement.
* Entities (Lore): monsters like "slime" with both PList-like traits and Ali:Chat-like example reactions.
- Recursive scanning (parent→child activation), constant vs conditional entries.
* Keys and specificity (primary/secondary keys, AND/NOT), trigger rules and placement.
- Exporters to JSON for use as a Lorebook, and transclusion for in-wiki pages.
* 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:
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.
* 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) ==
== Data Model (SMW) ==
We use pages for concepts and SMW properties for fields. Minimal property set:
We use pages for concepts and SMW properties for fields. Minimal property set:
- [[Has type::Page]] properties:
 
* [[Has type::Page]] properties:
   * [[Property:Belongs to world]] — parent world (e.g., Farlandia)
   * [[Property:Belongs to world]] — parent world (e.g., Farlandia)
   * [[Property:Parent concept]] — parent concept to support recursion (e.g., Monsters → Slimes)
   * [[Property:Parent concept]] — parent concept to support recursion (e.g., Monsters → Slimes)
- [[Has type::Text]] properties:
 
* [[Has type::Text]] properties:
   * [[Property:Plist]] — serialized PList-like content (single-line or semicolon-separated)
   * [[Property:Plist]] — serialized PList-like content (single-line or semicolon-separated)
   * [[Property:AliChat]] — example dialogues (Ali:Chat-style text)
   * [[Property:AliChat]] — example dialogues (Ali:Chat-style text)
Line 35: Line 41:
   * [[Property:Placement]] — hints like after-character | before-character | depth:5
   * [[Property:Placement]] — hints like after-character | before-character | depth:5
   * [[Property:Non-recursable]] — yes/no (to opt out from recursion)
   * [[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 ==
== Minimal Page Templates ==

Revision as of 16:58, 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:

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)

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.