Modding - common distribution pitfalls

Where modders get to tear my game apart when I *finally* release the tools

Modding - common distribution pitfalls

PostPosted by Railboy » Wed Apr 23, 2014 4:34 pm

I'm not an experienced modder. I'm accumulating quite a bit of experience in making games, but the last game I modded was the original Half Life. And modding has come a long way since then.

So as we near beta release I'd like to start collecting opinions on packaging, distributing and installing mods. You've already posted a few, and they've been invaluable - I want more. What games (or third-party systems) do it well? What games make it easy to install multiple mods alongside one another and report on their compatibility? And what games absolutely fuck it up? Stuff like that. This is your chance to rant.
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1845
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding - common distribution pitfalls

PostPosted by postnjam » Wed Apr 23, 2014 5:34 pm

Frontiers Nexis?

What about Steam workshop since the game will be on Steam?
User avatar
postnjam
Dungeon Crawler
 
Posts: 126
Joined: Thu Jul 18, 2013 8:36 pm
Location: Sawbridgeworth , UK

Re: Modding - common distribution pitfalls

PostPosted by Railboy » Wed Apr 23, 2014 5:47 pm

In this thread let's forget about what FRONTIERS modding may eventually be for a bit - instead let's talk about games that have been out for a while and that people have actually worked with. My goal is to gather up some concrete feedback based on real experiences.
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1845
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding - common distribution pitfalls

PostPosted by Zolana » Wed Apr 23, 2014 6:55 pm

Kerbal Space Program does it superbly. Literally just a case of dropping the files in the program directory and it all works.

Also, modding Minecraft with CraftBukkit is delightfully straightforward too.
-Zolana

Generic ramblings about life, the universe, and everything: http://awjc.wordpress.com

Feel free to add me on Steam, but PM me here so I know who is adding me :)!
Zolana
Moderator
Moderator
 
Posts: 323
Joined: Thu Jul 18, 2013 11:48 am
Location: Woking, United Kingdom

Re: Modding - common distribution pitfalls

PostPosted by Gazz » Wed Apr 23, 2014 6:59 pm

Railboy wrote: let's talk about games that have been out for a while and that people have actually worked with. My goal is to gather up some concrete feedback based on real experiences.


Bard's Tale 1 (Interplay)

Learned hex-editing savegames, tracing files on the 5 1/4" floppy sector by sector.


Jagged Alliance 2 v1.13

"Only" rebalanced game values for personal taste but... extensively so. Few stones remained unturned.

Changing game stats is trivially easy because there is a luxurious editor and (pretty much) every important piece of data has been externalised into XML tables.
(used to be all "hardcoded" in the original release)


UFO:Extraterrestrials

The "game" is written nearly completely in LUA scripts.
As a result you can change the gameplay in damn near any way you wish.
For instance, there were energy shields that would burn out when drained. I didn't like that so I rewrote that script so that an empty shield would have a "cooldown" before it started recharging again.

I don't think I ever released any mods for that because things were frightfully messy. When you "directly" alter the game code, things get incompatible with all other mods really fast.

Spoiler:      SHOW
Example snipped from a random mod file:

