Compare commits

...

633 Commits

Author SHA1 Message Date
Tim Sarbin 7f92c571bf
Update README.md 2021-10-21 09:18:36 -04:00
Tim Sarbin 0b4d5b0ccd
Update README.md 2021-10-21 09:09:01 -04:00
gravestench a688d660a0
Merge pull request #1124 from gucio321/golangci-lint-update
all: fix lint errors
2021-05-14 15:26:03 -07:00
gucio321 c938b745a2 all: fix lint errors 2021-05-12 08:30:28 +02:00
Tim Sarbin c5a8a8601a
Merge pull request #1122 from gucio321/panic-fix
fix panic while loading wrong version of game.
2021-05-10 15:00:09 -04:00
gucio321 146f5206cf
item_types_loader.txt: fix typo 2021-05-10 14:36:53 +02:00
Tim Sarbin 7791bb6cf1
Merge pull request #1120 from OpenDiablo2/dependabot/github_actions/toshimaru/auto-author-assign-v1.3.0
Bump toshimaru/auto-author-assign from v1.2.0 to v1.3.0
2021-05-10 08:33:44 -04:00
Tim Sarbin c73a958fe0
Merge pull request #1121 from gucio321/revert-1104-labeler
Revert "workflow: automatic labeling Pull Requests"
2021-05-10 08:32:25 -04:00
gucio321 966c86a6b5 itemTypeLoader: wrap panic, which occures, when a wrong version of patch_d2 is there 2021-04-30 17:55:52 +02:00
gucio321 79a0a6eda9
Revert "workflow: automatic labeling Pull Requests" 2021-04-26 08:43:04 +02:00
dependabot[bot] 064839dc91
Bump toshimaru/auto-author-assign from v1.2.0 to v1.3.0
Bumps [toshimaru/auto-author-assign](https://github.com/toshimaru/auto-author-assign) from v1.2.0 to v1.3.0.
- [Release notes](https://github.com/toshimaru/auto-author-assign/releases)
- [Commits](https://github.com/toshimaru/auto-author-assign/compare/v1.2.0...ac4eff2594b0c5d5b87327c2f6eb2c6c370c272e)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-26 06:30:07 +00:00
Tim Sarbin eb470533c4
Merge pull request #1104 from gucio321/labeler
workflow: automatic labeling Pull Requests
2021-04-19 06:32:54 -04:00
M. Sz ce217c6b93 workflow: automatic labeling Pull Requests 2021-04-10 12:02:33 +02:00
gravestench 2f4663c680
Merge pull request #1118 from gucio321/d2ds1-fixes
d2ds1: fixed bug, when the file was encoded incorrectly
2021-04-07 10:20:13 -07:00
gucio321 e48bc48aab d2ds1: fixed bug, when (if version was >= v16), the file was encoded incorrectly 2021-04-07 19:13:44 +02:00
gravestench 7ce4583fe1
Merge pull request #1117 from gucio321/d2ds1-fixes
d2ds1: bugfixes
2021-04-07 09:42:47 -07:00
gucio321 ce7586fa54 d2ds1: fixed bug, when cullNilLayers worked incorrect 2021-04-07 17:24:00 +02:00
gucio321 49e8e42596 d2ds1: LayerGroup.String method 2021-04-07 11:07:43 +02:00
gucio321 82295fddb0 d2ds1: getLayersGroup is public 2021-04-07 10:57:20 +02:00
gucio321 54645b138f d2ds1: getMaxGroupLen is public 2021-04-07 10:55:15 +02:00
M. Sz 53de5c17ad d2ds1: typofix - Substitutionlayergroup -> SubstitutionLayerGroup 2021-04-07 10:48:04 +02:00
gravestench f880c69399
Merge pull request #1116 from gravestench/ds1_export_layergrouptype
export d2ds1.LayourGroupType
2021-03-31 12:19:20 -07:00
gravestench 8cb96f24b4 export d2ds1.LayourGroupType 2021-03-31 12:14:00 -07:00
gravestench 3d68e9b838
Merge pull request #1115 from gucio321/hotfix2
hotfix: exported ds1.substitutionGroups
2021-03-31 12:07:18 -07:00
gravestench 87c121707d add comments to d2ds1/layer.go 2021-03-31 21:04:22 +02:00
M. Sz 198a28505e ds1: push/insert now sets new layer's size to current layer size 2021-03-31 13:52:31 +02:00
M. Sz 2cfacf8ee5 ds1: exported layer type 2021-03-31 13:50:40 +02:00
M. Sz a7f34788e8 hotfix: exported ds1.substitutionGroups 2021-03-31 11:55:39 +02:00
gravestench 59114a72ed
Merge pull request #1066 from gravestench/d2ds1_refactor
Refactoring d2ds1
2021-03-31 01:54:07 -07:00
gravestench 787d7f531e add layer schema test 2021-03-31 00:28:01 -07:00
gravestench 9fe2069040 adding size and version tests for ds1 2021-03-31 00:15:41 -07:00
gravestench 51cc23abef revert windows path string in config defaults 2021-03-31 00:10:21 -07:00
gravestench f832c2a809 add missing unit tests for ds1 layers 2021-03-30 23:57:26 -07:00
gravestench 738e1ba7d6 change ds1.unknown1 to uint64 2021-03-30 11:39:37 -07:00
gravestench 5074ed24ed export ds1.SubstitutionType 2021-03-30 11:22:34 -07:00
gravestench b155f970df revert windows path string 2021-03-30 11:13:45 -07:00
Tim Sarbin 9561086965
Merge pull request #1112 from shivanraptor/patch-1
Added macOS instructions
2021-03-30 14:09:38 -04:00
Tim Sarbin ad2cb34c26
Merge pull request #1106 from drpaneas/faq
Add FAQ
2021-03-30 14:07:53 -04:00
gravestench 91209fd540 add more ds1version methods 2021-03-30 11:04:25 -07:00
gravestench 1a3fc68d03 use error-wrapping verb in fmt.Errorf 2021-03-30 10:39:35 -07:00
gravestench 66435a264e adding a ds1 version setter/getter 2021-03-30 10:35:56 -07:00
gravestench 1a3f3ed91a
Merge pull request #1114 from gucio321/revert-1113-revert-1105-fix_ci
Revert "Revert "Fix golang-ci gocritic error""
2021-03-30 10:05:01 -07:00
gucio321 72fa20eff8
Revert "Revert "Fix golang-ci gocritic error"" 2021-03-30 18:31:21 +02:00
gravestench 1c0b9a1c9c
Merge pull request #1113 from gucio321/revert-1105-fix_ci
this broke opening mpq's
2021-03-30 09:15:52 -07:00
gucio321 67eb1cc103
Revert "Fix golang-ci gocritic error" 2021-03-30 18:04:56 +02:00
Raptor K 1b8971a52e
Added macOS instructions 2021-03-30 16:23:38 +08:00
Panagiotis Georgiadis e5f886e1e1
Add HellSpawner[] to statuspage 2021-03-29 20:05:14 +02:00
Panagiotis Georgiadis 3a8b8e15a2
Fix typo and remove Akara credit 2021-03-29 19:44:36 +02:00
gravestench c8ef8cf2a4
Merge pull request #1110 from gucio321/d2font
dt1: added New method and made unknownX bytes as an constant variable
2021-03-29 02:45:34 -07:00
M. Sz d946094162 dt1: lintfix 2021-03-29 11:41:31 +02:00
gravestench a3cb024b49 Merge branch 'master' of http://github.com/OpenDiablo2/OpenDiablo2 into merge_ds1_upstream 2021-03-29 01:44:52 -07:00
gravestench 8e3620ff45 remove unnecessary interface 2021-03-29 01:31:47 -07:00
M. Sz db89a09f9d dt1: added New method and mad unknownX bytes as an constant variable 2021-03-29 09:40:25 +02:00
Tim Sarbin 435fd1a61b
Merge pull request #1107 from fzwoch/go_version
Bump go.mod Go version to 1.16
2021-03-28 12:15:47 -04:00
Tim Sarbin cfcd303252
Merge pull request #1109 from gucio321/d2font
d2dat: add New method for DATPalette
2021-03-28 12:02:01 -04:00
M. Sz 3fd19f727e d2dat: lintfix 2021-03-28 17:56:53 +02:00
M. Sz 5962d2e832 d2dat: add New method for DATPalette 2021-03-28 17:56:53 +02:00
Tim Sarbin 5efe96ebe3
Merge pull request #1108 from gucio321/d2font
d2font: fixed creating file bug
2021-03-28 10:25:43 -04:00
M. Sz d72b3e3345 d2font: fixed bug, when was unable to create new file doing just &Font{}.Marshal() 2021-03-28 16:21:06 +02:00
Florian Zwoch 01b207b402 Bump go.mod Go version to 1.16 2021-03-28 14:14:28 +02:00
Panagiotis Georgiadis 2801ae5410
Add FAQ 2021-03-28 03:29:04 +02:00
gucio321 41c1d8e874
bugfix: "insert" bug (#12)
* remove magic 'et' file

* ds1 refactor: test for InsertFloors; fixed inserting bug

* ds1: global method for getting layers group; fixed Delete.. bug (group var in delete meghod wasn't pointer

* ds1: lintfix

* ds1: remove ds1.ds1Layers.Orientation

* ds1: lintfix

* ds1: addet getMaxGroupLen method

* ds1: insert now returns if after inserting group will be greater than max number of ...

* ds1: add common test for ds1Layers.Insert...

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-03-27 13:09:53 -07:00
Tim Sarbin 73bf269d48
Merge pull request #1100 from drpaneas/golangci
Update golangci
2021-03-26 18:22:24 -04:00
Tim Sarbin 26c6dde74d
Merge pull request #1102 from drpaneas/readme
Redesign the readme
2021-03-26 18:21:24 -04:00
gravestench 5b1debd3f0
Merge pull request #1105 from drpaneas/fix_ci
Fix golang-ci gocritic error
2021-03-26 11:03:14 -07:00
Panagiotis Georgiadis 4558da0136
Fix golang-ci gocritic error
It should fix this error:
`nilValReturn: returned expr is always nil; replace err with nil`
2021-03-26 18:52:53 +01:00
Panagiotis Georgiadis ded81770df
Link readme to individual docs 2021-03-26 18:28:52 +01:00
Panagiotis Georgiadis 578fa35446
Add user installation information 2021-03-26 18:25:35 +01:00
Panagiotis Georgiadis 6bcde98d6e
Reposition the order - emphasis 2021-03-25 12:17:48 +01:00
Panagiotis Georgiadis 07381b347c
Redesign the readme 2021-03-25 04:37:53 +01:00
Panagiotis Georgiadis 49ae2308f1
Add Go Report Card 2021-03-25 02:31:45 +01:00
Panagiotis Georgiadis db99c66d25
Comply with gofmt -s 2021-03-25 02:29:16 +01:00
Panagiotis Georgiadis e9031d97bd
Disable fieldalignment 2021-03-25 00:51:10 +01:00
gravestench 468f5682ae Merge branch 'd2ds1_refactor' into d2ds1_refactor_wip 2021-03-24 11:55:34 -07:00
gucio321 6e7e7b9d3f
Ds1 refactor (#11)
* Refactoring d2ds1

* Adding setters/getters so that state management can be maintained
internally when the ds1 struct is altered
* Adding unit tests for DS1

* unit tests for ds1 (#4)

* ds1 refactor: added test fore some methods; put tests in right order

* ds1 refactor: unit tests for all methods

* ds1 refactor: fixed build errors

* ds1 refactor: lintfix

* ds1 refactor: fixed bug with SetWidth, SetHeight methods

* ds1 refactor: rename tile_record.go -> tile.go

* ds1 refactor: unit test for SetTiles

Co-authored-by: M. Sz <mszeptuch@protonmail.com>

* renamed some files in d2ds1

* d2ds1.FloorShadow is now private

* renamed another file

* DS1.Tile() now calls update if dirty

* Ds1 refactor: some test improvement (#5)

* ds1 refactor: floor_shadow.go: methods Encode, Decode an Hidden are methods of floorShadow

* ds1 refactor: test checks, if our methods sets all fields correctly

* ds1 refactor: minor bugfixes

* i don't remember what's this, but i commit it ;-)

* ds1 refactor: reverted some pushed by mistake things

Co-authored-by: M. Sz <mszeptuch@protonmail.com>

* Ds1 refactor: test bugs + descriptive errors + SetNumberOfWall/FloorLayers (#6)

* ds1 refactor:
- removed DS1.layerStreamTypes field
- written unit test for setupStreamLayerTypes method
- added more descriptive error messages for LoadDS1 (and submethods)

* ds1 refactor: added some missing error messages

* ds1 refactor: fixed test bugs

* ds1 refactor: removed unnecessary c1. and c2. comments in ds1_test errors

* ds1 refactor: removed fmt from ds1_test

* ds1 refactor: fixed bug with SetTiles test + lintfix

* ds1 refactor: SetNumberOfWalls

* ds1 refactor: SetTile(s) now changes walls/floors length if neccesary

* ds1 refactor: removed unnecessary debugging fmt

* ds1 refactor: added substitution layer and object with paths to example data

Co-authored-by: M. Sz <mszeptuch@protonmail.com>

* Ds1 refactor: removed npcIndexes field+fixed SetNumberOfWalls bug (#7)

* ds1 refactor: removed npcIndexes field
it was unnecessary, because described a number of objects with paths to use in encoder, but we can calculate manually

* ds1 refactor: fixed set number of (layers) bug

* ds1 refactor: SetNumberOf...Layers now returns error if incorrect number given

* ds1 refactor: lintfix

* ds1 refactor: rename: setupStreamLayerTypes -> GetStreamLayerTypes

Co-authored-by: M. Sz <mszeptuch@protonmail.com>

* WIP

* Ds1 refactor - tests (#10)

* ds1 refactor test: example data

* added loader check

* ds1 refactor: fixed bug, with loading substitutions; added descriptive error message in engine.go:118 and changed Logger.Error with Logger.Fatal

* ds1 refactor: fixed loading bug

* ds1 refactor: fixed bug when walls wasn't rendered (now we can see only walls :-)

Co-authored-by: M. Sz <mszeptuch@protonmail.com>

* ds1: floor rendering bugfix

* ds1: implemented encode layers method

* ds1: implemented encoder

* ds1: update of ds1_test

Co-authored-by: gravestench <dknuth0101@gmail.com>
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-03-24 10:28:02 -07:00
gucio321 6f41387e30 Ds1 refactor - tests (#10)
* ds1 refactor test: example data

* added loader check

* ds1 refactor: fixed bug, with loading substitutions; added descriptive error message in engine.go:118 and changed Logger.Error with Logger.Fatal

* ds1 refactor: fixed loading bug

* ds1 refactor: fixed bug when walls wasn't rendered (now we can see only walls :-)

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-03-24 10:10:36 -07:00
gravestench 5dfca5e9f3 WIP 2021-03-24 10:10:36 -07:00
gucio321 a33117fb56 Ds1 refactor: removed npcIndexes field+fixed SetNumberOfWalls bug (#7)
* ds1 refactor: removed npcIndexes field
it was unnecessary, because described a number of objects with paths to use in encoder, but we can calculate manually

* ds1 refactor: fixed set number of (layers) bug

* ds1 refactor: SetNumberOf...Layers now returns error if incorrect number given

* ds1 refactor: lintfix

* ds1 refactor: rename: setupStreamLayerTypes -> GetStreamLayerTypes

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-03-24 10:10:36 -07:00
gucio321 5706a1c19a Ds1 refactor: test bugs + descriptive errors + SetNumberOfWall/FloorLayers (#6)
* ds1 refactor:
- removed DS1.layerStreamTypes field
- written unit test for setupStreamLayerTypes method
- added more descriptive error messages for LoadDS1 (and submethods)

* ds1 refactor: added some missing error messages

* ds1 refactor: fixed test bugs

* ds1 refactor: removed unnecessary c1. and c2. comments in ds1_test errors

* ds1 refactor: removed fmt from ds1_test

* ds1 refactor: fixed bug with SetTiles test + lintfix

* ds1 refactor: SetNumberOfWalls

* ds1 refactor: SetTile(s) now changes walls/floors length if neccesary

* ds1 refactor: removed unnecessary debugging fmt

* ds1 refactor: added substitution layer and object with paths to example data

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-03-24 10:10:36 -07:00
gucio321 ca47018e8a Ds1 refactor: some test improvement (#5)
* ds1 refactor: floor_shadow.go: methods Encode, Decode an Hidden are methods of floorShadow

* ds1 refactor: test checks, if our methods sets all fields correctly

* ds1 refactor: minor bugfixes

* i don't remember what's this, but i commit it ;-)

* ds1 refactor: reverted some pushed by mistake things

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-03-24 10:10:36 -07:00
gravestench 4bc4fa0221 DS1.Tile() now calls update if dirty 2021-03-24 10:10:36 -07:00
gravestench 2353ef2e70 renamed another file 2021-03-24 10:10:36 -07:00
gravestench 87d0803a4f d2ds1.FloorShadow is now private 2021-03-24 10:10:36 -07:00
gravestench ee758b785c renamed some files in d2ds1 2021-03-24 10:10:36 -07:00
gucio321 194c1e467c unit tests for ds1 (#4)
* ds1 refactor: added test fore some methods; put tests in right order

* ds1 refactor: unit tests for all methods

* ds1 refactor: fixed build errors

* ds1 refactor: lintfix

* ds1 refactor: fixed bug with SetWidth, SetHeight methods

* ds1 refactor: rename tile_record.go -> tile.go

* ds1 refactor: unit test for SetTiles

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-03-24 10:10:36 -07:00
gravestench 5e0e51d5e2 Refactoring d2ds1
* Adding setters/getters so that state management can be maintained
internally when the ds1 struct is altered
* Adding unit tests for DS1
2021-03-24 10:10:36 -07:00
Panagiotis Georgiadis f7d974f2a2
Update go modules 2021-03-24 16:02:10 +01:00
Panagiotis Georgiadis 4af41209e7
Enable fieldalignment 2021-03-24 16:01:54 +01:00
Panagiotis Georgiadis 841a3a34bc
Update golangci
* Replace 'maligned' with 'fieldalignment' [1]
* Remove 'interfacer' [2]
* Enable linting against go test files as well
* Disable 'funlen' linter for go table-driven tests

[1] https://github.com/golangci/golangci-lint/pull/1765
[2] https://github.com/golangci/golangci-lint/issues/541
2021-03-24 15:01:00 +01:00
Tim Sarbin ecab467a0f
Merge pull request #1099 from drpaneas/ci
Auto Author Assign
2021-03-24 09:58:47 -04:00
Panagiotis Georgiadis 00a55ad4d4
Auto Author Assign
In most cases, pull request author should be assigned as assignee
of the pull request. This action automatically assigns PR author
as an assignee.
2021-03-24 14:10:17 +01:00
Tim Sarbin 236a091997
Merge pull request #1098 from willroberts/windows-path-support
Use filepath instead of path for Windows support
2021-03-24 08:55:20 -04:00
Tim Sarbin 6c00b3241f
Merge pull request #1097 from drpaneas/fix_config
Fix config.json issues
2021-03-24 08:53:10 -04:00
Tim Sarbin 421d4252d9
Merge pull request #1094 from willroberts/bitmuncher-tests
Adds 100% test coverage for BitMuncher
2021-03-24 08:52:22 -04:00
Tim Sarbin d43ef2c04e
Merge pull request #1093 from anvil-of-fury/anvil-of-fury/cache-tests
Anvil of fury/cache tests
2021-03-24 08:51:44 -04:00
Will Roberts 804f4148d3 Satisfies golangci-lint 2021-03-23 23:40:24 -04:00
Will Roberts 34bc9cc697 Satisfies golangci-lint 2021-03-23 23:37:11 -04:00
Will Roberts d1c1e8bd26 No forward slashes in filepath.Join to satisfy golangci-lint 2021-03-23 23:30:00 -04:00
Will Roberts b18a70cef9 Updates more instances of path->filepath 2021-03-23 23:18:19 -04:00
Will Roberts 222b916002 Use filepath instead of path for Windows support 2021-03-23 23:08:32 -04:00
Panagiotis Georgiadis f8881ed832
Use the same MpqLoadOrder cross-platform
Fixes #1095
2021-03-24 04:02:47 +01:00
Panagiotis Georgiadis 268e65b309
Initialise config.path in case of MPQ error
Fixes #1096
2021-03-24 03:58:49 +01:00
Will Roberts 3b26d825d2 Replaces comparisons with assertions 2021-03-23 22:38:47 -04:00
Will Roberts bfa9c34ecc Adds 100% test coverage for BitMuncher 2021-03-23 22:30:57 -04:00
anvil-of-fury 4835cefef8 removing TODO comment since it broke linting during build 2021-03-21 02:44:37 +00:00
anvil-of-fury ddd72a3aed for now passing test when weight exceeds budget until someone confirms 2021-03-21 02:31:22 +00:00
anvil-of-fury 05c8b5b294 golang lint reformatting applied 2021-03-21 02:27:58 +00:00
evbo a900c6245d added Cache unit tests for non-trivial functions 2021-03-21 01:31:55 +00:00
gravestench 536233ffee
Merge pull request #1091 from gucio321/data-encoder-dat
d2pl2: added RGBA and SetRGBA methods
2021-03-11 22:24:29 -08:00
gravestench 98e13a706a
Merge pull request #1090 from gucio321/hotfix4
d2enum: String and full name methods
2021-03-11 22:23:58 -08:00
gravestench ed28ff1176
Merge pull request #1089 from gucio321/hotfix3
enum: added String method for d2enum.TileType
2021-03-11 22:23:00 -08:00
gravestench 54745a215b
Merge pull request #1088 from gucio321/hotfix2
d2dat: fixed encoder bug (make(..., nonzero) and append later)
2021-03-11 22:22:41 -08:00
gravestench 384db6e75e
Merge pull request #1087 from gucio321/anim-data-encoder
hotfix: string table encoder bug
2021-03-11 22:21:42 -08:00
gravestench 9cb5f2ef6b
Merge pull request #1085 from gucio321/readme-update
Readme update: status + formatting
2021-03-11 22:18:37 -08:00
gravestench 97516d5da9
Merge pull request #1082 from gucio321/hotfix
bugfix: d2util.Logger.Fatal()
2021-03-11 22:17:51 -08:00
M. Sz ce692eb829 pl2: lintfix 2021-03-11 20:24:08 +01:00
M. Sz a7b8f82204 pl2: RGBA and SetRGBA methods for PL2Color24Bits 2021-03-11 11:20:15 +01:00
M. Sz fd9c806928 d2pl2: lintfix 2021-03-11 10:19:19 +01:00
M. Sz 0f08c722f5 d2pl2: added RGBA and SetRGBA methods (to implement HellSpawner/hswidget/hspalettegridwidget.PaletteColor) 2021-03-11 10:07:52 +01:00
M. Sz 352e78ffba d2enum: lintfix 2021-03-11 09:29:08 +01:00
M. Sz a662bbaeb4 d2enum: move compositeType.Name method int composite_type.go; lintfix 2021-03-11 09:22:50 +01:00
M. Sz 00bd2c52a0 d2enum: weapon class: Name method 2021-03-11 09:11:54 +01:00
M. Sz 36f356d0c3 d2enum: draw effect string 2021-03-11 08:35:14 +01:00
M. Sz e7ea9cacce d2enum: composite_type: removed Int method 2021-03-11 08:23:50 +01:00
M. Sz 39ccf33947 enum: added String method for d2enum.TileType 2021-03-08 15:25:07 +01:00
M. Sz 7b77011977 d2dat: fixed encoder bug (make(..., nonzero) and append later) 2021-03-07 19:31:49 +01:00
M. Sz 6167a7755b readme: march 2020 -> march 2021 2021-03-07 14:15:45 +01:00
M. Sz 4beb20cb68 readme: replaced Diablo2 with Diablo 2 2021-03-07 14:15:45 +01:00
M. Sz 641fa535eb readme: status date update 2021-03-07 14:15:45 +01:00
M. Sz 7ef41b6218 readme: formated with remark 2021-03-07 14:15:45 +01:00
M. Sz da3fe0ed09 string table: fixed bug, when (despite OpenDiablo2#1080) string tables wasn't encoded correctly 2021-03-05 13:12:53 +01:00
Tim Sarbin fbd675c076
Delete .travis.yml 2021-03-04 17:10:03 -05:00
Tim Sarbin 09763f4cd2
Delete pullRequest.yml 2021-03-04 17:08:25 -05:00
Tim Sarbin 5e29ea8219
Merge pull request #1086 from gucio321/anim-data-encoder
animation data: methods for edition animation data records count and animation entries
2021-03-04 16:52:14 -05:00
M. Sz fb7279f6b0 animation data: lintfix 2021-03-04 11:55:33 +01:00
M. Sz 98539befe3 animation data: Add/Delete entry methods 2021-03-04 11:52:08 +01:00
M. Sz 2e2d086d71 animation data: lintfix 2021-03-04 11:37:16 +01:00
M. Sz c603eaafbc animation data: methods for edition animation data records count 2021-03-04 11:35:42 +01:00
gucio321 5d78275bbc
Merge branch 'master' into hotfix 2021-03-03 20:17:34 +01:00
gravestench b72017dbd9
Merge pull request #1079 from gucio321/anim-data-encoder
animation data: methods for editing
2021-03-03 11:13:35 -08:00
gravestench 9a2d92198e
Merge branch 'master' into anim-data-encoder 2021-03-03 11:13:11 -08:00
gravestench cdfbeb6878
Merge pull request #1080 from gucio321/data-encoding2
bugfix string table encoding
2021-03-03 11:13:00 -08:00
gucio321 555de49b43
Merge branch 'master' into data-encoding2 2021-03-03 20:08:09 +01:00
gravestench ecb21ff4d4
Merge branch 'master' into anim-data-encoder 2021-03-03 11:07:27 -08:00
gravestench d82fe04f4b
Merge pull request #1084 from gucio321/hotfix3
build.sh: upgrade go 1.13.4 => 1.16
2021-03-03 11:06:55 -08:00
gravestench 09bea929be
Merge branch 'master' into hotfix3 2021-03-03 11:06:48 -08:00
gravestench d112c87a0c
Merge pull request #1083 from gucio321/building
circleci: upgrade go 1.15.8 -> 1.16
2021-03-03 11:06:37 -08:00
gravestench 5a92082011
Merge branch 'master' into building 2021-03-03 11:06:29 -08:00
gravestench ddec561f9a
Merge branch 'master' into anim-data-encoder 2021-03-03 11:06:08 -08:00
gucio321 18e467d5cb
Merge branch 'master' into hotfix3 2021-03-03 20:06:06 +01:00
gucio321 cffa34103d
Merge branch 'master' into hotfix 2021-03-03 20:04:54 +01:00
gucio321 fb8d97dba7
Merge branch 'master' into data-encoding2 2021-03-03 20:04:02 +01:00
gravestench f1b625e23e
Merge pull request #1081 from OpenDiablo2/dependabot/github_actions/golangci/golangci-lint-action-v2.5.1
build(deps): bump golangci/golangci-lint-action from v2.4.0 to v2.5.1
2021-03-03 11:03:00 -08:00
M. Sz e311d76c41 build.sh: upgrade go 1.13.4 => 1.16 2021-03-02 14:17:00 +01:00
M. Sz 547b8cd53b circleci: upgrade go 1.15.8 -> 1.16 2021-03-02 09:55:51 +01:00
M. Sz 12f9efded7 bugfix: d2util.Logger.Fatal() didn't return any value, when d2util.Logger.level < LogLevelFatal 2021-03-01 13:49:20 +01:00
dependabot[bot] db5efa15b0
build(deps): bump golangci/golangci-lint-action from v2.4.0 to v2.5.1
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from v2.4.0 to v2.5.1.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v2.4.0...d9f0e73c0497685d68af8c58280f49fcaf0545ff)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-01 06:37:35 +00:00
M. Sz b43da8f083 text dictionary: added no-named string check as separated test function 2021-02-28 20:35:35 +01:00
M. Sz 3b41f9e89b text dictionary: removed #2 string from test 2021-02-28 20:31:44 +01:00
M. Sz 4104d9d9ae text dictionary: added support for non-name (#??) strings 2021-02-28 16:29:05 +01:00
M. Sz 287fb2bf4d hotfix: lintfix 2021-02-28 14:14:47 +01:00
M. Sz de1c0ebe5d hotfix: string table: fixed bug, when sometimes encoded end decoded table wasn't the same 2021-02-28 12:25:08 +01:00
M. Sz 90f13724cf animation data: methods for editing 2021-02-27 17:08:24 +01:00
gravestench fa4276156c
Merge pull request #1078 from gucio321/anim-data-encoder
animation data engine + encoder
2021-02-26 12:09:08 -08:00
gucio321 ab14168f50
Merge branch 'master' into anim-data-encoder 2021-02-26 21:04:14 +01:00
gravestench a96170561f
Merge pull request #1075 from gucio321/data-encoding2
general refactor of text dictionary encoder
2021-02-26 11:58:34 -08:00
gucio321 6d88c9eacc
Merge branch 'master' into data-encoding2 2021-02-26 20:56:06 +01:00
gravestench 23e93886d8
Merge pull request #1076 from gucio321/data-encoder-dat
pl2 encoder + test
2021-02-26 11:27:03 -08:00
gravestench 1ffafcb769
Merge branch 'master' into data-encoder-dat 2021-02-26 11:26:38 -08:00
gravestench 50f309dee1
Merge pull request #1074 from gucio321/hotfix
minor cof refactor
2021-02-26 11:24:53 -08:00
gravestench bbd88cef78
Merge branch 'master' into hotfix 2021-02-26 11:23:59 -08:00
gravestench 203b4fd986
Merge pull request #1077 from gucio321/hotfix2
go.mod: dependencies upgrade + circleci: golang version update
2021-02-26 11:23:42 -08:00
M. Sz 62c58468e9 anim data: lintfix 2021-02-26 15:11:56 +01:00
M. Sz 25ea801cd4 Revert "circleci: golang version update"
This reverts commit 1125a7dd45.
2021-02-26 15:01:19 +01:00
M. Sz ad192254a3 Revert "go.mod: dependencies upgrade"
This reverts commit 3f2e5d34d7.
2021-02-26 15:00:56 +01:00
M. Sz e039c8ee70 anim data encoder + unit test for encoding 2021-02-26 14:57:55 +01:00
M. Sz 00e26fb862 animdata: the game now uses animation data manager from d2fileformats/d2animdata (instead of d2data/animation_data.go) 2021-02-26 11:56:49 +01:00
M. Sz 1125a7dd45 circleci: golang version update 2021-02-26 10:57:43 +01:00
M. Sz 3f2e5d34d7 go.mod: dependencies upgrade 2021-02-26 10:56:21 +01:00
M. Sz b5fa6e77eb cof: buildfix 2021-02-25 20:25:39 +01:00
M. Sz 15d30ffcce cof: rename SpeedToFPS -> FPS 2021-02-25 20:21:44 +01:00
M. Sz 976d78e595 pl2: lintfix 2021-02-25 19:26:01 +01:00
M. Sz d61d829b98 pl2 encoder + test 2021-02-25 19:19:56 +01:00
M. Sz c933e4b891 fixed build error & tbl: removed version check (because of error screen; we don't need to check version) 2021-02-25 14:11:19 +01:00
M. Sz 8d5cf7a26b tbl: replaced lolstring with teststring in tests 2021-02-25 13:09:38 +01:00
M. Sz 3b8cdffe15 tbl: lintfix 2021-02-25 12:08:02 +01:00
M. Sz e163c40107 tbl: completed error mesages 2021-02-25 12:06:37 +01:00
M. Sz 91b3290322 tbl: completed test 2021-02-25 12:03:00 +01:00
M. Sz 522f749cfc tbl: encoder 2021-02-25 11:58:32 +01:00
M. Sz 2859eae91c Revert "data encoding: tbl"
This reverts commit 5a0571763e.
2021-02-25 10:02:10 +01:00
M. Sz 78404ed56c cof: added Duration method 2021-02-25 09:33:47 +01:00
M. Sz e7c5efe8e4 cof: added SpeedToFPS method 2021-02-25 09:30:59 +01:00
M. Sz b4cd34e351 cof: splited long Marshal method to avoid nolint:funlen 2021-02-25 09:28:11 +01:00
gucio321 4e7ec3e843
Ds1 refactor: removed npcIndexes field+fixed SetNumberOfWalls bug (#7)
* ds1 refactor: removed npcIndexes field
it was unnecessary, because described a number of objects with paths to use in encoder, but we can calculate manually

* ds1 refactor: fixed set number of (layers) bug

* ds1 refactor: SetNumberOf...Layers now returns error if incorrect number given

* ds1 refactor: lintfix

* ds1 refactor: rename: setupStreamLayerTypes -> GetStreamLayerTypes

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-02-23 12:35:06 -08:00
gucio321 5e62b12bc4
Ds1 refactor: test bugs + descriptive errors + SetNumberOfWall/FloorLayers (#6)
* ds1 refactor:
- removed DS1.layerStreamTypes field
- written unit test for setupStreamLayerTypes method
- added more descriptive error messages for LoadDS1 (and submethods)

* ds1 refactor: added some missing error messages

* ds1 refactor: fixed test bugs

* ds1 refactor: removed unnecessary c1. and c2. comments in ds1_test errors

* ds1 refactor: removed fmt from ds1_test

* ds1 refactor: fixed bug with SetTiles test + lintfix

* ds1 refactor: SetNumberOfWalls

* ds1 refactor: SetTile(s) now changes walls/floors length if neccesary

* ds1 refactor: removed unnecessary debugging fmt

* ds1 refactor: added substitution layer and object with paths to example data

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-02-22 12:18:35 -08:00
gucio321 46d2bc6559
Ds1 refactor: some test improvement (#5)
* ds1 refactor: floor_shadow.go: methods Encode, Decode an Hidden are methods of floorShadow

* ds1 refactor: test checks, if our methods sets all fields correctly

* ds1 refactor: minor bugfixes

* i don't remember what's this, but i commit it ;-)

* ds1 refactor: reverted some pushed by mistake things

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-02-20 11:28:06 -08:00
gravestench bc4bd7235b
Merge branch 'master' into d2ds1_refactor 2021-02-20 11:23:40 -08:00
Tim Sarbin d5d93df75c
Merge pull request #1071 from gucio321/hotfix2
hotfix: removed unnecessary 0 in unknown3 bytes list
2021-02-19 13:20:44 -05:00
M. Sz a4f12f6ebe hotfix: removed unnecessary 0 from d2fontglyph.unknown3 bytes list 2021-02-19 14:22:11 +01:00
gravestench 318e4ac7e2
Merge pull request #1070 from gucio321/hotfix2
hotfix: font format:
2021-02-19 01:11:00 -08:00
M. Sz 7574293624 hotfix: font format:
- removed magic numbers
- corrected unknown1 bytes count in d2fontglyph.Create
2021-02-19 08:49:04 +01:00
gravestench 30fb02bc10
Merge pull request #1069 from gucio321/hotfix2
hothotfix: fixed argument order in call to d2fontglyph.Create
2021-02-18 23:18:37 -08:00
M. Sz a9ccda1873 hothotfix: fixed argument order in call to d2fontglyph.Create 2021-02-19 07:42:25 +01:00
Tim Sarbin 99c7d49510
Merge pull request #1067 from gucio321/hotfix2
font format: NewFontGlyph method
2021-02-18 15:23:07 -05:00
Tim Sarbin f7e965f098
Merge branch 'master' into hotfix2 2021-02-18 15:11:05 -05:00
gravestench 2888b5782e
Merge pull request #1068 from se16n/master
Updated description of project location path on windows
2021-02-18 12:07:47 -08:00
M. Sz b75f82fc8c font format: refactor: moved font glyph into new package 2021-02-18 21:05:16 +01:00
se16n 93389d595d
Update description of project location path on windows 2021-02-18 22:40:09 +03:00
M. Sz aeef2d5c4b font format: NewFontGlyph method 2021-02-18 20:39:57 +01:00
gravestench 169521e546 DS1.Tile() now calls update if dirty 2021-02-17 10:57:38 -08:00
gravestench 8a5148647b renamed another file 2021-02-17 10:23:49 -08:00
gravestench 84d510fe16 d2ds1.FloorShadow is now private 2021-02-17 10:16:10 -08:00
gravestench 09bbcf0b4d renamed some files in d2ds1 2021-02-17 10:15:51 -08:00
gucio321 99908016be
unit tests for ds1 (#4)
* ds1 refactor: added test fore some methods; put tests in right order

* ds1 refactor: unit tests for all methods

* ds1 refactor: fixed build errors

* ds1 refactor: lintfix

* ds1 refactor: fixed bug with SetWidth, SetHeight methods

* ds1 refactor: rename tile_record.go -> tile.go

* ds1 refactor: unit test for SetTiles

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-02-17 10:04:44 -08:00
gravestench ec47f16cc4 Refactoring d2ds1
* Adding setters/getters so that state management can be maintained
internally when the ds1 struct is altered
* Adding unit tests for DS1
2021-02-17 02:36:25 -08:00
gravestench e9c9786af1
Merge pull request #1065 from gucio321/hotfix2
Hotfix: methods for editing FontGlyph (font glyph is now exported)
2021-02-17 02:35:02 -08:00
M. Sz 6fdbaa07bd font table format: lintfix 2021-02-17 10:37:04 +01:00
M. Sz 66ac5ff657 font table format: methods to set size and frame index 2021-02-17 10:28:36 +01:00
M. Sz 7a54465eb3 hotfix: font table format: fontGlyph is now exported 2021-02-17 09:05:29 +01:00
gravestench 6043ca531f
Merge pull request #1063 from gucio321/hotfix2
hotfix: methods neccesary for getting informations from font table
2021-02-16 23:45:31 -08:00
M. Sz 4243a1f6b1 font: rename GetSize, and GetFrameIndex -> Size, FrameIndex 2021-02-17 08:38:41 +01:00
M. Sz 9f47ed4b35 font encoder: lintfix 2021-02-17 08:32:41 +01:00
M. Sz 1d12c2036a hotfix: font table editor: added methods GetSize, and GetFrameIndex to font glyph 2021-02-17 08:28:23 +01:00
gravestench 6a94dfcfcf
Merge pull request #1053 from gucio321/date-encoder-font
Date encoder: font table
2021-02-16 22:59:10 -08:00
gucio321 b2e10ca43e
Merge branch 'master' into date-encoder-font 2021-02-17 07:56:00 +01:00
gravestench c4f8182f2f
Merge pull request #1061 from gravestench/master
bugfix: circle-ci build
2021-02-16 11:57:54 -08:00
gravestench 67f202fee2 bugfix: circle-ci build 2021-02-16 10:25:57 -08:00
gravestench a80851f6f0
Merge pull request #1060 from OpenDiablo2/revert-1059-hotfix2
Revert "hotfix: ds1: method setupLayerTypes is now exported + circleci build job bugfix"

These are two separate PR's. You can make a PR for the circl-ci bugfix, but exporting this ds1 method is a bad idea. This is a state manegement method that should only be called internally. By exporting this, we would be placing the responsibility of calling this method on users of `d2ds1`, which may not always be just od2 developers.
2021-02-16 10:05:43 -08:00
gravestench 97708c1349
Revert "hotfix: ds1: method setupLayerTypes is now exported + circleci build job bugfix" 2021-02-16 10:02:34 -08:00
gravestench 6191bd8546
Merge pull request #1059 from gucio321/hotfix2
hotfix: ds1: method setupLayerTypes is now exported + circleci build job bugfix
2021-02-16 09:39:55 -08:00
M. Sz ff694f6ad6 circleci: added apt-get update 2021-02-16 16:49:16 +01:00
M. Sz 6866a03f34 hotfix: ds1: method setupLayerTypes is now exported 2021-02-16 13:06:12 +01:00
Tim Sarbin 1dd6f1f29a
Merge pull request #1056 from OpenDiablo2/dependabot/github_actions/golangci/golangci-lint-action-v2.4.0
Bump golangci/golangci-lint-action from v2.3.0 to v2.4.0
2021-02-15 14:56:09 -05:00
Tim Sarbin 1cc8f1d063
Merge branch 'master' into dependabot/github_actions/golangci/golangci-lint-action-v2.4.0 2021-02-15 14:54:39 -05:00
Tim Sarbin 8f4a42fbf8
Merge pull request #1057 from gucio321/hotfix3
hotfix: ds1 editor: some fields need to be exported
2021-02-15 14:54:13 -05:00
M. Sz 0dd9ae6783 ds1 encoder: fixed build error 2021-02-15 15:07:33 +01:00
M. Sz dc53da4c6f circleci: added space at the end of circleci's apt-get to make test completable 2021-02-15 15:02:59 +01:00
M. Sz 8a4c138835 ds1 encoder: layerStreamTypes and npcIndexes are now exported 2021-02-15 15:01:14 +01:00
M. Sz bbeb4b48e2 hotfix: rename hidden -> HiddenBytes in wall_record.go and floor_shadow_record.go 2021-02-15 08:34:12 +01:00
dependabot[bot] 80c9a86428
Bump golangci/golangci-lint-action from v2.3.0 to v2.4.0
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from v2.3.0 to v2.4.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v2.3.0...544d2efb307b3f205f34886f2787046abe7fb26e)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-15 06:23:50 +00:00
gravestench 298fc786b8
Merge branch 'master' into date-encoder-font 2021-02-10 12:14:41 -08:00
gravestench 0428aa274a
Merge pull request #1052 from gucio321/data-encoder-dat
data encoder: d2dat
2021-02-10 12:14:28 -08:00
gravestench d268a987f3
Merge branch 'master' into data-encoder-dat 2021-02-10 12:05:10 -08:00
gravestench f8aeb657ca
Merge pull request #1049 from gucio321/data-encoding2
d2dc6 refactor + unit test for it
2021-02-10 11:54:27 -08:00
gravestench 0c5a3ae54c
Merge branch 'master' into data-encoding2 2021-02-10 11:52:00 -08:00
gravestench a8c85b754d
Merge pull request #1044 from gucio321/data-encoder-dt1
data encoder: ds1+ds1 refactor (#1046)
2021-02-10 11:51:40 -08:00
gucio321 025a172500 fixed lint errors in stream_writter 2021-02-10 19:53:11 +01:00
gucio321 004787597e
Merge branch 'master' into data-encoder-dt1 2021-02-10 19:43:32 +01:00
Tim Sarbin d981ae49a1
Merge pull request #1055 from gucio321/hotfix2
hotfix: cof encoder: coding weapon class
2021-02-10 11:49:18 -05:00
M. Sz 8a087dba6c stream writer and stream writer test:
- fixed typo
- cut PushBits... methods
- removed magic number
2021-02-10 14:00:03 +01:00
gucio321 6c230f66d7
d2dat encoder: removed typo in comment 2021-02-10 13:03:59 +01:00
M. Sz 7d0eeb0fd3 hotfix: d2cof encoder: changed way of pushing weapon class 2021-02-10 12:59:07 +01:00
M. Sz 1010353071 hotfix: d2cof encoder: removed magic number (len of weapon class) 2021-02-10 12:35:35 +01:00
M. Sz 8a15c0b074 hotfix: cof encoder: coding weapon class 2021-02-10 08:33:29 +01:00
M. Sz d9cfe7f435 d2font: removed d2interface.Animation argument from d2font.Load; added height reading in glyphs loader 2021-02-09 08:53:34 +01:00
M. Sz 622e54dfce dc6 refactor: lintfix 2021-02-08 18:50:58 +01:00
M. Sz 6d098de778 d2dc6 refactor + unit test for it 2021-02-08 18:50:58 +01:00
gucio321 b74bc3d0b6
Merge branch 'master' into data-encoder-dat 2021-02-08 18:43:58 +01:00
gucio321 98c38b0dbf
Merge branch 'master' into data-encoder-dt1 2021-02-08 18:40:27 +01:00
gucio321 51833ed2de
Merge branch 'master' into date-encoder-font 2021-02-08 18:40:07 +01:00
Tim Sarbin 227bc9fcb1
Merge pull request #1045 from gravestench/master
Minor refactor of d2cof
2021-02-08 12:39:41 -05:00
Tim Sarbin a85a7a18c1
Merge branch 'master' into master 2021-02-08 12:37:46 -05:00
M. Sz e2ec1c6613 d2font: fixed lint errors 2021-02-08 17:25:02 +01:00
M. Sz 662d4489c4 d2font: encoder 2021-02-08 15:03:59 +01:00
M. Sz 6df66b51c1 d2font: rewritten initGlyphs ethod to use stream reader 2021-02-08 14:11:51 +01:00
M. Sz 721a67b404 font table interpreter: moved d stuff responsible for font table into d2fileformats/d2font 2021-02-08 13:21:50 +01:00
gucio321 ac50f8274a
Merge branch 'master' into data-encoder-dat 2021-02-08 12:05:40 +01:00
gucio321 bcfb0fb5c2
Merge branch 'master' into data-encoder-dt1 2021-02-08 12:04:55 +01:00
Tim Sarbin 5e1dbf56f0
Merge pull request #1051 from gucio321/unit-tests
unit test: bitmuncher
2021-02-08 06:01:59 -05:00
M. Sz a76ce059e8 dat decoder: fixed lints 2021-02-08 10:03:34 +01:00
gucio321 794c246f64 fixed lint error in bitmuncher_test.go 2021-02-08 09:57:43 +01:00
M. Sz b6cb6f88a6 data encoder: d2dat 2021-02-08 09:49:43 +01:00
M. Sz 5caa93a399 unit test: bitmuncher 2021-02-08 08:23:43 +01:00
M. Sz 8e1ca1dd7f stream writer test: added test for pushing bits 2021-02-06 20:43:04 +01:00
M. Sz 20f0d8a3d5 removed some of nolint:govet 2021-02-06 19:10:15 +01:00
M. Sz 215ac8cfc5 ds1: splited loading function 2021-02-06 18:58:15 +01:00
M. Sz 32570d6ae5 replaced nolint:gomnd directives with "magic numbers'" names 2021-02-06 18:28:01 +01:00
gucio321 ac8794015e Merge branch 'master' into data-encoder-dt1 2021-02-06 17:30:10 +01:00
M. Sz aadfbbc8a6 stream writer: added warnings when bits count is greater then possible input size (in PUshBits... methods) 2021-02-06 17:23:11 +01:00
gravestench cc8f319298
Merge pull request #1040 from gucio321/data-encoding2
data encoding: tbl (Text Dictionary)
2021-02-05 16:55:08 -08:00
gravestench 248eaf9d79 Minor refactor of d2cof
* Changed `Load` to `Unmarshal`
* made `Marshal` and `Unmarshal` into methods of `COF`
* added `New` function which creates a new, empty COF instance
* added helper functions `Marshal` and `Unmarshal`
* Changed `StreamReader.ReadBytes` to account for edge case of reading 0
bytes (this was returning an error when it should not have)
* added really simple unit tests for COF
2021-02-05 14:43:42 -08:00
M. Sz b3a754a4a6 ds1 encoder: Marshal method was splited to avoid nolint directive 2021-02-05 15:05:11 +01:00
M. Sz 9227de3418 d2ds1 encoder: fixed lint errors 2021-02-05 14:54:35 +01:00
M. Sz 5702d96cac ds1 encoder: fixed bug, when decoded and encoded back data wasn't the same = records' encoding methods was rewritten to use streamWriter.Pushbit 2021-02-05 14:45:22 +01:00
M. Sz 3dafb3ebcd dt1 encoder: moved record encoders and decoders to appropriate files 2021-02-05 14:07:51 +01:00
M. Sz 9f56574066 data encoder: ds1 2021-02-05 12:52:51 +01:00
gucio321 1b8da9ec8e
Merge branch 'master' into data-encoding2 2021-02-03 16:28:10 +01:00
Tim Sarbin a89042c1f2
Merge pull request #1042 from gucio321/data-encoder-dt1
data encoder: dt1
2021-02-03 10:00:09 -05:00
Tim Sarbin ffe4e68108
Merge branch 'master' into data-encoder-dt1 2021-02-03 09:57:46 -05:00
Tim Sarbin 7ec8921f18
Merge pull request #1039 from gucio321/data-converting
data encoding: DC6
2021-02-03 09:57:07 -05:00
Tim Sarbin 89595329a5
Merge branch 'master' into data-converting 2021-02-03 09:48:54 -05:00
Tim Sarbin ef85feb098
Merge pull request #1037 from gucio321/hotfix2
asset manager: merged TranslateLabel to TranslateString
2021-02-03 09:48:25 -05:00
M. Sz 9f9c882653 data encoder: dt1 2021-02-02 19:25:27 +01:00
gucio321 73ca325a6b
Merge branch 'master' into data-converting 2021-02-02 17:01:05 +01:00
gucio321 5aded8de66
Merge branch 'master' into data-encoding2 2021-02-02 17:00:19 +01:00
M. Sz ae77badd18 fixed lint error 2021-02-02 16:57:57 +01:00
M. Sz d535ae6c2a Merge branch 'hotfix2' of https://github.com/gucio321/OpenDiablo2 into hotfix2 2021-02-02 16:48:01 +01:00
M. Sz aea1d86690 fixed lint error 2021-02-02 16:46:40 +01:00
Tim Sarbin 42a41d4817
Merge branch 'master' into hotfix2 2021-02-02 08:54:17 -05:00
Tim Sarbin a2f298a8ce
Merge pull request #1041 from gucio321/lintfix
lintfix: gomnd
2021-02-02 08:53:52 -05:00
M. Sz 909a0a9939 lintfix: gomnd 2021-02-02 12:02:11 +01:00
M. Sz 20f2649b65 asset manager: merged TranslateLabel to TranslateString 2021-02-02 10:15:18 +01:00
M. Sz 5a0571763e data encoding: tbl 2021-02-02 10:08:32 +01:00
M. Sz 2ebb36eba8 fixed stream-writer's test bug 2021-02-01 20:58:29 +01:00
M. Sz 84c87b2eb8 data encoding: DC6 2021-02-01 12:57:02 +01:00
Tim Sarbin 60c1b4ea26
Merge pull request #1038 from gucio321/data-converting
Data encoder: COF
2021-02-01 06:51:48 -05:00
M. Sz 7781b2cd6b removed PushByte method from StreamWriter 2021-02-01 11:20:44 +01:00
M. Sz 0fec9473ed rename: PushBytes(b []byte) -> PushBytes(b ...byte) 2021-02-01 11:15:42 +01:00
M. Sz 0f32ad5d62 data encoder: COF remade Cof encoder to use stream writter 2021-01-31 19:14:18 +01:00
gucio321 c5eb602de0
Update cof.go 2021-01-31 12:11:54 +01:00
gucio321 d0288e309f
removed WIP code 2021-01-30 18:31:44 +01:00
M. Sz 157f110105 data encoding: added COF encoder 2021-01-30 18:23:00 +01:00
Tim Sarbin 374944dfc4
Merge pull request #1036 from gucio321/hotfix
circleci update
2021-01-27 09:38:26 -05:00
M. Sz a24b7550cf circleci update 2021-01-27 09:18:16 +01:00
Tim Sarbin 5756d738d4
Merge pull request #1035 from gucio321/hotfix
readme update: removed unnecessary instructions from Contribute section
2021-01-26 20:29:37 -05:00
M. Sz 9b06fccb6d readme update: removed unnecessary instructions from Contribute section 2021-01-25 21:24:41 +01:00
Tim Sarbin 08d0a0fce2
Merge pull request #1034 from sz33psz/inventory_tooltips_fixes
Hotfix: Inventory display bugs
2021-01-25 09:56:10 -05:00
Tim Sarbin 987a2f5094
Merge branch 'master' into inventory_tooltips_fixes 2021-01-25 09:54:57 -05:00
Tim Sarbin c0c8df6701
Merge pull request #1033 from cardoso/fix/close_panel_viewport
Fix viewport and minipanel positions when closing panels via UI button
2021-01-25 09:54:40 -05:00
Tim Sarbin b4e6880cca
Merge branch 'master' into fix/close_panel_viewport 2021-01-25 09:53:12 -05:00
Tim Sarbin b7287ea3f0
Merge pull request #1032 from gucio321/hotfix
Hotfix: towo minor bugs
2021-01-25 09:53:00 -05:00
sz33psz 220f0febaa Linter error fixes
Fixed inventory display bugs
- Tooltip doesn't hide when was visible and inventory window was closed using keyboard
2021-01-23 18:08:03 +01:00
sz33psz ac572e3bf5 Fixed inventory display bugs
- No tooltips for equipped items
- Item tooltips are rendered before UI Frames
2021-01-23 13:50:04 +01:00
cardoso 7dd00645cd Fix mini panel not moving back on panel close button pressed 2021-01-22 15:50:03 -03:00
cardoso f83c74ffde Fix layout/viewport not updated when panel is closed from UI button 2021-01-22 15:14:24 -03:00
M. Sz 67525c8f4d game control: fixed panics on game start, when single player (party panel wasn't created) 2021-01-20 08:48:55 +01:00
M. Sz a126242f9f main menu: btnServerIPOk label 2021-01-19 19:42:17 +01:00
M. Sz c9859f25d3 party panel: party panel is created only, when game isn't singlePlayer game 2021-01-19 19:36:16 +01:00
Tim Sarbin e982430c55
Merge pull request #1031 from gucio321/party-screen
Party Panel
2021-01-19 12:55:11 -05:00
M. Sz 931df47607 d2ui: completed ButtonTypePartyBUtton's layout literal 2021-01-19 18:50:07 +01:00
M. Sz 27ca1c84ef party panel: removed unnecessary test data 2021-01-19 17:59:58 +01:00
M. Sz b25bbe31f4 d2ui: rename NewCustomButton -> NewDefaultButton; lintfix 2021-01-19 17:47:42 +01:00
M. Sz 07eeec4827 party panel: rearranged constants (moved part of them into d2enum) 2021-01-19 17:39:40 +01:00
M. Sz 57bf4078b6 party panel: tooltip over players' names, players' colors, invite button, level -> lvl, relationships button has gets ret if disabled; d2ui improvement: addeded switcher's enabled methods 2021-01-19 14:09:22 +01:00
M. Sz 9a7f27ae83 party panel: party panel & multiplayer init 2021-01-18 21:11:50 +01:00
M. Sz 151ed22c7c party panel: "seeing" and "listening" buttons' tooltips; d2ui improvement: added missed methods to switchable button widget 2021-01-18 15:17:18 +01:00
M. Sz 74f7705165 party panel: moved colors to panel's constants, removed index requirement from AddPlayer method 2021-01-18 13:08:51 +01:00
M. Sz bdf640c2b1 party panel: party index refactor 2021-01-18 12:39:41 +01:00
M. Sz 63ef18c178 party panel: party-index management methods 2021-01-18 10:08:33 +01:00
M. Sz 81ebabeeff party panel: bar positioning 2021-01-17 21:00:21 +01:00
M. Sz 71e4470c25 party panel: colored labels; d2ui improvement: SwitchableButton.SetState() method; lintfix 2021-01-17 20:47:40 +01:00
M. Sz 6e82942b1f party panel: level label 2021-01-17 18:46:38 +01:00
M. Sz 9fb6c17d62 rename: *dezactivate* -> *deactivate* 2021-01-16 22:04:05 +01:00
M. Sz ebfee273ac lintfix: fixed lint errors 2021-01-16 21:59:44 +01:00
M. Sz bd3fc4bb25 party panel: switchers creator, party indexes (name, classs labels and switchers) 2021-01-16 21:59:44 +01:00
M. Sz dbacb63fae party panel: removed commented out code 2021-01-16 21:59:44 +01:00
M. Sz 877da921bf lintfix: fixed lint errors 2021-01-16 21:59:44 +01:00
M. Sz 4575df572f d2ui improvement: added switchable button widget to d2ui 2021-01-16 21:59:44 +01:00
M. Sz 0de3aeabd3 Rename partyScreen->partyPanel 2021-01-16 21:59:44 +01:00
M. Sz d67454140a party screen: shortcut, whitch opens the panel 2021-01-16 21:59:44 +01:00
M. Sz abee77968d d2ui improvement: added NewCustomButton method to d2ui 2021-01-16 21:59:44 +01:00
M. Sz d74e171313 Party screen: frame, panel, close btn 2021-01-16 21:59:44 +01:00
gravestench 9f5cde36df
Merge pull request #1029 from gucio321/game-control-refactor
bugfix: prevent opening panels (skill/ inventory, etc) when escape menu is open
2021-01-13 21:02:05 +00:00
gravestench 66cd245eee
Merge branch 'master' into game-control-refactor 2021-01-13 19:58:18 +00:00
gravestench c4e2b98187
Merge pull request #1030 from gucio321/hotfix
bugfix: select-hero-class connection type
2021-01-13 19:53:09 +00:00
M. Sz 7178b8e56c fixed bug, when character created in multiplayer mode but game started in single 2021-01-13 20:37:45 +01:00
M. Sz 3ced5ab99d fixed bug, when we was able to open panel, when esc menu was open 2021-01-13 13:24:28 +01:00
Tim Sarbin 845abcae0f
Merge pull request #1028 from gravestench/master
d2datautil.StreamReader refactor
2021-01-12 19:32:56 -05:00
gravestench 87d531814d d2datautil.StreamReader refactor
*`StreamReader.Read` methods now return errors

The other edits in this commit are related to cleaning up lint errors
caused by the changes to StreamReader
2021-01-12 10:26:27 -08:00
Tim Sarbin 938ce20579
Merge pull request #1027 from gravestench/master
Code cleanup
2021-01-11 09:07:35 -05:00
gravestench 1fc787023d fixed lint errors 2021-01-11 01:31:57 -08:00
gravestench 90d4238f38 remove init func from d2thread, moved init logic into d2app.Create 2021-01-11 01:17:01 -08:00
gravestench aa8525ff31 removed commented code 2021-01-11 01:16:29 -08:00
gravestench 2c0f3d9cd9 d2tbl.LoadTextDictionary now returns an error 2021-01-11 01:12:46 -08:00
gravestench 5a8ba5dee7
Merge pull request #1025 from essial/master
Fixed various bugs, crashes, and slowdowns.
2021-01-10 09:17:18 +00:00
Tim Sarbin c99810ad0e Fixed various bugs, crashes, and slowdowns. 2021-01-10 02:44:42 -05:00
Intyre db83814527
d2mpq refactored (#1020)
* d2mpq refactor

* d2mpq refactor last standing lint error

* d2mpq refactor: less linter noise

* d2mpq refactor: more linter issues
2021-01-08 12:46:11 -08:00
gucio321 5cd404e4a5
d2ui.Frame refactor (#994)
* d2ui.Frame refactor

* removed unneccessery d2asset.AssetManager argument from d2ui.NewUIFrame

* d2ui.Frame refactor

* removed unneccessery d2asset.AssetManager argument from d2ui.NewUIFrame

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: Tim Sarbin <tim.sarbin@gmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>
2021-01-06 21:53:01 -08:00
gucio321 0e95fd44ce
removed unused fields from d2player.GameControl.actionableRegions (#997)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>
2021-01-06 21:51:36 -08:00
gucio321 6addf7a243
removed links to closed issues from code (#1005)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>
2021-01-06 21:48:12 -08:00
gucio321 b77d793698
added label-button widget (#989)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2021-01-06 21:45:05 -08:00
Tim Sarbin 68d2486fbb
Merge pull request #1016 from Ziemas/useless_dcc_math
This DCC frame size calculation seems useless
2021-01-01 20:03:28 -05:00
Tim Sarbin 9a1e8c67a9
Merge branch 'master' into useless_dcc_math 2021-01-01 20:02:19 -05:00
Tim Sarbin 594d65ea25
Merge pull request #1015 from ThomasChr/npc_label_above_inventory_fix
Render HUD before Panels
2021-01-01 20:00:28 -05:00
Ziemas 826b1224f6 This DCC frame size calculation seems useless
TBH there some to be some other overcomplicated things going on in
DCCAnimation but too tired to use brain right now.
2021-01-02 01:56:09 +01:00
ThomasChr db87175872 Render HUD before Panels (in this Case 'Panels' only does mean Inventory Panel). This is to avoid Entity Labels to be renderd above the Inventory Panel. Fixes #936 2021-01-01 20:49:49 +01:00
Tim Sarbin 17fe8f3277
Merge pull request #1014 from essial/master
Removed improper ebiten dependency in d2interface.
2020-12-30 02:12:34 -05:00
Tim Sarbin 92989d6d7a Removed improper ebiten dependency in d2interface. 2020-12-30 02:08:32 -05:00
Tim Sarbin ad1decc813
Merge pull request #1013 from essial/master
Yet another yaml fix
2020-12-29 15:31:05 -05:00
Tim Sarbin d18243102b Yet another yaml fix 2020-12-29 15:29:15 -05:00
Tim Sarbin a4db59931c
Merge pull request #1012 from essial/master
Fix yaml errors
2020-12-29 15:25:23 -05:00
Tim Sarbin 23f05752b3 Fix yaml errors 2020-12-29 15:24:17 -05:00
Tim Sarbin 72f34ea14e
Merge pull request #1011 from essial/master
Fiddling with actions names
2020-12-29 15:21:57 -05:00
Tim Sarbin b18936eed0 Updated workflows 2020-12-29 15:20:24 -05:00
Tim Sarbin edd0cb0aae Fiddling with actions names 2020-12-29 15:16:18 -05:00
Tim Sarbin 2a3b5b143a
Merge pull request #1010 from essial/master
remove build job requirement
2020-12-29 15:11:00 -05:00
Tim Sarbin 97a2923b25 remove build job requirement 2020-12-29 15:05:48 -05:00
Tim Sarbin c8723656c1
Merge pull request #1009 from essial/master
Switched to self hosted build agent
2020-12-29 15:04:02 -05:00
Tim Sarbin 15729b8691 Removed PR requirement for action 2020-12-29 15:01:40 -05:00
Tim Sarbin 936b3d5de5 Switched to self hosted build agent 2020-12-29 11:05:52 -05:00
Tim Sarbin 427170e2e8
Merge pull request #1001 from gucio321/game-control-refactor
Game control refactor
2020-12-28 20:27:33 -05:00
Tim Sarbin 4b870de39e
Merge pull request #1008 from ianling/master
Networking bugfixes and cleanup
2020-12-28 20:26:43 -05:00
Ian Ling 3936e01afb Networking bugfixes and cleanup
Make sure connections close properly, without weird error messages
Remove player map entity when a player disconnects from a multiplayer game
Close server properly when host disconnects, handle ServerClose on remote clients
Don't mix JSON decoders and raw TCP writes
Actually handle incoming packets from remote clients
General code cleanup for simplicity and consistency
2020-12-28 13:33:17 -08:00
Tim Sarbin f3a869c2be
Merge pull request #1006 from Intyre/engine
Reduce GetTiles slice allocation
2020-12-23 22:32:50 -05:00
Intyre 9ce9c2f848 Reduce GetTiles slice allocation 2020-12-23 22:03:57 +01:00
Tim Sarbin a8ebddb917
Merge pull request #1004 from Intyre/linter
Fixed gocritic linter issues
2020-12-23 15:44:11 -05:00
Tim Sarbin 49c54c5fae
Merge pull request #1003 from Intyre/d2datautils
Refactored Stream Writer and Reader
2020-12-23 15:43:20 -05:00
Intyre 5f9e06c09c Fixed gocritic linter issues 2020-12-23 11:02:58 +01:00
Intyre 1e91df996c Refactor StreamReader 2020-12-23 10:43:33 +01:00
Intyre b78dca52c2 Refactor StreamWriter 2020-12-23 10:43:33 +01:00
Tim Sarbin 8a7514cd1a
Merge pull request #1002 from gucio321/hotfix3
fixed bug with terminal's logLevel
2020-12-22 17:09:50 -05:00
Tim Sarbin b4f1e8cbbd
Merge pull request #1000 from gucio321/multi-language-support
ckecked value of italian modifier
2020-12-22 09:25:20 -05:00
M. Sz e6dd0bc35d fixed bug with terminal's logLevel 2020-12-22 15:18:14 +01:00
M. Sz 0e2ca7d851 removed unnecessary switch-case statments from onKeyUp and onEscKey 2020-12-22 15:00:24 +01:00
M. Sz 3a5175f034 fixed build and lint errors 2020-12-22 14:28:29 +01:00
M. Sz 8700d63f67 game-controls refactor 2020-12-22 14:21:34 +01:00
M. Sz bc17f2c422 ckecked value of italian modifier 2020-12-22 10:45:30 +01:00
Tim Sarbin 0f658d5dec
Merge pull request #999 from Intyre/term
Cleaned up d2term
2020-12-21 20:21:20 -05:00
Tim Sarbin f86365694b
Merge pull request #996 from gucio321/skill-icons
skill select menu dependencies
2020-12-21 19:25:49 -05:00
Intyre 570ec238ff Fixed linter issues 2020-12-21 22:22:27 +01:00
Intyre 74a006c252 A wild } appeared 2020-12-21 22:00:07 +01:00
Intyre 04ec879035 Cleaned up d2term 2020-12-21 21:46:58 +01:00
M. Sz 0c04e9b3d5 skill select menu dependencies (when we open skillselect menu, other panels are closed) 2020-12-21 17:37:59 +01:00
Tim Sarbin 8a55e1bd4b
Merge pull request #995 from gucio321/tcp-ip-tips
tip-labels in tcpip menu
2020-12-21 08:21:48 -05:00
M. Sz 2869aea4f5 tip-labels in tcpip menu 2020-12-21 12:48:59 +01:00
Tim Sarbin fdbfc9a58e
Merge pull request #993 from gucio321/onHover-bugs2
Revert "fixed onHover bug in d2ui.Sprite"
2020-12-20 12:28:17 -05:00
M. Sz 40c2fac4f8 Revert "fixed onHover bug in d2ui.Sprite"
This reverts commit 8b557062fb.
2020-12-20 13:51:29 +01:00
gucio321 5409dc4ef2
fixed onHover bug in d2ui.Label (#991)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2020-12-19 12:28:40 -08:00
gucio321 fbfea917cb
added static checks to d2ui (#990)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2020-12-19 12:28:07 -08:00
gucio321 540f285468
fixed onHover bug in d2ui.Sprite (#992)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2020-12-19 12:05:41 -08:00
Tim Sarbin a4a7c8d7e7
Merge pull request #988 from Intyre/log
Cleanup d2records logging
2020-12-18 19:24:01 -05:00
Intyre ec91203782 Renamed MonStat2Record 2020-12-18 19:03:13 +01:00
Intyre 05e9f34765 Renamed ObjectDetailRecord 2020-12-18 19:02:34 +01:00
Intyre 2dc490b152 Renamed MonStatRecord 2020-12-18 18:59:29 +01:00
Intyre af6a8272dd Renamed SoundDetailRecord 2020-12-18 18:51:41 +01:00
Intyre a62e21a572 Renamed CharStatRecord 2020-12-18 18:50:42 +01:00
Intyre c7288eec38 Cleanup d2records logging 2020-12-18 18:46:34 +01:00
Tim Sarbin 7509a0d963
Merge pull request #986 from Intyre/pr
Replaced kingpin with flag package
2020-12-18 12:22:34 -05:00
Tim Sarbin 719480e95b
Merge pull request #985 from gucio321/hotfix
level-up buttons tooltips
2020-12-18 12:21:51 -05:00
Intyre add41494be Replaced kingping with flag package 2020-12-17 21:26:05 +01:00
M. Sz 178703f280 level-up buttons tooltips 2020-12-17 18:58:12 +01:00
Tim Sarbin 6497493d57
Merge pull request #984 from gucio321/escape-menu2
hotfix: logger in d2app
2020-12-17 11:37:13 -05:00
Tim Sarbin c79c2a8c89
Merge pull request #983 from gucio321/quest-log-part2
Quest Log - quest completion animation
2020-12-17 11:35:25 -05:00
gucio321 84cac0d181
Merge pull request #15 from gucio321/master
update
2020-12-17 15:10:25 +01:00
M. Sz d58ed6202f fixed quest descr bug & added code description 2020-12-17 12:04:37 +01:00
M. Sz 8a027eb48d animation stops playing, when quest log is closed & quest socket gets highlighted, when animation is playing & fixed highlight bug 2020-12-17 11:02:39 +01:00
Tim Sarbin 27270e7d98
Merge pull request #975 from gucio321/add-skill-buttons
Add-skill buttons
2020-12-16 18:41:28 -05:00
Tim Sarbin 52dbaebc2f
Merge pull request #982 from gucio321/readme-update
readme update: screen shots and status
2020-12-16 18:40:49 -05:00
M. Sz 856138daca animation is played and last frame is completedFrame 2020-12-16 19:06:12 +01:00
M. Sz e42e4fbe48 corrected grammar errors 2020-12-16 17:16:01 +01:00
M. Sz dfb3e2705a updated d2hero.HeroStatsState 2020-12-16 17:05:49 +01:00
M. Sz 69fc4d30e4 hero save file 2020-12-16 16:57:48 +01:00
M. Sz caa4b2fc11 escape menu hotkeys 2020-12-16 16:57:48 +01:00
M. Sz 6c2e078b5e revert:hero save file (app.go) 2020-12-16 16:55:50 +01:00
M. Sz cf439ede2f skill tre - remaining points label 2020-12-16 16:49:50 +01:00
M. Sz be8b3e3157 stats changing: hero stats panel 2020-12-16 16:49:50 +01:00
M. Sz 3220cc93f4 add-buttons actions 2020-12-16 16:49:50 +01:00
M. Sz 7e346bd039 add-buttons init 2020-12-16 16:49:50 +01:00
M. Sz 9c019afd94 hero save file 2020-12-16 16:49:50 +01:00
M. Sz 60a3e66c95 escape menu hotkeys 2020-12-16 16:49:50 +01:00
Tim Sarbin 22ef2c3a85
Merge pull request #981 from gucio321/hotfix
hotfix for #971
2020-12-16 09:16:41 -05:00
Tim Sarbin 19619512a2
Merge pull request #974 from hajimehoshi/ebiten202
Update Ebiten to v2.0.2
2020-12-16 09:13:18 -05:00
M. Sz 08e590f3e4 quest animation initial. 2020-12-16 15:08:39 +01:00
M. Sz 921d8d5bd8 status and screens update 2020-12-16 11:47:33 +01:00
M. Sz 5eeb07e1c1 changed terminal color separator & changed logLevelNone to logLevelDefault in app.go 2020-12-16 09:04:07 +01:00
Hajime Hoshi 366df65c92 Update Ebiten to v2.0.2 2020-12-15 23:50:33 +09:00
Tim Sarbin ea3a66c17d
Merge pull request #972 from gucio321/help-overlay
HelpOverlay's text
2020-12-14 19:57:39 -05:00
Tim Sarbin 3a4e6005f6
Merge pull request #971 from gucio321/hotfix
code-cleanup: rgbaColor & terminal
2020-12-14 19:56:43 -05:00
M. Sz 4aa3c9420a removed unused issue comments 2020-12-14 18:32:56 +01:00
M. Sz 8186ecd55a help overlay text 2020-12-14 18:29:54 +01:00
M. Sz 208f2feed5 fixed saving key change bug 2020-12-14 16:47:13 +01:00
M. Sz 25f405838a terminal printing 2020-12-14 13:45:06 +01:00
M. Sz 84c036f8a3 removed unused functions for text colorizing 2020-12-14 11:40:19 +01:00
M. Sz c6ab79c388 quest log disabled icon 2020-12-14 11:25:37 +01:00
Tim Sarbin 469e4fa735
Merge pull request #966 from gucio321/hotfix
hotfix: helpOverlay hotkeys
2020-12-12 13:31:56 -05:00
gucio321 5b4e23eb8c
Merge branch 'master' into hotfix 2020-12-12 19:29:12 +01:00
gucio321 2e31f3d1ec
Move Gold Panel (#962)
* move gold panel
2020-12-12 01:39:26 -08:00
M. Sz 8f1aadc223 error handling in d2gamescreen.CreateNewGame 2020-12-11 10:26:49 +01:00
M. Sz 509dfda5e5 clean up error handling in d2game/d2player/inventory.go and d2core/d2item/diablo2item/ 2020-12-10 13:18:26 +01:00
M. Sz d302263ac1 code cleanup 2020-12-08 09:18:27 +01:00
Tim Sarbin 3f8dcf2232
Merge pull request #964 from OpenDiablo2/dependabot/github_actions/actions/checkout-v2.3.4
Bump actions/checkout from v1 to v2.3.4
2020-12-07 14:38:58 -05:00
Tim Sarbin dbcc619454
Merge pull request #963 from OpenDiablo2/dependabot/github_actions/actions/setup-go-v2.1.3
Bump actions/setup-go from v1 to v2.1.3
2020-12-07 14:38:42 -05:00
dependabot[bot] 4d876fb922
Bump actions/checkout from v1 to v2.3.4
Bumps [actions/checkout](https://github.com/actions/checkout) from v1 to v2.3.4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v1...5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-07 19:36:03 +00:00
dependabot[bot] 627a9488ce
Bump actions/setup-go from v1 to v2.1.3
Bumps [actions/setup-go](https://github.com/actions/setup-go) from v1 to v2.1.3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v1...37335c7bb261b353407cff977110895fa0b4f7d8)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-07 19:36:03 +00:00
Tim Sarbin fb4a5be1d4
Merge pull request #961 from jlosito/dependabot-config
Check github actions weekly with dependabot
2020-12-07 14:35:40 -05:00
Tim Sarbin cf069de879
Merge pull request #960 from gucio321/quest-log-part2
Quest log part2
2020-12-07 14:35:23 -05:00
M. Sz e9878fae0c removed unused button types 2020-12-07 19:57:04 +01:00
M. Sz cbe4466fea added max player act support in quest log 2020-12-07 19:17:46 +01:00
M. Sz 1d5516c374 code clean up 2020-12-07 19:07:20 +01:00
M. Sz 5c9d4a80e2 Optymalized tab placing 2020-12-07 15:29:52 +01:00
John Losito 27531de923 Check github actions weekly with dependabot 2020-12-06 11:37:38 -05:00
M. Sz dffa8ff865 moved max quests acts to d2enum 2020-12-04 10:43:33 +01:00
M. Sz ed89d91ae4 code cleanup 2020-12-04 09:20:38 +01:00
M. Sz 708ee12182 removed unused table 2020-12-03 12:53:27 +01:00
M. Sz 9f112f6c12 code optimalisation 2020-12-03 11:56:54 +01:00
M. Sz 2c303d74f2 quest status init 2020-12-03 10:40:01 +01:00
M. Sz b40627ad3e some useful functions for quest log 2020-12-02 17:16:16 +01:00
Tim Sarbin e6d5c8b9fd
Merge pull request #957 from gucio321/hotfix
Hotfix: minor bugs in quest log
2020-12-02 08:12:13 -05:00
Tim Sarbin b6518af6b3
Merge pull request #958 from juander-ux/ui_manager_reset
Properly reset uiManager
2020-12-02 08:11:21 -05:00
M. Sz 013264cb8e socket highlight init 2020-12-02 13:42:40 +01:00
juander 7ca113eed3 d2player/game_controls: Fix skilltree event opening inventory 2020-12-02 12:32:16 +01:00
juander 0d2a40a093 d2core/ui_manager: Properly reset widgetsGroups/tooltips 2020-12-02 12:30:45 +01:00
M. Sz 5804cd2c23 quest description init 2020-12-02 11:47:46 +01:00
M. Sz 7462a6c0a3 KeyAlt in help_overlay 2020-12-02 11:08:36 +01:00
M. Sz c7a841fe5a Merge branch 'hotfix' of https://github.com/gucio321/OpenDiablo2 into hotfix 2020-12-02 09:41:52 +01:00
M. Sz f02ccd86c4 hotfix: fixed errors in quest log & code cleanup 2020-12-02 09:24:14 +01:00
M. Sz 104177279e hotfix: fixed errors in quest log & code cleanup 2020-12-02 09:23:25 +01:00
gucio321 96916863ff
Quest log initial (#956)
* Adding character quest panel
2020-12-01 23:19:15 -08:00
Tim Sarbin b936608651
Merge pull request #955 from gucio321/escape-menu
d2resource - escape menu labels
2020-11-29 16:57:18 -05:00
M. Sz fa1e86acc3 added quest log items to d2resources and modified player movement speed 2020-11-29 17:08:46 +01:00
M. Sz b0af051f4c d2resource - escape menu labels 2020-11-28 19:58:22 +01:00
Tim Sarbin 73d381215e
Merge pull request #951 from gucio321/multi-language-labels
Multi language labels
2020-11-27 11:47:54 -05:00
Tim Sarbin b27f169c8a
Merge pull request #954 from gucio321/credits-color
colored credits
2020-11-27 11:46:45 -05:00
M. Sz 6a0512d0fe colored credits 2020-11-27 09:27:06 +01:00
Tim Sarbin 301a9698c8
Merge pull request #953 from gucio321/hotfix
Hotfix: minipanel bugs
2020-11-26 14:58:51 -05:00
M. Sz 2daf857b54 when both panels (e.g. inventory and hero stats) are open, mini panel will not be opened 2020-11-26 19:11:00 +01:00
M. Sz 77b915f471 fixed #949 2020-11-26 17:25:21 +01:00
M. Sz 3964d4f6f2 exit button in credits 2020-11-26 15:28:06 +01:00
M. Sz 52ace1ecb3 minipanel label 2020-11-26 14:16:55 +01:00
M. Sz 2dab48a2ee Updated some labels 2020-11-26 13:51:39 +01:00
M. Sz 640a9e043d code cleanup 2020-11-26 12:25:47 +01:00
M. Sz 570d71845e fixed build error 2020-11-26 11:50:00 +01:00
M. Sz 862951e440 code cleanup 2020-11-26 11:48:49 +01:00
M. Sz 1dcd63a238 fixed lint errors 2020-11-26 11:30:11 +01:00
M. Sz 76257ca351 moved some stuff 2020-11-26 11:13:35 +01:00
M. Sz e5bab6660b modification of comments 2020-11-26 10:17:56 +01:00
Tim Sarbin 3d00a0fd17
Merge pull request #942 from ThomasChr/#866_save_act_and_difficulty
save act and difficulty. Fixes #866
2020-11-25 20:44:00 -05:00
Tim Sarbin 0ef943a673
Merge pull request #952 from Ziemas/blendfix
Fix draweffect blending selection
2020-11-25 20:43:04 -05:00
Ziemas 31a21b50dd Fix draweffect blending selection
The blending mode picked by handleStateEffect was being overriden.

CompositeModeSourceOver is the default selection, we do not need to
initialise opts with this.
2020-11-26 02:35:10 +01:00
Tim Sarbin 31934b62c7
Merge pull request #950 from gucio321/hotfix
Hotfix: necromancer animation in select hero class menu
2020-11-25 19:32:09 -05:00
M. Sz 6f6516ae33 a bit updated comments 2020-11-25 20:57:52 +01:00
gucio321 0ab3d70952
Merge pull request #10 from gucio321/master
update
2020-11-25 20:00:29 +01:00
M. Sz 74cdc3913a fixed necromancer animation in select her class menu 2020-11-25 19:55:18 +01:00
M. Sz 91f28516ff fixed lints 2020-11-25 19:25:19 +01:00
M. Sz 122b49b1cf Adds translatable labels in most part of main menu 2020-11-25 18:40:56 +01:00
M. Sz 56787b13b8 Opimalisation 2020-11-25 12:37:16 +01:00
Thomas Christlieb 320583b5d4 save act and difficulty. Fixes #866 2020-11-25 11:51:20 +01:00
gucio321 a4ded26038
Merge pull request #9 from gucio321/master
update
2020-11-25 11:38:40 +01:00
M. Sz 03a2664675 function translateLabel is now global function of d2gamescreen 2020-11-25 11:06:05 +01:00
M. Sz d87e4a846a init of multi-language main menu 2020-11-25 10:03:50 +01:00
Tim Sarbin 19e3053c0a
Merge pull request #946 from gucio321/d2networking-logger2
d2networking logger
2020-11-25 03:09:36 -05:00
gucio321 33bc9fe434
locale strings for character select & hero stat panel (#948)
* character select screen's hero descriptions & hero stat panel

* cinematics names

* buttons on character select screen and "resistances" labels on hero stats panel

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2020-11-24 23:45:30 -08:00
M. Sz 80a1eb8eab fixed lints 2020-11-24 17:52:27 +01:00
M. Sz 0f2c5cecb1 fixed lints 2020-11-24 17:50:31 +01:00
M. Sz 0d6e8ac0be Merge branch 'multi-language-labels' into hotfix 2020-11-24 17:45:28 +01:00
M. Sz c704cc4c45 buttons on character select screen and "resistances" labels on hero stats panel 2020-11-24 17:44:15 +01:00
M. Sz 9e0a5d2df9 cinematics names 2020-11-24 12:11:53 +01:00
M. Sz ac6846b0ec Fixed lints 2020-11-24 11:50:58 +01:00
M. Sz 5a6f0c2dcb character select screen's hero descriptions & hero stat panel 2020-11-24 10:47:11 +01:00
Tim Sarbin b2a9477816
Merge pull request #947 from gucio321/square-button-types
Square button names
2020-11-23 20:18:15 -05:00
M. Sz a3b82f036b added squelch button description 2020-11-23 18:28:11 +01:00
M. Sz 5e0b28c09e "fixed" lint error 2020-11-23 18:16:56 +01:00
M. Sz 569608295c fixed build errors 2020-11-23 18:07:51 +01:00
M. Sz c9ad3741ae Fixed name error 2020-11-23 17:43:24 +01:00
M. Sz b5fa294e07 Added button type cancel description 2020-11-23 17:41:32 +01:00
M. Sz 3a0895bfd1 Added button type cancel description 2020-11-23 17:41:01 +01:00
M. Sz cf6761a50c Square button names 2020-11-23 15:35:10 +01:00
M. Sz 4bb305ea65 d2networking logger 2020-11-23 14:18:30 +01:00
gucio321 f9c607b734
Hotfix: gold button in left corner of game screen (#945)
* hotfix: remove gold button in 0,0
2020-11-23 02:29:35 -08:00
gucio321 7919b742bd
gold (#943)
* Init for gold button and label

* gold button and label in inventory menu

* gold value saved/loaded from player save file

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2020-11-22 14:53:33 -08:00
gucio321 24affd785c
close button label in help overlay (#941)
* resolved build errors

* fixed close button label in help overlay

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2020-11-21 21:38:09 -08:00
gucio321 518a667a84
modification of the logger (#940)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2020-11-21 21:36:59 -08:00
gucio321 36c92dc433
resolved build errors (#939)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2020-11-21 03:19:13 -08:00
juander-ux 1f49df62d1
Ui hud polishing (#938)
* d2ui/tooltip: Make it invisible by default

* d2ui/button: Add GetToggled() method

* d2player/HUD: Add tooltip for minipanel button

* d2ui/button: Add disabled frame to minipanel buttons

* d2ui/widget_group: Add SetEnable method for clickable widgets

* d2player/mini_panel: move menu button here from HUD

* d2ui/button: toggled buttons take preference over disabled buttons

* d2player/help_overlay: Make panel only use widgets

* d2player/hud: Group most widgets into widget group

* d2ui/custom_widget: Allow tooltip to be attached

* d2player/hud: Attach staminaBar tooltip to staminaBar

* d2player/hud: Attach experienceBar tooltip to experienceBar widget

* d2ui/ui_manager: Always draw tooltips last

* d2player/help_overlay: It should be drawn over the HUD

* d2player/globeWidget: Move tooltip here from HUD

* d2core/tooltip: Automatically add tooltips to the uiManager

* d2core/ui_manager: Remove special handling of widgetGroups for rendering

* d2player/help_overlay: Add button to widget group

* d2player/hud: Attack runwalk tooltip to button

* d2player/mini_panel: Add panelButton to its own widget group

* d2core/widget_group: When a clickable is added, it's also added to uiManager

* d2player/globeWidget: make tooltip un/lock on click

* d2player/hud: Add runbutton to widget group

* d2player/mini_panel: Add group for tooltips

this allows us to move the tooltip with the panelbuttons. They can't be
in the general panelGroup as they would all become visible when the
panel is opened.

* d2core/button: Remove debug log when a button with tooltip is hovered
2020-11-21 02:35:32 -08:00
Thomas Christlieb e89450daf1
Getting your local IP should not depend on an Internet connection (#937)
* Fixes #917 - Getting your local IP should not depend on an Internet connection
2020-11-21 02:34:46 -08:00
gucio321 9ffbf1320c
D2core logger (#934)
* logger for d2audio & d2map

* logger for d2ui e.t.c

* d2inventory now passes on error messages

* no more importing log in d2core

* implemented #925

* added logger to part of d2networking & fixed "need to be changed" comments

* fixed lints

* fixed errors

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
2020-11-21 02:33:22 -08:00
Tim Sarbin 34f9538afb
Merge pull request #933 from ThomasChr/#865_save_mana
save player: level, health, mana, experience
2020-11-19 23:55:38 -05:00
Thomas Christlieb 0b76e1a561 save player: level, health, mana, experience
Fixes #865
2020-11-19 20:12:31 +01:00
gucio321 2153f5ce64
implemented logger in d2gamescreen (#925)
* implemented logger in d2gamescreen

* logger in d2game/d2player

* logger for app.go
2020-11-18 13:02:49 -08:00
Tim Sarbin f6410f8961
Merge pull request #932 from gucio321/building
update build.sh
2020-11-18 09:39:39 -05:00
M. Sz 196627a283 fixed some errors 2020-11-18 09:13:16 +01:00
M. Sz a2bfa92692 updated build script 2020-11-18 09:07:00 +01:00
Michał Mrówka 2a36c956a4
Label fix (#931) 2020-11-17 23:50:31 -08:00
gravestench 28a16a36e0
updating lint workflow (#930)
lint workflow had begun to throw errors because something was deprecated in the github action. this fixes that issue.
2020-11-17 00:30:04 -08:00
Tim Sarbin cefea41810
Merge pull request #929 from PushUpek/charset_support
Added support for charset
2020-11-16 17:07:51 -05:00
Michał Mrówka bbba67487d
Added support for charset 2020-11-16 21:39:48 +01:00
Tim Sarbin 9ab89a59d4
Merge pull request #927 from PushUpek/remove_lang_option
Remove language option from config file
2020-11-16 08:38:26 -05:00
Michał Mrówka 5d5e10f229
Remove language option from config file 2020-11-16 12:47:11 +01:00
juander-ux ba5ea334cc
Ui minipanel refactor (#926)
* d2player/hud: Make minipanel button a real ui/button

* d2ui/button: Add implicit tooltips

for now it is only for close buttons.

* d2ui/frame: Add size caluclation

now frame.GetSize() returns meaningful values.

* d2ui/button: Add minipanel button types

* d2ui/hero_stats_panel: Fix cached image being way to big

* d2ui/widget_group: Fix widget groups size calculation

* d2ui/widget_group: Add debug rendering

* d2ui/widget_group: SetVisible() now sets the visibility of the group object

* d2player: Refactor mini_panel

we converted all elements to widgets. Thus rendering from game_controls
is no longer neccessary.

* d2ui/button: Add disabled color to layouts

* d2player/gamecontrols: temp hide minipanel when in esc menu

* d2ui/widget_group: Add OffsetPosition() method

* d2player/mini_panel: Implement moving of minipanel

this only occours when other panels are opened.

* d2player/minipanel: Fix inv/skilltree/char closebuttons

these would screw up the moving of the mini panel.

* Fix linter

* d2player/minipanel: Add tooltips to buttons

* d2player/skilltree: Fix icon rendering
2020-11-16 01:41:01 -08:00
juander-ux 12821147ce
Ui panel refactor part 2 (#921)
* d2ui/skilltree: Don't render availSPLabel

this is handled by the ui_manager now.

* d2ui/custom_widget: Allow them to be cached into static images

* d2player/hero_stats_panel: Remove render() function from game_controls

all ui elements are now grouped into a WidgetGroup, thus rendering is
done by the ui manager.

* d2player/hero_stats_panel: Remove unnecessary widgets from struct

we don't need to store them in the HeroStatsPanel struct anymore as they
are completly handled by the uiManager.

* d2ui/widget_group: Remove priority member

this is already defined by the BaseWidget.

* d2ui/widget: Move uiManager.contains() to the baseWidgets

this method makes more sense on a widget anyways.

* d2ui/widget: Add methods to handle widget hovering

* d2ui/custom_widget: Require define width/height

since the custom render() method can do whatever, we need the user to specify
the width/height such that GetSize() calls are meaningful.

* d2ui/widget: Allow widgets to return the uiManager

* d2player/HUD: Refactor health/mana globe into its own widget

* d2player/hud: Refactor load()

seperate each type of loading into its own method.

* d2player/HUD: Move stamina/exp bar into widgets

* d2player/HUD: Refactor left/right skills into widget

* d2ui/custom_widget: cached custom widgets should use widget.x/y

since we render to an image, we use widget.x/y to position the cached
image.

* d2player/HUD: User cached custom widget for all static images
2020-11-13 12:08:43 -08:00
Julien Ganichot 0d691dbffa
Key binding menu (#918)
* Feat(KeyBindingMenu): Adds dynamic box system with scrollbar

* Feat(Hotkeys): WIP Adds a lot of things

* Feat(KeyBindingMenu): WIP Adds logic to binding

* Feat(KeyBindingMenu): Fixes assignment logic

* Feat(KeyBindingMenu): Adds buttons logic

* Feat(KeyBindingMenu): Fixes sprites positions+add padding to Box

* Feat(KeyBindingMenu): Adds label blinking cap

* Feat(KeyBindingMenu): Removes commented func

* Feat(KeyBindingMenu): Fixes lint errors and refactors a bit

* Feat(KeyBindingMenu): Corrects few minor things from Grave

* Feat(KeyBindingMenu): removes forgotten key to string mapping
2020-11-13 12:03:30 -08:00
gravestench 7a75dbc284
updating git workflow to use golangci-lint version 1.32 (#922) 2020-11-13 10:59:38 -08:00
Tim Sarbin ef6f90c01c
Merge pull request #920 from juander-ux/fix-singleplayer-button-crash
d2gamescreen/char_select: Fix crash on double clicking singleplayer
2020-11-13 09:43:25 -05:00
Tim Sarbin 5b167df66d
Merge pull request #919 from gucio321/readme-update
readme screenshots update
2020-11-13 09:42:12 -05:00
juander dbbfdd6acf d2gamescreen/char_select: Fix crash on double clicking singleplayer
if we clicked twice on the single player button of the main menu, then
the first click would set the current screen to the
character_select. The second click directly after that will be
handled by the character_select screen. It's OnLoad() method was not
called yet, which initialized all widgets. The click handler of
character_select screen referenced the nil scrollbar and crashes.
2020-11-13 13:46:40 +01:00
M. Sz ab7705e835 readme screenshots update 2020-11-13 11:37:47 +01:00
gucio321 40dcd18487
added user IP in TCPIP menu (#916)
* added user IP in TCPIP menu
2020-11-12 02:43:05 -08:00
Tim Sarbin 2035a62ec6
Merge pull request #914 from juander-ux/drawable-refactor
d2ui/drawable refactor
2020-11-11 09:19:48 -05:00
juander 976431c8e5 Revert "d2ui/sprite: Refactor Render() to RenderNoError()"
This reverts commit 77cd538c2f.

Since we removed the return of errors from the Render() method, we no
longer require the RenderNoError() method.
2020-11-11 15:05:04 +01:00
juander 606fc028ac Revert "d2ui/label: Refactor Render() to RenderNoError()"
This reverts commit f2a55312e4.

Since we removed the return of errors from the Render() method, we no
longer require the RenderNoError() method.
2020-11-11 15:01:36 +01:00
juander c148941194 d2ui/drawable: Refactor render() method to not return error
this simplifies error handling statements all over the ui code. Before
we had to write:

if err := foo.Render(target); err != nil {
    return err
}

which simplifies now to foo.Render(target)
2020-11-11 14:55:59 +01:00
Tim Sarbin 437e65564f
Merge pull request #912 from gucio321/cinematics
Cinematics menu
2020-11-10 10:31:19 -05:00
M. Sz 60566b3b0d fixed lint errors 2020-11-10 15:09:25 +01:00
M. Sz 3e6ec4c1cc removed some unused lines 2020-11-10 15:00:40 +01:00
M. Sz a19d83ca7b fixed black screen in IPServer menu 2020-11-10 15:00:40 +01:00
M. Sz 15b2e7613f cinematics menu final commit 2020-11-10 15:00:40 +01:00
M. Sz 03ab084b42 fixed some lint errors 2020-11-10 15:00:40 +01:00
M. Sz 9ab694b6f6 Music in cinematics menu 2020-11-10 15:00:40 +01:00
M. Sz c1ad9025f9 Label in cinematics menu 2020-11-10 15:00:40 +01:00
M. Sz 2eda3827ce cinematics select background 2020-11-10 15:00:40 +01:00
M. Sz e20d544a8c Buttons in cinematics menu 2020-11-10 15:00:40 +01:00
M. Sz bd4cf1a334 cinematics menu init 2020-11-10 15:00:40 +01:00
Tim Sarbin 5e5d8415ba
Merge pull request #911 from juander-ux/ui_refactor
d2ui refactor
I *almost* squashed it!
2020-11-10 00:04:08 -05:00
juander 622bc832d3 d2player/skilltree: Move every element to widgets
the uiManager now handles every element of the ui, so we don't need to
render elements manually in game_controls. Now we can also use
widget_groups to simplify handling the opening/closing of the panel.
2020-11-09 18:26:34 +01:00
juander 83acaefea2 d2player/game_controls: learnskills cmd not always creating new skill objects
when we ran the command before we would always throw away old skill
objects and create a new one. This is nasty if we have a pointer to the
old object. By throwing it away we have to update all these pointers.

Now we rather increase the skillpoint, if the hero already has this
skill.
2020-11-09 18:26:32 +01:00
juander 77cd538c2f d2ui/sprite: Refactor Render() to RenderNoError()
this allows us to create a Render() method that implements the Widget
interface without killing us with linter warnings.
2020-11-09 18:26:32 +01:00
juander f2a55312e4 d2ui/label: Refactor Render() to RenderNoError()
this allows us to create a Render() method that implements the Widget
interface without killing us with linter warnings.
2020-11-09 18:26:28 +01:00
juander 01927d0f3b d2core/d2ui: Add checks to all widgets if they implement Widget
this also adds missing methods to elements not implementing widget. Note
here that we do not enable sprite and label, as this would produce a
crazy amount of linter warnings due to render() requiering error
handling then, which non of the callers handle. Since we remove the
render calls later anyways, we can postpone this static check for now.
2020-11-09 18:13:17 +01:00
juander bad07defe8 d2ui: Add a custom widget
this allows us to encapsulate any custom render functionality into this
custom widget. It further helps us to remove the renderer from
game_controls.
2020-11-09 18:13:17 +01:00
juander 627bc8ec65 d2ui: Add WidgetGroup
this allows us to groups screens together, such that they can be
de-/activated by de-/activating the group instead of every ui element by
hand.

Before we were deactivating buttons and stopped rendering to deactivate ui
elements. This tied the renderer to these elements.
2020-11-09 18:13:17 +01:00
juander 881d5f1f71 d2ui: Create default base widget
this encapsulates all the repeating functions defined for all widgets in
the same way, like Set/GetPosition().
2020-11-09 18:13:17 +01:00
juander aa1fca84d5 d2core/ui: Introduce clickable widgets
not all widgets need to be clickable, so let's make it it's own
interface.
2020-11-09 18:13:17 +01:00
Gürkan Kaymak e99fbf5c4b
showed an error message if the client cannot connect to a host (#910) 2020-11-08 14:03:51 -05:00
gravestench be9c29e9d2
Unfuck asset manager init (#906)
* moved loader bootstrap logic into d2app
2020-11-08 01:24:35 -08:00
gravestench f2ab13afae
moved asset manager initialization logic to d2app (#905)
* move asset manager initialization logic to d2app

* updating to loogger printf statements
2020-11-07 23:08:25 -05:00
gucio321 d5a26fd495
Hotkeys in Main Menu (#903)
* Escape in MainMenu cause game exit

* Escape in MainMenu cause game exit

* Shortcuts in d2 main menu

* Shortcuts in Main Menu

* Shortcuts in Main Menu

* hotfix for "Shortcuts in Main Menu"

* hotfix for "Shortcuts in Main Menu" - removet some lint error

* fix lint errors

Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>
2020-11-06 11:44:40 -05:00
juander-ux 9e89d5b891
game_control: HUD refactor (#904)
* d2player/game_controls: Refactor HUD into it's own class

game_controls slowly becomes a superclass that does too many things.
So move HUD into it's own class. This is the first step of getting the
renderer out of game_controls (#798)

* d2ui/tooltip: Allow background box to be disabled

* d2ui/tooltip: Make GetSize() a public function

* d2player: Move const from game_controls to hud

* d2player: Fix missing entity hover tooltip
2020-11-05 11:55:09 -08:00
gravestench f7fb35a4ec
removed debug printer singleton from d2util (#901)
resolves #504
2020-11-03 11:19:59 -08:00
gravestench af1f0a0eda
removed string table singleton from d2common/d2fileformats/d2tbl/ (#900) 2020-11-03 11:10:11 -08:00
gravestench 5ac03d6f49
refactored game bootstrap, removed `d2config.Config` singleton (#899)
* Remove d2config.Config singleton

* refactored config file bootstrap
* `d2loader.Loader` adds the config directories during init
* `d2asset.AssetManager` loads the config file during init
* mpq verification logic removed from d2config; this is done by d2loader
* added `errorMessage` to `d2app.App` for setting the error message for the error screen.

* fixed loader test
2020-11-03 07:54:15 -05:00
gravestench d6c9748fef
refactored logging in d2loader, d2record, and d2asset (#898)
* refactored logging in d2config, d2record, and d2asset

* asset manager, record manager, and file loader now utilitize d2util.Logger
* added colored logging to d2util.Logger (excluding windows platforms)
* removed mpq file verification from d2config; d2loader handles this
* record loaders now use the record manager's logger for printing info
* added command line argument for setting log level (`--loglevel 4`, `-l4`, or `-l 4`
* added `LogLevel` parameter to config file
* default log level will show errors, warnings, and info log messages
* specifying log level as an argument overrides setting from config file

* fixed log level tests
2020-11-02 21:23:07 -05:00
gravestench b052006922
add comment explaining significance of input handler return value (#895) 2020-11-02 01:14:03 -08:00
Julien Ganichot 004a3faf7d
Misc(CONTRIB): Adds Ganitzsh as a contributor (#896) 2020-11-02 01:13:46 -08:00
Julien Ganichot 8365400ff5
Feat(KeyMapping): Adds a configurable keymap to GameControls, resolves #793 (#893)
* Feat(KeyMapping): Adds a configurable keymap to GameControls + Updates help overlay to use it

Co-authored-by: gravestench <dknuth0101@gmail.com>
2020-11-01 19:43:23 -08:00
Julien Ganichot 1f2771e8bc
Resolves #874 and #892 (#894)
* Move engine initialization to d2app
* adding debug print of error returned from `App.Run`
* adding ClampInt utility function to d2math
* cleaned up argument parsing in app.go, dedicated server no longer starts a renderer

Co-authored-by: gravestench <dknuth0101@gmail.com>
2020-11-01 16:05:50 -08:00
gravestench 33cf06732e
Revert "Misc(CodeCleanup): Moves engine initialization to d2app #874 (#890)" (#891)
This reverts commit 40879db32f.
2020-11-01 12:12:45 -08:00
Julien Ganichot 40879db32f
Misc(CodeCleanup): Moves engine initialization to d2app #874 (#890)
Move engine initialization out of main and into d2app
2020-11-01 11:16:15 -08:00
gravestench aa680d030f
ServerFull Packet Implementation (#889)
* Implement ServerFullPacket including server side handling and a place holder client side.

* Making suggested edits to move to an empty packet

Co-authored-by: Stephen Horan <steve.horan@theatsgroup.com>
2020-11-01 08:26:15 -05:00
Stephen Horan 4dd69e7709
Health/Mana/Exp load values from save file #815 (#886)
* Return errors for #790

* Fixing lint issues

* Returning nil instead of empty struct pointer

* Removed new hero stat calls as new player requires stats to be passed in. These were using defaults regardless of the save file.

* Removed override on the default to health/mana/experience. These should now result is proper values.

* Removed new hero stat calls as new player requires stats to be passed in. These were using defaults regardless of the save file.

* Removed override on the default to health/mana/experience. These should now result is proper values.
2020-10-31 23:59:24 -04:00
Gürkan Kaymak 2c6c54acce
removed global variable for singleton game server (#888) 2020-10-31 23:58:55 -04:00
Thomas Christlieb 40cc421f51
Saving and loading game data, Fixes #868 #799 (#883)
* saving of player should be done on the server. That's also where the loading happens.

* refactor nearly everything, but this time it looks not that bad...

* MAke Linter happy

* Typo... uuups
2020-10-31 14:30:08 -04:00
Stephen Horan fb8e25ebdb
Code Cleanup for #790 (#870)
* Return errors for #790

* Fixing lint issues

* Returning nil instead of empty struct pointer
2020-10-30 20:19:06 -04:00
juander-ux 4506380dbb
d2player/game_controls: Fix health/mana globe text locking (#882)
when you click on a either of the globes the values should appear
above them.
2020-10-29 09:59:07 -04:00
juander-ux b1058d6085
Add labels for skill levels (#881)
* d2player/skilltree: Add label for skillpoints

this also darkens the skillicon if no skillpoint was invested yet.

* d2player/gamecontrols: learnskills <class> should only learn class
specific skills

the character would learn skills of enemies as well, like DiabWall. I
wasn't expecting the command to do that.
2020-10-29 09:50:15 -04:00
Tim Sarbin ce36dec7f8
Fix background music crash issue. (#880)
* Added useful config error panic. Added window title to panic window.

* Fixed background music crash.
2020-10-28 22:34:47 -04:00
Tim Sarbin a9d832b539
Added useful config error panic. Added window title to panic window. (#879) 2020-10-28 21:42:03 -04:00
Zaprit f98e1267fa
Dedicated server (#871)
* My brain hurts, here is a broken dedicated server

* somewhat stablised the server

* Made it less shouty, and added channel

* Split the DS functionallity from the main.go file

* Split the DS functionallity from the main.go file, and remembered to add the server file

* My brain hurts, here is a broken dedicated server

* somewhat stablised the server

* Made it less shouty, and added channel

* Split the DS functionallity from the main.go file

* Split the DS functionallity from the main.go file, and remembered to add the server file

* Added Stable Dedicated Server Functionallity

Co-authored-by: gravestench <dknuth0101@gmail.com>
2020-10-28 21:03:30 -04:00
Tim Sarbin 1a6c6b8e9f
Add panic screen (#878)
* Add panic screen

* Fixed lint error. Updated all module references
2020-10-28 21:02:12 -04:00
Thomas Christlieb 2d4c79484f
Animation mode for player should be checked (and possible changed) every tick. It needs to be changed when the player stops or starts running while follwing a path (single click, don't hold the mouse at target). Fixes #837 (#875) 2020-10-28 15:11:41 -04:00
gravestench 6e31cfb52a
migrate to ebiten v2.0 API (#860)
* migrate to ebiten v2.0 API

* fixed lint errors
2020-10-28 14:17:42 -04:00
juander-ux 79c147866e
Tooltip refactor (#872)
* d2ui: Add tooltip class

we reimplemented tooltips in several places
(inventory/skill_select_panel). Let's make it its own ui element.

* d2player: Refactor skill_select_panel to use the tooltip class

* d2player: Refactor inventory to use the tooltip class

* Make linter happy

* Make golangci-lint 1.27.0 happy as well

* tooltip: Remove const and rather disable linter
2020-10-28 13:54:55 -04:00
gravestench 4a62101b96
adding the rest of the data dictionary loaders (#869) 2020-10-28 13:52:15 -04:00
gravestench f0890d83fa
add notice about linting, remove broken badge (#857) 2020-10-27 08:46:41 -04:00
378 changed files with 19649 additions and 7382 deletions

View File

@ -1,12 +1,22 @@
# Golang CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-go/ for more details
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.14
working_directory: /go/src/github.com/OpenDiablo2/OpenDiablo2
steps:
- 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: go get -v -t -d ./...
- run: go build .
#- run: go test -v ./...
build:
docker:
- image: circleci/golang:1.16
working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}}
steps:
- checkout
- 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 build .
- 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
View File

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View 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 }}"

View File

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

View File

@ -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
View File

@ -13,3 +13,4 @@
tags
heap.out
heap.pdf
.DS_Store

View File

@ -18,16 +18,24 @@ linters-settings:
disabled-checks:
gocyclo:
min-complexity: 15
gofmt:
simplify: true
goimports:
local-prefixes: github.com/OpenDiablo2/OpenDiablo2
golint:
min-confidence: 0.8
govet:
enable-all: 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:
line-length: 140
maligned:
suggest-new: true
misspell:
locale: US
@ -57,14 +65,11 @@ linters:
- gosimple
- govet
- ineffassign
- interfacer
- lll
- maligned
- misspell
- nakedret
- prealloc
- rowserrcheck
- scopelint
- staticcheck
- structcheck
- stylecheck
@ -78,11 +83,19 @@ linters:
run:
timeout: 5m
tests: true
skip-dirs:
- .github
- build
- web
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
exclude-use-default: false

View File

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

View File

@ -20,6 +20,7 @@ Ripolak
dafe
presiyan
Natureknight
Ganitzsh
* PATREON SUPPORTERS
K C

164
README.md
View File

@ -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
[![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)
[Join us on Discord!](https://discord.gg/pRy8tdc)\
[Development Live stream](https://www.twitch.tv/essial/)\
[Support us on Patreon](https://www.patreon.com/bePatron?u=37261055)
[![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)
We are also working on a toolset:\
[https://github.com/OpenDiablo2/HellSpawner](https://github.com/OpenDiablo2/HellSpawner)\
Please consider helping out with this project as well!
----
[OpenDiablo2](https://opendiablo2.com/) is an ARPG game engine in the same vein of the 2000's games, and supports playing Diablo 2.
## 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`
On windows this folder will most likely be in `C:\users\(you)\go\src\github.com\OpenDiablo2\OpenDiablo2`
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.
* [Building](./docs/building.md) - Instructions for building the project
* [Development](./docs/development.md) - Instructions for developers who want to contribute
* [Profiling](./docs/profiling.md) - Debug performance issues
* [Debugging](./docs/debug.md) - Common errors and pitfalls
## Screenshots
@ -120,11 +66,25 @@ which will be updated over time with new requirements.
![Select Hero](docs/areas.gif)
![Gameplay](docs/Gameplay.png)
![Inventory Window](docs/Inventory.png)
![Game Panels](docs/game_panels.png)
## Additional Credits
- Diablo2 Logo
- Jose Pardilla (th3-prophetman)
- DT1 File Specifications
- Paul SIRAMY (http://paul.siramy.free.fr/_divers/dt1_doc/)
- Other Specifications and general info
- Various users on [Phrozen Keep](https://d2mods.info/home.php)
* Diablo2 Logo
* Jose Pardilla (th3-prophetman)
* DT1 File Specifications
* Paul SIRAMY (http://paul.siramy.free.fr/\_divers/dt1\_doc/)
* Other Specifications and general info
* 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.

144
build.sh
View File

@ -5,7 +5,7 @@
# License: GNU GPLv3
version="0.0.8"
go_version="1.13.4"
go_version="1.16"
echo "OpenDiablo 2 Build Script $version"
#=================================================
@ -14,103 +14,109 @@ echo "OpenDiablo 2 Build Script $version"
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}')
mesa_detect_arch=$(pacman -Q | grep mesa)
go_install() {
# Check OS & go
go_install(){
# 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"
read -r choice
[ "$choice" != y ] && [ "$choice" != Y ] && exit
echo "Install Go for OpenDiablo 2 ($distribution)? y/n"
read -r choice
[ "$choice" != y ] && [ "$choice" != Y ] && exit
if [ "$distribution" = "CentOS" ] || [ "$distribution" = "Red\ Hat" ] || [ "$distribution" = "Oracle" ]; then
echo "Downloading Go"
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz >/dev/null 2>&1
echo "Install Go"
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz >/dev/null 2>&1
echo "Clean unneeded files"
rm go*.linux-amd64.tar.gz
if [ "$distribution" = "CentOS" ] || [ "$distribution" = "Red\ Hat" ] || [ "$distribution" = "Oracle" ]; then
echo "Downloading Go"
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz > /dev/null 2>&1
echo "Install Go"
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz > /dev/null 2>&1
echo "Clean unneeded files"
rm go*.linux-amd64.tar.gz
elif [ "$distribution" = "Fedora" ]; then
echo "Downloading Go"
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz >/dev/null 2>&1
echo "Install Go"
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz >/dev/null 2>&1
echo "Clean unneeded files"
rm go*.linux-amd64.tar.gz
elif [ "$distribution" = "Fedora" ]; then
echo "Downloading Go"
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz > /dev/null 2>&1
echo "Install Go"
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz > /dev/null 2>&1
echo "Clean unneeded files"
rm go*.linux-amd64.tar.gz
elif [ "$distribution" = "Debian" ] || [ "$distribution" = "Ubuntu" ] || [ "$distribution" = "Deepin" ]; then
echo "Downloading Go"
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz >/dev/null 2>&1
echo "Install Go"
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz >/dev/null 2>&1
echo "Clean unneeded files"
rm go*.linux-amd64.tar.gz
elif [ "$distribution" = "Debian" ] || [ "$distribution" = "Ubuntu" ] || [ "$distribution" = "Deepin" ]; then
echo "Downloading Go"
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz > /dev/null 2>&1
echo "Install Go"
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz > /dev/null 2>&1
echo "Clean unneeded files"
rm go*.linux-amd64.tar.gz
elif [ "$distribution" = "Gentoo" ]; then
sudo emerge --ask n go
elif [ "$distribution" = "Gentoo" ]; then
sudo emerge --ask n go
elif [ "$distribution" = "Manjaro" ] || [ "$distribution" = "Arch\ Linux" ]; then
sudo pacman -S go --noconfirm
elif [ "$distribution" = "Manjaro" ] || [ "$distribution" = "Arch\ Linux" ]; then
sudo pacman -S go --noconfirm
elif [ "$distribution" = "OpenSUSE" ] || [ "$distribution" = "SUSE" ]; then
echo "Downloading Go"
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz >/dev/null 2>&1
echo "Install Go"
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz >/dev/null 2>&1
echo "Clean unneeded files"
rm go*.linux-amd64.tar.gz
elif [ "$distribution" = "OpenSUSE" ] || [ "$distribution" = "SUSE" ]; then
echo "Downloading Go"
wget https://dl.google.com/go/go"$go_version".linux-amd64.tar.gz > /dev/null 2>&1
echo "Install Go"
sudo tar -C /usr/local -xzf go*.linux-amd64.tar.gz > /dev/null 2>&1
echo "Clean unneeded files"
rm go*.linux-amd64.tar.gz
fi
fi
fi
fi
}
dep_install(){
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
dep_install() {
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
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
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
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
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
elif [ "$distribution" = "Gentoo" ]; then
sudo emerge --ask n libXcursor libXrandr libXinerama libXi libGLw libglvnd libsdl2 alsa-lib > /dev/null 2>&1
elif [ "$distribution" = "Gentoo" ]; then
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
sudo pacman -S libxcursor libxrandr libxinerama libxi mesa libglvnd sdl2 sdl2_mixer sdl2_net alsa-lib --noconfirm > /dev/null 2>&1
else
sudo pacman -S libxcursor libxrandr libxinerama libxi libglvnd sdl2 sdl2_mixer sdl2_net alsa-lib --noconfirm > /dev/null 2>&1
fi
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
else
sudo pacman -S libxcursor libxrandr libxinerama libxi libglvnd sdl2 sdl2_mixer sdl2_net alsa-lib --noconfirm >/dev/null 2>&1
fi
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
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
elif [[ "$OSTYPE" == "darwin"* ]]; then
# are there dependencies required? did I just have all of them already?
echo "Mac OS detected, no dependency installation necessary..."
elif [[ "$OSTYPE" == "darwin"* ]]; then
# are there dependencies required? did I just have all of them already?
echo "Mac OS detected, no dependency installation necessary..."
fi
fi
}
# Build
echo "Check Go"
go_install
echo "Install libraries"
if [ -e "$HOME/.config/OpenDiablo2/.libs" ]; then
echo "libraries is installed"
else
echo "OK" > "$HOME/.config/OpenDiablo2/.libs"
dep_install
if [ ! -e "$HOME/.config/OpenDiablo2" ]; then
mkdir -p $HOME/.config/OpenDiablo2
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"
go get -d
go build
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"

View File

@ -4,34 +4,41 @@ package d2app
import (
"bytes"
"container/ring"
"encoding/json"
"errors"
"flag"
"fmt"
"image"
"image/gif"
"image/png"
"log"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/pprof"
"strconv"
"strings"
"sync"
"syscall"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
"github.com/pkg/profile"
"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/d2math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"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/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/d2term"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2gamescreen"
"github.com/OpenDiablo2/OpenDiablo2/d2networking"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
"github.com/OpenDiablo2/OpenDiablo2/d2script"
@ -56,6 +63,8 @@ type App struct {
captureFrames []*image.RGBA
gitBranch string
gitCommit string
language string
charset string
asset *d2asset.AssetManager
inputManager d2interface.InputManager
terminal d2interface.Terminal
@ -66,12 +75,18 @@ type App struct {
ui *d2ui.UIManager
tAllocSamples *ring.Ring
guiManager *d2gui.GuiManager
config *d2config.Configuration
*d2util.Logger
errorMessage error
*Options
}
type bindTerminalEntry struct {
name string
description string
action interface{}
// Options is used to store all of the app options that can be set with arguments
type Options struct {
Debug *bool
profiler *string
Server *d2networking.ServerOptions
LogLevel *d2util.LogLevel
}
const (
@ -80,53 +95,217 @@ const (
debugPopN = 6
)
const (
appLoggerPrefix = "App"
)
// Create creates a new instance of the application
func Create(gitBranch, gitCommit string,
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)
func Create(gitBranch, gitCommit string) *App {
runtime.LockOSThread()
result := &App{
gitBranch: gitBranch,
gitCommit: gitCommit,
inputManager: inputManager,
terminal: terminal,
scriptEngine: scriptEngine,
audio: audio,
renderer: renderer,
ui: uiManager,
asset: asset,
tAllocSamples: createZeroedRing(nSamplesTAlloc),
logger := d2util.NewLogger()
logger.SetPrefix(appLoggerPrefix)
app := &App{
Logger: logger,
gitBranch: gitBranch,
gitCommit: gitCommit,
Options: &Options{
Server: &d2networking.ServerOptions{},
},
}
app.Infof("OpenDiablo2 - Open source Diablo 2 engine")
app.parseArguments()
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 == "" {
result.gitBranch = "Local Build"
c := make(chan os.Signal)
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
func (a *App) Run() error {
profileOption := kingpin.Flag("profile", "Profiles the program, one of (cpu, mem, block, goroutine, trace, thread, mutex)").String()
kingpin.Parse()
func (a *App) Run() (err error) {
// add our possible config directories
_ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath()), types.AssetSourceFileSystem)
_ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath()), types.AssetSourceFileSystem)
if len(*profileOption) > 0 {
profiler := enableProfiler(*profileOption)
if a.config, err = a.LoadConfig(); err != nil {
return err
}
// start profiler if argument was supplied
if len(*a.Options.profiler) > 0 {
profiler := enableProfiler(*a.Options.profiler, a)
if profiler != nil {
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)
// If we fail to initialize, we will show the error screen
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
}
@ -135,86 +314,16 @@ func (a *App) Run() error {
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 nil
}
func (a *App) initialize() error {
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 {
func (a *App) renderDebug(target d2interface.Surface) {
if !a.showFPS {
return nil
return
}
vsyncEnabled := a.renderer.GetVSyncEnabled()
@ -241,8 +350,6 @@ func (a *App) renderDebug(target d2interface.Surface) error {
target.PushTranslation(0, debugLineHeight)
target.DrawTextf("Coords " + strconv.FormatInt(int64(cx), 10) + "," + strconv.FormatInt(int64(cy), 10))
target.PopN(debugPopN)
return nil
}
func (a *App) renderCapture(target d2interface.Surface) error {
@ -274,35 +381,33 @@ func (a *App) renderCapture(target d2interface.Surface) error {
return nil
}
func (a *App) render(target d2interface.Surface) error {
if err := a.screen.Render(target); err != nil {
return err
}
func (a *App) render(target d2interface.Surface) {
a.screen.Render(target)
a.ui.Render(target)
if err := a.guiManager.Render(target); err != nil {
return err
return
}
if err := a.renderDebug(target); err != nil {
return err
}
a.renderDebug(target)
if err := a.renderCapture(target); err != nil {
return err
return
}
if err := a.terminal.Render(target); err != nil {
return err
return
}
return nil
}
func (a *App) advance(elapsed, elapsedUnscaled, current float64) error {
elapsedLastScreenAdvance := (current - a.lastScreenAdvance) * a.timeScale
func (a *App) advance() error {
current := d2util.Now()
elapsedUnscaled := current - a.lastTime
elapsed := elapsedUnscaled * a.timeScale
a.lastTime = current
elapsedLastScreenAdvance := (current - a.lastScreenAdvance) * a.timeScale
a.lastScreenAdvance = current
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 {
currentTime := d2util.Now()
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
}
a.render(target)
if target.GetDepth() > 0 {
return errors.New("detected surface stack leak")
@ -355,67 +449,24 @@ func (a *App) allocRate(totalAlloc uint64, fps float64) float64 {
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 {
fp, err := os.Create(a.capturePath)
if err != nil {
a.terminal.Errorf("failed to create %q", a.capturePath)
return err
}
defer func() {
if err := fp.Close(); err != nil {
log.Fatal(err)
}
}()
screenshot := target.Screenshot()
if err := png.Encode(fp, screenshot); err != nil {
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
}
@ -433,7 +484,7 @@ func (a *App) convertFramesToGif() error {
defer func() {
if err := fp.Close(); err != nil {
log.Fatal(err)
a.Fatal(err.Error())
}
}()
@ -475,49 +526,11 @@ func (a *App) convertFramesToGif() error {
return err
}
log.Printf("saved animation to %s", a.capturePath)
a.Infof("saved animation to %s", a.capturePath)
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 {
r := ring.New(n)
for i := 0; i < n; i++ {
@ -528,36 +541,36 @@ func createZeroedRing(n int) *ring.Ring {
return r
}
func enableProfiler(profileOption string) interface{ Stop() } {
func enableProfiler(profileOption string, a *App) interface{ Stop() } {
var options []func(*profile.Profile)
switch strings.ToLower(strings.Trim(profileOption, " ")) {
case "cpu":
log.Printf("CPU profiling is enabled.")
a.Logger.Debug("CPU profiling is enabled.")
options = append(options, profile.CPUProfile)
case "mem":
log.Printf("Memory profiling is enabled.")
a.Logger.Debug("Memory profiling is enabled.")
options = append(options, profile.MemProfile)
case "block":
log.Printf("Block profiling is enabled.")
a.Logger.Debug("Block profiling is enabled.")
options = append(options, profile.BlockProfile)
case "goroutine":
log.Printf("Goroutine profiling is enabled.")
a.Logger.Debug("Goroutine profiling is enabled.")
options = append(options, profile.GoroutineProfile)
case "trace":
log.Printf("Trace profiling is enabled.")
a.Logger.Debug("Trace profiling is enabled.")
options = append(options, profile.TraceProfile)
case "thread":
log.Printf("Thread creation profiling is enabled.")
a.Logger.Debug("Thread creation profiling is enabled.")
options = append(options, profile.ThreadcreationProfile)
case "mutex":
log.Printf("Mutex profiling is enabled.")
a.Logger.Debug("Mutex profiling is enabled.")
options = append(options, profile.MutexProfile)
}
@ -571,26 +584,22 @@ func enableProfiler(profileOption string) interface{ Stop() } {
return nil
}
func updateInitError(target d2interface.Surface) error {
err := target.Clear(colornames.Darkred)
if err != nil {
return err
}
func (a *App) updateInitError(target d2interface.Surface) error {
target.Clear(colornames.Darkred)
target.PushTranslation(errMsgPadding, errMsgPadding)
target.DrawTextf(`Could not find the MPQ files in the directory:
%s\nPlease put the files and re-run the game.`, d2config.Config.MpqPath)
target.DrawTextf(a.errorMessage.Error())
return nil
}
// 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}
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 {
log.Print(err)
a.Error(err.Error())
return
}
@ -599,9 +608,9 @@ func (a *App) ToMainMenu() {
// ToSelectHero forces the game to transition to the Select Hero (create character) screen
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 {
log.Print(err)
a.Error(err.Error())
return
}
@ -610,34 +619,49 @@ func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType,
// ToCreateGame forces the game to transition to the Create Game screen
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 {
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 {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/805
fmt.Printf("can not connect to the host: %s", host)
}
errorMessage := fmt.Sprintf("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.audio, gameClient, a.terminal, a.guiManager))
a.screen.SetNextScreen(game)
}
}
// ToCharacterSelect forces the game to transition to the Character Select (load character) screen
func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/790
characterSelect := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager,
a.audio, a.ui, connType, connHost)
characterSelect, err := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager,
a.audio, a.ui, connType, *a.Options.LogLevel, connHost)
if err != nil {
a.Errorf("unable to create character select screen: %s", err)
}
a.screen.SetNextScreen(characterSelect)
}
// ToMapEngineTest forces the game to transition to the map engine test screen
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 {
log.Print(err)
a.Error(err.Error())
return
}
@ -646,5 +670,10 @@ func (a *App) ToMapEngineTest(region, level int) {
// ToCredits forces the game to transition to the credits screen
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
View 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
View 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
}

View 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")
}
}

View File

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

View File

@ -401,10 +401,10 @@ Loop:
case 257:
newvalue := bitstream.ReadBits(8)
outputstream.PushByte(byte(newvalue))
outputstream.PushBytes(byte(newvalue))
tail = insertNode(tail, newvalue)
default:
outputstream.PushByte(byte(decoded))
outputstream.PushBytes(byte(decoded))
}
}

View File

@ -6,7 +6,7 @@ import (
// WavDecompress decompresses wav files
//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}
Array2 := make([]int, channelCount)
@ -35,20 +35,33 @@ func WavDecompress(data []byte, channelCount int) []byte { //nolint:funlen,gocog
input := d2datautils.CreateStreamReader(data)
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++ {
temp := input.GetInt16()
temp, err := input.ReadInt16()
if err != nil {
return nil, err
}
Array2[i] = int(temp)
output.PushInt16(temp)
}
channel := channelCount - 1
for input.GetPosition() < input.GetSize() {
value := input.GetByte()
for input.Position() < input.Size() {
value, err := input.ReadByte()
if err != nil {
return nil, err
}
if channelCount == 2 {
channel = 1 - channel
@ -129,5 +142,5 @@ func WavDecompress(data []byte, channelCount int) []byte { //nolint:funlen,gocog
}
}
return output.GetBytes()
return output.GetBytes(), nil
}

View File

@ -1,6 +1,7 @@
package d2video
import (
"errors"
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
@ -29,6 +30,12 @@ const (
BinkVideoModeWidthAndHeightInterlaced
)
const (
numHeaderBytes = 3
bikHeaderStr = "BIK"
numAudioTrackUnknownBytes = 2
)
// BinkAudioAlgorithm represents the type of bink audio algorithm
type BinkAudioAlgorithm uint32
@ -72,75 +79,157 @@ type BinkDecoder struct {
}
// CreateBinkDecoder returns a new instance of the bink decoder
func CreateBinkDecoder(source []byte) *BinkDecoder {
func CreateBinkDecoder(source []byte) (*BinkDecoder, error) {
result := &BinkDecoder{
streamReader: d2datautils.CreateStreamReader(source),
}
result.loadHeaderInformation()
err := result.loadHeaderInformation()
return result
return result, err
}
// GetNextFrame gets the next frame
func (v *BinkDecoder) GetNextFrame() {
func (v *BinkDecoder) GetNextFrame() error {
//nolint:gocritic // v.streamReader.SetPosition(uint64(v.FrameIndexTable[i] & 0xFFFFFFFE))
lengthOfAudioPackets := v.streamReader.GetUInt32() - 4 //nolint:gomnd // decode magic
samplesInPacket := v.streamReader.GetUInt32()
lengthOfAudioPackets, err := v.streamReader.ReadUInt32()
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)
v.frameIndex++
return nil
}
//nolint:gomnd // Decoder magic
func (v *BinkDecoder) loadHeaderInformation() {
//nolint:gomnd,funlen,gocyclo // Decoder magic, can't help the long function length for now
func (v *BinkDecoder) loadHeaderInformation() error {
v.streamReader.SetPosition(0)
headerBytes := v.streamReader.ReadBytes(3)
if string(headerBytes) != "BIK" {
log.Fatal("Invalid header for bink video")
var err error
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.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.HasAlphaPlane = ((videoFlags >> 20) & 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)
for i := 0; i < int(numberOfAudioTracks); i++ {
v.streamReader.SkipBytes(2) // Unknown
v.AudioTracks[i].AudioChannels = v.streamReader.GetUInt16()
v.streamReader.SkipBytes(numAudioTrackUnknownBytes)
v.AudioTracks[i].AudioChannels, err = v.streamReader.ReadUInt16()
if err != nil {
return err
}
}
for i := 0; i < int(numberOfAudioTracks); i++ {
v.AudioTracks[i].AudioSampleRateHz = v.streamReader.GetUInt16()
flags := v.streamReader.GetUInt16()
v.AudioTracks[i].AudioSampleRateHz, err = v.streamReader.ReadUInt16()
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].Algorithm = BinkAudioAlgorithm((flags >> 12) & 0x1)
}
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)
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
}

View 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")
}

View File

@ -39,6 +39,7 @@ func (v *BitStream) ReadBits(bitCount int) int {
return -1
}
// nolint:gomnd // byte expresion
result := v.current & (0xffff >> uint(maxBits-bitCount))
v.WasteBits(bitCount)
@ -51,6 +52,7 @@ func (v *BitStream) PeekByte() int {
return -1
}
// nolint:gomnd // byte
return v.current & 0xff
}

View File

@ -5,9 +5,9 @@ import (
)
const (
bytesPerInt16 = 2
bytesPerInt32 = 4
bytesPerInt64 = 8
bytesPerint16 = 2
bytesPerint32 = 4
bytesPerint64 = 8
)
// StreamReader allows you to read data from a byte array in various formats
@ -26,50 +26,72 @@ func CreateStreamReader(source []byte) *StreamReader {
return result
}
// GetPosition returns the current stream position
func (v *StreamReader) GetPosition() uint64 {
return v.position
}
// ReadByte reads a byte from the stream
func (v *StreamReader) ReadByte() (byte, error) {
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]
v.position++
return result
return result, nil
}
// GetUInt16 returns a uint16 word from the stream
func (v *StreamReader) GetUInt16() uint16 {
var result uint16
for offset := uint64(0); offset < bytesPerInt16; offset++ {
shift := uint8(bitsPerByte * offset)
result += uint16(v.data[v.position+offset]) << shift
}
v.position += bytesPerInt16
return result
// ReadInt16 returns a int16 word from the stream
func (v *StreamReader) ReadInt16() (int16, error) {
b, err := v.ReadUInt16()
return int16(b), err
}
// GetInt16 returns a int16 word from the stream
func (v *StreamReader) GetInt16() int16 {
var result int16
for offset := uint64(0); offset < bytesPerInt16; offset++ {
shift := uint8(bitsPerByte * offset)
result += int16(v.data[v.position+offset]) << shift
// ReadUInt16 returns a uint16 word from the stream
func (v *StreamReader) ReadUInt16() (uint16, error) {
b, err := v.ReadBytes(bytesPerint16)
if err != nil {
return 0, err
}
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
@ -77,64 +99,26 @@ func (v *StreamReader) SetPosition(newPosition uint64) {
v.position = newPosition
}
// GetUInt32 returns a uint32 dword from the stream
func (v *StreamReader) GetUInt32() uint32 {
var result uint32
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
// Size returns the total size of the stream in bytes
func (v *StreamReader) Size() uint64 {
return uint64(len(v.data))
}
// 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)]
v.position += uint64(count)
return result
return result, nil
}
// SkipBytes moves the stream position forward by the given amount
@ -144,10 +128,10 @@ func (v *StreamReader) SkipBytes(count int) {
// Read implements io.Reader
func (v *StreamReader) Read(p []byte) (n int, err error) {
streamLength := v.GetSize()
streamLength := v.Size()
for i := 0; ; i++ {
if v.GetPosition() >= streamLength {
if v.Position() >= streamLength {
return i, io.EOF
}
@ -155,7 +139,10 @@ func (v *StreamReader) Read(p []byte) (n int, err error) {
return i, nil
}
p[i] = v.GetByte()
p[i], err = v.ReadByte()
if err != nil {
return i, err
}
}
}

View File

@ -8,22 +8,26 @@ func TestStreamReaderByte(t *testing.T) {
data := []byte{0x78, 0x56, 0x34, 0x12}
sr := CreateStreamReader(data)
if sr.GetPosition() != 0 {
t.Fatal("StreamReader.GetPosition() did not start at 0")
if sr.Position() != 0 {
t.Fatal("StreamReader.Position() did not start at 0")
}
if ss := sr.GetSize(); ss != 4 {
t.Fatalf("StreamREader.GetSize() was expected to return %d, but returned %d instead", 4, ss)
if ss := sr.Size(); ss != 4 {
t.Fatalf("StreamREader.Size() was expected to return %d, but returned %d instead", 4, ss)
}
for i := 0; i < len(data); i++ {
ret := sr.GetByte()
ret, err := sr.ReadByte()
if err != nil {
t.Error(err)
}
if ret != data[i] {
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", data[i], ret)
}
if pos := sr.GetPosition(); pos != uint64(i+1) {
t.Fatalf("StreamReader.GetPosition() should be at %d, but was at %d instead", i, pos)
if pos := sr.Position(); pos != uint64(i+1) {
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) {
data := []byte{0x78, 0x56, 0x34, 0x12}
sr := CreateStreamReader(data)
ret := sr.GetUInt16()
ret, err := sr.ReadUInt16()
if err != nil {
t.Error(err)
}
if ret != 0x5678 {
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", 0x5678, ret)
}
if pos := sr.GetPosition(); pos != 2 {
t.Fatalf("StreamReader.GetPosition() should be at %d, but was at %d instead", 2, pos)
if pos := sr.Position(); pos != 2 {
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 {
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", 0x1234, ret)
}
if pos := sr.GetPosition(); pos != 4 {
t.Fatalf("StreamReader.GetPosition() should be at %d, but was at %d instead", 4, pos)
if pos := sr.Position(); pos != 4 {
t.Fatalf("StreamReader.Position() should be at %d, but was at %d instead", 4, pos)
}
}
func TestStreamReaderDword(t *testing.T) {
data := []byte{0x78, 0x56, 0x34, 0x12}
sr := CreateStreamReader(data)
ret := sr.GetUInt32()
ret, err := sr.ReadUInt32()
if err != nil {
t.Error(err)
}
if ret != 0x12345678 {
t.Fatalf("StreamReader.GetDword() was expected to return %X, but returned %X instead", 0x12345678, ret)
}
if pos := sr.GetPosition(); pos != 4 {
t.Fatalf("StreamReader.GetPosition() should be at %d, but was at %d instead", 4, pos)
if pos := sr.Position(); pos != 4 {
t.Fatalf("StreamReader.Position() should be at %d, but was at %d instead", 4, pos)
}
}

View File

@ -1,14 +1,15 @@
package d2datautils
import "bytes"
const (
byteMask = 0xFF
import (
"bytes"
"log"
)
// StreamWriter allows you to create a byte array by streaming in writes of various sizes
type StreamWriter struct {
data *bytes.Buffer
data *bytes.Buffer
bitOffset int
bitCache byte
}
// CreateStreamWriter creates a new StreamWriter instance
@ -20,41 +21,102 @@ func CreateStreamWriter() *StreamWriter {
return result
}
// PushByte writes a byte to the stream
func (v *StreamWriter) PushByte(val byte) {
v.data.WriteByte(val)
// GetBytes returns the the byte slice of the underlying data
func (v *StreamWriter) GetBytes() []byte {
return v.data.Bytes()
}
// PushUint16 writes an uint16 word to the stream
func (v *StreamWriter) PushUint16(val uint16) {
for count := 0; count < bytesPerInt16; count++ {
shift := count * bitsPerByte
v.data.WriteByte(byte(val>>shift) & byteMask)
// PushBytes writes a bytes to the stream
func (v *StreamWriter) PushBytes(b ...byte) {
for _, i := range b {
v.data.WriteByte(i)
}
}
// 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
func (v *StreamWriter) PushInt16(val int16) {
for count := 0; count < bytesPerInt16; count++ {
shift := count * bitsPerByte
v.data.WriteByte(byte(val>>shift) & byteMask)
}
v.PushUint16(uint16(val))
}
// 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
//nolint
func (v *StreamWriter) PushUint32(val uint32) {
for count := 0; count < bytesPerInt32; count++ {
shift := count * bitsPerByte
v.data.WriteByte(byte(val>>shift) & byteMask)
}
}
// 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)
}
v.data.WriteByte(byte(val))
v.data.WriteByte(byte(val >> 8))
v.data.WriteByte(byte(val >> 16))
v.data.WriteByte(byte(val >> 24))
}
// PushInt64 writes a uint64 qword to the stream
@ -62,7 +124,15 @@ func (v *StreamWriter) PushInt64(val int64) {
v.PushUint64(uint64(val))
}
// GetBytes returns the the byte slice of the underlying data
func (v *StreamWriter) GetBytes() []byte {
return v.data.Bytes()
// PushUint64 writes a uint64 qword to the stream
//nolint
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))
}

View File

@ -4,18 +4,75 @@ import (
"testing"
)
func TestStreamWriterByte(t *testing.T) {
func TestStreamWriterBits(t *testing.T) {
sr := CreateStreamWriter()
data := []byte{0x12, 0x34, 0x56, 0x78}
data := []byte{221, 19}
for _, d := range data {
sr.PushByte(d)
for _, i := range data {
sr.PushBits(i, bitsPerByte)
}
output := sr.GetBytes()
for i, d := range data {
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])
}
}
}

View File

@ -1,5 +1,9 @@
package d2enum
const (
unknown = "Unknown"
)
//go:generate stringer -linecomment -type CompositeType -output composite_type_string.go
// CompositeType represents a composite type
@ -25,3 +29,32 @@ const (
CompositeTypeSpecial8 // S8
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
}

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

View File

@ -45,3 +45,24 @@ const (
func (d DrawEffect) Transparent() bool {
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
}

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

View File

@ -3,212 +3,117 @@ package d2enum
// Key represents button on a traditional keyboard.
type Key int
// Input keys
const (
// Key0 is the number 0
Key0 Key = iota
// Key1 is the number 1
Key1
// Key2 is the number 2
Key2
// Key3 is the number 3
Key3
// Key4 is the number 4
Key4
// Key5 is the number 5
Key5
// Key6 is the number 6
Key6
// Key7 is the number 7
Key7
// Key8 is the number 8
Key8
// Key9 is the number 9
Key9
// KeyA is the letter A
KeyA
// KeyB is the letter B
KeyB
// KeyC is the letter C
KeyC
// KeyD is the letter D
KeyD
// KeyE is the letter E
KeyE
// KeyF is the letter F
KeyF
// KeyG is the letter G
KeyG
// KeyH is the letter H
KeyH
// KeyI is the letter I
KeyI
// KeyJ is the letter J
KeyJ
// KeyK is the letter K
KeyK
// KeyL is the letter L
KeyL
// KeyM is the letter M
KeyM
// KeyN is the letter N
KeyN
// KeyO is the letter O
KeyO
// KeyP is the letter P
KeyP
// KeyQ is the letter Q
KeyQ
// KeyR is the letter R
KeyR
// KeyS is the letter S
KeyS
// KeyT is the letter T
KeyT
// KeyU is the letter U
KeyU
// KeyV is the letter V
KeyV
// KeyW is the letter W
KeyW
// KeyX is the letter X
KeyX
// KeyY is the letter Y
KeyY
// KeyZ is the letter Z
KeyZ
// KeyApostrophe is the Apostrophe
KeyApostrophe
// KeyBackslash is the Backslash
KeyBackslash
// KeyBackspace is the Backspace
KeyBackspace
// KeyCapsLock is the CapsLock
KeyCapsLock
// KeyComma is the Comma
KeyComma
// KeyDelete is the Delete
KeyDelete
// KeyDown is the down arrow key
KeyDown
// KeyEnd is the End
KeyEnd
// KeyEnter is the Enter
KeyEnter
// KeyEqual is the Equal
KeyEqual
// KeyEscape is the Escape
KeyEscape
// KeyF1 is the function F1
KeyF1
// KeyF2 is the function F2
KeyF2
// KeyF3 is the function F3
KeyF3
// KeyF4 is the function F4
KeyF4
// KeyF5 is the function F5
KeyF5
// KeyF6 is the function F6
KeyF6
// KeyF7 is the function F7
KeyF7
// KeyF8 is the function F8
KeyF8
// KeyF9 is the function F9
KeyF9
// KeyF10 is the function F10
KeyF10
// KeyF11 is the function F11
KeyF11
// KeyF12 is the function F12
KeyF12
// KeyGraveAccent is the Grave Accent
KeyGraveAccent
// KeyHome is the home key
KeyHome
// KeyInsert is the insert key
KeyInsert
// KeyKP0 is keypad 0
KeyKP0
// KeyKP1 is keypad 1
KeyKP1
// KeyKP2 is keypad 2
KeyKP2
// KeyKP3 is keypad 3
KeyKP3
// KeyKP4 is keypad 4
KeyKP4
// KeyKP5 is keypad 5
KeyKP5
// KeyKP6 is keypad 6
KeyKP6
// KeyKP7 is keypad 7
KeyKP7
// KeyKP8 is keypad 8
KeyKP8
// KeyKP9 is keypad 9
KeyKP9
// KeyKPAdd is keypad Add
KeyKPAdd
// KeyKPDecimal is keypad Decimal
KeyKPDecimal
// KeyKPDivide is keypad Divide
KeyKPDivide
// KeyKPEnter is keypad Enter
KeyKPEnter
// KeyKPEqual is keypad Equal
KeyKPEqual
// KeyKPMultiply is keypad Multiply
KeyKPMultiply
// KeyKPSubtract is keypad Subtract
KeyKPSubtract
// KeyLeft is the left arrow key
KeyLeft
// KeyLeftBracket is the left bracket
KeyLeftBracket
// KeyMenu is the Menu key
KeyMenu
// KeyMinus is the Minus key
KeyMinus
// KeyNumLock is the NumLock key
KeyNumLock
// KeyPageDown is the PageDown key
KeyPageDown
// KeyPageUp is the PageUp key
KeyPageUp
// KeyPause is the Pause key
KeyPause
// KeyPeriod is the Period key
KeyPeriod
// KeyPrintScreen is the PrintScreen key
KeyPrintScreen
// KeyRight is the right arrow key
KeyRight
// KeyRightBracket is the right bracket key
KeyRightBracket
// KeyScrollLock is the scroll lock key
KeyScrollLock
// KeySemicolon is the semicolon key
KeySemicolon
// KeySlash is the front slash key
KeySlash
// KeySpace is the space key
KeySpace
// KeyTab is the tab key
KeyTab
// KeyUp is the up arrow key
KeyUp
// KeyAlt is the alt key
KeyAlt
// KeyControl is the control key
KeyControl
// KeyShift is the shift key
KeyShift
KeyTilde
KeyMouse3
KeyMouse4
KeyMouse5
KeyMouseWheelUp
KeyMouseWheelDown
// KeyMin is the lowest key
KeyMin = Key0
// KeyMax is the highest key
KeyMax = KeyShift
KeyMax = KeyMouseWheelDown
)
// KeyMod represents a "modified" key action. This could mean, for example, ctrl-S

View File

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

View 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]
}

View File

@ -0,0 +1,11 @@
package d2enum
// Frames of party Buttons
const (
PartyButtonListeningFrame = iota * 4
PartyButtonRelationshipsFrame
PartyButtonSeeingFrame
PartyButtonCorpsLootingFrame
PartyButtonNextButtonFrame = 2
)

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

View File

@ -69,3 +69,36 @@ func (tile TileType) Special() bool {
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
}

View File

@ -24,3 +24,31 @@ const (
WeaponClassOneHandToHand // ht1
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
}

View File

@ -56,7 +56,68 @@ func (ad *AnimationData) GetRecords(name string) []*AnimationDataRecord {
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
//nolint:gocognit,funlen // can't reduce
func Load(data []byte) (*AnimationData, error) {
reader := d2datautils.CreateStreamReader(data)
animdata := &AnimationData{}
@ -65,7 +126,11 @@ func Load(data []byte) (*AnimationData, error) {
animdata.entries = make(map[string][]*AnimationDataRecord)
for blockIdx := range animdata.blocks {
recordCount := reader.GetUInt32()
recordCount, err := reader.ReadUInt32()
if err != nil {
return nil, err
}
if recordCount > 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)
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) {
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)
frames := reader.GetUInt32()
speed := reader.GetUInt16()
frames, err := reader.ReadUInt32()
if err != nil {
return nil, err
}
speed, err := reader.ReadUInt16()
if err != nil {
return nil, err
}
reader.SkipBytes(byteCountSpeedPadding)
events := make(map[int]AnimationEvent)
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 {
events[eventIdx] = event
}
@ -122,9 +202,93 @@ func Load(data []byte) (*AnimationData, error) {
animdata.blocks[blockIdx] = b
}
if reader.GetPosition() != uint64(len(data)) {
return nil, errors.New("unable to parse animation data")
if reader.Position() != uint64(len(data)) {
return nil, fmt.Errorf("unable to parse animation data: %d != %d", reader.Position(), len(data))
}
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()
}

View File

@ -154,3 +154,137 @@ func TestAnimationDataRecord_FPS(t *testing.T) {
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")
}
}

View File

@ -8,6 +8,26 @@ type AnimationDataRecord struct {
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
func (r *AnimationDataRecord) FPS() float64 {
speedf := float64(r.speed)
@ -21,3 +41,23 @@ func (r *AnimationDataRecord) FPS() float64 {
func (r *AnimationDataRecord) FrameDurationMS() float64 {
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
}

View File

@ -7,8 +7,68 @@ import (
"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.
type COF struct {
// unknown bytes for header
unknownHeaderBytes []byte
// unknown bytes (first "body's" bytes)
unknownBodyBytes []byte
NumberOfDirections int
FramesPerDirection int
NumberOfLayers int
@ -19,58 +79,166 @@ type COF struct {
Priority [][][]d2enum.CompositeType
}
// Load loads a COF file.
func Load(fileData []byte) (*COF, error) {
result := &COF{}
// Unmarshal a byte slice to this COF
func (c *COF) Unmarshal(fileData []byte) error {
var err error
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)
result.CompositeLayers = make(map[d2enum.CompositeType]int)
c.CofLayers = make([]CofLayer, c.NumberOfLayers)
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.Type = d2enum.CompositeType(streamReader.GetByte())
layer.Shadow = streamReader.GetByte()
layer.Selectable = streamReader.GetByte() != 0
layer.Transparent = streamReader.GetByte() != 0
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
b, err := streamReader.ReadBytes(numLayerBytes)
if err != nil {
return err
}
layer.Type = d2enum.CompositeType(b[layerType])
layer.Shadow = b[layerShadow]
layer.Selectable = b[layerSelectable] > 0
layer.Transparent = b[layerTransparent] > 0
layer.DrawEffect = d2enum.DrawEffect(b[layerDrawEffect])
layer.WeaponClass = d2enum.WeaponClassFromString(strings.TrimSpace(strings.ReplaceAll(
string(b[layerWeaponClass:]), badCharacter, "")))
c.CofLayers[i] = layer
c.CompositeLayers[layer.Type] = i
}
animationFrameBytes := streamReader.ReadBytes(result.FramesPerDirection)
result.AnimationFrames = make([]d2enum.AnimationFrame, result.FramesPerDirection)
return nil
}
for i := range animationFrameBytes {
result.AnimationFrames[i] = d2enum.AnimationFrame(animationFrameBytes[i])
func (c *COF) loadAnimationFrames(b []byte) {
c.AnimationFrames = make([]d2enum.AnimationFrame, c.FramesPerDirection)
for i := range b {
c.AnimationFrames[i] = d2enum.AnimationFrame(b[i])
}
}
priorityLen := result.FramesPerDirection * result.NumberOfDirections * result.NumberOfLayers
result.Priority = make([][][]d2enum.CompositeType, result.NumberOfDirections)
priorityBytes := streamReader.ReadBytes(priorityLen)
func (c *COF) loadPriority(priorityBytes []byte) {
priorityIndex := 0
for direction := 0; direction < result.NumberOfDirections; direction++ {
result.Priority[direction] = make([][]d2enum.CompositeType, result.FramesPerDirection)
for frame := 0; frame < result.FramesPerDirection; frame++ {
result.Priority[direction][frame] = make([]d2enum.CompositeType, result.NumberOfLayers)
for i := 0; i < result.NumberOfLayers; i++ {
result.Priority[direction][frame][i] = d2enum.CompositeType(priorityBytes[priorityIndex])
for direction := 0; direction < c.NumberOfDirections; direction++ {
c.Priority[direction] = make([][]d2enum.CompositeType, c.FramesPerDirection)
for frame := 0; frame < c.FramesPerDirection; frame++ {
c.Priority[direction][frame] = make([]d2enum.CompositeType, c.NumberOfLayers)
for i := 0; i < c.NumberOfLayers; i++ {
c.Priority[direction][frame][i] = d2enum.CompositeType(priorityBytes[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()
}

View 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")
}
}

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

View File

@ -1,6 +1,8 @@
package d2dat
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
const (
// index offset helpers
@ -21,3 +23,14 @@ func Load(data []byte) (d2interface.Palette, error) {
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
}

View File

@ -15,6 +15,16 @@ type DATPalette struct {
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
func (p *DATPalette) NumColors() int {
return len(p.colors)

View File

@ -7,6 +7,9 @@ import (
const (
endOfScanLine = 0x80
maxRunLength = 0x7f
terminationSize = 4
terminatorSize = 3
)
type scanlineState int
@ -29,49 +32,179 @@ type DC6 struct {
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) {
const (
terminationSize = 4
terminatorSize = 3
)
d := New()
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)
var dc DC6
dc.Version = r.GetInt32()
dc.Flags = r.GetUInt32()
dc.Encoding = r.GetUInt32()
dc.Termination = r.ReadBytes(terminationSize)
dc.Directions = r.GetUInt32()
dc.FramesPerDirection = r.GetUInt32()
frameCount := int(dc.Directions * dc.FramesPerDirection)
dc.FramePointers = make([]uint32, frameCount)
for i := 0; i < frameCount; i++ {
dc.FramePointers[i] = r.GetUInt32()
err = d.loadHeader(r)
if err != nil {
return err
}
dc.Frames = make([]*DC6Frame, frameCount)
frameCount := int(d.Directions * d.FramesPerDirection)
d.FramePointers = make([]uint32, frameCount)
for i := 0; i < frameCount; i++ {
frame := &DC6Frame{
Flipped: r.GetUInt32(),
Width: r.GetUInt32(),
Height: r.GetUInt32(),
OffsetX: r.GetInt32(),
OffsetY: r.GetInt32(),
Unknown: r.GetUInt32(),
NextBlock: r.GetUInt32(),
Length: r.GetUInt32(),
d.FramePointers[i], err = r.ReadUInt32()
if err != nil {
return err
}
frame.FrameData = r.ReadBytes(int(frame.Length))
frame.Terminator = r.ReadBytes(terminatorSize)
dc.Frames[i] = frame
}
return &dc, nil
d.Frames = make([]*DC6Frame, frameCount)
if err := d.loadFrames(r); err != nil {
return err
}
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
@ -134,7 +267,7 @@ func (d *DC6) Clone() *DC6 {
for i := range d.Frames {
cloneFrame := *d.Frames[i]
clone.Frames = append(clone.Frames, &cloneFrame)
clone.Frames[i] = &cloneFrame
}
return &clone

View 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")
}
}

View File

@ -9,6 +9,13 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
)
const (
baseMinx = 100000
baseMiny = 100000
baseMaxx = -100000
baseMaxy = -100000
)
const cellsPerRow = 4
// DCCDirection represents a DCCDirection file.
@ -37,7 +44,9 @@ type DCCDirection struct {
}
// CreateDCCDirection creates an instance of a DCCDirection.
// nolint:funlen // no need to reduce
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}
result := &DCCDirection{
@ -53,10 +62,10 @@ func CreateDCCDirection(bm *d2datautils.BitMuncher, file *DCC) *DCCDirection {
Frames: make([]*DCCDirectionFrame, file.FramesPerDirection),
}
minx := 100000
miny := 100000
maxx := -100000
maxy := -100000
minx := baseMinx
miny := baseMiny
maxx := baseMaxx
maxy := baseMaxy
// Load the frame headers
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.")
}
// nolint:gomnd // byte operation
if (result.CompressionFlags & 0x2) > 0 {
result.EqualCellsBitstreamSize = 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 {
result.EncodingTypeBitsreamSize = 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++ {
cellWidths[i] = 4
}
// nolint:gomnd // constant
cellWidths[v.HorizontalCellCount-1] = v.Box.Width - (4 * (v.HorizontalCellCount - 1))
}
// Calculate the cell heights
// nolint:gomnd // constant
cellHeights := make([]int, v.VerticalCellCount)
if v.VerticalCellCount == 1 {
cellHeights[0] = v.Box.Height
@ -422,6 +436,8 @@ func (v *DCCDirection) calculateCells() {
for i := 0; i < v.VerticalCellCount-1; i++ {
cellHeights[i] = 4
}
// nolint:gomnd // constant
cellHeights[v.VerticalCellCount-1] = v.Box.Height - (4 * (v.VerticalCellCount - 1))
}
// Set the cell widths and heights in the cell buffer

View File

@ -55,6 +55,7 @@ func CreateDCCDirectionFrame(bits *d2datautils.BitMuncher, direction *DCCDirecti
}
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)
if (v.Width - w) <= 1 {
@ -62,6 +63,8 @@ func (v *DCCDirectionFrame) recalculateCells(direction *DCCDirection) {
} else {
tmp := v.Width - w - 1
v.HorizontalCellCount = 2 + (tmp / 4) //nolint:gomnd // magic math
// nolint:gomnd // constant
if (tmp % 4) == 0 {
v.HorizontalCellCount--
}
@ -75,6 +78,8 @@ func (v *DCCDirectionFrame) recalculateCells(direction *DCCDirection) {
} else {
tmp := v.Height - h - 1
v.VerticalCellCount = 2 + (tmp / 4) //nolint:gomnd // data decode
// nolint:gomnd // constant
if (tmp % 4) == 0 {
v.VerticalCellCount--
}
@ -88,6 +93,8 @@ func (v *DCCDirectionFrame) recalculateCells(direction *DCCDirection) {
for i := 1; i < (v.HorizontalCellCount - 1); i++ {
cellWidths[i] = 4
}
// nolint:gomnd // constants
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++ {
cellHeights[i] = 4
}
// nolint:gomnd // constants
cellHeights[v.VerticalCellCount-1] = v.Height - h - (4 * (v.VerticalCellCount - 2))
}

View File

@ -1,2 +1,2 @@
// Package d2ds1 provides functionality for loading/processing DS1 files
// Package d2ds1 provides functionality for loading/processing DS1 Files
package d2ds1

View File

@ -1,6 +1,8 @@
package d2ds1
import (
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
@ -8,290 +10,683 @@ import (
"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.
type DS1 struct {
Files []string // FilePtr table of file string pointers
Objects []Object // Objects
Tiles [][]TileRecord // The tile data 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
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
SubstitutionType int32 // SubstitutionType (layer type): 0 if no layer, else type 1 or type 2
NumberOfWalls int32 // WallNum number of wall & orientation layers used
NumberOfFloors int32 // number of floor layers used
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
*ds1Layers
Files []string // FilePtr table of file string pointers
Objects []Object // Objects
SubstitutionGroups []SubstitutionGroup // Substitution groups for the DS1
version ds1version
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
unknown1 [unknown1BytesCount]byte
unknown2 uint32
}
// LoadDS1 loads the specified DS1 file
func LoadDS1(fileData []byte) (*DS1, error) {
ds1 := &DS1{
Act: 1,
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
const (
defaultNumFloors = 1
defaultNumShadows = maxShadowLayers
defaultNumSubstitutions = 0
)
if ds1.Version >= 8 { //nolint:gomnd // Version number
ds1.Act = d2math.MinInt32(maxActNumber, br.GetInt32()+1)
// Unmarshal the given bytes to a DS1 struct
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
ds1.SubstitutionType = br.GetInt32()
if ds1.SubstitutionType == 1 || ds1.SubstitutionType == 2 {
ds1.NumberOfSubstitutionLayers = 1
}
if err := ds1.loadBody(stream); err != nil {
return nil, fmt.Errorf("loading body: %w", err)
}
if ds1.Version >= 3 { //nolint:gomnd // Version number
// These files reference things that don't exist anymore :-?
numberOfFiles := br.GetInt32()
ds1.Files = make([]string, numberOfFiles)
for i := 0; i < int(numberOfFiles); i++ {
ds1.Files[i] = ""
for {
ch := br.GetByte()
if ch == 0 {
break
}
ds1.Files[i] += string(ch)
}
}
}
if ds1.Version >= 9 && ds1.Version <= 13 {
// 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) {
if ds1.Version >= 2 { //nolint:gomnd // Version number
numberOfObjects := br.GetInt32()
ds1.Objects = make([]Object, numberOfObjects)
func (ds1 *DS1) loadHeader(br *d2datautils.StreamReader) error {
var err error
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())
var width, height int32
ds1.Objects[objIdx] = newObject
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)
}
} else {
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{})
}
}
err = ds1.loadFileList(br)
if err != nil {
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)
for i := 0; i < int(numberOfFiles); i++ {
ds1.Files[i] = ""
for {
ch, err := br.ReadByte()
if err != nil {
return fmt.Errorf("reading file character: %w", err)
}
if ch == 0 {
break
}
ds1.Files[i] += string(ch)
}
}
return nil
}
func (ds1 *DS1) loadObjects(br *d2datautils.StreamReader) error {
if !ds1.version.hasObjects() {
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) {
if ds1.Version >= 12 && (ds1.SubstitutionType == 1 || ds1.SubstitutionType == 2) {
if ds1.Version >= 18 { //nolint:gomnd // Version number
br.GetUInt32()
}
func (ds1 *DS1) loadSubstitutions(br *d2datautils.StreamReader) error {
var err error
numberOfSubGroups := br.GetInt32()
ds1.SubstitutionGroups = make([]SubstitutionGroup, numberOfSubGroups)
hasSubstitutions := ds1.version.hasSubstitutions() &&
(ds1.SubstitutionType == subType1 || ds1.SubstitutionType == subType2)
for subIdx := 0; subIdx < int(numberOfSubGroups); subIdx++ {
newSub := SubstitutionGroup{}
newSub.TileX = br.GetInt32()
newSub.TileY = br.GetInt32()
newSub.WidthInTiles = br.GetInt32()
newSub.HeightInTiles = br.GetInt32()
newSub.Unknown = br.GetInt32()
ds1.SubstitutionGroups[subIdx] = newSub
}
} else {
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)
}
ds1.SubstitutionGroups = make([]SubstitutionGroup, numberOfSubGroups)
for subIdx := 0; subIdx < int(numberOfSubGroups); subIdx++ {
newSub := SubstitutionGroup{}
newSub.TileX, err = br.ReadInt32()
if err != nil {
return fmt.Errorf("reading substitution's %d X: %v", subIdx, err)
}
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
}
return err
}
func (ds1 *DS1) setupStreamLayerTypes() []d2enum.LayerStreamType {
var layerStream []d2enum.LayerStreamType
func (ds1 *DS1) getLayerSchema() []layerStreamType {
var layerStream []layerStreamType
if ds1.Version < 4 { //nolint:gomnd // Version number
layerStream = []d2enum.LayerStreamType{
d2enum.LayerStreamWall1,
d2enum.LayerStreamFloor1,
d2enum.LayerStreamOrientation1,
d2enum.LayerStreamSubstitute,
d2enum.LayerStreamShadow,
if ds1.version.hasStandardLayers() {
layerStream = []layerStreamType{
layerStreamWall1,
layerStreamFloor1,
layerStreamOrientation1,
layerStreamSubstitute1,
layerStreamShadow1,
}
} else {
layerStream = make([]d2enum.LayerStreamType,
(ds1.NumberOfWalls*2)+ds1.NumberOfFloors+ds1.NumberOfShadowLayers+ds1.NumberOfSubstitutionLayers)
layerIdx := 0
for i := 0; i < int(ds1.NumberOfWalls); i++ {
layerStream[layerIdx] = d2enum.LayerStreamType(int(d2enum.LayerStreamWall1) + i)
layerStream[layerIdx+1] = d2enum.LayerStreamType(int(d2enum.LayerStreamOrientation1) + i)
layerIdx += 2
}
for i := 0; i < int(ds1.NumberOfFloors); i++ {
layerStream[layerIdx] = d2enum.LayerStreamType(int(d2enum.LayerStreamFloor1) + i)
layerIdx++
}
if ds1.NumberOfShadowLayers > 0 {
layerStream[layerIdx] = d2enum.LayerStreamShadow
layerIdx++
}
if ds1.NumberOfSubstitutionLayers > 0 {
layerStream[layerIdx] = d2enum.LayerStreamSubstitute
}
return layerStream
}
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
for i := 0; i < numWalls; i++ {
layerStream[layerIdx] = layerStreamType(int(layerStreamWall1) + i)
layerIdx++
layerStream[layerIdx] = layerStreamType(int(layerStreamOrientation1) + i)
layerIdx++
}
for i := 0; i < numFloors; i++ {
layerStream[layerIdx] = layerStreamType(int(layerStreamFloor1) + i)
layerIdx++
}
if numShadows > 0 {
layerStream[layerIdx] = layerStreamShadow1
layerIdx++
}
if numSubs > 0 {
layerStream[layerIdx] = layerStreamSubstitute1
}
return layerStream
}
func (ds1 *DS1) loadNPCs(br *d2datautils.StreamReader) {
if ds1.Version >= 14 { //nolint:gomnd // Version number
numberOfNpcs := br.GetInt32()
for npcIdx := 0; npcIdx < int(numberOfNpcs); npcIdx++ {
numPaths := br.GetInt32()
npcX := int(br.GetInt32())
npcY := int(br.GetInt32())
objIdx := -1
func (ds1 *DS1) loadNPCs(br *d2datautils.StreamReader) error {
var err error
for idx, ds1Obj := range ds1.Objects {
if ds1Obj.X == npcX && ds1Obj.Y == npcY {
objIdx = idx
break
}
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++ {
numPaths, err := br.ReadInt32() // nolint:govet // I want to re-use this error variable
if err != nil {
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
for idx, ds1Obj := range ds1.Objects {
if ds1Obj.X == int(npcX) && ds1Obj.Y == int(npcY) {
objIdx = idx
break
}
}
if objIdx > -1 {
ds1.loadNpcPaths(br, objIdx, int(numPaths))
if objIdx > -1 {
err = ds1.loadNpcPaths(br, objIdx, int(numPaths))
if err != nil {
return fmt.Errorf("loading paths for NPC %d: %v", npcIdx, err)
}
} else {
if ds1.version.specifiesNPCActions() {
br.SkipBytes(int(numPaths) * 3) //nolint:gomnd // Unknown data
} else {
if ds1.Version >= 15 { //nolint:gomnd // Version number
br.SkipBytes(int(numPaths) * 3) //nolint:gomnd // Unknown data
} 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 {
ds1.Objects[objIdx].Paths = make([]d2path.Path, numPaths)
}
for pathIdx := 0; pathIdx < numPaths; pathIdx++ {
newPath := d2path.Path{}
newPath.Position = d2vector.NewPosition(
float64(br.GetInt32()),
float64(br.GetInt32()))
if ds1.Version >= 15 { //nolint:gomnd // Version number
newPath.Action = int(br.GetInt32())
px, err := br.ReadInt32() //nolint:govet // i want to re-use the err variable...
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
}
return nil
}
func (ds1 *DS1) loadLayerStreams(br *d2datautils.StreamReader, layerStream []d2enum.LayerStreamType) {
var dirLookup = []int32{
func (ds1 *DS1) loadLayerStreams(br *d2datautils.StreamReader) error {
dirLookup := []int32{
0x00, 0x01, 0x02, 0x01, 0x02, 0x03, 0x03, 0x05, 0x05, 0x06,
0x06, 0x07, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10, 0x11, 0x12, 0x14,
}
for lIdx := range layerStream {
layerStreamType := layerStream[lIdx]
layerStreamTypes := ds1.getLayerSchema()
for y := 0; y < int(ds1.Height); y++ {
for x := 0; x < int(ds1.Width); x++ {
dw := br.GetUInt32()
for _, layerStreamType := range layerStreamTypes {
for y := 0; y < ds1.height; y++ {
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 {
case d2enum.LayerStreamWall1, d2enum.LayerStreamWall2, d2enum.LayerStreamWall3, d2enum.LayerStreamWall4:
wallIndex := int(layerStreamType) - int(d2enum.LayerStreamWall1)
ds1.Tiles[y][x].Walls[wallIndex].Prop1 = byte(dw & 0x000000FF) //nolint:gomnd // Bitmask
ds1.Tiles[y][x].Walls[wallIndex].Sequence = byte((dw & 0x00003F00) >> 8) //nolint:gomnd // Bitmask
ds1.Tiles[y][x].Walls[wallIndex].Unknown1 = byte((dw & 0x000FC000) >> 14) //nolint:gomnd // Bitmask
ds1.Tiles[y][x].Walls[wallIndex].Style = byte((dw & 0x03F00000) >> 20) //nolint:gomnd // Bitmask
ds1.Tiles[y][x].Walls[wallIndex].Unknown2 = byte((dw & 0x7C000000) >> 26) //nolint:gomnd // Bitmask
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
case layerStreamWall1, layerStreamWall2, layerStreamWall3, layerStreamWall4:
wallIndex := int(layerStreamType) - int(layerStreamWall1)
ds1.Walls[wallIndex].Tile(x, y).DecodeWall(dw)
case layerStreamOrientation1, layerStreamOrientation2,
layerStreamOrientation3, layerStreamOrientation4:
wallIndex := int(layerStreamType) - int(layerStreamOrientation1)
c := int32(dw & wallTypeBitmask)
if ds1.Version < 7 { //nolint:gomnd // Version number
if ds1.version < v7 {
if c < int32(len(dirLookup)) {
c = dirLookup[c]
}
}
ds1.Tiles[y][x].Walls[wallIndex].Type = d2enum.TileType(c)
ds1.Tiles[y][x].Walls[wallIndex].Zero = byte((dw & 0xFFFFFF00) >> 8) //nolint:gomnd // Bitmask
case d2enum.LayerStreamFloor1, d2enum.LayerStreamFloor2:
floorIndex := int(layerStreamType) - int(d2enum.LayerStreamFloor1)
ds1.Tiles[y][x].Floors[floorIndex].Prop1 = byte(dw & 0x000000FF) //nolint:gomnd // Bitmask
ds1.Tiles[y][x].Floors[floorIndex].Sequence = byte((dw & 0x00003F00) >> 8) //nolint:gomnd // Bitmask
ds1.Tiles[y][x].Floors[floorIndex].Unknown1 = byte((dw & 0x000FC000) >> 14) //nolint:gomnd // Bitmask
ds1.Tiles[y][x].Floors[floorIndex].Style = byte((dw & 0x03F00000) >> 20) //nolint:gomnd // Bitmask
ds1.Tiles[y][x].Floors[floorIndex].Unknown2 = byte((dw & 0x7C000000) >> 26) //nolint:gomnd // Bitmask
ds1.Tiles[y][x].Floors[floorIndex].Hidden = byte((dw&0x80000000)>>31) > 0 //nolint:gomnd // Bitmask
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
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
tile := ds1.Walls[wallIndex].Tile(x, y)
tile.Type = d2enum.TileType(c)
tile.Zero = byte((dw & wallZeroBitmask) >> wallZeroOffset)
case layerStreamFloor1, layerStreamFloor2:
floorIndex := int(layerStreamType) - int(layerStreamFloor1)
ds1.Floors[floorIndex].Tile(x, y).DecodeFloor(dw)
case layerStreamShadow1:
ds1.Shadows[0].Tile(x, y).DecodeShadow(dw)
case layerStreamSubstitute1:
ds1.Substitutions[0].Tile(x, y).Substitution = dw
}
}
}
}
return nil
}
// 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)
}

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

View 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)
}

View 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")
}
}
}

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

View File

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

View 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)
}

View 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())
}
}

View File

@ -13,3 +13,13 @@ type Object struct {
Flags int
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)
}

View File

@ -1,6 +0,0 @@
package d2ds1
// SubstitutionRecord represents a substitution record in a DS1 file.
type SubstitutionRecord struct {
Unknown uint32
}

View 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)
}

View File

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

View File

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

View File

@ -6,8 +6,25 @@ type Block struct {
Y int16
GridX byte
GridY byte
Format BlockDataFormat
format int16
EncodedData []byte
Length 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)
}

View File

@ -2,15 +2,11 @@ package d2dt1
import (
"fmt"
"sort"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
)
// DT1 represents a DT1 file.
type DT1 struct {
Tiles []Tile
}
// BlockDataFormat represents the format of the block data
type BlockDataFormat int16
@ -22,55 +18,169 @@ const (
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
//nolint:funlen // Can't reduce
//nolint:funlen,gocognit,gocyclo // Can't reduce
func LoadDT1(fileData []byte) (*DT1, error) {
result := &DT1{}
br := d2datautils.CreateStreamReader(fileData)
ver1 := br.GetInt32()
ver2 := br.GetInt32()
if ver1 != 7 || ver2 != 6 {
return nil, fmt.Errorf("expected to have a version of 7.6, but got %d.%d instead", ver1, ver2)
var err error
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()
br.SetPosition(uint64(br.GetInt32()))
if result.majorVersion != knownMajorVersion || result.minorVersion != knownMinorVersion {
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 {
newTile := Tile{}
newTile.Direction = br.GetInt32()
newTile.RoofHeight = br.GetInt16()
newTile.MaterialFlags = NewMaterialFlags(br.GetUInt16())
newTile.Height = br.GetInt32()
newTile.Width = br.GetInt32()
tile := Tile{}
br.SkipBytes(4) //nolint:gomnd // Unknown data
newTile.Type = br.GetInt32()
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())
tile.Direction, err = br.ReadInt32()
if err != nil {
return nil, err
}
br.SkipBytes(7) //nolint:gomnd // Unknown data
tile.RoofHeight, err = br.ReadInt16()
if err != nil {
return nil, err
}
newTile.blockHeaderPointer = br.GetInt32()
newTile.blockHeaderSize = br.GetInt32()
newTile.Blocks = make([]Block, br.GetInt32())
var matFlagBytes uint16
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 {
@ -78,34 +188,135 @@ func LoadDT1(fileData []byte) (*DT1, error) {
br.SetPosition(uint64(tile.blockHeaderPointer))
for blockIdx := range tile.Blocks {
result.Tiles[tileIdx].Blocks[blockIdx].X = br.GetInt16()
result.Tiles[tileIdx].Blocks[blockIdx].Y = br.GetInt16()
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].X, err = br.ReadInt16()
if err != nil {
return nil, err
}
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 {
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
}
}
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
}

View File

@ -7,7 +7,7 @@ const (
// DecodeTileGfxData decodes tile graphics data for a slice of dt1 blocks
func DecodeTileGfxData(blocks []Block, pixels *[]byte, tileYOffset, tileWidth int32) {
for _, block := range blocks {
if block.Format == BlockFormatIsometric {
if block.Format() == BlockFormatIsometric {
// 3D isometric decoding
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}

View File

@ -30,3 +30,50 @@ func NewMaterialFlags(data uint16) MaterialFlags {
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
}

View File

@ -77,3 +77,42 @@ func NewSubTileFlags(data byte) SubTileFlags {
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
}

View File

@ -2,6 +2,7 @@ package d2dt1
// Tile is a representation of a map tile
type Tile struct {
unknown2 []byte
Direction int32
RoofHeight int16
MaterialFlags MaterialFlags
@ -16,3 +17,15 @@ type Tile struct {
blockHeaderSize int32
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)
}

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

View File

@ -0,0 +1,3 @@
// Package d2font contains logic for loading and processing
// d2 fonts
package d2font

View 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()
}

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

View File

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

View File

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

View File

@ -2,12 +2,11 @@ package d2mpq
import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io/fs"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"runtime"
"strings"
@ -19,33 +18,11 @@ var _ d2interface.Archive = &MPQ{} // Static check to confirm struct conforms to
// MPQ represents an MPQ archive
type MPQ struct {
filePath string
file *os.File
hashEntryMap HashEntryMap
blockTableEntries []BlockTableEntry
data Data
}
// 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
filePath string
file *os.File
hashes map[uint64]*Hash
blocks []*Block
header Header
}
// PatchInfo represents patch info for the MPQ.
@ -53,296 +30,110 @@ type PatchInfo struct {
Length uint32 // Length of patch info header, in bytes
Flags uint32 // Flags. 0x80000000 = MD5 (?)
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
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
)
// 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}
// New loads an MPQ file and only reads the header
func New(fileName string) (*MPQ, error) {
mpq := &MPQ{filePath: fileName}
var err error
if runtime.GOOS == "linux" {
result.file, err = openIgnoreCase(fileName)
mpq.file, err = openIgnoreCase(fileName)
} 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 {
return nil, err
}
if err := result.readHeader(); err != nil {
return nil, err
if err := mpq.readHeader(); err != nil {
return nil, fmt.Errorf("failed to read reader: %v", err)
}
return result, nil
return mpq, nil
}
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 {
return mpqFile, err
}
mpqName := filepath.Base(mpqPath)
mpqDir := filepath.Dir(mpqPath)
files, err := ioutil.ReadDir(mpqDir)
// FromFile loads an MPQ file and returns a MPQ structure
func FromFile(fileName string) (*MPQ, error) {
mpq, err := New(fileName)
if err != nil {
return nil, err
}
for _, file := range files {
if strings.EqualFold(file.Name(), mpqName) {
mpqName = file.Name()
break
}
if err := mpq.readHashTable(); err != nil {
return nil, fmt.Errorf("failed to read hash table: %v", err)
}
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 {
err := binary.Read(v.file, binary.LittleEndian, &v.data)
if err != nil {
return err
// getFileBlockData gets a block table entry
func (mpq *MPQ) getFileBlockData(fileName string) (*Block, error) {
fileEntry, ok := mpq.hashes[hashFilename(fileName)]
if !ok {
return nil, errors.New("file not found")
}
if string(v.data.Magic[:]) != "MPQ\x1A" {
return errors.New("invalid mpq header")
if fileEntry.BlockIndex >= uint32(len(mpq.blocks)) {
return nil, errors.New("invalid block index")
}
err = v.loadHashTable()
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
return mpq.blocks[fileEntry.BlockIndex], nil
}
// Close closes the MPQ file
func (v *MPQ) Close() {
err := v.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)
func (mpq *MPQ) Close() error {
return mpq.file.Close()
}
// ReadFile reads a file from the MPQ and returns a memory stream
func (v *MPQ) ReadFile(fileName string) ([]byte, error) {
fileBlockData, err := v.getFileBlockData(fileName)
func (mpq *MPQ) ReadFile(fileName string) ([]byte, error) {
fileBlockData, err := mpq.getFileBlockData(fileName)
if err != nil {
return []byte{}, err
}
fileBlockData.FileName = strings.ToLower(fileName)
fileBlockData.calculateEncryptionSeed()
mpqStream, err := CreateStream(v, fileBlockData, fileName)
stream, err := CreateStream(mpq, fileBlockData, fileName)
if err != nil {
return []byte{}, err
}
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
}
// ReadFileStream reads the mpq file data and returns a stream
func (v *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
fileBlockData, err := v.getFileBlockData(fileName)
func (mpq *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
fileBlockData, err := mpq.getFileBlockData(fileName)
if err != nil {
return nil, err
}
fileBlockData.FileName = strings.ToLower(fileName)
fileBlockData.calculateEncryptionSeed()
mpqStream, err := CreateStream(v, fileBlockData, fileName)
stream, err := CreateStream(mpq, fileBlockData, fileName)
if err != nil {
return nil, err
}
return &MpqDataStream{stream: mpqStream}, nil
return &MpqDataStream{stream: stream}, nil
}
// ReadTextFile reads a file and returns it as a string
func (v *MPQ) ReadTextFile(fileName string) (string, error) {
data, err := v.ReadFile(fileName)
func (mpq *MPQ) ReadTextFile(fileName string) (string, error) {
data, err := mpq.ReadFile(fileName)
if err != nil {
return "", err
@ -351,20 +142,9 @@ func (v *MPQ) ReadTextFile(fileName string) (string, error) {
return string(data), nil
}
func (v *BlockTableEntry) calculateEncryptionSeed() {
fileName := path.Base(v.FileName)
v.EncryptionSeed = hashString(fileName, 3)
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)")
// Listfile returns the list of files in this MPQ
func (mpq *MPQ) Listfile() ([]string, error) {
data, err := mpq.ReadFile("(listfile)")
if err != nil {
return nil, err
@ -384,16 +164,44 @@ func (v *MPQ) GetFileList() ([]string, error) {
}
// Path returns the MPQ file path
func (v *MPQ) Path() string {
return v.filePath
func (mpq *MPQ) Path() string {
return mpq.filePath
}
// Contains returns bool for whether the given filename exists in the mpq
func (v *MPQ) Contains(filename string) bool {
return v.hashEntryMap.Contains(filename)
func (mpq *MPQ) Contains(filename string) bool {
_, ok := mpq.hashes[hashFilename(filename)]
return ok
}
// Size returns the size of the mpq in bytes
func (v *MPQ) Size() uint32 {
return v.data.ArchiveSize
func (mpq *MPQ) Size() uint32 {
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
}

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

View File

@ -11,14 +11,14 @@ type MpqDataStream struct {
// Read reads data from the data stream
func (m *MpqDataStream) Read(p []byte) (n int, err error) {
totalRead := m.stream.Read(p, 0, uint32(len(p)))
return int(totalRead), nil
totalRead, err := m.stream.Read(p, 0, uint32(len(p)))
return int(totalRead), err
}
// Seek sets the position of the data stream
func (m *MpqDataStream) Seek(offset int64, whence int) (int64, error) {
m.stream.CurrentPosition = uint32(offset + int64(whence))
return int64(m.stream.CurrentPosition), nil
m.stream.Position = uint32(offset + int64(whence))
return int64(m.stream.Position), nil
}
// Close closes the data stream

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

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

View File

@ -6,8 +6,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"log"
"strings"
"io"
"github.com/JoshVarga/blast"
@ -17,80 +16,63 @@ import (
// Stream represents a stream of data in an MPQ archive
type Stream struct {
BlockTableEntry BlockTableEntry
BlockPositions []uint32
CurrentData []byte
FileName string
MPQData *MPQ
EncryptionSeed uint32
CurrentPosition uint32
CurrentBlockIndex uint32
BlockSize uint32
Data []byte
Positions []uint32
MPQ *MPQ
Block *Block
Index uint32
Size uint32
Position uint32
}
// CreateStream creates an MPQ stream
func CreateStream(mpq *MPQ, blockTableEntry BlockTableEntry, fileName string) (*Stream, error) {
result := &Stream{
MPQData: mpq,
BlockTableEntry: blockTableEntry,
CurrentBlockIndex: 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
func CreateStream(mpq *MPQ, block *Block, fileName string) (*Stream, error) {
s := &Stream{
MPQ: mpq,
Block: block,
Index: 0xFFFFFFFF, //nolint:gomnd // MPQ magic
}
result.BlockSize = 0x200 << result.MPQData.data.BlockSize //nolint:gomnd // MPQ magic
if result.BlockTableEntry.HasFlag(FilePatchFile) {
log.Fatal("Patching is not supported")
if s.Block.HasFlag(FileFixKey) {
s.Block.calculateEncryptionSeed(fileName)
}
var err error
s.Size = 0x200 << s.MPQ.header.BlockSize //nolint:gomnd // MPQ magic
if (result.BlockTableEntry.HasFlag(FileCompress) || result.BlockTableEntry.HasFlag(FileImplode)) &&
!result.BlockTableEntry.HasFlag(FileSingleUnit) {
err = result.loadBlockOffsets()
if s.Block.HasFlag(FilePatchFile) {
return nil, errors.New("patching is not supported")
}
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 {
blockPositionCount := ((v.BlockTableEntry.UncompressedFileSize + v.BlockSize - 1) / v.BlockSize) + 1
v.BlockPositions = make([]uint32, blockPositionCount)
_, err := v.MPQData.file.Seek(int64(v.BlockTableEntry.FilePosition), 0)
if err != nil {
if _, err := v.MPQ.file.Seek(int64(v.Block.FilePosition), io.SeekStart); err != nil {
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 != nil {
if err := binary.Read(v.MPQ.file, binary.LittleEndian, &v.Positions); err != nil {
return err
}
for i := range v.BlockPositions {
idx := i * 4 //nolint:gomnd // MPQ magic
v.BlockPositions[i] = binary.LittleEndian.Uint32(mpqBytes[idx : idx+4])
}
if v.Block.HasFlag(FileEncrypted) {
decrypt(v.Positions, v.Block.EncryptionSeed-1)
blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic
if v.BlockTableEntry.HasFlag(FileEncrypted) {
decrypt(v.BlockPositions, v.EncryptionSeed-1)
if v.BlockPositions[0] != blockPosSize {
log.Println("Decryption of MPQ failed!")
blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic
if v.Positions[0] != blockPosSize {
return errors.New("decryption of MPQ failed")
}
if v.BlockPositions[1] > v.BlockSize+blockPosSize {
log.Println("Decryption of MPQ failed!")
if v.Positions[1] > v.Size+blockPosSize {
return errors.New("decryption of MPQ failed")
}
}
@ -98,16 +80,18 @@ func (v *Stream) loadBlockOffsets() error {
return nil
}
func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 {
if v.BlockTableEntry.HasFlag(FileSingleUnit) {
func (v *Stream) Read(buffer []byte, offset, count uint32) (readTotal uint32, err error) {
if v.Block.HasFlag(FileSingleUnit) {
return v.readInternalSingleUnit(buffer, offset, count)
}
toRead := count
readTotal := uint32(0)
var read uint32
toRead := count
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 {
break
@ -118,219 +102,228 @@ func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 {
toRead -= read
}
return readTotal
return readTotal, nil
}
func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) uint32 {
if len(v.CurrentData) == 0 {
v.loadSingleUnit()
func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) (uint32, error) {
if len(v.Data) == 0 {
if err := v.loadSingleUnit(); err != nil {
return 0, err
}
}
bytesToCopy := d2math.Min(uint32(len(v.CurrentData))-v.CurrentPosition, count)
copy(buffer[offset:offset+bytesToCopy], v.CurrentData[v.CurrentPosition:v.CurrentPosition+bytesToCopy])
v.CurrentPosition += bytesToCopy
return bytesToCopy
return v.copy(buffer, offset, v.Position, count)
}
func (v *Stream) readInternal(buffer []byte, offset, count uint32) uint32 {
v.bufferData()
func (v *Stream) readInternal(buffer []byte, offset, count uint32) (uint32, error) {
if err := v.bufferData(); err != nil {
return 0, err
}
localPosition := v.CurrentPosition % v.BlockSize
bytesToCopy := d2math.MinInt32(int32(len(v.CurrentData))-int32(localPosition), int32(count))
localPosition := v.Position % v.Size
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 {
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 uint32(bytesToCopy)
return bytesToCopy, nil
}
func (v *Stream) bufferData() {
requiredBlock := v.CurrentPosition / v.BlockSize
func (v *Stream) bufferData() (err error) {
blockIndex := v.Position / v.Size
if requiredBlock == v.CurrentBlockIndex {
return
if blockIndex == v.Index {
return nil
}
expectedLength := d2math.Min(v.BlockTableEntry.UncompressedFileSize-(requiredBlock*v.BlockSize), v.BlockSize)
v.CurrentData = v.loadBlock(requiredBlock, expectedLength)
v.CurrentBlockIndex = requiredBlock
expectedLength := d2math.Min(v.Block.UncompressedFileSize-(blockIndex*v.Size), v.Size)
if v.Data, err = v.loadBlock(blockIndex, expectedLength); err != nil {
return err
}
v.Index = blockIndex
return nil
}
func (v *Stream) loadSingleUnit() {
fileData := make([]byte, v.BlockSize)
_, err := v.MPQData.file.Seek(int64(v.MPQData.data.HeaderSize), 0)
if err != nil {
log.Print(err)
func (v *Stream) loadSingleUnit() (err error) {
if _, err = v.MPQ.file.Seek(int64(v.MPQ.header.HeaderSize), io.SeekStart); err != nil {
return err
}
_, err = v.MPQData.file.Read(fileData)
if err != nil {
log.Print(err)
fileData := make([]byte, v.Size)
if _, err = v.MPQ.file.Read(fileData); err != nil {
return err
}
if v.BlockSize == v.BlockTableEntry.UncompressedFileSize {
v.CurrentData = fileData
return
if v.Size == v.Block.UncompressedFileSize {
v.Data = fileData
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 (
offset uint32
toRead uint32
)
if v.BlockTableEntry.HasFlag(FileCompress) || v.BlockTableEntry.HasFlag(FileImplode) {
offset = v.BlockPositions[blockIndex]
toRead = v.BlockPositions[blockIndex+1] - offset
if v.Block.HasFlag(FileCompress) || v.Block.HasFlag(FileImplode) {
offset = v.Positions[blockIndex]
toRead = v.Positions[blockIndex+1] - offset
} else {
offset = blockIndex * v.BlockSize
offset = blockIndex * v.Size
toRead = expectedLength
}
offset += v.BlockTableEntry.FilePosition
offset += v.Block.FilePosition
data := make([]byte, toRead)
_, err := v.MPQData.file.Seek(int64(offset), 0)
if err != nil {
log.Print(err)
if _, err := v.MPQ.file.Seek(int64(offset), io.SeekStart); err != nil {
return []byte{}, err
}
_, err = v.MPQData.file.Read(data)
if err != nil {
log.Print(err)
if _, err := v.MPQ.file.Read(data); err != nil {
return []byte{}, err
}
if v.BlockTableEntry.HasFlag(FileEncrypted) && v.BlockTableEntry.UncompressedFileSize > 3 {
if v.EncryptionSeed == 0 {
panic("Unable to determine encryption key")
if v.Block.HasFlag(FileEncrypted) && v.Block.UncompressedFileSize > 3 {
if v.Block.EncryptionSeed == 0 {
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.BlockTableEntry.HasFlag(FileSingleUnit) {
data = decompressMulti(data, expectedLength)
} else {
data = pkDecompress(data)
if v.Block.HasFlag(FileCompress) && (toRead != expectedLength) {
if !v.Block.HasFlag(FileSingleUnit) {
return decompressMulti(data, expectedLength)
}
return pkDecompress(data)
}
if v.BlockTableEntry.HasFlag(FileImplode) && (toRead != expectedLength) {
data = pkDecompress(data)
if v.Block.HasFlag(FileImplode) && (toRead != expectedLength) {
return pkDecompress(data)
}
return data
return data, nil
}
//nolint:gomnd // Will fix enum values later
func decompressMulti(data []byte /*expectedLength*/, _ uint32) []byte {
//nolint:gomnd,funlen,gocyclo // Will fix enum values later, can't help function length
func decompressMulti(data []byte /*expectedLength*/, _ uint32) ([]byte, error) {
compressionType := data[0]
switch compressionType {
case 1: // Huffman
panic("huffman decompression not supported")
return []byte{}, errors.New("huffman decompression not supported")
case 2: // ZLib/Deflate
return deflate(data[1:])
case 8: // PKLib/Impode
return pkDecompress(data[1:])
case 0x10: // BZip2
panic("bzip2 decompression not supported")
return []byte{}, errors.New("bzip2 decompression not supported")
case 0x80: // IMA ADPCM Stereo
return d2compression.WavDecompress(data[1:], 2)
case 0x40: // IMA ADPCM Mono
return d2compression.WavDecompress(data[1:], 1)
case 0x12:
panic("lzma decompression not supported")
return []byte{}, errors.New("lzma decompression not supported")
// Combos
case 0x22:
// sparse then zlib
panic("sparse decompression + deflate decompression not supported")
return []byte{}, errors.New("sparse decompression + deflate decompression not supported")
case 0x30:
// sparse then bzip2
panic("sparse decompression + bzip2 decompression not supported")
return []byte{}, errors.New("sparse decompression + bzip2 decompression not supported")
case 0x41:
sinput := d2compression.HuffmanDecompress(data[1:])
sinput = d2compression.WavDecompress(sinput, 1)
sinput, err := d2compression.WavDecompress(d2compression.HuffmanDecompress(data[1:]), 1)
if err != nil {
return nil, err
}
tmp := make([]byte, len(sinput))
copy(tmp, sinput)
return tmp
return tmp, nil
case 0x48:
// byte[] result = PKDecompress(sinput, outputLength);
// return MpqWavCompression.Decompress(new MemoryStream(result), 1);
panic("pk + mpqwav decompression not supported")
return []byte{}, errors.New("pk + mpqwav decompression not supported")
case 0x81:
sinput := d2compression.HuffmanDecompress(data[1:])
sinput = d2compression.WavDecompress(sinput, 2)
sinput, err := d2compression.WavDecompress(d2compression.HuffmanDecompress(data[1:]), 2)
if err != nil {
return nil, err
}
tmp := make([]byte, len(sinput))
copy(tmp, sinput)
return tmp
return tmp, nil
case 0x88:
// byte[] result = PKDecompress(sinput, outputLength);
// return MpqWavCompression.Decompress(new MemoryStream(result), 2);
panic("pk + wav decompression not supported")
default:
panic(fmt.Sprintf("decompression not supported for unknown compression type %X", compressionType))
return []byte{}, errors.New("pk + wav decompression not supported")
}
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)
r, err := zlib.NewReader(b)
if err != nil {
panic(err)
return []byte{}, err
}
buffer := new(bytes.Buffer)
_, err = buffer.ReadFrom(r)
if err != nil {
log.Panic(err)
return []byte{}, err
}
err = r.Close()
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)
r, err := blast.NewReader(b)
r, err := blast.NewReader(b)
if err != nil {
panic(err)
return []byte{}, err
}
buffer := new(bytes.Buffer)
_, err = buffer.ReadFrom(r)
if err != nil {
panic(err)
if _, err = buffer.ReadFrom(r); err != nil {
return []byte{}, err
}
err = r.Close()
if err != nil {
panic(err)
return []byte{}, err
}
return buffer.Bytes()
return buffer.Bytes(), nil
}

View File

@ -41,3 +41,15 @@ func Load(data []byte) (*PL2, error) {
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
}

View File

@ -1,5 +1,14 @@
package d2pl2
const (
bitShift0 = 8 * iota
bitShift8
bitShift16
bitShift24
mask = 0xff
)
// PL2Color represents an RGBA color
type PL2Color struct {
R uint8
@ -7,3 +16,30 @@ type PL2Color struct {
B 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
}

View File

@ -6,3 +6,13 @@ type PL2Color24Bits struct {
G 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)
}

View 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")
}
}

View File

@ -1,7 +1,7 @@
package d2tbl
import (
"log"
"fmt"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
@ -10,6 +10,97 @@ import (
// TextDictionary is a string map
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 {
IsActive bool
Index uint16
@ -19,95 +110,151 @@ type textDictionaryHashEntry struct {
NameLength uint16
}
var lookupTable TextDictionary //nolint:gochecknoglobals // currently global by design
const (
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
func LoadTextDictionary(dictionaryData []byte) TextDictionary {
if lookupTable == nil {
lookupTable = make(TextDictionary)
}
func LoadTextDictionary(dictionaryData []byte) (TextDictionary, error) {
lookupTable := make(TextDictionary)
br := d2datautils.CreateStreamReader(dictionaryData)
// skip past the CRC
br.ReadBytes(crcByteCount)
_, _ = br.ReadBytes(crcByteCount)
numberOfElements := br.GetUInt16()
hashTableSize := br.GetUInt32()
var err error
// Version (always 0)
if _, err := br.ReadByte(); err != nil {
log.Fatal("Error reading Version record")
/*
number of indicates
(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
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.
br.GetUInt32() // FileSize
hashTableSize, err := br.ReadUInt32()
if err != nil {
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)
for i := 0; i < int(numberOfElements); i++ {
elementIndex[i] = br.GetUInt16()
}
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(),
elementIndex[i], err = br.ReadUInt16()
if err != nil {
return nil, fmt.Errorf("reading element index %d: %v", i, err)
}
}
for idx, hashEntry := range hashEntries {
if !hashEntry.IsActive {
continue
}
hashEntries := make([]*textDictionaryHashEntry, hashTableSize)
br.SetPosition(uint64(hashEntry.NameString))
nameVal := br.ReadBytes(int(hashEntry.NameLength - 1))
value := string(nameVal)
br.SetPosition(uint64(hashEntry.IndexString))
key := ""
for {
b := br.GetByte()
if b == 0 {
break
}
key += string(b)
}
if key == "x" || key == "X" {
key = "#" + strconv.Itoa(idx)
}
_, exists := lookupTable[key]
if !exists {
lookupTable[key] = value
}
err = lookupTable.loadHashEntries(hashEntries, br)
if err != nil {
return nil, fmt.Errorf("loading has entries: %v", err)
}
return lookupTable
return lookupTable, nil
}
// Marshal encodes text dictionary back into byte slice
func (td *TextDictionary) Marshal() []byte {
sw := d2datautils.CreateStreamWriter()
// https://github.com/OpenDiablo2/OpenDiablo2/issues/1043
sw.PushBytes(0, 0)
sw.PushUint16(0)
keys := make([]string, 0)
for key := range *td {
keys = append(keys, key)
}
sw.PushUint32(uint32(len(keys)))
// version (always 0)
sw.PushBytes(0)
// offset of start of data (unnecessary for our decoder)
sw.PushUint32(0)
// Max retry count for a hash hit.
sw.PushUint32(0)
// 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()
}

View 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")
}
}
}

View File

@ -9,13 +9,14 @@ import (
// Animation is an animation
type Animation interface {
BindRenderer(Renderer) error
BindRenderer(Renderer)
Clone() Animation
SetSubLoop(startFrame, EndFrame int)
Advance(elapsed float64) error
Render(target Surface) error
RenderFromOrigin(target Surface, shadow bool) error
RenderSection(sfc Surface, bound image.Rectangle) error
GetCurrentFrameSurface() Surface
Render(target Surface)
RenderFromOrigin(target Surface, shadow bool)
RenderSection(sfc Surface, bound image.Rectangle)
GetFrameSize(frameIndex int) (int, int, error)
GetCurrentFrameSize() (int, int)
GetFrameBounds() (int, int)

View File

@ -8,10 +8,9 @@ type Archive interface {
Path() string
Contains(string) bool
Size() uint32
Close()
FileExists(fileName string) bool
Close() error
ReadFile(fileName string) ([]byte, error)
ReadFileStream(fileName string) (DataStream, error)
ReadTextFile(fileName string) (string, error)
GetFileList() ([]string, error)
Listfile() ([]string, error)
}

View File

@ -3,42 +3,47 @@ package d2interface
// InputEventHandler is an event handler
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
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.
type KeyRepeatHandler interface {
OnKeyRepeat(event KeyEvent) bool
OnKeyRepeat(event KeyEvent) (preventPropagation bool)
}
// KeyUpHandler represents a handler for a keyboard key release event
type KeyUpHandler interface {
OnKeyUp(event KeyEvent) bool
OnKeyUp(event KeyEvent) (preventPropagation bool)
}
// KeyCharsHandler represents a handler associated with a keyboard character pressed event
type KeyCharsHandler interface {
OnKeyChars(event KeyCharsEvent) bool
OnKeyChars(event KeyCharsEvent) (preventPropagation bool)
}
// MouseButtonDownHandler represents a handler for a mouse button pressed event
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.
type MouseButtonRepeatHandler interface {
OnMouseButtonRepeat(event MouseEvent) bool
OnMouseButtonRepeat(event MouseEvent) (preventPropagation bool)
}
// MouseButtonUpHandler represents a handler for a mouse button release event
type MouseButtonUpHandler interface {
OnMouseButtonUp(event MouseEvent) bool
OnMouseButtonUp(event MouseEvent) (preventPropagation bool)
}
// MouseMoveHandler represents a handler for a mouse button release event
type MouseMoveHandler interface {
OnMouseMove(event MouseMoveEvent) bool
OnMouseMove(event MouseMoveEvent) (preventPropagation bool)
}

View File

@ -6,10 +6,11 @@ import (
// Navigator is used for transitioning between game screens
type Navigator interface {
ToMainMenu()
ToMainMenu(errorMessageOptional ...string)
ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, connHost string)
ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, connHost string)
ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string)
ToMapEngineTest(region int, level int)
ToCredits()
ToCinematics()
}

View File

@ -1,19 +1,24 @@
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
type Renderer interface {
GetRendererName() 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
CreateSurface(surface Surface) (Surface, error)
NewSurface(width, height int, filter d2enum.Filter) (Surface, error)
NewSurface(width, height int) Surface
IsFullScreen() bool
SetFullScreen(fullScreen bool)
SetVSyncEnabled(vsync bool)
GetVSyncEnabled() bool
GetCursorPos() (int, int)
CurrentFPS() float64
ShowPanicScreen(message string)
Print(target interface{}, str string) error
PrintAt(target interface{}, str string, x, y int)
}

View File

@ -10,7 +10,7 @@ import (
// Surface represents a renderable surface.
type Surface interface {
Renderer() Renderer
Clear(color color.Color) error
Clear(color color.Color)
DrawRect(width, height int, color color.Color)
DrawLine(x, y int, color color.Color)
DrawTextf(format string, params ...interface{})
@ -26,9 +26,9 @@ type Surface interface {
PushScale(x, y float64)
PushBrightness(brightness float64)
PushSaturation(saturation float64)
Render(surface Surface) error
Render(surface Surface)
// Renders a section of the surface enclosed by bounds
RenderSection(surface Surface, bound image.Rectangle) error
ReplacePixels(pixels []byte) error
RenderSection(surface Surface, bound image.Rectangle)
ReplacePixels(pixels []byte)
Screenshot() *image.RGBA
}

View File

@ -13,17 +13,17 @@ type Terminal interface {
OnKeyChars(event KeyCharsEvent) bool
Render(surface Surface) error
Execute(command string) error
OutputRaw(text string, category d2enum.TermCategory)
Outputf(format string, params ...interface{})
OutputInfof(format string, params ...interface{})
OutputWarningf(format string, params ...interface{})
OutputErrorf(format string, params ...interface{})
OutputClear()
IsVisible() bool
Rawf(category d2enum.TermCategory, format string, params ...interface{})
Printf(format string, params ...interface{})
Infof(format string, params ...interface{})
Warningf(format string, params ...interface{})
Errorf(format string, params ...interface{})
Clear()
Visible() bool
Hide()
Show()
BindAction(name, description string, action interface{}) error
UnbindAction(name string) error
Bind(name, description string, arguments []string, fn func(args []string) error) error
Unbind(name ...string) error
}
// TerminalLogger is used tomake the Terminal write out

View File

@ -2,14 +2,13 @@ package asset
import (
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
"io"
)
// Source is an abstraction for something that can load and list assets
type Source interface {
fmt.Stringer
Type() types.SourceType
Open(name string) (Asset, error)
Open(name string) (io.ReadSeeker, error)
Path() string
Exists(subPath string) bool
}

View File

@ -37,7 +37,8 @@ func Ext2SourceType(ext string) SourceType {
func CheckSourceType(path string) SourceType {
// on MacOS, the MPQ's from blizzard don't have file extensions
// 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
}

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

View File

@ -1,6 +1,7 @@
package filesystem
import (
"io"
"os"
"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
func (s *Source) Open(subPath string) (asset.Asset, error) {
file, err := os.Open(s.fullPath(subPath))
func (s *Source) Open(subPath string) (io.ReadSeeker, error) {
return os.Open(s.fullPath(subPath))
}
if err == nil {
a := &Asset{
assetType: types.Ext2AssetType(filepath.Ext(subPath)),
source: s,
path: subPath,
file: file,
}
return a, nil
}
return nil, err
// Exists returns true if the file exists
func (s *Source) Exists(subPath string) bool {
_, err := os.Stat(s.fullPath(subPath))
return os.IsExist(err)
}
func (s *Source) fullPath(subPath string) string {

View File

@ -2,29 +2,29 @@ package d2loader
import (
"fmt"
"os"
"io"
"path/filepath"
"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/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset"
"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/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
)
const (
defaultCacheBudget = 1024 * 1024 * 512
defaultCacheEntryWeight = 1
errFmtFileNotFound = "file not found: %s"
defaultCacheBudget = 1024 * 1024 * 512
errFmtFileNotFound = "file not found: %s"
)
const (
defaultLanguage = "ENG"
logPrefix = "File Loader"
)
const (
@ -33,64 +33,55 @@ const (
)
// NewLoader creates a new loader
func NewLoader(config *d2config.Configuration) *Loader {
func NewLoader(l d2util.LogLevel) (*Loader, error) {
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.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
// that have been added
type Loader struct {
config *d2config.Configuration
language *string
charset *string
d2interface.Cache
*d2util.Logger
Sources []asset.Source
LoaderProviders map[types.SourceType]func(path string) (asset.Source, error)
Sources []asset.Source
}
func (l *Loader) initFromConfig() {
if l.config == nil {
return
}
// SetLanguage sets the language for loader
func (l *Loader) SetLanguage(language *string) {
l.language = language
}
for _, mpqName := range l.config.MpqLoadOrder {
cleanDir := filepath.Clean(l.config.MpqPath)
srcPath := filepath.Join(cleanDir, mpqName)
_, err := l.AddSource(srcPath)
if err != nil {
fmt.Println(err.Error())
}
}
// SetCharset sets the charset for loader
func (l *Loader) SetCharset(charset *string) {
l.charset = charset
}
// 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)
func (l *Loader) Load(subPath string) (asset.Asset, error) {
lang := defaultLanguage
if l.config != nil {
lang = l.config.Language
}
func (l *Loader) Load(subPath string) (io.ReadSeeker, error) {
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 cached, found := l.Retrieve(subPath); found {
l.Debug(fmt.Sprintf("file `%s` exists in loader cache", subPath))
if l.language != nil {
charset := l.charset
language := l.language
a := cached.(asset.Asset)
_, err := a.Seek(0, 0)
return a, err
subPath = strings.ReplaceAll(subPath, fontToken, *charset)
subPath = strings.ReplaceAll(subPath, tableToken, *language)
}
// 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]
// if the source can open the file, then we cache it and return it
if loadedAsset, err := source.Open(subPath); err == nil {
err := l.Insert(subPath, loadedAsset, defaultCacheEntryWeight)
return loadedAsset, err
loadedAsset, err := source.Open(subPath)
if err != nil {
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)
@ -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
// to determine the type of asset source. In the case that the path points to a directory, a
// 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 {
l.Sources = make([]asset.Source, 0)
}
cleanPath := filepath.Clean(path)
info, err := os.Lstat(cleanPath)
source, err := l.LoaderProviders[sourceType](cleanPath)
if err != nil {
l.Warning(err.Error())
return nil, err
return err
}
mode := info.Mode()
l.Infof("Adding source: '%s'", cleanPath)
l.Sources = append(l.Sources, source)
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)
return source, nil
}
case types.AssetSourceFileSystem:
source := &filesystem.Source{
Root: cleanPath,
}
l.Debug(fmt.Sprintf("adding filesystem source `%s`", cleanPath))
l.Sources = append(l.Sources, source)
return source, nil
case types.AssetSourceUnknown:
l.Warning(fmt.Sprintf("unknown asset source `%s`", cleanPath))
}
return nil, fmt.Errorf("unknown asset source `%s`", cleanPath)
return nil
}
// Exists checks if the given path exists in at least one source
func (l *Loader) Exists(subPath string) bool {
subPath = filepath.Clean(subPath)
if l.language != nil {
charset := l.charset
language := l.language
subPath = strings.ReplaceAll(subPath, fontToken, *charset)
subPath = strings.ReplaceAll(subPath, tableToken, *language)
}
// 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
}

View File

@ -2,10 +2,13 @@ package d2loader
import (
"fmt"
"io"
"log"
"testing"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
)
const (
@ -24,7 +27,7 @@ const (
)
func TestLoader_NewLoader(t *testing.T) {
loader := NewLoader(nil)
loader, _ := NewLoader(d2util.LogLevelDefault)
if loader.Cache == nil {
t.Error("loader should not be nil")
@ -32,13 +35,13 @@ func TestLoader_NewLoader(t *testing.T) {
}
func TestLoader_AddSource(t *testing.T) {
loader := NewLoader(nil)
loader, _ := NewLoader(d2util.LogLevelDefault)
sourceA, errA := loader.AddSource(sourcePathA)
sourceB, errB := loader.AddSource(sourcePathB)
sourceC, errC := loader.AddSource(sourcePathC)
sourceD, errD := loader.AddSource(sourcePathD)
sourceE, errE := loader.AddSource(badSourcePath)
errA := loader.AddSource(sourcePathA, types.AssetSourceFileSystem)
errB := loader.AddSource(sourcePathB, types.AssetSourceFileSystem)
errC := loader.AddSource(sourcePathC, types.AssetSourceFileSystem)
errD := loader.AddSource(sourcePathD, types.AssetSourceFileSystem)
errE := loader.AddSource(badSourcePath, types.AssetSourceMPQ)
if errA != nil {
t.Error(errA)
@ -59,51 +62,32 @@ func TestLoader_AddSource(t *testing.T) {
if errE == nil {
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
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 {
t.Fail()
log.Print(err)
}
_, err = loader.AddSource(sourcePathD)
err = loader.AddSource(sourcePathD, types.AssetSourceMPQ)
if err != nil {
t.Fail()
log.Print(err)
}
_, err = loader.AddSource(sourcePathA)
err = loader.AddSource(sourcePathA, types.AssetSourceFileSystem)
if err != nil {
t.Fail()
log.Print(err)
}
_, err = loader.AddSource(sourcePathC)
err = loader.AddSource(sourcePathC, types.AssetSourceFileSystem)
if err != nil {
t.Fail()
log.Print(err)
@ -121,8 +105,6 @@ func TestLoader_Load(t *testing.T) {
if entryCommon == nil || errCommon != nil {
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 {
@ -142,7 +124,7 @@ func TestLoader_Load(t *testing.T) {
buffer := make([]byte, 1)
tests := []struct {
entry asset.Asset
entry io.ReadSeeker
data string
}{
{entryCommon, "b"}, // sourcePathB is loaded first, we expect a "b"
@ -169,8 +151,8 @@ func TestLoader_Load(t *testing.T) {
got := string(result[0])
if got != expected {
fmtStr := "unexpected data in file %s, loaded from source `%s`: expected `%s`, got `%s`"
msg := fmt.Sprintf(fmtStr, entry.Path(), entry.Source(), expected, got)
fmtStr := "unexpected data in file, expected %q, got %q"
msg := fmt.Sprintf(fmtStr, expected, got)
t.Error(msg)
}
}

Some files were not shown because too many files have changed in this diff Show More