Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Place to get help with not working mods / modding interface.
Zeritor
Burner Inserter
Burner Inserter
Posts: 14
Joined: Thu Mar 24, 2016 12:04 pm
Contact:

Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Zeritor »

As it's on the Prototype, it doesn't look like you can modify it at runtime and even if you could, you would affect all instances.

I want to be able to show/hide Layers at the Entity level based on conditions. However, I can't see a way to do that via run time scripting. Is there anything I can do?
robot256
Smart Inserter
Smart Inserter
Posts: 1269
Joined: Sun Mar 17, 2019 1:52 am
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by robot256 »

Your script can attach drawn sprites to a rolling stock entity and they will move with it. But it would be really hard to make it look like part of the wagon.

I wrote a library specifically to sneakily replay one tonight stock entity with another to change the graphics. You are welcome to depend on it if it's useful to you. https://mods.factorio.com/mod/Robot256Lib
My mods: Multiple Unit Train Control, RGB Pipes, Shipping Containers, Rocket Log, Smart Artillery Wagons.
Maintainer of Auto Deconstruct, Cargo Ships, Vehicle Wagon, Honk, Shortwave.
User avatar
Osmo
Fast Inserter
Fast Inserter
Posts: 151
Joined: Wed Oct 23, 2024 12:08 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Osmo »

You can use LuaRendering to draw stuff in a way thay it looks like a part of the wagon.
5526_0000h46m57s_nauvis.jpg
5526_0000h46m57s_nauvis.jpg (961.57 KiB) Viewed 377 times
But it is kind of complex, and swapping the wagon to a different one may be better. Or just not modifying graphics at all if its not strictly necessary
Zeritor
Burner Inserter
Burner Inserter
Posts: 14
Joined: Thu Mar 24, 2016 12:04 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Zeritor »

I've just been messing with the `rendering` global. It doesn't quote work perfectly I find. I might be doing something wrong.

Compared to using the normal way of spriting a wagon, I'm using an animation and using the wagon's rotation to index the correct frame in the animation.

Only it drifts off centre on corners compared to the normal way, as well as the rotation not being quite right (seems to be using the angle between the front/rear wheels, when a wagon is actually floatier than that).

Any tips?
User avatar
Osmo
Fast Inserter
Fast Inserter
Posts: 151
Joined: Wed Oct 23, 2024 12:08 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Osmo »

Zeritor wrote: Sun Sep 21, 2025 9:43 pm Only it drifts off centre on corners compared to the normal way, as well as the rotation not being quite right (seems to be using the angle between the front/rear wheels, when a wagon is actually floatier than that).

Any tips?
That's because due to the game's apparent camera angle, the actual rotation is not directly proportional to the rotation it should have in 3d.

Here's a function for that (project_2d_to_3d)

Code: Select all

local tau = 2 * math.pi -- ~6.2832
local tau_inv = 1 / tau -- ~0.1592
local sqrt2 = math.sqrt(2) -- ~1.4142
local sqrt2_inv = 1 / sqrt2 -- ~0.7071

function orientation_to_angle(orientation)
    return (0.25 - orientation) * tau
end

function angle_to_orientation(angle)
    return 0.25 - angle * tau_inv
end

local atan2, cos, sin = math.atan2, math.cos, math.sin
-- For a desired 2D orientation, return the 3D orientation factor necessary to visually match it.
function project_2d_to_3d(o)
    local angle = orientation_to_angle(o)
    angle = atan2(sin(angle)*sqrt2, cos(angle))
    return mod(angle_to_orientation(angle), 1)
end
Zeritor
Burner Inserter
Burner Inserter
Posts: 14
Joined: Thu Mar 24, 2016 12:04 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Zeritor »

Thanks so much, that's made a big difference. The overlay texture is now rotated correctly.

I still have an offset issue, then a totally broken rotation if I reverse the wagon.
09-22-2025, 00-54-52.png
09-22-2025, 00-54-52.png (6.91 MiB) Viewed 282 times
Not sure if this is to do with how I'm defining the animation - or how I'm setting the angle. Currently I'm setting the animation target to be the wagon, then doing this in an `onTick`

Code: Select all

local orientation = locked_wagon.entity.orientation
local osmoFix = project_2d_to_3d(orientation)
local frameIndex = math.floor(osmoFix * 256)
log("Wagon Animation Update, Wagon Orientastion " .. locked_wagon.entity.orientation .. " Frame Chosen:" .. frameIndex)
locked_wagon.animation.animation_offset = frameIndex
Zeritor
Burner Inserter
Burner Inserter
Posts: 14
Joined: Thu Mar 24, 2016 12:04 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Zeritor »

Fixed the wrong image by wrapping to keep the frame index in range. Just to tackle the position oddness now.
Zeritor
Burner Inserter
Burner Inserter
Posts: 14
Joined: Thu Mar 24, 2016 12:04 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Zeritor »

