1{
2 stdenv,
3 lib,
4 buildEnv,
5 replaceVars,
6 makeWrapper,
7 runCommand,
8 coreutils,
9 gawk,
10 dwarf-fortress,
11 dwarf-therapist,
12 SDL2_mixer,
13 enableDFHack ? false,
14 dfhack,
15 enableSoundSense ? false,
16 soundSense,
17 jre,
18 expect,
19 xvfb-run,
20 writeText,
21 enableStoneSense ? false,
22 enableTWBT ? false,
23 twbt,
24 themes ? { },
25 theme ? null,
26 extraPackages ? [ ],
27 # General config options:
28 enableIntro ? true,
29 enableTruetype ? null, # defaults to 24, see init.txt
30 enableFPS ? false,
31 enableTextMode ? false,
32 enableSound ? true,
33 # An attribute set of settings to override in data/init/*.txt.
34 # For example, `init.FOO = true;` is translated to `[FOO:YES]` in init.txt
35 settings ? { },
36# TODO world-gen.txt, interface.txt require special logic
37}:
38
39let
40 dfhack' = dfhack.override {
41 inherit enableStoneSense;
42 };
43
44 isAtLeast50 = dwarf-fortress.baseVersion >= 50;
45
46 # If TWBT is null or the dfVersion is wrong, it isn't supported (for example, on version 50).
47 enableTWBT' = enableTWBT && twbt != null && (twbt.dfVersion or null) == dwarf-fortress.version;
48
49 ptheme = if builtins.isString theme then builtins.getAttr theme themes else theme;
50
51 baseEnv = buildEnv {
52 name = "dwarf-fortress-base-env-${dwarf-fortress.dfVersion}";
53
54 # These are in inverse order for first packages to override the next ones.
55 paths =
56 extraPackages
57 ++ lib.optional (theme != null) ptheme
58 ++ lib.optional enableDFHack dfhack'
59 ++ lib.optional enableSoundSense soundSense
60 ++ lib.optionals enableTWBT' [
61 twbt.lib
62 twbt.art
63 ]
64 ++ [ dwarf-fortress ];
65
66 ignoreCollisions = true;
67 };
68
69 settings' = lib.recursiveUpdate {
70 init = {
71 PRINT_MODE =
72 if enableTextMode then
73 "TEXT"
74 else if enableTWBT' then
75 "TWBT"
76 else if stdenv.hostPlatform.isDarwin then
77 "STANDARD" # https://www.bay12games.com/dwarves/mantisbt/view.php?id=11680
78 else
79 null;
80 INTRO = enableIntro;
81 TRUETYPE = enableTruetype;
82 FPS = enableFPS;
83 SOUND = enableSound;
84 };
85 } settings;
86
87 forEach = attrs: f: lib.concatStrings (lib.mapAttrsToList f attrs);
88
89 toTxt =
90 v:
91 if lib.isBool v then
92 if v then "YES" else "NO"
93 else if lib.isInt v then
94 toString v
95 else if lib.isString v then
96 v
97 else
98 throw "dwarf-fortress: unsupported configuration value ${toString v}";
99
100 config =
101 runCommand "dwarf-fortress-config"
102 {
103 nativeBuildInputs = [
104 gawk
105 makeWrapper
106 ];
107 }
108 (
109 ''
110 mkdir -p $out/data/init
111
112 edit_setting() {
113 v=''${v//'&'/'\&'}
114 if [ -f "$out/$file" ]; then
115 if ! gawk -i inplace -v RS='\r?\n' '
116 { n += sub("\\[" ENVIRON["k"] ":[^]]*\\]", "[" ENVIRON["k"] ":" ENVIRON["v"] "]"); print }
117 END { exit(!n) }
118 ' "$out/$file"; then
119 echo "error: no setting named '$k' in $out/$file" >&2
120 exit 1
121 fi
122 else
123 echo "warning: no file $out/$file; cannot edit" >&2
124 fi
125 }
126 ''
127 + forEach settings' (
128 file: kv:
129 ''
130 file=data/init/${lib.escapeShellArg file}.txt
131 if [ -f "${baseEnv}/$file" ]; then
132 cp "${baseEnv}/$file" "$out/$file"
133 else
134 echo "warning: no file ${baseEnv}/$file; cannot copy" >&2
135 fi
136 ''
137 + forEach kv (
138 k: v:
139 lib.optionalString (v != null) ''
140 export k=${lib.escapeShellArg k} v=${lib.escapeShellArg (toTxt v)}
141 edit_setting
142 ''
143 )
144 )
145 + lib.optionalString enableDFHack ''
146 mkdir -p $out/hack
147
148 # Patch the MD5
149 orig_md5=$(< "${dwarf-fortress}/hash.md5.orig")
150 patched_md5=$(< "${dwarf-fortress}/hash.md5")
151 input_file="${dfhack'}/hack/symbols.xml"
152 output_file="$out/hack/symbols.xml"
153
154 echo "[DFHack Wrapper] Fixing Dwarf Fortress MD5:"
155 echo " Input: $input_file"
156 echo " Search: $orig_md5"
157 echo " Output: $output_file"
158 echo " Replace: $patched_md5"
159
160 substitute "$input_file" "$output_file" --replace-fail "$orig_md5" "$patched_md5"
161 ''
162 );
163
164 # This is a separate environment because the config files to modify may come
165 # from any of the paths in baseEnv.
166 env = buildEnv {
167 name = "dwarf-fortress-env-${dwarf-fortress.dfVersion}";
168 paths = [
169 config
170 baseEnv
171 ];
172 ignoreCollisions = true;
173 };
174in
175
176lib.throwIf (enableTWBT' && !enableDFHack) "dwarf-fortress: TWBT requires DFHack to be enabled"
177 lib.throwIf
178 (enableStoneSense && !enableDFHack)
179 "dwarf-fortress: StoneSense requires DFHack to be enabled"
180 lib.throwIf
181 (enableTextMode && enableTWBT')
182 "dwarf-fortress: text mode and TWBT are mutually exclusive"
183
184 stdenv.mkDerivation
185 {
186 pname = "dwarf-fortress";
187 version = dwarf-fortress.dfVersion;
188
189 dfInit = replaceVars ./dwarf-fortress-init.in {
190 inherit env;
191 stdenv_shell = "${stdenv.shell}";
192 cp = "${coreutils}/bin/cp";
193 rm = "${coreutils}/bin/rm";
194 ln = "${coreutils}/bin/ln";
195 cat = "${coreutils}/bin/cat";
196 mkdir = "${coreutils}/bin/mkdir";
197 printf = "${coreutils}/bin/printf";
198 uname = "${coreutils}/bin/uname";
199 SDL2_mixer = "${SDL2_mixer}/lib/libSDL2_mixer.so";
200 };
201
202 runDF = ./dwarf-fortress.in;
203 runSoundSense = ./soundSense.in;
204
205 passthru = {
206 inherit
207 dwarf-fortress
208 dwarf-therapist
209 twbt
210 env
211 ;
212 dfhack = dfhack';
213 };
214
215 dontUnpack = true;
216 dontBuild = true;
217 preferLocalBuild = true;
218 installPhase = ''
219 mkdir -p $out/bin
220
221 substitute $runDF $out/bin/dwarf-fortress \
222 --subst-var-by stdenv_shell ${stdenv.shell} \
223 --subst-var-by dfExe ${dwarf-fortress.exe} \
224 --subst-var dfInit
225 chmod 755 $out/bin/dwarf-fortress
226 ''
227 + lib.optionalString enableDFHack ''
228 substitute $runDF $out/bin/dfhack \
229 --subst-var-by stdenv_shell ${stdenv.shell} \
230 --subst-var-by dfExe dfhack \
231 --subst-var dfInit
232 chmod 755 $out/bin/dfhack
233 ''
234 + lib.optionalString enableSoundSense ''
235 substitute $runSoundSense $out/bin/soundsense \
236 --subst-var-by stdenv_shell ${stdenv.shell} \
237 --subst-var-by jre ${jre} \
238 --subst-var dfInit
239 chmod 755 $out/bin/soundsense
240 '';
241
242 doInstallCheck = stdenv.hostPlatform.isLinux;
243 nativeInstallCheckInputs = lib.optionals stdenv.hostPlatform.isLinux [
244 expect
245 xvfb-run
246 ];
247
248 installCheckPhase =
249 let
250 commonExpectStatements = ''
251 expect "Loading bindings from data/init/interface.txt"
252 '';
253 dfHackExpectScript = writeText "dfhack-test.exp" (
254 ''
255 spawn env NIXPKGS_DF_OPTS=debug xvfb-run $env(out)/bin/dfhack
256 ''
257 + commonExpectStatements
258 + ''
259 expect "DFHack is ready. Have a nice day!"
260 expect "DFHack version ${dfhack'.version}"
261 expect "\[DFHack\]#"
262 send -- "lua print(os.getenv('out'))\r"
263 expect "$env(out)"
264 # Don't send 'die' here; just exit. Some versions of dfhack crash on exit.
265 exit 0
266 ''
267 );
268 vanillaExpectScript =
269 fmod:
270 writeText "vanilla-test.exp" (
271 ''
272 spawn env NIXPKGS_DF_OPTS=debug,${lib.optionalString fmod "fmod"} xvfb-run $env(out)/bin/dwarf-fortress
273 ''
274 + commonExpectStatements
275 + ''
276 exit 0
277 ''
278 );
279 in
280 ''
281 export HOME="$(mktemp -dt dwarf-fortress.XXXXXX)"
282 ''
283 + lib.optionalString enableDFHack ''
284 expect ${dfHackExpectScript}
285 df_home="$(find ~ -name "df_*" | head -n1)"
286 test -f "$df_home/dfhack"
287 ''
288 + lib.optionalString isAtLeast50 ''
289 expect ${vanillaExpectScript true}
290 df_home="$(find ~ -name "df_*" | head -n1)"
291 test ! -f "$df_home/dfhack"
292 test -f "$df_home/libfmod_plugin.so"
293 ''
294 + ''
295 expect ${vanillaExpectScript false}
296 df_home="$(find ~ -name "df_*" | head -n1)"
297 test ! -f "$df_home/dfhack"
298 test ! -f "$df_home/libfmod_plugin.so"
299 ''
300 + ''
301 test -d "$df_home/data"
302 '';
303
304 inherit (dwarf-fortress) meta;
305 }