Fractal Softworks Forum

Please login or register.

Login with username, password and session length
Advanced search  

News:

Starsector 0.97a is out! (02/02/24); New blog post: Simulator Enhancements (03/13/24)

Pages: [1] 2

Author Topic: The black magic behind "modules on modules"  (Read 9520 times)

Sinosauropteryx

  • Captain
  • ****
  • Posts: 262
    • View Profile
The black magic behind "modules on modules"
« on: October 29, 2019, 10:31:00 AM »

Hi, who's interested in how this guy works?

Maybe you want to make your own? Keep reading!

There's several layers to the whole thing, but I'm mostly going to focus on the primary part, how to make articulated chains of modules that can move and bend however you want them to. It might even be simpler than you expected. To start, you'll need a few things:

1. .ship file for your ship.
The only unusual thing about this is you will want a station slot for every one of your modules, and it helps to label them in a coherent way (for later). For my 8-segment snake, I made 7 station slots called "SEGMENT 1" through "SEGMENT 7." Only the location of the first slot matters, the rest can be posed in a way that looks good on the refit screen. (Their locations will be moved dynamically during battle.)
Picture Example
[close]


2. .ship files for your modules.
Mine uses a single .ship file as all the segments are the same, but they don't have to be.
What IS important is, each .ship file must have the ship's center of gravity located at the point the module attaches to the previous one, which is probably heavily offset towards the front. The module will rotate around this point.
The other important thing is each ship must have a station slot called SEGMENT. This will be the point at which the next segment is attached.
Picture Examples
[close]


3. .variant files for your ship and module, as well as ship_data.csv entries. These are done like you would a normal module ship - just make sure all segments are attached to the station slots of your mothership.


4. A container class, which I called SinuousSegment, that contains a ShipAPI, the previous SinuousSegment, and the next SinuousSegment. I also put in a method that assigns a list of child modules to an array of SinuousSegments, based on the name of the station slot they're occupying.
KT_SinuousSegment
Code
package data.scripts.util;

import com.fs.starfarer.api.combat.ShipAPI;
import java.util.List;

public class KT_SinuousSegment {
    public ShipAPI ship = null;
    public KT_SinuousSegment nextSegment = null;
    public KT_SinuousSegment previousSegment = null;

    public static void setup(KT_SinuousSegment[] segments, List<ShipAPI> ships, String[] args){

        for (int f = 0; f < segments.length; f++){
            // Iterates through SinuousSegment array and connects them in order
            segments[f] = new KT_SinuousSegment();
            if (f > 0){
                segments[f].previousSegment = segments[f-1];
                segments[f-1].nextSegment = segments[f];
            }

            // Assigns each module to a segment based on its station slot name
            for (ShipAPI s : ships) {
                s.ensureClonedStationSlotSpec();

                if (s.getStationSlot() != null && s.getStationSlot().getId().equals(args[f])) {
                    segments[f].ship = s;

                    // First module only: Assigns mothership as its previousSegment
                    if (f == 0){
                        segments[f].previousSegment = new KT_SinuousSegment();
                        segments[f].previousSegment.ship = s.getParentStation();
                        segments[f].previousSegment.nextSegment = segments[f];
                    }
                }
            }
        }
    }

    public KT_SinuousSegment(){
    }

    public KT_SinuousSegment(ShipAPI newShip) {
        ship = newShip;
        previousSegment = new KT_SinuousSegment();
        previousSegment.ship = ship.getParentStation();
        previousSegment.nextSegment = this;
    }

