1
0
mirror of https://github.com/TheFunny/ArisuAutoSweeper synced 2025-12-16 19:55:12 +00:00
This commit is contained in:
RedDeadDepresso 2024-01-03 23:53:34 +00:00 committed by GitHub
commit 7a94c11144
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1483 additions and 141 deletions

View File

@ -134,7 +134,7 @@ class MCE_Manager(customtkinter.CTk):
self.template_labels.grid(row=0, column=0, sticky="ew")
self.mode_label = customtkinter.CTkLabel(self.template_labels, text="Mode:", font=customtkinter.CTkFont(underline=True))
self.mode_tooltip = CTkToolTip(self.mode_label, message="N:Mission Normal\nH:Mission Hard\nE:Event Quest\nBD:Commissions EXP\nIR:Commissions Credits\n")
self.mode_tooltip = CTkToolTip(self.mode_label, message="N : Mission Normal\nH : Mission Hard\nE : Event Quest\nXP : Commissions EXP\nCR : Commissions Credits\n", justify=tk.LEFT)
self.mode_label.grid(row=1, column=0, padx=(130, 0), pady=5)
self.stage_label = customtkinter.CTkLabel(self.template_labels, text="Stage:", font=customtkinter.CTkFont(underline=True))
@ -269,7 +269,7 @@ class MCE_Manager(customtkinter.CTk):
down_button = customtkinter.CTkButton(frame, text="Down", width=5, command=lambda f=frame, queue=queue: self.move_frame_down(f, queue), state=state)
down_button.grid(row=0, column=1, padx=5, pady=5, sticky="w")
# Dropdown menu for mode
mode_optionmenu = customtkinter.CTkOptionMenu(frame, width=60, values=["N", "H", "E", "BD", "IR"], state=state)
mode_optionmenu = customtkinter.CTkOptionMenu(frame, width=60, values=["N", "H", "E", "XP", "CR"], state=state)
mode_optionmenu.set(inner_list[0] if inner_list else "N")
mode_optionmenu.grid(row=0, column=2, padx=5, pady=5, sticky="w")
# Entry widget for stage

4
aas.py
View File

