🚀 뭘 하는가?
2년전 쯤 만들었던 스타듀밸리 순무모드를 한번 수정할 것이다!
오류가 있을 수 있고 완벽하지 않다는 건 알았지만, 뭘 해야 수정할 수 있는지 알지 못해 방치해 두었다..
사실 아무생각 없이 살았지만
최근에도 모드를 찾아주는 분들이 있어 내가 싼 똥을 치우려 한다.
😫 스타듀밸리 업데이트?
내 윈도우 머신에선 이미 visual studio가 없었기 때문에 초기 셋업부터 하나하나 시작했다.
그래도 이 모드는 쉽게 환경을 구성할 수 있었기 때문에 금방하는 줄 알았더니..
아마 예전 빌드시 사용했던 .Net Framework의 버전이 달라서 그런 것 같다.
이전 빌드는 4.5를 사용했는데, 비주얼 스튜디오 2022가 나오면서 사용할 수 없게 되었다.
2019를 설치해서 4.5 버전을 사용해도 문제는 해결되지 않았다.
알고보니 게임 자체가 net5.0으로 마이그레이션 했다고 한다..
아래의 지침을 따라 프로젝트를 업데이트했다.
프로젝트가 크지 않고 대부분의 설정파일을 건드리지 않았기 때문에 쉽게 업데이트 할 수 있었다!
🤗 잘 실행된다! 왠지 정감이 가는 친구인것 같다 흐흐
💡 가격 객체 WeekPrice 수정
namespace WhiteTurnip
{
class WeekPrice
{
public static int PATTERN_FLUCTUATING = 1;
public static int PATTERN_HIGH_SPIKE = 2;
public static int PATTERN_DECREASING = 3;
public static int PATTERN_LOW_SPIKE = 4;
public int pattern;
public int[] prices; // size: 14
public WeekPrice(int pattern)
{
this.pattern = pattern;
this.prices = new int[14];
this.prices[0] = TurnipPrice.TURNIP_BUY_PRICE;
this.prices[1] = TurnipPrice.TURNIP_BUY_PRICE;
}
}
}
위 코드가 기존 코드인데, 문제가 있다고 생각하는 부분은 아래와 같다.
- prices의 크기가 7가 아닌 14인 부분이 찝찝하다.
오전 오후로 나눠서 (7, 2)크기로 작성해야 하는게 아닐까? - 객체가 불변이 아니라서 어디서 가격을 건드릴지 괜히 걱정이 든다.
- 가격 패턴과 가격 객체는 게임이 끝날때도 따로 저장하지 않는다.
굳이 가격패턴을 가격객체가 가지고 있을 필요가 있을까?
결국 아래와 같이 수정했다.
class TurnipWeekPrice
{
private int[,] prices; // size: (7, 2)
public TurnipWeekPrice(int[,] prices)
{
this.prices = prices;
}
public int GetPrice(int dayOfWeek, bool isAfternoon)
{
if (isAfternoon)
{
return prices[dayOfWeek, 1];
}
else
{
return prices[dayOfWeek, 0];
}
}
}
💸 채소 값 인센티브 이슈
다른 스터디를 하며 자바 컬렉션 코드나 스프링 코드를 많이 뜯어본 경험을 바탕으로 제공된 스타듀밸리 코드도 뜯어보았다.
여기서 한가지 발견한 점이 있었는데, 바로 채소값 인센티브 이슈였다.
채소의 카테고리 코드는 -75인데, 아래 코드에서 보면 10%의 인센티브를 주는 로직이 들어가 있다.
카테고리를 다른 걸로 바꿔도 인센티브는 피하기 힘들다.
(과일도 1.1만큼의 인센티브가 들어가고 울타리로 지정하면 인센티브가 붙지 않지만 순무는 설치가 불가능한 오브젝트이기 때문에 울타리로 설정할 수는 없다.)
float num2 = 1f;
if (allFarmer.professions.Contains(0) && (flag || base.Category == -5 || base.Category == -6 || base.Category == -18))
{
num2 *= 1.2f;
}
if (allFarmer.professions.Contains(1) && (base.Category == -75 || base.Category == -80 || (base.Category == -79 && !isSpawnedObject)))
{
num2 *= 1.1f;
}
if (allFarmer.professions.Contains(4) && base.Category == -26)
{
num2 *= 1.4f;
}
게다가 설상가상으로 난이도에 따라 가격에 인센티브를 더해주는 로직도 들어가 있다.
public virtual int sellToStorePrice(long specificPlayerID = -1L)
{
if (this is Fence)
{
return price;
}
if (base.Category == -22)
{
return (int)((float)(int)price * (1f + (float)(int)quality * 0.25f) * (((float)(FishingRod.maxTackleUses - uses.Value) + 0f) / (float)FishingRod.maxTackleUses));
}
float startPrice = (int)((float)(int)price * (1f + (float)Quality * 0.25f));
startPrice = getPriceAfterMultipliers(startPrice, specificPlayerID);
if ((int)parentSheetIndex == 493)
{
startPrice /= 2f;
}
if (startPrice > 0f)
{
// 요부분
startPrice = Math.Max(1f, startPrice * Game1.MasterPlayer.difficultyModifier);
// 요부분
}
return (int)startPrice;
}
결국 가격을 설정하기 전 difficultyModifier * 1.1 만큼 나눈 값을 올림해서 설정해주었다.
오차가 발생하는 경우의 수는 0~1000 원 중에 100개정도 발생한다.
float multiplier = 1.1f * Game1.MasterPlayer.difficultyModifier;
int realPrice = (int)Math.Ceiling(price / multiplier);
성공했다!
어디서 구매하든 스타듀밸리에서 제공하는 로직을 사용한다면 원가격에 구매할 수 있다.
(이제 다른 모드에서 어둠의 경로로 판매하면 10% 더 싸게 팔아야 한다 흐흐)
😉 복잡한 가격 전략 리팩토링
원래 순무의 가격 전략은 아래와 같이 구성했다.
- 순 감소
- 파도형
- 높게 솟는 형
- 낮게 솟는 형
유저의 UniqueId와 날짜의 week로 랜덤 시드를 정하기 때문에 전략과 가격은 항상 일정하게 구성했다.
주마다 가격 전략이 랜덤으로 고정되고
간단한 로직에 따라 가격이 랜덤으로 요동친다.
모드를 사용한 사용자분들의 말에 따르면 가격이 가끔 마이너스가 되기도 한다고 하던데 아마 여기서 문제가 발생한게 아닌가 싶다.
기존 코드는 이 가격 전략을 TurnipPrice라는 클래스에서 모두 담당했다.
그래서 어디가 문제인지 알기 어렵고, random.next가 난발하기 때문에 불편했다. 🤮
아래는 기존의 가격 결정을 담당하는 클래스들의 다이어그램이다.
겉보기엔 간단하지만 사실 TurnipPrice가 위에서 보았던 가격 전략을 알아서 선택하고 알아서 WeekPrice를 만들어준다.
많은 일을 하고 있기 때문에 변경도 쉽지 않고 오류가 발생해도 알기 어렵다.
새롭게 리팩토링한 코드는 아래와 같다.
전략패턴을 적용해보았고, TurnipContext클래스를 만들어 제어의 역전 개념을 적용했다.
먼저 가격전략을 IPricePattern을 구현한 구현클래스로 나눴다.
그리고 TurnipPrice는 TurnipWeekPriceFactory로 바꾸었고,
가격객체인 TurnipWeekPrice를 생성하게 했다.
생성할 때, 전략은 IPricePattern 구현체를 이용해 가격을 결정하고 TurnipWeekPrice를 생성하게 구성했다.
그리고 이 IPricePattern구현체와 TurnipWeekPriceFactory는 서로 클래스를 참조하지 못하게 하고 싶었다.
(얼마전 토비의 스프링을 보았는데, 결합도를 낮추려면 인터페이스를 만들어 클래스 참조를 줄이는게 좋다고 했다.)
객체들의 생성과 생성전략을 담당하는 TurnipContext클래스에게 객체 생성과 어떤 구현체를 어떤 확률값으로 결정할지 정해주었다.
또, 전부 불변 객체로 구성해서 오류가 발생했을 시 찾아봐야 할 점검포인트가 줄어들었다.
모드의 시작 포인트인 ModEntry는 가격 객체가 필요할때 TurnipContext에게 Factory를 받아 간단하게 생성할 수 있게 되었다.
👀 업데이트 릴리즈
https://github.com/2jun0/WhiteTurnip/releases/tag/Demo
'프로그래밍 > 기타' 카테고리의 다른 글
9번째 스프린트 회고 - 게시판 좋아요 구현과 JWT활용 (0) | 2023.01.25 |
---|---|
백준 허브 사용 후기 (0) | 2023.01.21 |
1.2 IoC_DI를 위한 빈 설정 메타정보 작성 (0) | 2023.01.03 |
스프링 시큐리티 기본 user/password 안먹힐때 인코더 확인해라! (0) | 2023.01.02 |
RuntimeError: dictionary changed size during iteration (0) | 2022.10.21 |