├── .gitignore ├── abyssbadge.png ├── abysslogo.png ├── internal └── engine │ ├── doc.go │ ├── configuration │ ├── doc.go │ ├── load.go │ ├── configuration.go │ └── defaults.go │ ├── scenemanager │ └── scenemanager.go │ ├── backends │ ├── graphicsbackend │ │ ├── interface.go │ │ └── sdl2graphicsbackend │ │ │ ├── surface.go │ │ │ └── graphicsbackend.go │ └── inputbackend │ │ ├── mousebutton.go │ │ ├── interface.go │ │ ├── key.go │ │ └── sdl2inputbackend │ │ └── inputbackend.go │ ├── common │ └── enum │ │ └── regionid.go │ ├── engine.go │ └── resource │ ├── musicdefs.go │ ├── languagesmap.go │ └── resourcepaths.go ├── pkg └── fileformats │ ├── pl2file │ ├── doc.go │ ├── pl2_palette.go │ ├── pl2_color_24bits.go │ ├── pl2_color.go │ ├── pl2_palette_transform.go │ └── pl2.go │ ├── mpqfile │ ├── doc.go │ ├── mpq_file_record.go │ ├── mpq_header.go │ ├── mpq_data_stream.go │ ├── mpq_hash.go │ ├── mpq_block.go │ ├── crypto.go │ ├── mpq.go │ └── mpq_stream.go │ ├── tblfile │ ├── doc.go │ └── text_dictionary.go │ ├── coffile │ ├── doc.go │ ├── cof_layer.go │ ├── cof_dir_lookup.go │ └── cof.go │ ├── datfile │ ├── doc.go │ ├── dat.go │ ├── dat_palette.go │ └── dat_color.go │ ├── dc6file │ ├── doc.go │ ├── dc6_frame.go │ ├── dc6_header.go │ ├── dc6_frame_header.go │ ├── dc6.ksy │ └── dc6.go │ ├── dccfile │ ├── doc.go │ ├── dcc_pixel_buffer_entry.go │ ├── dcc_cell.go │ ├── dcc_dir_lookup.go │ ├── dcc.go │ ├── dcc_direction_frame.go │ └── dcc_direction.go │ ├── ds1file │ ├── doc.go │ ├── substitution_record.go │ ├── substitution_group.go │ ├── object.go │ ├── floor_shadow_record.go │ ├── tile_record.go │ ├── wall_record.go │ └── ds1.go │ ├── txtfile │ ├── doc.go │ └── data_dictionary.go │ ├── animdata │ ├── testdata │ │ ├── BadData.d2 │ │ └── AnimData.d2 │ ├── block.go │ ├── events.go │ ├── hash.go │ ├── record.go │ ├── doc.go │ ├── animdata_test.go │ └── animdata.go │ └── dt1file │ ├── doc.go │ ├── block.go │ ├── tile.go │ ├── subtile_test.go │ ├── material.go │ ├── gfx_decode.go │ ├── subtile.go │ └── dt1.go ├── .editorconfig ├── go.mod ├── CONTRIBUTORS ├── .golangci.yml ├── README.md ├── cmd └── abyssengine │ └── main.go ├── CODE_OF_CONDUCT.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vs 3 | .vscode 4 | abyssengine 5 | abyssengine.exe 6 | 7 | -------------------------------------------------------------------------------- /abyssbadge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenDiablo2/AbyssEngine/HEAD/abyssbadge.png -------------------------------------------------------------------------------- /abysslogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenDiablo2/AbyssEngine/HEAD/abysslogo.png -------------------------------------------------------------------------------- /internal/engine/doc.go: -------------------------------------------------------------------------------- 1 | // Package engine represents the main abyss engine 2 | package engine 3 | -------------------------------------------------------------------------------- /pkg/fileformats/pl2file/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2pl2 handles processing of PL2 palette files. 2 | package d2pl2 3 | -------------------------------------------------------------------------------- /pkg/fileformats/mpqfile/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2mpq contains the functions for handling MPQ files. 2 | package mpqfile 3 | -------------------------------------------------------------------------------- /pkg/fileformats/tblfile/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2tbl provides a file parser for tbl string table files 2 | package d2tbl 3 | -------------------------------------------------------------------------------- /pkg/fileformats/coffile/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2cof contains the logic for loading and processing COF files. 2 | package d2cof 3 | -------------------------------------------------------------------------------- /pkg/fileformats/datfile/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2dat contains the logic for loading and processing DAT files. 2 | package d2dat 3 | -------------------------------------------------------------------------------- /pkg/fileformats/dc6file/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2dc6 contains the logic for loading and processing DC6 files. 2 | package d2dc6 3 | -------------------------------------------------------------------------------- /pkg/fileformats/dccfile/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2dcc contains the logic for loading and processing DCC files. 2 | package d2dcc 3 | -------------------------------------------------------------------------------- /pkg/fileformats/ds1file/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2ds1 provides functionality for loading/processing DS1 files 2 | package d2ds1 3 | -------------------------------------------------------------------------------- /pkg/fileformats/txtfile/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2txt provides a parser implementation for diablo TSV data files 2 | package d2txt 3 | -------------------------------------------------------------------------------- /pkg/fileformats/animdata/testdata/BadData.d2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenDiablo2/AbyssEngine/HEAD/pkg/fileformats/animdata/testdata/BadData.d2 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | end_of_line = lf 4 | insert_final_newline = true 5 | charset = utf-8 6 | indent_style = tab 7 | indent_size = 4 8 | -------------------------------------------------------------------------------- /pkg/fileformats/animdata/testdata/AnimData.d2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenDiablo2/AbyssEngine/HEAD/pkg/fileformats/animdata/testdata/AnimData.d2 -------------------------------------------------------------------------------- /pkg/fileformats/animdata/block.go: -------------------------------------------------------------------------------- 1 | package d2animdata 2 | 3 | type block struct { 4 | recordCount uint32 5 | records []*AnimationDataRecord 6 | } 7 | -------------------------------------------------------------------------------- /internal/engine/configuration/doc.go: -------------------------------------------------------------------------------- 1 | // Package configuration contains structures and functions related to the engine's configuration 2 | package configuration 3 | -------------------------------------------------------------------------------- /pkg/fileformats/pl2file/pl2_palette.go: -------------------------------------------------------------------------------- 1 | package d2pl2 2 | 3 | // PL2Palette represents a PL2 palette. 4 | type PL2Palette struct { 5 | Colors [256]PL2Color 6 | } 7 | -------------------------------------------------------------------------------- /pkg/fileformats/dt1file/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2dt1 provides functionality for loading/processing DT1 files. 2 | // https://d2mods.info/forum/viewtopic.php?t=65163 3 | package d2dt1 4 | -------------------------------------------------------------------------------- /pkg/fileformats/pl2file/pl2_color_24bits.go: -------------------------------------------------------------------------------- 1 | package d2pl2 2 | 3 | // PL2Color24Bits represents an RGB color 4 | type PL2Color24Bits struct { 5 | R uint8 6 | G uint8 7 | B uint8 8 | } 9 | -------------------------------------------------------------------------------- /pkg/fileformats/pl2file/pl2_color.go: -------------------------------------------------------------------------------- 1 | package d2pl2 2 | 3 | // PL2Color represents an RGBA color 4 | type PL2Color struct { 5 | R uint8 6 | G uint8 7 | B uint8 8 | _ uint8 9 | } 10 | -------------------------------------------------------------------------------- /pkg/fileformats/pl2file/pl2_palette_transform.go: -------------------------------------------------------------------------------- 1 | package d2pl2 2 | 3 | // PL2PaletteTransform represents a PL2 palette transform. 4 | type PL2PaletteTransform struct { 5 | Indices [256]uint8 6 | } 7 | -------------------------------------------------------------------------------- /pkg/fileformats/ds1file/substitution_record.go: -------------------------------------------------------------------------------- 1 | package d2ds1 2 | 3 | // SubstitutionRecord represents a substitution record in a DS1 file. 4 | type SubstitutionRecord struct { 5 | Unknown uint32 6 | } 7 | -------------------------------------------------------------------------------- /internal/engine/scenemanager/scenemanager.go: -------------------------------------------------------------------------------- 1 | package scenemanager 2 | 3 | type SceneManager struct { 4 | } 5 | 6 | func New() *SceneManager { 7 | result := &SceneManager{} 8 | 9 | return result 10 | } 11 | -------------------------------------------------------------------------------- /pkg/fileformats/mpqfile/mpq_file_record.go: -------------------------------------------------------------------------------- 1 | package mpqfile 2 | 3 | // MpqFileRecord represents a file record in an MPQ 4 | type MpqFileRecord struct { 5 | MpqFile string 6 | IsPatch bool 7 | UnpatchedMpqFile string 8 | } 9 | -------------------------------------------------------------------------------- /pkg/fileformats/dccfile/dcc_pixel_buffer_entry.go: -------------------------------------------------------------------------------- 1 | package d2dcc 2 | 3 | // DCCPixelBufferEntry represents a single entry in the pixel buffer. 4 | type DCCPixelBufferEntry struct { 5 | Value [4]byte 6 | Frame int 7 | FrameCellIndex int 8 | } 9 | -------------------------------------------------------------------------------- /pkg/fileformats/ds1file/substitution_group.go: -------------------------------------------------------------------------------- 1 | package d2ds1 2 | 3 | // SubstitutionGroup represents a substitution group in a DS1 file. 4 | type SubstitutionGroup struct { 5 | TileX int32 6 | TileY int32 7 | WidthInTiles int32 8 | HeightInTiles int32 9 | Unknown int32 10 | } 11 | -------------------------------------------------------------------------------- /pkg/fileformats/ds1file/object.go: -------------------------------------------------------------------------------- 1 | package d2ds1 2 | 3 | import ( 4 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2path" 5 | ) 6 | 7 | // Object is a game world object 8 | type Object struct { 9 | Type int 10 | ID int 11 | X int 12 | Y int 13 | Flags int 14 | Paths []d2path.Path 15 | } 16 | -------------------------------------------------------------------------------- /pkg/fileformats/dccfile/dcc_cell.go: -------------------------------------------------------------------------------- 1 | package d2dcc 2 | 3 | // DCCCell represents a single cell in a DCC file. 4 | type DCCCell struct { 5 | Width int 6 | Height int 7 | XOffset int 8 | YOffset int 9 | LastWidth int 10 | LastHeight int 11 | LastXOffset int 12 | LastYOffset int 13 | } 14 | -------------------------------------------------------------------------------- /pkg/fileformats/dt1file/block.go: -------------------------------------------------------------------------------- 1 | package d2dt1 2 | 3 | // Block represents a DT1 block 4 | type Block struct { 5 | X int16 6 | Y int16 7 | GridX byte 8 | GridY byte 9 | Format BlockDataFormat 10 | EncodedData []byte 11 | Length int32 12 | FileOffset int32 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OpenDiablo2/AbyssEngine 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gravestench/akara v0.0.0-20210116041952-513d453f9ca8 7 | github.com/sirupsen/logrus v1.7.0 8 | github.com/veandco/go-sdl2 v0.4.5 9 | go.uber.org/fx v1.13.1 10 | golang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/fileformats/animdata/events.go: -------------------------------------------------------------------------------- 1 | package d2animdata 2 | 3 | // AnimationEvent represents an event that can happen on a frame of animation 4 | type AnimationEvent byte 5 | 6 | // Animation events 7 | const ( 8 | AnimationEventNone AnimationEvent = iota 9 | AnimationEventAttack 10 | AnimationEventMissile 11 | AnimationEventSound 12 | AnimationEventSkill 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/fileformats/ds1file/floor_shadow_record.go: -------------------------------------------------------------------------------- 1 | package d2ds1 2 | 3 | // FloorShadowRecord represents a floor or shadow record in a DS1 file. 4 | type FloorShadowRecord struct { 5 | Prop1 byte 6 | Sequence byte 7 | Unknown1 byte 8 | Style byte 9 | Unknown2 byte 10 | Hidden bool 11 | RandomIndex byte 12 | Animated bool 13 | YAdjust int 14 | } 15 | -------------------------------------------------------------------------------- /pkg/fileformats/animdata/hash.go: -------------------------------------------------------------------------------- 1 | package d2animdata 2 | 3 | import "strings" 4 | 5 | type hashTable [numBlocks]byte 6 | 7 | func hashName(name string) byte { 8 | hashBytes := []byte(strings.ToUpper(name)) 9 | 10 | var hash uint32 11 | for hashByteIdx := range hashBytes { 12 | hash += uint32(hashBytes[hashByteIdx]) 13 | } 14 | 15 | hash %= numBlocks 16 | 17 | return byte(hash) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/fileformats/coffile/cof_layer.go: -------------------------------------------------------------------------------- 1 | package d2cof 2 | 3 | import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" 4 | 5 | // CofLayer is a structure that represents a single layer in a COF file. 6 | type CofLayer struct { 7 | Type d2enum.CompositeType 8 | Shadow byte 9 | Selectable bool 10 | Transparent bool 11 | DrawEffect d2enum.DrawEffect 12 | WeaponClass d2enum.WeaponClass 13 | } 14 | -------------------------------------------------------------------------------- /pkg/fileformats/dc6file/dc6_frame.go: -------------------------------------------------------------------------------- 1 | package d2dc6 2 | 3 | // DC6Frame represents a single frame in a DC6. 4 | type DC6Frame struct { 5 | Flipped uint32 6 | Width uint32 7 | Height uint32 8 | OffsetX int32 9 | OffsetY int32 10 | Unknown uint32 11 | NextBlock uint32 12 | Length uint32 13 | FrameData []byte // size is the value of Length 14 | Terminator []byte // 3 bytes 15 | } 16 | -------------------------------------------------------------------------------- /pkg/fileformats/ds1file/tile_record.go: -------------------------------------------------------------------------------- 1 | package d2ds1 2 | 3 | // TileRecord represents a tile record in a DS1 file. 4 | type TileRecord struct { 5 | Floors []FloorShadowRecord // Collection of floor records 6 | Walls []WallRecord // Collection of wall records 7 | Shadows []FloorShadowRecord // Collection of shadow records 8 | Substitutions []SubstitutionRecord // Collection of substitutions 9 | } 10 | -------------------------------------------------------------------------------- /pkg/fileformats/dc6file/dc6_header.go: -------------------------------------------------------------------------------- 1 | package d2dc6 2 | 3 | // DC6Header represents the file header of a DC6 file. 4 | type DC6Header struct { 5 | Version int32 `struct:"int32"` 6 | Flags uint32 `struct:"uint32"` 7 | Encoding uint32 `struct:"uint32"` 8 | Termination []byte `struct:"[4]byte"` 9 | Directions int32 `struct:"int32"` 10 | FramesPerDirection int32 `struct:"int32"` 11 | } 12 | -------------------------------------------------------------------------------- /pkg/fileformats/ds1file/wall_record.go: -------------------------------------------------------------------------------- 1 | package d2ds1 2 | 3 | import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" 4 | 5 | // WallRecord represents a wall record. 6 | type WallRecord struct { 7 | Type d2enum.TileType 8 | Zero byte 9 | Prop1 byte 10 | Sequence byte 11 | Unknown1 byte 12 | Style byte 13 | Unknown2 byte 14 | Hidden bool 15 | RandomIndex byte 16 | YAdjust int 17 | } 18 | -------------------------------------------------------------------------------- /pkg/fileformats/dc6file/dc6_frame_header.go: -------------------------------------------------------------------------------- 1 | package d2dc6 2 | 3 | // DC6FrameHeader represents the header of a frame in a DC6. 4 | type DC6FrameHeader struct { 5 | Flipped int32 `struct:"int32"` 6 | Width int32 `struct:"int32"` 7 | Height int32 `struct:"int32"` 8 | OffsetX int32 `struct:"int32"` 9 | OffsetY int32 `struct:"int32"` 10 | Unknown uint32 `struct:"uint32"` 11 | NextBlock uint32 `struct:"uint32"` 12 | Length uint32 `struct:"uint32"` 13 | } 14 | -------------------------------------------------------------------------------- /internal/engine/backends/graphicsbackend/interface.go: -------------------------------------------------------------------------------- 1 | package graphicsbackend 2 | 3 | type Interface interface { 4 | GetRendererName() string 5 | SetWindowIcon(fileName string) 6 | IsFullScreen() bool 7 | SetFullScreen(fullScreen bool) 8 | SetVSyncEnabled(vsync bool) 9 | GetVSyncEnabled() bool 10 | GetCursorPos() (int, int) 11 | CurrentFPS() float64 12 | NewSurface(width, height int, pixelData *[]byte) (Surface, error) 13 | Render() error 14 | } 15 | 16 | type Surface interface { 17 | RenderTo(surface Surface) error 18 | } 19 | -------------------------------------------------------------------------------- /pkg/fileformats/dt1file/tile.go: -------------------------------------------------------------------------------- 1 | package d2dt1 2 | 3 | // Tile is a representation of a map tile 4 | type Tile struct { 5 | Direction int32 6 | RoofHeight int16 7 | MaterialFlags MaterialFlags 8 | Height int32 9 | Width int32 10 | Type int32 11 | Style int32 12 | Sequence int32 13 | RarityFrameIndex int32 14 | SubTileFlags [25]SubTileFlags 15 | blockHeaderPointer int32 16 | blockHeaderSize int32 17 | Blocks []Block 18 | } 19 | -------------------------------------------------------------------------------- /pkg/fileformats/datfile/dat.go: -------------------------------------------------------------------------------- 1 | package d2dat 2 | 3 | import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" 4 | 5 | const ( 6 | // index offset helpers 7 | b = iota 8 | g 9 | r 10 | o 11 | ) 12 | 13 | // Load loads a DAT file. 14 | func Load(data []byte) (d2interface.Palette, error) { 15 | palette := &DATPalette{} 16 | 17 | for i := 0; i < 256; i++ { 18 | // offsets look like i*3+n, where n is 0,1,2 19 | palette.colors[i] = &DATColor{b: data[i*o+b], g: data[i*o+g], r: data[i*o+r]} 20 | } 21 | 22 | return palette, nil 23 | } 24 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | * OPEN DIABLO 2 2 | Tim "essial" Sarbin 3 | ndechiara 4 | mewmew 5 | grazz 6 | Erexo 7 | Ziemas 8 | liberodark 9 | cardoso 10 | Mirey 11 | Lectem 12 | wtfblub 13 | q3cpma 14 | averrin 15 | Dylan "Gravestench" Knuth 16 | Intyre 17 | Gürkan Kaymak 18 | Maxime "malavv" Lavigne 19 | Ripolak 20 | dafe 21 | presiyan 22 | Natureknight 23 | Ganitzsh 24 | 25 | * PATREON SUPPORTERS 26 | K C 27 | Blixt 28 | Dylan Knuth 29 | Brendan Porter 30 | Frazier Phillips 31 | 32 | * DIABLO2 LOGO 33 | Jose Pardilla (th3-prophetman) 34 | 35 | * DT1 File Specifications 36 | Paul SIRAMY 37 | 38 | * Other Specifications and general info 39 | Various users on Phrozen Keep 40 | -------------------------------------------------------------------------------- /pkg/fileformats/dt1file/subtile_test.go: -------------------------------------------------------------------------------- 1 | package d2dt1 2 | 3 | import ( 4 | "testing" 5 | 6 | testify "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewSubTile(t *testing.T) { 10 | assert := testify.New(t) 11 | data := []byte{1, 2, 4, 8, 16, 32, 64, 128} 12 | 13 | for i, b := range data { 14 | tile := NewSubTileFlags(b) 15 | assert.Equal(i == 0, tile.BlockWalk) 16 | assert.Equal(i == 1, tile.BlockLOS) 17 | assert.Equal(i == 2, tile.BlockJump) 18 | assert.Equal(i == 3, tile.BlockPlayerWalk) 19 | assert.Equal(i == 4, tile.Unknown1) 20 | assert.Equal(i == 5, tile.BlockLight) 21 | assert.Equal(i == 6, tile.Unknown2) 22 | assert.Equal(i == 7, tile.Unknown3) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/fileformats/animdata/record.go: -------------------------------------------------------------------------------- 1 | package d2animdata 2 | 3 | // AnimationDataRecord represents a single record from the AnimData.d2 file 4 | type AnimationDataRecord struct { 5 | name string 6 | framesPerDirection uint32 7 | speed uint16 8 | events map[int]AnimationEvent 9 | } 10 | 11 | // FPS returns the frames per second for this animation record 12 | func (r *AnimationDataRecord) FPS() float64 { 13 | speedf := float64(r.speed) 14 | divisorf := float64(speedDivisor) 15 | basef := float64(speedBaseFPS) 16 | 17 | return basef * speedf / divisorf 18 | } 19 | 20 | // FrameDurationMS returns the duration in milliseconds that a frame is displayed 21 | func (r *AnimationDataRecord) FrameDurationMS() float64 { 22 | return milliseconds / r.FPS() 23 | } 24 | -------------------------------------------------------------------------------- /pkg/fileformats/mpqfile/mpq_header.go: -------------------------------------------------------------------------------- 1 | package mpqfile 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | // Header Represents a MPQ file 10 | type Header struct { 11 | Magic [4]byte 12 | HeaderSize uint32 13 | ArchiveSize uint32 14 | FormatVersion uint16 15 | BlockSize uint16 16 | HashTableOffset uint32 17 | BlockTableOffset uint32 18 | HashTableEntries uint32 19 | BlockTableEntries uint32 20 | } 21 | 22 | func (mpq *MPQ) readHeader() error { 23 | if _, err := mpq.file.Seek(0, io.SeekStart); err != nil { 24 | return err 25 | } 26 | 27 | if err := binary.Read(mpq.file, binary.LittleEndian, &mpq.header); err != nil { 28 | return err 29 | } 30 | 31 | if string(mpq.header.Magic[:]) != "MPQ\x1A" { 32 | return errors.New("invalid mpq header") 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/fileformats/datfile/dat_palette.go: -------------------------------------------------------------------------------- 1 | package d2dat 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" 7 | ) 8 | 9 | const ( 10 | numColors = 256 11 | ) 12 | 13 | // DATPalette represents a 256 color palette. 14 | type DATPalette struct { 15 | colors [numColors]d2interface.Color 16 | } 17 | 18 | // NumColors returns the number of colors in the palette 19 | func (p *DATPalette) NumColors() int { 20 | return len(p.colors) 21 | } 22 | 23 | // GetColors returns the slice of colors in the palette 24 | func (p *DATPalette) GetColors() [numColors]d2interface.Color { 25 | return p.colors 26 | } 27 | 28 | // GetColor returns a color by index 29 | func (p *DATPalette) GetColor(idx int) (d2interface.Color, error) { 30 | if color := p.colors[idx]; color != nil { 31 | return color, nil 32 | } 33 | 34 | return nil, fmt.Errorf("cannot find color index '%d in palette'", idx) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/fileformats/mpqfile/mpq_data_stream.go: -------------------------------------------------------------------------------- 1 | package mpqfile 2 | 3 | import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" 4 | 5 | var _ d2interface.DataStream = &MpqDataStream{} // Static check to confirm struct conforms to interface 6 | 7 | // MpqDataStream represents a stream for MPQ data. 8 | type MpqDataStream struct { 9 | stream *Stream 10 | } 11 | 12 | // Read reads data from the data stream 13 | func (m *MpqDataStream) Read(p []byte) (n int, err error) { 14 | totalRead, err := m.stream.Read(p, 0, uint32(len(p))) 15 | return int(totalRead), err 16 | } 17 | 18 | // Seek sets the position of the data stream 19 | func (m *MpqDataStream) Seek(offset int64, whence int) (int64, error) { 20 | m.stream.Position = uint32(offset + int64(whence)) 21 | return int64(m.stream.Position), nil 22 | } 23 | 24 | // Close closes the data stream 25 | func (m *MpqDataStream) Close() error { 26 | m.stream = nil 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/fileformats/dt1file/material.go: -------------------------------------------------------------------------------- 1 | package d2dt1 2 | 3 | // MaterialFlags represents the material flags. Lots of unknowns for now... 4 | type MaterialFlags struct { 5 | Other bool 6 | Water bool 7 | WoodObject bool 8 | InsideStone bool 9 | OutsideStone bool 10 | Dirt bool 11 | Sand bool 12 | Wood bool 13 | Lava bool 14 | Snow bool 15 | } 16 | 17 | // NewMaterialFlags represents the material flags 18 | // nolint:gomnd // Binary values 19 | func NewMaterialFlags(data uint16) MaterialFlags { 20 | return MaterialFlags{ 21 | Other: data&0x0001 == 0x0001, 22 | Water: data&0x0002 == 0x0002, 23 | WoodObject: data&0x0004 == 0x0004, 24 | InsideStone: data&0x0008 == 0x0008, 25 | OutsideStone: data&0x0010 == 0x0010, 26 | Dirt: data&0x0020 == 0x0020, 27 | Sand: data&0x0040 == 0x0040, 28 | Wood: data&0x0080 == 0x0080, 29 | Lava: data&0x0100 == 0x0100, 30 | Snow: data&0x0400 == 0x0400, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/engine/backends/inputbackend/mousebutton.go: -------------------------------------------------------------------------------- 1 | package inputbackend 2 | 3 | // MouseButton represents a traditional 3-button mouse 4 | type MouseButton int 5 | 6 | const ( 7 | // MouseButtonLeft is the left mouse button 8 | MouseButtonLeft MouseButton = iota 9 | // MouseButtonMiddle is the middle mouse button 10 | MouseButtonMiddle 11 | // MouseButtonRight is the right mouse button 12 | MouseButtonRight 13 | // MouseButtonMin is the lowest MouseButton 14 | MouseButtonMin = MouseButtonLeft 15 | // MouseButtonMax is the highest MouseButton 16 | MouseButtonMax = MouseButtonRight 17 | ) 18 | 19 | // MouseButtonMod represents a "modified" mouse button action. This could mean, for example, ctrl-mouse_left 20 | type MouseButtonMod int 21 | 22 | const ( 23 | // MouseButtonModLeft is a modified left mouse button 24 | MouseButtonModLeft MouseButtonMod = 1 << iota 25 | // MouseButtonModMiddle is a modified middle mouse button 26 | MouseButtonModMiddle 27 | // MouseButtonModRight is a modified right mouse button 28 | MouseButtonModRight 29 | ) 30 | -------------------------------------------------------------------------------- /internal/engine/common/enum/regionid.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // RegionIdType represents a region ID 4 | type RegionIdType int //nolint:golint,stylecheck // many changed needed when changing to ID 5 | 6 | // Regions 7 | const ( 8 | RegionNone RegionIdType = iota 9 | RegionAct1Town 10 | RegionAct1Wilderness 11 | RegionAct1Cave 12 | RegionAct1Crypt 13 | RegionAct1Monestary 14 | RegionAct1Courtyard 15 | RegionAct1Barracks 16 | RegionAct1Jail 17 | RegionAct1Cathedral 18 | RegionAct1Catacombs 19 | RegionAct1Tristram 20 | RegionAct2Town 21 | RegionAct2Sewer 22 | RegionAct2Harem 23 | RegionAct2Basement 24 | RegionAct2Desert 25 | RegionAct2Tomb 26 | RegionAct2Lair 27 | RegionAct2Arcane 28 | RegionAct3Town 29 | RegionAct3Jungle 30 | RegionAct3Kurast 31 | RegionAct3Spider 32 | RegionAct3Dungeon 33 | RegionAct3Sewer 34 | RegionAct4Town 35 | RegionAct4Mesa 36 | RegionAct4Lava 37 | RegonAct5Town 38 | RegionAct5Siege 39 | RegionAct5Barricade 40 | RegionAct5Temple 41 | RegionAct5IceCaves 42 | RegionAct5Baal 43 | RegionAct5Lava 44 | ) 45 | -------------------------------------------------------------------------------- /pkg/fileformats/animdata/doc.go: -------------------------------------------------------------------------------- 1 | // Package d2animdata provides a file parser for AnimData files. AnimData files have the '.d2' 2 | // file extension, but we do not call this package `d2d2` because multiple file types share this 3 | // extension, and because the project namespace prefix makes it sound terrible. 4 | package d2animdata 5 | 6 | /* 7 | The AnimData.d2 file is a binary file that contains speed and event data for animations. 8 | 9 | The data is encoded as little-endian binary data 10 | 11 | The file contents look like this: 12 | 13 | type animData struct { 14 | blocks [256]struct{ 15 | recordCount uint8 16 | records []struct{ // *see note below 17 | name [8]byte // last byte is always \0 18 | framesPerDirection uint32 19 | speed uint16 // **see note below 20 | _ uint16 // just padded 0's 21 | events [144]uint8 // ***see not below 22 | } 23 | } 24 | } 25 | 26 | *NOTE: can contain 0 to 67 records 27 | 28 | **NOTE: game fps is assumed to be 25, the speed is calculated as (25 * record.speed / 256) 29 | 30 | **NOTE: Animation events can be one of `None`, `Attack`, `Missile`, `Sound`, or `Skill` 31 | 32 | */ 33 | -------------------------------------------------------------------------------- /internal/engine/backends/graphicsbackend/sdl2graphicsbackend/surface.go: -------------------------------------------------------------------------------- 1 | package sdl2graphicsbackend 2 | 3 | import ( 4 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/backends/graphicsbackend" 5 | "github.com/veandco/go-sdl2/sdl" 6 | ) 7 | 8 | var _ graphicsbackend.Surface = &SDL2Surface{} 9 | 10 | type SDL2Surface struct { 11 | texture *sdl.Texture 12 | width, height int32 13 | renderer *sdl.Renderer 14 | } 15 | 16 | func CreateSDL2Surface(renderer *sdl.Renderer, texture *sdl.Texture, width, height int32) *SDL2Surface { 17 | result := &SDL2Surface{ 18 | renderer: renderer, 19 | texture: texture, 20 | width: width, 21 | height: height, 22 | } 23 | 24 | return result 25 | } 26 | 27 | func (s SDL2Surface) RenderTo(targetSurface graphicsbackend.Surface) error { 28 | sdlTargetSurface := targetSurface.(*SDL2Surface) 29 | 30 | destRect := &sdl.Rect{ 31 | W: s.width, H: s.height, 32 | } 33 | 34 | if err := s.renderer.SetRenderTarget(sdlTargetSurface.texture); err != nil { 35 | return err 36 | } 37 | 38 | if err := s.renderer.Copy(s.texture, nil, destRect); err != nil { 39 | return err 40 | } 41 | 42 | if err := s.renderer.SetRenderTarget(nil); err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/fileformats/pl2file/pl2.go: -------------------------------------------------------------------------------- 1 | package d2pl2 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/go-restruct/restruct" 7 | ) 8 | 9 | // PL2 represents a palette file. 10 | type PL2 struct { 11 | BasePalette PL2Palette 12 | 13 | LightLevelVariations [32]PL2PaletteTransform 14 | InvColorVariations [16]PL2PaletteTransform 15 | SelectedUintShift PL2PaletteTransform 16 | AlphaBlend [3][256]PL2PaletteTransform 17 | AdditiveBlend [256]PL2PaletteTransform 18 | MultiplicativeBlend [256]PL2PaletteTransform 19 | HueVariations [111]PL2PaletteTransform 20 | RedTones PL2PaletteTransform 21 | GreenTones PL2PaletteTransform 22 | BlueTones PL2PaletteTransform 23 | UnknownVariations [14]PL2PaletteTransform 24 | MaxComponentBlend [256]PL2PaletteTransform 25 | DarkendColorShift PL2PaletteTransform 26 | 27 | TextColors [13]PL2Color24Bits 28 | TextColorShifts [13]PL2PaletteTransform 29 | } 30 | 31 | // Load uses restruct to read the binary pl2 data into structs 32 | func Load(data []byte) (*PL2, error) { 33 | result := &PL2{} 34 | 35 | restruct.EnableExprBeta() 36 | 37 | err := restruct.Unpack(data, binary.LittleEndian, &result) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return result, nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/fileformats/mpqfile/mpq_hash.go: -------------------------------------------------------------------------------- 1 | package mpqfile 2 | 3 | import "io" 4 | 5 | // Hash represents a hashed file entry in the MPQ file 6 | type Hash struct { // 16 bytes 7 | A uint32 8 | B uint32 9 | Locale uint16 10 | Platform uint16 11 | BlockIndex uint32 12 | } 13 | 14 | // Name64 returns part A and B as uint64 15 | func (h *Hash) Name64() uint64 { 16 | return uint64(h.A)<<32 | uint64(h.B) 17 | } 18 | 19 | //nolint:gomnd // number 20 | func (mpq *MPQ) readHashTable() error { 21 | if _, err := mpq.file.Seek(int64(mpq.header.HashTableOffset), io.SeekStart); err != nil { 22 | return err 23 | } 24 | 25 | hashData, err := decryptTable(mpq.file, mpq.header.HashTableEntries, "(hash table)") 26 | if err != nil { 27 | return err 28 | } 29 | 30 | mpq.hashes = make(map[uint64]*Hash) 31 | 32 | for n, i := uint32(0), uint32(0); i < mpq.header.HashTableEntries; n, i = n+4, i+1 { 33 | e := &Hash{ 34 | A: hashData[n], 35 | B: hashData[n+1], 36 | // https://github.com/OpenDiablo2/OpenDiablo2/issues/812 37 | Locale: uint16(hashData[n+2] >> 16), //nolint:gomnd // // binary data 38 | Platform: uint16(hashData[n+2] & 0xFFFF), //nolint:gomnd // // binary data 39 | BlockIndex: hashData[n+3], 40 | } 41 | mpq.hashes[e.Name64()] = e 42 | } 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/engine/backends/inputbackend/interface.go: -------------------------------------------------------------------------------- 1 | package inputbackend 2 | 3 | // Interface represents an interface offering Keyboard and Mouse interactions. 4 | type Interface interface { 5 | // Process processes any events. 6 | Process() error 7 | // CursorPosition returns a position of a mouse cursor relative to the game screen (window). 8 | CursorPosition() (x int, y int) 9 | // InputChars return "printable" runes read from the keyboard at the time update is called. 10 | InputChars() []rune 11 | // IsKeyPressed checks if the provided key is down. 12 | IsKeyPressed(key Key) bool 13 | // IsKeyJustPressed checks if the provided key is just transitioned from up to down. 14 | IsKeyJustPressed(key Key) bool 15 | // IsKeyJustReleased checks if the provided key is just transitioned from down to up. 16 | IsKeyJustReleased(key Key) bool 17 | // IsMouseButtonPressed checks if the provided mouse button is down. 18 | IsMouseButtonPressed(button MouseButton) bool 19 | // IsMouseButtonJustPressed checks if the provided mouse button is just transitioned from up to down. 20 | IsMouseButtonJustPressed(button MouseButton) bool 21 | // IsMouseButtonJustReleased checks if the provided mouse button is just transitioned from down to up. 22 | IsMouseButtonJustReleased(button MouseButton) bool 23 | // KeyPressDuration returns how long the key is pressed in frames. 24 | KeyPressDuration(key Key) int 25 | } 26 | -------------------------------------------------------------------------------- /internal/engine/configuration/load.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | const ( 13 | od2ConfigDirName = "OpenDiablo2" 14 | od2ConfigFileName = "config.json" 15 | ) 16 | 17 | // Load loads the configuration file 18 | func Load() (*Configuration, error) { 19 | configFileFullPath := "" 20 | 21 | if configDir, err := os.UserConfigDir(); err == nil { 22 | configFileFullPath = path.Join(configDir, od2ConfigDirName, od2ConfigFileName) 23 | } else { 24 | configFileFullPath = path.Join(path.Dir(os.Args[0]), od2ConfigFileName) 25 | } 26 | 27 | result := &Configuration{} 28 | 29 | configAsset, err := os.Open(configFileFullPath) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | // create the default if not found 35 | if configAsset != nil { 36 | if err := json.NewDecoder(configAsset).Decode(result); err != nil { 37 | return nil, err 38 | } 39 | 40 | if err := configAsset.Close(); err != nil { 41 | return nil, err 42 | } 43 | 44 | return result, nil 45 | } 46 | 47 | result = DefaultConfig() 48 | 49 | fullPath := filepath.Join(result.Dir(), result.Base()) 50 | result.SetPath(fullPath) 51 | 52 | log.Warnf("creating default configuration file at %s...", fullPath) 53 | 54 | saveErr := result.Save() 55 | 56 | return result, saveErr 57 | } 58 | -------------------------------------------------------------------------------- /pkg/fileformats/dt1file/gfx_decode.go: -------------------------------------------------------------------------------- 1 | package d2dt1 2 | 3 | const ( 4 | blockDataLength = 256 5 | ) 6 | 7 | // DecodeTileGfxData decodes tile graphics data for a slice of dt1 blocks 8 | func DecodeTileGfxData(blocks []Block, pixels *[]byte, tileYOffset, tileWidth int32) { 9 | for _, block := range blocks { 10 | if block.Format == BlockFormatIsometric { 11 | // 3D isometric decoding 12 | xjump := []int32{14, 12, 10, 8, 6, 4, 2, 0, 2, 4, 6, 8, 10, 12, 14} 13 | nbpix := []int32{4, 8, 12, 16, 20, 24, 28, 32, 28, 24, 20, 16, 12, 8, 4} 14 | blockX := int32(block.X) 15 | blockY := int32(block.Y) 16 | length := int32(blockDataLength) 17 | x := int32(0) 18 | y := int32(0) 19 | idx := 0 20 | 21 | for length > 0 { 22 | x = xjump[y] 23 | n := nbpix[y] 24 | length -= n 25 | 26 | for n > 0 { 27 | offset := ((blockY + y + tileYOffset) * tileWidth) + (blockX + x) 28 | (*pixels)[offset] = block.EncodedData[idx] 29 | x++ 30 | n-- 31 | idx++ 32 | } 33 | y++ 34 | } 35 | 36 | continue 37 | } 38 | // RLE Encoding 39 | blockX := int32(block.X) 40 | blockY := int32(block.Y) 41 | x := int32(0) 42 | y := int32(0) 43 | idx := 0 44 | length := block.Length 45 | 46 | for length > 0 { 47 | b1 := block.EncodedData[idx] 48 | b2 := block.EncodedData[idx+1] 49 | idx += 2 50 | length -= 2 51 | 52 | if (b1 | b2) == 0 { 53 | x = 0 54 | y++ 55 | 56 | continue 57 | } 58 | 59 | x += int32(b1) 60 | length -= int32(b2) 61 | 62 | for b2 > 0 { 63 | offset := ((blockY + y + tileYOffset) * tileWidth) + (blockX + x) 64 | (*pixels)[offset] = block.EncodedData[idx] 65 | idx++ 66 | x++ 67 | b2-- 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/fileformats/dc6file/dc6.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: dc6 3 | title: Diablo CEL 6 4 | application: Diablo II 5 | file-extension: dc6 6 | license: MIT 7 | ks-version: 0.7 8 | encoding: ASCII 9 | endian: le 10 | seq: 11 | - id: dc6 12 | type: file 13 | types: 14 | file: 15 | seq: 16 | - id: header 17 | type: file_header 18 | - id: frame_pointers 19 | type: u4 20 | repeat: expr 21 | repeat-expr: header.directions * header.frames_per_dir 22 | - id: frames 23 | type: frame 24 | repeat: expr 25 | repeat-expr: header.directions * header.frames_per_dir 26 | file_header: 27 | seq: 28 | - id: version 29 | type: s4 30 | - id: flags 31 | type: u4 32 | enum: flags 33 | - id: encoding 34 | type: u4 35 | - id: termination 36 | size: 4 37 | - id: directions 38 | type: s4 39 | - id: frames_per_dir 40 | type: s4 41 | enums: 42 | flags: 43 | 1: celfile_serialised 44 | 4: celfile_24bit 45 | frame: 46 | seq: 47 | - id: header 48 | type: frame_header 49 | - id: block 50 | type: u1 51 | repeat: expr 52 | repeat-expr: header.length 53 | - id: terminator 54 | size: 3 55 | types: 56 | frame_header: 57 | seq: 58 | - id: flipped 59 | type: s4 60 | - id: width 61 | type: s4 62 | - id: height 63 | type: s4 64 | - id: offset_x 65 | type: s4 66 | - id: offset_y 67 | type: s4 68 | - id: unknown 69 | type: u4 70 | - id: next_block 71 | type: s4 72 | - id: length 73 | type: s4 74 | -------------------------------------------------------------------------------- /internal/engine/configuration/configuration.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | ) 9 | 10 | // Configuration represents the engine's configuration file. 11 | type Configuration struct { 12 | MpqLoadOrder []string 13 | MpqPath string 14 | TicksPerSecond int 15 | FpsCap int 16 | SfxVolume float64 17 | BgmVolume float64 18 | FullScreen bool 19 | RunInBackground bool 20 | VsyncEnabled bool 21 | Backend string 22 | filePath string 23 | } 24 | 25 | // Save saves the configuration object to disk 26 | func (c *Configuration) Save() error { 27 | configDir := path.Dir(c.filePath) 28 | if err := os.MkdirAll(configDir, 0o750); err != nil { 29 | return err 30 | } 31 | 32 | configFile, err := os.Create(c.filePath) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | buf, err := json.MarshalIndent(c, "", " ") 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if _, err := configFile.Write(buf); err != nil { 43 | return err 44 | } 45 | 46 | return configFile.Close() 47 | } 48 | 49 | // Dir returns the directory component of the path 50 | func (c *Configuration) Dir() string { 51 | return filepath.Dir(c.filePath) 52 | } 53 | 54 | // Base returns the base component of the path 55 | func (c *Configuration) Base() string { 56 | return filepath.Base(c.filePath) 57 | } 58 | 59 | // Path returns the configuration file path 60 | func (c *Configuration) Path() string { 61 | return c.filePath 62 | } 63 | 64 | // SetPath sets where the configuration file is saved to (a full path) 65 | func (c *Configuration) SetPath(p string) { 66 | c.filePath = p 67 | } 68 | 69 | func New() *Configuration { 70 | result, err := Load() 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | return result 76 | } 77 | -------------------------------------------------------------------------------- /pkg/fileformats/datfile/dat_color.go: -------------------------------------------------------------------------------- 1 | package d2dat 2 | 3 | // DATColor represents a single color in a DAT file. 4 | type DATColor struct { 5 | r uint8 6 | g uint8 7 | b uint8 8 | a uint8 9 | } 10 | 11 | const ( 12 | colorBits = 8 13 | mask = 0xff 14 | ) 15 | 16 | const ( 17 | bitShift0 = iota * colorBits 18 | bitShift8 19 | bitShift16 20 | bitShift24 21 | ) 22 | 23 | // R gets the red component 24 | func (c *DATColor) R() uint8 { 25 | return c.r 26 | } 27 | 28 | // G gets the green component 29 | func (c *DATColor) G() uint8 { 30 | return c.g 31 | } 32 | 33 | // B gets the blue component 34 | func (c *DATColor) B() uint8 { 35 | return c.b 36 | } 37 | 38 | // A gets the alpha component 39 | func (c *DATColor) A() uint8 { 40 | return mask 41 | } 42 | 43 | // RGBA gets the combination of the color components (0xRRGGBBAA) 44 | func (c *DATColor) RGBA() uint32 { 45 | return toComposite(c.r, c.g, c.b, c.a) 46 | } 47 | 48 | // SetRGBA sets the color components using the given RGBA form 49 | func (c *DATColor) SetRGBA(rgba uint32) { 50 | c.r, c.g, c.b, c.a = toComponent(rgba) 51 | } 52 | 53 | // BGRA gets the combination of the color components (0xBBGGRRAA) 54 | func (c *DATColor) BGRA() uint32 { 55 | return toComposite(c.b, c.g, c.r, c.a) 56 | } 57 | 58 | // SetBGRA sets the color components using the given BGRA form 59 | func (c *DATColor) SetBGRA(bgra uint32) { 60 | c.b, c.g, c.r, c.a = toComponent(bgra) 61 | } 62 | 63 | func toComposite(w, x, y, z uint8) uint32 { 64 | composite := uint32(w) << bitShift24 65 | composite += uint32(x) << bitShift16 66 | composite += uint32(y) << bitShift8 67 | composite += uint32(z) << bitShift0 68 | 69 | return composite 70 | } 71 | 72 | func toComponent(wxyz uint32) (w, x, y, z uint8) { 73 | w = uint8(wxyz >> bitShift24 & mask) 74 | x = uint8(wxyz >> bitShift16 & mask) 75 | y = uint8(wxyz >> bitShift8 & mask) 76 | z = uint8(wxyz >> bitShift0 & mask) 77 | 78 | return w, x, y, z 79 | } 80 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters-settings: 3 | dupl: 4 | threshold: 100 5 | funlen: 6 | lines: 100 7 | statements: 50 8 | goconst: 9 | min-len: 2 10 | min-occurrences: 2 11 | gocritic: 12 | enabled-tags: 13 | - diagnostic 14 | - experimental 15 | - opinionated 16 | - performance 17 | - style 18 | disabled-checks: 19 | gocyclo: 20 | min-complexity: 15 21 | goimports: 22 | local-prefixes: github.com/OpenDiablo2/OpenDiablo2 23 | golint: 24 | min-confidence: 0.8 25 | govet: 26 | check-shadowing: true 27 | lll: 28 | line-length: 140 29 | maligned: 30 | suggest-new: true 31 | misspell: 32 | locale: US 33 | 34 | linters: 35 | disable-all: true 36 | enable: 37 | - bodyclose 38 | - deadcode 39 | - depguard 40 | - dogsled 41 | - dupl 42 | - errcheck 43 | - funlen 44 | - gochecknoglobals 45 | - gochecknoinits 46 | - gocognit 47 | - goconst 48 | - gocritic 49 | - gocyclo 50 | - godox 51 | - gofmt 52 | - goimports 53 | - golint 54 | - gomnd 55 | - goprintffuncname 56 | - gosec 57 | - gosimple 58 | - govet 59 | - ineffassign 60 | - interfacer 61 | - lll 62 | - maligned 63 | - misspell 64 | - nakedret 65 | - prealloc 66 | - rowserrcheck 67 | - scopelint 68 | - staticcheck 69 | - structcheck 70 | - stylecheck 71 | - typecheck 72 | - unconvert 73 | - unparam 74 | - unused 75 | - varcheck 76 | - whitespace 77 | - wsl 78 | 79 | run: 80 | timeout: 5m 81 | skip-dirs: 82 | - .github 83 | - build 84 | - web 85 | 86 | issues: 87 | max-same-issues: 0 88 | exclude-use-default: false 89 | -------------------------------------------------------------------------------- /pkg/fileformats/coffile/cof_dir_lookup.go: -------------------------------------------------------------------------------- 1 | package d2cof 2 | 3 | type directionCount int 4 | 5 | const ( 6 | four directionCount = 4 << iota 7 | eight 8 | sixteen 9 | thirtyTwo 10 | sixtyFour 11 | ) 12 | 13 | // Dir64ToCof returns the cof direction based on the actual direction 14 | func Dir64ToCof(direction, numDirections int) int { 15 | var dir4 = [64]int{ 16 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 17 | 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 18 | 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 19 | 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0} 20 | 21 | var dir8 = [64]int{ 22 | 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 23 | 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 24 | 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 25 | 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 0, 0, 0, 0} 26 | 27 | var dir16 = [64]int{ 28 | 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 29 | 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 30 | 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 31 | 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 0, 0} 32 | 33 | var dir32 = [64]int{ 34 | 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 35 | 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 36 | 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 37 | 24, 25, 25, 26, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31, 31, 0} 38 | 39 | var dir64 = [64]int{ 40 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 41 | 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 42 | 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 43 | 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63} 44 | 45 | switch directionCount(numDirections) { 46 | case four: 47 | return dir4[direction] 48 | case eight: 49 | return dir8[direction] 50 | case sixteen: 51 | return dir16[direction] 52 | case thirtyTwo: 53 | return dir32[direction] 54 | case sixtyFour: 55 | return dir64[direction] 56 | default: 57 | return 0 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/fileformats/dt1file/subtile.go: -------------------------------------------------------------------------------- 1 | package d2dt1 2 | 3 | // SubTileFlags represent the sub-tile flags for a DT1 4 | type SubTileFlags struct { 5 | BlockWalk bool 6 | BlockLOS bool 7 | BlockJump bool 8 | BlockPlayerWalk bool 9 | Unknown1 bool 10 | BlockLight bool 11 | Unknown2 bool 12 | Unknown3 bool 13 | } 14 | 15 | // Combine combines a second set of flags into the current one 16 | func (s *SubTileFlags) Combine(f SubTileFlags) { 17 | s.BlockWalk = s.BlockWalk || f.BlockWalk 18 | s.BlockLOS = s.BlockLOS || f.BlockLOS 19 | s.BlockJump = s.BlockJump || f.BlockJump 20 | s.BlockPlayerWalk = s.BlockPlayerWalk || f.BlockPlayerWalk 21 | s.Unknown1 = s.Unknown1 || f.Unknown1 22 | s.BlockLight = s.BlockLight || f.BlockLight 23 | s.Unknown2 = s.Unknown2 || f.Unknown2 24 | s.Unknown3 = s.Unknown3 || f.Unknown3 25 | } 26 | 27 | // DebugString returns the debug string 28 | func (s *SubTileFlags) DebugString() string { 29 | result := "" 30 | 31 | if s.BlockWalk { 32 | result += "BlockWalk " 33 | } 34 | 35 | if s.BlockLOS { 36 | result += "BlockLOS " 37 | } 38 | 39 | if s.BlockJump { 40 | result += "BlockJump " 41 | } 42 | 43 | if s.BlockPlayerWalk { 44 | result += "BlockPlayerWalk " 45 | } 46 | 47 | if s.Unknown1 { 48 | result += "Unknown1 " 49 | } 50 | 51 | if s.BlockLight { 52 | result += "BlockLight " 53 | } 54 | 55 | if s.Unknown2 { 56 | result += "Unknown2 " 57 | } 58 | 59 | if s.Unknown3 { 60 | result += "Unknown3 " 61 | } 62 | 63 | return result 64 | } 65 | 66 | // NewSubTileFlags returns a list of new subtile flags 67 | //nolint:gomnd // binary flags 68 | func NewSubTileFlags(data byte) SubTileFlags { 69 | return SubTileFlags{ 70 | BlockWalk: data&1 == 1, 71 | BlockLOS: data&2 == 2, 72 | BlockJump: data&4 == 4, 73 | BlockPlayerWalk: data&8 == 8, 74 | Unknown1: data&16 == 16, 75 | BlockLight: data&32 == 32, 76 | Unknown2: data&64 == 64, 77 | Unknown3: data&128 == 128, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pkg/fileformats/dccfile/dcc_dir_lookup.go: -------------------------------------------------------------------------------- 1 | package d2dcc 2 | 3 | type directionCount int 4 | 5 | const ( 6 | four directionCount = 4 << iota 7 | eight 8 | sixteen 9 | thirtyTwo 10 | sixtyFour 11 | ) 12 | 13 | // Dir64ToDcc returns the DCC direction based on the actual direction. 14 | // Special thanks for Necrolis for these tables! 15 | func Dir64ToDcc(direction, numDirections int) int { 16 | var dir4 = [64]int{ 17 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 18 | 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 19 | 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 20 | 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0} 21 | 22 | var dir8 = [64]int{ 23 | 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 24 | 5, 5, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6, 6, 25 | 6, 6, 6, 6, 2, 2, 2, 2, 2, 2, 2, 2, 7, 7, 7, 7, 26 | 7, 7, 7, 7, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4} 27 | 28 | var dir16 = [64]int{ 29 | 4, 4, 8, 8, 8, 8, 0, 0, 0, 0, 9, 9, 9, 9, 5, 5, 30 | 5, 5, 10, 10, 10, 10, 1, 1, 1, 1, 11, 11, 11, 11, 6, 6, 31 | 6, 6, 12, 12, 12, 12, 2, 2, 2, 2, 13, 13, 13, 13, 7, 7, 32 | 7, 7, 14, 14, 14, 14, 3, 3, 3, 3, 15, 15, 15, 15, 4, 4} 33 | 34 | var dir32 = [64]int{ 35 | 4, 16, 16, 8, 8, 17, 17, 0, 0, 18, 18, 9, 9, 19, 19, 5, 36 | 5, 20, 20, 10, 10, 21, 21, 1, 1, 22, 22, 11, 11, 23, 23, 6, 37 | 6, 24, 24, 12, 12, 25, 25, 2, 2, 26, 26, 13, 13, 27, 27, 7, 38 | 7, 28, 28, 14, 14, 29, 29, 3, 3, 30, 30, 15, 15, 31, 31, 4} 39 | 40 | var dir64 = [64]int{ 41 | 4, 32, 16, 33, 8, 34, 17, 35, 0, 36, 18, 37, 9, 38, 19, 39, 42 | 5, 40, 20, 41, 10, 42, 21, 43, 1, 44, 22, 45, 11, 46, 23, 47, 43 | 6, 48, 24, 49, 12, 50, 25, 51, 2, 52, 26, 53, 13, 54, 27, 55, 44 | 7, 56, 28, 57, 14, 58, 29, 59, 3, 60, 30, 61, 15, 62, 31, 63} 45 | 46 | switch directionCount(numDirections) { 47 | case four: 48 | return dir4[direction] 49 | case eight: 50 | return dir8[direction] 51 | case sixteen: 52 | return dir16[direction] 53 | case thirtyTwo: 54 | return dir32[direction] 55 | case sixtyFour: 56 | return dir64[direction] 57 | default: 58 | return 0 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Abyss Engine 2 | 3 | ![Logo](abysslogo.png) 4 | 5 | [Join us on Discord!](https://discord.gg/pRy8tdc)\ 6 | [Development Live stream](https://www.twitch.tv/essial/)\ 7 | [Support us on Patreon](https://www.patreon.com/bePatron?u=37261055) 8 | 9 | We are also working on a toolset:\ 10 | [https://github.com/OpenDiablo2/HellSpawner](https://github.com/OpenDiablo2/HellSpawner)\ 11 | Please consider helping out with this project as well! 12 | 13 | ## About this project 14 | 15 | Abyss Engine is an ARPG game engine in the same vein of the 2000's games, and supports playing games similar to Diablo 2. The engine is written in golang and is cross platform. This engine does not ship with game specific files, and will require a game's assets in order to run. 16 | 17 | 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. 18 | 19 | Please note that **this game is neither developed by, nor endorsed by Blizzard or its parent company Activision**. 20 | 21 | 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. 22 | 23 | ALL OTHER TRADEMARKS ARE THE PROPERTY OF THEIR RESPECTIVE OWNERS. 24 | 25 | ## Status 26 | 27 | At the moment (december 2020) the game starts, you can select any character and run around Act1 town. 28 | You can also open any of the game's panels. 29 | 30 | Much work has been made in the background, but a lot of work still has to be done for the game to be playable. 31 | 32 | Feel free to contribute! 33 | 34 | ## Additional Credits 35 | 36 | - Diablo2 Logo 37 | - Jose Pardilla (th3-prophetman) 38 | - DT1 File Specifications 39 | - Paul SIRAMY (http://paul.siramy.free.fr/_divers/dt1_doc/) 40 | - Other Specifications and general info 41 | - Various users on [Phrozen Keep](https://d2mods.info/home.php) 42 | -------------------------------------------------------------------------------- /internal/engine/backends/inputbackend/key.go: -------------------------------------------------------------------------------- 1 | package inputbackend 2 | 3 | // Key represents button on a traditional keyboard. 4 | type Key int 5 | 6 | // Input keys 7 | const ( 8 | Key0 Key = iota 9 | Key1 10 | Key2 11 | Key3 12 | Key4 13 | Key5 14 | Key6 15 | Key7 16 | Key8 17 | Key9 18 | KeyA 19 | KeyB 20 | KeyC 21 | KeyD 22 | KeyE 23 | KeyF 24 | KeyG 25 | KeyH 26 | KeyI 27 | KeyJ 28 | KeyK 29 | KeyL 30 | KeyM 31 | KeyN 32 | KeyO 33 | KeyP 34 | KeyQ 35 | KeyR 36 | KeyS 37 | KeyT 38 | KeyU 39 | KeyV 40 | KeyW 41 | KeyX 42 | KeyY 43 | KeyZ 44 | KeyApostrophe 45 | KeyBackslash 46 | KeyBackspace 47 | KeyCapsLock 48 | KeyComma 49 | KeyDelete 50 | KeyDown 51 | KeyEnd 52 | KeyEnter 53 | KeyEqual 54 | KeyEscape 55 | KeyF1 56 | KeyF2 57 | KeyF3 58 | KeyF4 59 | KeyF5 60 | KeyF6 61 | KeyF7 62 | KeyF8 63 | KeyF9 64 | KeyF10 65 | KeyF11 66 | KeyF12 67 | KeyGraveAccent 68 | KeyHome 69 | KeyInsert 70 | KeyKP0 71 | KeyKP1 72 | KeyKP2 73 | KeyKP3 74 | KeyKP4 75 | KeyKP5 76 | KeyKP6 77 | KeyKP7 78 | KeyKP8 79 | KeyKP9 80 | KeyKPAdd 81 | KeyKPDecimal 82 | KeyKPDivide 83 | KeyKPEnter 84 | KeyKPEqual 85 | KeyKPMultiply 86 | KeyKPSubtract 87 | KeyLeft 88 | KeyLeftBracket 89 | KeyMenu 90 | KeyMinus 91 | KeyNumLock 92 | KeyPageDown 93 | KeyPageUp 94 | KeyPause 95 | KeyPeriod 96 | KeyPrintScreen 97 | KeyRight 98 | KeyRightBracket 99 | KeyScrollLock 100 | KeySemicolon 101 | KeySlash 102 | KeySpace 103 | KeyTab 104 | KeyUp 105 | KeyAlt 106 | KeyControl 107 | KeyShift 108 | KeyTilde 109 | KeyMouse3 110 | KeyMouse4 111 | KeyMouse5 112 | KeyMouseWheelUp 113 | KeyMouseWheelDown 114 | 115 | KeyMin = Key0 116 | KeyMax = KeyMouseWheelDown 117 | ) 118 | 119 | // KeyMod represents a "modified" key action. This could mean, for example, ctrl-S 120 | type KeyMod int 121 | 122 | const ( 123 | // KeyModAlt is the Alt key modifier 124 | KeyModAlt KeyMod = 1 << iota 125 | // KeyModControl is the Control key modifier 126 | KeyModControl 127 | // KeyModShift is the Shift key modifier 128 | KeyModShift 129 | ) 130 | -------------------------------------------------------------------------------- /pkg/fileformats/txtfile/data_dictionary.go: -------------------------------------------------------------------------------- 1 | package d2txt 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "io" 7 | "log" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // DataDictionary represents a data file (Excel) 13 | type DataDictionary struct { 14 | lookup map[string]int 15 | r *csv.Reader 16 | record []string 17 | Err error 18 | } 19 | 20 | // LoadDataDictionary loads the contents of a spreadsheet style txt file 21 | func LoadDataDictionary(buf []byte) *DataDictionary { 22 | cr := csv.NewReader(bytes.NewReader(buf)) 23 | cr.Comma = '\t' 24 | cr.ReuseRecord = true 25 | 26 | fieldNames, err := cr.Read() 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | data := &DataDictionary{ 32 | lookup: make(map[string]int, len(fieldNames)), 33 | r: cr, 34 | } 35 | 36 | for i, name := range fieldNames { 37 | data.lookup[name] = i 38 | } 39 | 40 | return data 41 | } 42 | 43 | // Next reads the next row, skips Expansion lines or 44 | // returns false when the end of a file is reached or an error occurred 45 | func (d *DataDictionary) Next() bool { 46 | var err error 47 | d.record, err = d.r.Read() 48 | 49 | if err == io.EOF { 50 | return false 51 | } else if err != nil { 52 | d.Err = err 53 | return false 54 | } 55 | 56 | if d.record[0] == "Expansion" { 57 | return d.Next() 58 | } 59 | 60 | return true 61 | } 62 | 63 | // String gets a string from the given column 64 | func (d *DataDictionary) String(field string) string { 65 | return d.record[d.lookup[field]] 66 | } 67 | 68 | // Number gets a number for the given column 69 | func (d *DataDictionary) Number(field string) int { 70 | n, err := strconv.Atoi(d.String(field)) 71 | if err != nil { 72 | return 0 73 | } 74 | 75 | return n 76 | } 77 | 78 | // List splits a delimited list from the given column 79 | func (d *DataDictionary) List(field string) []string { 80 | str := d.String(field) 81 | return strings.Split(str, ",") 82 | } 83 | 84 | // Bool gets a bool value for the given column 85 | func (d *DataDictionary) Bool(field string) bool { 86 | n := d.Number(field) 87 | if n > 1 { 88 | log.Panic("Bool on non-bool field ", field) 89 | } 90 | 91 | return n == 1 92 | } 93 | -------------------------------------------------------------------------------- /cmd/abyssengine/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/OpenDiablo2/AbyssEngine/internal/engine" 8 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/backends/graphicsbackend" 9 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/backends/graphicsbackend/sdl2graphicsbackend" 10 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/backends/inputbackend" 11 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/backends/inputbackend/sdl2inputbackend" 12 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/configuration" 13 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/scenemanager" 14 | log "github.com/sirupsen/logrus" 15 | "go.uber.org/fx" 16 | ) 17 | 18 | const sdl2BackendName = "sdl2" 19 | 20 | func main() { 21 | configureLogging() 22 | log.Printf("AbyssEngine - The open source ARPG engine") 23 | 24 | fx.New( 25 | fx.Options(fx.NopLogger), 26 | fx.Provide( 27 | // Standard instantiations 28 | configuration.New, 29 | engine.New, 30 | scenemanager.New, 31 | 32 | // Implementation-specific instantiations 33 | getGraphicsBackend, 34 | getInputBackend, 35 | ), 36 | fx.Invoke(run), 37 | ).Run() 38 | } 39 | 40 | func run(e *engine.Engine) error { 41 | return e.Run() 42 | } 43 | 44 | func configureLogging() { 45 | formatter := &log.TextFormatter{ 46 | PadLevelText: true, 47 | DisableTimestamp: true, 48 | } 49 | 50 | log.SetFormatter(formatter) 51 | log.SetOutput(os.Stdout) 52 | log.SetLevel(log.InfoLevel) 53 | } 54 | 55 | func getGraphicsBackend(config *configuration.Configuration) graphicsbackend.Interface { 56 | switch strings.ToLower(config.Backend) { 57 | case sdl2BackendName: 58 | result, err := sdl2graphicsbackend.Create() 59 | if err != nil { 60 | log.Panic(err) 61 | } 62 | 63 | return result 64 | default: 65 | panic("unknown backend") 66 | } 67 | } 68 | 69 | func getInputBackend(config *configuration.Configuration) inputbackend.Interface { 70 | switch strings.ToLower(config.Backend) { 71 | case sdl2BackendName: 72 | result, err := sdl2inputbackend.Create() 73 | if err != nil { 74 | log.Panic(err) 75 | } 76 | 77 | return result 78 | default: 79 | panic("unknown backend") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/scenemanager" 10 | 11 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/backends/inputbackend" 12 | 13 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/backends/graphicsbackend" 14 | 15 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/configuration" 16 | "github.com/gravestench/akara" 17 | ) 18 | 19 | // Engine represents an instance of the Abyss Engine. 20 | type Engine struct { 21 | ecs *akara.World 22 | gfx graphicsbackend.Interface 23 | input inputbackend.Interface 24 | sceneManager *scenemanager.SceneManager 25 | } 26 | 27 | // New creates a new instance of the abyss engine 28 | func New(config *configuration.Configuration, 29 | graphicsBackend graphicsbackend.Interface, 30 | inputBackend inputbackend.Interface, 31 | sceneManager *scenemanager.SceneManager, 32 | ) (*Engine, error) { 33 | result := &Engine{ 34 | gfx: graphicsBackend, 35 | input: inputBackend, 36 | sceneManager: sceneManager, 37 | } 38 | 39 | result.configureECS() 40 | 41 | return result, nil 42 | } 43 | 44 | // Run runs the engine 45 | func (engine *Engine) Run() error { 46 | lastUpdateTime := time.Now() 47 | go func() { 48 | engine.handleDebugger() 49 | }() 50 | 51 | for { 52 | currentTime := time.Now() 53 | timeDelta := currentTime.Sub(lastUpdateTime) 54 | lastUpdateTime = currentTime 55 | 56 | if err := engine.ecs.Update(timeDelta); err != nil { 57 | return err 58 | } 59 | 60 | if err := engine.gfx.Render(); err != nil { 61 | return err 62 | } 63 | 64 | if err := engine.input.Process(); err != nil { 65 | return err 66 | } 67 | } 68 | } 69 | 70 | func (engine *Engine) configureECS() { 71 | cfg := akara.NewWorldConfig(). 72 | With(&configuration.Configuration{}) 73 | 74 | engine.ecs = akara.NewWorld(cfg) 75 | } 76 | 77 | func (engine *Engine) handleDebugger() { 78 | reader := bufio.NewReader(os.Stdin) 79 | for { 80 | res, err := reader.ReadString('\n') 81 | if err != nil { 82 | log.Printf("Couldn't read stdin") 83 | } 84 | 85 | log.Printf("Recieved: %q", res) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/fileformats/dccfile/dcc.go: -------------------------------------------------------------------------------- 1 | package d2dcc 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" 7 | ) 8 | 9 | const dccFileSignature = 0x74 10 | const directionOffsetMultiplier = 8 11 | 12 | // DCC represents a DCC file. 13 | type DCC struct { 14 | Signature int 15 | Version int 16 | NumberOfDirections int 17 | FramesPerDirection int 18 | Directions []*DCCDirection 19 | directionOffsets []int 20 | fileData []byte 21 | } 22 | 23 | // Load loads a DCC file. 24 | func Load(fileData []byte) (*DCC, error) { 25 | result := &DCC{ 26 | fileData: fileData, 27 | } 28 | 29 | var bm = d2datautils.CreateBitMuncher(fileData, 0) 30 | 31 | result.Signature = int(bm.GetByte()) 32 | 33 | if result.Signature != dccFileSignature { 34 | return nil, errors.New("signature expected to be 0x74 but it is not") 35 | } 36 | 37 | result.Version = int(bm.GetByte()) 38 | result.NumberOfDirections = int(bm.GetByte()) 39 | result.FramesPerDirection = int(bm.GetInt32()) 40 | 41 | result.Directions = make([]*DCCDirection, result.NumberOfDirections) 42 | 43 | if bm.GetInt32() != 1 { 44 | return nil, errors.New("this value isn't 1. It has to be 1") 45 | } 46 | 47 | bm.GetInt32() // TotalSizeCoded 48 | 49 | result.directionOffsets = make([]int, result.NumberOfDirections) 50 | 51 | for i := 0; i < result.NumberOfDirections; i++ { 52 | result.directionOffsets[i] = int(bm.GetInt32()) 53 | result.Directions[i] = result.decodeDirection(i) 54 | } 55 | 56 | return result, nil 57 | } 58 | 59 | // decodeDirection decodes and returns the given direction 60 | func (d *DCC) decodeDirection(direction int) *DCCDirection { 61 | return CreateDCCDirection(d2datautils.CreateBitMuncher(d.fileData, 62 | d.directionOffsets[direction]*directionOffsetMultiplier), d) 63 | } 64 | 65 | // Clone creates a copy of the DCC 66 | func (d *DCC) Clone() *DCC { 67 | clone := *d 68 | copy(clone.directionOffsets, d.directionOffsets) 69 | copy(clone.fileData, d.fileData) 70 | clone.Directions = make([]*DCCDirection, len(d.Directions)) 71 | 72 | for i := range d.Directions { 73 | cloneDirection := *d.Directions[i] 74 | clone.Directions = append(clone.Directions, &cloneDirection) 75 | } 76 | 77 | return &clone 78 | } 79 | -------------------------------------------------------------------------------- /internal/engine/configuration/defaults.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "path" 7 | "runtime" 8 | ) 9 | 10 | // DefaultConfig creates and returns a default configuration 11 | func DefaultConfig() *Configuration { 12 | const ( 13 | defaultSfxVolume = 1.0 14 | defaultBgmVolume = 0.3 15 | ) 16 | 17 | config := &Configuration{ 18 | FullScreen: false, 19 | TicksPerSecond: -1, 20 | RunInBackground: true, 21 | VsyncEnabled: true, 22 | SfxVolume: defaultSfxVolume, 23 | BgmVolume: defaultBgmVolume, 24 | MpqPath: "C:/Program Files (x86)/Diablo II", 25 | Backend: "SDL2", 26 | MpqLoadOrder: []string{ 27 | "Patch_D2.mpq", 28 | "d2exp.mpq", 29 | "d2xmusic.mpq", 30 | "d2xtalk.mpq", 31 | "d2xvideo.mpq", 32 | "d2data.mpq", 33 | "d2char.mpq", 34 | "d2music.mpq", 35 | "d2sfx.mpq", 36 | "d2video.mpq", 37 | "d2speech.mpq", 38 | }, 39 | filePath: DefaultPath(), 40 | } 41 | 42 | switch runtime.GOOS { 43 | case "windows": 44 | if runtime.GOARCH == "386" { 45 | config.MpqPath = "C:/Program Files/Diablo II" 46 | } 47 | case "darwin": 48 | config.MpqPath = "/Applications/Diablo II/" 49 | config.MpqLoadOrder = []string{ 50 | "Diablo II Patch", 51 | "Diablo II Expansion Data", 52 | "Diablo II Expansion Movies", 53 | "Diablo II Expansion Music", 54 | "Diablo II Expansion Speech", 55 | "Diablo II Game Data", 56 | "Diablo II Graphics", 57 | "Diablo II Movies", 58 | "Diablo II Music", 59 | "Diablo II Sounds", 60 | "Diablo II Speech", 61 | } 62 | case "linux": 63 | if usr, err := user.Current(); err == nil { 64 | config.MpqPath = path.Join(usr.HomeDir, ".wine/drive_c/Program Files (x86)/Diablo II") 65 | } 66 | } 67 | 68 | return config 69 | } 70 | 71 | // DefaultPath returns the absolute path for the default config file location 72 | func DefaultPath() string { 73 | if configDir, err := os.UserConfigDir(); err == nil { 74 | return path.Join(configDir, od2ConfigDirName, od2ConfigFileName) 75 | } 76 | 77 | return LocalConfigPath() 78 | } 79 | 80 | // LocalPath returns the absolute path to the directory of the OpenDiablo2 executable 81 | func LocalConfigPath() string { 82 | return path.Join(path.Dir(os.Args[0]), od2ConfigFileName) 83 | } 84 | -------------------------------------------------------------------------------- /internal/engine/resource/musicdefs.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "github.com/OpenDiablo2/AbyssEngine/internal/engine/common/enum" 4 | 5 | // MusicDef stores the music definitions of a region 6 | type MusicDef struct { 7 | Region enum.RegionIdType 8 | InTown bool 9 | MusicFile string 10 | } 11 | 12 | func getMusicDefs() []MusicDef { 13 | return []MusicDef{ 14 | {enum.RegionAct1Town, false, BGMAct1Town1}, 15 | {enum.RegionAct1Wilderness, false, BGMAct1Wild}, 16 | {enum.RegionAct1Cave, false, BGMAct1Caves}, 17 | {enum.RegionAct1Crypt, false, BGMAct1Crypt}, 18 | {enum.RegionAct1Monestary, false, BGMAct1Monastery}, 19 | {enum.RegionAct1Courtyard, false, BGMAct1Monastery}, 20 | {enum.RegionAct1Barracks, false, BGMAct1Monastery}, 21 | {enum.RegionAct1Jail, false, BGMAct1Monastery}, 22 | {enum.RegionAct1Cathedral, false, BGMAct1Monastery}, 23 | {enum.RegionAct1Catacombs, false, BGMAct1Monastery}, 24 | {enum.RegionAct1Tristram, false, BGMAct1Tristram}, 25 | {enum.RegionAct2Town, false, BGMAct2Town2}, 26 | {enum.RegionAct2Sewer, false, BGMAct2Sewer}, 27 | {enum.RegionAct2Harem, false, BGMAct2Harem}, 28 | {enum.RegionAct2Basement, false, BGMAct2Harem}, 29 | {enum.RegionAct2Desert, false, BGMAct2Desert}, 30 | {enum.RegionAct2Tomb, false, BGMAct2Tombs}, 31 | {enum.RegionAct2Lair, false, BGMAct2Lair}, 32 | {enum.RegionAct2Arcane, false, BGMAct2Sanctuary}, 33 | {enum.RegionAct3Town, false, BGMAct3Town3}, 34 | {enum.RegionAct3Jungle, false, BGMAct3Jungle}, 35 | {enum.RegionAct3Kurast, false, BGMAct3Kurast}, 36 | {enum.RegionAct3Spider, false, BGMAct3Spider}, 37 | {enum.RegionAct3Dungeon, false, BGMAct3KurastSewer}, 38 | {enum.RegionAct3Sewer, false, BGMAct3KurastSewer}, 39 | {enum.RegionAct4Town, false, BGMAct4Town4}, 40 | {enum.RegionAct4Mesa, false, BGMAct4Mesa}, 41 | {enum.RegionAct4Lava, false, BGMAct4Mesa}, 42 | {enum.RegonAct5Town, false, BGMAct5XTown}, 43 | {enum.RegionAct5Siege, false, BGMAct5Siege}, 44 | {enum.RegionAct5Barricade, false, BGMAct5Siege}, // ? 45 | {enum.RegionAct5Temple, false, BGMAct5XTemple}, 46 | {enum.RegionAct5IceCaves, false, BGMAct5IceCaves}, 47 | {enum.RegionAct5Baal, false, BGMAct5Baal}, 48 | {enum.RegionAct5Lava, false, BGMAct5Nihlathak}, // ? 49 | } 50 | } 51 | 52 | // GetMusicDef returns the MusicDef of the given region 53 | func GetMusicDef(regionType enum.RegionIdType) *MusicDef { 54 | musicDefs := getMusicDefs() 55 | for idx := range musicDefs { 56 | if musicDefs[idx].Region != regionType { 57 | continue 58 | } 59 | 60 | return &musicDefs[idx] 61 | } 62 | 63 | return &musicDefs[0] 64 | } 65 | -------------------------------------------------------------------------------- /internal/engine/resource/languagesmap.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | func getLanguages() map[byte]string { 4 | return map[byte]string{ 5 | 0x00: "ENG", // (English) 6 | 0x01: "ESP", // (Spanish) 7 | 0x02: "DEU", // (German) 8 | 0x03: "FRA", // (French) 9 | 0x04: "POR", // (Portuguese) 10 | 0x05: "ITA", // (Italian) 11 | 0x06: "JPN", // (Japanese) 12 | 0x07: "KOR", // (Korean) 13 | 0x08: "SIN", // 14 | 0x09: "CHI", // (Chinese) 15 | 0x0A: "POL", // (Polish) 16 | 0x0B: "RUS", // (Russian) 17 | 0x0C: "ENG", // (English) 18 | } 19 | } 20 | 21 | // GetLanguageLiteral returns string representation of language code 22 | func GetLanguageLiteral(code byte) string { 23 | languages := getLanguages() 24 | 25 | return languages[code] 26 | } 27 | 28 | // Source https://github.com/eezstreet/OpenD2/blob/065f6e466048482b28b9dbc6286908dc1e0d10f6/Shared/D2Shared.hpp#L36 29 | func getCharsets() map[string]string { 30 | return map[string]string{ 31 | "ENG": "LATIN", // (English) 32 | "ESP": "LATIN", // (Spanish) 33 | "DEU": "LATIN", // (German) 34 | "FRA": "LATIN", // (French) 35 | "POR": "LATIN", // (Portuguese) 36 | "ITA": "LATIN", // (Italian) 37 | "JPN": "JPN", // (Japanese) 38 | "KOR": "KOR", // (Korean) 39 | "SIN": "LATIN", // 40 | "CHI": "CHI", // (Chinese) 41 | "POL": "LATIN2", // (Polish) 42 | "RUS": "CYR", // (Russian) 43 | } 44 | } 45 | 46 | // GetFontCharset returns string representation of font charset 47 | func GetFontCharset(language string) string { 48 | charset := getCharsets() 49 | 50 | return charset[language] 51 | } 52 | 53 | // GetLabelModifier returns modifier for language 54 | /* modifiers for labels (used in string tables) 55 | modifier is something like that: 56 | english table: polish table: 57 | key | value key | value 58 | #1 | v1 | 59 | #4 | v2 #4 | v1 60 | #5 | v3 #5 | v2 61 | #8 | v4 #8 | v3 62 | So, GetLabelModifier returns value of offset in locale languages table 63 | */ 64 | // some of values need to be set up. For now values with "checked" comment 65 | // was tested and works fine. 66 | func GetLabelModifier(language string) int { 67 | modifiers := map[string]int{ 68 | "ENG": 0, // (English) // checked 69 | "ESP": 0, // (Spanish) 70 | "DEU": 0, // (German) // checked 71 | "FRA": 0, // (French) 72 | "POR": 0, // (Portuguese) 73 | "ITA": 0, // (Italian) // checked 74 | "JPN": 0, // (Japanese) 75 | "KOR": 0, // (Korean) 76 | "SIN": 0, // 77 | "CHI": 0, // (Chinese) 78 | "POL": 1, // (Polish) // checked 79 | "RUS": 0, // (Russian) 80 | } 81 | 82 | return modifiers[language] 83 | } 84 | -------------------------------------------------------------------------------- /pkg/fileformats/mpqfile/mpq_block.go: -------------------------------------------------------------------------------- 1 | package mpqfile 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | ) 7 | 8 | // FileFlag represents flags for a file record in the MPQ archive 9 | type FileFlag uint32 10 | 11 | const ( 12 | // FileImplode - File is compressed using PKWARE Data compression library 13 | FileImplode FileFlag = 0x00000100 14 | // FileCompress - File is compressed using combination of compression methods 15 | FileCompress FileFlag = 0x00000200 16 | // FileEncrypted - The file is encrypted 17 | FileEncrypted FileFlag = 0x00010000 18 | // FileFixKey - The decryption key for the file is altered according to the position of the file in the archive 19 | FileFixKey FileFlag = 0x00020000 20 | // FilePatchFile - The file contains incremental patch for an existing file in base MPQ 21 | FilePatchFile FileFlag = 0x00100000 22 | // FileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit 23 | FileSingleUnit FileFlag = 0x01000000 24 | // FileDeleteMarker - File is a deletion marker, indicating that the file no longer exists. This is used to allow patch 25 | // archives to delete files present in lower-priority archives in the search chain. The file usually 26 | // has length of 0 or 1 byte and its name is a hash 27 | FileDeleteMarker FileFlag = 0x02000000 28 | // FileSectorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded. 29 | FileSectorCrc FileFlag = 0x04000000 30 | // FileExists - Set if file exists, reset when the file was deleted 31 | FileExists FileFlag = 0x80000000 32 | ) 33 | 34 | // Block represents an entry in the block table 35 | type Block struct { // 16 bytes 36 | FilePosition uint32 37 | CompressedFileSize uint32 38 | UncompressedFileSize uint32 39 | Flags FileFlag 40 | // Local Stuff... 41 | FileName string 42 | EncryptionSeed uint32 43 | } 44 | 45 | // HasFlag returns true if the specified flag is present 46 | func (b *Block) HasFlag(flag FileFlag) bool { 47 | return (b.Flags & flag) != 0 48 | } 49 | 50 | func (b *Block) calculateEncryptionSeed(fileName string) { 51 | fileName = fileName[strings.LastIndex(fileName, `\`)+1:] 52 | seed := hashString(fileName, 3) 53 | b.EncryptionSeed = (seed + b.FilePosition) ^ b.UncompressedFileSize 54 | } 55 | 56 | //nolint:gomnd // number 57 | func (mpq *MPQ) readBlockTable() error { 58 | if _, err := mpq.file.Seek(int64(mpq.header.BlockTableOffset), io.SeekStart); err != nil { 59 | return err 60 | } 61 | 62 | blockData, err := decryptTable(mpq.file, mpq.header.BlockTableEntries, "(block table)") 63 | if err != nil { 64 | return err 65 | } 66 | 67 | for n, i := uint32(0), uint32(0); i < mpq.header.BlockTableEntries; n, i = n+4, i+1 { 68 | mpq.blocks = append(mpq.blocks, &Block{ 69 | FilePosition: blockData[n], 70 | CompressedFileSize: blockData[n+1], 71 | UncompressedFileSize: blockData[n+2], 72 | Flags: FileFlag(blockData[n+3]), 73 | }) 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/engine/backends/graphicsbackend/sdl2graphicsbackend/graphicsbackend.go: -------------------------------------------------------------------------------- 1 | package sdl2graphicsbackend 2 | 3 | import ( 4 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/backends/graphicsbackend" 5 | "github.com/veandco/go-sdl2/sdl" 6 | "golang.org/x/tools/go/ssa/interp/testdata/src/errors" 7 | ) 8 | 9 | const ( 10 | screenWidth = 800 11 | screenHeight = 600 12 | ) 13 | 14 | var _ graphicsbackend.Interface = &SDL2GraphicsBackend{} 15 | var _ graphicsbackend.Surface = &SDL2GraphicsBackend{} 16 | 17 | type SDL2GraphicsBackend struct { 18 | window *sdl.Window 19 | renderer *sdl.Renderer 20 | } 21 | 22 | func (r *SDL2GraphicsBackend) Render() error { 23 | if err := r.renderer.Clear(); err != nil { 24 | return err 25 | } 26 | 27 | r.renderer.Present() 28 | 29 | return nil 30 | } 31 | 32 | func Create() (*SDL2GraphicsBackend, error) { 33 | if err := sdl.Init(sdl.INIT_VIDEO | sdl.INIT_EVENTS); err != nil { 34 | return nil, err 35 | } 36 | 37 | window, err := sdl.CreateWindow("OpenDiablo 2", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, 38 | 640, 480, sdl.WINDOW_SHOWN|sdl.WINDOW_RESIZABLE|sdl.WINDOW_INPUT_FOCUS) 39 | 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | r, err := sdl.CreateRenderer(window, -1, 45 | sdl.RENDERER_ACCELERATED|sdl.RENDERER_TARGETTEXTURE|sdl.RENDERER_PRESENTVSYNC) 46 | 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if err := r.SetDrawBlendMode(sdl.BLENDMODE_BLEND); err != nil { 52 | return nil, err 53 | } 54 | 55 | if err := r.SetIntegerScale(false); err != nil { 56 | return nil, err 57 | } 58 | 59 | if err := r.SetLogicalSize(800, 600); err != nil { 60 | return nil, err 61 | } 62 | window.SetMinimumSize(800, 600) 63 | 64 | result := &SDL2GraphicsBackend{ 65 | window: window, 66 | renderer: r, 67 | } 68 | 69 | return result, nil 70 | } 71 | 72 | func (r *SDL2GraphicsBackend) GetRendererName() string { 73 | return "SDL2" 74 | } 75 | 76 | func (r *SDL2GraphicsBackend) SetWindowIcon(fileName string) { 77 | panic("implement me") 78 | } 79 | 80 | func (r *SDL2GraphicsBackend) IsFullScreen() bool { 81 | panic("implement me") 82 | } 83 | 84 | func (r *SDL2GraphicsBackend) SetFullScreen(fullScreen bool) { 85 | panic("implement me") 86 | } 87 | 88 | func (r *SDL2GraphicsBackend) SetVSyncEnabled(vsync bool) { 89 | panic("implement me") 90 | } 91 | 92 | func (r *SDL2GraphicsBackend) GetVSyncEnabled() bool { 93 | panic("implement me") 94 | } 95 | 96 | func (r *SDL2GraphicsBackend) GetCursorPos() (int, int) { 97 | panic("implement me") 98 | } 99 | 100 | func (r *SDL2GraphicsBackend) CurrentFPS() float64 { 101 | panic("implement me") 102 | } 103 | 104 | func (r *SDL2GraphicsBackend) NewSurface(width, height int, pixelData *[]byte) (graphicsbackend.Surface, error) { 105 | var texture *sdl.Texture 106 | var err error 107 | 108 | if texture, err = r.renderer.CreateTexture(sdl.PIXELFORMAT_ARGB8888, sdl.TEXTUREACCESS_TARGET, 109 | int32(width), int32(height)); err != nil { 110 | return nil, err 111 | } 112 | 113 | if err = texture.Update(nil, *pixelData, width*4); err != nil { 114 | return nil, err 115 | } 116 | 117 | result := CreateSDL2Surface(r.renderer, texture, int32(width), int32(height)) 118 | 119 | return result, nil 120 | } 121 | 122 | func (r *SDL2GraphicsBackend) RenderTo(graphicsbackend.Surface) error { 123 | return errors.New("cannot render the output surface to another surface") 124 | } 125 | 126 | -------------------------------------------------------------------------------- /internal/engine/backends/inputbackend/sdl2inputbackend/inputbackend.go: -------------------------------------------------------------------------------- 1 | package sdl2inputbackend 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/OpenDiablo2/AbyssEngine/internal/engine/backends/inputbackend" 7 | "github.com/veandco/go-sdl2/sdl" 8 | ) 9 | 10 | const ( 11 | mousePressThreshold = 25 12 | ) 13 | 14 | type mouseEventInfo struct { 15 | state bool 16 | time uint32 17 | } 18 | 19 | var _ inputbackend.Interface = &SDL2InputBackend{} 20 | 21 | type SDL2InputBackend struct { 22 | mouseState [8]mouseEventInfo 23 | cursorPosX int 24 | cursorPosY int 25 | } 26 | 27 | func Create() (*SDL2InputBackend, error) { 28 | result := &SDL2InputBackend{} 29 | 30 | return result, nil 31 | } 32 | 33 | func (i SDL2InputBackend) Process() error { 34 | var event sdl.Event 35 | for event = sdl.PollEvent(); event != nil; event = sdl.PollEvent() { 36 | switch t := event.(type) { 37 | case *sdl.QuitEvent: 38 | os.Exit(0) 39 | case *sdl.MouseMotionEvent: 40 | i.cursorPosX = int(t.X) 41 | i.cursorPosY = int(t.Y) 42 | case *sdl.MouseButtonEvent: 43 | i.mouseState[t.Button].time = sdl.GetTicks() 44 | i.mouseState[t.Button].state = t.Type == sdl.MOUSEBUTTONDOWN 45 | } 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func (i *SDL2InputBackend) CursorPosition() (x int, y int) { 52 | return i.cursorPosX, i.cursorPosY 53 | } 54 | 55 | func (i *SDL2InputBackend) InputChars() []rune { 56 | return []rune{} 57 | } 58 | 59 | func (i *SDL2InputBackend) IsKeyPressed(key inputbackend.Key) bool { 60 | return false 61 | } 62 | 63 | func (i *SDL2InputBackend) IsKeyJustPressed(key inputbackend.Key) bool { 64 | return false 65 | } 66 | 67 | func (i *SDL2InputBackend) IsKeyJustReleased(key inputbackend.Key) bool { 68 | return false 69 | } 70 | 71 | func (i *SDL2InputBackend) IsMouseButtonPressed(button inputbackend.MouseButton) bool { 72 | switch button { 73 | case inputbackend.MouseButtonLeft: 74 | return i.mouseState[1].state 75 | case inputbackend.MouseButtonRight: 76 | return i.mouseState[2].state 77 | case inputbackend.MouseButtonMiddle: 78 | return i.mouseState[3].state 79 | default: 80 | return false 81 | } 82 | } 83 | 84 | func (i *SDL2InputBackend) IsMouseButtonJustPressed(button inputbackend.MouseButton) bool { 85 | switch button { 86 | case inputbackend.MouseButtonLeft: 87 | return i.mouseState[1].state == true && (sdl.GetTicks()-i.mouseState[1].time) < mousePressThreshold 88 | case inputbackend.MouseButtonRight: 89 | return i.mouseState[2].state == true && (sdl.GetTicks()-i.mouseState[2].time) < mousePressThreshold 90 | case inputbackend.MouseButtonMiddle: 91 | return i.mouseState[3].state == true && (sdl.GetTicks()-i.mouseState[3].time) < mousePressThreshold 92 | default: 93 | return false 94 | } 95 | } 96 | 97 | func (i *SDL2InputBackend) IsMouseButtonJustReleased(button inputbackend.MouseButton) bool { 98 | switch button { 99 | case inputbackend.MouseButtonLeft: 100 | return i.mouseState[1].state == false && (sdl.GetTicks()-i.mouseState[1].time) < mousePressThreshold 101 | case inputbackend.MouseButtonRight: 102 | return i.mouseState[2].state == false && (sdl.GetTicks()-i.mouseState[2].time) < mousePressThreshold 103 | case inputbackend.MouseButtonMiddle: 104 | return i.mouseState[3].state == false && (sdl.GetTicks()-i.mouseState[3].time) < mousePressThreshold 105 | default: 106 | return false 107 | } 108 | } 109 | 110 | func (i *SDL2InputBackend) KeyPressDuration(key inputbackend.Key) int { 111 | return 0 112 | } 113 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at tim.sarbin@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /pkg/fileformats/animdata/animdata_test.go: -------------------------------------------------------------------------------- 1 | package d2animdata 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestLoad(t *testing.T) { 10 | testFile, fileErr := os.Open("testdata/AnimData.d2") 11 | if fileErr != nil { 12 | t.Error("cannot open test data file") 13 | return 14 | } 15 | 16 | data := make([]byte, 0) 17 | buf := make([]byte, 16) 18 | 19 | for { 20 | numRead, err := testFile.Read(buf) 21 | 22 | data = append(data, buf[:numRead]...) 23 | 24 | if err != nil { 25 | break 26 | } 27 | } 28 | 29 | _, loadErr := Load(data) 30 | if loadErr != nil { 31 | t.Error(loadErr) 32 | } 33 | 34 | err := testFile.Close() 35 | if err != nil { 36 | t.Fail() 37 | log.Print(err) 38 | } 39 | } 40 | 41 | func TestLoad_BadData(t *testing.T) { 42 | testFile, fileErr := os.Open("testdata/BadData.d2") 43 | if fileErr != nil { 44 | t.Error("cannot open test data file") 45 | return 46 | } 47 | 48 | data := make([]byte, 0) 49 | buf := make([]byte, 16) 50 | 51 | for { 52 | numRead, err := testFile.Read(buf) 53 | 54 | data = append(data, buf[:numRead]...) 55 | 56 | if err != nil { 57 | break 58 | } 59 | } 60 | 61 | _, loadErr := Load(data) 62 | if loadErr == nil { 63 | t.Error("bad data file should not be parsed") 64 | } 65 | 66 | err := testFile.Close() 67 | if err != nil { 68 | t.Fail() 69 | log.Print(err) 70 | } 71 | } 72 | 73 | func TestAnimationData_GetRecordNames(t *testing.T) { 74 | animdata := &AnimationData{ 75 | hashTable: hashTable{}, 76 | blocks: [256]*block{}, 77 | entries: map[string][]*AnimationDataRecord{ 78 | "a": {{}}, 79 | "b": {{}}, 80 | "c": {{}}, 81 | }, 82 | } 83 | 84 | names := animdata.GetRecordNames() 85 | if len(names) != 3 { 86 | t.Error("record name count mismatch") 87 | } 88 | } 89 | 90 | func TestAnimationData_GetRecords(t *testing.T) { 91 | animdata := &AnimationData{ 92 | hashTable: hashTable{}, 93 | blocks: [256]*block{}, 94 | entries: map[string][]*AnimationDataRecord{ 95 | "a": { 96 | {name: "a", speed: 1, framesPerDirection: 1}, 97 | {name: "a", speed: 2, framesPerDirection: 2}, 98 | {name: "a", speed: 3, framesPerDirection: 3}, 99 | }, 100 | }, 101 | } 102 | 103 | if len(animdata.GetRecords("a")) != 3 { 104 | t.Error("record count is incorrect") 105 | } 106 | 107 | if len(animdata.GetRecords("b")) > 0 { 108 | t.Error("retrieved records for unknown record name") 109 | } 110 | } 111 | 112 | func TestAnimationData_GetRecord(t *testing.T) { 113 | animdata := &AnimationData{ 114 | hashTable: hashTable{}, 115 | blocks: [256]*block{}, 116 | entries: map[string][]*AnimationDataRecord{ 117 | "a": { 118 | {name: "a", speed: 1, framesPerDirection: 1}, 119 | {name: "a", speed: 2, framesPerDirection: 2}, 120 | {name: "a", speed: 3, framesPerDirection: 3}, 121 | }, 122 | }, 123 | } 124 | 125 | record := animdata.GetRecord("a") 126 | if record.speed != 3 { 127 | t.Error("record returned is incorrect") 128 | } 129 | } 130 | 131 | func TestAnimationDataRecord_FPS(t *testing.T) { 132 | record := &AnimationDataRecord{} 133 | 134 | var fps float64 135 | 136 | record.speed = 256 137 | fps = record.FPS() 138 | 139 | if fps != float64(speedBaseFPS) { 140 | t.Error("incorrect fps") 141 | } 142 | 143 | record.speed = 512 144 | fps = record.FPS() 145 | 146 | if fps != float64(speedBaseFPS)*2 { 147 | t.Error("incorrect fps") 148 | } 149 | 150 | record.speed = 128 151 | fps = record.FPS() 152 | 153 | if fps != float64(speedBaseFPS)/2 { 154 | t.Error("incorrect fps") 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /pkg/fileformats/coffile/cof.go: -------------------------------------------------------------------------------- 1 | package d2cof 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" 7 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" 8 | ) 9 | 10 | const ( 11 | unknownByteCount = 21 12 | numHeaderBytes = 4 + unknownByteCount 13 | numLayerBytes = 9 14 | ) 15 | 16 | const ( 17 | headerNumLayers = iota 18 | headerFramesPerDir 19 | headerNumDirs 20 | headerSpeed = numHeaderBytes - 1 21 | ) 22 | 23 | const ( 24 | layerType = iota 25 | layerShadow 26 | layerSelectable 27 | layerTransparent 28 | layerDrawEffect 29 | layerWeaponClass 30 | ) 31 | 32 | const ( 33 | badCharacter = string(byte(0)) 34 | ) 35 | 36 | // COF is a structure that represents a COF file. 37 | type COF struct { 38 | NumberOfDirections int 39 | FramesPerDirection int 40 | NumberOfLayers int 41 | Speed int 42 | CofLayers []CofLayer 43 | CompositeLayers map[d2enum.CompositeType]int 44 | AnimationFrames []d2enum.AnimationFrame 45 | Priority [][][]d2enum.CompositeType 46 | } 47 | 48 | // Load loads a COF file. 49 | func Load(fileData []byte) (*COF, error) { 50 | result := &COF{} 51 | streamReader := d2datautils.CreateStreamReader(fileData) 52 | 53 | var b []byte 54 | 55 | var err error 56 | 57 | b, err = streamReader.ReadBytes(numHeaderBytes) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | result.NumberOfLayers = int(b[headerNumLayers]) 63 | result.FramesPerDirection = int(b[headerFramesPerDir]) 64 | result.NumberOfDirections = int(b[headerNumDirs]) 65 | result.Speed = int(b[headerSpeed]) 66 | 67 | streamReader.SkipBytes(3) //nolint:gomnd // Unknown data 68 | 69 | result.CofLayers = make([]CofLayer, result.NumberOfLayers) 70 | result.CompositeLayers = make(map[d2enum.CompositeType]int) 71 | 72 | for i := 0; i < result.NumberOfLayers; i++ { 73 | layer := CofLayer{} 74 | 75 | b, err = streamReader.ReadBytes(numLayerBytes) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | layer.Type = d2enum.CompositeType(b[layerType]) 81 | layer.Shadow = b[layerShadow] 82 | layer.Selectable = b[layerSelectable] > 0 83 | layer.Transparent = b[layerTransparent] > 0 84 | layer.DrawEffect = d2enum.DrawEffect(b[layerDrawEffect]) 85 | 86 | layer.WeaponClass = d2enum.WeaponClassFromString(strings.TrimSpace(strings.ReplaceAll( 87 | string(b[layerWeaponClass:]), badCharacter, ""))) 88 | 89 | result.CofLayers[i] = layer 90 | result.CompositeLayers[layer.Type] = i 91 | } 92 | 93 | b, err = streamReader.ReadBytes(result.FramesPerDirection) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | result.AnimationFrames = make([]d2enum.AnimationFrame, result.FramesPerDirection) 99 | 100 | for i := range b { 101 | result.AnimationFrames[i] = d2enum.AnimationFrame(b[i]) 102 | } 103 | 104 | priorityLen := result.FramesPerDirection * result.NumberOfDirections * result.NumberOfLayers 105 | result.Priority = make([][][]d2enum.CompositeType, result.NumberOfDirections) 106 | 107 | priorityBytes, err := streamReader.ReadBytes(priorityLen) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | priorityIndex := 0 113 | 114 | for direction := 0; direction < result.NumberOfDirections; direction++ { 115 | result.Priority[direction] = make([][]d2enum.CompositeType, result.FramesPerDirection) 116 | for frame := 0; frame < result.FramesPerDirection; frame++ { 117 | result.Priority[direction][frame] = make([]d2enum.CompositeType, result.NumberOfLayers) 118 | for i := 0; i < result.NumberOfLayers; i++ { 119 | result.Priority[direction][frame][i] = d2enum.CompositeType(priorityBytes[priorityIndex]) 120 | priorityIndex++ 121 | } 122 | } 123 | } 124 | 125 | return result, nil 126 | } 127 | -------------------------------------------------------------------------------- /pkg/fileformats/mpqfile/crypto.go: -------------------------------------------------------------------------------- 1 | package mpqfile 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | cryptoBuffer [0x500]uint32 //nolint:gochecknoglobals // will fix later.. 11 | cryptoBufferReady bool //nolint:gochecknoglobals // will fix later.. 12 | ) 13 | 14 | func cryptoLookup(index uint32) uint32 { 15 | if !cryptoBufferReady { 16 | cryptoInitialize() 17 | 18 | cryptoBufferReady = true 19 | } 20 | 21 | return cryptoBuffer[index] 22 | } 23 | 24 | //nolint:gomnd // Decryption magic 25 | func cryptoInitialize() { 26 | seed := uint32(0x00100001) 27 | 28 | for index1 := 0; index1 < 0x100; index1++ { 29 | index2 := index1 30 | 31 | for i := 0; i < 5; i++ { 32 | seed = (seed*125 + 3) % 0x2AAAAB 33 | temp1 := (seed & 0xFFFF) << 0x10 34 | seed = (seed*125 + 3) % 0x2AAAAB 35 | temp2 := seed & 0xFFFF 36 | cryptoBuffer[index2] = temp1 | temp2 37 | index2 += 0x100 38 | } 39 | } 40 | } 41 | 42 | //nolint:gomnd // Decryption magic 43 | func decrypt(data []uint32, seed uint32) { 44 | seed2 := uint32(0xeeeeeeee) 45 | 46 | for i := 0; i < len(data); i++ { 47 | seed2 += cryptoLookup(0x400 + (seed & 0xff)) 48 | result := data[i] 49 | result ^= seed + seed2 50 | 51 | seed = ((^seed << 21) + 0x11111111) | (seed >> 11) 52 | seed2 = result + seed2 + (seed2 << 5) + 3 53 | data[i] = result 54 | } 55 | } 56 | 57 | //nolint:gomnd // Decryption magic 58 | func decryptBytes(data []byte, seed uint32) { 59 | seed2 := uint32(0xEEEEEEEE) 60 | for i := 0; i < len(data)-3; i += 4 { 61 | seed2 += cryptoLookup(0x400 + (seed & 0xFF)) 62 | result := binary.LittleEndian.Uint32(data[i : i+4]) 63 | result ^= seed + seed2 64 | seed = ((^seed << 21) + 0x11111111) | (seed >> 11) 65 | seed2 = result + seed2 + (seed2 << 5) + 3 66 | 67 | data[i+0] = uint8(result & 0xff) 68 | data[i+1] = uint8((result >> 8) & 0xff) 69 | data[i+2] = uint8((result >> 16) & 0xff) 70 | data[i+3] = uint8((result >> 24) & 0xff) 71 | } 72 | } 73 | 74 | //nolint:gomnd // Decryption magic 75 | func decryptTable(r io.Reader, size uint32, name string) ([]uint32, error) { 76 | seed := hashString(name, 3) 77 | seed2 := uint32(0xEEEEEEEE) 78 | size *= 4 79 | 80 | table := make([]uint32, size) 81 | buf := make([]byte, 4) 82 | 83 | for i := uint32(0); i < size; i++ { 84 | seed2 += cryptoBuffer[0x400+(seed&0xff)] 85 | 86 | if _, err := r.Read(buf); err != nil { 87 | return table, err 88 | } 89 | 90 | result := binary.LittleEndian.Uint32(buf) 91 | result ^= seed + seed2 92 | 93 | seed = ((^seed << 21) + 0x11111111) | (seed >> 11) 94 | seed2 = result + seed2 + (seed2 << 5) + 3 95 | table[i] = result 96 | } 97 | 98 | return table, nil 99 | } 100 | 101 | func hashFilename(key string) uint64 { 102 | a, b := hashString(key, 1), hashString(key, 2) 103 | return uint64(a)<<32 | uint64(b) 104 | } 105 | 106 | //nolint:gomnd // Decryption magic 107 | func hashString(key string, hashType uint32) uint32 { 108 | seed1 := uint32(0x7FED7FED) 109 | seed2 := uint32(0xEEEEEEEE) 110 | 111 | /* prepare seeds. */ 112 | for _, char := range strings.ToUpper(key) { 113 | seed1 = cryptoLookup((hashType*0x100)+uint32(char)) ^ (seed1 + seed2) 114 | seed2 = uint32(char) + seed1 + seed2 + (seed2 << 5) + 3 115 | } 116 | 117 | return seed1 118 | } 119 | 120 | //nolint:unused,deadcode,gomnd // will use this for creating mpq's 121 | func encrypt(data []uint32, seed uint32) { 122 | seed2 := uint32(0xeeeeeeee) 123 | 124 | for i := 0; i < len(data); i++ { 125 | seed2 += cryptoLookup(0x400 + (seed & 0xff)) 126 | result := data[i] 127 | result ^= seed + seed2 128 | 129 | seed = ((^seed << 21) + 0x11111111) | (seed >> 11) 130 | seed2 = data[i] + seed2 + (seed2 << 5) + 3 131 | data[i] = result 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pkg/fileformats/tblfile/text_dictionary.go: -------------------------------------------------------------------------------- 1 | package d2tbl 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | 7 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" 8 | ) 9 | 10 | // TextDictionary is a string map 11 | type TextDictionary map[string]string 12 | 13 | func (td TextDictionary) loadHashEntries(hashEntries []*textDictionaryHashEntry, br *d2datautils.StreamReader) error { 14 | for i := 0; i < len(hashEntries); i++ { 15 | entry := textDictionaryHashEntry{} 16 | 17 | active, err := br.ReadByte() 18 | if err != nil { 19 | return err 20 | } 21 | 22 | entry.IsActive = active > 0 23 | 24 | entry.Index, err = br.ReadUInt16() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | entry.HashValue, err = br.ReadUInt32() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | entry.IndexString, err = br.ReadUInt32() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | entry.NameString, err = br.ReadUInt32() 40 | if err != nil { 41 | return err 42 | } 43 | 44 | entry.NameLength, err = br.ReadUInt16() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | hashEntries[i] = &entry 50 | } 51 | 52 | for idx := range hashEntries { 53 | if !hashEntries[idx].IsActive { 54 | continue 55 | } 56 | 57 | if err := td.loadHashEntry(idx, hashEntries[idx], br); err != nil { 58 | return err 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (td TextDictionary) loadHashEntry(idx int, hashEntry *textDictionaryHashEntry, br *d2datautils.StreamReader) error { 66 | br.SetPosition(uint64(hashEntry.NameString)) 67 | 68 | nameVal, err := br.ReadBytes(int(hashEntry.NameLength - 1)) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | value := string(nameVal) 74 | 75 | br.SetPosition(uint64(hashEntry.IndexString)) 76 | 77 | key := "" 78 | 79 | for { 80 | b, err := br.ReadByte() 81 | if b == 0 { 82 | break 83 | } 84 | 85 | if err != nil { 86 | return err 87 | } 88 | 89 | key += string(b) 90 | } 91 | 92 | if key == "x" || key == "X" { 93 | key = "#" + strconv.Itoa(idx) 94 | } 95 | 96 | _, exists := td[key] 97 | if !exists { 98 | td[key] = value 99 | } 100 | 101 | return nil 102 | } 103 | 104 | type textDictionaryHashEntry struct { 105 | IsActive bool 106 | Index uint16 107 | HashValue uint32 108 | IndexString uint32 109 | NameString uint32 110 | NameLength uint16 111 | } 112 | 113 | const ( 114 | crcByteCount = 2 115 | ) 116 | 117 | // LoadTextDictionary loads the text dictionary from the given data 118 | func LoadTextDictionary(dictionaryData []byte) (TextDictionary, error) { 119 | lookupTable := make(TextDictionary) 120 | 121 | br := d2datautils.CreateStreamReader(dictionaryData) 122 | 123 | // skip past the CRC 124 | _, _ = br.ReadBytes(crcByteCount) 125 | 126 | var err error 127 | 128 | numberOfElements, err := br.ReadUInt16() 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | hashTableSize, err := br.ReadUInt32() 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | // Version (always 0) 139 | if _, err = br.ReadByte(); err != nil { 140 | return nil, errors.New("error reading Version record") 141 | } 142 | 143 | _, _ = br.ReadUInt32() // StringOffset 144 | 145 | // When the number of times you have missed a match with a 146 | // hash key equals this value, you give up because it is not there. 147 | _, _ = br.ReadUInt32() 148 | 149 | _, _ = br.ReadUInt32() // FileSize 150 | 151 | elementIndex := make([]uint16, numberOfElements) 152 | for i := 0; i < int(numberOfElements); i++ { 153 | elementIndex[i], err = br.ReadUInt16() 154 | if err != nil { 155 | return nil, err 156 | } 157 | } 158 | 159 | hashEntries := make([]*textDictionaryHashEntry, hashTableSize) 160 | 161 | err = lookupTable.loadHashEntries(hashEntries, br) 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | return lookupTable, nil 167 | } 168 | -------------------------------------------------------------------------------- /pkg/fileformats/dccfile/dcc_direction_frame.go: -------------------------------------------------------------------------------- 1 | package d2dcc 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" 7 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom" 8 | ) 9 | 10 | // DCCDirectionFrame represents a direction frame for a DCC. 11 | type DCCDirectionFrame struct { 12 | Box d2geom.Rectangle 13 | Cells []DCCCell 14 | PixelData []byte 15 | Width int 16 | Height int 17 | XOffset int 18 | YOffset int 19 | NumberOfOptionalBytes int 20 | NumberOfCodedBytes int 21 | HorizontalCellCount int 22 | VerticalCellCount int 23 | FrameIsBottomUp bool 24 | valid bool 25 | } 26 | 27 | // CreateDCCDirectionFrame Creates a DCCDirectionFrame for a DCC. 28 | func CreateDCCDirectionFrame(bits *d2datautils.BitMuncher, direction *DCCDirection) *DCCDirectionFrame { 29 | result := &DCCDirectionFrame{} 30 | 31 | bits.GetBits(direction.Variable0Bits) // Variable0 32 | 33 | result.Width = int(bits.GetBits(direction.WidthBits)) 34 | result.Height = int(bits.GetBits(direction.HeightBits)) 35 | result.XOffset = bits.GetSignedBits(direction.XOffsetBits) 36 | result.YOffset = bits.GetSignedBits(direction.YOffsetBits) 37 | result.NumberOfOptionalBytes = int(bits.GetBits(direction.OptionalDataBits)) 38 | result.NumberOfCodedBytes = int(bits.GetBits(direction.CodedBytesBits)) 39 | result.FrameIsBottomUp = bits.GetBit() == 1 40 | 41 | if result.FrameIsBottomUp { 42 | log.Panic("Bottom up frames are not implemented.") 43 | } else { 44 | result.Box = d2geom.Rectangle{ 45 | Left: result.XOffset, 46 | Top: result.YOffset - result.Height + 1, 47 | Width: result.Width, 48 | Height: result.Height, 49 | } 50 | } 51 | 52 | result.valid = true 53 | 54 | return result 55 | } 56 | 57 | func (v *DCCDirectionFrame) recalculateCells(direction *DCCDirection) { 58 | var w = 4 - ((v.Box.Left - direction.Box.Left) % 4) // Width of the first column (in pixels) 59 | 60 | if (v.Width - w) <= 1 { 61 | v.HorizontalCellCount = 1 62 | } else { 63 | tmp := v.Width - w - 1 64 | v.HorizontalCellCount = 2 + (tmp / 4) //nolint:gomnd // magic math 65 | if (tmp % 4) == 0 { 66 | v.HorizontalCellCount-- 67 | } 68 | } 69 | 70 | // Height of the first column (in pixels) 71 | h := 4 - ((v.Box.Top - direction.Box.Top) % 4) //nolint:gomnd // data decode 72 | 73 | if (v.Height - h) <= 1 { 74 | v.VerticalCellCount = 1 75 | } else { 76 | tmp := v.Height - h - 1 77 | v.VerticalCellCount = 2 + (tmp / 4) //nolint:gomnd // data decode 78 | if (tmp % 4) == 0 { 79 | v.VerticalCellCount-- 80 | } 81 | } 82 | // Calculate the cell widths and heights 83 | cellWidths := make([]int, v.HorizontalCellCount) 84 | if v.HorizontalCellCount == 1 { 85 | cellWidths[0] = v.Width 86 | } else { 87 | cellWidths[0] = w 88 | for i := 1; i < (v.HorizontalCellCount - 1); i++ { 89 | cellWidths[i] = 4 90 | } 91 | cellWidths[v.HorizontalCellCount-1] = v.Width - w - (4 * (v.HorizontalCellCount - 2)) 92 | } 93 | 94 | cellHeights := make([]int, v.VerticalCellCount) 95 | if v.VerticalCellCount == 1 { 96 | cellHeights[0] = v.Height 97 | } else { 98 | cellHeights[0] = h 99 | for i := 1; i < (v.VerticalCellCount - 1); i++ { 100 | cellHeights[i] = 4 101 | } 102 | cellHeights[v.VerticalCellCount-1] = v.Height - h - (4 * (v.VerticalCellCount - 2)) 103 | } 104 | 105 | v.Cells = make([]DCCCell, v.HorizontalCellCount*v.VerticalCellCount) 106 | offsetY := v.Box.Top - direction.Box.Top 107 | 108 | for y := 0; y < v.VerticalCellCount; y++ { 109 | offsetX := v.Box.Left - direction.Box.Left 110 | 111 | for x := 0; x < v.HorizontalCellCount; x++ { 112 | v.Cells[x+(y*v.HorizontalCellCount)] = DCCCell{ 113 | XOffset: offsetX, 114 | YOffset: offsetY, 115 | Width: cellWidths[x], 116 | Height: cellHeights[y], 117 | } 118 | 119 | offsetX += cellWidths[x] 120 | } 121 | 122 | offsetY += cellHeights[y] 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /pkg/fileformats/animdata/animdata.go: -------------------------------------------------------------------------------- 1 | package d2animdata 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" 9 | ) 10 | 11 | const ( 12 | numBlocks = 256 13 | maxRecordsPerBlock = 67 14 | byteCountName = 8 15 | byteCountSpeedPadding = 2 16 | numEvents = 144 17 | speedDivisor = 256 18 | speedBaseFPS = 25 19 | milliseconds = 1000 20 | ) 21 | 22 | // AnimationData is a representation of the binary data from `data/global/AnimData.d2` 23 | type AnimationData struct { 24 | hashTable 25 | blocks [numBlocks]*block 26 | entries map[string][]*AnimationDataRecord 27 | } 28 | 29 | // GetRecordNames returns a slice of all record name strings 30 | func (ad *AnimationData) GetRecordNames() []string { 31 | result := make([]string, 0) 32 | 33 | for name := range ad.entries { 34 | result = append(result, name) 35 | } 36 | 37 | return result 38 | } 39 | 40 | // GetRecord returns a single AnimationDataRecord with the given name string. If there is more 41 | // than one record with the given name string, the last record entry will be returned. 42 | func (ad *AnimationData) GetRecord(name string) *AnimationDataRecord { 43 | records := ad.GetRecords(name) 44 | numRecords := len(records) 45 | 46 | if numRecords < 1 { 47 | return nil 48 | } 49 | 50 | return records[numRecords-1] 51 | } 52 | 53 | // GetRecords returns all records that have the given name string. The AnimData.d2 files have 54 | // multiple records with the same name, but other values in the record are different. 55 | func (ad *AnimationData) GetRecords(name string) []*AnimationDataRecord { 56 | return ad.entries[name] 57 | } 58 | 59 | // Load loads the data into an AnimationData struct 60 | //nolint:gocognit,funlen // can't reduce 61 | func Load(data []byte) (*AnimationData, error) { 62 | reader := d2datautils.CreateStreamReader(data) 63 | animdata := &AnimationData{} 64 | hashIdx := 0 65 | 66 | animdata.entries = make(map[string][]*AnimationDataRecord) 67 | 68 | for blockIdx := range animdata.blocks { 69 | recordCount, err := reader.ReadUInt32() 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | if recordCount > maxRecordsPerBlock { 75 | return nil, fmt.Errorf("more than %d records in block", maxRecordsPerBlock) 76 | } 77 | 78 | records := make([]*AnimationDataRecord, recordCount) 79 | 80 | for recordIdx := uint32(0); recordIdx < recordCount; recordIdx++ { 81 | nameBytes, err := reader.ReadBytes(byteCountName) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | if nameBytes[byteCountName-1] != byte(0) { 87 | return nil, errors.New("animdata AnimationDataRecord name missing null terminator byte") 88 | } 89 | 90 | name := string(nameBytes) 91 | name = strings.ReplaceAll(name, string(byte(0)), "") 92 | 93 | animdata.hashTable[hashIdx] = hashName(name) 94 | 95 | frames, err := reader.ReadUInt32() 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | speed, err := reader.ReadUInt16() 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | reader.SkipBytes(byteCountSpeedPadding) 106 | 107 | events := make(map[int]AnimationEvent) 108 | 109 | for eventIdx := 0; eventIdx < numEvents; eventIdx++ { 110 | eventByte, err := reader.ReadByte() 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | event := AnimationEvent(eventByte) 116 | if event != AnimationEventNone { 117 | events[eventIdx] = event 118 | } 119 | } 120 | 121 | r := &AnimationDataRecord{ 122 | name, 123 | frames, 124 | speed, 125 | events, 126 | } 127 | 128 | records[recordIdx] = r 129 | 130 | if _, found := animdata.entries[r.name]; !found { 131 | animdata.entries[r.name] = make([]*AnimationDataRecord, 0) 132 | } 133 | 134 | animdata.entries[r.name] = append(animdata.entries[r.name], r) 135 | } 136 | 137 | b := &block{ 138 | recordCount, 139 | records, 140 | } 141 | 142 | animdata.blocks[blockIdx] = b 143 | } 144 | 145 | if reader.Position() != uint64(len(data)) { 146 | return nil, errors.New("unable to parse animation data") 147 | } 148 | 149 | return animdata, nil 150 | } 151 | -------------------------------------------------------------------------------- /pkg/fileformats/dc6file/dc6.go: -------------------------------------------------------------------------------- 1 | package d2dc6 2 | 3 | import ( 4 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" 5 | ) 6 | 7 | const ( 8 | endOfScanLine = 0x80 9 | maxRunLength = 0x7f 10 | 11 | terminationSize = 4 12 | terminatorSize = 3 13 | ) 14 | 15 | type scanlineState int 16 | 17 | const ( 18 | endOfLine scanlineState = iota 19 | runOfTransparentPixels 20 | runOfOpaquePixels 21 | ) 22 | 23 | // DC6 represents a DC6 file. 24 | type DC6 struct { 25 | Version int32 26 | Flags uint32 27 | Encoding uint32 28 | Termination []byte // 4 bytes 29 | Directions uint32 30 | FramesPerDirection uint32 31 | FramePointers []uint32 // size is Directions*FramesPerDirection 32 | Frames []*DC6Frame // size is Directions*FramesPerDirection 33 | } 34 | 35 | // Load uses restruct to read the binary dc6 data into structs then parses image data from the frame data. 36 | func Load(data []byte) (*DC6, error) { 37 | r := d2datautils.CreateStreamReader(data) 38 | 39 | var dc DC6 40 | 41 | var err error 42 | 43 | err = dc.loadHeader(r) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | frameCount := int(dc.Directions * dc.FramesPerDirection) 49 | 50 | dc.FramePointers = make([]uint32, frameCount) 51 | for i := 0; i < frameCount; i++ { 52 | dc.FramePointers[i], err = r.ReadUInt32() 53 | if err != nil { 54 | return nil, err 55 | } 56 | } 57 | 58 | dc.Frames = make([]*DC6Frame, frameCount) 59 | 60 | if err := dc.loadFrames(r); err != nil { 61 | return nil, err 62 | } 63 | 64 | return &dc, nil 65 | } 66 | 67 | func (d *DC6) loadHeader(r *d2datautils.StreamReader) error { 68 | var err error 69 | 70 | if d.Version, err = r.ReadInt32(); err != nil { 71 | return err 72 | } 73 | 74 | if d.Flags, err = r.ReadUInt32(); err != nil { 75 | return err 76 | } 77 | 78 | if d.Encoding, err = r.ReadUInt32(); err != nil { 79 | return err 80 | } 81 | 82 | if d.Termination, err = r.ReadBytes(terminationSize); err != nil { 83 | return err 84 | } 85 | 86 | if d.Directions, err = r.ReadUInt32(); err != nil { 87 | return err 88 | } 89 | 90 | if d.FramesPerDirection, err = r.ReadUInt32(); err != nil { 91 | return err 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func (d *DC6) loadFrames(r *d2datautils.StreamReader) error { 98 | var err error 99 | 100 | for i := 0; i < len(d.FramePointers); i++ { 101 | frame := &DC6Frame{} 102 | 103 | if frame.Flipped, err = r.ReadUInt32(); err != nil { 104 | return err 105 | } 106 | 107 | if frame.Width, err = r.ReadUInt32(); err != nil { 108 | return err 109 | } 110 | 111 | if frame.Height, err = r.ReadUInt32(); err != nil { 112 | return err 113 | } 114 | 115 | if frame.OffsetX, err = r.ReadInt32(); err != nil { 116 | return err 117 | } 118 | 119 | if frame.OffsetY, err = r.ReadInt32(); err != nil { 120 | return err 121 | } 122 | 123 | if frame.Unknown, err = r.ReadUInt32(); err != nil { 124 | return err 125 | } 126 | 127 | if frame.NextBlock, err = r.ReadUInt32(); err != nil { 128 | return err 129 | } 130 | 131 | if frame.Length, err = r.ReadUInt32(); err != nil { 132 | return err 133 | } 134 | 135 | if frame.FrameData, err = r.ReadBytes(int(frame.Length)); err != nil { 136 | return err 137 | } 138 | 139 | if frame.Terminator, err = r.ReadBytes(terminatorSize); err != nil { 140 | return err 141 | } 142 | 143 | d.Frames[i] = frame 144 | } 145 | 146 | return nil 147 | } 148 | 149 | // DecodeFrame decodes the given frame to an indexed color texture 150 | func (d *DC6) DecodeFrame(frameIndex int) []byte { 151 | frame := d.Frames[frameIndex] 152 | 153 | indexData := make([]byte, frame.Width*frame.Height) 154 | x := 0 155 | y := int(frame.Height) - 1 156 | offset := 0 157 | 158 | loop: // this is a label for the loop, so the switch can break the loop (and not the switch) 159 | for { 160 | b := int(frame.FrameData[offset]) 161 | offset++ 162 | 163 | switch scanlineType(b) { 164 | case endOfLine: 165 | if y == 0 { 166 | break loop 167 | } 168 | 169 | y-- 170 | 171 | x = 0 172 | case runOfTransparentPixels: 173 | transparentPixels := b & maxRunLength 174 | x += transparentPixels 175 | case runOfOpaquePixels: 176 | for i := 0; i < b; i++ { 177 | indexData[x+y*int(frame.Width)+i] = frame.FrameData[offset] 178 | offset++ 179 | } 180 | 181 | x += b 182 | } 183 | } 184 | 185 | return indexData 186 | } 187 | 188 | func scanlineType(b int) scanlineState { 189 | if b == endOfScanLine { 190 | return endOfLine 191 | } 192 | 193 | if (b & endOfScanLine) > 0 { 194 | return runOfTransparentPixels 195 | } 196 | 197 | return runOfOpaquePixels 198 | } 199 | 200 | // Clone creates a copy of the DC6 201 | func (d *DC6) Clone() *DC6 { 202 | clone := *d 203 | copy(clone.Termination, d.Termination) 204 | copy(clone.FramePointers, d.FramePointers) 205 | clone.Frames = make([]*DC6Frame, len(d.Frames)) 206 | 207 | for i := range d.Frames { 208 | cloneFrame := *d.Frames[i] 209 | clone.Frames = append(clone.Frames, &cloneFrame) 210 | } 211 | 212 | return &clone 213 | } 214 | -------------------------------------------------------------------------------- /pkg/fileformats/mpqfile/mpq.go: -------------------------------------------------------------------------------- 1 | package mpqfile 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | 14 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" 15 | ) 16 | 17 | var _ d2interface.Archive = &MPQ{} // Static check to confirm struct conforms to interface 18 | 19 | // MPQ represents an MPQ archive 20 | type MPQ struct { 21 | filePath string 22 | file *os.File 23 | hashes map[uint64]*Hash 24 | blocks []*Block 25 | header Header 26 | } 27 | 28 | // PatchInfo represents patch info for the MPQ. 29 | type PatchInfo struct { 30 | Length uint32 // Length of patch info header, in bytes 31 | Flags uint32 // Flags. 0x80000000 = MD5 (?) 32 | DataSize uint32 // Uncompressed size of the patch file 33 | MD5 [16]byte // MD5 of the entire patch file after decompression 34 | } 35 | 36 | // New loads an MPQ file and only reads the header 37 | func New(fileName string) (*MPQ, error) { 38 | mpq := &MPQ{filePath: fileName} 39 | 40 | var err error 41 | if runtime.GOOS == "linux" { 42 | mpq.file, err = openIgnoreCase(fileName) 43 | } else { 44 | mpq.file, err = os.Open(fileName) //nolint:gosec // Will fix later 45 | } 46 | 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if err := mpq.readHeader(); err != nil { 52 | return nil, fmt.Errorf("failed to read reader: %v", err) 53 | } 54 | 55 | return mpq, nil 56 | } 57 | 58 | // FromFile loads an MPQ file and returns a MPQ structure 59 | func FromFile(fileName string) (*MPQ, error) { 60 | mpq, err := New(fileName) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | if err := mpq.readHashTable(); err != nil { 66 | return nil, fmt.Errorf("failed to read hash table: %v", err) 67 | } 68 | 69 | if err := mpq.readBlockTable(); err != nil { 70 | return nil, fmt.Errorf("failed to read block table: %v", err) 71 | } 72 | 73 | return mpq, nil 74 | } 75 | 76 | // getFileBlockData gets a block table entry 77 | func (mpq *MPQ) getFileBlockData(fileName string) (*Block, error) { 78 | fileEntry, ok := mpq.hashes[hashFilename(fileName)] 79 | if !ok { 80 | return nil, errors.New("file not found") 81 | } 82 | 83 | if fileEntry.BlockIndex >= uint32(len(mpq.blocks)) { 84 | return nil, errors.New("invalid block index") 85 | } 86 | 87 | return mpq.blocks[fileEntry.BlockIndex], nil 88 | } 89 | 90 | // Close closes the MPQ file 91 | func (mpq *MPQ) Close() error { 92 | return mpq.file.Close() 93 | } 94 | 95 | // ReadFile reads a file from the MPQ and returns a memory stream 96 | func (mpq *MPQ) ReadFile(fileName string) ([]byte, error) { 97 | fileBlockData, err := mpq.getFileBlockData(fileName) 98 | if err != nil { 99 | return []byte{}, err 100 | } 101 | 102 | fileBlockData.FileName = strings.ToLower(fileName) 103 | 104 | stream, err := CreateStream(mpq, fileBlockData, fileName) 105 | if err != nil { 106 | return []byte{}, err 107 | } 108 | 109 | buffer := make([]byte, fileBlockData.UncompressedFileSize) 110 | if _, err := stream.Read(buffer, 0, fileBlockData.UncompressedFileSize); err != nil { 111 | return []byte{}, err 112 | } 113 | 114 | return buffer, nil 115 | } 116 | 117 | // ReadFileStream reads the mpq file data and returns a stream 118 | func (mpq *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) { 119 | fileBlockData, err := mpq.getFileBlockData(fileName) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | fileBlockData.FileName = strings.ToLower(fileName) 125 | 126 | stream, err := CreateStream(mpq, fileBlockData, fileName) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | return &MpqDataStream{stream: stream}, nil 132 | } 133 | 134 | // ReadTextFile reads a file and returns it as a string 135 | func (mpq *MPQ) ReadTextFile(fileName string) (string, error) { 136 | data, err := mpq.ReadFile(fileName) 137 | if err != nil { 138 | return "", err 139 | } 140 | 141 | return string(data), nil 142 | } 143 | 144 | // Listfile returns the list of files in this MPQ 145 | func (mpq *MPQ) Listfile() ([]string, error) { 146 | data, err := mpq.ReadFile("(listfile)") 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | raw := strings.TrimRight(string(data), "\x00") 152 | s := bufio.NewScanner(strings.NewReader(raw)) 153 | 154 | var filePaths []string 155 | 156 | for s.Scan() { 157 | filePath := s.Text() 158 | filePaths = append(filePaths, filePath) 159 | } 160 | 161 | return filePaths, nil 162 | } 163 | 164 | // Path returns the MPQ file path 165 | func (mpq *MPQ) Path() string { 166 | return mpq.filePath 167 | } 168 | 169 | // Contains returns bool for whether the given filename exists in the mpq 170 | func (mpq *MPQ) Contains(filename string) bool { 171 | _, ok := mpq.hashes[hashFilename(filename)] 172 | return ok 173 | } 174 | 175 | // Size returns the size of the mpq in bytes 176 | func (mpq *MPQ) Size() uint32 { 177 | return mpq.header.ArchiveSize 178 | } 179 | 180 | func openIgnoreCase(mpqPath string) (*os.File, error) { 181 | // First see if file exists with specified case 182 | mpqFile, err := os.Open(mpqPath) //nolint:gosec // Will fix later 183 | if err == nil { 184 | return mpqFile, err 185 | } 186 | 187 | mpqName := filepath.Base(mpqPath) 188 | mpqDir := filepath.Dir(mpqPath) 189 | 190 | files, err := ioutil.ReadDir(mpqDir) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | for _, file := range files { 196 | if strings.EqualFold(file.Name(), mpqName) { 197 | mpqName = file.Name() 198 | break 199 | } 200 | } 201 | 202 | return os.Open(path.Join(mpqDir, mpqName)) //nolint:gosec // Will fix later 203 | } 204 | -------------------------------------------------------------------------------- /pkg/fileformats/dt1file/dt1.go: -------------------------------------------------------------------------------- 1 | package d2dt1 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" 7 | ) 8 | 9 | // DT1 represents a DT1 file. 10 | type DT1 struct { 11 | Tiles []Tile 12 | } 13 | 14 | // BlockDataFormat represents the format of the block data 15 | type BlockDataFormat int16 16 | 17 | const ( 18 | // BlockFormatRLE specifies the block format is RLE encoded 19 | BlockFormatRLE BlockDataFormat = 0 20 | 21 | // BlockFormatIsometric specifies the block format isometrically encoded 22 | BlockFormatIsometric BlockDataFormat = 1 23 | ) 24 | 25 | const ( 26 | numUnknownHeaderBytes = 260 27 | knownMajorVersion = 7 28 | knownMinorVersion = 6 29 | numUnknownTileBytes1 = 4 30 | numUnknownTileBytes2 = 4 31 | numUnknownTileBytes3 = 7 32 | numUnknownTileBytes4 = 12 33 | ) 34 | 35 | // LoadDT1 loads a DT1 record 36 | //nolint:funlen,gocognit,gocyclo // Can't reduce 37 | func LoadDT1(fileData []byte) (*DT1, error) { 38 | result := &DT1{} 39 | br := d2datautils.CreateStreamReader(fileData) 40 | 41 | var err error 42 | 43 | majorVersion, err := br.ReadInt32() 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | minorVersion, err := br.ReadInt32() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | if majorVersion != knownMajorVersion || minorVersion != knownMinorVersion { 54 | const fmtErr = "expected to have a version of 7.6, but got %d.%d instead" 55 | return nil, fmt.Errorf(fmtErr, majorVersion, minorVersion) 56 | } 57 | 58 | br.SkipBytes(numUnknownHeaderBytes) 59 | 60 | numberOfTiles, err := br.ReadInt32() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | position, err := br.ReadInt32() 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | br.SetPosition(uint64(position)) 71 | 72 | result.Tiles = make([]Tile, numberOfTiles) 73 | 74 | for tileIdx := range result.Tiles { 75 | tile := Tile{} 76 | 77 | tile.Direction, err = br.ReadInt32() 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | tile.RoofHeight, err = br.ReadInt16() 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | var matFlagBytes uint16 88 | 89 | matFlagBytes, err = br.ReadUInt16() 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | tile.MaterialFlags = NewMaterialFlags(matFlagBytes) 95 | 96 | tile.Height, err = br.ReadInt32() 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | tile.Width, err = br.ReadInt32() 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | br.SkipBytes(numUnknownTileBytes1) 107 | 108 | tile.Type, err = br.ReadInt32() 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | tile.Style, err = br.ReadInt32() 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | tile.Sequence, err = br.ReadInt32() 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | tile.RarityFrameIndex, err = br.ReadInt32() 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | br.SkipBytes(numUnknownTileBytes2) 129 | 130 | for i := range tile.SubTileFlags { 131 | var subtileFlagBytes byte 132 | 133 | subtileFlagBytes, err = br.ReadByte() 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | tile.SubTileFlags[i] = NewSubTileFlags(subtileFlagBytes) 139 | } 140 | 141 | br.SkipBytes(numUnknownTileBytes3) 142 | 143 | tile.blockHeaderPointer, err = br.ReadInt32() 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | tile.blockHeaderSize, err = br.ReadInt32() 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | var numBlocks int32 154 | 155 | numBlocks, err = br.ReadInt32() 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | tile.Blocks = make([]Block, numBlocks) 161 | 162 | br.SkipBytes(numUnknownTileBytes4) 163 | 164 | result.Tiles[tileIdx] = tile 165 | } 166 | 167 | for tileIdx := range result.Tiles { 168 | tile := &result.Tiles[tileIdx] 169 | br.SetPosition(uint64(tile.blockHeaderPointer)) 170 | 171 | for blockIdx := range tile.Blocks { 172 | result.Tiles[tileIdx].Blocks[blockIdx].X, err = br.ReadInt16() 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | result.Tiles[tileIdx].Blocks[blockIdx].Y, err = br.ReadInt16() 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | br.SkipBytes(2) //nolint:gomnd // Unknown data 183 | 184 | result.Tiles[tileIdx].Blocks[blockIdx].GridX, err = br.ReadByte() 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | result.Tiles[tileIdx].Blocks[blockIdx].GridY, err = br.ReadByte() 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | formatValue, err := br.ReadInt16() 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | if formatValue == 1 { 200 | result.Tiles[tileIdx].Blocks[blockIdx].Format = BlockFormatIsometric 201 | } else { 202 | result.Tiles[tileIdx].Blocks[blockIdx].Format = BlockFormatRLE 203 | } 204 | 205 | result.Tiles[tileIdx].Blocks[blockIdx].Length, err = br.ReadInt32() 206 | if err != nil { 207 | return nil, err 208 | } 209 | 210 | br.SkipBytes(2) //nolint:gomnd // Unknown data 211 | 212 | result.Tiles[tileIdx].Blocks[blockIdx].FileOffset, err = br.ReadInt32() 213 | if err != nil { 214 | return nil, err 215 | } 216 | } 217 | 218 | for blockIndex, block := range tile.Blocks { 219 | br.SetPosition(uint64(tile.blockHeaderPointer + block.FileOffset)) 220 | 221 | encodedData, err := br.ReadBytes(int(block.Length)) 222 | if err != nil { 223 | return nil, err 224 | } 225 | 226 | tile.Blocks[blockIndex].EncodedData = encodedData 227 | } 228 | } 229 | 230 | return result, nil 231 | } 232 | -------------------------------------------------------------------------------- /pkg/fileformats/mpqfile/mpq_stream.go: -------------------------------------------------------------------------------- 1 | package mpqfile 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "io" 10 | 11 | "github.com/JoshVarga/blast" 12 | 13 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2compression" 14 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" 15 | ) 16 | 17 | // Stream represents a stream of data in an MPQ archive 18 | type Stream struct { 19 | Data []byte 20 | Positions []uint32 21 | MPQ *MPQ 22 | Block *Block 23 | Index uint32 24 | Size uint32 25 | Position uint32 26 | } 27 | 28 | // CreateStream creates an MPQ stream 29 | func CreateStream(mpq *MPQ, block *Block, fileName string) (*Stream, error) { 30 | s := &Stream{ 31 | MPQ: mpq, 32 | Block: block, 33 | Index: 0xFFFFFFFF, //nolint:gomnd // MPQ magic 34 | } 35 | 36 | if s.Block.HasFlag(FileFixKey) { 37 | s.Block.calculateEncryptionSeed(fileName) 38 | } 39 | 40 | s.Size = 0x200 << s.MPQ.header.BlockSize //nolint:gomnd // MPQ magic 41 | 42 | if s.Block.HasFlag(FilePatchFile) { 43 | return nil, errors.New("patching is not supported") 44 | } 45 | 46 | if (s.Block.HasFlag(FileCompress) || s.Block.HasFlag(FileImplode)) && !s.Block.HasFlag(FileSingleUnit) { 47 | if err := s.loadBlockOffsets(); err != nil { 48 | return nil, err 49 | } 50 | } 51 | 52 | return s, nil 53 | } 54 | 55 | func (v *Stream) loadBlockOffsets() error { 56 | if _, err := v.MPQ.file.Seek(int64(v.Block.FilePosition), io.SeekStart); err != nil { 57 | return err 58 | } 59 | 60 | blockPositionCount := ((v.Block.UncompressedFileSize + v.Size - 1) / v.Size) + 1 61 | v.Positions = make([]uint32, blockPositionCount) 62 | 63 | if err := binary.Read(v.MPQ.file, binary.LittleEndian, &v.Positions); err != nil { 64 | return err 65 | } 66 | 67 | if v.Block.HasFlag(FileEncrypted) { 68 | decrypt(v.Positions, v.Block.EncryptionSeed-1) 69 | 70 | blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic 71 | if v.Positions[0] != blockPosSize { 72 | return errors.New("decryption of MPQ failed") 73 | } 74 | 75 | if v.Positions[1] > v.Size+blockPosSize { 76 | return errors.New("decryption of MPQ failed") 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (v *Stream) Read(buffer []byte, offset, count uint32) (readTotal uint32, err error) { 84 | if v.Block.HasFlag(FileSingleUnit) { 85 | return v.readInternalSingleUnit(buffer, offset, count) 86 | } 87 | 88 | var read uint32 89 | 90 | toRead := count 91 | for toRead > 0 { 92 | if read, err = v.readInternal(buffer, offset, toRead); err != nil { 93 | return readTotal, err 94 | } 95 | 96 | if read == 0 { 97 | break 98 | } 99 | 100 | readTotal += read 101 | offset += read 102 | toRead -= read 103 | } 104 | 105 | return readTotal, nil 106 | } 107 | 108 | func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) (uint32, error) { 109 | if len(v.Data) == 0 { 110 | if err := v.loadSingleUnit(); err != nil { 111 | return 0, err 112 | } 113 | } 114 | 115 | return v.copy(buffer, offset, v.Position, count) 116 | } 117 | 118 | func (v *Stream) readInternal(buffer []byte, offset, count uint32) (uint32, error) { 119 | if err := v.bufferData(); err != nil { 120 | return 0, err 121 | } 122 | 123 | localPosition := v.Position % v.Size 124 | 125 | return v.copy(buffer, offset, localPosition, count) 126 | } 127 | 128 | func (v *Stream) copy(buffer []byte, offset, pos, count uint32) (uint32, error) { 129 | bytesToCopy := d2math.Min(uint32(len(v.Data))-pos, count) 130 | if bytesToCopy <= 0 { 131 | return 0, io.EOF 132 | } 133 | 134 | copy(buffer[offset:offset+bytesToCopy], v.Data[pos:pos+bytesToCopy]) 135 | v.Position += bytesToCopy 136 | 137 | return bytesToCopy, nil 138 | } 139 | 140 | func (v *Stream) bufferData() (err error) { 141 | blockIndex := v.Position / v.Size 142 | 143 | if blockIndex == v.Index { 144 | return nil 145 | } 146 | 147 | expectedLength := d2math.Min(v.Block.UncompressedFileSize-(blockIndex*v.Size), v.Size) 148 | if v.Data, err = v.loadBlock(blockIndex, expectedLength); err != nil { 149 | return err 150 | } 151 | 152 | v.Index = blockIndex 153 | 154 | return nil 155 | } 156 | 157 | func (v *Stream) loadSingleUnit() (err error) { 158 | if _, err = v.MPQ.file.Seek(int64(v.MPQ.header.HeaderSize), io.SeekStart); err != nil { 159 | return err 160 | } 161 | 162 | fileData := make([]byte, v.Size) 163 | 164 | if _, err = v.MPQ.file.Read(fileData); err != nil { 165 | return err 166 | } 167 | 168 | if v.Size == v.Block.UncompressedFileSize { 169 | v.Data = fileData 170 | return nil 171 | } 172 | 173 | v.Data, err = decompressMulti(fileData, v.Block.UncompressedFileSize) 174 | 175 | return err 176 | } 177 | 178 | func (v *Stream) loadBlock(blockIndex, expectedLength uint32) ([]byte, error) { 179 | var ( 180 | offset uint32 181 | toRead uint32 182 | ) 183 | 184 | if v.Block.HasFlag(FileCompress) || v.Block.HasFlag(FileImplode) { 185 | offset = v.Positions[blockIndex] 186 | toRead = v.Positions[blockIndex+1] - offset 187 | } else { 188 | offset = blockIndex * v.Size 189 | toRead = expectedLength 190 | } 191 | 192 | offset += v.Block.FilePosition 193 | data := make([]byte, toRead) 194 | 195 | if _, err := v.MPQ.file.Seek(int64(offset), io.SeekStart); err != nil { 196 | return []byte{}, err 197 | } 198 | 199 | if _, err := v.MPQ.file.Read(data); err != nil { 200 | return []byte{}, err 201 | } 202 | 203 | if v.Block.HasFlag(FileEncrypted) && v.Block.UncompressedFileSize > 3 { 204 | if v.Block.EncryptionSeed == 0 { 205 | return []byte{}, errors.New("unable to determine encryption key") 206 | } 207 | 208 | decryptBytes(data, blockIndex+v.Block.EncryptionSeed) 209 | } 210 | 211 | if v.Block.HasFlag(FileCompress) && (toRead != expectedLength) { 212 | if !v.Block.HasFlag(FileSingleUnit) { 213 | return decompressMulti(data, expectedLength) 214 | } 215 | 216 | return pkDecompress(data) 217 | } 218 | 219 | if v.Block.HasFlag(FileImplode) && (toRead != expectedLength) { 220 | return pkDecompress(data) 221 | } 222 | 223 | return data, nil 224 | } 225 | 226 | //nolint:gomnd,funlen,gocyclo // Will fix enum values later, can't help function length 227 | func decompressMulti(data []byte /*expectedLength*/, _ uint32) ([]byte, error) { 228 | compressionType := data[0] 229 | 230 | switch compressionType { 231 | case 1: // Huffman 232 | return []byte{}, errors.New("huffman decompression not supported") 233 | case 2: // ZLib/Deflate 234 | return deflate(data[1:]) 235 | case 8: // PKLib/Impode 236 | return pkDecompress(data[1:]) 237 | case 0x10: // BZip2 238 | return []byte{}, errors.New("bzip2 decompression not supported") 239 | case 0x80: // IMA ADPCM Stereo 240 | return d2compression.WavDecompress(data[1:], 2) 241 | case 0x40: // IMA ADPCM Mono 242 | return d2compression.WavDecompress(data[1:], 1) 243 | case 0x12: 244 | return []byte{}, errors.New("lzma decompression not supported") 245 | // Combos 246 | case 0x22: 247 | // sparse then zlib 248 | return []byte{}, errors.New("sparse decompression + deflate decompression not supported") 249 | case 0x30: 250 | // sparse then bzip2 251 | return []byte{}, errors.New("sparse decompression + bzip2 decompression not supported") 252 | case 0x41: 253 | sinput, err := d2compression.WavDecompress(d2compression.HuffmanDecompress(data[1:]), 1) 254 | if err != nil { 255 | return nil, err 256 | } 257 | 258 | tmp := make([]byte, len(sinput)) 259 | 260 | copy(tmp, sinput) 261 | 262 | return tmp, nil 263 | case 0x48: 264 | // byte[] result = PKDecompress(sinput, outputLength); 265 | // return MpqWavCompression.Decompress(new MemoryStream(result), 1); 266 | return []byte{}, errors.New("pk + mpqwav decompression not supported") 267 | case 0x81: 268 | sinput, err := d2compression.WavDecompress(d2compression.HuffmanDecompress(data[1:]), 2) 269 | if err != nil { 270 | return nil, err 271 | } 272 | 273 | tmp := make([]byte, len(sinput)) 274 | copy(tmp, sinput) 275 | 276 | return tmp, nil 277 | case 0x88: 278 | // byte[] result = PKDecompress(sinput, outputLength); 279 | // return MpqWavCompression.Decompress(new MemoryStream(result), 2); 280 | return []byte{}, errors.New("pk + wav decompression not supported") 281 | } 282 | 283 | return []byte{}, fmt.Errorf("decompression not supported for unknown compression type %X", compressionType) 284 | } 285 | 286 | func deflate(data []byte) ([]byte, error) { 287 | b := bytes.NewReader(data) 288 | 289 | r, err := zlib.NewReader(b) 290 | if err != nil { 291 | return []byte{}, err 292 | } 293 | 294 | buffer := new(bytes.Buffer) 295 | 296 | _, err = buffer.ReadFrom(r) 297 | if err != nil { 298 | return []byte{}, err 299 | } 300 | 301 | err = r.Close() 302 | if err != nil { 303 | return []byte{}, err 304 | } 305 | 306 | return buffer.Bytes(), nil 307 | } 308 | 309 | func pkDecompress(data []byte) ([]byte, error) { 310 | b := bytes.NewReader(data) 311 | 312 | r, err := blast.NewReader(b) 313 | if err != nil { 314 | return []byte{}, err 315 | } 316 | 317 | buffer := new(bytes.Buffer) 318 | 319 | if _, err = buffer.ReadFrom(r); err != nil { 320 | return []byte{}, err 321 | } 322 | 323 | err = r.Close() 324 | if err != nil { 325 | return []byte{}, err 326 | } 327 | 328 | return buffer.Bytes(), nil 329 | } 330 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 5 | github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0 h1:tDnuU0igiBiQFjsvq1Bi7DpoUjqI76VVvW045vpeFeM= 6 | github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0/go.mod h1:h/5OEGj4G+fpYxluLjSMZbFY011ZxAntO98nCl8mrCs= 7 | github.com/OpenDiablo2/OpenDiablo2 v0.0.0-20210113210205-9f5cde36df89 h1:A50Xu8c67B3Uxm0Qtz6TYD0zLPk9javIJuFccC05iWo= 8 | github.com/OpenDiablo2/OpenDiablo2 v0.0.0-20210113210205-9f5cde36df89/go.mod h1:3JsaDhsHD+3mmG/WLLct2Zy1HjXS39jsyLPFbgRkbN0= 9 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 10 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 15 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2 h1:Ac1OEHHkbAZ6EUnJahF0GKcU0FjPc/V8F1DvjhKngFE= 16 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 17 | github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= 18 | github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 19 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 20 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 21 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 22 | github.com/gravestench/akara v0.0.0-20201014060234-a64208a7fd3c/go.mod h1:fTeda1SogMg5Lkd4lXMEd/Pk/a5/gQuLGaAI2rn1PBQ= 23 | github.com/gravestench/akara v0.0.0-20210109074143-4f96689694a0 h1:vx9E7+djDJ/YGrsVmyfzCphg1wnkmSu+292SU13dvpg= 24 | github.com/gravestench/akara v0.0.0-20210109074143-4f96689694a0/go.mod h1:fTeda1SogMg5Lkd4lXMEd/Pk/a5/gQuLGaAI2rn1PBQ= 25 | github.com/gravestench/akara v0.0.0-20210116041952-513d453f9ca8 h1:3MOT9gUiXETmJJ3UsHgVa4+okRSdUVW+eSvPD4C+ZnQ= 26 | github.com/gravestench/akara v0.0.0-20210116041952-513d453f9ca8/go.mod h1:uxAqdO3YanpyVIUcFAJyMfpTHbYveWgm0MbJW1JBtMM= 27 | github.com/hajimehoshi/bitmapfont/v2 v2.1.0/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs= 28 | github.com/hajimehoshi/ebiten/v2 v2.0.2 h1:t8HXO9hJfKlS9tNhht8Ov6xecag0gRl7AkfKgC9hcLE= 29 | github.com/hajimehoshi/ebiten/v2 v2.0.2/go.mod h1:AbHP/SS226aFTex/izULVwW0D2AuGyqC4AVwilmRjOg= 30 | github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= 31 | github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= 32 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 33 | github.com/hajimehoshi/oto v0.6.8/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 34 | github.com/jakecoffman/cp v1.0.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg= 35 | github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk= 36 | github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= 37 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 38 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 39 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 40 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 41 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 42 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 43 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 44 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 45 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 46 | github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= 47 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= 50 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 51 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 52 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 53 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 54 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 55 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 56 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 57 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 58 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 59 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 60 | github.com/veandco/go-sdl2 v0.4.5 h1:GFIjMabK7y2XWpr9sGvN7RDKHt7vrA7XPTUW60eOw+Y= 61 | github.com/veandco/go-sdl2 v0.4.5/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= 62 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 63 | go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= 64 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 65 | go.uber.org/dig v1.10.0 h1:yLmDDj9/zuDjv3gz8GQGviXMs9TfysIUMUilCpgzUJY= 66 | go.uber.org/dig v1.10.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw= 67 | go.uber.org/fx v1.13.1 h1:CFNTr1oin5OJ0VCZ8EycL3wzF29Jz2g0xe55RFsf2a4= 68 | go.uber.org/fx v1.13.1/go.mod h1:bREWhavnedxpJeTq9pQT53BbvwhUv7TcpsOqcH4a+3w= 69 | go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= 70 | go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E= 71 | go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 72 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 73 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 74 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 75 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 76 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 77 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 78 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 79 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 80 | golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= 81 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 82 | golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 83 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 84 | golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= 85 | golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 86 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 87 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 88 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 89 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 90 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 91 | golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= 92 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 93 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 94 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 95 | golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 96 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 97 | golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 98 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 99 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 100 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 101 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 102 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 103 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 105 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 110 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20201028215240-c5abc1b1d397 h1:YZ169h3kkKEXsueizzMwOT9AaiffbOa6oXSmUFJ4vxM= 114 | golang.org/x/sys v0.0.0-20201028215240-c5abc1b1d397/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 116 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 117 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 118 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 119 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 120 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 121 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 122 | golang.org/x/tools v0.0.0-20191030062658-86caa796c7ab/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 123 | golang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508 h1:0FYNp0PF9kFm/ZUrvcJiQ12IUJJG7iAc6Cu01wbKrbU= 124 | golang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 125 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 126 | golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 127 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 128 | golang.org/x/tools v0.0.0-20201009162240-fcf82128ed91/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 129 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 130 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 131 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 133 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 134 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 135 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 136 | gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= 137 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 138 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 139 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 140 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 141 | -------------------------------------------------------------------------------- /pkg/fileformats/ds1file/ds1.go: -------------------------------------------------------------------------------- 1 | package d2ds1 2 | 3 | import ( 4 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" 5 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" 6 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" 7 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector" 8 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2path" 9 | ) 10 | 11 | const ( 12 | maxActNumber = 5 13 | subType1 = 1 14 | subType2 = 2 15 | v2 = 2 16 | v3 = 3 17 | v4 = 4 18 | v7 = 7 19 | v8 = 8 20 | v9 = 9 21 | v10 = 10 22 | v12 = 12 23 | v13 = 13 24 | v14 = 14 25 | v15 = 15 26 | v16 = 16 27 | v18 = 18 28 | ) 29 | 30 | // DS1 represents the "stamp" data that is used to build up maps. 31 | type DS1 struct { 32 | Files []string // FilePtr table of file string pointers 33 | Objects []Object // Objects 34 | Tiles [][]TileRecord // The tile data for the DS1 35 | SubstitutionGroups []SubstitutionGroup // Substitution groups for the DS1 36 | Version int32 // The version of the DS1 37 | Width int32 // Width of map, in # of tiles 38 | Height int32 // Height of map, in # of tiles 39 | Act int32 // Act, from 1 to 5. This tells which act table to use for the Objects list 40 | SubstitutionType int32 // SubstitutionType (layer type): 0 if no layer, else type 1 or type 2 41 | NumberOfWalls int32 // WallNum number of wall & orientation layers used 42 | NumberOfFloors int32 // number of floor layers used 43 | NumberOfShadowLayers int32 // ShadowNum number of shadow layer used 44 | NumberOfSubstitutionLayers int32 // SubstitutionNum number of substitution layer used 45 | SubstitutionGroupsNum int32 // SubstitutionGroupsNum number of substitution groups, datas between objects & NPC paths 46 | } 47 | 48 | // LoadDS1 loads the specified DS1 file 49 | //nolint:funlen,gocognit,gocyclo // will refactor later 50 | func LoadDS1(fileData []byte) (*DS1, error) { 51 | ds1 := &DS1{ 52 | Act: 1, 53 | NumberOfFloors: 0, 54 | NumberOfWalls: 0, 55 | NumberOfShadowLayers: 1, 56 | NumberOfSubstitutionLayers: 0, 57 | } 58 | 59 | br := d2datautils.CreateStreamReader(fileData) 60 | 61 | var err error 62 | 63 | ds1.Version, err = br.ReadInt32() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | ds1.Width, err = br.ReadInt32() 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | ds1.Height, err = br.ReadInt32() 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | ds1.Width++ 79 | ds1.Height++ 80 | 81 | if ds1.Version >= v8 { 82 | ds1.Act, err = br.ReadInt32() 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | ds1.Act = d2math.MinInt32(maxActNumber, ds1.Act+1) 88 | } 89 | 90 | if ds1.Version >= v10 { 91 | ds1.SubstitutionType, err = br.ReadInt32() 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | if ds1.SubstitutionType == 1 || ds1.SubstitutionType == 2 { 97 | ds1.NumberOfSubstitutionLayers = 1 98 | } 99 | } 100 | 101 | if ds1.Version >= v3 { 102 | // These files reference things that don't exist anymore :-? 103 | numberOfFiles, err := br.ReadInt32() //nolint:govet // i want to re-use the err variable... 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | ds1.Files = make([]string, numberOfFiles) 109 | 110 | for i := 0; i < int(numberOfFiles); i++ { 111 | ds1.Files[i] = "" 112 | 113 | for { 114 | ch, err := br.ReadByte() 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | if ch == 0 { 120 | break 121 | } 122 | 123 | ds1.Files[i] += string(ch) 124 | } 125 | } 126 | } 127 | 128 | if ds1.Version >= v9 && ds1.Version <= v13 { 129 | // Skipping two dwords because they are "meaningless"? 130 | br.SkipBytes(8) //nolint:gomnd // We don't know what's here 131 | } 132 | 133 | if ds1.Version >= v4 { 134 | ds1.NumberOfWalls, err = br.ReadInt32() 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | if ds1.Version >= v16 { 140 | ds1.NumberOfFloors, err = br.ReadInt32() 141 | if err != nil { 142 | return nil, err 143 | } 144 | } else { 145 | ds1.NumberOfFloors = 1 146 | } 147 | } 148 | 149 | layerStream := ds1.setupStreamLayerTypes() 150 | 151 | ds1.Tiles = make([][]TileRecord, ds1.Height) 152 | 153 | for y := range ds1.Tiles { 154 | ds1.Tiles[y] = make([]TileRecord, ds1.Width) 155 | for x := 0; x < int(ds1.Width); x++ { 156 | ds1.Tiles[y][x].Walls = make([]WallRecord, ds1.NumberOfWalls) 157 | ds1.Tiles[y][x].Floors = make([]FloorShadowRecord, ds1.NumberOfFloors) 158 | ds1.Tiles[y][x].Shadows = make([]FloorShadowRecord, ds1.NumberOfShadowLayers) 159 | ds1.Tiles[y][x].Substitutions = make([]SubstitutionRecord, ds1.NumberOfSubstitutionLayers) 160 | } 161 | } 162 | 163 | err = ds1.loadLayerStreams(br, layerStream) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | err = ds1.loadObjects(br) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | err = ds1.loadSubstitutions(br) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | err = ds1.loadNPCs(br) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | return ds1, nil 184 | } 185 | 186 | func (ds1 *DS1) loadObjects(br *d2datautils.StreamReader) error { 187 | if ds1.Version < v2 { 188 | ds1.Objects = make([]Object, 0) 189 | } else { 190 | numberOfObjects, err := br.ReadInt32() 191 | if err != nil { 192 | return err 193 | } 194 | 195 | ds1.Objects = make([]Object, numberOfObjects) 196 | 197 | for objIdx := 0; objIdx < int(numberOfObjects); objIdx++ { 198 | obj := Object{} 199 | objType, err := br.ReadInt32() 200 | if err != nil { 201 | return err 202 | } 203 | 204 | objID, err := br.ReadInt32() 205 | if err != nil { 206 | return err 207 | } 208 | 209 | objX, err := br.ReadInt32() 210 | if err != nil { 211 | return err 212 | } 213 | 214 | objY, err := br.ReadInt32() 215 | if err != nil { 216 | return err 217 | } 218 | 219 | objFlags, err := br.ReadInt32() 220 | if err != nil { 221 | return err 222 | } 223 | 224 | obj.Type = int(objType) 225 | obj.ID = int(objID) 226 | obj.X = int(objX) 227 | obj.Y = int(objY) 228 | obj.Flags = int(objFlags) 229 | 230 | ds1.Objects[objIdx] = obj 231 | } 232 | } 233 | 234 | return nil 235 | } 236 | 237 | func (ds1 *DS1) loadSubstitutions(br *d2datautils.StreamReader) error { 238 | var err error 239 | 240 | hasSubstitutions := ds1.Version >= v12 && (ds1.SubstitutionType == subType1 || ds1.SubstitutionType == subType2) 241 | 242 | if !hasSubstitutions { 243 | ds1.SubstitutionGroups = make([]SubstitutionGroup, 0) 244 | return nil 245 | } 246 | 247 | if ds1.Version >= v18 { 248 | _, _ = br.ReadUInt32() 249 | } 250 | 251 | numberOfSubGroups, err := br.ReadInt32() 252 | if err != nil { 253 | return err 254 | } 255 | 256 | ds1.SubstitutionGroups = make([]SubstitutionGroup, numberOfSubGroups) 257 | 258 | for subIdx := 0; subIdx < int(numberOfSubGroups); subIdx++ { 259 | newSub := SubstitutionGroup{} 260 | 261 | newSub.TileX, err = br.ReadInt32() 262 | if err != nil { 263 | return err 264 | } 265 | 266 | newSub.TileY, err = br.ReadInt32() 267 | if err != nil { 268 | return err 269 | } 270 | 271 | newSub.WidthInTiles, err = br.ReadInt32() 272 | if err != nil { 273 | return err 274 | } 275 | 276 | newSub.HeightInTiles, err = br.ReadInt32() 277 | if err != nil { 278 | return err 279 | } 280 | 281 | newSub.Unknown, err = br.ReadInt32() 282 | if err != nil { 283 | return err 284 | } 285 | 286 | ds1.SubstitutionGroups[subIdx] = newSub 287 | } 288 | 289 | return err 290 | } 291 | 292 | func (ds1 *DS1) setupStreamLayerTypes() []d2enum.LayerStreamType { 293 | var layerStream []d2enum.LayerStreamType 294 | 295 | if ds1.Version < v4 { 296 | layerStream = []d2enum.LayerStreamType{ 297 | d2enum.LayerStreamWall1, 298 | d2enum.LayerStreamFloor1, 299 | d2enum.LayerStreamOrientation1, 300 | d2enum.LayerStreamSubstitute, 301 | d2enum.LayerStreamShadow, 302 | } 303 | } else { 304 | layerStream = make([]d2enum.LayerStreamType, 305 | (ds1.NumberOfWalls*2)+ds1.NumberOfFloors+ds1.NumberOfShadowLayers+ds1.NumberOfSubstitutionLayers) 306 | 307 | layerIdx := 0 308 | for i := 0; i < int(ds1.NumberOfWalls); i++ { 309 | layerStream[layerIdx] = d2enum.LayerStreamType(int(d2enum.LayerStreamWall1) + i) 310 | layerStream[layerIdx+1] = d2enum.LayerStreamType(int(d2enum.LayerStreamOrientation1) + i) 311 | layerIdx += 2 312 | } 313 | for i := 0; i < int(ds1.NumberOfFloors); i++ { 314 | layerStream[layerIdx] = d2enum.LayerStreamType(int(d2enum.LayerStreamFloor1) + i) 315 | layerIdx++ 316 | } 317 | if ds1.NumberOfShadowLayers > 0 { 318 | layerStream[layerIdx] = d2enum.LayerStreamShadow 319 | layerIdx++ 320 | } 321 | if ds1.NumberOfSubstitutionLayers > 0 { 322 | layerStream[layerIdx] = d2enum.LayerStreamSubstitute 323 | } 324 | } 325 | 326 | return layerStream 327 | } 328 | 329 | func (ds1 *DS1) loadNPCs(br *d2datautils.StreamReader) error { 330 | var err error 331 | 332 | if ds1.Version < v14 { 333 | return err 334 | } 335 | 336 | numberOfNpcs, err := br.ReadInt32() 337 | if err != nil { 338 | return err 339 | } 340 | 341 | for npcIdx := 0; npcIdx < int(numberOfNpcs); npcIdx++ { 342 | numPaths, err := br.ReadInt32() //nolint:govet // i want to re-use the err variable... 343 | if err != nil { 344 | return err 345 | } 346 | 347 | npcX, err := br.ReadInt32() //nolint:govet // i want to re-use the err variable... 348 | if err != nil { 349 | return err 350 | } 351 | 352 | npcY, err := br.ReadInt32() //nolint:govet // i want to re-use the err variable... 353 | if err != nil { 354 | return err 355 | } 356 | 357 | objIdx := -1 358 | 359 | for idx, ds1Obj := range ds1.Objects { 360 | if ds1Obj.X == int(npcX) && ds1Obj.Y == int(npcY) { 361 | objIdx = idx 362 | break 363 | } 364 | } 365 | 366 | if objIdx > -1 { 367 | err = ds1.loadNpcPaths(br, objIdx, int(numPaths)) 368 | if err != nil { 369 | return err 370 | } 371 | } else { 372 | if ds1.Version >= v15 { 373 | br.SkipBytes(int(numPaths) * 3) //nolint:gomnd // Unknown data 374 | } else { 375 | br.SkipBytes(int(numPaths) * 2) //nolint:gomnd // Unknown data 376 | } 377 | } 378 | } 379 | 380 | return err 381 | } 382 | 383 | func (ds1 *DS1) loadNpcPaths(br *d2datautils.StreamReader, objIdx, numPaths int) error { 384 | var err error 385 | 386 | if ds1.Objects[objIdx].Paths == nil { 387 | ds1.Objects[objIdx].Paths = make([]d2path.Path, numPaths) 388 | } 389 | 390 | for pathIdx := 0; pathIdx < numPaths; pathIdx++ { 391 | newPath := d2path.Path{} 392 | 393 | px, err := br.ReadInt32() //nolint:govet // i want to re-use the err variable... 394 | if err != nil { 395 | return err 396 | } 397 | 398 | py, err := br.ReadInt32() //nolint:govet // i want to re-use the err variable... 399 | if err != nil { 400 | return err 401 | } 402 | 403 | newPath.Position = d2vector.NewPosition(float64(px), float64(py)) 404 | 405 | if ds1.Version >= v15 { 406 | action, err := br.ReadInt32() 407 | if err != nil { 408 | return err 409 | } 410 | 411 | newPath.Action = int(action) 412 | } 413 | 414 | ds1.Objects[objIdx].Paths[pathIdx] = newPath 415 | } 416 | 417 | return err 418 | } 419 | 420 | func (ds1 *DS1) loadLayerStreams(br *d2datautils.StreamReader, layerStream []d2enum.LayerStreamType) error { 421 | var err error 422 | 423 | var dirLookup = []int32{ 424 | 0x00, 0x01, 0x02, 0x01, 0x02, 0x03, 0x03, 0x05, 0x05, 0x06, 425 | 0x06, 0x07, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 426 | 0x0F, 0x10, 0x11, 0x12, 0x14, 427 | } 428 | 429 | for lIdx := range layerStream { 430 | layerStreamType := layerStream[lIdx] 431 | 432 | for y := 0; y < int(ds1.Height); y++ { 433 | for x := 0; x < int(ds1.Width); x++ { 434 | dw, err := br.ReadUInt32() //nolint:govet // i want to re-use the err variable... 435 | if err != nil { 436 | return err 437 | } 438 | 439 | switch layerStreamType { 440 | case d2enum.LayerStreamWall1, d2enum.LayerStreamWall2, d2enum.LayerStreamWall3, d2enum.LayerStreamWall4: 441 | wallIndex := int(layerStreamType) - int(d2enum.LayerStreamWall1) 442 | ds1.Tiles[y][x].Walls[wallIndex].Prop1 = byte(dw & 0x000000FF) //nolint:gomnd // Bitmask 443 | ds1.Tiles[y][x].Walls[wallIndex].Sequence = byte((dw & 0x00003F00) >> 8) //nolint:gomnd // Bitmask 444 | ds1.Tiles[y][x].Walls[wallIndex].Unknown1 = byte((dw & 0x000FC000) >> 14) //nolint:gomnd // Bitmask 445 | ds1.Tiles[y][x].Walls[wallIndex].Style = byte((dw & 0x03F00000) >> 20) //nolint:gomnd // Bitmask 446 | ds1.Tiles[y][x].Walls[wallIndex].Unknown2 = byte((dw & 0x7C000000) >> 26) //nolint:gomnd // Bitmask 447 | ds1.Tiles[y][x].Walls[wallIndex].Hidden = byte((dw&0x80000000)>>31) > 0 //nolint:gomnd // Bitmask 448 | case d2enum.LayerStreamOrientation1, d2enum.LayerStreamOrientation2, 449 | d2enum.LayerStreamOrientation3, d2enum.LayerStreamOrientation4: 450 | wallIndex := int(layerStreamType) - int(d2enum.LayerStreamOrientation1) 451 | c := int32(dw & 0x000000FF) //nolint:gomnd // Bitmask 452 | 453 | if ds1.Version < v7 { 454 | if c < int32(len(dirLookup)) { 455 | c = dirLookup[c] 456 | } 457 | } 458 | 459 | ds1.Tiles[y][x].Walls[wallIndex].Type = d2enum.TileType(c) 460 | ds1.Tiles[y][x].Walls[wallIndex].Zero = byte((dw & 0xFFFFFF00) >> 8) //nolint:gomnd // Bitmask 461 | case d2enum.LayerStreamFloor1, d2enum.LayerStreamFloor2: 462 | floorIndex := int(layerStreamType) - int(d2enum.LayerStreamFloor1) 463 | ds1.Tiles[y][x].Floors[floorIndex].Prop1 = byte(dw & 0x000000FF) //nolint:gomnd // Bitmask 464 | ds1.Tiles[y][x].Floors[floorIndex].Sequence = byte((dw & 0x00003F00) >> 8) //nolint:gomnd // Bitmask 465 | ds1.Tiles[y][x].Floors[floorIndex].Unknown1 = byte((dw & 0x000FC000) >> 14) //nolint:gomnd // Bitmask 466 | ds1.Tiles[y][x].Floors[floorIndex].Style = byte((dw & 0x03F00000) >> 20) //nolint:gomnd // Bitmask 467 | ds1.Tiles[y][x].Floors[floorIndex].Unknown2 = byte((dw & 0x7C000000) >> 26) //nolint:gomnd // Bitmask 468 | ds1.Tiles[y][x].Floors[floorIndex].Hidden = byte((dw&0x80000000)>>31) > 0 //nolint:gomnd // Bitmask 469 | case d2enum.LayerStreamShadow: 470 | ds1.Tiles[y][x].Shadows[0].Prop1 = byte(dw & 0x000000FF) //nolint:gomnd // Bitmask 471 | ds1.Tiles[y][x].Shadows[0].Sequence = byte((dw & 0x00003F00) >> 8) //nolint:gomnd // Bitmask 472 | ds1.Tiles[y][x].Shadows[0].Unknown1 = byte((dw & 0x000FC000) >> 14) //nolint:gomnd // Bitmask 473 | ds1.Tiles[y][x].Shadows[0].Style = byte((dw & 0x03F00000) >> 20) //nolint:gomnd // Bitmask 474 | ds1.Tiles[y][x].Shadows[0].Unknown2 = byte((dw & 0x7C000000) >> 26) //nolint:gomnd // Bitmask 475 | ds1.Tiles[y][x].Shadows[0].Hidden = byte((dw&0x80000000)>>31) > 0 //nolint:gomnd // Bitmask 476 | case d2enum.LayerStreamSubstitute: 477 | ds1.Tiles[y][x].Substitutions[0].Unknown = dw 478 | } 479 | } 480 | } 481 | } 482 | 483 | return err 484 | } 485 | -------------------------------------------------------------------------------- /pkg/fileformats/dccfile/dcc_direction.go: -------------------------------------------------------------------------------- 1 | package d2dcc 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" 7 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom" 8 | 9 | "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" 10 | ) 11 | 12 | const cellsPerRow = 4 13 | 14 | // DCCDirection represents a DCCDirection file. 15 | type DCCDirection struct { 16 | OutSizeCoded int 17 | CompressionFlags int 18 | Variable0Bits int 19 | WidthBits int 20 | HeightBits int 21 | XOffsetBits int 22 | YOffsetBits int 23 | OptionalDataBits int 24 | CodedBytesBits int 25 | EqualCellsBitstreamSize int 26 | PixelMaskBitstreamSize int 27 | EncodingTypeBitsreamSize int 28 | RawPixelCodesBitstreamSize int 29 | Frames []*DCCDirectionFrame 30 | PaletteEntries [256]byte 31 | Box d2geom.Rectangle 32 | Cells []*DCCCell 33 | PixelData []byte 34 | HorizontalCellCount int 35 | VerticalCellCount int 36 | PixelBuffer []DCCPixelBufferEntry 37 | } 38 | 39 | // CreateDCCDirection creates an instance of a DCCDirection. 40 | func CreateDCCDirection(bm *d2datautils.BitMuncher, file *DCC) *DCCDirection { 41 | var crazyBitTable = []byte{0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 26, 28, 30, 32} 42 | 43 | result := &DCCDirection{ 44 | OutSizeCoded: int(bm.GetUInt32()), 45 | CompressionFlags: int(bm.GetBits(2)), //nolint:gomnd // binary data 46 | Variable0Bits: int(crazyBitTable[bm.GetBits(4)]), //nolint:gomnd // binary data 47 | WidthBits: int(crazyBitTable[bm.GetBits(4)]), //nolint:gomnd // binary data 48 | HeightBits: int(crazyBitTable[bm.GetBits(4)]), //nolint:gomnd // binary data 49 | XOffsetBits: int(crazyBitTable[bm.GetBits(4)]), //nolint:gomnd // binary data 50 | YOffsetBits: int(crazyBitTable[bm.GetBits(4)]), //nolint:gomnd // binary data 51 | OptionalDataBits: int(crazyBitTable[bm.GetBits(4)]), //nolint:gomnd // binary data 52 | CodedBytesBits: int(crazyBitTable[bm.GetBits(4)]), //nolint:gomnd // binary data 53 | Frames: make([]*DCCDirectionFrame, file.FramesPerDirection), 54 | } 55 | 56 | minx := 100000 57 | miny := 100000 58 | maxx := -100000 59 | maxy := -100000 60 | 61 | // Load the frame headers 62 | for frameIdx := 0; frameIdx < file.FramesPerDirection; frameIdx++ { 63 | result.Frames[frameIdx] = CreateDCCDirectionFrame(bm, result) 64 | minx = int(d2math.MinInt32(int32(result.Frames[frameIdx].Box.Left), int32(minx))) 65 | miny = int(d2math.MinInt32(int32(result.Frames[frameIdx].Box.Top), int32(miny))) 66 | maxx = int(d2math.MaxInt32(int32(result.Frames[frameIdx].Box.Right()), int32(maxx))) 67 | maxy = int(d2math.MaxInt32(int32(result.Frames[frameIdx].Box.Bottom()), int32(maxy))) 68 | } 69 | 70 | result.Box = d2geom.Rectangle{Left: minx, Top: miny, Width: maxx - minx, Height: maxy - miny} 71 | 72 | if result.OptionalDataBits > 0 { 73 | log.Panic("Optional bits in DCC data is not currently supported.") 74 | } 75 | 76 | if (result.CompressionFlags & 0x2) > 0 { 77 | result.EqualCellsBitstreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data 78 | } 79 | 80 | result.PixelMaskBitstreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data 81 | 82 | if (result.CompressionFlags & 0x1) > 0 { 83 | result.EncodingTypeBitsreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data 84 | result.RawPixelCodesBitstreamSize = int(bm.GetBits(20)) //nolint:gomnd // binary data 85 | } 86 | 87 | for paletteEntryCount, i := 0, 0; i < 256; i++ { 88 | valid := bm.GetBit() != 0 89 | if valid { 90 | result.PaletteEntries[paletteEntryCount] = byte(i) 91 | paletteEntryCount++ 92 | } 93 | } 94 | 95 | // HERE BE GIANTS: 96 | // Because of the way this thing mashes bits together, BIT offset matters 97 | // here. For example, if you are on byte offset 3, bit offset 6, and 98 | // the EqualCellsBitstreamSize is 20 bytes, then the next bit stream 99 | // will be located at byte 23, bit offset 6! 100 | equalCellsBitstream := d2datautils.CopyBitMuncher(bm) 101 | 102 | bm.SkipBits(result.EqualCellsBitstreamSize) 103 | 104 | pixelMaskBitstream := d2datautils.CopyBitMuncher(bm) 105 | 106 | bm.SkipBits(result.PixelMaskBitstreamSize) 107 | 108 | encodingTypeBitsream := d2datautils.CopyBitMuncher(bm) 109 | 110 | bm.SkipBits(result.EncodingTypeBitsreamSize) 111 | 112 | rawPixelCodesBitstream := d2datautils.CopyBitMuncher(bm) 113 | 114 | bm.SkipBits(result.RawPixelCodesBitstreamSize) 115 | 116 | pixelCodeandDisplacement := d2datautils.CopyBitMuncher(bm) 117 | 118 | // Calculate the cells for the direction 119 | result.calculateCells() 120 | 121 | // Calculate the cells for each of the frames 122 | for _, frame := range result.Frames { 123 | frame.recalculateCells(result) 124 | } 125 | 126 | // Fill in the pixel buffer 127 | result.fillPixelBuffer(pixelCodeandDisplacement, equalCellsBitstream, pixelMaskBitstream, encodingTypeBitsream, rawPixelCodesBitstream) 128 | 129 | // Generate the actual frame pixel data 130 | result.generateFrames(pixelCodeandDisplacement) 131 | 132 | result.PixelBuffer = nil 133 | 134 | // Verify that everything we expected to read was actually read (sanity check)... 135 | result.verify(equalCellsBitstream, pixelMaskBitstream, encodingTypeBitsream, rawPixelCodesBitstream) 136 | 137 | bm.SkipBits(pixelCodeandDisplacement.BitsRead()) 138 | 139 | return result 140 | } 141 | 142 | func (v *DCCDirection) verify( 143 | equalCellsBitstream, 144 | pixelMaskBitstream, 145 | encodingTypeBitstream, 146 | rawPixelCodesBitstream *d2datautils.BitMuncher, 147 | ) { 148 | if equalCellsBitstream.BitsRead() != v.EqualCellsBitstreamSize { 149 | log.Panic("Did not read the correct number of bits!") 150 | } 151 | 152 | if pixelMaskBitstream.BitsRead() != v.PixelMaskBitstreamSize { 153 | log.Panic("Did not read the correct number of bits!") 154 | } 155 | 156 | if encodingTypeBitstream.BitsRead() != v.EncodingTypeBitsreamSize { 157 | log.Panic("Did not read the correct number of bits!") 158 | } 159 | 160 | if rawPixelCodesBitstream.BitsRead() != v.RawPixelCodesBitstreamSize { 161 | log.Panic("Did not read the correct number of bits!") 162 | } 163 | } 164 | 165 | // nolint:gocognit,gocyclo // Can't reduce 166 | func (v *DCCDirection) generateFrames(pcd *d2datautils.BitMuncher) { 167 | pbIdx := 0 168 | 169 | for _, cell := range v.Cells { 170 | cell.LastWidth = -1 171 | cell.LastHeight = -1 172 | } 173 | 174 | v.PixelData = make([]byte, v.Box.Width*v.Box.Height) 175 | frameIndex := -1 176 | 177 | for _, frame := range v.Frames { 178 | frameIndex++ 179 | 180 | frame.PixelData = make([]byte, v.Box.Width*v.Box.Height) 181 | c := -1 182 | 183 | for _, cell := range frame.Cells { 184 | c++ 185 | 186 | cellX := cell.XOffset / cellsPerRow 187 | cellY := cell.YOffset / cellsPerRow 188 | cellIndex := cellX + (cellY * v.HorizontalCellCount) 189 | bufferCell := v.Cells[cellIndex] 190 | pbe := v.PixelBuffer[pbIdx] 191 | 192 | if (pbe.Frame != frameIndex) || (pbe.FrameCellIndex != c) { 193 | // This buffer cell has an EqualCell bit set to 1, so copy the frame cell or clear it 194 | if (cell.Width != bufferCell.LastWidth) || (cell.Height != bufferCell.LastHeight) { 195 | // Different sizes 196 | for y := 0; y < cell.Height; y++ { 197 | for x := 0; x < cell.Width; x++ { 198 | v.PixelData[x+cell.XOffset+((y+cell.YOffset)*v.Box.Width)] = 0 199 | } 200 | } 201 | } else { 202 | // Same sizes 203 | // Copy the old frame cell into the new position 204 | for fy := 0; fy < cell.Height; fy++ { 205 | for fx := 0; fx < cell.Width; fx++ { 206 | // Frame (buff.lastx, buff.lasty) -> Frame (cell.offx, cell.offy) 207 | // Cell (0, 0,) -> 208 | // blit(dir->bmp, dir->bmp, buff_cell->last_x0, buff_cell->last_y0, cell->x0, cell->y0, cell->w, cell->h ); 209 | v.PixelData[fx+cell.XOffset+((fy+cell.YOffset)*v.Box.Width)] = 210 | v.PixelData[fx+bufferCell.LastXOffset+((fy+bufferCell.LastYOffset)*v.Box.Width)] 211 | } 212 | } 213 | // Copy it again into the final frame image 214 | for fy := 0; fy < cell.Height; fy++ { 215 | for fx := 0; fx < cell.Width; fx++ { 216 | // blit(cell->bmp, frm_bmp, 0, 0, cell->x0, cell->y0, cell->w, cell->h ); 217 | frame.PixelData[fx+cell.XOffset+((fy+cell.YOffset)*v.Box.Width)] = v.PixelData[cell.XOffset+fx+((cell.YOffset+fy)*v.Box.Width)] 218 | } 219 | } 220 | } 221 | } else { 222 | if pbe.Value[0] == pbe.Value[1] { 223 | // Clear the frame 224 | for y := 0; y < cell.Height; y++ { 225 | for x := 0; x < cell.Width; x++ { 226 | v.PixelData[x+cell.XOffset+((y+cell.YOffset)*v.Box.Width)] = pbe.Value[0] 227 | } 228 | } 229 | } else { 230 | // Fill the frame cell with the pixels 231 | bitsToRead := 1 232 | if pbe.Value[1] != pbe.Value[2] { 233 | bitsToRead = 2 234 | } 235 | for y := 0; y < cell.Height; y++ { 236 | for x := 0; x < cell.Width; x++ { 237 | paletteIndex := pcd.GetBits(bitsToRead) 238 | v.PixelData[x+cell.XOffset+((y+cell.YOffset)*v.Box.Width)] = pbe.Value[paletteIndex] 239 | } 240 | } 241 | } 242 | 243 | // Copy the frame cell into the frame 244 | for fy := 0; fy < cell.Height; fy++ { 245 | for fx := 0; fx < cell.Width; fx++ { 246 | // blit(cell->bmp, frm_bmp, 0, 0, cell->x0, cell->y0, cell->w, cell->h ); 247 | frame.PixelData[fx+cell.XOffset+((fy+cell.YOffset)*v.Box.Width)] = v.PixelData[fx+cell.XOffset+((fy+cell.YOffset)*v.Box.Width)] 248 | } 249 | } 250 | pbIdx++ 251 | } 252 | 253 | bufferCell.LastWidth = cell.Width 254 | bufferCell.LastHeight = cell.Height 255 | bufferCell.LastXOffset = cell.XOffset 256 | bufferCell.LastYOffset = cell.YOffset 257 | } 258 | 259 | // Free up the stuff we no longer need 260 | frame.Cells = nil 261 | } 262 | 263 | v.Cells = nil 264 | v.PixelData = nil 265 | v.PixelBuffer = nil 266 | } 267 | 268 | //nolint:funlen,gocognit,gocyclo // can't reduce 269 | func (v *DCCDirection) fillPixelBuffer(pcd, ec, pm, et, rp *d2datautils.BitMuncher) { 270 | var pixelMaskLookup = []int{0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4} 271 | 272 | lastPixel := uint32(0) 273 | maxCellX := 0 274 | maxCellY := 0 275 | 276 | for _, frame := range v.Frames { 277 | if frame == nil { 278 | continue 279 | } 280 | 281 | maxCellX += frame.HorizontalCellCount 282 | maxCellY += frame.VerticalCellCount 283 | } 284 | 285 | v.PixelBuffer = make([]DCCPixelBufferEntry, maxCellX*maxCellY) 286 | 287 | for i := 0; i < maxCellX*maxCellY; i++ { 288 | v.PixelBuffer[i].Frame = -1 289 | v.PixelBuffer[i].FrameCellIndex = -1 290 | } 291 | 292 | cellBuffer := make([]*DCCPixelBufferEntry, v.HorizontalCellCount*v.VerticalCellCount) 293 | frameIndex := -1 294 | pbIndex := -1 295 | 296 | var pixelMask uint32 297 | 298 | for _, frame := range v.Frames { 299 | frameIndex++ 300 | 301 | originCellX := (frame.Box.Left - v.Box.Left) / cellsPerRow 302 | originCellY := (frame.Box.Top - v.Box.Top) / cellsPerRow 303 | 304 | for cellY := 0; cellY < frame.VerticalCellCount; cellY++ { 305 | currentCellY := cellY + originCellY 306 | 307 | for cellX := 0; cellX < frame.HorizontalCellCount; cellX++ { 308 | currentCell := originCellX + cellX + (currentCellY * v.HorizontalCellCount) 309 | nextCell := false 310 | tmp := 0 311 | 312 | if cellBuffer[currentCell] != nil { 313 | if v.EqualCellsBitstreamSize > 0 { 314 | tmp = int(ec.GetBit()) 315 | } else { 316 | tmp = 0 317 | } 318 | 319 | if tmp == 0 { 320 | pixelMask = pm.GetBits(4) //nolint:gomnd // binary data 321 | } else { 322 | nextCell = true 323 | } 324 | } else { 325 | pixelMask = 0x0F 326 | } 327 | 328 | if nextCell { 329 | continue 330 | } 331 | 332 | // Decode the pixels 333 | var pixelStack [4]uint32 334 | 335 | lastPixel = 0 336 | numberOfPixelBits := pixelMaskLookup[pixelMask] 337 | encodingType := 0 338 | 339 | if (numberOfPixelBits != 0) && (v.EncodingTypeBitsreamSize > 0) { 340 | encodingType = int(et.GetBit()) 341 | } else { 342 | encodingType = 0 343 | } 344 | 345 | decodedPixel := 0 346 | 347 | for i := 0; i < numberOfPixelBits; i++ { 348 | if encodingType != 0 { 349 | pixelStack[i] = rp.GetBits(8) //nolint:gomnd // binary data 350 | } else { 351 | pixelStack[i] = lastPixel 352 | pixelDisplacement := pcd.GetBits(4) //nolint:gomnd // binary data 353 | pixelStack[i] += pixelDisplacement 354 | for pixelDisplacement == 15 { 355 | pixelDisplacement = pcd.GetBits(4) //nolint:gomnd // binary data 356 | pixelStack[i] += pixelDisplacement 357 | } 358 | } 359 | 360 | if pixelStack[i] == lastPixel { 361 | pixelStack[i] = 0 362 | break 363 | } else { 364 | lastPixel = pixelStack[i] 365 | decodedPixel++ 366 | } 367 | } 368 | 369 | oldEntry := cellBuffer[currentCell] 370 | 371 | pbIndex++ 372 | 373 | curIdx := decodedPixel - 1 374 | 375 | for i := 0; i < 4; i++ { 376 | if (pixelMask & (1 << uint(i))) != 0 { 377 | if curIdx >= 0 { 378 | v.PixelBuffer[pbIndex].Value[i] = byte(pixelStack[curIdx]) 379 | curIdx-- 380 | } else { 381 | v.PixelBuffer[pbIndex].Value[i] = 0 382 | } 383 | } else { 384 | v.PixelBuffer[pbIndex].Value[i] = oldEntry.Value[i] 385 | } 386 | } 387 | 388 | cellBuffer[currentCell] = &v.PixelBuffer[pbIndex] 389 | v.PixelBuffer[pbIndex].Frame = frameIndex 390 | v.PixelBuffer[pbIndex].FrameCellIndex = cellX + (cellY * frame.HorizontalCellCount) 391 | } 392 | } 393 | } 394 | 395 | // Convert the palette entry index into actual palette entries 396 | for i := 0; i <= pbIndex; i++ { 397 | for x := 0; x < 4; x++ { 398 | v.PixelBuffer[i].Value[x] = v.PaletteEntries[v.PixelBuffer[i].Value[x]] 399 | } 400 | } 401 | } 402 | 403 | func (v *DCCDirection) calculateCells() { 404 | // Calculate the number of vertical and horizontal cells we need 405 | v.HorizontalCellCount = 1 + (v.Box.Width-1)/cellsPerRow 406 | v.VerticalCellCount = 1 + (v.Box.Height-1)/cellsPerRow 407 | // Calculate the cell widths 408 | cellWidths := make([]int, v.HorizontalCellCount) 409 | if v.HorizontalCellCount == 1 { 410 | cellWidths[0] = v.Box.Width 411 | } else { 412 | for i := 0; i < v.HorizontalCellCount-1; i++ { 413 | cellWidths[i] = 4 414 | } 415 | cellWidths[v.HorizontalCellCount-1] = v.Box.Width - (4 * (v.HorizontalCellCount - 1)) 416 | } 417 | // Calculate the cell heights 418 | cellHeights := make([]int, v.VerticalCellCount) 419 | if v.VerticalCellCount == 1 { 420 | cellHeights[0] = v.Box.Height 421 | } else { 422 | for i := 0; i < v.VerticalCellCount-1; i++ { 423 | cellHeights[i] = 4 424 | } 425 | cellHeights[v.VerticalCellCount-1] = v.Box.Height - (4 * (v.VerticalCellCount - 1)) 426 | } 427 | // Set the cell widths and heights in the cell buffer 428 | v.Cells = make([]*DCCCell, v.VerticalCellCount*v.HorizontalCellCount) 429 | yOffset := 0 430 | 431 | for y := 0; y < v.VerticalCellCount; y++ { 432 | xOffset := 0 433 | 434 | for x := 0; x < v.HorizontalCellCount; x++ { 435 | v.Cells[x+(y*v.HorizontalCellCount)] = &DCCCell{ 436 | Width: cellWidths[x], 437 | Height: cellHeights[y], 438 | XOffset: xOffset, 439 | YOffset: yOffset, 440 | } 441 | 442 | xOffset += 4 443 | } 444 | 445 | yOffset += 4 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /internal/engine/resource/resourcepaths.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | // Paths of the resources inside the mpq files. 4 | const ( 5 | // --- Language 6 | 7 | LocalLanguage = "/data/local/use" 8 | LanguageFontToken = "{LANG_FONT}" //nolint:gosec // this is just a format string 9 | LanguageTableToken = "{LANG}" 10 | 11 | // --- Screens --- 12 | 13 | LoadingScreen = "/data/global/ui/Loading/loadingscreen.dc6" 14 | 15 | // --- Main Menu --- 16 | 17 | TrademarkScreen = "/data/global/ui/FrontEnd/trademarkscreenEXP.dc6" 18 | GameSelectScreen = "/data/global/ui/FrontEnd/gameselectscreenEXP.dc6" 19 | TCPIPBackground = "/data/global/ui/FrontEnd/TCPIPscreen.dc6" 20 | Diablo2LogoFireLeft = "/data/global/ui/FrontEnd/D2logoFireLeft.DC6" 21 | Diablo2LogoFireRight = "/data/global/ui/FrontEnd/D2logoFireRight.DC6" 22 | Diablo2LogoBlackLeft = "/data/global/ui/FrontEnd/D2logoBlackLeft.DC6" 23 | Diablo2LogoBlackRight = "/data/global/ui/FrontEnd/D2logoBlackRight.DC6" 24 | 25 | // --- Credits --- 26 | 27 | CreditsBackground = "/data/global/ui/CharSelect/creditsbckgexpand.dc6" 28 | CreditsText = "/data/local/ui/" + LanguageTableToken + "/ExpansionCredits.txt" 29 | 30 | // --- Cinematics --- 31 | 32 | CinematicsBackground = "/data/global/ui/FrontEnd/CinematicsSelectionEXP.dc6" 33 | 34 | // --- Video Paths --- 35 | 36 | Act1Intro = "/data/local/video/" + LanguageTableToken + "/d2intro640x292.bik" 37 | Act2Intro = "/data/local/video/" + LanguageTableToken + "/act02start640x292.bik" 38 | Act3Intro = "/data/local/video/" + LanguageTableToken + "/act03start640x292.bik" 39 | Act4Intro = "/data/local/video/" + LanguageTableToken + "/act04start640x292.bik" 40 | Act4Outro = "/data/local/video/" + LanguageTableToken + "/act04end640x292.bik" 41 | Act5Intro = "/data/local/video/" + LanguageTableToken + "/d2x_intro_640x292.bik" 42 | Act5Outro = "/data/local/video/" + LanguageTableToken + "/d2x_out_640x292.bik" 43 | 44 | // --- Character Select Screen --- 45 | 46 | CharacterSelectBackground = "/data/global/ui/FrontEnd/charactercreationscreenEXP.dc6" 47 | CharacterSelectCampfire = "/data/global/ui/FrontEnd/fire.DC6" 48 | 49 | CharacterSelectBarbarianUnselected = "/data/global/ui/FrontEnd/barbarian/banu1.DC6" 50 | CharacterSelectBarbarianUnselectedH = "/data/global/ui/FrontEnd/barbarian/banu2.DC6" 51 | CharacterSelectBarbarianSelected = "/data/global/ui/FrontEnd/barbarian/banu3.DC6" 52 | CharacterSelectBarbarianForwardWalk = "/data/global/ui/FrontEnd/barbarian/bafw.DC6" 53 | CharacterSelectBarbarianForwardWalkOverlay = "/data/global/ui/FrontEnd/barbarian/BAFWs.DC6" 54 | CharacterSelectBarbarianBackWalk = "/data/global/ui/FrontEnd/barbarian/babw.DC6" 55 | 56 | CharacterSelectSorceressUnselected = "/data/global/ui/FrontEnd/sorceress/SONU1.DC6" 57 | CharacterSelectSorceressUnselectedH = "/data/global/ui/FrontEnd/sorceress/SONU2.DC6" 58 | CharacterSelectSorceressSelected = "/data/global/ui/FrontEnd/sorceress/SONU3.DC6" 59 | CharacterSelectSorceressSelectedOverlay = "/data/global/ui/FrontEnd/sorceress/SONU3s.DC6" 60 | CharacterSelectSorceressForwardWalk = "/data/global/ui/FrontEnd/sorceress/SOFW.DC6" 61 | CharacterSelectSorceressForwardWalkOverlay = "/data/global/ui/FrontEnd/sorceress/SOFWs.DC6" 62 | CharacterSelectSorceressBackWalk = "/data/global/ui/FrontEnd/sorceress/SOBW.DC6" 63 | CharacterSelectSorceressBackWalkOverlay = "/data/global/ui/FrontEnd/sorceress/SOBWs.DC6" 64 | 65 | CharacterSelectNecromancerUnselected = "/data/global/ui/FrontEnd/necromancer/NENU1.DC6" 66 | CharacterSelectNecromancerUnselectedH = "/data/global/ui/FrontEnd/necromancer/NENU2.DC6" 67 | CharacterSelectNecromancerSelected = "/data/global/ui/FrontEnd/necromancer/NENU3.DC6" 68 | CharacterSelectNecromancerSelectedOverlay = "/data/global/ui/FrontEnd/necromancer/NENU3s.DC6" 69 | CharacterSelectNecromancerForwardWalk = "/data/global/ui/FrontEnd/necromancer/NEFW.DC6" 70 | CharacterSelectNecromancerForwardWalkOverlay = "/data/global/ui/FrontEnd/necromancer/NEFWs.DC6" 71 | CharacterSelectNecromancerBackWalk = "/data/global/ui/FrontEnd/necromancer/NEBW.DC6" 72 | CharacterSelectNecromancerBackWalkOverlay = "/data/global/ui/FrontEnd/necromancer/NEBWs.DC6" 73 | 74 | CharacterSelectPaladinUnselected = "/data/global/ui/FrontEnd/paladin/PANU1.DC6" 75 | CharacterSelectPaladinUnselectedH = "/data/global/ui/FrontEnd/paladin/PANU2.DC6" 76 | CharacterSelectPaladinSelected = "/data/global/ui/FrontEnd/paladin/PANU3.DC6" 77 | CharacterSelectPaladinForwardWalk = "/data/global/ui/FrontEnd/paladin/PAFW.DC6" 78 | CharacterSelectPaladinForwardWalkOverlay = "/data/global/ui/FrontEnd/paladin/PAFWs.DC6" 79 | CharacterSelectPaladinBackWalk = "/data/global/ui/FrontEnd/paladin/PABW.DC6" 80 | 81 | CharacterSelectAmazonUnselected = "/data/global/ui/FrontEnd/amazon/AMNU1.DC6" 82 | CharacterSelectAmazonUnselectedH = "/data/global/ui/FrontEnd/amazon/AMNU2.DC6" 83 | CharacterSelectAmazonSelected = "/data/global/ui/FrontEnd/amazon/AMNU3.DC6" 84 | CharacterSelectAmazonForwardWalk = "/data/global/ui/FrontEnd/amazon/AMFW.DC6" 85 | CharacterSelectAmazonForwardWalkOverlay = "/data/global/ui/FrontEnd/amazon/AMFWs.DC6" 86 | CharacterSelectAmazonBackWalk = "/data/global/ui/FrontEnd/amazon/AMBW.DC6" 87 | 88 | CharacterSelectAssassinUnselected = "/data/global/ui/FrontEnd/assassin/ASNU1.DC6" 89 | CharacterSelectAssassinUnselectedH = "/data/global/ui/FrontEnd/assassin/ASNU2.DC6" 90 | CharacterSelectAssassinSelected = "/data/global/ui/FrontEnd/assassin/ASNU3.DC6" 91 | CharacterSelectAssassinForwardWalk = "/data/global/ui/FrontEnd/assassin/ASFW.DC6" 92 | CharacterSelectAssassinBackWalk = "/data/global/ui/FrontEnd/assassin/ASBW.DC6" 93 | 94 | CharacterSelectDruidUnselected = "/data/global/ui/FrontEnd/druid/DZNU1.dc6" 95 | CharacterSelectDruidUnselectedH = "/data/global/ui/FrontEnd/druid/DZNU2.dc6" 96 | CharacterSelectDruidSelected = "/data/global/ui/FrontEnd/druid/DZNU3.DC6" 97 | CharacterSelectDruidForwardWalk = "/data/global/ui/FrontEnd/druid/DZFW.DC6" 98 | CharacterSelectDruidBackWalk = "/data/global/ui/FrontEnd/druid/DZBW.DC6" 99 | 100 | // -- Character Selection 101 | 102 | CharacterSelectionBackground = "/data/global/ui/CharSelect/characterselectscreenEXP.dc6" 103 | CharacterSelectionSelectBox = "/data/global/ui/CharSelect/charselectbox.dc6" 104 | PopUpOkCancel = "/data/global/ui/FrontEnd/PopUpOKCancel.dc6" 105 | 106 | // --- Game --- 107 | 108 | GamePanels = "/data/global/ui/PANEL/800ctrlpnl7.dc6" 109 | GameGlobeOverlap = "/data/global/ui/PANEL/overlap.DC6" 110 | HealthManaIndicator = "/data/global/ui/PANEL/hlthmana.DC6" 111 | AddSkillButton = "/data/global/ui/PANEL/level.DC6" 112 | MoveGoldDialog = "/data/global/ui/menu/dialogbackground.DC6" 113 | WPTabs = "/data/global/ui/menu/expwaygatetabs.dc6" 114 | WPBg = "/data/global/ui/menu/waygatebackground.dc6" 115 | WPIcons = "/data/global/ui/menu/waygateicons.dc6" 116 | UpDownArrows = "/data/global/ui/BIGMENU/numberarrows.dc6" 117 | 118 | // --- Escape Menu --- 119 | // main 120 | EscapeOptions = "/data/local/ui/" + LanguageTableToken + "/options.dc6" 121 | EscapeExit = "/data/local/ui/" + LanguageTableToken + "/exit.dc6" 122 | EscapeReturnToGame = "/data/local/ui/" + LanguageTableToken + "/returntogame.dc6" 123 | // options 124 | EscapeOptSoundOptions = "/data/local/ui/" + LanguageTableToken + "/soundoptions.dc6" 125 | EscapeOptVideoOptions = "/data/local/ui/" + LanguageTableToken + "/videoOptions.dc6" 126 | EscapeOptAutoMapOptions = "/data/local/ui/" + LanguageTableToken + "/automapOptions.dc6" 127 | EscapeOptCfgOptions = "/data/local/ui/" + LanguageTableToken + "/cfgOptions.dc6" 128 | EscapeOptPrevious = "/data/local/ui/" + LanguageTableToken + "/previous.dc6" 129 | 130 | // sound options 131 | EscapeSndOptSoundVolume = "/data/local/ui/" + LanguageTableToken + "/sound.dc6" 132 | EscapeSndOptMusicVolume = "/data/local/ui/" + LanguageTableToken + "/music.dc6" 133 | EscapeSndOpt3DBias = "/data/local/ui/" + LanguageTableToken + "/3dbias.dc6" 134 | // EscapeSndOptHWAcceleration = 135 | // EscapeSndOptENVEffects = 136 | EscapeSndOptNPCSpeech = "/data/local/ui/" + LanguageTableToken + "/npcspeech.dc6" 137 | EscapeSndOptNPCSpeechAudioAndText = "/data/local/ui/" + LanguageTableToken + "/audiotext.dc6" 138 | EscapeSndOptNPCSpeechAudioOnly = "/data/local/ui/" + LanguageTableToken + "/audioonly.dc6" 139 | EscapeSndOptNPCSpeechTextOnly = "/data/local/ui/" + LanguageTableToken + "/textonly.dc6" 140 | 141 | // video options 142 | EscapeVidOptRes = "/data/local/ui/" + LanguageTableToken + "/resolution.dc6" 143 | EscapeVidOptLightQuality = "/data/local/ui/" + LanguageTableToken + "/lightquality.dc6" 144 | EscapeVidOptBlendShadow = "/data/local/ui/" + LanguageTableToken + "/blendshadow.dc6" 145 | EscapeVidOptPerspective = "/data/local/ui/" + LanguageTableToken + "/prespective.dc6" 146 | EscapeVidOptGamma = "/data/local/ui/" + LanguageTableToken + "/gamma.dc6" 147 | EscapeVidOptContrast = "/data/local/ui/" + LanguageTableToken + "/contrast.dc6" 148 | 149 | // auto map 150 | EscapeAutoMapOptSize = "/data/local/ui/" + LanguageTableToken + "/automapmode.dc6" 151 | EscapeAutoMapOptFade = "/data/local/ui/" + LanguageTableToken + "/automapfade.dc6" 152 | EscapeAutoMapOptCenter = "/data/local/ui/" + LanguageTableToken + "/automapcenter.dc6" 153 | EscapeAutoMapOptNames = "/data/local/ui/" + LanguageTableToken + "/automappartynames.dc6" 154 | 155 | // automap size 156 | EscapeAutoMapOptFullScreen = "/data/local/ui/" + LanguageTableToken + "/full.dc6" 157 | EscapeAutoMapOptMiniMap = "/data/local/ui/" + LanguageTableToken + "/mini.dc6" 158 | 159 | // resolutions 160 | EscapeVideoOptRes640x480 = "/data/local/ui/" + LanguageTableToken + "/640x480.dc6" 161 | EscapeVideoOptRes800x600 = "/data/local/ui/" + LanguageTableToken + "/800x800.dc6" 162 | 163 | EscapeOn = "/data/local/ui/" + LanguageTableToken + "/smallon.dc6" 164 | EscapeOff = "/data/local/ui/" + LanguageTableToken + "/smalloff.dc6" 165 | EscapeYes = "/data/local/ui/" + LanguageTableToken + "/smallyes.dc6" 166 | EscapeNo = "/data/local/ui/" + LanguageTableToken + "/smallno.dc6" 167 | EscapeSlideBar = "/data/global/ui/widgets/optbarc.dc6" 168 | EscapeSlideBarSkull = "/data/global/ui/widgets/optskull.dc6" 169 | 170 | // --- Help Overlay --- 171 | 172 | // HelpBorder = "/data/global/ui/MENU/helpborder.DC6" 173 | HelpBorder = "/data/global/ui/MENU/800helpborder.DC6" 174 | HelpYellowBullet = "/data/global/ui/MENU/helpyellowbullet.DC6" 175 | HelpWhiteBullet = "/data/global/ui/MENU/helpwhitebullet.DC6" 176 | 177 | // Box pieces, used in all in game boxes like npc interaction menu on click, 178 | // the chat window and the key binding menu 179 | BoxPieces = "/data/global/ui/MENU/boxpieces.DC6" 180 | 181 | // TextSlider contains the pieces to build a scrollbar in the 182 | // menus, such as the one in the configure keys menu 183 | TextSlider = "/data/global/ui/MENU/textslid.DC6" 184 | 185 | // Issue #685 - used in the mini-panel 186 | GameSmallMenuButton = "/data/global/ui/PANEL/menubutton.DC6" 187 | SkillIcon = "/data/global/ui/PANEL/Skillicon.DC6" 188 | 189 | // --- Quest Log--- 190 | QuestLogBg = "/data/global/ui/MENU/questbackground.dc6" 191 | QuestLogDone = "/data/global/ui/MENU/questdone.dc6" 192 | QuestLogTabs = "/data/global/ui/MENU/expquesttabs.dc6" 193 | QuestLogQDescrBtn = "/data/global/ui/MENU/questlast.dc6" 194 | QuestLogSocket = "/data/global/ui/MENU/questsockets.dc6" 195 | QuestLogAQuestAnimation = "/data/global/ui/MENU/a%dq%d.dc6" 196 | QuestLogDoneSfx = "cursor/questdone.wav" 197 | 198 | // --- Mouse Pointers --- 199 | 200 | CursorDefault = "/data/global/ui/CURSOR/ohand.DC6" 201 | 202 | // --- Fonts & Locale (strings) --- 203 | Font6 = "/data/local/FONT/" + LanguageFontToken + "/font6" 204 | Font8 = "/data/local/FONT/" + LanguageFontToken + "/font8" 205 | Font16 = "/data/local/FONT/" + LanguageFontToken + "/font16" 206 | Font24 = "/data/local/FONT/" + LanguageFontToken + "/font24" 207 | Font30 = "/data/local/FONT/" + LanguageFontToken + "/font30" 208 | Font42 = "/data/local/FONT/" + LanguageFontToken + "/font42" 209 | FontFormal12 = "/data/local/FONT/" + LanguageFontToken + "/fontformal12" 210 | FontFormal11 = "/data/local/FONT/" + LanguageFontToken + "/fontformal11" 211 | FontFormal10 = "/data/local/FONT/" + LanguageFontToken + "/fontformal10" 212 | FontExocet10 = "/data/local/FONT/" + LanguageFontToken + "/fontexocet10" 213 | FontExocet8 = "/data/local/FONT/" + LanguageFontToken + "/fontexocet8" 214 | FontSucker = "/data/local/FONT/" + LanguageFontToken + "/ReallyTheLastSucker" 215 | FontRediculous = "/data/local/FONT/" + LanguageFontToken + "/fontridiculous" 216 | ExpansionStringTable = "/data/local/lng/" + LanguageTableToken + "/expansionstring.tbl" 217 | StringTable = "/data/local/lng/" + LanguageTableToken + "/string.tbl" 218 | PatchStringTable = "/data/local/lng/" + LanguageTableToken + "/patchstring.tbl" 219 | 220 | // --- UI --- 221 | 222 | WideButtonBlank = "/data/global/ui/FrontEnd/WideButtonBlank.dc6" 223 | MediumButtonBlank = "/data/global/ui/FrontEnd/MediumButtonBlank.dc6" 224 | CancelButton = "/data/global/ui/FrontEnd/CancelButtonBlank.dc6" 225 | NarrowButtonBlank = "/data/global/ui/FrontEnd/NarrowButtonBlank.dc6" 226 | ShortButtonBlank = "/data/global/ui/CharSelect/ShortButtonBlank.dc6" 227 | TextBox2 = "/data/global/ui/FrontEnd/textbox2.dc6" 228 | TallButtonBlank = "/data/global/ui/CharSelect/TallButtonBlank.dc6" 229 | Checkbox = "/data/global/ui/FrontEnd/clickbox.dc6" 230 | Scrollbar = "/data/global/ui/PANEL/scrollbar.dc6" 231 | 232 | PopUpLarge = "/data/global/ui/FrontEnd/PopUpLarge.dc6" 233 | PopUpLargest = "/data/global/ui/FrontEnd/PopUpLargest.dc6" 234 | PopUpWide = "/data/global/ui/FrontEnd/PopUpWide.dc6" 235 | PopUpOk = "/data/global/ui/FrontEnd/PopUpOk.dc6" 236 | PopUpOk2 = "/data/global/ui/FrontEnd/PopUpOk.dc6" 237 | PopUpOkCancel2 = "/data/global/ui/FrontEnd/PopUpOkCancel2.dc6" 238 | PopUp340x224 = "/data/global/ui/FrontEnd/PopUp_340x224.dc6" 239 | 240 | // --- GAME UI --- 241 | 242 | PentSpin = "/data/global/ui/CURSOR/pentspin.DC6" 243 | Minipanel = "/data/global/ui/PANEL/minipanel.DC6" 244 | MinipanelSmall = "/data/global/ui/PANEL/minipanel_s.dc6" 245 | MinipanelButton = "/data/global/ui/PANEL/minipanelbtn.DC6" 246 | 247 | Frame = "/data/global/ui/PANEL/800borderframe.dc6" 248 | InventoryCharacterPanel = "/data/global/ui/PANEL/invchar6.DC6" 249 | HeroStatsPanelStatsPoints = "/data/global/ui/PANEL/skillpoints.dc6" 250 | HeroStatsPanelSocket = "/data/global/ui/PANEL/levelsocket.dc6" 251 | InventoryWeaponsTab = "/data/global/ui/PANEL/invchar6Tab.DC6" 252 | SkillsPanelAmazon = "/data/global/ui/SPELLS/skltree_a_back.DC6" 253 | SkillsPanelBarbarian = "/data/global/ui/SPELLS/skltree_b_back.DC6" 254 | SkillsPanelDruid = "/data/global/ui/SPELLS/skltree_d_back.DC6" 255 | SkillsPanelAssassin = "/data/global/ui/SPELLS/skltree_i_back.DC6" 256 | SkillsPanelNecromancer = "/data/global/ui/SPELLS/skltree_n_back.DC6" 257 | SkillsPanelPaladin = "/data/global/ui/SPELLS/skltree_p_back.DC6" 258 | SkillsPanelSorcerer = "/data/global/ui/SPELLS/skltree_s_back.DC6" 259 | 260 | GenericSkills = "/data/global/ui/SPELLS/Skillicon.DC6" 261 | AmazonSkills = "/data/global/ui/SPELLS/AmSkillicon.DC6" 262 | BarbarianSkills = "/data/global/ui/SPELLS/BaSkillicon.DC6" 263 | DruidSkills = "/data/global/ui/SPELLS/DrSkillicon.DC6" 264 | AssassinSkills = "/data/global/ui/SPELLS/AsSkillicon.DC6" 265 | NecromancerSkills = "/data/global/ui/SPELLS/NeSkillicon.DC6" 266 | PaladinSkills = "/data/global/ui/SPELLS/PaSkillicon.DC6" 267 | SorcererSkills = "/data/global/ui/SPELLS/SoSkillicon.DC6" 268 | 269 | RunButton = "/data/global/ui/PANEL/runbutton.dc6" 270 | MenuButton = "/data/global/ui/PANEL/menubutton.DC6" 271 | GoldCoinButton = "/data/global/ui/panel/goldcoinbtn.dc6" 272 | BuySellButton = "/data/global/ui/panel/buysellbtn.dc6" 273 | 274 | ArmorPlaceholder = "/data/global/ui/PANEL/inv_armor.DC6" 275 | BeltPlaceholder = "/data/global/ui/PANEL/inv_belt.DC6" 276 | BootsPlaceholder = "/data/global/ui/PANEL/inv_boots.DC6" 277 | HelmGlovePlaceholder = "/data/global/ui/PANEL/inv_helm_glove.DC6" 278 | RingAmuletPlaceholder = "/data/global/ui/PANEL/inv_ring_amulet.DC6" 279 | WeaponsPlaceholder = "/data/global/ui/PANEL/inv_weapons.DC6" 280 | 281 | // --- Data --- 282 | 283 | LevelPreset = "/data/global/excel/LvlPrest.txt" 284 | LevelType = "/data/global/excel/LvlTypes.txt" 285 | ObjectType = "/data/global/excel/objtype.txt" 286 | LevelWarp = "/data/global/excel/LvlWarp.txt" 287 | LevelDetails = "/data/global/excel/Levels.txt" 288 | LevelMaze = "/data/global/excel/LvlMaze.txt" 289 | LevelSubstitutions = "/data/global/excel/LvlSub.txt" 290 | 291 | ObjectDetails = "/data/global/excel/Objects.txt" 292 | ObjectMode = "/data/global/excel/ObjMode.txt" 293 | SoundSettings = "/data/global/excel/Sounds.txt" 294 | ItemStatCost = "/data/global/excel/ItemStatCost.txt" 295 | ItemRatio = "/data/global/excel/itemratio.txt" 296 | ItemTypes = "/data/global/excel/ItemTypes.txt" 297 | QualityItems = "/data/global/excel/qualityitems.txt" 298 | LowQualityItems = "/data/global/excel/lowqualityitems.txt" 299 | Overlays = "/data/global/excel/Overlay.txt" 300 | Runes = "/data/global/excel/runes.txt" 301 | Sets = "/data/global/excel/Sets.txt" 302 | SetItems = "/data/global/excel/SetItems.txt" 303 | AutoMagic = "/data/global/excel/automagic.txt" 304 | BodyLocations = "/data/global/excel/bodylocs.txt" 305 | Events = "/data/global/excel/events.txt" 306 | Properties = "/data/global/excel/Properties.txt" 307 | Hireling = "/data/global/excel/hireling.txt" 308 | HirelingDescription = "/data/global/excel/HireDesc.txt" 309 | DifficultyLevels = "/data/global/excel/difficultylevels.txt" 310 | AutoMap = "/data/global/excel/AutoMap.txt" 311 | CubeRecipes = "/data/global/excel/cubemain.txt" 312 | CubeModifier = "/data/global/excel/CubeMod.txt" 313 | CubeType = "/data/global/excel/CubeType.txt" 314 | Skills = "/data/global/excel/skills.txt" 315 | SkillDesc = "/data/global/excel/skilldesc.txt" 316 | SkillCalc = "/data/global/excel/skillcalc.txt" 317 | MissileCalc = "/data/global/excel/misscalc.txt" 318 | TreasureClass = "/data/global/excel/TreasureClass.txt" 319 | TreasureClassEx = "/data/global/excel/TreasureClassEx.txt" 320 | States = "/data/global/excel/states.txt" 321 | SoundEnvirons = "/data/global/excel/soundenviron.txt" 322 | Shrines = "/data/global/excel/shrines.txt" 323 | MonProp = "/data/global/excel/Monprop.txt" 324 | ElemType = "/data/global/excel/ElemTypes.txt" 325 | PlrMode = "/data/global/excel/PlrMode.txt" 326 | PetType = "/data/global/excel/pettype.txt" 327 | NPC = "/data/global/excel/npc.txt" 328 | MonsterUniqueModifier = "/data/global/excel/monumod.txt" 329 | MonsterEquipment = "/data/global/excel/monequip.txt" 330 | UniqueAppellation = "/data/global/excel/UniqueAppellation.txt" 331 | MonsterLevel = "/data/global/excel/monlvl.txt" 332 | MonsterSound = "/data/global/excel/monsounds.txt" 333 | MonsterSequence = "/data/global/excel/monseq.txt" 334 | PlayerClass = "/data/global/excel/PlayerClass.txt" 335 | PlayerType = "/data/global/excel/PlrType.txt" 336 | Composite = "/data/global/excel/Composit.txt" 337 | HitClass = "/data/global/excel/HitClass.txt" 338 | ObjectGroup = "/data/global/excel/objgroup.txt" 339 | CompCode = "/data/global/excel/compcode.txt" 340 | Belts = "/data/global/excel/belts.txt" 341 | Gamble = "/data/global/excel/gamble.txt" 342 | Colors = "/data/global/excel/colors.txt" 343 | StorePage = "/data/global/excel/StorePage.txt" 344 | 345 | // --- Animations --- 346 | 347 | ObjectData = "/data/global/objects" 348 | AnimationData = "/data/global/animdata.d2" 349 | PlayerAnimationBase = "/data/global/CHARS" 350 | MissileData = "/data/global/missiles" 351 | ItemGraphics = "/data/global/items" 352 | 353 | // --- Inventory Data --- 354 | 355 | Inventory = "/data/global/excel/inventory.txt" 356 | Weapons = "/data/global/excel/weapons.txt" 357 | Armor = "/data/global/excel/armor.txt" 358 | ArmorType = "/data/global/excel/ArmType.txt" 359 | WeaponClass = "/data/global/excel/WeaponClass.txt" 360 | Books = "/data/global/excel/books.txt" 361 | Misc = "/data/global/excel/misc.txt" 362 | UniqueItems = "/data/global/excel/UniqueItems.txt" 363 | Gems = "/data/global/excel/gems.txt" 364 | 365 | // --- Affixes --- 366 | 367 | MagicPrefix = "/data/global/excel/MagicPrefix.txt" 368 | MagicSuffix = "/data/global/excel/MagicSuffix.txt" 369 | RarePrefix = "/data/global/excel/RarePrefix.txt" // these are for item names 370 | RareSuffix = "/data/global/excel/RareSuffix.txt" 371 | 372 | // --- Monster Prefix/Suffixes (?) --- 373 | UniquePrefix = "/data/global/excel/UniquePrefix.txt" 374 | UniqueSuffix = "/data/global/excel/UniqueSuffix.txt" 375 | 376 | // --- Character Data --- 377 | 378 | Experience = "/data/global/excel/experience.txt" 379 | CharStats = "/data/global/excel/charstats.txt" 380 | 381 | // --- Music --- 382 | 383 | BGMTitle = "/data/global/music/introedit.wav" 384 | BGMOptions = "/data/global/music/Common/options.wav" 385 | BGMAct1AndarielAction = "/data/global/music/Act1/andarielaction.wav" 386 | BGMAct1BloodRavenResolution = "/data/global/music/Act1/bloodravenresolution.wav" 387 | BGMAct1Caves = "/data/global/music/Act1/caves.wav" 388 | BGMAct1Crypt = "/data/global/music/Act1/crypt.wav" 389 | BGMAct1DenOfEvilAction = "/data/global/music/Act1/denofevilaction.wav" 390 | BGMAct1Monastery = "/data/global/music/Act1/monastery.wav" 391 | BGMAct1Town1 = "/data/global/music/Act1/town1.wav" 392 | BGMAct1Tristram = "/data/global/music/Act1/tristram.wav" 393 | BGMAct1Wild = "/data/global/music/Act1/wild.wav" 394 | BGMAct2Desert = "/data/global/music/Act2/desert.wav" 395 | BGMAct2Harem = "/data/global/music/Act2/harem.wav" 396 | BGMAct2HoradricAction = "/data/global/music/Act2/horadricaction.wav" 397 | BGMAct2Lair = "/data/global/music/Act2/lair.wav" 398 | BGMAct2RadamentResolution = "/data/global/music/Act2/radamentresolution.wav" 399 | BGMAct2Sanctuary = "/data/global/music/Act2/sanctuary.wav" 400 | BGMAct2Sewer = "/data/global/music/Act2/sewer.wav" 401 | BGMAct2TaintedSunAction = "/data/global/music/Act2/taintedsunaction.wav" 402 | BGMAct2Tombs = "/data/global/music/Act2/tombs.wav" 403 | BGMAct2Town2 = "/data/global/music/Act2/town2.wav" 404 | BGMAct2Valley = "/data/global/music/Act2/valley.wav" 405 | BGMAct3Jungle = "/data/global/music/Act3/jungle.wav" 406 | BGMAct3Kurast = "/data/global/music/Act3/kurast.wav" 407 | BGMAct3KurastSewer = "/data/global/music/Act3/kurastsewer.wav" 408 | BGMAct3MefDeathAction = "/data/global/music/Act3/mefdeathaction.wav" 409 | BGMAct3OrbAction = "/data/global/music/Act3/orbaction.wav" 410 | BGMAct3Spider = "/data/global/music/Act3/spider.wav" 411 | BGMAct3Town3 = "/data/global/music/Act3/town3.wav" 412 | BGMAct4Diablo = "/data/global/music/Act4/diablo.wav" 413 | BGMAct4DiabloAction = "/data/global/music/Act4/diabloaction.wav" 414 | BGMAct4ForgeAction = "/data/global/music/Act4/forgeaction.wav" 415 | BGMAct4IzualAction = "/data/global/music/Act4/izualaction.wav" 416 | BGMAct4Mesa = "/data/global/music/Act4/mesa.wav" 417 | BGMAct4Town4 = "/data/global/music/Act4/town4.wav" 418 | BGMAct5Baal = "/data/global/music/Act5/baal.wav" 419 | BGMAct5Siege = "/data/global/music/Act5/siege.wav" 420 | BGMAct5Shenk = "/data/global/music/Act5/shenkmusic.wav" 421 | BGMAct5XTown = "/data/global/music/Act5/xtown.wav" 422 | BGMAct5XTemple = "/data/global/music/Act5/xtemple.wav" 423 | BGMAct5IceCaves = "/data/global/music/Act5/icecaves.wav" 424 | BGMAct5Nihlathak = "/data/global/music/Act5/nihlathakmusic.wav" 425 | 426 | // --- Sound Effects --- 427 | 428 | SFXCursorSelect = "cursor_select" 429 | SFXButtonClick = "cursor_button_click" 430 | SFXAmazonDeselect = "cursor_amazon_deselect" 431 | SFXAmazonSelect = "cursor_amazon_select" 432 | SFXAssassinDeselect = "Cursor/intro/assassin deselect.wav" 433 | SFXAssassinSelect = "Cursor/intro/assassin select.wav" 434 | SFXBarbarianDeselect = "cursor_barbarian_deselect" 435 | SFXBarbarianSelect = "cursor_barbarian_select" 436 | SFXDruidDeselect = "Cursor/intro/druid deselect.wav" 437 | SFXDruidSelect = "Cursor/intro/druid select.wav" 438 | SFXNecromancerDeselect = "cursor_necromancer_deselect" 439 | SFXNecromancerSelect = "cursor_necromancer_select" 440 | SFXPaladinDeselect = "cursor_paladin_deselect" 441 | SFXPaladinSelect = "cursor_paladin_select" 442 | SFXSorceressDeselect = "cursor_sorceress_deselect" 443 | SFXSorceressSelect = "cursor_sorceress_select" 444 | 445 | // --- Enemy Data --- 446 | 447 | MonStats = "/data/global/excel/monstats.txt" 448 | MonStats2 = "/data/global/excel/monstats2.txt" 449 | MonPreset = "/data/global/excel/monpreset.txt" 450 | MonType = "/data/global/excel/Montype.txt" 451 | SuperUniques = "/data/global/excel/SuperUniques.txt" 452 | MonMode = "/data/global/excel/monmode.txt" 453 | MonsterPlacement = "/data/global/excel/MonPlace.txt" 454 | MonsterAI = "/data/global/excel/monai.txt" 455 | 456 | // --- Skill Data --- 457 | 458 | Missiles = "/data/global/excel/Missiles.txt" 459 | 460 | // --- Palettes --- 461 | 462 | PaletteAct1 = "/data/global/palette/act1/pal.dat" 463 | PaletteAct2 = "/data/global/palette/act2/pal.dat" 464 | PaletteAct3 = "/data/global/palette/act3/pal.dat" 465 | PaletteAct4 = "/data/global/palette/act4/pal.dat" 466 | PaletteAct5 = "/data/global/palette/act5/pal.dat" 467 | PaletteEndGame = "/data/global/palette/endgame/pal.dat" 468 | PaletteEndGame2 = "/data/global/palette/endgame2/pal.dat" 469 | PaletteFechar = "/data/global/palette/fechar/pal.dat" 470 | PaletteLoading = "/data/global/palette/loading/pal.dat" 471 | PaletteMenu0 = "/data/global/palette/menu0/pal.dat" 472 | PaletteMenu1 = "/data/global/palette/menu1/pal.dat" 473 | PaletteMenu2 = "/data/global/palette/menu2/pal.dat" 474 | PaletteMenu3 = "/data/global/palette/menu3/pal.dat" 475 | PaletteMenu4 = "/data/global/palette/menu4/pal.dat" 476 | PaletteSky = "/data/global/palette/sky/pal.dat" 477 | PaletteStatic = "/data/global/palette/static/pal.dat" 478 | PaletteTrademark = "/data/global/palette/trademark/pal.dat" 479 | PaletteUnits = "/data/global/palette/units/pal.dat" 480 | 481 | // --- Palette Transforms --- 482 | 483 | PaletteTransformAct1 = "/data/global/palette/act1/Pal.pl2" 484 | PaletteTransformAct2 = "/data/global/palette/act2/Pal.pl2" 485 | PaletteTransformAct3 = "/data/global/palette/act3/Pal.pl2" 486 | PaletteTransformAct4 = "/data/global/palette/act4/Pal.pl2" 487 | PaletteTransformAct5 = "/data/global/palette/act5/Pal.pl2" 488 | PaletteTransformEndGame = "/data/global/palette/endgame/Pal.pl2" 489 | PaletteTransformEndGame2 = "/data/global/palette/endgame2/Pal.pl2" 490 | PaletteTransformFechar = "/data/global/palette/fechar/Pal.pl2" 491 | PaletteTransformLoading = "/data/global/palette/loading/Pal.pl2" 492 | PaletteTransformMenu0 = "/data/global/palette/menu0/Pal.pl2" 493 | PaletteTransformMenu1 = "/data/global/palette/menu1/Pal.pl2" 494 | PaletteTransformMenu2 = "/data/global/palette/menu2/Pal.pl2" 495 | PaletteTransformMenu3 = "/data/global/palette/menu3/Pal.pl2" 496 | PaletteTransformMenu4 = "/data/global/palette/menu4/Pal.pl2" 497 | PaletteTransformSky = "/data/global/palette/sky/Pal.pl2" 498 | PaletteTransformTrademark = "/data/global/palette/trademark/Pal.pl2" 499 | ) 500 | --------------------------------------------------------------------------------