View Issue Details

IDProjectCategoryView StatusLast Update
0005050The Dark ModScript/Defpublic10.07.2022 07:31
ReporterDragofer Assigned To 
PrioritynormalSeveritynormalReproducibilityalways
Status newResolutionopen 
Product VersionTDM 2.07 
Summary0005050: Electric lights don't toggle hide/show of targeted entities
DescriptionSwitching an electric light on or off will not toggle targeted entities between hide/show. An example usage case would be to let rays of moonlight appear when the light is switched off. To my knowledge this only works with lamps that have def_attached light entities, where the def_attached light entity targets the hidden entity via for instance "set target0 on flame".
Steps To ReproduceFind attached a small test map with an electric light and a lantern. Toggling the lantern will toggle the hidden lit version of the window. However, flipping the switch of the electric light will have no effect on the window.
TagsFix
Attached Files
toggle_hide_show.map.txt (6,159 bytes)   
Version 2
// entity 0
{
"classname" "worldspawn"
"editor_drMapPos1" "145.117 609.786 279.045"
"editor_drMapAngle1" "-48 150.8 0"
"editor_drMapPos2" "414.798 255.441 -264.933"
"editor_drMapAngle2" "2.10002 84.5976 0"
"editor_drMapPos3" "-749.97 -1606.21 -470.066"
"editor_drMapAngle3" "-3.29998 6.59757 0"
"editor_drMapPos4" "1014.05 -953.679 -556.857"
"editor_drMapAngle4" "-10.2 355.498 0"
"editor_drMapPos5" "987.599 3513.79 -319.877"
"editor_drMapAngle5" "-61.1999 172.198 0"
"editor_drMapPos6" "-1032.5 -2840 739.5"
"editor_drMapAngle6" "-77.0999 175.798 0"
"editor_drMapPos7" "952.822 285.005 1366.04"
"editor_drMapAngle7" "-74.8 149.373 0"
"editor_drMapPos8" "976 424 320"
"editor_drMapAngle8" "-3 15.3467 0"
"editor_drMapPos9" "409.146 547.464 392.83"
"editor_drMapAngle9" "-16.8 284.947 0"
"editor_drLastCameraPos" "29.5975 -22.8642 72.2517"
"editor_drLastCameraAngle" "-32.1 168.9 0"
// primitive 0
{
brushDef3
{
( 0 0 1 -144 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 1 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 1 0 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 -1 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( -1 0 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 0 -1 128 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/darkmod/wood/boards/pier_platform" 0 0 0
}
}
// primitive 1
{
brushDef3
{
( 0 0 1 -128 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 1 0 -80 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 1 0 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 0 -1 0 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( -1 0 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 -1 0 64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/darkmod/wood/boards/pier_platform" 0 0 0
}
}
// primitive 2
{
brushDef3
{
( 0 0 1 -128 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 1 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 1 0 0 -80 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 0 -1 0 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 -1 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( -1 0 0 64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/darkmod/wood/boards/pier_platform" 0 0 0
}
}
// primitive 3
{
brushDef3
{
( 0 1 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 1 0 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 0 -1 -16 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 -1 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( -1 0 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 0 1 0 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/darkmod/wood/boards/pier_platform" 0 0 0
}
}
// primitive 4
{
brushDef3
{
( 0 0 1 -128 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 1 0 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 0 -1 0 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 -1 0 -80 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( -1 0 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 1 0 64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/darkmod/wood/boards/pier_platform" 0 0 0
}
}
// primitive 5
{
brushDef3
{
( 0 0 1 -128 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 1 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 0 -1 0 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 0 -1 0 -64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( -1 0 0 -80 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" 0 0 0
( 1 0 0 64 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/darkmod/wood/boards/pier_platform" 0 0 0
}
}
// primitive 6
{
patchDef2
{
"textures/darkmod/window/diamond_pattern02/diamond_pattern02"
( 3 3 0 0 0 )
(
( ( 16 54 24 0 0 ) ( 16 54 48 0 0.5 ) ( 16 54 72 0 1 ) )
( ( 32 54 24 0.5 0 ) ( 32 54 48 0.5 0.5 ) ( 32 54 72 0.5 1 ) )
( ( 48 54 24 1 0 ) ( 48 54 48 1 0.5 ) ( 48 54 72 1 1 ) )
)
}
}
}
// entity 1
{
"classname" "atdm:wall_sphere_lit"
"name" "atdm_wall_sphere_lit_1"
"origin" "-40 48 32"
"light_center" "0 0 28"
"light_radius" "180 180 180"
"model" "models/darkmod/lights/non-extinguishable/lamp_wall_sphere.ase"
"s_shader" "light_flicker_104"
"target0" "func_static_1"
}
// entity 2
{
"classname" "info_player_start"
"name" "info_player_start_1"
"origin" "-8 -48 0"
"angle" "90.000000"
}
// entity 3
{
"classname" "atdm:switch_flip"
"name" "atdm_switch_flip_1"
"origin" "-8 0 32"
"rotation" "1 0 0 0 1 0 0 0 1"
"rotate" "0 0 45"
"target0" "atdm_wall_sphere_lit_1"
"model" "models/darkmod/mechanical/switches/switch_rotate_lever.ase"
"frob_distance" "80"
"frob_box_size" "80"
}
// entity 4
{
"classname" "func_static"
"name" "func_static_1"
"model" "func_static_1"
"origin" "32 52 48"
"hide" "1"
// primitive 0
{
patchDef2
{
"textures/darkmod/window/diamond_pattern02/diamond_pattern02_brightlit"
( 3 3 0 0 0 )
(
( ( 16 52 24 0 0 ) ( 16 52 48 0 0.5 ) ( 16 52 72 0 1 ) )
( ( 32 52 24 0.5 0 ) ( 32 52 48 0.5 0.5 ) ( 32 52 72 0.5 1 ) )
( ( 48 52 24 1 0 ) ( 48 52 48 1 0.5 ) ( 48 52 72 1 1 ) )
)
}
}
}
// entity 5
{
"classname" "atdm:moveable_lantern_oil_hand02_lit"
"name" "atdm_moveable_lantern_oil_hand02_lit_1"
"origin" "-56 -16 12"
"rotation" "1 0 0 0 1 0 0 0 1"
"set target0 on light" "func_static_1"
}
// entity 6
{
"classname" "light"
"name" "ambient_world"
"origin" "56 -56 120"
"light_center" "0 0 0"
"light_radius" "320 320 320"
"_color" "0.1 0.1 0.1"
"texture" "lights/ambientlightnfo"
}
toggle_hide_show.map.txt (6,159 bytes)   

Relationships

related to 0002564 new add "toggleTargets" to tdm_light_holder script object 

Activities

VanishedOne

VanishedOne

08.09.2019 18:06

reporter   ~0011837

Last edited: 08.09.2019 18:26

I see it's only the electric light that generates these console errors:

WARNING:script/tdm_light_holders.script(212): Thread 'tdm_light_holder::LightsT
oggleResponse': Function 'Close' not supported on entity 'func_static_1'
WARNING:script/tdm_light_holders.script(213): Thread 'tdm_light_holder::LightsT
oggleResponse': Function 'Off' not supported on entity 'func_static_1'
WARNING:script/tdm_light_holders.script(173): Thread 'tdm_light_holder::LightsT
oggleResponse': Function 'Open' not supported on entity 'func_static_1'
WARNING:script/tdm_light_holders.script(174): Thread 'tdm_light_holder::LightsT
oggleResponse': Function 'On' not supported on entity 'func_static_1'

Edit: at the moment my guess is that tdm_light_holder is intercepting STIM_TRIGGER, and tries to do various things to the target but not simply activate it.

Dragofer

Dragofer

08.09.2019 18:13

developer   ~0011838

That might have to do with what I saw when I targeted doors from lamps: the doors would open/close when the lamps are lit/extinguished. Only wanted to swap the window skins on the doors.
VanishedOne

VanishedOne

08.09.2019 18:33

reporter   ~0011839

Apparently calling Open/Close was added in 0002676.

That leads me to http://forums.thedarkmod.com/index.php?/topic/12418-beleaguered-fence-broken-due-to-tdm-change/ -- 'So it would seem like extinguishable light targetting something else do not work anymore. Looks like a tdm change has broken something and most likely this applies to all maps with extinguishable lights targeting something.'

'The reason was the removal of the activateTargets( self ); call. I see that there were problems with that, but I guess setting up a proxy script function should do the trick?'

'The problem with "activateTargets()" is that it is different from On/Off, it always toggles, that is not what the light holder entities are supposed to do, they are supposed to turn things on/off in sync.'
VanishedOne

VanishedOne

08.09.2019 20:34

reporter   ~0011840

Last edited: 08.03.2020 16:23

I'm not sure The Beleaguered Fence has quite the original behaviour in its final revision: I found the same as http://forums.thedarkmod.com/index.php?/topic/11298-fan-mission-the-beleaguered-fence-by-sotha-20100623/&do=findComment&comment=323573 even on the highest difficulty, with the door already open. However, I confirmed that the torch *does* still target the door, and that the current behaviour is

torch on -> door opens (or stays open the first time)
torch off -> door closes

while with the original behaviour restored in the light holder script, the torch toggles the door again, so because it now starts open it becomes

torch on -> door closes
torch off -> door opens

The script could be modified to treat lights and light holders specially and simply activate other targets, which is what I'm trying now. Personally I think giving it special handling for frob movers was a mistake; they have nothing to do with light state. However, it's always *possible*, however unlikely, that some FM relies on the current non-toggle behaviour for frob mover targets. As noted, in TBF it does make a difference even if you have to go out of your way to discover it.
VanishedOne

VanishedOne

09.09.2019 14:51

reporter   ~0011841

Attaching my altered light holder script in case anyone wants to try it. I haven't tested it thoroughly, but the test map now works (with a nice lack of console spam) and routine gameplay seems normal so far.
tdm_light_holders.script (11,929 bytes)   
// vim:ts=4:sw=4:cindent
/***********************************************************************

A script object that can be used on any entity (movable, frobable etc.)
that acts as a holder for other light entities. Examples are candle
holders, oil lamps etc.

This script object just adds a few functions that allow to call script
events on the holder, like LightsOff(), LightsOn() and LightsToggle().

These events cause all bound lights to change state, and this in turn
will allow model and skin changes on the light holder.

Code copied from tdm_frobactions.script. Author: Tels

Note: the delay for noshadows is defined in scripts/tdm_lights.script

***********************************************************************/

#ifndef __DARKMOD_LIGHT_HOLDERS__
#define __DARKMOD_LIGHT_HOLDERS__

object tdm_light_holder
{
	boolean m_bExtinguished;
	// if someone toggles the light, store which entity this was, in case
	// it is also bound to us, to avoid toggling it again, resulting in endless
	// trigger loops
	entity activator;

	void init();
	void LightsOff();
	void LightsOn();
	void LightsToggle();
    void LightsToggleResponse(entity e, entity f); // Obsttorte #3760
    
	// internal routines:
	void toggle_children( float state );
	void toggle_child( entity child, float state, boolean isTarget );
};

void tdm_light_holder::init()
{
	entity child;
	float ind;

	//sys.println(sys.getTime() + ": DEBUG (holder::init): initializing light holder " + getName());
	// grayman #2603 - for the case where the holder is a light, fire initial visual stim
	StimClearIgnoreList (STIM_VISUAL);
	StimEnable (STIM_VISUAL, 1);
    
    // Obsttorte #3760
	ResponseAdd(STIM_TRIGGER);
	ResponseSetAction(STIM_TRIGGER,"LightsToggleResponse");
	ResponseEnable(STIM_TRIGGER,1);
    
	if (getKey("extinguished") == "0")
	{
    	//sys.println(sys.getTime() + ": DEBUG (holder::init): initializing light holder " + getName() + " on");
		m_bExtinguished = false;
	}
	else
	{
     	//sys.println(sys.getTime() + ": DEBUG (holder::init): initializing light holder '" + getName() + "' off");
		LightsOff();		// grayman #2823 - tell yourself and your children to go out

		// grayman #2905 - set self (in case we're a light) and light children that we were spawned off

    	//sys.println(sys.getTime() + ": DEBUG (holder::init): calling setStartedOff for self " + getName());
		setStartedOff();
		for ( ind = 0 ; ind < numBindChildren( ) ; ind++ )
		{
			child = getBindChild( ind );
			if ( child.isLight() )
			{
    			//sys.println(sys.getTime() + ": DEBUG (holder::init): calling setStartedOff for child " + getName());
				child.setStartedOff();
			}
		}
	}

	if (getKey("noshadows_lit") == "1")
	{
		//sys.println(sys.getTime() + ": DEBUG: light holder setting shadows");
		noShadows( !m_bExtinguished );
	}
}

// Helper function for toggling any bound or targetted light objects (like flames)
// or other entities like movers:
void tdm_light_holder::toggle_children( float state )
{
	//sys.println(sys.getTime() + ": DEBUG (holder::toggle_children): state = " + state);
	entity child;
	float ind;

	for( ind = 0; ind < numBindChildren( ); ind++ )
	{
		child = getBindChild( ind );
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_children): toggling child " + child.getName());
		toggle_child( child, state, false );
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_children): setting _light_toggled key to 1 on child " + child.getName());
		child.setKey( "_light_toggled", "1");	// mark as done
	}
	// and now do the same for our targets, but ignore any we already did
	for( ind = 0; ind < numTargets( ); ind++ )
	{
		child = getTarget( ind );
		//sys.println(sys.getTime() + ": DEBUG toggle target: " + child.getName() + " state = " + state); 
		toggle_child( child, state, true );
	}
	for( ind = 0; ind < numBindChildren( ); ind++ )
	{
		child = getBindChild( ind );
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_children): setting _light_toggled key to 0 on child " + child.getName());
		child.setKey( "_light_toggled", 0);	// reset mark
	}
}

// Helper function for toggling one bound or targeted object (like flames or movers)
void tdm_light_holder::toggle_child( entity child, float state, boolean isTarget )
{
	if ( child == $null_entity )
	{
		return;
	}
	//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " starting ...");
	// #3048: avoid endless loops when this child is a mover bound to us, and linked back to us
	if (child.getIntKey("_light_toggled") == 1)	// marked as done?
	{
		//sys.println(sys.getTime() + ": DEBUG child " + child.getName() + " already toggled");
		return;
	}

	if (child.getIntKey("noactivate") != 0)	// or as noactivate?
	{
		//sys.println(sys.getTime() + ": DEBUG child has noactivate");
		return;
	}

	//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): " + child.getName() + " state = " + state); 
	string unlit_skin = child.getKey( "skin_unlit" );
	string lit_skin = child.getKey( "skin_lit" );
	string cur_skin = child.getKey( "skin" );
	//sys.println(sys.getTime() + ": DEBUG skin: " + cur_skin + " skin_unlit: " + unlit_skin + " lit_skin: " + lit_skin);

	if ( state > 0) // 1 = turn on 
	{
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " will be relit");

		// tels: call this in a sep. thread, otherwise it doesn't return until the 
		// ext. animation+particle have finished, which can be several seconds.
		// this poses problems when you ignite/extinguish more than one bound flame:
		if (child.hasFunction("frob_ignite") )
		{
			//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " using frob_ignite() in a new thread");
			thread callFunctionOn( "frob_ignite", child);
		}
		else
	   	{
			if (child.hasFunction("LightsOn") )
			{
				// child is a light holder?
				//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " using LightsOn() because the child is a light holder, in a new thread");
				thread callFunctionOn( "LightsOn", child);
			} else
			{
				//sys.println(sys.getTime() + ": DEBUG light on");
				if ( child.isLight() ) child.On();
				else if (isTarget) child.activate(self);
			}
		}

		// #3109: in case the spawnarg is set and the current skin is different,
		// the swap the skin, too. For lights, or light holder, the skin might
		// already have been swapped, so skip this step:
		if (lit_skin)
		   	{
			if (cur_skin != lit_skin)
				{
				//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): setting lit skin");
				child.setSkin( lit_skin );
				// store for later retrival by scripts
				child.setKey( "skin", lit_skin );
				}
			}
	}
	else // 0 = turn off
	{
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " will be extinguished");
		if (child.hasFunction("frob_extinguish") )
		{
			//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): " + child.getName() + ".frob_extinguish() is being called in a new thread");
			thread callFunctionOn( "frob_extinguish", child );
		}
		else
	   	{
			if (child.hasFunction("LightsOff") )
			{
				// child is a light holder?
				thread callFunctionOn( "LightsOff", child);
			} else
			{
				//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): " + child.getName() + ".Off()");
				if ( child.isLight() ) child.Off();
				else if (isTarget) child.activate(self);
			}
		}
		// #3109: in case the spawnarg is set and the current skin is different,
		// then swap the skin, too. For lights, or light holder, the skin might
		// already have been swapped, so skip this step:
		if (unlit_skin)
		{
			if (cur_skin != unlit_skin)
			{
				//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): setting unlit skin");
				child.setSkin( unlit_skin );
				// store for later retrival by scripts
				child.setKey( "skin", unlit_skin );
			}
		}
	}
	// grayman #2603 - We have changed state, so entities that noticed us last time should notice us again
	// grayman #4392 - But only if we're a light.
	if ( child.isLight() )
	{
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " clearing ignore stim list and enabling visual stim");
		child.StimClearIgnoreList (STIM_VISUAL);
		child.StimEnable (STIM_VISUAL, 1);
	}
}

// Tels: to be used on f.i. candles, extinguishes the bound flame(s)
// and these in turn will set the unlit skin on us (the holder):
void tdm_light_holder::LightsOff()
{
	//sys.println(sys.getTime() + ": DEBUG (holder::LightsOff): " + getName());

	// nothing to do
	if (m_bExtinguished)
	{
		//sys.println(sys.getTime() + ": DEBUG (holder::LightsOff): " + getName() + " - already extinguished, nothing to do");
	   return;
	}

	// do this as first step to prevent endless recursion
	m_bExtinguished = true;

	//sys.println(sys.getTime() + ": DEBUG (holder::LightsOff): " + getName() + " calling toggle_children(OFF)");
	toggle_children( 0 );

	//sys.println(sys.getTime() + ": DEBUG (holder::LightsOff): " + getName() + " calling Off() on itself");
	// in case the light holder itself is a light?
	Off();
	//fadeOutLight(0.5);

	// if requested, toggle our shadows on (after the light is off!)
	if ( getIntKey("noshadows_lit") == 1 )
	{
		// delay turning on the shadow a bit to give the light a chance to fade out
		if ( getKey("lightType") == "AIUSE_LIGHTTYPE_TORCH" )
		{
			//sys.println(sys.getTime() + ": DEBUG: LightsOff - torch shadows ON on " + getName());
			noShadowsDelayed( false, SHADOW_SWITCH_DELAY_TORCH );
		}
		else
		{
			//sys.println(sys.getTime() + ": DEBUG: LightsOff - electric shadows ON on " + getName());
			noShadowsDelayed( false, SHADOW_SWITCH_DELAY_ELECTRIC );
		}
	}

	// only if the spawnarg is set
	string skin = getKey( "skin_unlit" );
	if (skin)
	{
		setSkin( skin );
		// store for later retrieval by scripts
		setKey( "skin", skin );
	}

	// grayman #2603 - for the case where the holder is a light
	// We have changed state, so entities that noticed us last time should notice us again
	StimClearIgnoreList (STIM_VISUAL);
	StimEnable (STIM_VISUAL, 1);
}

void tdm_light_holder::LightsOn()
{
	// sys.println(sys.getTime() + ": DEBUG (holder::LightsOn): " + getName());
	// nothing to do
	if (!m_bExtinguished)
	{
		//sys.println(sys.getTime() + ": DEBUG (holder::LightsOn): " + getName() + " - already lit, nothing to do");
		return;
	}

	m_bExtinguished = false;

	//sys.println(sys.getTime() + ": DEBUG (holder::LightsOn): " + getName() + " calling toggle_children(ON)");
	toggle_children( 1 );

	// if requested, toggle our shadows off before turning the light on
	// do so immediately, otherwise the light might cast briefly a shadow
	if (getIntKey("noshadows_lit") == 1)
	{
		noShadows(true);
	}

	// in case the light holder itself is a light?
	On();
	// TODO: make this configurable via spawnarg:
	// fadeInLight(0.5);

	// only if the spawnarg is set
	string skin = getKey( "skin_lit" );
	if (skin)
	{
		setSkin( skin );
		// store for later retrieval by scripts
		setKey( "skin", skin );
	}

	// grayman #2603 - for the case where the holder is a light
	// We have changed state, so entities that noticed us last time should notice us again
	StimClearIgnoreList (STIM_VISUAL);
	StimEnable (STIM_VISUAL, 1);
}

// Obsttorte #3760
void tdm_light_holder::LightsToggleResponse(entity e, entity f)
{
	LightsToggle();
}

// Tels: this one toggles bound flames on/off, depending on the
// state the flame is currently in.
void tdm_light_holder::LightsToggle()
{
	if (m_bExtinguished)
	{
		// turn on
		//sys.println(sys.getTime() + ": DEBUG (holder::LightsToggle): on " + getKey("name") + " calling LightsOn()");
		LightsOn();
	} else
	{
		// turn off
		//sys.println(sys.getTime() + ": DEBUG (holder::LightsToggle): off " + getKey("name") + " calling LightsOff()");
		LightsOff();
	}
}

#endif //__DARKMOD_LIGHT_HOLDERS__
tdm_light_holders.script (11,929 bytes)   
VanishedOne

VanishedOne

23.03.2020 00:05

reporter   ~0012299

Added more isLight() checks to quieten console warnings (idLight script events were being called on light holders regardless of whether they were lights). Now I can set "extinguished" "1" on my atdm:lamp_wall_gasflame03_lit and not get warnings. I'm attaching the updated script, though as of now I haven't extensively trialled it.
tdm_light_holders-2.script (11,980 bytes)   
// vim:ts=4:sw=4:cindent
/***********************************************************************

A script object that can be used on any entity (movable, frobable etc.)
that acts as a holder for other light entities. Examples are candle
holders, oil lamps etc.

This script object just adds a few functions that allow to call script
events on the holder, like LightsOff(), LightsOn() and LightsToggle().

These events cause all bound lights to change state, and this in turn
will allow model and skin changes on the light holder.

Code copied from tdm_frobactions.script. Author: Tels

Note: the delay for noshadows is defined in scripts/tdm_lights.script

***********************************************************************/

#ifndef __DARKMOD_LIGHT_HOLDERS__
#define __DARKMOD_LIGHT_HOLDERS__

object tdm_light_holder
{
	boolean m_bExtinguished;
	// if someone toggles the light, store which entity this was, in case
	// it is also bound to us, to avoid toggling it again, resulting in endless
	// trigger loops
	entity activator;

	void init();
	void LightsOff();
	void LightsOn();
	void LightsToggle();
    void LightsToggleResponse(entity e, entity f); // Obsttorte #3760
    
	// internal routines:
	void toggle_children( float state );
	void toggle_child( entity child, float state, boolean isTarget );
};

void tdm_light_holder::init()
{
	entity child;
	float ind;

	//sys.println(sys.getTime() + ": DEBUG (holder::init): initializing light holder " + getName());
	// grayman #2603 - for the case where the holder is a light, fire initial visual stim
	StimClearIgnoreList (STIM_VISUAL);
	StimEnable (STIM_VISUAL, 1);
    
    // Obsttorte #3760
	ResponseAdd(STIM_TRIGGER);
	ResponseSetAction(STIM_TRIGGER,"LightsToggleResponse");
	ResponseEnable(STIM_TRIGGER,1);
    
	if (getKey("extinguished") == "0")
	{
    	//sys.println(sys.getTime() + ": DEBUG (holder::init): initializing light holder " + getName() + " on");
		m_bExtinguished = false;
	}
	else
	{
     	//sys.println(sys.getTime() + ": DEBUG (holder::init): initializing light holder '" + getName() + "' off");
		LightsOff();		// grayman #2823 - tell yourself and your children to go out

		// grayman #2905 - set self (in case we're a light) and light children that we were spawned off

    	//sys.println(sys.getTime() + ": DEBUG (holder::init): calling setStartedOff for self " + getName());
		if ( isLight() ) setStartedOff();
		for ( ind = 0 ; ind < numBindChildren( ) ; ind++ )
		{
			child = getBindChild( ind );
			if ( child.isLight() )
			{
    			//sys.println(sys.getTime() + ": DEBUG (holder::init): calling setStartedOff for child " + getName());
				child.setStartedOff();
			}
		}
	}

	if (getKey("noshadows_lit") == "1")
	{
		//sys.println(sys.getTime() + ": DEBUG: light holder setting shadows");
		noShadows( !m_bExtinguished );
	}
}

// Helper function for toggling any bound or targetted light objects (like flames)
// or other entities like movers:
void tdm_light_holder::toggle_children( float state )
{
	//sys.println(sys.getTime() + ": DEBUG (holder::toggle_children): state = " + state);
	entity child;
	float ind;

	for( ind = 0; ind < numBindChildren( ); ind++ )
	{
		child = getBindChild( ind );
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_children): toggling child " + child.getName());
		toggle_child( child, state, false );
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_children): setting _light_toggled key to 1 on child " + child.getName());
		child.setKey( "_light_toggled", "1");	// mark as done
	}
	// and now do the same for our targets, but ignore any we already did
	for( ind = 0; ind < numTargets( ); ind++ )
	{
		child = getTarget( ind );
		//sys.println(sys.getTime() + ": DEBUG toggle target: " + child.getName() + " state = " + state); 
		toggle_child( child, state, true );
	}
	for( ind = 0; ind < numBindChildren( ); ind++ )
	{
		child = getBindChild( ind );
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_children): setting _light_toggled key to 0 on child " + child.getName());
		child.setKey( "_light_toggled", 0);	// reset mark
	}
}

// Helper function for toggling one bound or targeted object (like flames or movers)
void tdm_light_holder::toggle_child( entity child, float state, boolean isTarget )
{
	if ( child == $null_entity )
	{
		return;
	}
	//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " starting ...");
	// #3048: avoid endless loops when this child is a mover bound to us, and linked back to us
	if (child.getIntKey("_light_toggled") == 1)	// marked as done?
	{
		//sys.println(sys.getTime() + ": DEBUG child " + child.getName() + " already toggled");
		return;
	}

	if (child.getIntKey("noactivate") != 0)	// or as noactivate?
	{
		//sys.println(sys.getTime() + ": DEBUG child has noactivate");
		return;
	}

	//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): " + child.getName() + " state = " + state); 
	string unlit_skin = child.getKey( "skin_unlit" );
	string lit_skin = child.getKey( "skin_lit" );
	string cur_skin = child.getKey( "skin" );
	//sys.println(sys.getTime() + ": DEBUG skin: " + cur_skin + " skin_unlit: " + unlit_skin + " lit_skin: " + lit_skin);

	if ( state > 0) // 1 = turn on 
	{
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " will be relit");

		// tels: call this in a sep. thread, otherwise it doesn't return until the 
		// ext. animation+particle have finished, which can be several seconds.
		// this poses problems when you ignite/extinguish more than one bound flame:
		if (child.hasFunction("frob_ignite") )
		{
			//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " using frob_ignite() in a new thread");
			thread callFunctionOn( "frob_ignite", child);
		}
		else
	   	{
			if (child.hasFunction("LightsOn") )
			{
				// child is a light holder?
				//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " using LightsOn() because the child is a light holder, in a new thread");
				thread callFunctionOn( "LightsOn", child);
			} else
			{
				//sys.println(sys.getTime() + ": DEBUG light on");
				if ( child.isLight() ) child.On();
				else if (isTarget) child.activate(self);
			}
		}

		// #3109: in case the spawnarg is set and the current skin is different,
		// the swap the skin, too. For lights, or light holder, the skin might
		// already have been swapped, so skip this step:
		if (lit_skin)
		   	{
			if (cur_skin != lit_skin)
				{
				//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): setting lit skin");
				child.setSkin( lit_skin );
				// store for later retrival by scripts
				child.setKey( "skin", lit_skin );
				}
			}
	}
	else // 0 = turn off
	{
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " will be extinguished");
		if (child.hasFunction("frob_extinguish") )
		{
			//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): " + child.getName() + ".frob_extinguish() is being called in a new thread");
			thread callFunctionOn( "frob_extinguish", child );
		}
		else
	   	{
			if (child.hasFunction("LightsOff") )
			{
				// child is a light holder?
				thread callFunctionOn( "LightsOff", child);
			} else
			{
				//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): " + child.getName() + ".Off()");
				if ( child.isLight() ) child.Off();
				else if (isTarget) child.activate(self);
			}
		}
		// #3109: in case the spawnarg is set and the current skin is different,
		// then swap the skin, too. For lights, or light holder, the skin might
		// already have been swapped, so skip this step:
		if (unlit_skin)
		{
			if (cur_skin != unlit_skin)
			{
				//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): setting unlit skin");
				child.setSkin( unlit_skin );
				// store for later retrival by scripts
				child.setKey( "skin", unlit_skin );
			}
		}
	}
	// grayman #2603 - We have changed state, so entities that noticed us last time should notice us again
	// grayman #4392 - But only if we're a light.
	if ( child.isLight() )
	{
		//sys.println(sys.getTime() + ": DEBUG (holder::toggle_child): child " + child.getName() + " clearing ignore stim list and enabling visual stim");
		child.StimClearIgnoreList (STIM_VISUAL);
		child.StimEnable (STIM_VISUAL, 1);
	}
}

// Tels: to be used on f.i. candles, extinguishes the bound flame(s)
// and these in turn will set the unlit skin on us (the holder):
void tdm_light_holder::LightsOff()
{
	//sys.println(sys.getTime() + ": DEBUG (holder::LightsOff): " + getName());

	// nothing to do
	if (m_bExtinguished)
	{
		//sys.println(sys.getTime() + ": DEBUG (holder::LightsOff): " + getName() + " - already extinguished, nothing to do");
	   return;
	}

	// do this as first step to prevent endless recursion
	m_bExtinguished = true;

	//sys.println(sys.getTime() + ": DEBUG (holder::LightsOff): " + getName() + " calling toggle_children(OFF)");
	toggle_children( 0 );

	//sys.println(sys.getTime() + ": DEBUG (holder::LightsOff): " + getName() + " calling Off() on itself");
	// in case the light holder itself is a light?
	if ( isLight() ) Off();
	//fadeOutLight(0.5);

	// if requested, toggle our shadows on (after the light is off!)
	if ( getIntKey("noshadows_lit") == 1 )
	{
		// delay turning on the shadow a bit to give the light a chance to fade out
		if ( getKey("lightType") == "AIUSE_LIGHTTYPE_TORCH" )
		{
			//sys.println(sys.getTime() + ": DEBUG: LightsOff - torch shadows ON on " + getName());
			noShadowsDelayed( false, SHADOW_SWITCH_DELAY_TORCH );
		}
		else
		{
			//sys.println(sys.getTime() + ": DEBUG: LightsOff - electric shadows ON on " + getName());
			noShadowsDelayed( false, SHADOW_SWITCH_DELAY_ELECTRIC );
		}
	}

	// only if the spawnarg is set
	string skin = getKey( "skin_unlit" );
	if (skin)
	{
		setSkin( skin );
		// store for later retrieval by scripts
		setKey( "skin", skin );
	}

	// grayman #2603 - for the case where the holder is a light
	// We have changed state, so entities that noticed us last time should notice us again
	StimClearIgnoreList (STIM_VISUAL);
	StimEnable (STIM_VISUAL, 1);
}

void tdm_light_holder::LightsOn()
{
	// sys.println(sys.getTime() + ": DEBUG (holder::LightsOn): " + getName());
	// nothing to do
	if (!m_bExtinguished)
	{
		//sys.println(sys.getTime() + ": DEBUG (holder::LightsOn): " + getName() + " - already lit, nothing to do");
		return;
	}

	m_bExtinguished = false;

	//sys.println(sys.getTime() + ": DEBUG (holder::LightsOn): " + getName() + " calling toggle_children(ON)");
	toggle_children( 1 );

	// if requested, toggle our shadows off before turning the light on
	// do so immediately, otherwise the light might cast briefly a shadow
	if (getIntKey("noshadows_lit") == 1)
	{
		noShadows(true);
	}

	// in case the light holder itself is a light?
	if ( isLight() ) On();
	// TODO: make this configurable via spawnarg:
	// fadeInLight(0.5);

	// only if the spawnarg is set
	string skin = getKey( "skin_lit" );
	if (skin)
	{
		setSkin( skin );
		// store for later retrieval by scripts
		setKey( "skin", skin );
	}

	// grayman #2603 - for the case where the holder is a light
	// We have changed state, so entities that noticed us last time should notice us again
	StimClearIgnoreList (STIM_VISUAL);
	StimEnable (STIM_VISUAL, 1);
}

// Obsttorte #3760
void tdm_light_holder::LightsToggleResponse(entity e, entity f)
{
	LightsToggle();
}

// Tels: this one toggles bound flames on/off, depending on the
// state the flame is currently in.
void tdm_light_holder::LightsToggle()
{
	if (m_bExtinguished)
	{
		// turn on
		//sys.println(sys.getTime() + ": DEBUG (holder::LightsToggle): on " + getKey("name") + " calling LightsOn()");
		LightsOn();
	} else
	{
		// turn off
		//sys.println(sys.getTime() + ": DEBUG (holder::LightsToggle): off " + getKey("name") + " calling LightsOff()");
		LightsOff();
	}
}

#endif //__DARKMOD_LIGHT_HOLDERS__
tdm_light_holders-2.script (11,980 bytes)   
Dragofer

Dragofer

31.07.2020 16:25

developer   ~0012705

Thanks VanishedOne for the revised scriptobject, the reduction of console spam would be much appreciated. I'm not a fan of changing functionality with regards to Open/Close, however. Surely that'd be better than just calling a simple trigger event, plus there's the question of maps relying on exactly that behaviour. At the moment I'd appreciate a version of the script with just the console spam fix.
VanishedOne

VanishedOne

31.07.2020 19:22

reporter   ~0012706

If memory serves, I handled the cases where the script tries to call On()/Off() by making them conditional on a call to isLight(), but I'm not aware of any equivalent that could be used with Open()/Close(). You can test whether an entity's scriptobject supports a function by calling hasFunction() but to my knowledge there's no equivalent to find out whether an entity supports a certain script event. In principle you could check the spawnclass, but multiple spawnclasses support Open() and Close(), so the script would have to check for them all: that's inefficient, and if any more were added someone would have to remember the lights holder script, of all things, needed to be updated to accommodate it.

If you want to preserve current behaviour without the console spam from calling Open() and Close() on entities that don't support them, my suggestion is to smile winsomely at a coder and ask for something like hasFunction() for script events. That might prove generically useful in other places too.

I'd like to point out, however, that the current behaviour (the assumption that a light holder targeting a door or other frob mover should open it when lit and close it when doused, rather than triggering it in the usual manner without depending on the current door state) is itself a potential source of map bugs: note my report on TBF above, where the torch has no apparent effect when first lit, *then* starts affecting the door when doused and relit.
Dragofer

Dragofer

31.07.2020 20:43

developer   ~0012707

Last edited: 31.07.2020 20:44

Alright, so I may have misunderstood what you've changed in your script. This is my current understanding:

1) light-related functions like On, Off and startExtinguished are now only called on entities that are positive for isLight(). This is a fix for console spam related to these functions.
2) you've replaced Open and Close with simply "activate", like for all other non-light targets.

