Compare commits

...
302 Commits
Author SHA1 Message Date
Hide_D 10c4814f34 Merge remote-tracking branch 'origin/devel' into compare/scenario-effects 2026-07-30 18:38:44 +00:00
Hide_D fe221a7bd9 test(compare): trace scenario action effects 2026-07-30 18:38:35 +00:00
Hide_D 191abf83c0 test: isolate S100 inheritance browser logs 2026-07-29 08:51:51 +00:00
Hide_D edfeae5f0f fix: block S100 inheritance stat resets 2026-07-29 08:42:38 +00:00
Hide_D 424184a51a fix: define mixed S100 dex conversion ownership 2026-07-29 07:29:43 +00:00
Hide_D 03a321f95d fix: prevent S100 dex conversion refills 2026-07-29 06:36:33 +00:00
Hide_D e3615fa6bb fix: cap S100 NPC dex growth at forty percent 2026-07-29 05:20:53 +00:00
Hide_D 32991f736d fix: expose S100 event traits to frontend 2026-07-29 05:11:19 +00:00
Hide_D 8b2748d3d4 feat: 재선택 현재 능력치 기준 표시 2026-07-28 16:56:02 +00:00
Hide_D 883080e275 feat: 100기 NPC 동조 및 후보 추첨 기능 개선과 초기 능력치 계산 로직 추가 2026-07-28 16:28:13 +00:00
Hide_D ce807e2564 유니크 획득 확률 조금 더 증가 2026-07-28 16:11:24 +00:00
Hide_D 9a2ac82373 시나리오 문제점 수정 2026-07-28 16:07:21 +00:00
Hide_D 3c35539c16 CreateManyNPC 추가 2026-07-28 16:04:50 +00:00
Hide_D 5ab08403c6 feat: 100기 NPC 동조와 후보 추첨 보완 2026-07-28 16:02:14 +00:00
Hide_D c1ce478d22 feat: exclude prior event seasons from all-star pool 2026-07-28 00:43:46 +00:00
Hide_D d8bf8ba922 feat: expand 100th-season pool through phase 99 2026-07-28 00:29:25 +00:00
Hide_D 8cdc9c9b84 compare: trace nation betting user lifecycle 2026-07-27 11:08:24 +00:00
Hide_D df189f4408 compare: trace NPC nation support lifecycle 2026-07-27 11:02:08 +00:00
Hide_D 47fa7885c5 compare: trace invader lifecycle across months 2026-07-27 10:48:40 +00:00
Hide_D 004dbb39b4 compare: add staged tax rate fixtures 2026-07-27 10:07:34 +00:00
Hide_D cd94c15137 compare: configure scenario static events 2026-07-27 09:49:51 +00:00
Hide_D eeb965353e compare: trace scenario 911 troop join event 2026-07-27 09:36:48 +00:00
Hide_D 6dd09bd4b4 compare: project general ownership presence 2026-07-27 09:09:40 +00:00
Hide_D a73189a604 compare: trace monthly disaster snapshots 2026-07-27 00:34:17 +00:00
Hide_D 4341341a07 compare: run optional general turn lifecycle 2026-07-26 23:30:25 +00:00
Hide_D 4b82f0b35c compare: preserve ordered conflict fixtures 2026-07-26 23:09:26 +00:00
Hide_D b417e117ad compare: expose active action inheritance points 2026-07-26 21:49:35 +00:00
Hide_D 85e435e99d compare: expose disband membership fields 2026-07-26 20:12:13 +00:00
Hide_D e48c13a92f compare: expose founding cooldown field 2026-07-26 17:26:57 +00:00
Hide_D 4495f0c06e compare: expose status transition fields 2026-07-26 17:03:53 +00:00
Hide_D 312711b7ed compare: expose officer city in turn snapshots 2026-07-26 15:14:49 +00:00
Hide_D 3e8c18b8d3 compare: expose canonical history logs 2026-07-26 13:09:37 +00:00
Hide_D fb0f768f2c compare: expose world killturn in turn snapshots 2026-07-26 12:54:05 +00:00
Hide_D 5fc230d89b compare: align isolated rank nation fixtures 2026-07-26 12:51:10 +00:00
Hide_D 5fd1a6c752 compare: require matching args for multistep completion 2026-07-26 12:34:49 +00:00
Hide_D 76a5ea273a compare: observe nation history in turn snapshots 2026-07-26 12:04:55 +00:00
Hide_D 3d0a5a4be8 merge: observe betrayal in turn traces 2026-07-26 10:25:49 +00:00
Hide_D 23309113ec test: observe betrayal in turn traces 2026-07-26 10:25:41 +00:00
Hide_D 1a1bc541f5 merge: clone fixture general rank data 2026-07-26 09:46:12 +00:00
Hide_D 20060a89fd test: clone fixture general rank data 2026-07-26 09:45:57 +00:00
Hide_D 24f788d1a0 merge: observe selected diplomacy pairs 2026-07-26 09:30:30 +00:00
Hide_D 813957479d test: observe selected diplomacy pairs 2026-07-26 09:30:21 +00:00
Hide_D cdf58c54e0 merge: create missing fixture generals 2026-07-26 08:52:43 +00:00
Hide_D 6399134380 test: create missing fixture generals 2026-07-26 08:52:26 +00:00
Hide_D eb49e6f8ff merge: expose nation command cooldowns 2026-07-26 08:49:50 +00:00
Hide_D ef855eb2cc test: expose nation command cooldowns 2026-07-26 08:49:41 +00:00
Hide_D c970e6586d test: expose random capital city metadata 2026-07-26 07:58:49 +00:00
Hide_D 6eaa529048 merge: compare best-general rank data 2026-07-26 06:05:12 +00:00
Hide_D fe39241a90 test: compare general rank data state 2026-07-26 06:05:06 +00:00
Hide_D d936d93be8 merge: trace general command cooldown state 2026-07-26 04:55:23 +00:00
Hide_D 89127c0258 Trace general command cooldown state 2026-07-26 04:55:17 +00:00
Hide_D 57aa07453e merge: trace instant diplomacy responses 2026-07-26 03:11:09 +00:00
Hide_D 0722f6ab5e Add instant diplomacy response traces 2026-07-26 03:10:50 +00:00
Hide_D b9c51da782 merge: observe generated nation command generals 2026-07-26 02:29:03 +00:00
Hide_D c290404e42 test: observe generated nation command generals 2026-07-26 02:28:48 +00:00
Hide_D cc0f965cb4 merge: project nation strategy limit 2026-07-26 01:55:01 +00:00
Hide_D 64736d194e test: project nation strategy limit 2026-07-26 01:54:32 +00:00
Hide_D 963b04e8b0 merge: project nation receive-assist state 2026-07-26 01:44:38 +00:00
Hide_D e58ffab2a2 test: project nation receive-assist state 2026-07-26 01:44:22 +00:00
Hide_D de29e2e157 merge: support multi-turn nation command traces 2026-07-26 01:28:27 +00:00
Hide_D 7aa1b1889c test: support multi-turn nation command traces 2026-07-26 01:28:15 +00:00
Hide_D 618cfe5720 Merge general command comparison fixtures 2026-07-26 01:05:44 +00:00
Hide_D a8cecf8ec2 test: extend general command comparison fixtures 2026-07-26 01:05:30 +00:00
Hide_D d858c64a3c merge: expand turn command fixture controls 2026-07-26 00:11:00 +00:00
Hide_D f49b1d26a5 test: expand turn command fixture controls 2026-07-26 00:10:47 +00:00
Hide_D 92e5f2472d merge: add aggregate monthly boundary trace 2026-07-25 23:43:09 +00:00
Hide_D 11144f1f88 test: add aggregate monthly boundary trace 2026-07-25 23:43:03 +00:00
Hide_D 34c9a9b53e merge: trace core monthly event actions 2026-07-25 22:47:42 +00:00
Hide_D 84d9cf9eb4 test(compare): trace core monthly event actions 2026-07-25 22:47:35 +00:00
Hide_D 8a4cc5df21 merge: trace monthly post tail effects 2026-07-25 22:33:50 +00:00
Hide_D df0b8ccebe test(compare): trace monthly post tail effects 2026-07-25 22:33:41 +00:00
Hide_D ac47bc58bd merge: add monthly wander comparison trace 2026-07-25 22:21:01 +00:00
Hide_D 570ef6f005 test(compare): trace monthly wander disband 2026-07-25 22:20:55 +00:00
Hide_D 7eecde62fa merge: add monthly diplomacy comparison trace 2026-07-25 22:08:19 +00:00
Hide_D 4e0e5c9cd7 test(compare): trace monthly diplomacy transitions 2026-07-25 22:08:08 +00:00
Hide_D 18abcde0d5 merge: add monthly nation statistics trace 2026-07-25 21:57:21 +00:00
Hide_D 6176b64c28 test(compare): trace monthly nation statistics 2026-07-25 21:57:10 +00:00
Hide_D 3bae1157b6 merge: add monthly pre-update comparison trace 2026-07-25 21:36:49 +00:00
Hide_D 5a51ad5699 test(compare): trace monthly pre-update boundary 2026-07-25 21:36:26 +00:00
Hide_D 312d20269c merge: trace remaining monthly event actions 2026-07-25 21:19:49 +00:00
Hide_D 178d6f042b test: trace monthly unique and inheritance actions 2026-07-25 21:15:35 +00:00
Hide_D 3352b64d0f merge: trace monthly speciality and betrayal actions 2026-07-25 20:55:59 +00:00
Hide_D e7d86a16d5 test: trace monthly speciality and betrayal actions 2026-07-25 20:55:40 +00:00
Hide_D 820c25d8dc merge: trace monthly scout policy actions 2026-07-25 20:28:43 +00:00
Hide_D f2dcb4d4d1 test: trace monthly scout policy actions 2026-07-25 20:28:33 +00:00
Hide_D de357cfe95 merge: trace nation betting lifecycle 2026-07-25 20:16:41 +00:00
Hide_D 0f333f8c96 Add nation betting comparison fixtures 2026-07-25 20:16:24 +00:00
Hide_D 93e8adadac merge: trace city changes and NPC troop leaders 2026-07-25 19:50:02 +00:00
Hide_D 8f98626075 test(compare): trace city changes and NPC troop leaders 2026-07-25 19:47:36 +00:00
Hide_D a321eb1362 merge: trace monthly invader lifecycle 2026-07-25 19:20:19 +00:00
Hide_D 1a17fad0a5 test: trace monthly invader lifecycle 2026-07-25 19:19:56 +00:00
Hide_D c74efbb372 merge: trace RaiseNPCNation monthly action 2026-07-25 18:56:46 +00:00
Hide_D 66f79ca5d2 test: trace monthly NPC nation creation 2026-07-25 18:56:40 +00:00
Hide_D f42f844f17 merge: trace monthly NPC registration 2026-07-25 18:40:21 +00:00
Hide_D aee98e15d7 test: trace monthly NPC registration 2026-07-25 18:40:12 +00:00
Hide_D 0936dabc9e merge: add CreateManyNPC comparison fixture 2026-07-25 18:16:11 +00:00
Hide_D 39b209caf0 test(compare): trace CreateManyNPC monthly action 2026-07-25 18:15:27 +00:00
Hide_D 07867b3aaf merge: add CreateAdminNPC no-op comparison fixture 2026-07-25 17:50:27 +00:00
Hide_D de1f74cb44 Add CreateAdminNPC no-op comparison fixture 2026-07-25 17:50:21 +00:00
Hide_D 65c97107c6 merge: add monthly war income comparison fixture 2026-07-25 17:45:43 +00:00
Hide_D d796eda13c Add monthly war income comparison fixture 2026-07-25 17:45:38 +00:00
Hide_D 2a9029470f merge: add semi-annual monthly comparison fixture 2026-07-25 17:35:40 +00:00
Hide_D 254364bab8 Add semi-annual monthly comparison fixture 2026-07-25 17:35:30 +00:00
Hide_D 0319410ade merge: add nation level monthly comparison fixture 2026-07-25 17:20:17 +00:00
Hide_D 6b64f6f8ed Add nation level monthly comparison fixture 2026-07-25 17:20:09 +00:00
Hide_D 62fcb2e987 merge: add monthly event comparison trace 2026-07-25 16:56:34 +00:00
Hide_D ff3ef4e4f3 test: add monthly city supply trace 2026-07-25 16:56:26 +00:00
Hide_D 008b0e416c merge: add full turn command comparison runner 2026-07-25 14:17:50 +00:00
Hide_D bf96849027 test: execute legacy turn command fixtures 2026-07-25 14:17:25 +00:00
Hide_D 1b035e4a42 merge: add canonical turn fixture setup 2026-07-25 11:20:57 +00:00
Hide_D 47dec60c2b test: support canonical legacy turn fixtures 2026-07-25 11:20:53 +00:00
Hide_D c03a28be93 merge: add legacy turn command trace runner 2026-07-25 11:14:28 +00:00
Hide_D d1ab2b0450 test: trace legacy turn command execution 2026-07-25 11:14:19 +00:00
Hide_D 95f66e4c39 feat: add 100th-season all-star growth event 2026-07-25 10:46:23 +00:00
Hide_D fe9ae97821 merge: add turn state differential exporter 2026-07-25 09:35:22 +00:00
Hide_D 98e0a676cb test: export canonical legacy turn snapshots 2026-07-25 09:35:04 +00:00
Hide_D 2bb1387bc4 test: expose legacy trait catalog 2026-07-25 08:05:38 +00:00
Hide_D af382c541e test: expose legacy item catalog for comparison 2026-07-25 06:38:54 +00:00
Hide_D 8616a094e5 test: add deterministic battle trace exporter 2026-07-25 05:56:45 +00:00
Hide_D 4de7ebec17 요양 깃수 추가 2026-03-04 14:16:08 +00:00
Hide_D d43dff35fb fix: 디버그 코드 복구 2026-02-18 18:54:46 +00:00
Hide_D 5b13f8bd30 ip 기록 2026-02-17 22:31:08 +09:00
Hide_D e51bc6f711 fix: 턴 데몬에 cookie jar 적용 2026-02-17 20:35:15 +09:00
Hide_D 1b08aa0c58 wip: 디버그 코드 추가 2026-02-17 19:12:28 +09:00
Hide_D f2547fe207 dbg 2026-02-17 18:32:04 +09:00
류화영 cec7726731 fix: 방랑군이 빈번하게 해산하는 문제 해결 시도 2026-01-19 22:50:11 +09:00
Hide_D d679a6fed8 fix: InheritancePointManager 버그 수정 2025-11-01 17:38:02 +00:00
Hide_D 6fe156eb7c fix: composer uninstall 누락 2025-10-29 13:00:46 +00:00
Hide_D 663ddbe76a fix: inheritance_result 작성 문제 2025-10-28 20:24:25 +00:00
Hide_D 30297454f7 refac: inheritance_result로 데이터 이관 2025-10-28 20:15:05 +00:00
Hide_D bc602722bf feat: schema에 inheritance_result 추가
- storage에 의미없이 쌓이는 데이터 이관
2025-10-28 20:09:30 +00:00
Hide_D be8f147216 dep: brandonwamboldt/utilphp 제거 2025-10-28 19:56:02 +00:00
Hide_D b9495fb64a refac: 턴 데몬 전체를 async로 변경경 2025-10-07 20:05:55 +09:00
Hide_D 07b217b290 fix: 서버 오픈 핸들러를 00초에 맞추도록 변경경 2025-10-07 19:54:24 +09:00
Hide_D 42fac46062 refac: brandonwamboldt/utilphp 의존 코드 수정
- 실제 의존성 제거는 이후에에
2025-10-07 19:26:22 +09:00
Hide_D 24bc8985d0 __filename 2025-10-04 11:39:35 +00:00
Hide_D 76a2d5ff63 daemon typescript 버전 추가 2025-10-04 11:29:03 +00:00
Hide_D 148f32f78b fix: execute engine updated 2025-10-04 11:04:36 +00:00
Hide_D 62f5b6597e proc.php 일부 수정
- api.php 미사용 경로 대비
2025-10-04 10:59:56 +00:00
Hide_D 7e810b0279 LogText 끄기 2025-09-07 06:01:04 +00:00
Hide_D 2336b29b65 fix: ?? 2025-08-30 06:15:25 +00:00
Hide_D 185c0e16a0 fix: AI 일부 key 접근 문제 수정 2025-08-30 06:11:37 +00:00
Hide_D 656baeb9ee fix: 토너먼트 분노 공격 확률 오류 수정 (#263)
Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/263
2025-08-23 10:08:13 +09:00
류화영 3e06304dc7 fix: 토너먼트 분노 공격 확률 오류 수정 2025-08-23 09:23:00 +09:00
Hide_D 4bb940c8c5 fix: ?? 연산자 우선순위 2025-08-20 12:59:40 +00:00
Hide_D 2b376b6aab fix: logError 버그 수정 2025-07-06 06:15:18 +00:00
Hide_D 04df26ff83 misc: 군웅할거 손견 국명 손책으로 변경
- 서버 시작 시점에서 손견은 맞으나, 땅따 시작점에는 손책임
2025-06-28 12:37:16 +00:00
Hide_D aca4240b92 game: 강서유서 월드로 명칭 변경 2025-06-26 18:37:42 +00:00
Hide_D faf26fa8a2 game: 강서유서 소설 월드(비세력) 수정 2025-06-26 18:36:19 +00:00
Hide_D 030ab1e7ea game: 군웅축록 연도 수정 2025-06-26 18:25:14 +00:00
Hide_D 7f2c1d4782 game: 역사모드4-2 황제 원술
- 제작자: 강서유서
- 유비가 서주를 가지고 있어야 원술을 치지 않나?
2025-06-26 18:23:36 +00:00
Hide_D 9132138a99 game: 군웅축록 외교 2025-06-26 17:54:32 +00:00
Hide_D 84875d60bd game: 역사모드2-2 반동탁연합 결성(정사)
- 제작자: 강서유서
2025-06-26 17:51:39 +00:00
Hide_D 3cbaf76626 game: 역사모드3-2 군웅축록
- 제작자: 강서유서
2025-06-26 17:26:26 +00:00
Hide_D f4846d0d66 game: 무한대흥 NPC 능력치 감소 2025-06-25 15:07:52 +00:00
Hide_D f183dcc72b frontend: 기술 제한 표기 수정 2025-06-25 14:16:19 +00:00
Hide_D 63cc5d8e88 game: 무한대흥 초기 성수 3천 2025-06-25 13:58:47 +00:00
Hide_D f858c32fdf feat: 점령 후 일반 도시 성벽 수치 조정 가능 2025-06-25 13:58:29 +00:00
Hide_D 60eb127d71 game: 천지비급 상인 등장 2025-06-25 13:54:47 +00:00
Hide_D ede33f3a37 game: ban 메시지 빈도 감소 2025-06-25 13:53:36 +00:00
Hide_D ffd53a2390 game: 무한대흥 스탯 2025-06-25 13:20:21 +00:00
Hide_D 2591fbde67 game: 무한대흥 시나리오 2025-06-25 13:12:41 +00:00
Hide_D 922d3f1938 feat: 기술 등급 제한 수치 유연화 2025-06-25 13:12:32 +00:00
Hide_D 64522ba8a6 fix: ChangeCity에서 _max 키 입력 가능하게 2025-06-25 13:09:24 +00:00
Hide_D 7b1eedb574 feat: ChangeCity에 최대 내정 수치도 연산자로 바꿀 수 있도록 2025-06-25 12:31:29 +00:00
Hide_D 7ec6ca605f run_daemon timeout 2025-05-28 13:55:30 +00:00
Hide_D 5a5c613814 game: 랜임 확률 보정 완화
- 강국의 디버프 감소소
2025-03-05 17:35:08 +00:00
Hide_D 1a3ef20a35 fix: 다병종 밸런스 누락분 수정정 2025-02-08 13:54:01 +00:00
Hide_D fba966177d fix: 고급모드 연구턴 커맨드 창 넘치는 문제 2025-01-25 19:52:28 +00:00
Hide_D 62c72dfdbe fix: 척사 버그 수정 2025-01-24 15:26:56 +09:00
Hide_D 1ae1ff9b72 refac,fix: reqTech, reqRegions, reqCities 간이 호출 추가 2025-01-24 15:23:13 +09:00
Hide_D c1e12c2737 fix: 버그 수정 2025-01-24 15:19:23 +09:00
Hide_D de09a66d6f fix: 병종 연구 커맨드에서 금쌀 검새 안하는 문제 해결 2025-01-24 10:07:56 +09:00
Hide_D 3c2012da46 fix: 사령턴에서 불가능한 턴이 별도 강조가 없는 문제 수정 2025-01-23 17:32:36 +00:00
Hide_D d55be54320 fix: 연구커맨드 검사 조건 2025-01-23 17:27:20 +00:00
Hide_D dcbefa1a96 fix: 연구 커맨드에 금쌀 표기 없음 2025-01-23 17:20:10 +00:00
Hide_D be8f1fff96 fix: 버그 수정 2025-01-23 17:17:37 +00:00
Hide_D 6117dbdcc0 fix: 이벤트 다병종 화시병 위치 2025-01-23 17:14:35 +00:00
Hide_D 0a39ba1023 fix: NPC AI가 현재 병종 기술등급을 고려하지 않는 문제 수정
- 아직 사용하지 않는 징병 페이지 요구 기술 항목도 수정
2025-01-23 17:13:11 +00:00
Hide_D dae5f65baa fix: 특성초토화시에만 did_특성초토화 증가 2025-01-23 17:08:50 +00:00
Hide_D 080bb30103 fix: 버그수정2 2025-01-22 19:42:43 +00:00
Hide_D 9e647c7ce5 fix: 도시 규모조건 징병 불가 2025-01-22 19:41:20 +00:00
Hide_D 73606b0fb4 feat: 초토화시 내부 카운트 추가 2025-01-22 18:54:23 +00:00
Hide_D e62eb36cc2 feat: event 연구 커맨드 추가 2025-01-22 18:53:08 +00:00
Hide_D 6b81f744d4 feat: event용 연구 추가 2025-01-22 18:41:24 +00:00
Hide_D 1a9b3b78cb fix: 화륜차 조건 추가 2025-01-22 18:40:59 +00:00
Hide_D c3cbf956d0 fix: 병종 조건에서 같은 키가 여러개인 경우 대비 2025-01-22 18:40:49 +00:00
Hide_D 91188c555d fix: 커맨드의 aux값값 2025-01-22 18:16:37 +00:00
Hide_D 913028dd6c fix: 병종 특성관련 발생 버그 수정 2025-01-22 18:08:18 +00:00
Hide_D dfb3f6478d game(wip): 다병종 시나리오에 병종 지정 2025-01-22 17:56:17 +00:00
Hide_D 6afc6221d3 feat: 특정 등급 이상 도시 보유 조건 추가 2025-01-22 17:48:49 +00:00
Hide_D 7b5a02487e misc: ReqCitiesWithCityLevel 설명 보완 2025-01-22 17:36:17 +00:00
Hide_D 533b79762e game(wip): 다병종 깃수 준비 2025-01-22 17:29:58 +00:00
Hide_D 53b7dbfdd0 feat: 병종 징병 조건 추가
- 군주 및 수뇌
- 군주 및 수뇌 아님
- 도시의 규모 요구
- 특정 Nation Aux 값 요구
2025-01-22 17:17:30 +00:00
Hide_D fd14c5403b feat: NationAuxKey Enum 2025-01-22 16:57:23 +00:00
Hide_D dc476e73a1 feat: 병종 검사에서 $nation['aux']를 정식 입력 2025-01-22 16:54:27 +00:00
Hide_D e60150705e refac: Constraint의 generalObj destGeneralObj로 General 객체 입력 2025-01-22 16:53:39 +00:00
Hide_D 2869ba5c42 feat,refac: GameUnitConstraint 개념 추가
- GameUnitDetail의 요구 기술, 지역, 도시, 연도를 GameUnitConstraint로 통합
- GameUnitConstraint는 조건 검사, 설명 제공
- 병종 정의를 GameUnitConstraint 이용 재정의
- test 함수에 General 객체 요구,
- test 함수의 ownCities에 단순 도시 외 도시 정보 추가
- 변경된 값에 맞게 호출 함수 변경
2025-01-22 16:26:15 +00:00
Hide_D 91e56644b4 feat: 토너먼트 페이지 관리자 조작 버튼 제거
- 필요할땐 다른 페이지에서 만들어 쓰도록
2024-12-28 08:47:45 +00:00
Hide_D e0b7a3d763 game: 철기병 방어 향상 2024-12-22 05:25:47 +00:00
Hide_D 6a25f03687 fix: namespace 2024-12-22 04:28:29 +00:00
Hide_D 104f306deb fix: callSammoAPI apiPath 호출 2024-12-22 04:27:02 +00:00
Hide_D 09c21cd466 fix: api prefix 2024-12-22 04:23:30 +00:00
Hide_D 5b89797f57 fix: admin_member 페이지 수정 시도 2024-12-22 04:12:59 +00:00
Hide_D ab1cc5146d feat: 유저 관리 패널에 유저 차단 추가 2024-12-22 04:05:44 +00:00
Hide_D 2483a7723f feat: RootAPI/Admin/BanEmailAddress 2024-12-22 03:56:12 +00:00
Hide_D 367db15d93 misc: DB로는 global salt 넘기지 않음 2024-12-22 03:52:39 +00:00
Hide_D 30e02aac08 feat: 가입 시 영구 차단된 이메일 검사 2024-12-22 03:42:08 +00:00
Hide_D ce5ddf05fe fix: 1달이상 지난 삭제요청 계정에 대해 자동삭제하지 않는 버그 수정 2024-12-22 02:46:28 +00:00
Hide_D 475837bca0 feat: 영구 차단 계정용 table 2024-12-22 02:43:18 +00:00
Hide_D bd0ae25afd game: 징계가 남아있는 경우 탈퇴 불가 2024-12-21 09:14:14 +00:00
Hide_D 688de08feb game: 선양시에도 거병/수뇌/외교권자 페널티 검사 2024-12-21 08:56:24 +00:00
Hide_D 83b550adbe game: 거병에도 '건국 금지 패널티 적용' 2024-12-21 08:52:08 +00:00
Hide_D fa89bae7d9 game: NPC 장수 능력치 초기화 불가 2024-12-21 04:03:05 +00:00
Hide_D 456246362f game: NPC의 유저 관직 부여 로직 수정 2024-12-21 03:59:21 +00:00
Hide_D b472440ec1 fix: 이미지 경로에 빈칸이 있는 경우 2024-12-20 15:50:38 +00:00
Hide_D 7b5613d032 fix: 유저 부운영자는 시나리오 리셋시 생성하지 않음 2024-12-14 09:05:07 +00:00
Hide_D f5b3b985ce feat: 시스템 권한 분리
- 부운영자를 부운영자(시스템), 부운영자(유저)로 분리
2024-12-14 04:06:03 +00:00
Hide_D 515298700a term utf-8 2024-12-13 13:12:41 +00:00
Hide_D 41d7566acb game: 롤시나리오 메시지 빈도 재조정 2024-12-11 17:07:31 +00:00
Hide_D f4196263fa game: 롤 시나리오 NPC 메시지 빈도 조절 2024-12-11 17:04:07 +00:00
Hide_D 6bc69ac467 game: 리그오브레전드 v1.1 2024-12-09 17:49:37 +00:00
Hide_D 15b2c8b8e6 game: 재야 의술장은 국가 장수를 치료하지 않음 2024-12-06 18:12:24 +00:00
Hide_D 8e1d33e880 misc: 강서유서 소설 월드 추가 수정 2024-12-06 16:28:20 +00:00
Hide_D 797902552d misc: 강서유서 소설 월드 수정 2024-12-06 15:46:47 +00:00
Hide_D f3d2dcdad2 사소한 수정 2024-12-06 15:36:17 +00:00
Hide_D 928478a8ad misc: 강서유서 소설월드 업데이트 2024-12-06 15:34:50 +00:00
Hide_D 5023ee800b 시나리오 변환시 환경 변수에 json 허용 2024-12-06 15:22:47 +00:00
Hide_D 4a70625487 avif support 2024-12-06 15:09:07 +00:00
Hide_D 26327b5c8c 롤시나리오 m장 이름 지정 2024-12-06 12:37:09 +00:00
Hide_D 372f47760b 리그오브레전드 시나리오 연동 조정
- 군주가 '성인이 되어 나타납니다'로 무효화되는 문제 수정
2024-12-06 11:46:33 +00:00
Hide_D 3111a5eb71 롤 시나리오 1.0으로 업데이트, 비급 and/or 파죽지세도 추가 (#262)
Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/262
2024-12-05 17:07:54 +09:00
sars 74f914488b 빙의 금지 장수 포함 2024-12-05 00:04:10 -08:00
sars a8d219afec iconPath 재설정, 100에서 29로 숫자 조절 2024-12-04 23:26:54 -08:00
sars 4b4a7ffdf0 롤 시나리오 1.0으로 업데이트, 비급 and/or 파죽지세도 추가 2024-12-04 23:04:36 -08:00
Hide_D 98e2e2f125 리그오브레전드 베타 아이콘 추가 (#261)
Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/261
2024-12-03 12:24:50 +09:00
sars 05000ae954 리그오브레전드 베타 아이콘 추가 2024-12-02 19:21:45 -08:00
Hide_D 85bf53f317 챔피언들과 미니언들 시작날짜 조절 (#260)
Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/260
2024-11-27 17:42:28 +09:00
sars d65407079e 챔피언들과 미니언들 시작날짜 조절 v2 2024-11-27 00:11:52 -08:00
sars a8c9946aa9 챔피언들과 미니언들 시작날짜 조절 2024-11-26 23:59:35 -08:00
Hide_D 1e5b2de913 LoL 시나리오 생년월일 (#257)
Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/257
2024-11-27 16:26:34 +09:00
Hide_D 747ab3d563 LoL 시나리오 생년월일 2024-11-27 16:26:08 +09:00
sars 82ffbba661 수식을 수치로 수정해서 다시 변환시도 2024-11-26 23:18:01 -08:00
sars 3f08876079 리그오브레전드 시나리오 추가 2024-11-26 23:09:37 -08:00
Hide_D 5622839ffa feat: 설문 게시자 표시 2024-09-28 13:09:51 +00:00
Hide_D 4b3ab5e31c feat: 설문 등록 시 등록자 유저명 저장 2024-09-28 13:04:29 +00:00
Hide_D f774a28e2a fix: 로그인 시 세션에 저장되는 username을 id가 아니라 name값으로 변경 2024-09-28 13:03:00 +00:00
Hide_D 61bdb8da01 fix: 부대 이름 변경 오작동 2024-09-23 13:43:35 +00:00
Hide_D c117ada117 fix: event 커맨드 수정
- 탑승즉시이동: 불필요한 추가 조사 제거
- 발령즉시집합: 발령자가 탑승중인 경우 변수값 변경
  - 장수 커맨드 실행 시 cityID에 따라 다시 받아오므로 해당 변경으로 충분
2024-09-23 13:35:28 +00:00
Hide_D 1f0d0f992d game: 부대탈퇴지시 ui 2024-09-18 16:30:01 +00:00
Hide_D 6445b77b90 game: 부대탑승후 이동 이벤트시 로그 기록 2024-09-18 16:17:24 +00:00
Hide_D 6d935733e6 game: 충차 도구 치안치 하향 2024-09-18 16:09:02 +00:00
Hide_D cfd0a2d26f game: 충차 도구 추가 2024-09-18 16:08:37 +00:00
Hide_D 749e8f673b fix: StaticEventHandler 버그 수정 2024-09-18 16:08:25 +00:00
Hide_D 4812f324e0 fix: StaticEventHandler 버그 수정 2024-09-18 16:02:41 +00:00
Hide_D 085f34b307 feat: 부대탈퇴지시 2024-09-18 15:22:27 +00:00
Hide_D 622ce7b780 game: 축지 시나리오 벌점 제한 2배 2024-09-18 15:11:18 +00:00
Hide_D 34b1fc9c5e feat: 벌점 제한 계수를 GameConst로 이동 2024-09-18 15:10:34 +00:00
Hide_D 9cc27c40aa game: 축지 시나리오 수비 설정 제한 완화 2024-09-18 15:08:37 +00:00
Hide_D eece1cbae5 feat: 수비 설정 제한을 GameConst로 이동 2024-09-18 15:08:21 +00:00
Hide_D 7e6770b5e7 game: 축지 시나리오 유닛세트 2024-09-18 15:02:38 +00:00
Hide_D a0c032c2c6 game: 축지 이벤트 작업 2024-09-18 14:54:56 +00:00
Hide_D 3d12de3838 refac: API/Troop 의 $me를 GeneralLite로 변경
- StaticEventHandler 처리도 추가
2024-09-18 14:54:40 +00:00
Hide_D 477b77b59f feat: StaticEventHandler 처리기 추가
- 커맨드 종료 시점에 호출
  - pre, post 각각이 필요한지는 논의 필요
  - 일단 용도에 따라 pre, post 둘중에 하나만 처리
- eventType으로는 full qualified class name 사용
  - 꼭 그럴필요는 없지만, 커맨드에 한해서는 이렇게
2024-09-18 14:54:11 +00:00
Hide_D 04a7a34fcc 부대 이동 관련 초기 코드 2024-09-17 00:48:54 +09:00
Hide_D 86f1805160 fix: 병가 인구 상승률 버그 수정 (#255)
Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/255
2024-07-30 14:57:43 +09:00
jyp9 d573e9f98a fix: 병가 인구 상승률 버그 수정
fix: 병가 인구 상승률 버그 수정
2024-07-30 14:55:40 +09:00
Hide_D 3ad16d96be game: 긴급천도시 모든 장수에게 알림,
- 수뇌에게는 집합 메시지도 남김
2024-07-16 13:09:43 +09:00
Hide_D d8870caf29 feat: 포인트 초기화 추가
- 급하니까 일단 땜빵
2024-07-10 18:11:10 +00:00
Hide_D 7c9d5d34f6 feat: 스탯 초기화용 API 준비 2024-07-10 17:20:38 +00:00
Hide_D 5268ce9b71 game: 훈련, 사기진작 시 병력이 낮을 때도 숙련 상승량 조정 2024-07-10 17:00:35 +00:00
Hide_D e395c98a6c fix: 도시 주둔 장수 중 해당 국가 소속만 계략 확률 감소에 영향을 주도록 수정 (#254)
Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/254
2024-07-08 17:01:17 +09:00
Hide_D d5897e2a49 fix: 건국 유니크 100% 수정 2024-06-22 15:42:58 +00:00
Hide_D c82003f982 game: 건국시 확정 유니크, 유산 포인트 250점 2024-06-22 15:41:43 +00:00
Hide_D 5447274aa8 game: 사령턴 입력, 인사부 처리 시 삭턴 변경 2024-06-22 15:28:39 +00:00
Hide_D 688162ffbf fix: 이벤트 클래식은 907이 아니라 908이었어야함 2024-06-20 12:31:49 +00:00
Hide_D 220d1d821d fix: 삼황내문 로직 누락 수정 2024-06-19 13:23:07 +09:00
Hide_D 2b16c3c1a9 fix: 삼황내문을 새 저지 로직에 맞게 변경 2024-06-19 13:22:20 +09:00
Hide_D d731acdaef Revert "fix: 삼황내문을 새 저지 로직에 맞게 수정"
This reverts commit 3511b159a8.
2024-06-19 13:19:30 +09:00
Hide_D 3511b159a8 fix: 삼황내문을 새 저지 로직에 맞게 수정 2024-06-19 13:18:15 +09:00
Hide_D 28cea4ee09 fix: 메시지 수정 2024-06-08 18:55:48 +00:00
Hide_D 8e7227c29d fix: DummyGeneral에 penaltyList 2024-06-08 18:52:47 +00:00
Hide_D f0cc22d9d8 fix: NoChief 처리 부실 2024-06-08 18:48:46 +00:00
Hide_D b6f19fa38b feat: 유산포인트로 장수 소유자 확인 2024-06-08 18:42:39 +00:00
Hide_D 86305087f7 Revert "장수목록에서 장수명에 마우스를 올리면 본닉 공개"
This reverts commit a3d7ef30dd.
2024-06-08 17:52:13 +00:00
Hide_D 7b6f64db61 fix: 유저장이 수뇌 금지 장수밖에 없을때 NPC 임명 동작 2024-06-08 17:47:35 +00:00
Hide_D 06f086bcf0 fix: penalty 오타 2024-06-08 17:37:43 +00:00
Hide_D 606c1664f2 feat: 내정보에 징계목록 출력 2024-06-08 17:37:33 +00:00
Hide_D 0c07060f4d feat: 메시지 전송 페널티 2024-06-08 17:22:10 +00:00
Hide_D 8bd0cc1c64 feat: 건국 금지, 지정 임관 금지 패널티 2024-06-08 17:21:55 +00:00
Hide_D f12262b946 feat: 건국 불가 페널티 2024-06-08 16:16:52 +00:00
Hide_D 9415d79a3a feat: Constraint 시스템에 NoPaneltyKey 추가 2024-06-08 16:15:19 +00:00
Hide_D ad223a9c75 feat: NPC 임명로직에 NoChief, NoAmbassador 반영 2024-06-08 16:06:22 +00:00
Hide_D 8737b19e79 fix: 이미 수뇌인 장수는 NPC에게 외교권을 못받는 버그 수정 2024-06-08 15:58:52 +00:00
Hide_D 552d73430c feat: 유저장의 수뇌 임명 불가 penalty 2024-06-08 15:55:26 +00:00
Hide_D fba5fbdb6b feat: PenaltyKey::NoChiefTurnInput 2024-06-08 15:48:01 +00:00
Hide_D 3da11bf21c feat: 내 정보 설정 저장지 페널티 갱신 2024-06-08 15:41:27 +00:00
Hide_D c83f9a0465 game: 휴전 -> 종전 2024-06-08 14:43:40 +00:00
Hide_D 9f67ae1f4e feat: General::penaltyList 2024-06-08 14:38:31 +00:00
Hide_D 383431b79f feat: PenaltyKey enum 2024-06-08 14:06:12 +00:00
Hide_D d5c0d16758 game: 저지 작동 방식 변경
- 최대 페이즈를 1 깎는 형태로 변경
2024-06-08 13:24:32 +00:00
devsamanddevsam 87294d5477 fix: 도시 주둔 장수 중 해당 국가 소속만 계략 확률 감소에 영향을 주도록 수정 2024-06-01 07:00:35 +09:00
325 changed files with 138200 additions and 2654 deletions
+1
View File
@@ -29,6 +29,7 @@ d_pic/*.jpg
d_pic/*.gif d_pic/*.gif
d_pic/*.png d_pic/*.png
d_pic/*.webp d_pic/*.webp
d_pic/*.avif
d_pic/uploaded_image d_pic/uploaded_image
/dist_js /dist_js
-1
View File
@@ -12,7 +12,6 @@
} }
}, },
"require": { "require": {
"brandonwamboldt/utilphp": "^1.1",
"sergeytsalkov/meekrodb": "^2.5", "sergeytsalkov/meekrodb": "^2.5",
"league/plates": "^3.5", "league/plates": "^3.5",
"vlucas/valitron": "^1.4", "vlucas/valitron": "^1.4",
Generated
+2 -52
View File
@@ -4,58 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "deed9afdc95bc42f84784df67d295223", "content-hash": "7edae9b6be35e66ffd0d180f5fac6679",
"packages": [ "packages": [
{
"name": "brandonwamboldt/utilphp",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/brandonwamboldt/utilphp.git",
"reference": "36c32efc4f0679c05163464a550f45c8d83fe683"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brandonwamboldt/utilphp/zipball/36c32efc4f0679c05163464a550f45c8d83fe683",
"reference": "36c32efc4f0679c05163464a550f45c8d83fe683",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"satooshi/php-coveralls": "dev-master"
},
"type": "library",
"autoload": {
"psr-0": {
"utilphp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brandon Wamboldt",
"email": "brandon.wamboldt@gmail.com"
}
],
"description": "util.php is a collection of useful functions and snippets that you need or could use every day, designed to avoid conflicts with existing projects",
"homepage": "https://github.com/brandonwamboldt/utilphp",
"keywords": [
"collection",
"helpers",
"php",
"utility"
],
"support": {
"issues": "https://github.com/brandonwamboldt/utilphp/issues",
"source": "https://github.com/brandonwamboldt/utilphp/tree/master"
},
"time": "2015-02-02T17:56:14+00:00"
},
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.12.1", "version": "0.12.1",
@@ -5721,5 +5671,5 @@
"platform-overrides": { "platform-overrides": {
"php": "8.3" "php": "8.3"
}, },
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }
+4 -1
View File
@@ -99,7 +99,10 @@ function logError(string $err, string $errstr, string $errpath, array $trace)
$err = str_replace(ROOT, '{ROOT}', $err); $err = str_replace(ROOT, '{ROOT}', $err);
$errstr = str_replace(ROOT, '{ROOT}', $errstr); $errstr = str_replace(ROOT, '{ROOT}', $errstr);
$errpath = str_replace(ROOT, '{ROOT}', $errpath); $errpath = str_replace(ROOT, '{ROOT}', $errpath);
$trace = array_map(function (string $text) { $trace = array_map(function (string|array $text) {
if (is_array($text)) {
$text = Json::encode($text);
}
return str_replace(ROOT, '{ROOT}', $text); return str_replace(ROOT, '{ROOT}', $text);
}, $trace); }, $trace);
+1 -1
View File
@@ -56,7 +56,7 @@ $rootDB->insert('member', [
'token_valid_until' => '2999-01-01 00:00:00', 'token_valid_until' => '2999-01-01 00:00:00',
'pw' => $finalPassword, 'pw' => $finalPassword,
'salt' => $userSalt, 'salt' => $userSalt,
'grade' => 6, 'grade' => 7,
'name' => $nickname, 'name' => $nickname,
'reg_date' => $nowDate 'reg_date' => $nowDate
]); ]);
+10
View File
@@ -93,3 +93,13 @@ CREATE TABLE `login_token` (
) )
COLLATE='utf8mb4_general_ci' COLLATE='utf8mb4_general_ci'
ENGINE=Aria; ENGINE=Aria;
CREATE TABLE `banned_member` (
`no` INT NOT NULL AUTO_INCREMENT,
`hashed_email` VARCHAR(128) NOT NULL COLLATE 'utf8mb4_general_ci' COMMENT 'SHA512(salt | email | salt )',
`info` TEXT NULL COLLATE 'utf8mb4_general_ci' COMMENT '부가정보',
PRIMARY KEY (`no`),
UNIQUE INDEX `email` (`hashed_email`(128))
)
COLLATE='utf8mb4_general_ci'
ENGINE = Aria;
+1 -1
View File
@@ -6,7 +6,7 @@ include "func.php";
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if($session->userGrade < 5) { if($session->userGrade < 6) {
die(requireAdminPermissionHTML()); die(requireAdminPermissionHTML());
} }
+1 -1
View File
@@ -6,7 +6,7 @@ include "func.php";
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 5) { if ($session->userGrade < 6) {
header('location:_admin1.php'); header('location:_admin1.php');
die(); die();
} }
+1 -1
View File
@@ -7,7 +7,7 @@ include "func.php";
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 5) { if ($session->userGrade < 6) {
die(requireAdminPermissionHTML()); die(requireAdminPermissionHTML());
} }
+1 -1
View File
@@ -15,7 +15,7 @@ $msg = Util::getPost('msg', 'string');
//로그인 검사 //로그인 검사
$session = Session::requireLogin()->loginGame()->setReadOnly(); $session = Session::requireLogin()->loginGame()->setReadOnly();
if ($session->userGrade < 5) { if ($session->userGrade < 6) {
header('location:_admin2.php'); header('location:_admin2.php');
} }
+1 -1
View File
@@ -20,7 +20,7 @@ if ($type2 < 0 || $type2 > 6) {
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 5) { if ($session->userGrade < 6) {
die(requireAdminPermissionHTML()); die(requireAdminPermissionHTML());
} }
+1 -1
View File
@@ -12,7 +12,7 @@ $nation = Util::getPost('nation', 'int');
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
$userID = Session::getUserID(); $userID = Session::getUserID();
if ($session->userGrade < 5) { if ($session->userGrade < 6) {
header('location:_admin5.php'); header('location:_admin5.php');
die(); die();
} }
+2 -2
View File
@@ -31,13 +31,13 @@ $queryMap = [
]; ];
if ($reqQueryType === null || !key_exists($reqQueryType, $queryMap)) { if ($reqQueryType === null || !key_exists($reqQueryType, $queryMap)) {
$reqQueryType = Util::array_first_key($queryMap); $reqQueryType = array_key_first($queryMap);
} }
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 5) { if ($session->userGrade < 6) {
die(requireAdminPermissionHTML()); die(requireAdminPermissionHTML());
} }
+1 -1
View File
@@ -12,7 +12,7 @@ $type = 0;
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 5) { if ($session->userGrade < 6) {
die(requireAdminPermissionHTML()); die(requireAdminPermissionHTML());
} }
+1 -1
View File
@@ -7,7 +7,7 @@ include "func.php";
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 5) { if ($session->userGrade < 6) {
die('권한 부족'); die('권한 부족');
} }
+4 -1
View File
@@ -243,11 +243,14 @@ $templates = new \League\Plates\Engine(__DIR__ . '/templates');
"SELECT nation,no,name,owner_name as ownerName, owner, picture, imgsvr, "SELECT nation,no,name,owner_name as ownerName, owner, picture, imgsvr,
experience, dedication, experience, dedication,
dex1, dex2, dex3, dex4, dex5, dex1, dex2, dex3, dex4, dex5,
horse, weapon, book, item horse, weapon, book, item, aux
FROM general WHERE %l", FROM general WHERE %l",
$btn == "NPC 보기" ? "npc>=2" : "npc<2" $btn == "NPC 보기" ? "npc>=2" : "npc<2"
) as $general) { ) as $general) {
$generalID = $general['no']; $generalID = $general['no'];
foreach (['dex1', 'dex2', 'dex3', 'dex4', 'dex5'] as $dexKey) {
$general[$dexKey] = CentennialAllStarGrowthService::recordableRawValue($general, $dexKey);
}
$general['bgColor'] = $nationColor[$general['nation']] ?? GameConst::$basecolor4; $general['bgColor'] = $nationColor[$general['nation']] ?? GameConst::$basecolor4;
$general['fgColor'] = newColor($general['bgColor']); $general['fgColor'] = newColor($general['bgColor']);
$general['nationName'] = $nationName[$general['nation']]; $general['nationName'] = $nationName[$general['nation']];
+4 -15
View File
@@ -32,9 +32,11 @@ if ($type <= 0 || $type > 15) {
} }
$ownerNameList = []; $ownerNameList = [];
if ($gameStor->isunited) {
foreach (RootDB::db()->queryAllLists('SELECT no, name FROM member') as [$ownerID, $ownerName]) { foreach (RootDB::db()->queryAllLists('SELECT no, name FROM member') as [$ownerID, $ownerName]) {
$ownerNameList[$ownerID] = $ownerName; $ownerNameList[$ownerID] = $ownerName;
} }
}
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
@@ -52,15 +54,6 @@ foreach (RootDB::db()->queryAllLists('SELECT no, name FROM member') as [$ownerID
<?= WebUtil::printDist('vue', [], true) ?> <?= WebUtil::printDist('vue', [], true) ?>
<?= WebUtil::printDist('ts', 'common') ?> <?= WebUtil::printDist('ts', 'common') ?>
<style>
.hidden {
display: none;
}
.generalName:hover > .hidden {
display: block;
}
</style>
</head> </head>
<body> <body>
@@ -175,11 +168,7 @@ foreach (RootDB::db()->queryAllLists('SELECT no, name FROM member') as [$ownerID
$name = formatName($general['name'], $general['npc']); $name = formatName($general['name'], $general['npc']);
if (key_exists($general['owner'], $ownerNameList)) { if (key_exists($general['owner'], $ownerNameList)) {
if ($gameStor->isunited) { $name = $name . '<br><small>(' . $ownerNameList[$general['owner']] . ')</small>';
$name = $name . '<br><small class=\'userName\'>(' . $ownerNameList[$general['owner']] . ')</small>';
} else {
$name = $name . '<br><small class=\'userName hidden\'>(' . $ownerNameList[$general['owner']] . ')</small>';
}
} }
$general['refresh_score_total'] = Util::round($general['refresh_score_total'], -1); $general['refresh_score_total'] = Util::round($general['refresh_score_total'], -1);
@@ -196,7 +185,7 @@ foreach (RootDB::db()->queryAllLists('SELECT no, name FROM member') as [$ownerID
data-npc-type='{$general['npc']}' data-npc-type='{$general['npc']}'
> >
<td align=center><img class='generalIcon' width='64' height='64' src='{$imageTemp}/{$general['picture']}'></img></td> <td align=center><img class='generalIcon' width='64' height='64' src='{$imageTemp}/{$general['picture']}'></img></td>
<td align=center class='generalName'>$name</td> <td align=center>$name</td>
<td align=center>{$general['age']}세</td> <td align=center>{$general['age']}세</td>
<td align=center>" . displayCharInfo($general['personal']) . "</td> <td align=center>" . displayCharInfo($general['personal']) . "</td>
<td align=center>" . displaySpecialDomesticInfo($general['special']) . " / " . displaySpecialWarInfo($general['special2']) . "</td> <td align=center>" . displaySpecialDomesticInfo($general['special']) . " / " . displaySpecialWarInfo($general['special2']) . "</td>
+2 -2
View File
@@ -177,7 +177,7 @@ $templates = new \League\Plates\Engine('templates');
$valid = 1; $valid = 1;
} }
if ($userGrade >= 5) { if ($userGrade >= 6) {
$valid = true; $valid = true;
$showDetailedInfo = true; $showDetailedInfo = true;
} }
@@ -275,7 +275,7 @@ $templates = new \League\Plates\Engine('templates');
$ourGeneral = false; $ourGeneral = false;
} }
if ($userGrade == 6) { if ($userGrade == 7) {
$ourGeneral = true; $ourGeneral = true;
} }
+8
View File
@@ -143,6 +143,14 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
】<br><br> 】<br><br>
<input type=<?= $submit ?> id='set_my_setting' name=btn style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px; value=설정저장><br> <input type=<?= $submit ?> id='set_my_setting' name=btn style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px; value=설정저장><br>
∞<span style='color:orange'>설정저장은 이달중 <?= $myset ?>회 남았습니다.</span><br><br> ∞<span style='color:orange'>설정저장은 이달중 <?= $myset ?>회 남았습니다.</span><br><br>
<?php if ($me->getPenaltyList()->count()) : ?>
징계 목록(저장 시 갱신)<br>
<?php foreach ($me->getPenaltyList() as $penaltyKey => $penalty) : ?>
<span style='color:red'><?= $penaltyKey->getHelptext() ?> : <?= $penalty ?></span><br>
<?php endforeach; ?>
<br />
<?php endif; ?>
<?php if (!($gameStor->autorun_user['limit_minutes'] ?? false)) : ?> <?php if (!($gameStor->autorun_user['limit_minutes'] ?? false)) : ?>
휴 가 신 청<br> 휴 가 신 청<br>
<button type="button" id='vacation' style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>휴가 신청</button><br><br> <button type="button" id='vacation' style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>휴가 신청</button><br><br>
+1 -57
View File
@@ -125,63 +125,7 @@ $globalBetTotal = array_sum($globalBet);
</tr> </tr>
</table> </table>
<table align=center class='tb_layout bg0'> <table align=center class='tb_layout bg0'>
<?php if ($session->userGrade >= 5) : ?> <?php if ($me['no'] > 0 && $me['tournament'] == 0 && $admin['tournament'] == 1) : ?>
<form method=post action=c_tournament.php>
<tr>
<td colspan=8><input type=textarea size=150 style=color:white;background-color:black; name=msg><input type=submit name=btn value='메시지'></td>
</tr>
<tr>
<td colspan=8>
<button type="button" class="btn btn-secondary" onclick='location.reload()'>갱신</button>
<?php if ($admin['tournament'] == 0) : ?>
<select name=trig size=1 style=color:white;background-color:black;>
<option style=color:white; value=0 <?= !$admin['tnmt_trig'] ? 'selected' : '' ?>>수동진행</option>
<option style=color:white; value=1 <?= $admin['tnmt_trig'] ? 'selected' : '' ?>>자동진행</option>
</select>
<input type=submit name=btn value='자동개최설정'>
<?php else : ?>
<input type=submit name=btn value='중단' onclick='return confirm("진짜 중단하시겠습니까?")'>
<?php endif; ?>
<?php switch ($admin['tournament']) {
case 1:
echo "<input type=submit name=btn value='랜덤투입'>";
echo "<input type=submit name=btn value='랜덤전부투입'>";
break;
case 2:
echo "<input type=submit name=btn value='예선'><input type=submit name=btn value='예선전부'>";
break;
case 3:
echo "<input type=submit name=btn value='추첨'><input type=submit name=btn value='추첨전부'>";
break;
case 4:
echo "<input type=submit name=btn value='본선'><input type=submit name=btn value='본선전부'>";
break;
case 5:
echo "<input type=submit name=btn value='배정'>";
break;
case 6:
echo "<input type=submit name=btn value='베팅마감'>";
break;
case 7:
echo "<input type=submit name=btn value='16강'>";
break;
case 8:
echo "<input type=submit name=btn value='8강'>";
break;
case 9:
echo "<input type=submit name=btn value='4강'>";
break;
case 10:
echo "<input type=submit name=btn value='결승'>";
break;
} ?>
</td>
</tr>
</form>
<?php elseif ($me['no'] > 0 && $me['tournament'] == 0 && $admin['tournament'] == 1) : ?>
<form method=post action=c_tournament.php> <form method=post action=c_tournament.php>
<tr> <tr>
<td colspan=8><button type="button" class="btn btn-secondary" onclick='location.reload()'>갱신</button><button type="submit" class="btn btn-sammo-base2" onclick='return confirm("참가비 금<?= $admin['develcost'] ?>이 필요합니다. 참가하시겠습니까?")'>참가</button><input type='hidden' name='btn' value='참가'/></td> <td colspan=8><button type="button" class="btn btn-secondary" onclick='location.reload()'>갱신</button><button type="submit" class="btn btn-sammo-base2" onclick='return confirm("참가비 금<?= $admin['develcost'] ?>이 필요합니다. 참가하시겠습니까?")'>참가</button><input type='hidden' name='btn' value='참가'/></td>
+242
View File
@@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
namespace sammo;
use Ds\Map;
use sammo\Enums\RankColumn;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
final class ComparisonTracingRNG implements RNG
{
private int $sequence = 0;
/** @var array<int, array<string, mixed>> */
public array $calls = [];
public function __construct(private readonly RNG $inner)
{
}
public static function getMaxInt(): int
{
return LiteHashDRBG::getMaxInt();
}
public function nextBytes(int $bytes): string
{
$value = $this->inner->nextBytes($bytes);
$this->record('nextBytes', ['bytes' => $bytes], bin2hex($value));
return $value;
}
public function nextBits(int $bits): string
{
$value = $this->inner->nextBits($bits);
$this->record('nextBits', ['bits' => $bits], bin2hex($value));
return $value;
}
public function nextInt(?int $max = null): int
{
$value = $this->inner->nextInt($max);
$this->record('nextInt', ['maxInclusive' => $max], $value);
return $value;
}
public function nextFloat1(): float
{
$value = $this->inner->nextFloat1();
$this->record('nextFloat1', [], $value);
return $value;
}
private function record(string $operation, array $arguments, mixed $result): void
{
$this->calls[] = [
'seq' => $this->sequence++,
'operation' => $operation,
'arguments' => $arguments,
'result' => $result,
];
}
}
function comparisonExtractRankVar(array $raw): Map
{
$rankVars = new Map();
foreach ($raw as $rawKey => $rawValue) {
$key = RankColumn::tryFrom($rawKey);
if ($key !== null) {
$rankVars[$key] = $rawValue;
}
}
return $rankVars;
}
function comparisonBuildGeneral(
array $raw,
array $city,
array $nation,
int $year,
int $month,
bool $defender = false,
): General {
$aux = [];
if (array_key_exists('inheritBuff', $raw)) {
$aux['inheritBuff'] = $raw['inheritBuff'];
}
$raw['aux'] = Json::encode($aux);
$raw['owner'] = 0;
return new General(
$raw,
comparisonExtractRankVar($raw),
null,
$city,
$nation,
$year,
$month,
$defender,
);
}
function comparisonRunBattle(array $fixture): array
{
$seed = (string)($fixture['seed'] ?? 'battle-differential');
$year = (int)$fixture['year'];
$month = (int)$fixture['month'];
$startYear = (int)($fixture['startYear'] ?? 180);
$rawAttacker = $fixture['attackerGeneral'];
$rawAttackerCity = $fixture['attackerCity'];
$rawAttackerNation = $fixture['attackerNation'];
$rawDefenderCity = $fixture['defenderCity'];
$rawDefenderNation = $fixture['defenderNation'];
// 비교 fixture에 명시된 경우에만 ref runtime의 scenario action chain을
// 활성화합니다. 일반 비교 fixture의 기존 동작은 그대로 유지합니다.
$scenarioEffect = $fixture['scenarioEffect'] ?? null;
if ($scenarioEffect === '' || $scenarioEffect === 'None') {
$scenarioEffect = null;
}
if (
$scenarioEffect !== null
&& (!is_string($scenarioEffect) || getScenarioEffectClass($scenarioEffect) === null)
) {
throw new \InvalidArgumentException('scenarioEffect is invalid');
}
GameConst::$scenarioEffect = $scenarioEffect;
$tracingRng = new ComparisonTracingRNG(new LiteHashDRBG($seed));
$warRng = new RandUtil($tracingRng);
$attacker = new WarUnitGeneral(
$warRng,
comparisonBuildGeneral($rawAttacker, $rawAttackerCity, $rawAttackerNation, $year, $month),
$rawAttackerNation,
true,
);
$city = new WarUnitCity($warRng, $rawDefenderCity, $rawDefenderNation, $year, $month, $startYear);
$defenderList = [];
foreach ($fixture['defenderGenerals'] as $rawDefender) {
$defenderList[] = new WarUnitGeneral(
$warRng,
comparisonBuildGeneral($rawDefender, $rawDefenderCity, $rawDefenderNation, $year, $month, true),
$rawDefenderNation,
false,
);
}
if (count($defenderList) && extractBattleOrder($city, $attacker) > 0) {
$defenderList[] = $city;
}
usort(
$defenderList,
fn(WarUnit $lhs, WarUnit $rhs): int =>
-(extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker)),
);
$iterDefender = new \ArrayIterator($defenderList);
$iterDefender->rewind();
$finishedDefenders = [];
$getNextDefender = function (?WarUnit $previous, bool $requestNext) use (
$iterDefender,
$attacker,
&$finishedDefenders,
): ?WarUnit {
if ($previous !== null) {
$finishedDefenders[] = buildWarTraceUnitSnapshot($previous);
}
if (!$requestNext || !$iterDefender->valid()) {
return null;
}
$next = $iterDefender->current();
if (extractBattleOrder($next, $attacker) <= 0) {
return null;
}
$iterDefender->next();
return $next;
};
$events = [];
$conquered = processWar_NG(
$seed,
$attacker,
$getNextDefender,
$city,
static function (array $event) use (&$events): void {
$events[] = $event;
},
);
$attackerLogs = $attacker->getLogger()->rollback();
$defenderLogs = [];
foreach ($defenderList as $defenderUnit) {
if (!($defenderUnit instanceof WarUnitGeneral)) {
continue;
}
$snapshot = buildWarTraceUnitSnapshot($defenderUnit);
$defenderLogs[(string)$snapshot['id']] = $defenderUnit->getLogger()->rollback();
}
$cityLogs = $city->getLogger()->rollback();
return [
'engine' => 'ref',
'seed' => $seed,
'conquered' => $conquered,
'attacker' => buildWarTraceUnitSnapshot($attacker),
'city' => buildWarTraceUnitSnapshot($city),
'finishedDefenders' => $finishedDefenders,
'events' => $events,
'rng' => $tracingRng->calls,
'logs' => [
'attacker' => $attackerLogs,
'defenders' => $defenderLogs,
'city' => $cityLogs,
],
];
}
$fixturePath = $argv[1] ?? null;
if ($fixturePath === null || ($fixturePath !== '-' && !is_file($fixturePath))) {
fwrite(STDERR, "usage: php compare/battle_trace.php <fixture.json|->\n");
exit(2);
}
try {
$fixtureJson = $fixturePath === '-' ? stream_get_contents(STDIN) : file_get_contents($fixturePath);
$fixture = json_decode((string)$fixtureJson, true, flags: JSON_THROW_ON_ERROR);
echo json_encode(
comparisonRunBattle($fixture),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
} catch (\Throwable $throwable) {
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
exit(1);
}
@@ -0,0 +1,23 @@
{
"action": "AddGlobalBetray",
"args": [2, 1],
"setup": {
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2, 3],
"general": [
{ "id": 1, "values": { "name": "배신0", "betray": 0 } },
{ "id": 2, "values": { "name": "배신1", "betray": 1 } },
{ "id": 3, "values": { "name": "배신2", "betray": 2 } }
]
},
"environment": {
"year": 194,
"month": 1,
"startyear": 190
},
"observe": {
"generalIds": [1, 2, 3]
},
"compactOutput": true
}
@@ -0,0 +1,79 @@
{
"action": "AssignGeneralSpeciality",
"setup": {
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2, 3],
"general": [
{
"id": 1,
"values": {
"name": "내정대상",
"nation": 1,
"age": 30,
"leadership": 40,
"strength": 45,
"intel": 80,
"special": "None",
"specage": 30,
"special2": "che_신산",
"specage2": 99,
"aux": {
"prev_types_special": ["che_경작"]
}
}
},
{
"id": 2,
"values": {
"name": "전투대상",
"nation": 1,
"age": 30,
"leadership": 80,
"strength": 75,
"intel": 40,
"special": "che_인덕",
"specage": 99,
"special2": "None",
"specage2": 30,
"dex1": 200,
"dex2": 10,
"dex3": 10,
"dex4": 10,
"dex5": 10,
"aux": {
"prev_types_special2": ["che_돌격"]
}
}
},
{
"id": 3,
"values": {
"name": "계승대상",
"nation": 2,
"age": 30,
"leadership": 50,
"strength": 50,
"intel": 50,
"special": "che_경작",
"specage": 99,
"special2": "None",
"specage2": 30,
"aux": {
"inheritSpecificSpecialWar": "che_의술",
"marker": 3
}
}
}
]
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190
},
"observe": {
"generalIds": [1, 2, 3]
},
"compactOutput": true
}
@@ -0,0 +1,54 @@
{
"action": "AutoDeleteInvader",
"compactOutput": true,
"args": [1],
"setup": {
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"resetDiplomacy": true,
"nation": [
{
"id": 1,
"values": {
"name": "ⓞ남만족",
"capital": 69,
"gennum": 1,
"tech": 100,
"level": 2,
"type": "che_병가"
}
}
],
"general": [
{
"id": 1,
"values": {
"name": "ⓞ남만대왕",
"nation": 1,
"city": 69,
"officer_level": 12,
"npc": 9
}
}
],
"event": [
{
"id": 990001,
"action": [["AutoDeleteInvader", 1]]
}
]
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"currentEventID": 990001
},
"observe": {
"generalIds": [1]
}
}
@@ -0,0 +1,24 @@
{
"action": "BlockScoutAction",
"args": [true],
"setup": {
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"nation": [
{ "id": 1, "values": { "name": "위", "level": 2, "scout": 0 } },
{ "id": 2, "values": { "name": "촉", "level": 2, "scout": 0 } }
],
"gameEnvironment": {
"block_change_scout": false
}
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190
},
"observe": {
"nationIds": [1, 2]
}
}
@@ -0,0 +1,226 @@
{
"action": "MonthlyBoundary",
"compactOutput": true,
"environment": {
"year": 190,
"month": 12,
"startyear": 190,
"turnterm": 10,
"starttime": "2026-07-25 00:00:00",
"turntime": "2026-07-25 02:00:00"
},
"setup": {
"syncEnvironment": true,
"world": {
"hiddenSeed": "monthly-boundary-core-catalog"
},
"gameEnvironment": {
"isunited": 1,
"refreshLimit": 3,
"block_change_scout": false
},
"resetCities": true,
"deleteOtherCities": true,
"keepCityIds": [1, 2],
"city": [
{
"id": 1,
"values": {
"name": "경계도시",
"level": 1,
"nation": 1,
"pop": 10000,
"pop_max": 20000,
"agri": 1000,
"agri_max": 2000,
"comm": 1000,
"comm_max": 2000,
"secu": 1000,
"secu_max": 2000,
"trust": 50,
"trade": 100,
"def": 1000,
"def_max": 2000,
"wall": 1000,
"wall_max": 2000,
"officer_set": 1,
"state": 31,
"term": 1,
"supply": 1,
"front": 2,
"conflict": {},
"dead": 0
}
},
{
"id": 2,
"values": {
"name": "중립도시",
"level": 8,
"nation": 0,
"pop": 12000,
"pop_max": 22000,
"agri": 1200,
"agri_max": 2200,
"comm": 1200,
"comm_max": 2200,
"secu": 1200,
"secu_max": 2200,
"trust": 55,
"trade": 100,
"def": 1200,
"def_max": 2200,
"wall": 1200,
"wall_max": 2200,
"officer_set": 1,
"state": 0,
"term": 0,
"supply": 0,
"front": 0,
"conflict": {},
"dead": 0
}
}
],
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"general": [
{
"id": 1,
"values": {
"name": "경계군주",
"nation": 1,
"city": 1,
"officer_level": 12,
"npc": 0,
"age": 30,
"belong": 3,
"makelimit": 2,
"gold": 1000,
"rice": 1000,
"crew": 100,
"train": 80,
"atmos": 70,
"leadership": 50,
"strength": 50,
"intel": 50,
"experience": 0,
"dedication": 0,
"dex1": 0,
"dex2": 0,
"dex3": 0,
"dex4": 0,
"dex5": 0,
"personal": "None",
"special": "None",
"specage": 0,
"special2": "None",
"specage2": 0,
"betray": 0,
"bornyear": 160,
"deadyear": 250
}
},
{
"id": 2,
"values": {
"name": "경계재야",
"nation": 0,
"city": 2,
"officer_level": 0,
"npc": 0,
"age": 25,
"belong": 4,
"makelimit": 0,
"gold": 500,
"rice": 500,
"crew": 50,
"train": 60,
"atmos": 50,
"leadership": 40,
"strength": 40,
"intel": 40,
"experience": 0,
"dedication": 0,
"dex1": 0,
"dex2": 0,
"dex3": 0,
"dex4": 0,
"dex5": 0,
"personal": "None",
"special": "None",
"specage": 0,
"special2": "None",
"specage2": 0,
"betray": 0,
"bornyear": 165,
"deadyear": 250
}
}
],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"nation": [
{
"id": 1,
"values": {
"name": "경계국",
"color": "#123456",
"capital": 1,
"gennum": 1,
"gold": 10000,
"rice": 20000,
"tech": 100,
"power": 10,
"level": 2,
"type": "che_유가",
"bill": 100,
"rate": 20,
"rate_tmp": 10,
"scout": 1,
"chief_set": 1,
"strategic_cmd_limit": 2,
"surlimit": 1,
"spy": "{}"
}
}
],
"resetDiplomacy": true,
"resetEvents": true,
"event": [
{
"id": 991001,
"target": "PRE_MONTH",
"priority": 1000,
"condition": true,
"action": [
["NoticeToHistoryLog", "aggregate-pre", 6]
]
},
{
"id": 991002,
"target": "MONTH",
"priority": 1000,
"condition": true,
"action": [
["ProcessIncome", "gold"],
["NewYear"],
["ResetOfficerLock"],
["RandomizeCityTradeRate"],
["ChangeCity", "all", {"pop": "+10", "trust": 60}],
["NoticeToHistoryLog", "aggregate-month", 6],
["DeleteEvent"]
]
}
]
},
"observe": {
"generalIds": [1, 2],
"nationIds": [1],
"cityIds": [1, 2],
"eventIds": [991001, 991002],
"accessGeneralIds": [1, 2]
}
}
@@ -0,0 +1,34 @@
{
"action": "NewYear",
"environment": {
"year": 191,
"month": 1,
"startyear": 190
},
"setup": {
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"general": [
{
"id": 1,
"values": {
"nation": 1,
"age": 30,
"belong": 3
}
},
{
"id": 2,
"values": {
"nation": 0,
"age": 30,
"belong": 4
}
}
]
},
"observe": {
"generalIds": [1, 2]
}
}
@@ -0,0 +1,11 @@
{
"action": "NoticeToHistoryLog",
"args": ["<S>새해 알림</>", 6],
"environment": {
"year": 191,
"month": 1,
"startyear": 190
},
"setup": {},
"observe": {}
}
@@ -0,0 +1,75 @@
{
"action": "ProcessIncome",
"args": ["gold"],
"environment": {
"year": 190,
"month": 7,
"startyear": 190
},
"setup": {
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"general": [
{
"id": 1,
"values": {
"nation": 1,
"city": 1,
"officer_level": 5,
"npc": 0,
"gold": 1000,
"rice": 1000,
"dedication": 100
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "갑국",
"capital": 1,
"gold": 10000,
"rice": 20000,
"level": 1,
"rate_tmp": 20
}
}
],
"city": [
{
"id": 1,
"values": {
"name": "갑성",
"nation": 1,
"level": 4,
"supply": 1,
"pop": 10000,
"pop_max": 20000,
"agri": 1000,
"agri_max": 2000,
"comm": 1000,
"comm_max": 2000,
"secu": 1000,
"secu_max": 2000,
"trust": 80,
"def": 500,
"def_max": 1000,
"wall": 500,
"wall_max": 1000
}
}
]
},
"observe": {
"generalIds": [1],
"nationIds": [1],
"cityIds": [1],
"nationEnvironmentKeys": ["prev_income_gold", "prev_income_rice"]
}
}
@@ -0,0 +1,33 @@
{
"action": "ResetOfficerLock",
"environment": {
"year": 191,
"month": 1,
"startyear": 190
},
"setup": {
"resetCities": true,
"resetNations": true,
"city": [
{
"id": 1,
"values": {
"nation": 1,
"officer_set": 1
}
}
],
"nation": [
{
"id": 1,
"values": {
"chief_set": 1
}
}
]
},
"observe": {
"cityIds": [1],
"nationIds": [1]
}
}
@@ -0,0 +1,9 @@
{
"action": "CreateAdminNPC",
"environment": {
"year": 193,
"month": 12,
"startyear": 190
},
"observe": {}
}
@@ -0,0 +1,15 @@
{
"action": "CreateManyNPC",
"args": [2, 0],
"environment": {
"year": 193,
"month": 5,
"startyear": 190,
"turnterm": 10,
"turntime": "0193-05-01 00:00:00"
},
"setup": {
"resetGenerals": true
},
"observe": {}
}
@@ -0,0 +1,69 @@
{
"action": "FinishNationBetting",
"args": [1],
"setup": {
"clearBetting": true,
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"nation": [
{ "id": 1, "values": { "name": "위", "gennum": 1, "power": 100, "level": 2 } },
{ "id": 2, "values": { "name": "촉", "gennum": 1, "power": 300, "level": 0 } }
],
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"general": [
{ "id": 1, "values": { "name": "장수1", "nation": 1, "owner": 1, "npc": 0 } },
{ "id": 2, "values": { "name": "장수2", "nation": 2, "owner": 2, "npc": 0 } }
],
"bettingInfo": [
{
"id": 1,
"type": "bettingNation",
"name": "천통국 예상",
"finished": false,
"selectCnt": 1,
"isExclusive": null,
"reqInheritancePoint": true,
"openYearMonth": 2400,
"closeYearMonth": 2424,
"candidates": [
{
"title": "위",
"info": "국력: 100<br>장수 수: 1<br>도시 수: 1",
"isHtml": true,
"aux": { "nation": 1 }
},
{
"title": "촉",
"info": "국력: 300<br>장수 수: 1<br>도시 수: 1",
"isHtml": true,
"aux": { "nation": 2 }
}
],
"winner": null
}
],
"bet": [
{ "betting_id": 1, "general_id": 0, "user_id": null, "betting_type": "[-1]", "amount": 100 },
{ "betting_id": 1, "general_id": 1, "user_id": 1, "betting_type": "[0]", "amount": 100 },
{ "betting_id": 1, "general_id": 2, "user_id": 2, "betting_type": "[1]", "amount": 100 }
],
"inheritancePrevious": {
"1": 900,
"2": 900
},
"syncEnvironment": true
},
"environment": {
"year": 202,
"month": 1,
"startyear": 190
},
"observe": {
"generalIds": [1, 2],
"nationIds": [1, 2],
"ownerIds": [1, 2]
}
}
@@ -0,0 +1,40 @@
{
"action": "InvaderEnding",
"compactOutput": true,
"setup": {
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"setAllCitiesNation": 1,
"nation": [
{
"id": 1,
"values": {
"name": "기준국",
"capital": 1,
"gennum": 1,
"tech": 100,
"level": 2,
"type": "che_유가"
}
}
],
"gameEnvironment": {
"isunited": 1,
"refreshLimit": 3
},
"event": [
{
"id": 990002,
"action": [["InvaderEnding"]]
}
]
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"currentEventID": 990002
},
"observe": {}
}
@@ -0,0 +1,108 @@
{
"action": "InvaderLifecycle",
"args": [10, 150, 100, 20],
"setup": {
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"resetDiplomacy": true,
"resetEvents": true,
"city": [
{
"id": 1,
"values": {
"name": "기준도시",
"level": 3,
"nation": 1,
"pop": 1000,
"pop_max": 10000,
"agri": 2000,
"agri_max": 20000,
"comm": 3000,
"comm_max": 30000,
"secu": 4000,
"secu_max": 40000,
"def": 5000,
"def_max": 50000,
"wall": 6000,
"wall_max": 60000
}
},
{
"id": 48,
"values": {
"name": "남만",
"level": 4,
"nation": 0,
"pop": 1000,
"pop_max": 10000,
"agri": 2000,
"agri_max": 20000,
"comm": 3000,
"comm_max": 30000,
"secu": 4000,
"secu_max": 40000,
"def": 5000,
"def_max": 50000,
"wall": 6000,
"wall_max": 60000
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "기준국",
"capital": 1,
"gennum": 1,
"tech": 100,
"level": 2,
"type": "che_유가"
}
}
],
"general": [
{
"id": 1,
"values": {
"name": "군주",
"nation": 1,
"city": 1,
"officer_level": 12,
"npc": 0,
"leadership": 50,
"strength": 50,
"intel": 50,
"experience": 1000,
"dedication": 1000,
"dex1": 10,
"dex2": 20,
"dex3": 30,
"dex4": 40,
"dex5": 50,
"turntime": "0200-01-01 00:05:00"
}
}
],
"gameEnvironment": {
"isunited": 0,
"refreshLimit": 3
},
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {}
}
@@ -0,0 +1,42 @@
{
"action": "LostUniqueItem",
"args": [0.2],
"environment": {
"year": 210,
"month": 1,
"startyear": 180
},
"setup": {
"syncEnvironment": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"general": [
{
"id": 1,
"values": {
"name": "갑",
"npc": 0,
"horse": "che_명마_12_옥란백용구",
"weapon": "che_무기_15_의천검",
"book": "che_서적_01_효경전",
"item": "che_저지_삼황내문"
}
},
{
"id": 2,
"values": {
"name": "을",
"npc": 1,
"horse": "che_명마_12_사륜거",
"weapon": "che_무기_01_단도",
"book": "None",
"item": "che_부적_태현청생부"
}
}
]
},
"observe": {
"generalIds": [1, 2]
}
}
@@ -0,0 +1,74 @@
{
"action": "MergeInheritPointRank",
"environment": {
"year": 210,
"month": 1,
"startyear": 180
},
"setup": {
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"clearInheritanceOwnerIds": [100],
"gameEnvironment": {
"isunited": 0
},
"general": [
{
"id": 1,
"values": {
"name": "갑",
"nation": 1,
"owner": 100,
"npc": 0,
"belong": 3,
"dex1": 1000,
"dex2": 0,
"dex3": 0,
"dex4": 0,
"dex5": 0
}
}
],
"inheritancePoints": [
{"ownerId": 100, "key": "previous", "value": 999},
{"ownerId": 100, "key": "lived_month", "value": 2},
{"ownerId": 100, "key": "max_domestic_critical", "value": 4},
{"ownerId": 100, "key": "active_action", "value": 15},
{"ownerId": 100, "key": "unifier", "value": 6},
{"ownerId": 100, "key": "tournament", "value": 7}
],
"rankData": [
{"generalId": 1, "type": "warnum", "value": 2},
{"generalId": 1, "type": "firenum", "value": 1},
{"generalId": 1, "type": "betwin", "value": 2},
{"generalId": 1, "type": "betgold", "value": 4000},
{"generalId": 1, "type": "betwingold", "value": 2000},
{"generalId": 1, "type": "inherit_earned_dyn", "value": 999},
{"generalId": 1, "type": "inherit_earned_act", "value": 11},
{"generalId": 1, "type": "inherit_earned", "value": 999},
{"generalId": 1, "type": "inherit_spent_dyn", "value": 9},
{"generalId": 1, "type": "inherit_spent", "value": 999}
]
},
"observe": {
"generalIds": [1],
"ownerIds": [100],
"inheritanceKeys": [
"previous",
"lived_month",
"max_domestic_critical",
"active_action",
"unifier",
"tournament"
],
"rankGeneralIds": [1],
"rankTypes": [
"inherit_earned",
"inherit_earned_act",
"inherit_earned_dyn",
"inherit_spent",
"inherit_spent_dyn"
]
}
}
@@ -0,0 +1,75 @@
{
"action": "NationBettingLifecycle",
"args": [1, 100],
"bettorGeneralIds": [1, 2],
"bettorOwnerIds": [1, 2],
"setup": {
"clearBetting": true,
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"nation": [
{
"id": 1,
"values": {
"name": "위",
"color": "#111111",
"capital": 1,
"gennum": 1,
"gold": 1000,
"rice": 2000,
"tech": 100,
"power": 100,
"level": 2,
"type": "che_유가"
}
},
{
"id": 2,
"values": {
"name": "촉",
"color": "#222222",
"capital": 2,
"gennum": 1,
"gold": 3000,
"rice": 4000,
"tech": 200,
"power": 300,
"level": 2,
"type": "che_병가"
}
}
],
"resetCities": true,
"deleteOtherCities": true,
"keepCityIds": [1, 2],
"city": [
{ "id": 1, "values": { "name": "허창", "nation": 1, "level": 3 } },
{ "id": 2, "values": { "name": "성도", "nation": 2, "level": 3 } }
],
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"general": [
{ "id": 1, "values": { "name": "장수1", "nation": 1, "city": 1, "owner": 1, "npc": 0 } },
{ "id": 2, "values": { "name": "장수2", "nation": 2, "city": 2, "owner": 2, "npc": 0 } }
],
"inheritancePrevious": {
"1": 1000,
"2": 1000
},
"rankData": [
{ "generalId": 1, "type": "inherit_spent_dyn", "value": 0 },
{ "generalId": 1, "type": "inherit_earned_act", "value": 0 },
{ "generalId": 2, "type": "inherit_spent_dyn", "value": 0 },
{ "generalId": 2, "type": "inherit_earned_act", "value": 0 }
],
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190
},
"observe": {}
}
@@ -0,0 +1,77 @@
{
"action": "NpcNationLifecycle",
"setup": {
"resetCities": true,
"resetGenerals": true,
"resetNations": true,
"city": [
{
"id": 1,
"values": {
"name": "점유도시",
"level": 5,
"nation": 1,
"pop": 10001,
"pop_max": 50001,
"agri": 1001,
"agri_max": 5001,
"comm": 2001,
"comm_max": 6001,
"secu": 3001,
"secu_max": 7001,
"def": 4001,
"def_max": 8001,
"wall": 5001,
"wall_max": 9001
}
},
{
"id": 48,
"values": {
"name": "건국도시",
"level": 5,
"nation": 0,
"pop": 10048,
"pop_max": 50048,
"agri": 1048,
"agri_max": 5048,
"comm": 2048,
"comm_max": 6048,
"secu": 3048,
"secu_max": 7048,
"def": 4048,
"def_max": 8048,
"wall": 5048,
"wall_max": 9048
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "기준국",
"capital": 1,
"gennum": 0,
"tech": 120,
"level": 2,
"type": "che_유가"
}
}
],
"gameEnvironment": {
"lastNPCTroopLeaderID": 40
},
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {}
}
@@ -0,0 +1,65 @@
{
"action": "OpenNationBetting",
"args": [1, 500],
"setup": {
"clearBetting": true,
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"nation": [
{
"id": 1,
"values": {
"name": "위",
"color": "#111111",
"capital": 1,
"gennum": 1,
"gold": 1000,
"rice": 2000,
"tech": 100,
"power": 100,
"level": 2,
"type": "che_유가"
}
},
{
"id": 2,
"values": {
"name": "촉",
"color": "#222222",
"capital": 2,
"gennum": 1,
"gold": 3000,
"rice": 4000,
"tech": 200,
"power": 300,
"level": 2,
"type": "che_병가"
}
}
],
"resetCities": true,
"city": [
{ "id": 1, "values": { "nation": 1, "level": 3 } },
{ "id": 2, "values": { "nation": 2, "level": 3 } }
],
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"general": [
{ "id": 1, "values": { "name": "장수1", "nation": 1, "city": 1, "owner": 1, "npc": 0 } },
{ "id": 2, "values": { "name": "장수2", "nation": 2, "city": 2, "owner": 2, "npc": 0 } }
],
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190
},
"observe": {
"generalIds": [1, 2],
"cityIds": [1, 2],
"nationIds": [1, 2]
}
}
@@ -0,0 +1,178 @@
{
"action": "PostUpdateMonthly",
"environment": {
"year": 193,
"month": 2,
"startyear": 193,
"seedYear": 193,
"seedMonth": 1
},
"setup": {
"world": {
"hiddenSeed": "monthly-post-diplomacy-fixture"
},
"syncEnvironment": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2, 3, 4],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2, 3, 4],
"general": [
{
"id": 1,
"values": {
"name": "갑장",
"nation": 1,
"npc": 2,
"crew": 0,
"gold": 0,
"rice": 0,
"leadership": 10,
"strength": 10,
"intel": 10,
"experience": 0,
"dedication": 0
}
},
{
"id": 2,
"values": {
"name": "을장",
"nation": 2,
"npc": 2,
"crew": 0,
"gold": 0,
"rice": 0,
"leadership": 10,
"strength": 10,
"intel": 10,
"experience": 0,
"dedication": 0
}
},
{
"id": 3,
"values": {
"name": "병장",
"nation": 3,
"npc": 2,
"crew": 0,
"gold": 0,
"rice": 0,
"leadership": 10,
"strength": 10,
"intel": 10,
"experience": 0,
"dedication": 0
}
},
{
"id": 4,
"values": {
"name": "정장",
"nation": 4,
"npc": 2,
"crew": 0,
"gold": 0,
"rice": 0,
"leadership": 10,
"strength": 10,
"intel": 10,
"experience": 0,
"dedication": 0
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "갑국",
"gennum": 2,
"level": 1
}
},
{
"id": 2,
"values": {
"name": "을국",
"gennum": 1,
"level": 1
}
},
{
"id": 3,
"values": {
"name": "병국",
"gennum": 1,
"level": 1
}
},
{
"id": 4,
"values": {
"name": "정국",
"gennum": 1,
"level": 1
}
}
],
"resetDiplomacy": true,
"diplomacy": [
{
"fromNationId": 1,
"toNationId": 2,
"state": 1,
"term": 1,
"dead": 777
},
{
"fromNationId": 2,
"toNationId": 1,
"state": 1,
"term": 1,
"dead": 888
},
{
"fromNationId": 1,
"toNationId": 3,
"state": 0,
"term": 5,
"dead": 250
},
{
"fromNationId": 3,
"toNationId": 1,
"state": 0,
"term": 5,
"dead": 50
},
{
"fromNationId": 3,
"toNationId": 4,
"state": 0,
"term": 1,
"dead": 0
},
{
"fromNationId": 4,
"toNationId": 3,
"state": 0,
"term": 1,
"dead": 0
},
{
"fromNationId": 2,
"toNationId": 4,
"state": 7,
"term": 1,
"dead": 999
}
]
},
"observe": {
"generalIds": [1, 2, 3, 4],
"nationIds": [1, 2, 3, 4]
}
}
@@ -0,0 +1,162 @@
{
"action": "PostUpdateMonthly",
"environment": {
"year": 193,
"month": 2,
"startyear": 193,
"seedYear": 193,
"seedMonth": 1
},
"setup": {
"world": {
"hiddenSeed": "monthly-post-nation-stats-fixture"
},
"syncEnvironment": true,
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"resetDiplomacy": true,
"general": [
{
"id": 1,
"values": {
"nation": 1,
"npc": 0,
"gold": 1000,
"rice": 2000,
"crew": 100,
"leadership": 50,
"strength": 40,
"intel": 30,
"experience": 100,
"dedication": 200,
"dex1": 1000,
"dex2": 0,
"dex3": 0,
"dex4": 0,
"dex5": 0
}
},
{
"id": 2,
"values": {
"nation": 2,
"npc": 2,
"gold": 3000,
"rice": 4000,
"crew": 200,
"leadership": 60,
"strength": 50,
"intel": 40,
"experience": 300,
"dedication": 400,
"dex1": 2000,
"dex2": 1000,
"dex3": 0,
"dex4": 0,
"dex5": 0
}
}
],
"rankData": [
{"generalId": 1, "type": "killcrew_person", "value": 100},
{"generalId": 1, "type": "deathcrew_person", "value": 0},
{"generalId": 2, "type": "killcrew_person", "value": 0},
{"generalId": 2, "type": "deathcrew_person", "value": 100}
],
"nation": [
{
"id": 1,
"values": {
"name": "갑국",
"gold": 10000,
"rice": 20000,
"tech": 100,
"level": 2,
"gennum": 9,
"power": 7
}
},
{
"id": 2,
"values": {
"name": "을국",
"gold": 2000,
"rice": 3000,
"tech": 50,
"level": 1,
"gennum": 8,
"power": 8
}
}
],
"city": [
{
"id": 1,
"values": {
"name": "갑성",
"nation": 1,
"level": 4,
"supply": 1,
"pop": 1000,
"pop_max": 2000,
"agri": 100,
"agri_max": 200,
"comm": 100,
"comm_max": 200,
"secu": 100,
"secu_max": 200,
"def": 100,
"def_max": 200,
"wall": 100,
"wall_max": 200
}
},
{
"id": 2,
"values": {
"name": "을성",
"nation": 2,
"level": 4,
"supply": 1,
"pop": 2000,
"pop_max": 4000,
"agri": 200,
"agri_max": 400,
"comm": 200,
"comm_max": 400,
"secu": 200,
"secu_max": 400,
"def": 200,
"def_max": 400,
"wall": 200,
"wall_max": 400
}
}
],
"nationEnvironment": [
{
"nationId": 1,
"key": "max_power",
"value": {
"maxPower": 999,
"maxCrew": 50,
"maxCities": ["옛도시", "옛도시2"]
}
},
{"nationId": 1, "key": "available_war_setting_cnt", "value": 1}
]
},
"observe": {
"generalIds": [1, 2],
"nationIds": [1, 2],
"cityIds": [1, 2],
"rankGeneralIds": [1, 2],
"rankTypes": ["killcrew_person", "deathcrew_person"],
"nationEnvironmentKeys": ["max_power", "available_war_setting_cnt"]
}
}
@@ -0,0 +1,128 @@
{
"action": "PostUpdateMonthly",
"environment": {
"year": 193,
"month": 2,
"startyear": 193,
"seedYear": 193,
"seedMonth": 1
},
"setup": {
"world": {
"hiddenSeed": "monthly-post-tail-2"
},
"syncEnvironment": true,
"clearNeutralAuctions": true,
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"resetDiplomacy": true,
"gameEnvironment": {
"tournament": 0,
"tnmt_trig": true,
"tnmt_pattern": [0, 1, 2, 3],
"prev_winner": null
},
"general": [
{
"id": 1,
"values": {
"name": "갑장",
"nation": 1,
"city": 1,
"officer_level": 12,
"npc": 0,
"gold": 5000,
"rice": 7000,
"crew": 100,
"leadership": 50,
"strength": 40,
"intel": 30,
"experience": 100,
"dedication": 200
}
},
{
"id": 2,
"values": {
"name": "을장",
"nation": 2,
"city": 2,
"officer_level": 12,
"npc": 0,
"gold": 6000,
"rice": 8000,
"crew": 200,
"leadership": 60,
"strength": 50,
"intel": 40,
"experience": 300,
"dedication": 400
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "갑국",
"capital": 1,
"gold": 10000,
"rice": 20000,
"tech": 100,
"level": 2,
"gennum": 1,
"power": 7
}
},
{
"id": 2,
"values": {
"name": "을국",
"capital": 2,
"gold": 2000,
"rice": 3000,
"tech": 50,
"level": 1,
"gennum": 1,
"power": 8
}
}
],
"city": [
{
"id": 1,
"values": {
"name": "갑성",
"nation": 1,
"level": 4,
"supply": 1,
"front": 3,
"pop": 1000,
"pop_max": 2000
}
},
{
"id": 2,
"values": {
"name": "을성",
"nation": 2,
"level": 4,
"supply": 1,
"front": 3,
"pop": 2000,
"pop_max": 4000
}
}
]
},
"observe": {
"generalIds": [1, 2],
"nationIds": [1, 2],
"cityIds": [1, 2]
}
}
@@ -0,0 +1,143 @@
{
"action": "PostUpdateMonthly",
"environment": {
"year": 195,
"month": 2,
"startyear": 193,
"seedYear": 195,
"seedMonth": 1
},
"setup": {
"world": {
"hiddenSeed": "monthly-post-wander-fixture"
},
"syncEnvironment": true,
"resetCities": true,
"city": [
{
"id": 1,
"values": {
"name": "방랑성",
"nation": 4,
"level": 4,
"front": 1
}
},
{
"id": 2,
"values": {
"name": "존속성",
"nation": 1,
"level": 4,
"front": 1
}
}
],
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2, 3],
"general": [
{
"id": 1,
"values": {
"name": "방랑주",
"nation": 4,
"city": 1,
"officer_level": 12,
"npc": 2,
"gold": 2000,
"rice": 3000,
"crew": 100,
"makelimit": 0,
"belong": 7,
"leadership": 50,
"strength": 40,
"intel": 30,
"experience": 100,
"dedication": 200
}
},
{
"id": 2,
"values": {
"name": "방랑객",
"nation": 4,
"city": 1,
"officer_level": 1,
"npc": 0,
"gold": 1500,
"rice": 4000,
"crew": 200,
"makelimit": 0,
"belong": 5,
"leadership": 60,
"strength": 50,
"intel": 40,
"experience": 300,
"dedication": 400
}
},
{
"id": 3,
"values": {
"name": "존속장",
"nation": 1,
"city": 2,
"officer_level": 12,
"npc": 2,
"gold": 500,
"rice": 500,
"crew": 50,
"leadership": 50,
"strength": 50,
"intel": 50,
"experience": 0,
"dedication": 0
}
}
],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 4],
"nation": [
{
"id": 1,
"values": {
"name": "존속국",
"gennum": 1,
"level": 1
}
},
{
"id": 4,
"values": {
"name": "방랑국",
"gennum": 2,
"level": 0
}
}
],
"resetDiplomacy": true,
"diplomacy": [
{
"fromNationId": 1,
"toNationId": 4,
"state": 2,
"term": 0,
"dead": 0
},
{
"fromNationId": 4,
"toNationId": 1,
"state": 2,
"term": 0,
"dead": 0
}
]
},
"observe": {
"generalIds": [1, 2, 3],
"nationIds": [1, 4],
"cityIds": [1, 2]
}
}
@@ -0,0 +1,49 @@
{
"action": "PreUpdateMonthly",
"environment": {
"year": 200,
"month": 12,
"startyear": 190
},
"setup": {
"syncEnvironment": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"general": [
{"id": 1, "values": {"makelimit": 2}},
{"id": 2, "values": {"makelimit": 0}}
],
"generalAccessLog": [
{"generalId": 1, "refreshScoreTotal": 101},
{"generalId": 2, "refreshScoreTotal": 1}
],
"nation": [
{
"id": 1,
"values": {
"rate": 35,
"rate_tmp": 10,
"strategic_cmd_limit": 2,
"surlimit": 1,
"spy": "{\"1\":1,\"2\":2}"
}
}
],
"city": [
{"id": 1, "values": {"state": 31, "term": 1, "conflict": {"1": 10}}},
{"id": 2, "values": {"state": 32, "term": 2, "conflict": {"1": 20}}},
{"id": 3, "values": {"state": 33, "term": 0, "conflict": {"1": 30}}},
{"id": 4, "values": {"state": 34, "term": 3, "conflict": {"1": 40}}},
{"id": 5, "values": {"state": 41, "term": 1, "conflict": {"1": 50}}},
{"id": 6, "values": {"state": 42, "term": 2, "conflict": {"1": 60}}},
{"id": 7, "values": {"state": 43, "term": 3, "conflict": {"1": 70}}}
]
},
"observe": {
"generalIds": [1, 2],
"nationIds": [1],
"cityIds": [1, 2, 3, 4, 5, 6, 7],
"accessGeneralIds": [1, 2]
}
}
@@ -0,0 +1,76 @@
{
"action": "ProcessIncome",
"args": ["gold"],
"environment": {
"year": 190,
"month": 7,
"startyear": 190
},
"setup": {
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"general": [
{
"id": 1,
"values": {
"nation": 1,
"city": 1,
"officer_level": 5,
"npc": 0,
"gold": 1000,
"rice": 1000,
"dedication": 100
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "갑국",
"capital": 1,
"gold": 10000,
"rice": 20000,
"level": 1,
"rate": 35,
"rate_tmp": 10
}
}
],
"city": [
{
"id": 1,
"values": {
"name": "갑성",
"nation": 1,
"level": 4,
"supply": 1,
"pop": 10000,
"pop_max": 20000,
"agri": 1000,
"agri_max": 2000,
"comm": 1000,
"comm_max": 2000,
"secu": 1000,
"secu_max": 2000,
"trust": 80,
"def": 500,
"def_max": 1000,
"wall": 500,
"wall_max": 1000
}
}
]
},
"observe": {
"generalIds": [1],
"nationIds": [1],
"cityIds": [1],
"nationEnvironmentKeys": ["prev_income_gold", "prev_income_rice"]
}
}
@@ -0,0 +1,106 @@
{
"action": "ProcessSemiAnnual",
"resource": "gold",
"environment": {
"year": 193,
"month": 1,
"startyear": 190
},
"setup": {
"resetCities": true,
"resetGenerals": true,
"nation": [
{
"id": 1,
"values": {
"name": "반기검증국",
"gold": 100001,
"rice": 7777,
"rate_tmp": 20,
"type": "che_도가"
}
}
],
"city": [
{
"id": 1,
"values": {
"nation": 1,
"supply": 1,
"pop": 10000,
"pop_max": 50000,
"agri": 1001,
"agri_max": 5000,
"comm": 1001,
"comm_max": 5000,
"secu": 1001,
"secu_max": 2000,
"trust": 55,
"def": 1001,
"def_max": 5000,
"wall": 1001,
"wall_max": 5000,
"dead": 123
}
},
{
"id": 2,
"values": {
"nation": 1,
"supply": 0,
"pop": 10000,
"pop_max": 50000,
"agri": 1001,
"agri_max": 5000,
"comm": 1001,
"comm_max": 5000,
"secu": 1001,
"secu_max": 2000,
"trust": 55,
"def": 1001,
"def_max": 5000,
"wall": 1001,
"wall_max": 5000,
"dead": 123
}
},
{
"id": 3,
"values": {
"nation": 0,
"supply": 1,
"pop": 10000,
"pop_max": 50000,
"agri": 1001,
"agri_max": 5000,
"comm": 1001,
"comm_max": 5000,
"secu": 1001,
"secu_max": 2000,
"trust": 55,
"def": 1001,
"def_max": 5000,
"wall": 1001,
"wall_max": 5000,
"dead": 123
}
}
],
"general": [
{
"id": 1,
"values": {
"nation": 1,
"city": 1,
"gold": 10001,
"rice": 8888
}
}
]
},
"observe": {
"generalIds": [1],
"cityIds": [1, 2, 3],
"nationIds": [1]
}
}
@@ -0,0 +1,65 @@
{
"action": "ProcessSemiAnnual",
"resource": "gold",
"environment": {
"year": 193,
"month": 1,
"startyear": 190
},
"setup": {
"resetCities": true,
"resetGenerals": true,
"nation": [
{
"id": 1,
"values": {
"name": "반기검증국",
"gold": 100001,
"rice": 7777,
"rate": 50,
"rate_tmp": 20,
"type": "che_중립"
}
}
],
"city": [
{
"id": 1,
"values": {
"nation": 1,
"supply": 1,
"pop": 10000,
"pop_max": 50000,
"agri": 1001,
"agri_max": 5000,
"comm": 1001,
"comm_max": 5000,
"secu": 1001,
"secu_max": 2000,
"trust": 55,
"def": 1001,
"def_max": 5000,
"wall": 1001,
"wall_max": 5000,
"dead": 123
}
}
],
"general": [
{
"id": 1,
"values": {
"nation": 1,
"city": 1,
"gold": 10001,
"rice": 8888
}
}
]
},
"observe": {
"generalIds": [1],
"cityIds": [1],
"nationIds": [1]
}
}
@@ -0,0 +1,96 @@
{
"action": "ProcessWarIncome",
"environment": {
"year": 193,
"month": 1,
"startyear": 190
},
"setup": {
"resetCities": true,
"nation": [
{
"id": 1,
"values": {
"name": "전쟁수입국",
"gold": 1000,
"level": 1,
"type": "che_중립"
}
},
{
"id": 2,
"values": {
"name": "비활성국",
"gold": 2000,
"level": 0,
"type": "che_중립"
}
},
{
"id": 3,
"values": {
"name": "법가국",
"gold": 3000,
"level": 1,
"type": "che_법가"
}
}
],
"city": [
{
"id": 1,
"values": {
"nation": 1,
"supply": 1,
"pop": 1000,
"pop_max": 2000,
"dead": 101
}
},
{
"id": 2,
"values": {
"nation": 1,
"supply": 0,
"pop": 1000,
"pop_max": 2000,
"dead": 999
}
},
{
"id": 3,
"values": {
"nation": 2,
"supply": 1,
"pop": 1000,
"pop_max": 2000,
"dead": 1000
}
},
{
"id": 4,
"values": {
"nation": 3,
"supply": 1,
"pop": 1000,
"pop_max": 2000,
"dead": 105
}
},
{
"id": 5,
"values": {
"nation": 0,
"supply": 1,
"pop": 999,
"pop_max": 1000,
"dead": 10
}
}
]
},
"observe": {
"cityIds": [1, 2, 3, 4, 5],
"nationIds": [1, 2, 3]
}
}
@@ -0,0 +1,105 @@
{
"action": "RaiseInvader",
"compactOutput": true,
"args": [10, 150, 100, 20],
"setup": {
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"city": [
{
"id": 1,
"values": {
"level": 3,
"nation": 1,
"pop": 1000,
"pop_max": 10000,
"agri": 2000,
"agri_max": 20000,
"comm": 3000,
"comm_max": 30000,
"secu": 4000,
"secu_max": 40000,
"def": 5000,
"def_max": 50000,
"wall": 6000,
"wall_max": 60000
}
},
{
"id": 48,
"values": {
"level": 4,
"nation": 0,
"pop": 1000,
"pop_max": 10000,
"agri": 2000,
"agri_max": 20000,
"comm": 3000,
"comm_max": 30000,
"secu": 4000,
"secu_max": 40000,
"def": 5000,
"def_max": 50000,
"wall": 6000,
"wall_max": 60000
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "기준국",
"capital": 1,
"gennum": 1,
"tech": 100,
"level": 2,
"type": "che_유가"
}
}
],
"general": [
{
"id": 1,
"values": {
"name": "군주",
"nation": 1,
"city": 1,
"officer_level": 12,
"npc": 0,
"leadership": 50,
"strength": 50,
"intel": 50,
"experience": 1000,
"dedication": 1000,
"dex1": 10,
"dex2": 20,
"dex3": 30,
"dex4": 40,
"dex5": 50,
"turntime": "0200-01-01 00:05:00"
}
}
],
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {
"generalIds": [1],
"cityIds": [1, 48],
"nationIds": [1]
}
}
@@ -0,0 +1,77 @@
{
"action": "RaiseNPCNation",
"setup": {
"resetCities": true,
"resetGenerals": true,
"resetNations": true,
"city": [
{
"id": 1,
"values": {
"name": "점유도시",
"level": 5,
"nation": 1,
"pop": 10001,
"pop_max": 50001,
"agri": 1001,
"agri_max": 5001,
"comm": 2001,
"comm_max": 6001,
"secu": 3001,
"secu_max": 7001,
"def": 4001,
"def_max": 8001,
"wall": 5001,
"wall_max": 9001
}
},
{
"id": 48,
"values": {
"name": "건국도시",
"level": 5,
"nation": 0,
"pop": 10048,
"pop_max": 50048,
"agri": 1048,
"agri_max": 5048,
"comm": 2048,
"comm_max": 6048,
"secu": 3048,
"secu_max": 7048,
"def": 4048,
"def_max": 8048,
"wall": 5048,
"wall_max": 9048
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "기준국",
"capital": 1,
"gennum": 1,
"tech": 120,
"level": 2,
"type": "che_유가"
}
}
],
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {
"cityIds": [1, 48],
"nationIds": [1]
}
}
@@ -0,0 +1,35 @@
{
"action": "RegNeutralNPC",
"args": [
0,
"등록재야",
null,
0,
33,
45,
55,
65,
180,
245,
null,
"무쌍",
""
],
"setup": {
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {
"generalIds": [],
"cityIds": [33],
"nationIds": [1]
}
}
+36
View File
@@ -0,0 +1,36 @@
{
"action": "RegNPC",
"args": [
0,
"등록장수",
null,
0,
33,
60,
50,
40,
7,
186,
240,
null,
"인덕",
"등록 대사"
],
"setup": {
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {
"generalIds": [],
"cityIds": [33],
"nationIds": [1]
}
}
@@ -0,0 +1,24 @@
{
"action": "UnblockScoutAction",
"args": [false],
"setup": {
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"nation": [
{ "id": 1, "values": { "name": "위", "level": 2, "scout": 1 } },
{ "id": 2, "values": { "name": "촉", "level": 2, "scout": 1 } }
],
"gameEnvironment": {
"block_change_scout": true
}
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190
},
"observe": {
"nationIds": [1, 2]
}
}
@@ -0,0 +1,140 @@
{
"action": "UpdateCitySupply",
"environment": {
"year": 193,
"month": 1,
"startyear": 190
},
"setup": {
"nation": [
{
"id": 99,
"values": {
"name": "보급검증국",
"color": "#777777",
"capital": 1,
"level": 1,
"type": "che_중립"
}
},
{
"id": 98,
"values": {
"name": "불일치수도국",
"color": "#888888",
"capital": 2,
"level": 1,
"type": "che_중립"
}
}
],
"city": [
{
"id": 1,
"values": {
"nation": 99,
"supply": 0,
"front": 2,
"pop": 1001,
"agri": 501,
"comm": 499,
"secu": 99,
"trust": 50,
"def": 101,
"wall": 50,
"officer_set": 7,
"term": 2,
"conflict": {"98": 3}
}
},
{
"id": 9,
"values": {
"nation": 99,
"supply": 0,
"front": 2,
"pop": 1001,
"agri": 501,
"comm": 499,
"secu": 99,
"trust": 50,
"def": 101,
"wall": 50,
"officer_set": 7,
"term": 2,
"conflict": {"98": 3}
}
},
{
"id": 2,
"values": {
"nation": 99,
"supply": 1,
"front": 2,
"pop": 1001,
"agri": 501,
"comm": 499,
"secu": 99,
"trust": 33,
"def": 101,
"wall": 50,
"officer_set": 7,
"term": 2,
"conflict": {"98": 3}
}
},
{
"id": 3,
"values": {
"nation": 0,
"supply": 0,
"front": 0,
"trust": 50
}
}
],
"general": [
{
"id": 1,
"values": {
"nation": 99,
"city": 2,
"officer_level": 4,
"officer_city": 2,
"crew": 101,
"train": 51,
"atmos": 99
}
},
{
"id": 2,
"values": {
"nation": 99,
"city": 1,
"officer_level": 4,
"officer_city": 2,
"crew": 101,
"train": 51,
"atmos": 99
}
},
{
"id": 3,
"values": {
"nation": 98,
"city": 2,
"officer_level": 3,
"officer_city": 1,
"crew": 101,
"train": 51,
"atmos": 99
}
}
]
},
"observe": {
"generalIds": [1, 2, 3],
"cityIds": [1, 2, 3, 9],
"nationIds": [98, 99]
}
}
@@ -0,0 +1,85 @@
{
"action": "UpdateNationLevel",
"environment": {
"year": 193,
"month": 2,
"startyear": 190,
"killturn": 1000,
"turnterm": 10
},
"setup": {
"resetCities": true,
"resetGenerals": true,
"resetUniqueOccupancy": true,
"syncEnvironment": true,
"clearNationTurnIds": [1],
"clearInheritanceOwnerIds": [990061],
"nation": [
{
"id": 1,
"values": {
"name": "작위검증국",
"color": "#777777",
"capital": 1,
"gold": 10000,
"rice": 20000,
"level": 0,
"type": "che_중립",
"aux": {}
}
}
],
"city": [
{
"id": 1,
"values": {
"nation": 1,
"level": 4
}
},
{
"id": 2,
"values": {
"nation": 1,
"level": 4
}
}
],
"general": [
{
"id": 1,
"values": {
"name": "조조",
"nation": 1,
"city": 1,
"officer_level": 12,
"owner": 990061,
"npc": 0,
"killturn": 1000,
"belong": 10,
"horse": "None",
"weapon": "None",
"book": "None",
"item": "None",
"aux": {}
}
},
{
"id": 2,
"values": {
"nation": 0,
"city": 2,
"officer_level": 1,
"npc": 0,
"killturn": 0
}
}
]
},
"observe": {
"generalIds": [1],
"cityIds": [1, 2],
"nationIds": [1],
"ownerIds": [990061]
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
$input = stream_get_contents(STDIN);
$keys = Json::decode($input);
if (!is_array($keys)) {
fwrite(STDERR, "Expected a JSON array of item keys.\n");
exit(2);
}
$result = [];
foreach ($keys as $key) {
if (!is_string($key)) {
continue;
}
$item = buildItemClass($key);
$result[$key] = [
'rawName' => $item->getRawName(),
'name' => $item->getName(),
'info' => $item->getInfo(),
'cost' => $item->getCost(),
'buyable' => $item->isBuyable(),
'consumable' => $item->isConsumable(),
'reqSecu' => $item->getReqSecu(),
];
}
echo Json::encode($result);
+1579
View File
@@ -0,0 +1,1579 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli' || getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
error_reporting(E_ALL & ~E_DEPRECATED);
require_once 'lib.php';
require_once 'func.php';
require_once __DIR__ . '/turn_state_snapshot.php';
/** @param array<string, mixed> $values */
function comparisonMonthlyPatch(string $table, int $id, array $values): void
{
$definitions = [
'city' => [
'idColumn' => 'city',
'allowed' => [
'name', 'level', 'nation', 'supply', 'front', 'pop', 'pop_max',
'agri', 'agri_max', 'comm', 'comm_max', 'secu', 'secu_max',
'trust', 'trade', 'def', 'def_max', 'wall', 'wall_max',
'officer_set', 'state', 'region', 'term', 'conflict', 'dead',
],
],
'nation' => [
'idColumn' => 'nation',
'allowed' => [
'name', 'color', 'capital', 'gennum', 'gold', 'rice', 'tech',
'power', 'level', 'type', 'bill', 'rate_tmp', 'scout', 'chief_set', 'aux',
'rate', 'strategic_cmd_limit', 'surlimit', 'spy',
],
],
'general' => [
'idColumn' => 'no',
'allowed' => [
'name', 'nation', 'city', 'officer_level', 'officer_city',
'crew', 'train', 'atmos', 'owner', 'npc', 'killturn', 'belong',
'gold', 'rice', 'horse', 'weapon', 'book', 'item', 'aux',
'leadership', 'strength', 'intel', 'experience', 'dedication',
'injury',
'dex1', 'dex2', 'dex3', 'dex4', 'dex5', 'personal', 'special',
'specage', 'special2', 'specage2', 'betray', 'age',
'bornyear', 'deadyear', 'turntime', 'makelimit',
],
],
];
$definition = $definitions[$table] ?? null;
if ($definition === null) {
throw new \InvalidArgumentException("unsupported setup table: {$table}");
}
$patch = [];
foreach ($values as $key => $value) {
if (!is_string($key) || !in_array($key, $definition['allowed'], true)) {
throw new \InvalidArgumentException("unsupported {$table} setup field");
}
if (in_array($key, ['conflict', 'aux'], true) && is_array($value)) {
$value = json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
}
$patch[$key] = $value;
}
if ($table === 'nation') {
$patch += [
'nation' => $id,
'name' => "fixture-nation-{$id}",
'color' => '#777777',
'capital' => 0,
'gold' => 0,
'rice' => 0,
'level' => 1,
'type' => 'che_중립',
'aux' => '{}',
];
DB::db()->insertUpdate('nation', $patch, $patch);
} elseif ($patch !== []) {
DB::db()->update($table, $patch, "`{$definition['idColumn']}` = %i", $id);
}
}
/** @param array<string, mixed> $observe */
function comparisonMonthlyDetails(array $observe, int $worldHistoryAfterId): array
{
$db = DB::db();
$generals = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'no',
'nationId' => 'nation',
'cityId' => 'city',
'officerLevel' => 'officer_level',
'officerCityId' => 'officer_city',
'crew' => 'crew',
'train' => 'train',
'atmos' => 'atmos',
'gold' => 'gold',
'rice' => 'rice',
'injury' => 'injury',
'leadership' => 'leadership',
'strength' => 'strength',
'intelligence' => 'intel',
'age' => 'age',
'specialDomestic' => 'special',
'specAge' => 'specage',
'specialWar' => 'special2',
'specAge2' => 'specage2',
'betray' => 'betray',
'makeLimit' => 'makelimit',
'horse' => 'horse',
'weapon' => 'weapon',
'book' => 'book',
'item' => 'item',
'dex1' => 'dex1',
'dex2' => 'dex2',
'dex3' => 'dex3',
'dex4' => 'dex4',
'dex5' => 'dex5',
'aux' => 'aux',
],
['aux'],
),
comparisonRowsById('general', 'no', comparisonIntegerList($observe['generalIds'] ?? [], 'generalIds')),
);
$cities = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'city',
'nationId' => 'nation',
'state' => 'state',
'supplyState' => 'supply',
'frontState' => 'front',
'officerSet' => 'officer_set',
'term' => 'term',
'dead' => 'dead',
'population' => 'pop',
'populationMax' => 'pop_max',
'agriculture' => 'agri',
'agricultureMax' => 'agri_max',
'commerce' => 'comm',
'commerceMax' => 'comm_max',
'security' => 'secu',
'securityMax' => 'secu_max',
'trust' => 'trust',
'trade' => 'trade',
'defence' => 'def',
'defenceMax' => 'def_max',
'wall' => 'wall',
'wallMax' => 'wall_max',
'conflict' => 'conflict',
],
['conflict'],
),
comparisonRowsById('city', 'city', comparisonIntegerList($observe['cityIds'] ?? [], 'cityIds')),
);
$nations = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'nation',
'gold' => 'gold',
'rice' => 'rice',
'rate' => 'rate_tmp',
'baseRate' => 'rate',
'power' => 'power',
'generalCount' => 'gennum',
'tech' => 'tech',
'typeCode' => 'type',
'scout' => 'scout',
'strategicCommandLimit' => 'strategic_cmd_limit',
'surrenderLimit' => 'surlimit',
'spy' => 'spy',
'chiefSet' => 'chief_set',
],
['spy'],
),
comparisonRowsById('nation', 'nation', comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds')),
);
$worldHistory = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'id',
'nationId' => 'nation_id',
'year' => 'year',
'month' => 'month',
'text' => 'text',
],
),
$db->query(
'SELECT id, nation_id, year, month, text FROM world_history WHERE id > %i ORDER BY id',
$worldHistoryAfterId,
),
);
$inheritanceKeys = $observe['inheritanceKeys'] ?? ['unifier'];
if (!is_array($inheritanceKeys) || array_filter($inheritanceKeys, static fn($key): bool => !is_string($key))) {
throw new \InvalidArgumentException('inheritanceKeys must be an array of strings');
}
$inheritancePoints = [];
foreach (comparisonIntegerList($observe['ownerIds'] ?? [], 'ownerIds') as $ownerId) {
$entry = ['ownerId' => $ownerId];
foreach ($inheritanceKeys as $key) {
$value = $db->queryFirstField(
'SELECT value FROM storage WHERE namespace = %s AND `key` = %s',
"inheritance_{$ownerId}",
$key,
);
$entry[$key] = $value === null ? 0 : (comparisonJsonValue($value)[0] ?? 0);
}
$inheritancePoints[] = $entry;
}
$rankGeneralIds = comparisonIntegerList($observe['rankGeneralIds'] ?? [], 'rankGeneralIds');
$rankTypes = $observe['rankTypes'] ?? [];
if (!is_array($rankTypes) || array_filter($rankTypes, static fn($type): bool => !is_string($type))) {
throw new \InvalidArgumentException('rankTypes must be an array of strings');
}
$rankData = (!$rankGeneralIds || !$rankTypes)
? []
: iterator_to_array($db->query(
'SELECT general_id AS generalId, nation_id AS nationId, `type`, `value` FROM rank_data WHERE general_id IN %li AND `type` IN %ls ORDER BY general_id, `type`',
$rankGeneralIds,
$rankTypes,
));
$generalAccessLogs = [];
$accessGeneralIds = comparisonIntegerList($observe['accessGeneralIds'] ?? [], 'accessGeneralIds');
if ($accessGeneralIds) {
$generalAccessLogs = iterator_to_array($db->query(
'SELECT general_id AS generalId, refresh_score_total AS refreshScoreTotal FROM general_access_log WHERE general_id IN %li ORDER BY general_id',
$accessGeneralIds,
));
}
$nationEnvironment = [];
$nationEnvironmentKeys = $observe['nationEnvironmentKeys'] ?? [];
if (
!is_array($nationEnvironmentKeys)
|| array_filter($nationEnvironmentKeys, static fn($key): bool => !is_string($key))
) {
throw new \InvalidArgumentException('nationEnvironmentKeys must be an array of strings');
}
foreach (comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds') as $nationId) {
$entry = ['nationId' => $nationId];
$storage = KVStorage::getStorage($db, $nationId, 'nation_env');
foreach ($nationEnvironmentKeys as $key) {
$entry[$key] = $storage->getValue($key);
}
$nationEnvironment[] = $entry;
}
return [
'generals' => $generals,
'cities' => $cities,
'nations' => $nations,
'worldHistory' => $worldHistory,
'inheritancePoints' => $inheritancePoints,
'rankData' => $rankData,
'generalAccessLogs' => $generalAccessLogs,
'nationEnvironment' => $nationEnvironment,
];
}
/**
* Exercise the dynamically-created invader events against one evolving database.
*
* @param array<string, mixed> $request
* @param array<string, mixed> $environment
* @return array<string, mixed>
*/
function comparisonInvaderLifecycleTrace(
array $request,
array $environment,
int $generalIdBeforeAction,
int $nationIdBeforeAction,
int $eventIdBeforeAction,
int $worldHistoryIdBeforeAction,
): array {
$db = DB::db();
$args = $request['args'] ?? [];
if (!is_array($args) || count($args) > 4) {
throw new \InvalidArgumentException('InvaderLifecycle args must have at most four entries');
}
$year = $environment['year'];
$month = $environment['month'];
$startYear = $environment['startyear'];
$raiseEnvironment = [
'year' => $year,
'month' => $month,
'startyear' => $startYear,
'turnterm' => $environment['turnterm'],
'turntime' => $environment['turntime'],
'show_img_level' => $environment['show_img_level'] ?? 3,
'stored_icons' => $environment['stored_icons'] ?? [],
'icon_path' => $environment['icon_path'] ?? '.',
'fiction' => $environment['fiction'] ?? [0],
];
(new \sammo\Event\Action\RaiseInvader(...$args))->run($raiseEnvironment);
$invaderNationIds = array_map(
'intval',
$db->queryFirstColumn('SELECT nation FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction),
);
$existingNationId = (int)$db->queryFirstField(
'SELECT nation FROM nation WHERE nation <= %i ORDER BY nation LIMIT 1',
$nationIdBeforeAction,
);
$rulerIds = array_map(
'intval',
$db->queryFirstColumn(
'SELECT no FROM general WHERE nation IN %li AND officer_level = 12 ORDER BY nation',
$invaderNationIds,
),
);
if ($invaderNationIds === [] || $existingNationId <= 0 || count($rulerIds) !== count($invaderNationIds)) {
throw new \RuntimeException('InvaderLifecycle failed to create the expected entities');
}
$autoDeleteEvents = [];
$endingEventId = 0;
foreach ($db->query('SELECT id, action FROM event WHERE id > %i ORDER BY id', $eventIdBeforeAction) as $row) {
$action = comparisonJsonValue($row['action']);
$actionName = $action[0][0] ?? null;
if ($actionName === 'AutoDeleteInvader') {
$autoDeleteEvents[] = [
'eventId' => (int)$row['id'],
'nationId' => (int)($action[0][1] ?? 0),
];
} elseif ($actionName === 'InvaderEnding') {
$endingEventId = (int)$row['id'];
}
}
if (count($autoDeleteEvents) !== count($invaderNationIds) || $endingEventId <= 0) {
throw new \RuntimeException('InvaderLifecycle failed to create follow-up events');
}
$diplomacyRows = iterator_to_array($db->query(
'SELECT me, you, state, term FROM diplomacy WHERE (me IN %li AND you = %i) OR (me = %i AND you IN %li) ORDER BY me, you',
$invaderNationIds,
$existingNationId,
$existingNationId,
$invaderNationIds,
));
$diplomacyStates = array_values(array_unique(array_map(
static fn(array $row): string => "{$row['state']}:{$row['term']}",
$diplomacyRows,
)));
sort($diplomacyStates);
$generalCounts = array_map(
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
'SELECT count(*) FROM general WHERE nation = %i',
$nationId,
),
$invaderNationIds,
);
$diplomacyCounts = array_map(
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
'SELECT count(*) FROM diplomacy WHERE (me = %i AND you = %i) OR (me = %i AND you = %i)',
$nationId,
$existingNationId,
$existingNationId,
$nationId,
),
$invaderNationIds,
);
$gameStorage = KVStorage::getStorage($db, 'game_env');
$afterRaise = [
'createdNationCount' => count($invaderNationIds),
'createdGeneralCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM general WHERE no > %i',
$generalIdBeforeAction,
),
'followUpEventCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id > %i',
$eventIdBeforeAction,
),
'generalCountsPerNation' => array_values(array_unique($generalCounts)),
'diplomacyCountsPerNation' => array_values(array_unique($diplomacyCounts)),
'diplomacyStates' => $diplomacyStates,
'isunited' => (int)$gameStorage->isunited,
'blockChangeScout' => (bool)$gameStorage->block_change_scout,
];
$warResults = [];
foreach ($autoDeleteEvents as $autoDeleteEvent) {
$warResult = (new \sammo\Event\Action\AutoDeleteInvader($autoDeleteEvent['nationId']))->run([
'year' => $year,
'month' => $month + 1,
'startyear' => $startYear,
'currentEventID' => $autoDeleteEvent['eventId'],
]);
$warResults[] = $warResult[1] ?? null;
}
$atWar = [
'results' => array_values(array_unique($warResults)),
'autoDeleteEventCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id IN %li',
array_column($autoDeleteEvents, 'eventId'),
),
];
$db->update(
'diplomacy',
['state' => 2, 'term' => 0],
'me IN %li OR you IN %li',
$invaderNationIds,
$invaderNationIds,
);
$peaceResults = [];
foreach ($autoDeleteEvents as $autoDeleteEvent) {
$peaceResult = (new \sammo\Event\Action\AutoDeleteInvader($autoDeleteEvent['nationId']))->run([
'year' => $year,
'month' => $month + 2,
'startyear' => $startYear,
'currentEventID' => $autoDeleteEvent['eventId'],
]);
$peaceResults[] = $peaceResult[1] ?? null;
}
$atPeace = [
'results' => array_values(array_unique($peaceResults)),
'autoDeleteEventCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id IN %li',
array_column($autoDeleteEvents, 'eventId'),
),
'rulerWanderTurnCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM general_turn WHERE general_id IN %li AND action = %s',
$rulerIds,
'che_방랑',
),
'endingEventPresent' => (bool)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id = %i',
$endingEventId,
),
];
$invaderGeneralIds = array_map('intval', $db->queryFirstColumn(
'SELECT no FROM general WHERE nation IN %li',
$invaderNationIds,
));
if ($invaderGeneralIds !== []) {
$db->delete('general_turn', 'general_id IN %li', $invaderGeneralIds);
$db->delete('rank_data', 'general_id IN %li', $invaderGeneralIds);
$db->delete('troop', 'nation IN %li', $invaderNationIds);
$db->delete('general', 'no IN %li', $invaderGeneralIds);
}
$db->delete('nation_turn', 'nation_id IN %li', $invaderNationIds);
$db->delete('diplomacy', 'me IN %li OR you IN %li', $invaderNationIds, $invaderNationIds);
$db->delete('nation', 'nation IN %li', $invaderNationIds);
$db->update('city', ['nation' => $existingNationId], true);
$endingResult = (new \sammo\Event\Action\InvaderEnding())->run([
'year' => $year,
'month' => $month + 3,
'startyear' => $startYear,
'currentEventID' => $endingEventId,
]);
$gameStorage->resetCache();
$afterUserWin = [
'result' => $endingResult[1] ?? null,
'endingEventPresent' => (bool)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id = %i',
$endingEventId,
),
'isunited' => (int)$gameStorage->isunited,
'refreshLimit' => (int)$gameStorage->refreshLimit,
'logs' => array_values($db->queryFirstColumn(
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 AND year = %i AND month = %i ORDER BY id',
$worldHistoryIdBeforeAction,
$year,
$month + 3,
)),
];
return [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => 'InvaderLifecycle',
'phases' => [
'afterRaise' => $afterRaise,
'atWar' => $atWar,
'atPeace' => $atPeace,
'afterUserWin' => $afterUserWin,
],
];
}
/**
* Exercise NPC nation creation and the same-month troop leader follow-up.
*
* @param array<string, mixed> $environment
* @return array<string, mixed>
*/
function comparisonNpcNationLifecycleTrace(
array $environment,
int $generalIdBeforeAction,
int $nationIdBeforeAction,
int $worldHistoryIdBeforeAction,
): array {
$db = DB::db();
$actionEnvironment = [
'year' => $environment['year'],
'month' => $environment['month'],
'startyear' => $environment['startyear'],
'turnterm' => $environment['turnterm'],
'turntime' => $environment['turntime'],
'show_img_level' => $environment['show_img_level'] ?? 3,
'stored_icons' => $environment['stored_icons'] ?? [],
'icon_path' => $environment['icon_path'] ?? '.',
'fiction' => $environment['fiction'] ?? [0],
];
$gameStorage = KVStorage::getStorage($db, 'game_env');
$leaderCounterBefore = (int)($gameStorage->lastNPCTroopLeaderID ?? 0);
(new \sammo\Event\Action\RaiseNPCNation())->run($actionEnvironment);
$createdNationIds = array_map(
'intval',
$db->queryFirstColumn('SELECT nation FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction),
);
if ($createdNationIds === []) {
throw new \RuntimeException('NpcNationLifecycle failed to create a nation');
}
$createdNationGeneralCounts = array_map(
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
'SELECT count(*) FROM general WHERE nation = %i',
$nationId,
),
$createdNationIds,
);
$afterRaise = [
'createdNationCount' => count($createdNationIds),
'createdGeneralCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM general WHERE no > %i',
$generalIdBeforeAction,
),
'generalCountsPerCreatedNation' => array_values(array_unique($createdNationGeneralCounts)),
'historyLogs' => array_values($db->queryFirstColumn(
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id',
$worldHistoryIdBeforeAction,
)),
];
(new \sammo\Event\Action\ProvideNPCTroopLeader())->run($actionEnvironment);
$leaderIds = array_map(
'intval',
$db->queryFirstColumn('SELECT no FROM general WHERE no > %i AND npc = 5 ORDER BY no', $generalIdBeforeAction),
);
if ($leaderIds === []) {
throw new \RuntimeException('NpcNationLifecycle failed to create troop leaders');
}
$leaderNationCounts = array_map(
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
'SELECT count(*) FROM general WHERE nation = %i AND npc = 5',
$nationId,
),
array_map(
'intval',
$db->queryFirstColumn('SELECT nation FROM nation WHERE level > 0 ORDER BY nation'),
),
);
$gatherTurnCounts = array_map(
static fn(int $generalId): int => (int)DB::db()->queryFirstField(
'SELECT count(*) FROM general_turn WHERE general_id = %i AND action = %s',
$generalId,
'che_집합',
),
$leaderIds,
);
$gameStorage->resetCache();
$afterProvide = [
'createdLeaderCount' => count($leaderIds),
'leaderCountsPerActiveNation' => array_values(array_unique($leaderNationCounts)),
'troopCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM troop WHERE troop_leader IN %li',
$leaderIds,
),
'gatherTurnCounts' => array_values(array_unique($gatherTurnCounts)),
'leaderCounterDelta' => (int)$gameStorage->lastNPCTroopLeaderID - $leaderCounterBefore,
];
return [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => 'NpcNationLifecycle',
'phases' => [
'afterRaise' => $afterRaise,
'afterProvide' => $afterProvide,
],
];
}
/**
* Exercise nation betting from monthly open through real user bets and settlement.
*
* @param array<string, mixed> $request
* @param array<string, mixed> $environment
* @return array<string, mixed>
*/
function comparisonNationBettingLifecycleTrace(
array $request,
array $environment,
int $eventIdBeforeAction,
int $worldHistoryIdBeforeAction,
): array {
$db = DB::db();
$args = $request['args'] ?? [];
if (!is_array($args) || count($args) > 2) {
throw new \InvalidArgumentException('NationBettingLifecycle args must have at most two entries');
}
$bettorGeneralIds = comparisonIntegerList(
$request['bettorGeneralIds'] ?? [],
'bettorGeneralIds',
);
$bettorOwnerIds = comparisonIntegerList(
$request['bettorOwnerIds'] ?? [],
'bettorOwnerIds',
);
if (count($bettorGeneralIds) !== 2 || count($bettorOwnerIds) !== 2) {
throw new \InvalidArgumentException(
'NationBettingLifecycle requires exactly two bettorGeneralIds and bettorOwnerIds',
);
}
$year = $environment['year'];
$month = $environment['month'];
$startYear = $environment['startyear'];
(new \sammo\Event\Action\OpenNationBetting(...$args))->run([
'year' => $year,
'month' => $month,
'startyear' => $startYear,
]);
$gameStorage = KVStorage::getStorage($db, 'game_env');
$gameStorage->resetCache();
$bettingId = (int)$gameStorage->last_betting_id;
$betting = new Betting($bettingId);
$bettingInfo = $betting->getInfo();
$candidateNationIds = array_map(
static fn(\sammo\DTO\SelectItem $candidate): int => (int)($candidate->aux['nation'] ?? 0),
$bettingInfo->candidates,
);
if (count($candidateNationIds) !== 2) {
throw new \RuntimeException('NationBettingLifecycle requires exactly two candidates');
}
$finishEventCount = (int)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id > %i AND action LIKE %s',
$eventIdBeforeAction,
"%FinishNationBetting%{$bettingId}%",
);
$afterOpen = [
'bettingId' => $bettingId,
'candidateNationIds' => $candidateNationIds,
'systemBonus' => (int)($db->queryFirstField(
'SELECT amount FROM ng_betting WHERE betting_id = %i AND general_id = 0',
$bettingId,
) ?? 0),
'finishEventCount' => $finishEventCount,
'historyLogs' => array_values($db->queryFirstColumn(
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id',
$worldHistoryIdBeforeAction,
)),
];
$betting->bet($bettorGeneralIds[0], $bettorOwnerIds[0], [1], 100);
$betting->bet($bettorGeneralIds[1], $bettorOwnerIds[1], [0], 100);
$afterBets = [
'bets' => array_map(
static fn(array $row): array => [
'generalId' => (int)$row['general_id'],
'ownerId' => $row['user_id'] === null ? null : (int)$row['user_id'],
'selectionKey' => $row['betting_type'],
'amount' => (int)$row['amount'],
],
iterator_to_array($db->query(
'SELECT general_id, user_id, betting_type, amount FROM ng_betting WHERE betting_id = %i ORDER BY general_id',
$bettingId,
)),
),
'inheritancePrevious' => array_map(
static function (int $ownerId): int {
$value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous');
return is_array($value) ? (int)($value[0] ?? 0) : 0;
},
$bettorOwnerIds,
),
'spentRank' => array_map(
static fn(int $generalId): int => (int)(DB::db()->queryFirstField(
'SELECT value FROM rank_data WHERE general_id = %i AND type = %s',
$generalId,
\sammo\Enums\RankColumn::inherit_point_spent_dynamic->value,
) ?? 0),
$bettorGeneralIds,
),
];
$db->update('nation', ['level' => 0], 'nation = %i', $candidateNationIds[1]);
$finishResult = (new \sammo\Event\Action\FinishNationBetting($bettingId))->run([
'year' => $year,
'month' => $month + 1,
'startyear' => $startYear,
]);
$settledInfo = (new Betting($bettingId))->getInfo();
$afterFinish = [
'result' => $finishResult[1] ?? null,
'finished' => $settledInfo->finished,
'winner' => $settledInfo->winner,
'inheritancePrevious' => array_map(
static function (int $ownerId): int {
$value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous');
return is_array($value) ? (int)($value[0] ?? 0) : 0;
},
$bettorOwnerIds,
),
'earnedRank' => array_map(
static fn(int $generalId): int => (int)(DB::db()->queryFirstField(
'SELECT value FROM rank_data WHERE general_id = %i AND type = %s',
$generalId,
\sammo\Enums\RankColumn::inherit_point_earned_by_action->value,
) ?? 0),
$bettorGeneralIds,
),
'historyLogs' => array_values($db->queryFirstColumn(
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id',
$worldHistoryIdBeforeAction,
)),
];
return [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => 'NationBettingLifecycle',
'phases' => [
'afterOpen' => $afterOpen,
'afterBets' => $afterBets,
'afterFinish' => $afterFinish,
],
];
}
function comparisonMonthlyEventTraceMain(): void
{
try {
$input = stream_get_contents(STDIN);
$request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($request)) {
throw new \InvalidArgumentException('request must be an object');
}
$actionName = $request['action'] ?? null;
$supportedActions = [
'UpdateCitySupply',
'UpdateNationLevel',
'RaiseDisaster',
'ProcessSemiAnnual',
'ProcessWarIncome',
'CreateAdminNPC',
'CreateManyNPC',
'RegNPC',
'RegNeutralNPC',
'RaiseNPCNation',
'NpcNationLifecycle',
'RaiseInvader',
'InvaderLifecycle',
'AutoDeleteInvader',
'InvaderEnding',
'ChangeCity',
'ProvideNPCTroopLeader',
'NationBettingLifecycle',
'OpenNationBetting',
'FinishNationBetting',
'BlockScoutAction',
'UnblockScoutAction',
'AssignGeneralSpeciality',
'AddGlobalBetray',
'LostUniqueItem',
'MergeInheritPointRank',
'ProcessIncome',
'NoticeToHistoryLog',
'NewYear',
'ResetOfficerLock',
'PreUpdateMonthly',
'PostUpdateMonthly',
'MonthlyBoundary',
];
if (!in_array($actionName, $supportedActions, true)) {
throw new \InvalidArgumentException('unsupported monthly action');
}
$setup = $request['setup'] ?? [];
if (!is_array($setup)) {
throw new \InvalidArgumentException('setup must be an object');
}
foreach (['city', 'nation', 'general'] as $table) {
$rows = $setup[$table] ?? [];
if (!is_array($rows)) {
throw new \InvalidArgumentException("setup.{$table} must be an array");
}
foreach ($rows as $row) {
if (!is_array($row) || !is_int($row['id'] ?? null) || !is_array($row['values'] ?? null)) {
throw new \InvalidArgumentException("invalid setup.{$table} row");
}
comparisonMonthlyPatch($table, $row['id'], $row['values']);
}
}
if (($setup['resetCities'] ?? false) === true) {
DB::db()->query('UPDATE city SET nation = 0, level = 1');
foreach ($setup['city'] ?? [] as $row) {
comparisonMonthlyPatch('city', $row['id'], $row['values']);
}
}
if (($setup['deleteOtherCities'] ?? false) === true) {
$keepCityIds = comparisonIntegerList($setup['keepCityIds'] ?? [], 'keepCityIds');
if ($keepCityIds === []) {
throw new \InvalidArgumentException('deleteOtherCities requires keepCityIds');
}
DB::db()->query('DELETE FROM city WHERE city NOT IN %li', $keepCityIds);
}
if (($setup['resetGenerals'] ?? false) === true) {
DB::db()->query(
'UPDATE general SET nation = 0, officer_level = 1, npc = 2, killturn = 0, horse = %s, weapon = %s, book = %s, item = %s',
'None',
'None',
'None',
'None',
);
foreach ($setup['general'] ?? [] as $row) {
comparisonMonthlyPatch('general', $row['id'], $row['values']);
}
}
if (($setup['deleteOtherGenerals'] ?? false) === true) {
$keepGeneralIds = comparisonIntegerList($setup['keepGeneralIds'] ?? [], 'keepGeneralIds');
if (!$keepGeneralIds) {
throw new \InvalidArgumentException('deleteOtherGenerals requires keepGeneralIds');
}
DB::db()->query('DELETE FROM general_turn WHERE general_id NOT IN %li', $keepGeneralIds);
DB::db()->query('DELETE FROM rank_data WHERE general_id NOT IN %li', $keepGeneralIds);
DB::db()->query('DELETE FROM general WHERE no NOT IN %li', $keepGeneralIds);
}
if (($setup['resetNations'] ?? false) === true) {
DB::db()->query('UPDATE nation SET level = 0, gennum = 0, tech = 0');
foreach ($setup['nation'] ?? [] as $row) {
comparisonMonthlyPatch('nation', $row['id'], $row['values']);
}
}
if (($setup['deleteOtherNations'] ?? false) === true) {
$keepNationIds = comparisonIntegerList($setup['keepNationIds'] ?? [], 'keepNationIds');
if (!$keepNationIds) {
throw new \InvalidArgumentException('deleteOtherNations requires keepNationIds');
}
DB::db()->query(
'DELETE FROM diplomacy WHERE me NOT IN %li OR you NOT IN %li',
$keepNationIds,
$keepNationIds,
);
DB::db()->query('DELETE FROM nation_turn WHERE nation_id NOT IN %li', $keepNationIds);
DB::db()->query('DELETE FROM nation WHERE nation NOT IN %li', $keepNationIds);
}
if (($setup['resetUniqueOccupancy'] ?? false) === true) {
DB::db()->delete('ng_auction', '`type` = %s AND finished = 0', 'uniqueItem');
DB::db()->query('DELETE FROM storage WHERE namespace LIKE %s', 'ut\\_%');
}
if (($setup['clearNeutralAuctions'] ?? false) === true) {
DB::db()->delete(
'ng_auction',
'`type` IN %ls AND host_general_id = 0',
['buyRice', 'sellRice'],
);
}
foreach (comparisonIntegerList($setup['clearNationTurnIds'] ?? [], 'clearNationTurnIds') as $nationId) {
DB::db()->delete('nation_turn', 'nation_id = %i', $nationId);
}
foreach (comparisonIntegerList($setup['clearInheritanceOwnerIds'] ?? [], 'clearInheritanceOwnerIds') as $ownerId) {
DB::db()->delete('storage', 'namespace = %s', "inheritance_{$ownerId}");
}
if (($setup['clearBetting'] ?? false) === true) {
DB::db()->query('DELETE FROM ng_betting');
DB::db()->query('DELETE FROM storage WHERE namespace = %s', 'betting');
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
$gameStorage->setValue('last_betting_id', 0);
}
foreach ($setup['bettingInfo'] ?? [] as $rawBettingInfo) {
if (!is_array($rawBettingInfo) || !is_int($rawBettingInfo['id'] ?? null)) {
throw new \InvalidArgumentException('invalid setup.bettingInfo row');
}
KVStorage::getStorage(DB::db(), 'betting')->setValue(
"id_{$rawBettingInfo['id']}",
$rawBettingInfo,
);
}
foreach ($setup['bet'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['betting_id'] ?? null)
|| !is_int($row['general_id'] ?? null)
|| !is_string($row['betting_type'] ?? null)
|| !is_int($row['amount'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.bet row');
}
DB::db()->insert('ng_betting', [
'betting_id' => $row['betting_id'],
'general_id' => $row['general_id'],
'user_id' => $row['user_id'] ?? null,
'betting_type' => $row['betting_type'],
'amount' => $row['amount'],
]);
}
foreach ($setup['inheritancePrevious'] ?? [] as $ownerId => $value) {
if (!is_numeric($ownerId) || !is_int($value)) {
throw new \InvalidArgumentException('invalid setup.inheritancePrevious entry');
}
KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->setValue('previous', [$value, null]);
}
foreach ($setup['inheritancePoints'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['ownerId'] ?? null)
|| !is_string($row['key'] ?? null)
|| !is_numeric($row['value'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.inheritancePoints row');
}
KVStorage::getStorage(DB::db(), "inheritance_{$row['ownerId']}")->setValue(
$row['key'],
[$row['value'], null],
);
}
foreach ($setup['rankData'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['generalId'] ?? null)
|| !is_string($row['type'] ?? null)
|| !is_int($row['value'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.rankData row');
}
$nationId = (int)(DB::db()->queryFirstField(
'SELECT nation FROM general WHERE no = %i',
$row['generalId'],
) ?? 0);
DB::db()->insertUpdate('rank_data', [
'general_id' => $row['generalId'],
'nation_id' => $nationId,
'type' => $row['type'],
'value' => $row['value'],
], [
'nation_id' => $nationId,
'value' => $row['value'],
]);
}
foreach ($setup['generalAccessLog'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['generalId'] ?? null)
|| !is_int($row['refreshScoreTotal'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.generalAccessLog row');
}
DB::db()->insertUpdate('general_access_log', [
'general_id' => $row['generalId'],
'refresh_score_total' => $row['refreshScoreTotal'],
], [
'refresh_score_total' => $row['refreshScoreTotal'],
]);
}
foreach ($setup['nationEnvironment'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['nationId'] ?? null)
|| !is_string($row['key'] ?? null)
|| !array_key_exists('value', $row)
) {
throw new \InvalidArgumentException('invalid setup.nationEnvironment row');
}
KVStorage::getStorage(DB::db(), $row['nationId'], 'nation_env')->setValue(
$row['key'],
$row['value'],
);
}
if (($setup['resetDiplomacy'] ?? false) === true) {
DB::db()->query('DELETE FROM diplomacy');
}
foreach ($setup['diplomacy'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['fromNationId'] ?? null)
|| !is_int($row['toNationId'] ?? null)
|| !is_int($row['state'] ?? null)
|| !is_int($row['term'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.diplomacy row');
}
$diplomacyPatch = [
'me' => $row['fromNationId'],
'you' => $row['toNationId'],
'state' => $row['state'],
'term' => $row['term'],
'dead' => $row['dead'] ?? 0,
];
DB::db()->insertUpdate('diplomacy', $diplomacyPatch, $diplomacyPatch);
}
if (($setup['resetEvents'] ?? false) === true) {
DB::db()->query('DELETE FROM event');
}
foreach ($setup['event'] ?? [] as $row) {
if (!is_array($row) || !is_int($row['id'] ?? null) || !is_array($row['action'] ?? null)) {
throw new \InvalidArgumentException('invalid setup.event row');
}
$eventPatch = [
'id' => $row['id'],
'target' => $row['target'] ?? 'month',
'priority' => $row['priority'] ?? 1000,
'condition' => Json::encode($row['condition'] ?? true),
'action' => Json::encode($row['action']),
];
DB::db()->insertUpdate('event', $eventPatch, $eventPatch);
}
$gameEnvironmentPatch = $setup['gameEnvironment'] ?? [];
if (!is_array($gameEnvironmentPatch)) {
throw new \InvalidArgumentException('setup.gameEnvironment must be an object');
}
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
$worldPatch = $setup['world'] ?? [];
if (!is_array($worldPatch)) {
throw new \InvalidArgumentException('setup.world must be an object');
}
if (array_key_exists('hiddenSeed', $worldPatch)) {
if (!is_string($worldPatch['hiddenSeed']) || $worldPatch['hiddenSeed'] === '') {
throw new \InvalidArgumentException('setup.world.hiddenSeed must be a non-empty string');
}
UniqueConst::$hiddenSeed = $worldPatch['hiddenSeed'];
}
foreach ([
'isunited',
'refreshLimit',
'block_change_scout',
'lastNPCTroopLeaderID',
'tournament',
'tnmt_trig',
'tnmt_pattern',
'prev_winner',
] as $key) {
if (array_key_exists($key, $gameEnvironmentPatch)) {
$gameStorage->setValue($key, $gameEnvironmentPatch[$key]);
}
}
if (($setup['setAllCitiesNation'] ?? null) !== null) {
if (!is_int($setup['setAllCitiesNation'])) {
throw new \InvalidArgumentException('setup.setAllCitiesNation must be an integer');
}
DB::db()->update('city', ['nation' => $setup['setAllCitiesNation']], true);
}
$db = DB::db();
$generalIdBeforeAction =
(int)($db->queryFirstField('SELECT COALESCE(MAX(no), 0) FROM general') ?? 0);
$nationIdBeforeAction =
(int)($db->queryFirstField('SELECT COALESCE(MAX(nation), 0) FROM nation') ?? 0);
$eventIdBeforeAction =
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM event') ?? 0);
$auctionIdBeforeAction =
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM ng_auction') ?? 0);
$snapshotRequest = ['observe' => $request['observe'] ?? []];
$snapshotRequest['observe']['logAfterId'] =
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM general_record') ?? 0);
$worldHistoryAfterId =
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM world_history') ?? 0);
$before = comparisonTurnStateSnapshot($snapshotRequest);
$beforeDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);
$environment = $request['environment'] ?? [];
if (!is_array($environment)) {
throw new \InvalidArgumentException('environment must be an object');
}
$year = $environment['year'] ?? null;
$month = $environment['month'] ?? null;
$startYear = $environment['startyear'] ?? null;
if (!is_int($year) || !is_int($month) || !is_int($startYear)) {
throw new \InvalidArgumentException('environment year, month, and startyear must be integers');
}
if (($setup['syncEnvironment'] ?? false) === true) {
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
$environmentValues = [
'year' => $year,
'month' => $month,
'startyear' => $startYear,
'killturn' => $environment['killturn'] ?? $gameStorage->killturn,
'turnterm' => $environment['turnterm'] ?? $gameStorage->turnterm,
];
if (isset($environment['starttime'])) {
if (!is_string($environment['starttime']) || $environment['starttime'] === '') {
throw new \InvalidArgumentException('environment.starttime must be a non-empty string');
}
$environmentValues['starttime'] = $environment['starttime'];
}
foreach ($environmentValues as $key => $value) {
$gameStorage->setValue($key, $value);
}
if ($actionName === 'MonthlyBoundary') {
$before = comparisonTurnStateSnapshot($snapshotRequest);
$beforeDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);
}
}
if ($actionName === 'InvaderLifecycle') {
foreach (['turnterm', 'turntime'] as $requiredEnvironmentKey) {
if (!array_key_exists($requiredEnvironmentKey, $environment)) {
throw new \InvalidArgumentException(
"InvaderLifecycle requires environment.{$requiredEnvironmentKey}",
);
}
}
echo json_encode(
comparisonInvaderLifecycleTrace(
$request,
$environment,
$generalIdBeforeAction,
$nationIdBeforeAction,
$eventIdBeforeAction,
$worldHistoryAfterId,
),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
return;
}
if ($actionName === 'NpcNationLifecycle') {
foreach (['turnterm', 'turntime'] as $requiredEnvironmentKey) {
if (!array_key_exists($requiredEnvironmentKey, $environment)) {
throw new \InvalidArgumentException(
"NpcNationLifecycle requires environment.{$requiredEnvironmentKey}",
);
}
}
echo json_encode(
comparisonNpcNationLifecycleTrace(
$environment,
$generalIdBeforeAction,
$nationIdBeforeAction,
$worldHistoryAfterId,
),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
return;
}
if ($actionName === 'NationBettingLifecycle') {
echo json_encode(
comparisonNationBettingLifecycleTrace(
$request,
$environment,
$eventIdBeforeAction,
$worldHistoryAfterId,
),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
return;
}
$actionClass = "\\sammo\\Event\\Action\\{$actionName}";
if (in_array($actionName, ['PreUpdateMonthly', 'PostUpdateMonthly', 'MonthlyBoundary'], true)) {
$action = null;
} elseif ($actionName === 'ProcessSemiAnnual') {
$resource = $request['resource'] ?? null;
if (!is_string($resource)) {
throw new \InvalidArgumentException('ProcessSemiAnnual requires resource');
}
$action = new $actionClass($resource);
} elseif (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseInvader', 'AutoDeleteInvader', 'ChangeCity', 'OpenNationBetting', 'FinishNationBetting', 'BlockScoutAction', 'UnblockScoutAction', 'AddGlobalBetray', 'LostUniqueItem', 'ProcessIncome', 'NoticeToHistoryLog'], true)) {
$args = $request['args'] ?? [];
if (!is_array($args)) {
throw new \InvalidArgumentException("{$actionName} args must be an array");
}
if ($actionName === 'CreateManyNPC' && count($args) > 2) {
throw new \InvalidArgumentException('CreateManyNPC args must have at most two entries');
}
if ($actionName === 'RaiseInvader' && count($args) > 4) {
throw new \InvalidArgumentException('RaiseInvader args must have at most four entries');
}
if ($actionName === 'ChangeCity' && count($args) !== 2) {
throw new \InvalidArgumentException('ChangeCity args must have exactly two entries');
}
if ($actionName === 'OpenNationBetting' && count($args) > 2) {
throw new \InvalidArgumentException('OpenNationBetting args must have at most two entries');
}
if ($actionName === 'FinishNationBetting' && count($args) !== 1) {
throw new \InvalidArgumentException('FinishNationBetting args must have exactly one entry');
}
if (in_array($actionName, ['BlockScoutAction', 'UnblockScoutAction'], true) && count($args) > 1) {
throw new \InvalidArgumentException("{$actionName} args must have at most one entry");
}
if ($actionName === 'AddGlobalBetray' && count($args) > 2) {
throw new \InvalidArgumentException('AddGlobalBetray args must have at most two entries');
}
if ($actionName === 'LostUniqueItem' && count($args) > 1) {
throw new \InvalidArgumentException('LostUniqueItem args must have at most one entry');
}
if ($actionName === 'ProcessIncome' && count($args) !== 1) {
throw new \InvalidArgumentException('ProcessIncome args must have exactly one entry');
}
if ($actionName === 'NoticeToHistoryLog' && (count($args) < 1 || count($args) > 2)) {
throw new \InvalidArgumentException('NoticeToHistoryLog args must have one or two entries');
}
$action = new $actionClass(...$args);
} else {
$action = new $actionClass();
}
$actionEnvironment = [
'year' => $year,
'month' => $month,
'startyear' => $startYear,
];
if (isset($environment['currentEventID'])) {
if (!is_int($environment['currentEventID'])) {
throw new \InvalidArgumentException('currentEventID must be an integer');
}
$actionEnvironment['currentEventID'] = $environment['currentEventID'];
}
if ($actionName === 'UpdateNationLevel') {
$killturn = $environment['killturn'] ?? null;
$turnterm = $environment['turnterm'] ?? null;
if (!is_int($killturn) || !is_int($turnterm) || $turnterm <= 0) {
throw new \InvalidArgumentException('UpdateNationLevel requires integer killturn and positive turnterm');
}
$actionEnvironment['killturn'] = $killturn;
$actionEnvironment['turnterm'] = $turnterm;
}
if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseNPCNation', 'RaiseInvader', 'ProvideNPCTroopLeader'], true)) {
$turnterm = $environment['turnterm'] ?? null;
$turntime = $environment['turntime'] ?? null;
$showImgLevel = $environment['show_img_level'] ?? 3;
$fiction = $environment['fiction'] ?? [0];
if (
!is_int($turnterm)
|| $turnterm <= 0
|| !is_string($turntime)
|| !is_int($showImgLevel)
|| !is_array($fiction)
) {
throw new \InvalidArgumentException(
"{$actionName} requires positive turnterm, string turntime, integer show_img_level, and array fiction",
);
}
$actionEnvironment += [
'turnterm' => $turnterm,
'turntime' => $turntime,
'show_img_level' => $showImgLevel,
'stored_icons' => $environment['stored_icons'] ?? [],
'icon_path' => $environment['icon_path'] ?? '.',
'fiction' => $fiction,
];
}
if ($actionName === 'PreUpdateMonthly') {
if (!preUpdateMonthly()) {
throw new \RuntimeException('preUpdateMonthly failed');
}
} elseif ($actionName === 'PostUpdateMonthly') {
$seedYear = $environment['seedYear'] ?? null;
$seedMonth = $environment['seedMonth'] ?? null;
if (!is_int($seedYear) || !is_int($seedMonth)) {
throw new \InvalidArgumentException('PostUpdateMonthly requires seedYear and seedMonth');
}
postUpdateMonthly(new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'monthly',
$seedYear,
$seedMonth,
))));
} elseif ($actionName === 'MonthlyBoundary') {
$turntime = $environment['turntime'] ?? null;
if (!is_string($turntime) || $turntime === '') {
throw new \InvalidArgumentException('MonthlyBoundary requires turntime');
}
$gameStorage = KVStorage::getStorage($db, 'game_env');
$seedYear = (int)$gameStorage->year;
$seedMonth = (int)$gameStorage->month;
TurnExecutionHelper::runEventHandler($db, $gameStorage, Enums\EventTarget::PreMonth);
if (!preUpdateMonthly()) {
throw new \RuntimeException('preUpdateMonthly failed');
}
turnDate($turntime);
$gameStorage->resetCache();
if ((int)$gameStorage->month === 1) {
checkStatistic();
}
TurnExecutionHelper::runEventHandler($db, $gameStorage, Enums\EventTarget::Month);
postUpdateMonthly(new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'monthly',
$seedYear,
$seedMonth,
))));
} else {
$action->run($actionEnvironment);
}
unset($action);
gc_collect_cycles();
$after = comparisonTurnStateSnapshot($snapshotRequest);
$afterDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);
$createdGenerals = [];
if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseNPCNation', 'RaiseInvader', 'ProvideNPCTroopLeader'], true)) {
foreach ($db->query('SELECT * FROM general WHERE no > %i ORDER BY no', $generalIdBeforeAction) as $row) {
$generalId = (int)$row['no'];
$turnRows = $db->query(
'SELECT turn_idx, action, arg FROM general_turn WHERE general_id = %i ORDER BY turn_idx',
$generalId,
);
$createdGenerals[] = comparisonPickRow(
$row,
[
'id' => 'no',
'name' => 'name',
'nationId' => 'nation',
'cityId' => 'city',
'leadership' => 'leadership',
'strength' => 'strength',
'intelligence' => 'intel',
'experience' => 'experience',
'dedication' => 'dedication',
'officerLevel' => 'officer_level',
'gold' => 'gold',
'rice' => 'rice',
'crew' => 'crew',
'crewTypeId' => 'crewtype',
'train' => 'train',
'atmos' => 'atmos',
'turnTime' => 'turntime',
'killturn' => 'killturn',
'age' => 'age',
'npcState' => 'npc',
'npcOriginalState' => 'npc_org',
'affinity' => 'affinity',
'personality' => 'personal',
'specialDomestic' => 'special',
'specialWar' => 'special2',
'specAge' => 'specage',
'specAge2' => 'specage2',
'bornYear' => 'bornyear',
'deadYear' => 'deadyear',
'picture' => 'picture',
'dex1' => 'dex1',
'dex2' => 'dex2',
'dex3' => 'dex3',
'dex4' => 'dex4',
'dex5' => 'dex5',
],
) + [
'turnCount' => count($turnRows),
'turnActions' => array_values(array_unique(array_column($turnRows, 'action'))),
'rankCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM rank_data WHERE general_id = %i',
$generalId,
),
'nonZeroRankCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM rank_data WHERE general_id = %i AND value != 0',
$generalId,
),
'troop' => $db->queryFirstRow(
'SELECT troop_leader AS id, nation AS nationId, name FROM troop WHERE troop_leader = %i',
$generalId,
) ?: null,
];
}
}
$createdNations = [];
if (in_array($actionName, ['RaiseNPCNation', 'RaiseInvader'], true)) {
foreach ($db->query('SELECT * FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction) as $row) {
$nationId = (int)$row['nation'];
$createdNations[] = comparisonPickRow(
$row,
[
'id' => 'nation',
'name' => 'name',
'color' => 'color',
'capitalCityId' => 'capital',
'generalCount' => 'gennum',
'gold' => 'gold',
'rice' => 'rice',
'tech' => 'tech',
'level' => 'level',
'typeCode' => 'type',
'bill' => 'bill',
'rate' => 'rate',
'scout' => 'scout',
'war' => 'war',
'strategicCommandLimit' => 'strategic_cmd_limit',
'surrenderLimit' => 'surlimit',
'aux' => 'aux',
],
['aux'],
) + [
'nationTurnCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM nation_turn WHERE nation_id = %i',
$nationId,
),
'diplomacyCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM diplomacy WHERE me = %i OR you = %i',
$nationId,
$nationId,
),
];
}
}
$response = [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => $actionName,
'before' => $before,
'beforeDetails' => $beforeDetails,
'after' => $after,
'afterDetails' => $afterDetails,
];
if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC'], true)) {
$response['createdGenerals'] = $createdGenerals;
}
if (in_array($actionName, ['RaiseNPCNation', 'RaiseInvader'], true)) {
$response['createdGenerals'] = $createdGenerals;
$response['createdNations'] = $createdNations;
}
if ($actionName === 'ProvideNPCTroopLeader') {
$response['createdGenerals'] = $createdGenerals;
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
'lastNPCTroopLeaderID',
]);
}
if ($actionName === 'PreUpdateMonthly') {
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
'develcost',
]);
}
if ($actionName === 'PostUpdateMonthly') {
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
'tournament',
'tnmt_trig',
'tnmt_pattern',
'tnmt_type',
'tnmt_auto',
'phase',
]);
$response['createdNeutralAuctions'] = array_map(
static fn(array $row): array => [
'id' => (int)$row['id'],
'type' => $row['type'],
'target' => $row['target'],
'detail' => Json::decode($row['detail']),
],
iterator_to_array(
$db->query(
'SELECT id, `type`, target, detail FROM ng_auction WHERE id > %i AND host_general_id = 0 ORDER BY id',
$auctionIdBeforeAction,
),
),
);
}
if (in_array($actionName, ['OpenNationBetting', 'FinishNationBetting'], true)) {
$bettingStorage = KVStorage::getStorage(DB::db(), 'betting');
$response['betting'] = $bettingStorage->getAll();
$response['bets'] = iterator_to_array(
$db->query(
'SELECT betting_id AS bettingId, general_id AS generalId, user_id AS userId, betting_type AS selectionKey, amount FROM ng_betting ORDER BY id',
),
);
$response['createdEvents'] = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'id',
'targetCode' => 'target',
'priority' => 'priority',
'condition' => 'condition',
'action' => 'action',
],
['condition', 'action'],
),
iterator_to_array(
$db->query(
'SELECT id, target, priority, `condition`, action FROM event WHERE id > %i ORDER BY id',
$eventIdBeforeAction,
),
),
);
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
'last_betting_id',
]);
$response['inheritancePrevious'] = [];
foreach (comparisonIntegerList($request['observe']['ownerIds'] ?? [], 'ownerIds') as $ownerId) {
$value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous');
$response['inheritancePrevious'][(string)$ownerId] = is_array($value) ? ($value[0] ?? 0) : 0;
}
}
if (in_array($actionName, ['BlockScoutAction', 'UnblockScoutAction'], true)) {
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
'block_change_scout',
]);
}
if ($actionName === 'RaiseInvader') {
$response['createdEvents'] = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'id',
'targetCode' => 'target',
'priority' => 'priority',
'condition' => 'condition',
'action' => 'action',
],
['condition', 'action'],
),
iterator_to_array(
$db->query(
'SELECT id, target, priority, `condition`, action FROM event WHERE id > %i ORDER BY id',
$eventIdBeforeAction,
),
),
);
$response['createdDiplomacy'] = iterator_to_array(
$db->query(
'SELECT me AS fromNationId, you AS toNationId, state, term FROM diplomacy WHERE me > %i OR you > %i ORDER BY me, you',
$nationIdBeforeAction,
$nationIdBeforeAction,
),
);
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
$response['gameEnvironment'] = $gameStorage->getValues([
'isunited',
'turnterm',
'block_change_scout',
'refreshLimit',
]);
}
if (in_array($actionName, ['AutoDeleteInvader', 'InvaderEnding'], true)) {
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
$response['gameEnvironment'] = $gameStorage->getValues([
'isunited',
'turnterm',
'block_change_scout',
'refreshLimit',
]);
}
if (($request['compactOutput'] ?? false) === true) {
$observedGeneralTurns = [];
foreach (comparisonIntegerList($request['observe']['generalIds'] ?? [], 'generalIds') as $generalId) {
$observedGeneralTurns[(string)$generalId] = iterator_to_array(
$db->query(
'SELECT turn_idx AS turnIndex, action, arg FROM general_turn WHERE general_id = %i ORDER BY turn_idx',
$generalId,
),
);
}
$response = [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => $actionName,
'afterDetails' => $afterDetails,
'createdGeneralCount' => count($createdGenerals),
'firstCreatedGenerals' => array_slice($createdGenerals, 0, 3),
'createdNations' => $response['createdNations'] ?? [],
'createdEvents' => $response['createdEvents'] ?? [],
'createdDiplomacy' => $response['createdDiplomacy'] ?? [],
'gameEnvironment' => $response['gameEnvironment'] ?? [],
'logs' => $after['logs'],
'observedGeneralTurns' => $observedGeneralTurns,
'remainingEventIds' => array_map(
'intval',
$db->queryFirstColumn('SELECT id FROM event ORDER BY id'),
),
];
}
echo json_encode(
$response,
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
} catch (\Throwable $throwable) {
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
exit(1);
}
}
comparisonMonthlyEventTraceMain();
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
$groups = [
'nation' => ['ActionNationType', 'ActionNationType'],
'domestic' => ['ActionSpecialDomestic', 'ActionSpecialDomestic'],
'war' => ['ActionSpecialWar', 'ActionSpecialWar'],
'personality' => ['ActionPersonality', 'ActionPersonality'],
];
$result = [];
foreach ($groups as $kind => [$directory, $namespace]) {
foreach (glob(__DIR__ . "/../sammo/{$directory}/*.php") ?: [] as $path) {
$key = basename($path, '.php');
if ($key === 'None' || str_starts_with($key, 'che_event_') || $key === 'che_거상') {
continue;
}
$className = "\\sammo\\{$namespace}\\{$key}";
$trait = new $className();
$result[$kind][$key] = [
'name' => $trait->getName(),
'info' => $trait->getInfo(),
];
}
ksort($result[$kind]);
}
echo Json::encode($result);
+804
View File
@@ -0,0 +1,804 @@
<?php
declare(strict_types=1);
namespace sammo;
use sammo\Enums\RankColumn;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
error_reporting(E_ALL & ~E_DEPRECATED);
require_once 'lib.php';
require_once 'func.php';
require_once __DIR__ . '/turn_state_snapshot.php';
final class TurnComparisonTracingRNG implements RNG
{
private int $sequence = 0;
/** @var list<array<string, mixed>> */
public array $calls = [];
public function __construct(private readonly RNG $inner)
{
}
public static function getMaxInt(): int
{
return LiteHashDRBG::getMaxInt();
}
public function nextBytes(int $bytes): string
{
$value = $this->inner->nextBytes($bytes);
$this->record('nextBytes', ['bytes' => $bytes], bin2hex($value));
return $value;
}
public function nextBits(int $bits): string
{
$value = $this->inner->nextBits($bits);
$this->record('nextBits', ['bits' => $bits], bin2hex($value));
return $value;
}
public function nextInt(?int $max = null): int
{
$value = $this->inner->nextInt($max);
$this->record('nextInt', ['maxInclusive' => $max], $value);
return $value;
}
public function nextFloat1(): float
{
$value = $this->inner->nextFloat1();
$this->record('nextFloat1', [], $value);
return $value;
}
private function record(string $operation, array $arguments, mixed $result): void
{
$this->calls[] = [
'seq' => $this->sequence++,
'operation' => $operation,
'arguments' => $arguments === [] ? (object)[] : $arguments,
'result' => $result,
];
}
}
/** @return array<string, mixed> */
function comparisonMappedPatch(array $row, array $mapping): array
{
$patch = [];
foreach ($mapping as $canonical => $legacy) {
if (array_key_exists($canonical, $row)) {
$patch[$legacy] = $row[$canonical];
}
}
return $patch;
}
function comparisonApplyTurnFixtureSetup(mixed $setup): void
{
if ($setup === null) {
return;
}
if (!is_array($setup)) {
throw new \InvalidArgumentException('setup must be an object');
}
$db = DB::db();
$world = $setup['world'] ?? null;
if ($world !== null) {
if (!is_array($world)) {
throw new \InvalidArgumentException('setup.world must be an object');
}
$game = KVStorage::getStorage($db, 'game_env');
foreach (['year', 'month', 'startyear', 'init_year', 'init_month'] as $key) {
$fixtureKey = match ($key) {
'startyear' => 'startYear',
'init_year' => 'initYear',
'init_month' => 'initMonth',
default => $key,
};
if (!array_key_exists($fixtureKey, $world)) {
continue;
}
$value = $world[$fixtureKey];
if (
!is_int($value)
|| $value < 1
|| (in_array($key, ['month', 'init_month'], true) && $value > 12)
) {
throw new \InvalidArgumentException("setup.world.{$fixtureKey} is invalid");
}
$game->setValue($key, $value);
}
$game->resetCache();
if (array_key_exists('hiddenSeed', $world)) {
$hiddenSeed = $world['hiddenSeed'];
if (!is_string($hiddenSeed) || $hiddenSeed === '' || strlen($hiddenSeed) > 256) {
throw new \InvalidArgumentException('setup.world.hiddenSeed is invalid');
}
UniqueConst::$hiddenSeed = $hiddenSeed;
}
// 비교 fixture는 전역 상태에 의존하지 않도록 effect 기본값을 null로 고정하고,
// 명시된 경우에만 해당 effect를 활성화합니다.
GameConst::$scenarioEffect = null;
if (array_key_exists('scenarioEffect', $world)) {
$scenarioEffect = $world['scenarioEffect'];
if ($scenarioEffect === '' || $scenarioEffect === 'None') {
$scenarioEffect = null;
}
if ($scenarioEffect !== null) {
if (!is_string($scenarioEffect) || getScenarioEffectClass($scenarioEffect) === null) {
throw new \InvalidArgumentException('setup.world.scenarioEffect is invalid');
}
}
GameConst::$scenarioEffect = $scenarioEffect;
}
if (array_key_exists('staticEventHandlers', $world)) {
$handlersByEvent = $world['staticEventHandlers'];
if (!is_array($handlersByEvent)) {
throw new \InvalidArgumentException('setup.world.staticEventHandlers must be an object');
}
foreach ($handlersByEvent as $eventType => $handlers) {
if (!is_string($eventType) || $eventType === '' || !is_array($handlers)) {
throw new \InvalidArgumentException('setup.world.staticEventHandlers is invalid');
}
foreach ($handlers as $handler) {
if (!is_string($handler) || $handler === '') {
throw new \InvalidArgumentException('setup.world.staticEventHandlers is invalid');
}
}
}
GameConst::$staticEventHandlers = $handlersByEvent;
}
}
foreach (['nations', 'cities', 'generals', 'troops', 'diplomacy'] as $collection) {
if (isset($setup[$collection]) && !is_array($setup[$collection])) {
throw new \InvalidArgumentException("setup.{$collection} must be an array");
}
}
$generalCooldowns = comparisonGeneralCooldownSelectors(
$setup['generalCooldowns'] ?? [],
'setup.generalCooldowns',
);
if ($generalCooldowns !== []) {
$nextExecuteStorage = KVStorage::getStorage($db, 'next_execute');
foreach ($setup['generalCooldowns'] as $entry) {
$nextAvailableTurn = $entry['nextAvailableTurn'] ?? null;
if (!is_int($nextAvailableTurn) || $nextAvailableTurn < 0) {
throw new \InvalidArgumentException(
'setup.generalCooldowns entries require a non-negative nextAvailableTurn',
);
}
$key = "next_execute_{$entry['generalId']}_{$entry['actionName']}";
$nextExecuteStorage->setValue($key, $nextAvailableTurn);
}
$nextExecuteStorage->resetCache();
}
if (isset($setup['randomFoundingCandidateCityIds'])) {
$candidateCityIds = comparisonIntegerList(
$setup['randomFoundingCandidateCityIds'],
'randomFoundingCandidateCityIds',
);
$db->update('city', ['level' => 4], '1 = 1');
if ($candidateCityIds !== []) {
$db->update('city', ['level' => 5], 'city IN %li', $candidateCityIds);
}
}
if (($setup['isolateWorld'] ?? false) === true) {
$generalIds = array_values(array_map(
static fn(array $row): int => (int)$row['id'],
$setup['generals'] ?? [],
));
$nationIds = array_values(array_map(
static fn(array $row): int => (int)$row['id'],
$setup['nations'] ?? [],
));
if ($generalIds === [] || $nationIds === []) {
throw new \InvalidArgumentException('isolateWorld requires generals and nations');
}
$generalTemplate = $db->queryFirstRow(
'SELECT * FROM general WHERE no IN %li ORDER BY no LIMIT 1',
$generalIds,
);
if ($generalTemplate === null) {
throw new \InvalidArgumentException('isolateWorld requires at least one existing fixture general');
}
$generalTemplateId = (int)$generalTemplate['no'];
$rankDataTemplate = $db->query(
'SELECT `type`, `value` FROM rank_data WHERE general_id = %i',
$generalTemplateId,
);
$db->delete('general_turn', 'general_id NOT IN %li', $generalIds);
$db->delete('rank_data', 'general_id NOT IN %li', $generalIds);
$db->delete('nation_turn', 'nation_id NOT IN %li', $nationIds);
$db->delete('troop', '1 = 1');
$db->delete('diplomacy', '1 = 1');
$db->delete('general', 'no NOT IN %li', $generalIds);
foreach ($generalIds as $generalId) {
if ($db->queryFirstField('SELECT no FROM general WHERE no = %i', $generalId) !== null) {
continue;
}
$newGeneral = $generalTemplate;
$newGeneral['no'] = $generalId;
$newGeneral['owner'] = 0;
$newGeneral['name'] = "fixture-general-{$generalId}";
$db->insert('general', $newGeneral);
}
foreach ($generalIds as $generalId) {
foreach ($rankDataTemplate as $rankRow) {
$rankData = [
'general_id' => $generalId,
'type' => $rankRow['type'],
'value' => $rankRow['value'],
];
$db->insertUpdate('rank_data', $rankData, $rankData);
}
}
$db->delete('nation', 'nation NOT IN %li', $nationIds);
$db->update('city', [
'nation' => 0,
'supply' => 0,
'front' => 0,
'state' => 0,
'term' => 0,
'conflict' => '{}',
'officer_set' => 0,
], '1 = 1');
}
foreach ($setup['nations'] ?? [] as $row) {
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
throw new \InvalidArgumentException('setup.nations requires positive integer ids');
}
$data = comparisonMappedPatch($row, [
'id' => 'nation',
'name' => 'name',
'color' => 'color',
'capitalCityId' => 'capital',
'gold' => 'gold',
'rice' => 'rice',
'tech' => 'tech',
'level' => 'level',
'typeCode' => 'type',
'war' => 'war',
'diplomacyLimit' => 'surlimit',
'generalCount' => 'gennum',
'power' => 'power',
'capitalRevision' => 'capset',
'strategicCommandLimit' => 'strategic_cmd_limit',
]);
if (isset($row['meta'])) {
$data['aux'] = Json::encode($row['meta']);
}
$data += [
'name' => "fixture-nation-{$row['id']}",
'color' => '#777777',
'capital' => 0,
'gold' => 0,
'rice' => 0,
'level' => 1,
'type' => 'che_중립',
'aux' => '{}',
];
$db->insertUpdate('nation', $data, $data);
$nationStor = KVStorage::getStorage($db, $row['id'], 'nation_env');
if (($setup['isolateWorld'] ?? false) === true) {
$nationStor->deleteValue('recv_assist');
$nationStor->deleteValue('resp_assist');
}
if (array_key_exists('nationEnv', $row)) {
if (!is_array($row['nationEnv'])) {
throw new \InvalidArgumentException('setup.nations.nationEnv must be an object');
}
foreach ($row['nationEnv'] as $key => $value) {
if (!is_string($key) || $key === '') {
throw new \InvalidArgumentException('setup.nations.nationEnv keys must be non-empty strings');
}
$nationStor->setValue($key, $value);
}
$nationStor->resetCache();
}
if (array_key_exists('turnLastByOfficerLevel', $row)) {
$turnLastByOfficerLevel = $row['turnLastByOfficerLevel'];
if (!is_array($turnLastByOfficerLevel)) {
throw new \InvalidArgumentException('setup.nations.turnLastByOfficerLevel must be an object');
}
foreach ($turnLastByOfficerLevel as $officerLevel => $lastTurn) {
$officerLevel = filter_var($officerLevel, FILTER_VALIDATE_INT);
if (
$officerLevel === false
|| $officerLevel < 5
|| $officerLevel > 12
|| !is_array($lastTurn)
) {
throw new \InvalidArgumentException(
'setup.nations.turnLastByOfficerLevel requires officer levels 5 through 12'
);
}
$nationStor->setValue("turn_last_{$officerLevel}", $lastTurn);
}
$nationStor->resetCache();
}
}
if (($setup['isolateWorld'] ?? false) === true && ($setup['nations'] ?? []) !== []) {
$maxNationId = max(array_map(
static fn(array $row): int => (int)$row['id'],
$setup['nations'],
));
$db->query('ALTER TABLE nation AUTO_INCREMENT = %i', $maxNationId + 1);
}
foreach ($setup['cities'] ?? [] as $row) {
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
throw new \InvalidArgumentException('setup.cities requires positive integer ids');
}
$patch = comparisonMappedPatch($row, [
'nationId' => 'nation',
'level' => 'level',
'population' => 'pop',
'populationMax' => 'pop_max',
'agriculture' => 'agri',
'agricultureMax' => 'agri_max',
'commerce' => 'comm',
'commerceMax' => 'comm_max',
'security' => 'secu',
'securityMax' => 'secu_max',
'supplyState' => 'supply',
'frontState' => 'front',
'defence' => 'def',
'defenceMax' => 'def_max',
'wall' => 'wall',
'wallMax' => 'wall_max',
'state' => 'state',
'term' => 'term',
'trust' => 'trust',
'trade' => 'trade',
'officerSet' => 'officer_set',
]);
if (array_key_exists('conflictEntries', $row)) {
if (!is_array($row['conflictEntries'])) {
throw new \InvalidArgumentException('setup.cities.conflictEntries must be an array');
}
$orderedConflict = [];
foreach ($row['conflictEntries'] as $entry) {
if (
!is_array($entry)
|| count($entry) !== 2
|| !is_int($entry[0] ?? null)
|| $entry[0] < 1
|| !is_numeric($entry[1] ?? null)
) {
throw new \InvalidArgumentException(
'setup.cities.conflictEntries requires [positive nation id, numeric value] pairs'
);
}
$orderedConflict[$entry[0]] = $entry[1];
}
$patch['conflict'] = Json::encode($orderedConflict);
} elseif (array_key_exists('conflict', $row)) {
if (!is_array($row['conflict'])) {
throw new \InvalidArgumentException('setup.cities.conflict must be an object');
}
$patch['conflict'] = Json::encode($row['conflict']);
}
if ($patch !== []) {
$db->update('city', $patch, 'city = %i', $row['id']);
}
}
foreach ($setup['generals'] ?? [] as $row) {
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
throw new \InvalidArgumentException('setup.generals requires positive integer ids');
}
$patch = comparisonMappedPatch($row, [
'name' => 'name',
'nationId' => 'nation',
'cityId' => 'city',
'troopId' => 'troop',
'leadership' => 'leadership',
'strength' => 'strength',
'intelligence' => 'intel',
'leadershipExp' => 'leadership_exp',
'strengthExp' => 'strength_exp',
'intelExp' => 'intel_exp',
'experience' => 'experience',
'dedication' => 'dedication',
'expLevel' => 'explevel',
'officerLevel' => 'officer_level',
'officerCityId' => 'officer_city',
'belong' => 'belong',
'permission' => 'permission',
'betray' => 'betray',
'makeLimit' => 'makelimit',
'injury' => 'injury',
'age' => 'age',
'gold' => 'gold',
'rice' => 'rice',
'crew' => 'crew',
'crewTypeId' => 'crewtype',
'train' => 'train',
'atmos' => 'atmos',
'dex1' => 'dex1',
'dex2' => 'dex2',
'dex3' => 'dex3',
'dex4' => 'dex4',
'dex5' => 'dex5',
'specAge' => 'specage',
'specAge2' => 'specage2',
'killTurn' => 'killturn',
'npcState' => 'npc',
'blockState' => 'block',
'specialDomestic' => 'special',
'specialWar' => 'special2',
'personality' => 'personal',
'itemHorse' => 'horse',
'itemWeapon' => 'weapon',
'itemBook' => 'book',
'itemExtra' => 'item',
]);
if (isset($row['meta'])) {
$patch['aux'] = Json::encode($row['meta']);
}
if (array_key_exists('penalty', $row)) {
if (!is_array($row['penalty'])) {
throw new \InvalidArgumentException('setup.generals.penalty must be an object');
}
$patch['penalty'] = Json::encode($row['penalty']);
}
if (isset($row['lastTurn']) && is_array($row['lastTurn'])) {
$patch['last_turn'] = Json::encode($row['lastTurn']);
}
if ($patch !== []) {
$db->update('general', $patch, 'no = %i', $row['id']);
}
if (array_key_exists('nationId', $row)) {
$db->update('rank_data', [
'nation_id' => $row['nationId'],
], 'general_id = %i', $row['id']);
}
}
foreach ($setup['rankData'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['generalId'] ?? null)
|| !is_string($row['type'] ?? null)
|| RankColumn::tryFrom($row['type']) === null
|| !is_int($row['value'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.rankData row');
}
$nationId = (int)($db->queryFirstField(
'SELECT nation FROM general WHERE no = %i',
$row['generalId'],
) ?? 0);
$db->insertUpdate('rank_data', [
'general_id' => $row['generalId'],
'nation_id' => $nationId,
'type' => $row['type'],
'value' => $row['value'],
], [
'nation_id' => $nationId,
'value' => $row['value'],
]);
}
if (($setup['isolateWorld'] ?? false) === true && ($setup['generals'] ?? []) !== []) {
$maxGeneralId = max(array_map(
static fn(array $row): int => (int)$row['id'],
$setup['generals'],
));
$db->query('ALTER TABLE general AUTO_INCREMENT = %i', $maxGeneralId + 1);
}
foreach ($setup['troops'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['id'] ?? null)
|| !is_int($row['nationId'] ?? null)
|| !is_string($row['name'] ?? null)
|| $row['id'] < 1
|| $row['nationId'] < 1
|| $row['name'] === ''
) {
throw new \InvalidArgumentException('setup.troops requires positive id/nationId and non-empty name');
}
$data = [
'troop_leader' => $row['id'],
'nation' => $row['nationId'],
'name' => $row['name'],
];
$db->insertUpdate('troop', $data, $data);
}
foreach ($setup['diplomacy'] ?? [] as $row) {
if (
!is_array($row) ||
!is_int($row['fromNationId'] ?? null) ||
!is_int($row['toNationId'] ?? null) ||
$row['fromNationId'] < 1 ||
$row['toNationId'] < 1
) {
throw new \InvalidArgumentException('setup.diplomacy requires positive integer nation ids');
}
$data = comparisonMappedPatch($row, [
'fromNationId' => 'me',
'toNationId' => 'you',
'state' => 'state',
'term' => 'term',
'dead' => 'dead',
]);
$data += ['state' => 3, 'term' => 0, 'dead' => 0];
$db->insertUpdate('diplomacy', $data, $data);
}
}
function comparisonRunTurnCommand(array $request): array
{
if (getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
throw new \RuntimeException('TURN_DIFFERENTIAL_ENABLED=1 is required');
}
$kind = $request['kind'] ?? null;
if (!in_array($kind, ['general', 'nation', 'instantNation', 'troopJoinStaticEvent'], true)) {
throw new \InvalidArgumentException(
'kind must be general, nation, instantNation, or troopJoinStaticEvent',
);
}
$actorGeneralId = $request['actorGeneralId'] ?? null;
$action = $request['action'] ?? null;
$args = $request['args'] ?? null;
$includeLifecycle = $request['includeLifecycle'] ?? false;
if (!is_int($actorGeneralId) || $actorGeneralId < 1) {
throw new \InvalidArgumentException('actorGeneralId must be a positive integer');
}
if (!is_string($action) || $action === '') {
throw new \InvalidArgumentException('action must be a non-empty string');
}
if (!is_bool($includeLifecycle)) {
throw new \InvalidArgumentException('includeLifecycle must be a boolean');
}
if ($includeLifecycle && $kind !== 'general') {
throw new \InvalidArgumentException('includeLifecycle currently supports general commands only');
}
comparisonApplyTurnFixtureSetup($request['setup'] ?? null);
if ($includeLifecycle) {
DB::db()->insertUpdate('general_turn', [
'general_id' => $actorGeneralId,
'turn_idx' => 0,
'action' => $action,
'arg' => Json::encode($args),
'brief' => $action,
], [
'action' => $action,
'arg' => Json::encode($args),
'brief' => $action,
]);
}
$snapshotRequest = ['observe' => $request['observe'] ?? []];
$before = comparisonTurnStateSnapshot($snapshotRequest);
$db = DB::db();
$gameStorage = KVStorage::getStorage($db, 'game_env');
$gameStorage->resetCache();
$environment = $gameStorage->getAll();
if ($kind === 'troopJoinStaticEvent') {
if ($action !== \sammo\API\Troop\JoinTroop::class) {
throw new \InvalidArgumentException('troopJoinStaticEvent requires the JoinTroop API action');
}
$troopId = is_array($args) ? ($args['troopID'] ?? null) : null;
if (!is_int($troopId) || $troopId < 1) {
throw new \InvalidArgumentException('troopJoinStaticEvent requires a positive integer troopID');
}
$general = GeneralLite::createObjFromDB(
$actorGeneralId,
['troop'],
Enums\GeneralLiteQueryMode::Lite,
);
if ($general === null) {
throw new \RuntimeException('장수 정보를 불러올 수 없습니다.');
}
if ($general->getVar('troop') != 0) {
throw new \RuntimeException('이미 부대에 소속되어 있습니다.');
}
$nationId = $general->getNationID();
if ($nationId == 0) {
throw new \RuntimeException('국가에 소속되어 있지 않습니다.');
}
$troopExists = $db->queryFirstField(
'SELECT `troop_leader` FROM `troop` WHERE `troop_leader` = %i AND `nation` = %i',
$troopId,
$nationId,
);
if (!$troopExists) {
throw new \RuntimeException('부대가 올바르지 않습니다.');
}
GameConst::$staticEventHandlers = [
\sammo\API\Troop\JoinTroop::class => ['event_부대탑승즉시이동'],
];
$general->setVar('troop', $troopId);
StaticEventHandler::handleEvent($general, null, \sammo\API\Troop\JoinTroop::class, [], $args);
$general->applyDB($db);
$after = comparisonTurnStateSnapshot($snapshotRequest);
return [
'schemaVersion' => 1,
'engine' => 'ref',
'execution' => [
'kind' => $kind,
'actorGeneralId' => $actorGeneralId,
'action' => $action,
'args' => $args,
'seedDomain' => 'none',
'outcome' => ['completed' => true],
],
'before' => $before,
'after' => $after,
'rng' => [],
];
}
$general = General::createObjFromDB($actorGeneralId);
if ($kind === 'instantNation') {
$command = buildNationCommandClass($action, $general, $environment, new LastTurn(), $args);
if (!$command->hasFullConditionMet()) {
throw new \RuntimeException($command->getFailString());
}
$completed = $command->run(NoRNG::rngInstance());
$command->setNextAvailable();
$general->getLogger()->flush();
$after = comparisonTurnStateSnapshot($snapshotRequest);
return [
'schemaVersion' => 1,
'engine' => 'ref',
'execution' => [
'kind' => $kind,
'actorGeneralId' => $actorGeneralId,
'action' => $action,
'args' => $args,
'seedDomain' => 'none',
'outcome' => [
'lastTurn' => (object)[],
'commandName' => $command->getName(),
'completed' => $completed,
],
],
'before' => $before,
'after' => $after,
'rng' => [],
];
}
$turn = new TurnExecutionHelper($general);
$general->increaseInheritancePoint(Enums\InheritanceKey::lived_month, 1);
$preprocessRng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'preprocess',
(int)$environment['year'],
(int)$environment['month'],
$actorGeneralId,
)));
$turn->preprocessCommand($preprocessRng, $environment);
$seedDomain = $kind === 'general' ? 'generalCommand' : 'nationCommand';
$seed = Util::simpleSerialize(
UniqueConst::$hiddenSeed,
$seedDomain,
(int)$environment['year'],
(int)$environment['month'],
$actorGeneralId,
$action,
);
$tracingRng = new TurnComparisonTracingRNG(new LiteHashDRBG($seed));
$rng = new RandUtil($tracingRng);
if ($kind === 'general' && $action === 'che_견문') {
\sammo\TextDecoration\SightseeingMessage::setComparisonRng($rng);
}
if ($kind === 'general') {
$previousLastTurn = $general->getLastTurn();
$previousLastTurnRaw = $previousLastTurn->toRaw();
$command = buildGeneralCommandClass($action, $general, $environment, $args);
$commandFullConditionMet = $command->hasFullConditionMet();
$resultTurn = $turn->processCommand($rng, $command, false);
} else {
$nationStor = KVStorage::getStorage($db, $general->getNationID(), 'nation_env');
$lastNationTurnKey = "turn_last_{$general->getVar('officer_level')}";
$previousLastTurn = LastTurn::fromRaw($nationStor->getValue($lastNationTurnKey));
$previousLastTurnRaw = $previousLastTurn->toRaw();
$command = buildNationCommandClass($action, $general, $environment, $previousLastTurn, $args);
$commandFullConditionMet = $command->hasFullConditionMet();
$resultTurn = $turn->processNationCommand($rng, $command);
$nationStor->setValue($lastNationTurnKey, $resultTurn->toRaw());
}
if ($includeLifecycle) {
pullGeneralCommand($general->getID());
$general->increaseVarWithLimit(
'myset',
GameConst::$incDefSettingChange,
null,
GameConst::$maxDefSettingChange,
);
$turn->updateTurnTime();
}
$general->getLogger()->flush();
$turn->applyDB();
unset($turn);
$after = comparisonTurnStateSnapshot($snapshotRequest);
$resultTurnRaw = $resultTurn->toRaw();
$resultTerm = (int)($resultTurnRaw['term'] ?? 0);
$preReqTurn = $command->getPreReqTurn();
$acceptScoutCompleted = (
$action === 'che_등용수락'
&& is_array($args)
&& is_int($args['destNationID'] ?? null)
&& $general->getNationID() === $args['destNationID']
);
$foundNationCompleted = (
in_array($action, ['che_건국', 'cr_건국', 'che_무작위건국'], true)
&& is_array($args)
&& is_string($args['nationName'] ?? null)
&& DB::db()->queryFirstField(
'SELECT name FROM nation WHERE nation = %i',
$general->getNationID(),
) === $args['nationName']
);
$completedByState = (
$action === 'che_접경귀환'
&& $tracingRng->calls !== []
) || $acceptScoutCompleted || $foundNationCompleted || (($resultTurnRaw['command'] ?? null) === $command->getName()
&& (
($preReqTurn === 0 && $resultTerm === 0)
|| (
$preReqTurn > 0
&& ($previousLastTurnRaw['command'] ?? null) === $command->getName()
&& ($previousLastTurnRaw['arg'] ?? null) === $command->getArg()
&& (int)($previousLastTurnRaw['term'] ?? 0) === $preReqTurn
)
));
$completed = $commandFullConditionMet && $completedByState;
return [
'schemaVersion' => 1,
'engine' => 'ref',
'execution' => [
'kind' => $kind,
'actorGeneralId' => $actorGeneralId,
'action' => $action,
'args' => $args,
'seedDomain' => $seedDomain,
'outcome' => [
'lastTurn' => $resultTurnRaw,
'commandName' => $command->getName(),
'completed' => $completed,
],
],
'before' => $before,
'after' => $after,
'rng' => $tracingRng->calls,
];
}
try {
$input = stream_get_contents(STDIN);
$request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($request)) {
throw new \InvalidArgumentException('request must be an object');
}
echo json_encode(
comparisonRunTurnCommand($request),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
} catch (\Throwable $throwable) {
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
exit(1);
}
+643
View File
@@ -0,0 +1,643 @@
<?php
declare(strict_types=1);
namespace sammo;
use sammo\Enums\RankColumn;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
/** @return list<int> */
function comparisonIntegerList(mixed $value, string $label): array
{
if ($value === null) {
return [];
}
if (!is_array($value)) {
throw new \InvalidArgumentException("{$label} must be an array");
}
$result = [];
foreach ($value as $entry) {
if (!is_int($entry) || $entry < 0) {
throw new \InvalidArgumentException("{$label} entries must be non-negative integers");
}
$result[$entry] = $entry;
}
ksort($result, SORT_NUMERIC);
return array_values($result);
}
function comparisonJsonValue(mixed $value): mixed
{
if (!is_string($value) || $value === '') {
return $value;
}
try {
return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return $value;
}
}
function comparisonOptionalCode(mixed $value): ?string
{
if (!is_string($value) || $value === '' || $value === 'None') {
return null;
}
return $value;
}
/** @return list<array{generalId: int, actionName: string}> */
function comparisonGeneralCooldownSelectors(mixed $value, string $label): array
{
if ($value === null) {
return [];
}
if (!is_array($value)) {
throw new \InvalidArgumentException("{$label} must be an array");
}
$result = [];
foreach ($value as $entry) {
if (!is_array($entry)) {
throw new \InvalidArgumentException("{$label} entries must be objects");
}
$generalId = $entry['generalId'] ?? null;
$actionName = $entry['actionName'] ?? null;
if (!is_int($generalId) || $generalId < 1 || !is_string($actionName) || $actionName === '') {
throw new \InvalidArgumentException(
"{$label} entries require a positive generalId and non-empty actionName",
);
}
$result["{$generalId}:{$actionName}"] = [
'generalId' => $generalId,
'actionName' => $actionName,
];
}
ksort($result, SORT_STRING);
return array_values($result);
}
/** @return list<array{nationId: int, actionName: string}> */
function comparisonNationCooldownSelectors(mixed $value, string $label): array
{
if ($value === null) {
return [];
}
if (!is_array($value)) {
throw new \InvalidArgumentException("{$label} must be an array");
}
$result = [];
foreach ($value as $entry) {
if (!is_array($entry)) {
throw new \InvalidArgumentException("{$label} entries must be objects");
}
$nationId = $entry['nationId'] ?? null;
$actionName = $entry['actionName'] ?? null;
if (!is_int($nationId) || $nationId < 1 || !is_string($actionName) || $actionName === '') {
throw new \InvalidArgumentException(
"{$label} entries require a positive nationId and non-empty actionName",
);
}
$result["{$nationId}:{$actionName}"] = [
'nationId' => $nationId,
'actionName' => $actionName,
];
}
ksort($result, SORT_STRING);
return array_values($result);
}
/** @return list<array{fromNationId: int, toNationId: int}> */
function comparisonDiplomacyPairSelectors(mixed $value, string $label): array
{
if ($value === null) {
return [];
}
if (!is_array($value)) {
throw new \InvalidArgumentException("{$label} must be an array");
}
$result = [];
foreach ($value as $entry) {
if (!is_array($entry)) {
throw new \InvalidArgumentException("{$label} entries must be objects");
}
$fromNationId = $entry['fromNationId'] ?? null;
$toNationId = $entry['toNationId'] ?? null;
if (!is_int($fromNationId) || $fromNationId < 1 || !is_int($toNationId) || $toNationId < 1) {
throw new \InvalidArgumentException(
"{$label} entries require positive fromNationId and toNationId",
);
}
$result["{$fromNationId}:{$toNationId}"] = [
'fromNationId' => $fromNationId,
'toNationId' => $toNationId,
];
}
ksort($result, SORT_STRING);
return array_values($result);
}
/** @param array<string, mixed> $row */
function comparisonPickRow(array $row, array $mapping, array $jsonKeys = []): array
{
$result = [];
foreach ($mapping as $canonical => $legacy) {
if (!array_key_exists($legacy, $row)) {
continue;
}
$value = $row[$legacy];
if (in_array($legacy, $jsonKeys, true)) {
$value = comparisonJsonValue($value);
}
$result[$canonical] = $value;
}
return $result;
}
/** @param list<int> $ids */
function comparisonRowsById(string $table, string $idColumn, array $ids): array
{
$db = DB::db();
$rows = [];
foreach ($ids as $id) {
$row = $db->queryFirstRow(
"SELECT * FROM `{$table}` WHERE `{$idColumn}` = %i",
$id,
);
if ($row !== null) {
$rows[] = $row;
}
}
return $rows;
}
function comparisonTurnStateSnapshot(array $request): array
{
$observe = $request['observe'] ?? [];
if (!is_array($observe)) {
throw new \InvalidArgumentException('observe must be an object');
}
$generalIds = comparisonIntegerList($observe['generalIds'] ?? [], 'generalIds');
$cityIds = comparisonIntegerList($observe['cityIds'] ?? [], 'cityIds');
$nationIds = comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds');
$logAfterId = $observe['logAfterId'] ?? 0;
$messageAfterId = $observe['messageAfterId'] ?? 0;
$includeNationHistoryLogs = $observe['includeNationHistoryLogs'] ?? false;
$includeGlobalHistoryLogs = $observe['includeGlobalHistoryLogs'] ?? false;
$generalCooldownSelectors = comparisonGeneralCooldownSelectors(
$observe['generalCooldowns'] ?? [],
'generalCooldowns',
);
$nationCooldownSelectors = comparisonNationCooldownSelectors(
$observe['nationCooldowns'] ?? [],
'nationCooldowns',
);
$diplomacyPairSelectors = comparisonDiplomacyPairSelectors(
$observe['diplomacyPairs'] ?? [],
'diplomacyPairs',
);
if (
!is_int($logAfterId)
|| $logAfterId < 0
|| !is_int($messageAfterId)
|| $messageAfterId < 0
) {
throw new \InvalidArgumentException('logAfterId and messageAfterId must be non-negative integers');
}
if (!is_bool($includeNationHistoryLogs)) {
throw new \InvalidArgumentException('includeNationHistoryLogs must be a boolean');
}
if (!is_bool($includeGlobalHistoryLogs)) {
throw new \InvalidArgumentException('includeGlobalHistoryLogs must be a boolean');
}
$db = DB::db();
$game = KVStorage::getStorage($db, 'game_env');
$game->resetCache();
$worldValues = $game->getValues([
'year',
'month',
'turnterm',
'turntime',
'isunited',
'scenario',
'init_year',
'init_month',
'develcost',
'killturn',
]);
$nextExecuteStorage = KVStorage::getStorage($db, 'next_execute');
$nextExecuteStorage->resetCache();
$generalCooldowns = array_map(
static function (array $selector) use ($nextExecuteStorage): array {
$key = "next_execute_{$selector['generalId']}_{$selector['actionName']}";
$value = $nextExecuteStorage->getValue($key);
return [
...$selector,
'nextAvailableTurn' => is_int($value) ? $value : null,
];
},
$generalCooldownSelectors,
);
$nationCooldowns = array_map(
static function (array $selector) use ($db): array {
$nationStorage = KVStorage::getStorage($db, $selector['nationId'], 'nation_env');
$nationStorage->resetCache();
$value = $nationStorage->getValue("next_execute_{$selector['actionName']}");
return [
...$selector,
'nextAvailableTurn' => is_int($value) ? $value : null,
];
},
$nationCooldownSelectors,
);
$generals = array_map(
static function (array $row): array {
$result = comparisonPickRow(
$row,
[
'id' => 'no',
'name' => 'name',
'nationId' => 'nation',
'cityId' => 'city',
'troopId' => 'troop',
'leadership' => 'leadership',
'strength' => 'strength',
'intelligence' => 'intel',
'experience' => 'experience',
'dedication' => 'dedication',
'expLevel' => 'explevel',
'officerLevel' => 'officer_level',
'officerCityId' => 'officer_city',
'belong' => 'belong',
'permission' => 'permission',
'betray' => 'betray',
'makeLimit' => 'makelimit',
'injury' => 'injury',
'gold' => 'gold',
'rice' => 'rice',
'crew' => 'crew',
'crewTypeId' => 'crewtype',
'train' => 'train',
'atmos' => 'atmos',
'age' => 'age',
'npcState' => 'npc',
'turnTime' => 'turntime',
'recentWarTime' => 'recent_war',
'lastTurn' => 'last_turn',
'meta' => 'aux',
'penalty' => 'penalty',
'leadershipExp' => 'leadership_exp',
'strengthExp' => 'strength_exp',
'intelExp' => 'intel_exp',
'dex1' => 'dex1',
'dex2' => 'dex2',
'dex3' => 'dex3',
'dex4' => 'dex4',
'dex5' => 'dex5',
'specAge' => 'specage',
'specAge2' => 'specage2',
'killTurn' => 'killturn',
'mySet' => 'myset',
'specialDomestic' => 'special',
'specialWar' => 'special2',
'personality' => 'personal',
'itemHorse' => 'horse',
'itemWeapon' => 'weapon',
'itemBook' => 'book',
'itemExtra' => 'item',
],
['last_turn', 'aux', 'penalty'],
);
foreach ([
'specialDomestic',
'specialWar',
'personality',
'itemHorse',
'itemWeapon',
'itemBook',
'itemExtra',
] as $key) {
$result[$key] = comparisonOptionalCode($result[$key] ?? null);
}
$result['maxBelong'] = (int)($result['meta']['max_belong'] ?? 0);
$ownerId = (int)($row['owner'] ?? 0);
$activeActionPoints = 0;
if ($ownerId > 0) {
$inheritanceStorage = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}");
$inheritanceStorage->resetCache();
$stored = $inheritanceStorage->getValue('active_action');
if (is_array($stored) && (is_int($stored[0] ?? null) || is_float($stored[0] ?? null))) {
$activeActionPoints = $stored[0];
}
}
$result['hasOwner'] = $ownerId > 0;
$result['inheritActiveActionPoints'] = $activeActionPoints;
return $result;
},
comparisonRowsById('general', 'no', $generalIds),
);
$rankTypes = array_map(
static fn(RankColumn $column): string => $column->value,
RankColumn::cases(),
);
$rankData = $generalIds === []
? []
: iterator_to_array($db->query(
'SELECT general_id AS generalId, nation_id AS nationId, `type`, `value`'
. ' FROM rank_data WHERE general_id IN %li AND `type` IN %ls'
. ' ORDER BY general_id, `type`',
$generalIds,
$rankTypes,
));
$cities = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'city',
'name' => 'name',
'nationId' => 'nation',
'level' => 'level',
'population' => 'pop',
'populationMax' => 'pop_max',
'agriculture' => 'agri',
'agricultureMax' => 'agri_max',
'commerce' => 'comm',
'commerceMax' => 'comm_max',
'security' => 'secu',
'securityMax' => 'secu_max',
'supplyState' => 'supply',
'frontState' => 'front',
'defence' => 'def',
'defenceMax' => 'def_max',
'wall' => 'wall',
'wallMax' => 'wall_max',
'state' => 'state',
'term' => 'term',
'trust' => 'trust',
'trade' => 'trade',
'conflict' => 'conflict',
'officerSet' => 'officer_set',
],
['conflict'],
),
comparisonRowsById('city', 'city', $cityIds),
);
$nations = array_map(
static function (array $row) use ($db): array {
$projected = comparisonPickRow(
$row,
[
'id' => 'nation',
'name' => 'name',
'color' => 'color',
'capitalCityId' => 'capital',
'gold' => 'gold',
'rice' => 'rice',
'tech' => 'tech',
'level' => 'level',
'typeCode' => 'type',
'generalCount' => 'gennum',
'power' => 'power',
'war' => 'war',
'diplomacyLimit' => 'surlimit',
'capitalRevision' => 'capset',
'strategicCommandLimit' => 'strategic_cmd_limit',
'meta' => 'aux',
],
['aux'],
);
$nationStor = KVStorage::getStorage($db, (int)$row['nation'], 'nation_env');
$receivedAssist = $nationStor->getValue('recv_assist');
if (is_array($receivedAssist) && $receivedAssist !== []) {
$projected['meta']['recv_assist'] = $receivedAssist;
}
$respondedAssist = $nationStor->getValue('resp_assist');
if (is_array($respondedAssist) && $respondedAssist !== []) {
$projected['meta']['resp_assist'] = $respondedAssist;
}
return $projected;
},
comparisonRowsById('nation', 'nation', $nationIds),
);
$diplomacyPairs = [];
foreach ($nationIds as $fromNationId) {
foreach ($nationIds as $toNationId) {
if ($fromNationId === $toNationId) {
continue;
}
$diplomacyPairs["{$fromNationId}:{$toNationId}"] = [
'fromNationId' => $fromNationId,
'toNationId' => $toNationId,
];
}
}
foreach ($diplomacyPairSelectors as $pair) {
$diplomacyPairs["{$pair['fromNationId']}:{$pair['toNationId']}"] = $pair;
}
ksort($diplomacyPairs, SORT_STRING);
$diplomacy = [];
foreach ($diplomacyPairs as $pair) {
$row = $db->queryFirstRow(
'SELECT me, you, state, term, dead FROM diplomacy WHERE me = %i AND you = %i',
$pair['fromNationId'],
$pair['toNationId'],
);
if ($row !== null) {
$diplomacy[] = comparisonPickRow($row, [
'fromNationId' => 'me',
'toNationId' => 'you',
'state' => 'state',
'term' => 'term',
'dead' => 'dead',
]);
}
}
$generalTurns = [];
foreach ($generalIds as $generalId) {
foreach ($db->query(
'SELECT general_id, turn_idx, action, arg FROM general_turn WHERE general_id = %i ORDER BY turn_idx',
$generalId,
) as $row) {
$generalTurns[] = comparisonPickRow(
$row,
['generalId' => 'general_id', 'turnIndex' => 'turn_idx', 'action' => 'action', 'args' => 'arg'],
['arg'],
);
}
}
$nationTurns = [];
foreach ($nationIds as $nationId) {
foreach ($db->query(
'SELECT nation_id, officer_level, turn_idx, action, arg FROM nation_turn WHERE nation_id = %i ORDER BY officer_level, turn_idx',
$nationId,
) as $row) {
$nationTurns[] = comparisonPickRow(
$row,
[
'nationId' => 'nation_id',
'officerLevel' => 'officer_level',
'turnIndex' => 'turn_idx',
'action' => 'action',
'args' => 'arg',
],
['arg'],
);
}
}
$logs = [];
if ($generalIds !== []) {
foreach ($db->query(
'SELECT id, general_id, log_type, year, month, text FROM general_record WHERE id > %i ORDER BY id',
$logAfterId,
) as $row) {
if ((int)$row['general_id'] !== 0 && !in_array((int)$row['general_id'], $generalIds, true)) {
continue;
}
$generalId = (int)$row['general_id'];
$category = (string)$row['log_type'];
if ($category === 'battle') {
$category = 'battle_detail';
} elseif ($generalId === 0) {
$category = 'summary';
}
$logs[] = comparisonPickRow($row, [
'id' => 'id',
'generalId' => 'general_id',
'year' => 'year',
'month' => 'month',
'text' => 'text',
]) + [
'scope' => $generalId === 0 ? 'system' : 'general',
'category' => $category,
'nationId' => null,
];
}
}
if ($includeNationHistoryLogs && $nationIds !== []) {
foreach ($db->query(
'SELECT id, nation_id, year, month, text FROM world_history WHERE nation_id IN %li ORDER BY id',
$nationIds,
) as $row) {
$logs[] = comparisonPickRow($row, [
'id' => 'id',
'nationId' => 'nation_id',
'year' => 'year',
'month' => 'month',
'text' => 'text',
]) + [
'scope' => 'nation',
'category' => 'history',
'generalId' => null,
];
}
}
if ($includeGlobalHistoryLogs) {
foreach ($db->query(
'SELECT id, nation_id, year, month, text FROM world_history WHERE nation_id = 0 ORDER BY id',
) as $row) {
$logs[] = comparisonPickRow($row, [
'id' => 'id',
'nationId' => 'nation_id',
'year' => 'year',
'month' => 'month',
'text' => 'text',
]) + [
'scope' => 'system',
'category' => 'history',
'generalId' => null,
];
}
}
$messages = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'id',
'mailbox' => 'mailbox',
'type' => 'type',
'sourceId' => 'src',
'destinationId' => 'dest',
'createdAt' => 'time',
'payload' => 'message',
],
['message'],
),
$db->query(
'SELECT id, mailbox, type, src, dest, time, message FROM message WHERE id > %i ORDER BY id',
$messageAfterId,
),
);
return [
'schemaVersion' => 1,
'engine' => 'ref',
'world' => [
'year' => (int)$worldValues['year'],
'month' => (int)$worldValues['month'],
'tickMinutes' => (int)$worldValues['turnterm'],
'turnTime' => (string)$worldValues['turntime'],
'isUnited' => (int)$worldValues['isunited'],
'scenarioId' => (int)$worldValues['scenario'],
'initYear' => (int)$worldValues['init_year'],
'initMonth' => (int)$worldValues['init_month'],
'develCost' => (int)$worldValues['develcost'],
'killTurn' => (int)$worldValues['killturn'],
'generalCooldowns' => $generalCooldowns,
'nationCooldowns' => $nationCooldowns,
],
'generals' => $generals,
'rankData' => $rankData,
'cities' => $cities,
'nations' => $nations,
'diplomacy' => $diplomacy,
'generalTurns' => $generalTurns,
'nationTurns' => $nationTurns,
'logs' => $logs,
'messages' => $messages,
'watermarks' => [
'logId' => (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM general_record') ?? 0),
'historyLogId' => (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM world_history') ?? 0),
'messageId' => (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM message') ?? 0),
],
];
}
function comparisonTurnStateSnapshotMain(): void
{
try {
$input = stream_get_contents(STDIN);
$request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($request)) {
throw new \InvalidArgumentException('request must be an object');
}
echo json_encode(
comparisonTurnStateSnapshot($request),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
} catch (\Throwable $throwable) {
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
exit(1);
}
}
if (realpath((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) {
comparisonTurnStateSnapshotMain();
}
+16
View File
@@ -51,6 +51,22 @@
display: none; display: none;
} }
.picture_choice {
margin: 8px auto;
}
.picture_choice label,
.event_picture label {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 4px 8px;
}
.picture_choice img {
object-fit: cover;
}
.custom_picture { .custom_picture {
display: none; display: none;
} }
+15 -10
View File
@@ -9,6 +9,7 @@ use sammo\Enums\GeneralAccessLogColumn;
use sammo\Enums\GeneralColumn; use sammo\Enums\GeneralColumn;
use sammo\Enums\GeneralQueryMode; use sammo\Enums\GeneralQueryMode;
use sammo\Enums\InheritanceKey; use sammo\Enums\InheritanceKey;
use sammo\Enums\PenaltyKey;
use sammo\Enums\RankColumn; use sammo\Enums\RankColumn;
require_once 'process_war.php'; require_once 'process_war.php';
@@ -111,7 +112,7 @@ function GetImageURL($imgsvr, $filepath = '')
function checkLimit($refreshScore = null) function checkLimit($refreshScore = null)
{ {
$session = Session::getInstance(); $session = Session::getInstance();
if ($session->userGrade >= 4) { if ($session->userGrade >= 6) {
return 0; return 0;
} }
@@ -376,11 +377,11 @@ function myNationInfo(General $generalObj)
function checkSecretMaxPermission($penalty) function checkSecretMaxPermission($penalty)
{ {
$secretMax = 4; $secretMax = 4;
if ($penalty['noTopSecret'] ?? false) { if ($penalty[PenaltyKey::NoTopSecret->value] ?? false) {
$secretMax = 1; $secretMax = 1;
} else if ($penalty['noChief'] ?? false) { } else if ($penalty[PenaltyKey::NoChief->value] ?? false) {
$secretMax = 1; $secretMax = 1;
} else if ($penalty['noAmbassador'] ?? false) { } else if ($penalty[PenaltyKey::NoAmbassador->value] ?? false) {
$secretMax = 2; $secretMax = 2;
} }
return $secretMax; return $secretMax;
@@ -403,7 +404,7 @@ function checkSecretPermission(array $me, $checkSecretLimit = true)
} }
if ($penalty['noSecret'] ?? false) { if ($penalty[PenaltyKey::NoChief->value] ?? false) {
return 0; return 0;
} }
@@ -1008,7 +1009,7 @@ function increaseRefresh($type = "", $cnt = 1)
$isunited = $gameStor->isunited; $isunited = $gameStor->isunited;
$opentime = $gameStor->opentime; $opentime = $gameStor->opentime;
if ($userGrade == 6) { if ($userGrade >= 6) {
return; return;
} }
if ($isunited == 2) { if ($isunited == 2) {
@@ -1046,7 +1047,7 @@ function increaseRefresh($type = "", $cnt = 1)
$logDB = FileDB::db($logPath, $serverPath . '/../f_install/sql/api_log.sql'); $logDB = FileDB::db($logPath, $serverPath . '/../f_install/sql/api_log.sql');
$ip = $_SERVER['REMOTE_ADDR'] ?? 'local'; $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'local';
$date = date('Y-m-d H:i:s'); $date = date('Y-m-d H:i:s');
$logDB->insert('api_log', [ $logDB->insert('api_log', [
@@ -1106,7 +1107,7 @@ function CheckOverhead()
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
[$turnterm, $refreshLimit] = $gameStor->getValuesAsArray(['turnterm', 'refreshLimit']); [$turnterm, $refreshLimit] = $gameStor->getValuesAsArray(['turnterm', 'refreshLimit']);
$nextRefreshLimit = Util::round(pow($turnterm, 0.6) * 3) * 10; $nextRefreshLimit = Util::round(pow($turnterm, 0.6) * 3) * GameConst::$refreshLimitCoef;
if ($nextRefreshLimit != $refreshLimit) { if ($nextRefreshLimit != $refreshLimit) {
@@ -1402,6 +1403,9 @@ function CheckHall($no)
if ($valueType === 'natural') { if ($valueType === 'natural') {
$value = $generalObj->getVar($typeName); $value = $generalObj->getVar($typeName);
if (in_array($typeName, ['dex1', 'dex2', 'dex3', 'dex4', 'dex5'], true)) {
$value = CentennialAllStarGrowthService::recordableValue($generalObj, $typeName);
}
} else if ($valueType === 'rank') { } else if ($valueType === 'rank') {
$value = $generalObj->getRankVar(RankColumn::from($typeName)); $value = $generalObj->getRankVar(RankColumn::from($typeName));
} else if ($valueType === 'calc') { } else if ($valueType === 'calc') {
@@ -1668,8 +1672,6 @@ function tryUniqueItemLottery(RandUtil $rng, General $general, string $acquireTy
$prob = 1 / ($genCount * $itemTypeCnt * 0.7 / 3); // 투표율 70%, 설문조사 한번에 2~3개 등장 $prob = 1 / ($genCount * $itemTypeCnt * 0.7 / 3); // 투표율 70%, 설문조사 한번에 2~3개 등장
} else if ($acquireType == '랜덤 임관') { } else if ($acquireType == '랜덤 임관') {
$prob = 1 / ($genCount * $itemTypeCnt / 10 / 2); // 랜임시 2개(10%) 등장(200명중 20명 랜임시도?) $prob = 1 / ($genCount * $itemTypeCnt / 10 / 2); // 랜임시 2개(10%) 등장(200명중 20명 랜임시도?)
} else if ($acquireType == '건국') {
$prob = 1 / ($genCount * $itemTypeCnt / 10 / 4); // 건국시 4개(20%) 등장(200명시 20국 정도 됨)
} }
$prob *= GameConst::$uniqueTrialCoef; $prob *= GameConst::$uniqueTrialCoef;
@@ -1683,6 +1685,9 @@ function tryUniqueItemLottery(RandUtil $rng, General $general, string $acquireTy
//포인트로 랜덤 유니크 획득 //포인트로 랜덤 유니크 획득
$prob = 1; $prob = 1;
} }
else if ($acquireType == '건국') {
$prob = 1; // 건국시 100%
}
foreach (Util::range($maxCnt) as $_idx) { foreach (Util::range($maxCnt) as $_idx) {
if ($rng->nextBool($prob)) { if ($rng->nextBool($prob)) {
+15
View File
@@ -3,6 +3,7 @@ namespace sammo;
use sammo\Command\GeneralCommand; use sammo\Command\GeneralCommand;
use sammo\Command\NationCommand; use sammo\Command\NationCommand;
use sammo\Enums\PenaltyKey;
function getGeneralTurnBrief(General $generalObj, array $turnList) { function getGeneralTurnBrief(General $generalObj, array $turnList) {
$result = []; $result = [];
@@ -434,6 +435,13 @@ function setNationCommand(int $generalID, array $turnList, string $command, ?arr
]; ];
} }
if($general->hasPenalty(PenaltyKey::NoChiefTurnInput)){
return [
'result'=>false,
'reason'=>'수뇌 턴 입력 불가능'
];
}
try{ try{
$commandObj = buildNationCommandClass($command, $general, $env, $general->getLastTurn(), $arg); $commandObj = buildNationCommandClass($command, $general, $env, $general->getLastTurn(), $arg);
} }
@@ -473,6 +481,13 @@ function setNationCommand(int $generalID, array $turnList, string $command, ?arr
} }
_setNationCommand($commandObj, $turnList); _setNationCommand($commandObj, $turnList);
if($general->getNPCType() < 2){
//유저장인 경우에는 턴 리필
$general->updateVarWithLimit('killturn', $env['killturn'], $general->getVar('killturn'));
$general->applyDB($db);
}
return [ return [
'result'=>true, 'result'=>true,
'brief'=>$commandObj->getBrief(), 'brief'=>$commandObj->getBrief(),
+24 -1
View File
@@ -457,6 +457,29 @@ function buildWarUnitTriggerClass(?string $type, WarUnit $unit, ?array $args = n
return $class->newInstanceArgs(array_merge([$unit], $args)); return $class->newInstanceArgs(array_merge([$unit], $args));
} }
function getStaticEventClass(string $type){
static $basePath = __NAMESPACE__.'\\StaticEvent\\';
$classPath = ($basePath.$type);
if(class_exists($classPath)){
return $classPath;
}
throw new \InvalidArgumentException("{$type}은 StaticEvent가 아님");
}
function buildStaticEventClass(?string $type):BaseStaticEvent{
static $cache = [];
if(key_exists($type, $cache)){
return $cache[$type];
}
$class = getStaticEventClass($type);
$obj = new $class();
$cache[$type] = $obj;
return $obj;
}
function getGeneralPoolClass(string $type){ function getGeneralPoolClass(string $type){
static $basePath = __NAMESPACE__.'\\GeneralPool\\'; static $basePath = __NAMESPACE__.'\\GeneralPool\\';
$classPath = ($basePath.$type); $classPath = ($basePath.$type);
@@ -663,7 +686,7 @@ function TechLimit($startYear, $year, $tech) : bool {
$relYear = $year - $startYear; $relYear = $year - $startYear;
$relMaxTech = Util::valueFit( $relMaxTech = Util::valueFit(
floor($relYear / 5) + 1, floor($relYear / GameConst::$techLevelIncYear) + GameConst::$initialAllowedTechLevel,
1, 1,
GameConst::$maxTechLevel GameConst::$maxTechLevel
); );
+3 -3
View File
@@ -372,7 +372,7 @@ function postUpdateMonthly(RandUtil $rng)
continue; continue;
} }
//양측 기간 모두 0이 되는 상황이면 //양측 기간 모두 0이 되는 상황이면
$nation1 = getNationStaticInfo($me); $nation1 = getNationStaticInfo($me);
$name1 = $nation1['name']; $name1 = $nation1['name'];
$nation2 = getNationStaticInfo($you); $nation2 = getNationStaticInfo($you);
@@ -381,7 +381,7 @@ function postUpdateMonthly(RandUtil $rng)
$josaWa = JosaUtil::pick($name1, '와'); $josaWa = JosaUtil::pick($name1, '와');
$josaYi = JosaUtil::pick($name2, '이'); $josaYi = JosaUtil::pick($name2, '이');
$globalLogger->pushGlobalHistoryLog("<R><b>【전】</b></><D><b>$name1</b></>{$josaWa} <D><b>$name2</b></>{$josaYi} <S>전</>합니다."); $globalLogger->pushGlobalHistoryLog("<R><b>【전】</b></><D><b>$name1</b></>{$josaWa} <D><b>$name2</b></>{$josaYi} <S>전</>합니다.");
$db->update('diplomacy', [ $db->update('diplomacy', [
'state' => 2, 'state' => 2,
'term' => 0, 'term' => 0,
@@ -559,7 +559,7 @@ function checkStatistic()
$nationHist = ''; $nationHist = '';
foreach (GameConst::$availableNationType as $nationType) { foreach (GameConst::$availableNationType as $nationType) {
if (!Util::array_get($nationHists[$nationType])) { if (!($nationHists[$nationType] ?? null)) {
$nationHists[$nationType] = '-'; $nationHists[$nationType] = '-';
} }
$nationHist .= getNationType($nationType) . "({$nationHists[$nationType]}), "; $nationHist .= getNationType($nationType) . "({$nationHists[$nationType]}), ";
+2 -2
View File
@@ -138,7 +138,7 @@ function JSCitiesBasedOnDistance(int $cityNo, int $maxDistance = 1): array{
$distanceList = searchDistance($cityNo, $maxDistance, true); $distanceList = searchDistance($cityNo, $maxDistance, true);
$result = []; $result = [];
for ($dist = 1; $dist <= $maxDistance; $dist++) { for ($dist = 1; $dist <= $maxDistance; $dist++) {
$result[$dist] = Util::array_get($distanceList[$dist], []); $result[$dist] = $distanceList[$dist] ?? [];
} }
return $result; return $result;
} }
@@ -152,7 +152,7 @@ function printCitiesBasedOnDistance(int $cityNo, int $maxDistance = 1): string
for ($dist = 1; $dist <= $maxDistance; $dist++) { for ($dist = 1; $dist <= $maxDistance; $dist++) {
$cityList = array_map(function ($cityID) { $cityList = array_map(function ($cityID) {
return CityConst::byID($cityID)->name; return CityConst::byID($cityID)->name;
}, Util::array_get($distanceList[$dist], [])); }, $distanceList[$dist] ?? []);
$cityStr = join(', ', $cityList); $cityStr = join(', ', $cityList);
+7 -3
View File
@@ -1153,21 +1153,25 @@ function fight($tnmt_type, $tnmt, $phs, $group, $g1, $g2, $type)
]; ];
//막판 분노 //막판 분노
$factor1 = 1.0;
$factor2 = 1.0;
$ratio = rand() % 300; $ratio = rand() % 300;
if ($e1 / 5 > $energy1 && $damage1 > $damage2 && $gen1[$tp] >= $ratio) { if ($e1 / 5 > $energy1 && $damage1 > $damage2 && $gen1[$tp] >= $ratio) {
$damage2 *= Util::round((rand() % 301 + 200) / 100); // 200 ~ 500% $factor2 = Util::round((rand() % 301 + 200) / 100); // 200 ~ 500%
$critical1 = 1; $critical1 = 1;
$str = Util::choiceRandom($crticialSkillMap[$tnmt_type]); $str = Util::choiceRandom($crticialSkillMap[$tnmt_type]);
$log[] = "<S>●</> <Y>{$gen1['name']}</>의 분노의 <M>{$str}</> 공격!"; $log[] = "<S>●</> <Y>{$gen1['name']}</>의 분노의 <M>{$str}</> 공격!";
} }
$ratio = rand() % 300; $ratio = rand() % 300;
if ($e2 / 5 > $energy2 && $damage2 > $damage1 && $gen2[$tp] >= $ratio) { if ($e2 / 5 > $energy2 && $damage2 > $damage1 && $gen2[$tp] >= $ratio) {
$damage1 *= Util::round((rand() % 301 + 200) / 100); // 200 ~ 500% $factor1 = Util::round((rand() % 301 + 200) / 100); // 200 ~ 500%
$critical2 = 1; $critical2 = 1;
$str = Util::choiceRandom($crticialSkillMap[$tnmt_type]); $str = Util::choiceRandom($crticialSkillMap[$tnmt_type]);
$log[] = "<S>●</> <Y>{$gen2['name']}</>의 분노의 <M>{$str}</> 공격!"; $log[] = "<S>●</> <Y>{$gen2['name']}</>의 분노의 <M>{$str}</> 공격!";
} }
$damage1 *= $factor1;
$damage2 *= $factor2;
//1합 승부 //1합 승부
if ($phase == 1) { if ($phase == 1) {
+2 -2
View File
@@ -7,10 +7,10 @@ include "func.php";
$session = Session::requireLogin(); $session = Session::requireLogin();
if ($session->userGrade < 5) { if ($session->userGrade < 6) {
die('관리자 아님'); die('관리자 아님');
} }
if ($session->userGrade == 5) { if ($session->userGrade == 6) {
die('DB 리셋 권한을 가지고 있지 않습니다.'); die('DB 리셋 권한을 가지고 있지 않습니다.');
} }
+1 -1
View File
@@ -18,7 +18,7 @@ else{
$availableNextCall = $session->availableNextCallGetCityList??'2000-01-01 00:00:00'; $availableNextCall = $session->availableNextCallGetCityList??'2000-01-01 00:00:00';
$now = new \DateTimeImmutable(); $now = new \DateTimeImmutable();
if($now <= new \DateTimeImmutable($availableNextCall) && $session->userGrade < 5){ if($now <= new \DateTimeImmutable($availableNextCall) && $session->userGrade < 6){
Json::die([ Json::die([
'result'=>false, 'result'=>false,
'reason'=>"도시 목록은 10초에 한번 갱신 가능합니다.\n다음 시간 : ".$availableNextCall 'reason'=>"도시 목록은 10초에 한번 갱신 가능합니다.\n다음 시간 : ".$availableNextCall
+22 -4
View File
@@ -10,7 +10,23 @@ function sortTokens(&$tokens){
}); });
} }
function putInfoText(&$info){ function putInfoText(&$info, ?array $currentTargetEnv){
if (($info['event100Growth'] ?? false) === true) {
if ($currentTargetEnv === null) {
$displayStats = CentennialAllStarGrowthService::calculateUserInitialStats($info);
$info['selectionStatLabel'] = '시작 능력치';
} else {
$displayStats = CentennialAllStarGrowthService::calculateUserCurrentTargetStats(
$info,
$currentTargetEnv
);
$info['selectionStatLabel'] = '현재 변경 기준 능력치';
}
$info['selectionLeadership'] = $displayStats['leadership'];
$info['selectionStrength'] = $displayStats['strength'];
$info['selectionIntel'] = $displayStats['intel'];
}
if(key_exists('specialDomestic', $info)){ if(key_exists('specialDomestic', $info)){
$class = buildGeneralSpecialDomesticClass($info['specialDomestic']); $class = buildGeneralSpecialDomesticClass($info['specialDomestic']);
$info['specialDomesticName'] = $class->getName(); $info['specialDomesticName'] = $class->getName();
@@ -34,7 +50,8 @@ $now = $oNow->format('Y-m-d H:i:s');
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
$npcmode = $gameStor->getValue('npcmode'); $eventEnv = $gameStor->getValues(['npcmode', 'startyear', 'year', 'month']);
$npcmode = $eventEnv['npcmode'];
if($npcmode!=2){ if($npcmode!=2){
Json::die([ Json::die([
'result'=>false, 'result'=>false,
@@ -43,6 +60,7 @@ if($npcmode!=2){
} }
$rawGeneral = $db->queryFirstRow('SELECT no, aux FROM general WHERE `owner` = %i', $userID); $rawGeneral = $db->queryFirstRow('SELECT no, aux FROM general WHERE `owner` = %i', $userID);
$currentTargetEnv = $rawGeneral ? $eventEnv : null;
if($rawGeneral){ if($rawGeneral){
$generalAux = Json::decode($rawGeneral['aux']); $generalAux = Json::decode($rawGeneral['aux']);
if(key_exists('next_change', $generalAux)&& $generalAux['next_change'] > $now){ if(key_exists('next_change', $generalAux)&& $generalAux['next_change'] > $now){
@@ -62,7 +80,7 @@ if($tokens){
foreach($tokens as $token){ foreach($tokens as $token){
$valid_until = $token['reserved_until']; $valid_until = $token['reserved_until'];
$info = Json::decode($token['info']); $info = Json::decode($token['info']);
putInfoText($info); putInfoText($info, $currentTargetEnv);
$info['uniqueName'] = $token['unique_name']; $info['uniqueName'] = $token['unique_name'];
$pick[] = $info; $pick[] = $info;
} }
@@ -83,7 +101,7 @@ $valid_until = null;
foreach(pickGeneralFromPool($db, $rng, $userID, 14) as $pickObj){ foreach(pickGeneralFromPool($db, $rng, $userID, 14) as $pickObj){
$valid_until = $pickObj->getValidUntil(); $valid_until = $pickObj->getValidUntil();
$info = $pickObj->getInfo(); $info = $pickObj->getInfo();
putInfoText($info); putInfoText($info, $currentTargetEnv);
$pick[] = $info; $pick[] = $info;
} }
sortTokens($pick);//좀 무식하지만.. sortTokens($pick);//좀 무식하지만..
+47 -16
View File
@@ -1,9 +1,12 @@
<?php <?php
namespace sammo; namespace sammo;
use sammo\Enums\GeneralColumn;
use sammo\Enums\GeneralLiteQueryMode; use sammo\Enums\GeneralLiteQueryMode;
use sammo\Enums\GeneralQueryMode; use sammo\Enums\GeneralQueryMode;
use sammo\Enums\MessageType; use sammo\Enums\MessageType;
use sammo\Enums\PenaltyKey;
include "lib.php"; include "lib.php";
include "func.php"; include "func.php";
@@ -21,11 +24,12 @@ $userID = Session::getUserID();
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
$gameStor->cacheValues(['startyear','year','month','scenario']); $gameStor->cacheValues(['startyear', 'year', 'month', 'scenario', 'killturn']);
$me = $db->queryFirstRow('SELECT no,nation,officer_level from general where owner=%i', $userID); $me = $db->queryFirstRow('SELECT no,npc,nation,officer_level,penalty from general where owner=%i', $userID);
$myOfficerLevel = $me['officer_level']; $myOfficerLevel = $me['officer_level'];
$nationID = $me['nation']; $nationID = $me['nation'];
$myPenalty = Json::decode($me['penalty'] ?? '{}');
//수뇌가 아니면 아무것도 할 수 없음 //수뇌가 아니면 아무것도 할 수 없음
if ($myOfficerLevel < 5) { if ($myOfficerLevel < 5) {
@@ -45,8 +49,7 @@ if($action === '추방' && $destGeneralID==0){
if ($destGeneralID == 0) { if ($destGeneralID == 0) {
$general = new DummyGeneral(true); $general = new DummyGeneral(true);
$general->setVar('nation', $nationID); $general->setVar('nation', $nationID);
} } else {
else{
$general = General::createObjFromDB($destGeneralID); $general = General::createObjFromDB($destGeneralID);
if ($general instanceof DummyGeneral) { if ($general instanceof DummyGeneral) {
@@ -71,10 +74,21 @@ if($officerLevel == 12){
]); ]);
} }
function do수뇌임명(General $general, int $targetOfficerLevel):?string{ function do수뇌임명(General $general, int $targetOfficerLevel): ?string
{
global $myPenalty;
if ($myPenalty[PenaltyKey::NoChiefChange->value] ?? false) {
return '수뇌를 임명할 수 없는 상태입니다.';
}
$generalID = $general->getID(); $generalID = $general->getID();
$nationID = $general->getNationID(); $nationID = $general->getNationID();
if($general->hasPenalty(PenaltyKey::NoChief)){
return '수뇌가 될 수 없는 상태입니다.';
}
$db = DB::db(); $db = DB::db();
[$chiefSet, $nationLevel] = $db->queryFirstList('SELECT chief_set,level FROM nation WHERE nation = %i', $nationID); [$chiefSet, $nationLevel] = $db->queryFirstList('SELECT chief_set,level FROM nation WHERE nation = %i', $nationID);
@@ -97,15 +111,11 @@ function do수뇌임명(General $general, int $targetOfficerLevel):?string{
], 'nation=%i AND officer_level=%i AND no!=%i', $nationID, $targetOfficerLevel, $generalID); ], 'nation=%i AND officer_level=%i AND no!=%i', $nationID, $targetOfficerLevel, $generalID);
if ($targetOfficerLevel === 11) { if ($targetOfficerLevel === 11) {
} else if ($targetOfficerLevel % 2 == 0) {
}
else if($targetOfficerLevel % 2 == 0){
if ($general->getVar('strength') < GameConst::$chiefStatMin) { if ($general->getVar('strength') < GameConst::$chiefStatMin) {
return '무력이 부족합니다.'; return '무력이 부족합니다.';
} }
} else {
}
else{
if ($general->getVar('intel') < GameConst::$chiefStatMin) { if ($general->getVar('intel') < GameConst::$chiefStatMin) {
return '지력이 부족합니다.'; return '지력이 부족합니다.';
} }
@@ -122,7 +132,10 @@ function do수뇌임명(General $general, int $targetOfficerLevel):?string{
return null; return null;
} }
function do도시임명(General $general, int $cityID, int $targetOfficerLevel):?string{ function do도시임명(General $general, int $cityID, int $targetOfficerLevel): ?string
{
global $myPenalty;
$nationID = $general->getNationID(); $nationID = $general->getNationID();
$db = DB::db(); $db = DB::db();
@@ -131,6 +144,10 @@ function do도시임명(General $general, int $cityID, int $targetOfficerLevel):
return '올바르지 않은 도시입니다'; return '올바르지 않은 도시입니다';
} }
if ($general->getVar(GeneralColumn::officer_level) >= 4 && ($myPenalty[PenaltyKey::NoChiefChange->value] ?? false)) {
return '수뇌인 장수를 변경할 수 없는 상태입니다.';
}
$officerSet = $db->queryFirstField('SELECT officer_set FROM city WHERE nation=%i AND city=%i', $nationID, $cityID); $officerSet = $db->queryFirstField('SELECT officer_set FROM city WHERE nation=%i AND city=%i', $nationID, $cityID);
if ($officerSet === null) { if ($officerSet === null) {
return '아국 도시가 아닙니다'; return '아국 도시가 아닙니다';
@@ -169,12 +186,15 @@ function do도시임명(General $general, int $cityID, int $targetOfficerLevel):
return null; return null;
} }
function do추방(General $general, int $myOfficerLevel):?string{ function do추방(General $general, int $myOfficerLevel): ?string
{
$generalID = $general->getID(); $generalID = $general->getID();
$generalName = $general->getVar('name'); $generalName = $general->getVar('name');
$nationID = $general->getNationID(); $nationID = $general->getNationID();
if ($myPenalty[PenaltyKey::NoBanGeneral->value] ?? false) {
return '추방할 수 없는 상태입니다.';
}
//추방할사람이 외교권자이면 불가 //추방할사람이 외교권자이면 불가
$permission = checkSecretPermission($general->getRaw()); $permission = checkSecretPermission($general->getRaw());
@@ -222,8 +242,7 @@ function do추방(General $general, int $myOfficerLevel):?string{
// 명성/공헌 N*10%감소 // 명성/공헌 N*10%감소
if ($env['year'] <= $env['startyear'] && $general->getNPCType() < 2) { if ($env['year'] <= $env['startyear'] && $general->getNPCType() < 2) {
$general->setVar('makelimit', $oldMakeLimit); $general->setVar('makelimit', $oldMakeLimit);
} } else {
else{
$betrayCnt = $general->getVar('betray'); $betrayCnt = $general->getVar('betray');
$general->addExperience(-$general->getVar('experience') * 0.15 * $betrayCnt); $general->addExperience(-$general->getVar('experience') * 0.15 * $betrayCnt);
$general->addDedication(-$general->getVar('dedication') * 0.15 * $betrayCnt); $general->addDedication(-$general->getVar('dedication') * 0.15 * $betrayCnt);
@@ -323,6 +342,10 @@ if($action == "임명") {
'reason' => $result 'reason' => $result
]); ]);
} }
//수뇌의 삭턴 리필
$db->update('general', [
'killturn' => $db->sqleval('GREATEST(%b, %i)', 'killturn', $gameStor->getValue('killturn')),
], 'no=%i', $me['no']);
Json::die([ Json::die([
'result' => true, 'result' => true,
'reason' => 'success' 'reason' => 'success'
@@ -337,6 +360,10 @@ if($action == "임명") {
'reason' => $result 'reason' => $result
]); ]);
} }
//수뇌의 삭턴 리필
$db->update('general', [
'killturn' => $db->sqleval('GREATEST(%b, %i)', 'killturn', $gameStor->getValue('killturn')),
], 'no=%i', $me['no']);
Json::die([ Json::die([
'result' => true, 'result' => true,
'reason' => 'success' 'reason' => 'success'
@@ -357,6 +384,10 @@ if($action == "추방") {
'reason' => $result 'reason' => $result
]); ]);
} }
//수뇌의 삭턴 리필
$db->update('general', [
'killturn' => $db->sqleval('GREATEST(%b, %i)', 'killturn', $gameStor->getValue('killturn')),
], 'no=%i', $me['no']);
Json::die([ Json::die([
'result' => true, 'result' => true,
'reason' => 'success' 'reason' => 'success'
+1 -1
View File
@@ -8,7 +8,7 @@ include('func.php');
$session = Session::requireLogin([])->setReadOnly(); $session = Session::requireLogin([])->setReadOnly();
if(Session::getInstance()->userGrade < 5){ if(Session::getInstance()->userGrade < 6){
Json::die([ Json::die([
'reason'=>'권한이 부족합니다.' 'reason'=>'권한이 부족합니다.'
]); ]);
+1 -1
View File
@@ -41,7 +41,7 @@ $penaltyInfo = array_merge($memberPenalty['any'] ?? [], $memberPenalty[DB::prefi
$penalty = []; $penalty = [];
foreach($penaltyInfo as $penaltyKey => $penaltyValue){ foreach($penaltyInfo as $penaltyKey => $penaltyValue){
if($penaltyValue['expire'] ?? 0 > TimeUtil::now()){ if(($penaltyValue['expire'] ?? 0) > TimeUtil::now()){
$penalty[$penaltyKey] = $penaltyValue['value']; $penalty[$penaltyKey] = $penaltyValue['value'];
} }
} }
+78 -6
View File
@@ -8,10 +8,20 @@ WebUtil::requireAJAX();
$pick = Util::getPost('pick'); $pick = Util::getPost('pick');
$leadership = Util::getPost('leadership', 'int', GameConst::$defaultStatMin); $leadership = Util::getPost('leadership', 'int', GameConst::$defaultStatMin);
$strength = Util::getPost('leadership', 'int', GameConst::$defaultStatMin); $isCentennialAllStar = CentennialAllStarGrowthService::isActive();
$intel = Util::getPost('leadership', 'int', GameConst::$defaultStatMin); $strength = Util::getPost(
$isCentennialAllStar ? 'strength' : 'leadership',
'int',
GameConst::$defaultStatMin
);
$intel = Util::getPost(
$isCentennialAllStar ? 'intel' : 'leadership',
'int',
GameConst::$defaultStatMin
);
$personal = Util::getPost('personal', 'string', null); $personal = Util::getPost('personal', 'string', null);
$use_own_picture = Util::getPost('use_own_picture', 'bool', false); $use_own_picture = Util::getPost('use_own_picture', 'bool', false);
$pictureSource = Util::getPost('picture_source', 'string', 'selected');
if(!$pick){ if(!$pick){
@@ -60,13 +70,36 @@ if(!$selectInfo){
} }
$selectInfo = Json::decode($selectInfo); $selectInfo = Json::decode($selectInfo);
$ownerInfo = RootDB::db()->queryFirstRow('SELECT `name`,`picture`,`imgsvr` FROM member WHERE `NO`=%i',$userID); $ownerInfo = RootDB::db()->queryFirstRow(
'SELECT `name`,`picture`,`imgsvr`,`grade` FROM member WHERE `NO`=%i',
$userID
);
if(!$ownerInfo){ if(!$ownerInfo){
Json::die([ Json::die([
'result'=>false, 'result'=>false,
'reason'=>'멤버 정보를 가져오지 못했습니다.' 'reason'=>'멤버 정보를 가져오지 못했습니다.'
]); ]);
} }
if ($isCentennialAllStar) {
if (!in_array($pictureSource, ['selected', 'own'], true)) {
Json::die([
'result' => false,
'reason' => '올바르지 않은 전콘 선택입니다.',
]);
}
if ($pictureSource === 'own') {
$canUseOwnPicture = in_array('picture', GameConst::$generalPoolAllowOption, true)
&& $env['show_img_level'] >= 1
&& $ownerInfo['grade'] >= 1
&& $ownerInfo['picture'] !== '';
if (!$canUseOwnPicture) {
Json::die([
'result' => false,
'reason' => '사용할 수 있는 내 전콘이 없습니다.',
]);
}
}
}
$gencount = $db->queryFirstField('SELECT count(`no`) FROM general WHERE npc<2'); $gencount = $db->queryFirstField('SELECT count(`no`) FROM general WHERE npc<2');
@@ -80,12 +113,25 @@ if ($gencount >= $maxgeneral) {
$poolClass = getGeneralPoolClass(GameConst::$targetGeneralPool); $poolClass = getGeneralPoolClass(GameConst::$targetGeneralPool);
/** @var AbsGeneralPool */ /** @var AbsGeneralPool */
if ($isCentennialAllStar) {
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'selectPickedGeneral',
$userID,
$pick
)));
$pickedGeneral = new $poolClass($db, $rng, $selectInfo, $now);
} else {
$pickedGeneral = new $poolClass($db, $selectInfo, $now); $pickedGeneral = new $poolClass($db, $selectInfo, $now);
}
$builder = $pickedGeneral->getGeneralBuilder(); $builder = $pickedGeneral->getGeneralBuilder();
if ($isCentennialAllStar) {
CentennialAllStarGrowthService::prepareInitialUser($builder, $selectInfo);
}
foreach(GameConst::$generalPoolAllowOption as $allowOption){ foreach(GameConst::$generalPoolAllowOption as $allowOption){
if($allowOption == 'stat'){ if($allowOption == 'stat' && !$isCentennialAllStar){
$leadership = Util::valueFit($leadership, GameConst::$defaultStatMin, GameConst::$defaultStatMax); $leadership = Util::valueFit($leadership, GameConst::$defaultStatMin, GameConst::$defaultStatMax);
$strength = Util::valueFit($strength, GameConst::$defaultStatMin, GameConst::$defaultStatMax); $strength = Util::valueFit($strength, GameConst::$defaultStatMin, GameConst::$defaultStatMax);
$intel = Util::valueFit($intel, GameConst::$defaultStatMin, GameConst::$defaultStatMax); $intel = Util::valueFit($intel, GameConst::$defaultStatMin, GameConst::$defaultStatMax);
@@ -98,14 +144,23 @@ foreach(GameConst::$generalPoolAllowOption as $allowOption){
} }
$builder->setStat($leadership, $strength, $intel); $builder->setStat($leadership, $strength, $intel);
} }
else if($allowOption == 'picture' && $use_own_picture){ else if(
$allowOption == 'picture'
&& (
(!$isCentennialAllStar && $use_own_picture)
|| ($isCentennialAllStar && $pictureSource === 'own')
)
){
$builder->setPicture($ownerInfo['imgsvr'], $ownerInfo['picture']); $builder->setPicture($ownerInfo['imgsvr'], $ownerInfo['picture']);
} }
else if($allowOption == 'ego'){ else if($allowOption == 'ego'){
if(!$personal || $personal == 'Random'){ if(!$personal || $personal == 'Random'){
$personal = Util::choiceRandom(GameConst::$availablePersonality); $personal = Util::choiceRandom(GameConst::$availablePersonality);
} }
if(!array_search($personal, GameConst::$availablePersonality)){ $invalidPersonal = $isCentennialAllStar
? !in_array($personal, GameConst::$availablePersonality, true)
: !array_search($personal, GameConst::$availablePersonality);
if($invalidPersonal){
Json::die([ Json::die([
'result'=>false, 'result'=>false,
'reason'=>'올바르지 않은 성격입니다.' 'reason'=>'올바르지 않은 성격입니다.'
@@ -123,6 +178,23 @@ $builder->setKillturn(5);
$builder->setNPCType(0); $builder->setNPCType(0);
$builder->setAuxVar('next_change', TimeUtil::nowAddMinutes(12 * $env['turnterm'])); $builder->setAuxVar('next_change', TimeUtil::nowAddMinutes(12 * $env['turnterm']));
$builder->fillRemainSpecAsZero($env); $builder->fillRemainSpecAsZero($env);
if ($isCentennialAllStar) {
$candidateCities = $db->queryFirstColumn(
'SELECT city FROM city WHERE level >= 5 AND level <= 6 AND nation = 0'
);
if (!$candidateCities) {
$candidateCities = $db->queryFirstColumn(
'SELECT city FROM city WHERE level >= 5 AND level <= 6'
);
}
if (!$candidateCities) {
Json::die([
'result' => false,
'reason' => '장수를 생성할 소·중성이 없습니다.',
]);
}
$builder->setCityID($rng->choice($candidateCities));
}
$builder->build($env); $builder->build($env);
$generalID = $builder->getGeneralID(); $generalID = $builder->getGeneralID();
if(!$generalID){ if(!$generalID){
+28 -6
View File
@@ -1,4 +1,5 @@
<?php <?php
namespace sammo; namespace sammo;
include "lib.php"; include "lib.php";
@@ -26,8 +27,7 @@ if ($defence_train <= 40) {
if ($defence_train <= 90) { if ($defence_train <= 90) {
$defence_train = Util::round($defence_train, -1); $defence_train = Util::round($defence_train, -1);
} } else {
else{
$defence_train = 999; $defence_train = 999;
} }
@@ -49,8 +49,7 @@ if($defence_train !== $me->getVar('defence_train')){
$affectedAtmos = $me->onCalcDomestic('changeDefenceTrain', "atmos{$defence_train}", $affectedAtmos); $affectedAtmos = $me->onCalcDomestic('changeDefenceTrain', "atmos{$defence_train}", $affectedAtmos);
$me->increaseVarWithLimit('train', $affectedTrain, 20, GameConst::$maxTrainByWar); $me->increaseVarWithLimit('train', $affectedTrain, 20, GameConst::$maxTrainByWar);
$me->increaseVarWithLimit('atmos', $affectedAtmos, 20, GameConst::$maxAtmosByWar); $me->increaseVarWithLimit('atmos', $affectedAtmos, 20, GameConst::$maxAtmosByWar);
} } else {
else{
$me->increaseVar('myset', -1); $me->increaseVar('myset', -1);
$me->setVar('defence_train', $defence_train); $me->setVar('defence_train', $defence_train);
} }
@@ -65,13 +64,36 @@ if($me->getNPCType() == 1 && $detachNPC){
if ($turnterm < 10) { if ($turnterm < 10) {
$targetKillTurn = 30 / $turnterm; $targetKillTurn = 30 / $turnterm;
} } else {
else{
$targetKillTurn = 60 / $turnterm; $targetKillTurn = 60 / $turnterm;
} }
$me->setVar('killturn', $targetKillTurn); $me->setVar('killturn', $targetKillTurn);
} }
$rootDB = RootDB::db();
//회원 테이블에서 정보확인
$member = $rootDB->queryFirstRow('SELECT `no`, id, picture, grade, `name`, imgsvr, `penalty` FROM member WHERE no=%i', $userID);
if (!$member) {
//이런 경우가 있나?
//TODO: 로그 남겨둘 것
Json::die([
'result' => false,
'reason' => '회원 정보를 찾을 수 없습니다.'
]);
}
$memberPenalty = Json::decode($member['penalty'] ?? "{}");
$penaltyInfo = array_merge($memberPenalty['any'] ?? [], $memberPenalty[DB::prefix()] ?? []);
$penalty = [];
foreach ($penaltyInfo as $penaltyKey => $penaltyValue) {
if (($penaltyValue['expire'] ?? 0) > TimeUtil::now()) {
$penalty[$penaltyKey] = $penaltyValue['value'];
}
}
$me->setVar('penalty', Json::encode($penalty));
$me->applyDB($db); $me->applyDB($db);
Json::die([ Json::die([
+62 -10
View File
@@ -7,6 +7,7 @@ include "func.php";
WebUtil::requireAJAX(); WebUtil::requireAJAX();
$pick = Util::getPost('pick'); $pick = Util::getPost('pick');
$pictureSource = Util::getPost('picture_source', 'string', 'selected');
if(!$pick){ if(!$pick){
Json::die([ Json::die([
@@ -35,10 +36,20 @@ if(!$generalID){
list( list(
$year, $year,
$month, $month,
$startYear,
$maxgeneral, $maxgeneral,
$npcmode, $npcmode,
$turnterm $turnterm,
) = $gameStor->getValuesAsArray(['year', 'month', 'maxgeneral', 'npcmode', 'turnterm']); $showImgLevel
) = $gameStor->getValuesAsArray([
'year',
'month',
'startyear',
'maxgeneral',
'npcmode',
'turnterm',
'show_img_level',
]);
if($npcmode!=2){ if($npcmode!=2){
Json::die([ Json::die([
@@ -55,7 +66,10 @@ if(!$info){
]); ]);
} }
$ownerInfo = RootDB::db()->queryFirstRow('SELECT `name`,`picture`,`imgsvr` FROM member WHERE `NO`=%i',$userID); $ownerInfo = RootDB::db()->queryFirstRow(
'SELECT `name`,`picture`,`imgsvr`,`grade` FROM member WHERE `NO`=%i',
$userID
);
if(!$ownerInfo){ if(!$ownerInfo){
Json::die([ Json::die([
'result'=>false, 'result'=>false,
@@ -64,6 +78,27 @@ if(!$ownerInfo){
} }
$info = Json::decode($info); $info = Json::decode($info);
$isCentennialAllStar = CentennialAllStarGrowthService::isActive();
if ($isCentennialAllStar) {
if (!in_array($pictureSource, ['current', 'own', 'selected'], true)) {
Json::die([
'result' => false,
'reason' => '올바르지 않은 전콘 선택입니다.',
]);
}
if ($pictureSource === 'own') {
$canUseOwnPicture = in_array('picture', GameConst::$generalPoolAllowOption, true)
&& $showImgLevel >= 1
&& $ownerInfo['grade'] >= 1
&& $ownerInfo['picture'] !== '';
if (!$canUseOwnPicture) {
Json::die([
'result' => false,
'reason' => '사용할 수 있는 내 전콘이 없습니다.',
]);
}
}
}
$generalObj = General::createObjFromDB($generalID); $generalObj = General::createObjFromDB($generalID);
@@ -101,18 +136,19 @@ $db->update('select_pool',[
'reserved_until'=>null, 'reserved_until'=>null,
], '(owner=%i or reserved_until < %s) AND general_id is NULL', $userID, $now); ], '(owner=%i or reserved_until < %s) AND general_id is NULL', $userID, $now);
if ($isCentennialAllStar) {
CentennialAllStarGrowthService::prepareLegacyUserReselection($generalObj);
CentennialAllStarGrowthService::applyTarget($generalObj, $info, [
'startyear' => $startYear,
'year' => $year,
'month' => $month,
]);
} else {
if(key_exists('leadership', $info)){ if(key_exists('leadership', $info)){
$generalObj->updateVar('leadership', $info['leadership']); $generalObj->updateVar('leadership', $info['leadership']);
$generalObj->updateVar('strength', $info['strength']); $generalObj->updateVar('strength', $info['strength']);
$generalObj->updateVar('intel', $info['intel']); $generalObj->updateVar('intel', $info['intel']);
} }
if(key_exists('picture', $info)){
$generalObj->updateVar('imgsvr', $info['imgsvr']);
$generalObj->updateVar('picture', $info['picture']);
}
if(key_exists('generalName', $info)){
$generalObj->updateVar('name', $info['generalName']);
}
if(key_exists('dex', $info)){ if(key_exists('dex', $info)){
$generalObj->updateVar('dex1', $info['dex'][0]); $generalObj->updateVar('dex1', $info['dex'][0]);
$generalObj->updateVar('dex2', $info['dex'][1]); $generalObj->updateVar('dex2', $info['dex'][1]);
@@ -129,6 +165,22 @@ if(key_exists('specialDomestic', $info)){
if(key_exists('specialWar', $info)){ if(key_exists('specialWar', $info)){
$generalObj->updateVar('special2', $info['specialWar']); $generalObj->updateVar('special2', $info['specialWar']);
} }
}
if ($isCentennialAllStar) {
if ($pictureSource === 'own') {
$generalObj->updateVar('imgsvr', $ownerInfo['imgsvr']);
$generalObj->updateVar('picture', $ownerInfo['picture']);
} elseif ($pictureSource === 'selected' && key_exists('picture', $info)) {
$generalObj->updateVar('imgsvr', $info['imgsvr']);
$generalObj->updateVar('picture', $info['picture']);
}
} elseif(key_exists('picture', $info)){
$generalObj->updateVar('imgsvr', $info['imgsvr']);
$generalObj->updateVar('picture', $info['picture']);
}
if(key_exists('generalName', $info)){
$generalObj->updateVar('name', $info['generalName']);
}
$generalObj->setAuxVar('next_change', TimeUtil::nowAddMinutes(12 * $turnterm)); $generalObj->setAuxVar('next_change', TimeUtil::nowAddMinutes(12 * $turnterm));
$userNick = $ownerInfo['name']; $userNick = $ownerInfo['name'];
+3 -2
View File
@@ -51,8 +51,9 @@ function MessageBox($str)
function LogText($prefix, $variable) function LogText($prefix, $variable)
{ {
$text = sprintf('%s : %s'."\r\n", $prefix, TVarDumper::dump($variable)); return;
file_put_contents(ROOT.'/d_log/'.UniqueConst::$serverName.'_dbg_logs.txt', $text, FILE_APPEND); //$text = sprintf('%s : %s'."\r\n", $prefix, TVarDumper::dump($variable));
//file_put_contents(ROOT.'/d_log/'.UniqueConst::$serverName.'_dbg_logs.txt', $text, FILE_APPEND);
} }
function prepareDir(string $dirPath, bool $forceCreate=true):bool{ function prepareDir(string $dirPath, bool $forceCreate=true):bool{
-430
View File
@@ -1,430 +0,0 @@
<?php
namespace sammo;
use sammo\DTO\VoteInfo;
use sammo\Enums\GeneralQueryMode;
include "lib.php";
include "func.php";
$session = Session::requireLogin()->loginGame()->setReadOnly();
$userID = Session::getUserID();
increaseRefresh("메인", 1);
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
if (!$userID) {
header('Location:..');
die();
}
//턴 실행.
TurnExecutionHelper::executeAllCommand();
if (!$session->isGameLoggedIn()) {
header('Location:..');
die();
}
$me = $db->queryFirstRow(
'SELECT no,refresh_score,turntime,newmsg,newvote,`officer_level` FROM `general`
LEFT JOIN general_access_log AS l ON `general`.no = l.general_id WHERE owner = %i',
$userID
);
//그새 사망이면
if ($me === null) {
$session->logoutGame();
header('Location: ../');
die();
}
$gameStor->cacheAll(true);
if ($me['newmsg'] == 1 || $me['newvote'] == 1) {
$db->update('general', [
'newmsg' => 0,
'newvote' => 0
], 'owner=%i', $userID);
}
$plock = boolval($db->queryFirstField('SELECT plock FROM plock WHERE `type`="GAME" LIMIT 1'));
$limitState = checkLimit($me['refresh_score']);
if ($limitState >= 2) {
printLimitMsg($me['turntime']);
exit();
}
$generalObj = General::createObjFromDB($me['no'], null, GeneralQueryMode::FullWithAccessLog);
$generalObj->setRawCity($db->queryFirstRow('SELECT * FROM city WHERE city = %i', $generalObj->getCityID()));
$scenario = $gameStor->scenario_text;
$nationID = $generalObj->getNationID();
if ($nationID) {
$nationStor = KVStorage::getStorage($db, $nationID, 'nation_env');
$nationStor->cacheAll();
}
if ($gameStor->extended_general == 0) {
$extend = "표준";
} else {
$extend = "확장";
}
if ($gameStor->fiction == 0) {
$fiction = "사실";
} else {
$fiction = "가상";
}
if ($gameStor->npcmode == 0) {
$npcmode = "불가능";
} else if ($gameStor->npcmode == 1) {
$npcmode = "가능";
} else {
$npcmode = "선택 생성";
}
$color = "cyan";
$serverName = UniqueConst::$serverName;
$serverCnt = $gameStor->server_cnt;
$auctionCount = $db->queryFirstField('SELECT count(*) FROM ng_auction WHERE finished = 0');
$isTournamentActive = $gameStor->tournament > 0;
$isTournamentApplicationOpen = $gameStor->tournament == 1;
$isBettingActive = $gameStor->tournament == 6;
$myNationStatic = getNationStaticInfo($generalObj->getNationID());
$nationColorType = substr($myNationStatic['color'] ?? '#000000', 1);
$autorunUser = ($gameStor->autorun_user) ?? [];
$otherTextInfo = [];
if ($gameStor->join_mode == 'onlyRandom') {
$otherTextInfo[] = '랜덤 임관 전용';
}
if ($autorunUser['limit_minutes'] ?? false) {
$otherTextInfo[] = getAutorunInfo($autorunUser);
}
if (!$otherTextInfo) {
$otherTextInfo = '표준';
} else {
$otherTextInfo = join(', ', $otherTextInfo);
}
$lastVoteID = $gameStor->lastVote;
$lastVote = null;
if ($lastVoteID) {
$voteStor = KVStorage::getStorage($db, 'vote');
$lastVote = VoteInfo::fromArray($voteStor->getValue("vote_{$lastVoteID}"));
if ($lastVote->endDate && $lastVote->endDate < TimeUtil::now()) {
$lastVote = null;
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title><?= $serverName ?>: 메인</title>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=500" />
<?= WebUtil::printJS('../d_shared/common_path.js') ?>
<?= WebUtil::printJS('d_shared/base_map.js') ?>
<?= WebUtil::printCSS('../d_shared/common.css') ?>
<?= WebUtil::printDist('vue', 'v_main', true) ?>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css" />
<?= WebUtil::printCSS('css/map.css') ?>
<?= WebUtil::printStaticValues([
//deprecated, staticValues를 이용할 것
'serverName' => $serverName,
'serverCnt' => $serverCnt,
'serverNick' => DB::prefix(),
'serverID' => UniqueConst::$serverID,
'maxTurn' => GameConst::$maxTurn,
'maxPushTurn' => 12,
'serverNow' => TimeUtil::now(false),
'lastExecuted' => $gameStor->turntime,
'isLocked' => $plock,
'isTournamentActive' => $isTournamentActive,
'isBettingActive' => $isBettingActive,
'staticValues' => [
'serverName' => $serverName,
'serverCnt' => $serverCnt,
'serverNick' => DB::prefix(),
'serverID' => UniqueConst::$serverID,
'maxTurn' => GameConst::$maxTurn,
'maxPushTurn' => 12,
'serverNow' => TimeUtil::now(false),
'lastExecuted' => $gameStor->turntime,
'isLocked' => $plock,
'isTournamentActive' => $isTournamentActive,
'isBettingActive' => $isBettingActive,
'mapName' => GameConst::$mapName,
'unitSet' => GameConst::$unitSet,
]
]) ?>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.1/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
</head>
<body class="img_back sam-color-<?= $nationColorType ?>">
<div id="container" class="bg0">
<div class="row toolbars gx-0">
<div class="buttonPlate"><?= allButton($gameStor->npcmode == 1) ?></button></div>
</div>
<div class="tb_layout row gx-0">
<div id="server_title" class="row py-2 center">
<h3>삼국지 모의전투 HiDCHe <?= $serverName . $serverCnt ?>기 <span class="avoid-wrap">(<font color=cyan><?= $scenario ?></font>)</span></h3>
</div>
<div class="row center gx-0">
<div class="s-border-t col py-2 col-8 col-lg-4" style="color:<?= $color ?>;">
<?= $scenario ?>
</div>
<div class="s-border-t col py-2 col-4 col-lg-2" style="color:<?= $color ?>;">
NPC 수, 상성 : <?= $extend ?>,<?= $fiction ?>
</div>
<div class="s-border-t col py-2 col-4 col-lg-2" style="color:<?= $color ?>;">
NPC선택 : <?= $npcmode ?>
</div>
<div class="s-border-t col py-2 col-4 col-lg-2" style="color:<?= $color ?>;">
토너먼트 : <?= getTournamentTermText($gameStor->turnterm) ?>
</div>
<div class="s-border-t col py-2 col-4 col-lg-2" style="color:<?= $color ?>;">
기타 설정: <?= $otherTextInfo ?>
</div>
<div class="s-border-t col py-2 col-8 col-lg-4"><?= info(2) ?></div>
<div class="s-border-t col py-2 col-4 col-lg-2">전체 접속자 수 : <?= $gameStor->online_user_cnt ?> 명</div>
<div class="s-border-t col py-2 col-4 col-lg-2">턴당 갱신횟수 : <?= $gameStor->refreshLimit ?>회</div>
<div class="s-border-t col py-2 col-8 col-lg-4"><?= info(3) ?></div>
<div class="s-border-t py-2 col col-6 col-lg-4">
<?php if ($isTournamentActive) : ?>
↑<span style='color:cyan'><?= (['전력전', '통솔전', '일기토', '설전',])[$gameStor->tnmt_type] ?? '' ?> <?= getTournament($gameStor->tournament) ?> <?= getTournamentTime() ?></span>↑
<?php else : ?>
<span style='color:magenta'>현재 토너먼트 경기 없음</span>
<?php endif; ?>
</div>
<div class="s-border-t py-2 col col-6 col-lg-2">
<div style="display:inline-block;"><?= $plock ? ("<span style='color:magenta;'>동작 시각: " . substr($gameStor->turntime, 5, 14) . "</span>") : ("<span style='color:cyan;'>동작 시각: " . substr($gameStor->turntime, 5, 14) . "</span>") ?></div>
</div>
<div class="s-border-t py-2 col col-6 col-lg-2">
<?php if ($auctionCount > 0) : ?>
<span style='color:cyan'><?= $auctionCount ?>건</span> 거래 진행중
<?php else : ?>
<span style='color:magenta'>진행중 거래 없음</span>
<?php endif; ?>
</div>
<div class="s-border-t py-2 col col-6 col-lg-4 vote-cell">
<?php
?>
<?php if ($lastVote === null) : ?>
<span style='color:magenta'>진행중 설문 없음</span>
<?php else : ?>
<span style='color:cyan'>설문 진행중</span> : <span><?= $lastVote->title ?></span>
<?php endif; ?>
</div>
</div>
<div class="row gx-0">
<div class="col s-border-t px-2 py-2">접속중인 국가: <?= $gameStor->online_nation ?></div>
</div>
<div class="row gx-0">
<div class="col s-border-t px-2 py-2">운영자 메세지 : <span style='color:yellow;'><?= $gameStor->msg ?></span></div>
</div>
<div class="row gx-0">
<div class="col s-border-t py-2" id="nation-msg-position">
<div class="px-2">【 국가방침 】</div>
<div id='nation-msg-box'>
<div id='nation-msg'><?= nationMsg($generalObj) ?></div>
</div>
</div>
</div>
<div class="row gx-0">
<div class="col s-border-t px-2 py-2">
【 접속자 】<?= onlinegen($generalObj) ?>
</div>
</div>
<?php if ($session->userGrade >= 5) : ?>
<div class="row">
<div class="col">
<a href='_admin1.php' target='_blank'><button type='button'>게임관리</button></a>
<a href='_admin2.php' target='_blank'><button type='button'>회원관리</button></a>
<a href='_admin4.php' target='_blank'><button type='button'>멀티관리</button></a>
<a href='_admin5.php' target='_blank'><button type='button'>일제정보</button></a>
<a href='_admin7.php' target='_blank'><button type='button'>로그정보</button></a>
<a href='_admin8.php' target='_blank'><button type='button'>외교정보</button></a>
<a href='_119.php' target='_blank'><button type='button'>119</button></a>
</div>
</div>
<?php elseif ($session->userGrade == 4) : ?>
<div class="row">
<div class="col">
<a href='_119.php' target='_blank'><button type='button'>119</button></a>
</div>
</div>
<?php endif; ?>
<div id="map_view" class="gx-0">
<div id="mapZone" class="view-item"><?= getMapHtml() ?></div>
<div class="view-item" id="reservedCommandZone">
<div id="reservedCommandList"></div>
<div id="actionMiniPlate" class="gx-0 row">
<div class="col">
<div class="gx-1 row">
<div class="col-8 d-grid"><button type='button' class='btn btn-sammo-base2 refreshPage'>갱 신</button></div>
<div class="col-4 d-grid"><button type='button' class='btn btn-sammo-base2' onclick="location.replace('../')">로비로</button></div>
</div>
</div>
</div>
</div>
<div id="cityInfo" class="view-item" style="border:none;text-align:center;"><?= cityInfo($generalObj) ?></div>
<div id="nation-position"><?php myNationInfo($generalObj); ?></div>
<div id="general-position"><?php generalInfo($generalObj); ?></div>
<div id="generalCommandButton" class="row gx-0">
<div class="buttonPlate bg2">
?></div>
</div>
</div>
</div>
<div id="actionMiniPlateSub" class="gx-0 row">
<div class="col">
<div class="gx-1 row">
<div class="col-3 d-grid"><button type='button' class="btn btn-dark" onclick="scrollHardTo('reservedCommandList')">명령으로</button></div>
<div class="col-5 d-grid"><button type='button' class='btn btn-sammo-base2 refreshPage'>갱 신</button></div>
<div class="col-4 d-grid"><button type='button' class='btn btn-sammo-base2' onclick="location.replace('../')">로비로</button></div>
</div>
</div>
</div>
<div class="row gx-0">
<div class="col-lg-6" id="general_public_record-position">
<div class="bg1 center s-border-tb"><b>장수 동향</b></div>
<div id="general_public_record" style="text-align:left;"><?= formatHistoryToHTML(getGlobalActionLogRecent(15)) ?></div>
</div>
<div class="col-lg-6" id="general_log-position">
<div class="bg1 center s-border-tb"><b>개인 기록</b></div>
<div id="general_log" style="text-align:left;"><?= formatHistoryToHTML(getGeneralActionLogRecent($me['no'], 15)) ?></div>
</div>
<div class="col-12" id="world_history-position">
<div class="bg1 center s-border-tb"><b>중원 정세</b></div>
<div id="world_history" colspan=2 style="text-align:left;"><?= formatHistoryToHTML(getGlobalHistoryLogRecent(15)) ?></div>
</div>
</div>
<div class="row toolbars gx-0">
<div class="buttonPlate"><?= allButton($gameStor->npcmode == 1) ?></div>
<div id="message_board" class="row gx-0">
<div class="message_input_form bg0 gx-0 row">
<div id="mailbox_list-col" class="col-6 col-lg-2 d-grid">
<select id="mailbox_list" size="1" class="form-control bg-dark text-white">
</select>
</div>
<div id="msg_input-col" class="col-12 col-lg-8 d-grid">
<input type="text" id="msg_input" maxlength="99" class="form-control">
</div>
<div id="msg_submit-col" class="col-6 col-lg-2 d-grid"><button id="msg_submit" class="btn btn-primary">서신전달&amp;갱신</button></div>
</div>
<div class="col-lg-6 board_side bg0"><a id="public_talk_position"></a>
<div class="board_header bg0">전체 메시지(최고99자)</div>
<section class="public_message">
<div class="d-grid"><button type="button" class="load_old_message btn btn-secondary" data-msg_type="public">이전 메시지 불러오기</button></div>
</section>
</div>
<div class="col-lg-6 board_side bg0"><a id="national_talk_position"></a>
<div class="board_header bg0">국가 메시지(최고99자)</div>
<section class="national_message">
<div class="d-grid"><button type="button" class="load_old_message btn btn-secondary" data-msg_type="national">이전 메시지 불러오기</button></div>
</section>
</div>
<div class="col-lg-6 board_side bg0"><a id="private_talk_position"></a>
<div class="board_header bg0">개인 메시지(최고99자)</div>
<section class="private_message">
<div class="d-grid"><button type="button" class="load_old_message btn btn-secondary" data-msg_type="private">이전 메시지 불러오기</button></div>
</section>
</div>
<div class="col-lg-6 board_side bg0"><a id="diplomacy_talk_position"></a>
<div class="board_header bg0">외교 메시지(최고99자)</div>
<section class="diplomacy_message">
<div class="d-grid"><button type="button" class="load_old_message btn btn-secondary" data-msg_type="diplomacy">이전 메시지 불러오기</button></div>
</section>
</div>
</div>
<div class="row toolbars gx-0">
<div class="buttonPlate"><?= allButton($gameStor->npcmode == 1) ?></div><?= banner() ?>
</div>
</div>
<?php
if ($limitState == 1) {
MessageBox("접속제한이 얼마 남지 않았습니다!");
}
if ($me['newmsg'] == 1) {
MessageBox("새로운 서신이 도착했습니다!");
}
if ($me['newvote'] == 1) {
$develcost = $gameStor->develcost * 5;
MessageBox("설문조사에 참여하시면 금{$develcost}과 유니크템을 드립니다! (우측 상단 설문조사 메뉴)");
}
?>
</div>
<nav class="navbar navbar-expand fixed-bottom navbar-dark bg-dark d-sm-block d-lg-none p-0" id="navbar500">
<div class="container-fluid px-0">
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto mx-auto">
<li class="nav-item dropup">
<div class="dropdown-toggle text-white btn btn-sammo-base2" id="navbarGlobal" role="button" data-bs-toggle="dropdown" aria-expanded="false">
외부 메뉴
</div>
<ul class="dropdown-menu dropdown-menu-start" aria-labelledby="navbarGlobal" id="navbarGlobalItems">
<?= allButton($gameStor->npcmode == 1, ['btnBegin' => '<li>', 'btnEnd' => '</li>', 'btnClass' => 'dropdown-item']) ?>
</ul>
</li>
<li class="nav-item dropup">
<div class="dropdown-toggle btn btn-sammo-nation" id="navbarNation" role="button" data-bs-toggle="dropdown" aria-expanded="false">
국가 메뉴
</div>
<ul class="dropdown-menu" aria-labelledby="navbarNation" id="navbarNationItems">
</ul>
</li>
<li class="nav-item dropup">
<div class="dropdown-toggle text-white btn btn-dark" id="navbarQuick" role="button" data-bs-toggle="dropdown" aria-expanded="false">
빠른 이동
</div>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown" id="navbarQuickItems">
<li><a class="dropdown-item disabled">국가 정보</a></button></li>
<hr class="dropdown-divider">
</hr>
<li><button type="button" onclick="scrollHardTo('nation-msg-position')" class="dropdown-item">방침</button></li>
<li><button type="button" onclick="scrollHardTo('reservedCommandList')" class="dropdown-item">명령</button></li>
<li><button type="button" onclick="scrollHardTo('nation-position')" class="dropdown-item">국가</button></li>
<li><button type="button" onclick="scrollHardTo('general-position')" class="dropdown-item">장수</button></li>
<li><button type="button" onclick="scrollHardTo('cityInfo')" class="dropdown-item">도시</button></li>
<li><a class="dropdown-item disabled">동향 정보</a></button></li>
<hr class="dropdown-divider">
</hr>
<li><button type="button" onclick="scrollHardTo('mapZone')" class="dropdown-item">지도</button></li>
<li><button type="button" onclick="scrollHardTo('general_public_record-position')" class="dropdown-item">동향</button></li>
<li><button type="button" onclick="scrollHardTo('general_log-position')" class="dropdown-item">개인</button></li>
<li><button type="button" onclick="scrollHardTo('world_history-position')" class="dropdown-item">정세</button></li>
<li><span>&nbsp;</span></li>
<li><a class="dropdown-item disabled">메시지</a></button></li>
<hr class="dropdown-divider">
</hr>
<li><button type="button" onclick="scrollHardTo('public_talk_position')" class="dropdown-item">전체</button></li>
<li><button type="button" onclick="scrollHardTo('national_talk_position')" class="dropdown-item">국가</button></li>
<li><button type="button" onclick="scrollHardTo('private_talk_position')" class="dropdown-item">개인</button></li>
<li><button type="button" onclick="scrollHardTo('diplomacy_talk_position')" class="dropdown-item">외교</button></li>
<li><button type='button' class='btn btn-sammo-base2' onclick="location.replace('../')">로비로</button></li>
</ul>
</li>
<li class="nav-item">
<a class="refreshPage btn btn-sammo-base2 text-white" role="button">갱신</a>
</li>
</ul>
</div>
</div>
</nav>
</body>
</html>
+9 -1
View File
@@ -8,4 +8,12 @@ $session = Session::getInstance()->setReadOnly();
$db = DB::db(); $db = DB::db();
TurnExecutionHelper::executeAllCommand(); $updated = false;
$locked = false;
$lastExecuted = TurnExecutionHelper::executeAllCommand($updated, $locked);
Json::die([
'result' => true,
'updated' => $updated,
'locked' => $locked,
'lastExecuted' => $lastExecuted,
]);
+113 -10
View File
@@ -230,6 +230,7 @@ function processWar_NG(
WarUnitGeneral $attacker, WarUnitGeneral $attacker,
callable $getNextDefender, callable $getNextDefender,
WarUnitCity $city, WarUnitCity $city,
?callable $trace = null,
): bool { ): bool {
$templates = new \League\Plates\Engine(__DIR__ . '/templates'); $templates = new \League\Plates\Engine(__DIR__ . '/templates');
@@ -245,6 +246,26 @@ function processWar_NG(
/** @var WarUnit */ /** @var WarUnit */
$defender = ($getNextDefender)(null, true); $defender = ($getNextDefender)(null, true);
$conquerCity = false; $conquerCity = false;
$traceSeq = 0;
$emitTrace = function (string $event, ?WarUnit $currentDefender, array $details = []) use (
$trace,
&$traceSeq,
$attacker,
$city
): void {
if ($trace === null) {
return;
}
$trace([
'seq' => $traceSeq++,
'event' => $event,
'attacker' => buildWarTraceUnitSnapshot($attacker),
'defender' => $currentDefender === null ? null : buildWarTraceUnitSnapshot($currentDefender),
'city' => buildWarTraceUnitSnapshot($city),
'details' => $details,
]);
};
$emitTrace('battle_start', $defender, ['seed' => $warSeed]);
$josaRo = JosaUtil::pick($city->getName(), '로'); $josaRo = JosaUtil::pick($city->getName(), '로');
$josaYi = JosaUtil::pick($attacker->getName(), '이'); $josaYi = JosaUtil::pick($attacker->getName(), '이');
@@ -277,6 +298,7 @@ function processWar_NG(
$logger->pushGlobalHistoryLog("<M><b>【패퇴】</b></><D><b>{$defender->getNationVar('name')}</b></>{$josaYi} 병량 부족으로 <G><b>{$defender->getName()}</b></>{$josaUl} 뺏기고 말았습니다."); $logger->pushGlobalHistoryLog("<M><b>【패퇴】</b></><D><b>{$defender->getNationVar('name')}</b></>{$josaYi} 병량 부족으로 <G><b>{$defender->getName()}</b></>{$josaUl} 뺏기고 말았습니다.");
$conquerCity = true; $conquerCity = true;
$emitTrace('supply_retreat', $defender);
break; break;
} }
} }
@@ -332,18 +354,34 @@ function processWar_NG(
$initCaller->merge($defender->getGeneral()->getBattleInitSkillTriggerList($defender)); $initCaller->merge($defender->getGeneral()->getBattleInitSkillTriggerList($defender));
$initCaller->fire($attacker->rng, [], [$attacker, $defender]); $initCaller->fire($attacker->rng, [], [$attacker, $defender]);
$emitTrace('opponent_initialized', $defender);
} }
$attacker->beginPhase(); $attacker->beginPhase();
$defender->beginPhase(); $defender->beginPhase();
if ($trace !== null) {
$emitTrace('phase_power', $defender, [
'attackerAttack' => $attacker->getComputedAttack(),
'attackerDefence' => $attacker->getComputedDefence(),
'attackerTrain' => $attacker->getComputedTrain(),
'attackerAtmos' => $attacker->getComputedAtmos(),
'defenderAttack' => $defender->getComputedAttack(),
'defenderDefence' => $defender->getComputedDefence(),
'defenderTrain' => $defender->getComputedTrain(),
'defenderAtmos' => $defender->getComputedAtmos(),
]);
}
$battleCaller = $attacker->getGeneral()->getBattlePhaseSkillTriggerList($attacker); $battleCaller = $attacker->getGeneral()->getBattlePhaseSkillTriggerList($attacker);
$battleCaller->merge($defender->getGeneral()->getBattlePhaseSkillTriggerList($defender)); $battleCaller->merge($defender->getGeneral()->getBattlePhaseSkillTriggerList($defender));
$battleCaller->fire($attacker->rng, [], [$attacker, $defender]); $battleCaller->fire($attacker->rng, [], [$attacker, $defender]);
$emitTrace('phase_triggered', $defender);
$deadDefender = $attacker->calcDamage(); $deadDefender = $attacker->calcDamage();
$deadAttacker = $defender->calcDamage(); $deadAttacker = $defender->calcDamage();
$rawDeadAttacker = $deadAttacker;
$rawDeadDefender = $deadDefender;
$attackerHP = $attacker->getHP(); $attackerHP = $attacker->getHP();
$defenderHP = $defender->getHP(); $defenderHP = $defender->getHP();
@@ -371,6 +409,14 @@ function processWar_NG(
$attacker->increaseKilled($deadDefender); $attacker->increaseKilled($deadDefender);
$defender->increaseKilled($deadAttacker); $defender->increaseKilled($deadAttacker);
$emitTrace('phase_damage', $defender, [
'rawDeadAttacker' => $rawDeadAttacker,
'rawDeadDefender' => $rawDeadDefender,
'deadAttacker' => $deadAttacker,
'deadDefender' => $deadDefender,
'attackerHpBefore' => $attackerHP,
'defenderHpBefore' => $defenderHP,
]);
if($defender->getPhase() < 0){ if($defender->getPhase() < 0){
$phaseNickname = '先'; $phaseNickname = '先';
@@ -394,8 +440,10 @@ function processWar_NG(
$attacker->addPhase(); $attacker->addPhase();
$defender->addPhase(); $defender->addPhase();
$emitTrace('phase_end', $defender);
if (!$attacker->continueWar($noRice)) { if (!$attacker->continueWar($noRice)) {
$emitTrace('attacker_stopped', $defender, ['noRice' => (bool)$noRice]);
$logWritten = true; $logWritten = true;
$attacker->logBattleResult(); $attacker->logBattleResult();
@@ -420,6 +468,7 @@ function processWar_NG(
} }
if (!$defender->continueWar($noRice)) { if (!$defender->continueWar($noRice)) {
$emitTrace('defender_stopped', $defender, ['noRice' => (bool)$noRice]);
$logWritten = true; $logWritten = true;
$attacker->logBattleResult(); $attacker->logBattleResult();
@@ -462,6 +511,7 @@ function processWar_NG(
$defender->finishBattle(); $defender->finishBattle();
$defender = ($getNextDefender)($defender, true); $defender = ($getNextDefender)($defender, true);
$emitTrace('opponent_switched', $defender);
if ($defender !== null && !($defender instanceof WarUnitGeneral)) { if ($defender !== null && !($defender instanceof WarUnitGeneral)) {
throw new \RuntimeException('다음 수비자를 받아오는데 실패'); throw new \RuntimeException('다음 수비자를 받아오는데 실패');
@@ -497,10 +547,60 @@ function processWar_NG(
} }
($getNextDefender)($defender, false); ($getNextDefender)($defender, false);
$emitTrace('battle_end', $defender, ['conquered' => $conquerCity]);
return $conquerCity; return $conquerCity;
} }
function buildWarTraceUnitSnapshot(WarUnit $unit): array
{
$snapshot = [
'kind' => $unit instanceof WarUnitGeneral ? 'general' : 'city',
'id' => $unit instanceof WarUnitGeneral
? $unit->getGeneral()->getID()
: $unit->getVar('city'),
'name' => $unit->getName(),
'isAttacker' => $unit->isAttacker(),
'crewTypeId' => $unit->getCrewType()->id,
'phase' => $unit->getPhase(),
'realPhase' => $unit->getRealPhase(),
'maxPhase' => $unit->getMaxPhase(),
'hp' => $unit->getHP(),
'rawWarPower' => $unit->getRawWarPower(),
'warPower' => $unit->getWarPower(),
'warPowerMultiplier' => $unit->getWarPowerMultiply(),
'killed' => $unit->getKilled(),
'dead' => $unit->getDead(),
'activatedSkills' => $unit->getActivatedSkillLog(),
];
if ($unit instanceof WarUnitGeneral) {
$general = $unit->getGeneral();
$snapshot['general'] = [
'crew' => $general->getVar('crew'),
'rice' => $general->getVar('rice'),
'train' => $general->getVar('train'),
'atmos' => $general->getVar('atmos'),
'injury' => $general->getVar('injury'),
'experience' => $general->getVar('experience'),
'dedication' => $general->getVar('dedication'),
'dex1' => $general->getVar('dex1'),
'dex2' => $general->getVar('dex2'),
'dex3' => $general->getVar('dex3'),
'dex4' => $general->getVar('dex4'),
'dex5' => $general->getVar('dex5'),
];
} else {
$snapshot['cityState'] = [
'defence' => $unit->getVar('def'),
'wall' => $unit->getVar('wall'),
'population' => $unit->getVar('pop'),
];
}
return $snapshot;
}
function DeleteConflict($nation) function DeleteConflict($nation)
{ {
$db = DB::db(); $db = DB::db();
@@ -526,7 +626,7 @@ function DeleteConflict($nation)
function getConquerNation($city): int function getConquerNation($city): int
{ {
$conflict = Json::decode($city['conflict']); $conflict = Json::decode($city['conflict']);
return Util::array_first_key($conflict); return array_key_first($conflict);
} }
function ConquerCity(array $admin, General $general, array $city, array $defenderCityGeneralList) function ConquerCity(array $admin, General $general, array $city, array $defenderCityGeneralList)
@@ -718,14 +818,17 @@ function ConquerCity(array $admin, General $general, array $city, array $defende
$attackerLogger->pushGlobalHistoryLog("<M><b>【긴급천도】</b></><D><b>{$defenderNationName}</b></>{$josaYi} 수도가 함락되어 <G><b>$minCityName</b></>{$josaRo} 긴급천도하였습니다."); $attackerLogger->pushGlobalHistoryLog("<M><b>【긴급천도】</b></><D><b>{$defenderNationName}</b></>{$josaYi} 수도가 함락되어 <G><b>$minCityName</b></>{$josaRo} 긴급천도하였습니다.");
$moveLog = "수도가 함락되어 <G><b>$minCityName</b></>{$josaRo} <M>긴급천도</>합니다."; $moveLog = "수도가 함락되어 <G><b>$minCityName</b></>{$josaRo} <M>긴급천도</>합니다.";
//아국 수뇌부에게 로그 전달 foreach ($db->queryAllLists(
foreach ($db->queryFirstColumn( 'SELECT no, officer_level FROM general WHERE nation=%i',
'SELECT no FROM general WHERE nation=%i AND officer_level>=5',
$defenderNationID $defenderNationID
) as $defenderChiefID) { ) as [$defenderGeneralID, $defenderGeneralChiefLevel]) {
$chiefLogger = new ActionLogger($defenderChiefID, $defenderNationID, $year, $month); $defenderLogger = new ActionLogger($defenderGeneralID, $defenderNationID, $year, $month);
$chiefLogger->pushGeneralActionLog($moveLog, ActionLogger::PLAIN); $defenderLogger->pushGeneralActionLog($moveLog, ActionLogger::PLAIN);
$chiefLogger->flush(); if($defenderGeneralChiefLevel >= 5){
$defenderLogger->pushGeneralActionLog("수뇌는 <G><b>{$minCityName}</b></>{$josaRo} 집합되었습니다.");
}
$defenderLogger->flush();
} }
//천도 //천도
@@ -782,8 +885,8 @@ function ConquerCity(array $admin, General $general, array $city, array $defende
'officer_set' => 0, 'officer_set' => 0,
]; ];
if ($city['level'] > 3) { if ($city['level'] > 3) {
$query['def'] = 1000; $query['def'] = GameConst::$defaultCityWall;
$query['wall'] = 1000; $query['wall'] = GameConst::$defaultCityWall;
} else { } else {
$query['def'] = $db->sqleval('def_max/2'); $query['def'] = $db->sqleval('def_max/2');
$query['wall'] = $db->sqleval('wall_max/2'); $query['wall'] = $db->sqleval('wall_max/2');
+2 -2
View File
@@ -99,7 +99,7 @@ class Join extends \sammo\BaseAPI
} }
$gameID = UniqueConst::$serverID; $gameID = UniqueConst::$serverID;
$alreadyJoined = $db->queryFirstField('SELECT count(*) FROM `storage` WHERE `namespace` = %s AND `key` LIKE %s', 'inheritance_result', "{$gameID}_{$userID}_%"); $alreadyJoined = $db->queryFirstField('SELECT COUNT(*) FROM `inheritance_result` WHERE `server_id` = %s AND `owner` = %i', $gameID, $userID);
if ($alreadyJoined) { if ($alreadyJoined) {
//이미 받았음 //이미 받았음
return 0; return 0;
@@ -157,7 +157,7 @@ class Join extends \sammo\BaseAPI
$penalty = []; $penalty = [];
foreach($penaltyInfo as $penaltyKey => $penaltyValue){ foreach($penaltyInfo as $penaltyKey => $penaltyValue){
if($penaltyValue['expire'] ?? 0 > TimeUtil::now()){ if(($penaltyValue['expire'] ?? 0) > TimeUtil::now()){
$penalty[$penaltyKey] = $penaltyValue['value']; $penalty[$penaltyKey] = $penaltyValue['value'];
} }
} }
+1 -1
View File
@@ -54,7 +54,7 @@ class GeneralList extends \sammo\BaseAPI
$availableNextCall = $session->availableNextCallGetGeneralList ?? '2000-01-01 00:00:00'; $availableNextCall = $session->availableNextCallGetGeneralList ?? '2000-01-01 00:00:00';
$now = new \DateTimeImmutable(); $now = new \DateTimeImmutable();
if ($now <= new \DateTimeImmutable($availableNextCall) && $session->userGrade < 5) { if ($now <= new \DateTimeImmutable($availableNextCall) && $session->userGrade < 6) {
return "장수 리스트는 10초에 한번 갱신 가능합니다.\n다음 시간 : " . $availableNextCall; return "장수 리스트는 10초에 한번 갱신 가능합니다.\n다음 시간 : " . $availableNextCall;
} }
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace sammo\API\InheritAction;
use sammo\Session;
use DateTimeInterface;
use sammo\DB;
use sammo\Enums\APIRecoveryType;
use sammo\Enums\MessageType;
use sammo\Enums\RankColumn;
use sammo\GameConst;
use sammo\General;
use sammo\KVStorage;
use sammo\Message;
use sammo\MessageTarget;
use sammo\RootDB;
use sammo\TimeUtil;
use sammo\UserLogger;
use sammo\Validator;
use function sammo\GetImageURL;
use function sammo\getNationStaticInfo;
/**
*
* 유산 포인트 1000 포인트를 사용하면 지정한 상대의 본래 유저명을 확인 가능.
* 개인 메시지로 전달되며, 이 기능이 사용되었음을 상대에게도 알림.
*/
class CheckOwner extends \sammo\BaseAPI
{
public function validateArgs(): ?string
{
$v = new Validator($this->args);
$v->rule('required', [
'destGeneralID',
])
->rule('int', 'destGeneralID')
->rule('min', 'destGeneralID', 1);
if (!$v->validate()) {
return $v->errorStr();
}
return null;
}
public function getRequiredSessionMode(): int
{
return static::REQ_GAME_LOGIN;
}
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null|string|array|APIRecoveryType
{
$userID = $session->userID;
$generalID = $session->generalID;
$destGeneralID = $this->args['destGeneralID'];
if ($generalID == $destGeneralID) {
return '자신의 정보는 확인할 수 없습니다.';
}
$general = General::createObjFromDB($generalID);
if ($userID != $general->getVar('owner')) {
return '로그인 상태가 이상합니다. 다시 로그인해 주세요.';
}
$db = DB::db();
$destRawGeneral = $db->queryFirstRow('SELECT no,name,nation,owner,owner_name,imgsvr,picture FROM general WHERE no = %i', $destGeneralID);
if (!$destRawGeneral) {
return '대상 장수가 존재하지 않습니다.';
}
if (!$destRawGeneral['owner']) {
return '대상 장수는 NPC입니다.';
}
$gameStor = KVStorage::getStorage($db, 'game_env');
if ($gameStor->isunited) {
return '이미 천하가 통일되었습니다.';
}
$reqPoint = GameConst::$inheritCheckOwnerPoint;
$inheritStor = KVStorage::getStorage($db, "inheritance_{$userID}");
$previousPoint = ($inheritStor->getValue('previous') ?? [0, 0])[0];
if ($previousPoint < $reqPoint) {
return '충분한 유산 포인트를 가지고 있지 않습니다.';
}
$userLogger = new UserLogger($userID);
$userLogger->push("{$reqPoint} 포인트로 장수 소유자 확인", "inheritPoint");
$userLogger->flush();
$destGeneralName = $destRawGeneral['name'];
$destGeneralOwnerName = $destRawGeneral['owner_name'];
if (!$destGeneralOwnerName) {
$rootDB = RootDB::db();
$destGeneralOwnerName = $rootDB->queryFirstField('SELECT name FROM member WHERE no = %i', $destRawGeneral['owner']) ?? '알수없음';
}
$src = new MessageTarget(0, '', 0, 'System', '#000000');
if (true) {
$staticNation = $general->getStaticNation();
$dest = new MessageTarget(
$generalID,
$general->getName(),
$general->getNationID(),
$staticNation['name'],
$staticNation['color'],
GetImageURL($general->getVar('imgsvr'), $general->getVar('picture'))
);
$msg = new Message(
MessageType::private,
$src,
$dest,
"{$destGeneralName}의 소유자는 {$destGeneralOwnerName} 입니다.",
new \DateTime(),
new \DateTime('9999-12-31'),
[]
);
$msg->send(true);
}
$inheritStor->setValue('previous', [$previousPoint - $reqPoint, null]);
$general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, $reqPoint);
$general->applyDB($db);
if(true){
$destStaticNation = getNationStaticInfo($destRawGeneral['nation']);
$dest = new MessageTarget(
$destGeneralID,
$destGeneralName,
$destRawGeneral['nation'],
$destStaticNation['name'],
$destStaticNation['color'],
GetImageURL($destRawGeneral['imgsvr'], $destRawGeneral['picture'])
);
$msg = new Message(
MessageType::private,
$src,
$dest,
"소유자명이 누군가에 의해 확인되었습니다.",
new \DateTime(),
new \DateTime('9999-12-31'),
[]
);
$msg->send(true);
}
return null;
}
}
+188
View File
@@ -0,0 +1,188 @@
<?php
namespace sammo\API\InheritAction;
use sammo\Session;
use DateTimeInterface;
use sammo\CentennialAllStarGrowthService;
use sammo\DB;
use sammo\Enums\APIRecoveryType;
use sammo\Enums\RankColumn;
use sammo\GameConst;
use sammo\General;
use sammo\KVStorage;
use sammo\LiteHashDRBG;
use sammo\RandUtil;
use sammo\UniqueConst;
use sammo\UserLogger;
use sammo\Util;
use sammo\Validator;
class ResetStat extends \sammo\BaseAPI
{
public function validateArgs(): ?string
{
$v = new Validator($this->args);
$v->rule('required', [
'leadership',
'strength',
'intel',
])
->rule('int', [
'leadership',
'strength',
'intel',
])
->rule('min', [
'leadership',
'strength',
'intel'
], GameConst::$defaultStatMin)
->rule('max', [
'leadership',
'strength',
'intel'
], GameConst::$defaultStatMax)
->rule('integerArray', 'inheritBonusStat');
if (!$v->validate()) {
return $v->errorStr();
}
return null;
}
public function getRequiredSessionMode(): int
{
//General.aux 쓰므로 lock;
return static::REQ_GAME_LOGIN;
}
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType
{
$userID = $session->userID;
$generalID = $session->generalID;
$leadership = $this->args['leadership'];
$strength = $this->args['strength'];
$intel = $this->args['intel'];
$inheritBonusStat = $this->args['inheritBonusStat'] ?? null;
if ($leadership + $strength + $intel != GameConst::$defaultStatTotal) {
return "능력치 총합이 " . GameConst::$defaultStatTotal . "이 아닙니다. 다시 입력해주세요!";
}
if ($inheritBonusStat) {
if (count($inheritBonusStat) != 3) {
return "보너스 능력치가 잘못 지정되었습니다. 다시 입력해주세요!";
}
foreach ($inheritBonusStat as $stat) {
if ($stat < 0) {
return "보너스 능력치가 음수입니다. 다시 입력해주세요!";
}
}
$sum = array_sum($inheritBonusStat);
if ($sum == 0) {
$inheritBonusStat = null;
} else if ($sum < 3 || $sum > 5) {
return "보너스 능력치 합이 잘못 지정되었습니다. 다시 입력해주세요!";
}
}
$general = General::createObjFromDB($generalID);
if ($userID != $general->getVar('owner')) {
return '로그인 상태가 이상합니다. 다시 로그인해 주세요.';
}
$userLogger = new UserLogger($userID);
if($general->getNPCType() != 0){
return 'NPC는 능력치 초기화를 할 수 없습니다.';
}
if (!CentennialAllStarGrowthService::isStatResetAllowed()) {
return '100기 올스타 장수는 능력치 초기화를 사용할 수 없습니다.';
}
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$gameStor->cacheValues([
'isunited',
'season',
]);
if ($gameStor->isunited) {
return '이미 천하가 통일되었습니다.';
}
$userStor = KVStorage::getStorage($db, "user_{$userID}");
$lastUserStatReset = $userStor->getValue('last_stat_reset') ?? [];
$gameSeason = $gameStor->getValue('season');
if (array_search($gameSeason, $lastUserStatReset) !== false) {
return '이번 시즌에 이미 능력치를 초기화하셨습니다.';
}
$inheritStor = KVStorage::getStorage($db, "inheritance_{$userID}");
$previousPoint = ($inheritStor->getValue('previous') ?? [0, 0])[0];
$reqAmount = 0;
if ($inheritBonusStat !== null) {
$reqAmount += GameConst::$inheritBornStatPoint;
}
if ($previousPoint < $reqAmount) {
return '충분한 유산 포인트를 가지고 있지 않습니다.';
}
$userLogger->push("통솔 {$leadership}, 무력 {$strength}, 지력 {$intel} 스탯 재설정", "inheritPoint");
if ($inheritBonusStat) {
$pleadership = $inheritBonusStat[0] ?? 0;
$pstrength = $inheritBonusStat[1] ?? 0;
$pintel = $inheritBonusStat[2] ?? 0;
$userLogger->push("{$reqAmount}로 통솔 {$pleadership}, 무력 {$pstrength}, 지력 {$pintel} 보너스 능력치 적용", "inheritPoint");
} else {
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'ResetStat',
$userID,
)));
$pleadership = 0;
$pstrength = 0;
$pintel = 0;
foreach (Util::range($rng->nextRangeInt(3, 5)) as $statIdx) {
switch ($rng->choiceUsingWeight([$leadership, $strength, $intel])) {
case 0:
$pleadership++;
break;
case 1:
$pstrength++;
break;
case 2:
$pintel++;
break;
}
}
$userLogger->push("통솔 {$pleadership}, 무력 {$pstrength}, 지력 {$pintel} 보너스 능력치 적용", "inheritPoint");
}
$leadership += $pleadership;
$strength += $pstrength;
$intel += $pintel;
$lastUserStatReset[] = $gameSeason;
$general->setVar('leadership', $leadership);
$general->setVar('strength', $strength);
$general->setVar('intel', $intel);
$userLogger->flush();
$inheritStor->setValue('previous', [$previousPoint - $reqAmount, null]);
$userStor->setValue('last_stat_reset', $lastUserStatReset);
$general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, $reqAmount);
$general->applyDB($db);
return null;
}
}
+9 -1
View File
@@ -7,6 +7,7 @@ use DateTimeInterface;
use sammo\DB; use sammo\DB;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\Enums\MessageType; use sammo\Enums\MessageType;
use sammo\Enums\PenaltyKey;
use sammo\Json; use sammo\Json;
use sammo\Message; use sammo\Message;
use sammo\MessageTarget; use sammo\MessageTarget;
@@ -191,6 +192,9 @@ class SendMessage extends \sammo\BaseAPI
// 전체 메세지 // 전체 메세지
if ($mailbox === Message::MAILBOX_PUBLIC) { if ($mailbox === Message::MAILBOX_PUBLIC) {
if($penalty[PenaltyKey::NoSendPublicMsg->value] ?? 0) {
return '공개 메세지를 보낼 수 없습니다.';
}
$msgID = $this->genPublicMessage($src, $text)->send(); $msgID = $this->genPublicMessage($src, $text)->send();
return [ return [
'msgType' => 'public', 'msgType' => 'public',
@@ -231,7 +235,11 @@ class SendMessage extends \sammo\BaseAPI
$lastMsg = new \DateTime($session->lastMsg ?? '0000-00-00'); $lastMsg = new \DateTime($session->lastMsg ?? '0000-00-00');
$msg_interval = $now->getTimestamp() - $lastMsg->getTimestamp(); $msg_interval = $now->getTimestamp() - $lastMsg->getTimestamp();
$msg_min_interval = $penalty['sendPrivateMsgDelay'] ?? 2; if($penalty[PenaltyKey::NoSendPrivateMsg->value] ?? 0) {
return '개인 메세지를 보낼 수 없습니다.';
}
$msg_min_interval = $penalty[PenaltyKey::SendPrivateMsgDelay->value] ?? 2;
if ($msg_interval < $msg_min_interval) { if ($msg_interval < $msg_min_interval) {
return "개인메세지는 {$msg_min_interval}초당 1건만 보낼 수 있습니다!"; return "개인메세지는 {$msg_min_interval}초당 1건만 보낼 수 있습니다!";
} }
+1 -1
View File
@@ -50,7 +50,7 @@ class UploadImage extends \sammo\BaseAPI
} }
$extension = ltrim($contentType, 'image/'); $extension = ltrim($contentType, 'image/');
$validExtensions = ['png', 'jpeg', 'jpg', 'gif', 'webp']; $validExtensions = ['png', 'jpeg', 'jpg', 'gif', 'webp', 'avif'];
if (!in_array(strtolower($extension), $validExtensions)) { if (!in_array(strtolower($extension), $validExtensions)) {
return '지원하지 않는 이미지 파일입니다: ' . $contentType; return '지원하지 않는 이미지 파일입니다: ' . $contentType;
} }
+20 -5
View File
@@ -6,6 +6,9 @@ use sammo\Session;
use DateTimeInterface; use DateTimeInterface;
use sammo\DB; use sammo\DB;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\Enums\GeneralLiteQueryMode;
use sammo\GeneralLite;
use sammo\StaticEventHandler;
class ExitTroop extends \sammo\BaseAPI class ExitTroop extends \sammo\BaseAPI
{ {
@@ -24,23 +27,35 @@ class ExitTroop extends \sammo\BaseAPI
$generalID = $session->generalID; $generalID = $session->generalID;
$db = DB::db(); $db = DB::db();
$troopID = $db->queryFirstField('SELECT troop FROM general WHERE no = %i', $generalID); $me = GeneralLite::createObjFromDB($generalID, ['troop'], GeneralLiteQueryMode::Lite);
if (!$me) {
return '장수 정보를 불러올 수 없습니다.';
}
$troopID = $me->getVar('troop');
if($troopID == 0){ if($troopID == 0){
return '부대에 소속되어 있지 않습니다.'; return '부대에 소속되어 있지 않습니다.';
} }
if($generalID != $troopID){ if($troopID != $generalID){
$db->update('general', [ StaticEventHandler::handleEvent($me, null, $this::class, [
'troop' => 0, "isTroopLeader" => false,
], '`no` = %i', $generalID); ], $this->args);
$me->setVar('troop', 0);
$me->applyDB($db);
return null; return null;
} }
//부대장이다. //부대장이다.
StaticEventHandler::handleEvent($me, null, $this::class, [
"isTroopLeader" => true,
], $this->args);
$db->update('general', [ $db->update('general', [
'troop' => 0, 'troop' => 0,
], '`troop` = %i', $troopID); ], '`troop` = %i', $troopID);
$db->delete('troop', 'troop_leader = %i', $troopID); $db->delete('troop', 'troop_leader = %i', $troopID);
$me->setVar('troop', 0);
$me->applyDB($db);
return null; return null;
} }
+16 -8
View File
@@ -6,6 +6,9 @@ use sammo\Session;
use DateTimeInterface; use DateTimeInterface;
use sammo\DB; use sammo\DB;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\Enums\GeneralLiteQueryMode;
use sammo\GeneralLite;
use sammo\StaticEventHandler;
use sammo\StringUtil; use sammo\StringUtil;
use sammo\Validator; use sammo\Validator;
@@ -32,15 +35,21 @@ class JoinTroop extends \sammo\BaseAPI
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType
{ {
$userID = $session->userID; $generalID = $session->generalID;
$troopID = $this->args['troopID']; $troopID = $this->args['troopID'];
$db = DB::db(); $db = DB::db();
$me = $db->queryFirstRow('SELECT `no`,nation,`troop`,`officer_level`,permission,penalty FROM general WHERE `owner`=%i', $userID);
if ($me['troop'] != 0) { $me = GeneralLite::createObjFromDB($generalID, ['troop'], GeneralLiteQueryMode::Lite);
if(!$me){
return '장수 정보를 불러올 수 없습니다.';
}
if($me->getVar('troop') != 0){
return '이미 부대에 소속되어 있습니다.'; return '이미 부대에 소속되어 있습니다.';
} }
$nationID = $me['nation'];
$nationID = $me->getNationID();
if($nationID == 0){ if($nationID == 0){
return '국가에 소속되어 있지 않습니다.'; return '국가에 소속되어 있지 않습니다.';
} }
@@ -49,11 +58,10 @@ class JoinTroop extends \sammo\BaseAPI
if (!$troopExists) { if (!$troopExists) {
return '부대가 올바르지 않습니다.'; return '부대가 올바르지 않습니다.';
} }
$generalID = $me['no'];
$db->update('general', [ $me->setVar('troop', $troopID);
'troop' => $troopID, StaticEventHandler::handleEvent($me, null, $this::class, [], $this->args);
], '`no` = %i AND `troop` = %i', $generalID, 0); $me->applyDB($db);
return null; return null;
} }
+9 -4
View File
@@ -6,6 +6,9 @@ use sammo\Session;
use DateTimeInterface; use DateTimeInterface;
use sammo\DB; use sammo\DB;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\Enums\GeneralLiteQueryMode;
use sammo\GeneralLite;
use sammo\StaticEventHandler;
use sammo\Validator; use sammo\Validator;
class KickFromTroop extends \sammo\BaseAPI class KickFromTroop extends \sammo\BaseAPI
@@ -37,7 +40,8 @@ class KickFromTroop extends \sammo\BaseAPI
$generalID = $this->args['generalID']; $generalID = $this->args['generalID'];
$db = DB::db(); $db = DB::db();
$troopID = $db->queryFirstField('SELECT troop FROM general WHERE no = %i', $generalID); $destGeneral = GeneralLite::createObjFromDB($generalID, ['troop'], GeneralLiteQueryMode::Lite);
$troopID = $destGeneral->getVar('troop');
if($troopID == 0){ if($troopID == 0){
return '부대에 소속되어 있지 않습니다.'; return '부대에 소속되어 있지 않습니다.';
} }
@@ -50,9 +54,10 @@ class KickFromTroop extends \sammo\BaseAPI
return '부대장을 추방할 수 없습니다.'; return '부대장을 추방할 수 없습니다.';
} }
$db->update('general', [ StaticEventHandler::handleEvent($destGeneral, null, $this::class, [], $this->args);
'troop' => 0, $destGeneral->setVar('troop', 0);
], '`no` = %i AND `troop` = %i', $generalID, $troopID); $destGeneral->applyDB($db);
return null; return null;
} }
} }
+11 -9
View File
@@ -6,6 +6,9 @@ use sammo\Session;
use DateTimeInterface; use DateTimeInterface;
use sammo\DB; use sammo\DB;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\Enums\GeneralLiteQueryMode;
use sammo\GeneralLite;
use sammo\StaticEventHandler;
use sammo\StringUtil; use sammo\StringUtil;
use sammo\Validator; use sammo\Validator;
@@ -32,24 +35,23 @@ class NewTroop extends \sammo\BaseAPI
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType
{ {
$userID = $session->userID; $generalID = $session->generalID;
$troopName = StringUtil::neutralize($this->args['troopName']); $troopName = StringUtil::neutralize($this->args['troopName']);
if(!$troopName){ if(!$troopName){
return '부대 이름이 없습니다.'; return '부대 이름이 없습니다.';
} }
$db = DB::db(); $db = DB::db();
$me = $db->queryFirstRow('SELECT `no`,nation,`troop`,`officer_level`,permission,penalty FROM general WHERE `owner`=%i', $userID); $me = GeneralLite::createObjFromDB($generalID, ['troop'], GeneralLiteQueryMode::Lite);
if($me['troop'] != 0){ $troopID = $me->getVar('troop');
if($troopID != 0){
return '이미 부대에 소속되어 있습니다.'; return '이미 부대에 소속되어 있습니다.';
} }
$nationID = $me['nation']; $nationID = $me->getNationID();
if($nationID == 0){ if($nationID == 0){
return '국가에 소속되어 있지 않습니다.'; return '국가에 소속되어 있지 않습니다.';
} }
$generalID = $me['no'];
$db->insert('troop', [ $db->insert('troop', [
'name'=>$troopName, 'name'=>$troopName,
'troop_leader'=>$generalID, 'troop_leader'=>$generalID,
@@ -60,9 +62,9 @@ class NewTroop extends \sammo\BaseAPI
return '부대가 생성되지 않았습니다. 버그일 수 있습니다.'; return '부대가 생성되지 않았습니다. 버그일 수 있습니다.';
} }
$db->update('general', [ $me->setVar('troop', $generalID);
'troop'=>$generalID StaticEventHandler::handleEvent($me, null, $this::class, [], $this->args);
], '`no` = %i', $generalID); $me->applyDB($db);
return null; return null;
} }
+8 -5
View File
@@ -6,6 +6,9 @@ use sammo\Session;
use DateTimeInterface; use DateTimeInterface;
use sammo\DB; use sammo\DB;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\Enums\GeneralLiteQueryMode;
use sammo\GeneralLite;
use sammo\StaticEventHandler;
use sammo\StringUtil; use sammo\StringUtil;
use sammo\Validator; use sammo\Validator;
@@ -36,13 +39,12 @@ class SetTroopName extends \sammo\BaseAPI
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType
{ {
$userID = $session->userID; $generalID = $session->generalID;
$db = DB::db(); $db = DB::db();
$me = $db->queryFirstRow('SELECT `no`,nation,`officer_level`,permission,penalty FROM general WHERE `owner`=%i', $userID); $me = GeneralLite::createObjFromDB($generalID, ['troop', 'permission', 'penalty'], GeneralLiteQueryMode::Lite);
$permission = checkSecretPermission($me, false); $permission = checkSecretPermission($me->getRaw(), false);
$troopID = $this->args['troopID']; $troopID = $this->args['troopID'];
$generalID = $me['no'];
if($generalID != $troopID && $permission < 4){ if($generalID != $troopID && $permission < 4){
return "권한이 부족합니다."; return "권한이 부족합니다.";
} }
@@ -52,7 +54,7 @@ class SetTroopName extends \sammo\BaseAPI
return '부대 이름이 없습니다.'; return '부대 이름이 없습니다.';
} }
$nationID = $me['nation']; $nationID = $me->getNationID();
$db->update('troop', [ $db->update('troop', [
'name'=>$troopName 'name'=>$troopName
], 'troop_leader=%i AND `nation`=%i',$troopID, $nationID); ], 'troop_leader=%i AND `nation`=%i',$troopID, $nationID);
@@ -60,6 +62,7 @@ class SetTroopName extends \sammo\BaseAPI
if($db->affectedRows() == 0){ if($db->affectedRows() == 0){
return '부대가 없습니다.'; return '부대가 없습니다.';
} }
StaticEventHandler::handleEvent($me, null, $this::class, [], $this->args);
return null; return null;
} }
+5
View File
@@ -7,6 +7,7 @@ use sammo\DB;
use sammo\DTO\VoteInfo; use sammo\DTO\VoteInfo;
use sammo\Enums\APIRecoveryType; use sammo\Enums\APIRecoveryType;
use sammo\KVStorage; use sammo\KVStorage;
use sammo\RootDB;
use sammo\Session; use sammo\Session;
use sammo\TimeUtil; use sammo\TimeUtil;
use sammo\Util; use sammo\Util;
@@ -93,6 +94,8 @@ class NewVote extends \sammo\BaseAPI
} }
} }
$userName = $session->userName;
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
@@ -108,9 +111,11 @@ class NewVote extends \sammo\BaseAPI
$multipleOptions = Util::valueFit($multipleOptions, 0, count($options)); $multipleOptions = Util::valueFit($multipleOptions, 0, count($options));
$voteInfo = new VoteInfo( $voteInfo = new VoteInfo(
id: $voteID, id: $voteID,
title: $title, title: $title,
opener: $userName ?? '[SYSTEM]',
multipleOptions: $multipleOptions, multipleOptions: $multipleOptions,
startDate: $now, startDate: $now,
endDate: $endDate, endDate: $endDate,
+6 -2
View File
@@ -8,6 +8,11 @@ use sammo\Util;
abstract class AbsFromUserPool extends AbsGeneralPool{ abstract class AbsFromUserPool extends AbsGeneralPool{
protected static function getCandidateWeight(array $info, int $owner): int|float
{
return array_sum($info['dex'] ?? []);
}
public function occupyGeneralName(): bool public function occupyGeneralName(): bool
{ {
$generalID = $this->getGeneralBuilder()->getGeneralID(); $generalID = $this->getGeneralBuilder()->getGeneralID();
@@ -36,8 +41,7 @@ abstract class AbsFromUserPool extends AbsGeneralPool{
$pool = []; $pool = [];
foreach($db->query('SELECT id, unique_name, info FROM select_pool WHERE reserved_until IS NULL AND general_id IS NULL', $pickCnt) as $cand){ foreach($db->query('SELECT id, unique_name, info FROM select_pool WHERE reserved_until IS NULL AND general_id IS NULL', $pickCnt) as $cand){
$cand['info'] = Json::decode($cand['info']); $cand['info'] = Json::decode($cand['info']);
$dexTotal = array_sum($cand['info']['dex']); $pool[] = [$cand, static::getCandidateWeight($cand['info'], $owner)];
$pool[] = [$cand, $dexTotal];
} }
if(count($pool) < $pickCnt){ if(count($pool) < $pickCnt){
@@ -19,10 +19,14 @@ class che_저지_삼황내문 extends \sammo\BaseItem{
if($unit->isAttacker()){ if($unit->isAttacker()){
return null; return null;
} }
if($unit->getPhase() >= 2){
if($unit->getPhase() > 0){
return null; return null;
} }
if($unit->getPhase() == 1 && $unit->rng->nextBool(0.5)){ if($unit->hasActivatedSkillOnLog('저지') >= 2){
return null;
}
if($unit->hasActivatedSkillOnLog('저지') == 1 && $unit->getPhase() == 0 && $unit->rng->nextBool(0.5)){
return null; return null;
} }
@@ -15,7 +15,7 @@ class che_척사_오악진형도 extends \sammo\BaseItem{
public function getWarPowerMultiplier(WarUnit $unit):array{ public function getWarPowerMultiplier(WarUnit $unit):array{
$opposeCrewType = $unit->getOppose()->getCrewType(); $opposeCrewType = $unit->getOppose()->getCrewType();
if($opposeCrewType->reqCities || $opposeCrewType->reqRegions){ if($opposeCrewType->reqCities() || $opposeCrewType->reqRegions()){
return [1.15, 0.85]; return [1.15, 0.85];
} }
return [1, 1]; return [1, 1];
@@ -33,7 +33,7 @@ class event_전투특기_공성 extends \sammo\BaseItem{
} }
public function onCalcStat(General $general, string $statName, $value, $aux=null){ public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){ if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_SIEGE; $myArmType = 'dex'.GameUnitConst::T_SIEGE;
$opposeArmType = 'dex'.$aux['opposeType']->armType;; $opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){ if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -28,7 +28,7 @@ class event_전투특기_궁병 extends \sammo\BaseItem{
if($statName === 'warAvoidRatio'){ if($statName === 'warAvoidRatio'){
return $value + 0.2; return $value + 0.2;
} }
if(\sammo\Util::starts_with($statName, 'dex')){ if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_ARCHER; $myArmType = 'dex'.GameUnitConst::T_ARCHER;
$opposeArmType = 'dex'.$aux['opposeType']->armType;; $opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){ if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -28,7 +28,7 @@ class event_전투특기_귀병 extends \sammo\BaseItem{
if($statName === 'warMagicSuccessProb'){ if($statName === 'warMagicSuccessProb'){
return $value + 0.2; return $value + 0.2;
} }
if(\sammo\Util::starts_with($statName, 'dex')){ if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_WIZARD; $myArmType = 'dex'.GameUnitConst::T_WIZARD;
$opposeArmType = 'dex'.$aux['opposeType']->armType;; $opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){ if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -32,7 +32,7 @@ class event_전투특기_기병 extends \sammo\BaseItem{
} }
public function onCalcStat(General $general, string $statName, $value, $aux=null){ public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){ if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_CAVALRY; $myArmType = 'dex'.GameUnitConst::T_CAVALRY;
$opposeArmType = 'dex'.$aux['opposeType']->armType;; $opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){ if($aux['isAttacker'] && $opposeArmType === $statName){

Some files were not shown because too many files have changed in this diff Show More