GUIMRO_makeFieldVisible (radar, field)
{
object pos = field.getPosition();
if (pos.y >= radar.fogLayers.size()) return false;

string coords = vector2str(pos);
object fieldData = GUIMRO_fieldDataMap.get(coords);
if (fieldData == NULL)
{
// we need to create the data for this field
fieldData = createObject("CScriptObject");
fieldData.addProperty("fogImage", NULL);
fieldData.addProperty("wallImage", NULL);
fieldData.addProperty("wall", NULL);

fieldData.fogImage = GUIMRO_drawRemovedFog (radar, pos.x, pos.y, pos.z);
// See if we have a wall that is not destroyed
if ((field.getFlag() & 0x1) == 0) // only do the next stuff if the field is marked as "full"
{
list objects = field.getObjects();
if (objects != NULL)
{
for (int i = 0; i < #objects; ++i)
{
object o = objects;
if (o.hasProperty("isWall") && o.isWall && o.hp > 0)
{
// we found a wall
fieldData.wallImage = GUIMRO_drawWall (radar, pos.x, pos.y, pos.z, o);
fieldData.wall = o;
break;
}
}
}
}
GUIMRO_fieldDataMap.put (coords, fieldData);
}
else
{
// fieldData already exists which means we have already drawn it.
return false;
}
return true;
}

//--------------------------------------------------------------------------------------------
GUIMRO_radarOnClick()
{
object radar = getGlobalVariable("GUIMRO_radar");
if (!radar.opened)
{
handleLeftClick();
return;
}
object mouseCursor = getGlobalVariable("mouseCursor");
object camera = getGlobalVariable("camera");

float pX = mouseCursor.getPointerX();
float pY = mouseCursor.getPointerY();
float resX = mouseCursor.getResolutionX();
float resY = mouseCursor.getResolutionY();


// print(""+pX+"/"+py);

// convert mouse coord in virtual screen (1024x768)
pX *= 1024.0/resX;
pY *= 768.0/resY;

// print(""+pX+"/"+py+"/"+GUIMRO_VECTOR1_X+"/"+GUIMRO_VECTOR2_X+"/"+GUIMRO_VECTOR1_Z+"/"+GUIMRO_VECTOR2_Z+"/"+GUIMRO_ANKOR_X+"/"+GUIMRO_ANKOR_Z);

float tpz = (pY*GUIMRO_VECTOR1_X - pX*GUIMRO_VECTOR1_Z + GUIMRO_ANKOR_X*GUIMRO_VECTOR1_Z - GUIMRO_VECTOR1_X*GUIMRO_ANKOR_Z)/
(GUIMRO_VECTOR2_Z*GUIMRO_VECTOR1_X-GUIMRO_VECTOR2_X*GUIMRO_VECTOR1_Z);
float tpx = (pX - tpz*GUIMRO_VECTOR2_X - GUIMRO_ANKOR_X)/GUIMRO_VECTOR1_X;

// print(""+tpx+"/"+tpz);
if (!checkLevelBounds(tpx, camera.y, tpz)) {
handleLeftClick();
return;
}
playAmbientSound (radar.guimro_buttonSound, 1.0);

moveCameraX( round(tpx-camera.x)-2 );
moveCameraZ( round(tpz-camera.z)-2 );
camera.update();
}
//--------------------------------------------------------------------------------------------


Morrowind

Very very powerful editor(s) for everything.
Scripting is frightfully difficult, though. Writing your first Hello World script and making it work ingame is a herculean task.


X3:Reunion / TC / AP

(If you've had any contact with X3 S&M: yes, I'm that Gazz)

This game has 3 levels of "code".
1. engine. Only the bravest of coders dare touch an engine that's towards being 2 decades old.
2. K...something script. I forgot. This is the API that the ES coders use to access engine functions. Looks kinda like C somethingorother.
(I only had semi-access to that once when I worked on the OOS combat code/rebalance for AP)
3. AI scripts.
That is what X modders mean when they talk about "scripting".

Spoiler:      SHOW
Just a snippet from an X3:TC script that I wrote.
You'll notice that in some ways the syntax is pretty clunky. That's because you have to figure out how to make do with a lot of prefab function calls and modify their return to something useful.
You'll get there eventually, though...

691 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
692 Goblin.Deploy.Check:
693 $Goblin.has.launched = null
694 if [OWNER] != Player
695 |skip if $MARS.Config[86]
696 ||endsub
697 end
698
699 do if $Time < $Next.Goblin.Launch
700 |endsub
701 $Next.Goblin.Launch = $Time + $Goblin.Launch.Frequency
702 gosub Goblin.AI.resupply
703
704 if not [THIS] -> is in active sector
705 * Repair allowed?
706 |skip if $MARS.Config[106]
707 ||endsub
708 |$Dummy1 = [THIS] -> get hull percent
709 * Repair required?
710 |do if $Dummy1 == 100
711 ||endsub
712 end
713
714 $Dummy1 = null
715 if [OWNER] == Kha'ak
716 |$Ware.Fighter.Drone = $Khaak.Drone.Ware
717 |$Dummy1 = [THIS] -> get amount of ware $Ware.Fighter.Drone in cargo bay
718 else
719 |$Index1 = size of array $Ware.Fighter.Drone.Arr
720 |while $Index1
721 ||dec $Index1 =
722 ||$Ware.Fighter.Drone = $Ware.Fighter.Drone.Arr[$Index1]
723 ||$ShipType.Fighter.Drone = $ShipType.Fighter.Drone.Arr[$Index1]
724 ||if [THIS] -> get amount of ware $Ware.Fighter.Drone in cargo bay
725 |||$Dummy1 = [TRUE]
726 |||break
727 ||end
728 |end
729 end
730 * Got ANY drones?
731 skip if $Dummy1
732 |endsub
733
734 $Time = playing time
735 $Goblin.Swarm.Shield.Max = [THIS] -> get maximum shield strength
736 if [OWNER] == Player
737 |$Dummy1 = $MARS.Config[85]
738 |$Dummy2 = $MARS.Config[84]
739 * 84 decoy, 85 Missile defense
740 |
741 * 175 goblin swarm divisor
742 |$Dummy5 = $MARS.Config[175]
743 |
744 |if $Dummy5
745 ||$Goblin.Swarm.Shield.Max = $Goblin.Swarm.Shield.Max / $Dummy5
746 |else
747 ||$Goblin.Swarm.Shield.Max = 0
748 |end
749 |
750 |do if $Debug
751 ||write to log file #$PageID append=[TRUE] printf: fmt='Goblin deploy check. GLOBAL decoy msn: %s, MslDef: %s, LOCAL Turr: %s, GMD: %s', $Dummy2, $Dummy1, $Turrets.Online, $Goblin.Missile.Defense, null
752 |
753 |do if $Dummy1 == null AND $Dummy2 == null
754 ||endsub
755 |$Dummy1 = $MARS.Config[54]
756 |do if $Dummy1 > $Time
757 ||endsub
758 else if [OWNER] == Kha'ak
759 |$Goblin.Swarm.Shield.Max = $Goblin.Swarm.Shield.Max / 10
760 |do if $Goblin.Swarm.Shield.Max > 499000
761 ||$Goblin.Swarm.Shield.Max = 499000
762 else
763 |$Goblin.Swarm.Shield.Max = $Goblin.Swarm.Shield.Max / 40
764 end
765 skip if $Goblin.Swarm.Shield.Max
766 |$Goblin.Swarm.Shield.Max = -1
767
768 if $Goblin.Swarm.Target -> exists
769 |skip if $Goblin.Swarm.Target -> is in active sector
770 ||$Goblin.Swarm.Target = null
771 |skip if $Goblin.Swarm.Target -> is in a sector
772 ||$Goblin.Swarm.Target = null
773 |skip if find $Goblin.Swarm.Target in array: $Goblins.Target.Arr
774 ||$Goblin.Swarm.Target = null
775 else
776 |$Goblin.Swarm.Target = null
777 end
778
779 $Best.Target = null
780 $Best.Target.Threat = 0
781 $Best.Target.Swarm = null
782 $Goblins.Target.Nr.Possible = 0
783 $Target = null
784 gosub Count.Goblins.on.Target
785 $Goblins.Deployed = size of array $Goblins.Arr
786
787 if $Debug
788 |$Dummy1 = [THIS] -> get amount of ware $Ware.Fighter.Drone in cargo bay
789 |write to log file #$PageID append=[TRUE] printf: fmt=' Goblin status. %s on board, %s / %s deployed. Targets: %s', $Dummy1, $Goblins.Deployed, $Goblins.Deploy.Max, $Target.Arr, null
790 end
791
792 do if $Goblins.Deployed >= $Goblins.Deploy.Max * 2
793 |endsub
794
795 if $MARS.Mode
796 |$Goblin.max.Range = $Laser.Range.Short * 4
797 else
798 |$Goblin.max.Range = $Laser.Range.Short
799 end
800 do if $Goblin.max.Range > $My.Scanner.Range
801 |$Goblin.max.Range = $My.Scanner.Range
802 do if $Debug
803 |write to log file #$PageID append=[TRUE] printf: fmt=' MARS Mode: %s, Gob.max.range %s, Laser.range: %s, Ship.scanner.range: %s', $MARS.Mode, $Goblin.max.Range, $Laser.Range.Short, $My.Scanner.Range, null
804
805 * Only repair when OOS, never decoy.
806 $Index1 = size of array $Target.Arr
807 if $MARS.Config[106]
808 |skip if [THIS] -> is in active sector
809 ||$Index1 = 0
810 end
811
812 $Goblin.Repair.Mission = null
813 if not [OWNER] == Kha'ak
814 |if not $Index1
815 ||if not $Goblins.Deployed >= $Goblins.Deploy.Max
816 |||$Dummy1 = [THIS] -> get hull percent
817 |||do if $Dummy1 < 100
818 @ ||||$Goblin.Repair.Mission = [THIS] -> call script 'plugin.gz.mars.lib' : Mode='repair.ship' Turret ID=null Actor=[THIS] Target=[THIS]
819 ||end
820 |end
821 end
822
823 gosub Get.Performance.Wait
824 if $Goblin.Missile.Override == -1
825 |$Goblin.Missile.Override = [TRUE]
826 else
827 |$Goblin.Missile.Override = null
828 end
829 $Goblin.Missile.Override.Cap = [THIS] -> get current shield strength
830 $Goblin.Missile.Override.Cap = $Goblin.Missile.Override.Cap / 10
831
832
833 * Evaluate ship's target list for possible launches.
834 while $Index1
835 |dec $Index1 =
836 |if $Performance.Wait.After
837 ||do if $Index1 == ( $Index1 / $Performance.Wait.Temp ) * $Performance.Wait.Temp
838 @ |||= wait 1 ms
839 |end
840 |$Target = $Target.Arr[$Index1]
841 |skip if $Target -> exists
842 ||continue
843 |$Target.ID = $Target -> get ID code
844 |$Target.Target = $Target -> get attack target
845 |
846 |if $Debug
847 ||$Dummy3 = $Target.Target -> get ID code
848 ||write to log file #$PageID append=[TRUE] printf: fmt=' Goblin Loop ID %s, %s %s, Targeting: %s %s', $Index1, $Target.ID, $Target, $Dummy3, $Target.Target
849 |end
850 |
851 |if find $Target in array: $Goblins.Target.Arr
852 ||$Dummy1 = $Target -> get maximum shield strength
853 ||if $Debug
854 |||$Dummy2 = get index of $Target in array $Goblins.Target.Arr offset=-1 + 1
855 |||$Dummy2 = $Goblins.Arr[$Dummy2]
856 |||$Dummy3 = $Dummy2 -> get ID code
857 |||write to log file #$PageID append=[TRUE] printf: fmt=' Target already engaged by %s %s, T.Max.Shields: %s', $Dummy3, $Dummy2, $Dummy1, null, null
858 ||end
859 ||if $Dummy1 == 0 OR $Dummy1 > $Goblin.Swarm.Shield.Max OR $Goblins.Target.Nr.Possible > 0
860 * is engaged but cannot be swarmed anyway, 0 max shields on missiles
861 |||inc $Goblins.Target.Nr.Possible =
862 |||continue
863 ||end
864 |end
865 |
866 |$Target.MT = $Target -> get maintype
867 |$Target.Race = $Target -> get owner race
868 |$Target.Not.Distractable = $Target -> get attacker
869 |do if $Target == [PLAYERSHIP]
870 ||$Target.Not.Distractable = null
871 |$No.Ship.Target = [TRUE]
872 |
873 * Goblin.Control would quit immediately if the ship was chasing ANOTHER goblin.
874 |if $Target.Target -> exists
875 ||do if $Target.Target -> get local variable: name=$VarName.Is.Goblin
876 |||$Target.Not.Distractable = [TRUE]
877 |end
878 |
879 |if ( $Target.MT == 10 ) OR $Goblin.Missile.Override
880 ||inc $Goblins.Target.Nr.Possible =
881 ||goto label Skip.Anti.Ship
882 |else if not [THIS] -> is of class Big Ship
883 * Only Big ships launch gobs at other ships.
884 ||goto label Skip.Anti.Ship
885 |else if $Target -> get local variable: name=$VarName.Is.Goblin
886 ||goto label Skip.Anti.Ship
887 |else if not $MARS.Config[84]
888 ||goto label Skip.Anti.Ship
889 |else if $Goblins.Deployed >= $Goblins.Deploy.Max
890 * Higher cap for missiles
891 ||goto label Skip.Anti.Ship
892 |else if $Target -> is of class Fighter Drone
893 ||goto label Skip.Anti.Ship
894 |else if $Target -> is of class Station
895 ||goto label Skip.Anti.Ship
896 * Don't attack mines and LT
897 |else if $Target -> is of class Stationary Ship
898 ||goto label Skip.Anti.Ship
899 |else if $Target.Race == Kha'ak
900 ||goto label Skip.Anti.Ship
901 ||
902 |else if $Target.Not.Distractable
903 * Undistractable regular enemy ship.
904 * only THE swarm target or the ONLY Target can have additional gobs stacked on
905 ||do if $Target == $Goblin.Swarm.Target
906 |||$Best.Target.Swarm = $Target
907 ||skip if $Goblins.Target.Nr.Possible
908 |||$Best.Target.Swarm = $Target
909 ||inc $Goblins.Target.Nr.Possible =
910 ||
911 ||if $Debug
912 |||$Dummy1 = $Target -> get ID code
913 |||$Dummy2 = $Best.Target.Swarm -> get ID code
914 |||$Dummy3 = $Goblin.Swarm.Target -> get ID code
915 |||write to log file #$PageID append=[TRUE] printf: fmt=' Target NOT distractable: %s. Best.Sw.T: %s, Exist Sw.T: %s, Nr.of.Sh: %s', $Dummy1, $Dummy2, $Dummy3, $Goblins.Target.Nr.Possible, null
916 ||end
917 ||goto label Skip.Anti.Ship
918 ||
919 |else if $Target -> is of class Huge Ship
920 ||if $Target.Target == [THIS]
921 * launch allowed
922 ||else if $Target -> is of class TL
923 * launch allowed
924 ||else if not [THIS] -> is of class Huge Ship
925 * It may not be MY immediate problem but don't let the drones swarm, yet
926 |||inc $Goblins.Target.Nr.Possible =
927 |||goto label Skip.Anti.Ship
928 ||end
929 |end
930 |
931 |if [OWNER] == Player
932 ||skip if $MARS.Config[84]
933 |||goto label Skip.Anti.Ship
934 ||do if $Target -> get local variable: name=$VarName.Ignore
935 |||goto label Skip.Anti.Ship
936 |else
937 ||if $Target -> is of class Astronaut
938 |||skip if is marine: passenger/astronaut=$Target
939 ||||goto label Skip.Anti.Ship
940 ||end
941 |end
942 |
943 |if $Target -> is of class Little Ship
944 ||skip if $MARS.Config[30]
945 |||goto label Skip.Anti.Ship
946 |else if $Target -> is of class Big Ship
947 * can be either big or huge
948 ||if $Target -> is of class Huge Ship
949 |||skip if $MARS.Config[32]
950 ||||goto label Skip.Anti.Ship
951 ||else
952 |||skip if $MARS.Config[31]
953 ||||goto label Skip.Anti.Ship
954 ||end
955 |end
956 |
957 * Passed all ship exceptions:
958 |$No.Ship.Target = null
959 |inc $Goblins.Target.Nr.Possible =
960 |
961 |do if $Goblins.Target.Nr.Possible == 1
962 ||$Best.Target.Swarm = $Target
963 |
964 Skip.Anti.Ship:
965 |$No.Missile.Target = null
966 |if $Target.MT != 10
967 ||$No.Missile.Target = [TRUE]
968 |else
969 ||if not $MARS.Config[172]
970 |||$No.Missile.Target = [TRUE]
971 ||else if [OWNER] == Player
972 |||skip if $MARS.Config[85]
973 ||||$No.Missile.Target = [TRUE]
974 ||end
975 |end
976 |
977 |do if $Debug
978 ||write to log file #$PageID append=[TRUE] printf: fmt=' Precheck: Target is MT %s. No.Ship.Target: %s, No.Msl.Target: %s', $Target.MT, $No.Ship.Target, $No.Missile.Target, null, null
979 |
980 |do if $No.Missile.Target == [TRUE] AND $No.Ship.Target == [TRUE]
981 ||continue
982 |
983 |$Dummy5 = get distance between [THIS] and $Target
984 |if $Target.MT != 10
985 * Ship
986 ||skip if ( $Dummy5 < $Goblin.max.Range - 2500 ) OR ( $Target == $Goblin.Swarm.Target )
987 |||continue
988 * G.S.T can still run OOR of primary target acquisition
989 |else if $Dummy5 > $Gob.Missile.Defense.Range
990 ||continue
991 |end
992 |
993 |if $Target.MT == 10
994 * Incoming missile
995 ||$Dummy1 = $Target -> get ware type code of object
996 ||$Dummy1 = get missile max damage of $Dummy1
997 ||do if $Dummy1 > $Goblin.Missile.Override.Cap OR $Dummy1 > 99000
998 |||$Goblin.Missile.Override = [TRUE]
999 |else
1000 * Ship
1001 * Offense: can deploy gobs beyond laser range
1002 ||$Dummy2 = $Target -> get maximum laser strength
1003 ||$Dummy2 = $Dummy2 * 2
1004 ||
1005 * M8 + M7M
1006 ||do if $Target -> is missile boat
1007 |||$Dummy2 = get maximum, $Dummy2, 80000, null, null, null
1008 ||$Dummy1 = $Dummy2
1009 ||
1010 ||if $Best.Target.Swarm AND ( $Target != $Best.Target.Swarm )
1011 |||$Best.Target = null
1012 |||$Best.Target.Threat = 0
1013 |||$Best.Target.Swarm = null
1014 ||end
1015 |end
1016 |
1017 |do if $Debug
1018 ||write to log file #$PageID append=[TRUE] printf: fmt=' Threat: %s, %s', $Target, $Dummy1, null, null, null
1019 |
1020 |if $Dummy1 > $Best.Target.Threat OR $Best.Target.Threat == 0
1021 ||do if $Debug
1022 |||write to log file #$PageID append=[TRUE] printf: fmt=' New %s.Target.Threat is %s on %s. Was %s. Nr.of.ships: %s', 'Best', $Dummy1, $Target, $Best.Target.Threat, $Goblins.Target.Nr.Possible
1023 ||$Best.Target = $Target
1024 ||$Best.Target.Threat = $Dummy1
1025 |end
1026 end
1027 * = end while Index1
1028
1029 if $Goblins.Target.Nr.Possible != 1
1030 |$Best.Target.Swarm = null
1031 |do if find $Best.Target in array: $Goblins.Target.Arr
1032 ||$Best.Target = null
1033 end
1034
1035 do if $Best.Target.Swarm != null AND $Best.Target == null AND ( $Goblin.Swarm.Target == null OR $Goblin.Swarm.Target == $Best.Target.Swarm )
1036 |$Best.Target = $Best.Target.Swarm
1037
1038 if $Debug
1039 |$Dummy1 = $Best.Target -> get ID code
1040 |$Dummy2 = $Best.Target.Swarm -> get ID code
1041 |write to log file #$PageID append=[TRUE] printf: fmt=' Goblins: Best.Tar: %s %s, Best.Sw.T: %s %s, Repair.Mission: %s', $Dummy1, $Best.Target, $Dummy2, $Best.Target.Swarm, $Goblin.Repair.Mission
1042 end
1043
1044 if $Best.Target -> exists
1045 |gosub Goblin.Launch
1046 else if $Goblin.Repair.Mission
1047 |$Target = [THIS]
1048 |$Best.Target = [THIS]
1049 |$Best.Target.Threat = 100
1050 |gosub Goblin.Launch
1051 end
1052
1053 do if $Debug
1054 |write to log file #$PageID append=[TRUE] printf: fmt=' Goblin launch return', $Best.Target, $Target, null, null, null
1055 endsub
1056
1057 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


X3 is excellent at managing multiple scripts.
As long as you don't make serious blunders like overwriting needed files wholesale, you can [i]add scripts infinitely.
You add a "setup" script. This runs on every game load and you can properly install your script to the game. At this point your script (if smart enough) can even detect other scripts and mods that are already there and react to them or load different pre-set values to work with them.
Very powerful mechanic.


X3 also has a lot of game data externalised in text files, which can be "modded".
These are pure text tables, semicolon delimited.
In the vanilla game these are baked into resource files but will work solo if you unpacked them.
This would become a very long post if I began to list the things you can mod in that game. Sounds, models, UI, ships, weapons, stations, wares, sectors, or the entire universe map...
Requires quite a bit of technical understanding to actually produce working mods but is very powerful.

Mods are very difficult to make compatible with other mods because a table such as "laser data" can only exist once and in full.
This is the weakest point of X3 S&M.


Unity Engine

No previous experience. =)
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 680
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding - common distribution pitfalls

PostPosted by Gazz » Wed Apr 23, 2014 7:01 pm

The first thing to decide is what "modding" will be possible / supported anyway.

  1. Altering of existing game stats. (like the damage of "an iron dagger")
  2. Adding entirely new assets / content to the game. Models, areas / maps, UI graphics, items, NPC, quests...
  3. Changing the game's "code" that decides how things are calculated and what happens if actor A is hit by spell B.
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 680
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding - common distribution pitfalls

PostPosted by Railboy » Wed Apr 23, 2014 7:09 pm

Gazz wrote:The first thing to decide is what "modding" will be possible / supported anyway.


Assume everything will be moddable. Best (and/or worst) case scenario.
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1845
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding - common distribution pitfalls

PostPosted by Railboy » Wed Apr 23, 2014 7:21 pm

Again, there's two kinds of discussions to be had here. The first is, 'Given what FRONTIERS can do, what's the best possible way to do it?' That conversation has to happen soon (and I look forward to it) but that's not what I'm after in this particular thread.

Right now I'm more interested in your opinions about other games. 'I love [x] about modding in general and feel that [a,b,c] games do that thing well. And I loathe [y] about modding in general and feel that [d,e,f] games can go to hell for doing it.' Shop talk, basically. (The stuff you just laid down is great.)