@ -62,6 +62,10 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
from tasks.mission.mission import Mission
Mission(config=self.config, device=self.device).run()
def schedule(self):
from tasks.schedule.schedule import Schedule
Schedule(config=self.config, device=self.device).run()
def data_update(self):
from tasks.item.data_update import DataUpdate
DataUpdate(config=self.config, device=self.device).run()

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -70,6 +70,37 @@
"Substitute": false
}
},
"Schedule": {
"Scheduler": {
"Enable": false,
"NextRun": "2020-01-01 00:00:00",
"Command": "Schedule",
"ServerUpdate": "04:00"
},
"Schedule": {
"OnError": "skip"
},
"Choice1": {
"Location": "None",
"Classrooms": null
},
"Choice2": {
"Location": "None",
"Classrooms": null
},
"Choice3": {
"Location": "None",
"Classrooms": null
},
"Choice4": {
"Location": "None",
"Classrooms": null
},
"Choice5": {
"Location": "None",
"Classrooms": null
}
},
"Shop": {
"Scheduler": {
"Enable": false,

21
licenses/BAAH-license Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-present Electron React Boilerplate
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Akash Bora
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

121
licenses/CTkToolTip-license Normal file
View File

@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

View File

@ -318,6 +318,158 @@
}
}
},
"Schedule": {
"Scheduler": {
"Enable": {
"type": "checkbox",
"value": false,
"option": [
true,
false
]
},
"NextRun": {
"type": "datetime",
"value": "2020-01-01 00:00:00",
"validate": "datetime"
},
"Command": {
"type": "input",
"value": "Schedule",
"display": "hide"
},
"ServerUpdate": {
"type": "input",
"value": "04:00",
"display": "hide"
}
},
"Schedule": {
"OnError": {
"type": "select",
"value": "skip",
"option": [
"stop",
"skip"
]
}
},
"Choice1": {
"Location": {
"type": "select",
"value": "None",
"option": [
"None",
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Classrooms": {
"type": "textarea",
"value": null
}
},
"Choice2": {
"Location": {
"type": "select",
"value": "None",
"option": [
"None",
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Classrooms": {
"type": "textarea",
"value": null
}
},
"Choice3": {
"Location": {
"type": "select",
"value": "None",
"option": [
"None",
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Classrooms": {
"type": "textarea",
"value": null
}
},
"Choice4": {
"Location": {
"type": "select",
"value": "None",
"option": [
"None",
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Classrooms": {
"type": "textarea",
"value": null
}
},
"Choice5": {
"Location": {
"type": "select",
"value": "None",
"option": [
"None",
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Classrooms": {
"type": "textarea",
"value": null
}
}
},
"Shop": {
"Scheduler": {
"Enable": {

View File

@ -93,6 +93,47 @@ Invitation:
type: textarea
Substitute: false
Schedule:
OnError:
value: skip
option: [ stop, skip ]
Choice1:
Location:
value: None
option: [ None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Classrooms:
value: null
type: textarea
Choice2:
Location:
value: None
option: [ None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Classrooms:
value: null
type: textarea
Choice3:
Location:
value: None
option: [ None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Classrooms:
value: null
type: textarea
Choice4:
Location:
value: None
option: [ None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Classrooms:
value: null
type: textarea
Choice5:
Location:
value: None
option: [ None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Classrooms:
value: null
type: textarea
Bounty:
OnError:
value: skip

View File

@ -13,6 +13,7 @@
"page": "setting",
"tasks": [
"Cafe",
"Schedule",
"Shop"
]
},

View File

@ -29,6 +29,14 @@ Daily:
- Scheduler
- Cafe
- Invitation
Schedule:
- Scheduler
- Schedule
- Choice1
- Choice2
- Choice3
- Choice4
- Choice5
Shop:
- Scheduler
- NormalShop

View File

@ -52,6 +52,29 @@ class GeneratedConfig:
Invitation_Name = None
Invitation_Substitute = False
# Group `Schedule`
Schedule_OnError = 'skip' # stop, skip
# Group `Choice1`
Choice1_Location = 'None' # None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Choice1_Classrooms = None
# Group `Choice2`
Choice2_Location = 'None' # None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Choice2_Classrooms = None
# Group `Choice3`
Choice3_Location = 'None' # None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Choice3_Classrooms = None
# Group `Choice4`
Choice4_Location = 'None' # None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Choice4_Classrooms = None
# Group `Choice5`
Choice5_Location = 'None' # None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Choice5_Classrooms = None
# Group `Bounty`
Bounty_OnError = 'skip' # stop, skip

View File

@ -9,7 +9,7 @@ class ManualConfig:
SCHEDULER_PRIORITY = """
Restart
> Cafe > TacticalChallenge > Circle > Mail
> DataUpdate > Bounty > Scrimmage > Task > Shop > Mission > Momotalk
> DataUpdate > Bounty > Scrimmage > Schedule > Task > Shop > Mission > Momotalk
"""
"""

View File

@ -34,6 +34,10 @@
"name": "Cafe",
"help": ""
},
"Schedule": {
"name": "Lesson",
"help": "AAS will execute Lesson starting from Choice 1 to Choice 5.\nIt will ignore any Choice that have Location set as None or the text area for classrooms is empty.\nIf any of the active Choices have incorrect input, it will perform the action set in Error handling."
},
"Shop": {
"name": "Shop",
"help": ""
@ -277,6 +281,143 @@
"help": "Whether to replace the existing student with their alt.\nIf not, try to match the next student"
}
},
"Schedule": {
"_info": {
"name": "Lesson Settings",
"help": ""
},
"OnError": {
"name": "Error Handling",
"help": "Perform the selected action when an error occurs (ticket not enough or any invalid setting)",
"stop": "Stop script",
"skip": "Skip current task"
}
},
"Choice1": {
"_info": {
"name": "Choice 1",
"help": ""
},
"Location": {
"name": "Location",
"help": "",
"None": "None",
"0": "Schale Office",
"1": "Schale Residence",
"2": "Gehenna",
"3": "Abydos",
"4": "Millennium",
"5": "Trinity",
"6": "Red Winter",
"7": "Hyakkiyako",
"8": "D.U. Shiratori",
"9": "Shanhaijing"
},
"Classrooms": {
"name": "Classrooms",
"help": "Type a number from 1 to 9 that represents the classroom position in the locations popup.\nUse > to connect multiple classrooms and AAS will select them following the order they appear. Example:\n8 > 7 > 6 > 5 > 4 > 3 > 2 > 1"
}
},
"Choice2": {
"_info": {
"name": "Choice 2",
"help": ""
},
"Location": {
"name": "Location",
"help": "",
"None": "None",
"0": "Schale Office",
"1": "Schale Residence",
"2": "Gehenna",
"3": "Abydos",
"4": "Millennium",
"5": "Trinity",
"6": "Red Winter",
"7": "Hyakkiyako",
"8": "D.U. Shiratori",
"9": "Shanhaijing"
},
"Classrooms": {
"name": "Classrooms",
"help": ""
}
},
"Choice3": {
"_info": {
"name": "Choice 3",
"help": ""
},
"Location": {
"name": "Location",
"help": "",
"None": "None",
"0": "Schale Office",
"1": "Schale Residence",
"2": "Gehenna",
"3": "Abydos",
"4": "Millennium",
"5": "Trinity",
"6": "Red Winter",
"7": "Hyakkiyako",
"8": "D.U. Shiratori",
"9": "Shanhaijing"
},
"Classrooms": {
"name": "Classrooms",
"help": ""
}
},
"Choice4": {
"_info": {
"name": "Choice 4",
"help": ""
},
"Location": {
"name": "Location",
"help": "",
"None": "None",
"0": "Schale Office",
"1": "Schale Residence",
"2": "Gehenna",
"3": "Abydos",
"4": "Millennium",
"5": "Trinity",
"6": "Red Winter",
"7": "Hyakkiyako",
"8": "D.U. Shiratori",
"9": "Shanhaijing"
},
"Classrooms": {
"name": "Classrooms",
"help": ""
}
},
"Choice5": {
"_info": {
"name": "Choice 5",
"help": ""
},
"Location": {
"name": "Location",
"help": "",
"None": "None",
"0": "Schale Office",
"1": "Schale Residence",
"2": "Gehenna",
"3": "Abydos",
"4": "Millennium",
"5": "Trinity",
"6": "Red Winter",
"7": "Hyakkiyako",
"8": "D.U. Shiratori",
"9": "Shanhaijing"
},
"Classrooms": {
"name": "Classrooms",
"help": ""
}
},
"Bounty": {
"_info": {
"name": "Bounty Settings",

View File

@ -34,6 +34,10 @@
"name": "咖啡厅",
"help": ""
},
"Schedule": {
"name": "Task.Schedule.name",
"help": "Task.Schedule.help"
},
"Shop": {
"name": "商店",
"help": ""
@ -277,6 +281,143 @@
"help": "若咖啡厅已存在所邀请学生的不同服装,选择是否替换该学生\n若不替换则尝试匹配下一位学生"
}
},
"Schedule": {
"_info": {
"name": "Schedule._info.name",
"help": "Schedule._info.help"
},
"OnError": {
"name": "Schedule.OnError.name",
"help": "Schedule.OnError.help",
"stop": "stop",
"skip": "skip"
}
},
"Choice1": {
"_info": {
"name": "Choice1._info.name",
"help": "Choice1._info.help"
},
"Location": {
"name": "Choice1.Location.name",
"help": "Choice1.Location.help",
"None": "None",
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9"
},
"Classrooms": {
"name": "Choice1.Classrooms.name",
"help": "Choice1.Classrooms.help"
}
},
"Choice2": {
"_info": {
"name": "Choice2._info.name",
"help": "Choice2._info.help"
},
"Location": {
"name": "Choice2.Location.name",
"help": "Choice2.Location.help",
"None": "None",
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9"
},
"Classrooms": {
"name": "Choice2.Classrooms.name",
"help": "Choice2.Classrooms.help"
}
},
"Choice3": {
"_info": {
"name": "Choice3._info.name",
"help": "Choice3._info.help"
},
"Location": {
"name": "Choice3.Location.name",
"help": "Choice3.Location.help",
"None": "None",
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9"
},
"Classrooms": {
"name": "Choice3.Classrooms.name",
"help": "Choice3.Classrooms.help"
}
},
"Choice4": {
"_info": {
"name": "Choice4._info.name",
"help": "Choice4._info.help"
},
"Location": {
"name": "Choice4.Location.name",
"help": "Choice4.Location.help",
"None": "None",
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9"
},
"Classrooms": {
"name": "Choice4.Classrooms.name",
"help": "Choice4.Classrooms.help"
}
},
"Choice5": {
"_info": {
"name": "Choice5._info.name",
"help": "Choice5._info.help"
},
"Location": {
"name": "Choice5.Location.name",
"help": "Choice5.Location.help",
"None": "None",
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9"
},
"Classrooms": {
"name": "Choice5.Classrooms.name",
"help": "Choice5.Classrooms.help"
}
},
"Bounty": {
"_info": {
"name": "悬赏通缉设置",

View File

@ -31,6 +31,9 @@ starlette==0.14.2
uvicorn[standard]==0.17.6
aiofiles
# GUI
customtkinter
# For dev
# pip-tools
pynput

View File

@ -19,6 +19,8 @@ cigam==0.0.3 # via apkutils2
click==8.1.3 # via uvicorn
colorama==0.4.6 # via click, logzero, tqdm, uvicorn
coloredlogs==15.0.1 # via onnxruntime
customtkinter==5.2.1 # via -r requirements-in.txt
darkdetect==0.8.0 # via customtkinter
decorator==5.1.1 # via retry
deprecated==1.2.13 # via uiautomator2
deprecation==2.1.0 # via adbutils

View File

@ -3,17 +3,6 @@ from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
CHECK_BD = ButtonWrapper(
name='CHECK_BD',
jp=None,
en=Button(
file='./assets/en/mission/CHECK_BD.png',
area=(94, 135, 325, 194),
search=(74, 115, 345, 214),
color=(208, 215, 220),
button=(94, 135, 325, 194),
),
)
CHECK_COMMISSIONS = ButtonWrapper(
name='CHECK_COMMISSIONS',
jp=None,
@ -25,11 +14,11 @@ CHECK_COMMISSIONS = ButtonWrapper(
button=(646, 78, 909, 135),
),
)
CHECK_IR = ButtonWrapper(
name='CHECK_IR',
CHECK_CR = ButtonWrapper(
name='CHECK_CR',
jp=None,
en=Button(
file='./assets/en/mission/CHECK_IR.png',
file='./assets/en/mission/CHECK_CR.png',
area=(97, 137, 340, 191),
search=(77, 117, 360, 211),
color=(213, 220, 223),
@ -47,6 +36,61 @@ CHECK_MISSION_SWEEP = ButtonWrapper(
button=(654, 184, 703, 209),
),
)
CHECK_XP = ButtonWrapper(
name='CHECK_XP',
jp=None,
en=Button(
file='./assets/en/mission/CHECK_XP.png',
area=(94, 135, 325, 194),
search=(74, 115, 345, 214),
color=(208, 215, 220),
button=(94, 135, 325, 194),
),
)
EVENT_INDEX = ButtonWrapper(
name='EVENT_INDEX',
jp=None,
en=Button(
file='./assets/en/mission/EVENT_INDEX.png',
area=(704, 135, 761, 694),
search=(684, 115, 781, 714),
color=(189, 197, 195),
button=(704, 135, 761, 694),
),
)
EVENT_ITEM = ButtonWrapper(
name='EVENT_ITEM',
jp=None,
en=Button(
file='./assets/en/mission/EVENT_ITEM.png',
area=(691, 136, 1198, 232),
search=(671, 116, 1218, 252),
color=(201, 219, 224),
button=(691, 136, 1198, 232),
),
)
EVENT_LIST = ButtonWrapper(
name='EVENT_LIST',
jp=None,
en=Button(
file='./assets/en/mission/EVENT_LIST.png',
area=(695, 137, 1196, 688),
search=(675, 117, 1216, 708),
color=(171, 185, 190),
button=(695, 137, 1196, 688),
),
)
EVENT_STARS = ButtonWrapper(
name='EVENT_STARS',
jp=None,
en=Button(
file='./assets/en/mission/EVENT_STARS.png',
area=(704, 186, 761, 218),
search=(684, 166, 781, 238),
color=(228, 223, 194),
button=(704, 186, 761, 218),
),
)
HARD_OFF = ButtonWrapper(
name='HARD_OFF',
jp=None,
@ -146,25 +190,25 @@ RIGHT = ButtonWrapper(
button=(1202, 311, 1280, 412),
),
)
SELECT_BD = ButtonWrapper(
name='SELECT_BD',
SELECT_CR = ButtonWrapper(
name='SELECT_CR',
jp=None,
en=Button(
file='./assets/en/mission/SELECT_BD.png',
area=(1016, 165, 1227, 211),
search=(996, 145, 1247, 231),
color=(205, 212, 220),
button=(1016, 165, 1227, 211),
),
)
SELECT_IR = ButtonWrapper(
name='SELECT_IR',
jp=None,
en=Button(
file='./assets/en/mission/SELECT_IR.png',
file='./assets/en/mission/SELECT_CR.png',
area=(1004, 267, 1237, 321),
search=(984, 247, 1257, 341),
color=(214, 220, 227),
button=(1004, 267, 1237, 321),
),
)
SELECT_XP = ButtonWrapper(
name='SELECT_XP',
jp=None,
en=Button(
file='./assets/en/mission/SELECT_XP.png',
area=(1016, 165, 1227, 211),
search=(996, 145, 1247, 231),
color=(205, 212, 220),
button=(1016, 165, 1227, 211),
),
)

View File

@ -3,7 +3,7 @@ from enum import Enum
from module.base.timer import Timer
from module.exception import RequestHumanTakeover
from module.logger import logger
from tasks.mission.ui import MissionUI, CommissionsUI
from tasks.mission.ui import MissionUI, CommissionsUI, SWITCH_QUEST
from tasks.stage.ap import AP
from tasks.cafe.cafe import Cafe
from tasks.circle.circle import Circle
@ -13,7 +13,7 @@ from tasks.item.data_update import DataUpdate
import json
import math
from filelock import FileLock
from datetime import datetime
from datetime import datetime, timedelta
class MissionStatus(Enum):
AP = 0 # Calculate AP and decide to terminate Mission module or not
@ -26,11 +26,19 @@ class MissionStatus(Enum):
class Mission(MissionUI, CommissionsUI):
_stage_ap = [10, 15, 15, 15]
@property
def stage_ap(self):
return self._stage_ap
match self.current_mode:
case "N":
return 10
case "H":
return 20
case "E":
stage = int(self.current_stage, base=10)
return 20 if stage >= 9 else 10 + 5 * math.floor(stage / 5)
case "XP" | "CR":
stage = int(self.current_stage, base=10)
return 40 if stage >= 9 else 5 + 15 * math.floor(stage / 4)
@property
def mission_info(self) -> list:
@ -45,8 +53,8 @@ class Mission(MissionUI, CommissionsUI):
"N" : Normal Mission
"H" : Hard Mission
"E" : Event Quest
"IR" : Item Retrieval / Commission where you get credit
"BD" : Base Defense / Commission where you get exp
"CR" : Item Retrieval / Commission where you get credit
"XP" : Base Defense / Commission where you get exp
Returns:
list of list
@ -70,7 +78,7 @@ class Mission(MissionUI, CommissionsUI):
logger.error("Failed to read configuration file")
finally:
return queue
def check_reset_daily(self):
# Check if it's time to reset the queue
if self.reset_daily:
@ -80,10 +88,21 @@ class Mission(MissionUI, CommissionsUI):
last_run_datetime = datetime.strptime(self.last_run, "%Y-%m-%d %H:%M:%S")
reset_time = datetime.strptime(self.reset_time, "%H:%M:%S").time()
if current_date != last_run_datetime.date() and current_time >= reset_time:
self.last_run = str(datetime.now().replace(microsecond=0))
logger.info("Reset Daily activated.")
# Check if the difference between the current date and last run date is 2 or greater days
if (current_date - last_run_datetime.date()).days >= 2:
# Set self.last_run to yesterday's date with time as reset_time
yesterday_datetime = current_datetime - timedelta(days=1)
yesterday_date = yesterday_datetime.date()
self.last_run = str(datetime.combine(yesterday_date, reset_time))
logger.info("Reset Daily activated")
return True
# Check if the current date is different from the last run date and the current time is greater than or equal to the reset time
elif current_date != last_run_datetime.date() and current_time >= reset_time:
self.last_run = str(datetime.now().replace(microsecond=0))
logger.info("Reset Daily activated")
return True
return False
@property
@ -122,10 +141,10 @@ class Mission(MissionUI, CommissionsUI):
"""
if self.current_mode in ["N", "H"]:
return self.select_mission(self.current_mode, self.current_stage)
elif self.current_mode in ["BD", "IR"]:
elif self.current_mode in ["CR", "XP"]:
return self.select_commission(self.current_mode)
elif self.current_mode == "E":
return self.select_event()
return self.select_mode(SWITCH_QUEST)
else:
logger.error("Uknown mode")
return False
@ -134,9 +153,8 @@ class Mission(MissionUI, CommissionsUI):
"""
Calculate the possible number of sweeps based on the current AP
"""
ap_cost = 20 if self.current_mode == "H" else 10
required_ap = ap_cost * self.current_count
return math.floor(min(required_ap, self.current_ap) / ap_cost)
possible_count = math.floor(self.current_ap / self.stage_ap)
return min(possible_count, self.current_count)
def update_task(self, failure=False):
"""
@ -161,10 +179,9 @@ class Mission(MissionUI, CommissionsUI):
self.task = []
def update_ap(self):
ap_cost = 20 if self.current_mode == "H" else 10
ap = self.config.stored.AP
ap_old = ap.value
ap_new = ap_old - ap_cost * self.realistic_count
ap_new = ap_old - self.stage_ap * self.realistic_count
ap.set(ap_new, ap.total)
logger.info(f'Set AP: {ap_old} -> {ap_new}')
@ -208,7 +225,7 @@ class Mission(MissionUI, CommissionsUI):
self.update_task(failure=True)
return MissionStatus.AP
case MissionStatus.ENTER:
if self.enter_stage(self.current_stage):
if self.enter_stage(self.current_mode, self.current_stage):
return MissionStatus.SWEEP
self.update_task(failure=True)
return MissionStatus.AP
@ -235,24 +252,26 @@ class Mission(MissionUI, CommissionsUI):
with self.lock.acquire():
self.previous_mode = None
self.task = self.valid_task
action_timer = Timer(0.5, 1)
status = MissionStatus.AP
if self.task:
action_timer = Timer(0.5, 1)
status = MissionStatus.AP
"""Update the dashboard to accurately calculate AP"""
DataUpdate(config=self.config, device=self.device).run()
while 1:
self.device.screenshot()
if self.ui_additional():
continue
if action_timer.reached_and_reset():
logger.attr('Status', status)
status = self.handle_mission(status)
if status == MissionStatus.FINISH:
break
"""Update the dashboard to accurately calculate AP"""
DataUpdate(config=self.config, device=self.device).run()
while 1:
self.device.screenshot()
if self.ui_additional():
continue
if action_timer.reached_and_reset():
logger.attr('Status', status)
status = self.handle_mission(status)
if status == MissionStatus.FINISH:
break
self.config.task_delay(server_update=True)
# delay mission to 7 hours if there are still stages in the queue
self.config.task_delay(minute=420) if self.task else self.config.task_delay(server_update=True)

View File

@ -2,19 +2,19 @@ from module.base.timer import Timer
from module.logger import logger
from module.ui.switch import Switch
from module.ocr.ocr import Digit
from tasks.base.assets.assets_base_page import BACK, MISSION_CHECK, EVENT_CHECK
from tasks.base.page import page_mission, page_commissions #,page_event
from tasks.base.assets.assets_base_page import BACK, MISSION_CHECK, EVENT_CHECK, WORK_GO_TO_EVENT
from tasks.base.page import page_mission, page_commissions, page_work #,page_event
from tasks.base.ui import UI
from tasks.mission.assets.assets_mission import *
from tasks.stage.ap import AP
from tasks.stage.mission_list import StageList
from tasks.stage.sweep import StageSweep
SHARED_LIST = StageList('SharedList')
MISSION_SWEEP = StageSweep('MissionSweep', 60)
MISSION_SWEEP.set_button(button_check=CHECK_MISSION_SWEEP) # Check sweep is different for mission
SHARED_SWEEP = StageSweep('SharedSweep', 60)
EVENT_LIST = StageList('EventList', EVENT_LIST, EVENT_INDEX, EVENT_ITEM, button_stars=EVENT_STARS)
SHARED_SWEEP = StageSweep('MissionSweep', 99)
SHARED_SWEEP.set_button(button_check=CHECK_MISSION_SWEEP) # Check sweep is different for mission, event
COMMISSIONS_SWEEP = StageSweep('SharedSweep', 99)
SWITCH_NORMAL = Switch("Normal_switch")
SWITCH_NORMAL.add_state("on", NORMAL_ON)
@ -36,9 +36,9 @@ Missing for "E" because there are no event in Global and no page_event
MODE_TO_PAGE = {
"N": (MISSION_CHECK, page_mission),
"H": (MISSION_CHECK, page_mission),
"BD": (CHECK_BD, page_commissions),
"IR": (CHECK_IR, page_commissions),
"E" : (EVENT_CHECK) #page_event
"XP": (CHECK_XP, page_commissions),
"CR": (CHECK_CR, page_commissions),
"E" : (EVENT_CHECK, None) #page_event
}
@ -94,20 +94,16 @@ class MissionUI(UI, AP):
return False
switch.set('on', main=self)
return True
def select_event(self):
return self.select_mode(SWITCH_QUEST)
def enter_stage(self, index: int) -> bool:
if not index:
index = SHARED_LIST.insight_max_sweepable_index(self)
if SHARED_LIST.select_index_enter(self, index):
def enter_stage(self, mode, index: int) -> bool:
list = EVENT_LIST if mode == "E" else SHARED_LIST
if list.select_index_enter(self, index):
return True
return False
def do_sweep(self, mode, num: int) -> bool:
if mode in ["N", "H", "E"]:
return MISSION_SWEEP.do_sweep(self, num=num)
if mode in ["XP", "CR"]:
return COMMISSIONS_SWEEP.do_sweep(self, num=num)
else:
return SHARED_SWEEP.do_sweep(self, num=num)
@ -120,10 +116,10 @@ class MissionUI(UI, AP):
"""
if prev==next or (prev in ["N", "H"] and next in ["N", "H"]):
self.go_back(MODE_TO_PAGE[next][0])
elif prev in ["BD", "IR"] and next in ["BD", "IR"]:
elif prev in ["XP", "CR"] and next in ["XP", "CR"]:
self.go_back(CHECK_COMMISSIONS)
else:
self.ui_ensure(MODE_TO_PAGE[next][1])
self.goto_event() if next == "E" else self.ui_ensure(MODE_TO_PAGE[next][1])
def go_back(self, check):
while 1:
@ -132,12 +128,27 @@ class MissionUI(UI, AP):
return True
self.click_with_interval(BACK, interval=2)
def goto_event(self):
"""
Should be removed after implementing ui_ensure(page_event)
"""
self.ui_ensure(page_work)
timer = Timer(1).start()
while 1:
self.device.screenshot()
if self.appear(EVENT_CHECK):
break
self.appear_then_click(WORK_GO_TO_EVENT)
self.device.swipe((40,160), (260, 40))
while not timer.reached_and_reset():
pass
class CommissionsUI(UI, AP):
"""Works the same way as select_bounty"""
def select_commission(self, mode):
to_button = {
"IR": (SELECT_IR, CHECK_IR),
"BD": (SELECT_BD, CHECK_BD)
"CR": (SELECT_CR, CHECK_CR),
"XP": (SELECT_XP, CHECK_XP)
}
dest_enter, dest_check = to_button[mode]
timer = Timer(5, 10).start()

View File

@ -14,6 +14,17 @@ BEGIN_STORY = ButtonWrapper(
button=(796, 540, 1059, 591),
),
)
CHATTING = ButtonWrapper(
name='CHATTING',
jp=None,
en=Button(
file='./assets/en/momotalk/CHATTING.png',
area=(774, 563, 821, 585),
search=(754, 543, 841, 605),
color=(89, 102, 121),
button=(774, 563, 821, 585),
),
)
CHAT_AREA = ButtonWrapper(
name='CHAT_AREA',
jp=None,

View File

@ -33,7 +33,7 @@ class MomoTalk(MomoTalkUI):
return MomoTalkStatus.OPEN
case MomoTalkStatus.STORY:
if self.skip_story():
return MomoTalkStatus.OPEN
return MomoTalkStatus.CHAT
case MomoTalkStatus.FINISHED:
return status
case _:

View File

@ -26,13 +26,14 @@ SWITCH_SORT.add_state("descending", SORT_DESCENDING)
button can be found in different locations"""
REPLY_TEMPLATE = REPLY.matched_button.image
STORY_TEMPLATE = STORY.matched_button.image
CHATTING_TEMPLATE = CHATTING.matched_button.image
class MomoTalkUI(UI):
def __init__(self, config, device):
super().__init__(config, device)
self.swipe_vector_range = (0.65, 0.85)
self.list = CHAT_AREA
self.click_coords = self.device.click_methods.get(self.config.Emulator_ControlMethod, self.device.click_adb)
def swipe_page(self, direction: str, main: ModuleBase, vector_range=None, reverse=False):
"""
@ -58,28 +59,17 @@ class MomoTalkUI(UI):
vector = (-vector[0], -vector[1])
main.device.swipe_vector(vector, self.list.button)
def select_then_check(self, dest_enter: ButtonWrapper, dest_check: ButtonWrapper, similarity=0.85):
def select_then_disappear(self, dest_enter: ButtonWrapper, dest_check: ButtonWrapper):
timer = Timer(5, 10).start()
while 1:
self.device.screenshot()
self.appear_then_click(dest_enter, interval=1, similarity=similarity)
if self.appear(dest_check, similarity=similarity):
return True
if timer.reached():
return False
def select_then_disappear(self, dest_enter: ButtonWrapper, dest_check: ButtonWrapper, force_select=False):
timer = Timer(5, 10).start()
while 1:
self.device.screenshot()
if force_select or self.appear(dest_enter):
self.click_with_interval(dest_enter, interval=1)
self.click_with_interval(dest_enter, interval=1)
if not self.appear(dest_check):
return True
if timer.reached():
return False
def set_switch(self, switch):
def set_switch(self, switch, state='on'):
"""
Set switch to on. However, unsure why is inaccurate in momotalk.
Returns:
@ -88,17 +78,15 @@ class MomoTalkUI(UI):
if not switch.appear(main=self):
logger.info(f'{switch.name} not found')
return False
switch.set('on', main=self)
switch.set(state, main=self)
return True
def click_all(self, template, x_add=0, y_add=0):
"""
Find the all the locations of the template adding an offset if specified and click them.
TODO: filter coords that are not inside the chat area as otherwise it will close momotalk.
If after filter, no coords then swipe.
"""
click_coords = self.device.click_methods.get(self.config.Emulator_ControlMethod, self.device.click_adb)
image = self.device.screenshot()
result = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
threshold = 0.8
@ -108,9 +96,10 @@ class MomoTalkUI(UI):
center_pt = (int(pt[0] + template.shape[1] / 2 + x_add), int(pt[1] + template.shape[0] / 2 + y_add))
seen.add(center_pt)
if seen:
seen = filter(lambda x: point_in_area(x, CHAT_AREA.area), seen)
[click_coords(coords[0], coords[1]) for coords in seen]
self.swipe_page("down", self)
if y_add != 0:
seen = filter(lambda x: point_in_area(x, CHAT_AREA.area), seen)
[self.click_coords(coords[0], coords[1]) for coords in seen]
self.swipe_page("down", self)
return True
return False
@ -132,18 +121,22 @@ class MomoTalkUI(UI):
"""
Switch from newest to unread and sort the messages in descending order
"""
logger.info("Sorting messages...")
steps = [UNREAD, CONFIRM_SORT, UNREAD_OFF, UNREAD_ON]
for i in range(len(steps)-2):
self.select_then_check(steps[i], steps[i+1], similarity=0.95)
return not self.appear(CONFIRM_SORT) and self.appear(UNREAD) and self.appear(SORT_ON)
while 1:
self.device.screenshot()
if self.set_switch(SWITCH_UNREAD):
self.click_with_interval(CONFIRM_SORT, interval=2)
continue
if self.appear(UNREAD, similarity=0.95):
break
self.click_with_interval(UNREAD, interval=2)
return self.set_switch(SWITCH_SORT, "descending")
def check_first_student(self):
"""
If the first student has a red notification return True and start chat.
Otherwise it means no students are available for interaction.
"""
if self.match_color(FIRST_UNREAD, threshold=80) and self.select_then_disappear(FIRST_UNREAD, SELECT_STUDENT, force_select=True):
if self.match_color(FIRST_UNREAD, threshold=80) and self.select_then_disappear(FIRST_UNREAD, SELECT_STUDENT):
return True
logger.warning("No students available for interaction")
return False
@ -154,24 +147,23 @@ class MomoTalkUI(UI):
check if a reply or story button is found and click them.
If the begin story button is found skip story.
"""
timer = Timer(8, 5).start()
logger.info("Chatting with student...")
stability_counter = 0
while 1:
self.wait_until_stable(CHAT_AREA, timer=Timer(10, 10))
self.device.screenshot()
if self.appear(BEGIN_STORY):
logger.info("Begin Story detected")
return True
if self.click_all(REPLY_TEMPLATE, y_add=62):
elif self.click_all(CHATTING_TEMPLATE):
timer.reset()
elif self.click_all(REPLY_TEMPLATE, y_add=62):
logger.info("Clicked on reply")
stability_counter = 0
continue
if self.click_all(STORY_TEMPLATE, y_add=62):
timer.reset()
elif self.click_all(STORY_TEMPLATE, y_add=62):
logger.info("Clicked on story")
stability_counter = 0
continue
logger.info("No new message detected")
stability_counter += 1
if stability_counter > 3:
timer.reset()
elif timer.reached():
logger.info("No new message detected")
return False
def skip_story(self):
@ -180,11 +172,17 @@ class MomoTalkUI(UI):
button is clicked and disappears
"""
logger.info("Attempting to skip story...")
steps = [BEGIN_STORY, MENU, SKIP]
for step in steps:
self.appear_then_click(step)
if self.appear_then_click(CONFIRM_SKIP) and not self.appear(CONFIRM_SKIP, interval=5):
logger.info("Skipped story successfully")
return True
return False
steps = [CONFIRM_SKIP, SKIP, MENU, BEGIN_STORY]
timer = Timer(1).start()
while 1:
self.device.screenshot()
if self.handle_reward():
logger.info("Skipped story successfully")
return True
for step in steps:
if self.appear_then_click(step):
while not timer.reached_and_reset():
pass
break

View File

@ -3,6 +3,61 @@ from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
CONFIRM = ButtonWrapper(
name='CONFIRM',
jp=None,
en=Button(
file='./assets/en/schedule/CONFIRM.png',
area=(532, 528, 748, 589),
search=(512, 508, 768, 609),
color=(110, 207, 241),
button=(532, 528, 748, 589),
),
)
FIRST_ITEM = ButtonWrapper(
name='FIRST_ITEM',
jp=None,
en=Button(
file='./assets/en/schedule/FIRST_ITEM.png',
area=(727, 137, 1103, 239),
search=(707, 117, 1123, 259),
color=(200, 209, 220),
button=(727, 137, 1103, 239),
),
)
LOCATIONS = ButtonWrapper(
name='LOCATIONS',
jp=None,
en=Button(
file='./assets/en/schedule/LOCATIONS.png',
area=(1075, 638, 1256, 693),
search=(1055, 618, 1276, 713),
color=(107, 202, 237),
button=(1075, 638, 1256, 693),
),
)
LOCATIONS_POPUP = ButtonWrapper(
name='LOCATIONS_POPUP',
jp=None,
en=Button(
file='./assets/en/schedule/LOCATIONS_POPUP.png',
area=(534, 101, 750, 135),
search=(514, 81, 770, 155),
color=(194, 202, 210),
button=(534, 101, 750, 135),
),
)
OCR_TICKET = ButtonWrapper(
name='OCR_TICKET',
jp=None,
en=Button(
file='./assets/en/schedule/OCR_TICKET.png',
area=(220, 79, 266, 121),
search=(200, 59, 286, 141),
color=(214, 225, 229),
button=(220, 79, 266, 121),
),
)
SCROLL = ButtonWrapper(
name='SCROLL',
jp=Button(
@ -20,3 +75,14 @@ SCROLL = ButtonWrapper(
button=(727, 137, 1103, 671),
),
)
START_LESSON = ButtonWrapper(
name='START_LESSON',
jp=None,
en=Button(
file='./assets/en/schedule/START_LESSON.png',
area=(506, 523, 773, 585),
search=(486, 503, 793, 605),
color=(110, 205, 239),
button=(506, 523, 773, 585),
),
)

133
tasks/schedule/schedule.py Normal file
View File

@ -0,0 +1,133 @@
from enum import Flag
from module.base.timer import Timer
from module.exception import RequestHumanTakeover
from module.logger import logger
from tasks.base.assets.assets_base_page import BACK
from tasks.base.page import page_schedule
from tasks.schedule.ui import ScheduleUI
from tasks.base.assets.assets_base_page import SCHEDULE_CHECK
import re
class ScheduleStatus(Flag):
OCR = 0
ENTER = 1
SELECT = 2
END = 3
FINISH = 4
class Schedule(ScheduleUI):
@property
def schedule_info(self):
info = []
input_valid = True
schedule_config = self.config.cross_get("Schedule")
choices = ["Choice1", "Choice2", "Choice3", "Choice4", "Choice5"]
for choice in choices:
location, classrooms = schedule_config[choice]["Location"], schedule_config[choice]["Classrooms"]
if location == "None" or not classrooms or (isinstance(classrooms, str) and classrooms.replace(" ", "") == ""):
continue
elif isinstance(classrooms, int):
classrooms_list = [str(classrooms)]
else:
classrooms = classrooms.strip()
classrooms = re.sub(r'[ \t\r\n]', '', classrooms)
classrooms = (re.sub(r'[>﹥›˃ᐳ❯]', '>', classrooms)).split('>')
classrooms_list = []
# tried to convert to set to remove duplicates but doesn't maintain order
[classrooms_list.append(x) for x in classrooms if x not in classrooms_list]
if self.valid_classroom(classrooms_list):
info.append([location, classrooms_list])
else:
logger.error(f"Failed to read {choice}")
input_valid = False
return info if input_valid else []
def valid_classroom(self, classrooms_list):
if not classrooms_list:
return False
for classroom in classrooms_list:
if not classroom.isdigit():
return False
if not 1 <= int(classroom) <= 9:
return False
return True
@property
def valid_task(self) -> list:
task = self.schedule_info
if not task:
logger.warning('Lessons enabled but no task set')
self.error_handler()
return task
def error_handler(self):
action = self.config.Schedule_OnError
if action == 'stop':
raise RequestHumanTakeover
elif action == 'skip':
with self.config.multi_set():
self.config.task_delay(server_update=True)
self.config.task_stop()
@property
def current_location(self):
return self.task[0][0]
@property
def current_classrooms(self):
return self.task[0][1]
def handle_schedule(self, status):
match status:
case ScheduleStatus.OCR:
if self.task:
self.ticket = self.get_ticket()
if self.ticket not in [0, None]:
return ScheduleStatus.ENTER
return ScheduleStatus.FINISH
case ScheduleStatus.ENTER:
if self.enter_location(self.current_location):
return ScheduleStatus.SELECT
else:
self.error_handler()
case ScheduleStatus.SELECT:
if self.select_classrooms(self.ticket, self.current_classrooms):
self.task.pop(0)
return ScheduleStatus.END
return ScheduleStatus.FINISH
case ScheduleStatus.END:
if self.appear(SCHEDULE_CHECK):
return ScheduleStatus.OCR
self.click_with_interval(BACK, interval=2)
case ScheduleStatus.FINISH:
return status
case _:
logger.warning(f'Invalid status: {status}')
return status
def run(self):
self.ui_ensure(page_schedule)
self.task = self.valid_task
action_timer = Timer(0.5, 1)
status = ScheduleStatus.OCR
while 1:
self.device.screenshot()
if self.ui_additional():
continue
if action_timer.reached_and_reset():
logger.attr('Status', status)
status = self.handle_schedule(status)
if status == ScheduleStatus.FINISH:
break
self.config.task_delay(server_update=True)

View File

@ -0,0 +1,149 @@
"""
Original Author: sanmusen214(https://github.com/sanmusen214)
Adapted from https://github.com/sanmusen214/BAAH/blob/1.2/modules/AllTask/SubTask/ScrollSelect.py
"""
from module.logger import logger
from module.base.timer import Timer
class ScrollSelect:
"""
Scroll and select the corresponding level by clicking on the right-side window.
Parameters
----------
targetind : int
Index of the target level
window_starty:
Y-coordinate of the upper edge of the window
first_item_endy:
Y-coordinate of the lower edge of the first item
window_endy:
Y-coordinate of the lower edge of the window
clickx: int
Base X-coordinate for sliding and clicking the button
hasexpectimage: function
Function to determine the appearance of the expected image after clicking, returns a boolean
swipeoffsetx: int
X offset of the base X-coordinate during sliding to prevent accidental button clicks
finalclick: bool
Whether to click on clickx and the last row after the sliding ends
"""
def __init__(self, window_button, first_item_button, expected_button, clickx, swipeoffsetx=-100, finalclick=True) -> None:
# TODO: Actually, only concerned about the height of one element, completely displaying the Y of the first button, completely displaying the Y of the bottom button, the number of complete elements that the window can contain, the height of the last element in the window, and the left offset and response distance.
self.window_starty = window_button.area[1]
self.window_endy = window_button.area[3]
self.first_item_endy = first_item_button.area[3]
self.windowheight = window_button.height
self.itemheight = first_item_button.height
self.clickx = clickx
self.expected_button = expected_button
self.swipeoffsetx = swipeoffsetx
self.responsey = 40
self.finalclick = finalclick
def compute_swipe(self, main, x1, y1, distance, responsey):
"""
Swipe vertically from bottom to top, actual swipe distance calculated based on the distance between two target points, considering inertia.
"""
distance = abs(distance)
logger.info(f"Swipe distance: {distance}")
# 0-50
if distance < 50:
main.device.swipe((x1, y1), (x1, y1 - (distance + responsey)), duration=2)
else:
# Effective swipe distance for the Chinese server is 60
main.device.swipe((x1, y1), (x1, int(y1 - (distance + responsey - 4 * (1 + distance / 100)))), duration=1 + distance / 100)
def select_location(self, main, target_index) -> None:
click_coords = main.device.click_methods.get(main.config.Emulator_ControlMethod, main.device.click_adb)
logger.info("Scroll and select the {}-th level".format(target_index + 1))
self.scroll_right_up(main, scrollx=self.clickx + self.swipeoffsetx)
# Calculate how many complete elements are on one page
itemcount = self.windowheight // self.itemheight
# Calculate how much height the last incomplete element on this page occupies
lastitemheight = self.windowheight % self.itemheight
# Height below the incomplete element
hiddenlastitemheight = self.itemheight - lastitemheight
# Center point of the height of the first element
start_center_y = self.window_starty + self.itemheight // 2
# Center point of the last complete element on this page
end_center_y = start_center_y + (itemcount - 1) * self.itemheight
# If the target element is on the current page
if target_index < itemcount:
# Center point of the target element
target_center_y = start_center_y + self.itemheight * target_index
self.run_until(main,
lambda: click_coords(self.clickx, target_center_y),
lambda: main.appear(self.expected_button),
)
else:
# Start scrolling from the gap in the middle of the levels
scroll_start_from_y = self.window_endy - self.itemheight // 2
# The target element is on subsequent pages
# Calculate how much the page should be scrolled
scrolltotal_distance = (target_index - itemcount) * self.itemheight + hiddenlastitemheight
logger.info("Height hidden by the last element: %d" % hiddenlastitemheight)
# First, slide up the hidden part, add a little distance to let the system recognize it as a swipe event
self.compute_swipe(main, self.clickx + self.swipeoffsetx, scroll_start_from_y, hiddenlastitemheight, self.responsey)
logger.info(f"Swipe distance: {hiddenlastitemheight}")
# Update scrolltotal_distance
scrolltotal_distance -= hiddenlastitemheight
# Still need to scroll up (target_index - itemcount) * self.itemheight
# Important: slide the height of (itemcount - 1) elements each time
if itemcount == 1:
scroll_distance = itemcount * self.itemheight
else:
scroll_distance = (itemcount - 1) * self.itemheight
while scroll_distance <= scrolltotal_distance:
self.compute_swipe(main, self.clickx + self.swipeoffsetx, scroll_start_from_y, scroll_distance, self.responsey)
scrolltotal_distance -= scroll_distance
if scrolltotal_distance > 5:
# Last slide
self.compute_swipe(main, self.clickx + self.swipeoffsetx, scroll_start_from_y, scrolltotal_distance, self.responsey)
if self.finalclick:
# Click on the last row
self.run_until(main,
lambda: click_coords(self.clickx, self.window_endy - self.itemheight // 2),
lambda: main.appear(self.expected_button)
)
def run_until(self, main, func1, func2, times=6, sleeptime=1.5) -> bool:
"""
Repeat the execution of func1 up to a maximum of times or until func2 evaluates to True.
func1 should perform a single valid operation or internally call a screenshot function.
A screenshot is triggered before evaluating func2.
After each execution of func1, wait for sleeptime seconds.
If func2 evaluates to True, exit and return True. Otherwise, return False.
Note: The comment assumes that func1 produces a meaningful operation or internally calls a screenshot function,
and func2 is evaluated after each execution of func1.
"""
for i in range(times):
main.device.screenshot()
if func2():
return True
func1()
timer = Timer(sleeptime).start()
while not timer.reached_and_reset():
pass
main.device.screenshot()
if func2():
return True
logger.warning("run_until exceeded max times")
return False
def scroll_right_up(self, main, scrollx=928, times=3):
"""
scroll to top
"""
for i in range(times):
main.device.swipe((scrollx, 226), (scrollx, 561), duration=0.2)
timer = Timer(0.5).start()
while not timer.reached_and_reset():
pass

80
tasks/schedule/ui.py Normal file
View File

@ -0,0 +1,80 @@
from module.base.timer import Timer
from module.logger import logger
from module.ocr.ocr import DigitCounter
from tasks.base.ui import UI
from tasks.base.assets.assets_base_page import SCHEDULE_CHECK
from tasks.schedule.assets.assets_schedule import *
from tasks.schedule.scroll_select import ScrollSelect
import numpy as np
SCROLL_SELECT = ScrollSelect(window_button=SCROLL, first_item_button=FIRST_ITEM, expected_button=LOCATIONS, clickx=1116)
xs = np.linspace(299, 995, 3, dtype=int)
ys = np.linspace(268, 573, 3, dtype=int)
class ScheduleUI(UI):
def select_then_check(self, dest_enter: ButtonWrapper, dest_check: ButtonWrapper):
timer = Timer(8, 10).start()
while 1:
self.device.screenshot()
self.appear_then_click(dest_enter, interval=1)
self.handle_affection_level_up()
if self.appear(dest_check):
return True
if timer.reached():
return False
def click_then_check(self, coords, dest_check: ButtonWrapper):
click_coords = self.device.click_methods.get(self.config.Emulator_ControlMethod, self.device.click_adb)
timer = Timer(3, 5).start()
wait = Timer(1).start()
while 1:
click_coords(*coords)
self.device.screenshot()
if self.appear_then_click(dest_check):
return True
while not wait.reached_and_reset():
pass
if timer.reached():
return False
def enter_location(self, location):
SCROLL_SELECT.select_location(self, location)
if not self.appear(LOCATIONS):
logger.error("Unable to navigate to page for location {}".format(location + 1))
return False
return self.select_then_check(LOCATIONS, LOCATIONS_POPUP)
def select_classrooms(self, ticket, classrooms):
for classroom in classrooms:
if ticket == 0:
return False
classroom = int(classroom) - 1
col = int(classroom % len(xs))
row = int((classroom - col) / len(ys))
targetloc = (xs[col], ys[row])
if not self.click_then_check(targetloc, START_LESSON):
logger.info(f"Classroom {classroom + 1} does not exist or has already been clicked")
continue
if self.select_then_check(START_LESSON, CONFIRM):
ticket -= 1
if not self.select_then_check(CONFIRM, LOCATIONS_POPUP):
break
return True
def get_ticket(self):
"""
Page:
in: page_bounty
"""
if not self.appear(SCHEDULE_CHECK):
logger.warning('OCR failed due to invalid page')
return False
ticket, _, total = DigitCounter(OCR_TICKET).ocr_single_line(self.device.image)
if total == 0:
logger.warning('Invalid ticket')
return False
logger.attr('ScheduleTicket', ticket)
#self.config.stored.BountyTicket.set(ticket)
return ticket

View File

@ -75,7 +75,7 @@ class Shop(ShopUI):
self.select_items(self.current_item_list)
return ShopStatus.PURCHASE
case ShopStatus.PURCHASE:
if self.make_purchase():
if self.make_purchase() and self.current_purchase_count > 1:
return ShopStatus.REFRESH
return ShopStatus.END
case ShopStatus.REFRESH:

View File

@ -97,10 +97,10 @@ class ShopUI(UI):
one at 8 and the other at 16.
Only once for each checkpoint.
"""
if (8 < item < 16) and not self.swipe_flags[8]:
if (9 <= item <= 16) and not self.swipe_flags[8]:
self.swipe_flags[8] = True
return True
elif item > 16 and not self.swipe_flags[16]:
elif item >= 17 and not self.swipe_flags[16]:
self.swipe_flags[16] = True
return True
return False

View File

@ -25,7 +25,7 @@ class StageList:
):
self.name = name
self.stage = button_list if button_list else STAGE_LIST
self.index_ocr = Ocr(button_index if button_index else OCR_INDEX, lang='en')
self.index_ocr = Ocr(button_index if button_index else OCR_INDEX, lang='zhs')
self.stage_item = (button_item if button_item else STAGE_ITEM).button
self.enter = button_enter if button_enter else STAGE_ENTER
self.sweepable = button_stars if button_stars else STAGE_STARS