Number 2 is probably a good call, since in its current state lights aren't adequately able to sync up with doors:
- if the light starts lit, it doesn't have the intended initial effect (door is still closed at map start, as you said)
- if the light starts extinguished, a bizarre bug occurs where the door gets teleported to a nearby location and slowly floats back to its doorframe upon first frob. I observed this in Perilous Refuge

So it's reasonable to say no map has a functioning light-door interaction, so removing Open and Close wouldn't break maps more than they were already broken. Trying to fix this interaction isn't feasible unless we have that extra function you mentioned.

So if my understanding is correct, your revised script in its current state is suitable for inclusion and testing.
VanishedOne

VanishedOne

01.08.2020 21:31

reporter   ~0012710

Yes, that's right (I don't see a startExtinguished, but I'm guessing that should be setStartedOff).

Tels in https://forums.thedarkmod.com/index.php?/topic/12418-beleaguered-fence-broken-due-to-tdm-change/&do=findComment&comment=250276 seems to have thought that the on/off states of the light holder should be linked to corresponding state1/state2 of the frob mover, where in practice the implementation forces state1/state2 to mean open/closed in every case; perhaps, with (the previous version of) TBF as an example he thought it makes more inherent sense to associate turning lights on with opening doors and turning lights off with closing them than vice versa. However, there's no reason a mapper shouldn't make a door open when something gets doused (I don't find it intuitive to consider a door's open state the 'active' state), and the non-obvious way in which the door state affects behaviour is a potential trap for mappers who aren't aware of it. I'm banking on there being only a small chance that someone was aware of the current behaviour and made a map reliant on it.
VanishedOne

