1 /* 2 * Copyright (c) 2017-2019 sel-project 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 * 22 */ 23 module config; 24 25 import std.ascii : newline; 26 import std.conv : to, ConvException; 27 import std.file : exists, read, write, remove, tempDir, mkdirRecurse; 28 import std.json : JSONValue; 29 import std.path : dirSeparator, buildNormalizedPath; 30 import std.socket : Address; 31 import std..string : replace, split, join, toLower, toUpper, startsWith, endsWith; 32 import std.traits : isArray, isAssociativeArray, isIntegral, isFloatingPoint; 33 import std.typetuple : TypeTuple; 34 import std.uuid : UUID, parseUUID; 35 import std.zip : ZipArchive; 36 37 import selery.about; 38 import selery.config : Config, Files; 39 import selery.lang : LanguageManager; 40 import selery.plugin : Plugin; 41 42 import toml; 43 import toml.json; 44 45 enum bool portable = __traits(compiles, import("portable.zip")); 46 47 enum ConfigType : string { 48 49 default_ = "default", 50 hub = "hub", 51 node = "node" 52 53 } 54 55 mixin({ 56 string[] commands; 57 foreach(member ; __traits(allMembers, Config.Node)) { 58 static if(member.endsWith("Command")) commands ~= (`"` ~ member[0..$-7] ~ `"`); 59 } 60 return "alias Commands = TypeTuple!(" ~ commands.join(",") ~ ");"; 61 }()); 62 63 auto loadConfig(ConfigType type, ref string[] args) { 64 65 immutable filename = (){ 66 final switch(type) with(ConfigType) { 67 case default_: return "selery.toml"; 68 case hub: return "selery.hub.toml"; 69 case node: return "selery.node.toml"; 70 } 71 }(); 72 73 immutable isHub = type == ConfigType.default_ || type == ConfigType.hub; 74 immutable isNode = type == ConfigType.default_ || type == ConfigType.node; 75 76 bool hasArg(string a) { 77 foreach(i, arg; args) { 78 if(arg == a) { 79 args = args[0..i] ~ args[i+1..$]; 80 return true; 81 } 82 } 83 return false; 84 } 85 86 auto config = new class Config { 87 88 private string language; 89 90 public override void load() { 91 92 version(Windows) { 93 import std.utf : toUTF8; 94 import std..string : fromStringz; 95 import core.sys.windows.winnls; 96 wchar[] lang = new wchar[3]; 97 wchar[] country = new wchar[3]; 98 GetLocaleInfo(GetUserDefaultUILanguage(), LOCALE_SISO639LANGNAME, lang.ptr, 3); 99 GetLocaleInfo(GetUserDefaultUILanguage(), LOCALE_SISO3166CTRYNAME, country.ptr, 3); 100 this.language = fromStringz(toUTF8(lang).ptr) ~ "_" ~ fromStringz(toUTF8(country).ptr); 101 } else { 102 import std.process : environment; 103 this.language = environment.get("LANGUAGE", environment.get("LANG", "en_US")); 104 } 105 106 this.reload(); 107 108 immutable temp = buildNormalizedPath(tempDir() ~ dirSeparator ~ "selery" ~ dirSeparator ~ this.uuid.toString().toUpper()); 109 mkdirRecurse(temp); 110 111 static if(portable) { 112 113 this.files = new CompressedFiles(new ZipArchive(cast(void[])import("portable.zip")), temp); 114 115 } else { 116 117 this.files = new Files("assets", temp); 118 119 } 120 121 this.lang = new LanguageManager(this.files, this.language); 122 this.lang.load(); 123 124 } 125 126 public override void reload() { 127 128 TOMLDocument document; 129 130 if(exists(filename)) document = parseTOML(cast(string)read(filename)); 131 132 T get(T)(TOMLValue target) { 133 static if(is(T == string)) { 134 return target.str; 135 } else static if(isArray!T) { 136 T ret; 137 foreach(value ; target.array) { 138 ret ~= get!(typeof(ret[0]))(value); 139 } 140 return ret; 141 } else static if(isAssociativeArray!T) { 142 T ret; 143 foreach(key, value; target.table) { 144 ret[key] = get!(typeof(ret[""]))(value); 145 } 146 return ret; 147 } else static if(is(T == bool)) { 148 return target.type == TOML_TYPE.TRUE; 149 } else static if(isFloatingPoint!T) { 150 return cast(T)target.floating; 151 } else static if(isIntegral!T) { 152 return cast(T)target.integer; 153 } else static if(is(T == UUID)) { 154 return parseUUID(get!string(target)); 155 } else static if(is(T == JSONValue)) { 156 return toJSON(target); //TODO handle conversion errors 157 } else static if(is(T == Config.Hub.Address)) { 158 return convertAddress(target.str); 159 } else { 160 static assert(0); 161 } 162 } 163 164 TOMLValue getValue(TOMLValue[string] table, const(string)[] keys) { 165 auto value = keys[0] in table; 166 if(value) { 167 if(keys.length == 1) return *value; 168 else return getValue((*value).table, keys[1..$]); // throws exception if not a table 169 } else { 170 throw new TOMLException(keys[0] ~ " not in table"); 171 } 172 } 173 174 void set(T)(ref T value, const(string)[] keys...) { 175 static if(is(T == string) || isIntegral!T || isFloatingPoint!T || is(T == bool) || isArray!T) { 176 // override using --key=value 177 immutable option = "--" ~ keys.join("-"); 178 foreach(i, arg; args) { 179 if(arg.startsWith(option ~ "=")) { 180 args = args[0..i] ~ args[i+1..$]; 181 try { 182 immutable data = arg[option.length+1..$]; 183 static if(isArray!T && !is(T == string)) { 184 T _value; 185 alias A = typeof(_value[0]); 186 foreach(s_data ; split(data, ",")) { 187 static if(is(A == Config.Hub.Address)) _value ~= convertAddress(s_data); 188 else _value ~= to!A(s_data); 189 } 190 value = _value; 191 } else { 192 value = to!T(data); 193 } 194 } catch(ConvException) {} 195 return; 196 } 197 } 198 } 199 try { 200 value = get!T(getValue(document.table, keys)); 201 } catch(TOMLException) {} 202 } 203 204 void setProtocols(ref uint[] value, uint[] all, uint[] latest, const(string)[] keys...) { 205 string s; 206 set(s, keys); 207 if(s == "all" || s == "*") value = all; 208 else if(s == "latest") value = latest; 209 else set(value, keys); 210 } 211 212 set(this.uuid, "uuid"); 213 set(this.language, "language"); 214 215 if(isHub) with(this.hub = new Config.Hub()) { 216 217 set(displayName, "display-name"); 218 set(edu, "edu"); 219 set(bedrock.enabled, "bedrock", "enabled"); 220 set(bedrock.motd, "bedrock", "motd"); 221 set(bedrock.addresses, "bedrock", "addresses"); 222 setProtocols(bedrock.protocols, supportedBedrockProtocols, latestBedrockProtocols, "bedrock", "accepted-protocols"); 223 set(allowVanillaPlayers, "bedrock", "allow-vanilla-players"); 224 set(java.enabled, "java", "enabled"); 225 set(java.motd, "java", "motd"); 226 set(java.addresses, "java", "addresses"); 227 setProtocols(java.protocols, supportedJavaProtocols, latestJavaProtocols, "java", "accepted-protocols"); 228 set(query, "query-enabled"); 229 set(serverIp, "server-ip"); 230 set(favicon, "favicon"); 231 set(acceptedNodes, "hncom", "accepted-addresses"); 232 set(hncomPassword, "hncom", "password"); 233 set(maxNodes, "hncom", "node-limit"); 234 set(hncomPort, "hncom", "port"); 235 set(social, "social"); 236 237 // unlimited nodes 238 string unlimited; 239 set(unlimited, "hncom", "node-limit"); 240 if(unlimited.toLower() == "unlimited") maxNodes = 0; 241 242 } 243 244 if(isNode) with(this.node = new Config.Node()) { 245 246 // override default 247 transferCommand = type != ConfigType.default_; 248 249 set(name, "hub", "name"); 250 set(password, "hub", "password"); 251 set(ip, "hub", "ip"); 252 set(port, "hub", "port"); 253 set(main, "hub", "main"); 254 set(bedrock.enabled, "bedrock", "enabled"); 255 setProtocols(bedrock.protocols, supportedBedrockProtocols, latestBedrockProtocols, "bedrock", "accepted-protocols"); 256 set(java.enabled, "java", "enabled"); 257 setProtocols(java.protocols, supportedJavaProtocols, latestJavaProtocols, "java", "accepted-protocols"); 258 set(maxPlayers, "max-players"); 259 set(gamemode, "world", "gamemode"); 260 set(difficulty, "world", "difficulty"); 261 set(depleteHunger, "world", "deplete-hunger"); 262 set(doDaylightCycle, "world", "do-daylight-cycle"); 263 set(doEntityDrops, "world", "do-entity-drops"); 264 set(doFireTick, "world", "do-fire-tick"); 265 set(doScheduledTicks, "world", "do-scheduled-ticks"); 266 set(doWeatherCycle, "world", "do-weather-cycle"); 267 set(naturalRegeneration, "natural-regeneration"); 268 set(pvp, "world", "pvp"); 269 set(randomTickSpeed, "world", "random-tick-speed"); 270 set(viewDistance, "view-distance"); 271 272 // commands 273 foreach(command ; Commands) { 274 set(mixin(command ~ "Command"), "command", command); 275 } 276 277 // unlimited players 278 string unlimited; 279 set(unlimited, "max-players"); 280 if(unlimited.toLower() == "unlimited") maxPlayers = 0; 281 282 } 283 284 if(!exists(filename)) this.save(); 285 286 } 287 288 public override void save() { 289 290 string serializeProtocols(uint[] protocols, uint[] all, uint[] latest) { 291 if(protocols == latest) return `"latest"`; 292 else if(protocols == all) return `"all"`; 293 else return to!string(protocols); 294 } 295 296 // is this needed? 297 if(this.hub is null) this.hub = new Config.Hub(); 298 if(this.node is null) this.node = new Config.Node(); 299 300 string file = "# " ~ Software.name ~ " " ~ Software.fullVersion ~ " configuration file" ~ newline ~ newline; 301 302 file ~= "uuid = \"" ~ this.uuid.toString().toUpper() ~ "\"" ~ newline; 303 if(isHub) file ~= "display-name = \"" ~ this.hub.displayName ~ "\"" ~ newline; 304 if(isNode) file ~= "max-players = " ~ (this.node.maxPlayers == 0 ? "\"unlimited\"" : to!string(this.node.maxPlayers)) ~ newline; 305 file ~= "language = \"" ~ this.language ~ "\"" ~ newline; 306 if(isHub) file ~= "server-ip = \"" ~ this.hub.serverIp ~ "\"" ~ newline; 307 if(isHub) file ~= "query-enabled = " ~ to!string(this.hub.query) ~ newline; 308 if(isHub && !this.hub.edu) file ~= "favicon = \"" ~ this.hub.favicon ~ "\"" ~ newline; 309 if(isHub) file ~= "social = {}" ~ newline; //TODO 310 if(isHub) with(this.hub.bedrock) { 311 file ~= newline ~ "[bedrock]" ~ newline; 312 file ~= "enabled = " ~ to!string(enabled) ~ newline; 313 file ~= "motd = \"" ~ motd ~ "\"" ~ newline; 314 file ~= "online-mode = false" ~ newline; 315 file ~= "addresses = " ~ addressString(addresses) ~ newline; 316 file ~= "accepted-protocols = " ~ serializeProtocols(protocols, supportedBedrockProtocols, latestBedrockProtocols) ~ newline; 317 if(this.hub.edu) file ~= newline ~ "allow-vanilla-players = " ~ to!string(this.hub.allowVanillaPlayers); 318 } 319 if(isHub && !this.hub.edu) with(this.hub.java) { 320 file ~= newline ~ "[java]" ~ newline; 321 file ~= "enabled = " ~ to!string(enabled) ~ newline; 322 file ~= "motd = \"" ~ motd ~ "\"" ~ newline; 323 file ~= "online-mode = false" ~ newline; 324 file ~= "addresses = " ~ addressString(addresses) ~ newline; 325 file ~= "accepted-protocols = " ~ serializeProtocols(protocols, supportedJavaProtocols, latestJavaProtocols) ~ newline; 326 } 327 if(type == ConfigType.node) with(this.node) { 328 file ~= newline ~ "[hub]" ~ newline; 329 file ~= "name = \"" ~ name ~ "\"" ~ newline; 330 file ~= "password = \"" ~ password ~ "\"" ~ newline; 331 file ~= "ip = \"" ~ ip ~ "\"" ~ newline; 332 file ~= "port = " ~ to!string(port) ~ newline; 333 file ~= "main = " ~ to!string(main) ~ newline; 334 } 335 if(type == ConfigType.node) with(this.node.bedrock) { 336 file ~= newline ~ "[bedrock]" ~ newline; 337 file ~= "enabled = " ~ to!string(enabled) ~ newline; 338 file ~= "accepted-protocols = " ~ serializeProtocols(protocols, supportedBedrockProtocols, latestBedrockProtocols) ~ newline; 339 } 340 if(type == ConfigType.node) with(this.node.java) { 341 file ~= newline ~ "[java]" ~ newline; 342 file ~= "enabled = " ~ to!string(enabled) ~ newline; 343 file ~= "accepted-protocols = " ~ serializeProtocols(protocols, supportedJavaProtocols, latestJavaProtocols) ~ newline; 344 } 345 if(isNode) with(this.node) { 346 file ~= newline ~ "[world]" ~ newline; 347 file ~= "gamemode = \"" ~ to!string(gamemode) ~ "\"" ~ newline; 348 file ~= "difficulty = \"" ~ to!string(difficulty) ~ "\"" ~ newline; 349 file ~= "deplete-hunger = " ~ to!string(depleteHunger) ~ newline; 350 file ~= "do-daylight-cycle = " ~ to!string(doDaylightCycle) ~ newline; 351 file ~= "do-entity-drops = " ~ to!string(doEntityDrops) ~ newline; 352 file ~= "do-fire-tick = " ~ to!string(doFireTick) ~ newline; 353 file ~= "do-scheduled-ticks = " ~ to!string(doScheduledTicks) ~ newline; 354 file ~= "do-weather-cycle = " ~ to!string(doWeatherCycle) ~ newline; 355 file ~= "natural-regeneration = " ~ to!string(naturalRegeneration) ~ newline; 356 file ~= "pvp = " ~ to!string(pvp) ~ newline; 357 file ~= "random-tick-speed = " ~ to!string(randomTickSpeed) ~ newline; 358 file ~= "view-distance = " ~ to!string(viewDistance) ~ newline; 359 } 360 if(isNode) with(this.node) { 361 file ~= newline ~ "[command]" ~ newline; 362 foreach(command ; Commands) { 363 file ~= command ~ " = " ~ to!string(mixin(command ~ "Command")) ~ newline; 364 } 365 } 366 if(type == ConfigType.hub) with(this.hub) { 367 file ~= newline ~ "[hncom]" ~ newline; 368 file ~= "accepted-addresses = " ~ to!string(acceptedNodes) ~ newline; 369 file ~= "password = \"" ~ hncomPassword ~ "\"" ~ newline; 370 file ~= "node-limit = " ~ (maxNodes == 0 ? "\"unlimited\"" : to!string(maxNodes)) ~ newline; 371 file ~= "port = " ~ to!string(hncomPort); 372 file ~= newline; 373 } 374 375 write(filename, file); 376 377 } 378 379 }; 380 381 if(hasArg("--reset") || hasArg("-r")) remove(filename); 382 383 config.load(); 384 385 if(hasArg("--update-config") || hasArg("-uc")) config.save(); 386 387 return config; 388 389 } 390 391 class ReleaseFiles : Files { 392 393 public this(string assets, string temp) { 394 super(assets, temp); 395 } 396 397 public override inout bool hasPluginAsset(Plugin plugin, string file) { 398 return exists(this.assets ~ "plugins" ~ dirSeparator ~ plugin.name ~ dirSeparator ~ file); 399 } 400 401 public override inout void[] readPluginAsset(Plugin plugin, string file) { 402 return read(this.assets ~ "plugins" ~ dirSeparator ~ plugin.name ~ dirSeparator ~ file); 403 } 404 405 } 406 407 class CompressedFiles : Files { 408 409 private ZipArchive archive; 410 411 public this(ZipArchive archive, string temp) { 412 super("", temp); 413 this.archive = archive; 414 } 415 416 public override inout bool hasAsset(string file) { 417 return !!(convert(file) in (cast()this.archive).directory); 418 } 419 420 public override inout void[] readAsset(string file) { 421 auto member = (cast()this.archive).directory[convert(file)]; 422 if(member.expandedData.length != member.expandedSize) (cast()this.archive).expand(member); 423 return cast(void[])member.expandedData; 424 } 425 426 public override inout bool hasPluginAsset(Plugin plugin, string file) { 427 return this.hasAsset("plugins/" ~ plugin.name ~ "/" ~ file); 428 } 429 430 public override inout void[] readPluginAsset(Plugin plugin, string file) { 431 return this.readAsset("plugins/" ~ plugin.name ~ "/" ~ file); 432 } 433 434 private static string convert(string file) { 435 version(Windows) file = file.replace("\\", "/"); 436 while(file[$-1] == '/') file = file[0..$-1]; 437 return file; 438 } 439 440 } 441 442 /** 443 * Throws: ConvException 444 */ 445 Config.Hub.Address convertAddress(string str) { 446 Config.Hub.Address address; 447 auto s = str.split(":"); 448 if(s.length >= 2) { 449 address.port = to!ushort(s[$-1]); 450 address.ip = s[0..$-1].join(":"); 451 if(address.ip.startsWith("[")) address.ip = address.ip[1..$]; 452 if(address.ip.endsWith("]")) address.ip = address.ip[0..$-1]; 453 } 454 return address; 455 } 456 457 string addressString(Config.Hub.Address[] addresses) { 458 string[] ret; 459 foreach(address ; addresses) { 460 ret ~= address.toString(); 461 } 462 return to!string(ret); 463 }