-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmod-manager.lua
410 lines (383 loc) · 12.4 KB
/
mod-manager.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
-- a graphical mod manager for df
local gui=require 'gui'
local widgets=require 'gui.widgets'
--[====[
gui/mod-manager
===============
A simple way to install and remove small mods, which are not included
in DFHack. Examples are `available here <https://github.com/warmist/df-mini-mods>`_.
.. image:: /docs/images/mod-manager.png
Each mod is a lua script located in :file:`{<DF>}/mods/`, which MUST define
the following variables:
:name: a name that is displayed in list
:author: mod author, also displayed
:description: a description of the mod
Of course, this doesn't actually make a mod - so one or more of the
following should also be defined:
:raws_list: a list (table) of file names that need to be copied over to df raws
:patch_entity: a chunk of text to patch entity
*TODO: add settings to which entities to add*
:patch_init: a chunk of lua to add to lua init
:patch_dofile: a list (table) of files to add to lua init as "dofile"
:patch_files: a table of files to patch
:filename: a filename (in raws folder) to patch
:patch: what to add
:after: a string after which to insert
:guard: a token that is used in raw files to find additions and remove them on uninstall
:guard_init: a token for lua file
:[pre|post]_(un)install:
Callback functions, which can trigger more complicated behavior
]====]
local entity_file=dfhack.getDFPath().."/raw/objects/entity_default.txt"
local init_file=dfhack.getDFPath().."/raw/init.lua"
local mod_dir=dfhack.getDFPath().."/hack/mods"
function fileExists(filename)
local file=io.open(filename,"rb")
if file==nil then
return
else
file:close()
return true
end
end
if not fileExists(init_file) then
local initFile=io.open(init_file,"a")
initFile:close()
end
function copyFile(from,to) --oh so primitive
local filefrom=io.open(from,"rb")
local fileto=io.open(to,"w+b")
local buf=filefrom:read("*a")
printall(buf)
fileto:write(buf)
filefrom:close()
fileto:close()
end
function patchInit(initFileName,patch_guard,code)
local initFile=io.open(initFileName,"a")
initFile:write(string.format("\n%s\n%s\n%s",patch_guard[1],
code,patch_guard[2]))
initFile:close()
end
function patchDofile( luaFileName,patch_guard,dofile_list,mod_path )
local luaFile=io.open(luaFileName,"a")
luaFile:write(patch_guard[1].."\n")
for _,v in ipairs(dofile_list) do
local fixed_path=mod_path:gsub("\\","/")
luaFile:write(string.format("dofile('%s/%s')\n",fixed_path,v))
end
luaFile:write(patch_guard[2].."\n")
luaFile:close()
end
function patchFile(file_name,patch_guard,after_string,code)
local input_lines=patch_guard[1].."\n"..code.."\n"..patch_guard[2]
local badchars="[%:%[%]]"
local find_string=after_string:gsub(badchars,"%%%1") --escape some bad chars
local entityFile=io.open(file_name,"r")
local buf=entityFile:read("*all")
entityFile:close()
local entityFile=io.open(file_name,"w+")
buf=string.gsub(buf,find_string,after_string.."\n"..input_lines)
entityFile:write(buf)
entityFile:close()
end
function findGuards(str,start,patch_guard)
local pStart=string.find(str,patch_guard[1],start)
if pStart==nil then return nil end
local pEnd=string.find(str,patch_guard[2],pStart)
if pEnd==nil then error("Start guard token found, but end was not found") end
return pStart-1,pEnd+#patch_guard[2]+1
end
function findGuardsFile(filename,patch_guard)
local file=io.open(filename,"r")
local buf=file:read("*all")
return findGuards(buf,1,patch_guard)
end
function unPatchFile(filename,patch_guard)
local file=io.open(filename,"r")
local buf=file:read("*all")
file:close()
local newBuf=""
local pos=1
local lastPos=1
repeat
local endPos
pos,endPos=findGuards(buf,lastPos,patch_guard)
newBuf=newBuf..string.sub(buf,lastPos,pos)
if endPos~=nil then
lastPos=endPos
end
until pos==nil
local file=io.open(filename,"w+")
file:write(newBuf)
file:close()
end
function checkInstalled(dfMod) --try to figure out if installed
if dfMod.checkInstalled then
return dfMod.checkInstalled()
else
if dfMod.raws_list then
for k,v in pairs(dfMod.raws_list) do
if fileExists(dfhack.getDFPath().."/raw/objects/"..v) then
return true,v
end
end
end
if dfMod.patch_entity then
if findGuardsFile(entity_file,dfMod.guard)~=nil then
return true,"entity_default.txt"
end
end
if dfMod.patch_files then
for k,v in pairs(dfMod.patch_files) do
if findGuardsFile(dfhack.getDFPath().."/raw/objects/"..v.filename,dfMod.guard)~=nil then
return true,"v.filename"
end
end
end
if dfMod.patch_init then
if findGuardsFile(init_file,dfMod.guard_init)~=nil then
return true,"init.lua"
end
end
end
end
textbox=defclass(textbox,widgets.Widget) -- a widget for longer auto-wrapping text
textbox.ATTRS{
text = '',
text_pen = COLOR_WHITE,
text_dpen = COLOR_DARKGREY, -- disabled
}
function peek_next( txt,i )
if i+1<=#txt then
return string.byte(txt,i+1)
end
end
function textbox:onRenderBody(dc)
local pen=self.text_pen
if self.disabled then
pen=self.text_dpen
end
dc:pen(pen):fill(0,0,dc.width,dc.height)
local txt = self.text
txt=txt:gsub("\t"," ")
local skip_next=false
for i=1,#txt do
local b=string.byte(txt,i)
if not skip_next then
if b==10 or b==13 then
dc:newline()
local next_b=peek_next(txt,i)
if (b==10 and next_b==13) or (b==13 and next_b==10) then
skip_next=true
end
else
dc:char(b)
end
else
skip_next=false
end
local tx,ty=dc:cursor()
if tx>=dc.width then --TODO: add better line breaking
dc:newline()
end
end
end
function textbox:setText( txt )
self.text=txt
end
manager=defclass(manager,gui.FramedScreen)
function manager:init(args)
self.mods={}
local mods=self.mods
local mlist=dfhack.internal.getDir(mod_dir)
if mlist==nil or #mlist==0 then
qerror("Mod directory not found! Are you sure it is in:"..mod_dir)
end
for k,v in ipairs(mlist) do
if v~="." and v~=".." then
local f,modData=pcall(dofile,mod_dir.."/".. v .. "/init.lua")
if f then
mods[modData.name]=modData
modData.guard=modData.guard or {">>"..modData.name.." patch","<<End "..modData.name.." patch"}
modData.guard_init={"--"..modData.guard[1],"--"..modData.guard[2]}
modData.path=mod_dir.."/"..v..'/'
end
end
end
---show thingy
local modList={}
for k,v in pairs(self.mods) do
table.insert(modList,{text=k,data=v})
end
self:addviews{
widgets.Panel{subviews={
widgets.Label{
text="Info:",
frame={t=1,l=1}
},
textbox{
text="<no-info>",
--text={text=self:callback("formDescription")},
view_id='info',
frame={t=2,l=1,b=5,r=1,yalign=0},
},
widgets.Label{
text={"Author:",{text=self:callback("formAuthor")}},
view_id='author',
frame={b=5,l=1}
},
widgets.Label{
text={
{text="Install",key="CUSTOM_I",key_sep="()",disabled=self:callback("curModInstalled"),
on_activate=self:callback("installCurrent")},NEWLINE,
{text="Uninstall",key="CUSTOM_U",key_sep="()",enabled=self:callback("curModInstalled"),
on_activate=self:callback("uninstallCurrent")},NEWLINE,
{text="Settings",key="CUSTOM_S",key_sep="()",enabled=self:callback("hasSettings")},NEWLINE,
{text="Exit",key="LEAVESCREEN",key_sep="()",},NEWLINE
},
frame={l=1,b=0}
},
},
frame={l=21,t=1,b=1}
},
widgets.Panel{subviews={
widgets.Label{
text="Mods:",
frame={t=1,l=1}
},
widgets.List{
choices=modList,
frame={t=2,l=1},
on_select=self:callback("selectMod")
},
},
frame={w=20,t=1,l=1,b=1}
},
}
self:updateState()
end
function manager:postinit(args)
self:selectMod(1,{data=self.selected})-- workaround for first call, now the subviews are constructed
end
function manager:curModInstalled()
return self.selected.installed
end
function manager:hasSettings()
return self.selected.settings -- somehow add the entity selection as a default, if it mods entities
end
function manager:formDescription()
local ret={}
if self.selected.description then
return self.selected.description
--[[
local str=require('utils').split_string(self.selected.description,"\n")
for _,s in ipairs(str) do
table.insert(ret,{text=s})
table.insert(ret,NEWLINE)
end
return ret]]
else
return "<no-info>"
end
end
function manager:formAuthor()
return self.selected.author or "<no-info>"
end
function manager:selectMod(idx,choice)
self.selected=choice.data
if self.subviews.info then
self.subviews.info:setText(self:formDescription())
self:updateLayout()
end
end
function manager:updateState()
for k,v in pairs(self.mods) do
v.installed=checkInstalled(v)
end
end
function manager:installCurrent()
self:install(self.selected)
end
function manager:uninstallCurrent()
self:uninstall(self.selected)
end
function manager:install(trgMod,force)
if trgMod==nil then
qerror 'Mod does not exist'
end
if not force then
local isInstalled,file=checkInstalled(trgMod) -- maybe load from .installed?
if isInstalled then
qerror("Mod already installed. File:"..file)
end
end
print("installing:"..trgMod.name)
if trgMod.pre_install then
trgMod.pre_install(args)
end
if trgMod.raws_list then
for k,v in pairs(trgMod.raws_list) do
copyFile(trgMod.path..v,dfhack.getDFPath().."/raw/objects/"..v)
end
end
if trgMod.patch_entity then
local entity_target="[ENTITY:MOUNTAIN]" --TODO configure
patchFile(entity_file,trgMod.guard,entity_target,trgMod.patch_entity)
end
if trgMod.patch_files then
for k,v in pairs(trgMod.patch_files) do
patchFile(dfhack.getDFPath().."/raw/objects/"..v.filename,trgMod.guard,v.after,v.patch)
end
end
if trgMod.patch_init then
patchInit(init_file,trgMod.guard_init,trgMod.patch_init)
end
if trgMod.patch_dofile then
patchDofile(init_file,trgMod.guard_init,trgMod.patch_dofile,trgMod.path)
end
trgMod.installed=true
if trgMod.post_install then
trgMod.post_install(self)
end
print("done")
end
function manager:uninstall(trgMod)
print("Uninstalling:"..trgMod.name)
if trgMod.pre_uninstall then
trgMod.pre_uninstall(args)
end
if trgMod.raws_list then
for k,v in pairs(trgMod.raws_list) do
os.remove(dfhack.getDFPath().."/raw/objects/"..v)
end
end
if trgMod.patch_entity then
unPatchFile(entity_file,trgMod.guard)
end
if trgMod.patch_files then
for k,v in pairs(trgMod.patch_files) do
unPatchFile(dfhack.getDFPath().."/raw/objects/"..v.filename,trgMod.guard)
end
end
if trgMod.patch_init or trgMod.patch_dofile then
unPatchFile(init_file,trgMod.guard_init)
end
trgMod.installed=false
if trgMod.post_uninstall then
trgMod.post_uninstall(args)
end
print("done")
end
function manager:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
else
self:inputToSubviews(keys)
end
end
if dfhack.gui.getCurFocus()~='title' then
qerror("Can only be used in title screen")
end
local m=manager{}
m:show()