1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-06-26 17:15:24 +00:00

Compare commits

..

No commits in common. "master" and "v0.1-pre-alpha" have entirely different histories.

378 changed files with 7374 additions and 19641 deletions

View File

@ -1,22 +1,12 @@
# 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.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
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 ./...

View File

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

View File

@ -1,13 +0,0 @@
name: 'Auto Author Assign'
on:
pull_request_target:
types: [opened, reopened]
jobs:
assign-author:
runs-on: ubuntu-latest
steps:
- uses: toshimaru/auto-author-assign@v1.3.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

37
.github/workflows/pullRequest.yml vendored Normal file
View File

@ -0,0 +1,37 @@
---
name: pull_request
"on": [pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Set up Go 1.14
uses: actions/setup-go@v1
with:
go-version: 1.14
id: go
- name: Check out code
uses: actions/checkout@v1
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y xvfb libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libsdl2-dev libasound2-dev > /dev/null 2>&1
- name: Run golangci-lint
continue-on-error: false
uses: golangci/golangci-lint-action@v1.2.1
with:
version: v1.27
- name: Run tests
env:
DISPLAY: ":99.0"
run: |
xvfb-run --auto-servernum go test -v -race ./...
- name: Build binary
run: go build .

39
.github/workflows/pushToMaster.yml vendored Normal file
View File

@ -0,0 +1,39 @@
---
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,4 +13,3 @@
tags
heap.out
heap.pdf
.DS_Store

View File

@ -18,24 +18,16 @@ 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
@ -65,11 +57,14 @@ linters:
- gosimple
- govet
- ineffassign
- interfacer
- lll
- maligned
- misspell
- nakedret
- prealloc
- rowserrcheck
- scopelint
- staticcheck
- structcheck
- stylecheck
@ -83,19 +78,11 @@ 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

43
.travis.yml Normal file
View File

@ -0,0 +1,43 @@
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,7 +20,6 @@ Ripolak
dafe
presiyan
Natureknight
Ganitzsh
* PATREON SUPPORTERS
K C

164
README.md
View File

@ -1,62 +1,116 @@
# 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://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/)
[![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)
![Logo](d2logo.png)
[![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)
[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)
----
[OpenDiablo2](https://opendiablo2.com/) is an ARPG game engine in the same vein of the 2000's games, and supports playing Diablo 2.
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!
The engine is written in Go and is cross platform.
## About this project
> 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.
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.
If you like to contribute to OpenDiablo2, please be so kind to read our [Contribution Policy](./docs/CONTRIBUTING.md) first.
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.
----
Please note that **this game is neither developed by, nor endorsed by Blizzard or its parent company Activision**.
## Documentation
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.
_Stay awhile and listen_ ...
ALL OTHER TRADEMARKS ARE THE PROPERTY OF THEIR RESPECTIVE OWNERS.
### ⚡ Project Info
## Status
* 👉 **[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
At the moment (october 2020) the game starts, you can select any character and run around Act1 town.
### ⭐ For Users
Much work has been made in the background, but a lot of work still has to be done for the game to be playable.
* [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
Feel free to contribute!
### 🔥 For Developers
## Building
* [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
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.
## Screenshots
@ -66,25 +120,11 @@ _Stay awhile and listen_ ...
![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)
## 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.
- 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)

154
build.sh
View File

@ -5,7 +5,7 @@
# License: GNU GPLv3
version="0.0.8"
go_version="1.16"
go_version="1.13.4"
echo "OpenDiablo 2 Build Script $version"
#=================================================
@ -14,109 +14,103 @@ 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
if ! command -v go >/dev/null 2>&1; then
go_install(){
# Check OS & go
echo "Install Go for OpenDiablo 2 ($distribution)? y/n"
read -r choice
[ "$choice" != y ] && [ "$choice" != Y ] && exit
if ! command -v go > /dev/null 2>&1; then
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
echo "Install Go for OpenDiablo 2 ($distribution)? y/n"
read -r choice
[ "$choice" != y ] && [ "$choice" != Y ] && exit
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
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" = "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" = "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" = "Gentoo" ]; then
sudo emerge --ask n go
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" = "Manjaro" ] || [ "$distribution" = "Arch\ Linux" ]; then
sudo pacman -S go --noconfirm
elif [ "$distribution" = "Gentoo" ]; then
sudo emerge --ask n go
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
}
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" = "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" = "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
elif [ "$distribution" = "Manjaro" ] || [ "$distribution" = "Arch\ Linux" ]; then
sudo pacman -S go --noconfirm
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
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 [[ "$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
}
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
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" = "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
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 [[ "$OSTYPE" == "darwin"* ]]; then
# are there dependencies required? did I just have all of them already?
echo "Mac OS detected, no dependency installation necessary..."
fi
}
# Build
echo "Check Go"
go_install
echo "Install libraries"
if [ ! -e "$HOME/.config/OpenDiablo2" ]; then
mkdir -p $HOME/.config/OpenDiablo2
fi
if [ -e "$HOME/.config/OpenDiablo2/.libs" ]; then
echo "libraries is installed"
echo "libraries is installed"
else
echo "OK" >"$HOME/.config/OpenDiablo2/.libs"
dep_install
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,41 +4,34 @@ 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"
@ -63,8 +56,6 @@ type App struct {
captureFrames []*image.RGBA
gitBranch string
gitCommit string
language string
charset string
asset *d2asset.AssetManager
inputManager d2interface.InputManager
terminal d2interface.Terminal
@ -75,18 +66,12 @@ type App struct {
ui *d2ui.UIManager
tAllocSamples *ring.Ring
guiManager *d2gui.GuiManager
config *d2config.Configuration
*d2util.Logger
errorMessage error
*Options
}
// 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
type bindTerminalEntry struct {
name string
description string
action interface{}
}
const (
@ -95,217 +80,53 @@ const (
debugPopN = 6
)
const (
appLoggerPrefix = "App"
)
// Create creates a new instance of the application
func Create(gitBranch, gitCommit string) *App {
runtime.LockOSThread()
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)
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
result := &App{
gitBranch: gitBranch,
gitCommit: gitCommit,
inputManager: inputManager,
terminal: terminal,
scriptEngine: scriptEngine,
audio: audio,
renderer: renderer,
ui: uiManager,
asset: asset,
tAllocSamples: createZeroedRing(nSamplesTAlloc),
}
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
if result.gitBranch == "" {
result.gitBranch = "Local Build"
}
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
return result
}
// Run executes the application and kicks off the entire game process
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)
func (a *App) Run() error {
profileOption := kingpin.Flag("profile", "Profiles the program, one of (cpu, mem, block, goroutine, trace, thread, mutex)").String()
kingpin.Parse()
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 len(*profileOption) > 0 {
profiler := enableProfiler(*profileOption)
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 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 {
if gameErr := a.renderer.Run(updateInitError, 800, 600, windowTitle); gameErr != nil {
return gameErr
}
@ -314,16 +135,86 @@ func (a *App) Run() (err error) {
a.ToMainMenu()
if err := a.renderer.Run(a.update, a.advance, 800, 600, windowTitle); err != nil {
if err := a.renderer.Run(a.update, 800, 600, windowTitle); err != nil {
return err
}
return nil
}
func (a *App) renderDebug(target d2interface.Surface) {
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 {
if !a.showFPS {
return
return nil
}
vsyncEnabled := a.renderer.GetVSyncEnabled()
@ -350,6 +241,8 @@ func (a *App) renderDebug(target d2interface.Surface) {
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 {
@ -381,33 +274,35 @@ func (a *App) renderCapture(target d2interface.Surface) error {
return nil
}
func (a *App) render(target d2interface.Surface) {
a.screen.Render(target)
func (a *App) render(target d2interface.Surface) error {
if err := a.screen.Render(target); err != nil {
return err
}
a.ui.Render(target)
if err := a.guiManager.Render(target); err != nil {
return
return err
}
a.renderDebug(target)
if err := a.renderDebug(target); err != nil {
return err
}
if err := a.renderCapture(target); err != nil {
return
return err
}
if err := a.terminal.Render(target); err != nil {
return
return err
}
return nil
}
func (a *App) advance() error {
current := d2util.Now()
elapsedUnscaled := current - a.lastTime
elapsed := elapsedUnscaled * a.timeScale
a.lastTime = current
func (a *App) advance(elapsed, elapsedUnscaled, current float64) error {
elapsedLastScreenAdvance := (current - a.lastScreenAdvance) * a.timeScale
a.lastScreenAdvance = current
if err := a.screen.Advance(elapsedLastScreenAdvance); err != nil {
@ -432,7 +327,18 @@ func (a *App) advance() error {
}
func (a *App) update(target d2interface.Surface) error {
a.render(target)
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
}
if target.GetDepth() > 0 {
return errors.New("detected surface stack leak")
@ -449,24 +355,67 @@ 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
}
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)
log.Printf("saved frame to %s", a.capturePath)
return nil
}
@ -484,7 +433,7 @@ func (a *App) convertFramesToGif() error {
defer func() {
if err := fp.Close(); err != nil {
a.Fatal(err.Error())
log.Fatal(err)
}
}()
@ -526,11 +475,49 @@ func (a *App) convertFramesToGif() error {
return err
}
a.Infof("saved animation to %s", a.capturePath)
log.Printf("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++ {
@ -541,36 +528,36 @@ func createZeroedRing(n int) *ring.Ring {
return r
}
func enableProfiler(profileOption string, a *App) interface{ Stop() } {
func enableProfiler(profileOption string) interface{ Stop() } {
var options []func(*profile.Profile)
switch strings.ToLower(strings.Trim(profileOption, " ")) {
case "cpu":
a.Logger.Debug("CPU profiling is enabled.")
log.Printf("CPU profiling is enabled.")
options = append(options, profile.CPUProfile)
case "mem":
a.Logger.Debug("Memory profiling is enabled.")
log.Printf("Memory profiling is enabled.")
options = append(options, profile.MemProfile)
case "block":
a.Logger.Debug("Block profiling is enabled.")
log.Printf("Block profiling is enabled.")
options = append(options, profile.BlockProfile)
case "goroutine":
a.Logger.Debug("Goroutine profiling is enabled.")
log.Printf("Goroutine profiling is enabled.")
options = append(options, profile.GoroutineProfile)
case "trace":
a.Logger.Debug("Trace profiling is enabled.")
log.Printf("Trace profiling is enabled.")
options = append(options, profile.TraceProfile)
case "thread":
a.Logger.Debug("Thread creation profiling is enabled.")
log.Printf("Thread creation profiling is enabled.")
options = append(options, profile.ThreadcreationProfile)
case "mutex":
a.Logger.Debug("Mutex profiling is enabled.")
log.Printf("Mutex profiling is enabled.")
options = append(options, profile.MutexProfile)
}
@ -584,22 +571,26 @@ func enableProfiler(profileOption string, a *App) interface{ Stop() } {
return nil
}
func (a *App) updateInitError(target d2interface.Surface) error {
target.Clear(colornames.Darkred)
func updateInitError(target d2interface.Surface) error {
err := target.Clear(colornames.Darkred)
if err != nil {
return err
}
target.PushTranslation(errMsgPadding, errMsgPadding)
target.DrawTextf(a.errorMessage.Error())
target.DrawTextf(`Could not find the MPQ files in the directory:
%s\nPlease put the files and re-run the game.`, d2config.Config.MpqPath)
return nil
}
// ToMainMenu forces the game to transition to the Main Menu
func (a *App) ToMainMenu(errorMessageOptional ...string) {
func (a *App) ToMainMenu() {
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,
*a.Options.LogLevel, errorMessageOptional...)
mainMenu, err := d2gamescreen.CreateMainMenu(a, a.asset, a.renderer, a.inputManager, a.audio, a.ui, buildInfo)
if err != nil {
a.Error(err.Error())
log.Print(err)
return
}
@ -608,9 +599,9 @@ func (a *App) ToMainMenu(errorMessageOptional ...string) {
// 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, *a.Options.LogLevel, host)
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, host)
if err != nil {
a.Error(err.Error())
log.Print(err)
return
}
@ -619,49 +610,34 @@ 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.Options.LogLevel, a.scriptEngine)
gameClient, err := d2client.Create(connType, a.asset, a.scriptEngine)
if err != nil {
a.Error(err.Error())
}
if gameClient == nil {
a.Error("could not create client")
return
log.Print(err)
}
if err = gameClient.Open(host, filePath); err != nil {
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(game)
// https://github.com/OpenDiablo2/OpenDiablo2/issues/805
fmt.Printf("can not connect to the host: %s", host)
}
a.screen.SetNextScreen(d2gamescreen.CreateGame(a, a.asset, a.ui, a.renderer, a.inputManager,
a.audio, gameClient, a.terminal, a.guiManager))
}
// ToCharacterSelect forces the game to transition to the Character Select (load character) screen
func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) {
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)
}
// https://github.com/OpenDiablo2/OpenDiablo2/issues/790
characterSelect := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager,
a.audio, a.ui, connType, connHost)
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.Options.LogLevel, a.screen)
met, err := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer, a.inputManager, a.audio, a.screen)
if err != nil {
a.Error(err.Error())
log.Print(err)
return
}
@ -670,10 +646,5 @@ 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.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))
a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, a.ui))
}

View File

@ -1,139 +0,0 @@
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
}

View File

@ -1,180 +0,0 @@
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

@ -1,93 +0,0 @@
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

@ -0,0 +1,58 @@
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.PushBytes(byte(newvalue))
outputstream.PushByte(byte(newvalue))
tail = insertNode(tail, newvalue)
default:
outputstream.PushBytes(byte(decoded))
outputstream.PushByte(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, error) { //nolint:funlen,gocognit,gocyclo // can't reduce
func WavDecompress(data []byte, channelCount int) []byte { //nolint:funlen,gocognit,gocyclo // can't reduce
Array1 := []int{0x2c, 0x2c}
Array2 := make([]int, channelCount)
@ -35,33 +35,20 @@ func WavDecompress(data []byte, channelCount int) ([]byte, error) { //nolint:fun
input := d2datautils.CreateStreamReader(data)
output := d2datautils.CreateStreamWriter()
_, err := input.ReadByte()
if err != nil {
return nil, err
}
input.GetByte()
shift, err := input.ReadByte()
if err != nil {
return nil, err
}
shift := input.GetByte()
for i := 0; i < channelCount; i++ {
temp, err := input.ReadInt16()
if err != nil {
return nil, err
}
temp := input.GetInt16()
Array2[i] = int(temp)
output.PushInt16(temp)
}
channel := channelCount - 1
for input.Position() < input.Size() {
value, err := input.ReadByte()
if err != nil {
return nil, err
}
for input.GetPosition() < input.GetSize() {
value := input.GetByte()
if channelCount == 2 {
channel = 1 - channel
@ -142,5 +129,5 @@ func WavDecompress(data []byte, channelCount int) ([]byte, error) { //nolint:fun
}
}
return output.GetBytes(), nil
return output.GetBytes()
}

View File

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

View File

@ -1,107 +0,0 @@
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,7 +39,6 @@ func (v *BitStream) ReadBits(bitCount int) int {
return -1
}
// nolint:gomnd // byte expresion
result := v.current & (0xffff >> uint(maxBits-bitCount))
v.WasteBits(bitCount)
@ -52,7 +51,6 @@ 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,72 +26,50 @@ func CreateStreamReader(source []byte) *StreamReader {
return result
}
// ReadByte reads a byte from the stream
func (v *StreamReader) ReadByte() (byte, error) {
if v.position >= v.Size() {
return 0, io.EOF
}
// GetPosition returns the current stream position
func (v *StreamReader) GetPosition() uint64 {
return v.position
}
// 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, nil
return result
}
// ReadInt16 returns a int16 word from the stream
func (v *StreamReader) ReadInt16() (int16, error) {
b, err := v.ReadUInt16()
return int16(b), err
}
// GetUInt16 returns a uint16 word from the stream
func (v *StreamReader) GetUInt16() uint16 {
var result uint16
// 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
for offset := uint64(0); offset < bytesPerInt16; offset++ {
shift := uint8(bitsPerByte * offset)
result += uint16(v.data[v.position+offset]) << shift
}
return uint16(b[0]) | uint16(b[1])<<8, err
v.position += bytesPerInt16
return result
}
// ReadInt32 returns an int32 dword from the stream
func (v *StreamReader) ReadInt32() (int32, error) {
b, err := v.ReadUInt32()
return int32(b), err
}
// GetInt16 returns a int16 word from the stream
func (v *StreamReader) GetInt16() int16 {
var result int16
// 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
for offset := uint64(0); offset < bytesPerInt16; offset++ {
shift := uint8(bitsPerByte * offset)
result += int16(v.data[v.position+offset]) << shift
}
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24, err
}
v.position += bytesPerInt16
// 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
return result
}
// SetPosition sets the stream position with the given position
@ -99,26 +77,64 @@ func (v *StreamReader) SetPosition(newPosition uint64) {
v.position = newPosition
}
// Size returns the total size of the stream in bytes
func (v *StreamReader) Size() uint64 {
return uint64(len(v.data))
// 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
}
// ReadBytes reads multiple bytes
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
}
func (v *StreamReader) ReadBytes(count int) []byte {
result := v.data[v.position : v.position+uint64(count)]
v.position += uint64(count)
return result, nil
return result
}
// SkipBytes moves the stream position forward by the given amount
@ -128,10 +144,10 @@ func (v *StreamReader) SkipBytes(count int) {
// Read implements io.Reader
func (v *StreamReader) Read(p []byte) (n int, err error) {
streamLength := v.Size()
streamLength := v.GetSize()
for i := 0; ; i++ {
if v.Position() >= streamLength {
if v.GetPosition() >= streamLength {
return i, io.EOF
}
@ -139,10 +155,7 @@ func (v *StreamReader) Read(p []byte) (n int, err error) {
return i, nil
}
p[i], err = v.ReadByte()
if err != nil {
return i, err
}
p[i] = v.GetByte()
}
}

View File

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

View File

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

View File

@ -4,75 +4,18 @@ import (
"testing"
)
func TestStreamWriterBits(t *testing.T) {
sr := CreateStreamWriter()
data := []byte{221, 19}
for _, i := range data {
sr.PushBits(i, bitsPerByte)
}
output := sr.GetBytes()
for i, d := range data {
if output[i] != d {
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...)
for _, d := range data {
sr.PushByte(d)
}
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])
t.Fatalf("sr.PushByte() pushed %X, but wrote %X instead", d, output[i])
}
}
}

View File

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

@ -1,13 +0,0 @@
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,24 +45,3 @@ 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

@ -1,83 +0,0 @@
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,117 +3,212 @@ 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 = KeyMouseWheelDown
// KeyMax is the highest key
KeyMax = KeyShift
)
// KeyMod represents a "modified" key action. This could mean, for example, ctrl-S

View File

@ -0,0 +1,20 @@
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

@ -1,133 +0,0 @@
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

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

View File

@ -1,21 +0,0 @@
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
)

View File

@ -1,60 +0,0 @@
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,36 +69,3 @@ 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,31 +24,3 @@ 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,68 +56,7 @@ 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{}
@ -126,11 +65,7 @@ func Load(data []byte) (*AnimationData, error) {
animdata.entries = make(map[string][]*AnimationDataRecord)
for blockIdx := range animdata.blocks {
recordCount, err := reader.ReadUInt32()
if err != nil {
return nil, err
}
recordCount := reader.GetUInt32()
if recordCount > maxRecordsPerBlock {
return nil, fmt.Errorf("more than %d records in block", maxRecordsPerBlock)
}
@ -138,10 +73,7 @@ func Load(data []byte) (*AnimationData, error) {
records := make([]*AnimationDataRecord, recordCount)
for recordIdx := uint32(0); recordIdx < recordCount; recordIdx++ {
nameBytes, err := reader.ReadBytes(byteCountName)
if err != nil {
return nil, err
}
nameBytes := reader.ReadBytes(byteCountName)
if nameBytes[byteCountName-1] != byte(0) {
return nil, errors.New("animdata AnimationDataRecord name missing null terminator byte")
@ -152,27 +84,15 @@ func Load(data []byte) (*AnimationData, error) {
animdata.hashTable[hashIdx] = hashName(name)
frames, err := reader.ReadUInt32()
if err != nil {
return nil, err
}
speed, err := reader.ReadUInt16()
if err != nil {
return nil, err
}
frames := reader.GetUInt32()
speed := reader.GetUInt16()
reader.SkipBytes(byteCountSpeedPadding)
events := make(map[int]AnimationEvent)
for eventIdx := 0; eventIdx < numEvents; eventIdx++ {
eventByte, err := reader.ReadByte()
if err != nil {
return nil, err
}
event := AnimationEvent(eventByte)
event := AnimationEvent(reader.GetByte())
if event != AnimationEventNone {
events[eventIdx] = event
}
@ -202,93 +122,9 @@ func Load(data []byte) (*AnimationData, error) {
animdata.blocks[blockIdx] = b
}
if reader.Position() != uint64(len(data)) {
return nil, fmt.Errorf("unable to parse animation data: %d != %d", reader.Position(), len(data))
if reader.GetPosition() != uint64(len(data)) {
return nil, errors.New("unable to parse animation 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,137 +154,3 @@ 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,26 +8,6 @@ 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)
@ -41,23 +21,3 @@ 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,68 +7,8 @@ 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
@ -79,166 +19,58 @@ type COF struct {
Priority [][][]d2enum.CompositeType
}
// Unmarshal a byte slice to this COF
func (c *COF) Unmarshal(fileData []byte) error {
var err error
// Load loads a COF file.
func Load(fileData []byte) (*COF, error) {
result := &COF{}
streamReader := d2datautils.CreateStreamReader(fileData)
result.NumberOfLayers = int(streamReader.GetByte())
result.FramesPerDirection = int(streamReader.GetByte())
result.NumberOfDirections = int(streamReader.GetByte())
headerBytes, err := streamReader.ReadBytes(numHeaderBytes)
if err != nil {
return err
}
streamReader.SkipBytes(21) //nolint:gomnd // Unknown data
c.loadHeader(headerBytes)
result.Speed = int(streamReader.GetByte())
c.unknownBodyBytes, err = streamReader.ReadBytes(numUnknownBodyBytes)
if err != nil {
return err
}
streamReader.SkipBytes(3) //nolint:gomnd // Unknown data
c.CofLayers = make([]CofLayer, c.NumberOfLayers)
c.CompositeLayers = make(map[d2enum.CompositeType]int)
result.CofLayers = make([]CofLayer, result.NumberOfLayers)
result.CompositeLayers = make(map[d2enum.CompositeType]int)
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++ {
for i := 0; i < result.NumberOfLayers; i++ {
layer := CofLayer{}
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
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
}
return nil
}
animationFrameBytes := streamReader.ReadBytes(result.FramesPerDirection)
result.AnimationFrames = make([]d2enum.AnimationFrame, result.FramesPerDirection)
func (c *COF) loadAnimationFrames(b []byte) {
c.AnimationFrames = make([]d2enum.AnimationFrame, c.FramesPerDirection)
for i := range b {
c.AnimationFrames[i] = d2enum.AnimationFrame(b[i])
for i := range animationFrameBytes {
result.AnimationFrames[i] = d2enum.AnimationFrame(animationFrameBytes[i])
}
}
func (c *COF) loadPriority(priorityBytes []byte) {
priorityLen := result.FramesPerDirection * result.NumberOfDirections * result.NumberOfLayers
result.Priority = make([][][]d2enum.CompositeType, result.NumberOfDirections)
priorityBytes := streamReader.ReadBytes(priorityLen)
priorityIndex := 0
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])
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])
priorityIndex++
}
}
}
}
// 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()
return result, nil
}

View File

@ -1,35 +0,0 @@
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

@ -1,27 +0,0 @@
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,8 +1,6 @@
package d2dat
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
const (
// index offset helpers
@ -23,14 +21,3 @@ 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,16 +15,6 @@ 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,9 +7,6 @@ import (
const (
endOfScanLine = 0x80
maxRunLength = 0x7f
terminationSize = 4
terminatorSize = 3
)
type scanlineState int
@ -32,179 +29,49 @@ type DC6 struct {
Frames []*DC6Frame // size is Directions*FramesPerDirection
}
// 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
// Load uses restruct to read the binary dc6 data into structs then parses image data from the frame data.
func Load(data []byte) (*DC6, error) {
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
const (
terminationSize = 4
terminatorSize = 3
)
r := d2datautils.CreateStreamReader(data)
err = d.loadHeader(r)
if err != nil {
return err
}
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(d.Directions * d.FramesPerDirection)
frameCount := int(dc.Directions * dc.FramesPerDirection)
d.FramePointers = make([]uint32, frameCount)
dc.FramePointers = make([]uint32, frameCount)
for i := 0; i < frameCount; i++ {
d.FramePointers[i], err = r.ReadUInt32()
if err != nil {
return err
}
dc.FramePointers[i] = r.GetUInt32()
}
d.Frames = make([]*DC6Frame, frameCount)
dc.Frames = make([]*DC6Frame, frameCount)
if err := d.loadFrames(r); err != nil {
return err
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(),
}
frame.FrameData = r.ReadBytes(int(frame.Length))
frame.Terminator = r.ReadBytes(terminatorSize)
dc.Frames[i] = frame
}
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()
return &dc, nil
}
// DecodeFrame decodes the given frame to an indexed color texture
@ -267,7 +134,7 @@ func (d *DC6) Clone() *DC6 {
for i := range d.Frames {
cloneFrame := *d.Frames[i]
clone.Frames[i] = &cloneFrame
clone.Frames = append(clone.Frames, &cloneFrame)
}
return &clone

View File

@ -1,69 +0,0 @@
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,13 +9,6 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
)
const (
baseMinx = 100000
baseMiny = 100000
baseMaxx = -100000
baseMaxy = -100000
)
const cellsPerRow = 4
// DCCDirection represents a DCCDirection file.
@ -44,9 +37,7 @@ 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{
@ -62,10 +53,10 @@ func CreateDCCDirection(bm *d2datautils.BitMuncher, file *DCC) *DCCDirection {
Frames: make([]*DCCDirectionFrame, file.FramesPerDirection),
}
minx := baseMinx
miny := baseMiny
maxx := baseMaxx
maxy := baseMaxy
minx := 100000
miny := 100000
maxx := -100000
maxy := -100000
// Load the frame headers
for frameIdx := 0; frameIdx < file.FramesPerDirection; frameIdx++ {
@ -82,14 +73,12 @@ 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
@ -423,12 +412,9 @@ 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
@ -436,8 +422,6 @@ 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,7 +55,6 @@ 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 {
@ -63,8 +62,6 @@ 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--
}
@ -78,8 +75,6 @@ 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--
}
@ -93,8 +88,6 @@ 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))
}
@ -106,8 +99,6 @@ 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,8 +1,6 @@
package d2ds1
import (
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
@ -10,683 +8,290 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2path"
)
const (
subType1 = 1
subType2 = 2
)
const (
wallZeroBitmask = 0xFFFFFF00
wallZeroOffset = 8
wallTypeBitmask = 0x000000FF
)
const (
unknown1BytesCount = 8
)
const maxActNumber = 5
// DS1 represents the "stamp" data that is used to build up maps.
type DS1 struct {
*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
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
}
const (
defaultNumFloors = 1
defaultNumShadows = maxShadowLayers
defaultNumSubstitutions = 0
)
// 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
// 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 >= 8 { //nolint:gomnd // Version number
ds1.Act = d2math.MinInt32(maxActNumber, br.GetInt32()+1)
}
if err := ds1.loadBody(stream); err != nil {
return nil, fmt.Errorf("loading body: %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 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) loadHeader(br *d2datautils.StreamReader) error {
var err error
func (ds1 *DS1) loadObjects(br *d2datautils.StreamReader) {
if ds1.Version >= 2 { //nolint:gomnd // Version number
numberOfObjects := br.GetInt32()
ds1.Objects = make([]Object, numberOfObjects)
var width, height int32
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())
v, err := br.ReadInt32()
if err != nil {
return fmt.Errorf("reading version: %w", err)
}
ds1.version = ds1version(v)
width, err = br.ReadInt32()
if err != nil {
return fmt.Errorf("reading width: %w", err)
}
height, err = br.ReadInt32()
if err != nil {
return fmt.Errorf("reading height: %w", err)
}
width++
height++
ds1.SetSize(int(width), int(height))
if ds1.version.specifiesAct() {
ds1.Act, err = br.ReadInt32()
if err != nil {
return fmt.Errorf("reading Act: %w", err)
ds1.Objects[objIdx] = newObject
}
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() {
} else {
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) error {
var err error
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()
}
hasSubstitutions := ds1.version.hasSubstitutions() &&
(ds1.SubstitutionType == subType1 || ds1.SubstitutionType == subType2)
numberOfSubGroups := br.GetInt32()
ds1.SubstitutionGroups = make([]SubstitutionGroup, numberOfSubGroups)
if !hasSubstitutions {
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 {
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) getLayerSchema() []layerStreamType {
var layerStream []layerStreamType
func (ds1 *DS1) setupStreamLayerTypes() []d2enum.LayerStreamType {
var layerStream []d2enum.LayerStreamType
if ds1.version.hasStandardLayers() {
layerStream = []layerStreamType{
layerStreamWall1,
layerStreamFloor1,
layerStreamOrientation1,
layerStreamSubstitute1,
layerStreamShadow1,
if ds1.Version < 4 { //nolint:gomnd // Version number
layerStream = []d2enum.LayerStreamType{
d2enum.LayerStreamWall1,
d2enum.LayerStreamFloor1,
d2enum.LayerStreamOrientation1,
d2enum.LayerStreamSubstitute,
d2enum.LayerStreamShadow,
}
} else {
layerStream = make([]d2enum.LayerStreamType,
(ds1.NumberOfWalls*2)+ds1.NumberOfFloors+ds1.NumberOfShadowLayers+ds1.NumberOfSubstitutionLayers)
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
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
}
func (ds1 *DS1) loadNPCs(br *d2datautils.StreamReader) error {
var err error
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
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
for idx, ds1Obj := range ds1.Objects {
if ds1Obj.X == npcX && ds1Obj.Y == npcY {
objIdx = idx
break
}
}
}
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
if objIdx > -1 {
ds1.loadNpcPaths(br, objIdx, int(numPaths))
} else {
br.SkipBytes(int(numPaths) * 2) //nolint:gomnd // Unknown data
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
}
}
}
}
return err
}
func (ds1 *DS1) loadNpcPaths(br *d2datautils.StreamReader, objIdx, numPaths int) error {
func (ds1 *DS1) loadNpcPaths(br *d2datautils.StreamReader, objIdx, numPaths int) {
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()))
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)
if ds1.Version >= 15 { //nolint:gomnd // Version number
newPath.Action = int(br.GetInt32())
}
ds1.Objects[objIdx].Paths[pathIdx] = newPath
}
return nil
}
func (ds1 *DS1) loadLayerStreams(br *d2datautils.StreamReader) error {
dirLookup := []int32{
func (ds1 *DS1) loadLayerStreams(br *d2datautils.StreamReader, layerStream []d2enum.LayerStreamType) {
var 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,
}
layerStreamTypes := ds1.getLayerSchema()
for lIdx := range layerStream {
layerStreamType := layerStream[lIdx]
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)
}
for y := 0; y < int(ds1.Height); y++ {
for x := 0; x < int(ds1.Width); x++ {
dw := br.GetUInt32()
switch layerStreamType {
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)
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
if ds1.version < v7 {
if ds1.Version < 7 { //nolint:gomnd // Version number
if c < int32(len(dirLookup)) {
c = dirLookup[c]
}
}
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)
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
}
}
}
}
}
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

@ -1,370 +0,0 @@
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

@ -1,333 +0,0 @@
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

@ -1,247 +0,0 @@
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

@ -1,72 +0,0 @@
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

@ -0,0 +1,14 @@
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

@ -1,140 +0,0 @@
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

@ -1,29 +0,0 @@
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,13 +13,3 @@ 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

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

View File

@ -1,127 +0,0 @@
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

@ -0,0 +1,9 @@
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

@ -0,0 +1,17 @@
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,25 +6,8 @@ type Block struct {
Y int16
GridX byte
GridY byte
format int16
Format BlockDataFormat
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,11 +2,15 @@ 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
@ -18,169 +22,55 @@ 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,gocognit,gocyclo // Can't reduce
//nolint:funlen // Can't reduce
func LoadDT1(fileData []byte) (*DT1, error) {
result := &DT1{}
br := d2datautils.CreateStreamReader(fileData)
ver1 := br.GetInt32()
ver2 := br.GetInt32()
var err error
result.majorVersion, err = br.ReadInt32()
if err != nil {
return nil, err
if ver1 != 7 || ver2 != 6 {
return nil, fmt.Errorf("expected to have a version of 7.6, but got %d.%d instead", ver1, ver2)
}
result.minorVersion, err = br.ReadInt32()
if err != nil {
return nil, err
}
br.SkipBytes(260) //nolint:gomnd // Unknown data
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)
}
numberOfTiles := br.GetInt32()
br.SetPosition(uint64(br.GetInt32()))
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)
result.Tiles = make([]Tile, numberOfTiles)
for tileIdx := range result.Tiles {
tile := Tile{}
newTile := Tile{}
newTile.Direction = br.GetInt32()
newTile.RoofHeight = br.GetInt16()
newTile.MaterialFlags = NewMaterialFlags(br.GetUInt16())
newTile.Height = br.GetInt32()
newTile.Width = br.GetInt32()
tile.Direction, err = br.ReadInt32()
if err != nil {
return nil, err
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.RoofHeight, err = br.ReadInt16()
if err != nil {
return nil, err
}
br.SkipBytes(7) //nolint:gomnd // Unknown data
var matFlagBytes uint16
newTile.blockHeaderPointer = br.GetInt32()
newTile.blockHeaderSize = br.GetInt32()
newTile.Blocks = make([]Block, br.GetInt32())
matFlagBytes, err = br.ReadUInt16()
if err != nil {
return nil, err
}
br.SkipBytes(12) //nolint:gomnd // Unknown data
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
result.Tiles[tileIdx] = newTile
}
for tileIdx := range result.Tiles {
@ -188,135 +78,34 @@ func LoadDT1(fileData []byte) (*DT1, error) {
br.SetPosition(uint64(tile.blockHeaderPointer))
for blockIdx := range tile.Blocks {
result.Tiles[tileIdx].Blocks[blockIdx].X, err = br.ReadInt16()
if err != nil {
return nil, err
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].Y, err = br.ReadInt16()
if err != nil {
return nil, err
}
result.Tiles[tileIdx].Blocks[blockIdx].Length = br.GetInt32()
br.SkipBytes(numUnknownBlockBytes)
br.SkipBytes(2) //nolint:gomnd // Unknown data
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
}
result.Tiles[tileIdx].Blocks[blockIdx].FileOffset = br.GetInt32()
}
for blockIndex, block := range tile.Blocks {
br.SetPosition(uint64(tile.blockHeaderPointer + block.FileOffset))
encodedData, err := br.ReadBytes(int(block.Length))
if err != nil {
return nil, err
}
encodedData := br.ReadBytes(int(block.Length))
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,50 +30,3 @@ 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,42 +77,3 @@ 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,7 +2,6 @@ package d2dt1
// Tile is a representation of a map tile
type Tile struct {
unknown2 []byte
Direction int32
RoofHeight int16
MaterialFlags MaterialFlags
@ -17,15 +16,3 @@ 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

@ -1,67 +0,0 @@
// 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

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

View File

@ -1,213 +0,0 @@
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

@ -1,131 +0,0 @@
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

@ -0,0 +1,32 @@
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

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

View File

@ -1,77 +0,0 @@
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, err := m.stream.Read(p, 0, uint32(len(p)))
return int(totalRead), err
totalRead := m.stream.Read(p, 0, uint32(len(p)))
return int(totalRead), nil
}
// Seek sets the position of the data stream
func (m *MpqDataStream) Seek(offset int64, whence int) (int64, error) {
m.stream.Position = uint32(offset + int64(whence))
return int64(m.stream.Position), nil
m.stream.CurrentPosition = uint32(offset + int64(whence))
return int64(m.stream.CurrentPosition), nil
}
// Close closes the data stream

View File

@ -1,45 +0,0 @@
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

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

View File

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

@ -1,40 +0,0 @@
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 (
"fmt"
"log"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
@ -10,97 +10,6 @@ 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
@ -110,151 +19,95 @@ 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, error) {
lookupTable := make(TextDictionary)
func LoadTextDictionary(dictionaryData []byte) TextDictionary {
if lookupTable == nil {
lookupTable = make(TextDictionary)
}
br := d2datautils.CreateStreamReader(dictionaryData)
// skip past the CRC
_, _ = br.ReadBytes(crcByteCount)
br.ReadBytes(crcByteCount)
var err error
numberOfElements := br.GetUInt16()
hashTableSize := br.GetUInt32()
/*
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)
// Version (always 0)
if _, err := br.ReadByte(); err != nil {
log.Fatal("Error reading Version record")
}
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
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
elementIndex := make([]uint16, numberOfElements)
for i := 0; i < int(numberOfElements); i++ {
elementIndex[i], err = br.ReadUInt16()
if err != nil {
return nil, fmt.Errorf("reading element index %d: %v", i, err)
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(),
}
}
hashEntries := make([]*textDictionaryHashEntry, hashTableSize)
for idx, hashEntry := range hashEntries {
if !hashEntry.IsActive {
continue
}
err = lookupTable.loadHashEntries(hashEntries, br)
if err != nil {
return nil, fmt.Errorf("loading has entries: %v", err)
br.SetPosition(uint64(hashEntry.NameString))
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
}
}
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()
return lookupTable
}

View File

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

View File

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

View File

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

View File

@ -6,11 +6,10 @@ import (
// Navigator is used for transitioning between game screens
type Navigator interface {
ToMainMenu(errorMessageOptional ...string)
ToMainMenu()
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,24 +1,19 @@
package d2interface
type renderCallback = func(Surface) error
type updateCallback = func() error
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// Renderer interface defines the functionality of a renderer
type Renderer interface {
GetRendererName() string
SetWindowIcon(fileName string)
Run(r renderCallback, u updateCallback, width, height int, title string) error
Run(f func(Surface) error, width, height int, title string) error
IsDrawingSkipped() bool
CreateSurface(surface Surface) (Surface, error)
NewSurface(width, height int) Surface
NewSurface(width, height int, filter d2enum.Filter) (Surface, error)
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)
Clear(color color.Color) error
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)
Render(surface Surface) error
// Renders a section of the surface enclosed by bounds
RenderSection(surface Surface, bound image.Rectangle)
ReplacePixels(pixels []byte)
RenderSection(surface Surface, bound image.Rectangle) error
ReplacePixels(pixels []byte) error
Screenshot() *image.RGBA
}

View File

@ -13,17 +13,17 @@ type Terminal interface {
OnKeyChars(event KeyCharsEvent) bool
Render(surface Surface) error
Execute(command string) error
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
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
Hide()
Show()
Bind(name, description string, arguments []string, fn func(args []string) error) error
Unbind(name ...string) error
BindAction(name, description string, action interface{}) error
UnbindAction(name string) error
}
// TerminalLogger is used tomake the Terminal write out

View File

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

View File

@ -37,8 +37,7 @@ 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 mpq, err := d2mpq.New(path); err == nil {
_ = mpq.Close()
if _, err := d2mpq.Load(path); err == nil {
return AssetSourceMPQ
}

View File

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

View File

@ -2,29 +2,29 @@ package d2loader
import (
"fmt"
"io"
"os"
"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
errFmtFileNotFound = "file not found: %s"
defaultCacheBudget = 1024 * 1024 * 512
defaultCacheEntryWeight = 1
errFmtFileNotFound = "file not found: %s"
)
const (
logPrefix = "File Loader"
defaultLanguage = "ENG"
)
const (
@ -33,55 +33,64 @@ const (
)
// NewLoader creates a new loader
func NewLoader(l d2util.LogLevel) (*Loader, error) {
func NewLoader(config *d2config.Configuration) *Loader {
loader := &Loader{
LoaderProviders: make(map[types.SourceType]func(path string) (asset.Source, error), 2),
config: config,
}
loader.LoaderProviders[types.AssetSourceMPQ] = mpq.NewSource
loader.LoaderProviders[types.AssetSourceFileSystem] = filesystem.OnAddSource
loader.Cache = d2cache.CreateCache(defaultCacheBudget)
loader.Logger = d2util.NewLogger()
loader.Logger.SetPrefix(logPrefix)
loader.Logger.SetLevel(l)
loader.initFromConfig()
return loader, nil
return loader
}
// Loader represents the manager that handles loading and caching assets with the asset Sources
// that have been added
type Loader struct {
language *string
charset *string
config *d2config.Configuration
d2interface.Cache
*d2util.Logger
LoaderProviders map[types.SourceType]func(path string) (asset.Source, error)
Sources []asset.Source
Sources []asset.Source
}
// SetLanguage sets the language for loader
func (l *Loader) SetLanguage(language *string) {
l.language = language
}
func (l *Loader) initFromConfig() {
if l.config == nil {
return
}
// SetCharset sets the charset for loader
func (l *Loader) SetCharset(charset *string) {
l.charset = charset
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())
}
}
}
// 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) (io.ReadSeeker, error) {
func (l *Loader) Load(subPath string) (asset.Asset, error) {
lang := defaultLanguage
if l.config != nil {
lang = l.config.Language
}
subPath = filepath.Clean(subPath)
subPath = strings.ReplaceAll(subPath, fontToken, "latin")
subPath = strings.ReplaceAll(subPath, tableToken, lang)
if l.language != nil {
charset := l.charset
language := l.language
// 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))
subPath = strings.ReplaceAll(subPath, fontToken, *charset)
subPath = strings.ReplaceAll(subPath, tableToken, *language)
a := cached.(asset.Asset)
_, err := a.Seek(0, 0)
return a, err
}
// if it isn't in the cache, we check if each source can open the file
@ -89,16 +98,10 @@ func (l *Loader) Load(subPath string) (io.ReadSeeker, error) {
source := l.Sources[idx]
// if the source can open the file, then we cache it and return it
loadedAsset, err := source.Open(subPath)
if err != nil {
l.Debug(fmt.Sprintf("Checked `%s`, file not found", source.Path()))
continue
if loadedAsset, err := source.Open(subPath); err == nil {
err := l.Insert(subPath, loadedAsset, defaultCacheEntryWeight)
return loadedAsset, err
}
srcBase, _ := filepath.Abs(source.Path())
l.Info(fmt.Sprintf("Loaded %s -> %s", srcBase, subPath))
return loadedAsset, nil
}
return nil, fmt.Errorf(errFmtFileNotFound, subPath)
@ -108,46 +111,52 @@ func (l *Loader) Load(subPath string) (io.ReadSeeker, 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, sourceType types.SourceType) error {
func (l *Loader) AddSource(path string) (asset.Source, error) {
if l.Sources == nil {
l.Sources = make([]asset.Source, 0)
}
cleanPath := filepath.Clean(path)
source, err := l.LoaderProviders[sourceType](cleanPath)
info, err := os.Lstat(cleanPath)
if err != nil {
return err
l.Warning(err.Error())
return nil, err
}
l.Infof("Adding source: '%s'", cleanPath)
l.Sources = append(l.Sources, source)
mode := info.Mode()
return nil
}
sourceType := types.AssetSourceUnknown
// 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 mode.IsDir() {
sourceType = types.AssetSourceFileSystem
}
// 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 mode.IsRegular() {
sourceType = types.CheckSourceType(cleanPath)
}
// if the source can open the file, then we cache it and return it
if source.Exists(subPath) {
return true
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 false
return nil, fmt.Errorf("unknown asset source `%s`", cleanPath)
}

View File

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

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