VanishedOne

12.08.2020 14:20

reporter   ~0012729

I'd forgotten I added it as related, but apparently 0002564 was Tels's plan to make target-toggling mapper-configurable. Worth it? My sense is that trying to anticipate non-light-related cases in the light holder script doesn't have a great history, and doors should handle door logic, etc. (Toggling doesn't mean exactly the same thing for a door as for a light, since doors transition over time between two rest states and can be stopped while ajar...)
Dragofer

Dragofer

09.07.2022 23:21

developer   ~0015000

Toggling movers from lights doesn't make much sense to me since movers need time to change their state while light state changes happen instantly. This alone creates significant potential for desynchronisation. I also think it's quite unintuitive that a light would control a mover - if anything, I'd expect it to change the mover's skin. Both of these speak against spending resources on implementing synchronisation.

And we already have numerous ways to make things happen when a light state changes. I assume the intent behind the light<>movers interaction was to let mappers make secret doors that are opened when the player extinguishes a certain light. A new mapper can easily implement this by letting the light target a trigger_once which in turn triggers the door. This speaks for dropping Open()/Close(), using the more generic activateTargets() instead.
Dragofer

Dragofer

09.07.2022 23:27

developer   ~0015001

Last edited: 09.07.2022 23:27

In any case, the ticket was actually about electric lights lacking features that lights in holders have with respect to controlling targeted entities (skin, show/hide). This is likely due to the fact that, IIRC, non-extinguishable lights don't have a scriptobject. They're simply lights with a model. The question is whether (non-extinguishable ) lights can be given some of the features of the light holder scriptobject, and more importantly, whether this can be done without unforeseen consequences in released FMs.
Obsttorte