    public KT_SinuousSegment(ShipAPI newShip, KT_SinuousSegment newPrevious){
        ship = newShip;
        previousSegment = newPrevious;
        previousSegment.nextSegment = this;
    }

}
[close]
(Note, there's a problem with displaying code tags inside spoiler tags, so click the clipboard icon on the right and paste it into a notepad.)


5. A hullmod, which I called Sinuous Body. The bulk of the work is done here.
KT_SinuousBody
Code
package data.hullmods;

import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.combat.*;
import com.fs.starfarer.api.combat.ShipAPI.HullSize;
import com.fs.starfarer.api.combat.ShipwideAIFlags.AIFlags;
import data.scripts.util.KT_SinuousSegment;
import java.util.List;
import com.fs.starfarer.api.combat.ShipEngineControllerAPI.ShipEngineAPI;

public class KT_SinuousBody extends BaseHullMod {

    public static final int NUMBER_OF_SEGMENTS = 7;
    public static final float RANGE = 150f; // Flexibility constant. Range of movement of each segment.
    public static final float REALIGNMENT_CONSTANT = 8f; // Elasticity constant. How quickly the body unfurls after being curled up.

    private KT_SinuousSegment[] seg = new KT_SinuousSegment[NUMBER_OF_SEGMENTS];
    private String[] args = new String[NUMBER_OF_SEGMENTS];


    public String getDescriptionParam(int index, HullSize hullSize) {
return null;
}


    public void applyEffectsBeforeShipCreation(HullSize hullSize, MutableShipStatsAPI stats, String id) {
    }

    @Override
    public void applyEffectsAfterShipCreation(ShipAPI ship, String id) {


    }


    @Override
    public boolean isApplicableToShip(ShipAPI ship) {
        return true;
    }

    @Override
    public void advanceInCombat(ShipAPI ship, float amount) {

        super.advanceInCombat(ship, amount);

        // Initiates the SinuousSegment array.
        args[0] = "SEGMENT1";
        args[1] = "SEGMENT2";
        args[2] = "SEGMENT3";
        args[3] = "SEGMENT4";
        args[4] = "SEGMENT5";
        args[5] = "SEGMENT6";
        args[6] = "SEGMENT7";

        List<ShipAPI> children = ship.getChildModulesCopy();

        advanceParent(ship,children);
        for (ShipAPI s : children){
            advanceChild(s, ship);
        }

        KT_SinuousSegment.setup(seg, children, args);

        // Iterates through each SinuousSegment
        for (int f = 0; f < NUMBER_OF_SEGMENTS; f++) {
            if (seg[f] != null && seg[f].ship != null && seg[f].ship.isAlive()) {
                try {

                    // First segment is "vanilla" / attached to mothership. Rest are pseudo-attached to previous segment's SEGMENT slot
                    if (f != 0)
                        seg[f].ship.getLocation().set(seg[f].previousSegment.ship.getHullSpec().getWeaponSlotAPI("SEGMENT").computePosition(seg[f].previousSegment.ship));


                    // Each module hangs stationary in real space, instead of turning with the mothership, unless it's at max turning range
                    float angle = normalizeAngle(seg[f].ship.getFacing() - seg[f].ship.getParentStation().getFacing());

                    // angle of module is offset by angle of previous module, normalized to between 180 and -180
                    float angleOffset = getAngleOffset(seg[f]);
                    if (angleOffset > 180f)
                        angleOffset -= 360f;

                    // angle of range check is offset by angle of previous segment in relation to mothership
                    float localMod = normalizeAngle(seg[f].previousSegment.ship.getFacing() - seg[f].ship.getParentStation().getFacing());

                    // range limit handler. If the tail is outside the max range, it won't swing any farther.
                    if (angleOffset < RANGE * -0.5)
                        angle = normalizeAngle(RANGE * -0.5f + localMod);
                    if (angleOffset > RANGE * 0.5)
                        angle = normalizeAngle(RANGE * 0.5f + localMod);

                    // Tail returns to straight position, moving faster the more bent it is - spring approximation
                    angle -= (angleOffset / RANGE * 0.5f) * REALIGNMENT_CONSTANT;

                    seg[f].ship.getStationSlot().setAngle(normalizeAngle(angle));
                } catch (Exception e) {
                    // This covers the gap between when a segment and its dependents die
                }

            } else {
                // When a segment dies, remove all dependent segments
                for (int g = f; g < NUMBER_OF_SEGMENTS; g++){
                    if (seg[g] != null && seg[g].ship != null && seg[g].ship.isAlive()) {
                        try {
                            seg[g].ship.setHitpoints(1f);
                            seg[g].ship.applyCriticalMalfunction(seg[g].ship.getAllWeapons().get(0));
                            seg[g].ship.applyCriticalMalfunction(seg[g].ship.getEngineController().getShipEngines().get(0)); // The ONLY way I've found to kill a module
                        } catch (Exception e){
                        }
                        //seg[g].ship.getFleetMember().getStatus().setDetached(0,true);
                        //seg[g].ship.getFleetMember().getStatus().applyDamage(100000);
                        //Global.getCombatEngine().removeEntity(seg[g].ship);
                    }
//                    seg[g] = null;
                }
            }
        }

    }

    private float normalizeAngle (float f){
    if (f < 0f)
            return f + 360f;
    if (f > 360f)
        return f - 360f;
    return f;
    }

    private float getAngleOffset (KT_SinuousSegment seg){
        try {
            return normalizeAngle(seg.ship.getFacing() - seg.previousSegment.ship.getFacing());
        } catch (Exception e) {
            return 0f;
        }
    }

    //////////
    // This section of code was taken largely from the Ship and Weapon Pack mod.
    // I did not create it. Credit goes to DarkRevenant.
    //////////
    private static void advanceChild(ShipAPI child, ShipAPI parent) {
        ShipEngineControllerAPI ec = parent.getEngineController();
        if (ec != null) {
            if (parent.isAlive()) {
                if (ec.isAccelerating()) {
                    child.giveCommand(ShipCommand.ACCELERATE, null, 0);
                }
                if (ec.isAcceleratingBackwards()) {
                    child.giveCommand(ShipCommand.ACCELERATE_BACKWARDS, null, 0);
                }
                if (ec.isDecelerating()) {
                    child.giveCommand(ShipCommand.DECELERATE, null, 0);
                }
                if (ec.isStrafingLeft()) {
                    child.giveCommand(ShipCommand.STRAFE_LEFT, null, 0);
                }
                if (ec.isStrafingRight()) {
                    child.giveCommand(ShipCommand.STRAFE_RIGHT, null, 0);
                }
                if (ec.isTurningLeft()) {
                    child.giveCommand(ShipCommand.TURN_LEFT, null, 0);
                }
                if (ec.isTurningRight()) {
                    child.giveCommand(ShipCommand.TURN_RIGHT, null, 0);
                }
            }

            ShipEngineControllerAPI cec = child.getEngineController();
            if (cec != null) {
                if ((ec.isFlamingOut() || ec.isFlamedOut()) && !cec.isFlamingOut() && !cec.isFlamedOut()) {
                    child.getEngineController().forceFlameout(true);
                }
            }
        }
        /* Mirror parent's fighter commands */
        if (child.hasLaunchBays()) {
            if (parent.getAllWings().size() == 0 && (Global.getCombatEngine().getPlayerShip() != parent || !Global.getCombatEngine().isUIAutopilotOn()))
                parent.setPullBackFighters(false); // otherwise module fighters will only defend if AI parent has no bays
            if (child.isPullBackFighters() ^ parent.isPullBackFighters()) {
                child.giveCommand(ShipCommand.PULL_BACK_FIGHTERS, null, 0);
            }
            if (child.getAIFlags() != null) {
                if (((Global.getCombatEngine().getPlayerShip() == parent) || (parent.getAIFlags() == null))
                        && (parent.getShipTarget() != null)) {
                    child.getAIFlags().setFlag(AIFlags.CARRIER_FIGHTER_TARGET, 1f, parent.getShipTarget());
                } else if ((parent.getAIFlags() != null)
                        && parent.getAIFlags().hasFlag(AIFlags.CARRIER_FIGHTER_TARGET)
                        && (parent.getAIFlags().getCustom(AIFlags.CARRIER_FIGHTER_TARGET) != null)) {
                    child.getAIFlags().setFlag(AIFlags.CARRIER_FIGHTER_TARGET, 1f, parent.getAIFlags().getCustom(AIFlags.CARRIER_FIGHTER_TARGET));
                } else if (parent.getShipTarget() != null){
                    child.getAIFlags().setFlag(AIFlags.CARRIER_FIGHTER_TARGET, 1f, parent.getShipTarget());
                }
            }
        }
    }
    private static void advanceParent(ShipAPI parent, List<ShipAPI> children) {
        ShipEngineControllerAPI ec = parent.getEngineController();
        if (ec != null) {
            float originalMass = 2500;
            int originalEngines = 18;

            float thrustPerEngine = originalMass / originalEngines;

            /* Don't count parent's engines for this stuff - game already affects stats */
            float workingEngines = ec.getShipEngines().size();
            for (ShipAPI child : children) {
                if ((child.getParentStation() == parent) && (child.getStationSlot() != null) && child.isAlive()) {
                    ShipEngineControllerAPI cec = child.getEngineController();
                    if (cec != null) {
                        float contribution = 0f;
                        for (ShipEngineAPI ce : cec.getShipEngines()) {
                            if (ce.isActive() && !ce.isDisabled() && !ce.isPermanentlyDisabled() && !ce.isSystemActivated()) {
                                contribution += ce.getContribution();
                            }
                        }
                        workingEngines += cec.getShipEngines().size() * contribution;
                    }
                }
            }

            float thrust = workingEngines * thrustPerEngine;
            float enginePerformance = thrust / Math.max(1f, parent.getMassWithModules());
            parent.getMutableStats().getAcceleration().modifyMult("KT_sinuousbody", enginePerformance);
            parent.getMutableStats().getDeceleration().modifyMult("KT_sinuousbody", enginePerformance);
            parent.getMutableStats().getTurnAcceleration().modifyMult("KT_sinuousbody", enginePerformance);
            parent.getMutableStats().getMaxTurnRate().modifyMult("KT_sinuousbody", enginePerformance);
            parent.getMutableStats().getMaxSpeed().modifyMult("KT_sinuousbody", enginePerformance);
            parent.getMutableStats().getZeroFluxSpeedBoost().modifyMult("KT_sinuousbody", enginePerformance);
        }
    }

}
[close]
(Line 17, line 47) First, an array of SinuousSegments and an array of Strings are created. The Strings are assigned to the names of the station slots on the mothership. (SEGMENT1, etc.)
(Line 62) Next, SinuousSegment.setup is called, passing the SinuousSegment array, ship.getChildModules(), and the String array. This leaves you with a SinuousSegment array that iterates through the segments in order, and each segment can refer to its previous or next segment.
(Line 65) Then the main for loop is called. First comes a check - is the segment alive?
(Line 67) Then a try block - this is a little inelegant, but because there is only one way to kill a module with commands, and it takes several frames to make happen, there will be a several-frame gap that will throw exceptions every time a segment with dependents dies. The try-catch just gets us through those frames.
(Line 70) Then there's the real meat, starting with the key line to making this all work:
Code
if (f != 0)
    seg[f].ship.getLocation().set(seg[f].previousSegment.ship.getHullSpec().getWeaponSlotAPI("SEGMENT").computePosition(seg[f].previousSegment.ship));
Each segment's location (other than the first segment's) is updated to be where the previous segment's SEGMENT slot is. This is the whole illusion: the module has a virtual station slot on the back of another module, and is moved there every frame.
Most other stuff within the loop after that is what determines the way the segments move. For my Quetzalcoatl, modules are set to hang unmoving as the head turns, then adjust back to "neutral" with a simulated spring action, as well as each having a limited range of movement. But it can move in any number of ways, the possibilities are endless. Just remember you're using the module's station slot's rotation to change its angle:
Code
seg[f].ship.getStationSlot.setAngle()
(Line 99) After that, the "else" to the "if alive" check. This part handles killing all the dependent segments of a segment that's died.
Everything south of that is boilerplate-type module stuff, most of which was taken from DarkRevenant's glorious Ship & Weapon Pack. Really can't thank him enough for innovating in this area (and many others).

