mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-09-27 13:46:00 -04:00
Compare commits
633 Commits
v0.1-pre-a
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
7f92c571bf | ||
|
0b4d5b0ccd | ||
|
a688d660a0 | ||
|
c938b745a2 | ||
|
c5a8a8601a | ||
|
146f5206cf | ||
|
7791bb6cf1 | ||
|
c73a958fe0 | ||
|
966c86a6b5 | ||
|
79a0a6eda9 | ||
|
064839dc91 | ||
|
eb470533c4 | ||
|
ce217c6b93 | ||
|
2f4663c680 | ||
|
e48bc48aab | ||
|
7ce4583fe1 | ||
|
ce7586fa54 | ||
|
49e8e42596 | ||
|
82295fddb0 | ||
|
54645b138f | ||
|
53de5c17ad | ||
|
f880c69399 | ||
|
8cb96f24b4 | ||
|
3d68e9b838 | ||
|
87c121707d | ||
|
198a28505e | ||
|
2cfacf8ee5 | ||
|
a7f34788e8 | ||
|
59114a72ed | ||
|
787d7f531e | ||
|
9fe2069040 | ||
|
51cc23abef | ||
|
f832c2a809 | ||
|
738e1ba7d6 | ||
|
5074ed24ed | ||
|
b155f970df | ||
|
9561086965 | ||
|
ad2cb34c26 | ||
|
91209fd540 | ||
|
1a3fc68d03 | ||
|
66435a264e | ||
|
1a3f3ed91a | ||
|
72fa20eff8 | ||
|
1c0b9a1c9c | ||
|
67eb1cc103 | ||
|
1b8971a52e | ||
|
e5f886e1e1 | ||
|
3a8b8e15a2 | ||
|
c8ef8cf2a4 | ||
|
d946094162 | ||
|
a3cb024b49 | ||
|
8e3620ff45 | ||
|
db89a09f9d | ||
|
435fd1a61b | ||
|
cfcd303252 | ||
|
3fd19f727e | ||
|
5962d2e832 | ||
|
5efe96ebe3 | ||
|
d72b3e3345 | ||
|
01b207b402 | ||
|
2801ae5410 | ||
|
41c1d8e874 | ||
|
73bf269d48 | ||
|
26c6dde74d | ||
|
5b1debd3f0 | ||
|
4558da0136 | ||
|
ded81770df | ||
|
578fa35446 | ||
|
6bcde98d6e | ||
|
07381b347c | ||
|
49ae2308f1 | ||
|
db99c66d25 | ||
|
e9031d97bd | ||
|
468f5682ae | ||
|
6e7e7b9d3f | ||
|
6f41387e30 | ||
|
5dfca5e9f3 | ||
|
a33117fb56 | ||
|
5706a1c19a | ||
|
ca47018e8a | ||
|
4bc4fa0221 | ||
|
2353ef2e70 | ||
|
87d0803a4f | ||
|
ee758b785c | ||
|
194c1e467c | ||
|
5e0e51d5e2 | ||
|
f7d974f2a2 | ||
|
4af41209e7 | ||
|
841a3a34bc | ||
|
ecab467a0f | ||
|
00a55ad4d4 | ||
|
236a091997 | ||
|
6c00b3241f | ||
|
421d4252d9 | ||
|
d43ef2c04e | ||
|
804f4148d3 | ||
|
34bc9cc697 | ||
|
d1c1e8bd26 | ||
|
b18a70cef9 | ||
|
222b916002 | ||
|
f8881ed832 | ||
|
268e65b309 | ||
|
3b26d825d2 | ||
|
bfa9c34ecc | ||
|
4835cefef8 | ||
|
ddd72a3aed | ||
|
05c8b5b294 | ||
|
a900c6245d | ||
|
536233ffee | ||
|
98e13a706a | ||
|
ed28ff1176 | ||
|
54745a215b | ||
|
384db6e75e | ||
|
9cb5f2ef6b | ||
|
97516d5da9 | ||
|
ce692eb829 | ||
|
a7b8f82204 | ||
|
fd9c806928 | ||
|
0f08c722f5 | ||
|
352e78ffba | ||
|
a662bbaeb4 | ||
|
00bd2c52a0 | ||
|
36f356d0c3 | ||
|
e7ea9cacce | ||
|
39ccf33947 | ||
|
7b77011977 | ||
|
6167a7755b | ||
|
4beb20cb68 | ||
|
641fa535eb | ||
|
7ef41b6218 | ||
|
da3fe0ed09 | ||
|
fbd675c076 | ||
|
09763f4cd2 | ||
|
5e29ea8219 | ||
|
fb7279f6b0 | ||
|
98539befe3 | ||
|
2e2d086d71 | ||
|
c603eaafbc | ||
|
5d78275bbc | ||
|
b72017dbd9 | ||
|
9a2d92198e | ||
|
cdfbeb6878 | ||
|
555de49b43 | ||
|
ecb21ff4d4 | ||
|
d82fe04f4b | ||
|
09bea929be | ||
|
d112c87a0c | ||
|
5a92082011 | ||
|
ddec561f9a | ||
|
18e467d5cb | ||
|
cffa34103d | ||
|
fb8d97dba7 | ||
|
f1b625e23e | ||
|
e311d76c41 | ||
|
547b8cd53b | ||
|
12f9efded7 | ||
|
db5efa15b0 | ||
|
b43da8f083 | ||
|
3b41f9e89b | ||
|
4104d9d9ae | ||
|
287fb2bf4d | ||
|
de1c0ebe5d | ||
|
90f13724cf | ||
|
fa4276156c | ||
|
ab14168f50 | ||
|
a96170561f | ||
|
6d88c9eacc | ||
|
23e93886d8 | ||
|
1ffafcb769 | ||
|
50f309dee1 | ||
|
bbd88cef78 | ||
|
203b4fd986 | ||
|
62c58468e9 | ||
|
25ea801cd4 | ||
|
ad192254a3 | ||
|
e039c8ee70 | ||
|
00e26fb862 | ||
|
1125a7dd45 | ||
|
3f2e5d34d7 | ||
|
b5fa6e77eb | ||
|
15d30ffcce | ||
|
976d78e595 | ||
|
d61d829b98 | ||
|
c933e4b891 | ||
|
8d5cf7a26b | ||
|
3b8cdffe15 | ||
|
e163c40107 | ||
|
91b3290322 | ||
|
522f749cfc | ||
|
2859eae91c | ||
|
78404ed56c | ||
|
e7c5efe8e4 | ||
|
b4cd34e351 | ||
|
4e7ec3e843 | ||
|
5e62b12bc4 | ||
|
46d2bc6559 | ||
|
bc4bd7235b | ||
|
d5d93df75c | ||
|
a4f12f6ebe | ||
|
318e4ac7e2 | ||
|
7574293624 | ||
|
30fb02bc10 | ||
|
a9ccda1873 | ||
|
99c7d49510 | ||
|
f7e965f098 | ||
|
2888b5782e | ||
|
b75f82fc8c | ||
|
93389d595d | ||
|
aeef2d5c4b | ||
|
169521e546 | ||
|
8a5148647b | ||
|
84d510fe16 | ||
|
09bbcf0b4d | ||
|
99908016be | ||
|
ec47f16cc4 | ||
|
e9c9786af1 | ||
|
6fdbaa07bd | ||
|
66ac5ff657 | ||
|
7a54465eb3 | ||
|
6043ca531f | ||
|
4243a1f6b1 | ||
|
9f47ed4b35 | ||
|
1d12c2036a | ||
|
6a94dfcfcf | ||
|
b2e10ca43e | ||
|
c4f8182f2f | ||
|
67f202fee2 | ||
|
a80851f6f0 | ||
|
97708c1349 | ||
|
6191bd8546 | ||
|
ff694f6ad6 | ||
|
6866a03f34 | ||
|
1dd6f1f29a | ||
|
1cc8f1d063 | ||
|
8f4a42fbf8 | ||
|
0dd9ae6783 | ||
|
dc53da4c6f | ||
|
8a4c138835 | ||
|
bbeb4b48e2 | ||
|
80c9a86428 | ||
|
298fc786b8 | ||
|
0428aa274a | ||
|
d268a987f3 | ||
|
f8aeb657ca | ||
|
0c5a3ae54c | ||
|
a8c85b754d | ||
|
025a172500 | ||
|
004787597e | ||
|
d981ae49a1 | ||
|
8a087dba6c | ||
|
6c230f66d7 | ||
|
7d0eeb0fd3 | ||
|
1010353071 | ||
|
8a15c0b074 | ||
|
d9cfe7f435 | ||
|
622e54dfce | ||
|
6d098de778 | ||
|
b74bc3d0b6 | ||
|
98c38b0dbf | ||
|
51833ed2de | ||
|
227bc9fcb1 | ||
|
a85a7a18c1 | ||
|
e2ec1c6613 | ||
|
662d4489c4 | ||
|
6df66b51c1 | ||
|
721a67b404 | ||
|
ac50f8274a | ||
|
bcfb0fb5c2 | ||
|
5e1dbf56f0 | ||
|
a76ce059e8 | ||
|
794c246f64 | ||
|
b6cb6f88a6 | ||
|
5caa93a399 | ||
|
8e1ca1dd7f | ||
|
20f0d8a3d5 | ||
|
215ac8cfc5 | ||
|
32570d6ae5 | ||
|
ac8794015e | ||
|
aadfbbc8a6 | ||
|
cc8f319298 | ||
|
248eaf9d79 | ||
|
b3a754a4a6 | ||
|
9227de3418 | ||
|
5702d96cac | ||
|
3dafb3ebcd | ||
|
9f56574066 | ||
|
1b8da9ec8e | ||
|
a89042c1f2 | ||
|
ffe4e68108 | ||
|
7ec8921f18 | ||
|
89595329a5 | ||
|
ef85feb098 | ||
|
9f9c882653 | ||
|
73ca325a6b | ||
|
5aded8de66 | ||
|
ae77badd18 | ||
|
d535ae6c2a | ||
|
aea1d86690 | ||
|
42a41d4817 | ||
|
a2f298a8ce | ||
|
909a0a9939 | ||
|
20f2649b65 | ||
|
5a0571763e | ||
|
2ebb36eba8 | ||
|
84c87b2eb8 | ||
|
60c1b4ea26 | ||
|
7781b2cd6b | ||
|
0fec9473ed | ||
|
0f32ad5d62 | ||
|
c5eb602de0 | ||
|
d0288e309f | ||
|
157f110105 | ||
|
374944dfc4 | ||
|
a24b7550cf | ||
|
5756d738d4 | ||
|
9b06fccb6d | ||
|
08d0a0fce2 | ||
|
987a2f5094 | ||
|
c0c8df6701 | ||
|
b4e6880cca | ||
|
b7287ea3f0 | ||
|
220f0febaa | ||
|
ac572e3bf5 | ||
|
7dd00645cd | ||
|
f83c74ffde | ||
|
67525c8f4d | ||
|
a126242f9f | ||
|
c9859f25d3 | ||
|
e982430c55 | ||
|
931df47607 | ||
|
27ca1c84ef | ||
|
b25bbe31f4 | ||
|
07eeec4827 | ||
|
57bf4078b6 | ||
|
9a7f27ae83 | ||
|
151ed22c7c | ||
|
74f7705165 | ||
|
bdf640c2b1 | ||
|
63ef18c178 | ||
|
81ebabeeff | ||
|
71e4470c25 | ||
|
6e82942b1f | ||
|
9fb6c17d62 | ||
|
ebfee273ac | ||
|
bd3fc4bb25 | ||
|
dbacb63fae | ||
|
877da921bf | ||
|
4575df572f | ||
|
0de3aeabd3 | ||
|
d67454140a | ||
|
abee77968d | ||
|
d74e171313 | ||
|
9f5cde36df | ||
|
66cd245eee | ||
|
c4e2b98187 | ||
|
7178b8e56c | ||
|
3ced5ab99d | ||
|
845abcae0f | ||
|
87d531814d | ||
|
938ce20579 | ||
|
1fc787023d | ||
|
90d4238f38 | ||
|
aa8525ff31 | ||
|
2c0f3d9cd9 | ||
|
5a8ba5dee7 | ||
|
c99810ad0e | ||
|
db83814527 | ||
|
5cd404e4a5 | ||
|
0e95fd44ce | ||
|
6addf7a243 | ||
|
b77d793698 | ||
|
68d2486fbb | ||
|
9a1e8c67a9 | ||
|
594d65ea25 | ||
|
826b1224f6 | ||
|
db87175872 | ||
|
17fe8f3277 | ||
|
92989d6d7a | ||
|
ad1decc813 | ||
|
d18243102b | ||
|
a4db59931c | ||
|
23f05752b3 | ||
|
72f34ea14e | ||
|
b18936eed0 | ||
|
edd0cb0aae | ||
|
2a3b5b143a | ||
|
97a2923b25 | ||
|
c8723656c1 | ||
|
15729b8691 | ||
|
936b3d5de5 | ||
|
427170e2e8 | ||
|
4b870de39e | ||
|
3936e01afb | ||
|
f3a869c2be | ||
|
9ce9c2f848 | ||
|
a8ebddb917 | ||
|
49c54c5fae | ||
|
5f9e06c09c | ||
|
1e91df996c | ||
|
b78dca52c2 | ||
|
8a7514cd1a | ||
|
b4f1e8cbbd | ||
|
e6dd0bc35d | ||
|
0e2ca7d851 | ||
|
3a5175f034 | ||
|
8700d63f67 | ||
|
bc17f2c422 | ||
|
0f658d5dec | ||
|
f86365694b | ||
|
570ec238ff | ||
|
74a006c252 | ||
|
04ec879035 | ||
|
0c04e9b3d5 | ||
|
8a55e1bd4b | ||
|
2869aea4f5 | ||
|
fdbfc9a58e | ||
|
40c2fac4f8 | ||
|
5409dc4ef2 | ||
|
fbfea917cb | ||
|
540f285468 | ||
|
a4a7c8d7e7 | ||
|
ec91203782 | ||
|
05e9f34765 | ||
|
2dc490b152 | ||
|
af6a8272dd | ||
|
a62e21a572 | ||
|
c7288eec38 | ||
|
7509a0d963 | ||
|
719480e95b | ||
|
add41494be | ||
|
178703f280 | ||
|
6497493d57 | ||
|
c79c2a8c89 | ||
|
84cac0d181 | ||
|
d58ed6202f | ||
|
8a027eb48d | ||
|
27270e7d98 | ||
|
52dbaebc2f | ||
|
856138daca | ||
|
e42e4fbe48 | ||
|
dfb3e2705a | ||
|
69fc4d30e4 | ||
|
caa4b2fc11 | ||
|
6c2e078b5e | ||
|
cf439ede2f | ||
|
be8b3e3157 | ||
|
3220cc93f4 | ||
|
7e346bd039 | ||
|
9c019afd94 | ||
|
60a3e66c95 | ||
|
22ef2c3a85 | ||
|
19619512a2 | ||
|
08e590f3e4 | ||
|
921d8d5bd8 | ||
|
5eeb07e1c1 | ||
|
366df65c92 | ||
|
ea3a66c17d | ||
|
3a4e6005f6 | ||
|
4aa3c9420a | ||
|
8186ecd55a | ||
|
208f2feed5 | ||
|
25f405838a | ||
|
84c036f8a3 | ||
|
c6ab79c388 | ||
|
469e4fa735 | ||
|
5b4e23eb8c | ||
|
2e31f3d1ec | ||
|
8f1aadc223 | ||
|
509dfda5e5 | ||
|
d302263ac1 | ||
|
3f8dcf2232 | ||
|
dbcc619454 | ||
|
4d876fb922 | ||
|
627a9488ce | ||
|
fb4a5be1d4 | ||
|
cf069de879 | ||
|
e9878fae0c | ||
|
cbe4466fea | ||
|
1d5516c374 | ||
|
5c9d4a80e2 | ||
|
27531de923 | ||
|
dffa8ff865 | ||
|
ed89d91ae4 | ||
|
708ee12182 | ||
|
9f112f6c12 | ||
|
2c303d74f2 | ||
|
b40627ad3e | ||
|
e6d5c8b9fd | ||
|
b6518af6b3 | ||
|
013264cb8e | ||
|
7ca113eed3 | ||
|
0d2a40a093 | ||
|
5804cd2c23 | ||
|
7462a6c0a3 | ||
|
c7a841fe5a | ||
|
f02ccd86c4 | ||
|
104177279e | ||
|
96916863ff | ||
|
b936608651 | ||
|
fa1e86acc3 | ||
|
b0af051f4c | ||
|
73d381215e | ||
|
b27f169c8a | ||
|
6a0512d0fe | ||
|
301a9698c8 | ||
|
2daf857b54 | ||
|
77b915f471 | ||
|
3964d4f6f2 | ||
|
52ace1ecb3 | ||
|
2dab48a2ee | ||
|
640a9e043d | ||
|
570d71845e | ||
|
862951e440 | ||
|
1dcd63a238 | ||
|
76257ca351 | ||
|
e5bab6660b | ||
|
3d00a0fd17 | ||
|
0ef943a673 | ||
|
31a21b50dd | ||
|
31934b62c7 | ||
|
6f6516ae33 | ||
|
0ab3d70952 | ||
|
74cdc3913a | ||
|
91f28516ff | ||
|
122b49b1cf | ||
|
56787b13b8 | ||
|
320583b5d4 | ||
|
a4ded26038 | ||
|
03a2664675 | ||
|
d87e4a846a | ||
|
19e3053c0a | ||
|
33bc9fe434 | ||
|
80a1eb8eab | ||
|
0f2c5cecb1 | ||
|
0d6e8ac0be | ||
|
c704cc4c45 | ||
|
9e0a5d2df9 | ||
|
ac6846b0ec | ||
|
5a6f0c2dcb | ||
|
b2a9477816 | ||
|
a3b82f036b | ||
|
5e0b28c09e | ||
|
569608295c | ||
|
c9ad3741ae | ||
|
b5fa294e07 | ||
|
3a0895bfd1 | ||
|
cf6761a50c | ||
|
4bb305ea65 | ||
|
f9c607b734 | ||
|
7919b742bd | ||
|
24affd785c | ||
|
518a667a84 | ||
|
36c92dc433 | ||
|
1f49df62d1 | ||
|
e89450daf1 | ||
|
9ffbf1320c | ||
|
34f9538afb | ||
|
0b76e1a561 | ||
|
2153f5ce64 | ||
|
f6410f8961 | ||
|
196627a283 | ||
|
a2bfa92692 | ||
|
2a36c956a4 | ||
|
28a16a36e0 | ||
|
cefea41810 | ||
|
bbba67487d | ||
|
9ab89a59d4 | ||
|
5d5e10f229 | ||
|
ba5ea334cc | ||
|
12821147ce | ||
|
0d691dbffa | ||
|
7a75dbc284 | ||
|
ef6f90c01c | ||
|
5b167df66d | ||
|
dbbfdd6acf | ||
|
ab7705e835 | ||
|
40dcd18487 | ||
|
2035a62ec6 | ||
|
976431c8e5 | ||
|
606fc028ac | ||
|
c148941194 | ||
|
437e65564f | ||
|
60566b3b0d | ||
|
3e6ec4c1cc | ||
|
a19d83ca7b | ||
|
15b2e7613f | ||
|
03ab084b42 | ||
|
9ab694b6f6 | ||
|
c1ad9025f9 | ||
|
2eda3827ce | ||
|
e20d544a8c | ||
|
bd4cf1a334 | ||
|
5e5d8415ba | ||
|
622bc832d3 | ||
|
83acaefea2 | ||
|
77cd538c2f | ||
|
f2a55312e4 | ||
|
01927d0f3b | ||
|
bad07defe8 | ||
|
627bc8ec65 | ||
|
881d5f1f71 | ||
|
aa1fca84d5 | ||
|
e99fbf5c4b | ||
|
be9c29e9d2 | ||
|
f2ab13afae | ||
|
d5a26fd495 | ||
|
9e89d5b891 | ||
|
f7fb35a4ec | ||
|
af1f0a0eda | ||
|
5ac03d6f49 | ||
|
d6c9748fef | ||
|
b052006922 | ||
|
004a3faf7d | ||
|
8365400ff5 | ||
|
1f2771e8bc | ||
|
33cf06732e | ||
|
40879db32f | ||
|
aa680d030f | ||
|
4dd69e7709 | ||
|
2c6c54acce | ||
|
40cc421f51 | ||
|
fb8e25ebdb | ||
|
4506380dbb | ||
|
b1058d6085 | ||
|
ce36dec7f8 | ||
|
a9d832b539 | ||
|
f98e1267fa | ||
|
1a6c6b8e9f | ||
|
2d4c79484f | ||
|
6e31cfb52a | ||
|
79c147866e | ||
|
4a62101b96 | ||
|
f0890d83fa |
@ -1,12 +1,22 @@
|
|||||||
|
# Golang CircleCI 2.0 configuration file
|
||||||
|
#
|
||||||
|
# Check https://circleci.com/docs/2.0/language-go/ for more details
|
||||||
version: 2
|
version: 2
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.14
|
- image: circleci/golang:1.16
|
||||||
working_directory: /go/src/github.com/OpenDiablo2/OpenDiablo2
|
working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}}
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run: sudo apt-get install -y libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libsdl2-dev libasound2-dev > /dev/null 2>&1
|
- run: sudo apt-get --allow-releaseinfo-change update && sudo apt-get install -y libgtk-3-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libsdl2-dev libasound2-dev > /dev/null 2>&1
|
||||||
|
- run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
|
||||||
- run: go get -v -t -d ./...
|
- run: go get -v -t -d ./...
|
||||||
- run: go build .
|
- run: go build .
|
||||||
#- run: go test -v ./...
|
- run: xvfb-run --auto-servernum go test -v -race ./...
|
||||||
|
- run: golangci-lint run ./...
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build:
|
||||||
|
jobs:
|
||||||
|
- build
|
||||||
|
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
13
.github/workflows/auto-author-assign.yml
vendored
Normal file
13
.github/workflows/auto-author-assign.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
name: 'Auto Author Assign'
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
assign-author:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: toshimaru/auto-author-assign@v1.3.0
|
||||||
|
with:
|
||||||
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
37
.github/workflows/pullRequest.yml
vendored
37
.github/workflows/pullRequest.yml
vendored
@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
name: pull_request
|
|
||||||
"on": [pull_request]
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
continue-on-error: true
|
|
||||||
steps:
|
|
||||||
- name: Set up Go 1.14
|
|
||||||
uses: actions/setup-go@v1
|
|
||||||
with:
|
|
||||||
go-version: 1.14
|
|
||||||
id: go
|
|
||||||
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y xvfb libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libsdl2-dev libasound2-dev > /dev/null 2>&1
|
|
||||||
|
|
||||||
- name: Run golangci-lint
|
|
||||||
continue-on-error: false
|
|
||||||
uses: golangci/golangci-lint-action@v1.2.1
|
|
||||||
with:
|
|
||||||
version: v1.27
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
env:
|
|
||||||
DISPLAY: ":99.0"
|
|
||||||
run: |
|
|
||||||
xvfb-run --auto-servernum go test -v -race ./...
|
|
||||||
|
|
||||||
- name: Build binary
|
|
||||||
run: go build .
|
|
39
.github/workflows/pushToMaster.yml
vendored
39
.github/workflows/pushToMaster.yml
vendored
@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
name: build
|
|
||||||
"on":
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Set up Go 1.14
|
|
||||||
uses: actions/setup-go@v1
|
|
||||||
with:
|
|
||||||
go-version: 1.14
|
|
||||||
id: go
|
|
||||||
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y xvfb libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libsdl2-dev libasound2-dev > /dev/null 2>&1
|
|
||||||
|
|
||||||
- name: Run golangci-lint
|
|
||||||
continue-on-error: false
|
|
||||||
uses: golangci/golangci-lint-action@v1.2.1
|
|
||||||
with:
|
|
||||||
version: v1.27
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
env:
|
|
||||||
DISPLAY: ":99.0"
|
|
||||||
run: |
|
|
||||||
xvfb-run --auto-servernum go test -v -race ./...
|
|
||||||
|
|
||||||
- name: Build binary
|
|
||||||
run: go build .
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,3 +13,4 @@
|
|||||||
tags
|
tags
|
||||||
heap.out
|
heap.out
|
||||||
heap.pdf
|
heap.pdf
|
||||||
|
.DS_Store
|
||||||
|
@ -18,16 +18,24 @@ linters-settings:
|
|||||||
disabled-checks:
|
disabled-checks:
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 15
|
min-complexity: 15
|
||||||
|
gofmt:
|
||||||
|
simplify: true
|
||||||
goimports:
|
goimports:
|
||||||
local-prefixes: github.com/OpenDiablo2/OpenDiablo2
|
local-prefixes: github.com/OpenDiablo2/OpenDiablo2
|
||||||
golint:
|
golint:
|
||||||
min-confidence: 0.8
|
min-confidence: 0.8
|
||||||
govet:
|
govet:
|
||||||
|
enable-all: true
|
||||||
check-shadowing: true
|
check-shadowing: true
|
||||||
|
disable:
|
||||||
|
# While struct sizes could be smaller if fields aligned properly, that also leads
|
||||||
|
# to possibly non-intuitive layout of struct fields (harder to read). Disable
|
||||||
|
# `fieldalignment` check here until we evaluate if it is worthwhile.
|
||||||
|
- fieldalignment
|
||||||
|
# https://github.com/golangci/golangci-lint/issues/1973
|
||||||
|
- sigchanyzer
|
||||||
lll:
|
lll:
|
||||||
line-length: 140
|
line-length: 140
|
||||||
maligned:
|
|
||||||
suggest-new: true
|
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
|
|
||||||
@ -57,14 +65,11 @@ linters:
|
|||||||
- gosimple
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- interfacer
|
|
||||||
- lll
|
- lll
|
||||||
- maligned
|
|
||||||
- misspell
|
- misspell
|
||||||
- nakedret
|
- nakedret
|
||||||
- prealloc
|
- prealloc
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- scopelint
|
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
- structcheck
|
||||||
- stylecheck
|
- stylecheck
|
||||||
@ -78,11 +83,19 @@ linters:
|
|||||||
|
|
||||||
run:
|
run:
|
||||||
timeout: 5m
|
timeout: 5m
|
||||||
|
tests: true
|
||||||
skip-dirs:
|
skip-dirs:
|
||||||
- .github
|
- .github
|
||||||
- build
|
- build
|
||||||
- web
|
- web
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
- linters:
|
||||||
|
- funlen
|
||||||
|
# Disable 'funlen' linter for test functions.
|
||||||
|
# It's common for table-driven tests to be more than 60 characters long
|
||||||
|
source: "^func Test"
|
||||||
|
max-issues-per-linter: 0
|
||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
exclude-use-default: false
|
exclude-use-default: false
|
||||||
|
43
.travis.yml
43
.travis.yml
@ -1,43 +0,0 @@
|
|||||||
language: go
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
- osx
|
|
||||||
- windows
|
|
||||||
go:
|
|
||||||
- 1.13.3
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- libx11-dev
|
|
||||||
- mesa-common-dev
|
|
||||||
- libglfw3-dev
|
|
||||||
- libgles2-mesa-dev
|
|
||||||
- libasound2-dev
|
|
||||||
script:
|
|
||||||
- if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install 7zip.portable ; fi
|
|
||||||
- mkdir -p ./opendiablo2-$TRAVIS_TAG-$TRAVIS_OS_NAME
|
|
||||||
- go get
|
|
||||||
- go build -ldflags "-X main.GitCommit=$TRAVIS_COMMIT -X main.GitBranch=$TRAVIS_TAG"
|
|
||||||
- if [ "$TRAVIS_OS_NAME" = "windows" ]; then ./rh.exe -open OpenDiablo2.exe -save OpenDiablo2.exe -action addskip -res d2logo.ico -mask ICONGROUP,MAIN, ; fi
|
|
||||||
- if [ "$TRAVIS_OS_NAME" = "windows" ]; then cp ./OpenDiablo2.exe ./opendiablo2-$TRAVIS_TAG-$TRAVIS_OS_NAME ; else cp ./OpenDiablo2 ./opendiablo2-$TRAVIS_TAG-$TRAVIS_OS_NAME; fi
|
|
||||||
- cp ./d2logo.png ./opendiablo2-$TRAVIS_TAG-$TRAVIS_OS_NAME
|
|
||||||
- if [ "$TRAVIS_OS_NAME" = "windows" ]; then 7z a -r opendiablo2-$TRAVIS_TAG-$TRAVIS_OS_NAME.zip ./opendiablo2-$TRAVIS_TAG-$TRAVIS_OS_NAME ; else tar -cvzf opendiablo2-$TRAVIS_TAG-$TRAVIS_OS_NAME.tar.gz ./opendiablo2-$TRAVIS_TAG-$TRAVIS_OS_NAME ; fi
|
|
||||||
git:
|
|
||||||
quiet: true
|
|
||||||
depth: 1
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
before_deploy:
|
|
||||||
- git tag dev
|
|
||||||
deploy:
|
|
||||||
provider: releases
|
|
||||||
api_key:
|
|
||||||
secure: "$GithubApi"
|
|
||||||
file_glob: true
|
|
||||||
file: opendiablo2-$TRAVIS_TAG-$TRAVIS_OS_NAME.*
|
|
||||||
skip_cleanup: true
|
|
||||||
overwrite: true
|
|
||||||
name: "OpenDiablo Unstable (Build $TRAVIS_BUILD_NUMBER)"
|
|
||||||
prerelease: true
|
|
||||||
on:
|
|
||||||
branch: master
|
|
@ -20,6 +20,7 @@ Ripolak
|
|||||||
dafe
|
dafe
|
||||||
presiyan
|
presiyan
|
||||||
Natureknight
|
Natureknight
|
||||||
|
Ganitzsh
|
||||||
|
|
||||||
* PATREON SUPPORTERS
|
* PATREON SUPPORTERS
|
||||||
K C
|
K C
|
||||||
|
164
README.md
164
README.md
@ -1,116 +1,62 @@
|
|||||||
|
# NOTE
|
||||||
|
<image align="left" src="https://user-images.githubusercontent.com/242652/138285004-b27d55b3-163b-4fe3-a8ff-6c34518044bd.png">
|
||||||
|
This project is currently being split into an Engine+Toolset (called Abyss Engine) and the game as a project (still called OpenDiablo 2). The new project repo is located here:
|
||||||
|
<br /><br />
|
||||||
|
https://github.com/AbyssEngine/
|
||||||
|
|
||||||
|
<br clear="all" />
|
||||||
|
|
||||||
# OpenDiablo2
|
# OpenDiablo2
|
||||||
[![CircleCI](https://circleci.com/gh/OpenDiablo2/OpenDiablo2/tree/master.svg?style=svg)](https://circleci.com/gh/OpenDiablo2/OpenDiablo2/tree/master)
|
|
||||||
[![Code Status](https://www.codefactor.io/repository/github/OpenDiablo2/OpenDiablo2/badge)](https://www.codefactor.io/repository/github/OpenDiablo2/OpenDiablo2)
|
![CircleCI](https://img.shields.io/circleci/build/github/OpenDiablo2/OpenDiablo2/master)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/OpenDiablo2/OpenDiablo2)](https://goreportcard.com/report/github.com/OpenDiablo2/OpenDiablo2)
|
||||||
|
[![GoDoc](https://pkg.go.dev/badge/github.com/OpenDiablo2/OpenDiablo2?utm_source=godoc)](https://pkg.go.dev/mod/github.com/OpenDiablo2/OpenDiablo2)
|
||||||
|
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
[![Discord](https://img.shields.io/discord/515518620034662421?label=Discord&style=social)](https://discord.gg/pRy8tdc)
|
||||||
|
[![Twitch Status](https://img.shields.io/twitch/status/essial?style=social)](https://www.twitch.tv/essial)
|
||||||
|
[![Subreddit subscribers](https://img.shields.io/reddit/subreddit-subscribers/OpenDiablo2?label=reddit&style=social)](https://www.reddit.com/r/OpenDiablo2/)
|
||||||
|
|
||||||
|
|
||||||
![Logo](d2logo.png)
|
![Logo](d2logo.png)
|
||||||
|
|
||||||
[Join us on Discord!](https://discord.gg/pRy8tdc)\
|
[![Patreon](https://img.shields.io/badge/dynamic/json?color=%23e85b46&label=Support%20us%20on%20Patreon&query=data.attributes.patron_count&suffix=%20patrons&url=https://www.patreon.com/api/campaigns/4762180)](https://www.patreon.com/bePatron?u=37261055)
|
||||||
[Development Live stream](https://www.twitch.tv/essial/)\
|
|
||||||
[Support us on Patreon](https://www.patreon.com/bePatron?u=37261055)
|
|
||||||
|
|
||||||
We are also working on a toolset:\
|
----
|
||||||
[https://github.com/OpenDiablo2/HellSpawner](https://github.com/OpenDiablo2/HellSpawner)\
|
[OpenDiablo2](https://opendiablo2.com/) is an ARPG game engine in the same vein of the 2000's games, and supports playing Diablo 2.
|
||||||
Please consider helping out with this project as well!
|
|
||||||
|
|
||||||
## About this project
|
The engine is written in Go and is cross platform.
|
||||||
|
|
||||||
OpenDiablo2 is an ARPG game engine in the same vein of the 2000's games, and supports playing Diablo 2. The engine is written in golang and is cross platform. However, please note that this project does not ship with the assets or content required to play Diablo 2. You must have a legally purchased copy of [Diablo 2](https://us.shop.battle.net/en-us/product/diablo-ii) and its expansion [Lord of Destruction](https://us.shop.battle.net/en-us/product/diablo-ii-lord-of-destruction) installed on your computer in order to run that game on this engine. If you have an original copy of the disks, those files should work fine as well.
|
> The project does not ship with the assets or content required to play Diablo 2.
|
||||||
|
You must have a legally purchased copy of [Diablo 2](https://us.shop.battle.net/en-us/product/diablo-ii) and its expansion [Lord of Destruction](https://us.shop.battle.net/en-us/product/diablo-ii-lord-of-destruction) installed on your computer in order to run that game on this engine.
|
||||||
|
|
||||||
We are currently working on features necessary to play Diablo 2 in its entirety. After this is completed, we will work on expanding the project to include tools and plugin support for modding, as well as writing completely new games with the engine.
|
If you like to contribute to OpenDiablo2, please be so kind to read our [Contribution Policy](./docs/CONTRIBUTING.md) first.
|
||||||
|
|
||||||
Please note that **this game is neither developed by, nor endorsed by Blizzard or its parent company Activision**.
|
----
|
||||||
|
|
||||||
Diablo 2 and its content is ©2000 Blizzard Entertainment, Inc. All rights reserved. Diablo and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries.
|
## Documentation
|
||||||
|
|
||||||
ALL OTHER TRADEMARKS ARE THE PROPERTY OF THEIR RESPECTIVE OWNERS.
|
_Stay awhile and listen_ ...
|
||||||
|
|
||||||
## Status
|
### ⚡ Project Info
|
||||||
|
|
||||||
At the moment (october 2020) the game starts, you can select any character and run around Act1 town.
|
* 👉 **[Current Status](./docs/status.md)** 👈 - what you should focus on
|
||||||
|
* [Roadmap](./docs/roadmap.md) - Planning ahead
|
||||||
|
* Design - High-level overview of the OpenDiablo2 org and its projects
|
||||||
|
* [FAQ](./docs/faq.md) - Common questions from new people to the project
|
||||||
|
|
||||||
Much work has been made in the background, but a lot of work still has to be done for the game to be playable.
|
### ⭐ For Users
|
||||||
|
|
||||||
Feel free to contribute!
|
* [Purchase](./docs/purchase.md) - Buy the official game from Blizzard
|
||||||
|
* [MPQ](./docs/mpq.md) - Locate the MPQ files
|
||||||
|
* [Install](./docs/install.md) - Install OpenDiablo2 to your system (Linux/Windows/MacOS)
|
||||||
|
* [Run it](./docs/play.md) - How to play the game
|
||||||
|
|
||||||
## Building
|
### 🔥 For Developers
|
||||||
|
|
||||||
To pull the project down, run `go get github.com/OpenDiablo2/OpenDiablo2`
|
* [Building](./docs/building.md) - Instructions for building the project
|
||||||
|
* [Development](./docs/development.md) - Instructions for developers who want to contribute
|
||||||
On windows this folder will most likely be in `C:\users\(you)\go\src\github.com\OpenDiablo2\OpenDiablo2`
|
* [Profiling](./docs/profiling.md) - Debug performance issues
|
||||||
|
* [Debugging](./docs/debug.md) - Common errors and pitfalls
|
||||||
In the root folder, run `go get -d` to pull down all dependencies.
|
|
||||||
|
|
||||||
To run the project, run `go run .` from the root folder.
|
|
||||||
|
|
||||||
You can also open the root folder in VSCode. Make sure you have the `ms-vscode.go` plugin installed.
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
There are several dependencies which need to be installed additionally.
|
|
||||||
To install them you can use `./build.sh` in the project root folder - this script takes care of the installation for you.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
The imports for this project utilize `github.com/OpenDiablo2/OpenDiablo2`. This means that even if you clone the repo, changes will not be taken as it will
|
|
||||||
still pull from the main repo's files. In order to use your local version, add the following to `go.mod` in the base folder:
|
|
||||||
|
|
||||||
```
|
|
||||||
replace github.com/OpenDiablo2/OpenDiablo2 => /your/forked/import/path
|
|
||||||
```
|
|
||||||
|
|
||||||
This will tell go to use your local path instead of the official repo. Be sure to exclude this change from your pull requests!
|
|
||||||
|
|
||||||
If you find something you'd like to fix thats obviously broken, create a branch, commit your code, and submit a pull request. If it's a new or missing feature you'd like to see, add an issue, and be descriptive!
|
|
||||||
|
|
||||||
If you'd like to help out and are not quite sure how, you can look through any open issues and tasks, or ask
|
|
||||||
for tasks on our discord server.
|
|
||||||
|
|
||||||
## VS Code Extensions
|
|
||||||
|
|
||||||
The following extensions are recommended for working with this project:
|
|
||||||
|
|
||||||
- ms-vscode.go
|
|
||||||
- defaltd.go-coverage-viewer
|
|
||||||
|
|
||||||
When you open the workspace for the first time, Visual Studio Code will automatically suggest these extensions for installation.
|
|
||||||
|
|
||||||
Alternatively you can get to it by going to settings <kbd>Ctrl+,</kbd>, expanding `Extensions` and selecting `Go configuration`,
|
|
||||||
then clicking on `Edit in settings.json`. Just paste that section where appropriate.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The engine is configured via the `config.json` file. By default, the configuration assumes that you have installed Diablo 2 and the
|
|
||||||
expansion via the official Blizzard Diablo2 installers using the default file paths. If you are not on Windows, or have installed
|
|
||||||
the game in a different location, the base path may have to be adjusted.
|
|
||||||
|
|
||||||
## Profiling
|
|
||||||
|
|
||||||
There are many profiler options to debug performance issues. These can be enabled by suppling the following command-line option and are saved in the `pprof` directory:
|
|
||||||
|
|
||||||
`go run . --profile=cpu`
|
|
||||||
|
|
||||||
Available profilers:\
|
|
||||||
`cpu` `mem` `block` `goroutine` `trace` `thread` `mutex`
|
|
||||||
|
|
||||||
You can export the profiler output with the following command:\
|
|
||||||
`go tool pprof --pdf ./OpenDiablo2 pprof/profiler.pprof > file.pdf`
|
|
||||||
|
|
||||||
Ingame you can create a heap dump by pressing `~` and typing `dumpheap`. A heap.pprof is written to the `pprof` directory.
|
|
||||||
|
|
||||||
You may need to install [Graphviz](http://www.graphviz.org/download/) in order to convert the profiler output.
|
|
||||||
|
|
||||||
## Debugging
|
|
||||||
|
|
||||||
### Layouts
|
|
||||||
|
|
||||||
Layouts can show their boundaries and other visual debugging information when they render. Set `layoutDebug` to `true` in `d2core/d2gui/layout.go` to enable this behavior.
|
|
||||||
|
|
||||||
![Example layout in debug mode](https://user-images.githubusercontent.com/1004323/85792085-31816480-b733-11ea-867e-291946bfff83.png)
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
There is an in-progress [project roadmap](https://docs.google.com/document/d/156sWiuk-XBfomVxZ3MD-ijxnwM1X66KTHo2AcWIy8bE/edit?usp=sharing),
|
|
||||||
which will be updated over time with new requirements.
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@ -120,11 +66,25 @@ which will be updated over time with new requirements.
|
|||||||
|
|
||||||
![Select Hero](docs/areas.gif)
|
![Select Hero](docs/areas.gif)
|
||||||
|
|
||||||
|
![Gameplay](docs/Gameplay.png)
|
||||||
|
|
||||||
|
![Inventory Window](docs/Inventory.png)
|
||||||
|
|
||||||
|
![Game Panels](docs/game_panels.png)
|
||||||
|
|
||||||
## Additional Credits
|
## Additional Credits
|
||||||
|
|
||||||
- Diablo2 Logo
|
* Diablo2 Logo
|
||||||
- Jose Pardilla (th3-prophetman)
|
* Jose Pardilla (th3-prophetman)
|
||||||
- DT1 File Specifications
|
* DT1 File Specifications
|
||||||
- Paul SIRAMY (http://paul.siramy.free.fr/_divers/dt1_doc/)
|
* Paul SIRAMY (http://paul.siramy.free.fr/\_divers/dt1\_doc/)
|
||||||
- Other Specifications and general info
|
* Other Specifications and general info
|
||||||
- Various users on [Phrozen Keep](https://d2mods.info/home.php)
|
* Various users on [Phrozen Keep](https://d2mods.info/home.php)
|
||||||
|
|
||||||
|
## Legal Notice
|
||||||
|
|
||||||
|
Please note that **this game is neither developed by, nor endorsed by Blizzard or its parent company Activision**.
|
||||||
|
|
||||||
|
Diablo 2 and its content is ©2000 Blizzard Entertainment, Inc. All rights reserved. Diablo and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries.
|
||||||
|
|
||||||
|
ALL OTHER TRADEMARKS ARE THE PROPERTY OF THEIR RESPECTIVE OWNERS.
|
||||||
|
60
build.sh
60
build.sh
@ -5,7 +5,7 @@
|
|||||||
# License: GNU GPLv3
|
# License: GNU GPLv3
|
||||||
|
|
||||||
version="0.0.8"
|
version="0.0.8"
|
||||||
go_version="1.13.4"
|
go_version="1.16"
|
||||||
echo "OpenDiablo 2 Build Script $version"
|
echo "OpenDiablo 2 Build Script $version"
|
||||||
|
|
||||||
#=================================================
|
#=================================================
|
||||||
@ -14,13 +14,11 @@ echo "OpenDiablo 2 Build Script $version"
|
|||||||
export PATH=$PATH:/usr/local/go/bin
|
export PATH=$PATH:/usr/local/go/bin
|
||||||
|
|
||||||
distribution=$(cat /etc/*release | grep "PRETTY_NAME" | sed 's/PRETTY_NAME=//g' | sed 's/["]//g' | awk '{print $1}')
|
distribution=$(cat /etc/*release | grep "PRETTY_NAME" | sed 's/PRETTY_NAME=//g' | sed 's/["]//g' | awk '{print $1}')
|
||||||
mesa_detect_arch=$(pacman -Q | grep mesa)
|
|
||||||
|
|
||||||
|
go_install() {
|
||||||
go_install(){
|
|
||||||
# Check OS & go
|
# Check OS & go
|
||||||
|
|
||||||
if ! command -v go > /dev/null 2>&1; then
|
if ! command -v go >/dev/null 2>&1; then
|
||||||
|
|
||||||
echo "Install Go for OpenDiablo 2 ($distribution)? y/n"
|
echo "Install Go for OpenDiablo 2 ($distribution)? y/n"
|
||||||
read -r choice
|
read -r choice
|
||||||
@ -28,25 +26,25 @@ go_install(){
|
|||||||
|
|
||||||
if [ "$distribution" = "CentOS" ] || [ "$distribution" = "Red\ Hat" ] || [ "$distribution" = "Oracle" ]; then
|
if [ "$distribution" = "CentOS" ] || [ "$distribution" = "Red\ Hat" ] || [ "$distribution" = "Oracle" ]; then
|
||||||
echo "Downloading Go"
|
echo "Downloading Go"
|
||||||
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz > /dev/null 2>&1
|
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz >/dev/null 2>&1
|
||||||
echo "Install Go"
|
echo "Install Go"
|
||||||
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz > /dev/null 2>&1
|
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz >/dev/null 2>&1
|
||||||
echo "Clean unneeded files"
|
echo "Clean unneeded files"
|
||||||
rm go*.linux-amd64.tar.gz
|
rm go*.linux-amd64.tar.gz
|
||||||
|
|
||||||
elif [ "$distribution" = "Fedora" ]; then
|
elif [ "$distribution" = "Fedora" ]; then
|
||||||
echo "Downloading Go"
|
echo "Downloading Go"
|
||||||
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz > /dev/null 2>&1
|
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz >/dev/null 2>&1
|
||||||
echo "Install Go"
|
echo "Install Go"
|
||||||
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz > /dev/null 2>&1
|
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz >/dev/null 2>&1
|
||||||
echo "Clean unneeded files"
|
echo "Clean unneeded files"
|
||||||
rm go*.linux-amd64.tar.gz
|
rm go*.linux-amd64.tar.gz
|
||||||
|
|
||||||
elif [ "$distribution" = "Debian" ] || [ "$distribution" = "Ubuntu" ] || [ "$distribution" = "Deepin" ]; then
|
elif [ "$distribution" = "Debian" ] || [ "$distribution" = "Ubuntu" ] || [ "$distribution" = "Deepin" ]; then
|
||||||
echo "Downloading Go"
|
echo "Downloading Go"
|
||||||
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz > /dev/null 2>&1
|
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz >/dev/null 2>&1
|
||||||
echo "Install Go"
|
echo "Install Go"
|
||||||
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz > /dev/null 2>&1
|
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz >/dev/null 2>&1
|
||||||
echo "Clean unneeded files"
|
echo "Clean unneeded files"
|
||||||
rm go*.linux-amd64.tar.gz
|
rm go*.linux-amd64.tar.gz
|
||||||
|
|
||||||
@ -58,39 +56,40 @@ go_install(){
|
|||||||
|
|
||||||
elif [ "$distribution" = "OpenSUSE" ] || [ "$distribution" = "SUSE" ]; then
|
elif [ "$distribution" = "OpenSUSE" ] || [ "$distribution" = "SUSE" ]; then
|
||||||
echo "Downloading Go"
|
echo "Downloading Go"
|
||||||
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz > /dev/null 2>&1
|
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz >/dev/null 2>&1
|
||||||
echo "Install Go"
|
echo "Install Go"
|
||||||
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz > /dev/null 2>&1
|
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz >/dev/null 2>&1
|
||||||
echo "Clean unneeded files"
|
echo "Clean unneeded files"
|
||||||
rm go*.linux-amd64.tar.gz
|
rm go*.linux-amd64.tar.gz
|
||||||
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
dep_install(){
|
dep_install() {
|
||||||
if [ "$distribution" = "CentOS" ] || [ "$distribution" = "Red\ Hat" ] || [ "$distribution" = "Oracle" ]; then
|
if [ "$distribution" = "CentOS" ] || [ "$distribution" = "Red\ Hat" ] || [ "$distribution" = "Oracle" ]; then
|
||||||
sudo yum install -y libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel alsa-lib-devel libXi-devel > /dev/null 2>&1
|
sudo yum install -y libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel alsa-lib-devel libXi-devel >/dev/null 2>&1
|
||||||
|
|
||||||
elif [ "$distribution" = "Fedora" ]; then
|
elif [ "$distribution" = "Fedora" ]; then
|
||||||
sudo dnf install -y libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel alsa-lib-devel libXi-devel > /dev/null 2>&1
|
sudo dnf install -y libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel alsa-lib-devel libXi-devel >/dev/null 2>&1
|
||||||
|
|
||||||
elif [ "$distribution" = "Debian" ] || [ "$distribution" = "Ubuntu" ] || [ "$distribution" = "Deepin" ]; then
|
elif [ "$distribution" = "Debian" ] || [ "$distribution" = "Ubuntu" ] || [ "$distribution" = "Deepin" ]; then
|
||||||
sudo apt-get install -y libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libsdl2-dev libasound2-dev > /dev/null 2>&1
|
sudo apt-get install -y libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libsdl2-dev libasound2-dev >/dev/null 2>&1
|
||||||
|
|
||||||
elif [ "$distribution" = "Gentoo" ]; then
|
elif [ "$distribution" = "Gentoo" ]; then
|
||||||
sudo emerge --ask n libXcursor libXrandr libXinerama libXi libGLw libglvnd libsdl2 alsa-lib > /dev/null 2>&1
|
sudo emerge --ask n libXcursor libXrandr libXinerama libXi libGLw libglvnd libsdl2 alsa-lib >/dev/null 2>&1
|
||||||
|
|
||||||
elif [ "$distribution" = "Manjaro" ] || [ "$distribution" = "Arch\ Linux" ]; then
|
elif [ "$distribution" = "Manjaro" ] || [ "$distribution" = "Arch\ Linux" ]; then
|
||||||
|
mesa_detect_arch=$(pacman -Q | grep mesa)
|
||||||
|
|
||||||
if [ -z "$mesa_detect_arch" ]; then
|
if [ -z "$mesa_detect_arch" ]; then
|
||||||
sudo pacman -S libxcursor libxrandr libxinerama libxi mesa libglvnd sdl2 sdl2_mixer sdl2_net alsa-lib --noconfirm > /dev/null 2>&1
|
sudo pacman -S libxcursor libxrandr libxinerama libxi mesa libglvnd sdl2 sdl2_mixer sdl2_net alsa-lib --noconfirm >/dev/null 2>&1
|
||||||
else
|
else
|
||||||
sudo pacman -S libxcursor libxrandr libxinerama libxi libglvnd sdl2 sdl2_mixer sdl2_net alsa-lib --noconfirm > /dev/null 2>&1
|
sudo pacman -S libxcursor libxrandr libxinerama libxi libglvnd sdl2 sdl2_mixer sdl2_net alsa-lib --noconfirm >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [ "$distribution" = "OpenSUSE" ] || [ "$distribution" = "SUSE" ]; then
|
elif [ "$distribution" = "OpenSUSE" ] || [ "$distribution" = "SUSE" ]; then
|
||||||
sudo zypper install -y libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel Mesa-libGL-devel alsa-lib-devel libXi-devel > /dev/null 2>&1
|
sudo zypper install -y libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel Mesa-libGL-devel alsa-lib-devel libXi-devel >/dev/null 2>&1
|
||||||
|
|
||||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
# are there dependencies required? did I just have all of them already?
|
# are there dependencies required? did I just have all of them already?
|
||||||
@ -102,15 +101,22 @@ dep_install(){
|
|||||||
# Build
|
# Build
|
||||||
echo "Check Go"
|
echo "Check Go"
|
||||||
go_install
|
go_install
|
||||||
|
|
||||||
echo "Install libraries"
|
echo "Install libraries"
|
||||||
if [ -e "$HOME/.config/OpenDiablo2/.libs" ]; then
|
if [ ! -e "$HOME/.config/OpenDiablo2" ]; then
|
||||||
echo "libraries is installed"
|
mkdir -p $HOME/.config/OpenDiablo2
|
||||||
else
|
|
||||||
echo "OK" > "$HOME/.config/OpenDiablo2/.libs"
|
|
||||||
dep_install
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -e "$HOME/.config/OpenDiablo2/.libs" ]; then
|
||||||
|
echo "libraries is installed"
|
||||||
|
else
|
||||||
|
echo "OK" >"$HOME/.config/OpenDiablo2/.libs"
|
||||||
|
dep_install
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Build OpenDiablo 2"
|
echo "Build OpenDiablo 2"
|
||||||
go get -d
|
go get -d
|
||||||
go build
|
go build
|
||||||
|
|
||||||
echo "Build finished. Running OpenDiablo2 will generate a config.json file."
|
echo "Build finished. Running OpenDiablo2 will generate a config.json file."
|
||||||
echo "If there are subsequent errors, please inspect and edit the config.json file. See doc/index.html for more details"
|
echo "If there are subsequent errors, please inspect and edit the config.json file. See doc/index.html for more details"
|
||||||
|
553
d2app/app.go
553
d2app/app.go
@ -4,34 +4,41 @@ package d2app
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"container/ring"
|
"container/ring"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/gif"
|
"image/gif"
|
||||||
"image/png"
|
"image/png"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
|
||||||
|
|
||||||
"github.com/pkg/profile"
|
"github.com/pkg/profile"
|
||||||
"golang.org/x/image/colornames"
|
"golang.org/x/image/colornames"
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
||||||
|
ebiten2 "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio/ebiten"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2input"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render/ebiten"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2term"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2gamescreen"
|
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2gamescreen"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2networking"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client"
|
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
|
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2script"
|
"github.com/OpenDiablo2/OpenDiablo2/d2script"
|
||||||
@ -56,6 +63,8 @@ type App struct {
|
|||||||
captureFrames []*image.RGBA
|
captureFrames []*image.RGBA
|
||||||
gitBranch string
|
gitBranch string
|
||||||
gitCommit string
|
gitCommit string
|
||||||
|
language string
|
||||||
|
charset string
|
||||||
asset *d2asset.AssetManager
|
asset *d2asset.AssetManager
|
||||||
inputManager d2interface.InputManager
|
inputManager d2interface.InputManager
|
||||||
terminal d2interface.Terminal
|
terminal d2interface.Terminal
|
||||||
@ -66,12 +75,18 @@ type App struct {
|
|||||||
ui *d2ui.UIManager
|
ui *d2ui.UIManager
|
||||||
tAllocSamples *ring.Ring
|
tAllocSamples *ring.Ring
|
||||||
guiManager *d2gui.GuiManager
|
guiManager *d2gui.GuiManager
|
||||||
|
config *d2config.Configuration
|
||||||
|
*d2util.Logger
|
||||||
|
errorMessage error
|
||||||
|
*Options
|
||||||
}
|
}
|
||||||
|
|
||||||
type bindTerminalEntry struct {
|
// Options is used to store all of the app options that can be set with arguments
|
||||||
name string
|
type Options struct {
|
||||||
description string
|
Debug *bool
|
||||||
action interface{}
|
profiler *string
|
||||||
|
Server *d2networking.ServerOptions
|
||||||
|
LogLevel *d2util.LogLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -80,53 +95,217 @@ const (
|
|||||||
debugPopN = 6
|
debugPopN = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create creates a new instance of the application
|
const (
|
||||||
func Create(gitBranch, gitCommit string,
|
appLoggerPrefix = "App"
|
||||||
inputManager d2interface.InputManager,
|
)
|
||||||
terminal d2interface.Terminal,
|
|
||||||
scriptEngine *d2script.ScriptEngine,
|
|
||||||
audio d2interface.AudioProvider,
|
|
||||||
renderer d2interface.Renderer,
|
|
||||||
asset *d2asset.AssetManager,
|
|
||||||
) *App {
|
|
||||||
uiManager := d2ui.NewUIManager(asset, renderer, inputManager, audio)
|
|
||||||
|
|
||||||
result := &App{
|
// Create creates a new instance of the application
|
||||||
|
func Create(gitBranch, gitCommit string) *App {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
|
||||||
|
logger := d2util.NewLogger()
|
||||||
|
logger.SetPrefix(appLoggerPrefix)
|
||||||
|
|
||||||
|
app := &App{
|
||||||
|
Logger: logger,
|
||||||
gitBranch: gitBranch,
|
gitBranch: gitBranch,
|
||||||
gitCommit: gitCommit,
|
gitCommit: gitCommit,
|
||||||
inputManager: inputManager,
|
Options: &Options{
|
||||||
terminal: terminal,
|
Server: &d2networking.ServerOptions{},
|
||||||
scriptEngine: scriptEngine,
|
},
|
||||||
audio: audio,
|
}
|
||||||
renderer: renderer,
|
app.Infof("OpenDiablo2 - Open source Diablo 2 engine")
|
||||||
ui: uiManager,
|
|
||||||
asset: asset,
|
app.parseArguments()
|
||||||
tAllocSamples: createZeroedRing(nSamplesTAlloc),
|
|
||||||
|
app.SetLevel(*app.Options.LogLevel)
|
||||||
|
|
||||||
|
app.asset, app.errorMessage = d2asset.NewAssetManager(*app.Options.LogLevel)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNOOP() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) startDedicatedServer() error {
|
||||||
|
min, max := d2networking.ServerMinPlayers, d2networking.ServerMaxPlayersDefault
|
||||||
|
maxPlayers := d2math.ClampInt(*a.Options.Server.MaxPlayers, min, max)
|
||||||
|
|
||||||
|
srvChanIn := make(chan int)
|
||||||
|
srvChanLog := make(chan string)
|
||||||
|
|
||||||
|
srvErr := d2networking.StartDedicatedServer(a.asset, srvChanIn, srvChanLog, *a.Options.LogLevel, maxPlayers)
|
||||||
|
if srvErr != nil {
|
||||||
|
return srvErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.gitBranch == "" {
|
c := make(chan os.Signal)
|
||||||
result.gitBranch = "Local Build"
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM) // This traps Control-c to safely shut down the server
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-c
|
||||||
|
srvChanIn <- d2networking.ServerEventStop
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
for data := range srvChanLog {
|
||||||
|
a.Info(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) loadEngine() error {
|
||||||
|
// Create our renderer
|
||||||
|
renderer, err := ebiten.CreateRenderer(a.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
a.renderer = renderer
|
||||||
|
|
||||||
|
if a.errorMessage != nil {
|
||||||
|
return a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, "OpenDiablo2")
|
||||||
|
}
|
||||||
|
|
||||||
|
audio := ebiten2.CreateAudio(*a.Options.LogLevel, a.asset)
|
||||||
|
|
||||||
|
inputManager := d2input.NewInputManager()
|
||||||
|
|
||||||
|
term, err := d2term.New(inputManager)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptEngine := d2script.CreateScriptEngine()
|
||||||
|
|
||||||
|
uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, *a.Options.LogLevel, audio)
|
||||||
|
|
||||||
|
a.inputManager = inputManager
|
||||||
|
a.terminal = term
|
||||||
|
a.scriptEngine = scriptEngine
|
||||||
|
a.audio = audio
|
||||||
|
a.ui = uiManager
|
||||||
|
a.tAllocSamples = createZeroedRing(nSamplesTAlloc)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) parseArguments() {
|
||||||
|
const (
|
||||||
|
descProfile = "Profiles the program,\none of (cpu, mem, block, goroutine, trace, thread, mutex)"
|
||||||
|
descPlayers = "Sets the number of max players for the dedicated server"
|
||||||
|
descLogging = "Enables verbose logging. Log levels will include those below it.\n" +
|
||||||
|
" 0 disables log messages\n" +
|
||||||
|
" 1 shows fatal\n" +
|
||||||
|
" 2 shows error\n" +
|
||||||
|
" 3 shows warning\n" +
|
||||||
|
" 4 shows info\n" +
|
||||||
|
" 5 shows debug\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
a.Options.profiler = flag.String("profile", "", descProfile)
|
||||||
|
a.Options.Server.Dedicated = flag.Bool("dedicated", false, "Starts a dedicated server")
|
||||||
|
a.Options.Server.MaxPlayers = flag.Int("players", 0, descPlayers)
|
||||||
|
a.Options.LogLevel = flag.Int("l", d2util.LogLevelDefault, descLogging)
|
||||||
|
showVersion := flag.Bool("v", false, "Show version")
|
||||||
|
showHelp := flag.Bool("h", false, "Show help")
|
||||||
|
|
||||||
|
flag.Usage = func() {
|
||||||
|
fmt.Printf("usage: %s [<flags>]\n\nFlags:\n", os.Args[0])
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *a.Options.LogLevel >= d2util.LogLevelUnspecified {
|
||||||
|
*a.Options.LogLevel = d2util.LogLevelDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
if *showVersion {
|
||||||
|
a.Infof("version: OpenDiablo2 (%s %s)", a.gitBranch, a.gitCommit)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *showHelp {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig loads the OpenDiablo2 config file
|
||||||
|
func (a *App) LoadConfig() (*d2config.Configuration, error) {
|
||||||
|
// by now the, the loader has initialized and added our config dirs as sources...
|
||||||
|
configBaseName := filepath.Base(d2config.DefaultConfigPath())
|
||||||
|
|
||||||
|
configAsset, _ := a.asset.LoadAsset(configBaseName)
|
||||||
|
|
||||||
|
config := &d2config.Configuration{}
|
||||||
|
config.SetPath(d2config.DefaultConfigPath())
|
||||||
|
|
||||||
|
// create the default if not found
|
||||||
|
if configAsset == nil {
|
||||||
|
config = d2config.DefaultConfig()
|
||||||
|
|
||||||
|
fullPath := filepath.Join(config.Dir(), config.Base())
|
||||||
|
config.SetPath(fullPath)
|
||||||
|
|
||||||
|
a.Infof("creating default configuration file at %s...", fullPath)
|
||||||
|
|
||||||
|
saveErr := config.Save()
|
||||||
|
|
||||||
|
return config, saveErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(configAsset).Decode(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Infof("loaded configuration file from %s", config.Path())
|
||||||
|
|
||||||
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run executes the application and kicks off the entire game process
|
// Run executes the application and kicks off the entire game process
|
||||||
func (a *App) Run() error {
|
func (a *App) Run() (err error) {
|
||||||
profileOption := kingpin.Flag("profile", "Profiles the program, one of (cpu, mem, block, goroutine, trace, thread, mutex)").String()
|
// add our possible config directories
|
||||||
kingpin.Parse()
|
_ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath()), types.AssetSourceFileSystem)
|
||||||
|
_ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath()), types.AssetSourceFileSystem)
|
||||||
|
|
||||||
if len(*profileOption) > 0 {
|
if a.config, err = a.LoadConfig(); err != nil {
|
||||||
profiler := enableProfiler(*profileOption)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// start profiler if argument was supplied
|
||||||
|
if len(*a.Options.profiler) > 0 {
|
||||||
|
profiler := enableProfiler(*a.Options.profiler, a)
|
||||||
if profiler != nil {
|
if profiler != nil {
|
||||||
defer profiler.Stop()
|
defer profiler.Stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start the server if `--listen` option was supplied
|
||||||
|
if *a.Options.Server.Dedicated {
|
||||||
|
if err := a.startDedicatedServer(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.loadEngine(); err != nil {
|
||||||
|
a.renderer.ShowPanicScreen(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
windowTitle := fmt.Sprintf("OpenDiablo2 (%s)", a.gitBranch)
|
windowTitle := fmt.Sprintf("OpenDiablo2 (%s)", a.gitBranch)
|
||||||
|
|
||||||
// If we fail to initialize, we will show the error screen
|
// If we fail to initialize, we will show the error screen
|
||||||
if err := a.initialize(); err != nil {
|
if err := a.initialize(); err != nil {
|
||||||
if gameErr := a.renderer.Run(updateInitError, 800, 600, windowTitle); gameErr != nil {
|
if a.errorMessage == nil {
|
||||||
|
a.errorMessage = err // if there was an error during init, don't clobber it
|
||||||
|
}
|
||||||
|
|
||||||
|
gameErr := a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, windowTitle)
|
||||||
|
if gameErr != nil {
|
||||||
return gameErr
|
return gameErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,86 +314,16 @@ func (a *App) Run() error {
|
|||||||
|
|
||||||
a.ToMainMenu()
|
a.ToMainMenu()
|
||||||
|
|
||||||
if err := a.renderer.Run(a.update, 800, 600, windowTitle); err != nil {
|
if err := a.renderer.Run(a.update, a.advance, 800, 600, windowTitle); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initialize() error {
|
func (a *App) renderDebug(target d2interface.Surface) {
|
||||||
a.timeScale = 1.0
|
|
||||||
a.lastTime = d2util.Now()
|
|
||||||
a.lastScreenAdvance = a.lastTime
|
|
||||||
|
|
||||||
a.renderer.SetWindowIcon("d2logo.png")
|
|
||||||
a.terminal.BindLogger()
|
|
||||||
|
|
||||||
terminalActions := [...]bindTerminalEntry{
|
|
||||||
{"dumpheap", "dumps the heap to pprof/heap.pprof", a.dumpHeap},
|
|
||||||
{"fullscreen", "toggles fullscreen", a.toggleFullScreen},
|
|
||||||
{"capframe", "captures a still frame", a.setupCaptureFrame},
|
|
||||||
{"capgifstart", "captures an animation (start)", a.startAnimationCapture},
|
|
||||||
{"capgifstop", "captures an animation (stop)", a.stopAnimationCapture},
|
|
||||||
{"vsync", "toggles vsync", a.toggleVsync},
|
|
||||||
{"fps", "toggle fps counter", a.toggleFpsCounter},
|
|
||||||
{"timescale", "set scalar for elapsed time", a.setTimeScale},
|
|
||||||
{"quit", "exits the game", a.quitGame},
|
|
||||||
{"screen-gui", "enters the gui playground screen", a.enterGuiPlayground},
|
|
||||||
{"js", "eval JS scripts", a.evalJS},
|
|
||||||
}
|
|
||||||
|
|
||||||
for idx := range terminalActions {
|
|
||||||
action := &terminalActions[idx]
|
|
||||||
|
|
||||||
if err := a.terminal.BindAction(action.name, action.description, action.action); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
a.guiManager, err = d2gui.CreateGuiManager(a.asset, a.inputManager)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
a.screen = d2screen.NewScreenManager(a.ui, a.guiManager)
|
|
||||||
|
|
||||||
config := d2config.Config
|
|
||||||
a.audio.SetVolumes(config.BgmVolume, config.SfxVolume)
|
|
||||||
|
|
||||||
if err := a.loadStrings(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
a.ui.Initialize()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) loadStrings() error {
|
|
||||||
tablePaths := []string{
|
|
||||||
d2resource.PatchStringTable,
|
|
||||||
d2resource.ExpansionStringTable,
|
|
||||||
d2resource.StringTable,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tablePath := range tablePaths {
|
|
||||||
data, err := a.asset.LoadFile(tablePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d2tbl.LoadTextDictionary(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) renderDebug(target d2interface.Surface) error {
|
|
||||||
if !a.showFPS {
|
if !a.showFPS {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
vsyncEnabled := a.renderer.GetVSyncEnabled()
|
vsyncEnabled := a.renderer.GetVSyncEnabled()
|
||||||
@ -241,8 +350,6 @@ func (a *App) renderDebug(target d2interface.Surface) error {
|
|||||||
target.PushTranslation(0, debugLineHeight)
|
target.PushTranslation(0, debugLineHeight)
|
||||||
target.DrawTextf("Coords " + strconv.FormatInt(int64(cx), 10) + "," + strconv.FormatInt(int64(cy), 10))
|
target.DrawTextf("Coords " + strconv.FormatInt(int64(cx), 10) + "," + strconv.FormatInt(int64(cy), 10))
|
||||||
target.PopN(debugPopN)
|
target.PopN(debugPopN)
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) renderCapture(target d2interface.Surface) error {
|
func (a *App) renderCapture(target d2interface.Surface) error {
|
||||||
@ -274,35 +381,33 @@ func (a *App) renderCapture(target d2interface.Surface) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) render(target d2interface.Surface) error {
|
func (a *App) render(target d2interface.Surface) {
|
||||||
if err := a.screen.Render(target); err != nil {
|
a.screen.Render(target)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
a.ui.Render(target)
|
a.ui.Render(target)
|
||||||
|
|
||||||
if err := a.guiManager.Render(target); err != nil {
|
if err := a.guiManager.Render(target); err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.renderDebug(target); err != nil {
|
a.renderDebug(target)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.renderCapture(target); err != nil {
|
if err := a.renderCapture(target); err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.terminal.Render(target); err != nil {
|
if err := a.terminal.Render(target); err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) advance(elapsed, elapsedUnscaled, current float64) error {
|
func (a *App) advance() error {
|
||||||
elapsedLastScreenAdvance := (current - a.lastScreenAdvance) * a.timeScale
|
current := d2util.Now()
|
||||||
|
elapsedUnscaled := current - a.lastTime
|
||||||
|
elapsed := elapsedUnscaled * a.timeScale
|
||||||
|
|
||||||
|
a.lastTime = current
|
||||||
|
|
||||||
|
elapsedLastScreenAdvance := (current - a.lastScreenAdvance) * a.timeScale
|
||||||
a.lastScreenAdvance = current
|
a.lastScreenAdvance = current
|
||||||
|
|
||||||
if err := a.screen.Advance(elapsedLastScreenAdvance); err != nil {
|
if err := a.screen.Advance(elapsedLastScreenAdvance); err != nil {
|
||||||
@ -327,18 +432,7 @@ func (a *App) advance(elapsed, elapsedUnscaled, current float64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) update(target d2interface.Surface) error {
|
func (a *App) update(target d2interface.Surface) error {
|
||||||
currentTime := d2util.Now()
|
a.render(target)
|
||||||
elapsedTimeUnscaled := currentTime - a.lastTime
|
|
||||||
elapsedTime := elapsedTimeUnscaled * a.timeScale
|
|
||||||
a.lastTime = currentTime
|
|
||||||
|
|
||||||
if err := a.advance(elapsedTime, elapsedTimeUnscaled, currentTime); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.render(target); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if target.GetDepth() > 0 {
|
if target.GetDepth() > 0 {
|
||||||
return errors.New("detected surface stack leak")
|
return errors.New("detected surface stack leak")
|
||||||
@ -355,67 +449,24 @@ func (a *App) allocRate(totalAlloc uint64, fps float64) float64 {
|
|||||||
return deltaAllocPerFrame * fps / bytesToMegabyte
|
return deltaAllocPerFrame * fps / bytesToMegabyte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) dumpHeap() {
|
|
||||||
if _, err := os.Stat("./pprof/"); os.IsNotExist(err) {
|
|
||||||
if err := os.Mkdir("./pprof/", 0750); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileOut, err := os.Create("./pprof/heap.pprof")
|
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pprof.WriteHeapProfile(fileOut); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fileOut.Close(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) evalJS(code string) {
|
|
||||||
val, err := a.scriptEngine.Eval(code)
|
|
||||||
if err != nil {
|
|
||||||
a.terminal.OutputErrorf("%s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("%s", val)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) toggleFullScreen() {
|
|
||||||
fullscreen := !a.renderer.IsFullScreen()
|
|
||||||
a.renderer.SetFullScreen(fullscreen)
|
|
||||||
a.terminal.OutputInfof("fullscreen is now: %v", fullscreen)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) setupCaptureFrame(path string) {
|
|
||||||
a.captureState = captureStateFrame
|
|
||||||
a.capturePath = path
|
|
||||||
a.captureFrames = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) doCaptureFrame(target d2interface.Surface) error {
|
func (a *App) doCaptureFrame(target d2interface.Surface) error {
|
||||||
fp, err := os.Create(a.capturePath)
|
fp, err := os.Create(a.capturePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
a.terminal.Errorf("failed to create %q", a.capturePath)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := fp.Close(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
screenshot := target.Screenshot()
|
screenshot := target.Screenshot()
|
||||||
if err := png.Encode(fp, screenshot); err != nil {
|
if err := png.Encode(fp, screenshot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("saved frame to %s", a.capturePath)
|
if err := fp.Close(); err != nil {
|
||||||
|
a.terminal.Errorf("failed to create %q", a.capturePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.terminal.Infof("saved frame to %s", a.capturePath)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -433,7 +484,7 @@ func (a *App) convertFramesToGif() error {
|
|||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := fp.Close(); err != nil {
|
if err := fp.Close(); err != nil {
|
||||||
log.Fatal(err)
|
a.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -475,49 +526,11 @@ func (a *App) convertFramesToGif() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("saved animation to %s", a.capturePath)
|
a.Infof("saved animation to %s", a.capturePath)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) startAnimationCapture(path string) {
|
|
||||||
a.captureState = captureStateGif
|
|
||||||
a.capturePath = path
|
|
||||||
a.captureFrames = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) stopAnimationCapture() {
|
|
||||||
a.captureState = captureStateNone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) toggleVsync() {
|
|
||||||
vsync := !a.renderer.GetVSyncEnabled()
|
|
||||||
a.renderer.SetVSyncEnabled(vsync)
|
|
||||||
a.terminal.OutputInfof("vsync is now: %v", vsync)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) toggleFpsCounter() {
|
|
||||||
a.showFPS = !a.showFPS
|
|
||||||
a.terminal.OutputInfof("fps counter is now: %v", a.showFPS)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) setTimeScale(timeScale float64) {
|
|
||||||
if timeScale <= 0 {
|
|
||||||
a.terminal.OutputErrorf("invalid time scale value")
|
|
||||||
} else {
|
|
||||||
a.terminal.OutputInfof("timescale changed from %f to %f", a.timeScale, timeScale)
|
|
||||||
a.timeScale = timeScale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) quitGame() {
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) enterGuiPlayground() {
|
|
||||||
a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, a.asset))
|
|
||||||
}
|
|
||||||
|
|
||||||
func createZeroedRing(n int) *ring.Ring {
|
func createZeroedRing(n int) *ring.Ring {
|
||||||
r := ring.New(n)
|
r := ring.New(n)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
@ -528,36 +541,36 @@ func createZeroedRing(n int) *ring.Ring {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableProfiler(profileOption string) interface{ Stop() } {
|
func enableProfiler(profileOption string, a *App) interface{ Stop() } {
|
||||||
var options []func(*profile.Profile)
|
var options []func(*profile.Profile)
|
||||||
|
|
||||||
switch strings.ToLower(strings.Trim(profileOption, " ")) {
|
switch strings.ToLower(strings.Trim(profileOption, " ")) {
|
||||||
case "cpu":
|
case "cpu":
|
||||||
log.Printf("CPU profiling is enabled.")
|
a.Logger.Debug("CPU profiling is enabled.")
|
||||||
|
|
||||||
options = append(options, profile.CPUProfile)
|
options = append(options, profile.CPUProfile)
|
||||||
case "mem":
|
case "mem":
|
||||||
log.Printf("Memory profiling is enabled.")
|
a.Logger.Debug("Memory profiling is enabled.")
|
||||||
|
|
||||||
options = append(options, profile.MemProfile)
|
options = append(options, profile.MemProfile)
|
||||||
case "block":
|
case "block":
|
||||||
log.Printf("Block profiling is enabled.")
|
a.Logger.Debug("Block profiling is enabled.")
|
||||||
|
|
||||||
options = append(options, profile.BlockProfile)
|
options = append(options, profile.BlockProfile)
|
||||||
case "goroutine":
|
case "goroutine":
|
||||||
log.Printf("Goroutine profiling is enabled.")
|
a.Logger.Debug("Goroutine profiling is enabled.")
|
||||||
|
|
||||||
options = append(options, profile.GoroutineProfile)
|
options = append(options, profile.GoroutineProfile)
|
||||||
case "trace":
|
case "trace":
|
||||||
log.Printf("Trace profiling is enabled.")
|
a.Logger.Debug("Trace profiling is enabled.")
|
||||||
|
|
||||||
options = append(options, profile.TraceProfile)
|
options = append(options, profile.TraceProfile)
|
||||||
case "thread":
|
case "thread":
|
||||||
log.Printf("Thread creation profiling is enabled.")
|
a.Logger.Debug("Thread creation profiling is enabled.")
|
||||||
|
|
||||||
options = append(options, profile.ThreadcreationProfile)
|
options = append(options, profile.ThreadcreationProfile)
|
||||||
case "mutex":
|
case "mutex":
|
||||||
log.Printf("Mutex profiling is enabled.")
|
a.Logger.Debug("Mutex profiling is enabled.")
|
||||||
|
|
||||||
options = append(options, profile.MutexProfile)
|
options = append(options, profile.MutexProfile)
|
||||||
}
|
}
|
||||||
@ -571,26 +584,22 @@ func enableProfiler(profileOption string) interface{ Stop() } {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateInitError(target d2interface.Surface) error {
|
func (a *App) updateInitError(target d2interface.Surface) error {
|
||||||
err := target.Clear(colornames.Darkred)
|
target.Clear(colornames.Darkred)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
target.PushTranslation(errMsgPadding, errMsgPadding)
|
target.PushTranslation(errMsgPadding, errMsgPadding)
|
||||||
target.DrawTextf(`Could not find the MPQ files in the directory:
|
target.DrawTextf(a.errorMessage.Error())
|
||||||
%s\nPlease put the files and re-run the game.`, d2config.Config.MpqPath)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMainMenu forces the game to transition to the Main Menu
|
// ToMainMenu forces the game to transition to the Main Menu
|
||||||
func (a *App) ToMainMenu() {
|
func (a *App) ToMainMenu(errorMessageOptional ...string) {
|
||||||
buildInfo := d2gamescreen.BuildInfo{Branch: a.gitBranch, Commit: a.gitCommit}
|
buildInfo := d2gamescreen.BuildInfo{Branch: a.gitBranch, Commit: a.gitCommit}
|
||||||
|
|
||||||
mainMenu, err := d2gamescreen.CreateMainMenu(a, a.asset, a.renderer, a.inputManager, a.audio, a.ui, buildInfo)
|
mainMenu, err := d2gamescreen.CreateMainMenu(a, a.asset, a.renderer, a.inputManager, a.audio, a.ui, buildInfo,
|
||||||
|
*a.Options.LogLevel, errorMessageOptional...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
a.Error(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -599,9 +608,9 @@ func (a *App) ToMainMenu() {
|
|||||||
|
|
||||||
// ToSelectHero forces the game to transition to the Select Hero (create character) screen
|
// ToSelectHero forces the game to transition to the Select Hero (create character) screen
|
||||||
func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, host string) {
|
func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, host string) {
|
||||||
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, host)
|
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, *a.Options.LogLevel, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
a.Error(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,34 +619,49 @@ func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType,
|
|||||||
|
|
||||||
// ToCreateGame forces the game to transition to the Create Game screen
|
// ToCreateGame forces the game to transition to the Create Game screen
|
||||||
func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, host string) {
|
func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, host string) {
|
||||||
gameClient, err := d2client.Create(connType, a.asset, a.scriptEngine)
|
gameClient, err := d2client.Create(connType, a.asset, *a.Options.LogLevel, a.scriptEngine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
a.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if gameClient == nil {
|
||||||
|
a.Error("could not create client")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = gameClient.Open(host, filePath); err != nil {
|
if err = gameClient.Open(host, filePath); err != nil {
|
||||||
// https://github.com/OpenDiablo2/OpenDiablo2/issues/805
|
errorMessage := fmt.Sprintf("can not connect to the host: %s", host)
|
||||||
fmt.Printf("can not connect to the host: %s", host)
|
a.Error(errorMessage)
|
||||||
|
a.ToMainMenu(errorMessage)
|
||||||
|
} else {
|
||||||
|
game, err := d2gamescreen.CreateGame(
|
||||||
|
a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, *a.Options.LogLevel, a.guiManager,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
a.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
a.screen.SetNextScreen(d2gamescreen.CreateGame(a, a.asset, a.ui, a.renderer, a.inputManager,
|
a.screen.SetNextScreen(game)
|
||||||
a.audio, gameClient, a.terminal, a.guiManager))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToCharacterSelect forces the game to transition to the Character Select (load character) screen
|
// ToCharacterSelect forces the game to transition to the Character Select (load character) screen
|
||||||
func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) {
|
func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) {
|
||||||
// https://github.com/OpenDiablo2/OpenDiablo2/issues/790
|
characterSelect, err := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager,
|
||||||
characterSelect := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager,
|
a.audio, a.ui, connType, *a.Options.LogLevel, connHost)
|
||||||
a.audio, a.ui, connType, connHost)
|
if err != nil {
|
||||||
|
a.Errorf("unable to create character select screen: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
a.screen.SetNextScreen(characterSelect)
|
a.screen.SetNextScreen(characterSelect)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMapEngineTest forces the game to transition to the map engine test screen
|
// ToMapEngineTest forces the game to transition to the map engine test screen
|
||||||
func (a *App) ToMapEngineTest(region, level int) {
|
func (a *App) ToMapEngineTest(region, level int) {
|
||||||
met, err := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer, a.inputManager, a.audio, a.screen)
|
met, err := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer, a.inputManager, a.audio,
|
||||||
|
*a.Options.LogLevel, a.screen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
a.Error(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -646,5 +670,10 @@ func (a *App) ToMapEngineTest(region, level int) {
|
|||||||
|
|
||||||
// ToCredits forces the game to transition to the credits screen
|
// ToCredits forces the game to transition to the credits screen
|
||||||
func (a *App) ToCredits() {
|
func (a *App) ToCredits() {
|
||||||
a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, a.ui))
|
a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, *a.Options.LogLevel, a.ui))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToCinematics forces the game to transition to the cinematics menu
|
||||||
|
func (a *App) ToCinematics() {
|
||||||
|
a.screen.SetNextScreen(d2gamescreen.CreateCinematics(a, a.asset, a.renderer, a.audio, *a.Options.LogLevel, a.ui))
|
||||||
}
|
}
|
||||||
|
139
d2app/console_commands.go
Normal file
139
d2app/console_commands.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package d2app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime/pprof"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2gamescreen"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *App) initTerminalCommands() {
|
||||||
|
terminalCommands := []struct {
|
||||||
|
name string
|
||||||
|
desc string
|
||||||
|
args []string
|
||||||
|
fn func(args []string) error
|
||||||
|
}{
|
||||||
|
{"dumpheap", "dumps the heap to pprof/heap.pprof", nil, a.dumpHeap},
|
||||||
|
{"fullscreen", "toggles fullscreen", nil, a.toggleFullScreen},
|
||||||
|
{"capframe", "captures a still frame", []string{"filename"}, a.setupCaptureFrame},
|
||||||
|
{"capgifstart", "captures an animation (start)", []string{"filename"}, a.startAnimationCapture},
|
||||||
|
{"capgifstop", "captures an animation (stop)", nil, a.stopAnimationCapture},
|
||||||
|
{"vsync", "toggles vsync", nil, a.toggleVsync},
|
||||||
|
{"fps", "toggle fps counter", nil, a.toggleFpsCounter},
|
||||||
|
{"timescale", "set scalar for elapsed time", []string{"float"}, a.setTimeScale},
|
||||||
|
{"quit", "exits the game", nil, a.quitGame},
|
||||||
|
{"screen-gui", "enters the gui playground screen", nil, a.enterGuiPlayground},
|
||||||
|
{"js", "eval JS scripts", []string{"code"}, a.evalJS},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmd := range terminalCommands {
|
||||||
|
if err := a.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil {
|
||||||
|
a.Fatalf("failed to bind action %q: %v", cmd.name, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) dumpHeap([]string) error {
|
||||||
|
if _, err := os.Stat("./pprof/"); os.IsNotExist(err) {
|
||||||
|
if err := os.Mkdir("./pprof/", 0750); err != nil {
|
||||||
|
a.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileOut, err := os.Create("./pprof/heap.pprof")
|
||||||
|
if err != nil {
|
||||||
|
a.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pprof.WriteHeapProfile(fileOut); err != nil {
|
||||||
|
a.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fileOut.Close(); err != nil {
|
||||||
|
a.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) evalJS(args []string) error {
|
||||||
|
val, err := a.scriptEngine.Eval(args[0])
|
||||||
|
if err != nil {
|
||||||
|
a.terminal.Errorf(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Info("%s" + val)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) toggleFullScreen([]string) error {
|
||||||
|
fullscreen := !a.renderer.IsFullScreen()
|
||||||
|
a.renderer.SetFullScreen(fullscreen)
|
||||||
|
a.terminal.Infof("fullscreen is now: %v", fullscreen)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) setupCaptureFrame(args []string) error {
|
||||||
|
a.captureState = captureStateFrame
|
||||||
|
a.capturePath = args[0]
|
||||||
|
a.captureFrames = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) startAnimationCapture(args []string) error {
|
||||||
|
a.captureState = captureStateGif
|
||||||
|
a.capturePath = args[0]
|
||||||
|
a.captureFrames = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) stopAnimationCapture([]string) error {
|
||||||
|
a.captureState = captureStateNone
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) toggleVsync([]string) error {
|
||||||
|
vsync := !a.renderer.GetVSyncEnabled()
|
||||||
|
a.renderer.SetVSyncEnabled(vsync)
|
||||||
|
a.terminal.Infof("vsync is now: %v", vsync)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) toggleFpsCounter([]string) error {
|
||||||
|
a.showFPS = !a.showFPS
|
||||||
|
a.terminal.Infof("fps counter is now: %v", a.showFPS)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) setTimeScale(args []string) error {
|
||||||
|
timeScale, err := strconv.ParseFloat(args[0], 64)
|
||||||
|
if err != nil || timeScale <= 0 {
|
||||||
|
a.terminal.Errorf("invalid time scale value")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.terminal.Infof("timescale changed from %f to %f", a.timeScale, timeScale)
|
||||||
|
a.timeScale = timeScale
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) quitGame([]string) error {
|
||||||
|
os.Exit(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) enterGuiPlayground([]string) error {
|
||||||
|
a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, *a.Options.LogLevel, a.asset))
|
||||||
|
return nil
|
||||||
|
}
|
180
d2app/initialization.go
Normal file
180
d2app/initialization.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package d2app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2animdata"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *App) initialize() error {
|
||||||
|
if err := a.initConfig(a.config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.initLanguage()
|
||||||
|
|
||||||
|
if err := a.initDataDictionaries(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.timeScale = 1.0
|
||||||
|
a.lastTime = d2util.Now()
|
||||||
|
a.lastScreenAdvance = a.lastTime
|
||||||
|
|
||||||
|
a.renderer.SetWindowIcon("d2logo.png")
|
||||||
|
a.terminal.BindLogger()
|
||||||
|
a.initTerminalCommands()
|
||||||
|
|
||||||
|
gui, err := d2gui.CreateGuiManager(a.asset, *a.Options.LogLevel, a.inputManager)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.guiManager = gui
|
||||||
|
|
||||||
|
a.screen = d2screen.NewScreenManager(a.ui, *a.Options.LogLevel, a.guiManager)
|
||||||
|
|
||||||
|
a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume)
|
||||||
|
|
||||||
|
if err := a.loadStrings(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.ui.Initialize()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
fmtErrSourceNotFound = `file not found: %q
|
||||||
|
|
||||||
|
Please check your config file at %q
|
||||||
|
|
||||||
|
Also, verify that the MPQ files exist at %q
|
||||||
|
|
||||||
|
Capitalization in the file name matters.
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *App) initConfig(config *d2config.Configuration) error {
|
||||||
|
a.config = config
|
||||||
|
|
||||||
|
for _, mpqName := range a.config.MpqLoadOrder {
|
||||||
|
cleanDir := filepath.Clean(a.config.MpqPath)
|
||||||
|
srcPath := filepath.Join(cleanDir, mpqName)
|
||||||
|
|
||||||
|
err := a.asset.AddSource(srcPath, types.AssetSourceMPQ)
|
||||||
|
if err != nil {
|
||||||
|
// nolint:stylecheck // we want a multiline error message here..
|
||||||
|
return fmt.Errorf(fmtErrSourceNotFound, srcPath, a.config.Path(), a.config.MpqPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) initLanguage() {
|
||||||
|
a.language = a.asset.LoadLanguage(d2resource.LocalLanguage)
|
||||||
|
a.asset.Loader.SetLanguage(&a.language)
|
||||||
|
|
||||||
|
a.charset = d2resource.GetFontCharset(a.language)
|
||||||
|
a.asset.Loader.SetCharset(&a.charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) initDataDictionaries() error {
|
||||||
|
dictPaths := []string{
|
||||||
|
d2resource.LevelType, d2resource.LevelPreset, d2resource.LevelWarp,
|
||||||
|
d2resource.ObjectType, d2resource.ObjectDetails, d2resource.Weapons,
|
||||||
|
d2resource.Armor, d2resource.Misc, d2resource.Books, d2resource.ItemTypes,
|
||||||
|
d2resource.UniqueItems, d2resource.Missiles, d2resource.SoundSettings,
|
||||||
|
d2resource.MonStats, d2resource.MonStats2, d2resource.MonPreset,
|
||||||
|
d2resource.MonProp, d2resource.MonType, d2resource.MonMode,
|
||||||
|
d2resource.MagicPrefix, d2resource.MagicSuffix, d2resource.ItemStatCost,
|
||||||
|
d2resource.ItemRatio, d2resource.StorePage, d2resource.Overlays,
|
||||||
|
d2resource.CharStats, d2resource.Hireling, d2resource.Experience,
|
||||||
|
d2resource.Gems, d2resource.QualityItems, d2resource.Runes,
|
||||||
|
d2resource.DifficultyLevels, d2resource.AutoMap, d2resource.LevelDetails,
|
||||||
|
d2resource.LevelMaze, d2resource.LevelSubstitutions, d2resource.CubeRecipes,
|
||||||
|
d2resource.SuperUniques, d2resource.Inventory, d2resource.Skills,
|
||||||
|
d2resource.SkillCalc, d2resource.MissileCalc, d2resource.Properties,
|
||||||
|
d2resource.SkillDesc, d2resource.BodyLocations, d2resource.Sets,
|
||||||
|
d2resource.SetItems, d2resource.AutoMagic, d2resource.TreasureClass,
|
||||||
|
d2resource.TreasureClassEx, d2resource.States, d2resource.SoundEnvirons,
|
||||||
|
d2resource.Shrines, d2resource.ElemType, d2resource.PlrMode,
|
||||||
|
d2resource.PetType, d2resource.NPC, d2resource.MonsterUniqueModifier,
|
||||||
|
d2resource.MonsterEquipment, d2resource.UniqueAppellation, d2resource.MonsterLevel,
|
||||||
|
d2resource.MonsterSound, d2resource.MonsterSequence, d2resource.PlayerClass,
|
||||||
|
d2resource.MonsterPlacement, d2resource.ObjectGroup, d2resource.CompCode,
|
||||||
|
d2resource.MonsterAI, d2resource.RarePrefix, d2resource.RareSuffix,
|
||||||
|
d2resource.Events, d2resource.Colors, d2resource.ArmorType,
|
||||||
|
d2resource.WeaponClass, d2resource.PlayerType, d2resource.Composite,
|
||||||
|
d2resource.HitClass, d2resource.UniquePrefix, d2resource.UniqueSuffix,
|
||||||
|
d2resource.CubeModifier, d2resource.CubeType, d2resource.HirelingDescription,
|
||||||
|
d2resource.LowQualityItems,
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Info("Initializing asset manager")
|
||||||
|
|
||||||
|
for _, path := range dictPaths {
|
||||||
|
err := a.asset.LoadRecords(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.initAnimationData(d2resource.AnimationData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
fmtLoadAnimData = "loading animation data from: %s"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *App) initAnimationData(path string) error {
|
||||||
|
animDataBytes, err := a.asset.LoadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Debugf(fmtLoadAnimData, path)
|
||||||
|
|
||||||
|
animData, err := d2animdata.Load(animDataBytes)
|
||||||
|
if err != nil {
|
||||||
|
a.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Infof("Loaded %d animation data records", animData.GetRecordsCount())
|
||||||
|
|
||||||
|
a.asset.Records.Animation.Data = animData
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) loadStrings() error {
|
||||||
|
tablePaths := []string{
|
||||||
|
d2resource.PatchStringTable,
|
||||||
|
d2resource.ExpansionStringTable,
|
||||||
|
d2resource.StringTable,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tablePath := range tablePaths {
|
||||||
|
_, err := a.asset.LoadStringTable(tablePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
93
d2common/d2cache/cache_test.go
Normal file
93
d2common/d2cache/cache_test.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package d2cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCacheInsert(t *testing.T) {
|
||||||
|
cache := CreateCache(1)
|
||||||
|
insertError := cache.Insert("A", "", 1)
|
||||||
|
|
||||||
|
if insertError != nil {
|
||||||
|
t.Fatalf("Cache insert resulted in unexpected error: %s", insertError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheInsertWithinBudget(t *testing.T) {
|
||||||
|
cache := CreateCache(1)
|
||||||
|
insertError := cache.Insert("A", "", 2)
|
||||||
|
|
||||||
|
if insertError != nil {
|
||||||
|
t.Fatalf("Cache insert resulted in unexpected error: %s", insertError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheInsertUpdatesWeight(t *testing.T) {
|
||||||
|
cache := CreateCache(2)
|
||||||
|
_ = cache.Insert("A", "", 1)
|
||||||
|
_ = cache.Insert("B", "", 1)
|
||||||
|
_ = cache.Insert("budget_exceeded", "", 1)
|
||||||
|
|
||||||
|
if cache.GetWeight() != 2 {
|
||||||
|
t.Fatal("Cache with budget 2 did not correctly set weight after evicting one of three nodes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheInsertDuplicateRejected(t *testing.T) {
|
||||||
|
cache := CreateCache(2)
|
||||||
|
_ = cache.Insert("dupe", "", 1)
|
||||||
|
dupeError := cache.Insert("dupe", "", 1)
|
||||||
|
|
||||||
|
if dupeError == nil {
|
||||||
|
t.Fatal("Cache insert of duplicate key did not result in any err")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheInsertEvictsLeastRecentlyUsed(t *testing.T) {
|
||||||
|
cache := CreateCache(2)
|
||||||
|
// with a budget of 2, inserting 3 keys should evict the last
|
||||||
|
_ = cache.Insert("evicted", "", 1)
|
||||||
|
_ = cache.Insert("A", "", 1)
|
||||||
|
_ = cache.Insert("B", "", 1)
|
||||||
|
|
||||||
|
_, foundEvicted := cache.Retrieve("evicted")
|
||||||
|
if foundEvicted {
|
||||||
|
t.Fatal("Cache insert did not trigger eviction after weight exceedance")
|
||||||
|
}
|
||||||
|
|
||||||
|
// double check that only 1 one was evicted and not any extra
|
||||||
|
_, foundA := cache.Retrieve("A")
|
||||||
|
_, foundB := cache.Retrieve("B")
|
||||||
|
|
||||||
|
if !foundA || !foundB {
|
||||||
|
t.Fatal("Cache insert evicted more than necessary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheInsertEvictsLeastRecentlyRetrieved(t *testing.T) {
|
||||||
|
cache := CreateCache(2)
|
||||||
|
_ = cache.Insert("A", "", 1)
|
||||||
|
_ = cache.Insert("evicted", "", 1)
|
||||||
|
|
||||||
|
// retrieve the oldest node, promoting it head so it is not evicted
|
||||||
|
cache.Retrieve("A")
|
||||||
|
|
||||||
|
// insert once more, exceeding weight capacity
|
||||||
|
_ = cache.Insert("B", "", 1)
|
||||||
|
// now the least recently used key should be evicted
|
||||||
|
_, foundEvicted := cache.Retrieve("evicted")
|
||||||
|
if foundEvicted {
|
||||||
|
t.Fatal("Cache insert did not evict least recently used after weight exceedance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClear(t *testing.T) {
|
||||||
|
cache := CreateCache(1)
|
||||||
|
_ = cache.Insert("cleared", "", 1)
|
||||||
|
cache.Clear()
|
||||||
|
_, found := cache.Retrieve("cleared")
|
||||||
|
|
||||||
|
if found {
|
||||||
|
t.Fatal("Still able to retrieve nodes after cache was cleared")
|
||||||
|
}
|
||||||
|
}
|
@ -1,58 +0,0 @@
|
|||||||
package d2data
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
numCofNameBytes = 8
|
|
||||||
numFlagBytes = 144
|
|
||||||
)
|
|
||||||
|
|
||||||
// AnimationDataRecord represents a single entry in the animation data dictionary file
|
|
||||||
type AnimationDataRecord struct {
|
|
||||||
// COFName is the name of the COF file used for this animation
|
|
||||||
COFName string
|
|
||||||
// FramesPerDirection specifies how many frames are in each direction
|
|
||||||
FramesPerDirection int
|
|
||||||
// AnimationSpeed represents a value of X where the rate is a ration of (x/255) at 25FPS
|
|
||||||
AnimationSpeed int
|
|
||||||
// Flags are used in keyframe triggers
|
|
||||||
Flags []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnimationData represents all of the animation data records, mapped by the COF index
|
|
||||||
type AnimationData map[string][]*AnimationDataRecord
|
|
||||||
|
|
||||||
// LoadAnimationData loads the animation data table into the global AnimationData dictionary
|
|
||||||
func LoadAnimationData(rawData []byte) AnimationData {
|
|
||||||
animdata := make(AnimationData)
|
|
||||||
streamReader := d2datautils.CreateStreamReader(rawData)
|
|
||||||
|
|
||||||
for !streamReader.EOF() {
|
|
||||||
dataCount := int(streamReader.GetInt32())
|
|
||||||
for i := 0; i < dataCount; i++ {
|
|
||||||
cofNameBytes := streamReader.ReadBytes(numCofNameBytes)
|
|
||||||
data := &AnimationDataRecord{
|
|
||||||
COFName: strings.ReplaceAll(string(cofNameBytes), string(byte(0)), ""),
|
|
||||||
FramesPerDirection: int(streamReader.GetInt32()),
|
|
||||||
AnimationSpeed: int(streamReader.GetInt32()),
|
|
||||||
}
|
|
||||||
data.Flags = streamReader.ReadBytes(numFlagBytes)
|
|
||||||
cofIndex := strings.ToLower(data.COFName)
|
|
||||||
|
|
||||||
if _, found := animdata[cofIndex]; !found {
|
|
||||||
animdata[cofIndex] = make([]*AnimationDataRecord, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
animdata[cofIndex] = append(animdata[cofIndex], data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Loaded %d animation data records", len(animdata))
|
|
||||||
|
|
||||||
return animdata
|
|
||||||
}
|
|
@ -401,10 +401,10 @@ Loop:
|
|||||||
case 257:
|
case 257:
|
||||||
newvalue := bitstream.ReadBits(8)
|
newvalue := bitstream.ReadBits(8)
|
||||||
|
|
||||||
outputstream.PushByte(byte(newvalue))
|
outputstream.PushBytes(byte(newvalue))
|
||||||
tail = insertNode(tail, newvalue)
|
tail = insertNode(tail, newvalue)
|
||||||
default:
|
default:
|
||||||
outputstream.PushByte(byte(decoded))
|
outputstream.PushBytes(byte(decoded))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
// WavDecompress decompresses wav files
|
// WavDecompress decompresses wav files
|
||||||
//nolint:gomnd // binary decode magic
|
//nolint:gomnd // binary decode magic
|
||||||
func WavDecompress(data []byte, channelCount int) []byte { //nolint:funlen,gocognit,gocyclo // can't reduce
|
func WavDecompress(data []byte, channelCount int) ([]byte, error) { //nolint:funlen,gocognit,gocyclo // can't reduce
|
||||||
Array1 := []int{0x2c, 0x2c}
|
Array1 := []int{0x2c, 0x2c}
|
||||||
Array2 := make([]int, channelCount)
|
Array2 := make([]int, channelCount)
|
||||||
|
|
||||||
@ -35,20 +35,33 @@ func WavDecompress(data []byte, channelCount int) []byte { //nolint:funlen,gocog
|
|||||||
input := d2datautils.CreateStreamReader(data)
|
input := d2datautils.CreateStreamReader(data)
|
||||||
output := d2datautils.CreateStreamWriter()
|
output := d2datautils.CreateStreamWriter()
|
||||||
|
|
||||||
input.GetByte()
|
_, err := input.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
shift := input.GetByte()
|
shift, err := input.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < channelCount; i++ {
|
for i := 0; i < channelCount; i++ {
|
||||||
temp := input.GetInt16()
|
temp, err := input.ReadInt16()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
Array2[i] = int(temp)
|
Array2[i] = int(temp)
|
||||||
output.PushInt16(temp)
|
output.PushInt16(temp)
|
||||||
}
|
}
|
||||||
|
|
||||||
channel := channelCount - 1
|
channel := channelCount - 1
|
||||||
|
|
||||||
for input.GetPosition() < input.GetSize() {
|
for input.Position() < input.Size() {
|
||||||
value := input.GetByte()
|
value, err := input.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if channelCount == 2 {
|
if channelCount == 2 {
|
||||||
channel = 1 - channel
|
channel = 1 - channel
|
||||||
@ -129,5 +142,5 @@ func WavDecompress(data []byte, channelCount int) []byte { //nolint:funlen,gocog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return output.GetBytes()
|
return output.GetBytes(), nil
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package d2video
|
package d2video
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
||||||
@ -29,6 +30,12 @@ const (
|
|||||||
BinkVideoModeWidthAndHeightInterlaced
|
BinkVideoModeWidthAndHeightInterlaced
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
numHeaderBytes = 3
|
||||||
|
bikHeaderStr = "BIK"
|
||||||
|
numAudioTrackUnknownBytes = 2
|
||||||
|
)
|
||||||
|
|
||||||
// BinkAudioAlgorithm represents the type of bink audio algorithm
|
// BinkAudioAlgorithm represents the type of bink audio algorithm
|
||||||
type BinkAudioAlgorithm uint32
|
type BinkAudioAlgorithm uint32
|
||||||
|
|
||||||
@ -72,75 +79,157 @@ type BinkDecoder struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateBinkDecoder returns a new instance of the bink decoder
|
// CreateBinkDecoder returns a new instance of the bink decoder
|
||||||
func CreateBinkDecoder(source []byte) *BinkDecoder {
|
func CreateBinkDecoder(source []byte) (*BinkDecoder, error) {
|
||||||
result := &BinkDecoder{
|
result := &BinkDecoder{
|
||||||
streamReader: d2datautils.CreateStreamReader(source),
|
streamReader: d2datautils.CreateStreamReader(source),
|
||||||
}
|
}
|
||||||
|
|
||||||
result.loadHeaderInformation()
|
err := result.loadHeaderInformation()
|
||||||
|
|
||||||
return result
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNextFrame gets the next frame
|
// GetNextFrame gets the next frame
|
||||||
func (v *BinkDecoder) GetNextFrame() {
|
func (v *BinkDecoder) GetNextFrame() error {
|
||||||
//nolint:gocritic // v.streamReader.SetPosition(uint64(v.FrameIndexTable[i] & 0xFFFFFFFE))
|
//nolint:gocritic // v.streamReader.SetPosition(uint64(v.FrameIndexTable[i] & 0xFFFFFFFE))
|
||||||
lengthOfAudioPackets := v.streamReader.GetUInt32() - 4 //nolint:gomnd // decode magic
|
lengthOfAudioPackets, err := v.streamReader.ReadUInt32()
|
||||||
samplesInPacket := v.streamReader.GetUInt32()
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
v.streamReader.SkipBytes(int(lengthOfAudioPackets))
|
samplesInPacket, err := v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.streamReader.SkipBytes(int(lengthOfAudioPackets) - 4) //nolint:gomnd // decode magic
|
||||||
|
|
||||||
log.Printf("Frame %d:\tSamp: %d", v.frameIndex, samplesInPacket)
|
log.Printf("Frame %d:\tSamp: %d", v.frameIndex, samplesInPacket)
|
||||||
|
|
||||||
v.frameIndex++
|
v.frameIndex++
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gomnd // Decoder magic
|
//nolint:gomnd,funlen,gocyclo // Decoder magic, can't help the long function length for now
|
||||||
func (v *BinkDecoder) loadHeaderInformation() {
|
func (v *BinkDecoder) loadHeaderInformation() error {
|
||||||
v.streamReader.SetPosition(0)
|
v.streamReader.SetPosition(0)
|
||||||
headerBytes := v.streamReader.ReadBytes(3)
|
|
||||||
|
|
||||||
if string(headerBytes) != "BIK" {
|
var err error
|
||||||
log.Fatal("Invalid header for bink video")
|
|
||||||
|
headerBytes, err := v.streamReader.ReadBytes(numHeaderBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(headerBytes) != bikHeaderStr {
|
||||||
|
return errors.New("invalid header for bink video")
|
||||||
|
}
|
||||||
|
|
||||||
|
v.videoCodecRevision, err = v.streamReader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.fileSize, err = v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.numberOfFrames, err = v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.largestFrameSizeBytes, err = v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const numBytesToSkip = 4 // Number of frames again?
|
||||||
|
|
||||||
|
v.streamReader.SkipBytes(numBytesToSkip)
|
||||||
|
|
||||||
|
v.VideoWidth, err = v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.VideoHeight, err = v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsDividend, err := v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsDivider, err := v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
v.videoCodecRevision = v.streamReader.GetByte()
|
|
||||||
v.fileSize = v.streamReader.GetUInt32()
|
|
||||||
v.numberOfFrames = v.streamReader.GetUInt32()
|
|
||||||
v.largestFrameSizeBytes = v.streamReader.GetUInt32()
|
|
||||||
v.streamReader.SkipBytes(4) // Number of frames again?
|
|
||||||
v.VideoWidth = v.streamReader.GetUInt32()
|
|
||||||
v.VideoHeight = v.streamReader.GetUInt32()
|
|
||||||
fpsDividend := v.streamReader.GetUInt32()
|
|
||||||
fpsDivider := v.streamReader.GetUInt32()
|
|
||||||
v.FPS = uint32(float32(fpsDividend) / float32(fpsDivider))
|
v.FPS = uint32(float32(fpsDividend) / float32(fpsDivider))
|
||||||
v.FrameTimeMS = 1000 / v.FPS
|
v.FrameTimeMS = 1000 / v.FPS
|
||||||
videoFlags := v.streamReader.GetUInt32()
|
|
||||||
|
videoFlags, err := v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
v.VideoMode = BinkVideoMode((videoFlags >> 28) & 0x0F)
|
v.VideoMode = BinkVideoMode((videoFlags >> 28) & 0x0F)
|
||||||
v.HasAlphaPlane = ((videoFlags >> 20) & 0x1) == 1
|
v.HasAlphaPlane = ((videoFlags >> 20) & 0x1) == 1
|
||||||
v.Grayscale = ((videoFlags >> 17) & 0x1) == 1
|
v.Grayscale = ((videoFlags >> 17) & 0x1) == 1
|
||||||
numberOfAudioTracks := v.streamReader.GetUInt32()
|
|
||||||
|
numberOfAudioTracks, err := v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
v.AudioTracks = make([]BinkAudioTrack, numberOfAudioTracks)
|
v.AudioTracks = make([]BinkAudioTrack, numberOfAudioTracks)
|
||||||
|
|
||||||
for i := 0; i < int(numberOfAudioTracks); i++ {
|
for i := 0; i < int(numberOfAudioTracks); i++ {
|
||||||
v.streamReader.SkipBytes(2) // Unknown
|
v.streamReader.SkipBytes(numAudioTrackUnknownBytes)
|
||||||
v.AudioTracks[i].AudioChannels = v.streamReader.GetUInt16()
|
|
||||||
|
v.AudioTracks[i].AudioChannels, err = v.streamReader.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < int(numberOfAudioTracks); i++ {
|
for i := 0; i < int(numberOfAudioTracks); i++ {
|
||||||
v.AudioTracks[i].AudioSampleRateHz = v.streamReader.GetUInt16()
|
v.AudioTracks[i].AudioSampleRateHz, err = v.streamReader.ReadUInt16()
|
||||||
flags := v.streamReader.GetUInt16()
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags uint16
|
||||||
|
|
||||||
|
flags, err = v.streamReader.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
v.AudioTracks[i].Stereo = ((flags >> 13) & 0x1) == 1
|
v.AudioTracks[i].Stereo = ((flags >> 13) & 0x1) == 1
|
||||||
v.AudioTracks[i].Algorithm = BinkAudioAlgorithm((flags >> 12) & 0x1)
|
v.AudioTracks[i].Algorithm = BinkAudioAlgorithm((flags >> 12) & 0x1)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < int(numberOfAudioTracks); i++ {
|
for i := 0; i < int(numberOfAudioTracks); i++ {
|
||||||
v.AudioTracks[i].AudioTrackID = v.streamReader.GetUInt32()
|
v.AudioTracks[i].AudioTrackID, err = v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
v.FrameIndexTable = make([]uint32, v.numberOfFrames+1)
|
v.FrameIndexTable = make([]uint32, v.numberOfFrames+1)
|
||||||
|
|
||||||
for i := 0; i < int(v.numberOfFrames+1); i++ {
|
for i := 0; i < int(v.numberOfFrames+1); i++ {
|
||||||
v.FrameIndexTable[i] = v.streamReader.GetUInt32()
|
v.FrameIndexTable[i], err = v.streamReader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
107
d2common/d2datautils/bitmuncher_test.go
Normal file
107
d2common/d2datautils/bitmuncher_test.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package d2datautils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testData = []byte{33, 23, 4, 33, 192, 243} //nolint:gochecknoglobals // just a test
|
||||||
|
|
||||||
|
func TestBitmuncherCopy(t *testing.T) {
|
||||||
|
bm1 := CreateBitMuncher(testData, 0)
|
||||||
|
bm2 := CopyBitMuncher(bm1)
|
||||||
|
|
||||||
|
for i := range bm1.data {
|
||||||
|
assert.Equal(t, bm1.data[i], bm2.data[i], "original bitmuncher isn't equal to copied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherSetOffset(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
bm.SetOffset(5)
|
||||||
|
|
||||||
|
assert.Equal(t, bm.Offset(), 5, "Set Offset method didn't set offset to expected number")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherSteBitsRead(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
bm.SetBitsRead(8)
|
||||||
|
|
||||||
|
assert.Equal(t, bm.BitsRead(), 8, "Set bits read method didn't set bits read to expected value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherReadBit(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
|
||||||
|
var result byte
|
||||||
|
|
||||||
|
for i := 0; i < bitsPerByte; i++ {
|
||||||
|
v := bm.GetBit()
|
||||||
|
result |= byte(v) << byte(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, result, testData[0], "result of rpeated 8 times get bit didn't return expected byte")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherGetBits(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
|
||||||
|
assert.Equal(t, byte(bm.GetBits(bitsPerByte)), testData[0], "get bits didn't return expected value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherGetNoBits(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
|
||||||
|
assert.Equal(t, bm.GetBits(0), uint32(0), "get bits didn't return expected value: 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherGetSignedBits(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
|
||||||
|
assert.Equal(t, bm.GetSignedBits(6), -31, "get signed bits didn't return expected value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherGetNoSignedBits(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
|
||||||
|
assert.Equal(t, bm.GetSignedBits(0), 0, "get signed bits didn't return expected value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherGetOneSignedBit(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
|
||||||
|
assert.Equal(t, bm.GetSignedBits(1), -1, "get signed bits didn't return expected value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherSkipBits(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
|
||||||
|
bm.SkipBits(bitsPerByte)
|
||||||
|
|
||||||
|
assert.Equal(t, bm.GetByte(), testData[1], "skipping 8 bits didn't moved bit muncher's position into next byte")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherGetInt32(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
|
||||||
|
var testInt int32
|
||||||
|
|
||||||
|
for i := 0; i < bytesPerint32; i++ {
|
||||||
|
testInt |= int32(testData[i]) << int32(bitsPerByte*i)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, bm.GetInt32(), testInt, "int32 value wasn't returned properly")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmuncherGetUint32(t *testing.T) {
|
||||||
|
bm := CreateBitMuncher(testData, 0)
|
||||||
|
|
||||||
|
var testUint uint32
|
||||||
|
|
||||||
|
for i := 0; i < bytesPerint32; i++ {
|
||||||
|
testUint |= uint32(testData[i]) << uint32(bitsPerByte*i)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, bm.GetUInt32(), testUint, "uint32 value wasn't returned properly")
|
||||||
|
}
|
@ -39,6 +39,7 @@ func (v *BitStream) ReadBits(bitCount int) int {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gomnd // byte expresion
|
||||||
result := v.current & (0xffff >> uint(maxBits-bitCount))
|
result := v.current & (0xffff >> uint(maxBits-bitCount))
|
||||||
v.WasteBits(bitCount)
|
v.WasteBits(bitCount)
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ func (v *BitStream) PeekByte() int {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gomnd // byte
|
||||||
return v.current & 0xff
|
return v.current & 0xff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,9 +5,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
bytesPerInt16 = 2
|
bytesPerint16 = 2
|
||||||
bytesPerInt32 = 4
|
bytesPerint32 = 4
|
||||||
bytesPerInt64 = 8
|
bytesPerint64 = 8
|
||||||
)
|
)
|
||||||
|
|
||||||
// StreamReader allows you to read data from a byte array in various formats
|
// StreamReader allows you to read data from a byte array in various formats
|
||||||
@ -26,50 +26,72 @@ func CreateStreamReader(source []byte) *StreamReader {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPosition returns the current stream position
|
// ReadByte reads a byte from the stream
|
||||||
func (v *StreamReader) GetPosition() uint64 {
|
func (v *StreamReader) ReadByte() (byte, error) {
|
||||||
return v.position
|
if v.position >= v.Size() {
|
||||||
}
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
// GetSize returns the total size of the stream in bytes
|
|
||||||
func (v *StreamReader) GetSize() uint64 {
|
|
||||||
return uint64(len(v.data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByte returns a byte from the stream
|
|
||||||
func (v *StreamReader) GetByte() byte {
|
|
||||||
result := v.data[v.position]
|
result := v.data[v.position]
|
||||||
v.position++
|
v.position++
|
||||||
|
|
||||||
return result
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUInt16 returns a uint16 word from the stream
|
// ReadInt16 returns a int16 word from the stream
|
||||||
func (v *StreamReader) GetUInt16() uint16 {
|
func (v *StreamReader) ReadInt16() (int16, error) {
|
||||||
var result uint16
|
b, err := v.ReadUInt16()
|
||||||
|
return int16(b), err
|
||||||
for offset := uint64(0); offset < bytesPerInt16; offset++ {
|
|
||||||
shift := uint8(bitsPerByte * offset)
|
|
||||||
result += uint16(v.data[v.position+offset]) << shift
|
|
||||||
}
|
|
||||||
|
|
||||||
v.position += bytesPerInt16
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt16 returns a int16 word from the stream
|
// ReadUInt16 returns a uint16 word from the stream
|
||||||
func (v *StreamReader) GetInt16() int16 {
|
func (v *StreamReader) ReadUInt16() (uint16, error) {
|
||||||
var result int16
|
b, err := v.ReadBytes(bytesPerint16)
|
||||||
|
if err != nil {
|
||||||
for offset := uint64(0); offset < bytesPerInt16; offset++ {
|
return 0, err
|
||||||
shift := uint8(bitsPerByte * offset)
|
|
||||||
result += int16(v.data[v.position+offset]) << shift
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v.position += bytesPerInt16
|
return uint16(b[0]) | uint16(b[1])<<8, err
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
// ReadInt32 returns an int32 dword from the stream
|
||||||
|
func (v *StreamReader) ReadInt32() (int32, error) {
|
||||||
|
b, err := v.ReadUInt32()
|
||||||
|
return int32(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadUInt32 returns a uint32 dword from the stream
|
||||||
|
//nolint
|
||||||
|
func (v *StreamReader) ReadUInt32() (uint32, error) {
|
||||||
|
b, err := v.ReadBytes(bytesPerint32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadInt64 returns a uint64 qword from the stream
|
||||||
|
func (v *StreamReader) ReadInt64() (int64, error) {
|
||||||
|
b, err := v.ReadUInt64()
|
||||||
|
return int64(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadUInt64 returns a uint64 qword from the stream
|
||||||
|
//nolint
|
||||||
|
func (v *StreamReader) ReadUInt64() (uint64, error) {
|
||||||
|
b, err := v.ReadBytes(bytesPerint64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
|
||||||
|
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position returns the current stream position
|
||||||
|
func (v *StreamReader) Position() uint64 {
|
||||||
|
return v.position
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPosition sets the stream position with the given position
|
// SetPosition sets the stream position with the given position
|
||||||
@ -77,64 +99,26 @@ func (v *StreamReader) SetPosition(newPosition uint64) {
|
|||||||
v.position = newPosition
|
v.position = newPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUInt32 returns a uint32 dword from the stream
|
// Size returns the total size of the stream in bytes
|
||||||
func (v *StreamReader) GetUInt32() uint32 {
|
func (v *StreamReader) Size() uint64 {
|
||||||
var result uint32
|
return uint64(len(v.data))
|
||||||
|
|
||||||
for offset := uint64(0); offset < bytesPerInt32; offset++ {
|
|
||||||
shift := uint8(bitsPerByte * offset)
|
|
||||||
result += uint32(v.data[v.position+offset]) << shift
|
|
||||||
}
|
|
||||||
|
|
||||||
v.position += bytesPerInt32
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInt32 returns an int32 dword from the stream
|
|
||||||
func (v *StreamReader) GetInt32() int32 {
|
|
||||||
var result int32
|
|
||||||
|
|
||||||
for offset := uint64(0); offset < bytesPerInt32; offset++ {
|
|
||||||
shift := uint8(bitsPerByte * offset)
|
|
||||||
result += int32(v.data[v.position+offset]) << shift
|
|
||||||
}
|
|
||||||
|
|
||||||
v.position += bytesPerInt32
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUint64 returns a uint64 qword from the stream
|
|
||||||
func (v *StreamReader) GetUint64() uint64 {
|
|
||||||
var result uint64
|
|
||||||
|
|
||||||
for offset := uint64(0); offset < bytesPerInt64; offset++ {
|
|
||||||
shift := uint8(bitsPerByte * offset)
|
|
||||||
result += uint64(v.data[v.position+offset]) << shift
|
|
||||||
}
|
|
||||||
|
|
||||||
v.position += bytesPerInt64
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInt64 returns a uint64 qword from the stream
|
|
||||||
func (v *StreamReader) GetInt64() int64 {
|
|
||||||
return int64(v.GetUint64())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadByte implements io.ByteReader
|
|
||||||
func (v *StreamReader) ReadByte() (byte, error) {
|
|
||||||
return v.GetByte(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadBytes reads multiple bytes
|
// ReadBytes reads multiple bytes
|
||||||
func (v *StreamReader) ReadBytes(count int) []byte {
|
func (v *StreamReader) ReadBytes(count int) ([]byte, error) {
|
||||||
|
if count <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
size := v.Size()
|
||||||
|
if v.position >= size || v.position+uint64(count) > size {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
result := v.data[v.position : v.position+uint64(count)]
|
result := v.data[v.position : v.position+uint64(count)]
|
||||||
v.position += uint64(count)
|
v.position += uint64(count)
|
||||||
|
|
||||||
return result
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SkipBytes moves the stream position forward by the given amount
|
// SkipBytes moves the stream position forward by the given amount
|
||||||
@ -144,10 +128,10 @@ func (v *StreamReader) SkipBytes(count int) {
|
|||||||
|
|
||||||
// Read implements io.Reader
|
// Read implements io.Reader
|
||||||
func (v *StreamReader) Read(p []byte) (n int, err error) {
|
func (v *StreamReader) Read(p []byte) (n int, err error) {
|
||||||
streamLength := v.GetSize()
|
streamLength := v.Size()
|
||||||
|
|
||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
if v.GetPosition() >= streamLength {
|
if v.Position() >= streamLength {
|
||||||
return i, io.EOF
|
return i, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +139,10 @@ func (v *StreamReader) Read(p []byte) (n int, err error) {
|
|||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
p[i] = v.GetByte()
|
p[i], err = v.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,22 +8,26 @@ func TestStreamReaderByte(t *testing.T) {
|
|||||||
data := []byte{0x78, 0x56, 0x34, 0x12}
|
data := []byte{0x78, 0x56, 0x34, 0x12}
|
||||||
sr := CreateStreamReader(data)
|
sr := CreateStreamReader(data)
|
||||||
|
|
||||||
if sr.GetPosition() != 0 {
|
if sr.Position() != 0 {
|
||||||
t.Fatal("StreamReader.GetPosition() did not start at 0")
|
t.Fatal("StreamReader.Position() did not start at 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ss := sr.GetSize(); ss != 4 {
|
if ss := sr.Size(); ss != 4 {
|
||||||
t.Fatalf("StreamREader.GetSize() was expected to return %d, but returned %d instead", 4, ss)
|
t.Fatalf("StreamREader.Size() was expected to return %d, but returned %d instead", 4, ss)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(data); i++ {
|
for i := 0; i < len(data); i++ {
|
||||||
ret := sr.GetByte()
|
ret, err := sr.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
if ret != data[i] {
|
if ret != data[i] {
|
||||||
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", data[i], ret)
|
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", data[i], ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pos := sr.GetPosition(); pos != uint64(i+1) {
|
if pos := sr.Position(); pos != uint64(i+1) {
|
||||||
t.Fatalf("StreamReader.GetPosition() should be at %d, but was at %d instead", i, pos)
|
t.Fatalf("StreamReader.Position() should be at %d, but was at %d instead", i, pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -31,36 +35,48 @@ func TestStreamReaderByte(t *testing.T) {
|
|||||||
func TestStreamReaderWord(t *testing.T) {
|
func TestStreamReaderWord(t *testing.T) {
|
||||||
data := []byte{0x78, 0x56, 0x34, 0x12}
|
data := []byte{0x78, 0x56, 0x34, 0x12}
|
||||||
sr := CreateStreamReader(data)
|
sr := CreateStreamReader(data)
|
||||||
ret := sr.GetUInt16()
|
|
||||||
|
ret, err := sr.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
if ret != 0x5678 {
|
if ret != 0x5678 {
|
||||||
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", 0x5678, ret)
|
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", 0x5678, ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pos := sr.GetPosition(); pos != 2 {
|
if pos := sr.Position(); pos != 2 {
|
||||||
t.Fatalf("StreamReader.GetPosition() should be at %d, but was at %d instead", 2, pos)
|
t.Fatalf("StreamReader.Position() should be at %d, but was at %d instead", 2, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err = sr.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = sr.GetUInt16()
|
|
||||||
if ret != 0x1234 {
|
if ret != 0x1234 {
|
||||||
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", 0x1234, ret)
|
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", 0x1234, ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pos := sr.GetPosition(); pos != 4 {
|
if pos := sr.Position(); pos != 4 {
|
||||||
t.Fatalf("StreamReader.GetPosition() should be at %d, but was at %d instead", 4, pos)
|
t.Fatalf("StreamReader.Position() should be at %d, but was at %d instead", 4, pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStreamReaderDword(t *testing.T) {
|
func TestStreamReaderDword(t *testing.T) {
|
||||||
data := []byte{0x78, 0x56, 0x34, 0x12}
|
data := []byte{0x78, 0x56, 0x34, 0x12}
|
||||||
sr := CreateStreamReader(data)
|
sr := CreateStreamReader(data)
|
||||||
ret := sr.GetUInt32()
|
|
||||||
|
ret, err := sr.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
if ret != 0x12345678 {
|
if ret != 0x12345678 {
|
||||||
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", 0x12345678, ret)
|
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", 0x12345678, ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pos := sr.GetPosition(); pos != 4 {
|
if pos := sr.Position(); pos != 4 {
|
||||||
t.Fatalf("StreamReader.GetPosition() should be at %d, but was at %d instead", 4, pos)
|
t.Fatalf("StreamReader.Position() should be at %d, but was at %d instead", 4, pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
package d2datautils
|
package d2datautils
|
||||||
|
|
||||||
import "bytes"
|
import (
|
||||||
|
"bytes"
|
||||||
const (
|
"log"
|
||||||
byteMask = 0xFF
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StreamWriter allows you to create a byte array by streaming in writes of various sizes
|
// StreamWriter allows you to create a byte array by streaming in writes of various sizes
|
||||||
type StreamWriter struct {
|
type StreamWriter struct {
|
||||||
data *bytes.Buffer
|
data *bytes.Buffer
|
||||||
|
bitOffset int
|
||||||
|
bitCache byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateStreamWriter creates a new StreamWriter instance
|
// CreateStreamWriter creates a new StreamWriter instance
|
||||||
@ -20,41 +21,102 @@ func CreateStreamWriter() *StreamWriter {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushByte writes a byte to the stream
|
// GetBytes returns the the byte slice of the underlying data
|
||||||
func (v *StreamWriter) PushByte(val byte) {
|
func (v *StreamWriter) GetBytes() []byte {
|
||||||
v.data.WriteByte(val)
|
return v.data.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushUint16 writes an uint16 word to the stream
|
// PushBytes writes a bytes to the stream
|
||||||
func (v *StreamWriter) PushUint16(val uint16) {
|
func (v *StreamWriter) PushBytes(b ...byte) {
|
||||||
for count := 0; count < bytesPerInt16; count++ {
|
for _, i := range b {
|
||||||
shift := count * bitsPerByte
|
v.data.WriteByte(i)
|
||||||
v.data.WriteByte(byte(val>>shift) & byteMask)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushBit pushes single bit into stream
|
||||||
|
// WARNING: if you'll use PushBit, offset'll be less than 8, and if you'll
|
||||||
|
// use another Push... method, bits'll not be pushed
|
||||||
|
func (v *StreamWriter) PushBit(b bool) {
|
||||||
|
if b {
|
||||||
|
v.bitCache |= 1 << v.bitOffset
|
||||||
|
}
|
||||||
|
v.bitOffset++
|
||||||
|
|
||||||
|
if v.bitOffset != bitsPerByte {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v.PushBytes(v.bitCache)
|
||||||
|
v.bitCache = 0
|
||||||
|
v.bitOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushBits pushes bits (with max range 8)
|
||||||
|
func (v *StreamWriter) PushBits(b byte, bits int) {
|
||||||
|
if bits > bitsPerByte {
|
||||||
|
log.Print("input bits number must be less (or equal) than 8")
|
||||||
|
}
|
||||||
|
|
||||||
|
val := b
|
||||||
|
|
||||||
|
for i := 0; i < bits; i++ {
|
||||||
|
v.PushBit(val&1 == 1)
|
||||||
|
val >>= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushBits16 pushes bits (with max range 16)
|
||||||
|
func (v *StreamWriter) PushBits16(b uint16, bits int) {
|
||||||
|
if bits > bitsPerByte*bytesPerint16 {
|
||||||
|
log.Print("input bits number must be less (or equal) than 16")
|
||||||
|
}
|
||||||
|
|
||||||
|
val := b
|
||||||
|
|
||||||
|
for i := 0; i < bits; i++ {
|
||||||
|
v.PushBit(val&1 == 1)
|
||||||
|
val >>= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushBits32 pushes bits (with max range 32)
|
||||||
|
func (v *StreamWriter) PushBits32(b uint32, bits int) {
|
||||||
|
if bits > bitsPerByte*bytesPerint32 {
|
||||||
|
log.Print("input bits number must be less (or equal) than 32")
|
||||||
|
}
|
||||||
|
|
||||||
|
val := b
|
||||||
|
|
||||||
|
for i := 0; i < bits; i++ {
|
||||||
|
v.PushBit(val&1 == 1)
|
||||||
|
val >>= 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushInt16 writes a int16 word to the stream
|
// PushInt16 writes a int16 word to the stream
|
||||||
func (v *StreamWriter) PushInt16(val int16) {
|
func (v *StreamWriter) PushInt16(val int16) {
|
||||||
for count := 0; count < bytesPerInt16; count++ {
|
v.PushUint16(uint16(val))
|
||||||
shift := count * bitsPerByte
|
}
|
||||||
v.data.WriteByte(byte(val>>shift) & byteMask)
|
|
||||||
}
|
// PushUint16 writes an uint16 word to the stream
|
||||||
|
//nolint
|
||||||
|
func (v *StreamWriter) PushUint16(val uint16) {
|
||||||
|
v.data.WriteByte(byte(val))
|
||||||
|
v.data.WriteByte(byte(val >> 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushInt32 writes a int32 dword to the stream
|
||||||
|
func (v *StreamWriter) PushInt32(val int32) {
|
||||||
|
v.PushUint32(uint32(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushUint32 writes a uint32 dword to the stream
|
// PushUint32 writes a uint32 dword to the stream
|
||||||
|
//nolint
|
||||||
func (v *StreamWriter) PushUint32(val uint32) {
|
func (v *StreamWriter) PushUint32(val uint32) {
|
||||||
for count := 0; count < bytesPerInt32; count++ {
|
v.data.WriteByte(byte(val))
|
||||||
shift := count * bitsPerByte
|
v.data.WriteByte(byte(val >> 8))
|
||||||
v.data.WriteByte(byte(val>>shift) & byteMask)
|
v.data.WriteByte(byte(val >> 16))
|
||||||
}
|
v.data.WriteByte(byte(val >> 24))
|
||||||
}
|
|
||||||
|
|
||||||
// PushUint64 writes a uint64 qword to the stream
|
|
||||||
func (v *StreamWriter) PushUint64(val uint64) {
|
|
||||||
for count := 0; count < bytesPerInt64; count++ {
|
|
||||||
shift := count * bitsPerByte
|
|
||||||
v.data.WriteByte(byte(val>>shift) & byteMask)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushInt64 writes a uint64 qword to the stream
|
// PushInt64 writes a uint64 qword to the stream
|
||||||
@ -62,7 +124,15 @@ func (v *StreamWriter) PushInt64(val int64) {
|
|||||||
v.PushUint64(uint64(val))
|
v.PushUint64(uint64(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBytes returns the the byte slice of the underlying data
|
// PushUint64 writes a uint64 qword to the stream
|
||||||
func (v *StreamWriter) GetBytes() []byte {
|
//nolint
|
||||||
return v.data.Bytes()
|
func (v *StreamWriter) PushUint64(val uint64) {
|
||||||
|
v.data.WriteByte(byte(val))
|
||||||
|
v.data.WriteByte(byte(val >> 8))
|
||||||
|
v.data.WriteByte(byte(val >> 16))
|
||||||
|
v.data.WriteByte(byte(val >> 24))
|
||||||
|
v.data.WriteByte(byte(val >> 32))
|
||||||
|
v.data.WriteByte(byte(val >> 40))
|
||||||
|
v.data.WriteByte(byte(val >> 48))
|
||||||
|
v.data.WriteByte(byte(val >> 56))
|
||||||
}
|
}
|
||||||
|
@ -4,18 +4,75 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStreamWriterByte(t *testing.T) {
|
func TestStreamWriterBits(t *testing.T) {
|
||||||
sr := CreateStreamWriter()
|
sr := CreateStreamWriter()
|
||||||
data := []byte{0x12, 0x34, 0x56, 0x78}
|
data := []byte{221, 19}
|
||||||
|
|
||||||
for _, d := range data {
|
for _, i := range data {
|
||||||
sr.PushByte(d)
|
sr.PushBits(i, bitsPerByte)
|
||||||
}
|
}
|
||||||
|
|
||||||
output := sr.GetBytes()
|
output := sr.GetBytes()
|
||||||
for i, d := range data {
|
for i, d := range data {
|
||||||
if output[i] != d {
|
if output[i] != d {
|
||||||
t.Fatalf("sr.PushByte() pushed %X, but wrote %X instead", d, output[i])
|
t.Fatalf("sr.PushBits() pushed %X, but wrote %X instead", d, output[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamWriterBits16(t *testing.T) {
|
||||||
|
sr := CreateStreamWriter()
|
||||||
|
data := []uint16{1024, 19}
|
||||||
|
|
||||||
|
for _, i := range data {
|
||||||
|
sr.PushBits16(i, bitsPerByte*bytesPerint16)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := sr.GetBytes()
|
||||||
|
|
||||||
|
for i, d := range data {
|
||||||
|
// nolint:gomnd // offset in byte slice; bit shifts for uint16
|
||||||
|
outputInt := uint16(output[bytesPerint16*i]) |
|
||||||
|
uint16(output[bytesPerint16*i+1])<<8
|
||||||
|
if outputInt != d {
|
||||||
|
t.Fatalf("sr.PushBits16() pushed %X, but wrote %X instead", d, output[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamWriterBits32(t *testing.T) {
|
||||||
|
sr := CreateStreamWriter()
|
||||||
|
data := []uint32{19324, 87}
|
||||||
|
|
||||||
|
for _, i := range data {
|
||||||
|
sr.PushBits32(i, bitsPerByte*bytesPerint32)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := sr.GetBytes()
|
||||||
|
|
||||||
|
for i, d := range data {
|
||||||
|
// nolint:gomnd // offset in byte slice; bit shifts for uint32
|
||||||
|
outputInt := uint32(output[bytesPerint32*i]) |
|
||||||
|
uint32(output[bytesPerint32*i+1])<<8 |
|
||||||
|
uint32(output[bytesPerint32*i+2])<<16 |
|
||||||
|
uint32(output[bytesPerint32*i+3])<<24
|
||||||
|
|
||||||
|
if outputInt != d {
|
||||||
|
t.Fatalf("sr.PushBits32() pushed %X, but wrote %X instead", d, output[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamWriterByte(t *testing.T) {
|
||||||
|
sr := CreateStreamWriter()
|
||||||
|
data := []byte{0x12, 0x34, 0x56, 0x78}
|
||||||
|
|
||||||
|
sr.PushBytes(data...)
|
||||||
|
|
||||||
|
output := sr.GetBytes()
|
||||||
|
for i, d := range data {
|
||||||
|
if output[i] != d {
|
||||||
|
t.Fatalf("sr.PushBytes() pushed %X, but wrote %X instead", d, output[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
package d2enum
|
package d2enum
|
||||||
|
|
||||||
|
const (
|
||||||
|
unknown = "Unknown"
|
||||||
|
)
|
||||||
|
|
||||||
//go:generate stringer -linecomment -type CompositeType -output composite_type_string.go
|
//go:generate stringer -linecomment -type CompositeType -output composite_type_string.go
|
||||||
|
|
||||||
// CompositeType represents a composite type
|
// CompositeType represents a composite type
|
||||||
@ -25,3 +29,32 @@ const (
|
|||||||
CompositeTypeSpecial8 // S8
|
CompositeTypeSpecial8 // S8
|
||||||
CompositeTypeMax
|
CompositeTypeMax
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Name returns a full name of layer
|
||||||
|
func (i CompositeType) Name() string {
|
||||||
|
strings := map[CompositeType]string{
|
||||||
|
CompositeTypeHead: "Head",
|
||||||
|
CompositeTypeTorso: "Torso",
|
||||||
|
CompositeTypeLegs: "Legs",
|
||||||
|
CompositeTypeRightArm: "Right Arm",
|
||||||
|
CompositeTypeLeftArm: "Left Arm",
|
||||||
|
CompositeTypeRightHand: "Right Hand",
|
||||||
|
CompositeTypeLeftHand: "Left Hand",
|
||||||
|
CompositeTypeShield: "Shield",
|
||||||
|
CompositeTypeSpecial1: "Special 1",
|
||||||
|
CompositeTypeSpecial2: "Special 2",
|
||||||
|
CompositeTypeSpecial3: "Special 3",
|
||||||
|
CompositeTypeSpecial4: "Special 4",
|
||||||
|
CompositeTypeSpecial5: "Special 5",
|
||||||
|
CompositeTypeSpecial6: "Special 6",
|
||||||
|
CompositeTypeSpecial7: "Special 7",
|
||||||
|
CompositeTypeSpecial8: "Special 8",
|
||||||
|
}
|
||||||
|
|
||||||
|
layerName, found := strings[i]
|
||||||
|
if !found {
|
||||||
|
return unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
return layerName
|
||||||
|
}
|
||||||
|
13
d2common/d2enum/difficulty.go
Normal file
13
d2common/d2enum/difficulty.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package d2enum
|
||||||
|
|
||||||
|
// DifficultyType is an enum for the possible difficulties
|
||||||
|
type DifficultyType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DifficultyNormal is the normal difficulty
|
||||||
|
DifficultyNormal DifficultyType = iota
|
||||||
|
// DifficultyNightmare is the nightmare difficulty
|
||||||
|
DifficultyNightmare
|
||||||
|
// DifficultyHell is the hell difficulty
|
||||||
|
DifficultyHell
|
||||||
|
)
|
@ -45,3 +45,24 @@ const (
|
|||||||
func (d DrawEffect) Transparent() bool {
|
func (d DrawEffect) Transparent() bool {
|
||||||
return d != DrawEffectNone
|
return d != DrawEffectNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d DrawEffect) String() string {
|
||||||
|
strings := map[DrawEffect]string{
|
||||||
|
DrawEffectPctTransparency25: "25% alpha",
|
||||||
|
DrawEffectPctTransparency50: "50% alpha",
|
||||||
|
DrawEffectPctTransparency75: "75% alpha",
|
||||||
|
DrawEffectModulate: "Modulate",
|
||||||
|
DrawEffectBurn: "Burn",
|
||||||
|
DrawEffectNormal: "Normal",
|
||||||
|
DrawEffectMod2XTrans: "Mod2XTrans",
|
||||||
|
DrawEffectMod2X: "Mod2X",
|
||||||
|
DrawEffectNone: "None",
|
||||||
|
}
|
||||||
|
|
||||||
|
drawEffect, found := strings[d]
|
||||||
|
if !found {
|
||||||
|
return unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
return drawEffect
|
||||||
|
}
|
||||||
|
83
d2common/d2enum/game_event.go
Normal file
83
d2common/d2enum/game_event.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package d2enum
|
||||||
|
|
||||||
|
// GameEvent represents an envent in the game engine
|
||||||
|
type GameEvent int
|
||||||
|
|
||||||
|
// Game events
|
||||||
|
const (
|
||||||
|
// ToggleGameMenu will display the game menu
|
||||||
|
ToggleGameMenu GameEvent = iota + 1
|
||||||
|
|
||||||
|
// panel toggles
|
||||||
|
ToggleCharacterPanel
|
||||||
|
ToggleInventoryPanel
|
||||||
|
TogglePartyPanel
|
||||||
|
ToggleSkillTreePanel
|
||||||
|
ToggleHirelingPanel
|
||||||
|
ToggleQuestLog
|
||||||
|
ToggleHelpScreen
|
||||||
|
ToggleChatOverlay
|
||||||
|
ToggleMessageLog
|
||||||
|
ToggleRightSkillSelector // these two are for left/right speed-skill panel toggles
|
||||||
|
ToggleLeftSkillSelector
|
||||||
|
|
||||||
|
ToggleAutomap
|
||||||
|
CenterAutomap // recenters the automap when opened
|
||||||
|
FadeAutomap // reduces the brightness of the map (not the players/npcs)
|
||||||
|
TogglePartyOnAutomap // toggles the display of the party members on the automap
|
||||||
|
ToggleNamesOnAutomap // toggles the display of party members names and npcs on the automap
|
||||||
|
ToggleMiniMap
|
||||||
|
|
||||||
|
// there can be 16 hotkeys, each hotkey can have a skill assigned
|
||||||
|
UseSkill1
|
||||||
|
UseSkill2
|
||||||
|
UseSkill3
|
||||||
|
UseSkill4
|
||||||
|
UseSkill5
|
||||||
|
UseSkill6
|
||||||
|
UseSkill7
|
||||||
|
UseSkill8
|
||||||
|
UseSkill9
|
||||||
|
UseSkill10
|
||||||
|
UseSkill11
|
||||||
|
UseSkill12
|
||||||
|
UseSkill13
|
||||||
|
UseSkill14
|
||||||
|
UseSkill15
|
||||||
|
UseSkill16
|
||||||
|
|
||||||
|
// switching between prev/next skill
|
||||||
|
SelectPreviousSkill
|
||||||
|
SelectNextSkill
|
||||||
|
|
||||||
|
// ToggleBelts toggles the display of the different level for
|
||||||
|
// the currently equipped belt
|
||||||
|
ToggleBelts
|
||||||
|
UseBeltSlot1
|
||||||
|
UseBeltSlot2
|
||||||
|
UseBeltSlot3
|
||||||
|
UseBeltSlot4
|
||||||
|
|
||||||
|
SwapWeapons
|
||||||
|
ToggleChatBox
|
||||||
|
ToggleRunWalk
|
||||||
|
|
||||||
|
SayHelp
|
||||||
|
SayFollowMe
|
||||||
|
SayThisIsForYou
|
||||||
|
SayThanks
|
||||||
|
SaySorry
|
||||||
|
SayBye
|
||||||
|
SayNowYouDie
|
||||||
|
SayRetreat
|
||||||
|
|
||||||
|
// these events are fired while a player holds the corresponding key
|
||||||
|
HoldRun
|
||||||
|
HoldStandStill
|
||||||
|
HoldShowGroundItems
|
||||||
|
HoldShowPortraits
|
||||||
|
|
||||||
|
TakeScreenShot
|
||||||
|
ClearScreen // closes all active menus/panels
|
||||||
|
ClearMessages
|
||||||
|
)
|
@ -3,212 +3,117 @@ package d2enum
|
|||||||
// Key represents button on a traditional keyboard.
|
// Key represents button on a traditional keyboard.
|
||||||
type Key int
|
type Key int
|
||||||
|
|
||||||
|
// Input keys
|
||||||
const (
|
const (
|
||||||
// Key0 is the number 0
|
|
||||||
Key0 Key = iota
|
Key0 Key = iota
|
||||||
// Key1 is the number 1
|
|
||||||
Key1
|
Key1
|
||||||
// Key2 is the number 2
|
|
||||||
Key2
|
Key2
|
||||||
// Key3 is the number 3
|
|
||||||
Key3
|
Key3
|
||||||
// Key4 is the number 4
|
|
||||||
Key4
|
Key4
|
||||||
// Key5 is the number 5
|
|
||||||
Key5
|
Key5
|
||||||
// Key6 is the number 6
|
|
||||||
Key6
|
Key6
|
||||||
// Key7 is the number 7
|
|
||||||
Key7
|
Key7
|
||||||
// Key8 is the number 8
|
|
||||||
Key8
|
Key8
|
||||||
// Key9 is the number 9
|
|
||||||
Key9
|
Key9
|
||||||
// KeyA is the letter A
|
|
||||||
KeyA
|
KeyA
|
||||||
// KeyB is the letter B
|
|
||||||
KeyB
|
KeyB
|
||||||
// KeyC is the letter C
|
|
||||||
KeyC
|
KeyC
|
||||||
// KeyD is the letter D
|
|
||||||
KeyD
|
KeyD
|
||||||
// KeyE is the letter E
|
|
||||||
KeyE
|
KeyE
|
||||||
// KeyF is the letter F
|
|
||||||
KeyF
|
KeyF
|
||||||
// KeyG is the letter G
|
|
||||||
KeyG
|
KeyG
|
||||||
// KeyH is the letter H
|
|
||||||
KeyH
|
KeyH
|
||||||
// KeyI is the letter I
|
|
||||||
KeyI
|
KeyI
|
||||||
// KeyJ is the letter J
|
|
||||||
KeyJ
|
KeyJ
|
||||||
// KeyK is the letter K
|
|
||||||
KeyK
|
KeyK
|
||||||
// KeyL is the letter L
|
|
||||||
KeyL
|
KeyL
|
||||||
// KeyM is the letter M
|
|
||||||
KeyM
|
KeyM
|
||||||
// KeyN is the letter N
|
|
||||||
KeyN
|
KeyN
|
||||||
// KeyO is the letter O
|
|
||||||
KeyO
|
KeyO
|
||||||
// KeyP is the letter P
|
|
||||||
KeyP
|
KeyP
|
||||||
// KeyQ is the letter Q
|
|
||||||
KeyQ
|
KeyQ
|
||||||
// KeyR is the letter R
|
|
||||||
KeyR
|
KeyR
|
||||||
// KeyS is the letter S
|
|
||||||
KeyS
|
KeyS
|
||||||
// KeyT is the letter T
|
|
||||||
KeyT
|
KeyT
|
||||||
// KeyU is the letter U
|
|
||||||
KeyU
|
KeyU
|
||||||
// KeyV is the letter V
|
|
||||||
KeyV
|
KeyV
|
||||||
// KeyW is the letter W
|
|
||||||
KeyW
|
KeyW
|
||||||
// KeyX is the letter X
|
|
||||||
KeyX
|
KeyX
|
||||||
// KeyY is the letter Y
|
|
||||||
KeyY
|
KeyY
|
||||||
// KeyZ is the letter Z
|
|
||||||
KeyZ
|
KeyZ
|
||||||
// KeyApostrophe is the Apostrophe
|
|
||||||
KeyApostrophe
|
KeyApostrophe
|
||||||
// KeyBackslash is the Backslash
|
|
||||||
KeyBackslash
|
KeyBackslash
|
||||||
// KeyBackspace is the Backspace
|
|
||||||
KeyBackspace
|
KeyBackspace
|
||||||
// KeyCapsLock is the CapsLock
|
|
||||||
KeyCapsLock
|
KeyCapsLock
|
||||||
// KeyComma is the Comma
|
|
||||||
KeyComma
|
KeyComma
|
||||||
// KeyDelete is the Delete
|
|
||||||
KeyDelete
|
KeyDelete
|
||||||
// KeyDown is the down arrow key
|
|
||||||
KeyDown
|
KeyDown
|
||||||
// KeyEnd is the End
|
|
||||||
KeyEnd
|
KeyEnd
|
||||||
// KeyEnter is the Enter
|
|
||||||
KeyEnter
|
KeyEnter
|
||||||
// KeyEqual is the Equal
|
|
||||||
KeyEqual
|
KeyEqual
|
||||||
// KeyEscape is the Escape
|
|
||||||
KeyEscape
|
KeyEscape
|
||||||
// KeyF1 is the function F1
|
|
||||||
KeyF1
|
KeyF1
|
||||||
// KeyF2 is the function F2
|
|
||||||
KeyF2
|
KeyF2
|
||||||
// KeyF3 is the function F3
|
|
||||||
KeyF3
|
KeyF3
|
||||||
// KeyF4 is the function F4
|
|
||||||
KeyF4
|
KeyF4
|
||||||
// KeyF5 is the function F5
|
|
||||||
KeyF5
|
KeyF5
|
||||||
// KeyF6 is the function F6
|
|
||||||
KeyF6
|
KeyF6
|
||||||
// KeyF7 is the function F7
|
|
||||||
KeyF7
|
KeyF7
|
||||||
// KeyF8 is the function F8
|
|
||||||
KeyF8
|
KeyF8
|
||||||
// KeyF9 is the function F9
|
|
||||||
KeyF9
|
KeyF9
|
||||||
// KeyF10 is the function F10
|
|
||||||
KeyF10
|
KeyF10
|
||||||
// KeyF11 is the function F11
|
|
||||||
KeyF11
|
KeyF11
|
||||||
// KeyF12 is the function F12
|
|
||||||
KeyF12
|
KeyF12
|
||||||
// KeyGraveAccent is the Grave Accent
|
|
||||||
KeyGraveAccent
|
KeyGraveAccent
|
||||||
// KeyHome is the home key
|
|
||||||
KeyHome
|
KeyHome
|
||||||
// KeyInsert is the insert key
|
|
||||||
KeyInsert
|
KeyInsert
|
||||||
// KeyKP0 is keypad 0
|
|
||||||
KeyKP0
|
KeyKP0
|
||||||
// KeyKP1 is keypad 1
|
|
||||||
KeyKP1
|
KeyKP1
|
||||||
// KeyKP2 is keypad 2
|
|
||||||
KeyKP2
|
KeyKP2
|
||||||
// KeyKP3 is keypad 3
|
|
||||||
KeyKP3
|
KeyKP3
|
||||||
// KeyKP4 is keypad 4
|
|
||||||
KeyKP4
|
KeyKP4
|
||||||
// KeyKP5 is keypad 5
|
|
||||||
KeyKP5
|
KeyKP5
|
||||||
// KeyKP6 is keypad 6
|
|
||||||
KeyKP6
|
KeyKP6
|
||||||
// KeyKP7 is keypad 7
|
|
||||||
KeyKP7
|
KeyKP7
|
||||||
// KeyKP8 is keypad 8
|
|
||||||
KeyKP8
|
KeyKP8
|
||||||
// KeyKP9 is keypad 9
|
|
||||||
KeyKP9
|
KeyKP9
|
||||||
// KeyKPAdd is keypad Add
|
|
||||||
KeyKPAdd
|
KeyKPAdd
|
||||||
// KeyKPDecimal is keypad Decimal
|
|
||||||
KeyKPDecimal
|
KeyKPDecimal
|
||||||
// KeyKPDivide is keypad Divide
|
|
||||||
KeyKPDivide
|
KeyKPDivide
|
||||||
// KeyKPEnter is keypad Enter
|
|
||||||
KeyKPEnter
|
KeyKPEnter
|
||||||
// KeyKPEqual is keypad Equal
|
|
||||||
KeyKPEqual
|
KeyKPEqual
|
||||||
// KeyKPMultiply is keypad Multiply
|
|
||||||
KeyKPMultiply
|
KeyKPMultiply
|
||||||
// KeyKPSubtract is keypad Subtract
|
|
||||||
KeyKPSubtract
|
KeyKPSubtract
|
||||||
// KeyLeft is the left arrow key
|
|
||||||
KeyLeft
|
KeyLeft
|
||||||
// KeyLeftBracket is the left bracket
|
|
||||||
KeyLeftBracket
|
KeyLeftBracket
|
||||||
// KeyMenu is the Menu key
|
|
||||||
KeyMenu
|
KeyMenu
|
||||||
// KeyMinus is the Minus key
|
|
||||||
KeyMinus
|
KeyMinus
|
||||||
// KeyNumLock is the NumLock key
|
|
||||||
KeyNumLock
|
KeyNumLock
|
||||||
// KeyPageDown is the PageDown key
|
|
||||||
KeyPageDown
|
KeyPageDown
|
||||||
// KeyPageUp is the PageUp key
|
|
||||||
KeyPageUp
|
KeyPageUp
|
||||||
// KeyPause is the Pause key
|
|
||||||
KeyPause
|
KeyPause
|
||||||
// KeyPeriod is the Period key
|
|
||||||
KeyPeriod
|
KeyPeriod
|
||||||
// KeyPrintScreen is the PrintScreen key
|
|
||||||
KeyPrintScreen
|
KeyPrintScreen
|
||||||
// KeyRight is the right arrow key
|
|
||||||
KeyRight
|
KeyRight
|
||||||
// KeyRightBracket is the right bracket key
|
|
||||||
KeyRightBracket
|
KeyRightBracket
|
||||||
// KeyScrollLock is the scroll lock key
|
|
||||||
KeyScrollLock
|
KeyScrollLock
|
||||||
// KeySemicolon is the semicolon key
|
|
||||||
KeySemicolon
|
KeySemicolon
|
||||||
// KeySlash is the front slash key
|
|
||||||
KeySlash
|
KeySlash
|
||||||
// KeySpace is the space key
|
|
||||||
KeySpace
|
KeySpace
|
||||||
// KeyTab is the tab key
|
|
||||||
KeyTab
|
KeyTab
|
||||||
// KeyUp is the up arrow key
|
|
||||||
KeyUp
|
KeyUp
|
||||||
// KeyAlt is the alt key
|
|
||||||
KeyAlt
|
KeyAlt
|
||||||
// KeyControl is the control key
|
|
||||||
KeyControl
|
KeyControl
|
||||||
// KeyShift is the shift key
|
|
||||||
KeyShift
|
KeyShift
|
||||||
|
KeyTilde
|
||||||
|
KeyMouse3
|
||||||
|
KeyMouse4
|
||||||
|
KeyMouse5
|
||||||
|
KeyMouseWheelUp
|
||||||
|
KeyMouseWheelDown
|
||||||
|
|
||||||
// KeyMin is the lowest key
|
|
||||||
KeyMin = Key0
|
KeyMin = Key0
|
||||||
// KeyMax is the highest key
|
KeyMax = KeyMouseWheelDown
|
||||||
KeyMax = KeyShift
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyMod represents a "modified" key action. This could mean, for example, ctrl-S
|
// KeyMod represents a "modified" key action. This could mean, for example, ctrl-S
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
package d2enum
|
|
||||||
|
|
||||||
// LayerStreamType represents a layer stream type
|
|
||||||
type LayerStreamType int
|
|
||||||
|
|
||||||
// Layer stream types
|
|
||||||
const (
|
|
||||||
LayerStreamWall1 LayerStreamType = iota
|
|
||||||
LayerStreamWall2
|
|
||||||
LayerStreamWall3
|
|
||||||
LayerStreamWall4
|
|
||||||
LayerStreamOrientation1
|
|
||||||
LayerStreamOrientation2
|
|
||||||
LayerStreamOrientation3
|
|
||||||
LayerStreamOrientation4
|
|
||||||
LayerStreamFloor1
|
|
||||||
LayerStreamFloor2
|
|
||||||
LayerStreamShadow
|
|
||||||
LayerStreamSubstitute
|
|
||||||
)
|
|
133
d2common/d2enum/numeric_labels.go
Normal file
133
d2common/d2enum/numeric_labels.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package d2enum
|
||||||
|
|
||||||
|
// there are labels for "numeric labels (see AssetManager.TranslateString)
|
||||||
|
const (
|
||||||
|
RepairAll = iota
|
||||||
|
_
|
||||||
|
CancelLabel
|
||||||
|
CopyrightLabel
|
||||||
|
AllRightsReservedLabel
|
||||||
|
SinglePlayerLabel
|
||||||
|
_
|
||||||
|
OtherMultiplayerLabel
|
||||||
|
ExitGameLabel
|
||||||
|
CreditsLabel
|
||||||
|
CinematicsLabel
|
||||||
|
|
||||||
|
ViewAllCinematicsLabel
|
||||||
|
EpilogueLabel
|
||||||
|
SelectCinematicLabel
|
||||||
|
|
||||||
|
_
|
||||||
|
TCPIPGameLabel
|
||||||
|
TCPIPOptionsLabel
|
||||||
|
TCPIPHostGameLabel
|
||||||
|
TCPIPJoinGameLabel
|
||||||
|
TCPIPEnterHostIPLabel
|
||||||
|
TCPIPYourIPLabel
|
||||||
|
TipHostLabel
|
||||||
|
TipJoinLabel
|
||||||
|
IPNotFoundLabel
|
||||||
|
|
||||||
|
CharNameLabel
|
||||||
|
HardCoreLabel
|
||||||
|
SelectHeroClassLabel
|
||||||
|
AmazonDescr
|
||||||
|
NecromancerDescr
|
||||||
|
BarbarianDescr
|
||||||
|
SorceressDescr
|
||||||
|
PaladinDescr
|
||||||
|
|
||||||
|
_
|
||||||
|
|
||||||
|
HellLabel
|
||||||
|
NightmareLabel
|
||||||
|
NormalLabel
|
||||||
|
SelectDifficultyLabel
|
||||||
|
|
||||||
|
_
|
||||||
|
|
||||||
|
DelCharConfLabel
|
||||||
|
OpenLabel
|
||||||
|
|
||||||
|
_
|
||||||
|
|
||||||
|
YesLabel
|
||||||
|
NoLabel
|
||||||
|
|
||||||
|
_
|
||||||
|
|
||||||
|
ExitLabel
|
||||||
|
OKLabel
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseLabelNumbers returns base label value (#n in english string table table)
|
||||||
|
func BaseLabelNumbers(idx int) int {
|
||||||
|
baseLabelNumbers := []int{
|
||||||
|
128, // repairAll
|
||||||
|
127,
|
||||||
|
// main menu labels
|
||||||
|
1612, // CANCEL
|
||||||
|
1613, // (c) 2000 Blizzard Entertainment
|
||||||
|
1614, // All Rights Reserved.
|
||||||
|
1620, // SINGLE PLAYER
|
||||||
|
1621, // BATTLE.NET
|
||||||
|
1623, // OTHER MULTIPLAYER
|
||||||
|
1625, // EXIT DIABLO II
|
||||||
|
1627, // CREDITS
|
||||||
|
1639, // CINEMATICS
|
||||||
|
|
||||||
|
// cinematics menu labels
|
||||||
|
1640, // View All Earned Cinematics
|
||||||
|
1659, // Epilogue
|
||||||
|
1660, // SELECT CINEMATICS
|
||||||
|
|
||||||
|
// multiplayer labels
|
||||||
|
1663, // OPEN BATTLE.NET
|
||||||
|
1666, // TCP/IP GAME
|
||||||
|
1667, // TCP/IP Options
|
||||||
|
1675, // HOST GAME
|
||||||
|
1676, // JOIN GAME
|
||||||
|
1678, // Enter Host IP Address to Join Game
|
||||||
|
1680, // Your IP Address is:
|
||||||
|
1689, // Tip: host game
|
||||||
|
1690, // Tip: join game
|
||||||
|
1691, // Cannot detect a valid TCP/IP address.
|
||||||
|
1694, // Character Name
|
||||||
|
1696, // Hardcore
|
||||||
|
1697, // Select Hero Class
|
||||||
|
|
||||||
|
1698, // amazon description
|
||||||
|
1704, // nec description
|
||||||
|
1709, // barb description
|
||||||
|
1710, // sorc description
|
||||||
|
1711, // pal description
|
||||||
|
/*in addition, as many elements as the value
|
||||||
|
of the highest modifier must be listed*/
|
||||||
|
1712,
|
||||||
|
|
||||||
|
/* here, should be labels used to battle.net multiplayer, but they are not used yet,
|
||||||
|
therefore I don't list them here.*/
|
||||||
|
|
||||||
|
// difficulty levels:
|
||||||
|
1800, // Hell
|
||||||
|
1864, // Nightmare
|
||||||
|
1865, // Normal
|
||||||
|
1867, // Select Difficulty
|
||||||
|
|
||||||
|
1869, // not used, for locales with +1 mod
|
||||||
|
1878, // delete char confirm
|
||||||
|
1881, // Open
|
||||||
|
1889, // char name is currently taken (not used)
|
||||||
|
1896, // YES
|
||||||
|
1925, // NO
|
||||||
|
|
||||||
|
1926, // not used, for locales with +1 mod
|
||||||
|
|
||||||
|
970, // EXIT
|
||||||
|
971, // OK
|
||||||
|
1612,
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseLabelNumbers[idx]
|
||||||
|
}
|
11
d2common/d2enum/party_buttons.go
Normal file
11
d2common/d2enum/party_buttons.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package d2enum
|
||||||
|
|
||||||
|
// Frames of party Buttons
|
||||||
|
const (
|
||||||
|
PartyButtonListeningFrame = iota * 4
|
||||||
|
PartyButtonRelationshipsFrame
|
||||||
|
PartyButtonSeeingFrame
|
||||||
|
PartyButtonCorpsLootingFrame
|
||||||
|
|
||||||
|
PartyButtonNextButtonFrame = 2
|
||||||
|
)
|
21
d2common/d2enum/players_relationships.go
Normal file
21
d2common/d2enum/players_relationships.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package d2enum
|
||||||
|
|
||||||
|
// PlayersRelationships represents players relationships
|
||||||
|
type PlayersRelationships int
|
||||||
|
|
||||||
|
// Players relationships
|
||||||
|
const (
|
||||||
|
PlayerRelationNeutral PlayersRelationships = iota
|
||||||
|
PlayerRelationFriend
|
||||||
|
PlayerRelationEnemy
|
||||||
|
)
|
||||||
|
|
||||||
|
// determinates a level, which both players should reach to go hostile
|
||||||
|
const (
|
||||||
|
PlayersHostileLevel = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
// determinates max players number for one game
|
||||||
|
const (
|
||||||
|
MaxPlayersInGame = 8
|
||||||
|
)
|
60
d2common/d2enum/quests.go
Normal file
60
d2common/d2enum/quests.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package d2enum
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NormalActQuestsNumber is number of quests in standard act
|
||||||
|
NormalActQuestsNumber = 6
|
||||||
|
// HalfQuestsNumber is number of quests in act 4
|
||||||
|
HalfQuestsNumber = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// ActsNumber is number of acts in game
|
||||||
|
const ActsNumber = 5
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Act1 is act 1 in game
|
||||||
|
Act1 = iota + 1
|
||||||
|
// Act2 is act 2 in game
|
||||||
|
Act2
|
||||||
|
// Act3 is act 3 in game
|
||||||
|
Act3
|
||||||
|
// Act4 is act 4 in game
|
||||||
|
Act4
|
||||||
|
// Act5 is act 4 in game
|
||||||
|
Act5
|
||||||
|
)
|
||||||
|
|
||||||
|
/* I think, It should looks like that:
|
||||||
|
each quest has its own position in questStatus map
|
||||||
|
which should come from save file.
|
||||||
|
quests status values:
|
||||||
|
- -2 - done
|
||||||
|
- -1 - done, need to play animation
|
||||||
|
- 0 - not started yet
|
||||||
|
- and after that we have "in progress status"
|
||||||
|
so for status (from 1 to n) we have appropriate
|
||||||
|
quest descriptions and we'll have appropriate
|
||||||
|
actions
|
||||||
|
*/
|
||||||
|
const (
|
||||||
|
QuestStatusCompleted = iota - 2 // quest completed
|
||||||
|
QuestStatusCompleting // quest completed (need to play animation)
|
||||||
|
QuestStatusNotStarted // quest not started yet
|
||||||
|
QuestStatusInProgress // quest is in progress
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// QuestNone describes "no selected quest" status
|
||||||
|
QuestNone = iota
|
||||||
|
// Quest1 describes quest field 1
|
||||||
|
Quest1
|
||||||
|
// Quest2 describes quest field 2
|
||||||
|
Quest2
|
||||||
|
// Quest3 describes quest field 3
|
||||||
|
Quest3
|
||||||
|
// Quest4 describes quest field 4
|
||||||
|
Quest4
|
||||||
|
// Quest5 describes quest field 5
|
||||||
|
Quest5
|
||||||
|
// Quest6 describes quest field 6
|
||||||
|
Quest6
|
||||||
|
)
|
@ -69,3 +69,36 @@ func (tile TileType) Special() bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tile TileType) String() string {
|
||||||
|
strings := map[TileType]string{
|
||||||
|
TileFloor: "floor",
|
||||||
|
TileLeftWall: "Left Wall",
|
||||||
|
TileRightWall: "Upper Wall",
|
||||||
|
TileRightPartOfNorthCornerWall: "Upper part of an Upper-Left corner",
|
||||||
|
TileLeftPartOfNorthCornerWall: "Left part of an Upper-Left corner",
|
||||||
|
TileLeftEndWall: "Upper-Right corner",
|
||||||
|
TileRightEndWall: "Lower-Left corner",
|
||||||
|
TileSouthCornerWall: "Lower-Right corner",
|
||||||
|
TileLeftWallWithDoor: "Left Wall with Door object, but not always",
|
||||||
|
TileRightWallWithDoor: "Upper Wall with Door object, but not always",
|
||||||
|
TileSpecialTile1: "special",
|
||||||
|
TileSpecialTile2: "special",
|
||||||
|
TilePillarsColumnsAndStandaloneObjects: "billars, collumns or standalone object",
|
||||||
|
TileShadow: "shadow",
|
||||||
|
TileTree: "wall/object",
|
||||||
|
TileRoof: "roof",
|
||||||
|
TileLowerWallsEquivalentToLeftWall: "lower wall (left wall)",
|
||||||
|
TileLowerWallsEquivalentToRightWall: "lower wall (right wall)",
|
||||||
|
TileLowerWallsEquivalentToRightLeftNorthCornerWall: "lower wall (north corner wall)",
|
||||||
|
TileLowerWallsEquivalentToSouthCornerwall: "lower wall (south corner wall)",
|
||||||
|
}
|
||||||
|
|
||||||
|
str, found := strings[tile]
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
str = "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
@ -24,3 +24,31 @@ const (
|
|||||||
WeaponClassOneHandToHand // ht1
|
WeaponClassOneHandToHand // ht1
|
||||||
WeaponClassTwoHandToHand // ht2
|
WeaponClassTwoHandToHand // ht2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Name returns a full name of weapon class
|
||||||
|
func (w WeaponClass) Name() string {
|
||||||
|
strings := map[WeaponClass]string{
|
||||||
|
WeaponClassNone: "None",
|
||||||
|
WeaponClassHandToHand: "Hand To Hand",
|
||||||
|
WeaponClassBow: "Bow",
|
||||||
|
WeaponClassOneHandSwing: "One Hand Swing",
|
||||||
|
WeaponClassOneHandThrust: "One Hand Thrust",
|
||||||
|
WeaponClassStaff: "Staff",
|
||||||
|
WeaponClassTwoHandSwing: "Two Hand Swing",
|
||||||
|
WeaponClassTwoHandThrust: "Two Hand Thrust",
|
||||||
|
WeaponClassCrossbow: "Crossbow",
|
||||||
|
WeaponClassLeftJabRightSwing: "Left Jab Right Swing",
|
||||||
|
WeaponClassLeftJabRightThrust: "Left Jab Right Thrust",
|
||||||
|
WeaponClassLeftSwingRightSwing: "Left Swing Right Swing",
|
||||||
|
WeaponClassLeftSwingRightThrust: "Left Swing Right Thrust",
|
||||||
|
WeaponClassOneHandToHand: "One Hand To Hand",
|
||||||
|
WeaponClassTwoHandToHand: "Two Hand To Hand",
|
||||||
|
}
|
||||||
|
|
||||||
|
weaponClass, found := strings[w]
|
||||||
|
if !found {
|
||||||
|
return unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
return weaponClass
|
||||||
|
}
|
||||||
|
@ -56,7 +56,68 @@ func (ad *AnimationData) GetRecords(name string) []*AnimationDataRecord {
|
|||||||
return ad.entries[name]
|
return ad.entries[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRecordsCount returns number of animation data records
|
||||||
|
func (ad *AnimationData) GetRecordsCount() int {
|
||||||
|
return len(ad.entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushRecord adds a new record to entry named 'name'
|
||||||
|
func (ad *AnimationData) PushRecord(name string) {
|
||||||
|
ad.entries[name] = append(
|
||||||
|
ad.entries[name],
|
||||||
|
&AnimationDataRecord{
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRecord teletes specified index from specified entry
|
||||||
|
func (ad *AnimationData) DeleteRecord(name string, recordIdx int) error {
|
||||||
|
newRecords := make([]*AnimationDataRecord, 0)
|
||||||
|
|
||||||
|
for n, i := range ad.entries[name] {
|
||||||
|
if n == recordIdx {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newRecords = append(newRecords, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ad.entries[name]) == len(newRecords) {
|
||||||
|
return fmt.Errorf("index %d not found", recordIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
ad.entries[name] = newRecords
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEntry adds a new animation entry with name given
|
||||||
|
func (ad *AnimationData) AddEntry(name string) error {
|
||||||
|
_, found := ad.entries[name]
|
||||||
|
if found {
|
||||||
|
return fmt.Errorf("entry of name %s already exist", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
ad.entries[name] = make([]*AnimationDataRecord, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEntry deltees entry with specified name
|
||||||
|
func (ad *AnimationData) DeleteEntry(name string) error {
|
||||||
|
_, found := ad.entries[name]
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("entry named %s doesn't exist", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(ad.entries, name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Load loads the data into an AnimationData struct
|
// Load loads the data into an AnimationData struct
|
||||||
|
//nolint:gocognit,funlen // can't reduce
|
||||||
func Load(data []byte) (*AnimationData, error) {
|
func Load(data []byte) (*AnimationData, error) {
|
||||||
reader := d2datautils.CreateStreamReader(data)
|
reader := d2datautils.CreateStreamReader(data)
|
||||||
animdata := &AnimationData{}
|
animdata := &AnimationData{}
|
||||||
@ -65,7 +126,11 @@ func Load(data []byte) (*AnimationData, error) {
|
|||||||
animdata.entries = make(map[string][]*AnimationDataRecord)
|
animdata.entries = make(map[string][]*AnimationDataRecord)
|
||||||
|
|
||||||
for blockIdx := range animdata.blocks {
|
for blockIdx := range animdata.blocks {
|
||||||
recordCount := reader.GetUInt32()
|
recordCount, err := reader.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if recordCount > maxRecordsPerBlock {
|
if recordCount > maxRecordsPerBlock {
|
||||||
return nil, fmt.Errorf("more than %d records in block", maxRecordsPerBlock)
|
return nil, fmt.Errorf("more than %d records in block", maxRecordsPerBlock)
|
||||||
}
|
}
|
||||||
@ -73,7 +138,10 @@ func Load(data []byte) (*AnimationData, error) {
|
|||||||
records := make([]*AnimationDataRecord, recordCount)
|
records := make([]*AnimationDataRecord, recordCount)
|
||||||
|
|
||||||
for recordIdx := uint32(0); recordIdx < recordCount; recordIdx++ {
|
for recordIdx := uint32(0); recordIdx < recordCount; recordIdx++ {
|
||||||
nameBytes := reader.ReadBytes(byteCountName)
|
nameBytes, err := reader.ReadBytes(byteCountName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if nameBytes[byteCountName-1] != byte(0) {
|
if nameBytes[byteCountName-1] != byte(0) {
|
||||||
return nil, errors.New("animdata AnimationDataRecord name missing null terminator byte")
|
return nil, errors.New("animdata AnimationDataRecord name missing null terminator byte")
|
||||||
@ -84,15 +152,27 @@ func Load(data []byte) (*AnimationData, error) {
|
|||||||
|
|
||||||
animdata.hashTable[hashIdx] = hashName(name)
|
animdata.hashTable[hashIdx] = hashName(name)
|
||||||
|
|
||||||
frames := reader.GetUInt32()
|
frames, err := reader.ReadUInt32()
|
||||||
speed := reader.GetUInt16()
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
speed, err := reader.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
reader.SkipBytes(byteCountSpeedPadding)
|
reader.SkipBytes(byteCountSpeedPadding)
|
||||||
|
|
||||||
events := make(map[int]AnimationEvent)
|
events := make(map[int]AnimationEvent)
|
||||||
|
|
||||||
for eventIdx := 0; eventIdx < numEvents; eventIdx++ {
|
for eventIdx := 0; eventIdx < numEvents; eventIdx++ {
|
||||||
event := AnimationEvent(reader.GetByte())
|
eventByte, err := reader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
event := AnimationEvent(eventByte)
|
||||||
if event != AnimationEventNone {
|
if event != AnimationEventNone {
|
||||||
events[eventIdx] = event
|
events[eventIdx] = event
|
||||||
}
|
}
|
||||||
@ -122,9 +202,93 @@ func Load(data []byte) (*AnimationData, error) {
|
|||||||
animdata.blocks[blockIdx] = b
|
animdata.blocks[blockIdx] = b
|
||||||
}
|
}
|
||||||
|
|
||||||
if reader.GetPosition() != uint64(len(data)) {
|
if reader.Position() != uint64(len(data)) {
|
||||||
return nil, errors.New("unable to parse animation data")
|
return nil, fmt.Errorf("unable to parse animation data: %d != %d", reader.Position(), len(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
return animdata, nil
|
return animdata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marshal encodes animation data back into byte slice
|
||||||
|
// basing on AnimationData.records
|
||||||
|
func (ad *AnimationData) Marshal() []byte {
|
||||||
|
sw := d2datautils.CreateStreamWriter()
|
||||||
|
|
||||||
|
// keys - all entries in animationData
|
||||||
|
keys := make([]string, len(ad.entries))
|
||||||
|
|
||||||
|
// we must manually add index
|
||||||
|
idx := 0
|
||||||
|
|
||||||
|
for i := range ad.entries {
|
||||||
|
keys[idx] = i
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
// name terminates current name
|
||||||
|
name := 0
|
||||||
|
|
||||||
|
// recordIdx determinates current record index
|
||||||
|
recordIdx := 0
|
||||||
|
|
||||||
|
// numberOfEntries is a number of entries in all map indexes
|
||||||
|
var numberOfEntries = 0
|
||||||
|
|
||||||
|
for i := 0; i < len(keys); i++ {
|
||||||
|
numberOfEntries += len(ad.entries[keys[i]])
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := 0; idx < numBlocks; idx++ {
|
||||||
|
// number of records (max is maxRecordsPerObject)
|
||||||
|
l := 0
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// first condition: end up with all this and push 0 to dhe end
|
||||||
|
case numberOfEntries == 0:
|
||||||
|
sw.PushUint32(0)
|
||||||
|
continue
|
||||||
|
case numberOfEntries < maxRecordsPerBlock:
|
||||||
|
// second condition - if number of entries left is smaller than
|
||||||
|
// maxRecordsPerBlock, push...
|
||||||
|
l = numberOfEntries
|
||||||
|
sw.PushUint32(uint32(l))
|
||||||
|
default:
|
||||||
|
// else use maxRecordsPerBlock
|
||||||
|
l = maxRecordsPerBlock
|
||||||
|
sw.PushUint32(maxRecordsPerBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
for currentRecordIdx := 0; currentRecordIdx < l; currentRecordIdx++ {
|
||||||
|
numberOfEntries--
|
||||||
|
|
||||||
|
if recordIdx == len(ad.entries[keys[name]]) {
|
||||||
|
recordIdx = 0
|
||||||
|
name++
|
||||||
|
}
|
||||||
|
|
||||||
|
animationRecord := ad.entries[keys[name]][recordIdx]
|
||||||
|
recordIdx++
|
||||||
|
|
||||||
|
name := animationRecord.name
|
||||||
|
missingZeroBytes := byteCountName - len(name)
|
||||||
|
sw.PushBytes([]byte(name)...)
|
||||||
|
|
||||||
|
for i := 0; i < missingZeroBytes; i++ {
|
||||||
|
sw.PushBytes(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.PushUint32(animationRecord.framesPerDirection)
|
||||||
|
sw.PushUint16(animationRecord.speed)
|
||||||
|
|
||||||
|
for i := 0; i < byteCountSpeedPadding; i++ {
|
||||||
|
sw.PushBytes(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for event := 0; event < numEvents; event++ {
|
||||||
|
sw.PushBytes(byte(animationRecord.events[event]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sw.GetBytes()
|
||||||
|
}
|
||||||
|
@ -154,3 +154,137 @@ func TestAnimationDataRecord_FPS(t *testing.T) {
|
|||||||
t.Error("incorrect fps")
|
t.Error("incorrect fps")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnimationData_Marshal(t *testing.T) {
|
||||||
|
file, fileErr := os.Open("testdata/AnimData.d2")
|
||||||
|
if fileErr != nil {
|
||||||
|
t.Error("cannot open test data file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, 0)
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
|
||||||
|
for {
|
||||||
|
numRead, err := file.Read(buf)
|
||||||
|
|
||||||
|
data = append(data, buf[:numRead]...)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ad, err := Load(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newData := ad.Marshal()
|
||||||
|
|
||||||
|
newAd, err := Load(newData)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys1 := make([]string, 0)
|
||||||
|
for i := range ad.entries {
|
||||||
|
keys1 = append(keys1, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys2 := make([]string, 0)
|
||||||
|
for i := range newAd.entries {
|
||||||
|
keys2 = append(keys2, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keys1) != len(keys2) {
|
||||||
|
t.Fatalf("unexpected length of keys in first and second dict: %d, %d", len(keys1), len(keys2))
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range newAd.entries {
|
||||||
|
for n, i := range newAd.entries[key] {
|
||||||
|
if i.speed != ad.entries[key][n].speed {
|
||||||
|
t.Fatal("unexpected record set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnimationData_DeleteRecord(t *testing.T) {
|
||||||
|
ad := &AnimationData{
|
||||||
|
entries: map[string][]*AnimationDataRecord{
|
||||||
|
"a": {
|
||||||
|
{name: "a", speed: 1, framesPerDirection: 1},
|
||||||
|
{name: "a", speed: 2, framesPerDirection: 2},
|
||||||
|
{name: "a", speed: 3, framesPerDirection: 3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ad.DeleteRecord("a", 1)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ad.entries["a"]) != 2 {
|
||||||
|
t.Fatal("Delete record error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ad.entries["a"][1].speed != 3 {
|
||||||
|
t.Fatal("Invalid index deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnimationData_PushRecord(t *testing.T) {
|
||||||
|
ad := &AnimationData{
|
||||||
|
entries: map[string][]*AnimationDataRecord{
|
||||||
|
"a": {
|
||||||
|
{name: "a", speed: 1, framesPerDirection: 1},
|
||||||
|
{name: "a", speed: 2, framesPerDirection: 2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ad.PushRecord("a")
|
||||||
|
|
||||||
|
if len(ad.entries["a"]) != 3 {
|
||||||
|
t.Fatal("No record was pushed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ad.entries["a"][2].name != "a" {
|
||||||
|
t.Fatal("unexpected name of new record was set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnimationData_AddEntry(t *testing.T) {
|
||||||
|
ad := &AnimationData{
|
||||||
|
entries: make(map[string][]*AnimationDataRecord),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ad.AddEntry("a")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := ad.entries["a"]; !found {
|
||||||
|
t.Fatal("entry wasn't added")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnimationData_DeleteEntry(t *testing.T) {
|
||||||
|
ad := &AnimationData{
|
||||||
|
entries: map[string][]*AnimationDataRecord{
|
||||||
|
"a": {{}, {}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ad.DeleteEntry("a")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := ad.entries["a"]; found {
|
||||||
|
t.Fatal("Entry wasn't deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -8,6 +8,26 @@ type AnimationDataRecord struct {
|
|||||||
events map[int]AnimationEvent
|
events map[int]AnimationEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FramesPerDirection returns frames per direction value
|
||||||
|
func (r *AnimationDataRecord) FramesPerDirection() int {
|
||||||
|
return int(r.framesPerDirection)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFramesPerDirection sets frames per direction value
|
||||||
|
func (r *AnimationDataRecord) SetFramesPerDirection(fpd uint32) {
|
||||||
|
r.framesPerDirection = fpd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speed returns animation's speed
|
||||||
|
func (r *AnimationDataRecord) Speed() int {
|
||||||
|
return int(r.speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSpeed sets record's speed
|
||||||
|
func (r *AnimationDataRecord) SetSpeed(s uint16) {
|
||||||
|
r.speed = s
|
||||||
|
}
|
||||||
|
|
||||||
// FPS returns the frames per second for this animation record
|
// FPS returns the frames per second for this animation record
|
||||||
func (r *AnimationDataRecord) FPS() float64 {
|
func (r *AnimationDataRecord) FPS() float64 {
|
||||||
speedf := float64(r.speed)
|
speedf := float64(r.speed)
|
||||||
@ -21,3 +41,23 @@ func (r *AnimationDataRecord) FPS() float64 {
|
|||||||
func (r *AnimationDataRecord) FrameDurationMS() float64 {
|
func (r *AnimationDataRecord) FrameDurationMS() float64 {
|
||||||
return milliseconds / r.FPS()
|
return milliseconds / r.FPS()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Events returns events map
|
||||||
|
func (r *AnimationDataRecord) Events() map[int]AnimationEvent {
|
||||||
|
return r.events
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event returns specific event
|
||||||
|
func (r *AnimationDataRecord) Event(idx int) AnimationEvent {
|
||||||
|
event, found := r.events[idx]
|
||||||
|
if found {
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimationEventNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEvent sets event on specific index to given
|
||||||
|
func (r *AnimationDataRecord) SetEvent(index int, event AnimationEvent) {
|
||||||
|
r.events[index] = event
|
||||||
|
}
|
||||||
|
@ -7,8 +7,68 @@ import (
|
|||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
numUnknownHeaderBytes = 21
|
||||||
|
numUnknownBodyBytes = 3
|
||||||
|
numHeaderBytes = 4 + numUnknownHeaderBytes
|
||||||
|
numLayerBytes = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
headerNumLayers = iota
|
||||||
|
headerFramesPerDir
|
||||||
|
headerNumDirs
|
||||||
|
headerSpeed = numHeaderBytes - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
layerType = iota
|
||||||
|
layerShadow
|
||||||
|
layerSelectable
|
||||||
|
layerTransparent
|
||||||
|
layerDrawEffect
|
||||||
|
layerWeaponClass
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
badCharacter = string(byte(0))
|
||||||
|
)
|
||||||
|
|
||||||
|
// New creates a new COF
|
||||||
|
func New() *COF {
|
||||||
|
return &COF{
|
||||||
|
unknownHeaderBytes: make([]byte, numUnknownHeaderBytes),
|
||||||
|
unknownBodyBytes: make([]byte, numUnknownBodyBytes),
|
||||||
|
NumberOfDirections: 0,
|
||||||
|
FramesPerDirection: 0,
|
||||||
|
NumberOfLayers: 0,
|
||||||
|
Speed: 0,
|
||||||
|
CofLayers: make([]CofLayer, 0),
|
||||||
|
CompositeLayers: make(map[d2enum.CompositeType]int),
|
||||||
|
AnimationFrames: make([]d2enum.AnimationFrame, 0),
|
||||||
|
Priority: make([][][]d2enum.CompositeType, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal a COF to a new byte slice
|
||||||
|
func Marshal(c *COF) []byte {
|
||||||
|
return c.Marshal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal a byte slice to a new COF
|
||||||
|
func Unmarshal(data []byte) (*COF, error) {
|
||||||
|
c := New()
|
||||||
|
err := c.Unmarshal(data)
|
||||||
|
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
// COF is a structure that represents a COF file.
|
// COF is a structure that represents a COF file.
|
||||||
type COF struct {
|
type COF struct {
|
||||||
|
// unknown bytes for header
|
||||||
|
unknownHeaderBytes []byte
|
||||||
|
// unknown bytes (first "body's" bytes)
|
||||||
|
unknownBodyBytes []byte
|
||||||
NumberOfDirections int
|
NumberOfDirections int
|
||||||
FramesPerDirection int
|
FramesPerDirection int
|
||||||
NumberOfLayers int
|
NumberOfLayers int
|
||||||
@ -19,58 +79,166 @@ type COF struct {
|
|||||||
Priority [][][]d2enum.CompositeType
|
Priority [][][]d2enum.CompositeType
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads a COF file.
|
// Unmarshal a byte slice to this COF
|
||||||
func Load(fileData []byte) (*COF, error) {
|
func (c *COF) Unmarshal(fileData []byte) error {
|
||||||
result := &COF{}
|
var err error
|
||||||
|
|
||||||
streamReader := d2datautils.CreateStreamReader(fileData)
|
streamReader := d2datautils.CreateStreamReader(fileData)
|
||||||
result.NumberOfLayers = int(streamReader.GetByte())
|
|
||||||
result.FramesPerDirection = int(streamReader.GetByte())
|
|
||||||
result.NumberOfDirections = int(streamReader.GetByte())
|
|
||||||
|
|
||||||
streamReader.SkipBytes(21) //nolint:gomnd // Unknown data
|
headerBytes, err := streamReader.ReadBytes(numHeaderBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
result.Speed = int(streamReader.GetByte())
|
c.loadHeader(headerBytes)
|
||||||
|
|
||||||
streamReader.SkipBytes(3) //nolint:gomnd // Unknown data
|
c.unknownBodyBytes, err = streamReader.ReadBytes(numUnknownBodyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
result.CofLayers = make([]CofLayer, result.NumberOfLayers)
|
c.CofLayers = make([]CofLayer, c.NumberOfLayers)
|
||||||
result.CompositeLayers = make(map[d2enum.CompositeType]int)
|
c.CompositeLayers = make(map[d2enum.CompositeType]int)
|
||||||
|
|
||||||
for i := 0; i < result.NumberOfLayers; i++ {
|
err = c.loadCOFLayers(streamReader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFramesData, err := streamReader.ReadBytes(c.FramesPerDirection)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.loadAnimationFrames(animationFramesData)
|
||||||
|
|
||||||
|
priorityLen := c.FramesPerDirection * c.NumberOfDirections * c.NumberOfLayers
|
||||||
|
c.Priority = make([][][]d2enum.CompositeType, c.NumberOfDirections)
|
||||||
|
|
||||||
|
priorityBytes, err := streamReader.ReadBytes(priorityLen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.loadPriority(priorityBytes)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *COF) loadHeader(b []byte) {
|
||||||
|
c.NumberOfLayers = int(b[headerNumLayers])
|
||||||
|
c.FramesPerDirection = int(b[headerFramesPerDir])
|
||||||
|
c.NumberOfDirections = int(b[headerNumDirs])
|
||||||
|
c.unknownHeaderBytes = b[headerNumDirs+1 : headerSpeed]
|
||||||
|
c.Speed = int(b[headerSpeed])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *COF) loadCOFLayers(streamReader *d2datautils.StreamReader) error {
|
||||||
|
for i := 0; i < c.NumberOfLayers; i++ {
|
||||||
layer := CofLayer{}
|
layer := CofLayer{}
|
||||||
layer.Type = d2enum.CompositeType(streamReader.GetByte())
|
|
||||||
layer.Shadow = streamReader.GetByte()
|
b, err := streamReader.ReadBytes(numLayerBytes)
|
||||||
layer.Selectable = streamReader.GetByte() != 0
|
if err != nil {
|
||||||
layer.Transparent = streamReader.GetByte() != 0
|
return err
|
||||||
layer.DrawEffect = d2enum.DrawEffect(streamReader.GetByte())
|
|
||||||
weaponClassStr := streamReader.ReadBytes(4) //nolint:gomnd // Binary data
|
|
||||||
layer.WeaponClass = d2enum.WeaponClassFromString(strings.TrimSpace(strings.ReplaceAll(string(weaponClassStr), string(byte(0)), "")))
|
|
||||||
result.CofLayers[i] = layer
|
|
||||||
result.CompositeLayers[layer.Type] = i
|
|
||||||
}
|
}
|
||||||
|
|
||||||
animationFrameBytes := streamReader.ReadBytes(result.FramesPerDirection)
|
layer.Type = d2enum.CompositeType(b[layerType])
|
||||||
result.AnimationFrames = make([]d2enum.AnimationFrame, result.FramesPerDirection)
|
layer.Shadow = b[layerShadow]
|
||||||
|
layer.Selectable = b[layerSelectable] > 0
|
||||||
|
layer.Transparent = b[layerTransparent] > 0
|
||||||
|
layer.DrawEffect = d2enum.DrawEffect(b[layerDrawEffect])
|
||||||
|
|
||||||
for i := range animationFrameBytes {
|
layer.WeaponClass = d2enum.WeaponClassFromString(strings.TrimSpace(strings.ReplaceAll(
|
||||||
result.AnimationFrames[i] = d2enum.AnimationFrame(animationFrameBytes[i])
|
string(b[layerWeaponClass:]), badCharacter, "")))
|
||||||
|
|
||||||
|
c.CofLayers[i] = layer
|
||||||
|
c.CompositeLayers[layer.Type] = i
|
||||||
}
|
}
|
||||||
|
|
||||||
priorityLen := result.FramesPerDirection * result.NumberOfDirections * result.NumberOfLayers
|
return nil
|
||||||
result.Priority = make([][][]d2enum.CompositeType, result.NumberOfDirections)
|
}
|
||||||
priorityBytes := streamReader.ReadBytes(priorityLen)
|
|
||||||
|
func (c *COF) loadAnimationFrames(b []byte) {
|
||||||
|
c.AnimationFrames = make([]d2enum.AnimationFrame, c.FramesPerDirection)
|
||||||
|
|
||||||
|
for i := range b {
|
||||||
|
c.AnimationFrames[i] = d2enum.AnimationFrame(b[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *COF) loadPriority(priorityBytes []byte) {
|
||||||
priorityIndex := 0
|
priorityIndex := 0
|
||||||
|
|
||||||
for direction := 0; direction < result.NumberOfDirections; direction++ {
|
for direction := 0; direction < c.NumberOfDirections; direction++ {
|
||||||
result.Priority[direction] = make([][]d2enum.CompositeType, result.FramesPerDirection)
|
c.Priority[direction] = make([][]d2enum.CompositeType, c.FramesPerDirection)
|
||||||
for frame := 0; frame < result.FramesPerDirection; frame++ {
|
for frame := 0; frame < c.FramesPerDirection; frame++ {
|
||||||
result.Priority[direction][frame] = make([]d2enum.CompositeType, result.NumberOfLayers)
|
c.Priority[direction][frame] = make([]d2enum.CompositeType, c.NumberOfLayers)
|
||||||
for i := 0; i < result.NumberOfLayers; i++ {
|
for i := 0; i < c.NumberOfLayers; i++ {
|
||||||
result.Priority[direction][frame][i] = d2enum.CompositeType(priorityBytes[priorityIndex])
|
c.Priority[direction][frame][i] = d2enum.CompositeType(priorityBytes[priorityIndex])
|
||||||
priorityIndex++
|
priorityIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result, nil
|
|
||||||
|
// Marshal this COF to a byte slice
|
||||||
|
func (c *COF) Marshal() []byte {
|
||||||
|
sw := d2datautils.CreateStreamWriter()
|
||||||
|
|
||||||
|
sw.PushBytes(byte(c.NumberOfLayers))
|
||||||
|
sw.PushBytes(byte(c.FramesPerDirection))
|
||||||
|
sw.PushBytes(byte(c.NumberOfDirections))
|
||||||
|
sw.PushBytes(c.unknownHeaderBytes...)
|
||||||
|
sw.PushBytes(byte(c.Speed))
|
||||||
|
sw.PushBytes(c.unknownBodyBytes...)
|
||||||
|
|
||||||
|
for i := range c.CofLayers {
|
||||||
|
sw.PushBytes(byte(c.CofLayers[i].Type))
|
||||||
|
sw.PushBytes(c.CofLayers[i].Shadow)
|
||||||
|
|
||||||
|
if c.CofLayers[i].Selectable {
|
||||||
|
sw.PushBytes(byte(1))
|
||||||
|
} else {
|
||||||
|
sw.PushBytes(byte(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.CofLayers[i].Transparent {
|
||||||
|
sw.PushBytes(byte(1))
|
||||||
|
} else {
|
||||||
|
sw.PushBytes(byte(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.PushBytes(byte(c.CofLayers[i].DrawEffect))
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxCodeLength = 3 // we assume item codes to look like 'hax' or 'kit'
|
||||||
|
terminator = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
weaponCode := c.CofLayers[i].WeaponClass.String()
|
||||||
|
|
||||||
|
for idx, letter := range weaponCode {
|
||||||
|
if idx > maxCodeLength {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.PushBytes(byte(letter))
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.PushBytes(terminator)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range c.AnimationFrames {
|
||||||
|
sw.PushBytes(byte(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
for direction := 0; direction < c.NumberOfDirections; direction++ {
|
||||||
|
for frame := 0; frame < c.FramesPerDirection; frame++ {
|
||||||
|
for i := 0; i < c.NumberOfLayers; i++ {
|
||||||
|
sw.PushBytes(byte(c.Priority[direction][frame][i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sw.GetBytes()
|
||||||
}
|
}
|
||||||
|
35
d2common/d2fileformats/d2cof/cof_test.go
Normal file
35
d2common/d2fileformats/d2cof/cof_test.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package d2cof
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCOF_New(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
|
||||||
|
if c == nil {
|
||||||
|
t.Error("method New created nil instance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCOF_Marshal_Unmarshal(t *testing.T) {
|
||||||
|
cof1 := New()
|
||||||
|
cof2 := New()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
err = cof1.Unmarshal(make([]byte, 1000))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cof1.Speed = 255
|
||||||
|
data1 := cof1.Marshal()
|
||||||
|
|
||||||
|
err = cof2.Unmarshal(data1)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cof2.Speed != cof1.Speed {
|
||||||
|
t.Error("marshaled data does not match unmarshaled data")
|
||||||
|
}
|
||||||
|
}
|
27
d2common/d2fileformats/d2cof/helpers.go
Normal file
27
d2common/d2fileformats/d2cof/helpers.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package d2cof
|
||||||
|
|
||||||
|
// FPS returns FPS value basing on cof's speed
|
||||||
|
func (c *COF) FPS() float64 {
|
||||||
|
const (
|
||||||
|
baseFPS = 25
|
||||||
|
speedDivisor = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
fps := baseFPS * (float64(c.Speed) / speedDivisor)
|
||||||
|
if fps == 0 {
|
||||||
|
fps = baseFPS
|
||||||
|
}
|
||||||
|
|
||||||
|
return fps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration returns animation's duration
|
||||||
|
func (c *COF) Duration() float64 {
|
||||||
|
const (
|
||||||
|
milliseconds = 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
frameDelay := milliseconds / c.FPS()
|
||||||
|
|
||||||
|
return float64(c.FramesPerDirection) * frameDelay
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package d2dat
|
package d2dat
|
||||||
|
|
||||||
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
import (
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// index offset helpers
|
// index offset helpers
|
||||||
@ -21,3 +23,14 @@ func Load(data []byte) (d2interface.Palette, error) {
|
|||||||
|
|
||||||
return palette, nil
|
return palette, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marshal encodes data palette back into byte slice
|
||||||
|
func (p *DATPalette) Marshal() []byte {
|
||||||
|
result := make([]byte, 0)
|
||||||
|
|
||||||
|
for _, i := range &p.colors {
|
||||||
|
result = append(result, i.B(), i.G(), i.R())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -15,6 +15,16 @@ type DATPalette struct {
|
|||||||
colors [numColors]d2interface.Color
|
colors [numColors]d2interface.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new dat palette
|
||||||
|
func New() *DATPalette {
|
||||||
|
result := &DATPalette{}
|
||||||
|
for i := range result.colors {
|
||||||
|
result.colors[i] = &DATColor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// NumColors returns the number of colors in the palette
|
// NumColors returns the number of colors in the palette
|
||||||
func (p *DATPalette) NumColors() int {
|
func (p *DATPalette) NumColors() int {
|
||||||
return len(p.colors)
|
return len(p.colors)
|
||||||
|
@ -7,6 +7,9 @@ import (
|
|||||||
const (
|
const (
|
||||||
endOfScanLine = 0x80
|
endOfScanLine = 0x80
|
||||||
maxRunLength = 0x7f
|
maxRunLength = 0x7f
|
||||||
|
|
||||||
|
terminationSize = 4
|
||||||
|
terminatorSize = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type scanlineState int
|
type scanlineState int
|
||||||
@ -29,49 +32,179 @@ type DC6 struct {
|
|||||||
Frames []*DC6Frame // size is Directions*FramesPerDirection
|
Frames []*DC6Frame // size is Directions*FramesPerDirection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load uses restruct to read the binary dc6 data into structs then parses image data from the frame data.
|
// New creates a new, empty DC6
|
||||||
|
func New() *DC6 {
|
||||||
|
result := &DC6{
|
||||||
|
Version: 0,
|
||||||
|
Flags: 0,
|
||||||
|
Encoding: 0,
|
||||||
|
Termination: make([]byte, 4),
|
||||||
|
Directions: 0,
|
||||||
|
FramesPerDirection: 0,
|
||||||
|
FramePointers: make([]uint32, 0),
|
||||||
|
Frames: make([]*DC6Frame, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads a dc6 animation
|
||||||
func Load(data []byte) (*DC6, error) {
|
func Load(data []byte) (*DC6, error) {
|
||||||
const (
|
d := New()
|
||||||
terminationSize = 4
|
|
||||||
terminatorSize = 3
|
err := d.Unmarshal(data)
|
||||||
)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal converts bite slice into DC6 structure
|
||||||
|
func (d *DC6) Unmarshal(data []byte) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
r := d2datautils.CreateStreamReader(data)
|
r := d2datautils.CreateStreamReader(data)
|
||||||
|
|
||||||
var dc DC6
|
err = d.loadHeader(r)
|
||||||
dc.Version = r.GetInt32()
|
if err != nil {
|
||||||
dc.Flags = r.GetUInt32()
|
return err
|
||||||
dc.Encoding = r.GetUInt32()
|
}
|
||||||
dc.Termination = r.ReadBytes(terminationSize)
|
|
||||||
dc.Directions = r.GetUInt32()
|
|
||||||
dc.FramesPerDirection = r.GetUInt32()
|
|
||||||
|
|
||||||
frameCount := int(dc.Directions * dc.FramesPerDirection)
|
frameCount := int(d.Directions * d.FramesPerDirection)
|
||||||
|
|
||||||
dc.FramePointers = make([]uint32, frameCount)
|
d.FramePointers = make([]uint32, frameCount)
|
||||||
for i := 0; i < frameCount; i++ {
|
for i := 0; i < frameCount; i++ {
|
||||||
dc.FramePointers[i] = r.GetUInt32()
|
d.FramePointers[i], err = r.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dc.Frames = make([]*DC6Frame, frameCount)
|
d.Frames = make([]*DC6Frame, frameCount)
|
||||||
|
|
||||||
for i := 0; i < frameCount; i++ {
|
if err := d.loadFrames(r); err != nil {
|
||||||
frame := &DC6Frame{
|
return err
|
||||||
Flipped: r.GetUInt32(),
|
|
||||||
Width: r.GetUInt32(),
|
|
||||||
Height: r.GetUInt32(),
|
|
||||||
OffsetX: r.GetInt32(),
|
|
||||||
OffsetY: r.GetInt32(),
|
|
||||||
Unknown: r.GetUInt32(),
|
|
||||||
NextBlock: r.GetUInt32(),
|
|
||||||
Length: r.GetUInt32(),
|
|
||||||
}
|
|
||||||
frame.FrameData = r.ReadBytes(int(frame.Length))
|
|
||||||
frame.Terminator = r.ReadBytes(terminatorSize)
|
|
||||||
dc.Frames[i] = frame
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &dc, nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DC6) loadHeader(r *d2datautils.StreamReader) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if d.Version, err = r.ReadInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Flags, err = r.ReadUInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Encoding, err = r.ReadUInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Termination, err = r.ReadBytes(terminationSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Directions, err = r.ReadUInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.FramesPerDirection, err = r.ReadUInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DC6) loadFrames(r *d2datautils.StreamReader) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for i := 0; i < len(d.FramePointers); i++ {
|
||||||
|
frame := &DC6Frame{}
|
||||||
|
|
||||||
|
if frame.Flipped, err = r.ReadUInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.Width, err = r.ReadUInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.Height, err = r.ReadUInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.OffsetX, err = r.ReadInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.OffsetY, err = r.ReadInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.Unknown, err = r.ReadUInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.NextBlock, err = r.ReadUInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.Length, err = r.ReadUInt32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.FrameData, err = r.ReadBytes(int(frame.Length)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.Terminator, err = r.ReadBytes(terminatorSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Frames[i] = frame
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal encodes dc6 animation back into byte slice
|
||||||
|
func (d *DC6) Marshal() []byte {
|
||||||
|
sw := d2datautils.CreateStreamWriter()
|
||||||
|
|
||||||
|
// Encode header
|
||||||
|
sw.PushInt32(d.Version)
|
||||||
|
sw.PushUint32(d.Flags)
|
||||||
|
sw.PushUint32(d.Encoding)
|
||||||
|
|
||||||
|
sw.PushBytes(d.Termination...)
|
||||||
|
|
||||||
|
sw.PushUint32(d.Directions)
|
||||||
|
sw.PushUint32(d.FramesPerDirection)
|
||||||
|
|
||||||
|
// load frames
|
||||||
|
for _, i := range d.FramePointers {
|
||||||
|
sw.PushUint32(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range d.Frames {
|
||||||
|
sw.PushUint32(d.Frames[i].Flipped)
|
||||||
|
sw.PushUint32(d.Frames[i].Width)
|
||||||
|
sw.PushUint32(d.Frames[i].Height)
|
||||||
|
sw.PushInt32(d.Frames[i].OffsetX)
|
||||||
|
sw.PushInt32(d.Frames[i].OffsetY)
|
||||||
|
sw.PushUint32(d.Frames[i].Unknown)
|
||||||
|
sw.PushUint32(d.Frames[i].NextBlock)
|
||||||
|
sw.PushUint32(d.Frames[i].Length)
|
||||||
|
sw.PushBytes(d.Frames[i].FrameData...)
|
||||||
|
sw.PushBytes(d.Frames[i].Terminator...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sw.GetBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeFrame decodes the given frame to an indexed color texture
|
// DecodeFrame decodes the given frame to an indexed color texture
|
||||||
@ -134,7 +267,7 @@ func (d *DC6) Clone() *DC6 {
|
|||||||
|
|
||||||
for i := range d.Frames {
|
for i := range d.Frames {
|
||||||
cloneFrame := *d.Frames[i]
|
cloneFrame := *d.Frames[i]
|
||||||
clone.Frames = append(clone.Frames, &cloneFrame)
|
clone.Frames[i] = &cloneFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
return &clone
|
return &clone
|
||||||
|
69
d2common/d2fileformats/d2dc6/dc6_test.go
Normal file
69
d2common/d2fileformats/d2dc6/dc6_test.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package d2dc6
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDC6New(t *testing.T) {
|
||||||
|
dc6 := New()
|
||||||
|
|
||||||
|
if dc6 == nil {
|
||||||
|
t.Error("d2dc6.New() method returned nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExampleDC6() *DC6 {
|
||||||
|
exampleDC6 := &DC6{
|
||||||
|
Version: 6,
|
||||||
|
Flags: 1,
|
||||||
|
Encoding: 0,
|
||||||
|
Termination: []byte{238, 238, 238, 238},
|
||||||
|
Directions: 1,
|
||||||
|
FramesPerDirection: 1,
|
||||||
|
FramePointers: []uint32{56},
|
||||||
|
Frames: []*DC6Frame{
|
||||||
|
{
|
||||||
|
Flipped: 0,
|
||||||
|
Width: 32,
|
||||||
|
Height: 26,
|
||||||
|
OffsetX: 45,
|
||||||
|
OffsetY: 24,
|
||||||
|
Unknown: 0,
|
||||||
|
NextBlock: 50,
|
||||||
|
Length: 10,
|
||||||
|
FrameData: []byte{2, 23, 34, 128, 53, 64, 39, 43, 123, 12},
|
||||||
|
Terminator: []byte{2, 8, 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return exampleDC6
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDC6Unmarshal(t *testing.T) {
|
||||||
|
exampleDC6 := getExampleDC6()
|
||||||
|
|
||||||
|
data := exampleDC6.Marshal()
|
||||||
|
|
||||||
|
extractedDC6, err := Load(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exampleDC6.Version != extractedDC6.Version ||
|
||||||
|
len(exampleDC6.Frames) != len(extractedDC6.Frames) ||
|
||||||
|
exampleDC6.Frames[0].NextBlock != extractedDC6.Frames[0].NextBlock {
|
||||||
|
t.Fatal("encoded and decoded DC6 isn't the same")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDC6Clone(t *testing.T) {
|
||||||
|
exampleDC6 := getExampleDC6()
|
||||||
|
clonedDC6 := exampleDC6.Clone()
|
||||||
|
|
||||||
|
if exampleDC6.Termination[0] != clonedDC6.Termination[0] ||
|
||||||
|
len(exampleDC6.Frames) != len(clonedDC6.Frames) ||
|
||||||
|
exampleDC6.Frames[0].NextBlock != clonedDC6.Frames[0].NextBlock {
|
||||||
|
t.Fatal("cloned dc6 isn't equal to original")
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,13 @@ import (
|
|||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseMinx = 100000
|
||||||
|
baseMiny = 100000
|
||||||
|
baseMaxx = -100000
|
||||||
|
baseMaxy = -100000
|
||||||
|
)
|
||||||
|
|
||||||
const cellsPerRow = 4
|
const cellsPerRow = 4
|
||||||
|
|
||||||
// DCCDirection represents a DCCDirection file.
|
// DCCDirection represents a DCCDirection file.
|
||||||
@ -37,7 +44,9 @@ type DCCDirection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateDCCDirection creates an instance of a DCCDirection.
|
// CreateDCCDirection creates an instance of a DCCDirection.
|
||||||
|
// nolint:funlen // no need to reduce
|
||||||
func CreateDCCDirection(bm *d2datautils.BitMuncher, file *DCC) *DCCDirection {
|
func CreateDCCDirection(bm *d2datautils.BitMuncher, file *DCC) *DCCDirection {
|
||||||
|
// nolint:gomnd // constant
|
||||||
var crazyBitTable = []byte{0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 26, 28, 30, 32}
|
var crazyBitTable = []byte{0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 26, 28, 30, 32}
|
||||||
|
|
||||||
result := &DCCDirection{
|
result := &DCCDirection{
|
||||||
@ -53,10 +62,10 @@ func CreateDCCDirection(bm *d2datautils.BitMuncher, file *DCC) *DCCDirection {
|
|||||||
Frames: make([]*DCCDirectionFrame, file.FramesPerDirection),
|
Frames: make([]*DCCDirectionFrame, file.FramesPerDirection),
|
||||||
}
|
}
|
||||||
|
|
||||||
minx := 100000
|
minx := baseMinx
|
||||||
miny := 100000
|
miny := baseMiny
|
||||||
maxx := -100000
|
maxx := baseMaxx
|
||||||
maxy := -100000
|
maxy := baseMaxy
|
||||||
|
|
||||||
// Load the frame headers
|
// Load the frame headers
|
||||||
for frameIdx := 0; frameIdx < file.FramesPerDirection; frameIdx++ {
|
for frameIdx := 0; frameIdx < file.FramesPerDirection; frameIdx++ {
|
||||||
@ -73,12 +82,14 @@ func CreateDCCDirection(bm *d2datautils.BitMuncher, file *DCC) *DCCDirection {
|
|||||||
log.Panic("Optional bits in DCC data is not currently supported.")
|
log.Panic("Optional bits in DCC data is not currently supported.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gomnd // byte operation
|
||||||
if (result.CompressionFlags & 0x2) > 0 {
|
if (result.CompressionFlags & 0x2) > 0 {
|
||||||
result.EqualCellsBitstreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data
|
result.EqualCellsBitstreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data
|
||||||
}
|
}
|
||||||
|
|
||||||
result.PixelMaskBitstreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data
|
result.PixelMaskBitstreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data
|
||||||
|
|
||||||
|
// nolint:gomnd // byte operation
|
||||||
if (result.CompressionFlags & 0x1) > 0 {
|
if (result.CompressionFlags & 0x1) > 0 {
|
||||||
result.EncodingTypeBitsreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data
|
result.EncodingTypeBitsreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data
|
||||||
result.RawPixelCodesBitstreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data
|
result.RawPixelCodesBitstreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data
|
||||||
@ -412,9 +423,12 @@ func (v *DCCDirection) calculateCells() {
|
|||||||
for i := 0; i < v.HorizontalCellCount-1; i++ {
|
for i := 0; i < v.HorizontalCellCount-1; i++ {
|
||||||
cellWidths[i] = 4
|
cellWidths[i] = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gomnd // constant
|
||||||
cellWidths[v.HorizontalCellCount-1] = v.Box.Width - (4 * (v.HorizontalCellCount - 1))
|
cellWidths[v.HorizontalCellCount-1] = v.Box.Width - (4 * (v.HorizontalCellCount - 1))
|
||||||
}
|
}
|
||||||
// Calculate the cell heights
|
// Calculate the cell heights
|
||||||
|
// nolint:gomnd // constant
|
||||||
cellHeights := make([]int, v.VerticalCellCount)
|
cellHeights := make([]int, v.VerticalCellCount)
|
||||||
if v.VerticalCellCount == 1 {
|
if v.VerticalCellCount == 1 {
|
||||||
cellHeights[0] = v.Box.Height
|
cellHeights[0] = v.Box.Height
|
||||||
@ -422,6 +436,8 @@ func (v *DCCDirection) calculateCells() {
|
|||||||
for i := 0; i < v.VerticalCellCount-1; i++ {
|
for i := 0; i < v.VerticalCellCount-1; i++ {
|
||||||
cellHeights[i] = 4
|
cellHeights[i] = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gomnd // constant
|
||||||
cellHeights[v.VerticalCellCount-1] = v.Box.Height - (4 * (v.VerticalCellCount - 1))
|
cellHeights[v.VerticalCellCount-1] = v.Box.Height - (4 * (v.VerticalCellCount - 1))
|
||||||
}
|
}
|
||||||
// Set the cell widths and heights in the cell buffer
|
// Set the cell widths and heights in the cell buffer
|
||||||
|
@ -55,6 +55,7 @@ func CreateDCCDirectionFrame(bits *d2datautils.BitMuncher, direction *DCCDirecti
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *DCCDirectionFrame) recalculateCells(direction *DCCDirection) {
|
func (v *DCCDirectionFrame) recalculateCells(direction *DCCDirection) {
|
||||||
|
// nolint:gomnd // constant
|
||||||
var w = 4 - ((v.Box.Left - direction.Box.Left) % 4) // Width of the first column (in pixels)
|
var w = 4 - ((v.Box.Left - direction.Box.Left) % 4) // Width of the first column (in pixels)
|
||||||
|
|
||||||
if (v.Width - w) <= 1 {
|
if (v.Width - w) <= 1 {
|
||||||
@ -62,6 +63,8 @@ func (v *DCCDirectionFrame) recalculateCells(direction *DCCDirection) {
|
|||||||
} else {
|
} else {
|
||||||
tmp := v.Width - w - 1
|
tmp := v.Width - w - 1
|
||||||
v.HorizontalCellCount = 2 + (tmp / 4) //nolint:gomnd // magic math
|
v.HorizontalCellCount = 2 + (tmp / 4) //nolint:gomnd // magic math
|
||||||
|
|
||||||
|
// nolint:gomnd // constant
|
||||||
if (tmp % 4) == 0 {
|
if (tmp % 4) == 0 {
|
||||||
v.HorizontalCellCount--
|
v.HorizontalCellCount--
|
||||||
}
|
}
|
||||||
@ -75,6 +78,8 @@ func (v *DCCDirectionFrame) recalculateCells(direction *DCCDirection) {
|
|||||||
} else {
|
} else {
|
||||||
tmp := v.Height - h - 1
|
tmp := v.Height - h - 1
|
||||||
v.VerticalCellCount = 2 + (tmp / 4) //nolint:gomnd // data decode
|
v.VerticalCellCount = 2 + (tmp / 4) //nolint:gomnd // data decode
|
||||||
|
|
||||||
|
// nolint:gomnd // constant
|
||||||
if (tmp % 4) == 0 {
|
if (tmp % 4) == 0 {
|
||||||
v.VerticalCellCount--
|
v.VerticalCellCount--
|
||||||
}
|
}
|
||||||
@ -88,6 +93,8 @@ func (v *DCCDirectionFrame) recalculateCells(direction *DCCDirection) {
|
|||||||
for i := 1; i < (v.HorizontalCellCount - 1); i++ {
|
for i := 1; i < (v.HorizontalCellCount - 1); i++ {
|
||||||
cellWidths[i] = 4
|
cellWidths[i] = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gomnd // constants
|
||||||
cellWidths[v.HorizontalCellCount-1] = v.Width - w - (4 * (v.HorizontalCellCount - 2))
|
cellWidths[v.HorizontalCellCount-1] = v.Width - w - (4 * (v.HorizontalCellCount - 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +106,8 @@ func (v *DCCDirectionFrame) recalculateCells(direction *DCCDirection) {
|
|||||||
for i := 1; i < (v.VerticalCellCount - 1); i++ {
|
for i := 1; i < (v.VerticalCellCount - 1); i++ {
|
||||||
cellHeights[i] = 4
|
cellHeights[i] = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gomnd // constants
|
||||||
cellHeights[v.VerticalCellCount-1] = v.Height - h - (4 * (v.VerticalCellCount - 2))
|
cellHeights[v.VerticalCellCount-1] = v.Height - h - (4 * (v.VerticalCellCount - 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
// Package d2ds1 provides functionality for loading/processing DS1 files
|
// Package d2ds1 provides functionality for loading/processing DS1 Files
|
||||||
package d2ds1
|
package d2ds1
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package d2ds1
|
package d2ds1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
|
||||||
@ -8,61 +10,212 @@ import (
|
|||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2path"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2path"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxActNumber = 5
|
const (
|
||||||
|
subType1 = 1
|
||||||
|
subType2 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
wallZeroBitmask = 0xFFFFFF00
|
||||||
|
wallZeroOffset = 8
|
||||||
|
wallTypeBitmask = 0x000000FF
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
unknown1BytesCount = 8
|
||||||
|
)
|
||||||
|
|
||||||
// DS1 represents the "stamp" data that is used to build up maps.
|
// DS1 represents the "stamp" data that is used to build up maps.
|
||||||
type DS1 struct {
|
type DS1 struct {
|
||||||
|
*ds1Layers
|
||||||
Files []string // FilePtr table of file string pointers
|
Files []string // FilePtr table of file string pointers
|
||||||
Objects []Object // Objects
|
Objects []Object // Objects
|
||||||
Tiles [][]TileRecord // The tile data for the DS1
|
|
||||||
SubstitutionGroups []SubstitutionGroup // Substitution groups for the DS1
|
SubstitutionGroups []SubstitutionGroup // Substitution groups for the DS1
|
||||||
Version int32 // The version of the DS1
|
|
||||||
Width int32 // Width of map, in # of tiles
|
version ds1version
|
||||||
Height int32 // Height of map, in # of tiles
|
Act int32 // Act, from 1 to 5. This tells which Act table to use for the Objects list
|
||||||
Act int32 // Act, from 1 to 5. This tells which act table to use for the Objects list
|
|
||||||
SubstitutionType int32 // SubstitutionType (layer type): 0 if no layer, else type 1 or type 2
|
SubstitutionType int32 // SubstitutionType (layer type): 0 if no layer, else type 1 or type 2
|
||||||
NumberOfWalls int32 // WallNum number of wall & orientation layers used
|
unknown1 [unknown1BytesCount]byte
|
||||||
NumberOfFloors int32 // number of floor layers used
|
unknown2 uint32
|
||||||
NumberOfShadowLayers int32 // ShadowNum number of shadow layer used
|
|
||||||
NumberOfSubstitutionLayers int32 // SubstitutionNum number of substitution layer used
|
|
||||||
SubstitutionGroupsNum int32 // SubstitutionGroupsNum number of substitution groups, datas between objects & NPC paths
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadDS1 loads the specified DS1 file
|
const (
|
||||||
func LoadDS1(fileData []byte) (*DS1, error) {
|
defaultNumFloors = 1
|
||||||
ds1 := &DS1{
|
defaultNumShadows = maxShadowLayers
|
||||||
Act: 1,
|
defaultNumSubstitutions = 0
|
||||||
NumberOfFloors: 0,
|
)
|
||||||
NumberOfWalls: 0,
|
|
||||||
NumberOfShadowLayers: 1,
|
|
||||||
NumberOfSubstitutionLayers: 0,
|
|
||||||
}
|
|
||||||
br := d2datautils.CreateStreamReader(fileData)
|
|
||||||
ds1.Version = br.GetInt32()
|
|
||||||
ds1.Width = br.GetInt32() + 1
|
|
||||||
ds1.Height = br.GetInt32() + 1
|
|
||||||
|
|
||||||
if ds1.Version >= 8 { //nolint:gomnd // Version number
|
// Unmarshal the given bytes to a DS1 struct
|
||||||
ds1.Act = d2math.MinInt32(maxActNumber, br.GetInt32()+1)
|
func Unmarshal(fileData []byte) (*DS1, error) {
|
||||||
|
return (&DS1{}).Unmarshal(fileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal the given bytes to a DS1 struct
|
||||||
|
func (ds1 *DS1) Unmarshal(fileData []byte) (*DS1, error) {
|
||||||
|
ds1.ds1Layers = &ds1Layers{}
|
||||||
|
|
||||||
|
stream := d2datautils.CreateStreamReader(fileData)
|
||||||
|
|
||||||
|
if err := ds1.loadHeader(stream); err != nil {
|
||||||
|
return nil, fmt.Errorf("loading header: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ds1.Version >= 10 { //nolint:gomnd // Version number
|
if err := ds1.loadBody(stream); err != nil {
|
||||||
ds1.SubstitutionType = br.GetInt32()
|
return nil, fmt.Errorf("loading body: %w", err)
|
||||||
if ds1.SubstitutionType == 1 || ds1.SubstitutionType == 2 {
|
}
|
||||||
ds1.NumberOfSubstitutionLayers = 1
|
|
||||||
|
return ds1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds1 *DS1) loadHeader(br *d2datautils.StreamReader) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var width, height int32
|
||||||
|
|
||||||
|
v, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ds1.version = ds1version(v)
|
||||||
|
|
||||||
|
width, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading width: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
height, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading height: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
width++
|
||||||
|
height++
|
||||||
|
|
||||||
|
ds1.SetSize(int(width), int(height))
|
||||||
|
|
||||||
|
if ds1.version.specifiesAct() {
|
||||||
|
ds1.Act, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading Act: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ds1.Act = d2math.MinInt32(d2enum.ActsNumber, ds1.Act+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds1.version.specifiesSubstitutionType() {
|
||||||
|
ds1.SubstitutionType, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading substitution type: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ds1.SubstitutionType {
|
||||||
|
case subType1, subType2:
|
||||||
|
ds1.PushSubstitution(&Layer{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ds1.Version >= 3 { //nolint:gomnd // Version number
|
err = ds1.loadFileList(br)
|
||||||
// These files reference things that don't exist anymore :-?
|
if err != nil {
|
||||||
numberOfFiles := br.GetInt32()
|
return fmt.Errorf("loading file list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds1 *DS1) loadBody(stream *d2datautils.StreamReader) error {
|
||||||
|
var numWalls, numFloors, numShadows, numSubstitutions int32
|
||||||
|
|
||||||
|
numFloors = defaultNumFloors
|
||||||
|
numShadows = defaultNumShadows
|
||||||
|
numSubstitutions = defaultNumSubstitutions
|
||||||
|
|
||||||
|
if ds1.version.hasUnknown1Bytes() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
bytes, err := stream.ReadBytes(unknown1BytesCount)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading unknown1: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(ds1.unknown1[:], bytes[:unknown1BytesCount])
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds1.version.specifiesWalls() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
numWalls, err = stream.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading wall number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds1.version.specifiesFloors() {
|
||||||
|
numFloors, err = stream.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading number of Floors: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ; numWalls > 0; numWalls-- {
|
||||||
|
ds1.PushWall(&Layer{})
|
||||||
|
}
|
||||||
|
|
||||||
|
for ; numShadows > 0; numShadows-- {
|
||||||
|
ds1.PushShadow(&Layer{})
|
||||||
|
}
|
||||||
|
|
||||||
|
for ; numFloors > 0; numFloors-- {
|
||||||
|
ds1.PushFloor(&Layer{})
|
||||||
|
}
|
||||||
|
|
||||||
|
for ; numSubstitutions > 0; numSubstitutions-- {
|
||||||
|
ds1.PushSubstitution(&Layer{})
|
||||||
|
}
|
||||||
|
|
||||||
|
ds1.SetSize(ds1.width, ds1.height)
|
||||||
|
|
||||||
|
if err := ds1.loadLayerStreams(stream); err != nil {
|
||||||
|
return fmt.Errorf("loading layer streams: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ds1.loadObjects(stream); err != nil {
|
||||||
|
return fmt.Errorf("loading Objects: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ds1.loadSubstitutions(stream); err != nil {
|
||||||
|
return fmt.Errorf("loading Substitutions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ds1.loadNPCs(stream); err != nil {
|
||||||
|
return fmt.Errorf("loading npc's: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds1 *DS1) loadFileList(br *d2datautils.StreamReader) error {
|
||||||
|
if !ds1.version.hasFileList() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// These Files reference things that don't exist anymore :-?
|
||||||
|
numberOfFiles, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading number of Files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
ds1.Files = make([]string, numberOfFiles)
|
ds1.Files = make([]string, numberOfFiles)
|
||||||
|
|
||||||
for i := 0; i < int(numberOfFiles); i++ {
|
for i := 0; i < int(numberOfFiles); i++ {
|
||||||
ds1.Files[i] = ""
|
ds1.Files[i] = ""
|
||||||
|
|
||||||
for {
|
for {
|
||||||
ch := br.GetByte()
|
ch, err := br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading file character: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if ch == 0 {
|
if ch == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -70,228 +223,470 @@ func LoadDS1(fileData []byte) (*DS1, error) {
|
|||||||
ds1.Files[i] += string(ch)
|
ds1.Files[i] += string(ch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ds1.Version >= 9 && ds1.Version <= 13 {
|
return nil
|
||||||
// Skipping two dwords because they are "meaningless"?
|
|
||||||
br.SkipBytes(8) //nolint:gomnd // We don't know what's here
|
|
||||||
}
|
|
||||||
|
|
||||||
if ds1.Version >= 4 { //nolint:gomnd // Version number
|
|
||||||
ds1.NumberOfWalls = br.GetInt32()
|
|
||||||
if ds1.Version >= 16 { //nolint:gomnd // Version number
|
|
||||||
ds1.NumberOfFloors = br.GetInt32()
|
|
||||||
} else {
|
|
||||||
ds1.NumberOfFloors = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layerStream := ds1.setupStreamLayerTypes()
|
|
||||||
|
|
||||||
ds1.Tiles = make([][]TileRecord, ds1.Height)
|
|
||||||
|
|
||||||
for y := range ds1.Tiles {
|
|
||||||
ds1.Tiles[y] = make([]TileRecord, ds1.Width)
|
|
||||||
for x := 0; x < int(ds1.Width); x++ {
|
|
||||||
ds1.Tiles[y][x].Walls = make([]WallRecord, ds1.NumberOfWalls)
|
|
||||||
ds1.Tiles[y][x].Floors = make([]FloorShadowRecord, ds1.NumberOfFloors)
|
|
||||||
ds1.Tiles[y][x].Shadows = make([]FloorShadowRecord, ds1.NumberOfShadowLayers)
|
|
||||||
ds1.Tiles[y][x].Substitutions = make([]SubstitutionRecord, ds1.NumberOfSubstitutionLayers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ds1.loadLayerStreams(br, layerStream)
|
|
||||||
ds1.loadObjects(br)
|
|
||||||
ds1.loadSubstitutions(br)
|
|
||||||
ds1.loadNPCs(br)
|
|
||||||
|
|
||||||
return ds1, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds1 *DS1) loadObjects(br *d2datautils.StreamReader) {
|
func (ds1 *DS1) loadObjects(br *d2datautils.StreamReader) error {
|
||||||
if ds1.Version >= 2 { //nolint:gomnd // Version number
|
if !ds1.version.hasObjects() {
|
||||||
numberOfObjects := br.GetInt32()
|
|
||||||
ds1.Objects = make([]Object, numberOfObjects)
|
|
||||||
|
|
||||||
for objIdx := 0; objIdx < int(numberOfObjects); objIdx++ {
|
|
||||||
newObject := Object{}
|
|
||||||
newObject.Type = int(br.GetInt32())
|
|
||||||
newObject.ID = int(br.GetInt32())
|
|
||||||
newObject.X = int(br.GetInt32())
|
|
||||||
newObject.Y = int(br.GetInt32())
|
|
||||||
newObject.Flags = int(br.GetInt32())
|
|
||||||
|
|
||||||
ds1.Objects[objIdx] = newObject
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ds1.Objects = make([]Object, 0)
|
ds1.Objects = make([]Object, 0)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
numObjects, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading number of Objects: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ds1.Objects = make([]Object, numObjects)
|
||||||
|
|
||||||
|
for objIdx := 0; objIdx < int(numObjects); objIdx++ {
|
||||||
|
obj := Object{}
|
||||||
|
|
||||||
|
objType, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading object's %d type: %v", objIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objID, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading object's %d ID: %v", objIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objX, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading object's %d X: %v", objIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objY, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading object's %d Y: %v", objY, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objFlags, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading object's %d flags: %v", objIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.Type = int(objType)
|
||||||
|
obj.ID = int(objID)
|
||||||
|
obj.X = int(objX)
|
||||||
|
obj.Y = int(objY)
|
||||||
|
obj.Flags = int(objFlags)
|
||||||
|
|
||||||
|
ds1.Objects[objIdx] = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds1 *DS1) loadSubstitutions(br *d2datautils.StreamReader) {
|
func (ds1 *DS1) loadSubstitutions(br *d2datautils.StreamReader) error {
|
||||||
if ds1.Version >= 12 && (ds1.SubstitutionType == 1 || ds1.SubstitutionType == 2) {
|
var err error
|
||||||
if ds1.Version >= 18 { //nolint:gomnd // Version number
|
|
||||||
br.GetUInt32()
|
hasSubstitutions := ds1.version.hasSubstitutions() &&
|
||||||
|
(ds1.SubstitutionType == subType1 || ds1.SubstitutionType == subType2)
|
||||||
|
|
||||||
|
if !hasSubstitutions {
|
||||||
|
ds1.SubstitutionGroups = make([]SubstitutionGroup, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds1.version.hasUnknown2Bytes() {
|
||||||
|
ds1.unknown2, err = br.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading unknown 2: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfSubGroups, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading number of sub groups: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
numberOfSubGroups := br.GetInt32()
|
|
||||||
ds1.SubstitutionGroups = make([]SubstitutionGroup, numberOfSubGroups)
|
ds1.SubstitutionGroups = make([]SubstitutionGroup, numberOfSubGroups)
|
||||||
|
|
||||||
for subIdx := 0; subIdx < int(numberOfSubGroups); subIdx++ {
|
for subIdx := 0; subIdx < int(numberOfSubGroups); subIdx++ {
|
||||||
newSub := SubstitutionGroup{}
|
newSub := SubstitutionGroup{}
|
||||||
newSub.TileX = br.GetInt32()
|
|
||||||
newSub.TileY = br.GetInt32()
|
newSub.TileX, err = br.ReadInt32()
|
||||||
newSub.WidthInTiles = br.GetInt32()
|
if err != nil {
|
||||||
newSub.HeightInTiles = br.GetInt32()
|
return fmt.Errorf("reading substitution's %d X: %v", subIdx, err)
|
||||||
newSub.Unknown = br.GetInt32()
|
}
|
||||||
|
|
||||||
|
newSub.TileY, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading substitution's %d Y: %v", subIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSub.WidthInTiles, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading substitution's %d W: %v", subIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSub.HeightInTiles, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading substitution's %d H: %v", subIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSub.Unknown, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading substitution's %d unknown: %v", subIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
ds1.SubstitutionGroups[subIdx] = newSub
|
ds1.SubstitutionGroups[subIdx] = newSub
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ds1.SubstitutionGroups = make([]SubstitutionGroup, 0)
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds1 *DS1) setupStreamLayerTypes() []d2enum.LayerStreamType {
|
func (ds1 *DS1) getLayerSchema() []layerStreamType {
|
||||||
var layerStream []d2enum.LayerStreamType
|
var layerStream []layerStreamType
|
||||||
|
|
||||||
if ds1.Version < 4 { //nolint:gomnd // Version number
|
if ds1.version.hasStandardLayers() {
|
||||||
layerStream = []d2enum.LayerStreamType{
|
layerStream = []layerStreamType{
|
||||||
d2enum.LayerStreamWall1,
|
layerStreamWall1,
|
||||||
d2enum.LayerStreamFloor1,
|
layerStreamFloor1,
|
||||||
d2enum.LayerStreamOrientation1,
|
layerStreamOrientation1,
|
||||||
d2enum.LayerStreamSubstitute,
|
layerStreamSubstitute1,
|
||||||
d2enum.LayerStreamShadow,
|
layerStreamShadow1,
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
layerStream = make([]d2enum.LayerStreamType,
|
return layerStream
|
||||||
(ds1.NumberOfWalls*2)+ds1.NumberOfFloors+ds1.NumberOfShadowLayers+ds1.NumberOfSubstitutionLayers)
|
}
|
||||||
|
|
||||||
|
numWalls := len(ds1.Walls)
|
||||||
|
numOrientations := numWalls
|
||||||
|
numFloors := len(ds1.Floors)
|
||||||
|
numShadows := len(ds1.Shadows)
|
||||||
|
numSubs := len(ds1.Substitutions)
|
||||||
|
numLayers := numWalls + numOrientations + numFloors + numShadows + numSubs
|
||||||
|
|
||||||
|
layerStream = make([]layerStreamType, numLayers)
|
||||||
|
|
||||||
layerIdx := 0
|
layerIdx := 0
|
||||||
for i := 0; i < int(ds1.NumberOfWalls); i++ {
|
|
||||||
layerStream[layerIdx] = d2enum.LayerStreamType(int(d2enum.LayerStreamWall1) + i)
|
for i := 0; i < numWalls; i++ {
|
||||||
layerStream[layerIdx+1] = d2enum.LayerStreamType(int(d2enum.LayerStreamOrientation1) + i)
|
layerStream[layerIdx] = layerStreamType(int(layerStreamWall1) + i)
|
||||||
layerIdx += 2
|
layerIdx++
|
||||||
}
|
|
||||||
for i := 0; i < int(ds1.NumberOfFloors); i++ {
|
layerStream[layerIdx] = layerStreamType(int(layerStreamOrientation1) + i)
|
||||||
layerStream[layerIdx] = d2enum.LayerStreamType(int(d2enum.LayerStreamFloor1) + i)
|
|
||||||
layerIdx++
|
layerIdx++
|
||||||
}
|
}
|
||||||
if ds1.NumberOfShadowLayers > 0 {
|
|
||||||
layerStream[layerIdx] = d2enum.LayerStreamShadow
|
for i := 0; i < numFloors; i++ {
|
||||||
|
layerStream[layerIdx] = layerStreamType(int(layerStreamFloor1) + i)
|
||||||
layerIdx++
|
layerIdx++
|
||||||
}
|
}
|
||||||
if ds1.NumberOfSubstitutionLayers > 0 {
|
|
||||||
layerStream[layerIdx] = d2enum.LayerStreamSubstitute
|
if numShadows > 0 {
|
||||||
|
layerStream[layerIdx] = layerStreamShadow1
|
||||||
|
layerIdx++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if numSubs > 0 {
|
||||||
|
layerStream[layerIdx] = layerStreamSubstitute1
|
||||||
}
|
}
|
||||||
|
|
||||||
return layerStream
|
return layerStream
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds1 *DS1) loadNPCs(br *d2datautils.StreamReader) {
|
func (ds1 *DS1) loadNPCs(br *d2datautils.StreamReader) error {
|
||||||
if ds1.Version >= 14 { //nolint:gomnd // Version number
|
var err error
|
||||||
numberOfNpcs := br.GetInt32()
|
|
||||||
|
if !ds1.version.specifiesNPCs() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfNpcs, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading number of npcs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
for npcIdx := 0; npcIdx < int(numberOfNpcs); npcIdx++ {
|
for npcIdx := 0; npcIdx < int(numberOfNpcs); npcIdx++ {
|
||||||
numPaths := br.GetInt32()
|
numPaths, err := br.ReadInt32() // nolint:govet // I want to re-use this error variable
|
||||||
npcX := int(br.GetInt32())
|
if err != nil {
|
||||||
npcY := int(br.GetInt32())
|
return fmt.Errorf("reading number of paths for npc %d: %v", npcIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
npcX, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading X pos for NPC %d: %v", npcIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
npcY, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading Y pos for NPC %d: %v", npcIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
objIdx := -1
|
objIdx := -1
|
||||||
|
|
||||||
for idx, ds1Obj := range ds1.Objects {
|
for idx, ds1Obj := range ds1.Objects {
|
||||||
if ds1Obj.X == npcX && ds1Obj.Y == npcY {
|
if ds1Obj.X == int(npcX) && ds1Obj.Y == int(npcY) {
|
||||||
objIdx = idx
|
objIdx = idx
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if objIdx > -1 {
|
if objIdx > -1 {
|
||||||
ds1.loadNpcPaths(br, objIdx, int(numPaths))
|
err = ds1.loadNpcPaths(br, objIdx, int(numPaths))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading paths for NPC %d: %v", npcIdx, err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if ds1.Version >= 15 { //nolint:gomnd // Version number
|
if ds1.version.specifiesNPCActions() {
|
||||||
br.SkipBytes(int(numPaths) * 3) //nolint:gomnd // Unknown data
|
br.SkipBytes(int(numPaths) * 3) //nolint:gomnd // Unknown data
|
||||||
} else {
|
} else {
|
||||||
br.SkipBytes(int(numPaths) * 2) //nolint:gomnd // Unknown data
|
br.SkipBytes(int(numPaths) * 2) //nolint:gomnd // Unknown data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds1 *DS1) loadNpcPaths(br *d2datautils.StreamReader, objIdx, numPaths int) {
|
func (ds1 *DS1) loadNpcPaths(br *d2datautils.StreamReader, objIdx, numPaths int) error {
|
||||||
if ds1.Objects[objIdx].Paths == nil {
|
if ds1.Objects[objIdx].Paths == nil {
|
||||||
ds1.Objects[objIdx].Paths = make([]d2path.Path, numPaths)
|
ds1.Objects[objIdx].Paths = make([]d2path.Path, numPaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
for pathIdx := 0; pathIdx < numPaths; pathIdx++ {
|
for pathIdx := 0; pathIdx < numPaths; pathIdx++ {
|
||||||
newPath := d2path.Path{}
|
newPath := d2path.Path{}
|
||||||
newPath.Position = d2vector.NewPosition(
|
|
||||||
float64(br.GetInt32()),
|
|
||||||
float64(br.GetInt32()))
|
|
||||||
|
|
||||||
if ds1.Version >= 15 { //nolint:gomnd // Version number
|
px, err := br.ReadInt32() //nolint:govet // i want to re-use the err variable...
|
||||||
newPath.Action = int(br.GetInt32())
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading X point for path %d: %v", pathIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
py, err := br.ReadInt32() //nolint:govet // i want to re-use the err variable...
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading Y point for path %d: %v", pathIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPath.Position = d2vector.NewPosition(float64(px), float64(py))
|
||||||
|
|
||||||
|
if ds1.version.specifiesNPCActions() {
|
||||||
|
action, err := br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading action for path %d: %v", pathIdx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPath.Action = int(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
ds1.Objects[objIdx].Paths[pathIdx] = newPath
|
ds1.Objects[objIdx].Paths[pathIdx] = newPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds1 *DS1) loadLayerStreams(br *d2datautils.StreamReader, layerStream []d2enum.LayerStreamType) {
|
func (ds1 *DS1) loadLayerStreams(br *d2datautils.StreamReader) error {
|
||||||
var dirLookup = []int32{
|
dirLookup := []int32{
|
||||||
0x00, 0x01, 0x02, 0x01, 0x02, 0x03, 0x03, 0x05, 0x05, 0x06,
|
0x00, 0x01, 0x02, 0x01, 0x02, 0x03, 0x03, 0x05, 0x05, 0x06,
|
||||||
0x06, 0x07, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
0x06, 0x07, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
||||||
0x0F, 0x10, 0x11, 0x12, 0x14,
|
0x0F, 0x10, 0x11, 0x12, 0x14,
|
||||||
}
|
}
|
||||||
|
|
||||||
for lIdx := range layerStream {
|
layerStreamTypes := ds1.getLayerSchema()
|
||||||
layerStreamType := layerStream[lIdx]
|
|
||||||
|
|
||||||
for y := 0; y < int(ds1.Height); y++ {
|
for _, layerStreamType := range layerStreamTypes {
|
||||||
for x := 0; x < int(ds1.Width); x++ {
|
for y := 0; y < ds1.height; y++ {
|
||||||
dw := br.GetUInt32()
|
for x := 0; x < ds1.width; x++ {
|
||||||
|
dw, err := br.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading layer's dword: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
switch layerStreamType {
|
switch layerStreamType {
|
||||||
case d2enum.LayerStreamWall1, d2enum.LayerStreamWall2, d2enum.LayerStreamWall3, d2enum.LayerStreamWall4:
|
case layerStreamWall1, layerStreamWall2, layerStreamWall3, layerStreamWall4:
|
||||||
wallIndex := int(layerStreamType) - int(d2enum.LayerStreamWall1)
|
wallIndex := int(layerStreamType) - int(layerStreamWall1)
|
||||||
ds1.Tiles[y][x].Walls[wallIndex].Prop1 = byte(dw & 0x000000FF) //nolint:gomnd // Bitmask
|
ds1.Walls[wallIndex].Tile(x, y).DecodeWall(dw)
|
||||||
ds1.Tiles[y][x].Walls[wallIndex].Sequence = byte((dw & 0x00003F00) >> 8) //nolint:gomnd // Bitmask
|
case layerStreamOrientation1, layerStreamOrientation2,
|
||||||
ds1.Tiles[y][x].Walls[wallIndex].Unknown1 = byte((dw & 0x000FC000) >> 14) //nolint:gomnd // Bitmask
|
layerStreamOrientation3, layerStreamOrientation4:
|
||||||
ds1.Tiles[y][x].Walls[wallIndex].Style = byte((dw & 0x03F00000) >> 20) //nolint:gomnd // Bitmask
|
wallIndex := int(layerStreamType) - int(layerStreamOrientation1)
|
||||||
ds1.Tiles[y][x].Walls[wallIndex].Unknown2 = byte((dw & 0x7C000000) >> 26) //nolint:gomnd // Bitmask
|
c := int32(dw & wallTypeBitmask)
|
||||||
ds1.Tiles[y][x].Walls[wallIndex].Hidden = byte((dw&0x80000000)>>31) > 0 //nolint:gomnd // Bitmask
|
|
||||||
case d2enum.LayerStreamOrientation1, d2enum.LayerStreamOrientation2,
|
|
||||||
d2enum.LayerStreamOrientation3, d2enum.LayerStreamOrientation4:
|
|
||||||
wallIndex := int(layerStreamType) - int(d2enum.LayerStreamOrientation1)
|
|
||||||
c := int32(dw & 0x000000FF) //nolint:gomnd // Bitmask
|
|
||||||
|
|
||||||
if ds1.Version < 7 { //nolint:gomnd // Version number
|
if ds1.version < v7 {
|
||||||
if c < int32(len(dirLookup)) {
|
if c < int32(len(dirLookup)) {
|
||||||
c = dirLookup[c]
|
c = dirLookup[c]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ds1.Tiles[y][x].Walls[wallIndex].Type = d2enum.TileType(c)
|
tile := ds1.Walls[wallIndex].Tile(x, y)
|
||||||
ds1.Tiles[y][x].Walls[wallIndex].Zero = byte((dw & 0xFFFFFF00) >> 8) //nolint:gomnd // Bitmask
|
tile.Type = d2enum.TileType(c)
|
||||||
case d2enum.LayerStreamFloor1, d2enum.LayerStreamFloor2:
|
tile.Zero = byte((dw & wallZeroBitmask) >> wallZeroOffset)
|
||||||
floorIndex := int(layerStreamType) - int(d2enum.LayerStreamFloor1)
|
case layerStreamFloor1, layerStreamFloor2:
|
||||||
ds1.Tiles[y][x].Floors[floorIndex].Prop1 = byte(dw & 0x000000FF) //nolint:gomnd // Bitmask
|
floorIndex := int(layerStreamType) - int(layerStreamFloor1)
|
||||||
ds1.Tiles[y][x].Floors[floorIndex].Sequence = byte((dw & 0x00003F00) >> 8) //nolint:gomnd // Bitmask
|
ds1.Floors[floorIndex].Tile(x, y).DecodeFloor(dw)
|
||||||
ds1.Tiles[y][x].Floors[floorIndex].Unknown1 = byte((dw & 0x000FC000) >> 14) //nolint:gomnd // Bitmask
|
case layerStreamShadow1:
|
||||||
ds1.Tiles[y][x].Floors[floorIndex].Style = byte((dw & 0x03F00000) >> 20) //nolint:gomnd // Bitmask
|
ds1.Shadows[0].Tile(x, y).DecodeShadow(dw)
|
||||||
ds1.Tiles[y][x].Floors[floorIndex].Unknown2 = byte((dw & 0x7C000000) >> 26) //nolint:gomnd // Bitmask
|
case layerStreamSubstitute1:
|
||||||
ds1.Tiles[y][x].Floors[floorIndex].Hidden = byte((dw&0x80000000)>>31) > 0 //nolint:gomnd // Bitmask
|
ds1.Substitutions[0].Tile(x, y).Substitution = dw
|
||||||
case d2enum.LayerStreamShadow:
|
}
|
||||||
ds1.Tiles[y][x].Shadows[0].Prop1 = byte(dw & 0x000000FF) //nolint:gomnd // Bitmask
|
}
|
||||||
ds1.Tiles[y][x].Shadows[0].Sequence = byte((dw & 0x00003F00) >> 8) //nolint:gomnd // Bitmask
|
}
|
||||||
ds1.Tiles[y][x].Shadows[0].Unknown1 = byte((dw & 0x000FC000) >> 14) //nolint:gomnd // Bitmask
|
}
|
||||||
ds1.Tiles[y][x].Shadows[0].Style = byte((dw & 0x03F00000) >> 20) //nolint:gomnd // Bitmask
|
|
||||||
ds1.Tiles[y][x].Shadows[0].Unknown2 = byte((dw & 0x7C000000) >> 26) //nolint:gomnd // Bitmask
|
return nil
|
||||||
ds1.Tiles[y][x].Shadows[0].Hidden = byte((dw&0x80000000)>>31) > 0 //nolint:gomnd // Bitmask
|
}
|
||||||
case d2enum.LayerStreamSubstitute:
|
|
||||||
ds1.Tiles[y][x].Substitutions[0].Unknown = dw
|
// SetSize sets the size of all layers in the DS1
|
||||||
|
func (ds1 *DS1) SetSize(w, h int) {
|
||||||
|
ds1.ds1Layers.SetSize(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal encodes ds1 back to byte slice
|
||||||
|
func (ds1 *DS1) Marshal() []byte {
|
||||||
|
// create stream writer
|
||||||
|
sw := d2datautils.CreateStreamWriter()
|
||||||
|
|
||||||
|
// Step 1 - encode header
|
||||||
|
sw.PushInt32(int32(ds1.version))
|
||||||
|
sw.PushInt32(int32(ds1.width - 1))
|
||||||
|
sw.PushInt32(int32(ds1.height - 1))
|
||||||
|
|
||||||
|
if ds1.version.specifiesAct() {
|
||||||
|
sw.PushInt32(ds1.Act - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds1.version.specifiesSubstitutionType() {
|
||||||
|
sw.PushInt32(ds1.SubstitutionType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds1.version.hasFileList() {
|
||||||
|
sw.PushInt32(int32(len(ds1.Files)))
|
||||||
|
|
||||||
|
for _, i := range ds1.Files {
|
||||||
|
sw.PushBytes([]byte(i)...)
|
||||||
|
|
||||||
|
// separator
|
||||||
|
sw.PushBytes(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds1.version.hasUnknown1Bytes() {
|
||||||
|
sw.PushBytes(ds1.unknown1[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds1.version.specifiesWalls() {
|
||||||
|
sw.PushInt32(int32(len(ds1.Walls)))
|
||||||
|
|
||||||
|
if ds1.version.specifiesFloors() {
|
||||||
|
sw.PushInt32(int32(len(ds1.Floors)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2 - encode grid
|
||||||
|
ds1.encodeLayers(sw)
|
||||||
|
|
||||||
|
// Step 3 - encode Objects
|
||||||
|
if ds1.version.hasObjects() {
|
||||||
|
sw.PushInt32(int32(len(ds1.Objects)))
|
||||||
|
|
||||||
|
for _, i := range ds1.Objects {
|
||||||
|
sw.PushUint32(uint32(i.Type))
|
||||||
|
sw.PushUint32(uint32(i.ID))
|
||||||
|
sw.PushUint32(uint32(i.X))
|
||||||
|
sw.PushUint32(uint32(i.Y))
|
||||||
|
sw.PushUint32(uint32(i.Flags))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4 - encode Substitutions
|
||||||
|
hasSubstitutions := ds1.version.hasSubstitutions() &&
|
||||||
|
(ds1.SubstitutionType == subType1 || ds1.SubstitutionType == subType2)
|
||||||
|
|
||||||
|
if hasSubstitutions {
|
||||||
|
sw.PushUint32(ds1.unknown2)
|
||||||
|
|
||||||
|
sw.PushUint32(uint32(len(ds1.SubstitutionGroups)))
|
||||||
|
|
||||||
|
for _, i := range ds1.SubstitutionGroups {
|
||||||
|
sw.PushInt32(i.TileX)
|
||||||
|
sw.PushInt32(i.TileY)
|
||||||
|
sw.PushInt32(i.WidthInTiles)
|
||||||
|
sw.PushInt32(i.HeightInTiles)
|
||||||
|
sw.PushInt32(i.Unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5 - encode NPC's and its paths
|
||||||
|
ds1.encodeNPCs(sw)
|
||||||
|
|
||||||
|
return sw.GetBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds1 *DS1) encodeLayers(sw *d2datautils.StreamWriter) {
|
||||||
|
layerStreamTypes := ds1.getLayerSchema()
|
||||||
|
|
||||||
|
for _, layerStreamType := range layerStreamTypes {
|
||||||
|
for y := 0; y < ds1.height; y++ {
|
||||||
|
for x := 0; x < ds1.width; x++ {
|
||||||
|
dw := uint32(0)
|
||||||
|
|
||||||
|
switch layerStreamType {
|
||||||
|
case layerStreamWall1, layerStreamWall2, layerStreamWall3, layerStreamWall4:
|
||||||
|
wallIndex := int(layerStreamType) - int(layerStreamWall1)
|
||||||
|
ds1.Walls[wallIndex].Tile(x, y).EncodeWall(sw)
|
||||||
|
case layerStreamOrientation1, layerStreamOrientation2,
|
||||||
|
layerStreamOrientation3, layerStreamOrientation4:
|
||||||
|
wallIndex := int(layerStreamType) - int(layerStreamOrientation1)
|
||||||
|
dw |= uint32(ds1.Walls[wallIndex].Tile(x, y).Type)
|
||||||
|
dw |= (uint32(ds1.Walls[wallIndex].Tile(x, y).Zero) & wallZeroBitmask) << wallZeroOffset
|
||||||
|
|
||||||
|
sw.PushUint32(dw)
|
||||||
|
case layerStreamFloor1, layerStreamFloor2:
|
||||||
|
floorIndex := int(layerStreamType) - int(layerStreamFloor1)
|
||||||
|
ds1.Floors[floorIndex].Tile(x, y).EncodeFloor(sw)
|
||||||
|
case layerStreamShadow1:
|
||||||
|
ds1.Shadows[0].Tile(x, y).EncodeShadow(sw)
|
||||||
|
case layerStreamSubstitute1:
|
||||||
|
sw.PushUint32(ds1.Substitutions[0].Tile(x, y).Substitution)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ds1 *DS1) encodeNPCs(sw *d2datautils.StreamWriter) {
|
||||||
|
objectsWithPaths := make([]int, 0)
|
||||||
|
|
||||||
|
for n, obj := range ds1.Objects {
|
||||||
|
if len(obj.Paths) != 0 {
|
||||||
|
objectsWithPaths = append(objectsWithPaths, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5.1 - encode npc's
|
||||||
|
sw.PushUint32(uint32(len(objectsWithPaths)))
|
||||||
|
|
||||||
|
// Step 5.2 - enoce npcs' paths
|
||||||
|
for objectIdx := range objectsWithPaths {
|
||||||
|
sw.PushUint32(uint32(len(ds1.Objects[objectIdx].Paths)))
|
||||||
|
sw.PushUint32(uint32(ds1.Objects[objectIdx].X))
|
||||||
|
sw.PushUint32(uint32(ds1.Objects[objectIdx].Y))
|
||||||
|
|
||||||
|
for _, path := range ds1.Objects[objectIdx].Paths {
|
||||||
|
sw.PushUint32(uint32(path.Position.X()))
|
||||||
|
sw.PushUint32(uint32(path.Position.Y()))
|
||||||
|
|
||||||
|
if ds1.version >= v15 {
|
||||||
|
sw.PushUint32(uint32(path.Action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the ds1 version
|
||||||
|
func (ds1 *DS1) Version() int {
|
||||||
|
return int(ds1.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetVersion sets the ds1 version, can not be negative.
|
||||||
|
func (ds1 *DS1) SetVersion(v int) {
|
||||||
|
if v < 0 {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ds1.version = ds1version(v)
|
||||||
|
}
|
||||||
|
370
d2common/d2fileformats/d2ds1/ds1_layers.go
Normal file
370
d2common/d2fileformats/d2ds1/ds1_layers.go
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
package d2ds1
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxWallLayers = 4
|
||||||
|
maxFloorLayers = 2
|
||||||
|
maxShadowLayers = 1
|
||||||
|
maxSubstitutionLayers = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// LayerGroupType represents a type of layer (floor, wall, shadow, etc)
|
||||||
|
type LayerGroupType int
|
||||||
|
|
||||||
|
// Layer group types
|
||||||
|
const (
|
||||||
|
FloorLayerGroup LayerGroupType = iota
|
||||||
|
WallLayerGroup
|
||||||
|
ShadowLayerGroup
|
||||||
|
SubstitutionLayerGroup
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l LayerGroupType) String() string {
|
||||||
|
switch l {
|
||||||
|
case FloorLayerGroup:
|
||||||
|
return "floor"
|
||||||
|
case WallLayerGroup:
|
||||||
|
return "wall"
|
||||||
|
case ShadowLayerGroup:
|
||||||
|
return "shadow"
|
||||||
|
case SubstitutionLayerGroup:
|
||||||
|
return "substitution"
|
||||||
|
}
|
||||||
|
|
||||||
|
// should not be reached
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
type layerGroup []*Layer
|
||||||
|
|
||||||
|
type ds1Layers struct {
|
||||||
|
width, height int
|
||||||
|
Floors layerGroup
|
||||||
|
Walls layerGroup
|
||||||
|
Shadows layerGroup
|
||||||
|
Substitutions layerGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) ensureInit() {
|
||||||
|
if l.Floors == nil {
|
||||||
|
l.Floors = make(layerGroup, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.Walls == nil {
|
||||||
|
l.Walls = make(layerGroup, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.Shadows == nil {
|
||||||
|
l.Shadows = make(layerGroup, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.Substitutions == nil {
|
||||||
|
l.Substitutions = make(layerGroup, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes nil layers from all layer groups
|
||||||
|
func (l *ds1Layers) cull() {
|
||||||
|
l.cullNilLayers(FloorLayerGroup)
|
||||||
|
l.cullNilLayers(WallLayerGroup)
|
||||||
|
l.cullNilLayers(ShadowLayerGroup)
|
||||||
|
l.cullNilLayers(SubstitutionLayerGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes nil layers of given layer group type
|
||||||
|
func (l *ds1Layers) cullNilLayers(t LayerGroupType) {
|
||||||
|
group := l.GetLayersGroup(t)
|
||||||
|
if group == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// from last to first layer, remove first encountered nil layer and restart the culling procedure.
|
||||||
|
// exit culling procedure when no nil layers are found in entire group.
|
||||||
|
culling:
|
||||||
|
for {
|
||||||
|
for idx := len(*group) - 1; idx >= 0; idx-- {
|
||||||
|
if (*group)[idx] == nil {
|
||||||
|
*group = append((*group)[:idx], (*group)[idx+1:]...)
|
||||||
|
continue culling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break culling // encountered no new nil layers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) Size() (w, h int) {
|
||||||
|
l.ensureInit()
|
||||||
|
l.cull()
|
||||||
|
|
||||||
|
return l.width, l.height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) SetSize(w, h int) {
|
||||||
|
l.width, l.height = w, h
|
||||||
|
|
||||||
|
l.enforceSize(FloorLayerGroup)
|
||||||
|
l.enforceSize(WallLayerGroup)
|
||||||
|
l.enforceSize(ShadowLayerGroup)
|
||||||
|
l.enforceSize(SubstitutionLayerGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) enforceSize(t LayerGroupType) {
|
||||||
|
l.ensureInit()
|
||||||
|
l.cull()
|
||||||
|
|
||||||
|
group := l.GetLayersGroup(t)
|
||||||
|
if group == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range *group {
|
||||||
|
(*group)[idx].SetSize(l.width, l.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) Width() int {
|
||||||
|
w, _ := l.Size()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) SetWidth(w int) {
|
||||||
|
l.SetSize(w, l.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) Height() int {
|
||||||
|
_, h := l.Size()
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) SetHeight(h int) {
|
||||||
|
l.SetSize(l.width, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generic push func for all layer types
|
||||||
|
func (l *ds1Layers) push(t LayerGroupType, layer *Layer) {
|
||||||
|
l.ensureInit()
|
||||||
|
l.cull()
|
||||||
|
layer.SetSize(l.Size())
|
||||||
|
|
||||||
|
group := l.GetLayersGroup(t)
|
||||||
|
|
||||||
|
max := GetMaxGroupLen(t)
|
||||||
|
|
||||||
|
if len(*group) < max {
|
||||||
|
*group = append(*group, layer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generic pop func for all layer types
|
||||||
|
func (l *ds1Layers) pop(t LayerGroupType) *Layer {
|
||||||
|
l.ensureInit()
|
||||||
|
l.cull()
|
||||||
|
|
||||||
|
group := l.GetLayersGroup(t)
|
||||||
|
if group == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var theLayer *Layer
|
||||||
|
|
||||||
|
// remove last layer of slice and return it
|
||||||
|
if len(*group) > 0 {
|
||||||
|
lastIdx := len(*group) - 1
|
||||||
|
theLayer = (*group)[lastIdx]
|
||||||
|
*group = (*group)[:lastIdx]
|
||||||
|
|
||||||
|
return theLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) get(t LayerGroupType, idx int) *Layer {
|
||||||
|
l.ensureInit()
|
||||||
|
l.cull()
|
||||||
|
|
||||||
|
group := l.GetLayersGroup(t)
|
||||||
|
if group == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx >= len(*group) || idx < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return (*group)[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) insert(t LayerGroupType, idx int, newLayer *Layer) {
|
||||||
|
l.ensureInit()
|
||||||
|
l.cull()
|
||||||
|
|
||||||
|
if newLayer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newLayer.SetSize(l.Size())
|
||||||
|
|
||||||
|
group := l.GetLayersGroup(t)
|
||||||
|
if group == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*group)+1 > GetMaxGroupLen(t) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*group) == 0 {
|
||||||
|
*group = append(*group, newLayer) // nolint:staticcheck // we possibly use group later
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(*group) - 1; idx > l {
|
||||||
|
idx = l
|
||||||
|
}
|
||||||
|
|
||||||
|
// example:
|
||||||
|
// suppose
|
||||||
|
// idx=1
|
||||||
|
// newLayer=c
|
||||||
|
// existing layerGroup is [a, b]
|
||||||
|
*group = append((*group)[:idx], append([]*Layer{newLayer}, (*group)[idx:]...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) delete(t LayerGroupType, idx int) {
|
||||||
|
l.ensureInit()
|
||||||
|
l.cull()
|
||||||
|
|
||||||
|
group := l.GetLayersGroup(t)
|
||||||
|
if group == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx >= len(*group) || idx < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(*group)[idx] = nil
|
||||||
|
|
||||||
|
l.cull()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) GetFloor(idx int) *Layer {
|
||||||
|
return l.get(FloorLayerGroup, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) PushFloor(floor *Layer) *ds1Layers {
|
||||||
|
l.push(FloorLayerGroup, floor)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) PopFloor() *Layer {
|
||||||
|
return l.pop(FloorLayerGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) InsertFloor(idx int, newFloor *Layer) {
|
||||||
|
l.insert(FloorLayerGroup, idx, newFloor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) DeleteFloor(idx int) {
|
||||||
|
l.delete(FloorLayerGroup, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) GetWall(idx int) *Layer {
|
||||||
|
return l.get(WallLayerGroup, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) PushWall(wall *Layer) *ds1Layers {
|
||||||
|
l.push(WallLayerGroup, wall)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) PopWall() *Layer {
|
||||||
|
return l.pop(WallLayerGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) InsertWall(idx int, newWall *Layer) {
|
||||||
|
l.insert(WallLayerGroup, idx, newWall)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) DeleteWall(idx int) {
|
||||||
|
l.delete(WallLayerGroup, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) GetShadow(idx int) *Layer {
|
||||||
|
return l.get(ShadowLayerGroup, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) PushShadow(shadow *Layer) *ds1Layers {
|
||||||
|
l.push(ShadowLayerGroup, shadow)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) PopShadow() *Layer {
|
||||||
|
return l.pop(ShadowLayerGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) InsertShadow(idx int, newShadow *Layer) {
|
||||||
|
l.insert(ShadowLayerGroup, idx, newShadow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) DeleteShadow(idx int) {
|
||||||
|
l.delete(ShadowLayerGroup, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) GetSubstitution(idx int) *Layer {
|
||||||
|
return l.get(SubstitutionLayerGroup, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) PushSubstitution(sub *Layer) *ds1Layers {
|
||||||
|
l.push(SubstitutionLayerGroup, sub)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) PopSubstitution() *Layer {
|
||||||
|
return l.pop(SubstitutionLayerGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) InsertSubstitution(idx int, newSubstitution *Layer) {
|
||||||
|
l.insert(SubstitutionLayerGroup, idx, newSubstitution)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ds1Layers) DeleteSubstitution(idx int) {
|
||||||
|
l.delete(ShadowLayerGroup, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLayersGroup returns layer group depending on type given
|
||||||
|
func (l *ds1Layers) GetLayersGroup(t LayerGroupType) (group *layerGroup) {
|
||||||
|
switch t {
|
||||||
|
case FloorLayerGroup:
|
||||||
|
group = &l.Floors
|
||||||
|
case WallLayerGroup:
|
||||||
|
group = &l.Walls
|
||||||
|
case ShadowLayerGroup:
|
||||||
|
group = &l.Shadows
|
||||||
|
case SubstitutionLayerGroup:
|
||||||
|
group = &l.Substitutions
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxGroupLen returns maximum length of layer group of type given
|
||||||
|
func GetMaxGroupLen(t LayerGroupType) (max int) {
|
||||||
|
switch t {
|
||||||
|
case FloorLayerGroup:
|
||||||
|
max = maxFloorLayers
|
||||||
|
case WallLayerGroup:
|
||||||
|
max = maxWallLayers
|
||||||
|
case ShadowLayerGroup:
|
||||||
|
max = maxShadowLayers
|
||||||
|
case SubstitutionLayerGroup:
|
||||||
|
max = maxSubstitutionLayers
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return max
|
||||||
|
}
|
333
d2common/d2fileformats/d2ds1/ds1_layers_test.go
Normal file
333
d2common/d2fileformats/d2ds1/ds1_layers_test.go
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
package d2ds1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_ds1Layers_Delete(t *testing.T) {
|
||||||
|
t.Run("Floors", func(t *testing.T) {
|
||||||
|
ds1LayersDelete(t, FloorLayerGroup)
|
||||||
|
})
|
||||||
|
t.Run("Walls", func(t *testing.T) {
|
||||||
|
ds1LayersDelete(t, WallLayerGroup)
|
||||||
|
})
|
||||||
|
t.Run("Shadows", func(t *testing.T) {
|
||||||
|
ds1LayersDelete(t, ShadowLayerGroup)
|
||||||
|
})
|
||||||
|
t.Run("Substitution", func(t *testing.T) {
|
||||||
|
ds1LayersDelete(t, SubstitutionLayerGroup)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ds1LayersDelete(t *testing.T, lt LayerGroupType) {
|
||||||
|
ds1 := DS1{}
|
||||||
|
|
||||||
|
ds1.ds1Layers = &ds1Layers{
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
Floors: make(layerGroup, 1),
|
||||||
|
Walls: make(layerGroup, 1),
|
||||||
|
Shadows: make(layerGroup, 1),
|
||||||
|
Substitutions: make(layerGroup, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
var lg layerGroup
|
||||||
|
|
||||||
|
var del func(i int)
|
||||||
|
|
||||||
|
switch lt {
|
||||||
|
case FloorLayerGroup:
|
||||||
|
del = func(i int) { ds1.DeleteFloor(0) }
|
||||||
|
case WallLayerGroup:
|
||||||
|
del = func(i int) { ds1.DeleteWall(0) }
|
||||||
|
case ShadowLayerGroup:
|
||||||
|
del = func(i int) { ds1.DeleteShadow(0) }
|
||||||
|
case SubstitutionLayerGroup:
|
||||||
|
del = func(i int) { ds1.DeleteSubstitution(0) }
|
||||||
|
default:
|
||||||
|
t.Fatal("unknown layer type given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
del(0)
|
||||||
|
|
||||||
|
if len(lg) > 0 {
|
||||||
|
t.Errorf("unexpected layer present after deletion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ds1Layers_Get(t *testing.T) {
|
||||||
|
t.Run("Floors", func(t *testing.T) {
|
||||||
|
ds1LayersGet(t, FloorLayerGroup)
|
||||||
|
})
|
||||||
|
t.Run("Walls", func(t *testing.T) {
|
||||||
|
ds1LayersGet(t, WallLayerGroup)
|
||||||
|
})
|
||||||
|
t.Run("Shadows", func(t *testing.T) {
|
||||||
|
ds1LayersGet(t, ShadowLayerGroup)
|
||||||
|
})
|
||||||
|
t.Run("Substitution", func(t *testing.T) {
|
||||||
|
ds1LayersGet(t, SubstitutionLayerGroup)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ds1LayersGet(t *testing.T, lt LayerGroupType) {
|
||||||
|
ds1 := exampleData()
|
||||||
|
|
||||||
|
var get func(i int) *Layer
|
||||||
|
|
||||||
|
switch lt {
|
||||||
|
case FloorLayerGroup:
|
||||||
|
get = func(i int) *Layer { return ds1.GetFloor(0) }
|
||||||
|
case WallLayerGroup:
|
||||||
|
get = func(i int) *Layer { return ds1.GetWall(0) }
|
||||||
|
case ShadowLayerGroup:
|
||||||
|
get = func(i int) *Layer { return ds1.GetShadow(0) }
|
||||||
|
case SubstitutionLayerGroup:
|
||||||
|
get = func(i int) *Layer { return ds1.GetSubstitution(0) }
|
||||||
|
default:
|
||||||
|
t.Fatal("unknown layer type given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
layer := get(0)
|
||||||
|
|
||||||
|
// example has nil substitution layer, maybe we need another test
|
||||||
|
if layer == nil && lt != SubstitutionLayerGroup {
|
||||||
|
t.Errorf("layer expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ds1Layers_Insert(t *testing.T) {
|
||||||
|
t.Run("Floors", func(t *testing.T) {
|
||||||
|
ds1LayersInsert(t, FloorLayerGroup)
|
||||||
|
})
|
||||||
|
t.Run("Walls", func(t *testing.T) {
|
||||||
|
ds1LayersInsert(t, WallLayerGroup)
|
||||||
|
})
|
||||||
|
t.Run("Shadows", func(t *testing.T) {
|
||||||
|
ds1LayersInsert(t, ShadowLayerGroup)
|
||||||
|
})
|
||||||
|
t.Run("Substitution", func(t *testing.T) {
|
||||||
|
ds1LayersInsert(t, SubstitutionLayerGroup)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ds1LayersInsert(t *testing.T, lt LayerGroupType) {
|
||||||
|
ds1 := DS1{}
|
||||||
|
|
||||||
|
layers := make([]*Layer, GetMaxGroupLen(lt)+1)
|
||||||
|
|
||||||
|
for i := range layers {
|
||||||
|
i := i
|
||||||
|
layers[i] = &Layer{}
|
||||||
|
layers[i].tiles = make(tileGrid, 1)
|
||||||
|
layers[i].tiles[0] = make(tileRow, 1)
|
||||||
|
layers[i].SetSize(3, 3)
|
||||||
|
layers[i].tiles[0][0].Prop1 = byte(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
ds1.ds1Layers = &ds1Layers{}
|
||||||
|
|
||||||
|
var insert func(i int)
|
||||||
|
|
||||||
|
group := ds1.GetLayersGroup(lt)
|
||||||
|
|
||||||
|
switch lt {
|
||||||
|
case FloorLayerGroup:
|
||||||
|
insert = func(i int) { ds1.InsertFloor(0, layers[i]) }
|
||||||
|
case WallLayerGroup:
|
||||||
|
insert = func(i int) { ds1.InsertWall(0, layers[i]) }
|
||||||
|
case ShadowLayerGroup:
|
||||||
|
insert = func(i int) { ds1.InsertShadow(0, layers[i]) }
|
||||||
|
case SubstitutionLayerGroup:
|
||||||
|
insert = func(i int) { ds1.InsertSubstitution(0, layers[i]) }
|
||||||
|
default:
|
||||||
|
t.Fatal("unknown layer type given")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range layers {
|
||||||
|
insert(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*group) != GetMaxGroupLen(lt) {
|
||||||
|
t.Fatal("unexpected floor len after setting")
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := 0
|
||||||
|
for i := len(layers) - 2; i > 0; i-- {
|
||||||
|
if (*group)[idx].tiles[0][0].Prop1 != byte(i) {
|
||||||
|
t.Fatal("unexpected tile inserted")
|
||||||
|
}
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ds1Layers_Pop(t *testing.T) {
|
||||||
|
t.Run("Floor", func(t *testing.T) {
|
||||||
|
ds1layerPop(FloorLayerGroup, t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Wall", func(t *testing.T) {
|
||||||
|
ds1layerPop(WallLayerGroup, t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Shadow", func(t *testing.T) {
|
||||||
|
ds1layerPop(ShadowLayerGroup, t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Substitution", func(t *testing.T) {
|
||||||
|
ds1layerPop(SubstitutionLayerGroup, t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ds1layerPop(lt LayerGroupType, t *testing.T) {
|
||||||
|
ds1 := exampleData()
|
||||||
|
|
||||||
|
var pop func() *Layer
|
||||||
|
|
||||||
|
var numBefore, numAfter int
|
||||||
|
|
||||||
|
switch lt {
|
||||||
|
case FloorLayerGroup:
|
||||||
|
numBefore = len(ds1.Floors)
|
||||||
|
pop = func() *Layer {
|
||||||
|
l := ds1.PopFloor()
|
||||||
|
numAfter = len(ds1.Floors)
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
case WallLayerGroup:
|
||||||
|
numBefore = len(ds1.Walls)
|
||||||
|
pop = func() *Layer {
|
||||||
|
l := ds1.PopWall()
|
||||||
|
numAfter = len(ds1.Walls)
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
case ShadowLayerGroup:
|
||||||
|
numBefore = len(ds1.Shadows)
|
||||||
|
pop = func() *Layer {
|
||||||
|
l := ds1.PopShadow()
|
||||||
|
numAfter = len(ds1.Shadows)
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
case SubstitutionLayerGroup:
|
||||||
|
numBefore = len(ds1.Substitutions)
|
||||||
|
pop = func() *Layer {
|
||||||
|
l := ds1.PopSubstitution()
|
||||||
|
numAfter = len(ds1.Substitutions)
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("unknown layer type given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts := 10
|
||||||
|
|
||||||
|
for attempts > 0 {
|
||||||
|
attempts--
|
||||||
|
|
||||||
|
l := pop()
|
||||||
|
|
||||||
|
if l == nil && numBefore < numAfter {
|
||||||
|
t.Fatal("popped nil layer, expected layer count to remain the same")
|
||||||
|
}
|
||||||
|
|
||||||
|
if l != nil && numBefore <= numAfter {
|
||||||
|
t.Fatal("popped non-nil, expected layer count to be lower")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ds1Layers_Push(t *testing.T) {
|
||||||
|
t.Run("Floor", func(t *testing.T) {
|
||||||
|
ds1layerPush(FloorLayerGroup, t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Wall", func(t *testing.T) {
|
||||||
|
ds1layerPush(WallLayerGroup, t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Shadow", func(t *testing.T) {
|
||||||
|
ds1layerPush(ShadowLayerGroup, t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Substitution", func(t *testing.T) {
|
||||||
|
ds1layerPush(SubstitutionLayerGroup, t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// for all layer types, the test is the same
|
||||||
|
// when we push a layer, we expect an increment, and when we push a bunch of times,
|
||||||
|
// we expect to never exceed the max. we also expect to be able to retrieve a non-nil
|
||||||
|
// layer after we push.
|
||||||
|
func ds1layerPush(lt LayerGroupType, t *testing.T) { //nolint:funlen // no biggie
|
||||||
|
layers := &ds1Layers{}
|
||||||
|
|
||||||
|
// we need to set up some shit to handle the test in a generic way
|
||||||
|
var push func()
|
||||||
|
|
||||||
|
var get func(idx int) *Layer
|
||||||
|
|
||||||
|
var max int
|
||||||
|
|
||||||
|
var group *layerGroup
|
||||||
|
|
||||||
|
check := func(expected int) {
|
||||||
|
actual := len(*group)
|
||||||
|
got := get(expected - 1)
|
||||||
|
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("unexpected number of layers: expected %d, got %d", expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("got nil layer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch lt {
|
||||||
|
case FloorLayerGroup:
|
||||||
|
push = func() { layers.PushFloor(&Layer{}) }
|
||||||
|
get = layers.GetFloor
|
||||||
|
max = maxFloorLayers
|
||||||
|
group = &layers.Floors
|
||||||
|
case WallLayerGroup:
|
||||||
|
push = func() { layers.PushWall(&Layer{}) }
|
||||||
|
get = layers.GetWall
|
||||||
|
max = maxWallLayers
|
||||||
|
group = &layers.Walls
|
||||||
|
case ShadowLayerGroup:
|
||||||
|
push = func() { layers.PushShadow(&Layer{}) }
|
||||||
|
get = layers.GetShadow
|
||||||
|
max = maxShadowLayers
|
||||||
|
group = &layers.Shadows
|
||||||
|
case SubstitutionLayerGroup:
|
||||||
|
push = func() { layers.PushSubstitution(&Layer{}) }
|
||||||
|
get = layers.GetSubstitution
|
||||||
|
max = maxSubstitutionLayers
|
||||||
|
group = &layers.Substitutions
|
||||||
|
default:
|
||||||
|
t.Fatal("unknown layer type given")
|
||||||
|
}
|
||||||
|
|
||||||
|
// push one time, we expect a single layer to exist
|
||||||
|
push()
|
||||||
|
check(1)
|
||||||
|
|
||||||
|
// if we push a bunch of times, we expect to not exceed the max
|
||||||
|
push()
|
||||||
|
push()
|
||||||
|
push()
|
||||||
|
push()
|
||||||
|
push()
|
||||||
|
push()
|
||||||
|
push()
|
||||||
|
push()
|
||||||
|
push()
|
||||||
|
check(max)
|
||||||
|
}
|
247
d2common/d2fileformats/d2ds1/ds1_test.go
Normal file
247
d2common/d2fileformats/d2ds1/ds1_test.go
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
package d2ds1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2path"
|
||||||
|
)
|
||||||
|
|
||||||
|
func exampleData() *DS1 { //nolint:funlen // not a big deal if this is long func
|
||||||
|
exampleFloor1 := Tile{
|
||||||
|
// common fields
|
||||||
|
tileCommonFields: tileCommonFields{
|
||||||
|
Prop1: 2,
|
||||||
|
Sequence: 89,
|
||||||
|
Unknown1: 123,
|
||||||
|
Style: 20,
|
||||||
|
Unknown2: 53,
|
||||||
|
HiddenBytes: 1,
|
||||||
|
RandomIndex: 2,
|
||||||
|
YAdjust: 21,
|
||||||
|
},
|
||||||
|
tileFloorShadowFields: tileFloorShadowFields{
|
||||||
|
Animated: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
exampleFloor2 := Tile{
|
||||||
|
// common fields
|
||||||
|
tileCommonFields: tileCommonFields{
|
||||||
|
Prop1: 3,
|
||||||
|
Sequence: 89,
|
||||||
|
Unknown1: 213,
|
||||||
|
Style: 28,
|
||||||
|
Unknown2: 53,
|
||||||
|
HiddenBytes: 7,
|
||||||
|
RandomIndex: 3,
|
||||||
|
YAdjust: 28,
|
||||||
|
},
|
||||||
|
tileFloorShadowFields: tileFloorShadowFields{
|
||||||
|
Animated: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
exampleWall1 := Tile{
|
||||||
|
// common fields
|
||||||
|
tileCommonFields: tileCommonFields{
|
||||||
|
Prop1: 3,
|
||||||
|
Sequence: 89,
|
||||||
|
Unknown1: 213,
|
||||||
|
Style: 28,
|
||||||
|
Unknown2: 53,
|
||||||
|
HiddenBytes: 7,
|
||||||
|
RandomIndex: 3,
|
||||||
|
YAdjust: 28,
|
||||||
|
},
|
||||||
|
tileWallFields: tileWallFields{
|
||||||
|
Type: d2enum.TileRightWall,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
exampleWall2 := Tile{
|
||||||
|
// common fields
|
||||||
|
tileCommonFields: tileCommonFields{
|
||||||
|
Prop1: 3,
|
||||||
|
Sequence: 93,
|
||||||
|
Unknown1: 193,
|
||||||
|
Style: 17,
|
||||||
|
Unknown2: 13,
|
||||||
|
HiddenBytes: 1,
|
||||||
|
RandomIndex: 1,
|
||||||
|
YAdjust: 22,
|
||||||
|
},
|
||||||
|
tileWallFields: tileWallFields{
|
||||||
|
Type: d2enum.TileLeftWall,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
exampleShadow := Tile{
|
||||||
|
// common fields
|
||||||
|
tileCommonFields: tileCommonFields{
|
||||||
|
Prop1: 3,
|
||||||
|
Sequence: 93,
|
||||||
|
Unknown1: 173,
|
||||||
|
Style: 17,
|
||||||
|
Unknown2: 12,
|
||||||
|
HiddenBytes: 1,
|
||||||
|
RandomIndex: 1,
|
||||||
|
YAdjust: 22,
|
||||||
|
},
|
||||||
|
tileFloorShadowFields: tileFloorShadowFields{
|
||||||
|
Animated: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &DS1{
|
||||||
|
ds1Layers: &ds1Layers{
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
Floors: layerGroup{
|
||||||
|
// number of floors (one floor)
|
||||||
|
{
|
||||||
|
// tile grid = []tileRow
|
||||||
|
tiles: tileGrid{
|
||||||
|
// tile rows = []Tile
|
||||||
|
// 2x2 tiles
|
||||||
|
{
|
||||||
|
exampleFloor1,
|
||||||
|
exampleFloor2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exampleFloor2,
|
||||||
|
exampleFloor1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Walls: layerGroup{
|
||||||
|
// number of walls (two floors)
|
||||||
|
{
|
||||||
|
// tile grid = []tileRow
|
||||||
|
tiles: tileGrid{
|
||||||
|
// tile rows = []Tile
|
||||||
|
// 2x2 tiles
|
||||||
|
{
|
||||||
|
exampleWall1,
|
||||||
|
exampleWall2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exampleWall2,
|
||||||
|
exampleWall1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// tile grid = []tileRow
|
||||||
|
tiles: tileGrid{
|
||||||
|
// tile rows = []Tile
|
||||||
|
// 2x2 tiles
|
||||||
|
{
|
||||||
|
exampleWall1,
|
||||||
|
exampleWall2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exampleWall2,
|
||||||
|
exampleWall1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Shadows: layerGroup{
|
||||||
|
// number of shadows (always 1)
|
||||||
|
{
|
||||||
|
// tile grid = []tileRow
|
||||||
|
tiles: tileGrid{
|
||||||
|
// tile rows = []Tile
|
||||||
|
// 2x2 tiles
|
||||||
|
{
|
||||||
|
exampleShadow,
|
||||||
|
exampleShadow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exampleShadow,
|
||||||
|
exampleShadow,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Files: []string{"a.dt1", "bfile.dt1"},
|
||||||
|
Objects: []Object{
|
||||||
|
{0, 0, 0, 0, 0, nil},
|
||||||
|
{0, 1, 0, 0, 0, []d2path.Path{{}}},
|
||||||
|
{0, 2, 0, 0, 0, nil},
|
||||||
|
{0, 3, 0, 0, 0, nil},
|
||||||
|
},
|
||||||
|
SubstitutionGroups: nil,
|
||||||
|
version: 17,
|
||||||
|
Act: 1,
|
||||||
|
SubstitutionType: 0,
|
||||||
|
unknown2: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDS1_MarshalUnmarshal(t *testing.T) {
|
||||||
|
ds1 := exampleData()
|
||||||
|
|
||||||
|
data := ds1.Marshal()
|
||||||
|
|
||||||
|
_, loadErr := Unmarshal(data)
|
||||||
|
if loadErr != nil {
|
||||||
|
t.Error(loadErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDS1_Version(t *testing.T) {
|
||||||
|
ds1 := exampleData()
|
||||||
|
|
||||||
|
v := ds1.Version()
|
||||||
|
|
||||||
|
ds1.SetVersion(v + 1)
|
||||||
|
|
||||||
|
if ds1.Version() == v {
|
||||||
|
t.Fatal("expected different ds1 version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDS1_SetSize(t *testing.T) {
|
||||||
|
ds1 := exampleData()
|
||||||
|
|
||||||
|
w, h := ds1.Size()
|
||||||
|
|
||||||
|
ds1.SetSize(w+1, h-1)
|
||||||
|
|
||||||
|
w2, h2 := ds1.Size()
|
||||||
|
|
||||||
|
if w2 != (w+1) || h2 != (h-1) {
|
||||||
|
t.Fatal("unexpected width/height after setting size")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getLayerSchema(t *testing.T) {
|
||||||
|
ds1 := exampleData()
|
||||||
|
|
||||||
|
expected := map[int]layerStreamType{
|
||||||
|
0: layerStreamWall1,
|
||||||
|
1: layerStreamOrientation1,
|
||||||
|
2: layerStreamWall2,
|
||||||
|
3: layerStreamOrientation2,
|
||||||
|
4: layerStreamFloor1,
|
||||||
|
5: layerStreamShadow1,
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := ds1.getLayerSchema()
|
||||||
|
|
||||||
|
if len(schema) != len(expected) {
|
||||||
|
t.Fatal("unexpected schema length")
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range expected {
|
||||||
|
if schema[idx] != expected[idx] {
|
||||||
|
t.Fatal("unexpected layer type in schema")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
d2common/d2fileformats/d2ds1/ds1_version.go
Normal file
72
d2common/d2fileformats/d2ds1/ds1_version.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package d2ds1
|
||||||
|
|
||||||
|
type ds1version int
|
||||||
|
|
||||||
|
const (
|
||||||
|
v3 ds1version = 3
|
||||||
|
v4 ds1version = 4
|
||||||
|
v7 ds1version = 7
|
||||||
|
v8 ds1version = 8
|
||||||
|
v9 ds1version = 9
|
||||||
|
v10 ds1version = 10
|
||||||
|
v12 ds1version = 12
|
||||||
|
v13 ds1version = 13
|
||||||
|
v14 ds1version = 14
|
||||||
|
v15 ds1version = 15
|
||||||
|
v16 ds1version = 16
|
||||||
|
v18 ds1version = 18
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v ds1version) hasUnknown1Bytes() bool {
|
||||||
|
// just after the header will be some meaningless (?) bytes
|
||||||
|
return v >= v9 && v <= v13
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) hasUnknown2Bytes() bool {
|
||||||
|
return v >= v18
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) specifiesAct() bool {
|
||||||
|
// in the header
|
||||||
|
return v >= v8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) specifiesSubstitutionType() bool {
|
||||||
|
// in the header
|
||||||
|
return v >= v10
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) hasStandardLayers() bool {
|
||||||
|
// 1 of each layer, very simple ds1
|
||||||
|
return v < v4
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) specifiesWalls() bool {
|
||||||
|
// just after header, specifies number of Walls
|
||||||
|
return v >= v4
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) specifiesFloors() bool {
|
||||||
|
// just after header, specifies number of Floors
|
||||||
|
return v >= v16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) hasFileList() bool {
|
||||||
|
return v >= v3
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) hasObjects() bool {
|
||||||
|
return v >= v3
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) hasSubstitutions() bool {
|
||||||
|
return v >= v12
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) specifiesNPCs() bool {
|
||||||
|
return v > v14
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ds1version) specifiesNPCActions() bool {
|
||||||
|
return v > v15
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
package d2ds1
|
|
||||||
|
|
||||||
// FloorShadowRecord represents a floor or shadow record in a DS1 file.
|
|
||||||
type FloorShadowRecord struct {
|
|
||||||
Prop1 byte
|
|
||||||
Sequence byte
|
|
||||||
Unknown1 byte
|
|
||||||
Style byte
|
|
||||||
Unknown2 byte
|
|
||||||
Hidden bool
|
|
||||||
RandomIndex byte
|
|
||||||
Animated bool
|
|
||||||
YAdjust int
|
|
||||||
}
|
|
140
d2common/d2fileformats/d2ds1/layer.go
Normal file
140
d2common/d2fileformats/d2ds1/layer.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package d2ds1
|
||||||
|
|
||||||
|
// layerStreamType represents a layer stream type
|
||||||
|
type layerStreamType int
|
||||||
|
|
||||||
|
// Layer stream types
|
||||||
|
const (
|
||||||
|
layerStreamWall1 layerStreamType = iota
|
||||||
|
layerStreamWall2
|
||||||
|
layerStreamWall3
|
||||||
|
layerStreamWall4
|
||||||
|
layerStreamOrientation1
|
||||||
|
layerStreamOrientation2
|
||||||
|
layerStreamOrientation3
|
||||||
|
layerStreamOrientation4
|
||||||
|
layerStreamFloor1
|
||||||
|
layerStreamFloor2
|
||||||
|
layerStreamShadow1
|
||||||
|
layerStreamSubstitute1
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
tileRow []Tile // index is x coordinate
|
||||||
|
tileGrid []tileRow // index is y coordinate
|
||||||
|
)
|
||||||
|
|
||||||
|
// Layer is an abstraction of a tile grid with some helper methods
|
||||||
|
type Layer struct {
|
||||||
|
tiles tileGrid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile returns the tile at the given x,y coordinate in the grid, or nil if empty.
|
||||||
|
func (l *Layer) Tile(x, y int) *Tile {
|
||||||
|
if l.Width() < x || l.Height() < y {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &l.tiles[y][x]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTile sets the tile at the given x,y coordinate in the tile grid
|
||||||
|
func (l *Layer) SetTile(x, y int, t *Tile) {
|
||||||
|
if l.Width() > x || l.Height() > y {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.tiles[y][x] = *t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width returns the width of the tile grid
|
||||||
|
func (l *Layer) Width() int {
|
||||||
|
if len(l.tiles[0]) < 1 {
|
||||||
|
l.SetWidth(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(l.tiles[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWidth sets the width of the tile grid, minimum of 1
|
||||||
|
func (l *Layer) SetWidth(w int) *Layer {
|
||||||
|
if w < 1 {
|
||||||
|
w = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure at least one row
|
||||||
|
if len(l.tiles) < 1 {
|
||||||
|
l.tiles = make(tileGrid, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create/copy tiles as required to satisfy width
|
||||||
|
for y := range l.tiles {
|
||||||
|
if (w - len(l.tiles[y])) == 0 { // if requested width same as row width
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpRow := make(tileRow, w)
|
||||||
|
|
||||||
|
for x := range tmpRow {
|
||||||
|
if x < len(l.tiles[y]) { // if tile exists
|
||||||
|
tmpRow[x] = l.tiles[y][x] // copy it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.tiles[y] = tmpRow
|
||||||
|
}
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height returns the height of the tile grid
|
||||||
|
func (l *Layer) Height() int {
|
||||||
|
if len(l.tiles) < 1 {
|
||||||
|
l.SetHeight(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(l.tiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeight sets the height of the tile grid, minimum of 1
|
||||||
|
func (l *Layer) SetHeight(h int) *Layer {
|
||||||
|
if h < 1 {
|
||||||
|
h = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// make tmpGrid to move existing tiles into
|
||||||
|
tmpGrid := make(tileGrid, h)
|
||||||
|
|
||||||
|
for y := range tmpGrid {
|
||||||
|
tmpGrid[y] = make(tileRow, l.Width())
|
||||||
|
}
|
||||||
|
|
||||||
|
// move existing tiles over
|
||||||
|
for y := range l.tiles {
|
||||||
|
if y >= len(tmpGrid) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := range l.tiles[y] {
|
||||||
|
if x >= len(tmpGrid[y]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpGrid[y][x] = l.tiles[y][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.tiles = tmpGrid
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the width and height of the tile grid
|
||||||
|
func (l *Layer) Size() (w, h int) {
|
||||||
|
return l.Width(), l.Height()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSize sets the width and height of the tile grid
|
||||||
|
func (l *Layer) SetSize(w, h int) *Layer {
|
||||||
|
return l.SetWidth(w).SetHeight(h)
|
||||||
|
}
|
29
d2common/d2fileformats/d2ds1/layer_test.go
Normal file
29
d2common/d2fileformats/d2ds1/layer_test.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package d2ds1
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_layers(t *testing.T) {
|
||||||
|
const (
|
||||||
|
fmtWidthHeightError = "unexpected wall layer width/height: %dx%d"
|
||||||
|
)
|
||||||
|
|
||||||
|
l := &Layer{}
|
||||||
|
|
||||||
|
l.SetSize(0, 0)
|
||||||
|
|
||||||
|
if l.Width() != 1 || l.Height() != 1 {
|
||||||
|
t.Fatalf(fmtWidthHeightError, l.Width(), l.Height())
|
||||||
|
}
|
||||||
|
|
||||||
|
l.SetSize(4, 5)
|
||||||
|
|
||||||
|
if l.Width() != 4 || l.Height() != 5 {
|
||||||
|
t.Fatalf(fmtWidthHeightError, l.Width(), l.Height())
|
||||||
|
}
|
||||||
|
|
||||||
|
l.SetSize(4, 3)
|
||||||
|
|
||||||
|
if l.Width() != 4 || l.Height() != 3 {
|
||||||
|
t.Fatalf(fmtWidthHeightError, l.Width(), l.Height())
|
||||||
|
}
|
||||||
|
}
|
@ -13,3 +13,13 @@ type Object struct {
|
|||||||
Flags int
|
Flags int
|
||||||
Paths []d2path.Path
|
Paths []d2path.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equals checks if this Object is equivalent to the given Object
|
||||||
|
func (o *Object) Equals(other *Object) bool {
|
||||||
|
return o.Type == other.Type &&
|
||||||
|
o.ID == other.ID &&
|
||||||
|
o.X == other.X &&
|
||||||
|
o.Y == other.Y &&
|
||||||
|
o.Flags == other.Flags &&
|
||||||
|
len(o.Paths) == len(other.Paths)
|
||||||
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
package d2ds1
|
|
||||||
|
|
||||||
// SubstitutionRecord represents a substitution record in a DS1 file.
|
|
||||||
type SubstitutionRecord struct {
|
|
||||||
Unknown uint32
|
|
||||||
}
|
|
127
d2common/d2fileformats/d2ds1/tile.go
Normal file
127
d2common/d2fileformats/d2ds1/tile.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package d2ds1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
prop1Bitmask = 0x000000FF
|
||||||
|
prop1Offset = 0
|
||||||
|
prop1Length = 8
|
||||||
|
|
||||||
|
sequenceBitmask = 0x00003F00
|
||||||
|
sequenceOffset = 8
|
||||||
|
sequenceLength = 6
|
||||||
|
|
||||||
|
unknown1Bitmask = 0x000FC000
|
||||||
|
unknown1Offset = 14
|
||||||
|
unknown1Length = 6
|
||||||
|
|
||||||
|
styleBitmask = 0x03F00000
|
||||||
|
styleOffset = 20
|
||||||
|
styleLength = 6
|
||||||
|
|
||||||
|
unknown2Bitmask = 0x7C000000
|
||||||
|
unknown2Offset = 26
|
||||||
|
unknown2Length = 5
|
||||||
|
|
||||||
|
hiddenBitmask = 0x80000000
|
||||||
|
hiddenOffset = 31
|
||||||
|
hiddenLength = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type tileCommonFields struct {
|
||||||
|
Prop1 byte
|
||||||
|
Sequence byte
|
||||||
|
Unknown1 byte
|
||||||
|
Style byte
|
||||||
|
Unknown2 byte
|
||||||
|
HiddenBytes byte
|
||||||
|
RandomIndex byte
|
||||||
|
YAdjust int
|
||||||
|
}
|
||||||
|
|
||||||
|
type tileFloorShadowFields struct {
|
||||||
|
Animated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type tileSubstitutionFields struct {
|
||||||
|
Substitution uint32 // unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type tileWallFields struct {
|
||||||
|
Type d2enum.TileType
|
||||||
|
Zero byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile represents a tile record in a DS1 file.
|
||||||
|
type Tile struct {
|
||||||
|
tileCommonFields
|
||||||
|
tileSubstitutionFields
|
||||||
|
tileWallFields
|
||||||
|
tileFloorShadowFields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hidden returns if wall is hidden
|
||||||
|
func (t *Tile) Hidden() bool {
|
||||||
|
return t.HiddenBytes > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeWall decodes as a wall record
|
||||||
|
func (t *Tile) DecodeWall(dw uint32) {
|
||||||
|
t.Prop1 = byte((dw & prop1Bitmask) >> prop1Offset)
|
||||||
|
t.Sequence = byte((dw & sequenceBitmask) >> sequenceOffset)
|
||||||
|
t.Unknown1 = byte((dw & unknown1Bitmask) >> unknown1Offset)
|
||||||
|
t.Style = byte((dw & styleBitmask) >> styleOffset)
|
||||||
|
t.Unknown2 = byte((dw & unknown2Bitmask) >> unknown2Offset)
|
||||||
|
t.HiddenBytes = byte((dw & hiddenBitmask) >> hiddenOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeWall adds wall's record's bytes into stream writer given
|
||||||
|
func (t *Tile) EncodeWall(sw *d2datautils.StreamWriter) {
|
||||||
|
sw.PushBits32(uint32(t.Prop1), prop1Length)
|
||||||
|
sw.PushBits32(uint32(t.Sequence), sequenceLength)
|
||||||
|
sw.PushBits32(uint32(t.Unknown1), unknown1Length)
|
||||||
|
sw.PushBits32(uint32(t.Style), styleLength)
|
||||||
|
sw.PushBits32(uint32(t.Unknown2), unknown2Length)
|
||||||
|
sw.PushBits32(uint32(t.HiddenBytes), hiddenLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tile) decodeFloorShadow(dw uint32) {
|
||||||
|
t.Prop1 = byte((dw & prop1Bitmask) >> prop1Offset)
|
||||||
|
t.Sequence = byte((dw & sequenceBitmask) >> sequenceOffset)
|
||||||
|
t.Unknown1 = byte((dw & unknown1Bitmask) >> unknown1Offset)
|
||||||
|
t.Style = byte((dw & styleBitmask) >> styleOffset)
|
||||||
|
t.Unknown2 = byte((dw & unknown2Bitmask) >> unknown2Offset)
|
||||||
|
t.HiddenBytes = byte((dw & hiddenBitmask) >> hiddenOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tile) encodeFloorShadow(sw *d2datautils.StreamWriter) {
|
||||||
|
sw.PushBits32(uint32(t.Prop1), prop1Length)
|
||||||
|
sw.PushBits32(uint32(t.Sequence), sequenceLength)
|
||||||
|
sw.PushBits32(uint32(t.Unknown1), unknown1Length)
|
||||||
|
sw.PushBits32(uint32(t.Style), styleLength)
|
||||||
|
sw.PushBits32(uint32(t.Unknown2), unknown2Length)
|
||||||
|
sw.PushBits32(uint32(t.HiddenBytes), hiddenLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeFloor decodes as a floor record
|
||||||
|
func (t *Tile) DecodeFloor(dw uint32) {
|
||||||
|
t.decodeFloorShadow(dw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeFloor adds Floor's bits to stream writer given
|
||||||
|
func (t *Tile) EncodeFloor(sw *d2datautils.StreamWriter) {
|
||||||
|
t.encodeFloorShadow(sw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeShadow decodes as a shadow record
|
||||||
|
func (t *Tile) DecodeShadow(dw uint32) {
|
||||||
|
t.decodeFloorShadow(dw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeShadow adds shadow's bits to stream writer given
|
||||||
|
func (t *Tile) EncodeShadow(sw *d2datautils.StreamWriter) {
|
||||||
|
t.encodeFloorShadow(sw)
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
package d2ds1
|
|
||||||
|
|
||||||
// TileRecord represents a tile record in a DS1 file.
|
|
||||||
type TileRecord struct {
|
|
||||||
Floors []FloorShadowRecord // Collection of floor records
|
|
||||||
Walls []WallRecord // Collection of wall records
|
|
||||||
Shadows []FloorShadowRecord // Collection of shadow records
|
|
||||||
Substitutions []SubstitutionRecord // Collection of substitutions
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package d2ds1
|
|
||||||
|
|
||||||
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
|
||||||
|
|
||||||
// WallRecord represents a wall record.
|
|
||||||
type WallRecord struct {
|
|
||||||
Type d2enum.TileType
|
|
||||||
Zero byte
|
|
||||||
Prop1 byte
|
|
||||||
Sequence byte
|
|
||||||
Unknown1 byte
|
|
||||||
Style byte
|
|
||||||
Unknown2 byte
|
|
||||||
Hidden bool
|
|
||||||
RandomIndex byte
|
|
||||||
YAdjust int
|
|
||||||
}
|
|
@ -6,8 +6,25 @@ type Block struct {
|
|||||||
Y int16
|
Y int16
|
||||||
GridX byte
|
GridX byte
|
||||||
GridY byte
|
GridY byte
|
||||||
Format BlockDataFormat
|
format int16
|
||||||
EncodedData []byte
|
EncodedData []byte
|
||||||
Length int32
|
Length int32
|
||||||
FileOffset int32
|
FileOffset int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format returns block format
|
||||||
|
func (b *Block) Format() BlockDataFormat {
|
||||||
|
if b.format == 1 {
|
||||||
|
return BlockFormatIsometric
|
||||||
|
}
|
||||||
|
|
||||||
|
return BlockFormatRLE
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) unknown1() []byte {
|
||||||
|
return make([]byte, numUnknownBlockBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) unknown2() []byte {
|
||||||
|
return make([]byte, numUnknownBlockBytes)
|
||||||
|
}
|
||||||
|
@ -2,15 +2,11 @@ package d2dt1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DT1 represents a DT1 file.
|
|
||||||
type DT1 struct {
|
|
||||||
Tiles []Tile
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlockDataFormat represents the format of the block data
|
// BlockDataFormat represents the format of the block data
|
||||||
type BlockDataFormat int16
|
type BlockDataFormat int16
|
||||||
|
|
||||||
@ -22,55 +18,169 @@ const (
|
|||||||
BlockFormatIsometric BlockDataFormat = 1
|
BlockFormatIsometric BlockDataFormat = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
numUnknownHeaderBytes = 260
|
||||||
|
knownMajorVersion = 7
|
||||||
|
knownMinorVersion = 6
|
||||||
|
numUnknownTileBytes1 = 4
|
||||||
|
numUnknownTileBytes2 = 4
|
||||||
|
numUnknownTileBytes3 = 7
|
||||||
|
numUnknownTileBytes4 = 12
|
||||||
|
numUnknownBlockBytes = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// DT1 represents a DT1 file.
|
||||||
|
type DT1 struct {
|
||||||
|
majorVersion int32
|
||||||
|
minorVersion int32
|
||||||
|
numberOfTiles int32
|
||||||
|
bodyPosition int32
|
||||||
|
Tiles []Tile
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new DT1
|
||||||
|
func New() *DT1 {
|
||||||
|
result := &DT1{
|
||||||
|
majorVersion: knownMajorVersion,
|
||||||
|
minorVersion: knownMinorVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// LoadDT1 loads a DT1 record
|
// LoadDT1 loads a DT1 record
|
||||||
//nolint:funlen // Can't reduce
|
//nolint:funlen,gocognit,gocyclo // Can't reduce
|
||||||
func LoadDT1(fileData []byte) (*DT1, error) {
|
func LoadDT1(fileData []byte) (*DT1, error) {
|
||||||
result := &DT1{}
|
result := &DT1{}
|
||||||
br := d2datautils.CreateStreamReader(fileData)
|
br := d2datautils.CreateStreamReader(fileData)
|
||||||
ver1 := br.GetInt32()
|
|
||||||
ver2 := br.GetInt32()
|
|
||||||
|
|
||||||
if ver1 != 7 || ver2 != 6 {
|
var err error
|
||||||
return nil, fmt.Errorf("expected to have a version of 7.6, but got %d.%d instead", ver1, ver2)
|
|
||||||
|
result.majorVersion, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
br.SkipBytes(260) //nolint:gomnd // Unknown data
|
result.minorVersion, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
numberOfTiles := br.GetInt32()
|
if result.majorVersion != knownMajorVersion || result.minorVersion != knownMinorVersion {
|
||||||
br.SetPosition(uint64(br.GetInt32()))
|
const fmtErr = "expected to have a version of 7.6, but got %d.%d instead"
|
||||||
|
return nil, fmt.Errorf(fmtErr, result.majorVersion, result.minorVersion)
|
||||||
|
}
|
||||||
|
|
||||||
result.Tiles = make([]Tile, numberOfTiles)
|
br.SkipBytes(numUnknownHeaderBytes)
|
||||||
|
|
||||||
|
result.numberOfTiles, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.bodyPosition, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
br.SetPosition(uint64(result.bodyPosition))
|
||||||
|
|
||||||
|
result.Tiles = make([]Tile, result.numberOfTiles)
|
||||||
|
|
||||||
for tileIdx := range result.Tiles {
|
for tileIdx := range result.Tiles {
|
||||||
newTile := Tile{}
|
tile := Tile{}
|
||||||
newTile.Direction = br.GetInt32()
|
|
||||||
newTile.RoofHeight = br.GetInt16()
|
|
||||||
newTile.MaterialFlags = NewMaterialFlags(br.GetUInt16())
|
|
||||||
newTile.Height = br.GetInt32()
|
|
||||||
newTile.Width = br.GetInt32()
|
|
||||||
|
|
||||||
br.SkipBytes(4) //nolint:gomnd // Unknown data
|
tile.Direction, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
newTile.Type = br.GetInt32()
|
return nil, err
|
||||||
newTile.Style = br.GetInt32()
|
|
||||||
newTile.Sequence = br.GetInt32()
|
|
||||||
newTile.RarityFrameIndex = br.GetInt32()
|
|
||||||
|
|
||||||
br.SkipBytes(4) //nolint:gomnd // Unknown data
|
|
||||||
|
|
||||||
for i := range newTile.SubTileFlags {
|
|
||||||
newTile.SubTileFlags[i] = NewSubTileFlags(br.GetByte())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
br.SkipBytes(7) //nolint:gomnd // Unknown data
|
tile.RoofHeight, err = br.ReadInt16()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
newTile.blockHeaderPointer = br.GetInt32()
|
var matFlagBytes uint16
|
||||||
newTile.blockHeaderSize = br.GetInt32()
|
|
||||||
newTile.Blocks = make([]Block, br.GetInt32())
|
|
||||||
|
|
||||||
br.SkipBytes(12) //nolint:gomnd // Unknown data
|
matFlagBytes, err = br.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
result.Tiles[tileIdx] = newTile
|
tile.MaterialFlags = NewMaterialFlags(matFlagBytes)
|
||||||
|
|
||||||
|
tile.Height, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tile.Width, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
br.SkipBytes(numUnknownTileBytes1)
|
||||||
|
|
||||||
|
tile.Type, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tile.Style, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tile.Sequence, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tile.RarityFrameIndex, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tile.unknown2, err = br.ReadBytes(numUnknownTileBytes2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range tile.SubTileFlags {
|
||||||
|
var subtileFlagBytes byte
|
||||||
|
|
||||||
|
subtileFlagBytes, err = br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tile.SubTileFlags[i] = NewSubTileFlags(subtileFlagBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
br.SkipBytes(numUnknownTileBytes3)
|
||||||
|
|
||||||
|
tile.blockHeaderPointer, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tile.blockHeaderSize, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var numBlocks int32
|
||||||
|
|
||||||
|
numBlocks, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tile.Blocks = make([]Block, numBlocks)
|
||||||
|
|
||||||
|
br.SkipBytes(numUnknownTileBytes4)
|
||||||
|
|
||||||
|
result.Tiles[tileIdx] = tile
|
||||||
}
|
}
|
||||||
|
|
||||||
for tileIdx := range result.Tiles {
|
for tileIdx := range result.Tiles {
|
||||||
@ -78,34 +188,135 @@ func LoadDT1(fileData []byte) (*DT1, error) {
|
|||||||
br.SetPosition(uint64(tile.blockHeaderPointer))
|
br.SetPosition(uint64(tile.blockHeaderPointer))
|
||||||
|
|
||||||
for blockIdx := range tile.Blocks {
|
for blockIdx := range tile.Blocks {
|
||||||
result.Tiles[tileIdx].Blocks[blockIdx].X = br.GetInt16()
|
result.Tiles[tileIdx].Blocks[blockIdx].X, err = br.ReadInt16()
|
||||||
result.Tiles[tileIdx].Blocks[blockIdx].Y = br.GetInt16()
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
br.SkipBytes(2) //nolint:gomnd // Unknown data
|
|
||||||
|
|
||||||
result.Tiles[tileIdx].Blocks[blockIdx].GridX = br.GetByte()
|
|
||||||
result.Tiles[tileIdx].Blocks[blockIdx].GridY = br.GetByte()
|
|
||||||
formatValue := br.GetInt16()
|
|
||||||
|
|
||||||
if formatValue == 1 {
|
|
||||||
result.Tiles[tileIdx].Blocks[blockIdx].Format = BlockFormatIsometric
|
|
||||||
} else {
|
|
||||||
result.Tiles[tileIdx].Blocks[blockIdx].Format = BlockFormatRLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Tiles[tileIdx].Blocks[blockIdx].Length = br.GetInt32()
|
result.Tiles[tileIdx].Blocks[blockIdx].Y, err = br.ReadInt16()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
br.SkipBytes(2) //nolint:gomnd // Unknown data
|
br.SkipBytes(numUnknownBlockBytes)
|
||||||
|
|
||||||
result.Tiles[tileIdx].Blocks[blockIdx].FileOffset = br.GetInt32()
|
result.Tiles[tileIdx].Blocks[blockIdx].GridX, err = br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Tiles[tileIdx].Blocks[blockIdx].GridY, err = br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Tiles[tileIdx].Blocks[blockIdx].format, err = br.ReadInt16()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Tiles[tileIdx].Blocks[blockIdx].Length, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
br.SkipBytes(numUnknownBlockBytes)
|
||||||
|
|
||||||
|
result.Tiles[tileIdx].Blocks[blockIdx].FileOffset, err = br.ReadInt32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for blockIndex, block := range tile.Blocks {
|
for blockIndex, block := range tile.Blocks {
|
||||||
br.SetPosition(uint64(tile.blockHeaderPointer + block.FileOffset))
|
br.SetPosition(uint64(tile.blockHeaderPointer + block.FileOffset))
|
||||||
encodedData := br.ReadBytes(int(block.Length))
|
|
||||||
|
encodedData, err := br.ReadBytes(int(block.Length))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
tile.Blocks[blockIndex].EncodedData = encodedData
|
tile.Blocks[blockIndex].EncodedData = encodedData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marshal encodes dt1 data back to byte slice
|
||||||
|
func (d *DT1) Marshal() []byte {
|
||||||
|
sw := d2datautils.CreateStreamWriter()
|
||||||
|
|
||||||
|
// header
|
||||||
|
sw.PushInt32(d.majorVersion)
|
||||||
|
sw.PushInt32(d.minorVersion)
|
||||||
|
sw.PushBytes(d.unknownHeaderBytes()...)
|
||||||
|
sw.PushInt32(d.numberOfTiles)
|
||||||
|
sw.PushInt32(d.bodyPosition)
|
||||||
|
|
||||||
|
// Step 1 - encoding tiles headers
|
||||||
|
for i := 0; i < len(d.Tiles); i++ {
|
||||||
|
sw.PushInt32(d.Tiles[i].Direction)
|
||||||
|
sw.PushInt16(d.Tiles[i].RoofHeight)
|
||||||
|
sw.PushUint16(d.Tiles[i].MaterialFlags.Encode())
|
||||||
|
sw.PushInt32(d.Tiles[i].Height)
|
||||||
|
sw.PushInt32(d.Tiles[i].Width)
|
||||||
|
sw.PushBytes(d.Tiles[i].unknown1()...)
|
||||||
|
sw.PushInt32(d.Tiles[i].Type)
|
||||||
|
sw.PushInt32(d.Tiles[i].Style)
|
||||||
|
sw.PushInt32(d.Tiles[i].Sequence)
|
||||||
|
sw.PushInt32(d.Tiles[i].RarityFrameIndex)
|
||||||
|
sw.PushBytes(d.Tiles[i].unknown2...)
|
||||||
|
|
||||||
|
for _, j := range d.Tiles[i].SubTileFlags {
|
||||||
|
sw.PushBytes(j.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.PushBytes(d.Tiles[i].unknown3()...)
|
||||||
|
sw.PushInt32(d.Tiles[i].blockHeaderPointer)
|
||||||
|
sw.PushInt32(d.Tiles[i].blockHeaderSize)
|
||||||
|
sw.PushInt32(int32(len(d.Tiles[i].Blocks)))
|
||||||
|
sw.PushBytes(d.Tiles[i].unknown4()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we must sort blocks first
|
||||||
|
blocks := make(map[int][]Block)
|
||||||
|
for i := range d.Tiles {
|
||||||
|
blocks[int(d.Tiles[i].blockHeaderPointer)] = d.Tiles[i].Blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]int, 0, len(blocks))
|
||||||
|
for i := range blocks {
|
||||||
|
keys = append(keys, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Ints(keys)
|
||||||
|
|
||||||
|
// Step 2 - encoding blocks
|
||||||
|
for i := 0; i < len(keys); i++ {
|
||||||
|
// Step 2.1 - encoding blocks' header
|
||||||
|
for j := range blocks[keys[i]] {
|
||||||
|
sw.PushInt16(blocks[keys[i]][j].X)
|
||||||
|
sw.PushInt16(blocks[keys[i]][j].Y)
|
||||||
|
sw.PushBytes(blocks[keys[i]][j].unknown1()...)
|
||||||
|
sw.PushBytes(blocks[keys[i]][j].GridX)
|
||||||
|
sw.PushBytes(blocks[keys[i]][j].GridY)
|
||||||
|
sw.PushInt16(blocks[keys[i]][j].format)
|
||||||
|
sw.PushInt32(blocks[keys[i]][j].Length)
|
||||||
|
sw.PushBytes(blocks[keys[i]][j].unknown2()...)
|
||||||
|
sw.PushInt32(blocks[keys[i]][j].FileOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2.2 - encoding blocks' data
|
||||||
|
for j := range blocks[keys[i]] {
|
||||||
|
sw.PushBytes(blocks[keys[i]][j].EncodedData...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sw.GetBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DT1) unknownHeaderBytes() []byte {
|
||||||
|
result := make([]byte, numUnknownHeaderBytes)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@ const (
|
|||||||
// DecodeTileGfxData decodes tile graphics data for a slice of dt1 blocks
|
// DecodeTileGfxData decodes tile graphics data for a slice of dt1 blocks
|
||||||
func DecodeTileGfxData(blocks []Block, pixels *[]byte, tileYOffset, tileWidth int32) {
|
func DecodeTileGfxData(blocks []Block, pixels *[]byte, tileYOffset, tileWidth int32) {
|
||||||
for _, block := range blocks {
|
for _, block := range blocks {
|
||||||
if block.Format == BlockFormatIsometric {
|
if block.Format() == BlockFormatIsometric {
|
||||||
// 3D isometric decoding
|
// 3D isometric decoding
|
||||||
xjump := []int32{14, 12, 10, 8, 6, 4, 2, 0, 2, 4, 6, 8, 10, 12, 14}
|
xjump := []int32{14, 12, 10, 8, 6, 4, 2, 0, 2, 4, 6, 8, 10, 12, 14}
|
||||||
nbpix := []int32{4, 8, 12, 16, 20, 24, 28, 32, 28, 24, 20, 16, 12, 8, 4}
|
nbpix := []int32{4, 8, 12, 16, 20, 24, 28, 32, 28, 24, 20, 16, 12, 8, 4}
|
||||||
|
@ -30,3 +30,50 @@ func NewMaterialFlags(data uint16) MaterialFlags {
|
|||||||
Snow: data&0x0400 == 0x0400,
|
Snow: data&0x0400 == 0x0400,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encode encodes MaterialFlags back to uint16
|
||||||
|
func (m *MaterialFlags) Encode() uint16 {
|
||||||
|
var b uint16 = 0x000
|
||||||
|
|
||||||
|
if m.Other {
|
||||||
|
b |= 0x0001
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Water {
|
||||||
|
b |= 0x0002
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.WoodObject {
|
||||||
|
b |= 0x0004
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.InsideStone {
|
||||||
|
b |= 0x0008
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.OutsideStone {
|
||||||
|
b |= 0x0010
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Dirt {
|
||||||
|
b |= 0x0020
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Sand {
|
||||||
|
b |= 0x0040
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Wood {
|
||||||
|
b |= 0x0080
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Lava {
|
||||||
|
b |= 0x0100
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Snow {
|
||||||
|
b |= 0x0400
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
@ -77,3 +77,42 @@ func NewSubTileFlags(data byte) SubTileFlags {
|
|||||||
Unknown3: data&128 == 128,
|
Unknown3: data&128 == 128,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encode encodes SubTileFlags back to byte
|
||||||
|
func (s *SubTileFlags) Encode() byte {
|
||||||
|
var b byte
|
||||||
|
|
||||||
|
if s.BlockWalk {
|
||||||
|
b |= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.BlockLOS {
|
||||||
|
b |= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.BlockJump {
|
||||||
|
b |= 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.BlockPlayerWalk {
|
||||||
|
b |= 8
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Unknown1 {
|
||||||
|
b |= 16
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.BlockLight {
|
||||||
|
b |= 32
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Unknown2 {
|
||||||
|
b |= 64
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Unknown3 {
|
||||||
|
b |= 128
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ package d2dt1
|
|||||||
|
|
||||||
// Tile is a representation of a map tile
|
// Tile is a representation of a map tile
|
||||||
type Tile struct {
|
type Tile struct {
|
||||||
|
unknown2 []byte
|
||||||
Direction int32
|
Direction int32
|
||||||
RoofHeight int16
|
RoofHeight int16
|
||||||
MaterialFlags MaterialFlags
|
MaterialFlags MaterialFlags
|
||||||
@ -16,3 +17,15 @@ type Tile struct {
|
|||||||
blockHeaderSize int32
|
blockHeaderSize int32
|
||||||
Blocks []Block
|
Blocks []Block
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tile) unknown1() []byte {
|
||||||
|
return make([]byte, numUnknownTileBytes1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tile) unknown3() []byte {
|
||||||
|
return make([]byte, numUnknownTileBytes3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tile) unknown4() []byte {
|
||||||
|
return make([]byte, numUnknownTileBytes4)
|
||||||
|
}
|
||||||
|
67
d2common/d2fileformats/d2font/d2fontglyph/font_glyph.go
Normal file
67
d2common/d2fileformats/d2font/d2fontglyph/font_glyph.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// Package d2fontglyph represents a single font glyph
|
||||||
|
package d2fontglyph
|
||||||
|
|
||||||
|
// Create creates a new font glyph
|
||||||
|
func Create(frame, width, height int) *FontGlyph {
|
||||||
|
// nolint:gomnd // thes bytes are constant
|
||||||
|
// comes from https://d2mods.info/forum/viewtopic.php?t=42044
|
||||||
|
result := &FontGlyph{
|
||||||
|
frame: frame,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontGlyph represents a single font glyph
|
||||||
|
type FontGlyph struct {
|
||||||
|
frame int
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSize sets glyph's size to w, h
|
||||||
|
func (fg *FontGlyph) SetSize(w, h int) {
|
||||||
|
fg.width, fg.height = w, h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns glyph's size
|
||||||
|
func (fg *FontGlyph) Size() (w, h int) {
|
||||||
|
return fg.width, fg.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width returns font width
|
||||||
|
func (fg *FontGlyph) Width() int {
|
||||||
|
return fg.width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height returns glyph's height
|
||||||
|
func (fg *FontGlyph) Height() int {
|
||||||
|
return fg.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFrameIndex sets frame index to idx
|
||||||
|
func (fg *FontGlyph) SetFrameIndex(idx int) {
|
||||||
|
fg.frame = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// FrameIndex returns glyph's frame
|
||||||
|
func (fg *FontGlyph) FrameIndex() int {
|
||||||
|
return fg.frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown1 returns unknowns bytes
|
||||||
|
func (fg *FontGlyph) Unknown1() []byte {
|
||||||
|
return []byte{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown2 returns unknowns bytes
|
||||||
|
func (fg *FontGlyph) Unknown2() []byte {
|
||||||
|
return []byte{1, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown3 returns unknowns bytes
|
||||||
|
func (fg *FontGlyph) Unknown3() []byte {
|
||||||
|
return []byte{0, 0, 0, 0}
|
||||||
|
}
|
3
d2common/d2fileformats/d2font/doc.go
Normal file
3
d2common/d2fileformats/d2font/doc.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// Package d2font contains logic for loading and processing
|
||||||
|
// d2 fonts
|
||||||
|
package d2font
|
213
d2common/d2fileformats/d2font/font.go
Normal file
213
d2common/d2fileformats/d2font/font.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
package d2font
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2font/d2fontglyph"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
knownSignature = "Woo!\x01"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
numHeaderBytes = 12
|
||||||
|
bytesPerGlyph = 14
|
||||||
|
signatureBytesCount = 5
|
||||||
|
unknownHeaderBytesCount = 7
|
||||||
|
unknown1BytesCount = 1
|
||||||
|
unknown2BytesCount = 3
|
||||||
|
unknown3BytesCount = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// Font represents a displayable font
|
||||||
|
type Font struct {
|
||||||
|
sheet d2interface.Animation
|
||||||
|
table []byte
|
||||||
|
Glyphs map[rune]*d2fontglyph.FontGlyph
|
||||||
|
color color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads a new font from byte slice
|
||||||
|
func Load(data []byte) (*Font, error) {
|
||||||
|
sr := d2datautils.CreateStreamReader(data)
|
||||||
|
|
||||||
|
signature, err := sr.ReadBytes(signatureBytesCount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(signature) != knownSignature {
|
||||||
|
return nil, fmt.Errorf("invalid font table format")
|
||||||
|
}
|
||||||
|
|
||||||
|
font := &Font{
|
||||||
|
table: data,
|
||||||
|
color: color.White,
|
||||||
|
}
|
||||||
|
|
||||||
|
sr.SkipBytes(unknownHeaderBytesCount)
|
||||||
|
|
||||||
|
err = font.initGlyphs(sr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return font, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBackground sets font's background
|
||||||
|
func (f *Font) SetBackground(sheet d2interface.Animation) {
|
||||||
|
f.sheet = sheet
|
||||||
|
|
||||||
|
// recalculate max height
|
||||||
|
_, h := f.sheet.GetFrameBounds()
|
||||||
|
|
||||||
|
for i := range f.Glyphs {
|
||||||
|
f.Glyphs[i].SetSize(f.Glyphs[i].Width(), h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetColor sets the fonts color
|
||||||
|
func (f *Font) SetColor(c color.Color) {
|
||||||
|
f.color = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextMetrics returns the dimensions of the Font element in pixels
|
||||||
|
func (f *Font) GetTextMetrics(text string) (width, height int) {
|
||||||
|
var (
|
||||||
|
lineWidth int
|
||||||
|
lineHeight int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, c := range text {
|
||||||
|
if c == '\n' {
|
||||||
|
width = d2math.MaxInt(width, lineWidth)
|
||||||
|
height += lineHeight
|
||||||
|
lineWidth = 0
|
||||||
|
lineHeight = 0
|
||||||
|
} else if glyph, ok := f.Glyphs[c]; ok {
|
||||||
|
lineWidth += glyph.Width()
|
||||||
|
lineHeight = d2math.MaxInt(lineHeight, glyph.Height())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width = d2math.MaxInt(width, lineWidth)
|
||||||
|
height += lineHeight
|
||||||
|
|
||||||
|
return width, height
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderText prints a text using its configured style on a Surface (multi-lines are left-aligned, use label otherwise)
|
||||||
|
func (f *Font) RenderText(text string, target d2interface.Surface) error {
|
||||||
|
f.sheet.SetColorMod(f.color)
|
||||||
|
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
var (
|
||||||
|
lineHeight int
|
||||||
|
lineLength int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, c := range line {
|
||||||
|
glyph, ok := f.Glyphs[c]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.sheet.SetCurrentFrame(glyph.FrameIndex()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.sheet.Render(target)
|
||||||
|
|
||||||
|
lineHeight = d2math.MaxInt(lineHeight, glyph.Height())
|
||||||
|
lineLength++
|
||||||
|
|
||||||
|
target.PushTranslation(glyph.Width(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
target.PopN(lineLength)
|
||||||
|
target.PushTranslation(0, lineHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
target.PopN(len(lines))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) initGlyphs(sr *d2datautils.StreamReader) error {
|
||||||
|
glyphs := make(map[rune]*d2fontglyph.FontGlyph)
|
||||||
|
|
||||||
|
// for i := numHeaderBytes; i < len(f.table); i += bytesPerGlyph {
|
||||||
|
for i := numHeaderBytes; true; i += bytesPerGlyph {
|
||||||
|
code, err := sr.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// byte of 0
|
||||||
|
sr.SkipBytes(unknown1BytesCount)
|
||||||
|
|
||||||
|
width, err := sr.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
height, err := sr.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1, 0, 0
|
||||||
|
sr.SkipBytes(unknown2BytesCount)
|
||||||
|
|
||||||
|
frame, err := sr.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1, 0, 0, character code repeated, and further 0.
|
||||||
|
sr.SkipBytes(unknown3BytesCount)
|
||||||
|
|
||||||
|
glyph := d2fontglyph.Create(int(frame), int(width), int(height))
|
||||||
|
|
||||||
|
glyphs[rune(code)] = glyph
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Glyphs = glyphs
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal encodes font back into byte slice
|
||||||
|
func (f *Font) Marshal() []byte {
|
||||||
|
sw := d2datautils.CreateStreamWriter()
|
||||||
|
|
||||||
|
sw.PushBytes([]byte("Woo!\x01")...)
|
||||||
|
|
||||||
|
// unknown header bytes - constant
|
||||||
|
sw.PushBytes([]byte{1, 0, 0, 0, 0, 1}...)
|
||||||
|
|
||||||
|
// Expected Height of character cell and Expected Width of character cell
|
||||||
|
// not used in decoder
|
||||||
|
sw.PushBytes([]byte{0, 0}...)
|
||||||
|
|
||||||
|
for c, i := range f.Glyphs {
|
||||||
|
sw.PushUint16(uint16(c))
|
||||||
|
sw.PushBytes(i.Unknown1()...)
|
||||||
|
sw.PushBytes(byte(i.Width()))
|
||||||
|
sw.PushBytes(byte(i.Height()))
|
||||||
|
sw.PushBytes(i.Unknown2()...)
|
||||||
|
sw.PushUint16(uint16(i.FrameIndex()))
|
||||||
|
sw.PushBytes(i.Unknown3()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sw.GetBytes()
|
||||||
|
}
|
131
d2common/d2fileformats/d2mpq/crypto.go
Normal file
131
d2common/d2fileformats/d2mpq/crypto.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package d2mpq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cryptoBuffer [0x500]uint32 //nolint:gochecknoglobals // will fix later..
|
||||||
|
var cryptoBufferReady bool //nolint:gochecknoglobals // will fix later..
|
||||||
|
|
||||||
|
func cryptoLookup(index uint32) uint32 {
|
||||||
|
if !cryptoBufferReady {
|
||||||
|
cryptoInitialize()
|
||||||
|
|
||||||
|
cryptoBufferReady = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return cryptoBuffer[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gomnd // Decryption magic
|
||||||
|
func cryptoInitialize() {
|
||||||
|
seed := uint32(0x00100001)
|
||||||
|
|
||||||
|
for index1 := 0; index1 < 0x100; index1++ {
|
||||||
|
index2 := index1
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
seed = (seed*125 + 3) % 0x2AAAAB
|
||||||
|
temp1 := (seed & 0xFFFF) << 0x10
|
||||||
|
seed = (seed*125 + 3) % 0x2AAAAB
|
||||||
|
temp2 := seed & 0xFFFF
|
||||||
|
cryptoBuffer[index2] = temp1 | temp2
|
||||||
|
index2 += 0x100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gomnd // Decryption magic
|
||||||
|
func decrypt(data []uint32, seed uint32) {
|
||||||
|
seed2 := uint32(0xeeeeeeee)
|
||||||
|
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
seed2 += cryptoLookup(0x400 + (seed & 0xff))
|
||||||
|
result := data[i]
|
||||||
|
result ^= seed + seed2
|
||||||
|
|
||||||
|
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
|
||||||
|
seed2 = result + seed2 + (seed2 << 5) + 3
|
||||||
|
data[i] = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gomnd // Decryption magic
|
||||||
|
func decryptBytes(data []byte, seed uint32) {
|
||||||
|
seed2 := uint32(0xEEEEEEEE)
|
||||||
|
for i := 0; i < len(data)-3; i += 4 {
|
||||||
|
seed2 += cryptoLookup(0x400 + (seed & 0xFF))
|
||||||
|
result := binary.LittleEndian.Uint32(data[i : i+4])
|
||||||
|
result ^= seed + seed2
|
||||||
|
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
|
||||||
|
seed2 = result + seed2 + (seed2 << 5) + 3
|
||||||
|
|
||||||
|
data[i+0] = uint8(result & 0xff)
|
||||||
|
data[i+1] = uint8((result >> 8) & 0xff)
|
||||||
|
data[i+2] = uint8((result >> 16) & 0xff)
|
||||||
|
data[i+3] = uint8((result >> 24) & 0xff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gomnd // Decryption magic
|
||||||
|
func decryptTable(r io.Reader, size uint32, name string) ([]uint32, error) {
|
||||||
|
seed := hashString(name, 3)
|
||||||
|
seed2 := uint32(0xEEEEEEEE)
|
||||||
|
size *= 4
|
||||||
|
|
||||||
|
table := make([]uint32, size)
|
||||||
|
buf := make([]byte, 4)
|
||||||
|
|
||||||
|
for i := uint32(0); i < size; i++ {
|
||||||
|
seed2 += cryptoBuffer[0x400+(seed&0xff)]
|
||||||
|
|
||||||
|
if _, err := r.Read(buf); err != nil {
|
||||||
|
return table, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := binary.LittleEndian.Uint32(buf)
|
||||||
|
result ^= seed + seed2
|
||||||
|
|
||||||
|
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
|
||||||
|
seed2 = result + seed2 + (seed2 << 5) + 3
|
||||||
|
table[i] = result
|
||||||
|
}
|
||||||
|
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashFilename(key string) uint64 {
|
||||||
|
a, b := hashString(key, 1), hashString(key, 2)
|
||||||
|
return uint64(a)<<32 | uint64(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gomnd // Decryption magic
|
||||||
|
func hashString(key string, hashType uint32) uint32 {
|
||||||
|
seed1 := uint32(0x7FED7FED)
|
||||||
|
seed2 := uint32(0xEEEEEEEE)
|
||||||
|
|
||||||
|
/* prepare seeds. */
|
||||||
|
for _, char := range strings.ToUpper(key) {
|
||||||
|
seed1 = cryptoLookup((hashType*0x100)+uint32(char)) ^ (seed1 + seed2)
|
||||||
|
seed2 = uint32(char) + seed1 + seed2 + (seed2 << 5) + 3
|
||||||
|
}
|
||||||
|
|
||||||
|
return seed1
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:unused,deadcode,gomnd // will use this for creating mpq's
|
||||||
|
func encrypt(data []uint32, seed uint32) {
|
||||||
|
seed2 := uint32(0xeeeeeeee)
|
||||||
|
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
seed2 += cryptoLookup(0x400 + (seed & 0xff))
|
||||||
|
result := data[i]
|
||||||
|
result ^= seed + seed2
|
||||||
|
|
||||||
|
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
|
||||||
|
seed2 = data[i] + seed2 + (seed2 << 5) + 3
|
||||||
|
data[i] = result
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
package d2mpq
|
|
||||||
|
|
||||||
var cryptoBuffer [0x500]uint32 //nolint:gochecknoglobals // will fix later..
|
|
||||||
var cryptoBufferReady bool //nolint:gochecknoglobals // will fix later..
|
|
||||||
|
|
||||||
func cryptoLookup(index uint32) uint32 {
|
|
||||||
if !cryptoBufferReady {
|
|
||||||
cryptoInitialize()
|
|
||||||
|
|
||||||
cryptoBufferReady = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return cryptoBuffer[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:gomnd // magic cryptographic stuff here...
|
|
||||||
func cryptoInitialize() {
|
|
||||||
seed := uint32(0x00100001)
|
|
||||||
|
|
||||||
for index1 := 0; index1 < 0x100; index1++ {
|
|
||||||
index2 := index1
|
|
||||||
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
seed = (seed*125 + 3) % 0x2AAAAB
|
|
||||||
temp1 := (seed & 0xFFFF) << 0x10
|
|
||||||
seed = (seed*125 + 3) % 0x2AAAAB
|
|
||||||
temp2 := seed & 0xFFFF
|
|
||||||
cryptoBuffer[index2] = temp1 | temp2
|
|
||||||
index2 += 0x100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package d2mpq
|
|
||||||
|
|
||||||
// HashEntryMap represents a hash entry map
|
|
||||||
type HashEntryMap struct {
|
|
||||||
entries map[uint64]HashTableEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert inserts a hash entry into the table
|
|
||||||
func (hem *HashEntryMap) Insert(entry *HashTableEntry) {
|
|
||||||
if hem.entries == nil {
|
|
||||||
hem.entries = make(map[uint64]HashTableEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
hem.entries[uint64(entry.NamePartA)<<32|uint64(entry.NamePartB)] = *entry
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find finds a hash entry
|
|
||||||
func (hem *HashEntryMap) Find(fileName string) (*HashTableEntry, bool) {
|
|
||||||
if hem.entries == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
hashA := hashString(fileName, 1)
|
|
||||||
hashB := hashString(fileName, 2)
|
|
||||||
|
|
||||||
entry, found := hem.entries[uint64(hashA)<<32|uint64(hashB)]
|
|
||||||
|
|
||||||
return &entry, found
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains returns true if the hash entry contains the values
|
|
||||||
func (hem *HashEntryMap) Contains(fileName string) bool {
|
|
||||||
_, found := hem.Find(fileName)
|
|
||||||
return found
|
|
||||||
}
|
|
@ -2,12 +2,11 @@ package d2mpq
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@ -21,31 +20,9 @@ var _ d2interface.Archive = &MPQ{} // Static check to confirm struct conforms to
|
|||||||
type MPQ struct {
|
type MPQ struct {
|
||||||
filePath string
|
filePath string
|
||||||
file *os.File
|
file *os.File
|
||||||
hashEntryMap HashEntryMap
|
hashes map[uint64]*Hash
|
||||||
blockTableEntries []BlockTableEntry
|
blocks []*Block
|
||||||
data Data
|
header Header
|
||||||
}
|
|
||||||
|
|
||||||
// Data Represents a MPQ file
|
|
||||||
type Data struct {
|
|
||||||
Magic [4]byte
|
|
||||||
HeaderSize uint32
|
|
||||||
ArchiveSize uint32
|
|
||||||
FormatVersion uint16
|
|
||||||
BlockSize uint16
|
|
||||||
HashTableOffset uint32
|
|
||||||
BlockTableOffset uint32
|
|
||||||
HashTableEntries uint32
|
|
||||||
BlockTableEntries uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// HashTableEntry represents a hashed file entry in the MPQ file
|
|
||||||
type HashTableEntry struct { // 16 bytes
|
|
||||||
NamePartA uint32
|
|
||||||
NamePartB uint32
|
|
||||||
Locale uint16
|
|
||||||
Platform uint16
|
|
||||||
BlockIndex uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchInfo represents patch info for the MPQ.
|
// PatchInfo represents patch info for the MPQ.
|
||||||
@ -53,296 +30,110 @@ type PatchInfo struct {
|
|||||||
Length uint32 // Length of patch info header, in bytes
|
Length uint32 // Length of patch info header, in bytes
|
||||||
Flags uint32 // Flags. 0x80000000 = MD5 (?)
|
Flags uint32 // Flags. 0x80000000 = MD5 (?)
|
||||||
DataSize uint32 // Uncompressed size of the patch file
|
DataSize uint32 // Uncompressed size of the patch file
|
||||||
Md5 [16]byte // MD5 of the entire patch file after decompression
|
MD5 [16]byte // MD5 of the entire patch file after decompression
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileFlag represents flags for a file record in the MPQ archive
|
// New loads an MPQ file and only reads the header
|
||||||
type FileFlag uint32
|
func New(fileName string) (*MPQ, error) {
|
||||||
|
mpq := &MPQ{filePath: fileName}
|
||||||
const (
|
|
||||||
// FileImplode - File is compressed using PKWARE Data compression library
|
|
||||||
FileImplode FileFlag = 0x00000100
|
|
||||||
// FileCompress - File is compressed using combination of compression methods
|
|
||||||
FileCompress FileFlag = 0x00000200
|
|
||||||
// FileEncrypted - The file is encrypted
|
|
||||||
FileEncrypted FileFlag = 0x00010000
|
|
||||||
// FileFixKey - The decryption key for the file is altered according to the position of the file in the archive
|
|
||||||
FileFixKey FileFlag = 0x00020000
|
|
||||||
// FilePatchFile - The file contains incremental patch for an existing file in base MPQ
|
|
||||||
FilePatchFile FileFlag = 0x00100000
|
|
||||||
// FileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit
|
|
||||||
FileSingleUnit FileFlag = 0x01000000
|
|
||||||
// FileDeleteMarker - File is a deletion marker, indicating that the file no longer exists. This is used to allow patch
|
|
||||||
// archives to delete files present in lower-priority archives in the search chain. The file usually
|
|
||||||
// has length of 0 or 1 byte and its name is a hash
|
|
||||||
FileDeleteMarker FileFlag = 0x02000000
|
|
||||||
// FileSectorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded.
|
|
||||||
FileSectorCrc FileFlag = 0x04000000
|
|
||||||
// FileExists - Set if file exists, reset when the file was deleted
|
|
||||||
FileExists FileFlag = 0x80000000
|
|
||||||
)
|
|
||||||
|
|
||||||
// BlockTableEntry represents an entry in the block table
|
|
||||||
type BlockTableEntry struct { // 16 bytes
|
|
||||||
FilePosition uint32
|
|
||||||
CompressedFileSize uint32
|
|
||||||
UncompressedFileSize uint32
|
|
||||||
Flags FileFlag
|
|
||||||
// Local Stuff...
|
|
||||||
FileName string
|
|
||||||
EncryptionSeed uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasFlag returns true if the specified flag is present
|
|
||||||
func (v BlockTableEntry) HasFlag(flag FileFlag) bool {
|
|
||||||
return (v.Flags & flag) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load loads an MPQ file and returns a MPQ structure
|
|
||||||
func Load(fileName string) (d2interface.Archive, error) {
|
|
||||||
result := &MPQ{filePath: fileName}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
result.file, err = openIgnoreCase(fileName)
|
mpq.file, err = openIgnoreCase(fileName)
|
||||||
} else {
|
} else {
|
||||||
result.file, err = os.Open(fileName) //nolint:gosec // Will fix later
|
mpq.file, err = os.Open(fileName) //nolint:gosec // Will fix later
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := result.readHeader(); err != nil {
|
if err := mpq.readHeader(); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to read reader: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return mpq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func openIgnoreCase(mpqPath string) (*os.File, error) {
|
// FromFile loads an MPQ file and returns a MPQ structure
|
||||||
// First see if file exists with specified case
|
func FromFile(fileName string) (*MPQ, error) {
|
||||||
mpqFile, err := os.Open(mpqPath) //nolint:gosec // Will fix later
|
mpq, err := New(fileName)
|
||||||
if err == nil {
|
|
||||||
return mpqFile, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mpqName := filepath.Base(mpqPath)
|
|
||||||
mpqDir := filepath.Dir(mpqPath)
|
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(mpqDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range files {
|
if err := mpq.readHashTable(); err != nil {
|
||||||
if strings.EqualFold(file.Name(), mpqName) {
|
return nil, fmt.Errorf("failed to read hash table: %v", err)
|
||||||
mpqName = file.Name()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(path.Join(mpqDir, mpqName)) //nolint:gosec // Will fix later
|
if err := mpq.readBlockTable(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read block table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return file, err
|
return mpq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *MPQ) readHeader() error {
|
// getFileBlockData gets a block table entry
|
||||||
err := binary.Read(v.file, binary.LittleEndian, &v.data)
|
func (mpq *MPQ) getFileBlockData(fileName string) (*Block, error) {
|
||||||
|
fileEntry, ok := mpq.hashes[hashFilename(fileName)]
|
||||||
if err != nil {
|
if !ok {
|
||||||
return err
|
return nil, errors.New("file not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(v.data.Magic[:]) != "MPQ\x1A" {
|
if fileEntry.BlockIndex >= uint32(len(mpq.blocks)) {
|
||||||
return errors.New("invalid mpq header")
|
return nil, errors.New("invalid block index")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = v.loadHashTable()
|
return mpq.blocks[fileEntry.BlockIndex], nil
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v.loadBlockTable()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *MPQ) loadHashTable() error {
|
|
||||||
_, err := v.file.Seek(int64(v.data.HashTableOffset), 0)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hashData := make([]uint32, v.data.HashTableEntries*4) //nolint:gomnd // // Decryption magic
|
|
||||||
hash := make([]byte, 4)
|
|
||||||
|
|
||||||
for i := range hashData {
|
|
||||||
_, err := v.file.Read(hash)
|
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hashData[i] = binary.LittleEndian.Uint32(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(hashData, hashString("(hash table)", 3))
|
|
||||||
|
|
||||||
for i := uint32(0); i < v.data.HashTableEntries; i++ {
|
|
||||||
v.hashEntryMap.Insert(&HashTableEntry{
|
|
||||||
NamePartA: hashData[i*4],
|
|
||||||
NamePartB: hashData[(i*4)+1],
|
|
||||||
// https://github.com/OpenDiablo2/OpenDiablo2/issues/812
|
|
||||||
Locale: uint16(hashData[(i*4)+2] >> 16), //nolint:gomnd // // binary data
|
|
||||||
Platform: uint16(hashData[(i*4)+2] & 0xFFFF), //nolint:gomnd // // binary data
|
|
||||||
BlockIndex: hashData[(i*4)+3],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *MPQ) loadBlockTable() {
|
|
||||||
_, err := v.file.Seek(int64(v.data.BlockTableOffset), 0)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
blockData := make([]uint32, v.data.BlockTableEntries*4) //nolint:gomnd // // binary data
|
|
||||||
hash := make([]byte, 4)
|
|
||||||
|
|
||||||
for i := range blockData {
|
|
||||||
_, err = v.file.Read(hash) //nolint:errcheck // Will fix later
|
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
blockData[i] = binary.LittleEndian.Uint32(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(blockData, hashString("(block table)", 3))
|
|
||||||
|
|
||||||
for i := uint32(0); i < v.data.BlockTableEntries; i++ {
|
|
||||||
v.blockTableEntries = append(v.blockTableEntries, BlockTableEntry{
|
|
||||||
FilePosition: blockData[(i * 4)],
|
|
||||||
CompressedFileSize: blockData[(i*4)+1],
|
|
||||||
UncompressedFileSize: blockData[(i*4)+2],
|
|
||||||
Flags: FileFlag(blockData[(i*4)+3]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decrypt(data []uint32, seed uint32) {
|
|
||||||
seed2 := uint32(0xeeeeeeee) //nolint:gomnd // Decryption magic
|
|
||||||
|
|
||||||
for i := 0; i < len(data); i++ {
|
|
||||||
seed2 += cryptoLookup(0x400 + (seed & 0xff)) //nolint:gomnd // Decryption magic
|
|
||||||
result := data[i]
|
|
||||||
result ^= seed + seed2
|
|
||||||
|
|
||||||
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
|
|
||||||
seed2 = result + seed2 + (seed2 << 5) + 3 //nolint:gomnd // Decryption magic
|
|
||||||
data[i] = result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decryptBytes(data []byte, seed uint32) {
|
|
||||||
seed2 := uint32(0xEEEEEEEE) //nolint:gomnd // Decryption magic
|
|
||||||
for i := 0; i < len(data)-3; i += 4 {
|
|
||||||
seed2 += cryptoLookup(0x400 + (seed & 0xFF)) //nolint:gomnd // Decryption magic
|
|
||||||
result := binary.LittleEndian.Uint32(data[i : i+4])
|
|
||||||
result ^= seed + seed2
|
|
||||||
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
|
|
||||||
seed2 = result + seed2 + (seed2 << 5) + 3 //nolint:gomnd // Decryption magic
|
|
||||||
|
|
||||||
data[i+0] = uint8(result & 0xff) //nolint:gomnd // Decryption magic
|
|
||||||
data[i+1] = uint8((result >> 8) & 0xff) //nolint:gomnd // Decryption magic
|
|
||||||
data[i+2] = uint8((result >> 16) & 0xff) //nolint:gomnd // Decryption magic
|
|
||||||
data[i+3] = uint8((result >> 24) & 0xff) //nolint:gomnd // Decryption magic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hashString(key string, hashType uint32) uint32 {
|
|
||||||
seed1 := uint32(0x7FED7FED) //nolint:gomnd // Decryption magic
|
|
||||||
seed2 := uint32(0xEEEEEEEE) //nolint:gomnd // Decryption magic
|
|
||||||
|
|
||||||
/* prepare seeds. */
|
|
||||||
for _, char := range strings.ToUpper(key) {
|
|
||||||
seed1 = cryptoLookup((hashType*0x100)+uint32(char)) ^ (seed1 + seed2)
|
|
||||||
seed2 = uint32(char) + seed1 + seed2 + (seed2 << 5) + 3 //nolint:gomnd // Decryption magic
|
|
||||||
}
|
|
||||||
|
|
||||||
return seed1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFileBlockData gets a block table entry
|
|
||||||
func (v *MPQ) getFileBlockData(fileName string) (BlockTableEntry, error) {
|
|
||||||
fileEntry, found := v.hashEntryMap.Find(fileName)
|
|
||||||
|
|
||||||
if !found || fileEntry.BlockIndex >= uint32(len(v.blockTableEntries)) {
|
|
||||||
return BlockTableEntry{}, errors.New("file not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.blockTableEntries[fileEntry.BlockIndex], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the MPQ file
|
// Close closes the MPQ file
|
||||||
func (v *MPQ) Close() {
|
func (mpq *MPQ) Close() error {
|
||||||
err := v.file.Close()
|
return mpq.file.Close()
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileExists checks the mpq to see if the file exists
|
|
||||||
func (v *MPQ) FileExists(fileName string) bool {
|
|
||||||
return v.hashEntryMap.Contains(fileName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFile reads a file from the MPQ and returns a memory stream
|
// ReadFile reads a file from the MPQ and returns a memory stream
|
||||||
func (v *MPQ) ReadFile(fileName string) ([]byte, error) {
|
func (mpq *MPQ) ReadFile(fileName string) ([]byte, error) {
|
||||||
fileBlockData, err := v.getFileBlockData(fileName)
|
fileBlockData, err := mpq.getFileBlockData(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte{}, err
|
return []byte{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fileBlockData.FileName = strings.ToLower(fileName)
|
fileBlockData.FileName = strings.ToLower(fileName)
|
||||||
|
|
||||||
fileBlockData.calculateEncryptionSeed()
|
stream, err := CreateStream(mpq, fileBlockData, fileName)
|
||||||
mpqStream, err := CreateStream(v, fileBlockData, fileName)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte{}, err
|
return []byte{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := make([]byte, fileBlockData.UncompressedFileSize)
|
buffer := make([]byte, fileBlockData.UncompressedFileSize)
|
||||||
mpqStream.Read(buffer, 0, fileBlockData.UncompressedFileSize)
|
if _, err := stream.Read(buffer, 0, fileBlockData.UncompressedFileSize); err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
return buffer, nil
|
return buffer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFileStream reads the mpq file data and returns a stream
|
// ReadFileStream reads the mpq file data and returns a stream
|
||||||
func (v *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
|
func (mpq *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
|
||||||
fileBlockData, err := v.getFileBlockData(fileName)
|
fileBlockData, err := mpq.getFileBlockData(fileName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fileBlockData.FileName = strings.ToLower(fileName)
|
fileBlockData.FileName = strings.ToLower(fileName)
|
||||||
fileBlockData.calculateEncryptionSeed()
|
|
||||||
|
|
||||||
mpqStream, err := CreateStream(v, fileBlockData, fileName)
|
stream, err := CreateStream(mpq, fileBlockData, fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &MpqDataStream{stream: mpqStream}, nil
|
return &MpqDataStream{stream: stream}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadTextFile reads a file and returns it as a string
|
// ReadTextFile reads a file and returns it as a string
|
||||||
func (v *MPQ) ReadTextFile(fileName string) (string, error) {
|
func (mpq *MPQ) ReadTextFile(fileName string) (string, error) {
|
||||||
data, err := v.ReadFile(fileName)
|
data, err := mpq.ReadFile(fileName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -351,20 +142,9 @@ func (v *MPQ) ReadTextFile(fileName string) (string, error) {
|
|||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *BlockTableEntry) calculateEncryptionSeed() {
|
// Listfile returns the list of files in this MPQ
|
||||||
fileName := path.Base(v.FileName)
|
func (mpq *MPQ) Listfile() ([]string, error) {
|
||||||
v.EncryptionSeed = hashString(fileName, 3)
|
data, err := mpq.ReadFile("(listfile)")
|
||||||
|
|
||||||
if !v.HasFlag(FileFixKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
v.EncryptionSeed = (v.EncryptionSeed + v.FilePosition) ^ v.UncompressedFileSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFileList returns the list of files in this MPQ
|
|
||||||
func (v *MPQ) GetFileList() ([]string, error) {
|
|
||||||
data, err := v.ReadFile("(listfile)")
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -384,16 +164,44 @@ func (v *MPQ) GetFileList() ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Path returns the MPQ file path
|
// Path returns the MPQ file path
|
||||||
func (v *MPQ) Path() string {
|
func (mpq *MPQ) Path() string {
|
||||||
return v.filePath
|
return mpq.filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains returns bool for whether the given filename exists in the mpq
|
// Contains returns bool for whether the given filename exists in the mpq
|
||||||
func (v *MPQ) Contains(filename string) bool {
|
func (mpq *MPQ) Contains(filename string) bool {
|
||||||
return v.hashEntryMap.Contains(filename)
|
_, ok := mpq.hashes[hashFilename(filename)]
|
||||||
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size returns the size of the mpq in bytes
|
// Size returns the size of the mpq in bytes
|
||||||
func (v *MPQ) Size() uint32 {
|
func (mpq *MPQ) Size() uint32 {
|
||||||
return v.data.ArchiveSize
|
return mpq.header.ArchiveSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func openIgnoreCase(mpqPath string) (*os.File, error) {
|
||||||
|
// First see if file exists with specified case
|
||||||
|
mpqFile, err := os.Open(mpqPath) //nolint:gosec // Will fix later
|
||||||
|
if err != nil {
|
||||||
|
mpqName := filepath.Base(mpqPath)
|
||||||
|
mpqDir := filepath.Dir(mpqPath)
|
||||||
|
|
||||||
|
var files []fs.FileInfo
|
||||||
|
files, err = ioutil.ReadDir(mpqDir)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.EqualFold(file.Name(), mpqName) {
|
||||||
|
mpqName = file.Name()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Open(filepath.Join(mpqDir, mpqName)) //nolint:gosec // Will fix later
|
||||||
|
}
|
||||||
|
|
||||||
|
return mpqFile, err
|
||||||
}
|
}
|
||||||
|
77
d2common/d2fileformats/d2mpq/mpq_block.go
Normal file
77
d2common/d2fileformats/d2mpq/mpq_block.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package d2mpq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileFlag represents flags for a file record in the MPQ archive
|
||||||
|
type FileFlag uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FileImplode - File is compressed using PKWARE Data compression library
|
||||||
|
FileImplode FileFlag = 0x00000100
|
||||||
|
// FileCompress - File is compressed using combination of compression methods
|
||||||
|
FileCompress FileFlag = 0x00000200
|
||||||
|
// FileEncrypted - The file is encrypted
|
||||||
|
FileEncrypted FileFlag = 0x00010000
|
||||||
|
// FileFixKey - The decryption key for the file is altered according to the position of the file in the archive
|
||||||
|
FileFixKey FileFlag = 0x00020000
|
||||||
|
// FilePatchFile - The file contains incremental patch for an existing file in base MPQ
|
||||||
|
FilePatchFile FileFlag = 0x00100000
|
||||||
|
// FileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit
|
||||||
|
FileSingleUnit FileFlag = 0x01000000
|
||||||
|
// FileDeleteMarker - File is a deletion marker, indicating that the file no longer exists. This is used to allow patch
|
||||||
|
// archives to delete files present in lower-priority archives in the search chain. The file usually
|
||||||
|
// has length of 0 or 1 byte and its name is a hash
|
||||||
|
FileDeleteMarker FileFlag = 0x02000000
|
||||||
|
// FileSectorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded.
|
||||||
|
FileSectorCrc FileFlag = 0x04000000
|
||||||
|
// FileExists - Set if file exists, reset when the file was deleted
|
||||||
|
FileExists FileFlag = 0x80000000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Block represents an entry in the block table
|
||||||
|
type Block struct { // 16 bytes
|
||||||
|
FilePosition uint32
|
||||||
|
CompressedFileSize uint32
|
||||||
|
UncompressedFileSize uint32
|
||||||
|
Flags FileFlag
|
||||||
|
// Local Stuff...
|
||||||
|
FileName string
|
||||||
|
EncryptionSeed uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasFlag returns true if the specified flag is present
|
||||||
|
func (b *Block) HasFlag(flag FileFlag) bool {
|
||||||
|
return (b.Flags & flag) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) calculateEncryptionSeed(fileName string) {
|
||||||
|
fileName = fileName[strings.LastIndex(fileName, `\`)+1:]
|
||||||
|
seed := hashString(fileName, 3)
|
||||||
|
b.EncryptionSeed = (seed + b.FilePosition) ^ b.UncompressedFileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gomnd // number
|
||||||
|
func (mpq *MPQ) readBlockTable() error {
|
||||||
|
if _, err := mpq.file.Seek(int64(mpq.header.BlockTableOffset), io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
blockData, err := decryptTable(mpq.file, mpq.header.BlockTableEntries, "(block table)")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, i := uint32(0), uint32(0); i < mpq.header.BlockTableEntries; n, i = n+4, i+1 {
|
||||||
|
mpq.blocks = append(mpq.blocks, &Block{
|
||||||
|
FilePosition: blockData[n],
|
||||||
|
CompressedFileSize: blockData[n+1],
|
||||||
|
UncompressedFileSize: blockData[n+2],
|
||||||
|
Flags: FileFlag(blockData[n+3]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -11,14 +11,14 @@ type MpqDataStream struct {
|
|||||||
|
|
||||||
// Read reads data from the data stream
|
// Read reads data from the data stream
|
||||||
func (m *MpqDataStream) Read(p []byte) (n int, err error) {
|
func (m *MpqDataStream) Read(p []byte) (n int, err error) {
|
||||||
totalRead := m.stream.Read(p, 0, uint32(len(p)))
|
totalRead, err := m.stream.Read(p, 0, uint32(len(p)))
|
||||||
return int(totalRead), nil
|
return int(totalRead), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek sets the position of the data stream
|
// Seek sets the position of the data stream
|
||||||
func (m *MpqDataStream) Seek(offset int64, whence int) (int64, error) {
|
func (m *MpqDataStream) Seek(offset int64, whence int) (int64, error) {
|
||||||
m.stream.CurrentPosition = uint32(offset + int64(whence))
|
m.stream.Position = uint32(offset + int64(whence))
|
||||||
return int64(m.stream.CurrentPosition), nil
|
return int64(m.stream.Position), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the data stream
|
// Close closes the data stream
|
||||||
|
45
d2common/d2fileformats/d2mpq/mpq_hash.go
Normal file
45
d2common/d2fileformats/d2mpq/mpq_hash.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package d2mpq
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// Hash represents a hashed file entry in the MPQ file
|
||||||
|
type Hash struct { // 16 bytes
|
||||||
|
A uint32
|
||||||
|
B uint32
|
||||||
|
Locale uint16
|
||||||
|
Platform uint16
|
||||||
|
BlockIndex uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name64 returns part A and B as uint64
|
||||||
|
func (h *Hash) Name64() uint64 {
|
||||||
|
return uint64(h.A)<<32 | uint64(h.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gomnd // number
|
||||||
|
func (mpq *MPQ) readHashTable() error {
|
||||||
|
if _, err := mpq.file.Seek(int64(mpq.header.HashTableOffset), io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashData, err := decryptTable(mpq.file, mpq.header.HashTableEntries, "(hash table)")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mpq.hashes = make(map[uint64]*Hash)
|
||||||
|
|
||||||
|
for n, i := uint32(0), uint32(0); i < mpq.header.HashTableEntries; n, i = n+4, i+1 {
|
||||||
|
e := &Hash{
|
||||||
|
A: hashData[n],
|
||||||
|
B: hashData[n+1],
|
||||||
|
// https://github.com/OpenDiablo2/OpenDiablo2/issues/812
|
||||||
|
Locale: uint16(hashData[n+2] >> 16), //nolint:gomnd // // binary data
|
||||||
|
Platform: uint16(hashData[n+2] & 0xFFFF), //nolint:gomnd // // binary data
|
||||||
|
BlockIndex: hashData[n+3],
|
||||||
|
}
|
||||||
|
mpq.hashes[e.Name64()] = e
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
36
d2common/d2fileformats/d2mpq/mpq_header.go
Normal file
36
d2common/d2fileformats/d2mpq/mpq_header.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package d2mpq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Header Represents a MPQ file
|
||||||
|
type Header struct {
|
||||||
|
Magic [4]byte
|
||||||
|
HeaderSize uint32
|
||||||
|
ArchiveSize uint32
|
||||||
|
FormatVersion uint16
|
||||||
|
BlockSize uint16
|
||||||
|
HashTableOffset uint32
|
||||||
|
BlockTableOffset uint32
|
||||||
|
HashTableEntries uint32
|
||||||
|
BlockTableEntries uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mpq *MPQ) readHeader() error {
|
||||||
|
if _, err := mpq.file.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := binary.Read(mpq.file, binary.LittleEndian, &mpq.header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(mpq.header.Magic[:]) != "MPQ\x1A" {
|
||||||
|
return errors.New("invalid mpq header")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -6,8 +6,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"io"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/JoshVarga/blast"
|
"github.com/JoshVarga/blast"
|
||||||
|
|
||||||
@ -17,80 +16,63 @@ import (
|
|||||||
|
|
||||||
// Stream represents a stream of data in an MPQ archive
|
// Stream represents a stream of data in an MPQ archive
|
||||||
type Stream struct {
|
type Stream struct {
|
||||||
BlockTableEntry BlockTableEntry
|
Data []byte
|
||||||
BlockPositions []uint32
|
Positions []uint32
|
||||||
CurrentData []byte
|
MPQ *MPQ
|
||||||
FileName string
|
Block *Block
|
||||||
MPQData *MPQ
|
Index uint32
|
||||||
EncryptionSeed uint32
|
Size uint32
|
||||||
CurrentPosition uint32
|
Position uint32
|
||||||
CurrentBlockIndex uint32
|
|
||||||
BlockSize uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateStream creates an MPQ stream
|
// CreateStream creates an MPQ stream
|
||||||
func CreateStream(mpq *MPQ, blockTableEntry BlockTableEntry, fileName string) (*Stream, error) {
|
func CreateStream(mpq *MPQ, block *Block, fileName string) (*Stream, error) {
|
||||||
result := &Stream{
|
s := &Stream{
|
||||||
MPQData: mpq,
|
MPQ: mpq,
|
||||||
BlockTableEntry: blockTableEntry,
|
Block: block,
|
||||||
CurrentBlockIndex: 0xFFFFFFFF, //nolint:gomnd // MPQ magic
|
Index: 0xFFFFFFFF, //nolint:gomnd // MPQ magic
|
||||||
}
|
|
||||||
fileSegs := strings.Split(fileName, `\`)
|
|
||||||
result.EncryptionSeed = hashString(fileSegs[len(fileSegs)-1], 3)
|
|
||||||
|
|
||||||
if result.BlockTableEntry.HasFlag(FileFixKey) {
|
|
||||||
result.EncryptionSeed = (result.EncryptionSeed + result.BlockTableEntry.FilePosition) ^ result.BlockTableEntry.UncompressedFileSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.BlockSize = 0x200 << result.MPQData.data.BlockSize //nolint:gomnd // MPQ magic
|
if s.Block.HasFlag(FileFixKey) {
|
||||||
|
s.Block.calculateEncryptionSeed(fileName)
|
||||||
if result.BlockTableEntry.HasFlag(FilePatchFile) {
|
|
||||||
log.Fatal("Patching is not supported")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
s.Size = 0x200 << s.MPQ.header.BlockSize //nolint:gomnd // MPQ magic
|
||||||
|
|
||||||
if (result.BlockTableEntry.HasFlag(FileCompress) || result.BlockTableEntry.HasFlag(FileImplode)) &&
|
if s.Block.HasFlag(FilePatchFile) {
|
||||||
!result.BlockTableEntry.HasFlag(FileSingleUnit) {
|
return nil, errors.New("patching is not supported")
|
||||||
err = result.loadBlockOffsets()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, err
|
if (s.Block.HasFlag(FileCompress) || s.Block.HasFlag(FileImplode)) && !s.Block.HasFlag(FileSingleUnit) {
|
||||||
|
if err := s.loadBlockOffsets(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Stream) loadBlockOffsets() error {
|
func (v *Stream) loadBlockOffsets() error {
|
||||||
blockPositionCount := ((v.BlockTableEntry.UncompressedFileSize + v.BlockSize - 1) / v.BlockSize) + 1
|
if _, err := v.MPQ.file.Seek(int64(v.Block.FilePosition), io.SeekStart); err != nil {
|
||||||
v.BlockPositions = make([]uint32, blockPositionCount)
|
|
||||||
|
|
||||||
_, err := v.MPQData.file.Seek(int64(v.BlockTableEntry.FilePosition), 0)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mpqBytes := make([]byte, blockPositionCount*4) //nolint:gomnd // MPQ magic
|
blockPositionCount := ((v.Block.UncompressedFileSize + v.Size - 1) / v.Size) + 1
|
||||||
|
v.Positions = make([]uint32, blockPositionCount)
|
||||||
|
|
||||||
_, err = v.MPQData.file.Read(mpqBytes)
|
if err := binary.Read(v.MPQ.file, binary.LittleEndian, &v.Positions); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range v.BlockPositions {
|
if v.Block.HasFlag(FileEncrypted) {
|
||||||
idx := i * 4 //nolint:gomnd // MPQ magic
|
decrypt(v.Positions, v.Block.EncryptionSeed-1)
|
||||||
v.BlockPositions[i] = binary.LittleEndian.Uint32(mpqBytes[idx : idx+4])
|
|
||||||
}
|
|
||||||
|
|
||||||
blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic
|
blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic
|
||||||
|
if v.Positions[0] != blockPosSize {
|
||||||
if v.BlockTableEntry.HasFlag(FileEncrypted) {
|
|
||||||
decrypt(v.BlockPositions, v.EncryptionSeed-1)
|
|
||||||
|
|
||||||
if v.BlockPositions[0] != blockPosSize {
|
|
||||||
log.Println("Decryption of MPQ failed!")
|
|
||||||
return errors.New("decryption of MPQ failed")
|
return errors.New("decryption of MPQ failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.BlockPositions[1] > v.BlockSize+blockPosSize {
|
if v.Positions[1] > v.Size+blockPosSize {
|
||||||
log.Println("Decryption of MPQ failed!")
|
|
||||||
return errors.New("decryption of MPQ failed")
|
return errors.New("decryption of MPQ failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,16 +80,18 @@ func (v *Stream) loadBlockOffsets() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 {
|
func (v *Stream) Read(buffer []byte, offset, count uint32) (readTotal uint32, err error) {
|
||||||
if v.BlockTableEntry.HasFlag(FileSingleUnit) {
|
if v.Block.HasFlag(FileSingleUnit) {
|
||||||
return v.readInternalSingleUnit(buffer, offset, count)
|
return v.readInternalSingleUnit(buffer, offset, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
toRead := count
|
var read uint32
|
||||||
readTotal := uint32(0)
|
|
||||||
|
|
||||||
|
toRead := count
|
||||||
for toRead > 0 {
|
for toRead > 0 {
|
||||||
read := v.readInternal(buffer, offset, toRead)
|
if read, err = v.readInternal(buffer, offset, toRead); err != nil {
|
||||||
|
return readTotal, err
|
||||||
|
}
|
||||||
|
|
||||||
if read == 0 {
|
if read == 0 {
|
||||||
break
|
break
|
||||||
@ -118,219 +102,228 @@ func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 {
|
|||||||
toRead -= read
|
toRead -= read
|
||||||
}
|
}
|
||||||
|
|
||||||
return readTotal
|
return readTotal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) uint32 {
|
func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) (uint32, error) {
|
||||||
if len(v.CurrentData) == 0 {
|
if len(v.Data) == 0 {
|
||||||
v.loadSingleUnit()
|
if err := v.loadSingleUnit(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bytesToCopy := d2math.Min(uint32(len(v.CurrentData))-v.CurrentPosition, count)
|
return v.copy(buffer, offset, v.Position, count)
|
||||||
|
|
||||||
copy(buffer[offset:offset+bytesToCopy], v.CurrentData[v.CurrentPosition:v.CurrentPosition+bytesToCopy])
|
|
||||||
|
|
||||||
v.CurrentPosition += bytesToCopy
|
|
||||||
|
|
||||||
return bytesToCopy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Stream) readInternal(buffer []byte, offset, count uint32) uint32 {
|
func (v *Stream) readInternal(buffer []byte, offset, count uint32) (uint32, error) {
|
||||||
v.bufferData()
|
if err := v.bufferData(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
localPosition := v.CurrentPosition % v.BlockSize
|
localPosition := v.Position % v.Size
|
||||||
bytesToCopy := d2math.MinInt32(int32(len(v.CurrentData))-int32(localPosition), int32(count))
|
|
||||||
|
|
||||||
|
return v.copy(buffer, offset, localPosition, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Stream) copy(buffer []byte, offset, pos, count uint32) (uint32, error) {
|
||||||
|
bytesToCopy := d2math.Min(uint32(len(v.Data))-pos, count)
|
||||||
if bytesToCopy <= 0 {
|
if bytesToCopy <= 0 {
|
||||||
return 0
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(buffer[offset:offset+uint32(bytesToCopy)], v.CurrentData[localPosition:localPosition+uint32(bytesToCopy)])
|
copy(buffer[offset:offset+bytesToCopy], v.Data[pos:pos+bytesToCopy])
|
||||||
|
v.Position += bytesToCopy
|
||||||
|
|
||||||
v.CurrentPosition += uint32(bytesToCopy)
|
return bytesToCopy, nil
|
||||||
|
|
||||||
return uint32(bytesToCopy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Stream) bufferData() {
|
func (v *Stream) bufferData() (err error) {
|
||||||
requiredBlock := v.CurrentPosition / v.BlockSize
|
blockIndex := v.Position / v.Size
|
||||||
|
|
||||||
if requiredBlock == v.CurrentBlockIndex {
|
if blockIndex == v.Index {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedLength := d2math.Min(v.BlockTableEntry.UncompressedFileSize-(requiredBlock*v.BlockSize), v.BlockSize)
|
expectedLength := d2math.Min(v.Block.UncompressedFileSize-(blockIndex*v.Size), v.Size)
|
||||||
v.CurrentData = v.loadBlock(requiredBlock, expectedLength)
|
if v.Data, err = v.loadBlock(blockIndex, expectedLength); err != nil {
|
||||||
v.CurrentBlockIndex = requiredBlock
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Index = blockIndex
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Stream) loadSingleUnit() {
|
func (v *Stream) loadSingleUnit() (err error) {
|
||||||
fileData := make([]byte, v.BlockSize)
|
if _, err = v.MPQ.file.Seek(int64(v.MPQ.header.HeaderSize), io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
_, err := v.MPQData.file.Seek(int64(v.MPQData.data.HeaderSize), 0)
|
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = v.MPQData.file.Read(fileData)
|
fileData := make([]byte, v.Size)
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
if _, err = v.MPQ.file.Read(fileData); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.BlockSize == v.BlockTableEntry.UncompressedFileSize {
|
if v.Size == v.Block.UncompressedFileSize {
|
||||||
v.CurrentData = fileData
|
v.Data = fileData
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
v.CurrentData = decompressMulti(fileData, v.BlockTableEntry.UncompressedFileSize)
|
v.Data, err = decompressMulti(fileData, v.Block.UncompressedFileSize)
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Stream) loadBlock(blockIndex, expectedLength uint32) []byte {
|
func (v *Stream) loadBlock(blockIndex, expectedLength uint32) ([]byte, error) {
|
||||||
var (
|
var (
|
||||||
offset uint32
|
offset uint32
|
||||||
toRead uint32
|
toRead uint32
|
||||||
)
|
)
|
||||||
|
|
||||||
if v.BlockTableEntry.HasFlag(FileCompress) || v.BlockTableEntry.HasFlag(FileImplode) {
|
if v.Block.HasFlag(FileCompress) || v.Block.HasFlag(FileImplode) {
|
||||||
offset = v.BlockPositions[blockIndex]
|
offset = v.Positions[blockIndex]
|
||||||
toRead = v.BlockPositions[blockIndex+1] - offset
|
toRead = v.Positions[blockIndex+1] - offset
|
||||||
} else {
|
} else {
|
||||||
offset = blockIndex * v.BlockSize
|
offset = blockIndex * v.Size
|
||||||
toRead = expectedLength
|
toRead = expectedLength
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += v.BlockTableEntry.FilePosition
|
offset += v.Block.FilePosition
|
||||||
data := make([]byte, toRead)
|
data := make([]byte, toRead)
|
||||||
|
|
||||||
_, err := v.MPQData.file.Seek(int64(offset), 0)
|
if _, err := v.MPQ.file.Seek(int64(offset), io.SeekStart); err != nil {
|
||||||
if err != nil {
|
return []byte{}, err
|
||||||
log.Print(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = v.MPQData.file.Read(data)
|
if _, err := v.MPQ.file.Read(data); err != nil {
|
||||||
if err != nil {
|
return []byte{}, err
|
||||||
log.Print(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.BlockTableEntry.HasFlag(FileEncrypted) && v.BlockTableEntry.UncompressedFileSize > 3 {
|
if v.Block.HasFlag(FileEncrypted) && v.Block.UncompressedFileSize > 3 {
|
||||||
if v.EncryptionSeed == 0 {
|
if v.Block.EncryptionSeed == 0 {
|
||||||
panic("Unable to determine encryption key")
|
return []byte{}, errors.New("unable to determine encryption key")
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptBytes(data, blockIndex+v.EncryptionSeed)
|
decryptBytes(data, blockIndex+v.Block.EncryptionSeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.BlockTableEntry.HasFlag(FileCompress) && (toRead != expectedLength) {
|
if v.Block.HasFlag(FileCompress) && (toRead != expectedLength) {
|
||||||
if !v.BlockTableEntry.HasFlag(FileSingleUnit) {
|
if !v.Block.HasFlag(FileSingleUnit) {
|
||||||
data = decompressMulti(data, expectedLength)
|
return decompressMulti(data, expectedLength)
|
||||||
} else {
|
|
||||||
data = pkDecompress(data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.BlockTableEntry.HasFlag(FileImplode) && (toRead != expectedLength) {
|
return pkDecompress(data)
|
||||||
data = pkDecompress(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
if v.Block.HasFlag(FileImplode) && (toRead != expectedLength) {
|
||||||
|
return pkDecompress(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gomnd // Will fix enum values later
|
//nolint:gomnd,funlen,gocyclo // Will fix enum values later, can't help function length
|
||||||
func decompressMulti(data []byte /*expectedLength*/, _ uint32) []byte {
|
func decompressMulti(data []byte /*expectedLength*/, _ uint32) ([]byte, error) {
|
||||||
compressionType := data[0]
|
compressionType := data[0]
|
||||||
|
|
||||||
switch compressionType {
|
switch compressionType {
|
||||||
case 1: // Huffman
|
case 1: // Huffman
|
||||||
panic("huffman decompression not supported")
|
return []byte{}, errors.New("huffman decompression not supported")
|
||||||
case 2: // ZLib/Deflate
|
case 2: // ZLib/Deflate
|
||||||
return deflate(data[1:])
|
return deflate(data[1:])
|
||||||
case 8: // PKLib/Impode
|
case 8: // PKLib/Impode
|
||||||
return pkDecompress(data[1:])
|
return pkDecompress(data[1:])
|
||||||
case 0x10: // BZip2
|
case 0x10: // BZip2
|
||||||
panic("bzip2 decompression not supported")
|
return []byte{}, errors.New("bzip2 decompression not supported")
|
||||||
case 0x80: // IMA ADPCM Stereo
|
case 0x80: // IMA ADPCM Stereo
|
||||||
return d2compression.WavDecompress(data[1:], 2)
|
return d2compression.WavDecompress(data[1:], 2)
|
||||||
case 0x40: // IMA ADPCM Mono
|
case 0x40: // IMA ADPCM Mono
|
||||||
return d2compression.WavDecompress(data[1:], 1)
|
return d2compression.WavDecompress(data[1:], 1)
|
||||||
case 0x12:
|
case 0x12:
|
||||||
panic("lzma decompression not supported")
|
return []byte{}, errors.New("lzma decompression not supported")
|
||||||
// Combos
|
// Combos
|
||||||
case 0x22:
|
case 0x22:
|
||||||
// sparse then zlib
|
// sparse then zlib
|
||||||
panic("sparse decompression + deflate decompression not supported")
|
return []byte{}, errors.New("sparse decompression + deflate decompression not supported")
|
||||||
case 0x30:
|
case 0x30:
|
||||||
// sparse then bzip2
|
// sparse then bzip2
|
||||||
panic("sparse decompression + bzip2 decompression not supported")
|
return []byte{}, errors.New("sparse decompression + bzip2 decompression not supported")
|
||||||
case 0x41:
|
case 0x41:
|
||||||
sinput := d2compression.HuffmanDecompress(data[1:])
|
sinput, err := d2compression.WavDecompress(d2compression.HuffmanDecompress(data[1:]), 1)
|
||||||
sinput = d2compression.WavDecompress(sinput, 1)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
tmp := make([]byte, len(sinput))
|
tmp := make([]byte, len(sinput))
|
||||||
|
|
||||||
copy(tmp, sinput)
|
copy(tmp, sinput)
|
||||||
|
|
||||||
return tmp
|
return tmp, nil
|
||||||
case 0x48:
|
case 0x48:
|
||||||
// byte[] result = PKDecompress(sinput, outputLength);
|
// byte[] result = PKDecompress(sinput, outputLength);
|
||||||
// return MpqWavCompression.Decompress(new MemoryStream(result), 1);
|
// return MpqWavCompression.Decompress(new MemoryStream(result), 1);
|
||||||
panic("pk + mpqwav decompression not supported")
|
return []byte{}, errors.New("pk + mpqwav decompression not supported")
|
||||||
case 0x81:
|
case 0x81:
|
||||||
sinput := d2compression.HuffmanDecompress(data[1:])
|
sinput, err := d2compression.WavDecompress(d2compression.HuffmanDecompress(data[1:]), 2)
|
||||||
sinput = d2compression.WavDecompress(sinput, 2)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
tmp := make([]byte, len(sinput))
|
tmp := make([]byte, len(sinput))
|
||||||
copy(tmp, sinput)
|
copy(tmp, sinput)
|
||||||
|
|
||||||
return tmp
|
return tmp, nil
|
||||||
case 0x88:
|
case 0x88:
|
||||||
// byte[] result = PKDecompress(sinput, outputLength);
|
// byte[] result = PKDecompress(sinput, outputLength);
|
||||||
// return MpqWavCompression.Decompress(new MemoryStream(result), 2);
|
// return MpqWavCompression.Decompress(new MemoryStream(result), 2);
|
||||||
panic("pk + wav decompression not supported")
|
return []byte{}, errors.New("pk + wav decompression not supported")
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("decompression not supported for unknown compression type %X", compressionType))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return []byte{}, fmt.Errorf("decompression not supported for unknown compression type %X", compressionType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deflate(data []byte) []byte {
|
func deflate(data []byte) ([]byte, error) {
|
||||||
b := bytes.NewReader(data)
|
b := bytes.NewReader(data)
|
||||||
|
|
||||||
r, err := zlib.NewReader(b)
|
r, err := zlib.NewReader(b)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return []byte{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
_, err = buffer.ReadFrom(r)
|
_, err = buffer.ReadFrom(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
return []byte{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.Close()
|
err = r.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
return []byte{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer.Bytes()
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pkDecompress(data []byte) []byte {
|
func pkDecompress(data []byte) ([]byte, error) {
|
||||||
b := bytes.NewReader(data)
|
b := bytes.NewReader(data)
|
||||||
r, err := blast.NewReader(b)
|
|
||||||
|
|
||||||
|
r, err := blast.NewReader(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return []byte{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
_, err = buffer.ReadFrom(r)
|
if _, err = buffer.ReadFrom(r); err != nil {
|
||||||
if err != nil {
|
return []byte{}, err
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.Close()
|
err = r.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return []byte{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer.Bytes()
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
@ -41,3 +41,15 @@ func Load(data []byte) (*PL2, error) {
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marshal encodes PL2 back into byte slice
|
||||||
|
func (p *PL2) Marshal() []byte {
|
||||||
|
restruct.EnableExprBeta()
|
||||||
|
|
||||||
|
data, err := restruct.Pack(binary.LittleEndian, p)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
package d2pl2
|
package d2pl2
|
||||||
|
|
||||||
|
const (
|
||||||
|
bitShift0 = 8 * iota
|
||||||
|
bitShift8
|
||||||
|
bitShift16
|
||||||
|
bitShift24
|
||||||
|
|
||||||
|
mask = 0xff
|
||||||
|
)
|
||||||
|
|
||||||
// PL2Color represents an RGBA color
|
// PL2Color represents an RGBA color
|
||||||
type PL2Color struct {
|
type PL2Color struct {
|
||||||
R uint8
|
R uint8
|
||||||
@ -7,3 +16,30 @@ type PL2Color struct {
|
|||||||
B uint8
|
B uint8
|
||||||
_ uint8
|
_ uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RGBA returns RGBA values of PL2Color
|
||||||
|
func (p *PL2Color) RGBA() uint32 {
|
||||||
|
return toComposite(p.R, p.G, p.B, mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRGBA sets PL2Color's value to rgba given
|
||||||
|
func (p *PL2Color) SetRGBA(rgba uint32) {
|
||||||
|
p.R, p.G, p.B = toComponent(rgba)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toComposite(w, x, y, z uint8) uint32 {
|
||||||
|
composite := uint32(w) << bitShift24
|
||||||
|
composite += uint32(x) << bitShift16
|
||||||
|
composite += uint32(y) << bitShift8
|
||||||
|
composite += uint32(z) << bitShift0
|
||||||
|
|
||||||
|
return composite
|
||||||
|
}
|
||||||
|
|
||||||
|
func toComponent(wxyz uint32) (w, x, y uint8) {
|
||||||
|
w = uint8(wxyz >> bitShift24 & mask)
|
||||||
|
x = uint8(wxyz >> bitShift16 & mask)
|
||||||
|
y = uint8(wxyz >> bitShift8 & mask)
|
||||||
|
|
||||||
|
return w, x, y
|
||||||
|
}
|
||||||
|
@ -6,3 +6,13 @@ type PL2Color24Bits struct {
|
|||||||
G uint8
|
G uint8
|
||||||
B uint8
|
B uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RGBA returns RGBA values of PL2Color
|
||||||
|
func (p *PL2Color24Bits) RGBA() uint32 {
|
||||||
|
return toComposite(p.R, p.G, p.B, mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRGBA sets PL2Color's value to rgba given
|
||||||
|
func (p *PL2Color24Bits) SetRGBA(rgba uint32) {
|
||||||
|
p.R, p.G, p.B = toComponent(rgba)
|
||||||
|
}
|
||||||
|
40
d2common/d2fileformats/d2pl2/pl2_test.go
Normal file
40
d2common/d2fileformats/d2pl2/pl2_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package d2pl2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func exampleData() *PL2 {
|
||||||
|
result := &PL2{
|
||||||
|
BasePalette: PL2Palette{},
|
||||||
|
SelectedUintShift: PL2PaletteTransform{},
|
||||||
|
RedTones: PL2PaletteTransform{},
|
||||||
|
GreenTones: PL2PaletteTransform{},
|
||||||
|
BlueTones: PL2PaletteTransform{},
|
||||||
|
DarkendColorShift: PL2PaletteTransform{},
|
||||||
|
}
|
||||||
|
|
||||||
|
result.BasePalette.Colors[0].R = 8
|
||||||
|
result.DarkendColorShift.Indices[0] = 123
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPL2_MarshalUnmarshal(t *testing.T) {
|
||||||
|
pl2 := exampleData()
|
||||||
|
|
||||||
|
data := pl2.Marshal()
|
||||||
|
|
||||||
|
newPL2, err := Load(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newPL2.BasePalette.Colors[0] != pl2.BasePalette.Colors[0] {
|
||||||
|
t.Fatal("unexpected length")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pl2.DarkendColorShift.Indices[0] != newPL2.DarkendColorShift.Indices[0] {
|
||||||
|
t.Fatal("unexpected index set")
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
package d2tbl
|
package d2tbl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
|
||||||
@ -10,6 +10,97 @@ import (
|
|||||||
// TextDictionary is a string map
|
// TextDictionary is a string map
|
||||||
type TextDictionary map[string]string
|
type TextDictionary map[string]string
|
||||||
|
|
||||||
|
func (td TextDictionary) loadHashEntries(hashEntries []*textDictionaryHashEntry, br *d2datautils.StreamReader) error {
|
||||||
|
for i := 0; i < len(hashEntries); i++ {
|
||||||
|
entry := textDictionaryHashEntry{}
|
||||||
|
|
||||||
|
active, err := br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading active: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.IsActive = active > 0
|
||||||
|
|
||||||
|
entry.Index, err = br.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading Index: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.HashValue, err = br.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading hash value: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.IndexString, err = br.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading index string pos: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.NameString, err = br.ReadUInt32()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading name string pos: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.NameLength, err = br.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading name length: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hashEntries[i] = &entry
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range hashEntries {
|
||||||
|
if !hashEntries[idx].IsActive {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := td.loadHashEntry(idx, hashEntries[idx], br); err != nil {
|
||||||
|
return fmt.Errorf("loading entry %d: %v", idx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td TextDictionary) loadHashEntry(idx int, hashEntry *textDictionaryHashEntry, br *d2datautils.StreamReader) error {
|
||||||
|
br.SetPosition(uint64(hashEntry.NameString))
|
||||||
|
|
||||||
|
nameVal, err := br.ReadBytes(int(hashEntry.NameLength - 1))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading name value: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
value := string(nameVal)
|
||||||
|
|
||||||
|
br.SetPosition(uint64(hashEntry.IndexString))
|
||||||
|
|
||||||
|
key := ""
|
||||||
|
|
||||||
|
for {
|
||||||
|
b, err := br.ReadByte()
|
||||||
|
if b == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading kay char: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key += string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == "x" || key == "X" {
|
||||||
|
key = "#" + strconv.Itoa(idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, exists := td[key]
|
||||||
|
if !exists {
|
||||||
|
td[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type textDictionaryHashEntry struct {
|
type textDictionaryHashEntry struct {
|
||||||
IsActive bool
|
IsActive bool
|
||||||
Index uint16
|
Index uint16
|
||||||
@ -19,95 +110,151 @@ type textDictionaryHashEntry struct {
|
|||||||
NameLength uint16
|
NameLength uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
var lookupTable TextDictionary //nolint:gochecknoglobals // currently global by design
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
crcByteCount = 2
|
crcByteCount = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// TranslateString returns the translation of the given string
|
|
||||||
func TranslateString(key string) string {
|
|
||||||
result, ok := lookupTable[key]
|
|
||||||
if !ok {
|
|
||||||
// Fix to allow v.setDescLabels("#123") to be bypassed for a patch in issue #360. Reenable later.
|
|
||||||
// log.Panicf("Could not find a string for the key '%s'", key)
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadTextDictionary loads the text dictionary from the given data
|
// LoadTextDictionary loads the text dictionary from the given data
|
||||||
func LoadTextDictionary(dictionaryData []byte) TextDictionary {
|
func LoadTextDictionary(dictionaryData []byte) (TextDictionary, error) {
|
||||||
if lookupTable == nil {
|
lookupTable := make(TextDictionary)
|
||||||
lookupTable = make(TextDictionary)
|
|
||||||
}
|
|
||||||
|
|
||||||
br := d2datautils.CreateStreamReader(dictionaryData)
|
br := d2datautils.CreateStreamReader(dictionaryData)
|
||||||
|
|
||||||
// skip past the CRC
|
// skip past the CRC
|
||||||
br.ReadBytes(crcByteCount)
|
_, _ = br.ReadBytes(crcByteCount)
|
||||||
|
|
||||||
numberOfElements := br.GetUInt16()
|
var err error
|
||||||
hashTableSize := br.GetUInt32()
|
|
||||||
|
|
||||||
// Version (always 0)
|
/*
|
||||||
if _, err := br.ReadByte(); err != nil {
|
number of indicates
|
||||||
log.Fatal("Error reading Version record")
|
(https://d2mods.info/forum/viewtopic.php?p=202077#p202077)
|
||||||
|
Indices ...
|
||||||
|
An array of WORD. Each entry is an index into the hash table.
|
||||||
|
The actual string key index in the .bin file is an index into this table.
|
||||||
|
So to get a string from a key index ...
|
||||||
|
*/
|
||||||
|
numberOfElements, err := br.ReadUInt16()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading number of elements: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
br.GetUInt32() // StringOffset
|
hashTableSize, err := br.ReadUInt32()
|
||||||
br.GetUInt32() // When the number of times you have missed a match with a hash key equals this value, you give up because it is not there.
|
if err != nil {
|
||||||
br.GetUInt32() // FileSize
|
return nil, fmt.Errorf("reading hash table size: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
_, err = br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading version: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = br.ReadUInt32() // StringOffset
|
||||||
|
|
||||||
|
// When the number of times you have missed a match with a
|
||||||
|
// hash key equals this value, you give up because it is not there.
|
||||||
|
_, _ = br.ReadUInt32()
|
||||||
|
|
||||||
|
_, _ = br.ReadUInt32() // FileSize
|
||||||
|
|
||||||
elementIndex := make([]uint16, numberOfElements)
|
elementIndex := make([]uint16, numberOfElements)
|
||||||
for i := 0; i < int(numberOfElements); i++ {
|
for i := 0; i < int(numberOfElements); i++ {
|
||||||
elementIndex[i] = br.GetUInt16()
|
elementIndex[i], err = br.ReadUInt16()
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading element index %d: %v", i, err)
|
||||||
hashEntries := make([]textDictionaryHashEntry, hashTableSize)
|
|
||||||
for i := 0; i < int(hashTableSize); i++ {
|
|
||||||
hashEntries[i] = textDictionaryHashEntry{
|
|
||||||
br.GetByte() == 1,
|
|
||||||
br.GetUInt16(),
|
|
||||||
br.GetUInt32(),
|
|
||||||
br.GetUInt32(),
|
|
||||||
br.GetUInt32(),
|
|
||||||
br.GetUInt16(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx, hashEntry := range hashEntries {
|
hashEntries := make([]*textDictionaryHashEntry, hashTableSize)
|
||||||
if !hashEntry.IsActive {
|
|
||||||
continue
|
err = lookupTable.loadHashEntries(hashEntries, br)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("loading has entries: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
br.SetPosition(uint64(hashEntry.NameString))
|
return lookupTable, nil
|
||||||
nameVal := br.ReadBytes(int(hashEntry.NameLength - 1))
|
}
|
||||||
value := string(nameVal)
|
|
||||||
|
// Marshal encodes text dictionary back into byte slice
|
||||||
br.SetPosition(uint64(hashEntry.IndexString))
|
func (td *TextDictionary) Marshal() []byte {
|
||||||
|
sw := d2datautils.CreateStreamWriter()
|
||||||
key := ""
|
|
||||||
|
// https://github.com/OpenDiablo2/OpenDiablo2/issues/1043
|
||||||
for {
|
sw.PushBytes(0, 0)
|
||||||
b := br.GetByte()
|
|
||||||
if b == 0 {
|
sw.PushUint16(0)
|
||||||
break
|
|
||||||
}
|
keys := make([]string, 0)
|
||||||
|
for key := range *td {
|
||||||
key += string(b)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
if key == "x" || key == "X" {
|
sw.PushUint32(uint32(len(keys)))
|
||||||
key = "#" + strconv.Itoa(idx)
|
|
||||||
}
|
// version (always 0)
|
||||||
|
sw.PushBytes(0)
|
||||||
_, exists := lookupTable[key]
|
|
||||||
if !exists {
|
// offset of start of data (unnecessary for our decoder)
|
||||||
lookupTable[key] = value
|
sw.PushUint32(0)
|
||||||
}
|
|
||||||
}
|
// Max retry count for a hash hit.
|
||||||
|
sw.PushUint32(0)
|
||||||
return lookupTable
|
|
||||||
|
// offset to end of data (noop)
|
||||||
|
sw.PushUint32(0)
|
||||||
|
|
||||||
|
// indicates (len = 0, so nothing here)
|
||||||
|
|
||||||
|
// nolint:gomnd // 17 comes from the size of one "data-header index"
|
||||||
|
// dataPos is a position, when we're placing data stream
|
||||||
|
dataPos := len(sw.GetBytes()) + 17*len(*td)
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
value := (*td)[key]
|
||||||
|
// non-zero if record is used (for us, every record is used ;-)
|
||||||
|
sw.PushBytes(1)
|
||||||
|
|
||||||
|
// generally unused;
|
||||||
|
// string key index (used in .bin)
|
||||||
|
sw.PushUint16(0)
|
||||||
|
|
||||||
|
// also unused in our decoder
|
||||||
|
// calculated hash of the string.
|
||||||
|
sw.PushUint32(0)
|
||||||
|
|
||||||
|
sw.PushUint32(uint32(dataPos))
|
||||||
|
|
||||||
|
if key[0] == '#' {
|
||||||
|
// 1 for X, and 1 for separator
|
||||||
|
dataPos += 2
|
||||||
|
} else {
|
||||||
|
dataPos += len(key) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.PushUint32(uint32(dataPos))
|
||||||
|
dataPos += len(value) + 1
|
||||||
|
|
||||||
|
sw.PushUint16(uint16(len(value) + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// data stream: put all data in appropriate order
|
||||||
|
for _, key := range keys {
|
||||||
|
value := (*td)[key]
|
||||||
|
|
||||||
|
if key[0] == '#' {
|
||||||
|
key = "x"
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.PushBytes([]byte(key)...)
|
||||||
|
|
||||||
|
// 0 as separator
|
||||||
|
sw.PushBytes(0)
|
||||||
|
|
||||||
|
sw.PushBytes([]byte(value)...)
|
||||||
|
|
||||||
|
// 0 as separator
|
||||||
|
sw.PushBytes(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sw.GetBytes()
|
||||||
}
|
}
|
||||||
|
62
d2common/d2fileformats/d2tbl/text_dictionary_test.go
Normal file
62
d2common/d2fileformats/d2tbl/text_dictionary_test.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package d2tbl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func exampleData() *TextDictionary {
|
||||||
|
result := &TextDictionary{
|
||||||
|
"abc": "def",
|
||||||
|
"someStr": "Some long string",
|
||||||
|
"teststring": "TeStxwsas123 long strin122*8:wq",
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTBL_Marshal(t *testing.T) {
|
||||||
|
tbl := exampleData()
|
||||||
|
data := tbl.Marshal()
|
||||||
|
|
||||||
|
newTbl, err := LoadTextDictionary(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range *tbl {
|
||||||
|
newValue, ok := newTbl[key]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("string %s wasn't encoded to table", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newValue != value {
|
||||||
|
t.Fatal("unexpected value set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTBL_MarshalNoNameString(t *testing.T) {
|
||||||
|
tbl := &TextDictionary{
|
||||||
|
"#0": "OKEY",
|
||||||
|
}
|
||||||
|
|
||||||
|
data := tbl.Marshal()
|
||||||
|
|
||||||
|
newTbl, err := LoadTextDictionary(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range *tbl {
|
||||||
|
newValue, ok := newTbl[key]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("string %s wasn't encoded to table", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newValue != value {
|
||||||
|
t.Fatal("unexpected value set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,13 +9,14 @@ import (
|
|||||||
|
|
||||||
// Animation is an animation
|
// Animation is an animation
|
||||||
type Animation interface {
|
type Animation interface {
|
||||||
BindRenderer(Renderer) error
|
BindRenderer(Renderer)
|
||||||
Clone() Animation
|
Clone() Animation
|
||||||
SetSubLoop(startFrame, EndFrame int)
|
SetSubLoop(startFrame, EndFrame int)
|
||||||
Advance(elapsed float64) error
|
Advance(elapsed float64) error
|
||||||
Render(target Surface) error
|
GetCurrentFrameSurface() Surface
|
||||||
RenderFromOrigin(target Surface, shadow bool) error
|
Render(target Surface)
|
||||||
RenderSection(sfc Surface, bound image.Rectangle) error
|
RenderFromOrigin(target Surface, shadow bool)
|
||||||
|
RenderSection(sfc Surface, bound image.Rectangle)
|
||||||
GetFrameSize(frameIndex int) (int, int, error)
|
GetFrameSize(frameIndex int) (int, int, error)
|
||||||
GetCurrentFrameSize() (int, int)
|
GetCurrentFrameSize() (int, int)
|
||||||
GetFrameBounds() (int, int)
|
GetFrameBounds() (int, int)
|
||||||
|
@ -8,10 +8,9 @@ type Archive interface {
|
|||||||
Path() string
|
Path() string
|
||||||
Contains(string) bool
|
Contains(string) bool
|
||||||
Size() uint32
|
Size() uint32
|
||||||
Close()
|
Close() error
|
||||||
FileExists(fileName string) bool
|
|
||||||
ReadFile(fileName string) ([]byte, error)
|
ReadFile(fileName string) ([]byte, error)
|
||||||
ReadFileStream(fileName string) (DataStream, error)
|
ReadFileStream(fileName string) (DataStream, error)
|
||||||
ReadTextFile(fileName string) (string, error)
|
ReadTextFile(fileName string) (string, error)
|
||||||
GetFileList() ([]string, error)
|
Listfile() ([]string, error)
|
||||||
}
|
}
|
||||||
|
@ -3,42 +3,47 @@ package d2interface
|
|||||||
// InputEventHandler is an event handler
|
// InputEventHandler is an event handler
|
||||||
type InputEventHandler interface{}
|
type InputEventHandler interface{}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOTE: The return values of the handler methods below are used to prevent
|
||||||
|
other bound handlers from being called (if the handler returns `true`).
|
||||||
|
*/
|
||||||
|
|
||||||
// KeyDownHandler represents a handler for a keyboard key pressed event
|
// KeyDownHandler represents a handler for a keyboard key pressed event
|
||||||
type KeyDownHandler interface {
|
type KeyDownHandler interface {
|
||||||
OnKeyDown(event KeyEvent) bool
|
OnKeyDown(event KeyEvent) (preventPropagation bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyRepeatHandler represents a handler for a keyboard key held-down event; between a pressed and released.
|
// KeyRepeatHandler represents a handler for a keyboard key held-down event; between a pressed and released.
|
||||||
type KeyRepeatHandler interface {
|
type KeyRepeatHandler interface {
|
||||||
OnKeyRepeat(event KeyEvent) bool
|
OnKeyRepeat(event KeyEvent) (preventPropagation bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyUpHandler represents a handler for a keyboard key release event
|
// KeyUpHandler represents a handler for a keyboard key release event
|
||||||
type KeyUpHandler interface {
|
type KeyUpHandler interface {
|
||||||
OnKeyUp(event KeyEvent) bool
|
OnKeyUp(event KeyEvent) (preventPropagation bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyCharsHandler represents a handler associated with a keyboard character pressed event
|
// KeyCharsHandler represents a handler associated with a keyboard character pressed event
|
||||||
type KeyCharsHandler interface {
|
type KeyCharsHandler interface {
|
||||||
OnKeyChars(event KeyCharsEvent) bool
|
OnKeyChars(event KeyCharsEvent) (preventPropagation bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MouseButtonDownHandler represents a handler for a mouse button pressed event
|
// MouseButtonDownHandler represents a handler for a mouse button pressed event
|
||||||
type MouseButtonDownHandler interface {
|
type MouseButtonDownHandler interface {
|
||||||
OnMouseButtonDown(event MouseEvent) bool
|
OnMouseButtonDown(event MouseEvent) (preventPropagation bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MouseButtonRepeatHandler represents a handler for a mouse button held-down event; between a pressed and released.
|
// MouseButtonRepeatHandler represents a handler for a mouse button held-down event; between a pressed and released.
|
||||||
type MouseButtonRepeatHandler interface {
|
type MouseButtonRepeatHandler interface {
|
||||||
OnMouseButtonRepeat(event MouseEvent) bool
|
OnMouseButtonRepeat(event MouseEvent) (preventPropagation bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MouseButtonUpHandler represents a handler for a mouse button release event
|
// MouseButtonUpHandler represents a handler for a mouse button release event
|
||||||
type MouseButtonUpHandler interface {
|
type MouseButtonUpHandler interface {
|
||||||
OnMouseButtonUp(event MouseEvent) bool
|
OnMouseButtonUp(event MouseEvent) (preventPropagation bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MouseMoveHandler represents a handler for a mouse button release event
|
// MouseMoveHandler represents a handler for a mouse button release event
|
||||||
type MouseMoveHandler interface {
|
type MouseMoveHandler interface {
|
||||||
OnMouseMove(event MouseMoveEvent) bool
|
OnMouseMove(event MouseMoveEvent) (preventPropagation bool)
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,11 @@ import (
|
|||||||
|
|
||||||
// Navigator is used for transitioning between game screens
|
// Navigator is used for transitioning between game screens
|
||||||
type Navigator interface {
|
type Navigator interface {
|
||||||
ToMainMenu()
|
ToMainMenu(errorMessageOptional ...string)
|
||||||
ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, connHost string)
|
ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, connHost string)
|
||||||
ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, connHost string)
|
ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, connHost string)
|
||||||
ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string)
|
ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string)
|
||||||
ToMapEngineTest(region int, level int)
|
ToMapEngineTest(region int, level int)
|
||||||
ToCredits()
|
ToCredits()
|
||||||
|
ToCinematics()
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
package d2interface
|
package d2interface
|
||||||
|
|
||||||
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
type renderCallback = func(Surface) error
|
||||||
|
|
||||||
|
type updateCallback = func() error
|
||||||
|
|
||||||
// Renderer interface defines the functionality of a renderer
|
// Renderer interface defines the functionality of a renderer
|
||||||
type Renderer interface {
|
type Renderer interface {
|
||||||
GetRendererName() string
|
GetRendererName() string
|
||||||
SetWindowIcon(fileName string)
|
SetWindowIcon(fileName string)
|
||||||
Run(f func(Surface) error, width, height int, title string) error
|
Run(r renderCallback, u updateCallback, width, height int, title string) error
|
||||||
IsDrawingSkipped() bool
|
IsDrawingSkipped() bool
|
||||||
CreateSurface(surface Surface) (Surface, error)
|
CreateSurface(surface Surface) (Surface, error)
|
||||||
NewSurface(width, height int, filter d2enum.Filter) (Surface, error)
|
NewSurface(width, height int) Surface
|
||||||
IsFullScreen() bool
|
IsFullScreen() bool
|
||||||
SetFullScreen(fullScreen bool)
|
SetFullScreen(fullScreen bool)
|
||||||
SetVSyncEnabled(vsync bool)
|
SetVSyncEnabled(vsync bool)
|
||||||
GetVSyncEnabled() bool
|
GetVSyncEnabled() bool
|
||||||
GetCursorPos() (int, int)
|
GetCursorPos() (int, int)
|
||||||
CurrentFPS() float64
|
CurrentFPS() float64
|
||||||
|
ShowPanicScreen(message string)
|
||||||
|
Print(target interface{}, str string) error
|
||||||
|
PrintAt(target interface{}, str string, x, y int)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
// Surface represents a renderable surface.
|
// Surface represents a renderable surface.
|
||||||
type Surface interface {
|
type Surface interface {
|
||||||
Renderer() Renderer
|
Renderer() Renderer
|
||||||
Clear(color color.Color) error
|
Clear(color color.Color)
|
||||||
DrawRect(width, height int, color color.Color)
|
DrawRect(width, height int, color color.Color)
|
||||||
DrawLine(x, y int, color color.Color)
|
DrawLine(x, y int, color color.Color)
|
||||||
DrawTextf(format string, params ...interface{})
|
DrawTextf(format string, params ...interface{})
|
||||||
@ -26,9 +26,9 @@ type Surface interface {
|
|||||||
PushScale(x, y float64)
|
PushScale(x, y float64)
|
||||||
PushBrightness(brightness float64)
|
PushBrightness(brightness float64)
|
||||||
PushSaturation(saturation float64)
|
PushSaturation(saturation float64)
|
||||||
Render(surface Surface) error
|
Render(surface Surface)
|
||||||
// Renders a section of the surface enclosed by bounds
|
// Renders a section of the surface enclosed by bounds
|
||||||
RenderSection(surface Surface, bound image.Rectangle) error
|
RenderSection(surface Surface, bound image.Rectangle)
|
||||||
ReplacePixels(pixels []byte) error
|
ReplacePixels(pixels []byte)
|
||||||
Screenshot() *image.RGBA
|
Screenshot() *image.RGBA
|
||||||
}
|
}
|
||||||
|
@ -13,17 +13,17 @@ type Terminal interface {
|
|||||||
OnKeyChars(event KeyCharsEvent) bool
|
OnKeyChars(event KeyCharsEvent) bool
|
||||||
Render(surface Surface) error
|
Render(surface Surface) error
|
||||||
Execute(command string) error
|
Execute(command string) error
|
||||||
OutputRaw(text string, category d2enum.TermCategory)
|
Rawf(category d2enum.TermCategory, format string, params ...interface{})
|
||||||
Outputf(format string, params ...interface{})
|
Printf(format string, params ...interface{})
|
||||||
OutputInfof(format string, params ...interface{})
|
Infof(format string, params ...interface{})
|
||||||
OutputWarningf(format string, params ...interface{})
|
Warningf(format string, params ...interface{})
|
||||||
OutputErrorf(format string, params ...interface{})
|
Errorf(format string, params ...interface{})
|
||||||
OutputClear()
|
Clear()
|
||||||
IsVisible() bool
|
Visible() bool
|
||||||
Hide()
|
Hide()
|
||||||
Show()
|
Show()
|
||||||
BindAction(name, description string, action interface{}) error
|
Bind(name, description string, arguments []string, fn func(args []string) error) error
|
||||||
UnbindAction(name string) error
|
Unbind(name ...string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// TerminalLogger is used tomake the Terminal write out
|
// TerminalLogger is used tomake the Terminal write out
|
||||||
|
@ -2,14 +2,13 @@ package asset
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Source is an abstraction for something that can load and list assets
|
// Source is an abstraction for something that can load and list assets
|
||||||
type Source interface {
|
type Source interface {
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
Type() types.SourceType
|
Open(name string) (io.ReadSeeker, error)
|
||||||
Open(name string) (Asset, error)
|
|
||||||
Path() string
|
Path() string
|
||||||
|
Exists(subPath string) bool
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,8 @@ func Ext2SourceType(ext string) SourceType {
|
|||||||
func CheckSourceType(path string) SourceType {
|
func CheckSourceType(path string) SourceType {
|
||||||
// on MacOS, the MPQ's from blizzard don't have file extensions
|
// on MacOS, the MPQ's from blizzard don't have file extensions
|
||||||
// so we just attempt to init the file as an mpq
|
// so we just attempt to init the file as an mpq
|
||||||
if _, err := d2mpq.Load(path); err == nil {
|
if mpq, err := d2mpq.New(path); err == nil {
|
||||||
|
_ = mpq.Close()
|
||||||
return AssetSourceMPQ
|
return AssetSourceMPQ
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
d2common/d2loader/filesystem/loader_provider.go
Normal file
12
d2common/d2loader/filesystem/loader_provider.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package filesystem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnAddSource is a shim method to allow loading of filesystem sources
|
||||||
|
func OnAddSource(path string) (asset.Source, error) {
|
||||||
|
return &Source{
|
||||||
|
Root: path,
|
||||||
|
}, nil
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package filesystem
|
package filesystem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
@ -22,21 +23,14 @@ func (s *Source) Type() types.SourceType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open opens a file with the given sub-path within the Root dir of the file system source
|
// Open opens a file with the given sub-path within the Root dir of the file system source
|
||||||
func (s *Source) Open(subPath string) (asset.Asset, error) {
|
func (s *Source) Open(subPath string) (io.ReadSeeker, error) {
|
||||||
file, err := os.Open(s.fullPath(subPath))
|
return os.Open(s.fullPath(subPath))
|
||||||
|
}
|
||||||
|
|
||||||
if err == nil {
|
// Exists returns true if the file exists
|
||||||
a := &Asset{
|
func (s *Source) Exists(subPath string) bool {
|
||||||
assetType: types.Ext2AssetType(filepath.Ext(subPath)),
|
_, err := os.Stat(s.fullPath(subPath))
|
||||||
source: s,
|
return os.IsExist(err)
|
||||||
path: subPath,
|
|
||||||
file: file,
|
|
||||||
}
|
|
||||||
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) fullPath(subPath string) string {
|
func (s *Source) fullPath(subPath string) string {
|
||||||
|
@ -2,29 +2,29 @@ package d2loader
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/mpq"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/filesystem"
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/filesystem"
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/mpq"
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultCacheBudget = 1024 * 1024 * 512
|
defaultCacheBudget = 1024 * 1024 * 512
|
||||||
defaultCacheEntryWeight = 1
|
|
||||||
errFmtFileNotFound = "file not found: %s"
|
errFmtFileNotFound = "file not found: %s"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultLanguage = "ENG"
|
logPrefix = "File Loader"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -33,64 +33,55 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewLoader creates a new loader
|
// NewLoader creates a new loader
|
||||||
func NewLoader(config *d2config.Configuration) *Loader {
|
func NewLoader(l d2util.LogLevel) (*Loader, error) {
|
||||||
loader := &Loader{
|
loader := &Loader{
|
||||||
config: config,
|
LoaderProviders: make(map[types.SourceType]func(path string) (asset.Source, error), 2),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loader.LoaderProviders[types.AssetSourceMPQ] = mpq.NewSource
|
||||||
|
loader.LoaderProviders[types.AssetSourceFileSystem] = filesystem.OnAddSource
|
||||||
|
|
||||||
loader.Cache = d2cache.CreateCache(defaultCacheBudget)
|
loader.Cache = d2cache.CreateCache(defaultCacheBudget)
|
||||||
|
loader.Logger = d2util.NewLogger()
|
||||||
|
|
||||||
loader.initFromConfig()
|
loader.Logger.SetPrefix(logPrefix)
|
||||||
|
loader.Logger.SetLevel(l)
|
||||||
|
|
||||||
return loader
|
return loader, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loader represents the manager that handles loading and caching assets with the asset Sources
|
// Loader represents the manager that handles loading and caching assets with the asset Sources
|
||||||
// that have been added
|
// that have been added
|
||||||
type Loader struct {
|
type Loader struct {
|
||||||
config *d2config.Configuration
|
language *string
|
||||||
|
charset *string
|
||||||
d2interface.Cache
|
d2interface.Cache
|
||||||
*d2util.Logger
|
*d2util.Logger
|
||||||
|
LoaderProviders map[types.SourceType]func(path string) (asset.Source, error)
|
||||||
Sources []asset.Source
|
Sources []asset.Source
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Loader) initFromConfig() {
|
// SetLanguage sets the language for loader
|
||||||
if l.config == nil {
|
func (l *Loader) SetLanguage(language *string) {
|
||||||
return
|
l.language = language
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mpqName := range l.config.MpqLoadOrder {
|
// SetCharset sets the charset for loader
|
||||||
cleanDir := filepath.Clean(l.config.MpqPath)
|
func (l *Loader) SetCharset(charset *string) {
|
||||||
srcPath := filepath.Join(cleanDir, mpqName)
|
l.charset = charset
|
||||||
|
|
||||||
_, err := l.AddSource(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load attempts to load an asset with the given sub-path. The sub-path is relative to the root
|
// Load attempts to load an asset with the given sub-path. The sub-path is relative to the root
|
||||||
// of each asset source root (regardless of the type of asset source)
|
// of each asset source root (regardless of the type of asset source)
|
||||||
func (l *Loader) Load(subPath string) (asset.Asset, error) {
|
func (l *Loader) Load(subPath string) (io.ReadSeeker, error) {
|
||||||
lang := defaultLanguage
|
|
||||||
|
|
||||||
if l.config != nil {
|
|
||||||
lang = l.config.Language
|
|
||||||
}
|
|
||||||
|
|
||||||
subPath = filepath.Clean(subPath)
|
subPath = filepath.Clean(subPath)
|
||||||
subPath = strings.ReplaceAll(subPath, fontToken, "latin")
|
|
||||||
subPath = strings.ReplaceAll(subPath, tableToken, lang)
|
|
||||||
|
|
||||||
// first, we check the cache for an existing entry
|
if l.language != nil {
|
||||||
if cached, found := l.Retrieve(subPath); found {
|
charset := l.charset
|
||||||
l.Debug(fmt.Sprintf("file `%s` exists in loader cache", subPath))
|
language := l.language
|
||||||
|
|
||||||
a := cached.(asset.Asset)
|
subPath = strings.ReplaceAll(subPath, fontToken, *charset)
|
||||||
_, err := a.Seek(0, 0)
|
subPath = strings.ReplaceAll(subPath, tableToken, *language)
|
||||||
|
|
||||||
return a, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it isn't in the cache, we check if each source can open the file
|
// if it isn't in the cache, we check if each source can open the file
|
||||||
@ -98,10 +89,16 @@ func (l *Loader) Load(subPath string) (asset.Asset, error) {
|
|||||||
source := l.Sources[idx]
|
source := l.Sources[idx]
|
||||||
|
|
||||||
// if the source can open the file, then we cache it and return it
|
// if the source can open the file, then we cache it and return it
|
||||||
if loadedAsset, err := source.Open(subPath); err == nil {
|
loadedAsset, err := source.Open(subPath)
|
||||||
err := l.Insert(subPath, loadedAsset, defaultCacheEntryWeight)
|
if err != nil {
|
||||||
return loadedAsset, err
|
l.Debug(fmt.Sprintf("Checked `%s`, file not found", source.Path()))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
srcBase, _ := filepath.Abs(source.Path())
|
||||||
|
l.Info(fmt.Sprintf("Loaded %s -> %s", srcBase, subPath))
|
||||||
|
|
||||||
|
return loadedAsset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf(errFmtFileNotFound, subPath)
|
return nil, fmt.Errorf(errFmtFileNotFound, subPath)
|
||||||
@ -111,52 +108,46 @@ func (l *Loader) Load(subPath string) (asset.Asset, error) {
|
|||||||
// or a file on the host filesystem. In the case that it is a file, the file extension is used
|
// or a file on the host filesystem. In the case that it is a file, the file extension is used
|
||||||
// to determine the type of asset source. In the case that the path points to a directory, a
|
// to determine the type of asset source. In the case that the path points to a directory, a
|
||||||
// FileSystemSource will be added.
|
// FileSystemSource will be added.
|
||||||
func (l *Loader) AddSource(path string) (asset.Source, error) {
|
func (l *Loader) AddSource(path string, sourceType types.SourceType) error {
|
||||||
if l.Sources == nil {
|
if l.Sources == nil {
|
||||||
l.Sources = make([]asset.Source, 0)
|
l.Sources = make([]asset.Source, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanPath := filepath.Clean(path)
|
cleanPath := filepath.Clean(path)
|
||||||
|
|
||||||
info, err := os.Lstat(cleanPath)
|
source, err := l.LoaderProviders[sourceType](cleanPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warning(err.Error())
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mode := info.Mode()
|
l.Infof("Adding source: '%s'", cleanPath)
|
||||||
|
|
||||||
sourceType := types.AssetSourceUnknown
|
|
||||||
|
|
||||||
if mode.IsDir() {
|
|
||||||
sourceType = types.AssetSourceFileSystem
|
|
||||||
}
|
|
||||||
|
|
||||||
if mode.IsRegular() {
|
|
||||||
sourceType = types.CheckSourceType(cleanPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch sourceType {
|
|
||||||
case types.AssetSourceMPQ:
|
|
||||||
source, err := mpq.NewSource(cleanPath)
|
|
||||||
if err == nil {
|
|
||||||
l.Debug(fmt.Sprintf("adding MPQ source `%s`", cleanPath))
|
|
||||||
l.Sources = append(l.Sources, source)
|
l.Sources = append(l.Sources, source)
|
||||||
|
|
||||||
return source, nil
|
return nil
|
||||||
}
|
}
|
||||||
case types.AssetSourceFileSystem:
|
|
||||||
source := &filesystem.Source{
|
// Exists checks if the given path exists in at least one source
|
||||||
Root: cleanPath,
|
func (l *Loader) Exists(subPath string) bool {
|
||||||
}
|
subPath = filepath.Clean(subPath)
|
||||||
|
|
||||||
l.Debug(fmt.Sprintf("adding filesystem source `%s`", cleanPath))
|
if l.language != nil {
|
||||||
l.Sources = append(l.Sources, source)
|
charset := l.charset
|
||||||
|
language := l.language
|
||||||
return source, nil
|
|
||||||
case types.AssetSourceUnknown:
|
subPath = strings.ReplaceAll(subPath, fontToken, *charset)
|
||||||
l.Warning(fmt.Sprintf("unknown asset source `%s`", cleanPath))
|
subPath = strings.ReplaceAll(subPath, tableToken, *language)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("unknown asset source `%s`", cleanPath)
|
// if it isn't in the cache, we check if each source can open the file
|
||||||
|
for idx := range l.Sources {
|
||||||
|
source := l.Sources[idx]
|
||||||
|
|
||||||
|
// if the source can open the file, then we cache it and return it
|
||||||
|
if source.Exists(subPath) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,13 @@ package d2loader
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -24,7 +27,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestLoader_NewLoader(t *testing.T) {
|
func TestLoader_NewLoader(t *testing.T) {
|
||||||
loader := NewLoader(nil)
|
loader, _ := NewLoader(d2util.LogLevelDefault)
|
||||||
|
|
||||||
if loader.Cache == nil {
|
if loader.Cache == nil {
|
||||||
t.Error("loader should not be nil")
|
t.Error("loader should not be nil")
|
||||||
@ -32,13 +35,13 @@ func TestLoader_NewLoader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoader_AddSource(t *testing.T) {
|
func TestLoader_AddSource(t *testing.T) {
|
||||||
loader := NewLoader(nil)
|
loader, _ := NewLoader(d2util.LogLevelDefault)
|
||||||
|
|
||||||
sourceA, errA := loader.AddSource(sourcePathA)
|
errA := loader.AddSource(sourcePathA, types.AssetSourceFileSystem)
|
||||||
sourceB, errB := loader.AddSource(sourcePathB)
|
errB := loader.AddSource(sourcePathB, types.AssetSourceFileSystem)
|
||||||
sourceC, errC := loader.AddSource(sourcePathC)
|
errC := loader.AddSource(sourcePathC, types.AssetSourceFileSystem)
|
||||||
sourceD, errD := loader.AddSource(sourcePathD)
|
errD := loader.AddSource(sourcePathD, types.AssetSourceFileSystem)
|
||||||
sourceE, errE := loader.AddSource(badSourcePath)
|
errE := loader.AddSource(badSourcePath, types.AssetSourceMPQ)
|
||||||
|
|
||||||
if errA != nil {
|
if errA != nil {
|
||||||
t.Error(errA)
|
t.Error(errA)
|
||||||
@ -59,51 +62,32 @@ func TestLoader_AddSource(t *testing.T) {
|
|||||||
if errE == nil {
|
if errE == nil {
|
||||||
t.Error("expecting error on bad file path")
|
t.Error("expecting error on bad file path")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sourceA.String() != sourcePathA {
|
|
||||||
t.Error("source path not the same as what we added")
|
|
||||||
}
|
|
||||||
|
|
||||||
if sourceB.String() != sourcePathB {
|
|
||||||
t.Error("source path not the same as what we added")
|
|
||||||
}
|
|
||||||
|
|
||||||
if sourceC.String() != sourcePathC {
|
|
||||||
t.Error("source path not the same as what we added")
|
|
||||||
}
|
|
||||||
|
|
||||||
if sourceD.String() != sourcePathD {
|
|
||||||
t.Error("source path not the same as what we added")
|
|
||||||
}
|
|
||||||
|
|
||||||
if sourceE != nil {
|
|
||||||
t.Error("source for bad path should be nil")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:gocyclo // this is just a test, not a big deal if we ignore linter here
|
// nolint:gocyclo // this is just a test, not a big deal if we ignore linter here
|
||||||
func TestLoader_Load(t *testing.T) {
|
func TestLoader_Load(t *testing.T) {
|
||||||
loader := NewLoader(nil)
|
loader, _ := NewLoader(d2util.LogLevelDefault)
|
||||||
|
|
||||||
_, err := loader.AddSource(sourcePathB) // we expect files common to any source to come from here
|
// we expect files common to any source to come from here
|
||||||
|
err := loader.AddSource(sourcePathB, types.AssetSourceFileSystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = loader.AddSource(sourcePathD)
|
err = loader.AddSource(sourcePathD, types.AssetSourceMPQ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = loader.AddSource(sourcePathA)
|
err = loader.AddSource(sourcePathA, types.AssetSourceFileSystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = loader.AddSource(sourcePathC)
|
err = loader.AddSource(sourcePathC, types.AssetSourceFileSystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
@ -121,8 +105,6 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
|
|
||||||
if entryCommon == nil || errCommon != nil {
|
if entryCommon == nil || errCommon != nil {
|
||||||
t.Error("common entry should exist")
|
t.Error("common entry should exist")
|
||||||
} else if entryCommon.Source() != loader.Sources[0] {
|
|
||||||
t.Error("common entry should come from the first loader source")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if errA != nil || errB != nil || errC != nil || errD != nil {
|
if errA != nil || errB != nil || errC != nil || errD != nil {
|
||||||
@ -142,7 +124,7 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
buffer := make([]byte, 1)
|
buffer := make([]byte, 1)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
entry asset.Asset
|
entry io.ReadSeeker
|
||||||
data string
|
data string
|
||||||
}{
|
}{
|
||||||
{entryCommon, "b"}, // sourcePathB is loaded first, we expect a "b"
|
{entryCommon, "b"}, // sourcePathB is loaded first, we expect a "b"
|
||||||
@ -169,8 +151,8 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
got := string(result[0])
|
got := string(result[0])
|
||||||
|
|
||||||
if got != expected {
|
if got != expected {
|
||||||
fmtStr := "unexpected data in file %s, loaded from source `%s`: expected `%s`, got `%s`"
|
fmtStr := "unexpected data in file, expected %q, got %q"
|
||||||
msg := fmt.Sprintf(fmtStr, entry.Path(), entry.Source(), expected, got)
|
msg := fmt.Sprintf(fmtStr, expected, got)
|
||||||
t.Error(msg)
|
t.Error(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user