Godot: Set editable children on certain nodes as default - godot

My problem is that Editable Children is always disabled by default. However, certain scenes like a hitbox.tscn would really benefit from being able to set the default to enabled.
My question is: Is there a way to do this? Or at least is there a way to make it less painful to always enable Editable Children?
I am fairly sure that there is a way to at least make it less painful, but I can't remember the specifics.
Also, judging by these issues (1, 2) there doesn't seem to be a way to make it the default. Though I am fairly certain that there is a way to do it (at least to a degree).

There seems to be a way to do it for Godot 4: set_editable_instance.
For Godot 3 I have found a work-around.
I see that PackedScene has a _bundled property of type Dictionary with a editable_instances key. But before we can access that we need to get the path of the scene file of the currently edited scene.
We can get the currently edited scene from EditorInterface.get_edited_scene_root and then read the filename property. Since we are already talking about using EditorInterface we are going to make an EditorPlugin.
Once we have the file, we could load it, modify it, and save it.
We need a tool script, because we need our code to run on the editor.
And instead of making an EditorPlugin addon, we will just make a throw-away instance to get what we want:
tool
extends Node # Or whatever
func _ready() -> void:
var editor_plugin := EditorPlugin.new()
var editor_interface := editor_plugin.get_editor_interface()
var current_edited_scene := editor_interface.get_edited_scene_root()
print(current_edited_scene)
And, of course we can get the filename:
tool
extends Node # Or whatever
func _ready() -> void:
var editor_plugin := EditorPlugin.new()
var editor_interface := editor_plugin.get_editor_interface()
var current_edited_scene := editor_interface.get_edited_scene_root()
print(current_edited_scene.filename)
And with that a PackedScene:
tool
extends Node # Or whatever
func _ready() -> void:
var editor_plugin := EditorPlugin.new()
var editor_interface := editor_plugin.get_editor_interface()
var current_edited_scene := editor_interface.get_edited_scene_root()
var packed_scene := load(current_edited_scene.filename) as PackedScene
print(packed_scene)
Then access the _bundled property:
tool
extends Node # Or whatever
func _ready() -> void:
var editor_plugin := EditorPlugin.new()
var editor_interface := editor_plugin.get_editor_interface()
var current_edited_scene := editor_interface.get_edited_scene_root()
var packed_scene := load(current_edited_scene.filename) as PackedScene
print(packed_scene._bundled)
The editable_instances key:
tool
extends Node # Or whatever
func _ready() -> void:
var editor_plugin := EditorPlugin.new()
var editor_interface := editor_plugin.get_editor_interface()
var current_edited_scene := editor_interface.get_edited_scene_root()
var packed_scene := load(current_edited_scene.filename) as PackedScene
print(packed_scene._bundled["editable_instances"])
And write to it… Wait what do I need to write? Well, since I can now see the values, I can experiment. And after some testing, I can tell that what I need is the NodePath (not String). So let us do that:
tool
extends Node # Or whatever
func _ready() -> void:
var editor_plugin := EditorPlugin.new()
var editor_interface := editor_plugin.get_editor_interface()
var current_edited_scene := editor_interface.get_edited_scene_root()
var file_path := current_edited_scene.filename
var packed_scene := load(file_path) as PackedScene
var node_path := current_edited_scene.get_path_to(self)
print(node_path)
var editable_instances:Array = packed_scene._bundled["editable_instances"]
print(editable_instances)
editable_instances.append(node_path)
packed_scene._bundled["editable_instances"] = editable_instances
print(packed_scene._bundled["editable_instances"])
var err := ResourceSaver.save(file_path, packed_scene)
print(err)
This code executes correctly, but we don't get the result we want. We will solve that with this code:
var editor_filesystem := editor_interface.get_resource_filesystem()
editor_filesystem.scan()
yield(editor_filesystem, "sources_changed")
editor_interface.reload_scene_from_path(file_path)
editor_interface.save_scene()
Here we ask Godot to look for changes in the files, we wait it to finish, then we ask Godot to reload the scene. Finally we save it so it does not retain a modified mark.
This is my final code:
tool
extends Node # Or whatever
func _ready() -> void:
var editor_plugin := EditorPlugin.new()
var editor_interface := editor_plugin.get_editor_interface()
var current_edited_scene := editor_interface.get_edited_scene_root()
if current_edited_scene == self:
# modify scenes that instantiate this one, but not this one
return
var file_path := current_edited_scene.filename
var packed_scene := load(file_path) as PackedScene
var node_path := current_edited_scene.get_path_to(self)
var editable_instances:Array = packed_scene._bundled["editable_instances"]
if editable_instances.has(node_path):
# the instance of this scene already has editable children
return
editable_instances.append(node_path)
packed_scene._bundled["editable_instances"] = editable_instances
var err := ResourceSaver.save(file_path, packed_scene)
if err != OK:
# something went wrong while saving
prints("Error modifying scene to add editable children:", err)
return
var editor_filesystem := editor_interface.get_resource_filesystem()
editor_filesystem.scan()
yield(editor_filesystem, "sources_changed")
editor_interface.reload_scene_from_path(file_path)
# warning-ignore:return_value_discarded
editor_interface.save_scene()