Ahhhh, I just found there's `LuaEntity.draw_data.position`, setting the `target` of my animation to that now:
09-22-2025, 01-18-08.png
09-22-2025, 01-18-08.png (8.54 MiB) Viewed 251 times
Not perfect (and the renders are bad, just threw in random stuff I found on open source model sites to see if it was possible).

Thanks again - wouldn't have got here without that last push with your function.
Zeritor
Burner Inserter
Burner Inserter
Posts: 14
Joined: Thu Mar 24, 2016 12:04 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Zeritor »

Though when the train is going fast they still look bad, hmm. It's like I'm setting the position for the last draw, so it's always lagging behind.
09-22-2025, 01-37-39.png
09-22-2025, 01-37-39.png (376.72 KiB) Viewed 224 times
Even the rotation is wrong, hmm. It's like I'm getting the data at the start of the frame, applying that, then the train's update is handled, moving it forwards on the rail. Is there no way to do a "on post tick" or similar to other game engines?
User avatar
Osmo
Fast Inserter
Fast Inserter
Posts: 151
Joined: Wed Oct 23, 2024 12:08 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Osmo »

Zeritor wrote: Mon Sep 22, 2025 12:25 am Though when the train is going fast they still look bad, hmm. It's like I'm setting the position for the last draw, so it's always lagging behind.

Even the rotation is wrong, hmm. It's like I'm getting the data at the start of the frame, applying that, then the train's update is handled, moving it forwards on the rail. Is there no way to do a "on post tick" or similar to other game engines?
No, some events are raised later in the tick, but even then train position may not have updated yet. But you can add a train's velocity to the position. Not sure how to handle change in rotation though, but it shouldn't be visible as much as position, especially since the faster it moves the harder it is to see.
Zeritor
Burner Inserter
Burner Inserter
Posts: 14
Joined: Thu Mar 24, 2016 12:04 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Zeritor »

I can get the speed, as in a total magnitude but not the velocity, as in the direction. In fact I see very little for direction. I guess I can use orientation and speed with vector math to get x/y components.
Zeritor
Burner Inserter
Burner Inserter
Posts: 14
Joined: Thu Mar 24, 2016 12:04 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Zeritor »

Yeah, Vector maths works.
09-22-2025, 02-10-07.png
09-22-2025, 02-10-07.png (1.1 MiB) Viewed 171 times
Position is fine now, rotation not so much. It doesn't look nearly as bad and only really notice it if you pause/screenshot a very fast train.

Can't see a way to predict rotation as it looks like there's no info for what's next in regards to rail.

Code is now:

Code: Select all

local tau = 2 * math.pi -- ~6.2832
local tau_inv = 1 / tau -- ~0.1592
local sqrt2 = math.sqrt(2) -- ~1.4142

function orientation_to_angle(orientation)
    return (0.25 - orientation) * tau
end

function angle_to_orientation(angle)
    return 0.25 - angle * tau_inv
end


local atan2, cos, sin = math.atan2, math.cos, math.sin
-- For a desired 2D orientation, return the 3D orientation factor necessary to visually match it.
function project_2d_to_3d(o)
    local angle = orientation_to_angle(o)
    angle = atan2(sin(angle)*sqrt2, cos(angle))
    return math.fmod(angle_to_orientation(angle), 1)
end


-- Get converted angle of Wagon
local projectedOrientation = project_2d_to_3d(wagon.entity.draw_data.orientation)
local projectedOrientationAsAngle = orientation_to_angle(projectedOrientation)

-- Calculate frame index (based on 'animation' length)
local frameIndex = math.floor(projectedOrientation * 256)
-- Wrap frame index to always be within the 0-256 range
while(frameIndex < 0) do
frameIndex = 256 + frameIndex
end

while(frameIndex >= 256) do
frameIndex = frameIndex - 256
end

-- Set 'rotation' via the sprite sheet
wagon.animation.animation_offset = frameIndex

-- Grab current position/speed
local current_position = wagon.entity.draw_data.position
local speed = wagon.entity.speed

-- Calculate velocity vector from speed and orientation
local velocity_x = speed * math.cos(projectedOrientationAsAngle)
local velocity_y = speed * math.sin(projectedOrientationAsAngle)

-- Predict position
local predicted_position = {
x = current_position.x + velocity_x,
y = current_position.y - velocity_y
}

-- Draw at the predicted position
wagon.animation.target = predicted_position
Zeritor
Burner Inserter
Burner Inserter
Posts: 14
Joined: Thu Mar 24, 2016 12:04 pm
Contact:

Re: Anyway to modify/disable an individual Entity's (specifically a RollingStockPrototype) Pictures at runtime?

Post by Zeritor »

