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