code:
package com.lilithsthrone.main;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import com.lilithsthrone.controller.MainController;
import com.lilithsthrone.controller.TooltipUpdateThread;
import com.lilithsthrone.game.Game;
import com.lilithsthrone.game.Properties;
import com.lilithsthrone.game.PropertyValue;
import com.lilithsthrone.game.character.CharacterImportSetting;
import com.lilithsthrone.game.character.CharacterUtils;
import com.lilithsthrone.game.character.PlayerCharacter;
import com.lilithsthrone.game.character.body.valueEnums.Femininity;
import com.lilithsthrone.game.character.gender.Gender;
import com.lilithsthrone.game.character.persona.NameTriplet;
import com.lilithsthrone.game.character.quests.QuestLine;
import com.lilithsthrone.game.character.race.RaceStage;
import com.lilithsthrone.game.character.race.Subspecies;
import com.lilithsthrone.game.dialogue.DialogueNode;
import com.lilithsthrone.game.dialogue.DialogueNodeType;
import com.lilithsthrone.game.dialogue.responses.Response;
import com.lilithsthrone.game.dialogue.story.CharacterCreation;
import com.lilithsthrone.game.dialogue.utils.OptionsDialogue;
import com.lilithsthrone.game.inventory.enchanting.TFEssence;
import com.lilithsthrone.game.sex.Sex;
import com.lilithsthrone.utils.Colour;
import com.lilithsthrone.utils.CreditsSlot;
import com.lilithsthrone.world.Generation;
import com.lilithsthrone.world.WorldType;
import com.lilithsthrone.world.places.PlaceType;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.image.Image;
import javafx.scene.layout.Pane;
import javafx.scene.text.Font;
import javafx.stage.Stage;

/**
 * @since 0.1.0
 * @version 0.3.5.5
 * @author Innoxia
 */
public class Main extends Application {

	public static Game game;
	public static Sex sexEngine;

	public static MainController mainController;

	public static Scene mainScene;

	public static Stage primaryStage;

	public static final String AUTHOR = "Innoxia & community";
	public static final String GAME_NAME = "Modded Lilith's Throne";
	public static final String VERSION_NUMBER = "0.3.5.4";
	public static final String VERSION_DESCRIPTION = "Alpha: Lolipop Showtime 3.33";

	/**
	 * To turn it on, just add -Ddebug=true to java's VM options. (You should be able to do this in Eclipse through Run::Run Configurations...::Arguments tab::VM Arguments).
	 * Help page: https://help.eclipse.org/mars/index.jsp?topic=%2Forg.eclipse.pde.doc.user%2Fguide%2Ftools%2Flaunchers%2Farguments.htm
	 *  Or, from the command line java -Ddebug=true -jar LilithsThrone.jar
	 */
	public final static boolean DEBUG = Boolean.valueOf(System.getProperty("debug", "false"));

	public static final Image WINDOW_IMAGE = new Image("/com/lilithsthrone/res/images/windowIcon32.png");

	private static Properties properties;

	public static String patchNotes =

		"
"
			+ "Hello again!"
		+ "
"

		+ "
"
			+ "Sorry about how long this version has taken to get out."
			+ " I've hit a bit of a rough patch with Axel's quest, and as such it isn't quite ready just yet, but it will be in the next version."
		+ "
"

		+ "
"

		+ "
"
			+ "Thank you all for playing Lilith's Throne, and a very big thank you to all of you who support development by reporting bugs, making PRs, or backing me on SubscribeStar!"
			+ " If you wanted to ask me any specific questions about the game, you can either find me on my blog, or on the Lilith's Throne Discord. You can find a link to the discord on my blog. ^^"
		+ "
"

		+ "
"

		+ "<list>"
			+ "
v0.3.5.4
"
			+"
Engine:
"
			+"
Improved handling of turn updates, which has reduced the amount of lag encountered each turn, especially when the time would pass into a new hour.
"
			+"
Improved speed of body generation for randomly-generated NPCs.
"
			+"
Moved more text content into external txt files.
"

			+"
Gameplay:
"
			+"
Day/night time is now based on real-world sunrise/sunset times for the coordinates 51.4934 N, 0.0000 E, so days are shorter in winter, and longer in summer.
"
			+"
Added a minor darkening effect to the map when in civil & nautical twilight, and a major darkening effect when in astronomical twilight or night time.
"
			+"
Added sunrise/sunset info to the clock tooltip.
"
			+"
Sleeping in your room or a friend's apartment is now to the next sunrise or sunset, instead of to 07:00 or 21:00.
"
			+"
All shops in the Shopping Arcade are now only open between 06:00 and 22:00.
"
			+"
Candi's desk is only open between 09:00 and 17:00.
"
			+"
Zaranix's home is locked up between 22:00 and 06:00.
"
			+"
The harpy matriarchs go to sleep between 22:00 and 06:00.
"
			+"
Lilaya and Rose go to bed at 22:00 and return to the lab at 06:00. The 'lab assistant' and 'test subject' slave jobs are no longer available during these times.
"
			+"
Breastfeeding/Nipple-kissing is now an ongoing action, with the target's milk (if they are lactating) being drunk every turn. Also improved descriptions of these actions to factor in nipple-shape.
"
			+"
You can now take a bath in your room's ensuite, which cleans your companions and applies a regenerative-boosting status effect. (Descriptions are still a 'TODO')
"
			+"
Roxy now smokes and has this reflected in her dialogue. She also no longer wears lipstick (which I thought might be weird on a greater rat-morph face).
"
			+"
Added some unique sex actions in Roxy's facesitting scene: her smoking; her teasing you; a customer briefly interrupting her. Her sex scene now also correctly starts with you eating her out, and her dress pulled up.
"

			+"
Items:
"
			+"
Added cigarettes as individual items and as packs of 20. Smoking cigarettes provides a status effect which applies +10 aura and -5 health.
"
			+"
Added  'rat skull' tattoo.
"
			+"
Slightly improved the short sleeve shirt icon and added dye colours for the buttons & label.
"
			+"
Added 'Kerambit' weapon (one handed, sold by Vicky).
"
			+"
Added 'Gemstone ring' (finger slot, sold by Nyan).
"
			+"
Added 'Demonstone necklace' (neck slot, sold by Nyan).
"
			+"
Moved internal finger and neck clothing into the res folder.
"
			+"
Added secondary colours to heart and ankh necklaces, and you can now equip these necklaces (as well as sun and snowflake necklaces) into the wrist slot.
"
			+"
Improved bell collar and added secondary and tertiary dye colours for metal parts.
"

			+"
Other:
"
			+"
You can now equip unique clothing & weapons onto NPCs. If these NPCs are removed from the game at any point, the unique clothing/weapons which they have equipped are returned to your inventory.
"
			+"
Reduced the arcane lightning globe's base damage from 25 to 15, and reduced its value from 500,000 to 250,000.
"
			+"
If you have an alligator-morph vagina, you now get the egg-laying birthing variation.
"
			+"
Changed the 'rose' gift item into an item of clothing, which can now be worn in the 'hair' slot. Any roses already in your game will be automatically converted into their clothing counterpart when you load into this version.
"
			+"
Added 'pitch black' for eye colours.
"
			+"
Added 'bun' and 'chignon' hair styles, and reduced the chance of NPCs spawning with 'unusual' hair styles (from 20% to 10%).
"
			+"
Added a debug consumable item which turns the drinker into a youko, and adds a tail to drinkers who are already a youko.
"
			+"
Added 'footjobs' to the sex stats screen (only displayed if you have foot content turned on). Anal, nipple-penetration, and urethral sex stats are now hidden if you have the associated content disabled.
"
			+"
Added personality setting tab to debug menu.
"
			+"
Clothing & weapons enchanted to remove corruption now cost enchantment stability, equal to the value of corruption removed.
"
			+"
NPCs who have a positive desire towards the 'cum stud' or 'impregnation' fetishes, and are either selfish or in the rough pace, will now refuse to wear condoms in sex.
"
			+"
Added a 'fast travel' tab which is available everywhere within the shopping arcade.
"
			+"
You can now suggest a job (or change of job) to any of the friendly occupants either living with you or in their own apartment.
"
			+"
Added an indication that the main quest currently ends at the 'Finding the youko' quest in the quest log.
"
			+"
Added more slave greeting and sleeping dialogue variations for when they are assigned to your room. Also added slave-related quick/thorough wash descriptions.
"
			+"
Added office tile to gambling den map.
"
			+"
Reduced 'vaccuum' spell's 'secondary voids' effects from -20 to -5 energy shielding, and -25 to -15 critical damage.
"

			+"
Forced-transformation content:
"
			+"
NPCs now have a gender, subspecies, and race-stage preference, instead of a fully pre-generated body preference. This should help to cut down on save file size.
"
			+"
NPCs will no longer try to transform you if you are a demon, nor try to transform you into a demon or imp (although they will still force non-racial transformations onto you). Youko tail addition/removal transformations have also been disabled.
"
			+"
Fixed issues with forced TF potions not applying genital changes correctly, and the NPC getting stuck on trying to change your genitals even when you had the genitals they wanted.
"
			+"
Fixed issue with NPCs trying to transform away any strap-on that you were wearing.
"
			+"
Improved NPCs transformation selection for the potions they force on you.
"
			+"
Added more dialogue for each part that an NPC transforms.
"
			+"
Forced transformation content availability is no longer linked to the non-con content toggle.
"
			+"
NPCs who want to transform you into a slime will now be able to do so without getting stuck.
"
			+"
Demons in the dark alleyway tiles have had forced transformation behaviour added to them. (They usually end up preferring demons, however, in which case they will only attempt to transform your gender.)
"
			+"
Added penis girth, ball size, body size, muscle, vagina capacity, ass capacity, and ass wetness to forced transformation variations.
"
			+"
Increased non-racial TF amounts in a single forced-TF (such as ass size or height being increased/reduced by larger amounts).
"
			+"
NPCs will now transform both you and your companion.
"
			+"
NPCs will now apply fetish changes as well as body transformations in the same scene.
"
			+"
NPCs will no longer TF if mouth blocked.
"