Obsttorte

10.07.2022 07:30

developer   ~0015004

" This is likely due to the fact that, IIRC, non-extinguishable lights don't have a scriptobject."
That's not the case. I just checked and the electric lights do use the tdm_light_holder script object. (I haven't checked every single entity, but I guess it is defined in a base class anyways).
Looking at the (as it seems heavely altered) scriptobject tells me this: toggling a light will cause the same to happen to def_attached or targeted lights. Also, if the respective skins are defined, those get changed, and if the entity in question is a binary mover it gets opened/closed respectively. There is nothing in the script that would cause any other action, like show/hide to happen, as there is no triggering happening (a.k.a. activate).
So this isn't a bug, but a result of the changes made. The hide/show worked in the past (like the example provided in the ticket description).

We should consider what effects a light or a state change of that should be able to able to attached or targeted entities and how we control it (you obviously don't want a door to hide when a light goes out for instance).
Obsttorte

Obsttorte

10.07.2022 07:31

developer   ~0015005

On a second thought: It could be that the past setup was to hide/show entities with electric lights via the button that toggles the light. So not the light, but the button targets the moon ray for example. This still leads to issues with extinguishable lights, though.

Issue History

Date Modified Username Field Change
08.09.2019 15:37 Dragofer New Issue
08.09.2019 15:37 Dragofer Tag Attached: Fix
08.09.2019 15:37 Dragofer File Added: toggle_hide_show.map.txt
08.09.2019 18:06 VanishedOne Note Added: 0011837
08.09.2019 18:13 Dragofer Note Added: 0011838
08.09.2019 18:26 VanishedOne Note Edited: 0011837
08.09.2019 18:33 VanishedOne Note Added: 0011839
08.09.2019 19:08 VanishedOne Relationship added related to 0002564
08.09.2019 20:34 VanishedOne Note Added: 0011840
09.09.2019 14:51 VanishedOne File Added: tdm_light_holders.script
09.09.2019 14:51 VanishedOne Note Added: 0011841
08.03.2020 16:23 VanishedOne Note Edited: 0011840
23.03.2020 00:05 VanishedOne Note Added: 0012299
23.03.2020 00:05 VanishedOne File Added: tdm_light_holders-2.script
31.07.2020 16:25 Dragofer Note Added: 0012705
31.07.2020 19:22 VanishedOne Note Added: 0012706
31.07.2020 20:43 Dragofer Note Added: 0012707
31.07.2020 20:43 Dragofer Note Edited: 0012707
31.07.2020 20:44 Dragofer Note Edited: 0012707
01.08.2020 21:31 VanishedOne Note Added: 0012710
12.08.2020 14:20 VanishedOne Note Added: 0012729
09.07.2022 23:21 Dragofer Note Added: 0015000
09.07.2022 23:27 Dragofer Note Added: 0015001
09.07.2022 23:27 Dragofer Note Edited: 0015001
10.07.2022 07:30 Obsttorte Note Added: 0015004
10.07.2022 07:31 Obsttorte Note Added: 0015005