Right, I have it almost flawless apart from the very bad edge cases of extreme speed and tight angles. Even then you can't see it anymore without pausing/screenshotting.

I am bad at math and don't know how to apply it, so I have just done what I can and bodged in magic constants everywhere (that will probably vary by wagon size, texture size, etc). I had a few weird issues I couldn't explain so throwing in random rotation checks (always seemed to be when going from 0 -> 1 and wrapping).
09-22-2025, 03-04-44.png
09-22-2025, 03-04-44.png (1.14 MiB) Viewed 120 times
Up close you can see it's not perfect.

But in a video, it looks pretty good: https://streamable.com/a1zdqp

Code:

Code: Select all

local tau = 2 * math.pi -- ~6.2832
local tau_inv = 1 / tau -- ~0.1592
local sqrt2 = math.sqrt(2) -- ~1.4142
local sqrt2_inv = 1 / sqrt2 -- ~0.7071

function orientation_to_angle(orientation)
    return (0.25 - orientation) * tau
end

function angle_to_orientation(angle)
    return 0.25 - angle * tau_inv
end


local atan2, cos, sin = math.atan2, math.cos, math.sin
-- For a desired 2D orientation, return the 3D orientation factor necessary to visually match it.
function project_2d_to_3d(o)
    local angle = orientation_to_angle(o)
    angle = atan2(sin(angle)*sqrt2, cos(angle))
    return math.fmod(angle_to_orientation(angle), 1)
end


local check_locked_wagons = function(tick)
    -- Initialize rotation history storage if it doesn't exist
    if not storage.ftd.rotation_history then
        storage.ftd.rotation_history = {}
    end

    for _, locked_wagon in pairs(storage.ftd.locked_wagons) do
        if locked_wagon.entity and locked_wagon.entity.valid then
             -- Initialize rotation history for this wagon if it doesn't exist
            if not locked_wagon.cumulative_rotation then
                locked_wagon.cumulative_rotation = 0
                locked_wagon.last_orientation = locked_wagon.entity.draw_data.orientation
            end

            local current_angle = locked_wagon.entity.draw_data.orientation
            -- Calculate rotation change since last update
            local rotation_change = 0
            
            if locked_wagon.last_orientation then
                rotation_change = current_angle - locked_wagon.last_orientation
                
                -- Handle angle wrapping (crossing 0/1 boundary)
                if rotation_change > 0.5 then
                    rotation_change = rotation_change - 1
                elseif rotation_change < -0.5 then
                    rotation_change = rotation_change + 1
                end
            end

            -- Update cumulative rotation with decay and clamp to reasonable range
            locked_wagon.cumulative_rotation = locked_wagon.cumulative_rotation * 0.15 + rotation_change
            
            -- Update last orientation for next frame
            locked_wagon.last_orientation = current_angle

            -- Debug logging
            log("Current: " .. current_angle .. ", Cumulative: " .. locked_wagon.cumulative_rotation .. ", Change: " .. rotation_change)

            -- Get converted angle of Wagon using current orientation + prediction
            local predicted_orientation = current_angle + locked_wagon.cumulative_rotation
            
            -- Ensure predicted orientation is within valid range
            while predicted_orientation < 0 do
                predicted_orientation = predicted_orientation + 1
            end
            while predicted_orientation >= 1 do
                predicted_orientation = predicted_orientation - 1
            end
            local projectedOrientation = project_2d_to_3d(predicted_orientation)
            local projectedOrientationAsAngle = orientation_to_angle(projectedOrientation)
    
            -- Calculate frame index (based on 'animation' length)
            local frameIndex = math.floor(projectedOrientation * 256)
            -- Wrap frame index to always be within the 0-256 range
            while(frameIndex < 0) do
                frameIndex = 256 + frameIndex
            end

            while(frameIndex >= 256) do
                frameIndex = frameIndex - 256
            end

            -- Set 'rotation' via the sprite sheet
            locked_wagon.animation.animation_offset = frameIndex
            
            -- Grab current position/speed
            local current_position = locked_wagon.entity.draw_data.position
            local speed = locked_wagon.entity.speed
            
            -- Calculate velocity vector from speed and orientation
            local velocity_x = speed * math.cos(orientation_to_angle(project_2d_to_3d(current_angle)))
            local velocity_y = speed * math.sin(orientation_to_angle(project_2d_to_3d(current_angle)))
            -- Predict position
            local predicted_position = {
                x = current_position.x + (velocity_x * 1.1),
                y = current_position.y - (velocity_y * 0.85)
            }
            
            -- Draw at the predicted position
            locked_wagon.animation.target = predicted_position
        end
    end
end

return check_locked_wagons
I still need to tidy it as I very rapidly threw this together, but pretty happy now.
Post Reply

Return to “Modding help”