			+"
Random encounters:
"
			+"
Changed and expanded dialogue for post-defeat transformations and sex for: Dominion alleyway; Harpy nest; Submission tunnels; Demon alleyway; Cavern attacker/slime/bat.
"
			+"
Added quick TF options (in the combat's post-victory actions) to all these scenes.
"
			+"
NPCs will now choose whether to have sex with you, your companion, or both of you after defeating you.
"
			+"
NPCs who dislike raping will no longer rape.
"

			+"
Bugs:
"
			+"
Typo and parsing fixes.
"
			+"
Fixed issue with the Office's 'Occupancy ledger' sometimes causing a background error to be thrown, which was sometimes breaking the game.
"
			+"
Fixed similar issue where accessing inventories through the slave list would sometimes result in not being able to close the inventory, leading to numerous background errors being thrown.
"
			+"
Fixed another issue with the 'Occupancy Ledger', where inspecting a friendly occupant would cause the game to throw several background errors.
"
			+"
Fixed bug where slaves assigned to sex-related jobs would have their virginity loss descriptions reset.
"
			+"
Jules no longer wears socks on his hoofs.
"
			+"
Fixed bug where previously-creampied orifices would not be recognised as being lubricated by cum at the start of sex.
"
			+"
Fixed bug where room upgrades were not affecting obedience & affection gains for slaves working there.
"
			+"
Any weapons that are unequipped during sex are now correctly re-equipped when sex ends.
"
			+"
Fixed bug where backing out of the inventory screen during sex would sometimes return you to the scene you were in before sex, while not correctly ending sex, causing the game to break.
"
			+"
Fixed yet more bugs in stuttering characters' speech effects.
"
			+"
Fixed minor issue where checking imp gang inventories defaulted the response tab to the empty 'selected item' one, rather than the 'overview' one.
"
			+"
Fixed bug where breast/nipple interactions would be disabled when receiving a blowjob in the sixty-nine position.
"
			+"
Fixed bug where clothing tooltips would display the rarity of unidentified items, letting you know if they were jinxed or not without first identifying them.
"
			+"
Fixed 'Mystery Kink' not being recognised as a fetish-giving item, and 'Glowing Mushrooms' not being recognised as transformative, which was allowing some unique NPCs to consume them during sex.
"
			+"
Fixed issue where offspring could sometimes spawn with 'heterochromatic' eyes that both had the same colour.
"
			+"
Fixed bug causing incorrect descriptions when removing horns.
"
			+"
Fixed incorrect action labels in debug's race reset screen.
"
			+"
Fixed bug where buying items from Vicky would clear the entire stack of that item from her inventory the next time you opened her trade menu.
"
			+"
Fixed Takahashi not having 3 youko tails as intended.
"
			+"
Fixed Fury offspring (half-demon harpies) spawning in Dominion (they now only spawn in the harpy nests). Also fixed offspring of non-Submission half-demon subspecies spawning in Submission's tunnels.
"
			+"
Fixed equipped piercings displaying a 'No tattoo' tooltip when hovering over them in tattoo view mode.
"
			+"
Fixed tertiary dye colours not displaying for several items of clothing.
"
			+"
If, in a previous version, Brax has somehow bugged out after completing the side quest to obtain him, he will correctly have his ownership transferred to you when you load into this version.
"
			+"
Fixed item and clothing encyclopedia entries not being added if you used an item from an NPC's inventory, or equipped clothing onto an NPC.
"
			+"
Fixed incorrectly describing Kate's breasts as E-cup.
"
			+"
Fixed footjob climax tattoo counter not working.
"
			+"
Fixed bug where footjob sex stats were not being saved/loaded correctly.
"
			+"
Fixed sex bug where characters couldn't start oral actions while they were receiving a footjob.
"
			+"
Fixed bug where performing/receiving foot-related actions would result in a huge lust drain per turn if neither participant had a positive desire towards foot fetishes. This will not affect NPCs' decision to use foot actions (which they prefer to avoid unless they have an associated fetish).
"
			+"
Fixed bug where the 'Localised Storm' upgrade for the Arcane Cloud spell would affect enemies of the target, instead of all their allies.
"
			+"
Fixed bug where clothing was able to be equipped in sex into slots which were not supposed to be available.
"
			+"
You can no longer equip clothing during sex into inventory slots that are involved in an ongoing action.
"
			+"
Fixed Rose's handholding sex scene missing the mouth-finger interactions which were present in older versions.
"
			+"
Fixed 'Suck clit' sex action having arousal increment values reversed for the performer/target.
"
			+"
Fixed enslavement collars not spawning with the enslavement enchantment.
"
			+"
Fixed dates before 0CE being displayed as a negative value, and are now correctly displayed as 'xxxx BCE'.
"
			+"
Fixed bug in the flow of dialogue when talking to Candi for the first time about Brax.
"
			+"
Fixed clothing enchantments which reduce corruption being regarded as 'bad' enchantments.
"
			+"
Fixed broken condom equip behaviour for cultists, dark alleyway demon attackers, Kate, and Ralph.
"
			+"
Fixed bug where slaves who were given the permission to use you would only do so when they were supposed to be at work, instead of when they had free time (set to 'idle').
"
			+"
Actions using a parnter's clit are no longer displayed as greyed-out actions, until you know that the partner has a vagina.
"
			+"
Fixed another cause of the bug where slaves jumping you when sleeping would not work correctly.
"
			+"
Fixed some issues with the lactation fetish special tease attack, and made it available to characters who have crotch boobs which are lactating.
"
			+"
When starting sex, the active response tab should no longer start on one that has no actions available in it.
"
			+"
Fixed some incorrect descriptions of the 'choke' action in multiple-character sex scenes.
"
			+"
Fixed bug where character home icons would not be shown on any tiles that bordered the map's lower or left edges.
"
			+"
Fixed some references to Jhortrax fathering imps instead of half-demons.
"
			+"
Girlcum fluid modifiers are now described in a character's appearance page.
"
			+"
Characters performing oral on a vagina will now correctly ingest girlcum (once the vagina is lubricated by girlcum).
"
			+"
Fixed bug where incorrect dialogue would be displayed when slaves jumped you for sex after sleeping.
"
			+"
Fixed some issues with half-demon subspecies detection, which should have resolved the unusual self-transformation behaviour encountered after turning yourself into a half-demon through the debug menu.
"
			+"
Fixed alligator hair-scales and slime hair being called by the wrong name.
"
			+"
Fixed branching quests not correctly saving/loading the quest route you selected (was just affecting the slime queen quest, but wasn't having any impact on gameplay).
"
			+"
Blaze and Crystal (the Slime Queen's guards) are now correctly identified in the code as being siblings (but only if you have incest content turned on).
"
			+"
Fixed bug where post-sex dialogue after beating and then submitting to the imps in the Dark Siren's fortress would be the 'defeated' variant instead of the correct 'victory' variant.
"
		+ "</list>"

	;

	public static String disclaimer = "
You must read and agree to the following in order to play this game!
"

			+ "
This game is a fictional text-based erotic RPG. All content contained within this game forms part of a fictional universe that is not related to real-life places, people or events.

"

			+ " All of the characters that appear in this story are fictional entities who have given their consent to appear and act in this story."
			+ " If you find yourself concerned for the characters in the story then please be reassured that they are all consenting adults who are speaking lines from a script."
			+ " None of the characters portrayed within this game were or are being harmed in any way during the construction or execution of this game."
			+ " Every character in the game is at least 18 years of age (or is magically the legal age needed to appear in erotic literature in whatever country you are playing this)."
			+  " No character in this game is blood-related to any other; once again, they are simply speaking lines from a script.

"

			+ "By agreeing to this disclaimer and playing this game you agree to be exposed to graphic sexual and adult content that is presented as part of the game's fictional universe."
			+ " Such content consists of, but is not limited to; graphic depictions of sex, inter-species sex (with fantasy creatures), sexual transformation,"
			+ " rape fantasy/implied lack of consent, mild physical violence, sexual violence, and drug use.
"
			+ "Extreme fetish content such as gore/extreme violence, scat, and under/questionable age, is not a part of this game.

"

			+ "By agreeing to this disclaimer and playing this game you also agree that you are at least 18 years of age,"
			+ " or at least the legal age for you to purchase and view pornographic material in your country if that age is over 18.

"

			+ "As a final note, the creators of this game wish to stress that the content presented within is entirely fictional and does not reflect any of their personal views or opinions."
			+ " This game has been made in the spirit of creating a piece of artistic interactive literature, and it is imperative that you maintain a clear distinction between reality and the fictional events depicted in this game.
";

	public static List<CreditsSlot> credits = new ArrayList<>();

	// World generation:
	public static Generation gen;
	@Override
	public void start(Stage primaryStage) throws Exception {

		CheckForDataDirectory();
		CheckForResFolder();

		credits.add(new CreditsSlot("Anonymous", "", 99, 99, 99, 99));

		credits.add(new CreditsSlot("Kyle S P", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("Paradoxiso", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("luka_fateburn", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("CinnamonSuccubus", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("Luka_Fateburn", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("Phlarx", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("Moro", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("Neo", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("Abaddon_TMZ", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("ForgottenOne", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("Velvet", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("GentleTark", "", 0, 0, 0, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("QW", "", 0, 0, 0, 0, Subspecies.DEMON));

		credits.add(new CreditsSlot("Adhana Konker", "", 0, 0, 3, 0));
		credits.add(new CreditsSlot("Akira", "", 0, 0, 0, 2));
		credits.add(new CreditsSlot("Aleskah", "", 0, 0, 0, 1));
		credits.add(new CreditsSlot("Lexi <3", "", 0, 0, 0, 1));
		credits.add(new CreditsSlot("Alvinsimon", "", 0, 0, 3, 0));
		credits.add(new CreditsSlot("dragonouv2019", "", 0, 0, 4, 0));
		credits.add(new CreditsSlot("Aklev", "", 0, 0, 3, 0));
		credits.add(new CreditsSlot("AndroidSam", "", 0, 0, 2, 0));
		credits.add(new CreditsSlot("48days", "", 0, 0, 2, 17));
		credits.add(new CreditsSlot("Spaghetti Code", "", 0, 0, 2, 3));
		credits.add(new CreditsSlot("Anonymous_Platypus", "", 0, 0, 4, 0));
		credits.add(new CreditsSlot("Apthydragon", "", 0, 0, 9, 0));
		credits.add(new CreditsSlot("Archan9el S117", "", 0, 0, 0, 12));
		credits.add(new CreditsSlot("SchALLieS", "", 0, 0, 4, 12));
		credits.add(new CreditsSlot("Argmoe", "", 0, 0, 14, 0));
		credits.add(new CreditsSlot("HoneyNutQueerios", "", 0, 0, 16, 0));
		credits.add(new CreditsSlot("Arkhan", "", 0, 0, 6, 0));
		credits.add(new CreditsSlot("Ash", "", 0, 1, 0, 10));
		credits.add(new CreditsSlot("Jack Cloudie", "", 0, 1, 10, 0));
		credits.add(new CreditsSlot("b00marrows", "", 0, 1, 5, 0));
		credits.add(new CreditsSlot("Deimios", "", 0, 0, 3, 7));
		credits.add(new CreditsSlot("Baz GoldenClaw", "", 0, 0, 17, 0));
		credits.add(new CreditsSlot("Mhaak", "", 0, 0, 0, 9));
		credits.add(new CreditsSlot("FidelPinochetov", "", 0, 0, 0, 13));
		credits.add(new CreditsSlot("Tieria", "", 0, 0, 1, 0));
		credits.add(new CreditsSlot("Runehood66", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("Krissy2017", "", 0, 0, 2, 6));
		credits.add(new CreditsSlot("Blackcanine", "", 0, 0, 17, 0));
		credits.add(new CreditsSlot("Blackheart", "", 0, 0, 1, 3));
		credits.add(new CreditsSlot("Blacktouch", "", 0, 0, 2, 17));
		credits.add(new CreditsSlot("BlakLite", "", 0, 0, 9, 0));
		credits.add(new CreditsSlot("Blue999", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("BlueWolf", "", 0, 0, 5, 0));
		credits.add(new CreditsSlot("Brandon Stach", "", 0, 0, 15, 0));
		credits.add(new CreditsSlot("BreakerB", "", 0, 0, 6, 0));
		credits.add(new CreditsSlot("BRobort", "", 0, 0, 9, 0));
		credits.add(new CreditsSlot("BloodsailXXII", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("Burt", "", 0, 0, 6, 0));
		credits.add(new CreditsSlot("Atroykus", "", 0, 0, 0, 6));
		credits.add(new CreditsSlot("Calrak", "", 0, 0, 0, 18, Subspecies.DEMON));
		credits.add(new CreditsSlot("CancerMage", "", 0, 0, 12, 0));
		credits.add(new CreditsSlot("Captain_Sigmus", "", 0, 0, 7, 0));
		credits.add(new CreditsSlot("Casper "Cdaser" D.", "", 0, 0, 10, 0));
		credits.add(new CreditsSlot("CelestialNightmare", "", 0, 0, 0, 15));
		credits.add(new CreditsSlot("Sxythe", "", 0, 0, 0, 2));
		credits.add(new CreditsSlot("Lexi the slut", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("Chattyneko", "", 0, 0, 10, 0));
		credits.add(new CreditsSlot("Vmpireassassin (Chloe)", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("cinless", "", 0, 0, 0, 13));
		credits.add(new CreditsSlot("crashtestdummy", "", 0, 0, 9, 5));
		credits.add(new CreditsSlot("Crimson", "", 0, 0, 0, 17));
		credits.add(new CreditsSlot("CrowCorvus", "", 0, 0, 9, 0));
		credits.add(new CreditsSlot("Cryostorm", "", 0, 0, 15, 0));
		credits.add(new CreditsSlot("Cursed Rena", "", 0, 0, 1, 12));
		credits.add(new CreditsSlot("Cynical-Cy", "", 0, 0, 11, 0));
		credits.add(new CreditsSlot("Dace617", "", 0, 0, 0, 4));
		credits.add(new CreditsSlot("Saladofstones", "", 0, 0, 7, 0));
		credits.add(new CreditsSlot("Dan", "", 0, 1, 0, 15));
		credits.add(new CreditsSlot("Hikaru Lightbringer", "", 0, 0, 5, 0));
		credits.add(new CreditsSlot("Griff", "", 0, 0, 3, 0));
		credits.add(new CreditsSlot("Daniel D Magnan", "", 0, 0, 10, 0));
		credits.add(new CreditsSlot("DarKaz", "", 0, 0, 0, 2));
		credits.add(new CreditsSlot("Darthsawyer", "", 0, 0, 6, 0));
		credits.add(new CreditsSlot("Yllarius", "", 0, 0, 2, 0));
		credits.add(new CreditsSlot("DeadEyesSee", "", 0, 0, 11, 0));
		credits.add(new CreditsSlot("DeadMasterZero", "", 0, 0, 8, 6));
		credits.add(new CreditsSlot("CruellerTwo24 ", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("Demonicgamer666", "", 0, 0, 0, 8));
		credits.add(new CreditsSlot("John Scarlet", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("Desgax", "", 0, 0, 7, 0));
		credits.add(new CreditsSlot("Destont", "", 0, 0, 16, 0));
		credits.add(new CreditsSlot("Nordo", "", 0, 0, 0, 1));
		credits.add(new CreditsSlot("Di", "", 0, 0, 0, 2));
		credits.add(new CreditsSlot("DLI", "", 0, 0, 0, 14));
		credits.add(new CreditsSlot("suka", "", 0, 0, 19, 0));
		credits.add(new CreditsSlot("rinoskin", "", 0, 0, 0, 6));
		credits.add(new CreditsSlot("Alatar", "", 0, 0, 0, 2));
		credits.add(new CreditsSlot("Elmsdor", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("EnigmaticYoshi", "", 0, 0, 19, 0));
		credits.add(new CreditsSlot("Endless", "", 0, 0, 6, 2));
		credits.add(new CreditsSlot("Gr33n B3ans", "", 0, 0, 0, 2));
		credits.add(new CreditsSlot("Erin Kyan", "", 0, 0, 13, 0));
		credits.add(new CreditsSlot("Avandemine", "", 0, 0, 1, 11));
		credits.add(new CreditsSlot("Evit", "", 0, 0, 5, 0));
		credits.add(new CreditsSlot("F. Rowan", "", 0, 0, 7, 0));
		credits.add(new CreditsSlot("Farseeker", "", 0, 0, 8, 0));
		credits.add(new CreditsSlot("pupslut felix", "", 0, 0, 0, 12));
		credits.add(new CreditsSlot("Fenrakk101", "", 0, 0, 11, 0));
		credits.add(new CreditsSlot("Fiona", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("ForeverFree2MeTaMax", "", 0, 0, 14, 0));
		credits.add(new CreditsSlot("FossorTumulus", "", 0, 0, 11, 0));
		credits.add(new CreditsSlot("William E", "", 0, 0, 0, 15));
		credits.add(new CreditsSlot("Freekingamer", "", 0, 0, 0, 10));
		credits.add(new CreditsSlot("fun_bot", "", 0, 0, 0, 3));
		credits.add(new CreditsSlot("Niki Parks", "", 0, 0, 19, 0));
		credits.add(new CreditsSlot("Garkylal", "", 0, 0, 10, 0));
		credits.add(new CreditsSlot("Georgio154", "", 0, 0, 1, 6));
		credits.add(new CreditsSlot("glocknar", "", 0, 0, 11, 0));
		credits.add(new CreditsSlot("Goldmember", "", 0, 0, 0, 3));
		credits.add(new CreditsSlot("Grakcnar", "", 0, 0, 9, 0));
		credits.add(new CreditsSlot("Grave Indignation", "", 0, 0, 4, 0));
		credits.add(new CreditsSlot("GravyRainbow", "", 0, 0, 0, 6));
		credits.add(new CreditsSlot("WodashGSJ", "", 0, 0, 15, 0));
		credits.add(new CreditsSlot("Aceofspades", "", 0, 0, 2, 0));
		credits.add(new CreditsSlot("Assiyalos", "", 0, 0, 9, 0));
		credits.add(new CreditsSlot("Hedgehog", "", 0, 0, 0, 10));
		credits.add(new CreditsSlot("Helyriel", "", 0, 0, 15, 0));
		credits.add(new CreditsSlot("Jatch", "", 0, 0, 11, 0));
		credits.add(new CreditsSlot("JaminGold", "", 0, 0, 4, 0));
		credits.add(new CreditsSlot("Jason Paterson", "", 0, 0, 0, 9));
		credits.add(new CreditsSlot("no1skill", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("Bocaj91", "", 0, 0, 0, 13));
		credits.add(new CreditsSlot("Krejil", "", 0, 0, 13, 0));
		credits.add(new CreditsSlot("Joeybear", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("Eushully", "", 0, 0, 0, 12));
		credits.add(new CreditsSlot("Joshua Walter", "", 0, 0, 0, 1));
		credits.add(new CreditsSlot("Garth614", "", 0, 0, 0, 15));
		credits.add(new CreditsSlot("Justicia Anthony", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("HerrKommissar11", "", 0, 0, 1, 4));
		credits.add(new CreditsSlot("Kaerea", "", 0, 0, 7, 0));
		credits.add(new CreditsSlot("Kaleb the Wise", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("Karlimero", "", 0, 0, 0, 8));
		credits.add(new CreditsSlot("Tappi", "", 0, 0, 7, 0));
		credits.add(new CreditsSlot("KazukiNero", "", 0, 0, 12, 0));
		credits.add(new CreditsSlot("Kelly999", "", 0, 1, 12, 0));
		credits.add(new CreditsSlot("kenshin5491", "", 0, 0, 18, 0));
		credits.add(new CreditsSlot("Kestrel", "", 0, 0, 19, 0));
		credits.add(new CreditsSlot("BlueVulcan", "", 0, 0, 6, 0));
		credits.add(new CreditsSlot("KingofKings", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("Kiroberos", "", 0, 0, 0, 17));
		credits.add(new CreditsSlot("Kernog", "", 0, 0, 1, 0));
		credits.add(new CreditsSlot("Knight-Lord Xander", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("Krozoz", "", 0, 0, 2, 0, Subspecies.DEMON));
		credits.add(new CreditsSlot("Krulin", "", 0, 0, 1, 0));
		credits.add(new CreditsSlot("Kyralon", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("Chris Turpin", "", 0, 0, 17, 0));
		credits.add(new CreditsSlot("Lee Thompson", "", 0, 0, 14, 0));
		credits.add(new CreditsSlot("Leob", "", 0, 0, 10, 4));
		credits.add(new CreditsSlot("Pallid", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("ilderon", "", 0, 0, 5, 0));
		credits.add(new CreditsSlot("Littlemankitten", "", 0, 0, 0, 12));
		credits.add(new CreditsSlot("LadyofFoxes", "", 0, 0, 2, 0));
		credits.add(new CreditsSlot("Mr L", "", 0, 0, 4, 1));
		credits.add(new CreditsSlot("loveless", "", 0, 0, 0, 19, Subspecies.DEMON));
		credits.add(new CreditsSlot("Vaddex", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("Kitsune Lyn", "", 0, 0, 0, 10));
		credits.add(new CreditsSlot("Manwhore", "", 0, 0, 0, 3));
		credits.add(new CreditsSlot("Beldamon", "", 0, 0, 0, 10));
		credits.add(new CreditsSlot("matchsticks", "", 0, 0, 10, 0));
		credits.add(new CreditsSlot("masterpuppet", "", 0, 0, 15, 0));
		credits.add(new CreditsSlot("Nightmare", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("AlphaOneBravo", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("Max Nobody", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("Mega", "", 0, 0, 11, 0));
		credits.add(new CreditsSlot("Mia Montane", "", 0, 0, 0, 3, Subspecies.DEMON));
		credits.add(new CreditsSlot("Mora", "", 0, 0, 4, 0));
		credits.add(new CreditsSlot("Muhaku", "", 0, 0, 9, 0));
		credits.add(new CreditsSlot("Neximus", "", 0, 0, 10, 0));
		credits.add(new CreditsSlot("Mylerra", "", 0, 0, 10, 0));
		credits.add(new CreditsSlot("Kobu", "", 0, 0, 0, 14));
		credits.add(new CreditsSlot("Natemare", "", 0, 0, 8, 0));
		credits.add(new CreditsSlot("IreCobra", "", 0, 0, 10, 0));
		credits.add(new CreditsSlot("NeonRaven94", "", 0, 0, 0, 9));
		credits.add(new CreditsSlot("Neon Swaglord Chen", "", 0, 0, 4, 0));
		credits.add(new CreditsSlot("Nexusman", "", 0, 0, 11, 0));
		credits.add(new CreditsSlot("SisterFister420", "", 0, 0, 0, 10));
		credits.add(new CreditsSlot("Nick LaBlue", "", 0, 0, 16, 0));
		credits.add(new CreditsSlot("Kvernik", "", 0, 0, 6, 0));
		credits.add(new CreditsSlot("Niko", "", 0, 0, 16, 0));
		credits.add(new CreditsSlot("Nnxx", "", 0, 1, 3, 2));
		credits.add(new CreditsSlot("NorwegianMonster", "", 0, 0, 0, 6));
		credits.add(new CreditsSlot("Seo Leifthrasir", "", 0, 0, 0, 6));
		credits.add(new CreditsSlot("Odd8Ball", "", 0, 0, 0, 18));
		credits.add(new CreditsSlot("Party Commissar", "", 0, 0, 4, 13));
		credits.add(new CreditsSlot("Patrik Grönlund", "", 0, 0, 2, 0));
		credits.add(new CreditsSlot("Totes Amazeballs", "", 0, 0, 0, 3));
		credits.add(new CreditsSlot("Rohsie", "", 0, 0, 0, 10));
		credits.add(new CreditsSlot("P.", "", 0, 0, 0, 4));
		credits.add(new CreditsSlot("BLKCandy", "", 0, 0, 12, 0));
		credits.add(new CreditsSlot("Pierre Mura", "", 0, 0, 0, 11));
		credits.add(new CreditsSlot("Pokys", "", 0, 0, 9, 0));
		credits.add(new CreditsSlot("PoyntFury", "", 0, 0, 1, 4, Subspecies.DEMON));
		credits.add(new CreditsSlot("QQQ", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("awrfyu_", "", 0, 0, 0, 7));
		credits.add(new CreditsSlot("Rakesh", "", 0, 0, 8, 0));
		credits.add(new CreditsSlot("R.W", "", 0, 3, 11, 0));
		credits.add(new CreditsSlot("Raruke", "", 0, 0, 3, 0));
		credits.add(new CreditsSlot("The Void Prince", "", 0, 0, 13, 0));
		credits.add(new CreditsSlot("Master's dumb bitch", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("Arpie", "", 0, 0, 0, 1));
		credits.add(new CreditsSlot("Reila Oda", "", 0, 0, 0, 9));
		credits.add(new CreditsSlot("Roarik", "", 0, 0, 0, 10));
		credits.add(new CreditsSlot("Dark_Lord", "", 0, 0, 2, 6));
		credits.add(new CreditsSlot("redwulfen", "", 0, 0, 0, 18));
		credits.add(new CreditsSlot("Roger Reyne", "", 0, 0, 0, 3));
		credits.add(new CreditsSlot("RogueRandom", "", 0, 0, 16, 0));
		credits.add(new CreditsSlot("Horagen81", "", 0, 0, 0, 13, Subspecies.DEMON));
		credits.add(new CreditsSlot("RyubosJ", "", 0, 0, 6, 0));
		credits.add(new CreditsSlot("Saladine the Legendary", "", 0, 0, 0, 15));
		credits.add(new CreditsSlot("Sand9k", "", 0, 0, 0, 11));
		credits.add(new CreditsSlot("Schande", "", 0, 0, 0, 11));
		credits.add(new CreditsSlot("Scith", "", 0, 0, 5, 0));
		credits.add(new CreditsSlot("Blue Kobold", "", 0, 0, 12, 0));
		credits.add(new CreditsSlot("sebasjac", "", 0, 0, 0, 2));
		credits.add(new CreditsSlot("S", "", 0, 0, 1, 18));
		credits.add(new CreditsSlot("Shas'O Dal'yth Kauyon Kais Taku", "", 0, 0, 19, 0));
		credits.add(new CreditsSlot("Crow Invictus", "", 0, 0, 18, 0));
		credits.add(new CreditsSlot("Sheltem", "", 0, 0, 16, 0));
		credits.add(new CreditsSlot("Shilou", "", 0, 0, 0, 1, Subspecies.DEMON));
		credits.add(new CreditsSlot("shrikes", "", 0, 0, 8, 2));
		credits.add(new CreditsSlot("Sig", "", 0, 0, 4, 0));
		credits.add(new CreditsSlot("Silentark", "", 0, 0, 16, 0));
		credits.add(new CreditsSlot("Sir beans", "", 0, 0, 10, 0));
		credits.add(new CreditsSlot("Sorter", "", 0, 0, 0, 9));
		credits.add(new CreditsSlot("Spectacular", "", 0, 0, 11, 0));
		credits.add(new CreditsSlot("Spencer", "", 0, 0, 0, 7));
		credits.add(new CreditsSlot("Spookermen", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("Starchiller", "", 0, 0, 0, 8));
		credits.add(new CreditsSlot("Steph", "", 0, 0, 9, 0));
		credits.add(new CreditsSlot("Strigon888", "", 0, 0, 0, 10));
		credits.add(new CreditsSlot("Suvarestin", "", 0, 0, 2, 0));
		credits.add(new CreditsSlot("Swift Shot", "", 0, 0, 14, 0));
		credits.add(new CreditsSlot("TalonMort", "", 0, 0, 17, 0));
		credits.add(new CreditsSlot("Tanall", "", 0, 1, 17, 0));
		credits.add(new CreditsSlot("Tanner D.", "", 0, 0, 0, 6));
		credits.add(new CreditsSlot("Terrance", "", 0, 0, 3, 0));
		credits.add(new CreditsSlot("Testostetyrone", "", 0, 0, 15, 0));
		credits.add(new CreditsSlot("The Brocenary", "", 0, 0, 0, 4));
		credits.add(new CreditsSlot("Jordan Aitken", "", 0, 0, 15, 0));
		credits.add(new CreditsSlot("T. Garou", "", 0, 0, 0, 12));
		credits.add(new CreditsSlot("xerton", "", 0, 0, 3, 0));
		credits.add(new CreditsSlot("Timmybond24", "", 0, 0, 0, 4));
		credits.add(new CreditsSlot("TKaempfer", "", 0, 0, 8, 0));
		credits.add(new CreditsSlot("Tom Clancy's Pro Skater", "", 0, 0, 6, 0));
		credits.add(new CreditsSlot("FreakyHydra", "", 0, 0, 0, 4));
		credits.add(new CreditsSlot("Kahvi_Toope", "", 0, 0, 0, 6));
		credits.add(new CreditsSlot("Torinir", "", 0, 0, 18, 0));
		credits.add(new CreditsSlot("Torsten015", "", 0, 0, 0, 18));
		credits.add(new CreditsSlot("TreenVall", "", 0, 0, 3, 0));
		credits.add(new CreditsSlot("triangleman", "", 0, 0, 16, 0));
		credits.add(new CreditsSlot("Antriad", "", 0, 0, 1, 13));
		credits.add(new CreditsSlot("Jess", "", 0, 0, 3, 0));
		credits.add(new CreditsSlot("Isidoros", "", 0, 0, 7, 0));
		credits.add(new CreditsSlot("SolarEidolon", "", 0, 0, 8, 0));
		credits.add(new CreditsSlot("Vaelin", "", 0, 0, 4, 14));
		credits.add(new CreditsSlot("vasadariu", "", 0, 0, 15, 0));
		credits.add(new CreditsSlot("waaaghkus", "", 0, 0, 19, 0));
		credits.add(new CreditsSlot("Venomy", "", 0, 0, 0, 5));
		credits.add(new CreditsSlot("iloveyouMiaoNiNi", "", 0, 0, 0, 14));
		credits.add(new CreditsSlot("Weegschaal", "", 0, 0, 0, 3));
		credits.add(new CreditsSlot("Whatever", "", 0, 0, 17, 0));
		credits.add(new CreditsSlot("Will Landrum", "", 0, 0, 0, 7));
		credits.add(new CreditsSlot("William Brown", "", 0, 0, 5, 2));
		credits.add(new CreditsSlot("Marys", "", 0, 0, 0, 10));
		credits.add(new CreditsSlot("CMPirate9867", "", 0, 0, 8, 0));
		credits.add(new CreditsSlot("Wolfrave", "", 0, 0, 6, 0));
		credits.add(new CreditsSlot("Wolfregis", "", 0, 0, 0, 19));
		credits.add(new CreditsSlot("Yuki_Sukafu", "", 0, 0, 5, 0));
		credits.add(new CreditsSlot("Nelson Adams", "", 0, 0, 12, 0));
		credits.add(new CreditsSlot("Zakarin", "", 0, 0, 0, 14, Subspecies.DEMON));
		credits.add(new CreditsSlot("Zaya", "", 0, 0, 5, 0));
		credits.add(new CreditsSlot("Zero_One", "", 0, 0, 4, 0));

		credits.sort(Comparator.comparing((CreditsSlot a) -> a.getName().toLowerCase()));

		Main.primaryStage = primaryStage;

		Main.primaryStage.focusedProperty().addListener(new ChangeListener<Boolean>() {
			@Override
			public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
				if(t) {
					TooltipUpdateThread.cancelThreads = true;
				}
			}
		});

		Main.primaryStage.getIcons().add(WINDOW_IMAGE);

		Main.primaryStage.setTitle(GAME_NAME+" " + VERSION_NUMBER + " " + VERSION_DESCRIPTION+(DEBUG?" (Debug Mode)":""));

		loadFonts();

		FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/lilithsthrone/res/fxml/main.fxml"));

		Pane pane = loader.load();

		mainScene = new Scene(pane);

		if (properties.hasValue(PropertyValue.lightTheme)) {
			mainScene.getStylesheets().add("/com/lilithsthrone/res/css/stylesheet_light.css");
		} else {
			mainScene.getStylesheets().add("/com/lilithsthrone/res/css/stylesheet.css");
		}

		mainController = loader.getController();
		Main.primaryStage.setScene(mainScene);
		Main.primaryStage.show();
		Main.game = new Game();
		Main.sexEngine = new Sex();

		loader = new FXMLLoader(getClass().getResource("/com/lilithsthrone/res/fxml/main.fxml"));
		try {
			if (Main.mainScene == null) {
				pane = loader.load();
				Main.mainController = loader.getController();

				Main.mainScene = new Scene(pane);
				if (Main.getProperties().hasValue(PropertyValue.lightTheme))
					Main.mainScene.getStylesheets().add("/com/lilithsthrone/res/css/stylesheet_light.css");
				else
					Main.mainScene.getStylesheets().add("/com/lilithsthrone/res/css/stylesheet.css");
			}

			Main.primaryStage.setScene(Main.mainScene);

		} catch (Exception e) {
			e.printStackTrace();
		}

		Main.game.setContent(new Response("", "", OptionsDialogue.MENU));

	}

	protected static void CheckForDataDirectory() {
		File dir = new File("data/");
		if(!dir.exists()) {
			Alert a = new Alert(AlertType.ERROR,
					"Unable to find the 'data' folder. Saving and error logging is disabled."
							+ "\nMake sure that you've extracted the game from the zip file, and that the file has write permissions."
							+ "\n(Please read section 'MISSING FOLDERS' in the README.txt file.)"
							+ "\nContinue?",
					ButtonType.YES, ButtonType.NO);
			a.showAndWait().ifPresent(response -> {
			     if (response == ButtonType.NO) {
			         System.exit(1);
			     }
			 });
		}
	}

	protected static void CheckForResFolder() {
		File dir = new File("res/");
		if(!dir.exists()) {
			Alert a = new Alert(AlertType.WARNING,
					"Could not find the 'res' folder. This WILL cause errors and present sections of missing text."
							+ "\nMake sure that you've extracted the game from the zip file, and that the file has write permissions."
							+ "\n(Please read section 'MISSING FOLDERS' in the README.txt file.)"
							+ "\nContinue?",
					ButtonType.YES, ButtonType.NO);
			a.showAndWait().ifPresent(response -> {
				if(response == ButtonType.NO) {
					System.exit(1);
				}
			});
		}
	}

	/**
	 * Attempts to load fallback fonts to make sure that they are available later. The size doesn't actually matter as
	 * the WebEngine will reload other sizes as required. The files referenced must persist until application shutdown.
	 *
	 * Do not call Font.getFamilies() prior to this as additional fonts must be loaded before the list is cached.
	 */
	protected void loadFonts() {
		// Load fallback for Calibri
		if (Font.loadFont(toUri("res/fonts/Carlito/Carlito-Regular.ttf"), 11) != null) {
			// Load variants
			Font.loadFont(toUri("res/fonts/Carlito/Carlito-Bold.ttf"), 11);
			Font.loadFont(toUri("res/fonts/Carlito/Carlito-BoldItalic.ttf"), 11);
			Font.loadFont(toUri("res/fonts/Carlito/Carlito-Italic.ttf"), 11);
		} else {
			System.err.println("Carlito font could not be loaded.");
		}

		// Load fallback for Verdana
		if (Font.loadFont(toUri("res/fonts/DejaVu Sans/DejaVuSans.ttf"), 12) != null) {
			// Load variants
			Font.loadFont(toUri("res/fonts/DejaVu Sans/DejaVuSans-Bold.ttf"), 12);
			Font.loadFont(toUri("res/fonts/DejaVu Sans/DejaVuSans-BoldOblique.ttf"), 12);
			Font.loadFont(toUri("res/fonts/DejaVu Sans/DejaVuSans-ExtraLight.ttf"), 12);
			Font.loadFont(toUri("res/fonts/DejaVu Sans/DejaVuSans-Oblique.ttf"), 12);
		} else {
			System.err.println("DejaVu Sans font could not be loaded.");
		}
	}

	/**
	 * Creates a URI string with spaces. The given path can be absolute or relative to the current working directory.
	 * @param path The path to convert
	 * @return A string containing a URI
	 */
	public static String toUri(String path) {
		return Paths.get(path).toUri().toString().replaceAll("%20", " ");
	}

	public static void main(String[] args) {

		// Create folders:
		File dir = new File("data/");
		dir.mkdir();
		dir = new File("data/saves");
		dir.mkdir();
		dir = new File("data/characters");
		dir.mkdir();

		// Open error log
		if(!DEBUG) {
			System.out.println("Printing to error.log");
			try {
				@SuppressWarnings("resource")
				PrintStream stream = new PrintStream("data/error.log");
				System.setErr(stream);
				System.err.println("Version: "+VERSION_NUMBER);
				System.err.println("Java: "+System.getProperty("java.version"));
//				System.err.println("OS: "+System.getProperty("os.name"));

			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}
		}
		// Load properties:
		if (new File("data/properties.xml").exists()) {
			try {
				properties = new Properties();
				properties.loadPropertiesFromXML();
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		} else {
			properties = new Properties();
			properties.savePropertiesAsXML();
		}

		launch(args);
	}

	/**
	 * Starts a completely new game. Runs a new World Generation.
	 */
	public static void startNewGame(DialogueNode startingDialogueNode) {

		Main.game = new Game();

		// Generate world:
		if (!(gen == null))
			if (gen.isRunning()) {
				gen.cancel();
			}

		gen = new Generation();

		gen.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
			@Override
			public void handle(WorkerStateEvent t) {

				FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/lilithsthrone/res/fxml/main.fxml"));
				Pane pane;
				try {
					if (Main.mainScene == null) {
						pane = loader.load();
						Main.mainController = loader.getController();

						Main.mainScene = new Scene(pane);
						if (Main.getProperties().hasValue(PropertyValue.lightTheme))
							Main.mainScene.getStylesheets().add("/com/lilithsthrone/res/css/stylesheet_light.css");
						else
							Main.mainScene.getStylesheets().add("/com/lilithsthrone/res/css/stylesheet.css");
					}

					Main.primaryStage.setScene(Main.mainScene);

				} catch (Exception e) {
					e.printStackTrace();
				}

				Main.game.setPlayer(new PlayerCharacter(new NameTriplet("Player"), 1, null, Gender.M_P_MALE, Subspecies.HUMAN, RaceStage.HUMAN, WorldType.MUSEUM, PlaceType.MUSEUM_ENTRANCE));

				Main.game.initNewGame(startingDialogueNode);

				Main.game.endTurn(0);
				//Main.mainController.processNewDialogue();
			}
		});
		new Thread(gen).start();
	}

	public static boolean isVersionOlderThan(String versionToCheck, String versionToCheckAgainst) {
		String[] v1 = versionToCheck.split("\\.");
		String[] v2 = versionToCheckAgainst.split("\\.");

		try {
			int maxLength = (v1.length > v2.length) ? v1.length : v2.length;
			for (int i = 0; i < maxLength; i++) {
				int v1i;
				int v2i;

				if(v1[1].charAt(0)=='1') { // Versions prior to 0.2.x used an old system of the format: 0.1.10.1 being a lower version than 0.1.9.1:
					v1i = (i < v1.length) ? Integer.valueOf((v1[i]+"00").substring(0, 3)) : 0;
					v2i = (i < v2.length) ? Integer.valueOf((v2[i]+"00").substring(0, 3)) : 0;

				} else { // Versions of 0.2.x and higher use a new system of the format: 0.2.10.1 being a higher version than 0.2.9.1:
					v1i = (i < v1.length) ? Integer.valueOf(v1[i]) : 0;
					v2i = (i < v2.length) ? Integer.valueOf(v2[i]) : 0;
				}

				if (v1i < v2i) {
					return true;
				} else if (v1i > v2i) {
					return false;
				}
			}

		} catch(Exception ex) {
			return true;
		}

		return false;
	}

	public static int getFontSize() {
		return properties.fontSize;
	}

	public static void setFontSize(int size) {
		properties.fontSize = size;
		properties.savePropertiesAsXML();
	}

	public static void quickSaveGame() {
		if (Main.game.isInCombat()) {
			Main.game.flashMessage(Colour.GENERIC_BAD, "Cannot quicksave while in combat!");

		} else if (Main.game.isInSex()) {
			Main.game.flashMessage(Colour.GENERIC_BAD, "Cannot quicksave while in sex!");

		} else if (Main.game.getCurrentDialogueNode().getDialogueNodeType()!=DialogueNodeType.NORMAL) {
			Main.game.flashMessage(Colour.GENERIC_BAD, "Can only quicksave in a normal scene!");

		} else if (!Main.game.isStarted() || !Main.game.getCurrentDialogueNode().equals(Main.game.getDefaultDialogueNoEncounter())) {
			Main.game.flashMessage(Colour.GENERIC_BAD, "Cannot save in this scene!");

		} else {
			Main.getProperties().lastQuickSaveName = "QuickSave_"+Main.game.getPlayer().getName(false);
			saveGame("QuickSave_"+Main.game.getPlayer().getName(false), true);
		}
	}

	public static void quickLoadGame() {
		String name = "QuickSave_"+Main.game.getPlayer().getName(false);

//		if(new File("data/saves/"+name+".lts").exists()) {
			loadGame(name);
//		} else {
//			loadGame(Main.getProperties().lastQuickSaveName);
//		}
	}

	public static boolean isSaveGameAvailable() {
		return Main.game.isStarted() && Main.game.getSavedDialogueNode() == Main.game.getDefaultDialogueNoEncounter();
	}

	public static void saveGame(String name, boolean allowOverwrite) {
		if (name.length()==0) {
			Main.game.flashMessage(Colour.GENERIC_BAD, "Name too short!");
			return;
		}
		if (name.length() > 32) {
			Main.game.flashMessage(Colour.GENERIC_BAD, "Name too long!");
			return;
		}
		if (name.contains("\"")) {//!name.matches("[a-zA-Z0-9]+[a-zA-Z0-9' _]*")) {
			Main.game.flashMessage(Colour.GENERIC_BAD, "Incompatible characters!");
			return;
		}

		Game.exportGame(name, allowOverwrite);

		try {
			properties.lastSaveLocation = name;//"data/saves/"+name+".lts";
			properties.nameColour = Femininity.valueOf(game.getPlayer().getFemininityValue()).getColour().toWebHexString();
			properties.name = game.getPlayer().getName(false);
			properties.level = game.getPlayer().getLevel();
			properties.money = game.getPlayer().getMoney();
			properties.arcaneEssences = game.getPlayer().getEssenceCount(TFEssence.ARCANE);
			if (game.getPlayer().isFeminine()) {
				properties.race = game.getPlayer().getSubspecies().getSingularFemaleName(game.getPlayer());
			} else {
				properties.race = game.getPlayer().getSubspecies().getSingularMaleName(game.getPlayer());
			}
			properties.quest = game.getPlayer().getQuest(QuestLine.MAIN).getName();

			properties.savePropertiesAsXML();

		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	public static boolean isLoadGameAvailable(String name) {
		File file = new File("data/saves/"+name+".xml");

		return file.exists();
	}

	public static void loadGame(String name) {
		if (isLoadGameAvailable(name)) {
			Game.importGame(name);
		}
	}

	public static void loadGame(File f) {
		Game.importGame(f);
	}

	public static void deleteGame(String name) {
		File file = new File("data/saves/"+name+".xml");

		if (file.exists()) {
			try {
				file.delete();
				Main.game.setContent(new Response("", "", Main.game.getCurrentDialogueNode()));
			} catch (Exception ex) {
				ex.printStackTrace();
			}

		} else {
			Main.game.flashMessage(Colour.GENERIC_BAD, "File not found...");
		}
	}

	public static void deleteExportedGame(String name) {
		File file = new File("data/saves/"+name+".xml");

		if (file.exists()) {
			try {
				file.delete();
				Main.game.setContent(new Response("", "", Main.game.getCurrentDialogueNode()));
			} catch (Exception ex) {
				ex.printStackTrace();
			}

		} else {
			Main.game.flashMessage(Colour.GENERIC_BAD, "File not found...");
		}
	}

	public static void deleteExportedCharacter(String name) {
		File file = new File("data/characters/"+name+".xml");

		if (file.exists()) {
			try {
				file.delete();
				Main.game.setContent(new Response("", "", Main.game.getCurrentDialogueNode()));
			} catch (Exception ex) {
				ex.printStackTrace();
			}

		} else {
			Main.game.flashMessage(Colour.GENERIC_BAD, "File not found...");
		}
	}

	public static List<File> getSavedGames() {
		List<File> filesList = new ArrayList<>();

		File dir = new File("data/saves");
		if (dir.isDirectory()) {
			File[] directoryListing = dir.listFiles((path, name) -> name.endsWith(".xml"));
			if (directoryListing != null) {
				filesList.addAll(Arrays.asList(directoryListing));
			}
		}

		filesList.sort(Comparator.comparingLong(File::lastModified).reversed());

		return filesList;
	}

	public static List<File> getCharactersForImport() {
		List<File> filesList = new ArrayList<>();

		File dir = new File("data/characters");
		if (dir.isDirectory()) {
			File[] directoryListing = dir.listFiles((path, name) -> name.endsWith(".xml"));
			if (directoryListing != null) {
				filesList.addAll(Arrays.asList(directoryListing));
			}
		}

		filesList.sort(Comparator.comparingLong(File::lastModified).reversed());

		return filesList;
	}

	public static List<File> getSlavesForImport() {
		List<File> filesList = new ArrayList<>();

		File dir = new File("data/characters");
		if (dir.isDirectory()) {
			File[] directoryListing = dir.listFiles((path, name) -> name.endsWith(".xml"));
			if (directoryListing != null) {
				filesList.addAll(Arrays.asList(directoryListing));
			}
		}

		filesList.sort(Comparator.comparingLong(File::lastModified).reversed());

		return filesList;
	}

	public static List<File> getGamesForImport() {
		List<File> filesList = new ArrayList<>();

		File dir = new File("data/saves");
		if (dir.isDirectory()) {
			File[] directoryListing = dir.listFiles((path, name) -> name.endsWith(".xml"));
			if (directoryListing != null) {
				filesList.addAll(Arrays.asList(directoryListing));
			}
		}

		filesList.sort(Comparator.comparingLong(File::lastModified).reversed());

		return filesList;
	}

	public static void importCharacter(File file) {
		if (file != null) {
			try {
				Main.game.setPlayer(CharacterUtils.startLoadingCharacterFromXML());
				Main.game.setPlayer(CharacterUtils.loadCharacterFromXML(file, Main.game.getPlayer(),
						CharacterImportSetting.NEW_GAME_IMPORT,
						CharacterImportSetting.NO_PREGNANCY,
						CharacterImportSetting.NO_COMPANIONS,
						CharacterImportSetting.NO_ELEMENTAL,
						CharacterImportSetting.CLEAR_SLAVERY,
						CharacterImportSetting.CLEAR_KEY_ITEMS,
						CharacterImportSetting.CLEAR_COMBAT_HISTORY,
						CharacterImportSetting.CLEAR_SEX_HISTORY,
						CharacterImportSetting.REMOVE_RACE_CONCEALED));

				Main.game.getPlayer().getSlavesOwned().clear();
				Main.game.getPlayer().endPregnancy(false);

				Main.game.setRenderAttributesSection(true);
				Main.game.clearTextStartStringBuilder();
				Main.game.clearTextEndStringBuilder();
				Main.getProperties().setValue(PropertyValue.newWeaponDiscovered, false);
				Main.getProperties().setValue(PropertyValue.newClothingDiscovered, false);
				Main.getProperties().setValue(PropertyValue.newItemDiscovered, false);
				Main.game.getPlayer().calculateStatusEffects(0);

				Main.game.initNewGame(CharacterCreation.START_GAME_WITH_IMPORT);

			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
	}

	public static Properties getProperties() {
		return properties;
	}

	public static void saveProperties() {
		properties.savePropertiesAsXML();
	}
}

repo_name: klorpa/Pedo

license: unknown


code:
package com.lilithsthrone.rendering;

import java.util.List;

import com.lilithsthrone.utils.Colour;

/**
 * @since 0.2.2
 * @version 0.2.2
 * @author Innoxia
 */
public class Artist {

	private String name;
	private Colour colour;
	private String folderName;
	private List<ArtistWebsite> websites;

	public Artist(String name, Colour colour, String folderName, List<ArtistWebsite> websites) {
		this.name = name;
		this.colour = colour;
		this.folderName = folderName;
		this.websites = websites;
	}

	public String getName() {
		return name;
	}

	public Colour getColour() {
		return colour;
	}

	public String getFolderName() {
		return folderName;
	}

	public List<ArtistWebsite> getWebsites() {
		return websites;
	}

}


code:
package com.lilithsthrone.rendering;

/**
 * @since 0.2.2
 * @version 0.2.2
 * @author Innoxia
 */
public class ArtistWebsite {

	private String name;
	private String URL;

	public ArtistWebsite(String name, String URL) {
		this.name = name;
		this.URL = URL;
	}

	public String getName() {
		return name;
	}

	public String getURL() {
		return URL;
	}
}


code:
package com.lilithsthrone.rendering;

import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.lilithsthrone.utils.Colour;

/**
 * @since 0.2.2
 * @version 0.2.10
 * @author Innoxia
 */
public class Artwork {

	private Artist artist;

	private int index;

	private List<String> clothedImages;
	private List<String> partialImages;
	private List<String> nakedImages;

	public static List<Artist> allArtists;
	static {
		allArtists = new ArrayList<>();

		File dir = new File("res/images/characters");

		if(dir.exists()) {
			FilenameFilter textFilter = new FilenameFilter() {
				public boolean accept(File dir, String name) {
					return name.toLowerCase().endsWith(".xml");
				}
			};

			for(File subFile : dir.listFiles(textFilter)) {
				if (subFile.exists()) {
					try {
						DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
						DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
						Document doc = dBuilder.parse(subFile);

						// Cast magic:
						doc.getDocumentElement().normalize();

						Element artistElement = (Element) doc.getElementsByTagName("artist").item(0);

						String artistName = artistElement.getAttribute("name");
						Colour colour = Colour.valueOf(artistElement.getAttribute("colour"));
						String folderName = artistElement.getAttribute("folderName");

						List<ArtistWebsite> websites = new ArrayList<>();

						NodeList nodes = artistElement.getElementsByTagName("website");
						for(int i=0; i < nodes.getLength(); i++){
							Element websiteNode = (Element) nodes.item(i);
							websites.add(new ArtistWebsite(websiteNode.getAttribute("title"), websiteNode.getAttribute("url")));
						}

						allArtists.add(new Artist(artistName, colour, folderName, websites));

					} catch(Exception ex) {
					}
				}
			}

			// Add artist template for custom art
			allArtists.add(new Artist("Custom", Colour.BASE_GREY, "custom", new ArrayList<>()));
		}
	}

	public Artwork(File folder, Artist artist) {
		this.artist = artist;

		index = 0;

		this.clothedImages = new ArrayList<>();
		this.partialImages = new ArrayList<>();
		this.nakedImages = new ArrayList<>();

		// Add all images to their respective lists
		for (File f : folder.listFiles((dir, name) -> name.endsWith(".jpg") || name.endsWith(".png") || name.endsWith(".gif"))) {
			if (f.getName().startsWith("partial"))
				partialImages.add(f.getAbsolutePath());
			else if (f.getName().startsWith("naked"))
				nakedImages.add(f.getAbsolutePath());
			else
				clothedImages.add(f.getAbsolutePath());
		}
	}

	public Artist getArtist() {
		return artist;
	}

	public int getIndex() {
		return index;
	}

	public void setIndex(int index) {
		index = index % getTotalArtworkCount();
		if(index < 0) {
			index = getTotalArtworkCount() + index;
		}
		this.index = index;
	}

	public void incrementIndex(int increment) {
		setIndex(this.index + increment);
	}

	public int getTotalArtworkCount() {
		return clothedImages.size() + partialImages.size() + nakedImages.size();
	}

	public boolean isCurrentImageClothed() {
		return index < getClothedImages().size();
	}

	public File getCurrentImage() {
		String path;
		if(index < getClothedImages().size()) {
			path = getClothedImages().get(index);

		} else if(index < getClothedImages().size() + getPartialImages().size()){
			path = getPartialImages().get(index - getClothedImages().size());

		} else {
			path = getNakedImages().get(index - getClothedImages().size() - getPartialImages().size());
		}

		if (path.isEmpty()) return null;
		return new File(path);
	}

	public List<String> getClothedImages() {
		return clothedImages;
	}

	public List<String> getPartialImages() {
		return partialImages;
	}

	public List<String> getNakedImages() {
		return nakedImages;
	}

}


code:
package com.lilithsthrone.rendering;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Base64;

/**
 * @since 0.3.0
 * @version 0.3.0
 * @author Addi
 */
public class CachedGif extends CachedImage {
    private String thumbnailString;

    @Override
    public boolean load(File f) {
        // Load the first frame of the image
        CachedImage firstFrame = new CachedImage();
        if (!firstFrame.load(f)) return false;
        thumbnailString = firstFrame.getThumbnailString();
        percentageWidth = firstFrame.getPercentageWidth();
        width = firstFrame.getWidth();
        height = firstFrame.getHeight();

        if (f.length() / 1024 > 10240) {
            // Animated image is too large, use the first frame instead
            imageString = firstFrame.getImageString();
            System.err.println("Warning: Animated image " + f.getName() + " is too large. Using first frame instead.");
        } else {
            // Load the animation
            try {
                imageString = "data:image/gif;base64,"
                        + Base64.getEncoder().encodeToString(Files.readAllBytes(f.toPath()));
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
        }
        return true;
    }

    @Override
    public String getThumbnailString() {
        return thumbnailString;
    }
}


code:
package com.lilithsthrone.rendering;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Base64;

import javax.imageio.ImageIO;

/**
 * @since 0.2.5.5
 * @version 0.3.0
 * @author Addi
 */
public class CachedImage {
	protected String imageString = "";
	protected int width = 200, height = 200, percentageWidth = 35;

	/**
	 * Load an image from the given file path into a reusable string.
	 * @param f A File containing the path to a .jpg or .png image
	 * @return True if the image was successfully loaded, false otherwise
	 */
	public boolean load(File f) {
		try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) {
			// Load the image
			BufferedImage image = ImageIO.read(f);
			updatePercentageWidth(image);

			// Resize image
			int[] targetSize = getAdjustedSize(600, 600);
			image = scaleDown(image, targetSize[0], targetSize[1]);
			width = image.getWidth();
			height = image.getHeight();

			// Convert to string
			ImageIO.setUseCache(false);
			ImageIO.write(image, "PNG", byteStream);
			imageString = "data:image/png;base64," + Base64.getEncoder().encodeToString(byteStream.toByteArray());
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}

	void updatePercentageWidth(BufferedImage image) {
		width = image.getWidth();
		height = image.getHeight();
		if(height == width) {
			percentageWidth = 45;
		} else if(height < width) {
			percentageWidth = 65;
		}
	}

	/**
	 * Retrieve the image in base 64 encoded string format. The string must start with 'data:image/png;base64'.
	 * @return The image as base64 string
	 */
	public String getImageString() {
		return imageString;
	}

	/**
	 * Retrieve the thumbnail of the image.
	 * @return The thumbnail image as base64 string
	 */
	public String getThumbnailString() {
		return imageString;
	}

	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}

	/**
	 * Retrieve the width percentage that the image is supposed to take on the character information page. The value is
	 * calculated using the width to height ratio.
	 * @return 35 if the image is vertical, 45 if it's squared and 65 if it's horizontal.
	 */
	public int getPercentageWidth() {
		return percentageWidth;
	}

	/**
	 * Calculate and retrieve the size of the image after fitting it inside a box with the given dimensions.
	 * @param maxWidth Width of the box to fit the image into
	 * @param maxHeight Height of the box to fit the image into
	 * @return An integer array with the format [width, height] containing the fitted dimensions.
	 */
	public int[] getAdjustedSize(int maxWidth, int maxHeight) {
		// Calculate ratio between desired and original dimensions
		float widthRatio = (float) maxWidth / (float) width;
		float heightRatio = (float) maxHeight / (float) height;
		// Use the one that produces the smaller image as scaling factor
		float scale = Math.min(widthRatio, heightRatio);
		return new int[]{(int)(width * scale), (int)(height * scale)};
	}

	/**
	 * Scales down the given image using incremental, bilinear, anti-aliased sampling. Each step may only decrease the
	 * size by a factor of two, increasing the quality but taking additional time to complete. The parameters represent
	 * final values and do not respect the aspect ratio of the original image.
	 * @param original The BufferedImage to scale
	 * @param targetWidth The targeted width, ignoring aspect ratio
	 * @param targetHeight The targeted height, ignoring aspect ratio
	 * @return The rescaled image
	 */
	static BufferedImage scaleDown(BufferedImage original, int targetWidth, int targetHeight) {
		int width = original.getWidth();
		int height = original.getHeight();
		BufferedImage rv = original;

		while (width > targetWidth || height > targetHeight) {
			// Determine target dimensions while never decreasing size by more than half
			if (width > targetWidth)
				width = Math.max(width / 2, targetWidth);

			if (height > targetHeight)
				height = Math.max(height / 2, targetHeight);

			// Setup render target
			BufferedImage step = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
			Graphics2D canvas = step.createGraphics();
			canvas.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
			canvas.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
			canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

			// Render onto canvas
			canvas.drawImage(original, 0, 0, width, height, null);
			canvas.dispose();

			rv = step;
		}

		return rv;
	}
}


code:
package com.lilithsthrone.rendering;

import java.io.File;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * @since 0.2.5.5
 * @version 0.2.10
 * @author Addi
 */
public enum ImageCache {
	INSTANCE;

	protected Map<File, CachedImage> cache = Collections.synchronizedMap(new LinkedHashMap<File, CachedImage>(16, 0.75f, true) {
		private static final long serialVersionUID = 1L;

		@Override
		protected boolean removeEldestEntry(Map.Entry<File, CachedImage> eldest) {
			// Always allow as many as there can be in one room (player, two companions, five slaves)
			if (size() <= 8) return false;

			// Allow cache with up to 1/10 of the maximum memory
			long byteSize = size() * 500 * 1024;
			if (byteSize > Runtime.getRuntime().maxMemory() / 10) return true;

			// Drop entries until the cache is smaller than the remaining memory
			long adjustSize = (byteSize - Runtime.getRuntime().freeMemory()) / (500 * 1024);
			if (adjustSize > 0)
				for (int i = 0; i < adjustSize; ++i)
					remove(keySet().iterator().next());
			return false;
		}
	});

	protected LinkedBlockingQueue<File> queue = new LinkedBlockingQueue<>();
	protected Thread loaderThread = new Thread(() -> {
		while (!Thread.currentThread().isInterrupted()) {
			File f;
			try {
				// Fetch a new file to load or wait until one is available
				f = queue.take();
			} catch (InterruptedException e) {
				// Queue broke, end the thread
				break;
			}

			getImage(f);
		}
	});

	ImageCache() {
		// Start loader thread as daemon so it will be ended by application shutdown
		loaderThread.setDaemon(true);
		loaderThread.start();
	}

	/**
	 * Attempt to queue an image for caching if it is not queued already. This operation may fail if the queue is full.
	 * @param f A File containing the path to the image
	 */
	public void requestCache(File f) {
		if (cache.get(f) == null && !queue.contains(f)) {
			queue.offer(f);
		}
	}

	/**
	 * Attempts to retrieve a cached image. If it isn't in the cache, queue it and return immediately.
	 * @param f A File containing the path to the image
	 * @return A CachedImage object containing the image string if found in the cache, null otherwise
	 */
	public CachedImage requestImage(File f) {
		CachedImage image = cache.get(f);
		if (image == null) {
			requestCache(f);
		}
		return image;
	}

	/**
	 * Retrieves an image. If the image isn't in the cache, load it immediately and block the caller until it is ready.
	 * @param f A File containing the path to the image
	 * @return A CachedImage object containing the image string or null if the image failed to load
	 */
	public CachedImage getImage(File f) {
		CachedImage image = cache.get(f);
		if (image == null) {
			image = f.getName().endsWith(".gif") ? new CachedGif() : new CachedImage();
			if (image.load(f)) cache.put(f, image);
			else return null;
		}
		return image;
	}
}


code:
package com.lilithsthrone.rendering;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import com.lilithsthrone.controller.xmlParsing.Element;
import com.lilithsthrone.controller.xmlParsing.XMLLoadException;
import com.lilithsthrone.controller.xmlParsing.XMLMissingTagException;
import com.lilithsthrone.utils.Colour;
import com.lilithsthrone.utils.SvgUtil;

/**
 * @since 0.2.6
 * @version 0.2.6
 * @author Irbynx
 */
public class Pattern {

	private static Map<String, Pattern> allPatterns;
	private static Map<String, Pattern> defaultPatterns;

	private String name;
	private String displayName;

	private boolean primaryRecolourAvailable;
	private boolean secondaryRecolourAvailable;
	private boolean tertiaryRecolourAvailable;

	private String baseSVGString;
	private Map<Colour, Map<Colour, Map<Colour, String>>> SVGStringMap;

	static {
		allPatterns = new TreeMap<>();
		defaultPatterns = new TreeMap<>();

		File dir = new File("res/patterns");

		allPatterns.put("none", new Pattern("none")); // Adding empty pattern
		defaultPatterns.put("none", new Pattern("none"));

		if(dir.exists()) {
			FilenameFilter textFilter = new FilenameFilter() {
				public boolean accept(File dir, String name) {
					String lowercaseName = name.toLowerCase();
					if (lowercaseName.endsWith(".xml")) {
						return true;
					} else {
						return false;
					}
				}
			};

			for(File subFile : dir.listFiles(textFilter)) {
				if(subFile.exists()) {
					loadFromFile(subFile);

//					try {
//						String newPatternName = subFile.getName().substring(0, subFile.getName().indexOf(".svg"));
//						allPatterns.put(newPatternName, new Pattern(newPatternName));
//						if(!newPatternName.equalsIgnoreCase("rainbow")) {
//							defaultPatterns.put(newPatternName, new Pattern(newPatternName));
//						}
//						//System.out.println("Added Pattern: " + newPatternName);
//
//					} catch(Exception ex) {
//					}
				}
			}
		}
	}

	public Pattern(String name) {
		this.name = name;
		SVGStringMap = new HashMap<>();

		baseSVGString = "";
		if(!name.equals("none")) {
			try {
				File patternFile = new File(System.getProperty("user.dir")+"/res/patterns/" + this.getName().toLowerCase() + ".svg");
				List<String> lines = Files.readAllLines(patternFile.toPath());
				StringBuilder sb = new StringBuilder();
				for(String line : lines) {
					sb.append(line);
				}
				baseSVGString = sb.toString();
			} catch (IOException e) {
				e.printStackTrace();
			}

			primaryRecolourAvailable =
					baseSVGString.contains("#f4d7d7")
					|| baseSVGString.contains("#e9afaf")
					|| baseSVGString.contains("#de8787")
					|| baseSVGString.contains("#d35f5f")
					|| baseSVGString.contains("#c83737");

			secondaryRecolourAvailable =
					baseSVGString.contains("#f4e3d7")
					|| baseSVGString.contains("#e9c6af")
					|| baseSVGString.contains("#deaa87")
					|| baseSVGString.contains("#d38d5f")
					|| baseSVGString.contains("#c87137");

			tertiaryRecolourAvailable =
					baseSVGString.contains("#f4eed7")
					|| baseSVGString.contains("#e9ddaf")
					|| baseSVGString.contains("#decd87")
					|| baseSVGString.contains("#d3bc5f")
					|| baseSVGString.contains("#c8ab37");
		}

	}

	private static Pattern loadFromFile(File clothingXMLFile) {
		try {
			Element patternElement = Element.getDocumentRootElement(clothingXMLFile);
			String loadedDisplayName = patternElement.getMandatoryFirstOf("name").getTextContent();
			boolean loadedDefaultPattern = Boolean.valueOf(patternElement.getMandatoryFirstOf("defaultPattern").getTextContent());
			String loadedPatternName = patternElement.getMandatoryFirstOf("patternName").getTextContent().replace(".svg", "");

			Pattern pattern = new Pattern(loadedPatternName);
			pattern.displayName = loadedDisplayName;

			allPatterns.put(loadedPatternName, pattern);
			if(loadedDefaultPattern) {
				defaultPatterns.put(loadedPatternName, pattern);
			}

			return pattern;

		} catch (XMLLoadException e) {
			e.printStackTrace();
		} catch (XMLMissingTagException e) {
			e.printStackTrace();
		}

		return null;
	}

	/**
	 * Checks all found patterns and returns one if available.
	 */
	public static Pattern getPattern(String name) {
		return allPatterns.get(name);
	}

	/**
	 * Returns all available patterns
	 */
	public static Map<String, Pattern> getAllPatterns() {
		return allPatterns;
	}

	/**
	 * Returns all default patterns which clothing can spawn with
	 */
	public static Map<String, Pattern> getAllDefaultPatterns() {
		return defaultPatterns;
	}

	/**
	 * Returns name of the pattern
	 */
	public String getName() {
		return this.name;
	}

	/**
	 * Returns human readable name.
	 * @return
	 */
	public String getNiceName() {
		if(displayName!=null && displayName.length()>0) {
			return displayName;
		}
		return this.name.replace('_', ' ');
	}

	public boolean isPrimaryRecolourAvailable() {
		return primaryRecolourAvailable;
	}

	public boolean isSecondaryRecolourAvailable() {
		return secondaryRecolourAvailable;
	}

	public boolean isTertiaryRecolourAvailable() {
		return tertiaryRecolourAvailable;
	}

	public String getSVGString(Colour colour, Colour colourSecondary, Colour colourTertiary) {
		if(!SVGStringMap.containsKey(colour) || !SVGStringMap.get(colour).containsKey(colourSecondary) || !SVGStringMap.get(colour).get(colourSecondary).containsKey(colourTertiary)) {
			generateSVGImage(colour, colourSecondary, colourTertiary);
		}
		return SVGStringMap.get(colour).get(colourSecondary).get(colourTertiary);
	}

	private void generateSVGImage(Colour colour, Colour colourSecondary, Colour colourTertiary) {
		addSVGStringMapping(colour, colourSecondary, colourTertiary, SvgUtil.colourReplacementPattern(this.getName(), colour, colourSecondary, colourTertiary, baseSVGString));
	}

	private void addSVGStringMapping(Colour colour, Colour colourSecondary, Colour colourTertiary, String s) {
		if(SVGStringMap.get(colour)==null) {
			SVGStringMap.put(colour, new HashMap<>());
			SVGStringMap.get(colour).put(colourSecondary, new HashMap<>());

		} else if(SVGStringMap.get(colour).get(colourSecondary)==null) {
			SVGStringMap.get(colour).put(colourSecondary, new HashMap<>());
		}

		SVGStringMap.get(colour).get(colourSecondary).put(colourTertiary, s);
	}
}

// Can't find a good place to put the list for the future, so I'll put it here:
// Items supporting patterns as of the creation of the system:
//
// Bikini Bottoms
// Backless Panties
// Boxers
// Boyshorts
// Thigh highs
// Work boots
// Swimsuit
// Bikini
// Briefs
// Crotchless briefs
// Crotchless panties
// Crotchless thong
// Panties
// Vstring
// Elbow Length gloves
// Fingerless gloves
// Gloves
// Cargo trousers
// Thigh high socks
// Knee high socks
// Tshirt