Related

Validate a /DIR param and default {app} value

There is a way to validate the value passed by command line with /DIR= parameter? Something like this:
C:\>MySetup.exe /DIR="An\invalid\path\here"
By validate I mean: if the directory doesn't exist, I would like to use the default value of the constant {app}, considering that the software may already be installed (UserPreviousAppDir=yes).
I tried to validate the value passed by /DIR= with CurPageChanged() in [Code] section.
[Code]
procedure CurPageChanged(CurPageID: Integer);
var
Dir, DirCmd: String;
begin
if (CurPageID = wpSelectDir) then
begin
// default directory
Dir := ExpandConstant('{app}'); // <- Error here
// test /DIR parameter
DirCmd := ExpandConstant('{param:DIR|0}');
if ( DirExists(DirCmd) ) then
Dir := DirCmd;
// set Select Destination Location page
WizardForm.DirEdit.Text := Dir;
end;
end;
The problem I see is that before the Select Destination Location page the constant {app} has not been defined yet and WizardDirValue() has the same value passed by /DIR=. So I can check that the directory do or not exists, but I can't find a way to replace it with the default value of {app} if no /DIR= had been used.
There is a solution using AppId and InstallLocation for scripts with Uninstall configuration.
[Code]
function GetAppIdString(): String;
var
S: String;
begin
S := '{#SetupSetting("AppId")}';
// ignore first { when AppId={{
if ( (S[1] = '{') and (S[2] = '{') ) then
S := Copy(S, 2, Length(S) - 1);
Result := S;
end;
function GetRegistryInstallLocation(): String;
var
RegKey: String;
Path: String;
begin
// InnoSetup uninstall registry key
RegKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + GetAppIdString() + '_is1\';
// try LocalMachine and CurrentUser
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, RegKey, 'InstallLocation', Path) then
if not RegQueryStringValue(HKEY_CURRENT_USER , RegKey, 'InstallLocation', Path) then
Path := '';
// result
Result := Path;
end;
procedure InitializeWizard();
var
Dir, DirCmd: String;
begin
// default directory
//Dir := ExpandConstant('{app}'); // <- Error here
Dir := GetRegistryInstallLocation();
// test /DIR parameter
DirCmd := ExpandConstant('{param:DIR|0}');
if ( DirExists(DirCmd) ) then
Dir := DirCmd;
// set Select Destination Location page
WizardForm.DirEdit.Text := Dir;
end;

Discard the /TYPE and /COMPONENTS parameter on upgrade

