1 /*
2  * This file is part of OpenTTD.
3  * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4  * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5  * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6  */
7 
8 /** @file music.cpp The songs that OpenTTD knows. */
9 
10 #include "stdafx.h"
11 
12 
13 /** The type of set we're replacing */
14 #define SET_TYPE "music"
15 #include "base_media_func.h"
16 
17 #include "safeguards.h"
18 #include "random_access_file_type.h"
19 
20 
21 /**
22  * Read the name of a music CAT file entry.
23  * @param filename Name of CAT file to read from
24  * @param entrynum Index of entry whose name to read
25  * @return Pointer to string, caller is responsible for freeing memory,
26  *         nullptr if entrynum does not exist.
27  */
GetMusicCatEntryName(const char * filename,size_t entrynum)28 char *GetMusicCatEntryName(const char *filename, size_t entrynum)
29 {
30 	if (!FioCheckFileExists(filename, BASESET_DIR)) return nullptr;
31 
32 	RandomAccessFile file(filename, BASESET_DIR);
33 	uint32 ofs = file.ReadDword();
34 	size_t entry_count = ofs / 8;
35 	if (entrynum < entry_count) {
36 		file.SeekTo(entrynum * 8, SEEK_SET);
37 		file.SeekTo(file.ReadDword(), SEEK_SET);
38 		byte namelen = file.ReadByte();
39 		char *name = MallocT<char>(namelen + 1);
40 		file.ReadBlock(name, namelen);
41 		name[namelen] = '\0';
42 		return name;
43 	}
44 	return nullptr;
45 }
46 
47 /**
48  * Read the full data of a music CAT file entry.
49  * @param filename Name of CAT file to read from.
50  * @param entrynum Index of entry to read
51  * @param[out] entrylen Receives length of data read
52  * @return Pointer to buffer with data read, caller is responsible for freeind memory,
53  *         nullptr if entrynum does not exist.
54  */
GetMusicCatEntryData(const char * filename,size_t entrynum,size_t & entrylen)55 byte *GetMusicCatEntryData(const char *filename, size_t entrynum, size_t &entrylen)
56 {
57 	entrylen = 0;
58 	if (!FioCheckFileExists(filename, BASESET_DIR)) return nullptr;
59 
60 	RandomAccessFile file(filename, BASESET_DIR);
61 	uint32 ofs = file.ReadDword();
62 	size_t entry_count = ofs / 8;
63 	if (entrynum < entry_count) {
64 		file.SeekTo(entrynum * 8, SEEK_SET);
65 		size_t entrypos = file.ReadDword();
66 		entrylen = file.ReadDword();
67 		file.SeekTo(entrypos, SEEK_SET);
68 		file.SkipBytes(file.ReadByte());
69 		byte *data = MallocT<byte>(entrylen);
70 		file.ReadBlock(data, entrylen);
71 		return data;
72 	}
73 	return nullptr;
74 }
75 
76 INSTANTIATE_BASE_MEDIA_METHODS(BaseMedia<MusicSet>, MusicSet)
77 
78 /** Names corresponding to the music set's files */
79 static const char * const _music_file_names[] = {
80 	"theme",
81 	"old_0", "old_1", "old_2", "old_3", "old_4", "old_5", "old_6", "old_7", "old_8", "old_9",
82 	"new_0", "new_1", "new_2", "new_3", "new_4", "new_5", "new_6", "new_7", "new_8", "new_9",
83 	"ezy_0", "ezy_1", "ezy_2", "ezy_3", "ezy_4", "ezy_5", "ezy_6", "ezy_7", "ezy_8", "ezy_9",
84 };
85 /** Make sure we aren't messing things up. */
86 static_assert(lengthof(_music_file_names) == NUM_SONGS_AVAILABLE);
87 
88 template <class T, size_t Tnum_files, bool Tsearch_in_tars>
89 /* static */ const char * const *BaseSet<T, Tnum_files, Tsearch_in_tars>::file_names = _music_file_names;
90 
91 template <class Tbase_set>
GetExtension()92 /* static */ const char *BaseMedia<Tbase_set>::GetExtension()
93 {
94 	return ".obm"; // OpenTTD Base Music
95 }
96 
97 template <class Tbase_set>
DetermineBestSet()98 /* static */ bool BaseMedia<Tbase_set>::DetermineBestSet()
99 {
100 	if (BaseMedia<Tbase_set>::used_set != nullptr) return true;
101 
102 	const Tbase_set *best = nullptr;
103 	for (const Tbase_set *c = BaseMedia<Tbase_set>::available_sets; c != nullptr; c = c->next) {
104 		if (c->GetNumMissing() != 0) continue;
105 
106 		if (best == nullptr ||
107 				(best->fallback && !c->fallback) ||
108 				best->valid_files < c->valid_files ||
109 				(best->valid_files == c->valid_files &&
110 					(best->shortname == c->shortname && best->version < c->version))) {
111 			best = c;
112 		}
113 	}
114 
115 	BaseMedia<Tbase_set>::used_set = best;
116 	return BaseMedia<Tbase_set>::used_set != nullptr;
117 }
118 
FillSetDetails(IniFile * ini,const char * path,const char * full_filename)119 bool MusicSet::FillSetDetails(IniFile *ini, const char *path, const char *full_filename)
120 {
121 	bool ret = this->BaseSet<MusicSet, NUM_SONGS_AVAILABLE, false>::FillSetDetails(ini, path, full_filename);
122 	if (ret) {
123 		this->num_available = 0;
124 		IniGroup *names = ini->GetGroup("names");
125 		IniGroup *catindex = ini->GetGroup("catindex");
126 		IniGroup *timingtrim = ini->GetGroup("timingtrim");
127 		uint tracknr = 1;
128 		for (uint i = 0; i < lengthof(this->songinfo); i++) {
129 			const char *filename = this->files[i].filename;
130 			if (names == nullptr || StrEmpty(filename) || this->files[i].check_result == MD5File::CR_NO_FILE) {
131 				this->songinfo[i].songname[0] = '\0';
132 				continue;
133 			}
134 
135 			this->songinfo[i].filename = filename; // non-owned pointer
136 
137 			IniItem *item = catindex->GetItem(_music_file_names[i], false);
138 			if (item != nullptr && item->value.has_value() && !item->value->empty()) {
139 				/* Song has a CAT file index, assume it's MPS MIDI format */
140 				this->songinfo[i].filetype = MTT_MPSMIDI;
141 				this->songinfo[i].cat_index = atoi(item->value->c_str());
142 				char *songname = GetMusicCatEntryName(filename, this->songinfo[i].cat_index);
143 				if (songname == nullptr) {
144 					Debug(grf, 0, "Base music set song missing from CAT file: {}/{}", filename, this->songinfo[i].cat_index);
145 					this->songinfo[i].songname[0] = '\0';
146 					continue;
147 				}
148 				strecpy(this->songinfo[i].songname, songname, lastof(this->songinfo[i].songname));
149 				free(songname);
150 			} else {
151 				this->songinfo[i].filetype = MTT_STANDARDMIDI;
152 			}
153 
154 			const char *trimmed_filename = filename;
155 			/* As we possibly add a path to the filename and we compare
156 			 * on the filename with the path as in the .obm, we need to
157 			 * keep stripping path elements until we find a match. */
158 			for (; trimmed_filename != nullptr; trimmed_filename = strchr(trimmed_filename, PATHSEPCHAR)) {
159 				/* Remove possible double path separator characters from
160 				 * the beginning, so we don't start reading e.g. root. */
161 				while (*trimmed_filename == PATHSEPCHAR) trimmed_filename++;
162 
163 				item = names->GetItem(trimmed_filename, false);
164 				if (item != nullptr && item->value.has_value() && !item->value->empty()) break;
165 			}
166 
167 			if (this->songinfo[i].filetype == MTT_STANDARDMIDI) {
168 				if (item != nullptr && item->value.has_value() && !item->value->empty()) {
169 					strecpy(this->songinfo[i].songname, item->value->c_str(), lastof(this->songinfo[i].songname));
170 				} else {
171 					Debug(grf, 0, "Base music set song name missing: {}", filename);
172 					return false;
173 				}
174 			}
175 			this->num_available++;
176 
177 			/* Number the theme song (if any) track 0, rest are normal */
178 			if (i == 0) {
179 				this->songinfo[i].tracknr = 0;
180 			} else {
181 				this->songinfo[i].tracknr = tracknr++;
182 			}
183 
184 			item = trimmed_filename != nullptr ? timingtrim->GetItem(trimmed_filename, false) : nullptr;
185 			if (item != nullptr && item->value.has_value() && !item->value->empty()) {
186 				auto endpos = item->value->find(':');
187 				if (endpos != std::string::npos) {
188 					this->songinfo[i].override_start = atoi(item->value->c_str());
189 					this->songinfo[i].override_end = atoi(item->value->c_str() + endpos + 1);
190 				}
191 			}
192 		}
193 	}
194 	return ret;
195 }
196