Add map import and export with durable refresh progress
This commit is contained in:
155
supabase/migrations/20261007145000_map_sharing.sql
Normal file
155
supabase/migrations/20261007145000_map_sharing.sql
Normal file
@@ -0,0 +1,155 @@
|
||||
-- Shared copies stay private. Account ownership permits managing and playing a
|
||||
-- copy; publication and export rights remain with the original map owner.
|
||||
alter table public.datasets add column imported_from_id uuid;
|
||||
alter table public.datasets add column imported_owner_name text;
|
||||
alter table public.datasets add constraint imported_map_attribution check (
|
||||
(imported_from_id is null and imported_owner_name is null) or
|
||||
(imported_from_id is not null and imported_owner_name is not null and length(imported_owner_name) between 1 and 200)
|
||||
);
|
||||
|
||||
create function public.protect_shared_map() returns trigger
|
||||
language plpgsql security invoker set search_path='' as $$
|
||||
begin
|
||||
if TG_OP='UPDATE' and old.imported_from_id is not null and
|
||||
(new.imported_from_id is distinct from old.imported_from_id or new.imported_owner_name is distinct from old.imported_owner_name) then
|
||||
raise exception 'Shared map ownership cannot be changed';
|
||||
end if;
|
||||
if new.imported_from_id is not null and
|
||||
(new.state in ('review','published') or new.update_state='review' or new.allow_contributions) then
|
||||
raise exception 'Owned by "%". Shared maps cannot be submitted for moderator review.',new.imported_owner_name;
|
||||
end if;
|
||||
return new;
|
||||
end; $$;
|
||||
create trigger protect_shared_map before insert or update on public.datasets
|
||||
for each row execute function public.protect_shared_map();
|
||||
|
||||
create function public.create_map_import(p_owner uuid,p_manifest jsonb,p_original_map uuid,p_original_owner text)
|
||||
returns uuid language plpgsql security invoker set search_path='' as $$
|
||||
declare result uuid;
|
||||
begin
|
||||
if p_manifest->>'mediaFormat' is distinct from 'webp' or p_manifest ? 'contributionTo' or
|
||||
p_original_map is null or coalesce(length(p_original_owner),0) not between 1 and 200 then
|
||||
raise exception 'Invalid shared map';
|
||||
end if;
|
||||
result:=public.create_upload(p_owner,p_manifest);
|
||||
update public.datasets set imported_from_id=p_original_map,imported_owner_name=p_original_owner where id=result;
|
||||
return result;
|
||||
end; $$;
|
||||
|
||||
-- Take an immutable private snapshot when starting a game. Other accounts and
|
||||
-- guests can only start public maps; invited multiplayer players use room keys.
|
||||
create function public.playable_map_revision(p_dataset uuid,p_owner uuid) returns uuid
|
||||
language plpgsql security invoker set search_path='' as $$
|
||||
declare d public.datasets; m jsonb; rev uuid;
|
||||
begin
|
||||
select * into d from public.datasets where id=p_dataset for share;
|
||||
if not found then raise exception 'Map unavailable'; end if;
|
||||
if d.state='published' then return d.revision_id; end if;
|
||||
if d.imported_from_id is null or d.owner_id is distinct from p_owner or d.state<>'draft' or d.banned_at is not null then
|
||||
raise exception 'Map unavailable';
|
||||
end if;
|
||||
if exists(select 1 from public.upload_jobs where dataset_id=d.id and state in ('queued','running')) then
|
||||
raise exception 'Wait for map rendering to finish before playing';
|
||||
end if;
|
||||
select manifest into m from public.dataset_inputs where dataset_id=d.id and validated;
|
||||
if m is null or jsonb_array_length(m->'viewpoints')<5 then raise exception 'Map unavailable'; end if;
|
||||
insert into public.revisions(dataset_id,manifest,asset_root) values(d.id,m,d.id::text) returning id into rev;
|
||||
return rev;
|
||||
end; $$;
|
||||
|
||||
create or replace function public.create_game(p_dataset uuid,p_difficulty text,p_owner uuid,p_access_hash text)
|
||||
returns uuid language plpgsql security invoker set search_path='' as $$
|
||||
declare sid uuid; revision uuid; rev public.revisions; vp jsonb; n integer:=0;
|
||||
begin
|
||||
if p_difficulty not in ('easy','normal','hard') then raise exception 'Invalid difficulty'; end if;
|
||||
revision:=public.playable_map_revision(p_dataset,p_owner);
|
||||
select * into rev from public.revisions where id=revision;
|
||||
if not found then raise exception 'Map unavailable'; end if;
|
||||
if jsonb_array_length(rev.manifest->'viewpoints')<5 then raise exception 'Map has fewer than five viewpoints'; end if;
|
||||
insert into public.game_sessions(revision_id,owner_id,access_hash,difficulty) values(rev.id,p_owner,p_access_hash,p_difficulty) returning id into sid;
|
||||
for vp in select value from jsonb_array_elements(rev.manifest->'viewpoints') order by random() limit 5 loop
|
||||
insert into public.game_rounds(session_id,round_number,viewpoint) values(sid,n,vp); n:=n+1;
|
||||
end loop;
|
||||
return sid;
|
||||
end; $$;
|
||||
|
||||
create function public.create_map_room(p_dataset uuid,p_difficulty text,p_name text,p_limit integer,p_access_hash text,p_owner uuid)
|
||||
returns uuid language plpgsql security invoker set search_path='' as $$
|
||||
declare rid uuid; revision uuid; pid uuid:=gen_random_uuid(); rev public.revisions; vp jsonb; n integer:=0;
|
||||
begin
|
||||
if p_limit is null or p_limit not between 2 and 16 then raise exception 'Choose between 2 and 16 players'; end if;
|
||||
if p_difficulty is null or p_difficulty not in ('easy','normal','hard') then raise exception 'Invalid difficulty'; end if;
|
||||
revision:=public.playable_map_revision(p_dataset,p_owner);
|
||||
select * into rev from public.revisions where id=revision;
|
||||
if not found then raise exception 'Map unavailable'; end if;
|
||||
if coalesce(jsonb_array_length(rev.manifest->'viewpoints'),0)<5 then raise exception 'Map has fewer than five viewpoints'; end if;
|
||||
insert into public.multiplayer_rooms(revision_id,host_player_id,difficulty,player_limit) values(rev.id,pid,p_difficulty,p_limit) returning id into rid;
|
||||
insert into public.multiplayer_players(id,room_id,access_hash,display_name) values(pid,rid,p_access_hash,btrim(p_name));
|
||||
for vp in select value from jsonb_array_elements(rev.manifest->'viewpoints') order by random() limit 5 loop
|
||||
insert into public.multiplayer_rounds(room_id,round_number,viewpoint) values(rid,n,vp); n:=n+1;
|
||||
end loop;
|
||||
return rid;
|
||||
end; $$;
|
||||
|
||||
revoke all on function public.protect_shared_map(),public.create_map_import(uuid,jsonb,uuid,text),public.playable_map_revision(uuid,uuid),public.create_map_room(uuid,text,text,integer,text,uuid) from public,anon,authenticated;
|
||||
grant execute on function public.create_map_import(uuid,jsonb,uuid,text),public.playable_map_revision(uuid,uuid),public.create_map_room(uuid,text,text,integer,text,uuid) to service_role;
|
||||
|
||||
-- Private unreviewed copies are excluded from public scores.
|
||||
create or replace function public.player_leaderboard(
|
||||
p_difficulty text default 'all',
|
||||
p_search text default '',
|
||||
p_page integer default 1,
|
||||
p_limit integer default 25
|
||||
) returns jsonb
|
||||
language sql stable security invoker set search_path = '' as $$
|
||||
with completed as (
|
||||
select s.id, s.owner_id, sum((r.result->>'score')::integer) as score
|
||||
from public.game_sessions s
|
||||
join public.game_rounds r on r.session_id = s.id
|
||||
join public.revisions v on v.id = s.revision_id
|
||||
join public.datasets d on d.id = v.dataset_id
|
||||
where s.current_round = 5 and s.owner_id is not null and d.imported_from_id is null
|
||||
and (p_difficulty = 'all' or s.difficulty = p_difficulty)
|
||||
group by s.id, s.owner_id
|
||||
having count(*) = 5 and count(r.result) = 5
|
||||
), players as (
|
||||
select p.id as player_id, p.display_name,
|
||||
max(c.score) as best_score, avg(c.score) as average_score,
|
||||
count(*) as games_played
|
||||
from completed c join public.profiles p on p.id = c.owner_id
|
||||
group by p.id, p.display_name
|
||||
), ranked as (
|
||||
select row_number() over (
|
||||
order by best_score desc, average_score desc, games_played desc, player_id
|
||||
) as rank, player_id, display_name, best_score,
|
||||
round(average_score)::integer as average_score, games_played
|
||||
from players
|
||||
), matched as (
|
||||
select * from ranked
|
||||
where strpos(lower(display_name), lower(left(trim(coalesce(p_search, '')), 64))) > 0
|
||||
), settings as (
|
||||
select greatest(1, least(coalesce(p_limit, 25), 100)) as page_size
|
||||
), counts as (
|
||||
select (select count(*) from matched) as matching_players,
|
||||
greatest(1, ceil((select count(*) from matched)::numeric / settings.page_size)::integer) as pages
|
||||
from settings
|
||||
), pagination as (
|
||||
select *, greatest(1, least(coalesce(p_page, 1), pages)) as page
|
||||
from counts cross join settings
|
||||
), entries as (
|
||||
select * from matched order by rank
|
||||
limit (select page_size from pagination)
|
||||
offset (select (page - 1) * page_size from pagination)
|
||||
)
|
||||
select jsonb_build_object(
|
||||
'entries', coalesce((select jsonb_agg(to_jsonb(e) order by rank) from entries e), '[]'::jsonb),
|
||||
'leaders', coalesce((select jsonb_agg(to_jsonb(r) order by rank) from
|
||||
(select * from ranked order by rank limit 3) r), '[]'::jsonb),
|
||||
'total_players', (select count(*) from ranked),
|
||||
'total_games', (select count(*) from completed),
|
||||
'matching_players', matching_players, 'page', page, 'pages', pages
|
||||
) from pagination;
|
||||
$$;
|
||||
|
||||
revoke all on function public.player_leaderboard(text, text, integer, integer) from public, anon, authenticated;
|
||||
grant execute on function public.player_leaderboard(text, text, integer, integer) to service_role;
|
||||
Reference in New Issue
Block a user