Skip to content

Commit 0b54694

Browse files
authored
Merge pull request #1525 from amade-w/codex-pages
Add new tool 'fix/codex-pages'
2 parents 63290af + 3808875 commit 0b54694

File tree

3 files changed

+312
-0
lines changed

3 files changed

+312
-0
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ that repo.
1313
Template for new versions:
1414

1515
## New Tools
16+
- `fix/codex-pages`: add pages to written content that have unspecified page counts.
1617

1718
## New Features
1819

docs/fix/codex-pages.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
fix/codex-pages
2+
===============
3+
4+
.. dfhack-tool::
5+
:summary: Add pages to written content that have no pages.
6+
:tags: fort bugfix items
7+
8+
Add pages to codices, quires, and scrolls that do not have specified page counts.
9+
10+
Usage
11+
-----
12+
13+
``fix/codex-pages [this|site|all]``
14+
15+
Pages will be added to written works that do not have properly specified page
16+
counts. The number of pages to be added will be determined mainly by the type
17+
of the written content, modified by its writing style and the strength of the
18+
style, with weighted randomization.
19+
20+
Options
21+
-------
22+
23+
``this``
24+
Add pages to the selected codex, quire, or scroll item.
25+
26+
``site``
27+
Add pages to all written works that are currently in the player's fortress.
28+
29+
``all``
30+
Add pages to all written works to have ever existed in the world.
31+
32+
Note
33+
----
34+
35+
This tool mitigates :bug:`9268` by generating new, randomized information for
36+
written content that do not have the start and end pages specified in their
37+
data structure. It cannot retrieve page count from written content that was
38+
already missing the page count information.
39+
40+
Also, unbound quires and scrolls do not display the number of pages they contain
41+
in their item description even if the data structure of their written content
42+
holds the information. However, once a quire that has written content with
43+
appropriately specified page count information is bound into a codex, its page
44+
count will be properly displayed in the resulting codex's item description.

