Fetch and merge

develop
Ryan Williams 2022-06-04 12:17:28 -07:00
commit 23e85a0d24
26 changed files with 1516 additions and 830 deletions

@ -2,7 +2,8 @@
set -e set -e
tardest="df.tar.bz2" df_tardest="df.tar.bz2"
save_tardest="test_save.tgz"
selfmd5=$(openssl md5 < "$0") selfmd5=$(openssl md5 < "$0")
echo $selfmd5 echo $selfmd5
@ -16,40 +17,48 @@ cd "$DF_FOLDER/.."
if [ -f receipt ]; then if [ -f receipt ]; then
if [ "$selfmd5" != "$(cat receipt)" ]; then if [ "$selfmd5" != "$(cat receipt)" ]; then
echo "download-df.sh changed; removing DF" echo "download-df.sh changed; re-downloading tarballs"
rm receipt rm receipt
else else
echo "Already downloaded $DF_VERSION" echo "Already downloaded $DF_VERSION tarballs"
fi fi
fi fi
if [ ! -f receipt ]; then if [ ! -f receipt ]; then
rm -f "$tardest" rm -f "$df_tardest" "$save_tardest"
minor=$(echo "$DF_VERSION" | cut -d. -f2) minor=$(echo "$DF_VERSION" | cut -d. -f2)
patch=$(echo "$DF_VERSION" | cut -d. -f3) patch=$(echo "$DF_VERSION" | cut -d. -f3)
url="http://www.bay12games.com/dwarves/df_${minor}_${patch}_linux.tar.bz2" echo "Downloading DF $DF_VERSION"
echo Downloading
while read url; do while read url; do
echo "Attempting download: ${url}" echo "Attempting download: ${url}"
if wget -v "$url" -O "$tardest"; then if wget -v "$url" -O "$df_tardest"; then
break break
fi fi
done <<URLS done <<URLS
https://www.bay12games.com/dwarves/df_${minor}_${patch}_linux.tar.bz2 https://www.bay12games.com/dwarves/df_${minor}_${patch}_linux.tar.bz2
https://files.dfhack.org/DF/0.${minor}.${patch}/df_${minor}_${patch}_linux.tar.bz2 https://files.dfhack.org/DF/0.${minor}.${patch}/df_${minor}_${patch}_linux.tar.bz2
URLS URLS
echo $tardest echo $df_tardest
if ! test -f "$tardest"; then if ! test -f "$df_tardest"; then
echo "DF failed to download: $tardest not found" echo "DF failed to download: $df_tardest not found"
exit 1 exit 1
fi fi
echo "Downloading test save"
#test_save_url="https://files.dfhack.org/DF/0.${minor}.${patch}/test_save.tgz"
test_save_url="https://drive.google.com/uc?export=download&id=1XvYngl-DFONiZ9SD9OC4B2Ooecu8rPFz"
if ! wget -v "$test_save_url" -O "$save_tardest"; then
echo "failed to download test save"
exit 1
fi
echo $save_tardest
fi fi
rm -rf df_linux rm -rf df_linux
mkdir df_linux mkdir -p df_linux/data/save
echo Extracting echo Extracting
tar xf "$tardest" --strip-components=1 -C df_linux tar xf "$df_tardest" --strip-components=1 -C df_linux
tar xf "$save_tardest" -C df_linux/data/save
echo Done echo Done
echo "$selfmd5" > receipt echo "$selfmd5" > receipt

@ -73,7 +73,7 @@ with open(test_init_file, 'w') as f:
f.write(''' f.write('''
devel/dump-rpc dfhack-rpc.txt devel/dump-rpc dfhack-rpc.txt
:lua dfhack.internal.addScriptPath(dfhack.getHackPath()) :lua dfhack.internal.addScriptPath(dfhack.getHackPath())
test --resume --modes=none,title "lua scr.breakdown_level=df.interface_breakdown_types.%s" test --resume -- lua scr.breakdown_level=df.interface_breakdown_types.%s
''' % ('NONE' if args.no_quit else 'QUIT')) ''' % ('NONE' if args.no_quit else 'QUIT'))
test_config_file = 'test_config.json' test_config_file = 'test_config.json'

File diff suppressed because it is too large Load Diff

@ -0,0 +1,5 @@
#build label(build)
,,CSdd
1 #build label(build)
2 ,,CSdd

@ -0,0 +1,6 @@
#place label(place)
s(5x3)
,,afunswebhlzSgpd
1 #place label(place)
2 s(5x3)
3 ,,afunswebhlzSgpd

@ -1,4 +1,4 @@
#dig label(dig) #dig label(dig) start(3;3)
d,d,,,d d,d,,,d
d,,j,,d d,,j,,d
d,u,d,u,d d,u,d,u,d

1 #dig label(dig) #dig label(dig) start(3;3)
2 d,d,,,d
3 d,,j,,d
4 d,u,d,u,d

@ -1,4 +1,4 @@
#build label(build) #build label(build) start(14;14)
,trackNS,trackE,,trackW,trackS,trackN,,gs(1x2),ga(2x1),,gx(1x2),gw(1x2),,gw(1x2),gx(1x2),gd(2x1),,gs(1x2),,trackN,trackS,trackE,,trackW,trackNS ,trackNS,trackE,,trackW,trackS,trackN,,gs(1x2),ga(2x1),,gx(1x2),gw(1x2),,gw(1x2),gx(1x2),gd(2x1),,gs(1x2),,trackN,trackS,trackE,,trackW,trackNS
trackEW,,trackSE,,trackSW,trackNE,trackNW,,,gd(2x1),,,,,,,ga(2x1),,,,trackNE,trackNW,trackSE,,trackSW,,trackEW trackEW,,trackSE,,trackSW,trackNE,trackNW,,,gd(2x1),,,,,,,ga(2x1),,,,trackNE,trackNW,trackSE,,trackSW,,trackEW
trackS,trackSE,,,trackNSE,trackNSW,trackEW,,Mrsssqq(2x1),,,Msh,,,,,Msk,Mrsqq(2x1),,,trackEW,trackNSE,trackNSW,,,trackSW,trackS trackS,trackSE,,,trackNSE,trackNSW,trackEW,,Mrsssqq(2x1),,,Msh,,,,,Msk,Mrsqq(2x1),,,trackEW,trackNSE,trackNSW,,,trackSW,trackS

1 #build label(build) #build label(build) start(14;14)
2 ,trackNS,trackE,,trackW,trackS,trackN,,gs(1x2),ga(2x1),,gx(1x2),gw(1x2),,gw(1x2),gx(1x2),gd(2x1),,gs(1x2),,trackN,trackS,trackE,,trackW,trackNS
3 trackEW,,trackSE,,trackSW,trackNE,trackNW,,,gd(2x1),,,,,,,ga(2x1),,,,trackNE,trackNW,trackSE,,trackSW,,trackEW
4 trackS,trackSE,,,trackNSE,trackNSW,trackEW,,Mrsssqq(2x1),,,Msh,,,,,Msk,Mrsqq(2x1),,,trackEW,trackNSE,trackNSW,,,trackSW,trackS

@ -1,4 +1,4 @@
#dig label(dig) #dig label(dig) start(14;14)
,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d ,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d
d,,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,,d d,,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,,d
d,d,,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,,d,d d,d,,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,,d,d

1 #dig label(dig) #dig label(dig) start(14;14)
2 ,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d
3 d,,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,,d
4 d,d,,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,,d,d

@ -0,0 +1,2 @@
#place label(place)
s(5x3)
1 #place label(place)
2 s(5x3)

@ -0,0 +1,5 @@
#notes
description=integration test for the gui/quantum script
width=5
height=5
extra_fn=test_gui_quantum
1 #notes
2 description=integration test for the gui/quantum script
3 width=5
4 height=5
5 extra_fn=test_gui_quantum

