Compare commits
57
Commits
f53fc9746e
...
textures
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb57289f8 | ||
|
|
60c1329163 | ||
|
|
0e71c0f581 | ||
|
|
eb67a33331 | ||
|
|
39f20796ce | ||
|
|
e00165c56f | ||
|
|
02c6497700 | ||
|
|
0efd476676 | ||
|
|
237518638c | ||
|
|
fea1a98cea | ||
|
|
ecd9b3d824 | ||
|
|
9808b5d63d | ||
|
|
c13b7077a7 | ||
|
|
023ae809ef | ||
|
|
3a6f137c9a | ||
|
|
cb8447a096 | ||
|
|
3e98e1a498 | ||
|
|
4c31a91bb4 | ||
|
|
d6faaba01c | ||
|
|
56175e62d6 | ||
|
|
283838f444 | ||
|
|
49fe335c73 | ||
|
|
1875f050c6 | ||
|
|
52d8658be3 | ||
|
|
32a454c034 | ||
|
|
07646e8c42 | ||
|
|
20dcebbab9 | ||
|
|
c4c17b3adc | ||
|
|
cf758bfe60 | ||
|
|
8c7ec0a237 | ||
|
|
80462218a7 | ||
|
|
26b5f86983 | ||
|
|
ba6cf6d938 | ||
|
|
643545a18a | ||
|
|
b922487d76 | ||
|
|
54eb1ea735 | ||
|
|
9043c774f7 | ||
|
|
43b75b8e74 | ||
|
|
07c77f5485 | ||
|
|
d54a7e51ee | ||
|
|
6be6944dea | ||
|
|
3babde5513 | ||
|
|
a1912b96d8 | ||
|
|
e9880f8864 | ||
|
|
3449cf24b1 | ||
|
|
43edd49273 | ||
|
|
3d4d77bc73 | ||
|
|
5b158ae3f7 | ||
|
|
bbedd2257e | ||
|
|
6f2a48338b | ||
|
|
e6df93ff5c | ||
|
|
680506997e | ||
|
|
235f4fb60f | ||
|
|
6dbb36399f | ||
|
|
25e8dc8e8e | ||
|
|
5759d6c0dc | ||
|
|
96cc4610dd |
@@ -90,3 +90,4 @@ frontend/src/api
|
|||||||
generated
|
generated
|
||||||
|
|
||||||
liquibase*.properties
|
liquibase*.properties
|
||||||
|
node
|
||||||
|
|||||||
+21
-42
@@ -2,7 +2,6 @@ plugins {
|
|||||||
java
|
java
|
||||||
id("org.springframework.boot") version "3.4.4"
|
id("org.springframework.boot") version "3.4.4"
|
||||||
id("io.spring.dependency-management") version "1.1.7"
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
// id("com.github.johnrengelman.shadow") version "8.1.1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "com.alttd.altitudeweb"
|
group = "com.alttd.altitudeweb"
|
||||||
@@ -27,14 +26,16 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":open_api"))
|
implementation(project(":open_api"))
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation(project(":frontend"))
|
||||||
annotationProcessor("org.projectlombok:lombok")
|
annotationProcessor("org.projectlombok:lombok")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
|
||||||
implementation("com.mysql:mysql-connector-j:8.0.32")
|
implementation("com.mysql:mysql-connector-j:8.0.32")
|
||||||
implementation("org.mybatis:mybatis:3.5.13")
|
implementation("org.mybatis:mybatis:3.5.13")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
implementation("org.springframework.boot:spring-boot-configuration-processor")
|
implementation("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-hateoas")
|
implementation("org.springframework.boot:spring-boot-starter-hateoas")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
|
implementation("org.springframework.security:spring-security-oauth2-resource-server")
|
||||||
|
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||||
|
|
||||||
//AOP
|
//AOP
|
||||||
implementation("org.aspectj:aspectjrt:1.9.19")
|
implementation("org.aspectj:aspectjrt:1.9.19")
|
||||||
@@ -42,6 +43,12 @@ dependencies {
|
|||||||
implementation("org.springframework:spring-aop")
|
implementation("org.springframework:spring-aop")
|
||||||
implementation("org.springframework:spring-aspects")
|
implementation("org.springframework:spring-aspects")
|
||||||
|
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.compileJava {
|
||||||
|
dependsOn(":frontend:npmBuild")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
@@ -54,42 +61,14 @@ tasks.bootJar {
|
|||||||
archiveClassifier.set("")
|
archiveClassifier.set("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
dependsOn("includeFrontend")
|
||||||
|
}
|
||||||
|
|
||||||
//tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
|
tasks.register<Copy>("includeFrontend") {
|
||||||
// mergeServiceFiles()
|
description = "Copy the built frontend to the Spring Boot static resources directory"
|
||||||
// dependencies {
|
dependsOn(":frontend:npmBuild")
|
||||||
// include(dependency("com.mysql:mysql-connector-j"))
|
from("${project.rootDir}/frontend/dist")
|
||||||
// }
|
into(layout.buildDirectory.dir("resources/main/static"))
|
||||||
//}
|
doNotTrackState("Cannot reliably track state in the static directory")
|
||||||
|
}
|
||||||
//tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
|
|
||||||
// manifest {
|
|
||||||
// attributes["Main-Class"] = "com.alttd.altitudeweb.AltitudeWebApplication"
|
|
||||||
// }
|
|
||||||
// archiveBaseName.set("altitudeweb")
|
|
||||||
// archiveClassifier.set("")
|
|
||||||
// mergeServiceFiles()
|
|
||||||
//
|
|
||||||
// // Include everything
|
|
||||||
// from(sourceSets.main.get().output)
|
|
||||||
//
|
|
||||||
// // Include all project dependencies
|
|
||||||
// configurations = listOf(project.configurations.runtimeClasspath.get())
|
|
||||||
//
|
|
||||||
// // Ensure MySQL is included (even though it should be part of runtimeClasspath already)
|
|
||||||
// dependencies {
|
|
||||||
// include(dependency("com.mysql:mysql-connector-j"))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Enable zip64 mode for large JARs
|
|
||||||
// isZip64 = true
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//// Make the shadowJar task the default jar task
|
|
||||||
//tasks.named("jar") {
|
|
||||||
// enabled = false
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//tasks.named("assemble") {
|
|
||||||
// dependsOn("shadowJar")
|
|
||||||
//}
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
|||||||
public class AltitudeWebApplication {
|
public class AltitudeWebApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(AltitudeWebApplication.class, args);
|
|
||||||
Connection.initDatabases();
|
Connection.initDatabases();
|
||||||
|
SpringApplication.run(AltitudeWebApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.alttd.altitudeweb.config;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.controllers.login.KeyPairService;
|
||||||
|
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||||
|
import com.nimbusds.jose.jwk.JWK;
|
||||||
|
import com.nimbusds.jose.jwk.JWKSet;
|
||||||
|
import com.nimbusds.jose.jwk.RSAKey;
|
||||||
|
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||||
|
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||||
|
import com.nimbusds.jose.proc.SecurityContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final KeyPairService keyPairService;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
return http
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/login/userLogin/**", "/login/requestNewUserLogin/**").permitAll()
|
||||||
|
.requestMatchers("/team/**", "/history/**").permitAll()
|
||||||
|
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
||||||
|
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
)
|
||||||
|
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
|
||||||
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtEncoder jwtEncoder() {
|
||||||
|
KeyPair keyPair = keyPairService.getJwtSigningKeyPair();
|
||||||
|
JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
|
||||||
|
.privateKey((RSAPrivateKey) keyPair.getPrivate())
|
||||||
|
.build();
|
||||||
|
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
|
||||||
|
return new NimbusJwtEncoder(jwkSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtDecoder jwtDecoder() {
|
||||||
|
KeyPair keyPair = keyPairService.getJwtSigningKeyPair();
|
||||||
|
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.alttd.altitudeweb.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/**")
|
||||||
|
.addResourceLocations("classpath:/static/")
|
||||||
|
.resourceChain(true)
|
||||||
|
.addResolver(new PathResourceResolver() {
|
||||||
|
@Override
|
||||||
|
protected Resource getResource(String resourcePath, Resource location) throws IOException {
|
||||||
|
Resource requestedResource = location.createRelative(resourcePath);
|
||||||
|
|
||||||
|
if (requestedResource.exists() && requestedResource.isReadable()) {
|
||||||
|
return requestedResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClassPathResource("/static/index.html");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.application;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.AppealsApi;
|
||||||
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
|
import com.alttd.altitudeweb.model.AppealResponseDto;
|
||||||
|
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||||
|
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||||
|
import com.alttd.altitudeweb.model.UpdateMailDto;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.HOURS)
|
||||||
|
public class AppealController implements AppealsApi {
|
||||||
|
|
||||||
|
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<MinecraftAppealDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
|
||||||
|
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal")
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<AppealResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||||
|
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Minecraft appeals are not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<AppealResponseDto> updateMail(UpdateMailDto updateMailDto) {
|
||||||
|
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Updating mail is not yet supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
-31
@@ -1,13 +1,13 @@
|
|||||||
package com.alttd.altitudeweb.controllers.history;
|
package com.alttd.altitudeweb.controllers.history;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.api.HistoryApi;
|
import com.alttd.altitudeweb.api.HistoryApi;
|
||||||
import com.alttd.altitudeweb.controllers.limits.RateLimit;
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
import com.alttd.altitudeweb.model.HistoryCountDto;
|
import com.alttd.altitudeweb.model.HistoryCountDto;
|
||||||
|
import com.alttd.altitudeweb.model.PunishmentHistoryListDto;
|
||||||
import com.alttd.altitudeweb.setup.Connection;
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
import com.alttd.altitudeweb.database.litebans.*;
|
import com.alttd.altitudeweb.database.litebans.*;
|
||||||
import com.alttd.altitudeweb.model.PunishmentHistoryDto;
|
import com.alttd.altitudeweb.model.PunishmentHistoryDto;
|
||||||
import com.alttd.altitudeweb.model.PunishmentHistoryInnerDto;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -19,19 +19,19 @@ import java.util.concurrent.TimeUnit;
|
|||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.MINUTES)
|
@RateLimit(limit = 30, timeValue = 10, timeUnit = TimeUnit.SECONDS)
|
||||||
public class HistoryApiController implements HistoryApi {
|
public class HistoryApiController implements HistoryApi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<PunishmentHistoryDto> getHistoryForAll(String userType, String type, Integer page) {
|
public ResponseEntity<PunishmentHistoryListDto> getHistoryForAll(String userType, String type, Integer page) {
|
||||||
return getHistoryForUsers(userType, type, "", page);
|
return getHistoryForUsers(userType, type, "", page);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<PunishmentHistoryDto> getHistoryForUsers(String userType, String type, String user, Integer page) {
|
public ResponseEntity<PunishmentHistoryListDto> getHistoryForUsers(String userType, String type, String user, Integer page) {
|
||||||
UserType userTypeEnum = UserType.getUserType(userType);
|
UserType userTypeEnum = UserType.getUserType(userType);
|
||||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto();
|
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||||
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
|
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
|
||||||
|
|
||||||
Connection.getConnection(Databases.LITE_BANS)
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
@@ -46,14 +46,14 @@ public class HistoryApiController implements HistoryApi {
|
|||||||
historyRecords.completeExceptionally(e);
|
historyRecords.completeExceptionally(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return mapPunishmentHistory(punishmentHistory, historyRecords);
|
return mapPunishmentHistory(punishmentHistoryList, historyRecords);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<PunishmentHistoryDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) {
|
public ResponseEntity<PunishmentHistoryListDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) {
|
||||||
UserType userTypeEnum = UserType.getUserType(userType);
|
UserType userTypeEnum = UserType.getUserType(userType);
|
||||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto();
|
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||||
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
|
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
|
||||||
|
|
||||||
Connection.getConnection(Databases.LITE_BANS)
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
@@ -68,7 +68,7 @@ public class HistoryApiController implements HistoryApi {
|
|||||||
historyRecords.completeExceptionally(e);
|
historyRecords.completeExceptionally(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return mapPunishmentHistory(punishmentHistory, historyRecords);
|
return mapPunishmentHistory(punishmentHistoryList, historyRecords);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -153,6 +153,44 @@ public class HistoryApiController implements HistoryApi {
|
|||||||
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
|
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<PunishmentHistoryListDto> getAllHistoryForUUID(String uuid) {
|
||||||
|
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||||
|
CompletableFuture<List<HistoryRecord>> historyRecordsCompletableFuture = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading all history for uuid {}", uuid);
|
||||||
|
try {
|
||||||
|
List<HistoryRecord> punishments = sqlSession.getMapper(UUIDHistoryMapper.class)
|
||||||
|
.getAllHistoryForUUID(UUID.fromString(uuid));
|
||||||
|
historyRecordsCompletableFuture.complete(punishments);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load all history for uuid {}", uuid, e);
|
||||||
|
historyRecordsCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mapPunishmentHistory(punishmentHistoryList, historyRecordsCompletableFuture);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
|
||||||
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
CompletableFuture<HistoryRecord> historyRecordCompletableFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading history by id");
|
||||||
|
try {
|
||||||
|
HistoryRecord punishment = sqlSession.getMapper(IdHistoryMapper.class)
|
||||||
|
.getRecentHistory(historyTypeEnum, id);
|
||||||
|
historyRecordCompletableFuture.complete(punishment);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load history count", e);
|
||||||
|
historyRecordCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ResponseEntity.ok().body(mapPunishmentHistory(historyRecordCompletableFuture.join()));
|
||||||
|
}
|
||||||
|
|
||||||
private ResponseEntity<HistoryCountDto> mapHistoryCount(HistoryCount historyCount) {
|
private ResponseEntity<HistoryCountDto> mapHistoryCount(HistoryCount historyCount) {
|
||||||
HistoryCountDto historyCountDto = new HistoryCountDto();
|
HistoryCountDto historyCountDto = new HistoryCountDto();
|
||||||
historyCountDto.setBans(historyCount.getBans());
|
historyCountDto.setBans(historyCount.getBans());
|
||||||
@@ -162,28 +200,33 @@ public class HistoryApiController implements HistoryApi {
|
|||||||
return ResponseEntity.ok().body(historyCountDto);
|
return ResponseEntity.ok().body(historyCountDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResponseEntity<PunishmentHistoryDto> mapPunishmentHistory(PunishmentHistoryDto punishmentHistory, CompletableFuture<List<HistoryRecord>> historyRecords) {
|
private ResponseEntity<PunishmentHistoryListDto> mapPunishmentHistory(PunishmentHistoryListDto punishmentHistoryList, CompletableFuture<List<HistoryRecord>> historyRecords) {
|
||||||
historyRecords.join().forEach(historyRecord -> {
|
historyRecords.join().forEach(historyRecord -> {
|
||||||
PunishmentHistoryInnerDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) {
|
PunishmentHistoryDto innerDto = mapPunishmentHistory(historyRecord);
|
||||||
case "ban" -> PunishmentHistoryInnerDto.TypeEnum.BAN;
|
punishmentHistoryList.add(innerDto);
|
||||||
case "mute" -> PunishmentHistoryInnerDto.TypeEnum.MUTE;
|
|
||||||
case "warn" -> PunishmentHistoryInnerDto.TypeEnum.WARN;
|
|
||||||
case "kick" -> PunishmentHistoryInnerDto.TypeEnum.KICK;
|
|
||||||
default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType());
|
|
||||||
};
|
|
||||||
PunishmentHistoryInnerDto innerDto = new PunishmentHistoryInnerDto()
|
|
||||||
.uuid(historyRecord.getUuid())
|
|
||||||
.username(historyRecord.getPunishedName())
|
|
||||||
.reason(historyRecord.getReason())
|
|
||||||
.punishedByUuid(historyRecord.getBannedByUuid())
|
|
||||||
.punishedBy(historyRecord.getBannedByName())
|
|
||||||
.removedBy(historyRecord.getRemovedByName())
|
|
||||||
.punishmentTime(historyRecord.getTime())
|
|
||||||
.expiryTime(historyRecord.getUntil())
|
|
||||||
.removedReason(historyRecord.getRemovedByReason())
|
|
||||||
.type(type);
|
|
||||||
punishmentHistory.add(innerDto);
|
|
||||||
});
|
});
|
||||||
return ResponseEntity.ok().body(punishmentHistory);
|
return ResponseEntity.ok().body(punishmentHistoryList);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PunishmentHistoryDto mapPunishmentHistory(HistoryRecord historyRecord) {
|
||||||
|
PunishmentHistoryDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) {
|
||||||
|
case "ban" -> PunishmentHistoryDto.TypeEnum.BAN;
|
||||||
|
case "mute" -> PunishmentHistoryDto.TypeEnum.MUTE;
|
||||||
|
case "warn" -> PunishmentHistoryDto.TypeEnum.WARN;
|
||||||
|
case "kick" -> PunishmentHistoryDto.TypeEnum.KICK;
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType());
|
||||||
|
};
|
||||||
|
return new PunishmentHistoryDto()
|
||||||
|
.uuid(historyRecord.getUuid())
|
||||||
|
.username(historyRecord.getPunishedName())
|
||||||
|
.reason(historyRecord.getReason())
|
||||||
|
.punishedByUuid(historyRecord.getBannedByUuid())
|
||||||
|
.punishedBy(historyRecord.getBannedByName())
|
||||||
|
.removedBy(historyRecord.getRemovedByName())
|
||||||
|
.punishmentTime(historyRecord.getTime())
|
||||||
|
.expiryTime(historyRecord.getUntil())
|
||||||
|
.removedReason(historyRecord.getRemovedByReason())
|
||||||
|
.type(type)
|
||||||
|
.id(historyRecord.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.login;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.KeyPairEntity;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||||
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.security.*;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class KeyPairService {
|
||||||
|
|
||||||
|
private KeyPair cachedKeyPair = null;
|
||||||
|
private static final String RSA_ALGORITHM = "RSA";
|
||||||
|
private static final int RSA_KEY_SIZE = 2048;
|
||||||
|
|
||||||
|
public KeyPair getJwtSigningKeyPair() {
|
||||||
|
if (cachedKeyPair != null) {
|
||||||
|
return cachedKeyPair;
|
||||||
|
}
|
||||||
|
KeyPair keyPair = getOrCreateKeyPair();
|
||||||
|
if (keyPair != null) {
|
||||||
|
cachedKeyPair = keyPair;
|
||||||
|
return cachedKeyPair;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Failed to generate or load key pair");
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPair getOrCreateKeyPair() {
|
||||||
|
CompletableFuture<KeyPairEntity> keyPairFuture = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading key pair");
|
||||||
|
try {
|
||||||
|
KeyPairEntity entity = sqlSession.getMapper(KeyPairMapper.class).getKeyPair();
|
||||||
|
keyPairFuture.complete(entity);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to key pair", e);
|
||||||
|
keyPairFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
KeyPairEntity keyPairEntity = keyPairFuture.join();
|
||||||
|
if (keyPairEntity != null) {
|
||||||
|
try {
|
||||||
|
byte[] privateKeyBytes = Base64.getDecoder().decode(keyPairEntity.getPrivateKey());
|
||||||
|
byte[] publicKeyBytes = Base64.getDecoder().decode(keyPairEntity.getPublicKey());
|
||||||
|
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
|
||||||
|
PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
|
||||||
|
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
|
||||||
|
|
||||||
|
return new KeyPair(publicKey, privateKey);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load key pair from database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyPair keyPair = generateKeyPair();
|
||||||
|
|
||||||
|
try {
|
||||||
|
KeyPairEntity entity = new KeyPairEntity();
|
||||||
|
entity.setPrivateKey(Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
|
||||||
|
entity.setPublicKey(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
|
||||||
|
entity.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Saving key pair");
|
||||||
|
try {
|
||||||
|
sqlSession.getMapper(KeyPairMapper.class).save(entity);
|
||||||
|
log.info("Generated and saved new key pair");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to key pair", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to save key pair to database", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyPair generateKeyPair() {
|
||||||
|
try {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
|
||||||
|
keyPairGenerator.initialize(RSA_KEY_SIZE);
|
||||||
|
return keyPairGenerator.generateKeyPair();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("Error generating key pair", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.login;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.LoginApi;
|
||||||
|
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.PrivilegedUser;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
|
||||||
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
public class LoginController implements LoginApi {
|
||||||
|
|
||||||
|
private final JwtEncoder jwtEncoder;
|
||||||
|
|
||||||
|
@Value("${login.secret:#{null}}")
|
||||||
|
private String loginSecret;
|
||||||
|
|
||||||
|
private record CacheEntry(UUID uuid, Instant expiry) {}
|
||||||
|
|
||||||
|
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 300000) // 5 minutes in milliseconds
|
||||||
|
private void clearExpiredCacheEntries() {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
int initialCacheSize = cache.size();
|
||||||
|
cache.entrySet().removeIf(entry -> entry.getValue().expiry().isBefore(now));
|
||||||
|
log.info("Cleared {} expired cache entries", initialCacheSize - cache.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@RateLimit(limit = 100, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "addLogin")
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<String> requestLogin(String authorization, String uuid) {
|
||||||
|
UUID uuidFromString;
|
||||||
|
try {
|
||||||
|
uuidFromString = UUID.fromString(uuid);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorization == null || !authorization.startsWith("SECRET ")) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String secret = authorization.substring("SECRET ".length());
|
||||||
|
if (!isValidSecret(secret)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<String> key = cache.entrySet().stream()
|
||||||
|
.filter(entry -> entry.getValue().uuid.equals(uuidFromString))
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
if (key.isPresent()) {
|
||||||
|
return ResponseEntity.ok(key.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
String loginCode = generateLoginCode(uuidFromString);
|
||||||
|
return ResponseEntity.ok(loginCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<String> login(String code) {
|
||||||
|
CacheEntry cacheEntry1 = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
|
||||||
|
cache.put("23232323", cacheEntry1);
|
||||||
|
if (code == null) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheEntry cacheEntry = cache.get(code);
|
||||||
|
if (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now())) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = generateToken(cacheEntry.uuid);
|
||||||
|
|
||||||
|
cache.remove(code);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateLoginCode(UUID uuid) {
|
||||||
|
String characters = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||||
|
StringBuilder loginCode = new StringBuilder();
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
int index = (int) (Math.random() * characters.length());
|
||||||
|
loginCode.append(characters.charAt(index));
|
||||||
|
}
|
||||||
|
CacheEntry cacheEntry = new CacheEntry(uuid,
|
||||||
|
Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(15)));
|
||||||
|
cache.put(loginCode.toString(), cacheEntry);
|
||||||
|
return loginCode.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidSecret(String secret) {
|
||||||
|
if (loginSecret == null) {
|
||||||
|
log.warn("No login secret set, skipping secret validation");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (loginSecret.length() < 16) {
|
||||||
|
log.warn("Login secret is too short, skipping secret validation");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!loginSecret.equals(secret)) {
|
||||||
|
log.info("Received invalid secret attempt");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateToken(UUID uuid) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
//TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour)
|
||||||
|
Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
|
||||||
|
CompletableFuture<PrivilegedUser> privilegedUserCompletableFuture = new CompletableFuture<>();
|
||||||
|
List<PermissionClaimDto> claimList = new ArrayList<>();
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
try {
|
||||||
|
PrivilegedUser privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
|
||||||
|
.getUserByUuid(uuid.toString());
|
||||||
|
|
||||||
|
privilegedUserCompletableFuture.complete(privilegedUser);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load user by uuid", e);
|
||||||
|
privilegedUserCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
PrivilegedUser privilegedUser = privilegedUserCompletableFuture.join();
|
||||||
|
claimList.add(PermissionClaimDto.USER);
|
||||||
|
if (privilegedUser != null) {
|
||||||
|
privilegedUser.getPermissions().forEach(permission -> {
|
||||||
|
try {
|
||||||
|
claimList.add(PermissionClaimDto.valueOf(permission));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("Received invalid permission claim: {}", permission);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||||
|
.issuer("altitudeweb")
|
||||||
|
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
|
||||||
|
.issuedAt(now)
|
||||||
|
.expiresAt(expiryTime)
|
||||||
|
.subject(uuid.toString())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.alttd.altitudeweb.controllers.team;
|
package com.alttd.altitudeweb.controllers.team;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.api.TeamApi;
|
import com.alttd.altitudeweb.api.TeamApi;
|
||||||
import com.alttd.altitudeweb.controllers.limits.RateLimit;
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
import com.alttd.altitudeweb.setup.Connection;
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
import com.alttd.altitudeweb.database.luckperms.Player;
|
import com.alttd.altitudeweb.database.luckperms.Player;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.alttd.altitudeweb.controllers.limits;
|
package com.alttd.altitudeweb.services.limits;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.alttd.altitudeweb.controllers.limits;
|
package com.alttd.altitudeweb.services.limits;
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
package com.alttd.altitudeweb.controllers.limits;
|
package com.alttd.altitudeweb.services.limits;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -26,8 +26,8 @@ public class RateLimitAspect {
|
|||||||
private final InMemoryRateLimiterService rateLimiterService;
|
private final InMemoryRateLimiterService rateLimiterService;
|
||||||
|
|
||||||
@Around("""
|
@Around("""
|
||||||
@annotation(com.alttd.altitudeweb.controllers.limits.RateLimit)
|
@annotation(com.alttd.altitudeweb.services.limits.RateLimit)
|
||||||
|| @within(com.alttd.altitudeweb.controllers.limits.RateLimit)""")
|
|| @within(com.alttd.altitudeweb.services.limits.RateLimit)""")
|
||||||
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
|
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
if (requestAttributes == null) {
|
if (requestAttributes == null) {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.alttd.altitudeweb.controllers.limits;
|
package com.alttd.altitudeweb.services.limits;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.alttd.altitudeweb.services.user;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String uuid) throws UsernameNotFoundException {
|
||||||
|
try {
|
||||||
|
//Validate uuid
|
||||||
|
UUID.fromString(uuid);
|
||||||
|
return new User(uuid, "", Collections.emptyList());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new UsernameNotFoundException("Invalid UUID format: " + uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
spring.application.name=AltitudeWeb
|
||||||
|
database.name=${DB_NAME:web_db}
|
||||||
|
database.port=${DB_PORT:3306}
|
||||||
|
database.host=${DB_HOST:localhost}
|
||||||
|
database.user=${DB_USER:root}
|
||||||
|
database.password=${DB_PASSWORD:root}
|
||||||
|
cors.allowed-origins=${CORS:https://beta.alttd.com}
|
||||||
|
logging.level.com.alttd.altitudeweb=INFO
|
||||||
@@ -5,4 +5,5 @@ database.host=${DB_HOST:localhost}
|
|||||||
database.user=${DB_USER:root}
|
database.user=${DB_USER:root}
|
||||||
database.password=${DB_PASSWORD:root}
|
database.password=${DB_PASSWORD:root}
|
||||||
cors.allowed-origins=${CORS:https://alttd.com}
|
cors.allowed-origins=${CORS:https://alttd.com}
|
||||||
|
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||||
logging.level.com.alttd.altitudeweb=INFO
|
logging.level.com.alttd.altitudeweb=INFO
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ dependencies {
|
|||||||
implementation("org.mybatis:mybatis:3.5.13")
|
implementation("org.mybatis:mybatis:3.5.13")
|
||||||
compileOnly("org.slf4j:slf4j-api:2.0.17")
|
compileOnly("org.slf4j:slf4j-api:2.0.17")
|
||||||
compileOnly("org.slf4j:slf4j-simple:2.0.17")
|
compileOnly("org.slf4j:slf4j-simple:2.0.17")
|
||||||
|
compileOnly("org.jetbrains:annotations:26.0.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class HistoryRecord {
|
public class HistoryRecord {
|
||||||
private String uuid;
|
private String uuid;
|
||||||
|
private int id;
|
||||||
private String punishedName;
|
private String punishedName;
|
||||||
private String reason;
|
private String reason;
|
||||||
private String bannedByUuid;
|
private String bannedByUuid;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.alttd.altitudeweb.database.litebans;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Result;
|
||||||
|
import org.apache.ibatis.annotations.Results;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
public interface IdHistoryMapper {
|
||||||
|
@Results({
|
||||||
|
@Result(property = "id", column = "id"),
|
||||||
|
@Result(property = "uuid", column = "uuid"),
|
||||||
|
@Result(property = "punishedName", column = "punished_name"),
|
||||||
|
@Result(property = "reason", column = "reason"),
|
||||||
|
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
|
||||||
|
@Result(property = "bannedByName", column = "banned_by_name"),
|
||||||
|
@Result(property = "removedByName", column = "removed_by_name"),
|
||||||
|
@Result(property = "time", column = "time"),
|
||||||
|
@Result(property = "until", column = "until"),
|
||||||
|
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||||
|
})
|
||||||
|
@Select("""
|
||||||
|
SELECT punishment.id,
|
||||||
|
punishment.uuid,
|
||||||
|
user_lookup.name AS punished_name,
|
||||||
|
punishment.reason,
|
||||||
|
punishment.banned_by_uuid,
|
||||||
|
punishment.banned_by_name,
|
||||||
|
punishment.removed_by_name,
|
||||||
|
punishment.time,
|
||||||
|
punishment.until,
|
||||||
|
punishment.removed_by_reason
|
||||||
|
FROM (SELECT *
|
||||||
|
FROM ${table_name} AS punishment
|
||||||
|
WHERE punishment.id = #{id})
|
||||||
|
AS punishment
|
||||||
|
INNER JOIN user_lookup
|
||||||
|
ON user_lookup.uuid = punishment.uuid
|
||||||
|
""")
|
||||||
|
HistoryRecord getHistoryForId(@Param("table_name") String tableName,
|
||||||
|
@Param("type") String type,
|
||||||
|
@Param("id") int id);
|
||||||
|
|
||||||
|
default HistoryRecord getRecentHistory(HistoryType historyType, int id) {
|
||||||
|
HistoryRecord historyRecord = switch (historyType) {
|
||||||
|
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
|
||||||
|
case BAN -> getHistoryForId("litebans_bans", "ban", id);
|
||||||
|
case MUTE -> getHistoryForId("litebans_mutes", "mute", id);
|
||||||
|
case KICK -> getHistoryForId("litebans_kicks", "kick", id);
|
||||||
|
case WARN -> getHistoryForId("litebans_warnings", "warn", id);
|
||||||
|
};
|
||||||
|
historyRecord.setType(historyType.name().toLowerCase());
|
||||||
|
return historyRecord;
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-36
@@ -4,6 +4,7 @@ import org.apache.ibatis.annotations.Param;
|
|||||||
import org.apache.ibatis.annotations.Result;
|
import org.apache.ibatis.annotations.Result;
|
||||||
import org.apache.ibatis.annotations.Results;
|
import org.apache.ibatis.annotations.Results;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ public interface NameHistoryMapper {
|
|||||||
* order
|
* order
|
||||||
*/
|
*/
|
||||||
@Results({
|
@Results({
|
||||||
|
@Result(property = "id", column = "id"),
|
||||||
@Result(property = "uuid", column = "uuid"),
|
@Result(property = "uuid", column = "uuid"),
|
||||||
@Result(property = "punishedName", column = "punished_name"),
|
@Result(property = "punishedName", column = "punished_name"),
|
||||||
@Result(property = "reason", column = "reason"),
|
@Result(property = "reason", column = "reason"),
|
||||||
@@ -36,7 +38,8 @@ public interface NameHistoryMapper {
|
|||||||
@Result(property = "type", column = "type")
|
@Result(property = "type", column = "type")
|
||||||
})
|
})
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT all_punishments.uuid,
|
SELECT id,
|
||||||
|
all_punishments.uuid,
|
||||||
user_lookup.name AS punished_name,
|
user_lookup.name AS punished_name,
|
||||||
reason,
|
reason,
|
||||||
banned_by_uuid,
|
banned_by_uuid,
|
||||||
@@ -62,7 +65,6 @@ public interface NameHistoryMapper {
|
|||||||
* Retrieves a list of all types of recent punishment history records sorted
|
* Retrieves a list of all types of recent punishment history records sorted
|
||||||
* in descending time order. This result does NOT contain kicks history
|
* in descending time order. This result does NOT contain kicks history
|
||||||
*
|
*
|
||||||
* @param nameColumn the column name in the database indicating the name to use for filtering
|
|
||||||
* @param limit the maximum number of records to fetch
|
* @param limit the maximum number of records to fetch
|
||||||
* @param offset the starting offset position of the result set
|
* @param offset the starting offset position of the result set
|
||||||
*
|
*
|
||||||
@@ -70,6 +72,7 @@ public interface NameHistoryMapper {
|
|||||||
* order
|
* order
|
||||||
*/
|
*/
|
||||||
@Results({
|
@Results({
|
||||||
|
@Result(property = "id", column = "id"),
|
||||||
@Result(property = "uuid", column = "uuid"),
|
@Result(property = "uuid", column = "uuid"),
|
||||||
@Result(property = "punishedName", column = "punished_name"),
|
@Result(property = "punishedName", column = "punished_name"),
|
||||||
@Result(property = "reason", column = "reason"),
|
@Result(property = "reason", column = "reason"),
|
||||||
@@ -82,17 +85,19 @@ public interface NameHistoryMapper {
|
|||||||
@Result(property = "type", column = "type")
|
@Result(property = "type", column = "type")
|
||||||
})
|
})
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT punishments.uuid,
|
SELECT punishment.id,
|
||||||
|
punishment.uuid,
|
||||||
user_lookup.name AS punished_name,
|
user_lookup.name AS punished_name,
|
||||||
punishments.reason,
|
punishment.reason,
|
||||||
punishments.banned_by_uuid,
|
punishment.banned_by_uuid,
|
||||||
punishments.banned_by_name,
|
punishment.banned_by_name,
|
||||||
punishments.removed_by_name,
|
punishment.removed_by_name,
|
||||||
punishments.time,
|
punishment.time,
|
||||||
punishments.until,
|
punishment.until,
|
||||||
punishments.removed_by_reason,
|
punishment.removed_by_reason,
|
||||||
punishments.type
|
punishment.type
|
||||||
FROM (SELECT uuid,
|
FROM (SELECT uuid,
|
||||||
|
id,
|
||||||
reason,
|
reason,
|
||||||
banned_by_uuid,
|
banned_by_uuid,
|
||||||
banned_by_name,
|
banned_by_name,
|
||||||
@@ -103,9 +108,9 @@ public interface NameHistoryMapper {
|
|||||||
type
|
type
|
||||||
FROM all_punishments
|
FROM all_punishments
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
LIMIT #{limit} OFFSET #{offset}) AS punishments
|
LIMIT #{limit} OFFSET #{offset}) AS punishment
|
||||||
INNER JOIN user_lookup
|
INNER JOIN user_lookup
|
||||||
ON user_lookup.uuid = punishments.uuid;
|
ON user_lookup.uuid = punishment.uuid;
|
||||||
""")
|
""")
|
||||||
List<HistoryRecord> getRecentAllHistory(@Param("limit") int limit,
|
List<HistoryRecord> getRecentAllHistory(@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
@@ -124,6 +129,7 @@ public interface NameHistoryMapper {
|
|||||||
* order
|
* order
|
||||||
*/
|
*/
|
||||||
@Results({
|
@Results({
|
||||||
|
@Result(property = "id", column = "id"),
|
||||||
@Result(property = "uuid", column = "uuid"),
|
@Result(property = "uuid", column = "uuid"),
|
||||||
@Result(property = "punishedName", column = "punished_name"),
|
@Result(property = "punishedName", column = "punished_name"),
|
||||||
@Result(property = "reason", column = "reason"),
|
@Result(property = "reason", column = "reason"),
|
||||||
@@ -135,22 +141,24 @@ public interface NameHistoryMapper {
|
|||||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||||
})
|
})
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT punishment.uuid,
|
SELECT punishment.id,
|
||||||
|
punishment.uuid,
|
||||||
user_lookup.name AS punished_name,
|
user_lookup.name AS punished_name,
|
||||||
reason,
|
punishment.reason,
|
||||||
banned_by_uuid,
|
punishment.banned_by_uuid,
|
||||||
banned_by_name,
|
punishment.banned_by_name,
|
||||||
removed_by_name,
|
punishment.removed_by_name,
|
||||||
time,
|
punishment.time,
|
||||||
until,
|
punishment.until,
|
||||||
removed_by_reason
|
punishment.removed_by_reason
|
||||||
FROM ${tableName} AS punishment
|
FROM ${table_name} AS punishment
|
||||||
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
|
INNER JOIN user_lookup
|
||||||
|
ON user_lookup.uuid = punishment.uuid
|
||||||
WHERE ${name_column} LIKE #{partialName}
|
WHERE ${name_column} LIKE #{partialName}
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
LIMIT #{limit} OFFSET #{offset}
|
LIMIT #{limit} OFFSET #{offset}
|
||||||
""")
|
""")
|
||||||
List<HistoryRecord> getRecentHistoryForName(@Param("tableName") String tableName,
|
List<HistoryRecord> getRecentHistoryForName(@Param("table_name") String tableName,
|
||||||
@Param("partialName") String partialName,
|
@Param("partialName") String partialName,
|
||||||
@Param("name_column") String nameColumn,
|
@Param("name_column") String nameColumn,
|
||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@@ -161,8 +169,6 @@ public interface NameHistoryMapper {
|
|||||||
* parameters. The records are sorted in descending order by time and limited to a specified range.
|
* parameters. The records are sorted in descending order by time and limited to a specified range.
|
||||||
*
|
*
|
||||||
* @param tableName the name of the database table to query the history records from
|
* @param tableName the name of the database table to query the history records from
|
||||||
* @param partialName a partial or complete name of the user to filter the records, case-insensitive
|
|
||||||
* @param nameColumn the column name in the database indicating the name to use for filtering
|
|
||||||
* @param limit the maximum number of records to fetch
|
* @param limit the maximum number of records to fetch
|
||||||
* @param offset the starting offset position of the result set
|
* @param offset the starting offset position of the result set
|
||||||
*
|
*
|
||||||
@@ -170,6 +176,7 @@ public interface NameHistoryMapper {
|
|||||||
* order
|
* order
|
||||||
*/
|
*/
|
||||||
@Results({
|
@Results({
|
||||||
|
@Result(property = "id", column = "id"),
|
||||||
@Result(property = "uuid", column = "uuid"),
|
@Result(property = "uuid", column = "uuid"),
|
||||||
@Result(property = "punishedName", column = "punished_name"),
|
@Result(property = "punishedName", column = "punished_name"),
|
||||||
@Result(property = "reason", column = "reason"),
|
@Result(property = "reason", column = "reason"),
|
||||||
@@ -181,7 +188,8 @@ public interface NameHistoryMapper {
|
|||||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||||
})
|
})
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT punishment.uuid,
|
SELECT punishment.id,
|
||||||
|
punishment.uuid,
|
||||||
user_lookup.name AS punished_name,
|
user_lookup.name AS punished_name,
|
||||||
punishment.reason,
|
punishment.reason,
|
||||||
punishment.banned_by_uuid,
|
punishment.banned_by_uuid,
|
||||||
@@ -191,7 +199,8 @@ public interface NameHistoryMapper {
|
|||||||
punishment.until,
|
punishment.until,
|
||||||
punishment.removed_by_reason
|
punishment.removed_by_reason
|
||||||
FROM (
|
FROM (
|
||||||
SELECT uuid,
|
SELECT id,
|
||||||
|
uuid,
|
||||||
reason,
|
reason,
|
||||||
banned_by_uuid,
|
banned_by_uuid,
|
||||||
banned_by_name,
|
banned_by_name,
|
||||||
@@ -199,14 +208,13 @@ public interface NameHistoryMapper {
|
|||||||
time,
|
time,
|
||||||
until,
|
until,
|
||||||
removed_by_reason
|
removed_by_reason
|
||||||
FROM ${tableName} AS punishment
|
FROM ${table_name} AS punishment
|
||||||
WHERE ${name_column} LIKE #{partialName}
|
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
LIMIT #{limit} OFFSET #{offset}
|
LIMIT #{limit} OFFSET #{offset}
|
||||||
) AS punishment
|
) AS punishment
|
||||||
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
|
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
|
||||||
""")
|
""")
|
||||||
List<HistoryRecord> getRecentHistory(@Param("tableName") String tableName,
|
List<HistoryRecord> getRecentHistory(@Param("table_name") String tableName,
|
||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
@@ -254,23 +262,23 @@ public interface NameHistoryMapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) {
|
private @NotNull List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) {
|
||||||
return addType(getRecent("litebans_bans", userType, partialName, page), "ban");
|
return addType(getRecent("litebans_bans", userType, partialName, page), "ban");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) {
|
private @NotNull List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) {
|
||||||
return addType(getRecent("litebans_kicks", userType, partialName, page), "kick");
|
return addType(getRecent("litebans_kicks", userType, partialName, page), "kick");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) {
|
private @NotNull List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) {
|
||||||
return addType(getRecent("litebans_mutes", userType, partialName, page), "mute");
|
return addType(getRecent("litebans_mutes", userType, partialName, page), "mute");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) {
|
private @NotNull List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) {
|
||||||
return addType(getRecent("litebans_warnings", userType, partialName, page), "warn");
|
return addType(getRecent("litebans_warnings", userType, partialName, page), "warn");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) {
|
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, String type) {
|
||||||
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
|
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
|
||||||
return historyRecords;
|
return historyRecords;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-15
@@ -1,10 +1,11 @@
|
|||||||
package com.alttd.altitudeweb.database.litebans;
|
package com.alttd.altitudeweb.database.litebans;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.type_handler.UUIDTypeHandler;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Result;
|
import org.apache.ibatis.annotations.Result;
|
||||||
import org.apache.ibatis.annotations.Results;
|
import org.apache.ibatis.annotations.Results;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -14,10 +15,11 @@ public interface UUIDHistoryMapper {
|
|||||||
int PAGE_SIZE = 10;
|
int PAGE_SIZE = 10;
|
||||||
|
|
||||||
@Results({
|
@Results({
|
||||||
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
@Result(property = "id", column = "id"),
|
||||||
|
@Result(property = "uuid", column = "uuid"),
|
||||||
@Result(property = "punishedName", column = "punished_name"),
|
@Result(property = "punishedName", column = "punished_name"),
|
||||||
@Result(property = "reason", column = "reason"),
|
@Result(property = "reason", column = "reason"),
|
||||||
@Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
|
||||||
@Result(property = "bannedByName", column = "banned_by_name"),
|
@Result(property = "bannedByName", column = "banned_by_name"),
|
||||||
@Result(property = "removedByName", column = "removed_by_name"),
|
@Result(property = "removedByName", column = "removed_by_name"),
|
||||||
@Result(property = "time", column = "time"),
|
@Result(property = "time", column = "time"),
|
||||||
@@ -26,7 +28,7 @@ public interface UUIDHistoryMapper {
|
|||||||
@Result(property = "type", column = "type")
|
@Result(property = "type", column = "type")
|
||||||
})
|
})
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
SELECT id, all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
||||||
removed_by_name, time, until, removed_by_reason, type
|
removed_by_name, time, until, removed_by_reason, type
|
||||||
FROM all_punishments
|
FROM all_punishments
|
||||||
INNER JOIN user_lookup
|
INNER JOIN user_lookup
|
||||||
@@ -41,10 +43,11 @@ public interface UUIDHistoryMapper {
|
|||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
@Results({
|
@Results({
|
||||||
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
@Result(property = "id", column = "id"),
|
||||||
|
@Result(property = "uuid", column = "uuid"),
|
||||||
@Result(property = "punishedName", column = "punished_name"),
|
@Result(property = "punishedName", column = "punished_name"),
|
||||||
@Result(property = "reason", column = "reason"),
|
@Result(property = "reason", column = "reason"),
|
||||||
@Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
|
||||||
@Result(property = "bannedByName", column = "banned_by_name"),
|
@Result(property = "bannedByName", column = "banned_by_name"),
|
||||||
@Result(property = "removedByName", column = "removed_by_name"),
|
@Result(property = "removedByName", column = "removed_by_name"),
|
||||||
@Result(property = "time", column = "time"),
|
@Result(property = "time", column = "time"),
|
||||||
@@ -52,7 +55,7 @@ public interface UUIDHistoryMapper {
|
|||||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||||
})
|
})
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
SELECT punishment.id, punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
||||||
removed_by_name, time, until, removed_by_reason
|
removed_by_name, time, until, removed_by_reason
|
||||||
FROM ${tableName} AS punishment
|
FROM ${tableName} AS punishment
|
||||||
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
|
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
|
||||||
@@ -66,7 +69,8 @@ public interface UUIDHistoryMapper {
|
|||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
default List<HistoryRecord> getRecent(HistoryType historyType, UserType userType, UUID uuid, int page) {
|
default List<HistoryRecord> getRecent(@NotNull HistoryType historyType, @NotNull UserType userType,
|
||||||
|
@NotNull UUID uuid, int page) {
|
||||||
return switch (historyType) {
|
return switch (historyType) {
|
||||||
case ALL -> getRecentAll(userType, uuid, page);
|
case ALL -> getRecentAll(userType, uuid, page);
|
||||||
case BAN -> getRecentBans(userType, uuid, page);
|
case BAN -> getRecentBans(userType, uuid, page);
|
||||||
@@ -76,7 +80,12 @@ public interface UUIDHistoryMapper {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecent(String tableName, UserType userType, UUID uuid, int page) {
|
default List<HistoryRecord> getAllHistoryForUUID(@NotNull UUID uuid) {
|
||||||
|
return getRecentAllHistory(uuid.toString(), "uuid", 100, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<HistoryRecord> getRecent(@NotNull String tableName, @NotNull UserType userType,
|
||||||
|
@NotNull UUID uuid, int page) {
|
||||||
int offset = page * PAGE_SIZE;
|
int offset = page * PAGE_SIZE;
|
||||||
int limit = PAGE_SIZE;
|
int limit = PAGE_SIZE;
|
||||||
return switch (userType) {
|
return switch (userType) {
|
||||||
@@ -85,7 +94,7 @@ public interface UUIDHistoryMapper {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecentAll(UserType userType, UUID uuid, int page) {
|
private List<HistoryRecord> getRecentAll(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||||
int offset = page * PAGE_SIZE;
|
int offset = page * PAGE_SIZE;
|
||||||
int limit = PAGE_SIZE;
|
int limit = PAGE_SIZE;
|
||||||
return switch (userType) {
|
return switch (userType) {
|
||||||
@@ -96,23 +105,24 @@ public interface UUIDHistoryMapper {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecentBans(UserType userType, UUID uuid, int page) {
|
private @NotNull List<HistoryRecord> getRecentBans(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||||
return addType(getRecent("litebans_bans", userType, uuid, page), "ban");
|
return addType(getRecent("litebans_bans", userType, uuid, page), "ban");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecentKicks(UserType userType, UUID uuid, int page) {
|
private @NotNull List<HistoryRecord> getRecentKicks(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||||
return addType(getRecent("litebans_kicks", userType, uuid, page), "kick");
|
return addType(getRecent("litebans_kicks", userType, uuid, page), "kick");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecentMutes(UserType userType, UUID uuid, int page) {
|
private @NotNull List<HistoryRecord> getRecentMutes(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||||
return addType(getRecent("litebans_mutes", userType, uuid, page), "mute");
|
return addType(getRecent("litebans_mutes", userType, uuid, page), "mute");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecentWarns(UserType userType, UUID uuid, int page) {
|
private @NotNull List<HistoryRecord> getRecentWarns(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||||
return addType(getRecent("litebans_warnings", userType, uuid, page), "warn");
|
return addType(getRecent("litebans_warnings", userType, uuid, page), "warn");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) {
|
@Contract("_, _ -> param1")
|
||||||
|
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, @NotNull String type) {
|
||||||
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
|
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
|
||||||
return historyRecords;
|
return historyRecords;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public interface TeamMemberMapper {
|
|||||||
FROM luckperms_user_permissions AS permissions
|
FROM luckperms_user_permissions AS permissions
|
||||||
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
||||||
WHERE permission = #{groupPermission}
|
WHERE permission = #{groupPermission}
|
||||||
|
AND world = 'global'
|
||||||
""")
|
""")
|
||||||
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class KeyPairEntity {
|
||||||
|
private int id;
|
||||||
|
private String privateKey;
|
||||||
|
private String publicKey;
|
||||||
|
private Instant createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.*;
|
||||||
|
|
||||||
|
public interface KeyPairMapper {
|
||||||
|
|
||||||
|
@Select("SELECT * FROM key_pair ORDER BY id DESC LIMIT 1")
|
||||||
|
KeyPairEntity getKeyPair();
|
||||||
|
|
||||||
|
@Insert("""
|
||||||
|
INSERT INTO key_pair (id, private_key, public_key, created_at)
|
||||||
|
VALUES (#{id}, #{privateKey}, #{publicKey}, #{createdAt})
|
||||||
|
""")
|
||||||
|
void save(KeyPairEntity keyPair);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PrivilegedUser {
|
||||||
|
private int id;
|
||||||
|
private String uuid;
|
||||||
|
private List<String> permissions;
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PrivilegedUserMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a user by their UUID along with their permissions
|
||||||
|
* @param uuid The UUID of the user to retrieve
|
||||||
|
* @return The PrivilegedUser with their permissions, or null if not found
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
SELECT privileged_users.id, privileged_users.uuid, privileges.privileges as permission
|
||||||
|
FROM privileged_users
|
||||||
|
LEFT JOIN privileges ON privileged_users.id = privileges.user_id
|
||||||
|
WHERE privileged_users.uuid = #{uuid}
|
||||||
|
""")
|
||||||
|
@Results({
|
||||||
|
@Result(property = "id", column = "id"),
|
||||||
|
@Result(property = "uuid", column = "uuid"),
|
||||||
|
@Result(property = "permissions", column = "id", javaType = List.class,
|
||||||
|
many = @Many(select = "getPermissionsForUser"))
|
||||||
|
})
|
||||||
|
PrivilegedUser getUserByUuid(@Param("uuid") String uuid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all privileged users with their permissions
|
||||||
|
* @return List of all privileged users with their permissions
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
SELECT id, uuid
|
||||||
|
FROM privileged_users
|
||||||
|
""")
|
||||||
|
@Results({
|
||||||
|
@Result(property = "id", column = "id"),
|
||||||
|
@Result(property = "uuid", column = "uuid"),
|
||||||
|
@Result(property = "permissions", column = "id", javaType = List.class,
|
||||||
|
many = @Many(select = "getPermissionsForUser"))
|
||||||
|
})
|
||||||
|
List<PrivilegedUser> getAllUsers();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all permissions for a specific user
|
||||||
|
* @param userId The ID of the user
|
||||||
|
* @return List of permission strings
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
SELECT privileges
|
||||||
|
FROM privileges
|
||||||
|
WHERE user_id = #{userId}
|
||||||
|
""")
|
||||||
|
List<String> getPermissionsForUser(@Param("userId") int userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new privileged user
|
||||||
|
* @param user The PrivilegedUser object to add
|
||||||
|
* @return The number of rows affected
|
||||||
|
*/
|
||||||
|
@Insert("""
|
||||||
|
INSERT INTO privileged_users (uuid)
|
||||||
|
VALUES (#{user.uuid})
|
||||||
|
""")
|
||||||
|
@Options(useGeneratedKeys = true, keyProperty = "user.id", keyColumn = "id")
|
||||||
|
int addUser(@Param("user") PrivilegedUser user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a privileged user by their UUID
|
||||||
|
* @param uuid The UUID of the user to delete
|
||||||
|
* @return The number of rows affected
|
||||||
|
*/
|
||||||
|
@Delete("""
|
||||||
|
DELETE FROM privileged_users
|
||||||
|
WHERE uuid = #{uuid}
|
||||||
|
""")
|
||||||
|
int deleteUserByUuid(@Param("uuid") String uuid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a permission to a user
|
||||||
|
* @param userId The ID of the user
|
||||||
|
* @param permission The permission to add
|
||||||
|
* @return The number of rows affected
|
||||||
|
*/
|
||||||
|
@Insert("""
|
||||||
|
INSERT INTO privileges (user_id, privileges)
|
||||||
|
VALUES (#{userId}, #{permission})
|
||||||
|
""")
|
||||||
|
int addPermissionToUser(@Param("userId") int userId, @Param("permission") String permission);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a permission from a user
|
||||||
|
* @param userId The ID of the user
|
||||||
|
* @param permission The permission to remove
|
||||||
|
* @return The number of rows affected
|
||||||
|
*/
|
||||||
|
@Delete("""
|
||||||
|
DELETE FROM privileges
|
||||||
|
WHERE user_id = #{userId} AND privileges = #{permission}
|
||||||
|
""")
|
||||||
|
int removePermissionFromUser(@Param("userId") int userId, @Param("permission") String permission);
|
||||||
|
}
|
||||||
@@ -87,6 +87,7 @@ public class Connection {
|
|||||||
log.debug("Loaded default database settings {}", databaseSettings);
|
log.debug("Loaded default database settings {}", databaseSettings);
|
||||||
Connection connection = new Connection(databaseSettings, addMappers);
|
Connection connection = new Connection(databaseSettings, addMappers);
|
||||||
log.debug("Created default database connection {}", connection);
|
log.debug("Created default database connection {}", connection);
|
||||||
|
connections.put(Databases.DEFAULT, connection);
|
||||||
return CompletableFuture.completedFuture(connection);
|
return CompletableFuture.completedFuture(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class InitializeLiteBans {
|
|||||||
configuration.addMapper(NameHistoryMapper.class);
|
configuration.addMapper(NameHistoryMapper.class);
|
||||||
configuration.addMapper(UUIDHistoryMapper.class);
|
configuration.addMapper(UUIDHistoryMapper.class);
|
||||||
configuration.addMapper(HistoryCountMapper.class);
|
configuration.addMapper(HistoryCountMapper.class);
|
||||||
|
configuration.addMapper(IdHistoryMapper.class);
|
||||||
}).join()
|
}).join()
|
||||||
.runQuery(sqlSession -> {
|
.runQuery(sqlSession -> {
|
||||||
createAllPunishmentsView(sqlSession);
|
createAllPunishmentsView(sqlSession);
|
||||||
@@ -29,15 +30,15 @@ public class InitializeLiteBans {
|
|||||||
private static void createAllPunishmentsView(SqlSession sqlSession) {
|
private static void createAllPunishmentsView(SqlSession sqlSession) {
|
||||||
String query = """
|
String query = """
|
||||||
CREATE VIEW IF NOT EXISTS all_punishments AS
|
CREATE VIEW IF NOT EXISTS all_punishments AS
|
||||||
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||||
'ban' as type
|
'ban' as type
|
||||||
FROM litebans_bans
|
FROM litebans_bans
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||||
'mute' as type
|
'mute' as type
|
||||||
FROM litebans_mutes
|
FROM litebans_mutes
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||||
'warn' as type
|
'warn' as type
|
||||||
FROM litebans_warnings
|
FROM litebans_warnings
|
||||||
ORDER BY time DESC;
|
ORDER BY time DESC;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.alttd.altitudeweb.setup;
|
package com.alttd.altitudeweb.setup;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.ibatis.session.SqlSession;
|
import org.apache.ibatis.session.SqlSession;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Statement;
|
import java.sql.Statement;
|
||||||
@@ -12,15 +14,21 @@ import java.sql.Statement;
|
|||||||
public class InitializeWebDb {
|
public class InitializeWebDb {
|
||||||
|
|
||||||
protected static void init() {
|
protected static void init() {
|
||||||
log.info("Initializing LiteBans");
|
log.info("Initializing WebDb");
|
||||||
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
||||||
configuration.addMapper(SettingsMapper.class);
|
configuration.addMapper(SettingsMapper.class);
|
||||||
|
configuration.addMapper(KeyPairMapper.class);
|
||||||
}).join()
|
}).join()
|
||||||
.runQuery(InitializeWebDb::createSettingsTable);
|
.runQuery(SqlSession -> {
|
||||||
log.debug("Initialized LuckPerms");
|
createSettingsTable(SqlSession);
|
||||||
|
createKeyTable(SqlSession);
|
||||||
|
createPrivilegedUsersTable(SqlSession);
|
||||||
|
createPrivilegesTable(SqlSession);
|
||||||
|
});
|
||||||
|
log.debug("Initialized WebDb");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createSettingsTable(SqlSession sqlSession) {
|
private static void createSettingsTable(@NotNull SqlSession sqlSession) {
|
||||||
String query = """
|
String query = """
|
||||||
CREATE TABLE IF NOT EXISTS db_connection_settings
|
CREATE TABLE IF NOT EXISTS db_connection_settings
|
||||||
(
|
(
|
||||||
@@ -40,4 +48,53 @@ public class InitializeWebDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void createKeyTable(@NotNull SqlSession sqlSession) {
|
||||||
|
String query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS key_pair (
|
||||||
|
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
private_key TEXT NOT NULL,
|
||||||
|
public_key TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||||
|
statement.execute(query);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createPrivilegedUsersTable(@NotNull SqlSession sqlSession) {
|
||||||
|
String query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS privileged_users (
|
||||||
|
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
uuid VARCHAR(36) NOT NULL
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||||
|
statement.execute(query);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createPrivilegesTable(@NotNull SqlSession sqlSession) {
|
||||||
|
String query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS privileges (
|
||||||
|
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id int NOT NULL,
|
||||||
|
privileges VARCHAR(36) NOT NULL,
|
||||||
|
CONSTRAINT fk_privileges_user FOREIGN KEY (user_id)
|
||||||
|
REFERENCES privileged_users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||||
|
statement.execute(query);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-4
@@ -41,21 +41,39 @@
|
|||||||
"fileReplacements": [
|
"fileReplacements": [
|
||||||
{
|
{
|
||||||
"replace": "src/environments/environment.ts",
|
"replace": "src/environments/environment.ts",
|
||||||
"with": "src/environments/environment.prod.ts"
|
"with": "src/environments/environment.production.ts"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"extractCss": true,
|
|
||||||
"namedChunks": false,
|
"namedChunks": false,
|
||||||
"logging": false,
|
|
||||||
"vendorChunk": false,
|
"vendorChunk": false,
|
||||||
"buildOptimizer": true
|
"buildOptimizer": true
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"optimization": false
|
"optimization": false,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.development.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"beta": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.beta.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"namedChunks": false,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+54
-10
@@ -1,14 +1,58 @@
|
|||||||
tasks.register<Exec>("npmInstall") {
|
import com.github.gradle.node.NodeExtension
|
||||||
commandLine("npm.cmd", "install")
|
import com.github.gradle.node.npm.task.NpmTask
|
||||||
// commandLine("npm", "install")
|
|
||||||
|
plugins {
|
||||||
|
id("com.github.node-gradle.node") version "7.1.0"
|
||||||
|
id("base") // This adds the standard Gradle lifecycle tasks like "build"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Exec>("ngBuild") {
|
node {
|
||||||
dependsOn("npmInstall", "generateFrontendApi")
|
download.set(true)
|
||||||
// commandLine("npm", "run", "build")
|
version.set("22.14.0")
|
||||||
commandLine("npm.cmd", "run", "build")
|
npmVersion.set("10.9.2")
|
||||||
|
workDir.set(file("${project.projectDir}/node"))
|
||||||
|
npmWorkDir.set(file("${project.projectDir}/node"))
|
||||||
}
|
}
|
||||||
|
|
||||||
//dependencies {
|
// Clean the distribution directory
|
||||||
// implementation(project(":backend"))
|
tasks.register<Delete>("cleanDist") {
|
||||||
//}
|
description = "Clean the distribution directory"
|
||||||
|
delete("dist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a task that will run npm build
|
||||||
|
tasks.register("npmBuild") {
|
||||||
|
description = "Run 'npm run build'"
|
||||||
|
group = "build"
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
// Use nodeCommand directly from the plugin
|
||||||
|
project.exec {
|
||||||
|
workingDir(project.projectDir)
|
||||||
|
|
||||||
|
// Use node's npm to ensure it works on all environments
|
||||||
|
val nodeDir = "${project.projectDir}/node"
|
||||||
|
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
val npmCmd = file(nodeDir).listFiles()?.find { it.name.startsWith("npm") && it.isDirectory }?.let {
|
||||||
|
"${it.absolutePath}/npm.cmd"
|
||||||
|
} ?: "$nodeDir/node_modules/npm/bin/npm.cmd"
|
||||||
|
|
||||||
|
commandLine(npmCmd, "run", "build:dev")
|
||||||
|
} else {
|
||||||
|
val npmExecutable = file(nodeDir).listFiles()?.find { it.name.startsWith("npm") && it.isDirectory }?.let {
|
||||||
|
"${it.absolutePath}/bin/npm"
|
||||||
|
} ?: "$nodeDir/node_modules/npm/bin/npm"
|
||||||
|
|
||||||
|
commandLine(npmExecutable, "run", "build:beta")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependsOn("npmInstall")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("assemble") {
|
||||||
|
dependsOn("npmBuild")
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,19 +6,26 @@
|
|||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test"
|
"test": "ng test",
|
||||||
|
"build:beta": "ng build --configuration=beta",
|
||||||
|
"build:prod": "ng build --configuration=production",
|
||||||
|
"build:dev": "ng build --configuration=development"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@angular/cdk": "^19.2.18",
|
||||||
"@angular/common": "^19.2.0",
|
"@angular/common": "^19.2.0",
|
||||||
"@angular/compiler": "^19.2.0",
|
"@angular/compiler": "^19.2.0",
|
||||||
"@angular/core": "^19.2.0",
|
"@angular/core": "^19.2.0",
|
||||||
"@angular/forms": "^19.2.0",
|
"@angular/forms": "^19.2.0",
|
||||||
|
"@angular/material": "^19.2.18",
|
||||||
"@angular/platform-browser": "^19.2.0",
|
"@angular/platform-browser": "^19.2.0",
|
||||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||||
"@angular/router": "^19.2.0",
|
"@angular/router": "^19.2.0",
|
||||||
|
"@types/three": "^0.177.0",
|
||||||
"ngx-cookie-service": "^19.1.2",
|
"ngx-cookie-service": "^19.1.2",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
|
"three": "^0.177.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15.7955 15.8111L21 21M18 10.5C18 14.6421 14.6421 18 10.5 18C6.35786 18 3 14.6421 3 10.5C3 6.35786 6.35786 3 10.5 3C14.6421 3 18 6.35786 18 10.5Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
+1
-5
@@ -4,8 +4,4 @@
|
|||||||
# New page
|
# New page
|
||||||
|
|
||||||
- ng generate component <name>
|
- ng generate component <name>
|
||||||
- Go to the component (<name>.component.ts)
|
- Go to app.routes.ts and give it a path
|
||||||
- add standalone: false to the @Component
|
|
||||||
- remove imports: [] from that same block
|
|
||||||
- Go to app.module.ts and add the new page in declaration
|
|
||||||
- Add the new file to routes and give it a path (like 'new_page')
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ export const routes: Routes = [
|
|||||||
path: '',
|
path: '',
|
||||||
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
|
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'particles',
|
||||||
|
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'map',
|
path: 'map',
|
||||||
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
|
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
|
||||||
@@ -44,7 +48,69 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'bans',
|
path: 'bans',
|
||||||
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
|
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: 'bans/:type/:id',
|
||||||
|
loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'economy',
|
||||||
|
loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'claiming',
|
||||||
|
loadComponent: () => import('./claiming/claiming.component').then(m => m.ClaimingComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'mypet',
|
||||||
|
loadComponent: () => import('./mypet/mypet.component').then(m => m.MypetComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'warps',
|
||||||
|
loadComponent: () => import('./warps/warps.component').then(m => m.WarpsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'skyblock',
|
||||||
|
loadComponent: () => import('./skyblock/skyblock.component').then(m => m.SkyblockComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'customfeatures',
|
||||||
|
loadComponent: () => import('./customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'guide',
|
||||||
|
loadComponent: () => import('./guide/guide.component').then(m => m.GuideComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'ranks',
|
||||||
|
loadComponent: () => import('./ranks/ranks.component').then(m => m.RanksComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'commandlist',
|
||||||
|
loadComponent: () => import('./commandlist/commandlist.component').then(m => m.CommandlistComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'mapart',
|
||||||
|
loadComponent: () => import('./mapart/mapart.component').then(m => m.MapartComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'lag',
|
||||||
|
loadComponent: () => import('./lag/lag.component').then(m => m.LagComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'staffpowers',
|
||||||
|
loadComponent: () => import('./staffpowers/staffpowers.component').then(m => m.StaffpowersComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'forms/:form',
|
||||||
|
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'forms',
|
||||||
|
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'particles',
|
||||||
|
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,15 @@
|
|||||||
<span class="button-inner"
|
<span class="button-inner"
|
||||||
[ngClass]="active">Warnings</span>
|
[ngClass]="active">Warnings</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div [id]="getCurrentUserTypeButtonId('player')" class="button-outer" (click)="changeUserType('player')"
|
||||||
|
style="margin-left: 120px;">
|
||||||
|
<span class="button-inner"
|
||||||
|
[ngClass]="active">Player</span>
|
||||||
|
</div>
|
||||||
|
<div [id]="getCurrentUserTypeButtonId('staff')" class="button-outer" (click)="changeUserType('staff')">
|
||||||
|
<span class="button-inner"
|
||||||
|
[ngClass]="active">Staff</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="historySearchContainer">
|
<div class="historySearchContainer">
|
||||||
<input class="historySearch"
|
<input class="historySearch"
|
||||||
@@ -34,9 +43,9 @@
|
|||||||
placeholder="Search.."
|
placeholder="Search.."
|
||||||
[(ngModel)]="searchTerm"
|
[(ngModel)]="searchTerm"
|
||||||
(input)="filterNames()"
|
(input)="filterNames()"
|
||||||
|
(keyup.enter)="search()"
|
||||||
>
|
>
|
||||||
<button type="submit" class="searchButton" (click)="search()">Search</button>
|
<div class="dropdown-results" *ngIf="filteredNames.length > 0 && searchTerm">
|
||||||
<div class="search-dropdown" *ngIf="filteredNames.length > 0 && searchTerm">
|
|
||||||
<div
|
<div
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
*ngFor="let name of filteredNames"
|
*ngFor="let name of filteredNames"
|
||||||
@@ -48,7 +57,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="historyTable">
|
<div class="historyTable">
|
||||||
<app-history [userType]="userType" [punishmentType]="punishmentType"
|
<app-history [userType]="userType" [punishmentType]="punishmentType"
|
||||||
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)">
|
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)"
|
||||||
|
(selectItem)="setSearch($event)">
|
||||||
</app-history>
|
</app-history>
|
||||||
</div>
|
</div>
|
||||||
<div class="changePageButtons">
|
<div class="changePageButtons">
|
||||||
|
|||||||
@@ -12,14 +12,19 @@ main .container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.historyButtonContainer {
|
.historyButtonContainer {
|
||||||
width: 70%;
|
width: 80%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.historySearchContainer {
|
.historySearchContainer {
|
||||||
width: 30%;
|
width: 20%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-outer {
|
.button-outer {
|
||||||
@@ -38,8 +43,14 @@ main .container {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.historySearch {
|
input.historySearch {
|
||||||
vertical-align: -15px;
|
font-family: 'open-sans', sans-serif;
|
||||||
|
width: 260px;
|
||||||
|
border: 1px solid #DEDEDE;
|
||||||
|
padding: 9px 4px 9px 40px;
|
||||||
|
background: #DEDEDE url(/public/img/random/search.svg) no-repeat 10px;
|
||||||
|
background-size: 18px 18px;
|
||||||
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchButton {
|
.searchButton {
|
||||||
@@ -53,20 +64,24 @@ main .container {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-results {
|
.dropdown-results {
|
||||||
|
font-family: 'open-sans', sans-serif;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 35px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: white;
|
background-color: #DEDEDE;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
z-index: 1000;
|
z-index: 1050;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
@@ -150,5 +165,7 @@ main .container {
|
|||||||
.pageNumber {
|
.pageNumber {
|
||||||
font-family: 'open-sans', sans-serif;
|
font-family: 'open-sans', sans-serif;
|
||||||
margin: 0 25px 0 25px;
|
margin: 0 25px 0 25px;
|
||||||
|
color: var(--font-color);
|
||||||
|
transition: 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {HistoryCount, HistoryService} from '../../api';
|
|||||||
import {NgClass, NgForOf, NgIf} from '@angular/common';
|
import {NgClass, NgForOf, NgIf} from '@angular/common';
|
||||||
import {FormsModule} from '@angular/forms';
|
import {FormsModule} from '@angular/forms';
|
||||||
import {catchError, map, Observable} from 'rxjs';
|
import {catchError, map, Observable} from 'rxjs';
|
||||||
|
import {SearchParams} from './search-terms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-bans',
|
selector: 'app-bans',
|
||||||
@@ -74,6 +75,13 @@ export class BansComponent implements OnInit {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCurrentUserTypeButtonId(options: 'player' | 'staff') {
|
||||||
|
if (options == this.userType) {
|
||||||
|
return 'currentButton'
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public filterNames() {
|
public filterNames() {
|
||||||
if (!this.searchTerm) {
|
if (!this.searchTerm) {
|
||||||
this.filteredNames = [];
|
this.filteredNames = [];
|
||||||
@@ -88,6 +96,7 @@ export class BansComponent implements OnInit {
|
|||||||
public selectName(name: string) {
|
public selectName(name: string) {
|
||||||
this.searchTerm = name;
|
this.searchTerm = name;
|
||||||
this.filteredNames = [];
|
this.filteredNames = [];
|
||||||
|
this.search();
|
||||||
}
|
}
|
||||||
|
|
||||||
public search() {
|
public search() {
|
||||||
@@ -116,10 +125,13 @@ export class BansComponent implements OnInit {
|
|||||||
).subscribe();
|
).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
public changeHistoryType(type: 'player' | 'staff') {
|
public changeUserType(type: 'player' | 'staff') {
|
||||||
this.pushState();
|
this.pushState();
|
||||||
this.userType = type;
|
this.userType = type;
|
||||||
this.page = 0;
|
this.page = 0;
|
||||||
|
this.finalSearchTerm = '';
|
||||||
|
this.searchTerm = '';
|
||||||
|
this.filteredNames = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public changeHistoryPunishment(type: 'all' | 'ban' | 'mute' | 'kick' | 'warn') {
|
public changeHistoryPunishment(type: 'all' | 'ban' | 'mute' | 'kick' | 'warn') {
|
||||||
@@ -199,4 +211,11 @@ export class BansComponent implements OnInit {
|
|||||||
this.actualPage = this.page;
|
this.actualPage = this.page;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setSearch(searchParams: SearchParams) {
|
||||||
|
this.userType = searchParams.userType
|
||||||
|
this.searchTerm = searchParams.searchTerm
|
||||||
|
this.punishmentType = searchParams.punishmentType
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||||
|
[overlay_gradient]="0.5">>
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Minecraft Punishments</h1>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="detailsBackButton">
|
||||||
|
<ng-container *ngIf="punishment === undefined">
|
||||||
|
<p>Loading...</p>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<a [routerLink]="['/bans']">< Back</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="columnSection center">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<div>
|
||||||
|
<span class="tag tagInfo"
|
||||||
|
[ngClass]="{
|
||||||
|
'tagPermanent': this.historyFormat.isPermanent(punishment),
|
||||||
|
'tagExpired': !this.historyFormat.isPermanent(punishment)
|
||||||
|
}">
|
||||||
|
{{ this.historyFormat.getType(punishment) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="tag tagInfo"
|
||||||
|
[ngClass]="{
|
||||||
|
'tagActive': this.historyFormat.isActive(punishment),
|
||||||
|
'tagInactive': !this.historyFormat.isActive(punishment)
|
||||||
|
}">
|
||||||
|
{{ this.historyFormat.isActive(punishment) ? 'Active' : 'Inactive' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</section>
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<div class="playerContainer">
|
||||||
|
<h2>Player</h2>
|
||||||
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid, '150')"
|
||||||
|
width="150"
|
||||||
|
height="150"
|
||||||
|
alt="{{punishment.username}}'s Minecraft skin"
|
||||||
|
>
|
||||||
|
<h3 class="detailsUsername">{{ punishment.username }}</h3>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<div class="playerContainer">
|
||||||
|
<h2>Moderator</h2>
|
||||||
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid, '150')"
|
||||||
|
width="150"
|
||||||
|
height="150"
|
||||||
|
alt="{{punishment.punishedBy}}'s Minecraft skin"
|
||||||
|
>
|
||||||
|
<h3 class="detailsUsername">{{ punishment.punishedBy }}</h3>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<div class="detailsInfo">
|
||||||
|
<h2>Reason</h2>
|
||||||
|
<p>{{ punishment.reason | removeTrailingPeriod }}</p>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<div class="detailsInfo">
|
||||||
|
<h2>Date</h2>
|
||||||
|
<p>{{ this.historyFormat.getPunishmentTime(punishment) }}</p>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="columnSection">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<span>Expires</span>
|
||||||
|
<span>{{ this.historyFormat.getExpiredTime(punishment) }}</span>
|
||||||
|
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
|
||||||
|
<span>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</span>
|
||||||
|
<span>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</span>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
.detailsBackButton {
|
||||||
|
font-family: open-sans, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnSection {
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsUsername {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-family: 'opensans-bold', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsInfo {
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsInfo p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 10px;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: baseline;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: 'opensans-bold', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagInfo {
|
||||||
|
margin: 0 20px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagActive {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #EE5555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagInactive {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #F79720;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagPermanent {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #EE5555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagExpired {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #777777
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DetailsComponent } from './details.component';
|
||||||
|
|
||||||
|
describe('DetailsComponent', () => {
|
||||||
|
let component: DetailsComponent;
|
||||||
|
let fixture: ComponentFixture<DetailsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [DetailsComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DetailsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {HistoryService, PunishmentHistory} from '../../../api';
|
||||||
|
import {NgClass, NgIf, NgOptimizedImage} from '@angular/common';
|
||||||
|
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
|
||||||
|
import {HistoryFormatService} from '../history-format.service';
|
||||||
|
import {ActivatedRoute, RouterLink} from '@angular/router';
|
||||||
|
import {catchError, map} from 'rxjs';
|
||||||
|
import {HeaderComponent} from '../../header/header.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-details',
|
||||||
|
imports: [
|
||||||
|
NgIf,
|
||||||
|
NgOptimizedImage,
|
||||||
|
RemoveTrailingPeriodPipe,
|
||||||
|
HeaderComponent,
|
||||||
|
RouterLink,
|
||||||
|
NgClass
|
||||||
|
],
|
||||||
|
templateUrl: './details.component.html',
|
||||||
|
styleUrl: './details.component.scss'
|
||||||
|
})
|
||||||
|
export class DetailsComponent implements OnInit {
|
||||||
|
type: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
|
||||||
|
id: number = -1;
|
||||||
|
punishment: PunishmentHistory | undefined;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute,
|
||||||
|
private historyApi: HistoryService,
|
||||||
|
public historyFormat: HistoryFormatService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.route.paramMap.subscribe(params => {
|
||||||
|
switch (params.get('type')) {
|
||||||
|
case 'ban':
|
||||||
|
this.type = 'ban';
|
||||||
|
break;
|
||||||
|
case 'mute':
|
||||||
|
this.type = 'mute';
|
||||||
|
break;
|
||||||
|
case 'kick':
|
||||||
|
this.type = 'kick';
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
this.type = 'warn';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
|
}
|
||||||
|
this.id = Number(params.get('id') || '-1');
|
||||||
|
this.loadPunishmentData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadPunishmentData() {
|
||||||
|
if (!this.type || this.type === 'all' || this.id < 0 || isNaN(this.id)) {
|
||||||
|
console.error("Invalid type or id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.historyApi.getHistoryById(this.type, this.id).pipe(
|
||||||
|
map(punishment => {
|
||||||
|
this.punishment = punishment;
|
||||||
|
}),
|
||||||
|
catchError(err => {
|
||||||
|
console.error(err);
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HistoryFormatService } from './history-format.service';
|
||||||
|
|
||||||
|
describe('HistoryFormatService', () => {
|
||||||
|
let service: HistoryFormatService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(HistoryFormatService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {PunishmentHistory} from '../../api';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class HistoryFormatService {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPunishmentTime(entry: PunishmentHistory) {
|
||||||
|
const date = new Date(entry.punishmentTime);
|
||||||
|
return date.toLocaleString(navigator.language, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public isActive(entry: PunishmentHistory): boolean {
|
||||||
|
if (entry.removedBy !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (entry.expiryTime <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return entry.expiryTime > Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isPermanent(entry: PunishmentHistory): boolean {
|
||||||
|
return entry.expiryTime <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getType(entry: PunishmentHistory): string {
|
||||||
|
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getExpiredTime(entry: PunishmentHistory): string {
|
||||||
|
let suffix: string = '';
|
||||||
|
if (entry.removedBy !== null && entry.removedBy !== undefined) {
|
||||||
|
suffix = '(Unbanned by ' + entry.removedBy + ')';
|
||||||
|
}
|
||||||
|
if (entry.expiryTime <= 0) {
|
||||||
|
return "Permanent " + this.getType(entry) + " " + suffix;
|
||||||
|
}
|
||||||
|
const date = new Date(entry.expiryTime);
|
||||||
|
return date.toLocaleString(navigator.language, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
}) + " " + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAvatarUrl(entry: string, size: string = '25'): string {
|
||||||
|
let uuid = entry.replace('-', '');
|
||||||
|
if (uuid === 'C') {
|
||||||
|
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
|
||||||
|
}
|
||||||
|
return `https://crafatar.com/avatars/${uuid}?size=${size}&overlay`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,25 +18,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let entry of history">
|
<tr class="historyPlayerRow" *ngFor="let entry of history">
|
||||||
<td class="historyType">{{ getType(entry) }}</td>
|
<td class="historyType" (click)="showDetailedPunishment(entry)">
|
||||||
<td class="historyPlayer">
|
{{ this.historyFormat.getType(entry) }}
|
||||||
|
</td>
|
||||||
|
<td class="historyPlayer" (click)="setSearch(entry.username, 'player')">
|
||||||
<div class="playerContainer">
|
<div class="playerContainer">
|
||||||
<img class="avatar" [ngSrc]="getAvatarUrl(entry.uuid)" width="25" height="25"
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
|
||||||
alt="{{entry.username}}'s Minecraft skin">
|
alt="{{entry.username}}'s Minecraft skin">
|
||||||
<span class="username">{{ entry.username }}</span>
|
<span class="username">{{ entry.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="historyPlayer">
|
<td class="historyPlayer" (click)="setSearch(entry.punishedBy, 'staff')">
|
||||||
<div class="playerContainer">
|
<div class="playerContainer">
|
||||||
<img class="avatar" [ngSrc]="getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
|
||||||
alt="{{entry.punishedBy}}'s Minecraft skin">
|
alt="{{entry.punishedBy}}'s Minecraft skin">
|
||||||
<span>{{ entry.punishedBy }}</span>
|
<span>{{ entry.punishedBy }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="historyReason">{{ entry.reason | removeTrailingPeriod }}</td>
|
<td class="historyReason" (click)="showDetailedPunishment(entry)">
|
||||||
<td class="historyDate">{{ getPunishmentTime(entry) }}</td>
|
{{ entry.reason | removeTrailingPeriod }}
|
||||||
<td class="historyDate">{{ getExpiredTime(entry) }}</td>
|
</td>
|
||||||
|
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||||
|
{{ this.historyFormat.getPunishmentTime(entry) }}
|
||||||
|
</td>
|
||||||
|
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||||
|
{{ this.historyFormat.getExpiredTime(entry) }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ table tr td {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.historyPlayerRow:hover {
|
||||||
|
background-color: var(--history-table-row-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.historyTableHead {
|
.historyTableHead {
|
||||||
width: 1300px;
|
width: 1300px;
|
||||||
background-color: var(--history-table-head-color);
|
background-color: var(--history-table-head-color);
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
|
import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
|
||||||
import {BASE_PATH, HistoryService, PunishmentHistoryInner} from '../../../api';
|
import {BASE_PATH, HistoryService, PunishmentHistory} from '../../../api';
|
||||||
import {catchError, map, Observable, shareReplay} from 'rxjs';
|
import {catchError, map, Observable, shareReplay} from 'rxjs';
|
||||||
import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common';
|
import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common';
|
||||||
import {CookieService} from 'ngx-cookie-service';
|
import {CookieService} from 'ngx-cookie-service';
|
||||||
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
|
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
|
||||||
import {HttpErrorResponse} from '@angular/common/http';
|
import {HttpErrorResponse} from '@angular/common/http';
|
||||||
|
import {environment} from '../../../environments/environment';
|
||||||
|
import {HistoryFormatService} from '../history-format.service';
|
||||||
|
import {SearchParams} from '../search-terms';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-history',
|
selector: 'app-history',
|
||||||
@@ -18,7 +22,7 @@ import {HttpErrorResponse} from '@angular/common/http';
|
|||||||
styleUrl: './history.component.scss',
|
styleUrl: './history.component.scss',
|
||||||
providers: [
|
providers: [
|
||||||
CookieService,
|
CookieService,
|
||||||
{provide: BASE_PATH, useValue: 'http://localhost:8080/'}
|
{provide: BASE_PATH, useValue: environment.apiUrl}
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class HistoryComponent implements OnInit, OnChanges {
|
export class HistoryComponent implements OnInit, OnChanges {
|
||||||
@@ -29,12 +33,13 @@ export class HistoryComponent implements OnInit, OnChanges {
|
|||||||
@Input() searchTerm: string = '';
|
@Input() searchTerm: string = '';
|
||||||
|
|
||||||
@Output() pageChange = new EventEmitter<number>();
|
@Output() pageChange = new EventEmitter<number>();
|
||||||
|
@Output() selectItem = new EventEmitter<SearchParams>();
|
||||||
|
|
||||||
private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||||
|
|
||||||
public history: PunishmentHistoryInner[] = []
|
public history: PunishmentHistory[] = []
|
||||||
|
|
||||||
constructor(private historyApi: HistoryService) {
|
constructor(private historyApi: HistoryService, public historyFormat: HistoryFormatService, private router: Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
@@ -46,7 +51,7 @@ export class HistoryComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private reloadHistory(): void {
|
private reloadHistory(): void {
|
||||||
let historyObservable: Observable<PunishmentHistoryInner[]>;
|
let historyObservable: Observable<PunishmentHistory[]>;
|
||||||
if (this.searchTerm.length === 0) {
|
if (this.searchTerm.length === 0) {
|
||||||
historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page);
|
historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page);
|
||||||
} else {
|
} else {
|
||||||
@@ -66,6 +71,9 @@ export class HistoryComponent implements OnInit, OnChanges {
|
|||||||
let retrySeconds = 5;
|
let retrySeconds = 5;
|
||||||
|
|
||||||
if (err instanceof HttpErrorResponse) {
|
if (err instanceof HttpErrorResponse) {
|
||||||
|
if (err.status !== 429) {
|
||||||
|
return this.history;
|
||||||
|
}
|
||||||
const headers = err.headers;
|
const headers = err.headers;
|
||||||
const retryAfterHeader = headers.get('Retry-After');
|
const retryAfterHeader = headers.get('Retry-After');
|
||||||
console.warn(err.error);
|
console.warn(err.error);
|
||||||
@@ -85,42 +93,16 @@ export class HistoryComponent implements OnInit, OnChanges {
|
|||||||
).subscribe();
|
).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPunishmentTime(entry: PunishmentHistoryInner) {
|
setSearch(name: string, userType: 'player' | 'staff') {
|
||||||
const date = new Date(entry.punishmentTime);
|
let searchParams: SearchParams = {
|
||||||
return date.toLocaleString(navigator.language, {
|
userType: userType,
|
||||||
year: 'numeric',
|
punishmentType: 'all',
|
||||||
month: 'short',
|
searchTerm: name
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public getType(entry: PunishmentHistoryInner) {
|
|
||||||
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getExpiredTime(entry: PunishmentHistoryInner) {
|
|
||||||
if (entry.expiryTime <= 0) {
|
|
||||||
return "Permanent " + this.getType(entry);
|
|
||||||
}
|
}
|
||||||
const date = new Date(entry.expiryTime);
|
this.selectItem.emit(searchParams);
|
||||||
return date.toLocaleString(navigator.language, {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAvatarUrl(entry: string): string {
|
public showDetailedPunishment(entry: PunishmentHistory) {
|
||||||
let uuid = entry.replace('-', '');
|
this.router.navigate([`bans/${entry.type}/${entry.id}`]).then();
|
||||||
if (uuid === 'C') {
|
|
||||||
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
|
|
||||||
}
|
|
||||||
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface SearchParams {
|
||||||
|
userType: 'player' | 'staff';
|
||||||
|
searchTerm: string;
|
||||||
|
punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn';
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'claiming'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Claiming</h1>
|
||||||
|
<h2>Claiming allows you to protect your items and builds from other players.</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Getting Started</h2>
|
||||||
|
<p>You can “claim” land on Altitude to protect your builds. When you claim a section of land, other players
|
||||||
|
will be unable to place or break blocks, kill animals, take crops, or open chests. Everything will be
|
||||||
|
completely protected!</p>
|
||||||
|
<p>To claim land, you will need some basic tools: a golden shovel, and a stick. You can craft these items
|
||||||
|
yourself or do <b>/claim</b> to receive them for free. The shovel is used for modifying claims and the
|
||||||
|
stick is used for viewing claim information.</p>
|
||||||
|
<img ngSrc="/public/img/items/shovel.png" alt="Minecraft golden shovel" style="width: 25%;" height="114"
|
||||||
|
width="114">
|
||||||
|
<p>Players start with an allowance of 500 “claim blocks” and can purchase additional “claim blocks” with
|
||||||
|
in-game currency using <span style="font-family: 'opensans-bold', sans-serif;">/buyclaimblocks</span>. How
|
||||||
|
many claims you can create depends on the rank you have - the amount can be found on the <a
|
||||||
|
[routerLink]="['/ranks']">ranks</a> page. Additionally, there is also a limit on how many blocks in
|
||||||
|
total you can
|
||||||
|
claim.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Creating a Claim</h2>
|
||||||
|
<p>Before creating any claim, we should check if there are other claims by other players nearby. Claims by
|
||||||
|
different people should be at least 100 blocks apart unless both parties discuss otherwise. To do this,
|
||||||
|
hold a stick in your hand and <b>shift + right click</b> to visualize all nearby claims. Claims are
|
||||||
|
visualized using dotted lines of gold blocks, so look around for any dotted lines on the ground around
|
||||||
|
you.</p>
|
||||||
|
<p>Once you've confirmed that there are no claims too close, we can begin claiming! A claim doesn't go down
|
||||||
|
all the way. The lowest point of the claim starts at 5 blocks below the lower corner you made. It'll keep
|
||||||
|
expanding down as you place blocks lower and lower in the claim, but it may reset when you change the
|
||||||
|
claim size, trust, subclaims etc. If you want to make sure your claim stays extended all the way down,
|
||||||
|
make one of the initial corners at bedrock level. Remember to not claim further down than you need to.</p>
|
||||||
|
<p>If a claim is within 100 blocks you will be unable to create your claim. If the original claim owner is
|
||||||
|
fine with you having a claim closer than the 100 blocks, then they can trust you using <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/claimnearbytrust</span>. You will need to be trusted
|
||||||
|
in order to claim and resize. The trust expires on every server reboot, this is to remind you that there
|
||||||
|
are nearby claims.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Automatic Claim Deletion</h2>
|
||||||
|
<p>When a player has been inactive for 60 consecutive days, on a server (not cross-server), their claim(s)
|
||||||
|
on that server will be transferred into an admin claim(s). The claim will be in that state for 24 hours
|
||||||
|
after which is will be fully unclaimed. During the time the claim is an admin claim everyone will be
|
||||||
|
trusted. This means that you will be able to raid the claim, but not claim it. If you wish to claim the
|
||||||
|
build you will have to return 24 hours later.</p>
|
||||||
|
<p>If you are planning on being gone this long, all you need to do is join for a couple of minutes within
|
||||||
|
those 60 days to reset the timer. Unclaimed land is fair game to loot, so make sure your claims are active
|
||||||
|
and properly sized!</p>
|
||||||
|
<p>Any containers (i.e shulker, chest) that are placed within an active raid-claim are fair game - this also
|
||||||
|
includes containers that raiders place. This means that items a raider puts into their personal shulker
|
||||||
|
aren't safe while placed down or when on the ground as an item after being broken, as anyone that comes by
|
||||||
|
can take it.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Useful Commands</h2>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/buyclaimblocks -</span> Buy more claim
|
||||||
|
blocks
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sellclaimblocks -</span> Sell claim blocks
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/unclaim -</span> Remove a protection</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/claimslist -</span> List all of your
|
||||||
|
protections
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/expandclaim <number> -</span> Expand
|
||||||
|
the claim the direction you're facing (can also be used with negative numbers to downsize the claim)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/containertrust <player> -</span> Give
|
||||||
|
someone access to chests in your claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/permissiontrust <player> -</span> Give
|
||||||
|
someone the ability to trust and untrust others in your claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/accesstrust <player> -</span> Give
|
||||||
|
someone access to doors and trapdoors in your claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/subdivide -</span> Create claims within your
|
||||||
|
main claim that you can trust people to
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/restrictsubclaim -</span> Sets subclaim NOT
|
||||||
|
to inherit perms from the parent claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/trust <player> -</span> Allow someone
|
||||||
|
to build and use chests within your claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/trustlist -</span> See who is trusted in your
|
||||||
|
claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/untrust <player> -</span> Remove a
|
||||||
|
player from your protection
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/kickfromclaim <player> -</span> Kick a
|
||||||
|
player outside of your claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petkick -</span> Enter pet kicking mode
|
||||||
|
(right click pets you don't want in your claim)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Resizing Your Claim</h2>
|
||||||
|
<p>You can make your claim bigger or smaller any time by using a golden shovel. The process is similar to
|
||||||
|
creating the claim. To make things easier, start by right clicking within the claim with a stick. This
|
||||||
|
will show the border of the claim so you can see where the corners are.</p>
|
||||||
|
<p>To resize a claim, right click a corner you would like to move. Then, right click again wherever you
|
||||||
|
would like to move that corner to. To make things easier, you don't need to right click the actual block,
|
||||||
|
you can just point your crosshair at the block and right click from a distance.</p>
|
||||||
|
<p>If you don't have the needed amount of claim blocks to expand (or create) your claim, you will need to
|
||||||
|
purchase additional claim blocks. The price for purchasing claim blocks is different depending on how many
|
||||||
|
you have:</p>
|
||||||
|
<ul>
|
||||||
|
<li>0-10,000 blocks = $0.50/block</li>
|
||||||
|
<li>10,000-50,000 blocks = $0.75/block</li>
|
||||||
|
<li>50,000-300,000 blocks = $1.00/block</li>
|
||||||
|
<li>300,000-1,000,000 blocks = $2.00/block</li>
|
||||||
|
<li>1,000,000+ blocks = $5.00/block</li>
|
||||||
|
</ul>
|
||||||
|
<p>If you have left over claim blocks you can sell them using <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/sellclaimblocks</span>. The return will always be
|
||||||
|
$0.50/block. This means that you need to be mindful of purchasing large amounts of claim blocks; don't
|
||||||
|
purchase them unless you actually need them.</p>
|
||||||
|
<img ngSrc="/public/img/backgrounds/claim.png" alt="Screenshot showing claim border in-game"
|
||||||
|
style="width: 100%;"
|
||||||
|
width="480" height="250">
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Subdividing Claims</h2>
|
||||||
|
<p>Subdividing claims allows you to give players access to specific parts of your claim. This is the best
|
||||||
|
way to manage a town or shop!</p>
|
||||||
|
<p>The idea behind subdivisions is easy. You are creating mini-claims within your main claim so that you can
|
||||||
|
trust people to certain areas, but not the entire claim. You are still in control of the border of these
|
||||||
|
mini-claims (size and location). Once a subdivision is created you can trust people the same way as
|
||||||
|
before. Just stand in the mini-claim and use the trust commands. It will only trust them to the
|
||||||
|
mini-claim. (To trust someone to your main claim you will need to stand outside of any mini-claims you've
|
||||||
|
created.)</p>
|
||||||
|
<p>To create a subdivision you need to go into "subdivision mode". You can access this mode with <b>/subdivideclaims</b>.
|
||||||
|
Once you are in this mode you can create the subdivisions the same way you would make a regular claim,
|
||||||
|
just right click two corners. Subdivisions will be outlined with a white dotted line. To leave subdivision
|
||||||
|
mode use <b>/basicclaims</b>.</p>
|
||||||
|
<img ngSrc="/public/img/backgrounds/townsubdivisions.png" alt="Screenshot of town subdivided"
|
||||||
|
style="width: 100%;"
|
||||||
|
width="480" height="250">
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Trusting Others</h2>
|
||||||
|
<p>You can add other players to your protected area with various levels of access. To trust everyone on the
|
||||||
|
server replace <player> with "all". This works with all of the commands below.</p>
|
||||||
|
<ul>
|
||||||
|
<li>To give someone full access to your claim (break, build, access chests, etc) do: <b>/trust <player></b>
|
||||||
|
</li>
|
||||||
|
<li>To give someone just the ability to access storage items, but NOT build, do: <b>/containertrust <player></b>
|
||||||
|
</li>
|
||||||
|
<li>To give someone visitor access (opening the doors, but not take anything or build anything) do: <b>/accesstrust
|
||||||
|
<player></b></li>
|
||||||
|
<li>To give someone permission to manage your claim (trusting other players) do: <b>/permissiontrust <player></b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
main ul {
|
||||||
|
font-family: opensans, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main li {
|
||||||
|
margin-left: 30px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ClaimingComponent } from './claiming.component';
|
||||||
|
|
||||||
|
describe('ClaimingComponent', () => {
|
||||||
|
let component: ClaimingComponent;
|
||||||
|
let fixture: ComponentFixture<ClaimingComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ClaimingComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ClaimingComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {HeaderComponent} from '../header/header.component';
|
||||||
|
import {NgOptimizedImage} from '@angular/common';
|
||||||
|
import {RouterLink} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-claiming',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
NgOptimizedImage,
|
||||||
|
RouterLink
|
||||||
|
],
|
||||||
|
templateUrl: './claiming.component.html',
|
||||||
|
styleUrl: './claiming.component.scss'
|
||||||
|
})
|
||||||
|
export class ClaimingComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,496 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'commandlist'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Command list</h1>
|
||||||
|
<h2>Looking for a specific command, or the description of one? This is where you find it.</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding-bottom: 5px !important;">Claiming</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/claim -</span> Gives you the claim kit</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/buyclaimblocks <amount> -</span> Allows
|
||||||
|
you to buy claimblocks (0.5 in game per block)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sellclaimblocks <amount> -</span>
|
||||||
|
Allows you to sell claimblocks (0.5 in game per block)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/abandonallclaims -</span> Deletes all your
|
||||||
|
claims
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/abandonclaim -</span> Deletes the
|
||||||
|
claim/subdivision you're currently in
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/delclaim -</span> Deletes the
|
||||||
|
claim/subdivision you're currently in
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/unclaim -</span> Deletes the
|
||||||
|
claim/subdivision you're currently in
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/claimlist -</span> Shows you all your claims
|
||||||
|
and claimblocks
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/accesstrust <username/public> -</span>
|
||||||
|
Gives accesstrust to a user/everyone
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/containertrust <username/public> -</span> Gives
|
||||||
|
containertrust to a user/everyone
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/expandclaim <amount> -</span> Expands
|
||||||
|
claim by the specified amount in the direction you're facing
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/restrictsubclaim -</span> Seperates
|
||||||
|
permissions for the subclaim you are standing in from the main claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/subdivide -</span> Turns on subdivide mode
|
||||||
|
(hold golden shovel) to allow you to create subdivisions
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/trustlist -</span> Shows users trusted in the
|
||||||
|
claim/subclaim you're standing it
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/claimnearbytrust -</span> Allows other
|
||||||
|
players to claim within 100 blocks to your claim
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Creative</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot help -</span> Display all creative
|
||||||
|
commands
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot info -</span> Display plot info</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot confirm -</span> Confirm an action</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot claim -</span> Claim the current plot
|
||||||
|
you're standing on
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot auto -</span> Claim a random available
|
||||||
|
plot nearby
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot delete -</span> Delete your plot</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot trust -</span> Allow a player to build
|
||||||
|
in your plot
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot add -</span> Allow a user to build while
|
||||||
|
you are online
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot setowner -</span> Set the plot owner
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot setbiome -</span> Set the plot biome
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot visit -</span> Visit someone (or your
|
||||||
|
own) plot
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot kick -</span> Kick a player from your
|
||||||
|
plot
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot middle -</span> Teleports you to the
|
||||||
|
center of the current plot
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot untrust -</span> Remove a player from
|
||||||
|
your plot
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot merge [auto] -</span> Merge a plot (or
|
||||||
|
multiple)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot unmerge -</span> Unmerge plots</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot sethome -</span> Set the plot home</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot clear -</span> Clear a plot</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/nightvision -</span> Enables nightvision</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Dynamic Map</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dynmap -</span> Sends you a link to the
|
||||||
|
Altitude map page
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dynmap hide -</span> Hides you on the map
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dynmap show -</span> Shows you on the map
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">McMMO</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mctop <ability> -</span> Shows the top
|
||||||
|
10 players for Power Level or the specified ability
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mcrank -</span> Shows where you are on the
|
||||||
|
mctop per skill
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mcstats -</span> Shows your mcmmo levels in a
|
||||||
|
scoreboard
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mcability -</span> Toggles ability use on or
|
||||||
|
off
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/acrobatics [?] [page] -</span> Shows your
|
||||||
|
stats for Acrobatics. If you add ? it will show the guide for Acrobatics
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/archery [?] [page] -</span> Shows your stats
|
||||||
|
for Archery. If you add ? it will show the guide for Archery
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/axes [?] [page] -</span> Shows your stats for
|
||||||
|
Axes. If you add ? it will show the guide for Axes
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/excavation [?] [page] -</span> Shows your
|
||||||
|
stats for Excavation. If you add ? it will show the guide for Excavation
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/fishing [?] [page] -</span> Shows your stats
|
||||||
|
for Fishing. If you add ? it will show the guide for Fishing
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/herbalism [?] [page] -</span> Shows your
|
||||||
|
stats for Herbalism. If you add ? it will show the guide for Herbalism
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mining [?] [page] -</span> Shows your stats
|
||||||
|
for Mining. If you add ? it will show the guide for Mining
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/repair [?] [page] -</span> Shows your stats
|
||||||
|
for Repair. If you add ? it will show the guide for Repair
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/swords [?] [page] -</span> Shows your stats
|
||||||
|
for Swords. If you add ? it will show the guide for Swords
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/taming [?] [page] -</span> Shows your stats
|
||||||
|
for Taming. If you add ? it will show the guide for Taming
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/unarmed [?] [page] -</span> Shows your stats
|
||||||
|
for Unarmed. If you add ? it will show the guide for Unarmed
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/woodcutting [?] [page] -</span> Shows your
|
||||||
|
stats for Woodcutting. If you add ? it will show the guide for Woodcutting
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/inspect <username> -</span> Shows mcmmo
|
||||||
|
levels for specified user in a scoreboard if they are near you
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">MyPet</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinfo -</span> Shows information about your
|
||||||
|
MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petname <name> -</span> Gives your
|
||||||
|
current MyPet the specified name
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petrelease <name> -</span> Release the
|
||||||
|
specified MyPet (this will despawn it)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petcall (/petc) -</span> Teleports your
|
||||||
|
current MyPet to you
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/psa -</span> Sends your MyPet away, it can
|
||||||
|
still be called using /petc (does not store your pet)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petswitch (/psw) -</span> Switches between
|
||||||
|
current and stored MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petstore (/pst) -</span> Stores your MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pettrade <username> [price] -</span>
|
||||||
|
Trades current MyPet with a player
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petshop -</span> Opens the MyPet shop</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petskill -</span> Shows information about the
|
||||||
|
skills of your MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petstop -</span> Stops your MyPet from
|
||||||
|
attacking a target if it's not in farm or aggressive behavior modes
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petchooseskilltree (/pcst) -</span> Allows
|
||||||
|
you to select a skilltree for your MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petcapturehelper (/pch) -</span>
|
||||||
|
Enables/disables the CaptureHelper for MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petoptions healthbar -</span> Toggles
|
||||||
|
actionbar healthbar on/off
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petoptions idle-volume <%> -</span>
|
||||||
|
Sets volume percentage for the MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinventory (/peti) -</span> Opens the
|
||||||
|
inventory for your current MyPet if it has one
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petpickup (/pp) -</span> Toggles pet pickup
|
||||||
|
on/off if your pet has an inventory
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbehavior [mode] (/pb) -</span> Switches
|
||||||
|
between pet behaviors or to the specified one
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbeacon -</span> Opens the beacon editor
|
||||||
|
for your MyPet if it has one
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding-bottom: 5px !important;">Economy</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/bal -</span> Shows you your balance</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/cheque <amount> -</span> Creates a
|
||||||
|
cheque with the specified amount of money if you're holding a piece of paper
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pay <username> <amount> [-confirmed] -</span>
|
||||||
|
Pays the specified amount to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/shop -</span> Opens the shop GUI</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/points -</span> Displays your current amount
|
||||||
|
of sell/buy points
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sell <item> -</span> Tells you if the
|
||||||
|
entered item is sellable at spawn, and at what price
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/buy <item> -</span> Tells you if the
|
||||||
|
entered item is buyable at spawn, and at what price
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/playershop checkstock <radius> [max. stock] -</span>
|
||||||
|
highlights all of your shops which do not meet the minimum amount specified
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Party Chat</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party help -</span> Display the help menu
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party create <partyname> <password> -</span>
|
||||||
|
Creates a party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party info -</span> Displays information
|
||||||
|
about your current party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party invite <username> -</span> Invite
|
||||||
|
a user to your party
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/party join <partyname> [password] -</span> Join
|
||||||
|
a users party (password is required only if the party has one)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/p <message> -</span> Changes chat to
|
||||||
|
party chat. If a message is specified it is sent to party chat
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party leave -</span> Leave a party</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party name <newname> -</span> Leave a
|
||||||
|
party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party owner <username> -</span> Makes
|
||||||
|
the specified user owner of the party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party password <password> -</span> Sets
|
||||||
|
or changes a password for the party
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/party remove (/party kick) <username> -</span>
|
||||||
|
Kicks the specified user from the party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party disband -</span> Disband your party
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Voting</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/vote -</span> Sends a link to our vote page
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votebest -</span> Shows highest total votes
|
||||||
|
you reached per day/week/month
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votelast -</span> Shows when you last voted
|
||||||
|
on each of the websites
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votenext -</span> Shows when you can vote
|
||||||
|
again per website
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votestreak -</span> Shows your current and
|
||||||
|
best vote streak
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votetop [page] -</span> Shows the vote top
|
||||||
|
page
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votetotal -</span> Shows you your total votes
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Warps</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/warps [warp] -</span> Opens the warp GUI or
|
||||||
|
teleports you to the specified warp
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/warp apply -</span> Starts application
|
||||||
|
process for a new warp (costs 25k)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Altitude</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/queue -</span> Shows what server you are
|
||||||
|
queued for
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/find <username> -</span> Shows if and
|
||||||
|
where this user is online
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/back -</span> Teleports you back to where you
|
||||||
|
last teleported from
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dback -</span> Brings you back to the last
|
||||||
|
location you died at
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/colors -</span> Shows all color and format
|
||||||
|
codes
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/home [name] -</span> Shows your homes (click
|
||||||
|
to teleport), if a home is specified it will teleport you there
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/list -</span> Lists all online players on the
|
||||||
|
server
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/stafflist -</span> Lists all online staff
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/seen <username> -</span> Shows when a
|
||||||
|
player is/was online
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/playtime -</span> Shows your playtime</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sit -</span> Allows you to sit down</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/cooldown -</span> Shows the cooldown for the
|
||||||
|
rtp portal
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/hat -</span> Puts the current item on your
|
||||||
|
head
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/discord -</span> Sends you a link to the
|
||||||
|
Altitude Discord server
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/ranks -</span> Shows the ranks on the
|
||||||
|
Altitude server
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/contact -</span> Sends a link to our Contact
|
||||||
|
page
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/report -</span> Sends a link to the report
|
||||||
|
form
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/rules -</span> Sends a link to the rules page
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/store -</span> Sends a link to the Altitude
|
||||||
|
store
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/guide -</span> Sends a link to the guide page
|
||||||
|
for getting started
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/info -</span> Sends some links to our website
|
||||||
|
and Discord
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/se <signline> <text> -</span>
|
||||||
|
While looking at a sign changes text on specified line to specified text.
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/editsign -</span> Toggles shift click sign
|
||||||
|
editing
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/kickfromclaim -</span> Kicks specified user
|
||||||
|
from claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/kickpet -</span> Toggles kickpet mode, kicks
|
||||||
|
vanilla pets from claims after right clicking it
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/warnings -</span> Shows all active warnings
|
||||||
|
for yourself
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/realname <username/nickname> -</span>
|
||||||
|
Shows the specified users username and nickname
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart remove -</span> Deletes map from the
|
||||||
|
database if you own it and are holding it
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart save -</span> Saves map on the
|
||||||
|
database if you're holding it
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sneakclickmending -</span> Allows you to mend
|
||||||
|
your items by right clicking while sneaking
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pu rotateblock -</span> Rotate already
|
||||||
|
placed, direction-specific, blocks
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pu xpcalc <current xp> <goal xp> -</span>
|
||||||
|
Calculates the amount of XP needed to get from level X to level Y
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pu xpcheque <xp> -</span> Creates a
|
||||||
|
'cheque' in the form of an XP bottle with the specified amount of XP
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pt top [daily/weekly/monthly/total]-</span>
|
||||||
|
Displays the players with the highest amount of playtime for the specified time
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/finditem <item> -</span> Enables you to
|
||||||
|
search your chest monster for a specific item
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/local -</span> Toggles local chat, where
|
||||||
|
messages will only be sent to players within a 200 block distance
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/blockitempickup -</span> Prevents picking up
|
||||||
|
items
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Miscellaneous</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/apart -</span> Opens particles GUI</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/cmi armorstand [last/near] -</span> Opens
|
||||||
|
Armorstand editor
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/ping -</span> Shows your ping</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/time -</span> Shows in-game time</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/mail send <username> <message> -</span>
|
||||||
|
Sends a mail to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/ignore <username> -</span> Ignores a
|
||||||
|
users messages to you and hides their public chat messages from you
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/r <message> -</span> Replies to the
|
||||||
|
last user to send you a message
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/c <message> -</span> Replies to the
|
||||||
|
last user you sent a message to
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/msg <username> <message> -</span>
|
||||||
|
Sends a message to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pvp [on/off] -</span> Toggles your pvp on/off
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sethome <homename> -</span> Sets a home
|
||||||
|
with the specified name
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/delhome <homename> -</span> Shows your
|
||||||
|
homes (click to remove), or removes the specified home
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/spawn -</span> Teleports you to spawn</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpa <username> -</span> Sends a
|
||||||
|
teleport request to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpaaccept [username] -</span> Accepts last
|
||||||
|
teleport request, or the teleport request of the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpahere <username> -</span> Sends a
|
||||||
|
teleport here request to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpbypass -</span> Used to teleport to a
|
||||||
|
location the plugin marks as unsafe (such as void or lava)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpdeny [username] -</span> Denies last
|
||||||
|
teleport request, or the teleport request of the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/givepet <username> -</span> Gives
|
||||||
|
vanilla pet to specified user after you right click it
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/trapped -</span> Teleports you out of the
|
||||||
|
claim you are in if you do not have build permission there
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/colorsextra -</span> Displays useable RGB
|
||||||
|
colors (shift+lmb to paste in chat)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dispose (/trash) -</span> Opens a GUI where
|
||||||
|
you can dispose of items
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
main ul {
|
||||||
|
font-family: 'opensans', sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main li {
|
||||||
|
margin-left: 30px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
color: var(--font-color);
|
||||||
|
transition: 0.5s ease;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CommandlistComponent } from './commandlist.component';
|
||||||
|
|
||||||
|
describe('CommandlistComponent', () => {
|
||||||
|
let component: CommandlistComponent;
|
||||||
|
let fixture: ComponentFixture<CommandlistComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CommandlistComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CommandlistComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {HeaderComponent} from "../header/header.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-commandlist',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent
|
||||||
|
],
|
||||||
|
templateUrl: './commandlist.component.html',
|
||||||
|
styleUrl: './commandlist.component.scss'
|
||||||
|
})
|
||||||
|
export class CommandlistComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'skyblock'" height="460px" background_image="/public/img/backgrounds/path.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Custom Features</h1>
|
||||||
|
<h2>A list of custom features created specifically to enhance the survival experience on Altitude.</h2>
|
||||||
|
<h3
|
||||||
|
style="font-family: 'minecraft-text', sans-serif; font-size: 0.8rem; margin-top: 10px; color: #FFFFFF !important;">
|
||||||
|
Commands for the features are listed <a [routerLink]="['/commandlist']">here</a></h3>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Blocks</h2>
|
||||||
|
<p style="padding-bottom: 0 !important;">Beacons can be activated by redstone</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>Powering the block above a beacon disables the beam</li>
|
||||||
|
</ul>
|
||||||
|
<p>Glow lichen can be silk touched</p>
|
||||||
|
<p>Vines can be silk touched and bonemealed</p>
|
||||||
|
<p>Coral and coral fans can be bonemealed to grow a new coral tree</p>
|
||||||
|
<p>Mushroom blocks can be stripped with an axe</p>
|
||||||
|
<p>Candles can be placed without a block beneath them</p>
|
||||||
|
<p>Crying obsidian can be used to create nether portals</p>
|
||||||
|
<p style="padding-bottom: 0 !important;">Gravel and sand are craftable in stonecutters</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>Cobblestone -> Gravel</li>
|
||||||
|
<li>Gravel -> Sand</li>
|
||||||
|
</ul>
|
||||||
|
<p>Sponges can remove lava - wet sponges remove more (Note: the sponge will be consumed)</p>
|
||||||
|
<p>Item frames can be made invisible using a diamond</p>
|
||||||
|
<p>Rotation of item frames can be locked using a slime ball</p>
|
||||||
|
<p>Copper can be oxidized using a cauldron of water</p>
|
||||||
|
<p>Pink petals can be placed on any surface</p>
|
||||||
|
<p>Wither roses can be bonemealed</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Containers</h2>
|
||||||
|
<p>Dispensers can interact with campfires</p>
|
||||||
|
<p>Dispensers can interact with cauldrons</p>
|
||||||
|
<p>Dispensers can place pumpkins</p>
|
||||||
|
<p>Dispensers can carve pumpkins</p>
|
||||||
|
<p>Barrels have 54 inventory slots</p>
|
||||||
|
<p>Furnaces have a built in item filter, this moves any item they can't process to the output</p>
|
||||||
|
<p>Naming a container or villager "public" makes them publically available</p>
|
||||||
|
<p>Natual loot chests (in dungeons, etc.) are not protected by claims (but can only be broken by the claim
|
||||||
|
owner)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Compostable items</h2>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>leather: 0.10</li>
|
||||||
|
<li>wooden_shovel: 0.10</li>
|
||||||
|
<li>rotten_flesh: 0.15</li>
|
||||||
|
<li>salmon: 0.15</li>
|
||||||
|
<li>cod: 0.15</li>
|
||||||
|
<li>porkchop: 0.15</li>
|
||||||
|
<li>beef: 0.15</li>
|
||||||
|
<li>chicken: 0.15</li>
|
||||||
|
<li>rabbit: 0.15</li>
|
||||||
|
<li>mutton: 0.15</li>
|
||||||
|
<li>bamboo: 0.15</li>
|
||||||
|
<li>wooden_sword: 0.20</li>
|
||||||
|
<li>wooden_hoe: 0.20</li>
|
||||||
|
<li>wooden_pickaxe: 0.30</li>
|
||||||
|
<li>wooden_axe: 0.30</li>
|
||||||
|
<li>poisonous_potato: 0.35</li>
|
||||||
|
<li>leather_boots: 0.40</li>
|
||||||
|
<li>flowering_azalea_leaves: 0.50</li>
|
||||||
|
<li>leather_helmet: 0.50</li>
|
||||||
|
<li>leather_leggings: 0.70</li>
|
||||||
|
<li>leather_chestplate: 0.80</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Ender Dragon</h2>
|
||||||
|
<p>First kill of the month awards a dragon egg</p>
|
||||||
|
<p style="padding-bottom: 0 !important;">Can drop items when killed</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>elytra: 30%</li>
|
||||||
|
<li>dragon_head: 30%</li>
|
||||||
|
<li>dragon_breath: 40%</li>
|
||||||
|
<li>and seperately from the rest, a 0.1% chance for a dragon_egg</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Mobs</h2>
|
||||||
|
<p>Bats can spawn above sea level (bat spawning only enabled during halloween)</p>
|
||||||
|
<p style="padding-bottom: 0 !important;">Mobs can respawn in structures</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>Shulker -> End City</li>
|
||||||
|
<li>Piglin Brute, Piglin -> Bastion</li>
|
||||||
|
<li>Evoker, Vindicator, Allay -> Woodland Mansion</li>
|
||||||
|
<li>Allay -> Pillager Outpost</li>
|
||||||
|
<li>Camel -> Desert Village</li>
|
||||||
|
</ul>
|
||||||
|
<p>Naming a mob "muted", "silenced", "silent" or "silence me" silences a mob</p>
|
||||||
|
<p>Naming a mob "muted", "silenced", "silent", "silence me", "protected" or "protect me" prevents it from
|
||||||
|
being damaged by non-trusted players in a claim</p>
|
||||||
|
<p>Naming a mob "baby" prevents it from growing up</p>
|
||||||
|
<p>Wither health bar is removed when silenced</p>
|
||||||
|
<p>Turtles can hatch during daytime</p>
|
||||||
|
<p>Unclaim vanilla player pets (dogs, cats, parrots, horses) if the owner has been offline for 60+ days by
|
||||||
|
right clicking with a lead</p>
|
||||||
|
<p>Fisherman villagers can sell Bucket of Salmon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Restrictions & disabled vanilla features</h2>
|
||||||
|
<p>Restricted iron golem spawning (10 golems spawning in a radius of 112 blocks in a time span of 1
|
||||||
|
minute)</p>
|
||||||
|
<p>Disabled stacking raids (having multiple raids going on at once)</p>
|
||||||
|
<p>Disabled TNT duping</p>
|
||||||
|
<p>Disabled bat spawning</p>
|
||||||
|
<p>Disabled patrol spawning</p>
|
||||||
|
<p>Zombies don't attack villagers</p>
|
||||||
|
<p>Disabled processing of most things around AFK players</p>
|
||||||
|
<p>Disabled AI for villagers in a 1x1 area</p>
|
||||||
|
<p>Shulkers shooting each other does not spawn a new shulker since they respawn naturally in End Cities</p>
|
||||||
|
<p>Pigmen cannot spawn from portals</p>
|
||||||
|
<p>Allay duplication is disabled since they respawn natually in Mansions and Outposts</p>
|
||||||
|
<p>Players cannot claim land within 100 blocks of another claim without the owner's permission</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Miscellaneous</h2>
|
||||||
|
<p>Grindstones can strip shulker box & beehive NBT data</p>
|
||||||
|
<p>Sneak click mending</p>
|
||||||
|
<p>/iwanttobreakthisblock, required to break some natural generated blocks</p>
|
||||||
|
<p>/sneakclickmending, allows you to mend your items by right clicking while sneaking</p>
|
||||||
|
<p>Swift Sneak enchantment can be found in villager trades</p>
|
||||||
|
<p>Nether portals can be as small as 1x2 (1x2 portal, 3x4 base)</p>
|
||||||
|
<p>Lootchests refill once for every player</p>
|
||||||
|
<p>Empty maps can be duplicated using paper in a cartography table</p>
|
||||||
|
<p>Eating glowberries gives you a glowing effect</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
main ul {
|
||||||
|
font-family: opensans, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main li {
|
||||||
|
margin-left: 30px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CustomfeaturesComponent } from './customfeatures.component';
|
||||||
|
|
||||||
|
describe('CustomfeaturesComponent', () => {
|
||||||
|
let component: CustomfeaturesComponent;
|
||||||
|
let fixture: ComponentFixture<CustomfeaturesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CustomfeaturesComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CustomfeaturesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {HeaderComponent} from "../header/header.component";
|
||||||
|
import {RouterLink} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-customfeatures',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
RouterLink
|
||||||
|
],
|
||||||
|
templateUrl: './customfeatures.component.html',
|
||||||
|
styleUrl: './customfeatures.component.scss'
|
||||||
|
})
|
||||||
|
export class CustomfeaturesComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'economy'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Economy</h1>
|
||||||
|
<h2>Altitude features a dynamic in-game economy that uses virtual currency to allow for easier trading between
|
||||||
|
players.</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>What is the Economy?</h2>
|
||||||
|
<p>Altitude uses a combination of simple player shops and dynamic NPC merchants to create a truly
|
||||||
|
one-of-a-kind experience for our players. Shops and Merchants allow you to exchange items for virtual
|
||||||
|
currency, and vice-versa. The spawn of each server has some items that can be bought or sold by talking to
|
||||||
|
the various merchants, but player shops are the backbone of the system. These shops are created and run by
|
||||||
|
our players, and they allow those players to determine and set their own prices for their goods.</p>
|
||||||
|
<p>The virtual currency on Altitude has no value outside of the server, nor any item in-game representing
|
||||||
|
it. Each player has a number representing how much in-game currency they have. You can check this any time
|
||||||
|
with <span style="font-family: 'opensans-bold', sans-serif;">/bal</span>.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Player Shops</h2>
|
||||||
|
<p>Altitude allows players to create chest shops, which other players can either buy or sell items to. It’s
|
||||||
|
a really simple task to create your shops. There’s no complex commands or multi-line sign setups -
|
||||||
|
everything is done using a GUI.</p>
|
||||||
|
<p>To get started, grab a sign and place it on a container (does not work on double chests). Write [shop] on
|
||||||
|
the first line and you’re ready to go. Exit the sign interface and the GUI will open. With these simple
|
||||||
|
steps you will have successfully created your first shop!
|
||||||
|
Right click the sign to manage the shop. From there you can control the shop balance, item, item amount
|
||||||
|
and price.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Shop Balance</h2>
|
||||||
|
<p>Every shop will have its own balance. The balance is used to for making changes in the shop management
|
||||||
|
GUI, paying players if they sell items to the shop and receiving money when players buy items from the
|
||||||
|
shop.</p>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="columnparagraph">
|
||||||
|
<h2>Shop Upkeep</h2>
|
||||||
|
<p>Once a week shop upkeep need to be paid. The amount depends on the amount of shops you have; the higher the amount of shops, the higher the fee.
|
||||||
|
The fee will be taken from the shop’s balance. Should there not be enough money in the balance to pay the upkeep fee the shop will be deactivated. Once it had been deactivated other players will no longer be able to interact with it until the shop owner has reactivated it.</p>
|
||||||
|
<p>A shop that hasn’t been interacted with (players using the shop) for three weeks, even if it has the balance to pay for upkeep, will be deactivated and will also need to be reactivated. This is done to prevent dormant and forgotten shops.</p>
|
||||||
|
</div> -->
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Economist's Tips</h2>
|
||||||
|
<p>To get the most of the in-game economy, keep these tips in mind!</p>
|
||||||
|
<ul>
|
||||||
|
<li>Use the spawn merchants to buy and sell as a last resort! These merchants have intentionally worse
|
||||||
|
prices to allow for player markets to emerge for those same items.
|
||||||
|
</li>
|
||||||
|
<li>Build multiple, smaller farms for different types of items to sell rather than one massive farm that
|
||||||
|
only generates one type of item. You’ll earn more money because of the dynamic prices!
|
||||||
|
</li>
|
||||||
|
<li>Keep track of the points you have with each merchant! Knowing how many points you have will allow you
|
||||||
|
to time your buying and selling for the biggest potential profit!
|
||||||
|
</li>
|
||||||
|
<li>Remember that player shops have flat prices, while the spawn merchants have dynamic prices.</li>
|
||||||
|
<li>Try not to buy or sell when your points are at the worst levels! You will get a huge penalty for
|
||||||
|
having such an extreme amount of points and the transaction will likely not be worth it.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Spawn Merchants</h2>
|
||||||
|
<p>Every merchant at spawn buys and sells certain types of items. You can interact and trade with them
|
||||||
|
similar to how you would trade emeralds with a regular villager. Each of these merchants are located
|
||||||
|
around each of the server spawn, which you can conveniently get to by doing <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/spawn</span>!</p>
|
||||||
|
|
||||||
|
<!-- INSERT IMAGE OF VILLAGER MERCHANT -->
|
||||||
|
|
||||||
|
<p>These merchants buy and sell directly from the server, not from other players. Prices from these
|
||||||
|
merchants are typically quite high for buying, and quite low for selling. This means that they should be
|
||||||
|
used as a last resort for some quick cash or some quick materials; player shops will generally be a much
|
||||||
|
better value, especially because the prices are static.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>The Point System</h2>
|
||||||
|
<p>The prices offered by the merchants at spawn are dynamic in nature. This means that they change based on
|
||||||
|
the amount and type of items that you've bought or sold recently. Every merchant at spawn keeps track of
|
||||||
|
how many items you’ve traded with it using ‘points’. The amount of points you currently have with each
|
||||||
|
merchant is shown in the trading screen. To understand the point system, all you need to know is:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Positive points = higher buy prices, higher sell prices</li>
|
||||||
|
<li>Negative points = lower buy prices, lower sell prices</li>
|
||||||
|
<li>Points decay naturally over time. The decay is faster if you are online</li>
|
||||||
|
</ul>
|
||||||
|
<p>For most players, this dynamic point system won’t affect you much at all. As long as you aren’t buying or
|
||||||
|
selling huge quantities of items, you won’t notice a large change in the prices as you use the spawn
|
||||||
|
merchants.</p>
|
||||||
|
<p>However, players can take advantage of the point system to make the most amount of profit possible if
|
||||||
|
they are so inclined. To do this, you need to understand the multipliers that affect prices at different
|
||||||
|
price levels. You can see your current price multipliers and points at each merchant by doing <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/villagerui points</span>.</p>
|
||||||
|
<p style="padding-bottom: 0;">There are different thresholds for each price multiplier to take effect.
|
||||||
|
Between -500 and 500 points, prices will be unaltered. Once your points go above or below that range,
|
||||||
|
prices will become more and more extreme. You can see where these thresholds are based on your current
|
||||||
|
points here:</p>
|
||||||
|
<img ngSrc="/public/img/random/pointbracket.png" alt="Visualization of the point bracket."
|
||||||
|
style="width: 100%; padding: 0 0 15px 0;"
|
||||||
|
height="100" width="800">
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Useful Commands</h2>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/bal -</span> Check your current balance</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pay <player> <amount> -</span>
|
||||||
|
Pay other players
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/shop -</span> Click on settings, from there
|
||||||
|
you can change notification settings
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/villagerui points <villagerType> -</span>
|
||||||
|
Allows you to check your current points with each spawn merchant, or any specific one. Also shows your
|
||||||
|
current buy/sell price multipliers for those merchants
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/villagerui buy/sell <item_name> -</span> Tells
|
||||||
|
you if a spawn merchant buys or sells that item, and where that merchant is located
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
main ul {
|
||||||
|
font-family: opensans, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main li {
|
||||||
|
margin-left: 30px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { EconomyComponent } from './economy.component';
|
||||||
|
|
||||||
|
describe('EconomyComponent', () => {
|
||||||
|
let component: EconomyComponent;
|
||||||
|
let fixture: ComponentFixture<EconomyComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [EconomyComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(EconomyComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {HeaderComponent} from '../header/header.component';
|
||||||
|
import {NgOptimizedImage} from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-economy',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
NgOptimizedImage
|
||||||
|
],
|
||||||
|
templateUrl: './economy.component.html',
|
||||||
|
styleUrl: './economy.component.scss'
|
||||||
|
})
|
||||||
|
export class EconomyComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<app-forms [currentPage]="'appeal'" [formTitle]="'Minecraft Appeal'">
|
||||||
|
<div form-content>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</app-forms>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AppealComponent } from './appeal.component';
|
||||||
|
|
||||||
|
describe('AppealComponent', () => {
|
||||||
|
let component: AppealComponent;
|
||||||
|
let fixture: ComponentFixture<AppealComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AppealComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AppealComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {FormsComponent} from '../forms.component';
|
||||||
|
import {FormControl, FormGroup, Validators} from '@angular/forms';
|
||||||
|
import {AppealsService, MinecraftAppeal} from '../../../api';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-appeal',
|
||||||
|
imports: [
|
||||||
|
FormsComponent
|
||||||
|
],
|
||||||
|
templateUrl: './appeal.component.html',
|
||||||
|
styleUrl: './appeal.component.scss'
|
||||||
|
})
|
||||||
|
export class AppealComponent implements OnInit {
|
||||||
|
|
||||||
|
public form: FormGroup<Appeal>;
|
||||||
|
|
||||||
|
constructor(private appealApi: AppealsService) {
|
||||||
|
this.form = new FormGroup({
|
||||||
|
username: new FormControl('', {nonNullable: true, validators: [Validators.required]}),
|
||||||
|
punishmentId: new FormControl('', {nonNullable: true, validators: [Validators.required]}),
|
||||||
|
email: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.email]}),
|
||||||
|
appeal: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.minLength(10)]})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSubmit() {
|
||||||
|
if (this.form === undefined) {
|
||||||
|
console.error('Form is undefined');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.form.valid) {
|
||||||
|
this.sendForm()
|
||||||
|
} else {
|
||||||
|
// Mark all fields as touched to trigger validation display
|
||||||
|
Object.keys(this.form.controls).forEach(field => {
|
||||||
|
const control = this.form!.get(field);
|
||||||
|
if (!(control instanceof FormGroup)) {
|
||||||
|
console.error('Control [' + control + '] is not a FormGroup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
control.markAsTouched({onlySelf: true});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendForm() {
|
||||||
|
const rawValue = this.form.getRawValue();
|
||||||
|
const appeal: MinecraftAppeal = {
|
||||||
|
appeal: rawValue.appeal,
|
||||||
|
email: rawValue.email,
|
||||||
|
punishmentId: parseInt(rawValue.punishmentId),
|
||||||
|
username: rawValue.username,
|
||||||
|
uuid: ''//TODO
|
||||||
|
}
|
||||||
|
this.appealApi.submitMinecraftAppeal(appeal).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Appeal {
|
||||||
|
username: FormControl<string>;
|
||||||
|
punishmentId: FormControl<string>;
|
||||||
|
email: FormControl<string>;
|
||||||
|
appeal: FormControl<string>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum FormType {
|
||||||
|
APPEAL = 'appeal',
|
||||||
|
STAFF_APPLICATION = 'staff_application',
|
||||||
|
CONTACT = 'contact'
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="currentPage" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>{{ formTitle }}</h1>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
<ng-container *ngIf="!type">
|
||||||
|
<ng-container *ngFor="let formType of FormType | keyvalue">
|
||||||
|
<button mat-raised-button (click)="setFormType(formType.value)">
|
||||||
|
{{ formType }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<div>
|
||||||
|
<ng-content select="[form-content]"></ng-content>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FormsComponent } from './forms.component';
|
||||||
|
|
||||||
|
describe('FormsComponent', () => {
|
||||||
|
let component: FormsComponent;
|
||||||
|
let fixture: ComponentFixture<FormsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [FormsComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(FormsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
|
import {HeaderComponent} from '../header/header.component';
|
||||||
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
|
import {ActivatedRoute} from '@angular/router';
|
||||||
|
import {LoginDialogComponent} from '../login/login.component';
|
||||||
|
import {KeyValuePipe, NgForOf, NgIf} from '@angular/common';
|
||||||
|
import {FormType} from './form_type';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
import {AuthService} from '../services/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-forms',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
NgIf,
|
||||||
|
NgForOf,
|
||||||
|
MatButton,
|
||||||
|
KeyValuePipe
|
||||||
|
],
|
||||||
|
templateUrl: './forms.component.html',
|
||||||
|
styleUrl: './forms.component.scss'
|
||||||
|
})
|
||||||
|
export class FormsComponent implements OnInit {
|
||||||
|
@Input() formTitle: string = 'Form';
|
||||||
|
@Input() currentPage: string = 'forms';
|
||||||
|
|
||||||
|
public type: FormType | undefined;
|
||||||
|
|
||||||
|
constructor(private authService: AuthService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
) {
|
||||||
|
this.route.paramMap.subscribe(async params => {
|
||||||
|
const code = params.get('code');
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
this.authService.login(code).subscribe();
|
||||||
|
} else if (!this.authService.checkAuthStatus()) {
|
||||||
|
const dialogRef = this.dialog.open(LoginDialogComponent, {
|
||||||
|
width: '400px',
|
||||||
|
disableClose: true
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.route.paramMap.subscribe(params => {
|
||||||
|
switch (params.get('form')) {
|
||||||
|
case FormType.APPEAL:
|
||||||
|
this.type = FormType.APPEAL;
|
||||||
|
this.currentPage = 'appeal';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly FormType = FormType;
|
||||||
|
protected readonly Object = Object;
|
||||||
|
|
||||||
|
public setFormType(formType: FormType) {
|
||||||
|
this.type = formType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'guide'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Guide Book</h1>
|
||||||
|
<h2>We aim to be an inclusive community server where players of all ages can find something to enjoy.</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Backstory</h2>
|
||||||
|
<p>Altitude is a community-centered survival Minecraft server for {{ ALTITUDE_VERSION }}. Altitude opened
|
||||||
|
in January of 2015 and was originally a private server for a small group of friends. We’ve since opened
|
||||||
|
our doors to welcome anyone and everyone to enjoy the custom survival experience we’ve crafted!</p>
|
||||||
|
<p>The community grew rapidly in the beginning of 1.13 and therefore grew beyond the capacity of a single
|
||||||
|
server, so we opened two survival servers that each offered the same features and plugins, but a different
|
||||||
|
seed so that there is lots of land to explore!. The two servers later became four servers.</p>
|
||||||
|
<p>The Minecraft hype slowly died down and therefore we had to downscale first to two servers for 1.18 and
|
||||||
|
then for 1.20 down to one server again.</p>
|
||||||
|
<p>If you would like to learn more about the history of Altitude, you can visit our <a
|
||||||
|
[routerLink]="['/about']">about page</a>.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Claiming Land</h2>
|
||||||
|
<p>You should protect your home by “claiming” it to prevent others stealing your items or damaging your
|
||||||
|
build. This is important because we allow players to loot unclaimed buildings, claim them as their own,
|
||||||
|
and even completely remove them because we otherwise would have a world full of abandoned buildings. This
|
||||||
|
means that if your home is damaged in any way because it wasn’t protected, staff will not be able to
|
||||||
|
restore your items!</p>
|
||||||
|
<p>Claiming land on Altitude is easy. We use the very popular plugin “GriefPrevention”, which allows players
|
||||||
|
to use a golden shovel to make their claim.To get started enter the command <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/claim</span> which will give you the necessary tools.
|
||||||
|
After that you simply click two opposite corners with the golden shovel (can be any golden shovel, not
|
||||||
|
only the one given through /claim) to claim the area between the points.</p>
|
||||||
|
<p>If you need more help with claiming, including trusting others and expanding your claim, visit our <a
|
||||||
|
[routerLink]="['/claiming']">claiming page</a>.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Economy</h2>
|
||||||
|
<p>We offer a collection of economy features to provide a player-focused and player-operated economy.
|
||||||
|
Players can create their own shops with the ChestShop plugin to buy and sell goods with others, apply for
|
||||||
|
a warp so that other players can travel directly to their shop, and pay other players directly with the
|
||||||
|
<b>/pay</b> command.</p>
|
||||||
|
<p>There are also shops in the spawn of each world that sell basic items like raw ores, food, and building
|
||||||
|
blocks. The prices in the spawn shops are designed to be much higher than you would likely find at player
|
||||||
|
shops to encourage trading between players and only using spawn as a last resort.</p>
|
||||||
|
<p>If you would like to learn more about the economy and how to create your own chest shops, see our <a
|
||||||
|
[routerLink]="['/economy']">economy page</a>.</p>
|
||||||
|
<p>If you would like to learn about applying for your own warp, including the criteria you must meet, see
|
||||||
|
our <a [routerLink]="['/warps']">warps page</a>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Survival Features</h2>
|
||||||
|
<p>On Altitude, we want to provide an authentic survival experience for every player. In a multiplayer
|
||||||
|
environment, this can be challenging! To provide the best experience we possibly can, we’ve kept some
|
||||||
|
vanilla features on that other servers disable:</p>
|
||||||
|
<ul>
|
||||||
|
<li>TNT and creepers do damage! (Claimed land is safe)</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/back</span> to return to your last position
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dback</span> to return to your death
|
||||||
|
position. (This even works if you have used <b>/back</b> earlier)
|
||||||
|
</li>
|
||||||
|
<li>Loot chests by default cannot be broken. You can enable this by doing <b>/iwanttobreakthisblock <block></b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>McMMO & MyPet</h2>
|
||||||
|
<p>Our server feature the very popular community-requested plugins: McMMO & MyPet. McMMO introduces an
|
||||||
|
RPG-like experience to the survival game with skill leveling, special abilities, rare loot, and new
|
||||||
|
mechanics. The primary feature of McMMO is the detailed leveling system that breaks down Minecraft into
|
||||||
|
fifteen skills including mining, woodcutting, and herbalism. Each skill will level up the more you do it
|
||||||
|
unlocking powerful abilities related to that skill.</p>
|
||||||
|
<p>If you’d like to learn more about McMMO, <a href="https://mcmmo.org/wiki/Main_Page">visit their wiki
|
||||||
|
here</a>!</p>
|
||||||
|
<p>MyPet allows you to claim nearly any mob in-game as your “pet”. Each pet can be assigned a skilltree that
|
||||||
|
gives it unique abilities. Pets can learn to fight alongside you against mobs, pick up items for you as
|
||||||
|
you mine, and even provide faster travel around the world. Pets will level up in their skilltree the more
|
||||||
|
you play with them to unlock more abilities.</p>
|
||||||
|
<p>If you would like to learn more about MyPet and their abiltiies, visit our <a [routerLink]="['/mypet']">MyPet
|
||||||
|
page</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Creative</h2>
|
||||||
|
<p>Occasionally, you may find yourself with a creative itch, or need a way to experiment and collaborate
|
||||||
|
with other players on the server! We offer a creative server for just that purpose. Players can use as
|
||||||
|
many plots in creative as they need (just ask a staff for more if you reach the default limit) and each
|
||||||
|
plot is 100 by 100 blocks. Players frequently use these plots for testing our survival builds before
|
||||||
|
taking the time to collect the resources in survival to build it.</p>
|
||||||
|
<p>Due to the low traffic that the creative server gets, we do not moderate this server as closely. If you
|
||||||
|
see anything wrong with the server, please inform a staff member so that we can get it resolved!</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>More Help</h2>
|
||||||
|
<p>You are now on your way to becoming a member of the Altitude community! We hope we’ve provided you with
|
||||||
|
the tools necessary to be successful on our servers. If you have any other questions, please reach out to
|
||||||
|
a staff member in-game, or on <a href="https://discordapp.com/invite/TGqpzCJ">our Discord</a>!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
main ul {
|
||||||
|
font-family: opensans, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main li {
|
||||||
|
margin-left: 30px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { GuideComponent } from './guide.component';
|
||||||
|
|
||||||
|
describe('GuideComponent', () => {
|
||||||
|
let component: GuideComponent;
|
||||||
|
let fixture: ComponentFixture<GuideComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [GuideComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(GuideComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {HeaderComponent} from "../header/header.component";
|
||||||
|
import {RouterLink} from '@angular/router';
|
||||||
|
import {ALTITUDE_VERSION} from '../constant';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-guide',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
RouterLink
|
||||||
|
],
|
||||||
|
templateUrl: './guide.component.html',
|
||||||
|
styleUrl: './guide.component.scss'
|
||||||
|
})
|
||||||
|
export class GuideComponent {
|
||||||
|
|
||||||
|
protected readonly ALTITUDE_VERSION = ALTITUDE_VERSION;
|
||||||
|
}
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="nav_li">
|
<li class="nav_li">
|
||||||
<span
|
<span
|
||||||
[id]="getCurrentPageId(['guide', 'faq', 'ranks', 'rules', 'commandlist', 'art', 'lag', 'staffpowers',
|
[id]="getCurrentPageId(['guide', 'faq', 'ranks', 'rules', 'commandlist', 'mapart', 'lag', 'staffpowers',
|
||||||
'nicknames', 'bans', 'discord-bans'])" class="nav_link fake_link" [ngClass]="active">Reference</span>
|
'nicknames', 'bans', 'discord-bans'])" class="nav_link fake_link" [ngClass]="active">Reference</span>
|
||||||
<ul class="dropdown">
|
<ul class="dropdown">
|
||||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/guide']">Guide Book</a></li>
|
<li class="nav_li"><a class="nav_link2" [routerLink]="['/guide']">Guide Book</a></li>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
</section>
|
</section>
|
||||||
-->
|
-->
|
||||||
<section class="darkmodeSection">
|
<section class="darkmodeSection">
|
||||||
<div class="container" style="padding: 30px 0">
|
<div class="container" style="padding: 10px 0 80px 0">
|
||||||
<iframe id="discord-widget" src="https://discordapp.com/widget?id=141644560005595136&theme=dark"></iframe>
|
<iframe id="discord-widget" src="https://discordapp.com/widget?id=141644560005595136&theme=dark"></iframe>
|
||||||
<div class="paragraph" id="discordp">
|
<div class="paragraph" id="discordp">
|
||||||
<h2>Meet the Community</h2>
|
<h2>Meet the Community</h2>
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'lag'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Reducing Server Lag</h1>
|
||||||
|
<h2>How can we minimize lag on the server? Together we can create a lag-free survival server, but it takes
|
||||||
|
everyone's help!</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<div class="container">
|
||||||
|
<div class="paragraph">
|
||||||
|
<h2>Introduction</h2>
|
||||||
|
<p>Lag is not caused by one person, it is caused by all of us. Some of us cause more than others, but we can
|
||||||
|
all do small things to reduce the amount of lag we cause. Remember: do not ever accuse anyone else of being
|
||||||
|
the problem. If you believe someone’s build or behavior is a large contributor to the lag, please privately
|
||||||
|
tell a staff member so we can look into it. If you have any specific questions, staff are always here to
|
||||||
|
help. This is especially true if you want to make a redstone contraption, but are unsure how to have it
|
||||||
|
automatically shut off when not needed or to make it less taxing on the server.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<div class="container">
|
||||||
|
<div class="paragraph">
|
||||||
|
<h2>Mobs</h2>
|
||||||
|
<p style="margin-bottom: 0;">Mobs are the #1 cause of lag on a survival Minecraft server. They can be
|
||||||
|
surprisingly hard on the server and are a cause of lag that everyone contributes to. Because of this, never
|
||||||
|
keep more than you need of any given mob.</p>
|
||||||
|
<p>FarmLimiter is a plugin that introduces hard limits to mobs. The plugin groups mobs by chaining mobs
|
||||||
|
together that are within a minimum radius of each other. Please refer to the example image for a better idea
|
||||||
|
of how mob chaining works. We've provided the settings of this plugin below for your reference, but please
|
||||||
|
do not try to circumvent this system - it is in place for a reason!</p>
|
||||||
|
<ul class="full-page-list" style="margin-bottom: 0;">
|
||||||
|
<li>Villagers should not be allowed to build up from a breeder, and you should cull undesirables as needed
|
||||||
|
to keep your numbers as low as possible. Also think carefully about which villagers you really need and if
|
||||||
|
you can make dual use of some. Only use “infinite” villager breeder designs if you limit their breeding by
|
||||||
|
limiting their food.
|
||||||
|
</li>
|
||||||
|
<li>Villagers used for trading should be put on emerald blocks, emerald ore or smooth stone. This will
|
||||||
|
disable their AI for anything other than trading. This helps server performance a lot and any villagers
|
||||||
|
used for trading found without their AI disabled could be removed by staff if they are causing lag.
|
||||||
|
Villagers placed on these blocks will refresh their trades faster than normal. The following example
|
||||||
|
visualizes the 20 passive mobs within 5 blocks FarmLimiter restriction. <a style="cursor: pointer;"
|
||||||
|
(click)="toggleExceptions()">Show
|
||||||
|
example...</a></li>
|
||||||
|
</ul>
|
||||||
|
<div id="exceptions" [ngClass]="{'hide': !showExceptions}"
|
||||||
|
style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||||
|
<img ngSrc="/public/img/random/farm_limiter_darker.png" alt="Visualization of the farm limiter"
|
||||||
|
style="width: 70%; padding-top: 7px;" height="375" width="715">
|
||||||
|
</div>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>Turtles only need one breeding pair. The breeding mechanic of turtles allows you to breed one pair over
|
||||||
|
and over, as they become fertile immediately upon laying their eggs. Consider keeping only two adult
|
||||||
|
turtles in your farm.
|
||||||
|
</li>
|
||||||
|
<li>Consider not raising pigs at all, as their meat is exactly the same as beef and they have no secondary
|
||||||
|
drops
|
||||||
|
</li>
|
||||||
|
<li>Do not fill your base with more vanilla pets than you need</li>
|
||||||
|
<li>Never circumvent entity cramming. It is there because a lot of entities crammed into a small space is
|
||||||
|
hard on the server.
|
||||||
|
</li>
|
||||||
|
<li>FarmLimiter is a plugin that introduces hard limits to mobs. We've provided the settings of this plugin
|
||||||
|
below for your reference, but please do not try to circumvent this system - it is in place for a reason!
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="highlightedContent">
|
||||||
|
<h3>Farm Limiter Plugin</h3>
|
||||||
|
<p>No more than 25 mobs (of all types) within a radius of 3 blocks (this gets the chickens).</p>
|
||||||
|
<p>No more than 20 passive mobs within a radius of 5 blocks (limits farms - including villagers and
|
||||||
|
excluding chickens).</p>
|
||||||
|
<p>No more than 10 hostile mobs within a single block (limits grinders).</p>
|
||||||
|
<p>No more than 10 turtles within a radius of 5 blocks (turtles cause a LOT of lag).</p>
|
||||||
|
<p>No more than 70 villagers within a radius of 50 blocks.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<div class="container">
|
||||||
|
<div class="paragraph">
|
||||||
|
<h2>Minecarts</h2>
|
||||||
|
<p>Minecarts are another type of entity that cause a lot of lag. They are harder on the server than you might
|
||||||
|
think.</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>Do not store villagers or other mobs long-term in minecarts. Use them for transportation and drop them
|
||||||
|
off in an enclosed location on the other end.
|
||||||
|
</li>
|
||||||
|
<li>Do not use farm designs that rely on many hopper minecarts. They are convenient because they pick up
|
||||||
|
items through blocks and have a faster throughput than hoppers, but they are unnecessarily hard on the
|
||||||
|
server.
|
||||||
|
</li>
|
||||||
|
<li>Avoid using minecarts for entity cramming purposes when possible and try to design your farm to only
|
||||||
|
have one such killing chamber if it is necessary.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<div class="container">
|
||||||
|
<div class="paragraph">
|
||||||
|
<h2>Hoppers</h2>
|
||||||
|
<p>Hoppers cause lag because they are constantly checking for items, even when nothing is in them, so you
|
||||||
|
should minimize your use of them.</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>When bringing items down through a vertical line of hoppers, try alternating them with single chests.
|
||||||
|
This saves you iron and saves the server a bit of lag.
|
||||||
|
</li>
|
||||||
|
<li>Bulk storage systems where random items are dropped in and sent into chests should be avoided because
|
||||||
|
they are likely to cause a situation that is especially taxing. More specifically: A hopper pushing items
|
||||||
|
into a full chest that are not in the chest OR A hopper trying to pull items from a chest to complete
|
||||||
|
stacks but none of the items in the chest match.
|
||||||
|
</li>
|
||||||
|
<li>Myth: Droppers/Furnaces on top of hoppers reduces lag. Droppers on top of hoppers do not reduce lag, and
|
||||||
|
furnaces are a slightly laggy object in their own right, so do not use them unnecessarily.
|
||||||
|
</li>
|
||||||
|
<li>When moving items long distances use water streams. Also try to use water to gather newly farmed items
|
||||||
|
rather than an array of hoppers. However, be aware that water streams have their own downfalls: First, the
|
||||||
|
item entity is at risk to be picked up by a pet with /petpickup turned on. So, do not use them near public
|
||||||
|
areas of your base or for high value items. Second, do not make water streams so long that they flow into
|
||||||
|
unloaded chunks, as the items can back up where chunks unload and create a ticking lag bomb of item
|
||||||
|
entities. Finally, set things up so that, if the storage system at the other end fills up, the extra items
|
||||||
|
get automatically destroyed rather than sitting on full hoppers to despawn.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<div class="container">
|
||||||
|
<div class="paragraph">
|
||||||
|
<h2>Redstone Farms</h2>
|
||||||
|
<p>Redstone contraptions are, by their nature, going to contribute some lag. But you can do things to reduce
|
||||||
|
their impact.</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>Biggers is not better. Try to make farms a reasonable size for what you need and no larger. Think about
|
||||||
|
how much of a resource you actually need.
|
||||||
|
</li>
|
||||||
|
<li>Think about the design you use. If you have a farm with a lot of pistons to break crops automatically,
|
||||||
|
can you set it up so that the pistons fire in sequence instead of all at once? Can you use a slimeblock
|
||||||
|
push bar so that you only need one piston and one observer for every 12 plants instead of one per each?
|
||||||
|
Minimize how much redstone is running at once.
|
||||||
|
</li>
|
||||||
|
<li>Set up your machines to shut off when not in use.</li>
|
||||||
|
<li>If you use a minecart hopper/chest to transport items, make sure it automatically docks when there are
|
||||||
|
no items to move. You can set up the minecart pickup to only run after a harvest and an auto-smelter that
|
||||||
|
uses minecarts to fill the furnaces does not need to have the minecart running all the time, only when
|
||||||
|
it’s dropping off items.
|
||||||
|
</li>
|
||||||
|
<li>Redstone clocks rarely need to run all the time. For example, an item disposal dropper can be set up to
|
||||||
|
automatically trigger the clock when there are items in the dropper and to shut off the clock when the
|
||||||
|
dropper is empty.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<div class="container">
|
||||||
|
<div class="paragraph">
|
||||||
|
<h2>Other Entities</h2>
|
||||||
|
<p>You may have noticed a theme in this list. Everything we've listed is an <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">entity</span>. Entities are the biggest cause of lag on
|
||||||
|
Altitude, and anything we can do to reduce the number of entities on the server will help!</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>Block Entities. Any block entity causes more lag than a normal block, but some cause more than others.
|
||||||
|
<a href="https://minecraft.gamepedia.com/Block_entity">minecraft.gamepedia.com/Block_entity</a></li>
|
||||||
|
<li>Signs with text on them are laggier than empty signs, so consider if you need to write that sign. Also,
|
||||||
|
be sure to not write anything on signs used for technical reasons, like to hold back lava or water.
|
||||||
|
</li>
|
||||||
|
<li>Item frames also are a small contributor to lag, but can add up, so please do not use a lot of them for
|
||||||
|
no reason.
|
||||||
|
</li>
|
||||||
|
<li>Brewing Stands cause more lag than you might think, so please do not have more placed than you need.
|
||||||
|
</li>
|
||||||
|
<li>Item Entities. Please pick up item entities when mining, even if you don’t actually want them. You can
|
||||||
|
dispose of them by throwing them in lava or on a cactus. But leaving mass amounts of items to despawn puts
|
||||||
|
an unnecessary strain on the server.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
.full-page-list {
|
||||||
|
margin-left: 70px;
|
||||||
|
font-family: 'opensans', sans-serif;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: var(--font-color);
|
||||||
|
transition: 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-page-list li {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlightedContent {
|
||||||
|
width: 70%;
|
||||||
|
margin: auto;
|
||||||
|
background: var(--color-quinary);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlightedContent p {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
main .container {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 670px) {
|
||||||
|
main .container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LagComponent } from './lag.component';
|
||||||
|
|
||||||
|
describe('LagComponent', () => {
|
||||||
|
let component: LagComponent;
|
||||||
|
let fixture: ComponentFixture<LagComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LagComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LagComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {HeaderComponent} from "../header/header.component";
|
||||||
|
import {NgClass, NgOptimizedImage} from '@angular/common';
|
||||||
|
import {ScrollService} from '../scroll/scroll.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-lag',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
NgOptimizedImage,
|
||||||
|
NgClass
|
||||||
|
],
|
||||||
|
templateUrl: './lag.component.html',
|
||||||
|
styleUrl: './lag.component.scss'
|
||||||
|
})
|
||||||
|
export class LagComponent {
|
||||||
|
showExceptions: boolean = false;
|
||||||
|
|
||||||
|
constructor(public scrollService: ScrollService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleExceptions() {
|
||||||
|
this.showExceptions = !this.showExceptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<h2 mat-dialog-title>Login</h2>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<form [formGroup]="loginForm">
|
||||||
|
<mat-form-field appearance="fill" style="width: 100%">
|
||||||
|
<mat-label>Enter your code</mat-label>
|
||||||
|
<input matInput formControlName="code" type="text">
|
||||||
|
<mat-error *ngIf="formHasError()">
|
||||||
|
Code is required
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions align="end">
|
||||||
|
<button mat-button (click)="onCancel()">Cancel</button>
|
||||||
|
<button mat-flat-button color="primary" (click)="onSubmit()" [disabled]="!loginForm.valid">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-dialog-content {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-dialog-actions {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: LoginComponent;
|
||||||
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LoginComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle} from '@angular/material/dialog';
|
||||||
|
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {NgIf} from '@angular/common';
|
||||||
|
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||||
|
import {AuthService} from '../services/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatDialogTitle,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions,
|
||||||
|
NgIf
|
||||||
|
],
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrl: './login.component.scss'
|
||||||
|
})
|
||||||
|
export class LoginDialogComponent {
|
||||||
|
public loginForm: FormGroup;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<LoginDialogComponent>,
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private authService: AuthService,
|
||||||
|
private snackBar: MatSnackBar
|
||||||
|
) {
|
||||||
|
this.loginForm = this.fb.group({
|
||||||
|
code: ['', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.minLength(8),
|
||||||
|
Validators.maxLength(8),
|
||||||
|
Validators.pattern('^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]+$')
|
||||||
|
]]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (!this.loginForm.valid) {
|
||||||
|
this.snackBar.open('Invalid code', '', {duration: 2000});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.snackBar.open('Logging in...', '', {duration: 2000});
|
||||||
|
this.authService.login(this.loginForm.value.code).subscribe({
|
||||||
|
next: (jwt) => {
|
||||||
|
this.dialogRef.close(jwt);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loginForm.get('code')?.setErrors({
|
||||||
|
invalid: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public formHasError() {
|
||||||
|
return this.loginForm.get('code')?.hasError('required');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'mapart'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Mapart</h1>
|
||||||
|
<h2>Altitude features a way to save maparts.</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>What is mapart?</h2>
|
||||||
|
<p>Any map is a piece of art, but the common meaning of this is an actual drawing onto a map. By placing
|
||||||
|
blocks on a plot to color in a map one pixel at a time, you can create an image.</p>
|
||||||
|
<p>Creating these maps is time consuming, and Altitude wants to support these mapartists by offering an
|
||||||
|
easier way to share their mapart..</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>How to save mapart</h2>
|
||||||
|
<p>After you have placed all blocks on your plot and have the image recorded on your map, hold the map in
|
||||||
|
your main hand and do <span style="font-family: 'opensans-bold', sans-serif;">/mapart save</span>. This
|
||||||
|
will cost you $1000 in-game currency. The saving process can take several minutes.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Useful resources for creating mapart</h2>
|
||||||
|
<p>Mapart may be created simply by zooming in on an image you wish to create and placing pixels by eye.
|
||||||
|
First, however, you must know what blocks to use for each color. This <a
|
||||||
|
href="https://minecraft.gamepedia.com/Map_item_format">Minecraft wiki article</a> explains map item
|
||||||
|
formatting such as colors available on maps and the blocks that correspond.</p>
|
||||||
|
<p>Pick whichever blocks are the most convenient to collect and to break later if you plan on reusing blocks
|
||||||
|
for multiple maps.</p>
|
||||||
|
<p>Additionally, there is an incredibly useful online tool known as Mapartcraft by Rebane that can take an
|
||||||
|
image and automatically convert it to a schematic that can be used in conjunction with
|
||||||
|
litematica/schematica as a guide to create much more detailed mapart! Be sure to select the correct
|
||||||
|
version of Minecraft in the settings and pick your preferred blocks. This tool can be found <a
|
||||||
|
href="https://rebane2001.com/mapartcraft/">here</a>.</p>
|
||||||
|
<p>Reminder: While schematica and litematica are permitted mods on Altitude, any features like auto-build
|
||||||
|
and easy-place they may include are not!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Is my mapart protected?</h2>
|
||||||
|
<p>The only person that can remove or clone saved maps is the one who created them. Mapart can be copied by
|
||||||
|
using the vanilla method on a cartography table.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>What mapart can I make</h2>
|
||||||
|
<p>If you plan on making some kind of map art that some may consider inappropriate, explicit, or
|
||||||
|
controversial, you can submit a request for review through our AltitudeBot support bot on Discord. Simply
|
||||||
|
message the bot anything, and the bot will reply with commands that you can utilize. For mapart review
|
||||||
|
requests, you will be using the command <span style="font-family: 'opensans-bold', sans-serif;">!mapart <description of map art></span>.
|
||||||
|
Please be sure to also attach an image of the mapart (it is not possible to submit the request without an
|
||||||
|
image)!</p>
|
||||||
|
<p>Creating inappropriate mapart without prior approval may result in deletion of the mapart itself and
|
||||||
|
punishment for the creator.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Useful Commands</h2>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart save -</span> Save a mapart</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart remove -</span> Delete a mapart</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart find -</span> See who made a specific
|
||||||
|
mapart
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MapartComponent } from './mapart.component';
|
||||||
|
|
||||||
|
describe('MapartComponent', () => {
|
||||||
|
let component: MapartComponent;
|
||||||
|
let fixture: ComponentFixture<MapartComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MapartComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MapartComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {HeaderComponent} from "../header/header.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-mapart',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent
|
||||||
|
],
|
||||||
|
templateUrl: './mapart.component.html',
|
||||||
|
styleUrl: './mapart.component.scss'
|
||||||
|
})
|
||||||
|
export class MapartComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'mypet'" height="460px" background_image="/public/img/backgrounds/babywither.png"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>MyPet</h1>
|
||||||
|
<h2>Tame almost any mob in-game as your pet and train it to fight along your side, collect dropped items for you,
|
||||||
|
and more!</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Claim a Pet</h2>
|
||||||
|
<p>To claim a pet you will need to find the mob you want in the survival world and <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">kill it with a lead in your hand</span>. You must have a
|
||||||
|
lead in your hand when you kill the mob or it will die instead of becoming your pet.</p>
|
||||||
|
<p>New players can have up to 3 pets in storage and 1 active pet at a time. More pets can be stored by
|
||||||
|
ranking up. To claim an additional pet you must first store your current pet with <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/petstore</span>. Once you have stored your current
|
||||||
|
pet you can simply kill another mob to make it your pet. You will not be able to call it or interact with
|
||||||
|
your other pets while they are in storage. To interact with your MyPet you will need to make it your
|
||||||
|
active MyPet by doing <span style="font-family: 'opensans-bold', sans-serif;">/petswitch</span> and
|
||||||
|
selecting the one you want to use.</p>
|
||||||
|
<img ngSrc="/public/img/items/lead.png" alt="Minecraft lead/leash" style="width: 20%;" height="96"
|
||||||
|
width="96">
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Skilltrees and Levels</h2>
|
||||||
|
<p>Pets can be assigned to a skilltree with <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/pcst</span>, which allows it to level up and unlock new
|
||||||
|
skills. There are 8 skilltrees to choose from, but Mage, Tank, Marksman, and Warrior all require you to
|
||||||
|
start with "Fighter". Pets earn XP when they kill mobs, and also earn a small amount of XP any time you
|
||||||
|
kill a mob.</p>
|
||||||
|
<p><b>Note:</b> pets with inventories will drop it on player death.</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Utility</span> - Specializes in mining with
|
||||||
|
pickup and inventory skills + a Haste II beacon at level 150. (150 levels)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Mount</span> - A rideable pet specializes in
|
||||||
|
speed and jump height + jump beacon at level 150, and inventory at level 200. (200 levels)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Defender</span> - Specializes in defending
|
||||||
|
you, it absorbs damage dealt towards you. High HP + regen beacon at level 200. (200 levels)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Fighter</span> - Starter rank for all other
|
||||||
|
fighter categories. Increases pet attack damage. (50 levels)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Mage</span> - Specializes in specialty attacks
|
||||||
|
like fireballs and lightning. No melee abilities. (100 levels)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Tank</span> - Specializes in defense with
|
||||||
|
lots of HP, redirecting damage, and sweeping attacks. Resistance beacon at level 100. (100 levels)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Marksman</span> - Specializes in ranged
|
||||||
|
attacks with arrow and poison damage, no melee abilities + invisibility beacon at level 50. (100 levels)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Warrior</span> - An all-around class with no
|
||||||
|
speciality, has some skills from every other class + a strength beacon at level 100. (100 levels)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Equip Your Pet</h2>
|
||||||
|
<p>Some pets can hold items in their hands or wear armor. To equip a pet with something to hold or wear,
|
||||||
|
simply right click on your pet while shifting with the item in your hand. To remove the equipment right
|
||||||
|
click on your MyPet while shifting with shears in your hand.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Naming Your Pet</h2>
|
||||||
|
<p>You can name your pet with <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/petname <name></span>. Please note that
|
||||||
|
inappropriate names (anything that wouldn't be allowed to be said in chat) are not allowed.</p>
|
||||||
|
<p>Duke members and above can change the color of their pet's name. To get a list of all the color codes
|
||||||
|
please do <span style="font-family: 'opensans-bold', sans-serif;">/colors</span> in-game.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Pet Behavior</h2>
|
||||||
|
<p>You can change the way your pet attacks by changing its behavior. Different behaviors are unlocked as
|
||||||
|
your pet levels up. The available behaviors are listed below.</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Friendly:</span> Won't fight, even if it's
|
||||||
|
attacked by anything
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Normal:</span> Acts like a normal wolf</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Aggressive:</span> Attacks everything within
|
||||||
|
15 blocks of the owner
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Farm:</span> Attacks every hostile mob within
|
||||||
|
15 blocks of the owner
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Raid:</span> Like normal, but the MyPet won't
|
||||||
|
attack players and their pets
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">Duel</span> Attacks other pets with active
|
||||||
|
duel behavior within a 5 block radius
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Feed Your Pet</h2>
|
||||||
|
<p>You must feed your pet to keep its health up or it will die from hunger! If your pet is hungry it will
|
||||||
|
also be weaker and will deal less damage when attacking.</p>
|
||||||
|
<p>Each pet has a special food that it must be fed. You can see which food is right for your pet by doing
|
||||||
|
<span style="font-family: 'opensans-bold', sans-serif;">/petinfo</span>.</p>
|
||||||
|
<div class="inlineIcons">
|
||||||
|
<img ngSrc="/public/img/items/potato.png" alt="Minecraft potato" height="81" width="77">
|
||||||
|
<img ngSrc="/public/img/items/carrot.png" alt="Minecraft carrot" height="81" width="89">
|
||||||
|
<img ngSrc="/public/img/items/steak.png" alt="Minecraft steak" height="81" width="88">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>How to Hide/Disable a MyPet</h2>
|
||||||
|
<p>There are two ways to disable a pet, and they work very differently. The first, <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/petsendaway</span>, will hide your active MyPet until
|
||||||
|
you un-hide it with <span style="font-family: 'opensans-bold', sans-serif;">/petcall</span>. A pet that
|
||||||
|
has been hidden is still considered your "active MyPet", it just isn't visible right now. It also only
|
||||||
|
hides your pet during your current session.</p>
|
||||||
|
<p>The second way to disable a pet is to put it into storage with <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/petstore</span>. Pets that are in storage are
|
||||||
|
completely deactivated until you use <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/petswitch</span> to choose one you want to use. If
|
||||||
|
all of your pets are in storage, the server will tell you that you do not have a MyPet. This is also how
|
||||||
|
you can claim additional MyPets - by completely deactivating your current one first.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Blocked Mob Types</h2>
|
||||||
|
<p>The following mobs are not allowed to be claimed as a pet: the enderdragon, villagers and villager-like
|
||||||
|
mobs, shulkers, ghasts, and elder guardians. If you kill any of these mobs it will die and will NOT become
|
||||||
|
your pet.</p>
|
||||||
|
<p>Furthermore, all horses (including donkeys and mules) are currently disabled as pets due to them being
|
||||||
|
buggy.</p>
|
||||||
|
<div class="inlineIcons">
|
||||||
|
<img ngSrc="/public/img/items/enderdragon.png"
|
||||||
|
style="padding: 25px 25px;"
|
||||||
|
alt="Minecraft enderdragon mob"
|
||||||
|
height="100" width="100">
|
||||||
|
<img ngSrc="/public/img/items/villager.png"
|
||||||
|
style="padding: 25px 25px;"
|
||||||
|
alt="Minecraft villager mob"
|
||||||
|
height="100" width="100">
|
||||||
|
<img ngSrc="/public/img/items/zombievillager.png"
|
||||||
|
style="padding: 25px 25px;"
|
||||||
|
alt="Minecraft zombie villager mob"
|
||||||
|
height="100" width="100">
|
||||||
|
<img ngSrc="/public/img/items/shulker.png"
|
||||||
|
style="padding: 25px 25px;"
|
||||||
|
alt="Minecraft shulker mob"
|
||||||
|
height="100" width="100">
|
||||||
|
<img ngSrc="/public/img/items/ghast.png"
|
||||||
|
style="padding: 25px 25px;"
|
||||||
|
alt="Minecraft ghast mob"
|
||||||
|
height="100" width="100">
|
||||||
|
<img ngSrc="/public/img/items/elderguardian.png"
|
||||||
|
style="padding: 25px 25px;"
|
||||||
|
alt="Minecraft elder guardian mob"
|
||||||
|
height="100" width="100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Disclaimer</h2>
|
||||||
|
<p>When you die with your MyPet out, the MyPet will drop all of the items in its inventory. This means that
|
||||||
|
if your MyPet is in a dangerous area when you die, your items can be lost. And if you die in pvp the items
|
||||||
|
won't be protected. If your MyPet dies on it’s own, it will not drop it’s inventory.</p>
|
||||||
|
<p>Some skill trees require a base skill to be able to select them. The fighter skill tree is a base skill
|
||||||
|
and will not level beyond 50. When you reach level 50, you will need to switch to one of the secondary
|
||||||
|
skill trees with <span style="font-family: 'opensans-bold', sans-serif;">/pcst.</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Useful Commands</h2>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petcall -</span> Make your pet come to you if
|
||||||
|
it vanished
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petsendaway -</span> Make your pet vanish
|
||||||
|
temporarily
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petrelease <name>-</span> Release
|
||||||
|
your pet forever
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinfo -</span> Check your pet's health and
|
||||||
|
food as well as other stats
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pcst -</span> Choose a skilltree for your
|
||||||
|
pet.
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petskill -</span> Check your pet's skills and
|
||||||
|
strengths
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petname <name>-</span> Change the name
|
||||||
|
of your pet (see Naming Your Pet above)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinventory -</span> Open your pet's
|
||||||
|
inventory to see what it has picked up
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petpickup -</span> Toggle whether your pet
|
||||||
|
will pick up items or not
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbehavior -</span> Change the behavior
|
||||||
|
class of your pet (see Pet Behavior above)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbeacon -</span> Turn your pet into a
|
||||||
|
walking beacon to give you strength, speed, and other power-ups
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petswitch -</span> Allows you to switch
|
||||||
|
between your MyPets
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petstore -</span> Store your current pet to
|
||||||
|
claim another
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
main ul {
|
||||||
|
font-family: opensans, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main li {
|
||||||
|
margin-left: 30px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineIcons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MypetComponent } from './mypet.component';
|
||||||
|
|
||||||
|
describe('MypetComponent', () => {
|
||||||
|
let component: MypetComponent;
|
||||||
|
let fixture: ComponentFixture<MypetComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MypetComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MypetComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {HeaderComponent} from '../header/header.component';
|
||||||
|
import {NgOptimizedImage} from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-mypet',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
NgOptimizedImage
|
||||||
|
],
|
||||||
|
templateUrl: './mypet.component.html',
|
||||||
|
styleUrl: './mypet.component.scss'
|
||||||
|
})
|
||||||
|
export class MypetComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<mat-card>
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>Frames</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<div class="frames-container">
|
||||||
|
<mat-tab-group [selectedIndex]="frames.indexOf(currentFrame)"
|
||||||
|
(selectedIndexChange)="switchFrame(frames[$event])">
|
||||||
|
<mat-tab *ngFor="let frameId of frames" [label]="frameId">
|
||||||
|
<div class="frame-content">
|
||||||
|
<h3>Particles in {{ frameId }}</h3>
|
||||||
|
<div class="particles-list">
|
||||||
|
<div *ngFor="let particle of particleData.frames[frameId]; let i = index" class="particle-item">
|
||||||
|
<span>Particle {{ i + 1 }}: ({{ particle.x.toFixed(2) }}, {{ particle.y.toFixed(2) }}
|
||||||
|
, {{ particle.z.toFixed(2) }})</span>
|
||||||
|
<button mat-icon-button color="warn" (click)="removeParticle(frameId, i)">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!particleData.frames[frameId] || particleData.frames[frameId].length === 0"
|
||||||
|
class="no-particles">
|
||||||
|
No particles in this frame. Click on the plane to add particles.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="frame-actions">
|
||||||
|
<button mat-raised-button color="warn" (click)="removeFrame(frameId)"
|
||||||
|
[disabled]="frames.length <= 1">
|
||||||
|
Remove Frame
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
|
<div class="add-frame">
|
||||||
|
<button mat-raised-button color="primary" (click)="addFrame()">
|
||||||
|
Add New Frame
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user