Eavesdropping on shop talk gives me tons of useful information. I'm trying to (inorganically) create that conversation that here so I can learn from you guys.
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1845
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding - common distribution pitfalls

PostPosted by Gazz » Wed Apr 23, 2014 8:11 pm

I don't know anything about how Unity "ticks" so I'll stick to general terms. ;)


What is always bad is when something new must be entered into a table that only exists once in the game.
That's the "bad" part of X3 modding (and many other games). You must replace the entire laser table if you want to add a laser. The assets for that laser (sound etc) are then referenced from there and you can put them anywhere.



The best part of X3 S&M is the installation / activation of AI scripts.
You prefix a script name with "setup." and that script will be found and executed every time the game is loaded.
This script then references the rest of your custom script package. It starts background tasks (like healing you every time the game is loaded), ties functions to custom function buttons in the game, or adds scripts to "events" such as something being damaged by an attacker.

As long as you are careful about the script's context, nearly all scripts can co-exist peacefully because they all create their own environment. They put local (named) variables on assets, create global (named) variables that can be accessed from anywhere in the game - such as configuration values that the player enters for the script.
If your script knows the exact names of the local variables that my script puts on any "affected" NPC, your script can interact with my script, maybe act as an addon that makes an NPC sneeze regularly if my script has set a variable GZ.NPC.FLU = True for this NPC.
The various scripts' environments are only voluntarily separate. But it's very cool when they can interact where it makes sense. =)

