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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
use std::{
fmt::{self, Display, Formatter},
path::PathBuf,
};
use serde::{Deserialize, Serialize};
use url::Url;
use super::{dependency::Dependency, version::Version, FactorioVersion};
/// The info.json file identifies the mod and defines its version.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModInfo {
/// The internal name of mod.
///
/// The game accepts anything as a mod name, however the mod portal
/// restricts mod names to only consist of alphanumeric characters,
/// dashes and underscores. Note that the mod folder or mod zip file
/// name has to contain the mod name, where the restrictions of the
/// file system apply.
///
/// The game accepts mod names with a maximum length of 100 characters.
/// The mod portal only accepts mods with names that are longer than
/// 3 characters and shorter than 50 characters.
pub name: String,
/// Defines the version of the mod.
pub version: Version,
/// The display name of the mod, so it is not recommended to use
/// someUgly_pRoGrAmMeR-name here.
///
/// Can be overwritten with a locale entry in the `mod-name` category,
/// using the internal mod name as the key.
///
/// The game will reject a title field that is longer than 100 characters.
/// However, this can be worked around by using the locale entry.
/// The mod portal does not restrict mod title length.
pub title: String,
/// The author of the mod.
///
/// This field does not have restrictions, it can also be a list of
/// authors etc. The mod portal ignores this field, it will simply display
/// the uploader's name as the author.
pub author: String,
/// How the mod author can be contacted, for example an email address.
#[serde(skip_serializing_if = "Option::is_none")]
pub contact: Option<String>,
/// Where the mod can be found on the internet.
///
/// Note that the in-game mod browser shows the mod portal link additionally
/// to this field. Please don't put the string `"None"` here,
/// it makes the field on the mod portal website look ugly.
/// Just leave the field empty if the mod doesn't have
/// a website/forum thread/discord.
///
/// **Note:** The [`None`] variant is perfectly valid to use.
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage: Option<Url>,
/// A short description of what your mod does.
///
/// This is all that people get to see in-game.
/// Can be overwritten with a locale entry in the `mod-description` category,
/// using the internal mod name as the key.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// The Factorio version that this mod supports.
///
/// This can only be one Factorio version, not multiple.
/// However, it includes all `.sub` (`.patch`) versions.
/// While the field is optional, usually mods are developed for versions
/// higher than the default 0.12, so the field has to be added anyway.
///
/// Adding a sub (patch) part, e.g. "0.18.27" will make the mod portal
/// reject the mod and the game act weirdly.
/// That means this shouldn't be done;
/// use only the major and minor components "major.minor", for example "1.0".
///
/// Mods with the factorio_version "0.18" can also be loaded in 1.0
/// and the mod portal will return them
/// when queried for factorio_version 1.0 mods.
#[serde(default)]
pub factorio_version: FactorioVersion,
/// Mods that this mod depends on or is incompatible with.
///
/// If this mod depends on another, the other mod will load first,
/// see [Data-Lifecycle][].
/// An empty [`Vec`] allows to work around the default
/// and have no dependencies at all.
///
/// [data-lifecycle]: https://lua-api.factorio.com/latest/Data-Lifecycle.html
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dependencies: Vec<Dependency>,
/// Unofficial extensions to the `info.json` format.
///
/// Used by third party packaging tools (such as [Facti][]).
///
/// [facti]: https://facti.rs
#[serde(skip_serializing_if = "Option::is_none")]
pub package: Option<ModPackageInfo>,
}
/// Contains unofficial extensions to the `info.json` format.
///
/// Used by third party packaging tools.
#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModPackageInfo {
/// The path to a file that should be used as the content for
/// the "information" page on the mod portal.
///
/// The mod portal supports markdown formatting, so it's recommended
/// to specify the path to a markdown file here.
///
/// Note that this path is considered relative to the `info.json` file
/// (unless an absolute path is specified).
#[serde(rename = "information", skip_serializing_if = "Option::is_none")]
pub readme_path: Option<PathBuf>,
/// The path to a file that should be used as the content for
/// the "FAQ" page on the mod portal.
///
/// The mod portal supports markdown formatting, so it's recommended
/// to specify the path to a markdown file here.
///
/// Note that this path is considered relative to the `info.json` file
/// (unless an absolute path is specified).
#[serde(rename = "faq", skip_serializing_if = "Option::is_none")]
pub faq_path: Option<PathBuf>,
/// Paths to images that should be displayed on the mod portal.
///
/// If mod details are updated with Facti, the images will be displayed
/// on the mod portal in the same order that they are specified in this
/// [`Vec`].
///
/// Note that these paths are considered relative to the `info.json` file
/// (unless absolute paths are specified).
#[serde(default, rename = "gallery", skip_serializing_if = "Vec::is_empty")]
pub gallery_paths: Vec<PathBuf>,
}
impl ModInfo {
/// Creates a builder to more conveniently construct a [`ModInfo`] struct.
pub fn builder<T, U, V>(name: T, version: Version, title: U, author: V) -> ModInfoBuilder
where
T: Into<String>,
U: Into<String>,
V: Into<String>,
{
ModInfoBuilder::new(name, version, title, author)
}
}
impl Display for ModInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{} v{} by {}", self.name, self.version, self.author)
}
}
/// Contains an internal [`ModInfo`] value that is used to later construct a
/// finished [`ModInfo`]`.
pub struct ModInfoBuilder {
info: ModInfo,
}
/// Contains methods to successively build up a [`ModInfo`] struct.
impl ModInfoBuilder {
fn new<T, V, U, X>(name: T, version: V, title: U, author: X) -> Self
where
T: Into<String>,
V: Into<Version>,
U: Into<String>,
X: Into<String>,
{
Self {
info: ModInfo {
name: name.into(),
version: version.into(),
title: title.into(),
author: author.into(),
contact: None,
homepage: None,
description: None,
factorio_version: Default::default(),
dependencies: Vec::new(),
package: None,
},
}
}
/// Sets the [`contact`][ModInfo::contact] field.
pub fn contact<T: Into<String>>(&mut self, contact: T) -> &mut Self {
self.info.contact = Some(contact.into());
self
}
/// Sets the [`homepage`][ModInfo::homepage] field.
pub fn homepage(&mut self, homepage: Url) -> &mut Self {
self.info.homepage = Some(homepage);
self
}
/// Sets the [`description`][ModInfo::description] field.
pub fn description<T: Into<String>>(&mut self, description: T) -> &mut Self {
self.info.description = Some(description.into());
self
}
/// Sets the [`factorio_version`][ModInfo::factorio_version] field.
pub fn factorio_version(&mut self, factorio_version: FactorioVersion) -> &mut Self {
self.info.factorio_version = factorio_version;
self
}
/// Adds a dependency to [`dependencies`][ModInfo::dependencies].
pub fn dependency(&mut self, dependency: Dependency) -> &mut Self {
self.info.dependencies.push(dependency);
self
}
/// Adds multiple dependencies at once to [`dependencies`][ModInfo::dependencies].
pub fn dependencies(&mut self, dependencies: &[Dependency]) -> &mut Self {
self.info.dependencies.extend_from_slice(dependencies);
self
}
/// Sets a path to the readme file that should be displayed
/// on the mod portal.
///
/// The path is relative to where the `info.json` file is located.
///
/// **Note:** This is an unofficial extension to the `info.json` format.
pub fn information_path<T: Into<PathBuf>>(&mut self, path: T) -> &mut Self {
self.info
.package
.get_or_insert_with(Default::default)
.readme_path = Some(path.into());
self
}
/// Sets a path to the FAQ file that should be displayed
/// on the mod portal.
///
/// The path is relative to where the `info.json` file is located.
///
///
pub fn faq_path<T: Into<PathBuf>>(&mut self, path: T) -> &mut Self {
self.info
.package
.get_or_insert_with(Default::default)
.faq_path = Some(path.into());
self
}
/// Adds a path to the collection of gallery images.
pub fn gallery<T: Into<PathBuf>>(&mut self, gallery: T) -> &mut Self {
self.info
.package
.get_or_insert_with(Default::default)
.gallery_paths
.push(gallery.into());
self
}
/// Builds a finished [`ModInfo`] from the builder.
pub fn build(&mut self) -> ModInfo {
self.info.clone()
}
}
#[cfg(test)]
mod tests {
use crate::{modinfo::ModPackageInfo, version::VersionReq};
use super::*;
#[test]
fn test_builder() {
let expected = ModInfo {
name: "boblibrary".to_string(),
version: Version::parse("0.17.0").unwrap(),
title: "Bob's Library".to_string(),
author: "Bob".to_string(),
contact: None,
homepage: None,
description: None,
factorio_version: Default::default(),
dependencies: vec![Dependency::required(
"angel".to_string(),
VersionReq::Latest,
)],
package: Some(ModPackageInfo {
readme_path: Some(PathBuf::from("README.md")),
..Default::default()
}),
};
let mut builder = ModInfoBuilder::new(
"boblibrary".to_string(),
Version::parse("0.17.0").unwrap(),
"Bob's Library".to_string(),
"Bob".to_string(),
);
builder.dependency(Dependency::required(
"angel".to_string(),
VersionReq::Latest,
));
builder.information_path("README.md");
let built = builder.build();
assert_eq!(built, expected);
}
}