File: JJ2Episode.cpp

package info (click to toggle)
jazz2-native 3.5.0-2
  • links: PTS, VCS
  • area: contrib
  • in suites: forky, sid
  • size: 16,912 kB
  • sloc: cpp: 172,557; xml: 113; python: 36; makefile: 5; sh: 2
file content (225 lines) | stat: -rw-r--r-- 8,334 bytes parent folder | download
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#include "JJ2Episode.h"
#include "JJ2Anims.h"
#include "JJ2Anims.Palettes.h"
#include "../ContentResolver.h"

#include "../../nCine/Base/Algorithms.h"

#include <Containers/StringUtils.h>
#include <IO/FileSystem.h>

using namespace Death::IO;

namespace Jazz2::Compatibility
{
	JJ2Episode::JJ2Episode()
		: Position(0), ImageWidth(0), ImageHeight(0), TitleWidth(0), TitleHeight(0)
	{
	}

	JJ2Episode::JJ2Episode(StringView name, StringView displayName, StringView firstLevel, std::int32_t position)
		: Name(name), DisplayName(displayName), FirstLevel(firstLevel), Position(position), ImageWidth(0), ImageHeight(0), TitleWidth(0), TitleHeight(0)
	{
	}

	bool JJ2Episode::Open(StringView path)
	{
		auto s = fs::Open(path, FileAccess::Read);
		if (!s->IsValid()) {
			LOGE("Cannot open file \"{}\" for reading", path);
			return false;
		}

		Name = fs::GetFileNameWithoutExtension(path);
		StringUtils::lowercaseInPlace(Name);

		// TODO: Implement JJ2+ extended data, but I haven't seen it anywhere yet
		// the condition of unlocking (currently only defined for 0 meaning "always unlocked"
		// and 1 meaning "requires the previous episode to be finished", stored as a 4-byte-long
		// integer starting at byte 0x4), binary flags of various purpose (currently supported
		// flags are 1 and 2 used to reset respectively player ammo and lives when the episode
		// begins; stored as a 4-byte-long integer starting at byte 0x8)

		// Header (208 bytes)
		/*std::int32_t headerSize =*/ s->ReadValueAsLE<std::int32_t>();
		Position = s->ReadValueAsLE<std::int32_t>();
		/*std::uint32_t flags =*/ s->ReadValueAsLE<std::uint32_t>();	// 0x01 = Not Shareware
		/*std::uint32_t unknown1 =*/ s->ReadValueAsLE<std::uint32_t>();

		char tmpBuffer[64];

		// Episode name
		s->Read(tmpBuffer, 64);
		std::int32_t length = 0;
		while (tmpBuffer[length] != '\0' && length < 64) {
			length++;
		}
		DisplayName = String(tmpBuffer, length);

		// Previous Episode
		s->Read(tmpBuffer, 32);
		length = 0;
		while (tmpBuffer[length] != '\0' && length < 32) {
			length++;
		}
		PreviousEpisode = String(tmpBuffer, length);
		StringUtils::lowercaseInPlace(PreviousEpisode);

		// Next Episode
		s->Read(tmpBuffer, 32);
		length = 0;
		while (tmpBuffer[length] != '\0' && length < 32) {
			length++;
		}
		NextEpisode = String(tmpBuffer, length);
		StringUtils::lowercaseInPlace(NextEpisode);

		// First level
		s->Read(tmpBuffer, 32);
		length = 0;
		while (tmpBuffer[length] != '\0' && length < 32) {
			length++;
		}
		FirstLevel = String(tmpBuffer, length);
		StringUtils::lowercaseInPlace(FirstLevel);

		ImageWidth = s->ReadValueAsLE<std::int32_t>();
		ImageHeight = s->ReadValueAsLE<std::int32_t>();
		/*std::int32_t unknown2 =*/ s->ReadValueAsLE<std::int32_t>();
		/*std::int32_t unknown3 =*/ s->ReadValueAsLE<std::int32_t>();

		TitleWidth = s->ReadValueAsLE<std::int32_t>();
		TitleHeight = s->ReadValueAsLE<std::int32_t>();
		/*std::int32_t unknown4 =*/ s->ReadValueAsLE<std::int32_t>();
		/*std::int32_t unknown5 =*/ s->ReadValueAsLE<std::int32_t>();

		// Background image
		{
			std::int32_t imagePackedSize = s->ReadValueAsLE<std::int32_t>();
			std::int32_t imageUnpackedSize = ImageWidth * ImageHeight;
			JJ2Block imageBlock(s, imagePackedSize, imageUnpackedSize);
			ImageData = std::make_unique<std::uint8_t[]>(imageUnpackedSize);
			imageBlock.ReadRawBytes(ImageData.get(), imageUnpackedSize);
		}

		// Title image
		{
			std::int32_t titleLightPackedSize = s->ReadValueAsLE<std::int32_t>();
			std::int32_t titleLightUnpackedSize = TitleWidth * TitleHeight;
			JJ2Block titleLightBlock(s, titleLightPackedSize, titleLightUnpackedSize);
			TitleData = std::make_unique<std::uint8_t[]>(titleLightUnpackedSize);
			titleLightBlock.ReadRawBytes(TitleData.get(), titleLightUnpackedSize);
		}
		//{
		//    std::int32_t titleDarkPackedSize = Stream::FromLE(->ReadValue<std::int32_t>());
		//    std::int32_t titleDarkUnpackedSize = titleWidth * titleHeight;
		//    JJ2Block titleDarkBlock(s, titleDarkPackedSize, titleDarkUnpackedSize);
		//    episode.titleDark = ConvertIndicesToRgbaBitmap(titleWidth, titleHeight, titleDarkBlock, true);
		//}

		return true;
	}

