XML modification tutorial
Posted: Sat Mar 11, 2017 8:28 am
Have you ever wanted to make a mod that makes a small change to an existing type? If you read the Modding Reference section, I'm sure you have. Overriding the entire type may mean including huge amounts of code from the core game with only slight changes, and makes your mod incompatible with other mods that override the same type. Type overrides like <ShipClassOverride> avoid these problems, but can only make certain changes to certain types and are not well documented. XML functions to the rescue!
Here's George's post introducing these functions.
Now I'll demonstrate with a slightly more complicated example than George's. I'll use a request from relanat:
So, following George's steps:
Step 1:Make sure your mod has usesXML="true". As of API 37, this is no longer necessary.
Also, if it only affects types in a specific library, you should probably extend that library, so it can't be included in games where it won't do anything. You may also want to include the library so you can use UNIDs from it without declaring <!ENTITY>s for them. In this case, Korolov is definitely part of the Human Space Library, so I'll use that:
Step 2: Create an <OnGlobalTypesInit> event and get the XML of the type you want to modify.
This event is the only time you can change the XML of a type that's already in the game. So the changes will only take effect when starting a new game. However, you can get the XML of types and create new types at other times. (But you can't get the XML of a type that has an override type affecting it after this event. Also, there used to be a bug preventing you from getting the XML of a type that had been created or modified with typCreate, but this was fixed in the 1.7 betas.) In this case I'm going to add a generic <Type> to contain the event, but you can also put it inside another type:
Step 3: Modify the XML.
Since I want to add an element that doesn't already exist, I have to specify it somehow. There are a few options:
- Option A: Just include the XML elements you want to add in one of your own types.
This is the easiest option when you know exactly what you want to add. For instance, I can put relanat's <SellShip> element in my <Type>, after <Events>. This is usually fine, but if what you're adding might actually do something in the type you're adding it to, enclose it in another element that won't do anything, like <XMLToAdd>.
- Option B: Create the XML from a string with (xmlCreate).
This is the trickiest to format because the string is going to be parsed in several different steps. The < and > symbols in the string you want to become XML must be replaced by < and > or your mod will not be valid XML. Also, " symbols must be escaped by putting a backslash before them, like this: \", otherwise the TransLisp compiler will think the string ends and you'll get an error, which could be about mismatched quotes, mismatched parenthesis, unknown entity, etc.
- Option C: Assemble the XML with XML functions.
You still have to add any new tags like in option A or B, but you can change their attributes and text, and add subelements as I'll do below. Recommended if you want the XML to vary depending on other code.
- Option D: Use a CDATA block.
This has the advantage that you can include <, >, and ", but the disadvantage that you can't use replacements beginning with &, including text UNIDs like &unid;. I don't recommend it unless you know what you're doing. See this thread.
Now it's time to add the new element to the station type's <Trade> element:
Note that (xmlGetSubelement) doesn't just return the subelement; it's more like a pointer to the subelement as part of the larger block of XML. So modifying tradeSubelement also modifies KorlovXML. But it doesn't actually change the XML of the type, so we need to...
Step 4: Override the type with (typCreate).
Easy enough:
Putting it all together, using option A:
Now, you may notice that some of the variables are only used once. I've used more variables and more lines than necessary to improve clarity. The event could be condensed to this:
But it can't be condensed any further because some xml functions, including xmlAppendSubelement, don't return the result. So you have to store the data being modified in a variable.
Coming soon: Deleting subelements and safely handling types that may not exist, may not have gettable XML, and may not have the expected subelements.
Here's George's post introducing these functions.
Here are the XML functions:George Moromisato wrote:The basic recipe looks like this:
1. In the root element of your extension (<TranscendenceExtension>), add usesXML="true". This tells the engine that you need the XML for all the types to be kept around for you.
2. In <OnGlobalTypesInit>, you may use a new function, typGetXML to get the XML for a type that you wish to override.
3. There are new functions that manipulate the XML returned by typGetXML. For example, you can set attributes or add elements.
4. Once you've manipulated the XML, you may call typCreate to dynamically create a new type or override an existing type.
Code: Select all
(xmlAppendSubElement xml xmlToAdd [index]) -> True/Nil
(xmlAppendText xml text [index]) -> True/Nil
(xmlCreate xml) -> xml
(xmlDeleteSubElement xml index) -> True/Nil
(xmlGetAttrib xml attrib) -> value
(xmlGetAttribList xml) -> list of attribs
(xmlGetSubElement xml tag|index) -> xml
(xmlGetSubElementCount xml) -> number of sub-elements
(xmlGetSubElementList xml [tag]) -> list of xml
(xmlGetText xml [index]) -> text
(xmlGetTag xml) -> tag
(xmlSetAttrib xml attrib value) -> value
(xmlSetText xml text [index]) -> True/Nil
Now I'll demonstrate with a slightly more complicated example than George's. I'll use a request from relanat:
relanat wrote: ↑Fri Mar 10, 2017 1:01 amIs there a way to add <Trade> info to a station without overwriting the whole station code?
...
I would like to addwithout having to add the nearly 2000 lines of Korolov code to a mod to overwrite it.Code: Select all
<SellShip criteria="s L:1-13;" priceAdj="110"/>
So, following George's steps:
Step 1:
Also, if it only affects types in a specific library, you should probably extend that library, so it can't be included in games where it won't do anything. You may also want to include the library so you can use UNIDs from it without declaring <!ENTITY>s for them. In this case, Korolov is definitely part of the Human Space Library, so I'll use that:
Code: Select all
<TranscendenceExtension UNID="&unidKorolovXMLDemo;"
name= "Korolov XML demo"
extends= "&unidHumanSpaceLibrary;"
usesXML= "true"
apiVersion= "35"
version= "1.0"
credits= "Nathaniel Stalberg (NMS)"
>
<Library UNID="&unidHumanSpaceLibrary;"/>
This event is the only time you can change the XML of a type that's already in the game. So the changes will only take effect when starting a new game. However, you can get the XML of types and create new types at other times. (But you can't get the XML of a type that has an override type affecting it after this event. Also, there used to be a bug preventing you from getting the XML of a type that had been created or modified with typCreate, but this was fixed in the 1.7 betas.) In this case I'm going to add a generic <Type> to contain the event, but you can also put it inside another type:
Code: Select all
<Type UNID="&evAddKorolovShipSales;">
<Events>
<OnGlobalTypesInit>
(block (KorolovXML ...)
(setq KorolovXML (typGetXML &stKorolovShipping;))
Since I want to add an element that doesn't already exist, I have to specify it somehow. There are a few options:
- Option A: Just include the XML elements you want to add in one of your own types.
This is the easiest option when you know exactly what you want to add. For instance, I can put relanat's <SellShip> element in my <Type>, after <Events>. This is usually fine, but if what you're adding might actually do something in the type you're adding it to, enclose it in another element that won't do anything, like <XMLToAdd>.
Code: Select all
(setq subelementToAdd (xmlGetSubelement (typGetXML &evAddKorolovShipSales;) 'SellShip))
This is the trickiest to format because the string is going to be parsed in several different steps. The < and > symbols in the string you want to become XML must be replaced by < and > or your mod will not be valid XML. Also, " symbols must be escaped by putting a backslash before them, like this: \", otherwise the TransLisp compiler will think the string ends and you'll get an error, which could be about mismatched quotes, mismatched parenthesis, unknown entity, etc.
Code: Select all
(setq subelementToAdd (xmlCreate "<SellShip criteria=\"s L:1-13;\" priceAdj=\"110\"/>"))
You still have to add any new tags like in option A or B, but you can change their attributes and text, and add subelements as I'll do below. Recommended if you want the XML to vary depending on other code.
Code: Select all
(setq subelementToAdd (xmlCreate "<SellShip/>"))
(xmlSetAttrib subelementToAdd 'criteria "s L:1-13;")
(xmlSetAttrib subelementToAdd 'priceAdj '110)
This has the advantage that you can include <, >, and ", but the disadvantage that you can't use replacements beginning with &, including text UNIDs like &unid;. I don't recommend it unless you know what you're doing. See this thread.
Now it's time to add the new element to the station type's <Trade> element:
Code: Select all
(setq tradeSubelement (xmlGetSubelement KorolovXML 'Trade))
(xmlAppendSubelement tradeSubelement subelementToAdd)
Step 4: Override the type with (typCreate).
Easy enough:
Code: Select all
(typCreate &stKorolovShipping; KorolovXML)
Code: Select all
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TranscendenceExtension [
<!ENTITY unidHumanSpaceLibrary "0x00100000">
<!ENTITY unidKorolovXMLDemo "0xE127B100">
<!ENTITY evAddKorolovShipSales "0xE127B101">
]>
<TranscendenceExtension UNID="&unidKorolovXMLDemo;"
name= "Korolov XML demo"
extends= "&unidHumanSpaceLibrary;"
usesXML= "true"
apiVersion= "35"
version= "1.0"
credits= "Nathaniel Stalberg (NMS)"
>
<Library UNID="&unidHumanSpaceLibrary;"/>
<Type UNID="&evAddKorolovShipSales;">
<Events>
<OnGlobalTypesInit>
(block (KorolovXML tradeSubelement subelementToAdd)
(setq KorolovXML (typGetXML &stKorolovShipping;))
(setq subelementToAdd (xmlGetSubelement (typGetXML &evAddKorolovShipSales;) 'SellShip))
(setq tradeSubelement (xmlGetSubelement KorolovXML 'Trade))
(xmlAppendSubelement tradeSubelement subelementToAdd)
(typCreate &stKorolovShipping; KorolovXML)
)
</OnGlobalTypesInit>
</Events>
<SellShip criteria="s L:1-13;" priceAdj="110"/>
</Type>
</TranscendenceExtension>
Code: Select all
(block ((KorolovXML (typGetXML &stKorolovShipping;))) ; step 2
(xmlAppendSubelement (xmlGetSubelement KorolovXML 'Trade) (xmlGetSubelement (typGetXML &evAddKorolovShipSales;) 'SellShip)) ; step 3
(typCreate &stKorolovShipping; KorolovXML) ; step 4
)
Coming soon: Deleting subelements and safely handling types that may not exist, may not have gettable XML, and may not have the expected subelements.