And that's pretty much it. Once you're able to get your segments lined up the way you like, getting them to move right is just trial and error. I'm looking forward to seeing some cool stuff, happy coding!
« Last Edit: October 29, 2019, 10:34:01 AM by Sinosauropteryx »
Logged

Ed

  • Captain
  • ****
  • Posts: 442
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #1 on: October 29, 2019, 02:30:20 PM »

I love you
Logged
Check out my ships

Thaago

  • Global Moderator
  • Admiral
  • *****
  • Posts: 7173
  • Harpoon Affectionado
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #3 on: October 29, 2019, 03:16:51 PM »

Haha this looks awesome, thanks for sharing! This might be a cool thing to add to the Wiki's modding section if you are interested in crossposting there.
Logged

Nick XR

  • Admiral
  • *****
  • Posts: 712
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #4 on: October 29, 2019, 03:57:18 PM »

Now to have the engine nozzles vectors change relative to movement inputs... Hmmmm.

Sinosauropteryx

  • Captain
  • ****
  • Posts: 262
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #5 on: October 29, 2019, 05:22:28 PM »

I love you
Shucks
How do they play out on the refit screen?
The modules will be arranged to fit the slots you put on the mothership, so you can pose it however you want. It's also how it looks when out in space so keep that in mind.
Haha this looks awesome, thanks for sharing! This might be a cool thing to add to the Wiki's modding section if you are interested in crossposting there.
Good idea, I probably will. The wiki was invaluable for getting me started modding.
Now to have the engine nozzles vectors change relative to movement inputs... Hmmmm.
If I recall, MagicLib has a way to do just that, although it's resource intensive.
Logged