	void JJ2Episode::Convert(StringView targetPath, Function<JJ2Level::LevelToken(StringView)>&& levelTokenConversion, Function<String(JJ2Episode*)>&& episodeNameConversion, Function<Pair<String, String>(JJ2Episode*)>&& episodePrevNext)
	{
		auto so = fs::Open(targetPath, FileAccess::Write);
		DEATH_ASSERT(so->IsValid(), "Cannot open file for writing", );

		so->WriteValueAsLE<std::uint64_t>(0x2095A59FF0BFBBEF);
		so->WriteValue<std::uint8_t>(ContentResolver::EpisodeFile);

		std::uint16_t flags = 0x00;
		so->WriteValueAsLE<std::uint16_t>(flags);

		String displayName = (episodeNameConversion ? episodeNameConversion(this) : DisplayName);
		so->WriteValue<std::uint8_t>((std::uint8_t)displayName.size());
		so->Write(displayName.data(), displayName.size());

		so->WriteValueAsLE<std::uint16_t>(Position);

		MutableStringView firstLevel = FirstLevel;
		if (JJ2Level::StringHasSuffixIgnoreCase(firstLevel, ".j2l"_s) ||
			JJ2Level::StringHasSuffixIgnoreCase(firstLevel, ".lev"_s)) {
			firstLevel = firstLevel.exceptSuffix(4);
		}

		if (levelTokenConversion) {
			auto token = levelTokenConversion(firstLevel);
			so->WriteValue<std::uint8_t>(std::uint8_t(token.Level.size()));
			so->Write(token.Level.data(), token.Level.size());
		} else {
			so->WriteValue<std::uint8_t>(std::uint8_t(firstLevel.size()));
			so->Write(firstLevel.data(), firstLevel.size());
		}

		if (!PreviousEpisode.empty() || !NextEpisode.empty()) {
			MutableStringView previousEpisode = PreviousEpisode;
			if (JJ2Level::StringHasSuffixIgnoreCase(previousEpisode, ".j2e"_s)) {
				previousEpisode = previousEpisode.exceptSuffix(4);
			} else if (JJ2Level::StringHasSuffixIgnoreCase(previousEpisode, ".j2pe"_s)) {
				previousEpisode = previousEpisode.exceptSuffix(5);
			}
			MutableStringView nextEpisode = NextEpisode;
			if (JJ2Level::StringHasSuffixIgnoreCase(nextEpisode, ".j2e"_s)) {
				nextEpisode = nextEpisode.exceptSuffix(4);
			} else if(JJ2Level::StringHasSuffixIgnoreCase(nextEpisode, ".j2pe"_s)) {
				nextEpisode = nextEpisode.exceptSuffix(5);
			}

			so->WriteValue<std::uint8_t>((std::uint8_t)previousEpisode.size());
			so->Write(previousEpisode.data(), previousEpisode.size());
			so->WriteValue<std::uint8_t>((std::uint8_t)nextEpisode.size());
			so->Write(nextEpisode.data(), nextEpisode.size());
		} else if (episodePrevNext) {
			auto prevNext = episodePrevNext(this);
			so->WriteValue<std::uint8_t>((std::uint8_t)prevNext.first().size());
			so->Write(prevNext.first().data(), prevNext.first().size());
			so->WriteValue<std::uint8_t>((std::uint8_t)prevNext.second().size());
			so->Write(prevNext.second().data(), prevNext.second().size());
		} else {
			so->WriteValue<std::uint8_t>(0);
			so->WriteValue<std::uint8_t>(0);
		}

		// Write episode title image
		so->WriteValueAsLE<std::uint16_t>(TitleWidth);
		so->WriteValueAsLE<std::uint16_t>(TitleHeight);

		std::uint32_t titlePixelsCount = TitleWidth * TitleHeight;
		std::unique_ptr<std::uint8_t[]> titlePixels = std::make_unique<std::uint8_t[]>(titlePixelsCount * 4);
		for (std::uint32_t i = 0; i < titlePixelsCount; i++) {
			std::uint8_t colorIdx = TitleData[i];

			// Remove shadow
			if (colorIdx == 63 || colorIdx == 143) {
				colorIdx = 0;
			}

			const Color& src = MenuPalette[colorIdx];
			titlePixels[i * 4] = src.R;
			titlePixels[i * 4 + 1] = src.G;
			titlePixels[i * 4 + 2] = src.B;
			titlePixels[i * 4 + 3] = src.A;
		}

		JJ2Anims::WriteImageContent(*so, titlePixels.get(), TitleWidth, TitleHeight, 4);

		// Write episode background image
		so->WriteValueAsLE<std::uint16_t>(ImageWidth);
		so->WriteValueAsLE<std::uint16_t>(ImageHeight);

		std::uint32_t imagePixelsCount = ImageWidth * ImageHeight;
		std::unique_ptr<std::uint8_t[]> imagePixels = std::make_unique<std::uint8_t[]>(imagePixelsCount * 4);
		for (std::uint32_t i = 0; i < imagePixelsCount; i++) {
			std::uint8_t colorIdx = ImageData[i];

			const Color& src = MenuPalette[colorIdx];
			imagePixels[i * 4] = src.R;
			imagePixels[i * 4 + 1] = src.G;
			imagePixels[i * 4 + 2] = src.B;
			imagePixels[i * 4 + 3] = src.A;
		}

		JJ2Anims::WriteImageContent(*so, imagePixels.get(), ImageWidth, ImageHeight, 4);
	}
}