/
file.go
179 lines (166 loc) · 4.71 KB
/
file.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// Copyright (C) 2018-2023, John Chadwick <john@jchw.io>
//
// Permission to use, copy, modify, and/or distribute this software for any purpose
// with or without fee is hereby granted, provided that the above copyright notice
// and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
// THIS SOFTWARE.
//
// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick
// SPDX-License-Identifier: ISC
package iff
import (
"archive/zip"
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/go-restruct/restruct"
"github.com/pangbox/pangfiles/pak"
"github.com/rs/zerolog"
)
type Archive struct {
ItemMap map[uint32]*Item
}
// Filenames to look for to find client IFF.
var iffSearchOrder = []string{
"pangya_gb.iff",
"pangya_us.iff", // older US, before global
"pangya_jp.iff",
"pangya_eu.iff",
"pangya_th.iff",
"pangya_sg.iff", // nb: uses jp key
"pangya_idnes.iff",
"pangya.iff", // kr (present in some gb ver too)
}
func LoadFromPak(log zerolog.Logger, fs pak.FS) (*Archive, error) {
data, err := findPangYaIFF(fs)
if err != nil {
return nil, err
}
return Load(log, data)
}
func Load(log zerolog.Logger, data []byte) (*Archive, error) {
archive := &Archive{}
r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, err
}
for _, f := range r.File {
log.Debug().Str("file", f.Name).Msg("found IFF")
if f.Name == "Item.iff" {
r, err := f.Open()
if err != nil {
return nil, err
}
defer r.Close()
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
archive.loadItems(data)
}
}
return archive, nil
}
func (a *Archive) loadItems(data []byte) error {
file, err := LoadItems(data)
if err != nil {
return err
}
a.ItemMap = make(map[uint32]*Item)
for _, item := range file.Records {
a.ItemMap[item.ID] = &item
}
return nil
}
func LoadItems(data []byte) (*File[Item], error) {
recordCount := binary.LittleEndian.Uint16(data[:2])
recordLength := (len(data) - 0x8) / int(recordCount)
version := Version(binary.LittleEndian.Uint32(data[4:8]))
switch version {
case Version11:
switch recordLength {
case 0x78:
return loadItemVersion[ItemV11_78](data)
case 0x98:
return loadItemVersion[ItemV11_98](data)
case 0xB0:
return loadItemVersion[ItemV11_B0](data)
case 0xC0:
return loadItemVersion[ItemV11_C0](data)
case 0xC4:
return loadItemVersion[ItemV11_C4](data)
case 0xD8:
// JP4xx has the common times after the model name
// this is back to normal in JP5xx
var testItem ItemV11_D8_2
restruct.Unpack(data[8:], binary.LittleEndian, &testItem)
if testItem.StartTime.IsZero() || testItem.StartTime.IsValid() {
return loadItemVersion[ItemV11_D8_2](data)
} else {
return loadItemVersion[ItemV11_D8_1](data)
}
default:
return nil, fmt.Errorf("unknown item iff v%d record size %d (please report)", version, recordLength)
}
case Version13:
switch recordLength {
case 0xE0:
return loadItemVersion[ItemV13_E0](data)
case 0xF8:
return loadItemVersion[ItemV13_F8](data)
default:
return nil, fmt.Errorf("unknown item iff v%d record size %d (please report)", version, recordLength)
}
default:
return nil, errors.New("unknown item iff version")
}
}
func loadItemVersion[T itemGeneric](data []byte) (*File[Item], error) {
result := &File[Item]{}
f, err := LoadFile[T](data)
if err != nil {
return nil, err
}
result.Header = f.Header
for _, record := range f.Records {
result.Records = append(result.Records, record.Generic())
}
size, err := restruct.SizeOf(f)
if err != nil {
return nil, err
}
if len(data) > size {
return nil, fmt.Errorf("short read: read %d of %d bytes", size, len(data))
}
return result, nil
}
func LoadFile[T any](data []byte) (*File[T], error) {
file := &File[T]{}
if err := restruct.Unpack(data, binary.LittleEndian, file); err != nil {
return nil, err
}
return file, nil
}
func findPangYaIFF(fs pak.FS) ([]byte, error) {
var errs error
if fs.NumFiles() == 0 {
return nil, fmt.Errorf("no pak files found, aborting IFF search")
}
for _, fn := range iffSearchOrder {
data, err := fs.ReadFile(fn)
if err == nil {
return data, err
}
errs = errors.Join(errs, fmt.Errorf("trying: %q: %w", fn, err))
}
return nil, fmt.Errorf("error finding IFF file: %w", errs)
}