Yunru

  • Admiral
  • *****
  • Posts: 1560
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #6 on: October 30, 2019, 01:32:26 PM »

Apologies, I meant for things like fitting weapon slots.

(As a complete aside, did you have any trouble with getting modules to turn off shields?)

Wyvern

  • Admiral
  • *****
  • Posts: 3784
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #7 on: October 30, 2019, 01:58:40 PM »

Nice job!

(I've had something vaguely related to this in planning stages for a while - neat to see the proof that it's doable.  I'm still hoping we get a way to properly mirror decorative weapons, though...)
Logged
Wyvern is 100% correct about the math.

Shoat

  • Captain
  • ****
  • Posts: 262
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #8 on: October 30, 2019, 02:51:07 PM »

Articulated dragons? Majestic.
Someone is now bound to create some sort of eldritch horror space kraken tentacle monster, which is certainly something to look forward to.
Also who needs ordinary cargo ships when you can now have trains in space?

Thank you for adding this into the game's modding world.
Logged

Sinosauropteryx

  • Captain
  • ****
  • Posts: 262
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #9 on: October 30, 2019, 05:38:19 PM »

Apologies, I meant for things like fitting weapon slots.
Like, how does refitting it work? Basically, you start with the mothership, and can click on each module to highlight it, center the camera on it, and get the ability to edit it. All module ships are refit like this. It's a bit wonky until you get used to it.

Quote
(As a complete aside, did you have any trouble with getting modules to turn off shields?)
Not really, they act like normal AI ships and will shield the same things. So maybe they're a little overcautious but they seem to shield and unshield when they should.

Nice job!

(I've had something vaguely related to this in planning stages for a while - neat to see the proof that it's doable.  I'm still hoping we get a way to properly mirror decorative weapons, though...)
Thanks, and I agree about mirroring in general. I'd like to have a missile rack with two mirrored missile sprites.

Thank you for adding this into the game's modding world.
I appreciate it :)
Logged

Ed

  • Captain
  • ****
  • Posts: 442
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #10 on: October 30, 2019, 06:35:55 PM »

Also who needs ordinary cargo ships when you can now have trains in space?
Calling dibs on that, gimme a week or two.
Logged
Check out my ships

Ed

  • Captain
  • ****
  • Posts: 442
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #12 on: November 03, 2019, 05:23:38 PM »

I really want to thank you again Sinosauropteryx, thanks to your code i managed to make the most silly and fun dreadnought since the neutrino hammer
Logged
Check out my ships

Sinosauropteryx

  • Captain
  • ****
  • Posts: 262
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #13 on: November 03, 2019, 09:32:59 PM »

You're welcome, and the train looks great!
Logged

Professor Pinkie

  • Ensign
  • *
  • Posts: 23
  • Best cupcakes in a sector!
    • View Profile
Re: The black magic behind "modules on modules"
« Reply #14 on: June 22, 2020, 10:03:01 AM »

The code was shamelessly stolen by me!

Works. Roboarm dangles as it should!
Spoiler
[close]

I bet, the author did not expect his creation to be used for a useless decorative element.

Thank you!
Logged
Pages: [1] 2