/*
* Aphelion
* Copyright (c) 2013 Joris van der Wel
*
* This file is part of Aphelion
*
* Aphelion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* Aphelion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Aphelion. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, the following supplemental terms apply, based on section 7 of
* the GNU Affero General Public License (version 3):
* a) Preservation of all legal notices and author attributions
* b) Prohibition of misrepresentation of the origin of this material, and
* modified versions are required to be marked in reasonable ways as
* different from the original version (for example by appending a copyright notice).
*
* Linking this library statically or dynamically with other modules is making a
* combined work based on this library. Thus, the terms and conditions of the
* GNU Affero General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your
* choice, provided that you also meet, for each linked independent module,
* the terms and conditions of the license of that module. An independent
* module is a module which is not derived from or based on this library.
*/
package aphelion.shared.gameconfig;
import aphelion.shared.event.TickEvent;
import aphelion.shared.swissarmyknife.LinkedListEntry;
import aphelion.shared.swissarmyknife.LinkedListHead;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.error.YAMLException;
/**
*
* @author Joris
*/
public class GameConfig implements TickEvent
{
private static final Logger log = Logger.getLogger("aphelion.config");
// rule with the highest specificy first!
final LinkedListHead<Rule> rules = new LinkedListHead<>();
private final LinkedList<WeakReference<ConfigSelection>> usedSelections = new LinkedList<>();
public GameConfig()
{
}
public ConfigSelection newSelection()
{
ConfigSelection sel = new ConfigSelection(this);
usedSelections.add(new WeakReference<>(sel));
return sel;
}
public static List<Object> loadYaml(InputStream in) throws YAMLException
{
// SafeConstructor because we may not trust the server or a moderator
Yaml yaml = new Yaml(new SafeConstructor());
List<Object> ret = new ArrayList<>();
// parse the documents immediately (by invoking the iterator)
for (Object o : yaml.loadAll(in))
{
ret.add(o);
}
return ret;
}
public static List<Object> loadYaml(String str) throws YAMLException
{
// SafeConstructor because we may not trust the server or a moderator
Yaml yaml = new Yaml(new SafeConstructor());
List<Object> ret = new ArrayList<>();
// parse the documents immediately (by invoking the iterator)
for (Object o : yaml.loadAll(str))
{
ret.add(o);
}
return ret;
}
/** Add one or more parsed yaml documents to this instance.
* Call applyChanges() after having loaded all your documents.
*
* @param documents A list of documents as parsed by yaml.load().
* The contents are considered read-only; They are never modified by this method,
* and should never be modified by any other method.
* @param fileIdentifier The file identifier that may be used to undo this configuration using unloadFile()
*/
public void addFromYaml(List<Object> documents, String fileIdentifier)
{
for (Object data : documents)
{
if (!(data instanceof List))
{
log.log(Level.WARNING, "Error while parsing YAML for {0}, the document is not a List", fileIdentifier);
continue;
}
List list = (List) data;
for (Object ruleObj : list )
{
Map ruleMap;
try
{
ruleMap = (Map) ruleObj;
}
catch (ClassCastException ex)
{
log.log(Level.WARNING, "Error while parsing YAML for {0}, the rule entry is not a Map. (it is a {1})",
new Object[]{fileIdentifier, ruleObj.getClass().getName()});
continue;
}
Object selectorObj = ruleMap.get("selector");
Selector selector = new Selector();
if (selectorObj != null)
{
Map selectorMap;
try
{
selectorMap = (Map) selectorObj;
selector.setWeaponYaml(selectorMap.get("weapon"));
selector.setShipYaml(selectorMap.get("ship"));
selector.setFreqYaml(selectorMap.get("freq"));
selector.setPerkYaml(selectorMap.get("perk"));
selector.setImportance(selectorMap.get("importance"));
}
catch (ClassCastException ex)
{
log.log(Level.WARNING, "Error while parsing YAML for "+fileIdentifier+", invalid type for something in the selector", ex);
continue;
}
}
Rule rule = null;
for (Rule existingRule : rules)
{
if (fileIdentifier.equals(existingRule.getFileIdentifier()) &&
selector.equals(existingRule.selector))
{
rule = existingRule;
break;
}
}
boolean newRule = false;
if (rule == null)
{
rule = new Rule(fileIdentifier, selector);
newRule = true;
}
boolean addedSomething = false;
if (ruleMap.entrySet() != null)
{
Iterator<Map.Entry<Object, Object>> it = ruleMap.entrySet().iterator();
while (it.hasNext())
{
Map.Entry<Object, Object> entry = it.next();
String key;
try
{
key = (String) entry.getKey();
}
catch (ClassCastException ex)
{
log.log(Level.WARNING, "Error while parsing YAML for "+fileIdentifier+", rule attribute key is not a string", ex);
continue;
}
if (key.isEmpty())
{
continue;
}
if ("noop".equalsIgnoreCase(key))
{
continue;
}
if ("selector".equals(key))
{
continue;
}
addedSomething = true;
rule.addAttribute(key, entry.getValue());
}
}
if (addedSomething && newRule)
{
addRule(rule);
}
}
}
// make sure to call applyChanges after calling this method
}
private void addRule(Rule rule)
{
LinkedListEntry<Rule> entry = this.rules.first;
while (entry != null)
{
if (rule.compareTo(entry.data) >= 0)
{
// we have a higher or equal specificy to the looped rule
// add me before that rule
entry.prependData(rule);
return;
}
entry = entry.next;
}
rules.appendData(rule);
}
public void unloadFile(String fileIdentifier)
{
Iterator<Rule> it = rules.iterator();
while (it.hasNext())
{
Rule rule = it.next();
if (fileIdentifier.equals(rule.getFileIdentifier()))
{
it.remove();
}
}
// make sure to call applyChanges after calling this method
}
public void applyChanges()
{
applyChanges(0);
}
private void applyChanges(int n)
{
try
{
Iterator <WeakReference<ConfigSelection>> it = usedSelections.iterator();
while (it.hasNext())
{
ConfigSelection selection = it.next().get();
if (selection == null)
{
it.remove();
continue;
}
selection.resolveAllValues();
}
}
catch (ConcurrentModificationException ex)
{
// An event listener might have called newSelection()
// try again.
// Note: because we are not dealing with threads in this code,
// it is safe to rely ConcurrentModificationException
if (n == 100000)
{
throw new Error("Too many concurrent modifications. Probably caused by an infinite callback loop");
}
applyChanges(n + 1);
}
}
public void resetTo(GameConfig other)
{
rules.clear();
for (Rule otherRule : other.rules)
{
Rule rule = otherRule.clone();
addRule(rule);
}
// make sure to call applyChanges after calling this method
}
@Override
public void tick(long tick)
{
if (tick % 100 == 0)
{
// cleanup
Iterator <WeakReference<ConfigSelection>> it = usedSelections.iterator();
while (it.hasNext())
{
ConfigSelection selection = it.next().get();
if (selection == null)
{
it.remove();
continue;
}
selection.cleanup();
}
}
}
}