@ -563,37 +563,6 @@
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
[
{
"condition" : "AtLeast",
"flags" :
[
"collected",
"dyeable"
],
"item_type" : "THREAD",
"value" : 5
},
{
"condition" : "AtLeast",
"flags" :
[
"unrotten",
"dye"
],
"value" : 15
}
],
"job" : "DyeThread"
},
{
"amount_left" : 1,
"amount_total" : 1,
"frequency" : "Daily",
"id" : 18,
"is_active" : false,
"is_validated" : false,
"item_conditions" :
[ [
{ {
"condition" : "AtLeast", "condition" : "AtLeast",
@ -611,7 +580,7 @@
"unrotten", "unrotten",
"dye" "dye"
], ],
"value" : 15 "value" : 3
} }
], ],
"job" : "DyeCloth" "job" : "DyeCloth"
@ -620,7 +589,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 19, "id" : 18,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -655,20 +624,14 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 20, "id" : 19,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
[ [
{ {
"condition" : "AtLeast", "condition" : "AtLeast",
"flags" : "item_type" : "WOOD",
[
"non_economic",
"hard"
],
"item_type" : "BOULDER",
"material" : "INORGANIC",
"value" : 20 "value" : 20
}, },
{ {
@ -684,13 +647,16 @@
], ],
"item_subtype" : "ITEM_TOOL_JUG", "item_subtype" : "ITEM_TOOL_JUG",
"job" : "MakeTool", "job" : "MakeTool",
"material" : "INORGANIC" "material_category" :
[
"wood"
]
}, },
{ {
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 21, "id" : 20,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -719,7 +685,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 22, "id" : 21,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -747,7 +713,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 23, "id" : 22,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -775,7 +741,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 24, "id" : 23,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -805,7 +771,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 25, "id" : 24,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -835,7 +801,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 26, "id" : 25,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -864,7 +830,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 27, "id" : 26,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -895,7 +861,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 28, "id" : 27,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -931,7 +897,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 29, "id" : 28,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -967,7 +933,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 30, "id" : 29,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1003,7 +969,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 31, "id" : 30,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1022,9 +988,9 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 32, "id" : 31,
"is_active" : false, "is_active" : false,
"is_validated" : true, "is_validated" : false,
"item_conditions" : "item_conditions" :
[ [
{ {
@ -1048,7 +1014,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 33, "id" : 32,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1070,7 +1036,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 34, "id" : 33,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1096,7 +1062,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 35, "id" : 34,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1122,7 +1088,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 36, "id" : 35,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1148,7 +1114,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 37, "id" : 36,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1181,7 +1147,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 38, "id" : 37,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1220,7 +1186,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 39, "id" : 38,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1254,7 +1220,7 @@
"amount_left" : 4, "amount_left" : 4,
"amount_total" : 4, "amount_total" : 4,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 40, "id" : 39,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1277,7 +1243,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 41, "id" : 40,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1301,7 +1267,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 42, "id" : 41,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1335,7 +1301,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 43, "id" : 42,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1364,7 +1330,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 44, "id" : 43,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :
@ -1393,9 +1359,9 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 45, "id" : 44,
"is_active" : false, "is_active" : false,
"is_validated" : true, "is_validated" : false,
"item_conditions" : "item_conditions" :
[ [
{ {
@ -1424,7 +1390,7 @@
"amount_left" : 1, "amount_left" : 1,
"amount_total" : 1, "amount_total" : 1,
"frequency" : "Daily", "frequency" : "Daily",
"id" : 46, "id" : 45,
"is_active" : false, "is_active" : false,
"is_validated" : false, "is_validated" : false,
"item_conditions" : "item_conditions" :

@ -77,6 +77,11 @@ give10right: {give move={Right 10}}
togglesequence: &{Down} togglesequence: &{Down}
togglesequence2: &{Down 2} togglesequence2: &{Down 2}
# these aliases use the DFHack "search" plugin to filter the right column
forbidsearch: s{search}&f{Left}{Right}
permitsearch: s{search}&p{Left}{Right}
togglesearch: s{search}&&{Left}{Right}
masterworkonly: {prefix}{Right}{Up 2}f{Right}{Up 2}&^ masterworkonly: {prefix}{Right}{Up 2}f{Right}{Up 2}&^
artifactonly: {prefix}{Right}{Up 2}f{Right}{Up}&^ artifactonly: {prefix}{Right}{Up 2}f{Right}{Up}&^
@ -116,7 +121,7 @@ plants: {foodprefix}b{Right}{Down 4}p^
booze: {foodprefix}b{Right}{Down 5}p{Down}p^ booze: {foodprefix}b{Right}{Down 5}p{Down}p^
seeds: {foodprefix}b{Right}{Down 9}p^ seeds: {foodprefix}b{Right}{Down 9}p^
dye: {foodprefix}b{Right}{Down 11}{Right}{Down 28}{togglesequence 4}^ dye: {foodprefix}b{Right}{Down 11}{Right}{Down 28}{togglesequence 4}^
tallow: {foodprefix}b{Right}{Down 13}{Right}stallow&p^ tallow: {foodprefix}b{Right}{Down 13}{Right}{permitsearch search=tallow}^
miscliquid: {foodprefix}b{Right}{Down 18}p^ miscliquid: {foodprefix}b{Right}{Down 18}p^
wax: {foodprefix}b{Right}{Down 15}{Right}{Down 6}&^ wax: {foodprefix}b{Right}{Down 15}{Right}{Down 6}&^
@ -126,7 +131,7 @@ forbidplants: {foodprefix}{Right}{Down 4}f^
forbidbooze: {foodprefix}{Right}{Down 5}f{Down}f^ forbidbooze: {foodprefix}{Right}{Down 5}f{Down}f^
forbidseeds: {foodprefix}{Right}{Down 9}f^ forbidseeds: {foodprefix}{Right}{Down 9}f^
forbiddye: {foodprefix}{Right}{Down 11}{Right}{Down 28}{togglesequence 4}^ forbiddye: {foodprefix}{Right}{Down 11}{Right}{Down 28}{togglesequence 4}^
forbidtallow: {foodprefix}{Right}{Down 13}{Right}stallow&f^ forbidtallow: {foodprefix}{Right}{Down 13}{Right}{forbidsearch search=tallow}^
forbidmiscliquid: {foodprefix}{Right}{Down 18}f^ forbidmiscliquid: {foodprefix}{Right}{Down 18}f^
forbidwax: {foodprefix}{Right}{Down 15}{Right}{Down 6}&^ forbidwax: {foodprefix}{Right}{Down 15}{Right}{Down 6}&^
@ -136,7 +141,7 @@ permitplants: {foodprefix}{Right}{Down 4}p^
permitbooze: {foodprefix}{Right}{Down 5}p{Down}p^ permitbooze: {foodprefix}{Right}{Down 5}p{Down}p^
permitseeds: {foodprefix}{Right}{Down 9}p^ permitseeds: {foodprefix}{Right}{Down 9}p^
permitdye: {forbiddye} permitdye: {forbiddye}
permittallow: {foodprefix}{Right}{Down 13}{Right}stallow&p^ permittallow: {foodprefix}{Right}{Down 13}{Right}{permitsearch search=tallow}^
permitmiscliquid: {foodprefix}{Right}{Down 18}p^ permitmiscliquid: {foodprefix}{Right}{Down 18}p^
permitwax: {forbidwax} permitwax: {forbidwax}
@ -200,7 +205,8 @@ shells: {refuseprefix}b{Right}{Down 5}p^
teeth: {refuseprefix}b{Right}{Down 6}p^ teeth: {refuseprefix}b{Right}{Down 6}p^
horns: {refuseprefix}b{Right}{Down 7}p^ horns: {refuseprefix}b{Right}{Down 7}p^
hair: {refuseprefix}b{Right}{Down 8}p^ hair: {refuseprefix}b{Right}{Down 8}p^
craftrefuse: {skulls}{permitbones}{permitshells}{permitteeth}{permithorns}{permithair} usablehair: {refuseprefix}b{Right}{Down 8}{Right}{togglesearch search=sheep}{togglesearch search=llama}{togglesearch search=alpaca}{togglesearch search=troll}^
craftrefuse: {skulls}{permitbones}{permitshells}{permitteeth}{permithorns}{permitusablehair}
forbidcorpses: {refuseprefix}{Right}{Down}f^ forbidcorpses: {refuseprefix}{Right}{Down}f^
forbidrawhides: {refuseprefix}{Right 2}{Down}&^ forbidrawhides: {refuseprefix}{Right 2}{Down}&^
@ -211,7 +217,8 @@ forbidshells: {refuseprefix}{Right}{Down 5}f^
forbidteeth: {refuseprefix}{Right}{Down 6}f^ forbidteeth: {refuseprefix}{Right}{Down 6}f^
forbidhorns: {refuseprefix}{Right}{Down 7}f^ forbidhorns: {refuseprefix}{Right}{Down 7}f^
forbidhair: {refuseprefix}{Right}{Down 8}f^ forbidhair: {refuseprefix}{Right}{Down 8}f^
forbidcraftrefuse: {forbidskulls}{forbidbones}{forbidshells}{forbidteeth}{forbidhorns}{forbidhair} forbidusablehair: {refuseprefix}{Right}{Down 8}{Right}{forbidsearch search=sheep}{forbidsearch search=llama}{forbidsearch search=alpaca}{forbidsearch search=troll}^
forbidcraftrefuse: {forbidskulls}{forbidbones}{forbidshells}{forbidteeth}{forbidhorns}{forbidusablehair}
permitcorpses: {refuseprefix}{Right}{Down}p^ permitcorpses: {refuseprefix}{Right}{Down}p^
permitrawhides: {forbidrawhides} permitrawhides: {forbidrawhides}
@ -222,7 +229,8 @@ permitshells: {refuseprefix}{Right}{Down 5}p^
permitteeth: {refuseprefix}{Right}{Down 6}p^ permitteeth: {refuseprefix}{Right}{Down 6}p^
permithorns: {refuseprefix}{Right}{Down 7}p^ permithorns: {refuseprefix}{Right}{Down 7}p^
permithair: {refuseprefix}{Right}{Down 8}p^ permithair: {refuseprefix}{Right}{Down 8}p^
permitcraftrefuse: {permitskulls}{permitbones}{permitshells}{permitteeth}{permithorns}{permithair} permitusablehair: {refuseprefix}{Right}{Down 8}{Right}{permitsearch search=sheep}{permitsearch search=llama}{permitsearch search=alpaca}{permitsearch search=troll}^
permitcraftrefuse: {permitskulls}{permitbones}{permitshells}{permitteeth}{permithorns}{permitusablehair}
################################## ##################################
@ -352,9 +360,11 @@ finishedgoodsprefix: {enter_sp_config}{Down 10}
enablefinishedgoods: {finishedgoodsprefix}e^ enablefinishedgoods: {finishedgoodsprefix}e^
disablefinishedgoods: {finishedgoodsprefix}d^ disablefinishedgoods: {finishedgoodsprefix}d^
crafts: {finishedgoodsprefix}{Right}f{Right}{Down 9}{togglesequence 9}^ crafts: {finishedgoodsprefix}{Right}f{Right}{Down 9}{togglesequence 9}^
goblets: {finishedgoodsprefix}{Right}f{Right}{Down 2}&^ goblets: {finishedgoodsprefix}{Right}f{Right}{Down 2}&^
jugs: {finishedgoodsprefix}{Right}f{Right}{Up 2}&{Left}{Down 2}f{Down}f{Down}f^ jugs: {finishedgoodsprefix}{Right}f{Right}{Up 2}&{Left}{Down 2}f{Down}f{Down}f^
stonetools: {finishedgoodsprefix}{Right}f{Right}{Up 2}&{Left}{Down 2}f{Down}f{Down}f^
woodentools: {finishedgoodsprefix}{Right}f{Right}{Up 2}&{Left}{Down}f{Down}f{Down}f{Down}f{Right}&^
forbidcrafts: {finishedgoodsprefix}{Right 2}{Down 9}{togglesequence 9}^ forbidcrafts: {finishedgoodsprefix}{Right 2}{Down 9}{togglesequence 9}^
forbidgoblets: {finishedgoodsprefix}{Right 2}{Down 2}&^ forbidgoblets: {finishedgoodsprefix}{Right 2}{Down 2}&^
@ -385,6 +395,15 @@ adamantinethread: {clothprefix}b{Right}{Down 3}p^
cloth: {clothprefix}b{Right}{Down 4}p{Down}p{Down}p^ cloth: {clothprefix}b{Right}{Down 4}p{Down}p{Down}p^
adamantinecloth: {clothprefix}b{Right}{Up}p^ adamantinecloth: {clothprefix}b{Right}{Up}p^
forbidthread: {clothprefix}{Right}f{Down}f{Down}f^
forbidadamantinethread: {clothprefix}{Right}{Down 3}f^
forbidcloth: {clothprefix}{Right}{Down 4}f{Down}f{Down}f^
forbidadamantinecloth: {clothprefix}{Right}{Up}f^
permitthread: {clothprefix}{Right}p{Down}p{Down}p^
permitadamantinethread: {clothprefix}{Right}{Down 3}p^
permitcloth: {clothprefix}{Right}{Down 4}p{Down}p{Down}p^
permitadamantinecloth: {clothprefix}{Right}{Up}p^
################################## ##################################
# weapon stockpile adjustments # weapon stockpile adjustments

@ -36,6 +36,7 @@ keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
# gui/quickfort script - apply pre-made blueprints to the map # gui/quickfort script - apply pre-made blueprints to the map
keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort
keybinding add Alt-F@dwarfmode gui/quickfort
# gui/rename script - rename units and buildings # gui/rename script - rename units and buildings
keybinding add Ctrl-Shift-N gui/rename keybinding add Ctrl-Shift-N gui/rename

@ -3538,14 +3538,24 @@ The class defines the following attributes:
:visible: Specifies that the view should be painted. :visible: Specifies that the view should be painted.
:active: Specifies that the view should receive events, if also visible. :active: Specifies that the view should receive events, if also visible.
:view_id: Specifies an identifier to easily identify the view among subviews. :view_id: Specifies an identifier to easily identify the view among subviews.
This is reserved for implementation of top-level views, and should This is reserved for use by script writers and should not be set by
not be used by widgets for their internal subviews. library widgets for their internal subviews.
:on_focus: Called when the view gains keyboard focus; see ``setFocus()`` below.
:on_unfocus: Called when the view loses keyboard focus.
It also always has the following fields: It also always has the following fields:
:subviews: Contains a table of all subviews. The sequence part of the :subviews: Contains a table of all subviews. The sequence part of the
table is used for iteration. In addition, subviews are also table is used for iteration. In addition, subviews are also
indexed under their *view_id*, if any; see ``addviews()`` below. indexed under their ``view_id``, if any; see ``addviews()`` below.
:parent_view: A reference to the parent view. This field is ``nil`` until the
view is added as a subview to another view with ``addviews()``.
:focus_group: The list of widgets in a hierarchy. This table is unique and empty
when a view is initialized, but is replaced by a shared table when
the view is added to a parent via ``addviews()``. If a view in the
focus group has keyboard focus, that widget can be accessed via
``focus_group.cur``.
:focus: A boolean indicating whether the view currently has keyboard focus.
These fields are computed by the layout process: These fields are computed by the layout process:
@ -3639,8 +3649,27 @@ The class has the following methods:
Calls ``onInput`` on all visible active subviews, iterating the ``subviews`` Calls ``onInput`` on all visible active subviews, iterating the ``subviews``
sequence in *reverse order*, so that topmost subviews get events first. sequence in *reverse order*, so that topmost subviews get events first.
Returns *true* if any of the subviews handled the event. Returns ``true`` if any of the subviews handled the event. If a subview within
the view's ``focus_group`` has focus and it and all of its ancestors are
active and visible, that subview is offered the chance to handle the input
before any other subviews.
* ``view:getPreferredFocusState()``
Returns ``false`` by default, but should be overridden by subclasses that may
want to take keyboard focus (if it is unclaimed) when they are added to a
parent view with ``addviews()``.
* ``view:setFocus(focus)``
Sets the keyboard focus to the view if ``focus`` is ``true``, or relinquishes
keyboard focus if ``focus`` is ``false``. Views that newly acquire keyboard
focus will trigger the ``on_focus`` callback, and views that lose keyboard
focus will trigger the ``on_unfocus`` callback. While a view has focus, all
keyboard input is sent to that view before any of its siblings or parents.
Keyboard input is propagated as normal (see ``inputToSubviews()`` above) if
there is no view with focus or if the view with focus returns ``false`` from
its ``onInput()`` function.
.. _lua-gui-screen: .. _lua-gui-screen:
@ -3864,6 +3893,23 @@ Attributes:
:key: If specified, the field is disabled until this key is pressed. Must be given as a string. :key: If specified, the field is disabled until this key is pressed. Must be given as a string.
:key_sep: If specified, will be used to customize how the activation key is :key_sep: If specified, will be used to customize how the activation key is
displayed. See ``token.key_sep`` in the ``Label`` documentation below. displayed. See ``token.key_sep`` in the ``Label`` documentation below.
:modal: Whether the ``EditField`` should prevent input from propagating to other
widgets while it has focus. You can set this to ``true``, for example,
if you don't want a ``List`` widget to react to arrow keys while the
user is editing.
An ``EditField`` will only read and process text input if it has keyboard focus.
It will automatically acquire keyboard focus when it is added as a subview to
a parent that has not already granted keyboard focus to another widget. If you
have more than one ``EditField`` on a screen, you can select which has focus by
calling ``setFocus(true)`` on the field object.
If an activation ``key`` is specified, the ``EditField`` will manage its own
focus. It will start in the unfocused state, and pressing the activation key
will acquire keyboard focus. Pressing the Enter key will release keyboard focus
and then call the ``on_submit`` callback. Pressing the Escape key will also
release keyboard focus, but first it will restore the text that was displayed
before the ``EditField`` gained focus and then call the ``on_change`` callback.
Label class Label class
----------- -----------

@ -42,7 +42,17 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## Misc Improvements ## Misc Improvements
- `confirm`: added a confirmation dialog for removing manager orders - `confirm`: added a confirmation dialog for removing manager orders
- `dfhack-examples-guide`: refine food preparation orders and fix conditions for making jugs and pots in the ``basic`` manager orders - `confirm`: allow players to pause the confirmation dialog until they exit the current screen
- `dfhack-examples-guide`: refine food preparation orders so meal types are chosen intelligently according to the amount of meals that exist and the number of aviailable items to cook with
- `dfhack-examples-guide`: reduce required stock of dye for "Dye cloth" orders
- `dfhack-examples-guide`: fix material conditions for making jugs and pots
- `dfhack-examples-guide`: make wooden jugs by default to differentiate them from other stone tools. this allows players to more easily select jugs out with a properly-configured stockpile (i.e. the new ``woodentools`` alias)
- `quickfort-alias-guide`: new aliases: ``forbidsearch``, ``permitsearch``, and ``togglesearch`` use the `search-plugin` plugin to alter the settings for a filtered list of item types when configuring stockpiles
- `quickfort-alias-guide`: new aliases: ``stonetools`` and ``woodentools``. the ``jugs`` alias is deprecated. please use ``stonetools`` instead, which is the same as the old ``jugs`` alias.
- `quickfort-alias-guide`: new aliases: ``usablehair``, ``permitusablehair``, and ``forbidusablehair`` alter settings for the types of hair/wool that can be made into cloth: sheep, llama, alpaca, and troll. The ``craftrefuse`` aliases have been altered to use this alias as well.
- `quickfort-alias-guide`: new aliases: ``forbidthread``, ``permitthread``, ``forbidadamantinethread``, ``permitadamantinethread``, ``forbidcloth``, ``permitcloth``, ``forbidadamantinecloth``, and ``permitadamantinecloth`` give you more control how adamantine-derived items are stored
- `quickfort`: `Dreamfort <quickfort-blueprint-guide>` blueprint set improvements: automatically create tavern, library, and temple locations (restricted to residents only by default), automatically associate the rented rooms with the tavern
- `quickfort`: `Dreamfort <quickfort-blueprint-guide>` blueprint set improvements: new design for the services level, including a werebeast-proof hospital recovery rooms and an appropriately-themed interrogation room next to the jail! Also fits better in a 1x1 embark for minimalist players.
## Documentation ## Documentation
- ``dfhack.gui.revealInDwarfmodeMap``: document ``center`` bool for lua API - ``dfhack.gui.revealInDwarfmodeMap``: document ``center`` bool for lua API
@ -50,11 +60,14 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## API ## API
- add functions reverse-engineered from announcement code: ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter`` - add functions reverse-engineered from announcement code: ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``
- ``Gui::revealInDwarfmodeMap``: Now enforce valid view bounds when pos invalid, add variant accepting x, y, z - ``Gui::revealInDwarfmodeMap``: Now enforce valid view bounds when pos invalid, add variant accepting x, y, z
- ``word_wrap``: argument ``bool collapse_whitespace`` converted to enum ``word_wrap_whitespace_mode mode``, with valid modes ``WSMODE_KEEP_ALL``, ``WSMODE_COLLAPSE_ALL``, and ``WSMODE_TRIM_LEADING``.
## Lua ## Lua
- ``gui.View``: all ``View`` subclasses (including all ``Widgets``) can now acquire keyboard focus with the new ``View:setFocus()`` function. See docs for details.
- ``widgets.HotkeyLabel``: the ``key_sep`` string is now configurable - ``widgets.HotkeyLabel``: the ``key_sep`` string is now configurable
- ``widgets.EditField``: the ``key_sep`` string is now configurable - ``widgets.EditField``: the ``key_sep`` string is now configurable
- ``widgets.EditField``: can now display an optional string label in addition to the activation key - ``widgets.EditField``: can now display an optional string label in addition to the activation key
- ``widgets.EditField``: views that have an ``EditField`` subview no longer need to manually manage the ``EditField`` activation state and input routing. This is now handled automatically by the new ``gui.View`` keyboard focus subsystem.
# 0.47.05-r5 # 0.47.05-r5

@ -521,6 +521,9 @@ give10right
give move give move
togglesequence togglesequence
togglesequence2 togglesequence2
forbidsearch search
permitsearch search
togglesearch search
masterworkonly prefix masterworkonly prefix
artifactonly prefix artifactonly prefix
togglemasterwork prefix togglemasterwork prefix
@ -588,6 +591,12 @@ four adjacent items::
dye: {foodprefix}b{Right}{Down 11}{Right}{Down 28}{togglesequence 4}^ dye: {foodprefix}b{Right}{Down 11}{Right}{Down 28}{togglesequence 4}^
``forbidsearch``, ``permitsearch``, and ``togglesearch`` use the DFHack
`search-plugin` plugin to forbid or permit a filtered list, or toggle the first
(or only) item in the list. Specify the search string in the ``search``
sub-alias. Be sure to move the cursor over to the right column before invoking
these aliases. The search filter will be cleared before this alias completes.
Finally, the ``masterwork`` and ``artifact`` group of aliases configure the Finally, the ``masterwork`` and ``artifact`` group of aliases configure the
corresponding allowable core quality for the stockpile categories that have corresponding allowable core quality for the stockpile categories that have
them. This alias is used to implement category-specific aliases below, like them. This alias is used to implement category-specific aliases below, like
@ -724,13 +733,16 @@ shells forbidshells permitshells
teeth forbidteeth permitteeth teeth forbidteeth permitteeth
horns forbidhorns permithorns horns forbidhorns permithorns
hair forbidhair permithair hair forbidhair permithair
usablehair forbidusablehair permitusablehair
craftrefuse forbidcraftrefuse permitcraftrefuse craftrefuse forbidcraftrefuse permitcraftrefuse
=========== ================== ================== =========== ================== ==================
Notes: Notes:
* ``craftrefuse`` includes everything a craftsdwarf can use: skulls, bones, * ``usablehair`` Only hair and wool that can make usable clothing is included,
shells, teeth, horns, and hair. i.e. from sheep, llamas, alpacas, and trolls.
* ``craftrefuse`` includes everything a craftsdwarf or tailor can use: skulls,
bones, shells, teeth, horns, and "usable" hair/wool (defined above).
Stone stockpile adjustments Stone stockpile adjustments
``````````````````````````` ```````````````````````````
@ -802,7 +814,8 @@ Finished goods stockpile adjustments
======================= ============================= ============================= ======================= ============================= =============================
Exclusive Forbid Permit Exclusive Forbid Permit
======================= ============================= ============================= ======================= ============================= =============================
jugs stonetools
woodentools
crafts forbidcrafts permitcrafts crafts forbidcrafts permitcrafts
goblets forbidgoblets permitgoblets goblets forbidgoblets permitgoblets
masterworkfinishedgoods forbidmasterworkfinishedgoods permitmasterworkfinishedgoods masterworkfinishedgoods forbidmasterworkfinishedgoods permitmasterworkfinishedgoods
@ -812,17 +825,18 @@ artifactfinishedgoods forbidartifactfinishedgoods permitartifactfinishedgo
Cloth stockpile adjustments Cloth stockpile adjustments
``````````````````````````` ```````````````````````````
+------------------+ ================ ====================== ======================
| Exclusive | Exclusive Forbid Permit
+==================+ ================ ====================== ======================
| thread | thread forbidthread permitthread
+------------------+ adamantinethread forbidadamantinethread permitadamantinethread
| adamantinethread | cloth forbidcloth permitcloth
+------------------+ adamantinecloth forbidadamantinecloth permitadamantinecloth
| cloth | ================ ====================== ======================
+------------------+
| adamantinecloth | Notes:
+------------------+
* ``thread`` and ``cloth`` refers to all materials that are not adamantine.
Weapon stockpile adjustments Weapon stockpile adjustments
```````````````````````````` ````````````````````````````

@ -1418,15 +1418,10 @@ Tips and tricks
Caveats and limitations Caveats and limitations
----------------------- -----------------------
- If you use the ``jugs`` alias in your ``#query``-mode blueprints, be aware - If you use the the ``bags`` alias, be aware that the game does not
that there is no way to differentiate jugs from other types of tools in the differentiate between empty and full bags. Therefore, you can get bags of
game. Therefore, ``jugs`` stockpiles will also take nest boxes, scroll gypsum power in your "bags" stockpile unless you are careful to assign all
rollers, and other tools. The only workaround is not to have other tools your gypsum to your hospital.
lying around in your fort.
- Likewise for the ``bags`` alias. The game does not differentiate between
empty and full bags, so you'll get bags of gypsum power in your "bags"
stockpile unless you are careful to assign all your gypsum to your hospital.
- Weapon traps and upright spear/spike traps can currently only be built with a - Weapon traps and upright spear/spike traps can currently only be built with a
single weapon. single weapon.

@ -168,15 +168,15 @@ std::string to_search_normalized(const std::string &str)
return result; return result;
} }
bool word_wrap(std::vector<std::string> *out, const std::string &str, size_t line_length,
bool word_wrap(std::vector<std::string> *out, const std::string &str, word_wrap_whitespace_mode mode)
size_t line_length, bool collapse_whitespace)
{ {
if (line_length == 0) if (line_length == 0)
line_length = SIZE_MAX; line_length = SIZE_MAX;
std::string line; std::string line;
size_t break_pos = 0; size_t break_pos = 0;
bool ignore_whitespace = false;
for (auto &c : str) for (auto &c : str)
{ {
@ -185,19 +185,22 @@ bool word_wrap(std::vector<std::string> *out, const std::string &str,
out->push_back(line); out->push_back(line);
line.clear(); line.clear();
break_pos = 0; break_pos = 0;
ignore_whitespace = (mode == WSMODE_TRIM_LEADING);
continue; continue;
} }
if (isspace(c)) if (isspace(c))
{ {
if (break_pos == line.length() && collapse_whitespace) if (ignore_whitespace || (mode == WSMODE_COLLAPSE_ALL && break_pos == line.length()))
continue; continue;
line.push_back(collapse_whitespace ? ' ' : c); line.push_back((mode == WSMODE_COLLAPSE_ALL) ? ' ' : c);
break_pos = line.length(); break_pos = line.length();
} }
else { else
{
line.push_back(c); line.push_back(c);
ignore_whitespace = false;
} }
if (line.length() > line_length) if (line.length() > line_length)
@ -215,6 +218,7 @@ bool word_wrap(std::vector<std::string> *out, const std::string &str,
} }
line = line.substr(break_pos); line = line.substr(break_pos);
break_pos = 0; break_pos = 0;
ignore_whitespace = (mode == WSMODE_TRIM_LEADING);
} }
} }
if (line.length()) if (line.length())

@ -389,10 +389,16 @@ DFHACK_EXPORT std::string toUpper(const std::string &str);
DFHACK_EXPORT std::string toLower(const std::string &str); DFHACK_EXPORT std::string toLower(const std::string &str);
DFHACK_EXPORT std::string to_search_normalized(const std::string &str); DFHACK_EXPORT std::string to_search_normalized(const std::string &str);
enum word_wrap_whitespace_mode {
WSMODE_KEEP_ALL,
WSMODE_COLLAPSE_ALL,
WSMODE_TRIM_LEADING
};
DFHACK_EXPORT bool word_wrap(std::vector<std::string> *out, DFHACK_EXPORT bool word_wrap(std::vector<std::string> *out,
const std::string &str, const std::string &str,
size_t line_length = 80, size_t line_length = 80,
bool collapse_whitespace = false); word_wrap_whitespace_mode mode = WSMODE_KEEP_ALL);
inline bool bits_match(unsigned required, unsigned ok, unsigned mask) inline bool bits_match(unsigned required, unsigned ok, unsigned mask)
{ {

@ -376,10 +376,21 @@ View.ATTRS {
active = true, active = true,
visible = true, visible = true,
view_id = DEFAULT_NIL, view_id = DEFAULT_NIL,
on_focus = DEFAULT_NIL,
on_unfocus = DEFAULT_NIL,
} }
function View:init(args) function View:init(args)
self.subviews = {} self.subviews = {}
self.focus_group = {self}
self.focus = false
end
local function inherit_focus_group(view, focus_group)
for _,child in ipairs(view.subviews) do
inherit_focus_group(child, focus_group)
end
view.focus_group = focus_group
end end
function View:addviews(list) function View:addviews(list)
@ -388,6 +399,31 @@ function View:addviews(list)
local sv = self.subviews local sv = self.subviews
for _,obj in ipairs(list) do for _,obj in ipairs(list) do
-- absorb the focus groups of new children
for _,focus_obj in ipairs(obj.focus_group) do
table.insert(self.focus_group, focus_obj)
end
-- if the child's focus group has a focus owner, absorb it if we don't
-- already have one. otherwise keep the focus owner we have and clear
-- the focus of the child.
if obj.focus_group.cur then
if not self.focus_group.cur then
self.focus_group.cur = obj.focus_group.cur
else
obj.focus_group.cur:setFocus(false)
end
end
-- overwrite the child's focus_group hierarchy with ours
inherit_focus_group(obj, self.focus_group)
-- if we don't have a focus owner, give it to the new child if they want
if not self.focus_group.cur and obj:getPreferredFocusState() then
obj:setFocus(true)
end
-- set ourselves as the parent view of the new child
obj.parent_view = self
-- add the subview to our child list
table.insert(sv, obj) table.insert(sv, obj)
local id = obj.view_id local id = obj.view_id
@ -405,6 +441,33 @@ function View:addviews(list)
end end
end end
-- should be overridden by widgets that care about capturing keyboard focus
-- (e.g. widgets.EditField)
function View:getPreferredFocusState()
return false
end
function View:setFocus(focus)
if focus then
if self.focus then return end -- nothing to do if we already have focus
if self.focus_group.cur then
-- steal focus from current owner
self.focus_group.cur:setFocus(false)
end
self.focus_group.cur = self
self.focus = true
if self.on_focus then
self.on_focus()
end
elseif self.focus then
self.focus = false
self.focus_group.cur = nil
if self.on_unfocus then
self.on_unfocus()
end
end
end
function View:getWindowSize() function View:getWindowSize()
local rect = self.frame_body local rect = self.frame_body
return rect.width, rect.height return rect.width, rect.height
@ -476,12 +539,45 @@ end
function View:onRenderBody(dc) function View:onRenderBody(dc)
end end
-- Returns whether we should invoke the focus owner's onInput() function from
-- the given view's inputToSubviews() function. That is, returns true if:
-- - the view is not itself the focus owner since that would be an infinite loop
-- - the view is not a descendent of the focus owner (same as above)
-- - the focus owner and all of its ancestors are visible and active, since if
-- the focus owner is not (directly or transitively) visible or active, then
-- it shouldn't be getting input.
local function should_send_input_to_focus_owner(view, focus_owner)
local iter = view
while iter do
if iter == focus_owner then
return false
end
iter = iter.parent_view
end
iter = focus_owner
while iter do
if not iter.visible or not iter.active then
return false
end
iter = iter.parent_view
end
return true
end
function View:inputToSubviews(keys) function View:inputToSubviews(keys)
local children = self.subviews local children = self.subviews
-- give focus owner first dibs on the input
local focus_owner = self.focus_group.cur
if focus_owner and should_send_input_to_focus_owner(self, focus_owner) and
focus_owner:onInput(keys) then
return true
end
for i=#children,1,-1 do for i=#children,1,-1 do
local child = children[i] local child = children[i]
if child.visible and child.active and child:onInput(keys) then if child.visible and child.active and child ~= focus_owner and
child:onInput(keys) then
return true return true
end end
end end

@ -186,13 +186,25 @@ EditField.ATTRS{
on_submit = DEFAULT_NIL, on_submit = DEFAULT_NIL,
key = DEFAULT_NIL, key = DEFAULT_NIL,
key_sep = DEFAULT_NIL, key_sep = DEFAULT_NIL,
frame = {h=1},
modal = false,
} }
function EditField:init() function EditField:init()
local function on_activate()
self.saved_text = self.text
self:setFocus(true)
end
self:addviews{HotkeyLabel{frame={t=0,l=0}, self:addviews{HotkeyLabel{frame={t=0,l=0},
key=self.key, key=self.key,
key_sep=self.key_sep, key_sep=self.key_sep,
label=self.label_text}} label=self.label_text,
on_activate=self.key and on_activate or nil}}
end
function EditField:getPreferredFocusState()
return not self.key
end end
function EditField:postUpdateLayout() function EditField:postUpdateLayout()
@ -203,7 +215,7 @@ function EditField:onRenderBody(dc)
dc:pen(self.text_pen or COLOR_LIGHTCYAN):fill(0,0,dc.width-1,0) dc:pen(self.text_pen or COLOR_LIGHTCYAN):fill(0,0,dc.width-1,0)
local cursor = '_' local cursor = '_'
if not self.active or gui.blink_visible(300) then if not self.active or not self.focus or gui.blink_visible(300) then
cursor = ' ' cursor = ' '
end end
local txt = self.text .. cursor local txt = self.text .. cursor
@ -215,12 +227,36 @@ function EditField:onRenderBody(dc)
end end
function EditField:onInput(keys) function EditField:onInput(keys)
if self.on_submit and keys.SELECT then if not self.focus then
self.on_submit(self.text) -- only react to our hotkey
return self:inputToSubviews(keys)
end
if self.key and keys.LEAVESCREEN then
local old = self.text
self.text = self.saved_text
if self.on_change and old ~= self.saved_text then
self.on_change(self.text, old)
end
self:setFocus(false)
return true return true
elseif keys._STRING then end
if keys.SELECT then
if self.key then
self:setFocus(false)
end
if self.on_submit then
self.on_submit(self.text)
return true
end
return not not self.key
end
if keys._STRING then
local old = self.text local old = self.text
if keys._STRING == 0 then if keys._STRING == 0 then
-- handle backspace
self.text = string.sub(old, 1, #old-1) self.text = string.sub(old, 1, #old-1)
else else
local cv = string.char(keys._STRING) local cv = string.char(keys._STRING)
@ -233,6 +269,9 @@ function EditField:onInput(keys)
end end
return true return true
end end
-- if we're modal, then unconditionally eat all the input
return self.modal
end end
----------- -----------
@ -957,7 +996,6 @@ function FilteredList:init(info)
on_change = self:callback('onFilterChange'), on_change = self:callback('onFilterChange'),
on_char = self:callback('onFilterChar'), on_char = self:callback('onFilterChar'),
key = self.edit_key, key = self.edit_key,
active = (self.edit_key == nil),
} }
self.list = List{ self.list = List{
frame = { t = 2 }, frame = { t = 2 },
@ -1002,19 +1040,6 @@ function FilteredList:init(info)
end end
end end
function FilteredList:onInput(keys)
if self.edit_key and keys[self.edit_key] and not self.edit.active then
self.edit.active = true
return true
elseif keys.LEAVESCREEN and self.edit.active then
self.edit.active = false
return true
else
return self:inputToSubviews(keys)
end
end
function FilteredList:getChoices() function FilteredList:getChoices()
return self.choices return self.choices
end end

@ -824,162 +824,160 @@ misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution. 3. This notice may not be removed or altered from any source distribution.
*/ */
namespace { //----------------------------------------------------------------------------//
//----------------------------------------------------------------------------// // Utility function
// Utility function //
// //----------------------------------------------------------------------------//
//----------------------------------------------------------------------------// static std::pair<bool, bool> check_tropicality(df::region_map_entry& region,
std::pair<bool, bool> check_tropicality(df::region_map_entry& region, int y_pos
int y_pos )
) {
int flip_latitude = df::global::world->world_data->flip_latitude;
bool is_possible_tropical_area_by_latitude = false;
bool is_tropical_area_by_latitude = false;
if (flip_latitude == -1) // NO POLES
{ {
int flip_latitude = df::global::world->world_data->flip_latitude; // If there're no poles, tropical area is determined by temperature
is_possible_tropical_area_by_latitude = region.temperature >= 75;
is_tropical_area_by_latitude = region.temperature >= 85;
}
bool is_possible_tropical_area_by_latitude = false; else
bool is_tropical_area_by_latitude = false; {
int v6 = 0;
df::world_data* wdata = df::global::world->world_data;
if (flip_latitude == -1) // NO POLES if (flip_latitude == 0) // NORTH POLE ONLY
{ {
// If there're no poles, tropical area is determined by temperature v6 = y_pos;
is_possible_tropical_area_by_latitude = region.temperature >= 75;
is_tropical_area_by_latitude = region.temperature >= 85;
} }
else else if (flip_latitude == 1) // SOUTH_POLE ONLY
{ {
int v6 = 0; v6 = df::global::world->world_data->world_height - y_pos - 1;
}
df::world_data* wdata = df::global::world->world_data;
if (flip_latitude == 0) // NORTH POLE ONLY
{
v6 = y_pos;
}
else if (flip_latitude == 1) // SOUTH_POLE ONLY
{
v6 = df::global::world->world_data->world_height - y_pos - 1;
}
else if (flip_latitude == 2) // BOTH POLES else if (flip_latitude == 2) // BOTH POLES
{
if (y_pos < wdata->world_height / 2)
v6 = 2 * y_pos;
else
{ {
if (y_pos < wdata->world_height / 2) v6 = wdata->world_height + 2 * (wdata->world_height / 2 - y_pos) - 1;
v6 = 2 * y_pos; if (v6 < 0)
else v6 = 0;
{ if (v6 >= wdata->world_height)
v6 = wdata->world_height + 2 * (wdata->world_height / 2 - y_pos) - 1; v6 = wdata->world_height - 1;
if (v6 < 0)
v6 = 0;
if (v6 >= wdata->world_height)
v6 = wdata->world_height - 1;
}
} }
if (wdata->world_height == 17)
v6 *= 16;
else if (wdata->world_height == 33)
v6 *= 8;
else if (wdata->world_height == 65)
v6 *= 4;
else if (wdata->world_height == 129)
v6 *= 2;
is_possible_tropical_area_by_latitude = v6 > 170;
is_tropical_area_by_latitude = v6 >= 200;
} }
return std::pair<bool, bool>(is_possible_tropical_area_by_latitude, if (wdata->world_height == 17)
is_tropical_area_by_latitude v6 *= 16;
); else if (wdata->world_height == 33)
v6 *= 8;
else if (wdata->world_height == 65)
v6 *= 4;
else if (wdata->world_height == 129)
v6 *= 2;
is_possible_tropical_area_by_latitude = v6 > 170;
is_tropical_area_by_latitude = v6 >= 200;
} }
return std::pair<bool, bool>(is_possible_tropical_area_by_latitude,
is_tropical_area_by_latitude
);
}
//----------------------------------------------------------------------------// //----------------------------------------------------------------------------//
// Utility function // Utility function
// //
// return some unknow parameter as a percentage // return some unknow parameter as a percentage
//----------------------------------------------------------------------------// //----------------------------------------------------------------------------//
int get_region_parameter(int y, static int get_region_parameter(int y,
int x int x
) )
{
int world_height = df::global::world->world_data->world_height;
if (world_height > 65) // Medium and large worlds
{ {
int world_height = df::global::world->world_data->world_height; // access to region 2D array
if (world_height > 65) // Medium and large worlds df::region_map_entry& region = df::global::world->world_data->region_map[x][y];
{ int flip_latitude = df::global::world->world_data->flip_latitude;
// access to region 2D array int rainfall = region.rainfall;
df::region_map_entry& region = df::global::world->world_data->region_map[x][y]; int result;
int flip_latitude = df::global::world->world_data->flip_latitude; int y_pos = y;
int rainfall = region.rainfall; int ypos = y_pos;
int result;
int y_pos = y;
int ypos = y_pos;
if (flip_latitude == -1) // NO POLES
return 100;
else if (flip_latitude == 1) // SOUTH POLE
ypos = world_height - y_pos - 1;
else if (flip_latitude == 2) // NORTH & SOUTH POLE
{
if (ypos < world_height / 2)
ypos *= 2;
else
{
ypos = world_height + 2 * (world_height / 2 - ypos) - 1;
if (ypos < 0)
ypos = 0;
if (ypos >= world_height)
ypos = world_height - 1;
}
}
int latitude; // 0 - 256 (size of a large world) if (flip_latitude == -1) // NO POLES
switch (world_height) return 100;
else if (flip_latitude == 1) // SOUTH POLE
ypos = world_height - y_pos - 1;
else if (flip_latitude == 2) // NORTH & SOUTH POLE
{
if (ypos < world_height / 2)
ypos *= 2;
else
{ {
case 17: // Pocket world ypos = world_height + 2 * (world_height / 2 - ypos) - 1;
latitude = 16 * ypos; if (ypos < 0)
break; ypos = 0;
case 33: // Smaller world if (ypos >= world_height)
latitude = 8 * ypos; ypos = world_height - 1;
break;
case 65: // Small world
latitude = 4 * ypos;
break;
case 129: // Medium world
latitude = 2 * ypos;
break;
default: // Large world
latitude = ypos;
break;
} }
}
// latitude > 220 int latitude; // 0 - 256 (size of a large world)
if ((latitude - 171) > 49) switch (world_height)
return 100; {
case 17: // Pocket world
latitude = 16 * ypos;
break;
case 33: // Smaller world
latitude = 8 * ypos;
break;
case 65: // Small world
latitude = 4 * ypos;
break;
case 129: // Medium world
latitude = 2 * ypos;
break;
default: // Large world
latitude = ypos;
break;
}
// latitude > 220
if ((latitude - 171) > 49)
return 100;
// Latitude between 191 and 200
if ((latitude > 190) && (latitude < 201))
return 0;
// Latitude between 201 and 220 // Latitude between 191 and 200
if ((latitude > 190) && (latitude >= 201)) if ((latitude > 190) && (latitude < 201))
result = rainfall + 16 * (latitude - 207); return 0;
else
// Latitude between 0 and 190
result = (16 * (184 - latitude) - rainfall);
if (result < 0) // Latitude between 201 and 220
return 0; if ((latitude > 190) && (latitude >= 201))
result = rainfall + 16 * (latitude - 207);
else
// Latitude between 0 and 190
result = (16 * (184 - latitude) - rainfall);
if (result > 100) if (result < 0)
return 100; return 0;
return result; if (result > 100)
} return 100;
return 100; return result;
} }
return 100;
} }

@ -5,6 +5,7 @@
#include "Console.h" #include "Console.h"
#include "Core.h" #include "Core.h"
#include "DataDefs.h" #include "DataDefs.h"
#include "Debug.h"
#include "Error.h" #include "Error.h"
#include "Export.h" #include "Export.h"
#include "LuaTools.h" #include "LuaTools.h"
@ -17,6 +18,7 @@
#include "df/building_tradedepotst.h" #include "df/building_tradedepotst.h"
#include "df/general_ref.h" #include "df/general_ref.h"
#include "df/general_ref_contained_in_itemst.h" #include "df/general_ref_contained_in_itemst.h"
#include "df/interfacest.h"
#include "df/viewscreen_dwarfmodest.h" #include "df/viewscreen_dwarfmodest.h"
#include "df/viewscreen_jobmanagementst.h" #include "df/viewscreen_jobmanagementst.h"
#include "df/viewscreen_justicest.h" #include "df/viewscreen_justicest.h"
@ -44,6 +46,15 @@ static map<string, conf_wrapper*> confirmations;
string active_id; string active_id;
queue<string> cmds; queue<string> cmds;
// true when confirm is paused
bool paused = false;
// if set, confirm will unpause when this screen is no longer on the stack
df::viewscreen *paused_screen = NULL;
namespace DFHack {
DBG_DECLARE(confirm,status);
}
template <typename VT, typename FT> template <typename VT, typename FT>
inline bool in_vector (std::vector<VT> &vec, FT item) inline bool in_vector (std::vector<VT> &vec, FT item)
{ {
@ -229,6 +240,18 @@ namespace conf_lua {
lua_pushnil(L); lua_pushnil(L);
return 1; return 1;
} }
int unpause(lua_State *)
{
DEBUG(status).print("unpausing\n");
paused = false;
paused_screen = NULL;
return 0;
}
int get_paused (lua_State *L)
{
Lua::Push(L, paused);
return 1;
}
} }
} }
@ -247,6 +270,8 @@ DFHACK_PLUGIN_LUA_COMMANDS {
CONF_LUA_CMD(get_ids), CONF_LUA_CMD(get_ids),
CONF_LUA_CMD(get_conf_data), CONF_LUA_CMD(get_conf_data),
CONF_LUA_CMD(get_active_id), CONF_LUA_CMD(get_active_id),
CONF_LUA_CMD(unpause),
CONF_LUA_CMD(get_paused),
DFHACK_LUA_END DFHACK_LUA_END
}; };
@ -281,7 +306,15 @@ public:
return true; return true;
} }
bool feed (ikey_set *input) { bool feed (ikey_set *input) {
if (state == INACTIVE) if (paused)
{
// we can only detect that we've left the screen by intercepting the
// ESC key
if (!paused_screen && input->count(df::interface_key::LEAVESCREEN))
conf_lua::api::unpause(NULL);
return false;
}
else if (state == INACTIVE)
{ {
for (df::interface_key key : *input) for (df::interface_key key : *input)
{ {
@ -302,6 +335,18 @@ public:
set_state(INACTIVE); set_state(INACTIVE);
else if (input->count(df::interface_key::SELECT)) else if (input->count(df::interface_key::SELECT))
set_state(SELECTED); set_state(SELECTED);
else if (input->count(df::interface_key::CUSTOM_P))
{
DEBUG(status).print("pausing\n");
paused = true;
// only record the screen when we're not at the top viewscreen
// since this screen will *always* be on the stack. for
// dwarfmode screens, use ESC detection to discover when to
// unpause
if (!df::viewscreen_dwarfmodest::_identity.is_instance(screen))
paused_screen = screen;
set_state(INACTIVE);
}
else if (input->count(df::interface_key::CUSTOM_S)) else if (input->count(df::interface_key::CUSTOM_S))
show_options(); show_options();
return true; return true;
@ -316,6 +361,8 @@ public:
} }
void render() { void render() {
static vector<string> lines; static vector<string> lines;
static const std::string pause_message =
"Pause confirmations until you exit this screen";
Screen::Pen corner_ul = Screen::Pen((char)201, COLOR_GREY, COLOR_BLACK); Screen::Pen corner_ul = Screen::Pen((char)201, COLOR_GREY, COLOR_BLACK);
Screen::Pen corner_ur = Screen::Pen((char)187, COLOR_GREY, COLOR_BLACK); Screen::Pen corner_ur = Screen::Pen((char)187, COLOR_GREY, COLOR_BLACK);
Screen::Pen corner_dl = Screen::Pen((char)200, COLOR_GREY, COLOR_BLACK); Screen::Pen corner_dl = Screen::Pen((char)200, COLOR_GREY, COLOR_BLACK);
@ -329,7 +376,9 @@ public:
for (string line : lines) for (string line : lines)
max_length = std::max(max_length, line.size()); max_length = std::max(max_length, line.size());
int width = max_length + 4; int width = max_length + 4;
int height = lines.size() + 4; vector<string> pause_message_lines;
word_wrap(&pause_message_lines, pause_message, max_length - 3);
int height = lines.size() + pause_message_lines.size() + 5;
int x1 = (gps->dimx / 2) - (width / 2); int x1 = (gps->dimx / 2) - (width / 2);
int x2 = x1 + width - 1; int x2 = x1 + width - 1;
int y1 = (gps->dimy / 2) - (height / 2); int y1 = (gps->dimy / 2) - (height / 2);
@ -368,6 +417,14 @@ public:
{ {
Screen::paintString(Screen::Pen(' ', get_color(), COLOR_BLACK), x1 + 2, y1 + 2 + i, lines[i]); Screen::paintString(Screen::Pen(' ', get_color(), COLOR_BLACK), x1 + 2, y1 + 2 + i, lines[i]);
} }
y = y1 + 3 + lines.size();
for (size_t i = 0; i < pause_message_lines.size(); i++)
{
Screen::paintString(Screen::Pen(' ', COLOR_WHITE, COLOR_BLACK), x1 + 5, y + i, pause_message_lines[i]);
}
x = x1 + 2;
OutputString(COLOR_LIGHTRED, x, y, Screen::getKeyDisplay(df::interface_key::CUSTOM_P));
OutputString(COLOR_WHITE, x, y, ":");
} }
else if (state == SELECTED) else if (state == SELECTED)
{ {
@ -533,6 +590,22 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out)
return CR_OK; return CR_OK;
} }
static bool screen_found(df::viewscreen *target_screen)
{
if (!df::global::gview)
return false;
df::viewscreen *screen = &df::global::gview->view;
while (screen)
{
if (screen == target_screen)
return true;
screen = screen->child;
}
return false;
}
DFhackCExport command_result plugin_onupdate (color_ostream &out) DFhackCExport command_result plugin_onupdate (color_ostream &out)
{ {
while (!cmds.empty()) while (!cmds.empty())
@ -540,6 +613,11 @@ DFhackCExport command_result plugin_onupdate (color_ostream &out)
Core::getInstance().runCommand(out, cmds.front()); Core::getInstance().runCommand(out, cmds.front());
cmds.pop(); cmds.pop();
} }
// if the screen that we paused on is no longer on the stack, unpause
if (paused_screen && !screen_found(paused_screen))
conf_lua::api::unpause(NULL);
return CR_OK; return CR_OK;
} }

@ -17,3 +17,179 @@ function test.clear_pen()
tile_color = false, tile_color = false,
}) })
end end
WantsFocusView = defclass(WantsFocusView, gui.View)
function WantsFocusView:getPreferredFocusState()
return true
end
function test.view_wants_focus()
local parent = gui.View()
expect.false_(parent.focus)
-- expect first (regular) child to not get focus
local regular_child = gui.View()
expect.false_(regular_child.focus)
expect.ne(parent.focus_group, regular_child.focus_group)
parent:addviews{regular_child}
expect.false_(regular_child.focus)
expect.eq(parent.focus_group, regular_child.focus_group)
-- the first child who wants focus gets it
local focus_child = WantsFocusView()
expect.false_(focus_child.focus)
parent:addviews{focus_child}
expect.true_(focus_child.focus)
expect.eq(parent.focus_group.cur, focus_child)
-- the second child who wants focus doesn't
local focus_child2 = WantsFocusView()
parent:addviews{focus_child2}
expect.false_(focus_child2.focus)
expect.eq(parent.focus_group.cur, focus_child)
end
function test.inherit_focus_from_subview()
local parent = gui.View()
local regular_child = gui.View()
local focus_child = WantsFocusView()
regular_child:addviews{focus_child}
expect.true_(focus_child.focus)
parent:addviews{regular_child}
expect.eq(parent.focus_group.cur, focus_child)
end
function test.subviews_negotiate_focus()
local parent = gui.View()
local regular_child = gui.View()
local regular_child2 = gui.View()
local focus_child = WantsFocusView()
local focus_child2 = WantsFocusView()
regular_child:addviews{focus_child}
regular_child2:addviews{focus_child2}
expect.true_(focus_child.focus)
expect.true_(focus_child2.focus)
expect.ne(regular_child.focus_group, regular_child2.focus_group)
parent:addviews{regular_child}
expect.eq(parent.focus_group.cur, focus_child)
expect.true_(focus_child.focus)
expect.true_(focus_child2.focus)
parent:addviews{regular_child2}
expect.eq(parent.focus_group.cur, focus_child)
expect.eq(regular_child.focus_group, regular_child2.focus_group)
expect.true_(focus_child.focus)
expect.false_(focus_child2.focus)
end
MockInputView = defclass(MockInputView, gui.View)
function MockInputView:onInput(keys)
self.mock(keys)
MockInputView.super.onInput(self, keys)
return true
end
local function reset_child_mocks(parent)
for _,child in ipairs(parent.subviews) do
child.mock = mock.func()
reset_child_mocks(child)
end
end
-- verify that input got routed as expected
local function test_children(expected, parent)
local children = parent.subviews
for i,val in ipairs(expected) do
expect.eq(val, children[i].mock.call_count, 'child '..i)
end
end
function test.keyboard_follows_focus()
local parent = gui.View()
local regular_child = MockInputView{}
local regular_child2 = MockInputView{}
local last_child = MockInputView{}
parent:addviews{regular_child, regular_child2, last_child}
reset_child_mocks(parent)
parent:onInput({'a'})
test_children({0,0,1}, parent)
regular_child:setFocus(true)
reset_child_mocks(parent)
parent:onInput({'a'})
test_children({1,0,0}, parent)
regular_child2:setFocus(true)
reset_child_mocks(parent)
parent:onInput({'a'})
test_children({0,1,0}, parent)
regular_child2:setFocus(false)
reset_child_mocks(parent)
parent:onInput({'a'})
test_children({0,0,1}, parent)
end
function test.one_callback_on_double_focus()
local on_focus = mock.func()
local view = gui.View{on_focus=on_focus}
expect.eq(0, on_focus.call_count)
view:setFocus(true)
expect.eq(1, on_focus.call_count)
view:setFocus(true)
expect.eq(1, on_focus.call_count)
end
function test.one_callback_on_double_unfocus()
local on_unfocus = mock.func()
local view = gui.View{on_unfocus=on_unfocus}
expect.eq(0, on_unfocus.call_count)
view:setFocus(false)
expect.eq(0, on_unfocus.call_count)
view:setFocus(true)
expect.eq(0, on_unfocus.call_count)
view:setFocus(false)
expect.eq(1, on_unfocus.call_count)
view:setFocus(false)
expect.eq(1, on_unfocus.call_count)
end
function test.no_input_when_focus_owner_is_hidden()
local parent = gui.View()
local child1 = MockInputView()
local child2 = MockInputView()
parent:addviews{child1, child2}
child1:setFocus(true)
child1.visible = false
reset_child_mocks(parent)
parent:onInput({'a'})
test_children({0,1}, parent)
end
function test.no_input_when_ancestor_is_hidden()
local grandparent = gui.View()
local parent = MockInputView()
local child1 = MockInputView()
local child2 = MockInputView()
grandparent:addviews{parent}
parent:addviews{child1, child2}
child1:setFocus(true)
parent.visible = false
reset_child_mocks(grandparent)
grandparent:onInput({'a'})
test_children({0}, grandparent)
test_children({0,0}, parent)
end
function test.no_input_loop_in_children_of_focus_owner()
local grandparent = gui.View()
local parent = MockInputView()
local child = MockInputView()
grandparent:addviews{parent}
parent:addviews{child}
parent:setFocus(true)
reset_child_mocks(grandparent)
child:onInput({'a'})
test_children({0}, grandparent)
test_children({1}, parent)
end

