001 /** 002 * This software is provided under the terms of the Minecraft Forge Public 003 * License v1.0. 004 */ 005 006 package net.minecraftforge.common; 007 008 import java.io.*; 009 import java.text.DateFormat; 010 import java.util.ArrayList; 011 import java.util.Arrays; 012 import java.util.Collection; 013 import java.util.Date; 014 import java.util.Locale; 015 import java.util.Map; 016 import java.util.TreeMap; 017 import java.util.regex.Matcher; 018 import java.util.regex.Pattern; 019 020 import com.google.common.base.CharMatcher; 021 import com.google.common.base.Splitter; 022 import com.google.common.collect.Maps; 023 024 import cpw.mods.fml.common.FMLCommonHandler; 025 import cpw.mods.fml.common.FMLLog; 026 import cpw.mods.fml.common.Loader; 027 import cpw.mods.fml.relauncher.FMLInjectionData; 028 029 import net.minecraft.src.Block; 030 import net.minecraft.src.Item; 031 import static net.minecraftforge.common.Property.Type.*; 032 033 /** 034 * This class offers advanced configurations capabilities, allowing to provide 035 * various categories for configuration variables. 036 */ 037 public class Configuration 038 { 039 private static boolean[] configMarkers = new boolean[Item.itemsList.length]; 040 private static final int ITEM_SHIFT = 256; 041 private static final int MAX_BLOCKS = 4096; 042 043 public static final String CATEGORY_GENERAL = "general"; 044 public static final String CATEGORY_BLOCK = "block"; 045 public static final String CATEGORY_ITEM = "item"; 046 public static final String ALLOWED_CHARS = "._-"; 047 public static final String DEFAULT_ENCODING = "UTF-8"; 048 public static final String CATEGORY_SPLITTER = "."; 049 public static final String NEW_LINE; 050 private static final Pattern CONFIG_START = Pattern.compile("START: \"([^\\\"]+)\""); 051 private static final Pattern CONFIG_END = Pattern.compile("END: \"([^\\\"]+)\""); 052 public static final CharMatcher allowedProperties = CharMatcher.JAVA_LETTER_OR_DIGIT.or(CharMatcher.anyOf(ALLOWED_CHARS)); 053 private static Configuration PARENT = null; 054 055 File file; 056 057 public Map<String, ConfigCategory> categories = new TreeMap<String, ConfigCategory>(); 058 private Map<String, Configuration> children = new TreeMap<String, Configuration>(); 059 060 private boolean caseSensitiveCustomCategories; 061 public String defaultEncoding = DEFAULT_ENCODING; 062 private String fileName = null; 063 public boolean isChild = false; 064 065 static 066 { 067 Arrays.fill(configMarkers, false); 068 NEW_LINE = System.getProperty("line.separator"); 069 } 070 071 public Configuration(){} 072 073 /** 074 * Create a configuration file for the file given in parameter. 075 */ 076 public Configuration(File file) 077 { 078 this.file = file; 079 String basePath = ((File)(FMLInjectionData.data()[6])).getAbsolutePath().replace(File.separatorChar, '/').replace("/.", ""); 080 String path = file.getAbsolutePath().replace(File.separatorChar, '/').replace("/./", "/").replace(basePath, ""); 081 if (PARENT != null) 082 { 083 PARENT.setChild(path, this); 084 isChild = true; 085 } 086 else 087 { 088 fileName = path; 089 load(); 090 } 091 } 092 093 public Configuration(File file, boolean caseSensitiveCustomCategories) 094 { 095 this(file); 096 this.caseSensitiveCustomCategories = caseSensitiveCustomCategories; 097 } 098 099 /** 100 * Gets or create a block id property. If the block id property key is 101 * already in the configuration, then it will be used. Otherwise, 102 * defaultId will be used, except if already taken, in which case this 103 * will try to determine a free default id. 104 */ 105 public Property getBlock(String key, int defaultID) { return getBlock(CATEGORY_BLOCK, key, defaultID, null); } 106 public Property getBlock(String key, int defaultID, String comment) { return getBlock(CATEGORY_BLOCK, key, defaultID, comment); } 107 public Property getBlock(String category, String key, int defaultID) { return getBlockInternal(category, key, defaultID, null, 256, Block.blocksList.length); } 108 public Property getBlock(String category, String key, int defaultID, String comment) { return getBlockInternal(category, key, defaultID, comment, 256, Block.blocksList.length); } 109 110 /** 111 * Special version of getBlock to be used when you want to garentee the ID you get is below 256 112 * This should ONLY be used by mods who do low level terrain generation, or ones that add new 113 * biomes. 114 * EXA: ExtraBiomesXL 115 * 116 * Normal mods such as '50 new ores' do not need to be below 256 so should use the normal getBlock 117 */ 118 public Property getTerrainBlock(String category, String key, int defaultID, String comment) 119 { 120 return getBlockInternal(category, key, defaultID, comment, 0, 256); 121 } 122 123 private Property getBlockInternal(String category, String key, int defaultID, String comment, int lower, int upper) 124 { 125 Property prop = get(category, key, -1, comment); 126 127 if (prop.getInt() != -1) 128 { 129 configMarkers[prop.getInt()] = true; 130 return prop; 131 } 132 else 133 { 134 if (defaultID < lower) 135 { 136 FMLLog.warning( 137 "Mod attempted to get a block ID with a default in the Terrain Generation section, " + 138 "mod authors should make sure there defaults are above 256 unless explicitly needed " + 139 "for terrain generation. Most ores do not need to be below 256."); 140 FMLLog.warning("Config \"%s\" Category: \"%s\" Key: \"%s\" Default: %d", fileName, category, key, defaultID); 141 defaultID = upper; 142 } 143 144 if (Block.blocksList[defaultID] == null && !configMarkers[defaultID]) 145 { 146 prop.value = Integer.toString(defaultID); 147 configMarkers[defaultID] = true; 148 return prop; 149 } 150 else 151 { 152 for (int j = upper - 1; j > 0; j--) 153 { 154 if (Block.blocksList[j] == null && !configMarkers[j]) 155 { 156 prop.value = Integer.toString(j); 157 configMarkers[j] = true; 158 return prop; 159 } 160 } 161 162 throw new RuntimeException("No more block ids available for " + key); 163 } 164 } 165 } 166 167 public Property getItem(String key, int defaultID) { return getItem(CATEGORY_ITEM, key, defaultID, null); } 168 public Property getItem(String key, int defaultID, String comment) { return getItem(CATEGORY_ITEM, key, defaultID, comment); } 169 public Property getItem(String category, String key, int defaultID) { return getItem(category, key, defaultID, null); } 170 171 public Property getItem(String category, String key, int defaultID, String comment) 172 { 173 Property prop = get(category, key, -1, comment); 174 int defaultShift = defaultID + ITEM_SHIFT; 175 176 if (prop.getInt() != -1) 177 { 178 configMarkers[prop.getInt() + ITEM_SHIFT] = true; 179 return prop; 180 } 181 else 182 { 183 if (defaultID < MAX_BLOCKS - ITEM_SHIFT) 184 { 185 FMLLog.warning( 186 "Mod attempted to get a item ID with a default value in the block ID section, " + 187 "mod authors should make sure there defaults are above %d unless explicitly needed " + 188 "so that all block ids are free to store blocks.", MAX_BLOCKS - ITEM_SHIFT); 189 FMLLog.warning("Config \"%s\" Category: \"%s\" Key: \"%s\" Default: %d", fileName, category, key, defaultID); 190 } 191 192 if (Item.itemsList[defaultShift] == null && !configMarkers[defaultShift] && defaultShift > Block.blocksList.length) 193 { 194 prop.value = Integer.toString(defaultID); 195 configMarkers[defaultShift] = true; 196 return prop; 197 } 198 else 199 { 200 for (int x = Item.itemsList.length - 1; x >= ITEM_SHIFT; x--) 201 { 202 if (Item.itemsList[x] == null && !configMarkers[x]) 203 { 204 prop.value = Integer.toString(x - ITEM_SHIFT); 205 configMarkers[x] = true; 206 return prop; 207 } 208 } 209 210 throw new RuntimeException("No more item ids available for " + key); 211 } 212 } 213 } 214 215 public Property get(String category, String key, int defaultValue) 216 { 217 return get(category, key, defaultValue, null); 218 } 219 220 public Property get(String category, String key, int defaultValue, String comment) 221 { 222 Property prop = get(category, key, Integer.toString(defaultValue), comment, INTEGER); 223 if (!prop.isIntValue()) 224 { 225 prop.value = Integer.toString(defaultValue); 226 } 227 return prop; 228 } 229 230 public Property get(String category, String key, boolean defaultValue) 231 { 232 return get(category, key, defaultValue, null); 233 } 234 235 public Property get(String category, String key, boolean defaultValue, String comment) 236 { 237 Property prop = get(category, key, Boolean.toString(defaultValue), comment, BOOLEAN); 238 if (!prop.isBooleanValue()) 239 { 240 prop.value = Boolean.toString(defaultValue); 241 } 242 return prop; 243 } 244 245 public Property get(String category, String key, double defaultValue) 246 { 247 return get(category, key, defaultValue, null); 248 } 249 250 public Property get(String category, String key, double defaultValue, String comment) 251 { 252 Property prop = get(category, key, Double.toString(defaultValue), comment, DOUBLE); 253 if (!prop.isDoubleValue()) 254 { 255 prop.value = Double.toString(defaultValue); 256 } 257 return prop; 258 } 259 260 public Property get(String category, String key, String defaultValue) 261 { 262 return get(category, key, defaultValue, null); 263 } 264 265 public Property get(String category, String key, String defaultValue, String comment) 266 { 267 return get(category, key, defaultValue, comment, STRING); 268 } 269 270 public Property get(String category, String key, String[] defaultValue) 271 { 272 return get(category, key, defaultValue, null); 273 } 274 275 public Property get(String category, String key, String[] defaultValue, String comment) 276 { 277 return get(category, key, defaultValue, comment, STRING); 278 } 279 280 public Property get(String category, String key, int[] defaultValue) 281 { 282 return get(category, key, defaultValue, null); 283 } 284 285 public Property get(String category, String key, int[] defaultValue, String comment) 286 { 287 String[] values = new String[defaultValue.length]; 288 for (int i = 0; i < defaultValue.length; i++) 289 { 290 values[i] = Integer.toString(defaultValue[i]); 291 } 292 293 Property prop = get(category, key, values, comment, INTEGER); 294 if (!prop.isIntList()) 295 { 296 prop.valueList = values; 297 } 298 299 return prop; 300 } 301 302 public Property get(String category, String key, double[] defaultValue) 303 { 304 return get(category, key, defaultValue, null); 305 } 306 307 public Property get(String category, String key, double[] defaultValue, String comment) 308 { 309 String[] values = new String[defaultValue.length]; 310 for (int i = 0; i < defaultValue.length; i++) 311 { 312 values[i] = Double.toString(defaultValue[i]); 313 } 314 315 Property prop = get(category, key, values, comment, DOUBLE); 316 317 if (!prop.isDoubleList()) 318 { 319 prop.valueList = values; 320 } 321 322 return prop; 323 } 324 325 public Property get(String category, String key, boolean[] defaultValue) 326 { 327 return get(category, key, defaultValue, null); 328 } 329 330 public Property get(String category, String key, boolean[] defaultValue, String comment) 331 { 332 String[] values = new String[defaultValue.length]; 333 for (int i = 0; i < defaultValue.length; i++) 334 { 335 values[i] = Boolean.toString(defaultValue[i]); 336 } 337 338 Property prop = get(category, key, values, comment, BOOLEAN); 339 340 if (!prop.isBooleanList()) 341 { 342 prop.valueList = values; 343 } 344 345 return prop; 346 } 347 348 public Property get(String category, String key, String defaultValue, String comment, Property.Type type) 349 { 350 if (!caseSensitiveCustomCategories) 351 { 352 category = category.toLowerCase(Locale.ENGLISH); 353 } 354 355 ConfigCategory cat = getCategory(category); 356 357 if (cat.containsKey(key)) 358 { 359 Property prop = cat.get(key); 360 361 if (prop.getType() == null) 362 { 363 prop = new Property(prop.getName(), prop.value, type); 364 cat.set(key, prop); 365 } 366 367 prop.comment = comment; 368 return prop; 369 } 370 else if (defaultValue != null) 371 { 372 Property prop = new Property(key, defaultValue, type); 373 cat.set(key, prop); 374 prop.comment = comment; 375 return prop; 376 } 377 else 378 { 379 return null; 380 } 381 } 382 383 public Property get(String category, String key, String[] defaultValue, String comment, Property.Type type) 384 { 385 if (!caseSensitiveCustomCategories) 386 { 387 category = category.toLowerCase(Locale.ENGLISH); 388 } 389 390 ConfigCategory cat = getCategory(category); 391 392 if (cat.containsKey(key)) 393 { 394 Property prop = cat.get(key); 395 396 if (prop.getType() == null) 397 { 398 prop = new Property(prop.getName(), prop.value, type); 399 cat.set(key, prop); 400 } 401 402 prop.comment = comment; 403 404 return prop; 405 } 406 else if (defaultValue != null) 407 { 408 Property prop = new Property(key, defaultValue, type); 409 prop.comment = comment; 410 cat.set(key, prop); 411 return prop; 412 } 413 else 414 { 415 return null; 416 } 417 } 418 419 public boolean hasCategory(String category) 420 { 421 return categories.get(category) != null; 422 } 423 424 public boolean hasKey(String category, String key) 425 { 426 ConfigCategory cat = categories.get(category); 427 return cat != null && cat.containsKey(key); 428 } 429 430 public void load() 431 { 432 if (PARENT != null && PARENT != this) 433 { 434 return; 435 } 436 437 BufferedReader buffer = null; 438 try 439 { 440 if (file.getParentFile() != null) 441 { 442 file.getParentFile().mkdirs(); 443 } 444 445 if (!file.exists() && !file.createNewFile()) 446 { 447 return; 448 } 449 450 if (file.canRead()) 451 { 452 UnicodeInputStreamReader input = new UnicodeInputStreamReader(new FileInputStream(file), defaultEncoding); 453 defaultEncoding = input.getEncoding(); 454 buffer = new BufferedReader(input); 455 456 String line; 457 ConfigCategory currentCat = null; 458 Property.Type type = null; 459 ArrayList<String> tmpList = null; 460 int lineNum = 0; 461 String name = null; 462 463 while (true) 464 { 465 lineNum++; 466 line = buffer.readLine(); 467 468 if (line == null) 469 { 470 break; 471 } 472 473 Matcher start = CONFIG_START.matcher(line); 474 Matcher end = CONFIG_END.matcher(line); 475 476 if (start.matches()) 477 { 478 fileName = start.group(1); 479 categories = new TreeMap<String, ConfigCategory>(); 480 continue; 481 } 482 else if (end.matches()) 483 { 484 fileName = end.group(1); 485 Configuration child = new Configuration(); 486 child.categories = categories; 487 this.children.put(fileName, child); 488 continue; 489 } 490 491 int nameStart = -1, nameEnd = -1; 492 boolean skip = false; 493 boolean quoted = false; 494 495 for (int i = 0; i < line.length() && !skip; ++i) 496 { 497 if (Character.isLetterOrDigit(line.charAt(i)) || ALLOWED_CHARS.indexOf(line.charAt(i)) != -1 || (quoted && line.charAt(i) != '"')) 498 { 499 if (nameStart == -1) 500 { 501 nameStart = i; 502 } 503 504 nameEnd = i; 505 } 506 else if (Character.isWhitespace(line.charAt(i))) 507 { 508 // ignore space charaters 509 } 510 else 511 { 512 switch (line.charAt(i)) 513 { 514 case '#': 515 skip = true; 516 continue; 517 518 case '"': 519 if (quoted) 520 { 521 quoted = false; 522 } 523 if (!quoted && nameStart == -1) 524 { 525 quoted = true; 526 } 527 break; 528 529 case '{': 530 name = line.substring(nameStart, nameEnd + 1); 531 String qualifiedName = ConfigCategory.getQualifiedName(name, currentCat); 532 533 currentCat = categories.get(qualifiedName); 534 if (currentCat == null) 535 { 536 currentCat = new ConfigCategory(name, null); 537 categories.put(qualifiedName, currentCat); 538 } 539 name = null; 540 541 break; 542 543 case '}': 544 currentCat = currentCat.parent; 545 break; 546 547 case '=': 548 name = line.substring(nameStart, nameEnd + 1); 549 550 if (currentCat == null) 551 { 552 throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum)); 553 } 554 555 Property prop = new Property(name, line.substring(i + 1), type, true); 556 i = line.length(); 557 558 currentCat.set(name, prop); 559 560 break; 561 562 case ':': 563 type = Property.Type.tryParse(line.substring(nameStart, nameEnd + 1).charAt(0)); 564 nameStart = nameEnd = -1; 565 break; 566 567 case '<': 568 if (tmpList != null) 569 { 570 throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum)); 571 } 572 573 name = line.substring(nameStart, nameEnd + 1); 574 575 if (currentCat == null) 576 { 577 throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum)); 578 } 579 580 tmpList = new ArrayList<String>(); 581 582 break; 583 584 case '>': 585 if (tmpList == null) 586 { 587 throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum)); 588 } 589 590 currentCat.set(name, new Property(name, tmpList.toArray(new String[tmpList.size()]), type)); 591 name = null; 592 tmpList = null; 593 type = null; 594 break; 595 596 default: 597 throw new RuntimeException(String.format("Unknown character '%s' in '%s:%d'", line.charAt(i), fileName, lineNum)); 598 } 599 } 600 } 601 602 if (quoted) 603 { 604 throw new RuntimeException(String.format("Unmatched quote in '%s:%d'", fileName, lineNum)); 605 } 606 else if (tmpList != null) 607 { 608 tmpList.add(line.trim()); 609 } 610 } 611 } 612 } 613 catch (IOException e) 614 { 615 e.printStackTrace(); 616 } 617 finally 618 { 619 if (buffer != null) 620 { 621 try 622 { 623 buffer.close(); 624 } catch (IOException e){} 625 } 626 } 627 } 628 629 public void save() 630 { 631 if (PARENT != null && PARENT != this) 632 { 633 PARENT.save(); 634 return; 635 } 636 637 try 638 { 639 if (file.getParentFile() != null) 640 { 641 file.getParentFile().mkdirs(); 642 } 643 644 if (!file.exists() && !file.createNewFile()) 645 { 646 return; 647 } 648 649 if (file.canWrite()) 650 { 651 FileOutputStream fos = new FileOutputStream(file); 652 BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(fos, defaultEncoding)); 653 654 buffer.write("# Configuration file" + NEW_LINE); 655 buffer.write("# Generated on " + DateFormat.getInstance().format(new Date()) + NEW_LINE + NEW_LINE); 656 657 if (children.isEmpty()) 658 { 659 save(buffer); 660 } 661 else 662 { 663 for (Map.Entry<String, Configuration> entry : children.entrySet()) 664 { 665 buffer.write("START: \"" + entry.getKey() + "\"" + NEW_LINE); 666 entry.getValue().save(buffer); 667 buffer.write("END: \"" + entry.getKey() + "\"" + NEW_LINE + NEW_LINE); 668 } 669 } 670 671 buffer.close(); 672 fos.close(); 673 } 674 } 675 catch (IOException e) 676 { 677 e.printStackTrace(); 678 } 679 } 680 681 private void save(BufferedWriter out) throws IOException 682 { 683 for (ConfigCategory cat : categories.values()) 684 { 685 if (!cat.isChild()) 686 { 687 cat.write(out, 0); 688 out.newLine(); 689 } 690 } 691 } 692 693 public ConfigCategory getCategory(String category) 694 { 695 ConfigCategory ret = categories.get(category); 696 697 if (ret == null) 698 { 699 if (category.contains(CATEGORY_SPLITTER)) 700 { 701 String[] hierarchy = category.split("\\"+CATEGORY_SPLITTER); 702 ConfigCategory parent = categories.get(hierarchy[0]); 703 704 if (parent == null) 705 { 706 parent = new ConfigCategory(hierarchy[0]); 707 categories.put(parent.getQualifiedName(), parent); 708 } 709 710 for (int i = 1; i < hierarchy.length; i++) 711 { 712 String name = ConfigCategory.getQualifiedName(hierarchy[i], parent); 713 ConfigCategory child = categories.get(name); 714 715 if (child == null) 716 { 717 child = new ConfigCategory(hierarchy[i], parent); 718 categories.put(name, child); 719 } 720 721 ret = child; 722 parent = child; 723 } 724 } 725 else 726 { 727 ret = new ConfigCategory(category); 728 categories.put(category, ret); 729 } 730 } 731 732 return ret; 733 } 734 735 public void addCustomCategoryComment(String category, String comment) 736 { 737 if (!caseSensitiveCustomCategories) 738 category = category.toLowerCase(Locale.ENGLISH); 739 getCategory(category).setComment(comment); 740 } 741 742 private void setChild(String name, Configuration child) 743 { 744 if (!children.containsKey(name)) 745 { 746 children.put(name, child); 747 } 748 else 749 { 750 Configuration old = children.get(name); 751 child.categories = old.categories; 752 child.fileName = old.fileName; 753 } 754 } 755 756 public static void enableGlobalConfig() 757 { 758 PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg")); 759 PARENT.load(); 760 } 761 762 public static class UnicodeInputStreamReader extends Reader 763 { 764 private final InputStreamReader input; 765 private final String defaultEnc; 766 767 public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException 768 { 769 defaultEnc = encoding; 770 String enc = encoding; 771 byte[] data = new byte[4]; 772 773 PushbackInputStream pbStream = new PushbackInputStream(source, data.length); 774 int read = pbStream.read(data, 0, data.length); 775 int size = 0; 776 777 int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF); 778 int bom24 = bom16 << 8 | (data[2] & 0xFF); 779 int bom32 = bom24 << 8 | (data[3] & 0xFF); 780 781 if (bom24 == 0xEFBBBF) 782 { 783 enc = "UTF-8"; 784 size = 3; 785 } 786 else if (bom16 == 0xFEFF) 787 { 788 enc = "UTF-16BE"; 789 size = 2; 790 } 791 else if (bom16 == 0xFFFE) 792 { 793 enc = "UTF-16LE"; 794 size = 2; 795 } 796 else if (bom32 == 0x0000FEFF) 797 { 798 enc = "UTF-32BE"; 799 size = 4; 800 } 801 else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE, 802 { //but if anyone ever runs across a 32LE file, i'd like to disect it. 803 enc = "UTF-32LE"; 804 size = 4; 805 } 806 807 if (size < read) 808 { 809 pbStream.unread(data, size, read - size); 810 } 811 812 this.input = new InputStreamReader(pbStream, enc); 813 } 814 815 public String getEncoding() 816 { 817 return input.getEncoding(); 818 } 819 820 @Override 821 public int read(char[] cbuf, int off, int len) throws IOException 822 { 823 return input.read(cbuf, off, len); 824 } 825 826 @Override 827 public void close() throws IOException 828 { 829 input.close(); 830 } 831 } 832 }