diff --git a/src/main/java/ru/scarletredman/gd2spring/config/TestConfig.java b/src/main/java/ru/scarletredman/gd2spring/config/TestConfig.java index 1fd9281..c63d117 100644 --- a/src/main/java/ru/scarletredman/gd2spring/config/TestConfig.java +++ b/src/main/java/ru/scarletredman/gd2spring/config/TestConfig.java @@ -1,5 +1,7 @@ package ru.scarletredman.gd2spring.config; +import java.sql.Timestamp; +import java.time.Instant; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; @@ -7,6 +9,7 @@ import ru.scarletredman.gd2spring.model.Level; import ru.scarletredman.gd2spring.model.User; import ru.scarletredman.gd2spring.model.UserComment; +import ru.scarletredman.gd2spring.model.embedable.LevelRateInfo; import ru.scarletredman.gd2spring.service.LevelService; import ru.scarletredman.gd2spring.service.MessageService; import ru.scarletredman.gd2spring.service.UserCommentService; @@ -44,8 +47,10 @@ void createTestUser(boolean debugMode) { userCommentService.writeComment(new UserComment(u, "Hello world!!!")); } - var level = createTestLevel(user, "Test level"); - levelService.uploadLevel(level); + for (int i = 0; i < 30; i++) { + var level = createTestLevel(user, "Test level " + i, i, i * 2, i % 4, i % 3 == 0); + levelService.uploadLevel(level); + } var user2 = userService.findUserById(2).get(); for (int i = 0; i < 30; i++) { @@ -54,11 +59,14 @@ void createTestUser(boolean debugMode) { } } - Level createTestLevel(User owner, String name) { + Level createTestLevel(User owner, String name, int likes, int downloads, int stars, boolean featured) { var level = new Level(owner, name); level.setDescription("Hello world!"); level.setObjects(4); + level.setLikes(likes); + level.setDownloads(downloads); + var data = level.getData(); data.setExtra( "0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0"); @@ -68,6 +76,13 @@ Level createTestLevel(User owner, String name) { var rate = level.getRate(); rate.setRequestedStars(10); + rate.setStars(stars); + rate.setDifficulty(LevelRateInfo.Difficulty.values()[stars]); + rate.setFeatured(featured); + rate.setEpic(featured); + if (stars != 0 || featured) { + rate.setRateTime(Timestamp.from(Instant.now())); + } return level; } diff --git a/src/main/java/ru/scarletredman/gd2spring/controller/LevelController.java b/src/main/java/ru/scarletredman/gd2spring/controller/LevelController.java index cbbf78f..4d31d4a 100644 --- a/src/main/java/ru/scarletredman/gd2spring/controller/LevelController.java +++ b/src/main/java/ru/scarletredman/gd2spring/controller/LevelController.java @@ -6,9 +6,12 @@ import org.springframework.web.bind.annotation.RestController; import ru.scarletredman.gd2spring.controller.annotation.GeometryDashAPI; import ru.scarletredman.gd2spring.controller.response.GetLevelsResponse; +import ru.scarletredman.gd2spring.model.embedable.LevelFilters; +import ru.scarletredman.gd2spring.model.embedable.LevelRateInfo; import ru.scarletredman.gd2spring.security.annotation.GDAuthorizedOnly; import ru.scarletredman.gd2spring.service.LevelService; import ru.scarletredman.gd2spring.service.type.LevelListPage; +import ru.scarletredman.gd2spring.service.type.LevelSearchType; import ru.scarletredman.gd2spring.util.ResponseLogger; @GeometryDashAPI @@ -52,11 +55,11 @@ String uploadLevel( GetLevelsResponse getLevels( @RequestParam(name = "type") int type, @RequestParam(name = "str") String levelName, - @RequestParam(name = "diff") String difficulty, // "-" or number - @RequestParam(name = "len") String length, // "-" or number + @RequestParam(name = "diff") String difficulty, // "-" or numbers + @RequestParam(name = "len") String length, // "-" or numbers @RequestParam(name = "page") int page, @RequestParam(name = "total") int total, - @RequestParam(name = "uncompleted", required = false, defaultValue = "0") int isUncompleted, + @RequestParam(name = "uncompleted", required = false, defaultValue = "0") int isOnlyUncompleted, @RequestParam(name = "onlyCompleted", required = false, defaultValue = "0") int isOnlyCompleted, @RequestParam(name = "featured", required = false, defaultValue = "0") int isFeatured, @RequestParam(name = "original", required = false, defaultValue = "0") int isOriginal, @@ -64,12 +67,28 @@ GetLevelsResponse getLevels( @RequestParam(name = "coins", required = false, defaultValue = "0") int hasCoins, @RequestParam(name = "epic", required = false, defaultValue = "0") int isEpic, @RequestParam(name = "noStar", required = false, defaultValue = "0") int noStar, - @RequestParam(name = "demonFilter", required = false, defaultValue = "-1") int demonFilter, - @RequestParam(name = "song", required = false, defaultValue = "0") int song, - @RequestParam(name = "customSong", required = false, defaultValue = "0") int customSong) { + @RequestParam(name = "star", required = false, defaultValue = "0") int hasStar, + @RequestParam(name = "demonFilter", required = false, defaultValue = "0") int demonFilter, + @RequestParam(name = "song", required = false, defaultValue = "-1") int song, + @RequestParam(name = "customSong", required = false, defaultValue = "0") int isCustomSong) { var filters = new LevelListPage.Filters( - levelName, null, null, page, false, false, false, false, false, false, false, false, 0, 0, 0); + levelName, + LevelRateInfo.Difficulty.parseGDSearch(difficulty, demonFilter), + LevelFilters.Length.parseGDSearch(length), + page, + isOnlyCompleted == 1, + isOnlyUncompleted == 1, + isFeatured == 1, + isOriginal == 1, + isForTwoPlayers == 1, + hasCoins == 1, + isEpic == 1, + noStar == 1, + hasStar == 1, + song, + isCustomSong == 1, + LevelSearchType.find(type)); var levels = levelService.getLevels(filters); return responseLogger.result(new GetLevelsResponse(levels)); diff --git a/src/main/java/ru/scarletredman/gd2spring/controller/response/GetLevelsResponse.java b/src/main/java/ru/scarletredman/gd2spring/controller/response/GetLevelsResponse.java index c78551e..22da0da 100644 --- a/src/main/java/ru/scarletredman/gd2spring/controller/response/GetLevelsResponse.java +++ b/src/main/java/ru/scarletredman/gd2spring/controller/response/GetLevelsResponse.java @@ -56,7 +56,7 @@ public String getResponse() { String songs = String.join( DELIMITER_SONGS, songStats.stream().map(SongStat::getResponse).toList()); - String pageInfo = total + ":" + offset + ":10"; + String pageInfo = total + ":" + (offset * 10) + ":10"; String hash = generateHash(); return levels + "#" + users + "#" + songs + "#" + pageInfo + "#" + hash; diff --git a/src/main/java/ru/scarletredman/gd2spring/model/Level.java b/src/main/java/ru/scarletredman/gd2spring/model/Level.java index e8ecff2..463c310 100644 --- a/src/main/java/ru/scarletredman/gd2spring/model/Level.java +++ b/src/main/java/ru/scarletredman/gd2spring/model/Level.java @@ -12,7 +12,11 @@ @Entity @Table( name = "levels", - indexes = {@Index(name = "level_name_index", columnList = "name")}) + indexes = { + @Index(name = "level_name_index", columnList = "name"), + @Index(name = "level_rate_time_index", columnList = "rate_time"), + @Index(name = "level_difficulty_index", columnList = "difficulty") + }) public class Level { @Id @@ -30,9 +34,8 @@ public class Level { @JoinColumn(name = "owner", nullable = false) private User owner; - @ManyToOne - @JoinColumn(name = "original") - private Level original = null; + @Column(name = "original") + private Long original = null; @Embedded private LevelRateInfo rate = new LevelRateInfo(); diff --git a/src/main/java/ru/scarletredman/gd2spring/model/embedable/LevelFilters.java b/src/main/java/ru/scarletredman/gd2spring/model/embedable/LevelFilters.java index eb476b9..31fa3ca 100644 --- a/src/main/java/ru/scarletredman/gd2spring/model/embedable/LevelFilters.java +++ b/src/main/java/ru/scarletredman/gd2spring/model/embedable/LevelFilters.java @@ -2,6 +2,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import java.util.LinkedList; +import java.util.List; import lombok.*; @Getter @@ -30,5 +32,21 @@ public enum Length { XL(4); private final int code; + + public static List parseGDSearch(String input) { + var list = new LinkedList(); + if (input.equals("-")) return list; + + var nums = input.split(","); + for (var num : nums) { + try { + var len = values()[Integer.parseInt(num)]; + list.add(len); + } catch (NumberFormatException | IndexOutOfBoundsException ignore) { + } + } + + return list; + } } } diff --git a/src/main/java/ru/scarletredman/gd2spring/model/embedable/LevelRateInfo.java b/src/main/java/ru/scarletredman/gd2spring/model/embedable/LevelRateInfo.java index 9f45bbb..1ec3caf 100644 --- a/src/main/java/ru/scarletredman/gd2spring/model/embedable/LevelRateInfo.java +++ b/src/main/java/ru/scarletredman/gd2spring/model/embedable/LevelRateInfo.java @@ -3,6 +3,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.sql.Timestamp; +import java.util.LinkedList; +import java.util.List; import lombok.*; @Getter @@ -71,5 +73,42 @@ public enum Difficulty { public boolean isAuto() { return this == AUTO; } + + public static List parseGDSearch(String input, int demonFilter) { + var list = new LinkedList(); + if (input.equals("-")) return list; + + var nums = input.split(","); + for (var num : nums) { + int diff; + try { + diff = Integer.parseInt(num); + } catch (NumberFormatException ignore) { + continue; + } + + switch (diff) { + case -1 -> list.add(NONE); + case 1 -> list.add(EASY); + case 2 -> list.add(NORMAL); + case 3 -> list.add(HARD); + case 4 -> list.add(HARDER); + case 5 -> list.add(INSANE); + case -3 -> list.add(AUTO); + case -2 -> { + switch (demonFilter) { + case 0 -> list.add(DEMON); + case 1 -> list.add(EASY_DEMON); + case 2 -> list.add(MEDIUM_DEMON); + case 3 -> list.add(HARD_DEMON); + case 4 -> list.add(INSANE_DEMON); + case 5 -> list.add(EXTREME_DEMON); + } + } + } + } + + return list; + } } } diff --git a/src/main/java/ru/scarletredman/gd2spring/repository/impl/CustomLevelRepositoryImpl.java b/src/main/java/ru/scarletredman/gd2spring/repository/impl/CustomLevelRepositoryImpl.java index 9229b8f..efb5031 100644 --- a/src/main/java/ru/scarletredman/gd2spring/repository/impl/CustomLevelRepositoryImpl.java +++ b/src/main/java/ru/scarletredman/gd2spring/repository/impl/CustomLevelRepositoryImpl.java @@ -1,9 +1,12 @@ package ru.scarletredman.gd2spring.repository.impl; import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; import jakarta.persistence.PersistenceContext; import jakarta.persistence.criteria.*; import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; import org.springframework.lang.Nullable; import ru.scarletredman.gd2spring.model.Level; import ru.scarletredman.gd2spring.model.User; @@ -12,6 +15,7 @@ import ru.scarletredman.gd2spring.model.embedable.LevelRateInfo; import ru.scarletredman.gd2spring.repository.CustomLevelRepository; import ru.scarletredman.gd2spring.service.type.LevelListPage; +import ru.scarletredman.gd2spring.service.type.LevelSearchType; public class CustomLevelRepositoryImpl implements CustomLevelRepository { @@ -22,6 +26,10 @@ public class CustomLevelRepositoryImpl implements CustomLevelRepository { @Override public LevelListPage getLevels(LevelListPage.Filters filters) { + if (checkControversies(filters)) { + return new LevelListPage(new ArrayList<>(), 0, 0); + } + final String searchLevelName; { var temp = filters.name().trim(); @@ -37,12 +45,15 @@ public LevelListPage getLevels(LevelListPage.Filters filters) { var criteria = entityManager.getCriteriaBuilder(); + var selectById = selectById(searchLevelName, criteria); var selectQuery = selectLevelsQuery(criteria, searchLevelName, filters); - var levels = entityManager + var levels = new LinkedList(); + if (selectById != null) levels.add(selectById); + levels.addAll(entityManager .createQuery(selectQuery) .setFirstResult(10 * filters.page()) .setMaxResults(10) - .getResultList(); + .getResultList()); var total = entityManager .createQuery(countLevelsQuery(criteria, searchLevelName, filters)) @@ -51,6 +62,39 @@ public LevelListPage getLevels(LevelListPage.Filters filters) { return new LevelListPage(levels, total, filters.page()); } + private boolean checkControversies(LevelListPage.Filters filters) { + if (filters.page() < 0) return true; + if (filters.onlyCompleted() && filters.onlyUncompleted()) return true; + if ((filters.featured() || filters.epic()) && filters.noStar()) return true; + if (filters.song() < -1) return true; + if (filters.hasStar() && filters.noStar()) return true; + return false; + } + + private @Nullable GDLevelDTO selectById(@Nullable String input, CriteriaBuilder criteria) { + if (input == null) return null; + + long levelId; + try { + levelId = Long.parseLong(input); + } catch (NumberFormatException ex) { + return null; + } + + var query = criteria.createQuery(GDLevelDTO.class); + var rootLevel = query.from(Level.class); + var joinUser = rootLevel.join("owner", JoinType.INNER); + + query.select(createLevelDTO(criteria, rootLevel, joinUser)); + query.where(criteria.equal(rootLevel.get("id"), levelId)); + + try { + return entityManager.createQuery(query).getSingleResult(); + } catch (NoResultException ex) { + return null; + } + } + private CriteriaQuery selectLevelsQuery( CriteriaBuilder criteria, @Nullable String levelName, LevelListPage.Filters filters) { var query = criteria.createQuery(GDLevelDTO.class); @@ -59,11 +103,13 @@ private CriteriaQuery selectLevelsQuery( query.select(createLevelDTO(criteria, rootLevel, joinUser)); query.where(applyFilters(criteria, rootLevel, levelName, filters)); + // query.orderBy(applySorting(criteria, rootLevel, filters.type())); // TODO: FIX IT return query; } private CriteriaQuery countLevelsQuery( CriteriaBuilder criteria, @Nullable String levelName, LevelListPage.Filters filters) { + var query = criteria.createQuery(Long.class); var rootLevel = query.from(Level.class); @@ -72,18 +118,76 @@ private CriteriaQuery countLevelsQuery( return query; } + private List applySorting(CriteriaBuilder criteria, Root rootLevel, LevelSearchType type) { + return switch (type) { + case SEARCH_BY_NAME -> List.of(criteria.asc(rootLevel.get("name"))); + case MOST_DOWNLOADS -> List.of(criteria.desc(rootLevel.get("downloads"))); + case FEATURED, AWARDED, HALL_OF_FAME -> List.of( + criteria.desc(rootLevel.get("rate").get("rateTime"))); + case MAGIC -> List.of(criteria.desc(rootLevel.get("id"))); + default -> List.of(criteria.desc(rootLevel.get("likes"))); + }; + } + private Predicate[] applyFilters( CriteriaBuilder criteria, Root rootLevel, @Nullable String levelName, LevelListPage.Filters filters) { + var criteriaFilters = new ArrayList(); criteriaFilters.add(criteria.isFalse(rootLevel.get("unlisted"))); if (levelName != null) { criteriaFilters.add(criteria.like(criteria.lower(rootLevel.get("name")), levelName.toLowerCase() + "%")); } - // todo: implement filters - // todo: fix filters + if (!filters.difficulty().isEmpty()) { + criteriaFilters.add(rootLevel.get("rate").get("difficulty").in(filters.difficulty())); + } + if (!filters.length().isEmpty()) { + criteriaFilters.add(rootLevel.get("filters").get("length").in(filters.length())); + } + // todo: completed and uncompleted (need player leaderboards) + if (filters.featured()) { + criteriaFilters.add(criteria.isTrue(rootLevel.get("rate").get("featured"))); + } + if (filters.original()) { + criteriaFilters.add(rootLevel.get("original").isNull()); + } + if (filters.forTwoPlayers()) { + criteriaFilters.add(criteria.isTrue(rootLevel.get("filters").get("twoPlayers"))); + } + if (filters.coins()) { + criteriaFilters.add(criteria.gt(rootLevel.get("rate").get("coins"), 0)); + } + if (filters.epic()) { + criteriaFilters.add(criteria.isTrue(rootLevel.get("rate").get("epic"))); + } + if (filters.noStar()) { + criteriaFilters.add(criteria.equal(rootLevel.get("rate").get("stars"), 0)); + } + if (filters.hasStar()) { + criteriaFilters.add(criteria.gt(rootLevel.get("rate").get("stars"), 0)); + } + if (filters.song() != -1 && !filters.customSong()) { + criteriaFilters.add(criteria.equal(rootLevel.get("soundTrack"), filters.song())); + } + if (filters.song() != -1 && filters.customSong()) { + criteriaFilters.add(criteria.equal(rootLevel.get("songId"), filters.song())); + } + if (filters.type() == LevelSearchType.AWARDED + || filters.type() == LevelSearchType.FEATURED + || filters.type() == LevelSearchType.HALL_OF_FAME) { + criteriaFilters.add(criteria.isNotNull(rootLevel.get("rate").get("rateTime"))); + } + if (filters.type() == LevelSearchType.AWARDED) { + criteriaFilters.add(criteria.gt(rootLevel.get("rate").get("stars"), 0)); + } + if (filters.type() == LevelSearchType.FEATURED) { + criteriaFilters.add(criteria.isTrue(rootLevel.get("rate").get("featured"))); + } + if (filters.type() == LevelSearchType.HALL_OF_FAME) { + criteriaFilters.add(criteria.isTrue(rootLevel.get("rate").get("epic"))); + } return criteriaFilters.toArray(new Predicate[0]); } @@ -115,7 +219,7 @@ private CompoundSelection createLevelDTO( rootLevel.get("downloads"), rootLevel.get("soundTrack"), rootLevel.get("likes"), - rootLevel.get("original").get("id"), + rootLevel.get("original"), rootLevel.get("objects")); } } diff --git a/src/main/java/ru/scarletredman/gd2spring/service/type/LevelListPage.java b/src/main/java/ru/scarletredman/gd2spring/service/type/LevelListPage.java index 037d385..09da8c3 100644 --- a/src/main/java/ru/scarletredman/gd2spring/service/type/LevelListPage.java +++ b/src/main/java/ru/scarletredman/gd2spring/service/type/LevelListPage.java @@ -1,7 +1,6 @@ package ru.scarletredman.gd2spring.service.type; import java.util.List; -import org.springframework.lang.Nullable; import ru.scarletredman.gd2spring.model.dto.GDLevelDTO; import ru.scarletredman.gd2spring.model.embedable.LevelFilters; import ru.scarletredman.gd2spring.model.embedable.LevelRateInfo; @@ -10,10 +9,10 @@ public record LevelListPage(List levels, long total, int offset) { public record Filters( String name, - @Nullable LevelRateInfo.Difficulty difficulty, - @Nullable LevelFilters.Length length, + List difficulty, + List length, int page, - boolean uncompleted, + boolean onlyCompleted, boolean onlyUncompleted, boolean featured, boolean original, @@ -21,7 +20,8 @@ public record Filters( boolean coins, boolean epic, boolean noStar, - int demonFilter, + boolean hasStar, int song, - int customSong) {} + boolean customSong, + LevelSearchType type) {} } diff --git a/src/main/java/ru/scarletredman/gd2spring/service/type/LevelSearchType.java b/src/main/java/ru/scarletredman/gd2spring/service/type/LevelSearchType.java new file mode 100644 index 0000000..b620001 --- /dev/null +++ b/src/main/java/ru/scarletredman/gd2spring/service/type/LevelSearchType.java @@ -0,0 +1,33 @@ +package ru.scarletredman.gd2spring.service.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum LevelSearchType { + SEARCH_BY_NAME(0), + MOST_DOWNLOADS(1), + MOST_LIKED(2), + TRENDING(3), + FEATURED(6), + MAGIC(7), + MAP_PACK(10), + AWARDED(11), + FOLLOWED(12), + FRIENDS(13), + HALL_OF_FAME(16), + DAILY_SAFE(21), + WEEKLY_SAFE(22), + EVENT_SAFE(23), + ; + + private final int code; + + public static LevelSearchType find(int code) { + for (var type : values()) { + if (code == type.code) return type; + } + return MOST_LIKED; + } +}