Skip to content

Commit 8af27e4

Browse files
committed
add new tool fix/codex-pages
1 parent dd60bf2 commit 8af27e4

File tree

3 files changed

+301
-0
lines changed

3 files changed

+301
-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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
This tool will add pages to written works that do not have their start and end
16+
pages specified. The number of pages to be added will be determined mainly by
17+
the type of the written content, modified by its writing style and the strength
18+
of the 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 were authored 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+
Quires and scrolls will never display the number of pages they contain even if
36+
their page count is specified in the data structure of their written content.
37+
Once a quire is binded into a codex, the number of pages it contains will be
38+
displayed in its item description.

fix/codex-pages.lua

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

0 commit comments

Comments
 (0)