For the user the script installation is trivial because it amounts to unzipping the download into the game's main folder. Done.
(the zip would include the folder structure and all "script" files end up in the game's "scripts" folder)

One can also write an uninstall script.
Also a setup script but it has the same name as the script that "installs" your script package.
It removes all data arrays and generally undoes everything the script could have done. And since it overwrote the previous setup script, the original script is not reinstalled the next time the game loads.
What remains are a bunch of (script) files in a folder that are no longer referenced by the game. Not ideal but harmless.





The thing I am always interested in are a game's "scripts". (You've probably figured that out by now =)
What happens if you drop an item to the ground? How do I tie into that function and make the ground shake when the player drops Mjölnir to the ground?
How do I create a magical belt buckle that sometimes complains about your eating habits or makes suggestions for a healthier diet while you are preparing one?
How easily can one do stuff?
Making your modded sword green is neat but after you've seen it, that's it. It's green and nothing more interesting happens.
But if it changes colour every time orcs are near... that's a lot more interesting.
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 680
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding - common distribution pitfalls

PostPosted by yarnevk » Wed Apr 23, 2014 8:45 pm

Elder scrolls are horrid about script modding, their stated policy is if you mod a script the only way to revert it is an earlier save without the script. And of course their script languages always getting rewritten with script extenders from modders. I suspect Unity is much better at script languages and script management.

