Skip to content

Components

Alex Smith edited this page Jul 11, 2024 · 77 revisions

Components in Photon 2 are Photon-specific scripted entities (sometimes called sENTS in Garry's Mod jargon). They are based on Garry's Mod's native scripted-entity framework, but with significant modifications to integrate them with Photon 2.

In Photon, components usually refer to lighting equipment such as light bars and grille lights, but they can be other objects like siren speakers. Despite outward differences, all components in Photon 2 operate on the same internal logic. Like other entities in Garry's Mod, the core of a component is a model (.mdl). Photon 2 creates a "wrapper" around this model to manipulate it (such as changing body groups or sub-materials), and allow it to display lights and/or sounds. Components are tied to a central Photon Controller entity, which is responsible for changing component activity and networking parameters between the client and server.

Overview

Every component requires four distinct sections: Templates, Elements, Segments, and Inputs. (There are more optional sections for advanced functionality which are covered below.)

Elements are executable actions. In most cases they simply define lights (such as a sprite, sub-material, 3D mesh, or projected texture), but are also used for bone rotation control, sound playing, and more. Every element requires a template, which determines what type of element it should be.

Templates define an element's type and are used to set shared properties that sibling elements will then inherit. It's generally equivalent to the Meta concept in Photon LE. For those familiar with web development, they are also comparable to creating a CSS class.

Segments are independent blocks within a component that require two things: frames and sequences. Frames are distinct collections of elements. Each element within a frame is then assigned to apply a specific "state" (usually a light color like red or blue) whenever the frame is activated. Sequences are then created by chaining the frames together. They can be go in any order and will play from start to end before jumping back to the start (unless configured otherwise, see below).

At any given time a segment has only one active sequence, and each sequence has only one active frame.

Inputs configure what sequence a segment should play whenever an input condition is met. Multiple sequences can be activated and layered together, but again, a segment can only play one sequence at a time. In the event of a conflict with two segments attempting to control the same light, an "order" system can also be utilized.

Creation

Component files must be placed in the components library folder like this:

garrysmod/addons/my_custom_addon/photon_v2/library/components/my_custom_component.lua

Basic Properties

-- On dedicated component files, the filename is used 
-- instead and this property is overridden.
COMPONENT.Name = "my_component_name"
COMPONENT.Title = "My Component Title"
COMPONENT.Category = "Lightbar"
COMPONENT.Credits = {
   Model = "Model author",
   Code = "Code Author"
}
COMPONENT.WorkshopRequirements = {
   -- [workshop_id] = "Applicable name of addon"
   [123456789] = "SGM's MX7000"
}

Categories

Component categories can be set to anything, but it's recommended you use Photon's standard categories as much as possible. Current categories include:

  • Lightbar External, roof-mounted lightbars
  • Interior Warning lights designed to be inside the vehicle
  • Compartment Internal vehicle lighting
  • Bumper Push bars
  • Perimeter Smaller, exterior-mounted lights
  • Spotlight User-directed illumination
  • Controller Control heads
  • Speaker Audible warning
  • Traffic Dedicated traffic advisors/arrow sticks
  • Exterior Dedicated external brake/turn/marker lights

Templates

Templates serve two important roles for Elements.

First, Templates are used to specify the type of Element for all elements that use it (e.g. 2D, Mesh, Projected, etc.)

COMPONENT.Template = {
   -- 2D light templates
   ["2D"] = {
      My2DLightTemplate = {}
   },
   -- Mesh light templates
   ["Mesh"] = {
      MyMeshLightTemplate = {}
   }
}

Secondly, the define shared properties that all its elements will adopt.

My2DLightTemplate = {
   Width    = 4.0,
   Height   = 2.0,
   Detail   = "sprites/light_detail",
   Shape    = "sprites/light_shape"
}

In this example, all elements using the My2DLightTemplate have the same size and light textures.

Elements

In most cases, an element is the same as a light. The reason they're called an "element" and not a "light" is because elements can also be sounds (used for sirens), model bones (for rotating lights), or even a model animation sequence (to play model animations).

Elements are always defined in the COMPONENT.Elements = {} table of a component file. They are identified by a numerical index and must be assigned a template so Photon knows what type of element it is.

COMPONENT.Elements = {
   -- The first string is always the template name
   [1] = { "My2DLightTemplate", Vector( 0, 0, 0 ), Angle( 0, 0, 0 ) }
}

The values after the element name string depend on the element's type. For element types that need positioning data, this will be a vector position and angle. However, these values are not required. They are merely shortcuts for adjusting the most commonly manipulated properties to reduce the verbosity of the code.

Advanced information about Elements...

When interpreted by Lua, the above example is actually identical to the following:

COMPONENT.Elements = {
   [1] = { 
            [1] = "My2DLightTemplate", 
            [2] = Vector( 0, 0, 0 ), 
            [3] = Angle( 0, 0, 0 ) 
   }
}

Which, without shortcutting, is equivalent to:

COMPONENT.Elements = {
   [1] = {  
            [1]      = "My2DLightTemplate",
            Position = Vector( 0, 0, 0 ), 
            Angle    = Angle( 0, 0, 0 ) 
   }
}

Internally, element [1] is simply a 2D Light element that inherits from My2DLightTemplate. That means any properties of 2D Light elements can be set within the [1] table, which can even override properties set by My2DLightTemplate when desired:

COMPONENT.Elements = {
   [1] = {  
            [1]      = "My2DLightTemplate",
            Position = Vector( 0, 0, 0 ), 
            Angle    = Angle( 0, 0, 0 ),
            Width    = 6, -- Overrides template value of 4
            Height   = 4, -- Overrides template value of 2
            Scale    = 2 -- Newly declared property set for just this element
   }
}

But to keep elements visually more compact in the code, this can be simplified to one line and use shortcutting:

COMPONENT.Elements = {
   [1] = { "My2DLightTemplate", Vector( 0, 0, 0 ), Angle( 0, 0, 0 ), Width = 6, Height = 4, Scale = 2 }
}

For even more detailed information on Element types and their properties, check out the dedicated Elements page.

Element States

Abstractly speaking, states represent the transient value of an element. In other words, they are the "dynamic" aspects of an Element, and are expected to change both frequently and rapidly.

In the broadest of analogies, "on" and "off" can be considered different states. For lights, "red" and "blue" are states. For sounds, actual noises like "wail" or "yelp" are states. The precise effects and scope of a state varies by the type of Element you're working with.

Common & Default States

Light elements in Photon come with a common set of states to represent different colors, usually named single letters for brevity. R is typically "red," B is typically "blue," A is typically amber, etc.

Automatic Intensity/Transition States

Instead of needing to manually create colors of different intensities and transition speeds every time, Photon 2's frame string syntax supports directly specifying these common attributes in the frame itself.

COMPONENT.Segments = {
   MySegment = {
      Frames = {
         -- State based B/R that has intensity transitions,
         -- a gain factor of 0.5, a loss factor of 1, and an intensity of 0.5
         [1] = "[~(0.5,1)B*0.5] 2 4 6 8",
         [2] = "[~(0.5,1)R*0.5] 1 3 5 7",
         -- State based on B/R with intensity transitions enabled
         [3] = "[~B] 2 4",
         [4] = "[~R] 1 3",
         -- State based on B/R with an intensity of 0.8
         [5] = "[B*0.8] 2 4",
         [6] = "[R*0.8] 1 3",
         -- State based on B/R with intensity transitions a gain/loss factor of 2
         [7] = "[~B(2)] 6 8",
         [8] = "[~R(2)] 5 7"
      },
      Sequences = {
         ["FADE_ALT"] = { 1, 2 }
      }
   }
}
  • ~ Prefixed at the beginning marks the color with IntensityTransitions = true
  • (0.5,1) after the ~ denotes IntensityGainFactor = 0.5 and IntensityLossFactor = 1
  • (2) after ~ denotes IntensityGainFactor = 2 and IntensityLossFactor = 2
  • No () after the ~ means the gain and loss factor will be derived from the base color/state or light itself
  • B Is the original color/state name
  • *0.9 Is the target intensity of the state

Important

When manually defining a state, the special syntax described above cannot be used with the Inherit = parameter.

Custom States

How to create custom element states... If you want to create a new light color that isn't supported by default, or if you want to tweak the implementations of a default light color, you need to create a custom state (same for non-light elements).

Custom states can be defined in two different ways in a component file. The first is within Element templates:

COMPONENT.Templates = {
   ["Mesh"] = {
      MyMeshLightTemplate = {
         States = {
            PINK = {
               DrawColor = PhotonColor( 255, 128, 255 ),
               BloomColor = PhotonColor( 255, 0, 255 )
            }
         }
      }
   }
}

In this example, the custom state PINK is declared and made available for any elements that use the MyMeshLightTemplate.

COMPONENT.Elements = {
   -- Element [1] has access to PINK because it uses MyMeshLightTemplate
   [1] = { "MyMeshLightTemplate", Vector( 0, 0, 0 ), Angle( 0, 0, 0 ) }
   -- Element [2] does NOT have access to the PINK as it uses a different template
   [2] = { "My_OTHER_MeshLightTemplate", Vector( 0, 0, 0 ), Angle( 0, 0, 0 ) }
}

If you want all elements of the same type (e.g. Mesh, 2D, etc.) to have access to a specific state in the component, you can declare them in a different COMPONENT.ElementStates table like this:

COMPONENT.ElementStates = {
   -- Element type is required to act as a parent table, just like with Templates
   ["Mesh"] = {
      PINK = {
         DrawColor = PhotonColor( 255, 128, 255 ),
         BloomColor = PhotonColor( 255, 0, 255 )
      }
   }
}

-- Both Elements 1 and 2 can use PINK because they are both Mesh elements
COMPONENT.Elements = {
   [1] = { "MyMeshLightTemplate", Vector( 0, 0, 0 ), Angle( 0, 0, 0 ) }
   [2] = { "My_OTHER_MeshLightTemplate", Vector( 0, 0, 0 ), Angle( 0, 0, 0 ) }
}

Inheritance

Element states also support inheritance.

COMPONENT.Templates = {
   ["Mesh"] = {
      MyMeshLightTemplate = {
         States = {
            ["~R"] = {
               -- This state inherits the default red colors
               Inherit = "R",
               -- Intensity transitions are selectively enabled
               -- (which enables light fading effects)
               IntensityTransitions = true
            }
         }
      }
   }
}

Inheritance follows the same scoping as custom states. A custom state in one template cannot inherit from a custom state in a different sibling template. However, a custom state defined in COMPONENT.ElementStates can be inherited by any template (of the same type) in the component.

Element Groups

Element Groups allow you to create a group of related elements and refer to them collectively by using a readable name when creating frames, defining StateMaps, etc.

COMPONENT.ElementGroups = {
   -- Groups can be created for any purpose
   GroupA = { 1, 2, 3 },
   GroupB = { 4, 5, 6 },
   -- The same element can be included in multiple groups
   Odd    = { 1, 3, 5 },
   Even   = { 2, 4, 6 }
   -- You can also use them for a single element to give it a readable name
   LightA = { 1 },
   LightB = { 2 }
}

In frames and state maps, Photon 2 will always treat numbers as specific elements and alphabetical strings as light groups.

-- Makes W the default state of 1, 3, and 5. Makes A the default state of 2, 4, and 6.
COMPONENT.StateMap = "[W] Odd [A] Even"

COMPONENT.Segments = {
   MySegment = {
      Frames = {
         -- Assigns R to elements 1, 2, and 3.
         [1] = "GroupA:R",
         -- Assigns R to elements 1, 3, and 5. Then assigns B to elements 2, 4, and 6.
         [2] = "Odd:R Even:B"
         -- Assigns R to elements 3 and 5. Element 1 is assigned B because it is to the right of Odd.
         [3] = "Odd:R 1:B"
         -- Assigns element 1 to G
         [4] = "LightA:G"
         -- Identical to the frame above
         [5] = "1:G"
      },
      Sequences = { ... }
   }
}

Segments

Segments are arbitrary sub-divisions of a component. They consist of Frames and Sequences, which are entirely independent of other segments. They can be utilized to separate component functions or small patterns, then be mixed with other segments that layer together.

COMPONENT.Segments = {
    MySegment = {
        -- Optional property, changes the "flash" speed of all sequences
        FrameDuration = 1/24,
        -- Optional property
        Off
        Frames = {
            [1] = "1:R",
            [2] = "2:B"
        },
        Sequences = {
            Alternate = { 1, 2 }
        }
    }
}

While segments can define multiple sequences, only one sequence can be active at any given time within a segment. If multiple inputs are active at once, the input with the highest priority is the one the segment will respond to.

A common real-world example of this concept can be seen in some vehicle tail lights. Sometimes, one lamp is used for both a brake light and a turn signal. When the brake is pressed, the light shines bright red. But, when the turn signal is activated, the same lamp begins to flash on and off, "overriding" the brake signal. In Photon 2 terms, this is because turn signal has a "higher priority" than the brake light. More information on this is covered in Input Priorities.

Frames

Frames map elements to states, which are then applied whenever the frame becomes active in a sequence.

Frames = {
    -- Automatically apply state slot #1
    [1] = "1 2",
    -- Assign each element individually: 1 to R, 2 to B
    [2] = "1:R 2:B",
    -- Assign W to all elements to the right -- 1 and 2
    [3] = "[W] 1 2",
    -- Assigns R to 1 and 3, then B to 2 and 4
    [4] = "[R] 1 3 [B] 2 4",
    -- Assign state slot #2 to each light
    [5] = "[2] 1 2",
    -- Assign 1 to R and 2 to B using verbose table syntax
    [6] = { { 1, "R" }, { 2, "B" } }
}

Frames support a very basic script-like syntax that allows them to be composed using a string. It is designed to easily group together related lights and quickly change their state assignments. It is also tightly integrated with the component's state slot functionality, which allows users to customize states (usually light colors) when configuring a component to a vehicle/profile.

Zero Frame

Photon utilizes (and/or generates) the [0] index of frame tables to determine the inactive state of every element utilized in the segment.

Consider a basic segment that defines two frames, one that assigns element [1] to red and element [2] to blue:

Frames = {
   [1] = "1:R",
   [2] = "2:B"
}

Photon will automatically generate a zero frame based on each light used in this segment and assign it inactive value of OFF. It will then use the zero frame to add undefined element states to this inactive state. The result is equivalent to the following:

Frames = {
   -- Automatically generated when the component compiles
   [0] = "[OFF] 1 2",
   -- 2:OFF is added to frame [1] because element 2 is not explicitly set
   [1] = "1:R 2:OFF",
   -- 1:OFF is added to frame [2] because element 1 is not explicitly set
   [2] = "2:B 1:OFF"
}

(Note that this change happens behind the scenes and does not manipulate your actual code.)

This is done because OFF is actually the same as any other light state, but the omission of an element in a frame is implied. For this reason, it also recommended to keep Segments small and task specific. If a segment is in control of all the lights on a lightbar, it will end up forcing other lights off that you may otherwise be trying to use in a different segment.

Advanced Properties

Additional, optional properties for uncommon or advanced use-cases are also available.

COMPONENT.Segments = {
   MyAdvancedSegment = {
      -- Sets a custom "off" state for the segment (default is "OFF")
      -- (in this example, it will cause lights to fade out)
      Off = "~OFF", 
      -- Overrides the default frame duration (default is 1/24th of a second)
      FrameDuration = 1/10,
      Frames = {
         [1] = "1 2 3 4"
      },
      Sequences = {
         ["ON"] = { 1 }
      }
   }
}

Patterns

Patterns are groups of related sequences that can be referenced together using one shared name. This can be utilized to simply input assignments (more information can be found in the (Inputs)[#inputs] section).

COMPONENT.Patterns = {
   Pattern1 = {
      { "FrontSegment", "SEQUENCE_1" },
      { "RearSegment", "SEQUENCE_2" }
   },
   Pattern2 = {
      { "FrontSegment", "SEQUENCE_A" },
      { "RearSegment", "SEQUENCE_B" }
   }
}
See full example...
COMPONENT.Segments = {
   FrontSegment = {
      Frames = {
         [1] = "1 2"
      },
      Sequences = {
         ["SEQUENCE_1"] = { 1 },
         ["SEQUENCE_A"] = { 1, 0 }
      }
   },
   RearSegment = {
      Frames = {
         [1] = "3 4"
      },
      Sequences = {
         ["SEQUENCE_2"] = { 1 },
         ["SEQUENCE_B"] = { 0, 1 }
      }
   }
}

COMPONENT.Patterns = {
   Pattern1 = {
      { "FrontSegment", "SEQUENCE_1" },
      { "RearSegment", "SEQUENCE_2" }
   },
   Pattern2 = {
      { "FrontSegment", "SEQUENCE_A" },
      { "RearSegment", "SEQUENCE_B" }
   }
}

COMPONENT.Inputs = {
   ["Emergency.Warning"] = {
      ["MODE1"] = "Pattern1",
      ["MODE2"] = "Pattern2"
   }
}

Inputs

Inputs define what sequence a segment should play when a channel is set to a specific mode.

COMPONENT.Inputs = {
   ["Emergency.Warning"] = {
      -- Assign segment to play a sequence
      ["MODE1"] = {
         SegmentName = "SEQUENCE_NAME",
      },
      -- Use a pre-defined pattern from COMPONENT.Patterns
      ["MODE2"] = "Pattern1",
      -- Use a pattern AND manually assign a segment
      -- to play a sequence
      ["MODE3"] = {
         "Pattern2",
         SegmentName = "SEQUENCE"
      }
   }
}

In the example above, Emergency.Warning is the channel, MODE1, MODE2, and MODE3 are modes.

Using Patterns

Patterns are a convenience feature. They allow groups of sequences designed to work together to be referenced and swapped around using a single name. This is an alternative for manually writing Segment = "SEQUENCE" for each portion of a larger, multi-part sequence.

COMPONENT.Patterns = {
   -- Define a pattern called MyPattern
   MyPattern = {
      -- SegmentA plays Sequence1
      { "SegmentA", "Sequence1" },
      -- SegmentB plays Sequence2
      { "SegmentB", "Sequence2" }
   }
}

COMPONENT.Inputs = {
   ["Emergency.Warning"] = {
      -- Patterns can significantly simplify the Inputs table
      ["MODE1"] = "MyPattern",
      -- The result is identical to this
      ["MODE2"] = {
         ["SegmentA"] = "Sequence1",
         ["SegmentB"] = "Sequence2"
      }
   }
}

When should I use patterns?

Consider setting up COMPONENT.Patterns whenever your component offers different flash patterns, or you have a multi-part sequence you wish to reuse for multiple modes.

Ordering

Segment ordering allows you to manually set what segments have light control priority over others in the same mode (siblings).

COMPONENT.Inputs = {
    ["Emergency.Warning"] = {
        ["MODE3"] = {
            -- If left unset, Base will be automatically assigned an order number (which is unknown)
            Base = "RED_BLUE",
            -- White will always have priority over base because its order number is higher
            White = { "WHITE", Order = 100 },
            -- Ordering can also be used on patterns
            { "MyPatternName", Order = 90 }
        }
    }
}

You may sometimes encounter an issue where two or more segments are in conflict over the same light. Consider the following example:

COMPONENT.Segments = {
    Base = {
        Frames = {
            -- Element 1 is set to red, element 2 is set to blue
            [1] = "[R] 1 [B] 2"
        },
        Sequences = {
            ["RED_BLUE"] = { 1 }
        }
    },
    White = {
        Frames = {
            -- Element 2 is set to white
            [1] = "[W] 2"
        },
        Sequences = {
            ["WHITE"] = { 1 }
        }
    },
}

COMPONENT.Inputs = {
    ["Emergency.Warning"] = {
        ["MODE3"] = {
            Base = "RED_BLUE",
            White = "WHITE"
        }
    }
}

Note that segment Base is setting element [1] to red and element [2] to blue from the "RED_BLUE" sequence. Now, notice that segment White is setting element [2] to white with the "WHITE" sequence.

This creates a conflict, as both segments are now attempting to control the state of element [2]. If executed as written, the resulting state of element [2] will vary inconsistently.

Despite Base being placed above White in the file, this order is not preserved by Lua internally. Therefore, it is necessary to manually define the order, which can be done by replacing the sequence string with a table using the following format:

Segment = { "SEQUENCE_NAME", Order = # }`

In this case, setting the White segment to always override Base would look like this:

COMPONENT.Inputs = {
    ["Emergency.Warning"] = {
        ["MODE3"] = {
            Base = "RED_BLUE",
            -- Both Base and White are attempting to control the state of element [2]
            White = { "WHITE", Order = 100 }
        }
    }
}

The state of element [2] will now always be white when the "Emergency.Warning" -> MODE3 mode is active in this example.

Higher numbers override lower numbers, and the order number must be higher than the total number of segments to behave properly.

Virtual Outputs

Virtual outputs generate new channels whose modes are determined by the current modes of other channels.

COMPONENT.VirtualOutputs = {
    -- Any unique channel name can be used
    ["Virtual.Warning+Brake"] = {
        {
            -- This is the mode that will be outputted
            Mode = "BRAKE",
            -- Required mode combinations to activate this mode
            Conditions = {
                -- Emergency.Warning must be "MODE1" OR "MODE2"
                ["Emergency.Warning"] = { "MODE1", "MODE2" },
                -- AND Vehicle.Brake must be "ON"
                ["Vehicle.Brake"] = { "ON" }
            }
        }
    }
}

-- Virtual Outputs need to be assigned an input priority
COMPONENT.InputPriorities = {
    ["Virtual.Warning+Brake"] = 60
}

-- Virtual Output then treated like any other channel/mode input
COMPONENT.Inputs = {
   ["Virtual.Warning+Brake"] = {
      BRAKE = {
         Segment = "SEQUENCE"
      }
   }
}

Input Priorities

Input Priorities rank the priority of different input channels. Channels with a higher priority number override channels with a lower one.

COMPONENT.InputPriorities = {
   -- Makes emergency lights always override directional patterns
   ["Emergency.Warning"] = 81,
   ["Emergency.Directional"] = 80,
}

Additional information (including default values) is covered on the Inputs page here.

Phasing

Phasing allows for components to use alternating or different variants of the same pattern. Phasing in Photon 2 is functionally very similar to Photon LE, but supports falling back to non-phased patterns if a sequence isn't configured for a specified phase.

Automatic Degree-Based Phasing

phase-demo-small

Photon 2 supports automatic phasing using a degree-based offset. This type of phasing, which is how most real-world phasing is done, envisions flash patterns as a circular sequence, starting at the top (0 or 360 degrees) and rotating clockwise all of the way around. By adjusting this angular offset, you can move where a sequence should begin or end.

Beware that degree-based phasing is a relatively new feature and is still undergoing refinement.

-- Can be used with Inputs
COMPONENT.Inputs = {
   ["Emergency.Warning"] = {
      ["MODE1"] = {
         -- on individual segments
         Left = "FLASH:270",
         Right = "FLASH:90"
      },
      -- or on an entire pattern
      ["MODE2"] = "PHASED_PATTERN:180"
   }
}

-- as well as inside Patterns
COMPONENT.Patterns = {
   ["PHASED_PATTERN"] = {
      { "Left", "FLASH:0" },
      { "Right", "FLASH:180" }
   }
}

Most real-world phasing uses 45-degree increments. As 360/45 = 8, this means many sequences are divided into eight steps, or frames. Each 45-degree offset would then shift the sequence by one frame. This simplest sequence would be one with eight frames, beginning with four frames of ON (1), followed by four frames of OFF (0).

-- Original sequence, on for 4 and off for 4
["Sequence"] = { 1, 1, 1, 1, 0, 0, 0, 0 }

Notice how each phase shift adjusts this sequence:

-- Add 45
["Sequence:45"]  = { 0, 1, 1, 1, 1, 0, 0, 0 },
-- Add 45
["Sequence:90"]  = { 0, 0, 1, 1, 1, 1, 0, 0 },
-- Add 45
["Sequence:135"] = { 0, 0, 0, 1, 1, 1, 1, 0 },
-- Add 45, notice at 180 that the sequence is opposite to phase 0
["Sequence:180"] = { 0, 0, 0, 0, 1, 1, 1, 1 }

In most cases where an even back-and-forth pattern is desired, one side should use a phase of 0 while the other uses a phase of 180.

Legacy Phasing

The term "legacy phasing" refers to the manual creation of phases, which usually use names like "A" or "B." For users experienced with Photon LE (v1), this will be familiar.

To create phased sequences, append the sequence name with a colon and then the phase identifier:

COMPONENT.Segments = {
   MySegment = {
      Frames = {
         [1] = "1 2",
         [2] = "3 4",
         [3] = "1 2 3 4"
      },
      Sequences = {
         -- Normal, non-phased sequence
         ["PATTERN"] = { 3 },
         -- Sequence for phase A
         ["PATTERN:A"] = { 1, 1, 0, 0 },
         -- Sequence for phase B
         ["PATTERN:B"] = { 0, 0, 2, 2 },
      }
   }
}

Components can be configured to use a phase via the Phase property in an equipment entry:

Components = {
   {
      Component = "my_component",
      Position = Vector( 0, 0, 0 ),
      Angle = Angles( 0, 0, 0 ),
      Phase = "A"
   }
}

Tip

If you find that a given phase has no effect, it is because the component's sequences are not configured to accept it. This is either because the component is incomplete, not designed for legacy phasing, or is specifically intended to use degree-based phasing. In the latter case, you would typically use Phase = 0 instead of Phase = "A" and then Phase = 180 instead of Phase = "B".

State Mapping

State mapping is a new way to configure element states and light colors. Instead needing to explicitly define an element state or light color for each individual element in each frame, state mapping allows for default colors/states to be preset and automatically applied when an element is used in a frame.

Basic state assignment looks like this:

COMPONENT.StateMap = "[R] 1 3 5 7 9 [B] 2 4 6 8 10 [W] 11 12"

Like frames, Element Groups can be used as well:

COMPONENT.StateMap = "[R] ODD [B] EVEN [W] ILLUM"

By default, these assignments are placed in the first slot of each defined element, where they can be referenced numerically. From the example above slot [1] for element #1 is R, and slot [1] for element #2 is B. When using these elements in a frame, the value of [1] is implicitly used as the assigned state.

-- Each frame in this example has an identical effect.
Frames = {
   -- Using implicit state assignment
   [1] = "1 3 5 7 9 2 4 6 8 10",
   -- Explicitly using state slot [1]
   [2] = "[1] 1 3 5 7 9 2 4 6 8 10",
   -- Manually declaring the state instead of using slots
   [3] = "[R] 1 3 5 7 9 [B] 2 4 6 8 10",
}

Using Multiple States

Elements can also be configured to use multiple preset states by adding a / like [STATE_1/STATE_2/STATE_3]:

COMPONENT.StateMap = "[R/W] 1 3 5 7 9 [B/W] 2 4 6 8 10"

In the example above, the odd-numbered lights are mapped 1=R, 2=W, and the even numbered lights are mapped 1=B, 2=W.

This state can then be referenced in frames by using the corresponding slot, like [1], [2], [3], etc:

Frames = {
   -- Using slot 1:
   [1] = "[1] ODD [1] EVEN", -- Result: ODD is R, EVEN is B
   -- Using slot 2:
   [2] = "[2] ODD [2] EVEN", -- Result: ODD is W, EVEN is W
   -- Using a mix:
   [3] = "[1] ODD [2] EVEN" -- Result: ODD is R, EVEN is W

   -- Using implicit state assignment:
   [4] = "ODD EVEN", -- Result: ODD is R, EVEN is B
   -- Mixed:
   [5] = "ODD [2] EVEN" -- Result: ODD is R, EVEN is W
}

Component State Slots

State mapping slots allow users to easily change the primary colors/states of a component, and are similar to the Color1, Color2 pattern from Photon LE.

Components accept numeric state slots in the COMPONENT.States table:

COMPONENT.States = {
	[1] = "R",
	[2] = "B",
	[3] = "W"
}

The order of these state slots can be rearranged using the StateMap with numeric states:

COMPONENT.StateMap = "[1/2/3] 1 3 5 7 [2/1/3] 2 4 6 8"

Inheritance

One of the most significant changes in Photon 2 is the addition of inheritance support. In simple terms, inheritance allows one object to copy and modify any of the attributes of a parent object. This eliminates the need for duplicating code and allows changes to a parent to automatically affect all of its children.

In practical Photon terms, this allows you to easily change flash patterns (sequences), inputs, materials, and more while using the same elements and templates already defined in a parent component. For example, a generic Vision SLR component can be inherited by specific LVMPD and NYPD variants.

To inherit from a component, simply add COMPONENT.Base = "parent_component" into the component file.

Options

Options are custom Lua functions that can perform complex changes to a component based on simple user-configured parameters. Option actions are executed during the final stage of component "compilation." That means all configuration data (including inherited values) can be read and manipulated, but the component is not a spawned entity.

Warning

This is experimental. Use caution when incorporating this feature yourself. The implementation or functionality may change unexpectedly.

Defining

Options are defined in a COMPONENT.DefineOptions table.

COMPONENT.DefineOptions = {
   -- This example option takes a single number to change the lightbar's feet height and calculates the corresponding bone position
   Feet = {
      -- Annotations of the function arguments (for possible validation and UI integration later)
      Arguments = { [1] = { "amount", "number" } },
      -- What the option does
      Description = "Adjusts the feet extension height.",
      -- The action performed, which will probably be a function but macros may come later
      ---@param self PhotonLightingComponent Component data (always supplied first).
      ---@param height number Custom argument.
      Action = function(self, height)
         local scale = self.Scale or 1
         self.Bones = self.Bones or {}
         self.Bones["valor_44_feet_left"] = { Vector(height / -4.2857, 0, height) * scale, Angle(0, 0, 0), 1 }
         self.Bones["valor_44_feet_right"] = { Vector(height / 4.2857, 0, height) * scale, Angle(0, 0, 0), 1 }
      end
   }
}

Note

Options cannot be defined in an equipment entry in a vehicle file.

Viewing

Available component options can be found using the Component Browser. If a component supports options, an "Options" node will appear on the inspector tree.

image

Under each option the number, name and type of argument is displayed.

Usage

Options can be assigned values using a COMPONENT.Options table.

Options = {
   -- Option with a single argument
   Feet = 0.5,
   -- Multiple arguments
   Lenses = { "R", "B" }
}

Features

Warning

This is experimental. Use caution when incorporating this feature yourself. The implementation or functionality may change unexpectedly.

Features can be used to inject or add specific component behaviors that would otherwise be repetitive or tedious to configure manually. Features are currently a hard-coded piece of the component compilation process (meaning custom/third-party features aren't supported), but this may change.

COMPONENT.Features = {
   AutomaticHeadlights = true
}

Available Features

AutomaticHeadlights

Sets up automatic headlights and daytime running lights behavior.

AutomaticHeadlights = true,

ParkMode

Sets up park mode whenever the vehicle is parked and Emergency.Warning is MODE3.

ParkMode = { "Emergency.Warning", "MODE2" },

NightParkMode

Sets up park mode whenever the vehicle is parked, Emergency.Warning is MODE3, and the environment is dark.

FrontNoM1

Treats the component as a forward-facing light that should be disabled on when Emergency.Warning is MODE1 and should cutout when Emergency.Cut is FRONT.

FrontNoM1 = true

Advanced Properties

These are component properties used in very rare or special circumstances.

Deprecated

Experimental. Component files can use a COMPONENT.Deprecated table to mark the component as being unsupported and replaced by a newer version. This discourages new use while allowing the component to exist and not break vehicle profiles that utilize it.

The table can contain information explaining why the component is deprecated and what component should now be used instead. Photon will (eventually) utilize this to prevent the component from being discovered in the component browser.

COMPONENT.Deprecated = {
   -- Why the component is deprecated
   Description = "Model was updated with different materials and body groups.",
   -- What component should now be used instead
   Use = "new_component_name"
}

Troubleshooting

Not Loading or Not Found

If you're creating a component and are unable to find or use it on a vehicle, check the steps below.


1. Verify correct file path


Components must be placed in a specific directory path. Additionally, the filename must be unique and cannot use spaces, capital letters, or other special characters. A valid component file path will look like this:

garrysmod/addons/your_addon_folder/lua/photon-v2/library/components/your_component_file.lua

A single typo or mistake in the file path will prevent the file from being discovered and loaded. This is the most common issue when first creating a component. Do not overlook this step. Check each folder name very carefully, and then check it again.


2. Verify component name


While less common of an issue, ensure your vehicle file is using the correct component name. Unless you are defining multiple components in a single .lua file (advanced user scenario), then the component must exactly match the filename (minus the .lua). For example, if your component file is named schmals_whelen_ion.lua, then the component name in your code must be schmals_whelen_ion.

Again, do not overlook this step. A single typo or mistake in the component name will cause a "component not found" error to occur whenever the vehicle file is saved.


3. Check for errors


Lua syntax errors or significant issues in the component code structure can cause Photon 2 to attempt and then abort loading a component file. Whenever this occurs, it is noted in the log files. Please visit the Logging & Errors section for more information.