In case of an upgrade / re-installation, is there a way to discard the /TYPE and /COMPONENTS parameter value passed on the command line to the installer and instead use the previously used values ?
I can read the values used earlier from Registry (or alternatively make out the details based on existence of files assuming they have not been manually altered)
I have read the following threads and can disable the "Select Components" page in UI mode
Inno Setup Skip "Select Components" page when /Type command-line parameter is specified
InnoSetup: Disable components page on upgrade
However, if the aforesaid parameters are passed from command line, they seem to override the defaults.
You cannot discard them.
What you can do is to check if those parameters were provided and if they were:
Re-launch the installer without them (show below), or
Read the previously selected type and components from registry and re-set the controls accordingly.
Re-launching the installer without /TYPE= and /COMPONENTS=
const
UninstallKey =
'Software\Microsoft\Windows\CurrentVersion\Uninstall\' +
'{#SetupSetting("AppId")}_is1';
function IsUpgrade: Boolean;
var
Value: string;
begin
Result :=
(RegQueryStringValue(HKLM, UninstallKey, 'UninstallString', Value) or
RegQueryStringValue(HKCU, UninstallKey, 'UninstallString', Value)) and
(Value <> '');
end;
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
external 'ShellExecuteW#shell32.dll stdcall';
function InitializeSetup(): Boolean;
var
Params, S: string;
Relaunch: Boolean;
I, RetVal: Integer;
begin
Result := True;
if IsUpgrade then
begin
Relaunch := False;
// Collect current instance parameters
for I := 1 to ParamCount do
begin
S := ParamStr(I);
if (CompareText(Copy(S, 1, 7), '/TYPES=') = 0) or
(CompareText(Copy(S, 1, 12), '/COMPONENTS=') = 0) then
begin
Log(Format('Will re-launch due to %s', [S]));
Relaunch := True;
end
else
begin
// Unique log file name for the child instance
if CompareText(Copy(S, 1, 5), '/LOG=') = 0 then
begin
S := S + '-sub';
end;
// Do not pass our /SL5 switch
// This should not be needed since Inno Setup 6.2,
// see https://groups.google.com/g/innosetup/c/pDSbgD8nbxI
if CompareText(Copy(S, 1, 5), '/SL5=') <> 0 then
begin
Params := Params + AddQuotes(S) + ' ';
end;
end;
end;
if not Relaunch then
begin
Log('No need to re-launch');
end
else
begin
Log(Format('Re-launching setup with parameters [%s]', [Params]));
RetVal :=
ShellExecute(0, '', ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
Log(Format('Re-launching setup returned [%d]', [RetVal]));
Result := (RetVal > 32);
// if re-launching of this setup succeeded, then...
if Result then
begin
Log('Re-launching succeeded');
// exit this setup instance
Result := False;
end
else
begin
Log(Format('Elevation failed [%s]', [SysErrorMessage(RetVal)]));
end;
end;
end;
end;
The code is for Unicode version of Inno Setup.
The code can be further improved to keep the master installer waiting for the child installer to complete. When can make a difference, particularly if the installer is executed by some automatic deployment process.

Inno Setup Load defaults for custom installation settings from a file (.inf) for silent installation

I have a setup script that allows the user to specify where they would like to install my application. It is in the form of a Pascal script within the [Code] block.
var
SelectUsersPage: TInputOptionWizardPage;
IsUpgrade : Boolean;
UpgradePage: TOutputMsgWizardPage;
procedure InitializeWizard();
var
AlreadyInstalledPath: String;
begin
{ Determine if it is an upgrade... }
{ Read from registry to know if this is a fresh install or an upgrade }
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#MyAppId}_is1', 'Inno Setup: App Path', AlreadyInstalledPath) then
begin
{ So, this is an upgrade set target directory as installed before }
WizardForm.DirEdit.Text := AlreadyInstalledPath;
{ and skip SelectUsersPage }
IsUpgrade := True;
{ Create a page to be viewed instead of Ready To Install }
UpgradePage := CreateOutputMsgPage(wpReady,
'Ready To Upgrade', 'Setup is now ready to upgrade {#MyAppName} on your computer.',
'Click Upgrade to continue, or click Back if you want to review or change any settings.');
end
else
begin
IsUpgrade:= False;
end;
{ Create a page to select between "Just Me" or "All Users" }
SelectUsersPage := CreateInputOptionPage(wpLicense,
'Select Users', 'For which users do you want to install the application?',
'Select whether you want to install the library for yourself or for all users of this computer. Click next to continue.',
True, False);
{ Add items }
SelectUsersPage.Add('All users');
SelectUsersPage.Add('Just me');
{ Set initial values (optional) }
SelectUsersPage.Values[0] := False;
SelectUsersPage.Values[1] := True;
end;
So the question is how could I support a silent installation? When a user invokes /SILENT or /VERYSILENT the installer defaults to SelectUsersPage.Values[1], which is for Just Me. I want to help support the user that wants to change the installation directory with providing an answer file.
I didn't develop all of this code, and am a newbie with Pascal.
Thanks.
You can add a custom key (say Users) to the .inf file created by the /SAVEINF.
Then in the installer, lookup the /LOADINF command-line argument and read the key and act accordingly:
procedure InitializeWizard();
var
InfFile: string;
I: Integer;
UsersDefault: Integer;
begin
...
InfFile := ExpandConstant('{param:LOADINF}');
UsersDefault := 0;
if InfFile <> '' then
begin
Log(Format('Reading INF file %s', [InfFile]));
UsersDefault :=
GetIniInt('Setup', 'Users', UsersDefault, 0, 0, ExpandFileName(InfFile));
Log(Format('Read default "Users" selection %d', [UsersDefault]));
end
else
begin
Log('No INF file');
end;
SelectUsersPage.Values[UsersDefault] := True;
end;

Use a part of a registry key/value in the Inno Setup script

I have a need to retrieve a path to be used for some stuffs in the installer according an other application previously installed on the system.
This previous application hosts a service and only provides one registry key/value hosting this information: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\APPLICATION hosting the value ImagePath which Data is "E:\TestingDir\Filename.exe".
I need a way to only extract the installation path (E:\TestingDir) without the Filename.exe file.
Any suggestion?
thanks a lot
You can achieve this using a scripted constant.
You define a function that produces the value you need:
[Code]
function GetServiceInstallationPath(Param: string): string;
var
Value: string;
begin
if RegQueryStringValue(
HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Services\APPLICATION',
'ImagePath', Value) then
begin
Result := ExtractFileDir(Value);
end
else
begin
Result := { Some fallback value }
end;
end;
And then you refer to it using {code:GetServiceInstallationPath} where you need it (like in the [Run] section).
For example:
[Run]
Filename: "{code:GetServiceIntallationPath}\SomeApp.exe"
Actually, you probably want to retrieve the value in InitializeSetup already, and cache the value in a global variable for use in the scripted constant. And abort the installation (by returning False from InitializeSetup), in case the other application is not installed (= the registry key does not exist).
[Code]
var
ServiceInstallationPath: string;
function InitializeSetup(): Boolean;
var
Value: string;
begin
if RegQueryStringValue(
HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Services\APPLICATION',
'ImagePath', Value) then
begin
ServiceInstallationPath := ExtractFileDir(Value);
Log(Format('APPLICATION installed to %s', [ServiceInstallationPath]));
Result := True;
end
else
begin
MsgBox('APPLICATION not installed, aborting installation', mbError, MB_OK);
Result := False;
end;
end;
function GetServiceInstallationPath(Param: string): string;
begin
Result := ServiceInstallationPath;
end;
See also a similar question: Using global string script variable in Run section in Inno Setup.
Solved this way:
[code]
var
ServiceInstallationPath: string;
function MyProgCheck(): Boolean;
var
Value: string;
begin
if RegQueryStringValue(
HKEY_LOCAL_MACHINE, 'SYSTEM\ControlSet001\Services\JLR STONE VCATS TO MES',
'ImagePath', Value) then
begin
ServiceInstallationPath := ExtractFileDir(Value);
Result := True;
end
else
begin
Result := False;
end;
end;
and in the [RUN] section I put as check the TRUE condition or FALSE condition on this function according the needs...Thanks everybody answering!

Global, thread safe, cookies manager with Indy

My Delphi 2010 app uploads stuff using multi-threading, uploaded data is POSTed to a PHP/web application which requires login, so I need to use a shared/global cookies manager (I'm using Indy10 Revision 4743) since TIdCookieManager is not thread-safe :(
Also, server side, session id is automatically re-generated every 5 minutes, so I must keep both the global & local cookie managers in sync.
My code looks like this:
TUploadThread = class(TThread)
// ...
var
GlobalCookieManager : TIdCookieManager;
procedure TUploadThread.Upload(FileName : String);
var
IdHTTP : TIdHTTP;
TheSSL : TIdSSLIOHandlerSocketOpenSSL;
TheCompressor : TIdCompressorZLib;
TheCookieManager : TIdCookieManager;
AStream : TIdMultipartFormDataStream;
begin
ACookieManager := TIdCookieManager.Create(IdHTTP);
// Automatically sync cookies between local & global Cookie managers
#TheCookieManager.OnNewCookie := pPointer(Cardinal(pPointer( procedure(ASender : TObject; ACookie : TIdCookie; var VAccept : Boolean)
begin
OmniLock.Acquire;
try
GlobalCookieManager.CookieCollection.AddCookie(ACookie, TIdHTTP(TIdCookieManager(ASender).Owner).URL{IdHTTP.URL});
finally
OmniLock.Release;
end; // try/finally
VAccept := True;
end )^ ) + $0C)^;
// ======================================== //
IdHTTP := TIdHTTP.Create(nil);
with IdHTTP do
begin
HTTPOptions := [hoForceEncodeParams, hoNoParseMetaHTTPEquiv];
AllowCookies := True;
HandleRedirects := True;
ProtocolVersion := pv1_1;
IOHandler := TheSSL;
Compressor := TheCompressor;
CookieManager := TheCookieManager;
end; // with
OmniLock.Acquire;
try
// Load login info/cookies
TheCookieManager.CookieCollection.AddCookies(GlobalCookieManager.CookieCollection);
finally
OmniLock.Release;
end; // try/finally
AStream := TIdMultipartFormDataStream.Create;
with Stream.AddFile('file_name', FileName, 'application/octet-stream') do
begin
HeaderCharset := 'utf-8';
HeaderEncoding := '8';
end; // with
IdHTTP.Post('https://www.domain.com/post.php', AStream);
AStream.Free;
end;
But it doesn't work! I'm getting this exception when calling AddCookies()
Project MyEXE.exe raised exception class EAccessViolation with message
'Access violation at address 00000000. Read of address 00000000'.
I also tried using assign(), ie.
TheCookieManager.CookieCollection.Assign(GlobalCookieManager.CookieCollection);
But I still get the same exception, usually here:
TIdCookieManager.GenerateClientCookies()
Anyone knows how to fix this?
Don't use an anonymous procedure for the OnNewCookie event. Use a normal class method instead:
procedure TUploadThread.NewCookie(ASender: TObject; ACookie : TIdCookie; var VAccept : Boolean);
var
LCookie: TIdCookie;
begin
LCookie := TIdCookieClass(ACookie.ClassType).Create;
LCookie.Assign(ACookie);
OmniLock.Acquire;
try
GlobalCookieManager.CookieCollection.AddCookie(LCookie, TIdHTTP(TIdCookieManager(ASender).Owner).URL);
finally
OmniLock.Release;
end;
VAccept := True;
end;
Or:
procedure TUploadThread.NewCookie(ASender: TObject; ACookie : TIdCookie; var VAccept : Boolean);
begin
OmniLock.Acquire;
try
GlobalCookieManager.CookieCollection.AddServerCookie(ACookie.ServerCookie, TIdHTTP(TIdCookieManager(ASender).Owner).URL);
finally
OmniLock.Release;
end;
VAccept := True;
end;
Then use it like this:
procedure TUploadThread.Upload(FileName : String);
var
IdHTTP : TIdHTTP;
TheSSL : TIdSSLIOHandlerSocketOpenSSL;
TheCompressor : TIdCompressorZLib;
TheCookieManager : TIdCookieManager;
TheStream : TIdMultipartFormDataStream;
begin
IdHTTP := TIdHTTP.Create(nil);
try
...
TheCookieManager := TIdCookieManager.Create(IdHTTP);
TheCookieManager.OnNewCookie := NewCookie;
with IdHTTP do
begin
HTTPOptions := [hoForceEncodeParams, hoNoParseMetaHTTPEquiv];
AllowCookies := True;
HandleRedirects := True;
ProtocolVersion := pv1_1;
IOHandler := TheSSL;
Compressor := TheCompressor;
CookieManager := TheCookieManager;
end; // with
OmniLock.Acquire;
try
// Load login info/cookies
TheCookieManager.CookieCollection.AddCookies(GlobalCookieManager.CookieCollection);
finally
OmniLock.Release;
end;
TheStream := TIdMultipartFormDataStream.Create;
try
with TheStream.AddFile('file_name', FileName, 'application/octet-stream') do
begin
HeaderCharset := 'utf-8';
HeaderEncoding := '8';
end;
IdHTTP.Post('https://www.domain.com/post.php', TheStream);
finally
TheStream.Free;
end;
finally
IdHTTP.Free;
end;
end;
If I had to guess, I'd say your problem is in here somewhere:
// Automatically sync cookies between local & global Cookie managers
#TheCookieManager.OnNewCookie := pPointer(Cardinal(pPointer( procedure(ASender : TObject; ACookie : TIdCookie; var VAccept : Boolean)
begin
OmniLock.Acquire;
try
GlobalCookieManager.CookieCollection.AddCookie(ACookie, TIdHTTP(TIdCookieManager(ASender).Owner).URL{IdHTTP.URL});
finally
OmniLock.Release;
end; // try/finally
VAccept := True;
end )^ ) + $0C)^;
I'm not sure what the $0C magic number is there for, but I bet all those casts are there because you had a heck of a time getting the compiler to accept this. It gave you type errors saying you couldn't assign the one thing to the other.
Those type errors are there for a reason! If you hack your way around the type system, things are very likely to break. Try turning that anonymous method into a normal method on TUploadThread and assign it that way, and see if it doesn't work better.
Responding to the comment:
Thank you guys, I converted to a normal method, but I'm still getting
exceptions in AddCookies(), last one happened in the line that reads
FRWLock.BeginWrite; in this procedure
TIdCookies.LockCookieList(AAccessType: TIdCookieAccess):
TIdCookieList;
If your error is an Access Violation with Read of address 00000000, that's got a very specific meaning. It means you're trying to do something with an object that's nil.
When you get that, break to the debugger. If the error's taking place on the line you said it's happening on, then it's almost certain that either Self or FRWLock is nil at this point. Check both variables and figure out which one hasn't been constructed yet, and that'll point you to the solution.

Resources