-- All routines in dune schema, concatenated. Generated by _export.sh

-- _add_item_delete_log(in_item_id bigint, in_inventory_id bigint, in_template_id text) -> void
-- oid: 58085  kind: FUNCTION  category: player_persistence

CREATE OR REPLACE FUNCTION dune._add_item_delete_log(in_item_id bigint, in_inventory_id bigint, in_template_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    PERFORM _add_item_trace_log('delete_item', in_item_id, in_inventory_id, in_template_id);
END;
$function$


-- _add_item_trace_log(in_function_name dune.itemtrackingfunctiontype, in_item_locations dune.inventoryitemlocation[]) -> void
-- oid: 58086  kind: FUNCTION  category: player_persistence

CREATE OR REPLACE FUNCTION dune._add_item_trace_log(in_function_name dune.itemtrackingfunctiontype, in_item_locations dune.inventoryitemlocation[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    IF coalesce(current_setting('dune.item_tracking_enabled', true)::BOOLEAN, false) IS FALSE THEN
        return;
    END IF;
    
    INSERT INTO item_operations_staging_table (
        function_name,
        item_id,
        account_id,
        inventory_id,
        template_id,
        event_time,
        position_index
    )
    SELECT
        in_function_name,
        (loc).item_id,
        act.owner_account_id,
        (loc).inventory_id,
        NULL, -- template_id
        now(),
        (loc).position_index
    FROM UNNEST(in_item_locations) AS loc
    JOIN inventories inv ON inv.id = (loc).inventory_id
    JOIN actors act ON act.id = inv.actor_id;
END;
$function$


-- _add_item_trace_log(in_function_name dune.itemtrackingfunctiontype, in_item_id bigint, in_inventory_id bigint, in_template_id text, in_position_index bigint) -> void
-- oid: 58087  kind: FUNCTION  category: player_persistence

CREATE OR REPLACE FUNCTION dune._add_item_trace_log(in_function_name dune.itemtrackingfunctiontype, in_item_id bigint, in_inventory_id bigint, in_template_id text, in_position_index bigint DEFAULT NULL::bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    owner_account_id BIGINT;
BEGIN
    IF coalesce(current_setting('dune.item_tracking_enabled', true)::BOOLEAN, false) IS FALSE THEN
        return;
    END IF;
    
    -- get item owner's account id
    SELECT act.owner_account_id
    INTO owner_account_id
    FROM inventories inv
    JOIN actors act ON act.id = inv.actor_id
    WHERE inv.id = in_inventory_id;

    INSERT INTO item_operations_staging_table (
        function_name,
        item_id,
        account_id,
        inventory_id,
        template_id,
        event_time,
        position_index
    ) VALUES (
        in_function_name,
        in_item_id,
        owner_account_id,
        in_inventory_id,
        in_template_id,
        now(),
        in_position_index
    );
END;
$function$


-- _building_validate_totem_owner_id(in_totem_owner_id bigint) -> bigint
-- oid: 58094  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune._building_validate_totem_owner_id(in_totem_owner_id bigint)
 RETURNS bigint
 LANGUAGE sql
BEGIN ATOMIC
 SELECT
         CASE
             WHEN (in_totem_owner_id = 0) THEN NULL::bigint
             WHEN (EXISTS ( SELECT 1
                FROM dune.fgl_entities
               WHERE (fgl_entities.entity_id = _building_validate_totem_owner_id.in_totem_owner_id))) THEN in_totem_owner_id
             ELSE NULL::bigint
         END AS "case";
END


-- _character_transfer_allocate_id(kind dune._charactertransferentrykind, data jsonb) -> bigint
-- oid: 58095  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_allocate_id(kind dune._charactertransferentrykind, data jsonb)
 RETURNS bigint
 LANGUAGE sql
 IMMUTABLE
AS $function$
	select case
		when kind = 'acc' then nextval('encrypted_accounts_id_seq')
		when kind = 'act' then nextval('actors_id_seq')
		when kind = 'inv' then nextval('inventories_id_seq')
		when kind = 'itm' then nextval('items_id_seq')
		when kind = 'fgl' then nextval('character_transfer_fgl_entities_entity_id_seq')
		when kind = 'bbp' then nextval('building_blueprints_id_seq')

		when kind = 'VehicleModule' then nextval('vehicle_modules_id_seq')

		when kind = 'Faction' then (select id from factions where "name" = data->>'name')
		when kind = 'Tutorial' then (select id from tutorials where "name" = data->>'name')
		when kind = 'Keystone' then (select id from specialization_keystones_map where "name" = data->>'name')

		when kind = 'BaseBackup' then nextval('base_backups_id_seq')
		when kind = 'TaxInvoice' then nextval('tax_invoice_id_seq')

		when kind = 'DungeonCompletion' then nextval('dungeon_completion_completion_id_seq')
		else null
	end;
$function$


-- _character_transfer_create_data_table() -> void
-- oid: 58096  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_create_data_table()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	if not exists (
		select 1 from pg_catalog.pg_tables
		where schemaname=(select nspname from pg_catalog.pg_namespace where oid=pg_my_temp_schema())
		and tablename = 'export_data'
	) then
	    create temporary table if not exists pg_temp.export_data (
	        "id" BigInt DEFAULT NULL,
	        "transfer_id" BigSerial PRIMARY KEY NOT NULL,
	        "kind" _CharacterTransferEntryKind NOT NULL,
	        "data" JsonB NOT NULL
	    ) on commit drop;
		create index on pg_temp.export_data("id");
		create index on pg_temp.export_data("kind");
	end if;
	alter sequence pg_temp.export_data_transfer_id_seq restart with 1;
	truncate pg_temp.export_data;
end
$function$


-- _character_transfer_data_filter(id text, removed text[], VARIADIC refs dune._charactertransferdatafilterref[]) -> dune._charactertransferdatafilter
-- oid: 58097  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_data_filter(id text, removed text[], VARIADIC refs dune._charactertransferdatafilterref[] DEFAULT '{}'::dune._charactertransferdatafilterref[])
 RETURNS dune._charactertransferdatafilter
 LANGUAGE sql
 IMMUTABLE
AS $function$
	select (id, removed, refs)::_CharacterTransferDataFilter;
$function$


-- _character_transfer_data_table_load(entries jsonb) -> void
-- oid: 58098  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_data_table_load(entries jsonb)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	with parsed as (
		select
			(entry->>'id')::BigInt as transfer_id,
			(entry->>'kind')::_CharacterTransferEntryKind as kind,
			(entry->'data') as data
		from jsonb_array_elements(entries) as entry
	)
	insert into pg_temp.export_data("id", "transfer_id", "kind", "data")
		select _character_transfer_allocate_id(kind, data) as id, transfer_id, kind, data
		from parsed;
end
$function$


-- _character_transfer_data_table_save() -> jsonb
-- oid: 58099  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_data_table_save()
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$
begin
	return (select jsonb_agg(jsonb_build_object('id', transfer_id, 'kind', kind, 'data', data)) from pg_temp.export_data);
end
$function$


-- _character_transfer_ensure_player_is_owner_of_vbt_vehicle(in_vehicle_id bigint[]) -> void
-- oid: 58100  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune._character_transfer_ensure_player_is_owner_of_vbt_vehicle(in_vehicle_id bigint[])
 RETURNS void
 LANGUAGE sql
AS $function$
	update permission_actor_rank set "rank" = 1 where permission_actor_id = any(in_vehicle_id);
$function$


-- _character_transfer_get_filter(kind dune._charactertransferentrykind) -> dune._charactertransferdatafilter
-- oid: 58101  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_get_filter(kind dune._charactertransferentrykind)
 RETURNS dune._charactertransferdatafilter
 LANGUAGE sql
 IMMUTABLE
AS $function$
	select case
		when kind = 'acc' then _character_transfer_data_filter('id', '{user,funcom_id}')
		when kind = 'act' then _character_transfer_data_filter('id', '{}', ('owner_account_id', 'acc', false))
		when kind = 'inv' then _character_transfer_data_filter(
			'id',
			'{exchange_id,item_id}',
			('actor_id', 'act', false),
			('vehicle_module_id', 'VehicleModule', false)
		)
		when kind = 'itm' then _character_transfer_data_filter(
			'id', '{}',
			('inventory_id', 'inv', true)
		)
		when kind = 'fgl' then _character_transfer_data_filter(
			'entity_id', '{}',
			('actor_id', 'act', true)
		)
		when kind = 'bbp' then _character_transfer_data_filter(
			'id', '{}',
			('item_id', 'itm', true),
			('player_id', 'act', false)
		)

		when kind = 'Faction' then _character_transfer_data_filter('id', '{}')
		when kind = 'Tutorial' then _character_transfer_data_filter('id', '{}')
		when kind = 'Keystone' then _character_transfer_data_filter('id', '{}')

		when kind = 'Character' then _character_transfer_data_filter(
			null, '{character_name}',
			('account_id', 'acc', true),
			('player_pawn_id', 'act', true),
			('player_controller_id', 'act', true),
			('player_state_id', 'act', true)
		)
		when kind = 'RespawnLocation' then _character_transfer_data_filter(
			null, '{}',
			('account_id', 'acc', true)
		)
		when kind = 'PlayerMarker' then _character_transfer_data_filter(null, '{}', ('player_id', 'act', true))
		when kind = 'Marker' then _character_transfer_data_filter(null, '{}')
		when kind = 'DialogueMetNpc' then _character_transfer_data_filter(null, '{}', ('player_id', 'act', true))
		when kind = 'DialogueTakenNode' then _character_transfer_data_filter(null, '{}', ('player_id', 'act', true))

		when kind = 'PlayerFaction' then _character_transfer_data_filter(
			null, '{}',
			('actor_id', 'act', true), ('faction_id', 'Faction', true)
		)
		when kind = 'PlayerFactionReputation' then _character_transfer_data_filter(
			null, '{}',
			('actor_id', 'act', true), ('faction_id', 'Faction', true)
		)
		when kind = 'ConsumedLore' then _character_transfer_data_filter(null, '{}', ('actor_id', 'act', true))
		when kind = 'PlayerTutorial' then _character_transfer_data_filter(null, '{}', ('player_id', 'act', true), ('tutorial_id', 'Tutorial', true))
        when kind = 'PurchasedKeystone' then _character_transfer_data_filter(null, '{}', ('player_id', 'act', true), ('keystone_id', 'Keystone', true))
		when kind = 'SpecializationTracks' then _character_transfer_data_filter(null, '{}', ('player_id', 'act', true))
		when kind = 'SpecializationRefund' then _character_transfer_data_filter(null, '{}', ('player_id', 'act', true))

		when kind = 'BuildingFavorite' then _character_transfer_data_filter(null, '{}', ('account_id', 'acc', true))
		when kind = 'BuildingProgression' then _character_transfer_data_filter(null, '{}', ('account_id', 'acc', true))
		when kind = 'CommuninetPlayer' then _character_transfer_data_filter(null, '{}', ('account_id', 'acc', true))
		when kind = 'CommuninetPlayerChannel' then _character_transfer_data_filter(null, '{}', ('account_id', 'acc', true))
		when kind = 'JourneyStoryNode' then _character_transfer_data_filter(null, '{}', ('account_id', 'acc', true))
		when kind = 'MapArea' then _character_transfer_data_filter(null, '{}', ('account_id', 'acc', true))
		when kind = 'PlayerAccessCode' then _character_transfer_data_filter(null, '{}', ('account_id', 'acc', true))
		when kind = 'PlayerTag' then _character_transfer_data_filter(null, '{}', ('account_id', 'acc', true))

		when kind = 'ActorInventory' then _character_transfer_data_filter(null, '{}', ('inventory_id', 'inv', true))
		when kind = 'Sinkchart' then _character_transfer_data_filter(null, '{}', ('item_id', 'itm', true))

		when kind = 'BackupVehicle' then _character_transfer_data_filter(null, '{}', ('account_id', 'acc', true), ('vehicle_id', 'act', true))
		when kind = 'RecoveredVehicle' then _character_transfer_data_filter(null, '{}', ('account_id', 'acc', true), ('vehicle_id', 'act', true))
		when kind = 'Vehicle' then _character_transfer_data_filter(null, '{}', ('id', 'act', true))
		when kind = 'VehicleModule' then _character_transfer_data_filter('id', '{}', ('vehicle_id', 'act', true))
		when kind = 'VehicleModuleInventory' then _character_transfer_data_filter(null, '{}', ('inventory_id', 'inv', true))

		when kind = 'ActorState' then _character_transfer_data_filter(null, '{}', ('actor_id', 'act', true))

		when kind = 'PermissionActor' then _character_transfer_data_filter(null, '{}', ('actor_id', 'act', true))
		when kind = 'PermissionActorRank' then _character_transfer_data_filter(null, '{}', ('permission_actor_id', 'act', true), ('player_id', 'act', true))

		when kind = 'BuildingBlueprintInstance' then _character_transfer_data_filter(null, '{}', ('building_blueprint_id', 'bbp', true))
		when kind = 'BuildingBlueprintPlaceable' then _character_transfer_data_filter(null, '{}', ('building_blueprint_id', 'bbp', true))
		when kind = 'BuildingBlueprintPentashield' then _character_transfer_data_filter(null, '{}', ('building_blueprint_id', 'bbp', true))

		when kind = 'Building' then _character_transfer_data_filter(null, '{}', ('id', 'act', true))
		when kind = 'BuildingInstance' then _character_transfer_data_filter(null, '{}', ('building_id', 'act', true), ('owner_entity_id', 'fgl', false))
		when kind = 'Placeable' then _character_transfer_data_filter(null, '{}', ('id', 'act', true), ('owner_entity_id', 'fgl', false))
		when kind = 'BaseBackup' then _character_transfer_data_filter('id', '{}', ('player_id', 'act', true))
		when kind = 'BaseBackupLinkedActor' then _character_transfer_data_filter(null, '{}', ('id', 'BaseBackup', true), ('actor_id', 'act', true))
		when kind = 'LandclaimSegment' then _character_transfer_data_filter(null, '{}', ('totem_id', 'act', true))
		when kind = 'TaxInvoice' then _character_transfer_data_filter('id', '{}', ('totem_id', 'act', true))

		when kind = 'PlayerVirtualCurrencyBalance' then _character_transfer_data_filter(null, '{}', ('player_controller_id', 'act', true))

		when kind = 'DungeonCompletion' then _character_transfer_data_filter('completion_id', '{}')
		when kind = 'DungeonCompletionPlayer' then _character_transfer_data_filter(null, '{}', ('player_id', 'act', true), ('completion_id', 'DungeonCompletion', true))
		when kind = 'Totem' then _character_transfer_data_filter(null, '{}', ('id', 'act', true))

        when kind = 'LandsraadHouseRewards' then _character_transfer_data_filter(null, '{}', ('player_id', 'act', true))
	end;
$function$


-- _character_transfer_get_patches_checksum() -> text
-- oid: 58102  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_get_patches_checksum()
 RETURNS text
 LANGUAGE plpgsql
AS $function$
begin
	return (SELECT md5(coalesce(string_agg("name", ',' ORDER BY "name"), '')) FROM applied_patches);
end
$function$


-- _character_transfer_pre_export_validation(in_fls_id text) -> TABLE(out_acc_id bigint, out_funcom_id text, out_player_controller_id bigint, out_player_pawn_id bigint)
-- oid: 58103  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_pre_export_validation(in_fls_id text)
 RETURNS TABLE(out_acc_id bigint, out_funcom_id text, out_player_controller_id bigint, out_player_pawn_id bigint)
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_acc_id BigInt;
    v_funcom_id Text;
	v_player_controller_id BigInt;
	v_player_pawn_id BigInt;
BEGIN
	if not (select is_player_offline(in_fls_id)) then
        raise exception 'sbRF3$ - player_online: Player % must be Offline', in_fls_id;
    end if;

	v_acc_id := (select id from accounts where "user" = in_fls_id);
    IF v_acc_id is null THEN
        RAISE EXCEPTION 'sbFV2$ - unknown_fls_id: FLS ID % not found', in_fls_id;
    END IF;

    v_funcom_id := (select funcom_id from accounts where "user" = in_fls_id);
    IF v_funcom_id is null THEN
        RAISE EXCEPTION 'sb9X3$ - missing_funcom_id: Player % does not have a funcomId', in_fls_id;
    END IF;

	select into v_player_controller_id, v_player_pawn_id
		player_controller_id, player_pawn_id from player_state where account_id=v_acc_id;
	if v_player_controller_id is null then
		raise exception 'sbH84$ - missing_controller: Player % does not have a controller', in_fls_id;
	end if;
	if v_player_pawn_id is null then
		raise exception 'sbHF3$ - missing_character: Player % does not have a character', in_fls_id;
	end if;

	return query select v_acc_id, v_funcom_id, v_player_controller_id, v_player_pawn_id;
END;
$function$


-- _character_transfer_property_not_exported_is_expected(path text) -> boolean
-- oid: 58104  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_property_not_exported_is_expected(path text)
 RETURNS boolean
 LANGUAGE sql
 IMMUTABLE
AS $function$
	select (path = ANY(ARRAY[
		'.properties.BP_DunePlayerCharacter_C.m_CurrentVehicleId',
		'.properties.WeaponActorComponent.m_FavoriteWeaponItemDatabaseId',
		'.properties.ContractsCoordinatorComponent.m_TrackedContractItemUid',
		'.components.FItemCraftingComponent.*.RequestsQueue.*.InstigatorActorId',
		'.stats.FSinkchartsStats.*.CreatorPlayerId',
		'.components.FItemCraftingComponent.*.RequestsQueue.*.IngredientAllocations.*.ItemAllocNodes.*.AllocatedItems.*.ItemUniqueId', -- TODO [DA-4712]: Remove once this item loss is fixed
		'.stats.FBuildingBlueprintItemStats.*.PlayerBlueprintId', -- TODO [DA-4721]: Remove once this item loss is fixed
		'.properties.BP_TransportOrnithopter_CHOAM_C.m_HarnessedVehicleId',
		'.stats.FReferenceItemStats.*.ReferenceDatabaseId', -- These often point to items that no longer exist
		'.components.FTotemLandclaimComponent.*.m_PendingStakingUnitsEntityIds.*', -- Staking units are not transferred with the base backup. This is missing a cleanup
		'.components.FTotemLandclaimComponent.*.m_PendingVerticalStakingUnitsEntityIds.*' -- Staking units are not transferred with the base backup. This is missing a cleanup
	]::text[]));
$function$


-- _character_transfer_replace_local_id_with_transfer_id(data text, path text) -> text
-- oid: 58105  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_replace_local_id_with_transfer_id(data text, path text)
 RETURNS text
 LANGUAGE plpgsql
AS $function$
declare
	v_kind _CharacterTransferEntryKind;
	v_raw_kind Text;
	v_id BigInt;
	v_transfer_id BigInt;
begin
	v_raw_kind = substring(data from 3 for 3);
	begin
		v_kind := v_raw_kind::_CharacterTransferEntryKind;
	exception
		when invalid_text_representation then
			raise exception 'sbPH2$ - Invalid transfer id in data "%" at "%", unknown kind: %', data, path, v_raw_kind;
	end;

	v_id := (substring(data from 7))::BigInt;
	if v_id = 0 then
		return format('!!%s@0', v_kind::text);
	end if;

	if v_id < 0 and v_id >= -2147483648 then
		v_id := v_id + 4294967296;
	end if;


	v_transfer_id := (select transfer_id from pg_temp.export_data where id=v_id and kind=v_kind);
	if v_transfer_id is null then
		if _character_transfer_property_not_exported_is_expected(path) then
			return format('!!%s@0', v_kind::text);
		else
			raise exception 'sbQ73$ - Id % by % was not exported', data, path;
		end if;
	end if;

	return format('!!%s@%s', v_kind::text, v_transfer_id::text);
end
$function$


-- _character_transfer_replace_local_id_with_transfer_id_in_json(data jsonb, path text) -> jsonb
-- oid: 58106  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_replace_local_id_with_transfer_id_in_json(data jsonb, path text)
 RETURNS jsonb
 LANGUAGE sql
AS $function$
	select case
		when data is json array then
			(select coalesce(jsonb_agg(_character_transfer_replace_local_id_with_transfer_id_in_json(
				element, path || '.*'
			)), '[]'::jsonb) from jsonb_array_elements(data) as element)
		when data is json object then
			(select coalesce(jsonb_object_agg(key, _character_transfer_replace_local_id_with_transfer_id_in_json(
				value, path || '.' || key
			)), '{}'::jsonb) from jsonb_each(data))
		when data is json scalar and jsonb_typeof(data) = 'string' and (data #>> '{}') like '!!___#%' then
			(select to_jsonb(_character_transfer_replace_local_id_with_transfer_id(
				data #>> '{}', path
			)))
		else
			data
	end;
$function$


-- _character_transfer_replace_transfer_id_with_local_id(data text, path text) -> text
-- oid: 58107  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_replace_transfer_id_with_local_id(data text, path text)
 RETURNS text
 LANGUAGE plpgsql
AS $function$
declare
	v_kind _CharacterTransferEntryKind;
	v_id BigInt;
	v_transfer_id BigInt;
begin
	if substring(data from 6 for 1) != '@' then
		raise exception 'sbFM3$ - Invalid transfer id % at %: expected @ separator', data, path;
	end if;

	v_kind := substring(data from 3 for 3)::_CharacterTransferEntryKind;

	v_transfer_id := (substring(data from 7))::BigInt;
	if v_transfer_id = 0 then
		return format('!!%s#0', v_kind::text);
	end if;

	v_id := (select id from pg_temp.export_data where transfer_id=v_transfer_id and kind=v_kind);
	if v_id is null then
		raise exception 'sbQ64$ - Transfer id % at % was not imported', data, path;
	end if;

	return format('!!%s#%s', v_kind::text, v_id::text);
end
$function$


-- _character_transfer_replace_transfer_id_with_local_id_in_json(data jsonb, path text) -> jsonb
-- oid: 58108  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_replace_transfer_id_with_local_id_in_json(data jsonb, path text)
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$
declare
	v_id_str Text;
begin
	if data is json array then
		return (
			select coalesce(jsonb_agg(_character_transfer_replace_transfer_id_with_local_id_in_json(
				element, path || '.*'
			)), '[]'::jsonb) from jsonb_array_elements(data) as element
		);
	end if;

	if data is json object then
		return (
			select coalesce(jsonb_object_agg(key, _character_transfer_replace_transfer_id_with_local_id_in_json(
				value, path || '.' || key
			)), '{}'::jsonb) from jsonb_each(data)
		);
	end if;

	if data is json scalar then
		if jsonb_typeof(data) = 'string' then
			v_id_str := (data #>> '{}');

			if v_id_str like '!!___#%' then
				raise exception 'sbJ34$ - Non-transfer id % is found at %', v_id_str, path;
			end if;

			if v_id_str like '!!___@%' then
				return (select to_jsonb(_character_transfer_replace_transfer_id_with_local_id(v_id_str, path)));
			end if;

			if v_id_str = '!!@0' then
				raise exception 'sb5G3$ - Transfer id missing kind marker at %: %', path, v_id_str;
			end if;
		end if;
	end if;

	return data;
end
$function$


-- _character_transfer_store_in_world_owned_vehicles_into_recovery(in_player_id bigint) -> void
-- oid: 58109  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune._character_transfer_store_in_world_owned_vehicles_into_recovery(in_player_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    to_store RECORD;
BEGIN
	-- remove any vehicles in 'Travel' state, so that they get re-added as recovered vehicles
	DELETE FROM actor_state ast
		WHERE ast.state = 'Travel'
		AND ast.actor_id IN (
			SELECT v.id
			FROM actors a
				INNER JOIN vehicles v ON a.id = v.id
				INNER JOIN permission_actor p ON a.id = p.actor_id
				INNER JOIN permission_actor_rank r ON p.actor_id = r.permission_actor_id
			WHERE r.player_id = in_player_id
			AND r.rank = 1::smallint
		);

    FOR to_store IN
		SELECT v.id AS vehicle_id
		FROM actors a
			INNER JOIN vehicles v ON a.id = v.id
			INNER JOIN permission_actor p ON a.id = p.actor_id
			INNER JOIN permission_actor_rank r ON p.actor_id = r.permission_actor_id
		WHERE r.player_id = in_player_id
		AND r.rank = 1::smallint
		AND NOT EXISTS
		(
			SELECT 1 FROM actor_state
			WHERE actor_state.actor_id = v.id
			AND actor_state.state IS DISTINCT FROM 'Default'
		)
	LOOP
		-- note: storing hardcoded chassis durability because its not available from pure database :(
		-- this will only be shown incorrectly in UI though, the spawned vehicle will get the correct value
        PERFORM store_recovered_vehicle(to_store.vehicle_id, 1.0, 'None', true);

		-- we delete the permissions for this vehicle so that they don't get exported and cause duplicated key failures
        -- when the player tries to recover their vehicle on the target battlegroup
		-- note: the server will destroy the vehicle as soon as it gets the pg_notify emitted from the store_recovered_vehicle call above, so this would happen regardless
		DELETE FROM permission_actor pa WHERE pa.actor_id = to_store.vehicle_id;
    END LOOP;
END
$function$


-- _character_transfer_top_level_export(in_kind dune._charactertransferentrykind, data jsonb) -> jsonb
-- oid: 58110  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_top_level_export(in_kind dune._charactertransferentrykind, data jsonb)
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$
declare
    v_id BigInt;
	v_transfer_id BigInt;
    v_ref _CharacterTransferDataFilterRef;
	v_filter _CharacterTransferDataFilter;
begin
	v_filter := _character_transfer_get_filter(in_kind);
	data := data - (v_filter).removed;
	if (v_filter).id is not null then
		data := data - (v_filter).id;
	end if;
    foreach v_ref in array (v_filter).refs loop
        v_id := (data->>((v_ref).key))::BigInt;

        if v_id is null then
			if (v_ref).required then
	            raise exception 'sbP23$ - Required reference % not found in %', v_ref, data;
			else
				continue;
			end if;
        end if;

		v_transfer_id := (select transfer_id from pg_temp.export_data where kind=(v_ref).kind and id=v_id);
		if v_transfer_id is null then
			raise exception 'sbJG2$ - Id % in % not mapped into transfer id of kind %. Id exists as kind: %',
				v_id, data, (v_ref).kind, (select kind from pg_temp.export_data where id=v_id);
		end if;

        data := jsonb_set(data, array[(v_ref).key], to_jsonb(v_transfer_id));
    end loop;
    return data;
end
$function$


-- _character_transfer_top_level_import(in_kind dune._charactertransferentrykind, data jsonb, in_id bigint) -> jsonb
-- oid: 58111  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune._character_transfer_top_level_import(in_kind dune._charactertransferentrykind, data jsonb, in_id bigint)
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$
declare
    v_ref_transfer_id BigInt;
	v_ref_id BigInt;
    v_ref _CharacterTransferDataFilterRef;
	v_filter _CharacterTransferDataFilter;
begin
	v_filter := _character_transfer_get_filter(in_kind);

	if (v_filter).id is null then
		if in_id is not null then
			raise exception 'sbGM2$ - Transfer logic error: not-null in_id for secondary kind: %, data: %', in_kind, data;
		end if;
	else
		if in_id is null then
			raise exception 'sb8V2$ - Transfer logic error: null in_id for primary kind: %, data: %', in_kind, data;
		end if;
		data := data || jsonb_build_object((v_filter).id, in_id);
	end if;

    foreach v_ref in array (v_filter).refs loop
        v_ref_transfer_id := (data->>((v_ref).key))::BigInt;
        if v_ref_transfer_id is null then
			if (v_ref).required then
	            raise exception 'sb2C2$ - Missing reference % in import of kind %: %', (v_ref).key, in_kind, data;
			else
				continue;
			end if;
        end if;
		v_ref_id := (select to_jsonb(id) from pg_temp.export_data where kind=(v_ref).kind and transfer_id=v_ref_transfer_id);
		if v_ref_id is null then
			raise exception 'sb3W3$ - Unknown reference %s with transfer id % in import for kind %, reference id %: %', (v_ref).key, v_ref_transfer_id, in_kind, v_ref_id, data;
		end if;
        data := jsonb_set(data, array[(v_ref).key], to_jsonb(v_ref_id));
    end loop;
    return data;
end
$function$


-- _placeable_validate_totem_owner_id(in_totem_owner_id bigint) -> bigint
-- oid: 58112  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune._placeable_validate_totem_owner_id(in_totem_owner_id bigint)
 RETURNS bigint
 LANGUAGE sql
BEGIN ATOMIC
 SELECT
         CASE
             WHEN (in_totem_owner_id = 0) THEN NULL::bigint
             WHEN (EXISTS ( SELECT 1
                FROM dune.fgl_entities
               WHERE (fgl_entities.entity_id = _placeable_validate_totem_owner_id.in_totem_owner_id))) THEN in_totem_owner_id
             ELSE NULL::bigint
         END AS "case";
END


-- _user_data_encryption_initially_encrypt_existing_data() -> void
-- oid: 58113  kind: FUNCTION  category: encryption

CREATE OR REPLACE FUNCTION dune._user_data_encryption_initially_encrypt_existing_data()
 RETURNS void
 LANGUAGE sql
AS $function$
	update encrypted_accounts set encrypted_funcom_id=encrypt_user_data(convert_from(encrypted_funcom_id, 'utf8'));
	update encrypted_player_state set
		encrypted_character_name=encrypt_user_data(convert_from(encrypted_character_name, 'utf8'));
$function$


-- _user_data_encryption_setup_enabled(key_hash bytea) -> void
-- oid: 58114  kind: FUNCTION  category: encryption

CREATE OR REPLACE FUNCTION dune._user_data_encryption_setup_enabled(key_hash bytea)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	if key_hash is null then
		raise exception 'User-data encryption requested but the server does not have the encryption key set';
	end if;

	if (select get_stored_user_data_encryption_taint_xmax()) is not null then
		-- should have been filtered by the main setup function
		raise exception 'Trying to enable encryption on a tainted data';
	end if;

	execute format($x$
		create or replace function get_stored_user_data_encryption_key_hash() returns bytea immutable
    		as $y$select '%s'::bytea;$y$ language sql;
	$x$, key_hash::text);

    -- the data is encrypted and we have the key
	create or replace function get_stored_user_data_encryption_status() returns UserDataEncryptionStatus immutable
		as $x$select 'Enabled'::UserDataEncryptionStatus;$x$ language sql;

	-- We may bake the key into the function code but then dumping the functions will reveal the key
	create or replace function encrypt_user_data(in_data text) returns bytea immutable as $x$
		select ext.encrypt(convert_to(in_data, 'utf8'), current_setting('funcom.user_data_encryption_key')::bytea, 'aes')::bytea;
	$x$ language sql;

	create or replace function decrypt_user_data(in_encrypted_data bytea) returns text immutable as $x$
		select convert_from(
			ext.decrypt(in_encrypted_data, current_setting('funcom.user_data_encryption_key')::bytea, 'aes'), 'utf8'
		);
	$x$ language sql;
end
$function$


-- _user_data_encryption_setup_tainted() -> void
-- oid: 58115  kind: FUNCTION  category: encryption

CREATE OR REPLACE FUNCTION dune._user_data_encryption_setup_tainted()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	-- we not replacing get_stored_user_data_encryption_key_hash()
	if (select get_stored_user_data_encryption_key_hash()) is null then
		-- should have been filtered by the main setup function
		raise exception 'Tainted but the data is not encrypted in the first place';
	end if;

	execute format($x$
		create or replace function get_stored_user_data_encryption_taint_xmax() returns int8 immutable
    		as 'select %s;' language sql;
	$x$, pg_current_xact_id());

	-- the data is encrypted but we don't have the key (or don't want to)
	create or replace function get_stored_user_data_encryption_status() returns UserDataEncryptionStatus immutable
		as $x$select 'Tainted'::UserDataEncryptionStatus;$x$ language sql;

	create or replace function encrypt_user_data(in_data text) returns bytea immutable
		as $x$select convert_to(in_data, 'utf8')$x$ language sql;

	-- We may use the taint xmax to differentiate between encrypted/unencrypted data for that we would have to pass the
	-- xmin into the function (which is xid and not xid8)
	create or replace function decrypt_user_data(in_encrypted_data bytea) returns text immutable
		as $x$select encode(in_encrypted_data, 'hex');$x$ language sql;
end
$function$


-- accept_guild_invite(in_invite_id bigint, in_role_id smallint, in_max_guild_count_per_player integer, in_max_members_per_guild integer, in_neutral_faction_id smallint) -> void
-- oid: 58116  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.accept_guild_invite(in_invite_id bigint, in_role_id smallint, in_max_guild_count_per_player integer, in_max_members_per_guild integer, in_neutral_faction_id smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	member_count INTEGER := 0;
	player_id BIGINT := 0;
	found_guild_id BIGINT := 0;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	-- check if invite exists
	SELECT guild_invites.player_id, guild_invites.guild_id FROM guild_invites WHERE invite_id = in_invite_id INTO player_id, found_guild_id;
	IF NOT FOUND THEN
    	RAISE EXCEPTION 'Trying to accept non exiting invite %.', in_invite_id;
	END IF;

	-- delete invite
	DELETE FROM guild_invites WHERE invite_id = in_invite_id;

	SELECT INTO member_count COUNT(*) FROM guild_members where guild_members.guild_id = found_guild_id;

	-- check if we've reached guild member limit
	IF member_count >= in_max_members_per_guild THEN
		RAISE EXCEPTION 'Cannot insert more than % members per guild.', in_max_members_per_guild;
	END IF;

	-- add member
	PERFORM add_guild_member(player_id, found_guild_id, in_role_id, in_max_guild_count_per_player, in_max_members_per_guild, in_neutral_faction_id);
END
$function$


-- accept_party_invite(in_invite_id bigint, in_platform_session_id text, in_max_party_member_count integer) -> dune.partyacceptinviteresult
-- oid: 58117  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.accept_party_invite(in_invite_id bigint, in_platform_session_id text, in_max_party_member_count integer)
 RETURNS dune.partyacceptinviteresult
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_player_id BIGINT;
	out_sender_id BIGINT;
	out_party_id BIGINT;
	out_player_platform_name TEXT;
	out_player_platform_id TEXT;
	out_sender_platform_name TEXT;
	out_sender_platform_session_id TEXT;
	out_accept_error PartyAcceptInviteResult DEFAULT 'Success'::PartyAcceptInviteResult;
BEGIN
	PERFORM parties_get_exclusive_operation_lock();

	-- check if invite exists
	SELECT party_id, player_id, sender_player_id, sender_platform_name, sender_platform_session_id FROM party_invites 
	WHERE invite_id = in_invite_id INTO out_party_id, out_player_id, out_sender_id, out_sender_platform_name, out_sender_platform_session_id;
	IF NOT FOUND THEN
		RAISE EXCEPTION 'Trying to accept non exiting party invite %.', in_invite_id;
		out_accept_error = 'NonExistingInvite'::PartyAcceptInviteResult;
	END IF;
	
	-- query receiver platform data
	SELECT acc.platform_name, acc.platform_id INTO out_player_platform_name, out_player_platform_id
	FROM accounts acc LEFT JOIN player_state ps ON acc.id=ps.account_id
	WHERE ps.player_controller_id = out_player_id;

	IF out_party_id IS NULL THEN
		PERFORM internal_create_party(in_invite_id, out_sender_id, out_sender_platform_session_id, out_sender_platform_name, 
			out_player_id, in_platform_session_id, out_player_platform_name);
	ELSE
		out_accept_error := internal_add_party_member(in_invite_id, out_party_id, out_player_id, in_platform_session_id, out_player_platform_name, in_max_party_member_count);
	END IF;

	-- Delete any sent or reiceived invites from the player who's accepting the invitation
	DELETE FROM party_invites WHERE sender_player_id = out_player_id OR player_id = out_player_id;
	RETURN out_accept_error;
END
$function$


-- add_actor_audit(in_id bigint, in_class text) -> void
-- oid: 58118  kind: FUNCTION  category: anticheat

CREATE OR REPLACE FUNCTION dune.add_actor_audit(in_id bigint, in_class text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO actor_audit("id", "class") VALUES(in_id, in_class) ON CONFLICT(id) DO NOTHING;
END
$function$


-- add_event_log_data(in_game_event_owner bigint, in_universe_time bigint, in_map_name text, in_partition_id bigint, in_event_type integer, in_x_location double precision, in_y_location double precision, in_z_location double precision, in_is_player_facing boolean, in_custom_data text) -> void
-- oid: 58119  kind: FUNCTION  category: event_log

CREATE OR REPLACE FUNCTION dune.add_event_log_data(in_game_event_owner bigint, in_universe_time bigint, in_map_name text, in_partition_id bigint, in_event_type integer, in_x_location double precision, in_y_location double precision, in_z_location double precision, in_is_player_facing boolean, in_custom_data text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO game_events(actor_id, universe_time, map, partition_id, event_type, x, y, z, custom_data, player_facing_event) VALUES(in_game_event_owner, to_timestamp(in_universe_time), in_map_name, in_partition_id, in_event_type, in_x_location, in_y_location, in_z_location, in_custom_data::JsonB, in_is_player_facing);
END
$function$


-- add_event_log_data_batched(in_data dune.eventlogbulkentrydata[]) -> void
-- oid: 58120  kind: FUNCTION  category: event_log

CREATE OR REPLACE FUNCTION dune.add_event_log_data_batched(in_data dune.eventlogbulkentrydata[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	data_entry record = NULL;
BEGIN
	FOREACH data_entry IN ARRAY in_data
	LOOP
		PERFORM add_event_log_data(data_entry.game_event_owner, data_entry.universe_time, data_entry.map_name, data_entry.partition_id, data_entry.event_type,
			data_entry.x_location, data_entry.y_location, data_entry.z_location, data_entry.player_facing_event, data_entry.custom_args);
	END LOOP;
END $function$


-- add_guild_invite(in_player_id bigint, in_guild_id bigint, in_sender_player_id bigint, in_invite_sent_timespan bigint, in_max_guild_invites_per_guild integer) -> void
-- oid: 58121  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.add_guild_invite(in_player_id bigint, in_guild_id bigint, in_sender_player_id bigint, in_invite_sent_timespan bigint, in_max_guild_invites_per_guild integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	invite_count INTEGER := 0;
	out_invite_id INTEGER;
	out_guild_name TEXT;
	out_guild_description TEXT;
	out_player_name TEXT;
	out_sender_name TEXT;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	-- check if guild exists
	SELECT guild_name, guild_description INTO out_guild_name, out_guild_description FROM guilds WHERE guild_id = in_guild_id;
	IF NOT FOUND THEN
    	RAISE EXCEPTION 'Trying to add invite to non existing guild %.', in_guild_id;
	END IF;

	-- check if we've reached the invite limit
	SELECT INTO invite_count COUNT(*) FROM guild_invites WHERE guild_invites.guild_id = in_guild_id;
	IF invite_count >= in_max_guild_invites_per_guild THEN
		RAISE EXCEPTION 'Cannot insert more than % guild invites per guild.', in_max_guild_invites_per_guild;
	END IF;

	-- check if this player already has an invite to this guild
	IF EXISTS (SELECT FROM guild_invites WHERE guild_id = in_guild_id AND player_id = in_player_id) THEN
		RAISE EXCEPTION 'Trying to add invite to a player that already has an invite to this guild.';
	END IF;

	-- add invite
	INSERT INTO guild_invites("guild_id", "player_id", "sender_player_id", "invite_sent_timespan") VALUES(in_guild_id, in_player_id, in_sender_player_id, in_invite_sent_timespan) RETURNING "invite_id" INTO out_invite_id;

	SELECT player_state.character_name INTO out_player_name
	FROM player_state
	WHERE player_state.player_controller_id = in_player_id;

	SELECT player_state.character_name INTO out_sender_name
	FROM player_state
	WHERE player_state.player_controller_id = in_sender_player_id;

	PERFORM pg_notify('guild_notify_channel', format(
	'add_invite#{"InviteId" : %s, "PlayerId" : %s , "GuildId" : %s, "GuildName" : "%s", "PlayerName" : "%s", "GuildDescription" : "%s", "SenderPlayerId" : %s, "SenderPlayerName" : "%s", "InviteSentUniverseTime" : %s}',
	 out_invite_id, in_player_id, in_guild_id, out_guild_name, out_player_name, out_guild_description, in_sender_player_id, out_sender_name, in_invite_sent_timespan));
END
$function$


-- add_guild_member(in_player_id bigint, in_guild_id bigint, in_role_id smallint, in_max_guild_count_per_player integer, in_max_members_per_guild integer, in_neutral_faction_id smallint) -> void
-- oid: 58122  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.add_guild_member(in_player_id bigint, in_guild_id bigint, in_role_id smallint, in_max_guild_count_per_player integer, in_max_members_per_guild integer, in_neutral_faction_id smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	guild_count INTEGER := 0;
	should_clear_invites SMALLINT := 0;
	player_faction_id SMALLINT;
	guild_record record;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	-- we need to check if the player is already part of max amount of guilds before being able to add them
    SELECT INTO guild_count COUNT(*) FROM guild_members WHERE player_id = in_player_id;
    IF guild_count >= in_max_guild_count_per_player THEN
        RAISE EXCEPTION 'Cannot insert more than % guild entries for each user.', in_max_guild_count_per_player;
    END IF;

	SELECT * INTO guild_record
	FROM guilds
	WHERE guild_id = in_guild_id;

	-- check if guild exists
	IF guild_record IS NULL THEN
    	RAISE EXCEPTION 'Trying to add user to non existing guild %.', in_guild_id;
	END IF;

	player_faction_id := get_player_faction(in_player_id, in_neutral_faction_id);

	IF  player_faction_id != in_neutral_faction_id AND guild_record.guild_faction != in_neutral_faction_id AND player_faction_id != guild_record.guild_faction THEN
		RAISE EXCEPTION 'Trying to add user to with non compatible. player faction: %, guild faction: %', player_faction_id, guild_record.guild_faction;
	END IF;

	IF (SELECT COUNT(*) FROM guild_members where guild_members.guild_id = in_guild_id) = in_max_members_per_guild - 1 THEN
		should_clear_invites := 1;
	END IF;

	-- insert member
	INSERT INTO guild_members("player_id", "guild_id", "role_id") VALUES(in_player_id, in_guild_id, in_role_id);

	-- delete invite
    IF should_clear_invites = 1 THEN
        DELETE FROM guild_invites WHERE guild_id = in_guild_id;
    ELSE
	    DELETE FROM guild_invites WHERE guild_id = in_guild_id AND player_id = in_player_id;
    END IF;

	PERFORM pg_notify('guild_notify_channel', format('add_player#{"PlayerId" : %s , "PlayerFactionId" : %s, "GuildId" : %s, "RoleId" : %s, "ShouldClearInvites" : %s}', in_player_id, player_faction_id, in_guild_id, in_role_id, should_clear_invites));
END
$function$


-- add_landclaim_segment(in_totem_id bigint, in_grid_location_x bigint, in_grid_location_y bigint) -> void
-- oid: 58123  kind: FUNCTION  category: landclaim

CREATE OR REPLACE FUNCTION dune.add_landclaim_segment(in_totem_id bigint, in_grid_location_x bigint, in_grid_location_y bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO landclaim_segments(totem_id, grid_location_x, grid_location_y)
    VALUES(in_totem_id, in_grid_location_x, in_grid_location_y);
END; $function$


-- add_map_areas_surveyed_items(in_account_id bigint, in_area_id smallint, in_survey_point_marker_id bigint, in_surveyed_items_target jsonb, in_surveyed_items_progress jsonb, in_map_name text) -> void
-- oid: 58124  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.add_map_areas_surveyed_items(in_account_id bigint, in_area_id smallint, in_survey_point_marker_id bigint, in_surveyed_items_target jsonb, in_surveyed_items_progress jsonb, in_map_name text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO map_areas("account_id", "area_id", "survey_point_marker_id", "items_surveyed_target", "items_surveyed_progress", "map_name")
        VALUES(in_account_id, in_area_id, in_survey_point_marker_id, in_surveyed_items_target, in_surveyed_items_progress, in_map_name)
        ON CONFLICT ("account_id", "area_id", "map_name") DO UPDATE SET "survey_point_marker_id" = in_survey_point_marker_id, "items_surveyed_target" = in_surveyed_items_target, "items_surveyed_progress" = in_surveyed_items_progress
        WHERE map_areas.account_id = in_account_id AND map_areas.area_id = in_area_id and map_areas.map_name = in_map_name;
END;
$function$


-- add_map_areas_time_discovered(in_account_id bigint, in_area_id smallint, in_time_discovered timestamp without time zone, in_map_name text) -> void
-- oid: 58125  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.add_map_areas_time_discovered(in_account_id bigint, in_area_id smallint, in_time_discovered timestamp without time zone, in_map_name text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO map_areas("account_id", "area_id", "time_discovered", "map_name")
        VALUES(in_account_id, in_area_id, in_time_discovered, in_map_name)
        ON CONFLICT ("account_id", "area_id", "map_name") DO UPDATE SET "time_discovered" = in_time_discovered
        WHERE map_areas.account_id = in_account_id AND map_areas.area_id = in_area_id and map_areas.map_name = in_map_name;
END;
$function$


-- add_map_areas_time_first_entered(in_account_id bigint, in_area_id smallint, in_time_first_entered timestamp without time zone, in_map_name text) -> void
-- oid: 58126  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.add_map_areas_time_first_entered(in_account_id bigint, in_area_id smallint, in_time_first_entered timestamp without time zone, in_map_name text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO map_areas("account_id", "area_id", "time_first_entered", "map_name")
        VALUES(in_account_id, in_area_id, in_time_first_entered, in_map_name)
        ON CONFLICT ("account_id", "area_id", "map_name") DO UPDATE SET "time_first_entered" = in_time_first_entered
        WHERE map_areas.account_id = in_account_id AND map_areas.area_id = in_area_id and map_areas.map_name = in_map_name;
END;
$function$


-- add_partition_unique(in_map text, in_definition jsonb, in_dimension bigint, in_label text) -> bigint
-- oid: 58127  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.add_partition_unique(in_map text, in_definition jsonb, in_dimension bigint, in_label text DEFAULT NULL::text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	v_partition_id bigint;
BEGIN
	-- Don't use a constraint right now, this is only a dev-only helper function.
	-- We could add a constraint, but would need to check the performance of constraint on the jsonb field.
	insert into world_partition (map, partition_definition, dimension_index, label)
		select in_map, in_definition, in_dimension, in_label
	where not exists (
		select 1 from world_partition where map = in_map and partition_definition = in_definition and dimension_index = in_dimension
	) returning partition_id into v_partition_id;
	return v_partition_id;
END;
$function$


-- add_party_invite(in_sender_player_id bigint, in_sender_platform_name text, in_sender_platform_session_id text, in_player_id bigint, in_max_party_member_count integer, in_invite_sent_timespan bigint) -> void
-- oid: 58128  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.add_party_invite(in_sender_player_id bigint, in_sender_platform_name text, in_sender_platform_session_id text, in_player_id bigint, in_max_party_member_count integer, in_invite_sent_timespan bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_already_in_party BOOLEAN;
	out_already_invited BOOLEAN;
	out_sender_existing_party_id BIGINT;
	out_sender_party_count INTEGER;
	out_invite_id BIGINT;
	out_player_name TEXT;
	out_sender_name TEXT;
BEGIN

	-- check if invited player is already invited
	SELECT INTO out_already_invited EXISTS (SELECT 1 FROM party_invites where player_id = in_player_id AND sender_player_id = in_sender_player_id);
	IF out_already_invited THEN
		RAISE EXCEPTION 'The player % already has an invite from %.', in_player_id, in_sender_player_id;
	END IF;

	-- check if the sender party is full
	SELECT party_id INTO out_sender_existing_party_id from parties where party_leader_id = in_sender_player_id;
	SELECT INTO out_sender_party_count COUNT(*) FROM party_members WHERE party_id = out_sender_existing_party_id;

	IF out_sender_existing_party_id IS NOT NULL AND out_sender_party_count >= in_max_party_member_count THEN
		RAISE EXCEPTION 'Trying to invite player % for a party id % that is full.', in_player_id, out_sender_existing_party_id;
	END IF;

	-- add invite
	INSERT INTO party_invites("player_id", "party_id", "sender_player_id", "sender_platform_name", "sender_platform_session_id", "invite_sent_timespan") 
		VALUES(in_player_id, out_sender_existing_party_id, in_sender_player_id, in_sender_platform_name, in_sender_platform_session_id, in_invite_sent_timespan) RETURNING "invite_id" INTO out_invite_id;

	SELECT player_state.character_name INTO out_player_name
		FROM player_state WHERE player_state.player_controller_id = in_player_id;

	SELECT player_state.character_name INTO out_sender_name
		FROM player_state WHERE player_state.player_controller_id = in_sender_player_id;

	PERFORM pg_notify('party_notify_channel', format(
		'add_invite#{"InviteId" : %s, "SenderId" : %s, "SenderName" : "%s", "PlayerId" : %s , "PlayerName" : "%s", "InviteSentUniverseTime" : %s}',
		out_invite_id, in_sender_player_id, out_sender_name, in_player_id, out_player_name, in_invite_sent_timespan));
END
$function$


-- adjust_player_virtual_currency_balance(in_controller_id bigint, in_currency_id smallint, in_delta bigint) -> bigint
-- oid: 58129  kind: FUNCTION  category: currency

CREATE OR REPLACE FUNCTION dune.adjust_player_virtual_currency_balance(in_controller_id bigint, in_currency_id smallint, in_delta bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	current_balance BIGINT;
	current_delta BIGINT;
	new_delta BIGINT;
	fls_id TEXT;
	function_oid oid;
BEGIN
	SELECT INTO current_balance balance from player_virtual_currency_balances WHERE player_controller_id = in_controller_id AND currency_id = in_currency_id;
	INSERT INTO player_virtual_currency_balances("player_controller_id", "currency_id", "balance")
		VALUES (in_controller_id, in_currency_id, in_delta)
		ON CONFLICT (player_controller_id, currency_id) DO UPDATE SET balance = (player_virtual_currency_balances.balance + in_delta)
		RETURNING balance INTO current_balance;

    IF in_currency_id = get_solaris_id() THEN
	    GET DIAGNOSTICS function_oid = PG_ROUTINE_OID;
	    PERFORM log_event_solaris(function_oid, 'update_solaris', in_controller_id, current_balance, in_delta);
    END IF;

	current_delta = 0;
	IF current_balance < 0 THEN
		SELECT acc."user"
		INTO fls_id
		FROM accounts acc
		JOIN player_state ps on ps.account_id = acc.id
		WHERE ps.account_id = in_player_id
		LIMIT 1;

		PERFORM log_cheating(COALESCE(fls_id, in_player_id::text), 'negative_solaris');

		INSERT INTO player_virtual_currency_balances("player_controller_id", "currency_id", "balance")
			VALUES (in_controller_id, in_currency_id, 0)
			ON CONFLICT (player_controller_id, currency_id) DO UPDATE SET balance = 0;
		current_delta = current_balance;
	END IF;

	new_delta = in_delta + current_delta;
	RETURN new_delta;
END;
$function$


-- admin_get_character_details(in_account_id bigint) -> TABLE(account_id bigint, player_id text, character_name text, online_status text, last_avatar_activity timestamp with time zone, class text, map text, transform dune.transform, server_id text, partition_id bigint, partition_label text, dimension_index integer, gas_attributes jsonb, properties jsonb, slot_name text, fgl_data text)
-- oid: 58130  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.admin_get_character_details(in_account_id bigint)
 RETURNS TABLE(account_id bigint, player_id text, character_name text, online_status text, last_avatar_activity timestamp with time zone, class text, map text, transform dune.transform, server_id text, partition_id bigint, partition_label text, dimension_index integer, gas_attributes jsonb, properties jsonb, slot_name text, fgl_data text)
 LANGUAGE plpgsql
AS $function$
begin
	return query SELECT player_state.account_id, accounts.user As player_id, player_state.character_name, CASE WHEN is_player_offline(accounts.user) THEN 'Offline' ELSE 'Online' END AS online_status, player_state.last_avatar_activity,
		actors.class, actors.map, actors.transform, player_state.server_id, actors.partition_id, world_partition.label, actors.dimension_index,
		actors.gas_attributes, actors.properties, actor_fgl_entities.slot_name,
		string_agg(cast(to_json((select d from (select fgl_entities.components) d)) as varchar), ', ') as fgl_data
	from fgl_entities
	left join actor_fgl_entities on fgl_entities.entity_id = actor_fgl_entities.entity_id
	left join actors on actor_fgl_entities.actor_id = actors.id
	left join player_state on player_state.player_pawn_id = actors.id
	left join accounts on accounts.id = player_state.account_id
	left join world_partition on world_partition.partition_id = actors.partition_id
	where player_state.account_id = in_account_id
		and actor_fgl_entities.slot_name = 'DuneCharacter'
	group by player_state.account_id, player_state.character_name, player_state.last_avatar_activity, accounts.user,
		actors.class, actors.map, actors.transform, player_state.server_id, actors.partition_id, world_partition.label, actors.dimension_index,
		actors.gas_attributes, actors.properties, actor_fgl_entities.slot_name;
end
$function$


-- admin_get_character_ids(in_search_term text) -> TABLE(id bigint, "user" text, character_name text)
-- oid: 58131  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.admin_get_character_ids(in_search_term text)
 RETURNS TABLE(id bigint, "user" text, character_name text)
 LANGUAGE plpgsql
AS $function$
begin
	return query
	select accounts.id, accounts.user, player_state.character_name
	from accounts
	left join player_state on player_state.account_id = accounts.id
	where lower(accounts.user) like in_search_term or lower(player_state.character_name) like in_search_term;
end
$function$


-- admin_get_inventory_details(in_account_id bigint) -> TABLE(inventory_id bigint, item_id bigint, stack_size integer, template_id text, acquisition_time bigint)
-- oid: 58132  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.admin_get_inventory_details(in_account_id bigint)
 RETURNS TABLE(inventory_id bigint, item_id bigint, stack_size integer, template_id text, acquisition_time bigint)
 LANGUAGE sql
AS $function$
	select
		inventories.id as inventory_id, items.id as item_id, items.stack_size, items.template_id, items.acquisition_time
	from
		items
	left join
		inventories on inventories.id = items.inventory_id
	left join
		player_state on inventories.actor_id = player_state.player_pawn_id
	where
		player_state.account_id = in_account_id
	order by
		template_id;
$function$


-- admin_get_journey_details(in_player_id text, in_story_node_id text) -> TABLE(out_story_node_id text, out_override_reward_block boolean, out_has_pending_reward boolean, out_complete_condition_state jsonb, out_reveal_condition_state jsonb, out_fail_condition_state jsonb, out_metadata_state jsonb, out_reset_group dune.journeystoryresetgroup)
-- oid: 58133  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.admin_get_journey_details(in_player_id text, in_story_node_id text DEFAULT '%%'::text)
 RETURNS TABLE(out_story_node_id text, out_override_reward_block boolean, out_has_pending_reward boolean, out_complete_condition_state jsonb, out_reveal_condition_state jsonb, out_fail_condition_state jsonb, out_metadata_state jsonb, out_reset_group dune.journeystoryresetgroup)
 LANGUAGE sql
AS $function$
	select story_node_id, override_reward_block, has_pending_reward, complete_condition_state, reveal_condition_state, fail_condition_state, metadata_state, reset_group
	from journey_story_node
	where story_node_id like in_story_node_id
	and account_id in (
		select id
		from accounts a
		where a.user = in_player_id
	);
$function$


-- admin_get_mnemonic_recall_details(in_account_id bigint) -> TABLE(mnemonic_recall_id bigint, lesson_id text, lesson_state bigint, lesson_progress integer)
-- oid: 58134  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.admin_get_mnemonic_recall_details(in_account_id bigint)
 RETURNS TABLE(mnemonic_recall_id bigint, lesson_id text, lesson_state bigint, lesson_progress integer)
 LANGUAGE sql
AS $function$
	select
		mnemonic_recall.id, lesson_id, lesson_state, lesson_progress
	from
		mnemonic_recall
	where
		mnemonic_recall.account_id = in_account_id
$function$


-- admin_get_partitions() -> TABLE(out_partition_id bigint, out_server_id text, out_partition_definition jsonb, out_dimension_index integer, out_blocked boolean, out_label text, out_map text)
-- oid: 58135  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.admin_get_partitions()
 RETURNS TABLE(out_partition_id bigint, out_server_id text, out_partition_definition jsonb, out_dimension_index integer, out_blocked boolean, out_label text, out_map text)
 LANGUAGE sql
AS $function$
	select * from load_partition_definition_map();
$function$


-- admin_move_offline_player(in_fls_id text, in_target_partition_name text, in_target_location dune.vector) -> void
-- oid: 58136  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.admin_move_offline_player(in_fls_id text, in_target_partition_name text, in_target_location dune.vector)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	if not (select is_player_offline(in_fls_id)) then
		raise exception 'Player must be Offline';
	end if;

	if not exists(select 1 from world_partition where label = in_target_partition_name) then
		raise exception 'Partition with name % not found', in_target_partition_name;
	end if;

	perform (with target_partition as (
		select partition_id, map, dimension_index
		from world_partition
		where label = in_target_partition_name
		limit 1
	)
	select admin_move_offline_player_to_partition(in_fls_id, target_partition.partition_id, in_target_location)
	from target_partition);
end
$function$


-- admin_move_offline_player_to_partition(in_fls_id text, in_target_partition_id bigint, in_target_location dune.vector) -> void
-- oid: 58137  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.admin_move_offline_player_to_partition(in_fls_id text, in_target_partition_id bigint, in_target_location dune.vector)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	if not (select is_player_offline(in_fls_id)) then
		-- CAVEAT: DirectorDbApi.TryMoveOfflinePlayerToPartition depends on this string.
		raise exception 'Player must be Offline';
	end if;

	if not exists(select 1 from world_partition where partition_id = in_target_partition_id) then
		raise exception 'Partition with ID % not found', in_target_partition_id;
	end if;

	raise notice 'Moving player % to partition % location x=%, y=%, z=%', in_fls_id, in_target_partition_id, in_target_location.x, in_target_location.y, in_target_location.z;

	with target_partition as (
		select partition_id, map, dimension_index
		from world_partition
		where partition_id = in_target_partition_id
		limit 1
	), target_pawn_id as (
		select player_state.player_pawn_id as id
		from accounts, player_state
		where accounts.user = in_fls_id and accounts.id = player_state.account_id
		limit 1
	), update_overmap_player_location as (
		select
			case 
		        when target_partition.map = 'Overmap' then overmap_save_player_survival_data(target_pawn_id.id, null, false, in_target_location)
		        else null
    		end
		from target_pawn_id, target_partition
	)
	update actors
		set
			transform = (in_target_location, (transform).rotation),
			map = upgrade_map_name(target_partition.map),
			dimension_index = target_partition.dimension_index,
			partition_id = target_partition.partition_id
		from
			target_pawn_id, target_partition, update_overmap_player_location
		where actors.id = target_pawn_id.id;
end
$function$


-- admin_read_player_tags(in_account_id bigint) -> TABLE(tags text)
-- oid: 58138  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.admin_read_player_tags(in_account_id bigint)
 RETURNS TABLE(tags text)
 LANGUAGE sql
AS $function$
	select
		pt.tag
	from
		player_tags as pt
	where
		pt.account_id = in_account_id
$function$


-- advance_items_id_sequencer(count bigint) -> bigint
-- oid: 58139  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.advance_items_id_sequencer(count bigint DEFAULT 1)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
  next_val  BIGINT;
  next_free BIGINT;
BEGIN
  IF count < 1 THEN
    RAISE EXCEPTION 'count must be >= 1';
  END IF;

  PERFORM pg_advisory_xact_lock(('items_id_seq'::regclass)::oid::bigint);
  
  next_val := nextval('items_id_seq'::regclass);

  -- next free id after reserving `count` ids starting at next_val
  next_free := next_val + count;

  -- make the next nextval() return next_free
  PERFORM setval('items_id_seq'::regclass, next_free, false);

  RETURN next_val;
END $function$


-- assign_actor_id(in_class text) -> bigint
-- oid: 58140  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.assign_actor_id(in_class text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
    new_id BIGINT;
BEGIN
	INSERT INTO actors(id) VALUES(DEFAULT) RETURNING id INTO new_id;
	PERFORM add_actor_audit(new_id, in_class);

	RETURN new_id;
END
$function$


-- base_backup_delete(in_base_backup_id bigint) -> void
-- oid: 58141  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_delete(in_base_backup_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    actors_to_destroy BIGINT[];
BEGIN
    DELETE FROM actors a WHERE id = ANY(
        SELECT actor_id
        FROM base_backup_linked_actors bbla
        WHERE bbla.id = in_base_backup_id
    );

    DELETE FROM base_backups WHERE id = in_base_backup_id;
END
$function$


-- base_backup_find_totems_from_player_owner(in_player_id bigint) -> TABLE(totem_id bigint)
-- oid: 58142  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_find_totems_from_player_owner(in_player_id bigint)
 RETURNS TABLE(totem_id bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
        SELECT t.id
        FROM totems t
            JOIN permission_actor_rank par ON par.permission_actor_id = t.id
            WHERE par.player_id = in_player_id AND par.rank = 1;
END;
$function$


-- base_backup_finish_placing(in_base_backup_id bigint) -> void
-- oid: 58143  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_finish_placing(in_base_backup_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    WITH base_info AS (
        SELECT
            bb.id AS base_backup_id,
            a.partition_id,
            a.dimension_index,
            a.map
        FROM
            base_backups bb
            JOIN actors a on bb.player_id = a.id
        WHERE
            bb.id = in_base_backup_id
    )
    UPDATE actors
    SET
        partition_id = base_info.partition_id,
        dimension_index = base_info.dimension_index,
        map = base_info.map
    FROM
        base_backup_linked_actors bbl
        JOIN base_info ON bbl.id = base_info.base_backup_id
    WHERE
        actors.id = bbl.actor_id;

    DELETE FROM actor_state a
        WHERE actor_id = ANY(
            SELECT actor_id
            FROM base_backup_linked_actors bbla
            WHERE bbla.id = in_base_backup_id
        );

    DELETE FROM base_backups
        WHERE id = in_base_backup_id;
END
$function$


-- base_backup_get_actors_to_spawn(in_base_backup_id bigint) -> SETOF dune.actorspawninfo
-- oid: 58144  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_get_actors_to_spawn(in_base_backup_id bigint)
 RETURNS SETOF dune.actorspawninfo
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
		SELECT a.id, a.class as class_name, a.transform, a.partition_id, a.dimension_index
        FROM actors as a
        WHERE a.id IN (
            SELECT actor_id FROM base_backup_linked_actors as bbla WHERE bbla.id = in_base_backup_id
        );
END
$function$


-- base_backup_get_available_backups(in_player_id bigint) -> TABLE(id bigint, base_backup_name text, totem_id bigint, totem_buildable_type text, landclaim_original_global_location real[], base_backup_map text)
-- oid: 58145  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_get_available_backups(in_player_id bigint)
 RETURNS TABLE(id bigint, base_backup_name text, totem_id bigint, totem_buildable_type text, landclaim_original_global_location real[], base_backup_map text)
 LANGUAGE plpgsql
AS $function$
begin
    RETURN QUERY
    SELECT
        bb.id,
        bb.base_backup_name,
        t.id AS totem_id,
        p.building_type,
        t.landclaim_original_global_location,
        a.map
    FROM base_backups bb
        JOIN base_backup_linked_actors bbla ON bbla.id = bb.id
        JOIN totems t ON bbla.actor_id = t.id
        JOIN actors a ON a.id = t.id
        JOIN placeables p ON p.id = a.id
    WHERE bb.player_id = in_player_id;
END
$function$


-- base_backup_get_buildable_data(in_base_backup_id bigint) -> TABLE(buildable_type text, total_count integer)
-- oid: 58146  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_get_buildable_data(in_base_backup_id bigint)
 RETURNS TABLE(buildable_type text, total_count integer)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT t.buildable_type, SUM(t.cnt)::INT AS total_count
    FROM (
        SELECT bi.building_type AS buildable_type, COUNT(*) AS cnt
        FROM base_backup_linked_actors bla
        JOIN building_instances bi ON bla.actor_id = bi.building_id
        WHERE
            bla.id = in_base_backup_id AND
            (bi.building_flags IS NULL OR (bi.building_flags & (1 << 2) = 0 AND bi.building_flags & (1 << 7) = 0)) -- flag 2 and 7 not enabled, which relates to holograms and extensions
        GROUP BY bi.building_type

        UNION ALL

        SELECT p.building_type AS buildable_type, COUNT(*) AS cnt
        FROM base_backup_linked_actors bla
        JOIN placeables p ON bla.actor_id = p.id
        WHERE bla.id = in_base_backup_id AND p.is_hologram = FALSE
        GROUP BY p.building_type
    ) t
    GROUP BY t.buildable_type;
END
$function$


-- base_backup_get_data(in_base_backup_id bigint) -> dune.getbasebackupdata
-- oid: 58147  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_get_data(in_base_backup_id bigint)
 RETURNS dune.getbasebackupdata
 LANGUAGE plpgsql
AS $function$
DECLARE
    base_backup_name TEXT;
    totem_data BaseBackupTotemData;
    buildings_array BaseBackupBuildingItem[];
    placeables_array BaseBackupPlaceableItem[];
BEGIN

    SELECT bb.base_backup_name
        INTO base_backup_name
        FROM base_backups bb
        WHERE bb.id = in_base_backup_id;

    totem_data := base_backup_get_totem_data(in_base_backup_id);

    -- building pieces
    SELECT array_agg((bi.building_id, bi.instance_id, bi.building_type, bi.transform, bi.building_flags)::BaseBackupBuildingItem)
		into buildings_array
		FROM
            building_instances bi
            JOIN base_backup_linked_actors bbla ON bi.building_id = bbla.actor_id
		WHERE bbla.id = in_base_backup_id;

    -- placeables
    SELECT array_agg((p.building_type, a.transform)::BaseBackupPlaceableItem)
		into placeables_array
		FROM
            placeables p
            JOIN actors a ON p.id = a.id
            JOIN base_backup_linked_actors bbla ON a.id = bbla.actor_id
		WHERE
            bbla.id = in_base_backup_id;

    return ROW(base_backup_name, totem_data, buildings_array, placeables_array)::GetBaseBackupData;
END
$function$


-- base_backup_get_totem_data(in_base_backup_id bigint) -> dune.basebackuptotemdata
-- oid: 58148  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_get_totem_data(in_base_backup_id bigint)
 RETURNS dune.basebackuptotemdata
 LANGUAGE plpgsql
AS $function$
DECLARE
    totem_id BIGINT;
    result BaseBackupTotemData;
BEGIN
    SELECT t.id
        INTO totem_id
        FROM totems t JOIN base_backup_linked_actors bbla ON t.id = bbla.actor_id
        WHERE bbla.id = in_base_backup_id
        LIMIT 1;

    IF totem_id IS NULL THEN
        RAISE EXCEPTION 'No totem found for base_backup id %', in_base_backup_id;
    END IF;

    result := base_backup_get_totem_data_from_totem_id(totem_id);

    RETURN result;
END;
$function$


-- base_backup_get_totem_data_from_totem_id(in_totem_id bigint) -> dune.basebackuptotemdata
-- oid: 58149  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_get_totem_data_from_totem_id(in_totem_id bigint)
 RETURNS dune.basebackuptotemdata
 LANGUAGE plpgsql
AS $function$
DECLARE
    result BaseBackupTotemData;
BEGIN
    SELECT
        t.id,
        p.building_type,
        a.map,
        t.landclaim_original_global_location,
        t.landclaim_original_global_yaw_rotation,
        t.landclaim_vertical_level
    INTO
        result.totem_actor_id,
        result.totem_building_type,
        result.totem_map,
        result.landclaim_original_global_location,
        result.landclaim_original_global_yaw_rotation,
        result.landclaim_vertical_level
    FROM totems t
        JOIN placeables p ON p.id = t.id
        JOIN actors a ON a.id = t.id
    WHERE t.id = in_totem_id
    LIMIT 1;

    IF result.totem_actor_id IS NULL THEN
        RAISE EXCEPTION 'No totem found for totem_id %', in_totem_id;
    END IF;

    SELECT array_agg(ROW(grid_location_x, grid_location_y)::SMALLINTPOINT)
        INTO result.landclaim_grid
        FROM landclaim_segments s
        WHERE s.totem_id = result.totem_actor_id;

    RETURN result;
END;
$function$


-- base_backup_get_totem_id(backup_id bigint) -> bigint
-- oid: 58150  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_get_totem_id(backup_id bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
    result BIGINT;
BEGIN
    SELECT t.id
        INTO result
        FROM totems t
            JOIN base_backup_linked_actors bbla ON t.id = bbla.actor_id
        WHERE bbla.id = backup_id
        LIMIT 1;

    IF result IS NULL THEN
        RAISE EXCEPTION 'No totem found for base_backup id %', backup_id;
    END IF;

    RETURN result;
END;
$function$


-- base_backup_recycle(in_base_backup_id bigint, in_target_inventory_id bigint) -> integer
-- oid: 58151  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_recycle(in_base_backup_id bigint, in_target_inventory_id bigint)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
    base_backup_items_moved INT;
BEGIN
    UPDATE items
    SET inventory_id = in_target_inventory_id
    FROM
        inventories inv
        JOIN base_backup_linked_actors bbla ON inv.actor_id = bbla.actor_id
    WHERE
        items.inventory_id = inv.id
        AND bbla.id = in_base_backup_id;

    get diagnostics base_backup_items_moved = ROW_COUNT;

    -- Re-organize the index of all the items
    UPDATE items
    SET position_index = new_index
    FROM (
        SELECT
            id,
            ROW_NUMBER() OVER (ORDER BY position_index) - 1 AS new_index
        FROM items
        WHERE inventory_id = in_target_inventory_id
    ) AS sub
    WHERE items.id = sub.id;

    PERFORM base_backup_delete(in_base_backup_id);

    return base_backup_items_moved;
END
$function$


-- base_backup_save(in_player_actor_id bigint, in_base_backup_name text, in_building_pieces_to_link dune.basebackupbuildingitem[], in_placeables_to_link bigint[], in_placeables_to_remove_totem_owner bigint[]) -> bigint
-- oid: 58152  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_save(in_player_actor_id bigint, in_base_backup_name text, in_building_pieces_to_link dune.basebackupbuildingitem[], in_placeables_to_link bigint[], in_placeables_to_remove_totem_owner bigint[])
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_backup_id BIGINT;
    totem_id BIGINT;
BEGIN
    -- Find and Validate the Totem exists in the list
    SELECT t.id INTO totem_id
        FROM totems t
            JOIN unnest(in_placeables_to_link) AS ai(actor_id) ON t.id = ai.actor_id
        LIMIT 1;

    IF totem_id IS NULL THEN
        RAISE EXCEPTION 'No totem found for base_backup_save';
    END IF;

    INSERT INTO base_backups(player_id, base_backup_name)
        VALUES (in_player_actor_id, in_base_backup_name) RETURNING id INTO v_backup_id;

    -- for each building_id, create a new actor for those building pieces and then assign that new building_id to the building pieces.
    with
        input as (select DISTINCT building_id from unnest(in_building_pieces_to_link)),
        instances_input as (select building_id, instance_id from unnest(in_building_pieces_to_link)),
        new_actor_ids as (select nextval('actors_id_seq') as new_id, building_id as old_id from input),
        _copy_building_actors as (
            insert into actors("id", "class", "map", "transform", "partition_id", "dimension_index")
            select i.new_id, a."class", a."map", a."transform", a."partition_id", a."dimension_index"
            from new_actor_ids i join actors a on (i.old_id = a.id)),
        _insert_actor_states as (insert into actor_state(actor_id, state) select new_id, 'BaseBackup' from new_actor_ids),
        _insert_buildings as (insert into buildings("id") select new_id from new_actor_ids),
        _insert_base_backup_linked_actors as (insert into base_backup_linked_actors("id", "actor_id") select v_backup_id, new_id from new_actor_ids)
    update building_instances bi set building_id = ids.new_id from new_actor_ids ids join instances_input i on (ids.old_id = i.building_id) where bi.building_id = ids.old_id and bi.instance_id = i.instance_id;

    -- Link all placeables to the linked_base_backup_id and set them to BaseBackup ActorState in actor_state
    INSERT INTO base_backup_linked_actors(id, actor_id)
        SELECT v_backup_id, unnest(in_placeables_to_link);

    INSERT INTO actor_state(actor_id, state)
        SELECT unnest(in_placeables_to_link), 'BaseBackup'::ActorState;

    UPDATE placeables
        SET owner_entity_id = NULL
        WHERE id = ANY(in_placeables_to_remove_totem_owner);

    -- We need to remove permissions for the Totem
    PERFORM permission_actor_destroy(totem_id);

    -- Remove all invoices from the totem
    PERFORM taxation_remove_invoices_from_totem(totem_id);

    RETURN v_backup_id;
END
$function$


-- base_backup_save_all_totems_from_player_owner(in_player_id bigint) -> TABLE(base_backup_id bigint)
-- oid: 58153  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_save_all_totems_from_player_owner(in_player_id bigint)
 RETURNS TABLE(base_backup_id bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
        SELECT base_backup_save_from_totem(in_player_id, totem_id)
        FROM base_backup_find_totems_from_player_owner(in_player_id);
END;
$function$


-- base_backup_save_from_totem(in_player_id bigint, totem_id bigint) -> bigint
-- oid: 58154  kind: FUNCTION  category: base_backup

CREATE OR REPLACE FUNCTION dune.base_backup_save_from_totem(in_player_id bigint, totem_id bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
    totem_entity_id BIGINT;
    totem_name TEXT;
    building_pieces_to_link BaseBackupBuildingItem[];
    placeables_to_link BIGINT[];
    placeables_to_remove_totem_owner BIGINT[];
BEGIN
    SELECT entity_id INTO totem_entity_id
        FROM actor_fgl_entities
        WHERE actor_id = totem_id;

    SELECT COALESCE(actor_name, '') INTO totem_name
        FROM permission_actor
        WHERE actor_id = totem_id
        LIMIT 1;

    SELECT array_agg((bi.building_id, bi.instance_id, bi.building_type, bi.transform, bi.building_flags)::BaseBackupBuildingItem)
        INTO building_pieces_to_link
        FROM building_instances bi
        WHERE bi.owner_entity_id = totem_entity_id;

    SELECT array_agg(p.id)
        INTO placeables_to_link
        FROM placeables p
        WHERE (p.owner_entity_id = totem_entity_id AND p.has_buildable_support = TRUE) OR p.id = totem_id;

    SELECT array_agg(p.id)
        INTO placeables_to_remove_totem_owner
        FROM placeables p
        WHERE p.owner_entity_id = totem_entity_id AND p.has_buildable_support = FALSE AND p.id != totem_id;

    RETURN base_backup_save(
        in_player_id,
        totem_name,
        building_pieces_to_link,
        placeables_to_link,
        placeables_to_remove_totem_owner
    );
END;
$function$


-- break_guild_allegiance(in_guild_id bigint, in_neutral_faction_id smallint) -> void
-- oid: 58155  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.break_guild_allegiance(in_guild_id bigint, in_neutral_faction_id smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	guild_data_record record;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	SELECT * INTO guild_data_record FROM guilds WHERE guild_id = in_guild_id;
	IF guild_data_record IS NULL THEN
		RAISE EXCEPTION 'Trying to break guild allegiance of a non existing guild: %', in_guild_id;
	END IF;

	if guild_data_record.guild_faction = in_neutral_faction_id THEN
		RAISE EXCEPTION 'Guild already has neutral faction';
	END IF;

	UPDATE guilds SET guild_faction = in_neutral_faction_id WHERE guilds.guild_id = in_guild_id;

	PERFORM pg_notify('guild_notify_channel', format('break_guild_allegiance#{"GuildId" : %s , "OldGuildFactionDbId" : %s, "NewGuildFactionDbId" : %s}', in_guild_id, guild_data_record.guild_faction, in_neutral_faction_id));
END
$function$


-- can_takeover_account(in_user_id text) -> boolean
-- oid: 58156  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.can_takeover_account(in_user_id text)
 RETURNS boolean
 LANGUAGE sql
AS $function$
    select takeoverable from accounts WHERE "user" = in_user_id;
$function$


-- change_player_faction(in_player_id bigint, in_faction_id smallint, neutral_faction_id smallint, in_utc_time_faction_change timestamp without time zone) -> void
-- oid: 58157  kind: FUNCTION  category: faction

CREATE OR REPLACE FUNCTION dune.change_player_faction(in_player_id bigint, in_faction_id smallint, neutral_faction_id smallint, in_utc_time_faction_change timestamp without time zone)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	found_role_id SMALLINT;
BEGIN

	if in_faction_id = neutral_faction_id THEN
		DELETE FROM player_faction WHERE actor_id = in_player_id;

		PERFORM pg_notify('faction_notify_channel', format('remove_player#{"PlayerId" : %s}', in_player_id));
	ELSE
		INSERT INTO player_faction(actor_id, faction_id, utc_time_faction_change) VALUES(in_player_id, in_faction_id, in_utc_time_faction_change) 
			ON CONFLICT (actor_id) DO UPDATE SET faction_id = in_faction_id, utc_time_faction_change = in_utc_time_faction_change;

		PERFORM pg_notify('faction_notify_channel', format('add_player#{"PlayerId" : %s, "FactionId" : %s}', in_player_id, in_faction_id));
	END IF;

	PERFORM handle_player_faction_guild_effects(in_player_id, in_faction_id, neutral_faction_id);
END
$function$


-- character_migration_export(in_fls_id text) -> jsonb
-- oid: 58158  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.character_migration_export(in_fls_id text)
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$
DECLARE
	v_player_controller_id BigInt;
BEGIN
	select out_player_controller_id
		into v_player_controller_id
	from _character_transfer_pre_export_validation(in_fls_id);

	perform _character_transfer_store_in_world_owned_vehicles_into_recovery(v_player_controller_id);
	perform base_backup_save_all_totems_from_player_owner(v_player_controller_id);
	-- Add any other migration-specific actions here before the export call

	return character_transfer_export(in_fls_id);
END;
$function$


-- character_migration_import(in_data jsonb, in_fls_id text, in_character_name text) -> bigint
-- oid: 58159  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.character_migration_import(in_data jsonb, in_fls_id text, in_character_name text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
BEGIN
	-- Add any migration-specific actions here before the import call
	RETURN character_transfer_import(in_data, in_fls_id, in_character_name);
END
$function$


-- character_transfer_export(in_fls_id text) -> jsonb
-- oid: 58160  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.character_transfer_export(in_fls_id text)
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_acc_id BigInt;
    v_funcom_id Text;
	v_player_controller_id BigInt;
	v_player_pawn_id BigInt;
	v_vehicle_ids BigInt[];
BEGIN
	select out_acc_id, out_funcom_id, out_player_controller_id, out_player_pawn_id
		into v_acc_id, v_funcom_id, v_player_controller_id, v_player_pawn_id
	from _character_transfer_pre_export_validation(in_fls_id);

	perform _character_transfer_create_data_table();

	-- accounts

    insert into pg_temp.export_data("id", "kind", "data")
        select id, 'acc', _character_transfer_top_level_export('acc', to_jsonb(accounts))
			from accounts where accounts.id=v_acc_id;

	-- Vehicle and player actors

	insert into pg_temp.export_data("id", "kind", "data")
		select id, 'act', _character_transfer_top_level_export('act', to_jsonb(actors) - 'partition_id' #- '{properties,LandsraadCharacterComponent,m_NextContractAbandonUniverseTimeLimit}')
			from actors where id IN (
				-- player actors
            	select unnest(array[player_controller_id, player_state_id, player_pawn_id]) as id
					from player_state where account_id=v_acc_id
				union
				-- backup vehicles
				select vehicle_id as id from backup_vehicles where account_id=v_acc_id
				union
				-- recovered vehicles
				select vehicle_id as id from recovered_vehicles where account_id=v_acc_id
				union
				-- base backup actors
				select actor_id as id from base_backup_linked_actors where id IN (
					select id from base_backups where player_id=v_player_controller_id
				)
	        );

	insert into pg_temp.export_data("id", "kind", "data")
        select entity_id, 'fgl', _character_transfer_top_level_export(
			'fgl', to_jsonb(fgl_entities) || to_jsonb(actor_fgl_entities)
		) from actor_fgl_entities join fgl_entities using (entity_id) where actor_id IN (
			select id from pg_temp.export_data where kind='act'
		);


	-- Actor state entries tied to actors we export
	insert into pg_temp.export_data("id", "kind", "data")
	select actor_id, 'ActorState', _character_transfer_top_level_export('ActorState', to_jsonb(actor_state))
		from actor_state where
			actor_id IN (select id from pg_temp.export_data where kind = 'act');


	-- Permission data for the transferred actors
	insert into pg_temp.export_data("id", "kind", "data")
		select actor_id, 'PermissionActor', _character_transfer_top_level_export('PermissionActor', to_jsonb(permission_actor))
			from permission_actor where actor_id IN (select id from pg_temp.export_data where kind = 'act');
	insert into pg_temp.export_data("id", "kind", "data")
		select permission_actor_id, 'PermissionActorRank', _character_transfer_top_level_export('PermissionActorRank', to_jsonb(permission_actor_rank))
			from permission_actor_rank where
				permission_actor_id IN (select id from pg_temp.export_data where kind = 'act')
				and player_id IN (select id from pg_temp.export_data where kind = 'act');

	-- vehicles and vehicle modules

    insert into pg_temp.export_data("kind", "data")
        select 'Vehicle', _character_transfer_top_level_export('Vehicle', to_jsonb(vehicles))
			-- vehicle ids are aliases for actor ids
            from vehicles where id IN (select id from pg_temp.export_data where kind = 'act');

	insert into pg_temp.export_data("id", "kind", "data")
        select id, 'VehicleModule', _character_transfer_top_level_export('VehicleModule', to_jsonb(vehicle_modules))
			-- vehicle ids are aliases for actor ids
            from vehicle_modules where vehicle_id IN (select id from pg_temp.export_data where kind = 'act');

	insert into pg_temp.export_data("kind", "data")
		select 'BackupVehicle', _character_transfer_top_level_export('BackupVehicle', to_jsonb(backup_vehicles))
			from backup_vehicles where account_id=v_acc_id;

	insert into pg_temp.export_data("kind", "data")
		select 'RecoveredVehicle', _character_transfer_top_level_export('RecoveredVehicle', to_jsonb(recovered_vehicles))
			from recovered_vehicles where account_id=v_acc_id;

	-- inventories and items

    insert into pg_temp.export_data("id", "kind", "data")
        select id, 'inv', _character_transfer_top_level_export('inv', to_jsonb(inventories))
            from inventories where
				actor_id IN (select id from pg_temp.export_data where kind = 'act')
				or vehicle_module_id IN (select id from pg_temp.export_data where kind = 'VehicleModule');

    insert into pg_temp.export_data("id", "kind", "data")
        select id, 'itm', _character_transfer_top_level_export('itm', to_jsonb(items))
            from items where inventory_id IN (select id from pg_temp.export_data where kind = 'inv');

    insert into pg_temp.export_data("id", "kind", "data")
        select null, 'ActorInventory', _character_transfer_top_level_export('ActorInventory', to_jsonb(actor_inventories))
            from actor_inventories where inventory_id IN (select id from pg_temp.export_data where kind = 'inv');

	insert into pg_temp.export_data("id", "kind", "data")
        select null, 'VehicleModuleInventory',
			_character_transfer_top_level_export('VehicleModuleInventory', to_jsonb(vehicle_module_inventories))
            from vehicle_module_inventories where inventory_id IN (select id from pg_temp.export_data where kind = 'inv');

	-- Base backups (related actors are already added in the actors section)
	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'Building', _character_transfer_top_level_export('Building', to_jsonb(buildings))
			from buildings where id in (select id from pg_temp.export_data where kind = 'act');

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'BuildingInstance', _character_transfer_top_level_export('BuildingInstance', to_jsonb(building_instances))
			from building_instances where building_id in (select id from pg_temp.export_data where kind = 'act');

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'Placeable', _character_transfer_top_level_export('Placeable', to_jsonb(placeables))
			from placeables where id in (select id from pg_temp.export_data where kind = 'act');

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'Totem', _character_transfer_top_level_export('Totem', to_jsonb(totems))
			from totems where id in (select id from pg_temp.export_data where kind = 'act');

	insert into pg_temp.export_data("id", "kind", "data")
		select id, 'BaseBackup', _character_transfer_top_level_export('BaseBackup', to_jsonb(base_backups))
			from base_backups where player_id=v_player_controller_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select id, 'BaseBackupLinkedActor', _character_transfer_top_level_export('BaseBackupLinkedActor', to_jsonb(base_backup_linked_actors))
			from base_backup_linked_actors where id IN (
				select id from pg_temp.export_data where kind='BaseBackup'
			);

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'LandclaimSegment', _character_transfer_top_level_export('LandclaimSegment', to_jsonb(landclaim_segments))
			from landclaim_segments where totem_id IN (
				select id from pg_temp.export_data where kind='act'
			);

	insert into pg_temp.export_data("id", "kind", "data")
		select id, 'TaxInvoice', _character_transfer_top_level_export('TaxInvoice', to_jsonb(tax_invoice))
			from tax_invoice where totem_id IN (
				select id from pg_temp.export_data where kind='act'
			);

	-- Other stuff

	insert into pg_temp.export_data("id", "kind", "data")
		select id, 'Faction', _character_transfer_top_level_export('Faction', to_jsonb(factions))
			from factions;

	insert into pg_temp.export_data("id", "kind", "data")
		select id, 'Tutorial', _character_transfer_top_level_export('Tutorial', to_jsonb(tutorials))
			from tutorials where id IN (select distinct tutorial_id from tutorial_per_player where player_id=v_player_controller_id);

	insert into pg_temp.export_data("id", "kind", "data")
		select id, 'Keystone', _character_transfer_top_level_export('Keystone', to_jsonb(specialization_keystones_map))
			from specialization_keystones_map where id IN (select distinct keystone_id from purchased_specialization_keystones where player_id=v_player_controller_id);

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'RespawnLocation',
				_character_transfer_top_level_export('RespawnLocation', to_jsonb(player_respawn_locations))
				|| jsonb_build_object('id', gen_random_uuid())
				-- TODO: add default to id and just delete it by filter
			from player_respawn_locations
			where account_id=v_acc_id and "group" = ANY('{PlayerStart,Checkpoint,CheckpointSafe}');

	insert into pg_temp.export_data("kind", "data")
        select 'Character', _character_transfer_top_level_export('Character', to_jsonb(player_state) - 'last_avatar_activity' - 'reconnect_grace_period_end' - 'previous_server_partition_id')
			from player_state where player_state.account_id=v_acc_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'PlayerMarker', _character_transfer_top_level_export('PlayerMarker', to_jsonb(player_markers))
			from player_markers join markers using (marker_hash_id, dimension_index, map_name_id) join map_names using (map_name_id)
				where dimension_index=-1 and player_id=v_player_controller_id and map_name <> 'DeepDesert'
				and ((marker).payload_type <> 'EMarkerPayloadType::Default' or ((marker).payload_type = 'EMarkerPayloadType::Default' and (marker).marker_type like 'FlourSand%'));

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'Marker', _character_transfer_top_level_export('Marker', to_jsonb(markers))
			from markers join player_markers using (marker_hash_id, dimension_index, map_name_id) join map_names using (map_name_id)
				where dimension_index=-1 and player_id=v_player_controller_id and map_name <> 'DeepDesert'
				and ((marker).payload_type <> 'EMarkerPayloadType::Default' or ((marker).payload_type = 'EMarkerPayloadType::Default' and (marker).marker_type like 'FlourSand%'));

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'DialogueMetNpc', _character_transfer_top_level_export('DialogueMetNpc', to_jsonb(dialogue_met_npcs))
			from dialogue_met_npcs where player_id=v_player_controller_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'DialogueTakenNode', _character_transfer_top_level_export('DialogueTakenNode', to_jsonb(dialogue_taken_nodes))
			from dialogue_taken_nodes where player_id=v_player_controller_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'PlayerFaction', _character_transfer_top_level_export('PlayerFaction', to_jsonb(player_faction))
			from player_faction where actor_id=v_player_controller_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'PlayerFactionReputation', _character_transfer_top_level_export('PlayerFactionReputation', to_jsonb(player_faction_reputation))
			from player_faction_reputation where actor_id=v_player_controller_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'ConsumedLore', _character_transfer_top_level_export('ConsumedLore', to_jsonb(consumed_per_player_lore))
			from consumed_per_player_lore where actor_id=v_player_controller_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'PlayerTutorial', _character_transfer_top_level_export('PlayerTutorial', to_jsonb(tutorial_per_player))
			from tutorial_per_player where player_id=v_player_controller_id;

    insert into pg_temp.export_data("id", "kind", "data")
		select null, 'PurchasedKeystone', _character_transfer_top_level_export('PurchasedKeystone', to_jsonb(purchased_specialization_keystones))
			from purchased_specialization_keystones where player_id=v_player_controller_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'SpecializationTracks', _character_transfer_top_level_export('SpecializationTracks', to_jsonb(specialization_tracks))
			from specialization_tracks where player_id=v_player_controller_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'SpecializationRefund', _character_transfer_top_level_export('SpecializationRefund', to_jsonb(specialization_refund_id))
			from specialization_refund_id where player_id=v_player_controller_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'BuildingFavorite', _character_transfer_top_level_export('BuildingFavorite', to_jsonb(building_favorites))
			from building_favorites where account_id=v_acc_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'BuildingProgression', _character_transfer_top_level_export('BuildingProgression', to_jsonb(building_progression))
			from building_progression where account_id=v_acc_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'CommuninetPlayer', _character_transfer_top_level_export('CommuninetPlayer', to_jsonb(communinet_player))
			from communinet_player where account_id=v_acc_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'CommuninetPlayerChannel', _character_transfer_top_level_export('CommuninetPlayerChannel', to_jsonb(communinet_player_channels))
			from communinet_player_channels where account_id=v_acc_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'JourneyStoryNode', _character_transfer_top_level_export('JourneyStoryNode', to_jsonb(journey_story_node))
			from journey_story_node where account_id=v_acc_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'MapArea', _character_transfer_top_level_export('MapArea', to_jsonb(map_areas))
			from map_areas where account_id=v_acc_id and map_name <> 'DeepDesert';

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'PlayerAccessCode', _character_transfer_top_level_export('PlayerAccessCode', to_jsonb(player_access_codes))
			from player_access_codes where account_id=v_acc_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'PlayerTag', _character_transfer_top_level_export('PlayerTag', to_jsonb(player_tags))
			from player_tags where account_id=v_acc_id;

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'Sinkchart', _character_transfer_top_level_export('Sinkchart', to_jsonb(sinkcharts))
			from sinkcharts where item_id IN (select id from pg_temp.export_data where kind='itm');

	insert into pg_temp.export_data("id", "kind", "data")
		select id, 'bbp', _character_transfer_top_level_export('bbp', to_jsonb(building_blueprints))
			from building_blueprints where item_id IN (select id from pg_temp.export_data where kind='itm');

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'BuildingBlueprintInstance', _character_transfer_top_level_export('BuildingBlueprintInstance', to_jsonb(building_blueprint_instances))
			from building_blueprint_instances where building_blueprint_id IN (select id from pg_temp.export_data where kind='bbp');

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'BuildingBlueprintPlaceable', _character_transfer_top_level_export('BuildingBlueprintPlaceable', to_jsonb(building_blueprint_placeables))
			from building_blueprint_placeables where building_blueprint_id IN (select id from pg_temp.export_data where kind='bbp');

	insert into pg_temp.export_data("id", "kind", "data")
		select null, 'BuildingBlueprintPentashield', _character_transfer_top_level_export('BuildingBlueprintPentashield', to_jsonb(building_blueprint_pentashields))
			from building_blueprint_pentashields where building_blueprint_id IN (select id from pg_temp.export_data where kind='bbp');

	insert into pg_temp.export_data("id", "kind", "data")
	    select player_controller_id, 'PlayerVirtualCurrencyBalance', _character_transfer_top_level_export('PlayerVirtualCurrencyBalance', to_jsonb(player_virtual_currency_balances))
            from player_virtual_currency_balances where player_controller_id = v_player_controller_id;

    insert into pg_temp.export_data("id", "kind", "data")
	    select completion_id, 'DungeonCompletion', _character_transfer_top_level_export('DungeonCompletion', to_jsonb(dungeon_completion))
            from dungeon_completion where completion_id IN (select completion_id from dungeon_completion_players where player_id = v_player_controller_id);

	insert into pg_temp.export_data("id", "kind", "data")
	    select completion_id, 'DungeonCompletionPlayer', _character_transfer_top_level_export('DungeonCompletionPlayer', to_jsonb(dungeon_completion_players))
            from dungeon_completion_players where player_id = v_player_controller_id;

    insert into pg_temp.export_data("id","kind","data")
        select null, 'LandsraadHouseRewards', _character_transfer_top_level_export('LandsraadHouseRewards', to_jsonb(landsraad_house_rewards))
            from landsraad_house_rewards where player_id = v_player_controller_id;

    update pg_temp.export_data set data=_character_transfer_replace_local_id_with_transfer_id_in_json(data, '');

    return (select jsonb_build_object(
        '_patches_checksum', (_character_transfer_get_patches_checksum()),
        'funcom_id', (v_funcom_id),
        'player_controller_id', (v_player_controller_id),
        'entries', (_character_transfer_data_table_save())
    ));
END;
$function$


-- character_transfer_get_unsaved_counts(in_fls_id text) -> TABLE(unsaved_bases_count bigint, unsaved_vehicles_count bigint)
-- oid: 58162  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.character_transfer_get_unsaved_counts(in_fls_id text)
 RETURNS TABLE(unsaved_bases_count bigint, unsaved_vehicles_count bigint)
 LANGUAGE plpgsql
AS $function$
DECLARE
	v_acc_id BIGINT;
BEGIN
	v_acc_id := (SELECT acc.id FROM encrypted_accounts acc WHERE acc.user = in_fls_id);

	RETURN QUERY
		WITH unbacked_bases AS (
			SELECT COUNT(*) AS count
			FROM get_unsaved_base_totem_ids_for_account(v_acc_id)
		),
		unsaved_vehicles AS (
			SELECT COUNT(*) AS count
			FROM get_unbacked_up_vehicle_ids_for_account(v_acc_id)
		)
		SELECT
			ub.count AS unsaved_bases_count,
			uv.count AS unsaved_vehicles_count
		FROM unbacked_bases ub, unsaved_vehicles uv;
END
$function$


-- character_transfer_import(in_data jsonb, in_fls_id text, in_character_name text) -> bigint
-- oid: 58163  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.character_transfer_import(in_data jsonb, in_fls_id text, in_character_name text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_checksum TEXT;
	v_coriolis_seed BigInt;
	v_transfer_coriolis_seed BigInt;
	v_id BigInt;
	v_kind _CharacterTransferEntryKind;
	v_new_player_controller_id BigInt;
	v_vehicle_ids BigInt[];
BEGIN
	if not (select is_player_offline(in_fls_id)) then
        raise exception 'sbRP2$ - Player must be Offline';
    end if;

    PERFORM delete_account(in_fls_id, 'incoming char transfer');

    v_checksum := (select _character_transfer_get_patches_checksum());
    IF NOT in_data->>'_patches_checksum' = v_checksum THEN
        raise exception 'sb9R2$ - Patches checksum mismatch, expected: %, got: %', v_checksum, in_data->>'_patches_checksum';
    END IF;

	perform _character_transfer_create_data_table();
	perform _character_transfer_data_table_load(in_data->'entries');

	update pg_temp.export_data set data=_character_transfer_replace_transfer_id_with_local_id_in_json(data, '');

	-- accounts

	insert into encrypted_accounts
		select (jsonb_populate_record(
			null::encrypted_accounts,
			_character_transfer_top_level_import(kind, data, id)
			|| jsonb_build_object(
				'encrypted_funcom_id', encrypt_user_data(in_data->>'funcom_id'),
				'user', in_fls_id
			)
		)).*
		from pg_temp.export_data
		where kind = 'acc';

	-- player and vehicle actors

	insert into actors
		select (jsonb_populate_record(null::actors, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'act';

	insert into fgl_entities
		select (jsonb_populate_record(null::fgl_entities, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'fgl';

	insert into actor_fgl_entities
		select (jsonb_populate_record(null::actor_fgl_entities, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'fgl';

	insert into actor_state
		select (jsonb_populate_record(null::actor_state, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'ActorState';

	insert into permission_actor
		select (jsonb_populate_record(null::permission_actor, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'PermissionActor';

	insert into permission_actor_rank
		select (jsonb_populate_record(null::permission_actor_rank, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'PermissionActorRank';


	-- vehicle and vehicle modules

	insert into vehicles
		select (jsonb_populate_record(null::vehicles, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'Vehicle';

	insert into vehicle_modules
		select (jsonb_populate_record(null::vehicle_modules, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'VehicleModule';

	-- You can VBT as co-owner, but then there will be no owner in target BG. This is a state that permission system doesn't support.
	-- So for backed up vehicles we also update the players ownership so they are now set as owner.
	WITH inserted_backup_vehicles AS (
			INSERT INTO backup_vehicles
				SELECT (jsonb_populate_record(null::backup_vehicles, _character_transfer_top_level_import(kind, data, id))).*
				FROM pg_temp.export_data
				WHERE kind = 'BackupVehicle'
			RETURNING vehicle_id
		)
		SELECT array_agg(vehicle_id) INTO v_vehicle_ids FROM inserted_backup_vehicles;
	perform _character_transfer_ensure_player_is_owner_of_vbt_vehicle(v_vehicle_ids);

	insert into recovered_vehicles
		select (jsonb_populate_record(null::recovered_vehicles, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'RecoveredVehicle';

	-- inventories and items

	insert into inventories
		select (jsonb_populate_record(null::inventories, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data e
		where kind = 'inv';

	insert into items
		select (jsonb_populate_record(null::items, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'itm';

	insert into actor_inventories
		select (jsonb_populate_record(null::actor_inventories, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'ActorInventory';

	insert into vehicle_module_inventories
		select (jsonb_populate_record(null::vehicle_module_inventories, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'VehicleModuleInventory';

	-- Base backups
	insert into buildings
		select (jsonb_populate_record(null::buildings, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'Building';

	insert into building_instances
		select (jsonb_populate_record(null::building_instances, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'BuildingInstance';

	insert into placeables
		select (jsonb_populate_record(null::placeables, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'Placeable';

	insert into totems
		select (jsonb_populate_record(null::totems, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'Totem';

	insert into base_backups
		select (jsonb_populate_record(null::base_backups, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'BaseBackup';

	insert into base_backup_linked_actors
		select (jsonb_populate_record(null::base_backup_linked_actors, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'BaseBackupLinkedActor';

	insert into landclaim_segments
		select (jsonb_populate_record(null::landclaim_segments, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'LandclaimSegment';

	insert into tax_invoice
		select (jsonb_populate_record(null::tax_invoice, _character_transfer_top_level_import(kind, data, id))).*
		from pg_temp.export_data
		where kind = 'TaxInvoice';

	-- other stuff

	insert into encrypted_player_state
		select (jsonb_populate_record(
			null::encrypted_player_state,
			_character_transfer_top_level_import(kind, data, id)
			|| jsonb_build_object(
				'encrypted_character_name', encrypt_user_data(in_character_name)
			)
		)).*
		from pg_temp.export_data
		where kind = 'Character'
		returning player_controller_id into v_new_player_controller_id;

	insert into player_respawn_locations
		select (
			jsonb_populate_record(null::player_respawn_locations, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'RespawnLocation';

	insert into markers
		select (
			jsonb_populate_record(null::markers, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'Marker'
	on conflict ("marker_hash_id", "dimension_index", "map_name_id") do nothing;

	insert into player_markers
		select (
			jsonb_populate_record(null::player_markers, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'PlayerMarker';

	insert into dialogue_met_npcs
		select (
			jsonb_populate_record(null::dialogue_met_npcs, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'DialogueMetNpc';

	insert into dialogue_taken_nodes
		select (
			jsonb_populate_record(null::dialogue_taken_nodes, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'DialogueTakenNode';

	insert into player_faction
		select (
			jsonb_populate_record(null::player_faction, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'PlayerFaction';

	insert into player_faction_reputation
		select (
			jsonb_populate_record(null::player_faction_reputation, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'PlayerFactionReputation';

	insert into consumed_per_player_lore
		select (
			jsonb_populate_record(null::consumed_per_player_lore, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'ConsumedLore';

	insert into tutorial_per_player
		select (
			jsonb_populate_record(null::tutorial_per_player, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'PlayerTutorial';

    insert into purchased_specialization_keystones
		select (
			jsonb_populate_record(null::purchased_specialization_keystones, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'PurchasedKeystone';

    insert into specialization_tracks
		select (
			jsonb_populate_record(null::specialization_tracks, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'SpecializationTracks';

    insert into specialization_refund_id
		select (
			jsonb_populate_record(null::specialization_refund_id, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'SpecializationRefund';

	insert into building_favorites
		select (
			jsonb_populate_record(null::building_favorites, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'BuildingFavorite';

	insert into building_progression
		select (
			jsonb_populate_record(null::building_progression, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'BuildingProgression';

	insert into communinet_player
		select (
			jsonb_populate_record(null::communinet_player, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'CommuninetPlayer';

	insert into communinet_player_channels
		select (
			jsonb_populate_record(null::communinet_player_channels, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'CommuninetPlayerChannel';

	insert into journey_story_node
		select (
			jsonb_populate_record(null::journey_story_node, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'JourneyStoryNode';

	insert into map_areas
		select (
			jsonb_populate_record(null::map_areas, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'MapArea';

	insert into player_access_codes
		select (
			jsonb_populate_record(null::player_access_codes, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'PlayerAccessCode';

	insert into player_tags
		select (
			jsonb_populate_record(null::player_tags, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'PlayerTag';

	insert into sinkcharts
		select (
			jsonb_populate_record(null::sinkcharts, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'Sinkchart';

	insert into building_blueprints
		select (
			jsonb_populate_record(null::building_blueprints, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'bbp';

	insert into building_blueprint_instances
		select (
			jsonb_populate_record(null::building_blueprint_instances, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'BuildingBlueprintInstance';

	insert into building_blueprint_placeables
		select (
			jsonb_populate_record(null::building_blueprint_placeables, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'BuildingBlueprintPlaceable';

	insert into building_blueprint_pentashields
		select (
			jsonb_populate_record(null::building_blueprint_pentashields, _character_transfer_top_level_import(kind, data, id))
		).*
		from pg_temp.export_data
		where kind = 'BuildingBlueprintPentashield';

	insert into player_virtual_currency_balances
        select (
            jsonb_populate_record(null::player_virtual_currency_balances, _character_transfer_top_level_import(kind, data, id))
        ).*
        from pg_temp.export_data
        where kind = 'PlayerVirtualCurrencyBalance';

	insert into dungeon_completion
        select (
            jsonb_populate_record(null::dungeon_completion, _character_transfer_top_level_import(kind, data, id))
        ).*
        from pg_temp.export_data
        where kind = 'DungeonCompletion';

	insert into dungeon_completion_players
        select (
            jsonb_populate_record(null::dungeon_completion_players, _character_transfer_top_level_import(kind, data, id))
        ).*
        from pg_temp.export_data
        where kind = 'DungeonCompletionPlayer';

    insert into landsraad_house_rewards
        select (
                   jsonb_populate_record(null::landsraad_house_rewards, _character_transfer_top_level_import(kind, data, id))
                   ).*
        from pg_temp.export_data
        where kind = 'LandsraadHouseRewards';

	PERFORM set_character_import_state(in_fls_id, 'Complete'::TransferImportState);
	return v_new_player_controller_id;
END;
$function$


-- clean_expired_party_invites(in_invite_expire_seconds integer) -> void
-- oid: 58165  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.clean_expired_party_invites(in_invite_expire_seconds integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	PERFORM parties_get_exclusive_operation_lock();

	PERFORM remove_party_invite(invite_id, 0::smallint) FROM party_invites
	WHERE CURRENT_TIMESTAMP > TO_TIMESTAMP(invite_sent_timespan) + INTERVAL '1 second' * in_invite_expire_seconds;
END
$function$


-- clean_guild_invites_with_incompatible_faction(in_player_id bigint, in_faction_id smallint, neutral_faction_id smallint) -> void
-- oid: 58166  kind: FUNCTION  category: faction

CREATE OR REPLACE FUNCTION dune.clean_guild_invites_with_incompatible_faction(in_player_id bigint, in_faction_id smallint, neutral_faction_id smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	row_record record; -- Variable to hold individual rows
	out_guild_faction_id SMALLINT;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	FOR row_record IN
		SELECT invite_id, guild_invites.guild_id, guilds.guild_faction FROM get_player_guild_invites(in_player_id) as guild_invites
		JOIN guilds ON guilds.guild_id = guild_invites.guild_id
	LOOP	
		IF row_record.guild_faction != neutral_faction_id AND in_faction_id != neutral_faction_id AND row_record.guild_faction != in_faction_id THEN
			PERFORM reject_guild_invite(row_record.invite_id);
		END IF;
    END LOOP;
END
$function$


-- clean_old_guild_invites(in_cutoff_timespan bigint) -> void
-- oid: 58167  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.clean_old_guild_invites(in_cutoff_timespan bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM guild_invites WHERE invite_sent_timespan < in_cutoff_timespan;
END
$function$


-- clean_stock_for_player(in_player_id bigint) -> void
-- oid: 58168  kind: FUNCTION  category: stock_vendor

CREATE OR REPLACE FUNCTION dune.clean_stock_for_player(in_player_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM vendor_stock_cycle WHERE player_id = in_player_id;
	DELETE FROM vendor_stock_state WHERE player_id = in_player_id;
END
$function$


-- clean_stock_for_vendors(in_vendor_ids text[]) -> void
-- oid: 58169  kind: FUNCTION  category: stock_vendor

CREATE OR REPLACE FUNCTION dune.clean_stock_for_vendors(in_vendor_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM vendor_stock_cycle WHERE vendor_id = ANY(in_vendor_ids);
	DELETE FROM vendor_stock_state WHERE vendor_id = ANY(in_vendor_ids);
END
$function$


-- clean_vendors_older_than_timestamp(in_reference_timestamp bigint) -> void
-- oid: 58170  kind: FUNCTION  category: stock_vendor

CREATE OR REPLACE FUNCTION dune.clean_vendors_older_than_timestamp(in_reference_timestamp bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
   vendors_to_delete TEXT[];
BEGIN
	SELECT array_agg(vendor_id) INTO vendors_to_delete FROM vendor_stock_cycle WHERE last_interacted_timestamp <= in_reference_timestamp;
	DELETE FROM vendor_stock_cycle WHERE vendor_id = ANY(vendors_to_delete);
	DELETE FROM vendor_stock_state WHERE vendor_id = ANY(vendors_to_delete);
END
$function$


-- cleanup_account_log_and_orphaned_actors() -> void
-- oid: 58171  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.cleanup_account_log_and_orphaned_actors()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    delete from actors
        WHERE
            -- not is null instead of is not null to match the index expression
            not owner_account_id is null
            AND NOT EXISTS(select 1 from accounts where id=owner_account_id);
    truncate account_removal_log;
END
$function$


-- cleanup_accounts_marked_for_deletion_in_fls(in_account_ids text[]) -> void
-- oid: 58172  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.cleanup_accounts_marked_for_deletion_in_fls(in_account_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	perform delete_account(id, 'deleted in fls') from unnest(in_account_ids) as id;
end
$function$


-- cleanup_orphaned_entities() -> trigger
-- oid: 58173  kind: FUNCTION  category: cleanup

CREATE OR REPLACE FUNCTION dune.cleanup_orphaned_entities()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM fgl_entities WHERE fgl_entities.entity_id = OLD.entity_id;
	RETURN NULL;
END
$function$


-- clear_map_areas_data_for_player(in_id bigint) -> void
-- oid: 58174  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.clear_map_areas_data_for_player(in_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM map_areas WHERE account_id = in_id;
END
$function$


-- complete_journey_nodes_where_prerequisite_nodes_are_complete(story_ids_to_complete text[], prerequisite_completed_story_ids text[]) -> void
-- oid: 58175  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.complete_journey_nodes_where_prerequisite_nodes_are_complete(story_ids_to_complete text[], prerequisite_completed_story_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF ARRAY_LENGTH(story_ids_to_complete, 1) = 0 OR ARRAY_LENGTH(prerequisite_completed_story_ids, 1) = 0 THEN
		RAISE EXCEPTION 'The story ids to complete array and/or the prerequisite completed story ids array is empty - neither array may be empty';
	END IF;

	WITH account_ids_to_modify AS (
		SELECT account_id
		FROM journey_story_node
		WHERE story_node_id = ANY(prerequisite_completed_story_ids)
		AND complete_condition_state = to_jsonb(true)
		GROUP BY account_id
		HAVING COUNT(DISTINCT story_node_id) = ARRAY_LENGTH(prerequisite_completed_story_ids, 1)
	)
	INSERT INTO journey_story_node(account_id, story_node_id, override_reward_block, has_pending_reward, complete_condition_state, reveal_condition_state, metadata_state, reset_group, fail_condition_state)
	SELECT ids.account_id, completed_node.story_node_id, false, false, to_jsonb(true), to_jsonb(true), '{}', 'Default', '{}'
	FROM account_ids_to_modify AS ids
	CROSS JOIN (
		SELECT story_node_id
		FROM UNNEST(story_ids_to_complete) AS story_node_id
	) completed_node(story_node_id)
	ON CONFLICT ON CONSTRAINT journey_story_node_pkey
		DO UPDATE SET
			override_reward_block = EXCLUDED.override_reward_block,
            has_pending_reward = EXCLUDED.has_pending_reward,
			complete_condition_state = EXCLUDED.complete_condition_state,
			reveal_condition_state = EXCLUDED.reveal_condition_state;
END;
$function$


-- complete_journey_story_nodes_for_player(in_player_id text, in_story_node_ids text[]) -> void
-- oid: 58176  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.complete_journey_story_nodes_for_player(in_player_id text, in_story_node_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF NOT is_player_offline(in_player_id) THEN
		RAISE EXCEPTION 'Cannot execute query because the player is online - they must be offline in order for the journey data to be updated correctly without risking it being overwritten by player actions.';
	END IF;

	WITH player_account_id AS (
		SELECT id
		FROM accounts a
		WHERE a.user = in_player_id
	)
	INSERT INTO journey_story_node(account_id, story_node_id, override_reward_block, has_pending_reward, complete_condition_state, reveal_condition_state, fail_condition_state, metadata_state, reset_group)
	SELECT player_account_id.id, completed_node.story_node_id, completed_node.override_reward_block, completed_node.has_pending_reward, completed_node.complete_condition_state, completed_node.reveal_condition_state, completed_node.fail_condition_state, completed_node.metadata_state, completed_node.reset_group
	FROM player_account_id 
	CROSS JOIN (
		SELECT story_node_id, false, false, to_jsonb(true), to_jsonb(true), jsonb_object(ARRAY[]::text[]), jsonb_object(ARRAY[]::text[]), 'Default'::JourneyStoryResetGroup
		FROM UNNEST(in_story_node_ids) AS story_node_id
	) completed_node(story_node_id, override_reward_block, has_pending_reward, complete_condition_state, reveal_condition_state, fail_condition_state, metadata_state, reset_group)
	ON CONFLICT ON CONSTRAINT journey_story_node_pkey
		DO UPDATE SET
			complete_condition_state = EXCLUDED.complete_condition_state,
			reveal_condition_state = EXCLUDED.reveal_condition_state,
			fail_condition_state = EXCLUDED.fail_condition_state,
			metadata_state = EXCLUDED.metadata_state;
END $function$


-- corilis_cleanup_map(in_server_info dune.serverinfo, in_map_info dune.coriolismapinfo) -> void
-- oid: 58177  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.corilis_cleanup_map(in_server_info dune.serverinfo, in_map_info dune.coriolismapinfo)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	PERFORM delete_markers_for_all_players(in_map_info.marker_types_to_keep, in_server_info.map);

	IF in_map_info.should_clear_surveyed_areas
	THEN
		DELETE FROM map_areas WHERE map_name = in_server_info.map;
	END IF;

	IF in_map_info.is_outside_shieldwall
	THEN
		DELETE FROM resourcefield_state WHERE map = in_server_info.map;
	END IF;
END
$function$


-- coriolis_cleanup_farm(in_server_info dune.serverinfo, in_map_info dune.coriolismapinfo) -> void
-- oid: 58178  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.coriolis_cleanup_farm(in_server_info dune.serverinfo, in_map_info dune.coriolismapinfo)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM lore_pickups_temporary;
	DELETE FROM consumed_temporary_per_player_lore;
	UPDATE player_state SET is_coriolis_processed = FALSE;
END
$function$


-- coriolis_cleanup_partition(in_server_info dune.serverinfo, in_map_info dune.coriolismapinfo) -> void
-- oid: 58179  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.coriolis_cleanup_partition(in_server_info dune.serverinfo, in_map_info dune.coriolismapinfo)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF in_map_info.is_outside_shieldwall
	THEN
        PERFORM delete_actors_and_respawns_on_server(in_server_info, (in_map_info).vehicle_classes_spawned_on_map, TRUE);

		update player_state set life_state='DeadByCoriolis'
			from actors
			where player_state.player_pawn_id=actors.id AND server_info_match(actors, in_server_info) and
			not exists (SELECT 1 FROM actor_state WHERE actor_state.actor_id = player_state.player_pawn_id AND actor_state.state = 'Travel');

		-- Move players that died to Hagga Basin in their respective dimension and partition
		UPDATE actors
		SET
			map = 'HaggaBasin',
			dimension_index = player_state.return_dimension_index,
			partition_id = (
				SELECT world_partition.partition_id
				FROM world_partition
				WHERE player_state.return_dimension_index = world_partition.dimension_index AND world_partition.map = 'Survival_1'
			)
		FROM player_state
		WHERE
			(player_state.player_controller_id = actors.id OR player_state.player_pawn_id = actors.id OR player_state.player_state_id = actors.id) AND
			server_info_match(actors, in_server_info) AND
			NOT EXISTS (SELECT 1 FROM actor_state WHERE actor_state.actor_id = player_state.player_pawn_id AND actor_state.state = 'Travel');
	END IF;
END
$function$


-- coriolis_update_seed(in_server_info dune.serverinfo, in_new_coriolis_seed integer, in_map_info dune.coriolismapinfo) -> void
-- oid: 58180  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.coriolis_update_seed(in_server_info dune.serverinfo, in_new_coriolis_seed integer, in_map_info dune.coriolismapinfo)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	old_farm_coriolis_seed Integer;
	old_map_coriolis_seed Integer;
	old_partition_coriolis_seed Integer;
BEGIN
	LOCK TABLE world_farm_reset_seed, world_map_reset_seed, world_partition_reset_seed IN EXCLUSIVE MODE;

	SELECT INTO old_farm_coriolis_seed world_reset_seed FROM world_farm_reset_seed WHERE onerow_id = TRUE limit 1;
	UPDATE world_farm_reset_seed SET world_reset_seed = in_new_coriolis_seed WHERE onerow_id = TRUE;

	SELECT INTO old_map_coriolis_seed world_reset_seed FROM world_map_reset_seed WHERE map = in_server_info.map limit 1;
	INSERT INTO world_map_reset_seed (map, world_reset_seed) Values(in_server_info.map, in_new_coriolis_seed)
		ON CONFLICT(map) DO
		UPDATE SET world_reset_seed = in_new_coriolis_seed;

	SELECT INTO old_partition_coriolis_seed world_reset_seed FROM world_partition_reset_seed WHERE partition_id = in_server_info.partition_id limit 1;
	IF in_server_info.partition_id IS NOT NULL
	THEN
		INSERT INTO world_partition_reset_seed (partition_id, world_reset_seed) Values(in_server_info.partition_id, in_new_coriolis_seed)
			ON CONFLICT(partition_id) DO
			UPDATE SET world_reset_seed = in_new_coriolis_seed;
	END IF;

	IF old_farm_coriolis_seed IS NULL OR old_farm_coriolis_seed <> in_new_coriolis_seed
	THEN
		PERFORM coriolis_cleanup_farm(in_server_info, in_map_info);
	END IF;

	IF in_map_info.is_affected_by_coriolis
	THEN
		IF old_map_coriolis_seed IS NULL OR old_map_coriolis_seed <> in_new_coriolis_seed
		THEN
			PERFORM corilis_cleanup_map(in_server_info, in_map_info);
		END IF;

		IF (in_server_info.partition_id IS NOT NULL AND (old_partition_coriolis_seed IS NULL OR old_partition_coriolis_seed <> in_new_coriolis_seed)) OR
		   (old_map_coriolis_seed IS NULL OR old_map_coriolis_seed <> in_new_coriolis_seed)
		THEN
			PERFORM coriolis_cleanup_partition(in_server_info, in_map_info);
		END IF;
	END IF;
END
$function$


-- create_event_log_partition() -> trigger
-- oid: 58181  kind: FUNCTION  category: event_log

CREATE OR REPLACE FUNCTION dune.create_event_log_partition()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
    CALL create_event_log_partition_table('event_log', NEW.partition_id);

    RETURN NEW;
END;
$function$


-- create_event_log_partition_table(IN table_name text, IN partition_id bigint) -> void
-- oid: 58182  kind: PROCEDURE  category: event_log

CREATE OR REPLACE PROCEDURE dune.create_event_log_partition_table(IN table_name text, IN partition_id bigint)
 LANGUAGE plpgsql
AS $procedure$
DECLARE 
    start_range BIGINT;
    end_range BIGINT;
    partition_table_name TEXT;
BEGIN
    start_range := partition_id;
    end_range := partition_id + 1;
    partition_table_name := format('%s_p%s', table_name, partition_id);

    EXECUTE format('
        CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
        FOR VALUES FROM (%s) TO (%s);
    ', partition_table_name, table_name, start_range, end_range);

END;
$procedure$


-- create_guild(in_player_id bigint, in_neutral_faction smallint, in_guild_name text, in_guild_desc text, in_max_guild_count_per_player integer, OUT out_guild_id bigint, OUT out_success boolean, OUT out_fail_reason dune.guildcreatefailreason) -> record
-- oid: 58183  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.create_guild(in_player_id bigint, in_neutral_faction smallint, in_guild_name text, in_guild_desc text, in_max_guild_count_per_player integer, OUT out_guild_id bigint, OUT out_success boolean, OUT out_fail_reason dune.guildcreatefailreason)
 RETURNS record
 LANGUAGE plpgsql
AS $function$
DECLARE
	guild_count integer;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	IF EXISTS (SELECT 1 FROM guilds WHERE guild_name ILIKE in_guild_name) THEN
		out_guild_id := 0;
		out_success := FALSE;
		out_fail_reason := 'NameAlreadyTaken'::GuildCreateFailReason; -- 1 represents NAME_ALREADY_EXISTS
		RETURN;
	END IF;

	-- we need to check if the player is already part of max amount of guilds before being able to add them
	SELECT INTO guild_count COUNT(*) FROM guild_members WHERE player_id = in_player_id;
	IF guild_count >= in_max_guild_count_per_player THEN
		out_guild_id := 0;
		out_success := FALSE;
		out_fail_reason := 'QueryError'::GuildCreateFailReason;
		RETURN;
	END IF;

	INSERT INTO guilds("guild_id", "guild_name", "guild_faction", "guild_description") VALUES(DEFAULT, in_guild_name, in_neutral_faction , in_guild_desc) RETURNING "guild_id" INTO out_guild_id;
	INSERT INTO guild_members("player_id", "guild_id", "role_id") VALUES(in_player_id, out_guild_id, 100);

	PERFORM pg_notify('guild_notify_channel', format('add_player#{"PlayerId" : %s , "PlayerFactionId" : %s, "GuildId" : %s, "RoleId" : 100, "ShouldClearInvites" : 0}', in_player_id, get_player_faction(in_player_id, in_neutral_faction), out_guild_id));

	out_success := TRUE;
	out_fail_reason := 'None'::GuildCreateFailReason;
END
$function$


-- create_or_update_tutorial_entry(in_player_id bigint, in_tutorial_id smallint, in_tutorial_state smallint) -> void
-- oid: 58184  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.create_or_update_tutorial_entry(in_player_id bigint, in_tutorial_id smallint, in_tutorial_state smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO tutorial_per_player("player_id", "tutorial_id", "tutorial_state") VALUES(in_player_id, in_tutorial_id, in_tutorial_state)
    ON CONFLICT (player_id, tutorial_id) DO UPDATE SET "player_id" = in_player_id, "tutorial_id" = in_tutorial_id, "tutorial_state" = in_tutorial_state
    WHERE tutorial_per_player.player_id = in_player_id AND tutorial_per_player.tutorial_id = in_tutorial_id;
END
$function$


-- create_server_player_access_codes(in_account_id bigint, in_access_code integer, in_access_code_type integer, in_is_resettable boolean) -> void
-- oid: 58185  kind: FUNCTION  category: server

CREATE OR REPLACE FUNCTION dune.create_server_player_access_codes(in_account_id bigint, in_access_code integer, in_access_code_type integer, in_is_resettable boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO player_access_codes(account_id, access_code, access_code_type,is_resettable)
    VALUES(in_account_id, in_access_code, in_access_code_type, in_is_resettable);
END; $function$


-- create_sinkchart_for_map_area_id(in_item_id bigint, in_creator_id bigint, in_map_name text, in_area_id smallint) -> integer
-- oid: 58186  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.create_sinkchart_for_map_area_id(in_item_id bigint, in_creator_id bigint, in_map_name text, in_area_id smallint)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
	sinkchart_marker_ids BIGINT[];
	sinkchart_array_length INT;
BEGIN
	-- Aggregate marker hash IDs into an array based on the conditions
	SELECT ARRAY_AGG(markers.marker_hash_id) INTO sinkchart_marker_ids
	FROM player_markers,
         markers JOIN map_names USING(map_name_id)
	WHERE player_markers.player_id = in_creator_id
		AND map_names.map_name = in_map_name
		AND player_markers.map_name_id = markers.map_name_id
		AND player_markers.marker_hash_id = markers.marker_hash_id
		AND markers.area_id = in_area_id
		AND (player_markers.discovery_level = 2 OR player_markers.discovery_level = 3); -- 'EMarkerDiscoveryLevel::Mysterious' OR 'EMarkerDiscoveryLevel::Discovered'

	-- Check if is NOT NULL before proceeding with the INSERT
	IF array_length(sinkchart_marker_ids, 1) IS NOT NULL THEN
		INSERT INTO sinkcharts (item_id, marker_hash_ids)
		VALUES (in_item_id, sinkchart_marker_ids)
		RETURNING array_length(marker_hash_ids, 1) INTO sinkchart_array_length;

		RETURN sinkchart_array_length;
	ELSE
		RETURN 0;
	END IF;
END
$function$


-- debug_add_test_table_data(in_entry text) -> void
-- oid: 58187  kind: FUNCTION  category: debug

CREATE OR REPLACE FUNCTION dune.debug_add_test_table_data(in_entry text)
 RETURNS void
 LANGUAGE sql
AS $function$
	insert into debug_test_table("entry") VALUES (in_entry);
$function$


-- debug_collect_test_table_data() -> SETOF text
-- oid: 58188  kind: FUNCTION  category: debug

CREATE OR REPLACE FUNCTION dune.debug_collect_test_table_data()
 RETURNS SETOF text
 LANGUAGE sql
AS $function$
	select entry from debug_test_table;
$function$


-- debug_echo(in_text text, in_notices text[]) -> text
-- oid: 58189  kind: FUNCTION  category: debug

CREATE OR REPLACE FUNCTION dune.debug_echo(in_text text, in_notices text[])
 RETURNS text
 LANGUAGE plpgsql
AS $function$
BEGIN
	perform debug_raise_notices(in_notices);
	return in_text;
END;
$function$


-- debug_get_coriolis_seeds() -> TABLE(farm_seed integer, map_names text[], map_seeds integer[], partitions_ids bigint[], partitions_map text[], partitions_seeds integer[])
-- oid: 58190  kind: FUNCTION  category: debug

CREATE OR REPLACE FUNCTION dune.debug_get_coriolis_seeds()
 RETURNS TABLE(farm_seed integer, map_names text[], map_seeds integer[], partitions_ids bigint[], partitions_map text[], partitions_seeds integer[])
 LANGUAGE plpgsql
AS $function$
begin
	RETURN QUERY
		WITH
			map_seeds as (
				SELECT array_agg(map_name) as map_name, array_agg(seed) as seed
				FROM (
						SELECT COALESCE(map_seed.map, partition.map) AS map_name, COALESCE(map_seed.world_reset_seed, -1) AS seed
						FROM world_map_reset_seed AS map_seed FULL JOIN world_partition as partition ON map_seed.map = partition.map
						GROUP BY map_seed.map, partition.map, map_seed.world_reset_seed
						ORDER BY map_name ASC
					) as maps_temp

			),
			partitions_seeds as (
				SELECT array_agg(partition_id) as partition_id, array_agg(map_name) as map_name, array_agg(seed) as seed
				FROM (
						SELECT partition.partition_id as partition_id, partition.map as map_name, COALESCE(partition_seed.world_reset_seed, -1) AS seed
						FROM world_partition_reset_seed AS partition_seed FULL JOIN world_partition as partition ON partition_seed.partition_id = partition.partition_id
						GROUP BY partition.map, partition.partition_id, partition_seed.world_reset_seed
						ORDER BY partition.partition_id ASC
					) as partitions_temp
			)
		SELECT
			COALESCE(world_reset_seed, -1),
			COALESCE(map_seeds.map_name, array[]::TEXT[]),
			COALESCE(map_seeds.seed, array[]::Integer[]),
			COALESCE(partitions_seeds.partition_id, array[]::BigInt[]),
			COALESCE(partitions_seeds.map_name, array[]::TEXT[]),
			COALESCE(partitions_seeds.seed, array[]::Integer[])
		FROM
			world_farm_reset_seed,
			map_seeds,
			partitions_seeds;
end
$function$


-- debug_raise_exception(in_exception text, in_notices text[]) -> void
-- oid: 58191  kind: FUNCTION  category: debug

CREATE OR REPLACE FUNCTION dune.debug_raise_exception(in_exception text, in_notices text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	perform debug_raise_notices(in_notices);
	RAISE EXCEPTION '%', in_exception;
END;
$function$


-- debug_raise_notices(in_notices text[]) -> void
-- oid: 58192  kind: FUNCTION  category: debug

CREATE OR REPLACE FUNCTION dune.debug_raise_notices(in_notices text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF array_length(in_notices, 1) IS NOT NULL THEN
		FOR i IN 0..array_length(in_notices, 1)-1
		LOOP
			RAISE NOTICE '%', in_notices[i];
		END LOOP;
	END IF;
END;
$function$


-- debug_reset_test_table() -> void
-- oid: 58193  kind: FUNCTION  category: debug

CREATE OR REPLACE FUNCTION dune.debug_reset_test_table()
 RETURNS void
 LANGUAGE sql
AS $function$
	truncate debug_test_table;
$function$


-- debug_set_farm_seed(in_new_coriolis_seed integer) -> void
-- oid: 58194  kind: FUNCTION  category: debug

CREATE OR REPLACE FUNCTION dune.debug_set_farm_seed(in_new_coriolis_seed integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	UPDATE world_farm_reset_seed SET world_reset_seed = in_new_coriolis_seed WHERE onerow_id = TRUE;

	UPDATE world_map_reset_seed SET world_reset_seed = in_new_coriolis_seed;
	INSERT INTO world_map_reset_seed SELECT map, in_new_coriolis_seed FROM world_partition GROUP BY map
		ON CONFLICT(map) DO
		UPDATE SET world_reset_seed = in_new_coriolis_seed;

	UPDATE world_partition_reset_seed SET world_reset_seed = in_new_coriolis_seed;
	INSERT INTO world_partition_reset_seed SELECT partition_id, in_new_coriolis_seed FROM world_partition GROUP BY partition_id
		ON CONFLICT(partition_id) DO
		UPDATE SET world_reset_seed = in_new_coriolis_seed;
end
$function$


-- debug_set_map_seed(in_map text, in_new_coriolis_seed integer) -> void
-- oid: 58195  kind: FUNCTION  category: debug

CREATE OR REPLACE FUNCTION dune.debug_set_map_seed(in_map text, in_new_coriolis_seed integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	INSERT INTO world_map_reset_seed (map, world_reset_seed) Values(in_server_info.map, in_new_coriolis_seed)
		ON CONFLICT(map) DO
		UPDATE SET world_reset_seed = in_new_coriolis_seed;

	UPDATE world_partition_reset_seed SET world_reset_seed = in_new_coriolis_seed WHERE map = in_map;
	INSERT INTO world_partition_reset_seed SELECT partition_id, in_new_coriolis_seed FROM world_partition WHERE map = in_map GROUP BY partition_id
		ON CONFLICT(partition_id) DO
		UPDATE SET world_reset_seed = in_new_coriolis_seed;
end
$function$


-- debug_set_partition_seed(in_partition_id bigint, in_new_coriolis_seed integer) -> void
-- oid: 58196  kind: FUNCTION  category: debug

CREATE OR REPLACE FUNCTION dune.debug_set_partition_seed(in_partition_id bigint, in_new_coriolis_seed integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	INSERT INTO world_partition_reset_seed (partition_id, world_reset_seed) Values(in_server_info.partition_id, in_new_coriolis_seed)
		ON CONFLICT(partition_id) DO
		UPDATE SET world_reset_seed = in_new_coriolis_seed;
end
$function$


-- decrypt_user_data(in_encrypted_data bytea) -> text
-- oid: 58197  kind: FUNCTION  category: encryption

CREATE OR REPLACE FUNCTION dune.decrypt_user_data(in_encrypted_data bytea)
 RETURNS text
 LANGUAGE sql
 IMMUTABLE
AS $function$select convert_from(in_encrypted_data, 'utf8');$function$


-- delete_account(in_user_id text, in_reason text) -> boolean
-- oid: 58198  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.delete_account(in_user_id text, in_reason text)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
    was_found Boolean;
BEGIN
    WITH referenced_actor_ids as (
        SELECT ARRAY[player_controller_id, player_pawn_id, player_state_id] as ids
            FROM player_state ps JOIN accounts a ON (ps.account_id = a.id)
            WHERE a.user = in_user_id
    ),
    deleted_actors AS
    (
        DELETE FROM actors USING referenced_actor_ids WHERE id IN (SELECT id FROM actors WHERE id = ANY(referenced_actor_ids.ids) ORDER BY id FOR UPDATE) returning id
    ),
    selected_respawn_beacons AS
    (
        SELECT player_respawn_locations.locator_actor_id FROM player_respawn_locations
        INNER JOIN accounts ON accounts.id = player_respawn_locations.account_id
        WHERE accounts.user = in_user_id AND player_respawn_locations.group = 'RespawnBeacon'
    ),
    delete_respawn_beacons AS
    (
        DELETE FROM actors
        WHERE actors.id IN (SELECT * FROM selected_respawn_beacons)
    ),
    delete_accounts as (
        DELETE FROM accounts WHERE accounts.user = in_user_id returning id as account_id
    ),
    insert_into_removal_log as (
        insert into account_removal_log("fls_id", "account_id", "reason") select in_user_id, account_id, in_reason from delete_accounts
    ),
    delete_from_cascades as (
        SELECT referenced_actor_ids.ids[1] as id,
            guild_handle_actor_delete(referenced_actor_ids.ids[1]),
            remove_party_member(referenced_actor_ids.ids[1], 0::SMALLINT),
            ownership_handle_actor_delete(referenced_actor_ids.ids[1]),
            perform_notify_on_character_delete(in_user_id)
            FROM referenced_actor_ids
    )
    SELECT count(*) > 0 INTO was_found FROM delete_from_cascades;
    return was_found;
END;
$function$


-- delete_actor_states_travel(in_actor_id bigint) -> void
-- oid: 58199  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.delete_actor_states_travel(in_actor_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	WITH
		traveling_actor_ids AS (
			SELECT t.id FROM get_traveling_non_player_actor_ids(in_actor_id) AS t
		)
	DELETE FROM actor_state WHERE (actor_id IN (SELECT t.id FROM traveling_actor_ids AS t(id)) OR actor_id = in_actor_id) AND state = 'Travel';
END;
$function$


-- delete_actors(in_ids bigint[]) -> void
-- oid: 58200  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.delete_actors(in_ids bigint[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM actors WHERE id = ANY(in_ids);
END
$function$


-- delete_actors_and_respawns_on_server(in_server_info dune.serverinfo, in_vehicle_classes_spawned_on_map text[], in_allow_vehicle_recovery boolean) -> void
-- oid: 58201  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.delete_actors_and_respawns_on_server(in_server_info dune.serverinfo, in_vehicle_classes_spawned_on_map text[], in_allow_vehicle_recovery boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    WITH actors_to_delete AS (
	    SELECT a.id
        FROM actors a
        LEFT JOIN actor_state s ON a.id = s.actor_id
	    WHERE owner_account_id IS NULL
	    AND s.state IS DISTINCT FROM 'Travel'
	    AND s.state IS DISTINCT FROM 'VehicleBackup'
	    AND s.state IS DISTINCT FROM 'VehicleRecovery'
	    AND server_info_match(a, in_server_info)
        AND (
            -- Actors that are not vehicles should always be deleted
            NOT EXISTS (SELECT 1 FROM vehicles v WHERE v.id = a.id)
            -- Only vehicles that are allowed to be spawned on this map should be deleted
            OR in_vehicle_classes_spawned_on_map IS NULL -- If the list is NULL all vehicles are allowed
            OR a.class = ANY(in_vehicle_classes_spawned_on_map) -- Vehicle type is explicitly allowed on this map
        )
	    ORDER BY a.id FOR UPDATE OF a
    ),
    vehicles_to_recover AS (
        SELECT COALESCE(ARRAY_AGG(v.id), ARRAY[]::BIGINT[]) AS ids FROM actors_to_delete a JOIN vehicles v ON (a.id = v.id)
        WHERE in_allow_vehicle_recovery
    ),
    recovered_vehicles AS (
        SELECT ids, store_recovered_vehicles_wiped_before_spawn(ids) FROM vehicles_to_recover
    )
    DELETE FROM actors a USING recovered_vehicles rv
    WHERE a.id = ANY(SELECT id FROM actors_to_delete)
    AND NOT a.id = ANY(rv.ids);

	with
		deleted_ids as (
			DELETE from player_respawn_locations
				WHERE map = in_server_info.map AND dimension = in_server_info.dimension_index
				returning id
		)
		update player_state set pending_respawn_location_id=null
			where pending_respawn_location_id in (select * from deleted_ids);
END
$function$


-- delete_all_dungeon_completions(in_dungeon_id text) -> void
-- oid: 58202  kind: FUNCTION  category: dungeon

CREATE OR REPLACE FUNCTION dune.delete_all_dungeon_completions(in_dungeon_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	DELETE FROM dungeon_completion WHERE dungeon_id = in_dungeon_id;
end
$function$


-- delete_all_dungeon_completions_by_player(in_dungeon_id text, in_player_id bigint, in_keep_completion_for_other_players boolean) -> void
-- oid: 58203  kind: FUNCTION  category: dungeon

CREATE OR REPLACE FUNCTION dune.delete_all_dungeon_completions_by_player(in_dungeon_id text, in_player_id bigint, in_keep_completion_for_other_players boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	DELETE FROM dungeon_completion WHERE
		NOT in_keep_completion_for_other_players AND
		dungeon_id = in_dungeon_id AND 
		completion_id IN (SELECT completion_id FROM dungeon_completion_players WHERE player_id = in_player_id);

	DELETE FROM dungeon_completion_players
	WHERE
		in_keep_completion_for_other_players AND
		player_id = in_player_id AND
		completion_id IN (SELECT completion_id FROM dungeon_completion WHERE dungeon_id = in_dungeon_id);
end
$function$


-- delete_all_dungeon_completions_for_all_dungeons_by_player(in_player_id bigint, in_keep_completion_for_other_players boolean) -> void
-- oid: 58204  kind: FUNCTION  category: dungeon

CREATE OR REPLACE FUNCTION dune.delete_all_dungeon_completions_for_all_dungeons_by_player(in_player_id bigint, in_keep_completion_for_other_players boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	DELETE FROM dungeon_completion WHERE
		NOT in_keep_completion_for_other_players AND
		completion_id IN (SELECT completion_id FROM dungeon_completion_players WHERE player_id = in_player_id);
		
	DELETE FROM dungeon_completion_players WHERE in_keep_completion_for_other_players AND player_id = in_player_id;
end
$function$


-- delete_all_inactive_farms() -> void
-- oid: 58205  kind: FUNCTION  category: farm

CREATE OR REPLACE FUNCTION dune.delete_all_inactive_farms()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM farm_state WHERE server_id NOT IN (SELECT * FROM active_server_ids);
END
$function$


-- delete_all_journey_story_nodes(in_account_id bigint) -> void
-- oid: 58206  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.delete_all_journey_story_nodes(in_account_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM journey_story_node WHERE account_id = in_account_id;
END $function$


-- delete_all_static_shifting_sand() -> void
-- oid: 58207  kind: FUNCTION  category: shifting_sand

CREATE OR REPLACE FUNCTION dune.delete_all_static_shifting_sand()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	TRUNCATE shiftingsands_data;
END $function$


-- delete_all_tutorial_entries(in_player_id bigint) -> void
-- oid: 58208  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.delete_all_tutorial_entries(in_player_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM tutorial_per_player WHERE tutorial_per_player.player_id = in_player_id;
END
$function$


-- delete_building_blueprint(in_building_item_id bigint) -> void
-- oid: 58209  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.delete_building_blueprint(in_building_item_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM building_blueprints WHERE id = in_building_item_id;
END
$function$


-- delete_character(in_actor_id bigint) -> void
-- oid: 58210  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.delete_character(in_actor_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM actors WHERE id = in_actor_id;
    DELETE FROM properties WHERE object_id = in_actor_id;
    DELETE FROM fgl_data WHERE object_id = in_actor_id;
    DELETE FROM actor_transform WHERE actor_id = in_actor_id;
END
$function$


-- delete_crafted_map(in_item_id bigint) -> void
-- oid: 58211  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.delete_crafted_map(in_item_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM crafted_maps WHERE item_id = in_item_id;
END
$function$


-- delete_dialogue_data(in_player_controller_id bigint) -> void
-- oid: 58212  kind: FUNCTION  category: dialogue

CREATE OR REPLACE FUNCTION dune.delete_dialogue_data(in_player_controller_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM dialogue_met_npcs
    WHERE player_id = in_player_controller_id;

    DELETE FROM dialogue_taken_nodes
    WHERE player_id = in_player_controller_id;
END
$function$


-- delete_inventory_item(in_item_id bigint, in_count bigint) -> bigint
-- oid: 58213  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.delete_inventory_item(in_item_id bigint, in_count bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	remaining_stack_size BIGINT;
BEGIN
	SELECT INTO STRICT remaining_stack_size stack_size FROM items WHERE id = in_item_id;

	remaining_stack_size := remaining_stack_size - in_count;

	IF remaining_stack_size < 0 THEN
		RETURN NULL;
	END IF;

	IF remaining_stack_size > 0 THEN
		UPDATE items SET stack_size = remaining_stack_size WHERE id = in_item_id;
	ELSE
		PERFORM delete_item(in_item_id);
	END IF;
	RETURN remaining_stack_size;
END $function$


-- delete_item(in_id bigint) -> void
-- oid: 58214  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.delete_item(in_id bigint)
 RETURNS void
 LANGUAGE sql
AS $function$
	DELETE FROM items i
	USING inventories inv
	WHERE i.inventory_id = inv.id
	AND i.id = in_id
	RETURNING _add_item_delete_log(
		i.id,
		inv.id,
		i.template_id
	);
$function$


-- delete_items(in_ids bigint[]) -> void
-- oid: 58215  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.delete_items(in_ids bigint[])
 RETURNS void
 LANGUAGE sql
AS $function$
	DELETE FROM items i
	USING inventories inv
	WHERE i.inventory_id = inv.id
	AND i.id = ANY (in_ids)
	RETURNING _add_item_delete_log(
		i.id,
		inv.id,
		i.template_id
	);
$function$


-- delete_items_from_actor(in_actor_id bigint) -> void
-- oid: 58216  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.delete_items_from_actor(in_actor_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	PERFORM delete_items(
		(
	        SELECT array_agg(i.id)
	        FROM items i
	        JOIN inventories inv ON inv.id = i.inventory_id
	        WHERE inv.actor_id = in_actor_id
		)
	);
END $function$


-- delete_journey_story_ids(story_ids text[]) -> void
-- oid: 58217  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.delete_journey_story_ids(story_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM journey_story_node
	WHERE story_node_id = ANY(story_ids);

	DELETE FROM journey_story_node_cooldown
	WHERE story_node_id = ANY(story_ids);
END;
$function$


-- delete_journey_story_node(in_account_id bigint, in_story_node_id text) -> void
-- oid: 58218  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.delete_journey_story_node(in_account_id bigint, in_story_node_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM journey_story_node WHERE account_id = in_account_id AND story_node_id = in_story_node_id;
END $function$


-- delete_journey_story_nodes_for_group_for_player(in_account_id bigint, in_reset_group dune.journeystoryresetgroup) -> void
-- oid: 58219  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.delete_journey_story_nodes_for_group_for_player(in_account_id bigint, in_reset_group dune.journeystoryresetgroup)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM journey_story_node_cooldown
	WHERE story_node_id IN (
		SELECT story_node_id
		FROM journey_story_node a
		WHERE a.account_id = in_account_id AND a.reset_group = in_reset_group
	)
	AND account_id = in_account_id;

	DELETE FROM journey_story_node
	WHERE reset_group = in_reset_group AND account_id = in_account_id;
END $function$


-- delete_journey_story_nodes_for_player(in_player_id text, in_story_node_ids text[]) -> void
-- oid: 58220  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.delete_journey_story_nodes_for_player(in_player_id text, in_story_node_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM journey_story_node
	WHERE story_node_id = ANY(in_story_node_ids)
	AND account_id IN (
		SELECT id
		FROM accounts a
		WHERE a.user = in_player_id
	);

	DELETE FROM journey_story_node_cooldown
	WHERE story_node_id = ANY(in_story_node_ids)
	AND account_id IN (
		SELECT id
		FROM accounts a
		WHERE a.user = in_player_id
	);
END $function$


-- delete_journey_story_nodes_for_player_account(in_account_id bigint, in_story_node_ids text[]) -> void
-- oid: 58221  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.delete_journey_story_nodes_for_player_account(in_account_id bigint, in_story_node_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM journey_story_node
	WHERE story_node_id = ANY(in_story_node_ids)
	AND account_id = in_account_id;

	DELETE FROM journey_story_node_cooldown
	WHERE story_node_id = ANY(in_story_node_ids)
	AND account_id = in_account_id;
END $function$


-- delete_map_markers(in_dimension_index integer, in_map_name text, in_player_marker_data dune.deleteplayermarkerdata[]) -> void
-- oid: 58222  kind: FUNCTION  category: markers

CREATE OR REPLACE FUNCTION dune.delete_map_markers(in_dimension_index integer, in_map_name text, in_player_marker_data dune.deleteplayermarkerdata[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	WITH player_hash_ids AS (SELECT player_id, marker_hash_ids FROM UNNEST(in_player_marker_data)),
	player_markers_to_delete AS (
		SELECT player_markers.* FROM player_hash_ids, player_markers JOIN map_names USING(map_name_id)
		WHERE (player_markers.dimension_index = in_dimension_index OR player_markers.dimension_index = -1)
			AND map_names.map_name = in_map_name
			AND player_markers.player_id = player_hash_ids.player_id
			AND player_markers.marker_hash_id = ANY(player_hash_ids.marker_hash_ids)
		ORDER BY player_markers.player_id, player_markers.marker_hash_id, player_markers.dimension_index FOR UPDATE -- Ordering for deadlock avoidance
	)
	DELETE FROM player_markers USING player_markers_to_delete
	WHERE player_markers.dimension_index = player_markers_to_delete.dimension_index
		AND player_markers.map_name_id = player_markers_to_delete.map_name_id
		AND player_markers.player_id = player_markers_to_delete.player_id
		AND player_markers.marker_hash_id = player_markers_to_delete.marker_hash_id;
END;
$function$


-- delete_markers_by_id(in_marker_ids integer[]) -> void
-- oid: 58223  kind: FUNCTION  category: markers

CREATE OR REPLACE FUNCTION dune.delete_markers_by_id(in_marker_ids integer[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM markers WHERE marker_hash_id = ANY (in_marker_ids);
END
$function$


-- delete_markers_by_static_location_key(p_location_key text) -> void
-- oid: 58224  kind: FUNCTION  category: markers

CREATE OR REPLACE FUNCTION dune.delete_markers_by_static_location_key(p_location_key text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM markers WHERE (payload #>> '{LocationKey}') = p_location_key;
END;
$function$


-- delete_markers_for_all_players(in_marker_types_to_keep text[], in_map text) -> void
-- oid: 58225  kind: FUNCTION  category: markers

CREATE OR REPLACE FUNCTION dune.delete_markers_for_all_players(in_marker_types_to_keep text[], in_map text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	-- Lock markers matching query
	WITH affected_markers AS (
		SELECT * FROM markers JOIN map_names USING(map_name_id)
		WHERE (in_map IS NULL OR map_names.map_name = in_map) AND NOT (marker).marker_type = ANY(in_marker_types_to_keep)
		ORDER BY marker_hash_id, dimension_index, map_name_id FOR UPDATE -- Ordering to avoid deadlocks
	),
	-- Lock player_markers to be deleted on cascade
	referencing_player_markers AS (
		SELECT player_markers.* FROM player_markers, affected_markers
		WHERE affected_markers.marker_hash_id = player_markers.marker_hash_id
			AND affected_markers.dimension_index = player_markers.dimension_index
			AND affected_markers.map_name_id = player_markers.map_name_id
		ORDER BY player_id, player_markers.marker_hash_id, player_markers.dimension_index, player_markers.map_name_id FOR UPDATE -- Ordering to avoid deadlocks
	)
	-- Delete markers
	DELETE FROM markers USING affected_markers
	WHERE affected_markers.marker_hash_id = markers.marker_hash_id
		AND affected_markers.dimension_index = markers.dimension_index
		AND affected_markers.map_name_id = markers.map_name_id;
END;
$function$


-- delete_markers_return_actor_ids(in_dimension_index integer, in_map_name text, in_marker_ids integer[]) -> TABLE(actor_id bigint, marker_id integer)
-- oid: 58226  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.delete_markers_return_actor_ids(in_dimension_index integer, in_map_name text, in_marker_ids integer[])
 RETURNS TABLE(actor_id bigint, marker_id integer)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	-- Lock markers matching query
	WITH affected_markers AS (
		SELECT * FROM markers JOIN map_names USING(map_name_id)
		WHERE (dimension_index = in_dimension_index OR dimension_index = -1)
			AND map_names.map_name = in_map_name
			AND marker_hash_id = ANY(in_marker_ids)
		ORDER BY marker_hash_id, dimension_index FOR UPDATE -- Ordering to avoid deadlocks
	),
	-- Lock player_markers to be deleted on cascade
	referencing_player_markers AS (
		SELECT player_id, player_markers.marker_hash_id FROM player_markers, affected_markers
		WHERE affected_markers.marker_hash_id = player_markers.marker_hash_id
			AND affected_markers.dimension_index = player_markers.dimension_index
			AND affected_markers.map_name_id = player_markers.map_name_id
		ORDER BY player_id, player_markers.marker_hash_id, player_markers.dimension_index FOR UPDATE -- Ordering to avoid deadlocks
	),
	-- Delete markers
	deleted_markers AS (
		DELETE FROM markers USING affected_markers
		WHERE affected_markers.marker_hash_id = markers.marker_hash_id
			AND affected_markers.dimension_index = markers.dimension_index
			AND affected_markers.map_name_id = markers.map_name_id
	)
	SELECT * FROM referencing_player_markers;
END
$function$


-- delete_mnemonic_recall_lesson(in_account_id bigint, in_lesson_id text) -> void
-- oid: 58227  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.delete_mnemonic_recall_lesson(in_account_id bigint, in_lesson_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM mnemonic_recall WHERE account_id = in_account_id AND lesson_id = in_lesson_id;
END
$function$


-- delete_mnemonic_recall_lesson_all(in_account_id bigint) -> void
-- oid: 58228  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.delete_mnemonic_recall_lesson_all(in_account_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM mnemonic_recall WHERE account_id = in_account_id;
END
$function$


-- delete_server_player_access_codes(in_account_id bigint, in_access_code integer, in_access_code_type integer) -> void
-- oid: 58229  kind: FUNCTION  category: server

CREATE OR REPLACE FUNCTION dune.delete_server_player_access_codes(in_account_id bigint, in_access_code integer, in_access_code_type integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM player_access_codes
		WHERE account_id = in_account_id
		AND access_code = in_access_code
		AND access_code_type = in_access_code_type;
END
$function$


-- delete_spawner(in_map text, in_name text, in_dimension_index integer) -> void
-- oid: 58230  kind: FUNCTION  category: spawner

CREATE OR REPLACE FUNCTION dune.delete_spawner(in_map text, in_name text, in_dimension_index integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM actor_spawners WHERE map = in_map AND name = in_name AND dimension_index = in_dimension_index;
END
$function$


-- delete_static_location_markers(p_location_keys text[]) -> void
-- oid: 58231  kind: FUNCTION  category: markers

CREATE OR REPLACE FUNCTION dune.delete_static_location_markers(p_location_keys text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM markers WHERE (payload #>> '{LocationKey}') = ANY(p_location_keys);
END;
$function$


-- delete_world_partition_by_map_id(in_map_id text) -> void
-- oid: 58232  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.delete_world_partition_by_map_id(in_map_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM world_partition WHERE "map"=in_map_id;
END
$function$


-- demote_guild_member(in_guild_id bigint, in_player_id bigint, in_new_role smallint) -> void
-- oid: 58233  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.demote_guild_member(in_guild_id bigint, in_player_id bigint, in_new_role smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	-- check if new admin is actualy in guild
	IF NOT EXISTS(SELECT FROM guild_members WHERE player_id = in_player_id AND guild_id = in_guild_id) THEN
    	RAISE EXCEPTION 'Trying to demote player not in guild %.', in_player_id;
	END IF;

	IF is_player_guild_admin(in_player_id, in_guild_id) THEN
		RAISE EXCEPTION 'Trying to demote admin. promote a member to admin instead.';
	END IF;

	if in_new_role = 100 THEN
		RAISE EXCEPTION 'Trying to demote to admin.';
	END IF;

	-- set new player to new role
	UPDATE guild_members SET role_id = in_new_role WHERE player_id = in_player_id AND guild_id = in_guild_id;

	PERFORM pg_notify('guild_notify_channel', format('demote_player#{"PlayerId" : %s , "GuildId" : %s, "NewRole" : %s}', in_player_id, in_guild_id, in_new_role));
END
$function$


-- determine_partition_label(in_map text, in_dimension_index integer, in_label text, in_allow_overwrite boolean, in_partition_id bigint) -> text
-- oid: 58234  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.determine_partition_label(in_map text, in_dimension_index integer, in_label text DEFAULT NULL::text, in_allow_overwrite boolean DEFAULT true, in_partition_id bigint DEFAULT NULL::bigint)
 RETURNS text
 LANGUAGE plpgsql
AS $function$
DECLARE
	result_label TEXT := in_label;
	tmp_count INTEGER;
	tmp_box_max_x TEXT;
	tmp_box_max_y TEXT;
	tmp_box_min_x TEXT;
	tmp_box_min_y TEXT;
BEGIN
	-- If label is provided and we don't want to overwrite, return it
	IF result_label IS NOT NULL AND in_allow_overwrite = FALSE THEN
		RETURN result_label;
	END IF;

	CASE in_map
		WHEN 'SH_HarkoVillage' THEN return 'HarkoVillage' || '_' || in_dimension_index;
		WHEN 'SH_Arrakeen' THEN return 'Arrakeen' || '_' || in_dimension_index;
		WHEN 'SH_FallenLight' THEN return 'FallenLight' || '_' || in_dimension_index;
		WHEN 'CB_Story_Hephaestus' THEN return 'WreckOfHephaestus' || '_' || in_dimension_index;
		WHEN 'CB_Story_Ecolab_Carthag' THEN return 'BeneathCarthag' || '_' || in_dimension_index;
		WHEN 'CB_SurvivalChallenge_Station_15' THEN return 'Station15' || '_' || in_dimension_index;
		WHEN 'CB_Story_WaterFatManor' THEN return 'WaterFat' || '_' || in_dimension_index;
		WHEN 'Story_ProcesVerbal' THEN return 'ProcesVerbal' || '_' || in_dimension_index;
		WHEN 'DLC_Story_LostHarvest' THEN return 'LostHarvest' || '_' || in_dimension_index;
		WHEN 'DLC_Story_LostHarvest_EcolabA' THEN return 'LostHarvest_EcolabA' || '_' || in_dimension_index;
		WHEN 'DLC_Story_LostHarvest_EcolabB' THEN return 'LostHarvest_EcolabB' || '_' || in_dimension_index;
		WHEN 'DLC_Story_LostHarvest_ForgottenLab' THEN return 'LostHarvest_ForgottenLab' || '_' || in_dimension_index;
		WHEN 'Story_ArtOfKanly' THEN return 'ArtOfKanly' || '_' || in_dimension_index;
		WHEN 'Story_HeighlinerDungeon' THEN return 'HeighlinerDungeon' || '_' || in_dimension_index;
		WHEN 'CB_Dungeon_Hephaestus' THEN return 'WreckOfHephaestusDungeon' || '_' || in_dimension_index;
		WHEN 'CB_Dungeon_OldCarthag' THEN return 'OldCarthagDungeon' || '_' || in_dimension_index;
		WHEN 'CB_Story_BanditFortress01' THEN return 'SandfliesFortress' || '_' || in_dimension_index;
		WHEN 'CB_Overland_S_05' THEN return 'ClosedOffTestingStationIsland' || '_' || in_dimension_index;
		WHEN 'CB_Overland_S_06' THEN return 'GroundVehicleTimeTrialIsland' || '_' || in_dimension_index;
		WHEN 'CB_Overland_S_04' THEN return 'ErythriteCaveIsland' || '_' || in_dimension_index;
		WHEN 'CB_Overland_M_01' THEN return 'RadioactiveShipwreck' || '_' || in_dimension_index;
		WHEN 'Story_Faction_Outpost_Hark' THEN return 'Story_Faction_Outpost_Hark' || '_' || in_dimension_index;
		WHEN 'Story_Faction_Outpost_Atre' THEN return 'Story_Faction_Outpost_Atre' || '_' || in_dimension_index;
		WHEN 'CB_Ecolab_Bronze_Green_089' THEN return 'RadiationDungeon' || '_' || in_dimension_index;
		WHEN 'CB_Ecolab_Bronze_Green_152' THEN return 'ElectricityDungeon' || '_' || in_dimension_index;
		WHEN 'CB_Ecolab_Bronze_Green_195' THEN return 'PoisonDungeon' || '_' || in_dimension_index;
		WHEN 'CB_Ecolab_Bronze_Green_024' THEN return 'DarknessDungeon' || '_' || in_dimension_index;
		WHEN 'CB_Ecolab_Bronze_Green_136' THEN return 'FireDungeon' || '_' || in_dimension_index;
		WHEN 'CB_Overland_S_07' THEN return 'TheRuinsOfTsimpo' || '_' || in_dimension_index;
		WHEN 'CB_Story_DestroyedZanovar' THEN return 'DestroyedZanovar' || '_' || in_dimension_index;
		WHEN 'CB_Story_OrbitalMonitor' THEN return 'OrbitalMonitor' || '_' || in_dimension_index;
		WHEN 'CB_Dungeon_TheFacility' THEN return 'FacilityDungeon' || '_' || in_dimension_index;
		WHEN 'CB_Dungeon_ThePit' THEN return 'PitDungeon' || '_' || in_dimension_index;
		WHEN 'Overmap' THEN
			IF in_dimension_index = 0 THEN
				return 'Overland';
			END IF;
		WHEN 'Survival_1' THEN
			CASE in_dimension_index
				WHEN 0 THEN return 'Abbir';
				WHEN 1 THEN return 'Alraab';
				WHEN 2 THEN return 'Barkan';
				WHEN 3 THEN return 'Coanua';
				WHEN 4 THEN return 'Fajr Kulon';
				WHEN 5 THEN return 'Gara';
				WHEN 6 THEN return 'Hajar';
				WHEN 7 THEN return 'Jacurutu';
				WHEN 8 THEN return 'Kathib';
				WHEN 9 THEN return 'Legg';
				WHEN 10 THEN return 'Makab';
				WHEN 11 THEN return 'Nadir';
				WHEN 12 THEN return 'Ramal';
				WHEN 13 THEN return 'Rifana';
				WHEN 14 THEN return 'Sandrat';
				WHEN 15 THEN return 'Saajid';
				WHEN 16 THEN return 'Tabr Sink';
				WHEN 17 THEN return 'Tharwa';
				WHEN 18 THEN return 'Umbu';
				WHEN 19 THEN return 'Yaracuwan';
				WHEN 20 THEN return 'al-Mut';
				WHEN 21 THEN return 'Altuyur';
				WHEN 22 THEN return 'Ammit';
				WHEN 23 THEN return 'Ashia';
				WHEN 24 THEN return 'Eaqrab';
				WHEN 25 THEN return 'Hagga';
				WHEN 26 THEN return 'Hua';
				WHEN 27 THEN return 'Katal';
				WHEN 28 THEN return 'Khafash';
				WHEN 29 THEN return 'Matar';
				WHEN 30 THEN return 'Rabie';
				WHEN 31 THEN return 'Rajifiri';
				WHEN 32 THEN return 'Remmel';
				WHEN 33 THEN return 'Sahr';
				WHEN 34 THEN return 'Saqer';
				WHEN 35 THEN return 'Ta''lab';
				WHEN 36 THEN return 'Tarl';
				WHEN 37 THEN return 'Tasmin Sink';
				WHEN 38 THEN return 'Thueban';
				WHEN 39 THEN return 'Tuono';
            ELSE
                return 'Survival' || '_' || in_dimension_index;
			END CASE;
		ELSE
			-- Do nothing
	END CASE;

	-- DeepDesert per-partition handling: if all 9 partitions exist, assign based on ordering
	IF in_map = 'DeepDesert_1' THEN
		SELECT count(*) INTO tmp_count FROM world_partition WHERE map = 'DeepDesert_1' AND dimension_index = in_dimension_index;
		IF tmp_count = 9 AND in_partition_id IS NOT NULL THEN
			RETURN (
				WITH chess_notation AS (
					SELECT label, ROW_NUMBER() OVER (ORDER BY null) AS row_num
					FROM (select * from UNNEST(ARRAY['A1', 'B1', 'C1', 'A2', 'B2', 'C2', 'A3', 'B3', 'C3']) as label) as label_list
				),
				partitions AS (
					SELECT partition_id, ROW_NUMBER() OVER (
						order by
							coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'max_x',
							coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'max_y',
							coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'min_x',
							coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'min_y'
						) AS row_num
					FROM world_partition
					where map = 'DeepDesert_1' AND dimension_index = in_dimension_index
				)
				SELECT 'DeepDesert_' || cn.label
				FROM chess_notation cn
				JOIN partitions p ON cn.row_num = p.row_num
				WHERE p.partition_id = in_partition_id
				LIMIT 1
			);
		END IF;
	END IF;

	IF in_partition_id IS NOT NULL THEN
		SELECT coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'max_x', coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'max_y', coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'min_x', coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'min_y'
			INTO tmp_box_max_x, tmp_box_max_y, tmp_box_min_x, tmp_box_min_y
		FROM world_partition
		WHERE partition_id = in_partition_id
		LIMIT 1;

		IF tmp_box_max_x IS NOT NULL AND tmp_box_max_x IN ('1.0','1') AND tmp_box_max_y IN ('1.0','1') AND tmp_box_min_x IN ('0.0','0') AND tmp_box_min_y IN ('0.0','0') THEN
			RETURN upgrade_map_name(in_map) || '_' || in_dimension_index;
		END IF;
	ELSE
		-- If we don't have a partition id, try to read any partition for that map/dimension
		SELECT coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'max_x', coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'max_y', coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'min_x', coalesce(partition_definition->'box', partition_definition->'boxes'->0)->>'min_y'
			INTO tmp_box_max_x, tmp_box_max_y, tmp_box_min_x, tmp_box_min_y
		FROM world_partition
		WHERE map = in_map AND dimension_index = in_dimension_index
		LIMIT 1;

		IF tmp_box_max_x IS NOT NULL AND tmp_box_max_x IN ('1.0','1') AND tmp_box_max_y IN ('1.0','1') AND tmp_box_min_x IN ('0.0','0') AND tmp_box_min_y IN ('0.0','0') THEN
			RETURN upgrade_map_name(in_map) || '_' || in_dimension_index;
		END IF;
	END IF;

	RETURN NULL;
END;
$function$


-- determine_partition_label_trigger() -> trigger
-- oid: 58236  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.determine_partition_label_trigger()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF NEW.label IS NULL THEN
		UPDATE world_partition
		SET label = determine_partition_label(NEW.map, NEW.dimension_index, NULL, false)
		WHERE partition_id = NEW.partition_id;
	END IF;

	RETURN NULL; -- AFTER ROW trigger does not need to return a row
END;
$function$


-- disband_guild(in_guild_id bigint) -> void
-- oid: 58237  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.disband_guild(in_guild_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_guild_name TEXT;
	members_list BIGINT[];
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	-- check if guild exists
	SELECT guild_name INTO out_guild_name FROM guilds WHERE guild_id = in_guild_id;
	IF NOT FOUND THEN
    	RAISE EXCEPTION 'Trying to disband non existing guild %.', in_guild_id;
	END IF;

	-- get members list
	members_list := ARRAY(SELECT player_id FROM guild_members WHERE guild_id = in_guild_id);

	-- delete
	DELETE FROM guilds WHERE guild_id = in_guild_id;

	PERFORM pg_notify('guild_notify_channel', format('guild_disband#{"GuildId" : %s , "GuildName" : "%s", "PlayerIds" : %s}', in_guild_id, out_guild_name, to_json(members_list)::text));
END
$function$


-- disband_party(in_party_id bigint) -> void
-- oid: 58238  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.disband_party(in_party_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	invite_ids BIGINT[];
	sender_ids BIGINT[];
	player_ids BIGINT[];
BEGIN
	PERFORM parties_get_exclusive_operation_lock();

	DELETE FROM party_members WHERE party_id = in_party_id;
	DELETE FROM parties WHERE party_id = in_party_id;
	DELETE FROM platform_parties_mapping WHERE dune_party_id = in_party_id;

	WITH removed_invites AS (
		DELETE FROM party_invites
		WHERE party_id = in_party_id
		RETURNING *
	) SELECT array_agg(invite_id), array_agg(sender_player_id), array_agg(player_id) from removed_invites INTO invite_ids, sender_ids, player_ids;

	PERFORM pg_notify('party_notify_channel', format('disband_party#{"PartyId" : %s, "InviteIds" : [%s] ,  "SenderIds" : [%s] , "PlayerIds" : [%s]}',
		in_party_id, ARRAY_TO_STRING(invite_ids, ','), ARRAY_TO_STRING(sender_ids, ','), ARRAY_TO_STRING(player_ids, ',')));
END
$function$


-- downgrade_map_name(in_map_name text) -> text
-- oid: 58239  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.downgrade_map_name(in_map_name text)
 RETURNS text
 LANGUAGE plpgsql
AS $function$
BEGIN
	return
		CASE in_map_name
			WHEN 'HaggaBasin' THEN 'Survival_1'
			WHEN 'HarkoVillage' THEN 'SH_HarkoVillage'
			WHEN 'DeepDesert' THEN 'DeepDesert_1'
			WHEN 'Overland' THEN 'Overmap'
			WHEN 'Arrakeen' THEN 'SH_Arrakeen'
			WHEN 'WreckOfHephaestus' THEN 'CB_Story_Hephaestus'
			WHEN 'BeneathCarthag' THEN 'CB_Story_Ecolab_Carthag'
			WHEN 'Station15' THEN 'CB_SurvivalChallenge_Station_15'
			WHEN 'WaterFat' THEN 'CB_Story_WaterFatManor'
			WHEN 'FallenLight' THEN 'SH_FallenLight'
			WHEN 'ProcesVerbal' THEN 'Story_ProcesVerbal'
			WHEN 'LostHarvest' THEN 'DLC_Story_LostHarvest'
			WHEN 'LostHarvest_EcolabA' THEN 'DLC_Story_LostHarvest_EcolabA'
			WHEN 'LostHarvest_EcolabB' THEN 'DLC_Story_LostHarvest_EcolabB'
			WHEN 'LostHarvest_ForgottenLab' THEN 'DLC_Story_LostHarvest_ForgottenLab'
			WHEN 'ArtOfKanly' THEN 'Story_ArtOfKanly'
			WHEN 'HeighlinerDungeon' THEN 'Story_HeighlinerDungeon'
			WHEN 'WreckOfHephaestusDungeon' THEN 'CB_Dungeon_Hephaestus'
			WHEN 'OldCarthagDungeon' THEN 'CB_Dungeon_OldCarthag'
			WHEN 'SandfliesFortress' THEN 'CB_Story_BanditFortress01'
			WHEN 'ClosedOffTestingStationIsland' THEN 'CB_Overland_S_05'
			WHEN 'GroundVehicleTimeTrialIsland' THEN 'CB_Overland_S_06'
			WHEN 'ErythriteCaveIsland' THEN 'CB_Overland_S_04'
			WHEN 'RadioactiveShipwreck' THEN 'CB_Overland_M_01'
			WHEN 'TheRuinsOfTsimpo' THEN 'CB_Overland_S_07'
			WHEN 'Story_Faction_Outpost_Hark' THEN 'Story_Faction_Outpost_Hark'
			WHEN 'Story_Faction_Outpost_Atre' THEN 'Story_Faction_Outpost_Atre'
            WHEN 'RadiationDungeon' THEN 'CB_Ecolab_Bronze_Green_089'
			WHEN 'ElectricityDungeon' THEN 'CB_Ecolab_Bronze_Green_152'
			WHEN 'PoisonDungeon' THEN 'CB_Ecolab_Bronze_Green_195'
			WHEN 'DarknessDungeon' THEN 'CB_Ecolab_Bronze_Green_024'
			WHEN 'FireDungeon' THEN 'CB_Ecolab_Bronze_Green_136'
			WHEN 'DestroyedZanovar' THEN 'CB_Story_DestroyedZanovar'
			WHEN 'OrbitalMonitor' THEN 'CB_Story_OrbitalMonitor'
			WHEN 'FacilityDungeon' THEN 'CB_Dungeon_TheFacility'
			WHEN 'PitDungeon' THEN 'CB_Dungeon_ThePit'
			WHEN 'WindPass' THEN 'CB_Overland_S_08'
			ELSE in_map_name
		END;
END;
$function$


-- drain_item_tracking_data() -> TABLE(function_name dune.itemtrackingfunctiontype, item_id bigint, account_id bigint, inventory_id bigint, template_id text, event_time timestamp without time zone, position_index bigint)
-- oid: 58240  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.drain_item_tracking_data()
 RETURNS TABLE(function_name dune.itemtrackingfunctiontype, item_id bigint, account_id bigint, inventory_id bigint, template_id text, event_time timestamp without time zone, position_index bigint)
 LANGUAGE sql
AS $function$
    DELETE FROM item_operations_staging_table
    RETURNING
        function_name,
        item_id,
        account_id,
        inventory_id,
        template_id,
        event_time AT TIME ZONE 'UTC',
        position_index;
$function$


-- dune_exchange_add_sell_order(in_exchange_id bigint, in_access_point_id bigint, in_owner_id bigint, in_max_orders_per_player integer, in_expiration_time bigint, in_item_id bigint, in_count bigint, in_category_mask integer, in_category_depth smallint, in_durability_cur real, in_durability_max real, in_item_price bigint, in_wear_normalized_item_price bigint, in_quality_level bigint, in_solari_cost bigint) -> dune.duneexchangeaddsellorderresult
-- oid: 58241  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_add_sell_order(in_exchange_id bigint, in_access_point_id bigint, in_owner_id bigint, in_max_orders_per_player integer, in_expiration_time bigint, in_item_id bigint, in_count bigint, in_category_mask integer, in_category_depth smallint, in_durability_cur real, in_durability_max real, in_item_price bigint, in_wear_normalized_item_price bigint, in_quality_level bigint, in_solari_cost bigint)
 RETURNS dune.duneexchangeaddsellorderresult
 LANGUAGE plpgsql
AS $function$
DECLARE
	exchange_inventory_id BIGINT;
	user_id BIGINT;
	new_item_id BIGINT;
	item_template_id TEXT;
	result DuneExchangeAddSellOrderResult;
	fls_id TEXT;
BEGIN
	result.order_id = 0;
	SELECT INTO result.order_slots_used get_dune_exchange_used_order_slots(in_owner_id);

	IF result.order_slots_used >= in_max_orders_per_player THEN
		RETURN result;
	END IF;

	PERFORM * from dune_exchange_orders where item_id = in_item_id;
	IF FOUND THEN
	    -- Debug logging
        SELECT acc."user"
            INTO fls_id
            FROM accounts acc
			JOIN player_state ps on ps.account_id = acc.id
			WHERE ps.player_controller_id = in_owner_id
            LIMIT 1;
		PERFORM log_cheating(fls_id, 'exchange_order_dupe');
        RAISE WARNING 'Trying to dupe exchange sell orders FLS: %', fls_id;
		RETURN result;
	END IF;

	SELECT INTO user_id dune_exchange_get_user_id(in_owner_id);
	SELECT INTO exchange_inventory_id get_exchange_inventory_id(in_exchange_id);

	IF exchange_inventory_id IS NULL THEN
		RETURN result;
	END IF;

	UPDATE dune_exchange_users SET solari_balance = solari_balance - in_solari_cost WHERE id = user_id AND solari_balance >= in_solari_cost;

	IF NOT FOUND THEN
		RETURN result;
	END IF;

	SELECT INTO STRICT item_template_id template_id FROM items WHERE id = in_item_id FOR UPDATE;
	INSERT INTO dune_exchange_orders(exchange_id, access_point_id, owner_id, expiration_time, template_id, durability_cur, durability_max, category_mask, category_depth, item_price, quality_level)
	VALUES(in_exchange_id, in_access_point_id, in_owner_id, in_expiration_time, item_template_id, in_durability_cur, in_durability_max, in_category_mask, in_category_depth, in_item_price, in_quality_level)
	RETURNING id INTO result.order_id;

	INSERT INTO dune_exchange_sell_orders(order_id, initial_stack_size, wear_normalized_price) VALUES(result.order_id, in_count, in_wear_normalized_item_price);
	SELECT INTO new_item_id move_inventory_item(in_item_id, exchange_inventory_id, result.order_id, in_count);

	IF new_item_id IS NULL THEN
		RAISE EXCEPTION 'Failed to move inventory item %', in_item_id;
	END IF;

	UPDATE dune_exchange_orders SET item_id = new_item_id WHERE id = result.order_id;

	result.order_slots_used = result.order_slots_used + 1;

	RETURN result;
END $function$


-- dune_exchange_cancel_order(in_order_id bigint, in_purge_time bigint, in_completion_type integer) -> void
-- oid: 58242  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_cancel_order(in_order_id bigint, in_purge_time bigint, in_completion_type integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
	DELETE FROM dune_exchange_sell_orders WHERE order_id = in_order_id;
	IF NOT FOUND THEN
		RETURN;
	END IF;

	WITH item_stack_size AS
		(SELECT item.stack_size
			FROM dune_exchange_orders ord
			JOIN items item ON ord.item_id = item.id
			WHERE ord.id = in_order_id)
		INSERT INTO dune_exchange_fulfilled_orders(order_id, completion_type, stack_size, original_order_id) VALUES(in_order_id, in_completion_type, (SELECT * FROM item_stack_size), in_order_id);

	UPDATE dune_exchange_orders SET expiration_time = in_purge_time, revision = revision + 1 WHERE id = in_order_id;
END $function$


-- dune_exchange_expire_orders(in_exchange_id bigint, in_current_time bigint, in_purge_time bigint, in_expired_completion_type integer) -> SETOF dune.exchangeexpiredorder
-- oid: 58243  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_expire_orders(in_exchange_id bigint, in_current_time bigint, in_purge_time bigint, in_expired_completion_type integer)
 RETURNS SETOF dune.exchangeexpiredorder
 LANGUAGE plpgsql
AS $function$
DECLARE
	cur_order RECORD;
	order_is_npc_order BOOLEAN;
BEGIN
	FOR cur_order IN
		SELECT 
			ord.id AS order_id,
			ord.owner_id AS owner_id,
			ord.is_npc_order AS is_npc_order,
			item.stack_size AS stack_size,
			ord.item_price AS item_price,
            ord.quality_level AS quality_level
		FROM dune_exchange_orders ord
		JOIN dune_exchange_sell_orders sord ON (ord.id = sord.order_id)
		JOIN items item ON (ord.item_id = item.id)
		WHERE ord.exchange_id = in_exchange_id AND ord.expiration_time IS NOT NULL AND in_current_time >= ord.expiration_time
		FOR UPDATE
	LOOP
		IF NOT cur_order.is_npc_order THEN
			-- Make an item_storage record for the order item.
			DELETE FROM dune_exchange_sell_orders WHERE order_id = cur_order.order_id;
			INSERT INTO dune_exchange_fulfilled_orders(order_id, completion_type, stack_size, original_order_id) VALUES(cur_order.order_id, in_expired_completion_type, cur_order.stack_size, cur_order.order_id);
			UPDATE dune_exchange_orders SET revision = revision + 1, expiration_time = in_purge_time WHERE id = cur_order.order_id;

			RETURN NEXT (
				cur_order.order_id,
				cur_order.owner_id,
				in_expired_completion_type,
				cur_order.stack_size,
				cur_order.item_price,
				cur_order.order_id,
                cur_order.quality_level);
		ELSE
			-- Delete the order. Will cascade into the items table.
			DELETE FROM dune_exchange_orders WHERE id = order_id;
		END IF;

	END LOOP;
END $function$


-- dune_exchange_fulfill_sell_order(in_exchange_id bigint, in_max_orders_per_player integer, in_purchased_completion_type integer, in_sold_completion_type integer, in_instigator_id bigint, in_order_id bigint, in_order_revision bigint, in_dst_inventory_id bigint, in_dst_index bigint, in_count bigint, in_solaris_fee bigint, in_purge_time bigint) -> dune.duneexchangefulfillsellorderresult
-- oid: 58244  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_fulfill_sell_order(in_exchange_id bigint, in_max_orders_per_player integer, in_purchased_completion_type integer, in_sold_completion_type integer, in_instigator_id bigint, in_order_id bigint, in_order_revision bigint, in_dst_inventory_id bigint, in_dst_index bigint, in_count bigint, in_solaris_fee bigint, in_purge_time bigint)
 RETURNS dune.duneexchangefulfillsellorderresult
 LANGUAGE plpgsql
AS $function$
DECLARE
	exchange_inventory_id BIGINT;
	order_access_point_id BIGINT;
	seller_actor_id BIGINT;
	seller_user_id BIGINT;
	buyer_user_id BIGINT;
	user_solari_balance BIGINT;
	per_item_price BIGINT;
	total_cost BIGINT;
	order_revision BIGINT;
	order_item_id BIGINT;
	order_is_npc_order BOOLEAN;
	order_category_mask INT;
	order_category_depth SMALLINT;
	item_template_id TEXT;
	item_durability_cur REAL;
	item_durability_max REAL;
	storage_order_id BIGINT;
	log_order_id BIGINT;
	result DuneExchangeFulfillSellOrderResult;

BEGIN
	result.item_id = 0;
	SELECT INTO result.order_slots_used get_dune_exchange_used_order_slots(in_instigator_id);

	IF result.order_slots_used >= in_max_orders_per_player THEN
		RETURN result;
	END IF;

	SELECT INTO exchange_inventory_id get_exchange_inventory_id(in_exchange_id);
	SELECT INTO buyer_user_id dune_exchange_get_user_id(in_instigator_id);

	BEGIN
		SELECT INTO STRICT user_solari_balance solari_balance FROM dune_exchange_users WHERE id = buyer_user_id FOR UPDATE;
		SELECT INTO STRICT
			order_revision,
			order_access_point_id,
			order_item_id,
			item_template_id,
			item_durability_cur,
			item_durability_max,
			seller_actor_id,
			order_category_mask,
			order_category_depth,
			order_is_npc_order,
			per_item_price

			ord.revision,
			ord.access_point_id,
			ord.item_id,
			ord.template_id,
			ord.durability_cur,
			ord.durability_max,
			ord.owner_id,
			ord.category_mask,
			ord.category_depth,
			ord.is_npc_order,
			ord.item_price
		FROM
			dune_exchange_orders ord
		JOIN dune_exchange_sell_orders sord ON (ord.id = sord.order_id)
		WHERE
			id = in_order_id AND revision = in_order_revision
		FOR UPDATE;
	EXCEPTION
		WHEN NO_DATA_FOUND THEN RETURN result;
	END;

	IF order_revision != in_order_revision THEN
		RETURN result;
	END IF;

	IF NOT order_is_npc_order THEN
		SELECT INTO seller_user_id dune_exchange_get_user_id(seller_actor_id);
	END IF;
	total_cost = per_item_price * in_count + in_solaris_fee;

	IF total_cost > user_solari_balance THEN
		RETURN result;
	END IF;

	IF in_dst_inventory_id IS NULL THEN
		-- Item is to be transferred to exchange storage rather than an external inventory. Make a record for it.
		INSERT INTO dune_exchange_orders(exchange_id, access_point_id, owner_id, template_id, expiration_time, durability_cur, durability_max, item_price, category_mask, category_depth)
			VALUES(in_exchange_id, order_access_point_id, in_instigator_id, item_template_id, in_purge_time, item_durability_cur, item_durability_max, per_item_price, order_category_mask, order_category_depth)
			RETURNING id INTO storage_order_id;

		INSERT INTO dune_exchange_fulfilled_orders(order_id, completion_type, stack_size, original_order_id)
			VALUES(storage_order_id, in_purchased_completion_type, in_count, in_order_id);

		in_dst_inventory_id = exchange_inventory_id;
		in_dst_index = storage_order_id;
	END IF;

	UPDATE dune_exchange_orders SET item_id = NULL WHERE id = in_order_id;

	SELECT INTO result.item_id move_inventory_item(order_item_id, in_dst_inventory_id, in_dst_index, in_count);

	IF result.item_id IS NULL THEN
		IF storage_order_id IS NOT NULL THEN
			DELETE FROM dune_exchange_orders WHERE id = storage_order_id;
		END IF;
		RETURN result;
	END IF;

	-- Create an entry for the fulfilled orders log.
	SELECT INTO log_order_id order_id FROM dune_exchange_fulfilled_orders where source_order_id = in_order_id FOR SHARE;

	IF log_order_id IS NOT NULL THEN
		UPDATE dune_exchange_orders SET expiration_time = in_purge_time, revision = revision + 1 WHERE id = log_order_id;
		UPDATE dune_exchange_fulfilled_orders SET stack_size = stack_size + in_count WHERE order_id = log_order_id;
	ELSE
		INSERT INTO dune_exchange_orders(exchange_id, access_point_id, owner_id, template_id, expiration_time, durability_cur, durability_max, item_price, category_mask, category_depth)
			VALUES(in_exchange_id, order_access_point_id, seller_actor_id, item_template_id, in_purge_time, item_durability_cur, item_durability_max, per_item_price, order_category_mask, order_category_depth) RETURNING id INTO log_order_id;

		INSERT INTO dune_exchange_fulfilled_orders(order_id, source_order_id, completion_type, stack_size, original_order_id)
			VALUES(log_order_id, in_order_id, in_sold_completion_type, in_count, in_order_id);
	END IF;

	UPDATE dune_exchange_users SET solari_balance = solari_balance - total_cost WHERE id = buyer_user_id;
	-- If the new ID is equal to the old ID the item/stack was moved rather than split and the order is now empty
	IF result.item_id = order_item_id THEN
		DELETE FROM dune_exchange_orders WHERE id = in_order_id;
	ELSE
		UPDATE dune_exchange_orders SET item_id = order_item_id, revision = revision + 1 WHERE id = in_order_id; -- Item was split. Restore reference to remaining items.
	END IF;
	-- If the item was transferred to exchange storage rather than an external inventory, update the record with the item ID.
	IF storage_order_id IS NOT NULL THEN
		UPDATE dune_exchange_orders SET item_id = result.item_id WHERE id = storage_order_id;
	END IF;

	result.order_slots_used = result.order_slots_used + 1;

	RETURN result;
END $function$


-- dune_exchange_get_item_price_stats(in_template_ids text[]) -> TABLE(template_id text, minimum bigint, average bigint)
-- oid: 58246  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.dune_exchange_get_item_price_stats(in_template_ids text[])
 RETURNS TABLE(template_id text, minimum bigint, average bigint)
 LANGUAGE plpgsql
AS $function$
DECLARE
	average_price REAL;
	minimum_price REAL;
BEGIN
	RETURN QUERY SELECT ord.template_id, MIN(sord.wear_normalized_price), CAST(SUM(sord.wear_normalized_price * item.stack_size) / SUM(item.stack_size) AS BIGINT)
	FROM dune_exchange_orders ord
	JOIN items item ON item.id = ord.item_id
	JOIN dune_exchange_sell_orders sord ON sord.order_id = ord.id
	WHERE ord.template_id = ANY(in_template_ids) GROUP BY ord.template_id;
END $function$


-- dune_exchange_get_user_id(in_owner_id bigint) -> bigint
-- oid: 58247  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_get_user_id(in_owner_id bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	new_user_id BIGINT;
	user_id BIGINT;
BEGIN
	INSERT INTO dune_exchange_users(owner_id) VALUES(in_owner_id) ON CONFLICT DO NOTHING RETURNING id INTO new_user_id;
	SELECT INTO user_id COALESCE(new_user_id, id) FROM dune_exchange_users WHERE owner_id = in_owner_id;

	return user_id;
END $function$


-- dune_exchange_modify_user_solari_balance(in_controller_id bigint, in_solari_delta bigint) -> void
-- oid: 58248  kind: FUNCTION  category: currency

CREATE OR REPLACE FUNCTION dune.dune_exchange_modify_user_solari_balance(in_controller_id bigint, in_solari_delta bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	user_id BIGINT;
	current_balance BIGINT;
	new_balance BIGINT;
	delta_balance BIGINT;
	fls_id TEXT;
	function_oid oid;
BEGIN
	SELECT INTO user_id dune_exchange_get_user_id(in_controller_id);
	SELECT INTO current_balance balance from player_virtual_currency_balances WHERE currency_id = get_solaris_id() AND player_controller_id = in_controller_id;

	IF current_balance < 0 THEN
		SELECT acc."user"
		INTO fls_id
		FROM accounts acc
		JOIN player_state ps on ps.account_id = acc.id
		WHERE ps.player_controller_id = in_controller_id
		LIMIT 1;

		PERFORM log_cheating(COALESCE(fls_id, in_controller_id::text), 'exchange_negative_solaris');
		UPDATE player_virtual_currency_balances SET balance = 0 WHERE currency_id = get_solaris_id() AND player_controller_id = in_controller_id;
		current_balance = 0;
	END IF;

	delta_balance = in_solari_delta;
	IF current_balance < in_solari_delta THEN
		delta_balance = current_balance;
	END IF;

	UPDATE dune_exchange_users SET solari_balance = solari_balance + delta_balance WHERE id = user_id;
	
	UPDATE player_virtual_currency_balances SET balance = balance - delta_balance WHERE currency_id = get_solaris_id() AND player_controller_id = in_controller_id RETURNING player_virtual_currency_balances.balance INTO new_balance;

	GET DIAGNOSTICS function_oid = PG_ROUTINE_OID;
	PERFORM log_event_solaris(function_oid, 'update_solaris', in_controller_id, new_balance, delta_balance);
END $function$


-- dune_exchange_purge_completed_orders(in_exchange_id bigint, in_current_time bigint) -> SETOF dune.exchangeexpiredorder
-- oid: 58249  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_purge_completed_orders(in_exchange_id bigint, in_current_time bigint)
 RETURNS SETOF dune.exchangeexpiredorder
 LANGUAGE plpgsql
AS $function$
DECLARE
	cur_order RECORD;
	order_is_npc_order BOOLEAN;
BEGIN
	FOR cur_order IN
		SELECT 
			ord.id AS order_id,
			ord.owner_id AS owner_id,
			ord.is_npc_order AS is_npc_order,
			sord.stack_size AS stack_size,
			ord.item_price AS item_price,
			sord.completion_type AS completion_type,
			sord.original_order_id AS original_order_id,
            ord.quality_level AS quality_level
		FROM dune_exchange_orders ord
		JOIN dune_exchange_fulfilled_orders sord ON (ord.id = sord.order_id)
		WHERE ord.exchange_id = in_exchange_id AND ord.expiration_time IS NOT NULL AND in_current_time >= ord.expiration_time
		FOR UPDATE
	LOOP
		DELETE FROM dune_exchange_orders WHERE id = cur_order.order_id;
		RETURN NEXT (
			cur_order.order_id,
			cur_order.owner_id,
			cur_order.completion_type,
			cur_order.stack_size,
			cur_order.item_price,
			cur_order.original_order_id,
            cur_order.quality_level);
	END LOOP;
END $function$


-- dune_exchange_query_storage_item(in_order_id bigint) -> TABLE(completion_type integer, id bigint, revision bigint, expiration_time bigint, access_point_id bigint, ap_name text, owner_id bigint, item_id bigint, template_id text, stack_size bigint, item_price bigint, quality_level bigint, durability_cur real, durability_max real, dynamic_stats jsonb)
-- oid: 58250  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_query_storage_item(in_order_id bigint)
 RETURNS TABLE(completion_type integer, id bigint, revision bigint, expiration_time bigint, access_point_id bigint, ap_name text, owner_id bigint, item_id bigint, template_id text, stack_size bigint, item_price bigint, quality_level bigint, durability_cur real, durability_max real, dynamic_stats jsonb)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
		SELECT sord.completion_type, ord.id, ord.revision, ord.expiration_time, ord.access_point_id, ap.name, ord.owner_id, ord.item_id, ord.template_id, sord.stack_size, ord.item_price, ord.quality_level, ord.durability_cur, ord.durability_max, item.stats
		FROM dune_exchange_orders ord
		JOIN dune_exchange_fulfilled_orders sord ON (ord.id = sord.order_id)
		JOIN dune_exchange_accesspoints ap ON (ord.access_point_id = ap.id)
		LEFT JOIN items item ON ord.item_id = item.id
		WHERE ord.id = in_order_id;
END; $function$


-- dune_exchange_query_storage_items(in_exchange_id bigint, in_owner_id bigint) -> TABLE(completion_type integer, id bigint, revision bigint, expiration_time bigint, access_point_id bigint, ap_name text, owner_id bigint, item_id bigint, template_id text, stack_size bigint, item_price bigint, quality_level bigint, durability_cur real, durability_max real, dynamic_stats jsonb)
-- oid: 58251  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_query_storage_items(in_exchange_id bigint, in_owner_id bigint)
 RETURNS TABLE(completion_type integer, id bigint, revision bigint, expiration_time bigint, access_point_id bigint, ap_name text, owner_id bigint, item_id bigint, template_id text, stack_size bigint, item_price bigint, quality_level bigint, durability_cur real, durability_max real, dynamic_stats jsonb)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
		SELECT sord.completion_type, ord.id, ord.revision, ord.expiration_time, ord.access_point_id, ap.name, ord.owner_id, ord.item_id, ord.template_id, sord.stack_size, ord.item_price, ord.quality_level, ord.durability_cur, ord.durability_max, item.stats
		FROM dune_exchange_orders ord
		JOIN dune_exchange_fulfilled_orders sord ON (ord.id = sord.order_id)
		JOIN dune_exchange_accesspoints ap ON (ord.access_point_id = ap.id)
		LEFT JOIN items item ON ord.item_id = item.id
		WHERE ord.exchange_id = in_exchange_id AND ord.owner_id = in_owner_id;
END; $function$


-- dune_exchange_relist_order(in_order_id bigint, in_expiration_time bigint, in_item_price bigint, in_wear_normalized_item_price bigint, in_solari_cost bigint) -> bigint
-- oid: 58252  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_relist_order(in_order_id bigint, in_expiration_time bigint, in_item_price bigint, in_wear_normalized_item_price bigint, in_solari_cost bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	user_id BIGINT;
	initial_stack_size BIGINT;
BEGIN
	WITH order_owner_id AS
		(SELECT ord.owner_id
			FROM dune_exchange_orders ord
			JOIN dune_exchange_fulfilled_orders ford ON (ord.id = ford.order_id)
			WHERE id = in_order_id AND item_id IS NOT NULL)
		SELECT INTO user_id dune_exchange_get_user_id((SELECT * FROM order_owner_id));

	UPDATE dune_exchange_users SET solari_balance = solari_balance - in_solari_cost WHERE id = user_id AND solari_balance >= in_solari_cost;

	IF NOT FOUND THEN
		RETURN 0;
	END IF;

	DELETE FROM dune_exchange_fulfilled_orders WHERE order_id = in_order_id;

	IF NOT FOUND THEN
		RAISE EXCEPTION 'Order % is not a fulfilled order', in_order_id;
	END IF;

	WITH item_stack_size AS
		(SELECT item.stack_size
			FROM dune_exchange_orders ord
			JOIN items item ON ord.item_id = item.id
			WHERE ord.id = in_order_id)
		INSERT INTO dune_exchange_sell_orders(order_id, initial_stack_size, wear_normalized_price) VALUES(in_order_id, (SELECT * FROM item_stack_size), in_wear_normalized_item_price) RETURNING dune_exchange_sell_orders.initial_stack_size INTO initial_stack_size;

	UPDATE dune_exchange_orders SET item_price = in_item_price, expiration_time = in_expiration_time WHERE id = in_order_id;

	RETURN initial_stack_size;
END $function$


-- dune_exchange_retrieve_solari_balance(in_owner_id bigint) -> bigint
-- oid: 58253  kind: FUNCTION  category: currency

CREATE OR REPLACE FUNCTION dune.dune_exchange_retrieve_solari_balance(in_owner_id bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	current_balance BIGINT;
	fls_id TEXT;
BEGIN
	SELECT INTO current_balance solari_balance from dune_exchange_users WHERE owner_id = in_owner_id LIMIT 1;

	IF current_balance < 0 THEN
		SELECT acc."user"
		INTO fls_id
		FROM accounts acc
		JOIN player_state ps on ps.account_id = acc.id
		WHERE ps.player_controller_id = in_owner_id
		LIMIT 1;

		PERFORM log_cheating(COALESCE(fls_id, in_owner_id::text), 'exchange_negative_solaris');

		UPDATE dune_exchange_users SET solari_balance = 0 WHERE owner_id = in_owner_id;
	END IF;
	RETURN (SELECT solari_balance FROM dune_exchange_users WHERE owner_id = in_owner_id LIMIT 1);
END; $function$


-- dune_exchange_retrieve_solaris_from_item(in_controller_id bigint, in_order_id bigint) -> dune.duneexchangeretrievesolarisfromitemresult
-- oid: 58254  kind: FUNCTION  category: currency

CREATE OR REPLACE FUNCTION dune.dune_exchange_retrieve_solaris_from_item(in_controller_id bigint, in_order_id bigint)
 RETURNS dune.duneexchangeretrievesolarisfromitemresult
 LANGUAGE plpgsql
AS $function$
DECLARE
	result DuneExchangeRetrieveSolarisFromItemResult;
	new_balance BIGINT;
	function_oid oid;
BEGIN
	WITH
		delete_orders_prices AS (
			DELETE FROM dune_exchange_orders
				USING dune_exchange_fulfilled_orders
				WHERE (dune_exchange_orders.id = dune_exchange_fulfilled_orders.order_id)
					AND id = in_order_id AND (item_id IS NULL OR item_id = 0)
			RETURNING item_price * dune_exchange_fulfilled_orders.stack_size AS total_price
		),
		total_price AS (
			SELECT SUM(total_price) AS delta FROM delete_orders_prices
		)
	UPDATE player_virtual_currency_balances 
		SET balance = balance + total_price.delta
		FROM total_price
		WHERE currency_id = get_solaris_id() AND player_controller_id = in_controller_id
		RETURNING
			player_virtual_currency_balances.balance,
            total_price.delta, 
            (SELECT original_order_id FROM dune_exchange_fulfilled_orders WHERE order_id = in_order_id) 
        INTO
			new_balance,
            result.total_item_value,
            result.original_order_id;
	
	GET DIAGNOSTICS function_oid = PG_ROUTINE_OID;
	PERFORM log_event_solaris(function_oid, 'update_solaris', in_controller_id, new_balance, result.total_item_value);

	RETURN result;
END $function$


-- dune_exchange_retrieve_storage_item(in_exchange_id bigint, in_order_id bigint, in_dst_inventory_id bigint, in_dst_index bigint, in_count bigint) -> dune.duneexchangeretrievestorageorderresult
-- oid: 58255  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_retrieve_storage_item(in_exchange_id bigint, in_order_id bigint, in_dst_inventory_id bigint, in_dst_index bigint, in_count bigint)
 RETURNS dune.duneexchangeretrievestorageorderresult
 LANGUAGE plpgsql
AS $function$
DECLARE
	exchange_inventory_id BIGINT;
	order_owner_id BIGINT;
	order_item_id BIGINT;
	result DuneExchangeRetrieveStorageOrderResult;
BEGIN
	SELECT INTO exchange_inventory_id get_exchange_inventory_id(in_exchange_id);

	IF exchange_inventory_id IS NULL THEN
		RETURN NULL;
	END IF;

	SELECT INTO STRICT order_owner_id, order_item_id owner_id, item_id
	FROM dune_exchange_orders
	JOIN dune_exchange_fulfilled_orders ON (dune_exchange_orders.id = dune_exchange_fulfilled_orders.order_id)
	WHERE id = in_order_id
	FOR UPDATE;

	UPDATE dune_exchange_orders SET item_id = NULL WHERE id = in_order_id;

	SELECT INTO result.item_id move_inventory_item(order_item_id, in_dst_inventory_id, in_dst_index, in_count);

	IF result.item_id IS NULL THEN
		RAISE EXCEPTION 'Failed to move inventory item % on order %', in_item_id, in_order_id;
	END IF;

    SELECT INTO result.original_order_id original_order_id FROM dune_exchange_fulfilled_orders WHERE order_id = in_order_id;

	-- If the new ID is equal to the old ID the item/stack was moved rather than split and the order is now empty
	IF result.item_id = order_item_id THEN
		DELETE FROM dune_exchange_orders WHERE id = in_order_id;
	ELSE
		UPDATE dune_exchange_orders SET item_id = order_item_id WHERE id = in_order_id; -- Item was split. Restore reference to remaining items.
		UPDATE dune_exchange_fulfilled_orders SET stack_size = stack_size - in_count WHERE order_id = in_order_id;
	END IF;

	SELECT INTO result.order_slots_used get_dune_exchange_used_order_slots(order_owner_id);

	RETURN result;
END $function$


-- dune_exchange_update_recurring_sell_order(in_exchange_id bigint, in_expiration_time bigint, in_access_point_id bigint, in_owner_id bigint, in_item_id bigint, in_increment bigint, in_max_count bigint, in_category_mask integer, in_category_depth smallint, in_durability_cur real, in_durability_max real, in_item_price bigint, in_wear_normalized_item_price bigint, in_quality_level bigint) -> bigint
-- oid: 58256  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.dune_exchange_update_recurring_sell_order(in_exchange_id bigint, in_expiration_time bigint, in_access_point_id bigint, in_owner_id bigint, in_item_id bigint, in_increment bigint, in_max_count bigint, in_category_mask integer, in_category_depth smallint, in_durability_cur real, in_durability_max real, in_item_price bigint, in_wear_normalized_item_price bigint, in_quality_level bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	exchange_inventory_id BIGINT;
	new_item_id BIGINT;
	item_stack_size BIGINT;
	new_order_id BIGINT;
	item_template_id TEXT;
	old_count BIGINT;
	new_count BIGINT;
	delta_count BIGINT;
BEGIN
	LOCK TABLE dune_exchange_orders, items IN ROW EXCLUSIVE MODE;

	SELECT INTO exchange_inventory_id get_exchange_inventory_id(in_exchange_id);

	IF exchange_inventory_id IS NULL THEN
		RETURN 0;
	END IF;

	SELECT INTO STRICT item_template_id template_id FROM items WHERE id = in_item_id FOR SHARE;

	SELECT INTO new_order_id, new_item_id ord.id, ord.item_id
	FROM dune_exchange_orders ord
	JOIN dune_exchange_sell_orders sord ON (ord.id = sord.order_id)
	WHERE ord.is_npc_order = TRUE AND ord.exchange_id = in_exchange_id AND ord.access_point_id = in_access_point_id AND ord.template_id = item_template_id AND ord.item_price = in_item_price AND ord.quality_level = in_quality_level
	FOR SHARE;

	IF new_order_id IS NULL THEN
		INSERT INTO dune_exchange_orders(exchange_id, access_point_id, owner_id, is_npc_order, expiration_time, template_id, durability_cur, durability_max, category_mask, category_depth, item_price, quality_level)
		VALUES(in_exchange_id, in_access_point_id, in_owner_id, TRUE, in_expiration_time, item_template_id, in_durability_cur, in_durability_max, in_category_mask, in_category_depth, in_item_price, in_quality_level)
		RETURNING id INTO new_order_id;

		INSERT INTO dune_exchange_sell_orders(order_id, initial_stack_size, wear_normalized_price) VALUES(new_order_id, new_count, in_wear_normalized_item_price);
		SELECT INTO new_item_id move_inventory_item(in_item_id, exchange_inventory_id, new_order_id, in_increment);

		IF new_item_id IS NULL THEN
			DELETE FROM dune_exchange_orders WHERE id = new_order_id;
			RETURN 0;
		END IF;

		UPDATE dune_exchange_orders SET item_id = new_item_id WHERE id = new_order_id;

		RETURN in_increment;
	ELSE
		UPDATE dune_exchange_orders SET expiration_time = in_expiration_time WHERE id=new_order_id;
		SELECT INTO STRICT old_count stack_size FROM items WHERE id = new_item_id FOR SHARE;
		new_count = old_count + in_increment;
		IF new_count > in_max_count THEN new_count = in_max_count; END IF;
		IF new_count != old_count THEN
			delta_count = new_count - old_count;
			SELECT INTO new_item_id merge_or_move_inventory_item(in_item_id, exchange_inventory_id, new_order_id, delta_count);
			RETURN delta_count;
		END IF;

		RETURN 0;
	END IF;
END $function$


-- dune_get_account_id_by_user(in_user text) -> bigint
-- oid: 58257  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.dune_get_account_id_by_user(in_user text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	account_id BIGINT;
BEGIN
	SELECT INTO account_id id FROM accounts WHERE "user"=in_user;
	RETURN account_id;
END $function$


-- edit_guild_description(in_guild_id bigint, in_guild_desc text) -> void
-- oid: 58258  kind: FUNCTION  category: currency

CREATE OR REPLACE FUNCTION dune.edit_guild_description(in_guild_id bigint, in_guild_desc text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_guild_description TEXT;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	-- check if guild exists
	SELECT guild_description INTO out_guild_description FROM guilds WHERE guild_id = in_guild_id;
	IF NOT FOUND THEN
    	RAISE EXCEPTION 'Trying to add invite to non existing guild %.', in_guild_id;
	END IF;

	UPDATE guilds SET guild_description = in_guild_desc WHERE guilds.guild_id = in_guild_id;

	PERFORM pg_notify('guild_notify_channel', format('edit_guild_description#{"GuildId" : %s}', in_guild_id));
END
$function$


-- encrypt_user_data(in_data text) -> bytea
-- oid: 58259  kind: FUNCTION  category: encryption

CREATE OR REPLACE FUNCTION dune.encrypt_user_data(in_data text)
 RETURNS bytea
 LANGUAGE sql
 IMMUTABLE
AS $function$select convert_to(in_data, 'utf8')$function$


-- fetch_resourcefield_state(in_map text, in_dimension_index integer, in_field_kind_id smallint) -> TABLE(field_id bigint, spawn_time double precision, value_remaining bigint)
-- oid: 58260  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.fetch_resourcefield_state(in_map text, in_dimension_index integer, in_field_kind_id smallint)
 RETURNS TABLE(field_id bigint, spawn_time double precision, value_remaining bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY 
	SELECT resourcefield_state.field_id, resourcefield_state.spawn_time, resourcefield_state.value_remaining 
	FROM resourcefield_state 
	WHERE map = in_map AND dimension_index = in_dimension_index AND field_kind_id = in_field_kind_id;
END
$function$


-- fetch_server_spice_field_manifest(in_server_id text) -> TABLE(spicefield_type_id integer, inactive_fields_of_type integer, requested_spawned_of_type integer)
-- oid: 58261  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.fetch_server_spice_field_manifest(in_server_id text)
 RETURNS TABLE(spicefield_type_id integer, inactive_fields_of_type integer, requested_spawned_of_type integer)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT t.spicefield_type_id, t.inactive_fields_of_type, t.requested_spawned_of_type
    FROM spicefield_server_availability as t
    WHERE t.server_id = in_server_id;
END; $function$


-- fetch_spicefie_id_types_with_global_info(in_map_name text, in_dimension_index integer) -> TABLE(spicefield_type_id integer, max_globally_active integer, max_globally_primed integer, current_globally_active integer, current_globally_primed integer, is_spawning_active boolean, field_type text)
-- oid: 58262  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.fetch_spicefie_id_types_with_global_info(in_map_name text, in_dimension_index integer)
 RETURNS TABLE(spicefield_type_id integer, max_globally_active integer, max_globally_primed integer, current_globally_active integer, current_globally_primed integer, is_spawning_active boolean, field_type text)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT t.spicefield_type_id, t.max_globally_active, t.max_globally_primed, t.current_globally_active, t.current_globally_primed, t.is_spawning_active, t.field_type
    FROM spicefield_types as t
    WHERE t.map_name = in_map_name AND t.dimension_index = in_dimension_index;
END; $function$


-- find_actor_by_id(in_id bigint) -> dune.actorspawninfo
-- oid: 58263  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.find_actor_by_id(in_id bigint)
 RETURNS dune.actorspawninfo
 LANGUAGE plpgsql
AS $function$
begin
	return (select (id, class, transform, partition_id, dimension_index)::ActorSpawnInfo
		FROM actors WHERE actors.id = in_id
		LIMIT 1);
end
$function$


-- fix_broken_harkonnen_players_due_to_fooled_thufir() -> void
-- oid: 58264  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.fix_broken_harkonnen_players_due_to_fooled_thufir()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN

    -- Dropping tables due to them being created in temp_backup_tables.sql
    drop table if exists da_6358_pre_broken_players;
    drop table if exists da_6358_broken_players_12400;
    drop table if exists da_6358_broken_players_1300;

---Fix players that are in a broken state but haven't been blocked yet
--Tag combo to check for:
--DialogueFlags.Factions.Hark_ThufirBetrayedComplete
--DialogueFlags.Faction.FooledThufir
--PlayerIsFactionTier Harkonnen 5
--TO FIX:
--Remove DialogueFlags.Faction.FooledThufir
    create table if not exists da_6358_pre_broken_players as
    select pt.account_id from player_tags pt
    group by pt.account_id
    HAVING array_agg(DISTINCT tag) @> ARRAY['DialogueFlags.Factions.Hark_ThufirBetrayedComplete', 'DialogueFlags.Faction.FooledThufir', 'Faction.Harkonnen.Tier5'];

    delete from player_tags pt using da_6358_pre_broken_players where pt.account_id = da_6358_pre_broken_players.account_id and pt.tag = 'DialogueFlags.Faction.FooledThufir';

---Fix players that are in a broken state since 1.2.40
---Tag combo to check for:
---DialogueFlags.Factions.Hark_ThufirBetrayedComplete
---DialogueFlags.Faction.FooledThufir
---Contract.Tracking.FactionStory.R4C6Completed
---PlayerIsFactionTier Harkonnen 4
---TO FIX:
---Remove DialogueFlags.Faction.FooledThufir
---Promote player to Harkonnen 5

    create table if not exists da_6358_broken_players_12400 as
    with broken_accounts as (
        select pt.account_id
        from player_tags pt
        group by pt.account_id
        having array_agg(distinct pt.tag) @> array[
            'DialogueFlags.Factions.Hark_ThufirBetrayedComplete',
            'DialogueFlags.Faction.FooledThufir',
            'Contract.Tracking.FactionStory.R4C6Completed',
            'Faction.Harkonnen.Tier4'
            ]
           and not array_agg(distinct pt.tag) && array['Faction.Harkonnen.Tier5']
    ),
    one_state_row_per_account as (
        -- If player_state can theoretically have duplicates per account_id,
        -- pick one deterministically.
        select distinct on (ps.account_id)
             ps.account_id,
             ps.player_controller_id
         from player_state ps
                  join broken_accounts ba on ba.account_id = ps.account_id
         order by ps.account_id, ps.player_controller_id desc
    )
    select
        s.account_id,
        s.player_controller_id,
        a.properties as backup_properties
    from one_state_row_per_account s
             join actors a on a.id = s.player_controller_id;

    INSERT INTO player_tags (account_id, tag)
    SELECT account_id, 'Faction.Harkonnen.Tier5' as tag
    FROM da_6358_broken_players_12400
    ON CONFLICT (account_id, tag) DO NOTHING;

    delete from player_tags pt using da_6358_broken_players_12400 where pt.account_id = da_6358_broken_players_12400.account_id and pt.tag = 'DialogueFlags.Faction.FooledThufir';
    UPDATE actors
    SET properties = jsonb_set(
        properties,
        '{FactionPlayerComponent,m_FactionDataArray}',
        (
            SELECT coalesce(
                       jsonb_agg(
                           CASE
                               WHEN elem->'Faction'->>'Name' = 'Harkonnen'
                                   THEN jsonb_set(elem, '{ReputationAmount}', '2000'::jsonb)
                               ELSE elem
                               END
                       ),
                       '[]'::jsonb
                   )
            FROM jsonb_array_elements(properties->'FactionPlayerComponent'->'m_FactionDataArray') AS t(elem)
        )
    )
    FROM da_6358_broken_players_12400 b12400
    WHERE b12400.player_controller_id = actors.id;

---Fix players that are in a broken state in live aka 1.3.0
---Tag combo to check for:
---DialogueFlags.Factions.Hark_ThufirBetrayedComplete
---Contract.Tracking.FactionStory.R4C6Completed
---PlayerIsFactionTier Harkonnen 4
---NOT DialogueFlags.Faction.FooledThufir
    create table if not exists da_6358_broken_players_1300 as
    with broken_accounts as (
        select pt.account_id
        from player_tags pt
        group by pt.account_id
        HAVING array_agg(DISTINCT tag) @> ARRAY ['DialogueFlags.Factions.Hark_ThufirBetrayedComplete', 'Contract.Tracking.FactionStory.R4C6Completed', 'Faction.Harkonnen.Tier4']
           and not array_agg(distinct tag) && array['DialogueFlags.Faction.FooledThufir', 'Faction.Harkonnen.Tier5'] -- Since Tier4 players can also have Tier5 we need to exclude that
    ),
         one_state_row_per_account as (
             -- If player_state can theoretically have duplicates per account_id,
             -- pick one deterministically.
             select distinct on (ps.account_id)
                 ps.account_id,
                 ps.player_controller_id
             from player_state ps
                      join broken_accounts ba on ba.account_id = ps.account_id
             order by ps.account_id, ps.player_controller_id desc
         )
    select
        s.account_id,
        s.player_controller_id,
        a.properties as backup_properties
    from one_state_row_per_account s
             join actors a on a.id = s.player_controller_id;

    INSERT INTO player_tags (account_id, tag)
    SELECT account_id, 'Faction.Harkonnen.Tier5' as tag
    FROM da_6358_broken_players_1300
    ON CONFLICT (account_id, tag) DO NOTHING;

    UPDATE actors
    SET properties = jsonb_set(
        properties,
        '{FactionPlayerComponent,m_FactionDataArray}',
        (
            SELECT coalesce(
                       jsonb_agg(
                           CASE
                               WHEN elem->'Faction'->>'Name' = 'Harkonnen'
                                   THEN jsonb_set(elem, '{ReputationAmount}', '2000'::jsonb)
                               ELSE elem
                               END
                       ),
                       '[]'::jsonb
                   )
            FROM jsonb_array_elements(properties->'FactionPlayerComponent'->'m_FactionDataArray') AS t(elem)
        )
                     )
    FROM da_6358_broken_players_1300 b1300
    WHERE b1300.player_controller_id = actors.id;
END;
$function$


-- flag_player_as_cheater(in_account_id bigint, in_cheat_type dune.cheat_type_enum) -> void
-- oid: 58265  kind: FUNCTION  category: anticheat

CREATE OR REPLACE FUNCTION dune.flag_player_as_cheater(in_account_id bigint, in_cheat_type dune.cheat_type_enum)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_FLS_id TEXT;
BEGIN
    SELECT acc."user"
        INTO v_FLS_id
        FROM accounts acc
        WHERE acc.id = in_account_id
        LIMIT 1;

    PERFORM log_cheating(v_FLS_id, in_cheat_type);
END;
$function$


-- gather_ownerless_actors_on_server(in_server_info dune.serverinfo) -> SETOF dune.actorspawninfo
-- oid: 58266  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.gather_ownerless_actors_on_server(in_server_info dune.serverinfo)
 RETURNS SETOF dune.actorspawninfo
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
		SELECT a.id, a.class as class_name, a.transform, a.partition_id, a.dimension_index FROM actors as a
		WHERE a.owner_account_id is null AND server_info_match(a, in_server_info);
END;
$function$


-- gather_player_linked_actors(in_player_pawn_id bigint) -> SETOF dune.actorspawninfo
-- oid: 58267  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.gather_player_linked_actors(in_player_pawn_id bigint)
 RETURNS SETOF dune.actorspawninfo
 LANGUAGE plpgsql
AS $function$
begin
	return query
		select actors.id, actors.class as class_name, actors.transform, actors.partition_id, actors.dimension_index
		from actors
		left join actor_state on actor_state.actor_id = actors.id
		where actors.id in (select id from get_traveling_non_player_actor_ids(in_player_pawn_id)) and actor_state.state = 'Travel' and actors.owner_account_id is null
		order by actors.id;
end
$function$


-- gather_removed_accounts_that_left_orphaned_actors_on_server(in_server_info dune.serverinfo) -> TABLE(account_id bigint, removal_reason text, actors_left dune.orphanedplayeractorinfo[])
-- oid: 58268  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.gather_removed_accounts_that_left_orphaned_actors_on_server(in_server_info dune.serverinfo)
 RETURNS TABLE(account_id bigint, removal_reason text, actors_left dune.orphanedplayeractorinfo[])
 LANGUAGE plpgsql
AS $function$
BEGIN
    return query with
        orphaned_actors_per_account as (
            SELECT
                a.owner_account_id as account_id,
                array_agg((a.id, a.class)::OrphanedPlayerActorInfo) as actors_left
            FROM actors as a
            WHERE
                -- not is null instead of is not null to match the index expression
                not a.owner_account_id is null
                AND NOT EXISTS(select 1 from accounts where id=owner_account_id)
                AND server_info_match(a, in_server_info)
            GROUP BY a.owner_account_id
        )
        select orphans.account_id, log.reason, orphans.actors_left
            from orphaned_actors_per_account as orphans left join account_removal_log as log using (account_id);
END
$function$


-- get_account_actor_ids(in_account_id bigint) -> dune.playeractorids
-- oid: 58269  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.get_account_actor_ids(in_account_id bigint)
 RETURNS dune.playeractorids
 LANGUAGE plpgsql
AS $function$
BEGIN
    return (
        select (player_controller_id, player_state_id, player_pawn_id)::PlayerActorIds from player_state
        where account_id = in_account_id
        limit 1
    );
END
$function$


-- get_active_servers_for_gateway() -> TABLE(server_id text, map text, partition_id bigint, dimension_index integer, game_addr inet, game_port integer, revision integer)
-- oid: 58270  kind: FUNCTION  category: server
-- comment: Used by the gateway service to monitor for active servers.

CREATE OR REPLACE FUNCTION dune.get_active_servers_for_gateway()
 RETURNS TABLE(server_id text, map text, partition_id bigint, dimension_index integer, game_addr inet, game_port integer, revision integer)
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
	-- If we have no partitions, assume dimension 0
	return query select fs.server_id, fs.map, wp.partition_id, coalesce(wp.dimension_index, 0), fs.game_addr, fs.game_port, fs.revision from active_server_ids as asi left join world_partition as wp on asi.server_id = wp.server_id join farm_state as fs on fs.server_id = asi.server_id;
END
$function$


-- get_actor_server_info(in_id bigint) -> dune.serverinfo
-- oid: 58271  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.get_actor_server_info(in_id bigint)
 RETURNS dune.serverinfo
 LANGUAGE plpgsql
AS $function$
begin
	return (select (map, partition_id, dimension_index)::ServerInfo from actors where id=in_id limit 1);
end
$function$


-- get_actors_location_data_with_permission(in_actor_ids bigint[]) -> SETOF dune.actorpermissionlocationdata
-- oid: 58272  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.get_actors_location_data_with_permission(in_actor_ids bigint[])
 RETURNS SETOF dune.actorpermissionlocationdata
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	SELECT actors.id, actors.partition_id, actors.map, actors.dimension_index, actors.transform
	FROM actors
	WHERE actors.id = ANY(in_actor_ids);
END
$function$


-- get_all_demo_players() -> TABLE(fls_ids text)
-- oid: 58273  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_all_demo_players()
 RETURNS TABLE(fls_ids text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT fls_id
	FROM demo_users
    WHERE demo_playtime_seconds IS NOT NULL;
END
$function$


-- get_all_faction_members() -> TABLE(player_id bigint, fls_id text, faction_id smallint)
-- oid: 58274  kind: FUNCTION  category: faction

CREATE OR REPLACE FUNCTION dune.get_all_faction_members()
 RETURNS TABLE(player_id bigint, fls_id text, faction_id smallint)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	SELECT ps.player_controller_id as player_id, acc.user as fls_id, f.faction_id
	FROM accounts acc
	LEFT JOIN player_state ps ON acc.id = ps.account_id
	RIGHT JOIN player_faction f ON ps.player_controller_id = f.actor_id;
END
$function$


-- get_all_guild_members() -> TABLE(player_id bigint, fls_id text, guild_id bigint)
-- oid: 58275  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_all_guild_members()
 RETURNS TABLE(player_id bigint, fls_id text, guild_id bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	SELECT gm.player_id, acc.user, gm.guild_id
	FROM accounts acc
	LEFT JOIN player_state ps ON acc.id = ps.account_id
	RIGHT JOIN guild_members gm ON ps.player_controller_id = gm.player_id;
END
$function$


-- get_all_online_or_recently_disconnected_player_online_state() -> SETOF dune.playeronlinestateentry
-- oid: 58276  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_all_online_or_recently_disconnected_player_online_state()
 RETURNS SETOF dune.playeronlinestateentry
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT actors.id, player_state.character_name, (actors.map, actors.partition_id, actors.dimension_index)::ServerInfo, (player_state.last_avatar_activity AT TIME ZONE 'UTC')::TIMESTAMP, player_state.online_status
	FROM actors
	JOIN player_state ON player_state.player_controller_id = actors.id
	WHERE (player_state.online_status = 'Online' 
		or player_state.last_avatar_activity > NOW() - INTERVAL '1 minutes' );
END
$function$


-- get_all_parties() -> TABLE(party_id bigint, player_id bigint, player_name text, party_leader_id bigint, platform_session_id text, platform_name text, platform_players_count integer)
-- oid: 58277  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_all_parties()
 RETURNS TABLE(party_id bigint, player_id bigint, player_name text, party_leader_id bigint, platform_session_id text, platform_name text, platform_players_count integer)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT
        parties.party_id,
        party_members.player_id,
        player_state.character_name,
        parties.party_leader_id,
        platform_parties_mapping.platform_session_id,
        platform_parties_mapping.platform_name,
        platform_parties_mapping.num_of_players
    FROM party_members
    JOIN parties ON party_members.party_id = parties.party_id
    JOIN player_state ON player_state.player_controller_id = party_members.player_id
    LEFT JOIN platform_parties_mapping ON platform_parties_mapping.dune_party_id = parties.party_id;
END
$function$


-- get_all_party_invites() -> TABLE(invite_id bigint, party_id bigint, sender_player_id bigint, sender_name text, player_id bigint, player_name text, invite_sent_timespan bigint)
-- oid: 58278  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_all_party_invites()
 RETURNS TABLE(invite_id bigint, party_id bigint, sender_player_id bigint, sender_name text, player_id bigint, player_name text, invite_sent_timespan bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT party_invites.invite_id, party_invites.party_id, party_invites.sender_player_id, sender_player_state.character_name, party_invites.player_id, player_state.character_name, party_invites.invite_sent_timespan
	FROM party_invites
	JOIN player_state ON player_state.player_controller_id = party_invites.player_id
	JOIN player_state AS sender_player_state ON sender_player_state.player_controller_id = party_invites.sender_player_id;
END
$function$


-- get_all_party_members() -> TABLE(player_id bigint, fls_id text, party_id bigint)
-- oid: 58279  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_all_party_members()
 RETURNS TABLE(player_id bigint, fls_id text, party_id bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	SELECT pm.player_id, acc.user, pm.party_id
	FROM party_members pm
	LEFT JOIN player_state ps ON ps.player_controller_id = pm.player_id
	LEFT JOIN accounts acc ON acc.id = ps.account_id;
END
$function$


-- get_all_player_character_home_dimensions() -> TABLE(fls_id text, home_dimension integer)
-- oid: 58280  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_all_player_character_home_dimensions()
 RETURNS TABLE(fls_id text, home_dimension integer)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT accounts.user as fls_id, player_state.home_dimension_index as home_dimension
	FROM accounts
	LEFT JOIN player_state on player_state.account_id = accounts.id;
END
$function$


-- get_all_player_in_guild_online_state(in_guild_id bigint) -> SETOF dune.playeronlinestateentry
-- oid: 58281  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_all_player_in_guild_online_state(in_guild_id bigint)
 RETURNS SETOF dune.playeronlinestateentry
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT actors.id, player_state.character_name, (actors.map, actors.partition_id, actors.dimension_index)::ServerInfo, (player_state.last_avatar_activity AT TIME ZONE 'UTC')::TIMESTAMP, player_state.online_status
	FROM actors
	JOIN player_state ON player_state.player_controller_id = actors.id
    JOIN guild_members ON guild_members.player_id = actors.id
	WHERE guild_members.guild_id = in_guild_id;
END
$function$


-- get_all_player_travel_states() -> TABLE(fls_id text, login_target_dimension_index integer)
-- oid: 58282  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_all_player_travel_states()
 RETURNS TABLE(fls_id text, login_target_dimension_index integer)
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
    RETURN query SELECT pts.fls_id, pts.login_target_dimension_index FROM player_travel_state AS pts;
END;
$function$


-- get_all_tutorial_entries(in_player_id bigint) -> TABLE(tutorial_id smallint, tutorial_state smallint)
-- oid: 58283  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.get_all_tutorial_entries(in_player_id bigint)
 RETURNS TABLE(tutorial_id smallint, tutorial_state smallint)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY SELECT tutorial_per_player.tutorial_id, tutorial_per_player.tutorial_state FROM tutorial_per_player WHERE tutorial_per_player.player_id = in_player_id;
END
$function$


-- get_all_unresolved_character_imports() -> TABLE(flsid text, importstate dune.transferimportstate, lastupdatetime timestamp with time zone)
-- oid: 58284  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_all_unresolved_character_imports()
 RETURNS TABLE(flsid text, importstate dune.transferimportstate, lastupdatetime timestamp with time zone)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT ti.fls_id, ti.transfer_state, ti.last_update
	FROM character_transfer_imports ti;
END;
$function$


-- get_applied_patches() -> SETOF text
-- oid: 58285  kind: FUNCTION  category: schema_meta

CREATE OR REPLACE FUNCTION dune.get_applied_patches()
 RETURNS SETOF text
 LANGUAGE sql
AS $function$
	select "name" from applied_patches order by "date";
$function$


-- get_battlegroup_close_date() -> timestamp without time zone
-- oid: 58286  kind: FUNCTION  category: battlegroup

CREATE OR REPLACE FUNCTION dune.get_battlegroup_close_date()
 RETURNS timestamp without time zone
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN (SELECT farm_variables.battlegroup_close_date from farm_variables);
END
$function$


-- get_best_dungeon_completion(in_dungeon_id text) -> TABLE(out_difficulty integer, out_duration_ms integer, out_players_names text[])
-- oid: 58287  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_best_dungeon_completion(in_dungeon_id text)
 RETURNS TABLE(out_difficulty integer, out_duration_ms integer, out_players_names text[])
 LANGUAGE plpgsql
AS $function$
begin
	RETURN QUERY SELECT
		d.difficulty,
		d.duration_ms,
		COALESCE((SELECT array_agg(COALESCE(ps.character_name, ''))
			FROM player_state as ps
			FULL JOIN dungeon_completion_players as dcp ON dcp.player_id = ps.player_controller_id
			WHERE dcp.completion_id = d.completion_id), ARRAY[]::TEXT[])
		FROM dungeon_completion as d
		WHERE d.dungeon_id = in_dungeon_id
		ORDER BY d.difficulty DESC, d.players_num ASC, d.duration_ms ASC, d.completion_id ASC
		LIMIT 1;
end
$function$


-- get_best_dungeons_completions_for_player(in_player_id bigint) -> TABLE(out_dungeon_id text, out_difficulty integer, out_duration_ms integer, out_players_num smallint)
-- oid: 58288  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_best_dungeons_completions_for_player(in_player_id bigint)
 RETURNS TABLE(out_dungeon_id text, out_difficulty integer, out_duration_ms integer, out_players_num smallint)
 LANGUAGE plpgsql
AS $function$
begin
	RETURN QUERY SELECT DISTINCT ON (d.dungeon_id) d.dungeon_id, d.difficulty, d.duration_ms, d.players_num
		FROM dungeon_completion as d
			INNER JOIN dungeon_completion_players as p ON p.completion_id = d.completion_id
		WHERE p.player_id = in_player_id
		ORDER BY d.dungeon_id, d.difficulty DESC, d.players_num ASC, d.duration_ms ASC, d.completion_id ASC;
end
$function$


-- get_building_blueprint_copy_data(in_building_blueprint_id bigint) -> dune.buildingblueprintgetcopydata
-- oid: 58289  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.get_building_blueprint_copy_data(in_building_blueprint_id bigint)
 RETURNS dune.buildingblueprintgetcopydata
 LANGUAGE plpgsql
AS $function$
DECLARE
    buildings_array BuildingBlueprintItem[];
    placeables_array BuildingBlueprintPlaceableItem[];
    pentashields_array BuildingBlueprintPentashieldItem[];
BEGIN
    -- All Building Pieces
    SELECT array_agg((instance_id, building_type, transform, provides_stability, health, hologram)::BuildingBlueprintItem)
		into buildings_array
		FROM building_blueprint_instances
		WHERE building_blueprint_id = in_building_blueprint_id;

    -- All Placeables
    SELECT array_agg((placeable_id, building_type, transform, hologram)::BuildingBlueprintPlaceableItem)
		into placeables_array
		FROM building_blueprint_placeables
		WHERE building_blueprint_id = in_building_blueprint_id;

    -- Pentashields
    SELECT array_agg((placeable_id, scale)::BuildingBlueprintPentashieldItem)
		into pentashields_array
		FROM building_blueprint_pentashields
		WHERE building_blueprint_id = in_building_blueprint_id;

    return ROW(buildings_array, placeables_array, pentashields_array)::BuildingBlueprintGetCopyData;
END
$function$


-- get_building_favorites(in_account_id bigint) -> TABLE(building_types text[])
-- oid: 58290  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.get_building_favorites(in_account_id bigint)
 RETURNS TABLE(building_types text[])
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT building_favorites.building_types FROM building_favorites WHERE account_id = in_account_id;
END; $function$


-- get_building_id(in_actor_id bigint, in_class text) -> dune.buildinggetidcomposite
-- oid: 58291  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.get_building_id(in_actor_id bigint, in_class text)
 RETURNS dune.buildinggetidcomposite
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF not exists(select 1 from buildings where "id" = in_actor_id) THEN
		if in_actor_id is null or in_actor_id = 0 then
			in_actor_id := (SELECT assign_actor_id(in_class));
		end if;

		INSERT INTO buildings("id") VALUES(in_actor_id);
	END IF;

    return ROW(in_actor_id)::BuildingGetIdComposite;
END
$function$


-- get_character_import_state(in_fls_id text) -> dune.transferimportstate
-- oid: 58292  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_character_import_state(in_fls_id text)
 RETURNS dune.transferimportstate
 LANGUAGE plpgsql
AS $function$
DECLARE
	v_state TransferImportState;
BEGIN
	SELECT transfer_state INTO v_state FROM character_transfer_imports WHERE fls_id = in_fls_id;
	RETURN v_state;
END;
$function$


-- get_character_transfer_related_items(in_fls_id text) -> jsonb
-- oid: 58293  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.get_character_transfer_related_items(in_fls_id text)
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$
BEGIN

RETURN (
	WITH acct AS (
		SELECT id
		FROM accounts
		WHERE "user" = in_FLS_ID
		LIMIT 1
	)
	SELECT jsonb_build_object(
		'items', COALESCE((
			SELECT jsonb_agg(jsonb_build_object('name', sub.template_id, 'amount', sub.amount))
			FROM (
			SELECT i.template_id, SUM(i.stack_size) AS amount
			FROM inventories inv
			JOIN items i ON i.inventory_id = inv.id
			JOIN player_state ps ON inv.actor_id = ps.player_pawn_id
			WHERE ps.account_id = acct.id AND (
				(inv.inventory_type = 0 AND i.template_id = 'SolarisCoin') OR
				(i.template_id IN ('BaseBackupTool', 'VehicleBackupTool'))
				)
			GROUP BY i.template_id
			) sub
		), '[]'::jsonb),
		'coin_balance', COALESCE((
            SELECT vc.balance from player_virtual_currency_balances vc
            JOIN player_state ps ON ps.account_id = acct.id
            WHERE vc.currency_id = get_solaris_id()
            AND vc.player_controller_id = ps.player_controller_id
			LIMIT 1
		), 0)
	)
	FROM acct
);

END
$function$


-- get_consumed_lore_pickups(in_actor_id bigint, in_use_temporary boolean) -> SETOF bit
-- oid: 58294  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.get_consumed_lore_pickups(in_actor_id bigint, in_use_temporary boolean)
 RETURNS SETOF bit
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF in_use_temporary THEN
		RETURN query
			SELECT consumed_bit_array
			FROM consumed_temporary_per_player_lore
			WHERE actor_id = in_actor_id;
	ELSE
		RETURN query
			SELECT consumed_bit_array
			FROM consumed_per_player_lore
			WHERE actor_id = in_actor_id;
	END IF;
END
$function$


-- get_controller_id_from_platform_id(in_platform_id text) -> bigint
-- oid: 58295  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.get_controller_id_from_platform_id(in_platform_id text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
    out_controller_id BIGINT;
BEGIN
    SELECT ps.player_controller_id
    INTO out_controller_id
    FROM accounts acc LEFT JOIN player_state ps ON acc.id=ps.account_id
    WHERE acc.platform_id = in_platform_id
    LIMIT 1;
    RETURN out_controller_id;
END
$function$


-- get_dune_exchange_accesspoint_id(in_exchange_id bigint, in_name text) -> bigint
-- oid: 58296  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.get_dune_exchange_accesspoint_id(in_exchange_id bigint, in_name text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	new_ap_id BIGINT;
	ap_id BIGINT;
BEGIN
	INSERT INTO dune_exchange_accesspoints(exchange_id, name) VALUES(in_exchange_id, in_name) ON CONFLICT DO NOTHING RETURNING id INTO new_ap_id;
	SELECT INTO ap_id COALESCE(new_ap_id, id) FROM dune_exchange_accesspoints WHERE exchange_id = in_exchange_id AND name = in_name;

	return ap_id;
END $function$


-- get_dune_exchange_data(in_exchange_id bigint, in_controller_id bigint) -> dune.loadexchangedataresult
-- oid: 58297  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.get_dune_exchange_data(in_exchange_id bigint, in_controller_id bigint)
 RETURNS dune.loadexchangedataresult
 LANGUAGE plpgsql
AS $function$
DECLARE
	result LoadExchangeDataResult;
BEGIN
	SELECT INTO result.exchange_name exchange_name FROM dune_exchanges WHERE id=in_exchange_id;
	SELECT INTO result.used_order_slots get_dune_exchange_used_order_slots(in_controller_id);

	RETURN result;
END $function$


-- get_dune_exchange_id(in_name text) -> bigint
-- oid: 58298  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.get_dune_exchange_id(in_name text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	new_exchange_id BIGINT;
	exchange_id BIGINT;
BEGIN
	INSERT INTO dune_exchanges(exchange_name, inventory_id) VALUES(in_name, NULL) ON CONFLICT DO NOTHING RETURNING id INTO new_exchange_id;
	SELECT INTO exchange_id COALESCE(new_exchange_id, id) FROM dune_exchanges WHERE exchange_name = in_name;
	RETURN exchange_id;
END $function$


-- get_dune_exchange_used_order_slots(in_controller_id bigint) -> integer
-- oid: 58299  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.get_dune_exchange_used_order_slots(in_controller_id bigint)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
	result INT;
BEGIN
	SELECT INTO result COUNT(*) FROM dune_exchange_orders where owner_id=in_controller_id AND item_id IS NOT NULL;

	RETURN result;
END $function$


-- get_exchange_inventory_id(in_exchange_id bigint) -> bigint
-- oid: 58300  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.get_exchange_inventory_id(in_exchange_id bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	inv_id BIGINT;
BEGIN
	SELECT INTO inv_id id FROM inventories WHERE "exchange_id" = in_exchange_id;
	IF inv_id IS NULL THEN
		INSERT INTO inventories("id", exchange_id) VALUES(DEFAULT, in_exchange_id) RETURNING id INTO inv_id;
	END IF;
	RETURN inv_id;
END $function$


-- get_exchange_orders_by_mask(in_mask integer, in_depth smallint) -> SETOF bigint
-- oid: 58301  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.get_exchange_orders_by_mask(in_mask integer, in_depth smallint)
 RETURNS SETOF bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	check_mask INT;
	check_shift INT;
BEGIN
	check_shift := (4 - in_depth) * 8;
	check_mask := (in_mask >> check_shift);
	RETURN query SELECT id FROM dune_exchange_orders WHERE category_depth >= in_depth AND (category_mask >> check_shift) = check_mask FOR SHARE;
END
$function$


-- get_exchange_sell_orders(in_id bigint, in_exchange_id bigint, in_min_item_price bigint, in_max_item_price bigint, in_template_id text, in_mask integer, in_depth smallint) -> TABLE(id bigint, revision bigint, expiration_time bigint, access_point_id bigint, ap_name text, owner_id bigint, template_id text, stack_size bigint, initial_stack_size bigint, item_price bigint, quality_level bigint, durability_cur real, durability_max real, dynamic_stats jsonb)
-- oid: 58302  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.get_exchange_sell_orders(in_id bigint, in_exchange_id bigint, in_min_item_price bigint, in_max_item_price bigint, in_template_id text, in_mask integer, in_depth smallint)
 RETURNS TABLE(id bigint, revision bigint, expiration_time bigint, access_point_id bigint, ap_name text, owner_id bigint, template_id text, stack_size bigint, initial_stack_size bigint, item_price bigint, quality_level bigint, durability_cur real, durability_max real, dynamic_stats jsonb)
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
	RETURN query
		SELECT ord.id, ord.revision, ord.expiration_time, ord.access_point_id, ap.name, ord.owner_id, item.template_id, item.stack_size, sord.initial_stack_size, ord.item_price, ord.quality_level, ord.durability_cur, ord.durability_max, item.stats
		FROM dune_exchange_orders ord
		JOIN dune_exchange_sell_orders sord ON (ord.id = sord.order_id)
		JOIN items item ON (ord.item_id = item.id)
		JOIN dune_exchange_accesspoints ap ON (ord.access_point_id = ap.id)
		WHERE 	(in_id IS NOT NULL AND ord.id = in_id) OR
				(in_id IS NULL AND ord.exchange_id = in_exchange_id AND ord.item_price >= in_min_item_price AND (in_max_item_price <= 0 OR ord.item_price <= in_max_item_price) AND
					((in_template_id IS NOT NULL AND ord.template_id = in_template_id) OR
					(in_template_id IS NULL AND ord.id IN (SELECT * FROM get_exchange_orders_by_mask(in_mask, in_depth))))
				)
		FOR SHARE;
END
$function$


-- get_exchange_sell_orders_by_item_type(in_exchange_id bigint, in_template_ids text[]) -> TABLE(id bigint, revision bigint, expiration_time bigint, access_point_id bigint, ap_name text, owner_id bigint, template_id text, stack_size bigint, initial_stack_size bigint, item_price bigint, quality_level bigint, durability_cur real, durability_max real, dynamic_stats jsonb)
-- oid: 58303  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.get_exchange_sell_orders_by_item_type(in_exchange_id bigint, in_template_ids text[])
 RETURNS TABLE(id bigint, revision bigint, expiration_time bigint, access_point_id bigint, ap_name text, owner_id bigint, template_id text, stack_size bigint, initial_stack_size bigint, item_price bigint, quality_level bigint, durability_cur real, durability_max real, dynamic_stats jsonb)
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
	RETURN query
		SELECT ord.id, ord.revision, ord.expiration_time, ord.access_point_id, ap.name, ord.owner_id, item.template_id, item.stack_size, sord.initial_stack_size, ord.item_price, ord.quality_level, ord.durability_cur, ord.durability_max, item.stats
		FROM dune_exchange_orders ord
		JOIN dune_exchange_sell_orders sord ON (ord.id = sord.order_id)
		JOIN items item ON (ord.item_id = item.id)
		JOIN dune_exchange_accesspoints ap ON (ord.access_point_id = ap.id)
		WHERE ord.exchange_id = in_exchange_id AND ord.template_id = ANY(in_template_ids)
		FOR SHARE;
END
$function$


-- get_exchange_sell_orders_by_owner(in_exchange_id bigint, in_owner_id bigint) -> TABLE(id bigint, revision bigint, expiration_time bigint, access_point_id bigint, ap_name text, owner_id bigint, template_id text, stack_size bigint, initial_stack_size bigint, item_price bigint, quality_level bigint, durability_cur real, durability_max real, dynamic_stats jsonb)
-- oid: 58304  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.get_exchange_sell_orders_by_owner(in_exchange_id bigint, in_owner_id bigint)
 RETURNS TABLE(id bigint, revision bigint, expiration_time bigint, access_point_id bigint, ap_name text, owner_id bigint, template_id text, stack_size bigint, initial_stack_size bigint, item_price bigint, quality_level bigint, durability_cur real, durability_max real, dynamic_stats jsonb)
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
	RETURN query
		SELECT ord.id, ord.revision, ord.expiration_time, ord.access_point_id, ap.name, ord.owner_id, item.template_id, item.stack_size, sord.initial_stack_size, ord.item_price, ord.quality_level, ord.durability_cur, ord.durability_max, item.stats
		FROM dune_exchange_orders ord
		JOIN dune_exchange_sell_orders sord ON (ord.id = sord.order_id)
		JOIN items item ON (ord.item_id = item.id)
		JOIN dune_exchange_accesspoints ap ON (ord.access_point_id = ap.id)
		WHERE ord.owner_id = in_owner_id
		FOR SHARE;
END
$function$


-- get_farm_state() -> TABLE(server_id text, farm_id text, outgoing_s2s_connections integer, incoming_s2s_connections integer, connected_players integer, igw_addr inet, igw_port integer, game_addr inet, game_port integer, ready boolean, alive boolean, map text, revision integer)
-- oid: 58305  kind: FUNCTION  category: farm

CREATE OR REPLACE FUNCTION dune.get_farm_state()
 RETURNS TABLE(server_id text, farm_id text, outgoing_s2s_connections integer, incoming_s2s_connections integer, connected_players integer, igw_addr inet, igw_port integer, game_addr inet, game_port integer, ready boolean, alive boolean, map text, revision integer)
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
	RETURN QUERY SELECT fs.server_id, fs.farm_id, fs.outgoing_s2s_connections, fs.incoming_s2s_connections, fs.connected_players, fs.igw_addr, fs.igw_port, fs.game_addr, fs.game_port, fs.ready, fs.alive, fs.map, fs.revision FROM farm_state as fs
		JOIN active_server_ids USING(server_id);
END
$function$


-- get_friends_search(in_player_name text, in_max_players_count integer) -> TABLE(player_id bigint, character_name text, funcom_id text, platform_id text, platform_name text)
-- oid: 58306  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.get_friends_search(in_player_name text, in_max_players_count integer)
 RETURNS TABLE(player_id bigint, character_name text, funcom_id text, platform_id text, platform_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
		RETURN QUERY SELECT player_state.player_controller_id, player_state.character_name, accounts.funcom_id, accounts.platform_id, accounts.platform_name
		FROM player_state
		JOIN accounts ON player_state.account_id = accounts.id
		WHERE player_state.character_name ILIKE '%' || in_player_name || '%'
		ORDER BY ext.SIMILARITY(player_state.character_name, in_player_name) DESC
		LIMIT in_max_players_count;
END
$function$


-- get_guild_data(in_guild_id bigint) -> TABLE(guild_name text, guild_faction_id smallint, guild_description text)
-- oid: 58307  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.get_guild_data(in_guild_id bigint)
 RETURNS TABLE(guild_name text, guild_faction_id smallint, guild_description text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	SELECT guilds.guild_name, guilds.guild_faction, guilds.guild_description
	FROM guilds
	WHERE guild_id = in_guild_id;
END
$function$


-- get_guild_data_for_player(in_player_id bigint) -> TABLE(guild_id bigint, guild_factions_id smallint, guild_name text, guild_description text, player_id bigint, role_id smallint, player_faction_id smallint)
-- oid: 58308  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.get_guild_data_for_player(in_player_id bigint)
 RETURNS TABLE(guild_id bigint, guild_factions_id smallint, guild_name text, guild_description text, player_id bigint, role_id smallint, player_faction_id smallint)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	SELECT guilds.guild_id, guilds.guild_faction, guilds.guild_name, guilds.guild_description, guild_members.player_id, guild_members.role_id, player_faction.faction_id
	FROM guilds JOIN guild_members on (guilds.guild_id = guild_members.guild_id)
	LEFT JOIN player_faction on player_faction.actor_id = guild_members.player_id
	WHERE guild_members.player_id = in_player_id;
END
$function$


-- get_guild_for_player(in_player_id bigint) -> bigint
-- oid: 58309  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.get_guild_for_player(in_player_id bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	found_guild_id integer;
BEGIN
	SELECT guild_id FROM guild_members WHERE player_id = in_player_id INTO found_guild_id;
	IF NOT FOUND THEN
    	RETURN 0;
	END IF;
	RETURN found_guild_id;
END
$function$


-- get_guild_invites(in_guild_id bigint) -> TABLE(invite_id bigint, player_id bigint, sender_player_id bigint, invite_sent_timespan bigint, character_name text, sender_character_name text)
-- oid: 58310  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.get_guild_invites(in_guild_id bigint)
 RETURNS TABLE(invite_id bigint, player_id bigint, sender_player_id bigint, invite_sent_timespan bigint, character_name text, sender_character_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT guild_invites.invite_id, guild_invites.player_id, guild_invites.sender_player_id, guild_invites.invite_sent_timespan, player_state.character_name, sender_player_state.character_name AS sender_character_name
	FROM guild_invites
	JOIN player_state ON player_state.player_controller_id = guild_invites.player_id
	JOIN player_state AS sender_player_state ON sender_player_state.player_controller_id =  guild_invites.sender_player_id
	WHERE guild_id = in_guild_id;
END
$function$


-- get_guild_members(in_guild_id bigint) -> TABLE(player_id bigint, role_id smallint, player_faction_id smallint)
-- oid: 58311  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.get_guild_members(in_guild_id bigint)
 RETURNS TABLE(player_id bigint, role_id smallint, player_faction_id smallint)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	SELECT guild_members.player_id, guild_members.role_id, player_faction.faction_id
	FROM guild_members
	LEFT JOIN player_faction ON player_faction.actor_id = guild_members.player_id
	WHERE guild_id = in_guild_id;
END
$function$


-- get_inventory_data(in_inventory_id bigint) -> dune.inventorydata
-- oid: 58312  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.get_inventory_data(in_inventory_id bigint)
 RETURNS dune.inventorydata
 LANGUAGE plpgsql
AS $function$
DECLARE
    inventory_data InventoryData;
BEGIN
	SELECT INTO
        inventory_data.inventory_id, inventory_data.inventory_type, inventory_data.max_item_count, inventory_data.max_item_volume
        id, inventory_type, max_item_count, max_item_volume
    FROM inventories
    WHERE id = in_inventory_id;

	RETURN inventory_data;
END $function$


-- get_inventory_id(in_actor_id bigint, in_component_name_hash integer) -> bigint
-- oid: 58313  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.get_inventory_id(in_actor_id bigint, in_component_name_hash integer)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	inv_id BIGINT;
BEGIN
	SELECT INTO inv_id inventory_id FROM actor_inventories ai JOIN inventories i ON (ai.inventory_id = i.id) WHERE i.actor_id = in_actor_id AND ai.component_name_hash = in_component_name_hash;
	IF inv_id IS NULL THEN
		INSERT INTO inventories("id", "actor_id") VALUES(DEFAULT, in_actor_id) RETURNING id INTO inv_id;
		INSERT INTO actor_inventories("inventory_id", "component_name_hash") VALUES(inv_id, in_component_name_hash);
	END IF;
	RETURN inv_id;
END $function$


-- get_items_to_remove(items_to_remove text[]) -> text[]
-- oid: 58314  kind: FUNCTION  category: items_purge

CREATE OR REPLACE FUNCTION dune.get_items_to_remove(items_to_remove text[])
 RETURNS text[]
 LANGUAGE plpgsql
AS $function$
declare
    result text[];
begin
    select array(
        select unnest(items_to_remove)
        except
        select name
        from removed_items
    ) into result;
    return result;
end;
$function$


-- get_landclaim_segments(in_totem_id bigint) -> TABLE(grid_location_x bigint, grid_location_y bigint)
-- oid: 58315  kind: FUNCTION  category: landclaim

CREATE OR REPLACE FUNCTION dune.get_landclaim_segments(in_totem_id bigint)
 RETURNS TABLE(grid_location_x bigint, grid_location_y bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT t.grid_location_x, t.grid_location_y
    FROM landclaim_segments AS t
    WHERE t.totem_id = in_totem_id;
END; $function$


-- get_learned_building_sets(in_account_id bigint) -> SETOF text
-- oid: 58316  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_learned_building_sets(in_account_id bigint)
 RETURNS SETOF text
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT UNNEST(learned_building_sets) FROM building_progression WHERE account_id = in_account_id;
END; $function$


-- get_learned_new_buildable_pieces(in_account_id bigint) -> SETOF text
-- oid: 58317  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_learned_new_buildable_pieces(in_account_id bigint)
 RETURNS SETOF text
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT UNNEST(new_buildable_pieces) FROM building_progression WHERE account_id = in_account_id;
END; $function$


-- get_login_journey_nodes(in_account_id bigint) -> dune.journeynodeinfo[]
-- oid: 58318  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_login_journey_nodes(in_account_id bigint)
 RETURNS dune.journeynodeinfo[]
 LANGUAGE plpgsql
AS $function$
DECLARE
    result JourneyNodeInfo[];
BEGIN
    SELECT
        array_agg(
            (
             res.story_node_id,
             res.override_reward_block,
             res.has_pending_reward,
             res.complete_condition_state,
             res.reveal_condition_state,
             res.fail_condition_state,
             res.metadata_state,
             res.reset_group
            )::JourneyNodeInfo
        )
    INTO result
    FROM journey_story_node res
    WHERE res.account_id = in_account_id;

    RETURN result;
END
$function$


-- get_login_journey_nodes_cooldown(in_account_id bigint) -> dune.journeynodecooldowninfo[]
-- oid: 58319  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_login_journey_nodes_cooldown(in_account_id bigint)
 RETURNS dune.journeynodecooldowninfo[]
 LANGUAGE plpgsql
AS $function$
DECLARE
    result JourneyNodeCooldownInfo[];
BEGIN
    SELECT
        array_agg(
            (
             res.story_node_id,
             res.time_to_expire
            )::JourneyNodeCooldownInfo
        )
    INTO result
    FROM journey_story_node_cooldown res
    WHERE res.account_id = in_account_id;

    RETURN result;
END
$function$


-- get_mnemonic_recall_lessons(in_account_id bigint) -> TABLE(id bigint, lesson_id text, lession_state bigint, lesson_progress integer, is_new boolean)
-- oid: 58320  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.get_mnemonic_recall_lessons(in_account_id bigint)
 RETURNS TABLE(id bigint, lesson_id text, lession_state bigint, lesson_progress integer, is_new boolean)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT t.id, t.lesson_id, t.lesson_state, t.lesson_progress, t.is_new
    FROM mnemonic_recall as t
    WHERE t.account_id = in_account_id
    ORDER BY t.id;
END; $function$


-- get_online_player_controller_ids(in_map text) -> SETOF bigint
-- oid: 58321  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_online_player_controller_ids(in_map text)
 RETURNS SETOF bigint
 LANGUAGE plpgsql
AS $function$
BEGIN
    return query SELECT DISTINCT ps.player_controller_id
        FROM player_state ps
        JOIN actors a ON (a.id = ps.player_controller_id)
        WHERE ps.online_status = 'Online' AND a.map = in_map;
END;
$function$


-- get_online_player_controller_ids_on_farm() -> SETOF bigint
-- oid: 58322  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_online_player_controller_ids_on_farm()
 RETURNS SETOF bigint
 LANGUAGE plpgsql
AS $function$
BEGIN
    return query SELECT DISTINCT ps.player_controller_id
        FROM player_state ps
        JOIN actors a ON (a.id = ps.player_controller_id)
        WHERE ps.online_status = 'Online';
END;
$function$


-- get_partition_presets() -> SETOF text
-- oid: 58323  kind: FUNCTION  category: partition
-- comment: Adds a partition only if its unique. Not using constraints, as this is only a helper function.

CREATE OR REPLACE FUNCTION dune.get_partition_presets()
 RETURNS SETOF text
 LANGUAGE plpgsql
AS $function$
BEGIN
	return query SELECT routine_name::text as preset_function FROM information_schema.routines WHERE routine_type = 'FUNCTION' and routine_name ilike 'initialize_partitions_%';
END;
$function$


-- get_partitions(in_map text) -> SETOF bigint
-- oid: 58324  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.get_partitions(in_map text)
 RETURNS SETOF bigint
 LANGUAGE plpgsql
AS $function$
begin
	SELECT partition_id FROM world_partition where map = in_map order by partition_id ASC;
end
$function$


-- get_party_members(in_party_id bigint) -> TABLE(player_id bigint, fls_id text, party_id bigint)
-- oid: 58325  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.get_party_members(in_party_id bigint)
 RETURNS TABLE(player_id bigint, fls_id text, party_id bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	SELECT pm.player_id, acc.user, pm.party_id
	FROM party_members pm
	LEFT JOIN player_state ps ON ps.player_controller_id = pm.player_id
	LEFT JOIN accounts acc ON acc.id = ps.account_id
	WHERE pm.party_id = in_party_id;
END
$function$


-- get_permission_actors_for_server(in_server_info dune.serverinfo) -> SETOF dune.actorpermissioncombineddata
-- oid: 58326  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.get_permission_actors_for_server(in_server_info dune.serverinfo)
 RETURNS SETOF dune.actorpermissioncombineddata
 LANGUAGE plpgsql
AS $function$
BEGIN
	return query
		with ids as (
			select array_agg(actors.id) as ids
				from permission_actor join actors on actors.id = permission_actor.actor_id
				where server_info_match(actors, in_server_info) and actors.owner_account_id is null
		)
		select permissions.* from ids, get_permission_for_actors(ids.ids) as permissions;
END
$function$


-- get_permission_for_actor(in_actor_id bigint) -> dune.actorpermissioncombineddata
-- oid: 58327  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.get_permission_for_actor(in_actor_id bigint)
 RETURNS dune.actorpermissioncombineddata
 LANGUAGE plpgsql
AS $function$
BEGIN
	return (select get_permission_for_actors(array[in_actor_id]) limit 1);
END
$function$


-- get_permission_for_actors(in_actor_id bigint[]) -> SETOF dune.actorpermissioncombineddata
-- oid: 58328  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.get_permission_for_actors(in_actor_id bigint[])
 RETURNS SETOF dune.actorpermissioncombineddata
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
		SELECT
			-- ActorPermissionData
            ROW(
                ROW(
                    permission_actor.actor_id,
                    permission_actor.actor_name,
                    actors.class,
                    permission_actor.actor_type,
                    permission_actor.access_level,
                    permission_actor.is_child
                )::ActorPermissionEntry,
                array_agg(
                    ROW(
                        permission_actor_rank.rank,
                        permission_actor_rank.player_id
                    )::ActorPermissionRankData
                ) FILTER (WHERE permission_actor_rank.player_id IS NOT NULL),
                array_agg(guild_members.guild_id) FILTER (WHERE permission_actor_rank.player_id IS NOT NULL)
            )::ActorPermissionData AS data,

            -- ActorPermissionLocationData
            ROW(
                actors.id,
                actors.partition_id,
                actors.map,
                actors.dimension_index,
                actors.transform
            )::ActorPermissionLocationData AS loc
		FROM
			permission_actor
			LEFT JOIN permission_actor_rank on permission_actor.actor_id = permission_actor_rank.permission_actor_id
			LEFT JOIN guild_members ON guild_members.player_id = permission_actor_rank.player_id
			LEFT JOIN actors ON actors.id = permission_actor.actor_id
		WHERE
			permission_actor.actor_id = ANY(in_actor_id)
		GROUP BY
			permission_actor.actor_id,
			actors.id;
END
$function$


-- get_permission_for_player_actors(in_player_id bigint, in_min_rank smallint) -> SETOF dune.actorpermissioncombineddata
-- oid: 58329  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.get_permission_for_player_actors(in_player_id bigint, in_min_rank smallint)
 RETURNS SETOF dune.actorpermissioncombineddata
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	WITH player_owned_actors AS
	(
		SELECT array_agg(permission_actor_id) AS ids
		FROM permission_actor_rank join actors on actors.id = permission_actor_rank.permission_actor_id
		WHERE permission_actor_rank.player_id = in_player_id AND permission_actor_rank.rank <= in_min_rank and actors.owner_account_id is null
	)
	SELECT permissions.* FROM player_owned_actors, get_permission_for_actors(player_owned_actors.ids) AS permissions;
END
$function$


-- get_placeable_id(in_actor_id bigint, in_class text, in_building_type text) -> dune.placeablegetidcomposite
-- oid: 58330  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.get_placeable_id(in_actor_id bigint, in_class text, in_building_type text)
 RETURNS dune.placeablegetidcomposite
 LANGUAGE plpgsql
AS $function$
DECLARE
	placeable_id BIGINT;
BEGIN
	SELECT INTO placeable_id id FROM placeables WHERE "id" = in_actor_id;

	IF placeable_id IS NULL THEN
		SELECT assign_actor_id(in_class) id INTO placeable_id;
		INSERT INTO placeables("id", "building_type") VALUES(placeable_id, in_building_type);
	END IF;

    return ROW(placeable_id)::PlaceableGetIdComposite;
END
$function$


-- get_player_access_codes(in_account_id bigint) -> TABLE(access_code integer, access_code_type integer)
-- oid: 58331  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_player_access_codes(in_account_id bigint)
 RETURNS TABLE(access_code integer, access_code_type integer)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT p.access_code, p.access_code_type
    FROM player_access_codes AS p
    WHERE p.account_id = in_account_id;
END; $function$


-- get_player_current_faction_reputation(in_actor_id bigint, OUT out_faction_id smallint, OUT out_reputation_amount integer) -> record
-- oid: 58332  kind: FUNCTION  category: faction

CREATE OR REPLACE FUNCTION dune.get_player_current_faction_reputation(in_actor_id bigint, OUT out_faction_id smallint, OUT out_reputation_amount integer)
 RETURNS record
 LANGUAGE plpgsql
AS $function$
BEGIN
	SELECT
		pf.faction_id,
		COALESCE(pfr.reputation_amount, 0)
	INTO out_faction_id, out_reputation_amount
	FROM player_faction pf
	LEFT JOIN player_faction_reputation pfr
		ON pfr.actor_id = pf.actor_id AND pfr.faction_id = pf.faction_id
	WHERE pf.actor_id = in_actor_id
	limit 1;
END
$function$


-- get_player_faction(in_player_id bigint, in_neutral_faction_id smallint) -> smallint
-- oid: 58333  kind: FUNCTION  category: faction

CREATE OR REPLACE FUNCTION dune.get_player_faction(in_player_id bigint, in_neutral_faction_id smallint)
 RETURNS smallint
 LANGUAGE plpgsql
AS $function$
DECLARE 
	player_faction_id SMALLINT;
BEGIN
	SELECT player_faction.faction_id INTO player_faction_id
	FROM player_faction
	WHERE actor_id = in_player_id;

	IF player_faction_id IS NULL THEN
		player_faction_id := in_neutral_faction_id;
	END IF;

	RETURN player_faction_id;
END;
$function$


-- get_player_faction_name(in_actor_id bigint, OUT player_faction_name text, OUT utc_time_faction_change timestamp without time zone) -> record
-- oid: 58334  kind: FUNCTION  category: faction

CREATE OR REPLACE FUNCTION dune.get_player_faction_name(in_actor_id bigint, OUT player_faction_name text, OUT utc_time_faction_change timestamp without time zone)
 RETURNS record
 LANGUAGE plpgsql
AS $function$
BEGIN
	SELECT factions.name, player_faction.utc_time_faction_change AT TIME ZONE 'UTC' INTO player_faction_name, utc_time_faction_change
	FROM factions INNER JOIN player_faction ON factions.id = player_faction.faction_id 
	WHERE player_faction.actor_id = in_actor_id 
	limit 1;
END; $function$


-- get_player_guild_invites(in_player_id bigint) -> TABLE(invite_id bigint, guild_id bigint, guild_name text, guild_description text, sender_player_id bigint, invite_sent_timespan bigint, character_name text, sender_character_name text)
-- oid: 58335  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_player_guild_invites(in_player_id bigint)
 RETURNS TABLE(invite_id bigint, guild_id bigint, guild_name text, guild_description text, sender_player_id bigint, invite_sent_timespan bigint, character_name text, sender_character_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT guild_invites.invite_id, guilds.guild_id, guilds.guild_name, guilds.guild_description, guild_invites.sender_player_id, guild_invites.invite_sent_timespan, player_state.character_name, sender_player_state.character_name AS sender_character_name
	FROM guild_invites
	JOIN guilds ON guilds.guild_id = guild_invites.guild_id
	JOIN player_state ON player_state.player_controller_id = guild_invites.player_id
	JOIN player_state AS sender_player_state ON sender_player_state.player_controller_id = guild_invites.sender_player_id
	WHERE player_id = in_player_id;
END
$function$


-- get_player_ids_online_state(in_player_ids bigint[]) -> SETOF dune.playeronlinestateentry
-- oid: 58336  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_player_ids_online_state(in_player_ids bigint[])
 RETURNS SETOF dune.playeronlinestateentry
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT actors.id, player_state.character_name, (actors.map, actors.partition_id, actors.dimension_index)::ServerInfo, (player_state.last_avatar_activity AT TIME ZONE 'UTC')::TIMESTAMP, player_state.online_status
	FROM actors
	JOIN player_state ON player_state.player_controller_id = actors.id
	WHERE actors.id = ANY(in_player_ids);
END
$function$


-- get_player_infos_for_actor_ids(in_actor_ids bigint[]) -> TABLE(player_id bigint, character_name text, fls_id text, funcom_id text, platform_id text, platform_name text)
-- oid: 58337  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_player_infos_for_actor_ids(in_actor_ids bigint[])
 RETURNS TABLE(player_id bigint, character_name text, fls_id text, funcom_id text, platform_id text, platform_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT ps.player_controller_id, ps.character_name, acc.user, acc.funcom_id, acc.platform_id, acc.platform_name
	FROM accounts acc LEFT JOIN player_state ps ON acc.id=ps.account_id
	WHERE ps.player_controller_id = ANY(in_actor_ids);
END
$function$


-- get_player_infos_for_character_names(in_character_names text[]) -> TABLE(player_id bigint, character_name text, fls_id text, funcom_id text, platform_id text, platform_name text)
-- oid: 58338  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_player_infos_for_character_names(in_character_names text[])
 RETURNS TABLE(player_id bigint, character_name text, fls_id text, funcom_id text, platform_id text, platform_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT ps.player_controller_id, ps.character_name, acc.user, acc.funcom_id, acc.platform_id, acc.platform_name
	FROM accounts acc LEFT JOIN player_state ps ON acc.id=ps.account_id
	WHERE ps.character_name = ANY(in_character_names);
END
$function$


-- get_player_infos_for_fls_ids(in_fls_ids text[]) -> TABLE(player_id bigint, character_name text, fls_id text, funcom_id text, platform_id text, platform_name text)
-- oid: 58339  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_player_infos_for_fls_ids(in_fls_ids text[])
 RETURNS TABLE(player_id bigint, character_name text, fls_id text, funcom_id text, platform_id text, platform_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT ps.player_controller_id, ps.character_name, acc.user, acc.funcom_id, acc.platform_id, acc.platform_name
	FROM accounts acc LEFT JOIN player_state ps ON acc.id=ps.account_id
	WHERE acc.user = ANY(in_fls_ids);
END
$function$


-- get_player_infos_for_funcom_ids(in_funcom_ids text[]) -> TABLE(player_id bigint, character_name text, fls_id text, funcom_id text, platform_id text, platform_name text)
-- oid: 58340  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_player_infos_for_funcom_ids(in_funcom_ids text[])
 RETURNS TABLE(player_id bigint, character_name text, fls_id text, funcom_id text, platform_id text, platform_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT ps.player_controller_id, ps.character_name, acc.user, acc.funcom_id, acc.platform_id, acc.platform_name
	FROM accounts acc LEFT JOIN player_state ps ON acc.id=ps.account_id
	WHERE acc.funcom_id = ANY(in_funcom_ids);
END
$function$


-- get_player_online_state_within_grace_period_for_each_server() -> TABLE(fls_id text, previous_partition_id bigint, current_server_id text, online_status dune.playerconnectionstatus, within_grace_period boolean, last_disconnect timestamp without time zone, demo_playtime_seconds integer, logoff_persistence_end_time timestamp without time zone, party_id bigint)
-- oid: 58341  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_player_online_state_within_grace_period_for_each_server()
 RETURNS TABLE(fls_id text, previous_partition_id bigint, current_server_id text, online_status dune.playerconnectionstatus, within_grace_period boolean, last_disconnect timestamp without time zone, demo_playtime_seconds integer, logoff_persistence_end_time timestamp without time zone, party_id bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY SELECT accounts.user as fls_id, player_state.previous_server_partition_id as previous_partition_id, player_state.server_id as current_server_id, player_state.online_status as online_status, player_state.reconnect_grace_period_end > (now() AT TIME ZONE 'UTC')::TIMESTAMP as within_grace_period, (player_state.last_avatar_activity AT TIME ZONE 'UTC')::TIMESTAMP as last_disconnect, demo_users.demo_playtime_seconds, (player_state.logoff_persistence_end_time)::TIMESTAMP as logoff_persistence_end_time, party_members.party_id
    FROM player_state
    LEFT JOIN accounts ON accounts.id = player_state.account_id
    LEFT JOIN demo_users ON accounts.user = demo_users.fls_id
    LEFT JOIN party_members ON player_state.player_controller_id = party_members.player_id;
END
$function$


-- get_player_owned_vehicles_data(in_player_id bigint, in_account_id bigint) -> TABLE(out_actor_id bigint, out_name text, out_class text, out_map text, out_partition_id bigint, out_dimension integer, out_transform dune.transform, out_actor_state text)
-- oid: 58342  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.get_player_owned_vehicles_data(in_player_id bigint, in_account_id bigint)
 RETURNS TABLE(out_actor_id bigint, out_name text, out_class text, out_map text, out_partition_id bigint, out_dimension integer, out_transform dune.transform, out_actor_state text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY WITH owned_actors AS(
		SELECT (entry).actor_id, (entry).actor_name FROM get_permission_for_player_actors(in_player_id, 1::smallint) -- 1:owner
			WHERE (entry).actor_type = 2 -- 2:vehicles
		UNION SELECT vehicle_id, vehicle_name FROM recovered_vehicles -- note: recovered vehicles are removed from permission actors
			WHERE account_id = in_account_id
	) SELECT owned_actors.actor_id, actor_name, class, map, partition_id, dimension_index, transform, (actor_state).state::text
		FROM owned_actors LEFT JOIN actors ON actors.id = owned_actors.actor_id LEFT JOIN actor_state ON actor_state.actor_id = owned_actors.actor_id;
END
$function$


-- get_player_partition_id(in_fls_id text) -> bigint
-- oid: 58343  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_player_partition_id(in_fls_id text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
BEGIN
    -- Only the player pawn actually travels and gets it's partition id updated
    return (
        select actors.partition_id
        from accounts as acc
        join player_state as ps on ps.account_id = acc.id
        join actors on actors.id = ps.player_pawn_id
        where acc.user = in_fls_id
    );
END
$function$


-- get_player_pawn(in_account_id bigint) -> TABLE(description dune.actordescription, server_info dune.serverinfo, player_tags text[])
-- oid: 58344  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_player_pawn(in_account_id bigint)
 RETURNS TABLE(description dune.actordescription, server_info dune.serverinfo, player_tags text[])
 LANGUAGE plpgsql
AS $function$
BEGIN
    return query
        with id as (select player_pawn_id as id from player_state where account_id = in_account_id limit 1),
        tags as (select array_agg(tag) from player_tags where account_id = in_account_id)
            select load_full_actors(array[id]) as description, get_actor_server_info(id) as server_info, (select * from tags) as player_tags
            from id limit 1;
END
$function$


-- get_player_virtual_currency_balances(in_controller_id bigint) -> TABLE(out_currency_id smallint, out_currency_balance bigint)
-- oid: 58345  kind: FUNCTION  category: currency

CREATE OR REPLACE FUNCTION dune.get_player_virtual_currency_balances(in_controller_id bigint)
 RETURNS TABLE(out_currency_id smallint, out_currency_balance bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
	return query (
		with currencies as (select currency_id, balance from player_virtual_currency_balances where player_controller_id = in_controller_id),
		bad_currencies as (select * from currencies where balance < 0),
		target_account_id as (select account_id from player_state where player_controller_id = in_controller_id limit 1),
		report_cheaters as (select currency_id, flag_player_as_cheater(target_account_id.account_id, 'negative_solaris') from bad_currencies, target_account_id),
		fix_bad_currencies as (update player_virtual_currency_balances set balance = 0 from report_cheaters where player_controller_id = in_controller_id and player_virtual_currency_balances.currency_id = report_cheaters.currency_id)
		select currency_id, balance from currencies
	);
END;
$function$


-- get_players_demo_data(in_controller_ids bigint[]) -> SETOF dune.playerdemostatedescription
-- oid: 58346  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_players_demo_data(in_controller_ids bigint[])
 RETURNS SETOF dune.playerdemostatedescription
 LANGUAGE plpgsql
AS $function$
BEGIN
 	RETURN QUERY SELECT ps.player_controller_id, (ps.last_avatar_activity AT TIME ZONE 'UTC')::TIMESTAMP, demo_playtime_seconds, demo_state
	FROM encrypted_accounts acc
	JOIN player_state ps ON ps.account_id = acc.id
	JOIN demo_users du ON acc.user = du.fls_id
    WHERE ps.player_controller_id = ANY (in_controller_ids);
END
$function$


-- get_recipes_to_remove(recipes_to_remove text[]) -> text[]
-- oid: 58347  kind: FUNCTION  category: items_purge

CREATE OR REPLACE FUNCTION dune.get_recipes_to_remove(recipes_to_remove text[])
 RETURNS text[]
 LANGUAGE plpgsql
AS $function$
declare
    result text[];
begin
    select array(
        select unnest(recipes_to_remove)
        except
        select name
        from removed_recipes
    ) into result;
    return result;
end;
$function$


-- get_registered_spawned_actor(in_spawner_id bigint) -> SETOF bigint
-- oid: 58348  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.get_registered_spawned_actor(in_spawner_id bigint)
 RETURNS SETOF bigint
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
	SELECT actor_id FROM actor_spawner_actors WHERE spawner_id = in_spawner_id;
END; $function$


-- get_respawn_locations(in_account_id bigint) -> dune.respawnlocation[]
-- oid: 58349  kind: FUNCTION  category: spawner

CREATE OR REPLACE FUNCTION dune.get_respawn_locations(in_account_id bigint)
 RETURNS dune.respawnlocation[]
 LANGUAGE plpgsql
AS $function$
DECLARE
    result RespawnLocation[];
BEGIN
    SELECT
        array_agg(
            (res.id,
             (
                 CASE
                     WHEN res.locator_transform IS NOT NULL THEN 'Transform'
                     WHEN res.locator_actor_id IS NOT NULL THEN 'PersistentActor'
                     WHEN res.locator_name IS NOT NULL THEN 'StaticLocatorName'
                 END::SpawnLocatorType,
                 res.locator_transform,
                 res.locator_actor_id,
                 res.locator_name,
                 res.locator_name_index
             )::SpawnLocatorDescriptor,
             res.map,
             res.dimension,
             res.last_used_timestamp,
             res.group
            )::RespawnLocation
        )
    INTO result
    FROM player_respawn_locations res
    WHERE res.account_id = in_account_id;

    RETURN result;
END
$function$


-- get_schema_version() -> integer
-- oid: 58350  kind: FUNCTION  category: schema_meta

CREATE OR REPLACE FUNCTION dune.get_schema_version()
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
BEGIN
	return 999999;
END;
$function$


-- get_solaris_id() -> smallint
-- oid: 58351  kind: FUNCTION  category: currency

CREATE OR REPLACE FUNCTION dune.get_solaris_id()
 RETURNS smallint
 LANGUAGE plpgsql
 IMMUTABLE
AS $function$
DECLARE
    solaris_id CONSTANT SMALLINT := 0;
BEGIN
    return solaris_id;
END
$function$


-- get_spawner_id(in_map text, in_name text, in_dimension_index integer) -> bigint
-- oid: 58352  kind: FUNCTION  category: spawner

CREATE OR REPLACE FUNCTION dune.get_spawner_id(in_map text, in_name text, in_dimension_index integer)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	spawner_id BIGINT;
BEGIN
    SELECT INTO spawner_id "id" FROM actor_spawners WHERE map = in_map AND name = in_name AND dimension_index = in_dimension_index;
    IF spawner_id IS NULL THEN
        INSERT INTO actor_spawners("map", "name", "dimension_index") VALUES(in_map, in_name, in_dimension_index) ON CONFLICT DO NOTHING RETURNING "id" INTO spawner_id;
        IF spawner_id IS NULL THEN
            SELECT INTO spawner_id "id" FROM actor_spawners WHERE map = in_map AND name = in_name AND dimension_index = in_dimension_index;
        END IF;
    END IF;
    RETURN spawner_id;
END $function$


-- get_stored_user_data_encryption_key_hash() -> bytea
-- oid: 58353  kind: FUNCTION  category: encryption

CREATE OR REPLACE FUNCTION dune.get_stored_user_data_encryption_key_hash()
 RETURNS bytea
 LANGUAGE sql
 IMMUTABLE
AS $function$select null::bytea;$function$


-- get_stored_user_data_encryption_status() -> dune.userdataencryptionstatus
-- oid: 58354  kind: FUNCTION  category: encryption

CREATE OR REPLACE FUNCTION dune.get_stored_user_data_encryption_status()
 RETURNS dune.userdataencryptionstatus
 LANGUAGE sql
 IMMUTABLE
AS $function$select 'Disabled'::UserDataEncryptionStatus$function$


-- get_stored_user_data_encryption_taint_xmax() -> bigint
-- oid: 58355  kind: FUNCTION  category: encryption

CREATE OR REPLACE FUNCTION dune.get_stored_user_data_encryption_taint_xmax()
 RETURNS bigint
 LANGUAGE sql
 IMMUTABLE
AS $function$select null::int8;$function$


-- get_sub_inventory_id(in_owner_item_id bigint) -> bigint
-- oid: 58356  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.get_sub_inventory_id(in_owner_item_id bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	inv_id BIGINT;
BEGIN
	SELECT INTO inv_id id FROM inventories WHERE item_id = in_owner_item_id;
	IF inv_id IS NULL THEN
		INSERT INTO inventories("id", "item_id") VALUES(DEFAULT, in_owner_item_id) RETURNING id INTO inv_id;
	END IF;
	RETURN inv_id;
END $function$


-- get_traveling_actor_id_and_types(in_actor_id bigint) -> TABLE(id bigint, is_instigator boolean, is_player boolean, level integer)
-- oid: 58357  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_traveling_actor_id_and_types(in_actor_id bigint)
 RETURNS TABLE(id bigint, is_instigator boolean, is_player boolean, level integer)
 LANGUAGE plpgsql
AS $function$
begin
	return query
	select t.id, t.is_instigator, (ps.player_pawn_id is not null) as is_player, t.level
	from get_traveling_actor_ids(in_actor_id) as t
	left join player_state as ps
	on t.id = ps.player_pawn_id
	order by t.level, t.id;
end
$function$


-- get_traveling_actor_ids(in_actor_id bigint, in_max_recursion_level integer) -> TABLE(id bigint, is_instigator boolean, level integer)
-- oid: 58358  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_traveling_actor_ids(in_actor_id bigint, in_max_recursion_level integer DEFAULT 5)
 RETURNS TABLE(id bigint, is_instigator boolean, level integer)
 LANGUAGE plpgsql
AS $function$
begin
	return query
	with recursive
		-- go down the dependency tree and gather all the parents
        p(id, lvl) AS (
				select a.id, 0
				from actors as a
				where (in_actor_id = a.id)
			union all
				select ap.parent_id, (p.lvl - 1)
				from p, travel_actor_parent as ap
				where (p.id = ap.id) and (p.lvl > -in_max_recursion_level)
		),
		-- find the most distant parent (root)
		r as (
			select * from p	order by p.lvl limit 1
		),
		-- go up the tree from the root and gather all the children
		t(id, ins, lvl) as (
				select r.id, true, 0 from r
			union all
				select ap.id, ap.is_instigator, (t.lvl + 1)
				from t, travel_actor_parent as ap
				where (t.id = ap.parent_id) and (t.lvl < 2 * in_max_recursion_level)
		)
	select * from t order by t.lvl, id;
end
$function$


-- get_traveling_actors_fls_ids(in_actor_id bigint) -> TABLE(out_id text)
-- oid: 58359  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_traveling_actors_fls_ids(in_actor_id bigint)
 RETURNS TABLE(out_id text)
 LANGUAGE plpgsql
AS $function$
begin
	return query
	select a.user
	from get_traveling_actor_ids(in_actor_id) as t
	inner join player_state as ps
		on t.id = ps.player_pawn_id
	inner join accounts as a
		on a.id = ps.account_id;
end
$function$


-- get_traveling_non_player_actor_ids(in_actor_id bigint) -> TABLE(id bigint)
-- oid: 58360  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.get_traveling_non_player_actor_ids(in_actor_id bigint)
 RETURNS TABLE(id bigint)
 LANGUAGE plpgsql
AS $function$
begin
	return query
	select t.id
	from get_traveling_actor_ids(in_actor_id) as t
	left join player_state as ps
		on t.id = ps.player_pawn_id
	left join travel_actor_parent as ap
		on ap.id = in_actor_id
	where
			ps.player_pawn_id is null
		and ap.is_instigator is true
	order by t.level, t.id;
end
$function$


-- get_unbacked_up_vehicle_ids_for_account(in_account_id bigint) -> TABLE(vehicle_id bigint)
-- oid: 58361  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.get_unbacked_up_vehicle_ids_for_account(in_account_id bigint)
 RETURNS TABLE(vehicle_id bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
        SELECT v.id
		FROM vehicles v
		JOIN permission_actor_rank par ON v.id = par.permission_actor_id
		JOIN player_state ps ON par.player_id = ps.player_controller_id
		left join backup_vehicles bv ON v.id = bv.vehicle_id
		WHERE ps.account_id = in_account_id
			AND bv.vehicle_id IS NULL;
END
$function$


-- get_universe_time() -> TABLE(universe_time_timestamp timestamp without time zone, down_time_accumulation bigint)
-- oid: 58362  kind: FUNCTION  category: schema_meta

CREATE OR REPLACE FUNCTION dune.get_universe_time()
 RETURNS TABLE(universe_time_timestamp timestamp without time zone, down_time_accumulation bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT farm_variables.universe_time_timestamp, farm_variables.down_time_accumulation from farm_variables;
	RETURN;
END
$function$


-- get_unsaved_base_totem_ids_for_account(in_account_id bigint) -> TABLE(totem_id bigint)
-- oid: 58363  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.get_unsaved_base_totem_ids_for_account(in_account_id bigint)
 RETURNS TABLE(totem_id bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
		SELECT t.id
		FROM totems t
		JOIN permission_actor_rank par ON t.id = par.permission_actor_id
		JOIN player_state ps ON par.player_id = ps.player_controller_id
		LEFT JOIN base_backup_linked_actors bbla ON t.id = bbla.actor_id
		WHERE ps.account_id = in_account_id
			AND bbla.actor_id IS NULL;
END
$function$


-- get_vehicle_id(in_actor_id bigint, in_class text) -> bigint
-- oid: 58364  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.get_vehicle_id(in_actor_id bigint, in_class text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	vehicle_id BIGINT;
BEGIN
	SELECT INTO vehicle_id id FROM vehicles WHERE "id" = in_actor_id;
	IF vehicle_id IS NULL THEN
		SELECT assign_actor_id(in_class) id INTO vehicle_id;
		INSERT INTO vehicles("id") VALUES(vehicle_id);
	END IF;
	RETURN vehicle_id;
END
$function$


-- get_vehicle_module_inventory_id(in_vehicle_module_id bigint, in_vehicle_module_inventory_type integer) -> bigint
-- oid: 58365  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.get_vehicle_module_inventory_id(in_vehicle_module_id bigint, in_vehicle_module_inventory_type integer)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	inv_id BIGINT;
BEGIN
	SELECT INTO inv_id inventory_id FROM vehicle_module_inventories vi JOIN inventories i ON (vi.inventory_id = i.id) WHERE i.vehicle_module_id = in_vehicle_module_id AND vi.vehicle_module_inventory_type = in_vehicle_module_inventory_type;
	IF inv_id IS NULL THEN
		INSERT INTO inventories("id", "vehicle_module_id") VALUES(DEFAULT, in_vehicle_module_id) RETURNING id INTO inv_id;
		INSERT INTO vehicle_module_inventories("inventory_id", "vehicle_module_inventory_type") VALUES(inv_id, in_vehicle_module_inventory_type);
	END IF;
	RETURN inv_id;
END $function$


-- guild_handle_actor_delete(in_player_id bigint) -> void
-- oid: 58366  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.guild_handle_actor_delete(in_player_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_guild_id BIGINT;
	out_new_leader_id BIGINT;
	out_guild_count INT;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	-- remove invites
	PERFORM reject_guild_invite(invite_id) FROM guild_invites
	where player_id = in_player_id OR sender_player_id = in_player_id;

	-- Get member guild id
	SELECT guild_id INTO out_guild_id FROM guild_members WHERE player_id = in_player_id;
	IF FOUND THEN

		SELECT INTO out_guild_count COUNT(*) FROM guild_members WHERE guild_id = out_guild_id;

		IF out_guild_count < 2 THEN
			PERFORM disband_guild(out_guild_id);
		ELSE
			-- Promote new leder
			IF is_player_guild_admin(in_player_id, out_guild_id) THEN
				SELECT player_id into out_new_leader_id from guild_members
				WHERE guild_id = out_guild_id AND player_id <> in_player_id
				LIMIT 1;
				IF out_new_leader_id IS NOT NULL THEN
					PERFORM promote_guild_member(out_guild_id, out_new_leader_id, 100::smallint);
				END IF;
			END IF;
			-- remove member
			PERFORM remove_guild_members(ARRAY[in_player_id], out_guild_id, 0::smallint);
		END IF;
	END IF;

END
$function$


-- guilds_get_exclusive_operation_lock() -> void
-- oid: 58367  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.guilds_get_exclusive_operation_lock()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    PERFORM pg_advisory_xact_lock(601145);  -- GUILDS in leet :/
END;
$function$


-- handle_player_faction_guild_effects(in_player_id bigint, in_faction_id smallint, neutral_faction_id smallint) -> void
-- oid: 58368  kind: FUNCTION  category: faction

CREATE OR REPLACE FUNCTION dune.handle_player_faction_guild_effects(in_player_id bigint, in_faction_id smallint, neutral_faction_id smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	guild_member_record record;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	SELECT * INTO guild_member_record
	FROM guild_members
	JOIN guilds ON guilds.guild_id = guild_members.guild_id
	WHERE player_id = in_player_id;

	IF guild_member_record IS NOT NULL THEN
		PERFORM pg_notify('guild_notify_channel', format('player_guild_data_changed#{"GuildId" : %s , "PlayerId" : %s, "FactionId" : %s}', guild_member_record.guild_id, in_player_id, in_faction_id));
	 	IF guild_member_record.guild_faction != neutral_faction_id THEN
			-- If guild leader changes faction and guild already has a non neutral faction, break the guild allegiance
			IF is_player_guild_admin(in_player_id, guild_member_record.guild_id) THEN
				PERFORM break_guild_allegiance(guild_member_record.guild_id, neutral_faction_id);
			-- Neutral player changing to Faction A while Guild is Faction B must be kicked
			ELSEIF guild_member_record.guild_faction != in_faction_id AND in_faction_id != neutral_faction_id THEN
				PERFORM remove_guild_members(ARRAY[in_player_id], guild_member_record.guild_id, 2::smallint);
			END IF;
		END IF;
	END IF;
	PERFORM clean_guild_invites_with_incompatible_faction(in_player_id, in_faction_id, neutral_faction_id);
END
$function$


-- igwo_delete_world_partitions(in_partition_ids bigint[]) -> void
-- oid: 58369  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.igwo_delete_world_partitions(in_partition_ids bigint[])
 RETURNS void
 LANGUAGE sql
AS $function$
	delete from world_partition where partition_id = any(in_partition_ids);
$function$


-- igwo_get_partition_id_seq_last_value() -> bigint
-- oid: 58370  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.igwo_get_partition_id_seq_last_value()
 RETURNS bigint
 LANGUAGE sql
AS $function$
	select last_value from world_partition_partition_id_seq;
$function$


-- igwo_get_partition_ids() -> SETOF bigint
-- oid: 58371  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.igwo_get_partition_ids()
 RETURNS SETOF bigint
 LANGUAGE sql
AS $function$
	select partition_id from world_partition order by partition_id asc;
$function$


-- igwo_get_partitions() -> TABLE(partition_id bigint, map text, dimension_index integer, label text, min_x double precision, min_y double precision, max_x double precision, max_y double precision)
-- oid: 58372  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.igwo_get_partitions()
 RETURNS TABLE(partition_id bigint, map text, dimension_index integer, label text, min_x double precision, min_y double precision, max_x double precision, max_y double precision)
 LANGUAGE sql
AS $function$
	SELECT
		partition_id,
		map,
		dimension_index,
		label,
		(partition_definition->'box'->>'min_x')::float8,
		(partition_definition->'box'->>'min_y')::float8,
		(partition_definition->'box'->>'max_x')::float8,
		(partition_definition->'box'->>'max_y')::float8
	FROM world_partition
	ORDER BY partition_id ASC;
$function$


-- igwo_get_server_details() -> TABLE(address text, server_id text, ready boolean, partition_id bigint, map text, dimension_index integer, label text)
-- oid: 58373  kind: FUNCTION  category: igwo

CREATE OR REPLACE FUNCTION dune.igwo_get_server_details()
 RETURNS TABLE(address text, server_id text, ready boolean, partition_id bigint, map text, dimension_index integer, label text)
 LANGUAGE sql
AS $function$
	select
		host(fs.igw_addr)||':'||fs.igw_port as address,
		fs.server_id,
		fs.ready,
		wp.partition_id,
		wp.map,
		wp.dimension_index,
		wp.label
	from get_farm_state() fs
	left join world_partition wp on wp.server_id = fs.server_id
	where fs.server_id is not null;
$function$


-- igwo_insert_world_partition(in_partition_id bigint, in_map text, in_partition_definition jsonb, in_dimension_index integer, in_partition_label text) -> bigint
-- oid: 58374  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.igwo_insert_world_partition(in_partition_id bigint, in_map text, in_partition_definition jsonb, in_dimension_index integer DEFAULT 0, in_partition_label text DEFAULT NULL::text)
 RETURNS bigint
 LANGUAGE sql
AS $function$
	insert into world_partition(partition_id, map, partition_definition, dimension_index, label)
	values (
		in_partition_id,
		in_map,
		in_partition_definition,
		in_dimension_index,
		coalesce(in_partition_label, determine_partition_label(in_map, in_dimension_index, null, false, in_partition_id)))
	returning partition_id;
$function$


-- igwo_next_partition_id_seq() -> bigint
-- oid: 58375  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.igwo_next_partition_id_seq()
 RETURNS bigint
 LANGUAGE sql
AS $function$
	select nextval('world_partition_partition_id_seq');
$function$


-- igwo_notify_world_partition_update() -> void
-- oid: 58376  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.igwo_notify_world_partition_update()
 RETURNS void
 LANGUAGE sql
AS $function$
	notify world_partition_update;
$function$


-- igwo_restart_partition_id_seq(in_restart_with bigint) -> void
-- oid: 58377  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.igwo_restart_partition_id_seq(in_restart_with bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	-- Use execute to substitute the numeric parameter into the alter sequence command
	execute format('alter sequence world_partition_partition_id_seq restart with %s', in_restart_with);
end;
$function$


-- igwo_update_world_partition(in_map text, in_partition_definition jsonb, in_partition_id bigint, in_dimension_index integer, in_label text) -> void
-- oid: 58378  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.igwo_update_world_partition(in_map text, in_partition_definition jsonb, in_partition_id bigint, in_dimension_index integer, in_label text DEFAULT NULL::text)
 RETURNS void
 LANGUAGE sql
AS $function$
	update world_partition
	set map = in_map,
		partition_definition = in_partition_definition,
		dimension_index = in_dimension_index,
		label = coalesce(in_label, label)
	where partition_id = in_partition_id;
$function$


-- init_event_log(in_partition_id bigint) -> void
-- oid: 58379  kind: FUNCTION  category: event_log

CREATE OR REPLACE FUNCTION dune.init_event_log(in_partition_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    threshold_ts TIMESTAMPTZ;
	last_ts TIMESTAMPTZ;
	cleanup_threshold_days CONSTANT INTEGER := 14;
BEGIN
	-- calculate threshold
	threshold_ts := now() - (cleanup_threshold_days * interval '1 day');

	-- lock table
	LOCK TABLE event_log_maintanence IN EXCLUSIVE MODE;

	SELECT last_cleanup
	INTO last_ts
	FROM event_log_maintanence;

	IF last_ts < threshold_ts THEN    
		-- delete events older then the passed in threshold
		DELETE FROM event_log
		WHERE event_time < threshold_ts;

		-- Update last_cleanup in event_log_maintanence
		UPDATE event_log_maintanence
		SET last_cleanup = now();
	END IF;

	-- update partition_id
	PERFORM set_config('dune.partition_id', in_partition_id::TEXT, false);
END;
$function$


-- initialize_partitions_basic_battlegroup() -> void
-- oid: 58380  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.initialize_partitions_basic_battlegroup()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('SH_HarkoVillage', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('SH_HarkoVillage', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('SH_Arrakeen', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('SH_Arrakeen', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('DeepDesert_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('DeepDesert_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('DeepDesert_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 2);
	perform add_partition_unique('Overmap', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform update_partition_labels();
END;
$function$


-- initialize_partitions_basic_survival_1() -> void
-- oid: 58381  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.initialize_partitions_basic_survival_1()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 2);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 3);
	perform update_partition_labels();
END;
$function$


-- initialize_partitions_development_battlegroup() -> void
-- oid: 58382  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.initialize_partitions_development_battlegroup()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	-- Current battlegroup maps
	perform initialize_partitions_full_battlegroup();

	-- Core development maps
	perform initialize_partitions_editor_default_1x1();
	perform initialize_partitions_igw_test_small_2x2();
	perform initialize_partitions_igw_training();

	-- Additional Gyms
	perform add_partition_unique('CombatGym_01', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0, 'CombatGym_01');
	perform add_partition_unique('Audio_Gym', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0, 'Audio_Gym');
	perform add_partition_unique('CombatGym_Camps', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0, 'CombatGym_Camps');
END;
$function$


-- initialize_partitions_editor_default_1x1() -> void
-- oid: 58383  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.initialize_partitions_editor_default_1x1()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	perform add_partition_unique('Editor_Default', '{"type": "box2d_array", "boxes": [{"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}]}', 0, 'Editor_Default');
END;
$function$


-- initialize_partitions_full_battlegroup() -> void
-- oid: 58384  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.initialize_partitions_full_battlegroup()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 2);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 3);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 4);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 5);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 6);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 7);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 8);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 9);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 10);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 11);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 12);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 13);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 14);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 15);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 16);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 17);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 18);
	perform add_partition_unique('Survival_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 19);
	perform add_partition_unique('SH_HarkoVillage', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('SH_HarkoVillage', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('SH_Arrakeen', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('SH_Arrakeen', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('SH_FallenLight', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('SH_FallenLight', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('CB_Story_Hephaestus', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Story_Ecolab_Carthag', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Story_WaterFatManor', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('DeepDesert_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('DeepDesert_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('DeepDesert_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 2);
	perform add_partition_unique('Overmap', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('Story_ProcesVerbal', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('DLC_Story_LostHarvest', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('DLC_Story_LostHarvest_EcolabA', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('DLC_Story_LostHarvest_EcolabB', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('DLC_Story_LostHarvest_ForgottenLab', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('Story_ArtOfKanly', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('Story_HeighlinerDungeon', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Dungeon_Hephaestus', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Dungeon_OldCarthag', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Story_BanditFortress01', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Overland_S_05', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Overland_S_06', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Overland_S_04', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Overland_M_01', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Overland_S_07', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('Story_Faction_Outpost_Hark', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('Story_Faction_Outpost_Atre', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Ecolab_Bronze_Green_089', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Ecolab_Bronze_Green_152', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Ecolab_Bronze_Green_195', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Ecolab_Bronze_Green_024', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Ecolab_Bronze_Green_136', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('PolarCap_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('PolarCap_1', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 1);
	perform add_partition_unique('CB_Story_DestroyedZanovar', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Story_OrbitalMonitor', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Dungeon_TheFacility', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Dungeon_ThePit', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform add_partition_unique('CB_Overland_S_08', '{"box": {"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0}, "type": "box2d_array"}', 0);
	perform update_partition_labels();
END;
$function$


-- initialize_partitions_igw_test_small_2x1() -> void
-- oid: 58385  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.initialize_partitions_igw_test_small_2x1()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	perform add_partition_unique('IGW_Test_Small', '{"type": "box2d_array", "boxes": [{"max_x": 0.5, "max_y": 1, "min_x": 0, "min_y": 0}]}', 0, 'IGW_Test_Small_A1');
	perform add_partition_unique('IGW_Test_Small', '{"type": "box2d_array", "boxes": [{"max_x": 1, "max_y": 1, "min_x": 0.5, "min_y": 0}]}', 0, 'IGW_Test_Small_A2');
END;
$function$


-- initialize_partitions_igw_test_small_2x2() -> void
-- oid: 58386  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.initialize_partitions_igw_test_small_2x2()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	perform add_partition_unique('IGW_Test_Small', '{"type": "box2d_array", "boxes": [{"max_x": 0.5, "max_y": 0.5, "min_x": 0, "min_y": 0}]}', 0, 'IGW_Test_Small_A1');
	perform add_partition_unique('IGW_Test_Small', '{"type": "box2d_array", "boxes": [{"max_x": 0.5, "max_y": 1, "min_x": 0, "min_y": 0.5}]}', 0, 'IGW_Test_Small_A2');
	perform add_partition_unique('IGW_Test_Small', '{"type": "box2d_array", "boxes": [{"max_x": 1, "max_y": 0.5, "min_x": 0.5, "min_y": 0}]}', 0, 'IGW_Test_Small_B1');
	perform add_partition_unique('IGW_Test_Small', '{"type": "box2d_array", "boxes": [{"max_x": 1, "max_y": 1, "min_x": 0.5, "min_y": 0.5}]}', 0, 'IGW_Test_Small_B2');
END;
$function$


-- initialize_partitions_igw_training() -> void
-- oid: 58387  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.initialize_partitions_igw_training()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	perform add_partition_unique('IGW_Training', '{"type": "box2d_array", "boxes": [{"max_x": 1, "max_y": 0.33333, "min_x": 0, "min_y": 0}]}', 0, 'IGW_Training_A1');
	perform add_partition_unique('IGW_Training', '{"type": "box2d_array", "boxes": [{"max_x": 1, "max_y": 0.66667, "min_x": 0, "min_y": 0.33333}]}', 0, 'IGW_Training_A2');
	perform add_partition_unique('IGW_Training', '{"type": "box2d_array", "boxes": [{"max_x": 1, "max_y": 1, "min_x": 0, "min_y": 0.66667}]}', 0, 'IGW_Training_A3');
END;
$function$


-- initialize_specialization_keystones(in_keystones text[]) -> TABLE(keystone_id smallint, keystone_name text)
-- oid: 58388  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.initialize_specialization_keystones(in_keystones text[])
 RETURNS TABLE(keystone_id smallint, keystone_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	LOCK TABLE specialization_keystones_map IN SHARE ROW EXCLUSIVE MODE;

-- Note: we filter the existing values before the insert, otherwise it bumps the generated id in specialization_keystones_map 
	INSERT INTO specialization_keystones_map (name)
		SELECT in_keystone_name FROM UNNEST(in_keystones) in_keystone_name LEFT JOIN specialization_keystones_map k ON in_keystone_name = k.name
		WHERE name IS NULL;
	RETURN QUERY SELECT * from specialization_keystones_map;
END $function$


-- initialize_world_partition(in_map_name text, in_num_servers integer, in_dimension_index integer) -> SETOF bigint
-- oid: 58389  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.initialize_world_partition(in_map_name text, in_num_servers integer, in_dimension_index integer DEFAULT 0)
 RETURNS SETOF bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
	return query
		with _cleanup as (
			DELETE FROM world_partition WHERE map = in_map_name and dimension_index = in_dimension_index
		)
		INSERT INTO world_partition (map, partition_definition, dimension_index, label)
			select in_map_name, format('{"type": "cell_index", "index": %s}', generate_series)::JSONB, in_dimension_index, in_map_name || '_' || in_dimension_index || '_' || generate_series
			from generate_series(0, in_num_servers - 1)
			returning partition_id;
END
$function$


-- interact_get_vendor_items_bought_from_player(in_vendor_id text, in_player_id bigint, in_current_cycle_start_timestamp bigint) -> TABLE(out_template_id text, out_amount_bought integer)
-- oid: 58390  kind: FUNCTION  category: stock_vendor

CREATE OR REPLACE FUNCTION dune.interact_get_vendor_items_bought_from_player(in_vendor_id text, in_player_id bigint, in_current_cycle_start_timestamp bigint)
 RETURNS TABLE(out_template_id text, out_amount_bought integer)
 LANGUAGE plpgsql
AS $function$
DECLARE
   player_timestamp BIGINT;
BEGIN
	-- Clean items bought by player if the vendor's cycle was reset since the last time they interacted with it
	IF EXISTS
		(SELECT * FROM vendor_stock_cycle
		WHERE vendor_id = in_vendor_id AND player_id = in_player_id AND last_interacted_timestamp < in_current_cycle_start_timestamp)
	THEN
		DELETE FROM vendor_stock_state WHERE vendor_id = in_vendor_id AND player_id = in_player_id;	
	END IF;
	
	PERFORM update_vendor_timestamp_for_player(in_vendor_id, in_player_id, in_current_cycle_start_timestamp);
	
	RETURN QUERY
    SELECT template_id, amount_bought FROM vendor_stock_state WHERE vendor_id = in_vendor_id AND player_id = in_player_id;
END
$function$


-- internal_add_party_member(in_invite_id bigint, in_party_id bigint, in_player_id bigint, in_platform_session_id text, in_platform_name text, in_max_party_member_count integer) -> dune.partyacceptinviteresult
-- oid: 58391  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.internal_add_party_member(in_invite_id bigint, in_party_id bigint, in_player_id bigint, in_platform_session_id text, in_platform_name text, in_max_party_member_count integer)
 RETURNS dune.partyacceptinviteresult
 LANGUAGE plpgsql
AS $function$
DECLARE
	party_exists BOOLEAN;
	member_count INTEGER;
	out_platform_members_count INTEGER;
	out_player_name TEXT;
	out_accept_error PartyAcceptInviteResult DEFAULT 'Success'::PartyAcceptInviteResult;
BEGIN

	-- check if party exists
	SELECT INTO party_exists EXISTS (SELECT 1 FROM parties WHERE party_id = in_party_id);
	IF NOT party_exists THEN
		PERFORM remove_party_invite(in_invite_id, 2::smallint); -- PartyNoLongerExists = 2
		RAISE NOTICE 'Trying to add player % to non existing party %.', in_player_id, in_party_id;
		out_accept_error = 'NonExistingParty'::PartyAcceptInviteResult;
		RETURN out_accept_error;
	END IF;

	-- check party member count
	SELECT INTO member_count COUNT(*) FROM party_members WHERE party_id = in_party_id;
	IF member_count >= in_max_party_member_count THEN
		PERFORM remove_party_invite(in_invite_id, 1::smallint); -- PartyFull = 1
		RAISE NOTICE 'Trying to add more members than the allowed % to party %.', in_max_party_member_count, in_party_id;
		out_accept_error = 'PartyFull'::PartyAcceptInviteResult;
		RETURN out_accept_error;
	END IF;

	-- insert member
	INSERT INTO party_members("player_id", "party_id") VALUES(in_player_id, in_party_id);

    -- track platform information
    SELECT num_of_players INTO out_platform_members_count FROM platform_parties_mapping WHERE platform_name = in_platform_name AND dune_party_id = in_party_id;
    IF out_platform_members_count IS NOT NULL THEN
        -- there was a platform session for the player's party
		UPDATE platform_parties_mapping SET num_of_players = out_platform_members_count+1 WHERE platform_name = in_platform_name AND dune_party_id = in_party_id;
    ELSE
		-- no mapping for this platform yet, add
		INSERT INTO platform_parties_mapping ("platform_session_id", "platform_name", "dune_party_id", "num_of_players")
		SELECT in_platform_session_id, in_platform_name, in_party_id, 1
		WHERE in_platform_session_id <> '' AND in_platform_name <> '';
    END IF;

	-- Get player name
	SELECT player_state.character_name INTO out_player_name
		FROM player_state WHERE player_state.player_controller_id = in_player_id;

	PERFORM remove_party_invite(in_invite_id, 0::smallint); -- Silent = 0

	PERFORM pg_notify('party_notify_channel', format(
		'add_party_member#{"PartyId" : %s, "PlayerId" : %s, "PlayerName" : "%s", "PlayerPlatformName" : "%s", "PlayerPlatformSessionId" : "%s"}', 
		in_party_id, in_player_id, out_player_name, in_platform_name, in_platform_session_id));

	RETURN out_accept_error;
END
$function$


-- internal_create_party(in_invite_id bigint, in_leader_id bigint, in_leader_platform_session_id text, in_leader_platform_name text, in_member_id bigint, in_platform_session_id text, in_platform_name text) -> bigint
-- oid: 58392  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.internal_create_party(in_invite_id bigint, in_leader_id bigint, in_leader_platform_session_id text, in_leader_platform_name text, in_member_id bigint, in_platform_session_id text, in_platform_name text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	leader_registered BOOLEAN;
	players_belong_to_party BOOLEAN;
	out_party_id BIGINT;
	out_player_name TEXT;
	out_leader_name TEXT;
BEGIN

	-- Check if the leader already exists in the parties table
	SELECT INTO leader_registered EXISTS (SELECT 1 FROM parties WHERE party_leader_id = in_leader_id);
	IF leader_registered THEN
		RAISE EXCEPTION 'Leader already has a party.';
	END IF;

	-- Check if either the leader or member already exists in the party_members table
	SELECT INTO players_belong_to_party EXISTS (SELECT 1 FROM party_members WHERE player_id = in_leader_id OR player_id = in_member_id);
	IF players_belong_to_party THEN
		RAISE EXCEPTION 'One of the players is already in a party.';
	END IF;

	-- If neither condition is met, insert the new party and members
	INSERT INTO parties ("party_leader_id") VALUES (in_leader_id) RETURNING party_id INTO out_party_id;
	INSERT INTO party_members (player_id, party_id) VALUES (in_leader_id, out_party_id), (in_member_id, out_party_id);

	-- Update all of the leaders invites to have the new party as party id
	UPDATE party_invites SET party_id = out_party_id WHERE sender_player_id = in_leader_id;

	-- Get leader name
	SELECT player_state.character_name INTO out_leader_name
		FROM player_state WHERE player_state.player_controller_id = in_leader_id;

	-- Get member name
	SELECT player_state.character_name INTO out_player_name
		FROM player_state WHERE player_state.player_controller_id = in_member_id;

	-- Handle platform sessions mapping for new party
	IF in_leader_platform_name = in_platform_name THEN
		-- If players are from the same platform and leader has session id (console), we create mapping (if their platform_name and session_id are valid)
		INSERT INTO platform_parties_mapping ("platform_session_id", "platform_name", "dune_party_id", "num_of_players")
		SELECT in_leader_platform_session_id, in_leader_platform_name, out_party_id, 2
		WHERE in_leader_platform_session_id <> '' AND in_leader_platform_name <> '';
	ELSE
		-- Create leader's platform session mapping if their platform_name and session_id are valid
		INSERT INTO platform_parties_mapping ("platform_session_id", "platform_name", "dune_party_id", "num_of_players")
		SELECT in_leader_platform_session_id, in_leader_platform_name, out_party_id, 1
		WHERE in_leader_platform_session_id <> '' AND in_leader_platform_name <> '';

		-- Create member's platform session mapping if their platform_name and session_id are valid
		INSERT INTO platform_parties_mapping ("platform_session_id", "platform_name", "dune_party_id", "num_of_players")
		SELECT in_platform_session_id, in_platform_name, out_party_id, 1
		WHERE in_platform_session_id <> '' AND in_platform_name <> '';
	END IF;

	PERFORM remove_party_invite(in_invite_id, 0::smallint); -- Silent = 0

	PERFORM pg_notify('party_notify_channel', format(
		'create_party#{"PartyId" : %s, "LeaderId" : %s, "LeaderName" : "%s", "LeaderPlatformName" : "%s", "LeaderPlatformSessionId" : "%s", "MemberId" : %s, "MemberName" : "%s", "MemberPlatformName" : "%s", "MemberPlatformSessionId" : "%s"}',
		out_party_id, in_leader_id, out_leader_name, in_leader_platform_name, in_leader_platform_session_id, in_member_id, out_player_name, in_platform_name, in_platform_session_id));

	RETURN out_party_id;

END
$function$


-- is_player_guild_admin(in_player_id bigint, in_guild_id bigint) -> boolean
-- oid: 58393  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.is_player_guild_admin(in_player_id bigint, in_guild_id bigint)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
	found_role_id SMALLINT;
BEGIN
	SELECT role_id FROM guild_members WHERE player_id = in_player_id AND guild_id = in_guild_id INTO found_role_id;
	IF NOT FOUND THEN
    	RETURN FALSE;
	END IF;
	RETURN found_role_id = 100;
END
$function$


-- is_player_offline(in_fls_id text) -> boolean
-- oid: 58394  kind: FUNCTION  category: lookup
-- comment: Return true if player is marked as offline, taking into account server crashing before players online state was updated in DB.

CREATE OR REPLACE FUNCTION dune.is_player_offline(in_fls_id text)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
declare
    has_state int;
    is_offline boolean;
begin
    -- If there's no player_state row for this account, treat the player as offline
    select count(*) into has_state
    from player_state ps
    join accounts a on a.id = ps.account_id
    where a.user = in_fls_id;

    if has_state = 0 then
        return true;
    end if;

    select exists(
        select 1
        from player_state ps
        join accounts a on a.id = ps.account_id
        where a.user = in_fls_id
          and (
            ps.online_status = 'Offline'
            -- Player is treated as offline if last played server is offline/unavailable, or not set at all
            or (ps.server_id is null or ps.server_id not in (select * from active_server_ids))
          )
    ) into is_offline;

    return is_offline;
end
$function$


-- is_player_party_leader(in_player_id bigint, in_party_id bigint) -> boolean
-- oid: 58395  kind: FUNCTION  category: lookup

CREATE OR REPLACE FUNCTION dune.is_player_party_leader(in_player_id bigint, in_party_id bigint)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
	found_leader_id SMALLINT;
BEGIN
	SELECT party_leader_id FROM parties WHERE party_id = in_party_id INTO found_leader_id;
	IF NOT FOUND THEN
		RETURN FALSE;
	END IF;
	RETURN found_leader_id = in_player_id;
END
$function$


-- join_platform_session_party(in_leader_platform_id text, in_player_platform_id text, in_platform_session_id text, in_platform_name text, in_max_party_member_count integer) -> dune.partyacceptinviteresult
-- oid: 58396  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.join_platform_session_party(in_leader_platform_id text, in_player_platform_id text, in_platform_session_id text, in_platform_name text, in_max_party_member_count integer)
 RETURNS dune.partyacceptinviteresult
 LANGUAGE plpgsql
AS $function$
DECLARE
    out_party_id BIGINT;
	out_accept_error PartyAcceptInviteResult DEFAULT 'Success'::PartyAcceptInviteResult;
    out_leader_id BIGINT;
    out_player_id BIGINT;
BEGIN
    IF in_platform_name IS NULL OR length(in_platform_name) = 0 THEN
        RAISE EXCEPTION 'platform_name must not be empty';
    END IF;
    IF in_platform_session_id IS NULL OR length(in_platform_session_id) = 0 THEN
        RAISE EXCEPTION 'platform_session_id must not be empty';
    END IF;

    -- Fetch party id, but do nothing with it
    SELECT dune_party_id INTO out_party_id
    FROM platform_parties_mapping
    WHERE platform_session_id = in_platform_session_id
      AND platform_name = in_platform_name
    LIMIT 1;

	out_leader_id = get_controller_id_from_platform_id(in_leader_platform_id);
	out_player_id = get_controller_id_from_platform_id(in_player_platform_id);

	IF out_party_id IS NULL THEN
		-- Using same platform name and platform session id cause they are joining through system invite, they'll always be the same platform.
		PERFORM internal_create_party(NULL, out_leader_id, in_platform_session_id, in_platform_name,
			out_player_id, in_platform_session_id, in_platform_name);
	ELSE
		out_accept_error := internal_add_party_member(NULL, out_party_id, out_player_id, in_platform_session_id, in_platform_name, in_max_party_member_count);
	END IF;

	RETURN out_accept_error;
END
$function$


-- journey_story_node_cooldown_add(in_account_id bigint, in_story_node_id text, in_time_to_expire timestamp without time zone) -> void
-- oid: 58397  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.journey_story_node_cooldown_add(in_account_id bigint, in_story_node_id text, in_time_to_expire timestamp without time zone)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO journey_story_node_cooldown(account_id, story_node_id, time_to_expire)
	VALUES(in_account_id, in_story_node_id, in_time_to_expire);
END $function$


-- journey_story_node_cooldown_delete_expired(in_time_to_check timestamp without time zone) -> void
-- oid: 58398  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.journey_story_node_cooldown_delete_expired(in_time_to_check timestamp without time zone)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM journey_story_node_cooldown WHERE time_to_expire < in_time_to_check;
END $function$


-- landsraad_cast_vote(in_term_id bigint, in_player_id bigint, in_decree_name text) -> void
-- oid: 58399  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_cast_vote(in_term_id bigint, in_player_id bigint, in_decree_name text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	term_winning_faction_id SMALLINT = NULL;
	elected_decree_id BIGINT = NULL;
	player_guild_id BIGINT = NULL;
	guild_faction_id SMALLINT = NULL;
	voting_decree_id BIGINT = NULL;
	voting_influence INTEGER = 0;
	guild_ids_json JSON = NULL;
BEGIN
	LOCK TABLE landsraad_decree_votes IN EXCLUSIVE MODE;

	SELECT term.winning_faction_id, term.elected_decree_id FROM landsraad_decree_term AS term WHERE term.term_id = in_term_id INTO term_winning_faction_id, elected_decree_id;

	IF term_winning_faction_id IS NULL THEN
		RAISE EXCEPTION 'Cannot insert landsraad vote, term % has no winning faction yet', in_term_id;
	END IF;

	IF elected_decree_id IS NOT NULL THEN
		RAISE EXCEPTION 'Cannot insert landsraad vote, term % already has an elected decree', in_term_id;
	END IF;

	SELECT guilds.guild_id, guilds.guild_faction FROM guild_members 
		JOIN guilds ON guild_members.guild_id = guilds.guild_id WHERE guild_members.player_id = in_player_id INTO player_guild_id, guild_faction_id;

	IF player_guild_id IS NULL THEN
		RAISE EXCEPTION 'Cannot insert landsraad vote, player % not in guild', in_player_id;
	END IF;

	IF guild_faction_id != term_winning_faction_id THEN
		RAISE EXCEPTION 'Cannot insert landsraad vote, guild % not alligned to winning faction %', player_guild_id, term_winning_faction_id;
	END IF;

	IF is_player_guild_admin(in_player_id, player_guild_id) = FALSE THEN
		RAISE EXCEPTION 'Cannot insert landsraad vote, player % is not guild admin of guild %', in_player_id, player_guild_id;
	END IF;

	IF EXISTS (SELECT FROM landsraad_decree_votes AS votes WHERE votes.guild_id = player_guild_id) THEN
		RAISE WARNING 'Cannot insert landsraad vote, guild % has voted already', player_guild_id;
		RETURN;
	END IF;

	SELECT decrees.id FROM landsraad_decree_rotation AS rotation 
		INNER JOIN landsraad_decrees AS decrees ON rotation.decree_id = decrees.id
		WHERE decrees.decree_name = in_decree_name INTO voting_decree_id;

	IF voting_decree_id IS NULL THEN
		RAISE EXCEPTION 'Cannot insert landsraad vote, decree % not for election', in_decree_name;
	END IF;

	SELECT FLOOR(landsraad_load_guild_contribution(in_term_id, player_guild_id, term_winning_faction_id))::INTEGER INTO voting_influence;

	IF voting_influence IS NULL THEN
		RAISE WARNING 'Cannot insert landsraad vote, guild % has no contribution', player_guild_id;
		RETURN;
	END IF;

	INSERT INTO landsraad_decree_votes VALUES(voting_decree_id, player_guild_id, in_player_id, voting_influence);

	SELECT json_agg(player_guild_id) INTO guild_ids_json;
	PERFORM pg_notify('landsraad_notify_channel', format('guild_vote_changed#{"GuildIds": %s}', guild_ids_json));

END $function$


-- landsraad_change_term_end_time(end_term_id bigint, new_end_time timestamp without time zone, in_test_term boolean) -> void
-- oid: 58400  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_change_term_end_time(end_term_id bigint, new_end_time timestamp without time zone, in_test_term boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	LOCK TABLE landsraad_decree_term IN EXCLUSIVE MODE;
	UPDATE landsraad_decree_term SET test_term = in_test_term WHERE term_id = end_term_id AND test_term = false;
	UPDATE landsraad_decree_term SET end_time = new_end_time AT TIME ZONE 'UTC' WHERE term_id = end_term_id;
END $function$


-- landsraad_check_task_completion() -> trigger
-- oid: 58401  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_check_task_completion()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
DECLARE
	task_completed BOOLEAN = FALSE;
BEGIN
	-- if a faction reached the tasks's goal amount set the task as completed
	WITH faction_progress AS (SELECT landsraad_task_faction_contributions.task_id, SUM(landsraad_task_faction_contributions.amount) AS amount  FROM landsraad_task_faction_contributions 
	WHERE landsraad_task_faction_contributions.task_id = NEW.task_id AND faction_id = NEW.faction_id GROUP BY faction_id, landsraad_task_faction_contributions.task_id)
		SELECT COALESCE (faction_progress.amount, 0) >= landsraad_tasks.goal_amount 
		FROM landsraad_tasks LEFT JOIN faction_progress ON landsraad_tasks.id = faction_progress.task_id
		WHERE landsraad_tasks.id = NEW.task_id
		INTO task_completed;

	IF task_completed THEN
		UPDATE landsraad_tasks SET completed = TRUE, winning_faction_id = NEW.faction_id, completion_time = NOW() WHERE id = NEW.task_id AND completed = FALSE;
		PERFORM pg_notify('landsraad_notify_channel', 'state_changed');
	END IF;

	RETURN NULL;
END $function$


-- landsraad_check_term_won() -> trigger
-- oid: 58402  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_check_term_won()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
DECLARE
	row_completed BIGINT[] = NULL;
	column_completed BIGINT[] = NULL;
	diagonal_1_completed BIGINT[] = NULL; 
	diagonal_2_completed BIGINT[] = NULL;
	sysselraad_amount INTEGER = 0;
BEGIN
	-- check for sysselrad rule (a row, a column or a diagonal completed by a faction)
	WITH board AS (SELECT task.id as task_id, task.winning_faction_id, task.board_index / 5 AS row, task.board_index % 5 AS col, task.sysselraad FROM landsraad_tasks task WHERE task.term_id = NEW.term_id)
	SELECT 
		(SELECT array_agg(board.task_id) FROM board WHERE board.row = ANY(SELECT board.row FROM board WHERE board.winning_faction_id = NEW.winning_faction_id GROUP BY (board.row) HAVING COUNT(board.col) = 5)),
		(SELECT array_agg(board.task_id) FROM board WHERE board.col = ANY(SELECT board.col FROM board WHERE board.winning_faction_id = NEW.winning_faction_id GROUP BY (board.col) HAVING COUNT(board.row) = 5)),
		(SELECT CASE WHEN COUNT(board.winning_faction_id) = 5 THEN array_agg(board.task_id) END FROM ( VALUES (0, 0), (1, 1), (2, 2), (3, 3), (4, 4) ) AS t(row, col) JOIN board ON board.row = t.row AND board.col = t.col WHERE board.winning_faction_id = NEW.winning_faction_id),
		(SELECT CASE WHEN COUNT(board.winning_faction_id) = 5 THEN array_agg(board.task_id) END FROM ( VALUES (0, 4), (1, 3), (2, 2), (3, 1), (4, 0) ) AS t(row, col) JOIN board ON board.row = t.row AND board.col = t.col WHERE board.winning_faction_id = NEW.winning_faction_id),
		(SELECT COUNT(*) FROM board WHERE board.sysselraad)
	INTO row_completed, column_completed, diagonal_1_completed, diagonal_2_completed, sysselraad_amount;

	IF sysselraad_amount = 0 AND (row_completed IS NOT NULL OR column_completed IS NOT NULL OR diagonal_1_completed IS NOT NULL OR diagonal_2_completed IS NOT NULL) THEN
		UPDATE landsraad_tasks SET sysselraad = TRUE WHERE id = ANY(row_completed || column_completed || diagonal_1_completed || diagonal_2_completed);
		UPDATE landsraad_decree_term SET winning_faction_id = NEW.winning_faction_id WHERE term_id = NEW.term_id AND winning_faction_id IS NULL;
		PERFORM pg_notify('landsraad_notify_channel', 'state_changed');
	END IF;

	RETURN NULL;
END $function$


-- landsraad_collect_task_telemetry_for_faction(in_term_id bigint, in_faction_name text) -> TABLE(task_telemetry dune.landsraadtermtasktelemetry[])
-- oid: 58403  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_collect_task_telemetry_for_faction(in_term_id bigint, in_faction_name text)
 RETURNS TABLE(task_telemetry dune.landsraadtermtasktelemetry[])
 LANGUAGE plpgsql
AS $function$
DECLARE
	current_faction_id SMALLINT = NULL;
BEGIN
	SELECT id FROM factions WHERE factions.name = in_faction_name INTO current_faction_id;

	RETURN query SELECT ARRAY_AGG((in_faction_name, landsraad_tasks.house_name, task_reveal.revealed, (CASE WHEN winning_faction_id = current_faction_id THEN TRUE ELSE FALSE END), task_progress.participant_count, CAST(landsraad_tasks.board_index AS INTEGER), landsraad_tasks.completion_time)::LandsraadTermTaskTelemetry)
		FROM landsraad_tasks 
		LEFT JOIN landsraad_task_reveal_state task_reveal
            ON task_reveal.task_id = landsraad_tasks.id AND task_reveal.faction_id = current_faction_id
		LEFT JOIN LATERAL (
	        SELECT COUNT(DISTINCT ltpc.player_id) AS participant_count FROM landsraad_task_player_contributions ltpc WHERE ltpc.task_id = landsraad_tasks.id and ltpc.faction_id = current_faction_id GROUP BY task_id
        ) AS task_progress ON true
		WHERE landsraad_tasks.term_id = in_term_id;
END $function$


-- landsraad_collect_term_telemetry(in_term_id bigint, in_faction_names text[]) -> TABLE(term_telemetry dune.landsraadtermtelemetry[], task_telemetry dune.landsraadtermtasktelemetry[])
-- oid: 58404  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_collect_term_telemetry(in_term_id bigint, in_faction_names text[])
 RETURNS TABLE(term_telemetry dune.landsraadtermtelemetry[], task_telemetry dune.landsraadtermtasktelemetry[])
 LANGUAGE plpgsql
AS $function$
DECLARE
	faction_name TEXT = NULL;
	term_telemetry LandsraadTermTelemetry[];
	task_telemetry LandsraadTermTaskTelemetry[];
BEGIN
	FOREACH faction_name IN ARRAY in_faction_names
	LOOP
		term_telemetry = ARRAY_APPEND(term_telemetry, landsraad_collect_term_telemetry_for_faction(in_term_id, faction_name));
		task_telemetry = ARRAY_CAT(task_telemetry, landsraad_collect_task_telemetry_for_faction(in_term_id, faction_name));
	END LOOP;
	
	RETURN query SELECT term_telemetry, task_telemetry;
END $function$


-- landsraad_collect_term_telemetry_for_faction(in_term_id bigint, in_faction_name text) -> dune.landsraadtermtelemetry
-- oid: 58405  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_collect_term_telemetry_for_faction(in_term_id bigint, in_faction_name text)
 RETURNS dune.landsraadtermtelemetry
 LANGUAGE plpgsql
AS $function$
DECLARE
	current_faction_id SMALLINT = NULL;
	winning_faction_id SMALLINT = NULL;
	start_time TIMESTAMPTZ = NULL;
	end_time TIMESTAMPTZ = NULL;
	sysselraad_count INTEGER = NULL;
	term_result TEXT = NULL;
	faction_won BOOLEAN = FALSE;
	participants_num_faction INTEGER = NULL;
	tasks_completed INTEGER = NULL;
	tasks_revealed INTEGER = NULL;
BEGIN
	SELECT id FROM factions WHERE factions.name = in_faction_name INTO current_faction_id;

	SELECT term.winning_faction_id, term.start_time, term.end_time FROM landsraad_decree_term AS term WHERE term_id = in_term_id INTO winning_faction_id, start_time, end_time;
	
	IF winning_faction_id IS NULL THEN
		term_result = 'TIE';
	ELSE
		SELECT COUNT(id) FROM landsraad_tasks WHERE term_id = in_term_id AND sysselraad = true INTO sysselraad_count;
		IF sysselraad_count = 5 THEN
			term_result = 'SYSSELRAAD';
		ELSE
			term_result = 'TASK_COUNT';
		END IF;
	END IF;

	IF winning_faction_id = current_faction_id THEN
		faction_won = true;
	END IF;
	
	SELECT COUNT(DISTINCT player_id) FROM landsraad_task_player_contributions LEFT JOIN landsraad_tasks ON
		landsraad_task_player_contributions.task_id = landsraad_tasks.id 
		WHERE landsraad_tasks.term_id = in_term_id AND landsraad_task_player_contributions.faction_id = current_faction_id
		INTO participants_num_faction;
	
	SELECT COUNT(id) FROM landsraad_tasks WHERE landsraad_tasks.term_id = in_term_id
		AND landsraad_tasks.winning_faction_id = current_faction_id AND landsraad_tasks.completed = true
		INTO tasks_completed;
		
	SELECT COUNT(DISTINCT landsraad_tasks.id) FROM landsraad_task_reveal_state LEFT JOIN landsraad_tasks ON
		landsraad_task_reveal_state.task_id = landsraad_tasks.id
		WHERE landsraad_tasks.term_id = in_term_id AND landsraad_task_reveal_state.faction_id = current_faction_id AND landsraad_task_reveal_state.revealed = true
		INTO tasks_revealed;
		
	RETURN (in_faction_name, term_result, faction_won, participants_num_faction, tasks_completed, tasks_revealed, start_time, end_time)::LandsraadTermTelemetry;
END $function$


-- landsraad_collect_vote_telemetry(in_term_id bigint, in_winning_faction_id integer) -> TABLE(guild_id bigint, decree_name text, voting_influence integer)
-- oid: 58406  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_collect_vote_telemetry(in_term_id bigint, in_winning_faction_id integer)
 RETURNS TABLE(guild_id bigint, decree_name text, voting_influence integer)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN query SELECT guild_contribution.guild_id, landsraad_decrees.decree_name, FLOOR(SUM(guild_contribution.amount))::INTEGER
			FROM landsraad_tasks AS tasks 
			INNER JOIN landsraad_task_guild_contributions AS guild_contribution 
			ON guild_contribution.task_id = tasks.id AND guild_contribution.faction_id = tasks.winning_faction_id AND tasks.term_id = in_term_id AND guild_contribution.faction_id = in_winning_faction_id
			LEFT JOIN landsraad_decree_votes 
			ON landsraad_decree_votes.guild_id = guild_contribution.guild_id
			LEFT JOIN landsraad_decrees 
			ON landsraad_decree_votes.decree_id = landsraad_decrees.id
			GROUP BY (guild_contribution.guild_id, landsraad_decrees.decree_name);
END $function$


-- landsraad_collect_votes(in_term_id bigint) -> TABLE(elected_decree text, winning_faction_name text, available_decrees text[], guild_votes dune.landsraadguildvotetelemetry[])
-- oid: 58407  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_collect_votes(in_term_id bigint)
 RETURNS TABLE(elected_decree text, winning_faction_name text, available_decrees text[], guild_votes dune.landsraadguildvotetelemetry[])
 LANGUAGE plpgsql
AS $function$
DECLARE
	has_elected_decree BOOL = FALSE;
	has_winning_faction BOOL = FALSE;
	winning_decree_id BIGINT = NULL;
    winning_decree_name TEXT = NULL;
	winning_faction_name TEXT = NULL;
    winning_faction_id INT = NULL;
	available_decrees TEXT[];
    vote_telemetry LandsraadGuildVoteTelemetry[];
BEGIN
	LOCK TABLE landsraad_decree_term, landsraad_decree_rotation, landsraad_decree_votes IN EXCLUSIVE MODE;

	SELECT CASE WHEN landsraad_decree_term.elected_decree_id IS NULL THEN FALSE ELSE TRUE END, 
		   CASE WHEN landsraad_decree_term.winning_faction_id IS NULL THEN FALSE ELSE TRUE END
		FROM landsraad_decree_term WHERE term_id = in_term_id INTO has_elected_decree, has_winning_faction;
		
	IF has_winning_faction IS FALSE THEN
		RETURN query SELECT NULL, NULL, available_decrees, vote_telemetry;
		RETURN;
	END IF;
	
	SELECT factions.name, term.winning_faction_id FROM landsraad_decree_term AS term LEFT JOIN factions ON term.winning_faction_id = factions.id WHERE term.term_id = in_term_id INTO winning_faction_name, winning_faction_id;

	SELECT ARRAY_AGG(landsraad_decrees.decree_name) FROM landsraad_decree_rotation INNER JOIN landsraad_decrees ON landsraad_decree_rotation.decree_id = landsraad_decrees.id INTO available_decrees;
	
	SELECT ARRAY_AGG((guild_id, decree_name, voting_influence)::LandsraadGuildVoteTelemetry) FROM landsraad_collect_vote_telemetry(in_term_id, winning_faction_id) INTO vote_telemetry;

	-- Only resolve votes if the latest term has no elected decree
	IF has_elected_decree IS FALSE THEN
		WITH
			votes AS (SELECT decree_id, SUM(influence) AS amount FROM landsraad_decree_votes GROUP BY decree_id), 
			max_votes AS (SELECT MAX(amount) AS amount FROM votes)
		UPDATE landsraad_decree_term
			SET elected_decree_id = 
				CASE WHEN (SELECT amount FROM max_votes) IS NOT NULL THEN
					(SELECT decree_id FROM votes WHERE amount = (SELECT amount FROM max_votes) ORDER BY RANDOM() LIMIT 1)
				ELSE
					(SELECT decree_id FROM landsraad_decree_rotation ORDER BY RANDOM() LIMIT 1)
				END
			WHERE term_id = in_term_id
			returning elected_decree_id INTO winning_decree_id;
	ELSE
		SELECT term.elected_decree_id FROM landsraad_decree_term term ORDER BY term_id DESC LIMIT 1 INTO winning_decree_id;
	END IF;

	SELECT decree_name FROM landsraad_decrees WHERE id = winning_decree_id INTO winning_decree_name;
	
	RETURN query SELECT winning_decree_name, winning_faction_name, available_decrees, vote_telemetry;
END $function$


-- landsraad_determine_winner(in_term_id bigint) -> text
-- oid: 58408  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_determine_winner(in_term_id bigint)
 RETURNS text
 LANGUAGE plpgsql
AS $function$
DECLARE
	has_winning_faction BOOLEAN = FALSE;
	winning_faction_name TEXT = NULL;
BEGIN
	SELECT CASE WHEN winning_faction_id IS NULL THEN FALSE ELSE TRUE END FROM landsraad_decree_term WHERE term_id = in_term_id INTO has_winning_faction;
	
	-- only set winning faction if not set already (sysselraad has been secured)
	IF NOT has_winning_faction THEN
		WITH tasks_completed_by_faction AS (SELECT winning_faction_id AS faction, COUNT(id) AS num_tasks FROM landsraad_tasks WHERE term_id = in_term_id AND winning_faction_id IS NOT NULL GROUP BY (winning_faction_id)),
			winner_count AS (SELECT COUNT(faction) AS amount FROM tasks_completed_by_faction WHERE num_tasks = (SELECT MAX(num_tasks) FROM tasks_completed_by_faction) GROUP BY(num_tasks)),
			winner AS (SELECT faction FROM tasks_completed_by_faction ORDER BY num_tasks DESC LIMIT 1)
		UPDATE landsraad_decree_term SET winning_faction_id = CASE WHEN winner_count.amount = 1 THEN winner.faction ELSE NULL END FROM winner, winner_count;
		PERFORM pg_notify('landsraad_notify_channel', 'state_changed');
	END IF;

	SELECT factions.name FROM landsraad_decree_term AS term LEFT JOIN factions ON term.winning_faction_id = factions.id WHERE term.term_id = in_term_id INTO winning_faction_name;
	
	RETURN winning_faction_name;
END $function$


-- landsraad_force_end_term(end_term_id bigint) -> void
-- oid: 58409  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_force_end_term(end_term_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	PERFORM landsraad_change_term_end_time(end_term_id, now() AT TIME ZONE 'UTC', false);
END $function$


-- landsraad_has_term_of_task_ended(in_task_id bigint) -> boolean
-- oid: 58410  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_has_term_of_task_ended(in_task_id bigint)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
	term_ended BOOLEAN = FALSE;
BEGIN
    SELECT NOW() > term.end_time FROM landsraad_tasks AS task LEFT JOIN landsraad_decree_term AS term ON task.term_id = term.term_id WHERE task.id = in_task_id INTO term_ended;
    RETURN term_ended;
END $function$


-- landsraad_initialize_system(number_of_weeks_term_retention integer, number_of_nominated_decrees integer, in_end_time timestamp without time zone, in_test_term boolean, faction_names text[], decrees dune.landsraaddecree[], tasks dune.landsraadtask[], task_rewards dune.landsraadtaskreward[]) -> TABLE(term_id bigint, reigning_faction_name text, active_decree_name text, winning_faction_name text, elected_decree_name text, start_time timestamp without time zone, end_time timestamp without time zone)
-- oid: 58411  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_initialize_system(number_of_weeks_term_retention integer, number_of_nominated_decrees integer, in_end_time timestamp without time zone, in_test_term boolean, faction_names text[], decrees dune.landsraaddecree[], tasks dune.landsraadtask[], task_rewards dune.landsraadtaskreward[])
 RETURNS TABLE(term_id bigint, reigning_faction_name text, active_decree_name text, winning_faction_name text, elected_decree_name text, start_time timestamp without time zone, end_time timestamp without time zone)
 LANGUAGE plpgsql
AS $function$
DECLARE
	current_term_id BIGINT = NULL;
BEGIN
	LOCK TABLE landsraad_decree_term, landsraad_decree_rotation, landsraad_decrees, landsraad_decree_votes IN EXCLUSIVE MODE;

	CALL landsraad_update_decrees(decrees);
	
	CALL landsraad_update_factions(faction_names);

	SELECT term.term_id FROM landsraad_decree_term term ORDER BY term.term_id DESC LIMIT 1 INTO current_term_id;
	IF FOUND THEN
		RETURN query SELECT term.term_id, reigning_faction.name, active_decree.decree_name, winning_faction.name, elected_decree.decree_name, (term.start_time AT TIME ZONE 'UTC')::TIMESTAMP, (term.end_time AT TIME ZONE 'UTC')::TIMESTAMP
			FROM landsraad_decree_term term 
			LEFT JOIN factions reigning_faction ON term.reigning_faction_id = reigning_faction.id 
			LEFT JOIN landsraad_decrees active_decree ON term.active_decree_id = active_decree.id
			LEFT JOIN factions winning_faction ON term.winning_faction_id = winning_faction.id
			LEFT JOIN landsraad_decrees elected_decree ON term.elected_decree_id = elected_decree.id
			WHERE term.term_id = current_term_id;
	ELSE 
		RETURN query SELECT term.term_id, term.reigning_faction_name, term.active_decree_name, term.winning_faction_name, term.elected_decree_name, term.start_time, term.end_time
			FROM landsraad_initialize_term(number_of_weeks_term_retention, number_of_nominated_decrees, in_end_time, in_test_term, tasks, task_rewards) AS term;	
	END IF;
END $function$


-- landsraad_initialize_term(number_of_weeks_term_retention integer, number_of_nominated_decrees integer, in_end_time timestamp without time zone, in_test_term boolean, tasks dune.landsraadtask[], task_rewards dune.landsraadtaskreward[]) -> TABLE(term_id bigint, reigning_faction_name text, active_decree_name text, winning_faction_name text, elected_decree_name text, start_time timestamp without time zone, end_time timestamp without time zone)
-- oid: 58412  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_initialize_term(number_of_weeks_term_retention integer, number_of_nominated_decrees integer, in_end_time timestamp without time zone, in_test_term boolean, tasks dune.landsraadtask[], task_rewards dune.landsraadtaskreward[])
 RETURNS TABLE(term_id bigint, reigning_faction_name text, active_decree_name text, winning_faction_name text, elected_decree_name text, start_time timestamp without time zone, end_time timestamp without time zone)
 LANGUAGE plpgsql
AS $function$
DECLARE
	reigning_faction_id SMALLINT = NULL;
	active_decree_id BIGINT = NULL;
	last_active_decree_id BIGINT = NULL;
	current_term_id BIGINT = NULL;
BEGIN
	LOCK TABLE landsraad_decrees, landsraad_decree_term, landsraad_decree_rotation, landsraad_decree_votes IN EXCLUSIVE MODE;

	-- read winning faction, elected and active decree from previous term
	SELECT term.winning_faction_id, term.elected_decree_id, term.active_decree_id INTO reigning_faction_id, active_decree_id, last_active_decree_id FROM landsraad_decree_term term ORDER BY term.term_id DESC LIMIT 1;

	-- insert new term
	INSERT INTO landsraad_decree_term (reigning_faction_id, active_decree_id, start_time, end_time, test_term) VALUES(reigning_faction_id, active_decree_id, now(), in_end_time AT TIME ZONE 'UTC', in_test_term) RETURNING landsraad_decree_term.term_id INTO current_term_id;

	-- cleanup old terms, except for previous one
	DELETE FROM landsraad_decree_term term WHERE term.end_time < (now() - MAKE_INTERVAL(weeks => number_of_weeks_term_retention)) AND term.term_id < current_term_id - 1;

	-- insert tasks for new term
	CALL landsraad_insert_tasks(current_term_id, tasks, task_rewards);

	-- insert decrees for voting
	CALL landsraad_nominate_decrees_for_voting(last_active_decree_id, number_of_nominated_decrees);

	-- clean expired landsraad contracts
	PERFORM journey_story_node_cooldown_delete_expired(now() at time zone 'utc');

	RETURN query SELECT term.term_id, reigning_faction.name, active_decree.decree_name, winning_faction.name, elected_decree.decree_name, (term.start_time AT TIME ZONE 'UTC')::TIMESTAMP, (term.end_time AT TIME ZONE 'UTC')::TIMESTAMP
		FROM landsraad_decree_term term
		LEFT JOIN factions reigning_faction ON term.reigning_faction_id = reigning_faction.id 
		LEFT JOIN landsraad_decrees active_decree ON term.active_decree_id = active_decree.id
		LEFT JOIN factions winning_faction ON term.winning_faction_id = winning_faction.id
		LEFT JOIN landsraad_decrees elected_decree ON term.elected_decree_id = elected_decree.id
		WHERE term.term_id = current_term_id;
END $function$


-- landsraad_insert_task_progress(in_term_id bigint, in_player_id bigint, in_guild_id bigint, in_house_name text, in_faction_progress integer, in_guild_progress real, in_player_progress real, in_timestamp timestamp without time zone) -> void
-- oid: 58413  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_insert_task_progress(in_term_id bigint, in_player_id bigint, in_guild_id bigint, in_house_name text, in_faction_progress integer, in_guild_progress real, in_player_progress real, in_timestamp timestamp without time zone)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	guild_id BIGINT = NULL;
	faction_id BIGINT = NULL;
	faction_name TEXT = NULL;
	progress_id BIGINT = NULL;
BEGIN

	IF in_player_id IS NULL THEN
		-- use guild_id
		SELECT guilds.guild_id, factions.id, factions.name FROM guild_members 
			INNER JOIN guilds ON guild_members.guild_id = guilds.guild_id 
			LEFT JOIN factions ON guilds.guild_faction = factions.id
			WHERE guild_members.guild_id = in_guild_id INTO guild_id, faction_id, faction_name;
	ELSE
		-- use player_id
		SELECT guilds.guild_id, factions.id, factions.name FROM guild_members 
			INNER JOIN guilds ON guild_members.guild_id = guilds.guild_id 
			LEFT JOIN factions ON guilds.guild_faction = factions.id
			WHERE guild_members.player_id = in_player_id INTO guild_id, faction_id, faction_name;
	END IF;

	IF guild_id IS NULL THEN
		RAISE EXCEPTION 'Cannot insert landsraad task progress, player % not in guild', in_player_id;
	END IF;

	IF faction_id IS NULL OR faction_name = 'None' THEN 
		RAISE EXCEPTION 'Cannot insert landsraad task progress, guild % not aligned to faction', guild_id;
	END IF;

	INSERT INTO landsraad_task_progress (faction_id, task_id, faction_progress, guild_progress, player_progress, timestamp) 
		SELECT faction_id, tasks.id, in_faction_progress, in_guild_progress, in_player_progress, in_timestamp AT TIME ZONE 'UTC'
		FROM landsraad_tasks AS tasks 
		WHERE tasks.term_id = in_term_id AND tasks.house_name = in_house_name
		RETURNING id INTO progress_id;

	IF in_player_id IS NOT NULL THEN
		INSERT INTO landsraad_task_progress_player (progress_id, player_id) VALUES (progress_id, in_player_id);
	END IF;

	INSERT INTO landsraad_task_progress_guild (progress_id, guild_id) VALUES (progress_id, guild_id);

END $function$


-- landsraad_insert_task_progress_batched(in_term_id bigint, in_task_progress dune.landsraadtaskprogress[]) -> void
-- oid: 58414  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_insert_task_progress_batched(in_term_id bigint, in_task_progress dune.landsraadtaskprogress[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	task_progress record = NULL;
BEGIN
	FOREACH task_progress IN ARRAY in_task_progress
	LOOP
		PERFORM landsraad_insert_task_progress(in_term_id, task_progress.player_id, task_progress.guild_id ,task_progress.house_name, task_progress.faction_progress, task_progress.guild_progress, task_progress.player_progress, task_progress.timestamp);
	END LOOP;
END $function$


-- landsraad_insert_task_progress_faction(in_term_id bigint, in_faction_name text, in_house_name text, in_faction_progress integer, in_guild_progress real, in_player_progress real) -> void
-- oid: 58415  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_insert_task_progress_faction(in_term_id bigint, in_faction_name text, in_house_name text, in_faction_progress integer, in_guild_progress real, in_player_progress real)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	player_id BIGINT = NULL;
	guild_id BIGINT = NULL;
BEGIN
	SELECT guild_members.player_id, guilds.guild_id FROM guilds 
		LEFT JOIN factions ON guilds.guild_faction = factions.id
		RIGHT JOIN guild_members ON guild_members.guild_id = guilds.guild_id
		WHERE factions.name = in_faction_name
		ORDER BY random() LIMIT 1 INTO player_id, guild_id;
	
	IF player_id IS NULL THEN 
		RAISE EXCEPTION 'Cannot insert landsraad task progress for faction %, no guild member found', in_faction_name;
	END IF;
	
	PERFORM landsraad_insert_task_progress(in_term_id, player_id, guild_id, in_house_name, in_faction_progress, in_guild_progress, in_player_progress, now() AT TIME ZONE 'UTC');
END $function$


-- landsraad_insert_task_progress_random(in_term_id bigint, in_faction_names text[], in_num_rows integer) -> void
-- oid: 58416  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_insert_task_progress_random(in_term_id bigint, in_faction_names text[], in_num_rows integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	player_id BIGINT = NULL;
	guild_id BIGINT = NULL;
	house_name TEXT = NULL;
	random_amount INTEGER = NULL;
BEGIN
	FOR r IN 1..in_num_rows
	LOOP
		SELECT guild_members.player_id, guilds.guild_id FROM guilds 
			LEFT JOIN factions ON guilds.guild_faction = factions.id
			RIGHT JOIN guild_members ON guild_members.guild_id = guilds.guild_id
			WHERE factions.name = ANY(in_faction_names)
			ORDER BY random() LIMIT 1 INTO player_id, guild_id;
		SELECT tasks.house_name FROM landsraad_tasks tasks
			WHERE tasks.term_id = in_term_id 
			ORDER BY random() LIMIT 1 INTO house_name;
		SELECT (floor(random() * 5) + 1)::INTEGER * 10 INTO random_amount;
		IF player_id IS NOT NULL AND house_name IS NOT NULL THEN
			PERFORM landsraad_insert_task_progress(in_term_id, player_id, guild_id, house_name, random_amount * 10, (random_amount * 0.1)::REAL, (random_amount * 10)::REAL, now() AT TIME ZONE 'UTC');
		END IF;
	END LOOP;
END $function$


-- landsraad_insert_tasks(IN in_term_id bigint, IN in_tasks dune.landsraadtask[], IN in_task_rewards dune.landsraadtaskreward[]) -> void
-- oid: 58417  kind: PROCEDURE  category: landsraad

CREATE OR REPLACE PROCEDURE dune.landsraad_insert_tasks(IN in_term_id bigint, IN in_tasks dune.landsraadtask[], IN in_task_rewards dune.landsraadtaskreward[])
 LANGUAGE plpgsql
AS $procedure$
BEGIN
	INSERT INTO landsraad_tasks (term_id, board_index, house_name, goal_amount)
		SELECT in_term_id, tasks.board_index, tasks.house_name, tasks.goal_amount FROM UNNEST(in_tasks) AS tasks;
		
	INSERT INTO landsraad_task_rewards (task_id, threshold, template_id, amount)
		SELECT tasks.id, task_rewards.threshold, task_rewards.template_id, task_rewards.amount FROM UNNEST(in_task_rewards) AS task_rewards
		LEFT JOIN landsraad_tasks AS tasks ON task_rewards.house_name = tasks.house_name WHERE tasks.term_id = in_term_id;
END $procedure$


-- landsraad_load_current_rotation(in_term_id bigint) -> TABLE(decree_name text, received_votes integer, open_votes integer)
-- oid: 58418  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_load_current_rotation(in_term_id bigint)
 RETURNS TABLE(decree_name text, received_votes integer, open_votes integer)
 LANGUAGE plpgsql
AS $function$
DECLARE
	open_votes INTEGER = 0;
BEGIN
	WITH open_guild_votes AS (
		SELECT guild_contribution.guild_id, FLOOR(SUM(guild_contribution.amount))::INTEGER AS voting_influence
			FROM landsraad_tasks AS tasks 
			INNER JOIN landsraad_task_guild_contributions AS guild_contribution
			ON guild_contribution.task_id = tasks.id AND guild_contribution.faction_id = tasks.winning_faction_id
			LEFT JOIN landsraad_decree_votes 
			ON guild_contribution.guild_id = landsraad_decree_votes.guild_id 
			WHERE tasks.term_id = in_term_id AND tasks.winning_faction_id = (SELECT winning_faction_id FROM landsraad_decree_term WHERE term_id = in_term_id)
			AND landsraad_decree_votes.guild_id IS NULL
			GROUP BY (guild_contribution.guild_id, guild_contribution.faction_id)
	) 
	SELECT SUM(voting_influence)::INTEGER FROM open_guild_votes INTO open_votes;

	RETURN query (
		SELECT decrees.decree_name, SUM(decree_votes.influence)::INTEGER AS received_votes, open_votes
			FROM landsraad_decree_rotation AS rotation
			INNER JOIN landsraad_decrees AS decrees ON rotation.decree_id = decrees.id
			LEFT JOIN landsraad_decree_votes AS decree_votes ON decree_votes.decree_id = rotation.decree_id
			GROUP BY rotation.decree_id, decrees.decree_name
	);
END $function$


-- landsraad_load_current_term() -> TABLE(term_id bigint, reigning_faction_name text, active_decree_name text, winning_faction_name text, elected_decree_name text, start_time timestamp without time zone, end_time timestamp without time zone, tasks dune.landsraadtask[], term_task_rewards dune.landsraadtaskreward[], winner_history text[], testterm boolean)
-- oid: 58419  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_load_current_term()
 RETURNS TABLE(term_id bigint, reigning_faction_name text, active_decree_name text, winning_faction_name text, elected_decree_name text, start_time timestamp without time zone, end_time timestamp without time zone, tasks dune.landsraadtask[], term_task_rewards dune.landsraadtaskreward[], winner_history text[], testterm boolean)
 LANGUAGE plpgsql
AS $function$
DECLARE
	current_term_id BIGINT = NULL;
	reigning_faction_name TEXT = NULL;
	active_decree_name TEXT = NULL;
	winning_faction_name TEXT = NULL;
	elected_decree_name TEXT = NULL;
	start_time TIMESTAMP = NULL;
	end_time TIMESTAMP = NULL;
	test_term BOOL = NULL;
	term_tasks LandsraadTask[];
	term_task_rewards LandsraadTaskReward[];
	term_winner_history TEXT[];
BEGIN
	SELECT term.term_id, reigning_faction.name, active_decree.decree_name, winning_faction.name, elected_decree.decree_name, (term.start_time AT TIME ZONE 'UTC')::TIMESTAMP, (term.end_time AT TIME ZONE 'UTC')::TIMESTAMP, term.test_term
		INTO current_term_id, reigning_faction_name, active_decree_name, winning_faction_name, elected_decree_name, start_time, end_time, test_term
		FROM landsraad_decree_term AS term
		LEFT JOIN factions AS reigning_faction ON term.reigning_faction_id = reigning_faction.id 
		LEFT JOIN landsraad_decrees AS active_decree ON term.active_decree_id = active_decree.id
		LEFT JOIN factions AS winning_faction ON term.winning_faction_id = winning_faction.id
		LEFT JOIN landsraad_decrees AS elected_decree ON term.elected_decree_id = elected_decree.id
		ORDER BY term.start_time DESC LIMIT 1;

	SELECT ARRAY_AGG((tasks.board_index, tasks.house_name, tasks.completed, COALESCE(factions_winner.name, ''), tasks.sysselraad, tasks.goal_amount)::LandsraadTask)
        INTO term_tasks		
        FROM landsraad_tasks AS tasks 
		LEFT JOIN factions AS factions_winner ON tasks.winning_faction_id = factions_winner.id
		WHERE tasks.term_id = current_term_id;

	WITH task_rewards AS (
		SELECT tasks.house_name AS house_name, rewards.threshold AS threshold, rewards.template_id AS template_id, rewards.amount AS amount 
		FROM landsraad_task_rewards AS rewards
		LEFT JOIN landsraad_tasks AS tasks ON rewards.task_id = tasks.id
		WHERE tasks.term_id = current_term_id ORDER BY rewards.task_id ASC, rewards.threshold ASC
	)
	SELECT ARRAY_AGG((house_name, threshold, template_id, amount)::LandsraadTaskReward) FROM task_rewards INTO term_task_rewards;

	SELECT ARRAY_AGG(winning_factions.name::TEXT)
	INTO term_winner_history
	FROM
		(SELECT factions.name
			FROM landsraad_decree_term
			LEFT JOIN factions ON landsraad_decree_term.reigning_faction_id = factions.id
			ORDER BY landsraad_decree_term.term_id DESC) AS winning_factions;

	IF current_term_id IS NOT NULL THEN
		RETURN query SELECT current_term_id, reigning_faction_name, active_decree_name, winning_faction_name, elected_decree_name, start_time, end_time, term_tasks, term_task_rewards, term_winner_history, test_term;
	END IF;
END $function$


-- landsraad_load_guild_contribution(in_term_id bigint, in_guild_id bigint, in_faction_id bigint) -> TABLE(voting_influence real)
-- oid: 58420  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_load_guild_contribution(in_term_id bigint, in_guild_id bigint, in_faction_id bigint)
 RETURNS TABLE(voting_influence real)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN query (
		SELECT SUM(guild_contribution.amount)::REAL
			FROM landsraad_tasks AS tasks 
			INNER JOIN landsraad_task_guild_contributions AS guild_contribution
			ON guild_contribution.task_id = tasks.id
			WHERE tasks.term_id = in_term_id AND guild_contribution.guild_id = in_guild_id AND guild_contribution.faction_id = in_faction_id
			GROUP BY (guild_contribution.guild_id, guild_contribution.faction_id)
	);
END $function$


-- landsraad_load_guild_contributions(in_term_id bigint, in_num_guilds integer, in_faction_names text[]) -> TABLE(faction_name text, guild_name text, voting_influence real)
-- oid: 58421  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_load_guild_contributions(in_term_id bigint, in_num_guilds integer, in_faction_names text[])
 RETURNS TABLE(faction_name text, guild_name text, voting_influence real)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN query (
		SELECT factions.name, top_guilds.guild_name, top_guilds.influence FROM factions
		CROSS JOIN LATERAL (
			SELECT guilds.guild_name as guild_name, SUM(guild_contribution.amount)::REAL AS influence
			FROM landsraad_tasks AS tasks 
			INNER JOIN landsraad_task_guild_contributions AS guild_contribution
			ON guild_contribution.task_id = tasks.id AND guild_contribution.faction_id = factions.id
			JOIN guilds
			ON guild_contribution.guild_id = guilds.guild_id
			WHERE tasks.term_id = in_term_id
			GROUP BY (guilds.guild_id, guilds.guild_name)
			ORDER BY influence DESC LIMIT in_num_guilds
		) AS top_guilds
		WHERE factions.name = ANY(in_faction_names)	
	);
END $function$


-- landsraad_load_guild_vote(in_term_id bigint, in_player_id bigint) -> TABLE(decree_name text, voting_influence real)
-- oid: 58422  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_load_guild_vote(in_term_id bigint, in_player_id bigint)
 RETURNS TABLE(decree_name text, voting_influence real)
 LANGUAGE plpgsql
AS $function$
DECLARE
	term_winning_faction_id SMALLINT = NULL;
	player_guild_id BIGINT = NULL;
	guild_faction_id SMALLINT = NULL;
BEGIN
	SELECT guilds.guild_id, guilds.guild_faction FROM guild_members JOIN guilds ON guild_members.guild_id = guilds.guild_id WHERE guild_members.player_id = in_player_id INTO player_guild_id, guild_faction_id;

	RETURN query (
		SELECT 
			CASE WHEN player_guild_id IS NOT NULL AND guild_faction_id IS NOT NULL THEN
				(SELECT COALESCE(decrees.decree_name, '') FROM landsraad_decree_votes AS votes LEFT JOIN landsraad_decrees AS decrees ON votes.decree_id = decrees.id WHERE votes.guild_id = player_guild_id)
			ELSE
				''
			END,
			CASE WHEN player_guild_id IS NOT NULL AND guild_faction_id IS NOT NULL THEN
				(SELECT landsraad_load_guild_contribution(in_term_id, player_guild_id, guild_faction_id))
			ELSE
				0
			END
	);
END $function$


-- landsraad_load_house_rewards(in_player_id bigint) -> TABLE(house_name text, template_id text, amount integer, last_updated timestamp without time zone)
-- oid: 58423  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_load_house_rewards(in_player_id bigint)
 RETURNS TABLE(house_name text, template_id text, amount integer, last_updated timestamp without time zone)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN query (SELECT rewards.house_name, rewards.template_id, rewards.amount, (rewards.last_updated AT TIME ZONE 'UTC')::TIMESTAMP FROM landsraad_house_rewards AS rewards WHERE player_id = in_player_id AND rewards.amount > 0);
END $function$


-- landsraad_load_player_contributions(in_term_id bigint, in_player_ids bigint[]) -> TABLE(player_id bigint, board_index smallint, amount integer)
-- oid: 58424  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_load_player_contributions(in_term_id bigint, in_player_ids bigint[])
 RETURNS TABLE(player_id bigint, board_index smallint, amount integer)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN query (
		SELECT contributions.player_id, tasks.board_index, FLOOR(SUM(contributions.amount))::INTEGER FROM landsraad_task_player_contributions AS contributions
			INNER JOIN landsraad_tasks AS tasks ON contributions.task_id = tasks.id
			WHERE tasks.term_id = in_term_id AND contributions.player_id = ANY(in_player_ids)
			GROUP BY contributions.player_id, tasks.board_index
	);
END $function$


-- landsraad_load_task_faction_progress(in_term_id bigint) -> TABLE(task_board_index integer, faction_name text, progress integer)
-- oid: 58425  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_load_task_faction_progress(in_term_id bigint)
 RETURNS TABLE(task_board_index integer, faction_name text, progress integer)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN query SELECT CAST(faction_progress.board_index AS INTEGER), faction_progress.name, CAST(faction_progress.progress AS INTEGER) FROM 
		(SELECT tasks.id, tasks.board_index, factions.name, SUM(faction_contribution.amount) AS progress
			FROM landsraad_tasks tasks 
			INNER JOIN landsraad_task_faction_contributions faction_contribution
			ON faction_contribution.task_id = tasks.id
			LEFT JOIN factions factions
			ON factions.id = faction_contribution.faction_id
			WHERE tasks.term_id = in_term_id
			GROUP BY (tasks.id, tasks.board_index, factions.name)) AS faction_progress;
END $function$


-- landsraad_load_task_faction_reveal_state(in_term_id bigint) -> TABLE(task_board_index integer, faction_name text, reveal_state boolean, time_stamp timestamp without time zone)
-- oid: 58426  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_load_task_faction_reveal_state(in_term_id bigint)
 RETURNS TABLE(task_board_index integer, faction_name text, reveal_state boolean, time_stamp timestamp without time zone)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN query SELECT CAST(tasks.board_index AS INTEGER), factions.name, reveal_state.revealed, (reveal_state.timestamp AT TIME ZONE 'UTC')::TIMESTAMP
			FROM landsraad_tasks tasks 
			INNER JOIN landsraad_task_reveal_state reveal_state
			ON reveal_state.task_id = tasks.id
			INNER JOIN factions factions
			ON factions.id = reveal_state.faction_id
			WHERE tasks.term_id = in_term_id;
END $function$


-- landsraad_load_term_progress(in_term_id bigint, in_num_guilds integer, in_faction_names text[], in_player_ids bigint[]) -> TABLE(faction_progress dune.landsraadtaskfactionprogress[], faction_reveal_state dune.landsraadtaskfactionrevealstate[], guild_contributions dune.landsraadguildcontribution[], player_contributions dune.landsraadplayercontribution[])
-- oid: 58427  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_load_term_progress(in_term_id bigint, in_num_guilds integer, in_faction_names text[], in_player_ids bigint[])
 RETURNS TABLE(faction_progress dune.landsraadtaskfactionprogress[], faction_reveal_state dune.landsraadtaskfactionrevealstate[], guild_contributions dune.landsraadguildcontribution[], player_contributions dune.landsraadplayercontribution[])
 LANGUAGE plpgsql
AS $function$
DECLARE
	term_faction_progress LandsraadTaskFactionProgress[];
	term_faction_reveal_state LandsraadTaskFactionRevealState[];
	term_guild_contributions LandsraadGuildContribution[];
	term_player_contributions LandsraadPlayerContribution[];
BEGIN
	SELECT ARRAY_AGG((task_board_index, faction_name, progress)::LandsraadTaskFactionProgress) FROM landsraad_load_task_faction_progress(in_term_id) INTO term_faction_progress;
	SELECT ARRAY_AGG((task_board_index, faction_name, reveal_state, time_stamp)::LandsraadTaskFactionRevealState) FROM landsraad_load_task_faction_reveal_state(in_term_id) INTO term_faction_reveal_state;
	SELECT ARRAY_AGG((faction_name, guild_name, voting_influence)::LandsraadGuildContribution) FROM landsraad_load_guild_contributions(in_term_id, in_num_guilds, in_faction_names) INTO term_guild_contributions;
	SELECT ARRAY_AGG((player_id, board_index, amount)::LandsraadPlayerContribution) FROM landsraad_load_player_contributions(in_term_id, in_player_ids) INTO term_player_contributions;

	RETURN query SELECT term_faction_progress, term_faction_reveal_state, term_guild_contributions, term_player_contributions;
END $function$


-- landsraad_nominate_decrees_for_voting(IN last_active_decree_id bigint, IN num_decrees integer) -> void
-- oid: 58428  kind: PROCEDURE  category: landsraad

CREATE OR REPLACE PROCEDURE dune.landsraad_nominate_decrees_for_voting(IN last_active_decree_id bigint, IN num_decrees integer)
 LANGUAGE plpgsql
AS $procedure$
BEGIN
	LOCK TABLE landsraad_decrees, landsraad_decree_rotation, landsraad_decree_votes IN EXCLUSIVE MODE;
	
	TRUNCATE TABLE landsraad_decree_votes;
	TRUNCATE TABLE landsraad_decree_rotation;

	INSERT INTO landsraad_decree_rotation
		SELECT id FROM landsraad_decrees
		WHERE (
			CASE WHEN last_active_decree_id IS NULL THEN
				True
			ELSE
				last_active_decree_id != id
			END
		) AND disabled = FALSE
		ORDER BY RANDOM() * weight DESC
		LIMIT num_decrees;
END $procedure$


-- landsraad_notify_house_rewards_changed() -> trigger
-- oid: 58429  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_notify_house_rewards_changed()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
	PERFORM pg_notify('landsraad_notify_channel', format('house_rewards_changed#{"PlayerId" : %s}', NEW.player_id));
    RETURN NULL;
END $function$


-- landsraad_perform_daily_task_reveal(in_term_id bigint, in_faction_names text[], in_house_names_to_reveal text[], in_reveal_day integer) -> TABLE(faction_name text, house_name text, board_index integer)
-- oid: 58430  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_perform_daily_task_reveal(in_term_id bigint, in_faction_names text[], in_house_names_to_reveal text[], in_reveal_day integer)
 RETURNS TABLE(faction_name text, house_name text, board_index integer)
 LANGUAGE plpgsql
AS $function$
DECLARE
    last_processed_reveal_day INTEGER = NULL;
    faction_ids BIGINT[];
	newly_revealed_task_ids BIGINT[];
	newly_revealed_house_names TEXT[];
	newly_revealed_task_board_indices INTEGER[];
    faction_of_newly_revealed_task BIGINT[];
BEGIN
    LOCK TABLE landsraad_decree_term IN EXCLUSIVE MODE;
    
    SELECT landsraad_decree_term.last_processed_reveal_day FROM landsraad_decree_term WHERE term_id = in_term_id INTO last_processed_reveal_day;

    IF last_processed_reveal_day < in_reveal_day THEN
        SELECT ARRAY_AGG(factions.id) FROM factions WHERE factions.name = ANY(in_faction_names) INTO faction_ids;
                
        WITH revealed_task(id, faction_id) AS (
            SELECT task.id, faction.id FROM landsraad_tasks AS task
                CROSS JOIN UNNEST(faction_ids) AS faction(id)
                WHERE task.house_name = ANY (in_house_names_to_reveal) AND task.term_id = in_term_id)
        --filter out tasks already revealed from data to not stomp reveal date or send duplicate reveal event in telemetry
        SELECT ARRAY_AGG(task.id), ARRAY_AGG(task.house_name), ARRAY_AGG(task.board_index), ARRAY_AGG(revealed_task.faction_id) FROM revealed_task
            INNER JOIN landsraad_tasks AS task ON task.id = revealed_task.id
            LEFT JOIN landsraad_task_reveal_state AS reveal_state ON task.id = reveal_state.task_id AND revealed_task.faction_id = reveal_state.faction_id
            WHERE reveal_state.revealed IS NULL OR reveal_state.revealed IS FALSE
            INTO newly_revealed_task_ids, newly_revealed_house_names, newly_revealed_task_board_indices, faction_of_newly_revealed_task;
        
        INSERT INTO landsraad_task_reveal_state (task_id, faction_id, revealed, timestamp) SELECT UNNEST(newly_revealed_task_ids), UNNEST(faction_of_newly_revealed_task), TRUE, now() 
            ON CONFLICT(task_id, faction_id) DO UPDATE SET revealed = TRUE, timestamp = now();
        
        UPDATE landsraad_decree_term SET last_processed_reveal_day = in_reveal_day WHERE term_id = in_term_id;
        
        IF cardinality(newly_revealed_task_ids) > 0 THEN
            PERFORM pg_notify('landsraad_notify_channel', 'progress_updated#{"changed": true}');
        END IF;
    END IF;

	RETURN query
        WITH newly_revealed_tasks (house_name, board_index, faction_id) AS (
            SELECT UNNEST(newly_revealed_house_names), UNNEST(newly_revealed_task_board_indices), UNNEST(faction_of_newly_revealed_task))
        SELECT factions.name, newly_revealed_tasks.house_name, newly_revealed_tasks.board_index FROM newly_revealed_tasks JOIN factions ON newly_revealed_tasks.faction_id = factions.id;
END $function$


-- landsraad_process_house_rewards() -> trigger
-- oid: 58431  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_process_house_rewards()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
	WITH 
		task_player_contribution_threshold_passed (player_id, house_name, template_id, amount) AS (
			SELECT NEW.player_id, tasks.house_name, task_rewards.template_id, task_rewards.amount
			FROM landsraad_task_rewards as task_rewards
            INNER JOIN landsraad_tasks AS tasks
            ON task_rewards.task_id = tasks.id
			LEFT JOIN landsraad_task_player_contributions AS player_contributions
			ON player_contributions.task_id = tasks.id
			WHERE task_rewards.task_id = NEW.task_id
				AND tasks.id = NEW.task_id 
				AND player_contributions.player_id = NEW.player_id
                AND COALESCE(OLD.amount, 0) < task_rewards.threshold
				AND NEW.amount >= task_rewards.threshold)
	INSERT INTO landsraad_house_rewards (player_id, house_name, template_id, amount, last_updated)
		SELECT player_id, house_name, template_id, SUM(amount), CURRENT_TIMESTAMP FROM task_player_contribution_threshold_passed GROUP BY player_id, house_name, template_id
		ON CONFLICT (player_id, house_name, template_id) DO UPDATE SET amount = landsraad_house_rewards.amount + excluded.amount, last_updated = CURRENT_TIMESTAMP;

	RETURN NULL;
END $function$


-- landsraad_process_task_progress(max_rows integer) -> void
-- oid: 58432  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_process_task_progress(max_rows integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	task_progress RECORD = NULL;
	current_term_id BIGINT = NULL;
	last_progress_id BIGINT = NULL;
	old_processed_id BIGINT = NULL;
	new_processed_id BIGINT = NULL;
	new_amount INTEGER = 0;
	player BIGINT = NULL;
	guild BIGINT = NULL;
	notify_guild_ids BIGINT[];
	guild_ids_json JSON = NULL;
BEGIN
	LOCK TABLE landsraad_task_progress_processed IN EXCLUSIVE MODE;

	SELECT term_id FROM landsraad_decree_term ORDER BY start_time DESC LIMIT 1 INTO current_term_id;

	SELECT id FROM landsraad_task_progress ORDER BY id DESC LIMIT 1 INTO last_progress_id;

	SELECT last_processed_id FROM landsraad_task_progress_processed INTO old_processed_id;

	-- read batch of rows sorted by id, process ordered by timestamp
	FOR task_progress IN 
	WITH progress_batch AS (
		SELECT landsraad_task_progress.id, landsraad_task_progress.faction_id, landsraad_task_progress.task_id,
			landsraad_task_progress.faction_progress, landsraad_task_progress.guild_progress, landsraad_task_progress.player_progress, landsraad_task_progress.timestamp, 
			task_progress_players.players, task_progress_guilds.guilds
		FROM landsraad_task_progress,
		LATERAL (SELECT ARRAY_AGG(player_id) AS players FROM landsraad_task_progress_player WHERE landsraad_task_progress_player.progress_id = landsraad_task_progress.id) AS task_progress_players, 
		LATERAL (SELECT ARRAY_AGG(guild_id) AS guilds FROM landsraad_task_progress_guild WHERE landsraad_task_progress_guild.progress_id = landsraad_task_progress.id) AS task_progress_guilds
		WHERE CASE WHEN old_processed_id IS NOT NULL THEN id > old_processed_id ELSE TRUE END
		ORDER BY id LIMIT MAX_ROWS
	)
	SELECT id, faction_id, task_id, players, guilds, faction_progress, guild_progress, player_progress FROM progress_batch ORDER BY timestamp
	LOOP
		IF NOT (SELECT landsraad_has_term_of_task_ended(task_progress.task_id)) THEN
            -- player progress is allowed to happen even if the task was already completed
			IF task_progress.players IS NOT NULL THEN
				FOREACH player IN ARRAY task_progress.players 
				LOOP
					INSERT INTO landsraad_task_player_contributions AS player_contribution (player_id, faction_id, task_id, amount) 
						VALUES (player, task_progress.faction_id, task_progress.task_id, task_progress.player_progress) 
						ON CONFLICT (player_id, faction_id, task_id)
						DO UPDATE SET amount = player_contribution.amount + task_progress.player_progress;
				END LOOP;
			END IF;
            
            IF NOT (SELECT landsraad_task_has_been_completed(task_progress.task_id)) THEN
                IF task_progress.guilds IS NOT NULL THEN
                    FOREACH guild IN ARRAY task_progress.guilds
                    LOOP
                        -- only insert to guild contribution if no vote has been placed
                        IF (SELECT NOT EXISTS (SELECT 1 FROM landsraad_decree_votes WHERE landsraad_decree_votes.guild_id = guild)) THEN
                            INSERT INTO landsraad_task_guild_contributions AS guild_contribution (guild_id, faction_id, task_id, amount) 
                            VALUES (guild, task_progress.faction_id, task_progress.task_id, task_progress.guild_progress) 
                            ON CONFLICT (guild_id, faction_id, task_id)
                            DO UPDATE SET amount = guild_contribution.amount + task_progress.guild_progress;
                            notify_guild_ids = notify_guild_ids || guild;	
                        END IF;
                        
                    END LOOP;
                END IF;

                INSERT INTO landsraad_task_faction_contributions AS faction_contribution (faction_id, task_id, amount) 
                    VALUES (task_progress.faction_id, task_progress.task_id, task_progress.faction_progress) 
                    ON CONFLICT (faction_id, task_id)
                    DO UPDATE SET amount = faction_contribution.amount + task_progress.faction_progress;
            END IF;
		END IF;

		new_processed_id = task_progress.id;
	END LOOP;
	
	IF new_processed_id IS NOT NULL THEN
		IF old_processed_id IS NULL THEN
			INSERT INTO landsraad_task_progress_processed (last_processed_id) VALUES (new_processed_id);
		ELSE
			UPDATE landsraad_task_progress_processed SET last_processed_id = new_processed_id;
		END IF;
	END IF;
	
	IF last_progress_id > new_processed_id THEN
		PERFORM pg_notify('landsraad_notify_channel', format('progress_pressure#{"UnprocessedCount": %s}', last_progress_id - new_processed_id));
	END IF;
	
	IF new_processed_id > old_processed_id THEN
		PERFORM pg_notify('landsraad_notify_channel', 'progress_updated#{"changed": true}');
	ELSE
		PERFORM pg_notify('landsraad_notify_channel', 'progress_updated#{"changed": false}');
	END IF;

    IF cardinality(notify_guild_ids) > 0 THEN
        SELECT json_agg(DISTINCT guild_id) FROM (SELECT unnest(notify_guild_ids) guild_id) guilds INTO guild_ids_json;
        PERFORM pg_notify('landsraad_notify_channel', format('guild_vote_changed#{"GuildIds": %s}', guild_ids_json));
    END IF;

END $function$


-- landsraad_task_has_been_completed(in_task_id bigint) -> boolean
-- oid: 58433  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_task_has_been_completed(in_task_id bigint)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
    task_completed BOOLEAN = FALSE;
BEGIN
    SELECT task.completed FROM landsraad_tasks AS task WHERE task.id = in_task_id INTO task_completed;
    RETURN task_completed;
END $function$


-- landsraad_update_decrees(IN in_decrees dune.landsraaddecree[]) -> void
-- oid: 58434  kind: PROCEDURE  category: landsraad

CREATE OR REPLACE PROCEDURE dune.landsraad_update_decrees(IN in_decrees dune.landsraaddecree[])
 LANGUAGE plpgsql
AS $procedure$
BEGIN
	UPDATE landsraad_decrees SET disabled = TRUE WHERE decree_name NOT IN (
        SELECT(UNNEST(in_decrees)).decree_name
    );
	INSERT INTO landsraad_decrees (decree_name, version, disabled, weight) 
		SELECT decrees.decree_name, decrees.version, decrees.disabled, decrees.weight FROM UNNEST(in_decrees) AS decrees
		ON CONFLICT(decree_name) DO UPDATE SET version = excluded.version, disabled = excluded.disabled, weight = excluded.weight;
END $procedure$


-- landsraad_update_factions(IN in_faction_names text[]) -> void
-- oid: 58435  kind: PROCEDURE  category: landsraad

CREATE OR REPLACE PROCEDURE dune.landsraad_update_factions(IN in_faction_names text[])
 LANGUAGE plpgsql
AS $procedure$
BEGIN
	WITH new_factions AS (
        SELECT f FROM UNNEST(in_faction_names) f LEFT JOIN factions ON f = factions.name WHERE id IS NULL
    )
	INSERT INTO factions (name) SELECT * FROM new_factions;
END $procedure$


-- landsraad_update_task_faction_reveal_state(in_term_id bigint, in_task_board_index integer, faction_name text, reveal_state boolean) -> void
-- oid: 58436  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_update_task_faction_reveal_state(in_term_id bigint, in_task_board_index integer, faction_name text, reveal_state boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	taskid BIGINT = NULL;
	factionid BIGINT = NULL;
BEGIN
	SELECT id FROM landsraad_tasks tasks WHERE tasks.board_index = in_task_board_index AND tasks.term_id = in_term_id INTO taskid;
	
	IF taskid IS NULL THEN 
		RAISE EXCEPTION 'Cannot update landsraad task reveal state, no task id for index % term %', in_task_board_index, in_term_id;
	END IF;
	
	SELECT id FROM factions WHERE factions.name = faction_name INTO factionid;
	
	IF factionid IS NULL OR faction_name = 'None' THEN 
		RAISE EXCEPTION 'Cannot update landsraad task reveal state, invalid faction (%)', faction_name;
	END IF;
	
	INSERT INTO landsraad_task_reveal_state (task_id, faction_id, revealed, timestamp) VALUES (taskid, factionid, reveal_state, now()) ON CONFLICT(task_id, faction_id) DO UPDATE
		SET revealed = reveal_state, timestamp = now();

	PERFORM pg_notify('landsraad_notify_channel', 'progress_updated#{"changed": true}');	
END $function$


-- landsraad_withdraw_house_reward(in_player_id bigint, in_house_rewards dune.landsraadplayerhousereward[]) -> void
-- oid: 58437  kind: FUNCTION  category: landsraad

CREATE OR REPLACE FUNCTION dune.landsraad_withdraw_house_reward(in_player_id bigint, in_house_rewards dune.landsraadplayerhousereward[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	current_amount INTEGER = NULL;
	house_reward record = NULL;
    grouped_house_rewards LandsraadPlayerHouseReward[];
BEGIN
	LOCK TABLE landsraad_house_rewards IN EXCLUSIVE MODE;
    --group rewards to make sure multiple entries of the same item do not slip by amount verfication below
    WITH grouped_rewards AS (SELECT house_name, template_id, SUM(amount) as amount FROM UNNEST(in_house_rewards) GROUP BY house_name, template_id)
        SELECT ARRAY_AGG((grouped_rewards.house_name, grouped_rewards.template_id, grouped_rewards.amount)::LandsraadPlayerHouseReward) INTO grouped_house_rewards FROM grouped_rewards;
    
	FOREACH house_reward in ARRAY grouped_house_rewards
	LOOP
		SELECT lhr.amount INTO current_amount FROM landsraad_house_rewards AS lhr WHERE lhr.player_id = in_player_id AND lhr.house_name = house_reward.house_name AND lhr.template_id = house_reward.template_id;

		IF current_amount IS NULL OR current_amount < house_reward.amount THEN
			RAISE EXCEPTION 'Cannot withdraw house reward %s for player % and house %s', house_reward.template_id, in_player_id, house_reward.house_name;
			RETURN;
		END IF;
	END LOOP;
	-- finish full loop of checks first, all rewards need to be withdrawable before updating
	FOREACH house_reward in ARRAY grouped_house_rewards
	LOOP
		UPDATE landsraad_house_rewards SET amount = amount - house_reward.amount, last_updated = CURRENT_TIMESTAMP WHERE player_id = in_player_id AND house_name = house_reward.house_name AND template_id = house_reward.template_id;
	END LOOP;
END $function$


-- load_actors(in_actor_ids bigint[], in_actor_state dune.actorstate) -> TABLE(ord bigint, actor_id bigint, generic_data dune.actorgenericdata, serial bigint)
-- oid: 58438  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.load_actors(in_actor_ids bigint[], in_actor_state dune.actorstate DEFAULT 'Default'::dune.actorstate)
 RETURNS TABLE(ord bigint, actor_id bigint, generic_data dune.actorgenericdata, serial bigint)
 LANGUAGE plpgsql
AS $function$
begin
	return query
		with
			ids as (
				select * from unnest(in_actor_ids) with ordinality as t(id, ord)
			),
			entities as (
				select
					fgl_bridge.actor_id,
					(fgl_bridge.entity_id, fgl_bridge.slot_name, entity_data.components)::FglEntity as data
				from
					ids
					left join actor_fgl_entities as fgl_bridge on ids.id=fgl_bridge.actor_id
					left join fgl_entities as entity_data on fgl_bridge.entity_id = entity_data.entity_id
			)
		select
			ids.ord, actors.id,
			(
				coalesce(array_agg(entities.data) filter (where entities.data is not null), array[]::FglEntity[]),
				actors.properties,
				actors.gas_attributes,
				case
					when exists(select 1 from buildings where actors.id = buildings.id) then load_building(actors.id)
				end
				,
				case
					when exists(select 1 from placeables where actors.id = placeables.id) then load_placeable(actors.id)
				end
				,
				case
					when exists(select 1 from totems where actors.id = totems.id) then load_totem(actors.id)
				end
			)::ActorGenericData, actors.serial
		from
			ids
			join actors using (id)
			left join entities on actors.id = entities.actor_id
		where
        	case when (in_actor_state = 'Default') then
	        	not exists(select 1 from actor_state where actors.id = actor_state.actor_id)
    	    else
 	        	exists(select 1 from actor_state where actors.id = actor_state.actor_id and actor_state.state = in_actor_state)
    	    end
		group by ids.ord, actors.id, actors.properties, actors.gas_attributes, actors.serial
		order by ids.ord;
END
$function$


-- load_backup_vehicle(in_account_id bigint) -> TABLE(out_id bigint, out_class text, out_customization_id text)
-- oid: 58439  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.load_backup_vehicle(in_account_id bigint)
 RETURNS TABLE(out_id bigint, out_class text, out_customization_id text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
		SELECT a.id, a.class, bv.customization_id
		FROM actors a
		JOIN backup_vehicles bv on a.id = bv.vehicle_id 
		WHERE bv.account_id = in_account_id
		LIMIT 1;
END
$function$


-- load_building(in_building_id bigint) -> dune.buildingsavedata
-- oid: 58449  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.load_building(in_building_id bigint)
 RETURNS dune.buildingsavedata
 LANGUAGE sql
BEGIN ATOMIC
 RETURN ( SELECT ROW(array_agg(ROW(building_instances.instance_id, building_instances.building_type, building_instances.transform, building_instances.owner_entity_id, building_instances.building_flags, building_instances.health, building_instances.shelter, building_instances.stabilization_begin_timespan, building_instances.stabilization_end_timespan, building_instances.stabilization_state, building_instances.sand_buildup)::dune.buildinginstance), ARRAY[]::integer[], ARRAY[]::dune.buildinginstanceupdateowner[], ARRAY[]::dune.buildinginstanceupdatestabilization[], ARRAY[]::dune.buildinginstanceupdatehealth[], ARRAY[]::dune.buildinginstanceupdateshelter[], ARRAY[]::dune.buildinginstanceupdatesandbuildup[], ARRAY[]::dune.buildinginstanceupdatebuildingflags[], ARRAY[]::dune.buildinginstanceupdatetransform[])::dune.buildingsavedata AS "row"
            FROM dune.building_instances
           WHERE (building_instances.building_id = load_building.in_building_id));
END


-- load_communinet_player_data(in_account_id bigint) -> TABLE(is_active boolean, selected_channel_name text, channel_name text, is_tuned boolean)
-- oid: 58450  kind: FUNCTION  category: communinet

CREATE OR REPLACE FUNCTION dune.load_communinet_player_data(in_account_id bigint)
 RETURNS TABLE(is_active boolean, selected_channel_name text, channel_name text, is_tuned boolean)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
	SELECT cp.is_active, cp.selected_channel_name, cpc.channel_name, cpc.is_tuned
	FROM communinet_player AS cp  JOIN communinet_player_channels as cpc
	ON cp.account_id = cpc.account_id
	WHERE cpc.account_id = in_account_id;
END; $function$


-- load_dialogue_data(in_player_controller_id bigint, OUT met_npcs text[], OUT taken_nodes integer[]) -> record
-- oid: 58451  kind: FUNCTION  category: dialogue

CREATE OR REPLACE FUNCTION dune.load_dialogue_data(in_player_controller_id bigint, OUT met_npcs text[], OUT taken_nodes integer[])
 RETURNS record
 LANGUAGE plpgsql
AS $function$
BEGIN
    SELECT ARRAY_AGG(npc_name)
    INTO met_npcs
    FROM dialogue_met_npcs
    WHERE player_id = in_player_controller_id;

    SELECT ARRAY_AGG(node_id)
    INTO taken_nodes
    FROM dialogue_taken_nodes
    WHERE player_id = in_player_controller_id;
END
$function$


-- load_dimension_index(in_map text, in_partition_id bigint) -> integer
-- oid: 58452  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.load_dimension_index(in_map text, in_partition_id bigint)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN (SELECT dimension_index from world_partition where map = in_map and partition_id = in_partition_id limit 1);
END; $function$


-- load_events_log_data_from_player(in_actor_id bigint, in_limit_entries_num integer) -> TABLE(game_event_owner bigint, universe_time timestamp without time zone, map_name text, partition_id bigint, event_type integer, x_location double precision, y_location double precision, z_location double precision, custom_data jsonb)
-- oid: 58453  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.load_events_log_data_from_player(in_actor_id bigint, in_limit_entries_num integer)
 RETURNS TABLE(game_event_owner bigint, universe_time timestamp without time zone, map_name text, partition_id bigint, event_type integer, x_location double precision, y_location double precision, z_location double precision, custom_data jsonb)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	SELECT temp.actor_id, (temp.universe_time AT TIME ZONE 'UTC')::TIMESTAMP, temp.map, temp.partition_id, temp.event_type, temp.x, temp.y, temp.z, temp.custom_data 
	FROM (SELECT game_events.actor_id, game_events.universe_time, game_events.map, game_events.partition_id, game_events.event_type, game_events.x, game_events.y, game_events.z, game_events.custom_data FROM game_events WHERE game_events.actor_id = in_actor_id AND game_events.player_facing_event = true ORDER BY game_events.universe_time DESC LIMIT in_limit_entries_num) temp 
	ORDER BY temp.universe_time ASC;
END
$function$


-- load_full_actors(in_ids bigint[]) -> SETOF dune.actordescription
-- oid: 58454  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.load_full_actors(in_ids bigint[])
 RETURNS SETOF dune.actordescription
 LANGUAGE plpgsql
AS $function$
begin
	return query
		with
			ids as (
				select * from unnest(in_ids) with ordinality as t(id, ord)
			),
			entities as (
				select
					actor_id,
					(fgl_bridge.entity_id, fgl_bridge.slot_name, entity_data.components)::FglEntity as data
				from
					ids
					left join actor_fgl_entities as fgl_bridge on ids.id=fgl_bridge.actor_id
					left join fgl_entities as entity_data using (entity_id)
			)
		select
			id, "class", "transform", (
				coalesce(array_agg(entities.data) filter (where entities.data is not null), array[]::FglEntity[]),
				actors.properties,
				actors.gas_attributes,
				case
					when exists(select 1 from buildings where ids.id = buildings.id) then load_building(id)
					else null
				end,
				case
					when exists(select 1 from placeables where ids.id = placeables.id) then load_placeable(id)
					else null
				end
				,
				case
					when exists(select 1 from totems where id = totems.id) then load_totem(id)
				end
			)::ActorGenericData, actors.serial
		from
			ids
			join actors using (id)
			left join entities on id=entities.actor_id
		group by ids.ord, id, class, "transform", actors.properties, actors.gas_attributes, actors.serial
		order by ids.ord;
end
$function$


-- load_item(in_item_id bigint) -> TABLE(item_id bigint, stack_size bigint, quality_level bigint, volume_override real, position_index bigint, template_id text, inventory_id bigint, is_new boolean, acquisition_time bigint, stats jsonb, sub_inventory_id bigint)
-- oid: 58455  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.load_item(in_item_id bigint)
 RETURNS TABLE(item_id bigint, stack_size bigint, quality_level bigint, volume_override real, position_index bigint, template_id text, inventory_id bigint, is_new boolean, acquisition_time bigint, stats jsonb, sub_inventory_id bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT item.id, item.stack_size, item.quality_level, item.volume_override, item.position_index, item.template_id, item.inventory_id, item.is_new, item.acquisition_time, item.stats, inventory.id
    FROM items item
    LEFT JOIN inventories inventory ON (inventory.item_id = item.id)
    WHERE item.id = in_item_id;
END; $function$


-- load_items(in_inventory_id bigint) -> TABLE(item_id bigint, stack_size bigint, quality_level bigint, volume_override real, position_index bigint, template_id text, inventory_id bigint, is_new boolean, acquisition_time bigint, stats jsonb, sub_inventory_id bigint)
-- oid: 58456  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.load_items(in_inventory_id bigint)
 RETURNS TABLE(item_id bigint, stack_size bigint, quality_level bigint, volume_override real, position_index bigint, template_id text, inventory_id bigint, is_new boolean, acquisition_time bigint, stats jsonb, sub_inventory_id bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH RECURSIVE items_cte AS (
    SELECT item.id, item.stack_size, item.quality_level, item.volume_override, item.position_index, item.template_id, item.inventory_id, item.is_new, item.acquisition_time, item.stats, inventory.id as sub_inventory_id
    FROM items item
    LEFT JOIN inventories inventory ON (inventory.item_id = item.id)
    WHERE item.inventory_id = in_inventory_id
    UNION ALL
    SELECT item.id, item.stack_size, item.quality_level, item.volume_override, item.position_index, item.template_id, item.inventory_id, item.is_new, item.acquisition_time, item.stats, inventory.id as sub_inventory_id
    FROM items item
    LEFT JOIN inventories inventory ON (inventory.item_id = item.id)
    JOIN items_cte ON item.inventory_id = items_cte.sub_inventory_id
    WHERE items_cte.sub_inventory_id IS NOT NULL
)
SELECT * FROM items_cte order by id asc;
END; $function$


-- load_map_areas_entries(in_account_id bigint, in_map_name text) -> TABLE(account_id bigint, area_id smallint, time_discovered timestamp without time zone, time_first_entered timestamp without time zone, survey_point_marker_id bigint, items_surveyed_target jsonb, items_surveyed_progress jsonb, map_name text)
-- oid: 58457  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.load_map_areas_entries(in_account_id bigint, in_map_name text)
 RETURNS TABLE(account_id bigint, area_id smallint, time_discovered timestamp without time zone, time_first_entered timestamp without time zone, survey_point_marker_id bigint, items_surveyed_target jsonb, items_surveyed_progress jsonb, map_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
        SELECT
            map_areas.account_id,
            map_areas.area_id,
            map_areas.time_discovered AT TIME ZONE 'UTC',
            map_areas.time_first_entered AT TIME ZONE 'UTC',
            map_areas.survey_point_marker_id,
            map_areas.items_surveyed_target,
            map_areas.items_surveyed_progress,
            map_areas.map_name
        from map_areas WHERE map_areas.account_id = in_account_id AND map_areas.map_name = in_map_name;
END
$function$


-- load_markers(in_player_id bigint, in_dimension_id integer, in_map_name text) -> TABLE(out_marker_hash_id integer, out_marker_type text, out_x double precision, out_y double precision, out_z double precision, out_payload_type text, out_area_id smallint, out_area_radius real, out_long_range boolean, out_payload jsonb, out_discovery_level smallint, out_discovery_method smallint, out_player_payload jsonb)
-- oid: 58458  kind: FUNCTION  category: markers

CREATE OR REPLACE FUNCTION dune.load_markers(in_player_id bigint, in_dimension_id integer, in_map_name text)
 RETURNS TABLE(out_marker_hash_id integer, out_marker_type text, out_x double precision, out_y double precision, out_z double precision, out_payload_type text, out_area_id smallint, out_area_radius real, out_long_range boolean, out_payload jsonb, out_discovery_level smallint, out_discovery_method smallint, out_player_payload jsonb)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
   		SELECT
			player_markers.marker_hash_id,
			(markers.marker).marker_type,
			(markers.marker).x,
			(markers.marker).y,
			(markers.marker).z,
			(markers.marker).payload_type,
			area_id,
			area_radius,
			long_range,
			markers.payload,
			discovery_level,
			discovery_method,
			player_markers.payload
		FROM map_names JOIN markers ON markers.map_name_id = map_names.map_name_id
			JOIN player_markers ON markers.marker_hash_id = player_markers.marker_hash_id
			AND markers.dimension_index = player_markers.dimension_index
			AND markers.map_name_id = player_markers.map_name_id
		WHERE player_markers.player_id = in_player_id
			AND (player_markers.dimension_index = in_dimension_id OR player_markers.dimension_index = -1)
			AND map_names.map_name = in_map_name;
END
$function$


-- load_partition_definition_map() -> TABLE(out_partition_id bigint, out_server_id text, out_partition_definition jsonb, out_dimension_index integer, out_blocked boolean, out_label text, out_map text)
-- oid: 58459  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.load_partition_definition_map()
 RETURNS TABLE(out_partition_id bigint, out_server_id text, out_partition_definition jsonb, out_dimension_index integer, out_blocked boolean, out_label text, out_map text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
	SELECT wp.partition_id, active_server_ids.server_id AS server_id, wp.partition_definition,
		   wp.dimension_index, wp.blocked, wp.label, wp.map
	FROM world_partition as wp
	LEFT JOIN active_server_ids
	ON active_server_ids.server_id = wp.server_id;
END; $function$


-- load_placeable(in_placeable_id bigint) -> dune.placeablesavedata
-- oid: 58460  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.load_placeable(in_placeable_id bigint)
 RETURNS dune.placeablesavedata
 LANGUAGE plpgsql
AS $function$
DECLARE
	result PlaceableSaveData;
BEGIN
	SELECT
		owner_entity_id as in_owner_entity_id,
		health as in_health,
		building_type as in_building_type,
		has_hit_ground as in_has_hit_ground,
		has_buildable_support as in_has_buildable_support,
		is_hologram as in_is_hologram
	INTO result
	FROM placeables
	WHERE id = in_placeable_id;
	return result;
END
$function$


-- load_recovered_vehicles(in_account_id bigint, in_restore_time_limit integer) -> TABLE(out_vehicle_id bigint, out_class text, out_name text, out_time_stored timestamp without time zone, out_chassis_durability real, out_customization_id text, out_migrated boolean)
-- oid: 58461  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.load_recovered_vehicles(in_account_id bigint, in_restore_time_limit integer)
 RETURNS TABLE(out_vehicle_id bigint, out_class text, out_name text, out_time_stored timestamp without time zone, out_chassis_durability real, out_customization_id text, out_migrated boolean)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY
		SELECT vehicle_id, class, vehicle_name, time_stored AT TIME ZONE 'UTC', chassis_durability, customization_id, migrated 
		FROM actors 
		JOIN recovered_vehicles on id = vehicle_id 
		WHERE account_id = in_account_id
		AND (migrated = TRUE OR time_stored > NOW() - in_restore_time_limit * INTERVAL '1 second')
		AND EXISTS (SELECT 1 FROM actor_state WHERE actor_state.actor_id = id AND actor_state.state = 'VehicleRecovery')
		ORDER BY time_stored DESC;
END
$function$


-- load_static_encounter_name(in_map_name text, in_package_name text, in_actor_name text) -> TABLE(encounter_name text, waiting_for_reset boolean)
-- oid: 58462  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.load_static_encounter_name(in_map_name text, in_package_name text, in_actor_name text)
 RETURNS TABLE(encounter_name text, waiting_for_reset boolean)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY 
    SELECT t.encounter_name, t.waiting_for_reset
    FROM encounters_static as t
    WHERE t.map_name = in_map_name AND t.package_name = in_package_name AND t.actor_name = in_actor_name;
END; $function$


-- load_takeoverable_user_ids() -> SETOF dune.takeovercharacterdatacomposite
-- oid: 58463  kind: FUNCTION  category: takeover

CREATE OR REPLACE FUNCTION dune.load_takeoverable_user_ids()
 RETURNS SETOF dune.takeovercharacterdatacomposite
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
	SELECT acc.user, ps.character_name
	FROM accounts acc LEFT JOIN player_state ps ON acc.id=ps.account_id
	WHERE acc.takeoverable=true;
END; $function$


-- load_totem(in_id bigint) -> dune.totemsavedata
-- oid: 58464  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.load_totem(in_id bigint)
 RETURNS dune.totemsavedata
 LANGUAGE plpgsql
AS $function$
DECLARE
	result TotemSaveData;
BEGIN
	SELECT
		landclaim_vertical_level,
		last_backup_timestamp,
		landclaim_original_global_location,
		landclaim_original_global_yaw_rotation
	INTO result
	FROM totems
	WHERE id = in_id;
	return result;
END
$function$


-- load_travel_return_info(in_player_controller_id bigint) -> TABLE(map text, transform dune.transform)
-- oid: 58465  kind: FUNCTION  category: travel

CREATE OR REPLACE FUNCTION dune.load_travel_return_info(in_player_controller_id bigint)
 RETURNS TABLE(map text, transform dune.transform)
 LANGUAGE plpgsql
AS $function$
begin
	return query
	select travel_return_info.map, travel_return_info.transform
	from travel_return_info
	where player_controller_id = in_player_controller_id;
end
$function$


-- load_travel_to_player_info(in_player_controller_id bigint) -> TABLE(map text, transform dune.transform, partition_id bigint, dimension_index integer)
-- oid: 58466  kind: FUNCTION  category: travel

CREATE OR REPLACE FUNCTION dune.load_travel_to_player_info(in_player_controller_id bigint)
 RETURNS TABLE(map text, transform dune.transform, partition_id bigint, dimension_index integer)
 LANGUAGE plpgsql
AS $function$
begin
	return query
	select actors.map, actors.transform, actors.partition_id, actors.dimension_index
	from player_state
	join actors on player_state.player_pawn_id = actors.id
	where player_state.player_controller_id = in_player_controller_id
	and player_state.online_status = 'Online';
end
$function$


-- load_vehicle_modules(in_vehicle_id bigint) -> TABLE(module_id bigint, template_id text, stats jsonb)
-- oid: 58467  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.load_vehicle_modules(in_vehicle_id bigint)
 RETURNS TABLE(module_id bigint, template_id text, stats jsonb)
 LANGUAGE plpgsql
AS $function$
begin
    return query SELECT vm.id, vm.template_id, vm.stats FROM vehicle_modules vm WHERE vehicle_id = in_vehicle_id ORDER BY id;
end
$function$


-- load_world_partition(in_map_name text, in_server_id text, in_desired_dimension_index bigint, in_desired_partition_id bigint) -> TABLE(partition_id bigint, partition_definition jsonb, dimension_index integer, blocked boolean, label text)
-- oid: 58468  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.load_world_partition(in_map_name text, in_server_id text, in_desired_dimension_index bigint DEFAULT 0, in_desired_partition_id bigint DEFAULT NULL::bigint)
 RETURNS TABLE(partition_id bigint, partition_definition jsonb, dimension_index integer, blocked boolean, label text)
 LANGUAGE plpgsql
AS $function$
DECLARE
	tmp_partition RECORD;
BEGIN
	-- First check if the server already have a partition assigned
	SELECT INTO tmp_partition wp.partition_id, wp.partition_definition, wp.dimension_index, wp.blocked, wp.label
		FROM world_partition wp
		WHERE server_id = in_server_id AND wp.map = in_map_name AND wp.dimension_index = in_desired_dimension_index;
	IF tmp_partition.partition_id IS NOT NULL THEN
		RETURN QUERY SELECT tmp_partition.partition_id, tmp_partition.partition_definition, tmp_partition.dimension_index, tmp_partition.blocked, tmp_partition.label;
		RETURN;
	END IF;

	-- No partition assigned, so try to find an unassigned partition for this server
	SELECT INTO tmp_partition wp.partition_id, wp.partition_definition, wp.dimension_index, wp.blocked, wp.label
		FROM world_partition wp
		WHERE (server_id IS NULL OR server_id NOT IN (SELECT * FROM active_server_ids)) AND wp.map = in_map_name AND wp.dimension_index = in_desired_dimension_index
		ORDER BY (wp.partition_id = in_desired_partition_id) DESC, wp.partition_definition->'type', wp.partition_definition->'index', wp.partition_definition->'box'->'min_x', wp.partition_definition->'box'->'min_y'
		LIMIT 1
		FOR UPDATE SKIP LOCKED;
	IF tmp_partition.partition_id IS NULL THEN
		RETURN;
	ELSE
		-- Fake a server
		INSERT INTO farm_state(server_id, farm_id, outgoing_s2s_connections, incoming_s2s_connections, connected_players, igw_addr, igw_port, game_addr, game_port, map, revision)
			VALUES (in_server_id, '0', 0, 0, 0, '0.0.0.0', 0, '0.0.0.0', 0, '', 0) ON CONFLICT DO NOTHING;
		UPDATE world_partition SET server_id = in_server_id WHERE world_partition.partition_id = tmp_partition.partition_id;
		NOTIFY world_partition_update;
		RETURN QUERY SELECT tmp_partition.partition_id, tmp_partition.partition_definition, tmp_partition.dimension_index, tmp_partition.blocked, tmp_partition.label;
		RETURN;
	END IF;
END
$function$


-- log_cheating(in_fls_id text, in_cheat_type dune.cheat_type_enum, in_event_time timestamp with time zone) -> void
-- oid: 58469  kind: FUNCTION  category: anticheat

CREATE OR REPLACE FUNCTION dune.log_cheating(in_fls_id text, in_cheat_type dune.cheat_type_enum, in_event_time timestamp with time zone DEFAULT now())
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN

    -- Insert into suspicious_be
    INSERT INTO cheater_tracking (
        event_time,
        fls_id,
        cheat_type
    ) VALUES (
        in_event_time,
        in_fls_id,
        in_cheat_type
    );
END;
$function$


-- log_event_solaris(in_function_oid oid, in_message dune.logmessagetype, in_controller_id bigint, in_solaris_balance bigint, in_solaris_delta bigint) -> void
-- oid: 58470  kind: FUNCTION  category: currency

CREATE OR REPLACE FUNCTION dune.log_event_solaris(in_function_oid oid, in_message dune.logmessagetype, in_controller_id bigint, in_solaris_balance bigint, in_solaris_delta bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    partition_id BIGINT = 0;
	calling_function_name LogFunctionType;
	fls_id TEXT;
	fc_id BYTEA;
	char_name BYTEA;
BEGIN

    partition_id := coalesce(current_setting('dune.partition_id', true)::BIGINT, 0);

	-- map calling function name to LogFunctionType (each calling function must be added to LogFunctionType)
	SELECT proname::text::LogFunctionType
		INTO calling_function_name
		FROM pg_proc
		WHERE oid = in_function_oid;
	
	-- get the fls_id for the user performing the acction
	SELECT acc."user"
		INTO fls_id
		FROM accounts acc
		JOIN player_state ps on ps.account_id = acc.id
		WHERE ps.player_controller_id = in_controller_id
		LIMIT 1;

    INSERT INTO event_log (
        partition_id,
        category,
		function_name,
        message,
        event_time,
        meta
    ) VALUES (
        partition_id,
        'solaris',
		calling_function_name,
        in_message,
        now(),
        json_build_object('fls_id', fls_id, 'event', calling_function_name::text, 'solaris_balance', in_solaris_balance, 'solaris_delta', in_solaris_delta)
    );
END;
$function$


-- login_account(in_user_id text, in_funcom_id text, in_platform_id text, in_platform_name text, in_minimum_returning_player_time_seconds integer, in_character_name text, in_return_dimension_index integer, in_home_dimension_index integer) -> SETOF dune.playerdescription
-- oid: 58471  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.login_account(in_user_id text, in_funcom_id text, in_platform_id text, in_platform_name text, in_minimum_returning_player_time_seconds integer, in_character_name text, in_return_dimension_index integer, in_home_dimension_index integer)
 RETURNS SETOF dune.playerdescription
 LANGUAGE plpgsql
AS $function$
DECLARE
    user_account_id BigInt;
BEGIN
    PERFORM update_returning_player_status(in_user_id, in_minimum_returning_player_time_seconds);

    return query with
        acc as (
            INSERT INTO encrypted_accounts("id", "user", "platform_id", "platform_name", "encrypted_funcom_id")
                VALUES (default, in_user_id, in_platform_id, in_platform_name, encrypt_user_data(in_funcom_id))
                ON CONFLICT ("user") DO UPDATE SET
                    encrypted_funcom_id = excluded.encrypted_funcom_id,
                    platform_id = excluded.platform_id,
                    platform_name = excluded.platform_name
                RETURNING id, encrypted_accounts.user
        ),
        actor_ids as (
            -- TODO: unite this with accounts. One table to rule them all (until we want multiple chars per account)
            SELECT
                coalesce(player_controller_id, nextval('actors_id_seq')) as controller,
                coalesce(player_state_id, nextval('actors_id_seq')) as state,
                coalesce(player_pawn_id, nextval('actors_id_seq')) as pawn
            from acc left join player_state on player_state.account_id = acc.id
        ),
        actors_insert as (
            INSERT INTO actors("id", "owner_account_id")
            select unnest(array[controller, pawn, state]), acc.id from actor_ids, acc
            ON CONFLICT DO NOTHING
            returning id
        ),
        insert_actor_audit_log as (
            insert into actor_audit("id", "class")
                select
                    unnest(array[controller, pawn, state]) as id,
                    unnest(array['Controller', 'Pawn', 'State']) as clas
                from actor_ids
                on conflict do nothing
        ),
		demo as (
            UPDATE demo_users
                SET demo_state = CASE
                    WHEN demo_playtime_seconds IS NOT NULL THEN 'Demo'::DemoState
                    ELSE demo_state
                END
                WHERE fls_id = in_user_id
                RETURNING fls_id, demo_playtime_seconds, demo_state
		),
    	player_state_insert as (
            INSERT INTO encrypted_player_state(
            	"account_id", "encrypted_character_name", "online_status",
            	"player_controller_id", "player_pawn_id", "player_state_id",
				"return_dimension_index", "home_dimension_index", "last_login_time"
        	)
            select
                id, case
                    when in_character_name is not null then encrypt_user_data(in_character_name)
                    when encrypted_player_state.encrypted_character_name is null then encrypt_user_data('<TEMP>')
                    else encrypted_player_state.encrypted_character_name
                end,
                'Online', controller, pawn, state, in_return_dimension_index, in_home_dimension_index, now()
            from acc left join encrypted_player_state on acc.id = encrypted_player_state.account_id, actor_ids
            ON CONFLICT ("account_id")
            DO UPDATE SET
                online_status = 'Online',
                "return_dimension_index" = coalesce(in_return_dimension_index, encrypted_player_state.return_dimension_index),
                "home_dimension_index" = coalesce(in_home_dimension_index, encrypted_player_state.home_dimension_index),
                "last_login_time" = now()
            RETURNING
                account_id,
                "return_dimension_index",
                "home_dimension_index"
        ),
        inserted_count_dummy as (
            select count(*) from actors_insert
        ),
        player_actors as (
            select array_agg(full_actors.*) as actors
            from
                -- We need to refer 'returning' from inserts to ensure order of with statements
                inserted_count_dummy,
                actor_ids,
                load_full_actors(array[actor_ids.controller, actor_ids.state, actor_ids.pawn]) as full_actors
        ),
        pawn_info as (
            select id, (map, partition_id, dimension_index)::ServerInfo as server_info
            from actor_ids join actors on actors.id=actor_ids.pawn
        ),
        respawn_locations as (
            SELECT acc.id as account_id, get_respawn_locations(acc.id) as locations
            FROM acc
        ),
        this_player_tags as (
            select
                acc.id as account_id,
                array_agg(tag) as tags
            from acc join player_tags as tgs on tgs.account_id=acc.id
            group by acc.id
        ),
        keystones as (
            select player_id, array_agg(keystone_id) as purchased_keystones
            from purchased_specialization_keystones
            group by player_id
        ),
        tracks as (
            select player_id, array_agg(track_info) as progression_tracks
            from (
                select player_id, (track_type, xp_amount, level)::SpecializationTrackInfo as track_info
                from specialization_tracks
            )
            group by player_id
        ),
        journey_nodes as (
            SELECT acc.id as account_id, get_login_journey_nodes(acc.id) as journey_nodes_data
            FROM acc
        ),
        journey_nodes_cooldown as (
            SELECT acc.id as account_id, get_login_journey_nodes_cooldown(acc.id) as journey_nodes_cooldown_data
            FROM acc
        )
        select
            acc.id,
            player_actors.actors[1], player_actors.actors[2], player_actors.actors[3],
            coalesce(pawn_info.server_info, (null, null, null)::ServerInfo),
            (
                coalesce(respawn_locations.locations, array[]::RespawnLocation[]),
                player_state.pending_respawn_location_id
            )::RespawnInfo,
            coalesce(player_state.life_state, 'Alive'),
            coalesce(this_player_tags.tags, array[]::Text[]),
            player_state_insert.return_dimension_index,
            player_state.death_location,
            player_state_insert.home_dimension_index,
            demo.demo_state,
            demo.demo_playtime_seconds,
            (progression_tracks, purchased_keystones, refund_id)::SpecializationInfo,
            (
                coalesce(journey_nodes.journey_nodes_data, array[]::JourneyNodeInfo[]),
                coalesce(journey_nodes_cooldown.journey_nodes_cooldown_data, array[]::JourneyNodeCooldownInfo[]),
                coalesce(journey_tracked_cards.tracked_journey_card, ''),
                coalesce(journey_tracked_cards.tracked_landsraad_card, '')
            )::JourneyInfo,
            (player_state.last_returning_player_event_time AT TIME ZONE 'UTC')::TIMESTAMP,
            (player_state.last_returning_player_awarded_time AT TIME ZONE 'UTC')::TIMESTAMP
        from
            acc left join player_state on player_state.account_id = acc.id
                left join respawn_locations on respawn_locations.account_id = acc.id
                left join this_player_tags on this_player_tags.account_id = acc.id
                left join player_state_insert on player_state_insert.account_id = acc.id
                left join demo on demo.fls_id = acc.user
                left join journey_nodes on journey_nodes.account_id = acc.id
                left join journey_nodes_cooldown on journey_nodes_cooldown.account_id = acc.id
                left join journey_tracked_cards on journey_tracked_cards.player_id = player_state.player_controller_id
                left join keystones on keystones.player_id = player_state.player_controller_id
                left join tracks on tracks.player_id = player_state.player_controller_id
                left join specialization_refund_id on specialization_refund_id.player_id = player_state.player_controller_id,

            player_actors left join pawn_info on (player_actors.actors[3].id = pawn_info.id)
        limit 1;
END
$function$


-- mark_server_dead(in_server_id text) -> void
-- oid: 58473  kind: FUNCTION  category: server

CREATE OR REPLACE FUNCTION dune.mark_server_dead(in_server_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	UPDATE farm_state SET alive = false WHERE server_id = in_server_id;
END $function$


-- merge_inventory_items(in_item_id bigint, in_dst_inventory_id bigint, in_dst_index bigint, in_count bigint) -> bigint
-- oid: 58474  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.merge_inventory_items(in_item_id bigint, in_dst_inventory_id bigint, in_dst_index bigint, in_count bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	remaining_stack_size BIGINT;
	item_data items%ROWTYPE;
	dst_item_data items%ROWTYPE;
BEGIN
	SELECT INTO STRICT item_data * FROM items WHERE id = in_item_id;

	SELECT INTO dst_item_data * FROM items WHERE inventory_id = in_dst_inventory_id AND position_index = in_dst_index;

	IF dst_item_data.id IS NULL THEN
		RETURN NULL;
	END IF;

	remaining_stack_size := item_data.stack_size - in_count;

	IF remaining_stack_size < 0 THEN
		RETURN NULL;
	END IF;


	IF item_data.template_id != dst_item_data.template_id THEN
		RETURN NULL;
	END IF;
    
    -- log item tracking
    PERFORM _add_item_trace_log('merge_inventory_items', in_item_id, in_dst_inventory_id, NULL, in_dst_index);
    
	IF remaining_stack_size > 0 THEN
		UPDATE items SET stack_size = remaining_stack_size WHERE id = in_item_id;
	ELSE
		PERFORM delete_item(in_item_id);
	END IF;
	UPDATE items SET stack_size = dst_item_data.stack_size + in_count WHERE id = dst_item_data.id;

	RETURN dst_item_data.id;
END $function$


-- merge_or_move_inventory_item(in_item_id bigint, in_dst_inventory_id bigint, in_dst_index bigint, in_count bigint) -> bigint
-- oid: 58475  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.merge_or_move_inventory_item(in_item_id bigint, in_dst_inventory_id bigint, in_dst_index bigint, in_count bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	remaining_stack_size BIGINT;
	new_item_id BIGINT;
	item_data items%ROWTYPE;
	dst_item_data items%ROWTYPE;
BEGIN
	SELECT INTO new_item_id merge_inventory_items(in_item_id, in_dst_inventory_id, in_dst_index, in_count);

	IF new_item_id IS NULL THEN
		SELECT INTO new_item_id move_inventory_item(in_item_id, in_dst_inventory_id, in_dst_index, in_count);
	END IF;

	RETURN new_item_id;
END $function$


-- migrate_character(in_account_id bigint, home_dimension integer, max_solaris_allowed bigint) -> void
-- oid: 58476  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.migrate_character(in_account_id bigint, home_dimension integer, max_solaris_allowed bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    controller_id BIGINT;
    pawn_id BIGINT;
BEGIN
    SELECT player_controller_id, player_pawn_id into controller_id, pawn_id
    FROM player_state ps
    WHERE ps.account_id = in_account_id;

    UPDATE encrypted_player_state SET home_dimension_index = home_dimension WHERE account_id = in_account_id;

	UPDATE demo_users
	SET demo_state = 'DbMigratedToRetail'::DemoState, demo_playtime_seconds = NULL
    WHERE fls_id = (
		SELECT acc.user FROM accounts AS acc WHERE acc.id = in_account_id
		);

    PERFORM migrate_clamp_max_allow_solaris(pawn_id, max_solaris_allowed);
END
$function$


-- migrate_clamp_max_allow_solaris(in_pawn_id bigint, max_solaris_allowed bigint) -> void
-- oid: 58477  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.migrate_clamp_max_allow_solaris(in_pawn_id bigint, max_solaris_allowed bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    with all_solaris_items as (
        SELECT row_number() over (), i.id, i.stack_size FROM  inventories inv
        JOIN items i on i.inventory_id = inv.id
        WHERE inv.actor_id = in_pawn_id AND inv.inventory_type = 0 AND i.template_id = 'SolarisCoin' -- inventory_type 0 is backpack
    ), total_solaris as (
        SELECT sum(all_solaris_items.stack_size) as total
        from all_solaris_items
    ), items_to_delete AS (
        SELECT array_agg(id) AS item_ids
        FROM all_solaris_items
        WHERE row_number > 1
    ), deleted_items AS (
        SELECT delete_items(item_ids)
        FROM items_to_delete
    )
    UPDATE items
    SET stack_size = LEAST(max_solaris_allowed, total_solaris.total)
    FROM all_solaris_items, total_solaris, deleted_items
    WHERE items.id = all_solaris_items.id AND all_solaris_items.row_number = 1;
END
$function$


-- move_inventory_item(in_item_id bigint, in_dst_inventory_id bigint, in_dst_index bigint, in_count bigint) -> bigint
-- oid: 58478  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.move_inventory_item(in_item_id bigint, in_dst_inventory_id bigint, in_dst_index bigint, in_count bigint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	remaining_stack_size BIGINT;
	new_item_id BIGINT;
	item_data items%ROWTYPE;
BEGIN
	SELECT INTO STRICT item_data * FROM items WHERE id = in_item_id;

	remaining_stack_size := item_data.stack_size - in_count;

	IF remaining_stack_size < 0 THEN
		RETURN NULL;
	END IF;

    -- log item tracking
    PERFORM _add_item_trace_log('move_inventory_item', in_item_id, in_dst_inventory_id, NULL, in_dst_index);
        
	IF remaining_stack_size > 0 THEN
		item_data.stack_size := in_count;
		item_data.position_index := in_dst_index;
		item_data.inventory_id := in_dst_inventory_id;
		SELECT INTO item_data.id nextval('items_id_seq');
		INSERT INTO items VALUES(item_data.*) RETURNING id INTO new_item_id;
		UPDATE items SET stack_size = remaining_stack_size WHERE id = in_item_id;
	ELSE
		UPDATE items SET inventory_id = in_dst_inventory_id, position_index = in_dst_index WHERE id = in_item_id;
		new_item_id := in_item_id;
	END IF;

	RETURN new_item_id;
END $function$


-- overmap_delete_player_survival_data(in_player_id bigint) -> void
-- oid: 58479  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.overmap_delete_player_survival_data(in_player_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM overmap_players WHERE player_id = in_player_id;
END $function$


-- overmap_load_player_survival_data(in_player_id bigint) -> TABLE(out_vehicle_id bigint, out_has_polar_psu boolean, out_overmap_location dune.vector)
-- oid: 58480  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.overmap_load_player_survival_data(in_player_id bigint)
 RETURNS TABLE(out_vehicle_id bigint, out_has_polar_psu boolean, out_overmap_location dune.vector)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT vehicle_id, has_polar_psu, overmap_location FROM overmap_players WHERE player_id = in_player_id;
END $function$


-- overmap_save_player_survival_data(in_player_id bigint, in_vehicle_id bigint, in_has_polar_psu boolean, in_overmap_location dune.vector) -> void
-- oid: 58481  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.overmap_save_player_survival_data(in_player_id bigint, in_vehicle_id bigint, in_has_polar_psu boolean, in_overmap_location dune.vector)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	real_vehicle_id BigInt;
BEGIN
	real_vehicle_id := (select id from actors where id=in_vehicle_id);
	INSERT INTO
		overmap_players(player_id, vehicle_id, has_polar_psu, overmap_location) VALUES(in_player_id, real_vehicle_id, in_has_polar_psu, in_overmap_location)
	ON CONFLICT(player_id)
		DO UPDATE SET
			vehicle_id = real_vehicle_id, has_polar_psu = in_has_polar_psu, overmap_location = in_overmap_location
		WHERE
			overmap_players.player_id = in_player_id;
END $function$


-- ownership_handle_actor_delete(in_player_id bigint) -> void
-- oid: 58482  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.ownership_handle_actor_delete(in_player_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	owned_totem_ids BIGINT[];
	actors_with_permission BIGINT[];
BEGIN

	-- Get owner entity ids (totems) where in_player_id is the owner
	SELECT ARRAY_AGG(owner_entity_id) into owned_totem_ids
	FROM permission_actor_rank
	JOIN permission_actor ON actor_id = permission_actor_id
	JOIN placeables on placeables.id = permission_actor_id
	WHERE player_id = in_player_id AND rank = 1::smallint; -- 1:owner

	-- Get actors where in_player_id is the owner
	SELECT ARRAY_AGG(permission_actor_id) into actors_with_permission
	FROM permission_actor_rank WHERE player_id = in_player_id AND rank = 1::smallint; -- 1:owner

	-- Remove all permissions for those actors
	IF cardinality(actors_with_permission) > 0 THEN
		DELETE FROM permission_actor_rank WHERE permission_actor_id = ANY(actors_with_permission);
		DELETE FROM MARKERS	WHERE marker_hash_id = ANY(actors_with_permission);

		PERFORM pg_notify('permission_notify_channel', format('owner_delete#{"PlayerId" : %s}', in_player_id));
	END IF;
END
$function$


-- parties_get_exclusive_operation_lock() -> void
-- oid: 58483  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.parties_get_exclusive_operation_lock()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	PERFORM pg_advisory_xact_lock(9457135);  -- Parties in leet :/
END;
$function$


-- perform_notify_on_character_delete(in_user_id text) -> void
-- oid: 58484  kind: FUNCTION  category: player_persistence

CREATE OR REPLACE FUNCTION dune.perform_notify_on_character_delete(in_user_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	PERFORM pg_notify('player_info_notify_channel', format('character_deleted#%s', in_user_id));
END
$function$


-- permission_actor_create_or_update_base_marker(in_actor_id bigint, in_player_id bigint, in_rank smallint) -> void
-- oid: 58485  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.permission_actor_create_or_update_base_marker(in_actor_id bigint, in_player_id bigint, in_rank smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_marker_type TEXT := 'HomeBase';
	out_owner_id BIGINT;
	out_owner_name Text;
	out_dimension_index INTEGER;
	out_x REAL;
	out_y REAL;
	out_z REAL;
	out_totem_name Text;
	out_map_name Text;
	out_map_name_id SMALLINT;
	out_actor_type smallint;
BEGIN

	-- Get owner data
	SELECT player_id, character_name
	INTO out_owner_id, out_owner_name
	FROM permission_actor_rank
	JOIN player_state on player_controller_id = player_id
	WHERE rank = 1::smallint AND permission_actor_id = in_actor_id
	LIMIT 1;

	-- Get target and totem data
	SELECT dimension_index, (transform).location.x, (transform).location.y, (transform).location.z, actor_name, map, actor_type
	INTO out_dimension_index, out_x, out_y, out_z, out_totem_name, out_map_name, out_actor_type
	FROM permission_actor_rank
	JOIN permission_actor on actor_id = permission_actor_id
	JOIN player_state on player_controller_id = player_id
	JOIN actors ON permission_actor_id = actors.id
	WHERE permission_actor_id = in_actor_id	AND player_id = in_player_id
	LIMIT 1;

	SELECT map_name_id
	INTO out_map_name_id
	FROM map_names
	WHERE map_name = out_map_name;

	IF out_actor_type = 3 OR out_actor_type = 4 THEN -- Totem || TotemSmall
		INSERT INTO markers ("dimension_index", "marker_hash_id", "map_name_id", "marker", "area_id", "area_radius", "long_range", "payload")
		VALUES(out_dimension_index,
			in_actor_id,
			out_map_name_id,
			ROW(
				out_marker_type,
				out_x, out_y, out_z,
				'EMarkerPayloadType::Permissions'
			)::MARKER,
			0,
			0,
			FALSE,
			jsonb_build_object(
				'OwnerUID', out_owner_id,
				'OwnerName', out_owner_name,
				'TotemName', out_totem_name,
				'TotemId', in_actor_id
			)
		)
		ON CONFLICT (marker_hash_id, dimension_index, map_name_id)
		DO UPDATE SET
		marker = EXCLUDED.marker;

		INSERT INTO player_markers ("dimension_index", "player_id", "marker_hash_id", "map_name_id", "discovery_level", "discovery_method", "payload")
		VALUES(out_dimension_index,
			in_player_id,
			in_actor_id,
			out_map_name_id,
			3, -- EMarkerDiscoveryLevel::Discovered
			10, -- EMarkerDiscoveryMethod::Permissions
			'{}'::JSONB
		)
		ON CONFLICT (dimension_index, player_id, marker_hash_id, map_name_id)
		DO NOTHING;
	END IF;

END
$function$


-- permission_actor_destroy(in_actor_id bigint) -> void
-- oid: 58486  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.permission_actor_destroy(in_actor_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM permission_actor_rank WHERE permission_actor_id = in_actor_id;
	DELETE FROM permission_actor WHERE actor_id = in_actor_id;
	-- Destroy map markers related with this actor
	DELETE FROM markers WHERE marker_hash_id = in_actor_id;
	DELETE FROM player_markers WHERE marker_hash_id = in_actor_id;

    PERFORM pg_notify('permission_notify_channel', format('destroy#{"ActorId" : %s}', in_actor_id));
END
$function$


-- permission_actor_register(in_entry dune.actorpermissionentry, in_owner_rank dune.actorpermissionrankdata) -> void
-- oid: 58487  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.permission_actor_register(in_entry dune.actorpermissionentry, in_owner_rank dune.actorpermissionrankdata)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO
		permission_actor("actor_id", "actor_name", "actor_type", "access_level", "is_child")
		VALUES(in_entry.actor_id, in_entry.actor_name, in_entry.actor_type, in_entry.access_level, in_entry.is_child);

	IF NOT in_entry.is_child THEN
		INSERT INTO permission_actor_rank("permission_actor_id", "player_id", "rank")
			VALUES(in_entry.actor_id, in_owner_rank.player_id, in_owner_rank.rank);
	END IF;

    -- there is no pg_notify here as the use cases where it may be needed are very low and we do not want to pay that cost. If we find any scenario where we need it, it can be added
END
$function$


-- permission_actor_takeover(in_entry dune.actorpermissionentry, in_owner_rank dune.actorpermissionrankdata) -> void
-- oid: 58488  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.permission_actor_takeover(in_entry dune.actorpermissionentry, in_owner_rank dune.actorpermissionrankdata)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	found_actor_id BIGINT;
	found_guild_id BIGINT;
	found_previous_owner BIGINT;
BEGIN

	-- Check if the actor is already owned to avoid exploits
	SELECT player_id into found_previous_owner FROM permission_actor_rank WHERE permission_actor_id = in_entry.actor_id AND rank = 1::smallint;
	IF found_previous_owner IS NOT NULL THEN
		RAISE NOTICE 'Player % trying to claim ownership over actor % already owned by player %.',
			in_owner_rank.player_id, in_entry.actor_id, found_previous_owner;
		return;
	END IF;

	SELECT actor_id FROM permission_actor WHERE actor_id = in_entry.actor_id INTO found_actor_id;
	IF NOT FOUND THEN
		PERFORM permission_actor_register(in_entry, in_owner_rank);
		RETURN;
	END IF;

	SELECT guild_id FROM guild_members WHERE player_id = in_entry.actor_id INTO found_guild_id;
	IF NOT FOUND THEN
		found_guild_id := 0;
	END IF;

	DELETE FROM permission_actor_rank WHERE permission_actor_id = in_entry.actor_id;
	INSERT INTO permission_actor_rank("permission_actor_id", "player_id", "rank")
		VALUES(in_entry.actor_id, in_owner_rank.player_id, in_owner_rank.rank);

	PERFORM pg_notify('permission_notify_channel', format('takeover#{"ActorId" : %s , "PlayerId" : %s, "PlayerGuildId" : %s}', in_entry.actor_id, in_owner_rank.player_id, found_guild_id));
END
$function$


-- permission_actor_update_marker_location(in_actor_id bigint, in_location_x real, in_location_y real, in_location_z real) -> void
-- oid: 58489  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.permission_actor_update_marker_location(in_actor_id bigint, in_location_x real, in_location_y real, in_location_z real)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	UPDATE markers SET marker =
	(
		(marker).marker_type,
		in_location_x,
		in_location_y,
		in_location_z,
		(marker).payload_type
	)
	WHERE marker_hash_id = in_actor_id;
END
$function$


-- permission_remove_player_rank(in_actor_id bigint, in_player_id bigint) -> void
-- oid: 58490  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.permission_remove_player_rank(in_actor_id bigint, in_player_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM permission_actor_rank WHERE permission_actor_id = in_actor_id AND player_id = in_player_id;

	-- Remove from player_markers using player id and, actor id as hash id
	DELETE FROM player_markers WHERE player_id = in_player_id AND marker_hash_id = in_actor_id;

	PERFORM pg_notify('permission_notify_channel', format('remove_rank#{"ActorId" : %s , "PlayerId" : %s}', in_actor_id, in_player_id));
END
$function$


-- permission_set_access_level(in_actor_id bigint, in_access_level smallint) -> void
-- oid: 58491  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.permission_set_access_level(in_actor_id bigint, in_access_level smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	UPDATE permission_actor SET access_level = in_access_level WHERE permission_actor.actor_id = in_actor_id;

     PERFORM pg_notify('permission_notify_channel', format('set_access_level#{"ActorId" : %s , "AccessLevel" : %s}', in_actor_id, in_access_level));
END
$function$


-- permission_set_name(in_actor_id bigint, in_name text) -> void
-- oid: 58492  kind: FUNCTION  category: permission

CREATE OR REPLACE FUNCTION dune.permission_set_name(in_actor_id bigint, in_name text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	UPDATE permission_actor SET actor_name = in_name WHERE permission_actor.actor_id = in_actor_id;

	UPDATE markers SET marker =
	(
		(marker).marker_type,
		(marker).x,
		(marker).y,
		(marker).z,
		(marker).payload_type
	),
	payload = jsonb_set(payload, '{TotemName}', to_jsonb(in_name) , false)
	WHERE marker_hash_id = in_actor_id;

    PERFORM pg_notify('permission_notify_channel', format('set_name#{"ActorId" : %s , "Name" : "%s"}', in_actor_id, in_name));
END
$function$


-- permission_set_player_rank(in_actor_id bigint, in_player_id bigint, in_rank smallint, in_map_id text) -> void
-- oid: 58493  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.permission_set_player_rank(in_actor_id bigint, in_player_id bigint, in_rank smallint, in_map_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	found_actor_id BIGINT;
	found_guild_id BIGINT;
BEGIN
	SELECT permission_actor_id FROM permission_actor_rank WHERE permission_actor_id = in_actor_id AND player_id = in_player_id INTO found_actor_id;
	IF NOT FOUND THEN
    	INSERT INTO permission_actor_rank("permission_actor_id", "player_id", "rank") VALUES(in_actor_id, in_player_id, in_rank);
    ELSE
	    UPDATE permission_actor_rank SET rank = in_rank WHERE permission_actor_rank.permission_actor_id = in_actor_id AND player_id = in_player_id;
	END IF;

	SELECT guild_id FROM guild_members WHERE player_id = in_actor_id INTO found_guild_id;
	IF NOT FOUND THEN
		found_guild_id := 0;
	END IF;

	PERFORM permission_actor_create_or_update_base_marker(in_actor_id, in_player_id, in_rank);

    PERFORM pg_notify('permission_notify_channel', format('set_rank#{"ActorId" : %s , "PlayerId" : %s, "PlayerGuildId" : %s, "Rank" : %s, "Map" : %s}', in_actor_id, in_player_id, found_guild_id, in_rank, in_map_id));
END
$function$


-- player_purchased_item_from_vendor(in_vendor_id text, in_player_id bigint, in_template_id text, in_amount_bought integer) -> void
-- oid: 58494  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.player_purchased_item_from_vendor(in_vendor_id text, in_player_id bigint, in_template_id text, in_amount_bought integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	-- Add/update stock
	IF NOT EXISTS
		( SELECT * FROM vendor_stock_state
		WHERE vendor_id = in_vendor_id and player_id = in_player_id AND template_id = in_template_id)
	THEN
		INSERT INTO vendor_stock_state(vendor_id, player_id, template_id, amount_bought) VALUES(in_vendor_id, in_player_id, in_template_id, in_amount_bought);
	ELSE
		UPDATE vendor_stock_state SET amount_bought = amount_bought + in_amount_bought
		WHERE vendor_id = in_vendor_id and player_id = in_player_id AND template_id = in_template_id;
	END IF;
END
$function$


-- player_state_update(in_data dune.playerstateupdatedata[]) -> void
-- oid: 58495  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.player_state_update(in_data dune.playerstateupdatedata[])
 RETURNS void
 LANGUAGE sql
AS $function$
    -- online -> offline
    with
        update_data as (select * from unnest(in_data))
        update encrypted_player_state as ps
            set online_status = update_data.online_status
            from update_data
            where ps.player_controller_id = update_data.player_controller_id
                and ps.server_id = update_data.current_server_id -- make sure we don't update if the player is already online somewhere else
                and update_data.online_status != 'Online'
                and ps.online_status != update_data.online_status; -- avoid unnecessary data changes

    -- offline -> online
	with
		update_data as (select * from unnest(in_data))
        update encrypted_player_state as ps
            set online_status = update_data.online_status,
                server_id = update_data.current_server_id
            from update_data
            where ps.player_controller_id = update_data.player_controller_id
              and update_data.online_status = 'Online'
              and (ps.server_id is null or ps.server_id != update_data.current_server_id or ps.online_status != update_data.online_status); -- avoid unnecessary data changes

	with
		update_data as (select * from unnest(in_data))
        update encrypted_player_state as ps
            set reconnect_grace_period_end = update_data.reconnect_grace_period_end
            from update_data
            where ps.player_controller_id = update_data.player_controller_id
                and not update_data.reconnect_grace_period_end is null;

	with
		update_data as (select * from unnest(in_data))
        update encrypted_player_state as ps
            set
                last_avatar_activity = (update_data.on_disconnect).last_online_time AT TIME ZONE 'UTC',
                previous_server_partition_id = (update_data.on_disconnect).previous_server_partition_id
            from update_data
            where ps.player_controller_id = update_data.player_controller_id
                and not update_data.on_disconnect is null;
$function$


-- pledge_guild_allegiance(in_guild_id bigint, in_guild_leader_player_id bigint, in_neutral_faction_id smallint) -> void
-- oid: 58496  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.pledge_guild_allegiance(in_guild_id bigint, in_guild_leader_player_id bigint, in_neutral_faction_id smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	guilds_changed SMALLINT := 0;
	guild_data_record record;
	guild_leader_record record;
	guild_leader_faction_id SMALLINT;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	SELECT * INTO guild_leader_record FROM guild_members
	LEFT JOIN player_faction ON actor_id = in_guild_leader_player_id
	WHERE player_id = in_guild_leader_player_id AND is_player_guild_admin(in_guild_leader_player_id, in_guild_id);

	IF guild_leader_record IS NULL THEN
		RAISE EXCEPTION 'Trying to change a faction for a player: % without a guild', in_guild_leader_player_id;
	END IF;

	SELECT * INTO guild_data_record FROM guilds WHERE guild_id = in_guild_id;
	IF guild_data_record IS NULL THEN
		RAISE EXCEPTION 'Trying to change a faction in non existing guild: %', in_guild_id;
	END IF;

	IF guild_leader_record.faction_id IS NULL THEN
		guild_leader_faction_id := in_neutral_faction_id;
    ELSE
		guild_leader_faction_id := guild_leader_record.faction_id;
	END IF;

	if guild_leader_faction_id = in_neutral_faction_id THEN
		RAISE EXCEPTION 'Guild leader has neutral faction, cannot change faction to neutral';
	ELSEIF guild_data_record.guild_faction = guild_leader_faction_id THEN
		RAISE EXCEPTION 'Guild already has the same allegiance: % as the guild leader %', in_guild_id, guild_data_record.guild_faction;
	END IF;

	UPDATE guilds SET guild_faction = guild_leader_faction_id WHERE guilds.guild_id = in_guild_id;

	PERFORM pg_notify('guild_notify_channel', format('pledge_guild_allegiance#{"GuildId" : %s , "OldGuildFactionDbId" : %s, "NewGuildFactionDbId" : %s}', in_guild_id, guild_data_record.guild_faction, guild_leader_faction_id));
	PERFORM remove_guild_members(ARRAY(
		SELECT player_id FROM guild_members
		JOIN player_faction ON guild_members.player_id = player_faction.actor_id
		WHERE guild_leader_faction_id != player_faction.faction_id AND player_faction.faction_id != in_neutral_faction_id AND guild_members.guild_id = in_guild_id),
		in_guild_id,
		2::smallint
	);
END
$function$


-- produce_spicefield_manifest(in_map_name text, in_dimension_index integer) -> TABLE(server text, type_id integer, inactive_fields integer, requested_fields integer)
-- oid: 58497  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.produce_spicefield_manifest(in_map_name text, in_dimension_index integer)
 RETURNS TABLE(server text, type_id integer, inactive_fields integer, requested_fields integer)
 LANGUAGE plpgsql
AS $function$
begin
	return query
	select sa.server_id, sa.spicefield_type_id, sa.inactive_fields_of_type, sa.requested_spawned_of_type
	from spicefield_server_availability sa join spicefield_types st
	on sa.spicefield_type_id = st.spicefield_type_id
	where st.map_name = in_map_name and st.dimension_index = in_dimension_index
	order by server_id;
end $function$


-- promote_guild_member(in_guild_id bigint, in_player_id bigint, in_new_role smallint) -> void
-- oid: 58498  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.promote_guild_member(in_guild_id bigint, in_player_id bigint, in_new_role smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	-- check if new admin is actualy in guild
	IF NOT EXISTS(SELECT FROM guild_members WHERE player_id = in_player_id AND guild_id = in_guild_id) THEN
    	RAISE EXCEPTION 'Trying to promte player not in guild %.', in_player_id;
	END IF;

	if in_new_role = 100 THEN
		-- set admin to member
		UPDATE guild_members SET role_id = 50 WHERE guild_id = in_guild_id AND role_id = 100;
	END IF;

	-- set new player to new role
	UPDATE guild_members SET role_id = in_new_role WHERE player_id = in_player_id AND guild_id = in_guild_id;

	PERFORM pg_notify('guild_notify_channel', format('promote_player#{"PlayerId" : %s , "GuildId" : %s, "NewRole" : %s}', in_player_id, in_guild_id, in_new_role));
END
$function$


-- promote_new_party_leader(in_party_id bigint) -> void
-- oid: 58499  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.promote_new_party_leader(in_party_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_current_leader BIGINT;
	out_new_leader BIGINT;
BEGIN
	PERFORM parties_get_exclusive_operation_lock();

	-- get current leader
	SELECT party_leader_id FROM parties where party_id = in_party_id into out_current_leader;
	IF out_current_leader IS NULL THEN
		RAISE EXCEPTION 'Promoting a player to a non existing party %.', in_party_id;
	END IF;

	-- get first member in the party that is online and is not the party leader
	SELECT party_members.player_id INTO out_new_leader FROM party_members
	JOIN player_state ON player_state.player_controller_id = party_members.player_id
	WHERE party_members.party_id = in_party_id
	AND party_members.player_id <> out_current_leader
	AND player_state.online_status = 'Online';

	IF out_new_leader IS NOT NULL THEN
		-- promote
		UPDATE parties SET party_leader_id = out_new_leader WHERE party_id = in_party_id;
		PERFORM pg_notify('party_notify_channel', format('promote_party_leader#{"PartyId" : %s, "PlayerId" : %s}', in_party_id, out_new_leader));
	END IF;

END
$function$


-- promote_party_leader_to(in_party_id bigint, in_player_id bigint) -> void
-- oid: 58500  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.promote_party_leader_to(in_party_id bigint, in_player_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_current_leader BIGINT;
	out_new_leader BIGINT;
BEGIN
	PERFORM parties_get_exclusive_operation_lock();

	-- get current leader
	SELECT party_leader_id FROM parties where party_id = in_party_id into out_current_leader;
	IF out_current_leader IS NULL THEN
		RAISE EXCEPTION 'Promoting a player to a non existing party %.', in_party_id;
	END IF;

	-- get new member if it is online
	SELECT party_members.player_id INTO out_new_leader FROM party_members
	JOIN player_state ON player_state.player_controller_id = party_members.player_id
	WHERE party_members.party_id = in_party_id
	AND party_members.player_id = in_player_id
	AND player_state.online_status = 'Online';

	IF out_new_leader IS NOT NULL THEN
		-- promote
		UPDATE parties SET party_leader_id = out_new_leader WHERE party_id = in_party_id;
		PERFORM pg_notify('party_notify_channel', format('promote_party_leader#{"PartyId" : %s, "PlayerId" : %s}', in_party_id, out_new_leader));
	ELSE
		RAISE EXCEPTION 'Promopromoting player %, which is offline or does not belong to the party.', in_party_id;
		-- pg_notify for feedback?
	END IF;

END
$function$


-- purchase_specialization_keystone(in_player_id bigint, in_keystone text) -> boolean
-- oid: 58501  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.purchase_specialization_keystone(in_player_id bigint, in_keystone text)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
	found_id SMALLINT;
	inserted_id SMALLINT;
BEGIN
	SELECT id FROM specialization_keystones_map INTO found_id WHERE name = in_keystone;
	IF found_id IS NULL THEN
		RETURN FALSE;
	END IF;
	
	INSERT INTO purchased_specialization_keystones (player_id, keystone_id) VALUES (in_player_id, found_id)
	ON CONFLICT DO NOTHING
	RETURNING keystone_id INTO inserted_id;
	
	IF inserted_id IS NULL THEN
		RETURN FALSE;
	END IF;
	
	RETURN TRUE;
END $function$


-- record_deactivated_spice_field(in_server_id text, in_spicefield_type_id integer) -> void
-- oid: 58502  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.record_deactivated_spice_field(in_server_id text, in_spicefield_type_id integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE spicefield_server_availability
	SET inactive_fields_of_type = inactive_fields_of_type + 1
	WHERE server_id = in_server_id AND spicefield_type_id = in_spicefield_type_id;

	UPDATE spicefield_types
	SET current_globally_active = current_globally_active - 1
	WHERE spicefield_type_id = in_spicefield_type_id;
END; $function$


-- record_dungeon_completion(in_dungeon_id text, in_difficulty integer, in_duration_ms integer, players_ids bigint[]) -> void
-- oid: 58503  kind: FUNCTION  category: dungeon

CREATE OR REPLACE FUNCTION dune.record_dungeon_completion(in_dungeon_id text, in_difficulty integer, in_duration_ms integer, players_ids bigint[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	new_completion_id BIGINT;
begin
	INSERT INTO dungeon_completion VALUES (DEFAULT, in_dungeon_id, in_difficulty, in_duration_ms, array_length(players_ids, 1)) 
		RETURNING completion_id INTO new_completion_id; 
    INSERT INTO dungeon_completion_players SELECT t.player_id, new_completion_id FROM UNNEST(players_ids) AS t(player_id);
end
$function$


-- record_logoff_persistence_end_time(in_player_pawn_id bigint, in_logoff_persistence_end_time timestamp without time zone) -> void
-- oid: 58504  kind: FUNCTION  category: player_persistence

CREATE OR REPLACE FUNCTION dune.record_logoff_persistence_end_time(in_player_pawn_id bigint, in_logoff_persistence_end_time timestamp without time zone)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE encrypted_player_state
    SET logoff_persistence_end_time = in_logoff_persistence_end_time
    WHERE player_pawn_id = in_player_pawn_id;
END
$function$


-- record_static_shifting_sand(in_id text, in_alpha double precision, in_x double precision, in_y double precision, in_last_modified_time bigint) -> void
-- oid: 58505  kind: FUNCTION  category: shifting_sand

CREATE OR REPLACE FUNCTION dune.record_static_shifting_sand(in_id text, in_alpha double precision, in_x double precision, in_y double precision, in_last_modified_time bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO shiftingsands_data(id, alpha, x, y, last_modified_time) Values(in_id, in_alpha, in_x, in_y, to_timestamp(in_last_modified_time))
	ON CONFLICT(id) DO UPDATE SET alpha = $2, last_modified_time = to_timestamp(in_last_modified_time);
END $function$


-- record_unreadied_spice_fields(in_server_id text, in_spicefield_type_id integer, in_num_unreadied integer) -> void
-- oid: 58506  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.record_unreadied_spice_fields(in_server_id text, in_spicefield_type_id integer, in_num_unreadied integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE spicefield_types
	SET current_globally_primed = current_globally_primed - in_num_unreadied
	WHERE spicefield_type_id = in_spicefield_type_id;

	UPDATE spicefield_server_availability
	SET inactive_fields_of_type = inactive_fields_of_type + in_num_unreadied
	WHERE server_id = in_server_id AND spicefield_type_id = in_spicefield_type_id;
END; $function$


-- register_lore_pickup(in_lore_pickup_ids text[]) -> SETOF smallint
-- oid: 58507  kind: FUNCTION  category: server

CREATE OR REPLACE FUNCTION dune.register_lore_pickup(in_lore_pickup_ids text[])
 RETURNS SETOF smallint
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN query WITH
		input AS (
			SELECT UNNEST (in_lore_pickup_ids) as lore_pickup_id
		),
		existing AS (
			SELECT incremental_id, lore_pickup_id FROM lore_pickups WHERE lore_pickup_id = ANY(SELECT lore_pickup_id FROM input)
		),
		inserted AS (
			INSERT INTO lore_pickups("lore_pickup_id") SELECT lore_pickup_id FROM input WHERE NOT lore_pickup_id = ANY(SELECT lore_pickup_id FROM existing) returning incremental_id, lore_pickup_id
		),
		combined AS (
            SELECT incremental_id, lore_pickup_id FROM existing UNION ALL SELECT incremental_id, lore_pickup_id FROM inserted
        )
    SELECT incremental_id FROM combined ORDER BY lore_pickup_id;
END
$function$


-- register_new_factions(factions text[]) -> TABLE(faction_id smallint, faction_name text)
-- oid: 58508  kind: FUNCTION  category: faction

CREATE OR REPLACE FUNCTION dune.register_new_factions(factions text[])
 RETURNS TABLE(faction_id smallint, faction_name text)
 LANGUAGE plpgsql
AS $function$
DECLARE
	found_role_id SMALLINT;
BEGIN
     -- Lock the factions table to prevent concurrent modifications. This is only done once on server start up.
    LOCK TABLE factions IN SHARE ROW EXCLUSIVE MODE;
    WITH new_factions AS (
        SELECT f FROM UNNEST(factions) f LEFT JOIN factions ON f = factions.name WHERE id IS NULL
    )
	INSERT INTO factions (name) SELECT * FROM new_factions ON CONFLICT DO NOTHING;
	RETURN QUERY SELECT * from factions;
END
$function$


-- register_new_tutorials(tutorials text[]) -> TABLE(tutorial_id smallint, tutorial_name text)
-- oid: 58509  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.register_new_tutorials(tutorials text[])
 RETURNS TABLE(tutorial_id smallint, tutorial_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
    -- Lock the tutorials table to prevent concurrent modifications. This is only done once on server start up.
    LOCK TABLE tutorials IN SHARE ROW EXCLUSIVE MODE;

    WITH new_tutorials AS (
        SELECT q FROM unnest(tutorials) q LEFT JOIN tutorials ON q = tutorials.name WHERE id is NULL
    )
    INSERT INTO tutorials (name) SELECT * FROM new_tutorials;

    RETURN QUERY SELECT * from tutorials;
END
$function$


-- register_per_player_lore_pickup(in_lore_pickup_ids text[], in_use_temporary boolean) -> SETOF smallint
-- oid: 58510  kind: FUNCTION  category: server

CREATE OR REPLACE FUNCTION dune.register_per_player_lore_pickup(in_lore_pickup_ids text[], in_use_temporary boolean)
 RETURNS SETOF smallint
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF in_use_temporary THEN
		RETURN query select * from register_temporary_lore_pickup(in_lore_pickup_ids);
	ELSE
		RETURN query select * from register_lore_pickup(in_lore_pickup_ids);
	END IF;
END
$function$


-- register_spawned_actor(in_spawner_id bigint, in_actor_id bigint) -> void
-- oid: 58511  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.register_spawned_actor(in_spawner_id bigint, in_actor_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO actor_spawner_actors(spawner_id, actor_id) VALUES(in_spawner_id, in_actor_id);
END $function$


-- register_spice_field_server_resources(in_server_id text, in_spicefield_type_ids integer[], in_inactive_fields_of_types integer[]) -> void
-- oid: 58512  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.register_spice_field_server_resources(in_server_id text, in_spicefield_type_ids integer[], in_inactive_fields_of_types integer[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO spicefield_server_availability(server_id, spicefield_type_id, inactive_fields_of_type)
	SELECT in_server_id, unnest(in_spicefield_type_ids), unnest(in_inactive_fields_of_types)
	ON CONFLICT(server_id, spicefield_type_id)
	DO UPDATE SET server_id = excluded.server_id, spicefield_type_id = excluded.spicefield_type_id, inactive_fields_of_type = excluded.inactive_fields_of_type;
END; $function$


-- register_temporary_lore_pickup(in_lore_pickup_ids text[]) -> SETOF smallint
-- oid: 58513  kind: FUNCTION  category: server

CREATE OR REPLACE FUNCTION dune.register_temporary_lore_pickup(in_lore_pickup_ids text[])
 RETURNS SETOF smallint
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN query WITH
		input AS (
			SELECT UNNEST (in_lore_pickup_ids) as lore_pickup_id
		),
		existing AS (
			SELECT incremental_id, lore_pickup_id FROM lore_pickups_temporary WHERE lore_pickup_id = ANY(SELECT lore_pickup_id FROM input)
		),
		inserted AS (
			INSERT INTO lore_pickups_temporary("lore_pickup_id") SELECT lore_pickup_id FROM input WHERE NOT lore_pickup_id = ANY(SELECT lore_pickup_id FROM existing) returning incremental_id, lore_pickup_id
		),
		combined AS (
            SELECT incremental_id, lore_pickup_id FROM existing UNION ALL SELECT incremental_id, lore_pickup_id FROM inserted
        )
    SELECT incremental_id FROM combined ORDER BY lore_pickup_id;
END
$function$


-- reject_guild_invite(in_invite_id bigint) -> void
-- oid: 58514  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.reject_guild_invite(in_invite_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	player_id BIGINT := 0;
	guild_id BIGINT := 0;
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	-- check if invite exists
	SELECT guild_invites.player_id, guild_invites.guild_id FROM guild_invites WHERE invite_id = in_invite_id INTO player_id, guild_id;
	IF NOT FOUND THEN
    	RAISE EXCEPTION 'Trying to remove non exiting invite %.', in_invite_id;
	END IF;

	DELETE FROM guild_invites WHERE invite_id = in_invite_id;

	PERFORM pg_notify('guild_notify_channel', format('reject_invite#{"PlayerId" : %s , "GuildId" : %s}', player_id, guild_id));
END
$function$


-- remove_aborted_authority_transfer_actors(in_partition_id bigint) -> SETOF dune.actorspawninfo
-- oid: 58515  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.remove_aborted_authority_transfer_actors(in_partition_id bigint)
 RETURNS SETOF dune.actorspawninfo
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
        WITH removed_actors AS (
            DELETE FROM actor_state WHERE actor_state.state = 'AbortedAuthorityTransfer'
            RETURNING actor_state.actor_id
        )
        SELECT a.id, a.class AS class_name, a.transform, a.partition_id, a.dimension_index
        FROM actors AS a
        INNER JOIN removed_actors ON a.id = removed_actors.actor_id
        WHERE a.partition_id = in_partition_id;
END
$function$


-- remove_character_transfer_state(in_fls_id text) -> void
-- oid: 58516  kind: FUNCTION  category: transfer

CREATE OR REPLACE FUNCTION dune.remove_character_transfer_state(in_fls_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM character_transfer_imports WHERE fls_id = in_fls_id;
END;
$function$


-- remove_communinet_player_channel(in_account_id bigint, in_channel_name text) -> void
-- oid: 58517  kind: FUNCTION  category: communinet

CREATE OR REPLACE FUNCTION dune.remove_communinet_player_channel(in_account_id bigint, in_channel_name text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM communinet_player_channels WHERE account_id = in_account_id AND channel_name = in_channel_name;
END
$function$


-- remove_guild_members(in_player_ids bigint[], in_guild_id bigint, in_remove_reason smallint) -> void
-- oid: 58518  kind: FUNCTION  category: guild

CREATE OR REPLACE FUNCTION dune.remove_guild_members(in_player_ids bigint[], in_guild_id bigint, in_remove_reason smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_guild_name TEXT;
	players_removed BIGINT[];
BEGIN
	PERFORM guilds_get_exclusive_operation_lock();

	-- check if guild exists
	SELECT guild_name INTO out_guild_name FROM guilds WHERE guild_id = in_guild_id;
	IF NOT FOUND THEN
    	RAISE EXCEPTION 'Trying to disband non existing guild %.', in_guild_id;
	END IF;

	WITH removed_members AS (
		DELETE FROM guild_members
		WHERE player_id = ANY(in_player_ids) AND NOT is_player_guild_admin(player_id, in_guild_id)
		RETURNING *
	) SELECT array_agg(player_id) from removed_members INTO players_removed;

	PERFORM pg_notify('guild_notify_channel', format('remove_players#{"PlayerIds" : [%s] , "GuildId" : %s, "GuildName" : "%s", "GuildRemoveReason" : %s}', ARRAY_TO_STRING(players_removed, ','), in_guild_id, out_guild_name, in_remove_reason));
END
$function$


-- remove_items(items_to_remove text[]) -> void
-- oid: 58519  kind: FUNCTION  category: items_purge

CREATE OR REPLACE FUNCTION dune.remove_items(items_to_remove text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
    PERFORM delete_items(
        (
            SELECT array_agg(id)
            FROM items
            WHERE template_id = ANY (items_to_remove)
        )
    );
end;
$function$


-- remove_items_and_recipes(items_to_remove text[], recipes_to_remove text[]) -> void
-- oid: 58520  kind: FUNCTION  category: items_purge

CREATE OR REPLACE FUNCTION dune.remove_items_and_recipes(items_to_remove text[], recipes_to_remove text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
declare
    items_to_remove_filtered text[];
    recipes_to_remove_filtered text[];
begin
    lock table removed_items, removed_recipes, actors, fgl_entities, items in exclusive mode;
    
    -- get the items in items_to_remove that are not in removed_items
    items_to_remove_filtered := get_items_to_remove(items_to_remove);
    
    -- remove items from fgl entities
    perform remove_items_or_recipes_from_fgl_entities(items_to_remove_filtered);
    
    -- get the recipes in recipes_to_remove that are not in removed_recipes
    recipes_to_remove_filtered := get_recipes_to_remove(recipes_to_remove);
    
    -- removes the requests that contain items or recipes from fgl_entities.
    perform remove_items_or_recipes_from_fgl_entities(recipes_to_remove_filtered);
    
    -- delete the items
    perform remove_items(items_to_remove_filtered);

    -- remove the recipes from actors known item recipes
    perform remove_recipes_from_actor_properties(recipes_to_remove_filtered);
    
    -- insert removed items and recipes in their respective tables
    perform update_removed_items_and_recipes(items_to_remove_filtered, recipes_to_remove_filtered);
end
$function$


-- remove_items_or_recipes_from_fgl_entities(item_or_recipes text[]) -> void
-- oid: 58521  kind: FUNCTION  category: items_purge

CREATE OR REPLACE FUNCTION dune.remove_items_or_recipes_from_fgl_entities(item_or_recipes text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$ 
declare
   item_or_recipe text;
   ent_id bigint;
   components_data jsonb;
   components_data_updated jsonb;
   request jsonb;
   item_or_recipe_found boolean;
   updated_requests jsonb;
   ingredient_allocation jsonb;
   item_alloc_node jsonb;
   allocated_item jsonb;
   allocated_ingredient_info record;
begin
   drop table if exists allocated_ingredients_info_temp;
   create temp table allocated_ingredients_info_temp (item_unique_id bigint, amount bigint);
   if array_length(item_or_recipes, 1) is null then
      return;
   end if;
   foreach item_or_recipe in array(item_or_recipes)
   loop
      for ent_id, components_data in select entity_id, components
                                    from fgl_entities
                                    where components->'FItemCraftingComponent' is not null
      loop
         components_data_updated := components_data;
         if jsonb_array_length(components_data_updated->'FItemCraftingComponent'->1->'RequestsQueue') < 1 then
            return;
         end if;
         loop
            item_or_recipe_found := false;
            for request in select value::jsonb
                            from jsonb_array_elements(components_data_updated->'FItemCraftingComponent'->1->'RequestsQueue')
            loop
               if (request->'RecipeId'->>'Name' = item_or_recipe or
                  exists (select 1 from jsonb_array_elements(request->'ResultItems') where value->'ItemTemplateId'->>'Name' = item_or_recipe) or
                  exists (select 1 from jsonb_array_elements(request->'IngredientAllocations') where value->'ItemAllocNodes'->0->'ItemTemplateId'->>'Name' = item_or_recipe)) then
                  insert into allocated_ingredients_info_temp (item_unique_id, amount)
                    select (item->>'ItemUniqueId')::BigInt, (item->>'ItemAmount')::BigInt from (
                        select jsonb_array_elements(node->'AllocatedItems') as item, node as outer_node from (
                            select jsonb_array_elements(allocation->'ItemAllocNodes') as node from (
                                select jsonb_array_elements(request->'IngredientAllocations') as allocation
                            ) allocations
                        ) nodes
                    ) items;
                  updated_requests := (select jsonb_agg(value) from jsonb_array_elements(components_data_updated->'FItemCraftingComponent'->1->'RequestsQueue') where value::jsonb != request);
                  if updated_requests is null then
                     components_data_updated := jsonb_set(components_data_updated, '{FItemCraftingComponent,1,RequestsQueue}', '[]'::jsonb);
                     components_data_updated := jsonb_set(components_data_updated, '{FItemCraftingComponent,1,State}', '"Idle"'::jsonb);
                     components_data_updated := jsonb_set(components_data_updated, '{FItemCraftingComponent,1,TotalTimeToCraftInSec}', '0'::jsonb);
                     components_data_updated := jsonb_set(components_data_updated, '{FItemCraftingComponent,1,PreviouslyCompletedTimeToCraftInSec}', '0'::jsonb);
                  else
                     components_data_updated := jsonb_set(components_data_updated, '{FItemCraftingComponent,1,RequestsQueue}', updated_requests);
                  end if;
                  item_or_recipe_found := true;
                  exit;
               end if;
            end loop;
            exit when not item_or_recipe_found;
         end loop;
         update fgl_entities
         set components = components_data_updated
         where entity_id = ent_id;
      end loop;
   end loop;
   
   for allocated_ingredient_info in select * from allocated_ingredients_info_temp
   loop
      perform delete_inventory_item(allocated_ingredient_info.item_unique_id, allocated_ingredient_info.amount);
   end loop;
   
   drop table allocated_ingredients_info_temp;
end;
$function$


-- remove_members_offline_for(in_interval_seconds integer) -> void
-- oid: 58522  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.remove_members_offline_for(in_interval_seconds integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	PERFORM parties_get_exclusive_operation_lock();

	PERFORM remove_party_member(party_members.player_id, 0::SMALLINT) FROM party_members
	JOIN player_state ON player_state.player_controller_id = party_members.player_id
	WHERE player_state.online_status = 'Offline'
	AND CURRENT_TIMESTAMP > player_state.last_avatar_activity + INTERVAL '1 second' * in_interval_seconds;

END
$function$


-- remove_party_invite(in_invite_id bigint, in_remove_reason smallint) -> void
-- oid: 58523  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.remove_party_invite(in_invite_id bigint, in_remove_reason smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_sender_player_id BIGINT;
	out_player_id BIGINT;
BEGIN
	-- delete invite
	IF in_invite_id IS NOT NULL THEN
		DELETE FROM party_invites WHERE invite_id = in_invite_id RETURNING player_id, sender_player_id INTO out_player_id, out_sender_player_id;
		PERFORM pg_notify('party_notify_channel', format('remove_invite#{"InviteId" : %s, "Reason" : %s, "SenderId" : %s, "PlayerId" : %s}', in_invite_id, in_remove_reason, out_sender_player_id, out_player_id));
	END IF;
END
$function$


-- remove_party_member(in_player_id bigint, in_remove_reason smallint) -> void
-- oid: 58524  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.remove_party_member(in_player_id bigint, in_remove_reason smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	out_party_id BIGINT;
	out_player_platform_name TEXT;
	member_count BIGINT;
	out_platform_members_count BIGINT;
	removed_player_id BIGINT;
	removed_is_leader BOOLEAN;
BEGIN
	PERFORM parties_get_exclusive_operation_lock();

	DELETE FROM party_members WHERE player_id = in_player_id RETURNING player_id, party_id INTO removed_player_id, out_party_id;

	IF removed_player_id IS NOT NULL THEN
		-- check if there are more than 1 player in the party
		SELECT INTO member_count COUNT(*) FROM party_members WHERE party_id = out_party_id;
		IF member_count > 1 THEN
			-- check if removed player is leader
			SELECT INTO removed_is_leader EXISTS (SELECT 1 FROM parties WHERE party_leader_id = removed_player_id);
			IF removed_is_leader THEN
				-- TODO promote player other player
				PERFORM promote_new_party_leader(out_party_id);
			END IF;
		END IF;

		SELECT accounts.platform_name INTO out_player_platform_name FROM accounts 
		JOIN actors ON actors.id = in_player_id
		WHERE accounts.id = actors.owner_account_id;

		SELECT num_of_players INTO out_platform_members_count FROM platform_parties_mapping WHERE platform_name = out_player_platform_name AND dune_party_id = out_party_id;
		IF out_platform_members_count IS NOT NULL THEN
			-- there was a platform session for the player's party
			IF out_platform_members_count <= 1 THEN
				-- if player leaving causes no players to be in that platform session anymore, remove entry
				DELETE FROM platform_parties_mapping WHERE platform_name = out_player_platform_name AND dune_party_id = out_party_id;
			ELSE
				-- still players, decrease platform player count
				UPDATE platform_parties_mapping SET num_of_players = out_platform_members_count-1 WHERE platform_name = out_player_platform_name AND dune_party_id = out_party_id;
			END IF;
		END IF;

		PERFORM pg_notify('party_notify_channel', format(
			'remove_party_member#{"PlayerId" : %s, "PartyId" : %s, "PlayerPlatformName" : "%s", "PartyRemoveReason" : %s}', 
			removed_player_id, out_party_id, out_player_platform_name, in_remove_reason));

		If member_count <= 1 THEN
			PERFORM disband_party(out_party_id);
		END IF;

	END IF;
END
$function$


-- remove_recipes_from_actor_properties(recipes_to_remove text[]) -> void
-- oid: 58525  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.remove_recipes_from_actor_properties(recipes_to_remove text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
    with actors_properties as (
        select id, properties->'CraftingRecipesLibraryActorComponent'->'m_KnownItemRecipes' as recipes
        from actors
    ),
    modified_actors_properties as (
        select id, (
            select jsonb_agg(recipe)
            from jsonb_array_elements(actors_properties.recipes) as recipe
            where not (recipe->'BaseRecipeId'->>'Name' = any(recipes_to_remove))
        ) as filtered_recipes
        from actors_properties
    )
    update actors
    set properties = jsonb_set(
        actors.properties,
        '{CraftingRecipesLibraryActorComponent,m_KnownItemRecipes}',
        coalesce(modified_actors_properties.filtered_recipes, '[]'::jsonb)
    )
    from modified_actors_properties
    where actors.id = modified_actors_properties.id;
end;
$function$


-- remove_resourcefield_states(in_map text, in_dimension_index integer, in_field_ids bigint[]) -> void
-- oid: 58526  kind: FUNCTION  category: items_purge

CREATE OR REPLACE FUNCTION dune.remove_resourcefield_states(in_map text, in_dimension_index integer, in_field_ids bigint[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE 
	FROM resourcefield_state 
	WHERE field_id = ANY(in_field_ids) AND map = in_map AND dimension_index = in_dimension_index;
END
$function$


-- request_spawn_spice_field(in_server_id text, in_spicefield_type_id integer) -> void
-- oid: 58527  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.request_spawn_spice_field(in_server_id text, in_spicefield_type_id integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    PERFORM spicefield_type_id FROM spicefield_types AS t
		WHERE t.spicefield_type_id = in_spicefield_type_id AND t.is_spawning_active IS TRUE AND t.current_globally_primed < t.max_globally_primed AND t.current_globally_active < t.max_globally_active;
	IF NOT FOUND THEN
		RETURN;
	END IF;

	UPDATE spicefield_server_availability
	SET requested_spawned_of_type = requested_spawned_of_type + 1
	WHERE server_id = in_server_id AND spicefield_type_id = in_spicefield_type_id;
END; $function$


-- reset_all_players_from_server_ids_grace_period_and_logoff_timer(in_server_id text, in_reset_time timestamp without time zone) -> void
-- oid: 58528  kind: FUNCTION  category: cleanup

CREATE OR REPLACE FUNCTION dune.reset_all_players_from_server_ids_grace_period_and_logoff_timer(in_server_id text, in_reset_time timestamp without time zone)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE encrypted_player_state SET reconnect_grace_period_end = in_reset_time WHERE server_id = in_server_id AND reconnect_grace_period_end > in_reset_time;
    UPDATE encrypted_player_state SET logoff_persistence_end_time = in_reset_time WHERE server_id = in_server_id AND logoff_persistence_end_time > in_reset_time;
END
$function$


-- reset_global_spice_field_state(in_map_name text, in_dimension_index integer) -> void
-- oid: 58529  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.reset_global_spice_field_state(in_map_name text, in_dimension_index integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE spicefield_server_availability sa
	SET requested_spawned_of_type = 0
	FROM spicefield_types st
	WHERE sa.spicefield_type_id = st.spicefield_type_id AND st.map_name = in_map_name AND st.dimension_index = in_dimension_index;

	UPDATE spicefield_types
	SET current_globally_primed = 0, current_globally_active = 0
	WHERE map_name = in_map_name AND dimension_index = in_dimension_index;
END; $function$


-- reset_journey_story_nodes_for_player(in_player_id text, in_story_node_ids text[]) -> void
-- oid: 58530  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.reset_journey_story_nodes_for_player(in_player_id text, in_story_node_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF NOT is_player_offline(in_player_id) THEN
		RAISE EXCEPTION 'Cannot execute query because the player is online - they must be offline in order for the journey data to be updated correctly without risking it being overwritten by player actions.';
	END IF;

	UPDATE journey_story_node
	SET complete_condition_state = jsonb_object(ARRAY[]::text[])
	WHERE story_node_id = ANY(in_story_node_ids)
	AND account_id IN (
		SELECT id
		FROM accounts a
		WHERE a.user = in_player_id
	);

	DELETE FROM journey_story_node_cooldown
	WHERE story_node_id = ANY(in_story_node_ids)
	AND account_id IN (
		SELECT id
		FROM accounts a
		WHERE a.user = in_player_id
	);
END $function$


-- reset_server_all_player_access_codes(in_account_id bigint) -> void
-- oid: 58531  kind: FUNCTION  category: cleanup

CREATE OR REPLACE FUNCTION dune.reset_server_all_player_access_codes(in_account_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM player_access_codes
		WHERE account_id = in_account_id
		AND is_resettable = true;
END
$function$


-- reset_specialization_keystones(in_player_id bigint) -> void
-- oid: 58532  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.reset_specialization_keystones(in_player_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM purchased_specialization_keystones WHERE player_id = in_player_id;
END $function$


-- reset_specialization_tracks(in_player_id bigint) -> void
-- oid: 58533  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.reset_specialization_tracks(in_player_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM specialization_tracks WHERE player_id = in_player_id;
END $function$


-- restore_backup_vehicle(in_account_id bigint, in_server_info dune.serverinfo, in_transform dune.transform) -> bigint
-- oid: 58534  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.restore_backup_vehicle(in_account_id bigint, in_server_info dune.serverinfo, in_transform dune.transform)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
    found_vehicle_id BIGINT;
BEGIN
    -- Retrieve a backup vehicle for the provided account.
    SELECT vehicle_id INTO found_vehicle_id
      FROM backup_vehicles
     WHERE account_id = in_account_id
     LIMIT 1;  -- In case more than one is stored.

    IF NOT FOUND THEN
        RAISE EXCEPTION 'No backup vehicle found for account_id: %', in_account_id;
    END IF;

    -- Only restore vehicle if the actor state is vehicle backup
    PERFORM 1 FROM actor_state WHERE actor_id = found_vehicle_id AND state = 'VehicleBackup';
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Trying to restore vehicle % that does not belong to vehicle backup feature.', found_vehicle_id;
    END IF;

	-- Update the actors record to restore the vehicle.
    UPDATE actors
	SET map = in_server_info.map,
		partition_id = in_server_info.partition_id,
		dimension_index = in_server_info.dimension_index,
		transform = in_transform
	WHERE id = found_vehicle_id;
    IF NOT FOUND THEN
        RAISE WARNING 'No actor record found with id % during restore.', found_vehicle_id;
    END IF;

    -- Remove the restored vehicle from backup_vehicles.
    DELETE FROM backup_vehicles WHERE vehicle_id = found_vehicle_id;
    RAISE INFO 'Deleted backup record for vehicle %.', found_vehicle_id;

    -- There is no need to keep the actor state in the table anymore since the vehicle backup was successful data wise
    DELETE FROM actor_state WHERE actor_id = found_vehicle_id;
    RAISE INFO 'Deleted actor state for vehicle %.', found_vehicle_id;

    PERFORM verify_item_dup_backup_tool(in_account_id, found_vehicle_id, 'item_dup_on_restore_vbt');

    RETURN found_vehicle_id;
END;
$function$


-- restore_recovered_vehicle(in_account_id bigint, in_vehicle_id bigint, in_server_info dune.serverinfo, in_transform dune.transform, in_restore_time_limit integer) -> void
-- oid: 58535  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.restore_recovered_vehicle(in_account_id bigint, in_vehicle_id bigint, in_server_info dune.serverinfo, in_transform dune.transform, in_restore_time_limit integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    found_vehicle_id BIGINT;
	found_vehicle_name TEXT;
	found_player_id BIGINT;
BEGIN
    -- Retrieve a recovered vehicle for the provided account.
    SELECT vehicle_id, vehicle_name INTO found_vehicle_id, found_vehicle_name 
    FROM recovered_vehicles
    WHERE account_id = in_account_id AND vehicle_id = in_vehicle_id
	-- 60s leeway in favor of player in case they try to restore last second
	AND (migrated = TRUE OR time_stored > NOW() - (in_restore_time_limit + 60) * INTERVAL '1 second')
    LIMIT 1;  -- In case more than one is stored.

    IF NOT FOUND THEN
        RAISE EXCEPTION 'No recovered vehicle found for account_id: % and vehicle_id: %', in_account_id, in_vehicle_id;
    END IF;

    -- Only restore vehicle if the actor state is VehicleRecovery
    PERFORM 1 FROM actor_state WHERE actor_id = found_vehicle_id AND state = 'VehicleRecovery';
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Trying to restore vehicle % that does not belong to vehicle recovery feature.', found_vehicle_id;
    END IF;

	-- Update the actors record to restore the vehicle.
    UPDATE actors
	SET map = in_server_info.map,
		partition_id = in_server_info.partition_id,
		dimension_index = in_server_info.dimension_index,
		transform = in_transform
	WHERE id = found_vehicle_id;
    IF NOT FOUND THEN
        RAISE WARNING 'No actor record found with id % during restore.', found_vehicle_id;
    END IF;

	-- permissions use the player controllers id, so we need to get that from the account id
	SELECT player_controller_id INTO found_player_id FROM player_state WHERE account_id = in_account_id LIMIT 1;
	IF NOT FOUND THEN
		RAISE EXCEPTION 'No player_controller_id found for account: %', in_account_id;
	END IF;
	
	-- restore default permissions
	INSERT INTO	permission_actor("actor_id", "actor_name", "actor_type", "access_level", "is_child")
	VALUES(found_vehicle_id, found_vehicle_name, 2, 3, false);

	INSERT INTO permission_actor_rank("permission_actor_id", "player_id", "rank")
	VALUES(found_vehicle_id, found_player_id, 1);

    -- Remove the restored vehicle from recovered_vehicles.
    DELETE FROM recovered_vehicles WHERE vehicle_id = found_vehicle_id;
    RAISE INFO 'Deleted recovery record for vehicle %.', found_vehicle_id;

    -- There is no need to keep the actor state in the table anymore since the vehicle recovery was successful data wise
    DELETE FROM actor_state WHERE actor_id = found_vehicle_id;
    RAISE INFO 'Deleted actor state for vehicle %.', found_vehicle_id;

    --PERFORM verify_item_dup_backup_tool(in_account_id, found_vehicle_id, 'item_dup_on_restore_vbt');

END;
$function$


-- retrieve_all_static_shifting_sand() -> TABLE(out_id text, out_alpha double precision, out_x double precision, out_y double precision, out_last_modified_time bigint)
-- oid: 58536  kind: FUNCTION  category: shifting_sand

CREATE OR REPLACE FUNCTION dune.retrieve_all_static_shifting_sand()
 RETURNS TABLE(out_id text, out_alpha double precision, out_x double precision, out_y double precision, out_last_modified_time bigint)
 LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
        SELECT id, alpha, x, y, EXTRACT(EPOCH FROM last_modified_time)::BIGINT AS last_modified_time FROM shiftingsands_data;
END; $function$


-- returning_player_award_given(in_account_id bigint) -> void
-- oid: 58537  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.returning_player_award_given(in_account_id bigint)
 RETURNS void
 LANGUAGE sql
AS $function$
    UPDATE player_state SET last_returning_player_awarded_time=now(), last_returning_player_event_time=NULL WHERE account_id=in_account_id;
$function$


-- reveal_journey_story_nodes_for_player(in_player_id text, in_story_node_ids text[]) -> void
-- oid: 58538  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.reveal_journey_story_nodes_for_player(in_player_id text, in_story_node_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF NOT is_player_offline(in_player_id) THEN
		RAISE EXCEPTION 'Cannot execute query because the player is online - they must be offline in order for the journey data to be updated correctly without risking it being overwritten by player actions.';
	END IF;

	WITH player_account_id AS (
		SELECT id
		FROM accounts a
		WHERE a.user = in_player_id
	)
	INSERT INTO journey_story_node(account_id, story_node_id, override_reward_block, has_pending_reward, complete_condition_state, reveal_condition_state, fail_condition_state, metadata_state, reset_group)
	SELECT player_account_id.id, completed_node.story_node_id, completed_node.override_reward_block, completed_node.has_pending_reward, completed_node.complete_condition_state, completed_node.reveal_condition_state, completed_node.fail_condition_state, completed_node.metadata_state, completed_node.reset_group
	FROM player_account_id 
	CROSS JOIN (
		SELECT story_node_id, false, false, jsonb_object(ARRAY[]::text[]), to_jsonb(true), jsonb_object(ARRAY[]::text[]), jsonb_object(ARRAY[]::text[]), 'Default'::JourneyStoryResetGroup
		FROM UNNEST(in_story_node_ids) AS story_node_id
	) completed_node(story_node_id, override_reward_block, has_pending_reward, complete_condition_state, reveal_condition_state, fail_condition_state, metadata_state, reset_group)
	ON CONFLICT ON CONSTRAINT journey_story_node_pkey
		DO UPDATE SET
			reveal_condition_state = EXCLUDED.reveal_condition_state,
			metadata_state = EXCLUDED.metadata_state;
END $function$


-- save_aborted_authority_transfer_actors(in_actor_ids bigint[], in_partition_id bigint) -> void
-- oid: 58539  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.save_aborted_authority_transfer_actors(in_actor_ids bigint[], in_partition_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO actor_state(actor_id, state)
        SELECT a.id, 'AbortedAuthorityTransfer'
        FROM actors AS a
        WHERE a.id = ANY(in_actor_ids) AND a.partition_id = in_partition_id
    ON CONFLICT DO NOTHING;
END
$function$


-- save_actor_dislocation(in_actor_id bigint, in_current_server_info dune.serverinfo, in_target_location dune.vector, in_target_dimension_index integer) -> void
-- oid: 58540  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.save_actor_dislocation(in_actor_id bigint, in_current_server_info dune.serverinfo, in_target_location dune.vector, in_target_dimension_index integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	update actors
		set
			transform = (in_target_location, (transform).rotation),
			dimension_index = in_target_dimension_index,
			partition_id = null
		where id = in_actor_id
			and map = (in_current_server_info).map
			and dimension_index = (in_current_server_info).dimension_index
			and (
				partition_id is null
				or (in_current_server_info).partition_id is null
				or partition_id = (in_current_server_info).partition_id
			);
end
$function$


-- save_actors(in_server_info dune.serverinfo, in_actors dune.actordescription[], in_actor_state dune.actorstate) -> TABLE(actor_id bigint, current_saved_serial bigint, saved boolean)
-- oid: 58541  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.save_actors(in_server_info dune.serverinfo, in_actors dune.actordescription[], in_actor_state dune.actorstate DEFAULT 'Default'::dune.actorstate)
 RETURNS TABLE(actor_id bigint, current_saved_serial bigint, saved boolean)
 LANGUAGE plpgsql
AS $function$
BEGIN
	return query with
		input_actors as (
			select * from unnest(in_actors)
		),
		valid_input_actors as (
			select input_actors.id from input_actors
			left join actor_state on input_actors.id = actor_state.actor_id
			where actor_state.state = in_actor_state or (in_actor_state = 'Default' and actor_state.actor_id is null)
		),
		serial_checks as (
			select
				input.id,
				input.serial as input_serial,
				coalesce(actors.serial, 0) as saved_serial,
				input.serial >= coalesce(actors.serial, 0) and input.id in (select id from valid_input_actors) as should_save
			from
				input_actors as input left join actors using (id)
		),
		actors_to_save as (
			select i.* from input_actors as i join serial_checks as c using (id) where c.should_save
		),
		upsert_actors as (
			insert into actors(
				"id", "class", "transform",
				"gas_attributes",
				"properties",
				"map", "partition_id", "dimension_index",
				"serial"
			)
				select
					i.id, i.class_name, i.transform,
					(i.generic_data).gas_attribute_sets_json, (i.generic_data).properties_json,
					in_server_info.map, in_server_info.partition_id, coalesce(in_server_info.dimension_index, 0),
					i.serial
				from actors_to_save as i
			on conflict (id) do update
			set
				"class" = EXCLUDED.class, "transform" = case when EXCLUDED.transform is null or (EXCLUDED.transform).location = (zero_transform()).location then actors.transform else EXCLUDED.transform end,

				"gas_attributes" = EXCLUDED.gas_attributes, "properties" = EXCLUDED.properties,

				"map" = EXCLUDED.map, "partition_id" = EXCLUDED.partition_id, "dimension_index" = EXCLUDED.dimension_index,
				"serial" = EXCLUDED.serial
			returning id
		),
		fgl_entity_data as (
			select id as actor_id, (u).entity_id, (u).slot_name, (u).components_json as components
				from (select id, unnest((generic_data).entities) as u from actors_to_save) q
		),
		missing_entities as (
			select entity_id
				from actors_to_save join actor_fgl_entities as existing on (actors_to_save.id = existing.actor_id)
				where not exists(select 1 from fgl_entity_data as updated where updated.entity_id=existing.entity_id)
		),
		delete_missing_entity_links as (
			delete from actor_fgl_entities as existing using missing_entities
				where existing.entity_id=missing_entities.entity_id
				returning existing.entity_id as deleted_entity_id
		),
		delete_missing_entities as (
			delete from fgl_entities as existing using missing_entities
				where existing.entity_id=missing_entities.entity_id
		),
		upsert_entities as (
			insert into fgl_entities("entity_id", "components")
				select entity_id, components from fgl_entity_data
				on conflict (entity_id) do update
				set components=EXCLUDED.components
			returning entity_id
		),
		upsert_entity_links as (
			insert into actor_fgl_entities("actor_id", "entity_id", "slot_name")
				select fgl_entity_data.actor_id, fgl_entity_data.entity_id, fgl_entity_data.slot_name
					from fgl_entity_data left join upsert_entities using (entity_id)
					-- HACK: this is a temporary fix until we do TECH-23063
					where not entity_id in (select deleted_entity_id from delete_missing_entity_links)
				on conflict (entity_id) do update
				set
					actor_id=EXCLUDED.actor_id,
					slot_name=EXCLUDED.slot_name
			returning actor_fgl_entities.actor_id, actor_fgl_entities.entity_id
		),
		all_actor_entities as (
			select fgl_entity_data.actor_id as id, array_agg(entity_id) as entity_ids
				from fgl_entity_data
					left join upsert_entities using (entity_id)
					left join upsert_entity_links using (entity_id)
				group by fgl_entity_data.actor_id
		),
		extra_data as (
			select
				input_actors.id,
				serial_checks.saved_serial,
				(serial_checks.should_save) as saved,
				(input_actors.generic_data).building_actor_data as building_data,
				(input_actors.generic_data).placeable_actor_data as placeable_data,
				(input_actors.generic_data).totem_actor_data as totem_data,
				coalesce(all_actor_entities.entity_ids, array[]::int[]) as entity_ids
			from
				serial_checks
				left join input_actors using(id)
				left join upsert_actors using(id) -- this is needed for dependency
				left join all_actor_entities using(id) -- this is needed for dependency only
		),
		save_extras as (
			select
				extra_data.id,
				extra_data.saved_serial,
				extra_data.saved,
				case when extra_data.saved and building_data is not null then
					save_building(id, building_data)
				end,
				case when extra_data.saved and placeable_data is not null then
					save_placeable(id, placeable_data)
				end,
				case when extra_data.saved and totem_data is not null then
					save_totem(id, totem_data)
				end,
				entity_ids
			from extra_data
		)
		select id, saved_serial, save_extras.saved from save_extras;
END
$function$


-- save_building(in_building_id bigint, in_data dune.buildingsavedata) -> void
-- oid: 58543  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.save_building(in_building_id bigint, in_data dune.buildingsavedata)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	instance BUILDINGINSTANCE;
	instance_to_remove INTEGER;
	instance_owner BuildingInstanceUpdateOwner;
BEGIN
	-- ADD
	IF array_length(in_data.in_add_building_data, 1) > 0 THEN
		INSERT INTO building_instances(
			"building_id",
			"instance_id",
			"building_type",
			"transform",
			"owner_entity_id",
			"building_flags",
			"health",
			"shelter",
			"stabilization_begin_timespan",
			"stabilization_end_timespan",
			"stabilization_state",
			"sand_buildup"
		)
		SELECT
			in_building_id,
			add_data.instance_id,
			add_data.building_type,
			add_data.transform,
			_building_validate_totem_owner_id(add_data.owner_entity_id),
			add_data.building_flags,
			add_data.health,
			add_data.shelter,
			add_data.stabilization_begin_timespan,
			add_data.stabilization_end_timespan,
			add_data.stabilization_state,
			add_data.sand_buildup
		FROM unnest(in_data.in_add_building_data) as add_data
		ON CONFLICT ("building_id", "instance_id")
			DO UPDATE SET
				"building_type" = (instance).building_type,
				"transform" = (instance).transform,
				"owner_entity_id" = _building_validate_totem_owner_id((instance).owner_entity_id),
				"building_flags" = (instance).building_flags,
				"health" = (instance).health,
				"shelter" = (instance).shelter,
				"stabilization_begin_timespan" = (instance).stabilization_begin_timespan,
				"stabilization_end_timespan" = (instance).stabilization_end_timespan,
				"stabilization_state" = (instance).stabilization_state,
				"sand_buildup" = (instance).sand_buildup;
	END IF;

	-- REMOVE
	IF array_length(in_data.in_remove_building_data, 1) > 0 THEN
		DELETE FROM building_instances
			WHERE building_instances."building_id" = in_building_id AND building_instances."instance_id" = ANY(in_data.in_remove_building_data);
	END IF;

	-- OWNER. 99.99% of the time, this will only have 1 Owner Array.
	IF array_length(in_data.in_building_owner_data, 1) > 0 THEN
		FOREACH instance_owner IN ARRAY in_data.in_building_owner_data LOOP
			WITH owner_changes_table AS
			(
				SELECT unnest(instance_owner.instances) AS instance_id, instance_owner.owner_entity_id AS owner_entity_id
			)
			UPDATE building_instances
				SET owner_entity_id = _building_validate_totem_owner_id(owner_changes_table.owner_entity_id)
			FROM owner_changes_table
			WHERE building_instances.building_id = in_building_id AND building_instances.instance_id = owner_changes_table.instance_id;
		END LOOP;
	END IF;

	-- STABILIZATION
	IF array_length(in_data.in_building_stabilization_data, 1) > 0 THEN
		WITH stabilization_changes_table AS
		(
			select * FROM unnest(in_data.in_building_stabilization_data)
		)
		UPDATE building_instances SET stabilization_begin_timespan = stabilization_changes_table.stabilization_begin_timespan, stabilization_end_timespan = stabilization_changes_table.stabilization_end_timespan, stabilization_state = stabilization_changes_table.stabilization_state
		FROM stabilization_changes_table
		WHERE building_instances.building_id = in_building_id AND building_instances.instance_id = stabilization_changes_table.instance_id;
	END IF;

	-- HEALTH
	IF array_length(in_data.in_building_health_data, 1) > 0 THEN
		WITH health_changes_table AS
		(
			select * FROM unnest(in_data.in_building_health_data)
		)
		UPDATE building_instances SET health = health_changes_table.health
		FROM health_changes_table
		WHERE building_instances.building_id = in_building_id AND building_instances.instance_id = health_changes_table.instance_id;
	END IF;

	-- SHELTER
	IF array_length(in_data.in_building_shelter_data, 1) > 0 THEN
		WITH shelter_changes_table AS
		(
			select * FROM unnest(in_data.in_building_shelter_data)
		)
		UPDATE building_instances SET shelter = shelter_changes_table.shelter
		FROM shelter_changes_table
		WHERE building_instances.building_id = in_building_id AND building_instances.instance_id = shelter_changes_table.instance_id;
	END IF;

	-- SAND BUILDUP
	IF array_length(in_data.in_building_sand_buildup_data, 1) > 0 THEN
		WITH sand_buildup_changes_table AS
		(
			select * FROM unnest(in_data.in_building_sand_buildup_data)
		)
		UPDATE building_instances SET sand_buildup = sand_buildup_changes_table.sand_buildup
		FROM sand_buildup_changes_table
		WHERE building_instances.building_id = in_building_id AND building_instances.instance_id = sand_buildup_changes_table.instance_id;
	END IF;

	-- BUILDING FLAGS
	IF array_length(in_data.in_building_building_flags_data, 1) > 0 THEN
		WITH building_flags_changes_table AS
		(
			select * FROM unnest(in_data.in_building_building_flags_data)
		)
		UPDATE building_instances SET building_flags = building_flags_changes_table.building_flags
		FROM building_flags_changes_table
		WHERE building_instances.building_id = in_building_id AND building_instances.instance_id = building_flags_changes_table.instance_id;
	END IF;

	-- BUILDING TRANSFORM
	IF array_length(in_data.in_building_building_transform_data, 1) > 0 THEN
		WITH building_flags_transform_table AS
		(
			select * FROM unnest(in_data.in_building_building_transform_data)
		)
		UPDATE building_instances SET transform = building_flags_transform_table.transform
		FROM building_flags_transform_table
		WHERE building_instances.building_id = in_building_id AND building_instances.instance_id = building_flags_transform_table.instance_id;
	END IF;
END
$function$


-- save_building_blueprint_copy(in_building_item_id bigint, in_building_blueprint_id bigint, in_building_blueprint_building_data dune.buildingblueprintpiecesaveitemcontainer[], in_building_blueprint_placeable_data dune.buildingblueprintplaceablesaveitemcontainer[], in_building_blueprint_pentashield_data dune.buildingblueprintpentashielditem[]) -> bigint
-- oid: 58544  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.save_building_blueprint_copy(in_building_item_id bigint, in_building_blueprint_id bigint, in_building_blueprint_building_data dune.buildingblueprintpiecesaveitemcontainer[], in_building_blueprint_placeable_data dune.buildingblueprintplaceablesaveitemcontainer[], in_building_blueprint_pentashield_data dune.buildingblueprintpentashielditem[])
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	return_id BIGINT;
BEGIN
	IF in_building_blueprint_id != 0 THEN
		DELETE FROM building_blueprints WHERE id = in_building_blueprint_id;
    END IF;

    INSERT INTO building_blueprints(id, item_id, player_id, building_blueprint_map)
        VALUES(DEFAULT, in_building_item_id, NULL, '') RETURNING id INTO return_id;

 -- All Building Pieces
    INSERT INTO building_blueprint_instances(building_blueprint_id, instance_id, building_type, transform, provides_stability, health, hologram)
    SELECT
        return_id,
        piece_data.instance_id,
        container_data.building_type,
        piece_data.transform,
        piece_data.provides_stability,
        piece_data.health,
        True
    FROM
        unnest(in_building_blueprint_building_data) AS container_data
        CROSS JOIN LATERAL unnest(container_data.building_pieces) AS piece_data;

-- All Placeables
    INSERT INTO building_blueprint_placeables(building_blueprint_id, placeable_id, building_type, transform, hologram)
    SELECT
        return_id,
        placeable_data.placeable_id,
        container_data.building_type,
        placeable_data.transform,
        True
    FROM
        unnest(in_building_blueprint_placeable_data) AS container_data
        CROSS JOIN LATERAL unnest(container_data.placeables) AS placeable_data;

-- Pentashields
    INSERT INTO building_blueprint_pentashields("building_blueprint_id", "placeable_id", "scale")
    SELECT return_id as building_blueprint_id, placeable_id, scale FROM unnest(in_building_blueprint_pentashield_data);

    RETURN return_id;
END
$function$


-- save_demo_account_time(in_fls_id text, in_demo_playtime_seconds integer) -> void
-- oid: 58545  kind: FUNCTION  category: schema_meta

CREATE OR REPLACE FUNCTION dune.save_demo_account_time(in_fls_id text, in_demo_playtime_seconds integer)
 RETURNS void
 LANGUAGE sql
AS $function$
	INSERT INTO demo_users("fls_id", "demo_playtime_seconds", "demo_state")
	VALUES (in_fls_id, in_demo_playtime_seconds, 'Demo'::DemoState)
	ON CONFLICT ("fls_id") DO UPDATE
	SET demo_playtime_seconds = EXCLUDED.demo_playtime_seconds; 
$function$


-- save_dialogue_data(in_player_controller_id bigint, in_met_npcs text[], in_taken_nodes integer[]) -> void
-- oid: 58546  kind: FUNCTION  category: dialogue

CREATE OR REPLACE FUNCTION dune.save_dialogue_data(in_player_controller_id bigint, in_met_npcs text[], in_taken_nodes integer[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO dialogue_met_npcs(player_id, npc_name) SELECT in_player_controller_id, unnest(in_met_npcs)
    ON CONFLICT DO NOTHING;

    INSERT INTO dialogue_taken_nodes(player_id, node_id) SELECT in_player_controller_id, unnest(in_taken_nodes)
    ON CONFLICT DO NOTHING;
END
$function$


-- save_item(in_item dune.inventoryitem) -> void
-- oid: 58547  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.save_item(in_item dune.inventoryitem)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	INSERT INTO
		items (id, inventory_id, stack_size, quality_level, volume_override, position_index, template_id, stats, is_new, acquisition_time)
		VALUES (
			(in_item).item_id,
			(in_item).inventory_id,
			(in_item).stack_size,
			(in_item).quality_level,
            (in_item).volume_override,
			(in_item).position_index,
			(in_item).template_id,
			(in_item).stats,
			(in_item).is_new,
			(in_item).acquisition_time
		)
		ON CONFLICT (id)
			DO UPDATE SET
				inventory_id = (in_item).inventory_id,
				stack_size = (in_item).stack_size,
                quality_level = (in_item).quality_level,
                volume_override = (in_item).volume_override,
				position_index = (in_item).position_index,
				template_id = (in_item).template_id,
				stats = items.stats || (in_item).stats,
				is_new = (in_item).is_new,
				acquisition_time = (in_item).acquisition_time;
    
    -- log item tracking
    PERFORM _add_item_trace_log('save_item', (in_item).item_id, (in_item).inventory_id, (in_item).template_id, (in_item).position_index);
end
$function$


-- save_journey_story_node(in_account_id bigint, in_story_node_id text, in_override_reward_block boolean, in_has_pending_reward boolean, in_complete_condition_state jsonb, in_reveal_condition_state jsonb, in_fail_condition_state jsonb, in_metadata_state jsonb, in_reset_group dune.journeystoryresetgroup) -> void
-- oid: 58548  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.save_journey_story_node(in_account_id bigint, in_story_node_id text, in_override_reward_block boolean, in_has_pending_reward boolean, in_complete_condition_state jsonb, in_reveal_condition_state jsonb, in_fail_condition_state jsonb, in_metadata_state jsonb, in_reset_group dune.journeystoryresetgroup)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO journey_story_node(account_id, story_node_id, override_reward_block, has_pending_reward, complete_condition_state, reveal_condition_state, fail_condition_state, metadata_state, reset_group)
	VALUES(in_account_id, in_story_node_id, in_override_reward_block, in_has_pending_reward, in_complete_condition_state, in_reveal_condition_state, in_fail_condition_state, in_metadata_state, in_reset_group)
	ON CONFLICT (account_id, story_node_id)
	DO UPDATE SET
        override_reward_block = in_override_reward_block,
        has_pending_reward = in_has_pending_reward,
        complete_condition_state = in_complete_condition_state,
        reveal_condition_state = in_reveal_condition_state,
		fail_condition_state = in_fail_condition_state,
		metadata_state = in_metadata_state,
		reset_group = in_reset_group;
END $function$


-- save_journey_story_nodes(in_account_id bigint, in_journey_data dune.savejourneydata[]) -> void
-- oid: 58549  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.save_journey_story_nodes(in_account_id bigint, in_journey_data dune.savejourneydata[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO journey_story_node(account_id, story_node_id, override_reward_block, has_pending_reward, complete_condition_state, reveal_condition_state, fail_condition_state, metadata_state, reset_group)
		SELECT in_account_id, story_node_id, override_reward_block, has_pending_reward, to_jsonb(completion_state_string), to_jsonb(reveal_state_string), to_jsonb(fail_state_string), to_jsonb(metadata_state_string), reset_group
		FROM UNNEST(in_journey_data)
	ON CONFLICT ON CONSTRAINT journey_story_node_pkey
		DO UPDATE SET
			override_reward_block = EXCLUDED.override_reward_block,
            has_pending_reward = EXCLUDED.has_pending_reward,
			complete_condition_state = EXCLUDED.complete_condition_state,
			reveal_condition_state = EXCLUDED.reveal_condition_state,
			fail_condition_state = EXCLUDED.fail_condition_state,
			metadata_state = EXCLUDED.metadata_state,
			reset_group = EXCLUDED.reset_group;
END
$function$


-- save_login_target_dimension(in_fls_id text, in_login_target_dimension_index integer) -> void
-- oid: 58550  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.save_login_target_dimension(in_fls_id text, in_login_target_dimension_index integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO player_travel_state (fls_id, login_target_dimension_index)
	VALUES (in_fls_id, in_login_target_dimension_index)
	ON CONFLICT (fls_id) DO UPDATE
	SET login_target_dimension_index = EXCLUDED.login_target_dimension_index;
END;
$function$


-- save_markers(in_player_marker_data dune.saveplayermarkerdata[], in_marker_data dune.savemarkerdata[]) -> void
-- oid: 58551  kind: FUNCTION  category: markers

CREATE OR REPLACE FUNCTION dune.save_markers(in_player_marker_data dune.saveplayermarkerdata[], in_marker_data dune.savemarkerdata[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	null_player_marker_map_id_count INTEGER;
	null_marker_map_id_count INTEGER;
BEGIN

	SELECT COUNT(*)
    INTO null_player_marker_map_id_count
	FROM UNNEST(in_player_marker_data) AS umd
	LEFT JOIN map_names AS mn ON umd.map_name = mn.map_name
    WHERE mn.map_name_id IS NULL; -- Find map name that doesn't exist

	IF null_player_marker_map_id_count > 0 THEN
        RAISE EXCEPTION 'Found records with NULL map name id for player markers!';
    END IF;

    SELECT COUNT(*)
    INTO null_marker_map_id_count
	FROM UNNEST(in_marker_data) AS umd
	LEFT JOIN map_names AS mn ON umd.map_name = mn.map_name
    WHERE mn.map_name_id IS NULL; -- Find map name that doesn't exist

	IF null_marker_map_id_count > 0 THEN
        RAISE EXCEPTION 'Found records with NULL map name id for markers!';
    END IF;

    INSERT INTO markers("marker_hash_id", "dimension_index", "map_name_id", "marker", "area_id", "area_radius", "long_range", "payload")
        SELECT
           	umd.marker_hash_id,
            umd.dimension_index,
            mn.map_name_id,
            umd.marker,
            umd.area_id,
            umd.area_radius,
            umd.long_range,
            umd.payload
        FROM UNNEST(in_marker_data) umd JOIN map_names mn USING(map_name)
    	ORDER BY marker_hash_id, dimension_index, map_name_id
    ON CONFLICT ON CONSTRAINT markers_pkey
        DO UPDATE SET
            "area_id" = EXCLUDED.area_id,
            "area_radius" = EXCLUDED.area_radius,
            "long_range" = EXCLUDED.long_range,
            "payload" = EXCLUDED.payload;

	WITH marker_data_for_existing_players AS (
    	SELECT
			player_data.player_id,
			player_data.marker_hash_id,
			player_data.dimension_index,
			mn.map_name_id,
			player_data.discovery_level,
			player_data.discovery_method,
			player_data.player_payload
    	FROM UNNEST(in_player_marker_data) AS player_data
    	JOIN actors ON actors.id = player_data.player_id
    	JOIN map_names mn USING(map_name)
	)
	INSERT INTO player_markers("player_id", "marker_hash_id", "dimension_index", "map_name_id", "discovery_level", "discovery_method", "payload")
		SELECT
			in_data.player_id,
			in_data.marker_hash_id,
			in_data.dimension_index,
			in_data.map_name_id,
			in_data.discovery_level,
			in_data.discovery_method,
			in_data.player_payload
		FROM marker_data_for_existing_players AS in_data, markers
		WHERE markers.marker_hash_id = in_data.marker_hash_id AND markers.dimension_index = in_data.dimension_index AND markers.map_name_id = in_data.map_name_id
		ORDER BY player_id, in_data.marker_hash_id, in_data.dimension_index, in_data.map_name_id -- Ordering for deadlock avoidance
	ON CONFLICT ON CONSTRAINT player_markers_pkey
		DO UPDATE SET
			"discovery_level" = EXCLUDED.discovery_level,
			"discovery_method" = EXCLUDED.discovery_method,
			"payload" = EXCLUDED.payload;
END
$function$


-- save_mnemonic_recall_lesson(in_account_id bigint, in_lesson_id text, in_lesson_state bigint, in_lesson_progress integer, in_is_new boolean) -> void
-- oid: 58552  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.save_mnemonic_recall_lesson(in_account_id bigint, in_lesson_id text, in_lesson_state bigint, in_lesson_progress integer, in_is_new boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO mnemonic_recall(account_id, lesson_id, lesson_state, lesson_progress, is_new)
    VALUES(in_account_id, in_lesson_id, in_lesson_state, in_lesson_progress, in_is_new)
    ON CONFLICT (account_id, lesson_id)
    DO UPDATE SET
        lesson_state = in_lesson_state,
        lesson_progress = in_lesson_progress,
        is_new = in_is_new;
END; $function$


-- save_placeable(in_placeable_id bigint, in_data dune.placeablesavedata) -> void
-- oid: 58561  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.save_placeable(in_placeable_id bigint, in_data dune.placeablesavedata)
 RETURNS void
 LANGUAGE sql
BEGIN ATOMIC
 INSERT INTO dune.placeables (id, owner_entity_id, health, building_type, has_hit_ground, has_buildable_support, is_hologram)
   VALUES (save_placeable.in_placeable_id, dune._placeable_validate_totem_owner_id((save_placeable.in_data).in_owner_entity_id), (save_placeable.in_data).in_health, (save_placeable.in_data).in_building_type, (save_placeable.in_data).in_has_hit_ground, (save_placeable.in_data).in_has_buildable_support, (save_placeable.in_data).in_is_hologram) ON CONFLICT(id) DO UPDATE SET owner_entity_id = excluded.owner_entity_id, health = excluded.health, building_type = excluded.building_type, has_hit_ground = excluded.has_hit_ground, has_buildable_support = excluded.has_buildable_support, is_hologram = excluded.is_hologram;
END


-- save_player(in_player dune.playerdescription) -> boolean
-- oid: 58562  kind: FUNCTION  category: player_persistence

CREATE OR REPLACE FUNCTION dune.save_player(in_player dune.playerdescription)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
    fgl_entity FglEntity;
    player_actors ActorDescription[];
    all_serials_are_same Boolean;
    should_save_all_actors Boolean;
BEGIN
    player_actors := array[in_player.state, in_player.controller];

    IF NOT in_player.pawn IS NULL THEN
        foreach fgl_entity in array (in_player).pawn.generic_data.entities loop
            if fgl_entity.slot_name in ('DuneCharacter', 'PersonalCrafting') and fgl_entity.components_json = '{}'::jsonb then
                raise exception 'invalid player save with empty fgl state for pawn';
            end if;
        end loop;
        player_actors := player_actors || in_player.pawn;
    END IF;

    with
        ids_and_serials as (
            select
                input.serial = (in_player.controller).serial as is_matching_serial,
                input.serial >= existing.serial as should_save
            from unnest(player_actors) as input join actors as existing using (id)
        )
        select bool_and(is_matching_serial), bool_and(should_save) from ids_and_serials
        into all_serials_are_same, should_save_all_actors;

    IF NOT all_serials_are_same THEN
        raise exception 'serial: serial mismatch between the player actors';
    end if;

    -- Demo time remaining
    IF in_player.demo_playtime_seconds IS NOT NULL THEN
        UPDATE demo_users
        SET demo_playtime_seconds = in_player.demo_playtime_seconds
        WHERE fls_id = (
            SELECT acc.user FROM accounts AS acc
            WHERE acc.id = in_player.id
        );
    END IF;

    -- Demo state
    IF in_player.demo_state IS NOT NULL THEN
        UPDATE demo_users
		SET demo_state = in_player.demo_state
		WHERE fls_id = (
            SELECT acc.user FROM accounts AS acc
            WHERE acc.id = in_player.id
        );
    END IF;

    IF NOT should_save_all_actors THEN
        return false;
    end if;

    -- Allow saving of the player without having a player character (pawn)
    PERFORM save_actors(in_player.pawn_server_info, player_actors);

    PERFORM update_respawn_locations(in_player.id, (in_player.respawn_info).locations);

    PERFORM update_death_location(in_player.pawn, in_player.pawn_server_info, in_player.life_state);

    UPDATE player_state
        SET pending_respawn_location_id=(in_player.respawn_info).pending_location_id, life_state=in_player.life_state
        WHERE account_id=in_player.id;

    return true;
END;
$function$


-- save_player_pawn(in_pawn dune.actordescription, in_server_info dune.serverinfo, in_life_state dune.playerlifestate) -> boolean
-- oid: 58563  kind: FUNCTION  category: player_persistence

CREATE OR REPLACE FUNCTION dune.save_player_pawn(in_pawn dune.actordescription, in_server_info dune.serverinfo, in_life_state dune.playerlifestate)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
    should_save_pawn BOOLEAN;
BEGIN
    WITH
        ids_and_serials AS (
            SELECT actors.serial >= (in_pawn).serial AS should_save
            FROM actors
            WHERE actors.id = in_pawn.id
        )
        SELECT should_save FROM ids_and_serials
        INTO should_save_pawn;

    IF NOT should_save_pawn THEN
        RETURN false;
    END IF;

    PERFORM save_actors(in_server_info, ARRAY[in_pawn]);

    PERFORM update_death_location(in_pawn, in_server_info, in_life_state);

    RETURN true;
END;
$function$


-- save_static_encounter_name(in_map_name text, in_package_name text, in_actor_name text, in_encounter_name text) -> void
-- oid: 58564  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.save_static_encounter_name(in_map_name text, in_package_name text, in_actor_name text, in_encounter_name text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO encounters_static(map_name, package_name, actor_name, encounter_name, waiting_for_reset) 
    VALUES(in_map_name, in_package_name, in_actor_name, in_encounter_name, false) 
    ON CONFLICT(map_name, package_name, actor_name) 
    DO UPDATE SET encounter_name = in_encounter_name, waiting_for_reset = false 
    WHERE encounters_static.map_name = in_map_name AND encounters_static.package_name = in_package_name AND encounters_static.actor_name = in_actor_name;
END; $function$


-- save_static_encounter_waiting_for_reset(in_map_name text, in_package_name text, in_actor_name text, in_waiting_for_reset boolean) -> void
-- oid: 58565  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.save_static_encounter_waiting_for_reset(in_map_name text, in_package_name text, in_actor_name text, in_waiting_for_reset boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE encounters_static 
    SET waiting_for_reset = in_waiting_for_reset 
    WHERE encounters_static.map_name = in_map_name AND encounters_static.package_name = in_package_name AND encounters_static.actor_name = in_actor_name;
END; $function$


-- save_totem(in_id bigint, in_data dune.totemsavedata) -> void
-- oid: 58571  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.save_totem(in_id bigint, in_data dune.totemsavedata)
 RETURNS void
 LANGUAGE sql
BEGIN ATOMIC
 INSERT INTO dune.totems (id, landclaim_vertical_level, last_backup_timestamp, landclaim_original_global_location, landclaim_original_global_yaw_rotation)
   VALUES (save_totem.in_id, (save_totem.in_data).landclaim_vertical_level, (save_totem.in_data).last_backup_timestamp, (save_totem.in_data).landclaim_original_global_location, (save_totem.in_data).landclaim_original_global_yaw_rotation) ON CONFLICT(id) DO UPDATE SET landclaim_vertical_level = excluded.landclaim_vertical_level, last_backup_timestamp = excluded.last_backup_timestamp, landclaim_original_global_location = excluded.landclaim_original_global_location, landclaim_original_global_yaw_rotation = excluded.landclaim_original_global_yaw_rotation;
END


-- save_tracked_journey_cards(in_player_id bigint, in_tracked_journey_card text, in_tracked_landsraad_card text) -> void
-- oid: 58572  kind: FUNCTION  category: player_persistence

CREATE OR REPLACE FUNCTION dune.save_tracked_journey_cards(in_player_id bigint, in_tracked_journey_card text, in_tracked_landsraad_card text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO journey_tracked_cards (player_id, tracked_journey_card, tracked_landsraad_card) Values(in_player_id, in_tracked_journey_card, in_tracked_landsraad_card)
	ON CONFLICT(player_id) DO
	UPDATE SET tracked_journey_card = in_tracked_journey_card, tracked_landsraad_card = in_tracked_landsraad_card;
END $function$


-- save_travel_return_info(in_player_controller_id bigint, in_map text, in_transform dune.transform) -> void
-- oid: 58573  kind: FUNCTION  category: travel

CREATE OR REPLACE FUNCTION dune.save_travel_return_info(in_player_controller_id bigint, in_map text, in_transform dune.transform)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	insert into travel_return_info(
		"player_controller_id", "map", "transform"
	)
	values(
			  in_player_controller_id, in_map, in_transform
	)
	on conflict (player_controller_id) do update
		set
			"map" = EXCLUDED.map,
			"transform" = EXCLUDED.transform;
end
$function$


-- save_vehicle_modules(in_add_list dune.vehiclemodule[], in_delete_list bigint[], in_stat_update dune.itemstatupdate[]) -> SETOF bigint
-- oid: 58574  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.save_vehicle_modules(in_add_list dune.vehiclemodule[], in_delete_list bigint[], in_stat_update dune.itemstatupdate[])
 RETURNS SETOF bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	module VEHICLEMODULE;
	stat ItemStatUpdate;
	new_module_id BIGINT;
	currentstat RECORD;
BEGIN
	--RAISE NOTICE 'Add vehicle modules';
	-- add vehicle modules
	FOREACH module IN ARRAY in_add_list LOOP
		INSERT INTO vehicle_modules(
			"vehicle_id", "template_id", "stats"
		) VALUES(
			(module).vehicle_id, (module).template_id, (module).stats
		) RETURNING id INTO new_module_id;
		RETURN NEXT new_module_id;
	END LOOP;

	--RAISE NOTICE 'Delete vehicle modules';
	-- delete modules
	DELETE FROM vehicle_modules WHERE id = ANY(in_delete_list);

	--RAISE NOTICE 'Add vehicle module stats';
	-- add vehicle module stats
	FOREACH stat IN ARRAY in_stat_update LOOP
		UPDATE vehicle_modules SET "stats" = vehicle_modules.stats || (stat).value WHERE "id" = (stat).item_id;
	END LOOP;

	RETURN;
END
$function$


-- save_world_partition(in_map_name text, in_server_id text, in_dimension_index bigint, in_partition_definition jsonb, in_blocked boolean, in_label text) -> bigint
-- oid: 58575  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.save_world_partition(in_map_name text, in_server_id text, in_dimension_index bigint, in_partition_definition jsonb, in_blocked boolean DEFAULT false, in_label text DEFAULT NULL::text)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	partition_id BIGINT;
BEGIN
	LOCK TABLE world_partition; -- only one at a time, please
	INSERT INTO world_partition(partition_id, server_id, map, partition_definition, dimension_index, blocked, label) VALUES(DEFAULT, in_server_id, in_map_name, in_partition_definition, in_dimension_index, in_blocked, in_label)
		ON CONFLICT ("server_id", "map") DO UPDATE set partition_definition = in_partition_definition, blocked = in_blocked, label = in_label WHERE world_partition.server_id = in_server_id
		RETURNING world_partition.partition_id INTO partition_id;
	RETURN partition_id;
END
$function$


-- server_info_match(in_actor dune.actors, in_server_info dune.serverinfo) -> boolean
-- oid: 58586  kind: FUNCTION  category: server

CREATE OR REPLACE FUNCTION dune.server_info_match(in_actor dune.actors, in_server_info dune.serverinfo)
 RETURNS boolean
 LANGUAGE plpgsql
 STABLE STRICT
AS $function$
BEGIN
	return in_actor.map = in_server_info.map
        AND in_actor.dimension_index = in_server_info.dimension_index
        AND (
            in_actor.partition_id IS NULL
            OR
            in_server_info.partition_id IS NULL
            OR
            in_actor.partition_id = in_server_info.partition_id
        );
END
$function$


-- set_account_as_takeoverable(in_user_id text, in_new_user_id text) -> void
-- oid: 58587  kind: FUNCTION  category: takeover

CREATE OR REPLACE FUNCTION dune.set_account_as_takeoverable(in_user_id text, in_new_user_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE accounts SET "user"=in_new_user_id, takeoverable=TRUE WHERE "user"=in_user_id;
END; $function$


-- set_all_inactive_players_in_farm_offline() -> void
-- oid: 58588  kind: FUNCTION  category: farm

CREATE OR REPLACE FUNCTION dune.set_all_inactive_players_in_farm_offline()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE player_state SET online_status = 'Offline', last_avatar_activity = current_timestamp WHERE online_status <> 'Offline' AND server_id NOT IN (SELECT * FROM active_server_ids);
END
$function$


-- set_battlegroup_close_date(in_close_date timestamp without time zone) -> timestamp without time zone
-- oid: 58589  kind: FUNCTION  category: battlegroup

CREATE OR REPLACE FUNCTION dune.set_battlegroup_close_date(in_close_date timestamp without time zone)
 RETURNS timestamp without time zone
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO farm_variables (one_row, battlegroup_close_date) VALUES (true,in_close_date)
	ON CONFLICT (one_row) DO UPDATE SET battlegroup_close_date = in_close_date;
	RETURN (select * from get_battlegroup_close_date());
END
$function$


-- set_character_import_state(in_fls_id text, in_state dune.transferimportstate) -> void
-- oid: 58590  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.set_character_import_state(in_fls_id text, in_state dune.transferimportstate)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO character_transfer_imports (fls_id, last_update, transfer_state)
	VALUES (in_fls_id, now(), in_state)
	ON CONFLICT (fls_id) DO UPDATE
	SET last_update = now(),
		transfer_state = EXCLUDED.transfer_state;
END;
$function$


-- set_character_name(in_account_id bigint, in_name text) -> void
-- oid: 58591  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.set_character_name(in_account_id bigint, in_name text)
 RETURNS void
 LANGUAGE sql
AS $function$
    update encrypted_player_state set encrypted_character_name=encrypt_user_data(in_name) where account_id=in_account_id;
$function$


-- set_demo_state(in_user_id text, in_demo_state dune.demostate) -> void
-- oid: 58592  kind: FUNCTION  category: schema_meta

CREATE OR REPLACE FUNCTION dune.set_demo_state(in_user_id text, in_demo_state dune.demostate)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	UPDATE demo_users
	SET demo_state = in_demo_state
	WHERE fls_id = in_user_id;
END
$function$


-- set_item_tracking_enabled(in_enabled boolean) -> void
-- oid: 58593  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.set_item_tracking_enabled(in_enabled boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    PERFORM set_config('dune.item_tracking_enabled', in_enabled::TEXT, false);
END;
$function$


-- set_player_faction_reputation(in_actor_id bigint, in_faction_id smallint, in_reputation_amount integer) -> void
-- oid: 58594  kind: FUNCTION  category: faction

CREATE OR REPLACE FUNCTION dune.set_player_faction_reputation(in_actor_id bigint, in_faction_id smallint, in_reputation_amount integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO player_faction_reputation (actor_id, faction_id, reputation_amount)
	VALUES (in_actor_id, in_faction_id, in_reputation_amount)
	ON CONFLICT (actor_id, faction_id)
		DO UPDATE
		SET reputation_amount = EXCLUDED.reputation_amount;
END
$function$


-- set_players_from_server_ids_offline(in_server_ids text[]) -> void
-- oid: 58595  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.set_players_from_server_ids_offline(in_server_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE player_state SET online_status = 'Offline', last_avatar_activity = current_timestamp WHERE online_status <> 'Offline' AND server_id = ANY(in_server_ids);
END
$function$


-- set_specialization_xp_and_level(in_player_id bigint, in_track_type dune.specializationtracktype, in_xp_amount integer, in_level real) -> void
-- oid: 58596  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.set_specialization_xp_and_level(in_player_id bigint, in_track_type dune.specializationtracktype, in_xp_amount integer, in_level real)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO specialization_tracks (player_id, track_type, xp_amount, level) VALUES (in_player_id, in_track_type, in_xp_amount, in_level)
	ON CONFLICT(player_id, track_type) DO UPDATE SET xp_amount = in_xp_amount, level = in_level;
END $function$


-- setup_user_data_encryption(IN in_enable boolean) -> void
-- oid: 58597  kind: PROCEDURE  category: encryption

CREATE OR REPLACE PROCEDURE dune.setup_user_data_encryption(IN in_enable boolean)
 LANGUAGE plpgsql
AS $procedure$
declare
	encryption_key Text;
    encryption_key_hash bytea;
    stored_encryption_key_hash bytea;
	stored_encryption_status UserDataEncryptionStatus;
begin
	select current_setting('funcom.user_data_encryption_key', true) into encryption_key;
    -- might be null if the encryption_key is null
    select ext.digest(encryption_key, 'md5') into encryption_key_hash;
    -- might be null if the stored data is not encrypted
    select get_stored_user_data_encryption_key_hash() into stored_encryption_key_hash;

	-- should never be null
	select get_stored_user_data_encryption_status() into stored_encryption_status;

    drop index if exists encrypted_player_state_character_name_gin;

	if stored_encryption_status = 'Disabled' then
		if in_enable then
			perform _user_data_encryption_setup_enabled(encryption_key_hash);
			perform _user_data_encryption_initially_encrypt_existing_data();
		else
			-- technically we shouldn't do anything but let's just validate a few things
			-- we not replacing get_stored_user_data_encryption_key_hash()
			if (select get_stored_user_data_encryption_key_hash()) is not null then
				-- should have been filtered by the main setup function
				raise exception 'The data is encrypted, should use the taint version';
			end if;

			if (select get_stored_user_data_encryption_taint_xmax()) is not null then
				-- should have been filtered by the main setup function
				raise exception 'The data is tainted, should use the taint version';
			end if;
		end if;
	elseif stored_encryption_status = 'Tainted' then
		-- doesn't matter what we want, we get the tainted version
		perform _user_data_encryption_setup_tainted();
	else -- Enabled
		if in_enable is null or in_enable then
			if encryption_key_hash = stored_encryption_key_hash then -- and our key is the same
				perform _user_data_encryption_setup_enabled(encryption_key_hash);
			else
				raise warning 'User-data encryption requested but the data is already encrypted with a different key';
				perform _user_data_encryption_setup_tainted();
			end if;
		else
			raise warning 'User-data encryption not requested but the data is already encrypted';
			perform _user_data_encryption_setup_tainted();
		end if;
	end if;

	commit;

	CREATE INDEX encrypted_player_state_character_name_gin ON encrypted_player_state USING GIN((decrypt_user_data(encrypted_character_name)) ext.gin_trgm_ops);
end
$procedure$


-- store_backup_vehicle(in_vehicle_id bigint, in_account_id bigint, in_customization_id text) -> void
-- oid: 58598  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.store_backup_vehicle(in_vehicle_id bigint, in_account_id bigint, in_customization_id text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    -- Lock the backup_vehicles table exclusively to prevent race conditions.
    LOCK TABLE backup_vehicles IN EXCLUSIVE MODE;
 
     -- Check if the vehicle already belongs to an actor state
    PERFORM 1 FROM actor_state WHERE actor_id = in_vehicle_id;
    IF FOUND THEN
        RAISE EXCEPTION 'Trying to backup vehicle % that already has an actor state.', in_vehicle_id;
    END IF;
 
    -- Check if the vehicle is already in backup_vehicles.
    PERFORM 1 FROM backup_vehicles WHERE vehicle_id = in_vehicle_id;
    IF FOUND THEN
        RAISE EXCEPTION 'Vehicle % is already stored as a backup.', in_vehicle_id;
    END IF;
 
    -- Check if the vehicle exists in the actors table.
    PERFORM 1 FROM actors WHERE id = in_vehicle_id;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Vehicle % does not exist in actors.', in_vehicle_id;
    END IF;
 
    -- Insert the vehicle into backup_vehicles.
    INSERT INTO backup_vehicles (account_id, vehicle_id, customization_id) 
	VALUES (in_account_id, in_vehicle_id, in_customization_id);
    RAISE INFO 'Inserted vehicle % into backup_vehicles for account %.', in_vehicle_id, in_account_id;
 
    -- Mark the actor as belonging to the vehicle backup tool
    INSERT INTO actor_state("actor_id", "state") VALUES(in_vehicle_id, 'VehicleBackup');
    RAISE INFO 'Inserted vehicle % into actor_state with VehicleBackup state', in_vehicle_id;
    
    PERFORM verify_item_dup_backup_tool(in_account_id, in_vehicle_id, 'item_dup_on_store_vbt');
 
END;
$function$


-- store_recovered_vehicle(in_vehicle_id bigint, in_chassis_durability real, in_customization_id text, in_is_migration boolean) -> void
-- oid: 58599  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.store_recovered_vehicle(in_vehicle_id bigint, in_chassis_durability real, in_customization_id text, in_is_migration boolean DEFAULT false)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    owner_id BIGINT;
	player_id BIGINT;
	vehicle_class TEXT;
	vehicle_name TEXT;
BEGIN
    -- Lock the recovered_vehicles table exclusively to prevent race conditions.
    LOCK TABLE recovered_vehicles IN EXCLUSIVE MODE;
 
     -- Check if the vehicle already belongs to an actor state
    PERFORM 1 FROM actor_state WHERE actor_id = in_vehicle_id;
    IF FOUND THEN
        RAISE EXCEPTION 'Trying to store recovered vehicle % that already has an actor state.', in_vehicle_id;
    END IF;
 
    -- Check if the vehicle is already in recovered_vehicles.
    PERFORM 1 FROM recovered_vehicles WHERE vehicle_id = in_vehicle_id;
    IF FOUND THEN
        RAISE EXCEPTION 'Vehicle % is already recovered.', in_vehicle_id;
    END IF;
 
    -- Check if the vehicle exists in the actors table.
    SELECT class INTO vehicle_class
	FROM actors 
	WHERE id = in_vehicle_id
	LIMIT 1;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Vehicle % does not exist in actors.', in_vehicle_id;
    END IF;
 
	-- get the vehicles owner account id
	SELECT a.owner_account_id, r.player_id, p.actor_name INTO owner_id, player_id, vehicle_name
	FROM actors a
	INNER JOIN permission_actor_rank r ON r.player_id = a.id
	INNER JOIN permission_actor p ON p.actor_id = r.permission_actor_id
	WHERE r.permission_actor_id = in_vehicle_id
	AND r.rank = 1::smallint
	LIMIT 1;
	
	IF NOT FOUND THEN
		RAISE EXCEPTION 'No account_id found for vehicle: %', in_vehicle_id;
	END IF;
	
    -- Insert the vehicle into recovered_vehicles.
    INSERT INTO recovered_vehicles(account_id, vehicle_id, chassis_durability, vehicle_name, customization_id, migrated) 
	VALUES (owner_id, in_vehicle_id, in_chassis_durability, vehicle_name, in_customization_id, in_is_migration);
    RAISE INFO 'Inserted vehicle % into recovered_vehicles for account %.', in_vehicle_id, owner_id;
 
    -- Mark the actor as belonging to the vehicle recovery tool
    INSERT INTO actor_state("actor_id", "state") VALUES(in_vehicle_id, 'VehicleRecovery');
    RAISE INFO 'Inserted vehicle % into actor_state with VehicleRecovery state', in_vehicle_id;
    
    PERFORM pg_notify('vehicle_recovery_notify_channel', FORMAT('stored#{"PlayerId":%s, "VehicleId":%s, "VehicleClass":"%s", "VehicleName":"%s", "VehicleCustomizationId":"%s", "TimeStored":"%s", "ChassisDurability":%s, "bIsMigrated":%s}', player_id, in_vehicle_id, vehicle_class, vehicle_name, in_customization_id, NOW()  AT TIME ZONE 'UTC', in_chassis_durability, CASE WHEN in_is_migration THEN 'true' ELSE 'false' END) );
 
END;
$function$


-- store_recovered_vehicles_wiped_before_spawn(in_vehicle_ids bigint[], in_delete_items boolean) -> void
-- oid: 58600  kind: FUNCTION  category: vehicle

CREATE OR REPLACE FUNCTION dune.store_recovered_vehicles_wiped_before_spawn(in_vehicle_ids bigint[], in_delete_items boolean DEFAULT true)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	to_store RECORD;
BEGIN
	FOR to_store IN
		SELECT v.id AS vehicle_id, a.properties -> (regexp_replace("class", '^.*\.', '')) ->> 'm_CustomizationId' AS customization_id
		FROM actors a
		INNER JOIN vehicles v ON a.id = v.id
		INNER JOIN permission_actor p ON a.id = p.actor_id
		INNER JOIN permission_actor_rank r ON p.actor_id = r.permission_actor_id
		WHERE v.id = ANY(in_vehicle_ids)
		AND r.rank = 1::smallint
		AND NOT EXISTS
		(
			SELECT 1 FROM actor_state
			WHERE actor_state.actor_id = v.id
			AND actor_state.state IS DISTINCT FROM 'Default'
		)
	LOOP
		-- remove all items in all inventories of that vehicle
		if in_delete_items then
			PERFORM delete_items_from_actor(to_store.vehicle_id);
		end if;
	
		-- note: storing hardcoded chassis durability because its not available from pure database :(
		-- this will only be shown incorrectly in UI though, the spawned vehicle will get the correct value
		PERFORM store_recovered_vehicle(to_store.vehicle_id, 0.25, to_store.customization_id);

		-- remove permissions so the stored vehicles don't count towards the vehicle limit (the owner is preserved in the store function above)
		PERFORM permission_actor_destroy(to_store.vehicle_id);
	END LOOP;
END
$function$


-- takeover_account(in_user_to_takeover text, in_current_user text) -> void
-- oid: 58601  kind: FUNCTION  category: takeover

CREATE OR REPLACE FUNCTION dune.takeover_account(in_user_to_takeover text, in_current_user text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	current_funcom_id ByteA;
	current_account_id BigInt;

	takeover_funcom_id ByteA;
	takeover_account_id BigInt;
BEGIN
	select "encrypted_funcom_id", "id" into current_funcom_id, current_account_id
		from encrypted_accounts WHERE "user"=in_current_user;

	select "encrypted_funcom_id", "id" into takeover_funcom_id, takeover_account_id
		from encrypted_accounts WHERE "user"=in_user_to_takeover;

	-- Account swap
	UPDATE encrypted_accounts SET "user"=in_current_user || 'TempTakeover' WHERE "id"=current_account_id;
	UPDATE encrypted_accounts SET "user"=in_current_user, "encrypted_funcom_id"=current_funcom_id WHERE "id"=takeover_account_id;
	UPDATE encrypted_accounts SET "user"=in_user_to_takeover, "encrypted_funcom_id"=takeover_funcom_id WHERE "id"=current_account_id;
END
$function$


-- taxation_emit_invoices(new_tax_invoices dune.taxinvoicedata[]) -> void
-- oid: 58602  kind: FUNCTION  category: taxation

CREATE OR REPLACE FUNCTION dune.taxation_emit_invoices(new_tax_invoices dune.taxinvoicedata[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	invoice_ids BIGINT[];
    new_invoice_id BIGINT;
    new_tax_invoice TaxInvoiceData;
BEGIN
    IF array_length(new_tax_invoices, 1) > 0 THEN
		FOREACH new_tax_invoice IN ARRAY new_tax_invoices LOOP
        
            INSERT INTO tax_invoice("totem_id", "reference_timespan", "invoice_status", "amount") 
                VALUES(new_tax_invoice.totem_id, new_tax_invoice.reference_timespan, new_tax_invoice.invoice_status, new_tax_invoice.amount) RETURNING id INTO new_invoice_id;
            
            invoice_ids := array_append(invoice_ids, new_invoice_id);
		END LOOP;

        PERFORM pg_notify('taxation_notify_channel', format('emit_invoices|{"InvoiceIds" : %s, "InvoiceData" : %s}', to_json(invoice_ids)::text, to_json(new_tax_invoices)::text));
	END IF;
END
$function$


-- taxation_get_all_invoices_for_player(in_player_id bigint) -> TABLE(id bigint, totem_id bigint, reference_timestamp bigint, invoice_status smallint, amount integer, actor_name text)
-- oid: 58603  kind: FUNCTION  category: taxation

CREATE OR REPLACE FUNCTION dune.taxation_get_all_invoices_for_player(in_player_id bigint)
 RETURNS TABLE(id bigint, totem_id bigint, reference_timestamp bigint, invoice_status smallint, amount integer, actor_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT tax_invoice.id, tax_invoice.totem_id, tax_invoice.reference_timespan, tax_invoice.invoice_status, tax_invoice.amount, permission_actor.actor_name
	FROM tax_invoice 
	JOIN permission_actor ON permission_actor.actor_id = tax_invoice.totem_id
    JOIN permission_actor_rank ON permission_actor.actor_id = permission_actor_rank.permission_actor_id
    WHERE permission_actor_rank.player_id = in_player_id    
    ORDER BY tax_invoice.reference_timespan;
END
$function$


-- taxation_get_all_invoices_for_server(map_name text, in_dimension_index integer, in_partition_id bigint) -> TABLE(id bigint, totem_id bigint, reference_timestamp bigint, invoice_status smallint, amount integer, actor_name text)
-- oid: 58604  kind: FUNCTION  category: taxation

CREATE OR REPLACE FUNCTION dune.taxation_get_all_invoices_for_server(map_name text, in_dimension_index integer, in_partition_id bigint)
 RETURNS TABLE(id bigint, totem_id bigint, reference_timestamp bigint, invoice_status smallint, amount integer, actor_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT tax_invoice.id, actors.id, tax_invoice.reference_timespan, tax_invoice.invoice_status, tax_invoice.amount, permission_actor.actor_name
	FROM tax_invoice 
    JOIN permission_actor ON permission_actor.actor_id = tax_invoice.totem_id
	JOIN actors ON actors.id = tax_invoice.totem_id 
    WHERE actors.map = map_name
		AND actors.dimension_index = in_dimension_index
		AND ((in_partition_id = 0 AND actors.partition_id IS NULL) OR actors.partition_id = in_partition_id)
    ORDER BY tax_invoice.reference_timespan;
END
$function$


-- taxation_get_all_invoices_for_totem(in_totem_id bigint) -> TABLE(id bigint, totem_id bigint, reference_timestamp bigint, invoice_status smallint, amount integer, actor_name text)
-- oid: 58605  kind: FUNCTION  category: taxation

CREATE OR REPLACE FUNCTION dune.taxation_get_all_invoices_for_totem(in_totem_id bigint)
 RETURNS TABLE(id bigint, totem_id bigint, reference_timestamp bigint, invoice_status smallint, amount integer, actor_name text)
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN QUERY SELECT tax_invoice.id, actors.id, tax_invoice.reference_timespan, tax_invoice.invoice_status, tax_invoice.amount, permission_actor.actor_name
	FROM tax_invoice 
    JOIN permission_actor ON permission_actor.actor_id = tax_invoice.totem_id
	JOIN actors ON actors.id = tax_invoice.totem_id 
    WHERE actors.id = in_totem_id
    ORDER BY tax_invoice.reference_timespan;
END
$function$


-- taxation_pay_invoice(invoice_id bigint, paid_invoice_status smallint) -> bigint
-- oid: 58606  kind: FUNCTION  category: taxation

CREATE OR REPLACE FUNCTION dune.taxation_pay_invoice(invoice_id bigint, paid_invoice_status smallint)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
	found_invoice_id BIGINT;
BEGIN
    UPDATE tax_invoice SET invoice_status = paid_invoice_status WHERE tax_invoice.id = invoice_id AND tax_invoice.invoice_status != paid_invoice_status RETURNING id INTO found_invoice_id;
    if found_invoice_id IS NOT NULL THEN
        PERFORM pg_notify('taxation_notify_channel', format('pay_invoice|{"InvoiceId" : %s }', invoice_id));
    END IF;
    RETURN found_invoice_id;
END
$function$


-- taxation_remove_invoices(invoices_to_remove bigint[]) -> void
-- oid: 58607  kind: FUNCTION  category: taxation

CREATE OR REPLACE FUNCTION dune.taxation_remove_invoices(invoices_to_remove bigint[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    DELETE FROM tax_invoice WHERE id = ANY(invoices_to_remove);
    PERFORM pg_notify('taxation_notify_channel', format('remove_invoice|{"InvoiceIds" : %s}', to_json(invoices_to_remove)::text));
END
$function$


-- taxation_remove_invoices_from_totem(totem_actor_id bigint) -> void
-- oid: 58608  kind: FUNCTION  category: taxation

CREATE OR REPLACE FUNCTION dune.taxation_remove_invoices_from_totem(totem_actor_id bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	invoice_ids BIGINT[];
BEGIN
    SELECT array_agg(id) from tax_invoice into invoice_ids WHERE totem_id = totem_actor_id;

    DELETE FROM tax_invoice WHERE totem_id = totem_actor_id;
    PERFORM pg_notify('taxation_notify_channel', format('remove_invoice|{"InvoiceIds" : %s}', to_json(invoice_ids)::text));
END
$function$


-- taxation_update_invoice_status(invoices_to_overdue bigint[], invoices_to_defaulted bigint[], overdue_invoice_status smallint, defaulted_invoice_status smallint) -> void
-- oid: 58609  kind: FUNCTION  category: taxation

CREATE OR REPLACE FUNCTION dune.taxation_update_invoice_status(invoices_to_overdue bigint[], invoices_to_defaulted bigint[], overdue_invoice_status smallint, defaulted_invoice_status smallint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    IF array_length(invoices_to_overdue, 1) > 0 THEN
        UPDATE tax_invoice SET invoice_status = overdue_invoice_status WHERE id = ANY(invoices_to_overdue);
        PERFORM pg_notify('taxation_notify_channel', format('update_invoice_status|{"InvoiceStatus" : %s, "InvoiceIds" : %s}', overdue_invoice_status, to_json(invoices_to_overdue)::text));
    END IF;

    IF array_length(invoices_to_defaulted, 1) > 0 THEN
        UPDATE tax_invoice SET invoice_status = defaulted_invoice_status WHERE id = ANY(invoices_to_defaulted);
        PERFORM pg_notify('taxation_notify_channel', format('update_invoice_status|{"InvoiceStatus" : %s, "InvoiceIds" : %s}', defaulted_invoice_status, to_json(invoices_to_defaulted)::text));
    END IF;
END
$function$


-- try_prime_spicefield(in_source_server_id text, in_spicefield_id integer) -> boolean
-- oid: 58610  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.try_prime_spicefield(in_source_server_id text, in_spicefield_id integer)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
begin
	perform spicefield_type_id from spicefield_types as t
		where t.spicefield_type_id = in_spicefield_id and t.is_spawning_active is true and t.current_globally_primed < t.max_globally_primed and t.current_globally_active < t.max_globally_active;
	if not found then
		return false;
	end if;

	update spicefield_server_availability
	set inactive_fields_of_type = inactive_fields_of_type - 1
	where server_id = in_source_server_id and spicefield_type_id = in_spicefield_id;

	update spicefield_types
	set current_globally_primed = current_globally_primed + 1
	where spicefield_type_id = in_spicefield_id;

	return true;
	commit;
end $function$


-- try_restart_spicefield(in_server_id text, in_spicefield_type_id integer) -> boolean
-- oid: 58611  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.try_restart_spicefield(in_server_id text, in_spicefield_type_id integer)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
 BEGIN
	PERFORM spicefield_type_id
	FROM spicefield_types AS t
	WHERE t.spicefield_type_id = in_spicefield_type_id AND t.is_spawning_active IS TRUE;
	IF NOT FOUND THEN
		RETURN FALSE;
	END IF;

	UPDATE spicefield_server_availability
	SET inactive_fields_of_type = inactive_fields_of_type - 1
	WHERE server_id = in_server_id AND spicefield_type_id = in_spicefield_type_id;

	UPDATE spicefield_types
	SET current_globally_active = current_globally_active + 1
	WHERE spicefield_type_id = in_spicefield_type_id;

	RETURN TRUE;
END
$function$


-- try_spawn_spicefield(in_source_server_id text, in_spicefield_id integer) -> boolean
-- oid: 58612  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.try_spawn_spicefield(in_source_server_id text, in_spicefield_id integer)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
begin
	perform spicefield_type_id from spicefield_types as t
		where t.spicefield_type_id = in_spicefield_id and t.is_spawning_active is true and t.current_globally_active < t.max_globally_active;
	if not found then
		return false;
	end if;

	update spicefield_server_availability
	set requested_spawned_of_type = requested_spawned_of_type - 1
	where server_id = in_source_server_id and spicefield_type_id = in_spicefield_id;

	update spicefield_types
	set current_globally_active = current_globally_active + 1, current_globally_primed = current_globally_primed -1
	where spicefield_type_id = in_spicefield_id;

	return true;
	commit;
end $function$


-- try_update_exchange_categories_hash(in_new_hash integer) -> TABLE(item_template_id text, mask integer, depth smallint)
-- oid: 58613  kind: FUNCTION  category: exchange

CREATE OR REPLACE FUNCTION dune.try_update_exchange_categories_hash(in_new_hash integer)
 RETURNS TABLE(item_template_id text, mask integer, depth smallint)
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
	IF NOT EXISTS (SELECT * FROM dune_exchange_categories_hash) FOR UPDATE THEN
		INSERT INTO dune_exchange_categories_hash(id, hash) VALUES(1, in_new_hash);
		RETURN;
	END IF;
	IF NOT in_new_hash IN (SELECT hash FROM dune_exchange_categories_hash FOR SHARE) THEN
		UPDATE dune_exchange_categories_hash SET hash = in_new_hash WHERE dune_exchange_categories_hash.id = 1;
		RETURN QUERY SELECT DISTINCT template_id, category_mask, category_depth FROM dune_exchange_orders;
	END IF;
	RETURN;
END
$function$


-- unassign_partition(in_server_id text) -> boolean
-- oid: 58614  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.unassign_partition(in_server_id text)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
	v_affected_rows BigInt;
BEGIN
	UPDATE world_partition SET server_id = null WHERE server_id = in_server_id;
	get diagnostics v_affected_rows = ROW_COUNT;

	-- If we didn't actually unassign anything, we have no need to trigger the notification (as nothing changed)
	if v_affected_rows > 0 then
		NOTIFY world_partition_update;
		return true;
	end if;
	return false;
END $function$


-- update_communinet_player_channel(in_account_id bigint, in_channel_name text, in_is_tuned boolean) -> void
-- oid: 58615  kind: FUNCTION  category: communinet

CREATE OR REPLACE FUNCTION dune.update_communinet_player_channel(in_account_id bigint, in_channel_name text, in_is_tuned boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO communinet_player_channels(account_id, channel_name, is_tuned) VALUES (in_account_id, in_channel_name, in_is_tuned)
	ON CONFLICT(account_id, channel_name) DO UPDATE SET is_tuned = in_is_tuned WHERE communinet_player_channels.account_id = in_account_id AND communinet_player_channels.channel_name = in_channel_name;
END $function$


-- update_communinet_player_data(in_account_id bigint, in_is_active boolean, in_selected_channel_name text) -> void
-- oid: 58616  kind: FUNCTION  category: communinet

CREATE OR REPLACE FUNCTION dune.update_communinet_player_data(in_account_id bigint, in_is_active boolean, in_selected_channel_name text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO communinet_player(account_id, is_active, selected_channel_name) VALUES (in_account_id, in_is_active, in_selected_channel_name)
	ON CONFLICT(account_id) DO UPDATE SET is_active = in_is_active, selected_channel_name = in_selected_channel_name WHERE communinet_player.account_id = in_account_id;
END $function$


-- update_consumed_per_player_lore(in_actor_id bigint, in_consumed_bit_array bit, in_use_temporary boolean) -> void
-- oid: 58617  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.update_consumed_per_player_lore(in_actor_id bigint, in_consumed_bit_array bit, in_use_temporary boolean)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF in_use_temporary THEN
    	INSERT INTO consumed_temporary_per_player_lore(actor_id, consumed_bit_array)
    	VALUES(in_actor_id, in_consumed_bit_array)
    	ON CONFLICT (actor_id)
    	DO UPDATE SET
        	consumed_bit_array = in_consumed_bit_array;
	ELSE
    	INSERT INTO consumed_per_player_lore(actor_id, consumed_bit_array)
    	VALUES(in_actor_id, in_consumed_bit_array)
    	ON CONFLICT (actor_id)
    	DO UPDATE SET
        	consumed_bit_array = in_consumed_bit_array;
	END IF;
END; $function$


-- update_coriolis_for_player(in_controller_id bigint, OUT out_was_coriolis_processed boolean) -> boolean
-- oid: 58618  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.update_coriolis_for_player(in_controller_id bigint, OUT out_was_coriolis_processed boolean)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
begin
	SELECT is_coriolis_processed INTO out_was_coriolis_processed
		FROM player_state WHERE player_controller_id = in_controller_id;
	UPDATE player_state SET is_coriolis_processed = TRUE WHERE player_controller_id = in_controller_id;
end
$function$


-- update_death_location(in_pawn dune.actordescription, in_server_info dune.serverinfo, in_life_state dune.playerlifestate) -> void
-- oid: 58619  kind: FUNCTION  category: player_persistence

CREATE OR REPLACE FUNCTION dune.update_death_location(in_pawn dune.actordescription, in_server_info dune.serverinfo, in_life_state dune.playerlifestate)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    IF in_life_state IS NOT NULL THEN
        UPDATE encrypted_player_state
        SET
            life_state = in_life_state,
            death_location = CASE
                WHEN in_life_state != 'Alive'::PlayerLifeState THEN ((in_pawn).transform.location, (in_server_info).map, (in_server_info).dimension_index)::DeathLocation
                ELSE null
            END
        WHERE player_pawn_id = in_pawn.id;
    END IF;
END;
$function$


-- update_farm_state(in_server_id text, in_outgoing_s2s_connections integer, in_incoming_s2s_connections integer, in_connected_players integer, in_farm_id text, in_igw_addr inet, in_igw_port integer, in_ready boolean, in_alive boolean, in_game_addr inet, in_game_port integer, in_map text, in_revision integer) -> void
-- oid: 58620  kind: FUNCTION  category: farm

CREATE OR REPLACE FUNCTION dune.update_farm_state(in_server_id text, in_outgoing_s2s_connections integer, in_incoming_s2s_connections integer, in_connected_players integer, in_farm_id text, in_igw_addr inet, in_igw_port integer, in_ready boolean, in_alive boolean, in_game_addr inet, in_game_port integer, in_map text, in_revision integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO farm_state
		(server_id, outgoing_s2s_connections, incoming_s2s_connections, connected_players, farm_id, igw_addr, igw_port, ready, alive, game_addr, game_port, map, revision)
	VALUES
		(in_server_id, in_outgoing_s2s_connections, in_incoming_s2s_connections, in_connected_players, in_farm_id, in_igw_addr, in_igw_port, in_ready, in_alive, in_game_addr, in_game_port, in_map, in_revision)
	ON CONFLICT(server_id) DO UPDATE SET
		outgoing_s2s_connections=in_outgoing_s2s_connections, incoming_s2s_connections=in_incoming_s2s_connections, connected_players=in_connected_players, farm_id=in_farm_id, igw_addr=in_igw_addr, igw_port=in_igw_port, ready=in_ready, alive=in_alive, game_addr=in_game_addr, game_port=in_game_port, map=in_map, revision=in_revision
	WHERE
		farm_state.server_id = in_server_id;
END
$function$


-- update_global_spice_field_rules(in_max_globally_primed integer, in_max_globally_active integer, in_spicefield_type_id integer) -> void
-- oid: 58621  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.update_global_spice_field_rules(in_max_globally_primed integer, in_max_globally_active integer, in_spicefield_type_id integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE spicefield_types SET max_globally_primed = in_max_globally_primed, max_globally_active = in_max_globally_active WHERE spicefield_type_id = in_spicefield_type_id;
END; $function$


-- update_inventories_data(in_inventory_data_list dune.inventorydata[]) -> void
-- oid: 58622  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.update_inventories_data(in_inventory_data_list dune.inventorydata[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	UPDATE inventories
    SET inventory_type = (u).inventory_type, max_item_count = (u).max_item_count, max_item_volume = (u).max_item_volume
        FROM (SELECT inventory_id, inventory_type, max_item_count, max_item_volume FROM UNNEST(in_inventory_data_list)) u
    WHERE id = (u).inventory_id;
END
$function$


-- update_inventory(in_delete_list bigint[], in_stack_update dune.itemstackupdate[], in_quality_update dune.itemqualityupdate[], in_stat_update dune.itemstatupdate[], in_item_locations dune.inventoryitemlocation[]) -> void
-- oid: 58623  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.update_inventory(in_delete_list bigint[], in_stack_update dune.itemstackupdate[], in_quality_update dune.itemqualityupdate[], in_stat_update dune.itemstatupdate[], in_item_locations dune.inventoryitemlocation[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    delete_item_id BIGINT;
BEGIN
    -- log item movement
    PERFORM _add_item_trace_log('update_inventory_locations', in_item_locations);
    
	-- delete items
	PERFORM delete_items(in_delete_list);

	-- update item stacks
	UPDATE items SET "stack_size" = (u).stack_size FROM (SELECT stack_size, item_id FROM UNNEST(in_stack_update)) u WHERE "id" = (u).item_id;

	-- update item quality
	UPDATE items SET "quality_level" = (u).quality_level FROM (SELECT quality_level, item_id FROM UNNEST(in_quality_update)) u WHERE "id" = (u).item_id;

	-- update stats
	UPDATE items SET "stats" = "stats" || (u).value FROM (SELECT value, item_id FROM UNNEST(in_stat_update)) u WHERE "id" = (u).item_id;

    UPDATE items
    SET inventory_id = (item).inventory_id, position_index = (item).position_index
        FROM (SELECT item_id, inventory_id, position_index FROM UNNEST(in_item_locations)) item
    WHERE id = (item).item_id;
END
$function$


-- update_item_locations(in_item_locations dune.inventoryitemlocation[]) -> void
-- oid: 58624  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.update_item_locations(in_item_locations dune.inventoryitemlocation[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE items
    SET inventory_id = (item).inventory_id, position_index = (item).position_index
        FROM (SELECT item_id, inventory_id, position_index FROM UNNEST(in_item_locations)) item
    WHERE id = (item).item_id;

	-- log item tracking
    PERFORM _add_item_trace_log('update_item_locations', in_item_locations);
END
$function$


-- update_journey_story_ids(old_story_ids text[], new_story_ids text[]) -> void
-- oid: 58625  kind: FUNCTION  category: journey_progression

CREATE OR REPLACE FUNCTION dune.update_journey_story_ids(old_story_ids text[], new_story_ids text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
	index INT;
BEGIN
	IF array_length(old_story_ids, 1) != array_length(new_story_ids, 1) THEN
		RAISE EXCEPTION 'The length of the array of old IDs does not match the length of the array of new IDs - they must both have the same length';
	END IF;

	FOR index in 1 .. array_length(old_story_ids, 1) LOOP
		UPDATE journey_story_node
		SET story_node_id = new_story_ids[index]
		WHERE story_node_id = old_story_ids[index];

		UPDATE journey_story_node_cooldown
		SET story_node_id = new_story_ids[index]
		WHERE story_node_id = old_story_ids[index];
	END LOOP;
END;
$function$


-- update_marker_ids(in_old_ids integer[], in_new_ids integer[]) -> void
-- oid: 58626  kind: FUNCTION  category: markers

CREATE OR REPLACE FUNCTION dune.update_marker_ids(in_old_ids integer[], in_new_ids integer[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE markers SET marker_hash_id = data_table.new_id, marker.z = 0 from (select unnest(in_old_ids) as old_id, unnest(in_new_ids) as new_id) as data_table WHERE markers.marker_hash_id = data_table.old_id;
END; $function$


-- update_partition_labels(in_allow_overwrite boolean) -> void
-- oid: 58627  kind: FUNCTION  category: partition

CREATE OR REPLACE FUNCTION dune.update_partition_labels(in_allow_overwrite boolean DEFAULT true)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	-- Compute candidate labels per-partition using the helper and apply them where appropriate
	UPDATE world_partition wp
	SET label = sub.new_label
	FROM (
		SELECT partition_id, determine_partition_label(map, dimension_index, label, in_allow_overwrite, partition_id) AS new_label
		FROM world_partition
	) AS sub
	WHERE wp.partition_id = sub.partition_id
	  AND (wp.label IS NULL OR in_allow_overwrite = true)
	  AND sub.new_label IS NOT NULL;

	-- The default is `MAP_DIMENSION`
	-- Only set it for map, dimension combos that have a single partition (label must be unique), and for labels not already touched
	UPDATE world_partition SET label = map || '_' || dimension_index
	from (
		select grouping.partition_ids[1] as partition_id
			from (
				select count(*) as count, array_agg(partition_id) as partition_ids
				from world_partition
				group by map, dimension_index
			) as grouping
			where grouping.count = 1
	) as partition
	where
		world_partition.partition_id = partition.partition_id
		and label is null;
END;
$function$


-- update_party_platform_session(in_party_id bigint, in_platform_session_id text, in_platform_name text) -> void
-- oid: 58628  kind: FUNCTION  category: party

CREATE OR REPLACE FUNCTION dune.update_party_platform_session(in_party_id bigint, in_platform_session_id text, in_platform_name text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF in_platform_session_id IS NULL OR length(in_platform_session_id) = 0 THEN
        RAISE EXCEPTION 'in_platform_session_id must not be empty';
    END IF;

	IF in_platform_name IS NULL OR length(in_platform_name) = 0 THEN
        RAISE EXCEPTION 'platform_name must not be empty';
    END IF;
	
	INSERT INTO platform_parties_mapping (platform_session_id, platform_name, dune_party_id)
	VALUES (in_platform_session_id, in_platform_name, in_party_id)
	ON CONFLICT (platform_name, dune_party_id)
	DO UPDATE SET platform_session_id = EXCLUDED.platform_session_id;

	PERFORM pg_notify('party_notify_channel', format('update_party_platform_id#{"PartyId" : %s, "PlatformSessionId" : "%s", "PlatformName" : "%s"}', 
		in_party_id, in_platform_session_id, in_platform_name));
END
$function$


-- update_player_tags(in_account_id bigint, tags_to_add text[], tags_to_remove text[]) -> void
-- oid: 58629  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.update_player_tags(in_account_id bigint, tags_to_add text[], tags_to_remove text[])
 RETURNS void
 LANGUAGE sql
AS $function$
    insert into player_tags("account_id", "tag") select in_account_id, unnest(tags_to_add) on conflict do nothing;
    delete from player_tags where account_id = in_account_id and tag = ANY(tags_to_remove);
$function$


-- update_removed_items_and_recipes(items_removed text[], recipes_removed text[]) -> void
-- oid: 58630  kind: FUNCTION  category: items_purge

CREATE OR REPLACE FUNCTION dune.update_removed_items_and_recipes(items_removed text[], recipes_removed text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
    insert into removed_items (name) select unnest(items_removed);
    insert into removed_recipes (name) select unnest(recipes_removed);
end;
$function$


-- update_resourcefield_states(in_map text, in_dimension_index integer, in_field_kind_id smallint, in_field_states dune.resourcefieldstateentry[]) -> void
-- oid: 58631  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.update_resourcefield_states(in_map text, in_dimension_index integer, in_field_kind_id smallint, in_field_states dune.resourcefieldstateentry[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	INSERT INTO resourcefield_state(map, dimension_index, field_kind_id, field_id, spawn_time, value_remaining) 
	SELECT in_map, in_dimension_index, in_field_kind_id, * FROM UNNEST(in_field_states)
	ON CONFLICT("field_id", "map", "dimension_index") DO
	UPDATE
	SET value_remaining = EXCLUDED.value_remaining 
	WHERE resourcefield_state.field_id = EXCLUDED.field_id AND resourcefield_state.map = in_map AND resourcefield_state.dimension_index = in_dimension_index;
END
$function$


-- update_respawn_locations(player_id bigint, respawn_locations dune.respawnlocation[]) -> void
-- oid: 58632  kind: FUNCTION  category: spawner

CREATE OR REPLACE FUNCTION dune.update_respawn_locations(player_id bigint, respawn_locations dune.respawnlocation[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    PERFORM 1
    FROM unnest(respawn_locations) AS loc
        WHERE
        (loc.locator).type != 'Invalid' AND NOT (
            (loc.locator).type = 'Transform' AND (loc.locator).transform IS NOT NULL AND (loc.locator).actor_id IS NULL AND (loc.locator).name IS NULL
            OR
            (loc.locator).type = 'PersistentActor' AND (loc.locator).actor_id IS NOT NULL AND (loc.locator).transform IS NULL AND (loc.locator).name IS NULL
            OR
            (loc.locator).type = 'StaticLocatorName' AND (loc.locator).name IS NOT NULL AND (loc.locator).transform IS NULL AND (loc.locator).actor_id IS NULL
        );

    IF FOUND THEN
        RAISE EXCEPTION 'Invalid respawn location in input array. Check locator type and associated fields.';
    END IF;

    WITH
    updated_respawn_locations AS (
        SELECT
            id,
            "group",
            (locator).transform AS locator_transform,
            (locator).actor_id AS locator_actor_id,
            (locator).name AS locator_name,
            (locator).name_index AS locator_name_index,
            map,
            dimension,
            last_used_timestamp
        FROM unnest(respawn_locations) AS updated
    ),
    delete_missing_respawn_locations AS (
        DELETE FROM player_respawn_locations AS existing
        WHERE NOT EXISTS (
            SELECT 1 FROM updated_respawn_locations AS updated WHERE updated.id = existing.id
        )
        AND existing.account_id = player_id
    )
    INSERT INTO player_respawn_locations(
        "id", "account_id", "group", "locator_transform", "locator_actor_id", "locator_name", "locator_name_index", "map", "dimension", "last_used_timestamp"
    )
    SELECT
        up.id, player_id, up.group, up.locator_transform, up.locator_actor_id, up.locator_name, up.locator_name_index, up.map, up.dimension, up.last_used_timestamp
    FROM updated_respawn_locations AS up
    WHERE up.locator_actor_id IS NULL OR EXISTS (
        SELECT 1 FROM actors WHERE id = up.locator_actor_id
    )
    ON CONFLICT ("id", "account_id")
    DO UPDATE SET
        "account_id" = EXCLUDED.account_id,
        "group" = EXCLUDED.group,
        "locator_transform" = EXCLUDED.locator_transform,
        "locator_actor_id" = EXCLUDED.locator_actor_id,
        "locator_name" = EXCLUDED.locator_name,
        "locator_name_index" = EXCLUDED.locator_name_index,
        "map" = EXCLUDED.map,
        "dimension" = EXCLUDED.dimension,
        "last_used_timestamp" = EXCLUDED.last_used_timestamp;
END;
$function$


-- update_returning_player_status(in_user_id text, in_minimum_returning_player_time_seconds integer) -> void
-- oid: 58633  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.update_returning_player_status(in_user_id text, in_minimum_returning_player_time_seconds integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    user_account_id BigInt;
    last_login_time TIMESTAMPTZ;
    last_award_time TIMESTAMPTZ;
BEGIN
    SELECT INTO user_account_id, last_login_time, last_award_time id, ps.last_login_time, ps.last_returning_player_awarded_time
    FROM accounts acc
	JOIN player_state ps ON ps.account_id = acc.id
    WHERE acc.user=in_user_id;

    IF user_account_id IS NOT NULL THEN
        IF last_award_time + INTERVAL '1 second' * in_minimum_returning_player_time_seconds > CURRENT_TIMESTAMP THEN
            UPDATE player_state SET last_returning_player_event_time=NULL WHERE account_id=user_account_id;
        ELSIF last_login_time + INTERVAL '1 second' * in_minimum_returning_player_time_seconds < CURRENT_TIMESTAMP THEN
            UPDATE player_state SET last_returning_player_event_time=now() WHERE account_id=user_account_id;
        END IF;
    END IF;
END
$function$


-- update_sell_orders_categories(category_update_data dune.exchangecategoryupdatedata[]) -> void
-- oid: 58634  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.update_sell_orders_categories(category_update_data dune.exchangecategoryupdatedata[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
	UPDATE dune_exchange_orders SET category_mask = update_data.mask, category_depth = update_data.depth
	FROM UNNEST(category_update_data) update_data
	WHERE update_data.item_template_id = dune_exchange_orders.template_id;
END
$function$


-- update_server_building_favorites(in_account_id bigint, in_building_types text[]) -> void
-- oid: 58635  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.update_server_building_favorites(in_account_id bigint, in_building_types text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO building_favorites(account_id, building_types) 
    VALUES(in_account_id, in_building_types)
    ON CONFLICT(account_id) DO UPDATE SET building_types = in_building_types WHERE building_favorites.account_id = in_account_id;
END; $function$


-- update_server_learned_building_sets(in_account_id bigint, in_learned_building_sets text[]) -> void
-- oid: 58636  kind: FUNCTION  category: building_blueprint

CREATE OR REPLACE FUNCTION dune.update_server_learned_building_sets(in_account_id bigint, in_learned_building_sets text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO building_progression(account_id, learned_building_sets) 
    VALUES(in_account_id, in_learned_building_sets) 
    ON CONFLICT(account_id) DO UPDATE SET learned_building_sets = in_learned_building_sets WHERE building_progression.account_id = in_account_id;
END; $function$


-- update_server_learned_new_buildable_pieces(in_account_id bigint, in_new_buildable_pieces text[]) -> void
-- oid: 58637  kind: FUNCTION  category: server

CREATE OR REPLACE FUNCTION dune.update_server_learned_new_buildable_pieces(in_account_id bigint, in_new_buildable_pieces text[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    INSERT INTO building_progression(account_id, new_buildable_pieces) 
    VALUES(in_account_id, in_new_buildable_pieces) 
    ON CONFLICT(account_id) DO UPDATE SET new_buildable_pieces = in_new_buildable_pieces WHERE building_progression.account_id = in_account_id;
END; $function$


-- update_specialization_refund_id(in_player_id bigint, in_refund_id smallint, in_removed_keystones smallint[]) -> void
-- oid: 58638  kind: FUNCTION  category: character_mod

CREATE OR REPLACE FUNCTION dune.update_specialization_refund_id(in_player_id bigint, in_refund_id smallint, in_removed_keystones smallint[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM purchased_specialization_keystones WHERE player_id = in_player_id AND keystone_id = ANY(in_removed_keystones);

	INSERT INTO specialization_refund_id (player_id, refund_id) VALUES(in_player_id, in_refund_id)
	ON conflict (player_id) DO UPDATE SET refund_id = in_refund_id;
END $function$


-- update_spice_field_spawn_state(in_is_spawning_active boolean, in_spicefield_type_id integer) -> void
-- oid: 58639  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.update_spice_field_spawn_state(in_is_spawning_active boolean, in_spicefield_type_id integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    UPDATE spicefield_types SET is_spawning_active = in_is_spawning_active WHERE spicefield_type_id = in_spicefield_type_id;
    UPDATE spicefield_server_availability SET requested_spawned_of_type = 0 WHERE spicefield_type_id = in_spicefield_type_id;
END; $function$


-- update_traveling_actor_dependencies(in_dep dune.traveldependency[]) -> void
-- oid: 58640  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.update_traveling_actor_dependencies(in_dep dune.traveldependency[])
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin
	with valid_ids as (
		with d as (select * from unnest(in_dep))
		select d.id from d where d.id is not null
		union
		select d.parent_id from d where d.parent_id is not null
	),
	valid_ids_plus_dep as (
		select * from valid_ids
		union
		select tap.id from travel_actor_parent as tap where tap.parent_id in (select * from valid_ids)
	),
	-- remove valid ids and connected dependencies from actor_state
	delete_actor_state_ids AS (
		delete from actor_state as acs
		where acs.actor_id in (select * from valid_ids_plus_dep) and acs.state = 'Travel'
	)
	-- remove valid ids and connected dependencies from any other dependency tree
	delete from travel_actor_parent
	where id in (select * from valid_ids_plus_dep);
	
	-- add/update dependencies with valid parent and child ids
	with valid_dep as (
		select a1.id as id, a2.id as parent_id, d.is_instigator
		from unnest(in_dep) d
		left join actors as a1
			on d.id = a1.id
		left join actors as a2
			on d.parent_id = a2.id
		where
				a1.id is not null
			and a2.id is not null
	)
	insert into travel_actor_parent (id, parent_id, is_instigator)
		select * from valid_dep
	on conflict (id) do update set
		"parent_id" = excluded.parent_id,
		"is_instigator" = excluded.is_instigator;
end
$function$


-- update_traveling_actor_tree(in_actor_id bigint, in_target_transform dune.transform, in_target_map text, in_target_dimension_index integer, in_target_partition_id bigint) -> TABLE(out_id bigint, out_actor_state text)
-- oid: 58641  kind: FUNCTION  category: actors

CREATE OR REPLACE FUNCTION dune.update_traveling_actor_tree(in_actor_id bigint, in_target_transform dune.transform, in_target_map text, in_target_dimension_index integer, in_target_partition_id bigint)
 RETURNS TABLE(out_id bigint, out_actor_state text)
 LANGUAGE plpgsql
AS $function$
begin
	RETURN query WITH
		traveling_actor_ids AS (
			SELECT t.id FROM get_traveling_actor_ids(in_actor_id) AS t
		),
		invalid_traveling_actor_ids AS (
			SELECT id, actor_state.state::TEXT FROM traveling_actor_ids
			INNER JOIN actor_state ON actor_state.actor_id = traveling_actor_ids.id
			WHERE actor_state.state != 'Travel'
		),
		valid_traveling_actor_ids AS (
			SELECT id FROM traveling_actor_ids
			WHERE NOT EXISTS (SELECT 1 FROM invalid_traveling_actor_ids)
		),
		insert_actor_state AS (
			INSERT INTO actor_state(actor_id, state)
			SELECT id, 'Travel' FROM valid_traveling_actor_ids
			ON CONFLICT DO NOTHING
		),
		update_actors AS (
			UPDATE actors
			SET 
				transform = in_target_transform,
				dimension_index = in_target_dimension_index,
				map = in_target_map,
				partition_id = in_target_partition_id
			FROM valid_traveling_actor_ids
			WHERE actors.id = valid_traveling_actor_ids.id
		)
	SELECT * FROM invalid_traveling_actor_ids;
end
$function$


-- update_universe_time(in_farm_id text) -> TABLE(universe_time_timestamp timestamp without time zone, down_time_accumulation bigint)
-- oid: 58642  kind: FUNCTION  category: schema_meta

CREATE OR REPLACE FUNCTION dune.update_universe_time(in_farm_id text DEFAULT NULL::text)
 RETURNS TABLE(universe_time_timestamp timestamp without time zone, down_time_accumulation bigint)
 LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
	INSERT INTO farm_variables(farm_id, universe_time_timestamp, universe_lastactive_timestamp, down_time_accumulation, one_row)
	VALUES (in_farm_id, (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')::TIMESTAMP, (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')::TIMESTAMP, 0, true)
	ON CONFLICT(one_row) DO UPDATE
	SET
		down_time_accumulation = CASE
		WHEN farm_variables.farm_id != EXCLUDED.farm_id AND farm_variables.farm_id IS NOT NULL AND EXCLUDED.farm_id IS NOT NULL THEN farm_variables.down_time_accumulation + (EXTRACT(EPOCH FROM ((CURRENT_TIMESTAMP AT TIME ZONE 'UTC')::TIMESTAMP - farm_variables.universe_lastactive_timestamp)) * 1000000)::BIGINT
		ELSE farm_variables.down_time_accumulation
		END,
		universe_lastactive_timestamp = (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')::TIMESTAMP,
		farm_id = CASE
		WHEN EXCLUDED.farm_id IS NULL AND farm_variables.farm_id IS NOT NULL THEN farm_variables.farm_id
		ELSE EXCLUDED.farm_id
		END;
	RETURN QUERY select * from get_universe_time();
	RETURN;
END
$function$


-- update_vendor_timestamp_for_player(in_vendor_id text, in_player_id bigint, in_timestamp bigint) -> void
-- oid: 58643  kind: FUNCTION  category: stock_vendor

CREATE OR REPLACE FUNCTION dune.update_vendor_timestamp_for_player(in_vendor_id text, in_player_id bigint, in_timestamp bigint)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	IF NOT EXISTS
		( SELECT * FROM vendor_stock_cycle
		WHERE vendor_id = in_vendor_id AND player_id = in_player_id)
	THEN
		INSERT INTO vendor_stock_cycle(vendor_id, player_id, last_interacted_timestamp) VALUES(in_vendor_id, in_player_id, in_timestamp);
	ELSE
		UPDATE vendor_stock_cycle
		SET last_interacted_timestamp = in_timestamp
		WHERE vendor_id = in_vendor_id AND player_id = in_player_id;
	END IF;
END
$function$


-- upgrade_location_data_list(in_location_data_list jsonb, in_map_field_name text) -> jsonb
-- oid: 58644  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.upgrade_location_data_list(in_location_data_list jsonb, in_map_field_name text)
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$
BEGIN
	return COALESCE(
		(
		SELECT jsonb_agg(
			CASE
				WHEN elem ? in_map_field_name
				THEN
					jsonb_set(
						elem,
						('{' || in_map_field_name ||'}')::Text[],
						upgrade_map_value(elem->in_map_field_name)
					)
				ELSE
					elem
			END
		)
		FROM jsonb_array_elements(in_location_data_list) elem
		),
		'[]'::jsonb
	);
END
$function$


-- upgrade_map_name(in_map_name text) -> text
-- oid: 58645  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.upgrade_map_name(in_map_name text)
 RETURNS text
 LANGUAGE plpgsql
AS $function$
BEGIN
	return
		CASE in_map_name
			WHEN 'Survival_1' THEN 'HaggaBasin'
			WHEN 'SH_HarkoVillage' THEN 'HarkoVillage'
			WHEN 'DeepDesert_1' THEN 'DeepDesert'
			WHEN 'Overmap' THEN 'Overland'
			WHEN 'SH_Arrakeen' THEN 'Arrakeen'
			WHEN 'CB_Story_Hephaestus' THEN 'WreckOfHephaestus'
			WHEN 'CB_Story_Ecolab_Carthag' THEN 'BeneathCarthag'
			WHEN 'CB_SurvivalChallenge_Station_15' THEN 'Station15'
			WHEN 'CB_Story_WaterFatManor' THEN 'WaterFat'
			WHEN 'SH_FallenLight' THEN 'FallenLight'
			WHEN 'Story_ProcesVerbal' THEN 'ProcesVerbal'
			WHEN 'DLC_Story_LostHarvest' THEN 'LostHarvest'
			WHEN 'DLC_Story_LostHarvest_EcolabA' THEN 'LostHarvest_EcolabA'
			WHEN 'DLC_Story_LostHarvest_EcolabB' THEN 'LostHarvest_EcolabB'
			WHEN 'DLC_Story_LostHarvest_ForgottenLab' THEN 'LostHarvest_ForgottenLab'
			WHEN 'Story_ArtOfKanly' THEN 'ArtOfKanly'
			WHEN 'Story_HeighlinerDungeon' THEN 'HeighlinerDungeon'
			WHEN 'CB_Dungeon_Hephaestus' THEN 'WreckOfHephaestusDungeon'
			WHEN 'CB_Dungeon_OldCarthag' THEN 'OldCarthagDungeon'
			WHEN 'CB_Story_BanditFortress01' THEN 'SandfliesFortress'
			WHEN 'CB_Overland_S_05' THEN 'ClosedOffTestingStationIsland'
			WHEN 'CB_Overland_S_06' THEN 'GroundVehicleTimeTrialIsland'
			WHEN 'CB_Overland_S_04' THEN 'ErythriteCaveIsland'
			WHEN 'CB_Overland_M_01' THEN 'RadioactiveShipwreck'
			WHEN 'CB_Overland_S_07' THEN 'TheRuinsOfTsimpo'
			WHEN 'Story_Faction_Outpost_Hark' THEN 'Story_Faction_Outpost_Hark'
			WHEN 'Story_Faction_Outpost_Atre' THEN 'Story_Faction_Outpost_Atre'
			WHEN 'CB_Ecolab_Bronze_Green_089' THEN 'RadiationDungeon'
			WHEN 'CB_Ecolab_Bronze_Green_152' THEN 'ElectricityDungeon'
			WHEN 'CB_Ecolab_Bronze_Green_195' THEN 'PoisonDungeon'
			WHEN 'CB_Ecolab_Bronze_Green_024' THEN 'DarknessDungeon'
			WHEN 'CB_Ecolab_Bronze_Green_136' THEN 'FireDungeon'
			WHEN 'CB_Story_DestroyedZanovar' THEN 'DestroyedZanovar'
			WHEN 'CB_Story_OrbitalMonitor' THEN 'OrbitalMonitor'
			WHEN 'CB_Dungeon_TheFacility' THEN 'FacilityDungeon'
			WHEN 'CB_Dungeon_ThePit' THEN 'PitDungeon'
			WHEN 'CB_Overland_S_08' THEN 'WindPass'
			ELSE in_map_name
		END;
END;
$function$


-- upgrade_map_value(in_value jsonb) -> jsonb
-- oid: 58646  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.upgrade_map_value(in_value jsonb)
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$
BEGIN
	RETURN 
		CASE 
			WHEN jsonb_typeof(in_value) = 'string'
			THEN
				jsonb_build_object('Name', upgrade_map_name(in_value->>0))
			ELSE 
				in_value
		END;
END;
$function$


-- upsert_spicefield_types(in_max_globally_active integer[], in_max_globally_primed integer[], in_field_types text[], in_map_name text, in_dimension_index integer) -> TABLE(type_id integer, max_global integer, max_global_primed integer, spawning_active boolean, out_field_type text)
-- oid: 58647  kind: FUNCTION  category: spice_field

CREATE OR REPLACE FUNCTION dune.upsert_spicefield_types(in_max_globally_active integer[], in_max_globally_primed integer[], in_field_types text[], in_map_name text, in_dimension_index integer)
 RETURNS TABLE(type_id integer, max_global integer, max_global_primed integer, spawning_active boolean, out_field_type text)
 LANGUAGE plpgsql
AS $function$
begin
	insert into spicefield_types (max_globally_active, max_globally_primed, field_type, map_name, dimension_index)
		select unnest(in_max_globally_active), unnest(in_max_globally_primed), unnest(in_field_types), in_map_name, in_dimension_index
		on conflict do nothing;
	return query select spicefield_type_id, max_globally_active, max_globally_primed, is_spawning_active, field_type::text from spicefield_types where map_name = in_map_name and dimension_index = in_dimension_index;
end $function$


-- use_sinkchart(in_player_id bigint, in_account_id bigint, in_area_id smallint, in_item_id bigint, in_sinkchart_map_name text, in_player_map_name text, in_player_current_dimension integer) -> dune.usesinkchartreturndata
-- oid: 58648  kind: FUNCTION  category: map_areas

CREATE OR REPLACE FUNCTION dune.use_sinkchart(in_player_id bigint, in_account_id bigint, in_area_id smallint, in_item_id bigint, in_sinkchart_map_name text, in_player_map_name text, in_player_current_dimension integer)
 RETURNS dune.usesinkchartreturndata
 LANGUAGE plpgsql
AS $function$
DECLARE
    sinkchart_markers SinkchartMarkerData[];
    survey_target JSONB;
BEGIN
    -- Update map areas.
    INSERT INTO map_areas (account_id, time_first_entered, time_discovered, area_id, items_surveyed_target, map_name)
    SELECT in_account_id, NULL, NOW(), in_area_id, map_areas.items_surveyed_target, in_sinkchart_map_name
    FROM map_areas
    WHERE map_areas.area_id = in_area_id
      AND map_areas.map_name = in_sinkchart_map_name
      AND map_areas.items_surveyed_target IS NOT NULL
    LIMIT 1
    ON CONFLICT ON CONSTRAINT map_areas_pkey DO UPDATE
        SET items_surveyed_target = EXCLUDED.items_surveyed_target,
            time_discovered = CASE WHEN map_areas.time_discovered IS NULL THEN EXCLUDED.time_discovered ELSE map_areas.time_discovered END
    RETURNING items_surveyed_target INTO survey_target;

    -- Perform the INSERT operation explicitly.
    INSERT INTO player_markers (player_id, marker_hash_id, dimension_index, map_name_id, discovery_level, discovery_method, payload)
    SELECT DISTINCT ON (m.marker_hash_id, m.dimension_index)
        in_player_id, m.marker_hash_id, m.dimension_index, m.map_name_id, /*discovery_level*/ 2, /*discovery_method*/ 12, /*payload*/ '{}'::JSONB
    FROM (
        SELECT UNNEST(sc.marker_hash_ids) AS marker_hash_id
        FROM sinkcharts sc
        WHERE sc.item_id = in_item_id
    ) expanded_ids
    JOIN map_names mn
        ON mn.map_name = in_sinkchart_map_name
    INNER JOIN markers m
        ON expanded_ids.marker_hash_id = m.marker_hash_id AND m.map_name_id = mn.map_name_id
    ORDER BY m.marker_hash_id, m.dimension_index
    ON CONFLICT ON CONSTRAINT player_markers_pkey DO UPDATE
        SET discovery_level = GREATEST(player_markers.discovery_level, EXCLUDED.discovery_level);

    -- Get markers that should be shown to the player immediately (marker on same map and dimension).
    SELECT ARRAY_AGG(ROW(m.marker_hash_id, m.marker, m.area_id, m.area_radius, m.long_range, pm.payload, m.payload)::SinkchartMarkerData)
    INTO sinkchart_markers
    FROM (
        SELECT UNNEST(sc.marker_hash_ids) AS marker_hash_id
        FROM sinkcharts sc
        WHERE sc.item_id = in_item_id
    ) expanded_ids
    JOIN map_names smn
        ON smn.map_name = in_sinkchart_map_name
    INNER JOIN markers m
        ON expanded_ids.marker_hash_id = m.marker_hash_id AND m.map_name_id = smn.map_name_id
    INNER JOIN player_markers pm
        ON m.marker_hash_id = pm.marker_hash_id
        AND m.dimension_index = pm.dimension_index
        AND m.map_name_id = pm.map_name_id
        AND pm.player_id = in_player_id
    JOIN map_names pmn
        ON pmn.map_name = in_player_map_name
    WHERE m.map_name_id = pmn.map_name_id
      AND (m.dimension_index = -1 OR m.dimension_index = in_player_current_dimension);

    RETURN (sinkchart_markers, survey_target);
END;
$function$


-- verify_item_dup_backup_tool(in_account_id bigint, in_vehicle_id bigint, in_cheat_type dune.cheat_type_enum) -> void
-- oid: 58649  kind: FUNCTION  category: inventory

CREATE OR REPLACE FUNCTION dune.verify_item_dup_backup_tool(in_account_id bigint, in_vehicle_id bigint, in_cheat_type dune.cheat_type_enum)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_FLS_id TEXT;
BEGIN
	PERFORM 1 FROM inventories inv JOIN items it ON it.inventory_id = inv.id WHERE inv.actor_id = in_vehicle_id;
 	IF FOUND THEN

		-- Delete all items in this vehicle's inventories (excluding module inventories)
        PERFORM delete_items(
            (
                SELECT array_agg(i.id)
                FROM items i
                JOIN inventories inv ON inv.id = i.inventory_id
                WHERE inv.actor_id = in_vehicle_id
                AND inv.inventory_type = 0   -- 0 = vehicle backpack
            )
        );

        -- Debug logging
    	SELECT acc."user"
		    INTO v_FLS_id
		    FROM accounts acc
		    WHERE acc.id = in_account_id
		    LIMIT 1;
        RAISE WARNING 'Trying to vbt vehicle that has not an empty inventory. Behavior: % Vehicle: %, Account: %, FLS: %', in_cheat_type, in_vehicle_id, in_account_id, v_FLS_id;

        -- DB tracking
        PERFORM log_cheating(v_FLS_id, in_cheat_type);

    END IF;
END;
$function$


-- wipe_old_events_log(in_days_limit integer) -> void
-- oid: 58650  kind: FUNCTION  category: cleanup

CREATE OR REPLACE FUNCTION dune.wipe_old_events_log(in_days_limit integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
	DELETE FROM game_events WHERE universe_time < to_timestamp(in_days_limit);
END
$function$


-- zero_transform() -> dune.transform
-- oid: 58651  kind: FUNCTION  category: misc

CREATE OR REPLACE FUNCTION dune.zero_transform()
 RETURNS dune.transform
 LANGUAGE plpgsql
 IMMUTABLE
AS $function$
BEGIN
	RETURN (
		ROW(
      		ROW(0.0, 0.0, 0.0)::Vector,
      		ROW(0.0, 0.0, 0.0, 1.0)::Quaternion
    	)::Transform
  	);
END;
$function$