Most Bethesda modders use mash/bash/boss third party programs for loading, sorting, archiving, removing and depency checks. And of course Bethesda give you the same editor the game devs use for all the game objects.
yarnevk
Pathmaker
 
Posts: 178
Joined: Tue Sep 17, 2013 2:48 pm

Re: Modding - common distribution pitfalls

PostPosted by Sirinan » Wed Apr 30, 2014 8:30 am

The Bethesda games are supported terrifically well for modding (though I agree with Gazz about their scripting) but the one important thing that they are really poor at is having ways for multiple mods to work properly together. There needs to be a way to have diff-like changes to assets rather than a complete overwrite with a changed version, to allow multiple mods to work together unless they change the exact same data.

@postnjam - please, not steam workshop. Like everything which steam touches, it immediately locks out anyone who doesn't want to use steam.
Sirinan
Novice
Novice
 
Posts: 5
Joined: Sat Apr 19, 2014 10:09 pm

Re: Modding - common distribution pitfalls

PostPosted by postnjam » Wed Apr 30, 2014 10:03 am

Sirinan wrote:The Bethesda games are supported terrifically well for modding (though I agree with Gazz about their scripting) but the one important thing that they are really poor at is having ways for multiple mods to work properly together. There needs to be a way to have diff-like changes to assets rather than a complete overwrite with a changed version, to allow multiple mods to work together unless they change the exact same data.