fix/codex-pages.lua

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
-- Add pages to written content that have no pages.
2+
3+
local function isBook(item)
4+
if item and
5+
df.item_bookst:is_instance(item) or
6+
df.item_toolst:is_instance(item) and
7+
(item:getSubtype() == dfhack.items.findSubtype('TOOL:ITEM_TOOL_QUIRE') or
8+
item:getSubtype() == dfhack.items.findSubtype('TOOL:ITEM_TOOL_SCROLL')) and
9+
item:hasWriting()
10+
then
11+
return true
12+
end
13+
return false
14+
end
15+
16+
local function GetBooks(target)
17+
local books = {}
18+
local item
19+
if target.selected then
20+
local item = dfhack.gui.getSelectedItem(true)
21+
if item and isBook(item) then table.insert(books, item) end
22+
elseif target.site then
23+
local siteTools = df.global.world.items.other.TOOL
24+
for _, item in ipairs(siteTools) do
25+
if isBook(item) then table.insert(books, item) end
26+
end
27+
local siteBooks = df.global.world.items.other.BOOK
28+
for _, item in ipairs(siteBooks) do
29+
if isBook(item) then table.insert(books, item) end
30+
end
31+
end
32+
return books
33+
end
34+
35+
local function GetWrittenContent(book)
36+
for _, improvement in ipairs(book.improvements) do
37+
if df.itemimprovement_pagesst:is_instance(improvement) or
38+
df.itemimprovement_writingst:is_instance(improvement)
39+
then
40+
for _, content in ipairs(improvement.contents) do
41+
return df.written_content.find(content)
42+
end
43+
end
44+
end
45+
return nil
46+
end
47+
48+
local function GetPageCount(targetWcType)
49+
-- These values are based on polling page counts from various saves and may not be accurate.
50+
local types = {
51+
['NONE'] = {upperCount = 1, lowerCount = 1, mode = 1},
52+
['Manual'] = {upperCount = 250, lowerCount = 20, mode = 80},
53+
['Guide'] = {upperCount = 250, lowerCount = 20, mode = 100},
54+
['Chronicle'] = {upperCount = 450, lowerCount = 100, mode = nil},
55+
['ShortStory'] = {upperCount = 50, lowerCount = 10, mode = nil},
56+
['Novel'] = {upperCount = 450, lowerCount = 100, mode = 200},
57+
['Biography'] = {upperCount = 400, lowerCount = 100, mode = 250},
58+
['Autobiography'] = {upperCount = 450, lowerCount = 100, mode = 250},
59+
['Poem'] = {upperCount = 10, lowerCount = 1, mode = 1},
60+
['Play'] = {upperCount = 50, lowerCount = 20, mode = 30},
61+
['Letter'] = {upperCount = 10, lowerCount = 1, mode = nil},
62+
['Essay'] = {upperCount = 50, lowerCount = 10, mode = nil},
63+
['Dialog'] = {upperCount = 30, lowerCount = 5, mode = nil},
64+
['MusicalComposition'] = {upperCount = 20, lowerCount = 1, mode = 1},
65+
['Choreography'] = {upperCount = 1, lowerCount = 1, mode = 1},
66+
['ComparativeBiography'] = {upperCount = 300, lowerCount = 150, mode = nil},
67+
['BiographicalDictionary'] = {
68+
upperCount = math.max(300, math.min(500, math.ceil(df.global.hist_figure_next_id / 1000))),
69+
lowerCount = math.max(100, math.min(150, math.floor(df.global.hist_figure_next_id / 10000))),
70+
mode = nil}, -- Very few samples were available, so this one is mostly arbitrary.
71+
['Genealogy'] = {upperCount = 5, lowerCount = 1, mode = 4},
72+
['Encyclopedia'] = {upperCount = 150, lowerCount = 50, mode = nil},
73+
['CulturalHistory'] = {upperCount = 450, lowerCount = 100, mode = 200},
74+
['CulturalComparison'] = {upperCount = 400, lowerCount = 100, mode = 200},
75+
['AlternateHistory'] = {upperCount = 250, lowerCount = 100, mode = 150},
76+
['TreatiseOnTechnologicalEvolution'] = {upperCount = 300, lowerCount = 100, mode = nil},
77+
['Dictionary'] = {upperCount = 450, lowerCount = 100, mode = 250},
78+
['StarChart'] = {upperCount = 1, lowerCount = 1, mode = 1},
79+
['StarCatalogue'] = {upperCount = 150, lowerCount = 10, mode = 100},
80+
['Atlas'] = {upperCount = 30, lowerCount = 10, mode = 25},
81+
}
82+
local upperCount, lowerCount = 1, 1
83+
local mode
84+
for wcType, tab in pairs(types) do
85+
if df.written_content_type[wcType] == targetWcType then
86+
upperCount = tab.upperCount
87+
lowerCount = tab.lowerCount
88+
mode = tab.mode
89+
end
90+
end
91+
return upperCount, lowerCount, mode
92+
end
93+
94+
local function GetPageCountModifier(targetStyle, targetStrength)
95+
-- These values are arbitrary and may not even have any effect on page count in vanilla DF.
96+
local styles = {
97+
['NONE'] = 0,
98+
['Meandering'] = 0.5,
99+
['Cheerful'] = 0,
100+
['Depressing'] = 0.1,
101+
['Rigid'] = 0,
102+
['Serious'] = 0,
103+
['Disjointed'] = 0.2,
104+
['Ornate'] = 0.2,
105+
['Forceful'] = 0,
106+
['Humorous'] = 0,
107+
['Immature'] = 0.3,
108+
['SelfIndulgent'] = 0.5,
109+
['Touching'] = 0,
110+
['Compassionate'] = 0,
111+
['Vicious'] = 0,
112+
['Concise'] = -0.2,
113+
['Scornful'] = 0,
114+
['Witty'] = 0,
115+
['Ranting'] = 1,
116+
}
117+
local strength = {
118+
['NONE'] = 1,
119+
['Thorough'] = 1.5,
120+
['Somewhat'] = 1,
121+
['Hint'] = 0.5,
122+
}
123+
local pageCountModifier = 0
124+
for style, modifier in pairs(styles) do
125+
if df.written_content_style[style] == targetStyle then
126+
pageCountModifier = modifier
127+
break
128+
end
129+
end
130+
for strength, addModifier in pairs(strength) do
131+
if df.writing_style_modifier_type[strength] == targetStrength then
132+
if pageCountModifier ~= 0 then
133+
pageCountModifier = pageCountModifier * addModifier
134+
break
135+
end
136+
end
137+
end
138+
return pageCountModifier
139+
end
140+
141+
local rng = dfhack.random.new(nil, 10)
142+
local seed = dfhack.world.ReadCurrentTick()
143+
144+
local function SetPageCount(upperCount, lowerCount, mode)
145+
if upperCount > 1 then
146+
local range = upperCount - lowerCount
147+
local increment = 1 + math.floor(range ^ 2)
148+
local weightedTable = {}
149+
local weight = 0
150+
for i = lowerCount, upperCount, 1 do
151+
weight = weight + increment - math.floor(math.abs(i - mode) ^ 2)
152+
if i == mode and mode == 1 then
153+
-- Set heavy bias for very short written forms with mostly 1 page long works.
154+
weight = weight + increment ^ 2
155+
end
156+
table.insert(weightedTable, weight)
157+
end
158+
local limit = weight
159+
rng:init(seed, 10)
160+
local result = rng:random(limit)
161+
for i, weight in ipairs(weightedTable) do
162+
if result <= weight then
163+
return i + lowerCount - 1
164+
end
165+
end
166+
end
167+
return 1
168+
end
169+
170+
local function AddPages(wc)
171+
local pages = 0
172+
if wc.page_start == -1 and wc.page_end == -1 then
173+
local wcType = wc.type
174+
local upperCount, lowerCount, mode = GetPageCount(wcType)
175+
if upperCount and lowerCount then
176+
local modifier = 1
177+
for i, style in ipairs(wc.styles) do
178+
if wc.style_strength[i] then
179+
modifier = modifier + GetPageCountModifier(style, wc.style_strength[i])
180+
end
181+
end
182+
upperCount = math.max(1, math.ceil(upperCount * modifier))
183+
lowerCount = math.max(1, math.floor(lowerCount * modifier))
184+
if mode and mode ~= 1 then
185+
mode = math.max(1, math.floor(mode * modifier))
186+
end
187+
else
188+
upperCount, lowerCount = 1, 1
189+
end
190+
mode = mode or math.ceil((lowerCount + upperCount) / 2)
191+
wc.page_start = 1
192+
wc.page_end = SetPageCount(upperCount, lowerCount, mode)
193+
pages = wc.page_end
194+
end
195+
return pages
196+
end
197+
198+
local function FixPageCount(target)
199+
local writtenContents = {}
200+
if not target.all then
201+
local books = GetBooks(target)
202+
if #books == 0 then
203+
if target.selected then
204+
print('No book with written content selected.')
205+
elseif target.site then
206+
print('No books available in site.')
207+
end
208+
return
209+
end
210+
for _, book in ipairs(books) do
211+
table.insert(writtenContents, GetWrittenContent(book))
212+
end
213+
else
214+
writtenContents = df.global.world.written_contents.all
215+
end
216+
local booksModified = 0
217+
local pagesAdded = 0
218+
for _, wc in ipairs(writtenContents) do
219+
local pages = 0
220+
pages = AddPages(wc)
221+
if pages > 0 then
222+
local title
223+
if wc.title == '' then
224+
title = 'an untitled work'
225+
else
226+
title = ('"%s"'):format(wc.title)
227+
end
228+
print(('%d pages added to %s.'):format(pages, title))
229+
pagesAdded = pagesAdded + pages
230+
seed = seed + pages
231+
booksModified = booksModified + 1
232+
end
233+
end
234+
if booksModified > 0 then
235+
local plural = ''
236+
if booksModified > 1 then plural = 's' end
237+
print(('\nA total of %d pages were added to %d book%s.'):format(pagesAdded, booksModified, plural))
238+
elseif target.selected then
239+
print('Selected book already has pages in it.')
240+
else
241+
print('No written content with unspecified page counts were found; no pages were added to any books.')
242+
end
243+
end
244+
245+
local function Main(args)
246+
local target = {
247+
selected = false,
248+
site = false,
249+
all = false,
250+
}
251+
if #args > 0 then
252+
if args[1] == 'help' then
253+
print(dfhack.script_help())
254+
return
255+
end
256+
if args[1] == 'this' then target.selected = true end
257+
if args[1] == 'site' then target.site = true end
258+
if args[1] == 'all' then target.all = true end
259+
FixPageCount(target)
260+
end
261+
end
262+
263+
if not dfhack.isSiteLoaded() and not dfhack.world.isFortressMode() then
264+
qerror('This script requires the game to be in fortress mode.')
265+
end
266+
267+
Main({...})

0 commit comments

Comments
 (0)