individual level pages!!!!!!!!!
This commit is contained in:
parent
543a959829
commit
dc6d5767bb
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
|
@ -128,7 +128,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/accounts/notifications/" title="Notifiations" class="circle-button notifications <%= unread_notifications ? "notifications-unread" : "" %>">
|
||||
<a href="/accounts/notifications" title="Notifiations" class="circle-button notifications <%= unread_notifications ? "notifications-unread" : "" %>">
|
||||
<%= File.read("public/assets/icons/bell.svg") %>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<li>The <a href="https://git.oat.zone/oat/crystal-gauntlet">Git repository</a></li>
|
||||
<li><a href="/accounts">Account stuff</a></li>
|
||||
<li><a href="/tools/song_upload">Song reuploading</a> and <a href="/tools/song_search">searching</a></li>
|
||||
<li><a href="/tools/levels">Levels</a> and <a href="/tools/reupload">level reuploading</a></li>
|
||||
<li><a href="/levels">Levels</a> and <a href="/tools/reupload">level reuploading</a></li>
|
||||
<%- if config_get("sessions.allow").as(Bool | Nil) -%>
|
||||
<li>The <a href="/tools/create_session">session creation page</a> (for accessing features in 1.9)</li>
|
||||
<%- end -%>
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<title><%= name %> by <%= username %></title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--background-color-2);
|
||||
|
||||
border-radius: 1.5em;
|
||||
padding: 1em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.card.bright {
|
||||
background-color: var(--accent-color);
|
||||
color: #000;
|
||||
}
|
||||
.card.bright ::selection {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
.card.bright a {
|
||||
color: #000;
|
||||
}
|
||||
.card-l {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.level-img {
|
||||
display: block;
|
||||
height: 3rem;
|
||||
}
|
||||
.stars {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stars img {
|
||||
width: auto;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.card-r {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.card-top {
|
||||
display: flex;
|
||||
}
|
||||
.label-left {
|
||||
flex: 1 1 auto;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.label-right {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.card-header {
|
||||
font-weight: bold;
|
||||
color: var(--text-color-dark);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.favicon {
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.leaderboard {
|
||||
min-width: 100%;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
/* honestly quite horrendous */
|
||||
.leaderboard tr:nth-child(2) td:first-child {
|
||||
border-top-left-radius: 0.5em;
|
||||
}
|
||||
.leaderboard tr:nth-child(2) td:last-child {
|
||||
border-top-right-radius: 0.5em;
|
||||
}
|
||||
.leaderboard tr:last-child td:first-child {
|
||||
border-bottom-left-radius: 0.5em;
|
||||
}
|
||||
.leaderboard tr:last-child td:last-child {
|
||||
border-bottom-right-radius: 0.5em;
|
||||
}
|
||||
.leaderboard tr:not(.leaderboard-header):nth-child(even) td {
|
||||
background-color: var(--background-color-2);
|
||||
}
|
||||
.leaderboard th, .leaderboard td {
|
||||
padding: 0.2em 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
.leaderboard th {
|
||||
font-size: 0.8em;
|
||||
user-select: none;
|
||||
color: var(--text-color-dark);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.leaderboard td.rank {
|
||||
width: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.leaderboard td.percent {
|
||||
width: 75px;
|
||||
}
|
||||
.leaderboard td.coins {
|
||||
width: 3em;
|
||||
}
|
||||
.leaderboard td.icon {
|
||||
width: 2.5em;
|
||||
text-align: center;
|
||||
}
|
||||
.leaderboard td.name {
|
||||
width: 180px
|
||||
}
|
||||
.leaderboard td.name a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaderboard .player-icon {
|
||||
height: 1.2em;
|
||||
width: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/"><img src="/favicon.png" width="64" height="auto" class="spinny favicon"></a><br>
|
||||
|
||||
<div class="card bright">
|
||||
<div class="card-l">
|
||||
<%=
|
||||
difficulty = (difficulty_set || difficulty_community).try { |n| LevelDifficulty.new(n) }
|
||||
demon_difficulty = demon_difficulty_int.try { |n| DemonDifficulty.new(n) }
|
||||
"<img src='#{Templates.get_difficulty_icon(difficulty, featured, epic, demon_difficulty)}' class='level-img'>"
|
||||
%>
|
||||
<%- if stars -%>
|
||||
<div class="stars">
|
||||
<%= stars %> <img src="/assets/icons/gd/star.png">
|
||||
</div>
|
||||
<%- end -%>
|
||||
</div>
|
||||
<div class="card-r">
|
||||
<div class="card-top">
|
||||
<div class="label-left">
|
||||
<%= name %>
|
||||
</div>
|
||||
<div class="label-right">
|
||||
<%= downloads %> downloads <%= likes %> likes
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-bottom">
|
||||
by <a href="/user/<%= username %>"><%= username %></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card description">
|
||||
<div class="card-header">DESCRIPTION</div>
|
||||
<div>
|
||||
<%= description != "" ? HTML.escape(description) : "<i>No description provided</i>" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card description">
|
||||
<div class="card-header">SONG</div>
|
||||
<div>
|
||||
<%- if song_name -%>
|
||||
<!--
|
||||
song_name, song_author, song_url, song_author_url
|
||||
-->
|
||||
<div><a href="<%= song_url %>" target="_blank" rel="noopener"><%= song_name %></a></div>
|
||||
<%- if song_author && song_author != "" -%>
|
||||
<div>by <a href="<%= song_author_url %>" target="_blank" rel="noopener"><%= song_author %></a></div>
|
||||
<%- else -%>
|
||||
<div><i>unknown artist</i></div>
|
||||
<%- end -%>
|
||||
<%- else -%>
|
||||
idk
|
||||
<%- end -%>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="leaderboard">
|
||||
<tr class="leaderboard-header">
|
||||
<th class="rank">RANK</td>
|
||||
<th class="percent">PERCENT</td>
|
||||
<th class="coins">COINS</td>
|
||||
<th class="icon"></td>
|
||||
<th class="name">PLAYER</td>
|
||||
<th class="date">TIME</td>
|
||||
</tr>
|
||||
|
||||
<%
|
||||
rank = 0
|
||||
scores.each do |percent, coins, username, icon_type, color1, color2, cube, ship, ball, ufo, wave, robot, spider, special, set_at|
|
||||
rank = rank + 1
|
||||
|
||||
icon_value = [cube, ship, ball, ufo, wave, robot, spider][icon_type]
|
||||
type_str = ["cube", "ship", "ball", "ufo", "wave", "robot", "spider"][icon_type]
|
||||
|
||||
set_at_date = Time.parse(set_at, Format::TIME_FORMAT, Time::Location::UTC)
|
||||
%>
|
||||
<tr>
|
||||
<td class="rank">#<%= rank %></td>
|
||||
<td class="percent"><%= percent %>%</td>
|
||||
<td class="coins"></td>
|
||||
<td class="icon">
|
||||
<img src="https://gdicon.oat.zone/icon.png?type=<%=type_str%>&value=<%=icon_value%>&color1=<%=color1%>&color2=<%=color2%><%=special ? "&glow=1" : ""%>" class="player-icon">
|
||||
</td>
|
||||
<td class="name">
|
||||
<a href="/user/<%= username %>"><%= username %></a>
|
||||
</td>
|
||||
<td class="date">
|
||||
<time datetime="<%= Time::Format::RFC_3339.format(set_at_date) %>" title="<%= Time::Format::RFC_2822.format(set_at_date) %>"><%= Format.fmt_timespan(Time.utc - set_at_date) %></time>
|
||||
</td>
|
||||
</tr>
|
||||
<%- end -%>
|
||||
<%- if scores.size == 0 -%>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center">
|
||||
<i>No scores</i>
|
||||
</td>
|
||||
</tr>
|
||||
<%- end -%>
|
||||
</table>
|
||||
|
||||
<p>comments go here etc etc</p>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -55,7 +55,10 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1em
|
||||
gap: 1em;
|
||||
}
|
||||
.level a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
@ -69,8 +72,8 @@
|
|||
"<img src='#{Templates.get_difficulty_icon(difficulty, featured, epic, demon_difficulty)}' class='level-img'>"
|
||||
%>
|
||||
<div class="level-right">
|
||||
<span class="line"><span class="name"><%= name %></span><span class="id dim">#<%= id %></span></span>
|
||||
<small>by <%= username %></small><br>
|
||||
<span class="line"><span class="name"><a href="/levels/<%= id %>"><%= name %></a></span><span class="id dim">#<%= id %></span></span>
|
||||
<small>by <a href="/user/<%= username %>"><%= username %></a></small><br>
|
||||
</div>
|
||||
</div>
|
||||
<%- end -%>
|
||||
|
|
|
@ -64,18 +64,16 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C
|
|||
LOG.debug { "allowed objects: #{allowed_objects.inspect}" }
|
||||
|
||||
if forbidden_obj = level_objects.find do |obj|
|
||||
if !obj.has_key?("1")
|
||||
false
|
||||
end
|
||||
|
||||
id = obj["1"].to_i
|
||||
if allowed_objects.size > 0
|
||||
if !allowed_objects.includes?(id)
|
||||
true
|
||||
end
|
||||
else
|
||||
if forbidden_objects.includes?(id)
|
||||
true
|
||||
if obj.has_key?("1")
|
||||
id = obj["1"].to_i
|
||||
if allowed_objects.size > 0
|
||||
if !allowed_objects.includes?(id)
|
||||
true
|
||||
end
|
||||
else
|
||||
if forbidden_objects.includes?(id)
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -83,12 +81,12 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C
|
|||
return "-1"
|
||||
end
|
||||
|
||||
if exploit_obj = level_objects.find do |obj|
|
||||
if exploit_obj = level_objects.find { |obj|
|
||||
# target color ID
|
||||
(obj.has_key?("23") && obj["23"].to_i < 0 || obj["23"].to_i > 1100) ||
|
||||
(obj.has_key?("23") && (obj["23"].to_i < 0 || obj["23"].to_i > 1100)) ||
|
||||
# target group ID
|
||||
(obj.has_key?("51") && obj["51"].to_i < 0 || obj["51"].to_i > 1100)
|
||||
end
|
||||
(obj.has_key?("51") && (obj["51"].to_i < 0 || obj["51"].to_i > 1100))
|
||||
}
|
||||
LOG.info { "preventing upload of level attempting to exploit invalid color/group IDs" }
|
||||
return "-1"
|
||||
end
|
||||
|
@ -147,7 +145,7 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C
|
|||
# create new level
|
||||
next_id = IDs.get_next_id("levels")
|
||||
|
||||
DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, extra_data, level_info, editor_time, editor_time_copies, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, Clean.clean_special(params["levelName"])[..20-1], user_id, description[..140-1], params["original"].to_i, params["gameVersion"].to_i, (params["binaryVersion"]? || "0").to_i, params["password"] == "0" ? nil : params["password"].to_i, requested_stars, (params["unlisted"]? || "0").to_i == 1, params["levelVersion"].to_i, Clean.clean_special(extraString), Clean.clean_b64(params["levelInfo"]? || ""), (params["wt"]? || "0").to_i, (params["wt2"]? || "0").to_i, song_id.to_i, level_length, objects, coins, (params["ldm"]? || "0").to_i == 1, two_player)
|
||||
DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, extra_data, level_info, editor_time, editor_time_copies, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, Clean.clean_basic(params["levelName"])[..20-1], user_id, description[..140-1], params["original"].to_i, params["gameVersion"].to_i, (params["binaryVersion"]? || "0").to_i, params["password"] == "0" ? nil : params["password"].to_i, requested_stars, (params["unlisted"]? || "0").to_i == 1, params["levelVersion"].to_i, Clean.clean_special(extraString), Clean.clean_b64(params["levelInfo"]? || ""), (params["wt"]? || "0").to_i, (params["wt2"]? || "0").to_i, song_id.to_i, level_length, objects, coins, (params["ldm"]? || "0").to_i == 1, two_player)
|
||||
|
||||
File.write(DATA_FOLDER / "levels" / "#{next_id.to_s}.lvl", Base64.decode(params["levelString"]))
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ levels_per_page = 10
|
|||
|
||||
CrystalGauntlet.template_endpoints[{
|
||||
name: "list_levels",
|
||||
path: "/tools/levels",
|
||||
path: "/levels",
|
||||
methods: ["get"]
|
||||
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
|
||||
context.response.content_type = "text/html"
|
||||
|
@ -15,3 +15,24 @@ CrystalGauntlet.template_endpoints[{
|
|||
levels = DATABASE.query_all("select levels.id, name, users.username, levels.community_difficulty, levels.difficulty, levels.demon_difficulty, levels.featured, levels.epic from levels left join users on levels.user_id = users.id order by levels.id desc limit #{levels_per_page} offset #{page * levels_per_page}", as: {Int32, String, String, Int32?, Int32?, Int32?, Bool, Bool})
|
||||
ECR.embed("./public/template/levels.ecr", context.response)
|
||||
}
|
||||
|
||||
CrystalGauntlet.template_endpoints[{
|
||||
name: "level_page",
|
||||
path: "/levels/{id<\\d+>}",
|
||||
methods: ["get"]
|
||||
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
|
||||
context.response.content_type = "text/html"
|
||||
id = params["id"].as(String).to_i
|
||||
|
||||
begin
|
||||
name, username, difficulty_community, difficulty_set, demon_difficulty_int, featured, epic, downloads, likes, stars, description, song_id, song_name, song_author, song_url, song_author_url = DATABASE.query_one("select levels.name, users.username, community_difficulty, difficulty, demon_difficulty, featured, epic, downloads, likes, levels.stars, description, song_id, song_data.name, song_authors.name, songs.url, song_authors.source from levels left join users on levels.user_id = users.id left join songs on songs.id = levels.song_id left join song_data on song_data.id = levels.song_id left join song_authors on song_data.author_id = song_authors.id where levels.id = ?", id, as: {String, String, Int32?, Int32?, Int32?, Bool, Bool, Int32, Int32, Int32?, String, Int32, String?, String?, String?, String?})
|
||||
rescue err
|
||||
LOG.error {"whar.... #{err}"}
|
||||
context.response.status = HTTP::Status::NOT_FOUND
|
||||
return
|
||||
end
|
||||
|
||||
scores = DATABASE.query_all("select distinct percent, level_scores.coins, users.username, users.icon_type, users.color1, users.color2, users.cube, users.ship, users.ball, users.ufo, users.wave, users.robot, users.spider, users.special, set_at from level_scores join users on level_scores.account_id = users.account_id where level_id = ? order by percent desc, level_scores.coins desc, set_at limit 25", id, as: {Int32, Int32, String, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, String})
|
||||
|
||||
ECR.embed("./public/template/level.ecr", context.response)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue