_________________________________________
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
GreedFall(PC)
Modding Tips
_________________________________________
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
April 30, 2020
Version 2.00
Written by: Dheu
Email: Dheuster@gmail.com
Use subject: GreedFall Modding Tips 2.00
If you see any mistakes, or have anything that you want to add,
you can email me or post to the forums located at nexusmods:
https://www.nexusmods.com/greedfall/mods/116?tab=posts
______________________________________Notes____________________________________
This Tutorial\FAQ is published at:
https://www.nexusmods.com/greedfall/mods/116
This Document looks best in a fixed-width font, such as Courier New.
Word wrap should be turned OFF if the option is available.
GreedFall is Copyright © 2019 by Focus Home Interactive
I am not affiliated with Focus Home Interactive or anyone who had anything to
do with the creation of this game. This Document may be posted on any site.
You may not charge for, or in any way profit from this Document.
-------------------------------------------------------------------------------
Table of Some-Contents:
-------------------------------------------------------------------------------
I. Introduction
I.1 Standard Tools
II. Extracting and editing Game Resources
II.1 PGZ Files
II.2 PSSG Files
II.3 pssgExtractor.py
II.4 PSSG Format Details
III. Basics
III.1 Load Order and Contention
III.2 Registering new Content
III.3 Inputs
III.4 Spells versus Commands
III.5 Everything is a Skill
IV. Quests
IV.1 SQG FILE Contents
IV.2 ConditionSets
V. Dialogues
V.1 SDG FILE Contents
VI. Audio
VI.1 Spoken Audio
VI.2 Music
VI.3 Sounds/FX
VI.4 Adding/Replacing Audio
...
X. Models and Textures
X.1 Textures
X.2 Models
X.3 Tools?
XI. Miscellaneous
XI.1 Cutscenes
XI.2 Levels and Maps
XII. Legalities
-------------------------------------------------------------------------------
Table of No-Contents (TODO)
-------------------------------------------------------------------------------
VII. Items
VII.1 General
VII.2 Armor
VII.3 Weapons
VII.4 Potions and Grenades
VII.5 Loot and Rewards
VIII. Merchants
VIII.1 Defining Inventories
VIII.1 Linking NPCs to inventories
IX. Spells
IX.1 ???
===============================================================================
I. > > > > Introduction
===============================================================================
This document is a set of observations I have made over the past few months
as I have modded Greedfall. I thought I would share with others to give
them a jump start. I am also hoping in doing so, others might share their
own discoveries with me so I can add them to this document.
This is a work in progress. So please forgive some of the empty sections.
I make certain assumptions about the person reading this. In short, I assume
the reader is a programmer of some ilk with enough general programming
experience that terms like "scope", "classes" and "inheritance" don't sound
alien. Python experience is also useful if you want to pull apart some
of the scripts I have made.
There are many different aspects to modding a game. A mod may introduce a new
capability, new dialog, new quests. For some, modding is about creating and
incorporating new models and special effects.
Unfortunately, there are not currently any readily-available programs or
utilities that allow one to import new models into GreedFall, though I
feel like it could be done for someone determined. I simply lack the
3 modeling knowledge to write import/export plugins myself.
This tutorial focuses on modding GreedFall programatically. Deforming and
Re-skinning models is outside the scope of this document. I will touch on
what I know, but it isn't much.
-------------------------------------------------------------------------------
I.1 > > > > Standard Tools
-------------------------------------------------------------------------------
7-Zip:
GreedFall resources are compressed into files using gzip compression.
You will need a program that can recognize and uncompress gzip to access,
add and/or edit those resources. 7-Zip is the defacto compression utility
of choice.
https://www.7-zip.org/
EGO PSSG Editor:
=== THE === modding tool for GreedFall. And if you make a mod that was
only possible thanks to that tool, I highly recommend sending the author,
Ryder25, a donation.
The most useful ability of the PSSG editor is the ability to export game
resource files (PSSG files) to XML. And then import XML back to PSSG. XML
is human readable and much easier to mod than binary data.
https://ryder25.itch.io/ego-pssg-editor
HxD:
Once you extract PSSG files to XML, you will find large sequences of HEX
characters within that represent the internal binary data. So how to you
convert those sequences to files that you can actually open up with say..
an audio editor?
For that, you need a Hex editor that supports pasting of hex characters.
I recommend HxD. It is a windows based Hex editor. It is the key to
taking the binary data presented in the XML versions of the PSSG files
and saving it off to independent files. (I step you through this process
later when I extract the OGG audio data from one of the files).
For those technical types, it is also useful for figuring out things like
the format of binary files. (It is how I figured out the PSSG format).
https://download.cnet.com/HxD-Hex-Editor/3000-2352_4-10891068.html
Notepad++
Greedfall was developed by a French gaming shop, so to open some files
you need a text editor with international character support.
https://notepad-plus-plus.org/downloads/
CYGWIN:
CYGWIN gives you a Linux style command shell on Windows complete with
most Linux core programs. It adds an icon to the desktop:
"Cygwin(64) Terminal"
Mostly I use it for the program "grep", which is used to search through
all the files in a directory for a text string.
$ cd /cygdrive/c/path/to/extracted/resources/
$ grep -Harl 'searchforthisstring' .
The above 2 commands will search all files in the resources directory and
subdirectories for the string "searchforthisstring" and list any file
names that hit. For those unfamiliar with Linux style command prompts,
you can get the documentation for most commands by typing 'man' followed
by the command. IE:
$ man grep
I used grep and HxD to discover most of the things I know about GreedFall
It is also handy for batch processing when you want to run a script on
all files in a directory (or sub directories). I document how to do that
later when I talk about some of the helper scripts I wrote.
https://cygwin.com/install.html
Python 3.x:
If you install CYGWIN, by default it installs python 2.7. With version
2.0 of this document, I upgraded the scripts to use the latest version
of python available with CYGWIN as BLENDER only works with Python 3.x
and I didn't want to have to switch python versions .
To install the latest 3.x Python with CYGWIN, you will need to run
the CYGWIN setup and search for "Python". Find the highest 3.x version
you can (python38 when I made this) and then change "skip" to the latest
version you can select.
Hit next a few times and eventually CYGWIN will download and install
python 3.x packages. However you are not done. The default python
(symbolic link) is still in the /usr/bin directory linked to
python2.7.exe. To correct this, you will need to open a CYGWIN terminal,
go to /usr and type "chmod 777 bin". Then in /usr/bin, remove python
"rm python" and create a new symbolic link "link python3.8.exe python"
Type : "python --version" to confirm you are now using the latest.
In my case it returned "Python 3.8.2"
To download the same version for windows, go to:
https://www.python.org/downloads/release/python-382/
(substitute the version for the last few digits)
I recommend keeping your CYGWIN and WINDOWS versions in sync.
===============================================================================
II > > > > Extracting and editing Game Resources
===============================================================================
If you look inside the GreedFall/packs directory, you will notice some large
files with the extension .spk. All the resources for the game are compressed
within these files. This serves 3 purposes:
1) Faster Loading:
Easier to ask the OS to retrieve 1 big file than 300 small files.
2) Smaller Footprint:
Hey, who wants the game to take up another 5 gigs?
3) User Overrides:
When a user extracts a resource from the .spk file and places it within
GreedFall/datalocal, the game engine will load the users version on
startup instead of the one in the SPK file. While this design allows user
mods, the downside is that it promotes contention between mods.
If you want to really get into modding the game, you will want to extract all
the .spk files. DO NOT extract them to the game directory as that will cause
the game to try to load everything as overrides, making it take minutes to
startup and most things to become unplayable. Rather, extract everything to a
datalocal directory somewhere else on your machine.
IE: I extracted them to the parent directory:
C:\Steam\steamapps\common\datalocal
MOST of the files extracted are XML type files with various extensions. These
can be opened with a regular text editor (notepad++ recommended) and you can
use GREP to search through them for keywords:
.gui: Associated with the graphical user interface, but still xml.
.sli: Associated with game configuration, but still xml
.sqg: Associated with quest and event definitions, but still xml
.sdg: Associated with dialogue trees, but still xml
A bit ironically, the ".txt" extension files under the text directory use
"UCS-2 LE BOM" character encoding, which will not cleanly open in a normal
text editor. You will need an editor such as Notepad++ to open those files.
The .txt files provide the language translations of the various strings (What
you see when people talk, descriptions of items, etc...). More importantly,
if you scan for a term using grep, grep probably wont find it as the
character encoding puts junk between the characters and screws up plain text
scans. If you really want, you can make a separate directory and open/save
off all the .txt files in the other directory using UTF-8. Notepad++ can
convert to other character sets. However... you will have to open and save
every file (all 1237 of them). You can do it pretty fast if you make a
macro in Notepad++ and open/convert 10 or so at a time. But still takes
awhile.
-------------------------------------------------------------------------------
II.1 > > > > PGZ Files
-------------------------------------------------------------------------------
PGZ means "PSSG GZIPPED" File. The contents of .pgz files normally do not
have extensions, but they are all .pssg files. You can uncompress these
with 7-zip. Conversely, if you uncompress a file, make alterations and wish
to re-compress it, you can do so by compressing the altered artifact with
7-zip, but you have to:
1. Remove the .pssg extension
2. Choose "gzip" as the archive format
3. Change the output extension to ".pgz"
As to what is INSIDE the .pgz files... The answer is a PSSG file.
-------------------------------------------------------------------------------
II.2 > > > > PSSG Files
-------------------------------------------------------------------------------
What is PSSG? PSSG is a container format for game resources. Basically it is
a custom Binary XML representation.
Imagine you have an OGG audio file. In the Silk Engine, it would save the
resource in a .pssg file. A binary header would indicate to the engine what
the contents of the file are, the mime-type and the offset of where the data
begins and other information like the id that is used by the game to reference
the object.
Bottom line... if you know the PSSG header format, you can figure out where
the ogg data is and extract it. Conversely, if you know that OGG audio files
started with a specific binary sequence (4F 67 67 53), you can open the file
in a hex-editor, search for that sequence and extract the audio without even
knowing the .pssg format.
** This only works when the PSSG file only wraps a single OGG file and that
File is the last element of the PSSG file. Fortunately, most audio in
the game does exactly that... 1 file wraps 1 ogg which is the last thing
in the file. So with a script, you can extract audio data without even
parsing the PSSG file. See included script pssgOggExtract.py
From what I can tell, PSSG was a format invented for the Phyre Engine: A Sony
PlayStation engine released in 2008 to promote creation of games for the PS3.
The engine and dev environment were made free because Sony wanted games for
their platforms. As far as I can tell, support for the engine ended in 2016
when the PS4 came out. However, it was free and cross-platform. So a number
of development shops took the engine and expanded it into their own
customized versions (updating it in-house to meet the demands of newer
Direct X standards). The Ego Engine and more recently the Silk Engine are
byproducts of the Phyre Engine. GreedFall uses the Silk Engine.
So why the history? These dev shops didn't customize everything. Only the
parts that they needed. So an app made for an ego engine game can also be
used to open most of the silk engine pssg files.
One such program is the PSSG Editor created by Ryder25 mentioned above in the
Standard Tools section.
An Extraction/Insertion Example
-------------------------------
Lets suppose I want to extract/update and change an audio file. For the sake
of this example, we will look at:
voice_dialog/cha_huf_aphra/dlg_aphra_friendship_101.pgz
1 Extract the file with 7zip.
2 Open the extracted file with PSSG Editor. It will show you a number of
nodes. The LAST node is the binary data.
4 In PSSG Editor, highlight all the binary data for the last node. Then go
to Hxd, create a new file and paste. Save the file as something with an
"ogg" extension.
5. Now if you double click on the file in explorer, you will hear the audio
play.
Lets pretend you edit the file and remove the last word. Now you want to
re-inject the updated file into the PSSG. PSSG Editor will not let you paste
binary data directly into the window. But you can paste data into an
exported XML version of the PSSG file. XML is recommended as it helps to
ensure integrity and works even when the binary data isn't the last item in
the PSSG payload.
In PSSG Editor, highlight the root node "PSSGDATABASE", then select:
Nodes -> Export.
Export to the same directory and filename, but with ".xml" added. Open the
XML file and find the binary data block at the end. There you will see a
bunch of hexadecimal values. Open up your edited .ogg file in HxD (Hex
Editor). Hit CTRL + A to select all and then CTRL + C to copy to clipboard.
HxD is smart enough to copy the data to the clipboard as text based hex data.
Now in the XML File, delete the existing hex values and then paste the hex
data into the xml. Don't worry about the column formatting. The XML doesn't
care. The original XML had line feeds every 24 hex chars for human
readability, but it isn't needed.
In the PSSG Editor, select New. Highlight the root node and select Import.
Browse to your updated XML file and hit Okay. Don't forget to update the
"binaryDataSize" in the definition of the last node, otherwise the game may
crash.
(To figure out the size of the binary data, open the ogg once more in HxD,
Hit CTRL+A and then (View -> Offset base -> Decimal). Now the length in
the bottom of HxD is the length of the binary data you have highlighted
in plan decimal form. That is the value you want to enter for the
binary length)).
Save the file in PSSG format. Then remove the .pssg extension and re-compress
the pssg file using 7-zip with the gzip archive format selected. Change the
extension from .gz to .pgz. Finally, place the file in the Greedfall/datalocal
directory (location doesn't matter). The game will now load that file when it
searches for the .pgz files as an override and use your edited audio line.
NOTES: Spoken audio also has time length data associated with the dialogue.
So technically, you would have to update that as well if the length
of the audio is significantly different. Otherwise the person would
get cut off or there would be an unusual pause at the end of the line.
See "Using existing audio lines to update/create new conversations"
below for more info.
-------------------------------------------------------------------------------
II.3 > > > > pssgExtractor.py
-------------------------------------------------------------------------------
The latest release of EGO PSSG Editor was 2017. That program can be used to
open most PSSG files and update many resources... But if you want to mess
with some of the new features added by Silk (like cutscenes), EGO PSSG
Editor wont help. Sometimes you will also run into snags where the
PSSG Editor will refuse to import your updated file.
The Ego PSSG Editor is schema based, so if you happen to open a PSSG file
that uses any unrecognized schema elements, it barfs. This is actually
surprisingly rare, but if you happen to run across one of these (such as
the cutscenes or levels), you need something else.
To help with this issue, I made my own extractor/combiner. See
pssgExtract.py (Python script). It is a command line script, so it isn't
as nice or user friendly as EGO PSSG Editor, however it does not rely on
schemas. It reads the file in and makes a best guess at the schema (saves
it off in case a user wants to tweak it) and then converts to XML. You can
then update/edit the XML file and convert the XML back to PSSG.
If you compare the XML output of this script with the XML output of the
PSSG Editor's export command, the two will often be very similar, in some
cases even exactly the same. However, there are minor differences, so
in general you shouldn't try opening pssgExtractor xml files with PSSG
Editor or vice versa.
Tweaking the Schema:
Version 2.0 of pssgExtractor.py now supports 3 big new features:
1) It is now cross-compatible with Python 2.x and 3.x.
2) pschema files now support composite types. A composite type is
and array that describes the structure of data that is normally
binary and renders as HEX in XML. For example, the TRANSLATE
tag of level files (.sla) normally render as 64 Bytes (Hex).
But now you can update the .pschema and change it to:
"_Type" : "Float[8]",
or
"_Type" : "Float[]",
Then the next time you convert the PSSG to XML, you will see floats
(which you can edit as floats and convert back to PSSG).
3) Deep Analysus : The "-d" flag is experimental, but basically
attempts to auto-generate these composite types for you by
analysing the binary data and deducing what it most likely is.
My deduction logic is pretty rudimentory, but it provides a
starting point. Note "-d" will do nothing if a .pschema file already
exists. So make sure you either remove the existing .pschema first or
run with -o as well to force overrite the current schema.
-------------------------------------------------------------------------------
II.4 > > > > PSSG Format Details
-------------------------------------------------------------------------------
PSSG is a form of Binary XML that consists of 2 parts, a header and a body:
A: Header: Describes the schema (elements, attributes and their indexes)
1: 4 char header: PSSG
2: 4 bytes (UIN32): Size of the entire file in Bytes (minus the
8 byte header):
3: 4 bytes (UINT32): MAX ATTRIBUTE ID (1 greater than largest ID in file)
4: 4 bytes (UINT32): NUMBER of UNIQ ELEMENTS
5: 4 bytes (UINT32): Index of Element Type (1 if it is your first)
6: 4 bytes (UINT32): Size of the first element tag (string length)
7: N chars : Name of the first tag (N = value of previous UINT32)
8: 4 bytes (UINT32): number of attributes in Element
9: 4 bytes (UINT32): ID of Attribute (global index, not index relative to
element)
10: 4 bytes (UINT32): size of the first attribute name (string length)
11: N chars : The name of the first attributes (N = value of previous
UINT32)
12: Repeat steps 9 - 11 until you have read value of #8 attributes
13: Repeat steps 5 - 11 until you have read value of #4 elements
*SPECIAL: Element "XXX" lists attributes that can appear under any element.
A common value is "id".
NOTES: The original PSSG spec made all elements uppercase and all
attributes camel-case. Silk added some elements and attributes and
those specific additions do not use the original standard, but
instead Make all new Elements and Attributes use First letter
uppercase.
Fortunately, most PSSG files use the original spec. So it is pretty
easy to scan the header with a hex editor and create your own index
map by counting capital and camel-case strings.
B: Body: Instead of Verbose XML, uses the indexes from the header to define
the content. Begins after processing the end of the schema.
1: 4 bytes (UIN32): GLOBAL ELEMENT INDEX
2: 4 bytes (UINT32): Total Size of element (attributes + content) in bytes
3: 4 bytes (UINT32): Size of Attribute Data in Bytes (starting after this
value)
a: If #3 > 0:
4a: 4 bytes (UINT32): AttributeId (global index)
5a: 4 bytes (UINT32): Size of attribute value in bytes:
6a: Attribute Value:
- if #5 indicates 4 Bytes, assume INT32 follows.
- If INT32 value is greater than 981668463 or less than
-981668463, recast to float.
- if #5 is more than 4 Bytes, read/cast first 4 Bytes to UINT32
- If value of UINT32 is exactly 4 less than the value of #5,
assume remaining bytes are ASCII friendly characters (string
value)
- else... assume N bytes following #5 are a byteArray.
(N = value of #5) This includes the 4 you read in to test for
string
... return to #4a until SIZE OF Attribute Data (#3) has been read.
7a: In the original PSSG spec, elements stored most of their content
in attributes. When large content was needed, you would make an
element that had no attributes specifically for that content. For
this reason, MOST pssg files wont have 7a (Elements with both
attributes AND content)
However Silk engine updated the spec with elements that have both
attributes and content. (Granted an element with both can not
parent other elements).
So when an element has attributes, how do you know if the content
(space between #2 and #3) is a value associated with the element
or merely space for other child elements? The answer: You don't.
It would require a schema. However... to define an element with
either an attribute or 4 bytes of content, you would need
at least 24 bytes. So when writing a generic scanner, if the
content size is greater than 0 and less than 24, we assume the
bytes are the elements value. If the size is greater than 23, we
assume it is space for child elements (but that might be wrong).
Additionally, Silk specifically expanded the spec to support
generic data. They added the elements:
<Int Name=""></Int> (4 Bytes)
<Float Name=""></Float> (4 Bytes)
<UnsignedInt Name=""></UnsignedInt> (4 Bytes)
<String Name="" Value=""/> ***
<Vector3 Name=""></Vector> (Contains 3 floats (12 Bytes))
<Color Name=""></Color> (Contains 3 floats (12 Bytes))
*** String was the only one that doesn't have content
Basically they added a bunch of named primitive Elements and all
but String have content. Mostly I have seen sequences of these in
the Cut scene definitions. So alternatively (or additionally), you
could specifically look for these element names.
b: if #3 == 0:
4b: Element Content Value:
N bytes of data are associated with element #1, N = value of
(#2 - 4) (subtract 4 for the bytes needed to read #3)
... return to #1 until all data is read.
NOTES:
** 2: PSSGDATABASE is normally the first element described, so the size of
the first element is normally the distance to the end of the file
following the UINT32.
** 3: In the example file, size of attributes was 88 byes.
** 4a: In the example file, first attributeId was "2"
**: In many cases, the game doesn't actually care if the file is converted
back to PSSG. If you use EGO Editor (which is schema driven), the game
will often work with the XML file, so long as the extension is removed
and it is still placed in a .pgz file.
===============================================================================
III > > > > Basics
===============================================================================
-------------------------------------------------------------------------------
III.1 > > > > Load Order and Contention
-------------------------------------------------------------------------------
I mentioned that the game loads resources found under GreedFall/datalocal
as overrides. There are a few things to know.
First... all filenames in greedfall are unique. That is why they are all
so huge. For this same reason, you don't have to recreate the path that you
found the file in to get the game to use it. You could place all your edited
files in the directory:
GreedFall/datalocal/
However, this isn't recommended. If you do that and the user installs 2 mods
that do the same thing, then somebody is going to get clobbered.
So the best way to store your modded resources is to place them in a
directory named after your mod.
GreedFall/datalocal/myMod/
Second... the game is first come first serve. That is... if the game has
already encountered “library_game_components.sli” in another sub-directory
before it gets to your sub-directory, your SLI file (and edits) will be
ignored.
The game searches subdirectories FIRST (Depth first traversal). And it seems
to sort the directories alpha-numerically before it dives. This is why you
will see some mods place their resources in “0ModName”. The “0” is the
authors way of indicate loading priority.
I don't recommend getting into naming wars. Then you end up with crazy names
like “0000000MyModIsMoreImportantThanYours”. Remember, if your mod breaks
10 other mods, that will get you more flack than your mod simply not
working. Contention is part of life. So I recommend simply sticking with
something that makes sense.
Different mod authors handle contention in different ways. The most common
systems I have seen:
1) Patches... : The mod author downloads other mods where users report
issues and release patches in their optional download sections. This
works if the mod author remains active. But tends to fall apart once
the mod author (inevitably) moves on to other projects.
2) Compatible/Incompatible Lists: Use the forums to maintain lists of known
compatible and incompatible mods. Author can maintain a summary at the
top with a sticky, but ultimately, users can maintain the list once the
author moves on.
3) Super-compatible approaches and code: In some cases, mods can be made
using ADDITIVE techniques that minimize the probability of conflict
with other mods. Unfortunately, GreedFall has no "Load Loose Files"
option. So there is no 100% contention-proof method of making a mod
for GreedFall. Though... you can minimize the probability and at the
very least, make it easy for the user to resolve on their own.
-------------------------------------------------------------------------------
III.2 > > > > Registering new Content
-------------------------------------------------------------------------------
My first mod added a bunch of “new” items to the game. (reused existing
models/textures, but defined items with different stats and color overlays).
Like other games, GreedFall will not simply load “loose” files simply because
they are present in the datalocal directory. Anything you ADD to the game has
to be registered by one of the SLI config files.
The easiest way to discover what needs to edited/updated is to simply
download another mod that already does something like what you are trying to
accomplish. Not to toot my own horn, but I know the mod I made, so I will use
it as an example.
My first mod (Naut-E Wares) is located here:
https://www.nexusmods.com/greedfall/mods/110?tab=files
You don't have to download or install the mod. However, if you go to the files
section of the mod, you will see a “Preview File Contents” link. This will
list all the files I created and edited to make the mod. I EDITED these game
resources:
- cha_autogen_npc_variantes.sli
- library_description_tags.sli
- library_equipment_default.sli
- library_game_components.sli
- library_item.sli
- lvl_02_south_city_background.sqg
Most of these files are “loader” type files that reach out and load other
game resources. In pretty much all cases, my edit bulls down to 1 line where
I add a new path to loader that points to a file specific to my mod with
new content. The files above point to:
- library_game_components.sli -> naute_wares_merchant_inventory.sli
- library_description_tags.sli -> naute_wares_descriptions.sli
- library_item.sli -> naute_wares_items.sli
- lvl_02_south_city_background.sqg -> naute_wares_interaction_registration.sqg
There will still be conflicts, but combining files with nothing but ADDITIVE
changes is trivial. It would be easy to create a mod manager that auto-merges
library files like this if all mods made 1 line additive changes to the config
files. Conversely, it isn't too hard to explain to users how to make two mods
work together when edits are made in this manner.
My next project may very well be a trivial merge program that scans datalocal
for files of the same name and attempts to trivial merge them into a
directory that the mod-manager manages. Most likely the name of the
“merged” files will be placed in a directory that starts with “##”. So I
humbly request other mods avoid the name “##” or mod directories that start
with more than 1 '#', so that such a mod manager can eventually be created.
I mentioned colors. It turns out, the game developers decided to make it
easy to put a color overlay on items with a simple config tweak to an
item definition. It is so they didn't have to create dozens of versions of
item textures to add variety to the game. Instead, each item has a primary
and secondary color. You can apply a color overlay to the primary color of
an item with a special tag in its definition:
IE: customShiftColor="0,-30,10"
or: customShiftColor="#1, #2, #3"
Where:
#1 : Hue offset: range -360 to 0.
#2 : Saturation: range -100 to 100. (-100 = gray scale)
#3 : Brightness: range -64 to 64. (-64 = black. 64 = white)
-------------------------------------------------------------------------------
III.3 > > > > Inputs
-------------------------------------------------------------------------------
Inputs have to do with key mappings. For example, you hit the letter "m" and
the local map appears. The games default key mappings are setup in a folder
under "datalocal/inputs".
For modders interested in making mods that change the mappings (Say for a
left-handed mouse user), that is where you would want to start. You can also
map Inputs to spell effects (which is how the light works), but that is more
complicated.
-------------------------------------------------------------------------------
III.4 > > > > Spells versus Commands
-------------------------------------------------------------------------------
Spells are immersive. You caste a spell and something happens in the game
world. Commands are the opposite. They "break immersion" by doing something
like bringing up an inventory screen, or a travel gui. Most commands are
accessed from interactions (dialogues or iterative objects)
This distinction is one of the reasons you haven't seen someone come out
with a mod that allows Fast Travel using a potion or spell. There is no
direct connection between casting a spell and issuing a command to the
game engine. Though there may be work arounds.
-------------------------------------------------------------------------------
III.5 > > > > Everything is a skill
-------------------------------------------------------------------------------
Lets get something out of the way. Almost all magic in greedfall is a
"skill". When you define skills, you define what they are named and what
category they show up in if the user hits tab to do something mid-combat.
"Use Potion", "Throw Grenade", "Cast Magic Missile"... all of these are
skills and in truth, every player starts the game with all of them.
However, to show up when you hit tab, the skills have conditions defined.
For example, to cast certain spells, the condition may require that you
have purchased something from the skill tree to show up in your spell
list.
Throwing a grenade is just like casting a spell. The difference is that it
has a inventory requirement AND a skill requirement to show up. Casting the
"Throw Grenade" spell also subtracts a potion from your inventory. And yes,
even shooting a gun is a spell that requires a specific item in one of the
hand slots and ammunition.
When a skill is activated, a series of "effects" occur. These can include
animations, particle effects, and changing certain stats on the player or
a target like their health or resistances. Effects can also change global
variables. Point here is as far as the game is concerned, casting a spell
or throwing a grenade or even shooting a gun are all a series of effects
connected to a skill.
I will likely be making some new spells in future mods, so I will fill
this section in with more details and an example when I do.
===============================================================================
IV. > > > > Quests
===============================================================================
Technically what I call "quests" are called "callgraphs" in Greedfall.
However, having come from Gamebryo dev environments (Skyrim/Fallout 4), I am
more comfortable thinking about these graphs as quests. So for the sake of
conversation, I will call them quests, but you can search/replace quests
with "graphs" if you want.
Quests are the glue that holds the game together. Some quests function the
way you think of when you play the game: They have a visual component in
the game that tracks progress with waypoint markers and stages, etc.. But the
game also has hidden quests that the player never sees. Quests that always
run and manage companions and relationships. Hidden quests might be given a
name like "RomanceManager".
In greedfall, when you enter a map, the map often kicks off a set of
map-specific quests. So for example, when you enter New Serene, the quest:
datalocal/quests/levelsdedicated/lvl_02_south_city_background.sqg
Kicks off. These "master" quest in turn kick off about a dozen other (hidden)
quests. The quests do things like add handlers to statically embedded trigger
zones in the map so that the game knows what to do when you enter the trigger
zone. ANY quest can kick off other quests. So if you want to avoid merge
conflicts with other mods, you can look at these large quest kickoff files and
possibly place your initialization code in a child quest or even grandchild
quest. There is also likely a master quest higher up the food chain that I
haven't discovered yet that causes the lvl_## quests to kick off.
Example:
You walk up to a merchant. When you approach, you enter a trigger zone that
surrounds the merchant. It kicks off a hidden quest that causes the merchant
to shout "Come one, come all, buy my wares!". It might also enable an
interaction icon to appear if you put the mouse cursor over the merchant.
If you keep walking and leave the trigger zone, that same hidden quest will
have handlers set up to remove the interaction icon and make the merchant
stop talking.
Later in the game, you have started a visible quest that wants you to
interrogate the merchant. The visible quest sets a global variable indicating
that the quest is running. When you approach that same merchant, the hidden
quest we mentioned earlier checks to see if that global variable has a
specific value and if so, still presents an interaction icon, but this
interaction is connected to a different dialogue than the normal one. When
you go to interact with the merchant, a cut-scene will now kick off that is
specific to the visible quest. When the cut-scene is over, the hidden quest
that monitors the area around the merchant may actually update that variable.
(or the dialogue might. Just depends).
Meanwhile, an event monitor is watching that variable. When the variable's
value changes, the event monitor notices and kicks off some code to react.
Based on the new variables value, it updates the non-hidden quest's state so
that when you go to your quest log, the fact that you talked to the merchant
is now checked off a list of todo items. At least that is one way to do it.
There are multiple ways to accomplish many tasks.
-------------------------------------------------------------------------------
IV.1 > > > > SQG FILE Contents
-------------------------------------------------------------------------------
Most quest definitions are found under the directory:
datalocal\quests
Quests are defined by .sqg files. You can open these with a plain-text editor
and see the declarative, XML structured data within.
At the top of the SQG file is a questGraph block, which identifies the
contents as a quest graph. This is sometimes followed by an arguments
block which indicate arguments that might be passed into the questGraph
when it is invoked. For example:
<questGraph id="travel_service"> <arguments> <arg name="LevelName" type="STRING" /> <arg name="IsHum" type="BOOL" /> </arguments>
** isHum means "isHumanMale", not "isHuman".
This is then followed by a circuitinfo block:
<circuitInfo stream="..."/>
CircuitInfo is a large block with a URL encoded summary of the contents of
the questBlock. As far as I can tell, the circuitblock likely supports the
silk-engine editor. IE: Provides a quick-to-read way for some fancy game
editor to visualize what the file does. However, changes to the circuitinfo
block do not seem to have any impact on the game. I generally leave it alone.
You don't need to waste time keeping it in sync with the changes you make to
the file.
After circuit info is the <Blocks> tag. This is where the meat of the sqg
file is.
SQG files use Objects to perform things like incrementing a variables,
performing an IF condition, starting a dialogue, etc... The objects are
represented in the SQG as XML blocks.
Here are the basic top level Objects:
Primary Building Blocks:
------------------------
1) startBlock :
An entry point. If you "invoke" an sqg with no parameters, by default it
looks for the startBlock named "Start". However, you can have multiple
start blocks, each with different names allowing a single SQG file to have
different entry points.
The start block lists a number of connect tags. connections are a generic,
abstract concepts and what they do and how they behave depends on the
block parent type. In the case of startBlock, connections can be thought
of like newing an object. They point to blocks in the XML file (like
creating an instance of that xml object). In the case of start blocks,
all connections are created in parallel. Back to our merchant example,
you may want one object presenting an interaction icon, but at the same
time, you need another object watching to see if the player leaves the
trigger zone. These tasks can not be done one after the other. They both
need to be happening at the same time.
If you look at lvl_02_south_city_background.sqg, you will see a start
block with an enormous number of connections. All those quests are kicked
off in parallel.
2) pauseBlock :
As the name implies, pause blocks wait for a conditionset to evaluate to
true and when it does they follow the connections block.
Pause Blocks are really key to quests. You are unlikely to find many SQG
files without them. Most pause blocks have 1 connection within the
connections block, but like a start block, they can also invoke multiple
connections in parallel when the condition is reached. In our merchant
example, you might have a pauseBlock that waits for the player to enter
the merchants trigger zone. When the condition fires, instead of a
single connection, it has 2 connections. One to show the interaction
icon and another that directs to yet another pause block that waits for
the player to leave the trigger zone. When they leave, the second pause
block may clean up the interaction and ultimately redirect back to the
original pause block, waiting for the player to re-enter the trigger zone
again.
This trigger-zone looped pausing drives much of the game.
3) testBlock :
Similar to pauseBlock, however, it doesn't wait for the condition to
evaluate to true. Rather it evaluates a condition then invokes the
connections block with the pinIndex set to the evaluated value of the
condition. Value "0" indicates that the condition was true and value "1"
indicates that the condition was false. So within testBlocks, you will
normally have 2 connections. 1 with a pinIndex of "0" and one with a
pinIndex of "1". If you only have 1 connection, then that thread of
execution bails if the value doesn't match your pinIndex.
4) testSwitchBlock:
<testSwitchBlock name="TestSwitch - V_COM_FightingStyle" userSpecificedLevelSpecific="false" levelSpecific=""> <connections> <connection pinType="OUTPUT" pinIndex="0" target="SHOW CHEST_1" targetPinIndex="0" pinName="Out" targetPinName="" /> <connection pinType="OUTPUT" pinIndex="1" target="SHOW CHEST" targetPinIndex="0" pinName="Out" targetPinName="" /> <connection pinType="OUTPUT" pinIndex="2" target="SHOW CHEST_2" targetPinIndex="0" pinName="Default" targetPinName="" /> </connections> <conditionSets> <conditionSet isFalse="false" debugBreak="false" mode="ALL"> <conditions> <IsVariableInt isFalse="false" debugBreak="false" name="V_COM_FightingStyle" value="2" minValue="INT_MIN" maxValue="INT_MAX" /> </conditions> </conditionSet> <conditionSet isFalse="false" debugBreak="false" mode="ALL"> <conditions> <IsVariableInt isFalse="false" debugBreak="false" name="V_COM_FightingStyle" value="3" minValue="INT_MIN" maxValue="INT_MAX" /> </conditions> </conditionSet> </conditionSets> </testSwitchBlock>
TestSwitchBlock is equivalent to a switch or case statement in other
languages. Instead of a series of many testBlocks, you can have a single
testSwitchBlock that can evaluate many conditions regarding a single
variable at once. The last connection acts as a default if all tests
fail.
5) dialogBlock:
Dialogue blocks kick off a dialogue that is defined in an external file
(extension sdg). Dialogues have NPCs speak lines, sometimes present
choices, show cutscenes... etc. When a dialogue finishes it returns a
value which maps to a pinIndex and/or a targetPinName. This, in turn,
decides which of the connections to fire. As you can see above, you can
fire more than 1 connection in parallel for a given return type if you
wish.
6) timerPlayTimeBlock:
Similar to pause, it waits and then follows the connection. But instead
of a condition, it waits a specific amount of time. Can be used in a
number of ways. For example, you could use one to have an event pause for
a while, but eventually proceed anyway if the player doesn't take action.
Or you could have an interaction that disables itself for a set period of
time before becoming available again (to prevent spam)
7) commandActionBlock:
<commandActionBlock name="Trade_2" userSpecificedLevelSpecific="false" levelSpecific=""> <connections> <connection pinType="OUTPUT" pinIndex="0" target="Timer_2" targetPinIndex="0" pinName="" targetPinName="" /> </connections> <actionSet> <actions> <LaunchTrade traderName="Actor_Merchant_Trader_01" tradeMode="BUY" nextCharName="" nextDialogName="" /> </actions> </actionSet> </commandActionBlock>
commandAction blocks are used to invoke commandActions. These perform
non-quest related functions such as bringing up the Buy\Trade menu,
Travel Menu or increasing core game variables that are not within the
configurable scope of quests. (skill points, attribute points or effecting
the player's inventory with rewards and loot).
Some commandAction examples gathered using `grep -Hr -a3 '<actions>' .`
8) callGraphBlock
callGraphBlocks are used to invoke other SQG files. You mostly see them
used by level loaders. In terms of hitting a start block named something
other than start, then the connection that points to the callGraph would
specify the entry point as a targetPin. IE: notice the "target" parameter
below when it redirects to the callgraph.
9) setVariableBlock
Used to set a global variable to a specific value. All Global values have
to be pre-declared. To view the variables, see the directory:
/datalocal/variables/
For example, to see the variables used by the main quest:
/datalocal/variables/variables.mq.sli
10) incrementVariableBlock
<incrementVariableBlock name="incVar" variableName="V_Tutorial_Step" value="1" userSpecificedLevelSpecific="false" levelSpecific=""> <connections> <connection pinType="OUTPUT" pinIndex="0" target="Goal" targetPinIndex="0" pinName="" targetPinName="" /> </connections> </incrementVariableBlock>
Used to increment a global variable by a certain amount. All Global values
have to be pre-declared. To view the variables, see the directory:
/datalocal/variables/
For example, to see the variables used by the main quest:
/datalocal/variables/variables.mq.sli
10) endBlock
<endBlock name="End" userSpecificedLevelSpecific="false" levelSpecific="" />
Used by many scripts to provide a graceful single exit point. You don't
HAVE to have one of these, but if you don't add one and then later realize
there is something you want to do on all the exit points, it may be hard
to track them down (especially in a big file). By explicitly mapping to
an END block, it makes it easier to figure out where all the scripts exit
points are.
11) killBlock
The name of killBlock is deceiving. On Unix, there is a command called
"kill" It is used to send a signal to a running process. The signal might
be sleep or pause or interrupt or shutdown. The killBlock is like that
program. The type of signal sent is indicated by the pinType. The most
common two are OUTPUT and OUTPUT_KILL. The first is used to fork execution
to multiple threads, similar to the start block. The second is used to
kill an already running thread. So for example if you have a pause block
and a timer block. If the pause block fires first because a condition is
met, it might kill the timer block. Conversely, if the timer block fires
first because the timeout is reached, it might kill the pause block. So
killBlocks can be used for cleanup, but they also used just as much for
forking execution like start blocks.
Other less common Blocks:
-------------------------
What happens with Collisions?:
------------------------------
Suppose we have a start block or a kill block that forks to two separate
dialogue blocks and both attempt to start a conversation with the player.
What happens?
Good question. Someone should try this out and let me know. =)
In general, you should avoid collisions, so answering this question
shouldn't be a high priority. However, I SUSPECT what will happen
is one thread will "win" and reserve the player resource and the
other thread will "lose". Given the multi-core nature of the PS3
most likely, the engine uses real threads, so which thread wins
is probably a toss of the coin.
-------------------------------------------------------------------------------
IV.2 > > > > ConditionSets
-------------------------------------------------------------------------------
In the SQG File section, we often saw use of conditionSets. There are
all over the game, so it is worth having a dedicated section.
<conditionSet isFalse="false" debugBreak="false" mode="ALL">
isFalse: [ "true" | "false" ] Allows you to reverse the evaluation of the
result
debugBreak: ??? No idea...
mode: [ "ALL" | "AT_LEAST_ONE" ] ALL = conjunction or ... all conditions
must be true for the set to evaluate to true. AT_LEAST_ONE =
disjuction, which means if any of the conditions is true, the
set evaluates to true.
A conditionSet can also contain another conditionSet, which allows more
complicated conditions. For example:
<conditionSet isFalse="false" debugBreak="false" mode="ALL"> <conditions> <IsVariableInt isFalse="false" debugBreak="false" name="var" value="0" minValue="INT_MIN" maxValue="INT_MAX" /> <conditionSet isFalse="false" debugBreak="false" mode="AT_LEAST_ONE"> <conditions> <IsVariableInt isFalse="false" debugBreak="false" name="varA" value="5" minValue="INT_MIN" maxValue="INT_MAX" /> <IsVariableInt isFalse="false" debugBreak="false" name="varB" value="5" minValue="INT_MIN" maxValue="INT_MAX" /> <IsVariableInt isFalse="false" debugBreak="false" name="varC" value="5" minValue="INT_MIN" maxValue="INT_MAX" /> <IsVariableInt isFalse="false" debugBreak="false" name="varD" value="5" minValue="INT_MIN" maxValue="INT_MAX" /> </conditions> </conditionSet> </conditions> </conditionSet>
Which roughly translates to:
if (var == 0 || (varA == 5 || varB == 5 || varC == 5 || varD == 5))
As for the conditions themselves, there are a number of available checks.
Here are some samples gathered using:
`grep -Har -4 '<conditionSet ' .`
If you need more info about parameter values, just grep the extracted code
base for a specific tag. You will see lots of examples that will expose most
of the variable values like what "on" can point at:
<aliveTargeted value="true" on="SELF" /> <aliveTargeted isFalse="false" debugBreak="false" on="NAMED" onParam="Squad_MQ12B_Traitors_Library" value="false" /> <areTutorialsEnabled isFalse="false" debugBreak="false" /> <attackResult value="HURT_ANY" /> <attributeTargeted isFalse="false" debugBreak="false" on="SELF" onParam="" type="LIFE" value="50" delta="0.01f" comp=">" percent="true" /> <attributeTargeted on="SELF" type="WEAPON_MAINHAND_DAMAGE_ELEM" value="0.25" comp="GREATER_OR_EQUAL" /> <cartridgeIsItemCountTargeted on="SELF" value="1" percent="true" cartridgeItem="item_trap_elem" slot="WEAPON_MAIN"/> <CompareValues isFalse="false" debugBreak="false" leftOperand="@int_CraftLevel" operator="=" rightOperand="3" /> <CompareValues isFalse="false" debugBreak="false" leftOperand="@LockPickingLevel" operator="=" rightOperand="2" /> <containsLootTargeted lootType="PICK" on="SELF" /> <DialogOrCutscenePlaying isFalse="true" debugBreak="false" type="CINEMATIC" /> <DialogOrCutscenePlaying isFalse="true" debugBreak="false" type="FREECAM" /> <DialogOrCutscenePlaying isFalse="true" debugBreak="false" type="AMBIANCE" /> <DialogTalentRandom isFalse="false" debugBreak="false" dialogLevel="2" talent="CHARISMA" /> <difficulty value="EASY" isFalse="true"/> <difficulty isFalse="false" debugBreak="false" value="EXTREM" /> <effectEventTargeted isFalse="false" debugBreak="false" on="MAIN" onParam="" eventTypeDynamic="TUTO_PLA_ATTS_IMPACT" eventType="PASSIVE" /> <effectEventTargeted isFalse="false" debugBreak="false" on="MAIN" onParam="" eventTypeDynamic="" eventType="ADRENALINE_ON_MAX" /> <equipmentTypeTargeted on="SELF" slot="WEAPON_MAIN" value="AXE"/> <equipmentTypeTargeted on="SELF" slot="WEAPON_MAIN" value="CLAYMORE" isFalse="true"/> <equipmentTypeTargeted value="BROADSWORD" on="SELF" slot="WEAPON_MAIN" /> <equipmentTypeTargeted value="FLAMBERGE" on="SELF" slot="WEAPON_MAIN" /> <equipmentTypeTargeted isFalse="false" debugBreak="false" on="MAIN" onParam="" value="WEAPONRING" slot="WEAPON_MAIN" /> <experienceLevelTargeted isFalse="false" debugBreak="false" on="MAIN" onParam="" value="20" comp=">=" /> <experienceLevelTargeted isFalse="false" debugBreak="false" on="MAIN" onParam="" value="10" comp="<=" /> <GUI_isInfoMessageDisplayed isFalse="true" debugBreak="false" infoMessageID="quickinfo_tuto_disguise" /> <GUI_isInfoMessageDisplayed isFalse="false" debugBreak="false" infoMessageID="@GuiTutorialName" /> <GUI_isPanelDisplayed isFalse="false" debugBreak="false" panelId="INGAME_TEAM_SELECTION" dynamicPanelId="" isDisplayed="true" /> <GUI_isPanelDisplayed isFalse="false" debugBreak="false" panelId="ACTIVE_PAUSE" dynamicPanelId="" isDisplayed="false" /> <GUI_isPanelDisplayed isFalse="false" debugBreak="false" panelId="INGAME_TRAVEL_WORLDMAP" dynamicPanelId="" isDisplayed="false" /> <GUI_isPanelDisplayed isFalse="false" debugBreak="false" panelId="CRAFT" dynamicPanelId="" isDisplayed="false" /> <GUI_isPanelDisplayed isFalse="false" debugBreak="false" panelId="TRADE" dynamicPanelId="" isDisplayed="false" /> <hasSkillPlayingTargeted on="SELF" skillBindInput="gun_shoot" isFalse="true" /> <hasSkillPlayingTargeted on="SELF" skillBindInput="block" /> <hasSkillPlayingTargeted on="SELF" skillBindInput="dodge_any" /> <hasSkillPlayingTargeted on="SELF" skillBindInput="att1"/> <hasSkillPlayingTargeted on="SELF" skillBindInput="att2"/> <hasSkillPlayingTargeted on="SELF" skillBindInput="block"/> <INPUT_Command isFalse="false" debugBreak="false" commandId="LOCK_TARGET" dynamicCommandId="" disableCommandCurrentFrame="false" /> <INPUT_Command isFalse="false" debugBreak="false" commandId="ACTIVE_PAUSE_BIND" dynamicCommandId="" disableCommandCurrentFrame="false" /> <INPUT_Command isFalse="false" debugBreak="false" commandId="MOVE_RUN" dynamicCommandId="" disableCommandCurrentFrame="true" /> <insideTriggerTargeted isFalse="false" debugBreak="false" on="MAIN" onParam="" triggerName="ZoneAggro_MQ06_Puzzle_Fail_Mobs" /> <interactionTargeted isFalse="false" debugBreak="false" on="NAMED" onParam="CrossObject_Palace_BigRoom_DoorA" interactionName="quest_event_open_doubledoor_a" /> <isCompanionInTeam isFalse="false" debugBreak="false" companionTag="huf_comp_id_mq24_alchemist" / <isCompanionInTeam isFalse="false" debugBreak="false" companionTag="huf_comp_aphra" /> <isCompanionInTeamTargeted isFalse="false" debugBreak="false" on="SELF" onParam="" /> <isCompanionInTeamTargeted isFalse="true" on="SELF" /> <isCompanionLocked isFalse="true" debugBreak="false" companionTag="hum_comp_petrus" /> <isCompanionRecruited companionTag="%VAR_COMPANION_TAG%" /> <isCurrentLevel isFalse="false" debugBreak="false" value="lvl_01_continent_city" /> <IsCurrentDayTime isFalse="false" debugBreak="false" minHour="21" maxHour="24" /> <IsCurrentDayStage isFalse="false" debugBreak="false" dayStage="NIGHT_ONLY" /> <IsCurrentDayStage isFalse="false" debugBreak="false" dayStage="DARK_NIGHT" /> <IsCurrentDayStage isFalse="false" debugBreak="false" dayStage="NIGHT_SIMPLE" /> <IsCurrentDayStage isFalse="false" debugBreak="false" dayStage="DAY_ONLY" /> <isFightingTargeted isFalse="false" on="SELF" /> <isFightingTargeted isFalse="false" debugBreak="false" on="MAIN" onParam="" squadMode="ALL" /> <isFightingTargeted isFalse="false" debugBreak="false" on="NAMED" onParam="Squad_HarborSecurity_01" squadMode="AT_LEAST_ONE" /> <IsInDialog isFalse="true" debugBreak="false" dialogFullInPlace="Both" dialogType="All" speakerID="cha_hum_bridge_mq24_soldier_ingeneer" /> <isInPlayerCloseRangeTargeted isFalse="false" debugBreak="false" on="SELF" onParam="" /> <isInPlayerCloseRangeTargeted isFalse="false" debugBreak="false" on="SELF" onParam="" /> <isInPlayerInteractionRangeTargeted isFalse="false" debugBreak="false" on="SELF" onParam="" /> <isMapZoneDiscovered zoneID="mainzone" mapID="lvl_02_south_city" value="true" /> <isSpawnedTargeted isFalse="false" debugBreak="false" on="NAMED" onParam="SpawnCharacter_WizardCastle_ExtremistMageLeader" /> <IsVariableInt isFalse="false" debugBreak="false" name="V_MQ08_EscortStatus" value="INT_MAX" minValue="1" maxValue="2" /> <IsVariableGoalValueReached isFalse="false" debugBreak="false" name="V_SQGE02_ObjectiveCounter" /> <itemCount itemId="item_potion_life" min="1" /> <itemEquippedTargeted isFalse="false" debugBreak="false" on="NAMED" onParam="Actor_COMP_Aphra" itemName="bod_hum_ccm_medium_01" slot="NONE" loadoutIndex="-1" questFlag="NONE" /> <MainCharacterSex isFalse="false" debugBreak="false" sex="MALE" /> <MainCharacterSex isFalse="false" debugBreak="false" sex="FEMALE" /> <Never isFalse="false" debugBreak="false" /> <Once isFalse="false" debugBreak="false" value="true" /> <- Note: Evaluates to true on first check only. <poiHasDummy isFalse="false" debugBreak="false" on="SELF" onParam="" dummyName="dum_poi_02" /> <Quest_State isFalse="false" debugBreak="false" quest="MQ08_ScientificExp_S_EscortScientists" state="ACTIVE" /> <raceTargeted isFalse="false" debugBreak="false" on="SELF" onParam="" value="liz" /> <randomChance isFalse="false" debugBreak="false" value="0.3" /> <ReputationStage isFalse="false" debugBreak="false" groupName="R_HUM_COMP_VASCO" stage="NONE" minStage="NICE" maxStage="NONE" /> <ReputationStage isFalse="false" debugBreak="false" groupName="R_FACTIONS_ALLIES_NATIVE" stage="NONE" minStage="NONE" maxStage="NICE" /> <ReputationStage isFalse="false" debugBreak="false" groupName="R_FACTIONS_ALLIES_BRIDGE" stage="NONE" minStage="NICE" maxStage="NONE" /> <ReputationStage isFalse="false" debugBreak="false" groupName="R_FACTIONS_ALLIES_WIZARD" stage="NONE" minStage="FRIENDLY" maxStage="NONE" /> <ReputationStage isFalse="false" debugBreak="false" groupName="R_FACTIONS_ALLIES_DENIER" stage="NONE" minStage="FRIENDLY" maxStage="NONE" /> <secondaryEffectResult value="IS_FROMBEHIND" /> <SkillTreeItemBound isFalse="false" debugBreak="false" idSkillTree="skilltree_item_potion_life" /> <SkillTreeItemBound isFalse="true" debugBreak="false" idSkillTree="skilltree_traps" /> <skillTreeItemUnlockedTargeted on="SELF" idSkillTree="skilltree_grenades_range" isFalse="true" /> <skillTreeItemUnlocked isFalse="false" debugBreak="false" idSkillTree="skilltree_traps" /> <stateValueTargeted type="INERTIA_SPEED" value="RUN" on="SELF" /> <stateValueTargeted type="INERTIA_SPEED" value="SPRINT" on="SELF" /> <stanceTargeted isFalse="false" debugBreak="false" on="NAMED" onParam="Chest_CQSI01_LockedChestDocB" value="OBJECT_LOCKED" /> (Is object locked) <stanceTargeted isFalse="false" debugBreak="false" on="NAMED" onParam="CrossObject_MQ08_JailDoor" value="OBJECT_OPEN" /> (Is object unlocked) <tagTargeted isFalse="false" debugBreak="false" on="SELF" onParam="" value="dee_deer01" /> <targets_poi isFalse="false" debugBreak="false" />
Most conditions just check things. but some conditions are special. They
actually DO something. The main 2 examples of this are:
<interactionTargeted isFalse="false" debugBreak="false" on="MAIN" onParam="" interactionName="int_ld_pla_speak" />
and
<Once isFalse="false" debugBreak="false" value="true" />
1) interactionTargeted
Causes an interaction to appear in front of the player (MAIN) or on an
object (NAMED). If it is an object, the onParam will contain the name
of the object. For example "Actor_Merchant_foo" or "some_chest".
The interactionName points to a pre-defined interaction name.
Interactions are defined in the file:
game_components/game_components_ld/game_components_ld_interactions.sli
For example, "int_ld_pla_speak" puts "Speak" on the screen. But there is
also the interaction "speak_down" that puts an actors name into the
interaction. In order for those interactions to work, they have to be
attached to NAMED objects (like actors). IE: "Speak to Siora".
"Interactions" are tricky because in addition, the quests script needs to
handle what to do if the player chooses the interaction. Sometimes
interactions will have their own conditions and actions associated with
them in their definitions.
2) Once
The once tags magically track if the block has ever been tested before.
First time you hit them, they evaluate to true and they evaluate to false
after that. It is not clear how the game does this. It also isn't clear
if once will evaluate to true a second time if the script is updated.
(IE: You install a mod that replaces the original .sqg file with an
updated version).
===============================================================================
V. > > > > Dialogues (SDG FILE Breakdown)
===============================================================================
Most dialogue definitions are found under the directory:
datalocal\dialog_graphs
You can open SDG Files with a plain-text editor and see the declarative, XML
structured data within.
At the top of a dialogue file, is the dialogueGraph tag identifying the
contents of the file as being a dialogueGraph.
A. dialogGraph
--------------
<dialogGraph id="aphra_meet_constantin" dialogType="Cinematic" stage="cut_stage_default_02" dialogLevelPosition="" playerSideMainSpeaker="<--DEFAULT-->" interlocutorSideMainSpeaker="cha_huf_aphra">
dialogType : [Ambiance | Cinematic | FreeCam ]
stage : [ "" | cut_stage_default_01 |
cut_stage_default_01_camv |
cut_stage_default_01_camv_02 |
cut_stage_default_01_examine |
cut_stage_default_01_governorpalace |
cut_stage_default_01_mainint_leanback |
cut_stage_default_01_mainint_leanelbow |
cut_stage_default_01_mainint_sitarmchair |
cut_stage_default_01_mainint_sitarmchair_constantinpalace |
cut_stage_default_01_mainint_sitarmchair_torsteinpalace |
cut_stage_default_01_mainint_sitstool |
cut_stage_default_01_mainint_sitstool_02 |
cut_stage_default_01_mainint_stand_shop |
cut_stage_default_01_without_comp |
cut_stage_default_02 |
cut_stage_default_02_int_01_side |
cut_stage_default_02_mainint_far |
cut_stage_default_02_mainint_far_02 |
cut_stage_default_03 |
cut_stage_default_04 |
cut_stg_01_sitarmchair_mainint_pla_comp02
]
dialogLevelPosition : ["" |
DlgStgPos_cqpe03_hybrid_woman_askshaman |
DlgStgPos_test_lvl_test_dialog
]
playerSideMainSpeaker : ["<--DEFAULT-->" |
pla_hero |
cha_huf_aphra |
cha_huf_rebelcamp_chief |
cha_huf_sqgd04_denier_lieutnant_c |
cha_hum_donaghvillage_younghumanboy |
cha_hum_mq01_kidnap_guard |
cha_hum_npc_native_clanchief_shaman |
cha_hum_sqau04_native_trader |
cha_hum_sqau05_warrior_spy_01 |
cha_hum_sqgd02_gdd_intermediary |
cha_hum_sqgd03_gdd_customs_officer |
cha_hum_sqgd04_denier_real_anton |
]
interlocutorSideMainSpeaker : ["<--DEFAULT-->" |
cha_huf_donaghvillage_younghybridgirl |
cha_hum_constantin |
cha_hum_kurt |
cha_huf_siora |
cha_hum_vasco |
cha_huf_siora_sister |
cha_hum_cqap02_rogue_01 |
cha_hum_cqku02_soldier_a |
cha_hum_cqku03_traitor_captain |
cha_hum_cqku03_traitor_lieut_a |
cha_hum_cqku03_traitor_lieut_mob_a |
cha_hum_donaghvillage_younghumanboy |
cha_hum_merchant_trader_01 |
cha_hum_mq01_kidnap_guard |
cha_hum_npc_native_travel_guide |
cha_hum_rebelcamp_chief_lieutnant |
cha_hum_sqau04_rebel_squad_leader |
cha_hum_sqau05_extremist_mage_01 |
cha_hum_sqau05_warrior_spy_01 |
cha_hum_sqgd03_gdd_customs_officer |
cha_hum_sqgd04_denier_officer_franz |
cha_hum_sqgd05_denier_captain_ulric
]
How do I figure this stuff out?
-------------------------------
I use cygwin commands to parse and print uniq portions of the data. IE:
$ grep -Hir 'dialogGrap' . | grep -e 'stage="[^"]*"' -o | sort -u | uniq
1) grep -Hir 'dialogGrap' . : Search current and subdirectories for
'dialogGrap' and output the name of the file and the line that the
string is found on.
2) redirect output to another grep that searches for a Regular Expression
within each line that matches an attributeName="..." ([^"] means "is
not a quote) and the * that follows it means the previous element will
appear zero or more times. Not the "-o" option, which means only
output the matching regex, not the whole line.
3) redirect output into a sort. This pushes all the empty lines to the
top of the output.
4) redirect to "uniq"... this scans a sorted list of lines and removes
2 adjacent lines if they are identical, leaving on the unique results.
The dialogueGraph is followed by a circuitInfo block:
B. circuitInfo
--------------
As far as I can tell, the circuitInfo block exists for the silk-engine
tools so they can quickly open the file and deduce what it does without a
full parse. In other words, the contents don't matter and only serve the
game development tools (which we don't have). So you don't need to worry
about the contents of the circuitInfo or keeping it in sync with any
changes you make.
The third element is a Speakers block.
C. speakers
-----------
<speakers> <speaker speakerId="cha_hum_constantin" tag="" nameInGame="" defaultCamera="" talk="" listen="" idle="" mood="<-- DEFAULT -->" stagePos="" animPosture="STAND" animBodyPose="" forceIncludeCompanion="false" allowLipSync="" displayName="true" soundPreset="" /> <speaker speakerId="cha_huf_aphra" tag="" nameInGame="" defaultCamera="" talk="" listen="" idle="" mood="<-- DEFAULT -->" stagePos="way_interlocutor_01" animPosture="STAND" animBodyPose="" forceIncludeCompanion="false" allowLipSync="" displayName="true" soundPreset="" /> <speaker speakerId="pla_hero" tag="" nameInGame="" defaultCamera="" talk="" listen="" idle="" mood="<-- DEFAULT -->" stagePos="way_interlocutor_02" animPosture="STAND" animBodyPose="" forceIncludeCompanion="false" allowLipSync="" displayName="true" soundPreset="" /> <speaker speakerId="cha_huf_siora" tag="" nameInGame="" defaultCamera="" talk="" listen="" idle="" mood="<-- DEFAULT -->" stagePos="way_interlocutor_main" animPosture="STAND" animBodyPose="" forceIncludeCompanion="false" allowLipSync="" displayName="true" soundPreset="" /> <speaker speakerId="cha_hum_vasco" tag="" nameInGame="" defaultCamera="" talk="" listen="" idle="" mood="<-- DEFAULT -->" stagePos="" animPosture="STAND" animBodyPose="" forceIncludeCompanion="false" allowLipSync="" displayName="true" soundPreset="" /> <speaker speakerId="cha_hum_kurt" tag="" nameInGame="" defaultCamera="" talk="" listen="" idle="" mood="<-- DEFAULT -->" stagePos="" animPosture="STAND" animBodyPose="" forceIncludeCompanion="false" allowLipSync="" displayName="true" soundPreset="" /> <speaker speakerId="cha_hum_petrus" tag="" nameInGame="" defaultCamera="" talk="" listen="" idle="" mood="<-- DEFAULT -->" stagePos="" animPosture="STAND" animBodyPose="" forceIncludeCompanion="false" allowLipSync="" displayName="true" soundPreset="" /> </speakers>
This block identifies the POTENTIAL speakers that may partake in the
dialogue. However, not all speakers will necessarily be present.
tag = [ "" |
huf_comp_siora |
huf_npc_denier_sieglinde |
hum_comp_native_highking_shaman |
hum_mq22_camv
]
defaultCamera = [ "" |
american_std_l |
american_std_r
]
talk = [ "" ]
listen = [ "" ]
idle = [ "" ]
mood = [ "<-- DEFAULT -->" |
ANGRY_AGITATED |
ANGRY_RELAXED |
CLEVER_RELAXED |
HAPPY_RELAXED |
IRONIC_RELAXED |
IRRITATED_AGITATED |
IRRITATED_FIXED |
IRRITATED_RELAXED |
LOST_RELAXED |
NEUTRAL_FIXED |
NEUTRAL_RELAXED |
RESOLUTE_RELAXED |
SAD_RELAXED |
SCARED_AGITATED |
SCARED_RELAXED |
SILLY_AGITATED |
SMILING_RELAXED
]
animBodyPose = [ CROSSEDARMS,
HANDSBEHIND,
WOUNDED,
2HANDSHIP,
ELBOWSKNEES,
FIXED
]
animPosture = [ LEANBACK,
LEANELBOW,
SITARMCHAIR,
SITSTOOL,
STAND
]
Here are some of the supported combinations extracted from:
dialogues/autogen_dialogue_animations.sli
animPosture="LEANBACK" animBodyPose="CROSSEDARMS"
animPosture="LEANELBOW" animBodyPose="1HANDHIP"
animPosture="LEANELBOW" animBodyPose="2HANDSHIP"
animPosture="LEANELBOW" animBodyPose="AGITATED"
animPosture="LEANELBOW" animBodyPose="CROSSEDARMS"
animPosture="LEANELBOW" animBodyPose="FIXED"
animPosture="LEANELBOW" animBodyPose="FIXED"
animPosture="LEANELBOW" animBodyPose="FIXED"
animPosture="LEANELBOW" animBodyPose="HANDSBEHIND"
animPosture="LEANELBOW" animBodyPose="HOLDARM"
animPosture="SITARMCHAIR" animBodyPose="CROSSEDARMS"
animPosture="SITARMCHAIR" animBodyPose="ELBOWSKNEES"
animPosture="SITARMCHAIR" animBodyPose="FIXED"
animPosture="SITSTOOL" animBodyPose="CROSSEDARMS"
animPosture="SITSTOOL" animBodyPose="ELBOWSKNEES"
animPosture="STAND" animBodyPose="1HANDHIP"
animPosture="STAND" animBodyPose="2HANDSHIP"
animPosture="STAND" animBodyPose="AGITATED"
animPosture="STAND" animBodyPose="CROSSEDARMS"
animPosture="STAND" animBodyPose="FIXED"
animPosture="STAND" animBodyPose="FIXED"
animPosture="STAND" animBodyPose="FIXED"
animPosture="STAND" animBodyPose="HANDSBEHIND"
animPosture="STAND" animBodyPose="HOLDARM"
animPosture="STAND" animBodyPose="WOUNDED"
D. Blocks
The blocks parent element is where the meat of the dialogue definition is.
Primary tags:
1. start
<start name="Start"> <connections> <connection blockName="May I help you?" inputPinIndex="0" /> </connections> </start>
The start tag defines the entry point. Notice it doesn't have as many
attributes as the start QUEST element. Also notice that the
connection uses the attribute "blockName" to redirect instead of
the Quest versions "target".
There can be multiple start tags in an SDG file. The SQG file can
choose which one to use by using the start name in the targetPinName
of the connection that points to the DialogueBlock that kicks off the
dialogue. For example, to enter the SDG using the "AltStart" name:
(SQG File)
2. propositionBlock
<propositionBlock name="May I help you?"> <connections> <connection blockName="Choice" inputPinIndex="0" /> </connections> <propositions> <proposition locKey="#dlg_talk_lvl02_barman_1" textSample="May I help you?" annotate="" todo="" speaker="cha_hum_lvl02_barman" charTag="" allowLipSynch="<-- DEFAULT -->" speakerTargetPreset="<--DEFAULT-->" speakerTargetCustomName="" talkMood="<-- DEFAULT -->" talkAnim="" listenersGeneralMood="<-- DEFAULT -->" listenAnim="" endingTransition="Normal" soundPreset="" cutscene="" csRelativeTo="" camera="" cameraTarget="" isSkippable="true" /> </propositions> </propositionBlock>
Proposition Blocks are used to make NPCs speak. Each "proposition" has
a speaker tag that identifies the NPC who will be speaking the line.
Typically it alternates between some NPC and the Player:
speaker="pla_hero"
Remember that not every propositionBlock is associated with a
conversation. Sometimes you simply hear people talking as you walk
around. For example, the merchant that shouts "Come one, come all and
see my wares!".
Specific lines can override the global default settings set above in
the <speakers> tag. So if you want someone's mood to change because of
something that was said, you can specify that by providing a local
talkMood value for example.
In propositionBlocks, the lockKey references an audio file defined in
a sound bank associated with the the SDG file. This is ultimately how
the game knows what audio to play and what subtitles to use. More on
that below in the in section V.2
proposition elements can also kick off cutscenes after a spoken line if
one is provided by the cutscene attribute.
3. choiceBlock
<choiceBlock name="Choice"> <connections> <connection blockName="LongWindedTalkAboutPast" inputPinIndex="0" /> <connection blockName="StartAQuest" inputPinIndex="0" /> <connection blockName="HasEnoughGoldForAbeer" inputPinIndex="0" /> </connections> <choices> <choice locKey="#dlg_talk_lvl02_barman_9" textSample="His presence on the island" annotate="" todo="" speaker="pla_hero" charTag="" allowLipSynch="<-- DEFAULT -->" speakerTargetPreset="<--DEFAULT-->" speakerTargetCustomName="" talkMood="<-- DEFAULT -->" talkAnim="" listenersGeneralMood="<-- DEFAULT -->" listenAnim="" endingTransition="Normal" soundPreset="" cutscene="" csRelativeTo="" camera="" cameraTarget="" isSkippable="true"> <conditionSet isFalse="false" debugBreak="false" mode="ALL" /> </choice> <choice locKey="#dlg_talk_lvl02_barman_56" textSample="His business" annotate="" todo="" speaker="pla_hero" charTag="" allowLipSynch="<-- DEFAULT -->" speakerTargetPreset="<--DEFAULT-->" speakerTargetCustomName="" talkMood="<-- DEFAULT -->" talkAnim="" listenersGeneralMood="<-- DEFAULT -->" listenAnim="" endingTransition="Normal" soundPreset="" cutscene="" csRelativeTo="" camera="" cameraTarget="" isSkippable="true"> <conditionSet isFalse="false" debugBreak="false" mode="ALL" /> </choice> <choice locKey="#dlg_talk_lvl02_barman_8" textSample="Buy A Beer" annotate="" todo="" speaker="pla_hero" charTag="" allowLipSynch="<-- DEFAULT -->" speakerTargetPreset="<--DEFAULT-->" speakerTargetCustomName="" talkMood="<-- DEFAULT -->" talkAnim="" listenersGeneralMood="<-- DEFAULT -->" listenAnim="" endingTransition="Normal" soundPreset="" cutscene="" csRelativeTo="" camera="" cameraTarget="" isSkippable="true"> <conditionSet isFalse="false" debugBreak="false" mode="ALL" /> </choice> </choices> </choiceBlock>
Choice blocks present menus on the screen that the player can select.
Each Choice has a conditionset that determines if it appears or not.
The connections at the top correlate to the relative position of the
choices on the bottom.
ChoiceBlocks can connect to secondary ChoiceBlocks to simulate menus.
In a ChoiceBlock, the locKey is a reference to a String defined in a
language localized file:
(datalocal/text/<sdg_file_name>_eng.txt>)
The attributes of the choices are confusing. Greedfall supports
inheritance, and I believe the Choice elements inherit from generic
elements that have attributes like "speaker" and 'allowLipSynch", that
make no sense when you are simply displaying text on the screen. I'm
90% sure you can ignore every attribute aside from locKey and be
absolutely fine.
** NOTES: I have tried altering the text in the .txt files to no avail.
This is true for both dialogues and item descriptions.
I believe the strings were statically compiled into the game. Or
maybe there is a detail I am missing. Maybe with text files, you do
in fact have to place them in the datalocal/text directory. Don't
know. Maybe someone with more patience than me will figure this
out and tell me.
What I HAVE found is that one can inject text directly into the
locKey attribute and it will display. IE:
<choice locKey="Buy A Beer" textSample="" ..... > <conditionSet isFalse="false" debugBreak="false" mode="ALL" /> </choice>
Choice text also supports a number of special sequences for things
like icons in the text. Some examples:
@ICONE(img_misc_love_s)
@ICONE(img_book_talents_alchemy_s)
@ICONE(img_book_talents_craft_s)
@ICONE(img_map_marker_merchant_s)
@NAT<yellowtext>@END <- typically used to hi-light native words
4. test
Similar to the SQG testBlock, but with just a name attribute. SQG
ConditionSets are fully supported.
5. commandAction
<commandAction name="SorryNotEnough"> <connections> <connection blockName="MoveAlong" inputPinIndex="0" /> </connections> <actionSet> <actions> <LaunchInfoMessage id="quickinfo_not_enough_gold" /> </actions> </actionSet> </commandAction>
Similar to the SQG commandActionBlock, but with just a name
attribute. All SQG actions are fully supported.
6. setVariable
<setVariable name="BuyBeer" varName="BuyBeer" value="1"> <connections> <connection blockName="RemoveGold" inputPinIndex="0" /> </connections> </setVariable>
Similar to the SQG setVariableBlock, but with just a name,
varName and value attribute.
7. removeItemBlock
<removeItemBlock name="RemoveGold" itemId="item_gold" count="5"> <connections> <connection blockName="Reward" inputPinIndex="0" /> </connections> </removeItemBlock>
remove an item from the players inventory.
** You can use rewardBlock or CommandAction -> AddItem to
give the player money/items/experience (XP is normally
connected to loot as part of a reward).
8. launchDialog
Yes, a dialogue can redirect to another dialogue. If you need to
redirect to a specific start tag on another dialogue, consider the
commandAction LaunchDialog, as it supports an entry point parameter.
9. rewardBlock
<rewardBlock name="Reward" lootId="loot_barman_alcool" visible="true"> <connections> <connection blockName="LongWindedTalkAboutPast" inputPinIndex="0" /> </connections> </rewardBlock>
Loot definitions can be found under:
/datalocal/loots
Examples:
- loot_all_recipes
- loot_all_equipement
- loot_all_uniques
- loot_all_natives
- loot_25000_xp
- loot_default_ingredients
See: VI.5 Loot and Rewards
10 incrementVariable
<incrementVariable name="IncVar" varName="V_ROM_VASCO_SeductionPt" value="1"> <connections> <connection blockName="SetVariable3" inputPinIndex="0" /> </connections> </incrementVariable>
11 end
<end name="accept love" />
Similar to the SQG endBlock, but with just a name. Note that the Name
of the chosen end tag will often determine what the dialogBlock does
when it returns to the SQG file. IE: The connection with the
matching pinName will be followed when the thread returns to the
quest handler.
As you can see there are a lot of similarities with Quests in terms of
what dialogues can do. However there are some block types that ONLY exist
in dialogues.
12 testCompanion
<testCompanion name="TestCompanion"> <connections> <connection blockName="vascoSaySomething" inputPinIndex="0" /> <connection blockName="kurtSaySomething" inputPinIndex="0" /> <connection blockName="" inputPinIndex="0" /> <connection blockName="" inputPinIndex="0" /> <connection blockName="petrusSaySomething" inputPinIndex="0" /> <connection blockName="constantineSaySomething" inputPinIndex="0" /> <connection blockName="SioraAndAphraAreCompanions" inputPinIndex="0" /> </connections> <tagOrder> <companion characterTag="hum_comp_vasco" /> <companion characterTag="hum_comp_kurt" /> <companion characterTag="huf_comp_siora" /> <companion characterTag="huf_comp_aphra" /> <companion characterTag="hum_comp_petrus" /> <companion characterTag="hum_comp_constantin" /> <companion characterTag="__NONE__" /> </tagOrder> </testCompanion>
Test companion tests for the presence of tags in your current
companions using the order provided and when it has a hit, fires the
corresponding connection block unless the block is empty (""), in which
case it keeps going.
Because it is "first found speaks", order is important. It also
provides an easy way to detect if 2 companions are not with the player
thanks to the final "__NONE__" tag, which is only reached if no one
else speaks.
** Behavior of this element needs confirmation.
13 addReputationBlock
<addReputationBlock name="NAUTES_LIKEY" reputationName="R_FACTIONS_ALLIES_NAUTE" value="5"> <connections> <connection blockName="foundSailor" inputPinIndex="0" /> </connections> </addReputationBlock>
reputationName : [
R_FACTIONS_ALLIES_BRIDGE |
R_FACTIONS_ALLIES_DENIER |
R_FACTIONS_ALLIES_MERCHANT |
R_FACTIONS_ALLIES_NATIVE |
R_FACTIONS_ALLIES_NAUTE |
R_FACTIONS_ALLIES_WIZARD |
R_HUF_COMP_APHRA |
R_HUF_COMP_SIORA |
R_HUM_COMP_KURT |
R_HUM_COMP_PETRUS |
R_HUM_COMP_VASCO
]
Adjust the reputation with people or groups. Use negative values to
decrease reputation.
14 fadeInOut
The fadeInOut element can be used to visually simulate the passing of
time or can be used to lead into cutscenes to make the transitions
smoother.
15 addToTeamBlock
<addToTeamBlock name="huf_comp_siora" companionTag="huf_comp_siora"> <connections> <connection blockName="TeamManagementGUI" inputPinIndex="0" /> </connections> </addToTeamBlock>
addToTeamBlock is normally used with setCompanionLockBlock and
teamManagementGUIBlock, to allow you to start a quest that requires a
specific companion. IE: Bring up the gui with the companion added (in
case they were not following you when you spoke to them), a lock next
to their name and force you to get rid of someone if necessary.
16 teamManagementGUIBlock
<teamManagementGUIBlock name="TeamManagementGUI" forceShow="true" confirm="true"> <connections> <connection blockName="SetCompanionLock" inputPinIndex="0" /> </connections> </teamManagementGUIBlock>
I know this brings up the team management gui, but I don't know much
else about it. IE: What happens if confirm is false?
17 setCompanionLockBlock
<setCompanionLockBlock name="SetCompanionLock" companionTag="huf_comp_siora" locked="true"> <connections> <connection blockName="some_siora_required_quest_canstart" inputPinIndex="0" /> </connections> </setCompanionLockBlock>
Can be used to set a companion as required... or unset them as required
by using locked="false".
18 questLog
<questLog name="PayServant-ACTIVE" questName="SQNA01_FamilyAffair_S_PayServant" state="ACTIVE"> <connections> <connection blockName="if enough gold" inputPinIndex="0" /> </connections> </questLog>
Use this element to set state of quests.
state : [ ACTIVE | FAIL | INACTIVE | NONE | SUCCESS ]
19 random
<random name="Random" forceCycleThroughOutputs="true"> <connections> <connection blockName="proposition1" inputPinIndex="0" /> <connection blockName="proposition2" inputPinIndex="0" /> </connections> <randomOutputs> <randomOutput weight="50" /> <randomOutput weight="50" /> </randomOutputs> </random>
An way to have entire dialogue branches random. Don't forget that
conditionSets that can be attached to many things also support the
randomChance element.
Adding a Menu
--------------
If a the blockName of a Choice block connection links to a launchDialog
block, this will cause an arrow to appear in the choice menu, indicating
a submenu.
Below the user would see:
[ Menu1 > ]
[ Menu2 > ]
[ Close ]
Example:
<choiceBlock name="Choice"> <connections> <connection blockName="menu1" inputPinIndex="0" /> <connection blockName="menu2" inputPinIndex="0" /> <connection blockName="close" inputPinIndex="0" /> </connections> <choices> <choice locKey="Menu1" textSample="" annotate="" todo="" speaker="pla_hero" charTag="" allowLipSynch="<-- DEFAULT -->" speakerTargetPreset="<--DEFAULT-->" speakerTargetCustomName="" talkMood="<-- DEFAULT -->" talkAnim="" listenersGeneralMood="<-- DEFAULT -->" listenAnim="" endingTransition="Normal" soundPreset="" cutscene="" csRelativeTo="" camera="" cameraTarget="" isSkippable="true"> <conditionSet isFalse="false" debugBreak="false" mode="ALL"/> </choice> <choice locKey="Menu2" textSample="" annotate="" todo="" speaker="pla_hero" charTag="" allowLipSynch="<-- DEFAULT -->" speakerTargetPreset="<--DEFAULT-->" speakerTargetCustomName="" talkMood="<-- DEFAULT -->" talkAnim="" listenersGeneralMood="<-- DEFAULT -->" listenAnim="" endingTransition="Normal" soundPreset="" cutscene="" csRelativeTo="" camera="" cameraTarget="" isSkippable="true"> <conditionSet isFalse="false" debugBreak="false" mode="ALL"/> </choice> <choice locKey="Close" textSample="" annotate="" todo="" speaker="pla_hero" charTag="" allowLipSynch="<-- DEFAULT -->" speakerTargetPreset="<--DEFAULT-->" speakerTargetCustomName="" talkMood="<-- DEFAULT -->" talkAnim="" listenersGeneralMood="<-- DEFAULT -->" listenAnim="" endingTransition="Normal" soundPreset="" cutscene="" csRelativeTo="" camera="" cameraTarget="" isSkippable="true"> <conditionSet isFalse="false" debugBreak="false" mode="ALL"/> </choice> </choices> </choiceBlock> <launchDialog name="menu1" dialogName="aphra_romance"> <connections> <connection blockName="BridgeStart1" inputPinIndex="0" /> </connections> </launchDialog> <bridgeStart name="BridgeStart1"> <connections> <connection blockName="BridgeEnd" inputPinIndex="0" /> </connections> </bridgeStart> <bridgeEnd name="BridgeEnd"> <connections> <connection blockName="Choice" inputPinIndex="0" /> </connections> </bridgeEnd> <end name="End" />
I'm not 100% certain what the bridgeStart/Ends do (They are used by most
talk based menus), but I suspect they are somehow responsible for menu
condensing. IE: When a sub-menu only has 1 choice that would display,
the parent menu just displays that choice instead of a menu. And when a
submenu has nothing to display, the menu goes away all together. There are
plenty of examples of menus in the game if you look. For example, check out:
dialog_graphs/talk_companions/kurt_talk.sdg
Other less common Blocks:
-------------------------
Most of these are rare because they normally aren't handled from dialogue,
but rather their parent quests.
Location Location Location:
---------------------------
If a dialogue only involves two speakers, then things are simple from a
camera perspective. The game has cameras set up to handle two speaker
interactions with the player without anything special.
However... If you want more than two speakers, it gets tricky. The camera
likes to rotate between the speakers. If one of the speakers isn't around.
the camera may suddenly point at a wall or at the floor.
This isn't an issue for most dialogues as most are limited to the player
and someone else. However... what if you need a companion to speak up who
may or may not be traveling with the player? This often happens in the
game when reporting to quest givers like constantine.
In this case (need someone to speak up who might not be around), you can
specify a waypoint marker to teleport a speaker to when the dialogue
begins. Most of these markers are statically embedded into the maps.
Which means conversations that involve interjections from companions who
are not traveling with you normally happen in throne rooms and around
campfires.
The attribute to specify a waypoint is called stagePos and there are a
number of common values:
stagePos="way_player"
stagePos="way_interlocutor_main"
stagePos="way_interlocutor_01"
stagePos="way_interlocutor_02"
stagePos="way_interlocutor_03"
stagePos="way_companion_01"
stagePos="way_companion_02"
stagePos="way_companion_03"
stagePos="way_companion_04"
stagePos="way_companion_05"
stagePos="way_companion_06"
stagePos="way_hide"
===============================================================================
VI. > > > > Audio
===============================================================================
-------------------------------------------------------------------------------
VI.1 > > > > Spoken Audio
-------------------------------------------------------------------------------
A. Where is it?
Spoken audio lines can be found under:
datalocal/voice_dialog/<CHARACTER>/*.pgz
The PGZ Files are gzipped PSSG files. (See above).
B. Can I extract everything to listen to it?
Yes, though it takes a few steps and even these directions only tackle
one NPC at a time.
1) Goto the directory of an NPC who's lines you wish to extract
ie: .../datalocal/voice_dialog/pla_hero
2) Right click the background and select New -> Folder. Name
it "extracted"
3) Select all the PGZ files (Click on first, then scroll to bottom
and SHIFT + CLICK last file).
4) Right click the selected files and select: 7-Zip -> Extract files...
5) Tell 7-zip to extract the files in your new "extracted" directory.
7-zip will create a sub-directory for each extarcted .pgz file.
6) Place a copy of pssgOggExtract.py in the /extracted directory
7) Startup CYGWIN. In the terminal window, cd to the extracted
directory. The command will look something like:
$ cd /cygdrive/c/Steam/steamapps/common/datalocal/voice_dialog
$ cd <CHARACTER>/extracted
8) Use CYGWIN to exectute pssgOggExtract on all files. IE:
$ for f in **/* ; do python pssgOggExtract.py $f; done
C. How does it connect to dialogues?
Connecting spoken audio lines to game dialogues requires 2 files.
1) Every SDG (dialogue) file has a BANK file and a BANK COFIG filename
that indicate what audio lines to load when the dialogue is invoked.
If you simply try to paste a line between 2 separate SDG files, it
won'r work because at a minimum, the BANK file must also be updated.
BANK files are located under:
.../datalocal/banks/
And BANK CONFIG files are located under:
.../datalocal/sounds/banks/voices/voice_dialog/eng/
Note that the BANK directory has bank files for both SDG files
and other resources like background music and ambient noises.
As far as I can tell, the game engine uses file name conventions
to search for associated files. So if the SDG file was named:
aphra_romance.sdg
Then the game would looke for the BANK CONFIG:
bk_voc_eng_aphra_romance.sli
under the directory
.../datalocal/sounds/banks/voices/voice_dialog/eng/
At the top of the BANK CONFIG, it indicates the name of the bank
file to use. It also summarizes the contents, however changes to the
summary do not seem to have any impact on the game.
Technically this means that BANK files could be named anything, but
in practice, they all follow a convention. I tend to just ignore
the sli files.
The actual BANK file associated with aphra_romance.sdg is:
banks/bk_voc_aphra_romance.pgz
If you use 7-zip to decompress this file, and then use EGO PSSG
EDITOR to open in, you will see the IDs of the audio data as
well as metadata such as the length of the audio file. Note the
bank simply helps the engine know when to load the audio data
into memory. It doesn't actually have any audio in it, just
pointers to a list of files to load.
Finally with a bank in place, the "proposition" elements of
the SDG files can now make use of those ids in the locRefs
attribute.
So in summary: A quest kicks off an SDG file. The game engine
loads the config, then the bank and finally all the audio lines
identified by the bank. Then when the SDG proposition elements
make use of the audiofile ids, the audio is played.
The NPC who speaks the audio is identified by the proposition
elements 'speaker' tag.
D. Editing/Updating a BANK
1) Find the bank name (.pgz)
2) Extract it using 7-zip
3) Convert it to XML using the PSSG Editor:
- highlight top-most node and select Node -> Export to XML
4) Edit the XML. If you are re-using/exposing lines from other
conversations, extract the other conversation to XML as well
and copy the corresponding pieces across. Note there are two
parts needed for each line you wish to re-use: A one line
AUDIODATAPROXY element in the top and a 4 line SOUNDBANKITEM
Element at the bottom. Order should match up. Don't forget to
update the TYPEINFO/typeCount info for the AUDIODATAPROXY and
SOUNDBANKITEM at the top or the game might crash when it loads
the bank.
5) Use PSSG Editor to create a "New" file, highlight the top most node and
import your XML. Update the spoken line counts if you didn't
already do so. And then Export to PSSG
6) Remove the .pssg extension, then convert the PSSG to .pgz using 7-zip
with gzip archive compression (rename extension .pgz)
7) Place the updated .pgz into the datalocal.
** NOTES:
- If creating a new dialogue, you will need to create a
bk_voc_NAME_eng.sli file and copy the file:
sound_library_voices_eng.sli
From the directory:
/datalocal/sounds/banks/voices/voice_dialog/eng/
Additional updates may also be necessary (still investigating)
- If using EGO PSSG EDITOR, you may find that converting the XML
back to PSSG causes the game to crash on save game load. I'm not
sure what causes it, but there are two work arounds.
First work around is to NOT compress the XML back to PSSG. IE:
remove the .xml extention and compress the file into a .psg.
Surprisingly, the game still loads it. At least for banks. EGO's
schema is accurate enough that the game will accept the non-binary
XML.
Second option is to use pssgExtractor to extract and compile the
file. When I run into walls with EGO, I can normally get around
them using the script. For purists who want everything to be true
binary PSSG, this is the way to go.
E. How are the Phonemes mapped?
The lip movement information (phonemes) are pulled from the audio lines
.pgz file. (phoneme information and timing are embedded into the audio
wrapper). If you use EGO PSSG EDITOR to convert the PSSG wrapper to
XML, you will see a section at the top that provides the phoneme mappings:
Example:
<LIBRARY type="LIPSYNC_SENTENCE"> <LIPSYNC_SENTENCE langId="EN" baseText="I'm the one" bestPass="-1" id="AnimLipSyncSentence"> <LIPSYNC_PASS startTime="0" endTime="3.529999" soundRangeRatio="0.9822545" recognitionRatio="0.7361887" info="FULLTEXT|STRICT" recoGrammar="FULLTEXT" recoMethod="STRICT"> <LIPSYNC_WORD startTime="0.0" endTime="0.270000" recognition="0.9233977"> <LIPSYNC_PHONEME id="ay"/> <LIPSYNC_PHONEME id="m"/> </LIPSYNC_WORD> <LIPSYNC_WORD startTime="0.2700000" endTime="0.400000" recognition="0.5397803"> <LIPSYNC_PHONEME id="dh"/> <LIPSYNC_PHONEME id="ax"/> </LIPSYNC_WORD> <LIPSYNC_WORD startTime="0.400000" endTime="0.6200000" recognition="0.9162213"> <LIPSYNC_PHONEME id="w"/> <LIPSYNC_PHONEME id="ah"/> <LIPSYNC_PHONEME id="n"/> </LIPSYNC_WORD> </LIPSYNC_PASS> </LIPSYNC_SENTENCE> </LIBRARY>
F. Can I get a list of everything someone says?
Yes, though it takes a few steps and even these directions only tackle
one NPC at a time.
1) Goto the directory of an NPC who's lines you wish to extract
ie: .../datalocal/voice_dialog/pla_hero
2) Right click the background and select New -> Folder. Name
it "extracted" (If it doesn't already exist)
3) Select all the PGZ files (Click on first, then scroll to bottom
and SHIFT + CLICK last file).
4) Right click the selected files and select: 7-Zip -> Extract files...
5) Tell 7-zip to extract the files in your new "extracted" directory.
7-zip will create a sub-directory for each extarcted .pgz file.
6) Place a copy of pssgExtractor.py in the /extracted directory
7) Startup CYGWIN. In the terminal window, cd to the extracted
directory. The command will look something like:
$ cd /cygdrive/c/Steam/steamapps/common/datalocal/voice_dialog
$ cd <CHARACTER>/extracted
8) Use CYGWIN to exectute pssgOggExtract on all files. IE:
$ for f in **/* ; do python pssgExtractor.py $f; done
9) Wait... (it takes 20 to 30 minutes for the pla_hero dir)
Note: If you already extracted the ogg data, you will see errors
on the .ogg files, but you can ignore them.
10) Use grep to find the line from each XML file that describes the text,
then pipe the output into sed to format and redirect to file:
$ grep -Hir -e 'baseText=' . | sed -n -e 's/^\(\.\/\)\([^\/]*\)\(.*langId="EN" \)\(baseText="[^"]*"\)\(.*\)/\4 : \2/p' > lines.txt
Explanation:
grep : recurse through all subdirectories and print filename + line
if the string "baseText" is found.
| : redirect output to the next program listed
sed : Use regex to break the line up into groups and print the
4th and 2nd group (so you know which file to find the line in)
> : redirect the output to the file lines.txt
The above command will generate lines.txt which you can open up
and see all the spoken lines and what files to find them in. It
doesn't convey tone or pacing, but it is a good start when trying
to create new conversations as it allows you to search for key
terms.
-------------------------------------------------------------------------------
VI.2 > > > > Music
-------------------------------------------------------------------------------
A. Where is it?
Music files are located under:
.../datalocal/music/*
The PGZ Files are gzipped PSSG files. (See above).
B. Can I extract everything to listen to it?
Yes, though it takes a few steps and even these directions only tackle
one NPC at a time.
1) Goto the directory where the music is stored
ie: .../datalocal/music
2) Right click the background and select New -> Folder. Name
it "extracted"
3) Select all the PGZ files (Click on first, then scroll to bottom
and SHIFT + CLICK last file).
4) Right click the selected files and select: 7-Zip -> Extract files...
5) Tell 7-zip to extract the files in your new "extracted" directory.
7-zip will create a sub-directory for each extarcted .pgz file.
6) Place a copy of pssgOggExtract.py in the /extracted directory
7) Startup CYGWIN. In the terminal window, cd to the extracted
directory. The command will look something like:
$ cd /cygdrive/c/Steam/steamapps/common/datalocal/music/extracted
8) Use CYGWIN to exectute pssgOggExtract on all files. IE:
$ for f in **/* ; do python pssgOggExtract.py $f; done
C. How is music connected to the game?
There are three files involved in connecting music to the game, or
put a different way, letting the game know when to load them into
memory.
1) SOUND LIBRARY:
.../datalocal/sounds/sound_library.sli
Identifies all the Sound and Music BANK CONFIGURATION files to
load.
2) BANK CONFIG:
various SLI Bank config files are located under:
.../datalocal/sounds/banks/music/
At the top of these text based files, the name of a GZIPPED
PSSG file is identified with most of the bank data.
3) BANK FILES:
various PGZ files are located under:
.../datalocal/banks/
This directory contains all bank files including the ones
for vocal and amient noises.
While the system above allows files to have any name they want,
the game as a whole requires all files to have a unique name.
Therefore, the developers used some basic conventions when
creating things to ensure they didn't accidently use a name
that was already in use.
If the music PGZ file is named:
music_cut_mq01_intro_part2.pgz
Then the BANK for the file will be named:
bk_music_cut_mq01_intro_part2.sli
and
bk_music_cut_mq01_intro_part2.pgz
If you are updating existing content, you can generally ignore
the BANK SLI files. You only need to include them if you are
introducing new content. In which case you also need to include
sound_library.sli to ensure the games loads your new bank.
D. Cues
If you read the vocal section above, then you learned that vocal files
are triggered by proposition elements in SDG files. Music is triggered
by Cues.
A master music configuration file is found at:
.../datalocal/sounds/music_library.sli
This defines a number of additional sli files to load when the player
enters specific levels:
<include>music_library_lvl_00_travel.sli</include>
<include>music_library_lvl_01_continent_city.sli</include>
<include>music_library_lvl_02_south_city.sli</include>
<include>music_library_lvl_03_dungeon_crypt.sli</include>
...
If you open up the music configuration file for a level you will
find the declaration of events and the music to play when those
events occur:
.../datalocal/sounds/music_library/music_library_lvl_02_south_city.sli
<level name="lvl_02_south_city.sle">
<levelStartCues> <cue> <conditionSet> <conditions> <insideTriggerTargeted on="MAIN" triggerName="TriggerStatic_music_palace_02"/> </conditions> </conditionSet> <delay randomDelayMin="3" randomDelayMax="3" stopCurrentMusic="true"/> <transition mode="FADE" fadeOutTime="4"/> <musicActions> <set track="2"> <randomize> <music bank="bk_music_explo_palace_02_01_loop.pgz" sound="music_explo_palace_02_01_loop" weight="1"/> </randomize> </set> <play track="2"/> </musicActions> </cue> <include>music_library_default_cues_level_start.sli</include> </levelStartCues>
Above, the game is configured to play the music_explo_palace_02_01_
loop from the bk_music_explo_palace_02_01_loop BANK when the player
enters the trigger TriggerStatic_music_palace_02
There are <genericCueSets> wich map the various tracks to play when
the player enters various trigger zones.
There are <cutsceneCueSets> which indicate what music to play when
specific cutscenes are playing.
There are <dialogCueSets> which indicate music to play when certain
dialogue IDs are encountered.
There are <squadCueSets> which indicate what fight music to play
based on the squad that an enemy belongs to.
Finally, music can also be triggered from SQG and SDG files using
the commandAction:
Note that the LaunchSoundCue can't just play any music it wants
at any time. A Cue must be defined and configured for the loaded
area.
E. What are these Markers in the Music Wrappers?
The markers section is a sequence of UINT32 numbers. (every 4 HEX =
1 UINT32). Each consecutive value is larger than the previous. These
mark the sample offset at each half second. The number of markers
thus equals the length of the song in seconds times 2 and the distance.
The values could technically be different based on the audio
characteristics. However, all the MUSIC audio I have seen is
recorded at 44100 Hz. This means there are 44100 samples per
second. So a half second means there will be a 22050 sample
difference per marker.
The good news is, since all the audio is 44100 hz, the markers
sections all pretty much have the same values. The only real
difference is the number of markers and the last value which
equates to the total samples.
So if you need to create a markers section, just find a music
file that is bigger than yours. Then steal its markers and
shave off the values you don't need. (and update the last marker
to the total sample count). To get the total sample count,
I open the file in Audacity and change the view at the bottom
to samples. Then select all with CTRL + A.
As far as I can tell, the purpose of the markers is primarily
for Labels. If you open up a music file that ends with Loop
for example, you will see some labels after the markers. They
have a name like "LoopStart" and "LoopEnd" and a small value
which is the marker offset. This means you can have an audio
file that starts off with an intro but then goes into a loop.
If you were in audacity and wanted to create a Label for
a track name, you would just change the length view to sample
and divide the samples at a particular location by 22050 to
get the closest marker.
Possibly the best news is that markers are optional. You only
need them if you have looping audio or want to be able to
enter the audio at a specific point. (For example, there are
common label names for fight music to indicate the outro,
etc..) But you can always point a cue at track 1 of an audio
file and it will just play if there are no markers. (I have
had the game crash when using track 0, so I avoid track 0).
Finally... the markers are hints, and they are basically unused
unless you have a Label. Things to avoid:
- Don't let the last marker value be larger than the number of
samples in the audio.
- Don't make a Label that points to a marker index that doesn't
exist. Avoid those two things and you should be fine.
F. Subtitles:
Even though the pssg files that wrap vocal audio have the text line in
them, subtitles are controlled by separate files located under:
datalocal/text/dlg_.....txt (name of file matches the .sdg file).
I suspect that is because the audio probably wasn't recorded for every
supported language. So you can't assume the audio text will always be
there.
These text files are not ASCII based, but USC-2 LE BOM, which means you
need a compatible reader like Notepad++. It also means that grep wont find
text within.
Unfortunately, I have not yet been able to get the game to recognize
changes to these files. So for the time being, there is no way to
update the subtitles. If you re-use existing lines in whole (just add
existing IDs to banks and use them in dialogue), it isn't an issue as
the ID will still cause the subtitle to appear. But if you create new
lines or edit existing lines, there is no way to update the subtitle
text at the moment. Hopefully I will figure this out with some more
investigation.
-------------------------------------------------------------------------------
VI.3 > > > > Sounds/FX
-------------------------------------------------------------------------------
<< TODO >>
-------------------------------------------------------------------------------
VI.4 > > > > Adding/Replacing Audio
-------------------------------------------------------------------------------
A. Music
Before we start, know that the EGO PSSG Editor sometimes corrupts the
headers of files. I believe it occurs when it runs into unknown/new
attributes. In that case, it sometimes just silently ignores the value.
But often times those values are important. In the case of music files,
it can cause the audio to not play.
An easy test is to use it to convert a PSSG file to XML and then convert
it back with no changes. If the output is a different size than the
original, then EGO PSSG Editor is missing data.
I advise users to use pssgExtractor.py, at least with music files.
If you want to use EGO PSSG EDITOR, then make sure to do a test with
each file you use it on to make sure there are no missed schema elements
--------
Replace:
--------
We will start with a replacement example as that is much easier.
1) copy the following file from datalocal/ (where ever you have extracted
all the resources) to GreedFall/datalocal/ (game mods dir)
datalocal/music/music_cut_mq01_intro_part2.pgz
2) Extract the PGZ file with 7-zip to datalocal and then delete it.
3) Extract the PSSG file with pssgExtractor.py:
- Copy pssgExtractor.py to the GreedFall/datalocal using File Explorer
- Dbl-Click the Cygwin64 Terminal icon. Then in the terminal window
change directory to the games datalocal dir:
$ cd /cygdrive/c/Steam/steamapps/common/GreedFall/datalocal
- Convert the pssg file to xml using pssgExtractor.py
(IMPORTANT: Don't use EGO PSSG Editor for this)
$ python pssgExtractor.py music_cut_mq01_intro_part2
4) Use Notepad++ to edit music_cut_mq01_intro_part2.xml
- Remove the Markers.
If you haven't read it, check out the section above:
"What are these Markers in the Music Wrappers" above.
Even if you want Markers and Labels, you can come back and add them
later. For now, just remove them.
Change:
<LIBRARY type="AUDIODATA"> <OGGDATA binaryObjRef="#music_cut_mq01_intro_part2.ogg" markerCount="173" id="music_cut_mq01_intro_part2_audioData"/> <MARKERS> 00 00 00 00 00 00 56 22 00 .. .. .. ... ... </MARKERS> </OGGDATA> </LIBRARY>
To:
<LIBRARY type="AUDIODATA"> <OGGDATA binaryObjRef="#music_cut_mq01_intro_part2.ogg" markerCount="0" id="music_cut_mq01_intro_part2_audioData"/> </LIBRARY>
- Remove the Binary (Hex) data:
Go to the bottom and remove all the HEX between the <BINARYDATA>
tags. IE: Place cursor before first HEX digit, scroll to the bottom
and SHIFT+CLICK after the last HEX digit. Then hit Delete
The bottom should look like:
<BINARYDATA> </BINARYDATA> </BINARYOBJECT> </LIBRARY> </PSSGDATABASE> </PSSGFILE>
- Save the file.
5) Create your ogg file in the datalocal directory.
I'm not sure what compression limits/file sizes the engine can handle,
but it is probably best to use 44100 Hz sample rate as that is what all
the existing music uses. In my example, I used audacity to export the
file to OGG and in the options during the save process, I selected
quality level 6 and it worked fine.
6) Copy Ogg data into music_cut_mq01_intro_part2.xml:
Open the ogg file with HxD. Hit CTRL + A to select all
then select: File -> Export -> 'c'
Save the file to GreedFall/datalocal/music_cut_mq01_intro_part2.c
Close HxD and then open the new file in NotePad++. Within, you will see
something like:
unsigned char rawData[9999999] = {
0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, ...,
0x00, 0x00, 0x2E, 0x7B, 0x00, 0x00, 0x00, ...,
0x0A, 0x64, 0x01, 0x1E, 0x01, 0x76, 0x6F, ...,
...
};
Note the size of the rawData array above. (9999999). In
music_cut_mq01_intro_part2.xml, find the tag <BINARYOBJECT> and update the
binaryDataSize to match the size of the rawData array. In the case of
our example, it would look something like:
<BINARYOBJECT binaryDataSize="9999999" id="music_cut_mq01_intro_part2.ogg">
Finally, copy all the c-style hex to the clipboard and paste it into
music_cut_mq01_intro_part2.xml. (Hi-light all the data and hit CTRL+C to
copy to clipboard).
The BINARYDATA element of music_cut_mq01_intro_part2.xml should look
something like:
** If you are using pssgExtractor, you can use the data as-is. If
you wish to use EGO PSSD Editor, you will need to use the
replace tools to remove all instances of '0x' and ',' in the
.c file before you copy/paste.
** The reason we do this and don't simply copy/paste directly from
HxD into NotePad++ is because notepad++ has a line size limit
(around 6 million characters). So if you blindly paste from HxD
to NotePad++, sometimes the data gets truncated off the end. To
be safe, always export to a format that includes line feeds.
7) Use pssgExtractor to convert the XML back to .pssg:
$ python pssgExtractor.py -toPSSG music_cut_mq01_intro_part2.xml
8) In File Explorer, remove the .pssg extension from the new file. Then
right click on "music_cut_mq01_intro_part2" and select:
7-Zip -> Add to Archive.
Make sure Archive Format is set to "gzip". Update the file extension from
".gz" to ".pgz". Then hit OK. The following file should appear in the dir:
music_cut_mq01_intro_part2.pgz
9) Clean up:
And that's it. The game will now use your audio when that music plays.
(See the next example for setting up a cue if you wish to test it).
You can now remove :
GreedFall/datalocal/music_cut_mq01_intro_part2
GreedFall/datalocal/music_cut_mq01_intro_part2.xml
GreedFall/datalocal/pssgExtractor.py
----
ADD:
----
Adding content is a bit more involved as you have to create some registration
files so that the game will load your content.
Here I walk through an example. Modify the details as neeeded. (replace
<YOURMOD> with something that makes sense)
1) copy the following files from datalocal/ (where ever you have extracted
all the resources) to GreedFall/datalocal/ (game mods dir)
datalocal/music/music_cut_mq01_intro_part2.pgz
datalocal/banks/bk_music_cut_mq01_intro_part2.pgz
datalocal/sounds/banks/music/bk_music_cut_mq01_intro_part2.sli
datalocal/sounds/music_library/music_library_lvl_02_south_city.sli
datalocal/sounds/sound_library.sli
2) Extract the 2 PGZ files with 7-zip to datalocal and then delete them
3) Rename the files to match your mod:
music_cut_mq01_intro_part2 => music_<YOURMOD>
bk_music_cut_mq01_intro_part2 => bk_music_<YOURMOD>
bk_music_cut_mq01_intro_part2.sli => bk_music_<YOURMOD>.sli
4) Update the SLI files:
- Scan bk_music_<YOURMOD>.sli, and anywhere it says
"music_cut_mq01_intro_part2" -> change it to "music_<YOURMOD>"
It should look something like:
<?xml version="1.0" encoding="utf-8"?> <bank name="bk_music_<YOURMOD>.pgz"> <sounds> <sound name = "music_<YOURMOD>" file = "music_<YOURMOD>.pgz" inheritFrom = "preset_music_cut" isLoop = "false" /> </sounds> </bank>
** Note: if it is a looping audiofile, set isLoop to "true"
- At the bottom of sound_library.sli add a link to include your
mods SLI Bank. IE, it should look something like:
... <include>sound_library_voices_eng.sli</include> <include>bk_music_<YOURMOD>.sli</include>
- Add a test que to music_library_lvl_02_south_city.sli.
Search for "<dialogCueSets>", and add the following:
<dialogCueSets> <dialogCueSet locKey="dlg_siora_friendship_23"> <sentenceStartCues> <cue><conditionSet/> <transition mode="FADE" fadeOutTime="3"/> <musicActions> <set track="1"> <randomize> <music bank="bk_music_<YOURMOD>.pgz" sound="music_<YOURMOD>" weight="1"/> </randomize> </set> <play track="1"/> </musicActions> </cue> </sentenceStartCues> </dialogCueSet>
The above que, will play your new music if you talk to Siora and ask
her about her Father.
5) Extract the PSSG files with pssgExtractor.py:
- Copy pssgExtractor.py to the GreedFall/datalocal using File Explorer
- Dbl-Click the Cygwin64 Terminal icon. Then in the terminal window
change directory to the games datalocal dir:
$ cd /cygdrive/c/Steam/steamapps/common/GreedFall/datalocal
- Convert the pssg files to xml using pssgExtractor.py
(IMPORTANT: Don't use EGO PSSG Editor for this)
$ python pssgExtractor.py music_<YOURMOD>
$ python pssgExtractor.py bk_music_<YOURMOD>
6) Use Notepad++ to edit bk_music_<YOURMOD>.xml.
- Replace "music_cut_mq01_intro_part2" with "music_<YOURMOD>"
(Hit CTRL+H to bring up the replace tool)
- Save the file
7) Use pssgExtractor to convert bk_music_<YOURMOD>.xml to PSSG:
$ python pssgExtractor.py -toPSSG bk_music_<YOURMOD>.xml
8) In File Explorer, remove the .pssg extension from bk_music_<YOURMOD>.pssg
Right click the file and select 7-Zip -> Add to Archive. Make sure
Archive Format is set to "gzip". Update the file extension from ".gz" to
".pgz". Then hit OK. The following file should appear in the dir:
bk_music_<YOURMOD>.pgz
9) Use Notepad++ to edit music_<YOURMOD>.xml
- Replace "music_cut_mq01_intro_part2" with "" music_<YOURMOD>"
Hit CTRL + H to bring up the replace tool if you want to
save time.
- Remove the Markers.
If you haven't read it, check out the section above:
"What are these Markers in the Music Wrappers" above.
Even if you want Markers and Labels, you can come back and add them
later. For now, just remove them.
Change:
<LIBRARY type="AUDIODATA"> <OGGDATA binaryObjRef="#music_<YOURMOD>.ogg" markerCount="173" id="music_<YOURMOD>_audioData"/> <MARKERS> 00 00 00 00 00 00 56 22 00 .. .. .. ... ... </MARKERS> </OGGDATA> </LIBRARY>
To:
<LIBRARY type="AUDIODATA"> <OGGDATA binaryObjRef="#music_<YOURMOD>.ogg" markerCount="0" id="music_<YOURMOD>_audioData"/> </LIBRARY>
- Remove the Binary (Hex) data:
Go to the bottom and remove all the HEX between the <BINARYDATA>
tags. IE: Place cursor before first HEX digit, scroll to the bottom
and SHIFT+CLICK after the last HEX digit. Then hit Delete
The bottom should look like:
<BINARYDATA> </BINARYDATA> </BINARYOBJECT> </LIBRARY> </PSSGDATABASE> </PSSGFILE>
- Save the file.
10) Create your ogg files in the datalocal directory.
I'm not sure what compression limits/file sizes the engine can handle,
but it is probably best to use 44100 Hz sample rate as that is what all
the existing audio uses. In my example, I used audacity to export the
file to OGG and in the options during the save process, I selected
quality level 6 and it worked fine.
11) Copy Ogg data into music_<YOURMOD>.xml:
Open the ogg file with HxD. Hit CTRL + A to select all
then select: File -> Export -> 'c'
Save the file to GreedFall/datalocal/music_<YOURMOD>.c
Close HxD and then open the new file in NotePad++. Within, you will see
something like:
unsigned char rawData[9999999] = {
0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, ...,
0x00, 0x00, 0x2E, 0x7B, 0x00, 0x00, 0x00, ...,
0x0A, 0x64, 0x01, 0x1E, 0x01, 0x76, 0x6F, ...,
...
};
Note the size of the rawData array above. (9999999). In
music_<YOURMOD>.xml, find the tag <BINARYOBJECT> and update the
binaryDataSize to match the size of the rawData array. In the case of
our example, it would look something like:
<BINARYOBJECT binaryDataSize="9999999" id="music_<YOURMOD>.ogg">
Finally, copy all the c-style hex to the clipboard and paste it into
music_<YOURMOD>.xml. (Hi-light all the data and hit CTRL+C to copy
to clipboard).
The BINARYDATA element of music_<YOURMOD>.xml should look something
like:
** If you are using pssgExtractor, you can use the data as-is. If
you wish to use EGO PSSD Editor, you will need to use the
replace tools to remove all instances of '0x' and ',' in the
.c file before you copy/paste.
** The reason we do this and don't simply copy/paste directly from
HxD into NotePad++ is because notepad++ has a line size limit
(around 6 million characters). So if you blindly paste from HxD
to NotePad++, sometimes the data gets truncated off the end. To
be safe, always export to a format that includes line feeds.
12) Use pssgExtractor to convert the XML back to .pssg:
$ python pssgExtractor.py -toPSSG music_<YOURMOD>.xml
13) In File Explorer, remove the .pssg extension from music_<YOURMOD>.pssg
Right click the file "music_<YOURMOD>" and select:
7-Zip -> Add to Archive.
Make sure Archive Format is set to "gzip". Update the file extension
from ".gz" to ".pgz". Then hit OK. The following file should appear
in the dir:
music_<YOURMOD>.pgz
14) Test: Start up the game, head to New Serene and talk to Siora.
Ask her about her father and confirm your music starts to
play.
15) Clean up:
You can remove :
GreedFall/datalocal/bk_music_<YOURMOD>
GreedFall/datalocal/bk_music_<YOURMOD>.pschema
GreedFall/datalocal/bk_music_<YOURMOD>.xml
GreedFall/datalocal/music_<YOURMOD>
GreedFall/datalocal/music_<YOURMOD>.c
GreedFall/datalocal/music_<YOURMOD>.pschema
GreedFall/datalocal/music_<YOURMOD>.xml
GreedFall/datalocal/pssgExtractor.py
**GreedFall/datalocal/music_library_lvl_02_south_city.sli
** You will need to configure cues to determine when to play
your new music. music_library_lvl_02_south_city.sli was just
a testing example. Look at datalocal/sounds/music_library.sli
to follow the cue registration tree and decide when/how
to cue your music.
** If you are looking to simply replace the games existing
music, you would be better off clobbering the PGZ files
under datalocal/music with your own updated versions.
B. Spoken Lines
<< TODO >>
C. Sounds/FX
<< TODO >>
===============================================================================
VII. > > > > Items
===============================================================================
-------------------------------------------------------------------------------
VII.1 > > > > General
-------------------------------------------------------------------------------
< < < < < TODO / In PROGRESS> > > > >
-------------------------------------------------------------------------------
VII.2 > > > > Armor
-------------------------------------------------------------------------------
< < < < < TODO / In PROGRESS> > > > >
-------------------------------------------------------------------------------
VII.3 > > > > Weapons
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
VII.4 > > > > Potions and Grenades
-------------------------------------------------------------------------------
< < < < < TODO / In PROGRESS> > > > >
-------------------------------------------------------------------------------
VII.5 > > > > Loot and Rewards
-------------------------------------------------------------------------------
< < < < < TODO / In PROGRESS> > > > >
===============================================================================
VIII. > > > > Merchants
===============================================================================
-------------------------------------------------------------------------------
VIII.1 > > > > Defining Inventories
-------------------------------------------------------------------------------
< < < < < TODO / In PROGRESS> > > > >
-------------------------------------------------------------------------------
VIII.2 > > > > Linking NPCs to inventories
-------------------------------------------------------------------------------
< < < < < TODO / In PROGRESS> > > > >
===============================================================================
IX. > > > > Spells
===============================================================================
-------------------------------------------------------------------------------
IX.1 > > > > ???
-------------------------------------------------------------------------------
< < < < < TODO / In PROGRESS> > > > >
===============================================================================
X. > > > > Models and Textures
===============================================================================
-------------------------------------------------------------------------------
X.1 Textures
-------------------------------------------------------------------------------
Some pssg files wrap texture data. Here is an example PSSG wrapper:
Let me say upfront... disclaimer: I don't actually KNOW anything. The
following is an educated guess:
The hex appears to be a raw BC1 compressed data stream. The other files
mentioned (nameHigh and nameUltra) are likely also image data streams that
feed the same dds file.
Notice the "texelFormat" attribute. BC1 is a compression format used by
DirectX graphics cards. For an excellent article on the BC1-BC7 compression
formats, see :
http://www.reedbeta.com/blog/understanding-bcn-texture-compression-formats/
Normally, DirectX games use DirectDrawSurface (dds) files and the DDS file
internally has BCn compressed data. Here we don't actually have the dds file.
We only have the BC1 image data stream (that is fed to the graphics card). I
suspect this is because much of what the DDS header normally provides such
as width, height, compression format, etc... is already present in the PSSG
wrapper. So instead of repeating the information with a full DDS payload,
GreedFall devs (might have) decided to only store the BCn image data stream.
Bottom line: If you try to save the HEX data to a file and rename it .dds,
it won't work because it isn't DDS format. I suspect someone would need to
create DDS headers and inject the streams to create something that could be
edited using normal DDS editing tools. And a tool that could also go the
other way, as the updated texture would need to eventially be re-injected.
The link above includes a link to NVIDIA's texture tools and library,
however I looked at them. While they are valuable and include the
DirectDrawSurface.h and .cpp files, as is, they are not designed to do what
needs to be done. They are designed to compress common image formats like tga,
png into dds files and back. (They would have to be heavily modified to
accept a sequence of already compressed steams to feed into a new DDS file.
There is also https://github.com/hglm/detex It is C based and it was written
for linux. I installed CYGWIN's gcc, make, xinit, xlaunch, libgtk3-devel and
xorg-c11-fonts-*. Then compiled the program following its readme so that it
would run under cygwin. To run it, I had to start a multi-screen X Server
using the cygwin terminal command "xlaunch"
The advantage of detex is that you don't need to install or learn Visual
Studio to update the code. It is a (relatively) small program written in C
that you can compile using CYGWIN. So if I was going to tackle creating a
solution to this problem, I would probably start with detex.
Detex requires that files have a 64 Byte "ktx" header, followed by a 4 Byte
"Here is the size of the file". Problem is, a 64x64 BC1 image should be 2048
Bytes large. But the BC1 Stream size in most the PSSG files I saw was 680
bytes greater than that.
After a day of investigation, I gave up. I may revisit this in the future if
I find more time, but I don't currently have any ambitions to create/update
textures. So it is hard to find motivation to pursue this. But again, these
are tips so that someone else who cares might get started in the right
direction.
-------------------------------------------------------------------------------
X.2 Models
-------------------------------------------------------------------------------
If one uses EGO PSSG Editor on the model files and extracts them to XML, it
is pretty obvious the data within is meant for 3DS-MAX. I don't own 3DS-MAX,
so that is a problem.
I am aware that BLENDER supports python plugins. I also know years ago,
there was a python plugin that took the vertice data from MDL models
(Source Engine) and put them into a PROXY model that users could "nudge"
edit in Blender. (No new verticies could be introduced because they were
connected to the skeleton/animation system and no one knew how that worked.
So you could only do minor vertice nudging... think clothing seems. But
still... enough to make some new clothes with the help of BUMPMAP textures).
Then a separate python script would inject the edits into the original data.
I'm not sure if something like this might be possible with GreedFall. Or
if someone who knows 3DS-MAX might look at the XML output and go. "Oh...
I know where that goes!".
===============================================================================
XI. > > > > Miscellaneous
===============================================================================
An area for things that don't fit anywhere else right now....
-------------------------------------------------------------------------------
XI.1 > > > > Cutscenes
-------------------------------------------------------------------------------
Cutscenes use a custom shallow format that makes use of silks new PSSG tags.
If you use PSSGextract.py to XMLify one of the cutscenes, you will notice as
you follow the file, it is almost a scripting language built into the
attribute values. Most notable is the fact that some strings have a name and
value and some do not. I suspect Strings without a Value represent a marker
for a structure or instruction set.
When I edited cutscenes, all I did was swap out the names of the NPCs who
were playing certain parts. My edits were simple actor name changes. Someone
who is more interested in cutscenes could probably study some of these
cutscenes and learn the "language" of the cutscene. There are obviously
commands for things like hiding NPCs, removing or hiding items/weapons,
Changing cameras, invoking animation sequences, etc...
When adding a NEW cutscene to the game, you have to “register” it by editing
the library:
library_cutscene_loading.sli
This indicates what cutscenes to load into memory when the player enters
certain levels.
-------------------------------------------------------------------------------
XI.2 > > > > Levels and Maps
-------------------------------------------------------------------------------
The level files can be found under:
datalocal/level/
For size reasons, most of the files are either gzip compressed or PSSG files
even though they have extensions like:
.slr <- 7zip compressed pssg file that lists level resources to load/include
.sla <- pssg file that identifies objects/actors/triggerzone locations.
There are a number of .sla files. Not sure at this time what the numbering
means (if anything). What is clear is that the level content is spread
out, often times unevenly across the numbers. Sometimes I will see an
element defined in one file and then referenced from another, so it is
definitely confusing. Hopefully in time I will learn more.
If you extract these cells with pssgExtract and include the indent option,
it makes the file content a bit more clear. Some cells have Actor Library
blocks that define actors to spawn when the player enters trigger zones.
Example: lvl_00_travel-122.sla: (Travel Camp)
Each file is different. The one above has 2 sections. A section for "nodes".
which appears to define placement information. The id/name from nodes seems
to match up with the second section which defines Actors.
When I think of Actors, I think of people, but here Actor seems to include
anything that can be spawned. The Actors have a GAMEACTOR Type value which
dictates the remaining attributes that are found.
The companions/NPCs seem to use type "SpawnCompanion" while non-companions
use type "SpawnCharacter". And then there is a TriggerStaticSpawn that points
to the other SpawnCompanion nodes. Examples:
<LIBRARY type="NODE"> <LAYERROOT id="layerRoot_layer122"> ... <NODE id="SpawnCompanion$N5122BPKE_rootNode$N5122BPKE"> <USERDATA object="#SpawnCompanion_Travel_Camp_Merchant"/> <TRANSFORM> 3E FD 3F 6D BF 5E 7D C5 80 00 00 00 00 00 00 00 ... 00 00 00 00 80 00 00 00 3F 7F FF FE 00 00 00 00 ... </TRANSFORM> <BOUNDINGBOX> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... </BOUNDINGBOX> </NODE> </LAYERROOT> </LIBRARY> <LIBRARY type="ACTOR"> <ACTOR RootNode="#SpawnCompanion$N5122BPKE_rootNode$N5122BPKE" id="SpawnCompanion_Travel_Camp_Merchant"> <GAMEACTOR Type="SpawnCompanion"> <UnsignedInt Name="SpawnCompanionClassVersion">4</UnsignedInt> <String Name="NPCId" Value="hum_npc_merchant_trader_travel"/> <String Name="startStance" Value="NORMAL"/> <UnsignedInt Name="registeredByTriggers">0</UnsignedInt> <UnsignedInt Name="SpawnClassVersion">10</UnsignedInt> <UnsignedInt Name="checkDistance">1</UnsignedInt> <UnsignedInt Name="useRadius">0</UnsignedInt> <UnsignedInt Name="alwaysSpawned">1</UnsignedInt> <UnsignedInt Name="spawnedCH1">1</UnsignedInt> <UnsignedInt Name="spawnedCH2">1</UnsignedInt> <UnsignedInt Name="spawnedCH3">1</UnsignedInt> <UnsignedInt Name="WaypointClassVersion">7</UnsignedInt> <Color Name="color">220.000000 220.000000 0.000000</Color> <UnsignedInt Name="GameActorClassVersion">3</UnsignedInt> </GAMEACTOR> </ACTOR> <ACTOR RootNode="#PolygonShape$LM45OKAD" id="TriggerStaticSpawn_Travel_Camp_NPC"> <GAMEACTOR Type="TriggerStaticSpawn"> <UnsignedInt Name="TriggerStaticSpawnClassVersion">4</UnsignedInt> <UnsignedInt Name="TriggerStaticClassVersion">4</UnsignedInt> <UnsignedInt Name="ConditionListClassVersion">4</UnsignedInt> <Int Name="LibConditions">0</Int> <UnsignedInt Name="validationType">0</UnsignedInt> <UnsignedInt Name="ActionListClassVersion">5</UnsignedInt> <Int Name="LibActions">0</Int> <UnsignedInt Name="ActionListClassVersion">5</UnsignedInt> <Int Name="LibActions">0</Int> <UnsignedInt Name="ActionListClassVersion">5</UnsignedInt> <Int Name="LibActions">0</Int> <UnsignedInt Name="StaticTriggerClassVersion">2</UnsignedInt> <UnsignedInt Name="GameActorClassVersion">3</UnsignedInt> <UnsignedInt Name="spawnType">1</UnsignedInt> <UnsignedInt Name="spawnActorsCount">7</UnsignedInt> <String Name="spawnActor0" Value="Squad_Travel_Camp_NPC"/> <String Name="spawnActor1" Value="SpawnCompanion_Siora"/> <String Name="spawnActor2" Value="SpawnCompanion_Aphra"/> <String Name="spawnActor3" Value="SpawnCompanion_Petrus"/> <String Name="spawnActor4" Value="SpawnCompanion_Vasco"/> <String Name="spawnActor5" Value="SpawnCompanion_Kurt"/> <String Name="spawnActor6" Value="SpawnCompanion_Travel_Camp_Merchant"/> </GAMEACTOR> </ACTOR>
So far I have been able to move NPCs and objects around. The Transform
data is a series of floats/uints and I don't quite yet know what they all
mean. I suspect from working on other games, the transform likely
positions the object relative to the parent using X,Y,Z and various
angling indicators (facing, lean forward, lean side).
The X and Y portion of the object position is at (Float) TRANSFORM[12] and
(Float) TRANSFORM[13] respectively. (zero index based, all values are
Big-Endian).
As of right now I haven't been able to add new objects, but time will tell.
===============================================================================
XII. > > > > Legalities
===============================================================================
GreedFall offers no provisional rights to mod developers. This has different
implications in different regions, however the bottom line is this:
YOU CAN'T MAKE ANY MONEY FROM YOUR MOD (ie : you can't sell it)
This should not come as a surprise since free work is generally convention
within the modding communities.
Also, DO NOT distribute GreedFall.exe with your mod. Doing so is blatantly
illegal and would be construed as distributing a pirated or "cracked" version
of the game.
===============================================================================
< < < < < Contributing Authors > > > > >
===============================================================================
Dheu