@ -10,9 +10,11 @@
-- height (required) -- height (required)
-- depth (default is 1) -- depth (default is 1)
-- start (cursor offset for input blueprints, default is 1,1) -- start (cursor offset for input blueprints, default is 1,1)
-- extra_fn (the name of a global function in this file to run after applying
-- all blueprints but before comparing results)
-- --
-- depends on blueprint, buildingplan, and dig-now plugins (as well as the -- depends on blueprint, buildingplan, and dig-now plugins (as well as the
-- quickfort script, of course) -- quickfort script and anything else run in the extra_fns, of course)
-- --
-- note that this test harness cannot (yet) test #query blueprints that define -- note that this test harness cannot (yet) test #query blueprints that define
-- rooms since furniture is not actually built during the test. It also cannot -- rooms since furniture is not actually built during the test. It also cannot
@ -24,10 +26,18 @@
config.mode = 'fortress' config.mode = 'fortress'
local argparse = require('argparse') local argparse = require('argparse')
local gui = require('gui')
local guidm = require('gui.dwarfmode')
local utils = require('utils')
local blueprint = require('plugins.blueprint') local blueprint = require('plugins.blueprint')
local confirm = require('plugins.confirm')
local assign_minecarts = reqscript('assign-minecarts')
local quantum = reqscript('gui/quantum')
local quickfort = reqscript('quickfort')
local quickfort_list = reqscript('internal/quickfort/list') local quickfort_list = reqscript('internal/quickfort/list')
local quickfort_command = reqscript('internal/quickfort/command') local quickfort_command = reqscript('internal/quickfort/command')
local utils = require('utils')
local blueprints_dir = 'blueprints/' local blueprints_dir = 'blueprints/'
local input_dir = 'library/test/ecosystem/in/' local input_dir = 'library/test/ecosystem/in/'
@ -86,14 +96,14 @@ local function get_blueprint_sets()
local _,_,file_part = fname:find('/([^/]+)$') local _,_,file_part = fname:find('/([^/]+)$')
local _,_,basename = file_part:find('^([^-.]+)') local _,_,basename = file_part:find('^([^-.]+)')
if not sets[basename] then sets[basename] = {spec={}, phases={}} end if not sets[basename] then sets[basename] = {spec={}, phases={}} end
local golden_path = blueprints_dir..golden_dir..file_part local golden_path = golden_dir..file_part
if not os_exists(golden_path) then if not os_exists(blueprints_dir..golden_path) then
golden_path = blueprints_dir..fname golden_path = fname
end end
sets[basename].phases[phase] = { sets[basename].phases[phase] = {
listnum=listnum, listnum=listnum,
golden_filepath=golden_path, golden_filepath=golden_path,
output_filepath=blueprints_dir..output_dir..file_part} output_filepath=output_dir..file_part}
end end
end end
@ -193,17 +203,17 @@ local function get_cursor_arg(pos, start)
return ('--cursor=%d,%d,%d'):format(pos.x+start.x-1, pos.y+start.y-1, pos.z) return ('--cursor=%d,%d,%d'):format(pos.x+start.x-1, pos.y+start.y-1, pos.z)
end end
local function quickfort_cmd(cmd, listnum, pos, start) local function quickfort_cmd(cmd, listnum_or_path, pos, start)
dfhack.run_script('quickfort', cmd, '-q', listnum, dfhack.run_script('quickfort', cmd, '-q', listnum_or_path,
get_cursor_arg(pos, start)) get_cursor_arg(pos, start))
end end
local function quickfort_run(listnum, pos, start) local function quickfort_run(listnum_or_path, pos, start)
quickfort_cmd('run', listnum, pos, start) quickfort_cmd('run', listnum_or_path, pos, start)
end end
local function quickfort_undo(listnum, pos, start) local function quickfort_undo(listnum_or_path, pos, start)
quickfort_cmd('undo', listnum, pos, start) quickfort_cmd('undo', listnum_or_path, pos, start)
end end
local function designate_area(pos, spec) local function designate_area(pos, spec)
@ -224,10 +234,20 @@ local function run_dig_now(area)
format_pos(area.endpos), '--clean') format_pos(area.endpos), '--clean')
end end
local function run_blueprint(basename, set, pos) local function get_playback_start_arg(start)
blueprint.run(tostring(set.spec.width), tostring(set.spec.height), if not start then return end
tostring(-set.spec.depth), output_dir..basename, return ('--playback-start=%d,%d'):format(start.x, start.y)
get_cursor_arg(pos), '-tphase') end
local function run_blueprint(basename, spec, pos)
local args = {tostring(spec.width), tostring(spec.height),
tostring(-spec.depth), output_dir..basename,
get_cursor_arg(pos), '-tphase'}
local playback_start_arg = get_playback_start_arg(spec.start)
if playback_start_arg then
table.insert(args, playback_start_arg)
end
blueprint.run(table.unpack(args))
end end
local function reset_area(area, spec) local function reset_area(area, spec)
@ -308,13 +328,19 @@ function test.end_to_end()
if phases.zone then do_phase(phases.zone, area, spec) end if phases.zone then do_phase(phases.zone, area, spec) end
if phases.query then do_phase(phases.query, area, spec) end if phases.query then do_phase(phases.query, area, spec) end
-- run any extra commands, if defined by the blueprint spec
if spec.extra_fn then
_ENV[spec.extra_fn](area.pos)
end
-- run blueprint to generate files in output dir -- run blueprint to generate files in output dir
run_blueprint(basename, set, area.pos) run_blueprint(basename, spec, area.pos)
-- quickfort undo blueprints -- quickfort undo blueprints (order shouldn't matter)
for _,phase_name in ipairs(phase_names) do for _,phase_name in ipairs(phase_names) do
if phases[phase_name] then if phases[phase_name] then
quickfort_undo(phases[phase_name].listnum, area.pos, spec.start) quickfort_undo(phases[phase_name].golden_filepath,
area.pos, spec.start)
end end
end end
@ -326,8 +352,8 @@ function test.end_to_end()
for phase,phase_data in pairs(phases) do for phase,phase_data in pairs(phases) do
if phase == 'notes' then goto continue end if phase == 'notes' then goto continue end
print((' verifying phase: %s'):format(phase)) print((' verifying phase: %s'):format(phase))
local golden_filepath = phase_data.golden_filepath local golden_filepath = blueprints_dir..phase_data.golden_filepath
local output_filepath = phase_data.output_filepath local output_filepath = blueprints_dir..phase_data.output_filepath
local input_hash, input_size = md5File(golden_filepath) local input_hash, input_size = md5File(golden_filepath)
local output_hash, output_size = md5File(output_filepath) local output_hash, output_size = md5File(output_filepath)
expect.eq(input_hash, output_hash, expect.eq(input_hash, output_hash,
@ -345,6 +371,7 @@ function test.end_to_end()
input:close() input:close()
output:close() output:close()
expect.table_eq(input_lines, output_lines) expect.table_eq(input_lines, output_lines)
return nil
end end
::continue:: ::continue::
end end
@ -352,3 +379,98 @@ function test.end_to_end()
::continue:: ::continue::
end end
end end
local function send_keys(...)
local keys = {...}
for _,key in ipairs(keys) do
gui.simulateInput(dfhack.gui.getCurViewscreen(true), key)
end
end
function test_gui_quantum(pos)
local vehicles = assign_minecarts.get_free_vehicles()
local confirm_state = confirm.isEnabled()
local confirm_conf = confirm.get_conf_data()
local routes = df.global.ui.hauling.routes
local num_routes = #routes
local next_order_id = df.global.world.manager_order_next_id
return dfhack.with_finalize(
function()
-- unforbid the minecarts we forbade
for _,minecart in ipairs(vehicles) do
local item = df.item.find(minecart.item_id)
if not item then error('could not find item in list') end
item.flags.forbid = false
end
if confirm_state then
dfhack.run_command('enable confirm')
for _,c in pairs(confirm_conf) do
confirm.set_conf_state(c.id, c.enabled)
end
end
end,
function()
-- forbid all available minecarts
for _,minecart in ipairs(vehicles) do
local item = df.item.find(minecart.item_id)
if not item then error('could not find item in list') end
item.flags.forbid = true
end
dfhack.run_script('gui/quantum')
local view = quantum.view
view:onRender()
guidm.setCursorPos(pos)
-- select the feeder stockpile
send_keys('CURSOR_RIGHT', 'CURSOR_RIGHT', 'SELECT')
view:onRender()
-- deselect the feeder stockpile
send_keys('LEAVESCREEN')
view:onRender()
-- reselect the feeder stockpile
send_keys('SELECT')
view:onRender()
-- set a custom name
send_keys('CUSTOM_N')
view:onRender()
view:onInput({_STRING=string.byte('f')})
view:onInput({_STRING=string.byte('o')})
view:onInput({_STRING=string.byte('o')})
send_keys('SELECT')
-- rotate the dump direction to the south
send_keys('CUSTOM_D')
view:onRender()
-- move the cursor to the dump position
send_keys('CURSOR_DOWN', 'CURSOR_DOWN', 'CURSOR_DOWN')
view:onRender()
-- commit and dismiss the dialog
send_keys('SELECT', 'SELECT')
-- verify the created route
expect.eq(num_routes + 1, #routes)
local route = routes[#routes-1]
expect.eq(0, #route.vehicle_ids, 'minecart should not be assigned')
expect.eq(1, #route.stops, 'should have 1 stop')
expect.eq(1, #route.stops[0].stockpiles,
'should have 1 link')
-- verify the created order
expect.eq(next_order_id + 1, df.global.world.manager_order_next_id)
local orders = df.global.world.manager_orders
local order = orders[#orders - 1]
expect.eq(df.job_type.MakeTool, order.job_type)
-- if confirm is enabled, temporarily disable it so we can remove
-- the route and manager order via the ui (easier than walking the
-- structures and carefully deleting all the memory)
if confirm_state then
dfhack.run_command('disable confirm')
end
-- delete last route
quickfort.apply_blueprint{mode='config', data='h--x^'}
-- delete last manager order
quickfort.apply_blueprint{mode='config', data='jm{Up}r^^'}
end)
end