@postnjam - please, not steam workshop. Like everything which steam touches, it immediately locks out anyone who doesn't want to use steam.


Oh yeah, forgot about that.
User avatar
postnjam
Dungeon Crawler
 
Posts: 126
Joined: Thu Jul 18, 2013 8:36 pm
Location: Sawbridgeworth , UK

Re: Modding - common distribution pitfalls

PostPosted by Gazz » Wed Apr 30, 2014 10:26 am

The Steam Workshop may act a downloader but the game still needs to have the mechanisms to register / load mods so the Steam Workshop doesn't really change anything.

Users who don't use the Workshop would then have to unzip the mod package into the game/mod folder.

The Steam Workshop doesn't magically resolve resource or script conflicts, either. The game needs to do all the hard work. =)
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 680
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding - common distribution pitfalls

PostPosted by Railboy » Wed Apr 30, 2014 10:07 pm

Has anyone here played / modded a game that was capable of combining / resolving / assembling a non-destructive modded copy of the world data? Or is that a holy grail that has yet to be achieved?
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1845
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA

Re: Modding - common distribution pitfalls

PostPosted by Gazz » Thu May 01, 2014 10:01 am

You mean a game where one can add mods at every level and that doesn't blow up as a result of all the conflicts?

The only ones which don't blow up are the ones with severely restrict modding to accessing templates that will work together, such as adding a different weapon... that has a standardised handle and shoots something damaging forward.

Some modders have invested great effort into creating mod managers or conflict resolvers which try to mitigate the damage or at least point out that a mod can not be used with what you currently have installed.



How could you mod a game on a planetary surface without objects overlapping each other?
If I add a farmhouse where another mod has already added a market stand... that's no good.
Since the map is only one instance, you'd need some kind of meta-placement of assets, kind of like player-housing is handled in several MMOs.
For example:
  • A mod requests a size 3 plot for placing a structure of some sort at position 6545/423
  • The plot has to have flags for (largely) flat, grassland, and no road access.
  • The game assigns the first match closest to the stated position.
  • If no such match can be found the asset isn't placed.
The good news would be that such generic plots would always "work".
The bad news is that you can't guarantee that a mod will find the required free spots... and obviously it's a huge task to define all the potential plots in the first place.
A restriction like that also doesn't work when someone wants to place an entire town or build the Great Wall.

Alas, there is no perfect system other than modders voluntarily observing placement restrictions by logging all their objects and NPC in an online database. Assuming this isn't a total conversion and a completely different instance of the world...


And that only covers stationary assets. And the adding thereof. Replacing an existing structure with something else is another can of worms because other parts of the game might reference things or NPC in the original structure.


Scripts are... difficult because you won't know what they will do.
In theory scripters are awesome coders who take great care in not messing with the functionality of other scripts. In practice they are a wild and crazy bunch who do make the occasional mistake.

I wish I had any useful answers but even when building a game for modding from the ground up, compatibility of multiple mods without serious restrictions is always messy.



In a procedurally generated world mods can be inserted more easily because the world can be re-generated with them. But that doesn't help Frontiers. =)
The first rule of Tautology Club is the first rule of Tautology Club. - XKCD
User avatar
Gazz
Henchman
Henchman
 
Posts: 680
Joined: Thu Jul 18, 2013 7:28 am
Location: In your brains. Thinking your thoughts.

Re: Modding - common distribution pitfalls

PostPosted by yarnevk » Thu May 01, 2014 1:19 pm

Morrowind had so many world changing mods that someone created a mod map manager allowing you to move things and resolved terrain overlaps. That worked because Vardenfull is a volcanic island surrounded by seas. But terrain changers of the main area are the worst kind of mod conflicts to resolve. Using conditionals to place dynamically also means you need conditional maps and dynamic dialogue so that the place can be found, not easy to do. You are better off saying such things must go on their on islands over there with teleports or boats, which means adding something to the lore saying how they can get there, maybe paths can magically work like bridges. This is where the instanced dungeons are useful that the door teleports you there, someone even did MarioWorld as a dungeon for Skyrim. I am wanting to do a classic hack/rogue-like dungeon generator using game assets, so hope you have understandable dungeon kits (I gave up on doing it in skryim)
yarnevk
Pathmaker
 
Posts: 178
Joined: Tue Sep 17, 2013 2:48 pm

Re: Modding - common distribution pitfalls

PostPosted by Moses » Sat May 03, 2014 5:02 am

Now I've got Strongbad's "The Cheat" song stuck in my head from one of Railboy's posts regarding cheats, thanks Lars.

Anyway, in my experience in modding for Morrowind and Oblivion one thing I always thought they would benefit from is a change to how the mods are referenced in the save file. Normally the save file has one byte added to the reference ID's which specifies which mod in the mod list the object originates from. The problem is the list is saved in the ini, not the save file itself and changing things quickly makes save files no longer work.

A better method I believe would be to internalize the list per save file as this would prevent object ID's from getting mismatched if the load order changes. This happens fairly often when people get into installing updates to mods and deleting the old ones. This would also mean save files that are recovered or shared with friends contain the names of which mods are required to open them. Here is an example of what I am talking about

index'd list of the file names like so:
["INeedTobeLoadedSecond.mod","INeedTobeLoadedFourth.mod","INeedTobeLoadedThird.mod","INeedTobeLoadedFirst.mod"]

and a list which specifies the index of the file name to load in which order like so:
[4,1,3,2]

This second list allows the load order to be changed without having to edit the reference ID of each object in a save file as the object's reference ID's refer to the file name list, not the load order list. This would also allow for a more personalized mod list per save file and would keep people from having to disable certain mods, re-enable others, and change the load order in order for them to play a set of mods that are incompatible with or just unwanted in a different save file. This approach would cost hardly anything in save file space and would make things MUCH easier down the road, even if you aren't wanting to look that far ahead.

Also, when loading a save file that is missing a mod with active effects on the player(such as modified carry weight or active spells) you run across potentially game breaking stat bugs. I know quite a few people who have had save files unplayable because they loaded without the proper plugins, were heavily encumbered or had damaged stats, and the things modifying these things were simply gone, resulting in permanent phantom weight or damaged stats.

I'm sure you can come up with a solution that works perfectly for this, but one suggestion I have is to have stats and such saved MINUS changes caused by mods and active effects, recalculating them on reload instead. This way you don't end up with permanent residual effects from removing mods and allows changes to objects during mod updates to work properly.


P.S. People install a lot more than 256 mods sometimes so make it at least a two byte identifier.

P.P.S. You can never be too liberal with what you allow to be modded, nor should you ever expect your players will be incapable of modding it. If people want/need to change something they will write their own tools if they have to. I've done this for Halo Custom Edition and Xbox Halo. I can see modding becoming a central point of this game in the future like it is with Minecraft.
User avatar
Moses
Novice
Novice
 
Posts: 5
Joined: Sat Oct 05, 2013 9:00 pm


Return to Modding Central

Who is online

Users browsing this forum: No registered users and 0 guests

cron